Transformer : Guide Complet — Architecture Transformer et Self-Attention

Transformer : Guide Complet — Architecture Transformer et Self-Attention

Transformer : Guide complet — Architecture Transformer et Self-Attention

Résumé — Le Transformer, introduit par Vaswani et al. dans l’article Attention Is All You Need (2017), est une architecture de réseau de neurones qui a révolutionné le traitement des séquences. Contrairement aux RNN et LSTM qui traitent les données séquentiellement, le Transformer utilise exclusivement des mécanismes d’attention pour connecter chaque élément de la séquence à tous les autres. Cette architecture est à la base de BERT, GPT, T5 et de tous les grands modèles de langage modernes.


Principe mathématique

1. Scaled Dot-Product Attention

L’attention fondamentale repose sur la formule Query-Key-Value :

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

  • Q (Query) : ce que je cherche
  • K (Key) : ce que chaque élément peut offrir
  • V (Value) : le contenu réel de chaque élément
  • $\sqrt{d_k}$ : facteur d’échelle pour éviter les softmax saturés lorsque $d_k$ est grand

Le produit $QK^\top$ calcule la similarité entre chaque query et chaque key. Le softmax transforme ces scores en poids de distribution. Enfin, ces poids pondèrent les values pour produire la sortie.

2. Multi-Head Attention

Au lieu d’une seule attention, on utilise plusieurs têtes en parallèle :

$$\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, …, \text{head}_h) W^O$$

$$\text{head}_i = \text{Attention}(Q W_i^Q, K W_i^K, V W_i^V)$$

Chaque tête apprend un aspect différent des relations (syntaxe, sémantique, dépendance à longue distance, etc.) et la concaténation fusionne ces perspectives.

3. Positional Encoding

L’attention n’a pas de notion de position intrinsèque. On ajoute un encodage positionnel aux embeddings :

Les dimensions paires utilisent le sinus et les dimensions impaires le cosinus, selon les équations standards :

$$
\mathrm{PE}(pos, 2i) = \sin\left(\frac{pos}{10000^{2i / d_{\text{model}}}}\right)
$$

$$
\mathrm{PE}(pos, 2i+1) = \cos\left(\frac{pos}{10000^{2i / d_{\text{model}}}}\right)
$$

Ces signaux sinusoïdaux permettent au modèle de déduire les positions relatives entre tokens grâce aux propriétés des fonctions trigonométriques.

4. Architecture Encoder-Decoder

Encoder : $N$ couches identiques, chacune composée de :
1. Multi-Head Self-Attention
2. Add & Norm ($x + \text{Sublayer}(x)$ puis LayerNorm)
3. Feed-Forward : $\text{FFN}(x) = \max(0, x W_1 + b_1) W_2 + b_2$
4. Add & Norm

Decoder : $N$ couches identiques avec une couche supplémentaire :
1. Multi-Head Self-Attention (avec masque causal pour empêcher la fuite d’information future)
2. Multi-Head Cross-Attention (requêtes du décodeur, clés/valeurs de l’encoder)
3. Feed-Forward
4. Add & Norm à chaque étape

5. Variantes architecturales

Le Transformer original est encoder-decoder, mais deux familles dérivées dominent aujourd’hui :

Architecture Couches Usage Exemples
Encoder-only Encoder uniquement Compréhension (classification, NER) BERT, RoBERTa, DeBERTa
Decoder-only Décodeur uniquement Génération de texte GPT, LLaMA, Claude
Encoder-Decoder Les deux Traduction, résumé T5, BART, mBART

Intuition

Avant les Transformers, les RNN et LSTM lisaient un texte mot par mot, comme un lecteur lent qui avance lettre par lettre. Pour comprendre la fin d’une longue phrase, ils devaient se souvenir du début à travers leur état caché, ce qui causait des pertes d’information.

Le Transformer, lui, lit toute la phrase en même temps.

Pensez à la différence entre :
– Lire une phrase en la découvrant lettre par lettre à travers un trou d’épingle (RNN)
– Voir toute la phrase d’un coup et comprendre instantanément les liens entre les mots (Transformer)

Dans la phrase « Le chat que le chien poursuivait depuis ce matin s’est réfugié sur l’arbre le plus proche », pour comprendre à quoi se réfère « s’est réfugié », il faut remonter jusqu’à « chat ». Le RNN doit parcourir 14 mots séquentiellement pour faire ce lien. Le Transformer connecte directement « s’est réfugié » à « chat » en un seul calcul d’attention, indépendamment de la distance.

De plus, comme tous les tokens sont traités en parallèle, le Transformer est massivement plus rapide à entraîner sur GPU que les RNN.


Implémentation Python

1. Scaled Dot-Product Attention from scratch

import numpy as np

def scaled_dot_product_attention(Q, K, V, mask=None):
    """Attention(Q, K, V) = softmax(Q @ K^T / sqrt(d_k)) @ V"""
    d_k = Q.shape[-1]
    scores = Q @ K.T / np.sqrt(d_k)  # [seq_len, seq_len]

    if mask is not None:
        scores = np.where(mask == 0, -1e9, scores)

    scores = scores - np.max(scores, axis=-1, keepdims=True)
    weights = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True)
    output = weights @ V
    return output, weights

# Exemple d’utilisation
d_k = 64
Q = np.random.randn(10, d_k)  # 10 tokens, 64 dimensions
K = np.random.randn(10, d_k)
V = np.random.randn(10, d_k)

output, attn_weights = scaled_dot_product_attention(Q, K, V)
print(f"Attention weights shape: {attn_weights.shape}")  # (10, 10)

2. Positional Encoding

def positional_encoding(max_len, d_model):
    """Positional encoding sinusoidal."""
    pe = np.zeros((max_len, d_model))
    position = np.arange(0, max_len)[:, np.newaxis]
    div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))

    pe[:, 0::2] = np.sin(position * div_term)
    pe[:, 1::2] = np.cos(position * div_term)
    return pe

# Visualisation
pe = positional_encoding(100, 512)
print(f"Positional encoding: {pe.shape}")  # (100, 512)

3. Transformer Encoder Layer complète

import numpy as np

class TransformerEncoderLayer:
    def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_ff = d_ff
        self.head_dim = d_model // n_heads
        self.dropout = dropout

        # Projections pour multi-head attention
        self.W_Q = np.random.randn(d_model, d_model) * 0.01
        self.W_K = np.random.randn(d_model, d_model) * 0.01
        self.W_V = np.random.randn(d_model, d_model) * 0.01
        self.W_O = np.random.randn(d_model, d_model) * 0.01

        # Feed-forward
        self.W_1 = np.random.randn(d_model, d_ff) * 0.01
        self.b_1 = np.zeros(d_ff)
        self.W_2 = np.random.randn(d_ff, d_model) * 0.01
        self.b_2 = np.zeros(d_model)

    def layer_norm(self, x):
        mean = np.mean(x, axis=-1, keepdims=True)
        var = np.var(x, axis=-1, keepdims=True)
        return (x - mean) / np.sqrt(var + 1e-8)

    def multi_head_attention(self, Q, K, V, mask=None):
        batch_size = Q.shape[0]
        seq_len = Q.shape[1]
        Q_h = (Q @ self.W_Q).reshape(batch_size, seq_len, self.n_heads, self.head_dim)
        K_h = (K @ self.W_K).reshape(batch_size, seq_len, self.n_heads, self.head_dim)
        V_h = (V @ self.W_V).reshape(batch_size, seq_len, self.n_heads, self.head_dim)
        Q_h = Q_h.transpose(0, 2, 1, 3)
        K_h = K_h.transpose(0, 2, 1, 3)
        V_h = V_h.transpose(0, 2, 1, 3)
        scores = Q_h @ K_h.transpose(0, 1, 3, 2) / np.sqrt(self.head_dim)
        if mask is not None:
            scores = np.where(mask == 0, -1e9, scores)
        weights = np.exp(scores - np.max(scores, axis=-1, keepdims=True))
        weights = weights / np.sum(weights, axis=-1, keepdims=True)
        heads = weights @ V_h
        heads = heads.transpose(0, 2, 1, 3).reshape(batch_size, seq_len, self.d_model)
        return heads @ self.W_O

    def feed_forward(self, x):
        """FFN(x) = ReLU(x @ W_1 + b_1) @ W_2 + b_2"""
        return np.maximum(0, x @ self.W_1 + self.b_1) @ self.W_2 + self.b_2

    def __call__(self, x, mask=None):
        attn_out = self.multi_head_attention(x, x, x, mask)
        x = self.layer_norm(x + attn_out)
        ff_out = self.feed_forward(x)
        x = self.layer_norm(x + ff_out)
        return x

# Démonstration
encoder = TransformerEncoderLayer(256, 8, 1024)
x = np.random.randn(2, 20, 256)
out = encoder(x)
print(f"Encoder output: {out.shape}")

4. Transformer complet avec Keras

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

class TransformerBlock(layers.Layer):
    def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1):
        super().__init__()
        self.att = layers.MultiHeadAttention(
            num_heads=num_heads, key_dim=embed_dim // num_heads
        )
        self.ffn = keras.Sequential([layers.Dense(ff_dim, activation='relu'), layers.Dense(embed_dim)])
        self.layernorm1 = layers.LayerNormalization(epsilon=1e-6)
        self.layernorm2 = layers.LayerNormalization(epsilon=1e-6)
        self.dropout1 = layers.Dropout(rate)
        self.dropout2 = layers.Dropout(rate)

    def call(self, inputs, training=False):
        attn = self.att(inputs, inputs)
        attn = self.dropout1(attn, training=training)
        out1 = self.layernorm1(inputs + attn)
        ffn = self.ffn(out1)
        ffn = self.dropout2(ffn, training=training)
        return self.layernorm2(out1 + ffn)

# Modèle complet pour classification de texte
vocab_size, max_len, embed_dim = 10000, 200, 256
num_heads, ff_dim, n_layers = 8, 1024, 4

inputs = keras.layers.Input(shape=(max_len,))
x = layers.Embedding(vocab_size, embed_dim)(inputs)
positions = tf.range(start=0, limit=max_len, delta=1)
x += layers.Embedding(max_len, embed_dim)(positions)

for _ in range(n_layers):
    x = TransformerBlock(embed_dim, num_heads, ff_dim)(x)

x = layers.GlobalAveragePooling1D()(x)
outputs = layers.Dense(1, activation='sigmoid')(x)
model = keras.Model(inputs, outputs)
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])

Hyperparamètres

Hyperparamètre Paper originale Moderne (LLM) Description
$d_{model}$ 512 4096-8192 Dimension des embeddings
$n_{\text{heads}}$ 8 32-64 Nombre de têtes d’attention
$n_{\text{layers}}$ 6 (enc) + 6 (dec) 32-128 Nombre de couches empilées
$d_{ff}$ 2048 16384 Dimension du feed-forward
dropout 0.1 0.0-0.1 Régularisation
$L_{\text{max}}$ 512 4096-128000 Longueur maximale de la séquence

Avantages du Transformer

  1. Parallélisation massive : Contrairement aux RNN qui traitent les tokens un par un, le Transformer traite toute la séquence en parallèle. L’entraînement est 5 à 10 fois plus rapide sur GPU.
  2. Dépendances longue distance : La distance entre deux tokens n’affecte pas le nombre d’opérations pour les connecter.
  3. Universalité : La même architecture fonctionne pour la traduction, la classification, la génération, la vision (ViT), l’audio (Whisper) et même les données tabulaires.
  4. Scalabilité : Les performances continuent de s’améliorer avec plus de données, plus de paramètres et plus de compute. C’est cette propriété qui a permis l’échelle des LLMs modernes.

Limites du Transformer

  1. Complexité quadratique : L’attention coûte $O(n^2)$ en mémoire et calcul par rapport à la longueur de séquence. Pour des séquences de 100 000 tokens, cela devient prohibitif.
  2. Consommation énergétique : Entraîner un Transformer de 175 milliards de paramètres consomme autant d’énergie que des centaines de foyers pendant un an.
  3. Besoin colossal en données : Les Transformers atteignent leur plein potentiel uniquement avec des datasets de milliards de tokens.
  4. Interprétabilité : Les poids d’attention ne correspondent pas toujours aux dépendances linguistiques réelles du modèle.

4 cas d’usage concrets

1. Traduction automatique (Google Translate)

Le Transformer original a été conçu pour la traduction anglais-allemand et anglais-français. L’encodeur lit la phrase source, le décodeur génère la phrase cible token par token avec un masque causal. Les résultats ont surpassé tous les systèmes basés sur RNN et sont toujours utilisés dans les moteurs de traduction modernes.

2. Classification de documents juridiques

Un cabinet d’avocats utilise un Transformer encoder-only (comme BERT ou DeBERTa) pour classifier automatiquement des milliers de contrats par type (NDA, bail, contrat de travail) et extraire des clauses spécifiques. Le fine-tuning ne nécessite que quelques centaines d’exemples annotés.

3. Modèle de langage génératif (GPT)

Les modèles de type GPT utilisent un Transformer decoder-only entraîné sur des billions de tokens pour prédire le token suivant. La génération autoregressive produit du texte cohérent sur des milliers de mots, capable de rédiger, traduire, coder et raisonner.

4. Vision par Transformer (ViT)

Le Vision Transformer (ViT) applique l’architecture Transformer aux images en les découpant en patches (comme des tokens visuels). Un modèle ViT pré-entraîné sur ImageNet atteint des performances comparables aux CNNs de pointe, avec l’avantage d’une meilleure scalabilité à grande échelle.


Conclusion

Le Transformer est incontestablement l’architecture la plus influente du machine learning de cette décennie. En remplaçant la récurrence par l’attention, il a permis un bond en avant dans presque tous les domaines du traitement des séquences. De BERT à GPT, de la traduction automatique aux modèles de langage génératifs, cette architecture unique a démontré une capacité de généralisation sans précédent.

Même si la recherche explore désormais des alternatives (Mamba, RWKV, architectures à état linéaire) pour réduire la complexité quadratique, le Transformer reste le standard de l’industrie et continuera de dominer pour les années à venir.


Voir aussi


Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.