前沿重器[15] | R-Dropout——一次不行就两次

 

前沿重器

栏目主要给大家分享各种大厂、顶会的论文和分享,从中抽取关键精华的部分和大家分享,和大家一起把握前沿技术。具体介绍:仓颉专项:飞机大炮我都会,利器心法我还有

往期回顾

这周想要聊的文章是:R-Drop: Regularized Dropout for Neural Networks,这篇文章的内容其实非常简单,但是挺值得多聊聊展开思考的,所以专门用一篇文章来详细聊聊。

背景

Dropout在现在来看就是一个简单的不能再简单的操作,然后最近其实有一些操作让这个并不起眼的重新进入视野,近期对比学习在NLP应用的重要推手SimCSE(SimCSE: Simple Contrastive Learning of Sentence Embeddings),用dropout来增强样本,而且收益还不小这是让人没想到的,而R-Dropout的出现,可以说让Dropout的外延得到进一步拓展,他的效用被进一步研究到,结合论文的附录和苏神(https://spaces.ac.cn/archives/8496)的讲解,感觉Dropout本身起到的作用逐渐明确。

R-Dropout的原理

既然都聊到了,就说一下它的原理吧。

简单地说,就是模型中加入dropout,训练阶段的预测预测两次,要求两次的结果尽可能接近,这种接近体现在损失函数上。

那么,这个“接近”用的是什么呢?作者用的是KL散度。数学上的KL散度是用来对比两个分布是否相同,其连续型和离散型的公式分别是:

OK,有这个基础,来继续看R-Dropout就更清晰了,我们要让两次预测结果的KL散度尽可能小,那么这部分的损失函数就可以构造出来了:

KL散度本身是不具有自反性的,所以要用第一次预测对第二次的KL散度和第二次预测对第一次预测的KL散度的均值来进行计算。

这部分损失可以加入到整体损失里面作为最终优化的一部分,例如是log loss(当然,其他任务可以用其他的损失):

于是最终的损失函数就是这样的(这里给个alpha做一个调整项):

整套原理就是这样,怎么样,这个理解起来其实并不难吧。

深入思考

但我感觉还需要深入思考两个点,展开聊下:

  • 为什么R-Dropout会有用?

  • 为什么用KL散度?

为什么R-Dropout会有用

其实dropout的本质就是给模型加一些扰动,而R-dropout就是要扰动,更要保证这种扰动对结果尽可能小,毕竟这里还优化了两次预测的KL散度,所以其实这种训练就让模型的稳定性大幅提升。最近是遇到一些问题,一句话改一两个字意思还一样但是结果差距很大,这个r-dropout应该可以缓解这个问题,甚至说解决。

但是注意,这里是稳定性提升,我的感觉是并没有拉高模型本身的上限,甚至可能拉低上限。我们知道模型是存在不稳定性的,同一套数据的不同顺序,参数的不同初始化,不同的dropout都会导致模型效果存在波动,而且这个波动还不小,R-dropout本质上即使控制这种波动对结果的影响,从而保证了稳定性。而有关拉低上限,我的解释是最终的参数估计预测,相比不带有新的loss子项,这应该是一个有偏估计,还是可能一定程度拉低上限的。

为什么用KL散度

KL散度本质上是一个对比分布的函数,这与R-Dropout的初衷一致的,要求两次预测尽可能相同,这里是指完全相同,例如多分类下要求的是所有预测的对应概率也是一致的,相比于交叉熵的只针对最优值的prob,这个对比会更加全面和完整。

上代码

不得不说的是,我感觉R-Dropout论文给的源码并不好,我感觉比较舒服的是苏神版的,我就借着苏神的版本聊一下吧,这里我也用我比较熟悉的THENEWS文本分类数据来讲。(https://github.com/bojone/r-drop/blob/main/tnews.py)

对于R-Dropout的重现,其实就两个关键点:

  • 预测两次。

  • 预测两次的结果构造损失函数。

对于预测两次,作者是在数据加载的位置下了功夫,我的理解和keras的模型架构有关,keras的模型后续compile是直接就对接损失了,重点和pytorch不同,pytorch版本的还是去看原作者的源码吧,pytorch是可以真的分别预测两次,这两次混合然后对接到损失里面去,当然,pytorch也可以用keras的这套代码逻辑,看自己的选择了。回到keras,数据加载部分是这样的:

from bert4keras.snippets import sequence_padding, DataGenerator
class data_generator(DataGenerator):
    """数据生成器
    """
    def __iter__(self, random=False):
        batch_token_ids, batch_segment_ids, batch_labels = [], [], []
        for is_end, (text, label) in self.sample(random):
            token_ids, segment_ids = tokenizer.encode(text, maxlen=maxlen)
            for i in range(2):
                batch_token_ids.append(token_ids)
                batch_segment_ids.append(segment_ids)
                batch_labels.append([label])
            if len(batch_token_ids) == self.batch_size * 2 or is_end:
                batch_token_ids = sequence_padding(batch_token_ids)
                batch_segment_ids = sequence_padding(batch_segment_ids)
                batch_labels = sequence_padding(batch_labels)
                yield [batch_token_ids, batch_segment_ids], batch_labels
                batch_token_ids, batch_segment_ids, batch_labels = [], [], []

也就是拿了数据后,对每个batch,重复构造了一套一模一样的数据,也就是这样一个格式: ,然后灌进去模型内去进行计算,换言之,损失函数要把数组的单数位置和双数位置一联动,这功能就实现了。那么来看损失函数是怎么做的吧。

from keras.losses import kullback_leibler_divergence as kld
def crossentropy_with_rdrop(y_true, y_pred, alpha=4):
    """配合R-Drop的交叉熵损失
    """
    y_true = K.reshape(y_true, K.shape(y_pred)[:-1])
    y_true = K.cast(y_true, 'int32')
    loss1 = K.mean(K.sparse_categorical_crossentropy(y_true, y_pred))
    loss2 = kld(y_pred[::2], y_pred[1::2]) + kld(y_pred[1::2], y_pred[::2])
    return loss1 + K.mean(loss2) / 4 * alpha

损失函数就是这么做的。首先是交叉熵,sparse_categorical_crossentropy快速算完。然后是KL散度,如果是我可能要写循环了,但是作者很灵活地用了python的语法糖,[::2]表示双数位置,[1::2]表示双数位置,具体原因可以自己查哈,很灵活,快速把单数为和双数位的KL散度分别算了出来,最后就是求和了。

小结

不得不说,这的确是一个超简单的trick,日常中可以经常使用,而且效率还不低,非常推荐大家加入到自己的常用baseline里面~

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值