basu_← Tous les billets
FIELD NOTES

L’attention d’un Transformer, calculée à la main

Trois vecteurs suffisent pour comprendre les requêtes, les clés, les valeurs et le rôle exact du softmax.

Le mot « attention » invite à imaginer un lecteur qui choisit les mots importants. L'image est commode, mais elle cache le calcul. Dans un Transformer, une couche produit des combinaisons pondérées de vecteurs. Les coefficients de ces mélanges dépendent eux-mêmes des vecteurs présents dans la séquence. Il n'est pas nécessaire de prêter une intention au mécanisme pour le comprendre.

L'article Attention Is All You Need, publié par Vaswani et ses coauteurs en 2017, introduit une architecture fondée sur l'attention, sans récurrence ni convolution pour son modèle principal. Concentrons-nous sur un seul de ses blocs. Les nombres qui suivent sont un exemple pédagogique construit pour pouvoir refaire les opérations.

Une recherche, des étiquettes et du contenu

Chaque token est représenté par un vecteur. Trois transformations linéaires apprises construisent des requêtes QQ, des clés KK et des valeurs VV. Une requête est comparée aux clés ; les valeurs sont le contenu que l'on mélange ensuite. Dans une même séquence, ces trois objets viennent des mêmes représentations initiales, mais leurs matrices de projection diffèrent.

Pour une requête à deux dimensions, choisissons :

q=(1,0),k1=(1,0),k2=(0,1),k3=(1,1)q=(1,0),\quad k_1=(1,0),\quad k_2=(0,1),\quad k_3=(1,1)

Les produits scalaires valent 1,0,11,0,1. Le premier et le troisième token sont donc à égalité selon cette requête, même si leurs clés sont différentes. Le produit scalaire n'est pas une mesure magique de sens : il ne voit que les directions que l'apprentissage a placées dans cet espace.

Du score à un mélange

Avec une dimension de clé dk=2d_k=2, divisons les scores par 2\sqrt{2}. On obtient environ 0,707,0,0,7070{,}707,0,0{,}707. Le softmax transforme ces trois nombres en coefficients positifs dont la somme vaut un :

ai=exp(si)jexp(sj)a(0,401, 0,198, 0,401)a_i=\frac{\exp(s_i)}{\sum_j\exp(s_j)} \quad\Longrightarrow\quad a\approx(0{,}401,\ 0{,}198,\ 0{,}401)

Pourquoi cette division ? Si les composantes des requêtes et des clés sont indépendantes, centrées et de variance un, la variance de leur produit scalaire croît comme dkd_k. Diviser par dk\sqrt{d_k} stabilise cette échelle sous ces hypothèses. Sans cela, de grands scores peuvent rendre le softmax très concentré et réduire ses gradients utiles.

Prenons maintenant v1=(2,0)v_1=(2,0), v2=(0,2)v_2=(0,2) et v3=(2,2)v_3=(2,2). La sortie vaut :

z=iaivi(1,604, 1,198)z=\sum_i a_i v_i\approx(1{,}604,\ 1{,}198)

La sortie n'est donc pas le troisième token, ni son nom, ni une explication. C'est un nouveau vecteur, situé ici dans l'enveloppe convexe des trois valeurs.

Une ligne d’attentionUne ligne d’attention01 / Comparer q aux clés : 1 ; 0 ; 102 / Diviser par √2 : 0,707 ; 0 ; 0,70703 / Softmax : 0,401 ; 0,198 ; 0,40104 / Somme pondérée des valeurs : (1,604 ; 1,198)
Les clés servent à calculer les poids ; les valeurs sont mélangées. Confondre les deux masque le rôle des projections apprises.

Le même calcul pour toute la phrase

En empilant les requêtes dans une matrice, tous les produits scalaires deviennent une multiplication matricielle :

Attention(Q,K,V)=softmax(QKdk+M)V\operatorname{Attention}(Q,K,V) =\operatorname{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}+M\right)V

Le softmax s'applique ligne par ligne. Pour une séquence de nn tokens, QKQK^\top comporte n2n^2 scores. Si le modèle doit prédire la suite sans regarder le futur, le masque MM interdit les positions futures en leur attribuant un score négatif infini avant le softmax. Un score interdit reçoit ainsi un poids nul.

Ce masque ne fournit pas l'ordre des mots. Sans information de position, permuter les lignes permute les résultats de façon correspondante. Les représentations positionnelles apportent une information que le mélange, à lui seul, ne possède pas.

Une implémentation assez petite pour être inspectée

Le code suivant calcule notre ligne sans bibliothèque. Soustraire le maximum avant l'exponentielle préserve le résultat tout en limitant le risque de débordement numérique.

from math import exp, sqrt
 
q = [1.0, 0.0]
keys = [[1, 0], [0, 1], [1, 1]]
values = [[2, 0], [0, 2], [2, 2]]
scores = [sum(a*b for a, b in zip(q, k)) / sqrt(2) for k in keys]
shift = max(scores)
weights = [exp(s - shift) for s in scores]
weights = [w / sum(weights) for w in weights]
output = [sum(w*v[d] for w, v in zip(weights, values)) for d in range(2)]
print(weights, output)

Deux expériences éclairent le mécanisme. Multiplier les scores par dix concentre les poids sur les deux maxima, mais ne départage pas leur égalité. Changer seulement les valeurs modifie la sortie sans modifier aucun coefficient d'attention. On voit ainsi que « où regarder » et « quoi transporter » sont deux calculs distincts.

Les multiples têtes répètent ce principe dans plusieurs sous-espaces appris, puis combinent leurs sorties. Elles ne correspondent pas nécessairement à des fonctions linguistiques simples. Enfin, visualiser les coefficients peut être instructif, mais un coefficient élevé n'est pas une preuve causale qu'un mot explique la décision finale : d'autres couches, connexions résiduelles et transformations interviennent. Le calcul local est précis ; l'interprétation globale demande une expérience supplémentaire.

← Tous les billets