对抗样本(Adversarial Examples)是深度学习领域一个令人着迷又警惕的现象——它们就像AI的"视觉错觉",通过精心设计的微小扰动就能让最先进的神经网络产生荒谬的误判。想象一下,只需对像素做些人类难以察觉的调整,就能让模型把卡车认成猫,或者把停车标志看成限速标志。这种特性不仅揭示了神经网络的脆弱性,也为模型安全研究提供了重要视角。
本文将带您用PyTorch实现经典的FGSM(Fast Gradient Sign Method)攻击,通过CIFAR-10数据集演示如何让ResNet模型将卡车图像误分类为猫。不同于理论推导为主的教程,我们聚焦实战操作,从数据加载到扰动生成,步步拆解攻击原理,并可视化攻击前后的对比效果。无论您是安全研究员、AI开发者还是好奇的技术爱好者,都能从中获得对抗攻击的第一手经验。
在开始生成对抗样本之前,我们需要搭建实验环境。这里选择PyTorch作为深度学习框架,它不仅提供便捷的自动微分功能,还内置了常用的预训练模型和数据集接口。以下是基础配置步骤:
python复制import torch
import torch.nn as nn
import torchvision
from torchvision import models, transforms, datasets
import matplotlib.pyplot as plt
import numpy as np
# 设置计算设备(优先使用GPU)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
# 加载CIFAR-10测试集
transform = transforms.Compose([
transforms.ToTensor(),
])
test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1, shuffle=True)
# 类别标签映射
cifar10_classes = ['airplane', 'automobile', 'bird', 'cat', 'deer',
'dog', 'frog', 'horse', 'ship', 'truck']
我们使用ResNet-18作为目标模型,这个中等规模的卷积神经网络在图像分类任务上表现良好。虽然原始ResNet是为ImageNet设计的,但我们可以调整最后一层使其适配CIFAR-10的10类别分类:
python复制# 加载并修改ResNet-18
model = models.resnet18(pretrained=False, num_classes=10).to(device)
model.eval() # 设置为评估模式
# 注意:实际使用时应该加载在CIFAR-10上训练好的权重
# 这里为演示简化流程,随机初始化模型参数
关键细节:模型必须处于
eval()模式,这会关闭Dropout和BatchNorm等训练特有的层,确保攻击过程中的行为一致性。同时,我们需要冻结模型参数——对抗攻击只改变输入图像,不更新模型权重。
FGSM由Ian Goodfellow等人在2014年提出,其核心思想惊人地简单:沿着损失函数的梯度方向添加扰动。具体来说,给定输入图像x和真实标签y,攻击步骤如下:
数学表达式为:
$$ x_{adv} = x + \epsilon \cdot \text{sign}(\nabla_x J(\theta, x, y)) $$
为什么这个方法有效?梯度sign指示了哪些像素的小变化会最快速增大损失。通过故意增大损失,我们迫使模型产生错误预测。ϵ控制扰动幅度——值越大攻击越强,但也越容易被人类察觉。
下表对比了不同攻击方法的特性:
| 方法 | 计算复杂度 | 攻击效果 | 扰动可见性 | 所需模型知识 |
|---|---|---|---|---|
| FGSM | 低(单步) | 中等 | 低 | 完整 |
| PGD | 高(迭代) | 强 | 中 | 完整 |
| DeepFool | 中 | 强 | 很低 | 部分 |
| CW攻击 | 很高 | 很强 | 极低 | 完整 |
FGSM作为白盒攻击的典型代表,假设攻击者完全了解模型结构和参数。虽然这看起来限制性强,但研究表明,基于一个模型生成的对抗样本常常能迁移到其他模型(迁移攻击),这使得研究白盒攻击具有广泛意义。
现在我们将理论转化为代码。FGSM攻击的核心可以浓缩为以下几个关键函数:
python复制def fgsm_attack(image, epsilon, data_grad):
"""
生成FGSM对抗样本
:param image: 原始输入图像(tensor)
:param epsilon: 扰动系数(float)
:param data_grad: 输入图像的梯度(tensor)
:return: 对抗样本(tensor)
"""
# 获取梯度的符号方向
sign_grad = data_grad.sign()
# 生成扰动图像
perturbed_image = image + epsilon * sign_grad
# 将像素值裁剪到[0,1]范围
perturbed_image = torch.clamp(perturbed_image, 0, 1)
return perturbed_image
def generate_attack(model, device, dataloader, epsilon):
"""
对数据集生成对抗样本
:param model: 目标模型
:param device: 计算设备
:param dataloader: 数据加载器
:param epsilon: 扰动系数
:return: 成功攻击的样本列表
"""
attacked_samples = []
for data, target in dataloader:
data, target = data.to(device), target.to(device)
data.requires_grad = True # 追踪输入梯度
# 前向传播
output = model(data)
init_pred = output.argmax(dim=1)
# 如果初始预测错误,跳过该样本
if init_pred.item() != target.item():
continue
# 计算损失
loss = nn.CrossEntropyLoss()(output, target)
# 反向传播获取梯度
model.zero_grad()
loss.backward()
data_grad = data.grad.data
# 生成对抗样本
perturbed_data = fgsm_attack(data, epsilon, data_grad)
# 检查攻击是否成功
final_pred = model(perturbed_data).argmax(dim=1)
if final_pred.item() != target.item():
attacked_samples.append((
data.squeeze().detach().cpu().numpy(),
perturbed_data.squeeze().detach().cpu().numpy(),
target.item(),
final_pred.item()
))
if len(attacked_samples) >= 5: # 只收集少量示例
break
return attacked_samples
这段代码有几个关键实现细节值得注意:
requires_grad=True让PyTorch追踪输入张量的计算图,这是计算梯度的前提torch.clamp确保扰动后的图像仍在合法像素值范围内让我们选择一个适中的ϵ值(0.05)进行攻击,并可视化结果:
python复制# 执行攻击
epsilon = 0.05
attacked_samples = generate_attack(model, test_loader, epsilon, device)
# 可视化结果
plt.figure(figsize=(10, 8))
for i, (orig, adv, true_label, adv_label) in enumerate(attacked_samples):
# 调整图像维度顺序 (C,H,W) -> (H,W,C)
orig = np.transpose(orig, (1, 2, 0))
adv = np.transpose(adv, (1, 2, 0))
# 计算并缩放扰动
perturbation = (adv - orig) * 10 # 放大扰动便于观察
# 绘制子图
plt.subplot(3, 5, i+1)
plt.imshow(orig)
plt.title(f"Original: {cifar10_classes[true_label]}")
plt.axis('off')
plt.subplot(3, 5, i+6)
plt.imshow(adv)
plt.title(f"Adversarial: {cifar10_classes[adv_label]}")
plt.axis('off')
plt.subplot(3, 5, i+11)
plt.imshow(perturbation)
plt.title("Perturbation (x10)")
plt.axis('off')
plt.tight_layout()
plt.show()
典型输出如下图所示(虽然具体样本可能因随机性而异):

观察结果可以发现:
ϵ是控制攻击强度的关键参数。为全面理解其影响,我们系统测试不同ϵ值下的攻击成功率:
python复制epsilons = [0, 0.01, 0.03, 0.05, 0.1, 0.2]
accuracies = []
examples = []
for eps in epsilons:
correct = 0
total = 0
for data, target in test_loader:
data, target = data.to(device), target.to(device)
data.requires_grad = True
output = model(data)
init_pred = output.argmax(dim=1)
if init_pred.item() != target.item():
continue
loss = nn.CrossEntropyLoss()(output, target)
model.zero_grad()
loss.backward()
data_grad = data.grad.data
perturbed_data = fgsm_attack(data, eps, data_grad)
final_pred = model(perturbed_data).argmax(dim=1)
if final_pred.item() == target.item():
correct += 1
total += 1
acc = correct / total
accuracies.append(acc)
print(f"Epsilon: {eps:.2f}, Accuracy: {acc*100:.2f}%")
将结果绘制成图表:
python复制plt.figure(figsize=(8,5))
plt.plot(epsilons, accuracies, "*-")
plt.xlabel("Epsilon")
plt.ylabel("Accuracy")
plt.title("Model Accuracy vs Epsilon")
plt.grid(True)
plt.show()
随着ϵ增大,模型准确率通常呈现下降趋势。有趣的是,即使ϵ很小(如0.01),也可能导致显著性能下降,这揭示了神经网络对特定扰动的极端敏感性。
了解攻击方法后,我们自然想到如何防御。常见的防御策略包括:
以对抗训练为例,其核心是在训练过程中动态生成对抗样本:
python复制def adversarial_train(model, train_loader, optimizer, epsilon, device):
model.train()
for data, target in train_loader:
data, target = data.to(device), target.to(device)
# 生成对抗样本
data.requires_grad = True
output = model(data)
loss = nn.CrossEntropyLoss()(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.CrossEntropyLoss()(output, torch.cat([target, target]))
loss.backward()
optimizer.step()
值得注意的是,对抗安全是持续博弈的过程——新的防御方法出现后,攻击者又会开发更强大的攻击手段。这种动态对抗推动了机器学习安全领域的不断发展。