Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 6,020 Bytes
cd24b4a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | """Retrain the deployed ReStraV checkpoint with the corrected (official) recipe.
The previous restrav.pth was trained on ImageNet-normalized frames but is *served* raw [0,1]
(registry video_norm = identity) -- a train/serve mismatch that collapsed it (AUROC ~0.50,
real_acc ~2%). This retrains the SynthScope `SynthScopeReStraVModel` head end-to-end on raw [0,1] frames,
so training and serving use the exact same DINOv2 features.
Key fidelity choices:
* Features come from the model's OWN ``_encode`` + ``_geometry_features`` (so they are
bit-identical to what the deployed forward computes, incl. the official [mean,min,max,var]
summary order), with frames decoded by the SAME serving pipeline (media.video_clip_tensor,
center window + BICUBIC, raw [0,1]).
* Official training recipe: standardize (BatchNorm1d running stats, train mode), MLP
21->64->32->1, BCEWithLogits, plain Adam lr 1e-3 (no weight decay), 10 epochs, batch 32.
* Save the FINAL-epoch state_dict (no val-loss checkpoint selection).
Run: conda run -n SynthScope python interface/scripts/retrain_restrav.py [--n 3000]
"""
import argparse
import glob
import random
import sys
import time
from pathlib import Path
import torch
import torch.nn as nn
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # interface/
sys.path.insert(0, str(Path(__file__).resolve().parents[2])) # repo root (config_utils)
from backend.registry import get_spec # noqa: E402
from backend import media # noqa: E402
from backend.engine import _build_model # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
TRAIN_ROOT = "/hddstore/ssalehi/SynthScope/restrav_data/train/genvideo"
OUT_DIR = Path("/hddstore/ssalehi/SynthScope/checkpoints/_train/restrav_fixed")
DEPLOY_LINK = Path("/hddstore/ssalehi/SynthScope/checkpoints/restrav.pth")
def build_model(spec):
from config_utils import compose_config
cfg = compose_config(spec.overrides)
model = _build_model(cfg).to(DEV).float()
model.eval() # frozen backbone stays in eval
return model
@torch.no_grad()
def extract_features(model, spec, items, tag):
X, y = [], []
t0 = time.time()
for i, (p, lab) in enumerate(items):
try:
clip = media.video_clip_tensor(
p, spec.num_frames, spec.fps, spec.frame_size,
window_center=spec.window_center, interp=spec.resize_interp,
) # [T,3,H,W] raw [0,1]
z = model._encode(clip.unsqueeze(0).to(DEV)) # [1,T,D]
feat = model._geometry_features(z)[0] # [21]
if torch.isfinite(feat).all():
X.append(feat.cpu()); y.append(lab)
except Exception as e:
if i < 5:
print(f" skip {Path(p).name}: {e}", flush=True)
if (i + 1) % 300 == 0:
print(f" [{tag}] {i+1}/{len(items)} ({(time.time()-t0)/(i+1):.2f}s/vid)", flush=True)
return torch.stack(X), torch.tensor(y, dtype=torch.float32)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--n", type=int, default=3000, help="videos per class")
ap.add_argument("--epochs", type=int, default=10)
args = ap.parse_args()
spec = get_spec("restrav")
model = build_model(spec)
print(f"model built on {DEV}; serving recipe: num_frames={spec.num_frames} "
f"window_center={spec.window_center} interp={spec.resize_interp} video_norm={spec.video_norm}", flush=True)
reals = sorted(glob.glob(TRAIN_ROOT + "/0_real/*.mp4"))
fakes = sorted(glob.glob(TRAIN_ROOT + "/1_fake/*.mp4"))
random.Random(0).shuffle(reals); random.Random(1).shuffle(fakes)
# SynthScope label convention: 0 = real, 1 = fake.
items = [(p, 0) for p in reals[:args.n]] + [(p, 1) for p in fakes[:args.n]]
random.Random(2).shuffle(items)
print(f"extracting features for {len(items)} videos (raw [0,1], model._encode)...", flush=True)
X, y = extract_features(model, spec, items, "train")
X = X.to(DEV); y = y.to(DEV)
print(f"features: {tuple(X.shape)} reals={(y==0).sum().item()} fakes={(y==1).sum().item()}", flush=True)
# --- train head (official recipe). feature_norm is BatchNorm1d(affine=False): no params,
# just running stats accumulated in train mode. classifier holds the trainable weights. ---
model.feature_norm.train(); model.classifier.train()
opt = torch.optim.Adam(model.classifier.parameters(), lr=1e-3) # wd=0 (plain Adam)
crit = nn.BCEWithLogitsLoss()
for ep in range(args.epochs):
perm = torch.randperm(len(X), device=DEV)
tot = 0.0
for i in range(0, len(X), 32):
b = perm[i:i+32]
opt.zero_grad()
logit = model.classifier(model.feature_norm(X[b])).squeeze(-1) # fake logit
loss = crit(logit, y[b])
loss.backward(); opt.step()
tot += loss.item() * len(b)
print(f" epoch {ep+1}/{args.epochs} loss={tot/len(X):.4f}", flush=True)
# --- save FINAL state_dict (matches what engine._load_checkpoint expects) ---
model.eval()
OUT_DIR.mkdir(parents=True, exist_ok=True)
out = OUT_DIR / "model_final.pth"
torch.save(model.state_dict(), out)
print(f"saved {out}", flush=True)
# quick in-sample sanity (real_acc / fake_acc on the train features)
with torch.no_grad():
p_fake = torch.sigmoid(model.classifier(model.feature_norm(X)).squeeze(-1))
pred = (p_fake > 0.5).float()
racc = (pred[y == 0] == 0).float().mean().item()
facc = (pred[y == 1] == 1).float().mean().item()
print(f"in-sample: real_acc={racc*100:.1f} fake_acc={facc*100:.1f}", flush=True)
# relink the deployed checkpoint
if DEPLOY_LINK.is_symlink() or DEPLOY_LINK.exists():
DEPLOY_LINK.unlink()
DEPLOY_LINK.symlink_to(out)
print(f"relinked {DEPLOY_LINK} -> {out}", flush=True)
if __name__ == "__main__":
main()
|