BERT通俗笔记:从Word2Vec/Transformer逐步理解到BERT

Posted v_JULY_v

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了BERT通俗笔记:从Word2Vec/Transformer逐步理解到BERT相关的知识,希望对你有一定的参考价值。

前言

我在写上一篇博客《22下半年》时,有读者在文章下面评论道:“july大神,请问BERT的通俗理解还做吗?”,我当时给他发了张俊林老师的BERT文章,所以没太在意。

直到今天早上,刷到CSDN上一篇讲BERT的文章,号称一文读懂,我读下来之后,假定我是初学者,读不懂。

关于BERT的笔记,其实一两年前就想写了,迟迟没动笔的原因是国内外已经有很多不错的资料,比如国外作者Jay Alammar的一篇图解Transformer:The Illustrated Transformer,再比如国内张俊林老师的这篇《说说NLP中的预训练技术发展史:从Word Embedding到Bert模型》。

本文基本上可以认为是对这几篇文章在内的学习笔记(配图也基本上都来自文末的参考文献),但本篇笔记准备面对没有任何NLP模型背景的文科生/初学者来写,不出现任何没有解释的概念。作为初学者的你,读本文之前,很多文章你看不懂看不下去,读本文之后,网上1/3值得看的文章你都看得懂、看得下去,本文的目的之一就达到了

毕竟据我观察,网上关于Transformer/BERT的文章无外乎是以下这几种情况:

  • 对经典外文做各种解读或翻译的,比如围绕上面那篇《The Illustrated Transformer》,当然,有的翻的不错 受益匪浅,有的还不如有道翻译来得通顺/靠谱(关键阅读量还不低,一想到大学期间不小心看了很多烂翻译的IT外版书,就恶心,劝君多说人话,没那水平别翻译),当然,本文已汲众各长、去其槽粕
  • 对BERT原始论文做各种解读的,有的号称一文读懂BERT,但读下来感觉像是在看机器翻译出来的文章,文章可读性取决于Google翻译的水平
  • 用自己语言介绍transformer和BERT的,但有些文章有个显而易见的缺点是,文章自身水平抬得过高,作者经常性的自以为、想当然,导致各种背景知识、各种概念有意无意的忽略

我写博客从2010.10.11至今已经超过11年了,这11年还是写过很多通俗易懂的笔记,比如svm xgboost cnn rnn lstm这些,刚好总结一下把文章写通俗的方法,也算是对本文行文的指导思想:

  1. 行为逻辑/条理一定要清晰,这点是最基本的,没有逻辑就没有可读性,通俗是增加可读性
  2. 凡是背景知识得交待,不要想当然 不要自以为读者什么都懂,你自己变聪明了 不见得是聪明,你自己不聪明 让读者变聪明 才是真的聪明
  3. 爬楼梯逐级而上 有100级则爬100级,不出现断层,一出现断层,笔记就不完美了,所以本文准备从头开始写:NNLM → Word2Vec → Seq2Seq → Seq2Seq with Attention → Transformer → Elmo → GPT → BERT(从不懂到弄懂所有这些模型,我用了整整5个半天即2.5天,而有了本文,你从不懂到懂这些模型,可能只需要5个半小时即2.5h,这是本文的目的之二
  4. 公式能展开尽可能展开,不惜字如金,十句解释好过一句自以为是
  5. 多用图、多举例,有时一图胜千言,有时一个好的例子 可以避免绕来绕去

​当然 上面都是术,真正战略层面的则一句话:用初学者思维行文每一字每一句

最近我一直在看哲学相关的书,哲学里有一个很流行的观点是:反思一切司空见惯的事务、现象、常识,类比到大家写文章的话,则是你真的理解你行文的每一字每一句么还是想当然,你真的认为初学者能看懂你写的每一字每一句么还是自以为。

本文篇幅较长,没办法,一者 为了大一统BERT相关的所有概念/模型,篇幅不可能短,二者 不想因为所谓的篇幅有限而抹杀本文的通俗易懂性。另,行文过程中,得到了我司七月在线部分讲师的指点,有何问题 欢迎不吝指正,thanks。

第一部分 理解基本概念:从NNLM到Word2Vec

我博客内之前写过一篇word2vec笔记,如今再看 写的并不通俗易懂,巧的是,写本文开头那篇图解transformer文章的作者,他也有写一篇图解word2vec,本部分中的核心阐述和大部分配图均来自此文。

为了让每一个初学者可以从头看到尾,不至于因行文过程中的任何一句话而卡壳看不下去,我在原英文的基础上,加了大量自己的学习心得、说明解释(得益于看过很多文学和哲学书,且学过算法,所以文笔尚可、逻辑尚可)。

1.1 从向量表示到词嵌入

不知你可曾听说过华为对于求职人员的性格测试? 这个测试会问你一系列的问题,然后在很多维度给你打分,内向/外向就是其中之一,然后用0到100的范围来表示你是多么内向/外向(其中0是最内向的,100是最外向的)

假设一个叫Jay的人,其内向/外向得分为38/100,则可以用下图表示这个得分:

为了更好的表达数据,我们把范围收缩到-1到1:

考虑到人性复杂,对于一个人的描述只有一条信息显然是不够的,为此,我们添加另一测试的得分作为一个新的第二维度,而这两个维度均可以表现为图上的一个点(或称为从原点到该点的向量)

然后可以说这个向量部分地代表了Jay的人格。当你想要将另外两个人与Jay进行比较时,这种表示法就有用了。假设Jay是一家公司的CEO,某天被公共汽车给撞住院了,住院期间需要一个与Jay性格相似的人代行Jay的CEO之责。那在下图中,这两个人中哪一个更像Jay呢,更适合做代理CEO呢?

计算两个向量之间相似度得分的常用方法是余弦相似度,可曾还记得夹角余弦的计算公式?

通过该计算公式可得

从而可知,person 1在性格上与Jay更相似。其实,从坐标系里也可以看出,person1的向量指向与Jay的向量指向更相近,即他俩具有更高的余弦相似度。

更进一步,两个维度还不足以捕获有关不同人群的足够信息。可曾听说国内有七重人格,而国外心理学也研究出了五个主要人格特征(以及大量的子特征)。

为从简起见,就不用七个维度了,而用五个维度再度比较Jay与person1 2的相似性:

当使用五个维度时,我们没法在二维平面绘制向量小箭头了(毕竟你我都不曾见过五维坐标系)。而实际生活中,我们经常要在更高维度的空间中做研究(有的人把研究一词表达成思考,实际上,大部分人没法在高维度空间思考,但科学研究人员经常干这事,故表达成研究更准确),好在余弦相似度仍然有效,它适用于任意维度:

这些得分比上次的得分看起来更准确(对,目前为止,咱只能说看起来更准确,最近学哲学给我最大的感悟是,凡事客观,不要轻易绝对化),毕竟它们是根据被比较事物的更高维度算出的。

小结一下,有两点

1.我们可以将人和事物表示为代数向量

2.我们可以很容易地计算出相似向量之间的相互关系。


行文至此,可能有同学要问了,为何要把词向量化表示呢,其背后的深意在哪?

众所周知,咱们居住在各个国家的人们通过各自的语言进行交流,但机器无法直接理解人类的语言,所以需要先把人类的语言“计算机化”,那如何变成计算机可以理解的语言呢?

对于这个问题,我们考虑一个很简单的问题,比如对于计算机,它是如何判断一个词的词性,是动词还是名词的呢?

假定我们有一系列样本(x,y),对于计算机技术机器学习而言,这里的 x 是词语,y 是它们的词性,我们要构建的映射:

  1. 首先,这个数学模型 f(比如神经网络、SVM)只接受数值型输入;
  2. 而 NLP 里的词语,是人类语言的抽象总结,是符号形式的(比如中文、英文、拉丁文等等);
  3. 如此一来,咱们便需要把NLP里的词语转换成数值形式,或者嵌入到一个数学空间里;
  4. 进一步,可以把文本分散嵌入到另一个离散空间,称作分布式表示,又称为词嵌入(word embedding)或词向量。
  5. 在各种词向量中,有一个简单的词向量是one-hot encoder。所谓one-hot编码,其思想跟特征工程里处理类别变量的one-hot 一样,本质上是用一个只含一个 1、其他都是 0 的向量来唯一表示词语。当然,不是所有的编码都是01编码,且one-hot编码无法反应词与词之间的语义相似度。

这就是所谓的词嵌入了。

再举一个例子,这是一个单词“king”的词嵌入(在维基百科上训练的GloVe向量):

[ 0.50451 , 0.68607 , -0.59517 , -0.022801, 0.60046 , -0.13498 , -0.08813 , 0.47377 , -0.61798 , -0.31012 , -0.076666, 1.493 , -0.034189, -0.98173 , 0.68229 , 0.81722 , -0.51874 , -0.31503 , -0.55809 , 0.66421 , 0.1961 , -0.13495 , -0.11476 , -0.30344 , 0.41177 , -2.223 , -1.0756 , -1.0783 , -0.34354 , 0.33505 , 1.9927 , -0.04234 , -0.64319 , 0.71125 , 0.49159 , 0.16754 , 0.34344 , -0.25663 , -0.8523 , 0.1661 , 0.40102 , 1.1685 , -1.0137 , -0.21585 , -0.15155 , 0.78321 , -0.91241 , -1.6106 , -0.64426 , -0.51042 ]

这是一个包含50个数字的列表。通过观察数值我们看不出什么,但是让我们稍微给它可视化,以便比较其它词向量。故我们把所有这些数字放在一行:

让我们根据它们的值对单元格进行颜色编码(如果它们接近2则为红色,接近0则为白色,接近-2则为蓝色):

我们将忽略数字并仅查看颜色以指示单元格的值。现在让我们将“king”与其它单词进行比较(注意,世间有相似之人,也有相似之词):

你会发现“Man”这个词和“Woman”相比,比与“King”相比更相似,而这些向量图示很好的展现了这些单词的含义与关联。

这是另一个示例列表:

通过垂直扫描列来查找具有相似颜色的列,相信你可以看到以下这几点

  1. “woman”和“girl”在很多地方是相似的,“man”和“boy”也是一样
  2. 当然,“boy”和“girl”也有彼此相似的地方,但这些地方却与“woman”或“man”不同,为何呢,毕竟boy/girl特指青春年少,而woman/man更多指成人
  3. 此外,“king”和“queen”彼此之间相似,毕竟都是所谓的王室成员

1.2 从神经语言模型NNLMWord2Vec

现在我们已经看过训练好的词嵌入,接下来让我们更多地了解训练过程。 但在我们开始使用word2vec之前,我们需要看一下词嵌入的父概念:神经语言模型(NNLM)。

我们每天都会用手机或者电脑,比如我们经常会用到智能手机输入法中的下一单词预测功能,或者你在电脑上用Google搜索也会遇到类似的搜索智能提示(详见此文)。

比如当你输入thou shalt时,系统会预测/提示你想输入的下一个单词是不是not?

在上面这个手机截屏中,该模型接收到两个绿色单词(thou shalt)后,推荐了一组单词(当然,“not” 是其中最有可能被选用的一个):

我们可以把这个模型想象为这个黑盒:

当然,该模型不会只推荐一个单词。实际上,它对所有它知道的单词(模型的词库,可能有几千到几百万个单词)均按可能性打分,最终输入法程序选出其中分数最高的推荐给用户,比如not

自然语言模型的输出就是模型所知单词的概率评分,比如40%或者0.4,最终在完成训练后按下图中所示的三个步骤完成预测(请参考Bengio 2003):

第一步就是Embedding,模型在经过训练之后会生成一个映射单词表所有单词的矩阵,也称词嵌入矩阵,从而在进行预测的时候,我们的算法可以在这个映射矩阵(词嵌入矩阵)中查询输入的单词(即Look up embeddings),第二步则是计算出预测值:

接下来,我们重点看下模型训练,探讨如何构建这个映射矩阵(词嵌入矩阵)。

1.2.2 语言模型训练:N-Gram技术

我们通过找常出现在每个单词附近的词,就能获得它们的映射关系。机制如下:

  1. 先是获取大量文本数据(例如所有维基百科内容)
  2. 然后我们建立一个可以沿文本滑动的窗(例如一个窗里包含三个单词)
  3. 利用这样的滑动窗就能为训练模型生成大量样本数据

当这个窗口沿着文本滑动时,我们就能(真实地)生成一套用于模型训练的数据集

不用多久,我们就能得到一个较大的数据集,从数据集中我们能看到在不同的单词组后面会出现的单词:

在实际应用中,模型往往在我们滑动窗口时就被训练的。而怎么训练模型呢?除了使用神经网络建模之外,大家还常用一项名为N-Gram的技术进行模型训练。

举个例子,请你根据下面这句话前面的信息进行填空:

在空白前面,我提供的背景是五个单词(如果事先提及到‘bus’),可以肯定,大多数人都会把bus填入空白中。但是如果我再给你一条信息——比如空白后的一个单词,那答案会有变吗?

这下空白处改填的内容完全变了。这时’red’这个词最有可能适合这个位置的词之一。

从上面这个例子可以看到,一个单词的前后词语其实都带着有价值的信息,而且要尽可能考虑两个方向的单词(目标单词的左侧单词与右侧单词)。

1.2.3 Word2Vec的两种架构:从CBOW到Skipgram模型

更进一步,为了更好的预测,其实不仅要考虑目标单词的前两个单词,还要考虑其后两个单词。

如果这么做,我们实际上构建并训练的模型就如下所示:

上述的这种『以上下文词汇预测当前词』架构被称为连续词袋(CBOW),简单引用下此文的内容做个简单概括(详见原文或参考文献16):

 CBOW包括以下三层:

  • 输入层:包含context(w)中2c个词的词向量,其中,v表示单词的向量化表示函数,相当于此函数把一个个单词转化成了对应的向量化表示(类似one-hot编码似的),2c表示上下文取的总词数,m表示向量的维度;
  • 投影层:将输入层的2c个向量做累加求和;
  • 输出层:按理我们要通过确定的上下文决定一个我们想要的中心词wt,但怎么决定想要的中心词具体是中的哪个呢?通过计算各个可能中心词的概率大小,取概率最大的词便是我们想要的中心词,相当于是针对一个N维数组进行多分类,但计算复杂度太大,所以输出层改造成了一棵Huffman树,以语料中出现过的词当叶子结点,然后各个词出现的频率大小做权重

还有另一种架构,刚好反过来,根据当前词推测当前单词可能的前后单词,这种架构就是所谓的Skipgram架构

顺带提一句,关于什么是负采样,可以参见网上相关资料,比如参考文献1。

1.3 Word2vec训练流程:不断缩小error(target - sigmoid_scores)

在训练过程开始之前,我们预先处理我们正在训练模型的文本。

具体做法是先创建两个矩阵:词嵌入Embedding矩阵、上下文Context矩阵,这两个矩阵在我们的词汇表中嵌入了每个单词,且两个矩阵都有这两个维度

  1. 第一个维度,词典大小即vocab_size,比如可能10000,代表一万个词
  2. 第二个维度,每个词其嵌入的长度即embedding_size,比如300是一个常见值(当然,我们在前文也看过50的例子,比如上文1.1节中最后关于单词“king”的词嵌入长度)

训练的过程还是这个标准套路/方法,比如第一步,先用随机值初始化这些矩阵。在每个训练步骤中,我们采取一个相邻的例子及其相关的非相邻例子。

具体而言,针对这个例子:“Thou shalt not make a machine in the likeness of a human mind”,我们来看看我们的第一组(对于not 的前后各两个邻居单词分别是:Thou shalt 、make a):

现在有四个单词:输入单词not,和上下文单词:thou(实际邻居词),aaron和taco(负面例子)。

我们继续查找它们的嵌入

  • 对于输入词not,我们查看Embedding矩阵
  • 对于上下文单词,我们查看Context矩阵

第二步,计算输入嵌入与每个上下文嵌入的点积。

两个向量a = [a1, a2,…, an]和b = [b1, b2,…, bn]的点积定义为:

而这个点积的结果意味着输入上下文各个嵌入的各自相似性程度,结果越大代表越相似

为了将这些分数转化为看起来像概率的东西——比如正值且处于0到1之间,可以通过sigmoid这一逻辑函数转换下。

可以看到taco得分最高,aaron最低,无论是sigmoid操作之前还是之后。

第三步,既然未经训练的模型已做出预测,而且我们拥有真实目标标签来作对比,接下来便可以计算模型预测中的误差了,即让目标标签值减去sigmoid分数,得到所谓的损失函数。

error = target - sigmoid_scores

第四步,我们可以利用这个错误分数来调整not、thou、aaron和taco的嵌入,使下一次做出这一计算时,结果会更接近目标分数。

训练步骤到此结束。我们从中得到了这一步所使用词语更好一些的嵌入(not,thou,aaron和taco)。现在进行下一步(下一个相邻样本及其相关的非相邻样本),并再次执行相同的过程。

当我们循环遍历整个数据集多次时,嵌入会继续得到改进。然后我们就可以停止训练过程,丢弃Context矩阵,并使用Embeddings矩阵作为下一项任务的已被训练好的嵌入。
 

第二部分  从Seq2Seq到Seq2Seq with Attention

2.1 从Seq2Seq序列到Encoder-Decoder模型

2.1.1 什么是Seq2Seq:输入一个序列 输出一个序列

Seq2Seq(Sequence-to-sequence)正如字面意思:输入一个序列,输出另一个序列,当然,其中输入序列和输出序列的长度是可变的。

比如我们翻译英国经验派哲学家弗兰西斯・培根的一句名言“知识就是力量”,如下图:

简言之,只要满足「输入序列、输出序列」的目的,都可以统称为 Seq2Seq序列。

2.1.2 什么是Encoder-Decoder模型:RNN/LSTM与GRU

针对Seq2Seq序列问题,比如翻译一句话,可以通过Encoder-Decoder模型来解决。

从上文我们已经接触到编码的概念,有编码则自然有解码,而这种编码、解码的框架可以称之为Encoder-Decoder,中间一个向量C传递信息,且C的长度是固定的。

本节配图大半来源于参考文献2。

没太明白?可以这么理解,在上图中,我们可以根据不同的任务可以选择不同的编码器和解码器,具体化可以是一个RNN。

 在参考文献3里《如何从RNN起步,一步一步通俗理解LSTM》,我们已经详细了解了RNN和LSTM,如果忘了,一定要复习下(这是继续理解下文的重中之重)

为了建模序列问题,RNN引入了隐状态h(hidden state)的概念,隐状态h可以对序列形的数据提取特征,接着再转换为输出

在学习RNN之前,首先要了解一下最基本的单层网络,它的结构如下图所示:

 输入是x,经过变换和激活函数f,得到输出y。相信大家对这个已经非常熟悉了。

在实际应用中,我们还会遇到很多序列形的数据:

 如:

  1. 自然语言处理问题。x1可以看做是第一个单词,x2可以看做是第二个单词,依次类推
  2. 语音处理。此时,x1、x2、x3……是每帧的声音信号
  3. 时间序列问题。例如每天的股票价格等等

而其中,序列形的数据就不太好用原始的神经网络处理了。

为了建模序列问题,RNN引入了隐状态h(hidden state)的概念,隐状态h可以对序列形的数据提取特征,接着再转换为输出

先从计算开始看:

RNN可以被看做是上述同一神经网络的多次复制,每个神经网络模块会把消息传递给下一个:

当然,更多通常是其变种LSTM或者GRU

这样你就明白了吧,而只要是符合类似的框架,都可以统称为 Encoder-Decoder 模型。

2.2 从Seq2Seq到Seq2Seq with Attention

2.2.1 Attention应运而生:解决信息过长时信息丢失的问题

上文提到:Encoder(编码器)和 Decoder(解码器)之间只有一个「向量C」来传递信息,且C的长度固定。

比如翻译一段语句,翻译的句子短还好,句子一长呢?当输入句子比较长时,所有语义完全转换为一个中间语义向量C来表示,单词原始的信息已经消失,可想而知会丢失很多细节信息。

所以Encoder-Decoder是有缺陷的,其缺陷在于:当输入信息太长时,会丢失掉一些信息。

而为了解决「信息过长,信息丢失」的问题,Attention 机制就应运而生了。

Attention 模型的特点是 Eecoder 不再将整个输入序列编码为固定长度的「中间向量C」,而是编码成一个向量的序列。引入了Attention的Encoder-Decoder 模型如下图:

  • 从输出的角度讲
    每个输出的词Y会受到每个输入的整体影响,不是只受某一个词的影响,毕竟整个输入语句是整体而连贯的,但同时每个输入词对每个输出的影响又是不一样的,即每个输出Y受输入的影响权重不一样,而这个权重便是由Attention计算,也就是所谓的注意力分配系数,计算每一项输入对输出权重的影响大小
  • 从编码的角度讲
    在根据给到的信息进行编码时(或称特征提取),不同信息的重要程度是不一样的(可用权重表示),即有些信息是无关紧要的,比如一些语气助词之类的,所以这个时候在编码时,就可以有的放矢,根据不同的重要程度针对性汲取相关信息

2.2.2 通过翻译Tom chase Jerry揭示Attention的算法流程

再举一个机器翻译的例子(本猫追老鼠例子的配图和核心阐述均来源于参考文献4),即用Google翻译这句话:Tom chase Jerry

  1. 在翻译“杰瑞”的时候,带有注意力机制的模型会体现出英文单词对于翻译当前中文单词不同的影响程度,比如给出类似这样一个概率分布值:(Tom,0.3)(Chase,0.2) (Jerry,0.5),每个英文单词的概率代表了翻译当前单词“杰瑞”时,注意力分配模型分配给不同英文单词的注意力大小(类似我司七月在线开董事会,虽然每个人都有发言权,但对不同议题进行决策时,很明显对具体议题更擅长的人拥有更大的发言权,而这个发言权就像权重一样,不同的人对最终决策结果的产生有着不同大小的影响)

  2. 目标句子中的每个单词都应该学会其对应的源语句子中单词的注意力分配概率信息。这意味着在生成每个单词的时候,原先都是相同的中间语义表示C会被替换成根据当前生成单词而不断变化的(注:这里就是Attention模型的关键,即由固定的中间语义表示C换成了根据当前输出单词来调整成加入注意力模型的变化的)。

  3.  生成目标句子单词的过程成了下面的形式:

    而每个可能对应着不同的源语句子单词的注意力分配概率分布,比如对于上面的英汉翻译来说,其对应的信息可能如下: 

    其中,函数代表Encoder对输入英文单词的某种变换函数,比如如果Encoder是用的RNN模型的话,这个函数的结果往往是某个时刻输入后隐层节点的状态值;g代表Encoder根据单词的中间表示合成整个句子中间语义表示的变换函数,一般的做法中,g函数就是对构成元素加权求和,即下列公式: 

    其中,代表输入句子Source的长度,代表在Target输出第i个单词时Source输入句子中第j个单词的注意力分配系数,而则是Source输入句子中第j个单词的语义编码。

  4. 假设下标i就是上面例子所说的“ 汤姆” ,那么就是3,h1=f(“Tom”)、h2=f(“Chase”)、h3=f(“Jerry”)分别是输入句子每个单词的语义编码,对应的注意力模型权值则分别是0.6,0.2,0.2,所以g函数本质上就是个加权求和函数。如果形象表示的话,翻译中文单词“汤姆”的时候,数学公式对应的中间语义表示的形成过程类似下图。

这里有一个问题:生成目标句子某个单词,比如“汤姆”的时候,如何知道Attention模型所需要的输入句子单词注意力分配概率分布值呢?就是说“汤姆”对应的输入句子Source中各个单词的概率分布:(Tom,0.6) (Chase,0.2) (Jerry,0.2) 是如何得到的呢?为做说明,特引用参考文献4对应的内容,如下

为了便于说明,我们假设对非Attention模型的Encoder-Decoder框架进行细化,Encoder采用RNN模型,Decoder也采用RNN模型,这是比较常见的一种模型配置

那么用下图便可以较为便捷地说明注意力分配概率分布值的通用计算过程。

对于采用RNN的Decoder来说

  1. 在时刻i,如果要生成单词,我们是可以知道Target在生成之前的时刻i-1时,隐层节点在i-1时刻的输出值的(这是RNN结构的特性,如果忘了RNN结构特性请回顾参考文献3)
  2. 而我们的目的是要计算生成时输入句子中的单词“Tom”、“Chase”、“Jerry”对来说的注意力分配概率分布,那么可以用Target输出句子i-1时刻的隐层节点状态去一一和输入句子Source中每个单词对应的RNN隐层节点状态进行对比,即通过函数来获得目标单词和每个输入单词对应的对齐可能性,这个F函数在不同论文里可能会采取不同的方法,然后函数F的输出经过Softmax进行归一化就得到了符合概率分布取值区间的注意力分配概率分布数值

对上面这段,还是有必要再好好解释一下

  • 如我司杜助教所言:“ 这里举的例子是由 Tom chase Jerrry 得到 汤姆追逐杰瑞,现在我们假设要预测杰瑞(已经预测出来汤姆追逐),那么这个时候,i 就表示的是杰瑞这个时刻,i-1时刻的hidden就包含了汤姆追逐的信息,就是想计算i-1时刻的hidden和Tom、chase、Jerry的各自不同的Attention数值,进而更好地预测杰瑞这个词 ”
  • 至于注意力分配概率的分布数值,有多种不同的计算方法,具体参见此文:Attention? Attention!,或者参看参考文献14(33min至37min,以及50min都有讲)

2.2.3 Attention的算法流程总结:通过计算相似性得出权重最后加权求和

再比如,图书馆(source)里有很多书(value),为了方便查找,我们给书做了编号(key)。当我们想要了解漫威(query)的时候,我们就可以看看那些动漫、电影、甚至二战(美国队长)相关的书籍。

为了提高效率,并不是所有的书都会仔细看,针对漫威来说,动漫,电影相关的会看的仔细一些(权重高),但是二战的就只需要简单扫一下即可(权重低)。

当我们全部看完后就对漫威有一个全面的了解了。

可以看到,将Source中的构成元素想象成是由一系列的<Key,Value>数据对构成,此时给定Target中的某个元素Query,通过计算Query和各个Key的相似性或者相关性,得到每个Key对应Value的权重系数,然后对Value进行加权求和,即得到了最终的Attention数值。

所以本质上Attention机制是对Source中元素的Value值进行加权求和,而Query和Key用来计算对应Value的权重系数。即可以将其本质思想改写为如下公式:

整个过程具体如下图所示:

归纳整理一下,则为

  1. 第一步:代表漫威漫画的query 和 代表某本书的key 进行相似度计算(常见的方法包括:求两者的向量点积、求两者的向量Cosine相似性等),得到权值
  2. 第二步:将权值进行归一化(将原始计算分值整理成所有元素权重之和为1的概率分布,或者说通过SoftMax的内在机制更加突出重要元素的权重),得到直接可用的权重
  3. 第三步:将权重和 value 进行加权求和

值得一提的是,Attention 并不一定要在 Encoder-Decoder 框架下使用的,他是可以脱离 Encoder-Decoder 框架的。

了解了Attention的本质思想,理解所谓的Self-Attention就容易了,具体下文会详细阐述,这里先简单提一嘴:

在一般任务的Encoder-Decoder框架中,输入Source和输出Target内容是不一样的,比如对于英-中机器翻译来说,Source是英文句子,Target是对应的翻译出的中文句子,Attention机制发生在Target的元素Query和Source中的所有元素之间。

而Self Attention顾名思义,指的不是Target和Source之间的Attention机制,而是Source内部元素之间或者Target内部元素之间发生的Attention机制,也可以理解为Target=Source这种特殊情况下的注意力计算机制。其具体计算过程是一样的,只是计算对象发生了变化而已。


第三部分 通俗理解Transformer:理解它就理解了BERT的一半

自从2017年此文《Attention is All You Need》提出来Transformer后,便开启了大规模预训练的新时代,也在历史的长河中一举催生出了BERT这样的大一统模型。

有兴趣的,可以回顾下。比如2018年3月份华盛顿大学提出ELMO、2018年6月份OpenAI提出GPT、2018年10月份Google提出BERT、2019年6月份CMU+google brain提出XLNet等等。

不过,据目前我所看到的文章里面,介绍Transformer比较好懂的还是开头所提的这篇《The Illustrated Transformer》本部分中的核心阐述和大部分配图均来自此文。

接下来,我在此文的基础上加以大量解释、说明,以让之成为全网最通俗易懂的Transformer导论。因为这些解释、说明是你在其他文章中看不到的,而正因为有了这些解释、说明,才足以真正让每一个初学者都能快速理解到底什么是Transformer。

3.1 Transformer之编码:自注意力/位置编码/求和与归一化

3.1.1 从机器翻译模型开始谈起

还是考虑上文中已经出现过的机器翻译的模型。当我们从外部现象来看的话,这个机器翻译技术就像是一个黑箱操作:输入一种语言,系统输出另一种语言:

当我们拆开这个黑箱后,我们可以看到它是由编码组件、解码组件和它们之间的连接组成。

其中

  • 编码组件部分由一堆编码器/encoder构成(具体个数可以调)
  • 解码组件部分也是由相同数量(与编码器对应)的解码器decoder组成

进一步拆开这个编码器会发现,所有的编码器在结构上都是相同的,但是并不共享参数,且每个编码器都可以分解成两个子层:

从而,当我们把编码器和解码器联合起来看待的话,则整个流程就是(如下图从左至右所示):

  1. 首先,从编码器输入的句子会先经过一个自注意力层(即self-attention,下文会具体阐述),它会帮助编码器在对每个单词编码时关注输入句子的其他单词
  2. 接下来,自注意力层的输出会传递到前馈(feed-forward)神经网络中,每个位置的单词对应的前馈神经网络的结构都完全一样(注意:仅结构相同,但各自的参数不同)
  3. 最后,流入解码器中,解码器中除了也有自注意力层、前馈层外,这两个层之间还有一个编码-解码注意力层,用来关注输入句子的相关部分(和seq2seq模型的注意力作用相似)

3.1.2 将张量引入图景

我们已经了解了模型的主要部分,接下来我们看一下各种向量或张量(译注:张量概念是矢量概念的推广,可以简单理解矢量是一阶张量、矩阵是二阶张量)是怎样在模型的不同部分中,将输入转化为输出的。

像大部分NLP应用一样,我们首先将每个输入单词通过词嵌入算法转换为词向量。

具体而言,流程如下

  1. 每个单词都被嵌入为512维的向量(512是transformer原作者设定的一个维度,类似编码器/解码器的数量一样,也是我们可以设置的超参数。顺带提句,训练集中最长句子的长度论文中也设置的512。但为方便后续一系列的图示,这里用4个格子代表512维,即虽然你只看到4维,但你要明白实际背后代表着512维)
  2. 最底下的那个编码器接收的是嵌入向量,之后的编码器接收的是前一个编码器的输出

此时,我们可以看出Transformer的一个核心特性

  1. 输入序列中每个位置的单词都各自单独的路径流入编码器。不知你发现没有,各个单词是同时流入编码器中的,不是排队进入..
  2. 在自注意力self-attention层中,这些路径两两之间是相互依赖的,而前馈层(feed-forward)则没有这些依赖性,所以这些路径在流经前馈层(feed-forward)时可以并行计算

且慢,第2点好像不够通俗?如何更好的理解上面第2点呢,好办,上图:


3.1.3 什么是自注意力机制:从宏观视角看自注意力机制

下面,咱们再通过一个例子了解自注意力机制的工作原理。

例如,下列句子是我们想要翻译的句子:
“The animal didn't cross the street because it was too tired”

上面这个“it”是指的什么呢?它指的是street 还是这个 animal 呢?对人来说很简单的问题(必然是animal,因为animal才可能cross,才可能tired),但是对算法而言并不简单,算法不一定知道it指的是animal还是street。

那self-attention机制咋做呢?一般的文章会这么解释:

当模型处理单词“it”时,self-attention允许将“it”和“animal”联系起来。当模型处理每个位置的词时,self-attention允许模型看到句子的其他位置的单词/信息作为辅助线索,以更好地编码当前单词。

还记得RNN吧?回想一下RNN对隐藏状态的处理:将之前的隐藏状态与当前位置的输入结合起来。在Transformer中,自注意力机制则将对其他单词的“理解”融入到当前处理的单词中。

可能上面那段话稍微有点绕,为通俗起见,说的直白点就是,你如果要更好的理解句中某个特定单词的含义,你要把它放到整个语境之中去理解,比如通过对上下文的把握。那上下文哪些词对理解该特定词更重要呢?这个重要程度便用所谓的权重表示,所以才算出每个词对『该词』的权重,权重越大的单词代表对理解『该词』越重要,然后把该词编码为包括该词在内所有词的加权和。

比如下图中,it的上下文中,很明显the anima和tired等单词对it 的编码更重要,所以自注意力机制在编码it 时把更多的注意力/权重放在了the animal和tired等单词上。

3.1.4 通过向量计算自注意力:先三个向量后计算得分且softmax最后加权求和

3.1.4之1/4 计算自注意力第一步:生成查询向量、键向量和值向量向量

接下来的关键是如何计算自注意力,计算自注意力有两种方式:一种通过向量,一种通过矩阵。

先看向量的方式。通过向量方式计算自注意力的第一步,就是从每个编码器的输入向量(即每个单词的词向量)生成三个向量:查询向量query-vec、键向量key-vec、值向量value-vec。

至于它们的生成方法是把输入的向量分别乘以三个不同的权重矩阵,得到Q、K、V,而这些权重矩阵是在模型训练阶段中训练出来的。

对于上述这两段可能会有同学对一些细节有疑问,而有的文章可能都会觉得不言而喻,因为本文面向初学者,所以我把相关细节一并解释下:

  1. 首先,查询向量、键向量、值向量这三个向量的维度在论文中设置的是64,在维度上比词嵌入向量更低,因为词嵌入和编码器的输入/输出向量的维度是512,但值得注意的是也不是必须比编码器输入输出的维数小,这样做主要是为了让后续多头注意力的计算更稳定
    (在下文你会看到,transformer通过多头注意力机制multi headed attention,对每个512维的输入向量都设置了8个头,不同的头关注每个输入向量不同的部分,而你发现没有:512/8 = 64,且再多说一句,也可以设置为2个头,不一定非得设置为8个头)
  2. 其次,对于权重矩阵//如何训练出来的,还是标准老套路:先随机初始化,然后在损失函数中表示出来,最后通过反向传播不断优化学习得出。至于什么是反向传播,请参见参考文献17

有了权重矩阵后,对于单词分别而言(假定X1是thinking,X2是machines):

(c)2006-2024 SYSTEM All Rights Reserved IT常识