对抗攻击实操入门(一):从零搭建环境并复现 FGSM 与 PGD 攻击(超详细)
前言最近在搞自动驾驶感知安全方向想开个新坑分享一下。网上讲 FGSM、PGD 的文章一大堆但多数停在贴公式真正从建环境到跑出图的保姆级教程很少。所以这个系列从最基础的 FGSM 和 PGD 开始一步一步实操。为什么要先讲这俩。因为后面要写的所有东西NDSS 2025 打商业交通标志识别的、USENIX 2024 用 LiDAR 让自动驾驶撞车的、CVPR 2025 同时骗摄像头和雷达的底层全是 FGSM 和 PGD 的变种。这俩不搞懂后面没法看。本文开始介绍从零搭建环境完整复现 FGSM 和 PGD 两个攻击。环境配置到代码运行一步一步来踩坑都在正文里标出来了。系列前文链接这是第一篇后续补。1 对抗样本是什么2015 年 Goodfellow 等人在 ICLR 论文《Explaining and Harnessing Adversarial Examples》https://arxiv.org/abs/1412.6572里给出了一个经典案例直接引爆了整个领域。图分三块。左边是原始输入一张熊猫图片GoogLeNet 给出 panda 置信度 57.7%。中间那团彩色噪点叫扰动它是损失函数对输入求梯度再取符号乘上一个小系数得到的扰动量极小。把中间这层加到左边的熊猫上得到右边那张图人眼看还是熊猫模型却给出 gibbon 长臂猿置信度飙到 99.3%。核心结论。神经网络在高维空间里的决策边界和人眼感知存在系统性偏差。在输入空间施加范数有界的人眼不可见扰动就能让模型预测结果发生任意翻转。这个性质叫对抗脆弱性adversarial vulnerability是后面所有攻击方法的物理前提。自动驾驶领域的对抗贴片、LiDAR 欺骗、3D 对抗物体本质上都是在不同模态、不同物理约束下构造这种扰动。2 环境搭建从头开始注意注意以下操作建议在 Ubuntu 20.04 或 Windows Anaconda 下完成Python 版本用 3.10没 NVIDIA 显卡也能跑就是慢点。2.1 创建 conda 环境打开终端输入以下命令创建一个新的 conda 环境。conda create-nadv_attackpython3.10-y激活环境。conda activate adv_attack建议大家环境名字保持一致后面排查问题方便对照。2.2 安装 PyTorchPyTorch 版本很关键建议装 2.0 以上。有 NVIDIA 显卡的装 CUDA 版没有的装 CPU 版。有显卡的终端输入nvidia-smi能看到显卡信息输入以下命令。condainstallpytorch torchvision pytorch-cuda11.8-cpytorch-cnvidia-y没有显卡或者不想用 GPU 的装 CPU 版。condainstallpytorch torchvision cpuonly-cpytorch-y装完之后验证一下在终端输入。python-cimport torch; print(torch.__version__); print(CUDA:, torch.cuda.is_available())正常的话会打印类似下面这样。2.1.0 CUDA: True如果CUDA 显示 False说明 GPU 没用上。这不会报错代码也能跑就是慢。排查的话一般是 CUDA 版本和显卡驱动对不上先看nvidia-smi右上角的 CUDA Version对应着装。2.3 安装其他依赖还需要 matplotlib 画图numpy 做数值计算。pipinstallmatplotlib numpy2.4 准备数据集本文用的是CIFAR-10 数据集https://www.cs.toronto.edu/~kriz/cifar.htmlPyTorch 会自动下载。第一次运行代码的时候会下载大概 170MB存到当前目录的data文件夹下。注意国内下载 CIFAR 偶尔会失败多试几次或者挂梯子。下载成功一次以后就不用再下了。3 FGSM 和 PGD 原理3.1 FGSM 原理FGSM 全称 Fast Gradient Sign Method快速梯度符号法Goodfellow 等人 2015 年提出。核心公式。x a d v x ϵ ⋅ sign ( ∇ x L ( θ , x , y ) ) x_{adv} x \epsilon \cdot \text{sign}(\nabla_x L(\theta, x, y))xadvxϵ⋅sign(∇xL(θ,x,y))这个公式里最关键的一点也是新手最容易搞错的就是这个梯度是对输入图片x xx求的不是对模型参数θ \thetaθ求的。正常训练神经网络的时候我们算的是∇ θ L \nabla_\theta L∇θL让参数沿负梯度方向走损失变小模型变聪明。FGSM 反过来参数不动让输入沿正梯度方向走一步损失变大模型预测就倾向于出错。FGSM 本质就是一句话。固定模型反向训练输入。公式里几个符号说明一下。x xx是原始输入图片∇ x L \nabla_x L∇xL是损失函数对输入图片的梯度sign ( ⋅ ) \text{sign}(\cdot)sign(⋅)是取符号函数每个元素变成 1 或 -1ϵ \epsilonϵ是扰动大小控制走多远为什么要取 sign 而不直接用梯度原值。因为取符号能让每个像素的扰动量都一样大等于ϵ \epsilonϵ这样扰动最均匀人眼不容易察觉。要是按梯度原值加有些像素会被改得很大一眼就看出来了。ϵ \epsilonϵ取多少。CIFAR 和 ImageNet 上常用ϵ 8 / 255 ≈ 0.031 \epsilon 8/255 \approx 0.031ϵ8/255≈0.031意思是每个像素最多变化 8 个灰度级0 到 255 范围内。这个数小到人眼分不出来但够把模型干翻。3.2 PGD 原理FGSM 有个硬伤它只走一步。一步走出去对不对全看运气。Madry 等人在 2018 年 ICLR 的论文《Towards Deep Learning Models Resistant to Adversarial Attacks》https://arxiv.org/abs/1706.06083里提出 PGD全称 Projected Gradient Descent投影梯度下降。PGD 相对 FGSM 多了三件事。(1) 走很多步。迭代 20 到 40 步每步走一个小一点的步长α \alphaα比如2 / 255 2/2552/255。(2) 随机起点。不从原图开始从原图附近随机一个点开始。这一步看着不起眼但对后面做对抗训练特别重要能找到更多局部最大值。(3) 每步投影回去。这是 PGD 的精髓。走多步有个问题走着走着总扰动超过ϵ \epsilonϵ了怎么办。PGD 每走一步都把当前点投影回以原图为中心、半径ϵ \epsilonϵ的框里保证不管走多少步总扰动永远不超过ϵ \epsilonϵ。Madry 那篇论文有个重要结论。在L ∞ L_\inftyL∞约束下PGD 找到的对抗样本基本就是模型能达到的局部最坏情况了。也就是说扛得住 PGD 的模型基本就扛得住所有一阶攻击。所以后来做鲁棒性的论文baseline 清一色 PGD 对抗训练。FGSM 和 PGD 的关系记一句就够了。FGSM 是 PGD 只走一步的特例PGD 是多步加投影的 FGSM。4 完整代码可直接复制运行先把完整代码放这里可以直接复制粘贴到一个文件里跑通。下面 5、6 节再逐段讲解。importtorchimporttorch.nnasnnimporttorch.nn.functionalasFimporttorchvisionimportmatplotlib.pyplotasplt# 0. 设置设备 devicecudaiftorch.cuda.is_available()elsecpuprint(f运行设备:{device})# 1. 加载模型 modeltorchvision.models.resnet18(weightsIMAGENET1K_V1)model.fcnn.Linear(512,10)# 改成 CIFAR-10 的 10 分类modelmodel.to(device).eval()# CIFAR-10 类别CIFAR_CLASSES[airplane,auto,bird,cat,deer,dog,frog,horse,ship,truck]# 2. 加载一张测试图片 transformtorchvision.transforms.ToTensor()cifartorchvision.datasets.CIFAR10(root./data,trainFalse,downloadTrue,transformtransform)img,labelcifar[0]imgimg.unsqueeze(0).to(device)labeltorch.tensor([label]).to(device)# 3. FGSM 攻击函数 deffgsm_attack(image,epsilon,data_grad):FGSM 单步攻击perturbed_imageimageepsilon*data_grad.sign()returntorch.clamp(perturbed_image,0,1)# 4. PGD 攻击函数 defpgd_attack(model,image,label,epsilon8/255,alpha2/255,steps20):PGD 多步投影攻击imageimage.clone().detach().to(device)labellabel.to(device)# 随机起点adv_imageimagetorch.empty_like(image).uniform_(-epsilon,epsilon)adv_imagetorch.clamp(adv_image,0,1).detach()foriinrange(steps):adv_image.requires_grad_(True)outputmodel(adv_image)lossF.cross_entropy(output,label)model.zero_grad()loss.backward()data_gradadv_image.grad.datawithtorch.no_grad():adv_imageadv_imagealpha*data_grad.sign()# 投影回 epsilon 球内deltatorch.clamp(adv_image-image,-epsilon,epsilon)adv_imagetorch.clamp(imagedelta,0,1).detach()returnadv_image# 5. 执行 FGSM 攻击 print(\n FGSM 攻击 )img.requires_grad_(True)outputmodel(img)lossF.cross_entropy(output,label)model.zero_grad()loss.backward()data_gradimg.grad.data adv_fgsmfgsm_attack(img,8/255,data_grad)withtorch.no_grad():clean_predmodel(img).argmax(1).item()fgsm_predmodel(adv_fgsm).argmax(1).item()clean_confF.softmax(model(img),dim1)[0,clean_pred].item()fgsm_confF.softmax(model(adv_fgsm),dim1)[0,fgsm_pred].item()print(f干净样本:{CIFAR_CLASSES[clean_pred]}({clean_conf:.3f}))print(fFGSM攻击后:{CIFAR_CLASSES[fgsm_pred]}({fgsm_conf:.3f}))# 6. 执行 PGD 攻击 print(\n PGD 攻击 )img2,label2cifar[0]img2img2.unsqueeze(0).to(device)label2torch.tensor([label2]).to(device)adv_pgdpgd_attack(model,img2,label2,epsilon8/255,alpha2/255,steps20)withtorch.no_grad():pgd_predmodel(adv_pgd).argmax(1).item()pgd_confF.softmax(model(adv_pgd),dim1)[0,pgd_pred].item()print(f干净样本:{CIFAR_CLASSES[clean_pred]}({clean_conf:.3f}))print(fPGD攻击后:{CIFAR_CLASSES[pgd_pred]}({pgd_conf:.3f}))# 7. 可视化 fig,axesplt.subplots(1,3,figsize(10,3.2))axes[0].imshow(img2.squeeze().permute(1,2,0).cpu().numpy())axes[0].set_title(fClean:{CIFAR_CLASSES[clean_pred]}({clean_conf:.2f}))axes[1].imshow(adv_pgd.squeeze().permute(1,2,0).cpu().numpy())axes[1].set_title(fAdv:{CIFAR_CLASSES[pgd_pred]}({pgd_conf:.2f}))perturbation(adv_pgd-img2).squeeze().permute(1,2,0).abs().cpu().numpy()axes[2].imshow(perturbation*50)axes[2].set_title(Perturbation x50)foraxinaxes:ax.axis(off)plt.tight_layout()plt.savefig(pgd_result.png,dpi120)print(\n图片已保存到 pgd_result.png)5 代码逐段讲解5.1 加载模型modeltorchvision.models.resnet18(weightsIMAGENET1K_V1)model.fcnn.Linear(512,10)modelmodel.to(device).eval()用 ImageNet 预训练的 ResNet18把最后一层全连接改成 CIFAR-10 的 10 分类。注意这里要用eval()切到测试模式不然 BatchNorm 和 Dropout 的行为不对攻击效果会受影响。5.2 FGSM 攻击函数deffgsm_attack(image,epsilon,data_grad):perturbed_imageimageepsilon*data_grad.sign()returntorch.clamp(perturbed_image,0,1)逻辑很简单沿梯度符号方向加扰动再用torch.clamp把像素值限制在 0 到 1 之间防止超出合法范围。5.3 PGD 攻击函数defpgd_attack(model,image,label,epsilon8/255,alpha2/255,steps20):imageimage.clone().detach().to(device)labellabel.to(device)adv_imageimagetorch.empty_like(image).uniform_(-epsilon,epsilon)adv_imagetorch.clamp(adv_image,0,1).detach()foriinrange(steps):adv_image.requires_grad_(True)outputmodel(adv_image)lossF.cross_entropy(output,label)model.zero_grad()loss.backward()data_gradadv_image.grad.datawithtorch.no_grad():adv_imageadv_imagealpha*data_grad.sign()deltatorch.clamp(adv_image-image,-epsilon,epsilon)adv_imagetorch.clamp(imagedelta,0,1).detach()returnadv_image逐行说明。(1)image.clone().detach()复制一份原图避免污染原始数据。(2) 随机起点在 epsilon 球内随机取一个点开始。(3) 每步迭代里先requires_grad_(True)让对抗图需要梯度前向算 loss反向求梯度。(4) 每步走完投影回 epsilon 球内限制总扰动不超过ϵ \epsilonϵ。注意注意每步迭代结束的.detach()不能省新手第一次写经常漏结果跑到第 5 步左右显存直接爆了。原因是 PyTorch 会把每一步的计算图都存着越堆越大最后 OOM。加上.detach()就是把计算图截断每步重新开始。5.4 执行攻击关键求梯度部分img.requires_grad_(True)outputmodel(img)lossF.cross_entropy(output,label)model.zero_grad()loss.backward()data_gradimg.grad.data adv_fgsmfgsm_attack(img,8/255,data_grad)注意注意loss.backward() 之前一定要 model.zero_grad() 清掉模型参数的梯度不然历史梯度会累积求出来的输入梯度是错的很多教程漏了这一步照着跑攻击成功率贼低怎么调都不对根本原因就在这。6 踩坑总结把正文里提到过的坑集中放一下方便排查。坑一 梯度方向算反了。攻击成功率贼低。原因要么是漏了model.zero_grad()要么是梯度对象搞错了。检查方法打印data_grad.abs().mean()如果接近 0基本就是梯度算错了。坑二 像素范围没统一。模型预处理是 0 到 1 还是 0 到 255 还是 ImageNet 标准化。三个混用是新手最常见的 bug。建议统一在0 到 1 空间做攻击。模型如果用了 ImageNet 标准化epsilon 也要换算到标准化后的空间换算公式是ϵ n o r m ϵ / σ \epsilon_{norm} \epsilon / \sigmaϵnormϵ/σ。坑三 detach 忘了加。PGD 多步迭代时每步末尾必须.detach()。漏了会导致计算图越堆越大第 5 步左右显存溢出。坑四 epsilon 设太大。新手为了攻击成功率高把ϵ \epsilonϵ设到 32/255 甚至更大扰动人眼已经能看见了这就不是对抗样本了是瞎改图。标准取值CIFAR 和 ImageNet 用 8/255MNIST 用 0.3。7 这俩跟自动驾驶什么关系FGSM 和 PGD 看着是数字空间攻击直接改图片像素离真实世界还远。但它定了三个贯穿整个领域的基础概念。第一对抗脆弱性。神经网络对输入的微小扰动极度敏感这是后面所有攻击的前提条件。第二梯度攻击。用损失对输入的梯度指导攻击CW、DeepFool、AutoAttack 全是这个套路。PGD 是其中最强的一阶攻击。第三L p L_pLp范数约束。限制扰动大小用L ∞ L_\inftyL∞、L 2 L_2L2、L 0 L_0L0这是衡量攻击强度的通用语言。后面这个系列要做的事就是把 PGD 这套思路搬到不同的物理载体上。物理世界攻击把扰动改造成能打印的贴片、能 3D 打印的物体。LiDAR 和点云攻击把像素扰动换成加点云点或删点云点。多模态融合攻击同时打摄像头和 LiDAR。FGSM 和 PGD 不是入门科普是后面所有论文的公共语言。做自动驾驶安全这关必须先过。8 下一步下一篇介绍物理世界对抗攻击的全景图从贴纸到激光器把能在真实世界打出来的攻击分类讲清楚。再后面精读 USENIX Security 2024 那篇用 LiDAR 让自动驾驶碰撞率到 63% 的论文。系列持续更新中有问题可以在评论区聊。参考文献(1) Goodfellow I, Shlens J, Szegedy C. Explaining and Harnessing Adversarial Examples. ICLR 2015. https://arxiv.org/abs/1412.6572(2) Madry A, Makelov A, Schmidt L, et al. Towards Deep Learning Models Resistant to Adversarial Attacks. ICLR 2018. https://arxiv.org/abs/1706.06083(3) Kurakin A, Goodfellow I, Bengio S. Adversarial Examples in the Physical World. ICLR 2017. https://arxiv.org/abs/1607.02533(4) Carlini N, Wagner D. Towards Evaluating the Robustness of Neural Networks. IEEE SP 2017(5) CIFAR-10 数据集。https://www.cs.toronto.edu/~kriz/cifar.html(6) PyTorch FGSM 官方教程。https://docs.pytorch.org/tutorials/beginner/fgsm_tutorial.html文中第一张图来源 Goodfellow et al. 2015 Figure 1经 TensorFlow 官方教程https://www.tensorflow.org/tutorials/generative/adversarial_fgsm复现CC BY 4.0。代码基于 PyTorch 官方 FGSM 教程改写Apache 2.0 协议。