EXPLAINING AND HARNESSING ADVERSARIAL EXAMPLES
- 研究动机:
机器学习模型始终会受到对抗扰动的影响,从而导致错误分类
传统的理解认为是由于深度神经网络的极端非线性或者模型本身有关
- 主要工作
提出了神经网络易受对抗性扰动影响的主要原因是它们的线性本质
提出了一种快速梯度法(FGSM)的扰动方式
提出了基于FGSM的对抗训练方法
解释了对抗样本的泛化原因
- 内容:
- 对抗扰动
目标:模型将其分类错误;人眼无法辨别是否添加了扰动
加入扰动的公式为:
其中样本添加的扰动非常小,满足
-
- 线性扰动:
对于线性模型,扰动后的x在模型中通常要与权重相乘
- 线性扰动:
模型出现错误的原因在于最后一项会导致模型出现错误的结果,同时η = sign(w)来约束η的增加。如果w的维度为n,元素的平均大小为m,那么增量为mn。从而解释了为什么在线性模型中一个小的扰动会使得结果发生改变
-
- 非线性模型的线性扰动:
通过改变模型的梯度值,将梯度值作用到权重后引起线性变换从而达到扰动的目的。
Sign控制扰动的方向
-
- 线性模型的对抗训练
在逻辑回归(逻辑回归是一种有效的二分类方法,通过将线性回归的输出映射到0到1之间,可以预测样本属于某一类的概率)模型上运用FGSM,提高模型的鲁棒性。
对于二分类任务:
σ是sigmoid函数 ,softplus function 是一个输出值永远大于0属于激活函数。、
公式的推导过程:
-
- 深度网络的对抗训练
由于深度网络的非线性结构,可以在训练网络过程中抵御对抗扰动攻击。
J(θ,x,y) 是原始的损失函数,它基于原始输入x与y的计算。a是一个超参数,平衡扰动前后的一个权重。
这深度对抗训练过程中是不断迭代的跟新对抗样本,当对抗训练验证错误趋向平稳时候停止训练。 ----由于经过对抗训练之后,其权重变得更加局部化和可解释,这意味着模型学会了更具体和有意义的特征
如果数据集是零均值和零方差的情况下,由于向量与噪声之间的点积为零对抗效果较差。
零均值:
定义:如果一个数据集的均值是0,则称这个数据集是零均值的。
意义:零均值意味着数据集的平均值是0,数据集的中心在原点。
零方差:
定义:如果一个数据集的方差是0,则称这个数据集是零方差的。
意义:零方差意味着数据集中的所有数据点都具有相同的值,数据集的波动为0。
在激活函数为无界函数时,在输入样本中添加扰动效果更好---无界函数是指:输出值可以取任意大小的激活函数 ReLU Tanh
在激活函数使用饱和激活函数的模型时,隐藏层和样本中效果相同----饱和激活函数的输出值被限制在一个固定的范围内,例如Sigmoid的输出范围是[0, 1],Tanh的输出范围是[-1, 1]
二者区别,隐藏层扰动:添加的扰动较小。在数据中添加绕能够增加正则化(防止过拟合)效果
提高对抗训练的成功率的方法:
增大模型,即使用1600个隐藏层神经单元代替240个隐藏层神经单元
在验证集上使用早停算法--一旦验证集的性能不再提高(即不再下降),就停止训练。
RBF网络可以抵抗对抗性的例子
-
- 对抗样本泛化的原因
在一个特定模型上产生的对抗样本通常也容易被其他模型误分类,即使这些模型的结构不同或者模型在不同的训练集上训练。甚至,不同的模型对对抗样本误分类的结果相同。
作者提出了一个广泛的子空间的概念解释
如图在不同的扰动下,FGSM可以在一维的连续子空间产生对抗样本,而不是特定的区域。
为了解释为什么不同的分类器将对抗样本误分类到同一个类,者假设目前的方法训练神经网络都类似于在同一个训练集上学习的线性分类器。由于机器学习算法的泛化能力,所以线性分类器可以在训练集的不同子集上训练出大致相同的分类权重。―――相当于每个模型在使用相同的数据集学习的时候,虽然是不同的子集,但是会导致不同的分类器关注的都是照片同一个部分其他假设
作者反驳了两个假设:
1.生成训练可以在训练过程中提供更多的约束或限制,或者使模型学习如何分辨 "real" 或 "fake" 的数据,并且只对"real"的数据更加自信。
2.单个模型有奇怪的行为特点(strange quirks),但对多个模型进行平均(模型平均)可能会抵抗对抗样本的干扰。
论文总结
此篇论文提出了以下结论:
- 对抗样本可以解释为高维点积的一种特征属性,它们的产生是由于模型过于线性化,而非过于非线性化。
- 对抗样本在不同模型中的泛化可以解释为对抗扰动与模型的权重向量高度一致,且不同模型(当执行相同任务时,如手写数字分类)在训练中学习的函数相同。
- 扰动的方向是最重要的,而非空间中的特定点。对抗样本广泛存在于空间中。
- 对抗扰动会泛化到不同的干净样本中。
- 提出了生成对抗样本的方法——FGSM。
- 作者证明了对抗训练可以提供正则作用,甚至比dropout更强。
- 作者进行了控制实验,但未能使用简单 效率低的正则化器来重现这种效果。
- 易于优化的模型容易收到对抗样本的干扰。
- 线性模型缺乏对对抗扰动的抵抗能力,具有隐藏层结构的模型才能通过对抗训练来抵抗它们。
- RBF网络对对抗样本有抵抗能力。
- 为输入分布建模而训练的模型对对抗样本没有抵抗能力。
- 集成对对抗样本没有抵抗能力
from __future__ import print_function
import random
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import numpy as np
import matplotlib.pyplot as plt
# 预训练模型路径(训练好的模型文件的存储路径)
#可以自己下载一个,也可以自己训练模型参数.
pretrained_model = "FGSM-master\\FGSM/lenet_mnist_model.pth"
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('./datasets', train=False, download=Ture, transform=transforms.ToTensor()),
batch_size=1,
shuffle=True
)
all_indices = list(range(len(test_loader.dataset)))
random_indices = random.sample(all_indices, 1000)
sampler = torch.utils.data.sampler.SubsetRandomSampler(random_indices)
test_loader = torch.utils.data.DataLoader(
test_loader.dataset,
batch_size=1,
sampler=sampler
)
# 定义LeNet模型
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)
#Dropout是一种用于防止过拟合的正则化技术。在训练神经网络时,Dropout会随机地“丢弃”一些神经元
self.conv2_drop = nn.Dropout2d()
#全连接层它的作用是将 320 维的输入特征映射到 50 维的输出特征
self.fc1 = nn.Linear(320, 50)
#因为最后只有10个结果,所以最后的结果是10
self.fc2 = nn.Linear(50, 10)
def forward(self, x):
#F.max_pool2d 最大化池化,2,是2* 2的窗口大小
x = F.relu(F.max_pool2d(self.conv1(x), 2))
x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
#将二维的特征图展平为一维的向量
#torch.Size([1, 20, 4, 4])表示一个大小为1个样本,20个通道,4x4的像素的张量转化为torch.Size([1, 320])方便全连接使用
x = x.view(-1, 320)
x = F.relu(self.fc1(x))
#而在评估模式下(即training=False),不会进行操作,后面设置了为ture
x = F.dropout(x, training=self.training)
x = self.fc2(x)
#softmax函数将每个输入值转换为概率分布,使得每个值都大于等于0,且所有值的和为1。
return F.log_softmax(x, dim=1)
def fgsm_attack(input_data, epsilon, data_grad):
"""
:param epsilon: 扰动值的范围
:param data_grad: 图像的梯度
:return: 扰动后的图像
"""
# 收集数据梯度的元素符号
# 将输入数据包装在requires_grad=True的变量中,以便计算梯度
# 获取梯度
# 沿着梯度的反方向更新输入数据
perturbed_input = input_data + epsilon * torch.sign(data_grad)
# 添加剪切以维持[0,1]范围
perturbed_input = torch.clamp(perturbed_input, 0, 1)
# 返回扰动后的输入数据
return perturbed_input
def test( model, device, test_loader, epsilon ):
# 精度计数器
correct = 0
adv_examples = []
# 循环遍历测试集中的所有示例
for data, target in test_loader:
# 把数据和标签发送到设备
data, target = data.to(device), target.to(device)
#PyTorch文档中提到,如果grad属性不为空,新计算出来的梯度值会直接加到旧值上面。
#为什么不直接覆盖旧的结果呢?这是因为有些Tensor可能有多个输出,那么就需要调用多个backward。
#叠加的处理方式使得backward不需要考虑之前有没有被计算过导数,只需要加上去就行了。
#我们的情况很简单,就一个输出,所以需要使用这条语句
data.requires_grad = True
# 通过模型前向传递数据
output = model(data)
init_pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability
# 如果初始预测是错误的,不打断攻击,继续
if init_pred.item() != target.item():
continue
# 计算损失
loss = F.nll_loss(output, target)
# 将所有现有的渐变归零
model.zero_grad()
# 计算后向传递模型的梯度
loss.backward()
#获得图片的梯度
data_grad = data.grad.data
# FGSM进行攻击
perturbed_data = fgsm_attack(data, epsilon, data_grad)
# 重新分类受扰乱的图像
output = model(perturbed_data)
# 查看加入扰动后的图片
final_pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability
if final_pred.item() == target.item():
correct += 1
# 保存0 epsilon示例的特例
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代表加了扰动后,模型还能正确识别数据的概率
final_acc = correct/float(len(test_loader))
print("Epsilon: {}\tTest Accuracy = {} / {} = {}".format(epsilon, correct, len(test_loader), final_acc))
# 返回准确性和对抗性示例
return final_acc, adv_examples
if __name__ == '__main__':
# 初始化网络
model = Net().to(device)
# 加载已经预训练的模型
model.load_state_dict(torch.load(pretrained_model, map_location='cpu'))
# 在评估模式下设置模型(Dropout层不被考虑)
epsilons = [0, .05, .1, .15, .2, .25, .3]
model.eval()
accuracies = []
examples = []
# 对每个epsilon运行测试
for eps in epsilons:
acc, ex = test(model, device, test_loader, eps)
accuracies.append(acc)
examples.append(ex)
plt.figure(figsize=(5,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()
# 在每个epsilon上绘制几个对抗样本的例子
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()