Contrastive Predictive Coding
Résumé
Le Contrastive Predictive Coding (CPC) est une méthode révolutionnaire d’apprentissage de représentations self-supervisé. Introduit par Aaron van den Oord et ses collaborateurs chez DeepMind en 2018, le CPC repose sur une idée simple mais élégante : au lieu de prédire les données brutes du futur, le modèle apprend à prédire des représentations latentes dans un espace de plus haute dimension. Cette approche permet de capturer l’essence structurelle des données sans jamais avoir besoin d’étiquettes manuelles. Le moteur d’apprentissage est la fonction de perte InfoNCE (Information Noise-Contrastive Estimation), qui maximise une borne inférieure de l’information mutuelle entre le contexte présent et les observations futures. Le résultat est un système capable d’extraire des caractéristiques riches et transférables à des tâches variées : classification audio, reconnaissance d’images, compréhension du langage naturel, et bien d’autres applications.
Principe Mathématique du Contrastive Predictive Coding
Le CPC repose sur trois composants fondamentaux qui s’articulent dans un pipeline élégant de transformation progressive des données.
1. Encodeur — Production des Représentations Locales
Chaque observation brute $x_t$ (un échantillon audio, un patch d’image, un mot dans une phrase) est transformée en une représentation latente compacte $z_t$ par un encodeur non linéaire :
$$z_t = f_{\text{enc}}(x_t)$$
L’encodeur $f_{\text{enc}}$ est généralement un réseau de neurones convolutif (CNN) qui réduit la dimensionalité des données tout en préservant l’information sémantique pertinente. Pour un signal audio par exemple, le spectrogramme brut (de dimension élevée) est compressé en un vecteur $z_t$ de taille fixe, disons 256 ou 512 dimensions. Ce vecteur capture les caractéristiques locales essentielles — la fréquence dominante, le timbre, l’énergie — dans une forme beaucoup plus compacte.
2. Réseau d’Agrégation Contextuelle — Construction du Contexte
Les représentations locales $z_1, z_2, …, z_t$ sont ensuite agrégées séquentiellement par un réseau autorégressif $f_{\text{autoreg}}$ (généralement un RNN de type GRU ou LSTM, ou bien un Transformeur) pour produire un vecteur de contexte $c_t$ :
$$c_t = f_{\text{autoreg}}(z_{1:t})$$
Ce vecteur $c_t$ résume toute l’histoire des représentations jusqu’au pas de temps $t$. Il contient l’information accumulée du passé récent et lointain, comprimée dans un unique espace vectoriel. C’est le rôle du réseau d’agrégation contextuelle : agir comme une mémoire dynamique qui intègre progressivement l’information au fur et à mesure qu’elle arrive. Contrairement à un simple empilement de caractéristiques, le réseau autorégressif apprend quelles informations du passé sont pertinentes pour la prédiction future, et lesquelles peuvent être oubliées.
3. Prédiction dans l’Espace Latent
Le cœur du CPC réside dans la prédiction. Pour chaque pas futur $k \geq 1$, le modèle apprend une transformation linéaire qui prédit la représentation future $z_{t+k}$ à partir du contexte courant $c_t$ :
$$\phi_k(c_t) = W_k \cdot c_t$$
Ici, $W_k$ est une matrice de poids spécifique au pas de prédiction $k$. L’idée est que chaque horizon temporel nécessite une projection différente : prédire 1 pas en avant n’est pas la même tâche que prédire 10 pas en avant. La similarité score entre la prédiction et la représentation future réelle est calculée par produit scalaire :
$$f_k(x_{t+k}, c_t) = \exp(z_{t+k}^T \cdot W_k \cdot c_t)$$
4. Perte InfoNCE — Maximisation de l’Information Mutuelle
La perte centrale du CPC est la fonction InfoNCE (Information Noise-Contrastive Estimation). Pour un contexte $c_t$ donné, le modèle doit distinguer le véritable échantillon futur $x_{t+k}$ d’un ensemble de distracteurs (échantillons négatifs) ${x_j}$ tirés d’autres séquences :
$$L_{\text{CPC}} = -\sum_k \log \frac{\exp(z_{t+k}^T \cdot W_k \cdot c_t)}{\sum_{x_j} \exp(z_j^T \cdot W_k \cdot c_t)}$$
Cette formule est remarquablement efficace. Le numérateur mesure la similarité entre la prédiction correcte ($z_{t+k}$ prédit depuis $c_t$) et le dénominateur normalise par rapport à tous les candidats possibles. En minimisant cette perte, le modèle apprend à assigner un score élevé à la paire correcte (contexte + vrai futur) et un score bas aux paires incorrectes. La perte InfoNCE maximise en réalité une borne inférieure de l’information mutuelle entre $c_t$ et $x_{t+k}$, ce qui justifie théoriquement l’approche : plus la perte est faible, plus le contexte $c_t$ contient d’information sur le futur.
Intuition — Comprendre le CPC à travers une Analogie
Le CPC, c’est comme un journaliste sportif qui essaie de deviner la suite d’un événement en cours. Imaginez qu’un match de football vienne de commencer. Le journaliste ne va pas prédire le détail exact de chaque action — « Ronaldo va marquer à la 37e minute », « Modric va faire une passe dans l’axe à la 52e ». Ce serait beaucoup trop difficile, presque impossible.
À la place, il prédit l’idée générale, le contexte global : « Il y aura des buts », « les deux équipes vont attaquer », « le score sera serré », « il y aura des corners et des cartons jaunes ». Cette prédiction à haut niveau capture l’essence du match sans se perdre dans les détails imprévisibles.
C’est exactement ce que fait le CPC ! Au lieu de prédire le pixel exact qui va suivre dans une image ou l’échantillon audio brut (tâche extrêmement difficile, car le bruit et la variabilité sont énormes), le modèle prédit la représentation latente future — c’est-à-dire les caractéristiques sémantiques abstraites qui décrivent ce qui va arriver. Il ne prédit pas « le 440 Hz à l’amplitude 0,73 », il prédit « c’est un accord majeur, probablement la tonique, dans un contexte musical jazz ».
Cette abstraction est la clé du succès. En travaillant dans l’espace latent au lieu de l’espace des données brutes, le CPC se concentre sur ce qui est structurellement important et ignore le bruit superficiel. Les représentations ainsi apprises sont beaucoup plus robustes et transférables que des représentations entraînées par reconstruction pure.
Une autre façon de voir les choses : c’est comme si vous écoutiez une conversation téléphonique avec une qualité médiocre. Vous ne percevez pas chaque syllabe parfaitement, mais votre cerveau reconstitue le sens global de la conversation. Le CPC fonctionne de manière similaire — il extrait le signal sémantique du bruit, le sens de la forme brute.
Implémentation Python Complète avec PyTorch
Voici une implémentation complète du CPC appliqué à des données audio sous forme de spectrogrammes. Le code est structuré en modules clairs et commenté en détail.
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
import numpy as np
class CPCEncoder(nn.Module):
"""Encodeur CNN: transforme les spectrogrammes bruts
en représentations latentes compactes z_t."""
def __init__(self, latent_dim=256):
super().__init__()
self.latent_dim = latent_dim
# Chaque couche CNN réduit la dimension temporelle
# par un facteur 2 et extrait des caractéristiques
self.conv_layers = nn.Sequential(
nn.Conv1d(1, 64, kernel_size=10, stride=5, padding=2),
nn.BatchNorm1d(64),
nn.ReLU(inplace=True),
nn.Conv1d(64, 128, kernel_size=8, stride=4, padding=2),
nn.BatchNorm1d(128),
nn.ReLU(inplace=True),
nn.Conv1d(128, 256, kernel_size=4, stride=2, padding=1),
nn.BatchNorm1d(256),
nn.ReLU(inplace=True),
nn.Conv1d(256, self.latent_dim, kernel_size=4, stride=2, padding=1),
nn.BatchNorm1d(self.latent_dim),
nn.ReLU(inplace=True),
)
def forward(self, x):
# x: (batch, 1, temps) — spectrogramme mono-canal
# z: (batch, latent_dim, temps_reduit)
z = self.conv_layers(x)
return z # Chaque pas de temps est une représentation locale z_t
class CPCAutoregressor(nn.Module):
"""Réseau autorégressif (GRU) qui agrège
l'historique des représentations en un contexte c_t."""
def __init__(self, latent_dim=256, num_layers=3):
super().__init__()
self.gru = nn.GRU(
input_size=latent_dim,
hidden_size=latent_dim,
num_layers=num_layers,
batch_first=True
)
def forward(self, z):
# z: (batch, temps, latent_dim)
# On transpose car l'encodeur produit (batch, dim, temps)
z_seq = z.transpose(1, 2)
# c: (batch, temps, latent_dim) — contexte à chaque pas
c, _ = self.gru(z_seq)
return c
class CPCModel(nn.Module):
"""Modèle CPC complet : encodeur + autorégresseur + projections."""
def __init__(self, latent_dim=256, n_future_steps=12,
autoregressor_layers=3):
super().__init__()
self.latent_dim = latent_dim
self.n_future_steps = n_future_steps
self.encoder = CPCEncoder(latent_dim=latent_dim)
self.autoregressor = CPCAutoregressor(
latent_dim=latent_dim,
num_layers=autoregressor_layers
)
# Matrices W_k pour chaque pas de prédiction futur
self.W_k = nn.ModuleList([
nn.Linear(latent_dim, latent_dim)
for _ in range(n_future_steps)
])
def forward(self, x):
"""Renvoie le contexte c_t et les représentations z."""
# Encodage
z = self.encoder(x) # (batch, dim, temps')
z_seq = z.transpose(1, 2) # (batch, temps', dim)
# Contexte autorégressif
c = self.autoregressor(z) # (batch, temps', dim)
return c, z_seq
def compute_logits(self, c_t, z_future, k, negatives=None):
"""Calcule les scores pour InfoNCE."""
# Prédiction: W_k * c_t
prediction = self.W_k[k](c_t) # (batch, 1, dim)
# Score positif: similarité avec le vrai futur
pos_score = torch.bmm(z_future.unsqueeze(1),
prediction.transpose(1, 2)).squeeze(-1)
if negatives is not None:
neg_scores = torch.bmm(negatives,
prediction.transpose(1, 2))
logits = torch.cat([pos_score, neg_scores.squeeze(-1)], dim=1)
else:
logits = pos_score
return logits
def info_nce_loss(self, c, z, n_negatives=8):
"""Calcule la perte InfoNCE sur tous les pas futurs."""
batch_size, seq_len, dim = c.shape
total_loss = 0.0
n_valid_k = 0
for k in range(1, min(self.n_future_steps, seq_len)):
# Contexte au temps t
c_t = c[:, :-k, :] # (batch, T-k, dim)
# Représentation vraie au temps t+k
z_future = z[:, k:, :] # (batch, T-k, dim)
# Échantillons négatifs: on mélange les z dans le batch
shuffled_z = z.flatten(0, 1)
neg_indices = torch.randperm(shuffled_z.size(0))
neg_z = shuffled_z[neg_indices]
neg_z = neg_z[:batch_size * (seq_len - k) * n_negatives]
neg_z = neg_z.view(batch_size, seq_len - k, n_negatives, dim)
# Calcul des scores
prediction = self.W_k[k - 1](c_t) # (batch, T-k, dim)
# Score positif
pos_score = (z_future * prediction).sum(dim=-1, keepdim=True)
# Scores négatifs
neg_score = torch.einsum('btd,bknd->btkn', z_future, neg_z)
neg_score = neg_score.sum(dim=-1) # (batch, T-k)
logits = torch.cat([pos_score.squeeze(-1), neg_score], dim=1)
# Labels : la classe 0 est toujours le positif
labels = torch.zeros(batch_size * (seq_len - k),
dtype=torch.long, device=logits.device)
logits_flat = logits.view(-1, logits.size(-1))
loss = F.cross_entropy(logits_flat, labels)
total_loss += loss
n_valid_k += 1
return total_loss / max(n_valid_k, 1)
# ============================================================
# Entrainement
# ============================================================
def train_cpc(model, dataloader, optimizer, n_epochs=50, device='cuda'):
"""Boucle d'entraînement du modèle CPC."""
model = model.to(device)
model.train()
for epoch in range(n_epochs):
epoch_loss = 0.0
n_batches = 0
for batch_x, _ in dataloader: # _ = labels non utilisés en CPC
batch_x = batch_x.to(device)
# Forward pass
c, z = model(batch_x)
loss = model.info_nce_loss(c, z, n_negatives=8)
# Backward pass
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
epoch_loss += loss.item()
n_batches += 1
avg_loss = epoch_loss / max(n_batches, 1)
print(f"Epoch {epoch+1}/{n_epochs} | Perte InfoNCE: {avg_loss:.4f}")
return model
# ============================================================
# Évaluation en Aval — Classification
# ============================================================
class DownstreamClassifier(nn.Module):
"""Classificateur linéaire entraîné sur les
caractéristiques CPC gelées (évaluation en aval)."""
def __init__(self, latent_dim, n_classes):
super().__init__()
self.classifier = nn.Sequential(
nn.Linear(latent_dim, 128),
nn.ReLU(inplace=True),
nn.Dropout(0.3),
nn.Linear(128, 64),
nn.ReLU(inplace=True),
nn.Linear(64, n_classes),
)
def forward(self, features):
# features: (batch, latent_dim) — Moyenne sur la dimension temporelle
return self.classifier(features)
def evaluate_downstream(cpc_model, train_loader, test_loader,
n_classes=10, device='cuda'):
"""Gèle le CPC, extrait les features, entraîné un classificateur."""
cpc_model.eval()
cpc_model.to(device)
# Extraction des caractéristiques
def extract_features(loader):
all_features, all_labels = [], []
with torch.no_grad():
for x, y in loader:
x = x.to(device)
c, z = cpc_model(x)
# Moyenne globale sur le temps
features = c.mean(dim=1) # (batch, latent_dim)
all_features.append(features.cpu())
all_labels.append(y)
return torch.cat(all_features), torch.cat(all_labels)
train_feats, train_labels = extract_features(train_loader)
test_feats, test_labels = extract_features(test_loader)
# Entraînement du classificateur
classifier = DownstreamClassifier(cpc_model.latent_dim, n_classes)
optimizer = torch.optim.Adam(classifier.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
train_dataset = TensorDataset(train_feats, train_labels)
train_dl = DataLoader(train_dataset, batch_size=128, shuffle=True)
for epoch in range(30):
classifier.train()
for feats, labels in train_dl:
optimizer.zero_grad()
logits = classifier(feats)
loss = criterion(logits, labels)
loss.backward()
optimizer.step()
# Évaluation
classifier.eval()
with torch.no_grad():
test_logits = classifier(test_feats.to(device))
preds = test_logits.argmax(dim=1)
accuracy = (preds.cpu() == test_labels).float().mean().item()
print(f"Précision en aval: {accuracy:.4f}")
return accuracy
# ============================================================
# Exemple d'utilisation
# ============================================================
if __name__ == '__main__':
torch.manual_seed(42)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# Données synthétiques: spectrogrammes simulés
# (batch=32, canal=1, temps=256)
dummy_audio = torch.randn(32, 1, 256)
dummy_labels = torch.randint(0, 10, (32,))
dataset = TensorDataset(dummy_audio, dummy_labels)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
# Modèle CPC
model = CPCModel(latent_dim=256, n_future_steps=12,
autoregressor_layers=3)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
print("Début de l'entranement CPC...")
model = train_cpc(model, dataloader, optimizer,
n_epochs=10, device=device)
print("Entranement terminé avec succès!")
Hyperparamètres Clés
Le choix des hyperparamètres influence considérablement la qualité des représentations apprises par le Contrastive Predictive Coding. Voici les paramètres les plus critiques et leur rôle :
latent_dim (256–512) — La dimension de l’espace latent détermine la capacité d’information du modèle. Trop faible (64), et le modèle perd des nuances importantes. Trop élevé (1024+), et le modèle risque de mémoriser des détails insignifiants au détriment de l’abstraction sémantique. Pour la plupart des applications audio et visuelles, 256 à 512 constitue un bon compromis entre expressivité et généralisation.
n_future_steps (6–12) — Le nombre de pas futurs prédits détermine l’horizon temporel du modèle. Un petit nombre (3-5) capture des dépendances locales et immédiates. Un grand nombre (12-24) force le modèle à comprendre la structure à long terme. En pratique, 12 pas fonctionnent bien pour l’audio, tandis que 6 suffisent pour les images où la séquentialité est moins prononcée.
n_negatives (4–128) — Le nombre d’échantillons négatifs dans la perte InfoNCE influence directement la qualité de l’apprentissage. Peu de négatifs (2-4) rendent la tâche trop facile — le modèle n’apprend pas à discriminer finement. Beaucoup de négatifs (64-256) rendent la tâche plus discriminative mais augmentent le coût computationnel. La littérature recommande généralement entre 8 et 128 négatifs selon les ressources disponibles.
autoregressor_layers (2–4) — Le nombre de couches du GRU (ou LSTM) contrôle la profondeur de la mémoire contextuelle. Deux couches sont suffisantes pour des séquences courtes. Trois à quatre couches sont nécessaires pour capturer des dépendances complexes et à long terme, par exemple dans la parole continue ou les séquences vidéo.
Taux d’apprentissage — Un taux initial de 1×10⁻³ avec un décroissance (schedule) ou un warmup progressif est recommandé. Le clipping de gradient (à 1,0) est essentiel pour stabiliser l’entraînement, car la perte InfoNCE peut produire des gradients importants au début.
Avantages et Limites du CPC
Avantages
- Aucune étiquette nécessaire — Le CPC est entièrement self-supervisé. Il apprend des représentations riches à partir de données brutes non annotées, ce qui est crucial dans les domaines où l’annotation manuelle est coûteuse ou impossible.
- Représentations transférables — Les caractéristiques apprises par le CPC se transfèrent remarquablement bien à des tâches en aval : classification, détection, segmentation. C’est l’un des modèles foundation model avant l’heure.
- Efficacité computationnelle relative — En travaillant dans l’espace latent plutôt que dans l’espace des données brutes, le CPC réduit considérablement la complexité de la tâche de prédiction par rapport à un décodeur reconstructionniste classique (comme un autoencodeur variationnel).
- Base théorique solide — La connexion entre la perte InfoNCE et la maximisation de l’information mutuelle donne au CPC des garanties théoriques que peu d’autres méthodes possèdent. On sait mathématiquement ce que le modèle optimise.
- Universalité — L’architecture CPC s’adapte à presque tous les types de données séquentielles : audio, vidéo, texte, séries temporelles médicales ou financières, signaux capteurs.
Limites
- Coût des négatifs — La perte InfoNCE nécessite un échantillonnage de distracteurs. Avec un grand nombre de négatifs, la consommation mémoire et le temps de calcul augmentent significativement. Les méthodes modernes comme SimCLR ont partiellement adressé ce problème en utilisant des augmentations de données comme source de négatifs.
- Effondrement du latent — Dans certaines configurations, le modèle peut apprendre un encodeur trivial qui produit toujours le même vecteur $z_t$, rendant la perte InfoNCE artificiellement faible. Des techniques de régularisation et d’augmentation de données sont nécessaires pour éviter ce phénomène.
-
Sensibilité aux hyperparamètres — Le choix de
latent_dim,n_future_stepsetn_negativesinflue fortement sur les performances. Un mauvais réglage peut produire des représentations de qualité médiocre, et la recherche systématique (grid search) est coûteuse. - Pas de génération — Le CPC apprend des représentations discriminatives mais ne modélise pas explicitement la distribution des données. Contrairement aux VAEs ou aux GANs, il ne peut pas générer de nouvelles données à partir de bruit aléatoire.
- Dépendance à l’agrégation séquentielle — Le réseau autorégressif (GRU) limite le parallélisme pendant l’entraînement. Des variantes utilisant des Transformeurs ont été proposées pour résoudre ce problème, mais elles augmentent la complexité computationnelle.
4 Cas d’Usage Concrets du Contrastive Predictive Coding
1. Reconnaissance de la Parole (Speech Recognition)
Le CPC a révolutionné l’apprentissage de représentations audio. En s’entraînant sur des milliers d’heures de parole non étiquetée (LibriSpeech, CommonVoice), le modèle apprend des caractéristiques acoustiques riches qui capturent la phonétique, la prosodie et même certains aspects sémantiques. Ces représentations sont ensuite utilisées pour initialiser des modèles de reconnaissance de parole, réduisant le besoin de données annotées de manière drastique. Des études ont montré que le pré-entraînement CPC améliore le taux d’erreur de mots (WER) de 10 à 15 % sur des benchmarks comme LibriSpeech, surtout lorsque les données annotées sont limitées.
2. Classification d’Images par Patch Séquentiel
En traitant une image comme une séquence de patches (balayage de gauche à droite, comme un texte), le CPC peut apprendre des représentations visuelles puissantes. L’encodeur CNN produit des caractéristiques locales pour chaque patch, le GRU agrège le contexte spatial, et la prédiction contrastive force le modèle à comprendre la structure globale de l’image. Ces représentations se transfèrent efficacement à la classification d’images (ImageNet), à la détection d’objets, et à la segmentation sémantique. C’est l’ancêtre conceptuel de méthodes comme MAE (Masked Autoencoder) et DINO.
3. Analyse de Séries Temporelles Médicales
En médecine, les données temporelles abondent : électrocardiogrammes (ECG), électroencéphalogrammes (EEG), signaux de monitoring en réanimation. Le CPC peut s’entraîner sur ces signaux bruts pour apprendre des représentations qui capturent les motifs physiologiques normaux et anormaux. Un classificateur linéaire entraîné sur ces caractéristiques peut ensuite détecter des arythmies, des crises d’épilepsie, ou des signes avant-coureurs de sepsis — avec des performances supérieures aux méthodes traditionnelles, même avec peu de données annotées.
4. Compréhension du Langage Naturel
Bien que les Transformeurs aient dominé le NLP, le CPC offre une perspective intéressante pour l’apprentissage de représentations textuelles. En traitant une phrase ou un paragraphe comme une séquence, le modèle apprend à prédire les représentations des mots futurs à partir du contexte. Ces caractéristiques capturent la syntaxe et la sémantique locale, et peuvent être utilisées comme entrée pour des tâches en aval : analyse de sentiments, classification de textes, détection de langues. Le CPC est particulièrement utile pour les langues à faibles ressources où les modèles de type BERT ne sont pas disponibles.
Voir Aussi
- Implémentez l’algorithme de hachage SHA-1 en Python : Guide étape par étape
- Tracer un Triangle avec des Arcs de Cercle en Python : Guide Complet pour les Développeurs

