模型攻击主要指人为地制造干扰迷惑模型,使之产生错误的结果。随着深度学习模型的广泛使用,人们发现它很容易被数据的轻微扰动所欺骗,于是开始寻找更加有效的攻击方法,针对攻击又有对抗攻击的方法,二者相互推进,不仅加强了模型的健壮性,有时还能提升模型的准确度。

原理

想让攻击更加有效,导致模型分类错误,也就是使损失函数的值变大。正常训练模型时,输入x是固定的,标签y也是固定的,通过训练调整分类模型的参数w,使损失函数逐渐变小。而梯度攻击的分类模型参数w不变(分类逻辑不变),y也固定不变,若希望损失函数值变大,就只能修改输入。下面就来看看如何利用梯度方法修改输入数据。

FGSM

FGSM是比较早期的梯度攻击算法,源于2015年的论文《EXPLAINING AND HARNESSING ADVERSARIAL EXAMPLES》,论文地址:https://arxiv.org/pdf/1412.6572.pdf。FGSM全称是Fast Gradient Sign Method快速梯度下降法。其原理是求模型误差函数对输入的导数,然后用符号函数得到其梯度方向,并乘以一个步长ε,将得到的“扰动”加在原来的输入数据之上就得到了攻击样本。其公式如下:

其中L是损失函数,θ是模型参数,x是输入,x’是扰动后的输入,y是输出,sgn是符号函数,ε为步长。由于只对数据做一次扰动,计算速度非常快,因此有Fast,而累加项是梯度方向,而不是梯度本身,因此有Sign,此种添加扰动的方法就称为Fast Gradient Sign Method。

2017年,FGSM的作者又提出了FGM,对 FGSM 中符号函数部分做了一些修改,以便攻击文本。

PGD

PGD是FGSM的改进版本,它源于2018年的论文《Towards Deep Learning Models Resistant to Adversarial Attacks》,论文地址:https://arxiv.org/pdf/1706.06083.pdf

FGSM从始至终只做了一次修改,改动的大小依赖步长ε,如果步长太大,则原数据被改得面目全非,如果改动太小又无法骗过模型。一般做干扰的目的是保持数据原始的性质,只为骗过模型,而非完全替换数据。如下图所示,当做了一个步长很大的扰动之后,原图直接变成了另一张图,不但机器无法辨认,人也无法辨认。

为了让扰动更加小而有效,出现了迭代修改的方法PGD,每次进行少量修改,扰动多次,这样既避免了抖动过大,同时多次迭代又能在多个方向上修改复杂模型。

其公式如下:

注意等式右边的∏在这里不是连乘符号,它保证扰动过程中的数据x始终处于限制范围之内(若过大则裁剪)。此公式与类似FGSM类似,其中的α是每次修改的步长,而每一次修改的xt+1都基于其前一次修改xt。论文中提到,PGD攻击是最强的一阶攻击,由于相对复杂,PGD也需要更多的时间和算力。

例程

以上两种算法原理都非常简单,而实际操作中,比较困难的是如何对输入求取梯度。下面使用识别Mnist手写体数据集为例展示攻击效果,先训练一个识别率在95%以上的模型,然后分别用两种方法加入扰动,并分析其识别率的变化。例程分为三部分:训练分类模型、攻击、做图。

训练分类模型:

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.autograd import Variable
import numpy as np
import matplotlib.pyplot as plt

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return x

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') #启用GPU
train_loader = torch.utils.data.DataLoader(  # 加载训练数据
    datasets.MNIST('datasets', train=True, download=True,
                   transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=64, shuffle=True)

model = Net()
model = model.to(device)
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)  # 初始化优化器

for epoch in range(1, 10 + 1):  # 共迭代10次
    for batch_idx, (data, target) in enumerate(train_loader):
        data = data.to(device)
        target = target.to(device)
        data, target = Variable(data), Variable(target)
        optimizer.zero_grad()
        output = model(data) # 代入模型
        loss = F.cross_entropy(output,target)
        loss.backward()
        optimizer.step()
        if batch_idx % 100 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                       100. * batch_idx / len(train_loader), loss.item()))

torch.save(model, 'datasets/model.pth') #保存模型

攻击:

USE_PGD = False

def draw(data):
    ex = data.squeeze().detach().cpu().numpy()
    plt.imshow(ex, cmap="gray")
    plt.show()
    
def test(model, device, test_loader, epsilon, t = 5, debug = False):
    correct = 0
    adv_examples = []

    for data, target in test_loader:
        data, target = data.to(device), target.to(device)
        data.requires_grad = True # 以便对输入求导 ** 重要 **
        output = model(data)
        init_pred = output.max(1, keepdim=True)[1]
        if init_pred.item() != target.item(): # 如果不扰动也预测不对,则跳过
            continue
        if debug:
            draw(data)
            
        if USE_PGD:            
            alpha = epsilon / t # 每次只改变一小步
            perturbed_data = data
            final_pred = init_pred
            #while target.item() == final_pred.item(): # 只要修改成功就退出
            for i in range(t): # 共迭代 t 次
                if debug:
                    print("target", target.item(), "pred", final_pred.item())
                loss = F.cross_entropy(output, target) 
                model.zero_grad()
                loss.backward(retain_graph=True)
                data_grad = data.grad.data # 输入数据的梯度 ** 重要 **

                sign_data_grad = data_grad.sign() # 取符号(正负)
                perturbed_image = perturbed_data + alpha * sign_data_grad # 添加扰动
                perturbed_data = torch.clamp(perturbed_image, 0, 1) # 把各元素压缩到[0,1]之间

                output = model(perturbed_data) # 代入扰动后的数据
                final_pred = output.max(1, keepdim=True)[1] # 预测选项
                if debug:
                    draw(perturbed_data)
        else:
            loss = F.cross_entropy(output, target) 
            model.zero_grad()
            loss.backward()

            data_grad = data.grad.data # 输入数据的梯度 ** 重要 **
            sign_data_grad = data_grad.sign() # 取符号(正负)
            perturbed_image = data + epsilon*sign_data_grad # 添加扰动
            perturbed_data = torch.clamp(perturbed_image, 0, 1) # 把各元素压缩到[0,1]之间

            output = model(perturbed_data) # 代入扰动后的数据
            final_pred = output.max(1, keepdim=True)[1]
        
        # 统计准确率并记录,以便后面做图
        if final_pred.item() == target.item():
            correct += 1
            if (epsilon == 0) and (len(adv_examples) < 5):
                adv_ex = perturbed_data.squeeze().detach().cpu().numpy()
                adv_examples.append((init_pred.item(), final_pred.item(), adv_ex))
        else: # 保存扰动后错误分类的图片
            if len(adv_examples) < 5:
                adv_ex = perturbed_data.squeeze().detach().cpu().numpy()
                adv_examples.append((init_pred.item(), final_pred.item(), adv_ex))

    final_acc = correct / float(len(test_loader)) # 计算整体准确率
    print("Epsilon: {}\tTest Accuracy = {} / {} = {}".format(epsilon, correct, len(test_loader), final_acc))
    return final_acc, adv_examples

epsilons = [0, .05, .1, .15, .2, .25, .3] # 使用不同的调整力度
pretrained_model = "datasets/model.pth"  # 使用的预训练模型路径

test_loader = torch.utils.data.DataLoader(
    datasets.MNIST('datasets', train=False, download=True, transform=transforms.Compose([
        transforms.ToTensor(),
    ])),
    batch_size=1, shuffle=True
)
model = torch.load(pretrained_model, map_location='cpu').to(device)
model.eval()

accuracies = []
examples = []
for eps in epsilons:  # 每次测一种超参数
    acc, ex = test(model, device, test_loader, eps)
    accuracies.append(acc)
    examples.append(ex)

做图

# 做图
plt.figure(figsize=(8,5))
plt.plot(epsilons, accuracies, "*-")
plt.yticks(np.arange(0, 1.1, step=0.1))
plt.xticks(np.arange(0, .35, step=0.05))
plt.title("Accuracy vs Epsilon")
plt.xlabel("Epsilon")
plt.ylabel("Accuracy")
plt.show()

cnt = 0
plt.figure(figsize=(8,10))
for i in range(len(epsilons)):
    for j in range(len(examples[i])):
        cnt += 1
        plt.subplot(len(epsilons),len(examples[0]),cnt)
        plt.xticks([], [])
        plt.yticks([], [])
        if j == 0:
            plt.ylabel("Eps: {}".format(epsilons[i]), fontsize=14)
        orig,adv,ex = examples[i][j]
        plt.title("{} -> {}".format(orig, adv))
        plt.imshow(ex, cmap="gray")
plt.tight_layout()
plt.show()

从例程运行结果可以看到,同样的改动大小ε,PGD明显比FGSM效果好,这让开发者使用更小的改动达到更好的效果,当然,PGD速度也比较慢。

用以上方法得到的不是一个神经网络模型,而是根据现有模型计算出来的,对单个实例的调整方法和结果。

本例是对图片分类模型的攻击,攻击文字模型时,主要是在Embedding层上添加扰动,涉及Token与Embedded转换问题,可以借助gensim等工具将梯度调整后的Embedded转换成文字。

posted on 2020-03-11 10:24  xieyan0811  阅读(58)  评论(0编辑  收藏  举报