Aller au contenu
The loss curve

Chapitre 9 · 14 min

Multi-têtes et résidus

Combine les têtes, normalise les flux résiduels et rapproche ton code d’attention local d’un bloc transformer empilable.

Au chapitre 8, tu as construit une tête d’. Elle calculait un motif d’ — une matrice de qui écoute qui — et réarrangeait les représentations des en conséquence. C’est un seul motif de routage par couche.

Ce n’est pas assez. Un vrai texte contient plusieurs relations simultanées : accord sujet/verbe, résolution de pronoms, rattachement des modificateurs, similarité sémantique, biais de position. N’importe laquelle peut être la bonne chose à regarder selon le . Un modèle avec une seule par couche doit choisir.

La correction est celle qu’on devine : lancer plusieurs têtes en parallèle. Chaque tête a ses propres W_Q, W_K, W_V, donc chacune finit avec un motif d’ différent. On concatène leurs sorties, on projette le résultat, on passe à la suite. C’est l’ .

Ce chapitre introduit aussi les connexions et la — les deux pièces de plomberie qui permettent d’empiler réellement des couches d’ sans que le réseau s’effondre à l’. Elles ressemblent à des détails ajoutés après coup ; elles sont porteuses. En local, tu ajouteras ces opérations qui préservent la forme aux pièces que tu as déjà écrites.

Même phrase jouet qu’au chapitre 8 (the cat sat on mat), mais avec d_model = 8 et H = 4 têtes, donc d_head = 2 pour chaque tête.

1. Combiner plusieurs têtes

Tu as déjà une tête d’ qui fonctionne. Le chapitre a pré-calculé 4 têtes différentes (des graines aléatoires différentes pour W_Q, W_K, W_V) et te donne la sortie de chacune sous forme de matrice [seq_len × d_head]. La cellule les combine.

La recette standard :

  1. Concaténer la sortie de chaque tête sur l’axe des features. La ligne de chaque devient [head₀, head₁, head₂, head₃], de longueur H × d_head = d_model.
  2. Projeter la matrice concaténée à travers un W_O appris, de forme [d_model × d_model].

La projection de sortie laisse le modèle décider comment mélanger les têtes. Sans elle, les sorties des têtes seraient simplement collées bout à bout, sans aucune chance d’interagir.

À toi de jouer · JavaScript

Le résultat a la même forme que l’entrée — [seq_len × d_model] — mais la nouvelle représentation de chaque reflète désormais quatre motifs d’ différents, mélangés.

2. Inspecter ce que les têtes apprennent vraiment

On a affirmé que les têtes voient des choses différentes. Vérifions. Les quatre matrices d’ pré-calculées sont affichées ci-dessous en heatmaps. Certaines sont nettes (quelques cellules dominent chaque ligne) ; d’autres sont diffuses (la masse est répartie uniformément sur la ligne).

Une façon standard de quantifier la « concentration » est l’ : H = -Σ p log p. Une basse veut dire que la tête se concentre sur peu de . L’ maximale pour une ligne de 5 vaut log(5) ≈ 1,61.

Calcule l’ moyenne par tête.

À toi de jouer · JavaScript

Attention patterns across heads · log(n) ≈ 1.61 = maximum entropy for 5 tokens

Head 0

thecatsatonmatthecatsatonmatthe, the: 0.240the, cat: 0.172the, sat: 0.211the, on: 0.193the, mat: 0.183cat, the: 0.181cat, cat: 0.218cat, sat: 0.191cat, on: 0.206cat, mat: 0.203sat, the: 0.210sat, cat: 0.198sat, sat: 0.198sat, on: 0.206sat, mat: 0.187on, the: 0.193on, cat: 0.201on, sat: 0.201on, on: 0.195on, mat: 0.210mat, the: 0.193mat, cat: 0.213mat, sat: 0.192mat, on: 0.211mat, mat: 0.192

Head 1

thecatsatonmatthecatsatonmatthe, the: 0.196the, cat: 0.205the, sat: 0.198the, on: 0.193the, mat: 0.207cat, the: 0.197cat, cat: 0.194cat, sat: 0.200cat, on: 0.210cat, mat: 0.198sat, the: 0.197sat, cat: 0.205sat, sat: 0.199sat, on: 0.193sat, mat: 0.206on, the: 0.188on, cat: 0.196on, sat: 0.198on, on: 0.210on, mat: 0.208mat, the: 0.204mat, cat: 0.198mat, sat: 0.201mat, on: 0.202mat, mat: 0.195

Head 2

thecatsatonmatthecatsatonmatthe, the: 0.234the, cat: 0.180the, sat: 0.205the, on: 0.201the, mat: 0.181cat, the: 0.169cat, cat: 0.223cat, sat: 0.192cat, on: 0.197cat, mat: 0.220sat, the: 0.206sat, cat: 0.195sat, sat: 0.202sat, on: 0.200sat, mat: 0.196on, the: 0.217on, cat: 0.192on, sat: 0.201on, on: 0.201on, mat: 0.190mat, the: 0.156mat, cat: 0.229mat, sat: 0.191mat, on: 0.195mat, mat: 0.229

Head 3

thecatsatonmatthecatsatonmatthe, the: 0.215the, cat: 0.194the, sat: 0.200the, on: 0.189the, mat: 0.201cat, the: 0.189cat, cat: 0.205cat, sat: 0.200cat, on: 0.209cat, mat: 0.197sat, the: 0.190sat, cat: 0.203sat, sat: 0.196sat, on: 0.203sat, mat: 0.209on, the: 0.199on, cat: 0.201on, sat: 0.202on, on: 0.203on, mat: 0.196mat, the: 0.185mat, cat: 0.204mat, sat: 0.195mat, on: 0.206mat, mat: 0.210

Tu devrais voir des différences nettes entre les têtes. Certaines finissent presque uniformes ( haute — la tête ne préfère rien en particulier). D’autres ont des pics marqués ( basse — la tête a décidé de se concentrer). Dans un vrai entraîné, on trouve des têtes spécialisées : têtes de copie, têtes d’induction, têtes qui suivent les noms propres, têtes qui suivent la syntaxe. On a nommé des motifs qu’on a trouvés, pas des motifs qu’on avait demandés.

3. Résiduel + LayerNorm

Si on branchait la sous-couche d’ directement dans la sous-couche d’ suivante, l’ s’effondrerait. Deux raisons, toutes deux pénibles :

  • qui disparaît. Chaque sous-couche comprime un peu le signal du . Empiles-en 12 et le tout en bas est microscopique.
  • Dérive des représentations. Chaque sous-couche transforme les activations dans une géométrie différente. Empiles-en beaucoup et les magnitudes explosent ou s’écrasent à zéro.

La connexion règle le premier problème : au lieu de output = sublayer(input), on écrit output = input + sublayer(input). Le dispose maintenant d’un chemin propre pour revenir en arrière à travers l’addition, quoi que fasse la sous-couche. (C’est l’astuce qui a rendu les ResNets praticables en 2015, et elle est aujourd’hui dans tous les modèles profonds.)

La règle le second : après l’addition , on normalise la ligne de chaque pour qu’elle ait une moyenne de 0 et un écart-type de 1. La couche suivante voit des activations d’échelle connue, quoi qu’il se soit passé avant.

Ensemble, la recette par sous-couche est :

output=LayerNorm(input+sublayer(input))\text{output} = \text{LayerNorm}(\text{input} + \text{sublayer}(\text{input}))

Exécute-la. Le chapitre donne à la cellule l’input d’origine (X) et sublayerOutput (la sortie multi-têtes de la cellule 1), plus un petit eps pour la stabilité numérique dans la division par l’écart-type.

À toi de jouer · JavaScript

Regarde les statistiques de ligne. La moyenne de chaque ligne doit valoir 0 (à la précision des flottants près) et son écart-type doit être très proche de 1. C’est l’invariant que la offre à la sous-couche suivante : chaque , à chaque couche, arrive à la même échelle.

Pourquoi ce chapitre compte

Nous avons maintenant toutes les pièces d’un bloc :

  • L’ (chapitre 8) : aller chercher de l’information à d’autres positions.
  • Le (ce chapitre) : faire tourner plusieurs routes d’ en parallèle.
  • Résiduel + (ce chapitre) : la connectivité et la normalisation qui permettent d’empiler beaucoup de blocs.

Le chapitre suivant les assemble en un bloc complet, puis en empile plusieurs pour former l’architecture réelle.

4. Ajouter les helpers résiduels et de normalisation

Ajoute ces helpers à llm/nn.py :

import math
 
 
def add(a: Matrix, b: Matrix) -> Matrix:
    return [
        [x + y for x, y in zip(row_a, row_b)]
        for row_a, row_b in zip(a, b)
    ]
 
 
def layer_norm(x: Matrix, eps: float = 1e-5) -> Matrix:
    out: Matrix = []
    for row in x:
        mean = sum(row) / len(row)
        var = sum((value - mean) ** 2 for value in row) / len(row)
        denom = math.sqrt(var + eps)
        out.append([(value - mean) / denom for value in row])
    return out

Ces helpers sont petits parce que leur rôle est structurel :

  • add est la connexion . Elle garde l’ancienne représentation et y ajoute le changement proposé par la sous-couche.
  • layer_norm travaille ligne par ligne, donc chaque est normalisé indépendamment des autres.
  • mean recentre les features d’un autour de zéro.
  • var mesure à quel point ces features sont dispersées.
  • Diviser par sqrt(var + eps) donne à la couche suivante une échelle prévisible. eps évite la division par zéro.

Tu as maintenant l’invariant dont dépend un : chaque sous-couche accepte une matrice et retourne une matrice de la même forme, pour que le flux puisse continuer à circuler.

Recap

  • L’ lance H calculs d’ en parallèle, avec des projections Q/K/V séparées par tête, puis concatène les sorties et les projette avec W_O.
  • Des têtes différentes apprennent des motifs différents, simplement parce qu’elles partent de poids aléatoires différents et sont entraînées sur la même . La recherche en interprétabilité catalogue les motifs récurrents.
  • Connexion = output = input + sublayer(input). Elle laisse les circuler proprement dans les piles profondes.
  • La normalise la ligne de chaque à moyenne 0 et écart-type 1. Elle stabilise l’échelle des activations d’une couche à l’autre.
  • Ton projet local a maintenant les helpers et , la colle qui permet à l’ de s’empiler.
  • Le bloc n’est que ces trois pièces collées ensemble, deux fois : une fois pour l’, une fois pour le . Le chapitre suivant assemble le tout.

Pour aller plus loin

Prochaine étape : le bloc transformer complet — combiner tout ce qu’on a construit dans l’unité qu’on empile réellement.