Clean DecoyFace implementation and checkpoints
Browse files- README.md +70 -0
- build_bank.py +63 -0
- build_rssd.py +114 -0
- checkpoints/adaface_ir50_ms1mv2.backbone.pth +3 -0
- checkpoints/bank.pt +3 -0
- checkpoints/rssd.pt +3 -0
- common.py +29 -0
- data.py +35 -0
- decoyface.py +114 -0
- models.py +182 -0
- train_defender.py +111 -0
README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: pytorch
|
| 3 |
+
tags:
|
| 4 |
+
- face-recognition
|
| 5 |
+
- privacy
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
# DecoyFace
|
| 9 |
+
|
| 10 |
+
Implementation of DecoyFace split face recognition privacy protection:
|
| 11 |
+
client-side Decoy-Guided Coherent Mixing (DGCM) and the server-side Authorized Canonicalization Module (ACM),
|
| 12 |
+
together with the scripts that build the Reconstruction-Sensitive Subspace Decomposition (RSSD) statistics and train the server backbone.
|
| 13 |
+
|
| 14 |
+
The client encoder `F_c` is AdaFace IR-50 (MS1MV2) stem + stage 1 (output 64x56x56);
|
| 15 |
+
the server backbone `F_s` is the rest of the IR-50.
|
| 16 |
+
|
| 17 |
+
## Files
|
| 18 |
+
|
| 19 |
+
| File | Content |
|
| 20 |
+
| --- | --- |
|
| 21 |
+
| `decoyface.py` | `DecoyProtector`: `protect(z_A, z_B)` (client, DGCM) and `server_recover(z_t)` (server, ACM) |
|
| 22 |
+
| `models.py` | IR-50, its split into `ClientEncoder` (F_c) / `ServerBackbone` (F_s), and the proxy inversion U-Net used by RSSD |
|
| 23 |
+
| `common.py` | DecoyFace hyper-parameters `CFG`, fixed channel permutation, checkpoint paths |
|
| 24 |
+
| `data.py` | MS1M identity-folder dataset |
|
| 25 |
+
| `build_rssd.py` | Trains the proxy inversion U-Net, estimates the reconstruction-sensitive subspace -> `checkpoints/rssd.pt` |
|
| 26 |
+
| `build_bank.py` | Builds the auxiliary complementary bank and `mu_rec` -> `checkpoints/bank.pt` |
|
| 27 |
+
| `train_defender.py` | Trains F_s + ArcFace head on canonicalized protected features -> `checkpoints/defender.pt` |
|
| 28 |
+
|
| 29 |
+
## Checkpoints
|
| 30 |
+
|
| 31 |
+
| File | Content |
|
| 32 |
+
| --- | --- |
|
| 33 |
+
| `checkpoints/adaface_ir50_ms1mv2.backbone.pth` | AdaFace IR-50 MS1MV2 backbone state dict (from https://github.com/mk-minchul/AdaFace) |
|
| 34 |
+
| `checkpoints/rssd.pt` | `P_rec`, `P_N` (64x64), `U_rec`, `evals`, `sigma` |
|
| 35 |
+
| `checkpoints/bank.pt` | `aux_bank` (5000x64x56x56, fp16), `mu_rec` (64x56x56), `perm` (64) |
|
| 36 |
+
|
| 37 |
+
`proxy_unet.pt` and `defender.pt` are not included.
|
| 38 |
+
|
| 39 |
+
## Usage
|
| 40 |
+
|
| 41 |
+
```python
|
| 42 |
+
import torch
|
| 43 |
+
from common import ADAFACE_CKPT, CKPT, CFG
|
| 44 |
+
from models import load_ir50, ClientEncoder
|
| 45 |
+
from decoyface import DecoyProtector
|
| 46 |
+
|
| 47 |
+
Fc = ClientEncoder(load_ir50(ADAFACE_CKPT, "cuda")).cuda().eval()
|
| 48 |
+
rssd = torch.load(CKPT / "rssd.pt", map_location="cuda")
|
| 49 |
+
bank = torch.load(CKPT / "bank.pt", map_location="cuda")
|
| 50 |
+
protector = DecoyProtector(rssd["P_rec"], rssd["P_N"], bank["mu_rec"],
|
| 51 |
+
bank["aux_bank"].float(), bank["perm"], CFG, "cuda")
|
| 52 |
+
|
| 53 |
+
# x, decoy: (B,3,112,112) RGB in [0,1], ArcFace-aligned; decoy is a different identity
|
| 54 |
+
with torch.no_grad():
|
| 55 |
+
z_t = protector.protect(Fc(x), Fc(decoy)) # transmitted by the client
|
| 56 |
+
z_can = protector.server_recover(z_t) # fed to F_s on the server
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
## Rebuilding
|
| 60 |
+
|
| 61 |
+
MS1M root: identity folders named by integer, each holding 112x112 aligned `.jpg` faces.
|
| 62 |
+
Identities used: proxy U-Net first 5000, auxiliary bank 50000-60000, defender first 30000 (`--num_ids`).
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
python build_rssd.py --ms1m_root /path/to/ms1m
|
| 66 |
+
python build_bank.py --ms1m_root /path/to/ms1m
|
| 67 |
+
python train_defender.py --ms1m_root /path/to/ms1m
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
Scripts log to Weights & Biases (project `decoyface-repro`).
|
build_bank.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build the auxiliary complementary bank D_N = {P_N F_c(y_j)} and the fixed
|
| 2 |
+
reconstruction-sensitive anchor mu_rec = mean_j P_rec F_c(y_j) (paper Eq. 5, 17).
|
| 3 |
+
|
| 4 |
+
Auxiliary samples are drawn from a dedicated MS1M identity range disjoint from
|
| 5 |
+
the proxy / defender training identities, mirroring the paper's use of a
|
| 6 |
+
separate auxiliary set.
|
| 7 |
+
|
| 8 |
+
Outputs checkpoints/bank.pt with aux_bank (M,C,56,56, fp16), mu_rec (C,56,56)
|
| 9 |
+
and the fixed channel-shuffle permutation perm (C,).
|
| 10 |
+
"""
|
| 11 |
+
import os
|
| 12 |
+
import glob
|
| 13 |
+
import argparse
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
from models import load_ir50, ClientEncoder
|
| 18 |
+
from decoyface import proj
|
| 19 |
+
from data import load_rgb01
|
| 20 |
+
from common import ADAFACE_CKPT, CKPT, CFG, DEVICE, fixed_perm
|
| 21 |
+
|
| 22 |
+
AUX_ID_LO, AUX_ID_HI = 50000, 60000 # dedicated auxiliary identity range
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def main():
|
| 26 |
+
ap = argparse.ArgumentParser()
|
| 27 |
+
ap.add_argument("--ms1m_root", required=True)
|
| 28 |
+
args = ap.parse_args()
|
| 29 |
+
|
| 30 |
+
dev = DEVICE
|
| 31 |
+
ir50 = load_ir50(ADAFACE_CKPT, dev)
|
| 32 |
+
Fc = ClientEncoder(ir50).to(dev).eval()
|
| 33 |
+
rssd = torch.load(CKPT / "rssd.pt", map_location=dev)
|
| 34 |
+
P_rec, P_N = rssd["P_rec"].to(dev), rssd["P_N"].to(dev)
|
| 35 |
+
|
| 36 |
+
ids = sorted(os.listdir(args.ms1m_root), key=int)[AUX_ID_LO:AUX_ID_HI]
|
| 37 |
+
paths = []
|
| 38 |
+
for ident in ids:
|
| 39 |
+
paths.extend(sorted(glob.glob(f"{args.ms1m_root}/{ident}/*.jpg")))
|
| 40 |
+
assert len(paths) >= CFG["M"], f"aux pool {len(paths)} < M {CFG['M']}"
|
| 41 |
+
g = torch.Generator().manual_seed(7)
|
| 42 |
+
pick = torch.randperm(len(paths), generator=g)[:CFG["M"]]
|
| 43 |
+
paths = [paths[i] for i in pick.tolist()]
|
| 44 |
+
|
| 45 |
+
bank = torch.empty(CFG["M"], CFG["C"], 56, 56, dtype=torch.float16)
|
| 46 |
+
mu_rec = torch.zeros(CFG["C"], 56, 56, device=dev)
|
| 47 |
+
bs = 128
|
| 48 |
+
with torch.no_grad():
|
| 49 |
+
for i in range(0, len(paths), bs):
|
| 50 |
+
imgs = torch.stack([load_rgb01(p) for p in paths[i:i + bs]]).to(dev)
|
| 51 |
+
z = Fc(imgs)
|
| 52 |
+
bank[i:i + imgs.shape[0]] = proj(P_N, z).half().cpu()
|
| 53 |
+
mu_rec += proj(P_rec, z).sum(0)
|
| 54 |
+
mu_rec /= len(paths)
|
| 55 |
+
|
| 56 |
+
torch.save({"aux_bank": bank, "mu_rec": mu_rec.cpu(),
|
| 57 |
+
"perm": fixed_perm(CFG["C"])}, CKPT / "bank.pt")
|
| 58 |
+
print(f"bank {tuple(bank.shape)} mu_rec {tuple(mu_rec.shape)} "
|
| 59 |
+
f"from {len(paths)} aux samples", flush=True)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
main()
|
build_rssd.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""RSSD: train the proxy inversion network Phi, then estimate the
|
| 2 |
+
reconstruction-sensitive subspace from the gradient covariance (paper Sec. IV-B).
|
| 3 |
+
|
| 4 |
+
Outputs checkpoints/rssd.pt with P_rec, P_N (C x C), U_rec, eigenvalues, and the
|
| 5 |
+
trained proxy checkpoint checkpoints/proxy_unet.pt.
|
| 6 |
+
"""
|
| 7 |
+
import argparse
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
from torch.utils.data import DataLoader
|
| 12 |
+
import wandb
|
| 13 |
+
|
| 14 |
+
from models import load_ir50, ClientEncoder, UNetInverter
|
| 15 |
+
from data import MS1MDataset
|
| 16 |
+
from common import ADAFACE_CKPT, CKPT, CFG, DEVICE
|
| 17 |
+
|
| 18 |
+
PROXY_IDS = 5000 # identity folders used to train the proxy
|
| 19 |
+
PROXY_ITERS = 30000 # paper: 30k iterations
|
| 20 |
+
BATCH = 128
|
| 21 |
+
COV_SAMPLES = 20000 # samples used to estimate the gradient covariance
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def infinite(loader):
|
| 25 |
+
while True:
|
| 26 |
+
for batch in loader:
|
| 27 |
+
yield batch
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def main():
|
| 31 |
+
ap = argparse.ArgumentParser()
|
| 32 |
+
ap.add_argument("--ms1m_root", required=True)
|
| 33 |
+
args = ap.parse_args()
|
| 34 |
+
|
| 35 |
+
CKPT.mkdir(exist_ok=True)
|
| 36 |
+
dev = DEVICE
|
| 37 |
+
wandb.init(project="decoyface-repro", name="rssd", mode="online", config=CFG)
|
| 38 |
+
|
| 39 |
+
ir50 = load_ir50(ADAFACE_CKPT, dev)
|
| 40 |
+
Fc = ClientEncoder(ir50).to(dev).eval()
|
| 41 |
+
for p in Fc.parameters():
|
| 42 |
+
p.requires_grad_(False)
|
| 43 |
+
|
| 44 |
+
ds = MS1MDataset(args.ms1m_root, PROXY_IDS)
|
| 45 |
+
loader = DataLoader(ds, batch_size=BATCH, shuffle=True, num_workers=8,
|
| 46 |
+
drop_last=True, pin_memory=True, persistent_workers=True)
|
| 47 |
+
it = infinite(loader)
|
| 48 |
+
|
| 49 |
+
proxy = UNetInverter().to(dev)
|
| 50 |
+
opt = torch.optim.Adam(proxy.parameters(), lr=1e-3)
|
| 51 |
+
|
| 52 |
+
proxy_path = CKPT / "proxy_unet.pt"
|
| 53 |
+
resume_path = CKPT / "proxy_unet_resume.pt"
|
| 54 |
+
start = 0
|
| 55 |
+
if resume_path.exists():
|
| 56 |
+
ck = torch.load(resume_path, map_location=dev)
|
| 57 |
+
proxy.load_state_dict(ck["model"])
|
| 58 |
+
opt.load_state_dict(ck["opt"])
|
| 59 |
+
start = ck["step"]
|
| 60 |
+
print(f"resuming proxy from step {start}", flush=True)
|
| 61 |
+
for step in range(start, PROXY_ITERS):
|
| 62 |
+
x, _ = next(it)
|
| 63 |
+
x = x.to(dev, non_blocking=True)
|
| 64 |
+
with torch.no_grad():
|
| 65 |
+
z = Fc(x)
|
| 66 |
+
rec = proxy(z)
|
| 67 |
+
loss = F.l1_loss(rec, x)
|
| 68 |
+
opt.zero_grad(set_to_none=True)
|
| 69 |
+
loss.backward()
|
| 70 |
+
opt.step()
|
| 71 |
+
if step % 50 == 0:
|
| 72 |
+
wandb.log({"proxy/l1": loss.item()}, step=step)
|
| 73 |
+
if step % 2000 == 0:
|
| 74 |
+
print(f"proxy step {step} l1 {loss.item():.4f}", flush=True)
|
| 75 |
+
torch.save({"step": step, "model": proxy.state_dict(),
|
| 76 |
+
"opt": opt.state_dict()}, resume_path)
|
| 77 |
+
torch.save(proxy.state_dict(), proxy_path)
|
| 78 |
+
|
| 79 |
+
# ---- gradient covariance over the channel-averaged reconstruction grad ----
|
| 80 |
+
proxy.eval()
|
| 81 |
+
gbar = []
|
| 82 |
+
seen = 0
|
| 83 |
+
for x, _ in DataLoader(ds, batch_size=64, shuffle=True, num_workers=8):
|
| 84 |
+
x = x.to(dev)
|
| 85 |
+
z = Fc(x).detach().clone().requires_grad_(True)
|
| 86 |
+
loss = F.l1_loss(proxy(z), x)
|
| 87 |
+
g = torch.autograd.grad(loss, z)[0] # (B,C,H,W)
|
| 88 |
+
gbar.append(g.mean(dim=(2, 3)).detach().cpu()) # (B,C)
|
| 89 |
+
seen += x.shape[0]
|
| 90 |
+
if seen >= COV_SAMPLES:
|
| 91 |
+
break
|
| 92 |
+
gbar = torch.cat(gbar, 0) # (N,C)
|
| 93 |
+
mu = gbar.mean(0, keepdim=True)
|
| 94 |
+
gc = gbar - mu
|
| 95 |
+
sigma = (gc.T @ gc) / (gbar.shape[0] - 1) # (C,C)
|
| 96 |
+
evals, evecs = torch.linalg.eigh(sigma) # ascending
|
| 97 |
+
order = torch.argsort(evals, descending=True)
|
| 98 |
+
evals, evecs = evals[order], evecs[:, order]
|
| 99 |
+
U_rec = evecs[:, :CFG["r"]] # (C,r)
|
| 100 |
+
P_rec = U_rec @ U_rec.T
|
| 101 |
+
P_N = torch.eye(CFG["C"]) - P_rec
|
| 102 |
+
|
| 103 |
+
torch.save({"P_rec": P_rec, "P_N": P_N, "U_rec": U_rec,
|
| 104 |
+
"evals": evals, "sigma": sigma}, CKPT / "rssd.pt")
|
| 105 |
+
ev = evals / evals.sum()
|
| 106 |
+
print("top-8 eigenvalue energy:", ev[:8].tolist(), flush=True)
|
| 107 |
+
print(f"RSS energy captured by top-{CFG['r']}: "
|
| 108 |
+
f"{ev[:CFG['r']].sum().item():.4f}", flush=True)
|
| 109 |
+
wandb.log({"rssd/rss_energy": ev[:CFG["r"]].sum().item()})
|
| 110 |
+
wandb.finish()
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
if __name__ == "__main__":
|
| 114 |
+
main()
|
checkpoints/adaface_ir50_ms1mv2.backbone.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:321a032dd99ce7ffeab078c92436e0371e8d752d66994c48825ff296cac58490
|
| 3 |
+
size 174645926
|
checkpoints/bank.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ba04ddb79804abb619daaae9eec4ca5c51155347d266422bd367505336851c6f
|
| 3 |
+
size 2007845254
|
checkpoints/rssd.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9d8629eaaa70ff55684262ce4e1f6c3b2be982d5c6becb36a3021ae260ecd8e2
|
| 3 |
+
size 68024
|
common.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared config and paths for DecoyFace."""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
CKPT = Path(__file__).resolve().parent / "checkpoints"
|
| 7 |
+
ADAFACE_CKPT = CKPT / "adaface_ir50_ms1mv2.backbone.pth"
|
| 8 |
+
|
| 9 |
+
# DecoyFace hyper-parameters (paper Sec. V-B "Implementation Details").
|
| 10 |
+
CFG = {
|
| 11 |
+
"C": 64,
|
| 12 |
+
"r": 32, # RSS rank
|
| 13 |
+
"G": 16, # groups
|
| 14 |
+
"K": 3, # preserved true-sample groups
|
| 15 |
+
"M": 5000, # auxiliary complementary bank size
|
| 16 |
+
"sc_lo": 8, "sc_hi": 12, # coherent subset size s_c ~ U[8,12]
|
| 17 |
+
"L_lo": 2, "L_hi": 4, # shared source count L ~ U[2,4]
|
| 18 |
+
"t": 3, # random scale exponent k ~ U[-3,3]
|
| 19 |
+
"q": 0.95, # quantile
|
| 20 |
+
"tau_s": 0.8, # server target magnitude
|
| 21 |
+
"eps": 1e-6,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
DEVICE = "cuda:0"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def fixed_perm(C=64, seed=1234):
|
| 28 |
+
g = torch.Generator().manual_seed(seed)
|
| 29 |
+
return torch.randperm(C, generator=g)
|
data.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MS1M dataset: identity folders of 112x112 ArcFace-aligned faces."""
|
| 2 |
+
import os
|
| 3 |
+
import glob
|
| 4 |
+
|
| 5 |
+
import cv2
|
| 6 |
+
import torch
|
| 7 |
+
from torch.utils.data import Dataset
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def load_rgb01(path):
|
| 11 |
+
img = cv2.imread(path) # BGR uint8, 112x112
|
| 12 |
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 13 |
+
t = torch.from_numpy(img).float().permute(2, 0, 1) / 255.0
|
| 14 |
+
return t
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class MS1MDataset(Dataset):
|
| 18 |
+
"""Aligned MS1M faces. Uses `num_ids` identity folders (sorted
|
| 19 |
+
numerically) starting at `start` so different scripts see the same
|
| 20 |
+
deterministic split."""
|
| 21 |
+
|
| 22 |
+
def __init__(self, root, num_ids, start=0):
|
| 23 |
+
ids = sorted(os.listdir(root), key=int)[start:start + num_ids]
|
| 24 |
+
self.samples = []
|
| 25 |
+
for label, ident in enumerate(ids):
|
| 26 |
+
for f in sorted(glob.glob(f"{root}/{ident}/*.jpg")):
|
| 27 |
+
self.samples.append((f, label))
|
| 28 |
+
self.num_classes = len(ids)
|
| 29 |
+
|
| 30 |
+
def __len__(self):
|
| 31 |
+
return len(self.samples)
|
| 32 |
+
|
| 33 |
+
def __getitem__(self, i):
|
| 34 |
+
path, label = self.samples[i]
|
| 35 |
+
return load_rgb01(path), label
|
decoyface.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DecoyFace client-side protection (DGCM) and server-side recovery (ACM).
|
| 2 |
+
|
| 3 |
+
All projections act on the channel dimension of a C x H x W feature at every
|
| 4 |
+
spatial location. C = 64 (F_c output), r = 32, G = 16 groups of size 4.
|
| 5 |
+
|
| 6 |
+
The RSSD statistics (P_rec, P_N, mu_rec), the fixed channel-shuffle permutation
|
| 7 |
+
and the auxiliary complementary bank D_N are built once (see build_rssd.py /
|
| 8 |
+
build_bank.py) and loaded into DecoyProtector.
|
| 9 |
+
"""
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def proj(P, z):
|
| 15 |
+
"""Apply channel-space projection P (C,C) to feature z (B,C,H,W)."""
|
| 16 |
+
b, c, h, w = z.shape
|
| 17 |
+
out = P @ z.reshape(b, c, h * w)
|
| 18 |
+
return out.reshape(b, c, h, w)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def group_shuffle(z, perm, G):
|
| 22 |
+
"""CSG: permute channels by `perm`, then split into G contiguous groups.
|
| 23 |
+
z (B,C,H,W) -> (B,G,c_g,H,W)."""
|
| 24 |
+
b, c, h, w = z.shape
|
| 25 |
+
z = z[:, perm]
|
| 26 |
+
return z.reshape(b, G, c // G, h, w)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def group_unshuffle(zg, inv_perm):
|
| 30 |
+
"""Inverse CSG: (B,G,c_g,H,W) -> (B,C,H,W) in the original channel order."""
|
| 31 |
+
b, G, cg, h, w = zg.shape
|
| 32 |
+
z = zg.reshape(b, G * cg, h, w)
|
| 33 |
+
return z[:, inv_perm]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def group_instance_norm(zg, eps=1e-5):
|
| 37 |
+
"""InstanceNorm applied to every group's channels over the spatial dims."""
|
| 38 |
+
b, G, cg, h, w = zg.shape
|
| 39 |
+
x = zg.reshape(b * G, cg, h, w)
|
| 40 |
+
x = F.instance_norm(x, eps=eps)
|
| 41 |
+
return x.reshape(b, G, cg, h, w)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class DecoyProtector:
|
| 45 |
+
def __init__(self, P_rec, P_N, mu_rec, aux_bank, perm, cfg, device):
|
| 46 |
+
self.device = device
|
| 47 |
+
self.P_rec = P_rec.to(device)
|
| 48 |
+
self.P_N = P_N.to(device)
|
| 49 |
+
self.mu_rec = mu_rec.to(device) # (C,H,W)
|
| 50 |
+
self.perm = perm.to(device) # (C,)
|
| 51 |
+
self.inv_perm = torch.argsort(perm).to(device)
|
| 52 |
+
self.cfg = cfg
|
| 53 |
+
self.G = cfg["G"]
|
| 54 |
+
self.K = cfg["K"]
|
| 55 |
+
self.M = aux_bank.shape[0]
|
| 56 |
+
# Pre-group and pre-normalize the auxiliary complementary bank once.
|
| 57 |
+
# aux_bank holds n_j = P_N F_c(y_j); grouped+IN gives the decoy groups.
|
| 58 |
+
ag = group_shuffle(aux_bank.to(device), self.perm, self.G)
|
| 59 |
+
self.aux_grouped_norm = group_instance_norm(ag) # (M,G,c_g,H,W)
|
| 60 |
+
|
| 61 |
+
def _sample_routing(self, B):
|
| 62 |
+
"""Per-sample mask m (B,G) and decoy source indices src (B,G)."""
|
| 63 |
+
G, K, M, cfg = self.G, self.K, self.M, self.cfg
|
| 64 |
+
mask = torch.zeros(B, G, dtype=torch.bool)
|
| 65 |
+
src = torch.zeros(B, G, dtype=torch.long)
|
| 66 |
+
for b in range(B):
|
| 67 |
+
keep = torch.randperm(G)[:K]
|
| 68 |
+
mask[b, keep] = True
|
| 69 |
+
g0 = torch.nonzero(~mask[b], as_tuple=False).flatten()
|
| 70 |
+
sc = int(torch.randint(cfg["sc_lo"], cfg["sc_hi"] + 1, (1,)))
|
| 71 |
+
sc = min(sc, g0.numel())
|
| 72 |
+
L = int(torch.randint(cfg["L_lo"], cfg["L_hi"] + 1, (1,)))
|
| 73 |
+
L = min(L, sc) if sc > 0 else L
|
| 74 |
+
perm_g0 = g0[torch.randperm(g0.numel())]
|
| 75 |
+
coherent = perm_g0[:sc]
|
| 76 |
+
independent = perm_g0[sc:]
|
| 77 |
+
shared_pool = torch.randperm(M)[:L]
|
| 78 |
+
for g in coherent.tolist():
|
| 79 |
+
src[b, g] = shared_pool[torch.randint(L, (1,))]
|
| 80 |
+
for g in independent.tolist():
|
| 81 |
+
src[b, g] = torch.randint(M, (1,))
|
| 82 |
+
return mask.to(self.device), src.to(self.device)
|
| 83 |
+
|
| 84 |
+
def protect(self, z_A, z_B):
|
| 85 |
+
"""Client-side DGCM. z_A, z_B: (B,C,H,W). Returns transmitted z_t."""
|
| 86 |
+
B = z_A.shape[0]
|
| 87 |
+
r_B = proj(self.P_rec, z_B)
|
| 88 |
+
n_A = proj(self.P_N, z_A)
|
| 89 |
+
|
| 90 |
+
uA = group_instance_norm(group_shuffle(n_A, self.perm, self.G))
|
| 91 |
+
mask, src = self._sample_routing(B)
|
| 92 |
+
ar = torch.arange(self.G, device=self.device)
|
| 93 |
+
decoy = self.aux_grouped_norm[src, ar] # (B,G,c_g,H,W)
|
| 94 |
+
m = mask[:, :, None, None, None]
|
| 95 |
+
mixed = torch.where(m, uA, decoy)
|
| 96 |
+
n_tilde = group_unshuffle(mixed, self.inv_perm)
|
| 97 |
+
|
| 98 |
+
z_tilde = n_tilde + r_B
|
| 99 |
+
k = torch.randint(-self.cfg["t"], self.cfg["t"] + 1, (B, 1, 1, 1),
|
| 100 |
+
device=self.device)
|
| 101 |
+
z_t = (2.0 ** k.float()) * z_tilde
|
| 102 |
+
return z_t
|
| 103 |
+
|
| 104 |
+
def server_recover(self, z_t):
|
| 105 |
+
"""Server-side ACM. z_t: (B,C,H,W). Returns canonicalized z_can."""
|
| 106 |
+
z_s = proj(self.P_N, z_t)
|
| 107 |
+
zg = group_shuffle(z_s, self.perm, self.G) # (B,G,c_g,H,W)
|
| 108 |
+
b, G, cg, h, w = zg.shape
|
| 109 |
+
flat = zg.reshape(b, G, cg * h * w).abs()
|
| 110 |
+
q = torch.quantile(flat, self.cfg["q"], dim=2) # (B,G)
|
| 111 |
+
scale = self.cfg["tau_s"] / (q + self.cfg["eps"])
|
| 112 |
+
zg = zg * scale[:, :, None, None, None]
|
| 113 |
+
n_cal = group_unshuffle(zg, self.inv_perm)
|
| 114 |
+
return self.mu_rec[None] + n_cal
|
models.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Backbone (IR-50) split into F_c / F_s, and the U-Net feature-inversion network.
|
| 2 |
+
|
| 3 |
+
The IR-50 definition follows face.evoLVe / AdaFace exactly so the pretrained
|
| 4 |
+
AdaFace MS1Mv2 checkpoint loads without surgery. The client encoder F_c is the
|
| 5 |
+
stem plus the first residual stage, whose output is 64x56x56 -- the intermediate
|
| 6 |
+
feature used throughout DecoyFace (paper Sec. V-B). F_s is the remainder.
|
| 7 |
+
"""
|
| 8 |
+
from collections import namedtuple
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn as nn
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# ----------------------------- IR-50 backbone ------------------------------ #
|
| 15 |
+
class Flatten(nn.Module):
|
| 16 |
+
def forward(self, x):
|
| 17 |
+
return x.view(x.size(0), -1)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class BasicBlockIR(nn.Module):
|
| 21 |
+
def __init__(self, in_channel, depth, stride):
|
| 22 |
+
super().__init__()
|
| 23 |
+
if in_channel == depth:
|
| 24 |
+
self.shortcut_layer = nn.MaxPool2d(1, stride)
|
| 25 |
+
else:
|
| 26 |
+
self.shortcut_layer = nn.Sequential(
|
| 27 |
+
nn.Conv2d(in_channel, depth, (1, 1), stride, bias=False),
|
| 28 |
+
nn.BatchNorm2d(depth))
|
| 29 |
+
self.res_layer = nn.Sequential(
|
| 30 |
+
nn.BatchNorm2d(in_channel),
|
| 31 |
+
nn.Conv2d(in_channel, depth, (3, 3), (1, 1), 1, bias=False),
|
| 32 |
+
nn.BatchNorm2d(depth),
|
| 33 |
+
nn.PReLU(depth),
|
| 34 |
+
nn.Conv2d(depth, depth, (3, 3), stride, 1, bias=False),
|
| 35 |
+
nn.BatchNorm2d(depth))
|
| 36 |
+
|
| 37 |
+
def forward(self, x):
|
| 38 |
+
return self.res_layer(x) + self.shortcut_layer(x)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
Bottleneck = namedtuple('Block', ['in_channel', 'depth', 'stride'])
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def get_block(in_channel, depth, num_units, stride=2):
|
| 45 |
+
return [Bottleneck(in_channel, depth, stride)] + \
|
| 46 |
+
[Bottleneck(depth, depth, 1) for _ in range(num_units - 1)]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def get_blocks_ir50():
|
| 50 |
+
return [
|
| 51 |
+
get_block(64, 64, 3), # stage 1 -> 64 x 56 x 56
|
| 52 |
+
get_block(64, 128, 4), # stage 2 -> 128 x 28 x 28
|
| 53 |
+
get_block(128, 256, 14), # stage 3 -> 256 x 14 x 14
|
| 54 |
+
get_block(256, 512, 3), # stage 4 -> 512 x 7 x 7
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class IR50(nn.Module):
|
| 59 |
+
"""Full IR-50 recognizer (input 112x112 -> 512-d embedding)."""
|
| 60 |
+
|
| 61 |
+
def __init__(self):
|
| 62 |
+
super().__init__()
|
| 63 |
+
self.input_layer = nn.Sequential(
|
| 64 |
+
nn.Conv2d(3, 64, (3, 3), 1, 1, bias=False),
|
| 65 |
+
nn.BatchNorm2d(64), nn.PReLU(64))
|
| 66 |
+
modules = []
|
| 67 |
+
for block in get_blocks_ir50():
|
| 68 |
+
for b in block:
|
| 69 |
+
modules.append(BasicBlockIR(b.in_channel, b.depth, b.stride))
|
| 70 |
+
self.body = nn.Sequential(*modules)
|
| 71 |
+
self.output_layer = nn.Sequential(
|
| 72 |
+
nn.BatchNorm2d(512), nn.Dropout(0.4), Flatten(),
|
| 73 |
+
nn.Linear(512 * 7 * 7, 512), nn.BatchNorm1d(512, affine=False))
|
| 74 |
+
|
| 75 |
+
def forward(self, x):
|
| 76 |
+
x = self.input_layer(x)
|
| 77 |
+
x = self.body(x)
|
| 78 |
+
return self.output_layer(x)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# The first residual stage (stage 1) spans body[0:3]; splitting there yields the
|
| 82 |
+
# 64x56x56 intermediate feature the paper transmits.
|
| 83 |
+
SPLIT_INDEX = 3
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def adaface_preprocess(x_rgb01):
|
| 87 |
+
"""RGB image in [0,1] -> AdaFace input (BGR, normalized to [-1,1])."""
|
| 88 |
+
x_bgr = x_rgb01.flip(1) # RGB -> BGR on the channel dim
|
| 89 |
+
return (x_bgr - 0.5) / 0.5
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class ClientEncoder(nn.Module):
|
| 93 |
+
"""F_c: AdaFace stem + stage 1. Frozen. Consumes RGB [0,1], emits 64x56x56."""
|
| 94 |
+
|
| 95 |
+
def __init__(self, ir50: IR50):
|
| 96 |
+
super().__init__()
|
| 97 |
+
self.input_layer = ir50.input_layer
|
| 98 |
+
self.stage1 = ir50.body[:SPLIT_INDEX]
|
| 99 |
+
|
| 100 |
+
def forward(self, x_rgb01):
|
| 101 |
+
x = adaface_preprocess(x_rgb01)
|
| 102 |
+
x = self.input_layer(x)
|
| 103 |
+
return self.stage1(x)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class ServerBackbone(nn.Module):
|
| 107 |
+
"""F_s: AdaFace stage 2-4 + output layer. 64x56x56 -> 512-d embedding."""
|
| 108 |
+
|
| 109 |
+
def __init__(self, ir50: IR50):
|
| 110 |
+
super().__init__()
|
| 111 |
+
self.rest = ir50.body[SPLIT_INDEX:]
|
| 112 |
+
self.output_layer = ir50.output_layer
|
| 113 |
+
|
| 114 |
+
def forward(self, feat):
|
| 115 |
+
return self.output_layer(self.rest(feat))
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def load_ir50(ckpt_path, device):
|
| 119 |
+
net = IR50()
|
| 120 |
+
sd = torch.load(ckpt_path, map_location="cpu")
|
| 121 |
+
if isinstance(sd, dict) and "state_dict" in sd:
|
| 122 |
+
sd = sd["state_dict"]
|
| 123 |
+
missing, unexpected = net.load_state_dict(sd, strict=True)
|
| 124 |
+
net.eval().to(device)
|
| 125 |
+
return net
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# ------------------------------- U-Net ------------------------------------- #
|
| 129 |
+
class ConvBlock(nn.Module):
|
| 130 |
+
def __init__(self, cin, cout, norm="batch"):
|
| 131 |
+
super().__init__()
|
| 132 |
+
if norm == "batch":
|
| 133 |
+
norm1 = nn.BatchNorm2d(cout)
|
| 134 |
+
norm2 = nn.BatchNorm2d(cout)
|
| 135 |
+
elif norm == "instance":
|
| 136 |
+
norm1 = nn.InstanceNorm2d(cout)
|
| 137 |
+
norm2 = nn.InstanceNorm2d(cout)
|
| 138 |
+
else:
|
| 139 |
+
raise ValueError(norm)
|
| 140 |
+
self.net = nn.Sequential(
|
| 141 |
+
nn.Conv2d(cin, cout, 3, 1, 1, bias=False),
|
| 142 |
+
norm1, nn.LeakyReLU(0.2, inplace=True),
|
| 143 |
+
nn.Conv2d(cout, cout, 3, 1, 1, bias=False),
|
| 144 |
+
norm2, nn.LeakyReLU(0.2, inplace=True))
|
| 145 |
+
|
| 146 |
+
def forward(self, x):
|
| 147 |
+
return self.net(x)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class UNetInverter(nn.Module):
|
| 151 |
+
"""Feature-inversion U-Net: 64x56x56 feature -> 3x112x112 RGB image in [0,1].
|
| 152 |
+
|
| 153 |
+
Encoder-decoder with skip connections over the feature map, plus a final
|
| 154 |
+
upsampling stage to reach the 112x112 image resolution.
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
def __init__(self, in_ch=64, norm="batch"):
|
| 158 |
+
super().__init__()
|
| 159 |
+
self.enc0 = ConvBlock(in_ch, 64, norm) # 56
|
| 160 |
+
self.down1 = nn.MaxPool2d(2) # 28
|
| 161 |
+
self.enc1 = ConvBlock(64, 128, norm)
|
| 162 |
+
self.down2 = nn.MaxPool2d(2) # 14
|
| 163 |
+
self.enc2 = ConvBlock(128, 256, norm)
|
| 164 |
+
self.bott = ConvBlock(256, 256, norm)
|
| 165 |
+
|
| 166 |
+
self.up2 = nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False)
|
| 167 |
+
self.dec2 = ConvBlock(256 + 128, 128, norm) # 28
|
| 168 |
+
self.up1 = nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False)
|
| 169 |
+
self.dec1 = ConvBlock(128 + 64, 64, norm) # 56
|
| 170 |
+
self.up0 = nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False)
|
| 171 |
+
self.dec0 = ConvBlock(64, 32, norm) # 112
|
| 172 |
+
self.head = nn.Conv2d(32, 3, 3, 1, 1)
|
| 173 |
+
|
| 174 |
+
def forward(self, feat):
|
| 175 |
+
e0 = self.enc0(feat)
|
| 176 |
+
e1 = self.enc1(self.down1(e0))
|
| 177 |
+
e2 = self.enc2(self.down2(e1))
|
| 178 |
+
b = self.bott(e2)
|
| 179 |
+
d2 = self.dec2(torch.cat([self.up2(b), e1], 1))
|
| 180 |
+
d1 = self.dec1(torch.cat([self.up1(d2), e0], 1))
|
| 181 |
+
d0 = self.dec0(self.up0(d1))
|
| 182 |
+
return torch.sigmoid(self.head(d0))
|
train_defender.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Defender training (paper Sec. IV-E): F_c frozen; train F_s + ArcFace head on
|
| 2 |
+
the server-side canonicalized feature z_can with V=2 protected views and the
|
| 3 |
+
multi-view consistency loss. L = L_id + lambda_cons * L_cons.
|
| 4 |
+
|
| 5 |
+
Outputs checkpoints/defender.pt with the trained F_s and ArcFace head.
|
| 6 |
+
"""
|
| 7 |
+
import copy
|
| 8 |
+
import argparse
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn as nn
|
| 12 |
+
import torch.nn.functional as F
|
| 13 |
+
from torch.utils.data import DataLoader
|
| 14 |
+
import wandb
|
| 15 |
+
|
| 16 |
+
from models import load_ir50, ClientEncoder, ServerBackbone
|
| 17 |
+
from data import MS1MDataset
|
| 18 |
+
from decoyface import DecoyProtector
|
| 19 |
+
from common import ADAFACE_CKPT, CKPT, CFG, DEVICE
|
| 20 |
+
|
| 21 |
+
V = 2
|
| 22 |
+
LAMBDA_CONS = 0.1
|
| 23 |
+
BATCH = 256
|
| 24 |
+
EPOCHS = 15
|
| 25 |
+
LR = 0.1
|
| 26 |
+
DECAY_EPOCHS = [4, 8, 12]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class ArcFace(nn.Module):
|
| 30 |
+
def __init__(self, in_dim, n_cls, s=64.0, m=0.5):
|
| 31 |
+
super().__init__()
|
| 32 |
+
self.W = nn.Parameter(torch.empty(n_cls, in_dim))
|
| 33 |
+
nn.init.xavier_normal_(self.W)
|
| 34 |
+
self.s, self.m = s, m
|
| 35 |
+
|
| 36 |
+
def forward(self, emb, y):
|
| 37 |
+
emb = F.normalize(emb)
|
| 38 |
+
w = F.normalize(self.W)
|
| 39 |
+
cos = emb @ w.T
|
| 40 |
+
theta = torch.acos(cos.clamp(-1 + 1e-6, 1 - 1e-6))
|
| 41 |
+
target = torch.cos(theta + self.m)
|
| 42 |
+
onehot = F.one_hot(y, cos.shape[1]).float()
|
| 43 |
+
logits = self.s * (onehot * target + (1 - onehot) * cos)
|
| 44 |
+
return F.cross_entropy(logits, y)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def main():
|
| 48 |
+
ap = argparse.ArgumentParser()
|
| 49 |
+
ap.add_argument("--ms1m_root", required=True)
|
| 50 |
+
ap.add_argument("--num_ids", type=int, default=30000)
|
| 51 |
+
ap.add_argument("--epochs", type=int, default=EPOCHS)
|
| 52 |
+
args = ap.parse_args()
|
| 53 |
+
dev = DEVICE
|
| 54 |
+
|
| 55 |
+
ir50 = load_ir50(ADAFACE_CKPT, dev)
|
| 56 |
+
Fc = ClientEncoder(ir50).to(dev).eval()
|
| 57 |
+
for p in Fc.parameters():
|
| 58 |
+
p.requires_grad_(False)
|
| 59 |
+
Fs = ServerBackbone(copy.deepcopy(ir50)).to(dev).train()
|
| 60 |
+
rssd = torch.load(CKPT / "rssd.pt", map_location=dev)
|
| 61 |
+
bank = torch.load(CKPT / "bank.pt", map_location=dev)
|
| 62 |
+
protector = DecoyProtector(rssd["P_rec"], rssd["P_N"], bank["mu_rec"],
|
| 63 |
+
bank["aux_bank"].float(), bank["perm"], CFG, dev)
|
| 64 |
+
|
| 65 |
+
ds = MS1MDataset(args.ms1m_root, args.num_ids)
|
| 66 |
+
head = ArcFace(512, ds.num_classes).to(dev)
|
| 67 |
+
loader = DataLoader(ds, batch_size=BATCH, shuffle=True, num_workers=12,
|
| 68 |
+
drop_last=True, pin_memory=True, persistent_workers=True)
|
| 69 |
+
|
| 70 |
+
params = list(Fs.parameters()) + list(head.parameters())
|
| 71 |
+
opt = torch.optim.SGD(params, lr=LR, momentum=0.9, weight_decay=5e-4)
|
| 72 |
+
wandb.init(project="decoyface-repro", name="defender", mode="online",
|
| 73 |
+
config={**CFG, "num_ids": args.num_ids, "epochs": args.epochs})
|
| 74 |
+
|
| 75 |
+
step = 0
|
| 76 |
+
for epoch in range(args.epochs):
|
| 77 |
+
lr = LR * (0.1 ** sum(epoch >= d for d in DECAY_EPOCHS))
|
| 78 |
+
for g in opt.param_groups:
|
| 79 |
+
g["lr"] = lr
|
| 80 |
+
for x, y in loader:
|
| 81 |
+
x, y = x.to(dev, non_blocking=True), y.to(dev, non_blocking=True)
|
| 82 |
+
with torch.no_grad():
|
| 83 |
+
z = Fc(x)
|
| 84 |
+
embs = []
|
| 85 |
+
id_loss = 0.0
|
| 86 |
+
for _ in range(V):
|
| 87 |
+
z_B = z.roll(torch.randint(1, x.shape[0], (1,)).item(), 0)
|
| 88 |
+
z_can = protector.server_recover(protector.protect(z, z_B))
|
| 89 |
+
e = Fs(z_can)
|
| 90 |
+
id_loss = id_loss + head(e, y)
|
| 91 |
+
embs.append(F.normalize(e))
|
| 92 |
+
id_loss = id_loss / V
|
| 93 |
+
cons = ((embs[0] - embs[1]) ** 2).sum(1).mean()
|
| 94 |
+
loss = id_loss + LAMBDA_CONS * cons
|
| 95 |
+
opt.zero_grad(set_to_none=True)
|
| 96 |
+
loss.backward()
|
| 97 |
+
opt.step()
|
| 98 |
+
if step % 100 == 0:
|
| 99 |
+
wandb.log({"def/loss": loss.item(), "def/id": id_loss.item(),
|
| 100 |
+
"def/cons": cons.item(), "def/lr": lr}, step=step)
|
| 101 |
+
if step % 1000 == 0:
|
| 102 |
+
print(f"ep{epoch} step{step} loss{loss.item():.3f} "
|
| 103 |
+
f"id{id_loss.item():.3f} cons{cons.item():.4f}", flush=True)
|
| 104 |
+
step += 1
|
| 105 |
+
torch.save({"Fs": Fs.state_dict(), "head": head.state_dict()},
|
| 106 |
+
CKPT / "defender.pt")
|
| 107 |
+
wandb.finish()
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
if __name__ == "__main__":
|
| 111 |
+
main()
|