Download decoyface.py from weathon/DecoyFace: direct link, hf CLI and curl.
- Browser
- Download file 4.61 kB
-
https://huggingface.co/weathon/DecoyFace/resolve/main/decoyface.py
- Command line
-
hf download hf://weathon/DecoyFace/decoyface.py
-
curl -L -o decoyface.py https://huggingface.co/weathon/DecoyFace/resolve/main/decoyface.py
4.61 kB
| """DecoyFace client-side protection (DGCM) and server-side recovery (ACM). | |
| All projections act on the channel dimension of a C x H x W feature at every | |
| spatial location. C = 64 (F_c output), r = 32, G = 16 groups of size 4. | |
| The RSSD statistics (P_rec, P_N, mu_rec), the fixed channel-shuffle permutation | |
| and the auxiliary complementary bank D_N are built once (see build_rssd.py / | |
| build_bank.py) and loaded into DecoyProtector. | |
| """ | |
| import torch | |
| import torch.nn.functional as F | |
| def proj(P, z): | |
| """Apply channel-space projection P (C,C) to feature z (B,C,H,W).""" | |
| b, c, h, w = z.shape | |
| out = P @ z.reshape(b, c, h * w) | |
| return out.reshape(b, c, h, w) | |
| def group_shuffle(z, perm, G): | |
| """CSG: permute channels by `perm`, then split into G contiguous groups. | |
| z (B,C,H,W) -> (B,G,c_g,H,W).""" | |
| b, c, h, w = z.shape | |
| z = z[:, perm] | |
| return z.reshape(b, G, c // G, h, w) | |
| def group_unshuffle(zg, inv_perm): | |
| """Inverse CSG: (B,G,c_g,H,W) -> (B,C,H,W) in the original channel order.""" | |
| b, G, cg, h, w = zg.shape | |
| z = zg.reshape(b, G * cg, h, w) | |
| return z[:, inv_perm] | |
| def group_instance_norm(zg, eps=1e-5): | |
| """InstanceNorm applied to every group's channels over the spatial dims.""" | |
| b, G, cg, h, w = zg.shape | |
| x = zg.reshape(b * G, cg, h, w) | |
| x = F.instance_norm(x, eps=eps) | |
| return x.reshape(b, G, cg, h, w) | |
| class DecoyProtector: | |
| def __init__(self, P_rec, P_N, mu_rec, aux_bank, perm, cfg, device): | |
| self.device = device | |
| self.P_rec = P_rec.to(device) | |
| self.P_N = P_N.to(device) | |
| self.mu_rec = mu_rec.to(device) # (C,H,W) | |
| self.perm = perm.to(device) # (C,) | |
| self.inv_perm = torch.argsort(perm).to(device) | |
| self.cfg = cfg | |
| self.G = cfg["G"] | |
| self.K = cfg["K"] | |
| self.M = aux_bank.shape[0] | |
| # Pre-group and pre-normalize the auxiliary complementary bank once. | |
| # aux_bank holds n_j = P_N F_c(y_j); grouped+IN gives the decoy groups. | |
| ag = group_shuffle(aux_bank.to(device), self.perm, self.G) | |
| self.aux_grouped_norm = group_instance_norm(ag) # (M,G,c_g,H,W) | |
| def _sample_routing(self, B): | |
| """Per-sample mask m (B,G) and decoy source indices src (B,G).""" | |
| G, K, M, cfg = self.G, self.K, self.M, self.cfg | |
| mask = torch.zeros(B, G, dtype=torch.bool) | |
| src = torch.zeros(B, G, dtype=torch.long) | |
| for b in range(B): | |
| keep = torch.randperm(G)[:K] | |
| mask[b, keep] = True | |
| g0 = torch.nonzero(~mask[b], as_tuple=False).flatten() | |
| sc = int(torch.randint(cfg["sc_lo"], cfg["sc_hi"] + 1, (1,))) | |
| sc = min(sc, g0.numel()) | |
| L = int(torch.randint(cfg["L_lo"], cfg["L_hi"] + 1, (1,))) | |
| L = min(L, sc) if sc > 0 else L | |
| perm_g0 = g0[torch.randperm(g0.numel())] | |
| coherent = perm_g0[:sc] | |
| independent = perm_g0[sc:] | |
| shared_pool = torch.randperm(M)[:L] | |
| for g in coherent.tolist(): | |
| src[b, g] = shared_pool[torch.randint(L, (1,))] | |
| for g in independent.tolist(): | |
| src[b, g] = torch.randint(M, (1,)) | |
| return mask.to(self.device), src.to(self.device) | |
| def protect(self, z_A, z_B): | |
| """Client-side DGCM. z_A, z_B: (B,C,H,W). Returns transmitted z_t.""" | |
| B = z_A.shape[0] | |
| r_B = proj(self.P_rec, z_B) | |
| n_A = proj(self.P_N, z_A) | |
| uA = group_instance_norm(group_shuffle(n_A, self.perm, self.G)) | |
| mask, src = self._sample_routing(B) | |
| ar = torch.arange(self.G, device=self.device) | |
| decoy = self.aux_grouped_norm[src, ar] # (B,G,c_g,H,W) | |
| m = mask[:, :, None, None, None] | |
| mixed = torch.where(m, uA, decoy) | |
| n_tilde = group_unshuffle(mixed, self.inv_perm) | |
| z_tilde = n_tilde + r_B | |
| k = torch.randint(-self.cfg["t"], self.cfg["t"] + 1, (B, 1, 1, 1), | |
| device=self.device) | |
| z_t = (2.0 ** k.float()) * z_tilde | |
| return z_t | |
| def server_recover(self, z_t): | |
| """Server-side ACM. z_t: (B,C,H,W). Returns canonicalized z_can.""" | |
| z_s = proj(self.P_N, z_t) | |
| zg = group_shuffle(z_s, self.perm, self.G) # (B,G,c_g,H,W) | |
| b, G, cg, h, w = zg.shape | |
| flat = zg.reshape(b, G, cg * h * w).abs() | |
| q = torch.quantile(flat, self.cfg["q"], dim=2) # (B,G) | |
| scale = self.cfg["tau_s"] / (q + self.cfg["eps"]) | |
| zg = zg * scale[:, :, None, None, None] | |
| n_cal = group_unshuffle(zg, self.inv_perm) | |
| return self.mu_rec[None] + n_cal | |