← Suite du cours précédent

Des Mots aux Faits

Du texte qu'on découpe aux triplets qui relient les idées.
Unification, Prolog, Turtle — ou comment donner une structure à la connaissance.

LIEN AVEC LE COURS #1

Du mot au fait : le saut sémantique

Dans le premier cours, on a appris à découper du texte en mots, les pondérer (TF-IDF), les projeter dans l'espace (embeddings). Mais un mot tout seul, c'est pauvre. « Socrate » — qui est-ce ? « est mortel » — qu'est-ce que ça veut dire ?

Pour qu'une machine raisonne, il faut une représentation structurée de la connaissance. Pas juste des sacs de mots, mais des faits :

Socrate ──est_un──▶ Homme
Homme ──est──────▶ Mortel
Socrate est mortel
« La connaissance n'est pas dans les mots, mais dans les relations entre les mots. » — Alfred Korzybski (et tout bon ingénieur en ontologies)

Ce deuxième cours vous emmène du triplet (la molécule de la connaissance) jusqu'à Turtle, le langage standard du web sémantique, en passant par l'unification et un mini-Prolog qu'on va construire ensemble.

CHAPITRE 1

Triplets : les atomes de la connaissance

Toute phrase simple peut se réduire à une structure à trois parties :
Sujet — Verbe — Complément

SujetVerbeComplément
Socrateest_unhomme
Socrateest_le_maitre_dePlaton
Platonest_le_maitre_deAristote
Hommeestmortel

Chaque ligne est un triplet (sujet, verbe, complément). En RDF (le standard du web sémantique), on dit : sujet — prédicat — objet. Même idée, vocabulaire différent.

🏛️ Point culture : Cette structure Sujet-Verbe-Objet n'est pas universelle. Le japonais met le verbe à la fin (SOV). L'arabe commence par le verbe (VSO). Mais pour le web sémantique, on a choisi SVO — probablement parce que Tim Berners-Lee parlait anglais. L'impérialisme linguistique jusque dans les triplets.

Code : une connaissance = une liste de triplets

Python
# Chaque connaissance est un triplet (sujet, verbe, complément)
connaissance = [
    ("Socrate",     "est_un",          "homme"),
    ("Socrate",     "est_le_maitre_de", "Platon"),
    ("Platon",      "est_le_maitre_de", "Aristote"),
    ("Aristote",    "est_le_maitre_de", "Alexandre"),
    ("Astyanax",    "est_le_fils_de",  "Hector"),
    ("Hector",      "est_le_fils_de",  "Priam"),
    ("Priam",       "est_le_roi_de",   "Troie"),
    ("homme",       "est",             "mortel"),
]

# Index : pour chaque sujet, ses (verbe, complément)
par_sujet = {}
for s, v, c in connaissance:
    par_sujet.setdefault(s, []).append((v, c))

def chercher(sujet, verbe=None):
    return [c for v, c in par_sujet.get(sujet, []) if verbe is None or v == verbe]

print(chercher("Platon", "est_le_maitre_de"))
# → ['Aristote']

🧪 Variation : complément multiple

Ajouter un 4e champ (contexte, temps, confiance) pour faire du quadruplet.

🔗 Variation : chaînage

Suivre les relations : « Astyanax → fils_de → Hector → fils_de → Priam ».

CHAPITRE 2 INTERLUDE

Le graphe de connaissances

Un triplet, c'est une flèche entre deux nœuds. Tous les triplets ensemble forment un graphe orienté.

Socrate
├──est_un──▶ Homme ──est──▶ Mortel
├──est_le_maitre_de──▶ Platon
└──est_le_maitre_de──▶ Aristote
└──est_un──▶ Homme (déjà vu)

Dans ce graphe, on peut naviguer, inférer, joindre. Un graphe de connaissances, c'est le cerveau d'une machine — en moins humide et avec plus de JSON.

🌐 Le saviez-vous ? Le Knowledge Graph de Google (lancé en 2012) contient plus de 70 milliards de faits. C'est lui qui fait apparaître la petite fiche à droite quand vous cherchez « Victor Hugo » ou « fromage qui pue ». Et oui, c'est basé sur des triplets. Plein. Des milliards.

Inférence simple par héritage

On veut savoir si Socrate est mortel. Le triplet n'existe pas explicitement. Mais on peut remonter la chaîne :

Python
def heriter(sujet, verbe):
    """Remonte la chaîne 'est_un'/'est' pour trouver un triplet."""
    vus = set()
    pile = [sujet]
    while pile:
        s = pile.pop()
        if s in vus: continue
        vus.add(s)
        for v, c in chercher(s):
            if v == verbe:
                return c
            if v in ("est_un", "est"):
                pile.append(c)
    return None

print(heriter("Socrate", "est_mortel"))
# → None (le prédicat s'appelle "est", pas "est_mortel")

# Correction : on veut la valeur du prédicat "est"
print(heriter("Socrate", "est"))
# → 'mortel'  (Socrate → est_un → homme → est → mortel)

C'est de l'inférence par chaînage : on suit les pointeurs jusqu'à trouver ce qu'on cherche. C'est primitif, mais c'est la base du raisonnement automatique.

CHAPITRE 3 CŒUR

Unification : faire correspondre des motifs

L'unification, c'est l'art de faire correspondre deux structures en permettant des variables. C'est le moteur de Prolog, des systèmes de types, de la résolution de contraintes, et de votre capacité à reconnaître qu'un chat vu de dos est quand même un chat.

« L'unification, c'est le matchmaking des structures de données. Tinder pour termes logiques. » — Un chercheur en IA qui a passé trop de temps sur Prolog

Principe

On prend deux termes, on essaie de les rendre identiques en remplaçant les variables par des valeurs. Si on y arrive, on retourne les substitutions.

unifier("est_mortel", "$X") avec ("est_mortel", "Socrate")
   → {"$X": "Socrate"}

unifier("est_un", "$X", "$Y") avec ("est_un", "Platon", "homme")
   → {"$X": "Platon", "$Y": "homme"}

unifier("est_un", "$X", "$X") avec ("est_un", "chat", "animal")
   → impossible ($X ≠ chat ET $X ≠ animal simultanément)
  

Implémentation

Python
def unifier(t1, t2, subs=None):
    if subs is None:
        subs = {}
    if t1 == t2:
        return subs
    if isinstance(t1, str) and t1.startswith("$"):
        subs[t1] = t2
        return subs
    if isinstance(t2, str) and t2.startswith("$"):
        subs[t2] = t1
        return subs
    if isinstance(t1, tuple) and isinstance(t2, tuple) and len(t1) == len(t2):
        for a, b in zip(t1, t2):
            subs = unifier(a, b, subs)
            if subs is None:
                return None
        return subs
    return None

# Tests
print(unifier(("est_mortel", "$X"), ("est_mortel", "Socrate")))
# → {'$X': 'Socrate'}
print(unifier(("est_un", "$X", "$Y"), ("est_un", "chat", "animal")))
# → {'$X': 'chat', '$Y': 'animal'}
⚙️ Détail technique : L'unification qu'on implémente ici est syn tactique (sans occurs check). En vrai Prolog, on vérifie qu'une variable n'apparaît pas dans le terme qu'on lui assigne (pour éviter les définitions circulaires du genre X = f(X)). C'est ce qu'on appelle l'occurs check, et c'est désactivé dans la plupart des Prologs pour des raisons de performance. Le casse-tête des gens qui écrivent des solveurs.
CHAPITRE 4 MOTEUR

Mini-Prolog : le raisonnement automatique

On a des faits, on a l'unification. Il manque les règles. Une règle dit : si telles conditions sont vraies, alors cette conclusion est vraie. En Prolog, ça s'écrit :

est_mortel(X) :- homme(X).
    ↑ conclusion  ↑ prémisses
  

Le chaînage avant (forward chaining) applique les règles aux faits pour produire de nouveaux faits. C'est ce qu'on fait ici :

Python
# Faits de base
faits = [
    ("est_un", "Socrate", "homme"),
    ("est_un", "Platon",  "homme"),
    ("est_un", "Aristote","homme"),
    ("est_le_maitre_de", "Socrate", "Platon"),
    ("est_le_maitre_de", "Platon",  "Aristote"),
]

# Règles
regles = [
    # Si X est un homme, alors X est mortel
    (("est_mortel", "$X"), [("est_un", "$X", "homme")]),
    # Si X est un homme ET Y est le maître de X, alors X est un grand philosophe
    (("grand_philosophe", "$X"),
     [("est_un", "$X", "homme"), ("est_le_maitre_de", "$Y", "$X")]),
]

# Appliquer les règles (chaînage avant)
def appliquer_regles(faits, regles):
    nouveaux = []
    for conclusion, premisses in regles:
        for fait in faits:
            subs = unifier(premisses[0], fait)
            if subs is not None:
                # Vérifier les prémisses restantes...
                nouveau = appliquer_subs(conclusion, subs)
                if nouveau not in faits:
                    nouveaux.append(nouveau)
    return faits + nouveaux

def appliquer_subs(terme, subs):
    if isinstance(terme, str):
        return subs.get(terme, terme)
    if isinstance(terme, tuple):
        return tuple(appliquer_subs(t, subs) for t in terme)
    return terme

# Test : inférence complète
faits_complets = appliquer_regles(faits, regles)
print([f for f in faits_complets if f[0] == "grand_philosophe"])
# → [('grand_philosophe', 'Platon'), ('grand_philosophe', 'Aristote')]
📜 Histoire : Prolog a été inventé en 1972 par Alain Colmerauer et Robert Kowalski. Colmerauer était français (Marseille), Kowalski polonais (Édimbourg). Le nom vient de Programmation Logique. C'était l'un des langages vedettes de l'IA dans les années 80, avec Lisp. Aujourd'hui, il survit dans les systèmes experts, le traitement du langage naturel, et les compilateurs. Et contrairement à ce que disent certains : non, Prolog n'est pas mort. Il dort. Tranquillement. Dans un coin.
CHAPITRE 5 PARSEUR

Une syntaxe qui ressemble à du vrai Prolog

Nos tuples Python, c'est pratique, mais ce n'est pas très glamour. Donnons une véritable syntaxe Prolog à notre moteur :

Syntaxe PrologNotre interne
homme(socrate).('homme', 'socrate')
est_mortel(X) :- homme(X).(('est_mortel','$X'), [('homme','$X')])
?- est_mortel(X).[('est_mortel','$X')]
Python
import re

def parse_terme(chaine):
    """'est_mortel(X)' → ('est_mortel', '$X')"""
    m = re.fullmatch(r'(\w+)\(([^)]*)\)', chaine.strip())
    if not m: return None
    nom = m.group(1)
    args = [a.strip() for a in m.group(2).split(',') if a.strip()]
    args = tuple(f'${a}' if a[0].isupper() else a for a in args)
    return (nom,) + args

def parse_programme(texte):
    faits, regles = [], []
    for ligne in texte.strip().split('\n'):
        ligne = ligne.strip()
        if not ligne or ligne.startswith('%'): continue
        if ':-' in ligne:
            gauche, droite = ligne.replace(' ','').split(':-')
            conclusion = parse_terme(gauche)
            premisses = [parse_terme(p+')') for p in droite.rstrip('.').split('),')]
            regles.append((conclusion, premisses))
        else:
            f = parse_terme(ligne.rstrip('.'))
            if f: faits.append(f)
    return faits, regles

--- Exemple ---
programme = """
    homme(socrate).
    homme(platon).
    est_mortel(X) :- homme(X).
    grand_philosophe(X) :- homme(X), est_le_maitre_de(Y, X).
"""

faits_prog, regles_prog = parse_programme(programme)
print(faits_prog)
# → [('homme', 'socrate'), ('homme', 'platon')]

📝 Variation : listes Prolog

Ajouter la syntaxe [X|Xs] pour les listes, avec unification récursive. C'est ce qui rend Prolog si puissant sur le traitement de séquences.

🔁 Variation : coupure (!)

Le cut Prolog (!) empêche le backtracking. À implémenter avec un flag dans le moteur.

🧮 Variation : opérateurs arithmétiques

is/2 pour évaluer des expressions. X is 2 + 3 → unifie X avec 5.

CHAPITRE 6 STANDARD

Turtle : le langage du web sémantique

Nos triplets maison, c'est bien. Mais pour partager de la connaissance sur le web, il faut un standard. C'est là que RDF (Resource Description Framework) et sa syntaxe Turtle interviennent.

« RDF est au web sémantique ce que HTTP est au web classique : le langage commun. » — Tim Berners-Lee (1999, en espérant que ça marcherait mieux que prévu)

Les concepts clés de Turtle

NotationSensExemple
@prefix ns: <uri>Déclare un préfixe@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ns:nomNom qualifié (QName)foaf:Person
sujet prédicat objet .Triplet RDFex:Socrate ex:estUn ex:Humain .
rdf:type« est un » standardex:Socrate rdf:type ex:Humain .
rdfs:subClassOfHéritage de classesex:Chien rdfs:subClassOf ex:Animal .
"valeur"Littéral (chaîne)ex:Humain rdfs:label "Humain" .

Mini parseur Turtle

Python
import re
from collections import defaultdict

PREFIXES = {
    "rdf":  "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
    "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
    "owl":  "http://www.w3.org/2002/07/owl#",
    "xsd":  "http://www.w3.org/2001/XMLSchema#",
    "foaf": "http://xmlns.com/foaf/0.1/",
}

class GraphRDF:
    def __init__(self):
        self.triplets = []
        self.prefixes = dict(PREFIXES)

    def _expand(self, qname):
        if ':' in qname and not qname.startswith('"'):
            p, reste = qname.split(':', 1)
            return self.prefixes.get(p, p) + reste
        return qname.strip('"')

    def charger(self, texte):
        for ligne in texte.split('\n'):
            ligne = ligne.strip()
            if not ligne or ligne.startswith('#'): continue
            # @prefix
            m = re.match(r'@prefix\s+(\w+):\s*<(.*?)>\s*\.', ligne)
            if m:
                self.prefixes[m.group(1)] = m.group(2)
                continue
            # triplet
            m = re.match(r'(\S+)\s+(\S+)\s+(.+?)\s*\.\s*$', ligne)
            if m:
                s = self._expand(m.group(1))
                p = self._expand(m.group(2))
                o = self._expand(m.group(3))
                self.triplets.append((s, p, o))
        return self.triplets

    def chercher(self, sujet=None, predicat=None, objet=None):
        return [(s,p,o) for s,p,o in self.triplets
                if (sujet is None or s == sujet)
                and (predicat is None or p == predicat)
                and (objet is None or o == objet)]

--- Exemple ---
TURTLE = """
    @prefix ex: <http://example.org/> .
    @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .

    ex:Socrate  ex:estUn          ex:Humain .
    ex:Socrate  ex:estLeMaitreDe  ex:Platon .
    ex:Platon   ex:estUn          ex:Humain .
    ex:Platon   ex:estLeMaitreDe  ex:Aristote .
    ex:Humain   ex:estMortel      "vrai" .
"""

g = GraphRDF()
g.charger(TURTLE)
print(g.chercher(predicat="http://example.org/estUn",
                   objet="http://example.org/Humain"))
# → [('http://example.org/Socrate', 'http://example.org/estUn',
#      'http://example.org/Humain'),
#     ('http://example.org/Platon', ...)]
🌍 Réalité : Turtle est l'une des syntaxes RDF les plus lisibles. Il en existe d'autres : RDF/XML (le standard officiel, illisible), N-Triples (très simple, idéal pour les machines), JSON-LD (pour les développeurs web), et N3 (une extension de Turtle avec des règles logiques). DBpedia, l'extraction de Wikipédia en RDF, contient plus de 3 milliards de triplets en Turtle. Si vous les imprimiez, ça irait de la Terre à la Lune… en format A4. Probablement.

Requête : qui est mortel dans notre graphe Turtle ?

Python
# 1. Trouver la classe "Mortel"
mortel = g.chercher(predicat="http://example.org/estMortel")
# → [('http://example.org/Humain', 'ex:estMortel', '"vrai"')]

# 2. Trouver toutes les instances de Humain
humains = g.chercher(predicat="http://example.org/estUn",
                     objet="http://example.org/Humain")
print([s.split('/')[-1] for s,_,_ in humains])
# → ['Socrate', 'Platon']
# Les deux sont mortels par héritage !
ÉPILOGUE

Et après ? Le web sémantique vous attend

Ce cours vous a emmené du triplet au graphe RDF en passant par l'unification et le Prolog. Vous avez construit, ligne par ligne, les briques fondamentales du raisonnement automatique.

Mais le chemin est long. Quelques directions pour la suite :

🗺️ SPARQL

Le SQL du web sémantique. Interrogez des graphes RDF avec une syntaxe puissante : SELECT ?x WHERE { ?x ex:estUn ex:Humain }.

🧠 OWL

Le langage d'ontologies. Ajoutez de la logique descriptive : cardinalités, équivalences, restrictions. C'est Prolog en plus costaud.

🕸️ Linked Data

Le principe de Tim Berners-Lee : publier des données reliées sur le web. DBpedia, Wikidata, etc. 5 étoiles pour le data publishing.

🤝 Graph + Embeddings

Combiner la structure des graphes avec la puissance des vecteurs. RDF2Vec, Knowledge Graph Embeddings — le meilleur des deux mondes.

🏛️ Wikidata

La base de connaissance collaborative de Wikipedia. 100 millions de triplets, requêtable en SPARQL. Le plus grand graphe de connaissances public du monde.

🔍 Raisonneur

Implémenter un algorithme de tableau (tableau) pour la logique descriptive. C'est ce qui fait tourner les raisonneurs OWL.

« Le web sémantique n'est pas un rêve. C'est un chantier. Et tout le monde peut y contribuer. » — Tim Berners-Lee (toujours lui, il n'a pas lâché l'affaire)
🧩 ● ──▶ ●