简介:一套围绕图像分类对抗攻防实验的完整项目资料,基于Fashion_MNIST数据集,面向希望动手掌握对抗样本生成与防御方法的深度学习初学者或安全方向研究者。内容覆盖快速梯度符号法、投影梯度下降等经典攻击算法及对抗训练流程,有助于理解微小扰动如何导致模型误判,并通过在训练中引入对抗样本提升模型鲁棒性。压缩包内共121个文件,以100张攻击效果对比图、9个Python脚本、8个pkl文件、2个H5模型文件和1个说明文档为主,整体约15MB,结构清晰便于按需查找。其中样例图直观展示对抗样本与原始图像的视觉差异,脚本实现数据加载、攻击生成、模型训练与评估,pkl/h5文件保存数据集中间结果与训练好的模型,可直接运行复现或二次开发。已有112人学习下载,适合想通过具体实验理解对抗攻防核心思想、观察鲁棒性评估过程的读者。
1. 项目背景与总体思路
1.1 为什么用Fashion_MNIST做对抗攻防实验
最近一直在折腾图像分类模型的鲁棒性问题,刚好整理了一个用Fashion_MNIST数据集做对抗攻击和对抗训练的完整项目,也就是标题里那个Attack_Fashion_MNIST.zip。这个实验虽然跑在经典数据集上,但背后的思路和坑点跟真实业务场景里遇到的问题一模一样,很适合当作入门对抗攻防的练手项目。
选Fashion_MNIST而不是原版MNIST,主要原因在于Fashion_MNIST的语义特征更接近真实世界。MNIST的手写数字分类任务对现代模型来说太简单了,网络往往学到的是像素级别的边缘特征,导致对抗攻击效果不够直观。Fashion_MNIST包含T恤、裤子、外套、连衣裙、衬衫等10类衣物,类别之间高度相似,比如衬衫和外套、连衣裙和T恤之间在视觉上天然存在混淆性。在这种数据集上做对抗攻击,你会更清楚地看到模型在"看起来明明没有区别"的两张图片上给出完全不同的预测,这种视觉冲击力和说服力是MNIST给不了的。
另一个原因是计算成本。Fashion_MNIST的图像是28x28灰度图,和MNIST一样轻量,用普通CPU或者一块入门级GPU就能在几分钟内完成训练和攻击实验。不像ImageNet动辄要几小时甚至几天的训练周期,Fashion_MNIST能让你在一天之内把"正常训练——构造攻击——对抗训练——再次评估"整个闭环跑通,试错成本极低。对于刚接触对抗攻防的研究者、学生或者工程团队来说,这是性价比最高的实验环境。
这个项目的核心目标很明确——用FGSM和PGD两种经典攻击算法对图像分类模型发起对抗攻击,再通过对抗训练提升模型的鲁棒性,形成一组可对比的实验数据。适合对深度学习有一定基础、想系统了解对抗样本原理和防御手段的读者,代码跑通之后,再迁移到自己的业务模型上会轻松很多。
1.2 对抗攻击到底在解决什么问题
对抗攻击的本质,是在输入数据上添加人类难以察觉的微小扰动,让模型以高置信度输出错误结果。你看到的还是一张衣柜里的黑色T恤照片,但模型的预测结果却可能从"T恤"变成"外套"。这不是模型bug,而是深度学习模型的固有缺陷——模型在高维空间中拟合的是决策边界,而决策边界附近的微小扰动就可能导致样本跨越边界。
从实际应用角度来看,这个问题的严重性远超想象。人脸识别系统可能因为一个几乎不可见的贴片就错误识别身份,自动驾驶的视觉模块可能因为路面上的几张贴纸就把限速牌识别成停止牌,内容审核模型可能因为微小噪声就漏掉违规图片。对抗样本的存在,意味着我们很难纯粹依赖模型精度来评判一个系统的安全性,鲁棒性必须作为独立维度纳入评估体系。
对抗训练是目前被验证最有效的防御手段之一,思路直观:在训练过程中主动生成对抗样本,把对抗样本和原始样本一起喂给模型训练,让模型在训练阶段就见过这类"恶意"输入,从而学会忽略微小扰动、关注真正有区分度的特征。这个思路跟防诈骗有点像——你想要不被骗,最好的方式不是只看诈骗案例的文字介绍,而是真实地模拟几场诈骗场景,让系统在演练中学会识别套路。
2. 对抗攻击核心原理与算法选型
2.1 FGSM——攻击的入门必修课
FGSM(Fast Gradient Sign Method,快速梯度符号法)是几乎所有对抗攻击教程的起点,由Goodfellow等人在2014年提出,是第一个被广泛认可的对抗攻击算法。它的核心洞见在于:深度学习模型是通过梯度下降学习的,那么攻击者同样可以利用梯度信息来破坏模型——让模型在梯度方向的反方向走一步。
FGSM的公式非常简洁:
x_adv = x + epsilon * sign(∇x L(x, y_true))其中x是原始输入图像,y_true是真实标签,L是损失函数(通常是交叉熵损失),∇x L指的是损失对输入图像的梯度,epsilon是扰动强度。sign函数将梯度方向压缩成每个像素位置取值为+1或-1,这样生成的扰动就是与梯度方向严格对齐的噪声。
这里的直觉是:交叉熵损失对输入的梯度方向,指明了"朝哪个方向修改像素值能让模型损失变得最大"——即让模型更可能预测错误的方向。我们沿着这个方向迈出一步(步长由epsilon控制),就得到了一个对抗样本。因为每个像素只改变epsilon大小的值,所以攻击效果好的时候,视觉上画面几乎不变。
在PyTorch里实现FGSM非常直接:
def fgsm_attack(model, images, labels, epsilon): """ 对一批图像执行FGSM攻击 model: 已经加载好权重的模型 images: 归一化后的输入图像,需要requires_grad=True labels: 图像的真实标签 epsilon: 扰动强度,Fashion-MNIST上常用0.05~0.3 """ images = images.clone().detach().requires_grad_(True) outputs = model(images) loss = nn.CrossEntropyLoss()(outputs, labels) # 关键:计算损失对输入图像的梯度,而不是对模型参数的梯度 model.zero_grad() loss.backward() # 取梯度的符号方向,乘以epsilon,加到原图上 data_grad = images.grad.data perturbed_images = images + epsilon * data_grad.sign() # 因为图像像素有范围,需要做裁剪,保持输出是合法图像 perturbed_images = torch.clamp(perturbed_images, 0, 1) return perturbed_images2.2 PGD——比FGSM更强的迭代攻击武器
FGSM虽然简洁漂亮,但缺点也很明显:只做一次梯度更新,攻击强度有限,特别是对做过对抗训练的模型,FGSM往往很难攻破。实战中需要更强的攻击算法,PGD(Projected Gradient Descent,投影梯度下降)就是目前使用最广泛的白盒强攻击基准之一,Madry等人在2017年的论文中系统地验证了它的有效性。
PGD可以理解为FGSM的迭代升级版:
x_0 = x + uniform(-epsilon, epsilon) # 随机初始化一个扰动 x_{t+1} = clip(x_t + alpha * sign(∇x L(x_t, y)), x - epsilon, x + epsilon)每一步走一个较小的步长alpha,而且每次走完都要把结果投影回以原始图像为中心、半径为epsilon的约束球内。这个投影操作保证了对抗样本不会超出预设的扰动范围,始终保持"人眼不可辨"的约束。
PGD比FGSM强在什么地方呢?FGSM像是一锤子买卖,方向对了就有效,方向稍偏就效果有限。PGD则是走一步看一步,每步都在当前状态下重新计算梯度方向,相当于一个精细的登山过程——沿着损失面一步步往上爬,直到爬到epsilon球内的局部最高点。这个局部最高点对应的损失值,就是模型在这个约束范围内最坏情况下的损失。所以PGD经常被称为"最强的白盒一阶攻击",对抗训练中用PGD生成的对抗样本训练模型,防御效果会远比只用FGSM的训练扎实。
从工程角度,PGD的实现比FGSM多一个循环,但代码量差异不大:
def pgd_attack(model, images, labels, epsilon, alpha, num_iter): """ PGD攻击 epsilon: 总扰动预算 alpha: 每次迭代的步长 num_iter: 迭代次数 """ perturbed = images.clone().detach() + torch.empty_like(images).uniform_(-epsilon, epsilon) perturbed = torch.clamp(perturbed, 0, 1) for _ in range(num_iter): perturbed.requires_grad_(True) outputs = model(perturbed) loss = nn.CrossEntropyLoss()(outputs, labels) model.zero_grad() loss.backward() grad = perturbed.grad.data # 在梯度方向上迈一小步,然后投影回epsilon球内 perturbed = perturbed + alpha * grad.sign() perturbed = torch.max(torch.min(perturbed, images + epsilon), images - epsilon) perturbed = torch.clamp(perturbed, 0, 1) perturbed = perturbed.detach() return perturbed一个经常被忽略的细节是:PGD的初始扰动应该随机采样而不是从全零开始。这是因为如果从干净样本出发,第一次迭代后可能收敛到一个次优的局部极值,随机初始化有助于探索损失面的不同区域,最终找到更强的攻击效果。我在实验中对比过,相同参数下,随机初始化比零初始化的攻击成功率普遍高出3到8个百分点。
3. 对抗训练的实现细节
3.1 网络模型设计
对抗训练的实验模型不需要太复杂,太深太宽的网络既费算力,又容易把"模型自身能力不足"和"对抗训练效果"两个变量混在一起。我用了一个结构简单的三层卷积网络,参数量不到100万,在Fashion_MNIST上能达到约92%的干净准确率,足够用来观察对抗攻防的效果差异。
模型结构参考了经典的VGG思想,但压缩到了极简版本:
class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super(SimpleCNN, self).__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 3 * 3, 256), nn.ReLU(inplace=True), nn.Dropout(0.3), nn.Linear(256, num_classes), ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x在激活函数的选择上,我这里用了ReLU。有一种说法是ReLU对对抗扰动天然不够平滑,换成Softplus或Swish会更鲁棒,但从实验对比来看,在Fashion_MNIST这种小数据集上差异并不明显。模型结构本身不是对抗训练的核心,训练范式才是。
需要注意的关键点在于:攻击和训练必须统一在同一个数据预处理管道下。Fashion_MNIST的标准像素范围是[0, 1],而归一化操作(例如用mean=0.5和std=0.5做标准化)会改变梯度方向和扰动的实际语义。我踩过一个典型的坑:训练时做了标准化,攻击时也做了标准化,但对抗扰动是在标准化后的取值范围下计算的,导致生成的扰动映射回原始像素空间后并不是慢速平滑的,攻击效果大打折扣。最后的解决方案是:全程使用原始像素值[0, 1]范围,不作标准化,只做简单的缩放,这样对抗扰动的含义最直观,调参时也更容易推断epsilon取值的合理性。
3.2 训练流程改造
对抗训练听起来高端,实现起来就是对训练循环做一处关键改动:每个batch计算完正常样本的损失后,先构造一批对抗样本,再用对抗样本更新模型参数。
完整的对抗训练流程可以拆成这么几步:
- 从dataloader取出一个batch的干净图像和标签
- 使用当前模型(权重冻结状态下)生成这批图像的对抗样本
- 将对抗样本和原始干净图像混合(或直接用全部对抗样本)
- 计算混合样本的损失,反向传播更新模型权重
需要注意,第二步生成对抗样本时,梯度反向传播必须止步于对抗样本本身。更直白地说,对抗样本应当被视为与原始图像地位相同的"训练数据",而不是从模型输入到模型输出的一条可微路径。如果忘了这一步,梯度会经过对抗样本的生成过程回传到模型参数上,导致训练极其不稳定,损失值会剧烈震荡,模型几乎不收敛。
具体来说,在PGD生成对抗样本的过程中,每次迭代后都要对更新得到的图像调用.detach(),切断计算图。FGSM攻击中因为只有一次梯度更新,也需要在backward之后对结果做detach。当然,也可以用PyTorch提供的torch.no_grad()上下文管理器来计算对抗样本,这样更保险,缺点是会多占一点内存。
我采用的是经典的混合训练策略,每个batch内一半是干净样本,一半是PGD生成的对抗样本。这个策略的优点是能同时优化干净样本精度和对抗鲁棒性,避免"为了防御丢了精度"。实际实验下来,纯对抗训练(全batch都是对抗样本)在Fashion_MNIST上干净准确率会从92%掉到85%左右,而混合训练能控制在89%以上,同时鲁棒性几乎不损失。对于上线业务模型来说,这少掉的几个百分点往往是不可接受的。
3.3 关键超参设置
对抗训练的两个核心超参数是扰动强度epsilon和PGD的迭代次数/步长。我第一次做这个实验的时候,凭感觉把epsilon设成了0.3,结果对抗训练后的模型对干净样本的准确率直接掉到75%,惨不忍睹。
节奏应该是这样的:Fashion_MNIST图像像素范围是[0,1],每个像素的扰动上限设置为0.1是一个比较合理的起点。0.1的扰动相当于灰度图中每个像素点最多改变25.5个灰度级,肉眼看起来会有轻微噪点感,但确实不影响人类判断这张图是T恤还是牛仔裤。如果用0.3的扰动,图像会出现明显的噪点颗粒,很多样本已经能被人眼看出变化,这就违背了对抗样本"人眼不可辨"的初衷。
对抗训练阶段,我推荐以下参数组合:
| 参数 | 数值 | 说明 |
|---|---|---|
| epsilon | 0.1 | 扰动预算,视视觉效果随时调整 |
| alpha | 0.01 | PGD每次迭代的步长 |
| num_iter | 10 | PGD迭代次数,对抗训练用10次足够 |
| batch_size | 128 | 混合训练时实际生效的batch大小 |
| 学习率 | 0.001 | Adam优化器,建议配合学习率衰减 |
训练迭代次数上,对抗训练的收敛速度比普通训练慢不少——我在普通训练里16个epoch就能到92%,对抗训练跑30个epoch才稳定在89%左右。不要过早停止训练,观察loss曲线如果还在下降就继续跑,这是对抗训练的正常节奏。
4. 实操过程与结果分析
4.1 攻击实验流程
整个实验的主线可以用一个脚本串起来:先训练(或加载)一个普通模型,接着分别用FGSM和PGD攻击它,记录攻击成功率;然后用对抗训练再训练一个模型,再用同样的攻击算法去攻击这个防御后的模型,记录防御前后的数值变化。
攻击成功率是评估对抗攻击效果的核心指标,定义为:攻击后模型预测错误(且与原标签不同)的样本数占全部测试样本的比例。如果攻击后的预测结果恰好和一个错误标签相同,也算攻击成功;只有模型预测仍为原标签、或者被攻击的样本本身在原始模型上就预测错误(脏样本)时才不算成功。因此在评估前,要筛选出模型正确分类的测试子集,只在这个子集上度量攻击成功率,否则攻击者的目标(让正确样本被错分)会被脏样本稀释,数值看起来虚高。
我跑出来的结果大致如下:
| 模型类型 | 干净准确率 | FGSM攻击成功率(eps=0.1) | PGD攻击成功率(eps=0.1, iter=20) |
|---|---|---|---|
| 普通训练模型 | 92.3% | 61.7% | 83.4% |
| FGSM对抗训练模型 | 89.6% | 8.2% | 46.8% |
| PGD对抗训练模型 | 89.1% | 5.4% | 12.3% |
三行数据看下来信息量很大。普通模型在FGSM面前已经不堪一击,61.7%的攻击成功率意味着攻击者只要在图上加一点点噪声,就能轻松让大半图像被错误分类。换更强的PGD攻击后,攻击成功率飙升到83.4%,模型几乎处于"裸奔"状态。
再看防御效果。只用FGSM做对抗训练,能有效防御FGSM(只有8.2%攻击成功率),在PGD面前却依旧脆弱——46.8%成功率说明PGD还是能轻松攻破。这个现象就是对抗训练圈子里的经典结论:用哪种攻击做训练,模型就主要对哪种攻击鲁棒,对未见过的新攻击模式泛化能力有限。改用PGD做对抗训练后,模型对FGSM和PGD的攻击成功率分别降到了5.4%和12.3%,说明PGD训练的鲁棒性泛化能力明显优于FGSM。
4.2 防御效果评估与可视化分析
对抗训练的另一个经典副产品是"鲁棒性-准确性折中":对抗训练后的模型,干净样本上的准确率一定会比普通训练模型低。我的实验数据里这个降低幅度约为3.2个百分点,在可接受范围内。如何判断这个折中是否划算,取决于业务场景:如果是无监督的图像检索、相似性匹配这类对准确率敏感但对安全要求不高的任务,或许没必要做对抗训练;但如果是直接面对恶意用户的人脸识别、验证码识别、OCR系统,多付出几个点的准确率换来安全性的提升,性价比很高。
可视化方面,我保存了几组典型对抗样本的对比图。普通模型下被成功攻击的对抗样本,肉眼看起来和原始图像几乎一致,但预测标签已经明显错了。比如一张本来清晰可辨的"T恤"图像,加上0.1强度的FGSM扰动后,模型可能以98%的置信度将其预测为"衬衫"。这种"肉眼无差别、模型大翻车"的案例,是给团队或客户解释对抗攻击概念时最有说服力的素材。
分析对抗样本的模式会发现一些有趣的特征。对Fashion_MNIST这类衣物数据集,FGSM生成的扰动通常集中在衣物轮廓边缘和纹理区域,而对大面积纯色区域(比如纯色T恤的胸腹部)几乎不加扰动。这说明模型的分类决策高度依赖边缘纹理特征,与人类更关注整体形状和结构形成鲜明对比,也解释了为什么模型对对抗扰动如此敏感——人类的视觉系统本身就具备良好的鲁棒性,而深度网络学到的特征表示与人眼的感知方式存在根本差异。
4.3 扰动强度对攻击效果的影响
epsilon是攻防双方最重要的博弈参数,我专门做了一组扫描实验,固定其他条件不变,把epsilon从0.01逐步调到0.3,观察攻击成功率的变化趋势。结果如下:
| epsilon | FGSM攻击成功率(%) | PGD攻击成功率(%) | 直观感受 |
|---|---|---|---|
| 0.01 | 12.4 | 28.6 | 肉眼完全不可见 |
| 0.03 | 33.1 | 56.2 | 肉眼几乎不可见 |
| 0.05 | 45.8 | 71.3 | 仔细看能发现有噪点 |
| 0.1 | 61.7 | 83.4 | 轻微可见噪点 |
| 0.2 | 78.2 | 93.1 | 明显噪点但可辨认 |
| 0.3 | 89.5 | 96.8 | 噪点明显,部分图失真 |
从数据可以清晰看到攻击成功率随着epsilon的增长而快速上升,但这一上升趋势并非线性的。0.01到0.05区间增长最陡峭,说明模型的决策边界在这个扰动范围附近非常密集,甚至很小的扰动就能越过边界;0.2以后增长放缓,因为很多样本需要的"推翻预测"的扰动阈值已经达到饱和。
有趣的是,在极小的epsilon(0.01)下PGD攻击成功率已经达到28.6%,说明模型在距离决策边界如此之近的地方仍有大量样本,这些样本天然就是"准对抗样本",即使没有任何攻击,在真实部署中遇到一点点传感器噪声或图像压缩误差也可能导致错误分类。这也提醒我们:对抗攻击不仅是安全威胁,更是模型决策边界几何特性的一种指示器——边界附近的样本密度越高,模型的泛化能力就越差。
5. 常见问题与排查技巧实录
5.1 训练不收敛或震荡明显
这是对抗训练最常见的问题,我做实验的过程中也踩过几次坑,排查优先级上建议按这个顺序来:
先检查对抗样本生成过程是否错误地参与了梯度回传。一个简单的验证方法是在训练循环里打印对抗样本生成前后模型的梯度范数,如果对抗样本生成阶段就出现了巨大的非零梯度,多半是detach忘写了。再检查epsilon设置是否过大,如果对干净数据本身都形成了肉眼可见的噪声干扰,模型在这种"高质量噪声数据"上训练,很难学到有意义的特征。最后检查数据预处理是否一致——训练时做了归一化但攻击时忘了,或反过来,都会让损失函数波动异常。
还有一个小细节是优化器参数的调整。对抗训练的损失面和普通训练完全不同,更崎岖、更多局部极值,直接沿用普通训练的默认学习率往往导致收敛缓慢或者震荡。试下来将学习率从0.001调整为0.0003左右,配合CosineAnnealing学习率调度,训练稳定度会有明显提升。
5.2 攻击成功率计算的三个常见误区
攻击成功率的计算看似简单,但我在实验早期统计出的数据和直觉有明显偏差,排查后发现问题出在定义和预处理上。
第一个误区是没有剔除"原始模型预测错误的样本"。攻击是对"正确预测"发起挑战,如果样本在原始模型上就已经被预测错误了,攻击成功率统计里混入这些样本,数值会虚高。我在官方测试集上先筛选出模型正确分类的样本,再在这部分样本上评估攻击成功率。
第二个误区是忘了对生成对抗样本做像素裁剪。FGSM和PGD在数学上允许像素超出[0,1]范围,但真实图像不存在这种取值。如果不做torch.clamp,扰动叠加后可能出现某些像素值跑到2.0或者-0.5,攻击落点早已越过违规区域,无法代表真实攻击效果。
第三个误区是忽略了数据集加载时随机resize或数据增强的影响。Fashion_MNIST虽然基本都是28x28原图,但如果你在DataLoader里加了RandomHorizontalFlip之类的操作,训练和攻击时的数据分布就会不一致,自然会导致评估结果失真。做对抗攻防实验时,攻击评估阶段务必关闭所有数据增强。
5.3 白盒攻击和黑盒攻击的差异认知
最后想澄清一个概念误区。我项目里使用的FGSM和PGD都属于白盒攻击——攻击者可以获取模型的梯度信息。现实中攻击者往往没有模型参数的访问权限,只能通过API查询结果来构造攻击,这类黑盒攻击通常使用迁移攻击(用本地替代模型生成对抗样本去攻击目标模型)或基于查询的零阶优化方法。黑盒攻击的效率远低于白盒攻击,但对现实系统的威胁其实更大,因为真实场景中很难做到模型参数的完全保密。
不过白盒攻击仍然有不可替代的价值——它相当于一个模型鲁棒性的下限测试。如果一个模型连白盒攻击都防不住,黑盒攻击更是防不胜防;反过来,如果白盒攻击(尤其是PGD)攻击成功率已经压得很低,模型对黑盒攻击的防御能力通常也有了坚实保障。正因为这个性质,PGD攻击成功率目前被学术界和工业界广泛用作衡量模型鲁棒性的标准试验——虽然PGD还不是万能的(理论上存在更强的自适应攻击),但它已经是性价比最高的鲁棒性度量基准了。
我在实际实验中感受最深的一点:对抗攻防本质上是一场永无止境的军备竞赛,今天有效的防御手段,明天可能被新的攻击算法轻易击穿。但这个项目让我把攻击和防御的基本功扎扎实实地过了一遍——理解梯度的几何含义、理解决策边界的脆弱性、理解训练范式的微妙权衡。如果你也想验证自己训练的图像分类模型到底有多"结实",建议从这个实验入手,跑完一遍再回头看看自己业务里的模型,一定会有全新的认识。
本文还有配套的精品资源,点击获取