Module 8 — Encodeur-décodeur pour la traduction
Jusqu'ici, la sortie du modèle avait une forme prévisible : 24 valeurs pour prévoir 24 heures, une étiquette par pas pour l'étiquetage. La traduction casse cette régularité : « je mange » (deux tokens) peut donner « I am eating » (trois tokens), et la correspondance entre positions n'est même pas monotone. L'architecture encodeur-décodeur répond à ce cas où les deux séquences n'ont ni la même longueur ni la même structure.
Le fil rouge de ce module : petit corpus FR-EN
On quitte la consommation électrique pour un jeu public de paires de phrases
courtes français-anglais (par exemple tatoeba réduit à 20 000 paires de
moins de 10 tokens). Les paires ressemblent à :
je mange une pomme . --> i am eating an apple .
il fait froid . --> it is cold .
On tokenise par mots (ou sous-mots), on ajoute deux jetons spéciaux de contrôle :
<debut>: marque le début de la séquence cible ;<fin>: marque la fin, le décodeur s'arrêtera dessus.
Ce détail est fondamental pour l'inférence : sans <fin>, le décodeur ne
sait jamais quand arrêter de produire des mots.
L'architecture en deux blocs
Un encodeur-décodeur se compose de deux RNN chaînés.
L'encodeur lit la séquence source et résume tout ce qu'il a vu dans son état final. Il n'y a pas de sortie par pas ; seul l'état compte.
Le décodeur commence avec l'état de l'encodeur comme état initial. Il
produit la séquence cible pas à pas : à chaque pas, il consomme le jeton
précédent et son état, et prédit le jeton suivant. Il s'arrête quand il
prédit <fin>.
Le vecteur d'état passé de l'encodeur au décodeur s'appelle le vecteur de contexte. Il condense tout le sens de la phrase source dans un espace de taille fixe. C'est le goulot d'étranglement du modèle, et la motivation directe du cours 12.
teacher forcing à l'entraînement
Pendant l'entraînement, on connaît la cible. Deux stratégies existent pour alimenter le décodeur.
Décodage libre : à chaque pas, on prend la prédiction du décodeur comme entrée du pas suivant. Le modèle apprend à corriger ses propres erreurs, mais il commence par ne rien produire de sensé et l'entraînement est extrêmement lent.
teacher forcing : à chaque pas, on prend le vrai jeton précédent
comme entrée du pas suivant, indépendamment de ce que le modèle a prédit. Le
signal d'apprentissage est immédiat et cohérent, l'entraînement converge
beaucoup plus vite. C'est la méthode par défaut.
Concrètement, on décale la séquence cible :
cible d'entree decodeur : <debut> i am eating an apple .
cible predite : i am eating an apple . <fin>
Cette astuce permet d'entraîner le décodeur en une seule passe sur toute la séquence, sans boucle Python.
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
VOCAB_SRC = 8000
VOCAB_TGT = 8000
DIM_PLONGEMENT = 128
H = 256
# Encodeur
entrees_src = keras.Input(shape=(None,), name="src")
plong_src = layers.Embedding(VOCAB_SRC, DIM_PLONGEMENT, mask_zero=True)(entrees_src)
_, h_enc, c_enc = layers.LSTM(H, return_state=True)(plong_src)
# Decodeur (teacher forcing : reçoit la cible decalee)
entrees_tgt = keras.Input(shape=(None,), name="tgt_in")
plong_tgt = layers.Embedding(VOCAB_TGT, DIM_PLONGEMENT, mask_zero=True)(entrees_tgt)
sorties_dec, _, _ = layers.LSTM(H, return_sequences=True, return_state=True)(
plong_tgt, initial_state=[h_enc, c_enc]
)
logits = layers.Dense(VOCAB_TGT)(sorties_dec)
modele = keras.Model([entrees_src, entrees_tgt], logits)
modele.compile(
optimizer="adam",
loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
)
L'entrée tgt_in contient la cible décalée de un cran (<debut> ajouté au
début, <fin> retiré à la fin). La cible pour la perte est la vraie
séquence (<fin> conservé, <debut> retiré).
Le décalage entre entraînement et inférence
À l'inférence, la vraie cible n'existe pas : on ne peut pas alimenter le décodeur avec elle. On boucle en Python, en réinjectant à chaque pas le jeton qu'on vient de prédire.
def traduire(modele, tokenizer_src, tokenizer_tgt, phrase, max_pas=20):
id_debut = tokenizer_tgt.word_index["<debut>"]
id_fin = tokenizer_tgt.word_index["<fin>"]
# Encodage : un seul appel pour toute la source
ids_src = tokenizer_src.texts_to_sequences([phrase])[0]
ids_src = tf.constant([ids_src])
h, c = encodeur.predict(ids_src)
# Decodage pas a pas
sortie = []
entree = tf.constant([[id_debut]])
for _ in range(max_pas):
logits, h, c = decodeur.predict([entree, h, c])
prochain_id = int(tf.argmax(logits[0, -1]))
if prochain_id == id_fin:
break
sortie.append(prochain_id)
entree = tf.constant([[prochain_id]])
return tokenizer_tgt.sequences_to_texts([sortie])[0]
Ce décalage entre entraînement (toute la séquence, teacher forcing) et
inférence (pas à pas, réinjection) crée un biais dit d'exposition : le
modèle n'a jamais vu ses propres erreurs. Un token mal prédit au pas 3 fait
diverger tout ce qui suit. Le problème est réel mais souvent acceptable ; il
motive des méthodes comme le scheduled sampling, plus lourdes.
Décodage glouton et ses limites
Le code ci-dessus utilise argmax : à chaque pas, on prend le token le plus
probable. C'est le décodage glouton. Il est simple et rapide, mais il
peut être trompé par un choix localement optimal qui pénalise toute la suite.
Deux alternatives valent d'être connues :
Le décodage par faisceau (beam search) maintient les meilleures
séquences partielles à chaque pas et n'élague qu'à la fin. Il donne des
sorties nettement meilleures pour un coût fois plus grand ; ou
est le standard en traduction.
L'échantillonnage stochastique tire un jeton selon la distribution des probabilités, avec une température qui la lisse ou la durcit. Utile pour la génération créative, pas pour la traduction.
Le goulot du vecteur de contexte
L'encodeur-décodeur souffre d'une limitation intrinsèque : toute la séquence source doit tenir dans un seul vecteur de dimension . Pour une phrase de 5 mots, ça passe. Pour une phrase de 50, l'information la plus ancienne est écrasée par la suivante, et la qualité s'effondre. La courbe « qualité vs longueur » chute nettement à partir de 20-30 tokens.
C'est ce goulot qui a motivé le mécanisme d'attention (Bahdanau, 2014), que le cours 12 traite en détail. L'idée : au lieu d'un unique vecteur de contexte, le décodeur consulte à chaque pas tous les états cachés de l'encodeur, en les pondérant. Cette pondération est apprise. La qualité cesse de se dégrader avec la longueur, et surtout l'attention devient la brique fondatrice des transformeurs.
Le rôle du masquage
Un lot de traduction contient des phrases de longueurs différentes. On les
remplit avec le jeton 0 pour former un tenseur régulier. Sans précaution,
ces zéros seraient traités comme des mots réels. mask_zero=True dans
Embedding fabrique un masque qui se propage à travers les couches
récurrentes : les pas remplis ne contribuent ni à la perte ni au calcul de
l'état final. Le module 9 revient dessus en détail.
<fin> en entraînement fait boucler l'inférenceSi votre corpus n'a pas de jeton <fin> dans la cible, le décodeur ne peut
pas apprendre à s'arrêter. À l'inférence, il produira des tokens
indéfiniment jusqu'à max_pas. Le bug typique : traductions qui se répètent
indéfiniment ou se terminent au milieu d'un mot. Ajoutez toujours <debut>
au début et <fin> à la fin de chaque cible avant tokenisation, et vérifiez
avec un print sur trois exemples avant d'entraîner.
La perte d'entraînement d'un modèle de traduction ne dit pas grand-chose de la qualité perçue. Le score standard est BLEU, qui compare les n-grammes prédits aux n-grammes de référence. Un LSTM encodeur-décodeur atteint typiquement 20 à 25 BLEU sur un petit corpus ; les transformeurs modernes dépassent 40 sur les mêmes données. Ce n'est pas une compétition, c'est un ordre de grandeur à connaître pour ne pas se réjouir d'une perte qui baisse.
En résumé
- Un encodeur-décodeur enchaîne deux RNN : le premier résume la source dans un vecteur de contexte, le second produit la cible pas à pas à partir de cet état.
- Le
teacher forcingalimente le décodeur avec la vraie cible décalée pendant l'entraînement ; l'inférence, elle, réinjecte les prédictions et crée un biais d'exposition. - Les jetons
<debut>et<fin>sont indispensables : le premier amorce le décodage, le second signale la fin d'une séquence de longueur variable. - Le vecteur de contexte est un goulot : tout ce qu'il faut se souvenir passe par nombres, ce qui limite les phrases longues et motive directement le mécanisme d'attention du cours 12.
Module suivant : les questions pratiques de remplissage, de masquage et de lots que la traduction et la prévision partagent, mais qui traînent souvent au fond des scripts.