DecoyFace / build_rssd.py
weathon's picture
Clean DecoyFace implementation and checkpoints
35ec68e verified
Raw History Blame Contribute Delete
4.05 kB
"""RSSD: train the proxy inversion network Phi, then estimate the
reconstruction-sensitive subspace from the gradient covariance (paper Sec. IV-B).
Outputs checkpoints/rssd.pt with P_rec, P_N (C x C), U_rec, eigenvalues, and the
trained proxy checkpoint checkpoints/proxy_unet.pt.
"""
import argparse
import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader
import wandb
from models import load_ir50, ClientEncoder, UNetInverter
from data import MS1MDataset
from common import ADAFACE_CKPT, CKPT, CFG, DEVICE
PROXY_IDS = 5000 # identity folders used to train the proxy
PROXY_ITERS = 30000 # paper: 30k iterations
BATCH = 128
COV_SAMPLES = 20000 # samples used to estimate the gradient covariance
def infinite(loader):
while True:
for batch in loader:
yield batch
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--ms1m_root", required=True)
args = ap.parse_args()
CKPT.mkdir(exist_ok=True)
dev = DEVICE
wandb.init(project="decoyface-repro", name="rssd", mode="online", config=CFG)
ir50 = load_ir50(ADAFACE_CKPT, dev)
Fc = ClientEncoder(ir50).to(dev).eval()
for p in Fc.parameters():
p.requires_grad_(False)
ds = MS1MDataset(args.ms1m_root, PROXY_IDS)
loader = DataLoader(ds, batch_size=BATCH, shuffle=True, num_workers=8,
drop_last=True, pin_memory=True, persistent_workers=True)
it = infinite(loader)
proxy = UNetInverter().to(dev)
opt = torch.optim.Adam(proxy.parameters(), lr=1e-3)
proxy_path = CKPT / "proxy_unet.pt"
resume_path = CKPT / "proxy_unet_resume.pt"
start = 0
if resume_path.exists():
ck = torch.load(resume_path, map_location=dev)
proxy.load_state_dict(ck["model"])
opt.load_state_dict(ck["opt"])
start = ck["step"]
print(f"resuming proxy from step {start}", flush=True)
for step in range(start, PROXY_ITERS):
x, _ = next(it)
x = x.to(dev, non_blocking=True)
with torch.no_grad():
z = Fc(x)
rec = proxy(z)
loss = F.l1_loss(rec, x)
opt.zero_grad(set_to_none=True)
loss.backward()
opt.step()
if step % 50 == 0:
wandb.log({"proxy/l1": loss.item()}, step=step)
if step % 2000 == 0:
print(f"proxy step {step} l1 {loss.item():.4f}", flush=True)
torch.save({"step": step, "model": proxy.state_dict(),
"opt": opt.state_dict()}, resume_path)
torch.save(proxy.state_dict(), proxy_path)
# ---- gradient covariance over the channel-averaged reconstruction grad ----
proxy.eval()
gbar = []
seen = 0
for x, _ in DataLoader(ds, batch_size=64, shuffle=True, num_workers=8):
x = x.to(dev)
z = Fc(x).detach().clone().requires_grad_(True)
loss = F.l1_loss(proxy(z), x)
g = torch.autograd.grad(loss, z)[0] # (B,C,H,W)
gbar.append(g.mean(dim=(2, 3)).detach().cpu()) # (B,C)
seen += x.shape[0]
if seen >= COV_SAMPLES:
break
gbar = torch.cat(gbar, 0) # (N,C)
mu = gbar.mean(0, keepdim=True)
gc = gbar - mu
sigma = (gc.T @ gc) / (gbar.shape[0] - 1) # (C,C)
evals, evecs = torch.linalg.eigh(sigma) # ascending
order = torch.argsort(evals, descending=True)
evals, evecs = evals[order], evecs[:, order]
U_rec = evecs[:, :CFG["r"]] # (C,r)
P_rec = U_rec @ U_rec.T
P_N = torch.eye(CFG["C"]) - P_rec
torch.save({"P_rec": P_rec, "P_N": P_N, "U_rec": U_rec,
"evals": evals, "sigma": sigma}, CKPT / "rssd.pt")
ev = evals / evals.sum()
print("top-8 eigenvalue energy:", ev[:8].tolist(), flush=True)
print(f"RSS energy captured by top-{CFG['r']}: "
f"{ev[:CFG['r']].sum().item():.4f}", flush=True)
wandb.log({"rssd/rss_energy": ev[:CFG["r"]].sum().item()})
wandb.finish()
if __name__ == "__main__":
main()