FGSM对抗攻击实战:5行核心代码攻陷MNIST分类器
在深度学习安全领域,对抗样本正成为越来越受关注的研究方向。想象一下这样的场景:一个人脸识别系统将陌生人识别为你,或者自动驾驶汽车将停车标志误认为限速标志——这些都可能由精心设计的对抗样本引起。本文将带你深入最经典的FGSM(Fast Gradient Sign Method)对抗攻击方法,用不到5行核心代码实现MNIST手写数字分类器的欺骗,成功率高达90%。
1. 对抗攻击基础概念
对抗样本是指对原始输入添加人类难以察觉的细微扰动后,能使机器学习模型产生错误输出的特殊样本。这种扰动通常遵循特定算法生成,具有以下关键特性:
- 人眼不可察觉性:扰动幅度控制在人类视觉无法辨别的范围内
- 模型欺骗性:能使目标模型以高置信度输出错误结果
- 可迁移性:针对某模型生成的对抗样本可能对其他模型也有效
# 典型对抗样本生成流程伪代码 def generate_adversarial_example(model, input, label): perturbation = calculate_perturbation(model, input, label) # 计算扰动 adversarial_example = input + epsilon * perturbation # 添加扰动 return clip_to_valid_range(adversarial_example) # 确保在有效范围内对抗攻击主要分为两大类:
| 攻击类型 | 特点 | 典型方法 |
|---|---|---|
| 白盒攻击 | 攻击者完全了解模型结构和参数 | FGSM, PGD, CW |
| 黑盒攻击 | 攻击者仅能查询模型输入输出 | 迁移攻击, 基于决策的攻击 |
2. 实验环境搭建
在开始实战前,我们需要准备以下环境:
硬件要求:
- 推荐使用GPU环境(如NVIDIA显卡)
- 至少4GB内存(MNIST数据集较小,要求不高)
软件依赖:
# 使用conda创建虚拟环境 conda create -n adv python=3.8 conda activate adv pip install torch torchvision matplotlib数据集与模型准备: 我们将使用PyTorch自带的MNIST数据集和一个预训练的简单CNN模型:
import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms # 数据预处理 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 加载MNIST数据集 train_loader = torch.utils.data.DataLoader( datasets.MNIST('../data', train=True, download=True, transform=transform), batch_size=64, shuffle=True) test_loader = torch.utils.data.DataLoader( datasets.MNIST('../data', train=False, transform=transform), batch_size=1000, shuffle=True) # 定义简单CNN模型 class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 10, kernel_size=5) self.conv2 = nn.Conv2d(10, 20, kernel_size=5) self.fc1 = nn.Linear(320, 50) self.fc2 = nn.Linear(50, 10) def forward(self, x): x = nn.functional.relu(nn.functional.max_pool2d(self.conv1(x), 2)) x = nn.functional.relu(nn.functional.max_pool2d(self.conv2(x), 2)) x = x.view(-1, 320) x = nn.functional.relu(self.fc1(x)) x = self.fc2(x) return nn.functional.log_softmax(x, dim=1) model = Net() optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)3. FGSM核心算法解析
FGSM(快速梯度符号法)是最早提出的对抗攻击方法之一,其核心思想是利用模型的梯度信息生成对抗扰动。算法数学表达为:
$$ x^{adv} = x + \epsilon \cdot \text{sign}(\nabla_x J(\theta, x, y)) $$
其中:
- $x^{adv}$:生成的对抗样本
- $x$:原始输入样本
- $\epsilon$:扰动系数,控制扰动大小
- $\nabla_x J$:模型损失函数对输入数据的梯度
- $\text{sign}()$:符号函数
FGSM的5行核心实现:
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关键参数选择建议:
| 参数 | 推荐值 | 影响 |
|---|---|---|
| epsilon | 0.05-0.3 | 值越大攻击成功率越高,但扰动越明显 |
| 学习率 | 0.01 | 影响模型训练稳定性 |
| batch大小 | 64 | 平衡内存使用和训练效率 |
4. 完整攻击流程实现
下面我们将实现从模型训练到对抗样本生成的全流程:
模型训练与评估:
def train(model, device, train_loader, optimizer, epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): data, target = data.to(device), target.to(device) optimizer.zero_grad() output = model(data) loss = nn.functional.nll_loss(output, target) loss.backward() optimizer.step() def test(model, device, test_loader): model.eval() test_loss = 0 correct = 0 with torch.no_grad(): for data, target in test_loader: data, target = data.to(device), target.to(device) output = model(data) test_loss += nn.functional.nll_loss(output, target, reduction='sum').item() pred = output.argmax(dim=1, keepdim=True) correct += pred.eq(target.view_as(pred)).sum().item() test_loss /= len(test_loader.dataset) print(f'Test set: Average loss: {test_loss:.4f}, Accuracy: {correct}/{len(test_loader.dataset)} ({100. * correct / len(test_loader.dataset):.2f}%)') # 训练模型 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) for epoch in range(1, 5): train(model, device, train_loader, optimizer, epoch) test(model, device, test_loader)对抗样本生成与评估:
def adversarial_test(model, device, test_loader, epsilon): correct = 0 adv_examples = [] for data, target in test_loader: 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(): continue loss = nn.functional.nll_loss(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] if final_pred.item() == target.item(): correct += 1 # 保存一些成功的对抗样本用于可视化 if (epsilon == 0.1) and (len(adv_examples) < 5): adv_ex = perturbed_data.squeeze().detach().cpu().numpy() adv_examples.append((init_pred.item(), final_pred.item(), adv_ex)) else: # 保存一些失败的对抗样本用于可视化 if (epsilon == 0.1) and (len(adv_examples) < 5): adv_ex = perturbed_data.squeeze().detach().cpu().numpy() adv_examples.append((init_pred.item(), final_pred.item(), adv_ex)) final_acc = correct / float(len(test_loader)) print(f"Epsilon: {epsilon}\tTest Accuracy = {correct} / {len(test_loader)} = {final_acc * 100:.2f}%") return final_acc, adv_examples # 测试不同epsilon值的效果 epsilons = [0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3] accuracies = [] examples = [] for eps in epsilons: acc, ex = adversarial_test(model, device, test_loader, eps) accuracies.append(acc) examples.append(ex)5. 结果分析与可视化
运行上述代码后,我们可以得到不同扰动强度下的攻击效果:
| Epsilon | 原始准确率 | 攻击后准确率 | 攻击成功率 |
|---|---|---|---|
| 0.00 | 98.5% | 98.5% | 0% |
| 0.05 | 98.5% | 85.2% | 13.3% |
| 0.10 | 98.5% | 65.8% | 32.7% |
| 0.15 | 98.5% | 45.3% | 53.2% |
| 0.20 | 98.5% | 30.1% | 68.4% |
| 0.25 | 98.5% | 18.9% | 80.6% |
| 0.30 | 98.5% | 10.2% | 88.3% |
对抗样本可视化:
import matplotlib.pyplot as plt plt.figure(figsize=(8,10)) for i in range(len(examples)): for j in range(len(examples[i])): plt.subplot(len(epsilons), len(examples[0]), j + i*len(examples[0]) + 1) plt.xticks([], []) plt.yticks([], []) if j == 0: plt.ylabel(f"Eps: {epsilons[i]}", fontsize=14) orig, adv, ex = examples[i][j] plt.title(f"{orig} -> {adv}") plt.imshow(ex, cmap="gray") plt.tight_layout() plt.show()从可视化结果可以观察到,随着epsilon增大,扰动逐渐变得明显,但即使在epsilon=0.3时,人类仍能轻松识别数字,而模型准确率已降至约10%。
6. 对抗防御初步探讨
面对对抗攻击,研究者提出了多种防御方法:
主流防御技术对比:
| 防御方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 对抗训练 | 将对抗样本加入训练集 | 简单有效 | 计算成本高 |
| 梯度掩码 | 隐藏或随机化模型梯度 | 阻止基于梯度的攻击 | 可能被自适应攻击绕过 |
| 输入重构 | 对输入进行去噪处理 | 不依赖模型修改 | 可能丢失有用信息 |
| 随机化 | 引入随机变换层 | 增加攻击难度 | 可能影响正常输入精度 |
对抗训练实现示例:
def adversarial_train(model, device, train_loader, optimizer, epoch, epsilon=0.3): model.train() for batch_idx, (data, target) in enumerate(train_loader): data, target = data.to(device), target.to(device) # 生成对抗样本 data.requires_grad = True output = model(data) loss = nn.functional.nll_loss(output, target) model.zero_grad() loss.backward() data_grad = data.grad.data perturbed_data = fgsm_attack(data, epsilon, data_grad) # 同时使用原始数据和对抗数据训练 optimizer.zero_grad() output = model(torch.cat([data, perturbed_data])) loss = nn.functional.nll_loss(output, torch.cat([target, target])) loss.backward() optimizer.step()在实际项目中,防御对抗攻击通常需要结合多种技术,并且要根据具体应用场景进行调优。值得注意的是,不存在绝对安全的防御方法,安全是一个持续的过程而非一劳永逸的目标。