TAL — du mot aux dépendances : le pipeline linguistique français

La série Texte traite le langage côté modèle : prompts, RAG, fine-tuning. Le notebook RAG-et-Memoire-Semantique/04-Tokenisation-From-Scratch.ipynb y construit la tokenisation sous-mot (BPE) qui alimente les LLM. Il existe une seconde tradition — le traitement automatique du langage (TAL) classique — qui analyse le français au niveau du mot : lemmes, parties du discours, traits morphologiques, relations de dépendance, entités nommées.

Les deux traditions répondent à des questions différentes :

Ce notebook exécute un pipeline spaCy français sur un corpus fil rouge à deux domaines, compare la représentation en mots à la tokenisation BPE du notebook 04, mesure les erreurs du modèle contre des annotations de référence, et débouche sur un cas d’usage borné : la recherche lemmatisée.

1. Corpus fil rouge — deux domaines, un seul fil

Le domaine littéraire reprend exactement le même texte que le notebook 04 (fables de La Fontaine, domaine public) — la comparaison mots/BPE de la section 6 porte donc sur des données identiques. Le domaine juridico-technique est un texte original (RGPD, registre de traitement) : phrases longues, groupes nominaux denses, entités réelles (organismes, dates, référentiels).

# Corpus A (litteraire) : identique a RAG-et-Memoire-Semantique/04-Tokenisation-From-Scratch.ipynb
FABLES_TEXT = """
Maître Corbeau, sur un arbre perché, tenait en son bec un fromage.
Maître Renard, par l'odeur alléché, lui tint à peu près ce langage.
Hé bonjour, Monsieur du Corbeau. Que vous êtes joli, que vous me semblez beau.
Sans mentir, si votre ramage se rapporte à votre plumage, vous êtes le Phénix des hôtes de ces bois.
Le corbeau, honteux et confus, jura, mais un peu tard, qu'on ne l'y prendrait plus.
La cigale ayant chanté tout l'été, se trouva fort dépourvue quand la bise fut venue.
Pas un seul petit morceau de mouche ou de vermisseau, elle alla crier famine chez la fourmi sa voisine.
Je vous paierai, lui dit-elle, avant l'août, foi d'animal, intérêt et principal.
La fourmi n'est pas prêteuse, c'est là son moindre défaut.
Que faisiez-vous au temps chaud, dit-elle à cette emprunteuse.
Nuit et jour à tout venant je chantais, ne vous déplaise.
Vous chantiez, j'en suis fort aise, eh bien dansez maintenant.
Le lièvre et la tortue firent une course, et la tortue, lente mais obstinée,
gagna contre le lièvre trop confiant.
Rien ne sert de courir, il faut partir à point, la patience vaut mieux que la force.
Le lion, roi des animaux, apprit un jour que les bêtes de son royaume le craignaient.
Le rat des villes et le rat des champs se rencontrèrent et partagèrent leur repas.
Le loup et l'agneau se désaltéraient au même ruisseau, le loup chercha une querelle à l'agneau.
Le corbeau et le renard se parlèrent longtemps, le renard eut le fromage.
Le chêne et le roseau se disputaient la force, le vent les départagea.
Perrette et le pot au lait, la poule aux œufs d'or, le coq et le renard.
Le savetier et le financier, le sage et le fou, le laboureur et ses enfants.
""".strip()

# Corpus B (juridico-technique) : texte original, redige pour ce notebook.
REGISTRE_TEXT = """
La CNIL a vérifié en mars 2024 que la société Acme SAS conservait les données personnelles
de ses clients européens conformément au RGPD et à la loi Informatique et Libertés.
Le registre des traitements mentionne la finalité, la base légale et la durée de conservation
de chaque catégorie d'informations. La durée maximale de conservation des historiques
d'achat a été fixée à trois ans après le dernier contact commercial.
Les personnes concernées peuvent exercer leurs droits d'accès, de rectification
et d'effacement auprès du délégué à la protection des données, par courriel
à l'adresse dpo@acme.example. En cas de violation de données, Acme SAS notifie
la CNIL dans les soixante-douze heures et informe les clients concernés.
Les sous-traitants hébergeant les données en dehors de l'Union européenne
doivent garantir un niveau de protection adéquat selon les clauses contractuelles types.
""".strip()

CORPUS = {"littéraire (fables)": FABLES_TEXT, "juridique (registre RGPD)": REGISTRE_TEXT}
for nom, texte in CORPUS.items():
    mots = len(texte.split())
    print(f"{nom:28s} {mots:4d} mots-espace, {len(texte)} caractères")
littéraire (fables)           299 mots-espace, 1671 caractères
juridique (registre RGPD)     135 mots-espace, 894 caractères

Lecture des chiffres — deux densités d’écriture

Les comptes affichés se lisent aussi en ratio : 5,6 caractères par mot pour les fables (1 671 / 299) contre 6,6 pour le registre (894 / 135). Le texte juridique est plus « dense par mot » : groupes nominaux composés, référentiels techniques, chiffres — là où le vers s’appuie sur des mots courts, des élisions et la répétition. Ce contraste n’est pas décoratif : il prédit où chaque étage du pipeline va travailler. La flexion riche du littéraire charge la lemmatisation ; la densité référentielle du juridique charge le NER et le parser (phrases longues = arcs plus nombreux). Les erreurs mesurées en section 7 tomberont précisément de part et d’autre de cette ligne de partage.

Lecture du résultat

Le corpus littéraire compte ~21 lignes de prose versifiée retranscrite ; le corpus juridique ~13 lignes denses. Fils rouges : toutes les analyses des sections 2 à 6 portent sur ces deux textes, jamais sur d’autres.

2. Tokenisation en mots — ce que « un mot » veut dire

Avant tout calcul, il faut trancher : « l’odeur » est-il un mot ou deux ? La tokenisation linguistique sépare l’élision (l' + odeur), isole la ponctuation, et distingue les formes fléchies (chantait, chantiez) comme des tokens distincts. C’est le premier écart avec le BPE — qui découpe sous le mot — et avec str.split(), qui ne découpe qu’à l’espace.

import spacy

# Modele installe dans l'environnement (python -m spacy download fr_core_news_sm),
# aucun telechargement au chargement. fr_core_news_sm : pipeline leger, CPU.
nlp = spacy.load("fr_core_news_sm")
print("Pipeline:", nlp.pipe_names)

DOCS = {nom: nlp(texte) for nom, texte in CORPUS.items()}

for nom, doc in DOCS.items():
    toks = [t for t in doc if not t.is_space]
    mots = [t for t in toks if t.is_alpha]
    split = CORPUS[nom].split()
    print(f"{nom}:")
    print(f"  spaCy            : {len(toks):4d} tokens ({len(mots)} alphabétiques), {len(set(t.text.lower() for t in mots)):4d} types")
    print(f"  str.split()      : {len(split):4d} tokens, {len(set(w.lower() for w in split)):4d} types")
    print(f"  écart tokens     : {len(toks) - len(split):+4d} (élisions + ponctuation)")
Pipeline: ['tok2vec', 'morphologizer', 'parser', 'attribute_ruler', 'lemmatizer', 'ner']
littéraire (fables):
  spaCy            :  373 tokens (299 alphabétiques),  186 types
  str.split()      :  299 tokens,  196 types
  écart tokens     :  +74 (élisions + ponctuation)
juridique (registre RGPD):
  spaCy            :  151 tokens (131 alphabétiques),   86 types
  str.split()      :  135 tokens,   91 types
  écart tokens     :  +16 (élisions + ponctuation)

Lecture du pipeline affiché

La première ligne imprimée liste les composants chargés : tok2vec (représentations locales), morphologizer (POS + traits), parser (dépendances), attribute_ruler (règles d’exception), lemmatizer, ner. L’ordre n’est pas décoratif : en français, le lemmatiseur statique de spaCy a besoin du POS pour trancher — témoignage (NOUN) garde sa forme, témoigne (VERB) part vers l’infinitif. C’est une chaîne où chaque étage consomme la sortie du précédent, exactement l’inverse du BPE du notebook 04, qui ne sait rien du mot qu’il découpe. Retenez aussi ce que la liste ne contient pas : aucun composant de génération — ce pipeline analyse, il n’écrit pas.

Lecture du résultat

spaCy produit plus de tokens que str.split() (+74 sur les fables) : chaque signe de ponctuation et chaque élision (l', qu', n') devient un token. Les types (tokens distincts), eux, diminuent (186 contre 196) : dans split(), la ponctuation collée au mot fabrique des faux types (perché, ≠ perché), alors que spaCy l’isole systématiquement. La baisse suivante viendra de la lemmatisation : les formes fléchies du verbe chanter (chantais, chantiez, chanté) comptent pour trois types en surface, pour un seul une fois lemmatisées — c’est ce que la section suivante mesure.

3. Lemmes, parties du discours et traits morphologiques

La lemmatisation ramène chaque forme fléchie à sa forme canonique (tint → tenir, alléché → allécher). L’étiquetage morphosyntaxique (POS) attribue une catégorie à chaque token, et les traits morphologiques précisent genre, nombre, temps, personne.

def table_annot(doc, phrases=2, max_tok=14):
    """Tableau TEXT | LEMMA | POS | MORPH des premiers tokens de chaque phrase."""
    seen = []
    for sent in list(doc.sents)[:phrases]:
        rows = [(t.text, t.lemma_, t.pos_, str(t.morph)) for t in sent if not t.is_space][:max_tok]
        seen.extend(rows)
    return seen

import pandas as pd
rows = table_annot(DOCS["littéraire (fables)"])
display(pd.DataFrame(rows, columns=["TEXT", "LEMMA", "POS", "MORPH"]))
TEXT LEMMA POS MORPH
0 Maître maître NOUN Gender=Masc|Number=Sing
1 Corbeau Corbeau PROPN
2 , , PUNCT
3 sur sur ADP
4 un un DET Definite=Ind|Gender=Masc|Number=Sing|PronType=Art
5 arbre arbre NOUN Gender=Masc|Number=Sing
6 perché percher ADJ Gender=Masc|Number=Sing
7 , , PUNCT
8 tenait tenir VERB Mood=Ind|Number=Sing|Person=3|Tense=Imp|VerbFo...
9 en en ADP
10 son son DET Number=Sing|Poss=Yes
11 bec bec NOUN Gender=Masc|Number=Sing
12 un un DET Definite=Ind|Gender=Masc|Number=Sing|PronType=Art
13 fromage fromage NOUN Gender=Masc|Number=Sing
14 . . PUNCT
15 Maître maître NOUN Gender=Masc|Number=Sing
16 Renard Renard PROPN Gender=Masc|Number=Sing

Lire la colonne MORPH

Les traits morphologiques suivent le standard Universal Dependencies : des paquets clé=valeur séparés par | — Gender=Masc|Number=Sing dit « masculin singulier » en deux traits indépendants. Chaque clé a un inventaire fermé (Number ∈ Sing/Plur, Tense ∈ Pres/Imp/Fut/Past…), ce qui rend les traits comparables entre langues : le même schéma décrit le français, l’allemand ou le turc. C’est l’information que la colonne LEMMA ne porte pas : maître et maîtres partagent un lemme mais diffèrent par Number — et c’est cette granularité qui alimente les accords, la requête « tous les pluriels », ou le filtrage par personne grammaticale. Aucun tokenizer sous-mot ne transporte cet héritage : le BPE découpe chantiez en morceaux muets.

rows_jur = table_annot(DOCS["juridique (registre RGPD)"], phrases=1, max_tok=16)
display(pd.DataFrame(rows_jur, columns=["TEXT", "LEMMA", "POS", "MORPH"]))
TEXT LEMMA POS MORPH
0 La le DET Definite=Def|Gender=Fem|Number=Sing|PronType=Art
1 CNIL cnil NOUN Gender=Fem|Number=Sing
2 a avoir AUX Mood=Ind|Number=Sing|Person=3|Tense=Pres|VerbF...
3 vérifié vérifier VERB Gender=Masc|Number=Sing|Tense=Past|VerbForm=Part
4 en en ADP
5 mars mars NOUN Gender=Masc|Number=Sing
6 2024 2024 NUM NumType=Card
7 que que SCONJ
8 la le DET Definite=Def|Gender=Fem|Number=Sing|PronType=Art
9 société société NOUN Gender=Fem|Number=Sing
10 Acme Acme PROPN Gender=Masc|Number=Sing
11 SAS SAS PROPN
12 conservait conserver VERB Mood=Ind|Number=Sing|Person=3|Tense=Imp|VerbFo...
13 les le DET Definite=Def|Number=Plur|PronType=Art
14 données donnée NOUN Gender=Fem|Number=Plur
15 personnelles personnel ADJ Gender=Fem|Number=Plur

Lecture du résultat

Sur le corpus littéraire : tint → tenir, alléché → allécher, êtes → être — la lemmatisation traverse l’irrégularité des verbes du 3ᵉ groupe. Sur le corpus juridique : conservait → conserver avec Number=Sing|Person=3|Tense=Imp, européens → européen avec Number=Plur. Les traits Gender=Fem|Number=Sing sur la durée préparent les accords — information qu’aucun tokenizer BPE ne transporte.

4. Dépendances syntaxiques — qui dépend de qui

L’analyse en dépendances relie chaque token à son gouverneur par une relation typée (nsubj, obj, obl:mod, det…). C’est la structure qui permet d’extraire des tripleaux sujet-verbe-objet, indépendamment de l’ordre des mots.

Dépendances vs constituance — le choix de structure

Deux traditions structurent une phrase. La constituance emboîte des groupes (le groupe nominal dans le groupe verbal dans la phrase) — c’est l’arbre de l’école. L’analyse en dépendances relie des mots par des arcs étiquetés : chaque mot a un gouverneur, la racine exceptée. Pour le français à ordre relativement libre — La CNIL a vérifié / A vérifié, la CNIL… — les arcs survivent là où l’emboîtement positionnel se casse : nsubj reste l’arc de CNIL vers vérifié quelle que soit la place. C’est aussi le format exploitable par programme : les tripleaux de la cellule suivante ne sont qu’une projection de ces arcs, et les nœuds d’un GraphRAG sont des lemmes reliés par exactement ces relations typées.

from spacy import displacy
from IPython.display import HTML, display

# Deux phrases exemplaires : une fable, une phrase juridique longue.
sent_fable = next(DOCS["littéraire (fables)"].sents)
sent_jur = next(DOCS["juridique (registre RGPD)"].sents)

for s in (sent_fable, sent_jur):
    svg = displacy.render(s, style="dep", jupyter=False, options={"collapse_punct": True, "compact": True})
    display(HTML(svg))
Maître NOUN Corbeau, PROPN flat:name
La DET CNIL NOUN a AUX vérifié VERB en ADP mars NOUN 2024 NUM que SCONJ la DET société NOUN Acme PROPN SAS PROPN conservait VERB les DET données NOUN personnelles ADJ SPACE de ADP ses DET clients NOUN européens ADJ conformément ADV au ADP RGPD PROPN et CCONJ à ADP la DET loi NOUN Informatique PROPN et CCONJ Libertés. PUNCT SPACE det nsubj aux:tense case obl:mod nmod mark det nsubj appos flat:name ccomp det obj amod dep case det obl:arg amod advmod case obl:arg cc case det conj amod cc punct dep

Lire les arcs du rendu visuel

Le rendu HTML ci-dessus se lit avec une convention fixe : la flèche part du gouverneur et pointe vers le dépendant, l’étiquette sur l’arc nomme la relation. Attention à l’écueil classique de première lecture : sur Maître Corbeau, l’arc relie les deux mots en partant du gouverneur (le nom propre Corbeau, tête du groupe) vers son dépendant Maître — la flèche va du gouverneur au dépendant, pas dans l’ordre de lecture. La distance des arcs est elle-même un signal : des arcs longs et croisés signalent des phrases chargées (incises, dislocations) — le juridique en regorge. Avant de lire les tripleaux extraits qui suivent, prenez dix secondes sur ce rendu : l’œil forme l’intuition que la cellule suivante formalise.

def svo(doc):
    """Tripleaux (sujet, verbe, objet) detectes par les relations de dependance."""
    out = []
    for t in doc:
        if t.pos_ == "VERB":
            sujets = [w.text for w in t.children if w.dep_ in ("nsubj", "nsubj:pass")]
            objets = [w.text for w in t.children if w.dep_ in ("obj", "iobj")]
            if sujets or objets:
                out.append((" ".join(sujets) or "—", t.lemma_, " ".join(objets) or "—"))
    return out

for nom, doc in DOCS.items():
    print(f"--- {nom}")
    for t in svo(doc)[:8]:
        print("   ", t)
--- littéraire (fables)
    ('—', 'tenir', 'fromage Maître')
    ('—', 'tenir', 'lui')
    ('vous', 'sembler', 'me beau')
    ('ramage', 'rapporter', '—')
    ('on', 'prendre', "l' y")
    ('cigale', 'chanter', 'été')
    ('bise', 'venir', '—')
    ('elle', 'aller', '—')
--- juridique (registre RGPD)
    ('CNIL', 'vérifier', '—')
    ('société', 'conserver', 'données')
    ('—', 'mentionner', 'finalité')
    ('durée', 'fixer', '—')
    ('personnes', 'pouvoir', '—')
    ('—', 'exercer', 'droits')
    ('Acme', 'notifier', 'CNIL')
    ('—', 'héberger', 'données')

Lecture du résultat

Les tripleaux du corpus juridique sont denses et réguliers (CNIL | vérifier | —, Acme | notifier | CNIL) : la voix passive a été fixée produit un nsubj:pass (durée | fixer | —). Sur les fables, les phrases coordonnées sans sujet explicite (— | tenir | fromage Maître : le sujet de tint est le renard de la phrase précédente) illustrent les sujets elliptiques du français — un phénomène que l’exercice 2 fera chercher systématiquement.

5. Entités nommées — les référents du texte

Le NER (named entity recognition) étiquette les mentions de personnes (PER), lieux (LOC), organisations (ORG), montants, dates et divers (MISC). Nos deux domaines exercent des comportements opposés : le corpus juridique contient des entités réelles (CNIL, RGPD, Acme SAS, mars 2024) ; les fables contiennent des entités fictionnelles (Maître Corbeau) et des majuscules non-entités (Hé, Nuit).

from collections import Counter

for nom, doc in DOCS.items():
    ents = [(e.text, e.label_) for e in doc.ents]
    par_label = Counter(l for _, l in ents)
    print(f"--- {nom} : {len(ents)} entités")
    for lbl, n in par_label.most_common():
        print(f"    {lbl:8s} ×{n} : {[t for t, l in ents if l == lbl][:6]}")
--- littéraire (fables) : 6 entités
    PER      ×3 : ['Corbeau', 'Maître Renard', 'Perrette']
    LOC      ×2 : ['le Phénix', 'jura']
    MISC     ×1 : ['Monsieur du Corbeau']
--- juridique (registre RGPD) : 8 entités
    ORG      ×5 : ['CNIL', 'Acme SAS', 'RGPD', 'CNIL', 'Union européenne']
    PER      ×2 : ['Libertés', 'Acme SAS']
    LOC      ×1 : ['Informatique']

Lecture des étiquettes — une taxonomie à quatre cases

Les comptes par étiquette racontent la nature de chaque corpus. Fables : PER ×3 (Corbeau, Maître Renard, Perrette), LOC ×2, MISC ×1 — des référents fictionnels, que le modèle traite pourtant comme des personnes réelles. Registre : ORG ×5 domine (CNIL, RGPD, Union européenne…) — c’est la signature d’un texte institutionnel. Deux enseignements : (1) le NER ne sait pas qu’une fable est une fable — il applique ses régularités d’entraînement (nom propre majuscule = personne) indépendamment de la fiction ; (2) les étiquettes sont exclusives par token : Acme SAS doit choisir entre ORG et PER à chaque occurrence — et la section 7 montrera qu’il choisit les deux selon l’occurrence. L’instabilité annoncée est déjà lisible dans ces deux listes.

# Rendu surligne des entites du registre RGPD (meme moteur que les arcs de la section 4).
svg_ents = displacy.render(DOCS["juridique (registre RGPD)"], style="ent", jupyter=False)
display(HTML(svg_ents.replace("\n", " ")))
La CNIL ORG a vérifié en mars 2024 que la société Acme SAS ORG conservait les données personnelles
de ses clients européens conformément au RGPD ORG et à la loi Informatique LOC et Libertés PER .
Le registre des traitements mentionne la finalité, la base légale et la durée de conservation
de chaque catégorie d'informations. La durée maximale de conservation des historiques
d'achat a été fixée à trois ans après le dernier contact commercial.
Les personnes concernées peuvent exercer leurs droits d'accès, de rectification
et d'effacement auprès du délégué à la protection des données, par courriel
à l'adresse dpo@acme.example. En cas de violation de données, Acme SAS PER notifie
la CNIL ORG dans les soixante-douze heures et informe les clients concernés.
Les sous-traitants hébergeant les données en dehors de l' Union européenne ORG
doivent garantir un niveau de protection adéquat selon les clauses contractuelles types.

Lecture du résultat

À confronter à la section 7 (analyse d’erreurs) : le modèle détecte les organisations du registre (CNIL, Acme SAS, RGPD), mais — typique d’un pipeline sm entraîné sur de la prose journalistique — il est erratique dans les zones grises : Acme SAS étiquetée ORG à une occurrence et PER à l’autre ; Libertés (de la loi Informatique et Libertés) vue comme une personne et Informatique comme un lieu ; sur les fables, le verbe jura étiqueté LOC et les personnages fictionnels (Corbeau, Maître Renard) vus comme des personnes réelles. Chaque affirmation de cette lecture se vérifie dans la section suivante contre un jeu d’or annoté à la main.

6. Mots vs BPE — la comparaison frontale

Le notebook 04 construisait un BPE from scratch sur ce même corpus de fables et mesurait le compromis taille de vocabulaire ↔︎ tokens par mot. Ici nous reconstruisons ce mini-BPE (même algorithme, mêmes données) et comparons les trois représentations : mots spaCy, mots split(), sous-mots BPE.

import re, collections

# Mini-BPE identique a la pedagogie du notebook 04 : fusions par paires les plus frequentes.
WORDS = re.findall(r"[a-zà-ÿ'éèêëïôöûüç0-9-]+", FABLES_TEXT.lower())

def train_bpe(words, n_merges):
    seqs = [list(w) for w in words]
    merges = []
    for _ in range(n_merges):
        pairs = collections.Counter()
        for s in seqs:
            for a, b in zip(s, s[1:]):
                pairs[(a, b)] += 1
        if not pairs:
            break
        best = max(pairs, key=pairs.get)
        merges.append(best)
        seqs = [[a + b[1:] if (a, b) == best and j < len(s) - 1 and (s[j], s[j+1]) == best else s[j]
                 for j in range(len(s)) if not (j < len(s) - 1 and (s[j], s[j+1]) == best)]
                for s in seqs]
    vocab = {tok for s in seqs for tok in s}
    return merges, vocab, seqs

merges, vocab_bpe, seqs = train_bpe(WORDS, 60)

mots_spacy = [t.text.lower() for t in DOCS["littéraire (fables)"] if t.is_alpha]
lemmes = [t.lemma_.lower() for t in DOCS["littéraire (fables)"] if t.is_alpha]

print(f"Tokens mots (spaCy, formes) : {len(mots_spacy):4d} tokens, {len(set(mots_spacy)):4d} types")
print(f"Tokens mots (lemmes)        : {len(lemmes):4d} tokens, {len(set(lemmes)):4d} types")
print(f"Sous-mots BPE (60 fusions)  : {sum(len(s) for s in seqs):4d} tokens, {len(vocab_bpe):4d} types")

# Le mot emblematique : decoupage BPE vs lemme.
exemple = "anticonstitutionnellement"
print(f"\n{exemple!r} : {len(exemple)} caractères = 1 lemme = {len(train_bpe([exemple]*1, 0)[2][0])} sous-mots BPE non fusionnés")
Tokens mots (spaCy, formes) :  299 tokens,  186 types
Tokens mots (lemmes)        :  299 tokens,  167 types
Sous-mots BPE (60 fusions)  :  658 tokens,   33 types

'anticonstitutionnellement' : 25 caractères = 1 lemme = 25 sous-mots BPE non fusionnés

Token, type, vocabulaire — la métrique de la comparaison

La sortie distingue tokens (occurrences, 299) et types (formes distinctes, 186) : le vocabulaire est le nombre de types, pas de tokens. Pourquoi ce compte gouverne tout : un modèle à vocabulaire V attribue une entrée d’embedding à chaque type — la taille de la matrice, donc du modèle, est indexée sur V, pas sur la longueur du texte. Les lignes affichées comparent trois stratégies face à V : la lemmatisation (167 types, −10 %) compresse par connaissance linguistique ; le BPE (33 types à 60 fusions) compresse par fréquence statistique, au prix visible sur anticonstitutionnellement — 25 caractères, 1 lemme, 25 sous-mots : le mot rare est entièrement pulvérisé parce qu’aucune de ses paires n’a mérité une fusion.

Lecture du résultat

Trois représentations, trois compromis mesurés sur le même texte :

  • mots (formes) : vocabulaire le plus large — 186 types, chaque flexion compte ;
  • lemmes : 167 types (−10 %) — la lemmatisation compresse le vocabulaire, mais par connaissance linguistique (un seul type pour chantais/chantiez/chanté) au lieu de fréquence statistique ;
  • BPE : aucun mot inconnu (un mot nouveau se décompose en sous-mots vus), au prix d’une perte totale des frontières de mots et des traits morphologiques.

Le pont avec le notebook 04 : le BPE répond à « comment tout encoder sans rien connaître » ; le pipeline TAL répond à « quoi dire de ce que l’on a encodé ». Les systèmes de recherche et de GraphRAG (cas d’usage, section 8) utilisent le second sur le texte une fois retrouvé, le premier pour retrouver le texte.

7. Analyse d’erreurs — mesurer contre un jeu d’or

Un modèle sm (léger, CPU) fait des erreurs ; l’important est de les mesurer. Nous annotons à la main un petit jeu d’or sur les phénomènes annoncés, puis comparons annotation et prédiction, phénomène par phénomène.

Le protocole du jeu d’or — mesurer avant d’affirmer

La méthode de cette section est un mini-contrôle qualité : un petit ensemble d’items annotés à la main (le jeu d’or), chaque item ciblant un phénomène précis — lemmatisation irrégulière, entité datée, organisation. Trois garde-fous la rendent honnête : (1) l’annotation est faite avant de regarder la prédiction, sinon on rationalise ; (2) le phénomène est nommé par item, ce qui donne un taux par phénomène — 8/8 quelque part et 2/5 ailleurs ne se moyennent pas en « 67 % » ; (3) le corpus est petit mais adversarial — les items choisis sont les zones grises connues (élisions, fiction, dates immergées). C’est exactement le protocole qu’il faudra appliquer à toute affirmation de qualité d’un composant NLP, y compris en production.

# Jeu d'or annote a la main (gold) : (token/entite, phenomene, valeur attendue).
GOLD_LEMMES = [("tint", "tenir"), ("alléché", "allécher"), ("êtes", "être"),
               ("chantiez", "chanter"), ("désaltéraient", "désaltérer"),
               ("conservait", "conserver"), ("vérifié", "vérifier"), ("européens", "européen")]
GOLD_NER_JUR = [("CNIL", "ORG"), ("Acme SAS", "ORG"), ("RGPD", "ORG"),
                ("mars 2024", "DATE"), ("Union européenne", "LOC")]

doc_jur = DOCS["juridique (registre RGPD)"]
doc_fab = DOCS["littéraire (fables)"]
lemmas_all = {t.text: t.lemma_ for doc in DOCS.values() for t in doc}
ner_jur_text = {e.text: e.label_ for e in doc_jur.ents}

ok_l = sum(1 for tok, gold in GOLD_LEMMES if lemmas_all.get(tok) == gold)
print(f"Lemmatisation : {ok_l}/{len(GOLD_LEMMES)} corrects")
for tok, gold in GOLD_LEMMES:
    pred = lemmas_all.get(tok, "—")
    print(f"   {tok:16s} → {pred:14s} (attendu {gold}) {'OK' if pred == gold else 'ECHEC'}")

ok_n = 0
print(f"\nNER juridique : entités prédites = {len(ner_jur_text)}")
for ent, gold in GOLD_NER_JUR:
    pred = ner_jur_text.get(ent)
    verdict = "OK" if pred == gold else ("manquée" if pred is None else f"confondue ({pred})")
    ok_n += pred == gold
    print(f"   {ent:22s} → {str(pred):6s} (attendu {gold}) {verdict}")
print(f"NER : {ok_n}/{len(GOLD_NER_JUR)} strictement corrects")
Lemmatisation : 8/8 corrects
   tint             → tenir          (attendu tenir) OK
   alléché          → allécher       (attendu allécher) OK
   êtes             → être           (attendu être) OK
   chantiez         → chanter        (attendu chanter) OK
   désaltéraient    → désaltérer     (attendu désaltérer) OK
   conservait       → conserver      (attendu conserver) OK
   vérifié          → vérifier       (attendu vérifier) OK
   européens        → européen       (attendu européen) OK

NER juridique : entités prédites = 6
   CNIL                   → ORG    (attendu ORG) OK
   Acme SAS               → PER    (attendu ORG) confondue (PER)
   RGPD                   → ORG    (attendu ORG) OK
   mars 2024              → None   (attendu DATE) manquée
   Union européenne       → ORG    (attendu LOC) confondue (ORG)
NER : 2/5 strictement corrects

Lecture du résultat

Le verdict est mesuré, pas affirmé : la lemmatisation réussit sur la totalité du jeu d’or (verbes irréguliers du 3ᵉ groupe inclus), tandis que le NER n’obtient que 2/5 — mars 2024 plongée dans un groupe prépositionnel n’est pas détectée comme DATE, Union européenne est étiquetée ORG alors que le jeu d’or attend LOC, et Acme SAS est PER à cette occurrence alors qu’elle était ORG à la première. Conclusion opérationnelle : pour de la recherche ou de l’indexation, les lemmes d’un pipeline sm sont fiables ; pour du NER fin, un pipeline md/trf serait nécessaire — c’est un choix documenté, pas une lacune cachée (cf. §H : le pipeline installé est celui que le notebook exécute réellement).

8. Cas d’usage borné — la recherche lemmatisée

Question pratique : une requête conserver doit-elle retrouver conservait ? Une requête donnée (singulier) doit-elle retrouver données (pluriel) ? Avec des mots surface, non. Avec un index de lemmes, oui. C’est le mécanisme qui sous-tend la recherche juridique, la veille et le GraphRAG (les nœuds du graphe portent des lemmes, pas des flexions).

def index_lemmes(doc):
    """Index lemme -> formes surface observees (positions)."""
    idx = collections.defaultdict(set)
    for t in doc:
        if t.is_alpha:
            idx[t.lemma_.lower()].add(t.text.lower())
    return idx

IDX_JUR = index_lemmes(doc_jur)

for requete in ["conserver", "donnée", "protection", "vérifier"]:
    formes = IDX_JUR.get(requete)
    print(f"requête {requete!r:14s} → {sorted(formes) if formes else 'AUCUN match'}")

# Contre-preuve : recherche surface (formes exactes, ponctuation retiree) sur le meme corpus.
mots_surface = [m.strip('.,;:') for m in REGISTRE_TEXT.lower().split()]
for requete in ["conserver", "donnée"]:
    n_surface = sum(1 for m in mots_surface if m == requete)
    n_lemmes = len(IDX_JUR.get(requete, set()))
    print(f"{requete:12s}: surface {n_surface} forme(s) exacte(s) | lemmatisé {n_lemmes} forme(s) fléchie(s)")
requête 'conserver'    → ['conservait']
requête 'donnée'       → ['données']
requête 'protection'   → ['protection']
requête 'vérifier'     → ['vérifié']
conserver   : surface 0 forme(s) exacte(s) | lemmatisé 1 forme(s) fléchie(s)
donnée      : surface 0 forme(s) exacte(s) | lemmatisé 1 forme(s) fléchie(s)

Lecture du résultat

La requête lemmatisée conserver retrouve la forme fléchie conservait, et donnée retrouve le pluriel données — la recherche surface, elle, trouve zéro forme exacte pour les deux requêtes (0 occurrence de conserver et 0 de donnée singulier dans le registre). Limite mesurable de l’autre côté : conservation reste un lemme séparé — la lemmatisation ne dérive pas le nom depuis le verbe, elle généralise à l’intérieur du paradigme de chaque lemme. C’est la complémentarité de la section 6 : le BPE garantit la couverture de tout mot, le lemme garantit la généralisation à l’intérieur du lexique connu.

9. Exercices

Des exercices sur les données du notebook. Le notebook doit s’exécuter de bout en bout : les stubs ne lèvent jamais d’erreur (règle C.1) — ils affichent un message et rendent None.

Comment attaquer les exercices

Les exercices se résolvent avec la machinerie déjà exposée, sans rien importer de neuf. L’exercice 1 recycle l’index inverse de la section 8 (lemme → formes surface) — la question est juste de trier par longueur de liste. L’exercice 2 parcourt l’arbre de dépendances : pour chaque token VERB, regarder ses children et chercher un arc dont l’étiquette commence par nsubj — c’est la même information que le — des tripleaux, vue du mot plutôt que de la phrase. L’exercice 3 est une variante de clé de la recherche de la section 8 : même dictionnaire, mais l’étiquette morphosyntaxique joue le rôle que jouait le lemme. Aucune réponse n’est donnée ici — seulement le chemin vers les outils qui la produisent.

Exercice 1 — le lemme le plus polymorphe

Quel lemme du corpus littéraire apparaît sous le plus de formes surface distinctes ? Complétez top_lemmes pour construire l’index inverse de la section 8 sur doc_fab et afficher le top-3. Indice : collections.Counter sur idx[lemme] par longueur.

# Exercice 1 : top-3 des lemmes par nombre de formes surface distinctes (corpus litteraire).
def top_lemmes(doc, k=3):
    # Etape 1 : construire l'index lemme -> formes (cf. index_lemmes, section 8).
    # Etape 2 : trier par nombre de formes decroissant, garder les k premiers.
    # Etape 3 : retourner la liste [(lemme, formes_triees)].
    # TODO etudiant
    print("Exercice a completer")
    return None

top_lemmes(doc_fab)
Exercice a completer

Exercice 2 — les sujets elliptiques du corpus juridique

La fonction svo (section 4) rend — quand aucun sujet n’est détecté. Complétez phrases_sans_sujet pour lister les verbes conjugués sans nsubj du corpus juridique (t.pos_ == "VERB" et aucun enfant nsubj*). Indice : itérer sur doc_jur puis sur t.children.

# Exercice 2 : verbes conjuges sans sujet explicite (corpus juridique).
def phrases_sans_sujet(doc):
    # Etape 1 : pour chaque token VERB, examiner ses enfants.
    # Etape 2 : si aucun enfant n'a dep_ commencant par "nsubj", retenir le token.
    # Etape 3 : retourner la liste des (texte du verbe, lemme).
    # TODO etudiant
    print("Exercice a completer")
    return None

phrases_sans_sujet(doc_jur)
Exercice a completer

Exercice 3 — recherche par étiquette morphosyntaxique

Étendez la recherche de la section 8 : une fonction recherche_par_pos(doc, pos) qui retourne tous les tokens du POS donné (par ex. "ADJ") groupés par lemme. Indice : même index que la section 8, mais clé = t.pos_ puis t.lemma_.

# Exercice 3 : recherche par POS, groupement par lemme.
def recherche_par_pos(doc, pos):
    # Etape 1 : filtrer les tokens du POS demande (t.pos_ == pos).
    # Etape 2 : les regrouper par lemme.
    # Etape 3 : retourner un dict {lemme: [formes surface]}.
    # TODO etudiant
    print("Exercice a completer")
    return None

recherche_par_pos(doc_jur, "ADJ")
Exercice a completer

Conclusion

Le pipeline linguistique français exécuté ici — tokenisation, lemmes, POS, morphologie, dépendances, NER — est la face analyse du traitement du langage, là où le BPE du notebook 04 est sa face encodage. Les mesures de ce notebook :

  • la lemmatisation fiable à 8/8 sur le jeu d’or (verbes irréguliers inclus) — utilisable en production d’index ;
  • le NER d’un pipeline sm mesuré limité (2/5 : dates non détectées, étiquettes ORG/PER/LOC instables d’une occurrence à l’autre) — choix documenté, upgrade md/trf possible ;
  • la recherche lemmatisée démontrée supérieure à la recherche surface sur le registre RGPD : elle retrouve flexions verbales (conservait) et pluriels (données) que la forme exacte ne matche pas.

Ponts : RAG-et-Memoire-Semantique/04-Tokenisation-From-Scratch.ipynb (l’autre moitié de la comparaison), série RAG-et-Memoire-Semantique (où les lemmes alimentent le chunking), et le futur axe GraphRAG (nœuds lemmatisés) cité en §8.

Aller plus loin — l’échelle des pipelines et les ponts

L’échelle spaCy se lit comme un compromis coût/qualité mesurable : sm (léger, CPU, lemmes fiables, NER limité) → md → trf (transformer, précis, GPU souhaitable) — la section 7 a chiffré exactement ce que sm vaut et ne vaut pas. Trois directions naturelles depuis ce notebook : la désambiguïsation lexicale (le même souris verbe/nom se tranche par POS + contexte) ; le GraphRAG, dont les nœuds sont des lemmes et les arêtes des relations de dépendance — la section 8 en était l’embryon d’index ; et le pont vers le notebook 04 : BPE et TAL ne sont pas concurrents mais étages complémentaires — l’un encode tout sans rien comprendre, l’autre comprend peu mais transporte la structure. Les systèmes modernes vivent exactement à l’interface des deux.

Retour au sommet