3.7 — Distillation maître-élève : quand le savoir se transfère par des cibles molles

Navigation : << 3.6-Modèles génératifs · Feuille de route de la série · 3.8 — Représentations contrastives >>

Le dépôt couvre la quantification, LoRA et le model merging, mais jamais la mécanique teacher/student : comment un maître déjà entraîné supervise un élève plus petit, via des cibles molles (soft targets). Ce notebook ouvre cette mécanique étape par étape, sur une tâche bornée (Fashion-MNIST) : le maître apprend sur toutes les données, puis distille son savoir — y compris ce que les étiquettes dures ne disent pas — vers un élève nettement plus petit. On compare à un élève entraîné sans distillation, au même budget.

Le fil directeur reprend celui de la série : le mécanisme d’abord (la loss de distillation, écrite au complet), puis la mesure (exactitude, calibration, taille, latence), puis le verdict explicite.

Kernel : coursia-ml-training (torch + torchvision). Première exécution : télécharge Fashion-MNIST (~30 Mo) dans data/fmnist/.

1. Le cadre : un maître entraîné, un élève plus petit, une tâche bornée

La tâche est la classification de 10 vêtements (Fashion-MNIST), 784 pixels en entrée. Deux réseaux de capacités distinctes :

  • Le maître — un MLP 784→256→128→10. Assez large pour bien apprendre sur les 60 000 images d’entraînement.
  • L’élève — un MLP 784→32→32→10, environ dix fois plus petit. C’est lui qu’on veut au final (moins de paramètres, moins de latence), mais il apprend moins bien seul.

L’astuce de la distillation : l’élève n’apprend pas seulement les étiquettes dures (0/1/…) mais aussi la distribution de probabilités que le maître associe à chaque entrée — ses confiances. Parce que le maître est bien entraîné, sa distribution est riche : pour un pullover il est quasi sûr, mais pour un vêtement ambigu il répartit sa croyance entre des classes proches (pullover, manteau, chemise). Cette information est le dark knowledge : absente de l’étiquette dure, elle aide un petit élève.

On va le mesurer plutôt que l’affirmer : le maître est figé (une seule fois), puis on entraîne l’élève (a) sur les étiquettes dures — la baseline — et (b) sur un mélange d’étiquettes dures + cibles molles — la distillation. Quatre graines partagées, pour que la conclusion ne dépende pas d’un tirage chanceux.

import torch, torch.nn as nn, torch.nn.functional as F
import numpy as np, statistics as st, time, io
from scipy import stats as sps   # test t apparie du verdict (section 9)
from pathlib import Path
from torchvision import datasets

torch.set_num_threads(1)   # thread unique : calcul CPU reproducible (meme graine -> meme resultat)

# --- configuration canonique ---
SEEDS = [0, 1, 2, 3]          # quatre graines partagees
N_LABEL = 1500                # budget d'etiquettes commun (baseline et distillation)
ALPHAS  = [0.0, 0.5, 1.0]     # alpha=1 : CE dure pure ; alpha=0 : distillation pure
TEMPS   = [1.0, 2.0, 5.0]     # trois temperatures
DEVICE  = "cpu"

ROOT = Path("data/fmnist")
tr = datasets.FashionMNIST(ROOT, train=True, download=True)
te = datasets.FashionMNIST(ROOT, train=False, download=True)
Xtr = tr.data.float().view(-1, 784) / 255.0
ytr = tr.targets
Xte = te.data.float().view(-1, 784) / 255.0
yte = te.targets
print("torch", torch.__version__, "| train", Xtr.shape, "| test", Xte.shape)
torch 2.6.0+cu124 | train torch.Size([60000, 784]) | test torch.Size([10000, 784])
def mlp(h1, h2):
    return nn.Sequential(nn.Linear(784, h1), nn.ReLU(),
                        nn.Linear(h1, h2), nn.ReLU(),
                        nn.Linear(h2, 10))

def n_params(m):
    return sum(p.numel() for p in m.parameters())

def seed_model(h1, h2, seed):
    # init deterministe : le meme seed donne le meme poids d'init, pour que la
    # comparaison baseline / distillation partage la meme graine au sens strict.
    torch.manual_seed(seed)
    return mlp(h1, h2)

teacher = mlp(256, 128)
student = mlp(32, 32)
print(f"maitre  : {n_params(teacher):>7} params")
print(f"eleve   : {n_params(student):>7} params")
print(f"ratio   : {n_params(teacher) / n_params(student):>6.1f}x")
maitre  :  235146 params
eleve   :   26506 params
ratio   :    8.9x

2. La loss de distillation — CE dure + KL sur logits tempérés

La loss mélange deux termes :

\[L = \alpha \, L_{\text{dure}} + (1-\alpha)\, T^2\, D_{\text{KL}}\!\left(\mathrm{softmax}(z_m/T)\,\|\,\mathrm{softmax}(z_e/T)\right)\]

  • Terme dur L_dure : la cross-entropie entre les logits de l’élève et l’étiquette dure. C’est l’apprentissage classique ; il garantit que l’élève, sans rien d’autre, apprend quand même la tâche.
  • Terme mou L_mou : la divergence de Kullback-Leibler entre la distribution du maître (tempérée) et celle de l’élève (tempérée). C’est le véhicule du dark knowledge. La température T lisse la distribution du maître : à la limite T → ∞ toutes les classes deviennent équiprobables (l’information s’efface), à T → 1 on retombe sur la distribution dure du maître.
  • alpha règle l’équilibre : alpha = 1 → uniquement les étiquettes dures (baseline) ; alpha = 0 → uniquement les cibles molles (distillation pure).
  • Le facteur T² — voir la section suivante — compense l’amortissement du gradient des cibles molles quand T grandit.

On l’écrit telle quelle, et on vérifie le facteur T² numériquement.

def distill_loss(logits_student, logits_teacher, y, alpha=0.5, T=1.0):
    # terme dur : cross-entropie sur l'etiquette (formule classique)
    L_hard = F.cross_entropy(logits_student, y)
    # terme mou : KL sur logits temperes (T lisse la cible du maitre)
    L_soft = F.kl_div(
        F.log_softmax(logits_student / T, dim=1),   # eleve, tempere puis log-softmax
        F.softmax(logits_teacher / T, dim=1),        # maitre, tempere puis softmax = cible
        reduction="batchmean",
    ) * (T * T)                                      # facteur d'echelle (cf section 3)
    return alpha * L_hard + (1 - alpha) * L_soft

def train(model, X, y, epochs=50, seed=0, bs=64, lr=1e-3,
          teacher=None, alpha=0.5, T=1.0):
    torch.manual_seed(seed)
    opt = torch.optim.Adam(model.parameters(), lr=lr)
    n = X.shape[0]
    for ep in range(epochs):
        perm = torch.randperm(n, generator=torch.Generator().manual_seed(seed + ep))
        for i in range(0, n, bs):
            idx = perm[i:i + bs]; xb = X[idx]; yb = y[idx]
            opt.zero_grad()
            ls = model(xb)
            if teacher is None:
                loss = F.cross_entropy(ls, yb)          # baseline : apprentissage dur
            else:
                lt = teacher(xb).detach()               # le maitre est fige
                loss = distill_loss(ls, lt, yb, alpha=alpha, T=T)
            loss.backward(); opt.step()
    model.eval()
    return model

3. Le facteur d’échelle T² — expliqué et testé

Pourquoi multiplier la KL par T² ? La divergence de KL entre deux softmax tempérées a un gradient par rapport aux logits de l’élève qui diminue comme 1/T² quand T grandit. Sans la compensation, à grande température le terme mou pèse quasi rien : la distillation s’éteint toute seule, et l’élève est piloté par le seul terme dur.

On le vérifie : on fixe des logits aléatoires et on mesure la norme du gradient de la KL brute puis de la KL scalée par T², pour T ∈ {1, 2, 5}. On attend une KL brute qui chute ~1/T², et une KL scalée qui reste constante (invariance d’échelle).

def grad_norm(T, scaled):
    torch.manual_seed(0)
    ls = torch.randn(64, 10, requires_grad=True)
    lt = torch.randn(64, 10)
    kl = F.kl_div(F.log_softmax(ls / T, 1), F.softmax(lt / T, 1), reduction="batchmean")
    if scaled:
        kl = kl * (T * T)
    g, = torch.autograd.grad(kl, ls)
    return g.norm().item()

print(f"{'T':>4} {'KL brute':>10} {'T^2*KL':>10}   {'facteur':>8}")
for T in [1.0, 2.0, 5.0]:
    nb = grad_norm(T, False); sc = grad_norm(T, True)
    print(f"{T:>4.1f} {nb:>10.4f} {sc:>10.4f}   {sc / nb:>8.2f}")
   T   KL brute     T^2*KL    facteur
 1.0     0.0591     0.0591       1.00
 2.0     0.0143     0.0573       4.00
 5.0     0.0022     0.0549      25.00

4. Le maître — entraîné sur toutes les données

On entraîne le maître sur les 60 000 images, avec des étiquettes dures, une seule fois. Il est ensuite figé. On mesure son exactitude et sa calibration (erreur de calibration attendue, ECE) : une bonne calibration signifie que quand le modèle se dit confiant à 90 %, il a raison ~90 % du temps. Le maître sert de référence.

def acc(model):
    return (model(Xte).argmax(1) == yte).float().mean().item()

def ece(model, nbins=10):
    with torch.no_grad():
        p = F.softmax(model(Xte), 1)
        conf, pred = p.max(1); correct = (pred == yte).float()
    b = torch.linspace(0, 1, nbins + 1); e = 0.0
    for k in range(nbins):
        m = (conf >= b[k]) & (conf < b[k + 1]) if k < nbins - 1 else (conf >= b[k]) & (conf <= b[k + 1])
        if m.sum() > 0:
            e += (m.sum().item() / len(yte)) * abs(conf[m].mean().item() - correct[m].mean().item())
    return e

t0 = time.time()
teacher = seed_model(256, 128, 0)
train(teacher, Xtr, ytr, epochs=20, seed=0, bs=128)
print(f"maitre : acc={acc(teacher):.4f}  ece={ece(teacher):.4f}  params={n_params(teacher)}  [{time.time()-t0:.0f}s]")
maitre : acc=0.8906  ece=0.0362  params=235146  [42s]

5. La baseline et l’ablation alpha × T — même budget, quatre graines

On isole 1500 étiquettes du train. C’est le budget commun : la baseline et la distillation reçoivent exactement les mêmes 1500 exemples étiquetés (mêmes capitales, mêmes graines). La seule différence est le signal de supervision — dur, ou dur + cibles molles du maître sur ces mêmes exemples.

On balaie alpha ∈ {0, 0.5, 1} et T ∈ {1, 2, 5}, sur les quatre graines partagées, et on note l’exactitude et l’ECE. alpha = 1 doit retomber sur la baseline (pas de distillation).

torch.manual_seed(99)
sub = torch.randperm(Xtr.shape[0])[:N_LABEL]
Xs = Xtr[sub]; ys = ytr[sub]

def run(alpha, T, use_teacher):
    A = []; E = []
    for seed in SEEDS:
        m = train(seed_model(32, 32, seed), Xs, ys, seed=seed,
                  teacher=teacher if use_teacher else None,
                  alpha=alpha, T=T)
        A.append(acc(m)); E.append(ece(m))
    return A, E

BASE_A, BASE_E = run(1.0, 1.0, False)     # alpha=1 : apprentissage dur pur
print("=== BASELINE student-from-scratch (1500 etiquettes, 4 graines) ===")
print(f"  acc = {st.mean(BASE_A):.4f} +/- {st.pstdev(BASE_A):.4f}    ece = {st.mean(BASE_E):.4f}\n")

RES = {}     # RES[(alpha,T)] = (list acc, list ece)
print(f"{'alpha':>6} {'T':>4} {'acc_moy':>9} {'ece_moy':>9}")
for a in ALPHAS:
    for T in TEMPS:
        if a == 1.0:
            A, E = BASE_A, BASE_E      # alpha=1 : pas de distillation, la reference
        else:
            A, E = run(a, T, True)     # T se lit dans run pour a<1
        RES[(a, T)] = (A, E)
        tag = "  <- baseline" if a == 1.0 else ""
        print(f"{a:>6.1f} {T:>4.0f} {st.mean(A):>9.4f} {st.mean(E):>9.4f}{tag}")
=== BASELINE student-from-scratch (1500 etiquettes, 4 graines) ===
  acc = 0.8029 +/- 0.0054    ece = 0.0684

 alpha    T   acc_moy   ece_moy
   0.0    1    0.8145    0.0458
   0.0    2    0.8137    0.0731
   0.0    5    0.7940    0.0934
   0.5    1    0.8105    0.0536
   0.5    2    0.8132    0.0720
   0.5    5    0.8007    0.0838
   1.0    1    0.8029    0.0684  <- baseline
   1.0    2    0.8029    0.0684  <- baseline
   1.0    5    0.8029    0.0684  <- baseline

6. Lecture du dark knowledge

Le tableau précédent raconte trois choses :

  1. La baseline (alpha=1) plafonne autour de 0.803 : avec 1500 étiquettes seulement, un petit élève ne peut pas faire mieux. C’est le budget de référence.
  2. La calibration est le vrai gain : la meilleure calibration est obtenue en distillation pure (alpha=0, T=1) — ECE 0.0458 contre 0.0684 pour la baseline, soit ~33 % d’erreur de calibration en moins, gagnée sur les quatre graines. En exactitude le gain est réel mais modeste (+0.0116 en moyenne : 0.8145 contre 0.8029) et plus sensible à la graine.
  3. T=5 casse la distillation : l’ECE remonte nettement (0.094 pour alpha=0), l’exactitude chute. Une température trop haute répartit la croyance du maître sur trop de classes ; l’élève apprend du bruit. Il y a donc un réglage de température, pas une valeur toute faite.

Le dark knowledge est mesurable là où il compte : à alpha=0, T=1, l’élève approche la distribution du maître sur les exemples ambigus, et sa calibration s’améliore — il sait quand il est incertain — même quand l’exactitude ne bouge que peu. C’est le bénéfice principal de la distillation sur cette tâche.

7. Un exemple concret de dark knowledge

On prend une image de test (ici un manteau, classe 4) et on regarde la distribution du maître. La classe vraie domine, mais le maître répartit une partie de sa croyance sur des classes sémantiquement proches — pullover, chemise, robe. Cette répartition est le dark knowledge : l’étiquette dure ne dit que « manteau », la cible molle dit « manteau, mais un peu pullover, un peu chemise ». C’est exactement ce qui aide un élève à ne pas sur-trancher sur les exemples ambigus.

names = ["T-shirt", "Trouser", "Pullover", "Dress", "Coat", "Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot"]
idx = (yte == 4).nonzero()[0].item()     # une image de la classe 4 = Coat
img = Xte[idx]
with torch.no_grad():
    q = F.softmax(teacher(img[None]) / 5.0, 1)[0].topk(5)
print("image reelle : classe", names[4])
print("etiquette dure :", names[4])
print("maitre, softmax a T=5 :", [(names[i], round(float(v), 3)) for v, i in zip(q.values, q.indices)])
image reelle : classe Coat
etiquette dure : Coat
maitre, softmax a T=5 : [('Coat', 0.868), ('Pullover', 0.059), ('Shirt', 0.059), ('T-shirt', 0.008), ('Dress', 0.003)]

8. Mesures complémentaires : taille, latence, calibration

Le compromis compression–qualité se lit sur quatre axes : nombre de paramètres, taille sérialisée sur disque, latence CPU d’un passage avant, et calibration. On compare le maître, la baseline (élève dur) et l’élève distillé. Pour la taille et la latence on utilise un passage representatif (une seule graine, seed=0) — ce sont des axes de compression, pas des mesures de robustesse ; les resultats multi-graines sont aux sections 5 et 9. On observe que l’élève distillé est beaucoup plus petit et plus rapide que le maître, avec une calibration meilleure que la baseline.

def size_kb(m):
    buf = io.BytesIO(); torch.save(m.state_dict(), buf); return buf.tell() / 1024

def latency_ms(m, n=1000, bs=200):
    m.eval(); xb = Xte[:bs]
    with torch.no_grad():
        for _ in range(5): m(xb)          # warmup
        t0 = time.perf_counter()
        for _ in range(n // bs): m(xb)
    return (time.perf_counter() - t0) / (n // bs) * 1000

baseline = train(seed_model(32, 32, 0), Xs, ys, seed=0)
distille = train(seed_model(32, 32, 0), Xs, ys, seed=0, teacher=teacher, alpha=0.0, T=1.0)

print(f"{'':>9}{'params':>9}{'size(KB)':>10}{'lat(ms/b)':>10}{'acc':>8}{'ece':>8}")
for name, m in [("maitre", teacher), ("baseline", baseline), ("distille", distille)]:
    print(f"{name:>9}{n_params(m):>9}{size_kb(m):>10.1f}{latency_ms(m):>10.2f}{acc(m):>8.4f}{ece(m):>8.4f}")
            params  size(KB) lat(ms/b)     acc     ece
   maitre   235146     920.8      1.24  0.8906  0.0362
 baseline    26506     105.8      0.19  0.7948  0.0736
 distille    26506     105.8      0.23  0.8132  0.0462

9. Verdict

On tranche explicitement, par le protocole complet du dépôt : 5 folds × 4 graines. Chaque fold tire son budget de 1500 étiquettes du réservoir d’entraînement (tirages seedés, distincts des graines de modèle) ; le jeu de test reste fixe. Chaque paire (fold, graine) entraîne la baseline (élève dur) ET l’élève distillé (alpha=0, T=1) — un plan apparié : chaque paire fournit une différence directe. Le verdict est BEATS si : (a) le gain moyen atteint au moins deux écarts-types des différences appariées, sur l’exactitude et sur la calibration ; (b) le test de Diebold–Mariano sur la perte par exemple (cross-entropie) donne un p médian < 0.05. Le rapport de biais (perte moyenne par exemple, modèle et baseline) est affiché. Sinon on dégrade honnêtement : INCONCLUSIVE si une jambe passe et pas l’autre, NO BEATS si aucune ne passe.

# Protocole complet : 5 folds (tirages seedes du budget d'etiquettes) x 4 graines.
# Chaque paire (fold, graine) entraine la baseline ET le distille sur le MEME tirage :
# la difference est appariee deux fois. Le test  est  derive  de  ces  sorties.
FOLDS = 5
d_acc, d_ece, dm_ps, bias_b, bias_d = [], [], [], [], []
for f in range(FOLDS):
    rng = np.random.default_rng(100 + f)          # tirages distincts des graines de modele
    idx = torch.from_numpy(rng.choice(len(Xtr), size=N_LABEL, replace=False))
    Xf, yf = Xtr[idx], ytr[idx]
    for seed in SEEDS:
        b = train(seed_model(32, 32, seed), Xf, yf, seed=seed)
        d = train(seed_model(32, 32, seed), Xf, yf, seed=seed, teacher=teacher, alpha=0.0, T=1.0)
        d_acc.append(acc(d) - acc(b))
        d_ece.append(ece(b) - ece(d))             # gain de calibration = ECE qui baisse
        with torch.no_grad():
            lb = F.cross_entropy(b(Xte), yte, reduction="none")
            ld = F.cross_entropy(d(Xte), yte, reduction="none")
        diff = (lb - ld).numpy()                  # > 0 : le distille perd moins, par exemple
        dm = diff.mean() / np.sqrt(diff.var(ddof=1) / len(diff))   # DM h=1, exemples i.i.d.
        dm_ps.append(2 * sps.norm.sf(abs(dm)))
        bias_b.append(lb.mean().item()); bias_d.append(ld.mean().item())

n = len(d_acc)

def leg(nom, diffs):
    m, s = st.mean(diffs), st.stdev(diffs)
    vict = sum(1 for x in diffs if x > 0)
    print(f"{nom:<11}: gain {m:+.4f} +/- {s:.4f} | edge {m / s:+.1f} sigma | victoires {vict}/{n}")
    return m >= 2 * s

print(f"{FOLDS} folds x {len(SEEDS)} graines = {n} paires appariees (budget {N_LABEL} etiquettes par fold, test fixe)")
ok_acc = leg("exactitude", d_acc)
ok_ece = leg("calibration", d_ece)
p_med = st.median(dm_ps)
print(f"DM (CE/exemple): p median = {p_med:.4f} | biais CE baseline {st.mean(bias_b):.4f} vs distille {st.mean(bias_d):.4f}")
ok_dm = p_med < 0.05
verdict = "BEATS" if (ok_acc and ok_ece and ok_dm) else ("NO BEATS" if not (ok_acc or ok_ece or ok_dm) else "INCONCLUSIVE")
print("VERDICT :", verdict)
5 folds x 4 graines = 20 paires appariees (budget 1500 etiquettes par fold, test fixe)
exactitude : gain +0.0064 +/- 0.0113 | edge +0.6 sigma | victoires 17/20
calibration: gain +0.0166 +/- 0.0122 | edge +1.4 sigma | victoires 18/20
DM (CE/exemple): p median = 0.0000 | biais CE baseline 0.6337 vs distille 0.5813
VERDICT : INCONCLUSIVE

10. Distillation vs quantification (FT-02) et KL de régularisation (RLHF)

Trois techniques utilisent des distributions « molles » mais ne font pas la même chose :

  • Quantification (FT-02) : réduire la précision binaire des poids d’un modèle déjà entraîné. Même fonction, même connaissance, représentation plus compacte. Aucun entraînement, aucune connaissance nouvelle.
  • Distillation : entraîner un nouveau modèle plus petit à imiter la distribution de sortie d’un maître. L’élève apprend une version compressée du savoir — pas seulement les étiquettes, mais les confiances du maître.
  • KL de régularisation RLHF : un terme de pénalité dans la loss d’entraînement RL qui ancre la politique au modèle de référence pour l’empêcher de dériver. Ce n’est pas une façon de compresser du savoir dans un plus petit réseau : c’est un garde-fou de stabilité pendant l’optimisation.

En résumé : la quantification compacte un modèle existant, la distillation reproduit le savoir dans un modèle plus petit, la KL de RLHF stabilise un entraînement. Le notebook enseigne la deuxième.

Exercices

Les trois exercices sont exécutables de bout en bout (aucune erreur volontaire) ; il manque juste l’action de l’étudiant, marquée # TODO etudiant. Le troisième est un diagnostic : trouver pourquoi une distillation échoue.

# Exercice 1 : entre dur et mou.
# Entraine l'eleve avec alpha=0.5, T=4 et compare son exactitude a la baseline (alpha=1),
# a la distillation pure (alpha=0, T=1) et au meilleur reglage.
# Avec un vrai seed, une T moyenne (4) donne souvent un resultat EN DESSOUS de la baseline :
# observe, puis commente pourquoi.
m = train(seed_model(32, 32, 0), Xs, ys, seed=0, teacher=teacher, alpha=0.5, T=4.0)
print("alpha=0.5 T=4 ->", round(acc(m), 4))
# TODO etudiant : comparer a la baseline (environ 0.8029) et a la distillation pure T=1 (0.8145), et commenter.
alpha=0.5 T=4 -> 0.8002
# Exercice 2 : pousser la temperature trop loin.
# Regle T=20 (tres chaud). L'exactitude doit chuter. Explique pourquoi le dark knowledge est noye.
m = train(seed_model(32, 32, 0), Xs, ys, seed=0, teacher=teacher, alpha=0.0, T=20.0)
print("alpha=0 T=20 ->", round(acc(m), 4))
# TODO etudiant : expliquer l'effet d'une T trop grande (la cible tend vers l'uniforme).
alpha=0 T=20 -> 0.7577
# Exercice 3 : diagnostiquer une distillation qui echoue.
# On ne donne ici que PEU d'etiquettes au maitre, qui est donc un mauvais maitre (sur-confiant ou faux).
# On distille quand meme un eleve. Que se passe-t-il ? Pourquoi la distillation n'aide pas ?
mauvais_maitre = seed_model(256, 128, 0)
train(mauvais_maitre, Xs[:100], ys[:100], epochs=5, seed=0, bs=64)   # maitre faible, 100 exemples seulement
print("mauvais maitre acc =", round(acc(mauvais_maitre), 4))
echec = train(seed_model(32, 32, 0), Xs, ys, seed=0, teacher=mauvais_maitre, alpha=0.0, T=1.0)
print("eleve distille par mauvais maitre ->", round(acc(echec), 4))
# TODO etudiant : diagnostiquer. (Indice : quelle connaissance un mauvais maitre transmet-il ?)
mauvais maitre acc = 0.3726
eleve distille par mauvais maitre -> 0.3725

Conclusion et transition

La distillation enseigne un compromis mesuré : un petit élève, entraîné sur les cibles molles d’un maître, atteint une exactitude au moins aussi bonne — et surtout une calibration meilleure — qu’un élève entraîné sur les seules étiquettes dures, pour une fraction des paramètres. La température règle le dosage du dark knowledge : trop basse, elle redonne les étiquettes ; trop haute, elle noie l’information. Le facteur T² garantit que la distillation reste active à grande température.

Sur le protocole complet (5 folds × 4 graines, section 9), le verdict est INCONCLUSIVE au seuil strict du dépôt : le gain par exemple est sans ambiguïté (test de Diebold–Mariano p < 0.001, perte CE moyenne 0.63 → 0.58) et présent dans ~85-90 % des tirages d’étiquettes, mais son amplitude sur l’exactitude et l’ECE dépend du tirage (edge 0.6σ et 1.4σ, sous la barre des 2σ). À ce budget de 1500 étiquettes, la distillation améliore clairement chaque exemple ; affirmer qu’elle améliore l’exactitude du modèle exigerait un budget d’étiquettes plus grand — c’est la limite honnête de cette démonstration.

La suite naturelle est de faire l’économie des étiquettes elles-mêmes : sur des exemples non étiquetés, les cibles molles du maître servent de pseudo-étiquettes (distillation semi-supervisée). Et pour un vrai grand modèle, la même mécanique est ce que la série GenAI/FineTuning utilise sous d’autres noms — cf. la distinction de la section 10.

Retour au sommet