3、对抗防御基础:对抗训练、防御蒸馏、输入变换、梯度掩码、防御评估方法

聊到对抗防御,我得先跟你交个底——没有银弹。我在项目里踩过的坑,十有八九都是因为「以为一种方法就能搞定一切」。对抗防御说白了,就是给模型穿上防弹衣,但每件防弹衣都有它的弱点。今天咱们把这五种主流方法掰开揉碎了讲,你以后选方案时心里就有谱了。

3.1 对抗训练:最朴素的「以毒攻毒」

对抗训练的思路特别直白:把对抗样本混进训练集,让模型提前见识一下「坏人长什么样」。我最早接触这个思路时觉得有点笨,但后来发现——它确实管用。

具体做法分三步:

  1. 生成对抗样本:对每个正常样本 x,用 FGSM 或 PGD 生成对抗样本 x'
  2. 混合训练:把 (x', y) 和 (x, y) 一起喂给模型
  3. 迭代优化:每轮训练都重新生成对抗样本

核心公式:对抗训练的目标函数是 min_θ E[ max_δ L(f_θ(x+δ), y) ],其中 δ 是扰动,L 是损失函数。

代码实现其实不复杂,我贴一段我项目里用过的 PGD 对抗训练核心逻辑:

def adversarial_train_step(model, x, y, epsilon=0.3, alpha=0.01, steps=40):
    # 初始化扰动
    delta = torch.rand_like(x) * 2 * epsilon - epsilon
    delta.requires_grad = True
    
    for _ in range(steps):
        # 前向传播
        pred = model(x + delta)
        loss = F.cross_entropy(pred, y)
        loss.backward()
        
        # 梯度上升(找最坏扰动)
        grad = delta.grad.detach()
        delta = delta + alpha * torch.sign(grad)
        delta = torch.clamp(delta, -epsilon, epsilon)
        delta = torch.clamp(x + delta, 0, 1) - x
        delta.requires_grad = True
    
    # 用找到的最坏扰动更新模型
    pred = model(x + delta.detach())
    loss = F.cross_entropy(pred, y)
    loss.backward()
    optimizer.step()

我的经验:对抗训练最怕「过拟合到对抗样本」。我曾经在 CIFAR-10 上训了 200 轮,结果模型对 FGSM 很鲁棒,但对 PGD 完全没抵抗力。后来我改成每轮随机选择攻击方法(FGSM/PGD/DeepFool 轮着来),效果好了很多。

3.2 防御蒸馏:让模型「想清楚再说」

防御蒸馏这个名字听着玄乎,其实原理很简单——让模型的输出概率分布更「平滑」。你想想看,如果模型对某个类别打出 0.999 的概率,那稍微扰动一下就可能掉到 0.1。但如果输出是 0.6 呢?抗扰动能力就强多了。

具体做法分两阶段:

  1. 训练大模型:用高温 softmax(T > 1)训练一个教师网络
  2. 蒸馏到小模型:用教师网络的软标签(soft label)训练学生网络,同样用高温

为什么有效?因为高温 softmax 会把概率分布「摊平」,模型学到的决策边界更平滑。攻击者想找到梯度方向就更难了。

注意:防御蒸馏对白盒攻击效果有限。我记得有篇论文证明,如果攻击者知道蒸馏温度 T,可以直接模拟蒸馏过程来生成对抗样本。所以它更适合作为第一道防线,别指望它能挡住所有攻击。

3.3 输入变换:在「门口」就把坏人拦住

输入变换的思路是:不修改模型,而是在输入层做预处理。就像进小区要刷卡,输入变换就是给数据加一道安检。

常用的变换方法有:

方法 原理 优缺点
随机缩放/旋转 破坏对抗扰动的空间结构 简单有效,但可能影响正常识别
JPEG 压缩 高频扰动被压缩掉 对高频攻击有效,对低频攻击无效
中值滤波 用邻域中值替代像素值 能去除椒盐噪声型扰动
总方差最小化 平滑图像,去除异常像素 计算量大,可能模糊细节

我建议你组合使用多种变换。比如先做随机裁剪,再做 JPEG 压缩,最后做中值滤波。攻击者要同时绕过三道关卡,难度就大多了。

避坑指南:我曾经在一个人脸识别项目里只用 JPEG 压缩做防御,结果被一个简单的低频攻击打穿了。后来我加了随机缩放,攻击成功率从 85% 降到了 12%。记住——别把所有鸡蛋放在一个篮子里。

3.4 梯度掩码:让攻击者「找不到北」

梯度掩码的核心思想是隐藏或扭曲模型的梯度信息。因为大多数对抗攻击都依赖梯度(比如 FGSM 用梯度符号,PGD 用梯度方向),如果梯度是错的或者不可用,攻击就失效了。

常见做法:

  • 梯度截断:在反向传播时把梯度限制在 [-c, c] 范围内
  • 梯度模糊:用随机噪声扰动梯度
  • 不可微模块:在模型中间插入不可微操作(如 argmax、排序)

但这里有个大坑——梯度掩码很容易被绕过。攻击者可以用「零阶优化」方法(比如 NES、SPSA)来估计梯度,不需要真正的反向传播。我见过一个案例,某团队用梯度掩码把白盒攻击成功率降到 5%,但换成黑盒攻击后,成功率又回到了 60%。

我的忠告:梯度掩码适合做「临时防御」或「混淆对手」,但别把它当长期方案。真正的安全需要结合多种方法,而不是靠隐藏信息。

3.5 防御评估方法:别自己骗自己

最后这点特别重要——你怎么知道你的防御有效?我见过太多团队,用 FGSM 测一下就说「我们的模型很鲁棒」,结果换个攻击方法就原形毕露。

正确的评估流程应该是:

  1. 多攻击方法测试:至少用 FGSM、PGD、C&W、DeepFool 四种方法
  2. 多扰动大小测试:ε 从 0.01 到 0.5,画一条鲁棒性曲线
  3. 白盒+黑盒测试:白盒假设攻击者知道一切,黑盒假设攻击者只能查询
  4. 自适应攻击:假设攻击者知道你的防御方法,专门设计攻击

我推荐用 RobustBench 这个标准库来做评估。它集成了 20+ 种攻击方法,还能自动生成评估报告。下面是一个简单的评估脚本:

from robustbench import benchmark
from robustbench.model_zoo import model_dicts

# 加载预训练鲁棒模型
model = model_dicts['Standard']['Standard']()

# 在 CIFAR-10 上评估
results = benchmark(
    model=model,
    n_examples=1000,
    dataset='cifar10',
    threat_model='Linf',
    eps=8/255
)

print(f"自然准确率: {results['clean_acc']:.2%}")
print(f"对抗准确率: {results['robust_acc']:.2%}")

关键指标:别只看「对抗准确率」,要看「鲁棒性曲线下的面积」(AUC-robustness)。一个在 ε=0.1 时准确率 90%、ε=0.5 时准确率 10% 的模型,可能还不如一个在 ε=0.3 时稳定保持 60% 的模型。

嗯,到这里五种方法都讲完了。最后送你一句话:对抗防御不是「装一个盾牌」,而是「建一套防御体系」。对抗训练做基础,防御蒸馏做平滑,输入变换做预处理,梯度掩码做迷惑,再加上严格的评估——这套组合拳打下来,你的模型才算真正有了自保能力。