[NLG]《Neural Text Generation: A Practical Guide》
一.前言
在文本生成的问题上,主流方法分为基于模版的方法,基于神经网络的方法和介于二者之间的方法。GAN/VAE/RL在文本生成任务上的研究还在进行中,无论是训练过程还是生成效果问题较大,因此暂时不在个人的关注范围内。
基于模版的方法可控性和可预测性比较强,基于模型的方法灵活性和可表达性比较强。这篇博客重点讨论神经文本生成的相关问题。
二.评估指标
评估指标分为三个方面:perplexity,BLEU/ROUGE(基于ngram)和人类评估指标。
perplexity并不能与下游任务的评估指标建立直接的联系。BLEU/ROUGE是一种近似评估手段,但是并不能获取语言学流畅性和内在一致性。但是,虽然在模型的性能超过基线的时候,并不是一种特别好的评估方式,但是这些指标对于发现特别差的例子很有用,从而可以评估相似的模型。
三.预处理
字符编码,分词方式的选择需要格外注意。
四.训练
整个训练过程的一些好用的经验性方法:
(1)训练前,在一个小的batch上overfitting;
(2)训练中,如果损失函数炸了,降低学习率或者对梯度进行剪裁。如果过拟合,用dropout或者weight decay;优化器使用SGD及其变种的时候,当验证损失函数不再下降的时候,做周期性的学习率退火。
除此之外,一些对超参数设定和优化参数设定比较鲁棒的做法:
(1)batch的bucketing。在tensorflow的一些实现中已经是默认技术;
(2)对于生成任务,数据常常是模型性能的瓶颈;当训练集很小时,正则化相关的技术是性能提升的关键;
(3)当验证损失不下降的时候,一般可以通过学习率退火的方式进一步下降。但是有时候会因为优化器的设置和验证集过小导致损失函数震荡比较厉害,这个时候可以多等几个epoch再退火;
(4)验证集的损失函数和最终的模型表现之间的联系不像我们想象的那么紧密,所以多保存几个checkpoint;
(5)ensemble可以提升性能。做average checkpoint是一种非常简单的方式去近似集成效果;
五.解码
解码这块主要讨论两个问题,分别是如何对输出结果进行有效地诊断和解决解码时的一些常见问题(这些问题从Attention矩阵的角度来看会比较有意思)。其中诊断的策略主要包括两个角度,分别是语言模型的应用和预测/真实之间的相关度量指标。前者通过训练一个语言模型给预测输出打分,后者计算一些浅层的度量指标,具体如下:
(1)预测输出的平均长度和真实输出的平均长度
(2)预测输出的语言模型打分和真实输出的语言模型打分的比值。过低,意味着预测输出质量较差,可能beam search存在bug或者需要增加beam size;过高,打分函数有可能有问题
(3)预测输出和真实输出之间的编辑距离(插入,替换和删除)
有了一些浅层的评估之后,会遇到一些常见的问题,这些问题可能在摘要,翻译和对话生成等任务中经常遇到,针对每个问题有一些一定程度上可以尝试的Trick(能够缓解和彻底解决是两个不同的问题),这里是一个梳理。
(1)OOV问题
具体例子如下:
预测输出:杭州 <UNK> 科技 是 一家 <UNK> 初创公司 。 <EOS>
真实输出:杭州 艾耕 科技 是 一家 人工智能 初创公司 。 <EOS>
现在通行的做法是通过基于字或者子词的方法去分词(sentencepiece/wordpiece/subword),包括BERT的训练,机器翻译等相关任务。围绕Attention机制的一个Trick是Copy机制,多个seq2seq框架都会实现的经典Trick。
初此之外,Luong在2014年的工作《Addressing the Rare Word Problem in Neural Machine Translation》中讨论了其他一些方法。
(2)预测输出过短,存在截断现象
具体例子如下:
预测输出:心情 不 太好 ,<EOS>
真实输出:心情 不 太好 , 下雨 了 。 <EOS>
原因是因为随着预测输出长度的增加(乘性关系),整体的log概率输出会减小,那么这样就会倾向于生成较短的预测输出。如果在损失函数中添加一个语言模型项,这种现象会更加严重。比如在对话系统中,容易生成一些较短的回复。既然是长度的问题,自然可以对长度进行处理。四个方法如下:
(a)Length Normalization。也就是得分score/长度T。
(b)Length Bonus。也就是得分score+beta*长度T,其中beta是一个可学习的参数。
(c)Coverage Penalty。利用Attention矩阵缓解该问题。
(d)显式的设置源端和目标端长度的限制。MAX(S-alpha, (1-beta)*S)<=T<=MAX(S+alpha, (1+beta)*S),其中alpha和beta都是超参数。
一般情况下,读过的一些源码中,对(a)的实现比较常见。(c)的实现更多地用于解决重复问题。(b)和(d)看到的相对较少。
(3)预测输出重复
具体例子如下:
预测输出:我 晕 了 ,真的 。 真的 。 真的 。<EOS>
真实输出:我 晕 了 ,真的 。<EOS>
对该现象的检查可以通过Attention矩阵看到,encoder端已经attend的token被多次attend,至于原因,自己并不了解。当然,从另外一方面,模型训练的有问题也是可能的原因之一。那么,解决的办法也是从Attention矩阵开始,这就是经典的Coverage机制。
至此,Copy和Coverage两大经典Trick都会在OpenNMT-py中有实现。
(4)缺乏多样性
该问题是指在对话生成领域中,输出万能无效回复。比如“我不知道!”,“是啊!”。为了解决这个问题,一个简单的方法是给softmax添加温度参数。在对话生成这个任务上,李纪为的文章有点多。
六.部署
一般而言,可以从两个角度考虑部署优化。分别是:通用优化和运行时解码。
通用优化:模型部署的时候开启GPU和使用一些相对底层的高度优化的矩阵-向量操作,比如LAPACK/MKL等。
运行时解码:为了更好地优化运行时解码,需要首先找到解码的耗时操作在哪里?具体的分析结果可以是时空复杂度O(kn^2t^2),具体分析如下:
一般而言,解码复杂度为O(kn^2t^2),其中k为beam size(不使用batch的时候,运行时和k是线性关系;使用batch时,二者为次线性关系1/k),n为网络隐藏层的节点个数(这里的平方关系不严格,可以简单理解为两个全连接层。不管怎样,精确度量下需要针对特定模型计算复杂度。或者另外一种理解是词典的大小,之所以是n的平方是因为相邻两个token的转移是平方关系。),t为使用attention机制时候的句子的长度(output每个token要和input每个token计算相关性,所以是平方关系)。
搞清楚了解码耗时的原因,就基本定下了可以优化的方法。具体如下:
(1)用一些启发式方法继续给beam search减枝。
(2)在词典大小和解码长度之间做trade off。
(3)batch化。(在实际模型部署中,不仅针对生成模型,其他模型的推断batch化也是最简单最有效的方式)
(4)cache一个graph中上一步的计算。(在训练时,cache技术也有很多应用。比如在之前的博客PyTorch用于大模型训练中就提到了相关技术。)
(5)将更多的计算放在graph中做编译。(具体举例子:在自己之前的一个部署模型中,需要得到每个token的prob的矩阵,vocab大小为21128,batch化下,这个prob矩阵会非常大。得到该矩阵后,需要对prob进行argmax操作,得到最大的prob。一种方式是:在tensorflow的graph中输出prob矩阵,argmax操作放在后处理;另一种方式是:graph直接输出argmax后的结果。显然后者更好一些,可以利用GPU,可以利用编译优化过后的graph,同时减少巨大的参数传递过程。)
七.总结
现在在某些场景下可以实用的较好的seq2seq的模型是Transformer,配合一些经典的Trick,但是仍旧不能避免有时候生成结果很奇怪的问题。这个问题个人认为在短期内不能够很好的解决,但是以推荐辅助的方式来呈现是可以接受的。另外的一个有意思的问题是,预训练语言模型在文本生成任务上的应用,特别是在seq2seq架构下的应用,目前还没有看到特别有趣和有效的结果。
文章地址,如果感兴趣更细节的内容,可以直接读原始论文。