← Suite des cours précédents

Des Faits aux Concepts

Des triplets qu'on aligne aux ontologies qui raisonnent.
OWL, classes, propriétés, restrictions — ou comment donner du sens aux sens.

LIEN AVEC LE COURS #2

De la structure à la connaissance

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.

« Une ontologie, c'est comme un dictionnaire, mais en plus relou et avec plus de logique. » — Toute personne ayant déjà lu une spec OWL
🧠 TL;DR : Cours #1 : les mots → des vecteurs (sémantique statistique)
Cours #2 : les faits → des triplets (sémantique relationnelle)
Cours #3 : les types → des ontologies (sémantique formelle)
Chaque cours ajoute une couche de structure à la précédente.
CHAPITRE 1

Classes : ce que les choses sont

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 ».)

Code : définir des classes

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
🏛️ Étymologie : Le mot « ontologie » vient du grec ontos (être) et logos (discours). Littéralement : « discours sur l'être ». Aristote a écrit là-dessus il y a 2300 ans. On appelle ça la métaphysique. Aujourd'hui, on appelle ça OWL et on l'encode en XML. Le progrès est parfois une question de syntaxe.

Remonter la hiérarchie

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.

🧬 Héritage multiple

OWL supporte l'héritage multiple : Chien ⊑ Animal, EstDomestique. Pas de diamant, pas de problème (contrairement à C++).

🚫 Disjoints

Sans disjoints déclarés, OWL suppose que tout peut être n'importe quoi. Chat ⊥ Chien empêche un individu d'être les deux.

CHAPITRE 2 RÔLES

Propriétés : ce qui relie les choses

Une propriété (ou rôle) relie des individus entre eux ou à des valeurs. Chaque propriété peut avoir :

domaine
la classe des sujets qui peuvent porter cette propriété
range
la classe des objets que la propriété peut prendre
fonctionnelle
un sujet ne peut avoir qu'une seule valeur pour cette propriété
transitive
si a→b et b→c alors a→c

Code : domaine et range

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).

⚠️ Piège : En OWL, si on déclare 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.
CHAPITRE 3 PUISSANCE

Restrictions : le nerf de la guerre OWL

Une restriction, c'est une contrainte sur une propriété qui permet de définir une classe. Les deux principales :

TypeNotationSens
∃ (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)

Code : restrictions

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.

« Une restriction OWL, c'est comme une clause de contrat : si la condition est remplie, la classification s'applique. Sans avocat. Sans paperasse. Juste du ⊧ (modèle logique). » — Quelqu'un qui lit trop de papiers de recherche
CHAPITRE 4 LOGIQUE

Constructeurs logiques : l'algèbre des concepts

OWL permet de combiner des classes avec des opérateurs logiques pour former des descriptions de classes complexes :

OpérateurSymboleExempleSens
IntersectionFemme ⊓ Parent« Les femmes qui sont parents » → Mère
UnionChat ⊔ 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 »

Code : classes par intersection

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
🧮 Symbole amusant : Le symbole ⊓ (intersection) sappelle « sqcap » en LaTeX. Le symbole ⊥ (bottom, classe vide) sappelle « bot » ou « perp » si vous êtes en train de lire un article de logique à 2h du matin. OWL a emprunté tous ses symboles à la logique descriptive (Description Logic), une branche de la logique mathématique née dans les années 1980. Les logiciens avaient déjà inventé tous les symboles dont on avait besoin. Merci les logiciens.
CHAPITRE 5 CODE

Mini-moteur de classification

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"

Exécution complète

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)
🎯 Le vrai raisonnement OWL : Ce mini-moteur est très simplifié. Un vrai raisonneur OWL (HermiT, Pellet, RacerPro, ELK) implémente des algorithmes de tableau (tableaux) pour la logique descriptive. Ces algorithmes essaient de construire un modèle de l'ontologie et détectent les contradictions. La complexité ? EXPTIME-complet dans le pire des cas. Autrement dit : sur une ontologie mal conçue, le raisonneur peut prendre plus de temps que l'univers n'a d'âge. En pratique, ça marche bien (sauf le vendredi après-midi, voir plus haut).
CHAPITRE 6 BIBLIOTHÈQUE

Owlready2 : la vraie vie

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.

Installation

Bash
pip install owlready2

Code

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 :

🧙 HermiT : Le raisonneur HermiT a été développé à l'université d'Oxford. Son nom vient de « Hermit » (ermite), parce qu'il vit tout seul dans son coin et qu'il sort rarement de sa grotte (un processeur Java). Il a remporté plusieurs compétitions OWL Reasoner Evaluation (ORE). C'est le champion du monde des raisonneurs. Moins médiatisé que les JO, mais tout aussi compétitif.
CHAPITRE 7 CLASSIQUE

La Pizza Ontology : le Hello World d'OWL

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).

Concept : une pizza est définie par ses ingrédients

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
🍕 Le saviez-vous ? La Pizza Ontology originale contient plus de 80 classes de pizzas et 35 ingrédients. Elle a été utilisée dans des cours à Manchester, Stanford, et la Sorbonne. Il existe même une ontologie des bières (Beer Ontology) pour accompagner la pizza. Le web sémantique a soif. Et faim.

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.

CHAPITRE 8 INTERROGER

SPARQL : interroger le graphe

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.

« SPARQL est à RDF ce que SQL est aux bases relationnelles : un langage de requête normalisé. En mieux, parce que les développeurs n'ont pas eu à le réinventer (ils ont juste copié SQL). » — Un ingénieur sémantique qui a trop fait de SELECT * FROM

Principes de base

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
ClauseRôleExemple
SELECTChoisir les colonnesSELECT ?s ?p ?o
WHEREFiltrer par motifs{ ?s rdf:type ex:Humain }
DISTINCTÉliminer les doublonsSELECT DISTINCT ?s
PREFIXRaccourcir les URIsPREFIX ex: <http://...>
FILTERConditions avancéesFILTER(?age > 30)
LIMITLimiter le nombreLIMIT 10

Apache Jena & Fuseki

Apache Jena est la boîte à outils Java de référence pour le web sémantique. Elle contient :

📦 ARQ

Le moteur SPARQL de Jena. Parse, optimise, exécute des requêtes. Supporte SPARQL 1.1 (sous-requêtes, aggregation, UPDATE).

🗄️ TDB

Stockage natif RDF sur disque. Optimisé pour les gros volumes (milliards de triplets). Transactionnel, persistant.

🧠 Raisonneur

Inférence OWL et RDFS intégrée. Supporte les règles personnalisées (forward/backward chaining).

🌐 Fuseki

Serveur SPARQL HTTP. Interface web, API REST, chargement de données. Le « Apache HTTPD » du web sémantique.

🏺 Pourquoi « Fuseki » ? Fuseki est le nom d'un célèbre joueur de go du XVIIe siècle (Honinbō Dosaku, aussi appelé Fuseki). Le go est un jeu de stratégie où on pose des pierres sur un plateau — comme on pose des triplets dans un graphe. Les développeurs de Jena avaient le sens de la métaphore. Ou alors ils aimaient juste le thé vert. Les deux, probablement.

Mini Fuseki — notre implémentation

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"} },
      ...
    ]
  }
}

Requêtes SPARQL pas à pas

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.

Le cœur du moteur SPARQL

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, {}))
⚡ Performance : Un vrai Fuseki (Apache Jena) utilise des index B+ Trees, du query optimization (réordonnancement des patterns), et du cache de résultats. Notre version est pédagogique — elle montre le principe sans la complexité. C'est comme comparer un avion en papier et un A380. Les deux volent. L'un des deux peut transporter 500 passagers.

Variations et extensions

🔍 FILTER

Ajouter des conditions : FILTER(?age > 30). Il suffit de parser les expressions et de les évaluer sur chaque binding.

🔄 OPTIONAL

Jointure gauche : même si le pattern ne matche pas, on garde la ligne. Ajoute des valeurs NULL.

📊 ORDER BY

Trier les résultats. Nécessite de comparer des valeurs (numériques, alphabétiques, URIs).

🏗️ Requêtes CONSTRUCT

Au lieu de SELECT (table), CONSTRUCT crée un nouveau graphe RDF. Permet de transformer des données à la volée.

ÉPILOGUE

Où aller maintenant ?

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 :

📐 Logique descriptive (DL)

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.

🧪 Raisonneurs OWL avancés

Pellet (le plus ancien), RacerPro (commercial), ELK (très rapide sur les ontologies EL). Chacun a ses forces et faiblesses.

🌐 Ontologies célèbres

SNOMED CT (médecine, 300K+ classes), FMA (anatomie), Gene Ontology (biologie), CIDOC-CRM (musées), Dublin Core (bibliothèques).

🔗 OWL + Bases de données

Stocker des ontologies dans une base relationnelle (OWL-RDBMS). Des systèmes comme Stardog ou GraphDB combinent OWL et SPARQL.

🤖 OWL + NLP

Extraire automatiquement des ontologies du texte (ontology learning). FRED, Text2Onto, ou des approches modernes avec LLMs.

🏛️ Wikidata comme ontologie

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éé.

« Une ontologie ne dit pas ce qui existe. Elle dit comment on parle de ce qui existe. » — Nicola Guarino, père de l'ingénierie ontologique
⊓ ⊔ ¬ ∃ ∀
ANNEXE

Guide pratique

Résumé des fichiers du cours

FichierDescriptionCommande
recherche.pyMoteur de recherche (TF-IDF, BM25, embeddings)python recherche.py
raisonnement.pyMini OWL / Prolog : classes, restrictions, Owlready2python raisonnement.py
mini_fuseki.pyServeur SPARQL (mini Fuseki)python mini_fuseki.py --exemple

Commandes Make

CommandeAction
makeAffiche l'aide
make prepareTélécharge les codes depuis semantic.lambda-flow.fr
make rechercheLance la démo de recherche
make prologLance la démo Prolog/OWL manuelle
make raisonnementLance la démo Owlready2 (si installé)
make serveur ou make fusekiLance le serveur SPARQL sur :8080
make webLance le serveur Flask sur :5000
make allExécute toutes les démos

Installer les bibliothèques

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
🎓 Bilan des trois cours :
Cours #1 : Du texte aux vecteurs (TF-IDF → Word2Vec → Whoosh)
Cours #2 : Des triplets aux graphes (RDF → Turtle → Prolog)
Cours #3 : Des classes aux ontologies (OWL → Raisonneur → SPARQL)

Vous avez maintenant les clés pour comprendre, construire et interroger un web sémantique fonctionnel. Pas mal pour trois cours. Tim Berners-Lee serait fier. Ou au moins intéressé.