Transfer learning et fine-tuning en PyTorch : Réutiliser des modèles pré-entraînés
Entraîner un réseau de neurones from scratch nécessite des dizaines de milliers d’images et plusieurs jours sur GPU. Et si vous pouviez obtenir d’excellents résultats avec seulement 100-1000 images et quelques heures ? C’est la promesse du transfer learning : réutiliser des modèles déjà entraînés et les adapter à votre tâche spécifique.
Pourquoi transfer learning ?
Comparez ces deux approches :
Entraînement from scratch
❌ 10,000-100,000 images nécessaires
❌ 2-7 jours d'entraînement sur GPU
❌ Risque d'overfitting si peu de données
❌ Nécessite expertise en hyperparamètres
Transfer learning
✅ 100-1,000 images suffisent
✅ Quelques heures d'entraînement
✅ Meilleure généralisation
✅ Convergence plus rapide et stable

Les deux approches
Feature Extraction (extraction de features)
On gèle tous les poids du modèle pré-entraîné et entraîne seulement une nouvelle dernière couche :
import torch
import torch.nn as nn
from torchvision import models
# 1. Charger modèle pré-entraîné
model = models.resnet50(pretrained=True)
# 2. Geler TOUS les paramètres
for param in model.parameters():
param.requires_grad = False
# 3. Remplacer la dernière couche
num_features = model.fc.in_features # 2048 pour ResNet50
model.fc = nn.Linear(num_features, num_classes) # num_classes = vos classes
# 4. Optimizer seulement sur la nouvelle couche
optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3)
Quand utiliser :
- ✅ Très peu de données (<1000 images)
- ✅ Votre tâche est similaire à ImageNet
- ✅ Vous voulez un entraînement ultra-rapide
- ❌ Votre domaine est très différent (imagerie médicale, satellites)
Fine-tuning complet
On dégèle progressivement des couches et les entraîne avec des learning rates différenciés :
# 1. Charger modèle pré-entraîné
model = models.resnet50(pretrained=True)
# 2. Remplacer dernière couche
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, num_classes)
# 3. Learning rates différenciés
optimizer = torch.optim.Adam([
{'params': model.layer4.parameters(), 'lr': 1e-5}, # Dernière conv block
{'params': model.fc.parameters(), 'lr': 1e-3} # Nouvelle tête
])
# Note : les autres couches restent gelées
for name, param in model.named_parameters():
if 'layer4' not in name and 'fc' not in name:
param.requires_grad = False
Quand utiliser :
- ✅ Dataset de taille moyenne (1000-10,000 images)
- ✅ Votre domaine diffère d’ImageNet
- ✅ Vous cherchez la meilleure accuracy possible
- ❌ Très peu de données (risque d’overfitting)
Modèles pré-entraînés disponibles
PyTorch fournit des dizaines de modèles pré-entraînés via torchvision.models.
ResNet (Residual Networks)
Très populaire, excellent compromis performance/vitesse :
from torchvision import models
# Différentes tailles
resnet18 = models.resnet18(pretrained=True) # 11M params, rapide
resnet34 = models.resnet34(pretrained=True) # 21M params
resnet50 = models.resnet50(pretrained=True) # 25M params, bon défaut
resnet101 = models.resnet101(pretrained=True) # 44M params
resnet152 = models.resnet152(pretrained=True) # 60M params, très précis
# Voir l'architecture
print(resnet50)
EfficientNet (efficace en paramètres)
State-of-the-art en 2020, excellent compromis taille/performance :
# EfficientNet B0 à B7 (du plus petit au plus grand)
efficientnet_b0 = models.efficientnet_b0(pretrained=True) # 5M params
efficientnet_b1 = models.efficientnet_b1(pretrained=True) # 7M params
efficientnet_b4 = models.efficientnet_b4(pretrained=True) # 19M params
efficientnet_b7 = models.efficientnet_b7(pretrained=True) # 66M params
Vision Transformers (ViT)
Architecture Transformer appliquée à la vision, SOTA 2024 :
# Vision Transformer
vit_b_16 = models.vit_b_16(pretrained=True) # Base, patch 16x16
vit_b_32 = models.vit_b_32(pretrained=True) # Base, patch 32x32
vit_l_16 = models.vit_l_16(pretrained=True) # Large, très précis
# Swin Transformer (variante performante)
swin_t = models.swin_t(pretrained=True) # Tiny
swin_s = models.swin_s(pretrained=True) # Small
swin_b = models.swin_b(pretrained=True) # Base
MobileNet (pour mobile/edge)
Optimisé pour mobile et edge devices :
mobilenet_v2 = models.mobilenet_v2(pretrained=True) # 3.5M params
mobilenet_v3_small = models.mobilenet_v3_small(pretrained=True)
mobilenet_v3_large = models.mobilenet_v3_large(pretrained=True)
Tableau comparatif
| Modèle | Params | Top-1 Acc | Vitesse | Use Case |
|---|---|---|---|---|
| ResNet18 | 11M | 69.8% | ⚡⚡⚡⚡⚡ | Prototypage rapide |
| ResNet50 | 25M | 76.1% | ⚡⚡⚡⚡ | Bon défaut |
| EfficientNet-B0 | 5M | 77.7% | ⚡⚡⚡⚡ | Efficacité |
| EfficientNet-B4 | 19M | 83.0% | ⚡⚡⚡ | Haute précision |
| ViT-B/16 | 86M | 81.1% | ⚡⚡ | Transformers, gros datasets |
| Swin-T | 28M | 81.3% | ⚡⚡⚡ | SOTA efficient |
| MobileNet-V2 | 3.5M | 71.9% | ⚡⚡⚡⚡⚡ | Mobile, edge |
Quel modèle choisir ? ResNet50 : Excellent point de départ, rapide et performant. EfficientNet-B4 : Meilleur accuracy avec peu de paramètres. ViT : Si vous avez beaucoup de données (>10K images). MobileNet : Pour déploiement mobile/edge.
Feature extraction : étape par étape
Implémentons une feature extraction complète sur un dataset custom.
Charger et préparer le modèle
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import models
# Charger ResNet50 pré-entraîné
model = models.resnet50(pretrained=True)
# Geler tous les paramètres
for param in model.parameters():
param.requires_grad = False
# Remplacer la dernière couche (fc) pour votre nombre de classes
num_classes = 10 # Ex: 10 classes custom
num_features = model.fc.in_features # 2048 pour ResNet50
model.fc = nn.Linear(num_features, num_classes)
# Vérifier ce qui est trainable
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"Total params: {total_params:,}")
print(f"Trainable params: {trainable_params:,}") # Seulement la nouvelle fc layer
Préparer les données
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# Transformations (ImageNet normalization)
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406], # ImageNet mean
std=[0.229, 0.224, 0.225] # ImageNet std
)
])
# Charger dataset
train_dataset = datasets.ImageFolder('data/train/', transform=transform)
val_dataset = datasets.ImageFolder('data/val/', transform=transform)
# DataLoaders
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4)
Important : Utilisez toujours la normalisation ImageNet (mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) pour les modèles pré-entraînés sur ImageNet. Sinon, les features extraites seront incorrectes !
Entraîner
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
# Optimizer seulement sur la nouvelle layer
optimizer = optim.Adam(model.fc.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
# Training loop
num_epochs = 10
for epoch in range(num_epochs):
# Training
model.train()
train_loss = 0.0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
# Validation
model.eval()
val_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
val_loss += loss.item()
_, predicted = outputs.max(1)
correct += predicted.eq(labels).sum().item()
total += labels.size(0)
train_loss /= len(train_loader)
val_loss /= len(val_loader)
val_acc = 100.0 * correct / total
print(f"Epoch {epoch+1}/{num_epochs}")
print(f" Train Loss: {train_loss:.4f}")
print(f" Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%")
Fine-tuning : Dégel progressif
Pour obtenir de meilleures performances, dégelez progressivement les couches.
Dégel en deux phases
# Phase 1 : Feature extraction (5 epochs)
model = models.resnet50(pretrained=True)
for param in model.parameters():
param.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, num_classes)
optimizer = optim.Adam(model.fc.parameters(), lr=1e-3)
# Entraîner 5 epochs...
# train(model, train_loader, val_loader, epochs=5)
# Phase 2 : Fine-tuning (10 epochs)
# Dégeler layer4 (dernière conv block)
for param in model.layer4.parameters():
param.requires_grad = True
# Learning rates différenciés
optimizer = optim.Adam([
{'params': model.layer4.parameters(), 'lr': 1e-5}, # LR faible
{'params': model.fc.parameters(), 'lr': 1e-4} # LR moyen
])
# Entraîner 10 epochs supplémentaires...
# train(model, train_loader, val_loader, epochs=10)
Learning rates discriminatifs
Différents LR pour chaque groupe de couches :
def get_parameter_groups(model, base_lr=1e-5):
"""
Crée des groupes de paramètres avec LR décroissants
"""
# ResNet structure : conv1 → layer1 → layer2 → layer3 → layer4 → fc
param_groups = [
{'params': model.conv1.parameters(), 'lr': base_lr * 0.1},
{'params': model.layer1.parameters(), 'lr': base_lr * 0.2},
{'params': model.layer2.parameters(), 'lr': base_lr * 0.4},
{'params': model.layer3.parameters(), 'lr': base_lr * 0.6},
{'params': model.layer4.parameters(), 'lr': base_lr * 0.8},
{'params': model.fc.parameters(), 'lr': base_lr * 10} # Nouvelle layer
]
return param_groups
# Utilisation
model = models.resnet50(pretrained=True)
model.fc = nn.Linear(model.fc.in_features, num_classes)
param_groups = get_parameter_groups(model, base_lr=1e-5)
optimizer = optim.Adam(param_groups)
Dégel graduel (layer by layer)
def unfreeze_layer(model, layer_name):
"""
Dégèle une couche spécifique
"""
for name, param in model.named_parameters():
if layer_name in name:
param.requires_grad = True
# Entraînement progressif
model = models.resnet50(pretrained=True)
model.fc = nn.Linear(model.fc.in_features, num_classes)
# Tout geler sauf fc
for param in model.parameters():
param.requires_grad = False
for param in model.fc.parameters():
param.requires_grad = True
# Epoch 1-5 : fc seulement
# train(model, epochs=5)
# Epoch 6-10 : fc + layer4
unfreeze_layer(model, 'layer4')
optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-5)
# train(model, epochs=5)
# Epoch 11-15 : fc + layer4 + layer3
unfreeze_layer(model, 'layer3')
optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-5)
# train(model, epochs=5)
Adapter à différentes architectures
Chaque architecture a sa propre structure pour la dernière couche.
ResNet
model = models.resnet50(pretrained=True)
# Dernière couche : model.fc (Linear)
num_features = model.fc.in_features # 2048
model.fc = nn.Linear(num_features, num_classes)
EfficientNet
model = models.efficientnet_b0(pretrained=True)
# Dernière couche : model.classifier (Sequential)
num_features = model.classifier[1].in_features # 1280
model.classifier = nn.Sequential(
nn.Dropout(p=0.2),
nn.Linear(num_features, num_classes)
)
Vision Transformer (ViT)
model = models.vit_b_16(pretrained=True)
# Dernière couche : model.heads.head (Linear)
num_features = model.heads.head.in_features # 768
model.heads.head = nn.Linear(num_features, num_classes)
MobileNet
model = models.mobilenet_v2(pretrained=True)
# Dernière couche : model.classifier (Sequential)
num_features = model.classifier[1].in_features # 1280
model.classifier = nn.Sequential(
nn.Dropout(p=0.2),
nn.Linear(num_features, num_classes)
)
Data Augmentation pour Transfer Learning
L’augmentation est encore plus importante avec peu de données :
from torchvision import transforms
# Augmentation agressive pour training
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.7, 1.0)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomVerticalFlip(p=0.2),
transforms.RandomRotation(degrees=15),
transforms.ColorJitter(
brightness=0.3,
contrast=0.3,
saturation=0.3,
hue=0.1
),
transforms.RandomAffine(
degrees=0,
translate=(0.1, 0.1),
scale=(0.9, 1.1)
),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
# Pas d'augmentation pour validation
val_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
Exemple complet : Transfer learning sur dataset custom
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import models, datasets, transforms
from torch.utils.data import DataLoader
# Configuration
num_classes = 10
batch_size = 32
num_epochs_phase1 = 5
num_epochs_phase2 = 10
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Transformations
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
val_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
# Datasets
train_dataset = datasets.ImageFolder('data/train/', transform=train_transform)
val_dataset = datasets.ImageFolder('data/val/', transform=val_transform)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=4)
# Modèle
model = models.resnet50(pretrained=True)
for param in model.parameters():
param.requires_grad = False
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, num_classes)
model = model.to(device)
# Phase 1 : Feature Extraction
print("=== Phase 1: Feature Extraction ===")
optimizer = optim.Adam(model.fc.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)
best_acc = 0.0
for epoch in range(num_epochs_phase1):
# Training
model.train()
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
scheduler.step()
# Validation
model.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = outputs.max(1)
correct += predicted.eq(labels).sum().item()
total += labels.size(0)
acc = 100.0 * correct / total
print(f"Epoch {epoch+1}: Val Acc = {acc:.2f}%")
if acc > best_acc:
best_acc = acc
torch.save(model.state_dict(), 'best_model_phase1.pth')
# Phase 2 : Fine-Tuning
print("\n=== Phase 2: Fine-Tuning ===")
for param in model.layer4.parameters():
param.requires_grad = True
optimizer = optim.Adam([
{'params': model.layer4.parameters(), 'lr': 1e-5},
{'params': model.fc.parameters(), 'lr': 1e-4}
])
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_epochs_phase2)
for epoch in range(num_epochs_phase2):
# Training
model.train()
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
scheduler.step()
# Validation
model.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = outputs.max(1)
correct += predicted.eq(labels).sum().item()
total += labels.size(0)
acc = 100.0 * correct / total
print(f"Epoch {epoch+1}: Val Acc = {acc:.2f}%")
if acc > best_acc:
best_acc = acc
torch.save(model.state_dict(), 'best_model_phase2.pth')
print(f"\nBest Validation Accuracy: {best_acc:.2f}%")
Conseils et best practices
Choisir le bon learning rate
# Learning rates recommandés
lr_feature_extraction = 1e-3 # Nouvelle couche
lr_fine_tuning_high = 1e-4 # Nouvelle couche en fine-tuning
lr_fine_tuning_low = 1e-5 # Couches pré-entraînées
Éviter l’overfitting
# Techniques anti-overfitting
model = models.resnet50(pretrained=True)
model.fc = nn.Sequential(
nn.Dropout(0.5), # Dropout avant dernière layer
nn.Linear(2048, num_classes)
)
# Data augmentation agressive
# Early stopping
# Régularisation L2 (weight_decay)
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
Surveiller la convergence
# Signes de bonne convergence :
# - Val acc augmente régulièrement
# - Gap train/val < 5-10%
# - Loss diminue progressivement
# Signes de problèmes :
# - Val acc stagne dès epoch 1 → LR trop faible ou modèle mal initialisé
# - Val acc diminue après quelques epochs → Overfitting, ajouter régularisation
# - Loss explose → LR trop élevé
Exercices pratiques
Comparer modèles
Comparez ResNet50, EfficientNet-B0 et MobileNet-V2 sur le même dataset :
models_to_test = {
'resnet50': models.resnet50(pretrained=True),
'efficientnet_b0': models.efficientnet_b0(pretrained=True),
'mobilenet_v2': models.mobilenet_v2(pretrained=True)
}
for name, model in models_to_test.items():
# Adapter dernière couche
# Entraîner
# Mesurer accuracy et temps
pass
Impact du dégel progressif
Mesurez l’impact du fine-tuning vs feature extraction pure :
# Scénario 1 : Feature extraction seulement
# Scénario 2 : Feature extraction + fine-tuning layer4
# Scénario 3 : Feature extraction + fine-tuning complet
# Comparer les accuracy finales
Optimal learning rate
Trouvez le meilleur learning rate avec un learning rate finder :
def find_lr(model, train_loader, start_lr=1e-7, end_lr=1, num_iter=100):
lrs = []
losses = []
# ... implémenter LR finder
return lrs, losses
Conclusion
Dans cet article, vous avez maîtrisé le transfer learning avec PyTorch :
- ✅ Principe du transfer learning (réutiliser modèles pré-entraînés)
- ✅ Feature extraction (geler tout sauf dernière couche)
- ✅ Fine-tuning complet (dégel progressif)
- ✅ Learning rates différenciés par couche
- ✅ Modèles disponibles (ResNet, EfficientNet, ViT)
- ✅ Adaptation à différentes architectures
- ✅ Data augmentation spécifique
- ✅ Stratégies d’entraînement en 2 phases
Prochaine étape : Dans le prochain article, nous allons plonger dans les CNNs et Computer Vision. Vous apprendrez les convolutions en détail, les architectures classiques, et des tâches avancées comme la détection d’objets.
Pour aller plus loin :
- Explorez le Model Zoo de torchvision
- Consultez les benchmarks de modèles sur Papers with Code
- Découvrez Hugging Face Hub pour plus de modèles
Projet pratique : Collectez 100-500 images par classe pour un problème de classification (ex: chiens vs chats, fleurs, produits). Entraînez ResNet50 avec transfer learning. Comparez feature extraction vs fine-tuning. Objectif : >90% accuracy avec <1000 images totales !