第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)

嗯,对抗样本攻击这块,说白了就是「道高一尺,魔高一丈」的游戏。你学会了攻击,自然也要学会防御。下一章我们会讲对抗训练,那就是防守反击的招数了。