MLM — Modélisation de Langage Masqué : Guide Complet
Résumé
Le MLM (Masked Language Modeling) est une technique fondamentale d’auto-supervision en traitement du langage naturel (NLP). Popularisée par le modèle BERT de Google en 2018, cette approche révolutionne la manière dont les machines apprennent à comprendre le langage humain. Contrairement aux modèles de langage traditionnels qui lisent le texte dans une seule direction, le MLM permet au modèle d’accéder au contexte complet — à gauche et à droite — pour prédire des mots délibérément masqués.
Ce guide complet explore les fondements mathématiques, l’intuition pédagogique, l’implémentation pratique en PyTorch, les hyperparamètres essentiels, ainsi que les avantages, limites et cas d’usage concrets de la modélisation de langage masqué. Si vous cherchez à comprendre comment BERT et ses dérivés acquièrent leur compréhension profonde du langage, ce guide est fait pour vous.
Principe Mathématique
Formalisation du masquage
Étant donné une séquence d’entrée $x = [x_1, x_2, \ldots, x_n]$ composée de $n$ tokens, le principe du MLM consiste à masquer aléatoirement un sous-ensemble de ces tokens selon une stratégie précise. Le taux de masquage standard est de 15 %, mais cette opération ne se résume pas à un simple remplacement par un token [MASK].
Plus précisément, pour chaque token sélectionné parmi les 15 % :
- 80 % du temps : le token est remplacé par le token spécial
[MASK] - 10 % du temps : le token est remplacé par un token aléatoire issu du vocabulaire
- 10 % du temps : le token est conservé inchangé
Cette stratégie, appelée masquage à trois branches, est cruciale. Si l’on remplaçait systématiquement tous les tokens masqués par [MASK], le modèle ne verrait jamais ce token pendant la phase de fine-tuning (puisque [MASK] n’apparaît pas dans les données réelles). En conservant certains tokens intacts et en introduisant du bruit aléatoire, on crée une version plus robuste et plus généralisable de la tâche d’apprentissage.
Fonction de perte
Le modèle apprend en maximisant la vraisemblance des tokens masqués. La fonction de perte s’écrit formellement :
$$\mathcal{L} = -\mathbb{E}\left[\sum_{i \in \mathcal{M}} \log P(x_i \mid x_{\setminus \mathcal{M}})\right]$$
où :
- $\mathcal{M}$ désigne l’ensemble des indices des tokens masqués
- $x_{\setminus \mathcal{M}}$ représente la séquence observée (tous les tokens sauf les masqués)
- $P(x_i \mid x_{\setminus \mathcal{M}})$ est la probabilité que le modèle attribue au token original $x_i$ étant donné tous les autres tokens visibles
En pratique, cette perte est calculée via une entropie croisée (cross-entropy) entre la distribution prédite et les labels réels, moyennée sur l’ensemble des positions masquées dans le batch d’entraînement.
Bidirectionnalité : la révolution du MLM
C’est ici que réside la différence fondamentale avec les modèles de langage unidirectionnels comme GPT. Un modèle de langage classique prédit le token suivant $P(x_t \mid x_1, \ldots, x_{t-1})$ en ne voyant que le contexte gauche. Cette approche est naturelle pour la génération de texte, mais elle limite la compréhension sémantique : le modèle ne peut pas utiliser les indices qui apparaissent après le mot qu’il analyse.
Le MLM, en revanche, est intrinsèquement bidirectionnel. Lorsqu’il doit prédire un token masqué à la position $i$, il dispose de l’intégralité du contexte — les tokens à gauche et à droite. Cette capacité à observer le texte dans les deux directions simultanément permet au modèle d’apprendre des représentations bien plus riches et nuancées.
Exemple illustratif : Considérons la phrase « Le banquier est allé à la ___ pour retirer de l’argent. » Un modèle unidirectionnel classique ne peut prédire le mot manquant qu’en se basant sur ce qui précède. Le MLM, lui, verrait l’intégralité de la phrase avec un trou, et utiliserait aussi bien « banquier » (avant le trou) que « retirer de l’argent » (après le trou) pour déduire que le mot manquant est très probablement « banque ». C’est cette vision globale et contextuelle qui rend le MLM si puissant.
Intuition
Imaginez un étudiant qui prépare un examen de français en s’entraînant avec des textes à trous. On lui donne un paragraphe dont certains mots ont été effacés, et il doit deviner les mots manquants en utilisant le reste du texte. Plus il s’entraîne, plus il développe une compréhension fine de la grammaire, du vocabulaire et des relations sémantiques entre les mots.
C’est exactement ce que fait le MLM. Chaque phrase d’entraînement devient un exercice de « mots croisés » à grande échelle :
« Le [MASK] du chat est doux et chaud. »
Pour deviner le mot manquant, le modèle observe « du », « chat », « est », « doux », « chaud ». Il apprend progressivement que le mot manquant doit être un nom, probablement un attribut physique du chat, et il finit par proposer « poil ». En répétant cet exercice des milliards de fois sur des corpus variés, le modèle acquiert une représentation contextuelle extrêmement riche de chaque mot.
Cette approche rappelle étrangement la manière dont les êtres humains apprennent leur langue maternelle. Un enfant n’apprend pas les mots dans l’isolement : il les entend dans des phrases complètes, avec un contexte riche qui l’aide à deviner le sens même des mots qu’il ne connaît pas encore. Le MLM reproduit ce mécanisme à l’échelle computationnelle.
Un autre parallèle utile est celui des mots croisés. Pour deviner un mot dans une grille, on utilise tous les indices disponibles horizontalement et verticalement. De la même manière, le MLM utilise tout le contexte disponible — avant et après le trou — pour faire sa prédiction. Cette approche bidirectionnelle est ce qui distingue fondamentalement le MLM des modèles de langage causals comme GPT.
Implémentation Python avec PyTorch
Architecture du modèle
Nous allons implémenter un modèle de type BERT simplifié composé des éléments suivants :
- Embedding tokens : transformation des tokens en vecteurs denses
- Embeddings de position : injection de l’information positionnelle
- Embeddings de segment : distinction entre phrases (pour les tâches de paire)
- Couches Transformer (encodeur) : traitement bidirectionnel du contexte
- MLM Head : couche de prédiction des tokens masqués
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from torch.utils.data import Dataset, DataLoader
import random
# ============================================================
# 1. Masquage des tokens
# ============================================================
def mask_tokens(inputs, vocab_size, tokenizer_pad_id, mask_token_id,
mask_ratio=0.15, device='cpu'):
"""
Applique le masquage MLM stratégique :
- 80% des tokens sélectionnés -> remplacés par [MASK]
- 10% des tokens sélectionnés -> remplacés par un token aléatoire
- 10% des tokens sélectionnés -> gardés inchangés
Args:
inputs: tensor de tokens [batch_size, seq_len]
vocab_size: taille du vocabulaire
tokenizer_pad_id: ID du token de padding
mask_token_id: ID du token [MASK]
mask_ratio: proportion de tokens à masquer (défaut: 0.15)
Returns:
masked_inputs: tensor avec les tokens masqués
labels: tensor des labels (=-100 sauf aux positions masquées)
"""
labels = inputs.clone()
masked_inputs = inputs.clone()
# Crée un masque de probabilité pour chaque token
prob_matrix = torch.full(inputs.shape, mask_ratio, device=device)
# Ne pas masquer les tokens spéciaux [PAD], [CLS], [SEP]
prob_matrix[inputs == tokenizer_pad_id] = 0.0
# Sélection aléatoire des tokens à masquer
mask_selector = torch.bernoulli(prob_matrix).bool()
# Initialiser les labels
labels[~mask_selector] = -100 # -100 est ignoré par CrossEntropyLoss
# Appliquer la stratégie de masquage à trois branches
for i in range(inputs.size(0)):
for j in range(inputs.size(1)):
if mask_selector[i, j]:
rand = random.random()
if rand < 0.8:
# 80% : remplacer par [MASK]
masked_inputs[i, j] = mask_token_id
elif rand < 0.9:
# 10% : remplacer par un token aléatoire
random_token = random.randint(0, vocab_size - 1)
masked_inputs[i, j] = random_token
else:
# 10% : garder inchangé
masked_inputs[i, j] = inputs[i, j]
return masked_inputs, labels
# ============================================================
# 2. Embeddings positionnels
# ============================================================
class PositionalEmbedding(nn.Module):
"""Embeddings positionnelles sinusoidales, comme dans l'attention originale."""
def __init__(self, d_model, max_len=512):
super().__init__()
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, dtype=torch.float)
* (-math.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) # [1, max_len, d_model]
self.register_buffer('pe', pe)
def forward(self, x):
"""x: [batch_size, seq_len, d_model]"""
return x + self.pe[:, :x.size(1), :]
# ============================================================
# 3. Modèle BERT-like simplifié
# ============================================================
class SimpleBERTMLM(nn.Module):
"""
Modèle de type BERT simplifié pour le MLM.
Comprend embeddings, encodeur Transformer et tête de prédiction.
"""
def __init__(self, vocab_size, hidden_size=768, num_layers=6,
num_heads=12, ff_size=3072, max_seq_len=512,
dropout=0.1):
super().__init__()
self.vocab_size = vocab_size
self.hidden_size = hidden_size
# Embeddings
self.token_embedding = nn.Embedding(vocab_size, hidden_size)
self.position_embedding = PositionalEmbedding(hidden_size, max_seq_len)
self.segment_embedding = nn.Embedding(2, hidden_size)
self.layer_norm = nn.LayerNorm(hidden_size)
self.dropout = nn.Dropout(dropout)
# Encodeur Transformer (empilement de couches)
encoder_layer = nn.TransformerEncoderLayer(
d_model=hidden_size,
nhead=num_heads,
dim_feedforward=ff_size,
dropout=dropout,
batch_first=True,
activation='gelu'
)
self.transformer_encoder = nn.TransformerEncoder(
encoder_layer, num_layers=num_layers
)
# Tête MLM : projection vers l'espace du vocabulaire
self.mlm_head = nn.Sequential(
nn.Linear(hidden_size, hidden_size),
nn.GELU(),
nn.LayerNorm(hidden_size),
nn.Linear(hidden_size, vocab_size)
)
# Initialisation des poids
self._init_weights()
def _init_weights(self):
"""Initialisation inspirée de BERT."""
for module in self.modules():
if isinstance(module, nn.Linear):
module.weight.data.normal_(mean=0.0, std=0.02)
if module.bias is not None:
module.bias.data.zero_()
elif isinstance(module, nn.Embedding):
module.weight.data.normal_(mean=0.0, std=0.02)
elif isinstance(module, nn.LayerNorm):
module.bias.data.zero_()
module.weight.data.fill_(1.0)
def forward(self, input_ids, segment_ids=None, attention_mask=None):
"""
Propagation avant du modèle MLM.
Args:
input_ids: [batch_size, seq_len] — indices des tokens
segment_ids: [batch_size, seq_len] — identifiants de segment (optionnel)
attention_mask: [batch_size, seq_len] — masque d'attention (1=visible, 0=masqué)
Returns:
predictions: [batch_size, seq_len, vocab_size] — scores pour chaque token
"""
batch_size, seq_len = input_ids.size()
# 1. Embeddings de tokens
x = self.token_embedding(input_ids)
# 2. Embeddings positionnelles
x = self.position_embedding(x)
# 3. Embeddings de segment
if segment_ids is not None:
x = x + self.segment_embedding(segment_ids)
# 4. Normalisation et dropout
x = self.dropout(self.layer_norm(x))
# 5. Passage à travers l'encodeur Transformer
if attention_mask is not None:
key_padding_mask = (attention_mask == 0)
else:
key_padding_mask = None
encoded = self.transformer_encoder(
x, mask=None, src_key_padding_mask=key_padding_mask
)
# 6. Tête de prédiction MLM
predictions = self.mlm_head(encoded)
return predictions
# ============================================================
# 4. Dataset personnalisé
# ============================================================
class MLMDataset(Dataset):
"""Dataset pour l'entraînement MLM à partir de phrases tokenisées."""
def __init__(self, texts, tokenizer, max_len=128):
self.texts = texts
self.tokenizer = tokenizer
self.max_len = max_len
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
text = self.texts[idx]
# Tokenisation avec tokens spéciaux [CLS] et [SEP]
tokens = self.tokenizer.tokenize(text)
tokens = ['[CLS]'] + tokens[:self.max_len - 2] + ['[SEP]']
# Conversion en indices
input_ids = self.tokenizer.convert_tokens_to_ids(tokens)
# Padding
padding_len = self.max_len - len(input_ids)
input_ids = input_ids + [self.tokenizer.pad_token_id] * padding_len
# Masque d'attention
attention_mask = [1] * (len(input_ids) - padding_len) + [0] * padding_len
return {
'input_ids': torch.tensor(input_ids, dtype=torch.long),
'attention_mask': torch.tensor(attention_mask, dtype=torch.long)
}
# ============================================================
# 5. Boucle d'entraînement
# ============================================================
def train_mlm_model(model, dataloader, tokenizer, device='cuda',
num_epochs=10, learning_rate=5e-5, gradient_clip=1.0):
"""
Boucle d'entraînement complète pour le modèle MLM.
"""
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate, eps=1e-6)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=num_epochs * len(dataloader)
)
criterion = nn.CrossEntropyLoss(ignore_index=-100)
model.train()
model.to(device)
for epoch in range(num_epochs):
total_loss = 0.0
total_tokens_predicted = 0
total_correct = 0
for batch_idx, batch in enumerate(dataloader):
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
# Appliquer le masquage MLM
masked_inputs, labels = mask_tokens(
inputs=input_ids,
vocab_size=model.vocab_size,
tokenizer_pad_id=tokenizer.pad_token_id,
mask_token_id=tokenizer.mask_token_id,
mask_ratio=0.15,
device=device
)
# Propagation avant
predictions = model(masked_inputs, attention_mask=attention_mask)
# Calcul de la perte
loss = criterion(
predictions.view(-1, model.vocab_size),
labels.view(-1)
)
# Rétropropagation
optimizer.zero_grad()
loss.backward()
# Gradient clipping
torch.nn.utils.clip_grad_norm_(model.parameters(), gradient_clip)
optimizer.step()
scheduler.step()
total_loss += loss.item()
# Calcul de la précision
pred_labels = predictions.view(-1, model.vocab_size).argmax(dim=-1)
true_labels = labels.view(-1)
mask_positions = true_labels != -100
total_tokens_predicted += mask_positions.sum().item()
total_correct += (
pred_labels[mask_positions] == true_labels[mask_positions]
).sum().item()
if (batch_idx + 1) % 50 == 0:
avg_loss = total_loss / (batch_idx + 1)
accuracy = total_correct / max(total_tokens_predicted, 1)
print(f"Époque {epoch+1}/{num_epochs} | "
f"Batch {batch_idx+1}/{len(dataloader)} | "
f"Perte : {avg_loss:.4f} | "
f"Précision : {accuracy:.4f}")
epoch_loss = total_loss / len(dataloader)
epoch_accuracy = total_correct / max(total_tokens_predicted, 1)
print(f"\n=== Fin de l'époque {epoch+1}/{num_epochs} ===")
print(f"Perte moyenne : {epoch_loss:.4f}")
print(f"Précision MLM : {epoch_accuracy:.4f}\n")
return model
# ============================================================
# 6. Exemple d'utilisation
# ============================================================
if __name__ == "__main__":
VOCAB_SIZE = 30000
HIDDEN_SIZE = 256
NUM_LAYERS = 4
NUM_HEADS = 8
FF_SIZE = 512
MAX_SEQ_LEN = 128
BATCH_SIZE = 32
NUM_EPOCHS = 5
LEARNING_RATE = 3e-4
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print("Appareil :", device)
print(f"Vocabulaire : {VOCAB_SIZE}")
print(f"Couches : {NUM_LAYERS}")
print(f"Longueur max : {MAX_SEQ_LEN}")
model = SimpleBERTMLM(
vocab_size=VOCAB_SIZE,
hidden_size=HIDDEN_SIZE,
num_layers=NUM_LAYERS,
num_heads=NUM_HEADS,
ff_size=FF_SIZE,
max_seq_len=MAX_SEQ_LEN,
dropout=0.1
)
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Paramètres totaux : {total_params:,}")
print(f"Paramètres entraînés : {trainable_params:,}")
Hyperparamètres Clés
Le choix des hyperparamètres détermine profondément la qualité du modèle MLM. Voici les plus importants :
| Hyperparamètre | Valeur typique (base) | Valeur typique (large) | Description |
|---|---|---|---|
| mask_ratio | 0,15 (15 %) | 0,15 (15 %) | Proportion de tokens masqués. Augmenter au-delà rend la tâche trop facile (trop de contexte), diminuer réduit le signal d’apprentissage. |
| vocab_size | 30 000 | 30 000 | Taille du vocabulaire (tokens BPE/WordPiece). Un vocabulaire plus grand capture plus de nuances mais augmente la taille de la tête de prédiction. |
| hidden_size | 768 | 1 024 | Dimension des vecteurs d’embedding et des représentations internes. Détermine la capacité du modèle à encoder des informations complexes. |
| num_layers | 12 | 24 | Nombre de couches Transformer empilées. Plus de couches = plus de capacité d’abstraction, mais aussi plus de calcul et risque de surapprentissage. |
| max_seq_len | 512 | 512 | Longueur maximale des séquences en tokens. L’attention quadratique rend les longues séquences coûteuses en mémoire (O(n²)). |
Autres hyperparamètres importants :
- num_heads (12 pour base, 16 pour large) : nombre de têtes d’attention multi-têtes. Chaque tête apprend un type différent de relation contextuelle.
- ff_size : dimension de la couche feed-forward interne (généralement 4 × hidden_size).
- dropout (0,1) : probabilité de dropout appliquée aux embeddings et aux couches feed-forward. Empêche le surapprentissage.
- learning_rate (2×10⁻⁵ à 5×10⁻⁵ pour le fine-tuning, 1×10⁻⁴ à 3×10⁻⁴ pour le pré-entraînement) : taux d’apprentissage. Le warm-up (augmentation progressive pendant les premières étapes) est essentiel pour la stabilité.
- batch_size : généralement entre 256 et 8 192 séquences pour le pré-entraînement. Des batchs plus grands nécessitent un learning rate plus élevé.
- gradient_clip (1,0) : seuil de clipping du gradient. Empêche les explosions de gradient, fréquentes dans les grands modèles Transformer.
Avantages et Limites
Avantages
- Représentations contextuelles riches : Contrairement à Word2Vec ou GloVe qui produisent des embeddings statiques (un mot = un vecteur fixe), le MLM génère des représentations dynamiques qui varient selon le contexte. Le mot « banque » aura une représentation différente dans « la banque du fleuve » et « la banque centrale ».
- Compréhension bidirectionnelle : Le modèle accède au contexte complet de chaque phrase, lui permettant de capturer des relations syntaxiques et sémantiques complexes qui seraient inaccessibles à un modèle unidirectionnel.
- Pré-entraînement efficace : Le MLM exploite des corpus textuels massifs et non annotés (Wikipédia, livres, articles web) pour apprendre des représentations générales transférables à pratiquement n’importe quelle tâche aval.
- Transfert learning puissant : Une fois pré-entraîné, un modèle MLM peut être affiné (fine-tuned) avec peu de données étiquetées pour des tâches spécifiques comme la classification, l’extraction d’entités nommées, ou la réponse aux questions.
- Robustesse au bruit : La stratégie de masquage à trois branches (80/10/10) rend le modèle résistant aux perturbations et améliore sa capacité de généralisation.
Limites
-
Inadaptation à la génération : Pendant le pré-entraînement, le modèle voit toujours
[MASK]comme entrée, mais ce token n’apparaît pas dans les données réelles. Cette discordance pré-entraînement/fine-tuning rend le MLM médiocre pour la génération de texte, d’où l’utilisation de modèles causals (GPT) pour cette tâche. - Indépendance conditionnelle : Le MLM prédit chaque token masqué indépendamment des autres tokens masqués. Il ne modélise pas les interactions entre plusieurs mots manquants simultanément. Cette limitation a conduit au développement du MLM itératif et des approches comme ELECTRA.
- Coût computationnel : L’entraînement d’un modèle MLM de grande taille nécessite des dizaines ou centaines de GPU pendant des semaines, avec des consommations énergétiques conséquentes.
- Sensibilité au masquage : Le choix du ratio de masquage (15 %) et de la stratégie (80/10/10) est un compromis empirique. Des données très structurées (code, formules mathématiques) peuvent nécessiter des stratégies de masquage adaptées.
- Biais dans les données : Le modèle apprend les biais présents dans le corpus d’entraînement. Si les données contiennent des stéréotypes, le modèle les reproduira — un défi majeur en NLP contemporain.
4 Cas d’Usage Concrets
Cas d’usage №1 — Classification de textes
Le MLM constitue une base exceptionnelle pour la classification de documents. Un modèle comme BERT, pré-entraîné via MLM sur Wikipédia et BookCorpus, peut être affiné avec seulement quelques milliers d’exemples pour classer des critiques cinématographiques (sentiment positif/négatif), des articles juridiques par catégorie, ou des tickets de support client par priorité. La couche [CLS] du modèle, qui agrège l’information de toute la séquence, sert de représentation globale du document pour la classification.
Cas d’usage №2 — Réponse aux questions (Question Answering)
Les systèmes de réponse aux questions s’appuient massivement sur le MLM. Étant donné un contexte (un paragraphe) et une question, un modèle BERT affiné est capable d’identifier la span de texte (début et fin) qui contient la réponse. Cette capacité découle directement de la compréhension bidirectionnelle acquise pendant le pré-entraînement MLM, qui permet au modèle de relier les éléments de la question aux éléments correspondants dans le contexte, quel que soit leur ordre d’apparition.
Cas d’usage №3 — Étiquetage d’entités nommées (NER)
L’extraction d’entités nommées — identifier les personnes, organisations, lieux, dates dans un texte — bénéficie particulièrement du MLM. La compréhension contextuelle bidirectionnelle permet de distinguer « Paris » (ville) dans « il vit à Paris » de « Paris » (mythologie) dans « Paris enleva Hélène ». Des variantes comme CamemBERT (français), BioBERT (biomédical), ou LegalBERT (juridique) sont pré-entraînées via MLM sur des corpus spécialisés pour maximiser les performances dans leurs domaines respectifs.
Cas d’usage №4 — Recherche sémantique et moteurs de recherche
Les moteurs de recherche modernes utilisent des modèles pré-entraînés via MLM pour améliorer la compréhension des requêtes. Plutôt que de chercher une correspondance exacte de mots-clés, le modèle MLM encode la requête et les documents dans un espace sémantique commun, permettant de trouver des documents pertinents même lorsqu’ils n’utilisent pas les mêmes termes que la requête. C’est le principe derrière Google Search, qui a intégré BERT en 2019 pour mieux comprendre les requêtes naturelles des utilisateurs.
Voir Aussi
- Découvrez ‘Factor Shuffle’ en Python : Guide Complet pour Réorganiser vos Collections Efficacement
- Découverte des Voisins Lexicographiques en Python : Guide Complet pour Développeurs

