摘要:根据 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 攻击是 白盒 攻击,其目标是错误分类。有了这些背景信息,我们现在就可以详细讨论攻击。
Fast Gradient Sign Attack(FGSM)
它旨在通过以下方式攻击神经网络:
利用他们的学习方式,梯度。这个想法很简单,而是而不是通过根据反向传播梯度,攻击调整输入数据以最大化基于相同反向传播梯度的损失。换句话说,攻击使用损失的梯度 w.r.t 输入数据,然后调整输入数据以最大化损失。
在我们进入代码之前,让我们看一下著名的 FGSM熊猫示例 和摘录一些符号。
从图中可以看出, x \mathbf{x} x 是原始输入图像
正确归类为“熊猫”, y y y是地面真相标签
for x \mathbf{x} x, θ \mathbf{\theta} θ 表示模型
参数, J ( θ , x , y ) J(\mathbf{\theta}, \mathbf{x}, y) J(θ,x,y) 是损失
用于训练网络。攻击反向传播
梯度返回输入数据进行计算
∇ x J ( θ , x , y ) \nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y) ∇xJ(θ,x,y).然后,它调整
按一小步输入数据( ϵ \epsilon ϵ 或 0.007 0.007 0.007中的
图片)在方向(即
s i g n ( ∇ x J ( θ , x , y ) ) sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)) sign(∇xJ(θ,x,y)))
最大化损失。由此产生的扰动图像 x ′ x' x′,则
被目标网络错误地分类为“长臂猿”,当它仍然显然是一只“熊猫”。
下面进入实现。
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)
在本节中,我们将讨论本教程的输入参数,定义受攻击的模型,然后对攻击进行编码并运行一些测试。
本教程只有三个输入,定义为遵循:
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。
# 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_attack
函数需要三个输入,图像是原始干净的图像( x x x),epsilon是
像素级扰动量 ( ϵ \epsilon ϵ) 和 data_grad
是输入图像的损耗的梯度
( ∇ x J ( θ , x , y ) \nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y) ∇xJ(θ,x,y)).功能
然后创建扰动图像作为
KaTeX parse error: No such environment: align at position 8: \begin{̲a̲l̲i̲g̲n̲}̲perturbed\_imag…
最后,为了保持数据的原始范围,
扰动图像被裁剪到 [ 0 , 1 ] [0,1] [0,1]范围内。
# 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 ϵ。更具体地说,对于测试集中的每个样本,该函数计算输入数据( d a t a _ g r a d data\_grad data_grad)的损失会产生扰动带有“fgsm_attack”( p e r t u r b e d _ d a t a perturbed\_data perturbed_data)的图像,然后检查以查看如果令人不安的例子是对抗性的。除了测试模型的精度,该函数还保存并返回一些成功的对抗性示例将在以后可视化。
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 ϵ 值运行完整的测试步骤。
对于每一个ε,我们还保存了最终的精度和一些成功的结果.在接下来的章节中,我们将列举一些对抗性的例子。注意随着ε值的增加,模型精度降低。而且注意 ϵ = 0 \epsilon=0 ϵ=0 表示原始测试精度,即不进行攻击。
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
第一个结果是准确性与 ϵ \epsilon ϵ 的关系图。
如前所述,早些时候,随着 ϵ \epsilon ϵ 的增加,我们预计测试精度会降低。
这是因为更大的 ϵ \epsilon ϵ 意味着我们在方向将最大化损失。请注意,即使 ϵ \epsilon ϵ 值是线性间隔的,准确性也不是线性的。
例如, ϵ = 0.05 \epsilon=0.05 ϵ=0.05时的精度仅低约4%
大于 ϵ = 0 \epsilon=0 ϵ=0,但 ϵ = 0.2 \epsilon=0.2 ϵ=0.2 时的准确率为 25%
低于 ϵ = 0.15 \epsilon=0.15 ϵ=0.15。另外,请注意模型的准确性
达到 10 类分类器的随机精度,介于
ϵ = 0.25 \epsilon=0.25 ϵ=0.25 和 ϵ = 0.3 \epsilon=0.3 ϵ=0.3。
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()
还记得没有免费午餐的想法吗?在这种情况下,随着 ϵ \epsilon ϵ 的增加,测试正确率降低但是扰动变得更加容易感知。
实际上,攻击者必须考虑权衡。在这里,我们在每个 ϵ \epsilon ϵ 上展示一些成功的对抗示例价值。图的每一行都显示不同的 ϵ \epsilon ϵ 值。第一个行是 ϵ = 0 \epsilon=0 ϵ=0 示例,表示原始“干净”的图像,无扰动。每个图像的标题显示原始分类 - >对抗性分类
。请注意,扰动在 ϵ = 0.15 \epsilon=0.15 ϵ=0.15 时开始变得明显,并且很明显在 ϵ = 0.3 \epsilon=0.3 ϵ=0.3。
然而,在所有情况下,人类都是仍然能够识别正确的类。
# 绘制每个 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()
Adversarial Example Generation — PyTorch Tutorials 1.11.0+cu102 documentation