先讲一个我印象很深的场景。某次做图像识别项目,模型在验证集上已经跑到99.8%的准确率,我自己都挺满意。结果团队里一位做安全的前辈随手拿来一张猫的照片,肉眼看起来和正常图片没有任何区别,模型却以99.7%的置信度分类成了“鳄梨”。当时我第一反应是数据有问题,直到他把图片放大、把像素差叠加出来给我看——只是在原图上加了一层人眼几乎感知不到的微小噪声,模型就彻底翻车。后来我才知道,这类问题正是对抗性机器学习研究的核心:通过精心构造的微小扰动让模型出错,同时让人眼无法察觉。这件事也彻底改变了我的认知——模型精度高,和模型可靠,是两码事。
这篇博文我想把对抗性机器学习这条线完整捋一遍:对抗样本是怎么产生的、常见的攻击方法背后是什么原理、防御思路有哪些、真正落地时又会踩哪些坑。适合正在做图像识别、模型鲁棒性评估或者对AI安全感兴趣的开发者参考。哪怕是刚接触机器学习的新手,只要会用训练好的模型,这篇文章里的内容也能直接上手复现。
1. 项目拆解:这到底是在解决什么问题
1.1 核心需求与威胁模型
对抗性机器学习,本质上研究的是:当模型面对“刻意构造的输入”时,是否能保持正确判断。这个“刻意构造”非常关键,它不是随机噪声,而是攻击者基于某种策略精心设计的微小扰动。
理解这个问题,第一步要建立“威胁模型”思维。安全领域做攻防,永远先问三个问题:攻击者能拿到什么信息?攻击者能改变什么?攻击者想达到什么目的?
拿对抗攻击来套这三个问题:
- 攻击者能拿到什么信息?这决定了攻击是白盒、黑盒还是灰盒。白盒意味着攻击者知道模型结构、权重、梯度信息;黑盒意味着攻击者只能通过输入输出接口不断试探。
- 攻击者能改变什么?在数字域,攻击者可以修改输入图片的每个像素;在物理世界,攻击者只能改变物体表面的贴纸、光照、角度等现实因素。
- 攻击者想达到什么目的?非目标攻击只要求模型分错就行;目标攻击则要求模型把输入分类成某个指定类别。
这三个问题一旦想清楚,你就会发现对抗性机器学习不是单纯“加噪声”的游戏,它是一套完整的安全评估方法论。
1.2 攻击者能力分级与分类框架
对抗攻击的分类方式,业界基本上可以按照下面这个表格来梳理:
| 分类维度 | 类型 | 核心特征 |
|---|---|---|
| 信息获取 | 白盒攻击 | 攻击者掌握模型全部参数与梯度,攻击成功率最高 |
| 信息获取 | 黑盒攻击 | 攻击者只通过API输入输出探测,依赖迁移性 |
| 信息获取 | 灰盒攻击 | 攻击者掌握部分信息,比如只知道模型结构不知道权重 |
| 攻击目的 | 非目标攻击 | 只要输出不等于正确标签即可 |
| 攻击目的 | 目标攻击 | 强制模型输出某一指定错误标签 |
| 扰动范围 | 数字域攻击 | 直接修改像素值,精确控制噪声大小 |
| 扰动范围 | 物理域攻击 | 通过打印贴纸、3D打印物体等方式实现对真实世界的攻击 |
| 约束方式 | 范数约束攻击 | 限制扰动的L2或L∞范数,让扰动人眼不可见 |
白盒攻击是研究的基础,几乎所有防御手段都先拿白盒攻击做基准测试。黑盒攻击则更接近真实世界的威胁模型,因为现实里攻击者往往拿不到模型权重。物理域攻击是近几年的热点,比如在路牌上贴几块黑白贴纸让自动驾驶模型把“停止”识别成“限速”,已经有很多研究团队做出来了。
1.3 为什么说这是工程问题而非论文游戏
很多人觉得对抗样本只是学术圈的自娱自乐,实际项目中根本遇不到。这个想法很危险。对抗性机器学习本质上是模型可靠性工程的延伸。
想想看,现在有多少系统把模型直接接入关键决策链路:内容审核系统每天处理几亿张图片,某个像素级扰动就可能让违规内容漏过;人脸识别门禁如果被一张打印出来的对抗贴纸骗过,物理门禁就等于虚设;垃圾邮件过滤模型如果被对抗扰动绕过,整个邮箱系统都会被垃圾邮件淹没。这些都不是假设,过去几年里真实发生过的事故和已公开的研究都在反复印证同一个结论:模型的鲁棒性,决定了系统在对抗环境下的真实安全性。
所以正确的态度是:把对抗攻击当作一类必须纳入测试的“边界情况”,把防御当作模型上线前的例行健康检查。这也正是我写这篇文章想传达的核心思路——不是为了水论文,而是为了给模型加一道保险。
2. 对抗攻击的核心原理:微小扰动是怎么让模型翻车的
2.1 高维线性偏置:工资单类比
要理解对抗样本为什么存在,先要破除一个直觉误区:很多人以为模型是学会了某种“本质特征”,只要输入看着像猫就会判成猫。但实际上,深度网络在高维空间里学到的决策边界非常“锋利”,它对每个维度的微小变化都有强烈反应。
更本质的解释来自“高维线性偏置”:模型对输入 x 的每个维度都学习了一组权重 w,最终判断依赖于 w 与 x 的内积。在高维空间里,输入的维度成百上千甚至上百万,即使每个维度只有极微小的偏移,累加起来也可能产生巨大的输出变化。
这里我用工资单打个比方。你每个月的工资单上有二十个明细项目,每个项目的金额正常都是整数元。如果某个心怀不轨的人把每个项目的金额都偷偷加上一分钱,单看每一项毫无问题,但月底汇总时总额就多出两毛钱。如果项目明细不是二十项而是十万项,每项加一分钱,总额就凭空多出一千块钱——而你的眼睛根本发现不了哪一项出了问题。对抗样本和这事如出一辙:高维度放大了微小扰动的累积效果,模型看到的“汇总结果”已经完全变了,人眼却在每个像素层面看不出异常。
2.2 单步攻击FGSM:原理、公式与PyTorch实现
快速梯度符号法(FGSM)是最经典的对抗攻击算法,它把“高维线性偏置”这个思路用一个极其简洁的公式落地:
x_adv = x + ε · sign(∇_x J(θ, x, y))
公式里面每个符号的意义拆开来看:
x_adv是生成的对抗样本x是原始输入ε是扰动幅度上限,控制噪声的可见程度sign(∇_x J(θ, x, y))是损失函数对输入 x 的梯度方向符号
计算过程非常简单:先做一次正向传播算出损失,再对输入做反向传播得到梯度,然后只取梯度的正负号,乘以一个较小的 ε 叠加到原图上。为什么用符号而不是直接用梯度值?因为我们要限制单步扰动的幅度,符号函数把这个幅度严格控制在 ε 以内,同时保证方向是让损失增大的方向。
下面是一段可以直接跑的PyTorch实现:
import torch import torch.nn.functional as F def fgsm_attack(model, x, y, epsilon=0.03): # x: 输入图像,需要requires_grad=True x.requires_grad = True logits = model(x) loss = F.cross_entropy(logits, y) model.zero_grad() loss.backward() # 取梯度符号方向,乘epsilon得到扰动 grad_sign = x.grad.sign() x_adv = x + epsilon * grad_sign # 裁剪到合法像素范围 x_adv = torch.clamp(x_adv, 0, 1) return x_adv实操时 ε 的选择直接影响效果和隐蔽性。以像素值归一化到 0~1 的图像为例,ε=0.01 的扰动肉眼几乎不可见,但对一个脆弱模型可能已经足够翻转分类;ε=0.05 时对抗性明显增强,但仔细看会有淡淡的噪点;ε=0.1 以上就会留下比较明显的纹路。我实际测试时通常从 0.01 开始,逐步加大直到找到攻击成功率超过 90% 的临界点。
2.3 多步攻击PGD:为什么多走几步就能打穿防线
FGSM 是一步到位,方向够准但幅度有限。如果模型稍微强壮一点,单步攻击往往会被弹回来。投影梯度下降(PGD)则把单步攻击扩展成了多步迭代,思路可以用一句话概括:每次沿着梯度方向走一小步,如果走出了 ε 的“势力范围”,就投影回边界,如此反复。
PGD 的更新公式:
x_{t+1} = Π_{x + S}(x_t + α · sign(∇_x J(θ, x_t, y)))
其中α是每次迭代的步长,S是允许扰动的范围,Π表示投影操作。打个比方,FGSM 是看准一个方向一步跨过去,输了就认了;PGD 则是先迈一小步探探路,发现不行再换个方向继续试,每次都保证自己还在边界内,最后总能找到让模型最难受的位置。
实现上 PGD 也不复杂,本质上就是循环调用 FGSM 的更新步骤:
def pgd_attack(model, x, y, epsilon=0.03, alpha=0.01, iterations=10): x_adv = x.clone().detach().requires_grad_(True) lower = torch.clamp(x - epsilon, 0, 1) upper = torch.clamp(x + epsilon, 0, 1) for _ in range(iterations): logits = model(x_adv) loss = F.cross_entropy(logits, y) model.zero_grad() loss.backward() grad = x_adv.grad.data # 沿梯度方向前进 alpha 步长 x_adv.data = x_adv.data + alpha * grad.sign() # 投影回 epsilon 球内 x_adv.data = torch.clamp(x_adv.data, lower, upper) x_adv.grad.data.zero_() return x_adv.detach()PGD 被很多研究者视为“最强的一阶攻击”,主要原因是它能把模型表面那些不太深的局部极小值都掀翻。防御方做对抗训练时,几乎都会用 PGD 作为基准攻击器来评估鲁棒性,因为它比 FGSM 可靠得多。
2.4 黑盒迁移与查询攻击:不知道梯度也能打
现实世界里的攻击者通常拿不到模型权重,所以黑盒攻击才是威胁模型中最贴近实战的一类。黑盒攻击有两条主流路径:
一条是迁移攻击。攻击者在本地训练一个替代模型,用白盒方法生成对抗样本,然后拿去打目标模型。由于不同模型在相似数据上学到的决策边界有相当大的重叠,很多对抗样本从一个模型迁移到另一个模型后依然有效。这也是为什么对抗训练防御效果有限的原因之一——即使目标模型足够鲁棒,攻击者换一个更容易攻击的替代模型,依然可能找到“通用对抗扰动”。
另一条是基于查询的攻击。攻击者只通过模型的输入输出接口来试探,比如用边界攻击不断在决策边界附近采样,观察输出变化,逐步逼近能让模型误判的方向。这类攻击在开源模型上实测效果相当可观,往往几百到几千次查询就能构造出有效的对抗样本。对于任何对外开放的模型服务来说,这都意味着必须考虑请求频率限制和异常输入检测。
理解了这些攻击形态之后,你会意识到一件事:防御不能只防白盒,也不能只做“眼不见为净”,而是要做多层次的纵深防御。
3. 防御路线与核心算法实现
3.1 防御思路全景:让模型壮、让输入净、让推理稳
对抗防御大致可以分成三类思路,这三类之间的关系不是互斥,而是互补:
- 让模型壮:通过对抗训练、正则化、模型结构修改,让模型本身的决策边界更平滑,从根本上提高鲁棒性。
- 让输入净:在推理前对输入做预处理,比如降噪、压缩、平滑,把可能携带的对抗扰动洗掉。但这类方法很容易被自适应攻击绕过,因为攻击者可以把预处理过程也纳入梯度计算。
- 让推理稳:在推理环节引入随机性,比如随机缩放输入、随机裁剪、集成多个模型的投票结果,让攻击者的精确扰动失效。
这三层防御,对抗训练是最受认可的“正路”,因为它切中要害——让模型真正在对抗样本上也学到正确的分类边界。
3.2 对抗训练:从min-max公式到可落地的训练循环
对抗训练的核心思想可以写成一条 min-max 公式:
min_θ max_δ J(θ, x + δ, y), 其中 ||δ||_p ≤ ε
读起来有点绕,拆开理解就清楚了。内层的 max 是在找让模型损失最大的扰动 δ,也就是攻击者的角色;外层的 min 是在调整模型参数,使其在最坏情况下的损失尽可能小,也就是防御者的角色。整个训练过程像是在和攻击者博弈:攻击者不断制造最刁钻的样本,模型不断在这种压力下变强。
直接理解公式是一回事,落地又是另一回事。实际训练时很少有人对每个 batch 都做完整的 PGD 攻击,因为计算开销太大。一个比较实用的做法是先在干净数据上训练一个基础模型,然后用 PGD 生成对抗样本,把对抗样本和干净样本混合后做微调。下面这个训练循环的思路可以参考:
def adversarial_train_one_epoch(model, dataloader, optimizer, epsilon=0.03, alpha=0.01, pgd_iters=5, alpha_clean=0.5): model.train() for x_batch, y_batch in dataloader: # 生成 PGQ 对抗样本 x_adv = pgd_attack(model, x_batch, y_batch, epsilon, alpha, pgd_iters) # 将对抗样本和干净样本混合 x_mixed = torch.cat([x_batch, x_adv], dim=0) y_mixed = torch.cat([y_batch, y_batch], dim=0) logits = model(x_mixed) loss = F.cross_entropy(logits, y_mixed) optimizer.zero_grad() loss.backward() optimizer.step()这个方案里有两个关键心得。第一,对抗样本和干净样本的混合比例非常关键,我试过纯对抗训练(即只用对抗样本)会导致模型在干净样本上的准确率大幅下降,而按 1:1 混合通常能在鲁棒性和精确度之间取得较好的平衡。第二,PGD 的攻击强度不要一上来就拉满。我给一个非常具体的经验路线(针对小型图像分类模型):
- 用 ε=0.01、迭代 3 步的弱攻击训练前几个 epoch,让模型逐渐适应。
- 每过 5 个 epoch 把 ε 提升到 0.02~0.03,迭代步数增加到 5~7 步。
- 学习率不要用平时训练的大值,对抗训练很容易震荡,建议降到常规学习率的 1/3 到 1/5。
- 训练过程中同时监控干净样本准确率和对抗样本准确率,两条曲线都要看,只盯一条必然出问题。
3.3 输入预处理与随机化推理:便宜但别迷信
预处理防御的思路很直观:既然是噪声导致的误判,那把噪声洗掉不就行?实际常见的做法包括中值滤波、高斯模糊、JPEG 压缩、图像缩放裁剪等。这些操作确实能在一定程度上削弱部分对抗扰动,尤其对低强度的扰动比较有效。它们最大的优点是便宜:推理阶段不需要重新训练模型,改动很小直接嵌入现有 pipeline。
但代价也很明显:攻击者一旦知道你的预处理方式,完全可以把预处理过程当成整个模型的一部分来求梯度,生成对“滤波后输入”依然有效的对抗样本。业内有个专门词汇叫“自适应攻击”——防御方案必须在考虑攻击者自适应的情况下评估,才会发现大部分预处理只能挡住那些不去针对你的攻击者,挡不住真正有耐心的攻击者。
随机化推理则是另一种思路:每次推理前随机对输入做小幅度的缩放、padding 或像素随机擦除,让攻击者无法精确预知模型实际看到的是什么。这类方法有点像给系统加了一层“随机抖动”,让精确构造的扰动难以稳定生效。实测下来它确实能提高一定的鲁棒性,但它更像是一种权宜之计,不能替代对抗训练。
3.4 防御评估的正确姿势:别把梯度遮蔽当防御
这部分我想多说两句,因为太多人在这里栽过跟头。早期有不少防御方法被提出时报告了很高的鲁棒性,后来却被后续研究用自适应攻击逐个击穿。问题出在哪?出在这些方法实质上是在“遮蔽梯度”——比如用不可微的预处理、带温度缩放的非线性层、随机化网络结构等方式,让白盒攻击者难以计算有效的梯度。梯度算不出来,攻击就无法进行,于是测出来的鲁棒性虚高。
真正的鲁棒防御必须经得起考验。我给自己定的评估流程至少包含以下几项:
- 至少测试三种攻击方法:FGSM、PGD、以及一种黑盒迁移攻击。
- 至少覆盖两个扰动预算:一个对应人眼不可见的低扰动(ε=0.01~0.03),一个对应明显但合理的更大扰动(ε=0.05~0.1)。
- 同时报告干净样本准确率和对抗准确率,两者都报才有意义。
- 在多个数据集上独立测试,避免模型在某个数据集上出现过拟合式的鲁棒性。
这样一套流程走下来,基本能筛掉大部分“看着有效其实一戳就破”的防御方案。
4. 工程踩坑实录:常见问题与排查套路
4.1 对抗训练后干净样本准确率雪崩
第一个常见坑:对抗训练跑完后,模型在干净测试集上的准确率从 98% 掉到 85% 以下。很多团队在这个环节会选择放弃对抗训练,觉得牺牲太大。
我的经验是先别急着下结论。对抗训练本质上是在“干净性能”和“鲁棒性”之间做权衡,准确率掉一些是正常的。但如果掉太多,大概率是训练策略的问题:
- 混合比例可能在失衡。把对抗样本比例从 50% 降到 30%,通常能拉回不少干净准确率。
- ε 可能设置得过大。如果模型本身很小、很浅,面对较大扰动时学习难度陡增,可以先降低 ε 到 0.01。
- 学习率可能偏高。对抗训练对学习率异常敏感,我把学习率降到原来的 1/5 后,训练稳定程度显著改善。
另外,优先选择“先干净后对抗”的策略:先用常规方式把模型训练到收敛,再切换到对抗训练模式做微调。直接从头做对抗训练,收敛速度慢而且最终性能往往较差。
4.2 单点防御扛不住多点攻击
还有一种情况是:模型在 FGSM 攻击下表现很好,但换成 PGD 就立刻失效;或者换了攻击扰动类型(比如从 L∞ 换成 L2)后防御效果大幅下降。
这说明模型只在某一类攻击上产生了“定向鲁棒性”,并没有获得通用的平滑决策边界。对抗训练时如果只用一种攻击方式生成样本,模型很容易过拟合到这种攻击的分布上。解决办法是:在对抗训练过程中混用多种攻击生成方式,每次迭代随机选择不同攻击参数,这样模型被迫面对更多样化的扰动模式。类似的思路叫“攻击集成对抗训练”,实测下来虽然训练时间增加约 1.5 倍,但防御的覆盖面明显提升。
4.3 训练过程震荡不收敛
对抗训练的损失函数本身是一个 min-max 博弈,训练不稳定是常态。常见表现是损失曲线上下震荡,或者对抗准确率波动剧烈。
这里有三条实操建议。第一,固定 PGD 攻击的迭代步数,不要在训练过程中随意改变,因为模型在适应一个变化的目标函数时很难收敛。第二,给真实梯度更新加裁剪,限制梯度的 L2 范数在 5 以内,防止个别对抗样本产生过大的梯度把权重推出稳定区。第三,把 epsilon 从默认值调低后训练一段时间再逐步增加,类似于课程学习的思路,让模型先学简单再学难。
4.4 评估指标的隐藏坑:只报对抗精度等于自我安慰
最后还想提醒一个评估阶段的常见问题:部分评估报告只写“在 PGD-20 攻击下准确率 87%”这种单一指标,却不提干净样本准确率,也不说明攻击的强度配置。这种报告不具备任何参考价值,因为模型完全可能为了鲁棒性把整体精度压得很低,甚至退化成“对什么都不敏感”的摆设。
更隐蔽的坑是“鲁棒过拟合”——模型在训练用的攻击配置下表现优异,但换一个更强的攻击者或更大的扰动预算后,鲁棒性急剧下降。所以评估对抗训练效果时,最好在训练集和验证集上都做攻击测试,并且测试时用的攻击配置要独立于训练时的配置。
顺带提一句,实际落地时我还会记录每个样本的置信度变化曲线。有些防御方案虽然在分类标签层面表现不佳,但它会把对抗样本的置信度显著压低,这种特性在真实系统中可以用于二次告警——置信度异常低的请求需要人工复核,这也是一种可用的纵深防御手段。
我个人在实际操作中体会最深的一条是:做对抗性机器学习,心态上不能把自己定位成“调参侠”,而是要同时拥有攻击者和防御者两套思维。拿到一个模型,先想办法把它打穿,再反过来修;修完再换一种方式打。每经历一轮攻防,你对模型的认知就会深一层。如果你正准备入门这个方向,我的建议是先把 FGSM 和 PGD 两个攻击完整复现一遍,在真实模型上观察对抗样本的形态变化,建立好直觉之后再碰防御。最后再分享一个小技巧:做攻击实验时,不妨把 epsilon 一次性拉到 0.05 以上,先直观感受一下模型被击穿的效果,再逐步调小去看人眼不可见的临界点——这种从极端到精细的调试路径,往往比直接抠细节更能帮你理解对抗样本的本质。