FGSM对抗样本生成算法实现(pytorch版)

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

步骤

  1. 计算损失函数对输入的梯度: ∇ x J ( x , y ) \nabla_x J(x, y) xJ(x,y)
  2. 获取梯度的符号方向: sign ( ∇ x J ( x , y ) ) \text{sign}(\nabla_x J(x, y)) sign(xJ(x,y))
  3. 生成对抗样本: 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.03sign(xJ(x,y))
  4. 裁剪对抗样本的像素值到 [ 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)

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

入梦风行

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值