如何攻击神经网络?人工智能VS人工智障

一、前言

自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)则更为复杂,无法直接给出解析解,所以需要使用迭代算法求解θ。深度学习中用的就是梯度下降算法,梯度下降算法的表达式如下:

<math xmlns="http://www.w3.org/1998/Math/MathML"> θ = θ − η ∂ L ∂ θ \theta = \theta - \eta \frac{\partial L}{\partial \theta} </math>θ = θ − η ∂θ∂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,并且方向是梯度方向。那么生成对抗样本的操作可以用下面公式表示:

<math xmlns="http://www.w3.org/1998/Math/MathML"> a d v e r s a r i a l X = x + ϵ ∂ L ∂ x adversarialX = x + \epsilon \frac{\partial L}{\partial x} </math>adversarialX = x + ϵ ∂ x∂L

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

<math xmlns="http://www.w3.org/1998/Math/MathML"> 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}) </math>adversarialX = x + ϵ sign(∂ x∂L)

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

四、代码实现

接下来我们用代码来实现FGSA攻击,这里使用白盒攻击。所以需要先实现一个网络,这里以手写数字为例。

4.1 手写数字识别

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

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

4.2 FGSA

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

<math xmlns="http://www.w3.org/1998/Math/MathML"> 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}) </math>adversarialX = x + ϵ sign(∂ x∂L)

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

  1. 获取梯度方向
  2. 代入上述公式得到对抗样本

代码如下:

ini 复制代码
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自动求导,只需要下面一句即可:

ini 复制代码
x.requires_grad = True

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

ini 复制代码
x_grad = x.grad.data

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

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

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

css 复制代码
[[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. 目标值改为1(具体类别)
  2. 数据往梯度反方向更新

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

ini 复制代码
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

把攻击的代码修改为:

ini 复制代码
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值,得到的攻击图像如下:

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

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

五、总结

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

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

相关推荐
_OLi_6 分钟前
力扣 LeetCode 459. 重复的子字符串(Day4:字符串)
算法·leetcode·职场和发展·kmp
Romanticroom14 分钟前
计算机23级数据结构上机实验(第3-4周)
数据结构·算法
白藏y14 分钟前
数据结构——归并排序
数据结构·算法·排序算法
ahadee26 分钟前
蓝桥杯每日真题 - 第12天
c++·vscode·算法·蓝桥杯
zhentiya39 分钟前
微积分第五版课后习题答案详解PDF电子版 赵树嫄
算法·pdf
luky!1 小时前
算法--解决熄灯问题
python·算法
鸽鸽程序猿1 小时前
【算法】【优选算法】二分查找算法(下)
java·算法·二分查找算法
_OLi_1 小时前
力扣 LeetCode 150. 逆波兰表达式求值(Day5:栈与队列)
算法·leetcode·职场和发展
远望清一色1 小时前
基于MATLAB身份证号码识别
开发语言·图像处理·算法·matlab
醉颜凉3 小时前
【NOIP提高组】潜伏者
java·c语言·开发语言·c++·算法