我想去面试系列——BERT

2020-10-10

最近在准备一些面试的东西,正在以面试的角度去温习一些知识点,本文记录的是Bert相关的内容,主要包括基本原理、模型框架、其他变型、细节解读&面试题。

Bert的面试重点在transformer架构、multi-head attention、position embedding、源码

基本原理

BERT:Bidirectional Encoder Representations from Transformers,整体是一个自编码语言模型(Autoencoder LM)。Bidirectional体现在每一个词向量的产生都同时依赖该单词左侧和右侧的语境信息。

Bert设计了两个任务来联合预训练该模型,MLM+NSP,训练时候的loss是两个任务的loss sum:

  • 第一个任务是采用 MaskLM 的方式来训练语言模型,形式化为在给定单词上下文序列之后,球当前单词出现的条件概率的乘积。具体的,训练集中选择15%的mask单词,并以特殊标记[MASK]进行替换。为减少微调时对[MASK]标记的过拟合,数据生成器将不是始终用[MASK]替换所选单词,而是80%的时间里将单词替换成[MASK],10%的时间里用随机单词替换,10%的时间保持单词不变。这样做的目的是使表示偏向实际观察到的单词。这么做的主要原因是:在后续微调任务中语句中并不会出现 [MASK] 标记,而且这么做的另一个好处是:预测一个词汇时,模型并不知道输入对应位置的词汇是否为正确的词汇( 10% 概率),这就迫使模型更多地依赖于上下文信息去预测词汇,并且赋予了模型一定的纠错能力。上述提到了这样做的一个缺点,其实这样做还有另外一个缺点,就是每批次数据中只有 15% 的标记被预测,这意味着模型可能需要更多的预训练步骤来收敛。与去噪的自动编码器(Vincent et al., 2008)不同的是,MLM任务只是让模型预测被遮蔽的标记,而不是要求模型重建整个输入。

  • 第二个任务在双向语言模型的基础上额外增加了一个句子级别的连续性预测任务,即预测输入 BERT 的两段文本是否为连续的文本,引入这个任务可以更好地让模型学到连续的文本片段之间的关系。(RoBerta里说这项任务其实没啥用,起主要作用的还是MLM),这里的masking是静态生成的,每次mask的在训练之前都固定好了。具体的,当为每个预训练选择句子A和B时,50%的时间是选择紧跟着A的实际的下一个句子作为B,而另外50%的时间是随机采样语料库中其他的错误句子。最终的预训练模型在这个任务中达到了 97%-98% 的准确率。

  • 对于预训练语料库,使用 BooksCorpus(800M 单词)(Zhu et al., 2015)和英语维基百科(2,500M 单词)。对于维基百科,只提取文本段落,而忽略列表、表格和标题。

    这里$L$代表Transformer的层数,$H$代表隐藏层大小,$A$代表自注意力的头的个数。

模型框架

  • Encoder:Transformer Encoder。实际上,Transformer的Encoder(双向Transformer)就是Bert,Transformer的Decoder(单左向Transformer)稍微改了一点就是GPT。与 Transformer 本身的 Encoder 端相比,BERT 的 Transformer Encoder 端输入的向量表示多了 Segment Embeddings。

  • 每一层的输入是token+seg+pos embedding,记为$e_0$,将$e_0$输入mult-head attention(注意这里的multi-head的输入是QKV,但是里面的每一个self-attention的输入是QW,KW,VW),然后将该输出与原输入残差求和ResidualConnection层归一化LayerNormalization,得到$e_{mid}$,再将$e_{mid}$输入FFN再残差求和得到每一层最后的输出。这里残差机制的意义是可以使得模型更深,避免梯度爆炸和梯度消失的问题;所有层的FFN是参数共享的。

    其中 $e_i \in \mathbb{R}^{N \times d_{model}}$。EncoderLayer的架构是:

  • Multi-Head-Attention:输入向量序列$e_{in} = (e_{in1},e_{in2},…,e_{inN}) \in \mathbb{R}^{N \times d_{model}}, Q = e_{in}, K = e_{in}, V = e_{in}$。

    其中,多头输出$head_i = Attention(QW_i^Q, KW_i^K, VW_i^V)$,$Concat(head_1, …, head_h) \in \mathbb{R}^{N \times hd_v}$,可学习的参数矩阵$W_i^Q \in \mathbb{R}^{d_{model} \times d_k}$,$W_i^K \in \mathbb{R}^{d_{model} \times d_k}$,$W_i^V \in \mathbb{R}^{d_{model} \times d_v}$,$W_O \in \mathbb{R}^{hd_v \times d_{model}}$。

    使用缩放点积作为打分函数的自注意力机制:

  • 前馈神经网络FFN:这里需要注意的是使用了GELU作为激活函数(这里的目的是对于学习率的 warm-up 策略,使用的激活函数不再是普通的 ReLu),这里参数矩阵$W_1 \in \mathbb{R}^{d_{model} \times d_{ff}}$,$W_2 \in \mathbb{R}^{d_{ff} \times d_{model}}$,$b_1 \in \mathbb{R}^{d_{ff}}$,$b_2 \in \mathbb{R}^{d_{model}}$。

  • 下游任务:句子级别:Sentence Pair Classification(取[CLS]后接全连接层+sigmoid)、Single Sentence Classification(取[CLS]后接全连接层+softmax);单词级别:Question Answering(取特定区间的token后接其他NN)、Single Sentence Tagging(序列标注多分类,对于每一个token有几个 label 就连接到几个全连接层,再接softmax,然后遍历特定区间的所有token)。分析证实,在QA任务上,使用最高层的[CLS]效果更好;在序列标注任务上,使用Attention方法集成多层词向量效果最好;在小数据集上BiLSTM要比BERT要好。这里存在个疑问,SQuAD上预测起始/结束位置时,在具体实现的时候是两个独立的条件概率呢,还是联合概率呢?

实现

利用huggingface的transformers包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from transformers import BertTokenizer, BertModel
# BertTokenizer: 分词工具
# BertModel: BERT模型
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
model = BertModel.from_pretrained("bert-base-uncased")

# inputs = tokenizer("This is sentence 1.", return_tensors="pt")
inputs = tokenizer("This is sentence 1.", "This is sentence 2.", return_tensors="pt") # 返回{'input_ids':tensor([[]]), 'token_type_ids':tensor([[]]), 'attention_mask':tensor([[]])}
tokenizer.decode(inputs["input_ids"].data.cpu().numpy().reshape(-1)) # 可反向还原句子(OOV单词会被替换为[UNK])
sequence_outputs, pooled_outputs = model(**inputs) #前者是token embedding(1*len_token*hidden_size),后者是segment embedding(1*len_token),对[CLS]做了池化之后的结果
# 视频讲师说他基本上不用pooled_outputs来当整个句子的表示,因为不怎么能吸收整个句子的语义
# 他提供的trick是,所有向量取平均
sen_vec = sequence_outputs.mean(1)
cos_similarity = (sen_vec * sen_vec).sum(-1) / torch.sqrt((sen_vec * sen_vec).sum(-1)) # cosine similarity

其他变形

XLNet,是BERT之后的一个自回归(Auto Regression,AR)语言模型。可以理解为BERT是一个自编码(Auto Encoder,AE)模型,将输入句子的某些单词 mask 掉,然后再通过 BERT 还原数据。而XLNet这类自回归语言模型则是不断地使用当前得到的信息预测下一个输出。AR 的方法可以更好地学习 token 之间的依赖关系但是它只能利用单向信息(纯前向或者纯后向),而 AE 的方法可以更好地利用深层的双向信息。因此 XLNet 希望将 AR 和 AE 两种方法的优点结合起来,XLNet 使用了 Permutation Language Model (PLM)实现这一目的。目的就是为了解决BERT的[MASK]在训练和推理中不一致的问题。

  • 使用排列语言模型,该模型不再对传统的AR模型的序列的值按顺序进行建模,而是最大化所有可能的序列的因式分解顺序的期望对数似然。Permutation 指排列组合的意思,XLNet 将句子中的 token 随机排列,然后采用 AR 的方式预测末尾的几个 token。这样一来,在预测 token 的时候就可以同时利用该 token 双向的信息,并且能学到 token 间的依赖。XLNet 中通过 Attention Mask 实现 PLM,而无需真正修改句子 token 的顺序。
  • 采用基于目标感知特征的双流自注意力。无论预测目标的位置在哪里,因式分解后得到的所有情况都是一样的,并且transformer的权重对于不同的情况是一样的,因此无论目标位置怎么变都能得到相同的分布结果。为了解决这个问题,论文中提出来新的分布计算方法Two-Stream Self-Attention,来实现目标位置感知。这个需要看原论文。
    • 如果目标是预测$x_{z_t},g_{\theta}(x_{z_{<t}},z_t)$那么只能有其位置信息$z_t$而不能包含内容信息$x_{z_t}$
    • 如果目标是预测其他tokens即$x_{z_j}, j>t$,那么应该包含$x_{z_t}$的内容信息这样才有完整的上下文信息
  • 作者还将transformer-xl的两个最重要的技术点应用了进来,即相对位置编码片段循环机制。transformer-xl的提出主要是为了解决超长序列的依赖问题,对于普通的transformer由于有一个最长序列的超参数控制其长度,对于特别长的序列就会导致丢失一些信息,transformer-xl就能解决这个问题。对于超长文本,如果采用transformer-xl,首先取第一个段进行计算,然后把得到的结果的隐藏层的值进行缓存,第二个段计算的过程中,把缓存的值拼接起来再进行计算。该机制不但能保留长依赖关系还能加快训练,因为每一个前置片段都保留了下来,不需要再重新计算,在transformer-xl的论文中,经过试验其速度比transformer快了1800倍。另一方面BERT的position embedding采用的是绝对位置编码,但是绝对位置编码在transformer-xl中有一个致命的问题,因为没法区分到底是哪一个片段里的,这就导致了一些位置信息的损失,这里被替换为了transformer-xl中的相对位置编码(相对距离)。
  • 去除了NSP任务,作者发现该任务对结果的提升并没有太大的影响,主要原因是NSP其实包含了两个子任务,主题预测与关系一致性预测,但是主题预测相比于关系一致性预测简单太多了。

Ernie清华,提出用知识图谱来增强预训练模型的能力,其实就是在与训练的时候在BERT的基础上加入一个实体对齐Entity Alignment任务。模型有两个encoder,T-encoder和K-encoder,其实这里的K-encoder只有在预训练的时候有作用,在之后的fine-tuning阶段只要使用T-encoder就可以了,所以这里的重要就是引入了实体对齐这个任务而已。本文提出了随机mask tokens-entity中的entity,然后去预测该位置对应的entity,本质上和MLM(mask language model)任务一致,都属于去噪自编码。KG的引入使得在一些和知识图谱相关的上游任务中,该模型的表现要优于BERT。

Ernie百度,也是引入了知识信息, 但是做法与清华的Ernie完全不一样,这里主要的改变是针对bert中的MLM任务做了一些改进。在bert中只是mask了单个token,但是在语言中,很多时候都是以短语或者实体存在的,如果不考虑短语或者实体中词之间的相关性,而将所有的词独立开来,不能很好的表达句法,语义等信息,因此本文引入了三种mask的方式,分别对token,entity,phrase进行mask。除此之外,本论文中还引入了对话语料,丰富语料的来源,并针对对话语料,给出了一个和NSP相似的任务Dialogue Language Model (DLM)

RoBERTa,改进版Bert,是目前最好用的,它在模型层面没有改变原BERT,改变的只是预训练的方法。

  • 更大的batch size,加大训练数据16GB->160GB,训练时间更长。原本的BERTbase 的batch size是256,训练1M个steps。RoBERTa的batch size为8k。为什么要用更大的batch size呢?(除了因为他们有钱玩得起外)作者借鉴了在机器翻译中,用更大的batch size配合更大学习率能提升模型优化速率和模型性能的现象,并且也用实验证明了确实Bert还能用更大的batch size。
  • 不需要Next Sentence Prediction Loss(同时也发现了segment embedding其实作用不大)RoBERTa去除了NSP,每次输入连续的多个句子,直到最大长度512(可以跨文章)。这种训练方式叫做(FULL - SENTENCES),而原来的Bert每次只输入两个句子。实验表明在MNLI这种推断句子关系的任务上RoBERTa也能有更好性能。
  • 使用更长的训练sequence。
  • dynamic masking。原来的BERT采用的是static masking,也就是在dataloader创造预训练数据的时候就已经决定了要mask哪个。整个训练过程,这15%的Tokens一旦被选择就不再改变,也就是说从一开始随机选择了这15%的Tokens,之后的N个epoch里都不再改变了。而RoBERTa一开始把预训练的数据复制10份,每一份都随机选择15%的Tokens进行Masking,也就是说,同样的一句话有10种不同的mask方式。然后每份数据都训练N/10个epoch。这就相当于在这N个epoch的训练中,每个序列的被mask的tokens是会变化的。这就叫做动态Masking。

Albert,新的轻量版的BERT,参数比BERT少了18倍。

  • 对Embedding因式分解(Factorized embedding parameterization)。ALBERT的作者注意到,对于BERT、XLNet和RoBERTa,word embedding的维度(E)与encoder输出的hidden embedding维度(H)是一样的都是768,H==E,这就意味着二者表达含义的是同等重要的。然而实际上word embedding是用来学习上下文独立表示的,hidden embedding是为了学习上下文依赖表示的。理论上来说hidden embedding的表述包含的信息应该更多一些,因此应该让 H>>E。在NLP任务中,通常词典都会很大,embedding matrix的大小是 E×V,如果和BERT一样让 H==E,那么embedding matrix的参数量会很大,并且反向传播的过程中,更新的内容也比较稀疏。结合上述说的两个点,ALBERT采用了一种因式分解的方法来降低参数量。首先把one-hot向量映射到一个低维度的空间,大小为E,然后再映射到一个高维度的空间,说白了就是先经过一个维度很低的embedding matrix,然后再经过一个高维度matrix把维度变到隐藏层的空间内,从而把参数量从O(V×H)降低到了O(V×E+E×H),当E<<H时参数量减少的很明显。
  • 跨层的参数共享(Cross-layer parameter sharing)。Albert的核心思想是共享层与层之间的参数,全连接层与attention层都进行参数共享,也就是说共享encoder内的所有参数,每一层的hidden_size变大,所有24层的transformer的参数都用同一个。虽说看起来模型更小更深了,但是实际inference的时候还是会比bert慢的,因为虽然参数共享了,但推理的时候还是得过相同的N层。实际上是通过参数共享的方式降低了内存,预测阶段还是需要和BERT一样的时间,如果采用了xxlarge版本的ALBERT,那实际上预测速度会更慢。
  • 把NSP任务换成了sentence ordering objective。BERT的NSP任务实际上是一个二分类,训练数据的正样本是通过采样同一个文档中的两个连续的句子,而负样本是通过采用两个不同的文档的句子。该任务主要是希望能提高下游任务的效果,例如NLI自然语言推理任务。但是后续的研究发现该任务效果并不好,主要原因是因为其任务过于简单。NSP其实包含了两个子任务,主题预测与关系一致性预测,但是主题预测相比于关系一致性预测简单太多了,并且在MLM任务中其实也有类型的效果。在ALBERT中,为了只保留一致性任务去除主题识别的影响,提出了一个新的任务 sentence-order prediction(SOP),SOP的正样本和NSP的获取方式是一样的,负样本把正样本的顺序反转即可。SOP因为实在同一个文档中选的,其只关注句子的顺序并没有主题方面的影响。并且SOP能解决NSP的任务,但是NSP并不能解决SOP的任务。
  • ALBERT的作者还发现一个很有意思的点,ALBERT在训练了100w步之后,模型依旧没有过拟合,于是乎作者果断移除了dropout,没想到对下游任务的效果竟然有一定的提升。这也是业界第一次发现dropout对大规模的预训练模型会造成负面影响。

Electra,最主要的贡献是提出了新的预训练任务和框架,把生成式的Masked language model(MLM)预训练任务改成了判别式的Replaced token detection(RTD)任务,判断当前token是否被语言模型替换过。

  • 提出了 Replaced Token Detection (RTD) 预训练任务,判断每个词是否是被替换过的词。
  • ELECTRA 由两个部分组成,第一部分是生成器 (Generator),生成器将句子中的部分单词进行替换。第二部分是判别器 (Discriminator),判别器用于判断一个句子中每一个单词是否被替换了,训练的过程会预测所有的单词,比 BERT 更高效。
  • 使用GAN的训练思路,对于一段文本,ELECTRA 使用了 MLM 对生成器进行训练,也是随机 [mask] 部分单词,然后用Generator预测的结果替换该单词;Discriminator的任务是预测每个位置的单词是来自于原文还是来自Generator的文本。该模型的主观思想是,生成mask的时候有些位置很好学,但是有些位置的东西很难学,那么模型更有机会学到一些很难的场景,能力更强。
  • 因为由于这种对抗生成的方式不同于GAN可以梯度连续从Generator传到Discriminator,Electra的梯度不能从Generator到Discriminator,所以只能综合两者的损失值对Generator进行损失传递。ELECTRA 总体的损失函数由生成器的损失函数 LMLM 和判别器的损失函数 LDisc 组成。生成器的训练损失函数仍然是 MLM 损失函数,主要原因是生成器将单词进行替换,而单词是离散的,导致判别器到生成器的梯度中断了。具体来说是利用Generator loss对Generator进行传导,用Generator loss + Discriminator loss对Discriminator进行传导。

OpenAI-GPTELMo的架构和Bert几乎是一样的,都是输入之后用特征提取器提取特征,然后输出。

ELMo和OpenAI GPT的思想其实非常非常简单,就是用海量的无标注数据学习语言模型,在学习语言模型的过程中自然而然的就学到了上下文的语义关系。它们都是来学习一个语言模型,前者使用的是LSTM而后者使用Transformer,在进行下游任务处理的时候也有所不同,ELMo是把它当成特征。拿分类任务来说,输入一个句子,ELMo用LSTM把它扫一次,这样就可以得到每个词的表示,这个表示是考虑上下文的,因此”He deposited his money in this bank”和”His soldiers were arrayed along the river bank”中的两个bank的向量是不同的。下游任务用这些向量来做分类,它会增加一些网络层,但是ELMo语言模型的参数是固定的。而OpenAI GPT不同,它直接用特定任务来Fine-Tuning Transformer的参数。因为用特定任务的数据来调整Transformer的参数,这样它更可能学习到与这个任务特定的上下文语义关系,因此效果也更好。

差别是,GPT只用了前序序列没用后续序列(Bert发现了这点于是用了MLM或者是CBOW),ELMo用的是BiLSTM(Bert采用了更强的特征提取器Transformer)。Bert相对于其之前工作word2vec、GPT、ELMo的优势在于:

  • 相比于word2vec包含了语境信息;
  • 相比于ELMo速度更快,并行程度更高,ELMo 使用独立训练的从左到右和从右到左的 LSTM 的连接来为下游任务生成特征;
  • 相比于GPT包含了双向的语境信息。BERT Transformer 使用的是双向的自注意力,而 GPT Transformer 使用的是受限的自注意力,每个标记只能关注其左边的语境。
  • 更多的训练语料,GPT 是在 BooksCorpus 上训练出来的然而 BERT 是在 BooksCorpus和 Wikipedia上训练出来的。GPT 仅在微调时使用[SEP][CLS] 而BERT 在预训练时使用 [SEP][CLS]Segment embedding

总体来说,基于Transformer的模型统治了NLP,主要原因有:①更大规模的预训练数据搭配更大的模型和更强大的算力②一些局部的小技巧(数据预处理、masking、训练任务等)③模型的压缩与蒸馏、加速与并行。缺点就是在少训练数据时容易过拟合。

细节 & 面试题搜集

后续更新…

参考文献

  1. Pre-training of Deep Bidirectional Transformers for Language Understanding:https://arxiv.org/abs/1810.04805
  2. BERT 模型详解:http://fancyerii.github.io/2019/03/09/bert-theory/
  3. 七月在线NLP褚博士:BERT模型深度修炼指南:http://www.julyedu.com/video/play/264/8448
  4. NewBeeNLP关于BERT,面试官们都怎么问:https://mp.weixin.qq.com
  5. 一文读懂BERT(原理篇):https://blog.csdn.net/jiaowoshouzi/article/details/89073944
  6. The Illustrated Transformer:http://jalammar.github.io/illustrated-transformer/
  7. A Visual Guide to Using BERT for the First Time:http://jalammar.github.io/a-visual-guide-to-using-bert-for-the-first-time/
  8. XLNet详解:https://zhuanlan.zhihu.com/p/110204573
  9. XLNet详解:https://www.jianshu.com/p/2b5b368cbaa0
  10. 超细节的BERT/Transformer知识点:https://zhuanlan.zhihu.com/p/132554155
  11. 一文揭开ALBERT的神秘面纱:https://blog.csdn.net/u012526436/article/details/101924049
  12. BERT知识点总结:https://blog.csdn.net/XiangJiaoJun_/article/details/107129808
  13. 后BERT时代:15个预训练模型对比分析与关键点探索:https://www.jiqizhixin.com/articles/2019-08-26-16
  14. BERT模型详解:http://fancyerii.github.io/2019/03/09/bert-theory/

本文来源:「想飞的小菜鸡」的个人网站 vodkazy.cn

版权声明:本文为「想飞的小菜鸡」的原创文章,采用 BY-NC-SA 许可协议,转载请附上原文出处链接及本声明。

原文链接:https://vodkazy.cn/2020/10/10/我想去面试系列——BERT

支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者,更多文章请访问想飞的小菜鸡