- Il s’agit d’une expérience où l’on conçoit directement les poids d’un transformer décodeur-only proche de GPT-2, sans apprentissage, afin de lui faire prédire le motif
(aab)* - La prédiction de
"aabaabaabaab..."nécessite de regarder les deux tokens précédents, ce qui met mieux en évidence le fonctionnement de l’attention qu’un simple motif alterné - Le modèle est volontairement petit, avec
N_CTX=5,N_VOCAB=2,N_EMBED=8, et utilise la tokenisationa=0,b=1ainsi que des embeddings one-hot - Une unique tête d’attention répartit l’attention à parts égales,
0.5, sur les deux tokens les plus récents, et calcule le token suivant en exploitant l’annulation d’un encodagea=1,b=-1 - Dans les contextes non ambigus, il atteint une précision de 100,0 % (27/27), mais une seule prédiction avec un contexte de 5 tokens demande environ 4 000 FLOPs, ce qui le rend bien moins efficace qu’une règle implémentée directement
Mini GPT-2 aux poids fixés à la main, sans apprentissage
- L’objectif est d’acquérir une compréhension intuitive de ce que font réellement les différents composants d’un transformer et de l’attention
- Le modèle n’est pas entraîné, n’utilise pas de poids préentraînés, et a été construit en assignant directement chaque poids à la main en une soirée
- L’architecture est un transformer décodeur-only proche de GPT-2, avec une implémentation simplifiée à partir de l’implémentation picoGPT de jaymody
- suppression du layer norm
- utilisation d’une seule tête au lieu de la multi-head attention
- suppression de la couche feed-forward
mlpdu bloc transformer
Pourquoi choisir la séquence (aab)*
- Au départ, l’idée était de prédire une séquence comme
"ababababab", mais comme le transformer prédit une shifted sequence, la tâche devenait trop simple- il suffit de prédire
bsi c’esta, sinona, sans avoir besoin d’utiliser les embeddings de position
- il suffit de prédire
- La tâche finale a donc été fixée à la prédiction de
"aabaabaabaab...", c’est-à-dire la séquence(aab)*- si les deux tokens précédents sont
abouba, le token suivant esta - si les deux tokens précédents sont
aa, le token suivant estb bbest traité comme un cas hors du périmètre de la tâche
- si les deux tokens précédents sont
- La tokenisation est volontairement simple et ne manipule que deux symboles
avaut0bvaut1
Dimensions du modèle et flux de calcul
- Trois paramètres de modèle ont été choisis
N_CTX = 5: longueur maximale de contexte vue par le modèle en une foisN_VOCAB = 2: les deux tokensaetbN_EMBED = 8: taille d’embedding servant à stocker tokens, positions et espace de calcul
- En pratique, seuls les deux tokens précédents sont nécessaires, mais
N_CTX=5permet d’inclure le cas où il faut ignorer des tokens non pertinents - La fonction
gpts’exécute dans l’ordre suivant- elle additionne embeddings de tokens et embeddings de position avec
wte[inputs] + wpe[range(len(inputs))] - elle fait passer le résultat dans un unique bloc transformer
- elle produit enfin les logits dans l’espace du vocabulaire via
x @ wte.T
- elle additionne embeddings de tokens et embeddings de position avec
Des embeddings one-hot pour position et token
wpeest l’embedding de position, et les 5 premières dimensions d’embedding servent de one-hot de position- la position 0 est
[1, 0, 0, 0, 0] - la position 4 est
[0, 0, 0, 0, 1]
- la position 0 est
wteest l’embedding de token, et les 2 dimensions suivantes servent de one-hot de token- le token
aest représenté par[1, 0]sur ces dimensions - le token
best représenté par[0, 1]
- le token
- La 8e position de l’embedding n’est pas utilisée au départ et sert ensuite de scratch space dans le bloc transformer
- Par exemple,
"aabaa"est représenté par une matrice d’embedding5 x 8, dont chaque ligne contient à la fois le one-hot de position et le one-hot de token
Comment la tête d’attention sélectionne les deux tokens les plus récents
- Le bloc transformer se compose d’une seule tête d’attention et d’une couche linéaire
c_projqui reprojette le résultat de l’attention dans l’espace d’embedding c_attnest une couche linéaire de tailleembed_size x (embed_size * 3)qui transforme l’embedding d’entrée en matricesqkv, puis les sépare enq,k,vkisole les embeddings de position afin de représenter l’information de position portée par chaque tokenqreprésente la plage de positions que chaque position souhaite consulter, etq @ k.Tproduit ainsi la matrice des scores d’attention- Après softmax et causal mask, la matrice d’attention possède les propriétés suivantes
- la première ligne met 100 % de l’attention sur le premier token uniquement
- les lignes suivantes répartissent chacune l’attention à
0.5sur les deux tokens les plus récents accessibles
- Le causal mask empêche de voir les futurs tokens en ajoutant une très petite valeur aux positions futures,
-1e10dans le code réel- ce modèle conçu à la main n’est pas fait pour tricher en regardant dans le futur, mais le masque est conservé pour rester plus proche de l’architecture GPT-2
- La normalisation par
np.sqrt(q.shape[-1])aide à améliorer les gradients dans un entraînement réel, mais n’a pas d’effet ici sur ce modèle artisanal
Encodage de v et prédiction par annulation additive
vtransforme le one-hot du token en un encodagea=1,b=-1- Comme le résultat de l’attention moyenne les deux tokens les plus récents avec un poids de
0.5chacun, cet encodage calcule les règles suivantesa, b→0.5 * 1 + 0.5 * (-1) = 0b, a→0.5 * (-1) + 0.5 * 1 = 0a, a→0.5 * 1 + 0.5 * 1 = 1
- En conséquence, la 7e position de la ligne contient la valeur suivante
0lorsque le bon token à prédire esta1lorsque le bon token à prédire estb
- Avec l’entrée
"aabaa", la première ligne peut produire une prédictionbfaute d’information suffisante, mais les prédictions suivantes respectent bien la règle(aab)*
Reprojeter la prédiction dans l’espace du vocabulaire
c_projreconvertit la valeur de la 7e position du résultat d’attention en format one-hot de token- Plutôt que de produire simplement
[..., 1, 0, ...]ou[..., 0, 1, ...], il produit un one-hot mis à l’échelle par1024embedding[row, 5] = 1024 + (-1024) * predictionembedding[row, 6] = 0 + 1024 * prediction
- Le bloc transformer comporte une residual connection, donc
x = x + causal_self_attention(...)ajoute le résultat au tenseur d’origine - Comme ce signal résiduel resterait sinon inutilement présent, l’échelle
1024sert à l’écraser - Enfin, on calcule
x @ wte.Tpour obtenir les logits, puis on applique le softmax- dans le contexte
"aabaa", la ligne de prédiction finale pointe versb - pendant l’entraînement, les prédictions de toutes les lignes sont utiles, mais en inférence seule la dernière ligne est nécessaire
- dans le contexte
Résultats de génération et précision
- La fonction
completeenvoie au modèle les 5 derniers tokens au maximum, puis choisit le token suivant avecargmaxsur la dernière ligne du résultat softmax - Exemples de génération
complete("a")→a :: baabaabaabcomplete("ba")→ba :: abaabaabaacomplete("abaab")→abaab :: aabaabaaba
- Même avec des entrées hors périmètre, le modèle peut parfois revenir à un motif répétitif
complete("ababa")→ababa :: abaabaabaacomplete("bbbbb")→bbbbb :: aabaabaaba
- Sur le test
"aab" * 10, la précision atteint 100,0 % (27/27) si l’on n’évalue que les contextes non ambigus
4 000 FLOPs contre 8 instructions
- Lorsqu’il utilise l’intégralité d’un contexte de 5 tokens, ce modèle a besoin d’environ 4 000 floating point operations pour prédire un seul token
- l’essentiel du coût provient du calcul de l’attention
- il est possible de réduire ce coût avec une fenêtre de contexte plus petite, des fused multiply-add, du kv caching, etc.
- malgré cela, une seule prédiction de token nécessite encore des centaines d’instructions machine
- Une implémentation directe de la même règle
(aab)*en assembleur x64 calcule le token suivant en 8 instructions - Cela laisse ouverte la question de savoir s’il est possible d’entraîner, pour la génération en langage naturel, un modèle de langage 1000 fois plus efficace que les modèles actuels
1 commentaires
Avis de Hacker News
Un travail connexe est « Thinking Like Transformers »
Il présente un langage de programmation primitif appelé RASP, composé d’opérations modélisables avec des composants de Transformer, et montre qu’il permet d’écrire des programmes comme des histogrammes ou du tri.
Il existe aussi d’excellents billets de blog de Sasha Rush et Gail Weiss, et des travaux ultérieurs ont montré que des programmes de type RASP pouvaient être compilés en véritables poids de modèle sans apprentissage.
[1] https://arxiv.org/abs/2106.06981
[2] https://srush.github.io/raspy/
[3] https://arxiv.org/abs/2301.05062
Si ce domaine vous intéresse, mon travail HandCrafted Transformers vaut aussi le détour : j’y choisis directement les poids d’un modèle Transformer pour lui faire effectuer une addition posée d’une manière proche de celle qu’on apprend à l’école primaire.
[1] https://colab.research.google.com/github/newhouseb/handcraft...
Aller des réseaux de neurones vers le code serait aussi très intéressant du point de vue de l’explicabilité.
Je pensais bien comprendre les Transformers, mais je n’en avais jamais implémenté un moi-même.
Un jour, je m’y suis mis, et il ne fonctionnait ni ne s’entraînait aussi bien que le Transformer PyTorch standard ; j’ai fini par comprendre que la cause était que j’avais ignoré le dropout.
Je l’entraînais à faire des additions de nombres et je pensais que le surapprentissage était impossible, puisque je ne lui avais jamais montré deux fois la même paire, mais le rôle du dropout s’est révélé bien plus important que prévu.
En résumé, il vaut vraiment mieux implémenter soi-même un Transformer, et plus on part de zéro, mieux c’est.
Toutes les personnes qui l’ont fait ont appris quelque chose d’inattendu, et ce qu’elles ont compris variait selon les cas, de la parallélisation de l’apprentissage au niveau des tokens jusqu’au fonctionnement réel de la rétropropagation.
Les ressources de Karpathy sont très bonnes aussi, mais la vidéo qui m’a enfin fait comprendre les Transformers est celle-ci : https://youtu.be/kWLed8o5M2Y?si=SJT5_lCJ0hSR7Z_k
J’ai eu des idées similaires pendant un moment.
Ne pourrait-on pas créer une interface intuitive vers les poids d’un modèle, qui permette à un expert du domaine de les ajuster à la main pour accélérer l’apprentissage ?
Par exemple, dans un modèle de vision qui détecte les cônes de signalisation, on augmenterait le groupe de poids correspondant à la « tendance à être orange ».
Ainsi, au lieu de demander des milliers ou des millions d’exemples supplémentaires pour calibrer correctement cette « tendance à être orange », un humain pourrait accélérer le processus.
Bien sûr, la difficulté est que cette interface devrait se mapper sur des groupes de poids ayant des significations différentes ; je me demande s’il y a une raison technique qui rende cela impossible.
[1] https://www.youtube.com/watch?v=8SF_h3xF3cE&t=1358s
En bref, les méthodes où l’humain aide l’IA sont presque toujours moins rentables que celles qui mobilisent davantage de puissance de calcul.
Pendant qu’un humain calibre une couche de poids pour détecter des cônes orange, un cluster de GPU entraîne l’IA à détecter des cônes de signalisation, des feux tricolores, des arbres, d’autres voitures et même des cônes d’un orange légèrement différent.
Même si l’on ajuste l’image pour mieux voir l’orange, si l’on ne peut pas surveiller en même temps la précision pour toutes les autres couleurs, on risque de créer sans s’en rendre compte des problèmes sur d’autres couleurs.
Ce n’est pas qu’un cluster précis de neurones corresponde à un concept précis ; dans l’ensemble, tout contribue un peu à tout.
Il y a trop de paramètres impliqués.
L’article sur les Transformers est tellement technique que j’ai toujours voulu le comprendre, même superficiellement, mais c’était difficile.
Ce billet m’a vraiment aidé à comprendre comment cela fonctionne, et au moins les exemples étaient très clairs.
Il m’a aussi permis de me remémorer les matrices que j’avais apprises à l’université.
N’est-ce pas une sorte de machine abstraite, comme une machine de Turing ou une machine qui parse des expressions régulières ?
On commence par définir une liste de tokens ; pour faire simple, disons par exemple 24 caractères.
Cette machine prend une séquence de tokens en entrée, effectue des opérations matricielles déterministes, puis produit une liste de probabilités pour tous les tokens.
« L’apprentissage » n’est que le processus qui consiste à fixer une partie des nombres dans les matrices utilisées par ces opérations.
Il est intéressant de noter que le code final ne contient qu’une seule instruction
if, et encore, elle sert à évaluer l’exactitude du résultat.Toute la « logique » vient du résultat des opérations matricielles.
Dans les réseaux de neurones, tout est en général un peu flou, et il n’y a presque pas de
if/else, même s’il existe des cas comme dans l’exemple du Transformer où l’on « masque » des valeurs avec 0 ou -∞.La sortie est aussi presque toujours un ensemble de scores ou de probabilités : si un modèle qui distingue des photos de chats et de chiens produit quelque chose comme
dog:0.95 cat:0.05, on dit qu’il a prédit un chien parce que le score du chien est plus élevé.Le mécanisme d’attention, qui est au cœur du Transformer, repose sur une sorte d’opération de recherche douce.
Dans un système non flou, on parcourrait chaque token de la séquence pour vérifier s’il est pertinent par rapport au token courant, puis on ferait quelque chose s’il l’est ; dans un Transformer, la pertinence n’est pas une décision binaire.
À la place, on calcule un score de pertinence continu entre toutes les paires de tokens de la séquence, puis on utilise ce score pour décider de la suite.
Cela dit, certaines choses ne se généralisent pas facilement à partir de systèmes fondés directement sur des décisions binaires.
Par exemple, ces scores de pertinence servent de poids pour calculer une moyenne pondérée sur les tokens du vocabulaire, ce qui donne un « token moyen » pour la position courante.
Je ne vois pas vraiment comment interpréter facilement cela comme une extension d’un processus basé sur une logique de branchement.
Cet article, qui explique que les Linear Transformers sont en fait des Fast Weight Programmers, vaut le détour : https://arxiv.org/abs/2102.11174
En réglant soigneusement les poids, on peut leur faire effectuer n’importe quel calcul.
Cela dit, ce serait bien d’avoir un compilateur qui ne repose pas sur l’approximation.
Je me demande à quoi sert l’idée que « cela pourrait vous donner envie de fabriquer votre propre modèle », en dehors d’un exercice d’apprentissage destiné à satisfaire la curiosité.
J’ai l’impression que les modèles de machine learning complexes commencent à devenir irréalistes à manipuler pour quelqu’un qui lit des blogs chez lui.
Il génère beaucoup de mots pseudo-vieil anglais plausibles, et apprend les bases de la grammaire anglaise ainsi que la forme des pièces de théâtre, entre autres.
J’ai trouvé assez étonnant qu’il aille aussi loin en si peu de temps.
En entraînant localement plusieurs modèles jusqu’à un niveau de fidélité façon Shakespeare-from-Wish.com, on pourrait sans doute déterminer si l’on a trouvé une bonne architecture et s’il est temps d’essayer de la passer à l’échelle.
L’auteur voulait mieux comprendre les Transformers et l’attention ; il avait lu The Illustrated Transformer, mais n’avait pas d’intuition claire sur ce que faisaient réellement les différents morceaux de l’attention.
Il disait en substance ne pas comprendre la différence entre
qetk, sans même parler dev.Au-delà de la simple satisfaction de la curiosité, cela aide à construire et approfondir la compréhension.
Ce serait bien de pouvoir mettre une expression comme neural network dans le titre.
Il ne s’agit pas d’un ensemble de bobines couplant électromagnétiquement deux circuits, mais de l’architecture « Transformer » du machine learning.