Loading
Neuf entrées du vocabulaire ne survivaient pas à la frappe. Le bug est resté invisible des semaines : la sortie semblait parfaitement raisonnable.
Ouvrir l'instrumentPendant des semaines, Mind Map a dessiné des arcs d'attention pour une phrase que personne n'avait écrite.
Si vous tapiez It costs £5, le modèle ne recevait pas £5. Il recevait £ puis 5, deux choses distinctes sans plus de lien qu'une paire de voisins quelconque. Le vocabulaire contient pourtant une entrée pour £5 — la ligne 27 813, présente depuis 2018 — et le tokenizer passait devant sans la voir.
Rien ne paraissait anormal. L'interface affichait des mots, les arcs portaient des poids plausibles, les nombres se sommaient correctement. L'arithmétique en aval était irréprochable. C'était une arithmétique irréprochable appliquée à la mauvaise entrée : la forme la plus coûteuse d'exactitude.
L'idée décisive : un modèle ne lit pas votre phrase. Il lit une liste d'entiers, et tout ce qu'il pourra jamais remarquer a été décidé avant l'exécution de la première couche.
“hens” → 2 fragments. Voici ce que le modèle reçoit. Pas le mot que vous avez tapé.
Sortie réelle du tokenizer WordPiece livré, face au vocabulaire livré de 30 522 entrées. Un fragment marqué ## est une continuation : il ne peut que s'attacher au fragment précédent.
Entre votre clavier et la première multiplication matricielle se glisse une étape de traduction, plus lossy et plus étrange que presque personne ne l'imagine.
BERT n'a pas un mot pour chaque mot. Il possède exactement 30 522 entrées, figées à l'entraînement et immuables ensuite. Toute phrase, dans toute langue que vous pourriez taper, doit s'exprimer dans ces 30 522 symboles — comme une presse typographique dont la casse fixe doit composer n'importe quel livre avec les caractères qu'elle possède.
Quand un mot est dans la casse, il est utilisé entier. Sinon, il est reconstruit à partir de fragments.
La règle de reconstruction s'appelle WordPiece et elle est d'une simplicité brutale : prendre depuis le début le plus long fragment qui existe dans le vocabulaire, le détacher, recommencer sur le reste. Chaque fragment après le premier s'écrit avec le préfixe ##, qui le marque comme une continuation — un morceau qui ne peut que s'attacher à ce qui précède.
Cette règle gloutonne explique pourquoi les fragments coïncident si rarement avec les syllabes que vous auriez choisies :
| Vous tapez | Le modèle reçoit |
|---|---|
hens | hen + ##s |
unopened | uno + ##pen + ##ed |
syringe | sy + ##ring + ##e |
tokenization | token + ##ization |
hen + ##s est une décomposition qu'un lecteur approuverait. uno + ##pen + ##ed ne l'est pas : « unopened » n'est en aucun sens construit à partir de uno et pen. Le tokenizer ne cherche pas du sens. Il cherche à couvrir la chaîne avec le moins de fragments possible, les plus longs disponibles, et il tendra volontiers uno et pen au modèle parce que ces morceaux couvrent les caractères.
La conséquence mérite qu'on s'y arrête : le modèle ne voit jamais le mot « unopened ». Il voit trois fragments et doit reconstituer toute notion de ce mot à travers trois positions, par l'attention. Cette reconstitution est réelle, et elle fonctionne bien mieux qu'on ne pourrait l'espérer — mais c'est un travail que le modèle effectue, pas un fait qu'on lui a donné.
29 mots affichés · sur 30,522
Le vocabulaire est figé à 30 522 entrées. Tout ce qui en sort est reconstruit à partir de fragments, gloutonnement, plus longue correspondance d'abord — d'où des morceaux qui coïncident rarement avec des syllabes reconnaissables.
Voici la règle que j'avais mal comprise.
Le tokenizer de HuggingFace, avant même que WordPiece intervienne, effectue un premier passage qui découpe le texte sur les espaces et sur la ponctuation. Mon implémentation découpait sur les espaces et sur tout ce qui n'était ni lettre ni chiffre.
Cela sonne comme la même règle. Ce n'en est pas une, et la différence est exactement l'ensemble des caractères qui sont des symboles et non de la ponctuation : £, °, ¹⁄₂, les emoji. HuggingFace ne les découpe pas. Moi si.
La divergence est restée invisible pour une raison précise et inconfortable. Les symboles ASCII — $, %, +, =, &, * — tombent dans les plages de ponctuation de HuggingFace : les deux implémentations les découpent donc identiquement. Or tous les tests qu'on pense à écrire utilisent l'ASCII. Le bug vivait entièrement dans la queue non-ASCII.
Neuf entrées du vocabulaire ne survivaient pas à la frappe :
°c (6362) °f £1 £2 £3 £5 £10 £100 ¹⁄₂Ainsi que deux emoji, que HuggingFace transforme en un seul token inconnu et qui devenaient deux inconnus distincts.
Neuf entrées sur 30 522, c'est 0,03 % du vocabulaire. C'est aussi 100 % des phrases qui les contiennent, et chacune de ces phrases se voyait calculer un diagramme d'attention sur une entrée que le modèle n'aurait jamais reçue.
Les tests passaient parce que les tests et le code partageaient une hypothèse.
J'avais écrit le tokenizer, puis ses tests, à partir du même modèle mental de ce qu'est la tokenisation. Quand votre modèle de la règle est faux, vous écrivez une implémentation fausse et un test faux qui s'accordent parfaitement, et tout est vert.
Ce qui a rompu l'impasse, c'est un second tokenizer ne partageant rien avec le premier : autre langage, autre jour, écrit depuis la spécification et non depuis le code, exécuté sur les poids d'origine en fp64 NumPy. Quand deux implémentations indépendantes divergent, l'une des deux a tort, et le confort disparaît.
Elles ont divergé sur £5. La seconde avait raison.
Je veux rester prudent, car cet oracle a une humilité durement acquise : il s'est trompé, lui aussi, une fois. Sa première version ignorait l'étape où HuggingFace entoure d'espaces chaque caractère CJK pour en faire un mot à part entière. Il divergeait du runtime sur 中文 — et cette fois c'était le runtime qui avait raison. Une fixture versionnée a tranché.
Un oracle que personne n'a validé n'est qu'un second avis. Celui-ci est désormais vérifié contre une exécution de référence PyTorch avant d'avoir le droit de juger quoi que ce soit, et cette vérification s'exécute avant chaque comparaison.
Le garde-fou le plus solide s'est révélé le moins astucieux. Plutôt que d'énumérer les mots auxquels je pensais : prendre chacune des 30 522 entrées du vocabulaire, la repasser seule dans le tokenizer, et exiger qu'elle revienne identique à elle-même. Une entrée qui ne survit pas à cet aller-retour est un mot que le modèle possède et qu'on ne pourra jamais lui transmettre. Ce test a trouvé la classe de bug plutôt que l'instance, et c'est pour cela que je sais qu'aucune dixième entrée ne se cache derrière les neuf.
Trois conséquences, qui modifient votre lecture de toutes les visualisations de cette série.
Les unités à l'écran ne sont pas des mots. Quand Mind Map affiche hens, il affiche un mot assemblé à partir de deux positions de tokens, et le poids affiché est la fusion de ces deux positions. Cette fusion est un choix — moyenne sur les fragments de la requête, somme sur ceux de la cible — défendable, mais c'est une couche d'interprétation entre vous et le modèle.
Certaines entrées sont réellement hors du modèle. Tapez un emoji et vous obtenez [UNK] : non pas une approximation, mais son absence. Le modèle reçoit un token signifiant il y avait quelque chose ici et je n'en ai aucune idée. Tout arc tracé vers lui est de l'attention réelle portée à un haussement d'épaules.
La longueur n'est pas ce que vous croyez. L'interface limite l'entrée à 24 tokens, pas 24 mots. Water boils at 100°c. fait cinq mots et dix tokens. La limite n'est connaissable qu'une fois le vocabulaire téléchargé, raison pour laquelle l'outil la signale honnêtement après coup plutôt que de la prédire.
On enseigne généralement la tokenisation comme un préliminaire — l'étape ennuyeuse avant la partie intéressante. C'est l'étape où l'univers perceptif entier du modèle est fixé. Tout ce que les douze multiplications matricielles suivantes pourront remarquer de votre phrase doit déjà être présent dans cette liste d'entiers.
Ratez-la, et le reste du système continuera de produire des réponses belles, assurées et bien formées à une question que vous n'avez pas posée.
Prochain épisode : ce que calcule réellement une seule tête d'attention — trois matrices, un softmax, et aucune métaphore.
Plus à lire
Trois matrices, un produit scalaire, un softmax. La ligne somme toujours à 1 — une contrainte que le modèle doit satisfaire, pas un résultat qu'il rapporte.
Choisir la tête la plus confiante sélectionnait une tête positionnelle à chaque fois. Elle rapportait « it → was, 0,962 » — la réponse qu'elle donne pour chaque mot de chaque phrase.
L'int8 naïf plafonnait la parité d'attention à 2,97e-2 — trente fois pire que la barre. Où dépenser la précision : voilà toute l'ingénierie.