一、前言
自2012年起,人工智能快速发展,频繁出现在大众视野。从Alpha GO到ChatGPT,人工智能已成为不可阻挡的发展趋势。但是由于神经学习的黑盒性质,导致神经网络难以解释,且难以控制。即使像ChatGPT这种强大的模型,在联网的情况下也会出现一些低级错误。
神经网络出错让人很难琢磨,比如人脸检测有时会检测出和人脸毫无相关的人脸(对人而言)。ChatGPT也会回答一些毫无头绪的答案,比如GPT3.5当遇到问题“2022飞洒发生范德萨分”时,会出现短路情况。又或是李世石的“神之一手”,都是神经网络难以琢磨的表现。
今天的主题并非讨论为什么会出现这些情况,而是讨论如何创造这些情况,也就是攻击神经网络。看完今天的内容,相信大家对神经网络的智能会有新的认识。
二、网络训练
现在不管是什么网络,几乎用的都是梯度下降算法。首先需要定义一个网络,这里用y=f(θ;x)表示,其中θ是网络的权重。θ可选的值有无穷种可能,但是只有少数θ可以得到比较好的结果。为了评估θ的好坏,可以定义一个损失函数loss=L(f(θ;x), target),其中target是真实值。现在只需要找一组让loss最小的θ就能完成训练。
但是f(θ;x)是一个非常复杂的函数,L(f(θ;x), target)则更为复杂,无法直接给出解析解,所以需要使用迭代算法求解θ。深度学习中用的就是梯度下降算法,梯度下降算法的表达式如下:
θ = θ − η ∂ L ∂ θ \theta = \theta - \eta \frac{\partial L}{\partial \theta} θ = θ − η ∂θ∂L
其中η是用来调节更新幅度的参数,叫学习率。当loss比较小时,网络可以正确预测结果。而攻击也是围绕梯度和loss来的。攻击网络就是生成一个对抗样本,让这个样本输入网络后得到一个较大的loss。或者让对抗样本与假真实值有较小的loss。
三、对抗攻击(Adversarial Attack)
攻击神经网络的方式有很多,基于不同的先验知识可以分为黑盒攻击
和白盒攻击
。基于不同的目的,可以分为源/目标误分类
、针对性误分类
、误分类
、置信度降低
。其中误分类
攻击目的最简单,就是让模型分类错误,这也是本文要实现的一种攻击。
其中白盒攻击比较简单,在白盒攻击中,我们对模型了如指掌。我们知道网络的每一处细节,也可以拿到网络进行推理和梯度回传。在白盒攻击中,可以通过梯度信息来生成对抗样本。训练的过程中我们的目的是降低loss,而对抗的过程则是增加loss。当生成的对抗样本计算出较大loss时,网络会有较大概率分类错误,这样就达到了欺骗网络的目的。
而黑盒攻击要更为复杂,黑盒攻击假设我们不知道网络的详细信息,网络结构、网络权重,但是我们可以使用这个网络。我们知道网络输入什么,以及当前输入对应的输出。这种情况下,要攻击神经网络会比较复杂。
已经上线的网络通常都属于黑盒情况,在对抗样本提出后,大家并不认为在黑盒情况下能有正确攻击网络。而GAN的作者Goodfellow则发现情况并非如此。黑盒攻击可以用集成学习的方式来实现,在本文不会详细介绍。本文主要针对白盒攻击进行讨论。
四、Fast Gradient Sign Attack
实现攻击的方式也是多种多样的,本文使用一种名为Fast Gradient Sign Attack(FGSA)
的攻击方式,这种方式利用梯度信息对输入进修改,来达到攻击的目的。
在前面已经提到了,模型的训练是使用梯度下降算法实现的。这里需要注意两个点,一个是更新方向,一个是更新参数。在训练过程中,我们的目的是minimize L(f(θ;x), target),并且是找一组最优的θ。由此可以知道我们要更新的参数是θ,并且更新方向是梯度的反方向。
攻击模型的目的则不同,首先讨论误分类的情况。在误分类的情况中,我们的目的是生成对抗样本,使模型分类错误,此时我们的目的是让L(f(θ;x), target)比较大。这里我们要找的是对抗样本,因此更新的参数是x,并且方向是梯度方向。那么生成对抗样本的操作可以用下面公式表示:
a d v e r s a r i a l X = x + ϵ ∂ L ∂ x adversarialX = x + \epsilon \frac{\partial L}{\partial x} adversarialX = x + ϵ ∂ x∂L
在FGSA中,不考虑梯度大小的问题,只关注梯度方向。因此FGSA中应该用下面公式表示:
a d v e r s a r i a l X = x + ϵ s i g n ( ∂ L ∂ x ) adversarialX = x + \epsilon sign(\frac{\partial L}{\partial x}) adversarialX = x + ϵ sign(∂ x∂L)
其中sign是符号函数,会返回梯度的正负号。
四、代码实现
接下来我们用代码来实现FGSA攻击,这里使用白盒攻击。所以需要先实现一个网络,这里以手写数字为例。
4.1 手写数字识别
白盒攻击的特点是我们知道网络的全部细节,因此我们自己实现一个网络,这个网络的所有细节我们都可以知道。网络可以自由设计,此处我们选择用一个两层的卷积神经网络,训练代码如下:
import torch
from torch import nn
from torch import optim
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor
from collections import OrderedDict
device = "cuda" if torch.cuda.is_available() else "cpu"
# 超参数
epochs = 10
batch_size = 64
lr = 0.001
# 1、加载数据
train_dataset = datasets.MNIST('./', True, ToTensor(), download=True)
train_loader = DataLoader(train_dataset, batch_size)
# 2、构建模型
class DigitalNet(nn.Module):
def __init__(self):
super(DigitalNet, self).__init__()
self.model = nn.Sequential(OrderedDict({
"conv1": nn.Conv2d(1, 6, 5),
"relu1": nn.ReLU(),
"pool1": nn.MaxPool2d(2),
"conv2": nn.Conv2d(6, 16, 5),
"relu2": nn.ReLU(),
"pool2": nn.MaxPool2d(2),
"flatten": nn.Flatten(),
"fc1": nn.Linear(4 * 4 * 16, 128),
"relu3": nn.ReLU(),
"fc2": nn.Linear(128, 10),
}))
def forward(self, inputs):
return self.model(inputs)
# 3、定义loss
loss_fn = nn.CrossEntropyLoss()
# 4、定义优化器
model = DigitalNet().to(device)
optimizer = optim.Adam(model.parameters(), lr)
# 5、训练
for epoch in range(epochs):
for image, target in train_loader:
image, target = image.to(device), target.to(device)
# 正向传播
output = model(image)
loss = loss_fn(output, target)
model.zero_grad()
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
print(f'epoch: {epoch+1}, loss: {loss.item()}')
torch.save(model.state_dict(), 'digital.pth')
这里为了方便,省略了测试相关代码,准确率的计算也省去了。代码运行完成后,可以得到一个digital.pth
文件,这个就是模型文件。后续生成对抗样本需要使用到这个文件。
4.2 FGSA
得到模型后,我们就可以开始生成对抗样本了。这里使用FGSA方法,在前面我们推导出FGSA的表达式为:
a d v e r s a r i a l X = x + ϵ s i g n ( ∂ L ∂ x ) adversarialX = x + \epsilon sign(\frac{\partial L}{\partial x}) adversarialX = x + ϵ sign(∂ x∂L)
现在只需要用代码把这个函数实现即可,这个函数有两个输入,分别是输入x和x的梯度。该函数的操作可以分为下面几步:
- 获取梯度方向
- 代入上述公式得到对抗样本
代码如下:
def fgsa_attack(x, epsilon, x_grad):
# 获取x梯度方向
sign_grad = x_grad.sign()
# 更新x,让x往梯度方向更新
adversarial_x = x + epsilon * sign_grad
# 把结果映射到0-1之间
adversarial_x = torch.clamp(adversarial_x, 0, 1)
return adversarial_x
其中x是我们已有的数据,epsilon是超参数,需要我们自己设置,x_grad是x的梯度信息,这个还没有获取。接下来要做的就是拿到x_grad,即求损失函数对x的导数。
默认情况下x是不会求导的,因此需要设置x自动求导,只需要下面一句即可:
x.requires_grad = True
而后要做的就是计算loss,反向传播即可。调用loss.backward()方法后,张量中就存储了梯度信息,而x的梯度可以通过下面方式获取:
x_grad = x.grad.data
这样fgsa_attack需要的值我们都有了,接下来就可以生成对抗样本了。攻击网络的完整代码如下:
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, utils
from torchvision.transforms import ToTensor
from collections import OrderedDict
import matplotlib.pyplot as plt
device = "cuda" if torch.cuda.is_available() else "cpu"
# 超参数
epochs = 10
batch_size = 64
lr = 0.001
# 1、加载数据
train_dataset = datasets.MNIST('./', True, ToTensor(), download=True)
train_loader = DataLoader(train_dataset, batch_size)
loss_fn = nn.CrossEntropyLoss()
# 加载模型
model = DigitalNet()
model.load_state_dict(torch.load('digital.pth'))
for image, target in train_loader:
# 设置输入自动求导
image.requires_grad = True
output = model(image)
loss = loss_fn(output, target)
model.zero_grad()
loss.backward()
# loss对image的梯度
image_grad = image.grad.data
# 对image进行修改
adversarial_x = fgsa_attack(image, .15, image_grad)
# 对攻击数据预测
output = model(adversarial_x)
grid = utils.make_grid(adversarial_x, normalize=True)
with torch.no_grad():
grid = grid.cpu().numpy().transpose((1, 2, 0))
print(output.argmax(dim=1).cpu().numpy().reshape((8, 8)))
plt.imshow(grid)
plt.show()
break
这里测试了64张图像,下面是带有攻击性的输入图像:
对人来说,这副图像依旧是原来的数字,但是对神经网络来说并非如此了,下面的矩阵是各个图像对应的预测结果:
[[2 9 3 8 8 9 8 8]
[3 3 0 8 8 8 8 8]
[8 7 3 2 9 5 8 8]
[3 3 8 3 7 2 7 7]
[9 7 0 2 3 0 2 9]
[8 3 5 8 8 8 8 8]
[5 0 5 0 5 3 8 7]
[5 8 9 8 2 7 3 5]]
4.3 分类成指定类别
在前面的程序中,我们只要求生成数据,让网络错误分类。在一些场景下,我们需要生成数据,让网络分类成指定类别,比如想欺骗人脸识别,就需要生成可以让网络识别为某人的数据。这个应该如何实现呢?其实非常简单,错误分类的操作就是改变输入,让输入网梯度方向更新,此时loss会增加,从而达到错误分类的效果。
错误分类成某个类别则不太一样,比如现在想生成数据,让模型错误分类成数字1,我们要做的是让loss_fn(output, 1)
变小,因此需要修改两个地方:
- 目标值改为1(具体类别)
- 数据往梯度反方向更新
下面把fgsa_attack函数修改为如下:
def fgsm_attack(x, epsilon, x_grad):
# 获取梯度的反方向
sign_grad = -x_grad.sign()
# 让输入添加梯度信息,即让输入添加能让loss减小的信息
adversarial_x = x + epsilon * sign_grad
# 把结果映射到0-1之间
adversarial_x = torch.clamp(adversarial_x, 0, 1)
return adversarial_x
把攻击的代码修改为:
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, utils
from torchvision.transforms import ToTensor
from collections import OrderedDict
import matplotlib.pyplot as plt
device = "cuda" if torch.cuda.is_available() else "cpu"
# 超参数
epochs = 10
batch_size = 64
lr = 0.001
# 1、加载数据
train_dataset = datasets.MNIST('./', True, ToTensor(), download=True)
train_loader = DataLoader(train_dataset, batch_size)
loss_fn = nn.CrossEntropyLoss()
# 加载模型
model = DigitalNet()
model.load_state_dict(torch.load('digital.pth'))
for image, target in train_loader:
# 设置输入自动求导
image.requires_grad = True
output = model(image)
# 把目标值修改为1
target[::] = 1
loss = loss_fn(output, target)
model.zero_grad()
loss.backward()
# loss对image的梯度
image_grad = image.grad.data
# 对image进行修改
adversarial_x = fgsa_attack(image, .2, image_grad)
# 对攻击数据预测
output = model(adversarial_x)
grid = utils.make_grid(adversarial_x, normalize=True)
with torch.no_grad():
grid = grid.cpu().numpy().transpose((1, 2, 0))
print(output.argmax(dim=1).cpu().numpy().reshape((8, 8)))
plt.imshow(grid)
plt.show()
break
这里做的就是把目标值改为了1,并且调整了fgsa_attack的epsilon值,得到的攻击图像如下:
模型对图像的预测结果为:
[[3 0 1 1 4 8 1 1]
[1 1 1 1 3 6 1 9]
[0 1 1 1 1 1 1 1]
[1 1 8 1 0 1 1 1]
[5 1 1 1 1 0 1 1]
[1 1 1 1 3 4 8 1]
[1 1 1 9 0 8 4 1]
[0 4 1 1 9 1 5 9]]
虽然结果并非全为1,但是预测结果为1的数量远多于真实为1的数量,这表明此次攻击是成功的。
五、总结
神经网络虽然非常强大,但是对神经网络的理解仍是一个待解决的问题。由于神经网络非常庞大,我们难以把握每一个细节,很难确定网络如何推理出结果,正因为此,一个看似训练良好的模型在应用的实际任务时会出现很多离奇现象。只有理解这些离奇现象为何会发生,才能更好地理解模型,并改进模型。
因为现在大多数网络都是使用梯度下降来更新模型,因此梯度是攻击网络的一个很好的突破点。在上面对网络进行了两种攻击,看似都非常有效。但是白盒攻击的前提是我们能够知道网络具体结构,对网络有完全的控制能力,但是在实际情况中这并不常见,因此也不用过于担心自己的网络会被攻击。