作者:腾科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实例:
手写数字识别
白盒攻击的特点是我们知道网络的全部细节,因此我们自己实现一个网络,这个网络的所有细节我们都可以知道。网络可以自由设计,此处我们选择用一个两层的卷积神经网络,训练代码如下:
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删除。