对抗样本生成
学习目标
本课程将提高你对机器学习模型安全漏洞的意识,并深入探讨对抗性机器学习这一热门话题。你可能会惊讶地发现,对图像添加难以察觉的微小扰动,可能会导致模型性能的显著差异。我们将通过一个图像分类器的例子来探讨这个话题。具体来说,我们将使用最早且最受欢迎的攻击方法之一——快速梯度符号攻击(Fast Gradient Sign Attack, FGSM)来欺骗一个 MNIST 分类器。
相关知识点
- 对抗样本生成
学习内容
1 对抗样本生成
1.1 威胁模型
为了更好地理解,对抗性攻击有多个类别,每个类别都有不同的目标和对攻击者知识的假设。然而,总体目标是向输入数据添加最少的扰动,以实现期望的错误分类。攻击者的知识假设主要有两种:白盒攻击和黑盒攻击。白盒攻击假设攻击者对模型有完全的了解和访问权限,包括架构、输入、输出和权重。黑盒攻击假设攻击者只能访问模型的输入和输出,对底层架构或权重一无所知。此外,攻击目标也有多种类型,包括错误分类和源/目标错误分类。错误分类的目标是攻击者只希望输出分类是错误的,但不关心新的分类是什么。源/目标错误分类的目标是攻击者希望将原本属于特定源类别的图像改变为被分类为特定目标类别的图像。
在本课程中,FGSM 攻击是一种具有错误分类目标的白盒攻击。有了这些背景信息,我们现在可以详细讨论这种攻击。
1.2 快速梯度符号攻击
最早且最受欢迎的对抗性攻击之一是 快速梯度符号攻击(Fast Gradient Sign Attack, FGSM),由 Goodfellow 等人在Explaining and Harnessing Adversarial Examples中描述。这种攻击非常强大且直观。它通过利用神经网络的学习方式——梯度来攻击神经网络。其核心思想很简单:与其通过根据反向传播的梯度调整权重来最小化损失,攻击则通过相同的反向传播梯度来调整输入数据以最大化损失。换句话说,攻击利用损失对输入数据的梯度,然后调整输入数据以最大化损失。
在我们进入代码之前,让我们先看看著名的FGSM熊猫示例并提取一些符号。

从图中可以看出,x\mathbf{x}x 是被正确分类为“熊猫”的原始输入图像,yyy 是 x\mathbf{x}x 的真实标签,θ\mathbf{\theta}θ 表示模型参数,J(θ,x,y)J(\mathbf{\theta}, \mathbf{x}, y)J(θ,x,y) 是用于训练网络的损失函数。攻击通过反向传播将梯度回传到输入数据,以计算 ∇xJ(θ,x,y)\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)∇xJ(θ,x,y)。然后,它在最大化损失的方向(即 sign(∇xJ(θ,x,y))sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y))sign(∇xJ(θ,x,y)))上调整输入数据一个小步长(图中的 ϵ\epsilonϵ 或 0.0070.0070.007)。最终生成的扰动图像 x′x'x′ 被目标网络错误分类为“长臂猿”,尽管它仍然明显是一只“熊猫”。
注:下列命令是Jupyter notebook或Ipython的即时显示图像。在命令行中需要自行输入plt.show()显示图像。
%matplotlib inline
from __future__ import print_function
import torch
import torch_npu
from torch_npu.contrib import transfer_to_npu
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
from six.moves import urllib
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
urllib.request.install_opener(opener)
1.3 实现
在本课程中,将设置输入参数,定义被攻击的模型,然后编写攻击代码并运行一些测试。
1.3.1 输入
本课程只有三个输入参数,定义如下:
-
epsilons - 用于运行的 epsilon 值列表。在列表中保留 0 是非常重要的,因为 0 代表模型在原始测试集上的性能。此外,直观上期望 epsilon 值越大,扰动就越明显,但攻击在降低模型准确率方面也越有效。由于数据范围是 [0,1][0,1][0,1],因此任何 epsilon 值都不应超过 1。
-
pretrained_model - 预训练 MNIST 模型的路径,为了方便起见,可以运行下一单元格下载预训练模型。
-
use_cuda - 一个布尔标志,用于在需要且可用时启用 CUDA。需要注意的是,对于本课程,使用的是NPU环境。
!wget https://model-community-picture.obs.cn-north-4.myhuaweicloud.com/ascend-zone/notebook_models/c15903caea9a11efa507fa163edcddae/lenet_mnist_model.pth
!wget https://model-community-picture.obs.cn-north-4.myhuaweicloud.com/ascend-zone/notebook_datasets/c02b287e324a11f0937ef8fe5e46a8fb/data.zip
!unzip data.zip
epsilons = [0, .05, .1, .15, .2, .25, .3]
pretrained_model = "lenet_mnist_model.pth"
use_cuda=True
1.3.2 被攻击的模型
如上所述,被攻击的模型是 MNIST 模型。你可以自行训练并保存一个 MNIST 模型,也可以下载并使用提供的模型。这里的 Net 定义和测试数据加载器是从 MNIST 示例中复制过来的。本节的目的是定义模型和数据加载器,然后初始化模型并加载预训练的权重。
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.conv2_drop = nn.Dropout2d()
self.fc1 = nn.Linear(320, 50)
self.fc2 = nn.Linear(50, 10)
def forward(self, x):
x = F.relu(F.max_pool2d(self.conv1(x), 2))
x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
x = x.view(-1, 320)
x = F.relu(self.fc1(x))
x = F.dropout(x, training=self.training)
x = self.fc2(x)
return F.log_softmax(x, dim=1)
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('./data', train=False, download=False, transform=transforms.Compose([
transforms.ToTensor(),
])),
batch_size=1, shuffle=True)
print("CUDA Available: ",torch.cuda.is_available())
device = torch.device("cuda" if (use_cuda and torch.cuda.is_available()) else "cpu")
model = Net().to(device)
model.load_state_dict(torch.load(pretrained_model, map_location='cpu'))
model.eval()
1.3.3 FGSM 攻击
现在,我们可以定义一个通过扰动原始输入来创建对抗样本的函数。fgsm_attack 函数接受三个输入:image 是原始干净的图像 (xxx),epsilon 是像素级扰动量 (ϵ\epsilonϵ),而 data_grad 是损失对输入图像的梯度 (∇xJ(θ,x,y)\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)∇xJ(θ,x,y))。该函数随后按照以下公式创建扰动图像:
\begin{align}perturbed_image = image + epsilon*sign(data_grad) = x + \epsilon * sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y))\end{align}
最后,为了保持数据的原始范围,扰动后的图像会被裁剪到范围 [0,1][0,1][0,1]。
def fgsm_attack(image, epsilon, data_grad):
sign_data_grad = data_grad.sign()
perturbed_image = image + epsilon*sign_data_grad
perturbed_image = torch.clamp(perturbed_image, 0, 1)
return perturbed_image
1.3.4 测试函数
最后,本课程的核心结果来自于 test 函数。每次调用该测试函数都会对 MNIST 测试集执行完整的测试步骤,并报告最终的准确率。然而,注意这个函数还接受一个 epsilon 输入。这是因为 test 函数报告的是在受到强度为 ϵ\epsilonϵ 的对手攻击时模型的准确率。更具体地说,对于测试集中的每个样本,该函数计算损失对输入数据的梯度(data_graddata\_graddata_grad),使用 fgsm_attack 创建扰动图像(perturbed_dataperturbed\_dataperturbed_data),然后检查扰动样本是否为对抗样本。除了测试模型的准确率之外,该函数还会保存并返回一些成功的对抗样本,以便后续可视化。
def test(model, device, test_loader, epsilon):
correct = 0
adv_examples = []
for data, target in tqdm(test_loader,
desc=f"Testing with epsilon={epsilon:.2f}",
unit="batch",
dynamic_ncols=True):
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 = F.nll_loss(output, target)
model.zero_grad()
loss.backward()
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]
if final_pred.item() == target.item():
correct += 1
if (epsilon == 0) 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 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.dataset)) # 注意这里应该是数据集的总样本数
print("Epsilon: {}\tTest Accuracy = {} / {} = {}".format(epsilon, correct, len(test_loader.dataset), final_acc))
return final_acc, adv_examples

1.3.5 运行攻击
实现的最后部分是真正运行攻击。在这里,对 epsilons 输入中的每个 epsilon 值运行一个完整的测试步骤。对于每个 epsilon,还会保存最终的准确率和一些成功的对抗样本,以便在接下来的部分中进行绘制。注意,随着 epsilon 值的增加,打印出的准确率会下降。此外,ϵ=0\epsilon=0ϵ=0 的情况代表原始测试准确率,即没有攻击时的情况。
accuracies = []
examples = []
for eps in epsilons:
acc, ex = test(model, device, test_loader, eps)
accuracies.append(acc)
examples.append(ex)
1.4 结果
1.4.1 准确率与 Epsilon 的关系
第一个结果是准确率与 epsilon 的关系图。正如前面提到的,随着 epsilon 的增加,期望测试准确率会下降。这是因为较大的 epsilon 意味着在最大化损失的方向上迈出了更大的步长。注意,尽管 epsilon 的值是线性分布的,但曲线的趋势并不是线性的。例如,在 ϵ=0.05\epsilon=0.05ϵ=0.05 时,准确率仅比 ϵ=0\epsilon=0ϵ=0 时低约 4%,而在 ϵ=0.2\epsilon=0.2ϵ=0.2 时,准确率比 ϵ=0.15\epsilon=0.15ϵ=0.15 时低了 25%。此外,注意模型的准确率在 ϵ=0.25\epsilon=0.25ϵ=0.25 和 ϵ=0.3\epsilon=0.3ϵ=0.3 之间达到了一个 10 类分类器的随机准确率水平。
plt.figure(figsize=(5,5))
plt.plot(epsilons, accuracies, "*-")
plt.yticks(np.arange(0, 1.1, step=0.1))
plt.xticks(np.arange(0, .35, step=0.05))
plt.title("Accuracy vs Epsilon")
plt.xlabel("Epsilon")
plt.ylabel("Accuracy")
plt.show()

1.4.2 对抗样本示例
还记得“没有免费午餐”的原则吗?在这种情况下,随着 epsilon 的增加,测试准确率会下降,但扰动变得更加容易察觉。在现实中,攻击者必须考虑准确率下降和可察觉性之间的权衡。在这里,展示了每个 epsilon 值下成功的对抗样本示例。图中的每一行代表一个不同的 epsilon 值。第一行是 ϵ=0\epsilon=0ϵ=0 的示例,代表原始的“干净”图像,没有任何扰动。每张图像的标题显示了“原始分类 -> 对抗分类”。注意,扰动在 ϵ=0.15\epsilon=0.15ϵ=0.15 时开始变得明显,并且在 ϵ=0.3\epsilon=0.3ϵ=0.3 时非常明显。然而,在所有情况下,人类仍然能够识别正确的类别,尽管添加了噪声。
cnt = 0
plt.figure(figsize=(8,10))
for i in range(len(epsilons)):
for j in range(len(examples[i])):
cnt += 1
plt.subplot(len(epsilons),len(examples[0]),cnt)
plt.xticks([], [])
plt.yticks([], [])
if j == 0:
plt.ylabel("Eps: {}".format(epsilons[i]), fontsize=14)
orig,adv,ex = examples[i][j]
plt.title("{} -> {}".format(orig, adv))
plt.imshow(ex, cmap="gray")
plt.tight_layout()
plt.show()



9万+

被折叠的 条评论
为什么被折叠?



