Training SLMs (5) - On démonte le plafond.
- Ca c'est du plan de bataille ! - Avec ça, on ne peut pas perdre !
Depuis deux logs, je répète la même phrase avec l’assurance tranquille de celui qui n’a rien vérifié : la différence entre un feat et un fix tient à l’intention de l’auteur (moi), cette intention n’est pas dans le diff, donc mes modèles sont au plafond et il n’y a plus rien à gratter.
C’est une conclusion très confortable : elle explique l’échec, elle innocente les modèles, elle innocente accessoirement celui qui les entraîne, et elle permet de ranger le matériel la conscience tranquille. Elle repose sur trois exemples que j’ai choisis moi-même parce qu’ils illustraient bien mon propos.
J’ai fini par la mesurer. Elle est vraie à 19%.
Mesurer un plafond, c’est mesurer un humain
Le problème posé est le suivant : quand mon modèle répond fix là où j’avais écrit feat, qui a tort ? Si l’information permettant de trancher est dans le diff, c’est le modèle. Si elle n’y est pas, personne n’a tort et le score est plafonné par la nature de la tâche.
Il n’y a qu’une façon de le savoir : prendre un lecteur compétent, lui montrer exactement ce que le modèle voit, et regarder ce qu’il en tire. Le lecteur compétent, faute de budget, c’est moi.
Première passe : l’annotation à l’aveugle
Pour chacun des 68 cas du jeu de test, un script me montre le diff et le résumé du message, et rien d’autre. Pas le type que j’avais commité à l’époque, pas la réponse du modèle. La question posée n’est pas “quel est le bon type” mais “quels types un lecteur compétent pourrait-il défendre ici”, et j’y réponds par un ensemble plutôt que par un choix unique. Répondre par un ensemble compte, parce que ça permet de distinguer “je pense que c’est feat” de “ça peut être feat ou fix et les deux se tiennent”, ce qu’une note de confiance sur dix ne dirait jamais.
Résultat : le lecteur à l’aveugle retrouve le type commité dans 40 cas sur 68, soit 59%. Le modèle de 135 millions de paramètres, lui, est à 41/68, soit 60%.
Donc mon modèle fait jeu égal avec moi. J’ai failli publier le chiffre, et clore le sujet : si un humain qui connaît le projet ne fait pas mieux qu’un modèle qu’on peut faire tourner sur un grille-pain, c’est bien que l’information manquante n’est nulle part, et mon plafond était le bon.
Mais… ce raisonnement est faux.
Le piège : un annotateur humain est un instrument, et un instrument a une précision
Voilà ce que j’ai fait avant de regarder les résultats, et ça a été mon petit moment de génie (des fois ça arrive, oui) de cet article : j’ai glissé dix cas en double dans le lot. Les mêmes diffs, présentés deux fois, à distance, sans que rien ne les signale. Ma mémoire de poisson rouge faisant le reste.
Sur ces dix cas, je suis d’accord avec moi-même six fois sur dix.
Ce chiffre change tout, parce qu’il rend le 59% inexploitable en l’état. Un lecteur qui retrouve l’étiquette d’origine six fois sur dix peut aussi bien décrire une tâche réellement ambiguë qu’un annotateur fatigué qui coche au jugé à partir du quarantième cas. Les deux hypothèses prédisent exactement le même 59%, et elles mènent à des conclusions opposées. L’une dit “le plafond est là, arrête-toi”, l’autre dit “ta règle est molle, recommence”.
Détail qui va dans le même sens : sur les 68 cas, 21 réponses à l’aveugle étaient des réponses à un seul type, donc données sans la moindre hésitation, et rataient quand même l’étiquette. Ma confiance dans mes propres réponses n’était donc pas calibrée du tout, ce qui est vexant mais informatif.
C’est ici une leçon importante de cet article : une passe d’annotation humaine est un instrument de mesure, et elle a besoin de sa propre mesure d’erreur.
Deuxième passe : l’arbitrage
Les 28 cas où le lecteur à l’aveugle et l’auteur ne sont pas d’accord repassent donc à la moulinette, cette fois sous forme de duel. Les deux types candidats sont présentés côte à côte, dans un ordre tiré au hasard, et sans indiquer lequel des deux est celui que j’avais réellement commité.
Ce masquage est tout le design. Si on me dit “voici ton étiquette d’origine, la contestes-tu ?”, je vais la ratifier, comme tout le monde, parce que personne n’aime se déjuger et que l’étiquette d’origine bénéficie du crédit de celui qui était là au moment des faits. Masquée, elle doit gagner sur pièces. Et l’égalité est une réponse autorisée, qui signifie très précisément : le diff ne permet pas de trancher.
| Verdict | Nombre |
|---|---|
| L’étiquette commitée est meilleure | 14/28 (50%) |
| La réponse à l’aveugle est meilleure | 3/28 (11%) |
| Égalité, vraiment indécidable | 11/28 (39%) |
Quatorze à trois. Quand les deux réponses s’affrontent à armes égales et anonymement, mes étiquettes d’origine écrasent mes réponses à l’aveugle. Ce n’était donc pas la tâche qui était ambiguë, c’était mon annotation qui était mauvaise. L’instrument (moi) était en cause, pas l’objet mesuré.
Un deuxième groupe de 5 cas passe par le même processus : ceux où ma réponse à l’aveugle contenait bien le bon type, mais en avait coché un autre à côté. Ils avaient été comptés comme ambigus sur la seule foi de la passe à l’aveugle, c’est-à-dire sur la parole de l’instrument que je venais de discréditer. Résultat : l’étiquette d’origine tient seule dans 3 cas, égalité dans 2, et l’alternative ne l’emporte jamais.
Le verdict
Reventilé sur les 68 cas :
| Nombre | ||
|---|---|---|
| Étiquette d’origine confirmée | 52/68 (77%) | |
| Vraiment indécidable | 13/68 (19%) | le vrai terme de plafond |
| Étiquette d’origine douteuse | 3/68 (4%) | de vraies erreurs de ma part |
Le plafond n’est pas à 59%, il est à environ 81%. Et mon modèle est à 60%.
Il reste donc à peu près vingt points inexpliqués entre ce que mon modèle fait et ce que la tâche autorise. Vingt points que j’avais rangés dans la case “ambiguïté irréductible” sur la base de trois exemples bien choisis.
Notons au passage ce que ça dit du dataset, parce que c’est une bonne nouvelle qui passerait inaperçue : sur 68 commits écrits au fil de l’eau pendant des mois, trois seulement sont mal typés. Le corpus est propre. Ce n’est pas l’étiquetage qui limite le modèle.
Et la campagne d’optimisation du log précédent, dans tout ça ? Elle reste un échec, et pour les mêmes raisons. Les époques et le rang ont été testés, ils n’ont rien donné, ce résultat-là ne bouge pas d’un iota. Ce qui change, c’est son interprétation : je concluais “il n’y a plus rien à apprendre”, alors que la conclusion correcte était “je n’ai pas trouvé le bon levier”. Ce n’est pas la même phrase, et ça m’oblige à continuer à chercher.
“Indécidable” n’est pas un phénomène, c’en est trois
La partie la plus utile de cette expérience vient du champ de texte libre laissé à côté de chaque arbitrage, pour pouvoir râler (ou plutôt, expliquer mon raisonnement, pour faire plus joli). En relisant les 13 cas classés indécidables, ils ne racontent pas du tout la même histoire :
- Le commit fait deux choses. Sur un cas
fix/feat, ma note dit : “ce commit est mauvais, ça aurait dû être un fix pour la purge ET un feat pour l’endpoint”. Un autre mélange un bump de version et une correction de README : “devrait être 2 commits séparés”. Aucun type n’est correct parce qu’aucun type ne peut l’être. - Le
diffest opaque. Sur unperf/refactor: “à partir de ce diff je n’ai aucune idée de ce qu’il y a là-dedans”. Après vérification, le diff était tronqué car très long, cachant ainsi l’objectif du commit. - La frontière est réellement floue. Sur un
chore/feat: “ça peut vraiment être les deux à mon avis. Peut-être chore un chouïa plus”.
Seul le troisième cas est le plafond que j’imaginais. Le premier n’est pas une limite de la tâche, c’est un défaut de mes commits, et surtout c’est détectable indépendamment : un commit qui fait deux choses est repérable sans avoir à trancher son type. Dire à l’auteur “ça, ça devrait être deux commits” est infiniment plus utile que de deviner entre deux étiquettes qui sont fausses toutes les deux.
Il y a donc une fonctionnalité entière de mon futur outil qui est sortie d’un champ de commentaire libre, et pas d’une métrique. Morale à l’usage de ceux qui font annoter quoi que ce soit : prévoyez toujours que l’annotateur puisse vous répondre autre chose que ce que vous lui demandez.
Ces vingt points sont-ils atteignables ?
Savoir que le plafond est à 81% ne dit pas encore si on peut y monter. Mon 9/13 sur les feat en aveugle peut venir de deux endroits très différents : soit j’ai lu le diff, et alors l’information y est et mon modèle passe à côté, soit je me souviens vaguement de ce commit et de ce que j’avais en tête ce jour-là, et alors l’information n’est nulle part sauf dans ma tête.
Une expérience simple sépare les deux : faire passer les mêmes 68 cas à Claude Opus 5, sans aucun entraînement, sur des entrées rigoureusement identiques à celles que voit le 135M et avec le même prompt système que tous mes autres scoreurs. Claude ne connaît ni mon projet, ni mes conventions, ni ce que j’avais en tête ce jour-là. Tout ce qu’il obtient, il l’a lu dans le diff.
Le résultat
Claude Opus marque 47/68, contre 41/68 pour mon 135M entraîné.
Sur les 13 cas indécidables, où deux réponses comptent comme justes, il en marque 12. C’est mécanique et ça gonfle le total. La mesure honnête de ce qui est réellement lisible dans un diff, c’est son score sur les 55 cas décidables : 43/55, soit 78%. Il ne touche donc pas le plafond de 81%, et ses douze erreurs sur ces cas-là sont de vraies erreurs.
Six points d’écart, ce n’est pas le résultat. Le résultat, c’est où ils sont.
| Type | Cas | SmolLM2-135M entraîné | Claude Opus 5, zéro entraînement |
|---|---|---|---|
chore |
17 | 15 | 12 |
fix |
16 | 12 | 8 |
feat |
13 | 5 | 9 |
refactor |
6 | 1 | 5 |
docs |
5 | 3 | 5 |
test |
5 | 4 | 5 |
perf |
5 | 1 | 2 |
style |
1 | 0 | 1 |
| Total | 68 | 41 | 47 |
Le feat à 9/13, c’est trait pour trait le score du lecteur à l’aveugle, obtenu par un modèle qui n’a jamais entendu parler de mes projets. La frontière que je décrétais illisible depuis deux articles est parfaitement lisible dans le diff, et c’est mon modèle qui ne la lit pas. Le refactor raconte la même histoire en pire, 5/6 contre 1/6.
Le fine-tuning achète le dialecte, l’échelle achète le jugement
Maintenant regardons les deux premières lignes, parce que la lecture “le gros modèle est meilleur” ne survit pas à ce tableau : un modèle frontière, entraîné sur une bonne partie du code open source de la planète, se fait battre sur chore et fix par un modèle de 135 millions de paramètres qui a vu 824 de mes commits. Et il ne se fait pas battre de peu.
Mon 135M gagne exactement deux catégories, chore et fix, mais il les gagne largement, +3 et +4. Et il perd les six autres, pour un total de 13 points. Or chore et fix, ce sont 33 des 68 cas, soit à peu près la moitié du corpus. Autrement dit : le petit modèle gagne précisément là où il a le plus d’exemples, et perd partout où la réponse est un jugement sur l’intention plutôt qu’une convention.
Ce qui donne la conclusion la plus intéressante du projet à ce stade, et elle n’oppose pas les deux approches, elle les articule : le fine-tuning achète le dialecte, l’échelle achète le jugement. Que du travail de linter soit typé chore chez moi, c’est une convention maison que quelques centaines d’exemples enseignent mieux que n’importe quelle capacité générale, et Claude ne peut pas la deviner parce qu’elle n’est écrite nulle part. Que tel changement soit une fonctionnalité et non un fix, c’est une lecture de ce que le code est pour, et là c’est mes 135 millions de paramètres qui ne suivent pas.
Au passage, ça dessine une forme de produit qu’aucun des deux modèles ne donne seul : router selon la catégorie. Le fine-tune répond déjà mieux qu’un modèle frontière sur la moitié du corpus, en une seconde environ sur ma vieille carte graphique, et gratuitement.
Et maintenant ?
Je termine ce log avec vingt points de marge que je ne sais qu’en partie expliquer, une piste précise pour aller les chercher, et une conclusion que j’avais publiée deux fois et qu’il a fallu retirer.
C’est un beau pas en avant sur ce projet. Un plafond qu’on suppose est un mur ; un plafond qu’on mesure est un chiffre, et un chiffre laisse voir la distance qui reste.
Mais je me suis rendu compte d’une chose : alors que je montais toute cette machinerie pour comparer mon petit modèle à un modèle frontière, le meilleur score du projet dormait déjà dans mes fichiers de résultats. Il y était depuis trois semaines. Je ne l’avais pas vu parce que je l’avais mesuré avec l’ancienne règle, celle de 30 cas dont l’article précédent a raconté l’enterrement. Prochain log, on parle enfin d’échelle et de nombre de paramètres, et le classement ne sera pas celui que ce tableau laisse deviner.
TL;DR
- Une conclusion confortable qui explique un échec et innocente son auteur mérite d’être mesurée avant d’être publiée. La mienne l’a été deux fois avant que je m’en occupe.
- Le lecteur humain à l’aveugle retrouve l’étiquette d’origine dans 59% des cas, contre 60% pour le modèle. Lu naïvement : parité humain/machine, projet terminé. C’est un piège.
- Une passe d’annotation humaine est un instrument et a besoin de sa propre barre d’erreur. Dix cas glissés en double révèlent un accord avec moi-même de 6/10 : l’instrument était trop mou pour conclure quoi que ce soit.
- Réarbitrées en duel anonyme, mes étiquettes d’origine battent mes réponses à l’aveugle 14 à 3. Ce n’était pas la tâche qui était ambiguë.
- Le plafond est à ~81%, pas à ~59%, et le modèle est à 60%. Seuls 19% des cas sont réellement indécidables, et 4% sont de vraies erreurs de typage de ma part.
- “Indécidable” recouvre trois phénomènes distincts, et un seul est un vrai plafond. Un commit qui fait deux choses est un défaut de commit, détectable pour lui-même, et ça vaut une fonctionnalité d’outil.
- Claude Opus 5, sans aucun entraînement, marque 47/68 et 9/13 sur les
feat, exactement le score du lecteur humain. La frontière que je disais illisible est lisible. - Mais il perd
choreetfix, soit la moitié du corpus, face à un modèle de 135 millions de paramètres qui tourne sur une carte graphique de 2019. Le fine-tuning achète le dialecte, l’échelle achète le jugement, et les deux sont complémentaires plutôt que concurrents.