对抗训练是一种引入噪声的训练方式,可以对参数进行正则化,提升模型鲁棒性和泛化能力
就是在原始输入样本 x 上加上一个扰动Δx得到对抗样本,再用其进行训练,这个问题可以抽象成这样一个模型:
m a x θ P ( y ∣ x + Δ x ; θ ) max_{\theta}P(y|x+\Delta x; \theta) maxθP(y∣x+Δx;θ)
其中,y 是ground truth
, θ \theta θ 是模型参数。意思就是即使在扰动的情况下求使得预测出y的概率最大的参数,扰动可以被定义为:
Δ x = ϵ ∗ s i g n ( ∇ x L ( x , y ; θ ) ) \Delta x= \epsilon *sign(\nabla x L(x,y; \theta)) Δx=ϵ∗sign(∇xL(x,y;θ))
其中,sign为符号函数,L 为损失函数
最后,GoodFellow还总结了对抗训练的两个作用:
Madry在2018年的ICLR论文Towards Deep Learning Models Resistant to Adversarial Attacks中总结了之前的工作,对抗训练可以统一写成如下格式:
m i n θ E ( x , y ) ∼ D [ m a x Δ x ∈ Ω L ( x + Δ x , y ; θ ) ] min_{\theta} E_{(x,y) \sim D}[max_{\Delta x \in \Omega }L(x+\Delta x,y; \theta)] minθE(x,y)∼D[maxΔx∈ΩL(x+Δx,y;θ)]
其中D代表训练集,x代表输入,y代表标签,θ是模型参数,L(x,y;θ)是单个样本的loss,Δx是对抗扰动,Ω是扰动空间。这个统一的格式首先由论文《Towards Deep Learning Models Resistant to Adversarial Attacks》提出。
这个式子可以分步理解如下:
1、往属于x里边注入扰动Δx,Δx的目标是让L(x+Δx,y;θ)越大越好,也就是说尽可能让现有模型的预测出错;
2、当然Δx也不是无约束的,它不能太大,否则达不到“看起来几乎一样”的效果,所以Δx要满足一定的约束,常规的约束是∥Δx∥≤ϵ,其中ϵ是一个常数;
3、每个样本都构造出对抗样本x+Δx之后,用(x+Δx,y)作为数据对去最小化loss来更新参数θ(梯度下降);
4、反复交替执行1、2、3步。
由此观之,整个优化过程是max和min交替执行,这确实跟GAN很相似,不同的是,GAN所max的自变量也是模型的参数,而这里max的自变量则是输入(的扰动量),也就是说要对每一个输入都定制一步max。
现在的问题是如何计算Δx,它的目标是增大L(x+Δ,y;θ),而我们知道让loss减少的方法是梯度下降,那反过来,让loss增大的方法自然就是梯度上升,因此可以简单地取
Δx=ϵ∇xL(x,y;θ)
当然,为了防止Δx过大,通常要对∇xL(x,y;θ)做些标准化,比较常见的方式是
Δ x = ϵ ∇ x L ( x , y ; θ ) ∥ ∇ x L ( x , y ; θ ) ∥ 或 Δ x = ϵ s i g n ( ∇ x L ( x , y ; θ ) ) Δx=ϵ \frac{∇xL(x,y;θ)}{∥∇xL(x,y;θ)∥}或Δx=ϵsign(∇xL(x,y;θ)) Δx=ϵ∥∇xL(x,y;θ)∥∇xL(x,y;θ)或Δx=ϵsign(∇xL(x,y;θ))
有了Δx之后,就可以代回式(1)进行优化
m i n θ E ( x , y ) ∼ D [ L ( x + Δ x , y ; θ ) ] min_θE_{(x,y)∼D}[L(x+Δx,y;θ)] minθE(x,y)∼D[L(x+Δx,y;θ)]
这就构成了一种对抗训练方法,被称为Fast Gradient Method(FGM),它由GAN之父Goodfellow在论文《Explaining and Harnessing Adversarial Examples》首先提出。
对于CV领域的任务,上述对抗训练的流程可以顺利执行下来,因为图像可以视为普通的连续实数向量,ΔxΔx也是一个实数向量,因此x+Δxx+Δx依然可以是有意义的图像。但NLP不一样,NLP的输入是文本,它本质上是one hot向量(如果还没认识到这一点,欢迎阅读《词向量与Embedding究竟是怎么回事?》),而两个不同的one hot向量,其欧氏距离恒为2−−√2,因此对于理论上不存在什么“小扰动”。
一个自然的想法是像论文《Adversarial Training Methods for Semi-Supervised Text Classification》一样,将扰动加到Embedding层。这个思路在操作上没有问题,但问题是,扰动后的Embedding向量不一定能匹配上原来的Embedding向量表,这样一来对Embedding层的扰动就无法对应上真实的文本输入,这就不是真正意义上的对抗样本了,因为对抗样本依然能对应一个合理的原始输入。
那么,在Embedding层做对抗扰动还有没有意义呢?有!实验结果显示,在很多任务中,在Embedding层进行对抗扰动能有效提高模型的性能。
对于CV任务来说,一般输入张量的shape是(b,h,w,c),这时候我们需要固定模型的batch size(即b),然后给原始输入加上一个shape同样为(b,h,w,c)、全零初始化的Variable
,比如就叫做Δx,那么我们可以直接求loss对x的梯度,然后根据梯度给Δx赋值,来实现对输入的干扰,完成干扰之后再执行常规的梯度下降。
对于NLP任务来说,原则上也要对Embedding层的输出进行同样的操作,Embedding层的输出shape为(b,n,d),所以也要在Embedding层的输出加上一个shape为(b,n,d)的Variable
,然后进行上述步骤。但这样一来,我们需要拆解、重构模型,对使用者不够友好。
不过,我们可以退而求其次。Embedding层的输出是直接取自于Embedding参数矩阵的,因此我们可以直接对Embedding参数矩阵进行扰动。这样得到的对抗样本的多样性会少一些(因为不同样本的同一个token共用了相同的扰动),但仍然能起到正则化的作用,而且这样实现起来容易得多。
def adversarial_training(model, embedding_name, epsilon=1):
"""给模型添加对抗训练
其中model是需要添加对抗训练的keras模型,embedding_name
则是model里边Embedding层的名字。要在模型compile之后使用。
"""
if model.train_function is None: # 如果还没有训练函数
model._make_train_function() # 手动make
old_train_function = model.train_function # 备份旧的训练函数
# 查找Embedding层
for output in model.outputs:
embedding_layer = search_layer(output, embedding_name)
if embedding_layer is not None:
break
if embedding_layer is None:
raise Exception('Embedding layer not found')
# 求Embedding梯度
embeddings = embedding_layer.embeddings # Embedding矩阵
gradients = K.gradients(model.total_loss, [embeddings]) # Embedding梯度
gradients = K.zeros_like(embeddings) + gradients[0] # 转为dense tensor
# 封装为函数
inputs = (model._feed_inputs +
model._feed_targets +
model._feed_sample_weights) # 所有输入层
embedding_gradients = K.function(
inputs=inputs,
outputs=[gradients],
name='embedding_gradients',
) # 封装为函数
def train_function(inputs): # 重新定义训练函数
grads = embedding_gradients(inputs)[0] # Embedding梯度
delta = epsilon * grads / (np.sqrt((grads**2).sum()) + 1e-8) # 计算扰动
K.set_value(embeddings, K.eval(embeddings) + delta) # 注入扰动
outputs = old_train_function(inputs) # 梯度下降
K.set_value(embeddings, K.eval(embeddings) - delta) # 删除扰动
return outputs
model.train_function = train_function # 覆盖原训练函数
定义好上述函数后,给Keras模型增加对抗训练就只需要一行代码了:
# 写好函数后,启用对抗训练只需要一行代码
adversarial_training(model, 'Embedding-Token', 0.5)
需要指出的是,由于每一步算对抗扰动也需要计算梯度,因此每一步训练一共算了两次梯度,因此每步的训练时间会翻倍。
https://www.jianshu.com/p/3e354f735fd4
对抗训练浅谈:意义、方法和思考(附Keras实现)