Framist's Little House

◇ 自顶而下 - 面向未来 ◇

0%

对抗性样本攻击实验:FGSM

对抗性样本攻击实验

摘要:根据 PyTorch 官网教程中 Adversarial Example Generation 章节内容,完整实现 Fast Gradient Sign Attack (FGSM) 算法。

题目描述

根据 PyTorch 官网教程中 Adversarial Example Generation 章节内容,完整实现 Fast Gradient Sign Attack (FGSM) 算法。
网址:https://pytorch.org/tutorials/beginner/fgsm_tutorial.html

欺骗模型的方法

本文参考:https://pytorch.org/tutorials/beginner/fgsm_tutorial.html

添加不可察觉对图像的扰动可以导致截然不同的模型性能。

我们将探讨该主题通过图像分类器上的示例。具体来说,我们将使用其中之一第一种也是最流行的攻击方法,快速梯度符号攻击(FGSM),以愚弄 MNIST 分类器。

有许多类别的对抗性攻击,每种攻击都有不同的目标和对攻击者知识的假设。但是,总体而言,首要目标是向输入数据添加最少量的扰动,从而导致所需的错误分类。攻击者的知识有几种假设,其中两种是:白盒黑匣子

  • 白盒攻击假设攻击者具有对模型的全部了解和访问权限,包括体系结构、输入、输出和权重。
  • 黑盒攻击假定攻击者只能访问模型的输入和输出,而对底层体系结构或权重一无所知。

还有几种类型的目标,包括错误分类源/目标错误分类

  • 错误分类的目标意味着对手只希望输出分类是错误的,但并不关心新分类是什么。
  • 源/目标错误分类意味着对手想要更改原始属于特定源类的图像,以便将其分类为特定目标类。

在这种情况下,FGSM 攻击是 白盒 攻击,其目标是错误分类。有了这些背景信息,我们现在就可以详细讨论攻击。

FGSM 快速梯度符号攻击

Fast Gradient Sign Attack (FGSM)

它旨在通过以下方式攻击神经网络:
利用他们的学习方式,梯度。这个想法很简单,而是而不是通过根据反向传播梯度,攻击调整输入数据以最大化基于相同反向传播梯度的损失。换句话说,攻击使用损失的梯度 w.r.t 输入数据,然后调整输入数据以最大化损失。

原理介绍

在我们进入代码之前,让我们看一下著名的 FGSM 熊猫示例 和摘录一些符号。

从图中可以看出,$\mathbf{x}$ 是原始输入图像
正确归类为“熊猫”,$y$是地面真相标签
for $\mathbf{x}$, $\mathbf{\theta}$ 表示模型
参数,$J(\mathbf{\theta}, \mathbf{x},y)$ 是损失
用于训练网络。攻击反向传播
梯度返回输入数据进行计算
$\nabla_{x} J(\mathbf{\theta}, \mathbf{x},y)$.然后,它调整
按一小步输入数据($\epsilon$ 或 $0.007$中的
图片)在方向(即
$sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y))$)
最大化损失。由此产生的扰动图像$x’$,则
被目标网络错误地分类为“长臂猿”,当它仍然显然是一只“熊猫”。

下面进入实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from __future__ import print_function
import torch
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

# NOTE: This is a hack to get around "User-agent" limitations when downloading MNIST datasets
# see, https://github.com/pytorch/vision/issues/3497 for more information
# from six.moves import urllib
# opener = urllib.request.build_opener()
# opener.addheaders = [('User-agent', 'Mozilla/5.0')]
# urllib.request.install_opener(opener)

FGSM 实现

在本节中,我们将讨论本教程的输入参数,定义受攻击的模型,然后对攻击进行编码并运行一些测试。

本教程只有三个输入,定义为遵循:

  • epsilons - 用于运行的 $\epsilon$值的列表。是的
    重要的是在列表中保留 0,因为它表示模型
    原始测试集上的性能。此外,直观地说,我们会
    预期 $\epsilon$越大,扰动越明显
    但攻击在退化模型方面越有效
    准确性。由于此处的数据范围是 $[0,1]$,因此没有 epsilon
    值应超过 1。
  • pretrained_model - 通往预训练的 MNIST 模型的路径
  • use_cuda - 使用 CUDA 或不使用。
1
2
3
epsilons = [0, .05, .1, .15, .2, .25, .3]
pretrained_model = "./data/lenet_mnist_model.pth"
use_cuda=True

受攻击的模型

如前所述,受到攻击的模型与来自 pytorch/examples/mnist

您可以训练并保存自己的 MNIST 模型,也可以下载并使用提供的模型。此处的 Net 定义和测试数据加载器具有
是从 MNIST 示例复制的。

本节的目的是定义模型和数据加载器,然后初始化模型并加载预先训练的 weight。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# LeNet 模型定义
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() # dropout 层
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) # dropout 层
x = self.fc2(x) # 全连接层
return F.log_softmax(x, dim=1) # 输出


# MNIST 测试数据集和数据加载器声明
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('./data',
train=False, # 不使用训练集
download=True, # 下载数据集
transform=transforms.Compose([
transforms.ToTensor(),
])),
batch_size=1, shuffle=True) # 批大小为 1,打乱数据

# 自动选择 GPU 或 CPU
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'))

# 将模型设置为评估模式。在本例中,这是针对 Dropout layers 的
model.eval()
CUDA Available:  True

Net(
  (conv1): Conv2d(1, 10, kernel_size=(5, 5), stride=(1, 1))
  (conv2): Conv2d(10, 20, kernel_size=(5, 5), stride=(1, 1))
  (conv2_drop): Dropout2d(p=0.5, inplace=False)
  (fc1): Linear(in_features=320, out_features=50, bias=True)
  (fc2): Linear(in_features=50, out_features=10, bias=True)
)

FGSM 攻击函数

现在,我们可以通过以下方式定义创建对抗性示例的函数:

扰动原始输入。fgsm_attack函数需要三个输入,图像是原始干净的图像($x$),epsilon
像素级扰动量($\epsilon$)和 data_grad
是输入图像的损耗的梯度
($\nabla_{x} J(\mathbf{\theta}, \mathbf{x},y)$).功能
然后创建扰动图像作为

$$
\begin{align}perturbed_image = image + epsilonsign(data_grad) = x + \epsilon sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y))\end{align}
$$

最后,为了保持数据的原始范围,
扰动图像被裁剪到 $[0,1]$范围内。

1
2
3
4
5
6
7
8
9
10
# FGSM attack code
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

测试攻击效果函数

最后,本教程的核心结果来自test。每次调用此测试函数都会执行一个完整的测试步骤 MNIST 测试集并报告最终精度。但是,请注意此函数还采用 epsilon 输入。这是因为“测试”函数报告受攻击的模型的准确性来自具有实力的对手 $\epsilon$。更具体地说,对于测试集中的每个样本,该函数计算输入数据($data_grad$)的损失会产生扰动带有“fgsm_attack”($perturbed_data$)的图像,然后检查以查看如果令人不安的例子是对抗性的。除了测试模型的精度,该函数还保存并返回一些成功的对抗性示例将在以后可视化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def test( model, device, test_loader, epsilon):

# 正确率计数器
correct = 0
adv_examples = []

# 循环访问测试集中的所有示例
for data, target in test_loader:

# 将数据和标签发送到 device
data, target = data.to(device), target.to(device)

# 设置 tensor 的 requires_grad 属性。这对攻击很重要
data.requires_grad = True

# 通过模型向前传递数据
output = model(data)
init_pred = output.max(1, keepdim=True)[1] # 获取最大对数概率(log-probability)的索引

# 如果最初的预测是错误的,直接跳过
if init_pred.item() != target.item():
continue

# 计算 loss
loss = F.nll_loss(output, target)

# Zero all existing gradients
model.zero_grad()

# Calculate gradients of model in backward pass
loss.backward()

# 收集数据梯度
data_grad = data.grad.data

# Call FGSM Attack
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
# 保存 0 epsilon 示例的特殊情况
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) )

# 计算此 epsilon 的最终精度
final_acc = correct/float(len(test_loader))
print("Epsilon: {}\tTest Accuracy = {} / {} = {}".format(epsilon, correct, len(test_loader), final_acc))

# 返回准确性和对抗性示例
return final_acc, adv_examples

实施攻击

实现的最后一部分是实际运行攻击。在这里我们为epsilons输入中的每个 $\epsilon$ 值运行完整的测试步骤。

对于每一个ε,我们还保存了最终的精度和一些成功的结果。在接下来的章节中,我们将列举一些对抗性的例子。注意随着ε值的增加,模型精度降低。而且注意 $\epsilon=0$ 表示原始测试精度,即不进行攻击。

1
2
3
4
5
6
7
8
accuracies = []
examples = []

# 对每个ε运行测试
for eps in epsilons:
acc, ex = test(model, device, test_loader, eps)
accuracies.append(acc)
examples.append(ex)
Epsilon: 0	Test Accuracy = 9810 / 10000 = 0.981
Epsilon: 0.05	Test Accuracy = 9426 / 10000 = 0.9426
Epsilon: 0.1	Test Accuracy = 8510 / 10000 = 0.851
Epsilon: 0.15	Test Accuracy = 6826 / 10000 = 0.6826
Epsilon: 0.2	Test Accuracy = 4301 / 10000 = 0.4301
Epsilon: 0.25	Test Accuracy = 2082 / 10000 = 0.2082
Epsilon: 0.3	Test Accuracy = 869 / 10000 = 0.0869

结果分析

准确性 vs $\epsilon$

第一个结果是准确性与 $\epsilon$ 的关系图。

如前所述,早些时候,随着 $\epsilon$ 的增加,我们预计测试精度会降低。

这是因为更大的 $\epsilon$ 意味着我们在方向将最大化损失。请注意,即使 $\epsilon$ 值是线性间隔的,准确性也不是线性的。

例如,$\epsilon=0.05$时的精度仅低约 4%
大于 $\epsilon=0$,但 $\epsilon=0.2$ 时的准确率为 25%
低于 $\epsilon=0.15$。另外,请注意模型的准确性
达到 10 类分类器的随机精度,介于
$\epsilon=0.25$ 和 $\epsilon=0.3$。

1
2
3
4
5
6
7
8
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()

Image

对抗样本实例

还记得没有免费午餐的想法吗?在这种情况下,随着 $\epsilon$ 的增加,测试正确率降低但是扰动变得更加容易感知。

实际上,攻击者必须考虑权衡。在这里,我们在每个 $\epsilon$ 上展示一些成功的对抗示例价值。图的每一行都显示不同的 $\epsilon$ 值。第一个行是 $\epsilon=0$ 示例,表示原始“干净”的图像,无扰动。每个图像的标题显示原始分类 - >对抗性分类。请注意,扰动在 $\epsilon=0.15$ 时开始变得明显,并且很明显在 $\epsilon=0.3$。

然而,在所有情况下,人类都是仍然能够识别正确的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 绘制每个 epsilon 处的对抗性样本的几个示例
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),color=("green" if orig==adv else "red"))
plt.imshow(ex, cmap="gray")
plt.tight_layout()
plt.show()

Image

参考

Adversarial Example Generation — PyTorch Tutorials 1.11.0+cu102 documentation