Pytorch学习笔记(十七)Image and Video - Adversarial Example Generation
这篇博客瞄准的是 pytorch 官方教程中 Image and Video
章节的 Adversarial Example Generation
部分。
- 官网链接:https://pytorch.org/tutorials/beginner/fgsm_tutorial.html
完整网盘链接: https://pan.baidu.com/s/1L9PVZ-KRDGVER-AJnXOvlQ?pwd=aa2m 提取码: aa2m
Adversarial Example Generation
本教程将提高你对 ML 模型安全漏洞的认识,并深入了解对抗性机器学习。试验过程中你会发现,向图像添加人眼难以察觉的扰动会导致模型性能发生巨大变化。这里将通过图像分类器来对这一现象进行演示,使用最早也是最流行的攻击方法之一,快速梯度符号攻击 (FGSM),来欺骗 MNIST 分类器。
这个攻击方式中有一个关键部分 符号,如果你对模型很敏感的话会立即联想到CV模型中最常用的激活函数 ReLU,该函数将输入转为0/1的int量,所以该攻击的主要对象就是ReLU函数,让其在计算过程中将0反算成1,1反算成0 就可以达到攻击目的。
Threat Model
对抗性攻击有很多种,每种攻击都有不同的目标和对攻击者的假设。但通常来说,总目标是对输入数据添加最少的扰动以导致所需的错误分类。对攻击者的其中两种假设为 “白盒” 和 “黑盒”。
- 白盒攻击:假设攻击者完全了解并可以访问模型,包括架构、输入、输出和权重;
- 黑盒攻击:假设攻击者只能访问模型的输入和输出,对底层架构或权重一无所知;
还有两种类型的目标,包括 “错误分类”、“源/目标错误”分类:
- 错误分类:只希望输出分类是错误的,但不关心新的分类是什么;
- 源/目标错误分类:想要改变原本属于特定源类的图像,使其被归类为特定的目标类;
FGSM 攻击是一种白盒攻击,目的是让模型进行错误分类。
Fast Gradient Sign Attack
最早也是最流行的对抗性攻击之一被称为 快速梯度符号攻击 (FGSM),Goodfellow 等人在论文《Explaining and Harnessing Adversarial Examples》
中对此进行了描述。这种攻击非常强大而且很直观,旨在通过利用神经网络的学习方式(梯度)来攻击神经网络,其核心思想为:
- 传统的神经网络训练是通过梯度下降来最小化损失函数,即调整权重,让模型的输出更接近真实标签;
- FGSM 的思路是相反的:它不是调整权重,而是调整输入数据,让损失函数最大化,从而欺骗模型;
具体操作如下:
- 计算损失函数相对于输入数据的梯度(而不是相对于权重的梯度);
- 沿着梯度的正方向调整输入数据,使得损失增大,从而让模型更容易被误导;
x ′ = x + ϵ ⋅ s i g n ( ∇ x J ( θ , x , y ) ) x^{'}=x+\epsilon\cdot sign(\nabla_{x}J(\theta,x,y)) x′=x+ϵ⋅sign(∇xJ(θ,x,y))
其中 x x x 是原始输入; x ′ x^{'} x′ 是被扰动后的是输入; J ( θ , x , y ) J(\theta,x,y) J(θ,x,y) 是损失函数如CrossEntropyLoss; ∇ x J ( θ , x , y ) \nabla_{x}J(\theta,x,y) ∇xJ(θ,x,y) 是损失函数在输入方向的梯度; ϵ \epsilon ϵ 是扰动大小;
根据官网的描述,在上面式子中只要
ϵ
\epsilon
ϵ 为一个非常小的值如0.007都会导致模型将整个图像分类错误。
那么为什么神经网络对微小扰动敏感?这与神经网络的特性、数据的高维性、以及 ReLU(或其他激活函数)共同作用导致:
- 高维空间的超平面决策边界:
- 在高维空间中,数据样本通常距离决策边界很近,而 FGSM 的微小扰动可能正好沿着最容易翻转分类的方向;
- 由于 FGSM 是基于梯度的,它会精准地找到“最致命”的扰动方向,即最容易跨越决策边界的方向,即使是微小的 ϵ \epsilon ϵ 也足够让样本“跳过边界”;
- ReLU 激活函数导致的非线性放大:
- ReLU(或其他非线性激活函数)可能在某些维度上对输入的变化特别敏感;
- 如果某个神经元的输入略微变化,使其激活状态翻转(从 0 变成一个大数,或者反之),则整个网络的激活模式都会改变,导致最终分类完全不同;
- 权重矩阵的累积放大效应:
- 由于神经网络是多个层叠加的,前一层的小变化会在后一层被权重矩阵不断放大(或者在特定方向被削弱);
- 这种效应在 FGSM 里可能表现为,即使输入数据变化极小(比如 ϵ = 0.001 \epsilon=0.001 ϵ=0.001),但由于网络的层层累积,最终分类可能完全不同;
Implementation
导入必要的库
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
FGSM攻击输入部分
epsilons = [0.0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3]
pretrained_model = "data/lenet_mnist_model.pth"
torch.manual_seed(42)
Model Under Attack
这里使用 pytorch/examples/mnist
中的 MNIST 模型,此处的网络定义和测试数据加载器是从 MNIST 示例中复制而来的。本节的目的是定义模型和数据加载器,然后初始化模型并加载预训练权重,点击这个 链接 下载预训练模型权重,然后将其移动到 ./data
文件夹下。
准备模型
class Net(torch.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)
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
准备数据集加载器
FGSM Attack
根据上面提到的FGSM公式,将输入到模型中的图像进行修改
def fgsm_accack(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
def denorm(batch, mean=[0.1307], std=[0.3081]):
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)
Testing Function
定义一个测试函数对模型进行FGSM攻击
def test( model, device, test_loader, epsilon ):
correct = 0
adv_examples = []
for data, target in test_loader:
data, target = data.to(device), target.to(device)
data.requires_grad = True
output = model(data)
init_pred = output.max(1, keepdim=True)[1]
# 如果模型本身预测错误则直接跳过这个case
if init_pred.item() != target.item():
continue
loss = F.nll_loss(output, target)
model.zero_grad()
loss.backward()
data_grad = data.grad.data
data_denorm = denorm(data)
perturbed_data = fgsm_attack(data_denorm, epsilon, data_grad)
perturbed_data_normalized = transforms.Normalize((0.1307,), (0.3081,))(perturbed_data)
output = model(perturbed_data_normalized)
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))
print(f"Epsilon: {epsilon}\tTest Accuracy = {correct} / {len(test_loader)} = {final_acc}")
return final_acc, adv_examples
Run Attack
accuracies = []
examples = []
for eps in epsilons:
acc, ex = test(model, device, test_loader, eps)
accuracies.append(acc)
examples.append(ex)
Results
Accuracy vs Epsilon
首先查看acc与 epsilon 的关系。随着 epsilon 的增加预计测试acc会下降。这是因为更大的 epsilon 意味朝着最大化loss的方向迈出了更大的一步。但即使在图片中看到 epsilon 值是线性间隔的,实际上曲线中的趋势也不是线性的。
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()
Sample Adversarial Examples
随着 epsilon 的增加测试acc会降低,但扰动会变得更容易被察觉。攻击者必须考虑acc下降和可感知性之间的权衡。这里展示了每个 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()