Bert详解

1. BERT模型

BERT的全称是Bidirectional Encoder Representation from Transformers,即双向Transformer的Encoder,因为decoder是不能获要预测的信息的。
模型的主要创新点都在pre-train方法上,即用了Masked LM和Next Sentence Prediction两种方法分别捕捉词语和句子级别的representation

1.1 模型结构

由于模型的构成元素Transformer已经解析过,就不多说了,BERT模型的结构如下图最左:
Bert详解_第1张图片
Bert详解_第2张图片
BERT对比这两个算法的优点是只有BERT表征会基于所有层中的左右两侧语境BERT能做到这一点得益于Transformer中Attention机制将任意位置的两个单词的距离转换成了1(并行处理,所以可以看做是任意位置两个单词举例均为1)。

1.2 Embedding


其中:WordPiece是指将单词划分成一组有限的公共子词单元,能在单词的有效性和字符的灵活性之间取得一个折中的平衡。例如上图的示例中‘playing’被拆分成了‘play’和‘ing’。

在BERT中, 主要是以两种预训练的方式(两个自监督任务)来建立语言模型,一种是Masked LM,另一种是Next sentence prediction,通过这两种预训练方式来建立语言模型。

1.3 Pre-training Task 1#: Masked LM

第一步预训练的目标就是做语言模型,从上文模型结构中看到了这个模型的不同,即bidirectional。关于为什么要如此的bidirectional,作者在reddit上做了解释,意思就是如果使用预训练模型处理其他任务,那人们想要的肯定不止某个词左边的信息,而是左右两边的信息。而考虑到这点的模型ELMo只是将left-to-right和right-to-left分别训练拼接起来。直觉上来讲我们其实想要一个deeply bidirectional的模型,但是普通的LM又无法做到,因为在训练时可能会“穿越”所以作者用了一个加mask的trick。

这里对双向的理解可以进一步详细描述一下:经过这样的操作之后, 序列里面的每一个字, 都含有这个字前面的信息和后面的信息, 这就是双向的理解, 在这里, 一句话中每一个字, 经过注意力机制和加权之后, 当前这个字等于用这句话中其他所有字重新表达了一遍, 每个字含有了这句话中所有成分的信息.

在训练的时候随即从输入预料上mask掉一些单词,然后通过的上下文预测该单词,在训练过程中作者随机mask 15%的token,而不是把像cbow一样把每个词都预测一遍。最终的损失函数只计算被mask掉那个token模型通过上下文的理解预测被遮盖或替换的部分。

Mask如何做也是有技巧的,如果一直用标记[MASK]代替(当然,在实际预测时肯定是碰不到这个标记的)会影响模型(如果一直用一种MASK方式,模型学习到的信息是有限的),所以随机mask的时候10%的单词会被替代成其他单词,10%的单词不替换,剩下80%才被替换为[MASK]。具体为什么这么分配,作者没有说,个人觉得是经验上的设定(和Transformer中的三角函数位置编码一样,经验设定),要注意的是,Masked LM预训练阶段模型是不知道真正被mask的是哪个词,所以模型每个词都要关注。

因为序列长度太大(512包括标识符)会影响训练速度,所以90%的steps都用seq_len=128训练,余下的10%步数训练512长度的输入。
Bert详解_第3张图片

1.4 Pre-training Task 2#: Next Sentence Prediction

因为涉及到QA和NLI(自然语言推断)之类的任务,增加了第二个预训练任务,目的是让模型理解两个句子之间的联系训练的输入是句子A和B,B有一半的几率是A的下一句,输入这两个句子,模型预测B是不是A的下一句。预训练的时候可以达到97-98%的准确度。

注意:作者特意说了语料的选取很关键,要选用document-level的而不是sentence-level的,这样可以具备抽象连续长序列特征的能力。

  1. 首先我们拿到属于上下文的一对句子, 也就是两个句子, 之后我们要在这两段连续的句子里面加一些特殊 t o k e n token token:
    [ c l s ] [cls] [cls]上一句话, [ s e p ] [sep] [sep]下一句话. [ s e p ] [sep] [sep]
    也就是在句子开头加一个 [ c l s ] [cls] [cls], 在两句话之中和句末加 [ s e p ] [sep] [sep]
  2. 我们假设两句话是 [ c l s ] [cls] [cls] my dog is cute [ s e p ] [sep] [sep] he likes playing [ s e p ] [sep] [sep] [ c l s ] [cls] [cls]我的狗很可爱 [ s e p ] [sep] [sep]他喜欢玩耍 [ s e p ] [sep] [sep]), 除此之外, 我们还要准备同样格式的两句话, 但他们不属于上下文关系的情况;
    [ c l s ] [cls] [cls]我的狗很可爱 [ s e p ] [sep] [sep]企鹅不擅长飞行 [ s e p ] [sep] [sep], 可见这属于上下句不属于上下文关系的情况;
    在实际的训练中, 我们让上面两种情况出现的比例为 1 : 1 1:1 1:1, 也就是一半的时间输出的文本属于上下文关系, 一半时间不是.
  3. 我们进行完上述步骤之后, 还要随机初始化一个可训练的 s e g m e n t   e m b e d d i n g s segment \ embeddings segment embeddings, 见上图中, 作用就是用 e m b e d d i n g s embeddings embeddings的信息让模型分开上下句, 我们一把给上句全 0 0 0 t o k e n token token, 下句啊全 1 1 1 t o k e n token token, 让模型得以判断上下句的起止位置, 例如:
    [ c l s ] [cls] [cls]我的狗很可爱 [ s e p ] [sep] [sep]企鹅不擅长飞行 [ s e p ] [sep] [sep]
    0   0    0    0    0    0    0    0     1    1    1    1    1    1    1    1 0 \quad \ 0 \ \ 0 \ \ 0 \ \ 0 \ \ 0 \ \ 0 \ \ 0 \ \ \ 1 \ \ 1 \ \ 1 \ \ 1 \ \ 1 \ \ 1 \ \ 1 \ \ 1 0 0  0  0  0  0  0  0   1  1  1  1  1  1  1  1
    上面 0 0 0 1 1 1就是 s e g m e n t   e m b e d d i n g s segment \ embeddings segment embeddings.
  4. 注意力机制就是, 让每句话中的每一个字对应的那一条向量里, 都融入这句话所有字的信息, 那么我们在最终隐藏层的计算结果里, 只要取出 [ c l s ] t o k e n [cls]token [cls]token所对应的一条向量, 里面就含有整个句子的信息, 因为我们期望这个句子里面所有信息都会往 [ c l s ] t o k e n [cls]token [cls]token所对应的一条向量里汇总:
    模型最终输出的隐藏层的计算结果的维度是:
    我们 X h i d d e n : [ b a t c h _ s i z e ,   s e q _ l e n ,   e m b e d d i n g _ d i m ] X_{hidden}: [batch\_size, \ seq\_len, \ embedding\_dim] Xhidden:[batch_size, seq_len, embedding_dim]
    我们要取出 [ c l s ] t o k e n [cls]token [cls]token所对应的一条向量, [ c l s ] [cls] [cls]对应着   s e q _ l e n \ seq\_len  seq_len维度的第 0 0 0条:
    c l s _ v e c t o r = X h i d d e n [ : ,   0 ,   : ] cls\_vector = X_{hidden}[:, \ 0, \ :] cls_vector=Xhidden[:, 0, :]
    c l s _ v e c t o r ∈ R b a t c h _ s i z e ,   e m b e d d i n g _ d i m cls\_vector \in \mathbb{R}^{batch\_size, \ embedding\_dim} cls_vectorRbatch_size, embedding_dim
    之后我们再初始化一个权重, 完成从 e m b e d d i n g _ d i m embedding\_dim embedding_dim维度到 1 1 1的映射, 也就是逻辑回归, 之后用 s i g m o i d sigmoid sigmoid函数激活, 就得到了而分类问题的推断.
    我们用 y ^ \hat{y} y^来表示模型的输出的推断, 他的值介于 ( 0 ,   1 ) (0, \ 1) (0, 1)之间:
    y ^ = s i g m o i d ( L i n e a r ( c l s _ v e c t o r ) ) y ^ ∈ ( 0 ,   1 ) \hat{y} = sigmoid(Linear(cls\_vector)) \quad \hat{y} \in (0, \ 1) y^=sigmoid(Linear(cls_vector))y^(0, 1)

1.5 Fine-tunning

Bert详解_第4张图片
Bert详解_第5张图片
如上图所示,
Bert详解_第6张图片
其他预测任务需要进行一些调整,如图:
Bert详解_第7张图片
Bert详解_第8张图片
Bert详解_第9张图片
Bert详解_第10张图片

Bert详解_第11张图片

2. 优缺点

Bert详解_第12张图片
还有一个缺点,MASK标记间是假设独立同分布的,未考虑MASK间的联系!

为了解决2.2缺点mask问题1,作者才想到15%mask中:80%mask,10%随机替换,10%不变。问题2一直没有很好的解决,但是付出的代价换来的性能提升是值得的。

3. 总结

BERT是两阶段模型,第⼀阶段双向语⾔模型预训练,这里注意要用双向⽽不是单向,第⼆阶段采用具体任务Fine-tuning或者做特征集成;特征抽取要用Transformer作为特征提取器⽽不是 RNN或者CNN;双向语⾔模型可以采取MASK的⽅法去做。

BERT的本质上也可以理解是通过在海量的语料的基础上运行自监督学习方法为单词学习一个好的特征表示。在以后特定的NLP任务中,我们可以直接使用BERT的特征表示作为该任务的词嵌入特征。所以BERT提供的是一个供其它任务迁移学习的模型,该模型可以根据任务微调或者固定之后作为特征提取器。

我们还可以利用Bert去生成句向量或词向量。每一层transformer的输出值,理论上来说都可以作为句/词向量,但是到底应该取哪一层呢,根据hanxiao大神的实验数据,最佳结果是取倒数第二层,最后一层的值太接近于目标,前面几层的值可能语义还未充分的学习到获取词向量的方式主要是通过项目中的extract_features.py脚本,该脚本最后可以输出如下的数据:

{
 "linex_index": 0,
 "features": [
  {  "token": "[CLS]",//句子开始标志
      "layers": [{  "index": -1, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },
     				 {  "index": -2, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },
     				 {  "index": -3, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },
     				 {  "index": -4, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },]
  },
  {  "token": ""token": "\u769f"",//句子中第一个字
      "layers": [{  "index": -1, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },//第一个词的最后一层(-1)网络的参数
     				 {  "index": -2, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },//第一个词的倒数二层(-2)网络的参数
     				 {  "index": -3, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },//第一个词的倒数三层(-3)网络的参数
     				 {  "index": -4, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },]/
  },
    {  "token": ""token": "\u45ef"",//句子中第2个字
      "layers": [{  "index": -1, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },
     				 {  "index": -2, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },
     				 {  "index": -3, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },
     				 {  "index": -4, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },]
     				 ......
     {  "token": ""token": "\SEP"",//句子结束标志
      "layers": [{  "index": -1, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },
     				 {  "index": -2, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },
     				 {  "index": -3, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },
     				 {  "index": -4, "values": [0.402158,    -7.281092,  -0.351869, -0.432365, -0.453649 ...(dim=768)] },]
  }]}

layers: 是输出那些层的参数,-1就是最后一层,-2是倒数第二层,以此类推
可以根据需要设置layers参数,获取到需要层数中的网络参数(词向量)
PS:为了表示方便,上面词向量我是直接复制一行,粘贴若干次,真实的词向量是不同的。

4. 补充

Bert详解_第13张图片

1、不考虑多头的原因,self-attention中词向量不乘QKV参数矩阵,会有什么问题?

如果不乘QKV参数矩阵,那这个词对应的q,k,v就是完全一样的。在相同量级的情况下,q与k点积的值会是最大的(可以从“两数和相同的情况下,两数相等对应的积最大”类比过来)。那在softmax后的加权平均中,该词本身所占的比重将会是最大的,使得其他词的比重很少,无法有效利用上下文信息来增强当前词的语义表示。而乘以QKV参数矩阵,会使得每个词的q,k,v都不一样,能很大程度上减轻上述的影响。当然,QKV参数矩阵也使得多头可以,类似于CNN中的多核,使捕捉更丰富的特征/信息成为可能。

2、为什么BERT选择mask掉15%这个比例的词,可以是其他的比例吗?

这个问题可以类比到CBOW上下文预测中间词时的滑动窗口的大小,一般情况下我们可能设窗口大小为7,或者上下浮动的数值,那么对于100长度上下文,100/15=7,也就是说对于100长度的上下文,用7窗口大小大概可以覆盖到15%左右,再回到刚刚Bert的这个问题,15%这样一看,其实是比较合理且符合经验直觉了。所以更多是因为经验而设定的。和Transformer位置编码的三角函数的设定也是一样,都是经验设定。

3、使用BERT预训练模型为什么最多只能输入512个词,最多只能两个句子合成一句?

这是Google BERT预训练模型初始设置的原因,前者对应Position Embeddings,后者对应Segment Embeddings。

在BERT config中
“max_position_embeddings”: 512
“type_vocab_size”: 2
因此,在直接使用Google 的BERT预训练模型时,输入最多512个词(还要除掉[CLS]和[SEP]),最多两个句子合成一句。这之外的词和句子会没有对应的embedding。
当然,如果有足够的硬件资源自己重新训练BERT,可以更改 BERT config,设置更大max_position_embeddings 和 type_vocab_size值去满足自己的需求。

4、为什么BERT在第一句前会加一个[CLS]标志?

BERT在第一句前会加一个[CLS]标志,最后一层该位对应向量可以作为整句话的语义表示,从而用于下游的分类任务等。
为什么选它呢,因为与文本中已有的其它词相比,这个无明显语义信息的符号会更“公平”地融合文本中各个词的语义信息,从而更好的表示整句话的语义。

这里补充一下bert的输出,有两种:
一种是get_pooled_out(),就是上述[CLS]的表示,输出shape是[batch size,hidden size]。
一种是get_sequence_out(),获取的是整个句子每一个token的向量表示,输出shape是[batch_size, seq_length, hidden_size],这里也包括[CLS],因此在做token级别的任务时要注意它。

5、Self-Attention 的时间复杂度是怎么计算的?

Bert详解_第14张图片

6、Transformer在哪里做了权重共享,为什么可以做权重共享?

Transformer在两个地方进行了权重共享:
(1)Encoder和Decoder初始输入的Embedding处;
(2)预测时的全连接。

7、BERT非线性的来源在哪里?

前馈层的relu激活函数和self-attention(self-attention是非线性的)

8、BERT的三个Embedding直接相加会对语义有影响吗?

这三个Embedding相加,是非常符合自然直觉的,就如同不同频率的三个信号进行相加,这个当然是完全可以的。

9、在BERT应用中,如何解决长文本问题?

举例: 在阅读理解问题中,article 常常长达1000+, 而Bert 对于这个量级的表示并不支持。

  1. 用Sliding Window(划窗),主要见于诸阅读理解任务(如Stanford的SQuAD)。Sliding Window即把文档分成有重叠的若干段,然后每一段都当作独立的文档送入BERT进行处理。最后再对于这些独立文档得到的结果进行整合。
  2. 还可以分段来执行,各段可以求平均、求max、或者加个attention融合。

10、预训练语言模型ELMo、GPT、Bert双向单向的思考

首先预训练语言模型,因为后续要进行微调,所以其应用场景很多时候需要用到上下文的信息,那么如果直接去使用上下文信息,就会造成语言模型的泄密问题,先来看最早的ELMo,为了避免泄密,但又使用上下文,相当于前向和反向加一起,但这其实不是深层次的双向;然后又出现GPT,那么GPT从整体来看,其实还是单向的语言模型,对吧,但是实际上使用的transformer的decoder做特征抽取,将句子中上文的各个词的位置变成了1,所以GPT可以说是单向,但也不完全是单向,说双向,也不完全是双向;然后便是Bert了,Bert直接用的双向,这样大家就疑问了,不是泄密了吗,但是Bert的双向的预训练语言模型的任务的trick在于,其用上下文去预测mask,这就曲线救国的解决了,想双向又不敢双向的问题。所以Bert效果特别好,主要是这两个预训练任务的提出太神了。但是这样其实也造成了Bert不支持生成任务,像GPT、GPT-2生成任务非常牛逼,但是Bert不行,GPT、GPT-2语言模型的设定,自然可以很好的进行生成任务,而Bert则不行。但是Bert的双向效果之好,的确众人皆知。

你可能感兴趣的:(NLP(包含深度学习))