0%
Traitement du Langage Naturel (NLP)

NLP

Du texte brut aux grands modèles de langue.

10-15 min

Traitement du Langage Naturel (NLP)

Un modèle ne sait manipuler que des nombres. Le texte, lui, est une suite de caractères dont le sens dépend de l’ordre, du contexte, de l’implicite et parfois de ce qui n’est pas écrit. Entre ces deux mondes, il faut construire un pont — et la façon dont ce pont est construit détermine presque tout ce que le système saura faire, et tout ce qu’il ratera.

C’est ce qui explique des comportements déroutants. Un modèle de langue capable de rédiger une dissertation nuancée peut se tromper en comptant les lettres d’un mot. Il peut affirmer avec un aplomb parfait une référence bibliographique qui n’a jamais existé. Ces échecs paraissent aléatoires ; ils ne le sont pas. Chacun se déduit d’un choix de conception précis, situé quelque part sur le pont entre les caractères et les nombres.

Cette leçon parcourt ce pont dans l’ordre : découper le texte, transformer les morceaux en vecteurs, faire en sorte que chaque morceau tienne compte des autres. Ce troisième point est l’attention, l’idée qui a fait basculer le domaine en 2017 et sur laquelle repose tout ce qui s’appelle aujourd’hui « grand modèle de langue ». Nous la coderons en cinq lignes de NumPy.

Nous terminerons par le mécanisme de l’hallucination. Non pas pour la déplorer, mais pour montrer qu’elle est la conséquence directe et prévisible de l’objectif que l’on donne à ces modèles.

Découper le texte : la tokenisation

Première décision : quelle est l’unité élémentaire ? La réponse évidente — le mot, séparé par des espaces — révèle vite ses limites.

texte = "Le chat dort. Les chats dorment, le chaton dort aussi !"

mots = texte.lower().replace(".", " ").replace(",", " ").replace("!", " ").split()
print(mots)
# ['le', 'chat', 'dort', 'les', 'chats', 'dorment', 'le', 'chaton', 'dort', 'aussi']
print(len(mots), len(set(mots)))   # 10 8

Pour la machine, chat, chats et chaton sont trois symboles sans aucun rapport entre eux — aussi étrangers l’un à l’autre que chat et aussi. Il en va de même pour dort et dorment. Chaque forme fléchie devra être apprise séparément, ce qui multiplie les exemples nécessaires.

Le second problème est le vocabulaire hors liste. Un mot absent du vocabulaire d’entraînement — nom propre, néologisme, faute de frappe — n’a aucune représentation et se retrouve remplacé par un symbole inconnu générique.

C’est pourquoi les modèles actuels découpent en sous-mots. L’algorithme le plus répandu, le Byte Pair Encoding, part des caractères et fusionne itérativement les paires les plus fréquentes du corpus. Le résultat est un compromis : les mots courants restent d’un seul morceau, les mots rares se décomposent en fragments connus.

Découpage en mots        : ["anticonstitutionnellement"]  → inconnu si absent
Découpage en sous-mots   : ["anti", "constitution", "nelle", "ment"]  → tout est connu

Ce choix a une conséquence directe et souvent mal comprise : un modèle de langue ne voit pas les lettres. Il voit des identifiants de fragments. Lui demander combien de fois la lettre « r » apparaît dans un mot revient à lui demander une information qu’il n’a pas sous les yeux, ce qui explique la fragilité bien documentée de ces modèles sur les tâches de comptage de caractères, d’anagrammes ou de rimes.

Pour les approches classiques, on représente ensuite chaque document par un vecteur de comptages :

from sklearn.feature_extraction.text import CountVectorizer

corpus = [
    "le chat dort sur le canape",
    "le chien dort sur le tapis",
    "le chat mange une souris",
    "le chien mange une croquette",
    "la souris mange du fromage",
]

v = CountVectorizer()
M = v.fit_transform(corpus)
print(M.shape)                    # (5, 14)
print(v.get_feature_names_out())
# ['canape' 'chat' 'chien' 'croquette' 'dort' 'du' 'fromage' 'la' 'le'
#  'mange' 'souris' 'sur' 'tapis' 'une']

Cinq phrases très courtes produisent déjà 14 colonnes. Sur un corpus réel, ce vecteur compte des dizaines de milliers de dimensions dont la quasi-totalité valent zéro pour un document donné. Et surtout, cette représentation ignore complètement l’ordre : « le chat mange la souris » et « la souris mange le chat » y sont identiques.

Des mots aux vecteurs : les plongements lexicaux

L’idée qui débloque la situation est linguistique avant d’être mathématique : des mots qui apparaissent dans les mêmes contextes ont des sens voisins. C’est l’hypothèse distributionnelle, et elle se vérifie par le calcul.

import numpy as np

vocab = v.get_feature_names_out().tolist()

# Matrice de co-occurrence : combien de fois deux mots apparaissent
# dans la même phrase.
C = np.zeros((len(vocab), len(vocab)))
for phrase in corpus:
    presents = [vocab.index(m) for m in phrase.split() if m in vocab]
    for i in presents:
        for j in presents:
            if i != j:
                C[i, j] += 1

def cosinus(u, w):
    return float(u @ w / (np.linalg.norm(u) * np.linalg.norm(w)))

i_chat = vocab.index("chat")
i_chien = vocab.index("chien")
i_fromage = vocab.index("fromage")

print(round(cosinus(C[i_chat], C[i_chien]), 3))     # 0.867
print(round(cosinus(C[i_chat], C[i_fromage]), 3))   # 0.258

Sur cinq phrases seulement, chat et chien obtiennent une similarité de 0,867, contre 0,258 entre chat et fromage. Le calcul n’a reçu aucune information sur les animaux : il a simplement constaté que chat et chien sont entourés des mêmes mots (le, dort, sur, mange), alors que fromage fréquente un autre voisinage.

Les plongements lexicaux (word embeddings) industrialisent cette idée. Plutôt que des vecteurs creux de la taille du vocabulaire, on apprend pour chaque mot un vecteur dense de quelques centaines de dimensions, optimisé pour prédire son contexte. C’est le principe de word2vec (Mikolov et coll., 2013) et de GloVe (Pennington et coll., 2014).

La mesure de similarité, elle, reste la même :

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

T = TfidfVectorizer().fit_transform(corpus)
print(cosine_similarity(T[0:1], T).round(2))
# [[1.   0.61 0.38 0.18 0.  ]]

La phrase 0 (« le chat dort sur le canapé ») est proche de la phrase 1 (« le chien dort sur le tapis ») à 0,61, et totalement étrangère à la phrase 4 (« la souris mange du fromage ») avec 0,00. Le pondérateur TF-IDF corrige au passage un défaut du comptage brut : il abaisse le poids des mots présents partout, comme le, qui n’apportent aucune information discriminante.

Une limite subsiste, et elle est de taille : un plongement classique attribue un seul vecteur par mot. Le mot « avocat » a donc la même représentation dans « un avocat au barreau » et « un avocat bien mûr ». Résoudre cela exige de faire dépendre la représentation du contexte, ce qui est exactement l’apport du mécanisme suivant.

L’attention : ne pas regarder tous les mots pareil

L’attention répond à une question simple : pour représenter un mot donné, quels autres mots de la phrase faut-il prendre en compte, et dans quelle proportion ?

Le calcul se fait avec trois objets par mot. Une requête (ce que ce mot cherche), une clé (ce que chaque mot offre) et une valeur (l’information à récupérer). On compare la requête à toutes les clés, on transforme les scores en proportions qui somment à 1, et on en fait une moyenne pondérée des valeurs.

import numpy as np
np.set_printoptions(suppress=True)

mots = ["chat", "souris", "dort"]

# Clés et valeurs illustratives, choisies pour rendre le calcul lisible.
K = np.array([[4.0, 0.0, 0.0, 0.0],     # chat
              [3.0, 2.0, 0.0, 0.0],     # souris
              [0.0, 0.0, 4.0, 0.0]])    # dort
V = np.array([[10.0], [6.0], [1.0]])

Q = np.array([[4.0, 0.0, 0.0, 0.0]])    # la requête cherche un sujet animé

d = K.shape[1]
scores = Q @ K.T / np.sqrt(d)           # division : évite des scores trop étalés
poids = np.exp(scores - scores.max())
poids = poids / poids.sum()             # softmax

print(scores.round(2))       # [[8. 6. 0.]]
print(poids.round(3))        # [[0.881 0.119 0.   ]]
print((poids @ V).round(2))  # [[9.52]]

La lecture est directe : 88,1 % de l’attention va sur chat, 11,9 % sur souris, et pratiquement rien sur dort. La représentation produite, 9,52, est presque celle de chat (10,0), légèrement tirée vers celle de souris (6,0).

Trois propriétés expliquent le succès de ce mécanisme :

  • Les poids sont calculés, pas fixés. Le même mot obtiendra une répartition différente dans une autre phrase. La représentation devient contextuelle, ce qui règle le problème de l’« avocat ».
  • La distance ne coûte rien. Un mot en position 1 peut recevoir toute l’attention d’un mot en position 400 aussi facilement que de son voisin immédiat. Les architectures récurrentes qui précédaient, traitant le texte mot après mot, perdaient progressivement l’information lointaine.
  • Tout est parallélisable. Les scores de tous les mots se calculent en un produit matriciel, donc sur carte graphique. C’est cette propriété, autant que la qualité des résultats, qui a permis le passage à l’échelle.

Les transformeurs et les grands modèles de langue

L’architecture qui empile des couches d’attention s’appelle le transformeur, introduite en 2017 dans l’article « Attention Is All You Need » de Vaswani et coll. Comme l’attention seule ignore les positions, on y ajoute un encodage de position, plus des couches denses classiques et des normalisations.

Un grand modèle de langue est un transformeur entraîné sur une tâche d’une simplicité désarmante : étant donné les jetons précédents, prédire le suivant. Aucune annotation humaine n’est requise, le texte est sa propre étiquette, ce qui permet de s’entraîner sur des corpus gigantesques.

Quelques ordres de grandeur documentés, utiles pour situer les échelles :

ModèleAnnéeNombre de paramètres
BERT-base2018environ 110 millions
BERT-large2018environ 340 millions
GPT-32020175 milliards

Ces chiffres proviennent des articles de publication respectifs. Les modèles commerciaux récents ne publient plus systématiquement cette information : en l’absence de source, mieux vaut ne rien avancer.

Le fait remarquable, et qui n’était pas anticipé, est que cet objectif étroit — prédire le mot suivant — produit des capacités qui n’ont pas été programmées : traduire, résumer, reformuler, suivre une consigne. Il faut cependant rester précis sur ce qui est établi. Ce qui est établi, c’est la performance mesurée sur des tâches d’évaluation. Ce qui reste débattu, c’est l’interprétation : savoir si ces capacités relèvent d’une forme de généralisation ou d’une interpolation très fine à partir d’un corpus immense est une question ouverte, et toute affirmation catégorique dans un sens ou dans l’autre dépasse ce que les données permettent d’établir.

Pourquoi un modèle de langue hallucine

Le mécanisme se démontre sur un modèle minuscule. Un modèle de bigrammes ne retient qu’une chose : pour chaque mot, la liste des mots qui l’ont suivi dans le corpus. Puis il génère en tirant au hasard dans cette liste.

import random
from collections import defaultdict

texte = ("le chat dort sur le canape le chien dort sur le tapis "
         "le chat mange une souris le chien mange une croquette")
mots = texte.split()

suivants = defaultdict(list)
for a, b in zip(mots, mots[1:]):
    suivants[a].append(b)

random.seed(3)
mot = "le"
phrase = [mot]
for _ in range(11):
    if mot not in suivants:
        break
    mot = random.choice(suivants[mot])
    phrase.append(mot)

print(" ".join(phrase))
# le canape le chien mange une souris le chien dort sur le

Le résultat est presque du français. Les enchaînements locaux sont irréprochables : « le chien mange une souris » est une phrase parfaitement formée. Elle est aussi fausse par rapport au corpus, où le chien mange une croquette et où c’est le chat qui mange la souris. Le modèle n’a pas menti et ne s’est pas trompé au sens d’un bogue : il a produit une continuation statistiquement plausible, ce qui est exactement, et uniquement, ce qu’on lui a demandé.

Voilà l’hallucination, à l’échelle de vingt mots de vocabulaire. Un modèle de plusieurs milliards de paramètres fait la même chose avec une fluidité incomparablement supérieure, ce qui rend l’erreur beaucoup plus difficile à repérer. La différence de taille change la qualité de la surface, pas la nature de l’objectif.

Quatre conséquences pratiques en découlent, toutes vérifiables :

  • La fluidité n’est pas un indice de fiabilité. Une référence inventée est formulée avec la même assurance qu’une référence exacte, parce que les deux sont produites par le même processus.
  • Il n’y a pas de source. Le modèle ne recopie pas une base de connaissances consultable ; il produit une suite de jetons. Demander « d’où vient cette information ? » obtient une réponse générée, pas une trace.
  • La fenêtre de contexte est finie. Au-delà d’une certaine longueur, les jetons anciens sortent du contexte pris en compte.
  • Les correctifs atténuent sans supprimer. Fournir des documents en contexte et exiger des citations vérifiables — ce que l’on appelle la génération augmentée par récupération — réduit fortement le phénomène en ancrant la réponse sur des textes réels. À ce jour, aucune technique publiée ne l’élimine complètement.

La bonne posture d’ingénierie en découle : traiter la sortie d’un modèle de langue comme une proposition à vérifier, et non comme un résultat. Partout où l’exactitude est contractuelle, la vérification doit être extérieure au modèle.

À retenir

  • La tokenisation est le premier choix de conception et il a des effets durables. Le découpage en sous-mots règle le problème des mots inconnus, mais fait qu’un modèle ne voit jamais les lettres — d’où sa fragilité sur le comptage de caractères.
  • L’hypothèse distributionnelle se vérifie par le calcul : sur cinq phrases, chat et chien atteignent 0,867 de similarité contre 0,258 entre chat et fromage, sans qu’aucune connaissance zoologique n’ait été fournie.
  • L’attention calcule, pour chaque mot, la proportion d’attention à accorder aux autres. Elle rend la représentation dépendante du contexte, supprime le coût de la distance et se calcule en parallèle.
  • Un grand modèle de langue est un transformeur entraîné à prédire le jeton suivant. Cet objectif unique produit des capacités non programmées ; leur interprétation théorique reste, elle, débattue.
  • L’hallucination n’est pas une panne mais la conséquence de l’objectif : le modèle optimise la plausibilité, pas la vérité. Un modèle de bigrammes de vingt mots la reproduit déjà. Toute sortie destinée à un usage sérieux doit être vérifiée en dehors du modèle.

Commentaires

Les commentaires sont alimentés par GitHub Discussions

Connectez-vous avec GitHub pour participer à la discussion

Lien copié !