系列文章目录
这里写目录标题
前言
下文是Transformer论文解读
一、Abstract
1.1 摘要说明
摘要主要说明当时主流的序列翻译模型都是通过解码器和编码器的方式搭建复杂的循环神经网络或者卷积神经网络,作者团队尝试提出了一种自注意力机制的模型Transformer,该模型主要有两点优势,一是可以并行计算,提高模型计算效率;二是,相较于当时的模型可以有效减少训练时间。
1.2 解码器和编码器架构
机器翻译是序列转换模型的一个核心问题,其输入和输出都是长度可变的序列。 为了处理这种类型的输入和输出,我们可以设计一个包含两个主要组件的架构:第一个组件是一个编码器(encoder):它接受一个长度可变的序列作为输入,并将其转换为具有固定形状的编码状态。 第二个组件是解码器(decoder):它将固定形状的编码状态映射到长度可变的序列。 这被称为编码器-解码器(encoder-decoder)架构,解码器-编码器架构适用于输出的结构化信息较多的情况。
二、Introduction
该段落介绍了当前的一些主流循环神经网络RNN和LSTM模型,指出该类网络模型计算
h
t
=
f
(
x
t
−
1
,
h
t
−
1
)
.
h_t = f(x_{t-1},h_{t-1}).
ht=f(xt−1,ht−1). 但这样固定的序列转录方式导致无法进行并行计算。同时受限于训练显存的限制,想要大批量地训练较长的序列文本是十分困难的。
在当时近期的一些工作虽然通过一些调参或者模型优化的一系列方法,取得了不错的进展,改善了计算效率,但是最根本的序列计算问题仍然存在,比如在长序列输入文本,模型提取依赖关系受序列长度影响较大。
所以作者提出了一种基于注意力机制的模型:Transformer。它可以允许对输入或输出序列中任意距离的依赖关系进行建模,也就是学习能力强,可以学习到各个词元token之间的依赖关系,且不受序列距离影响。
三、Background
该部分以以减少序列计算的问题为目标,同时期的两个基于卷积的研究工作ByteNet和ConvS2S,计算方式很复杂,但是Transformer将其简化一个固定大小的操作数,同时尽管平均注意力机制造成了分辨率下降的问题,但是在下文通过多有注意力机制可以很好地解决该问题。
同样这种端到端(解码器和编码器)的循环注意力机制在翻译和问答等任务中都表现良好。接下来开始进行详细的模型介绍和架构说明。
四、 Model Architecture
4.1 Transformer模型总览
上图是论文中 Transformer 的内部结构图,左侧为 Encoder block,右侧为 Decoder block。红色圈中的部分为 Multi-Head Attention,是由多个 Self-Attention组成的,可以看到 Encoder block 包含一个 Multi-Head Attention,而 Decoder block 包含两个 Multi-Head Attention (其中有一个用到 Masked)。Multi-Head Attention 上方还包括一个 Add & Norm 层,Add 表示残差连接 (Residual Connection) 用于防止网络退化,Norm 表示 Layer Normalization,用于对每一层的激活值进行归一化。
首先来看Encoder部分(左半部分),它是由N层方框里面的内容堆叠起来的。对于每一层来说,都由两部分构成:一部分是multi-head self-attention机制,另一部分是一个简单的全连接前馈网络。在每一部分上,都使用残差和
l
a
y
e
r
n
o
r
m
a
l
i
z
a
t
i
o
n
layer normalization
layernormalization 来进行处理。Encoder输出的编码信息作为Decoder的键和值(Key和Value)。模型的隐层单元数
d
m
o
d
e
l
d_{model}
dmodel = 512,
d
m
o
d
e
l
d_{model}
dmodel 等同于下文代码中的num_hiddens。
论文中,这样的方框有6个,即
N
N
N = 6 ,如上图所绘制。这一部分只是对模型框架大致的进行一个介绍,接下来进行每个组成部为详细的介绍说明。
4.2 注意力机制代码解读
一般来说,当查询和键是不同长度的矢量时,我们可以使用加性注意力作为评分函数。给定查询
q
∈
R
q
\mathbf{q} \in \mathbb{R}^q
q∈Rq和键
k
∈
R
k
\mathbf{k} \in \mathbb{R}^k
k∈Rk,加性注意力(additive attention)的评分函数为
a
(
q
,
k
)
=
w
v
⊤
tanh
(
W
q
q
+
W
k
k
)
∈
R
,
a(\mathbf q, \mathbf k) = \mathbf w_v^\top \text{tanh}(\mathbf W_q\mathbf q + \mathbf W_k \mathbf k) \in \mathbb{R},
a(q,k)=wv⊤tanh(Wqq+Wkk)∈R,
class AdditiveAttention(nn.Module):
"""加性注意力"""
def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs):
super(AdditiveAttention, self).__init__(**kwargs)
self.W_k = nn.Linear(key_size, num_hiddens, bias=False)
self.W_q = nn.Linear(query_size, num_hiddens, bias=False)
self.w_v = nn.Linear(num_hiddens, 1, bias=False)
self.dropout = nn.Dropout(dropout)
def forward(self, queries, keys, values, valid_lens):
queries, keys = self.W_q(queries), self.W_k(keys)
# 使用广播方式进行求和
features = queries.unsqueeze(2) + keys.unsqueeze(1)
# 在维度扩展后,
# queries的形状:(batch_size,查询的个数/步数,1,num_hidden)
# key的形状:(batch_size,1,“键-值”对的个数,num_hiddens)
features = torch.tanh(features)
# self.w_v仅有一个输出,因此从形状中移除最后那个维度。
# scores的形状:(batch_size,查询的个数,“键-值”对的个数)
scores = self.w_v(features).squeeze(-1)
self.attention_weights = masked_softmax(scores, valid_lens)
# values的形状:(batch_size,“键-值”对的个数,值的维度)
return torch.bmm(self.dropout(self.attention_weights), values)
我们用一个小例子来[演示上面的AdditiveAttention
类],其中查询、键和值的形状为(批量大小,步数或词元序列长度,特征大小),实际输出为
(
2
,
1
,
20
)
(2,1,20)
(2,1,20)、
(
2
,
10
,
2
)
(2,10,2)
(2,10,2)和
(
2
,
10
,
4
)
(2,10,4)
(2,10,4)。注意力汇聚输出的形状为(批量大小,查询的步数,值的维度)。
下面是点击注意力机制:
使用点积可以得到计算效率更高的评分函数,但是点积操作要求查询和键具有相同的长度
d
d
d。假设查询和键的所有元素都是独立的随机变量,并且都满足零均值和单位方差,那么两个向量的点积的均值为
0
0
0,方差为
d
d
d。为确保无论向量长度如何,点积的方差在不考虑向量长度的情况下仍然是
1
1
1,我们将点积除以
d
\sqrt{d}
d,则缩放点积注意力(scaled dot-product attention)评分函数为:
a ( Q , K ) = Q ⊤ K / d k . a(\mathbf Q, \mathbf K) = \mathbf{Q}^\top \mathbf{K} /\sqrt{d_k}. a(Q,K)=Q⊤K/dk.
在实践中,我们通常从小批量的角度来考虑提高效率,例如基于
n
n
n个查询和
m
m
m个键-值对计算注意力,其中查询和键的长度为
d
d
d,值的长度为
v
v
v。查询
Q
∈
R
n
×
d
\mathbf Q\in\mathbb R^{n\times d}
Q∈Rn×d、键
K
∈
R
m
×
d
\mathbf K\in\mathbb R^{m\times d}
K∈Rm×d和值
V
∈
R
m
×
v
\mathbf V\in\mathbb R^{m\times v}
V∈Rm×v的缩放点积注意力是:
在下面的缩放点积注意力的实现中,我们使用了暂退法进行模型正则化。
class DotProductAttention(nn.Module):
"""缩放点积注意力"""
def __init__(self, dropout, **kwargs):
super(DotProductAttention, self).__init__(**kwargs)
self.dropout = nn.Dropout(dropout)
# queries的形状:(batch_size,查询的个数,d)
# keys的形状:(batch_size,“键-值”对的个数,d)
# values的形状:(batch_size,“键-值”对的个数,值的维度)
# valid_lens的形状:(batch_size,)或者(batch_size,查询的个数)
def forward(self, queries, keys, values, valid_lens=None):
d = queries.shape[-1]
# 设置transpose_b=True为了交换keys的最后两个维度
scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d)
self.attention_weights = masked_softmax(scores, valid_lens)
return torch.bmm(self.dropout(self.attention_weights), values)
其中点积注意力机制计算方面更具有效率。
因为点积需要对应维度相同,
d
k
d_k
dk对于Q和K是相同的,也就是q的特征维度等于k的特征维度。
加性注意力因为q与k特征维度不等,所以用Wq和Wk将他们的维度放缩到同一个维度,然后使用广播机制让同一个q与每个k相加,然后使用共享的Wv来将他们点积得到一个标量,也就是相关系数,这个系数是可以进行训练学习的。
也就是说,缩放点积直接通过点积得到注意力分数,没有任何的学习参数。而加性注意力是通过可学习参数先进行放缩,然后进行q与k的特征相加。区别在于:①是否可学习 ②特征进行点积还是特征进行相加。
4.3 Transformer中Q、K、V参数解读
有一种解释说,Attention中的Query,Key,Value的概念源于信息检索系统。举个简单的例子,当你在淘宝搜索某件商品时,你在搜索栏中输入的信息为Query,然后系统根据Query为你匹配Key,根据Query和Key的相似度得到匹配内容。继续以“搜索灰色男士毛衣”举例:
Query(to match others):输入信息,具有引导作用,包含我们需要哪些信息这个想法(我在搜索栏中输入:灰色男士毛衣,我希望系统返回一些相似的商品给我)Key(to be matched):内容信息,表示其他待匹配的商品(当系统收到灰色、男士毛衣这样的信息后,去匹配数据库中的所有商品)Attention(Q, K):表示Query和Key的匹配程度
(系统中商品(Key)很多,其中符合我的描述(Query)的商品的匹配程度会高一点)Value(information to be extracted):信息本身,V只是单纯表达了输入特征的信息
。
其中Query(查询)、Key(键)和value(值),先计算
Q
⊤
K
\mathbf{Q}^\top \mathbf{K}
Q⊤K,这一步的操作时Q和K计算出来的一个类似协方差(相关性)矩阵的注意力关系矩阵,该系数矩阵的维度是样本输入的token个数(这就是输入的
x
1
,
x
2
.
.
.
x
n
x_1,x_2...x_n
x1,x2...xn的个数
n
n
n),得到出来各个词元的关联依赖程度,最后做归一化处理后再乘以V,将按照查询Q在K中的关系映射到和V有关的语言信息,当然输出的向量的维度也是
d
k
d_k
dk。
这一部分和协方差矩阵的思想相近,可以进行探讨挖坑。
4.4 多头注意力机制
class MultiHeadAttention(nn.Module):
"""多头注意力"""
def __init__(self, key_size, query_size, value_size, num_hiddens,
num_heads, dropout, bias=False, **kwargs):
super(MultiHeadAttention, self).__init__(**kwargs)
self.num_heads = num_heads
self.attention = d2l.DotProductAttention(dropout)
self.W_q = nn.Linear(query_size, num_hiddens, bias=bias)
self.W_k = nn.Linear(key_size, num_hiddens, bias=bias)
self.W_v = nn.Linear(value_size, num_hiddens, bias=bias)
self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=bias)
def forward(self, queries, keys, values, valid_lens):
# queries,keys,values的形状:
# (batch_size,查询或者“键-值”对的个数,num_hiddens)
# valid_lens 的形状:
# (batch_size,)或(batch_size,查询的个数)
# 经过变换后,输出的queries,keys,values 的形状:
# (batch_size*num_heads,查询或者“键-值”对的个数,
# num_hiddens/num_heads)
queries = transpose_qkv(self.W_q(queries), self.num_heads)
keys = transpose_qkv(self.W_k(keys), self.num_heads)
values = transpose_qkv(self.W_v(values), self.num_heads)
if valid_lens is not None:
# 在轴0,将第一项(标量或者矢量)复制num_heads次,
# 然后如此复制第二项,然后诸如此类。
valid_lens = torch.repeat_interleave(
valid_lens, repeats=self.num_heads, dim=0)
# output的形状:(batch_size*num_heads,查询的个数,
# num_hiddens/num_heads)
output = self.attention(queries, keys, values, valid_lens)
# output_concat的形状:(batch_size,查询的个数,num_hiddens)
output_concat = transpose_output(output, self.num_heads)
return self.W_o(output_concat)
def transpose_qkv(X, num_heads):
"""为了多注意力头的并行计算而变换形状"""
# 输入X的形状:(batch_size,查询或者“键-值”对的个数,num_hiddens)
# 输出X的形状:(batch_size,查询或者“键-值”对的个数,num_heads,
# num_hiddens/num_heads)
X = X.reshape(X.shape[0], X.shape[1], num_heads, -1)
# 输出X的形状:(batch_size,num_heads,查询或者“键-值”对的个数,
# num_hiddens/num_heads)
X = X.permute(0, 2, 1, 3)
# 最终输出的形状:(batch_size*num_heads,查询或者“键-值”对的个数,
# num_hiddens/num_heads)
return X.reshape(-1, X.shape[2], X.shape[3])
#@save
def transpose_output(X, ):
"""逆转transpose_qkv函数的操作"""
X = X.reshape(-1, num_heads, X.shape[1], X.shape[2])
X = X.permute(0, 2, 1, 3)
return X.reshape(X.shape[0], X.shape[1], -1)
这段代码定义了一个多头注意力(Multi-Head Attention)模块。这个模块使用了多个注意力头以并行的方式处理输入序列的查询(queries)、键(keys)和值(values)信息,并输出一个整合后的注意力表示。下面是对这个模块的简要介绍:
构造函数初始化了多头注意力模块。它接受以下参数 :
key_size、query_size 和 value_size:分别表示键、查询和值的特征维度。
num_hiddens:每个多头注意力头的隐藏单元数,也就是上文中的
d
k
d_k
dk。
num_heads:多头注意力的头数。
dropout:Dropout 概率,用于在注意力计算中引入随机性。
在初始化过程中,该模块创建了用于进行线性变换的权重矩阵(W_q、W_k、W_v 和 W_o),以及一个内部的DotProductAttention 实例(self.attention)。
queries、keys 和 values 表示输入的查询、键和值,形状为 (batch_size, 查询/键/值的个数, 特征维度)。
valid_lens 是一个可选参数,表示每个查询的有效长度,形状为 (batch_size,) 或 (batch_size, 查询的个数),训练的过程中用于遮蔽填充标记。
首先,通过线性变换将输入的查询、键和值投影到隐藏空间层,然后使用 transpose_qkv 函数将它们变换成 (batch_size*num_heads, 查询/键/值的个数, num_hiddens/num_heads) 的形状。
接着,调用 self.attention 实例,通过多头注意力机制计算输出。最后,使用 transpose_output 函数将多头注意力的输出变换回 (batch_size, 查询的个数, num_hiddens) 的形状,并通过线性变换 self.W_o 输出最终的多头注意力结果。
多头注意力代码里是对每个单词的特征维度拆开来补充多头注意力数量,相当于在批量大小上乘注意力头数h。所以q,k,v并不是简单的复制h份,而是将输入特征拆分为h份,特征维属列空间,不同注意力机制处理输入的特征子空间。
或者说,原来是Q与K直接点积,注意力直接体现在点积结果,缺点是在完整特征维度考虑相似性,没有考虑局部特征相似性;而现在,将Q与K从特征维拆成了h份,每一份单独点积计算注意力,考虑了局部特征相似程度,最后再concat到一起,便于与W_o相乘,从而为每一个局部特征分配关注权重,这样,不同的查询对应了不同局部特征的关注程度,相比于加权平均来说提升了一个维度,即在局部特征上分配权重求和,这样得到的结果更复杂,也体现了局部特征的重要性(图像识别中感受野过大不利于局部特征提取是一个意思)。
这个模块的主要目的是引入并行性,通过多头注意力机制,能够更充分地捕捉输入序列中不同位置的信息,从而提高模型在处理序列数据时的表现。
4.5 位置编码和词嵌入
这一部分进行文本的词嵌入和位置编码的计算
对于离散的文本序列信息,不能直接作为输入。经常用的一种方法是将文本信息统计出来个数,然后进行one-hot编码,但是这样的方法很明显很明显,计算简单,稀疏矩阵做矩阵计算的时候,只需要把1对应位置的数相乘求和就行,计算方便快捷、表达能力强。
然而最明显的缺点:过于稀疏时,过度占用资源。
比如:其实我们这篇文章,虽然100W字,但是其实我们整合起来,有99W字是重复的,只有1W字是完全不重复的。
然后我们这时候就利用Embedding层,做矩阵乘法用来降维。
同样Transformer模型在多头注意力和并行训练的过程中,没有每个样本输入的先后顺序,就缺失了位置信息,因此改论文的作者团队提出了一个位置编码的方式,利用模型足够强的学习能力,能够略微的学习到样本间的位置先后信息,但同样这样的方式存在不足,在文章就发现Transformer模型的位置信息,无法很好的提取时序信息。
Are Transformers Effective for Time Series Forecasting?
Transformer是否真的适合解决时序预测问题
下文详细进行介绍位置编码(Positional Encoding)为什么进行这样设置以及进行探讨可能有用的地方。
class PositionalEncoding(nn.Module):
"""位置编码"""
def __init__(self, num_hiddens, dropout, max_len=1000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(dropout)
# 创建一个足够长的P
self.P = torch.zeros((1, max_len, num_hiddens))
X = torch.arange(max_len, dtype=torch.float32).reshape(
-1, 1) / torch.pow(10000, torch.arange(
0, num_hiddens, 2, dtype=torch.float32) / num_hiddens)
self.P[:, :, 0::2] = torch.sin(X)
self.P[:, :, 1::2] = torch.cos(X)
def forward(self, X):
X = X + self.P[:, :X.shape[1], :].to(X.device)
return self.dropout(X)
这是一个实现位置编码(Positional Encoding)的PyTorch模块。在自然语言处理中,位置编码用于为序列模型(例如Transformer)提供关于输入序列中词语或标记位置的信息。在该模块中,PositionalEncoding 类接受三个参数:num_hiddens(隐藏单元的数量),dropout(Dropout概率),和 max_len(输入序列的最大长度,默认为1000)。
在初始化函数中,该类使用正弦和余弦函数来创建位置编码矩阵 P。该矩阵的维度为 (1, max_len, num_hiddens),其中 max_len 表示序列的最大长度,num_hiddens 表示隐藏单元的数量。通过正弦和余弦函数,P 矩阵为输入序列的每个位置分配了一个唯一的编码。这个编码矩阵 P 被加到输入序列 X 上,从而为输入序列的每个位置增加了一个特定的编码信息。
在前向传播函数中,输入序列 X 被加上位置编码矩阵 P 的前 X.shape[1] 列(取决于输入序列的实际长度),然后通过Dropout层进行随机失活处理,最终返回加了位置编码的序列。这样,模型就能够区分输入序列中不同位置的词语或标记,从而更好地捕捉序列的结构信息。
关于位置编码的中为什么可以提取出来位置信息和三角函数来进行表示,可以见文章使用Positional Encoding的作用和原因
4.6 模型输入步骤
前置知识请见下面的文章,关于Seq to Seq模型同样适用于本次的模型训练。Seq2Seq里发生了啥?——图解李沐《动手学深度强化学习 v2-9.7》
4.6.1 模型训练
假设有一个样本 ”我爱你“ 对应的.正确的标注Ground Truth 为:“I love you”这样一个长度为3(可以划分为3个token)的句子。
在训练过程中,某训练样本是 我爱你 这个句子,经过embeding后得到的一个大小为(num_step,num_hiddens)的向量,再和位置编码相加在一起,作为Encoder层的输入。
该部分不需要将 我爱你 按照每个词进行顺序输入,即可以直接将整个样本进行输入给Encoder进行训练。
得到该样本中的各个词元间的依赖关系后,输出的语义信息传递给Decoder,作为Decoder中间层注意力机制,作为Mutil-Head Attention中的键值对关系(Key和Value)。
训练:在训练过程中,Decoder输入的是Ground Truth,输出的也应该是要翻译的目标文本,也是Ground Truth 为:“I love you”, 也就是在预测时间步 S t e p 2 Step_2 Step2的时候,遮盖住 S t e p 2 Step_2 Step2真实的输出,和当前的预测值进行交叉熵损失回归进行优化,同时下一个时间步 S t e p 3 Step_3 Step3将
S t e p 1 Step_1 Step1:给解码器模块输入“<start>” 和 编码器的输出结果,解码器目标输出“I” 和 预测值进行 交叉熵损失回归
S t e p 2 Step_2 Step2:给解码器模块输入“<start> I” 和 编码器的输出结果,解码器输出“Iove”和 预测值进行 交叉熵损失回归
S t e p 3 Step_3 Step3:给解码器模块输入“<start> I love” 和 编码器的输出结果,解码器输出“you”和 预测值进行 交叉熵损失回归
S t e p 4 Step_4 Step4:给解码器模块输入“<start> I love you” 和 编码器的输出结果,解码器输出“”,至此完成。
训练时:第i个decoder的输入 = encoder输出 + ground truth embeding
这就是一个“完美”解码器按时序处理的基本流程。之所以说完美,是因为解码器每次都成功的预测出了正确的单词,如果编码器在某一轮预测错了,那么给下一轮解码器的输入中就包含了错误的信息,然后继续解码。
但在训练时这样做效率太低了,所以我们会将target一次性给到Transformer(当然,你也可以按照推理过程做),但是在时间步Decoder要进行输入整体右移操作,并行为了方便,可以采用掩码Mask的操作,训练时因为知道ground truth embeding,相当于知道正确答案,网络可以一次训练完成,并行输出多个推理预测结果,
一口气进行参数的更新和优化,并且不要让前面的字具备后面字的上下文信息。
Transformer之所以能支持Decoder部分并行化训练,是基于以下两个关键点:
1.teacher force。对于teacher force,在其他seq2seq模型中也有应用。它是指在每一轮预测时,不使用上一轮预测的输出,而强制使用正确的单词。
还以上面这个例子来说,第二轮时,给解码器模块输入“ I” 和 编码器的输出结果,解码器没有正确预测出“Iove”,而是得到了“want”。如果没有采用teacher force,在第三轮时,解码器模块输入的就是“ I want”。
如果采用了 teacher force,第三轮时,解码器模块输入的仍然是“ I love”。通过这样的方法可以有效的避免因中间预测错误而对后续序列的预测,从而加快训练速度。
而Transformer采用这个方法,为并行化训练提供了可能,因为每个时刻的输入不再依赖上一时刻的输出,而是依赖正确的样本,而正确的样本在训练集中已经全量提供了。值得注意的一点是:Decoder的并行化仅在训练阶段,在测试阶段,因为我们没有正确的目标语句,t时刻的输入必然依赖t-1时刻的输出,这时跟之前的seq2seq就没什么区别了
。
2.masked self attention。有teacher force,所以可以通过掩码的方式,并行输入经过不同掩码的同一样本,输出多个位置的预测,然后输出每个位置的误差
,然后进行见以下文章对masked self attention的详细解读。
4.6.2 模型预测
预测时Encoder和训练时操作相近,但是decoder的操作中输入是上一个decoder的输出,最开始的时候输入时<start>,其余的位置填充0。
预测时:第i个decoder的输入 = encoder输出 + 第(i-1)个decoder输出
预测时,首先输入start,输出预测的第一个单词,然后start和新单词组成新的query,再输入decoder来预测下一个单词,循环往复直至end.
总结:Transformer推理时是一个一个词预测,而训练时会把所有的结果一次性给到Transformer,但效果等同于一个一个词给,而之所以可以达到该效果,就是因为对tgt进行了掩码,防止其看到后面的信息,也就是不要让前面的字具备后面字的上下文信息。
4.7 模型代码整体流程
五、训练技巧
token是经过embedding,词嵌入特征的特称维度 d k = 512 d_k=512 dk=512,而W被 Xavier初始化,其方差和嵌入维数成反比。
也就是嵌入维数越大,方差越小,权重越集中于0,后续再和positional encoding相加,词嵌入特征由于绝对值太小,可能被位置信息掩盖,难以影响模型后续计算。因此需要放大W的方差,最直接的方法就是乘以维度的平方根。
六、参考引用
[1]The Illustrated Transformer . By Jay Alammar