配合源码才能看懂系列
介绍
传统的语言模型是从左到右或者从右到左的利用上文或者下文去预测当前词,这种设计是依据最初的概率语言模型的思想。但是实际上,当前词出现不只是单单依靠上文 或者下文,其实应该是同时依赖于上下文,在ELMo里面,就是用了bi-lm的结构,但是这种bi-lm只是两个独立的前向和后向模型合并起来的,并不是一种完美的结合上下文,所以需要一种新的语言模型去更好的利用上下文的信息对当前词进行编码。因此谷歌在《Bidirectional Encoder Representation from Transformers》一文中,提出了一种Masked Language Model,他的语言模型结构是在一个句子中随机挑选一部分词汇,用一个MASK标记替换掉该词汇,然后在模型训练的时候去预测该词汇,完成训练过程,最终用encoder的输出作为语言的representation。
上述过程是一个无监督的过程,可以充分利用新闻,文章,对话等无标记的语料,进行无监督的预训练,得到一个预训练模型,然后我们将预训练得到的模型利用有标签的下游任务语料进行有监督的fine tuning,得到可以满足下游任务的模型。所以我们要介绍bert模型需要介绍两处地方,第一处是无监督的预训练过程,产出的是预训练语言模型,第二处是有监督的fine tuning过程,得到的是可以满足下游任务的模型。
预训练过程
Bert模型的预训练过程是利用了无监督的语料训练得到一个包含了大量参数的模型,该模型可以作为下游任务的权值初始值,这种方式类似于迁移学习的理念。
模型语料
模型的输入语料是无标签的文章,利用这些语料本身自带的上下文语义联系,获取语言的表征向量。一篇文章内的句子是有顺序的,不同文章之间的句子是无关联的,bert在预训练的时候,会将这种上下句子的关联也作为一种训练目标。
输入语料预处理
句子对的构造
模型输入的语料是许多个由句子构成的文章(document),模型的目标是构建一个句子a和一个句子b,两个token之间如果是从源于同一个文章的上下文,那这两个句子就是连贯的,如果不是,则需要记录这种两个句子是不连贯,这就是next_sequence_prediction的设计,因为模型并不能处理过长的句子,因此,需要用这种设计将长文本的上下文信息纳入到预训练过程。
我们每次构建的句子对的样本有一个最大长度,称之为max_sequence_len,构建预训练样本时,我们可以轻松将句子a的长度填充到max_sequence_len,但是我们需要和fine tuning过程保持一致,fine tuning可能很难填充这么大的长度。所以我们sometimes(约为10%的概率)将句子a的长度随机选取一个较小的值,作为构建句子a的实际最大限制长度。构建时,从当前文章中连续选取句子,直到句子长度大于实际最大限制长度或者文章已经被选取完了。构建完句子a之后,构建句子b,句子b会以50%的概率选择从句子a后面紧挨着的下一句,还有50%概率随机从另一篇文章中随机抽取连续的几句话,当然如果原文章的句子已经全部用来构造句子a了,那么句子b就只能随机从另一篇文章中随机抽取了。
这样构建出来的句子a和句子b都有可能超过样本最大长度。这时候将对样本进行截断,动态判断句子a或者句子b哪一个更长,将更长的句子随机从开头或者结尾移除一个字符,然后再重新判断,直到句子a加上句子b的总长度,满足最大长度限制。
构建mask字符
模型除了上述的下一句预测next_sequence_prediction,还有mask language model prediction,这里会从两个token中,随机选择15%的词(不包含[CLS]和[SEP]),然后对这15%的词,有80%的概率替换为[MASK],有10%的概率替换为词汇表中的其余词,还有10%的概率不变,然后记录下这15%的词的位置,以及他们原词本身,作为后面计算mask位置的预测函数损失的数据来源。
由此我们获得了以下一组输入,
input_ids:句子中的token位于词表中位置id组成的一维列表,长度为max_seq_len,其中有一些位置已经被替换为[MASK]的id。
input_mask:padding矩阵,非padding部分为1,padding部分为0
segment_ids:句子a部分为0,句子b部分为1
masked_lm_positions:mask的位置序列,也进行了padding到max_predictions_per_seq,padding部分为0
masked_lm_ids:各mask位置对应的真实的token id
masked_lm_weights:各mask位置预测时计算loss的权重比例,暂时都为1
next_sentence_labels:句子a和句子b是否来源于连续的句子,是的话为0,随机下一句则是1.
模型结构
embedding
假设输入的batch大小为B,max_seq_len长度为S,首先要进行word embedding,假设word embedding的大小为E,则word embedding是将(B,S)转换为(B,S,E),word embedding之后,需要进行positional embeddings 和token type embedding,其中positional embedding是编码了位置信息进去,因为后续用的是attention结构,所以需要提前将位置信息写进去,这里positional embedding和之前的transformer还是不一样的,这里其实是根据位置学出了一个embedding出来,具体操作是从一个trainable的shape为(max_position_embeddings,E)的矩阵里面切出来(S,E)作为各位置的embedding。而token type embedding是句子a和句子b的区别,是对segment_ids进行了embedding,其维度也是E。最后这三者相加作为最终的embedding。
embedding的结果还需要增加一个layer norm和drop out
transformer层的encoder层
上面输入的(B,S,E)将进入transformer的结构里面,该结构可以参考transformer里的描述,bert输出了每个attention layer的隐藏层输出,我们将最高层的attention layer隐藏层作为序列中各token对应的输出即sequence_output。
将序列输出的第一个位置[CLS]的隐藏层输出,接一个全连接激活层作为句子的输出pooled_output。
masked language model
由sequence_output获取masked_lm_positions这些位置的隐藏层输出,之后接一个全连接激活层和layer normal获取表征各mask位置的向量,之后需要利用这些维度为E的向量,与之前embedding lookup table向量的转置进行矩阵乘法,假设mask位置为M,那么需要进行mask 预测的向量转为(BM,E),然后将embedding lookup table向量(vocab_size,E)转置为(E,vocab_size),这样矩阵乘法之后得到(BM,vocab_size)的logits向量,logits向量的每个位置有一个真实的值masked_lm_ids,这几个位置互不影响,所以各自独立做一个交叉熵损失函数,然后将损失函数的值求个平均值,这里求平均值是因为不同batch,以及同一个batch的不同sequence里面的mask数目是不一样的。
next sentence prediction
将transformer里面的pooled_output(B,E),然后将该tensor与一个(E,2)的初始化变量进行矩阵乘法,得到句子a与句子b是否连续的logits向量,然后与next_sentence_labels联合起来求出每个句子的交叉熵损失函数,同样的,最终我们仍然获取的是均值。
loss
最终的loss是masked_lm_loss与next_sentence_loss的相加,由此就可以进行反向传播了。
fine tuning过程
fine tuning过程是指利用预训练出来的参数作为初始值,然后在下游任务的监督语料上进行微调,以达到快速收敛,并增强泛化能力的作用。这里预训练结构的参数并非完全使用,而是只利用到sequence_output和pooled_output,这两个一个作为序列表征,另一个作为句子的表征,可以用在下游的任务上,损失函数也是由下游任务的具体需要自行构建,比如序列标注任务,可以利用sequence_output作为各词的编码结构,然后直接用上softmax求交叉熵损失函数,比如文本分类任务,可以利用pooled_output的值,然后接一个全连接激活层,然后再利用softmax求多分类的交叉熵损失函数。