\[ \begin{align}\begin{aligned}\newcommand{\ba}{\boldsymbol{a}} \newcommand{\bb}{\boldsymbol{b}} \newcommand{\be}{\boldsymbol{e}} \newcommand{\bw}{\boldsymbol{w}} \newcommand{\bx}{\boldsymbol{x}} \newcommand{\by}{\boldsymbol{y}} \newcommand{\bz}{\boldsymbol{z}} \newcommand{\bd}{\boldsymbol{d}} \newcommand{\bv}{\boldsymbol{v}} \newcommand{\bs}{\boldsymbol{s}}\\\newcommand{\btheta}{\boldsymbol{\theta}} \newcommand{\bbeta}{\boldsymbol{\beta}} \newcommand{\bgamma}{\boldsymbol{\gamma}} \newcommand{\bsigma}{\boldsymbol{\sigma}} \newcommand{\md}{\mbox{d}} \newcommand{\bmu}{\boldsymbol{\mu}} \newcommand{\bone}{\boldsymbol{1}} \newcommand{\trans}{^{\rm\scriptsize T}} \newcommand{\var}{\mathrm{var}}\\\newcommand{\bA}{\boldsymbol{A}} \newcommand{\bB}{\boldsymbol{B}} \newcommand{\bC}{\boldsymbol{C}} \newcommand{\bD}{\boldsymbol{D}} \newcommand{\bI}{\boldsymbol{I}} \newcommand{\bM}{\boldsymbol{M}} \newcommand{\bW}{\boldsymbol{W}} \newcommand{\bX}{\boldsymbol{X}} \newcommand{\bY}{\boldsymbol{Y}} \newcommand{\bZ}{\boldsymbol{Z}} \newcommand{\cotp}{\textcolor{ #30D158FF }{TP}} \newcommand{\cotn}{\textcolor{#64D2FFFF}{TN}} \newcommand{\cofp}{\textcolor{#5E5CE6FF}{FP}} \newcommand{\cofn}{\textcolor{#BF5AF2FF}{FN}}\\\newcommand{\numcotp}{\textcolor{ #30D158FF }{50}} \newcommand{\numcotn}{\textcolor{#64D2FFFF}{30}} \newcommand{\numcofp}{\textcolor{#5E5CE6FF}{10}} \newcommand{\numcofn}{\textcolor{#BF5AF2FF}{10}}\end{aligned}\end{align} \]

注意力机制#

\(\hspace{1.5em}\) 虽然LSTM和GRU在一定程度上能缓解梯度消失的问题,但当处理很长的序列时,它们仍可能面临信息丢失或记忆不足的困境。比如在机器翻译中,我们常用考虑使用 seq2seq 架构。编码器将输入序列压缩为一个固定长度的向量,作为整个序列的“总结”,然后解码器根据这个向量生成目标序列。这里的问题在于:如果输入的序列非常长,编码器输出的向量往往更偏向序列后部分的信息,而无法全面代表整个序列。此外,在解码器的每一步中,我们使用的上下文信息是相同的,忽视了序列中不同部分的重要性差异。

\(\hspace{1.5em}\) 第一个问题出现的原因是,编码器中的最后一个隐藏状态(last hidden state)并不能很好的代表整个输入序列的信息。那么一个简单的想法是,我将编码器中的所有信息同步输入到解码器中,这样就能充分利用整个输入序列的信息。为了解决第二个问题,我们可以考虑对输入序列的不同部分加入权重,这样就能更好的利用重要的部分信息。上述两者的结合就是注意力机制(attention mechanism)。从这个角度来理解,注意力机制相当于是在编码器和解码器之间添加了一条捷径(shortcuts),用于更好的利用输入序列的信息。

\(\hspace{1.5em}\) 下面,我们给出注意力机制的基本结构:

../_images/Figure_6_11_attention.png

注意力机制的基本结构#

\(\hspace{1.5em}\) 在上图中, \(a_t \in \mathcal{R}^{d_h}\) 是编码器的隐藏状态,\(a_t=s_0 \in \mathcal{R}^{d_h}\) 是编码器最后的隐藏状态,同时也是解码器的初始隐藏状态。与普通的seq2seq不同,在基于注意力机制的解码器其中,我们会多一个上下文变量 \(c_t \in \mathcal{R}^{d_h}\)。简单来说这个 \(c_t\) 是编码器中各个时刻隐藏状态的 加权平均。我们可以理解为,假设时刻 \(t\) 中编码器的输入很重要,那么其权重就会更大,反之则权重更小。那么我们应该怎么去衡量这个权重呢?假设我们考虑输入和输出是一对一的情况,也就是说,在每一个时刻 \(t\) 编码器的输入和解码器的输出是也应该是一一对应的。在解码器中,时刻 \(t\) 时我们会有一个 \(a_t\) 作为编码器的隐藏状态,同时有一个 \(s_t\) 作为解码器的隐藏状态,如果两个隐藏状态相似,那么这两个隐藏状态对应的信息也是相似的。以机器翻译为例,如果输入序列的第 \(t\) 个词是“猫”,那么输出序列的第 \(t\) 个词也应该是“cat”。因此,我们可以使用一个函数来衡量这两个隐藏状态的相似程度,这个函数就是注意力机制的核心。通过将 \(s_t\) 与输入序列(编码器隐藏状态)一一计算相似度,最后我们可以通过 softmax 函数来得到权重,然后使用这些权重对编码器隐藏状态进行加权求和得到上下文变量 \(c_t\)。下面我们给出注意力机制的计算公式:

\[\begin{split}e_{ij} &= score(s_i, a_j)\\ \omega_{ij} &= \frac{\exp(e_{ij})}{\sum_{k=1}^{T} \exp(e_{ik})} \\ c_i &= \sum_{j=1}^{T} \omega_{ij} a_j\end{split}\]

\(\hspace{1.5em}\) 上面我们提到了我们可以使用一个函数来衡量两个隐藏状态的相似程度,下面我们将介绍几种文献中常用的计算相似度的方法:

  • Content-base attention: \(score(s_i, a_j) = cosine(s_i, a_j)\)

  • Additive: \(score(s_i, a_j) = v_a^{\top} \tanh(W_a s_i + U_a a_j)\)

  • Location-Base: \(score(s_i, a_j) = softmax(W_a s_i)\)

  • General: \(score(s_i, a_j) = s_i^{\top} W_a a_j\)

  • Dot-Product: \(score(s_i, a_j) = s_i^{\top} a_j\)

其中, \(v_a, W_a, U_a\) 是需要学习的参数。

自注意力和位置编码#

\(\hspace{1.5em}\) 在上一小节中,我们介绍了在seq2seq架构中如何使用注意力机制来获取全局的信息。其基本思想是,通过对编码器的隐藏状态进行加权求和,得到一个上下文变量,然后将这个上下文变量与解码器的隐藏状态进行拼接,从而更好的捕捉序列中的信息。既然能通过注意力机制来获取整个序列的信息,那么我们是不是可以舍弃RNN中的循环结构呢?答案是肯定的。下面,我们将介绍自注意力机制(self-attention mechanism)以及位置编码(positional encoding)。

自注意力机制#

\(\hspace{1.5em}\) 在循环神经网络中,序列之间的相关性是通过隐藏状态(在LSTM中还包括了单元状态)来进行传递的。假设输入序列为 \(x_1, x_2, \dots, x_T\),我们的目标是在每一个时刻 \(t\) 获得当前输入的一个潜在表达形式 \(h_t\)。如果我们不使用循环结果,而是使用注意力机制来捕捉序列间的相关性,那么我们可以考虑使用如下方式:

\[\begin{split}e_{t,i} &= \text{score}(x_t, x_i) \\ \alpha_{t,i} &= \frac{\exp(e_{t,i})}{\sum_{k=1}^{n} \exp(e_{t,k})} \\ h_t &= \sum_{i=1}^{n} \alpha_{t,i} x_i\end{split}\]

\(\hspace{1.5em}\) 在自注意力机制中,我们是将 \(t\) 时刻的输入 \(x_t\) 与所有输入 \(x_i, i = 1, \dots, T\) 进行比较,然后通过 softmax 函数得到权重,最后以这些权重对输入进行加权得到上下文变量 \(h_t\)。这样的好处在于,我们可以同时考虑到所有的输入,而不是像RNN一样一个接一个的考虑。由上述公式我们可以发现,自注意力机制的计算方式与普通的注意力机制的计算方式是完全相同的。不过在自注意力中,是将 \(t\) 时刻的输入与所有输入进行比较;而在普通注意力机制中,将 \(t\) 时刻解码器的隐藏状态与所有编码器的隐藏状态进行比较。

位置编码#

\(\hspace{1.5em}\) 在自注意力机制中,我们是对所有的输入进行比较,然后通过 softmax 函数得到权重。这种情况下,自注意力机制没办法考虑到输入的顺序信息。为了解决这个问题,我们可以引入位置编码(positional encoding)。位置编码是指,我们为每一个输入的位置添加一个特定的编码,这样就能保留输入的顺序信息。这个编码信息可以是预先设定(pre-specified),也可以作为参数进行学习。

\(\hspace{1.5em}\) 假设输入序列为 \(x_1, x_2, \dots, x_T \in \mathcal{R}^d\),位置编码需要与输入的维度相同。例如,对于 \(x_i\)\(2j\) 和第 \(2j + 1\) 上的元素,我们可以使用如下公式进行编码 1该位置编码可以理解为二进制的浮点数表示,具体参见 《动手学深度学习(中文版)》 第10章。

\[\begin{split}PE_{(i, 2j)} &= \sin(i / 10000^{2j / d}) \\ PE_{(i, 2j + 1)} &= \cos(i / 10000^{2j / d})\end{split}\]

\(\hspace{1.5em}\) 最后通过将 \(x_i\) 与位置编码 \(PE_{i}\) 相加,我们就能得到一个新的输入向量,从而保留输入的顺序信息。

2推荐阅读 Attention is All You Need 以及观看视频 Transformer论文逐段精读【论文精读】

Transformer 2推荐阅读 Attention is All You Need 以及观看视频 Transformer论文逐段精读【论文精读】#

\(\hspace{1.5em}\) 在之前的章节中,我们介绍了RNN及其拓展模型LSTM和GRU。在实际应用中,这些模型取得了非常不错的结果。但是,循环神经网络(RNNs)存在的一个非常大的缺陷是,不能并行化。这是因为我们在计算 \(t\) 时刻隐藏状态时,必须要先计算出 \(h_{t-1}\)。以输入为一个句子为例,循环神经网络是一个词一个词处理。以人类阅读为例,这种方式的阅读效率是非常低的,这就导致这些模型在实际应用中会有一定局限性。除此之外,我们也提到过,针对于长序列,循环神经网络在实际应用时也会存在许多问题。下面,我们将介绍大语言模型的基础:Transformer。为了更好的理解Transformer,我们首先给出Transformer的基本结构,然后分别介绍每个部分的计算方式,最后,以 pytorch 库中的 Transformer 为例来说明Transformer是如何实现的。

../_images/Figure_6_12_transformer.png

Transformer模型的基本结构#

\(\hspace{1.5em}\) 从上图我们可以发现,Transformer使用了经典的seq2seq架构,其中编码器和解码器旁边的 Nx 表示由 N 个完全相同的块(block)堆叠(stack)在一起。和之前的模型相比,Transformer多了一些额外的操作,例如(带掩码的,Masked)多头自注意力机制(Multi-Head-Attention,MHA),层次归一化(Layer Normalization,Add&Norm)等。忽略这部分,Transformer就和普通的seq2seq架构一样:输入是一个文本序列,输出是一个下一个词元概率分布。下面,我们先从多头注意力机制开始介绍Transformer的组成结构。

多头自注意力机制(Multi-Head-Attention,MHA)#

../_images/Figure_6_13_MHA.png

注意力机制(左)和多头注意力机制(右)示意图。#

\(\hspace{1.5em}\) 上图给出了Transformer中多头自注意力机制的示意图。其中左边部分与之前介绍过的自注意力机制相同,核心思想也是对输入变量进行加权求和;而右边的多头(Multi-Head)可以与CNN中的通道(Channel)做类比,用于学习不同类型的相关性。假设我们的序列为 \(X = (x_1, \dots, x_T)^{\top} \in \mathcal{R}^{T \times d}\),为了对自注意力的计算进行矩阵操作,这里使用了 查询(Query,Q)键(Key,K) 以及 值(Value,V) 来进行计算,我们首先给出三个矩阵的计算公式:

\[Q = X W_Q, K = X W_K, V = X W_V,\]

其中 \(W_Q \in \mathcal{R}^{d \times d_k}, W_K \in \mathcal{R}^{d \times d_k}, W_V \in \mathcal{R}^{d \times d_v}\) 是需要学习的参数,\(d_k\)QK 的维度,\(d_v\)V 的维度。然后我们可以通过如下公式计算注意力分数(基于 Dot-Product):

\[\text{Attention}(Q, K, V) = \text{softmax}(\frac{Q K^{\top}}{\sqrt{d}}) V \in \mathcal{R}^{n \times d_v}.\]

\(\hspace{1.5em}\) 在注意力机制中:

  1. Query :代表了我们正在询问的信息或我们关心的上下文。在自注意力机制中,每个序列元素都有一个对应的查询,它试图从其他部分找到相关信息。

  2. Key :这是可以查询的条目或“索引”。在自注意力机制中,每个序列元素都有一个对应的键。

  3. Value :对于每一个“键”,都有一个与之关联的“值”,它代表实际的信息内容。当查询匹配到一个特定的键时,其对应的值就会被选中并返回。

\(\hspace{1.5em}\) 这种思路与数据库查询非常相似,可以将 Query 看作是搜索查询,Key 看作是数据库索引,而 Value 则是实际的数据库条目。以自注意力机制为例,我们需要序列中每个时刻的值 \(x_t\)Query)与所有时刻的值 \(x_1, x_2, \dots, x_T\)Key)进行比较,然后通过 softmax 函数得到权重,最后将这些权重与所有时刻的值 \(x_1, x_2, \dots, x_T\)Value)相乘,得到最终的输出。放到普通的注意力机制中,Query 就是解码器中的隐藏状态,Value 就是编码器中的隐藏状态,Key 就是编码器中的隐藏状态。

\(\hspace{1.5em}\) 多头自注意力中的多头,与CNN中的 多通道 类似,通过不同的 \(W_Q, W_V, W_k\) (类似于CNN中的 kernel )来学习不同类型的相关性。在实际应用中,我们可以设置多个头,然后将这些头的输出进行拼接,最后通过一个线性变换得到最终的输出。下面我们给出多头自注意力的计算公式:

\[\begin{split}\text{MultiHead}(Q, K, V) &= \text{Concat}(\text{head}_1, \dots, \text{head}_h) W^O \\ \text{head}_i &= \text{Attention}(Q_i, K_i, V_i),\end{split}\]

其中 \(Q_i = X W_i^Q, K_i = X W_i^K, V_i = X W_i^V\)\(W_i^Q \in \mathcal{R}^{d \times d_k}, W_i^K \in \mathcal{R}^{d \times d_k}, W_i^V \in \mathcal{R}^{d \times d_v}\) 是需要学习的参数, \(W^O \in \mathcal{R}^{h*d_v \times d}\) 是需要学习的参数, \(h\) 是头的个数。在Transformer中,\(h = 8\)\(d = 512\)\(d_k = d_v = 64\)

掩码操作(masked MHA)#

\(\hspace{1.5em}\) 上一小节中,我们介绍了多头自注意力机制。但是我们发现,在Transformer的解码器中,有一个特殊的结构叫做带掩码的自注意力机制,下面我们先介绍为什么需要带掩码的自注意力机制,然后介绍掩码操作的计算方式。

\(\hspace{1.5em}\) RNN在进行预测 3需要注意的是,RNN中的预测是自回归的,也就是说,我们在预测时,总是需要先拿到前一时刻的预测,然后再预测下一个时刻的输出。在 训练阶段,我们是将真实的标签作为输入,然后预测下一个时刻的输出;而在 预测阶段,我们是将前一时刻的预测作为输入,然后预测下一个时刻的输出。Transformer中同样是如此。 时,总是需要先拿到前一时刻的预测 \(\hat y_{t-1}\),将 \(\hat y_{t-1}\) 作为输入,然后再预测下一个时刻的输出 \(\hat y_t\) (自回归)。但是在Transformer中,模型训练时我们是一次性拿到整个序列的输入,然后一次性输出整个序列的输出。这就导致了一个问题,如果我们在预测时,拿到了后面的信息,那么这个信息就会影响到前面的预测 4因为Transformer在计算注意力时是对整个句子中的每个词进行加权求和,因此在预测时,如果拿到了后面的信息,那么这个信息就会影响到前面的预测。。为了解决这个问题,我们需要将后面的信息隐藏起来。掩码操作的思想是,我们在预测时,只能看到当前时刻之前的信息,而不能看到当前时刻之后的信息。这样就能保证我们的预测不会受到未来信息的影响。

\(\hspace{1.5em}\) 在Transformer中,一共有三种掩码:

  1. Padding Mask:之前我们提到,文本序列的长度通常是不一样的,为了保证输入序列的长度一致(这样才能使用定长的 tensor 来计算)。对于较长的序列,通常会对序列进行截断;而对于较短的序列,通常会在序列后面加上一些特殊的符号。在Transformer中,通常使用 <PAD> 词元作为填充符号,这样就会导致一些无效的信息。为了让模型能够忽略这些无效信息,我们需要引入填充掩码,将填充的位置的权重设置为0,这样就能保证填充的位置不会对模型的训练产生影响;

  2. Attention Mask:上面我们提到,为了保证在预测时,只能看到当前时刻之前的信息,在每个时刻我们要保证当前时刻之后的信息不能被看到。在Transformer中,我们可以通过将当前时刻之后的信息设置为负无穷,然后通过 softmax 函数得到的权重就会接近于0,这样就能保证在预测时,只能看到当前时刻之前的信息;

../_images/Figure_6_14_attn_mask.png

Attention Mask的示意图。(图片来源:Transformers: The Nuts and Bolts#

  1. Sequence Mask:用于隐藏输入序列的某些部分。例如,在双向模型(BERT)中,我们可能希望根据特定标准忽略序列的某些部分。

Layer Normalization#

\(\hspace{1.5em}\) 在之前的章节中,我们介绍了通过批量归一化(Batch Normalization)来解决内部变量偏移(internal covariate shift)的问题。故名思义,批量归一化是在 Batch 的维度上做归一化。在序列数据中,输入的维度通常是 [batch_size, seq_len, embedding_dim],批量归一化是指对于每一个 embedding_dim,计算均值和方差,然后对整个 batch 进行归一化。但是在RNN中,由于序列的长度是不一样的,因此我们无法在 Batch 的维度上做归一化。为了解决这个问题,我们可以使用层次归一化(Layer Normalization)。层次归一化是指,对每一个 batchseq_len,分别做归一化 。下面我们给出层次归一化的计算公式以及两种归一化方式的对比:

\[\text{LayerNorm}(x) = \gamma \odot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta,\]
../_images/Figure_6_15_layer_vs_batch.png

层次归一化和批量归一化的对比。(图片来源:Shen et al. (2020)#

\(\hspace{1.5em}\) 下面,我们以 pytorch 中的 LayerNormBatchNorm1d 为例来介绍这两种归一化方式的区别:

 1import torch
 2import torch.nn as nn
 3
 4# 假设输入是文本序列,那么输入的维度为 [batch_size, seq_len, embedding_dim]
 5# 即:batch_size 个句子,每个句子有 seq_len 个词,每个词用 embedding_dim 维的向量表示
 6batch_size, seq_len, embedding_dim = 2, 2, 3
 7
 8# 创建一个张量,x = [batch_size, seq_len, embedding_dim]
 9w = [[[1, 2, 4], [2, 3, 4]], [[3, 4, 4], [4, 4, 4]]]
10w = torch.tensor(w, dtype=torch.float32)
11
12# 定义层次归一化
13layer_norm = nn.LayerNorm(embedding_dim)
14layer_norm_output = layer_norm(w)
15
16# 定义批量归一化
17batch_norm = nn.BatchNorm1d(embedding_dim)
18# BatchNorm1d 期望输入的形状为 (batch_size, embedding_dim, seq_len)
19# 请参考 nn.BatchNorm1d 的官方文档
20batch_norm_output = batch_norm(w.permute(0, 2, 1)).permute(0, 2, 1)
21
22# 打印张量和归一化后的输出
23print('原始向量:')
24print(w)
25print('层次归一化结果:')
26print(layer_norm_output.data)
27print('批量归一化结果:')
28print(batch_norm_output.data)

\(\hspace{1.5em}\) 上述代码会产生如下结果:

原始向量:
tensor([[[1., 2., 4.],
         [2., 3., 4.]],

      [[3., 4., 4.],
         [4., 4., 4.]]])
层次归一化结果:
tensor([[[-1.0690, -0.2673,  1.3363],
         [-1.2247,  0.0000,  1.2247]],

      [[-1.4142,  0.7071,  0.7071],
         [ 0.0000,  0.0000,  0.0000]]])
批量归一化结果:
tensor([[[-1.3416, -1.5075,  0.0000],
         [-0.4472, -0.3015,  0.0000]],

      [[ 0.4472,  0.9045,  0.0000],
         [ 1.3416,  0.9045,  0.0000]]])

\(\hspace{1.5em}\) 从上面的结果我们可以看到,层次归一化是在每个词的所有特征上做归一化,例如 x[2, 1] = [4, 4, 4] (第3个句子,第2个词),层次归一化后的结果是 [0, 0, 0];而批量归一化是在每个特征的所有样本上做归一化,例如 x[:, :, 2] = [4, 4, 4, 4] (所有句子中,最后一个词的最后一个 feature),批量归一化后的结果是 [0, 0, 0, 0]

数据流(dataflow)#

\(\hspace{1.5em}\) 在了解了Transformer的基本结构之后,下面我们按照模型的工作流程来介绍Transformer的计算方式 5针对Transformer的实现,可以参考 The Annotated Transformer 。针对Transformer数据流的可视化,可以参考 Transformer Explainer。我们首先打印 pytorchtransformer 的结构,然后按照源码的顺序来介绍Transformer的计算方式。

1from torch.nn import Transformer
2
3# 定义一个Transformer模型
4model = Transformer(d_model=512, nhead=8, num_encoder_layers=6, num_decoder_layers=6, dim_feedforward=2048, batch_first=True)
5
6# 打印模型结构
7print(model)

\(\hspace{1.5em}\) 上述代码的运行结果为:

Transformer(
   (encoder): TransformerEncoder(
      (layers): ModuleList(
         (0-5): 6 x TransformerEncoderLayer(
         (self_attn): MultiheadAttention(
            (out_proj): NonDynamicallyQuantizableLinear(in_features=512, out_features=512, bias=True)
         )
         (linear1): Linear(in_features=512, out_features=2048, bias=True)
         (dropout): Dropout(p=0.1, inplace=False)
         (linear2): Linear(in_features=2048, out_features=512, bias=True)
         (norm1): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
         (norm2): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
         (dropout1): Dropout(p=0.1, inplace=False)
         (dropout2): Dropout(p=0.1, inplace=False)
         )
      )
      (norm): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
   )
   (decoder): TransformerDecoder(
      (layers): ModuleList(
         (0-5): 6 x TransformerDecoderLayer(
         (self_attn): MultiheadAttention(
            (out_proj): NonDynamicallyQuantizableLinear(in_features=512, out_features=512, bias=True)
         )
         (multihead_attn): MultiheadAttention(
            (out_proj): NonDynamicallyQuantizableLinear(in_features=512, out_features=512, bias=True)
         )
         (linear1): Linear(in_features=512, out_features=2048, bias=True)
         (dropout): Dropout(p=0.1, inplace=False)
         (linear2): Linear(in_features=2048, out_features=512, bias=True)
         (norm1): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
         (norm2): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
         (norm3): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
         (dropout1): Dropout(p=0.1, inplace=False)
         (dropout2): Dropout(p=0.1, inplace=False)
         (dropout3): Dropout(p=0.1, inplace=False)
         )
      )
      (norm): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
   )
)

其中 self_attn 表示编码器和解码器中的 Multi-head Attentionmultihead_attn 表示解码器中的 Masked Multi-head Attentionlinearlayer_normdropout 分别表示全连接层、层次归一化和 Dropout 层。下面以英文翻译中文为例来解释Transformer中的数据流。假设输入是一段英文文本,输出是翻译的中文文本(文本序列长度不同)。令 \(w_1, w_2, \dots, w_n\) 为原始输入文本(Transformer模型的基本结构 中的 Inputs),语料中一共有 \(\mathcal{V}\) 个词,Transformer中的数据流(模型训练阶段)可以表示如下:

  • 编码器:

      6在实际应用中,还需要对原始序列进行截断或者填充,同时会加上一些特殊词元(例如 <bos>, <eos> 等)。通常来说,这一个操作可以通过模型自带的 tokenizer 实现。
    1. 词元化:首先,我们需要将输入文本转换为词元(token)的形式,然后利用词表(vocabulary)将需入序列转换为一个长度相同的 id 序列 \(v_1, v_2, \dots, v_n\) 6在实际应用中,还需要对原始序列进行截断或者填充,同时会加上一些特殊词元(例如 <bos>, <eos> 等)。通常来说,这一个操作可以通过模型自带的 tokenizer 实现。

    2. 7请参考 nn.Embedding 的官方文档。其本质是将标量(id)映射为向量(embedding)。这里的 embedding 可以看作是每一个词元的原始词嵌入,并没有利用上下文信息。
    3. 词嵌入:拿到 id 序列之后,我们需要将这些 id 转换为对应的词向量。这个操作可以通过 Embedding 层来实现,假设词向量的维度为 \(d = 512\),那么我们可以用 nn.Embedding() 7请参考 nn.Embedding 的官方文档。其本质是将标量(id)映射为向量(embedding)。这里的 embedding 可以看作是每一个词元的原始词嵌入,并没有利用上下文信息。 来实现。最后,我们拿到了原始的词嵌入向量 \(x_1, x_2, \dots, x_n \in \mathcal{R}^{d}\),这也就是 Transformer模型的基本结构 中的 Input Embedding。为了保证 embeddingpositional encoding 相比数值上不会太小,Transformer在会将 embedding 乘以一个系数 \(\sqrt{d}\)

    4. 添加位置编码:对于词嵌入向量,我们需要添加位置编码。计算每个位置的位置编码后,会直接将位置编码与词嵌入向量相加,得到新的输入向量。我们用 \(x_1, x_2, \dots, x_n \in \mathcal{R}^{d}\) 来表示添加位置编码后的输入向量。

    5. 8在多头自注意力机制中,我们将输入向量的维度 \(d\) 分为 \(h\) 个头,每个头的维度为 \(d_v = d_k = d / h\)。最后拼接在一起的向量维度仍然为 \(d\)
    6. 多头自注意力机制:对于添加位置编码后的输入向量,我们需要通过多头自注意力机制来获取全局的信息。由于是自注意力机制,因此我们对每个向量乘上一个矩阵 \(W_Q, W_K, W_V \in \mathcal{R}^{d \times d / h}\) 8在多头自注意力机制中,我们将输入向量的维度 \(d\) 分为 \(h\) 个头,每个头的维度为 \(d_v = d_k = d / h\)。最后拼接在一起的向量维度仍然为 \(d\) 得到每个向量对应的 \(Q, K, V \in \mathcal{R}^{d / h}\) 值,最后拿到加权后的输出向量 \(z_{h, 1}, z_{h, 2}, \dots, z_{h_n} \in \mathcal{R}^{d / h}\)。通过将每个头对应位置的向量拼接在一起,我们就得到了最终的输出向量 \(z_1, z_2, \dots, z_n \in \mathcal{R}^{d}\)

    7. Add&Norm:在这一步,我们需要将多头自注意力机制的输出向量与输入向量相加(与 Resnet 相似),然后再进行层次归一化,最后输出 \(x_1, x_2, \dots, x_n \in \mathcal{R}^{d}\)

    8. 逐位置的MLP:在这一步,我们需要对 Add&Norm 的输出向量进行全连接操作,先将维度增加到 \(d_{ff} = 2048\),然后再将维度降回到 \(d = 512\)。计算公式为:math:MLP(x) = ReLU(xW_1 + b_1)W_2 + b2。这一步对应 Transformer模型的基本结构 中的 Feed Forward,在代码中包括了 linear1linear2。最后得到 \(x_1, x_2, \dots, x_n \in \mathcal{R}^{d}\)

    9. Add&Norm:与步骤 5 相同。

    10. 重复步骤 4-7N=6 (编码器层数)次。注意在这个过程中,每个块的输入和输出维度都是不变的,所以最后我们拿到的还是 \(x_1, x_2, \dots, x_n \in \mathcal{R}^{d}\)

  • 解码器:

    1. 与编码器中的步骤1-3相同,我们将目标序列(中文)转换为了词嵌入向量 \(y_1, y_2, \dots, y_n \in \mathcal{R}^{d}\)。值得注意的是,Transformer的编码器和解码器使用了同一个词嵌入矩阵。

    2. 带掩码的自注意力机制:与编码器中步骤 4 类似,只是在计算注意力得分时,我们需要引入掩码操作。最后我们同样拿到了 \(y_1, y_2, \dots, y_n \in \mathcal{R}^{d}\)

    3. Add&Norm:与编码器中的步骤 5 相同。

    4. 注意力机制:在这一步,我们将编码器中的输出作为 KeyValue,将带掩码的自注意力机制的输出作为 Query,然后通过多头自注意力机制得到最终的输出。

    5. Add&Norm:与编码器中的步骤 5 相同。

    6. 逐位置的MLP:与编码器中的步骤 6 相同。

    7. Add&Norm:与编码器中的步骤 5 相同。

    8. 重复步骤 2-7N=6 (解码器层数)次。最后拿到 \(y_1, y_2, \dots, y_n \in \mathcal{R}^{d}\)

    9. token to id:这一步骤与编码器中的步骤 2 相反,是通过 Embedding 层将词嵌入向量转换为 id 序列,可以通过线性变换实现。

    10. Softmax:通过 Softmax 函数得到最终的输出概率分布。

Transformer中的Dropout

\(\hspace{1.5em}\) 在Transformer中,Dropout 出现在三个地方。第一个是多头自注意力机制当中,对 Attenion(Q, V, K) 执行 dropout 操作,对应代码中的 dropout;第二个是加上位置编码后的 embedding 之后,即编码器中的步骤 3 (解码器的步骤 1)之后;第三个个是层归一化之前,对应代码中的 dropout1dropout2dropout3。Transformer的层归一化通常是在 Add 之后,因此可以表示为 \(\text{LayerNorm}(x + \text{Dropout}(f(x)))\),其中 \(f(x)\) 表示 Multi-head AttentionFeed Forward 的输出。