FGSM网络对抗攻击

在线运行下载Notebook下载样例代码查看源文件

近年来随着数据、计算能力、理论的不断发展演进,深度学习在图像、文本、语音、自动驾驶等众多领域都得到了广泛应用。与此同时,人们也越来越关注各类模型在使用过程中的安全问题,因为AI模型很容易受到外界有意无意的攻击而产生错误的结果。

在本案例中,我们将以梯度符号攻击(Fast Gradient Sign Method,FGSM)为例,演示此类攻击是如何误导模型的。

对抗样本定义

Szegedy在2013年最早提出对抗样本的概念:

在原始样本处加入人类无法察觉的微小扰动,使得深度模型性能下降,这种样本即对抗样本。如下图所示,本来预测为“panda”的图像在添加噪声之后,模型就将其预测为“gibbon”,右边的样本就是一个对抗样本:

fgsm-panda-image

攻击方法

对模型的攻击方法在宏观层面可以从如下两个方面进行分类:

  1. 攻击者掌握的信息多少:

    • 白盒攻击:攻击者具有对模型的全部知识和访问权限,包括模型结构、权重、输入、输出,在产生对抗性攻击数据的过程中能够与模型系统有所交互。由于模型的信息完全被攻击者掌握,攻击者可以针对被攻击模型的特性设计特定的攻击算法。

    • 黑盒攻击:与白盒攻击相反,攻击者仅具有关于模型的有限知识。攻击者对模型的结构权重一无所知,仅了解部分输入输出。

  2. 攻击者的目的:

    • 有目标的攻击:攻击者将模型结果误导为特定分类。

    • 无目标的攻击:攻击者只想产生错误结果,而不在乎新结果是什么。

本案例中用到的梯度符号攻击FGSM是一种白盒攻击方法,既可以是有目标也可以是无目标攻击。

更多的模型安全功能可参考MindSpore Armour,现已支持FGSM、LLC、Substitute Attack等多种对抗样本生成方法,并提供对抗样本鲁棒性模块、Fuzz Testing模块、隐私保护与评估模块,帮助用户增强模型安全性。

快速梯度符号攻击(FGSM)

分类网络的训练会定义一个损失函数,用于衡量模型输出值与样本真实标签的距离,通过反向传播计算模型梯度,梯度下降更新网络参数,减小损失值,提升模型精度。

FGSM(Fast Gradient Sign Method)是一种简单高效的对抗样本生成方法。不同于正常分类网络的训练过程,FGSM通过计算loss对于输入的梯度为 \(\nabla_x J(\theta ,x ,y)\),梯度表征了loss对于输入变化的敏感性。

然后在原始输入加上上述梯度,使得loss增大,导致模型对于改造后的输入样本分类效果变差,从而达到攻击效果。

对抗样本的另一要求是生成样本与原始样本的差异要尽可能的小,使用sign函数可以使得修改图片时尽可能的均匀。

产生的对抗扰动用公式可以表示为:

\[\eta = \varepsilon sign(\nabla_x J(\theta)) \tag{1}\]

对抗样本可公式化为:

\[x' = x + \varepsilon sign(\nabla_x J(\theta ,x ,y)) \tag{2}\]

其中,

  • \(x\):正确分类为“Pandas”的原始输入图像。

  • \(y\):是\(x\)的输出。

  • \(\theta\):模型参数。

  • \(\varepsilon\):攻击系数。

  • \(J(\theta, x, y)\):训练网络的损失。

  • \(\nabla_x J(\theta)\):反向传播梯度。

数据处理

本案例将使用MNIST训练一个精度达标的LeNet网络,然后运行上文中所提到的FGSM攻击方法,达到欺骗网络模型,让模型实现错误分类的效果。

以下示例代码将数据集下载并解压到指定位置。

[1]:
import mindspore.dataset.vision as transforms
from mindspore.dataset.vision import Inter
from mindspore.dataset import MnistDataset
from download import download

url = "https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/" \
      "notebook/datasets/MNIST_Data.zip"
path = download(url, "./", kind="zip", replace=True)

dataset_train = MnistDataset(dataset_dir="./MNIST_Data/train", usage="train", shuffle=True)
dataset_eval = MnistDataset(dataset_dir="./MNIST_Data/test", usage="test", shuffle=True)

trans_transform = [
    transforms.Resize(size=32, interpolation=Inter.LINEAR),
    transforms.Rescale(1.0 / 255.0, 0.0),
    transforms.Rescale(1 / 0.3081, -1 * 0.1307 / 0.3081),
    transforms.HWC2CHW(),
]

dataset_train = dataset_train.map(operations=trans_transform, input_columns=["image"])
dataset_train = dataset_train.map(operations=lambda x: x.astype("int32"), input_columns=["label"])
dataset_train = dataset_train.batch(batch_size=32, drop_remainder=True)

dataset_eval = dataset_eval.map(operations=trans_transform, input_columns=["image"])
dataset_eval = dataset_eval.map(operations=lambda x: x.astype("int32"), input_columns=["label"])
dataset_eval = dataset_eval.batch(batch_size=32, drop_remainder=True)
Downloading data from https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/datasets/MNIST_Data.zip (10.3 MB)

file_sizes: 100%|███████████████████████████| 10.8M/10.8M [00:11<00:00, 961kB/s]
Extracting zip file...
Successfully downloaded / unzipped to ./

下载的数据集文件的目录结构如下:

./MNIST_Data
├── test
│   ├── t10k-images-idx3-ubyte
│   └── t10k-labels-idx1-ubyte
└── train
    ├── train-images-idx3-ubyte
    └── train-labels-idx1-ubyte

训练LeNet网络

实验中使用LeNet作为演示模型完成图像分类,这里先定义网络并使用MNIST数据集进行训练。

定义LeNet网络:

[2]:
from mindspore import nn, ops, grad
from mindspore.common.initializer import Normal


class LeNet5(nn.Cell):
    """LeNet5"""
    def __init__(self, num_classes=10, num_channel=1, include_top=True):
        super(LeNet5, self).__init__()
        self.include_top = include_top

        self.conv1 = nn.Conv2d(num_channel, 6, 5, pad_mode='valid')
        self.conv2 = nn.Conv2d(6, 16, 5, pad_mode='valid')
        self.relu = nn.ReLU()
        self.max_pool2d = nn.MaxPool2d(kernel_size=2, stride=2)

        if self.include_top:
            self.flatten = nn.Flatten()
            self.fc1 = nn.Dense(16 * 5 * 5, 120, weight_init=Normal(0.02))
            self.fc2 = nn.Dense(120, 84, weight_init=Normal(0.02))
            self.fc3 = nn.Dense(84, num_classes, weight_init=Normal(0.02))

    def construct(self, x):
        """
        LeNet5 construct.
        """
        x = self.conv1(x)
        x = self.relu(x)
        x = self.max_pool2d(x)
        x = self.conv2(x)
        x = self.relu(x)
        x = self.max_pool2d(x)
        if self.include_top:
            x = self.flatten(x)
            x = self.relu(self.fc1(x))
            x = self.relu(self.fc2(x))
            x = self.fc3(x)
        return x


network = LeNet5()

定义优化器与损失函数:

[3]:
net_loss = nn.CrossEntropyLoss()
net_opt = nn.Momentum(network.trainable_params(), learning_rate=0.01, momentum=0.9)

定义网络参数:

[4]:
from mindspore.train import Model, LossMonitor, CheckpointConfig, ModelCheckpoint

config_ck = CheckpointConfig(save_checkpoint_steps=1875, keep_checkpoint_max=10)
ckpoint = ModelCheckpoint(prefix="checkpoint_lenet", config=config_ck)

训练LeNet网络:

[5]:

model = Model(network, loss_fn=net_loss, optimizer=net_opt, metrics={'accuracy'}) model.train(5, dataset_train, callbacks=[ckpoint, LossMonitor(1875)])
epoch: 1 step: 1875, loss is 0.05328045040369034
epoch: 2 step: 1875, loss is 0.05333159118890762
epoch: 3 step: 1875, loss is 0.010423079133033752
epoch: 4 step: 1875, loss is 0.000635102391242981
epoch: 5 step: 1875, loss is 0.23182234168052673

测试此时的网络,可以观察到LeNet已经达到比较高的精度:

[6]:
acc = model.eval(dataset_eval)
print("{}".format(acc))
{'accuracy': 0.9879807692307693}

加载已经训练好的LeNet模型:

[7]:
from mindspore import load_checkpoint, load_param_into_net

param_dict = load_checkpoint("checkpoint_lenet-5_1875.ckpt")
load_param_into_net(network, param_dict)
[7]:
[]

实现FGSM

在得到精准的LeNet网络之后,下面将会采用FSGM攻击方法,在图像中加载噪声后重新进行测试。

先通过损失函数求取反向梯度:

[8]:
def forward_fn(inputs, targets):
    out = network(inputs)
    loss = net_loss(out, targets)
    return loss

然后根据公式(2)实现FGSM攻击:

[9]:
from mindspore import grad

grad_fn = grad(forward_fn, 0)

def generate(inputs, labels, eps):
    # 实现FGSM
    gradient = grad_fn(inputs, labels)
    # 产生扰动
    perturbation = eps * ops.sign(gradient)
    # 生成受到扰动的图片
    adv_x = inputs + perturbation
    return adv_x


def batch_generate(inputs, labels, eps, batch_size):
    # 对数据集进行处理
    arr_x = inputs
    arr_y = labels
    len_x = len(inputs)
    batches = int(len_x / batch_size)
    res = []
    for i in range(batches):
        x_batch = arr_x[i * batch_size: (i + 1) * batch_size]
        y_batch = arr_y[i * batch_size: (i + 1) * batch_size]
        adv_x = generate(x_batch, y_batch, eps=eps)
        res.append(adv_x)
    adv_x = ops.concat(res)
    return adv_x

再次处理MINIST数据集中测试集的图片:

[10]:
images = []
labels = []
test_images = []
test_labels = []
predict_labels = []

ds_test = dataset_eval.create_dict_iterator()

for data in ds_test:
    images = data['image']
    labels = data['label']
    test_images.append(images)
    test_labels.append(labels)
    pred_labels = model.predict(images).argmax(1)
    predict_labels.append(pred_labels)

test_images = ops.concat(test_images)
predict_labels = ops.concat(predict_labels)
true_labels = ops.concat(test_labels)

运行攻击

由FGSM攻击公式中可以看出,攻击系数\(\varepsilon\)越大,对梯度的改变就越大。当\(\varepsilon\) 为零时则攻击效果不体现。

\[\eta = \varepsilon sign(\nabla_x J(\theta)) \tag{3}\]

现在先观察当\(\varepsilon\)为零时的攻击效果:

[11]:
import mindspore as ms

advs = batch_generate(test_images, true_labels, batch_size=32, eps=0.0)

adv_predicts = model.predict(advs).argmax(1)
accuracy = ops.equal(adv_predicts, true_labels).astype(ms.float32).mean()
print(accuracy)
0.9879808

再将\(\varepsilon\)设定为0.5,尝试运行攻击:

[12]:
advs = batch_generate(test_images, true_labels, batch_size=32, eps=0.5)

adv_predicts = model.predict(advs).argmax(1)
accuracy = ops.equal(adv_predicts, true_labels).astype(ms.float32).mean()
print(accuracy)
0.411258

从上面的打印结果可以看出,此时LeNet模型的精度大幅降低。

下面演示受攻击照片现在的实际形态,可以看出图片只是发生了很小的变化,然而在精度测试中却严重下降:

[13]:
import matplotlib.pyplot as plt
%matplotlib inline

adv_examples = advs[:10].transpose(0, 2, 3, 1)
ori_examples = test_images[:10].transpose(0, 2, 3, 1)

plt.figure(figsize=(10, 3), dpi=120)
for i in range(10):
    plt.subplot(3, 10, i + 1)
    plt.axis("off")
    plt.imshow(ori_examples[i].squeeze().asnumpy())
    plt.subplot(3, 10, i + 11)
    plt.axis("off")
    plt.imshow(adv_examples[i].squeeze().asnumpy())
plt.show()
../_images/cv_fgsm_26_0.png