导读
预训练语言模型在NLP的各个应用中都有及其广泛的应用;然而,经典的预训练语言模型(例如BERT)缺乏对知识的理解,例如知识图谱中的关系三元组。知识增强预训练模型使用外部知识(知识图谱,字典和文本等)或者句子内部的语言学知识进行增强。我们发现,知识注入的过程都伴随着很大规模的知识参数,下游任务fine-tune的时候仍然需要外部数据的支撑才能达到比较好的效果,从而无法在云环境中很好的提供给用户进行使用。CKBERT(Chinese Knowledge-enhanced BERT)是EasyNLP团队自研的中文预训练模型,结合了两种知识类型(外部知识图谱,内部语言学知识)对模型进行知识注入,同时使得知识注入的方式方便模型可扩展。我们的实验验证也表明CKBERT的模型精度超越了多种经典中文模型。在本次的框架升级中,我们将多种规模的CKBERT模型贡献给开源社区,并且这些CKBERT模型与HuggingFace Models完全兼容。此外,用户也可以在阿里云机器学习平台PAI上方便地利用云资源使用CKBERT模型。
EasyNLP(https://github.com/alibaba/EasyNLP)是阿⾥云机器学习PAI 团队基于 PyTorch 开发的易⽤且丰富的中⽂NLP算法框架,⽀持常⽤的中⽂预训练模型和⼤模型落地技术,并且提供了从训练到部署的⼀站式 NLP 开发体验。EasyNLP 提供了简洁的接⼝供⽤户开发 NLP 模型,包括NLP应⽤ AppZoo 和预训练 ModelZoo,同时提供技术帮助⽤户⾼效的落地超⼤预训练模型到业务。由于跨模态理解需求的不断增加,EasyNLP也⽀持各种跨模态模型,特别是中⽂领域的跨模态模型,推向开源社区,希望能够服务更多的 NLP 和多模态算法开发者和研 究者,也希望和社区⼀起推动 NLP /多模态技术的发展和模型落地。
本⽂简要介绍CKBERT的技术解读,以及如何在EasyNLP框架、HuggingFace Models和阿里云机器学习平台PAI上使⽤CKBERT模型。
中文预训练语言模型概览
在这一节中,我们首先简要回顾经典的中文预训练语言模型。目前中文预训练语言模型主要包括了两种类型:
- 通用领域的预训练语言模型,主要包括了BERT、MacBERT和PERT等模型;
- 知识增强的中文预训练模型,主要包括了ERNIE-baidu,Lattice-BERT,K-BERT和ERNIE-THU等模型。
通用领域的预训练语言模型
BERT直接使用Google发布的基于中文维基文本语料进行训练的模型。MacBERT是BERT的改进版本,引入了纠错型掩码语言模型(MLM as correction,Mac)预训练任务,缓解了“预训练-下游任务”不一致的问题。在掩码语言模型(MLM)中,引入了[MASK]标记进行掩码,但[MASK]标记并不会出现在下游任务中。在MacBERT中,使用相似词来取代[MASK]标记。相似词通过Synonyms toolkit 工具获取,算法基于word2vec相似度计算。同时,MacBERT也引入了Whole Word Masking和N-gram Masking技术。当要对N-gram进行掩码时,会对N-gram里的每个词分别查找相似词;当没有相似词可替换时,将使用随机词进行替换。由于一定程度的乱序文本不影响语义理解,PBERT从乱序文本中学习语义知识。它对原始输入文本进行一定的词序调换,从而形成乱序文本(因此不会引入额外的[MASK]标记),其学习目标是预测原Token所在的位置。
知识增强的中文预训练模型
BERT在预训练过程中使用的数据仅是对单个字符进行屏蔽,例如下图所示,训练BERT时通过“哈”与“滨”的局部共现判断出“尔”字,但是模型其实并没有学习到与“哈尔滨”相关的知识,即只是学习到“哈尔滨”这个词,但是并不知道“哈尔滨”所代表的含义。ERNIE-Baidu在预训练时使用的数据是对整个词进行屏蔽,从而学习词与实体的表达,例如屏蔽“哈尔滨”与“冰雪”这样的词,使模型能够建模出“哈尔滨”与“黑龙江”的关系,学到“哈尔滨”是“黑龙江”的省会以及“哈尔滨”是个冰雪城市这样的含义。
与ERNIE-Baidu类似,Lattice-BERT利用Word-Lattice结构整合词级别信息。具体来说,Lattice-BERT设计了一个Lattice位置注意机制,来表达词级别的信息,同时提出了Masked Segment Prediction的预测任务,以推动模型学习来自丰富但冗余的内在Lattice信息。
除了语言学知识,更多的工作利用知识图谱中的事实性知识丰富中文预训练模型的表征。其中,K-BERT提出了面向知识图谱的知识增强语言模型,将三元组作为领域知识注入到句子中。然而,过多的知识融入会导致知识噪音,使句子偏离其正确的含义。为了克服知识噪音, K-BERT引入了Soft-position和Visibel Matrix来限制知识的影响。由于K-BERT能够从预训练的BERT中加载模型参数,因此通过配备KG,可以很容易地将领域知识注入到模型中,而不需要对模型进行预训练。EasyNLP框架也集成了K-BERT的模型和功能(看这里)。
ERNIE-THU是一种融入知识Embedding的预训练模型。它首先使用TAGME提取文本中的实体,并将这些实体链指到KG中的对应实体对象,然后获得这些实体对象对应的Embedding。实体对象的Embedding由知识表示方法(例如TransE)训练得到。此外,ERNIE-THU在BERT模型的基础上进行改进,除了MLM、NSP任务外,重新添加了一个和KG相关的预训练目标:Mask掉Token和Entity (实体) 的对齐关系,并要求模型从图谱的实体中选择合适的Entity完成对齐。
自研CKBERT模型技术详解
由于当前的知识增强预训练模型大都使用外部知识(知识图谱,字典和文本等)或者句子内部的语言学知识进行增强,同时知识注入的过程都伴随着很大规模的知识参数,下游任务fine-tune的时候仍然需要外部数据的支撑才能达到比较好的效果,从而无法在云环境中很好的提供给用户进行使用。CKBERT(Chinese Knowledge-enhanced BERT)是EasyNLP团队自研的中文预训练模型,结合了两种知识类型(外部知识图谱,内部语言学知识)对模型进行知识注入,同时使得知识注入的方式方便模型可扩展。针对实际的业务需求,我们提供了三种不同规模参数量的模型,详细配置如下所示:
模型配置 | alibaba-pai/pai-ckbert-base-zh | alibaba-pai/pai-ckbert-large-zh | alibaba-pai/pai-ckbert-huge-zh |
参数量(Parameters) | 151M | 428M | 1.3B |
层数(Number of Layers) | 12 | 24 | 24 |
注意力头数(Attention Heads) | 12 | 16 | 8 |
隐向量维度(Hidden Size) | 768 | 1024 | 2048 |
文本长度(Text Length) | 128 | 128 | 128 |
FFN 层维度 | 3072 | 4096 | 8192 |
CKBERT的模型架构如下图所示:
为了方便模型进行扩展参数,模型只在数据输入层面和预训练任务层面进行了改动,没有对模型架构进行改动。因此,CKBERT的模型结构与社区版的BERT模型对齐。在数据输入层,一共要处理两部分的知识,外部图谱三元组和句子级内部的语言学知识。针对语言学知识,我们使用了哈工大LTP平台进行句子数据的处理,进行语义角色标注和依存句法分析等,然后根据规则,将识别结果中重要的成分进行标注。针对外部三元组知识是根据句子中出现的实体构造实体的正负三元组样本,正样本是根据图谱中1-hop 实体进行的采样,负样本是根据图谱中multi-hop进行的采样,但负样本的采样过程只能在规定的多跳范围内,而不能在图谱中距离太远。
CKBERT采用两种预训练任务进行模型的预训练,语言学感知的掩码语言模型和多跳知识对比学习:
- 语言学感知的掩码语言模型(Linguistic-aware MLM):在语义依存关系中的主体角色(施事者AGT和当事者EXP )部分用[MASK]进行遮掩,同时在词的前后都加上[SDP][/SDP],附加上词汇的边界信息。在依存句法关系中,将主谓冰关系,定中关系,并列关系等按照上述mask机制进行处理为[DEP][/DEP]。整体进行预训练的token数量是整句话的15%,其中40%进行随机MASK,30%和30%分配到语义依存关系和依存句法关系词汇上来。损失函数如下:
- 多跳知识对比学习:将上述构造的正负样本数据针对该注入的实体进行处理,每一个句中实体构造1个正样本,4个负样本,通过标准的infoNCE损失任务进行外部知识的学习。损失函数如下:
其中,
是预训练模型产生的上下文实体表示,
表示正样本的三元组表示结果,
表示负样本的三元组表示结果。
CKBERT模型的实现
在EasyNLP框架中,我们的模型实现分为三个部分:数据预处理,模型结构微调和损失函数的设计。首先,在数据预处理环节,主要由以下两个步骤组成:1.NER实体及语义关系的提取;2.知识图谱的信息注入。关于NER实体及语义信息的提取,主要采用LTP(Language Technology Platform)对原始句子进行分词和句法分析,该部分的核心代码如下所示:
def ltp_process(ltp: LTP,
data: List[Dict[str, Union[str, List[Union[int, str]]]]]):
"""use ltp to process the data
Args:
Dict ([str, str]): data
example:
{'text':['我叫汤姆去拿伞。'],...}
Returns:
Dict[str, str]: result
"""
new_data = list(map(lambda x:x['text'][0].replace(" ", ""), data))
seg, hiddens = ltp.seg(new_data)
result = {}
result['seg'] = seg
result['ner'] = ltp.ner(hiddens)
result['dep'] = ltp.dep(hiddens)
result['sdp'] = ltp.sdp(hiddens)
for index in range(len(data)):
data[index]['text'][0] = data[index]['text'][0].replace(" ", "")
data[index]['seg'] = result['seg'][index]
data[index]['ner'] = result['ner'][index]
data[index]['dep'] = result['dep'][index]
data[index]['sdp'] = result['sdp'][index]
该部分完成之后需要基于原始句子中的语义依存关系对相应的词进行整体的mask,该部分的mask策略参考BERT的mask策略的设计,给不同类型的关系分配特定的概率,并基于该概率对不同类型关系进行mask,该部分的核心代码如下:
def dep_sdp_mask(left_numbers: List[int],
data_new: List[List[Union[int, str]]],
markers_: List[List[int]],
selected_numbers_: set,
number_: int,
marker_attrs: Dict[str, List[int]]) -> int:
""" mask the `mask_labels` for sdp and dep and record the maskers for each mask item
Args:
left_numbers (List[int]): the options that have not been used
data_new (List[List[Union[int, str]]]): preprocessed data for original dep and sdp
markers_ (List[List[int]]): a list that is uesd to save the maskers for each mask item
selected_numbers_ (set): a set that is used to save the selected options
number_ (int): the number of mask labels
marker_attrs Dict[str, List[int]]: marker attributes
Returns:
int: 0 mean no mask, the others mean the number of masked ids
"""
np.random.shuffle(left_numbers)
for item_ in left_numbers:
target_item = data_new[item_]
seg_ids = np.array(target_item[:2]) - 1
delete_ids = np.where(seg_ids < 1)[0]
seg_ids = np.delete(seg_ids, delete_ids)
temp_ids = seg2id(seg_ids)
ids = []
for item in temp_ids:
ids += item.copy()
if check_ids(ids):
length_ = len(ids)
if number_ > length_:
for id_ in ids:
mask_labels[id_] = 1
if target_item[2] in marker_attrs:
detail_info.append([
target_item,
[seg_data[target_item[0] - 1],seg_data[target_item[1] - 1]],
])