transformer学习记录(二)原始结构

整体结构

原始的transformer应用在NLP领域,输入一个句子,输出另一个句子。整个结构可以分为编码器和解码器两大模块,其中编码器模块由若干个(6)结构相同的编码器按顺序连接,而解码器也有若干个顺序连接。如下图所示

输入

通过embedding algorithm(每个单词按字典对应一个向量)可以将输入的原始句子转换为embeddings。例如,一个单词转化为512维vector,一个句子的所有embeddings组成一个list. list的长度是固定的(通常可以设为训练集中最长的句子的长度),设定某个特殊embedding作为实际的句子结束符。

encoder

最底层的encoder接收输入向量,而接下来每一个encoder把上一个encoder的输出作为输入。每一个小的encoder结构是相同的。

自注意力层

每一个小的编码器在结构上可以分为两层:自注意力和前馈层。

每个位置的单词在编码器中都流经其自己的路径。自注意力层中这些路径存在依赖关系,因为自注意力层将寻找单词之间的联系。而前馈层则没有这种依赖关系,因此前馈层可以并行运行。

从high level理解self-attention

假设我们想要翻译下面这个句子:

1
"The animal didn't cross the street because it was too tired"

it在句子里指代哪个单词呢,我们希望自注意力将其与animal联系起来。当模型处理每个单词时,自注意力层查看其它单词并用当前词和其他词的关联程度,对当前词进行编码,希望为当前位置加上上下文的线索。

从detail理解self-attention

我们首先来看怎么用vector来计算self-attention,这个过程更容易理解,再进一步看它实际是如何用矩阵实现的。

vector形式

第一步:生成q,k,v

从每个输入向量生成3个向量。即对一个单词,我们生成一个query vector, key vector, value vector。 这些vector的维度通常小于输入embedding,主要是为了在多头注意力机制下保持输出维度的不变。

但是多头注意力的输出并不是拼在一起后就直接输入下一个encoder,而是还要经过一个聚合的矩阵,那这里为什么要保持维度呢?

第二步:计算注意力分数

假设我们要计算所有单词相对于thinking单词的自注意力分数(也就是所有单词与这个单词的联系),例如某个单词machine,其相对于thinking的注意力表示了我们要将多少程度的machines的含义加到thinking单词输出的编码值中。

这个分数通过对 query vector 和 key vector的点积得到,即在算thinking的注意力分数时,thinking本身的注意力是 \(q_1^T k_1\) , machines对于thinking的注意力是 \(q_1^T k_2\). 相对于是用代表单词1的query去和其他单词的 key 相乘,相似度表示了两者的联系。

第三步和第四步

先将分数除以8(key vector 维度的平方根),使得梯度更加稳定(值太大的话经过softmax 梯度趋向于0)。再经过一个softmax层,所有注意力转化为了和为1的概率值

第五步和第六步

各个注意力分数与value vector相乘。即把各个注意力乘上单词本身的值。再把所有乘完的值加起来,作为第一个单词在自注意力层一个头的输出。

整个过程可理解为:当前词的输出希望能计算一个由所有词的含义得到的加权和,那么问题就是所有词的权重怎么计算,要加权的值怎么计算。在实现中,要加权的值是由原始输入根据学习参数得到的value,权重值是由每个词自身的query去乘所有其他词的key得到,query相当于是当前词与其他词寻找联系的信息,key相当于是当前词能提供给其他词的信息。

矩阵形式

在实际的实现中,这些计算是通过矩阵来做的。所有的输入单词组成一个矩阵X(n x m), n是单词个数,m是每个单词的embedding维度。

第一步

X乘上三个系数矩阵(权重由训练进行优化)得到Q,K,V矩阵,如下图所示

第二步

最终输出可由Q,K,V计算得到:

\[Z=softmax(\frac{QK^T}{\sqrt{d_k}})V\]

多头注意力机制

多头注意力机制是指上述这样的Q,K,V计算了多次(由不同的权重矩阵生成新的Q,K,V),产生多个输出Z,拼在一起组成了下一个encoder的输入。

这样做的好处是:

  1. 它扩展了模型的能力,使其能关注不同的位置(可以理解为每个头的一个输出代表了输出位置的一个单词)
  2. 它使得注意力层有了多个表示子空间(每组随机初始化的Q,K,V能学习到一个不同的表示子空间),增强了模型的表达能力

用实际例子来说明:

1
"The animal didn't cross the street because it was too tired"

在对it进行编码时,训练得到的多头注意力的一个头会最关注animal,其他头更关注tired,因此最终模型对it的表示是animaltired表示的综合。

经过多头注意力机制,我们得到了多个矩阵Z:

为了把这个多个输出拼在一起,我们先将其直接拼接,再乘以一个权重矩阵W来聚合所有Z的信息:

残差

另外在实现上的一个细节是,自注意力和FFN层都连接了一个残差层。残差层使得网络只需拟合增量,而不需要拟合恒等映射,使得模型更能拟合到正确的表达。

LayerNorm

在经过残差连接后,再经过残差连接对特征做归一化。LayerNorm对每个样本内部做归一化,下图展示了LayerNorm 和 BatchNorm 归一化的维度的差别:

BatchNorm和LayerNorm的比较,transformer中使用LayerNorm的原因(参考https://zhuanlan.zhihu.com/p/492803886,但仍然没有太理解)

Feed Forward Network

对自注意力的输出进行多个线性连接和非线性激活函数,相对于对每个位置的向量进行非线性变换,从而提高模型的表达能力。

positional encoding

在前面描述的注意力机制中,我们用q,k,v能够为当前单词添加其他各个单词的信息,但这里却忽略了单词之间的距离,没有用上句子中各个单词的顺序信息,而这对于理解语义来说也是非常重要的。

为了表示sequence的顺序,transformer加入了一个positional encoding。

这些向量遵循一种特定模式,有助于确定每个单词的位置。在计算注意力期间,这些值在embedding中点积可以提供有意义的距离。

将positional encoding 可视化:

左图是Tenso2Tensor中的实现:横坐标为embedding的维度512,纵坐标为词的顺序,可以看到分成了两块区域,左半边是用sine函数生成的,右半边是用cosine函数生成的。右图是原始论文中的,把cosine sine两个信号穿插在一起。

decoder

encoder和decoder的连接方式如下图所示:

可以看到一个decoder内部包含两个注意力层。第一个是对decoder输入的自注意力,第二个使用了从encoder编码向量中得到的K,V矩阵。

decoder中的自注意力层的操作和encoder中有些许不同。自注意力层只被允许考虑之前位置的输出,这通过将未来的位置加上mask(将值设为 -Inf) 实现。

encoder的输出通过权重矩阵转化为了一些注意力向量K和V。这些向量在每个decoder的 encoder-decoder attention层中使用(即此时的Q来自decoder的上一层输出,K,V来自encoder的输出), 帮助decoder关注输入sequence中合适的位置。

每一步,decoder得到一个输出,这个输出会再次输入最底层的decoder得到一个新的输出,一直这样循环进行直到特殊标志输出。每一步的输出在输入decoder前也会加入位置编码 (decoder的第一个输入是设置的某个起始符,知道输出终止符时结束)

最后的线性层和softmax层

decoder 输出的一个向量,通过一个全连接层,转化为一个大的多的向量叫做逻辑向量 logits vector (它的长度是模型的词汇量的大小),这个向量的每个单元对应一个单词,单元的值代表这个单词的分数概率。 softmax层将所有分数转化到0-1 分数最高的单元对应的单词作为这个时刻的输出。

参考资料

[1] https://jalammar.github.io/illustrated-transformer/


transformer学习记录(二)原始结构
https://sisyphus-99.github.io/2023/10/18/transformer学习记录2/
Author
sisyphus
Posted on
October 18, 2023
Licensed under