写在前面:本文参考了很多大牛的图文,如有侵权,请联系删除。 BERT模型的全称是:Bidirectional Encoder Representations from Transformer。从名字中可以看出,BERT模型的目标是利用大规模无标注语料训练、获得文本的包含丰富语义信息的Representation,即:文本的语义表示,然后将文本的语义表示在特定NLP任务中作微调,最终应用于该NLP任务。煮个栗子,BERT模型训练文本语义表示的过程就好比我们在高中阶段学习语数英、物化生等各门基础学科,夯实基础知识;而模型在特定NLP任务中的参数微调就相当于我们在大学期间基于已有基础知识、针对所选专业作进一步强化,从而获得能够应用于实际场景的专业技能。 1 注意力机制1.1 基本原理(参考https://zhuanlan.zhihu.com/p/265108616) 在Attention诞生之前,已经有CNN和RNN及其变体模型了,那为什么还要引入attention机制?主要有两个方面的原因,如下: (1)计算能力的限制:当要记住很多“信息“,模型就要变得更复杂,然而目前计算能力依然是限制神经网络发展的瓶颈。 (2)优化算法的限制:LSTM只能在一定程度上缓解RNN中的长距离依赖问题,且信息“记忆”能力并不高。 在介绍什么是注意力机制之前,先让大家看一张图片。 当大家看到上面图片,会首先看到什么内容?当过载信息映入眼帘时,我们的大脑会把注意力放在主要的信息上,这就是大脑的注意力机制。同样,当我们读一句话时,大脑也会首先记住重要的词汇,这样就可以把注意力机制应用到自然语言处理任务中,于是人们就通过借助人脑处理信息过载的方式,提出了Attention机制。 ![]() 从本质上理解,Attention是从大量信息中有筛选出少量重要信息,并聚焦到这些重要信息上,忽略大多不重要的信息。权重越大越聚焦于其对应的Value值上,即权重代表了信息的重要性,而Value是其对应的信息。 至于Attention机制的具体计算过程,如果对目前大多数方法进行抽象的话,可以将其归纳为两个过程:第一个过程是根据Query和Key计算权重系数,第二个过程根据权重系数对Value进行加权求和。而第一个过程又可以细分为两个阶段:第一个阶段根据Query和Key计算两者的相似性或者相关性;第二个阶段对第一阶段的原始分值进行归一化处理;这样,可以将Attention的计算过程抽象为如图展示的三个阶段。 ![]() 在第一个阶段,可以引入不同的函数和计算机制,根据Query和某个 Key,计算两者的相似性或者相关性,最常见的方法包括:求两者的向量点积、求两者的向量Cosine相似性或者通过再引入额外的神经网络来求值,即如下方式: 第一阶段产生的分值根据具体产生的方法不同其数值取值范围也不一样,第二阶段引入类似SoftMax的计算方式对第一阶段的得分进行数值转换,一方面可以进行归一化,将原始计算分值整理成所有元素权重之和为1的概率分布;另一方面也可以通过SoftMax的内在机制更加突出重要元素的权重。即一般采用如下公式计算: 第二阶段的计算结果 即为 对应的权重系数,然后进行加权求和即可得到Attention数值: 通过如上三个阶段的计算,即可求出针对Query的Attention数值,目前绝大多数具体的注意力机制计算方法都符合上述的三阶段抽象计算过程。 1.2 注意力机制示例(参考http://www.360doc.com/content/19/1125/16/32196507_875387868.shtml) 在本示例中,我们从 3 个输入开始,每个输入的尺寸为 4。 1.2.1 准备输入绿色部分为输入,输入1:[1,0,1,0] 输入2:[0,2,0,2] 输入3:[1,1,1,1] 1.2.2 初始化权重每个输入必须具有三个表示形式(请参见下图)。这些表示形式称为键key(橙色),查询query(红色)和值value(紫色)。对于此示例,让我们假设这些表示的尺寸为 3。因为每个输入的尺寸为 4,这意味着每组权重都必须为 4×3。稍后将看到值的维数也是输出的维数。 首先对三种权重进行初始化。 键的权重: [[0,0,1], [1,1,0], [0,1,0], [1,1,0]] 查询权重: [[1,0,1], [1,0,0], [0,0,1], [0,1,1]] 价值权重: [[0,2,0], [0,3,0], [1,0,3], [1,1,0]] 注意:在神经网络设置中,这些权重通常是小数,使用适当的随机分布(如高斯,Xavier和Kaiming分布)随机初始化。 1.2.3 计算每个输入的键、查询和值表示形式为了获得这些表示形式,每个input(绿色)都将与一组key的权重,一组query的权重和一组value的权重相乘。在我们的示例中,计算流程如下: 输入 1 的键表示结果为input1向量与初始化后的键矩阵相乘,结果为: [0,0,1] [1,0,1,0] x [1,1,0] = [0,1,1] [0,1,0] [1,1,0] 同理可以得到input2和input3的键表示为:[4,4,0],[2,3,1]。 当然,也可以通过矩阵乘法直接得到: [0,0,1] [1,0,1,0] [1,1,0] [0,1,1] [0,2,0,2] x [0,1,0] = [4 ,4,0] [1,1,1,1] [1,1,0] [2,3,1] 以上是键key的表示形式,同理可以得到值value和查询query的表示: 值value表示: [0,2,0] [1,0,1,0] [0,3,0] [1, 2,3] [0,2,0,2] x [1,0,3] = [2 ,8,0] [1,1,1,1] [1,1,0] [2,6,3] 查询query表示: [1,0,1] [1,0,1,0] [1,0,0] [1,0,2] [0,2,0,2] x [0,0,1] = [2 ,2,2] [1,1,1,1] [0,1,1] [2,1,3] 图 1.2:从每个输入得出键,查询和值表示 1.2.4 计算注意力得分以计算input1的注意力得分为例。 ![]() 为了获得注意力得分,我们首先在输入 1 的查询(红色)与所有键(橙色)(包括其自身)之间取一个点积。由于有 3 个关键表示(因为我们有3个输入),因此我们获得 3 个注意力得分(蓝色)。 [0,4,2] [1,0,2] x [1,4,3] = [2,4,4] [1,0,1] 以上矩阵每一列表示一个key。这里仅使用输入 1 的查询。同理,可对其他查询重复相同的步骤。 1.2.5 计算 softmax在所有注意力得分中使用 softmax(蓝色)。 softmax([2,4,4])= [0.0,0.5,0.5](严格来讲这里并不是绝对的0,但是softmax指数变换之后太小,忽略为0)。 1.2.6 获取加权值每个经过 softmax 的输入的最大注意力得分(蓝色)乘以其相应的值(紫色),得到 3 个对齐向量(黄色),即加权值。 0.0 * [1,2,3] = [0.0,0.0,0.0] 0.5 * [2,8,0] = [1.0,4.0,0.0] 0.5 * [2,6,3] = [1.0,3.0,1.5] ![]() 取所有加权值(黄色)并将它们按元素求和: [0.0,0.0,0.0] + [1.0,4.0,0.0] + [1.0,3.0,1.5] = [2.0,7.0,1.5] ![]() 所得向量[2.0、7.0、1.5](深绿色)为input1注意力输出向量,它代表了input1与所有其他键(包括其自身)交互的查询表示形式,可以理解为所有词对当前词的贡献程度(包括词本身对自己的贡献程度)。 同理,input 2 和input 3的输出权重只需要重复以上步骤即可。 2 自注意力机制通过Attention机制发生在Target(即Query)和Source(即所有的key和value)中的所有元素之间。简单的讲就是Attention机制中的权重的计算需要Target来参与的,在Encoder-Decoder模型中,比如中-英机器翻译,Source 对应中文语句,Target 对应英文语句,Attention权值的计算不仅需要Encoder中的隐状态而且还需要Decoder 中的隐状态,这时Source 和输出 Target 的内容是联系的,使用attention有比较好的效果。现在有另一个不是翻译的任务,给定一个句子:The animal didn't cross the street because it was too tired,这里的it到底代表的是animal还是street呢,对于我们来说能很简单的判断出来,但是对于机器来说,是很难判断的,self-attention就能够让机器把it和animal联系起来。 2.1 基本流程顾名思义,self-attention指的是 Source 内部元素之间或者 Target 内部元素之间(即Source 和 Target 的内容是相对独立的)发生的 Attention 机制,也可以理解为 Source = Target 这种特殊情况下的 Attention 机制。例如在Transformer中在计算权重参数时将文字向量转成对应的KQV,只需要在Source处进行对应的矩阵操作,用不到Target中的信息。 自注意力机制是注意力机制的变体,其减少了对外部信息的依赖,更擅长捕捉数据或特征的内部相关性。自注意力机制在文本中的应用,主要是通过计算单词间的互相影响,来解决长距离依赖问题。自注意力机制的计算过程: 1.将输入单词转化成嵌入向量; 2.根据嵌入向量得到q,k,v三个向量; 3.为每个向量计算一个score=q . k ; 4.为了梯度的稳定,Transformer使用了score归一化,即除以 (是向量维度,即Q,K矩阵列数); 5.对score施以softmax激活函数; 6.softmax点乘Value值v,得到加权的每个输入向量的评分v; 7.相加之后得到最终的输出结果z :。 2.2 示例假设有两个字thinking和machines,现在需要计算thinking的自注意力权重,很显然,这张图完整对应了3.1的流程。但在实际的应用场景,为了提高计算速度,我们采用的是矩阵的方式,直接计算出Query, Key, Value的矩阵,然后把embedding的值与三个矩阵直接相乘,把得到的新矩阵Q与K相乘,乘以一个常数,做softmax操作,最后乘上V矩阵,得到: 这种通过 query 和 key 的相似性程度来确定 value 的权重分布的方法被称为scaled dot-product attention: 2.3多头注意力了解自注意力机制后,下面这张图理解起来就方便多了。 在Transformer及BERT模型中用到的Multi-headed Self-attention结构与之略有差异,具体体现在:如果将前文中得到的整体看做一个“头”,则“多头”即指对于特定的来说,需要用多组与之相乘,进而得到多组。如下图所示: ![]() 如上图所示,以右侧示意图中输入的为例,通过多头(这里取head=3)机制得到了三个输出, , ,为了获得与对应的输出,在Multi-headed Self-attention中,我们会将这里得到的, , 进行拼接(向量首尾相连),然后通过线性转换(即不含非线性激活层的单层全连接神经网络,可以是可学习的权重矩阵)得到。对于序列中的其他输入也是同样的处理过程,且它们共享这些网络的参数。更常见的一张图是下面的。 用更加微观的角度来看,可以表达成下图所示。 ![]() 3 transformer3.1 整体结构(参考https://zhuanlan.zhihu.com/p/338817680) transformer完全摒弃了rnn和lstm的序列模型,采取了全新的encoder、decoder模型,是一种很重要的创新,可拓展到cv领域。首先介绍 Transformer 的整体结构,下图是 Transformer 用于中英文翻译的整体结构: ![]() Transformer 的整体结构,左图Encoder和右图Decoder 可以看到 Transformer 由 Encoder 和 Decoder 两个部分组成,Encoder 和 Decoder 都包含 6 个 block。Transformer 的工作流程大体如下: 第一步:获取输入句子的每一个单词的表示向量 X,X由单词的 Embedding(Embedding就是从原始数据提取出来的Feature) 和单词位置的 Embedding 相加得到。 ![]() Transformer 的输入表示 第二步:将得到的单词表示向量矩阵 (如上图所示,每一行是一个单词的表示 x) 传入 Encoder 中,经过 6 个 Encoder block 后可以得到句子所有单词的编码信息矩阵 C,如下图。单词向量矩阵用 表示, n 是句子中单词个数,d 是表示向量的维度 (论文中 d=512)。每一个 Encoder block 输出的矩阵维度与输入完全一致。 ![]() ransformer Encoder 编码句子信息 第三步:将 Encoder 输出的编码信息矩阵 C传递到 Decoder 中,Decoder 依次会根据当前翻译过的单词 1~ i 翻译下一个单词 i+1,如下图所示。在使用的过程中,翻译到单词 i+1 的时候需要通过 Mask (掩盖)操作遮盖住 i+1 之后的单词。 ![]() Transofrmer Decoder 预测 上图 Decoder 接收了 Encoder 的编码矩阵 C,然后首先输入一个翻译开始符 '',预测第一个单词 'I';然后输入翻译开始符 '' 和单词 'I',预测单词 'have',以此类推。这是 Transformer 使用时候的大致流程,接下来是里面各个部分的细节。 下面详细介绍各部分。 3.2 输入Transformer 中单词的输入表示 x由单词 Embedding和位置 Embedding(Positional Encoding)相加得到。 ![]() Transformer 的输入表示 3.2.1 单词 Embedding单词的 Embedding 有很多种方式可以获取,例如可以采用 Word2Vec、Glove 等算法预训练得到,也可以在 Transformer 中训练得到。 3.2.2 位置 EmbeddingTransformer 中除了单词的 Embedding,还需要使用位置 Embedding 表示单词出现在句子中的位置。因为 Transformer 不采用 RNN 的结构,而是使用全局信息,这样极有可能无论句子结构怎么打乱,transformer都会得到类似的结果。所以单词的顺序信息对于 NLP 来说非常重要。所以 Transformer 中使用位置 Embedding 保存单词在序列中的相对或绝对位置。 位置 Embedding 用 PE表示,PE的维度与单词 Embedding 是一样的。加入位置信息的方式非常多,最简单的可以是直接将绝对坐标0,1,2编码成512个长度向量即可。作者实际上提出了两种方式:
提前假设单词嵌入并且组成batch后,shape为(b,N,512),N是序列最大长度,512是每个单词的嵌入向量长度,b是batch。 网络自动学习比较简单,因为位置编码向量需要和输入嵌入(b,N,512)相加,所以其shape为(1,N,512)表示N个位置,每个位置采用512长度向量进行编码,在 Transformer 中则采用了后者,计算公式如下: ![]() 其中,pos 表示单词在句子中的位置,即0~N,i是0-511,d 表示 PE的维度 (与词 Embedding 一样),2i 表示偶数的维度,2i+1 表示奇数维度 (即 2i≤d, 2i+1≤d)。 def get_position_angle_vec(position): 上面例子的可视化如下: ![]() 如此编码的优点是能够扩展到未知的序列长度,例如前向时候有特别长的句子。其可视化如下: ![]() 作者为啥要设计如此复杂的编码规则?原因是sin和cos的如下特性: 可以将用和进行线性表出: ![]() 使用这种公式计算 PE 有以下的好处:
将单词的词 Embedding 和位置 Embedding 相加,就可以得到单词的表示向量 x,x就是 Transformer 的输入。 3.3 编码器encoder![]() Transformer Encoder block 上图红色部分是 Transformer 的 Encoder block 结构,可以看到是由 Multi-Head Attention, Add & Norm, Feed Forward, Add & Norm组成的。刚刚已经了解了 Multi-Head Attention 的计算过程,现在简单了解一下 Add & Norm 和 Feed Forward 部分。 3.3.1 Add & NormAdd & Norm 层由 Add 和 Norm 两部分组成,其计算公式如下: ![]() Add & Norm 公式 其中 X表示 Multi-Head Attention 或者 Feed Forward 的输入,MultiHeadAttention(X) 和 FeedForward(X) 表示输出 (输出与输入 X维度是一样的,所以可以相加)。 Add指 X+MultiHeadAttention(X),是一种残差连接,通常用于解决多层网络训练的问题,可以让网络只关注当前差异的部分,在 ResNet 中经常用到残差连接: ![]() Norm指 Layer Normalization,通常用于 RNN 结构,Layer Normalization 会将每一层神经元的输入都转成均值方差都一样的,这样可以加快收敛。 3.3.2 Feed ForwardFeed Forward 层比较简单,是一个两层的全连接层,第一层的激活函数为 Relu,第二层不使用激活函数,对应的公式如下。 ![]() X是输入,Feed Forward 最终得到的输出矩阵的维度与X一致。 3.3.3 Encoder通过上面描述的 Multi-Head Attention, Feed Forward, Add & Norm 就可以构造出一个 Encoder block,Encoder block 接收输入矩阵 ,并输出一个矩阵 。通过多个 Encoder block 叠加就可以组成 Encoder。 第一个 Encoder block 的输入为句子单词的表示向量矩阵,后续 Encoder block 的输入是前一个 Encoder block 的输出,最后一个 Encoder block 输出的矩阵就是编码信息矩阵 C,这一矩阵后续会用到 Decoder 中。 ![]() 将上述编码过程重复n遍即可,除了第一个模块输入是单词嵌入向量与位置编码的和外,其余编码层输入是上一个编码器输出即后面的编码器输入不需要位置编码向量。综合以上信息,encoder层可视化如下所示: 3.4 解码器decoder![]() 上图红色部分为 Transformer 的 Decoder block 结构,与 Encoder block 相似,但是存在一些区别:
3.4.1 含Mask的Multi-Head Attention 层为啥要mask?原因依然是顺序解码导致的。试想模型训练好了,开始进行翻译(测试),其流程就是上面写的:输入BOS_WORD,解码器输出i;输入前面已经解码的BOS_WORD和i,解码器输出am...,输入已经解码的BOS_WORD、i、am、a和student,解码器输出解码结束标志位EOS_WORD,每次解码都会利用前面已经解码输出的所有单词嵌入信息,这个测试过程是没有问题,但是训练时候我肯定不想采用上述顺序解码类似rnn即一个一个目标单词嵌入向量顺序输入训练,肯定想采用类似编码器中的矩阵并行算法,一步就把所有目标单词预测出来。要实现这个功能就可以参考编码器的操作,把目标单词嵌入向量组成矩阵一次输入即可,但是在解码am时候,不能利用到后面单词a和student的目标单词嵌入向量信息,否则这就是作弊(测试时候不可能能未卜先知)。为此引入mask,目的是构成下三角矩阵,右上角全部设置为负无穷(相当于忽略),从而实现当解码第一个字的时候,第一个字只能与第一个字计算相关性,当解出第二个字的时候,只能计算出第二个字与第一个字和第二个字的相关性。具体是:在解码器中,自注意力层只被允许处理输出序列中更靠前的那些位置,在softmax步骤前,它会把后面的位置给隐去(把它们设为-inf)。 ![]() 举例说明mask的计算过程(其实与前述基本一致)。用 0 1 2 3 4 5 分别表示 'I have a cat'。 第一步:是 Decoder 的输入矩阵和 Mask矩阵,输入矩阵包含 'I have a cat' (0, 1, 2, 3, 4) 五个单词的表示向量,Mask是一个 5×5 的矩阵。在 Mask可以发现单词 0 只能使用单词 0 的信息,而单词 1 可以使用单词 0, 1 的信息,即只能使用之前的信息。 ![]() 第二步:接下来的操作和之前的 Self-Attention 一样,通过输入矩阵X计算得到Q,K,V矩阵。然后计算Q和 的乘积 。 ![]() 第三步:在得到 之后需要进行 Softmax,计算 attention score,我们在 Softmax 之前需要使用Mask矩阵遮挡住每一个单词之后的信息,遮挡操作如下: ![]() 得到 Mask之后,在 Mask上进行 Softmax,每一行的和都为 1。但是单词 0 在单词 1, 2, 3, 4 上的 attention score 都为 0。 第四步:使用 Mask与矩阵 V相乘,得到输出 Z,则单词 1 的输出向量 是只包含单词 1 信息的。 ![]() 第五步:通过上述步骤就可以得到一个 Mask Self-Attention 的输出矩阵 ,然后和 Encoder 类似,通过 Multi-Head Attention 拼接多个输出, 然后计算得到第一个 Multi-Head Attention 的输出Z,Z与输入X维度一样。 3.4.2 Multi-Head Attention 层带有mask的MultiHeadAttention和MultiHeadAttention结构和代码写法是完全相同,区别一是是否输入了mask。二是Decoder block 第二个 Multi-Head Attention 的 K, V矩阵不是使用 上一个 Decoder block 的输出计算的,而是使用 Encoder 的编码信息矩阵 C计算的。 解码器内部的带有mask的Multi-HeadAttention的qkv向量输入来自目标单词嵌入或者前一个解码器输出,三者是相同的,但是后面的Multi-HeadAttention的qkv向量中的kv来自最后一层编码器的输入,而q来自带有mask的Multi-HeadAttention模块的输出(实际上是encoder和decoder之间的一种信息交流)。根据 Encoder 的输出 C计算得到 K, V,根据上一个 Decoder block 的输出 Z计算 Q(如果是第一个 Decoder block 则使用输入矩阵 X进行计算),后续的计算方法与之前描述的一致。这样做的好处是在 Decoder 的时候,每一位单词都可以利用到 Encoder 所有单词的信息 (这些信息无需 Mask)。 3.4.3 softmax分类器在进行编码器-解码器后输出依然是向量,需要在后面接fc+softmax层进行分类训练。假设当前训练过程是翻译任务需要输出i am a student EOS_WORD这5个单词。 Decoder block 最后的部分是利用fc+softmax层预测下一个单词,在之前的网络层我们可以得到一个最终的输出 Z,因为 Mask 的存在,使得单词 0 的输出 Z0 只包含单词 0 的信息,如下: ![]() Softmax 根据输出矩阵的每一行预测下一个单词,假设我们的模型是从训练集中学习一万个不同的英语单词(我们模型的“输出词表”)。因此softmax后输出为一万个单元格长度的向量,每个单元格对应某一个单词的分数,这其实就是普通多分类问题,只不过维度比较大而已。 : ![]() 假设编码器输出shape是(b,100,512),经过fc后变成(b,100,10000),然后对最后一个维度进行softmax操作,得到bx100个单词的概率分布,在训练过程中bx100个单词是知道label的,故可以直接采用ce-loss进行训练。 这就是 Decoder block 的定义,与 Encoder 一样,Decoder 是由多个 Decoder block 组合而成。 3.5 总结transformer
4 Bert模型(参考https://zhuanlan.zhihu.com/p/552270356) 首先回顾一下bert的名字。 Encoder。BERT是仅仅采用了Transformer中的Encoder部分,因此它是个编码器模型。 Bidirectional。传统的语言模型都是单向的,也就是从左到右,或者是从右到左。作者认为单向的语言模型会影响某些下游任务的性能。实践后发现如果能实现双向的阅读,效果更好。BERT通过自注意力机制实现了双向的阅读,所以被称为自编码模型。 预训练。BERT采用了两种任务对文本进行无监督的预训练,MLM(Masked Language Model)和 NSP(Next Sentence Prediction) 微调。有四类任务:(1)输入序列,输出分类。比如情感分析,文本分类(2)输入序列,输出也是序列,比如词性标注(3)输入两个句子,输出分类。比如自然语言推理,即给定一个前提,然后给出一个假设,模型要判断出这个假设是 正确、错误还是不知道;(4)QA问答 (来自李宏毅老师课程) encoder和bidirectional在前面和双向lstm中都有介绍,本章主要在详细分析bert输入与输出基础上,对bert的预训练和微调任务进行介绍。 4.1 模型输入与输出BERT模型的主要输入依然是文本中各个字/词的原始词向量,该向量既可以随机初始化,也可以利用Word2Vector等算法进行预训练以作为初始值;输出是文本中各个字/词融合了全文语义信息后的向量表示,如下图所示(为方便描述且与BERT模型的当前中文版本保持一致,本文统一以字向量作为输入): 从上图中可以看出,BERT模型通过查询字向量表将文本中的每个字(是基于字,不是词哟)转换为一维向量,作为模型输入;模型输出则是输入各字对应的融合全文语义信息后的向量表示。可以发现,BERT将输入文本中的每一个词(token)送入token embedding层从而将每一个词转换成向量形式。但不同于其他模型的是,BERT又多了两个嵌入层,即segment embeddings(文本嵌入)和 position embeddings(位置嵌入) 。 ![]() 4.1.1 position embeddings(位置嵌入)前面已经介绍过位置向量,这里不再赘述。 4.1.2 token embedding(字嵌入)输入文本在送入token embeddings 层之前要先进行tokenization处理。此外,两个特殊的token会被插入到tokenization的结果的开头 ([CLS])和结尾 ([SEP]) 。它们视为后面的分类任务和划分句子对服务的。 tokenization使用的方法是WordPiece tokenization. 这是一个数据驱动式的tokenization方法,旨在权衡词典大小和oov(Out of Vocabulary)词的个数。这种方法把例子中的“strawberries”切分成了“straw” 和“berries”(对于中文,目前作者尚未对输入文本进行分词,而是直接将单字作为构成文本的基本单位。)。这种方法的详细内容不在本文的范围内。有兴趣的读者可以参阅 Wu et al. (2016) 和 Schuster & Nakajima (2012)。使用WordPiece tokenization让BERT在处理英文文本的时候仅需要存储30,522 个词,而且很少遇到oov的词。 Token Embeddings 层会将每一个wordpiece token转换成特定维度的向量。例如6个token转换成了一个(6, 768) 的矩阵或者是(1, 6, 768)的张量(如果考虑batch_size的话)。 4.1.3 segment embeddings如果处理的是两个句子,如下一个句子预测任务(NSP,Next Sentence Prediction),则需要句子以成对的方式输入。或者处理对输入句子对的分类任务(可用于判断两个文本是否是语义相似的),输入同样是要判定的两个句子,输出则是相似或不相似标签(也可以是相似量化值)。句子对中的两个句子被简单的拼接在一起后送入到模型中。因此需要对两个句子进行区分。BERT如何去区分一个句子对中的两个句子呢?答案就是segment embeddings。对于第一个句子的所有词可以用EA表示,第二个句子的所有词可以用EB表示(如上图)。文本向量的取值是在模型训练过程中自动学习,用于刻画文本的全局语义信息,并与单字/词的语义信息相融合。 假设有这样一对句子 (“I like cats”, “I like dogs”)。下面的图呈现了segment embeddings如何帮助BERT区分两个句子: ![]() Segment Embeddings 层只有两种向量表示。前一个向量是把0赋给第一个句子中的各个token, 后一个向量是把1赋给第二个句子中的各个token。如果输入仅仅只有一个句子,那么它的segment embedding就是全0。当然更有可能是多分类或多个句子预测,此时输入的Segment Embeddings 层有对应的多种向量表示。 最后,BERT模型将字向量、文本向量和位置向量的加和作为模型输入。最后强调Token,Position,Segment Embeddings 都是通过学习来得到的。 4.2 BERT 框架组装好TransformerEncoder之后,再把多个Transformer Encoder一层一层地堆叠起来,BERT模型框架就大功告成了! ![]() 在论文中,作者分别用12层和24层Transformer Encoder组装了两套BERT模型,两套模型的参数总数分别为110M和340M。 4.3 预训练BERT的预训练任务有两个:
4.3.1 MLM(Masked Language Model)假设有句话 'the cat sat on the mat'。现在将 cat 遮挡住,希望Encoder去训练预测出来 计算流程
在这个任务里,BERT会随机遮挡单词,记作[MASK]。任务的目的是要预测 [MASK],预测值和实际值的损失越小越好。因此,这里的loss是计算被遮盖的[MASK]的Loss。 ![]() ![]() [MASK] 遮盖的操作如下:
好处是,可以强迫模型在编码当前时刻词的时候不能太依赖当前的词,而要考虑它的上下文,甚至根据上下文进行 '纠错'。 4.3.2 NSP(Next Sentence Prediction)拿到两个句子A和B,在两个句子之间加上特殊的 token 标记: ![]() 训练的目的是判断这两句话在原文中是否相邻(这个目标其实和预测下个句子是一样的,模型最终都是要学会根据一句话输出得到下句话)。在训练中,50%的B是相邻的,50%是不相邻的。预测的结果会存在Figure 1 的C 中,如下图: ![]() 假如输入数据:
输入两个句子,中间用【SEP】符号隔开,其中一个句子一定是原文中真实存在的句子,另一个句子则是从训练数据中随机抽取的句子,比如上述的句子是真实的原文我们把它标签为true 。 [CLS]这是一个很好看的杯子 而上述这两句话是随机匹配的我们把它标记为false。 向量C虽然在CLS的位置上,但它包含的是输入的两句话的全部信息,把C作为特征向量输入分类器Binary,得到一个介于0-1之间的值f,其中,1是代表两句话true,0代表两句话毫无关联,依旧用将CrossEntropy(e,f)作为损失函数,用反向传播算出损失函数关于模型的梯度,然后作梯度下降来更新模型参数 计算流程图如下 ![]() 4.3.3 MLM + NSPBERT 预训练是将两个任务结合起来,同时进行,然后将所有的loss相加。 ![]() ![]() MLM和NSP任务的训练是在transformers/src/transformers/models/bert/modeling_bert.py 中的BertForPreTraining类,该类的框架如下图所示: ![]() 以上图为例,NSP要判断这两句话是否相邻;MLM需要预测两个被遮住的单词
4.4 微调经过预训练后,得到的模型可以用来微调各类任务。 4.4.1 单文本分类任务刚才提到,BERT模型在文本前插入一个 ![]() 4.4.2 语句对分类任务语句对分类任务的实际应用场景包括:问答(判断一个问题与一个答案是否匹配)、语句匹配(两句话是否表达同一个意思)等。对于该任务,BERT模型除了添加 ![]() 4.4.3 序列标注任务序列标注任务的实际应用场景包括:命名实体识别、中文分词、新词发现(标注每个字是词的首字、中间字或末字)、答案抽取(答案的起止位置)等。对于该任务,BERT模型利用文本中每个Token对应的输出向量对该Token进行标注(分类),如下图所示(B(Begin)、I(Inside)、E(End)分别表示一个词的第一个字、中间字和最后一个字)。 ![]() 4.4.4 问答任务主要用于SQuAD数据集,输入是一个问题和问题对应的段落,用[SEP]分隔,这里输出的结果就不是某个class label而是答案在给定段落的开始和终止位置,主要用于阅读理解任务。要将问答任务交给 BERT,我们将问题与相关文本打包作为输入。 ![]() 两份文本用特殊符号 |
|