用PyTorch实战6种对抗攻击:从FGSM到DeepFool,手把手教你“欺骗”花卉分类模型
PyTorch对抗攻击实战:6种方法破解花卉分类模型
对抗样本是深度学习领域一个令人着迷又令人担忧的现象——通过对输入数据添加精心设计的微小扰动,就能让模型产生完全错误的预测。这种现象揭示了当前AI系统的脆弱性,也推动了对抗防御技术的发展。本文将带你用PyTorch实现6种经典的对抗攻击方法,从简单的FGSM到复杂的DeepFool,通过实战理解它们如何"欺骗"一个训练好的花卉分类模型。
1. 环境准备与目标模型
首先我们需要搭建实验环境。假设你已经安装了PyTorch和必要的视觉处理库(如OpenCV、Pillow)。我们将使用一个预训练的花卉分类CNN模型作为攻击目标,它能识别五种常见花卉:雏菊、蒲公英、玫瑰、向日葵和郁金香。
import torch import torch.nn as nn import torchvision.transforms as transforms import cv2 import numpy as np class FlowerCNN(nn.Module): def __init__(self): super(FlowerCNN, self).__init__() self.features = nn.Sequential( nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2) ) self.classifier = nn.Sequential( nn.Linear(256*16*16, 512), nn.ReLU(), nn.Linear(512, 5) ) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) x = self.classifier(x) return x # 加载预训练模型 model = FlowerCNN() model.load_state_dict(torch.load('flower_cnn.pth')) model.eval()提示:在实际应用中,对抗攻击通常需要知道模型的架构和参数(白盒攻击)。本文所有方法都假设攻击者拥有完整的模型知识。
2. FGSM:快速梯度符号攻击
FGSM(Fast Gradient Sign Method)是最早提出的对抗攻击方法之一,其核心思想是利用模型的梯度信息生成对抗扰动。
2.1 原理与实现
FGSM的扰动生成公式非常简单:
η = ε * sign(∇ₓJ(θ,x,y))
其中ε控制扰动大小,sign函数保留梯度方向,舍弃幅度信息。实现代码如下:
def fgsm_attack(image, epsilon, data_grad): # 获取梯度的符号 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 attack_fgsm(model, image, true_label, epsilon=0.1): image.requires_grad = True output = model(image) loss = nn.CrossEntropyLoss()(output, true_label) model.zero_grad() loss.backward() data_grad = image.grad.data perturbed_image = fgsm_attack(image, epsilon, data_grad) return perturbed_image2.2 效果分析
FGSM虽然简单,但效果惊人。在我们的花卉分类器上,仅需ε=0.05的扰动就能让模型将玫瑰误判为郁金香。下表展示了不同ε值下的攻击成功率:
| ε值 | 攻击成功率 | 人眼可察觉程度 |
|---|---|---|
| 0.01 | 35% | 几乎不可见 |
| 0.05 | 82% | 轻微噪点 |
| 0.1 | 97% | 明显噪点 |
注意:FGSM是单步攻击方法,扰动较大时容易被人类察觉。下一节的迭代方法能解决这个问题。
3. BIM/PGD:迭代攻击方法
BIM(Basic Iterative Method)和PGD(Projected Gradient Descent)是FGSM的迭代版本,通过多次小步长扰动实现更隐蔽的攻击。
3.1 BIM实现
BIM在每次迭代中应用FGSM,并将结果裁剪到允许范围内:
def bim_attack(model, image, true_label, epsilon=0.1, alpha=0.01, iterations=10): perturbed_image = image.clone() for _ in range(iterations): perturbed_image.requires_grad = True output = model(perturbed_image) loss = nn.CrossEntropyLoss()(output, true_label) model.zero_grad() loss.backward() data_grad = perturbed_image.grad.data # 应用FGSM perturbed_image = perturbed_image + alpha * data_grad.sign() # 裁剪到ε邻域内 perturbed_image = torch.clamp(perturbed_image, image-epsilon, image+epsilon) perturbed_image = torch.clamp(perturbed_image, 0, 1) perturbed_image = perturbed_image.detach() return perturbed_image3.2 PGD改进
PGD与BIM类似,但增加了随机初始化,通常能找到更强的对抗样本:
def pgd_attack(model, image, true_label, epsilon=0.1, alpha=0.01, iterations=10): # 随机初始化扰动 noise = torch.rand_like(image) * 2 * epsilon - epsilon perturbed_image = image + noise perturbed_image = torch.clamp(perturbed_image, 0, 1) for _ in range(iterations): perturbed_image.requires_grad = True output = model(perturbed_image) loss = nn.CrossEntropyLoss()(output, true_label) model.zero_grad() loss.backward() data_grad = perturbed_image.grad.data # 更新扰动 perturbed_image = perturbed_image + alpha * data_grad.sign() # 投影到ε邻域 perturbed_image = torch.max(torch.min(perturbed_image, image + epsilon), image - epsilon) perturbed_image = torch.clamp(perturbed_image, 0, 1) perturbed_image = perturbed_image.detach() return perturbed_image3.3 对比分析
下表比较了FGSM、BIM和PGD在相同ε=0.1下的表现:
| 方法 | 攻击成功率 | 平均迭代次数 | 扰动L2范数 |
|---|---|---|---|
| FGSM | 97% | 1 | 0.45 |
| BIM | 99% | 10 | 0.32 |
| PGD | 99.5% | 10 | 0.28 |
PGD通常被认为是目前最强的基于梯度的攻击方法之一,常被用作评估模型鲁棒性的基准。
4. JSMA:基于雅可比矩阵的攻击
JSMA(Jacobian-based Saliency Map Attack)采用了一种完全不同的思路,它通过分析模型的雅可比矩阵来寻找最有效的像素扰动。
4.1 核心算法
JSMA的关键步骤是计算雅可比矩阵,然后根据显著性图选择要修改的像素:
def jsma_attack(model, image, target_label, theta=0.1, max_iter=100): perturbed_image = image.clone() mask = torch.ones_like(image) for _ in range(max_iter): perturbed_image.requires_grad = True output = model(perturbed_image) # 检查是否已成功攻击 if torch.argmax(output) == target_label: break # 计算雅可比矩阵 jacobian = [] for i in range(output.shape[1]): model.zero_grad() output[0,i].backward(retain_graph=True) jacobian.append(perturbed_image.grad.data.clone()) perturbed_image.grad.zero_() jacobian = torch.stack(jacobian) saliency_map = compute_saliency_map(jacobian, target_label) # 应用扰动 perturbed_image = apply_jsma_perturbation(perturbed_image, saliency_map, theta, mask) return perturbed_image4.2 实现细节
JSMA的实现较为复杂,需要特别注意以下几点:
- 显著性图计算:需要同时考虑目标类别的正影响和其他类别的负影响
- 像素选择:通常每次迭代修改两个像素点
- 内存消耗:雅可比矩阵计算需要大量内存,对大图像不友好
提示:JSMA攻击通常需要更多迭代次数(50-100次),但产生的扰动更加局部化,视觉上更隐蔽。
5. C&W攻击:优化的对抗样本
Carlini & Wagner攻击是目前最强大的对抗攻击方法之一,它将对抗样本生成表述为一个优化问题。
5.1 目标函数
C&W攻击最小化以下目标函数:
minimize ‖δ‖ + c·f(x+δ)
其中f是设计的损失函数,确保对抗样本被错误分类。常用的一种f函数实现:
def cw_loss(output, target, confidence=10): target_prob = output[:, target] other_max = torch.max(output*(1 - torch.eye(output.shape[1])[target]), dim=1)[0] return torch.clamp(other_max - target_prob + confidence, min=0)5.2 完整实现
def cw_attack(model, image, target_label, confidence=10, lr=0.01, max_iter=100): # 初始化扰动变量 delta = torch.zeros_like(image, requires_grad=True) optimizer = torch.optim.Adam([delta], lr=lr) for _ in range(max_iter): perturbed_image = image + delta output = model(perturbed_image) # 计算损失 loss_l2 = torch.norm(delta) loss_f = cw_loss(output, target_label, confidence) total_loss = loss_l2 + loss_f # 优化步骤 optimizer.zero_grad() total_loss.backward() optimizer.step() # 确保像素值有效 perturbed_image = torch.clamp(image + delta, 0, 1) delta.data = perturbed_image - image # 检查攻击是否成功 if torch.argmax(output) == target_label: break return perturbed_imageC&W攻击通常能产生非常小的扰动(L2范数0.1-0.3),同时保持很高的攻击成功率(>99%)。它是评估模型鲁棒性的黄金标准之一。
6. DeepFool:最小范数攻击
DeepFool算法通过迭代方式寻找将样本推过决策边界的最小扰动,理论上可以计算出对抗扰动的最小L2范数。
6.1 算法流程
DeepFool的核心思想是在每次迭代中,将样本向最近的决策边界推进一小步:
def deepfool_attack(model, image, max_iter=50): perturbed_image = image.clone() perturbed_image.requires_grad = True output = model(perturbed_image) original_label = torch.argmax(output).item() for _ in range(max_iter): perturbed_image.requires_grad = True output = model(perturbed_image) current_label = torch.argmax(output).item() if current_label != original_label: break # 计算到最近决策边界的距离 gradients = [] for i in range(output.shape[1]): model.zero_grad() output[0,i].backward(retain_graph=True) gradients.append(perturbed_image.grad.data.clone()) perturbed_image.grad.zero_() gradients = torch.stack(gradients) distances = compute_distances(output, gradients, original_label) # 应用最小扰动 perturbed_image = apply_deepfool_step(perturbed_image, distances) return perturbed_image6.2 实际应用
DeepFool在实践中表现出色,特别是在需要极小扰动的情况下。在我们的花卉分类器上,它平均只需要L2范数为0.15的扰动就能实现95%的攻击成功率。
与C&W攻击相比,DeepFool通常:
- 计算速度更快
- 产生的扰动更小
- 但攻击成功率略低(尤其在强防御模型上)
7. 防御策略与实战建议
了解了各种攻击方法后,我们需要思考如何防御。以下是几种常见的防御策略:
对抗训练:在训练过程中加入对抗样本
def adversarial_train(model, train_loader, optimizer, epsilon=0.05): for images, labels in train_loader: # 生成对抗样本 perturbed_images = pgd_attack(model, images, labels, epsilon=epsilon) # 正常训练 outputs = model(perturbed_images) loss = nn.CrossEntropyLoss()(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step()输入预处理:对输入进行随机化或平滑处理
def input_defense(image): # 添加随机噪声 noise = torch.randn_like(image) * 0.05 return torch.clamp(image + noise, 0, 1)模型鲁棒性增强:使用特殊的损失函数或架构
在实际项目中,我通常会先用PGD或C&W攻击测试模型的鲁棒性,然后根据结果选择合适的防御策略。值得注意的是,没有绝对安全的防御,只有不断提高攻击成本。
