对抗训练的核心是在梯度方向对输入加扰动,需在loss.backward()后、optimizer.step()前手动实现;FGSM用sign(grad)单步扰动,PGD则多步迭代并投影,关键在detach、clamp和梯度重置。

对抗训练的核心是“在梯度方向加扰动”,不是简单加噪声
PyTorch 本身不内置对抗训练循环,必须手动在 backward() 后、optimizer.step() 前插入扰动更新。关键点在于:扰动要基于当前 batch 的梯度(即 loss.backward() 后的 model.parameters().grad),且只作用于输入(x),而非模型参数。
常见错误是把扰动加在模型权重上,或在 forward() 前就固定噪声——这等价于数据增强,不是对抗训练。
- 正确时机:在
loss.backward()之后、optimizer.step()之前 - 扰动方向:用
torch.sign(grad_x)(FGSM)或归一化梯度(PGD) - 扰动范围:通常控制在
epsilon=0.031(对应 [0,1] 归一化图像的 8/255) - 注意:输入
x必须是requires_grad=True,否则无法计算grad_x
FGSM 实现只需 4 行关键代码,但需绕过两个陷阱
FGSM 是最简对抗训练变体,但新手常卡在 x 的梯度计算和原图范围恢复上。以下是在标准训练循环中插入的最小可行片段:
x_adv = x.detach().clone() x_adv.requires_grad_(True) loss = criterion(model(x_adv), y) loss.backward() x_adv = x_adv + epsilon * x_adv.grad.sign() x_adv = torch.clamp(x_adv, 0, 1) # 必须裁剪,否则像素越界
两个易错点:
立即学习“Python免费学习笔记(深入)”;
-
x.detach().clone()不可省略:直接用x.clone()会继承原始计算图,导致backward()报错 -
torch.clamp()必须放在扰动后:否则对抗样本可能含负值或 >1,后续 loss 计算失真
PGD 需要多步迭代,但每步都得重新 detach 和 clamp
PGD 比 FGSM 更强,本质是带投影的多步 FGSM。核心是:每步都要重置 x_adv 的梯度依赖,否则计算图爆炸;且每次更新后都必须裁剪到合法范围并投影回 epsilon-ball。
典型实现(K=7 步):
x_adv = x.detach().clone()
for _ in range(K):
x_adv.requires_grad_(True)
loss = criterion(model(x_adv), y)
loss.backward()
with torch.no_grad():
x_adv = x_adv + alpha * x_adv.grad.sign()
x_adv = torch.max(torch.min(x_adv, x + epsilon), x - epsilon)
x_adv = torch.clamp(x_adv, 0, 1)
x_adv = x_adv.detach() # 关键:断开计算图
注意点:
-
alpha通常取epsilon / 4,太大会跳过局部极值,太小收敛慢 -
torch.max/min投影确保x_adv始终在x ± epsilon内,不是只靠clamp(0,1) - 每轮末尾的
detach()是必须的,否则下一轮requires_grad_(True)会连上旧图
对抗训练显著拖慢训练,但可避免重复 forward
标准做法是:每个 batch 做一次 clean loss + 一次 adv loss(两遍 forward)。但若用 PGD,K=7 就意味着 7 次 forward + backward,速度暴跌。
折中方案(推荐):
- 训练时只用 1–2 步 PGD(即 “fast PGD”),效果接近 full PGD 且速度可控
- 不要在验证阶段做对抗训练(验证时只跑 clean acc),否则 eval 速度也崩
- 如果显存吃紧,可复用
x_adv的中间特征(如用torch.no_grad()提前缓存 clean forward 的 feature map),但需重写 model 结构,复杂度上升
真正难的不是写这几行代码,而是调参:epsilon 太小没效果,太大让模型学不到干净样本;PGD 步数和学习率也得配合调整。这些没法靠模板解决,只能靠 loss 曲线和 robust acc 的实际下降趋势来判断。


















