SimBERTv2来了!融合检索和生成的RoFormer-Sim模型
By 苏剑林 | 2021-06-11 | 119952位读者 |去年我们放出了SimBERT模型,它算是我们开源的比较成功的模型之一,获得了不少读者的认可。简单来说,SimBERT是一个融生成和检索于一体的模型,可以用来作为句向量的一个比较高的baseline,也可以用来实现相似问句的自动生成,可以作为辅助数据扩增工具使用,这一功能是开创性的。
近段时间,我们以RoFormer为基础模型,对SimBERT相关技术进一步整合和优化,最终发布了升级版的RoFormer-Sim模型。
简介 #
RoFormer-Sim是SimBERT的升级版,我们也可以通俗地称之为“SimBERTv2”,而SimBERT则默认是指旧版。从外部看,除了基础架构换成了RoFormer外,RoFormer-Sim跟SimBERT没什么明显差别,事实上它们主要的区别在于训练的细节上,我们可以用两个公式进行对比:
SimBERT=BERT+UniLM+对比学习RoFormer-Sim=RoFormer+UniLM+对比学习+BART+蒸馏
除此之外,RoFormer-Sim用到了更多的训练数据,并且拓展到了一般句式,也就是说,不同于SimBERT仅仅局限于疑问句,RoFormer-Sim可以用来做一般句子的相似句生成,适用场景更大。其他训练细节还包括RoFormer-Sim用了更大的batch_size和maxlen等,这些在后面我们都会进一步介绍。
语料 #
SimBERT和RoFormer-Sim的关键之处,都是在于训练语料的构建。RoFormer-Sim的训练语料包括两部分:1、疑问类型相似句;2、通用类型相似句。对于疑问类相似句,我们还是像SimBERT一样,通过收集百度知道的相似问句,然后通过规则进一步清洗,这部分对我们来说已经很成熟了;对于通用类相似句,我们没有现成的地方可以搜集,于是我们提出了两种方案,一定程度上可以无监督地构建(伪)相似句对。
第一个方案是基于“同一个问题的答案是相似的”思想,假如我们有现成的问答语料,该语料对于同一个问题有多个答案,那么我们可以将每个答案分句,然后用一个现成的相似度函数来比较答案之间的相似度,挑出相似度超过某个阈值的句对作为相似句对使用;
第二个方案则是基于“同一篇章的句子是相似的”思想,它更加简单直接一点,就是将每个篇章分句,然后用一个现成的相似度函数两两计算相似度,挑出相似度超过某个阈值的句对作为相似句对使用,显然该方案的合理性更弱,所以它的阈值也更高。
这里涉及到一个“现成的相似度函数”,我们是直接使用Jaccard相似度的一个变体,换言之只需要一个规则的、字符级别的相似度就好了,语义上的关联,则通过篇章内部的关联以及预训练模型本身的泛化能力来获得。通过第一个方案,我们从几个阅读理解数据集中构建了约450万个(伪)相似句对;通过第二个方案,我们从30多G的平行预料中构建了约470万个(伪)相似句对;而爬取的问句则达到了约3000万个相似句组(一组可以构成多对)。从这个角度看来,问句的数目是远超于一般句式的,所以我们按照1:1的方式从中采样,使得每种句式的样本都均衡。
生成 #
RoFormer-Sim的训练方式跟SimBERT基本一样,如下图所示。稍微不同的是,为了增强模型的生成能力,在构造训练语料的时候,我们还随机地将输入句子的部分token替换为[MASK],这种预训练方法首先由BART提出。而我们跟BART的区别在于:BART是“输入带噪声的句子,输出原句子”,我们是“输入带噪声的句子,输出原句子的一个相似句”,理论上我们的任务还更难。
生成效果没什么特别好的评测指标,我们直接目测一些例子就好:
gen_synonyms(u'广州和深圳哪个好?')
[
'深圳和广州哪个好?',
'广州和深圳哪个好',
'广州和深圳哪个更好?',
'深圳和广州哪个更好?',
'深圳和广州,那个更好?',
'深圳和广州哪个好一些呢?',
'深圳好还是广州好?',
'广州和深圳哪个地方好点?',
'广州好还是深圳好?',
'广州和深圳哪个好一点',
'广州和深圳哪个发展好?',
'深圳好还是广州好',
'深圳和广州哪个城市更好些',
'深圳比广州好吗?',
'到底深圳和广州哪个好?为什么呢?',
'深圳究竟好还是广州好',
'一般是深圳好还是广州好',
'广州和深圳那个发展好点',
'好一点的深圳和广州那边好?',
'深圳比广州好在哪里?'
]
gen_synonyms(u'科学技术是第一生产力。')
[
'科学技术是第一生产力!',
'科学技术是第一生产力',
'一、科学技术是第一生产力。',
'一是科学技术是第一生产力。',
'第一,科学技术是第一生产力。',
'第一生产力是科学技术。',
'因为科学技术是第一生产力。',
'科学技术是第一生产力知。',
'也即科学技术是第一生产力。',
'科学技术是第一生产力吗',
'科技是第一生产力。',
'因此,科学技术是第一生产力。',
'其次,科学技术是第一生产力。',
'科学技术才是第一生产力。',
'科学技术是第一生产力吗?',
'第二,科学技术是第一生产力。',
'所以说科学技术是第一生产力。',
'科学技术确实是第一生产力。',
'科学技术还是第一生产力',
'科学技术是第一生产力对吗?'
]
总的来说,初步实现了任意句式的相似扩增,但问句的扩增效果优于一般句型,这是因为训练语料中问句的质量就明显高于一般句型。由于进行了仿BART式训练,所以除了直接进行相似句生成外,我们还可以自行把某些部分mask掉,让模型自行发散扩充,比如:
gen_synonyms(u'科学技术是第一生产力。', mask_idxs=[6, 7]) # mask掉“第一”
[
"科学技术是第一生产力",
"2、科学技术是第一生产力。",
"科学技术是第一生产力,也是第二生产力。",
"科学技术是第一生产力,科学发展是第二生产力。",
"9、科学技术是第一生产力。",
"第一,科学技术是一种生产力。",
"科学技术是生产力。",
"科学技术是第二生产力。",
"科学技术是第一生产力”现在提出来的。",
"一、科学技术是一种生产力。",
"科学技术是第一生产力是什么意思",
"科学技术是一种主要生产力。",
"一:科学技术是最高生产力。",
"指科学技术不是第一生产力。",
"科学技术是第二生产力,第一生产力又是第二生产力。",
"二、科学技术是一种生产力。",
"世界上第一种生产力是科学技术。",
"科学技术是社会主义生产力之一。",
"第二,科学技术也是第二生产力。",
"科技是一切生产力。"
]
更多玩法,请大家自行挖掘了。
检索 #
增加一般句式的语料、引入仿BART式训练,这些改动都相对来说提升了生成模型的效果。然而,我们意外地发现,检索模型(即句子编码模型)的效果却降低了。估计的原因,可能是更多的语料、更大的噪声虽然加大了生成模型的难度,但对于对比学习来说,这些不同句式的或者带噪声的样本作为负样本,反而是难度降低了。比如,如果一个batch同时有疑问句和陈述句,那么模型可以简单地通过句式(而不是语义)就可以识别出不少负样本,从而降低了对语义的理解能力。
当然,SimBERT和RoFormer-Sim的本质定位都是相似句扩增模型,检索模型只是它的“副产品”,但我们仍然希望这个“副产品”能尽可能好一些。为此,我们在RoFormer-Sim训练完之后,进一步通过蒸馏的方式把SimBERT的检索效果转移到RoFormer-Sim上去,从而使得RoFormer-Sim的检索效果基本持平甚至优于SimBERT。蒸馏的方式很简单,假如对于同一批句子,SimBERT出来的句向量为u1,u2,⋯,un,RoFormer-Sim出来的句向量为v1,v2,⋯,vn,那么就以
Lsim=λn2n∑i=1n∑j=1(cos(ui,uj)−cos(vi,vj))2
为loss进行学习,这里λ=100。当然,为了防止模型“遗忘”掉生成模型,蒸馏的同时还要加上生成损失,即L=Lsim+Lgen。base版的蒸馏不需要很多步,大致5000步左右就可以训练完成了。
跟《无监督语义相似度哪家强?我们做了个比较全面的评测》一样,我们用同样的任务对比了SimBERT和RoFormer的检索效果(其中每一格的三个数据,分别代表“不加whitening”、“加whitening”、“加whitening-256”的效果,同之前的评测):
ATECBQLCQMCPAWSXSTS-BV1-P138.50/23.64/30.7948.54/31.78/40.0176.23/75.05/74.5015.10/18.49/15.6474.14/73.37/75.29V1-P238.93/27.06/30.7949.93/35.38/40.1475.56/73.45/74.3914.52/18.51/15.7473.18/73.43/75.12V1-P336.50/31.32/31.2445.78/29.17/40.9874.42/73.79/73.4315.33/18.39/15.8767.31/70.70/72.00V1-P433.53/29.04/28.7845.28/34.70/39.0073.20/71.22/72.0914.16/17.32/14.3966.98/70.55/71.43V2-P139.52/25.31/31.1050.26/33.47/40.1676.02/74.92/74.5814.37/19.31/14.8174.46/71.00/76.29V2-P239.71/32.60/30.8950.80/37.62/40.1275.83/73.45/74.5213.87/19.50/14.8873.47/74.56/76.40V2-P339.55/24.61/31.8250.25/29.59/41.4374.90/73.95/74.0614.57/18.85/15.2668.89/71.40/73.36V2-P436.02/29.71/29.6148.22/35.02/39.5273.76/71.19/72.6813.60/16.67/13.8668.39/71.04/72.43
从表中可以看出,不管加不加whiteining,RoFormer-Sim在大部分任务上都超过了SimBERT,可见蒸馏之后的RoFormer-Sim的检索效果确实能获得提高,这个“副产品”也不至于太差了。
用同样的方法,我们也搞了个small版的RoFormer-Sim,这时候蒸馏用的是base版的RoFormer-Sim作为teacher模型,但蒸馏的步数需要比较多(50万左右),最终效果如下:
ATECBQLCQMCPAWSXSTS-BV1small-P130.68/27.56/29.0743.41/30.89/39.7874.73/73.21/73.5015.89/17.96/16.7570.54/71.39/72.14V1small-P231.00/29.14/29.1143.76/36.86/39.8474.21/73.14/73.6716.17/18.12/16.8170.10/71.40/72.28V1small-P330.03/21.24/29.3043.72/31.69/40.8172.12/70.27/70.5216.93/21.68/18.7566.55/66.11/69.19V1small-P429.52/28.41/28.5743.52/36.56/40.4970.33/68.75/69.0115.39/21.57/16.3464.73/68.12/68.24V2small-P137.33/23.59/31.3147.90/29.21/42.0774.72/74.94/74.6913.41/15.30/13.6171.48/69.01/75.10V2small-P237.42/31.25/31.1849.15/38.01/41.9875.21/73.47/74.7813.38/15.87/13.6972.06/73.92/75.69V2small-P336.71/30.33/31.2549.73/31.03/42.7474.25/72.72/74.1914.58/18.68/14.4069.12/71.07/72.68V2small-P432.80/27.87/29.6546.80/36.93/41.3172.30/69.94/72.3813.45/16.93/13.3867.21/70.42/71.39
总结 #
本文介绍和发布了我们SimBERT的升级版——RoFormer-Sim(SimBERTv2),它既可以用来扩充相似句子,也是语义相似度问题的一个较高的baseline。相比SimBERT,它最大的特点是将句型拓展到了一般类型,不再局限于相似问句。更多的玩法欢迎读者进一步挖掘和分享~
转载到请包括本文地址:https://spaces.ac.cn/archives/8454
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Jun. 11, 2021). 《SimBERTv2来了!融合检索和生成的RoFormer-Sim模型 》[Blog post]. Retrieved from https://spaces.ac.cn/archives/8454
@online{kexuefm-8454,
title={SimBERTv2来了!融合检索和生成的RoFormer-Sim模型},
author={苏剑林},
year={2021},
month={Jun},
url={\url{https://spaces.ac.cn/archives/8454}},
}
November 26th, 2021
ft是什么意思啊,finetuning么
是
January 9th, 2022
[...]KeyBERT中其实支持选择编码模型的,中文选择了多语言paraphrase-multilingual-MiniLM-L12-v2481M的大模型。既然SBERT编码句向量这个思路得到了验证,在解决中文问题时,可选择的句向量模型就很多了。比如苏神开放的SimBERT,以及进一步微调过的模型RoFormer-Sim,都可以做句向量。最近刚出的CoSENT比Sentence-BERT更有效的句向量方案[...]
March 15th, 2022
苏老师您好,请问`supervised.py`代码里建立加载模型里的
```
# 建立加载模型
encoder = build_transformer_model(
config_path,
checkpoint_path,
model='roformer',
with_pool='linear',
dropout_rate=0.2,
ignore_invalid_weights=True
)
```
里的with_pool='linear'的作用是对CLS位置的编码向量进行pooling操作吗?另外不太懂这里'linear'的作用是什么?
此外,我在尝试用CoSENT去微调RoFormer-sim的时候,发现需要把with_pool='linear'注释掉才能微调通,效果确实比Sentence-BERT收敛的更快,效果也更好。但是这样句向量的维度就是[1, seq_len, hidden_size],和之前加with_pool='linear'的维度[1, hidden_size]不一样了。所以我对with_pool='linear'的作用有点疑惑,请问有什么方法能对注释掉with_pool='linear'后微调得到的[1, seq_len, hidden_size]句向量得到和加with_pool='linear'的维度[1, hidden_size]一样的结果呢?
烦请苏老师指点,不胜感激!
1、with_pool='linear'是指加上cls pooler并使用线性激活函数(不加激活函数);
2、CoSENT不会是把with_pool='linear'注释掉才能跑通,请检查出错时的具体错误,充分理解原理和代码后再跑,在理解之前不要强行运行,以免浪费自己的时间。
April 5th, 2022
苏神,您好,请问用simbertv2做生成的时候,我看github上的参考代码里是单句输入的。是否可以批量输入呢?有没有参考代码呢?
可以批量输入,没有参考代码,需要自行想办法实现。
April 21st, 2022
您好,我想问一下用自己的数据训练stage1.py时,显卡内存太小怎么设置梯度累计呢?试了您之前写的accum_optimizer.py脚本,出现下面的错误,请问该怎么解决呢?
Traceback (most recent call last):
File "/home/workspace/soft/anaconda3_cudnn7/envs/tensorflow1.14/lib/python3.7/site-packages/tensorflow/python/util/nest.py", line 297, in assert_same_structure
expand_composites)
ValueError: The two structures don't have the same nested structure.
First structure: type=IndexedSlices str=IndexedSlices(indices=Tensor("training/AccumOptimizer/gradients/concat_1:0", shape=(?,), dtype=int32), values=Tensor("training/AccumOptimizer/gradients/concat:0", shape=(?, 768), dtype=float32), dense_shape=Tensor("training/AccumOptimizer/gradients/Shape_51:0", shape=(2,), dtype=int32))
Second structure: type=Tensor str=Tensor("training/AccumOptimizer/add:0", shape=(12000, 768), dtype=float32)
More specifically: Substructure "type=IndexedSlices str=IndexedSlices(indices=Tensor("training/AccumOptimizer/gradients/concat_1:0", shape=(?,), dtype=int32), values=Tensor("training/AccumOptimizer/gradients/concat:0", shape=(?, 768), dtype=float32), dense_shape=Tensor("training/AccumOptimizer/gradients/Shape_51:0", shape=(2,), dtype=int32))" is a sequence, while substructure "type=Tensor str=Tensor("training/AccumOptimizer/add:0", shape=(12000, 768), dtype=float32)" is not
我把AccumOptimizer改成了继承from bert4keras.optimizers的Adam,调用时这样写的optimizer = AccumOptimizer(Adam(learning_rate=1e-4), 15),请问还有什么需要改的地方吗?
class AccumOptimizer(Adam):
def __init__(self, optimizer, steps_per_update=1, **kwargs):
super(AccumOptimizer, self).__init__(**kwargs)
self.optimizer = optimizer
with K.name_scope(self.__class__.__name__):
self.steps_per_update = steps_per_update
self.iterations = K.variable(0, dtype='int64', name='iterations')
self.cond = K.equal(self.iterations % self.steps_per_update, 0)
self.learning_rate = self.optimizer.learning_rate
self.optimizer.learning_rate = K.switch(self.cond, self.optimizer.learning_rate, 0.)
参考这里的用法:https://github.com/bojone/bert4keras/blob/master/pretraining/pretraining.py
December 8th, 2022
苏神,数据集还是不方便公开吗?
嗯嗯