项目小结GEC模型训练&评估TRICK——自定义损失函数&预校正模型(autocorrect包)

Posted 囚生CY

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了项目小结GEC模型训练&评估TRICK——自定义损失函数&预校正模型(autocorrect包)相关的知识,希望对你有一定的参考价值。

序言

关于GEC问题的概况可以参考笔者之前的几篇博客:

【项目小结】英语语法错误检测(GEC)开题论文阅读记录

【论文阅读】D19-1435——GEC问题解决的一种方法:PIE架构

【论文阅读】D18-1274——GEC模型优化的一种方法:使用质量评估模型

【论文阅读】W19-4423——预训练与迁移学习在GEC的应用

【项目小结】GEC模型中的难点:分词(Tokenizer)与回译(Backtranslation)(本文提供了Lang8平行语料)

最近一两周的时间,笔者的用游戏本马不停蹄地训练自己的GEC模型,调参调架构跑了几十种模型,实验结果表明瓶颈始终是卡在召回率上,实验中即便使用Nucle+Lang8共计110W平行语料训练的模型,不管利用是GRULSTM还是Transformer训练的效果竟然都无法突破2%的召回率,即模型几乎无法捕捉到语句序列中存在语法错误的确切位置。

事实上召回率本身也是GEC问题的难点,很多时候如果基于知道某个单词存在错误,如何去校正是较为容易的,无论是使用语言模型或者是分类模型都是较为直观的方法,在https://nlpprogress.com/english/grammatical_error_correction.html中目前很多学者对于CoNLL2014的后续研究中得到的先进GEC模型都普遍表现出召回率比准确率低20%的水平的现象。

笔者经过一段时间的研究,在这里提出一些能够提升GEC模型训练的Trick以及自己对GEC模型训练的一些理解。

预校正模型

其实GEC模型属于广义上的seq2seq模型,但是与神经机器翻译、文本摘要等模型有很大的区别。目前笔者认为GEC模型最大的一个Trick就是模型的输出可以继续输入进模型得到输出。常规的seq2seq模型输入与输出所属的集合或特征并不相同,神经机器翻译输入与输出隶属于不同的词汇表,文本摘要输入输出具有不同特征,但是GEC模型的输入与输出分别是可能存在语法错误的语句序列与校正后的语句序列,事实上校正后的语句序列仍然可能存在语法错误,因此可以继续输入到模型中得到输出。从算法角度上来说可以不断将模型输出输入到模型中,直到输出收敛为止。

基于这个特征笔者认为存在两个可以参考的想法,首先基于GEC模型存在“可迭代性iterable”的特征,可以将深度学习网络架构“复制增强”,即在架构模型时使用多层相同或类似的网络结构迭代出现,在模型训练时体现iterable这种特征,很多时候限于计算资源,并不能使得模型架构达到很深的程度。往往可以训练出多个相对简单的模型,然后进行纵向集成。

这里介绍python自带的autocorrect包,可以自动进行单词级别与语句级别的拼写纠错校正,这本身就可以作为一个预校正模型,笔者在Nucle测试集上测试下来的结果,准确率可以达到10%,召回率大约是1%,虽然评估效果很差,但是可以预先将单词拼写错误这种难以通过神经模型学习出的问题给预先过滤掉,从而提升模型的效果。本身来说autocorrect仅是单词级别的统计校正方法,在运行速度上有很大优势,处理速度大约是100句/秒,完全可以作为一个合格的额外的预校正模型使用。

autocorrect包使用示例:

from autocorrect import Speller

correct = Speller()
sentence = correct.autocorrect_sentence("This is a worng sentense .")
word = correct.autocorrect_word("Inteligence")

print(sentence) # This is a wrong sentence .
print(word) # Intelligence

 

这里其实就是一个很Trick的事情,因为GEC模型的集成增强的方式与一般的模型集成(如bagging投票,boosting学习残差)不同,它可以直接把多个GEC模型纵向拼接起来,上一个模型输出与下一个模型输入对接,然后就可以达到模型增强的效果,理论上如果获得所有前人的先进的GEC模型,将它们拼接起来就可以达到更好的效果。

 

自定义损失函数

召回率奇低,这个问题困扰了我很长时间,笔者使用的GEC架构是基于seq2edit实现的,即模型的实际输出是如何修正当前位置的单词,即是删除delete/插入insert/替换replace/不修正equal,为什么召回率很低?因为GEC问题确切地说seq2seq问题里面的序列局部修正LSTLocal Sequence Transit)分支,即模型的输入与输出差异很小,只是在少数几个位置发生改变。因此实际输出中equal所占的比例会非常高,最终模型就会倾向于输出equal,因此模型的召回率就会非常差。

如何改善这个问题?直到今天笔者才想到可以修正损失函数来解决,之前一直困死在觉得是数据量不够,于是疯狂增强数据,花大量的时间跑完所有数据集,结果仍然提升的很小很小。以使用多分类的交叉熵损失函数(categorcial crossentropy loss)为例,可以手动修正,笔者使用keras+tensorflow搭建深度学习网络,查看categorcial_crossentropy损失函数的源码可以看到:

E:\\Python\\Lib\\site-packages\\keras\\losses.py   Line68~69

...

def categorical_crossentropy(y_true, y_pred):
    return K.categorical_crossentropy(y_true, y_pred)

...

 E:\\Python\\Lib\\site-packages\\keras\\backend\\tensorflow_backend.py Line3258~3298

...

def categorical_crossentropy(target, output, from_logits=False, axis=-1):
    """Categorical crossentropy between an output tensor and a target tensor.

    # Arguments
        target: A tensor of the same shape as `output`.
        output: A tensor resulting from a softmax
            (unless `from_logits` is True, in which
            case `output` is expected to be the logits).
        from_logits: Boolean, whether `output` is the
            result of a softmax, or is a tensor of logits.
        axis: Int specifying the channels axis. `axis=-1`
            corresponds to data format `channels_last`,
            and `axis=1` corresponds to data format
            `channels_first`.

    # Returns
        Output tensor.

    # Raises
        ValueError: if `axis` is neither -1 nor one of
            the axes of `output`.
    """
    output_dimensions = list(range(len(output.get_shape())))
    if axis != -1 and axis not in output_dimensions:
        raise ValueError(
            ''.format(
                'Unexpected channels axis . '.format(axis),
                'Expected to be -1 or one of the axes of `output`, ',
                'which has  dimensions.'.format(len(output.get_shape()))))
    # Note: tf.nn.softmax_cross_entropy_with_logits
    # expects logits, Keras expects probabilities.
    if not from_logits:
        # scale preds so that the class probas of each sample sum to 1
        output /= tf.reduce_sum(output, axis, True)
        # manual computation of crossentropy
        _epsilon = _to_tensor(epsilon(), output.dtype.base_dtype)
        output = tf.clip_by_value(output, _epsilon, 1. - _epsilon)
        return - tf.reduce_sum(target * tf.log(output), axis)
    else:
        return tf.nn.softmax_cross_entropy_with_logits(labels=target,
                                                       logits=output)

...

比如模型输入的维度是(?, 100, 2015)?即为可能的batch_size100为语句长度,2015即为所有常用的编辑操作方式数量(如equaldeleteinsert(the),insert(a),...,replace(is),replace(takes),...),我们将equal类的输出放在输出的2015个维度的第一个位置,为了提升模型的召回率,需要使得模型不倾向于输出equal,所以需要降低交叉熵中对应equal位置的权重,修改上述源码得到weighted_categorical_crossentropy函数即可👇

import tensorflow as tf

def weighted_categorical_crossentropy(y_true,y_pred,axis=-1):			 # 带权重的交叉熵损失函数
	y_pred /= tf.reduce_sum(y_pred,axis,True)							 # 标准化
	_epsilon = tf.convert_to_tensor(1e-7,dtype=y_pred.dtype.base_dtype)
	y_pred = tf.clip_by_value(y_pred,_epsilon,1.-_epsilon)				 #
	return -1e-6*tf.reduce_sum(y_true[:,:,:1]*tf.log(y_pred[:,:,:1]),axis)-tf.reduce_sum(y_true[:,:,1:]*tf.log(y_pred[:,:,1:]),axis)	 # 
		

如上所示,笔者设置equal对应位置的交叉熵损失值权重仅为1e-6,至少在这样的配置下,目前笔者取得了远远优于之前的损失函数的效果。

后记

目前毕设基本走到收尾工作了,限于计算资源与指导资源的缺失,走到这一步也基本差强人意,现在还在继续优化模型争取能突破30%的P/R值就算是非常完美的结局了。总归这次毕设写得代码让我很有收获,一次深入的实践确实对熟练度的磨练有很大的帮助,而且这次代码写的也算是我独立开发以来做得最满足我强迫症的一次项目了,最终大约是近4000行的代码量。每次做完一件事后,总是忽然觉得有很多事情要继续跟进,现在又忽然想pytorch里会不会也有很多有趣的东西可以挖掘。

今天这篇博客是下午跟微软开workshop视频会议时抽空写的,公司请了微软的老师给开发人员讲Azure平台的使用,看到微软开发的Azure平台上内置了很多模型训练架构与并发调优的接口,包括从未接触过的模型架构方法如AutoML,关于AutoML的论文大概是去年发表出来的,当时也算是比较轰动了,因为模型可以自己学习如何“搭积木”,省去了人的经验调优,而且别人的计算资源几分钟就可以出一个很复杂的模型,真的是羡慕不来的了。

且行且珍惜吧,去年推免之后总觉得自己会有大把的时间做自己的事情,结果一个一直很想做的WEB项目刚起手没多久就给搁置了,之后就遇到很多事情。今年还被疫情恶心了一遭,加上自己在家里也是怠惰得不行,如今被实习跟一专和二专业毕业论文卡得无所适从。计划永远赶不上变化,带我的项目经理ZhaoKezhen也就比我大了4岁,人总是到了年纪才发现剩下的时间真的不多了。

以上是关于项目小结GEC模型训练&评估TRICK——自定义损失函数&预校正模型(autocorrect包)的主要内容,如果未能解决你的问题,请参考以下文章

交叉验证(Cross Validation)原理小结

项目小结英语语法错误检测(GEC)开题论文阅读记录

交叉验证(Cross Validation)原理小结

论文阅读W19-4423——预训练与迁移学习在GEC的应用

模型评价指标(CTR)

:模型训练和预测的三种方法(fit&tf.GradientTape&train_step&tf.data)