写在前面:本文主要参考了张俊林等大神的文章,这里对其相关文章进行了更精简的总结整理和扩充,更多内容请详见文末的参考文献,尊重原创。
——口天丶木乔

0. Pre-train

预训练技术:深度学习中,多层叠加网络结构,可以先用大型公共训练集对这个网络进行预先训练,学到的参数存起备用。在面临其他三方任务(采用相同网络结构)时,在较浅几层网络中的参数直接选用预训练好的参数初始化,而深层参数仍随机初始化。之后用新任务数据训练网络时,浅层参数有两种做法:

  • “Frozen”:训练过程中浅层参数保持不动;
  • “Fine-Tuning”:训练过程中对参数进行微调。

预训练的好处:

  • 训练数据集少,网络太深,参数过多,训练集很难较好的训练处网络参数;
  • 极大加快任务收敛。

为啥预训练的这种思路是可行的:

  • 底层特征可复用性
  • 高层特征相关性

image.png
对于层级的CNN结构来说,不同层级的神经元学习到了不同类型的图像特征,由底向上特征形成层级结构,如上图所示,如果我们手头是个人脸识别任务,训练好网络后,把每层神经元学习到的特征可视化,会看到最底层的神经元学到的是线段等特征,第二个隐层学到的是人脸五官的轮廓,第三层学到的是人脸的轮廓,通过三步形成了特征的层级结构,越是底层的特征越是所有不论什么领域的图像都会具备的比如边角线弧线等底层基础特征,越往上抽取出的特征越与手头任务相关。正因为此,所以预训练好的网络参数,尤其是底层的网络参数抽取出特征跟具体任务越无关,越具备任务的通用性,所以这是为何一般用底层预训练好的参数初始化新任务网络参数的原因。而高层特征跟任务关联较大,实际可以不用使用,或者采用Fine-tuning用新数据集合清洗掉高层无关的特征抽取器。
预训练在NLP领域提升效果并不明显,直到2018年的NLP预训练及迁移学习的元年开启(ELMO、GPT、ULM-FiT、BERT)

1. word embedding(NNLM 2003)

这个工作是Bengio早在2003年就发表在JMLR上的论文,它生于2003,不过火于2013(w2v Google):
截屏2020-02-18下午2.29.26.png
前面任意单词review BERT:NLP的高光逆袭时刻 - 图3用Onehot编码作为原始单词输入,之后乘以矩阵Q后获得向量review BERT:NLP的高光逆袭时刻 - 图4,每个单词的向量review BERT:NLP的高光逆袭时刻 - 图5拼接,上接隐层,然后接softmax去预测后面应该后续接哪个单词。这个review BERT:NLP的高光逆袭时刻 - 图6是什么?这其实就是单词对应的Word Embedding值,那个矩阵Q包含V行,V代表词典大小,每一行内容代表对应单词的Word embedding值。只不过Q的内容也是网络参数,需要学习获得,训练刚开始用随机值初始化矩阵Q,当这个网络训练好之后,矩阵Q的内容被正确赋值,每一行代表一个单词对应的Word embedding值。所以,通过这个网络学习语言模型任务,这个网络不仅自己能够根据上文预测后接单词是什么,同时获得一个副产品,就是那个矩阵Q,这就是单词的Word Embedding是被如何学会的。之后出现的以产生词向量为主要目标的工具Word2Vec或者Glove与这个NNLM基本类似,我在另一篇博文中对其进行了详细的阐述。
WE(word enbedding)其实就是标准的预训练过程,要理解这一点要看看学会Word Embedding后下游任务是怎么用它的。
截屏2020-02-18下午2.39.58.png
假设如上图所示,我们有个NLP的下游任务,比如QA,就是问答问题,所谓问答问题,指的是给定一个问题X,给定另外一个句子Y,要判断句子Y是否是问题X的正确答案。问答问题假设设计的网络结构如上图所示,句子中每个单词以Onehot形式作为输入,然后乘以学好的Word Embedding矩阵Q,就直接取出单词对应的Word Embedding了。那个Word Embedding矩阵Q其实就是网络Onehot层到embedding层映射的网络参数矩阵。所以使用Word Embedding等价于把Onehot层到embedding层的网络用预训练好的参数矩阵Q初始化了。这跟前面讲的图像领域的低层预训练过程其实是一样的,区别无非Word Embedding只能初始化第一层网络参数,再高层的参数就无能为力了。下游NLP任务在使用Word Embedding的时候也类似图像有两种做法,一种是Frozen,就是Word Embedding那层网络参数固定不动;另外一种是Fine-Tuning,就是Word Embedding这层参数使用新的训练集合训练也需要跟着训练过程更新掉。
WE存在的最大问题就是无法解决多义词问题:
截屏2020-02-18下午2.49.25.png
如上图所示,多义词Bank,有两个常用含义,但是Word Embedding在对bank这个单词进行编码的时候,是区分不开这两个含义的,因为它们尽管上下文环境中出现的单词不同,但是在用语言模型训练的时候,不论什么上下文的句子经过word2vec,都是预测相同的单词bank,而同一个单词占的是同一行的参数空间,这导致两种不同的上下文信息都会编码到相同的word embedding空间里去。所以word embedding无法区分多义词的不同语义,这就是它的一个比较严重的问题。这一问题知道2018年才由ELMO模型提供了一种简洁优雅的解决方案。

2. ELMO (2018)

ELMO原论文是顶会NAACL2018年的最佳论文,很值得借鉴学习。ELMO本质思想是根据当前上下文对Word Embedding动态的调整。WE本质上是个静态的方式,所谓静态指的是训练好之后每个单词的表达就固定住了,以后使用的时候,不论新句子上下文单词是什么,这个单词的Word Embedding不会跟着上下文场景的变化而改变,而ELMO会根据下游任务单词的上下文的不同而动态的调整该单词在预训练时产生的WE。
截屏2020-02-18下午3.02.03.png
ELMO采用了典型的两阶段过程(NNLM类似),第一个阶段是利用语言模型进行预训练;第二个阶段是在做下游任务时,从预训练网络中提取对应单词的网络各层的Word Embedding作为新特征补充到下游任务中。上图展示的是其预训练过程,它的网络结构采用了双层双向LSTM,语言模预型训练的任务目标是分别根据单词的上文和下文去正确预测该单词。从该网络可以看出,每个单词产生三个与之对应的embedding,最底层是单词的Word Embedding,往上走是第一层双向LSTM中对应单词位置的Embedding,这层编码单词的句法信息更多一些;再往上走是第二层LSTM中对应单词位置的Embedding,这层编码单词的语义信息更多一些。而这三者后面都有用。
截屏2020-02-18下午3.12.06.png
上面介绍的是ELMO的第一阶段:预训练阶段。那么预训练好网络结构后,如何给下游任务使用呢?上图展示了下游任务的使用过程,比如我们的下游任务仍然是QA问题,此时对于问句X,我们可以先将句子X作为预训练好的ELMO网络的输入,这样句子X中每个单词在ELMO网络中都能获得对应的三个Embedding,之后给予这三个Embedding中的每一个Embedding一个权重a,这个权重可以学习得来,根据各自权重累加求和,将三个Embedding整合成一个。然后将整合后的这个Embedding作为X句在自己任务的那个网络结构中对应单词的输入,以此作为补充的新特征给下游任务使用。对于上图所示下游任务QA中的回答句子Y来说也是如此处理。因为ELMO给下游提供的是每个单词的特征形式,所以这一类预训练的方法被称为“Feature-based Pre-Training”。
ELMO的局限:

  • LSTM特征抽取能力远弱于Transformer;
  • ELMO采取双向拼接这种融合特征的能力可能比Bert一体化的融合特征方式弱,但是,这只是一种从道理推断产生的怀疑,目前并没有具体实验说明这一点。

    3. GPT (2018)

    GPT和ELMO很类似,不过有以下两点不同:

  • 特征抽取器不是用的RNN,而是用的Transformer;

  • GPT的预训练虽然仍然是以语言模型作为目标任务,但是采用的是单向的语言模型。

截屏2020-02-18下午3.20.08.png

GPT在下游使用阶段有一点变化,对于不同的下游任务来说,本来你可以任意设计自己的网络结构,现在不行了,你要向GPT的网络结构看齐,把任务的网络结构改造成和GPT的网络结构是一样的。然后,在做下游任务的时候,利用第一步预训练好的参数初始化GPT的网络结构,这样通过预训练学到的语言学知识就被引入到你手头的任务里来,对于不同的任务改造如下:
截屏2020-02-16上午10.58.29.png
GPT的局限:

  • 语言模型为单向模型,即只利用单词的上文或者下文来预测该单词。

    4. BERT (2018)

    4.0 Look in High Level

    (Bidirectional Encoder Representation from Transformers)下图是原论文给出的框架对比,可以看出BERT并没有太大的创新,它其实是将ELMO,GPT的全部优点集合在一体,再加上一些小trick,但其效果就是好,且普适性强,在NLP领域可谓是引起了极大的轰动:
    截屏2020-02-16上午10.58.17.png
    Bert采用和GPT完全相同的两阶段模型,首先是语言模型预训练;其次是使用Fine-Tuning模式解决下游任务。和GPT的最主要不同在于在预训练阶段采用了类似ELMO的双向语言模型,当然另外一点是语言模型的数据规模要比GPT大。
    Bert本身在模型和方法角度有什么创新呢?就是论文中指出的Masked 语言模型和Next Sentence Prediction。而Masked语言模型本质思想其实是CBOW,但是细节方面有改进。
    v2-146b89cf7ec3eceb349c0d39f8aea228_r.jpg

    4.1 Input

    Bert输入部分由三部分组成,其中segment emb为句子embedding,用于句子粒度间的预测。
    截屏2020-02-18下午3.31.34.png

    4.2 Masked Language Model

    v2-5893870247eedd20b9cb43507d065150_r.jpg
    Masked双向语言模型像上图展示这么做:随机选择语料中15%的单词,把它抠掉,也就是用[Mask]掩码代替原始单词,然后要求模型去正确预测被抠掉的单词。但是这里有个问题:训练过程大量看到[mask]标记,但是真正后面用的时候是不会有这个标记的,这会引导模型认为输出是针对[mask]这个标记的,但是实际使用又见不到这个标记,这自然会有问题。为了避免这个问题,Bert改造了一下,15%的被上天选中要执行[mask]替身这项光荣任务的单词中,只有80%真正被替换成[mask]标记,10%被狸猫换太子随机替换成另外一个单词,10%情况这个单词还待在原地不做改动。这就是Masked双向语言模型的具体做法。

    4.3 Next Sentence Prediction

    v2-5a51a24c706135ddb9515791715be9bc_r.jpg
    至于说“Next Sentence Prediction”,指的是做语言模型预训练的时候,分两种情况选择两个句子,一种是选择语料中真正顺序相连的两个句子;另外一种是第二个句子从语料库中抛色子,随机选择一个拼到第一个句子后面。我们要求模型除了做上述的Masked语言模型任务外,附带再做个句子关系预测,判断第二个句子是不是真的是第一个句子的后续句子。之所以这么做,是考虑到很多NLP任务是句子关系判断任务,单词预测粒度的训练到不了句子关系这个层级,增加这个任务有助于下游句子关系判断任务。所以可以看到,它的预训练是个多任务过程。这也是Bert的一个创新。

    4.4 Task specific-Models

    截屏2020-02-18下午3.49.10.png

    5. 四者联系

    v2-330788d33e39396db17655e42c7f6afa_r.jpg
    可以看出:Bert最关键两点,一点是特征抽取器采用Transformer;第二点是预训练的时候采用双向语言模型。此外,bert并没有像ELMO那样采用双向模型分别进行预测,它采用的是将部分单词mask掉后一体化预测它,这其实和w2v中的CBOW模型也非常像。

    6. 细节思考

    往往看下来是理解了,但是难免有很多细节会看不到,侧面可以根据不同的问题反思,进一步了解bert的各个细节

    1. 为什么Transformer 需要进行 Multi-head Attention,是如何实现每个head提取的信息空间互斥的?

    原论文的表述:Multi-head attention allows the model to jointly attend to information from different representation subspaces at different positions. With a single attention head, averaging inhibits this. 即多头机制可以更好的从不同的表达空间中获取联合信息

    因为attetion机制的有效性本身就没有很好的数学完备的理论解释,大部分都是直觉直观化的解释,所以这里也给出较为直观化的解释:

    直观化:多头的注意力有助于网络捕捉到更丰富的特征/信息。可以把多头注意力看作是一种ensemble,模型内部的集成。

    较为数学化的解释:为什么不同的头回捕捉到不同的信息?根据原论文多头的公式: 截屏2021-04-14 上午11.42.37.png 可以看出,每个头的处理流程都完全一样,所有的参数随机初始化,然后用相同的方法前传,在输出端得到相同的损失,用相同的方法后传,更新参数。 其中唯一的变量就是随机初始化,不同的头随机初始化成不同的矩阵,从而引入了noise,最终导致了不同的头更新的参数不完全相同,从而捕捉到不同的信息。因为多头之间的参数是彼此独立不共享(相互独立)的,学习到不同的参数矩阵后结合(拼接)在一起,泛化性能会更好, 其核心思想在于,允许模型在不同的表示子空间里学习到相关的信息,从而能够抽取到更加丰富的特征信息。

    延展一下,其原理有点类似于采用QKV三个embedding表示token,参考下面回答。 此外需注意多头实现的细节,每个attention的输入不再是原始词向量QKV,而是将其乘以相应参数矩阵,映射为更小的维度d/h,整合后维度与原向量一致为nd,之后再乘以参数矩阵review BERT:NLP的高光逆袭时刻 - 图21(维度为review BERT:NLP的高光逆袭时刻 - 图22)映射为nd的矩阵,这样做是为了保证最终多头拼接后的输出维度与原attention相似,并且计算复杂度与原机制一致。

5. 为什么在进行多头关注的时候需要对每个head进行降维切割?

在不增加时间复杂度的情况下,同时,借鉴CNN多核的思想,在更低的维度,在多个独立的特征空间,更容易学习到更丰富的特征信息。

对于 Multi-Head Attention,简单来说就是多个 Self-Attention 的组合,但多头的实现不是循环的计算每个头,而是通过 transposes and reshapes,用矩阵乘法来完成的。

因此Multi-Head Attention时间复杂度也是 ,复杂度相较单头并没有变化,主要还是transposes and reshapes 的操作,相当于把一个大矩阵相乘变成了多个小矩阵的相乘。 正是经过这样维度的“分割”,才有了Multi-Head Attention,不增加复杂度,又提升了效果,但其实并没有什么必然性,是实验之后的结果,验证了它的有效性。

2. Transformer为什么Q和K使用不同的权重矩阵生成,为什么不能使用同一个值进行自身的点乘?

其实本质上和第一个问题相似(多头机制),都是为了在不同的表达空间上增强模型的泛化能力。

从源头说起,两个向量的点乘的物理意义是表示两个向量的相似度,而attention机制中的QKV矩阵都是表示同一个句子中的不同token向量所组成的矩阵,矩阵中的每一行表示每个token的embedding。

而Q与K相乘代表每个token与其他token相乘后的attention分数权重,相当于每个token有三个embedding,这样是为了将每个token在三个不同的空间进行映射表示,提升泛化性,若每个token都用一个向量计算,最终的attention矩阵就是一个对称矩阵,因为都是同一个Q,即投射到了同一个空间,泛化性差。

因为一开始QKV三个矩阵是随机初始化的,所以计算后的矩阵也是完全不同的矩阵,表达能力更强。

延展一下,其实FFM对于FM的优化思想和这个很类似,FFM是对于每个特征训练针对不同的field训练多个不同的embedding用于与其他field的特征做交叉组合,增强表达泛化能力。

3. 为什么在进行softmax之前需要对注意力分数(权重)进行scaled(为什么除以dk的平方根),并使用公式推导进行讲解?

原论文的解释是:向量的点积结果会很大,将softmax函数push到梯度很小的区域,scaled会缓解这种现象。

深入一下,比较大的输入会使softmax函数的梯度变额很小,不利于模型训练的收敛。为什么会这样,可以参考sigmoid曲线,softmax在二分类情况下就退化为了sigmoid函数。 截屏2021-04-14 下午4.05.11.png 接下来,为啥除以维度的平方根?原论文注解处之处,考虑最简单的情况,假设向量Q和K的各个分量是互相独立的随机变量,均值是0,方差是1,那么矩阵乘法QK的均值是0,方差是dk。 证明也很简单,只要熟悉统计概率中的期望和方差的相关性质就不难推导出: 截屏2021-04-14 下午4.07.34.png

4. 在计算注意力分数的时候如何对padding做mask操作?

mask是将一些不要用的值掩盖掉,使其不产生作用。有两种mask。

第一种是padding mask,在所有scaled dot-product attention计算attention值时都用到; 因为输入序列长度不固定,需要padding到相同长度,针对于padding的位置mask掉,具体做法就是赋予一个很小(负无穷)的值,这样点积缩放再经过softmax后就变为了0,即其位置IDE注意力分数权重为0,不将其考虑进来。

第二种是sequence mask,在decoder的self-attention里面用到。使其不能看到下文信息,当encoder和decoder的输入序列长度一致时需要在decoder侧利用这种策略。具体,可产生一个对角线为0的上三角矩阵,将其作用到每个decoder的输入列上。

bert模型只利用的encoder,所以采用了padding mask方式,同时,在预训练阶段采用MLM的方式mask某些token,

6. 大概讲一下Transformer的Encoder模块?

https://jalammar.github.io/illustrated-transformer/

7. 为什么inputs embedding要加入positional encoding?你还了解些关于位置编码的技术,各自的优缺点是什么?

self-attentino与位置无关,引入位置信息会使模型效果更好。 transform原论文位置编码(绝对位置)如下: 截屏2021-04-14 下午4.34.12.png 注意,bert源码中位置编码时随机初始化的,通过训练自学习得到最终的位置编码。

8. Transformer 为什么使用 layer normalization,而不是其他的归一化方法?

主要还是NLP数据与CV数据特性的差别对训练过程产生了影响,使得训练中batch的统计量不稳定。

(NLP任务里普遍BN比LN差太多,迄今为止,依然没有一个非常严谨的理论来证明LN相比BN在NLP任务里的优越性。甚至,连BN自身为什么work的问题都一直存在争议。)

NLP和图像特征不同,BN更强调不同数据间的统计相似性,LN更在意数据内变量的相关性,看哪个方差小了。

BN本质上需要统一一个minibatch内的实例的一阶(均值)和二阶(方差)统计量,所以它依赖于一个batch内的实例之间的相互影响关系。而LN不依赖于多个实例,只针对单个实例来获取相应统计量。

9. Transformer的并行化

Transformer的并行化主要体现在self-attention模块。QKV可以直接并行化计算

10. BPE和WordPiece

BPE(字节对编码),是对英文更细粒度的分词方法,能够更好的解决OOV问题。 论文源码如下: 先将词分为一个个字符,接着在词的范围内统计字符对出现的次数,每次将次数最多的字符对保存,直至循环结束。 截屏2021-04-28 下午2.28.33.png WordPiece算法可以看作是BPE的变种。不同点在于,WordPiece基于概率生成新的subword而不是下一最高频字节对。

11. Transformer训练的时候学习率是如何设定的

学习率的变化如下图所示 v2-a0cfb3b713233ebe91bb3ef43e0f69f6_r.jpg 一开始会设定4000step的warm-up。warm-up机制,即在一开始设置很小的学习率,之后的学习率设置和平时一样(开始学习率较大,慢慢缩小)。

一开始就使用较大的学习率而不采用warm-up会使训练不稳定,一开始用较小的学习率(wram-up)可以减缓震荡,但会使收敛极其缓慢。

warm-up机制一开始在ResNet论文中提出;由于刚开始训练时,模型的权重(weights)是随机初始化的,此时若选择一个较大的学习率,可能带来模型的不稳定(振荡),选择Warmup预热学习率的方式,可以使得开始训练的几个epoches或者一些steps内学习率较小,在预热的小学习率下,模型可以慢慢趋于稳定,等模型相对稳定后再选择预先设置的学习率进行训练,使得模型收敛速度变得更快,模型效果更佳

On Layer Normalization in the Transformer Architecture论文中证实了warm-up的必要性,并提出了优化方案。它提出通过将原始transformer的post-LN改进为pre-LN,可以不必warm-up操作,从而明显减少预训练的时间。

12. Transformer的一些细节

https://www.zhihu.com/question/362131975

13. 为什么Bert的三个Embedding可以进行相加?

因为神经元本质上的操作就是对不同的向量乘以权重后再相加,即w向量乘以x向量的转置。另,向量相加本质上其实和向量拼接一样,相加是拼接的特殊情况而已。

向量相加、拼接和点积的区别和使用场景:

举个简单例子说明一下,A=[a1, a2], B=[b1, b2],假设经过最简单的全连接变化后:

  1. 向量相加:权重w=[w1, w2], 则输出WX=w1a1 + w2a2 + w1b1 + w2b2
  2. 向量拼接:w=[w11, w22, w33, w44],则输出WX=w11a1 + w22a2 + w33b1 + w44b2
  3. 向量点积:w=[w1, w2],则输出WX=w1a1b1 + w2a2b2

可以看到当w11=w22=w1,w33=w44=w2时,向量相加和拼接是等价的,因此拼接是可以替代相加的,相应模型的参数和复杂度也会增加(模型表达能力会更强),但训练难度也会增加。

对于点乘积形式需要从另外的角度去思考,当我们进行反向梯度传播时,比如我们对于a1, b1 求偏导:

  1. 特征相加:a1的偏导为w1, b1的偏导为w2
  2. 特征拼接:a1的偏导为w11, b1的偏导为w33
  3. 特征点乘:a1的偏导为w1b1,b1的偏导为w2b2

可以看出对于相加和拼接的情况,不同特征之间的维度是互不相关的,而点乘不同特征对应维度之间是相互关联的。对于transformer来说,向量的三个特征分别是字特征、位置特征、和句子分隔特征(向量),三者应该是相互独立互不关联的,所以不用点积的形式。

总结三点:

  1. 向量特征相加是拼接的特殊形式,二者是等价的
  2. 特征拼接增加了模型参数,增加的模型表达能力,但训练难度也会增加
  3. 向量特征点积会来带特征之间对应维度的关联性,如果不同向量特征之间需要保持学习独立的表示,不推荐用向量点积的形式作为输入

14. 为什么BERT输入的最大长度要限制为512?模型复杂度比较、如何解决长文本问题?

长度限制为512,可以从两方面考虑,一方面是机器的gpu的缓存空间能力限制,一方面是计算时长的限制。

第一方面不必多说,主要说说第二方面,self-attention的时间复杂度为O(n^2*d),n为句子拆分(tokenizer)后的长度(去除掉[CLS]和[SEP]实际是大于510的),d为词向量维度。

transformer原论文给出的具体不同模型的时间复杂度比较如下: 截屏2021-04-30 上午10.47.58.png 一个一个详细分析: transformer:时间复杂度主要体现在attentino矩阵的计算,即review BERT:NLP的高光逆袭时刻 - 图29,其中review BERT:NLP的高光逆袭时刻 - 图30为两个矩阵相乘,维度分别为review BERT:NLP的高光逆袭时刻 - 图31review BERT:NLP的高光逆袭时刻 - 图32,时间复杂度为review BERT:NLP的高光逆袭时刻 - 图33,之后与V矩阵相乘,两个矩阵的维度分别为review BERT:NLP的高光逆袭时刻 - 图34review BERT:NLP的高光逆袭时刻 - 图35,时间复杂度也为review BERT:NLP的高光逆袭时刻 - 图36。限制的self-attetion,每个token只能与周围r个元素进行交互,所以时间复杂度为review BERT:NLP的高光逆袭时刻 - 图37 RNN:截屏2021-04-30 上午11.42.25.png CNN:

那如何处理更长文本?

  1. 直接截断:最简单的方法,直接截断,但可能会丢失原句某些重要信息。
  2. 抽取重要片段:抽取长文本的关键句子作为摘要,作为输入。
  3. 优化模型:通过优化模型从根源上解决长文本问题,如LongFormer和BigBird(google2021)通过稀疏self-attention矩阵(滑动窗口、全局att、随机att)来优化,使self-attention的时间复杂度从n^2降低至线性复杂度,从而能够处理更长的文本(原论文是能够处理8倍的更长数据)。

15. 为什么BERT选择mask掉15%这个比例的词,可以是其他的比例吗?

15%的概率是通过实验得到的最好的概率,xlnet也是在这个概率附近,说明在这个概率下,既能有充分的mask样本可以学习,又不至于让segment的信息损失太多,以至于影响mask样本上下文信息的表达。然而因为在下游任务中不会出现token“”,所以预训练和fine-tune出现了不一致,为了减弱不一致性给模型带来的影响,被mask的token有80%的概率用“”表示,有10%的概率随机替换成某一个token,有10%的概率保留原来的token,这3个百分比也是多次实验得到的最佳组合,在这3个百分比的情况下,下游任务的fine-tune可以达到最佳的实验结果。

16. 为什么BERT在第一句前会加一个[CLS]标志?

原论文中表示训练好后的cls标识位向量可以用于表示句向量来进行下游的分类任务。因为相对于句子中的其他词来说,该词能更公平的同和句子中的各部分信息,从而更好的表达句子的语义。 另,bert源码中,输出有两种,一种是get_pooled_out(),输出的是[CLS]标志位对应的向量(shape为[batch_size, hidden_size]),另一种是get_sequence_out(),是整个句子每个token的向量(包括CLS,shape为[batch_size, seq_length, hidden_size])。

17. BERT中的非线性和激活函数

bert中的非线性主要体现在两方面,一个是self-attention机制,一个是FFNN的激活函数GELU。 详情可以查看损失函数一节讲解

18. Bert和Transformer在loss上的差异

首先简介一下BERT预训练的loss的计算:

BERT的损失函数有两部分,分别是MLM和NSP两个任务对应的损失函数,这样通过这两个任务的联合学习,可以使得 BERT 学习到的表征既有 token 级别信息,同时也包含了句子级别的语义信息。

被mask的token经过encoder后乘以embedding matrix的转置会生成在vocab上的分布,然后计算分布和真实的token的one-hot形式的cross entropy,最后sum起来当作loss。这两部分loss相加起来当作total loss,利用adam进行训练。 截屏2021-05-06 下午3.11.30.png MLM任务中,是只计算mask中的部分的损失(查看源码可知,label_weights相当于过滤器,通过tf.reduce_sum和label_weights将未mask的loss过滤掉了),相当于词典为|V|的多分类任务(交叉熵损失函数),即: v2-2a6c4b3c16c4b41eadee28febb42628a_r.jpg 而NSP任务相当于二分类任务: v2-29cb9d4276d9dd05824fcbc9e3cd27f7_r.png 二者联合起来即为bert的联合损失函数: v2-50aa01d5d92ed3ae9674b4d32594291f_r.png

以上是bert预训练的loss计算过程,其次是微调阶段的loss(预训练和微调两阶段的loss不同):

微调阶段的loss是根据不同的下游任务特性来设计与之对应的loss的(具体不同下游任务的loss和预训练的loss计算详情可参考源码

在分类任务中就是[cls]这个token的emb经过1层Dense,然后接一个多分类的loss; QA任务中会在paragraph上的token中预测一个起始位置,一个终止位置,然后以起始位置和终止位置的预测分布和真实分布为基础设计loss; 序列标注任务中会预测每一个token的词性,然后以每一个token在词性的预测分布和真实分布为基础设计loss

而transformer的loss是在decoder阶段计算的。而transformer在decoder之后就直接计算loss了,中间没有Dense操作。

19. Bert的mask为何不学习transformer在attention处进行屏蔽score的技巧?

二者的目的不同。

bert是语言预训练模型,需要充分考虑上下文的信息。 而transformer是seq2seq的,第i个位置需要利用它之前的信息,而与之后位置的信息无关。

所以被mask的词是需要与其他词进行self-attention的,而不是在attention时屏蔽掉,这样就无法利用上线文信息来预测这个词了。

另一种角度就是为了防止信息泄露,被mask词的“内积偏好” 也是一种信息泄露,比如如果句子里刚好有同样的词被分到的attend就会很高。要做到完全无信息泄露确实最好的办法还是用[MASK]来替换。

20.为什么BERT句向量表现出来的语义相似度计算效果不佳

BERT句向量的空间分布是不均匀的,受到词频的影响。因为词向量在训练的过程中起到连接上下文的作用,词向量分布受到了词频的影响,导致了上下文句向量中包含的语义信息也受到了破坏。image.pngBERT句向量空间是各向异性的,高频词分布较密集且整体上更靠近,低频词的词向量分布较稀疏,因此它们周围存在较多的“空洞”,所谓的“空洞”即几乎不能表征语义或者说在进行语义表征时,该部分空间没有被使用到,是语义不明确的(poorly definded)。嵌入在向量空间中占据一个狭窄的圆锥体。高频离原点近,低频离原点远;句向量是对句子中的词的词向量取平均池化得到的,是保凸性运算,而BERT词向量空间存在“空洞”,即BERT句向量空间在某种程度上说是语义不平滑的。BERT词向量空间存在“空洞”,当句向量落到这些“空洞”时,语义的不确定性就影响了相似度。因为高词频的词和低频词的空间分布特性,导致了相似度计算时,相似度过高或过低的问题。在句子级:如果两个句子都是由高频词组成,那么它们存在共现词时,相似度可能会很高,而如果都是由低频词组成时,得到的相似度则可能会相对较低;在单词级:假设两个词在语义上是等价的,但是它们的词频差异导致了它们空间上的距离偏差,这时词向量的距离就不能很好的表征语义相关度。

21. BERT线上快速infer优化方法

22. BERT的分词(中英)原理,字和词的各自优缺点,如何解决OOV问题

具体分词原理参考BERT源码中的分词策略部分。 中文分字处理的优点: 1、参数更少,不容易过拟合; 2、不依赖于分词算法,避免边界切分错误; 3、没那么严重的稀疏性,基本上不会出现未登录词。 分词的优点:

  1. 序列变短,处理速度更快;

23. bert中的正则化

  1. dropout
  2. label smoothing

v2-56899017cd0d5c113edc8002997381d8_r.jpeg v2-858823f138177de7f61b725b5075e491_r.png 将分类标签平滑化,防止过拟合。模型通过抑制正负样本输出差值,使得网络有更强的泛化能力。

  1. Adamw。

目前bert训练采用的优化方法就是adamw,对除了layernorm,bias项之外的模型参数做weight decay。 L2 regularization 和 Weight decay 只在SGD优化的情况下是等价的。Adam优化的最终精度略低于SGD。其实Adam本身没有问题,问题在于目前大多数DL框架的L2 regularization实现用的是weight decay的方式,而weight decay在与Adam共同使用的时候有互相耦合。

  1. 激活函数正则化

GELU激活函数本身也有正则化的效果。即输入x越小,越容易被dropout。ReLU和Dropout都会返回一个神经元的输出,其中,ReLU会确定性的将输入乘上一个0或者1,Dropout则是随机乘上0。而GELU也是通过将输入乘上0或1来实现这个功能,但是输入是乘以0还是1,是在同时取决于输入自身分布的情况下随机选择的。换句话说,是0还是1取决于当前的输入有多大的概率大于其余的输入。公式如下: 截屏2021-08-12 下午8.25.46.png 即,神经元的输入x乘以其对应标准正态分布的分布函数。分布函数的参数为神经元的x本身,代表的意义是,X有多大概率是小于x的。标准正态分布的分布函数如下: 截屏2021-08-12 下午8.27.35.png 图像如下所示: v2-45d6312c444b7548558132cea30e3808_1440w.jpeg 因为其没有精确的表达式,原论文给出了两种近似表达式,一种基于sigmoid函数,一种基于tanh函数: 截屏2021-08-12 下午8.29.56.png 截屏2021-08-12 下午8.30.05.png 实现源码: 可参考bert中的源码(采用的是tanh的近似表达): 截屏2021-08-12 下午8.32.29.png

24. Bert中的权值共享

表现为Embedding层和FC(full connectio)层的权重共享。

Embedding层可以说是通过onehot去取到对应的embedding向量,参数维度为dvdk,FC层可以说是相反的,通过向量(定义为 x)去得到它可能是某个词的softmax概率,取概率最大(贪婪情况下)的作为预测值, 参数维度为dkdv。

因此,Embedding层和FC层权重共享,Embedding层中和向量 x 最接近的那一行对应的词,会获得更大的预测概率。实际上,Decoder中的Embedding层和FC层有点像互为逆过程。

通过这样的权重共享可以减少参数的数量,加快收敛。

源码分析: Embedding层参数维度是:(v,d),v为词表大小,d为隐向量维度。Linear 层的权重维度和emb一样,实际相乘时,会将权重矩阵转置再乘以当前向量x,再通过softmax获取词表中对应的概率分布。

7. BERT源码细节

1. bert分词策略

源码中有两种分词其,一种是basicTokenizer,一种是WordpieceTokenizer

basicTokenizer根据空格标点进行普通的分词,返回关于词的列表,对于中文是关于字的列表

WordpieceTokenizer是经典的BPE(Byte Pair Encoding字符对编码),为解决大量OOV未登录词问题,进行更细粒度的拆分,当然这一过程只针对英文,对中文无影响,准备好词典后利用正向最大匹配算法进行英文分词,这里要注意实现的源码,如果单词中有不存在字典中的字符,则将该词置为一个特殊的unk字符

8. Sentence BERT

1. Why

原始BERT模型产生的句子向量,其衡量语义相似度效果很不好,因为向量空间具有各向异性。

单塔BERT在计算句子相似度时,infer需要将两个句子分别放入模型中进行推理,对于10000个句子,就需要计算(10000*9999/2)次,需要大约65个小时。Bert模型的构造使得它既不适合语义相似度搜索,也不适合非监督任务,比如聚类。

Sentence-BERT就是为了解决这一问题。简单通俗地讲,就是借鉴孪生网络模型的框架,将不同的句子输入到两个bert模型中(但这两个bert模型是参数共享的,也可以理解为是同一个bert模型),获取到每个句子的句子表征向量;而最终获得的句子表征向量,可以用于语义相似度计算,也可以用于无监督的聚类任务。对于同样的10000个句子,我们想要找出最相似的句子对,只需要计算10000次,需要大约5秒就可计算完全。

2. What

截屏2021-08-05 下午4.12.00.png
三种获取句向量的方法:
CLS向量策略、平均池化策略(效果最好,照比CLS优0.8)、最大值池化策略。

分类损失函数
们分别获得两句话的句子向量u和v,并将u、 v和二者按位求差向量|u-v|进行拼接,再将拼接好的向量乘上一个可训练的权重review BERT:NLP的高光逆袭时刻 - 图53,其中n为句子向量维度,k为类别数。
review BERT:NLP的高光逆袭时刻 - 图54

3. How

数据构造
正样本构造:同类别下的query对,标签为1
负样本构造:不同类别下的query对,标签为0
损失函数:二分类交叉熵损失函数
具体实现分为数据构造和模型构造,两部分,损失函数不变,对于分类仍是交叉熵损失函数。

  1. def data_proceed(path,batch_size,tokenizer):
  2. data = pd.read_csv(path)
  3. data = data.sample(frac=1)
  4. inputs_1 = tokenizer(list(data['sentence1']), padding=True, truncation=True, return_tensors="tf",max_length=30)
  5. inputs_2 = tokenizer(list(data['sentence2']), padding=True, truncation=True, return_tensors="tf",max_length=30)
  6. inputs_1 = dict(inputs_1)
  7. inputs_1['input_ids_2'] = inputs_2['input_ids']
  8. inputs_1['token_type_ids_2'] = inputs_2['token_type_ids']
  9. inputs_1['attention_mask_2'] = inputs_2['attention_mask']
  10. label = list(data['label'])
  11. steps = len(label)//batch_size
  12. x = tf.data.Dataset.from_tensor_slices((dict(inputs_1),label))
  13. return x,steps
  1. class BertNerModel(tf.keras.Model):
  2. dense_layer = 512
  3. class_num = 2
  4. drop_out_rate = 0.5
  5. def __init__(self,pretrained_path,config,*inputs,**kwargs):
  6. super(BertNerModel,self).__init__()
  7. config.output_hidden_states = True
  8. self.bert = TFBertModel.from_pretrained(pretrained_path,config=config,from_pt=True)
  9. self.liner_layer = tf.keras.layers.Dense(self.dense_layer,activation='relu')
  10. self.softmax = tf.keras.layers.Dense(self.class_num,activation='softmax')
  11. self.drop_out = tf.keras.layers.Dropout(self.drop_out_rate)
  12. def call(self,input_1):
  13. hidden_states_1,_,_ = self.bert((input_1['input_ids'],input_1['token_type_ids'],input_1['attention_mask']))
  14. hidden_states_2,_,_ = self.bert((input_1['input_ids_2'],input_1['token_type_ids_2'],input_1['attention_mask_2']))
  15. hidden_states_1 = tf.math.reduce_mean(hidden_states_1,1)
  16. hidden_states_2 = tf.math.reduce_mean(hidden_states_2,1)
  17. concat_layer = tf.concat((hidden_states_1,hidden_states_2,tf.abs(tf.math.subtract(hidden_states_1, hidden_states_2))),1,)
  18. drop_out_l = self.drop_out(concat_layer)
  19. Dense_l = self.liner_layer(drop_out_l)
  20. outputs = self.softmax(Dense_l)
  21. print(outputs.shape)
  22. return outputs

4. 细节Trick

  1. Warm up

由于刚开始训练时,模型的权重(weights)是随机初始化的,此时若选择一个较大的学习率,可能带来模型的不稳定(振荡),选择Warmup预热学习率的方式,可以使得开始训练的几个epoches或者一些steps内学习率较小,在预热的小学习率下,模型可以慢慢趋于稳定,等模型相对稳定后再选择预先设置的学习率进行训练,使得模型收敛速度变得更快,模型效果更佳。

  1. focal loss

在实际应用中,负样本往往来自于负采样,大量的负采样会时训练时负样本数量远多余正样本数量导致训练样本不平衡,且软负采样的负样本往往非常弱,在模型推理时置信度一般较高,加入focal loss可以让模型专注于那些置信度低的比较难区分的样本,提高模型的训练效果。
截屏2022-05-23 上午10.10.30.png

在公开数据集上:该focal_loss可以很好的抑制模型过拟合且模型效果也有1个多点的提升。

参考文献

  1. 从Word Embedding到Bert模型—自然语言处理中的预训练技术发展史
  2. The Illustrated BERT, ELMo, and co. (How NLP Cracked Transfer Learning)
  3. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
  4. (GPT) Improving Language Understanding by Generative Pre-Training
  5. (ELMO) Deep contextualized word representations
  6. (ULM-FiT) Universal Language Model Fine-tuning for Text Classification.pdf
  7. (word2vec) Efficient Estimation of Word Representations in Vector Space