Aller au contenu principal

Chapitre 3 : Encodage positionnel et embeddings

Introduction​

Contrairement aux RNNs qui traitent les séquences token par token, les Transformers traitent toutes les positions simultanément. Cela crée un défi fondamental : comment le modèle connaît-il l'ordre des tokens ? Ce chapitre explique l'encodage positionnel et le processus plus large d'embedding des tokens.

Le problème de la position​

Pourquoi la position est importante​

Considérez ces phrases :

  • "Le chien a mordu l'homme"
  • "L'homme a mordu le chien"

Mêmes tokens, significations complètement différentes. La position est cruciale pour comprendre le langage.

Transformers et invariance par permutation​

Sans information positionnelle, l'auto-attention est invariante par permutation :

SelfAttention(π(X))=π(SelfAttention(X))\text{SelfAttention}(\pi(X)) = \pi(\text{SelfAttention}(X))

Où π\pi est n'importe quelle permutation. Le modèle traite "chien mordu homme" et "homme mordu chien" de manière identique !

# Démonstration : les scores d'attention sont indépendants de la position
import torch

def attention_scores(Q, K):
return torch.softmax(Q @ K.T, dim=-1)

# Séquence originale
x1 = torch.tensor([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])

# Séquence permutée (échanger les positions 0 et 1)
x2 = torch.tensor([[0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])

# Même motif d'attention (juste permuté) !
print(attention_scores(x1, x1))
print(attention_scores(x2, x2))

Embeddings de tokens​

Des tokens aux vecteurs​

Avant d'ajouter l'information positionnelle, nous convertissons d'abord les tokens en vecteurs denses :

E=Embedding(token_ids)E = \text{Embedding}(\text{token\_ids})

Implémentation​

import torch
import torch.nn as nn

class TokenEmbedding(nn.Module):
"""
Convertir les IDs de tokens en vecteurs denses.
"""
def __init__(self, vocab_size, d_model):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
self.d_model = d_model

def forward(self, token_ids):
"""
token_ids : (batch, seq_len) d'IDs de tokens entiers
Retourne : (batch, seq_len, d_model) embeddings
"""
# Échelle des embeddings (à partir de l'article original sur les Transformers)
return self.embedding(token_ids) * (self.d_model ** 0.5)

# Exemple
vocab_size = 50000
d_model = 512
embedding_layer = TokenEmbedding(vocab_size, d_model)

tokens = torch.tensor([[101, 7592, 2088, 102]]) # [CLS] Bonjour Monde [SEP]
embeddings = embedding_layer(tokens)
print(f"Shape: {embeddings.shape}") # (1, 4, 512)

Encodage positionnel sinusoïdal​

L'approche originale​

L'article original sur les Transformers utilisait des fonctions sinusoïdales fixes :

PE(pos,2i)=sin⁡(pos100002i/dmodel)PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i+1)=cos⁡(pos100002i/dmodel)PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right)

Où :

  • pospos = position dans la séquence
  • ii = indice de dimension
  • dmodeld_{model} = dimension du modèle

Intuition​

Chaque dimension oscille à une fréquence différente :

  • Basses dimensions : oscillation lente (capture les positions à longue portée)
  • Hautes dimensions : oscillation rapide (capture les positions fines)

Implémentation​

import numpy as np

def get_sinusoidal_encoding(max_len, d_model):
"""
Générer des encodages positionnels sinusoïdaux.

Args:
max_len: Longueur maximale de la séquence
d_model: Dimension du modèle

Returns:
Matrice d'encodage positionnel (max_len, d_model)
"""
position = np.arange(max_len)[:, np.newaxis]
div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))

pe = np.zeros((max_len, d_model))
pe[:, 0::2] = np.sin(position * div_term) # Indices pairs
pe[:, 1::2] = np.cos(position * div_term) # Indices impairs

return pe

# Visualiser
import matplotlib.pyplot as plt

pe = get_sinusoidal_encoding(100, 512)
plt.figure(figsize=(12, 4))
plt.imshow(pe.T, aspect='auto', cmap='RdBu')
plt.xlabel('Position')
plt.ylabel('Dimension')
plt.colorbar(label='Valeur')
plt.title('Encodage positionnel sinusoïdal')
plt.show()

Propriétés de l'encodage sinusoïdal​

  1. Unique : Chaque position a un encodage unique
  2. Borné : Valeurs toujours dans [-1, 1]
  3. Positions relatives : La relation linéaire permet d'apprendre les positions relatives
  4. Extrapolation : Peut se généraliser à des séquences plus longues que celles vues lors de l'entraînement

Implémentation PyTorch​

class SinusoidalPositionalEncoding(nn.Module):
"""
Encodage positionnel sinusoïdal de 'Attention Is All You Need'.
"""
def __init__(self, d_model, max_len=5000, dropout=0.1):
super().__init__()
self.dropout = nn.Dropout(p=dropout)

# Créer la matrice d'encodage positionnel
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2).float() * (-np.log(10000.0) / d_model)
)

pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0) # Ajouter la dimension batch

# Enregistrer comme tampon (pas un paramètre, mais sauvegardé avec le modèle)
self.register_buffer('pe', pe)

def forward(self, x):
"""
x : (batch, seq_len, d_model)
"""
x = x + self.pe[:, :x.size(1)]
return self.dropout(x)

Embeddings positionnels appris​

Une approche alternative​

De nombreux LLM modernes utilisent des embeddings de position appris :

class LearnedPositionalEmbedding(nn.Module):
"""
Embeddings positionnels appris (par exemple, utilisés dans GPT-2).
"""
def __init__(self, max_len, d_model):
super().__init__()
self.position_embedding = nn.Embedding(max_len, d_model)

def forward(self, x):
"""
x : (batch, seq_len, d_model)
"""
positions = torch.arange(x.size(1), device=x.device)
return x + self.position_embedding(positions)

Comparaison​

AspectSinusoïdalAppris
ParamètresAucunmax_len × d_model
ExtrapolationMeilleureLimitée à max_len
PerformanceComparableComparable
FlexibilitéFixeS'adapte aux données

Embedding de position rotatif (RoPE)​

Innovation moderne​

RoPE, utilisé dans LLaMA, Mistral, et de nombreux modèles modernes, encode la position par rotation :

fq(xm,m)=xmeimθf_q(x_m, m) = x_m e^{im\theta}

Principaux avantages​

  1. Positions relatives : Capture naturellement l'information de position relative
  2. Extrapolation : Meilleure gestion des séquences plus longues
  3. Efficacité : Appliqué directement à Q, K sans paramètres supplémentaires

Implémentation simplifiée​

def rotary_embedding(x, seq_len, d_model):
"""
Implémentation simplifiée de RoPE.
x : (batch, seq_len, d_model)
"""
# Générer les angles de rotation
position = torch.arange(seq_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
angles = position * div_term

# Appliquer la rotation aux paires de dimensions
x_rotated = x.clone()
x_rotated[..., 0::2] = x[..., 0::2] * torch.cos(angles) - x[..., 1::2] * torch.sin(angles)
x_rotated[..., 1::2] = x[..., 0::2] * torch.sin(angles) + x[..., 1::2] * torch.cos(angles)

return x_rotated

ALiBi : Attention avec biais linéaires​

Une autre approche​

ALiBi (Attention with Linear Biases) ajoute une pénalité linéaire basée sur la distance :

softmax(qi⋅kj−m⋅∣i−j∣)\text{softmax}(q_i \cdot k_j - m \cdot |i - j|)

Où mm est une pente spécifique à la tête.

Avantages​

  1. Aucun paramètre supplémentaire
  2. Excellente extrapolation de longueur
  3. Implémentation simple

Couche d'embedding complète​

Tout mettre ensemble​

class TransformerEmbedding(nn.Module):
"""
Couche d'embedding complète : token + position.
"""
def __init__(self, vocab_size, d_model, max_len=5000,
pos_encoding='sinusoidal', dropout=0.1):
super().__init__()

self.token_embedding = nn.Embedding(vocab_size, d_model)
self.d_model = d_model
self.dropout = nn.Dropout(dropout)

if pos_encoding == 'sinusoidal':
self.pos_encoding = SinusoidalPositionalEncoding(d_model, max_len)
elif pos_encoding == 'learned':
self.pos_encoding = LearnedPositionalEmbedding(max_len, d_model)
else:
raise ValueError(f"Unknown pos_encoding: {pos_encoding}")

def forward(self, token_ids):
"""
token_ids : (batch, seq_len)
Retourne : (batch, seq_len, d_model)
"""
# Embeddings de tokens (échelonnés)
x = self.token_embedding(token_ids) * (self.d_model ** 0.5)

# Ajouter l'encodage positionnel
x = self.pos_encoding(x)

return self.dropout(x)

# Utilisation
embedding = TransformerEmbedding(
vocab_size=50000,
d_model=512,
max_len=2048,
pos_encoding='sinusoidal'
)

tokens = torch.randint(0, 50000, (32, 100)) # Batch de 32, seq_len 100
embedded = embedding(tokens)
print(f"Output shape: {embedded.shape}") # (32, 100, 512)

La vue d'ensemble​

Voici comment les tokens passent des embeddings à l'attention :

IDs de tokens : [101, 7592, 2088, 102]
↓
Embedding de tokens : Recherche dans la table d'embedding
↓
[0.23, -0.45, ...] [0.89, 0.12, ...] [0.45, -0.23, ...] [0.67, 0.34, ...]
↓
Ajouter l'encodage positionnel
↓
[0.23+PE_0, -0.45+PE_0, ...] [0.89+PE_1, 0.12+PE_1, ...] ...
↓
Attention multi-tête
↓
Sortie avec contexte conscient de la position

Points clés à retenir​

Résumé
  1. L'information de position est essentielle—les Transformers sont autrement invariants par permutation
  2. Les embeddings de tokens convertissent les tokens discrets en vecteurs continus
  3. L'encodage sinusoïdal utilise des motifs de fréquence fixes (Transformer original)
  4. Les embeddings appris s'adaptent aux données mais ont une longueur maximale fixe
  5. RoPE encode les positions relatives par rotation (LLMs modernes)
  6. ALiBi utilise des biais d'attention basés sur la distance

À venir​

Maintenant que nous comprenons l'architecture complète du Transformer—attention, attention multi-tête, et encodage positionnel—nous sommes prêts à passer au Module 3, où nous explorerons comment ces modèles massifs sont entraînés.