第4章:对抗样本攻击:FGSM、PGD攻击原理与实操
对抗样本攻击,说白了就是给AI模型「下套」。
你想想看,一张熊猫的照片,人眼看还是熊猫,但模型却认成了长臂猿。为什么会这样?因为我们在图片上加了人眼几乎察觉不到的噪声。这就是对抗样本。
我个人习惯把对抗样本比作「视觉错觉」——只不过这次被骗的不是人眼,是神经网络。
4.1 对抗样本的核心思想
对抗样本的本质,是在输入数据上添加精心设计的微小扰动。这个扰动要满足两个条件:
- 足够小:人眼看不出来,或者不影响人类判断
- 足够有效:能让模型输出完全不同的结果
数学上可以这样理解:
x' = x + δ
其中:
x = 原始输入(比如一张猫的图片)
δ = 对抗扰动(人眼不可见的噪声)
x' = 对抗样本(模型会认成狗)
我在项目中遇到过最离谱的一次,是在人脸识别系统上。仅仅在图片上加了几个像素的噪声,系统就把A员工识别成了B员工。嗯,当时甲方爸爸的脸都绿了。
4.2 FGSM:快速梯度符号法
FGSM(Fast Gradient Sign Method)是最经典的对抗攻击方法之一。它的思路非常直接:
沿着梯度方向走一步,让损失函数最大化。
你想想看,模型训练时我们沿着梯度反方向走,让损失变小。那攻击时反过来,沿着梯度方向走,让损失变大,模型自然就认错了。
FGSM的公式很简单:
x' = x + ε · sign(∇x J(θ, x, y))
其中:
ε = 扰动强度(超参数,控制噪声大小)
sign() = 取符号函数(+1或-1)
∇x J = 损失函数对输入的梯度
这里有个关键点:为什么用sign()而不是直接用梯度?
我个人理解是,sign()让每个像素的修改幅度都一样,保证了扰动的一致性。如果直接用梯度,某些像素的改动会特别大,容易被发现。
FGSM的特点:
- 单步攻击:一次梯度计算就能生成对抗样本
- 速度快:适合大规模攻击场景
- 成功率有限:对于防御较强的模型效果一般
4.3 PGD:投影梯度下降
PGD(Projected Gradient Descent)可以理解为FGSM的「升级版」。它不满足于只走一步,而是走多步,每一步都做投影约束。
为什么需要多步?
我打个比方:FGSM像是闭着眼睛跳一步,跳到哪算哪。PGD则是睁着眼睛,一步一步试探着走,每一步都确保不超出「安全范围」。
PGD的迭代过程:
1. 从原始样本x开始
2. 重复以下步骤K次:
a. 计算当前点的梯度
b. 沿梯度方向走一步:x = x + α · sign(∇x J)
c. 投影回约束范围:x = clip(x, x_original - ε, x_original + ε)
3. 返回最终的x作为对抗样本
这里的clip操作就是「投影」——确保修改后的像素值不会偏离原始值太远。ε就是我们的「安全边界」。
避坑指南:
我曾经在调PGD参数时踩过坑。迭代次数K不是越大越好。K太大,对抗样本会过度拟合当前模型,迁移性反而变差。一般K取7-20步就够了。
4.4 使用CleverHans库实操
CleverHans是IBM开源的对抗样本库,封装了FGSM、PGD等主流攻击方法。我们直接上手试试。
安装CleverHans:
pip install cleverhans
完整代码示例:
import tensorflow as tf
import numpy as np
from cleverhans.tf2.attacks.fast_gradient_method import fast_gradient_method
from cleverhans.tf2.attacks.projected_gradient_descent import projected_gradient_descent
# 加载预训练模型(以MNIST为例)
model = tf.keras.models.load_model('mnist_model.h5')
# 准备一张测试图片
x_test = np.random.randn(1, 28, 28, 1).astype(np.float32)
y_test = np.array([[0, 1, 0, 0, 0, 0, 0, 0, 0, 0]]) # 假设真实标签是1
# === FGSM攻击 ===
eps = 0.3 # 扰动强度
x_adv_fgsm = fast_gradient_method(
model_fn=model,
x=x_test,
eps=eps,
norm=np.inf,
targeted=False
)
# 预测对抗样本
pred_fgsm = model.predict(x_adv_fgsm)
print(f"FGSM攻击后预测结果:{np.argmax(pred_fgsm)}")
# === PGD攻击 ===
x_adv_pgd = projected_gradient_descent(
model_fn=model,
x=x_test,
eps=eps,
eps_iter=0.01, # 每步的步长
nb_iter=40, # 迭代次数
norm=np.inf,
targeted=False
)
# 预测对抗样本
pred_pgd = model.predict(x_adv_pgd)
print(f"PGD攻击后预测结果:{np.argmax(pred_pgd)}")
重要提醒:
实际使用时,注意以下几点:
- eps值不要太大,否则人眼能看出明显噪声
- 对于彩色图片(RGB三通道),扰动要加到所有通道上
- CleverHans要求模型输出logits(未经过softmax),记得检查模型结构
4.5 FGSM vs PGD:对比总结
| 对比维度 | FGSM | PGD |
|---|---|---|
| 攻击步数 | 单步 | 多步迭代 |
| 攻击强度 | 较弱 | 强(一阶攻击的上限) |
| 计算开销 | 低 | 高(K倍于FGSM) |
| 迁移性 | 较好 | 较差(过拟合当前模型) |
| 适用场景 | 快速评估、大规模测试 | 对抗训练、鲁棒性评估 |
我个人建议:做渗透测试时,先用FGSM快速扫描一遍,找出明显脆弱的样本。再用PGD对重点样本做深度攻击。这样效率最高。
4.6 实战中的避坑指南
最后分享几个我踩过的坑:
- 归一化问题:CleverHans默认输入范围是[0,1],如果你的模型用的是[0,255]的输入,记得先归一化。我曾经因为这个原因,折腾了一下午才发现问题。
- 模型模式:生成对抗样本时,模型要设置为训练模式(启用Dropout等)。但预测时要用推理模式。这个细节很容易忽略。
- 随机性影响:同样的参数,每次运行结果可能不同。因为梯度计算有随机性。建议固定随机种子:
tf.random.set_seed(42)
嗯,对抗样本攻击这块,说白了就是「道高一尺,魔高一丈」的游戏。你学会了攻击,自然也要学会防御。下一章我们会讲对抗训练,那就是防守反击的招数了。