Attention multi-têtes et encodage de position
Une tête d'attention ne peut regarder les caractères précédents que selon un seul critère ; avec plusieurs têtes, on regarde plusieurs choses à la fois. Et l'attention elle-même ne distingue pas l'ordre des caractères, il faut donc lui indiquer la position. Cette leçon rend ces deux points visibles par l'expérience, et tombe sur un résultat inattendu.
- Environ 40 minutes
- Niveau : Approfondi
- Testé : 2026-09-15 torch 2.14, CPU, graine aléatoire fixée
Le code et les sorties des programmes sont reproduits tels qu’ils ont tourné : commentaires et sorties sont donc en chinois.
La leçon précédente a écrit une tête d'attention. Pour en faire un GPT, il manque encore deux choses : les têtes multiples, et l'information de position.
python heads_positions.py
Les expériences de cette leçon utilisent le modèle de gpt.py, mais sans entraînement : on examine les propriétés de la structure elle-même.
Plusieurs têtes : regarder plusieurs choses à la fois
Un caractère a souvent besoin de recueillir plus d'un type d'information auprès des précédents. Pour écrire le « 尽 » de « 白日依山尽 » (le soleil blanc s'efface derrière les monts), le modèle a peut-être besoin de savoir en même temps : quel paysage précède (le soleil blanc, les monts), à quelle position est ce caractère (dans un poème à cinq caractères, le cinquième doit rimer ou marquer la césure), et quels mots le vers précédent a employés.
Une tête d'attention n'a qu'un jeu de q et k, ne note que selon un critère, et obtient un seul jeu de poids. L'attention multi-têtes procède ainsi : découper le vecteur en plusieurs segments, faire une attention sur chacun, puis tout réassembler. Chaque tête a ses propres q, k et v, et peut apprendre à s'intéresser à autre chose.
== 2. 多头注意力:32 维的向量切成 4 个头,每个头 8 维
q 的形状 (1, 5, 32)
拆成多个头之后 (1, 4, 5, 8):(句子数, 头数, 字数, 每个头的维度)
注意力分数 (1, 4, 5, 5):每个头都有自己的一张 5×5 的表
一个注意力层的参数:qkv 3168 个,proj 1056 个,头数多少都不影响这个数
Dans gpt.py, l'implémentation ne demande que quelques changements de forme en plus :
q, k, v = self.qkv(x).split(C, dim=2)
# 拆成多个头:(B, T, C) -> (B, 头数, T, 每个头的维度)
q, k, v = (t.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) for t in (q, k, v))
self.qkv est une couche linéaire qui calcule d'un coup les trois vecteurs q, k et v, séparés ensuite par split. On considère alors le vecteur de 32 dimensions comme 4 segments de 8, et on déplace la dimension « tête » vers l'avant. La notation, le masque, softmax et la somme pondérée qui suivent sont exactement ceux de la leçon précédente ; la multiplication matricielle de PyTorch calcule automatiquement chaque tête séparément, et traite les 4 têtes d'un coup.
Ensuite, on réassemble les résultats des 4 têtes en 32 dimensions, puis on les mélange par une couche linéaire proj, pour que les informations recueillies par chaque tête puissent se combiner :
out = out.transpose(1, 2).contiguous().view(B, T, C) # 各个头拼回去
return self.proj(out)
Regardez la dernière ligne de sortie : le nombre de paramètres ne dépend pas du nombre de têtes. 4 têtes de 8 dimensions ou 1 tête de 32 dimensions utilisent des matrices de même taille, simplement découpées différemment. Les têtes multiples n'ajoutent pas de paramètres, presque pas de calcul, mais permettent au modèle d'apprendre en même temps plusieurs façons de prêter attention.
Notre GPT utilise 4 têtes de 32 dimensions chacune (128 dimensions de vecteur au total). Les grands modèles ont généralement des dizaines de têtes, de 64 ou 128 dimensions chacune.
L'attention ne distingue pas l'ordre
Revenons à la formule d'attention de la leçon précédente : la notation ne regarde que le contenu de q et k, la somme pondérée que celui de v ; nulle part n'intervient « à quelle position se trouve ce caractère ».
Cela signifie que, pour l'attention, « 白日依山尽 » et « 山依日白尽 » pourraient ne pas différer.
Expérience : on fait lire ces deux vers au modèle ; tous deux se terminent par « 尽 », les quatre caractères précédents étant permutés. On compare de combien diffère la sortie du modèle à la dernière position (c'est-à-dire les scores pour le caractère suivant) :
model.pos_emb.weight.zero_() # 把位置嵌入清零,等于没有位置信息
a = model(torch.tensor([tok.encode("白日依山尽")]))[0][0, -1]
b = model(torch.tensor([tok.encode("山依日白尽")]))[0][0, -1]
== 1. '白日依山尽' 和 '山依日白尽',最后一个位置的输出差多少
1 层:没有位置嵌入 3.7e-08,有位置嵌入 2.8e-03
2 层:没有位置嵌入 1.9e-02,有位置嵌入 1.5e-02
D'abord la première ligne, un modèle d'une seule couche. Sans information de position, les sorties des deux vers diffèrent de 3,7e-08, c'est-à-dire seulement l'erreur d'arrondi des flottants : elles sont identiques. Pour la dernière position, les cinq caractères devant elle sont les mêmes, seul l'ordre change. L'attention les note et en fait la somme pondérée, et le résultat ne dépend pas de l'ordre.
Pour un modèle de langue, c'est fatal. « 白日依山尽 » est un poème, « 山依日白尽 » n'a pas de sens ; « 我打你 » (je te frappe) et « 你打我 » (tu me frappes) disent le contraire. Le modèle doit connaître l'ordre.
La deuxième ligne, inattendue
La deuxième ligne est un modèle à 2 couches. Selon le raisonnement ci-dessus, sans embedding de position, les sorties des deux vers devraient aussi être identiques. Mais l'expérience donne un écart de 1,9e-02, à peu près aussi grand que les 1,5e-02 avec embedding de position.
Je ne m'y attendais pas non plus en écrivant l'expérience. La cause tient au masque causal :
- Dans la couche 1, chaque position ne voit qu'elle-même et les caractères précédents. La 2e position de « 白日依山尽 » voit « 白日 », celle de « 山依日白尽 » voit « 山依 ». Les sorties de la couche 1 aux positions 2, 3 et 4 diffèrent donc entre les deux vers.
- Dans la couche 2, la dernière position regarde les sorties de la couche 1 aux positions précédentes, qui sont déjà différentes.
Autrement dit, le masque causal lui-même laisse fuiter l'ordre : le nombre de caractères qu'une position peut voir lui indique indirectement sa propre position. Dès qu'il y a deux couches ou plus, le modèle peut en tirer une certaine information de position.
Ce n'est pas un hasard de notre code. Un article de 2022 a étudié précisément ce phénomène (Haviv et al., « Transformer Language Models without Positional Encodings Still Learn Positional Information ») et a montré que des modèles de langue causaux sans aucun encodage de position apprennent aussi l'information de position, avec des résultats à peine inférieurs à ceux qui en ont un.
Mais l'information de position « devinée » grâce au masque est indirecte et floue. Les vrais modèles ajoutent tous explicitement l'information de position.
L'embedding de position
La méthode la plus simple, celle de GPT-2 : préparer aussi un vecteur pour chaque position, et l'additionner au vecteur du caractère.
self.pos_emb = nn.Embedding(cfg.block_size, cfg.n_embd)
...
pos = torch.arange(start, start + T, device=idx.device)
x = self.drop(self.tok_emb(idx) + self.pos_emb(pos))
pos_emb est une table de 128 lignes : la ligne 0 est le vecteur de la position 0, la ligne 1 celui de la position 1, et ainsi de suite. Ces vecteurs sont appris à l'entraînement, comme les autres paramètres.
Après l'addition, un même caractère a un vecteur différent selon sa position. « 白 » en position 1 et en position 4 donne deux vecteurs différents, et l'attention peut distinguer l'ordre. Dans l'expérience ci-dessus, avec l'embedding de position, l'écart entre les sorties des deux vers du modèle à 1 couche passe de 3,7e-08 à 2,8e-03 : le modèle sait distinguer les deux vers (il n'est pas encore entraîné, d'où un écart faible et encore dépourvu de sens).
Cette méthode a une limite : la table n'a que 128 lignes, le modèle ne peut traiter que 128 positions au plus. Pour une position jamais vue à l'entraînement, il n'a pas de vecteur.
L'encodage de position rotatif
En septembre 2026, la plupart des grands modèles open source majeurs (Llama, Qwen, DeepSeek, etc.) n'utilisent pas l'embedding de position de GPT-2, mais l'encodage de position rotatif (RoPE, issu de l'article RoFormer de 2021).
L'idée : ne pas additionner la position au vecteur du caractère, mais, avant de calculer les scores d'attention, « faire tourner » q et k d'un angle selon leur position respective. Plus la position est tardive, plus la rotation est grande. Ainsi, quand on fait le produit scalaire des q et k de deux positions, le résultat ne dépend que de leur distance, et non de leurs positions absolues respectives.
« 月 » vient un caractère après « 明 » ; que ces deux caractères apparaissent au début ou à la fin d'un poème, leur relation est la même. La position relative correspond mieux aux régularités de la langue que la position absolue, et se généralise plus facilement à des textes plus longs que ceux de l'entraînement.
Le GPT de ce cours garde l'embedding de position le plus simple, qui suffit amplement pour nos expériences et se comprend facilement. L'exercice 3 vous fait essayer RoPE.
Exercices
- Ajoutez un groupe à
heads_positions.py: un modèle à 3 couches sans embedding de position. De combien diffèrent les sorties des deux vers ? - Passez le modèle d'entraînement de
gpt.pyde 4 têtes à 1 puis à 8 (n_embdrestant à 128), entraînez chacun 1000 pas avec letrain.pyde la leçon suivante, et comparez les pertes de validation. - Défi : en vous appuyant sur l'article RoFormer ou sur le code d'un modèle open source, ajoutez l'encodage de position rotatif à
CausalSelfAttention, retirezpos_emb, et observez l'effet sur l'entraînement.
Auto-test
1. Comment fonctionne l'attention multi-têtes ? Ajoute-t-elle des paramètres ?
Le vecteur de chaque caractère est découpé en plusieurs segments ; sur chacun, on fait une attention (avec ses propres q, k, v et poids), puis on réassemble les résultats et on les mélange par une couche linéaire. Le nombre de paramètres est le même qu'avec une seule tête, seules les matrices sont découpées autrement ; l'avantage est que chaque tête peut apprendre à s'intéresser à autre chose.
2. Pourquoi l'attention a-t-elle besoin d'information de position ?
La notation et la somme pondérée de l'attention ne regardent que le contenu des vecteurs, pas leur position. Pour un modèle d'une seule couche, permuter les caractères précédents ne change absolument pas la sortie de la dernière position. Or le sens de la langue dépend de l'ordre ; il faut donc ajouter l'information de position.
3. Dans l'expérience, pourquoi le modèle à 2 couches sans embedding de position distingue-t-il quand même l'ordre des deux vers ?
Le masque causal ne laisse chaque position voir qu'elle-même et les caractères précédents ; dans la première couche, les différentes positions voient des nombres de caractères différents, et les sorties dépendent alors de l'ordre. Quand la deuxième couche lit ces sorties, elle obtient indirectement une information de position. Des recherches ont montré que des modèles de langue causaux sans encodage de position apprennent aussi l'information de position, mais les vrais modèles ajoutent tout de même explicitement un encodage de position.