BERT
1.预备知识
1.1 word2vec(词向量模型)
word2vec可以分为两部分:模型与通过模型获得的词向量。
在Word2vec出现之前,文本表示一般只用one-hot独热编码或者直接用整数编码,这种做法太拉跨了。word2vec是用一个一层的神经网络(即CBOW)把one-hot形式的稀疏词向量映射称为一个n维(n一般为几百)的稠密向量的过程。为了加快模型训练速度,其中的tricks包括Hierarchical softmax,negative sampling, Huffman Tree等。
词向量:Word2vec训练完后的模型参数(这里特指神经网络的权重),作为输入 x 的某种向量化的表示,这个向量便叫做词向量。
词嵌入:用机器学习的思路,我们有一系列样本(x,y),这里 x 是词语,y 是它们的词性,我们要构建 f(x)->y 的映射,但这里的数学模型 f(比如神经网络、SVM)只接受数值型输入,而 NLP 里的词语,需要把他们转换成数值形式,或者说——嵌入到一个数学空间里,这种嵌入方式,就叫词嵌(word embedding),而 Word2vec,就是词嵌入( word embedding) 的一种方法之一。
1.2 RNN网络模型
一种递归网络模型,适合时间序列数据。
Transformer为什么能够彻底淘汰掉传统的RNN循环神经网络呢?
仔细想想确实如此,卷积神经网络动辄好几十层,而循环神经网络一般情况下只有两三层,这又是什么原因呢?
答:RNN网络不能做加速的并行计算。RNN当前时刻的输出,不仅需要当前时刻的输入,还需要前一时刻的输出,这种串行结构的运算方式决定了RNN运算速度慢的特性。
这就能引出另外一个知识点:RNN与CNN区别在于RNN具有记忆功能,被遍历的单元具有因果联系作用(记忆信息传送),上一时刻隐层的状态参与到了这个时刻的计算过程中,这句话的举例说明就是第一个单元计算的结果会作为第二个单元输入的一部分,所以,当前单元必须等上一单元计算结束,有多少单元就需要计算多少次。而CNN同一层次单元没有因果关系都是等价的,这样就可以依据单元核直接复制出所需所有单元核(参数相同),然后采用矩阵并行运算,只需计算一次。例如如果CNN的out_channel是10,那么这10个out_channel可以分给10个gpu并行算,但RNN不存在这种性质。
out_channel
卷积神经网络关于这个小知识点这里引出来两个问题:
1.为什么out_channel会大于in_channel?
2.为什么out_channel要大于in_channel?
答:我们以为kernel size只有长和宽,事实上kernel还有channel。感受野如果是三通道的,每一个真实的kernel的channel也为3。在滑动扫描的过程中,感受野会变成三层,而kernel也会变成三层,对应通道的地方对应扫描。
- in_features - 每个输入样本的大小
- out_features - 每个输出样本的大小
总的来说就是in_channel输入通道数决定了卷积核的通道数也就是层数,out_channe输出通道数决定了卷积核的个数。
如下图所示,in_features的形状为6行3列,所以行数对应的out_channels的数目,列数对应in_channels的数目。 - 既然RNN不能并行,Transformer就能并行吗?
答:当然可以,核心机制就在于Self-Attention,在 Self-Attention 能够 并行的 计算 句子中不同 的 query,因为每个 query 之间并不存在先后依赖关系,也使得 transformer 能够并行化。
1.3 词向量的建模方式
将word2vec和RNN融合在一起解决实际问题。
CBOW(Continuous Bag-of-Words):根据某个词前面的C个词或者前后C个连续的词,来计算某个词出现的概率。
Skip-Gram Model:根据某个词,然后分别计算它前后出现某几个词的各个概率。
tricks:文本分类实战技巧。
Hierarchical:Hierarchical Softmax是 wor2vec中用于提高性能的一项关键技术.为描述方便起见,在具体介绍这个技术之前,先引入若干相关记号,整体过程为从输入输出的降维到哈夫曼编码。
negative sampling:既然名字叫Negative Sampling(负采样),那么肯定使用了采样的方法。采样的方法有很多种,比如之前讲到的大名鼎鼎的MCMC。
2.Transformer
Transformer的基本组成依旧是机器翻译模型中常见的Seq2Seq网络(序列网络模型)。其核心内容Self-Attention机制来进行并行计算。
在了解Attention之前需要先了解Encoder-Decoder框架顾名思义也就是编码-解码框架。以前在NLP领域的具体E-D框架的具体步骤是通过RNN生成隐藏层的状态值<h1,h2,h3,h4>,最简单的办法直接用最后时刻输出的ht作为C的状态值,也可以将所有时刻的隐藏层的值进行汇总,然后生成语义编码C的值。得到了语义编码C之后,接下来就是要在Decoder中对语义编码C进行解码了。
针对这里,Decoder的语义编码解码过程一般有两种选择:
1.因为语义编码C包含了整个输入序列的信息,所以在解码的每一步都引入C。
2.只在Decoder的初始输入引入语义编码C,将语义编码C作为隐藏层状态值h0的初始值。
那么Decoder用哪一种方法比较好呢?答案是两种方法都不好。如果按照方法一,在生成目标句子的单词时,不论生成哪个,他们使用的语义编码C都是一样的,没有任何区别。其实输入序列X中任意单词对生成某个目标单词yi来说影响力都是相同的,没有任何区别(其实如果Encoder是RNN的话,理论上越是后输入的单词影响越大,并非等权的,估计这也是为何Google提出Sequence to Sequence模型时发现把输入句子逆序输入做翻译效果会更好的小Trick的原因)。如果是方法二的话,将整个序列的信息压缩在了一个语义编码C中,用一个语义编码C来记录整个序列的信息,那么只是用一个语义编码C来表示整个序列的信息肯定会损失很多信息,而且序列一长,就可能出现梯度消失问题,这样将所有信息压缩在一个C里面显然就不合理。
既然有这么多的问题,那是不是只要把一个C换成多个C就好了呢?所以注意力机制由此诞生,同样,我们先把attention机制仍然基于基于Encoder-Decoder就好了。
2.1 Attention
下图就是引入了Attention 机制的Encoder-Decoder框架。明显看出不是单一的语义编码C,而是有多个C1,C2,C3这样的编码。当我们在预测Y1时,可能Y1的注意力是放在C1上,那咱们就用C1作为语义编码,当预测Y2时,Y2的注意力集中在C2上,那咱们就用C2作为语义编码,以此类推,就模拟了人类的注意力机制。
那么现在只剩下一个问题就是怎么计算出C1,C2,C3…Cn呢?如何判断我每次在做解码的时候注意力应该放在哪个位置呢?
为了体现出输入序列中英文单词对于翻译当前中文单词不同的影响程度,注意力分配模型分配给不同英文单词的注意力大小。这意味着在生成每个单词Yi的时候,原先都是相同的中间语义表示C会替换成根据当前生成单词而不断变化的Ci。
上图就是用Attention网络改善Seq2Seq模型,因为Seq2Seq模型本质上还是E-D结构。
2.2 Attention机制的本质思想
上面咱们说了Attention的原理,但是是基于Encoder-Decoder框架来介绍地,Attention机制并不是一定要基于Encoder-Decoder框架,那么他的本质思想是什么呢?参照下图可以这么来理解Attention,将Source中的构成元素想象成是由一系列的<Key,Value>键值对构成(对应到咱们上里面的例子,key和value是相等地,都是encoder的输出值h),此时给定Target中的某个元素Query(对应到上面的例子也就是decoder中的Hi),通过计算Query和各个Key的相似性或者相关性,得到每个Key对应Value的权重系数,然后对Value进行加权求和,即得到了最终的Attention数值。
Attention机制的具体计算过程,如果对目前大多数方法进行抽象的话,可以将其归纳为两个过程:第一个过程是根据Query和Key计算权重系数,第二个过程根据权重系数对Value进行加权求和。而第一个过程又可以细分为两个阶段:第一个阶段根据Query和Key计算两者的相似性或者相关性;第二个阶段对第一阶段的原始分值进行归一化处理;这样,可以将Attention的计算过程抽象为如图展示的三个阶段。
所以本质上Attention机制是对Source中元素的Value值进行加权求和,而Query和Key用来计算对应Value的权重系数。即可以将其本质思想改写为如下公式:
Lx表示source的长度,Similarity(Q,Ki)计算如下:
点积:就是将Query和Keyi进行点积,Transformer中就是用的这种方法。
Cosine相似性-余弦相似度:
分子就是两个向量Query与Key的点积
分母就是两个向量的L2范数,(L2范数:指向量各元素的平方和然后求平方根)计算出Query和Keyi的相似性后,第二阶段引入类似SoftMax的计算方式对第一阶段的相似性得分进行数值转换,一方面可以进行归一化,将原始计算分值整理成所有元素权重之和为1的概率分布;另一方面也可以通过SoftMax的内在机制更加突出重要元素的权重。即一般采用如下公式计算:
计算结果 ai 即为 Valuei 对应的权重系数,然后进行加权求和即可得到Attention数值:
Attention机制不再依赖于RNN,解决了RNN不能并行计算的问题。但是这样子就真的足够了吗?仔细想想还有一些问题:1.只能在Decoder阶段实现并行运算,Encoder部分依旧采用的是RNN,LSTM这些按照顺序编码的模型,Encoder部分还是无法实现并行运算,不够完美。2.就是因为Encoder部分目前仍旧依赖于RNN,所以对于中长距离之间,两个词相互之间的关系没有办法很好的获取。
2.3 Self-Attention
Self-Attention摆脱了Attention的缺点,只用在一个网络上也没什么问题。Attention的第一篇论文发表在2015年,用于改善Seq2Seq模型,解决RNN的遗忘问题。而Self-Attention发表在2016年。
下图详细的描述了SimpleRNN+Self-Attention的整个过程。
Self-Attention的优点在于解决RNN遗忘问题,每次更新状态之前都会context vector C 回顾一下之前所有的状态。
2.4 Transformer网络架构
Transformer模型完全基于Attention,是一种Seq2Seq模型,适合做翻译,且没有任何关于循环的结构。Transformer只有Attention和全连接层。这里参考了一篇
博客。将Transformer的结构拆解开来。
接下来将会分成四个小节拆解Transformer的结构:
2.4.1 inputs
首先Transformer的输入是一个序列数据,也就是分词后的词向量,词向量的形式并不做限制,word2vec,GloVe,one-hot编码都可以。分别对应的Encoder接受source数据,Decoder接受Target数据。而图中输入的Input Embedding和Output Embedding只会在有监督的训练时才会接受,预测试不会嵌入词向量。
如图所示,将词向量词嵌入之后,要先添加位置编码positional encoding。因为同样的字词在句子中出现的位置如果不同,那么语义将会发生翻天覆地的变化。但是Transformer 的是完全基于self-Attention的,而self-attention是不能获取词语位置信息的,这样一来如果打乱一句话中词语的位置,对于计算attention值是没有任何影响的,所以在我们输入的时候需要给每一个词向量添加位置编码。
那么问题来了,这个positional encoding怎么获取呢?有两种方法。
1.可以通过数据训练学习得到positional encoding,类似于训练学习词向量,goole在之后的bert中的positional encoding便是由训练得到地。
2.通过复杂的数学公式算出位置向量,但是这种方法反而不是很靠谱。
为什么是将positional encoding与词向量相加,而不是拼接呢?
答:拼接相加都可以,只是本身词向量的维度512维就已经蛮大了,再拼接一个512维的位置向量,变成1024维,这样训练起来会相对慢一些,影响效率。两者的效果是差不多地,既然效果差不多当然是选择学习习难度较小的相加了。
2.4.2 Encoder block
编码器块是由N个encoder堆叠而成,一个灰框就是一个encoder,好好研究一下内部结构。其内部结构共有一下三个部分:
Multi-Head Attention:
再介绍Multi-Head Attention之前先回顾一下Self-Attention,简单描述一下整个过程:首先source先转变为词向量,然后添加位置编码,词向量通过三个权值矩阵Wq,Wk,Wv转变成为计算Attention值所需要的Query,Keys,Values向量。得到这三个向量接下来就是常规步骤了。用点积发计算相关性得分,对相关性得分进行归一化,归一化的目的是能使梯度稳定,通过softmax函数将得分向量转换成概率分布,最后加权求和,最后得到的z矩阵就是Attention值。
self-Attention只使用了一组权值矩阵得到Query,Keys,Values。而Multi-Head Attention使用了多组权值矩阵,从而得到多个z矩阵。
如图,得到z矩阵之后并没有立刻传入全连接神经网络,而是经过了一步:Add&Normalize。
Add&Norm:
首先Add,就是在Z的基础上加了一个残差块,目的是为了防止在深度神经网络训练中发生退化问题,退化的意思就是深度神经网络通过增加网络的层数,Loss逐渐减小,然后趋于稳定达到饱和,然后再继续增加网络层数,Loss反而增大。
网络为什么会产生退化:我们有层数越深越好的误解。
为什么要进行Normalize呢?
答:在神经网络进行训练之前,都需要对于输入数据进行Normalize归一化,目的有二:1,能够加快训练的速度。2.提高训练的稳定性。
为什么使用Layer Normalization(LN)而不使用Batch Normalization(BN)呢?
答:
如图所示,LN是在同一个样本中不同神经元之间进行归一化,而BN是在同一个batch中不同样本之间的同一位置的神经元之间进行归一化。
BN是对于相同的维度进行归一化,但是咱们NLP中输入的都是词向量,一个300维的词向量,单独去分析它的每一维是没有意义地,在每一维上进行归一化也是适合地,因此这里选用的是LN。
Feed Forward:
全连接层就比较简单了,这里的全连接层是一个两层的神经网络,先线性变换,然后ReLU非线性,再线性变换。
2.4.3 Decoder block
首先Decoder有两种输入,Decoder的输入分为两类:一种是训练时的输入,一种是预测时的输入。
训练时的输入就是已经对准备好对应的target数据,例如翻译任务,Encoder输入"Tom chase Jerry",Decoder输入"汤姆追逐杰瑞"。预测时的输入,一开始输入的是起始符,然后每次输入是上一时刻Transformer的输出,例如,输入"",输出"汤姆",输入"汤姆",输出"汤姆追逐",输入"汤姆追逐",输出"汤姆追逐杰瑞",输入"汤姆追逐杰瑞",输出"汤姆追逐杰瑞"结束。
Decoder block也是由N个decoder堆叠而成,从图中我们可以看出一个decoder由Masked Multi-Head Attention,Multi-Head Attention 和 全连接神经网络FNN构成。比Encoder多了一个Masked Multi-Head Attention,其他的结构与encoder相同,那么咱们就先来看看这个Masked Multi-Head Attention。
Masked Multi-Head Attention
与Encoder的Multi-Head Attention计算原理一样,只是多加了一个mask码。mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 padding mask 和 sequence mask。为什么需要添加这两种mask码呢?
1.padding mask
什么是 padding mask 呢?因为每个批次输入序列长度是不一样的也就是说,我们要对输入序列进行对齐。具体来说,就是给在较短的序列后面填充 0。但是如果输入的序列太长,则是截取左边的内容,把多余的直接舍弃。因为这些填充的位置,其实是没什么意义的,所以我们的attention机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。
具体的做法是,把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 softmax,这些位置的概率就会接近0!
2.sequence mask
sequence mask 是为了使得 decoder 不能看见未来的信息。对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。这在训练的时候有效,因为训练的时候每次我们是将target数据完整输入进decoder中地,预测时不需要,预测的时候我们只能得到前一时刻预测出的输出。
那么具体怎么做呢?也很简单:产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每一个序列上,就可以达到我们的目的。
事实上,在Encoder中的Multi-Head Attention也是需要进行mask地,只不过Encoder中只需要padding mask即可,而Decoder中需要padding mask和sequence mask。OK除了这点mask不一样以外,其他的部分均与Encoder一样啦。
基于Encoder-Decoder 的Multi-Head Attention
Encoder中的Multi-Head Attention是基于Self-Attention。
而Decoder中搞两个Multi-Head Attention,为什么整俩呢?
答:第一个Masked Multi-Head Attention是为了得到之前已经预测输出的信息,相当于记录当前时刻的输入之间的信息的意思。第二个Multi-Head Attention是为了通过当前输入的信息得到下一时刻的信息,也就是输出的信息,是为了表示当前的输入与经过encoder提取过的特征向量之间的关系来预测输出。所以Decoder中的第二个Multi-Head Attention就只是基于Attention,它的输入Quer来自于Masked Multi-Head Attention的输出,Keys和Values来自于Encoder中最后一层的输出。
2.5 Transformer的输出
输出也很简单,首先经过一次线性变换,然后Softmax得到输出的概率分布,然后通过词典,输出概率最大的对应的单词作为我们的预测输出。
3.BERT
前面的铺垫终于做完了,我们的主角也终于要登场了。
BERT我自己把它一句话概括成多层Transformer的双向两阶段式语言模型。
3.1BERT原理
3.1.1 多层Transformer
BERT就是基于Transformer的模型,但是BERT只运用了Transformer的Encoder部分,多层Transformer也就是多个Encoder堆叠。Google发布了两个版本的BERT,一个Base版,一个Large版。
Base版本:L(Layers)=12,H(Hidden)=768,A(attention head)=12,Total Parameters=110M
Large版本:L(Layers)=24,H(Hidden)=1024,A(attention head)=16,Total Parameters=340M
还有一些基础的信息,BERT中共有12个transformer-block(也称为12层),还有768 embedding-dimension(字向量)的维度=768,还有个12注意力机制头的个数,也就是12个heads。
能把这两个版本的Total Parameters算出来就代表理解了多层Transformer。
模型的输入
通过上图我们可以看到BERT的输入部分由3部分组成:
1.Token embedding: 表示当前词的embedding
2.Segment Embedding: 表示当前词所在句子的index embedding,句子的Embedding,BERT的训练数据都是由两个句子构成的,那么每个句子有个句子整体的embedding项对应给每个单词。
3.Position Embedding :表示当前词所在位置的index embedding,LP中单词顺序是很重要的特征,需要在这里对位置信息进行编码,用来表示每个单词在输入序列中的位置信息。
因此将这三步所有参数汇总为:
( 32000 + 512 + 2 ) ∗ 768 + 12 ∗ ( 3 ∗ 768 ∗ 64 ∗ 12 + 768 ∗ 768 + 768 ∗ 4 ∗ 768 ∗ 2 ) = 109905408 = 110 M (32000+512+2)768+12(37686412+768768+7684768*2)=109 905 408=110M (32000+512+2)∗768+12∗(3∗768∗64∗12+768∗768+768∗4∗768∗2)=109905408=110M
为了计算方便,以上计算全部忽略了偏置b,因为b的数量相对来说比较少,暂且不计,比110M少的0.1M应该就是b的参数量。
3.1.2 双向
在说双向之前,之前讲到Transformer模型中有两种mask,分别是 padding mask 和 sequence mask。padding mask的目的是为了使输入序列中因为填充而没有意义的片段加上负无穷和softmax函数。而sequence mask是为了使得 decoder 不能看见未来的信息,不是训练的时候而是在预测的时刻把当前时刻之后的信息隐藏起来,做法就是产生一个上三角矩阵,上三角的值全为0,把这个矩阵作用在每一个序列上。
MLM(Masked Language Model)
又称掩码语言模型,实现双向的做法就是:随机抠掉语料中15%的单词(从输入的token中随机选择15%的token,而不是从总的BERT词表中随机选择15%),用[Mask]掩码代替原始单词,然后要求模型去正确预测被抠掉的单词。但是这里有个问题:训练过程大量看到[mask]标记,这会引导模型认为输出是针对[mask]这个标记的,但是真正后面在进行预测任务的时候是不会有这个标记的,这自然会有问题。为了避免这个问题,Bert改造了一下,15%的被上天选中要执行[mask]替身这项光荣任务的单词中,只有80%真正被替换成[mask]标记,10%被狸猫换太子随机替换成另外一个单词,10%情况这个单词还待在原地不做改动。
因为每次只能预测输入token的15%,而且是随机地,要想将整个句子都预测出来的话需要多迭代几次,这也是为什么BERT预训练需要消耗大量时间的原因。引用BERT作者原论文中的例子:
为什么要这样分成80%,10%,10%呢?
作者在原论文中也有解释:
BERT是一个多任务学习模型,上面MLM只是BERT预训练中的一个任务
BERT的[CLS]
[CLS]就是classification的意思,可以理解为用于两类下游的分类任务,分别是单文本分类任务和语句对分类任务。
单文本分类任务:于文本分类任务,BERT模型在文本前插入一个[CLS]符号,并将该符号对应的输出向量作为整篇文本的语义表示,用于文本分类。如下图所示。可以理解为:与文本中已有的其它字/词相比,这个无明显语义信息的符号会更“公平”地融合文本中各个字/词的语义信息。
语句对分类任务:该任务的实际应用场景包括:问答(判断一个问题与一个答案是否匹配)、语句匹配(两句话是否表达同一个意思)等。对于该任务,BERT模型除了添加[CLS]符号并将对应的输出作为文本的语义表示,还对输入的两句话用一个[SEP]符号作分割,并分别对两句话附加两个不同的文本向量以作区分,如下图所示。
BERT的输入表示
具体地,我们会将输入的自然语言句子通过WordPiece embeddings来转化为token序列。这个token序列的开头要加上[CLS]这个特殊的token,最终输出的[CLS]这个token的embedding可以看做句子的embedding,可以使用这个embedding来做分类任务。
由于句子对被pack到了一起,因此我们需要在token序列中区分它们,具体需要两种方式:
①在token序列中两个句子的token之间添加[SEP]这样一个特殊的token;
②我们为每个token添加一个用来学习的embedding来区分token属于句子A还是句子B,这个embedding叫做segment embedding。
NSP(Next Sentence Prediction)
BERT的训练数据是句子对的形式,NSP,指的是做语言模型预训练的时候,分两种情况选择两个句子,一种是选择语料中真正顺序相连的两个句子;另外一种是第二个句子从语料库中抛色子,随机选择一个拼到第一个句子后面。我们要求模型除了做上述的Masked语言模型任务外,附带再做个句子关系预测,判断第二个句子是不是真的是第一个句子的后续句子。之所以这么做,是考虑到很多NLP任务是句子关系判断任务,单词预测粒度的训练到不了句子关系这个层级,增加这个任务有助于下游句子关系判断任务。
BERT的输入由三部分相加组成:token embeddings、segment embeddings和position embeddings。
3.1.3 两阶段式模型
BERT是一个两阶段模型,分为Pre-Train 和 Fine-Tuning两阶段。上面介绍地都是BERT的Pre-Train部分,那么接下来就来看看BERT的Fine-Tuning部分。
Fine-Tuning就是在BERT预训练好的基础上再通过自己当前场景业务实际的数据来进行微调。
BERT的使用分为两个阶段:预训练(pre-training)和微调(fine-tuning)。预训练阶段模型通过两种不同的预训练任务来训练无标注数据。微调阶段模型使用预训练参数初始化,然后使用下游任务(downstream task)的标注数据来微调参数。
下游任务就是该领域称之为利用预先训练的模型或组件的监督学习任务。
BERT的一个显著特点是它在不同的任务上有统一的架构,使用时只需要在BERT后面接上下游任务的结构即可使用。
BERT 预训练的输出其实是输入中每个Token经过训练后的Embedding值,就是每个Token的词向量,然后当我们真正在使用的时候就可以根据自己的实际业务将这些Token词向量再进行Fine-Tune微调。
可以看到输出序列依旧是 [CLS] A [SEP] B [SEP],将其作为咱们Fine-Tune的输入,那么Fine-Tune的结构也应该按照BERT的预训练结构来设计。然后训练好了之后,就可以运用到各种NLP下游task中了。
接下来说说 BERT 具体的NLP下游任务应该怎么样实现。
句子关系类任务:Entailment,QA,语义改写,自然语言推理等任务都是这个模式,它的特点是给定两个句子,模型判断出两个句子是否具备某种语义关系
使用BERT预训练的模型参数处理这类问题,只需要给输入加上一个起始[CLS]和终结符号[SEP],句子之间加个分隔符[SEP]即可。对于输出来说,把第一个起始符号[CLS]对应的Transformer最后一层位置上面串接一个softmax分类层即可
分类任务:文本分类,情感分析等任务,特点是不管文章有多长,总体给出一个分类类别即可
对于分类问题,咱们一般都把一句话或者一篇文档当做一个整体,那么就是输入单句,只需要给输入文本增加起始[CLS]和终结符号[SEP],输出部分与句子关系类任务一样,是要把把第一个起始符号[CLS]对应的Transformer最后一层位置上面串接一个softmax分类层即可
序列标注:这是最典型的NLP任务,比如中文分词,词性标注,命名实体识别,语义角色标注等都可以归入这一类问题,它的特点是句子中每个单词要求模型根据上下文都要给出一个分类类别。
对于序列标注问题,输入部分和单句分类是一样的,只需要输出部分Transformer最后一层的每个单词对应位置都进行softmax分类
生成式任务:比如机器翻译,文本摘要,写诗造句,看图说话等都属于这一类。它的特点是输入文本内容后,需要自主生成另外一段文字。
关于生成式任务,原论文没提及,我个人感觉之所以没提及是因为BERT是一个有监督的模型,而对于像生成任务式这种,生成一句话,是不应该有一个统一的固定的答案地,就是我个人觉得生成式任务不适合用有监督的学习解决。人工智能那么就要像人,我们人不说别人,自己对于同一张图片每一次的描述可能都不一样,同一句英语每次翻译成汉语可能也会有些微的出入,因此用给定了固定生成式答案地有监督学习解决生成式问题都是不合理地。虽然说BERT也是可以做生成式任务地,我们通过BERT得到每个单词的词向量后,然后根据自己的业务数据,输入到Seq2Seq模型中再训练就能处理生成式任务了。但我个人觉得生成式任务采用有监督的学习模型是不合理地。
3.2 BERT训练方法
在下游任务中应用预训练语言模型表示的方法有两种:feature-based的方法和fine-tuning的方法。
截止BERT之前的预训练语言模型都是单向的(unidirectional),包括GPT和ELMo,这样的方法对句子层级的任务不是最优的,而且对于token层级的任务比如问答非常有害。BERT使用masked language model(MLM)的方法来预训练,这种方法能够训练一个双向的(directional)语言模型。除了masked language model的预训练的方法,BERT还使用了next sentence prediction的预训练方法。
4 项目复现Liunx
1)创建环境,下载预训练模型和权重参数
项目在liunx服务器上的部署还是得到了一位师兄的帮助。我也得回去学习一下子
首先新建虚拟环境env3,新建文件夹wjt3,把下载下来的bert-master项目放在wjt3这个文件夹下,在wjt3下再midir一个文件夹GLUE,这个文件夹中放一些数据集和权重参数文件。
从github官网上下载预训练模型uncased_L-12_H-768_A-12(不同实例任务下载的模型还不一样),先把uncased_L-12_H-768_A-12预训练模型放在GLUE这个文件夹中。
预训练模型uncased_L-12_H-768_A-12目录如下:
先用记事本打开的一个json文件,里面是一个像字典的结构,存了一些参数,这些参数是用来训练模型的。
{
"attention_probs_dropout_prob": 0.1,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"initializer_range": 0.02,
"intermediate_size": 3072,
"max_position_embeddings": 512,
"num_attention_heads": 12,
"num_hidden_layers": 12,
"type_vocab_size": 2,
"vocab_size": 30522
}
接下来的三个文件就是训练下来大的权重参数文件。
最后一个文件就是语料库,当前模型用到的词和对应的ID。
2)下载数据集
接下来就下载一个数据集:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nlBG4WID-1633261248791)(E:\博客写作图片\QQ截图20210929104309.png)]
后缀为msi是windows操作系统的数据库类型的可执行文件,liunx无法运行,这该怎么办呢?先打开这个
一探究竟,这里有几行命令,切换到项目目录里一个一个来:
mkdir MRPC
然后把下载到windows里面的msi文件拖入MRPC中
cabextract MSRParaphraseCorpus.msi -d MRPC
有错误的话就pip一下子cabextract
cat MRPC/_2DEC3DBE877E4DB192D17C0256E90F1D | tr -d $'\r' > MRPC/msr_paraphrase_train.txt
cat MRPC/_D7B391F9EAFF4B1B8BCE8F21B20B1B61 | tr -d $'\r' > MRPC/msr_paraphrase_test.txt
rm MRPC/_*
rm MSRParaphraseCorpus.msi
以上命令都可以正常执行,但是并没有下载数据集啊,接下来我就自己整不会了,在一个师兄的帮助下还是成功的下载了数据集。直接开门见山为什么这么麻烦呢?是因为
是用python2写的,而我的虚拟环境env3是用的python3.6,整体思路就是把这段代码改变成3的版本。
所以先vim一个名为download_glue_data.py的文件,为什么不能在jupyter里面新建文件前面加感叹号呢?肯定不行,因为jupyter中新建的文件后缀一般都是 .ipynb ,这类型文件执行命令类代码时只能按块执行,这里的download_glue_data.py是python的可执行文件,且其中有main函数(main函数一定会调用其他函数),用jupyter来new的话一定会导致文件的性质改变从而运行失败。
进入文件后,i进入输入模式,接下来用鼠标把所有文件都复制过去,在esc进入命令模式,再:进入底线模式,最后wq保存退出。
python download_glue_data.py --data_dir glue_data --tasks all
用这个命令来运行download_glue_data.py文件,结果刚刚下载了两种数据集就出现问题了。
错误说‘URLLIB’没有被定义,上谷歌搜一圈,其实没搜出个啥,细心的师兄还是发现了大写有问题,原来在python3对比python2中,大写被换成了小写。所以再次进入vim编辑器,把大写的URLLIB都换成小写的urllib。搜索是先进入底线模式,然后/被搜索的内容,再进入输入模式一个一个完成替换。但是又出现了错误:
说明上一个错误被成功修复,现在有了新的错误,谷歌一搜,python2 与python3的urllib不同在与python3要加上.request
比如:import urllib.request
res=urllib.request.urlopen(html)
data = urllib.request.urlretrieve(“http://…”)
照猫画虎继续,错误被改正之后,又出现了新的问题:
话说这作者也不懂的更新一下自己的代码,没办法继续改吧,直接在代码里加入import io。
成功啦,但是我真的是服了,就想跑个MRPC,结果就MRPC没下载成。。。。。算了,从博客里提供的百度云网盘中提供了一份数据集,没办法只能手动下载了。liunx操作系统中解压只能解压到当前文件夹,所以只能新建文件夹再解压。
可算把数据集都下载好了。
简单介绍一下MRPC就是判断两个字符串是不是表达同一个意思(二分类任务)。
首先用记事本打开train.tsv:
Quality表示标签,第一个ID表示第一个String串的ID,第二个ID表示第二个String串的ID,判断这两个String串是不是说的是同一个意思。
测试集就没有标签了。但是文件结构有点混乱:重新调整一下:把预训练模型和数据集都放在了GLUE中。
接下来就要完成基于MRPC任务用BERT模型解决每一步应该做一些什么事情
3)项目参数
写到这里我感觉用linux搞这个项目有点麻烦,都想转windows了。
export BERT_BASE_DIR=/path/to/bert/uncased_L-12_H-768_A-12
export GLUE_DIR=/path/to/glue
然后进入项目里面,这次不vim文件了,直接把脚本命令粘贴在命令行里:
看一下上面的参数,因为数据集是他们提供的,如果使用的是自己的数据集的话,就得修改这些参数,首先任务名字MRPC,做不做训练,做不做验证,数据集的位置,语料表,bert的超参数用config文件传进去,需要做微调的预训练模型,每句话的最长长度,batch_size自己往小挑一挑,学习率,epoch,tensorflow.save保存训练好了的模型保存的位置(这里需要提前在GLUE文件夹下把output文件夹提前创建好了)。
这里出现各种各样的问题,一般都是版本问题,比如缺少tensorflow,pip install tensorflow之后,还是会出现版本问题,pip install tensorflow-gpu==1.15.0之后,是不需要卸载掉原来的tensorflow的,因为会自动卸载,直接覆盖。
但是有些东西不会自动卸载,比如这个gast,得手动pip uninstall才行。就依次这样缺啥换啥,版本号不对就卸载了重新安装。就这样就把参数调好了。
遇到这种脚本文件,在Liunx操作系统上面,就得分开粘贴执行,不要一次性全部都站上去
![QQ截图20210930220709](E:\博客写作图片\QQ截图20210930220709.png)export BERT_BASE_DIR=/path/to/bert/uncased_L-12_H-768_A-12
export GLUE_DIR=/path/to/glue
export TRAINED_CLASSIFIER=/path/to/fine/tuned/classifier
python run_classifier.py \
--task_name=MRPC \
--do_predict=true \
--data_dir=$GLUE_DIR/MRPC \
--vocab_file=$BERT_BASE_DIR/vocab.txt \
--bert_config_file=$BERT_BASE_DIR/bert_config.json \
--init_checkpoint=$TRAINED_CLASSIFIER \
--max_seq_length=128 \
--output_dir=/tmp/mrpc_output/
结果就是又错了,错误的原因师兄帮我找到了,以后在git上拉代码的时候,遇到看不懂的代码或者命令,一定要搜一下看看。比如再次遇到export,首先export命令在哪运行都可以,但是配置参数就必须要在有需要配置参数文件的目录下才行。
重点:遇到带路径的命令行,一定要会修改这个路径修改成自己的路径,因为所有带路径的命令,都有斜杠,非常明显。一般都是如果是字母全是大写的全局变量就不用修改,一般如果是带斜杠的小写十有八九肯定得修改。