论文摘要
主流的序列转换模型基于复杂的循环神经网络或卷积神经网络,这些网络包括编码器和解码器。表现最好的模型还通过注意力机制将编码器和解码器连接起来。我们提出了一种新的简单网络架构,Transformer,仅依赖注意力机制,完全去除了循环和卷积。在两个机器翻译任务上的实验表明,这些模型在质量上表现更优,同时具有更好的并行性,并且需要显著更少的训练时间。我们的模型在WMT 2014英语到德语翻译任务上取得了28.4的BLEU分数,相比现有的最佳结果(包括集成模型)提高了超过2个BLEU分。在WMT 2014英语到法语翻译任务中,我们的模型经过8个GPU、3.5天的训练后,建立了单一模型新的最先进BLEU分数41.8,这仅为文献中最佳模型训练成本的一小部分。我们还通过将Transformer成功应用于英语成分解析任务,展示了其在大数据和有限数据条件下的良好泛化能力。
论文链接:[1706.03762] Attention Is All You Need (arxiv.org)
前言
Transformer通过引入自注意力机制(Self-Attention Mechanism)和去除循环神经网络(RNN)及卷积神经网络(CNN),解决了传统序列模型在长距离依赖和并行计算上的局限性。
Embedding
因为计算机无法对一个单词或者汉字直接进行处理,所以就需要把转化成的token变成可以操作的向量,这个过程就是Embeding。
比如“我爱吃苹果”这句话经过Embeding操作之后可以变成这个样子
每一个token都被映射到一个n维空间中,关系越相近在这个n维空间中的聚集就越近。
在学习的过程中,就是通过token之间的关系来更新token在这个n维空间中的位置。
Self Attention
Q(Query)、K(Key)、V(Value)
对于每一个token都会生成一个初始的向量,每一个向量都会通过线性变换生成三个向量,也就是Q、K、V。
假设有一个二维的token向量的值为[2, 3],下面就是Q,K,V向量的生成过程。
线性变换生成Q:将x与权重矩阵相册得到Q
Q = [2, 3] * [[1, 0], [0, 1]] = [2, 3]
线性变换生成K:将x与权重矩阵相册得到K
K = [2, 3] * [[2, 1], [1, 2]] = [7, 8]
V同上,其中Q、K和V权重矩阵是在训练过程中通过优化算法自动学习得到的。
Q向量:用于向其他的token进行查询的向量。
K向量:用于对查询进行应答的向量。
V向量:用于更新Embedding的向量。
在训练的过程中,每一个token的Q向量都会分别与其他token的K向量做点积运算(相似性匹配)。
一个token的Q分别与所有的K做点积运算后,可以组成一个相似度向量,这个向量接下来会经过softmax处理得到一个百分比向量,这个百分比分别与所有的token的V向量对应相乘,就可以得到更新后的新的token向量表示。
MultiHead Attention
在MultiHead Attention中,一个token的embedding被转化成多组完全一样的Q,K和V。在本文中使用的是8组。
具体如下所示:
这样就能并行执行多个Attention操作
对于一个句子的多个token,在运算时会分别将每一token的Q、K、V向量组合到一起,组成三个矩阵方便计算,每一行就代表一个token的对应值。
下面是矩阵形式的计算公式:
MultiHead
在训练的过程中不会直接将一整个embedding进行运算,而是拆分成多个Head,这就是叫做MultiHead的原因。
这样分别进行操作的Head部分最后都会产生一个Attention(Q、K、V),这几个Attention会经过Concat操作处理,得到最终的Attention。
这样的操作应该是为了适应多显卡训练而设计的。
def forward(self, q, k, v, mask):
query = self.w_q(q) # (batch, seq_len, d_model) --> (batch, seq_len, d_model)
key = self.w_k(k) # (batch, seq_len, d_model) --> (batch, seq_len, d_model)
value = self.w_v(v) # (batch, seq_len, d_model) --> (batch, seq_len, d_model)
# (batch, seq_len, d_model) --> (batch, seq_len, h, d_k) --> (batch, h, seq_len, d_k)
query = query.view(query.shape[0], query.shape[1], self.h, self.d_k).transpose(1, 2)
key = key.view(key.shape[0], key.shape[1], self.h, self.d_k).transpose(1, 2)
value = value.view(value.shape[0], value.shape[1], self.h, self.d_k).transpose(1, 2)
# Calculate attention
x, self.attention_scores = MultiHeadAttentionBlock.attention(query, key, value, mask, self.dropout)
# Combine all the heads together
# (batch, h, seq_len, d_k) --> (batch, seq_len, h, d_k) --> (batch, seq_len, d_model)
x = x.transpose(1, 2).contiguous().view(x.shape[0], -1, self.h * self.d_k)
# Multiply by Wo
# (batch, seq_len, d_model) --> (batch, seq_len, d_model)
return self.w_o(x)
ResidualConnection
在每次经过MultiHead Attention操作之后会进行归一化,然后就使用了ResudialConnection。
class ResidualConnection(nn.Module):
def __init__(self, features: int, dropout: float) -> None:
super().__init__()
self.dropout = nn.Dropout(dropout)
self.norm = LayerNormalization(features)
def forward(self, x, sublayer):
return x + self.dropout(sublayer(self.norm(x)))
Feed Forward
Feed Forward由两个线性层,一个dropout组成,它的功能是增强模型的表达能力和复杂性,是模型能更好地捕捉数据中地复杂特征。
代码如下:
class FeedForwardBlock(nn.Module):
def __init__(self, d_model: int, d_ff: int, dropout: float) -> None:
super().__init__()
self.linear_1 = nn.Linear(d_model, d_ff) # w1 and b1
self.dropout = nn.Dropout(dropout)
self.linear_2 = nn.Linear(d_ff, d_model) # w2 and b2
def forward(self, x):
# (batch, seq_len, d_model) --> (batch, seq_len, d_ff) --> (batch, seq_len, d_model)
return self.linear_2(self.dropout(torch.relu(self.linear_1(x))))
整体架构
这就是Transformer的整体架构,左侧是Encoder部分,右侧是Decoder部分。
我们都知道如果改变词语的位置,可能意思会发生很大的改变,例如“他给了我一份礼物”和“我给了他一份礼物”的意思完全不一样,但是前面的Attention机制并没有注意到位置上的信息,所以,从架构图中可以看到,Positional Encoding做的就是给Embedding加上位置信息。
Positional Encoding
在文中使用了sin和cos函数来作为位置编码。
最后这个计算出来的位置编码与embedding本身相加作为输出。