趋近智
这一实践指导展示了如何实现两种重要的规避攻击:投影梯度下降(PGD)和Carlini & Wagner(C&W)。这些动手实现利用对抗鲁棒性工具箱(ART)库,这是一个旨在评估机器学习模型安全性的流行Python框架。ART为攻击和防御提供了便捷的抽象,并与PyTorch和TensorFlow等常见的深度学习框架良好集成。
通过这些示例的学习,您将更好地理解这些攻击如何生成对抗样本,以及它们的参数如何影响最终结果。我们假设您已设置好Python环境,并安装了PyTorch(或TensorFlow)和ART。
首先,请确保您已安装所需的库。在本例中,我们将使用ART和PyTorch。您可以使用pip进行安装:
pip install adversarial-robustness-toolbox[pytorch] torch torchvision
我们还需要一个训练好的模型和一些数据。为简化起见,我们使用在MNIST数据集上预训练的简单卷积神经网络(CNN)。您通常会加载自己训练好的模型,但ART为常见数据集和基本模型提供了实用工具,这有助于进行试验。
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# 定义一个简单的CNN模型(示例)
class SimpleMNISTCNN(nn.Module):
def __init__(self):
super(SimpleMNISTCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.fc1 = nn.Linear(7*7*64, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = F.relu(F.max_pool2d(self.conv1(x), 2))
x = F.relu(F.max_pool2d(self.conv2(x), 2))
x = x.view(-1, 7*7*64)
x = F.relu(self.fc1(x))
x = self.fc2(x)
# ART分类器需要输出logit值
return x
# 加载MNIST数据
transform = transforms.Compose([transforms.ToTensor()])
test_dataset = datasets.MNIST('./data', train=False, download=True, transform=transform)
test_loader = DataLoader(test_dataset, batch_size=100, shuffle=False)
# --- 重要提示 ---
# 对于本次实践,假设'model'是SimpleMNISTCNN的一个预训练实例
# 并且设置为评估模式: model.eval()
# 例如:
# model = SimpleMNISTCNN()
# model.load_state_dict(torch.load('path/to/your/trained_mnist_cnn.pth'))
# model.eval()
# device = torch.device("cuda" if torch.cuda_is_available() else "cpu")
# model.to(device)
#
# 您需要将此处替换为加载您实际训练好的模型。
# 为演示目的,我们将假设'model'存在。
# 让我们创建一个占位模型(未训练)仅用于代码结构。
# !!! 请将其替换为实际训练模型的加载代码 !!!
model = SimpleMNISTCNN()
model.eval()
device = torch.device("cpu") # 在此示例结构中使用CPU
model.to(device)
# !!! 占位符结束 !!!
# 定义损失函数和优化器(ART分类器封装器需要)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 获取一批测试数据
data_iter = iter(test_loader)
x_test_batch, y_test_batch = next(data_iter)
x_test_batch, y_test_batch = x_test_batch.to(device), y_test_batch.to(device)
# 转换为numpy以供ART使用(一些ART函数更偏好numpy)
x_test_np = x_test_batch.cpu().numpy()
y_test_np = y_test_batch.cpu().numpy() # 保持为整数标签
ART要求将您的原生模型(PyTorch、TensorFlow等)封装在ART分类器对象中。此封装器为攻击和防御提供了一个标准化的API。
from art.estimators.classification import PyTorchClassifier
# 使用ART的PyTorchClassifier封装PyTorch模型
classifier = PyTorchClassifier(
model=model,
loss=criterion,
optimizer=optimizer, # 优化器对于推理攻击可能不是必需的,但这是一个好习惯
input_shape=(1, 28, 28), # MNIST图像形状(通道、高、宽)
nb_classes=10, # MNIST中的类别数量
clip_values=(0.0, 1.0) # 输入数据范围(MNIST张量通常在[0, 1]之间)
)
现在,classifier已准备好与ART的攻击实现配合使用。
PGD是FGSM的一种迭代扩展。它沿梯度方向执行多个小步骤,每步之后将结果投影回允许的扰动空间(Lp球)。这通常比单步方法能找到更有效的对抗样本。
PGD的重要参数:
norm:用于约束扰动的Lp范数(例如,L∞使用np.inf,L2使用2)。L∞常用于图像,限制每个像素的最大变化。eps:最大扰动幅度 ϵ。控制攻击的“强度”。eps_step:每次迭代的步长。应小于eps。max_iter:迭代次数。更多迭代可以找到更好的样本,但耗时更长。targeted:如果为True,则尝试使模型预测特定目标类别。如果为False(默认),则尝试导致任意错误分类。我们来实现L∞ PGD攻击:
from art.attacks.evasion import ProjectedGradientDescent
# 配置PGD攻击
pgd_attack = ProjectedGradientDescent(
estimator=classifier,
norm=np.inf, # 使用L-无限范数
eps=0.1, # 最大扰动(epsilon)- 根据模型/数据进行调整
eps_step=0.01, # 每次迭代的步长
max_iter=40, # 迭代次数
targeted=False, # 无目标攻击
num_random_init=1, # 使用随机初始化以增加攻击的有效性
batch_size=100
)
# 为测试批次生成对抗样本
print("正在生成PGD对抗样本...")
x_test_adv_pgd = pgd_attack.generate(x=x_test_np)
print("PGD生成完成。")
# 在原始样本和对抗样本上评估模型
predictions_clean = classifier.predict(x_test_np)
accuracy_clean = np.sum(np.argmax(predictions_clean, axis=1) == y_test_np) / len(y_test_np)
print(f"在原始样本上的准确率: {accuracy_clean * 100:.2f}%")
predictions_pgd = classifier.predict(x_test_adv_pgd)
accuracy_pgd = np.sum(np.argmax(predictions_pgd, axis=1) == y_test_np) / len(y_test_np)
print(f"在PGD对抗样本上的准确率 (eps={pgd_attack.eps:.2f}): {accuracy_pgd * 100:.2f}%")
# 计算平均L-无限失真
avg_linf_distortion_pgd = np.mean(np.max(np.abs(x_test_adv_pgd - x_test_np), axis=(1, 2, 3)))
print(f"平均L-无限失真(PGD): {avg_linf_distortion_pgd:.4f}")
与原始样本相比,您应该观察到对抗样本上的准确率显著下降,前提是您的模型没有专门训练以对抗PGD。平均L∞失真应接近指定的eps值。
C&W攻击是基于优化的,它将对抗样本的寻找视为一个约束优化问题。L2版本在寻找L2距离小的扰动方面特别有效,这意味着变化的整体幅度被最小化,与相似成功率下的L∞攻击相比,通常会产生视觉上不那么明显/可察觉的扰动。
C&W L2的参数:
confidence:控制错误类别的logit值与其它类别最大logit值之间的所需间隙。值越高,攻击越强,但可能增加失真。learning_rate:优化过程的学习率。binary_search_steps:寻找失真与分类损失之间最优折衷常数的步数。max_iter:每个二分搜索步骤内优化的最大迭代次数。batch_size:分批处理样本。我们来实现C&W L2攻击:
from art.attacks.evasion import CarliniL2Method
# 配置C&W L2攻击
# 注意:C&W可能计算成本很高,特别是在迭代/步数很多的情况下。
# 如有需要,可减少max_iter或binary_search_steps以加快执行速度。
cw_attack = CarliniL2Method(
classifier=classifier,
confidence=0.0, # 最小置信度间隙
learning_rate=0.01, # 优化器学习率
binary_search_steps=5, # 二分搜索步数
max_iter=10, # 每个二分搜索步骤的最大迭代次数
batch_size=100,
targeted=False # 无目标攻击
)
# 生成对抗样本
print("正在生成C&W L2对抗样本...")
# 警告:这可能很慢!为方便试验,请考虑在x_test_np的较小子集上运行。
# x_test_adv_cw = cw_attack.generate(x=x_test_np[:10]) # 示例:使用前10个样本
x_test_adv_cw = cw_attack.generate(x=x_test_np)
print("C&W L2生成完成。")
# 在C&W样本上评估模型
# 如果您使用了上面的子集,请仅在该子集和相应的y_test_np上进行评估
# y_test_subset = y_test_np[:10]
# accuracy_cw = np.sum(np.argmax(classifier.predict(x_test_adv_cw), axis=1) == y_test_subset) / len(y_test_subset)
predictions_cw = classifier.predict(x_test_adv_cw)
accuracy_cw = np.sum(np.argmax(predictions_cw, axis=1) == y_test_np) / len(y_test_np)
print(f"在C&W L2对抗样本上的准确率: {accuracy_cw * 100:.2f}%")
# 计算平均L2失真
avg_l2_distortion_cw = np.mean(np.linalg.norm((x_test_adv_cw - x_test_np).reshape(len(x_test_np), -1), axis=1))
print(f"平均L2失真(C&W): {avg_l2_distortion_cw:.4f}")
通常,C&W L2在达到较高攻击成功率(低准确率)的同时,能够保持较低的平均L2失真,这与PGD在相似成功率下可能达到的效果不同,尽管也存在受L2范数约束的PGD。其中的权衡点在于计算成本;C&W比PGD慢得多。
将原始图像、对抗版本以及扰动本身进行可视化是很有帮助的。
import matplotlib.pyplot as plt
# 选择一个示例索引(例如,第一个)
idx = 0
# 确保数据格式适合matplotlib (H, W) 或 (H, W, C)
# 压缩MNIST的通道维度
original_image = x_test_np[idx].squeeze()
adv_image_pgd = x_test_adv_pgd[idx].squeeze()
adv_image_cw = x_test_adv_cw[idx].squeeze()
perturbation_pgd = adv_image_pgd - original_image
perturbation_cw = adv_image_cw - original_image
# 获取此特定示例的模型预测
pred_orig_probs = F.softmax(torch.tensor(predictions_clean[idx:idx+1]), dim=1).detach().numpy().flatten()
pred_pgd_probs = F.softmax(torch.tensor(predictions_pgd[idx:idx+1]), dim=1).detach().numpy().flatten()
pred_cw_probs = F.softmax(torch.tensor(predictions_cw[idx:idx+1]), dim=1).detach().numpy().flatten()
pred_orig_label = np.argmax(pred_orig_probs)
pred_pgd_label = np.argmax(pred_pgd_probs)
pred_cw_label = np.argmax(pred_cw_probs)
true_label = y_test_np[idx]
# 绘图
fig, axes = plt.subplots(2, 3, figsize=(12, 8))
# 第1行: PGD
axes[0, 0].imshow(original_image, cmap='gray')
axes[0, 0].set_title(f"原始图像\n真实: {true_label}, 预测: {pred_orig_label}\n置信度: {pred_orig_probs[pred_orig_label]:.2f}")
axes[0, 0].axis('off')
# 增强扰动可视化效果
# 将颜色图中心设为零,并使用发散映射
pert_vis_pgd = axes[0, 1].imshow(perturbation_pgd, cmap='coolwarm', vmin=-pgd_attack.eps, vmax=pgd_attack.eps)
axes[0, 1].set_title(f"PGD扰动 ($L_\infty={np.max(np.abs(perturbation_pgd)):.3f}$)\n(视觉缩放)")
axes[0, 1].axis('off')
fig.colorbar(pert_vis_pgd, ax=axes[0, 1], shrink=0.7)
axes[0, 2].imshow(adv_image_pgd, cmap='gray')
axes[0, 2].set_title(f"PGD对抗样本\n预测: {pred_pgd_label}\n置信度: {pred_pgd_probs[pred_pgd_label]:.2f}")
axes[0, 2].axis('off')
# 第2行: C&W L2
axes[1, 0].imshow(original_image, cmap='gray')
axes[1, 0].set_title(f"原始图像\n真实: {true_label}, 预测: {pred_orig_label}\n置信度: {pred_orig_probs[pred_orig_label]:.2f}")
axes[1, 0].axis('off')
# 计算特定C&W扰动的L2范数
l2_pert_cw = np.linalg.norm(perturbation_cw.flatten())
pert_vis_cw = axes[1, 1].imshow(perturbation_cw, cmap='coolwarm', vmin=-np.abs(perturbation_cw).max(), vmax=np.abs(perturbation_cw).max())
axes[1, 1].set_title(f"C&W扰动 ($L_2={l2_pert_cw:.3f}$)\n(视觉缩放)")
axes[1, 1].axis('off')
fig.colorbar(pert_vis_cw, ax=axes[1, 1], shrink=0.7)
axes[1, 2].imshow(adv_image_cw, cmap='gray')
axes[1, 2].set_title(f"C&W L2对抗样本\n预测: {pred_cw_label}\n置信度: {pred_cw_probs[pred_cw_label]:.2f}")
axes[1, 2].axis('off')
plt.tight_layout()
plt.show()
观察对抗样本在人眼看来与原始图像非常相似,但模型的预测却发生了明显变化。请注意PGD (L∞) 和 C&W (L2) 扰动之间的结构和幅度差异。PGD通常会在许多像素上用满 ϵ 预算,而C&W L2可能会产生更平滑、更分散的变化。
本次实践为实现规避攻击提供了一个起点。请考虑以下后续步骤:
eps、eps_step、max_iter,以及C&W的confidence、learning_rate、max_iter、binary_search_steps。观察这些如何影响攻击成功率和失真(L∞, L2)。norm=2的PGD,并将其结果(准确率、L2失真)与PGD L∞和C&W L2进行比较。targeted=True。您将需要提供目标标签(例如,y_target = (y_test_np + 1) % 10)。分析有目标攻击是否更难或更容易生成。FastGradientMethod)、基本迭代方法(BIM - 本质上是num_random_init=0的PGD)、DeepFool(DeepFool),或基于决策的攻击,如边界攻击(BoundaryAttack)。eps等参数可能需要根据数据集和输入归一化进行显著调整。通过实现和试验这些基础攻击,您将获得对构建对抗样本机制的实践理解,这对于理解攻击能力以及构建防御的挑战都非常重要。
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造