Après les modèles d’AP5, nous allons construire un petit générateur de
caractères. Il reçoit par ex. ^abc| et tente de produire abc puis un
retour à la ligne. Comment peut-il retrouver des caractères placés plusieurs
positions avant celui qu’il doit prédire ? C’est le rôle de l’attention,
un calcul qui laisse chaque position lire les précédentes, et que vous allez
programmer. Un transformer est précisément une architecture de réseau
construite autour de ce calcul ; nous en écrivons une version minuscule.
Ce TP long est un sujet au choix d’AP9, commencé avec l’enseignant puis terminé à la maison. Il comporte une implémentation, des tests personnels et une comparaison expérimentale. Les modalités de notation et l’échéance seront précisées avant le lancement en janvier ; les étapes ne sont pas un barème.
Téléchargez le squelette petit-transformer.
Son README contient le sujet complet, les contrats, les indications Nx,
les commandes et un guide de dépannage ; COMPTE-RENDU.md prépare l’analyse.
Tout le corpus est fourni, ainsi que l’entraînement, la génération, les
sauvegardes et les figures SVG. Vous n’avez pas besoin de GPU, de compte de
service ni de connaissances web. Le dépôt du cours IA n’est pas requis.
Obtenir un premier résultat
Avec Elixir 1.17 ou plus récent, ouvrez un terminal dans le dossier extrait :
mix deps.get
mix tiny.check
mix test --only fourni
mix tiny.train bigram --steps 200 --out results/bigram-200
La première commande télécharge les dépendances verrouillées. Les sept tests
des briques fournies doivent passer. Le bigramme fonctionne dès le départ ;
les tests de l’attention échoueront avec « À compléter » tant que vous ne
l’aurez pas écrite. Ouvrez results/bigram-200/curve.svg dans votre éditeur.
Le travail utilise Nx sur CPU, dans des fonctions defn comme en AP4.
Le gradient et Adam sont fournis. On retrouve ainsi l’apprentissage d’AP5,
sans demander de réécrire l’infrastructure ou d’installer un backend GPU.
Prédire un caractère à chaque position
Le corpus contient les 64 triplets possibles sur a, b, c, d, chacun
écrit sous la forme ^abc|abc suivie d’un retour à la ligne. Le vocabulaire
est fixé à sept tokens, c-à-d. sept unités que le modèle peut prédire ;
ici, un token est un caractère. La cible est décalée d’un caractère par
rapport à l’entrée :
Les lignes sont séparées en 48 exemples d’apprentissage, 8 de validation et 8 de test, avant de fabriquer les fenêtres. Aucun triplet n’apparaît dans deux partitions. Les données synthétiques sont livrées sous CC0 avec leur notice de provenance. Il n’y a aucun texte à télécharger.
Le bigramme est le modèle le plus simple : il prédit le caractère suivant à
partir du seul caractère courant, grâce à une table de scores de forme
{7, 7} dont il sélectionne la ligne du dernier caractère. Peut-il distinguer
les contextes ^abc| et ^ddd| ? Tous deux finissent par | : le triplet
précédent est inaccessible à ce modèle. Nous allons permettre au second
modèle de lire plusieurs positions.
Construire une moyenne qui dépend du contexte
Un embedding est un vecteur de nombres appris représentant un token ou une
position. À partir des embeddings, trois projections apprises donnent, pour
chaque position, une requête Q, une clé K et une valeur V. Le produit scalaire
de Q à la position i et de K à la position j donne un score de lecture.
On le divise par la racine de la dimension des vecteurs.
Un softmax convertit ensuite les scores d’une ligne en poids positifs dont la somme vaut 1. La sortie est une somme des valeurs V, pondérée par cette ligne. Ainsi, le vecteur produit à une position peut dépendre de plusieurs positions du préfixe. Ce vocabulaire décrit un calcul : il ne prête pas une intention au modèle.
Mais il faut interdire les positions futures : pendant l’apprentissage,
le prochain caractère est déjà présent dans l’entrée ! Le masque causal
conserve seulement les colonnes j <= i, avant le softmax.
Complétez les trois fonctions de lib/tiny_lm/attention.ex :
| Fonction | Entrées | Sortie et vérification |
|---|---|---|
scores/2 | Q et K, {B, T, D} | {B, T, T} ; produit scalaire divisé par √D, chaque lot séparé. |
probabilities/1 | Scores, {B, T, T} | Même forme ; futur nul, lignes de somme 1, softmax stable. |
combine/2 | Poids {B, T, T} et valeurs {B, T, D} | {B, T, D} ; somme pondérée dans chaque exemple. |
B est le nombre d’exemples du lot, T le contexte et D la dimension des
vecteurs. Le README fournit un exemple de Nx.dot/6 avec lots, puis les
indications pour le masque et le softmax. Vous pouvez vérifier chaque étape :
mix test --only scores
mix test --only poids
mix test --only valeurs
mix test
mix test --include integration
Ajoutez un test avec une seule position et un test permettant de détecter un mélange entre deux exemples du lot. Les tests du modèle complet vérifient également que modifier le futur ne change pas les logits du préfixe. Le test d’intégration vérifie qu’un petit lot répété peut être mieux ajusté ; cela ne suffit pas à établir une généralisation.
Utiliser le transformer fourni
Votre attention s’insère dans un vrai bloc à une tête : embeddings de tokens et de positions, normalisations, attention, deux connexions résiduelles et un réseau dense avec ReLU. Une connexion résiduelle additionne l’entrée d’une brique à sa sortie, de même forme. En sortie, une projection donne sept logits par position : des scores réels, un par caractère possible, avant leur conversion en probabilités. Ces couches et leur assemblage sont fournis.
Dans model.ex, repérez et expliquez les deux additions résiduelles, puis lancez :
mix tiny.train transformer --steps 200 --out results/transformer-200
mix tiny.sample results/transformer-200 --prompt '^abc|' --tokens 4
La génération recharge les poids et exporte aussi une carte attention.svg
pour le prompt. Les mesures, configurations, empreintes du corpus, versions,
courbes et poids sont conservés dans le dossier d’expérience. Utilisez un
autre nom de dossier à chaque nouvel essai.
La configuration de départ contient 3 520 paramètres, des lots de 4 exemples, un contexte de 8 et des vecteurs de dimension 16. Sur le portable Apple M3 de préparation, 200 pas ont pris environ neuf secondes, hors compilation. Commencez avec 25 pas sur votre poste. Aucun entraînement de grand modèle n’est demandé.
Comparer, puis reconnaître une limite
Comparez d’abord le bigramme et le transformer avec les mêmes lots et le même
nombre de pas. Notez qu’ils ont des nombres de paramètres et des coûts différents.
Changez ensuite un seul choix, par exemple --dim 8, en annonçant votre
hypothèse avant l’essai.
Conservez les pertes de validation, le temps, la précision sur les trois caractères copiés et des générations réussies ou ratées. La validation ne contient que 24 caractères de copie : une variation de quelques points ne permet pas une conclusion générale. La perte globale peut également masquer les différences sur la partie à copier.
La mesure de copie utilise les vrais caractères précédents, alors que la génération réutilise ses propres sorties : une première erreur peut s’amplifier. Expliquez cette différence dans le compte rendu. Une carte d’attention est un outil pour observer le calcul ; elle ne démontre pas à elle seule que le réseau a appris la règle.
Gardez le test réservé au modèle finalement choisi. Le README donne la
commande d’évaluation (mix tiny.evaluate) et les extensions possibles :
positions, deux têtes, ou nouveau corpus avec partitions indépendantes. Le
dossier results/ est ignoré par Git : copiez les expériences retenues dans
rendu/ et indiquez leurs chemins dans le compte rendu. Le projet ne demande pas
un score minimal ni un texte français convaincant. Il demande un calcul
correct, une expérience reproductible et une conclusion proportionnée aux
observations.
La progression s’inspire du mini-GPT de Karpathy, avec une taille et un code fourni adaptés au cours. L’attention reprend le mécanisme de Vaswani et al.. Ces références sont facultatives : le README permet de commencer seul. Pour toute question, vous pouvez me contacter par email avec la commande, l’erreur et l’exemple qui reproduit le souci.
Pour les curieux : le fascicule
Un petit fascicule d’approfondissement accompagne ce sujet : les idées derrière le projet, leur histoire, ce qu’elles deviennent dans les systèmes réels et quoi lire ensuite. C’est un cadeau de fin de cours ; rien n’y est à rendre.
