Boucle PyTorch minimale
Dataset, loader, forward, backward, step.
Énoncé
Sur les deux lunes synthétiques de scikit-learn, entraînez un MLP PyTorch à deux couches. Faites un découpage train/validation/test avant tout entraînement ; gardez le test fermé pendant le choix de l'époque. Utilisez DataLoader, BCEWithLogitsLoss, Adam et une courbe des pertes train/validation. Rechargez les meilleurs poids selon la validation, puis calculez accuracy et F1 une seule fois sur test.
Contraintes
CPU suffisant. Notez les versions de PyTorch et scikit-learn, la graine et le bruit des lunes. model.train() pour optimiser ; model.eval() et torch.no_grad() pour validation et test. Ne regardez aucune métrique du test pendant les 100 époques. Le test est synthétique : ne généralisez pas son score à des données réelles.
Indices
nn.Sequential(nn.Linear(2,32), nn.ReLU(), nn.Linear(32,1)) renvoie des logits : pas de sigmoïde avant BCEWithLogitsLoss. Sauvegardez une copie des poids quand la perte de validation baisse, pas seulement une référence à state_dict() qui continuerait à changer. En test, appliquez torch.sigmoid puis le seuil 0,5.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Enregistrez sous lunes_torch.py, puis lancez python lunes_torch.py après avoir installé PyTorch dans le même environnement que scikit-learn et Matplotlib :
import matplotlib.pyplot as plt
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
from sklearn.datasets import make_moons
from sklearn.metrics import accuracy_score, f1_score
from sklearn.model_selection import train_test_split
torch.manual_seed(42)
X, y = make_moons(n_samples=600, noise=0.25, random_state=42)
X_train, X_reste, y_train, y_reste = train_test_split(
X, y, test_size=0.4, stratify=y, random_state=42
)
X_val, X_test, y_val, y_test = train_test_split(
X_reste, y_reste, test_size=0.5, stratify=y_reste, random_state=42
)
def tenseurs(x, cible):
return (
torch.tensor(x, dtype=torch.float32),
torch.tensor(cible, dtype=torch.float32).reshape(-1, 1),
)
x_train, t_train = tenseurs(X_train, y_train)
x_val, t_val = tenseurs(X_val, y_val)
x_test, _ = tenseurs(X_test, y_test)
jeu_train = TensorDataset(x_train, t_train)
chargeur = DataLoader(
jeu_train, batch_size=32, shuffle=True,
generator=torch.Generator().manual_seed(42),
)
net = nn.Sequential(nn.Linear(2, 32), nn.ReLU(), nn.Linear(32, 1))
optimiseur = torch.optim.Adam(net.parameters(), lr=0.01)
perte = nn.BCEWithLogitsLoss()
historique_train, historique_val = [], []
meilleure_val = float("inf")
meilleurs_poids = None
for epoque in range(100):
net.train()
cumul = 0.
for x_lot, y_lot in chargeur:
optimiseur.zero_grad()
logits = net(x_lot)
erreur = perte(logits, y_lot)
erreur.backward()
optimiseur.step()
cumul += erreur.item() * len(x_lot)
historique_train.append(cumul / len(jeu_train))
net.eval()
with torch.no_grad():
val = perte(net(x_val), t_val).item()
historique_val.append(val)
if val < meilleure_val:
meilleure_val = val
meilleurs_poids = {cle: valeur.detach().clone() for cle, valeur in net.state_dict().items()}
net.load_state_dict(meilleurs_poids)
net.eval()
with torch.no_grad():
proba = torch.sigmoid(net(x_test)).numpy().ravel()
prediction = (proba >= 0.5).astype(int)
print("accuracy test :", round(accuracy_score(y_test, prediction), 3))
print("F1 test :", round(f1_score(y_test, prediction), 3))
plt.plot(historique_train, label="train")
plt.plot(historique_val, label="validation")
plt.xlabel("époque")
plt.ylabel("log-loss")
plt.legend()
plt.tight_layout()
plt.savefig("lunes_pertes.png")
X_test n'apparaît qu'après le rechargement des meilleurs poids : c'est le contrôle le plus important de cet atelier. Si la validation remonte alors que le train baisse, retenez une époque antérieure. Si les scores bougent entre machines, vérifiez versions et déterminisme avant d'interpréter quelques points d'écart.