▊ 1 引言
该论文出自于牛津大学,主要是关于对抗训练的研究。目前已经有研究表明使用单步进行对抗训练会导致一种严重的过拟合现象,在该论文中作者经过理论分析和实验验证重新审视了对抗噪声和梯度剪切在单步对抗训练中的作用。
作者发现对于大的对抗扰动半径可有效避免过拟合现象。基于该观察结果,作者提出了一种随机噪声对抗训练,实验表明该方法不仅提供了单步对抗训练的减少计算开销的好处,而且也不会受到过拟合现象的影响。
论文里没有提供相关源代码,本文最后一节是关于该论文算法的一个简单实现。
论文链接:https://arxiv.org/abs/2202.01181
▊ 2 预备知识
给定一个参数为的分类器,一个对抗扰动集合。如果对于任意的对抗扰动,有,则可以说在点关于对抗扰动集合是鲁棒的。对抗扰动集合的定义为:
为了使得神经网络模型能够在范数具有鲁棒性。对抗训练在数据集上修正类别训练进程并最小化损失函数,其中对抗训练的目标为:
其中是图片分类器的交叉熵损失函数。由于找到内部最大化的最优解是非常困难的,对抗训练最常见的方法就是通过来近似最坏情况下的对抗扰动。虽然这已经被证明可以产生鲁棒性模型,但是计算开销随着迭代数量而线性增加。
因此,当前的工作专注于通过一步逼近内部最大化最优解来降低对抗训练的成本。假设损失函数对于输入的变化是局部线性的,那么可以知道对抗训练内部最大化具有封闭形式的解。
利用这一点提出了,其中对抗扰动遵循梯度符号的方向,等人建议在之前添加一个随机初始化。然而,这两种方法后来都被证明容易受到多步攻击,具体公式表示为:
其中,服从概率分布。当是投影到操作,并且是均匀分布,是输入空间的维数。
▊ 3 N-FGSM对抗训练
在进行对抗性训练时,一种常见的做法是将训练期间使用的干扰限制在范围。其背后原理是,在训练期间增加扰动的幅度可能不必要地降低分类精度,因为在测试时不会评估约束球外的扰动。
虽然通过剪裁或限制噪声大小来限制训练期间使用的扰动是一种常见做法,但是由于梯度剪切是在采取梯度上升步骤后执行的,所以剪切点可能不再进行有效的对抗训练。
基于上述动机,作者主要探索梯度剪裁操作和随机步长中噪声的大小在单步方法中获得的鲁棒性的作用。作者本文中提出了一种简单有效的单步对抗训练方法,具体的计算公式如下所示:
其中是从均分布中采样得来。由于不涉及梯度剪裁,可以发它扰动的期望平方范数大于。相关算法流程图,引理和定理的证明如下所示。
引理1(对抗扰动的期望): 已知的对抗扰动如下定义:
其中,分布是均匀分布,并且对抗扰动步长为,则有:
证明:由不等式可知,当时函数)是凹函数,则有:
则以下不等式成立:
以下主要计算期望并将缩写为,具体证明步骤如下所示:
进而则有:
证毕。
定理1 令是方法生成的对抗扰动,是方法生成的对抗扰动,是方法生成的对抗扰动,对于任意的,则有以下不等式成立:
证明:由引理1可知:
又因为
如果令超参数,,,则有:
证毕。
▊ 4 实验结果
下图表示的是在数据集(左)和(右)上比较和的多步方法在不同的扰动半径下使用神经网络的分类准确率。
可以发现尽管所有方法都达到干净样本的分类精度(虚线),但和单步法之间在鲁棒精度方面存在差距,而且,最重要的是是的计算开销的10倍。
下图表示的是在数据在(左)和(右)上的单步方法与网络在不同扰动半径上的比较。可以发现该论文的方法可以匹配或超过现有技术的结果,同时将计算成本降低3倍。
下图表示的是在训练开始(顶部)和结束(底部)的几个时期,对抗扰动和梯度平均值的可视化图。可以发现当过拟合之后,和无法对对抗扰动进行解释,其梯度也是如此,但是和却可以避免这种情况的发生。
▊ 5 论文代码
该论文并没有提供源码,以下是在数据集中对论文中代码进行的实现。
代码语言:javascript复制import argparse
import logging
import time
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, Dataset
import os
import argparse
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument('--batch-size', default=100, type=int)
parser.add_argument('--data-dir', default='mnist-data', type=str)
parser.add_argument('--epochs', default=10, type=int)
parser.add_argument('--epsilon', default=0.3, type=float)
parser.add_argument('--alpha', default=0.375, type=float)
parser.add_argument('--lr-max', default=5e-3, type=float)
parser.add_argument('--lr-type', default='cyclic')
parser.add_argument('--fname', default='mnist_model', type=str)
parser.add_argument('--seed', default=0, type=int)
return parser.parse_args()
class Flatten(nn.Module):
def forward(self, x):
return x.view(x.size(0), -1)
def mnist_net():
model = nn.Sequential(
nn.Conv2d(1, 16, 4, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 32, 4, stride=2, padding=1),
nn.ReLU(),
Flatten(),
nn.Linear(32*7*7,100),
nn.ReLU(),
nn.Linear(100, 10)
)
return model
class Attack_methods(object):
def __init__(self, model, X, Y, epsilon, alpha):
self.model = model
self.epsilon = epsilon
self.X = X
self.Y = Y
self.epsilon = epsilon
self.alpha = alpha
def nfgsm(self):
eta = torch.zeros_like(self.X).uniform_(-self.epsilon, self.epsilon)
delta = torch.zeros_like(self.X)
eta.requires_grad = True
output = self.model(self.X eta)
loss = nn.CrossEntropyLoss()(output, self.Y)
loss.backward()
grad = eta.grad.detach()
delta.data = eta self.alpha * torch.sign(grad)
return delta
class Adversarial_Trainings(object):
def __init__(self, epochs, train_loader, model, opt, epsilon, alpha, iter_num, lr_max, lr_schedule,
fname, logger):
self.epochs = epochs
self.train_loader = train_loader
self.model = model
self.opt = opt
self.epsilon = epsilon
self.alpha = alpha
self.iter_num = iter_num
self.lr_max = lr_max
self.lr_schedule = lr_schedule
self.fname = fname
self.logger = logger
def fast_training(self):
for epoch in range(self.epochs):
start_time = time.time()
train_loss = 0
train_acc = 0
train_n = 0
for i, (X, y) in enumerate(self.train_loader):
X, y = X.cuda(), y.cuda()
lr = self.lr_schedule(epoch (i 1) / len(self.train_loader))
self.opt.param_groups[0].update(lr=lr)
# Generating adversarial example
adversarial_attack = Attack_methods(self.model, X, y, self.epsilon, self.alpha)
delta = adversarial_attack.nfgsm()
# Update network parameters
output = self.model(torch.clamp(X delta, 0, 1))
loss = nn.CrossEntropyLoss()(output, y)
self.opt.zero_grad()
loss.backward()
self.opt.step()
train_loss = loss.item() * y.size(0)
train_acc = (output.max(1)[1] == y).sum().item()
train_n = y.size(0)
train_time = time.time()
self.logger.info('%d t %.1f t %.4f t %.4f t %.4f', epoch, train_time - start_time, lr, train_loss/train_n, train_acc/train_n)
torch.save(self.model.state_dict(), self.fname)
logger = logging.getLogger(__name__)
logging.basicConfig(
format='[%(asctime)s] - %(message)s',
datefmt='%Y/%m/%d %H:%M:%S',
level=logging.DEBUG)
def main():
args = get_args()
logger.info(args)
np.random.seed(args.seed)
torch.manual_seed(args.seed)
torch.cuda.manual_seed(args.seed)
mnist_train = datasets.MNIST("mnist-data", train=True, download=True, transform=transforms.ToTensor())
train_loader = torch.utils.data.DataLoader(mnist_train, batch_size=args.batch_size, shuffle=True)
model = mnist_net().cuda()
model.train()
opt = torch.optim.Adam(model.parameters(), lr=args.lr_max)
if args.lr_type == 'cyclic':
lr_schedule = lambda t: np.interp([t], [0, args.epochs * 2 // 5, args.epochs], [0, args.lr_max, 0])[0]
elif args.lr_type == 'flat':
lr_schedule = lambda t: args.lr_max
else:
raise ValueError('Unknown lr_type')
logger.info('Epoch t Time t LR t t Train Loss t Train Acc')
adversarial_training = Adversarial_Trainings(args.epochs, train_loader, model, opt, args.epsilon, args.alpha, 40,
args.lr_max, lr_schedule, args.fname, logger)
adversarial_training.fast_training()
if __name__ == "__main__":
main()
运行的实验结果如下所示
END