bert 中文 代码 谷歌_Bert 预训练小结

最近正在预训练一个中文pytorch版本的bert,模型部分代码是基于huggingface发布的版本,预训练过程还是参考google的代码。

阅读这篇文章之前,希望读者能对BERT有所了解,建议仔细阅读论文。

值得吐槽的是huggingface之前发布的分支名叫pytorch-pretrain-bert,后来又加上了gpt2、XLNet一系列当红模型,结果索性改了个名字叫做pytorch-transformers。

下面是正文部分,整个正文我按照数据生成、预训练、finetune以及其他闲杂部分组织,如有不当的地方还请大家指正。


数据生成

这一部分主要解析预训练bert所需要的数据获取和处理,主要代码来自google官方代码。

首先我只用了wiki语料,bert原始论文中用的都是英文语料,我用的是wiki官方中文语料,下载地址:https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles.xml.bz2,按照google官方推荐使用了attardi/wikiextractor对文本进行了处理,然后进行了简繁转换(这一步可能不是必要的)。总之,经过简单的处理后,我们有了纯净的训练语料。

google处理好之后的数据是采用换行来分句子,采用空行来分割上下文,我简单粗暴得直接采用了json文件,这个只影响后续文件的读取方式。举个例子,google处理完之后文件应该是人如下形式:

知乎
在知乎寻找知识一定是搞错了什么。

微博
同上。

下一步,是构造预训练任务所需要的训练数据。众所周知,BERT预训练任务有两个:masked language model和next sentence prediction。简单描述为:在一句话中mask掉几个单词然后对mask掉的单词做预测、判断两句话是否为上下文的关系,而这两个训练任务是同时进行的,也就意味着训练语料的形式大约为:

[CLS]谷歌和[MASK][MASK]都是不存在的。[SEP]同时,[MASK]也是不存在的。[SEP]

先看几个比较重要(或者论文没有提及)的超参数:

  1. dupe_factor 这意味着我们对同一份维基语料可以生成多次的数据,因为mask具有一定的随机性,所以当文本比较少时可以设置较大的重复次数来取得较多的训练样本。
  2. do_whole_word_mask,英语中首先要对单词做BPE,把每个词切分成word piece,比如说apparent切分成ap ##pa ##rent,这样可以减轻OOV并且对于词语之间的关系有更直观的解释。在BERT最初的版本中,对单词做了word piece,但是在做mask的时候完全没有考虑word piece之间是否属于同一个词。比如在对ap ##pa ##rent进行mask时不考虑这是一个词语而对它整体mask,那么模型可能很容易不需要上下文信息就猜出来被mask掉的部分,这样训练出来的模型就没能充分理解上下文信息,这是我们不希望看到的。所以在mask的时候把ap ##pa ##rent当做一个”整体“。值得注意的是:这里的mask不是狭义的mask,即并不是把三个piece替换成三个[MASK],而是对三个piece分别按照80%换成[MASK],10%不变,10%随机进行替换。
  3. max_predictions_per_seq,一个样本中最多有多少个token被mask掉,这个值应该接近最大序列长度*mask概率。
  4. short_seq_prob,并不是一个样本多长就生成多长的样本,也以一定概率生成短句,提高多样性。
# 重复几次生成
for _ in range(dupe_factor):
    #对每篇文章
    for document_index in range(len(all_documents)):
      instances.extend(
          create_instances_from_document(
              all_documents, document_index, max_seq_length, short_seq_prob,
              masked_lm_prob, max_predictions_per_seq, vocab_words, rng))

下面是从一篇文章中生成训练样本,代码比较长就没有放出来。简单描述生成过程为:

从当前文章中抽取句子,直到整篇文章抽完了或者token长度超过指定值(如128);如果next sentence为True的话直接做MASK,如果为False就从另一篇文章中抽取一些句子组成next sentence,最后再加上[CLS][SEP]。

就说几点我觉得值得注意的:

  1. 每篇文章并不一定只是生成一个样本。假如有一篇文章比较长,有1000个token,规定文本最长为128,那么这篇文章将会有好多样本。
  2. 这里sentence的概念并不是自然意义上的一句话,而是连续的token,一般可能是很多句话而非一句话。
  3. 如果采样出来的句子长度超过了限制,随机从头或者尾去掉比较长sentence的一些token。

模型和预训练

有必要把BERT的模型简单介绍一下,想起两篇文章介绍Transformer和BERT特别好的,建议直接阅读。

模型结构

BERT模型是基于Transformer的Encoder,主要模型结构就是Transformer的堆叠。

当我们组建好Bert模型之后,只要把对应的token喂给BERT,每一层Transformer层吐出相应数量的hidden vector,一层层传递下去,直到最后输出。模型就这么简单,专治花里胡哨,这大概就是谷歌的暴力美学。

bert 中文 代码 谷歌_Bert 预训练小结_第1张图片
BERT模型和Transformer的关系

预训练输入和输出

我们的任务有两个,一个是完形填空式的预测,一个是上下句关系的预测。

bert 中文 代码 谷歌_Bert 预训练小结_第2张图片
模型输入

由于使用了Transformer的模型结构,Transformer可以并行一个序列,对于它来说每个词的位置没有意义,因为self attention对所有输入一直看待,就算打乱顺序输入到模型也没有任何差别,所以需要加上一个位置embedding。值得注意的时候,这个也直接让模型的finetune有了长度限制。因为预训练的时候,position embedding也是跟着一起训练的,在预训练的时候只训练了128或者512的position embedding,如果在下游任务的输入过长,一般就做截断处理。(或者让position embedding跟下游任务的一起fine tune? 好像没人这么做,可能embedding位置离下游位置太远了梯度已经很小了,而且512也够用了吧。)

segment embedding实际上就是一个2*hidden_size的embedding而已。

这里embedding都是随机初始化的,所以直接加起来得到最终的输入,也不存在 什么拼接,最终实际一条样本的输入就是[seq_len*hidden_size]。

模型认为输出的每个位置和输入的位置是一一对应的,在[CLS]位置,我们认为这个位置集中了两个句子之间的关系,而每个单词位置的hidden state代表了预测的单词。

所以做单词预测的时候,先过一个hidden到hidden的线性层,再和训练的word_embedding做乘法(需要转置)。对每一个位置的输出是一个维度为hidden_size的vector,word_embedding维度为[vocab_size*hidden_size],变换之后得到一个维度为vocab_size的vector,softmax后得到对这个位置单词的预测。

做next sentence预测更简单,只需要取[CLS]的hidden state,经过[hidden,2]线性层得到对两句话关系的预测。


fine tuning

bert 中文 代码 谷歌_Bert 预训练小结_第3张图片
预训练于fine tuning

由于模型训练的时候,mask任务让模型尽力去理解上下文关系,next sentence任务则是句子间关系。我们可以认为一个训练好的BERT模型在每个词的位置都充分融合了上下文信息,在[CLS]位置包含了句子关系信息。

所以针对不同的下游任务,只需要在整个大框架下用一些简单的数据进行简单调整。以SQuAD1.1任务为例子,输入为[[CLS]+问题+[SEP]+正文+[SEP]],假设答案span位置在[5,7],那么我们只选择第五个位置的hidden vector

和第7个位置的hidden vector
,然后用
作为每个词的是start的概率分布,这会让对应的位置与S越来越接近。
这部分由于不太想写就先写这么多。

零碎

warmup学习率:

(1.0-is_warmup)*learning_rate+is_warmup*warmup_learning_rate

你可能感兴趣的:(bert,中文,代码,谷歌)