Contrastive Predictive Coding (CPC) — Le Guide Complet
Résumé
Le Contrastive Predictive Coding (CPC) est une méthode d’apprentissage auto-supervisé introduite par Aaron van den Oord et ses collaborateurs chez DeepMind en 2018. L’idée fondamentale est révolutionnaire dans sa simplicité : au lieu de chercher à reconstruire fidèlement les données d’entrée — comme le font les autoencodeurs classiques — le CPC apprend à prédire des échantillons futurs dans un espace latent. Cette approche permet au modèle de capturer la structure temporelle profonde des données sans aucune étiquette explicite.
Le CPC s’applique à tout type de données séquentielles : la parole, la musique, le texte, les séries temporelles, et même les données visuelles traitées comme des séquences. Grâce à sa fonction de perte contrastive de type InfoNCE, le modèle apprend à distinguer le signal véritable du bruit, ce qui produit des représentations d’une richesse remarquable. Ces représentations peuvent ensuite être exploitées pour des tâches en aval telles que la reconnaissance vocale, la classification d’images, ou la détection d’anomalies dans des flux de capteurs industriels.
Ce guide explore en détail le principe mathématique du CPC, son intuition profonde, son implémentation complète en PyTorch, ainsi que ses applications pratiques. Que vous soyez chercheur en apprentissage profond ou ingénieur souhaitant comprendre les mécanismes à la base des représentations auto-supervisées, vous trouverez ici tout ce qu’il vous faut pour maîtriser le Contrastive Predictive Coding.
Principe Mathématique du CPC
Le fonctionnement du Contrastive Predictive Coding repose sur trois composants essentiels qui s’enchaînent dans un pipeline séquentiel rigoureux.
Étape 1 : L’encodage dans l’espace latent
Le CPC encode les données brutes x_t observées au pas de temps t en une représentation latente compacte z_t :
z_t = g_enc(x_t)
Cette fonction d’encodage g_enc est un réseau de neurones non linéaire — généralement un réseau convolutif (CNN) pour les signaux audio, ou un réseau convolutif résiduel pour les images. L’objectif de cet encodeur est de projeter les données de haute dimension dans un espace latent de dimension réduite, tout en préservant l’information pertinente pour la prédiction future. Contrairement aux méthodes de reconstruction qui cherchent à minimiser l’erreur quadratique moyenne, l’encodeur CPC est entraîné indirectement à travers la perte contrastive globale.
Étape 2 : L’autorégresseur contextuel
Une fois les représentations latentes z_t obtenues, un modèle autorégressif — typiquement un GRU (Gated Recurrent Unit) ou un Transformer — calcule progressivement un vecteur de contexte c_t :
c_t = g_ar(z_{≤t})
Ce vecteur c_t est le résumé compressé de tout le passé disponible jusqu’au moment t. Le GRU maintient un état caché h_t qui évolue à chaque pas :
h_t = GRU(h_{t-1}, z_t)
c_t = W_proj · h_t
La matrice W_proj effectue une projection linéaire pour adapter la dimension de l’état caché du GRU à celle de l’espace latent. Le contexte c_t doit contenir suffisamment d’information pour anticiper les représentations futures.
Étape 3 : La prédiction contrastive via InfoNCE
La prédiction de l’échantillon futur au décalage k est évaluée par un produit scalaire pondéré :
f_k(z_{t+k}, c_t) = exp(z_{t+k}^T · W_k · c_t)
Ici, W_k est une matrice de poids spécifique au pas futur k. L’exponentielle garantit que la fonction de score est strictement positive, ce qui est essentiel pour la formulation de la perte.
La loss InfoNCE pour chaque pas futur k se définit comme suit :
L_k = -E[ log( f_k(z_{t+k}^{positif}, c_t) / Σ_{j=1}^{N} f_k(z_{t+k}^{négatif j}, c_t) ) ]
Dans cette expression :
– z_{t+k}^{positif} est la véritable représentation future au décalage k, c’est-à-dire l’échantillon positif.
– Les z_{t+k}^{négatif j} sont N – 1 échantillons négatifs tirés d’autres séquences du même lot, plus l’échantillon positif lui-même, ce qui donne un total de N candidats.
– Le dénominateur somme les scores de tous les candidats, créant ainsi une distribution de probabilité normalisée.
– La perte cherche à maximiser le score de l’échantillon positif par rapport aux négatifs, ce qui revient à maximiser une borne inférieure de l’information mutuelle entre c_t et z_{t+k}.
La perte totale du modèle est la somme des pertes sur tous les pas futurs considérés :
L = Σ_{k=1}^{K} L_k
où K est le nombre total de pas de prédiction.
Maximisation de l’information mutuelle
Le but profond du CPC est de maximiser l’information mutuelle I(c_t ; z_{t+k}) entre le contexte courant et les échantillons futurs. En maximisant cette quantité, on force le modèle à capturer la structure temporelle inhérente aux données. Les représentations latentes apprises deviennent alors riches en sémantique : pour la parole, elles encodent les phonèmes et la prosodie ; pour la musique, les notes et le rythme ; pour les séries temporelles, les tendances et les cycles saisonniers.
La relation formelle s’exprime ainsi :
I(c_t ; z_{t+k}) ≥ log(N) - L_k
où N est le nombre total de candidats (positif plus négatifs). Cette borne montre que minimiser la perte InfoNCE revient directement à maximiser une borne inférieure de l’information mutuelle — d’où le nom InfoNCE.
Intuition Profonde
Imaginez le CPC comme un jeu de devinettes sophistiqué. On présente au modèle le début d’une phrase ou les premières mesures d’un morceau de musique, puis on lui montre plusieurs suites potentielles — une seule est correcte, les autres sont des leurres tirés au hasard d’autres séquences. Le modèle doit reconnaître la bonne continuation parmi ces candidates.
Pour réussir ce jeu de manière consistante, le modèle ne peut pas se contenter de mémoriser des motifs superficiels. Il doit véritablement comprendre la structure sous-jacente du signal : la grammaire et la syntaxe d’une langue, la rythmique et l’harmonie d’une mélodie, les corrélations à long terme d’une série temporelle. Et tout cela, sans jamais avoir reçu la moindre étiquette explicite — pas de transcription, pas de partition, pas d’annotation de quelque type que ce soit. Voici toute la puissance de l’apprentissage auto-supervisé.
La différence fondamentale avec les méthodes de reconstruction classique (comme les VAE ou les autoencodeurs débruiteurs) est que le CPC ne cherche pas à reconstruire pixel par pixel ou échantillon par échantillon. Reconstruire un signal est une tâche souvent trop facile : un modèle peut apprendre des raccourcis superficiels qui minimisent l’erreur de reconstruction tout en ignorant la véritable structure du signal. En revanche, prédire quelle séquence future est correcte parmi plusieurs distracteurs oblige le modèle à extraire des caractéristiques hautement informatives et discriminantes.
Considérons un exemple concret : en reconnaissance vocale, le CPC apprend que certaines suites de phonèmes sont plausibles et d’autres improbables. Il découvre que le son “tion” en français est fréquemment suivi d’un espace ou d’une voyelle, mais rarement d’une consonne occlusive. Cette connaissance phonotactique émerge spontanément de la tâche de prédiction future — aucun linguiste n’a eu besoin d’annoter ces régularités.
Implémentation Python Complète
Voici une implémentation complète du Contrastive Predictive Coding en PyTorch, structurée de manière modulaire pour être facilement adaptable à différents types de données.
1. L’Encodeur
L’encodeur transforme les données brutes en représentations latentes. Pour l’audio, on utilise généralement un CNN à une dimension. Pour les images, un CNN à deux dimensions avec des couches résiduelles.
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
class CPCEncoder(nn.Module):
"""Encodeur CPC utilisant des convolutions 1D pour les signaux séquentiels."""
def __init__(self, input_dim: int = 1, latent_dim: int = 256,
num_layers: int = 5, kernel_size: int = 10, stride: int = 5):
super().__init__()
self.latent_dim = latent_dim
# Empilement de couches convolutives
layers = []
in_channels = input_dim
for i in range(num_layers):
out_channels = latent_dim if i == num_layers - 1 else latent_dim // 2
layers.append(nn.Conv1d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=kernel_size // 2
))
layers.append(nn.BatchNorm1d(out_channels))
layers.append(nn.ReLU(inplace=True))
in_channels = out_channels
self.encoder = nn.Sequential(*layers)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# x: (batch, seq_len, input_dim) -> (batch, input_dim, seq_len)
x = x.transpose(1, 2)
z = self.encoder(x)
# Retour à (batch, latent_dim, seq_len_reduite)
return z.transpose(1, 2)
2. L’Autorégresseur (Context Network)
Le réseau contextual encode la dynamique temporelle à travers un GRU.
class CPCContextNetwork(nn.Module):
"""Réseau autorégressif (GRU) qui calcule le vecteur contexte c_t."""
def __init__(self, latent_dim: int = 256, hidden_dim: int = 256,
num_layers: int = 2):
super().__init__()
self.gru = nn.GRU(
input_size=latent_dim,
hidden_size=hidden_dim,
num_layers=num_layers,
batch_first=True,
dropout=0.1 if num_layers > 1 else 0.0
)
self.projection = nn.Linear(hidden_dim, latent_dim)
def forward(self, z: torch.Tensor) -> torch.Tensor:
# z: (batch, seq_len, latent_dim)
out, _ = self.gru(z)
# out: (batch, seq_len, hidden_dim)
c = self.projection(out)
# c: (batch, seq_len, latent_dim)
return c
3. La Perte InfoNCE
C’est le cœur du CPC : la fonction de perte contrastive qui distingue les échantillons positifs des négatifs.
def compute_infonce_loss(context: torch.Tensor,
latent_future: torch.Tensor,
weight_matrices: nn.ParameterList,
num_negatives: int = 127):
"""
Calcule la perte InfoNCE du CPC.
Arguments:
context: (batch, seq_len, latent_dim) - vecteurs de contexte c_t
latent_future: (batch, seq_len + max_k, latent_dim) - representations futures
weight_matrices: liste de matrices W_k pour chaque pas de prediction
num_negatives: nombre total de candidats (1 positif + N-1 negatifs)
Retourne:
total_loss: somme des pertes sur tous les pas futurs
individual_losses: liste des pertes par pas k
"""
batch_size, seq_len, latent_dim = context.shape
total_loss = 0.0
individual_losses = []
for k, w_k in enumerate(weight_matrices, start=1):
# Selectionner les contextes et les futurs correspondants
c = context[:, :seq_len - k, :] # (batch, seq_len-k, latent_dim)
z_pos = latent_future[:, k:seq_len, :] # (batch, seq_len-k, latent_dim)
# Score positif: exp(z_pos^T . W_k . c)
wz = torch.matmul(z_pos, w_k) # (batch, seq_len-k, latent_dim)
pos_score = torch.sum(wz * c, dim=-1) # (batch, seq_len-k)
pos_score = torch.exp(pos_score) # (batch, seq_len-k)
# Generer les negatifs en melangeant aleatoirement dans le lot
n_neg = num_negatives - 1
neg_indices = torch.randint(0, batch_size, (batch_size * (seq_len - k) * n_neg,),
device=context.device)
z_neg = z_pos.view(-1, latent_dim)[neg_indices]
z_neg = z_neg.view(batch_size, seq_len - k, n_neg, latent_dim)
wz_neg = torch.matmul(z_neg, w_k)
neg_score = torch.sum(wz_neg * c.unsqueeze(2), dim=-1)
neg_score = torch.exp(neg_score)
# Combiner positif et negatifs
all_scores = torch.cat([pos_score.unsqueeze(-1), neg_score], dim=-1)
# Normalisation log-softmax
log_softmax = F.log_softmax(all_scores, dim=-1)
loss_k = -log_softmax[:, :, 0].mean()
total_loss = total_loss + loss_k
individual_losses.append(loss_k.item())
return total_loss, individual_losses
4. Le Modèle CPC Complet
Voici le modèle complet qui assemble tous les composants :
class CPCModel(nn.Module):
"""Modele CPC complet : encodeur + autoregresseur + tetes de prediction."""
def __init__(self, input_dim: int = 1, latent_dim: int = 256,
context_hidden: int = 256, num_layers_gru: int = 2,
num_prediction_steps: int = 12, num_negatives: int = 127):
super().__init__()
self.encoder = CPCEncoder(input_dim, latent_dim)
self.context_net = CPCContextNetwork(latent_dim, context_hidden,
num_layers_gru)
self.num_prediction_steps = num_prediction_steps
self.num_negatives = num_negatives
# Une matrice W_k par pas de prediction futur
self.prediction_weights = nn.ParameterList([
nn.Parameter(torch.randn(latent_dim, latent_dim) * 0.01)
for _ in range(num_prediction_steps)
])
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
x: (batch, seq_len, input_dim)
Retourne la perte totale CPC.
"""
z = self.encoder(x)
c = self.context_net(z)
loss, per_step = compute_infonce_loss(
c, z, self.prediction_weights, self.num_negatives
)
return loss, per_step
5. Boucle d’Entraînement
def train_cpc(model: CPCModel, dataloader, num_epochs: int = 50,
lr: float = 1e-3, device: str = "cuda"):
"""Entraînement du modele CPC avec la perte InfoNCE."""
model = model.to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_epochs)
for epoch in range(num_epochs):
model.train()
epoch_loss = 0.0
num_batches = 0
for batch in dataloader:
x = batch.to(device) # (batch, seq_len, input_dim)
loss, per_step = model(x)
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
epoch_loss += loss.item()
num_batches += 1
scheduler.step()
avg_loss = epoch_loss / max(num_batches, 1)
print(f"[Époque {epoch+1}/{num_epochs}] Perte CPC moyenne : {avg_loss:.4f}")
return model
6. Exemple d’Utilisation sur un Signal Audio Simulé
if __name__ == "__main__":
# Parametres
BATCH_SIZE = 64
SEQ_LEN = 512
INPUT_DIM = 1 # Signal mono
LATENT_DIM = 128
NUM_EPOCHS = 30
# Donnees simulees : bruit colore avec structure temporelle
num_samples = 5000
raw_data = np.zeros((num_samples, SEQ_LEN, INPUT_DIM), dtype=np.float32)
for i in range(num_samples):
signal = np.cumsum(np.random.randn(SEQ_LEN)) # Marche aleatoire
signal = (signal - signal.mean()) / (signal.std() + 1e-8)
raw_data[i, :, 0] = signal
dataset = torch.tensor(raw_data)
dataloader = torch.utils.data.DataLoader(
dataset, batch_size=BATCH_SIZE, shuffle=True
)
# Creation du modele
model = CPCModel(
input_dim=INPUT_DIM,
latent_dim=LATENT_DIM,
context_hidden=LATENT_DIM,
num_layers_gru=2,
num_prediction_steps=12,
num_negatives=64
)
# Entraînement
trained_model = train_cpc(model, dataloader, num_epochs=NUM_EPOCHS)
print("\nEntraînement CPC termine avec succès !")
Hyperparamètres Clés
Le réglage des hyperparamètres est crucial pour les performances du CPC. Voici les quatre paramètres les plus importants et leurs valeurs recommandées :
| Hyperparamètre | Description | Valeur typique | Impact |
|---|---|---|---|
| num_negatives | Nombre total de candidats dans la perte InfoNCE (1 positif + N-1 negatifs) | 128–256 | Plus de negatifs ameliorent la borne d’information mutuelle mais augmentent le cout memoire |
| context_size | Dimension de l’etat cache du GRU (dimension du contexte c_t) | 256–512 | Un contexte plus grand capture des dependances plus longues |
| prediction_steps | Nombre de pas futurs K a predire simultanement | 8–24 | Plus de pas capturent la dynamique a long terme |
| encoder_type | Architecture de l’encodeur g_enc (CNN, WaveNet, ResNet) | CNN pour l’audio, ResNet pour la vision | Determine la granularite des representations latentes |
Recommandations supplémentaires
- Taux d’apprentissage : commencez à 1×10⁻³ avec un ordonnanceur Cosine Annealing. Le CPC est relativement robuste au choix du taux d’apprentissage grâce à la nature autoclassifiante de la perte InfoNCE.
- Dimension latente : 128 est un bon point de départ pour l’audio, 256 pour les images. Des dimensions plus élevées n’apportent pas toujours de béné́fice net.
- Taille du lot : plus le lot est grand, mieux c’est pour la génération de négociatifs. Visez au moins 64 échantillons par lot.
- Décroissance du poids (weight decay) : 1×10⁻⁴ à 1×10⁻⁵ pour prévenir le surapprentissage.
Choix de l’autorégresseur
Le GRU est le choix par défaut du CPC original et reste excellent pour la plupart des applications. Cependant, un Transformer avec attention causale peut capturer des dépendances encore plus longues au prix d’un coût computationnel plus élevé. Pour des séquences très longues (plusieurs milliers de pas), le Transformer est souvent supérieur, surtout si on y ajoute un mécanisme d’attention linéaire ou une fenêtre glissante.
Avantages et Limites du CPC
Avantages
- Aucune étiquette nécessaire : le CPC est entièrement auto-supervisé. Il tire sa supervision de la structure temporelle intrinsèque des données elles-mêmes. C’est un avantage considérable quand les annotations sont rares, coûteuses ou tout simplement inexistantes.
- Représentations riches et transférables : les représentations apprises par CPC se sont révélées extrêmement efficaces pour le transfert vers des tâches en aval. L’article original a démontré que des représentations pré-entraînées avec CPC surpassaient des modèles entraînés de manière supervisée sur des tâches de reconnaissance automatique de la parole.
- Modularité́ architecturale : l’encodeur peut être adapté́ à presque n’importe quel type de données — CNN pour l’audio, ResNet pour les images, embeddings pour le texte. L’autorégresseur et la perte InfoNCE restent inchangés.
- Efficacité computationnelle : comparé aux méthodes génératives comme les VAE ou les GAN, le CPC est relativement léger à entraîner car il ne nécessite pas de décodeur complexe ni de phase de génération d’échantillons.
- Base théorique solide : la connexion formelle avec la maximisation de l’information mutuelle donne au CPC des garanties théoriques que beaucoup d’autres méthodes auto-supervisées n’ont pas.
Limites
- Dépendance à la structure séquentielle : le CPC suppose implicitement qu’il existe une structure temporelle exploitable dans les données. Pour des données i.i.d. (indépendantes et identiquement distribuées) sans ordre temporel signifiant, le CPC n’apporte pas de bénéficé par rapport à d’autres méthodes contrastives.
- Sensibilité́ aux biais du lot : la génération de négociatifs dépend de la composition du lot d’entraînement. Si le lot contient des échantillons très similaires entre eux, les négociatifs peuvent être trop faciles, ce qui affaiblit le signal d’apprentissage.
- Coût mémoire pour les grands lots : la nécessité́ de nombreux échantillons négatifs implique des lots de grande taille, ce qui peut devenir prohibitif en mémoire GPU, surtout pour des séquences longues.
- Difficulté́́ avec les dépendances très longues : bien que l’autorégresseur puisse théoriquement modéliser des dépendances arbitrairement longues, les GRU pratiques ont une mémoire effective limitée. Les étapes de prédiction éloignées (k élevé́) sont donc souvent moins bien apprises.
4 Cas d’Usage Concrets
Cas d’usage 1 : Reconnaissance automatique de la parole (ASR)
C’est l’application phare du CPC. En pré-entraînant un encodeur CPC sur des milliers d’heures d’audio non transcrit, on obtient des représentations qui capturent les phonèmes, les syllabes et même certains aspects syntaxiques. Un classifieur léger (par exemple un petit réseau linéaire) greffé sur ces représentations atteint des performances remarquables sur la reconnaissance de la parole, même avec très peu de données étiquetées pour le réglage fin.
Exemple concret : pré-entraîner CPC sur le corpus Common Voice de Mozilla (des milliers d’heures de parole libre dans de nombreuses langues), puis fine-tuner avec seulement 100 heures de transcription pour un nouveau dialecte. Les résultats sont souvent supérieurs à un entraînement supervisé à partir de zéro sur ces 100 heures.
Cas d’usage 2 : Classification de la musique par genre
La musique possèdé une structure temporelle riche — mélodie, harmonie, rythme, timbre — que le CPC est particulièrement bien placé pour capturer. En appliquant le CPC à des spectrogrammes musicaux, le modèle apprend à distinguer les motifs caractéristiques de chaque genre musical sans connaître les étiquettes de genre pendant l’entraînement.
Exemple concret : encoder des extraits de 30 secondes de musique avec un CPC pré-entraîné, puis utiliser ces représentations pour alimenter un classifieur k-NN ou une forêt aléatoire. Cette approche atteint des précisions de l’ordre de 85–90% sur des tâches de classification multi-genres, surpassant les méthodes basées sur les caractéristiques manuelles comme les MFCC.
Cas d’usage 3 : Détection d’anomalies dans les séries temporelles industrielles
Dans les environnements industriels, les capteurs produisent des flux continus de données (température, pression, vibration, courant). Le CPC peut apprendre la dynamique normale du système pendant une phase de pré-entraînement. Lors du déploiement, toute déviation significative entre la prédiction du contexte et l’observation réelle signale une anomalie potentielle.
Exemple concret : sur les données du benchmark Numenta Anomaly Benchmark (NAB), un détecteur basé sur CPC détecte les anomalies avec une latence nettement inférieure aux méthodes statistiques classiques (CUSUM, EWMA), tout en produisant moins de fausses alertes.
Cas d’usage 4 : Représentations visuelles auto-supervisées
Bien que le CPC ait été conçu pour les séquences, il s’applique également aux images en les traitant comme des séquences de patchs. On divise l’image en une grille de régions (par exemple 7×7), on encode chaque région avec un petit CNN, puis l’autoré́gresseur traite ces patchs dans un ordre balayé (de gauche à droite, de haut en bas). La prédiction des patchs futurs force le modèle à apprendre des caractéristiques visuelles riches.
Exemple concret : CPC-v2 appliqué à ImageNet sans étiquette produit des représentations qui, une fois transférées vers une tâche de classification avec étiquettes limitées, surpassent les modèles pré-entraînés de manière supervisée sur ImageNet. Cette approche a ouvert la voie aux méthodes modernes comme SimCLR et MoCo.
Voir Aussi
- Trouver la Plus Grande Sous-Matrice Zéro en Python
- Maîtriser Python : Résoudre le Problème ‘Birds on a Wire’ avec Efficacité

