File size: 8,409 Bytes
63f8a47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
"""Evaluate a trained V-PVP head on the AIGVDBench Open-Source split.

Computes per-generator AUC, mean-of-generators AUC (mga AUC), and overall
AUC on the test set. Each backbone is loaded once (frozen, fp16 autocast),
patch features ``z`` of shape ``(B, P, T', D)`` are extracted, the velocity
field ``v = z[:, :, 1:] - z[:, :, :-1]`` is computed, and the head produces
a scalar logit per video.

Usage:
    python -m src.eval_opensource \
        --backbone videomae_k400 \
        --backbone_weights <path-or-hub-id> \
        --head_ckpt    checkpoints/pvp_heads/videomae_k400_vpvp.pt \
        --data_root    <path-to-aigvdbench> \
        --num_frames   16 \
        --batch_size   32 \
        --out_json     results/videomae_open_source.json

Expected mga AUC on the Open-Source split with the released checkpoint
(20 generators, 3000 real + 60000 fake videos):

    videomae_k400     ->  0.9528   (V-PVP row of Table 1)
"""
import argparse
import json
import os
import sys
import time

import numpy as np
import torch
import torch.nn as nn
from sklearn.metrics import roc_auc_score
from torch.utils.data import DataLoader

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from backbones import BACKBONE_REGISTRY
from dataset import OpenSourceConfig, OpenSourceTest, collate
from pvp_head import PVPHead


# Frame count the released checkpoint was trained at.
DEFAULT_NUM_FRAMES = {
    "videomae_k400": 16,
}

# Dataset-side normalization applied by ``OpenSourceTest`` (mean 0.45 /
# std 0.225). The eval loop undoes it and re-applies the backbone's own
# statistics below; this is the exact preprocessing path the released
# checkpoint was trained and evaluated with.
DS_MEAN = (0.45, 0.45, 0.45)
DS_STD  = (0.225, 0.225, 0.225)


def renormalize(clips, ds_mean, ds_std, bb_mean, bb_std):
    """Undo dataset normalization, re-apply backbone's. clips: (B, C, T, H, W)."""
    x = clips * ds_std + ds_mean
    return (x - bb_mean) / bb_std


def compute_metrics(scores, labels, cats, threshold=0.5):
    """mga AUC = mean-of-generators AUC over the per-(real, generator) pools."""
    cats   = np.asarray(cats)
    labels = np.asarray(labels)
    scores = np.asarray(scores)
    real   = (labels == 0)
    preds  = (scores > threshold).astype(int)

    auc_per_gen, acc_per_gen = {}, {}
    for c in sorted(set(cats[~real].tolist())):
        gen_mask = (cats == c) & (labels == 1)
        if gen_mask.sum() < 2 or real.sum() < 2:
            continue
        y = np.r_[np.zeros(real.sum()), np.ones(gen_mask.sum())]
        s = np.r_[scores[real],          scores[gen_mask]]
        p = np.r_[preds[real],           preds[gen_mask]]
        auc_per_gen[c] = float(roc_auc_score(y, s))
        acc_per_gen[c] = float((p == y).mean())

    return {
        "auc_per_gen": auc_per_gen,
        "acc_per_gen": acc_per_gen,
        "mga_auc":     float(np.mean(list(auc_per_gen.values()))),
        "mga_acc":     float(np.mean(list(acc_per_gen.values()))),
        "overall_auc": float(roc_auc_score(labels, scores)),
        "overall_acc": float((preds == labels).mean()),
        "n_real":      int(real.sum()),
        "n_fake":      int((~real).sum()),
    }


@torch.no_grad()
def evaluate(backbone, head, loader, device, ds_norm, bb_norm):
    head.eval()
    backbone.eval()
    scores_all, labels_all, cats_all = [], [], []
    n_done = 0
    t0 = time.time()
    for clips, labels, cats in loader:
        clips = clips.to(device, non_blocking=True)
        x = renormalize(clips, *ds_norm, *bb_norm)
        with torch.autocast(device_type="cuda", dtype=torch.float16):
            z = backbone.extract_patch_features(x)
        z = z.to(torch.float32)
        if z.size(2) < 2:
            raise RuntimeError(f"backbone returned T'={z.size(2)} < 2; need >=2 frames")
        v = z[:, :, 1:, :] - z[:, :, :-1, :]
        logit = head(z, v)
        scores_all.append(torch.sigmoid(logit).cpu().numpy())
        labels_all.append(labels.numpy())
        cats_all.extend(cats)
        n_done += clips.size(0)
        if n_done % 1024 == 0:
            print(f"  [{n_done}] {n_done/(time.time()-t0):.1f} v/s", flush=True)
    return (np.concatenate(scores_all),
            np.concatenate(labels_all),
            np.array(cats_all))


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--backbone", required=True,
                    choices=list(BACKBONE_REGISTRY.keys()))
    ap.add_argument("--backbone_weights", required=True,
                    help="path (or HuggingFace repo id) of the K400 backbone weights")
    ap.add_argument("--head_ckpt", required=True,
                    help="path to the V-PVP head state_dict (.pt)")
    ap.add_argument("--data_root", required=True,
                    help="AIGVDBench root containing Real/ and OpenSource/ folders")
    ap.add_argument("--num_frames", type=int, default=0,
                    help="0 -> backbone default (16 for VideoMAE)")
    ap.add_argument("--batch_size", type=int, default=32)
    ap.add_argument("--num_workers", type=int, default=8)
    ap.add_argument("--threshold", type=float, default=0.5)
    ap.add_argument("--out_json", default="",
                    help="if set, write per-generator AUC + summary to this JSON")
    args = ap.parse_args()

    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    nf = args.num_frames or DEFAULT_NUM_FRAMES[args.backbone]
    print(f"backbone={args.backbone}  num_frames={nf}  bs={args.batch_size}", flush=True)
    print(f"backbone_weights={args.backbone_weights}", flush=True)
    print(f"head_ckpt={args.head_ckpt}", flush=True)

    # ---- backbone (frozen) ----
    bb_cls   = BACKBONE_REGISTRY[args.backbone]
    backbone = bb_cls(pretrained_model=args.backbone_weights,
                      num_frames=nf, img_size=224).to(device).eval()
    for p in backbone.parameters():
        p.requires_grad = False
    D = backbone.embed_dim
    print(f"  P={backbone.num_patches_spatial}  D={D}  "
          f"input_mean={backbone.input_mean}  input_std={backbone.input_std}", flush=True)

    # ---- head ----
    head = PVPHead("velocity_gated", D=D).to(device)
    sd = torch.load(args.head_ckpt, map_location=device)
    if isinstance(sd, dict) and "state_dict" in sd:
        sd = sd["state_dict"]
    head.load_state_dict(sd)
    head.eval()
    n_head = sum(p.numel() for p in head.parameters())
    print(f"  head params: {n_head:,}", flush=True)

    # ---- data ----
    cfg = OpenSourceConfig(data_root=args.data_root, num_frames=nf,
                           crop_size=224, max_per_category=0)
    ds  = OpenSourceTest(cfg)
    print(f"  test N={len(ds)}", flush=True)
    loader = DataLoader(ds, batch_size=args.batch_size, shuffle=False,
                        num_workers=args.num_workers, pin_memory=True,
                        collate_fn=collate)

    # ---- normalization tensors ----
    ds_mean = torch.tensor(DS_MEAN, device=device).view(1, 3, 1, 1, 1)
    ds_std  = torch.tensor(DS_STD,  device=device).view(1, 3, 1, 1, 1)
    bb_mean = torch.tensor(backbone.input_mean, device=device).view(1, 3, 1, 1, 1)
    bb_std  = torch.tensor(backbone.input_std,  device=device).view(1, 3, 1, 1, 1)

    # ---- run ----
    t0 = time.time()
    scores, labels, cats = evaluate(backbone, head, loader, device,
                                    (ds_mean, ds_std), (bb_mean, bb_std))
    elapsed = (time.time() - t0) / 60
    print(f"  inference done in {elapsed:.1f} min  ({len(scores)} videos)", flush=True)

    m = compute_metrics(scores, labels, cats, threshold=args.threshold)
    print(f"\nOpen-Source split results ({m['n_real']} real / {m['n_fake']} fake):")
    print(f"  mga AUC   = {m['mga_auc']:.4f}")
    print(f"  mga ACC   = {m['mga_acc']:.4f}")
    print(f"  overall AUC = {m['overall_auc']:.4f}")
    print(f"  overall ACC = {m['overall_acc']:.4f}")
    print("\nPer-generator AUC:")
    for c, a in m["auc_per_gen"].items():
        print(f"  {c:<32s} {a*100:6.2f}")

    if args.out_json:
        os.makedirs(os.path.dirname(args.out_json) or ".", exist_ok=True)
        out = {
            "backbone":   args.backbone,
            "num_frames": nf,
            "head_ckpt":  args.head_ckpt,
            **m,
        }
        with open(args.out_json, "w") as f:
            json.dump(out, f, indent=2)
        print(f"\nsaved {args.out_json}")


if __name__ == "__main__":
    main()