Transformer笔记
前言背景
Transformer 依赖于 Self Attention 的知识。Attention 是一种在深度学习中广泛使用的方法,Attention的思想提升了机器翻译的效果。
2017 年,Google 提出了 Transformer 模型,用 Self Attention
的结构,取代了以往 NLP 任务中的 RNN 网络结构,在 WMT 2014 Englishto-German
和 WMT 2014 English-to-French
两个机器翻译任务上都取得了当时 SOTA 的效果。这个模型的其中一个优点,就是使得模型训练过程能够并行计算。在 RNN 中,每一个 time step 的计算都依赖于上一个 time step 的输出,这就使得所有的 time step 必须串行化,无法并行计算,如下图所示。
而在 Transformer 中,所有 time step 的数据,都是经过 Self Attention 计算,使得整个运算过程可以并行化计算。
Transformer 使用了 Seq2Seq
任务中常用的结构——包括两个部分:Encoder 和 Decoder。一般的结构图,都是像下面这样。
Transformer内部剖析
首先,我们将整个模型视为黑盒。在机器翻译任务中,接收一种语言的句子作为输入,然后将其翻译成其他语言输出。
中间部分的 Transformer 可以拆分为 2 部分:左边是编码部分(encoding component),右边是解码部分(decoding component)。
其中编码部分是多层的编码器(Encoder)组成(Transformer 的论文中使用了 6 层编码器,这里的层数 6 并不是固定的,你也可以根据实验效果来修改层数)。同理,解码部分也是由多层的解码器(Decoder)组成(论文里也使用了 6 层的解码器)。
注意:每一个编码器在结构上都是一样的,但它们的权重参数是不同的。
每一个编码器里面,可以分为 2 层:
- Self-Attention Layer
- Feed Forward Neural Network(前馈神经网络,缩写为 FFNN)
执行流程
输入编码器的文本数据,首先会经过一个 Self Attention
层,这个层处理一个词的时候,不仅会使用这个词本身的信息,也会使用句子中其他词的信息(你可以类比为:当我们翻译一个词的时候,不仅会只关注当前的词,也会关注这个词的上下文的其他词的信息)。
接下来,Self Attention 层的输出会经过前馈神经网络。
同理,解码器也具有这两层,但是这两层中间还插入了一个 Encoder-Decoder Attention
层,这个层能帮助解码器聚焦于输入句子的相关部分(类似于 seq2seq 模型 中的 Attention)。
Transformer细节
Transformer输入
和通常的 NLP 任务一样,我们首先会使用词嵌入算法(embedding algorithm),将每个词转换为一个词向量。实际中向量一般是 256 或者 512 维。为了简化起见,这里将每个词的转换为一个 4 维的词向量。
那么整个输入的句子是一个向量列表,其中有 3 个词向量。在实际中,每个句子的长度不一样,我们会取一个适当的值,作为向量列表的长度。
- 如果一个句子达不到这个长度,那么就填充全为 0 的词向量;
- 如果句子超出这个长度,则做截断。
编码器(Encoder)接收的输入都是一个向量列表,输出也是大小同样的向量列表,然后接着输入下一个编码器。第一个编码器的输入是词向量,而后面的编码器的输入是上一个编码器的输出。下面,我们来看这个向量列表在编码器里面是如何流动的。
在 Self Attention 层中,这些路径之间是有依赖关系的;而在 Feed Forward (前馈神经网络)层中,这些路径之间是没有依赖关系的。另外,这些词向量在经过 Feed Forward 层中可以并行计算。
Encoder
一个编码器接收的输入是一个向量列表,它会把向量列表输入到 Self Attention 层,然后经过 feed-forward neural network (前馈神经网络)层,最后得到输出,传入下一个编码器。
每个位置的词都经过 Self Attention 层,得到的每个输出向量z都单独经过前馈神经网络层,每个向量经过的前馈神经网络都是一样的。
Self-Attention 整体理解
假设我们想要翻译的句子是:
The animal didn't cross the street because it was too tired
这个句子中的 it
是一个指代词,那么 it
指的是什么呢?它是指animal
还是street
?这个问题对人来说,是很简单的,但是对算法来说并不是那么容易。
当模型在处理(翻译)it
的时候,Self Attention
机制能够让模型把it
和animal
关联起来。
同理,当模型处理句子中的每个词时,Self Attention
机制使得模型不仅能够关注这个位置的词,而且能够关注句子中其他位置的词,作为辅助线索,进而可以更好地编码当前位置的词。
如果你熟悉 RNN,回忆一下:RNN 在处理一个词时,会考虑前面传过来的hidden state
,而hidden state
就包含了前面的词的信息。而 Transformer 使用Self Attention
机制,会把其他单词的理解融入处理当前的单词。
当我们在第五层编码器中(编码部分中的最后一层编码器)编码“it”时,有一部分注意力集中在“The animal”上,并且把这两个词的信息融合到了"it"这个单词中。
Self-Attention 细节
计算Query 向量,Key 向量,Value 向量
计算 Self Attention 的第 1步是:对输入编码器的每个词向量Xn,都创建 3 个向量,分别是:Query 向量,Key 向量,Value 向量。这 3 个向量是词向量Xn分别和 3 个矩阵WQ、WK、WV相乘得到的,而这个矩阵是我们要学习的参数。
注意:这 3 个新得到的向量一般比原来的词向量的长度更小。假设这 3 个向量的长度是 dkey=64,而原始的词向量或者最终输出的向量的长度是 512(这 3 个向量的长度,和最终输出的向量长度,是有倍数关系的)。关于 Multi-head Attention,后面会给出实际代码。这里为了简化,假设只有一个 head 的 Self-Attention。
上图中,有两个词向量:Thinking 的词向量 X1 和 Machines 的词向量 X2。以 X1 为例, X1 乘以 WQ得到 q1,q1 就是 X1对应的 Query 向量。同理, X1 乘以 WK 得到 k1, k1是X1 对应的 Key 向量;X1乘以 WV得到 v1,v1 是 X1对应的 Value 向量。
计算 Attention Score(注意力分数)
第 2 步,是计算 Attention Score(注意力分数)。假设我们现在计算第一个词 Thinking
的 Attention Score(注意力分数),需要根据 Thinking
这个词,对句子中的其他每个词都计算一个分数。这些分数决定了我们在编码Thinking
这个词时,需要对句子中其他位置的每个词放置多少的注意力。
这些分数,是通过计算 "Thinking" 对应的 Query 向量和其他位置的每个词的 Key 向量的点积,而得到的。例如我们计算句子中第一个位置单词的 Attention Score(注意力分数),那么第一个分数就是 q1 和k1的内积,第二个分数就是q1和 k2 的点积。
第 3 步就是把每个分数除以 根号dk( dk是 Key 向量的长度)。你也可以除以其他数,除以一个数是为了在反向传播时,求取梯度更加稳定。
第 4 步,接着把这些分数经过一个 Softmax 层,Softmax可以将分数归一化,这样使得分数都是正数并且加起来等于 1。这些分数决定了在编码当前位置(这里的例子是第一个位置)的词时,对所有位置的词分别有多少的注意力。很明显,在上图的例子中,当前位置(这里的例子是第一个位置)的词会有最高的分数,但有时,关注到其他位置上相关的词也很有用。
第 5 步,得到每个位置的分数后,将每个分数分别与每个 Value 向量相乘。这种做法背后的直觉理解就是:对于分数高的位置,相乘后的值就越大,我们把更多的注意力放到了它们身上;对于分数低的位置,相乘后的值就越小,这些位置的词可能是相关性不大的,这样我们就忽略了这些位置的词。
第 6 步是把上一步得到的向量相加,就得到了 Self Attention 层在这个位置(这里的例子是第一个位置)的输出。
上面这张图,包含了 Self Attention 的全过程,最终得到的当前位置(这里的例子是第一个位置)的向量会输入到前馈神经网络。但这样每次只能计算一个位置的输出向量,在实际的代码实现中,Self Attention 的计算过程是使用矩阵来实现的,这样可以加速计算,一次就得到所有位置的输出向量。下面让我们来看,如何使用矩阵来计算所有位置的输出向量。
使用矩阵计算 Self-Attention
第一步是计算 Query,Key,Value 的矩阵。首先,我们把所有词向量放到一个矩阵 X 中,然后分别和 3 个权重矩阵WQ、WK、WV 相乘,得到 Q,K,V 矩阵。
矩阵 X 中的每一行,表示句子中的每一个词的词向量,长度是 512。Q,K,V 矩阵中的每一行表示 Query 向量,Key 向量,Value 向量,向量长度是 64。接着,由于我们使用了矩阵来计算,我们可以把上面的第 2 步到第 6 步压缩为一步,直接得到 Self Attention 的输出。
多头注意力机制(multi-head attention)
Transformer 的论文通过增加多头注意力机制(一组注意力称为一个 attention head),进一步完善了 Self Attention 层。这种机制从如下两个方面增强了 attention 层的能力:
- 它扩展了模型关注不同位置的能力。在上面的例子中,第一个位置的输出 z1 包含了句子中其他每个位置的很小一部分信息,但 z1 可能主要是由第一个位置的信息决定的。当我们翻译句子:
The animal didn’t cross the street because it was too tired
时,我们想让机器知道其中的it
指代的是什么。这时,多头注意力机制会有帮助。 - 多头注意力机制赋予 attention 层多个“子表示空间”。下面我们会看到,多头注意力机制会有多组 WQ、WK、WV的权重矩阵(在 Transformer 的论文中,使用了 8 组注意力(attention heads)。因此,接下来我也是用 8 组注意力头 (attention heads))。每一组注意力的WQ、WK、WV的权重矩阵都是随机初始化的。经过训练之后,每一组注意力可以看作是把输入的向量映射到一个”子表示空间“。
在多头注意力机制中,我们为每组注意力维护单独的WQ、WK、WV权重矩阵,将输入 X 和每组注意力的WQ、WK、WV相乘,得到8组Q, K, V 矩阵。
接着,我们把每组 K, Q, V 计算得到每组的 Z 矩阵,就得到 8 个 Z 矩阵。
接下来就有点麻烦了,因为前馈神经网络层(FFNN)接收的是 1 个矩阵(其中每行的向量表示一个词),而不是 8 个矩阵。所以我们需要一种方法,把 8 个矩阵整合为一个矩阵。我们可以把这8个矩阵拼接起来,然后和另一个权重矩阵WO 相乘。
具体步骤如下:
- 把 8 个矩阵 {Z0,Z1...,Z7} 拼接起来
- 把拼接后的矩阵和 WO 权重矩阵相乘
- 得到最终的矩阵 Z,这个矩阵包含了所有 attention heads(注意力头) 的信息。这个矩阵会输入到 FFNN (Feed Forward Neural Network)层。
这就是多头注意力的全部内容。用一张图总结如下:
代码实现矩阵计算 Attention
使用 PyTorch 库的实现
PyTorch 提供了 MultiheadAttention
来实现 attention 的计算。
torch.nn.MultiheadAttention(embed_dim, num_heads, dropout=0.0, bias=True, add_bias_kv=False, add_zero_attn=False, kdim=None, vdim=None)
参数说明如下:
embed_dim
:最终输出的 K、Q、V 矩阵的维度,这个维度需要和词向量的维度一样num_heads
:设置多头注意力的数量。如果设置为 1,那么只使用一组注意力。如果设置为其他数值,那么 num_heads 的值需要能够被 embed_dim 整除;dropout
:这个 dropout 加在 attention score 后面。
现在来解释一下,为什么 num_heads 的值需要能够被 embed_dim 整除。这是为了把词的隐向量长度平分到每一组,这样多组注意力也能够放到一个矩阵里,从而并行计算多头注意力。
例如,我们前面说到,8 组注意力可以得到 8 组 Z 矩阵,然后把这些矩阵拼接起来,得到最终的输出。如果最终输出的每个词的向量维度是 512,那么每组注意力的向量维度应该是 512/8=64,如果不能够整除,那么这些向量的长度就无法平均分配。
定义了MultiheadAttention
的对象后,调用时传入的参数如下:
forward(query, key, value, key_padding_mask=None, need_weights=True, attn_mask=None)
参数说明如下:
- query:对应于 query矩阵,形状是 (L,N,E) 。其中 L 是输出序列长度,N 是 batch size,E 是词向量的维度;
- key:对应于 Key 矩阵,形状是 (S,N,E) 。其中 S 是输入序列长度,N 是 batch size,E 是词向量的维度;
- value:对应于 Value 矩阵,形状是 (S,N,E) 。其中 S 是输入序列长度,N 是 batch size,E 是词向量的维度;
- key_padding_mask:如果提供了这个参数,那么计算 attention score 时,忽略 Key 矩阵中某些 padding 元素,不参与计算 attention。形状是 (N,S)。其中 N 是 batch size,S 是输入序列长度。
- 如果 key_padding_mask 是 ByteTensor,那么非 0 元素对应的位置会被忽略
- 如果 key_padding_mask 是 BoolTensor,那么 True 对应的位置会被忽略
注意:在前面的讲解中,我们的 K、Q、V 矩阵的序列长度都是一样的。但是在实际中,K、V 矩阵的序列长度是一样的,而 Q 矩阵的序列长度可以不一样。这种情况发生在:在解码器部分的Encoder-Decoder Attention
层中,Q 矩阵是来自解码器下层,而 K、V 矩阵则是来自编码器的输出。
在完成了编码(encoding)阶段之后,我们开始解码(decoding)阶段。解码(decoding )阶段的每一个时间步都输出一个翻译后的单词(这里的例子是英语翻译)。
输出:
- attn_output:形状是 (L,N,E)
- attn_output_weights:形状是 (N,L,S)
标签:Transformer,Self,Attention,矩阵,编码器,模块,初探,注意力,向量 From: https://www.cnblogs.com/afei688/p/16598561.html