泛化性乱弹:从随机噪声、梯度惩罚到虚拟对抗训练
By 苏剑林 | 2020-06-01 | 99400位读者 |提高模型的泛化性能是机器学习致力追求的目标之一。常见的提高泛化性的方法主要有两种:第一种是添加噪声,比如往输入添加高斯噪声、中间层增加Dropout以及进来比较热门的对抗训练等,对图像进行随机平移缩放等数据扩增手段某种意义上也属于此列;第二种是往loss里边添加正则项,比如L1,L2惩罚、梯度惩罚等。本文试图探索几种常见的提高泛化性能的手段的关联。
随机噪声 #
我们记模型为f(x),D为训练数据集合,l(f(x),y)为单个样本的loss,那么我们的优化目标是
argminθL(θ)=E(x,y)∼D[l(f(x),y)]
θ是f(x)里边的可训练参数。假如往模型输入添加噪声ε,其分布为q(ε),那么优化目标就变为
argminθLε(θ)=E(x,y)∼D,ε∼q(ε)[l(f(x+ε),y)]
当然,可以添加噪声的地方不仅仅是输入,也可以是中间层,也可以是权重θ,甚至可以是输出y(等价于标签平滑),噪声也不一定是加上去的,比如Dropout是乘上去的。对于加性噪声来说,q(ε)的常见选择是均值为0、方差固定的高斯分布;而对于乘性噪声来说,常见选择是均匀分布U([0,1])或者是伯努利分布。
添加随机噪声的目的很直观,就是希望模型能学会抵御一些随机扰动,从而降低对输入或者参数的敏感性,而降低了这种敏感性,通常意味着所得到的模型不再那么依赖训练集,所以有助于提高模型泛化性能。
提高效率 #
添加随机噪声的方式容易实现,而且在不少情况下确实也很有效,但它有一个明显的缺点:不够“特异性”。噪声ε是随机的,而不是针对x构建的,这意味着多数情况下x+ε可能只是一个平凡样本,也就是没有对原模型造成比较明显的扰动,所以对泛化性能的提高帮助有限。
增加采样 #
从理论上来看,加入随机噪声后,单个样本的loss变为
˜l(x,y)=Eε∼q(ε)[l(f(x+ε),y)]=∫q(ε)l(f(x+ε),y)dε
但实践上,对于每个特定的样本(x,y),我们一般只采样一个噪声,所以并没有很好地近似上式。当然,我们可以采样多个噪声ε1,ε2,⋯,εk∼q(ε),然后更好地近似
˜l(x,y)≈1kk∑i=1l(f(x+εi),y)
但这样相当于batch_size扩大为原来的k倍,增大了计算成本,并不是那么友好。
近似展开 #
一个直接的想法是,如果能事先把式(3)中的积分算出来,那就用不着低效率地采样了(或者相当于一次性采样无限多的噪声)。我们就往这个方向走一下试试。当然,精确的显式积分基本上是做不到的,我们可以做一下近似展开:
l(f(x+ε),y)≈l(f(x),y)+(ε⋅∇x)l(f(x),y)+12(ε⋅∇x)2l(f(x),y)
然后两端乘以q(ε)积分,这里假设ε的各个分量是独立同分布的,并且均值为0、方差为σ2,那么积分结果就是
∫q(ε)l(f(x+ε),y)dε≈l(f(x),y)+12σ2Δl(f(x),y)
这里的Δ是拉普拉斯算子,即Δf=∑i∂2∂x2if。这个结果在形式上很简单,就是相当于往loss里边加入正则项12σ2Δl(f(x),y),然而实践上却相当困难,因为这意味着要算l的二阶导数,再加上梯度下降,那么就一共要算三阶导数,这是现有深度学习框架难以高效实现的。
转移目标 #
直接化简l(f(x+ε),y)的积分是行不通了,但我们还可以试试将优化目标换成
l(f(x+ε),f(x))+l(f(x),y)
也就是变成同时缩小f(x),y、f(x+ε),f(x)的差距,两者双管齐下,一定程度上也能达到缩小f(x+ε),y差距的目标。关键的是,这个目标能得到更有意思的结果。
思路解析 #
用数学的话来讲,如果l是某种形式的距离度量,那么根据三角不等式就有
l(f(x+ε),y)≤l(f(x+ε),f(x))+l(f(x),y)
如果l不是度量,那么通常根据詹森不等式也能得到一个类似的结果,比如l(f(x+ε),y)=‖f(x+ε)−y‖2,那么我们有
‖f(x+ε)−f(x)+f(x)−y‖2=‖12×2[f(x+ε)−f(x)]+12×2[f(x)−y]‖2≤12‖2[f(x+ε)−f(x)]‖2+12‖2[f(x)−y]‖2=2(‖f(x+ε)−f(x)‖2+‖f(x)−y‖2)
这也就是说,目标(7)(的若干倍)可以认为是l(f(x+ε),y)的上界,原始目标不大好优化,所以我们改为优化它的上界。
注意到,目标(7)的两项之中,l(f(x+ε),f(x))衡量了模型本身的平滑程度,跟标签没关系,用无标签数据也可以对它进行优化,这意味着它可以跟带标签的数据一起,构成一个半监督学习流程。
勇敢地算 #
对于目标(7)来说,它的积分结果是:
∫q(ε)[l(f(x+ε),f(x))+l(f(x),y)]dε=l(f(x),y)+∫q(ε)l(f(x+ε),f(x))dε
还是老路子,近似展开ε:
l(f(x+ε),f(x))≈l(f(x),f(x))+∑i,j∂l(F(x),f(x))∂Fi(x)∂fi(x)∂xjεj|F(x)=f(x)+12∑i,j,k∂l(F(x),f(x))∂Fi(x)∂2fi(x)∂xj∂xkεjεk|F(x)=f(x)+12∑i,i′,j,k∂2l(F(x),f(x))∂Fi(x)∂Fi′(x)∂fi(x)∂xj∂fi′(x)∂xkεjεk|F(x)=f(x)
很恐怖?不着急,我们回顾一下,作为loss函数的l,它一般会有如下几个特点:
1、l是光滑的;
2、l(x,x)=0;
3、∂∂xl(x,y)|x=y=0,∂∂yl(x,y)|y=x=0。
这其实就是说l是光滑的,并且在x=y的时候取到极(小)值,且极(小)值为0,这几个特点几乎是所有loss的共性了。基于这几个特点,恐怖的(11)式的前三项就直接为0了,所以最后的积分结果是:
∫q(ε)l(f(x+ε),f(x))dε≈12σ2∑i,i′,j∂2l(F(x),f(x))∂Fi(x)∂Fi′(x)∂fi(x)∂xj∂fi′(x)∂xj|F(x)=f(x)
梯度惩罚 #
看上去依然让人有些心悸,但总比(11)好多了。上式也是一个正则项,其特点是只包含一阶梯度项,而对于特定的损失函数,∂2l(F(x),f(x))∂Fi(x)∂Fi′(x)|F(x)=f(x)可以提前算出来,特别地,对于常见的几个损失函数,当i≠i′时∂2l(F(x),f(x))∂Fi(x)∂Fi′(x)|F(x)=f(x)=0,所以仅需计算i=i′的分量,我们记它为λi(x),那么
∫q(ε)l(f(x+ε),f(x))dε≈12σ2∑iλi(x)‖∇xfi(x)‖2
可见,形式上就是对每个f(x)的每个分量都算一个梯度惩罚项‖∇xfi(x)‖2,然后按λi(x)加权求和。
例如,对于MSE来说,l(f(x),y)=‖f(x)−y‖2,这时候可以算得λi(x)≡2,所以对应的正则项为∑i‖∇xfi(x)‖2;对于KL散度来说,l(f(x),y)=∑iyilogyifi(x),这时候λi(x)=1fi(x),那么对应的正则项为∑ifi(x)‖∇xlogfi(x)‖2。这些结果大家多多少少可以从著名的“花书”《深度学习》中找到类似的,并非新的结果。类似的推导还可以参考文献《Training with noise is equivalent to Tikhonov regularization》。
采样近似 #
当然,虽然能求出只带有一阶梯度的正则项∑iλi(x)‖∇xfi(x)‖2,但事实上这个计算量也不低,因为需要对每个fi(x)都要求梯度,如果输出的分量数太大,这个计算量依然难以承受。
这时候可以考虑的方案是通过采样近似计算:假设q(η)是均值为0、方差为1的分布,那么我们有
∑i‖∇xfi(x)‖2=∑i‖∇xfi(x)‖2=Eηi∼q(η)[‖∑iηi∇xfi(x)‖2]
这样一来,每步我们只需要算∑iηifi(x)的梯度,不需要算多次梯度。q(η)的一个最简单的取法是空间为{−1,1}的均匀分布,也就是ηi等概率地从{−1,1}中选取一个。
对抗训练 #
回顾前面的流程,我们先是介绍了添加随机噪声这一增强泛化性能的手段,然后指出随机加噪声可能太没特异性,所以想着先把积分算出来,才有了后面推导的关于近似展开与梯度惩罚的一些结果。那么换个角度来想,如果我们能想办法更特异性地构造噪声信号,那么也能提高训练效率,增强泛化性能了。
监督对抗 #
有监督的对抗训练,关注的是原始目标(3),优化的目标是让loss尽可能小,所以如果我们要选择更有代表性的噪声,那么应该选择能让loss变得更大的噪声,而
l(f(x+ε),y)≈l(f(x),y)+ε⋅∇xl(f(x),y)
所以让l(f(x+ε),y)尽可能大就意味着ε要跟∇xl(f(x),y)同向,换言之扰动要往梯度上升方向走,即
ε∼∇xl(f(x),y)
这便构成了对抗训练中的FGM方法,之前在《对抗训练浅谈:意义、方法和思考(附Keras实现)》就已经介绍过了。
值得注意的是,在《对抗训练浅谈:意义、方法和思考(附Keras实现)》一文中我们也推导过,对抗训练在一定程度上也等价于往loss里边加入梯度惩罚项‖∇xl(f(x),y)‖2,这又跟前一节的关于噪声积分的结果类似。这表明梯度惩罚应该是通用的能提高模型性能的手段之一。
虚拟对抗 #
在前面我们提到,l(f(x+ε),f(x))这一项不需要标签信号,因此可以用来做无监督学习,并且关于它的展开高斯积分我们得到了梯度惩罚(13)。如果沿着对抗训练的思想,我们不去计算积分,而是去寻找让l(f(x+ε),f(x))尽可能大的扰动噪声,这就构成了“虚拟对抗训练(VAT)”,首次出现在文章《Virtual Adversarial Training: A Regularization Method for Supervised and Semi-Supervised Learning》中。
基于前面对损失函数l的性质的讨论,我们知道l(f(x+ε),f(x))关于ε的一阶梯度为0,所以要算对抗扰动,还必须将它展开到二阶:
l(f(x+ε),f(x))≈l(f(x),f(x))+ε⊤∇xl(f(x),fng(x))+12ε⊤∇2xl(f(x),fng(x))ε=12ε⊤∇2xl(f(x),fng(x))ε
这里用fng(x)表示不需要对里边的x求梯度。这样一来,我们需要解决两个问题:1、如何高效计算Hessian矩阵H=∇2xl(f(x),fng(x));2、如何求单位向量u使得u⊤Hu最大?
事实上,不难证明u的最优解实际上就是“H的最大特征根对应的特征向量”,也称为“H的主特征向量”,而要近似求主特征向量,一个行之有效的方法就是“幂迭代法”:从一个随机向量u0出发,迭代执行ui+1=Hui‖Hui‖。相关推导可以参考《深度学习中的Lipschitz约束:泛化与生成模型》的“主特征根”和“幂迭代”两节。
在幂迭代中,我们发现并不需要知道H具体值,只需要知道Hu的值,这可以通过差分来近似计算:
Hu=∇2xl(f(x),fng(x))u=∇x(u⋅∇xl(f(x),fng(x)))≈∇x(l(f(x+ξu),fng(x))−l(f(x),fng(x))ξ)=1ξ∇xl(f(x+ξu),fng(x))
其中ξ是一个标量常数。根据这个近似结果,我们就可以得到如下的VAT流程:
初始化向量u∼N(0,1)、标量ϵ和ξ;
迭代r次:
u←u‖u‖;
u←∇xl(f(x+ξu),fng(x))
u←u‖u‖;
用l(f(x+ϵu),fng(x))作为loss执行常规梯度下降。
实验表明一般迭代1次就不错了,而如果迭代0次,那么就是本文开头提到的添加高斯噪声。这表明虚拟对抗训练就是通过∇xl(f(x+ξu),fng(x))来提高噪声的“特异性”的。
参考实现 #
关于对抗训练的Keras实现,在《对抗训练浅谈:意义、方法和思考(附Keras实现)》一文中已经给出过,这里笔者给出Keras下虚拟对抗训练的参考实现:
def virtual_adversarial_training(
model, embedding_name, epsilon=1, xi=10, iters=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
) # 所有输入层
model_outputs = K.function(
inputs=inputs,
outputs=model.outputs,
name='model_outputs',
) # 模型输出函数
embedding_gradients = K.function(
inputs=inputs,
outputs=[gradients],
name='embedding_gradients',
) # 模型梯度函数
def l2_normalize(x):
return x / (np.sqrt((x**2).sum()) + 1e-8)
def train_function(inputs): # 重新定义训练函数
outputs = model_outputs(inputs)
inputs = inputs[:2] + outputs + inputs[3:]
delta1, delta2 = 0.0, np.random.randn(*K.int_shape(embeddings))
for _ in range(iters): # 迭代求扰动
delta2 = xi * l2_normalize(delta2)
K.set_value(embeddings, K.eval(embeddings) - delta1 + delta2)
delta1 = delta2
delta2 = embedding_gradients(inputs)[0] # Embedding梯度
delta2 = epsilon * l2_normalize(delta2)
K.set_value(embeddings, K.eval(embeddings) - delta1 + delta2)
outputs = old_train_function(inputs) # 梯度下降
K.set_value(embeddings, K.eval(embeddings) - delta2) # 删除扰动
return outputs
model.train_function = train_function # 覆盖原训练函数
# 写好函数后,启用虚拟对抗训练只需要一行代码
virtual_adversarial_training(model_vat, 'Embedding-Token')
完整的使用脚本请参考:task_sentiment_virtual_adversarial_training.py。大概是将模型建立两次,一个模型通过标注数据正常训练,一个模型通过无标注数据虚拟对抗训练,两者交替执行,请读懂源码后再使用,不要乱套代码。实验任务为情况分类,大约有2万的标注数据,取前200个作为标注样本,剩下的作为无标注数据,VAT和非VAT的表现对比如下(每个实验都重复了三次,取平均):
验证集测试集非VAT88.93%89.34%VAT89.83%90.37%
说明:前面提到fng(x)表示不对x求梯度,不过f自身的参数θ的梯度还是需要求的。但是读懂了上述代码的读者会发现,上述实现中相当于把fng(x)中x,θ的梯度都去掉了,理论上不完全等价于标准的VAT。问题是在Keras中实现标准的VAT有点麻烦,而且计算量会加大,此外实验发现上述“山寨”版本也已经能带来提升了,标准的VAT相对它而言差别是二阶小量的,所以差别不大,上述代码基本满足需求了。
文章小结 #
本文先介绍了添加随机噪声这一常规的正则化手段,然后通过近似展开与积分的过程,推导了它与梯度惩罚之间的联系,并从中引出了可以用于半监督训练的模型平滑损失,接着进一步联系到了监督式的对抗训练和半监督的虚拟对抗训练,最后给出了Keras下虚拟对抗训练的实现和例子。
转载到请包括本文地址:https://spaces.ac.cn/archives/7466
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Jun. 01, 2020). 《泛化性乱弹:从随机噪声、梯度惩罚到虚拟对抗训练 》[Blog post]. Retrieved from https://spaces.ac.cn/archives/7466
@online{kexuefm-7466,
title={泛化性乱弹:从随机噪声、梯度惩罚到虚拟对抗训练},
author={苏剑林},
year={2020},
month={Jun},
url={\url{https://spaces.ac.cn/archives/7466}},
}
December 6th, 2021
想请问一下:(14)式的最右侧部分的范数符号是否应写在求和符号之内呢?
December 6th, 2021
Sorry. Get了