Download code/train.py from sraivante/TinyFaceNet-1M: direct link, hf CLI and curl.
- Browser
- Download file 4.73 kB
-
https://huggingface.co/sraivante/TinyFaceNet-1M/resolve/main/code/train.py
- Command line
-
hf download hf://sraivante/TinyFaceNet-1M/code/train.py
-
curl -L -o train.py https://huggingface.co/sraivante/TinyFaceNet-1M/resolve/main/code/train.py
4.73 kB
| import os, numpy as np, torch, torch.nn as nn, torch.nn.functional as F, time, json, collections, sys | |
| from model import TinyFaceNet | |
| torch.manual_seed(0); np.random.seed(0); torch.set_num_threads(int(os.environ.get("THREADS","10"))) # leave cores for the desktop | |
| import os; d=np.load(os.environ.get("DATA","dataset/faces.npz")); X=d["imgs"]; T=d["embs"]; L=d["labels"]; S=d["src"]; DT=d["date"] | |
| names=sorted(set(L)); idx={n:i for i,n in enumerate(names)}; Y=np.array([idx[l] for l in L]) | |
| # date-based split: hold out Nov-2025 and May/Jun-2026 | |
| test=np.array([(x[:6] in ("202511","202605","202606")) for x in DT]); train=~test | |
| print("classes",names); print("train",train.sum(),"test",test.sum()) | |
| print("train per class",dict(collections.Counter(L[train]))); print("test per class",dict(collections.Counter(L[test]))) | |
| EPOCHS=int(sys.argv[1]) if len(sys.argv)>1 else 40; BS=128 | |
| def aug(x): # x: B,112,112,3 uint8 numpy -> float tensor B,3,112,112 | |
| x=torch.from_numpy(x).permute(0,3,1,2).float()/255. | |
| B=x.shape[0] | |
| flip=torch.rand(B)<0.5; x[flip]=x[flip].flip(-1) | |
| # random shift/scale via affine | |
| th=torch.zeros(B,2,3); sc=1+0.15*(torch.rand(B)*2-1); th[:,0,0]=sc; th[:,1,1]=sc; th[:,:,2]=0.12*(torch.rand(B,2)*2-1) | |
| x=F.grid_sample(x,F.affine_grid(th,x.shape,align_corners=False),padding_mode="border",align_corners=False) | |
| # color jitter | |
| x=x*(1+0.4*(torch.rand(B,1,1,1)-0.5))+0.2*(torch.rand(B,1,1,1)-0.5) | |
| # low-light sim: gamma darken (p=0.4), contrast crush (p=0.3), sensor noise (p=0.4) | |
| g=torch.rand(B)<0.4 | |
| if g.any(): x[g]=x[g].clamp(1e-3,1)**(1/(0.3+0.7*torch.rand(int(g.sum()),1,1,1)))*(0.5+0.5*torch.rand(int(g.sum()),1,1,1)) | |
| cc=torch.rand(B)<0.3 | |
| if cc.any(): mu=x[cc].mean((1,2,3),keepdim=True); x[cc]=(x[cc]-mu)*(0.4+0.6*torch.rand(int(cc.sum()),1,1,1))+mu | |
| nz=torch.rand(B)<0.4 | |
| if nz.any(): x[nz]=x[nz]+torch.randn_like(x[nz])*(0.08*torch.rand(int(nz.sum()),1,1,1)) | |
| gray=torch.rand(B)<0.15; x[gray]=x[gray].mean(1,keepdim=True).expand(-1,3,-1,-1) | |
| blur=torch.rand(B)<0.2 | |
| if blur.any(): x[blur]=F.avg_pool2d(x[blur],3,1,1) | |
| # cutout | |
| for i in torch.nonzero(torch.rand(B)<0.3).flatten(): | |
| cy,cx=np.random.randint(0,112,2); x[i,:,max(0,cy-16):cy+16,max(0,cx-16):cx+16]=0 | |
| return (x.clamp(0,1)-0.5)/0.5 | |
| def prep(x): return (torch.from_numpy(x).permute(0,3,1,2).float()/255.-0.5)/0.5 | |
| m=TinyFaceNet(n_cls=len(names)); opt=torch.optim.AdamW(m.parameters(),2e-3,weight_decay=5e-4) | |
| tr=np.nonzero(train)[0]; te=np.nonzero(test)[0]; Tt=torch.from_numpy(T) | |
| # ---- cap "other" in train: prefer jpg crops, fill with video frames (spread out) ---- | |
| OTHER_CAP=600 | |
| oth=tr[L[tr]=="other"]; fam=tr[L[tr]!="other"] | |
| oj=oth[np.char.startswith(S[oth],"jpg:")]; ov=oth[~np.char.startswith(S[oth],"jpg:")] | |
| need=max(0,OTHER_CAP-len(oj)); ov=ov[np.linspace(0,len(ov)-1,min(need,len(ov))).astype(int)] if need else ov[:0] | |
| tr=np.concatenate([fam,oj[:OTHER_CAP],ov]) | |
| print("train after other-cap",len(tr),dict(collections.Counter(L[tr]))) | |
| # ---- class-balanced sampling (sqrt inverse freq) ---- | |
| cnt=collections.Counter(Y[tr]); w=np.array([1/np.sqrt(cnt[y]) for y in Y[tr]]); w/=w.sum() | |
| steps=EPOCHS*(len(tr)//BS+1); sched=torch.optim.lr_scheduler.OneCycleLR(opt,2e-3,total_steps=steps,pct_start=0.1) | |
| def embed(ix): | |
| m.eval(); out=[] | |
| with torch.no_grad(): | |
| for i in range(0,len(ix),256): out.append(F.normalize(m(prep(X[ix[i:i+256]])),dim=1)) | |
| return torch.cat(out) | |
| def evaluate(): | |
| Etr=embed(tr); Ete=embed(te); ytr=torch.from_numpy(Y[tr]); yte=torch.from_numpy(Y[te]) | |
| C=torch.stack([F.normalize(Etr[ytr==c].mean(0),dim=0) if (ytr==c).any() else torch.zeros(Etr.shape[1]) for c in range(len(names))]) | |
| sim=Ete@C.T; pred=sim.argmax(1); acc=(pred==yte).float().mean().item() | |
| per={str(names[c]):round((pred[yte==c]==c).float().mean().item(),3) for c in range(len(names)) if (yte==c).any()} | |
| fam=yte!=idx["other"]; facc=(pred[fam]==yte[fam]).float().mean().item() if fam.any() else 0. | |
| return facc,per,C | |
| best=0 | |
| for ep in range(EPOCHS): | |
| m.train(); ep_ix=np.random.choice(tr,size=len(tr),replace=True,p=w); t0=time.time(); tl=0 | |
| for i in range(0,len(ep_ix),BS): | |
| b=ep_ix[i:i+BS]; x=aug(X[b]); y=torch.from_numpy(Y[b]); t=Tt[b] | |
| e=m(x); lc=F.cross_entropy(m.cls(e),y,label_smoothing=0.1); ld=1-F.cosine_similarity(m.distill(e),t).mean() | |
| loss=lc+2.0*ld; opt.zero_grad(); loss.backward(); opt.step(); sched.step(); tl+=loss.item()*len(b) | |
| acc,per,C=evaluate() | |
| print(f"ep{ep} loss={tl/len(tr):.3f} fam_acc={acc:.4f} {per} {time.time()-t0:.0f}s",flush=True) | |
| if acc>=best: best=acc; torch.save({"model":m.state_dict(),"names":names,"centroids":C},os.environ.get("OUT","dataset/best.pt")) | |
| print("BEST",best) | |