1. 什么是 Transformer
Transformer 是 Google 提出的一种将 Attention 思想发挥到极致的模型。该论文提出了一个全新的架构,抛弃了以往深度学习任务中常用的 CNN 和 RNN。目前大热的 BERT 就是基于 Transformer 构建的,该模型广泛应用于 NLP 领域,例如机器翻译、问答系统、文本摘要和语音识别等方向。
2. Transformer 结构
2.1 总体结构
Transformer 采用了 Encoder-Decoder 架构。相比于传统的 Attention 模型,其结构更为复杂。在原始论文中,Encoder 层由 6 个相同的 Encoder 堆叠而成,Decoder 层同样由 6 个相同的 Decoder 堆叠而成。
每个 Encoder 和 Decoder 的内部结构包含多个子层(Sub-layers)。
- Encoder:包含两层,一个是 Self-Attention 层,另一个是前馈神经网络(Feed Forward Neural Network)。Self-Attention 帮助当前节点不仅仅关注当前的词,从而获取上下文的语义信息。
- Decoder:也包含类似 Encoder 的两层网络,但在中间增加了一层 Masked Multi-Head Attention,帮助当前节点获取到当前需要关注的重点内容,同时防止看到未来的信息。
2.2 Encoder 层结构
首先,模型需要对输入的数据进行 Embedding 操作,类似于 Word2Vec。Embedding 结束后,数据输入到 Encoder 层。Self-Attention 处理完数据后,将其送入前馈神经网络。前馈神经网络的计算可以并行,得到的输出会输入到下一个 Encoder 层。
2.2.1 Positional Encoding
Transformer 模型本身缺乏解释输入序列中单词顺序的方法,这与 RNN 等序列模型不同。为了解决这个问题,Transformer 给 Encoder 层和 Decoder 层的输入添加了一个额外的向量 Positional Encoding,其维度与 Embedding 的维度相同。这个向量采用了一种独特的方式让模型学习位置信息,决定当前词的位置或句子中不同词之间的距离。
论文中的计算方法如下:
$$PE(pos, 2i) = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$
$$PE(pos, 2i+1) = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$
其中 $pos$ 是指当前词在句子中的位置,$i$ 是指向量中每个值的索引。可以看出,在偶数位置使用正弦编码,在奇数位置使用余弦编码。最后将这个 Positional Encoding 与 Embedding 的值相加,作为输入送到下一层。
2.2.2 Self-Attention
Self-Attention 的思想与 Attention 类似,但它是 Transformer 用来将其他相关单词的'理解'转换成正在处理的单词的一种机制。例如在句子 "The animal didn't cross the street because it was too tired" 中,判断 it 指代的是 animal 还是 street。Self-Attention 能够让机器建立这种联系。
处理过程如下:
- 计算三个新的向量:Query (Q)、Key (K)、Value (V)。这三个向量是用 Embedding 向量与随机初始化的矩阵相乘得到的结果。
- 计算 Self-Attention 的分数值,决定了当我们在某个位置 Encode 一个词时,对输入句子的其他部分的关注程度。分数值的计算方法是 Query 与 Key 做点积。
- 将点积结果除以一个常数(通常是 $\sqrt{d_k}$),然后进行 Softmax 计算。得到的结果是每个词对于当前位置的词的相关性大小。
- 将 Value 和 Softmax 得到的值进行加权求和,得到的结果即是 Self-Attention 在当前节点的值。
在实际应用场景中,为了提高计算速度,通常采用矩阵方式直接计算出 Q、K、V 的矩阵,然后将 Embedding 的值与三个矩阵相乘,得到新矩阵 Q 与 K 相乘,乘以常数,做 Softmax 操作,最后乘上 V 矩阵。这种方法被称为 Scaled Dot-Product Attention。
2.2.3 Multi-Headed Attention
为了增强模型的表达能力,论文引入了 Multi-Headed Attention 机制。简单来说,不仅仅是初始化一组 Q、K、V 的矩阵,而是初始化多组。在 Transformer 中通常使用了 8 组多头注意力。这样可以让模型在不同的表示子空间中关注不同的信息,最后将各个头的输出拼接起来。
2.2.4 Layer Normalization
在 Transformer 中,每一个子层(Self-Attention, Feed Forward Neural Network)之后都会接一个残差连接(Residual Connection),并且有一个 Layer Normalization (LN)。
Normalization 的目的是把输入转化成均值为 0、方差为 1 的数据。在送入激活函数之前进行归一化,可以避免输入数据落在激活函数的饱和区。
- Batch Normalization (BN):在每一层的每一批数据上进行归一化。随着网络加深,数据偏差可能变大,迫使使用较小的学习率。
- Layer Normalization (LN):在每一个样本上计算均值和方差,而不是 BN 那种在批次方向计算。公式如下:
$$LN(x_i) = \alpha \cdot \frac{x_i - \mu_L}{\sqrt{\sigma^2_L + \epsilon}} + \beta$$
2.2.5 Feed Forward Neural Network
Multi-Head Attention 输出 8 个矩阵,需要一种方式将它们合并为一个。通常的做法是将 8 个矩阵拼接在一起,再通过全连接层进行处理,最后得到一个最终的矩阵。前馈神经网络通常包含两个线性变换和一个 ReLU 激活函数。
2.3 Decoder 层结构
Decoder 部分与 Encoder 大同小异,刚开始也是先添加一个位置向量 Positional Encoding。接下来接的是 Masked Multi-Head Attention。这里的 Mask 是 Transformer 的关键技术之一。
2.3.1 Masked Multi-Head Attention
Mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里涉及两种 Mask:Padding Mask 和 Sequence Mask。
- Padding Mask:因为每个批次输入序列长度不一样,需要对输入序列进行对齐,即在较短的序列后面填充 0。这些填充位置没有意义,Attention 机制不应关注它们。具体做法是将这些位置的值加上一个非常大的负数(负无穷),经过 Softmax 后概率接近 0。
- Sequence Mask:为了防止 Decoder 看见未来的信息。在 time_step 为 t 的时刻,解码输出只能依赖于 t 时刻之前的输出。具体做法是产生一个上三角矩阵,上三角的值全为 0(或负无穷),作用在序列上即可隐藏未来信息。
对于 Decoder 的 Self-Attention,attn_mask 通常是 Padding Mask 和 Sequence Mask 的组合。
2.3.2 Output 层
当 Decoder 层全部执行完毕后,需要将得到的向量映射为我们需要的词。通常在结尾再添加一个全连接层和 Softmax 层。如果词典大小为 1w,Softmax 会输出 1w 个词的概率,概率值最大的对应的词即为最终结果。
2.4 动态流程
编码器通过处理输入序列开启工作。顶端编码器的输出转化为包含向量 K(键向量)和 V(值向量)的注意力向量集,这是并行化操作。这些向量将被每个解码器用于自身的'编码 - 解码注意力层',帮助解码器关注输入序列哪些位置合适。
在完成编码阶段后,开始解码阶段。解码阶段的每个步骤都会输出一个输出序列的元素。接下来的步骤重复这个过程,直到到达一个特殊的终止符号,表示 Transformer 的解码器完成了输出。
3. Transformer 为什么需要进行 Multi-head Attention
原论文指出,进行 Multi-head Attention 的原因是将模型分为多个头,形成多个子空间,让模型去关注不同方面的信息,最后再将各个方面的信息综合起来。直观上讲,多次 Attention 综合的结果能够起到增强模型的作用,类比 CNN 中同时使用多个卷积核的作用,多头的注意力有助于网络捕捉到更丰富的特征/信息。
4. Transformer 相比于 RNN/LSTM 的优势
RNN 系列的模型并行计算能力较差。因为 T 时刻的计算依赖 T-1 时刻的隐层计算结果,形成了序列依赖关系。而 Transformer 的特征抽取能力比 RNN 系列模型要好得多,且支持完全的并行计算。
需要注意的是,并不是说 Transformer 就能完全替代 RNN 系列的模型。任何模型都有其适用范围,RNN 系列模型在很多特定任务上仍是首选。熟悉各种模型的内部原理,才能遇到新任务时快速分析选择合适的模型。
5. Transformer 与 Seq2Seq 的比较
Seq2Seq 缺点:Seq2Seq 最大的问题在于将 Encoder 端的所有信息压缩到一个固定长度的向量中,并将其作为 Decoder 端首个隐藏状态的输入。在输入序列比较长的时候,这样做显然会损失 Encoder 端的很多信息,且 Decoder 端无法灵活关注到想要关注的信息。
Transformer 优点:Transformer 对 Seq2Seq 模型的这两点缺点有了实质性的改进,引入了多头交互式 Attention 模块,以及 Self-Attention 模块,让源序列和目标序列首先'自关联'起来。此外,Transformer 并行计算的能力远远超过 Seq2Seq 系列模型。
6. PyTorch 代码实现示例
以下是一个简化的 Transformer 核心组件实现,展示了 Self-Attention 和 Multi-Head Attention 的基本逻辑。
import torch
import torch.nn as nn
import math
class SelfAttention(nn.Module):
def __init__(self, embed_size, heads):
super(SelfAttention, self).__init__()
self.embed_size = embed_size
self.heads = heads
self.head_dim = embed_size // heads
assert self.head_dim * heads == embed_size, "Embed size needs to be divisible by heads"
self.values = nn.Linear(self.head_dim, self.head_dim, bias=False)
self.keys = nn.Linear(self.head_dim, self.head_dim, bias=False)
self.queries = nn.Linear(self.head_dim, self.head_dim, bias=False)
self.fc_out = nn.Linear(heads * self.head_dim, embed_size)
def forward(self, values, keys, query, mask):
N = query.shape[0]
value_len, key_len, query_len = values.shape[1], keys.shape[1], query.shape[1]
values = values.reshape(N, value_len, self.heads, self.head_dim)
keys = keys.reshape(N, key_len, self.heads, self.head_dim)
queries = query.reshape(N, query_len, self.heads, .head_dim)
V = .values(values)
K = .keys(keys)
Q = .queries(queries)
energy = torch.einsum(, [Q, K])
mask :
energy = energy.masked_fill(mask == , ())
attention = torch.softmax(energy / (.embed_size ** (/)), dim=)
out = torch.einsum(, [attention, V]).reshape(
N, query_len, .heads * .head_dim
)
out = .fc_out(out)
out
(nn.Module):
():
(TransformerBlock, ).__init__()
.attention = SelfAttention(embed_size, heads)
.norm1 = nn.LayerNorm(embed_size)
.norm2 = nn.LayerNorm(embed_size)
.feed_forward = nn.Sequential(
nn.Linear(embed_size, forward_expansion * embed_size),
nn.ReLU(),
nn.Linear(forward_expansion * embed_size, embed_size)
)
.dropout = nn.Dropout(dropout)
():
attention = .attention(value, key, query, mask)
x = .dropout(.norm1(attention + query))
forward = .feed_forward(x)
out = .dropout(.norm2(forward + x))
out
以上代码展示了如何构建基础的 Self-Attention 模块和前馈网络块。实际应用中,还需结合 Positional Encoding 和完整的 Encoder-Decoder 堆叠结构。