引言

  自从Google于2017年发布一个十分强大的自注意力机制模型Transformer之后,NLP在各大应用领域的应用也变得更加广泛,其性能也有了很大的改进。如今已到2021年,我们带领大家通过本文来彻头彻尾地总结一下Transformer以及BERT中的所有知识点。本文既适用于在AI或深度学习领域有一定经验的行家们,来帮他们系统地重温Self-Attention的基础知识,也适用于刚入行这一领域打算从事相关工作或科研的新人们。

  同时,作者也希望本技术贴能够成为Transformer完整的“说明书”,在你们需要查阅相关的技术细节以及概念时,都能在文章中找到。

  本文是将网络上大部分介绍Tramsformer相关知识的博客和技术贴来一次整合,在文字上都以严谨而易懂的方式重新进行了讲述,因此如果读者之前在学习的时候如果还有不懂的地方,或许在看了这篇文章后,会茅塞顿开。由于截图是参考网络上分享的,因此在文章的最后我们附上了图片的来源链接,供读者参考。

  也许你会问,Transformer和BERT都发布好几年了,现在还要去介绍它,有那个必要吗?作为从事NLP领域已达4年时间的人,我告诉你还是十分有必要。原因如下:一、虽然Transformer在2017年就已经出现,现在有很多后起之秀不断超越它,如GPT 3.0等,但无疑都是在前者基础上做的改进,核心思想和技术都还要回归到Transformer和BERT中;二、Transformer和BERT在NLP中所带来的影响力至今犹存,很多研究成果也都是在此基础所做,由此可见其强大之处;三、Transformer和BERT用到了很多史无前例的思想、算法和方法,这对我们的研究和工作无疑有着引导和启发的价值。因此我要用一篇3万多字的长文,来为大家全面解读一下Transformer和BERT。话不多说,我们开始正文。

一、Encoder-Decoder

1.1 Encoder-Decoder介绍

  Encoder-Decoder即编码器-解码器模型机制,其内部包含了很多神经网络的复杂计算,最终输出的也是一个有意义的序列。在训练学习的意义上,Encoder实现了模型对输入序列的理解,Decoder利用模型学到的先验知识,实现了对某个具体任务的单步预测并输出。根据不同的任务我们可以设计出不同的序列。举个例子,如果我们的任务是机器翻译,则我们可以在编码器的输入端以源语言句子作为输入,通过一定的训练和调参,可以使得解码端输出目标语言的准确译句。

  以下是Encoder-Decoder机制的总体结构(以翻译任务为例):
在这里插入图片描述
在这里插入图片描述

  Encoder和Decoder可以包含RNN、Bi-RNN、LSTM、Bi-LSTM等循环神经网络,当然也可以是Transformer结构。

1.2 Transformer与Encoder-Decoder

  Google在2017年介绍Transformer的论文《Attention is All You Need》中首次提出了Transformer以及由Transformer组成的Encoder-Decoder结构。接下来,我们一睹该形式是怎么构成以及如何工作的,如下图:
在这里插入图片描述
  不难看出,Transformer组成的编解码器分为两列结构,左列即为编码器(Encoder),右列即为解码器(Decoder)。左列的最终输出作为右列Multi-Head Attention模块的输入。

  Google在论文中将编码器和解码器分别增加到6层(如下图),以便于更充分地提取到序列的语义信息和深层关联。所有的编码器在结构上都是相同的,它们之间可以认为是叠加关系,中间层的每一个Encoder都以上层Encoder的输出作为输入,且其输出也成为下一个Encoder的输入。第一层Encoder的输入为Embedding值,最后一层Encoder的输出是作为Decoder的输入。对于Decoder而言,它同Encoder的不同之处在于其输入内容除了上一层Decoder的输出信息(第一层Decoder除外),在每一个Decoder中间子层的Attention机制中还包含了最后一层Encoder的输入。我们后续会介绍到这个细节。
在这里插入图片描述
  我们来进一步介绍,首先看如下两张图,不难看出编码器结构的主要组成部分是自注意力机制加上前馈神经网络,而解码器机制是自注意力机制、编码-解码注意力机制再加上前馈神经网络。
在这里插入图片描述
在这里插入图片描述
  为了方便读者的理解,我们在Google的原始架构中标记了Encoder-Decoder中相关网络层的位置,并给出说明,如下图:
在这里插入图片描述
  我们能够看出,数据在通过Multi-Head注意力机制和前馈神经网络(Feed Forward)运算并输出后均使用了Add&Norm操作;在解码器中,前期多出了一个Masked Multi-Head注意力机制,在训过程中每一次Decoder运算开始时,Embedding需首先进入到Masked注意力机制来实现掩盖式编码,这里的相关细节后续会详细介绍,随后其输出同Encoder的输出一起进入到正常的Multi-Head注意力机制进行联合再编码。

  以上我们学习了Transformer的Encoder-Decoder总体架构及其基本模块组成。我们不难看出该结构在执行训练的过程中主要依赖的就是Attention机制,接下来我们就介绍让Transformer如此强大的两项关键技术,即融合位置信息的Embedding和Self-Attention机制。

二、Transformer

2.1 融合位置信息的Embedding

2.1.1 Word Embedding

  Word Embedding是每一项NLP任务的基础,它的目的是将输入序列转换为数学上可以进行线性变换、点乘或函数映射等操作的数值形式,使模型能够输出有意义的信息。通常情况下Word Embedding将输入的各个词映射到一个固定维度的欧式向量空间中,其中含义相似的字符或单词向量之间的距离也相近(这里距离的度量也分很多种)。并且不同的词语所对应的Embedding也是唯一的。

  在汉语中,由于句子是以文字为单元的,因此不容易表示出Word Embedding,由此汉语的Embedding输入一般是以文字为单位映射的,也就是Character Embedding。除特别说明外,本文所指的Embedding均指Character Embedding.

  举个例子,以汉语为例,假设待处理的数据集包含10,000个不同的汉字,定义Embedding的维度为512维,则我们可以得出一个随机生成的由汉字字嵌入向量构成的Embedding矩阵,如下图:在这里插入图片描述
  注:表中的Embedding初始化方式是通过Xavier方法生成的,该方法生成的向量服从特定参数下的均匀分布。

  由图中看出,表的左侧两列中,word列为所有的字符列表,num列为字符所对应的编号,原始文字序列在被读取后首先转换为对应的num值,随后通过映射准确找到该num值在Embedding矩阵中对应的向量,得到相应的初始编码。Transformer中输入的初始Embedding一般随机生成即可,无需考虑距离、相似度等信息。

2.1.2 Transformer中的位置编码

  接下来我们介绍一下在Transformer中有一个独具特色的编码方式—位置编码。首先我们来解释一下为什么在Transformer中要用到位置编码信息。我们思考一下,在汉语中一个字或词,在不同的位置下含义是否不一样呢?举个例子:柏拉图是亚里士多德的老师,苏格拉底是柏拉图的老师。这里“柏拉图”、“亚里士多德”和“苏格拉底”三个命名实体的位置就决定了整个句子的语义,如果我们把句子中的任何一个“是”的左右命名实体调换位置,都会造成语义的改变而导致模型知识理解出错。因此在Transformer中位置编码就变得极为重要了。

  那么另外一个问题就来了,为什么在传统的循环神经网络和卷积神经网络中没有加入位置编码呢?因为循环神经网络读取数据是按序列的顺序读取的,一般情况下都是从序列的一端开始向另一端按时间步读取,因此每一个时间步所对应的cell中已经包含了和位置有关的信息;而在卷积神经网络中,虽然数据读取的方式是并行的,没有时间先后的概念,但是卷积操作基本保留了原序列的先后顺序,因此对后续的进一步特征提取和语义理解造成的影响并不大。因此在上述两种结构中,一般情况下没有必要加入位置编码信息。

  因此位置编码信息在Transformer中就变得很重要了,需要设计一套位置编码算法,来清晰地体现出每一个token的位置信息。由此Transformer中给出了一种位置编码策略,我们介绍Transformer中的位置编码如何实现。首先在处理一个原始序列时,我们对序列中的每一个汉字首先用编号来编码,形式为 ( 0 , 1 , 2 , . . . , N ) (0,1,2,...,N) (0,1,2,...,N),其中N为句子长度减1,其次我们取每一个汉字的Embeddding维度,这里假设为512维,对每一维中的数据分别按以下公式进行位置编码:
P E ( p o s , 2 i ) = s i n ( p o s / 1000 0 2 i / d m o d e l ) P E ( p o s , 2 i + 1 ) = c o s ( p o s / 1000 0 2 i / d m o d e l ) PE(pos, 2i)=sin(pos/{10000^{2i/{d_{model}}}})\\ PE(pos, 2i+1)=cos(pos/{10000^{2i/{d_{model}}}})\\ PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)
  下表可以清楚看出不同位置、不同维度的编码方法:
在这里插入图片描述
  这里我们附上一张编码效果图来帮助读者理解,图中序列长度为10,维度设置为64维,采用上述方法实现位置编码:
在这里插入图片描述
  有些文章使用的编码方法和上文的有出入之处,上文的方法是在奇偶的维度位置上分别使用cos、sin函数,而有些做法在是前0~255维统一使用了sin函数,后256~511维使用了cos函数。我们看下效果,首先展示待编码的前三个单词的Position Embedding内容(假设维度为4维):
在这里插入图片描述
  接下来展示一个20个词、维度为512的输入序列位置编码效果图:
在这里插入图片描述
  图片以行输出每一个字符的位置编码,因此左列标签为不同字符的编号,底部标签为同一个字符的不同维度。右侧的颜色条可以清晰地表示出值的大小和差异,我们从效果图中可以看出,序列的位置编码值按序列编号的增加呈较为连续、均匀的变化。

2.1.3 Embedding with Positional Encoding

  在得出原始的映射Embedding和位置编码Embedding后,最终的Embedding值就可以为这两个Embedding的点相加,如下图:
在这里插入图片描述
  图中“基于时间步的词嵌入”即最终的编码。我们再给出一个输入序列为英文的参考图,如下:
在这里插入图片描述
  其中Token Embeddings是token向量,这里的token表示的是单词的预处理形式和标记(下文会讲到)。图中的预处理是动词、名词原形等基本不做改变,而动名词形式(后缀ing)或过去分词形式(后缀ed)要拆分为原形部分和后缀部分;Segment Embeddings作为两个句子的分界标志,是便于模型能够区分出不同的句子;Position Embeddings可以取上文的算法来计算。

  对于中文而言,token一般用单个汉字来填充,无需采取任何预处理流程。Transformer的中文输入一般只需先进行语料清洗、去除无效字符,再以文字为单位做Embedding。

  这里我们来介绍一下输入序列中的标记符(如上图的[CLS]、[SEP]),它们在不同的任务中都代表了不同的作用。[SEP]为不同句子的区分符,代表句子之间的边界;而[CLS]代表句子的类别标识,用于做句子分类任务,具体如何实现下文会讲。

2.1.4 和角公式与位置编码的意义

  本小节我们来探讨上述位置编码的计算公式同和角函数的关系,进而引出该算法取得的位置编码之间存在的线性关系,由此来证明Transformer的位置编码使原始token具备了绝对位置和相对位置的特征。首先我们给出高中数学的知识—和角公式:
s i n ( α + β ) = s i n α ⋅ c o s β + c o s α ⋅ s i n β c o s ( α + β ) = c o s α ⋅ c o s β − s i n α ⋅ s i n β sin(\alpha+\beta)=sin\alpha\cdot cos\beta+cos\alpha\cdot sin\beta\\ cos(\alpha+\beta)=cos\alpha\cdot cos\beta-sin\alpha\cdot sin\beta sin(α+β)=sinαcosβ+cosαsinβcos(α+β)=cosαcosβsinαsinβ
  我们用三角函数原理来解释位置编码的有效性,假设位置pos的字符位置编码为:
P E ( p o s , 2 i ) PE_{(pos, 2_i)} PE(pos,2i)
  那么位于 p o s + k pos+k pos+k上的位置编码可以用和角公式表示为:
P E ( p o s + k , 2 i ) = s i n ( w i ⋅ ( p o s + k ) ) = s i n ( w i ⋅ p o s ) c o s ( w i k ) + c o s ( w i ⋅ p o s ) s i n ( w i k ) P E ( p o s + k , 2 i + 1 ) = c o s ( w i ⋅ ( p o s + k ) ) = c o s ( w i ⋅ p o s ) c o s ( w i k ) − s i n ( w i ⋅ p o s ) s i n ( w i k ) PE_{(pos+k, 2_i)}=sin(w_i\cdot (pos+k))=sin(w_i\cdot pos)cos(w_ik)+cos(w_i\cdot pos)sin(w_ik)\\ PE_{(pos+k, 2_i+1)}=cos(w_i\cdot (pos+k))=cos(w_i\cdot pos)cos(w_ik)-sin(w_i\cdot pos)sin(w_ik) PE(pos+k,2i)=sin(wi(pos+k))=sin(wipos)cos(wik)+cos(wipos)sin(wik)PE(pos+k,2i+1)=cos(wi(pos+k))=cos(wipos)cos(wik)sin(wipos)sin(wik)
  其中
w i = 1 / 1000 0 2 i / d m o d e l w_i=1/{10000^{2i/{d_{model}}}} wi=1/100002i/dmodel
  进一步调整上述公式,得出以下变形:
P E ( p o s + k , 2 i ) = P E ( p o s , 2 i ) c o s ( w i k ) + P E ( p o s , 2 i + 1 ) s i n ( w i k ) P E ( p o s + k , 2 i + 1 ) = P E ( p o s , 2 i + 1 ) c o s ( w i k ) − P E ( p o s , 2 i ) s i n ( w i k ) PE_{(pos+k, 2_i)}=PE_{(pos, 2_i)}cos(w_ik)+PE_{(pos, 2_i+1)}sin(w_ik)\\ PE_{(pos+k, 2_i+1)}=PE_{(pos, 2_i+1)}cos(w_ik)-PE_{(pos, 2_i)}sin(w_ik) PE(pos+k,2i)=PE(pos,2i)cos(wik)+PE(pos,2i+1)sin(wik)PE(pos+k,2i+1)=PE(pos,2i+1)cos(wik)PE(pos,2i)sin(wik)
  将上式的表达进行矩阵变换如下:
( P E ( p o s + k , 2 i ) P E ( p o s + k , 2 i + 1 ) ) = ( u v − v u ) ( P E ( p o s , 2 i ) P E ( p o s , 2 i + 1 ) ) \begin{pmatrix} PE_{(pos+k, 2_i)} \\ PE_{(pos+k, 2_i+1)} \end{pmatrix}=\begin{pmatrix} u&v \\ -v&u \end{pmatrix}\begin{pmatrix} PE_{(pos, 2_i)} \\ PE_{(pos, 2_i+1)} \end{pmatrix} (PE(pos+k,2i)PE(pos+k,2i+1))=(uvvu)(PE(pos,2i)PE(pos,2i+1))
  其中 u = c o s ( w i ⋅ k ) u=cos(w_i\cdot k) u=cos(wik) v = s i n ( w i ⋅ k ) v=sin(w_i\cdot k) v=sin(wik) w i = 1 / 1000 0 2 i / d m o d e l w_i=1/{10000^{2i/{d_{model}}}} wi=1/100002i/dmodel.

  可知 w i w_i wi为常数,因为 2 i 2i 2i d m o d e l d_{model} dmodel在每一次运算中都是不变的,因此距离的改变仅和k的大小有关。由此可知在任何一个维度上, P E p o s PE_{pos} PEpos P E p o s + k PE_{pos+k} PEpos+k之间都可以互相线性表示,也就使得Transformer中位置编码算法取得的编码值既可以表达各个字符的绝对位置,它们之间也能表示出相对关系,其中绝对位置可以通过编码值来判定,相对位置可以通过编码值之间的线性表示来判断。这些信息都能够在后续的注意力计算中当作特征被考虑。

  这里有一个读者问的较多的问题,那就是如果将token的Embedding同其位置Embedding直接相加成一个向量,会不会造成信息混淆,模型能否区分出来呢?同时会不会和其它不同token的Embedding互相冲突或相似从而影响模型判别?这里我们给出解释:当向量空间足够大,维度很高的时候,Embedding向量之间发生冲突的概率极其小。我们举个例子,假设有个向量空间,维度为128维,每一个元素规定只能取0或1,则这个向量空间会有 2 128 2^{128} 2128种不同的向量元素,这个数量是极其庞大的。如果我们假设常用汉字有20,000个,且输入序列长度512,每一个汉字都能出现在序列的任意个位置(忽略标识符),那么会有10,240,000个不同的向量,相比起 2 128 2^{128} 2128这个庞大的数字它已经很小了,所以发生冲突或近似的概率很低。而且实际应用中Embedding大小一般都为512,且每一维数字都是分布在-1~1之间的浮点数,在这样的表示空间中,即便产出上千万个融合位置的Embedding向量其分布也是很稀疏的,模型可以很容易通过映射找出它们的异同。所以我们完全不必担心冲突或近似情况的发生。

  由此,我们现在总结一下在Transformer处理一个序列时,对于字符的位置判断可以做到如下几点:

  1. 分辨出在不同的位置的同一个token;
  2. 分辨出在相同的位置的不同token;
  3. 分辨出两个token的先后位置关系;
  4. 大致判断两个token之间的距离间隔。

  上述的位置相关信息在模型做映射和推断时都会被考虑。到目前为止我们介绍了Transformer对位置编码的算法,其实位置编码算法有很多种,甚至读者可以自行设计,只要对自己的任务有帮助意义就可以。通常一个好的位置编码算法需要满足以下几点要求:1、编码后的值可以表达出原token在序列中的位置先后,再严格一点就是符合上文Transformer对字符位置判断时的四个准则;2、间隔相同的两个token的位置编码之差不因其在序列中的位置而改变太大(比如序列长度是512,POS(5) - POS(0)同POS(511) - POS(506)差距不能太大)。

2.2 Self-Attention机制

  Self-Attention机制可谓是Transformer的核心部分了,我们在Encoder-Decoder部分中就能看出该机制应用较为频繁,基本成为了数据流编码、解码的重要环节。本章节我们来详细介绍一下该神奇结构的工作原理。我们会从两个不同的思路带读者了解它的处理流程,3.1章节和3.2章节都介绍了Self-Attention的运算过程。其中3.1从向量运算的角度来阐述,从每一个token的细节角度上为读者描述运算过程,然后推广到矩阵的运算来引出Multi-Head Attention;而3.2直接从矩阵的切分、变换和运算来阐述。两种思路方便读者进行参考。

  首先简单介绍一下Self-Attention机制的意义,有下列例句:

The animal didn’t cross the street because it was too tired

  我们只要具备一定的语言知识基础,都能明白句子中的“it”代表着前面的“animal”,因此我们在阅读这句话时,当读到“it”时注意力就会偏重于“animal”,这样就极大的帮助了我们理解这句话的语义。而在模型中的注意力机制亦是如此,模型读取到当前词时,通过一定的运算,将该词同输入句子中其他词分别计算权重系数,权重越大表示模型认为当前词和该词的关联性越大。比如上述的例句,我们希望模型在读取到“it”时,能够为前文的“animal”分配最大的权重。这就是Self-Attention机制的基本概念。

2.2.1 向量运算的角度

  Attention机制从生成三个基本向量开始:q(Query), k(Key)和v(Value),分别为3个分布在欧式空间中的向量,这三个向量都是由输入的Embedding生成,方法是将Embedding分别同三个权重矩阵 W q , W k , W v W_q,W_k,W_v Wq,Wk,Wv相乘。

  举个例子,假设输入为 ( X 1 , X 2 , . . . X n ) (X_1,X_2,...X_n) (X1,X2,...Xn),则执行矩阵相乘后分别得到的向量为 ( [ q 1 , k 1 , v 1 ] , [ q 2 , k 2 , v 2 ] , . . . , [ q n , k n , v n ] ) , ([q_1,k_1,v_1],[q_2,k_2,v_2],...,[q_n,k_n,v_n]), ([q1,k1,v1],[q2,k2,v2],...,[qn,kn,vn]),,其中 q 1 = X 1 W q , k 1 = X 1 W k , v 1 = X 1 W v q_1=X_1W_q,k_1=X_1W_k,v_1=X_1W_v q1=X1Wq,k1=X1Wk,v1=X1Wv,后面与其类似。可以参考下图来理解:
在这里插入图片描述
  值得注意的是,新向量 q i , k i , v i q_i,k_i,v_i qi,ki,vi的维度一般都小于原Embedding的维度。在Google的《Attention is all you need》论文中,Embedding的维度为512,得到的三个新向量变成了64维,由此可以得出 W q , W k , W v W_q,W_k,W_v Wq,Wk,Wv三个矩阵的维度均为512×64.

  接下来,我们开始计算打分。举个例子,假设我们要计算图片中序列的第一个单词 “Thinking” 对序列中任何其他单词之间的关联度,就要通过使用 “Thinking”的q向量分别乘以每个单词的k向量来计算内积,得出n个score值(包括对Thinking自己也要计算),如下所示:
( s c o r e 1 , 1 , s c o r e 1 , 2 , . . . , s c o r e 1 , n ) = ( q 1 T ⋅ k 1 , q 1 T ⋅ k 2 , . . . , q 1 T ⋅ k n ) (score_{1,1},score_{1,2},...,score_{1,n})=(q_1^T\cdot k_1,q_1^T\cdot k_2,...,q_1^T\cdot k_n) (score1,1,score1,2,...,score1,n)=(q1Tk1,q1Tk2,...,q1Tkn)
  读者可以参考下图来帮助理解:
在这里插入图片描述
  随后将每一个得分进行放缩处理,方法是将得分除以k向量维数的平方根(本例即为 64 = 8 \sqrt{64}=8 64 =8),执行放缩的原因是防止分值过大造成梯度不稳定,除数也可以取其他值。
( s c o r e 1 , 1 , s c o r e 1 , 2 , . . . , s c o r e 1 , n ) / 8 = ( s c a l e d _ s c o r e 1 , 1 , s c a l e d _ s c o r e 1 , 2 , . . . , s c a l e d _ s c o r e 1 , n ) (score_{1,1},score_{1,2},...,score_{1,n})/8=\\(scaled\_score_{1,1},scaled\_score_{1,2},...,scaled\_score_{1,n}) (score1,1,score1,2,...,score1,n)/8=(scaled_score1,1,scaled_score1,2,...,scaled_score1,n)
  得出放缩后的分数值列表后,执行softmax归一化操作:
s o f t m a x ( s c a l e d _ s c o r e 1 , j ) = s c a l e d _ s c o r e 1 , j ∑ i = 1 n s c a l e d _ s c o r e 1 , n softmax(scaled\_score_{1,j})=\frac {scaled\_score_{1,j}}{\sum_{i=1}^n scaled\_score_{1,n}} softmax(scaled_score1,j)=i=1nscaled_score1,nscaled_score1,j
  这个 softmax 分数决定了序列中每个单词对该单词的关联程度。我们可以参考下图来理解:
在这里插入图片描述
  接下来我们用当前单词同各个位置上单词的得分与每个单词的value向量做乘法运算,并计算所有结果的总和 z 1 z_1 z1,这个总和便是元素 x 1 x_1 x1经过Attention编码后的加权和。公式如下:
z 1 = ∑ i = 1 n s c a l e d _ s c o r e 1 , i ⋅ v i z_1=\sum_{i=1}^n scaled\_score_{1,i}\cdot v_i z1=i=1nscaled_score1,ivi
  同时 z 1 z_1 z1也成为了单词“Thinking”在本层Self-Attention的输出。可以参考下图来帮助理解:在这里插入图片描述
  至此,我们已经详细介绍了单个Self-Attention机制以向量运算为视角的处理过程。下面从矩阵相乘角度给出整个输入序列在Attention层运算的过程:
在这里插入图片描述

  其中X矩阵中的每一行对应于输入句子中每一个单词的Embedding向量。即矩阵X的维度是n×512,其中n为输入序列的长度,通过将X分别同三个W矩阵相乘,得到的Q、K、V矩阵的行向量便是每一个单词的q、k、v向量。接下来展示计算score值并归一化等后续步骤的矩阵运算形式:
在这里插入图片描述
  请读者根据前文所讲的细节,思考上图来领会Attention的运算过程。

2.2.2 Multi-Head Attention

  上文为读者详细介绍了Tramsformer中Attention机制的工作原理,但在实际的业务场景中一次Encoder或Decoder需要多次执行Attention操作,并且每一次执行时都要用到不同的 W q , W k , W v W_q,W_k,W_v Wq,Wk,Wv,这就产生了Multi-Head Attention的思想,该思想是初始化N组W矩阵来对输入向量做N次Attention运算。多头注意力机制可以映射注意力层的多个表示子空间,是注意力机制的一种强化。这里我们假设N=8,来目睹一下多头注意力机制的运算过程。在这里插入图片描述
  上图为输入的向量组X以及多重 W q , W k , W v W_q,W_k,W_v Wq,Wk,Wv权重矩阵(图上只列出了两组),X中的向量都要通过这8组矩阵映射为不同的q、k、v向量,并分别进行score计算、求和等操作,每一个输入向量最终都会得到8个输出向量z,整个输入序列就拼接为8个输出矩阵 z 0 , z 1 , . . . , z 7 z_0,z_1,...,z_7 z0,z1,...,z7. 如下图:
在这里插入图片描述
  随后用拼接将这些矩阵合成一个较大的矩阵,再同另一个权重矩阵 W O W^O WO相乘。最终输出Z矩阵,其每一行即为对应的输入Embedding的输出,向量维度保持一致。具体步骤如下图所示:
在这里插入图片描述
  这样Multi-Headed Self-Attention的操作就基本介绍完了。现在咱们用一个框图来表示整个计算的过程,便于加深理解:
在这里插入图片描述
  注:对于最后输出的 z 0 , . . . , z 7 z_0,...,z_7 z0,...,z7拼接起来后是否做线性变换的问题,要取决于向量z是否和向量x维度相同,如果按照3.1.1的形式则不必线性变换(输入维度为512,做矩阵变换后的q、k、v向量维度为64,执行8头注意力机制再拼接后维度恰好又变为512)。

2.2.3 矩阵变换的角度

  接下来,我们直接通过矩阵的一系列切分、重组、乘积等操作来学习Attention机制的运算过程,本节对刚入门Transformer的读者可能要难以理解一些,因为从矩阵的视角不太容易看出单个向量的处理过程。话不多说,我们开始介绍。

  同上一节,我们首先仍然需要一组权重矩阵 W q , W k , W v W_q,W_k,W_v Wq,Wk,Wv来分别对输入进行变换,只是不同的是这次的 W q , W k , W v W_q,W_k,W_v Wq,Wk,Wv矩阵维度都是固定的 d m o d e l × d m o d e l d_{model}×d_{model} dmodel×dmodel,即都是方阵,维度和输入维度相同。(注:这里可以使用激活函数来重新调整变换后矩阵的值,如果使用激活函数则相当于将原输入分别通过三个单层神经网络运算处理)

  接下来希望读者跟随思路,因为这里会有很大的不同。我们假设上述的输入维度为512,输入序列的长度为10,得到上文的矩阵后,取出其中的Q矩阵,其维度为10×512. 我们在512的维度上将矩阵切分为八组,并且将8个子矩阵由横向变为纵向排列,效果如下:
在这里插入图片描述
  同样,我们对K、V矩阵也做这样的操作。

  然后,我们将重新排列的K矩阵的8个子矩阵分别进行转置,即原来每个子矩阵是10行64列,转置后变为64行10列,转置后的K矩阵记为K’. 随后我们用Q中的8个子矩阵分别同K’中对应位置的子矩阵做相乘操作,得出Outputs结果矩阵。如下图:在这里插入图片描述
  我们对结果矩阵Outputs的每一行执行softmax计算,确保行元素归一化。并将归一化后的Outputs同V中的子矩阵同样对应相乘,如下图:在这里插入图片描述
  最后将新的Outputs矩阵的8个子矩阵重新复原成横向的排列,并拼接成一个大矩阵,维度为10×512. 如下图:
在这里插入图片描述
  通过上述方法,我们可以得到上一节中类似的矩阵输出。如果我们把上一节多头注意力部分所有的 W q , W k , W v W_q,W_k,W_v Wq,Wk,Wv维度设置为512×64,则操作流程就会和本节基本相同。接下来我们分析本节中的每一步,并解释相关操作。

  1. 用输入的Embedding分别乘以方阵 W q , W k , W v W_q,W_k,W_v Wq,Wk,Wv,目的是为多头注意力机制作准备。在多头注意力机制中将Embedding分别乘以多组矩阵来分别得到不同的Q、K、V矩阵,在此将Embedding直接在一个大矩阵中统一变换,其实道理都是相同的。
  2. 将变换得到的三个矩阵切分成8个子矩阵并重新纵向排列,随后用重排的Q矩阵的每一个子矩阵乘以重排并转置的K矩阵中对应位置的子矩阵,这等同于将Q矩阵中的每一个元素的Query值分别同相同的注意力头内的每一个元素的Key值相乘。所有的子矩阵都运算完后便实现了多头计算操作。
  3. 对Outputs以行做softmax,相当于第一节中的将 s c a l e d _ s c o r e scaled\_score scaled_score做softmax归一化,因此Outputs的每一行为当前元素对所有输入元素的权值序列;
  4. 将Outputs同V进行对应子矩阵的相乘,目的是根据Outputs每一行的权重序列分别和V矩阵的每一个对应元素相乘,最后得到每一个v向量的加权和;
  5. 最终的还原排列并重新拼接的操作,对应了上节中对 z 0 , . . . , z 7 z_0,...,z_7 z0,...,z7进行拼接的操作。

  我们已经学习了多头Attention是如何从单头操作中扩展来的,总结起来就是利用输入向量做多组Self-Attention操作,每一组操作都有单独的 W q , W k , W v W_q,W_k,W_v Wq,Wk,Wv矩阵,并且计算点积、归一化、计算加权和这一系列过程也都是在各组操作的内部进行的,最后的结果是把每组操作所得到的output值拼接起来,再做一次线性变换操作。我们列出原论文中的多头Attention的运算公式如下:
M u l t i H e a d ( Q , K , V ) = C o n c a t ( h e a d 1 , h e a d 2 , . . . , h e a d h ) W O where  h e a d i = A t t e n t i o n ( Q W i Q , K W i K , V W i V ) MultiHead(Q, K, V)=Concat(head_1, head_2, ..., head_h)W^O\\ \text {where }head_i=Attention(QW_i^Q, KW_i^K, VW_i^V) MultiHead(Q,K,V)=Concat(head1,head2,...,headh)WOwhere headi=Attention(QWiQ,KWiK,VWiV)
  这里, Q W i Q , K W i K , V W i V QW_i^Q, KW_i^K, VW_i^V QWiQ,KWiK,VWiV都代表输入序列在三个矩阵上的线性变换,然后进行正常的Attention操作,最终MultiHead将所有结点的Attention输出值拼接,然后选择性的乘以矩阵 W O W^O WO来执行映射。

  现在我们再附上Attention的总体框架图:在这里插入图片描述
Multi-Head Attention下的框架图:
在这里插入图片描述
  通过上述的学习结合框架图,我们已经大致明白多头Attention的运算过程了(Mask结点读者可以暂时忽略,因为该节点仅仅在Decoder中才有,稍后会讲到)。

2.2.4 效果分析

  我们继续用之前的例句来评价Attention的效果。评价的方法为考察例句中的单词在不同的Attention Head下会有怎样的关注点。下图显示了在例句中“it”在不同Attention结点中关注度的分布情况,能够看出其中一个结点的注意力主要集中在“animal”上,而另一个结点注意力集中在“tired”。因此得出的结论是,在本例中,Transformer的注意力机制认为“it”是 “animal”和“tired”的一种表现形式。这也符合了我们日常语言中对该句话的理解,因此Transformer是有效的。
在这里插入图片描述
  现在我们同时展示8个结点中的“it”同序列中所有词的权重大小情况,能够看出他们有不同的偏重点,但是大多数偏重点都有合理之处。除上述列举的外,第四个结点和第七个结点将注意力集中在了动作上(分别是“didn’t”和“cross”),这有助于完成一些需要识别主语动作的任务比如摘要、事件抽取、句子复述等;第六个头和第八个头将权重分配到了一些重要性不高的词如介词、连词中,这有助于模型去忽略这些无关紧要的信息,帮助更有效的理解句子含义。如下两张图所示:
在这里插入图片描述

2.3 Add&Normalization

  Add和Normalization操作也是Transformer中必不可少的,其中Add操作即残差连接,用于将原始信息同模块的输出信息相加。为什么要进行残差连接呢?由于Encoder和Decoder都包含了很多层Transformer机制的叠加,当结构深度较大时,不能确保模块中矩阵的所有参数都能参与到有效的训练中去。也就是说给定不同的输入 x x x,在反向传播过程中某些参数可能不会随损失下降而改变,这使得模块中虽然有着很大的、看似表现能力很好的矩阵,但实际为最终预测带来贡献的参数并不多。且随着训练迭代的增多,这种情况会越发典型。通俗点表达就是,模型也懂得“偷懒”和“走捷径”。

  这类似于算法中的贪心思想,当模型发现仅仅使用一部分参数就能对数据集有效预测时,会慢慢忽视其他参数值。解决这一问题的有效办法是上层输出同其他模块的输出相加后再输入到下层处理,因为不同的输出一般都会遵循不同的分布,将其他模块的输出加入到该模块可以使该模块中矩阵的参数值重新得到使用。

  Normalization则用于调整数据的分布使其一致,同时也避免了梯度消失所带来的收敛变慢问题。原论文中统一使用了Layer Normalization. 其实这里的Layer Normalization即对每一个残差连接后的Attention输出向量作如下操作:对向量中的所有元素求均值 μ \mu μ和方差 σ 2 \sigma^2 σ2,然后将所有的元素作如下变换:

L a y e r N o r m ( x i ) = α ⊙ x i − μ σ 2 + ϵ + β LayerNorm(x_i) =\alpha \odot \frac {x_i-\mu}{\sqrt {\sigma^2+\epsilon}}+\beta LayerNorm(xi)=ασ2+ϵ xiμ+β

  其中 ϵ \epsilon ϵ为平滑项,为防止分母为0而添加; α \alpha α β \beta β都是可训练向量,用于微调正则化后的向量值,以解决因正则化给向量信息带来的偏差或损失问题。 ⊙ \odot 符号为向量的点对点相乘,结果仍然是相同维度的向量。

  Add&Norm公式如下:
O u t p u t a d d e d = S u b l a y e r ( x ) + x x L a y e r _ N o r m a l i z e d = L a y e r N o r m ( O u t p u t a d d e d ) Output_{added}=Sublayer(x)+x\\ x_{Layer\_Normalized}=LayerNorm(Output_{added}) Outputadded=Sublayer(x)+xxLayer_Normalized=LayerNorm(Outputadded)
  上式中 x x x为Input Embedding或上层模块、编解码层的输出,Sublayer即Encoder、Decoder中的MultiHead Attention或Feed Forward等,执行操作即两个向量的点对点相加,并将其结果进行Layer Normalization.

2.4 Feed Forward Neural Network

  原文将其称作“Position-wise Feed-Forward Network”,翻译成中文即“位置全连接前馈神经网络”,即含义是该网络所处理的上层Attention输出是基于位置的值输出。该网络在结构上同感知机类似,处理流程如下:

  • 将上层的输出先进行一次线性变换,使其维度变为原始维度的4倍;
  • 使用ReLU激活函数处理输出;
  • 进行第二次线性变换,并输出结果。

  前馈神经网络的公式如下:

F F N ( x ) = R e L U ( x W 1 + b 1 ) W 2 + b 2 FFN(x)=ReLU(xW_1+b_1)W_2+b_2 FFN(x)=ReLU(xW1+b1)W2+b2
  其中如果x的维度为d,则矩阵 W 1 W_1 W1的维度为d×4d,矩阵 W 2 W_2 W2的维度为4d×d。ReLU函数可以替换做平滑版本GeLU,以确保其结果分布符合一定的正则特性。

2.5 重新探索Encoder-Decoder

2.5.1 Encoder内部结构

  有了上文我们对Transformer中各个模块细节的学习,我们来看一下其编码器和解码器的构成。下图展示一个标准Transformer编码器的内部结构:
在这里插入图片描述
在这里插入图片描述
  可以很清晰地看出,Encoder机制内部是一层多头自注意力层和一层Feed Forward网络的叠加,其中每一层的输出都经过了Add&Norm调整,顶层Add&Norm输出后会将数据传送给下一步迭代时的Encoder作为输入,或者在迭代结束后作为Decoder中间层的输入。那么接下来我们讲解Decoder的结构,读者可以从中看出其与Encoder有何不同。

2.5.2 Decoder内部结构及原理讲解

  同Encoder不同的是,Decoder会在结构的低层部分加入一个称为“Masked Multi-Head Attention”的注意力层,该层主要帮助模型来完句子生成任务(如机器翻译、摘要生成等)。本小节来重点介绍该层结构。

  先给出一个简单的基于RNN的句子生成模型图:

在这里插入图片描述
  其中绿色点和浅蓝色点为seq2seq网络层,比如RNN、LSTM等。绿色点代表接受原序列输入的编码层,浅蓝色点代表解码层,其在输入结束后,以“start”为开始标记,执行生成,每一次生成的数据既用作概率输出,指示当前位置最可能是哪个词,同时也用作下一步解码的输入,直到输出“stop”标记则完成(起始符和结束符可随意设置,有的任务也设置为‘\s’和‘\e’,这个没有硬性规定,因为它仅仅作为一个识别标志,模型以起始标志为信号开始序列生成,生成时遇到结束标记便停止并输出结果。在Bert中仅需要起始符即可)。

  上述便是生成任务中流行的“Teacher-Forcing”思想,其工作原理是:在训练过程的 t t t时刻,使用数据集在当前状态的期望输出 y t y_t yt,作为下一个时间步的输入。这与传统的“Free-Running”思想不同,后者是在训练过程中将当前Decoder层的输出直接作为下一个Decoder的输入。公式如下:

x t + 1 = { D e c o d e r t ( x t ) , Free-Running Mode , y t , Teacher-Forcing Mode . x_{t+1}=\begin{cases} Decoder_{t}(x_t), & \text {Free-Running Mode}, \\ y_t, & \text {Teacher-Forcing Mode}. \end{cases} xt+1={Decodert(xt),yt,Free-Running Mode,Teacher-Forcing Mode.

  “Free-Running”和“Teacher-Forcing”仅仅针对于模型训练而言,对于测试则每一步解码的输入均为上一步的输出,因为测试时是获取不到期望输出的。

  Transformer中的解码器训练时同样采用的是“Teacher-Forcing”模式,因为该模式可以促进Transformer能够完全并行推断每一步的输出。我们在前文学习Attention时,已经在框架图中看到了scale操作后可以加入一个mask操作,且该操作只在Decoder的下层部分时使用。

  那么这个mask操作有何用呢?因为传统的Decoder都以上一步的输出为输入来预测本步输出,而Transformer在训练时直接输入整个序列,因此需要掩盖掉当前词后面位置的所有词,以避免在执行注意力计算时融入这些词的信息,使得最终模型输出当前词时获得一些不应有的“提示”从而很”轻松“地把词给预测了。mask处理方式介绍如下:

  设 m m m为输出序列长度,那么对于第 i i i个词( 1 ≤ i ≤ m ) 1\leq i\leq m) 1im),其相对于序列中任意单词 j j j的Masked Multi-Head Attention计算方式如下:
s c o r e i , j = { q i T k j , if  1 ≤ j ≤ i , N E G A _ M A X _ V A L U E , if  i < j ≤ m . score_{i, j}=\begin{cases} q_i^Tk_j, & \text {if } 1\leq j\leq i, \\ NEGA\_MAX\_VALUE, & \text {if } i <j \leq m. \end{cases} scorei,j={qiTkj,NEGA_MAX_VALUE,if 1ji,if i<jm.
  其中 N E G A _ M A X _ V A L U E NEGA\_MAX\_VALUE NEGA_MAX_VALUE为负极大值,表明训练过程中对于该位置的单词分配最低权重,使得模型无法从中捕捉到相关信息。接下来这张图可以帮助我们理解,图中序列长度为10,通过使用mask矩阵来决定score的值如何获得:
在这里插入图片描述
  Masked Multi-Head Attention输出Output的自注意力编码信息,通过残差连接和归一化后,后面的工作同Encoder类似了,不过这里将Output编码直接作为下一层Attention的q向量,而k、v向量均来自Encoder的最终输出。这里我们给出解释:低层的Attention在经过mask编码后,将output序列的token完成注意力编码以得到output序列充分的关系和语义信息,在同Encoder的输出进行注意力计算时,将取得语义信息的output和同样具备丰富语义信息的input进行分别匹配以计算score,以找出输出序列和输入序列的关系特征,使Attention的输出可以得到涵盖输入和输出的综合表征,以更好地预测output的下一个token.

2.5.3 Decoder训练与预测

  我们在Transformer的Encoder-Decoder框架图中,看到了Decoder顶部的输出结果需要通过Linear和Softmax来调整为logits后才能作为评价依据。在绝大多数任务中的输出通过此方法处理即可,只是Linear的维度设定可能随任务而不同。以句子生成类型的任务(如问答、摘要、翻译等)为例,输出一般都为词表大小的向量形式,向量值表达了当前位置为对应词的概率。后续讲到下游任务迁移时会对输出部分继续探讨。

  我们用图片来说明解码完成后如何输出结果,图片如下所示:

在这里插入图片描述
  图中我们可以看出,在预测“am”时,将Decoder迭代后的最终输出取“am”所在位置的向量,并对其做词表维度的线性变换得到logits向量,归一化后取其概率值较大位置的词作为输出词。

  下面我们来为大家演示Decoder输出一个序列时的性能表现,假设我们要输出序列“I am a student”,首先给出待输出的词表(即整个语言的词表,其中包括了待输出序列的所有词):
在这里插入图片描述
  如果我们选取上述词表来输出,则应该指定Decoder输出后的线性变换维度为6维,即需要通过变换得到一个维度为6的向量。我们还要引入每一个单词的one-hot编码,如下图:

在这里插入图片描述
  该one-hot编码用于对输出做损失函数以实现训练,比如某一步需要输出的是“am”,就将该步的编码结果取“am”对应位置的向量做线性变换至6维,并将其与“am”对应的one-hot做损失。当一个模型未接受训练时,其输出向量会与one-hot参考值相差很大。如下图:
在这里插入图片描述
  如果模型要输出目标序列,则我们在每一步分别取下列one-hot值:
在这里插入图片描述
  通过训练直到模型成功输出目标序列后,每一步的判别概率如下:
在这里插入图片描述
  我们现在梳理一下Decoder执行训练时的步骤(以英汉翻译任务为例):

  1. 将英语和汉语分别用各自的Embedding方式进行初始编码;
  2. 英语部分输入Encoder中执行N轮以完成编码,完成后Encoder环节也结束了,将最终的输出保留用于解码;
  3. 将汉语译文Embedding开头插入起始符(模型框架图中的shifted right便是将序列右移以便于加入起始符),并输入到Decoder的Outputs中;
  4. 执行Masked注意力层,其输出作为Encoder-Decoder注意力的value向量值,而其q、k值均为Encoder的输出,继续执行编码-解码注意力层和Feed Forward;
  5. N轮Decoder迭代结束后将结果做变换、归一化后输出,并计算损失、反向传播进行训练。

  而预测时Decoder的操作同训练类似,只是每一步Decoder的输入为上一步的输出,由此可见执行预测时低层Attention中的mask作用并不大,因为预测时本来就无法获取到后面的信息。注:Decoder训练时Outputs只需要一次编码输入即可,N轮迭代后分别同时对每个位置的预测概率传播训练,因此迭代N轮即可;而在预测时,每一次输入并迭代后只能预测一个位置,并把预测出的值同上一步的输入序列再重新输入到Outputs迭代,如此需要迭代L×N轮方可完成本序列的预测,L为输出序列的长度。

  由此读者应该发现,训练时Decoder低层Attention的mask使得模型可以将完整的输入序列当作“只知道当前及之前位置字符的序列”来处理,就基本等同于预测的每一步输入时低层Attention计算的场景。接下来我们讲解一下Transformer中mask的知识。

2.6 Mask机制及应用

  上文我们讲过,Decoder的低层Attention机制中有一个独有的mask方法,它用来掩盖当前位置未来的词,防止预测时捕获到和它们相关的特征。其实这个mask有一个专有名称,叫Sequence Mask(序列掩盖)。

  在Transformer中,mask的应用并不仅仅于此,在其对输入进行处理时也要用到另一种mask方式,叫做Padding Mask(填充掩盖),什么是Padding Mask呢?上面介绍过Transformer的输入都是以序列的形式处理,这个序列一般都是一到几句话,因此它们的长度往往会有差别。但是Transformer处理单个序列时的长度大小一般固定,往往由算法人员在代码中指定,当输入序列长度大于指定长度时需要截取,并分多次处理;当输入序列小于指定长度时,则需要在序列后通过补充Padding来扩充至指定长度。这个Padding可以通过在原语料中加入识别符号来实现,比如为语料中的每一个待处理序列都指定对应的input_mask序列,其长度为处理长度,内容只包含数字1、0,其中序列的前N个位置均为1,后面均为0,其中N是待处理序列长度。下面是一个例子:
在这里插入图片描述
  当一个input_mask序列中从某个位置开始的部分往后均为0时,在Attention机制中这些位置的score值都会被指定为负极大值(同上文的 N E G A _ M A X _ V A L U E NEGA\_MAX\_VALUE NEGA_MAX_VALUE),以使模型忽略他们。具体算法同Decoder中的mask类似。

  其实读者读到这里会发现,上述两种mask机制原理都是相同的,即都以不使模型关注某个位置开始往后的元素为目的。只是Sequence Mask的掩盖起点会随元素的不同而有所不同,而Padding Mask对序列的任意一个元素掩盖起始位置都是一样的。因此在Decoder低层的Attention中,Sequence Mask基本能够代替Padding Mask,只用前者即可。

三、BERT

  2018年,Google发表的论文《BERT:Pre-training of Deep Bidirectional Transformers for Language Understanding》,首次提出BERT及其预训练任务,该工作在11项NLP任务中取得SOTA结果,从此BERT和预训练模型受到了NLP领域的广泛关注。本章我们来介绍一下BERT的结构及训练相关内容。首先我们用两个章节来回顾一下预训练模型的相关知识。

3.1 预训练模型

  预训练首先通过自监督学习的方式,从大规模的数据中获得一个较为通用的语言模型,其目的通常是映射出一个词在一个特定上下文中的语义表征。预训练模型作为一种迁移学习的应用,通过学习到每一个句子中的词或字符的上下文相关的表示,使其具备了通用的语法甚至是深层语义的知识。而对于具体的目标任务,预训练模型又具备着强大的迁移能力,通过在具体任务中不断地对网络参数进一步微调、修正,来实现针对下游任务如分类、序列标注、关系判断以及阅读理解等任务的应用。

  预训练模型总共包含三个关键技术:1、Transformer注意力机制;2、自监督学习;3、微调技术。注意力机制上文已经讲过,接下来简单介绍一下自监督学习技术,其中包括两种模型预训练方法:自回归方法和自编码方法。

3.1.1 自回归语言模型

  在预训练模型中,自回归(Auto-Regressive,简称AR)语言模型和自编码(Auto-Encoding,简称AE)语言模型是最常用的自监督学习方法。其中,AR语言模型旨在利用前面的词序列预测下个词的出现概率,例如,给定一个输入序列 X = ( x 1 , . . . , x T ) X=(x_1,...,x_T) X=(x1,...,xT),AR模型的训练目标是最大化极大似然估计 P ( X ) = ∏ t = 1 T p ( x t ∣ x 1 , . . . , x t − 1 ) P(X)=\prod^T_{t=1}p(x_t|x_1,...,x_{t-1}) P(X)=t=1Tp(xtx1,...,xt1),即已知 x t x_t xt之前的序列来预测 x t x_t xt的概率模型,有些算法也可以通过 x t x_t xt之后的序列来逆向预测当前值。

  当今比较有代表性的AR预训练模型有GPT、GPT2.0、GPT3.0、ELMo和XLNet等。

  下图是AR语言模型的简单描述:
AR语言模型学习过程

  • 优点:类似于RNN和LSTM等输入和输出均为序列的神经网路,AR语言模型更擅长生成任务,例如问答系统、机器翻译或复述生成等。
  • 缺点:正如上文所介绍,AR模型只能从一个方向来对当前词进行预测(前向或后向),而无法同时获取两个方向的上下文信息。

3.1.2 自编码语言模型

  自编码(Auto-Encoding)语言模型则同时可以获取到上下文的信息。给定一个序列 X = ( x 1 , . . . , x T ) X=(x_1,...,x_T) X=(x1,...,xT),AE模型的训练目标是最大化似然估计 P ( X ) = ∏ t = 1 T p ( x t ∣ x 1 , . . . , x t − 1 , x t + 1 , . . . , x T ) P(X)=\prod^T_{t=1}p(x_t|x_1,...,x_{t-1},x_{t+1},...,x_T) P(X)=t=1Tp(xtx1,...,xt1,xt+1,...,xT)。自编码模型的训练任务也与自回归模型不同,前者在训练时多以预测掩盖词、判断句子关系等做任务目标。

  比较有代表性的例子是BERT及其改进版如ALBERT、RoBERTa等。
在这里插入图片描述

  • 优点:能够同时获取来自上下文两个方向的信息,有助于对当前词进行预测。
  • 缺点:由于AE模型多以预测掩盖词、判断句子关联等任务做训练,因此训练时的训练目标较多关注了输出向量的某个维度,使得该类模型在生成任务上不如AR模型;其次就是AE模型会造成预训练任务和下游任务的差异。

3.2 BERT预训练模型

3.2.1 BERT介绍

  不同于之前的神经网络模型,BERT在网络结构上抛弃了传统的循环、卷积等结构,而仅仅使用了Transformer来构建,Transformer的Attention机制有效解决了序列中元素距离上的依赖,使元素之间无论间隔多大,都能通过注意力计算来建立联系,这无疑提高了模型充分捕捉句子含义的能力。BERT在结构上属于双向结构,这点不同于GPT和ELMo,下图反映了它们之间的差异:
在这里插入图片描述
  由上图的BERT结构不难看出,它是不具备Encoder-Decoder机制的,而仅仅使用L层Transformer Encoder进行全连接(图中简化为两层),每层有N个Transformer,运算完后输出结果。这种全连接结构使得BERT能够在最终输出时,将每一层中每个结点的特征都能考虑并融入。一开始官方为BERT发布了两个版本:BERT_BASE和 BERT_LARGE,以下是对版本的介绍:
在这里插入图片描述
  其中L为Transformer层数即深度,H代表输出的维度,A表示Multi-Head Attention中Head的数量。最终输出如何处理同样依任务目标而定,从参数量中可以看出BERT是一个规模庞大的模型。下图反映了BERT的预训练时间成本:
在这里插入图片描述

3.2.2 预训练任务

  BERT的预训练过程分两大任务:预测掩盖词(Masked Language Model)和句子关系判断(Next Sentence Prediction),接下来我们讲解它们的细节。

  • 预测掩盖词(Masked Language Model):

  Masked Language Model(简称MLM)任务就是将一个句子中的某些词掩盖成mask标记(注意此处的mask与上文讲过的Padding Mask和Sequence Mask毫无关联,读者不必将它们联系),使模型无法捕捉其内容,目标是让模型根据上下文准确预测这些词。例如给定例句:

My dog is hairy → My dog is [MASK]

  此例句将“hairy”进行了遮掩,然后采用非监督学习的方法准确将其预测。原始工作将训练集中15%的单词掩盖。因此该方法有一个问题,如果某个单词仅仅在某个句中出现,而该单词恰好在预训练时被指定为待mask单词,则模型学习时可能会误认为该词只有通过Mask标记才能推测出来。这有可能导致后续任务微调时,模型推测该单词失败。为了解决这个问题,作者做了如下的处理:

对于训练句子“My dog is hairy“,指定”hairy“为待Mask单词。预训练时总共需要将该句子输入N次进行学习。
N×80%次中,将“hairy”用[mask]标记代替;
N×10%次中,将“hairy”用其他单词代替,代替的单词随即从词表中选取;
N×10%次中,将“hairy”不做任何改变,原句直接输入。

  同样文章中讨论了将其他词替代“hairy”时是否会对模型学习带来负面影响,其解释是:随机替换的情况只有15%×10% =1.5%的概率,对整体训练的影响可以忽略不计。

  • 句子关系判断(Next Sentence Prediction):
      Next Sentence Prediction(简称NSP)任务是给定句对A、B,判断B是否可以作为A的下一句,这个问题类似于分类问题。生成句对的方法是从文本语料中抽取相邻的两个句子,训练时同样输入N次学习,其中有50%的情况使用的B是A的下一句,而另50%的情况中B是随机从语料中抽取的句子。

  从预训练任务可以看出,BERT的预训练输出也是单个向量的形式,MLM需要预测具体的单词,因此输出的是词表大小的向量;而NSP则输出二维向量即可,因为目标标签只有两个。

3.2.3 BERT位置编码

  原始的BERT没有采用第二章所介绍的位置编码方式,而是使用了一些数据专门训练出位置向量信息,这些向量都可以较好的反应序列的位置关系,随后将字符Embedding和向量信息进行拼接作为输入。因此向量信息的数量是有限的,一般反映了BERT单次处理序列的最大长度(通常限制为512)。读者要注意的是,Transformer和BERT的位置编码方式都是绝对位置编码,即采取的编码方式只能在单个序列中使用,而无法表达当前同前后序列中元素之间的位置关系。因此后来的Transformer-XL和华为研发的NEZHA都引入了相对位置编码机制,一定程度上缓解了长序列问题,有兴趣可以参考。

3.3 BERT下游任务微调

  到目前为止,我们讲到的BERT仅仅是前期预训练所做的工作,其最终目的还是提升各种下游任务、应用场景的性能,这就用到了BERT微调策略。网上将BERT的下游微调称为模型训练的“最后一英里”,由此看出微调工作并不困难,本节我们介绍一下适用于BERT的下游任务,以及如何微调使预训练模型发挥效果。

  首先我们介绍BERT一般适用的四大类微调任务,如下图:
在这里插入图片描述
  我们展开讲解一下四大任务类,以及如何来微调。

3.3.1 句对分类任务

  句对分类任务描述如下:

输入:句子对A、B
输出:所属类别

应用场景:
判断A、B语义是否相同或相似(类别为“是”和“否”);
判断句子B语义上是否蕴含了句子A(类别为“是”和“否”);
判断句子B能否构成句子A的回答(类别为“是”和“否”)。

  输入句子对,第一个句子前插入[CLS]标记,句子之间插入[SEP]标记。[SEP]标记方便模型识别出句子间隔以避免混淆。通过BERT迭代后,取出“[CLS]”所对应的最后一层Transformer的logits输出向量,对其做线性变换,使其维度映射到二维或N维,在执行归一化后判断即可。公式如下:
l o g i t s = T r m l a s t ( C L S ) P r e d i c t i o n = S o f t m a x ( l o g i t s ⋅ W ) logits=Trm_{last}(CLS)\\ Prediction=Softmax(logits\cdot W) logits=Trmlast(CLS)Prediction=Softmax(logitsW)

3.3.2 单一句子分类任务

  文本分类在NLP任务中十分普遍,一般用到新闻分类、文章推荐等应用中。但BERT由于对序列输入长度的限制,因此一般用于句子分类。单一句子分类任务描述如下:

输入:句子A
输出:所属类别

应用场景:
判断一句话的情绪如何(类别为“积极”和“消极”);
判断一个句子语法是否正确(类别为“是”和“否”);

  输入为单一句子,在其起始位置插入“[CLS]”标识。通过BERT迭代后,取出“[CLS]”所对应的最后一层Transformer的logits输出向量,对其做线性变换,使其维度映射到二维或N维,在执行归一化后判断即可。公式如下:
l o g i t s = T r m l a s t ( C L S ) P r e d i c t i o n = S o f t m a x ( l o g i t s ⋅ W ) logits=Trm_{last}(CLS)\\ Prediction=Softmax(logits\cdot W) logits=Trmlast(CLS)Prediction=Softmax(logitsW)
  不难看出单一句子分类任务和句对分类任务的微调方法都是一样的,读者可以参考下图来加深理解:
在这里插入图片描述

3.3.3 句子定位任务

  原文中的描述为“Question Answering Tasks”(问答任务),在这里要说明一下原文所指的“问答”并不是我们所讨论的问答系统,也就是说它并不是“给定一个问题,通过生成的方式反馈正确回答”的系统,而是给定问题以及段落,让模型从段落中找到有助于回答问题的句子部分。这个任务相对分类而言较为复杂,我们来描述一下:

输入:问句A、段落B
输出:起始位置、结束位置

应用场景:
阅读理解任务,从文章中找到问句相关部分;

  输入问句、段落,问句起始部分插入[CLS](CLS标志在本任务中作用并不大),问句和段落之间插入[SEP]。通过BERT迭代后,拼接最后一层Transformer的所有输出向量为一个矩阵,并乘以一个W矩阵得到维度为2×N的logits矩阵,其中N为输入序列长度。随后将logits矩阵在长度为N的维度上做softmax归一化可用作定位判断。其中矩阵的第一行中概率最大的下标位置对应的原输入位置就是定位的起点,第二行中概率最大的下标位置对应原输入的终点。起点和终点的区间便是能正确回答问句的部分。现给出公式如下:
l o g i t s = [ C ] N × H [ K ] 2 × H T P r e d i c t i o n = S o f t m a x ( l o g i t s ) logits=[C]_{N×H}[K]_{2×H}^T\\ Prediction=Softmax(logits) logits=[C]N×H[K]2×HTPrediction=Softmax(logits)

3.3.4 单句标注任务

  序列标注任务涵盖了命名实体识别、专有名词抽取、词性识别以及汉语中的分词等词法任务,它们都是NLP各种应用任务的基础。在文中序列标注以单句英文命名实体识别任务为例。我们来描述一下单句标注任务:

输入:句子A
输出:和句子长度相同的标签序列

应用场景:
命名实体识别;
词性标注;
中文分词;
专业术语、专有名词识别。

  输入为待标注的句子,前面插入[CLS]标志,然后通过BERT编码。下游微调有好几种方法,比较简单的一种是迭代后取出最后一层Transformer的向量序列,对其用权重矩阵W进行变换,使其维度变为C×N,其中C为标签数,N为输入序列长度。随后按长度为D的维度取softmax归一化,每一个列向量即对应的词取不同标签的概率。公式如下:
l o g i t s = [ C ] N × H [ K ] C × H T P r e d i c t i o n = S o f t m a x ( l o g i t s ) logits=[C]_{N×H}[K]_{C×H}^T\\ Prediction=Softmax(logits) logits=[C]N×H[K]C×HTPrediction=Softmax(logits)
  还有另一种思路,考虑到早些年序列标注所用的方法----Bi-LSTM+CRF,我们将该结构同样添加到BERT的最后输出层,使得BERT的输出作为LSTM的输入再次做上下文关系的编码,最终通过CRF解码输出。

3.4 BERT的主要缺点

  虽然Transformer和BERT为NLP深度学习领域带来了革命性的改变,但随着其大规模应用和实验,研究人员也从中发现了很多不足之处,本节我们来梳理一下BERT的主要几个缺点。

  1. 预训练时的mask标志和下游任务不对等
      上文学习中不难发现,由于掩盖词预测仅仅在预训练中加入,所以mask标志也只存在预训练阶段,而下游任务迁移一般情况下用不到mask. 所以我们在BERT预训练阶段时,进行掩盖词预测任务的实质就是令模型找出每一个输入序列的mask标识,并用适当的词进行替换。在执行替换操作时,模型会参考上下文来预测,因而可以学到语言知识,但模型对mask产生了一定的依赖性,会将mask同上下文一同视作本输入的特征,由此训练出的预训练模型也是一定程度考虑了mask。这样的模型在迁移到下游任务时性能往往达不到最好。后来出现了一些预训练模型如Roberta和XLNet等,都针对这一问题进行了改进。Roberta采取的是动态调整mask策略,使每次处理同一个序列时mask的位置都不同,这样减轻了mask与上下文的固定对模型所造成的“误导”;XLNet则是采用排列语言模型(Permutation Language Model)的方法,将一个序列随机排列,然后让模型预测最后的几个词。

  2. 模型收敛速度欠佳
      上文讲过,由于Transformer是AE语言模型,需要对一个序列的每一个token之间计算注意力,且BERT为层与层之间全连接的双向结构,所以执行反向传播时模型效率不是特别理想。

  3. 对较长的序列表现欠佳
      当输入序列长度较长时,由于模型的结构使得其在计算注意力机制时效率会大打折扣,解释起来就是它在深层理解一句话或几句话时十分出色,但对于太长的序列,深层理解如此大的数据量时间成本也会很高。由此看出,BERT适合处理输入序列较短的任务,句子级或多句级,而不适合段落级甚至篇章级的任务。在后来出现的如Transformer-XL中加入了Segment-Level Recurrence算法,通过将长序列切分成N段,然后依次处理,保存每层Tramsformer的隐层输入(即要参与同三个矩阵相乘以映射出q、k、v向量的部分),在处理下一个segment时将对应层的隐层输入同上一个隐层输入拼接或点相加作为本层的隐层向量。另一个原因是上文讲过的BERT的位置编码机制,其采用的预先训练的位置编码向量有限,序列过长会导致编码数量用尽,因此只有截取分批处理。这一点同样Transformer-XL所开创的相对位置编码可以有效地解决。感兴趣的读者可以自行参考。

  4. 预训练方式存在局限
      上文讲过,BERT预训练采用两大任务,即MLM和NSP,这两大任务训练了模型对句子关系判断的能力以及对句子语义理解的能力,因此这两种预训练方式所产生的模型,非常适用于需要匹配或分类的下游任务,比如阅读理解、情感分类、句对判断等,因此BERT的下游任务也基本围绕它们展开。而对于NER,其性能提升可以认为是BERT对深层语义的理解所训练出的能力,有人认为NER或中文分词等标注任务不需要太复杂的模型即可取得良好效果,使用BERT来完成可能投入产出比不是很高。此外其他任务如翻译等生成任务BERT或许达不到最佳表现。

Logo

尧米是由西云算力与CSDN联合运营的AI算力和模型开源社区品牌,为基于DaModel智算平台的AI应用企业和泛AI开发者提供技术交流与成果转化平台。

更多推荐