梯度攻击 | Word count: 2.1k | Reading time: 8min | Post View:
梯度攻击
模型攻击主要指人为地制造干扰迷惑模型,使之产生错误的结果。随着深度学习模型的广泛使用,人们发现它很容易被数据的轻微扰动所欺骗,于是开始寻找更加有效的攻击方法,针对攻击又有对抗攻击的方法,二者相互推进,不仅加强了模型的健壮性,有时还能提升模型的准确度。
原理
想让攻击更加有效,导致模型分类错误,也就是使损失函数的值变大。正常训练模型时,输入
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%
以上的模型,然后分别用两种方法加入扰动,并分析其识别率的变化。例程分为三部分:训练分类模型、攻击、做图。
训练分类模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import torchimport torch.nn as nnimport torch.nn.functional as Fimport torch.optim as optimfrom torchvision import datasets, transformsfrom torch.autograd import Variableimport numpy as npimport matplotlib.pyplot as pltclass 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' ) 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 ): 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' )
攻击:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 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 for i in range (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 ) 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 ) 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)
做图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 做图 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 转换成文字。