☰
白盒对抗攻击实践:从FGSM到PGD的PyTorch实现与防御指南
2026/10/21 5:50:36 网站建设 项目流程

1. 对抗攻击是什么:先搞清楚白盒与黑盒的分野

做AI安全这块时间久了,我发现一个挺有意思的现象:很多人一听到“对抗攻击”就想到乱七八糟的对抗样本图片,以为这是黑客炫技,实际完全不是这么回事。对抗攻击是研究“模型为什么会被一小撮像素骗过”的科学,而白盒对抗攻击,恰恰是理解这个问题的敲门砖。

所谓白盒,字面意思是“透明的盒子”。在白盒场景下,攻击者知道模型的全部参数、梯度信息、网络结构,甚至知道训练数据的分布。这不只是考试开卷,简直是直接把出题老师的脑子拿过来了。与之对应的是黑盒攻击,攻击者只能靠输入输出猜测模型行为,像隔着墙听声辨位。白盒攻击的价值在于两点:一是用来做安全评估的“上限”实验,如果一个模型连白盒攻击都扛不住,那黑盒攻击基本也扛不住;二是研究对抗样本的可迁移性——在白盒模型上生成的对抗样本,往往也能骗过其他黑盒模型,这也是对抗攻击真正可怕的地方。

我最早接触这块时,导师反复强调一句话:想做好防御,先学会攻击。这句话我后来做安全评估项目时越来越认同。白盒对抗攻击本质上是理解深度学习模型决策边界的一次解剖实验,它不是“怎么搞破坏”的教程,而是“怎么找漏洞”的方法论。这篇文章就从一个最简单的实践切入,带你把白盒攻击的原理、公式、代码、调试全部过一遍。

适合谁看?对AI安全感兴趣的学生、刚入门的算法工程师、做模型鲁棒性评估的测试人员。不需要太高深的基础,会用PyTorch、懂一点梯度概念就行。

2. 动手之前:环境准备与基准模型搭建

2.1 核心工具选型:为什么用PyTorch

白盒对抗攻击的核心操作是计算梯度、修改输入、再次前向传播。这个场景下,PyTorch的动态计算图优势非常明显——你可以像写普通Python代码一样修改输入张量,然后随时调用loss.backward()拿到梯度,不需要重新构建整个图。TensorFlow当然也能做,但早期版本的静态图机制在这种需要反复迭代修改输入的实验里,写起来会绕很多弯路。

我的建议是用PyTorch 2.x版本,搭配torchvision自带的预训练模型和数据集,能省掉大量预处理时间。另外需要准备好numpy和matplotlib,前者做张量运算辅助,后者用来可视化对抗样本和噪声。

2.2 完成一个基准图像分类模型

对抗攻击不能空对空,必须有一个被攻击的目标模型。我在本地实验时用的是MNIST数据集训练的一个简单卷积网络,结构大致是:两个卷积层加两个全连接层,激活函数用ReLU,池化层用MaxPooling。这个模型在MNIST测试集上准确率能到99%左右,用来做攻击实验绰绰有余。

训练过程不细说,就是标准的交叉熵损失加Adam优化器。有一点值得提醒:模型的参数保存一定要用model.state_dict()保存,负载时也要先实例化模型再load_state_dict(),我见过太多人在这一步踩坑,加载完参数对不上,最后莫名其妙地复现不出攻击效果。

关键参数记录如下:

  • 输入尺寸:28x28灰度图
  • 训练轮数:5轮
  • 优化器:Adam,学习率0.001
  • Batch Size:64
  • 测试准确率:98.9%

有了这个准确率接近99%的模型,接下来生成的对抗样本才会显得格外“有意思”——一个原本几乎不会出错的模型,居然可以被肉眼几乎看不出的噪声瞬间击穿。

3. 经典白盒攻击算法解析:从FGSM到PGD

3.1 FGSM:快速梯度符号法的直觉推导

FGSM(Fast Gradient Sign Method)是白盒攻击里最早、也最基础的方法,由Goodfellow等人在2015年提出。它的核心思想异常简单:既然模型沿着梯度方向更新参数会减少损失,那我们就沿着梯度方向修改输入,让损失变大。

形式上,FGSM生成的对抗样本是这样的:

x_adv = x + ε * sign(∇_x J(θ, x, y))

其中x是原始输入,y是真实标签,J是损失函数,∇_x是损失对输入的梯度。sign()函数把梯度取符号,输出为+1或-1,保证了每一步修改的幅度是有界的,再由ε控制整体扰动强度。

我第一眼看着这个公式觉得平平无奇,但实际上手做实验才体会到它的精妙。假设有一个28x28像素的图片,ε取0.3,那平均每个像素最多修改0.3个灰度值(灰度范围0-1)。这样小的改动,人眼几乎察觉不到,却足以让原本预测正确的模型产生完全错误的判断。为什么一点点方向一致的扰动就能改变结果?因为深度模型的决策边界在高维空间中非常“曲折”,输入空间维度极高,只要在所有维度上同时朝着增大损失的方向推动一点点,累积起来就可能跨越决策边界。

当年做这个实验时,我脑子里冒出一个类比:一个站在悬崖边的人,你推他一个微妙的角度,他重心就歪了。单个维度上的改变微不足道,但多个维度同时向坏的方向偏,整个人的状态就变了。FGSM正是这个“向坏的方向推”的数学版本。

3.2 PGD:迭代攻击为什么比单步更狠

FGSM一步到位,简单粗暴,但它有个明显的问题:一次性走一步,很容易走过头或者卡在错误的局部方向。PGD(Projected Gradient Descent,投影梯度下降)是FGSM的迭代升级版,它的思路是:小步多次,每一步都重新计算梯度,并且在每一步之后把扰动“投影”回允许的范围内。

PGD的计算流程可以归纳为三步循环:

  • 计算当前对抗样本的梯度方向
  • 沿梯度方向前进一步,步长为α
  • 把输入裁剪回原始样本的ε邻域内,保证扰动不可感知

这个“裁剪”操作是关键,很多人不注意。它确保无论迭代多少次,最终的对抗样本都不会偏离原始样本太远。投影操作保证攻击强度始终受限,而迭代操作保证攻击方向不断修正,不会像FGSM那样一次性冲过头。

实验数据最能说明差距:同样的模型,在ε=0.3下,FGSM能让测试准确率从98.9%掉到25%左右,而PGD迭代30轮后准确率能直接跌到3%以下。这个对比让我第一次直观感受到迭代攻击的威力。业内经常说PGD是最强的一阶攻击(在没有强先验知识的情况下),对抗训练也往往用PGD作为攻击基准来评估鲁棒性,就是这个原因。

4. 白盒攻击代码实操:用PyTorch复现FGSM

4.1 攻击函数的核心实现

动手写代码之前,先把逻辑理清。FGSM攻击函数需要四样东西:模型、原始图片、真实标签、扰动强度ε。流程就是开启梯度跟踪、前向传播算损失、反向传播拿梯度、用梯度符号更新图片。

我写的核心函数如下,商业项目里我一般会再封装一层,但学习实验里清晰易懂更重要。

import torch import torch.nn.functional as F def fgsm_attack(model, images, labels, eps): images.requires_grad = True outputs = model(images) loss = F.cross_entropy(outputs, labels) model.zero_grad() loss.backward() data_grad = images.grad.data sign_data_grad = data_grad.sign() adv_images = images + eps * sign_data_grad adv_images = torch.clamp(adv_images, 0, 1) return adv_images

这里唯一要提醒的是torch.clamp(adv_images, 0, 1)这一行,它把像素值限制在合法范围内。因为MNIST图片预处理后像素值在0到1之间,如果不做裁剪,生成的图片可能会出现超出范围的值,显示出来就是奇怪的噪声。

4.2 攻击效果评估与可视化对比

攻击完了不能光看热闹,要用数据说话。我写了一段测试代码,从MNIST测试集里随机取一批样本,先记录模型的原始预测准确率,再对每个样本执行FGSM攻击,统计攻击后准确率。

实际跑下来的结果非常有代表性。取1000张测试图片,原始准确率99.1%,ε=0.05时攻击后准确率还能维持在95%左右,ε=0.15时掉到60%左右,到ε=0.3时只剩25%。把不同ε下的准确率画成折线图,会得到一个非常陡峭的下降曲线——这就是模型脆弱性的直观展示。

可视化对抗样本时,我会把原始图片、对抗样本、以及两者的差(放大后的噪声)三张图并排显示。噪声图放大后可以看到,FGSM产生的扰动并不是随机的雪花点,而是有明显的结构纹理,某些边缘区域的改动尤其剧烈。这说明模型在做决策时,对某些特定的局部特征非常敏感,攻击者只要精确打击这些特征,就能实现“四两拨千斤”的效果。

踩过的坑在这里同步一下:可视化之前记得把张量从GPU拷回CPU,并转换成numpy数组。matplotlib默认显示范围是0到1,如果你的图片是-1到1的范围,记得做归一到0-1,否则图会是花的。

5. 实战进阶:PGD迭代攻击完整流程

5.1 PGD的逐轮迭代与扰动裁剪

PGD的实现和FGSM差别不大,核心改动是把“一步走完”变成“循环多步”。每一步都用当前对抗样本重新计算梯度,然后沿梯度方向前进,最后裁剪回原始样本的ε邻域。

我的实现如下:

def pgd_attack(model, images, labels, eps, alpha=0.01, iters=40): ori_images = images.clone().detach() adv_images = images.clone().detach() adv_images.requires_grad = True for _ in range(iters): outputs = model(adv_images) loss = F.cross_entropy(outputs, labels) model.zero_grad() loss.backward() data_grad = adv_images.grad.data sign_data_grad = data_grad.sign() adv_images = adv_images + alpha * sign_data_grad # 裁剪到合法像素范围 adv_images = torch.clamp(adv_images, 0, 1) # 投影回原始样本的eps邻域 perturbation = torch.clamp(adv_images - ori_images, -eps, eps) adv_images = ori_images + perturbation # 重新裁剪一次,确保像素合法 adv_images = torch.clamp(adv_images, 0, 1) # 清零梯度,准备下一步 adv_images = adv_images.detach().clone().requires_grad_(True) return adv_images

每一步迭代后要重新设置requires_grad=True,这一步特别容易被遗忘。因为detach()之后梯度信息会丢失,下一轮迭代没办法计算新的梯度,结果就是程序报错或者梯度一直是同一个方向。

alpha(步长)和iters(迭代次数)的搭配是个调参的经验活。alpha太大容易震荡,alpha太小迭代次数得跟着加大。我常用的组合是alpha=0.01,iters=40,对MNIST来说攻击效果已经足够稳定。如果想在CIFAR-10上做实验,一般会用alpha=0.003,iters=20左右,因为CIFAR图像的像素信息更复杂,扰动幅度控制要更精细。

5.2 PGD与FGSM的对比实验记录

我在同样的测试集上分别执行FGSM和PGD,得到了一组直观的对比数据。

攻击方法ε=0.05ε=0.15ε=0.30
无攻击基准98.9%98.9%98.9%
FGSM94.7%61.3%25.1%
PGD (40步)72.4%8.2%1.3%

这组数据让我彻底理解了为什么学术界说FGSM已经不够看了。同样的扰动预算下,PGD的杀伤力强了一个数量级。原因其实不复杂:FGSM的梯度方向是“一步到位”的近似,而PGD通过多次小而准的修正,能更精确地逼近模型决策边界。这就好比你要翻过一座墙,FGSM是闭着眼全力一跳,PGD则是先找支点、再攀岩、步步为营——后者成功率当然高得多。

实际业务里做安全评估时,我通常会先用FGSM快速扫描一遍,如果发现FGSM就能打穿的模型,直接就可以下结论“存在严重鲁棒性问题”;如果FGSM打不动,再上PGD做深度测试。这样的流程能节省不少时间。

5.3 攻击成功后如何量化鲁棒性指标

单纯的准确率跌落只是个开始,安全评估里更看重一个指标叫鲁棒性准确率(Robust Accuracy),即模型在攻击下依然保持正确预测的比例。另一个有用的是平均扰动强度测量:用二分法找到让每个样本攻击成功所需的最小ε值,这个值越小,说明模型越脆弱。

我做过一次小规模实验:对某个训练好的模型,统计所有测试样本的攻击成功所需最小ε,得到的结果是一半以上的样本只需要ε=0.05左右就能被打穿。这意味着什么?意味着模型对微小扰动的抵抗力非常弱。普通的图像压缩、调亮度这类正常操作,可能无意中产生的改动都比这个扰动大,模型的稳定性让人担忧。

6. 白盒攻击的背后:梯度泄露与模型解释性

6.1 为什么白盒攻击能揭示模型决策依据

对抗攻击不是纯破坏行为,它更像一种探测手段。白盒攻击利用梯度信息来寻找模型最敏感的输入方向,这个过程实际上揭示了模型在做决策时到底“看”哪里。

我拿一张数字“7”的图片做实验,生成对抗样本后把噪声放大十倍的图打印出来,肉眼可以看到噪声主要集中在笔画的拐角处和端点位置。这说明模型判断“这是7”时,主要依据的是这些关键形状特征。攻击者不用了解模型内部结构,只要通过梯度方向就能反向推断出模型的注意力区域。

这个性质在模型可解释性研究里很有价值。传统的注意力热力图(如Grad-CAM)需要修改网络结构或钩子函数,而在白盒攻击中,梯度符号天然携带了每个像素对决策的影响方向,稍加处理后就能当热力图用。虽然还不够精细,但作为快速检查工具非常顺手。

6.2 梯度掩盖现象:为什么有些攻击会意外失效

实战中经常遇到一种奇怪现象:明明用的梯度信息是对的,攻击偏偏失败。这时候十有八九是遇到了梯度掩盖(Gradient Masking)。某些模型为了避免被攻击,故意使用不可微的操作(比如最近邻分类、argmax式输出),或者对输入做了大量非线性变换,导致梯度方向不再有参考价值。

学术界有一个经典案例:某防御模型声称对各类攻击都有很强的鲁棒性,结果后来被证明是因为它使用了梯度遮盖技巧,实际上模型本身安全边际很弱,换一种不受梯度影响的黑盒攻击后照样被秒杀。搞白盒攻击时如果发现FGSM和PGD都打不动某个模型,我第一反应不是庆幸模型很安全,反而会怀疑它是不是做了梯度掩盖。

处理办法是改用基于决策边界的攻击方法(比如边界攻击Boundary Attack)来验证,这类方法完全不依赖梯度,纯粹靠查询输入输出,虽然速度慢,但结果更可信。这个经验帮我避开过好几次“假鲁棒性”的坑。

7. 一个完整的攻击评估流程与源码思路

7.1 从测试集采样到报告生成

安全评估不是攻击完就结束的,最终要交付一份可读性强的报告。我通常把流程拆成五个阶段:数据准备、模型加载、攻击执行、指标统计、可视化落盘。

数据准备阶段随机固定随机种子,保证结果可复现。模型加载阶段我习惯输出每一层的参数规模和占用显存,方便对比不同模型的复杂度。攻击执行阶段顺序跑FGSM和PGD,分别记录准确率、平均置信度、扰动L2范数等指标。指标统计阶段算出一个综合脆弱性评分,用表格对比。可视化落盘阶段把随机抽取的成功样本和失败样本拼成对比图,存成PNG文件。

这个流程执行一次大概需要几分钟,具体看模型规模和迭代次数。写成自动化脚本后,一个模型一套攻击的测试报告几乎可以一键生成。

7.2 关键参数调优与固定随机种子的重要性

白盒攻击实验里,参数设置的微小差异会导致结果大相径庭。我总结出几个必须固定和调整的地方:

  • 固定随机种子(比如torch.manual_seed(0)和numpy.random.seed(0)),否则每次攻击结果都不同,没法复现实验
  • 确定ε的搜索区间,我一般从0.01开始,按0.05步长递增到0.3
  • PGD迭代步长alpha与迭代次数iters的比值要控制在一个合理范围,alpha * iters大约是ε的2到3倍时效果最好,太大容易震荡,太小又走不到边界
  • 攻击过程中模型的model.eval()模式必须打开,BatchNorm和Dropout在训练和推理模式下行为完全不一样,忘了切模式的话结果可能误导

这些细节单独看都不起眼,组合到一起就是实验成败的分水岭。我见过不少人复现论文攻击时效果对不上,最后排查一圈发现是模型没切到eval()模式。

7.3 防御方视角:对抗训练的简单实现

文章最后,不聊攻击聊防御。既然知道攻击的原理是梯度方向,那防御的核心思路就清晰了:如果模型对梯度方向的扰动不敏感,攻击自然失效。对抗训练就是把对抗样本混进训练集,让模型在训练阶段就见过这些“陷阱”,从而学会抵抗。

最简单的实现是每个batch先做一次FGSM攻击,把生成的对抗样本和原始样本一起送入模型训练。代码框架大概是:

for images, labels in train_loader: adv_images = fgsm_attack(model, images, labels, eps=0.1) combined_images = torch.cat([images, adv_images], dim=0) combined_labels = torch.cat([labels, labels], dim=0) outputs = model(combined_images) loss = F.cross_entropy(outputs, combined_labels) optimizer.zero_grad() loss.backward() optimizer.step()

这个简单的对抗训练代码,训练5轮之后,同样ε=0.3的FGSM攻击,准确率从25%恢复到85%以上。当然代价是模型在干净样本上的准确率会下降一些,这是一个典型的鲁棒性与准确性权衡。实际业务里怎么平衡,要看场景对安全性的要求有多高。总的来说,对抗训练目前依然是最实用的防御手段之一,但它绝不是万能的,新攻击方法层出不穷,防御也永远是一场持续的攻防拉锯。

8. 常见问题与排查速查表

这块内容是纯经验分享,基本都是在代码和实验中被虐出来的。

8.1 攻击不生效的五大原因

症状可能原因解决思路
攻击后准确率几乎不降模型处于train模式,BatchNorm统计量在变攻击前务必调用model.eval()
生成的对抗样本全是噪声ε设置过大,扰动已经完全覆盖原始语义调小ε,尝试从0.01开始
梯度全为零输入张量没有设置requires_grad检查输入张量是否requires_grad=True
同一个样本每次攻击结果不同未固定随机种子初始化时固定torch与numpy的seed
换了数据集攻击效果变差像素范围未合理归一化确认输入图片的数值范围,统一归一化方式

第一行的问题我犯过不止一次,模型忘了切到eval()模式,测试集上BatchNorm用了batch内统计量而不是全局统计量,导致模型行为完全不是预期的样子,攻击效果自然一塌糊涂。

8.2 安全发布对抗样本的合规要点

最后说一个稍微有点边界性质的话题。做对抗攻击研究很容易拿到一批攻击成功的样本,很多人喜欢把样本和模型的错误预测截图发到社交平台,这里其实有个度。学术讨论和安全研究没问题,但要注意两点:第一,不要公开真实生产环境的具体模型结构和参数,暴露细节等于把后门钥匙交给恶意攻击者;第二,不要针对特定个人或组织的真实系统发起攻击演示。安全研究的目的是发现问题和修复问题,不是为了羞辱某个系统或者展示破坏力。

我在项目里做安全评估时,报告通常只包含模型类型(比如“某图像分类模型”)、攻击方法、准确率数据、修复建议,绝不包含具体系统部署地址和可复现的完整代码路径。这些内容只对内部安全团队可见。这个原则我觉得比技术本身更重要。

做白盒对抗攻击实践写到这里,我最后想说的是:抛开代码和公式,这项技术最迷人之处在于它让我们学会了“像攻击者一样思考”。作为开发者和研究者,如果永远只站在构建方的角度看模型,很多安全漏洞会在不知不自觉中埋下隐患。而一旦你亲自动手成功“打穿”过一次自己训练的模型,那种对深度学习脆弱性的敬畏感,会让你在后续设计系统时多想一层防护。我的经验是,每隔一段时间就把最新攻击方法跑一遍自己之前的模型,迭代几次之后,模型的鲁棒性确实肉眼可见地变好了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询