目录
1. 学习参考:
Transformer的PyTorch实现(超详细)
2. 说明:
为了更好的了解Transformer,也是先认真观看了别人的文章学习了Attention, 这几天看了几篇Transformer的文章,看完仍是一头雾水,不明白这些数据到底是以怎样的形式,比如怎样的维度计算的,也不知道编码器译码这些输出的到底是什么,又有什么用。
这次通过别人视频讲解,并且跟着他用PyTorch复现的代码学习,最终基本上明白了Transformer的工作原理。
此文章只为了在一定基础上更好的理解,至于有些为什么要出现的功能及其作用不一定会阐述,主要针对各部分输入输出数据的维度的讲述。我原先不理解的地方可能与你不同,如果你之前已经通过其他文章大概了解了Transformer,且有一些不能理解的地方,那么这篇文章可能会有你想要的。本文的主要目的还是记录自己的学习过程。
3. Transformer的基本架构
从基本架构中可以看出,Transformer由输入(编码器的输入和编码器的输出), 编码块(这里由6个编码器组成,这里统称为编码块,下面的译码器同理), 译码块以及输出构成。
4. 各部分详解:
1. 编码块输入:
输入由 n (这里的 n 用 batch_size 表示)个句子组成,每个句子又由sequence_length 个单词构成,实际上也可以不等长,我们设定最长句子的长度为sequence_length,其他不足长度的可以使用占位符来填充。那么输入可以表示为一个 batch_size * sequence_length 的矩阵。通过图中的 Input Embedding,我们将每个单词用一个维度为 d_model 的向量表示 (文章中d_model = 512)。这样,X_embedding 的维度为 batch_size * sequence_length * d_model。然后为其添加位置编码, 这里通过直接相加的方法,故位置编码的维度应当与 X_embedding 相当。
2. 编码块:
编码块由6个编码器组成,第一个编码器的输出作为第二个编码器的输入,第二个编码器的输出作为的第三个编码器的输入,以此类推,每一个编码器的输出将会传入下一个编码器。因此,保证每个编码输入输出的维度相当是必要的。
编码器的输入 enc_inputs ( 这里仅取X_embedding 中的一个句子, 相当于 sequence_length * d_model )
图示过程处理的对象是每个字向量,按照原文为一个 512 维的向量。图中X1,X2为不同的两个字,如果按这个图,X是一个 4 维的向量,通过矩阵得到的K等序列是一个 3 维的向量,输入的每一个字的维度由 4 变为了 3。而按照原文,这三种不同类型的矩阵由pytorch中的nn.linear 生成,且输入层维度和输出层维度在数值上是相等的 ( d_model ---> n_heads * d_k ) ,n_heads 表示多头机制下"头"的个数,d_k 表示多头机制下每个序列的维度,n_heads = 8, d_k = 64。故 X,Q,K,V的维度是一致的。
多头机制( Multi-heads ):简单理解为原本维度为 512 维的字向量分成 8 个( 原文中 ) 64 维的子向量,然后每个子向量使用不同的 W_Q, W_K, W_V ,得到.......最后再将结果拼接。这也是原文代码中将nn.linear的输出层维度表示为 n_heads * d_k 的原因。后面的讲解可能不再涉及,原理明白了就行。
对于 sequence_length 个字向量的句子,通过上面的变换可以到 sequence_length 组 K,Q,V 序列集,对于每个字向量,使用相同的一组 W_Q, W_K, W_V 。针对某一个字向量,我们使用它的 Q 序列与所有字向量的 K 序列相乘( Q * K.T ),1 * d_model 的矩阵乘以 d_model * 1 的矩阵得到一个值,对所有字向量计算便得到 sequence_length 个值,将得到的值经过 softmax,使得它们的和为 1,即每个字向量对于该字向量的权重或者说概率。原文在计算权重使用softmax时除以了一个根号下 d_k 。
得到的每个值乘以对应的 V 序列
最后把这 sequence_length 个结果相加得到 attention 值
总的实现过程如下:
实际上我们可以把每个字向量拼接成为一个矩阵 ( sequence_length * d_model ),即将完整的一句话作为输出,这样使用矩阵运算可以节省很多时间。
因为 W_Q,W_K,W_V 使用的 nn.linear 的输入输出层的维数是一样的,所以输入与经过 Multi-heads-Attention 得到的输出的维度是一样的,图中的 input # 的维度是 4,把 K, Q, V 也想象是维度是4即可。
Add & Norm 为 残缺连接和归一化, FFN 在原文提供的代码中由一次 nn.linear,一次 relu,最后再一次 nn.linear 组成,这里的 nn.linear 的输入输出层的维度也是一样的。这两步操作也都保证了数据维度的一致性。
因此,整个编码器,从 Multi-heads-Attention 到 Add & Norm,再到 FFN,最后经过 Add & Norm 输出,数据的维度保持不变,这样就可以在 6 个相同的编码器中传递。
enc_inputs,K,Q,V,enc_outputs 的维度在传递中保持不变,经过 6 个编码器得到的 enc_outputs 同时作为 K 和 V 传入到译码块中
3. 译码块:
相较于编码器,译码其中多了一个部分:Mask-Multi-heads-Attention,相较于 Multi-heads-Attention,这个 mask 是为了保证译码模块中当前时刻不能看到未来时刻的信息。假设对于一个句子 [ He is a rubbish],向译码模块输入 ‘He’ , 译码模块预测出 ‘is’ , 然后将 ‘is’ 作为输入,预测出 ‘a’ 。这是有时间的先后顺序的,是 RNN 等固有的特点,而 transformer 完全基于 Attention,它输入的一个完整的句子,并没有考虑时间先后的问题,所以我们自己实现这个特点,这便是 mask 存在的原因。
Mask-Multi-heads-Attention 的过程与Multi-heads-Attention 基本相同,前者的输出作为 Q,与来自编码器的 K 和 V 组成新的 K,Q,V 序列对,输入到 Multi-heads-Attention 中,然后就是相同的操作。
这里的 K, V 来自于编码块,V 来自与译码器,由于编码块输入和译码块输入的句子长度,即对于整个结构输入输出的句子长度可能不同,V 的维度应该是 target_length * d_model,target_length 表示最终输出句子的长度。而 K,V 的维度为 sequence_length * d_model,但实际上,K ,V 在 Multi-head-Attention 中的运算最终为一个权重值,这个权重值与 V 相乘,结果的维度与 V 相同。所以译码块的最终输出的维度是 target_length * d_model
4. 译码块输入:
跟编码块一样,该讲的也在译码块中讲了。
5. 输出:
首先经过一次线性变换,然后Softmax得到输出的概率分布,然后通过词典,输出概率最大的对应的单词作为我们的预测输出。
传入线性层的 dec_outputs 类似 enc_outputs,得到的数据维度为:target_length * d_model,这里的线性层用 nn.linear 表示的话, 输入层和输出层的维度分别是:d_model 和 tgt_vocab_size, tgt_vocab_size 是 输出所有可能符号的字典的长度,因此经过线性层得到的维度为:target_length * tgt_vocab_size,然后 softmax 。表示输入一个句子,最终生成的句子的长度为 target_length,这 target_length 个单词,每一个都有 tgt_vocab_size 个概率对应字典中每种可能。
5. 总结:
本篇文章通过原文提供的代码继续学习 Transformer,理解仍然不懂的地方,其他方面作者在原文中解释的很完美了,可以结合作者的B站视频讲解和代码分析学习。