目录

对抗样本生成

创建日期: 2018年8月14日 | 最后更新日期: 2024年8月27日 | 最后验证: 未验证

作者: Nathan Inkawhich

如果你正在阅读这段文字,希望你能体会到一些机器学习模型的有效性。研究工作不断推动机器学习模型变得更快、更准确、更高效。然而,在设计和训练模型时,一个经常被忽视的因素是安全性和鲁棒性,尤其是在面对试图欺骗模型的对手时。

本教程将提高您对ML模型安全漏洞的认识,并深入探讨对抗机器学习这一热门话题。您可能会惊讶地发现,向图像添加不可感知的扰动 可以 导致模型性能发生巨大变化。鉴于这是一篇教程,我们将通过一个图像分类器的例子来探索该主题。具体来说,我们将使用最早且最流行的攻击方法之一,快速梯度符号攻击(FGSM),来欺骗一个MNIST分类器。

威胁模型

为了背景信息,有许多类别对抗攻击,每种攻击都有不同的目标和对攻击者知识的不同假设。然而,总体目标通常是通过添加最少的扰动来使输入数据产生所需的误分类。攻击者的知识有几种假设,其中两种是:白盒黑盒。在白盒攻击中,攻击者假设有完整的模型知识和访问权限,包括架构、输入、输出和权重。而在黑盒攻击中,攻击者仅能访问模型的输入和输出,并不了解底层架构或权重。此外,还有几种目标类型,包括:误分类源/目标误分类。一个误分类的目标意味着对手只想让输出分类错误,但并不关心新的分类是什么。而源/目标误分类的目标则是对手希望将原本属于特定源类别的图像修改为特定目标类别。

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

快速梯度符号攻击

迄今为止,最早和最流行的对抗攻击之一被称为快速梯度符号攻击(FGSM),由Goodfellow等人在解释和利用对抗样本中描述。这种攻击非常强大,但也很直观。它的设计目的是通过利用神经网络的学习方式——即梯度来攻击神经网络。其思路很简单,不是通过调整权重来最小化损失,而是基于反向传播的梯度,调整输入数据以最大化损失。换句话说,该攻击使用相对于输入数据的损失梯度,然后调整输入数据以最大化损失。

在我们跳入代码之前,让我们看一下著名的 FGSM 熊猫示例并提取一些符号。

fgsm_panda_image

从图中可以看出,\(\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

实现

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

Inputs

这个教程只有三个输入,并且定义如下:

  • epsilons - 列出用于运行的epsilon值。重要的是要在列表中保留0,因为它代表了模型在原始测试集上的性能。此外,直观上我们期望epsilon值越大,扰动越明显,但攻击对模型准确性的削弱效果也越显著。由于数据范围在这里是\([0,1]\),因此没有任何epsilon值应该超过1。

  • pretrained_model - 预训练的MNIST模型路径,该模型使用 pytorch/examples/mnist 训练。 为了简化操作,请在这里下载预训练模型 这里.

  • use_cuda - 使用CUDA的布尔标志,如果需要且可用的话。 请注意,对于本教程而言,一个带有CUDA的GPU并不是必需的,因为CPU不会花费太多时间。

epsilons = [0, .05, .1, .15, .2, .25, .3]
pretrained_model = "data/lenet_mnist_model.pth"
use_cuda=True
# Set random seed for reproducibility
torch.manual_seed(42)
<torch._C.Generator object at 0x7fce85f71b30>

模型受到攻击

正如提到的,受到攻击的模型是来自 pytorch/examples/mnist 的同一MNIST模型。 您可以训练并保存自己的MNIST模型,或者下载并使用提供的模型。这里的 Net 定义和测试数据加载器均是从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)

# Define what device we are using
print("CUDA Available: ",torch.cuda.is_available())
device = torch.device("cuda" if use_cuda and torch.cuda.is_available() else "cpu")

# 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()
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
Failed to download (trying next):
HTTP Error 403: Forbidden

Downloading https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz
Downloading https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz to ../data/MNIST/raw/train-images-idx3-ubyte.gz

  0%|          | 0.00/9.91M [00:00<?, ?B/s]
100%|##########| 9.91M/9.91M [00:00<00:00, 130MB/s]
Extracting ../data/MNIST/raw/train-images-idx3-ubyte.gz to ../data/MNIST/raw

Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz
Failed to download (trying next):
HTTP Error 403: Forbidden

Downloading https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz
Downloading https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz to ../data/MNIST/raw/train-labels-idx1-ubyte.gz

  0%|          | 0.00/28.9k [00:00<?, ?B/s]
100%|##########| 28.9k/28.9k [00:00<00:00, 42.7MB/s]
Extracting ../data/MNIST/raw/train-labels-idx1-ubyte.gz to ../data/MNIST/raw

Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz
Failed to download (trying next):
HTTP Error 403: Forbidden

Downloading https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz
Downloading https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz to ../data/MNIST/raw/t10k-images-idx3-ubyte.gz

  0%|          | 0.00/1.65M [00:00<?, ?B/s]
100%|##########| 1.65M/1.65M [00:00<00:00, 75.8MB/s]
Extracting ../data/MNIST/raw/t10k-images-idx3-ubyte.gz to ../data/MNIST/raw

Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz
Failed to download (trying next):
HTTP Error 403: Forbidden

Downloading https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz
Downloading https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz to ../data/MNIST/raw/t10k-labels-idx1-ubyte.gz

  0%|          | 0.00/4.54k [00:00<?, ?B/s]
100%|##########| 4.54k/4.54k [00:00<00:00, 24.2MB/s]
Extracting ../data/MNIST/raw/t10k-labels-idx1-ubyte.gz to ../data/MNIST/raw

CUDA Available:  True

Net(
  (conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1))
  (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1))
  (dropout1): Dropout(p=0.25, inplace=False)
  (dropout2): Dropout(p=0.5, inplace=False)
  (fc1): Linear(in_features=9216, out_features=128, bias=True)
  (fc2): Linear(in_features=128, out_features=10, bias=True)
)

FGSM攻击

现在,我们可以定义通过扰动原始输入来生成对抗样本的函数。该fgsm_attack函数接受三个输入参数,图像是原始干净的图像(\(x\)),epsilon是像素级别的扰动量(\(\epsilon\)),而data_grad是损失相对于输入图像的梯度(\(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)\))。该函数然后创建扰动图像为

\[perturbed\_image = image + epsilon*sign(data\_grad) = x + \epsilon * sign(\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: 0      Test Accuracy = 9912 / 10000 = 0.9912
Epsilon: 0.05   Test Accuracy = 9605 / 10000 = 0.9605
Epsilon: 0.1    Test Accuracy = 8743 / 10000 = 0.8743
Epsilon: 0.15   Test Accuracy = 7111 / 10000 = 0.7111
Epsilon: 0.2    Test Accuracy = 4877 / 10000 = 0.4877
Epsilon: 0.25   Test Accuracy = 2717 / 10000 = 0.2717
Epsilon: 0.3    Test Accuracy = 1418 / 10000 = 0.1418

结果

准确率 vs epsilon

第一个结果显示的是准确率与epsilon的关系图。正如之前提到的,随着epsilon的增加,我们期望测试准确率降低。这是因为较大的epsilon意味着我们在最大化损失的方向上采取了更大的步长。注意曲线的趋势并非线性的,即使epsilon值是线性分布的。例如,准确率在\(\epsilon=0.05\)时只比\(\epsilon=0\)低约4%,但在\(\epsilon=0.2\)时却比\(\epsilon=0.15\)低25%。此外,注意模型的准确率在\(\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()
Accuracy vs Epsilon

样本对抗示例

Remember the idea of no free lunch? In this case, as epsilon increases the test accuracy decreases BUT the perturbations become more easily perceptible. In reality, there is a tradeoff between accuracy degradation and perceptibility that an attacker must consider. Here, we show some examples of successful adversarial examples at each epsilon value. Each row of the plot shows a different epsilon value. The first row is the \(\epsilon=0\) examples which represent the original “干净”图像,没有任何扰动。每张图片的标题显示“原始分类 -> 对抗分类”。注意,扰动在\(\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()
7 -> 7, 9 -> 9, 0 -> 0, 3 -> 3, 5 -> 5, 2 -> 8, 1 -> 3, 3 -> 5, 4 -> 6, 4 -> 9, 9 -> 4, 5 -> 6, 9 -> 5, 9 -> 5, 3 -> 2, 3 -> 5, 5 -> 3, 1 -> 6, 4 -> 9, 7 -> 9, 7 -> 2, 8 -> 2, 4 -> 8, 3 -> 7, 5 -> 3, 8 -> 3, 0 -> 8, 6 -> 5, 2 -> 3, 1 -> 8, 1 -> 9, 1 -> 8, 5 -> 8, 7 -> 8, 0 -> 2

下一步去哪里?

希望本教程能对对抗性机器学习的主题有所启发。从这里出发,有许多潜在的方向可以探索。 这种攻击代表了对抗性攻击研究的开端,此后出现了许多关于如何攻击和防御来自对手的机器学习模型的想法。事实上,在2017年的NIPS会议上,有一场对抗性攻击与防御竞赛,竞赛中使用的许多方法都在这篇论文中有描述:对抗性攻击与防御竞赛。防御方面的工作也引出了使机器学习模型在面对自然扰动和对抗性设计输入时更加稳健的想法。

另一个方向是不同领域的对抗攻击和防御。对抗研究不仅限于图像领域,看看这个对语音转文本模型的攻击。但也许了解更多关于对抗机器学习的最佳方式是亲自动手实践。尝试实现NIPS 2017竞赛中的不同攻击,并观察它与FGSM有何不同。然后,尝试防御你自己发起的攻击。

根据可用资源,进一步的方向是修改代码以支持批量、并行或分布式处理工作,而不是在上述每个epsilon test()循环中一次处理一个攻击。

脚本总运行时间: (4分钟1.060秒)

通过 Sphinx-Gallery 生成的画廊

文档

访问 PyTorch 的全面开发人员文档

查看文档

教程

获取面向初学者和高级开发人员的深入教程

查看教程

资源

查找开发资源并解答您的问题

查看资源