1. 对抗样本攻击的底层逻辑与整体分类框架
搞机器学习安全这块的人,迟早会碰到对抗样本这个绕不开的坎。我第一次接触这个概念是在做一个图像分类的小项目时,模型在测试集上准确率跑到98%以上,结果把一张熊猫照片加上一层肉眼几乎看不出来的噪声,模型就斩钉截铁地把它认成了长臂猿,置信度还高达99.3%。当时我就意识到,模型的“聪明”和人类的“聪明”完全是两回事。对抗样本攻击方法总结这件事,本质上是在梳理一个核心问题:如何用最小的扰动,让模型做出最离谱的判断。这篇文章我会把主流攻击方法从原理到实现逐层拆开,包括梯度类、优化类、迁移类、物理世界攻击等方向,适合有一定深度学习基础、想系统理解对抗攻防的读者。
先说清楚对抗样本的定义。给定一个原始输入 ( x ) 和真实标签 ( y ),以及一个目标模型 ( f ),对抗样本 ( x' = x + \delta ) 满足两个条件:一是 ( f(x') \neq y )(非目标攻击)或 ( f(x') = y_{target} )(目标攻击),二是扰动 ( |\delta|p \leq \epsilon ) 足够小,人眼不可察觉。这里的 ( p ) 通常是 ( L\infty )、( L_2 ) 或 ( L_0 ) 范数。( L_\infty ) 限制每个像素的最大变化量,( L_2 ) 限制整体能量,( L_0 ) 限制被修改的像素个数。理解这三种范数的区别非常关键,因为它直接决定了攻击的视觉隐蔽性和实现难度。
为什么对抗样本会存在?目前学界比较公认的解释有几个方向。一个是线性假设:在高维空间中,即使每个维度的扰动很小,累加起来的内积变化也足以让决策边界翻转。另一个是决策边界曲率:模型学到的决策边界在某些区域非常接近数据点,稍微推一下就越界了。还有一个是特征脆弱性:模型依赖的某些特征和人类感知的特征不一致,攻击者恰好可以利用这种错位。这些解释各有道理,实际做攻击时不需要纠结哪个对,但理解它们有助于你设计更有效的攻击策略。
从攻击者掌握的信息来看,对抗攻击分为三大类。白盒攻击是攻击者完全知道模型结构、参数、梯度,这类攻击通常效果最好,扰动最小,代表方法有FGSM、PGD、C&W等。黑盒攻击是攻击者只能查询模型的输入输出,不知道内部细节,又分为基于查询的和基于迁移的两种。灰盒攻击介于两者之间,比如知道模型结构但不知道参数。实际场景中,黑盒攻击更贴近真实威胁模型,因为攻击者通常拿不到目标模型的权重。
从攻击目标来看,分为非目标攻击和目标攻击。非目标攻击只要让模型分类错误就行,难度较低;目标攻击要让模型输出指定的错误类别,难度明显更高,通常需要更大的扰动或更多的迭代步数。还有一个维度是有目标类别 vs 无目标类别,以及定向 vs 非定向,这些术语在不同论文里叫法不太统一,但核心意思就这两类。
从扰动范数约束来看,( L_\infty ) 攻击最常用,因为它实现简单,用clip操作就能控制;( L_2 ) 攻击生成的扰动更分散,视觉上更自然;( L_0 ) 攻击只改少量像素,适合研究稀疏攻击。选择哪种范数取决于你的应用场景:如果是研究图像分类的鲁棒性,( L_\infty ) 是标配;如果要做物理世界攻击,( L_2 ) 或 ( L_0 ) 可能更合适,因为物理扰动往往不是均匀分布的。
下面这张表是我自己整理的攻击方法分类速查,后面每个方法都会展开讲。
| 分类维度 | 类别 | 代表方法 | 特点 |
|---|---|---|---|
| 信息掌握 | 白盒 | FGSM, PGD, C&W | 效果好,扰动小 |
| 信息掌握 | 黑盒-查询 | ZOO, NES | 需要大量查询 |
| 信息掌握 | 黑盒-迁移 | MI-FGSM, DI-FGSM | 无需查询,依赖迁移性 |
| 攻击目标 | 非目标 | FGSM, PGD | 难度低 |
| 攻击目标 | 目标 | Targeted-PGD, C&W | 难度高 |
| 范数约束 | L∞ | FGSM, PGD | 像素级限制 |
| 范数约束 | L2 | C&W, DeepFool | 整体能量限制 |
| 范数约束 | L0 | JSMA, SparseFool | 稀疏像素修改 |
理解了这些分类,你在读论文或做实验时就能快速定位一个方法的位置,知道它解决的是什么问题,代价是什么。接下来我会按攻击方法的发展脉络,从最经典的FGSM开始,一路讲到最新的迁移攻击和物理攻击。
2. 白盒攻击方法逐层拆解与实操要点
2.1 FGSM:一行代码就能跑通的入门攻击
FGSM全称Fast Gradient Sign Method,是Goodfellow在2014年提出的,可以说是对抗攻击领域的“Hello World”。它的核心思想极其简单:沿着损失函数对输入的梯度方向,取符号函数,乘以步长 ( \epsilon ),就得到了扰动。
公式就一行:
[ x' = x + \epsilon \cdot \text{sign}(\nabla_x J(f(x), y)) ]
其中 ( J ) 是损失函数,通常是交叉熵。为什么取sign?因为我们要在 ( L_\infty ) 约束下最大化损失,而 ( L_\infty ) 球面上的最优方向就是梯度的符号方向。这个推导用到了Holder不等式,简单说就是在每个维度上,梯度绝对值最大的方向就是符号方向。
我用PyTorch写一个最小实现,你可以直接抄:
import torch import torch.nn as nn def fgsm_attack(model, x, y, epsilon): x_adv = x.clone().detach().requires_grad_(True) output = model(x_adv) loss = nn.CrossEntropyLoss()(output, y) model.zero_grad() loss.backward() grad_sign = x_adv.grad.data.sign() x_adv = x + epsilon * grad_sign x_adv = torch.clamp(x_adv, 0, 1) return x_adv.detach()这段代码有几个细节要注意。第一,x_adv需要requires_grad_(True),否则拿不到梯度。第二,model.zero_grad()要放在backward()之前,清掉之前的梯度。第三,最后要clamp到合法像素范围,否则生成的图片可能超出[0,1]区间,保存时会出现奇怪的颜色。
FGSM的优点是快,一次前向一次反向就搞定,适合大规模生成对抗样本做对抗训练。缺点是扰动不够精细,同样的 ( \epsilon ) 下,它的攻击成功率通常低于迭代方法。我实测下来,在CIFAR-10上,( \epsilon=8/255 ) 时FGSM的非目标攻击成功率大概在60%到70%之间,而PGD能到95%以上。
注意:FGSM对 ( \epsilon ) 非常敏感。( \epsilon ) 太小攻击无效,太大则肉眼可见。做实验时建议从 ( \epsilon=1/255 ) 开始试,逐步增加到 ( 8/255 )、( 16/255 ),观察攻击成功率和视觉质量的变化。
还有一个变体叫FGSM with random start,就是在原始输入上先加一个小的随机扰动,再做FGSM。这个技巧能提升攻击成功率,因为随机起点让攻击者有机会跳出损失函数的局部平坦区域。实现上就是在FGSM之前加一行x = x + torch.empty_like(x).uniform_(-epsilon, epsilon),然后clamp。
2.2 PGD:迭代攻击的标杆方法
PGD全称Projected Gradient Descent,也叫I-FGSM(Iterative FGSM),是目前白盒攻击里最常用的基准方法。它的思路是把FGSM的单步变成多步,每步走小一点,走完后再投影回 ( \epsilon ) 球内。
算法流程是这样的:
- 初始化 ( x_0' = x + \text{random_noise} ),噪声在 ( [-\epsilon, \epsilon] ) 内均匀采样
- 对 ( t = 1 ) 到 ( T ):
- 计算梯度 ( g_t = \nabla_x J(f(x_{t-1}'), y) )
- 更新 ( x_t' = x_{t-1}' + \alpha \cdot \text{sign}(g_t) )
- 投影 ( x_t' = \text{clip}(x_t', x - \epsilon, x + \epsilon) )
- 再clip到[0,1]
- 返回 ( x_T' )
这里的 ( \alpha ) 是每步步长,通常取 ( \epsilon / 4 ) 或 ( \epsilon / 10 ),( T ) 是迭代次数,常见取10、20、40、100。步长和迭代次数的关系是:( \alpha \cdot T ) 要略大于 ( \epsilon ),这样才有足够的“探索空间”让攻击找到好的解。
def pgd_attack(model, x, y, epsilon, alpha, num_iter): x_adv = x.clone().detach() x_adv = x_adv + torch.empty_like(x_adv).uniform_(-epsilon, epsilon) x_adv = torch.clamp(x_adv, 0, 1) for i in range(num_iter): x_adv.requires_grad_(True) output = model(x_adv) loss = nn.CrossEntropyLoss()(output, y) model.zero_grad() loss.backward() grad_sign = x_adv.grad.data.sign() x_adv = x_adv.detach() + alpha * grad_sign x_adv = torch.min(torch.max(x_adv, x - epsilon), x + epsilon) x_adv = torch.clamp(x_adv, 0, 1) return x_adv.detach()PGD为什么比FGSM强?直观理解是,FGSM只走一步,容易走到损失函数的“沟壑”里出不来;PGD走多步,每步都重新计算梯度,能更充分地探索 ( \epsilon ) 球内的空间。理论上,PGD可以近似求解 ( L_\infty ) 约束下的最优扰动,所以它常被用作评估模型鲁棒性的“最强白盒攻击”。
实操中有几个坑我踩过。第一,随机初始化很重要。如果不用随机起点,PGD可能陷入某个局部最优,攻击成功率会下降。第二,步长不要设太大。( \alpha ) 太大时,每步更新会“跳过”好的解,反而降低效果。我一般用 ( \alpha = \epsilon / 10 ),迭代20到50次。第三,目标攻击需要改损失函数。非目标攻击是最大化真实标签的损失,目标攻击是最小化目标标签的损失,代码上就是把loss = -CrossEntropy(output, target_label)。
提示:PGD的攻击成功率随迭代次数增加而提升,但边际收益递减。迭代20次和迭代100次的差距可能只有2到3个百分点,但计算时间差5倍。做对抗训练时通常用7到10次迭代,做鲁棒性评估时用20到50次。
2.3 C&W攻击:优化视角下的精细扰动
C&W攻击是Carlini和Wagner在2017年提出的,它的出发点不是“快速生成”,而是“最小扰动”。FGSM和PGD优化的是 ( L_\infty ) 范数下的攻击成功率,C&W直接优化扰动大小,目标是找到最小的 ( \delta ) 使得模型分类错误。
C&W的原始形式是一个约束优化问题:
[ \min_{\delta} |\delta|_p + c \cdot f(x + \delta) ]
其中 ( f ) 是一个自定义的损失函数,设计目标是让模型把 ( x+\delta ) 分类到目标类别。C&W提出了7种 ( f ) 的候选形式,最常用的是:
[ f(x') = \max(\max_{i \neq t} Z(x')_i - Z(x')_t, -\kappa) ]
这里 ( Z ) 是logits输出,( t ) 是目标类别,( \kappa ) 是一个置信度参数,控制攻击的“强度”。( \kappa ) 越大,攻击越强,但扰动也越大。
C&W的厉害之处在于,它生成的对抗样本在视觉上几乎完美,而且对防御方法(如对抗训练、防御蒸馏)有很强的穿透力。但代价是计算量大,需要对每个样本单独优化,通常用Adam或L-BFGS求解,一个样本可能要跑几百到几千次迭代。
def cw_attack(model, x, target, c=1e-4, kappa=0, num_iter=1000, lr=0.01): delta = torch.zeros_like(x, requires_grad=True) optimizer = torch.optim.Adam([delta], lr=lr) for i in range(num_iter): x_adv = torch.clamp(x + delta, 0, 1) output = model(x_adv) target_logit = output[0, target] other_logits = output.clone() other_logits[0, target] = -1e10 max_other = other_logits.max() f_loss = torch.clamp(max_other - target_logit, min=-kappa) l2_loss = torch.norm(delta.view(delta.size(0), -1), p=2) loss = l2_loss + c * f_loss optimizer.zero_grad() loss.backward() optimizer.step() return torch.clamp(x + delta, 0, 1).detach()C&W的调参经验:( c ) 是最关键的超参数,太小则攻击无效,太大则扰动过大。实践中常用二分搜索找合适的 ( c ),从 ( 10^{-4} ) 开始,如果攻击失败就乘10,如果扰动太大就除以10。( \kappa ) 一般设0,表示只要分类错误就行;如果要更强的攻击,可以设 ( \kappa=20 ) 或 ( \kappa=40 )。
注意:C&W攻击对 ( L_2 ) 范数的优化效果最好,对 ( L_\infty ) 需要改一下损失函数,把 ( L_2 ) 范数换成 ( L_\infty ) 范数的近似。另外,C&W的计算成本很高,不适合大规模生成对抗样本,更适合做小规模的精调攻击或防御评估。
2.4 DeepFool:寻找最近决策边界
DeepFool的思路很优雅:它想找到从原始样本到最近决策边界的最短路径。对于二分类问题,这个最短路径就是到超平面的垂线;对于多分类问题,DeepFool用迭代的方式,每次找最近的决策边界,走一小步,直到分类翻转。
算法核心是:在每个迭代步,对每个非真实类别 ( k ),计算把样本推到类别 ( k ) 的决策边界所需的最小扰动,然后选最小的那个,更新样本。重复直到分类错误。
DeepFool生成的扰动通常比FGSM小,但比C&W略大。它的优点是无需调参(除了迭代次数和过冲系数),而且对 ( L_2 ) 范数有理论保证。缺点是只适用于白盒,且对复杂模型可能不收敛。
def deepfool_attack(model, x, num_classes=10, max_iter=50, overshoot=0.02): x_adv = x.clone().detach().requires_grad_(True) for i in range(max_iter): output = model(x_adv) pred = output.argmax().item() if pred != x.argmax().item(): break grad_orig = torch.autograd.grad(output[0, pred], x_adv, retain_graph=True)[0] min_dist = float('inf') min_grad = None for k in range(num_classes): if k == pred: continue grad_k = torch.autograd.grad(output[0, k], x_adv, retain_graph=True)[0] diff = grad_k - grad_orig dist = abs(output[0, k] - output[0, pred]) / (diff.norm() + 1e-8) if dist < min_dist: min_dist = dist min_grad = diff r = (min_dist + 1e-4) * min_grad / (min_grad.norm() + 1e-8) x_adv = (x_adv + (1 + overshoot) * r).detach().requires_grad_(True) return x_adv.detach()DeepFool的过冲系数overshoot一般设0.02,作用是让样本稍微越过决策边界,避免在边界上震荡。迭代次数50通常够用,如果模型很深,可以加到100。
3. 黑盒攻击与迁移攻击的实战策略
3.1 迁移攻击:不查模型也能打
迁移攻击是黑盒场景下最实用的方法。核心思想是:在一个替代模型(surrogate model)上生成对抗样本,然后直接拿去攻击目标模型。之所以有效,是因为不同模型在相似数据上训练时,学到的决策边界有相似性,对抗样本的“攻击方向”可以跨模型迁移。
迁移攻击的关键是提升对抗样本的迁移性。早期直接用FGSM或PGD在替代模型上生成,迁移成功率通常只有20%到40%。后来一系列工作通过改进梯度计算方式,把迁移成功率提升到了80%以上。
MI-FGSM(Momentum Iterative FGSM)是第一个重要改进。它在PGD的基础上引入了动量项,让梯度更新更稳定,避免陷入替代模型的局部最优。公式是:
[ g_{t+1} = \mu \cdot g_t + \frac{\nabla_x J(f(x_t'), y)}{|\nabla_x J(f(x_t'), y)|_1} ]
[ x_{t+1}' = x_t' + \alpha \cdot \text{sign}(g_{t+1}) ]
动量系数 ( \mu ) 通常取1.0。归一化用 ( L_1 ) 范数而不是 ( L_2 ),是因为 ( L_1 ) 归一化后每个像素的梯度贡献更均匀,迁移性更好。
DI-FGSM(Diverse Inputs FGSM)是另一个重要改进。它在每次迭代时,对输入做随机resize和padding,让攻击者看到的输入有变化,从而生成对输入变换更鲁棒的对抗样本。实现上就是在计算梯度前,对 ( x_t' ) 做一次随机变换:
def random_transform(x, prob=0.5): if torch.rand(1) > prob: return x # 随机resize到不同尺寸再resize回来 scale = torch.randint(200, 260, (1,)).item() / 256 new_size = int(x.size(2) * scale) x_t = torch.nn.functional.interpolate(x, size=(new_size, new_size), mode='bilinear') # 随机padding pad_top = torch.randint(0, 20, (1,)).item() pad_left = torch.randint(0, 20, (1,)).item() x_t = torch.nn.functional.pad(x_t, (pad_left, 20-pad_left, pad_top, 20-pad_top), mode='constant', value=0) x_t = torch.nn.functional.interpolate(x_t, size=(x.size(2), x.size(3)), mode='bilinear') return x_tTI-FGSM(Translation-Invariant FGSM)用高斯核卷积梯度,让梯度在空间上更平滑,提升对平移变换的鲁棒性。SI-FGSM(Scale-Invariant FGSM)在多个尺度上计算梯度并平均,提升对尺度变化的鲁棒性。NI-FGSM(Nesterov Iterative FGSM)用Nesterov加速梯度,提升收敛速度。
实际做迁移攻击时,我通常把MI、DI、TI、SI组合起来用,效果比单用好很多。组合方式是在每次迭代时,先对输入做DI变换,计算梯度,然后用TI的高斯核卷积梯度,再用MI的动量更新,最后用SI的多尺度平均。这套组合拳在ImageNet上能把迁移成功率从30%提升到90%以上。
提示:迁移攻击的替代模型选择很关键。通常用多个模型集成(ensemble)效果更好,比如同时用ResNet、VGG、Inception生成对抗样本,然后平均梯度。集成时每个模型的损失要归一化,避免某个模型的损失主导梯度方向。
3.2 基于查询的黑盒攻击
如果攻击者可以查询目标模型,但不知道内部细节,可以用基于查询的方法。这类方法把攻击建模成优化问题,通过查询来估计梯度或直接搜索扰动。
ZOO(Zeroth Order Optimization)用有限差分估计梯度。对每个像素,分别加一个小扰动 ( h ) 和减一个小扰动 ( h ),用输出变化除以 ( 2h ) 估计梯度。这个方法精度高,但查询次数是像素数的两倍,对于224x224的图像,一次梯度估计要查询10万次以上,实际不可行。改进方法是用坐标下降,每次只优化一个像素,查询次数降到几千次。
NES(Natural Evolution Strategies)用随机方向采样估计梯度。每次采样一个随机方向 ( u ),计算 ( f(x + \sigma u) - f(x - \sigma u) ),用这个差值乘以 ( u ) 作为梯度的无偏估计。查询次数比ZOO少,但方差大,需要多次采样平均。
Bandits方法把攻击建模成多臂老虎机问题,每次选择一个像素或一个区域进行扰动,根据反馈更新策略。这类方法查询效率高,但实现复杂,调参困难。
基于查询的攻击在实际中受限于查询次数。很多线上模型有查询频率限制,或者每次查询有成本。所以这类方法更多用于研究场景,实际攻击更常用迁移攻击。
3.3 物理世界攻击:从数字到现实的跨越
物理世界攻击是对抗样本最“出圈”的方向,因为它能影响现实系统,比如让自动驾驶误判停车标志,或者让人脸识别系统认错人。物理攻击的难点在于:数字扰动经过打印、拍摄、光照变化后,可能完全失效。
EOT(Expectation Over Transformation)是物理攻击的核心技术。它的思想是:在生成对抗样本时,不是优化单个数字图像,而是优化一个变换分布上的期望。具体来说,对原始图像做随机旋转、缩放、亮度调整、噪声添加,然后在这些变换后的图像上计算损失,反向传播时把梯度累加回原始图像。这样生成的对抗样本对变换更鲁棒。
def eot_attack(model, x, y, epsilon, num_iter, num_samples=10): x_adv = x.clone().detach().requires_grad_(True) optimizer = torch.optim.Adam([x_adv], lr=0.01) for i in range(num_iter): total_loss = 0 for j in range(num_samples): # 随机变换 angle = torch.empty(1).uniform_(-15, 15).item() scale = torch.empty(1).uniform_(0.9, 1.1).item() brightness = torch.empty(1).uniform_(0.8, 1.2).item() x_t = transform(x_adv, angle, scale, brightness) output = model(x_t) loss = nn.CrossEntropyLoss()(output, y) total_loss += loss total_loss /= num_samples optimizer.zero_grad() total_loss.backward() optimizer.step() # 投影到epsilon球 with torch.no_grad(): x_adv.data = torch.clamp(x_adv.data, x - epsilon, x + epsilon) x_adv.data = torch.clamp(x_adv.data, 0, 1) return x_adv.detach()物理攻击的另一个关键是可打印性。数字扰动可以是任意浮点数,但打印机只能打印有限色域。所以生成对抗样本时,要限制颜色在可打印范围内,或者用CMYK色彩空间优化。另外,打印后的图像会有墨点扩散、纸张纹理、光照不均等问题,EOT的变换分布要覆盖这些因素。
我做过一个简单的物理攻击实验:在停车标志上贴几个黑白贴纸,让分类器把它认成限速标志。数字环境下攻击成功率95%,打印出来贴在墙上,用手机拍照测试,成功率降到60%左右。后来增加了EOT的变换种类(加了透视变换和运动模糊),成功率回到80%以上。这个实验让我深刻体会到,物理攻击的难度比数字攻击高一个数量级。
注意:物理世界攻击涉及现实系统安全,做实验时务必在受控环境下进行,不要影响公共设施或他人财产。学术研究用模拟场景即可,不要真的去攻击生产系统。
4. 常见问题排查与防御视角的攻防对抗
4.1 攻击失败的原因排查
做对抗攻击实验时,最常见的问题就是“攻击不成功”。我整理了一个排查清单,按可能性从高到低排列。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 攻击成功率极低 | epsilon太小 | 打印epsilon和扰动范数 | 增大epsilon到8/255或16/255 |
| 攻击成功率极低 | 梯度计算错误 | 检查requires_grad和zero_grad | 确保输入需要梯度,梯度清零 |
| 攻击成功率低 | 迭代次数不够 | 增加迭代次数到50或100 | 用PGD替代FGSM |
| 攻击成功率低 | 步长不合适 | 尝试epsilon/4, epsilon/10 | 调整alpha |
| 目标攻击失败 | 损失函数方向错 | 检查是最大化还是最小化 | 目标攻击用负损失 |
| 扰动肉眼可见 | epsilon太大 | 计算PSNR或SSIM | 减小epsilon或改用C&W |
| 迁移攻击失败 | 替代模型太弱 | 换更强的替代模型 | 用集成模型 |
| 物理攻击失败 | 变换不够 | 增加EOT变换种类 | 加透视、模糊、噪声 |
还有一个隐蔽的坑是模型预处理。很多模型在推理时会做归一化,比如把[0,1]的像素转到[-1,1]或减去均值除以标准差。如果你在攻击时没有复现这个预处理,梯度方向就是错的,攻击自然失败。我踩过这个坑,调了一下午才发现是归一化没对齐。
另一个坑是batch normalization。如果模型在训练时用了BN,推理时用的是running mean和var,攻击时如果模型处于train模式,BN会用当前batch的统计量,导致梯度不稳定。攻击前一定要model.eval()。
4.2 对抗防御的常见思路
了解攻击方法后,自然要问:怎么防?目前主流的防御思路有几类。
对抗训练是最有效的防御方法。核心思想是在训练时把对抗样本加入训练集,让模型学会抵抗攻击。最经典的方案是用PGD生成对抗样本,然后最小化对抗样本上的损失。对抗训练能显著提升鲁棒性,但代价是标准准确率会下降几个百分点,而且训练成本高(每步都要生成对抗样本)。
输入预处理是在推理前对输入做变换,比如JPEG压缩、位深度降低、随机resize、总变差最小化等。这些变换能破坏对抗扰动,但对正常图像的准确率也有影响。而且攻击者如果知道预处理方法,可以针对性地生成对抗样本,所以这类防御通常不够鲁棒。
梯度掩码是让攻击者拿不到有用的梯度,比如用非可微的预处理、随机化、或者梯度正则化。但这类防御容易被“梯度-free”攻击绕过,比如迁移攻击或基于查询的攻击。
** certified defense**是提供数学保证的防御,比如随机平滑、区间 bound propagation。这类方法能证明在某个扰动范围内模型输出不变,但通常只适用于小模型或小扰动,实用性有限。
从攻防对抗的角度看,目前没有“完美”的防御。对抗训练虽然有效,但计算成本高,而且对未见过的攻击类型可能失效。实际部署时,通常是多种防御组合使用,比如对抗训练加输入预处理,再加异常检测。
4.3 实操心得与避坑指南
最后分享几个我在做对抗攻击实验时总结的经验。
第一,固定随机种子。对抗攻击涉及随机初始化、随机变换、dropout等随机因素,不固定种子的话,实验结果不可复现。我一般在代码开头加:
import torch import numpy as np import random def set_seed(seed=42): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) np.random.seed(seed) random.seed(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False第二,保存对抗样本时注意格式。对抗样本的像素值可能超出[0,1],保存为图片时会被截断,导致攻击失效。保存前一定要clamp,或者保存为npy文件保留原始浮点值。
第三,评估攻击成功率时要用正确的指标。非目标攻击的成功率是“分类错误的比例”,目标攻击的成功率是“分类到目标类别的比例”。不要混淆。另外,如果原始样本本身就被分类错误,应该从分母中排除。
第四,注意计算资源。PGD迭代50次,每个样本要50次前向反向,1000个样本就是5万次。用GPU跑,CIFAR-10上大概几分钟,ImageNet上可能要几小时。做大规模实验前先在小批量上验证代码正确性。
第五,多读论文的官方实现。对抗攻击的论文很多,但细节往往在代码里。比如FGSM的随机起点、PGD的投影方式、C&W的二分搜索,不同实现有差异。建议直接看作者的开源代码,或者用成熟的库如Foolbox、Adversarial Robustness Toolbox,避免重复造轮子。
第六,理解攻击的局限性。对抗攻击在数字世界很有效,但物理世界受限于拍摄条件、光照、角度等因素。而且防御方法在不断进化,今天有效的攻击明天可能就失效了。做研究时要关注最新的攻防进展,不要停留在几年前的基准上。
这个领域变化很快,每年都有新的攻击和防御方法出来。我个人的体会是,与其追求“最强攻击”,不如深入理解攻击背后的原理,这样才能在防御端设计出更鲁棒的方案。对抗样本本质上是模型决策边界在高维空间中的脆弱性体现,只要模型还是用梯度下降训练的,这个问题就不会完全消失。