Module 8 — Encodeur-décodeur : T5 et la traduction
Encodeur et décodeur sont pris séparément dans les modules 6 et 7. L'architecture originale de 2017 les combine dans un même modèle, relié par une attention croisée. C'est le schéma des systèmes de traduction, de résumé, et de la famille T5. Ce module explique ce qui se passe entre les deux moitiés, replace notre fil rouge dans cette famille, et donne la règle de choix entre les trois architectures.
Deux piles reliées par l'attention croisée
Dans un modèle encodeur-décodeur, la séquence source (par exemple « 3 mars 2026 ») passe par la pile d'encodeurs, ce qui produit une mémoire de forme : un vecteur par jeton source, contextualisé par toute la source. Cette mémoire n'est calculée qu'une fois par entrée.
En parallèle, la séquence cible (par exemple « 2026-03-03 ») passe par la pile de décodeurs. Chaque couche de décodeur contient trois sous-blocs, dans cet ordre :
- une auto-attention causale sur la cible partiellement générée ;
- une attention croisée dont les viennent de la cible et dont les viennent de la mémoire encodée ;
- un feed-forward appliqué position par position.
Chaque sous-bloc conserve son résidu et sa normalisation, comme au module 5. C'est le deuxième bloc, l'attention croisée, qui donne au décodeur un accès direct à toute la source à chaque étape de génération. C'est l'héritage direct de Bahdanau, généralisé et parallélisé.
import torch
import torch.nn as nn
class CoucheDecodeur(nn.Module):
def __init__(self, d, h, d_ff, dropout=0.1):
super().__init__()
self.norme_1 = nn.LayerNorm(d)
self.auto_attention = AttentionMultiTetes(d, h, dropout) # module 3
self.norme_2 = nn.LayerNorm(d)
self.attention_croisee = AttentionMultiTetes(d, h, dropout)
self.norme_3 = nn.LayerNorm(d)
self.feed_forward = nn.Sequential(
nn.Linear(d, d_ff), nn.GELU(), nn.Linear(d_ff, d),
)
self.dropout = nn.Dropout(dropout)
def forward(self, cible, memoire, masque_causal=None, masque_source=None):
y, _ = self.auto_attention(
self.norme_1(cible), self.norme_1(cible), self.norme_1(cible),
masque=masque_causal,
)
cible = cible + self.dropout(y)
y, _ = self.attention_croisee(
self.norme_2(cible), memoire, memoire, masque=masque_source,
)
cible = cible + self.dropout(y)
y = self.feed_forward(self.norme_3(cible))
return cible + self.dropout(y)
Le point remarquable est le paramètre memoire : c'est le tenseur produit par l'encodeur et gardé constant pour toute la génération. Un décodeur pur, module 7, ne reçoit pas ce paramètre et n'a pas de sous-bloc d'attention croisée.
T5 formule tout en texte à texte
En 2020, Raffel et ses coauteurs publient T5 avec un principe fort : toute tâche de NLP est reformulée comme une génération de texte. Une classification devient « classifier la phrase X en une des étiquettes Y » où la sortie attendue est le mot de l'étiquette. Une question-réponse devient « répondre à Q avec le passage P ». Une traduction reste une traduction, mais tout partage la même architecture et la même perte.
Cette uniformité permet de préentraîner un seul modèle sur un mélange de tâches et de le réutiliser tel quel pour des tâches nouvelles, en changeant seulement le préfixe fourni en entrée. L'objectif de préentraînement lui-même est le débruitage de portions masquées (span corruption) : des morceaux du texte sont remplacés par des jetons sentinelles et le décodeur doit générer le texte manquant.
Sur notre fil rouge, la formulation T5 s'écrit :
entree : traduire date : 3 mars 2026
sortie : 2026-03-03
Le préfixe « traduire date : » informe le modèle du type de transformation demandée. On peut ainsi entraîner un même modèle sur plusieurs formats de date, plusieurs langues, plusieurs directions (ISO vers français et français vers ISO), en changeant seulement ce préfixe.
Quand préférer encodeur, décodeur ou encodeur-décodeur
Le choix se fait sur trois critères pratiques : la nature de l'entrée, la nature de la sortie, et la contrainte de latence.
| Tâche | Famille recommandée | Modèle typique |
|---|---|---|
| Classification de texte | Encodeur | BERT, CamemBERT |
| Extraction d'entités | Encodeur | BERT, DistilBERT |
| Recherche par similarité | Encodeur | Sentence-BERT |
| Génération libre à partir d'un prompt | Décodeur | GPT, LLaMA, Mistral |
| Complétion de code | Décodeur | Code Llama, StarCoder |
| Traduction | Encodeur-décodeur | T5, mT5, MarianMT |
| Résumé abstrait | Encodeur-décodeur | BART, T5, Pegasus |
| Question-réponse générative | Encodeur-décodeur ou décodeur | T5, GPT selon le cas |
Il faut noter une évolution récente : les très grands décodeurs (GPT-4, LLaMA-3) sont si capables qu'ils absorbent aujourd'hui des tâches historiquement réservées aux encodeurs-décodeurs, comme le résumé ou la traduction. Le tableau reste juste pour les modèles de taille modeste, où la spécialisation par architecture reste rentable.
Si l'entrée est un texte fixe et la sortie une étiquette ou une portion du texte, prendre un encodeur. Si l'entrée est un prompt libre et la sortie un texte libre, prendre un décodeur. Si les deux sont des textes de natures différentes (français à ISO, article à résumé), un encodeur-décodeur reste souvent le choix le plus économique en paramètres.
Le préentraînement conjoint change les compromis
Un encodeur-décodeur porte deux piles à entraîner, ce qui semble doubler le coût par rapport à un décodeur seul. Ce n'est pas exactement le cas : T5-Base pèse environ 220 millions de paramètres, alors que BERT-Base en pèse 110 et GPT-2 Small 124. L'encodeur y est plus petit que dans BERT parce que la représentation partagée n'a pas besoin d'être aussi dense — le décodeur en extrait ce qu'il lui faut à chaque étape.
En revanche, la génération, elle, est plus lente qu'avec un décodeur pur, parce qu'il faut à chaque pas passer par l'attention croisée (deux fois l'attention par couche). Le cache clé-valeur (module 7) s'applique aussi ici, mais sur les de la mémoire encodée, qui sont calculés une seule fois.
Le T5 d'origine, comme BERT, est massivement entraîné sur de l'anglais. Pour du français, il faut passer à mT5 (multilingue) ou FrenchT5, sous peine de résultats médiocres. C'est un piège classique de projet : partir d'un modèle générique et découvrir que son préentraînement ne couvre pas la langue cible.
Sur le fil rouge
Notre Transformer maison de fin de cours est un encodeur-décodeur : deux couches d'encodeur, deux couches de décodeur, une attention croisée entre les deux. Il est entraîné sur la traduction de dates comme s'il s'agissait d'une T5 miniature. Au module 10, on visualisera les cartes d'attention croisée et l'on y verra clairement que le décodeur pointe vers le chiffre du jour de la source quand il produit les deux derniers chiffres de la date ISO. C'est cette lisibilité qui a motivé le choix de la tâche.
En résumé
- Un encodeur-décodeur produit d'abord une mémoire de la source par la pile d'encodeurs, puis génère la cible avec une pile de décodeurs qui consulte cette mémoire par attention croisée.
- Chaque couche de décodeur contient trois sous-blocs : auto-attention causale, attention croisée, feed-forward, chacun avec son résidu et sa norme.
- T5 reformule toute tâche en génération de texte avec un préfixe d'instruction, ce qui permet de partager un même modèle sur des tâches très différentes.
- La règle rapide : encodeur pour classer, décodeur pour générer librement, encodeur-décodeur pour transformer un texte source en un texte cible de nature différente.
Module suivant : le coût quadratique de l'attention en mémoire et en calcul, et les mécanismes qui le contournent pour traiter des contextes longs.