درس 5 — Préparer un jeu de données d'ajustement

Découper en train, validation et test

5 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Découper en train, validation et test

Trois jeux, trois rôles

JeuTaille dans le labQui le voitSert à
train (entraînement)70le job de réglage, pour ajuster les poidsapprendre
validation15le job de réglage, seulement pour mesurer à chaque étapedétecter le surapprentissage, choisir le nombre d'époques
test15personne pendant le réglage : seulement evaluate.pyla note finale, avant / après

Le surapprentissage (overfitting) se produit quand le modèle apprend les exemples par cœur au lieu de la règle : sa perte (loss, l'erreur mesurée) baisse sur train mais remonte sur validation. Vertex AI affiche les deux courbes pendant le job.

flowchart LR
  A["100 exemples"] --> S["stratified_split, graine 42"]
  S --> T["train.jsonl : 70"]
  S --> V["validation.jsonl : 15"]
  S --> X["test.jsonl : 15"]
  T --> J["Job de réglage"]
  V --> J
  X --> E["evaluate.py, avant et après"]

Un découpage stratifié et reproductible

Stratifié veut dire que chaque jeu garde la même répartition des étiquettes que l'ensemble. Sans cela, le hasard peut mettre toutes les résiliations dans le train et aucune dans le test.

files/distill.py (extrait)
def stratified_split(rows: list[dict], ratios=(0.7, 0.15, 0.15), seed=SEED):
    """Découpe train/validation/test en gardant la même proportion de chaque étiquette."""
    rng = random.Random(seed)
    parts = ([], [], [])
    for label in LABELS:
        group = [r for r in rows if r["label"] == label]
        rng.shuffle(group)
        n_train = round(len(group) * ratios[0])
        n_val = round(len(group) * ratios[1])
        parts[0].extend(group[:n_train])
        parts[1].extend(group[n_train:n_train + n_val])
        parts[2].extend(group[n_train + n_val:])
    for p in parts:
        rng.shuffle(p)
    return parts

random.Random(42) rend le découpage reproductible : relancer le script donne les mêmes jeux, donc des évaluations comparables. Le test unitaire correspondant vérifie les tailles, la stratification et l'absence de recouvrement :

files/test_lab.py (extrait)
def test_split_is_stratified_and_disjoint():
    rows = [{"ticket": f"{l} {i}", "label": l} for l in LABELS for i in range(20)]
    train, val, test = stratified_split(rows)
    assert (len(train), len(val), len(test)) == (70, 15, 15)
    for part, n in ((train, 14), (val, 3), (test, 3)):
        assert all(sum(r["label"] == l for r in part) == n for l in LABELS)

Pendant le job, la perte sur train continue de baisser mais celle sur validation remonte depuis la 3e époque. Que se passe-t-il ?

Les pièges classiques

PiègeSymptômeParade
Quasi-doublons répartis entre train et testnote de test anormalement hautedédoublonner avant de découper (norm())
Découpage non stratifiéune classe absente du teststratified_split
Données réelles découpées au hasard dans le tempsle modèle « connaît le futur »pour des tickets réels, test = les plus récents
Même client dans train et testle modèle apprend ses tournuresdécouper par client ou par conversation

Pourquoi fixer la graine aléatoire du découpage ?

Exercice : un découpage 80 / 20

Votre équipe ne veut pas de jeu de test séparé : « 80 % train, 20 % validation, et on évalue sur la validation ». Écrivez l'appel à stratified_split correspondant et expliquez en deux phrases le risque.

Voir la solution
train, val, test = stratified_split(rows, ratios=(0.8, 0.2, 0.0))
# test est vide : 16 + 4 par étiquette

Risque : la validation a été vue par le job (pour la perte de validation et, si l'on choisit un point de contrôle, pour sélectionner le meilleur). La note mesurée dessus est donc légèrement optimiste : on a « choisi » le modèle qui réussit le mieux ce jeu précis. Un test jamais touché donne une estimation honnête de ce qui se passera en production.

Résultat obtenu

Exécution réelle le 2026-10-15 : les trois jeux produits par distill.py, puis la suite de tests hors ligne (pytest, sans clé) :

train.jsonl: {"exemples": 70, "par_etiquette": {"demande_fonctionnalite": 14, "resiliation": 14, "facturation": 14, "acces_compte": 14, "bug_technique": 14}, ...}
validation.jsonl: {"exemples": 15, "par_etiquette": {"resiliation": 3, "facturation": 3, "bug_technique": 3, "demande_fonctionnalite": 3, "acces_compte": 3}, ...}
test.jsonl: {"exemples": 15, "par_etiquette": {"facturation": 3, "acces_compte": 3, "resiliation": 3, "demande_fonctionnalite": 3, "bug_technique": 3}, ...}
OK : format valide

$ pytest -q
..............                                                           [100%]
14 passed in 0.31s

Je sais faire

  • Expliquer les rôles de train, validation et test
  • Reconnaître le surapprentissage sur les courbes de perte
  • Faire un découpage stratifié et reproductible
  • Éviter les fuites : doublons, temps, même client
التالي