Download scripts/eval_challenge.py from ManhQuangAI/dinov3-deepfake-detection: direct link, hf CLI and curl.
- Browser
- Download file 9.13 kB
-
https://huggingface.co/ManhQuangAI/dinov3-deepfake-detection/resolve/main/scripts/eval_challenge.py
- Command line
-
hf download hf://ManhQuangAI/dinov3-deepfake-detection/scripts/eval_challenge.py
-
curl -L -o eval_challenge.py https://huggingface.co/ManhQuangAI/dinov3-deepfake-detection/resolve/main/scripts/eval_challenge.py
9.13 kB
| """Đánh giá 2 backbone DINOv3 (ViT-S/16 Plus vs ConvNeXt-Tiny, cùng ~28M params) | |
| trên bộ Test-Challenge (DeepFakeFace challenge). | |
| - Nhãn: đọc streaming từ Test-Challenge_poly.json (COCO-style). Ảnh "fake" nếu có | |
| ≥1 annotation category_id=1 (vùng bị chỉnh sửa), ngược lại là "real". | |
| - Protocol: linear probe (frozen backbone + LogisticRegression balanced) huấn luyện | |
| trên train_insight, đánh giá trên Test-Challenge — giống report. | |
| - RAM-safe: stream JSON theo chunk, 1 model tại 1 thời điểm, batch nhỏ, giải phóng | |
| model giữa 2 model. | |
| Chạy: | |
| .venv/bin/python scripts/eval_challenge.py --labels-only | |
| .venv/bin/python scripts/eval_challenge.py --limit-train 2000 --limit-test 400 # smoke test | |
| .venv/bin/python scripts/eval_challenge.py # full | |
| """ | |
| import argparse | |
| import csv | |
| import os | |
| import re | |
| import sys | |
| import time | |
| import numpy as np | |
| import torch | |
| from PIL import Image | |
| from sklearn.linear_model import LogisticRegression | |
| from sklearn.metrics import accuracy_score, confusion_matrix, f1_score, precision_score, recall_score, roc_auc_score | |
| from sklearn.pipeline import make_pipeline | |
| from sklearn.preprocessing import StandardScaler | |
| from torchvision import transforms | |
| from tqdm import tqdm | |
| sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) | |
| from src.models.dinov3_vit import load_dinov3 | |
| from src.models.dinov3_convnext import load_dinov3_convnext | |
| IMG_SIZE = 256 | |
| MEAN = [0.485, 0.456, 0.406] | |
| STD = [0.229, 0.224, 0.225] | |
| CHUNK = 16 * 1024 * 1024 | |
| def build_transform(): | |
| return transforms.Compose([ | |
| transforms.Resize((IMG_SIZE, IMG_SIZE)), | |
| transforms.ToTensor(), | |
| transforms.Normalize(mean=MEAN, std=STD), | |
| ]) | |
| # --------------------------------------------------------------------------- | |
| # Labels from Test-Challenge_poly.json (streaming, RAM-safe) | |
| # --------------------------------------------------------------------------- | |
| def load_challenge_labels(json_path, img_dir): | |
| """Trả về list[(path, label)] cho mọi ảnh có trong img_dir. | |
| label = 1 (fake) nếu image_id có annotation category_id=1, ngược lại 0 (real). | |
| """ | |
| with open(json_path, "rb") as f: | |
| head = f.read(8_000_000) | |
| images_section = head[: head.find(b'"annotations"')] | |
| objs = re.findall( | |
| rb'\{\s*"id":\s*(\d+),\s*"file_name":\s*"([^"]+)",\s*"width":\s*\d+,\s*"height":\s*\d+\s*\}', | |
| images_section, | |
| ) | |
| basename_to_id = {os.path.basename(n.decode()): int(i) for i, n in objs} | |
| print(f" JSON images: {len(basename_to_id)}") | |
| start = head.find(b'"annotations"') | |
| pat = re.compile(rb'"image_id":\s*(\d+).*?"category_id":\s*(\d+)', re.S) | |
| fake_ids = set() | |
| with open(json_path, "rb") as f: | |
| f.seek(start) | |
| buf = b"" | |
| while True: | |
| c = f.read(CHUNK) | |
| if not c: | |
| break | |
| buf += c | |
| proc, buf = buf[:-1024], buf[-1024:] | |
| for m in pat.finditer(proc): | |
| if int(m.group(2)) == 1: | |
| fake_ids.add(int(m.group(1))) | |
| for m in pat.finditer(buf): | |
| if int(m.group(2)) == 1: | |
| fake_ids.add(int(m.group(1))) | |
| print(f" Fake image ids: {len(fake_ids)}") | |
| items = [] | |
| for fn in sorted(os.listdir(img_dir)): | |
| if not fn.lower().endswith((".jpg", ".jpeg", ".png")): | |
| continue | |
| iid = basename_to_id.get(fn) | |
| if iid is None: | |
| continue | |
| items.append((os.path.join(img_dir, fn), 1 if iid in fake_ids else 0)) | |
| return items | |
| def load_csv(csv_path): | |
| items = [] | |
| with open(csv_path, newline="") as f: | |
| reader = csv.reader(f) | |
| next(reader, None) | |
| for row in reader: | |
| if len(row) >= 2 and os.path.exists(row[0]): | |
| items.append((row[0], int(row[1]))) | |
| return items | |
| def extract_features(model, items, device, batch_size, desc): | |
| model.to(device).eval() | |
| tf = build_transform() | |
| feats, labels = [], [] | |
| for i in tqdm(range(0, len(items), batch_size), desc=desc, unit="batch"): | |
| batch = items[i : i + batch_size] | |
| imgs = [tf(Image.open(p).convert("RGB")) for p, _ in batch] | |
| x = torch.stack(imgs).to(device) | |
| f = model(x) | |
| feats.append(f.cpu().numpy()) | |
| labels.extend(l for _, l in batch) | |
| return np.vstack(feats), np.array(labels) | |
| def evaluate(X_train, y_train, X_test, y_test, seed=42): | |
| clf = make_pipeline(StandardScaler(), LogisticRegression(class_weight="balanced", max_iter=2000, random_state=seed)) | |
| clf.fit(X_train, y_train) | |
| y_pred = clf.predict(X_test) | |
| y_prob = clf.predict_proba(X_test)[:, 1] | |
| cm = confusion_matrix(y_test, y_pred, labels=[0, 1]) | |
| return { | |
| "accuracy": float(accuracy_score(y_test, y_pred)), | |
| "precision": float(precision_score(y_test, y_pred, zero_division=0)), | |
| "recall": float(recall_score(y_test, y_pred, zero_division=0)), | |
| "f1": float(f1_score(y_test, y_pred, zero_division=0)), | |
| "roc_auc": float(roc_auc_score(y_test, y_prob)), | |
| "detection_rate_fake": float(recall_score(y_test, y_pred, pos_label=1, zero_division=0)), | |
| "confusion_matrix": cm.tolist(), | |
| "n_real": int((y_test == 0).sum()), | |
| "n_fake": int((y_test == 1).sum()), | |
| "real_correct": int(((y_pred == 0) & (y_test == 0)).sum()), | |
| } | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--json", default="data/Test-Challenge_poly.json") | |
| ap.add_argument("--img-dir", default="data/Test-Challenge") | |
| ap.add_argument("--train-csv", default="data/splits/train_insight.csv") | |
| ap.add_argument("--vit-model", default="models/dinov3-vits16plus-pretrain-lvd1689m/model-3.safetensors") | |
| ap.add_argument("--cnn-model", default="models/dinov3_next_cnn/model-2.safetensors") | |
| ap.add_argument("--output", default="outputs/results/challenge_report.json") | |
| ap.add_argument("--labels-only", action="store_true") | |
| ap.add_argument("--limit-train", type=int, default=0) | |
| ap.add_argument("--limit-test", type=int, default=0) | |
| ap.add_argument("--batch-size", type=int, default=32) | |
| ap.add_argument("--device", default="auto", choices=["auto", "cuda", "mps", "cpu"]) | |
| args = ap.parse_args() | |
| device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" if args.device == "auto" else args.device | |
| print(f"Device: {device}") | |
| test_items = load_challenge_labels(args.json, args.img_dir) | |
| real = sum(1 for _, l in test_items if l == 0) | |
| fake = sum(1 for _, l in test_items if l == 1) | |
| print(f"Test-Challenge available: {len(test_items)} (real={real}, fake={fake})") | |
| if args.labels_only: | |
| return | |
| train_items = load_csv(args.train_csv) | |
| if args.limit_train: | |
| r = [t for t in train_items if t[1] == 0][: args.limit_train // 2] | |
| fk = [t for t in train_items if t[1] == 1][: args.limit_train // 2] | |
| train_items = r + fk | |
| if args.limit_test: | |
| test_items = test_items[: args.limit_test] | |
| print(f"Train: {len(train_items)} | Test: {len(test_items)}") | |
| models = [ | |
| {"name": "DINOv3 ViT-S/16 Plus", "key": "vit", "path": args.vit_model, | |
| "loader": load_dinov3, "kw": {"img_size": IMG_SIZE}}, | |
| {"name": "DINOv3 ConvNeXt-Tiny", "key": "cnn", "path": args.cnn_model, | |
| "loader": load_dinov3_convnext, "kw": {}}, | |
| ] | |
| report = {"config": {"device": device, "batch_size": args.batch_size, | |
| "train_samples": len(train_items), "test_samples": len(test_items)}, | |
| "test_class_balance": {"real": real, "fake": fake}, "models": {}} | |
| for cfg in models: | |
| print(f"\n→ {cfg['name']}") | |
| model = cfg["loader"](cfg["path"], **cfg["kw"]) | |
| n_params = sum(p.numel() for p in model.parameters()) / 1e6 | |
| t0 = time.time() | |
| Xtr, ytr = extract_features(model, train_items, device, args.batch_size, f" Train features ({cfg['key']})") | |
| Xte, yte = extract_features(model, test_items, device, args.batch_size, f" Test features ({cfg['key']})") | |
| dt = time.time() - t0 | |
| m = evaluate(Xtr, ytr, Xte, yte) | |
| print(f" Params={n_params:.1f}M | Time={dt:.0f}s | Acc={m['accuracy']:.4f} " | |
| f"| DetRate(fake)={m['detection_rate_fake']:.4f} | AUC={m['roc_auc']:.4f} " | |
| f"| real correct={m['real_correct']}/{m['n_real']}") | |
| report["models"][cfg["key"]] = {"name": cfg["name"], "params_M": round(n_params, 1), | |
| "extract_time_s": round(dt, 1), "metrics": m} | |
| del model | |
| import gc | |
| gc.collect() | |
| if device == "mps": | |
| torch.mps.empty_cache() | |
| elif device == "cuda": | |
| torch.cuda.empty_cache() | |
| os.makedirs(os.path.dirname(args.output), exist_ok=True) | |
| with open(args.output, "w") as f: | |
| import json | |
| json.dump(report, f, indent=2, ensure_ascii=False) | |
| print(f"\nSaved: {args.output}") | |
| if __name__ == "__main__": | |
| main() | |