把一张正常图片加入人眼几乎无法察觉的噪声,神经网络就可能以极高置信度给出错误分类。这种被精心构造的输入称为对抗样本,而对抗训练正是针对这一现象的正则化手段。它的思路并不复杂:既然模型会因微小扰动而犯错,那就把这些扰动直接放进训练集,让模型在训练阶段学会抵抗它们。与普通随机噪声增强不同,对抗扰动不是随机撒在图像上,而是沿着损失函数的梯度方向专门生成,能够更准确地暴露模型决策边界上的薄弱位置。

对抗训练要解决的min-max问题
对抗训练的形式化目标可以写成一个min-max优化问题:
min θ E(x,y)[max δ∈S L(fθ(x+δ), y)]
这里的θ表示模型参数,x是原始输入,y是真实标签,δ是对抗扰动,S是扰动的约束集合。内层的max负责在当前模型参数下寻找一个损失最大的扰动,也就是让模型预测错误最严重的对抗样本;外层的min则要求模型参数优化,使得即便输入被施加了这种最坏扰动,损失也能被控制在较低水平。这个结构决定了对抗训练不是简单地增加数据量,而是不断在模型最脆弱的位置做针对性修正。
通常S会选择L∞球,即限制每个像素点的最大偏移不超过ε。这样做的原因有两个:一是L∞约束容易投射到图像像素范围,计算相对简单;二是它能保证扰动在视觉上难以察觉。如果只使用普通的高斯噪声或随机裁剪,模型学到的可能只是在随机波动下保持稳定,而对抗训练要求模型在一个紧致的局部区域中输出不变,这种约束更严格,也更能刻画决策边界与数据流形之间的距离。
当ε设置得过大时,对抗训练会使模型倾向于记住强扰动样本,干净样本精度可能明显下降;当ε过小时,训练出的模型又难以防御实际攻击。因此在实际实现中,ε通常表示为图像像素范围的较小比例,例如将像素归一化到0到1后取8/255作为默认扰动半径。
FGSM与PGD:两种常用攻击生成方式
内层最大化问题的求解方式直接决定了对抗训练的效果和计算成本。FGSM是单步方法,它只沿着损失函数关于输入的梯度方向走一步:
def fgsm_attack(model, x, y, epsilon):
x.requires_grad_(True)
loss = nn.CrossEntropyLoss()(model(x), y)
grad = torch.autograd.grad(loss, x)[0]
delta = epsilon * grad.sign()
return x + delta
FGSM的优势在于速度极快,每一步训练只需要额外一次前向和反向传播。它假设损失函数在输入附近是近似线性的,因此梯度符号能够代表最坏扰动方向。但对于深度非线性网络来说,这个假设常常不成立,单步攻击容易低估真实的最坏损失,模型经过FGSM对抗训练后可能只对FGSM攻击鲁棒,在面对多步迭代攻击时仍然脆弱。
PGD则把内层求解扩展为多次小步迭代。它的核心是每一步都沿梯度方向移动,然后把扰动投影回允许范围内,确保最终对抗样本始终落在x附近:
def pgd_attack(model, x, y, epsilon, alpha, steps):
x_adv = x.clone().detach()
for _ in range(steps):
x_adv.requires_grad_(True)
loss = nn.CrossEntropyLoss()(model(x_adv), y)
grad = torch.autograd.grad(loss, x_adv)[0]
x_adv = x_adv.detach() + alpha * grad.sign()
delta = torch.clamp(x_adv - x, min=-epsilon, max=epsilon)
x_adv = torch.clamp(x + delta, min=0, max=1)
return x_adv
每次迭代的步长α通常设置为ε的1/4或更小,迭代步数常取10步。投影操作可以通过两层clamp实现:第一层保证扰动每个分量不超过±ε,第二层保证最终图像像素值仍然在合法范围内。PGD并不是保证找到全局最坏扰动,但在经验上它能够稳定逼近一个较强的局部扰动,因此使用PGD对抗训练得到的模型通常比FGSM训练具有更可靠的鲁棒性。
从训练信号的角度看,FGSM相当于用当前模型附近线性近似下的最坏点来更新参数,而PGD则尽可能去逼近真实的最坏点。虽然PGD训练开销显著增大,但它减少了模型利用虚假鲁棒性通过梯度掩蔽欺骗单步攻击的可能性。
PyTorch中的对抗训练实现
将PGD集成到标准训练循环中并不复杂,核心是在每一轮前向传播之前先把普通样本替换成对抗样本。以下是一个简化版的训练步骤:
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
adv_images = pgd_attack(model, images, labels,
epsilon=8.0 / 255,
alpha=2.0 / 255,
steps=10)
optimizer.zero_grad()
outputs = model(adv_images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
这里先调用pgd_attack生成对抗样本,再把对抗样本输入模型计算损失。注意pgd_attack内部多次调用模型并获取梯度,这些梯度仅用于构造扰动,不参与参数更新。训练循环中的optimizer.zero_grad放在攻击之后,可以避免攻击过程中产生的梯度污染模型参数的累积梯度。
在更完整的实现中,可以在每个epoch开始时随机初始化对抗样本,例如在原始样本上加入均匀分布的小扰动,再执行PGD迭代。这个随机初始化有助于防止多步攻击在每次训练中都收敛到相同的局部最坏点,从而提升模型的泛化防御能力。也可以混合一定比例的干净样本与对抗样本共同训练,或对干净样本和对抗样本分别计算损失后加权求和。
训练开销、精度权衡与调优实践
对抗训练最明显的代价是计算量。PGD每生成一个对抗样本都要执行steps次前向和反向传播,假设steps为10,训练时间大约会变为普通训练的10倍。对于图像分类任务,这会让原本数小时的训练延长到一天以上。缓解方法包括:只在部分训练阶段开启对抗训练、使用更小的steps进行预热、在分布式训练中把对抗样本生成分散到不同设备上,或采用更高效的近似攻击作为训练信号。
另一个需要接受的现实是干净精度与鲁棒精度之间的权衡。强化对抗训练后,模型在未扰动测试集上的准确率通常会下降几个百分点,但在对抗攻击下准确率会显著提升。这种权衡并不是绝对的,可以通过更宽的网络、标签平滑、MixUp或CutMix等数据增强来减轻。训练过程中应同时监控干净验证集和对抗验证集指标,当对抗精度开始下降时考虑早停或降低扰动半径。
评估模型鲁棒性时不要只用FGSM或训练时使用的相同攻击。因为有些模型会通过让梯度变得不连续或饱和来制造梯度掩蔽,使得基于梯度的攻击难以找到有效扰动,但面对CW攻击或自适应攻击时仍然脆弱。合理的方式是使用多步PGD、随机重启PGD,以及尽可能与威胁模型匹配的攻击参数进行测试。只有在这种多角度评估下仍能保持稳定表现的模型,才具备实际部署条件下的对抗鲁棒性。