MENU

BERT详解(附带ELMo、GPT介绍)

July 21, 2020 • Read: 35597 • Deep Learning阅读设置

首先我会详细阐述BERT原理,然后简单介绍一下ELMO以及GPT

BERT详解

BERT全称为Bidirectional Encoder Representation from Transformer,是Google以无监督的方式利用大量无标注文本「炼成」的语言模型,其架构为Transformer中的Encoder(BERT=Encoder of Transformer)

我在Transformer详解中已经详细的解释了所有Transformer的相关概念,这里就不再赘述

以往为了解决不同的NLP任务,我们会为该任务设计一个最合适的神经网络架构并做训练,以下是一些简单的例子

不同的NLP任务通常需要不同的模型,而设计这些模型并测试其performance是非常耗成本的(人力,时间,计算资源)。如果有一个能直接处理各式NLP任务的通用架构该有多好?

随着时代演进,不少人很自然地有了这样子的想法,而BERT就是其中一个将此概念付诸实践的例子

Google在预训练BERT时让它同时进行两个任务:

  1. 漏字填空(完型填空),学术点的说法是 Masked Language Model
  2. 判断第2个句子在原始本文中是否跟第1个句子相接(Next Sentence Prediction

对正常人来说,要完成这两个任务非常简单。只要稍微看一下前后文就知道完形填空任务中[MASK]里应该填退了;而醒醒吧后面接你没有妹妹也十分合理

接下来我会分别详细介绍论文中这两个任务的设计细节

BERT语言模型任务一:Masked Language Model

在BERT中,Masked LM(Masked Language Model)构建了语言模型,简单来说,就是随机遮盖或替换一句话里面的任意字或词,然后让模型通过上下文预测那一个被遮盖或替换的部分,之后做Loss的时候也只计算被遮盖部分的Loss,这其实是一个很容易理解的任务,实际操作如下:

  1. 随机把一句话中15%的token(字或词)替换成以下内容:

    1. 这些token有80%的几率被替换成[MASK],例如my dog is hairy→my dog is [MASK]
    2. 有10%的几率被替换成任意一个其它的token,例如my dog is hairy→my dog is apple
    3. 有10%的几率原封不动,例如my dog is hairy→my dog is hairy
  2. 之后让模型预测和还原被遮盖掉或替换掉的部分,计算损失的时候,只计算在第1步里被随机遮盖或替换的部分,其余部分不做损失,其余部分无论输出什么东西,都无所谓

这样做的好处是,BERT并不知道[MASK]替换的是哪一个词,而且任何一个词都有可能是被替换掉的,比如它看到的apple可能是被替换的词。这样强迫模型在编码当前时刻词的时候不能太依赖当前的词,而要考虑它的上下文,甚至根据上下文进行"纠错"。比如上面的例子中,模型在编码apple时,根据上下文my dog is,应该把apple编码成hairy的语义而不是apple的语义

BERT语言模型任务二:Next Sentence Prediction

我们首先拿到属于上下文的一对句子,也就是两个句子,之后我们要在这两个句子中加一些特殊的token:[CLS]上一句话[SEP]下一句话[SEP]。也就是在句子开头加一个[CLS],在两句话之间和句末加[SEP],具体地如下图所示

可以看到,上图中的两句话明显是连续的。如果现在有这么一句话[CLS]我的狗很可爱[SEP]企鹅不擅长飞行[SEP],可见这两句话就不是连续的。在实际训练中,我们会让这两种情况出现的数量为1:1

Token Embedding就是正常的词向量,即PyTorch中的nn.Embedding()

Segment Embedding的作用是用embedding的信息让模型分开上下句,我们给上句的token全0,下句的token全1,让模型得以判断上下句的起止位置,例如

[CLS]我的狗很可爱[SEP]企鹅不擅长飞行[SEP]
 0   0 0 0 0 0 0 0  1 1 1 1 1 1 1 1

Position Embedding和Transformer中的不一样,不是三角函数,而是学习出来的

Multi-Task Learning

BERT预训练阶段实际上是将上述两个任务结合起来,同时进行,然后将所有的Loss相加,例如

Input:
[CLS] calculus is a branch of math [SEP] panda is native to [MASK] central china [SEP]

Targets: false, south
----------------------------------
Input:
[CLS] calculus is a [MASK] of math [SEP] it [MASK] developed by newton and leibniz [SEP]

Targets: true, branch, was

Fine-Tuning

BERT的Fine-Tuning共分为4中类型,以下内容、图片均来自台大李宏毅老师Machine Learning课程(以下内容 图在上,解释在下)

如果现在的任务是classification,首先在输入句子的开头加一个代表分类的符号[CLS],然后将该位置的output,丢给Linear Classifier,让其predict一个class即可。整个过程中Linear Classifier的参数是需要从头开始学习的,而BERT中的参数微调就可以了

这里李宏毅老师有一点没讲到,就是为什么要用第一个位置,即[CLS]位置的output。这里我看了网上的一些博客,结合自己的理解解释一下。因为BERT内部是Transformer,而Transformer内部又是Self-Attention,所以[CLS]的output里面肯定含有整句话的完整信息,这是毋庸置疑的。但是Self-Attention向量中,自己和自己的值其实是占大头的,现在假设使用$w_1$的output做分类,那么这个output中实际上会更加看重$w_1$,而$w_1$又是一个有实际意义的字或词,这样难免会影响到最终的结果。但是[CLS]是没有任何实际意义的,只是一个占位符而已,所以就算[CLS]的output中自己的值占大头也无所谓。当然你也可以将所有词的output进行concat,作为最终的output

如果现在的任务是Slot Filling,将句子中各个字对应位置的output分别送入不同的Linear,预测出该字的标签。其实这本质上还是个分类问题,只不过是对每个字都要预测一个类别

如果现在的任务是NLI(自然语言推理)。即给定一个前提,然后给出一个假设,模型要判断出这个假设是 正确、错误还是不知道。这本质上是一个三分类的问题,和Case 1差不多,对[CLS]的output进行预测即可

如果现在的任务是QA(问答),举例来说,如上图,将一篇文章,和一个问题(这里的例子比较简单,答案一定会出现在文章中)送入模型中,模型会输出两个数s,e,这两个数表示,这个问题的答案,落在文章的第s个词到第e个词。具体流程我们可以看下面这幅图

首先将问题和文章通过[SEP]分隔,送入BERT之后,得到上图中黄色的输出。此时我们还要训练两个vector,即上图中橙色和黄色的向量。首先将橙色和所有的黄色向量进行dot product,然后通过softmax,看哪一个输出的值最大,例如上图中$d_2$对应的输出概率最大,那我们就认为s=2

同样地,我们用蓝色的向量和所有黄色向量进行dot product,最终预测得$d_3$的概率最大,因此e=3。最终,答案就是s=2,e=3

你可能会觉得这里面有个问题,假设最终的输出s>e怎么办,那不就矛盾了吗?其实在某些训练集里,有的问题就是没有答案的,因此此时的预测搞不好是对的,就是没有答案

以上就是BERT的详细介绍,参考以下文章

ELMo

ELMo是Embedding from Language Model的缩写,它通过无监督的方式对语言模型进行预训练来学习单词表示

这篇论文的想法其实非常简单,但是效果却很好。它的思路是用深度的双向Language Model在大量未标注数据上训练语言模型,如下图所示

在实际任务中,对于输入的句子,我们使用上面的语言模型来处理它,得到输出向量,因此这可以看作是一种特征提取。但是ELMo与普通的Word2Vec或GloVe不同,ELMo得到的Embedding是有上下文信息的

具体来说,给定一个长度为N的句子,假设为$t_1,t_2,…,t_N$,语言模型会计算给定$t_1,t_2,…,t_{k-1}$的条件下出现$t_k$的概率:

$$ p(t_1,...,t_N)=\prod_{i=1}^{k}p(t_k\mid t_1,...,t_{k-1}) $$

传统的N-gram模型不能考虑很长的历史,因此现在的主流是使用多层双向LSTM。在时刻$k$,LSTM的第$j$层会输出一个隐状态$\overrightarrow{h_{kj}}$,其中$j=1,...,L$,$L$是LSTM的层数。最上层是$\overrightarrow{h_{kL}}$,对它进行softmax之后得到输出词的概率

类似地,我们可以用一个反向LSTM来计算概率:

$$ p(t_1,...,t_N)=\prod_{i=1}^{k}p(t_k\mid t_{k+1},...,t_N) $$

通过这个LSTM,我们可以得到$\overleftarrow{h_{kj}}$。我们的损失函数是这两个LSTM的加和

$$ \begin{aligned} \mathcal{L} = - \sum_{i=1}^n \Big( \log p(t_i \mid t_1, \dots, t_{i-1}; \Theta_e, \overrightarrow{\Theta}_\text{LSTM}, \Theta_s) + \\ \log p(t_i \mid t_{i+1}, \dots, t_n; \Theta_e, \overleftarrow{\Theta}_\text{LSTM}, \Theta_s) \Big) \end{aligned} $$

这两个LSTM有各自的参数$\overrightarrow{\Theta}_{LSTM}$和$\overleftarrow{\Theta}_{LSTM}$,而Word Embedding参数$\Theta_x$和Softmax参数$\Theta_s$是共享的

ELMo Representations

对于输入的一个词$t_k$,ELMo会计算$2L+1$个representation(输入词的word embedding,前向后向的2L个representation)

$$ \begin{aligned} R_k&=\{x_k, \overrightarrow{h_{kj}}, \overleftarrow{h_{kj}}\mid j=1,2,...,L\} \\ &=\{{h_{kj}}\mid j=0,1,...,L\} \end{aligned} $$

其中:

  • $h_{k0}$是词$t_k$的Embedding,上下文无关
  • $h_{kj}=[\overrightarrow{h}_{kj}; \overleftarrow{h}_{kj}], j>0$,上下文相关

为了用于下游(DownStream)的特定任务,我们会把不同层的隐状态组合起来,具体组合的参数是根据不同的特定任务学习出来的,公式如下:

$$ ELMo_k^{task}=E(R_k;\Theta_{task})=\gamma^{task}\sum_{j=0}^{L}s_j^{task}h_{kj} $$

这里的$\gamma^{task}$是一个缩放因子,而$s_j^{task}$用于把不同层的输出加权组合。在处理特定任务时,LSTM的参数$h_{kj}$都是固定的(或者是微调的),主要调的参数只是$\gamma^{task}$和$s_j^{task}$,当然这里ELMo只是一个特征提取,实际任务会再加上一些其它的网络架构

GPT(Generative Pre-training Transformer)

GPT得到的语言模型参数不是固定的,它会根据特定的任务进行调整(通常是微调),这样的到的句子表示能更好的适配特定任务。它的思想也很简单,使用单向Transformer学习一个语言模型,对句子进行无监督的Embedding,然后根据具体任务对Transformer的参数进行微调。GPT与ELMo有两个主要的区别:

  1. 模型架构不同:ELMo是浅层的双向RNN;GPT是多层的transformer decoder
  2. 针对下游任务的处理不同:ELMo将词嵌入添加到特定任务中,作为附加功能;GPT则针对所有任务微调相同的基本模型

无监督的Pretraining

这里解释一下上面提到的单向Transformer。在Transformer的文章中,提到了Encoder与Decoder使用的Transformer Block是不同的。在Decoder Block中,使用了Masked Self-Attention,即句子中的每个词都只能对包括自己在内的前面所有词进行Attention,这就是单向Transformer。GPT使用的Transformer结构就是将Encoder中的Self-Attention替换成了Masked Self-Attention,具体结构如下图所示

具体来说,给定一个未标注的预料库$\mathcal{U}=\{u_1,…,u_n\}$,我们训练一个语言模型,对参数进行最大(对数)似然估计:

$$ L_1(\mathcal{U})=\sum_i \log P(u_i|u_1,...,u_{k-1};\Theta) $$

训练的过程也非常简单,就是将n个词的词嵌入($W_e$)加上位置嵌入($W_p$),然后输入到Transformer中,n个输出分别预测该位置的下一个词

$$ \begin{split} h_0 & =UW_e+W_p \\ h_l & = \text{transformer_block}(h_{l-1}) \\ P(u) & = \text{softmax}(h_n W_e^T) \end{split} $$

这里的位置编码没有使用传统Transformer固定编码的方式,而是动态学习的

监督的Fine-Tuning

Pretraining之后,我们还需要针对特定任务进行Fine-Tuning。假设监督数据集合$\mathcal{C}$的输入$X$是一个词序列$x^1,...,x^m$,输出是一个分类的标签$y$,比如情感分类任务

我们把$x^1,...,x^m$输入Transformer模型,得到最上层最后一个时刻的输出$h_l^m$,将其通过我们新增的一个Softmax层(参数为$W_y$)进行分类,最后用CrossEntropyLoss计算损失,从而根据标准数据调整Transformer的参数以及Softmax的参数$W_y$。这等价于最大似然估计:

$$ \begin{aligned} &L_2(\mathcal{C})=\sum_{(x,y)}\log P(y\mid x^1,...,x^m) \\ &P(y\mid x^1,...,x^m)=\text{softmax}(h_l^mW_y) \end{aligned} $$

正常来说,我们应该调整参数使得$L_2$最大,但是为了提高训练速度和模型的泛化能力,我们使用Multi-Task Learning,同时让它最大似然$L_1$和$L_2$

$$ L_3(\mathcal{C})=L_2(\mathcal{C})+\lambda \times L_1(\mathcal{C}) $$

这里使用的$L_1$还是之前语言模型的损失(似然),但是使用的数据不是前面无监督的数据$\mathcal{U}$,而是使用当前任务的数据$\mathcal{C}$,而且只使用其中的$X$,而不需要标签$y$

其它任务

针对不同任务,需要简单修改下输入数据的格式,例如对于相似度计算或问答,输入是两个序列,为了能够使用GPT,我们需要一些特殊的技巧把两个输入序列变成一个输入序列

  • Classification:对于分类问题,不需要做什么修改
  • Entailment:对于推理问题,可以将先验与假设使用一个分隔符分开
  • Similarity:对于相似度问题,由于模型是单向的,但相似度与顺序无关,所以要将两个句子顺序颠倒后,把两次输入的结果相加来做最后的推测
  • Multiple-Choice:对于问答问题,则是将上下文、问题放在一起与答案分隔开,然后进行预测

ELMo、GPT的问题

ELMo和GPT最大的问题就是传统的语言模型是单向的——我们根据之前的历史来预测当前词。但是我们不能利用后面的信息。比如句子The animal didn’t cross the street because it was too tired。我们在编码it的语义的时候需要同时利用前后的信息,因为在这个句子中,it可能指代animal也可能指代street。根据tired,我们推断它指代的是animal。但是如果把tired改成wide,那么it就是指代street了。传统的语言模型,都只能利用单方向的信息。比如前向的RNN,在编码it的时候它看到了animalstreet,但是它还没有看到tired,因此它不能确定it到底指代什么。如果是后向的RNN,在编码的时候它看到了tired,但是它还根本没看到animal,因此它也不能知道指代的是animal。Transformer的Self-Attention理论上是可以同时关注到这两个词的,但是根据前面的介绍,为了使用Transformer学习语言模型,必须用Mask来让它看不到未来的信息,所以它也不能解决这个问题的

注意:即使ELMo训练了双向的两个RNN,但是一个RNN只能看一个方向,因此也是无法"同时"利用前后两个方向的信息的。也许有的读者会问,我的RNN有很多层,比如第一层的正向RNN在编码it的时候编码了animalstreet的语义,反向RNN编码了tired的语义,然后第二层的RNN就能同时看到这两个语义,然后判断出it指代animal。理论上是有这种可能,但是实际上很难。举个反例,理论上一个三层(一个隐层)的全连接网络能够拟合任何函数,那我们还需要更多层的全连接网络或者CNN、RNN干什么呢?如果数据量不是及其庞大,或者如果不对网络结构做任何约束,那么它有很多种拟合的方法,其中大部分是过拟合的。但是通过对网络结构的约束,比如CNN的局部特效,RNN的时序特效,多层网络的层次结构,对它进行了很多约束,从而使得它能够更好的收敛到最佳的参数。我们研究不同的网络结构(包括Resnet、Dropout、BatchNorm等等)都是为了对网络增加额外的(先验的)约束

下图是从BERT论文中截取的三种模型的对比

参考文章

Last Modified: December 17, 2021
Archives Tip
QR Code for this page
Tipping QR Code
Leave a Comment

26 Comments
  1. BERT的PyTorch实现 – 闪念基因 – 个人技术分享

    [...]本文主要介绍一下如何使用 PyTorch 复现BERT。请先花上 10 分钟阅读我的这篇文章 BERT详解(附带ELMo、GPT介绍)[...]

    1. Juheim Juheim

      @BERT的PyTorch实现 – 闪念基因 – 个人技术分享文中介绍GPT的地方博主好像打错了 GPT应该是多层的transformer decoder
      感谢博主的分享~ 从transformer详解过来的 博主讲的很清晰了~

    2. mathor mathor

      @Juheim已修改,感谢提醒

  2. xiaolan-Lin xiaolan-Lin

    感谢作者分享~

    1. mathor mathor

      @xiaolan-Lin不客气

  3. 柏陆 柏陆

    博主真的太厉害啦,看到之前的评论,博主好像是研一,我和博主一样研一,但是觉得博主的水平至少是博士水平了。我会一直关注博主的网站,多多向优秀的同龄人小伙伴学习!~~~@(哈哈)

    1. mathor mathor

      @柏陆谢谢

  4. 胡图图 胡图图

    大佬,请教一下,为什么bert的最后一个线性层要与embedding用同一组参数呢?是什么动机?

    1. mathor mathor

      @胡图图embedding?线性层?这俩应该八竿子打不着吧,你具体说清楚一点

    2. 胡图图 胡图图

      @mathor就是我看到你bert代码里的一段实现:

      fc2 is shared with embedding layer embed_weight = self.tok_embed.weight self.fc2 = nn.Linear(config.d_model, config.vocab_size, bias=False) self.fc2.weight = embed_weight
    3. mathor mathor

      @胡图图哦,那您下次应该在那篇文章底下进行评论

      关于您提的这个问题,动机说不上,就是随便写,没有什么理由,您也可以不共享参数。如果您要问我哪一种方法好,我只能说实践出真知

    4. 胡图图 胡图图

      @mathor好的好的,再次感谢分享,哈哈哈

  5. NLP小白 NLP小白

    请问为什么GPT里面要用masked- self- attention,预训练的时候看见更多词不是更好吗?是为了照顾微调吗?

    1. mathor mathor

      @NLP小白看见后面还没预测的词相当于作弊了

  6. zzflybird zzflybird

    您好,关于本文中的“GPT 是多层的 transformer encoder”这句话,GPT的原文中是"We trained a 12-layer decoder-only transformer with masked self-attention heads (768 dimensional states and 12 attention heads)."
    不过结合本文后面的“GPT 使用的 Transformer 结构就是将 Encoder 中的 Self-Attention 替换成了 Masked Self-Attention”这一句来看,其实是一个意思,就是GPT的重点是“Masked Self-Attention”,抛开这一点,用Encoder还是Decoder其实差不多。
    最后,看的作者的文章学习的Transformer,现在看到BERT了,发现BERT和GPT分别使用了Transformer的Encoder和Decoder,收获良多,感谢作者!!会持续关注的!

  7. 杨

    博主你好,我很喜欢你的文章和视频。学习了你的BERT介绍我还有些困惑的地方,希望能听一下你的理解,具体问题如下。
    假如我需要做词性预测的任务,即对于输入句子的每个词汇我都需要做一个单独的预测。输入 【she is a girl】,输出可以通过对这个句子中单词的bert编码进行linear分类得到类别预测。那么,当我需要测试的时候,我只想知道一个单词的类别,如boy, 我把这个单词padding成一个句子的长度,即【boy 0 0 0】,那么我能获得一个合理的对boy类别的预测吗? (假设训练集全是大于一个单词长度的句子,测试是单个词汇进行测试)

  8. jie jie

    博主我想问下Fine-Tuning部分写的几个任务,黄色框BERT的输入(eg:case1单句子分类任务当中:[CLS] w1 w2 w3 的embedding)是在BERT_1(两个任务:Masked Language Model 和上下文预测同时训练得到的)提前就训练好得到的,然后只用图中的BERT_2(单任务句子分类)进行的单句子分类任务做训练;还是只使用图中的单任务的BERT_2做训练呢?
    另外就是关于微调,虽然理解,但是代码实现的时候不知怎么做...希望大佬@(滑稽)有时间解答一下

  9. xiayu xiayu

    真的谢谢你

  10. Pytorch implementation of Bert (super detailed) R11; Open Source Biology & Genetics Interest Group

    [...]This article mainly introduces how to use PyTorch Reappear BERT. Please spend it first 10 Minutes to read my article BERT Detailed explanation ( Incidental ELMo、GPT Introduce ), Let’s look [...]

  11. 哥依然潇洒 哥依然潇洒

    太厉害了

    1. 年小糕 年小糕

      @哥依然潇洒确实@(哈哈)

  12. TransformNoob TransformNoob

    感谢分享,想请教一下SEP token在BERT里面起什么作用?code是怎样处理SEPtoken的呢?第二个问题是您是怎么理解segment embedding的?segment embedding只是用0 和 1来区分两个句子并简单的和其他embedding加到一起的。

    1. mathor mathor

      @TransformNoob作用就只是单纯分隔两个句子

  13. 姚

    好精致的评论功能,这是一条测试评论,如有打搅多有冒犯

  14. 姚不可及 姚不可及

    博主你好,我想问一下关于使用BERT做分类的问题。我们在对BERT做预训练时输入的是两个句子拼接成的长句子token,而做分类任务时输入的内容其实只有一个句子,这里的输入其实只有训练时输入的一半吧?这个要怎么解决呢

  15. 安德安德鲁 安德安德鲁

    作者写的太好了呜呜呜,把NLP的具体任务实现写的特别好,对我BERT的理解有非常大的帮助