1 引言

各位朋友大家好,欢迎来到月来客栈,我是掌柜空字符。

经过前面六篇文章的介绍,我们已经清楚了BERT的基本原理[1]、如何从零实现BERT[2]、如何基于BERT预训练模型来完成文本分类任务[3]、文本蕴含任务[4]、问答选择任务(SWAG)[5]以及问题回答任务(SQuAD)[6],算是完成了BERT模型第一部分内容(如何在下游任务中运用预训练BERT)的介绍。在接下来的这篇文章中,掌柜将开始就BERT模型的第二部分内容,即如何利用Mask LM和NSP这两个任务来训练BERT模型进行介绍。通常,你既可以通过MLM和NSP任务来从头训练一个BERT模型,当然也可以在开源预训练模型的基础上再次通过MLM和NSP任务来在特定语料中进行追加训练,以使得模型参数更加符合这一场景。

在文章BERT的基本原理[1]中,掌柜已经就MLM和NSP两个任务的原理做了详细的介绍,所以这里就不再赘述。一句话概括,如图1所示MLM就是随机掩盖掉部分Token让模型来预测,而NSP则是同时输入模型两句话让模型判断后一句话是否真的为前一句话的下一句话,最终通过这两个任务来训练BERT中的权重参数。

图 1. MLM和NSP任务网络结构图

 

2 数据预处理

在正式介绍数据预处理之前,我们还是依照老规矩先通过一张图来大致了解一下整个处理流程,以便做到心中有数不会迷路。

图 2. MLM和NSP任务数据集构造流程图

如图2所示便是整个NSP和MLM任务数据集的构建流程。第①②步是根据原始语料来构造NSP任务所需要的输入和标签;第③步则是随机MASK掉部分Token来构造MLM任务的输入,并同时进行padding处理;第④步则是根据第③步处理后的结果来构造MLM任务的标签值,其中[P]表示Padding的含义,这样做的目的是为了忽略那些不需要进行预测的Token在计算损失时的损失值。在大致清楚了整个数据集的构建流程后,我们下面就可以一步一步地来完成数据集的构建了。

同时,为了能够使得整个数据预处理代码具有通用性,同时支持构造不同场景语料下的训练数据集,因此我们需要为每一类不同的数据源定义一个格式化函数来完成标准化的输入。这样即使是换了不同的语料只需要重写一个针对该数据集的格式化函数即可,其余部分的代码都不需要进行改动。

2.1 英文维基百科数据格式化

这里首先以英文维基百科数据wiki2 [7]为例来介绍如何得到格式化后的标准数据。如下所示便是wiki2中的原始文本数据:

在上述示例数据中,每一行都表示一个段落,其由一句话或多句话组成。下面我们需要定义一个函数来对其进行预处理:

在上述代码中,第2-3行用于一次读取所有原始数据,每一行为一个段落;第5-7行用于遍历每一个段落,并进行相应的处理;第6行用于过滤掉段落中只有一个句子的情况,因为后续我们要构造NSP任务所需的数据集所以只有一句话的段落需要去掉;第7行用于将所有字母转换为小写并同时将每句话给分割开;第8行则是将所有的段落给打乱,注意不是句子。

最终,经过read_wiki2函数处理后,我们便能得到一个标准的二维列表,格式形如:

例如上述语料处理后的结果为:

这种格式就是后续代码处理所接受的标准格式。

2.2 中文宋词数据格式化

在介绍完英文数据集的格式化过程后我们再来看一个中文原始数据的格式化过程。如下所示便是我们后续所需要用到的中文宋词数据集:

在上述示例中,每一行表示一首词,句与句之间通过句号进行分割。下面我们同样需要定义一个函数来对其进行预处理并返回指定的标准格式:

在上述代码中,第2-3行用于一次读取所有原始数据,每一行为一首词(段落);第5-9行用于遍历每一个段落,并进行相应的处理;第6-7行用于过滤掉字符乱码的情况;第8-9行用于过滤掉段落中只有一个句子的情况;第10行则是将所有的段落给打乱,注意不是句子。

例如上述语料处理后的结果为:

可以看到, 预处理完成后的结果同上面wiki2数据预处理完后的格式一样。

2.3 构造NSP任务数据

在正式构造NSP任务数据之前,我们需要先定义一个类并对定义相关的类成员变量以方便在其它成员方法中使用,代码如下:

由于后续会有一系列的随机操作,所以上面代码第31行加入了随机状态用于固定随机结果。

紧接着,我们需要定义一个成员函数来封装格式化原始数据集的函数,代码如下:

从上述代码可以看出,该函数的作用就是给出了一个标准化的格式化函数调用方式,可以根据指定的数据集名称返回相应的格式化函数。但是需要注意的是,格式化函数返回的格式需要同read_wiki2()函数返回的样式保持一致。

进一步,我们便可以来定义构造NSP任务数据的处理函数,用来根据给定的连续两句话和对应的段落返回NSP任务中的句子对和标签,具体代码如下:

在上述代码中,第3行用于根据均匀分布产生[0,1)之间的一个随机数作为概率值;第6行则是先从所有段落中随机出一个段落,再从随机出的一个段落中随机出一句话,以此来随机选择下一句话;第8行则是返回构造好的一条NSP任务样本。最后,由于该方法只是功能性的函数没有引用到类中的其它成员,所以通过第1行代码将其申明为了静态方法。

到此,对于NSP任务样本的构造就介绍完了,后续我们只需要调用get_next_sentence_sample()函数即可。

2.4 构造MLM任务数据

为了方便后续构造MLM任务中的数据样本,我们这里需要先定义一个辅助函数,其作用是根据给定的token_ids、候选mask位置以及需要mask的数量来返回被mask后的token_ids和标签label信息,代码如下:

在上述代码中,第1行里token_ids表示经过get_next_sentence_sample()函数处理后的上下句,且已经转换为ids后的结果,candidate_pred_positions表示所有可能被maks掉的候选位置,num_mlm_preds表示根据15%的比例计算出来的需要被mask掉的位置数量;第4-6行为依次遍历每一个候选Token的索引,如果已满足需要被mask的数量则跳出循环;第8-9行则表示将其中80%的Token替换为[MASK](注意,这里其实就是15%里面的80%);第10-14行则是分别保持10%的Token不变以及将另外10%替换为随机Token;第15-16则是对Token进行替换,以及记录下哪些位置上的Token进行了替换;第17-18行则是根据已记录的Token替换信息得到对应的标签信息,其做法便是如果该位置没出现在pred_positions中则表示该位置不是需要被预测的对象,因此在进行损失计算时需要忽略掉这些位置(即为PAD_IDX);而如果其出现在mask的位置,则其标签为原始token_ids对应的id,即正确标签。

例如以下输入:

经过函数replace_masked_tokens()处理后的结果则类似为:

在这之后,我们便可以定义一个函数来构造MLM任务所需要用到的训练数据,代码如下:

在上述代码中,第1行token_ids便是传入的模型输入序列的Token ID(一个样本);第3-6行是用来记录所有可能进行掩盖的Token的索引,并同时排除掉特殊Token;第7行则是将所有候选位置打乱,更利于后续随机抽取;第8行则是用来计算需要进行掩盖的Token的数量,例如原始论文中是15%;第10-11行便是上面介绍到的replace_masked_tokens()功能函数;第12行则是返回最终MLM任务和NSP任务的输入mlm_input_tokens_id和MLM任务的标签mlm_label

2.5 构造整体任务数据

在分别介绍完MLM和NSP两个任务各自的样本构造方法后,下面我们再通过一个方法将两者组合起来便得到了最终整个样本数据的构建,代码如下:

在上述代码中,第1行中的@cache修饰器用于保存或直接载入已预处理完成后的结果,具体原理可以参见文章如何用@修饰器来缓存数据与处理结果?;第4行中的max_len用来记录整个数据集中最长序列的长度,在后续可将其作为padding长度的标准;从第6-7行开始,便是依次遍历每个段落以及段落中的每个句子来构造MLM和NSP任务样本;第8-9行用于构建NSP任务数据样本;第13-18行则是将得到的Token序列转换为token_ids,其中16-17行用于判断序列长度,对于超出部分进行截取。

紧接着,第20-25行则是分别构造segment embedding输入和NSP任务的真实标签;第26-28行则是分别构造得到MLM任务的输入和正确标签;第34行则是将每个构造完成的样本保存到data这个列表中;第35-36行则是返回最终生成的结果。

例如在处理宋词语料时,上述代码便会输出如下类似结果:

如果在构造数据集时不想输出上述结果,只需要将日志等级设置为log_level=logging.INFO即可。

2.6 构造数据集DataLoader

在整个数据预处理结束后,我们便可以构造得到最终模型训练时所需要的DataLoader了。如下代码所示便是训练集、验证集和测试集三部分DataLoader的构建过程:

在上述代码中,第6-7行是根据传入的相关参数来构建一个数据预处理结果的缓存名称,因为不同的参数会处理得到不同的结果,最终缓存后的数据预处理结果将会类似如下所示:

这样在每次载入数据集时如果已经有相应的预处理缓存则直接载入即可。

第8-13行便是用来构造测试集所对应的DataLoader;第14-20行则用于构建训练集所对应的DataLoader,其中如果self.max_sen_lensame,那么在对样本进行padding时会以整个数据中最长样本的长度为标准进行padding,该参数默认情况下为None,即以每个batch中最长的样本为标准进行padding,更多相关内容可以参见文章[3]第2.4节第4步中的介绍;第21-24行则是构造验证集所对应的DataLoader

到此,对于整个BERT模型预训练的数据集就算是构建完成了。

2.7 数据集使用示例

在整个数据集的DataLoader构建完毕后,我们便可以通过如下方式来进行使用:

在上述代码中,第4-10行为wiki2数据集的相关路径,而12-18行则为songci数据集的相关路径,可以根据需要直接进行切换;第20-32行则是其它数据预处理的相关数据。

最后,我们便可通过如下方式来实例化类LoadBertPretrainingDataset并输出相应的结果:

输出结果如下:

3 总结

在这篇文章中,掌柜首先带着各位客官一起回顾了MLM和NSP任务的基本原理;然后站在全局的角度介绍了整个MLM和NSP任务的数据集构建流程,以便于让各位客官在看后续内容时做到心中有数;接着分别介绍了英文维基百科和中文宋词语料的格式化过程,即如何得到一个统一的标准化格式输出以适应后续的数据集构建;最后详细一步步地介绍了NSP和MLM任务数据集的构建过程。在下一篇文章中,掌柜将会接着本篇文章的内容继续如何用编码实现BERT预训练中的NSP和MLM任务、如何完成整个模型的训练以及复用等。

本次内容就到此结束,感谢您的阅读!如果你觉得上述内容对你有所帮助,欢迎点赞、转发、分享三连!若有任何疑问与建议,请在文末进行留言。青山不改,绿水长流,我们月来客栈见

引用

[1]BERT原理与NSL和MLM

[2]BERT模型的分步实现

[3]基于BERT预训练模型的文本分类任务

[4]基于BERT预训练模型的文本蕴含任务

[5]基于BERT预训练模型的SWAG问答任务

[6]基于BERT预训练模型的SQuAD回答任务

[7] wiki2地址 https://s3.amazonaws.com/research.metamind.io/wikitext/wikitext-2-v1.zip

[8] https://docs.python.org/3.6/library/random.html?highlight=random#random.random

[9] 动手深度学习,李沐

[10]https://github.com/google-research/bert/

[11]示例代码:https://github.com/moon-hotel/BertWithPretrained