如何攻击神经网络?人工智能的局限

作者:腾科IT教育|魏志伟
来源:华为认证


前言

自2012年起,人工智能快速发展,频繁出现在大众视野。从Alpha GO到ChatGPT,人工智能已成为不可阻挡的发展趋势。但是由于神经学习的黑盒性质,导致神经网络难以解释,且难以控制。即使像ChatGPT这种强大的模型,在联网的情况下也会出现一些低级错误。

神经网络出错让人很难琢磨,比如人脸检测有时会检测出和人脸毫无相关的人脸(对人而言)。ChatGPT也会回答一些毫无头绪的答案,比如GPT3.5当遇到问题“2022飞洒发生范德萨分”时,会出现短路情况。又或是李世石的“神之一手”,都是神经网络难以琢磨的表现。
今天的主题并非讨论为什么会出现这些情况,而是讨论如何创造这些情况,也就是攻击神经网络。看完今天的内容,相信大家对神经网络的智能会有新的认识。

本文会牵涉一些AI相关的实验,需要Python及Pytorch的环境。为了方便,这里使用华为ModelArts平台,详情可见:https://www.huaweicloud.com/product/modelarts.html。ModelArts平台可以直接创建Jupyter实例,我们无需关注环境的问题。


网络训练

现在不管是什么网络,几乎用的都是梯度下降算法。首先需要定义一个网络,这里用y=f(θ;x)表示,其中θ是网络的权重。θ可选的值有无穷种可能,但是只有少数θ可以得到比较好的结果。为了评估θ的好坏,可以定义一个损失函数loss=L(f(θ;x), target),其中target是真实值。现在只需要找一组让loss最小的θ就能完成训练。

但是f(θ;x)是一个非常复杂的函数,L(f(θ;x), target)则更为复杂,无法直接给出解析解,所以需要使用迭代算法求解θ。深度学习中用的就是梯度下降算法,梯度下降算法的表达式如下:


其中η是用来调节更新幅度的参数,叫学习率。当loss比较小时,网络可以正确预测结果。而攻击也是围绕梯度和loss来的。攻击网络就是生成一个对抗样本,让这个样本输入网络后得到一个较大的loss。或者让对抗样本与假真实值有较小的loss。


对抗攻击

攻击神经网络的方式有很多,基于不同的先验知识可以分为黑盒攻击和白盒攻击。基于不同的目的,可以分为源/目标误分类、针对性误分类、误分类、置信度降低。其中误分类攻击目的最简单,就是让模型分类错误,这也是本文要实现的一种攻击。

其中白盒攻击比较简单,在白盒攻击中,我们对模型了如指掌。我们知道网络的每一处细节,也可以拿到网络进行推理和梯度回传。在白盒攻击中,可以通过梯度信息来生成对抗样本。训练的过程中我们的目的是降低loss,而对抗的过程则是增加loss。当生成的对抗样本计算出较大loss时,网络会有较大概率分类错误,这样就达到了欺骗网络的目的。

而黑盒攻击要更为复杂,黑盒攻击假设我们不知道网络的详细信息,网络结构、网络权重,但是我们可以使用这个网络。我们知道网络输入什么,以及当前输入对应的输出。这种情况下,要攻击神经网络会比较复杂。

已经上线的网络通常都属于黑盒情况,在对抗样本提出后,大家并不认为在黑盒情况下能有正确攻击网络。而GAN的作者Goodfellow则发现情况并非如此。黑盒攻击可以用集成学习的方式来实现,在本文不会详细介绍。本文主要针对白盒攻击进行讨论。


Fast Gradient Sign Attack

实现攻击的方式也是多种多样的,本文使用一种名为Fast Gradient Sign Attack(FGSA)的攻击方式,这种方式利用梯度信息对输入进修改,来达到攻击的目的。

在前面已经提到了,模型的训练是使用梯度下降算法实现的。这里需要注意两个点,一个是更新方向,一个是更新参数。在训练过程中,我们的目的是minimize L(f(θ;x), target),并且是找一组最优的θ。由此可以知道我们要更新的参数是θ,并且更新方向是梯度的反方向。

攻击模型的目的则不同,首先讨论误分类的情况。在误分类的情况中,我们的目的是生成对抗样本,使模型分类错误,此时我们的目的是让L(f(θ;x), target)比较大。这里我们要找的是对抗样本,因此更新的参数是x,并且方向是梯度方向。

那么生成对抗样本的操作可以用下面公式表示:


在FGSA中,不考虑梯度大小的问题,只关注梯度方向。因此FGSA中应该用下面公式表示:


其中sign是符号函数,会返回梯度的正负号。


代码实现

接下来我们用代码来实现FGSA攻击,这里使用白盒攻击。所以需要先实现一个网络,这里以手写数字为例。首先进入控制台,创建notebook实例:

设置镜像、CPU/GPU等信息,因为案例比较轻量,所以选取Pytorch+CPU即可。创建完成就可以打开notebook,具体使用与Jupyter Notebook一样。

手写数字识别

白盒攻击的特点是我们知道网络的全部细节,因此我们自己实现一个网络,这个网络的所有细节我们都可以知道。网络可以自由设计,此处我们选择用一个两层的卷积神经网络,训练代码如下:

Python
 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文件,这个就是模型文件。后续生成对抗样本需要使用到这个文件。


FGSA

得到模型后,我们就可以开始生成对抗样本了。这里使用FGSA方法,在前面我们推导出FGSA的表达式为:


现在只需要用代码把这个函数实现即可,这个函数有两个输入,分别是输入x和x的梯度。该函数的操作可以分为下面几步:

  •   获取梯度方向

  •   代入上述公式得到对抗样本

代码如下:

Python
 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自动求导,只需要下面一句即可:

Python
 x.requires_grad  = True

而后要做的就是计算loss,反向传播即可。调用loss.backward()方法后,张量中就存储了梯度信息,而x的梯度可以通过下面方式获取:

Python
 x_grad  = x.grad.data

这样fgsa_attack需要的值我们都有了,接下来就可以生成对抗样本了。攻击网络的完整代码如下:

Python
 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张图像,下面是带有攻击性的输入图像:


对人来说,这幅图像依旧是原来的数字,但是对神经网络来说并非如此了,下面的矩阵是各个图像对应的预测结果:

Python
 [[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]]

分类成制定类别

在前面的程序中,我们只要求生成数据,让网络错误分类。在一些场景下,我们需要生成数据,让网络分类成指定类别,比如想欺骗人脸识别,就需要生成可以让网络识别为某人的数据。这个应该如何实现呢?其实非常简单,错误分类的操作就是改变输入,让输入网梯度方向更新,此时loss会增加,从而达到错误分类的效果。

错误分类成某个类别则不太一样,比如现在想生成数据,让模型错误分类成数字1,我们要做的是让loss_fn(output, 1)变小,因此需要修改两个地方:
  •  目标值改为1(具体类别)
  •  数据往梯度反方向更新

下面把fgsa_attack函数修改为如下:

Python
 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

把攻击的代码修改为:

Python
 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值,得到的攻击图像如下:


模型对图像的预测结果为:

Python
 [[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的数量,这表明此次攻击是成功的。


总结

神经网络虽然非常强大,但是对神经网络的理解仍是一个待解决的问题。由于神经网络非常庞大,我们难以把握每一个细节,很难确定网络如何推理出结果,正因为此,一个看似训练良好的模型在应用的实际任务时会出现很多离奇现象。只有理解这些离奇现象为何会发生,才能更好地理解模型,并改进模型。

因为现在大多数网络都是使用梯度下降来更新模型,因此梯度是攻击网络的一个很好的突破点。在上面对网络进行了两种攻击,看似都非常有效。但是白盒攻击的前提是我们能够知道网络具体结构,对网络有完全的控制能力,但是在实际情况中这并不常见,因此也不用过于担心自己的网络会被攻击。


本文转自:华为认证,转载此文目的在于传递更多信息,版权归原作者所有。如不支持转载,请联系小编demi@eetrend.com删除。

最新文章