Chapitre 1 : Mécanisme d'attention
Introduction
Le mécanisme d'attention est l'innovation centrale qui rend les Transformers possibles. Il permet aux modèles de se concentrer dynamiquement sur les parties pertinentes de l'entrée lors de la production de chaque sortie, surmontant les limitations des vecteurs de contexte de taille fixe.
L'intuition derrière l'attention
Une analogie humaine
Pensez à la façon dont vous lisez une phrase complexe :
"L'animal n'a pas traversé la rue parce qu'il était trop fatigué."
Pour comprendre à quoi "il" se réfère, vous vous concentrez mentalement sur "l'animal" plutôt que sur "la rue". C'est exactement ce que fait l'attention de manière computationnelle.
L'idée principale
L'attention calcule une somme pondérée de valeurs, où les poids sont déterminés par la pertinence de chaque valeur par rapport à une requête donnée :
Query, Key, Value : Les trois projections
Comprendre Q, K, V
| Composant | Rôle | Analogie |
|---|---|---|
| Query (Q) | Ce que nous recherchons | Une requête de recherche |
| Key (K) | Ce que chaque position offre | Entrées d'index |
| Value (V) | Le contenu réel | Les documents |
Définition mathématique
Étant donné une entrée (séquence de n tokens, chacun avec d dimensions) :
Où :
Implémentation
import torch
import torch.nn as nn
import math
class AttentionProjections(nn.Module):
def __init__(self, d_model, d_k, d_v):
super().__init__()
self.W_Q = nn.Linear(d_model, d_k, bias=False)
self.W_K = nn.Linear(d_model, d_k, bias=False)
self.W_V = nn.Linear(d_model, d_v, bias=False)
def forward(self, x):
"""
x: (batch, seq_len, d_model)
Returns Q, K, V projections
"""
Q = self.W_Q(x) # (batch, seq_len, d_k)
K = self.W_K(x) # (batch, seq_len, d_k)
V = self.W_V(x) # (batch, seq_len, d_v)
return Q, K, V
Attention par produit scalaire pondéré
Calcul étape par étape
Étape 1 : Calculer les scores d'attention
Cela nous donne une matrice où l'entrée représente combien la position doit s'attendre à la position .
# Q: (batch, seq_len, d_k)
# K: (batch, seq_len, d_k)
scores = torch.matmul(Q, K.transpose(-2, -1))
# scores: (batch, seq_len, seq_len)
Étape 2 : Mettre à l'échelle les scores
Sans mise à l'échelle, pour un grand , les produits scalaires peuvent devenir très grands en magnitude, poussant softmax dans des régions avec des gradients extrêmement petits. La mise à l'échelle par maintient la variance stable.
d_k = Q.size(-1)
scaled_scores = scores / math.sqrt(d_k)
Étape 3 : Appliquer Softmax
attention_weights = torch.softmax(scaled_scores, dim=-1)
# Chaque ligne somme à 1
Étape 4 : Somme pondérée des valeurs
output = torch.matmul(attention_weights, V)
# output: (batch, seq_len, d_v)
Implémentation complète
def scaled_dot_product_attention(Q, K, V, mask=None):
"""
Compute scaled dot-product attention.
Args:
Q: Query tensor (batch, seq_len, d_k)
K: Key tensor (batch, seq_len, d_k)
V: Value tensor (batch, seq_len, d_v)
mask: Optional mask tensor
Returns:
output: Attention output (batch, seq_len, d_v)
attention_weights: Attention weights (batch, seq_len, seq_len)
"""
d_k = Q.size(-1)
# Step 1 & 2: Compute scaled scores
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
# Optional: Apply mask (for causal attention)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
# Step 3: Softmax
attention_weights = torch.softmax(scores, dim=-1)
# Step 4: Weighted sum
output = torch.matmul(attention_weights, V)
return output, attention_weights
Visualisation de l'attention
Matrice d'attention
Pour une phrase comme "Le chat est assis" :
Le chat est assis
Le [ 0.7 0.2 0.1 ]
chat [ 0.3 0.5 0.2 ]
est [ 0.2 0.4 0.4 ]
Chaque ligne montre combien cette position s'attend à toutes les autres positions.
Code pour visualiser
import matplotlib.pyplot as plt
import seaborn as sns
def visualize_attention(attention_weights, tokens):
"""
Visualize attention weights as a heatmap.
"""
plt.figure(figsize=(10, 8))
sns.heatmap(
attention_weights.detach().numpy(),
xticklabels=tokens,
yticklabels=tokens,
cmap='Blues',
annot=True,
fmt='.2f'
)
plt.xlabel('Keys')
plt.ylabel('Queries')
plt.title('Attention Weights')
plt.show()
Attention causale (masquée)
Pour les modèles de langage autoregressifs, chaque position ne doit s'attendre qu'aux positions précédentes.
Le masque causal
def create_causal_mask(seq_len):
"""
Create a causal mask for autoregressive attention.
Position i can only attend to positions <= i.
"""
mask = torch.tril(torch.ones(seq_len, seq_len))
return mask
# Exemple pour seq_len=4 :
# [[1, 0, 0, 0],
# [1, 1, 0, 0],
# [1, 1, 1, 0],
# [1, 1, 1, 1]]
Pourquoi le masquage causal ?
Pendant l'entraînement, nous voulons que le modèle prédise le prochain token uniquement en se basant sur les tokens précédents, pas sur ceux futurs (qu'il n'aura pas lors de l'inférence).
def causal_attention(Q, K, V):
"""
Attention with causal masking.
"""
seq_len = Q.size(1)
mask = create_causal_mask(seq_len).to(Q.device)
return scaled_dot_product_attention(Q, K, V, mask)
Complexité de l'attention
Complexité temporelle
Où :
- = longueur de la séquence
- = dimension du modèle
Cette complexité quadratique en longueur de séquence est la raison pour laquelle les modèles à long contexte sont difficiles.
Complexité mémoire
Pour la matrice d'attention elle-même (stockant tous les scores par paire).
Techniques d'optimisation
Plusieurs approches traitent de la complexité quadratique de l'attention :
- Flash Attention : Implémentation efficace en mémoire
- Linear Attention : Approximation de l'attention en O(n)
- Sparse Attention : Ne s'attend qu'à un sous-ensemble de positions
- Sliding Window : Attention locale avec tokens globaux
Mettre tout ensemble
class Attention(nn.Module):
"""
Complete attention module.
"""
def __init__(self, d_model, d_k=None, d_v=None, causal=False):
super().__init__()
d_k = d_k or d_model
d_v = d_v or d_model
self.W_Q = nn.Linear(d_model, d_k, bias=False)
self.W_K = nn.Linear(d_model, d_k, bias=False)
self.W_V = nn.Linear(d_model, d_v, bias=False)
self.W_O = nn.Linear(d_v, d_model, bias=False)
self.causal = causal
self.d_k = d_k
def forward(self, x, mask=None):
batch_size, seq_len, _ = x.size()
# Project to Q, K, V
Q = self.W_Q(x)
K = self.W_K(x)
V = self.W_V(x)
# Compute attention
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
# Apply causal mask if needed
if self.causal:
causal_mask = torch.tril(torch.ones(seq_len, seq_len, device=x.device))
scores = scores.masked_fill(causal_mask == 0, float('-inf'))
# Apply additional mask if provided
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
# Softmax and weighted sum
attn_weights = torch.softmax(scores, dim=-1)
output = torch.matmul(attn_weights, V)
# Output projection
output = self.W_O(output)
return output, attn_weights
Points clés à retenir
- L'attention calcule des sommes pondérées basées sur la pertinence
- Q, K, V sont trois projections de l'entrée avec des rôles distincts
- La mise à l'échelle par stabilise l'entraînement
- Le masquage causal empêche de regarder les tokens futurs
- La complexité est en longueur de séquence
À venir
Dans le prochain chapitre, nous explorerons comment l'attention est utilisée au sein d'une seule séquence (auto-attention) et comment plusieurs mécanismes d'attention sont combinés (attention multi-tête).