自2018年以来,bert以及改造版本对多项NLP任务进行“屠榜”,引起了工业界、学术界的轩然大波。目前,很多算法团队蠢蠢欲动,希望引入SOTA模型,对现有业务进行提升。
不过有点令人遗憾的是,bert支持的最长序列长度为512。处理评论,标题等短文本基本上够用了,但对于较长的文本,比如新闻正文,会经常出现超出字符限制的情况。因此怎么处理好长文本不失为使用BERT的一个小trick。
1.截断法
截断法是非常常用办法,大致分为三种,head截断,tail截断,head+tail 截断。
- head截断即从文本开头直到限制的字数。
- tail截断是从结尾开始往前截断。
- head+tail 截断,开头和结尾各保留一部分,比例参数是一个可以调节超参数。
最长序列字数是512,其中还包括一些特殊token,在文本分类中,要包含开头的[CLS]和结尾的[SEP],因此实际只能最多装510个字。如果一个长文本的重要信息是在开头,可能head截断效果是比tail截断要好。同理,tail截断对信息点在结尾的长文本效果较好。具体哪种截断效果好,不同数据集不一样,需要多试。使用head+tail 截断,一般而言是好于单一的截断方式。
截断法丢失序列信息,显得非常暴力,一般使用在文本不是特别长的场景。如果是篇章级,文本长度好几千,如果直接使用截断法,必然会丢失大量信息。因此面对这种场景,首先想到的是“拆”。
2.Pooling法
将一个整段的文本拆分为多个segment,每一个segment的长度小于510。segment的拆分可以暴力地通过510的大小进行chunk,或者通过断句的方式,将相邻的句子放入一个segment。每一个segment都通过BERT,对得到的[CLS]进行Pooling。可以是用Max-Pooling、Mean-Pooling。亦或将 Max-Pooling、Mean-Pooling进行concat,然后再通过一个FC。如果考虑性能、只能使用一个Pooling的话,就使用Max-Pooling,因为捕获的特征很稀疏、Max-Pooling会保留突出的特征,Mean-Pooling会将特征打平。这一点和TextCNN后接Max-Pooing是一个道理。
Pooling法将所有序列都放入模型之中。考虑到了全局的信息,对文本很长且截断敏感的任务有较好的效果。但有一些缺点
- 性能较差,原来截断法需要encode一次,Pooling法需要encode多次,篇章越长,速度越慢。
- segment之间的联系丢失,可能会出badcase。
3.压缩法
压缩法的宗旨是选取“精华”,去除“糟粕”。断句之后整个篇章分割成segment,通过规则或者训练一个小模型,将无意义的segment进行剔除。
举个例子,最近笔者做了一个长篇章文本分类的任务,数据是自媒体自己创作发布的文章。一些作者,为了凑字数,或者进行引流会写一些冗余的话术,比如在结尾会说道:“不知道各位看官,对小编的创作是否感兴趣,如果感兴趣的话,希望点个赞”等。通过统计,该任务与主题相关的句子,不到总字数的60%,40%的时间都是在浪费在这些"糟粕"上,同时这些冗余文本也会带来一些噪声,不利于模型的学习。
因此在某一些场景,可以尝试对原始文本进行过滤,降低有效文本的大小。关键点是如何筛选出有效句子,然而筛选方法因任务而定,没有统一的方法。笔者曾经使用一个非常暴力压缩篇章的trick:让正文的句子和标题做字符相似度,只选取字符相似度最大的TOP-K句子来代表这个篇章。在F1的降低的可接受的范围内,让线上的QPS提升了数倍,节省的都是白花花的钱。
如果压缩之后,大部分的句子还是超过510,继续使用截断法或者Pooling法。
4.总结
本文只是笔者对使用bert进行篇章级长文本处理的一些经验和心得。具体使用什么方法,还要看数据和问题,不能一概而论。如果是工业级的实战,还要考虑性能和经济性。要是做出来的模型效果好,但因性能差上不了线,岂不是闹心。最后祝大家能在修炼的道路上越走越远。^_^