FGSM对抗样本生成算法
- 一、理论部分
- 1.1 目标
- 1.2 数学公式
- 1.3 推导过程
- 1.4 直观解释
- 1.5 示例
- 1.6 总结
- 二、代码实现
- 2.1 导包
- 2.2 数据加载和处理
- 2.3 网络构建
- 2.4 模型加载
- 2.5 生成对抗样本
- 2.6 攻击测试
- 2.7 启动攻击
- 2.8 效果展示
一、理论部分
FGSM
(Fast Gradient Sign Method) 是一种经典的对抗样本生成方法
,其核心思想
是通过在输入数据的梯度方向上添加扰动,从而生成对抗样本
1.1 目标
给定一个输入样本 x x x 和对应的真实标签 y y y ,FGSM 的目标是生成一个对抗样本 x adv x_{\text{adv}} xadv ,使得:
- 对抗样本 x adv x_{\text{adv}} xadv 与原始样本 x x x 的差异很小(通常用 L ∞ L_\infty L∞ 范数衡量)
- 模型在对抗样本 x adv x_{\text{adv}} xadv 上的预测结果与真实标签 y y y 不一致。
1.2 数学公式
FGSM 的对抗样本生成公式: x adv = x + ϵ ⋅ sign ( ∇ x J ( x , y ) ) x_{\text{adv}} = x + \epsilon \cdot \text{sign}(\nabla_x J(x, y)) xadv=x+ϵ⋅sign(∇xJ(x,y))
其中:
- x x x:原始输入样本
- y y y:输入样本的真实标签
- ϵ \epsilon ϵ:扰动强度(一个小的正数,控制扰动的幅度)
- ∇ x J ( x , y ) \nabla_x J(x, y) ∇xJ(x,y):损失函数 J J J 对输入 x x x 的梯度
- sign ( ⋅ ) \text{sign}(\cdot) sign(⋅):符号函数,返回梯度的符号( + 1 +1 +1 或 − 1 -1 −1)
1.3 推导过程
(1)定义损失函数
假设模型的损失函数为 J ( x , y ) J(x, y) J(x,y),其中:
- x x x 是输入样本
- y y y 是真实标签
(2)计算梯度
计算损失函数 J ( x , y ) J(x, y) J(x,y) 对输入 x x x 的梯度: ∇ x J ( x , y ) \nabla_x J(x, y) ∇xJ(x,y)
梯度表示损失函数在输入空间中的变化方向
(3)生成对抗样本
为了最大化损失函数 J ( x , y ) J(x, y) J(x,y),FGSM 沿着梯度的符号方向扰动输入数据: x adv = x + ϵ ⋅ sign ( ∇ x J ( x , y ) ) x_{\text{adv}} = x + \epsilon \cdot \text{sign}(\nabla_x J(x, y)) xadv=x+ϵ⋅sign(∇xJ(x,y))
- sign ( ∇ x J ( x , y ) ) \text{sign}(\nabla_x J(x, y)) sign(∇xJ(x,y)):梯度的符号方向,表示损失函数增加最快的方向
- ϵ \epsilon ϵ:扰动强度,控制扰动的幅度
(4)限制扰动范围
为了确保对抗样本
x
adv
x_{\text{adv}}
xadv 与原始样本
x
x
x 的差异不会过大,通常会将对抗样本的像素值限制在合理范围内(例如
[
0
,
1
]
[0, 1]
[0,1]):
x
adv
=
clip
(
x
+
ϵ
⋅
sign
(
∇
x
J
(
x
,
y
)
)
,
0
,
1
)
x_{\text{adv}} = \text{clip}(x + \epsilon \cdot \text{sign}(\nabla_x J(x, y)), 0, 1)
xadv=clip(x+ϵ⋅sign(∇xJ(x,y)),0,1)
其中, clip ( ⋅ ) \text{clip}(\cdot) clip(⋅) 是裁剪函数,将像素值限制在 [ 0 , 1 ] [0, 1] [0,1] 范围内。
1.4 直观解释
- 梯度方向:梯度 ∇ x J ( x , y ) \nabla_x J(x, y) ∇xJ(x,y) 表示损失函数在输入空间中的变化方向。沿着梯度方向增加输入数据,可以最大化损失函数
- 符号函数:符号函数 sign ( ⋅ ) \text{sign}(\cdot) sign(⋅) 将梯度方向简化为 + 1 +1 +1 或 1 1 1,从而生成一个简单的扰动
- 扰动强度: ϵ \epsilon ϵ 控制扰动的幅度。 ϵ \epsilon ϵ 越大,扰动越明显,对抗样本的欺骗性越强
1.5 示例
假设:
- 输入样本 x x x 是一个图像
- 模型的损失函数是交叉熵损失 J ( x , y ) J(x, y) J(x,y)
- 扰动强度 ϵ = 0.03 \epsilon = 0.03 ϵ=0.03
步骤:
- 计算损失函数对输入的梯度: ∇ x J ( x , y ) \nabla_x J(x, y) ∇xJ(x,y)
- 获取梯度的符号方向: sign ( ∇ x J ( x , y ) ) \text{sign}(\nabla_x J(x, y)) sign(∇xJ(x,y))
- 生成对抗样本: x adv = x + 0.03 ⋅ sign ( ∇ x J ( x , y ) ) x_{\text{adv}} = x + 0.03 \cdot \text{sign}(\nabla_x J(x, y)) xadv=x+0.03⋅sign(∇xJ(x,y))
- 裁剪对抗样本的像素值到 [ 0 , 1 ] [0, 1] [0,1] 范围内
1.6 总结
FGSM 的数学公式可以总结为: x adv = clip ( x + ϵ ⋅ sign ( ∇ x J ( x , y ) ) , 0 , 1 ) x_{\text{adv}} = \text{clip}(x + \epsilon \cdot \text{sign}(\nabla_x J(x, y)), 0, 1) xadv=clip(x+ϵ⋅sign(∇xJ(x,y)),0,1)
- 目标:通过在输入数据的梯度方向上添加扰动,生成对抗样本
- 核心思想:沿着损失函数增加最快的方向扰动输入数据
- 优点:简单高效,计算速度快
- 缺点:生成的对抗样本可能不够鲁棒,容易被防御方法检测到
FGSM 是理解对抗样本生成的基础方法,后续的许多对抗攻击方法(如 PGD、CW 攻击等)都是基于 FGSM 的改进
二、代码实现
- 利用训练好的LeNet模型实现对抗攻击效果
- 可视化展示攻击效果
2.1 导包
import torch
import torch.nn as nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
2.2 数据加载和处理
# 加载 MNIST 数据集
def load_data(batch_size=64):
transform = transforms.Compose([
transforms.ToTensor(), # 将图像转换为张量
transforms.Normalize((0.5,), (0.5,)) # 归一化
])
# 下载训练集和测试集
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
# 创建 DataLoader
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=True)
return train_loader, test_loader
2.3 网络构建
- LeNet 的网络结构如下:
- 卷积层 1:输入通道 1,输出通道 6,卷积核大小 5x5
- 池化层 1:2x2 的最大池化
- 卷积层 2:输入通道 6,输出通道 16,卷积核大小 5x5。
- 池化层 2:2x2 的最大池化。
- 全连接层 1:输入 16x5x5,输出 120
- 全连接层 2:输入 120,输出 84
- 全连接层 3:输入 84,输出 10(对应 10 个类别)
#定义LeNet网络架构
class LeNet(nn.Module):
def __init__(self):
super(LeNet,self).__init__()
self.net=nn.Sequential(
#卷积层1
nn.Conv2d(in_channels=1,out_channels=6,kernel_size=5,stride=1,padding=2),nn.BatchNorm2d(6),nn.Sigmoid(),
nn.MaxPool2d(kernel_size=2,stride=2),
#卷积层2
nn.Conv2d(in_channels=6,out_channels=16,kernel_size=5,stride=1),nn.BatchNorm2d(16),nn.Sigmoid(),
nn.MaxPool2d(kernel_size=2,stride=2),
nn.Flatten(),
#全连接层1
nn.Linear(16*5*5,120),nn.BatchNorm1d(120),nn.Sigmoid(),
#全连接层2
nn.Linear(120,84),nn.BatchNorm1d(84),nn.Sigmoid(),
#全连接层3
nn.Linear(84,10)
)
def forward(self,X):
return self.net(X)
2.4 模型加载
def load_model(pre_model="./model/best_lenet_mnist.pth",device="cuda"):
model = LeNet()
model.load_state_dict(torch.load(pre_model, map_location=device,weights_only=True))
model.to(device)
model.eval() #评估模式
return model
2.5 生成对抗样本
# 定义 FGSM 攻击函数
def fgsm_attack(image, epsilon, data_grad):
"""
FGSM 攻击函数
:param image: 原始输入图像
:param epsilon: 扰动强度
:param data_grad: 输入图像的梯度
:return: 对抗样本
"""
# 获取梯度的符号方向
sign_data_grad = data_grad.sign()
# 生成对抗样本
perturbed_image = image + epsilon * sign_data_grad
# 将像素值裁剪到 [0, 1] 范围内
# perturbed_image = torch.clamp(perturbed_image, 0, 1)
return perturbed_image
2.6 攻击测试
# 测试模型在对抗样本上的表现
def fgsm_test(model,test_loader,epsilon,criterion,device):
"""
测试模型在对抗样本上的表现
:param model: 模型
:param test_loader: 测试数据加载器
:param epsilon: 扰动强度
:param criterion: 损失函数
:param device: 设备
"""
correct = 0 #累加器,用于统计 test_loader 中预测正确的总样本数量
total = 0 #累加器,用于统计 test_loader 中已处理的总样本数量
adv_examples_list=[] #对抗样本
flag=True
loop=tqdm(test_loader, desc=f"FGSM epsilon {epsilon}")
for images, labels in loop:
images, labels = images.to(device), labels.to(device)
images.requires_grad = True # 对images进行梯度计算,默认false
# 前向传播
init_outputs = model(images)
loss = criterion(init_outputs, labels)
# 获取未攻击时预测的数字
_,init_preds=torch.max(init_outputs.data,dim=1)
# 反向传播,计算梯度
model.zero_grad()
loss.backward()
data_grad = images.grad.data
# 生成对抗样本
adv_images = fgsm_attack(images, epsilon, data_grad)
# 测试模型
adv_outputs = model(adv_images)
_, adv_preds = torch.max(adv_outputs.data, 1) #获取攻击后预测的数字
# 获取预测正确个数
correct += adv_preds.eq(labels).sum().item()
total += labels.size(0)
# 单样本带批量维情况(批量大小为1的情况)
if adv_images.dim() == 4 and adv_images.shape[0] == 1:
if len(adv_examples_list)<6: # 只保留这个数据集的前六张对抗样本图片,作为返回
adv_image=adv_images.squeeze().detach().cpu().numpy() #形状[1,1,28,28]->[28,28]
adv_examples_list.append((init_preds.item(),adv_preds.item(),adv_image))
else:
# 批量数据或多维数据(批量大小大于1的情况)
if flag: # 只保留这个数据集第一个批量中的前六张对抗样本图片,作为返回
for init_pred, adv_pred, adv_image in zip(init_preds[:6], adv_preds[:6], adv_images[:6]):
adv_examples_list.append((
init_pred.item(), # 单预测值用.item()
adv_pred.item(),
adv_image.squeeze().detach().cpu().numpy() # [1,28,28] -> [28,28]
))
flag=False
# 更新进度条
loop.set_postfix(acc=100. * correct / total)
return adv_examples_list
2.7 启动攻击
# 检查设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#加载数据
_, test_loader=load_data(batch_size=64)
# 加载模型
pre_model="./model/best_lenet_mnist.pth"
model = load_model(pre_model,device)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
# 攻击测试
adv_examples_list=[]
epsilons = [ 0,0.1,0.15,0.2,0.25,0.3]
for epsilon in epsilons:
adv_examples=fgsm_test(model,test_loader,epsilon,criterion,device)
adv_examples_list.append(adv_examples)
FGSM epsilon 0: 100%|██████████████████████████████████████████████████████| 157/157 [00:03<00:00, 43.72it/s, acc=99.1]
FGSM epsilon 0.1: 100%|████████████████████████████████████████████████████| 157/157 [00:03<00:00, 42.71it/s, acc=81.1]
FGSM epsilon 0.15: 100%|█████████████████████████████████████████████████████| 157/157 [00:03<00:00, 40.88it/s, acc=67]
FGSM epsilon 0.2: 100%|████████████████████████████████████████████████████| 157/157 [00:03<00:00, 42.41it/s, acc=53.8]
FGSM epsilon 0.25: 100%|███████████████████████████████████████████████████| 157/157 [00:03<00:00, 45.22it/s, acc=43.3]
FGSM epsilon 0.3: 100%|████████████████████████████████████████████████████| 157/157 [00:03<00:00, 44.33it/s, acc=36.1]
2.8 效果展示
def plot_adv_examples(adv_examples_list, epsilons):
"""展示对抗攻击效果"""
cnt = 0
plt.figure(figsize=(8, 10))
for i in range(len(epsilons)):
for j in range(len(adv_examples_list[i])):
cnt += 1
plt.subplot(len(epsilons), len(adv_examples_list[0]), cnt)
plt.xticks([], [])
plt.yticks([], [])
if j == 0:
plt.ylabel("Eps: {}".format(epsilons[i]), fontsize=14)
orig_pred, adv_pred, adv_image = adv_examples_list[i][j]
plt.title("{} -> {}".format(orig_pred, adv_pred),color=("green" if orig_pred==adv_pred else "red"))
plt.imshow(adv_image, cmap="gray")
plt.tight_layout()
plt.show()
plot_adv_examples(adv_examples_list, epsilons)