Des triplets qu'on aligne aux ontologies qui raisonnent.
OWL, classes, propriétés, restrictions — ou comment donner du sens aux sens.
Dans le cours #2, on a vu que la connaissance pouvait s'écrire comme une liste de triplets — des faits bruts. Mais un fait tout seul, c'est comme un mot tout seul : il n'a pas de type.
« Socrate » — qu'est-ce que c'est ? Un humain ? Un philosophe ? Une marque de lessive ? En l'absence de typage, c'est juste une chaîne de caractères. Le web sémantique résout ça avec OWL (Web Ontology Language) : un langage pour décrire ce que sont les choses, pas seulement ce qu'on dit d'elles.
En OWL, tout commence par des classes. Une classe,
c'est un ensemble d'individus qui partagent des caractéristiques.
Humain, Animal, Pizza, Fromage...
Les classes s'organisent en hiérarchie :
Chien ⊑ Animal ⊑ ÊtreVivant.
(Le symbole ⊑ se lit « est sous-classe de ».)
Python class Concept: def __init__(self, nom): self.nom = nom self.parents = [] # super-classes self.disjoints = [] # classes disjointes self.equivalent_a = None self.restrictions = [] # contraintes OWL def sub_class_of(self, *parents): self.parents.extend(parents) return self def disjoint_with(self, *concepts): self.disjoints.extend(concepts) return self # Exemple top = Concept("ÊtreVivant") humain = Concept("Humain").sub_class_of(top) animal = Concept("Animal").sub_class_of(top) chien = Concept("Chien").sub_class_of(animal) chat = Concept("Chat").sub_class_of(animal) humain.disjoint_with(animal) # un humain n'est pas un animal chat.disjoint_with(chien) # un chat n'est pas un chien
Python def super_concepts(concept, memo=None): if memo is None: memo = set() if concept in memo: return memo memo.add(concept) for parent in concept.parents: super_concepts(parent, memo) return memo print([c.nom for c in super_concepts(chien)]) # → ['Chien', 'Animal', 'ÊtreVivant']
La classification d'un individu, c'est l'ensemble de tous ses types + tous les ancêtres de ces types. Si Médor est un Chien, il est aussi Animal et ÊtreVivant.
OWL supporte l'héritage multiple : Chien ⊑ Animal, EstDomestique.
Pas de diamant, pas de problème (contrairement à C++).
Sans disjoints déclarés, OWL suppose que tout peut être n'importe quoi.
Chat ⊥ Chien empêche un individu d'être les deux.
Une propriété (ou rôle) relie des individus entre eux ou à des valeurs. Chaque propriété peut avoir :
Python class Propriete: def __init__(self, nom): self.nom = nom self.domaine = None self.image = None # range (évite conflit avec range()) def domain(self, concept): self.domaine = concept return self def range(self, concept): self.image = concept return self # Seule un Humain peut être le maître de quelqu'un (domaine) # Seul un Humain peut être le maître (range) est_le_maitre_de = Propriete("est_le_maitre_de") est_le_maitre_de.domain(humain).range(humain) # Seul un Animal peut manger, et on mange des ÊtresVivants mange = Propriete("mange").domain(animal).range(top)
Le domaine permet d'inférer le type du sujet : si Socrate est_le_maitre_de Platon, alors Socrate est un Humain.
Le range permet de vérifier la cohérence : si Platon → a_pour_animal → Médor, et que le range est Animal, alors Médor DOIT être un Animal (cohérent si Médor est un Chien).
mange avec
domaine Animal et qu'on a un triplet
Socrate mange une pomme, le raisonneur en déduit que
Socrate est un Animal ! C'est l'inférence de domaine.
Pour l'éviter, il faut que Socrate soit déclaré Humain et Humain ⊥ Animal.
Les concepteurs d'ontologies apprennent ça à leurs dépens,
généralement un vendredi après-midi.
Une restriction, c'est une contrainte sur une propriété qui permet de définir une classe. Les deux principales :
| Type | Notation | Sens |
|---|---|---|
| ∃ (some) | ∃ mange.Viande | « Mange au moins de la viande » — existence |
| ∀ (only) | ∀ mange.Végétal | « Ne mange que des végétaux » — universalité |
| = n (exactly) | =3 a_pour_enfant | « A exactement 3 enfants » — cardinalité |
| ≥ n (min) | ≥2 a_pour_enfant | « A au moins 2 enfants » |
| ≤ n (max) | ≤1 a_pour_conjoint | « A au plus 1 conjoint » (monogamie) |
Python # Classe définie par restriction : Herbivore = Animal qui ne mange que des plantes plante = Concept("Plante").sub_class_of(top) herbivore = Concept("Herbivore").sub_class_of(animal) herbivore.restriction_all("mange", plante) # Classe définie par existence : Carnivore = Animal qui mange au moins de la viande viande = Concept("Viande") carnivore = Concept("Carnivore").sub_class_of(animal) carnivore.restriction_some("mange", viande) # Cohérence : si un individu est herbivore ET carnivore → INCOHÉRENT herbivore.disjoint_with(carnivore)
L'intérêt ? Si Médor est un Chien, et qu'on déclare que tout Chien
∃ mange.Viande, alors Médor est classifié comme Carnivore
automatiquement. C'est la classification
— le raisonneur déduit des types que vous n'avez pas explicitement déclarés.
OWL permet de combiner des classes avec des opérateurs logiques pour former des descriptions de classes complexes :
| Opérateur | Symbole | Exemple | Sens |
|---|---|---|---|
| Intersection | ⊓ | Femme ⊓ Parent | « Les femmes qui sont parents » → Mère |
| Union | ⊔ | Chat ⊔ Chien | « Les chats ou les chiens » → AnimalDomestique |
| Complément | ¬ | ¬ Animal | « Tout ce qui n'est pas un animal » |
| Contrainte existentielle | ∃ | ∃ a_pour_enfant.Humain | « Ceux qui ont au moins un enfant humain » |
| Contrainte universelle | ∀ | ∀ a_pour_enfant.Humain | « Ceux dont tous les enfants sont humains » |
Python # Définir des classes par combinaison humain = Concept("Humain") homme = Concept("Homme").sub_class_of(humain) femme = Concept("Femme").sub_class_of(humain) parent = Concept("Parent").sub_class_of(humain) # Mère = Femme ⊓ Parent mere = Concept("Mère").sub_class_of(femme, parent) pere = Concept("Père").sub_class_of(homme, parent) # Si Alice est une Femme et a des enfants → elle est Mère alice = Individu("Alice") alice.a_pour_type(femme) alice.a_pour_fait("a_pour_enfant", bob) # Bob est un Humain # Le raisonneur infère qu'Alice est une Mère
Voici le cœur du raisonneur : il prend des individus avec leurs types et faits, et il classifie (trouve tous les types auxquels ils appartiennent) et vérifie la cohérence (détecte les contradictions).
Python class MiniReasoner: def __init__(self): self.concepts = {} self.proprietes = {} self.individus = {} def classifier(self, individu): """Déduit tous les types d'un individu.""" inferes = set(individu.types) # 1. Héritage : remonter les super-classes for t in list(individu.types): inferes |= self.super_concepts(t) # 2. Domaine : si une propriété a un domaine, l'appliquer for prop, val in individu.faits: p = self.proprietes.get(prop) if p and p.domaine and p.domaine not in inferes: inferes.add(p.domaine) return inferes def verifier_coherence(self, individu): types = self.classifier(individu) # Vérifier les disjoints for t in types: for d in t.disjoints: if d in types: return False, f"{individu} est à la fois {t} et {d}" # Vérifier domaine/range for prop, val in individu.faits: p = self.proprietes.get(prop) if p and p.domaine and p.domaine not in types: return False, f"{individu} utilise {prop} mais n'est pas du domaine" return True, "✓ cohérent"
Python r = MiniReasoner() # ... définir concepts, propriétés, individus ... r.afficher() # Résultat : # Socrate : Humain [✓ cohérent] → inféré: ÊtreVivant # Médor : Chien [✓ cohérent] → inféré: Animal, ÊtreVivant # Félix : Chat [✓ cohérent] → inféré: Animal, ÊtreVivant # Platon : Humain [✓ cohérent] → inféré: ÊtreVivant print("Socrate est-il un Animal ?", animal in r.classifier(socrate)) # → False (car Humain ⊥ Animal)
Assez de jouets. Owlready2 est une bibliothèque Python qui permet de manipuler de vraies ontologies OWL, de les raisonner avec HermiT (un vrai raisonneur Java), et de les exporter en Turtle, RDF/XML, etc.
Bash
pip install owlready2
Python import owlready2 onto = owlready2.get_ontology("http://example.org/animaux.owl") with onto: class ÊtreVivant(Thing): pass class Animal(ÊtreVivant): pass class Humain(ÊtreVivant): pass class Chien(Animal): pass class Chat(Animal): pass # Disjoints (le raisonneur les détectera) AllDisjoint([Humain, Animal]) AllDisjoint([Chat, Chien]) # Propriétés class est_le_maitre_de(ObjectProperty): domain = [Humain] range = [Humain] class mange(ObjectProperty): domain = [Animal] # Individus socrate = Humain("Socrate") platon = Humain("Platon") medor = Chien("Médor") socrate.est_le_maitre_de.append(platon) # Lancer le raisonneur HermiT sync_reasoner() # Explorer les inférences for i in onto.individuals(): print(i.name, [c.name for c in i.is_a if c is not Thing]) # Requêtes : l'API de Owlready2 permet d'interroger comme avec un ORM humains = onto.Humain.instances() print([h.name for h in humains])
Owlready2 utilise le raisonneur HermiT sous le capot (un raisonneur OWL 2 DL écrit en Java, lancé automatiquement en sous-processus). Il gère :
Il existe une ontologie célèbre dans le monde OWL : la Pizza Ontology. Créée par le Manchester University pour leurs cours, elle est à OWL ce que « Hello World » est à la programmation. Pourquoi des pizzas ? Parce que c'est plus digeste que des ontologies médicales (et tout le monde a déjà mangé une pizza).
Python pizza = Concept("Pizza") pizza_normale = Concept("PizzaNormale").sub_class_of(pizza) pizza_végé = Concept("PizzaVégétarienne").sub_class_of(pizza) pizza_viande = Concept("PizzaViande").sub_class_of(pizza) # Ingrédients ingredient = Concept("Ingrédient") fromage = Concept("Fromage").sub_class_of(ingredient) légume = Concept("Légume").sub_class_of(ingredient) viande = Concept("Viande").sub_class_of(ingredient) tomate = Concept("Tomate").sub_class_of(légume) jambon = Concept("Jambon").sub_class_of(viande) # Propriété a_pour_ingrédient = Propriete("a_pour_ingrédient").domain(pizza).range(ingredient) # Définitions par restrictions pizza_végé.restriction_all("a_pour_ingrédient", Concept("NonViande")) pizza_viande.restriction_some("a_pour_ingrédient", viande) pizza_végé.disjoint_with(pizza_viande) # Des pizzas concrètes margherita = Individu("Margherita") margherita.a_pour_type(pizza_normale) margherita.a_pour_fait("a_pour_ingrédient", "tomate") margherita.a_pour_fait("a_pour_ingrédient", "mozzarella") regina = Individu("Regina") regina.a_pour_type(pizza_normale) regina.a_pour_fait("a_pour_ingrédient", "jambon") → Classification : Margherita : PizzaNormale → inféré: Pizza Regina : PizzaNormale → inféré: Pizza
Ce qui rend la Pizza Ontology intéressante, c'est que la classification est basée sur les ingrédients : si une pizza contient au moins un ingrédient carnée, c'est une PizzaViande. Si elle n'en contient aucun, c'est une PizzaVégétarienne. C'est la définition par restriction qui fait le travail.
On a des classes, des propriétés, des restrictions, des raisonneurs. Il manque un moyen de poser des questions au graphe de connaissances. C'est le rôle de SPARQL (SPARQL Protocol and RDF Query Language), le SQL du web sémantique.
Une requête SPARQL utilise des triples patterns avec
des variables (préfixées par ?) :
SELECT ?sujet ?age
WHERE {
?sujet <http://example.org/age> ?age .
}
↑ les variables dans SELECT sont les colonnes du résultat
| Clause | Rôle | Exemple |
|---|---|---|
SELECT | Choisir les colonnes | SELECT ?s ?p ?o |
WHERE | Filtrer par motifs | { ?s rdf:type ex:Humain } |
DISTINCT | Éliminer les doublons | SELECT DISTINCT ?s |
PREFIX | Raccourcir les URIs | PREFIX ex: <http://...> |
FILTER | Conditions avancées | FILTER(?age > 30) |
LIMIT | Limiter le nombre | LIMIT 10 |
Apache Jena est la boîte à outils Java de référence pour le web sémantique. Elle contient :
Le moteur SPARQL de Jena. Parse, optimise, exécute des requêtes. Supporte SPARQL 1.1 (sous-requêtes, aggregation, UPDATE).
Stockage natif RDF sur disque. Optimisé pour les gros volumes (milliards de triplets). Transactionnel, persistant.
Inférence OWL et RDFS intégrée. Supporte les règles personnalisées (forward/backward chaining).
Serveur SPARQL HTTP. Interface web, API REST, chargement de données. Le « Apache HTTPD » du web sémantique.
Notre serveur mini_fuseki.py implémente un sous-ensemble
de SPARQL en pur Python :
Python # Lancer le serveur $ python mini_fuseki.py --exemple 🚀 Mini Fuseki démarré sur http://127.0.0.1:8080/ # Requête GET (URL-encodée) $ curl 'http://localhost:8080/sparql?query= \ SELECT+?s+?o+WHERE+%7B+?s+%3Chttp://example.org/estUn%3E+?o+%7D' # Résultat (JSON standard SPARQL) { "head": { "vars": ["s", "o"] }, "results": { "bindings": [ { "s": {"type":"uri","value":"http://example.org/Socrate"}, "o": {"type":"uri","value":"http://example.org/Humain"} }, ... ] } }
SPARQL # 1. Tout voir (exploration) SELECT * WHERE { ?s ?p ?o } # 2. Tous les Humains SELECT ?s WHERE { ?s <http://example.org/estUn> <http://example.org/Humain> } # 3. Les maîtres et leurs élèves (jointure naturelle sur ?s) SELECT ?s ?m WHERE { ?s <http://example.org/estLeMaitreDe> ?m } # 4. Les philosophes (via rdf:type) SELECT ?s WHERE { ?s <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/Philosophe> } # 5. Qui a un âge (littéral numérique) SELECT ?s ?age WHERE { ?s <http://example.org/age> ?age }
Le moteur exécute la requête par jointure itérative : pour chaque pattern, on cherche dans les index, on lie les variables, et on combine les résultats. C'est exactement comme une jointure SQL mais avec des graphes au lieu de tables.
Python def execute_sparql(store, query): # 1. Parser les préfixes # 2. Extraire SELECT et WHERE par regex # 3. Découper le WHERE en triple patterns # 4. Pour chaque pattern, chercher dans le store # 5. Joindre les résultats par variable def match_triple(s, p, o): for s2, p2, o2 in store.chercher( sujet if not sujet.startswith('?') else None, predicat if not predicat.startswith('?') else None, objet if not objet.startswith('?') else None ): yield {'?s': s2, '?p': p2, '?o': o2} # si variables # Jointure : combiner les résultats de chaque pattern def joindre(idx_pattern, bindings): if idx_pattern >= len(patterns): yield bindings return s, p, o = patterns[idx_pattern] for nouveau in match_triple(s, p, o, bindings): yield from joindre(idx_pattern + 1, {**bindings, **nouveau}) rows = list(joindre(0, {}))
FILTERAjouter des conditions : FILTER(?age > 30).
Il suffit de parser les expressions et de les évaluer sur chaque binding.
OPTIONALJointure gauche : même si le pattern ne matche pas, on garde la ligne. Ajoute des valeurs NULL.
ORDER BYTrier les résultats. Nécessite de comparer des valeurs (numériques, alphabétiques, URIs).
Au lieu de SELECT (table), CONSTRUCT crée un nouveau graphe RDF. Permet de transformer des données à la volée.
Ce troisième cours vous a emmené des classes aux ontologies OWL, en passant par les propriétés, les restrictions, et la classification automatique.
Ce qu'il reste à explorer :
La théorie derrière OWL. Les logiques ALC, SROIQ, les tableaux, les arbres de modèles. C'est beau, c'est complexe, c'est de la logique pure.
Pellet (le plus ancien), RacerPro (commercial), ELK (très rapide sur les ontologies EL). Chacun a ses forces et faiblesses.
SNOMED CT (médecine, 300K+ classes), FMA (anatomie), Gene Ontology (biologie), CIDOC-CRM (musées), Dublin Core (bibliothèques).
Stocker des ontologies dans une base relationnelle (OWL-RDBMS). Des systèmes comme Stardog ou GraphDB combinent OWL et SPARQL.
Extraire automatiquement des ontologies du texte (ontology learning). FRED, Text2Onto, ou des approches modernes avec LLMs.
Wikidata n'est pas à proprement parler une ontologie OWL, mais son modèle (items, propriétés, contraintes) s'en inspire largement. Et c'est le plus large graphe de connaissances jamais créé.
| Fichier | Description | Commande |
|---|---|---|
recherche.py | Moteur de recherche (TF-IDF, BM25, embeddings) | python recherche.py |
raisonnement.py | Mini OWL / Prolog : classes, restrictions, Owlready2 | python raisonnement.py |
mini_fuseki.py | Serveur SPARQL (mini Fuseki) | python mini_fuseki.py --exemple |
| Commande | Action |
|---|---|
make | Affiche l'aide |
make prepare | Télécharge les codes depuis semantic.lambda-flow.fr |
make recherche | Lance la démo de recherche |
make prolog | Lance la démo Prolog/OWL manuelle |
make raisonnement | Lance la démo Owlready2 (si installé) |
make serveur ou make fuseki | Lance le serveur SPARQL sur :8080 |
make web | Lance le serveur Flask sur :5000 |
make all | Exécute toutes les démos |
Bash # Pour Owlready2 (raisonnement OWL réel) pip install owlready2 # Pour le cours #1 (sklearn, gensim, whoosh) pip install scikit-learn gensim whoosh flask rank-bm25 # Pour Apache Jena Fuseki (version Java complète) # https://jena.apache.org/download/ wget https://dlcdn.apache.org/jena/binaries/apache-jena-fuseki-5.3.0.tar.gz tar xzf apache-jena-fuseki-*.tar.gz ./apache-jena-fuseki-*/fuseki-server --update --mem /ds