Transformer出自于论文Attention is All You Need,Tensorflow实现的版本可以由Tensor2Tensor下载查看。Pytorch版本见guide annotating the paper with PyTorch implementation。本篇文章会试着简化概念并且一个一个介绍,以便于初学者理解。
让我们从将模型视为一个模块开始。在机器翻译应用中,模块输入一种语言的句子(sentence 序列),输出目标语言的句子。
在擎天柱善良的背后,我们发现一个连接着的编码组件和解码组件。(Transformer有变压器,变形金刚的意思,所以这是个冷笑话=-=)。
编码组件是由编码器堆叠起来组成的(论文里是由6个编码器堆成,选择6这个数字没什么特别的原因,你当然可以试试其他方案。)。解码组件也是用同样数量的解码器组成。
每个编码器结构相同,但是不共享权重。每一个编码器由两个子层组成。
编码器输入首先经过一个自注意力层(self-attention layer),自注意力层可以帮助编码器在编码具体单词时查看句子中的其他单词。我们会在后面详细说明自注意力层。
自注意力层的输出会进入到一个前馈神经网络。完全相同的前馈神经网络独立作用于每个位置。
解码器也有同样的两个层,但是在自注意层和前馈网络层之间有一个注意力层,注意力层可以帮助解码器注意输入句子的其他部分(和seq2seq中注意力机制类似)
目前为止我们已经看到了模型的主要组成部分,现在让我们看看各个向量(张量)是怎么经过训练模型的这些部分,由输入变为输出的。
和一般的NLP应用一样,我们将通过词嵌入技术(embedding algrithm)将单词变为向量。
每个词都被嵌入到512大小的向量中,这里用简单的盒子代替表示一下。
嵌入(embedding)只会在编码器最底层使用。概括的说就是所有编码器都接收一个由很多512大小的向量组成的列表,在最底层的编码器接收的是词嵌入层的输出(就是词转为向量,接收向量),但是在其他层的编码器直接接收前一层的输出。这个列表长度(大小)是一个可以设置的超参数,一般是我们训练集中最长句子的长度
当我们句子中的词经过嵌入层变成向量后,它们都会经过编码器的两个层。
这里我们可以看到Transformer的一个关键性质——每个位置的词经过编码器时都有自己的路径。路径在自注意力层中相互依赖,但是在前馈层中没有依赖关系,因此各个路径通过前馈层时可以并行执行。
接下来我们会用一个短句作为例子,看看编码器的每个层里它发生了什么。
像我们之前提到的,编码器接收一个输入的向量列表。它将这个列表经过自注意力层(self-attention layer)处理,然后输入到一个前馈神经网络层,最后输出给下一个编码器
别被我用“自注意力”这个词误导了,好像这个概念是我们每个人都熟悉的那个概念。我在阅读论文“Attention is All You Need”之前,我个人对它一无所知。让我们看看它的具体情况。
假设下面的句子是我们想要翻译的句子:
”The animal didn't cross the street because it was too tired”
这个句子里“it”是指什么?是指“street”还是指“animal”?这对人来说很简单,但是算法设计上很难(就是人很容易理解,但是机器很难)。
当模型处理“it”这个词的时候,自注意力机制可以让“it”和“animal”相关联。
当模型处理每个目标词时(输入句子(序列)的每个词(位置)),自注意力机制会查看其余词与目标词的关系,这有助于更好的编码目标词。
如果你熟悉RNN,想一想RNN是怎么保留隐藏状态来让当前处理的词包含之前的处理词的表达。“self-attention”就是Transformer用来将其他相关单词的“理解”融入当前处理单词的方法。
当我们对"it"进行编码的时候,自注意力机制将“it”与其他词的相关性传入“it”的编码中,并会给“The animal”更大的权重。
最好仔细研究一下Tensor2Tensor,运行并根据这张图理解其中的运行机制。
首先让我们用向量来简单的看看自注意力是怎么计算的,然后再让我们看看我们实际用的矩阵是怎么计算的。
第一步,自注意力机制会将编码器输入向量(这个例子中,输入向量是每个词经过embedding后的向量)变为三个向量。所以对于每个词,我们会创建一个Query向量,一个Key向量,一个Value向量。这三个向量是由词嵌入的向量乘以我们训练得出的三个矩阵得到的。
注意,这些新向量维度小于词嵌入的向量。当嵌入层和编码器输入和输出的向量维度是512时,这些向量的维度是64。但是他们并非一定要比原向量更小,这是一种架构选择,它可以使多头注意力(multiheaded attention)(大多数的多头注意力)计算保持不变。
X1乘以WQ得到q1,X2乘以WQ得到q2,(WQ是我们训练出来得到的)以此类推,最后得到所有的Query,Key,Value.。
那究竟什么是“query”, “key”, and “value” 向量呢?
他们就是抽象出来方便理解和计算注意力的。看下文你就知道他们是什么意思了。
第二步,计算注意力的第二步是打分。假设我们计算例子中的第一个单词“Think”。我们需要根据这个单词给输入句子中的每一个词打分。当我们编码当前位置的单词的时候,分数决定了我们给其他位置的单词多少关注(权重)。
分数是通过点乘(dot) 当前位置单词的query 向量 和 需要打分位置单词的key 向量 得到的。所以如果我们在位置#1使用注意力给其他部分打分,第一个分数是(qi dot k1),第二个分数是(q1 dot k2)。
第三步和第四步,第三步和第四步是将维度除以8(key 向量维度的平方根,论文里key维度是64。目的是得到更稳定的梯度。当然可以是其他值,不过这个是默认的),然后将结果通过softmax处理。softmax标准化(softmax normalizes)可以将值映射(压缩、标准化)到0-1之间。
第五步,第五步是将每个 value 向量乘以 softmax处理后的得分(为相加做准备)。这一步的目的是保留重要的词(想要关注的词)的完整性,去除不相关的词。(如给不相干的词乘以一个极小的数-0.001)
第六步将权重值向量相加。结果就是自注意力层(self-attention layer)在这个位置的输出(本例子里是第一个词)
这就是自注意力的整个计算过程。计算后的结果我们会传递给前向反馈神经网络。然而在实际应用中,计算都是以矩阵形式而不是以向量,因为矩阵运算速度快。接下来让我们看一下单词级别矩阵计算的样子。
第一步是计算Query,Key,和Value矩阵。他们是通过嵌入层(embedding)输入的矩阵X和我们训练出来的权重矩阵(WQ,WK,WV)相乘得到的
X矩阵中的每一行对应于输入句子中的一个单词。我们再次看到嵌入向量的大小差异(512,或图中的4个方框)和q / k / v向量(64,或图中的3个方框)
最后,处理矩阵时,我们可以将2-6步变为一步计算自注意力层的输出。
论文通过一个叫“多头注意力”(multi-headed attention)的机制进一步提高了自注意力层的性能。多头注意力机制从两个方面提高注意力层的性能:
1.它提高了模型关注不同位置的能力。的确,上面的例子中,Z1包含了一些其他位置的编码信息,但是实际上它却是由实际的词本身决定。我们翻译一个句子像 “The animal didn’t cross the street because it was too tired”,我们想知道“it”指代的是什么词时,多头注意力很有用。(翻译这段的时候好多 it 一下子把我看晕了…)(就是说it用一个注意力只能关注到“animal”,但是其实“tired”也是和“it”有关的)
2.它给予注意力层多个“表示子空间”。正如我们接下来看到的那样,多头注意力可以让我们拥有多组Q/K/V矩阵(Transformer使用8个注意力头,所以最终我们的编码/解码器有8组)。每组都是随机初始化的。经过训练后,每组将嵌入层的输出(或者底层编码器/解码器的输出)投射到不同的表示子空间中。
如果我们做和之前提到的8头自注意力计算的话,只需要8次和8个不同的权重矩阵相乘,我们会得到8个不同的Z矩阵。(Query,Key,Value首先经过一个线性变换,然后输入到放缩点积attention,注意这里要做h次,其实也就是所谓的多头,每一次算一个头。而且每次Q,K,V进行线性变换的参数W是不一样的)
这就有个问题。前馈神经网络只接受一个矩阵,不接受多个矩阵。所以我们需要把这8个矩阵组合成1个矩阵。
怎么做呢?我们把8个矩阵连接起来,然后乘以一个额外的权重矩阵WO。
以上就是多头自注意力机制的所有内容了。但是我只是展示了实际计算中的一部分矩阵以便理解。让我们把多头注意力计算放到一张图上看看。
现在我们已经基本了解了多头注意力了,让我们看看例句中,当我们编码“it”的时候,不同头的注意力的注意机制是怎么样的。
当我们编码单词“it”的时候,一个注意力头更关注“the animal”,另一个更关注"tired"(假设是这样),那么模型用“animal”和“tired”一起表示“it”。
如果我们把所有的注意力头都放到一张图片里,看起来就会很复杂。
目前为止我们忽略了一件事,就是模型没有表示句子中单词的位置信息。
为了标记位置,transformer对每一个输入的嵌入层(embedding)都加了一个向量。这些向量遵循模型学习的特定模式(意思就是向量的维度和模型要的维度一样。)。这有助于决定每个单词的位置,或者句子中不同单词的距离。这里的逻辑是,将这些值添加到嵌入层,嵌入层向量会被投影到Q/K/V向量中,Q/K/V在注意力期间进行点积计算时,就有了位置信息。
这玩意在实际模型中到底长啥样?
下面这个图每一行都对应位置信息编码成的向量。也就是说,第一行是我们要往嵌入层添加的输入句子中第一个词的位置向量。每一行由从-1到1之间的512个值构成,我给这些值做了颜色编码,看起来更直观一些。
一个真实的512大小嵌入层和20个词位置编码的例子。你可以看到它在中间很明显的分为两部分。这是因为左半部分的值是由一个函数生成(sin),右半部分是由另一个函数生成(cos)。然后组合 成为一个位置编码向量。
位置编码的具体公式在论文里有描述(3.5节),你可以从源码的get_timing_signal_1d().
函数看到公式的代码具体实现。这并不是唯一的一种位置编码方式,只是这种方式可以处理未知长度的序列。(比如要求模型翻译的句子比训练集中所有句子都长)
我们之前提到过一个细节,每个编码模块的子层(自注意力层和前馈网络层)中间都有残差(residual)连接,然后紧跟着一个层标准化步骤。
如果我们将自注意力机制中向量和层标准化的运作方式如下图:
解码模块里自注意力子层的样子也差不多。如果整体看Transformer的编、解码模块(以两层为例),它长这样:
我们既然已经知道了编码模块的组成,那么解码模块也就知道了。所以解码端就不详细说了,还是让我们看看它们在一起工作的样子。
编码模块从处理输入的句子开始,由最顶端的编码器输出是由注意力向量K和V组成的集合。解码器会在每个“注意力编-解码层(encoder-decoder attention)”使用,它们能让解码器关注输入句子中恰当的地方。
编码阶段结束后就是解码阶段。解码阶段每一步会输出目标句子的一个元素。(比如翻译出来的句子中的一个词)
下面这些步骤会循环,直到解码器收到一个特殊的标记(EOS),这就代表解码结束。
解码模块每一步输出都会作为下一步的输入,解码模块内部动作和编码器一致,而且也会将每个词的位置嵌入并添加到输入向量中。
解码模块的自注意力层和编码模块的有点不一样。
在解码模块中(有些人将解码模块翻译为解码端),自注意力层只关注输入句子中之前位置的单词。这个操作是在自注意力计算中的softmax层之前,通过屏蔽未来的位置(将它们设置成 -inf)来实现的。
“编码-解码注意力”层的计算方式和多头自注意力是一样的。只不过它是从它下面的层创建Q矩阵,同时从编码模块的输出中获得K和V。
解码模块输出的是一个向量或者小数,我们怎么把他们变成一个词呢?这就是最后的线性层和softmax层要做的工作。
线性层是个简单的全连接神经网络,它将解码模块输出的一堆向量投影成为一个炒鸡炒鸡大的向量,这个向量成为logits vector(对数向量)。
假设我们模型从训练集中获得10,000个不同的英文单词(就是字典中的单词),线性层会生成10,000个单元宽度的logits向量,每个单元代表一个词的分数。这就是为什么模型后面紧跟着线性层的原因。
softmax层会将这些分数转为概率(全为正,加起来和是1)。最高概率的单元会被选出来,这个单元代表的词就是这一步的输出。
现在我们已经了解了Transformer的整个前向训练过程。
训练期间,未经训练的模型会进行相同的前向训练,但是因为我们是有监督学习,所以是有标签可以对比训练输出的结果来判断正确与否。
为了方便理解,我们假设输出词典只有六个词(“a”, “am”, “i”, “thanks”, “student”, and “” ( ‘end of sentence’缩写)).
一旦我们定义了单词表(字典),我们就可以用同样宽度的向量表示每个词,这也叫作one-hot编码。比如,我们可以用下面的向量表示“am”
下一节我们会讨论损失函数(loss function)---- 在训练期间为了得到高准确率的模型,我们要优化的目标。
假设我们在训练模型,而且是我们训练阶段的第一波。我们用个简单的例子训练-----把“merci”翻译成“thanks”。
什么意思呢?就是我们想让模型输出是“thanks”的概率分布,但是模型还没有开始训练,所以它还没能力输出。
模型的初始化参数(权重)是随机的,也就是会在每个单元(词)上产生一个随机的概率值。我们把初始的概率值和真正的概率比较,通过反向传播调整初始值,努力让模型输出的概率分布和真正的概率分布相同。
那么两个概率分布怎么比较呢?简单的方法就是一个减去另一个,更多的方法请看交叉熵损失-cross-entropy和相对熵/KL散度(Kullback-Leibler divergence)。
但是注意,上面的只是个超级简化的例子。更实际点,我们用一个句子代替一个词来举例。比如,输入“je suis étudiant”,然后期望输出“i am a student”。也就是说,我们想要我们的模型成功的输出如下要求的概率分布:
‘
”符号,这个符号也是单词表(词典)里的。在足够大的数据集上训练模型足够次数后,我们希望模型会生成如下概率分布。
这只是我们训练后期望的输出。当然这并不意味着短语是训练集的一部分(见cross-validation).。请注意每个单元都有一个很小的概率,即使它完全不可能是这个时间步的输出—这就是softmax的效果,这种方式有助于模型训练。
现在,因为模型一次产生一个输出,我们可以假设模型选择了概率分布中概率最高的词并且把其他词和概率丢弃了。这种选择方式称为贪婪解码(greedy decoding)。另一种选择方式是保留,比如,保留概率最高的两个单词(假设是“i”和“me”),然后下一步,模型运行两次:一次是假设第一个位置输出的是单词“i”,第二次假设第一个位置输出的单词是“me”,然后模型考虑位置#1和#2,保留错误更少的作为第一个位置(#1)的输出。重复这个步骤在位置#2和#3…等等。(束搜索可以看束搜索和贪婪搜索)。这种方式称为“束搜索”(beam search),我们的例子中,束搜索的大小(beam_size)是2(因为我们只比较了#1和#2两个位置),束搜索的数量(top_beams)也是2(“i”和"me",我们只比较了两个词)。这两个参数都是可以随你需要而设置的。
第一步:计算query,key,value的值,
q_embedding * w_q[embedding_dim,atten_dim] = query
k_embedding * w_k[embedding_dim,atten_dim] = key
v_embedding * w_v[embedding_dim,atten_dim] = value
第二步:score
q1 dot k1
q1 dot k2
q1 dot k3
-----
q1 dot ki
q1 dot [k1,k2,k3,.....ki....kl] l为输入句子的长度,ki_dim = q1_dim
q1_score = [q1_score1,q1_score2,......q1_scorei....q1_scorel]
如果 q 的长度也为l
可以表示成两个矩阵的相乘,[q1,
q2,
- * [k1,k2,k3,...ki,......kl] = [ dim * dim ]的矩阵 == score矩阵
ql]
【 dim*l 】* 【 l*dim 】= 【 dim*dim 】
第三步:
【 dim*dim 】/ dim的平方根
第四步:
对第三步结果,softmax(*)
第五步:
softmax() * value === [ dim,dim] * [ dim * l ] = [ dim * l ] = [ v1,v2,...vi....vl]
第六步:
权值向量相加 [ 1 * l] = [weight_1,weight_2,....,weight_i,...weight_l ]
1. 引入循环机制
与vanilla Transformer的基本思路一样,Transformer-XL仍然是使用分段的方式进行建模,但其与vanilla Transformer的本质不同是在于引入了段与段之间的循环机制,使得当前段在建模的时候能够利用之前段的信息来实现长期依赖性。如下图所示:
在训练阶段,处理后面的段时,每个隐藏层都会接收两个输入:
该段的前面隐藏层的输出,与vanilla Transformer相同(上图的灰色线)。
前面段的隐藏层的输出(上图的绿色线),可以使模型创建长期依赖关系。
这两个输入会被拼接,然后用于计算当前段的Key和Value矩阵。对于某个段的某一层的具体计算公式如下:
绝对位置编码:A_i,j = qi_T * kj =[ w_q*(E_xi+U_xi)] * [w_k*(E_xj+U_xj)] 展开后的结果为
Relative positional emb 代码解析
rel_shift(*)
token emb
和反向的absolute pos emb
attention score
矩阵后,在token emb
维pad,产生1位错位;token emb
个数个分数组成行,对角全是pad