Keras实现两个优化器:Lookahead和LazyOptimizer
By 苏剑林 | 2019-07-30 | 45939位读者 |最近用Keras实现了两个优化器,也算是有点实现技巧,遂放在一起写篇文章简介一下(如果只有一个的话我就不写了)。这两个优化器的名字都挺有意思的,一个是look ahead(往前看?),一个是lazy(偷懒?),难道是两个完全不同的优化思路么?非也非也~只能说发明者们起名字太有创意了。
Lookahead #
首先登场的是Lookahead优化器,它源于论文《Lookahead Optimizer: k steps forward, 1 step back》,是最近才提出来的优化器,有意思的是大牛Hinton和Adam的作者之一Jimmy Ba也出现在了论文作者列表当中,有这两个大神加持,这个优化器的出现便吸引了不少目光。
Lookahead的思路很朴素,准确来说它并不是一个优化器,而是一个使用现有优化器的方案。简单来说它就是下面三个步骤的循环执行:
1、备份模型现有的权重$\theta$;
2、从$\theta$出发,用指定优化器更新$k$步,得到新权重$\tilde{\theta}$;
3、更新模型权重为$\theta \leftarrow \theta + \alpha\left(\tilde{\theta} - \theta\right)$。
下面则是我的Keras实现,写法在之前的《“让Keras更酷一些!”:小众的自定义优化器》一文中就提到过了,属于一种“侵入式”的写法:
用法就很简单了:
model.compile(optimizer=Adam(1e-3), loss='mse') # 用你想用的优化器
lookahead = Lookahead(k=5, alpha=0.5) # 初始化Lookahead
lookahead.inject(model) # 插入到模型中
至于效果,原论文中做了不少实验,有些有轻微提高(cifar10和cifar100那两个),有些提升还比较明显(LSTM做语言模型那个),我自己简单实验了一下,结果是没什么变化。我一直觉得优化器是一个很玄乎的存在,有时候非得SGD才能达到最优效果,有时候又非得Adam才能收敛得下去,总之不能指望单靠换一个优化器就能大幅度提升模型效果。Lookahead的出现,也就是让我们多一种选择罢了,训练时间充足的读者,多去尝试一下就好。
LazyOptimizer #
LazyOptimizer优化器基本上就是为NLP准备的,当然更准确来说是为Embedding层准备的。
LazyOptimizer指出所有带动量的优化器(自然也就包括Adam以及带动量的SGD)都存在一个问题:当前batch中没被采样到的词,依然会使用历史动量来更新,这可能导致Embedding层过拟合(参考知乎讨论)。具体来说,当一个词的被采样过后,它的Embedding的梯度不为0,这个梯度也会被记录在动量中,实际更新是用动量去更新的;在后面的batch中,假如该词没有被采样到,它的Embedding的梯度为0,但是它的动量并不为0,所以该词还是被更新了。这样一来就算没有被反复采样的词,对应的Embedding也被反复更新了,就导致了过拟合。
所以,一个改进的方案是只有当该词被采样过才更新,这就是LazyOptimizer的基本原理了。
在实现上,我们要如何判断一个词有没有被采样过呢?当然终极方法肯定是传入被采样过的词的index了,但这使用上不够友好。我这里使用了一个近似的方法:判断该词的Embedding对应的梯度是否为0,如果为0意味着它“很可能”在当前batch没有被采样到。背后的原理在于,如果它没有被采样到,那么梯度一定为0,如果它被采样到了,那么梯度为0的概率是非常小的,毕竟那么多分量,同时为0的可能性很小,所以这样实现也够用了。
我的Keras实现位于:
这个用法也很简单,就是包装一个带动量的优化器,传入所有Embedding层,使得它成为一个新的Lazy版的优化器:
model.compile(
loss='mse',
optimizer=LazyOptimizer(Adam(1e-3), embedding_layers)
)
Github中还带有一个IMDB的例子,在这个例子中,如果直接用Adam(1e-3)
做优化器,那么验证准确率最高只有83.7%左右,而如果用LazyOptimizer(Adam(1e-3), embedding_layers)
,那么基本上最优验证准确率能跑到84.9%以上,效果可见一斑。总的来说,我觉得Embedding层很大的模型(尤其是以词为单位的模型)都可以试一下,总的来说就是因为Embedding层参数量太大了,减少更新频率,让模型重点去优化其余部分。
注:这个LazyOptimizer和标准的LazyOptimizer有点不一样。标准的LazyOptimizer对没有采样过的词,所有相关的缓存量(比如动量等)也不去更新,但我这个实现中,就算该词没有被采样到,该词对应的所有缓存量还是会被更新的,有评测说这样做其实效果会更好些。
文末小结 #
也没啥内容,就用Keras实现了两个优化器,让用Keras的朋友可以及时尝尝鲜,或者用Keras用得更有意思一些。
转载到请包括本文地址:https://kexue.fm/archives/6869
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Jul. 30, 2019). 《Keras实现两个优化器:Lookahead和LazyOptimizer 》[Blog post]. Retrieved from https://kexue.fm/archives/6869
@online{kexuefm-6869,
title={Keras实现两个优化器:Lookahead和LazyOptimizer},
author={苏剑林},
year={2019},
month={Jul},
url={\url{https://kexue.fm/archives/6869}},
}
August 8th, 2019
LazyOptimizer这个在阿里CTR的论文DIN里见到了类似的问题。背景是对高维稀疏的id特征做embedding的时候,如果不对embedding加正则(比如L2),整个模型无法收敛。但是增加正则以后,所有的index不管在当前batch出现过,梯度都不为0。这个只能传入被采样过的词的index了吧?
办法总比困难多。比如你说的这种情况,可以直接把权重衰减写到优化器里边去,而不是在loss那里加l2,而且AdamW不是也表明直接权重衰减要比loss里加l2更合理了吗?
这个衰减是只针对Embedding层的,不是所有参数。
好的,我找个时间试一下
我知道啊,lazy optimizer的实现里不是已经传入了Embedding层了吗?可以只针对Embedding层加衰减呀。
September 5th, 2019
What do you mean "有时候非得SGD才能达到最优效果"? Do you mean that we can't use only one optimizer? Is it true that first train the model with Adam for several epochs and then change the optimizer to SGD for the final polishing? Could you give me an example to explain it? Many thanks
这类工作有很多~SWATS、AdaBound、RAdam这三个优化器,都算是慢慢从Adam切换到SGD的优化器。