Transformer逐层分解2
什么是Transformer?
Transformer架构擅长处理文本数据,这些数据本身是有顺序的。他们将一个文本序列作为输入,并产生另一个文本序列作为输出。例如,讲一个输入的英语句子翻译成西班牙语。
Transformer的核心部分,包含一个编码器层和解码器层的堆栈。
为了避免混淆,我们把单个层称为编码器或解码器,并使用编码器堆栈或解码器堆栈分别表示一组编码器与一组解码器。
在编码器堆栈和解码器堆栈之前,都有对应的嵌入层。而在解码器堆栈后,有一个输出层来生成最终的输出。
编码器堆栈中的每个编码器的结构相同。解码器堆栈也是如此。其各自结构如下:
- 编码器:一般有两个子层:包含自注意力层self-attention,用于计算序列中不同词之间的关系;同时包含一个前馈层feed-forward。
- 解码器:一般有三个子层:包含自注意力层self-attention,前馈层feed-forward,编码器-解码器注意力层Decoder-Encoder self attention。
- 每个编码器和解码器都有独属于本层的一组权重。
注意,编码器与解码器的自注意力层self-attention、前馈层feed-forward,以及解码器中的编码器-解码器注意力层Decoder-Encoder self attention 均有残差连接以及正则化层。
基于Transofrmer的变体有许多。一些Transformer构架甚至没有Decoder结构,而仅仅依赖Encoder。
Transformer训练过程
Transformer的训练和推理有一些细微差别。
首先来看训练。每一条训练数据都包括两部分内容:
- 输入序列,或称为“源序列”(例如对于一个翻译问题,“you are welcome” 是一个输入序列)
- 输出序列,或称为“目标序列”(上述的翻译问题“De nada”即为"you are welcome"的西班牙语翻译,为输出序列)
而Transformer训练的目标就是,通过对训练数据中源序列与目标序列之间规律的学习,在测试或实际的任务中,给定原序列,生成目标序列。
如上图所示,Transformer在训练过程中,模型对数据的处理过程如下,大体可以分为6个步骤:
- 在送入第一个编码器之前,输入序列(src_seq)首先被转换为嵌入(同时带有位置编码),产生词嵌入表示(src_position_embed),之后送入第一个编码器。
- 由各编码器组成的编码器堆栈按照顺序对第一步中的输出进行处理,产生输入序列的编码表示(enc_outputs)。
- 在右侧的解码器堆栈中,目标序列首先加入一个句首标记,被转换成嵌入(带位置编码),产生词嵌入表示(tgt_position_embed),之后送入第一个解码器。
- 右各解码器组成的解码器堆栈,将第三步的词嵌入表示(tgt_position_embed),与编码器堆栈的编码表示(enc_outputs)一起处理,产生目标序列的解码表示(dec_outputs)。
- 输出层将其转换为词概率和最终的输出序列(out_seq)。
- 损失函数将这个输出序列(out_seq)与训练数据中的目标序列(tgt_seq)进行比较。这个损失被用来产生梯度,在反向传播过程中训练模型。
词嵌入层与位置编码
Transformer的输入需要关注每个词的两个信息:该词的含义和它在序列中的位置。
- 第一个信息,可通过嵌入层对词的含义进行编码。
- 第二个信息,可通过位置编码层表示该词的位置。
Transformer通过添加两个层来完成两种不同的信息编码。
1.嵌入层(Embedding)
Transformer的编码器和解码器各有一个嵌入层(Embedding)。
在编码器中,输入序列被送入编码器的嵌入层,被称为输入嵌入(input Embedding)。
在解码器中,目标序列右移一个位置,然后在第一个位置插入一个Start token后被送入解码器的嵌入层。
注意,在推理过程中,我们没有目标序列,而是循环地将输出序列送入解码器的嵌入层,此过程为输出嵌入(Output Embedding)。
每个文本序列在输入嵌入层之前,都已被映射成词汇表中单词ID的数字序列。
嵌入层再将每个数字序列映射成一个嵌入向量,这是该词含义一个丰富的表示。
2.位置编码层(Position Encoding)
RNN在循环过程中,每个词按顺序输入,因此隐含地知道每个词的位置。
然而,Transformer一个序列中的所有词都是并行输入的。这是其相对于RNN架构的主要优势;但同时也意味着位置信息会丢失,必须单独添加回来。
解码器堆栈和编码器堆栈各有一个位置编码层。
位置编码的计算是独立于输入序列的,是固定值,只取决于序列的最大长度。
- 第一项是一个常熟代码,表示第一个位置。
- 第二项是一个表示第二位置的常量代码。
pos是该词在序列中的位置,d_model是编码向量的长度(与嵌入向量相同),i是这个向量的索引值。公式表示的是矩阵第pos行、第2i列和(2i+1)列上的元素。
换句话说:位置编码交织了一系列正弦曲线和一系列余弦曲线,对于每个位置pos,当i为偶数时,使用正弦函数计算;当i为奇数时,使用余弦函数计算。
三、矩阵维度(Matrix Dimensions)
深度学习模型一次梳理一批训练样本。
嵌入层和位置编码层对一批序列样本的矩阵进行操作。
嵌入层接受一个(samples, sequence_length)形状为二位单词ID矩阵,将每个单词ID编成一个单词向量,其大小为embedding_size,从而得到一个(samples, sequence_length, embedding_size)形状的三维输出矩阵。
位置编码使用的编码尺寸等于嵌入尺寸,它产生一个类似矩阵,能添加到嵌入矩阵中。
由于嵌入层和位置编码层产生的(samples, sequence_length, embedding_size)形状在模型中被保留下来,随数据在编码器和解码器堆栈中流动,知道它被最终的输出层改变形状。
[实际上变成了(samples, sequence_length, vocab_size)]
以上对Transformer中的矩阵维度有了一个形象的认识。为了简化可视化,从这里开始,暂时放弃第一个维度(samples维度),并使用单个样本的二维表示。
四、Encoder
编码器和解码器堆栈分别由几个(通常是6个)编码器和解码器组成,按顺序连接。
- 堆栈中的第一个编码器从嵌入和位置编码器中接收其输入。堆栈中的其他编码器从前一个编码器接收它们的输入。
- 当编码器接受上一个编码器的输入,并将其传入当前编码器的自注意力层。当前自注意力层的输出被传入前馈层,然后将其输出至下一个编码器。
1、自注意力层和前馈网络都会接入一个残差连接,之后再送如正则化层。注意,上一个解码器的输入进入当前解码器时,也有一个残差连接。
2、具体来说,该前馈网络由一个线性层,一个激活函数和另外一个线性层组成。
并且这个前馈网络同样不会改变输入的形状。
编码器堆栈中的最后一个编码器的输出,会送入解码器堆栈中的每一个解码器中。
五、Decoder
解码器的结构与编码器的结构非常类似,但有一些区别。
- 与编码器一样,解码器堆栈中的第一个解码器从嵌入层(词嵌入+位置编码)中接受输入;堆栈中的其他解码器从上一个解码器接受输入。
- 在一个解码器内部,输入首先进入自注意力层,这一层的运行方式与编码器自注意力层,这一层的运行方式与编码器自注意力层的区别在于:
- 训练过程中解码器的自注意力层接收整个输出序列。
- 为了避免在生成每个输出时看到未来的数据(即避免信息泄露),使用所谓的“掩码”技术,确保在生成第i个词时,模型只能看到第1个到第i个词。
- 推理过程中,每个时间步的输入,是知道当前时间步所产生的整个输出序列。
- 解码器与编码器的另一个不同在于,解码器有第二个注意层,即编码器-解码器注意力层(Encoder-Decoder-attention)层。其工作方式与自注意力层类似,只是其输入来源有两处:位于其前的自注意力层及编码器堆栈的输出。
- 编码器-解码器注意力层的输出被传入前馈层,然后将其输送至下一个解码器。
- 解码器中的每一个子层,包括自注意力层、编码器-解码器注意力层、前馈层,均由一个残差连接,斌进行层规范化。
六、注意力层-Attention
Transformer中,注意力备用在三个地方:
- Encoder中的Self-attention:输入序列对自身的注意力计算;
- Decoder中的Self-attention:目标序列对自身的注意力计算;
- Decoder中的Encoder-Decoder-attention:目标序列对输入序列的注意力计算。
注意力层通过三个参数进行计算,这三个参数称为:
- 查询(Query)
- 键 (key)
- 值 (Value)
- 在Encoder中的Self-attention,编码器的输入与相应的参数矩阵相乘,得到Query、Key和Value三个参数。
- 在Decoder中的self-attention,解码器的输入通过相同的方式得到Query、Key和Value。
- 在解码器的Encoder-Decoder-attention中,解码器的输入通过相同的方式得到Query、Key和Value。
- 位于Encoder-Decoder-attention的Self-attention和Layer Norm模块的输出被传递给Query参数。
七、多头注意力(Multi-head Attention)
在Transformer中,注意力模块会并行多次重复计算,每个注意力计算单元被称为一个注意力头(Attention Head)。
多个注意力头并行运算,即所谓的多头注意力:Multi-head Attention。
它通过融合几个相同的注意力计算,使注意力计算具有更强大的分辨能力。
1、注意力超参数(AttentionHyperparameters)
Transformer内部流动的数据维度由三个超参数决定。
- Embedding Size:嵌入向量的大小。这个维度在整个Transformer模型中都是向前传递的,因此有时也被称为“model size - 模型大小”等其他它名称。
- Query Size(与Key size和Value size相等):查询向量的长度,与键向量和值向量的长度相等,也是分别用来产生Query、Key和Value矩阵的三个线性层的权重大小。
- Number of Attention heads:注意力头个数。
此外,还有Batch_size,批量大小,是一次输入模型中样本的数量。
2、输入层(Input Layer)
经过词嵌入和位置编码后,进入编码器之前,输入的数据维度为:(batch_size,seq_length,embedding_size),之后数据进入编码器堆栈中的第一个Encoder,与Query、Key、Value矩阵相乘。
为了方便理解,一下的图示与介绍中将去掉batch_size维度,聚焦于剩下的维度:
3、线性层(Linear Layers)
Query,Key,Value实际上是三个独立的线性层。每个线性层都有自己独立的权重。
输入数据与三个线性层分别相乘,产生Q、K、V。
注意力模块将其查询Query、键Key和值Value的参数矩阵进行N次拆分,并将每次拆分独立通过一个单独的注意力头。
能够更细致地捕捉并表达每个词汇之间的多种联系和微妙差异。这些结果通过如下所示的注意力公式组合在一起,产生注意力分数(Attention Score)。
- 需要注意的重要一点是,Q、K、V的值是对序列中每个词的编码表示。
- 注意力计算将每个词与序列中的其他词联系起来,这样注意力分数就为序列中的每个词编码了一个分数。
4、通过注意力头切分数据(Splitting data across Attention heads)
需要注意:
- “切分”只是逻辑上的分割。对于参数矩阵Query,Key,Value而言,并没有物理切分成对应于每个注意力头的独立矩阵,
- 逻辑上,每个注意力头对应于Query、Key、Value的独立一部分。各注意力头没有单独的线性层,而是所有的注意力头共用线性层,只是不同的注意力头在独属于各自的逻辑部分上进行操作。
这种切分可以按照两个步骤理解:
第一步,线性层权重矩阵的切分
这种逻辑分割,是通过将输入数据以及线性层权重,均匀划分到各注意头中来完成的。我们可以通过选择下面的Query size大小来实现:
在我们的例子中,这就是为什么Query_Size = 6 / 2 = 3。尽管层权重(和输入数据)均为单一矩阵,我们可以认为它是“将每个头的独立层权重堆叠在一起”成为一个矩阵。
也因此,在代码中定义W_Q、W_K、W_V矩阵时可这样定义:
self.W_Q = nn.Linear(embedding_size,Query_size * n_heads,bias = False)
self.W_K = nn.Linear(embedding_size,Query_size * n_heads,bias = False)
self.W_V = nn.Linear(embedding_size,Value_size * n_heads,bias = False)
基于此,所有Heads的计算可通过对一个矩阵操作来实现,而不需要N个单独操作,这使得计算更加有效,同时保持模型的简单:所需线性层更少,同时或得了多头注意力的效果。
input的维度是:(batch_size,seq_length,embedding_size)
线性层的维度是:(batch_size,embedding_size,Query_size * n_heads)
由于embedding_size = Query_size * n_heads,所以线性层的维度并未发生变化。
输入经过线性层映射后,得到Q、K、V矩阵形状是:(batch_zie,seq_length,Query_size * n_heads)
第二步,重塑Q、K和V矩阵形状
经由线性层输入得到Q、K和V矩阵要经过Reshape操作,以产生一个Head维度。现在每个“切片”对应于代表没个头的一个矩阵。
通过交换n_heads和seq_length这两个维度,改变Q、K和V矩阵的形状。
图示中虽然为表达出Batch维度,但对应于每一个注意力头的‘Q’的维度是:(batch_size,n_heads,seq_length,Query_size)
在上图中,我们可以看到从线性层(Wq)出来后,分割Q矩阵的完整过程。
最后一个阶段只是为了形象化--实际上Q矩阵仍然是一个单一矩阵,但可以把它看做是每个注意力头的逻辑上独立的Q矩阵。
八、注意力掩码(Attention Masks)
在计算Attention Score的同时,Attention模块应用一个掩码操作。掩码操作有两个目的:
- 在Encoder Self-attention和Encoder-Decoder-attention中掩码的作用是:
在输入序列padding对应的位置,将输出的注意力分数(Attention Score)归零,以确保padding对Self-attention的计算没有贡献。
- padding的作用:由于输入序列可能有不同的长度,因此会像大多数NLP方法一样,使用padding作为填充标记,以得到固定长度的向量,从而可以将一个样本的序列作为矩阵被输入到Transform中。
- 当计算注意力分数(Attention Score)时,在Softmax计算之前的分子上进行了掩码。被屏蔽的元素(白色方块)设置为符无穷大,这样Softmax就会把这些值变为零。
对padding掩码操作的图示:
Encoder-Decoder-attention的掩码操作图示:
- 在Decoder中的Self-attention中掩蔽的作用是:
防止解码器在当前时间步预测时“偷看”目标句余下几个时间步的部分:
- 解码器处理源序列source sequence中的单词,并利用它们来预测目标序列中的单词。训练期间,这个过程是通过Teacher Forcing进行的,完整的目标序列别作为解码器的输入。因此,在预测某个位置的词时,解码器可以使用该词之前的目标词以及该词之后的目标词。这使得解码器可以通过使用未来“时间步”的目标词来“作弊”。
- 如下图:当预测“Word3”时,解码器应只参考目标词的前三个输入词,不含第四个单词“Ketan”。因此,Decoder中的Self-attention掩码操作掩盖了序列中位于当前时间步之后的目标词。
九、产生输出(Generate Output)
解码器堆栈中的最后一个解码器将其输出传给输出组件,输出组件将其转换为最终目标句子。
- 线性层将解码器向量投射到单词分数中,目标词汇中每个对特的单词在句子中的每个位置都有一个分数值。
例如,如果我们的最终的输出句子有7个词,而目标西班牙语词汇有10000个独特的词,我们为这7个词中的每一个生成10000个分数值。分数值表示词汇中的每个词在句子的那个位置出现的可能性。 - Softmax层将分数变成概率(加起来为1.0)。在每个位置找到概率最高的单词索引(贪婪搜索),将该索引映射到词表中的相应单词,就构成了Transformer的输出序列。
十、训练与损失函数(Training and Loss Function)
训练中使用交叉熵作为损失函数,比较生成的输出概率分布和目标序列。概率分布给出了每个词在该位置出现的概率。
假设我们的目标词汇只包含四个词。我们的目标是产生一个与我们预期的目标序列“De nada END”相符的概率分布。
这意味着第一个词位的概率分布中,“De”的概率应该是1,而词汇中所有其他词的概率都是0、
同样地,在第二和第三词位中,“nada”和“End”的概率应该都是1,而词汇表中其他词的概率都是0。像往常一样,对损失倍计算梯度,通过反向传播来训练模型。