备注
点击 这里 下载完整示例代码
对抗样本生成¶
Created On: Aug 14, 2018 | Last Updated: Jan 27, 2025 | Last Verified: Not Verified
作者: Nathan Inkawhich
如果你正在阅读这篇文章,希望你能够欣赏一些机器学习模型的强大。研究正在不断推动机器学习模型变得更快、更准、更高效。然而,在设计和训练模型时,被忽视的一个方面是安全性和鲁棒性,特别是在面对想欺骗模型的攻击者时。
本教程将提高你对机器学习模型的安全漏洞的认识,给予对抗性机器学习这一热门话题的洞见。你可能会惊讶地发现,对图像加入几乎察觉不到的扰动会大幅改变模型表现。由于这是一个教程,我们将通过一个图像分类器的示例来探索这一主题。具体来说,我们将使用一种最早且最流行的攻击方法——快速梯度符号攻击(FGSM)来欺骗 MNIST 分类器。
威胁模型¶
从背景上看,对抗性攻击有许多类别,每种类别有不同的目标和对攻击者知识的假设。然而,一般来说,总体目标是对输入数据添加尽可能少的扰动以实现期望的错误分类。攻击者知识的假设有几种类型,其中两种是:白盒**和**黑盒。白盒攻击*假设攻击者对模型拥有完全的知识和访问权限,包括架构、输入、输出和权重。*黑盒攻击*假设攻击者只能访问模型的输入和输出,对底层架构和权重一无所知。攻击目标也有几种类型,包括**错误分类**和**源/目标错误分类*。目标为*错误分类*意味着攻击者只希望输出分类是错误的,而不关心新的分类是什么。*源/目标错误分类*则意味着攻击者希望将原本属于某个特定源类的图像修改为被分类为某个特定目标类。
在这种情况下,FGSM 攻击是一种*白盒*攻击,目标是*错误分类*。有了这些背景知识,现在可以详细讨论攻击方式。
快速梯度符号攻击¶
迄今为止最早且最流行的对抗性攻击之一被称为*快速梯度符号攻击(FGSM)*,由 Goodfellow 等人在 解释和利用对抗性样本 中描述。该攻击非常强大,同时也很直观。它旨在通过利用神经网络的学习方式(即梯度)来攻击神经网络。其思路简单,与通过根据反向传播的梯度调整权重以最小化损失相反,该攻击是使用相同的反向传播梯度来*调整输入数据以最大化损失*。换句话说,该攻击使用损失相对于输入数据的梯度,然后调整输入数据以最大化损失。
在我们进入代码之前,让我们看一下著名的 FGSM 熊猫示例并提取一些符号。

从图中,\(\mathbf{x}\) 是被正确分类为“熊猫”的原始输入图像,\(y\) 是 \(\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'\) 被目标网络*错误分类*为“长臂猿”,尽管它仍显然是一只“熊猫”。
希望现在本教程的动机已经很清晰了,那我们开始进行实现吧。
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
实现¶
在本节中,我们将讨论教程的输入参数,定义受攻击的模型,然后编写攻击代码并运行一些测试。
输入¶
本教程只有三个输入参数,定义如下:
epsilons
- 用于运行的 epsilon 值列表。确保列表中包含 0,这表示模型在原始测试集上的表现。另外,直观上我们可以预期 epsilon 越大,扰动越明显,但攻击效果越显著(模型准确率下降)。由于数据范围为 \([0,1]\),epsilon 值不得超过 1。pretrained_model
- 训练好的 MNIST 模型的路径,该模型使用 pytorch/examples/mnist 训练。为了简便,可以从 这里 下载预训练模型。
epsilons = [0, .05, .1, .15, .2, .25, .3]
pretrained_model = "data/lenet_mnist_model.pth"
# Set random seed for reproducibility
torch.manual_seed(42)
受攻击的模型¶
如前所述,受攻击的模型是 pytorch/examples/mnist 中的 MNIST 模型。你可以自己训练并保存 MNIST 模型,也可以下载并使用提供的模型。此部分的目的在于定义模型和数据加载器,然后初始化模型并加载训练好的权重。
# LeNet Model definition
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 32, 3, 1)
self.conv2 = nn.Conv2d(32, 64, 3, 1)
self.dropout1 = nn.Dropout(0.25)
self.dropout2 = nn.Dropout(0.5)
self.fc1 = nn.Linear(9216, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x = F.relu(x)
x = F.max_pool2d(x, 2)
x = self.dropout1(x)
x = torch.flatten(x, 1)
x = self.fc1(x)
x = F.relu(x)
x = self.dropout2(x)
x = self.fc2(x)
output = F.log_softmax(x, dim=1)
return output
# MNIST Test dataset and dataloader declaration
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('../data', train=False, download=True, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,)),
])),
batch_size=1, shuffle=True)
# We want to be able to train our model on an `accelerator <https://pytorch.org/docs/stable/torch.html#accelerators>`__
# such as CUDA, MPS, MTIA, or XPU. If the current accelerator is available, we will use it. Otherwise, we use the CPU.
device = torch.accelerator.current_accelerator().type if torch.accelerator.is_available() else "cpu"
print(f"Using {device} device")
# Initialize the network
model = Net().to(device)
# Load the pretrained model
model.load_state_dict(torch.load(pretrained_model, map_location=device, weights_only=True))
# Set the model in evaluation mode. In this case this is for the Dropout layers
model.eval()
FGSM 攻击¶
现在,我们可以定义创建对抗样本的函数,通过扰动原始输入来生成对抗样本。fgsm_attack
函数接收三个输入,image 是原始干净图片 (\(x\)),epsilon 是逐像素的扰动量 (\(\epsilon\)),data_grad 是损失相对于输入图片的梯度 (\(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)\))。函数将生成扰动图像为:
最后,为了保持数据的原始范围,对扰动图像进行裁剪,使其范围在 \([0,1]\)。
# FGSM attack code
def fgsm_attack(image, epsilon, data_grad):
# Collect the element-wise sign of the data gradient
sign_data_grad = data_grad.sign()
# Create the perturbed image by adjusting each pixel of the input image
perturbed_image = image + epsilon*sign_data_grad
# Adding clipping to maintain [0,1] range
perturbed_image = torch.clamp(perturbed_image, 0, 1)
# Return the perturbed image
return perturbed_image
# restores the tensors to their original scale
def denorm(batch, mean=[0.1307], std=[0.3081]):
"""
Convert a batch of tensors to their original scale.
Args:
batch (torch.Tensor): Batch of normalized tensors.
mean (torch.Tensor or list): Mean used for normalization.
std (torch.Tensor or list): Standard deviation used for normalization.
Returns:
torch.Tensor: batch of tensors without normalization applied to them.
"""
if isinstance(mean, list):
mean = torch.tensor(mean).to(device)
if isinstance(std, list):
std = torch.tensor(std).to(device)
return batch * std.view(1, -1, 1, 1) + mean.view(1, -1, 1, 1)
测试功能¶
最后,本教程的核心结果来自``test``函数。每次调用此测试函数都会对MNIST测试集执行完整的测试步骤并报告最终准确率。然而,请注意,该函数还接受一个 epsilon 输入。这是因为``test``函数会报告模型在由强度 \(\epsilon\) 的对抗性攻击下的准确率。具体来说,对于测试集中的每个样本,该函数计算损失相对于输入数据的梯度 (\(data\_grad\)),然后使用``fgsm_attack``创建一个扰动图像 (\(perturbed\_data\)),接着检查这个扰动样本是否为对抗样本。除了测试模型的准确率外,函数还保存并返回一些成功的对抗样本,以便后续可视化。
def test( model, device, test_loader, epsilon ):
# Accuracy counter
correct = 0
adv_examples = []
# Loop over all examples in test set
for data, target in test_loader:
# Send the data and label to the device
data, target = data.to(device), target.to(device)
# Set requires_grad attribute of tensor. Important for Attack
data.requires_grad = True
# Forward pass the data through the model
output = model(data)
init_pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability
# If the initial prediction is wrong, don't bother attacking, just move on
if init_pred.item() != target.item():
continue
# Calculate the loss
loss = F.nll_loss(output, target)
# Zero all existing gradients
model.zero_grad()
# Calculate gradients of model in backward pass
loss.backward()
# Collect ``datagrad``
data_grad = data.grad.data
# Restore the data to its original scale
data_denorm = denorm(data)
# Call FGSM Attack
perturbed_data = fgsm_attack(data_denorm, epsilon, data_grad)
# Reapply normalization
perturbed_data_normalized = transforms.Normalize((0.1307,), (0.3081,))(perturbed_data)
# Re-classify the perturbed image
output = model(perturbed_data_normalized)
# Check for success
final_pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability
if final_pred.item() == target.item():
correct += 1
# Special case for saving 0 epsilon examples
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:
# Save some adv examples for visualization later
if len(adv_examples) < 5:
adv_ex = perturbed_data.squeeze().detach().cpu().numpy()
adv_examples.append( (init_pred.item(), final_pred.item(), adv_ex) )
# Calculate final accuracy for this epsilon
final_acc = correct/float(len(test_loader))
print(f"Epsilon: {epsilon}\tTest Accuracy = {correct} / {len(test_loader)} = {final_acc}")
# Return the accuracy and an adversarial example
return final_acc, adv_examples
运行攻击¶
实现过程的最后部分是实际运行攻击。在这里,我们对每个 epsilons 输入中的epsilon值运行完整的测试步骤。对于每个epsilon值,我们还保存最终准确率以及一些成功的对抗样本以便在后续部分绘制。注意打印出的准确率随着epsilon值的增加而下降。此外,请注意 \(\epsilon=0\) 的情况代表原始测试准确率,没有受到攻击。
accuracies = []
examples = []
# Run test for each epsilon
for eps in epsilons:
acc, ex = test(model, device, test_loader, eps)
accuracies.append(acc)
examples.append(ex)
结果¶
准确率与Epsilon¶
第一个结果是准确率对epsilon的曲线图。此前提到过,当epsilon增加时,我们预计测试准确率会下降。这是因为较大的epsilon意味着我们在最大化损失方向上采取了较大的步骤。注意曲线中的趋势并非线性,尽管epsilon值是线性间隔。例如,\(\epsilon=0.05\) 的准确率比 \(\epsilon=0\) 仅低约4%,但 \(\epsilon=0.2\) 的准确率比 \(\epsilon=0.15\) 低25%。此外,注意在10类别分类器中,模型的随机准确率出现在 \(\epsilon=0.25\) 和 \(\epsilon=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值。第一行是 \(\epsilon=0\) 的示例,代表未经扰动的“干净”图像。每张图的标题显示“原始分类 -> 对抗分类”。注意 \(\epsilon=0.15\) 时扰动开始变得明显,而 \(\epsilon=0.3\) 时非常明显。然而,在所有情况下,人类仍然能够在加入噪声后识别正确类别。
# Plot several examples of adversarial samples at each 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(f"Eps: {epsilons[i]}", fontsize=14)
orig,adv,ex = examples[i][j]
plt.title(f"{orig} -> {adv}")
plt.imshow(ex, cmap="gray")
plt.tight_layout()
plt.show()
接下来去哪儿?¶
希望本教程能够为对抗性机器学习领域提供一些洞见。从这里可以有很多潜在的方向。这种攻击代表了对抗性攻击研究的开端,之后人们针对如何攻击和防御机器学习模型提出了众多后续方案。事实上,在2017年NIPS大会上,有一个对抗攻击和防御的竞赛,竞赛所使用的许多方法都被描述在这篇论文中: Adversarial Attacks and Defences Competition 。关于防御的工作也引出了一个思路,即使机器学习模型可以更加泛化**鲁棒性**,无论是在自然扰动输入还是对抗生成输入的情况下。
另一个方向是研究不同领域的对抗攻击与防御。对抗性研究并不仅限于图像领域,看看 这个 对语音到文本模型的攻击。但或许了解对抗性机器学习最好的方式就是亲身实践。试着用2017年NIPS竞赛中的方法实现一种不同的攻击,并观察与FGSM的差异。然后,尝试防御你自己的攻击。
进一步的方向取决于可用资源,可以修改代码支持批量处理、并行处理或者分布式处理,而不是像上面的各个``epsilon test()``循环一次完成一个攻击。
脚本总运行时间: (0 分钟 0.000 秒)