Aller au contenu principal

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 :

Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V

Query, Key, Value : Les trois projections​

Comprendre Q, K, V​

ComposantRôleAnalogie
Query (Q)Ce que nous recherchonsUne requête de recherche
Key (K)Ce que chaque position offreEntrées d'index
Value (V)Le contenu réelLes documents

Définition mathématique​

Étant donné une entrée X∈Rn×dX \in \mathbb{R}^{n \times d} (séquence de n tokens, chacun avec d dimensions) :

Q=XWQ,K=XWK,V=XWVQ = XW_Q, \quad K = XW_K, \quad V = XW_V

Où :

  • WQ∈Rd×dkW_Q \in \mathbb{R}^{d \times d_k}
  • WK∈Rd×dkW_K \in \mathbb{R}^{d \times d_k}
  • WV∈Rd×dvW_V \in \mathbb{R}^{d \times d_v}

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

scores=QKT\text{scores} = QK^T

Cela nous donne une matrice où l'entrée (i,j)(i, j) représente combien la position ii doit s'attendre à la position jj.

# 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

scaled_scores=QKTdk\text{scaled\_scores} = \frac{QK^T}{\sqrt{d_k}}
Pourquoi mettre à l'échelle ?

Sans mise à l'échelle, pour un grand dkd_k, 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 dk\sqrt{d_k} maintient la variance stable.

d_k = Q.size(-1)
scaled_scores = scores / math.sqrt(d_k)

Étape 3 : Appliquer Softmax

attention_weights=softmax(scaled_scores)\text{attention\_weights} = \text{softmax}(\text{scaled\_scores})
attention_weights = torch.softmax(scaled_scores, dim=-1)
# Chaque ligne somme à 1

Étape 4 : Somme pondérée des valeurs

output=attention_weights⋅V\text{output} = \text{attention\_weights} \cdot V
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(n2⋅d)O(n^2 \cdot d)

Où :

  • nn = longueur de la séquence
  • dd = 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​

O(n2)O(n^2)

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 :

  1. Flash Attention : Implémentation efficace en mémoire
  2. Linear Attention : Approximation de l'attention en O(n)
  3. Sparse Attention : Ne s'attend qu'à un sous-ensemble de positions
  4. 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​

Résumé
  1. L'attention calcule des sommes pondérées basées sur la pertinence
  2. Q, K, V sont trois projections de l'entrée avec des rôles distincts
  3. La mise à l'échelle par dk\sqrt{d_k} stabilise l'entraînement
  4. Le masquage causal empêche de regarder les tokens futurs
  5. La complexité est O(n2)O(n^2) 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).