File size: 5,476 Bytes
e23cf4c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Tạo report Markdown kết quả theo TỪNG METHOD (40 method) từ eval JSON v3.

Đọc 2 file JSON của eval_identity_disjoint (vit + cnn) trên test_data_v3,
gộp thành bảng markdown. Vì 1 method có thể trải nhiều domain (cdc/ffc/oth),
report dùng trường `per_method_domain` để tách từng (method, domain).

Chạy:
  .venv/bin/python scripts/make_method_report_md.py \
      --vit outputs/eval/identity_disjoint_v3_vit.json \
      --cnn outputs/eval/identity_disjoint_v3_cnn.json \
      --output outputs/eval/report_40_methods_v3.md
"""
import argparse
import json
import os


def load(path):
    with open(path) as f:
        return json.load(f)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--vit", required=True)
    ap.add_argument("--cnn", required=True)
    ap.add_argument("--output", required=True)
    args = ap.parse_args()

    V = load(args.vit)
    C = load(args.cnn)

    # ---- 40 method, tách theo domain ----
    # keys "method/domain" (loại real/*)
    md_keys = sorted(k for k in V["per_method_domain"] if not k.startswith("real/"))
    rows = []
    for k in md_keys:
        m, d = k.split("/", 1)
        v = V["per_method_domain"][k]
        c = C["per_method_domain"].get(k, {})
        rows.append((m, d, v["n"], v["rate"], c.get("rate")))
    # gộp 1 method nhiều domain thành 1 khối liên tục, method sắp alphabet
    rows.sort(key=lambda r: (r[0].lower(), r[1]))

    # ---- real theo domain ----
    real_d = {}
    for d, v in V.get("per_domain", {}).items():
        real_d[d] = {"n": v.get("n"), "n_real": None, "vit": v.get("real_acc"),
                     "cnn": C.get("per_domain", {}).get(d, {}).get("real_acc")}
    # n_real từ per_method_domain real/{d}
    for k, v in V["per_method_domain"].items():
        if k.startswith("real/"):
            d = k.split("/", 1)[1]
            if d in real_d:
                real_d[d]["n_real"] = v["n"]

    L = []
    L.append("# Kết quả 40 method — DINOv3 ViT-S/16 vs ConvNeXt-Tiny")
    L.append("")
    L.append("- **Dataset:** `test_data_v3/` — cấu trúc `real/` + `<method>/fake/`")
    L.append("- **Protocol:** identity-disjoint (split seed 42, train_ratio 0.7, theo cột `identity`) — "
             "model test chỉ trên identity chưa từng thấy trong train")
    L.append("- **Probe:** frozen backbone (feature extractor) + LogisticRegression linear probe "
             "(class_weight=balanced)")
    L.append(f"- **Train/test:** {V.get('n_train'):,} / {V.get('n_test'):,} ảnh, "
             f"{V.get('n_identity_keys'):,} identity keys")
    L.append("")

    m = V["metrics"]; cm = C["metrics"]
    L.append("## Tổng quan")
    L.append("")
    L.append("| Metric | DINOv3 ViT-S/16 | ConvNeXt-Tiny |")
    L.append("|---|---|---|")
    for k, label in [("accuracy", "Accuracy"), ("precision", "Precision"),
                     ("recall", "Recall"), ("f1", "F1"), ("roc_auc", "AUC")]:
        L.append(f"| {label} | {m[k]:.4f} | {cm[k]:.4f} |")
    L.append(f"| Nhận đúng REAL (acc) | {V['real_acc']:.4f} (FPR {1-V['real_acc']:.1%}) | "
             f"{C['real_acc']:.4f} (FPR {1-C['real_acc']:.1%}) |")
    L.append(f"| Bắt FAKE (det) | {V['fake_detection']:.4f} | {C['fake_detection']:.4f} |")
    if V.get("paired_only"):
        p, cp = V["paired_only"], C.get("paired_only") or {}
        L.append(f"| Paired-only Acc | {p['accuracy']:.4f} | "
                 f"{cp.get('accuracy', float('nan')):.4f} |")
    L.append("")

    L.append("## Theo domain (test)")
    L.append("")
    L.append("| Domain | n | ViT acc | ViT real | ViT fake | CNN acc | CNN real | CNN fake |")
    L.append("|---|---|---|---|---|---|---|---|")
    for d in sorted(V.get("per_domain", {})):
        v = V["per_domain"][d]; c = C["per_domain"].get(d, {})
        L.append(f"| {d} | {v['n']} | {v.get('acc', float('nan')):.4f} | "
                 f"{v.get('real_acc', float('nan')):.4f} | {v.get('fake_det', float('nan')):.4f} | "
                 f"{c.get('acc', float('nan')):.4f} | {c.get('real_acc', float('nan')):.4f} | "
                 f"{c.get('fake_det', float('nan')):.4f} |")
    L.append("")

    L.append("## 40 method — detection rate (fake) theo (method, domain)")
    L.append("")
    L.append("Domain: **cdc**=Celeb-DF · **ffc**=FF++ · **efs**=tổng hợp · "
             "**oth**=không ghép · **fe**=expression. Một method có thể trải nhiều domain.")
    L.append("")
    L.append("| Method | domain | n | ViT det | CNN det |")
    L.append("|---|---|---:|---:|---:|")
    for m, d, n, vd, cd in rows:
        cd_s = f"{cd:.4f}" if cd is not None else "—"
        L.append(f"| {m} | {d} | {n:,} | {vd:.4f} | {cd_s} |")
    L.append("")

    L.append("## REAL theo domain")
    L.append("")
    L.append("| Domain | n real | ViT real acc | CNN real acc |")
    L.append("|---|---|---:|---:|")
    for d in sorted(real_d):
        r = real_d[d]
        if r["n_real"] is None or r["vit"] is None:
            continue  # domain không có ảnh real
        cnn_s = f"{r['cnn']:.4f}" if r["cnn"] is not None else "—"
        L.append(f"| {d} | {r['n_real']} | {r['vit']:.4f} | {cnn_s} |")
    L.append("")

    os.makedirs(os.path.dirname(args.output), exist_ok=True)
    with open(args.output, "w") as f:
        f.write("\n".join(L) + "\n")
    print(f"Saved: {args.output} ({len(md_keys)} dòng method/domain)")


if __name__ == "__main__":
    main()