对抗样本就像一次精心构造的哈希碰撞——两张在人眼看来一模一样的图片,模型却输出天差地别("熊猫"→"长臂猿")。攻击者不是随机试,而是像逆向一个哈希函数,精确算出"往哪个方向推几个像素"能翻转分类。关键认知:这不是模型没训好,而是高维空间的结构性弱点。
2013 年 Szegedy 等人发现一个反常现象:给一张分类正确的图加上人眼不可见的微小扰动,SOTA 网络就以高置信度分错。更诡异的是——这些扰动不是随机噪声,而是精心定向的。为什么会这样?
Goodfellow 2014 的线性假说(linear hypothesis)给了最简洁的解释:神经网络在局部近似线性。盯住一个 logit 的计算 w·(x+η) = w·x + w·η。攻击者取最坏扰动 η = ε·sign(w)(沿权重符号方向,每维都朝抬高 logit 的方向推),代入得到扰动带来的变化 w·η = ε·‖w‖₁。这里 ε 是每个像素的改动上限(小到人眼不可见),‖w‖₁ 是权重绝对值之和。核心在于 ‖w‖₁ 随维度 n 线性增长:
一张 224×224×3 的图有约 15 万维。每维只动 ε=0.007,累积的 logit 变化却是"十几万个小量之和"——足以把决策推过分类边界。脆弱性不是缺陷,是高维线性的代价。
import numpy as np # 演示"线性放大":为什么每维只动 ε 却能翻转输出 n = 150000 # 输入维度(≈ 一张彩色图的像素数) w = np.random.randn(n) # 某个 logit 对输入的权重 eps = 0.007 # 每维扰动上限,人眼不可见 eta = eps * np.sign(w) # 最坏扰动:沿权重符号方向 delta_logit = w @ eta # logit 的变化量 = ε·‖w‖₁ print(f"单维扰动: {eps}") # 0.007 print(f"logit 变化: {delta_logit:.0f}") # 上百——随维度线性增长 # 结论:高维下"不可见的扰动"累积成"输出的巨变"
训练时你对权重做梯度下降来降低 loss;攻击时你冻结权重,反过来对输入做梯度上升来抬高 loss。同一套自动微分机器,只是把矛头从"改模型"转向"改输入"。FGSM 是贪心一步,PGD 是带约束的迭代——像"一次性启发式" vs "带 clamp 的迭代优化器"。
FGSM(Fast Gradient Sign Method,快速梯度符号法):一步到位。x_adv = x + ε·sign(∇ₓL)。为什么取 sign 而不用梯度本身?因为攻击约束通常是 L∞(每个像素改动 ≤ ε);在这个约束下,沿每一维的梯度符号方向走满 ε,就是最大化 loss 上升的一步。快,但粗。
PGD(Projected Gradient Descent,投影梯度下降):把 FGSM 拆成很多小步(步长 α),每步之后投影回 ε-球——超出预算就 clip 回边界。这个"投影"就是你熟悉的边界钳制 / 限流:把值 clamp 到 [x−ε, x+ε]。PGD 是当前最强的一阶白盒攻击,也是评估鲁棒性的事实标准:一个防御若扛不住 PGD,基本就是"假鲁棒"。
import torch, torch.nn.functional as F def fgsm(model, x, y, eps): x = x.clone().requires_grad_(True) loss = F.cross_entropy(model(x), y) # 用真标签算 loss loss.backward() # 对"输入"求梯度,不是权重 return (x + eps * x.grad.sign()).detach() # 沿升 loss 方向走一步 def pgd(model, x, y, eps, alpha, steps): x_adv = x.clone().detach() for _ in range(steps): # 迭代版 FGSM x_adv.requires_grad_(True) loss = F.cross_entropy(model(x_adv), y) grad = torch.autograd.grad(loss, x_adv)[0] x_adv = x_adv.detach() + alpha * grad.sign() x_adv = torch.min(torch.max(x_adv, x - eps), x + eps) # 投影回 ε-球 x_adv = x_adv.clamp(0, 1) # 保持合法像素范围 return x_adv
普通训练喂"正常样本";对抗训练在每个 batch 里先用 PGD 现造最坏样本再学。本质是把混沌工程搬进训练循环:不等线上被攻击,训练时就持续注入最坏扰动,逼模型学会"在最坏情况下也答对"。
Madry 2017 把防御形式化成一个 min-max 鲁棒优化问题:
minθ 𝔼(x,y) [ max‖δ‖≤ε L(θ, x+δ, y) ]
读法:内层 maxδ = 在预算 ε 内找最坏扰动(用 PGD 近似求解);外层 minθ = 在这些最坏样本上做 SGD、更新权重。这个 saddle-point(鞍点)视角优雅地把攻击与防御统一在一个目标里。
代价是鲁棒性–准确率权衡:对抗训练后,模型在干净样本上的准确率往往下降。直觉是它被迫放弃那些"高预测力但脆弱"的非鲁棒特征,只保留稳的特征——更安全,但也更"迟钝"。
迁移性(transferability)是另一个关键现象:在模型 A 上造的对抗样本,常常也能骗过结构完全不同的模型 B。为什么?因为不同模型从同一份数据学到了相同的非鲁棒特征(Ilyas 2019)。后果很实际:攻击者不需要访问你的模型,在自己的替身模型上造样本就能打你——黑盒攻击由此成立,像一个跨实现通用的零日漏洞。
# 对抗训练循环:内层 max 由 PGD 近似,外层 min 由 SGD 完成 for x, y in loader: # ① 内层:先用 PGD 现造这个 batch 的"最坏输入" x_adv = pgd(model, x, y, eps=0.03, alpha=0.007, steps=10) # ② 外层:在最坏输入上做正常的梯度下降 loss = F.cross_entropy(model(x_adv), y) opt.zero_grad() loss.backward() opt.step() # 对应 min_θ E[ max_δ L(θ, x+δ, y) ] # 注意:训练成本 ≈ 普通训练 × (PGD steps+1),因为每步都要跑一遍 PGD
经验防御是"我们试过的攻击都没破"——像"没被黑过"不等于"安全";认证鲁棒性要的是可证明的保证。随机平滑的做法特别像quorum 读:不信单次预测,而是给输入加大量高斯噪声、采样上千次、取多数票;单个对抗性翻转撼动不了一个强共识。
Pain point:攻防军备竞赛无止境——每个经验防御几个月后就被更强的攻击打破。认证鲁棒性(certified robustness)跳出这个循环:给出一个半径 R,数学保证任何 L2 范数 ≤ R 的扰动都无法改变输出。
随机平滑(randomized smoothing,Cohen 2019)是最实用的方法:把原分类器 f 包成一个新分类器 g(x) = 在 x 上加噪 N(0, σ²I) 后 f 的多数票类别。定理给出认证半径:
R = σ · Φ⁻¹(pA)
符号含义:σ 是加的噪声强度;pA 是最高票类别在噪声下的得票率;Φ⁻¹ 是标准正态分布的逆累积分布函数(把"得票率"翻译成"标准差距离")。直觉:若加了很多噪声、模型还是压倒性地投 A,说明 A 的"票仓"很深,小扰动动不了它。得票率 pA 越接近 1,认证半径越大。
import torch from scipy.stats import norm # norm.ppf 就是 Φ⁻¹ def smoothed_predict(model, x, sigma=0.25, n=1000, k=10): votes = torch.zeros(k) # k 个类别的票箱 for _ in range(n): # 采样 n 次带噪预测(= quorum 读) noisy = x + sigma * torch.randn_like(x) votes[model(noisy).argmax()] += 1 # 每次投一票 top = votes.argmax() p_A = (votes[top] / n).item() # 最高票类的得票率 radius = sigma * norm.ppf(p_A) # 认证半径 R = σ·Φ⁻¹(p_A) return top.item(), radius # 在 L2 半径 R 内,保证不被翻转 # 单个对抗性像素翻转撼动不了一个 950:50 的强共识