Bootstrap

从原理到代码:如何通过 FGSM 生成对抗样本并进行攻击

从原理到代码:如何通过 FGSM 生成对抗样本并进行攻击

简介

在机器学习领域,深度神经网络的强大表现令人印象深刻,尤其是在图像分类等任务上。然而,随着对深度学习的深入研究,研究人员发现了神经网络的一个脆弱性:对抗样本(Adversarial Examples)。简单来说,对抗样本是通过对原始输入数据添加微小扰动,导致神经网络预测错误的一类特殊样本。

Fast Gradient Sign Method (FGSM) 是最早提出的一种简单且有效的对抗攻击方法。它通过利用神经网络的梯度信息,快速生成对抗样本。本篇博客将深入剖析 FGSM 的数学原理,并提供一份基于 PyTorch 的代码实现,帮助你快速上手对抗攻击。

最终效果

当前图片为 truck,但经过对抗攻击后,结果看似应该同样是 truck,然而模型却错误地将其分类为了 cat

在这里插入图片描述

数学原理

1. 神经网络的基本工作原理

神经网络通过对输入数据 x x x 进行一系列的非线性变换,输出一个预测值 y ^ \hat{y} y^。以图像分类任务为例,输入 x x x 是图像,网络输出 y ^ \hat{y} y^ 是一个分类结果。网络的目标是使得预测值 y ^ \hat{y} y^ 尽可能接近真实标签 y y y,通过最小化损失函数 L ( y ^ , y ) L(\hat{y}, y) L(y^,y) 来优化网络的权重。

2. 对抗攻击的目标

对抗攻击的核心思想是在输入数据上添加一个微小的扰动 η \eta η,使得网络在预测时发生错误。这种扰动应尽量保持小到人类肉眼难以察觉,但足以让网络产生不同的输出,即:

y ^ a d v = f ( x + η ) and y ^ a d v ≠ y \hat{y}_{adv} = f(x + \eta) \quad \text{and} \quad \hat{y}_{adv} \neq y y^adv=f(x+η)andy^adv=y

3. FGSM 的核心思想

FGSM 利用了梯度上升的思想,通过损失函数相对于输入图像的梯度来找到 最容易 迷惑网络的方向,并沿着这个方向对图像进行微小的扰动。

具体来说,给定输入图像 x x x 和其真实标签 y y y,我们可以计算损失函数 L ( x , y ) L(x, y) L(x,y),并对输入图像 x x x 计算其梯度:

∇ x L ( f ( x ) , y ) \nabla_x L(f(x), y) xL(f(x),y)

这个梯度告诉我们,如何改变输入图像才能最大化损失。FGSM 的基本想法是,沿着这个梯度的符号方向对图像进行微调,以最大化损失函数。具体公式为:

x a d v = x + ϵ ⋅ sign ( ∇ x L ( f ( x ) , y ) ) x_{adv} = x + \epsilon \cdot \text{sign}(\nabla_x L(f(x), y)) xadv=x+ϵsign(xL(f(x),y))

其中:

  • ϵ \epsilon ϵ 是一个小的常数,控制扰动的大小。
  • sign ( ∇ x L ( f ( x ) , y ) ) \text{sign}(\nabla_x L(f(x), y)) sign(xL(f(x),y)) 是损失函数对输入图像梯度的符号,即正负号矩阵。

通过这种方式,我们可以生成一个对抗样本 x a d v x_{adv} xadv,它与原始图像 x x x 看起来几乎相同,但神经网络会将其错误分类。

4. 梯度上升与对抗样本

FGSM 是基于梯度上升的攻击方法。为了让网络犯错,我们希望最大化损失,因此通过扰动让损失函数 L L L 增加。梯度 ∇ x L \nabla_x L xL 的方向是使 L L L 增加最快的方向,因此,我们可以通过对梯度取符号(sign)来生成一个扰动 η \eta η,即:

η = ϵ ⋅ sign ( ∇ x L ( f ( x ) , y ) ) \eta = \epsilon \cdot \text{sign}(\nabla_x L(f(x), y)) η=ϵsign(xL(f(x),y))

这样生成的扰动被加到原图 x x x 上,即对抗样本的公式为:

x a d v = x + η = x + ϵ ⋅ sign ( ∇ x L ( f ( x ) , y ) ) x_{adv} = x + \eta = x + \epsilon \cdot \text{sign}(\nabla_x L(f(x), y)) xadv=x+η=x+ϵsign(xL(f(x),y))

代码实现与解读

接下来,我们通过实际的代码示例,展示如何使用 PyTorch 实现 FGSM 攻击。

1. 加载CIFAR-10数据集与预训练模型

首先,我们需要加载 CIFAR-10 数据集和一个预训练的模型。这里我们使用 ResNet-18 模型,并针对 CIFAR-10 数据集进行了调整。

import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torchvision.models as models

# 设置设备 (GPU 优先)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 加载 CIFAR-10 数据集
transform = transforms.Compose([
    transforms.ToTensor(),
])
test_dataset = datasets.CIFAR10(root='./data', train=False, download=False, transform=transform)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1, shuffle=True)

# 加载微调后的 ResNet 模型
model = models.resnet18(pretrained=False, num_classes=10).to(device)
model.eval()  # 设置为评估模式

在这一步中,我们准备了数据集,并将模型设置为评估模式,这样模型的参数不会在攻击过程中被更新。

提示:如果没有下载过 CIFAR-10 数据集,请将 download=False 改为 download=True

2. 实现FGSM攻击方法

FGSM 的关键在于利用损失函数的梯度来生成对抗样本。具体步骤如下:

# 定义 FGSM 攻击函数
def fgsm_attack(image, epsilon, data_grad):
    # 生成扰动方向
    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
数学公式:

在代码中,我们实际上实现了如下的公式:

x a d v = x + ϵ ⋅ sign ( ∇ x L ( f ( x ) , y ) ) x_{adv} = x + \epsilon \cdot \text{sign}(\nabla_x L(f(x), y)) xadv=x+ϵsign(xL(f(x),y))

该函数实现了 FGSM 算法,通过将计算得到的梯度方向乘以一个小的扰动系数 ϵ \epsilon ϵ,并添加到原始图像上来生成对抗样本。

3. 攻击流程

接下来,我们定义一个函数,来完成攻击过程:

def attack_example(model, device, data, target, epsilon):
    # 将数据和标签移动到设备上
    data, target = data.to(device), target.to(device)
    data.requires_grad = True  # 追踪输入图像的梯度

    # 前向传播得到初始预测
    output = model(data)
    init_pred = output.max(1, keepdim=True)[1]  # 选择概率最大的类别作为预测结果

    # 如果初始预测错误,则不进行攻击
    if init_pred.item() != target.item():
        return None, None, None

    # 计算损失并进行反向传播计算梯度
    loss_fn = torch.nn.CrossEntropyLoss()
    loss = loss_fn(output, target)
    model.zero_grad()  # 清除现有的梯度
    loss.backward()  # 计算损失对输入图像的梯度

    # 使用 FGSM 方法生成对抗样本
    data_grad = data.grad.data
    perturbed_data = fgsm_attack(data, epsilon, data_grad)

    # 使用对抗样本进行新的预测
    output = model(perturbed_data)
    final_pred = output.max(1, keepdim=True)[1]  # 获取对抗样本的预测标签

    return data, perturbed_data, final_pred

对抗攻击的基本思路是:

  1. 前向传播:将输入数据(如图片)传递给模型,得到输出,并预测标签。
  2. 计算损失:使用损失函数来评估模型的预测与真实标签之间的差距。
  3. 反向传播计算梯度:通过反向传播,计算损失函数相对于输入数据的梯度。
  4. 生成对抗样本:利用这些梯度来调整输入数据,生成扰动的对抗样本。
  5. 重新预测:使用生成的对抗样本进行重新预测,观察模型是否做出错误的分类。

4. 展示原始图像与对抗样本

最后,我们选择一个样本进行攻击,并展示原始图像和对抗样本的效果。

# Step 6: 选择 epsilon 并进行测试
epsilon = 0.01  # 扰动强度

# 遍历数据集,找到预测正确的样本
for data, target in test_loader:
    orig_data, perturbed_data, final_pred = attack_example(model, device, data, target, epsilon)
    if orig_data is not None:
        break

# CIFAR-10 类别
cifar10_classes = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']

if orig_data is not None:
    # 显示原始图像和对抗样本
    orig_img = orig_data.squeeze().permute(1, 2, 0).detach().cpu().numpy()
    perturbed_img = perturbed_data.squeeze().permute(1, 2, 0).detach().cpu().numpy()

    # 获取语义化标签
    original_label_name = cifar10_classes[target.item()]
    adversarial_label_name = cifar10_classes[final_pred.item()]

    # Step 7: 展示结果
    fig, (ax1, ax2) = plt.subplots(1, 2)
    ax1.imshow(orig_img)
    ax1.set_title(f"Original Label: {original_label_name}")
    ax2.imshow(perturbed_img)
    ax2.set_title(f"Adversarial Label: {adversarial_label_name}")
    plt.show()

    print(f"Original Label: {original_label_name}, Adversarial Label: {adversarial_label_name}")
else:
    print("Initial prediction was incorrect. No attack performed.")

在运行这段代码后,你会看到一张原始图像及其对抗样本。尽管对抗样本与原图在视觉上几乎没有区别,但它却被神经网络错误分类了。这种现象突显了神经网络的脆弱性。

结论

FGSM 是一种简单且有效的对抗攻击方法,它利用了神经网络的梯度信息生成对抗样本。这些对抗样本虽然肉眼难以察觉,但能显著影响模型的预测结果。本文介绍了 FGSM 的数学原理,并提供了基于 PyTorch 的实现代码。

通过这个例子,我们可以看到即使是看似强大的深度学习模型,也会受到一些精心设计的输入的严重影响。了解这些脆弱性有助于我们开发出更加健壮和安全的模型。

附录:完整代码

import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torchvision.models as models
import matplotlib.pyplot as plt
import numpy as np

# Step 1: 设置设备 (GPU 优先)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Step 2: 加载 CIFAR-10 数据集
transform = transforms.Compose([
    transforms.ToTensor(),
])

# 加载测试数据集
test_dataset = datasets.CIFAR10(root='./data', train=False, download=False, transform=transform)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1, shuffle=True)

# model = models.resnet18(pretrained=True).to(device)

# 1. 加载在 CIFAR-10 上微调的 ResNet 模型
model = models.resnet18(pretrained=False, num_classes=10).to(device)

model.eval()  # 设置为评估模式

# 使用的损失函数
loss_fn = nn.CrossEntropyLoss()


# Step 4: FGSM 对抗攻击函数
def fgsm_attack(image, epsilon, data_grad):
    # 生成扰动方向
    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


# Step 5: 攻击流程
def attack_example(model, device, data, target, epsilon):
    data, target = data.to(device), target.to(device)

    # 确保计算图对输入的梯度
    data.requires_grad = True

    # 前向传播
    output = model(data)
    init_pred = output.max(1, keepdim=True)[1]  # 预测标签

    # 如果预测错误,则不攻击
    if init_pred.item() != target.item():
        return None, None, None

    # 计算损失
    loss = loss_fn(output, target)

    # 反向传播计算梯度
    model.zero_grad()
    loss.backward()

    # 提取梯度
    data_grad = data.grad.data

    # 执行 FGSM 攻击
    perturbed_data = fgsm_attack(data, epsilon, data_grad)

    # 再次进行预测
    output = model(perturbed_data)
    final_pred = output.max(1, keepdim=True)[1]  # 对抗样本的预测标签

    return data, perturbed_data, final_pred


# Step 6: 选择 epsilon 并进行测试
epsilon = 0.01  # 扰动强度

# 遍历数据集,找到预测正确的样本
for data, target in test_loader:
    orig_data, perturbed_data, final_pred = attack_example(model, device, data, target, epsilon)
    if orig_data is not None:
        break

# CIFAR-10 类别
cifar10_classes = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']


if orig_data is not None:
    # 显示原始图像和对抗样本

    orig_img = orig_data.squeeze().permute(1, 2, 0).detach().cpu().numpy()
    perturbed_img = perturbed_data.squeeze().permute(1, 2, 0).detach().cpu().numpy()

    # 标签
    # orig_label = target.item()
    # perturbed_label = final_pred.item()

    # 获取语义化标签
    original_label_name = cifar10_classes[target.item()]
    adversarial_label_name = cifar10_classes[final_pred.item()]

    # Step 7: 展示结果
    fig, (ax1, ax2) = plt.subplots(1, 2)
    ax1.imshow(orig_img)
    ax1.set_title(f"Original Label: {original_label_name}")
    ax2.imshow(perturbed_img)
    ax2.set_title(f"Adversarial Label: {adversarial_label_name}")
    plt.show()

    print(f"Original Label: {original_label_name}, Adversarial Label: {adversarial_label_name}")
else:
    print("Initial prediction was incorrect. No attack performed.")

;