savastakan's picture
Initial upload: code + 5 record checkpoints + fuse
f211247 verified
Raw
History Blame Contribute Delete
7.64 kB
#!/usr/bin/env python3
"""Knowledge distillation: ensemble teacher → single DINOv3-L student.
Loss: DIST (Huang et al., NeurIPS 2022) — Pearson correlation across class dim
+ classical KD (KL soft + CE hard).
Usage:
python distillation.py \
--teachers dinov3_vitb14:...ema.pt convnextv2_large:... siglip2_so400m:... \
--teacher-weights 0.45 0.30 0.25 \
--student-arch dinov3_vitb14 \
--manifest ... --output runs/h100/distilled_student/
"""
import os, sys, json, argparse, time
from pathlib import Path
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset
from torch.optim.swa_utils import AveragedModel
from PIL import Image
ROOT = Path("/arf/scratch/stakan/hitit-proje")
sys.path.insert(0, str(ROOT / 'hitit_ocr/src'))
from train_classification import build_backbone, HititClsDataset
def log(m): print(f"[{time.strftime('%H:%M:%S')}] {m}", flush=True)
def dist_loss(student_logits, teacher_probs, T=4.0):
"""DIST = KL(teacher || student) + Pearson correlation."""
# KL (soft)
s_logp = F.log_softmax(student_logits / T, dim=-1)
t_p = teacher_probs # already softmax (ensemble, mixed T)
t_p_T = F.softmax(torch.log(t_p.clamp_min(1e-12)) / T, dim=-1)
kd = F.kl_div(s_logp, t_p_T, reduction='batchmean') * (T * T)
# Inter-class correlation (Pearson over class dim, per-sample)
def _zscore(x):
return (x - x.mean(dim=-1, keepdim=True)) / (x.std(dim=-1, keepdim=True) + 1e-6)
s_z = _zscore(F.softmax(student_logits, dim=-1))
t_z = _zscore(t_p)
inter = 1.0 - (s_z * t_z).mean()
# Intra-sample correlation (Pearson across batch, per-class)
s_z2 = _zscore(F.softmax(student_logits, dim=-1).T)
t_z2 = _zscore(t_p.T)
intra = 1.0 - (s_z2 * t_z2).mean()
return kd + inter + intra
@torch.no_grad()
def ensemble_teacher_probs(teachers, teacher_ws, x, dtype):
probs = []
for m in teachers:
with torch.amp.autocast('cuda', dtype=dtype, enabled=True):
logits = m(x)
probs.append(F.softmax(logits.float(), dim=-1))
stacked = torch.stack(probs)
w = torch.tensor(teacher_ws, dtype=stacked.dtype, device=stacked.device).view(-1, 1, 1)
return (stacked * w).sum(0)
def load_model(arch, ckpt_path, n_cls, device):
model = build_backbone(arch, n_classes=n_cls).to(device)
ck = torch.load(ckpt_path, map_location='cpu', weights_only=False)
sd = ck['model']
sd = {k.replace('module.', '', 1): v for k, v in sd.items()}
sd = {k.replace('_orig_mod.', '', 1): v for k, v in sd.items()}
sd = {k: v for k, v in sd.items() if k != 'n_averaged'}
model.load_state_dict(sd, strict=False)
return model, ck
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--teachers', nargs='+', required=True, help='arch:path ...')
ap.add_argument('--teacher-weights', nargs='+', type=float, default=None)
ap.add_argument('--student-arch', default='dinov3_vitb14')
ap.add_argument('--manifest', default=str(ROOT / 'datasets/sources/hitit_local/manifest_classification.jsonl'))
ap.add_argument('--output', required=True)
ap.add_argument('--epochs', type=int, default=30)
ap.add_argument('--batch-size', type=int, default=64)
ap.add_argument('--lr', type=float, default=1e-4)
ap.add_argument('--T', type=float, default=4.0)
ap.add_argument('--hard-weight', type=float, default=0.3)
ap.add_argument('--val-fold', type=int, default=0)
args = ap.parse_args()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
dtype = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float32
first_arch, first_path = args.teachers[0].split(':', 1)
ck0 = torch.load(first_path, map_location='cpu', weights_only=False)
label_to_idx = ck0['label_to_idx']
n_cls = len(label_to_idx)
# Load teachers (frozen)
teachers = []
for t in args.teachers:
a, p = t.split(':', 1)
m, _ = load_model(a, p, n_cls, device)
for param in m.parameters(): param.requires_grad = False
m.eval()
teachers.append(m)
tw = args.teacher_weights or [1.0/len(teachers)]*len(teachers)
s = sum(tw); tw = [x/s for x in tw]
log(f"Teachers: {[t.split(':', 1)[0] for t in args.teachers]} weights={tw}")
# Student — initialize from strongest teacher of same arch if exists, else fresh
student, _ = None, None
for t in args.teachers:
a, p = t.split(':', 1)
if a == args.student_arch:
student, _ = load_model(a, p, n_cls, device)
log(f"Student init from teacher {a}:{p}"); break
if student is None:
student = build_backbone(args.student_arch, n_classes=n_cls).to(device)
log(f"Student fresh: {args.student_arch}")
# Data — use train split (not val)
import yaml
cfg = {'training': {'batch_size': args.batch_size}}
train_ds = HititClsDataset(args.manifest, cfg, is_train=True,
val_fold=args.val_fold, label_to_idx=label_to_idx)
val_ds = HititClsDataset(args.manifest, cfg, is_train=False,
val_fold=args.val_fold, label_to_idx=label_to_idx)
tr_dl = DataLoader(train_ds, batch_size=args.batch_size, shuffle=True,
num_workers=6, pin_memory=True, drop_last=True)
va_dl = DataLoader(val_ds, batch_size=args.batch_size, shuffle=False,
num_workers=4, pin_memory=True)
opt = torch.optim.AdamW(student.parameters(), lr=args.lr, weight_decay=0.05)
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=args.epochs)
ema = AveragedModel(student, avg_fn=lambda a, n, _: 0.9995*a + 0.0005*n)
out_dir = Path(args.output); out_dir.mkdir(parents=True, exist_ok=True)
best_acc = 0.0
for ep in range(args.epochs):
student.train()
tloss, nb = 0.0, 0
for x, y in tr_dl:
x = x.to(device, non_blocking=True); y = y.to(device, non_blocking=True)
t_probs = ensemble_teacher_probs(teachers, tw, x, dtype)
with torch.amp.autocast('cuda', dtype=dtype, enabled=True):
s_logits = student(x)
loss_kd = dist_loss(s_logits, t_probs, T=args.T)
loss_ce = F.cross_entropy(s_logits, y, label_smoothing=0.1)
loss = (1 - args.hard_weight) * loss_kd + args.hard_weight * loss_ce
opt.zero_grad(set_to_none=True); loss.backward()
torch.nn.utils.clip_grad_norm_(student.parameters(), 1.0)
opt.step()
ema.update_parameters(student)
tloss += loss.item(); nb += 1
sched.step()
# Validate EMA
ema.eval()
correct, total = 0, 0
with torch.no_grad():
for x, y in va_dl:
x = x.to(device, non_blocking=True); y = y.to(device, non_blocking=True)
with torch.amp.autocast('cuda', dtype=dtype, enabled=True):
logits = ema(x)
correct += (logits.argmax(-1) == y).sum().item(); total += y.size(0)
acc = correct / max(1, total)
log(f"ep {ep+1}/{args.epochs}: loss={tloss/max(1,nb):.4f} val_acc={acc:.4f}")
if acc > best_acc:
best_acc = acc
torch.save({'model': ema.state_dict(), 'label_to_idx': label_to_idx,
'arch': args.student_arch, 'acc': acc},
out_dir / 'best_ema.pt')
log(f"BEST student acc: {best_acc:.4f}{out_dir / 'best_ema.pt'}")
if __name__ == '__main__':
main()