论文地址:attention is all you need
论文代码:https://paperswithcode.com/paper/attention-is-all-you-need
目录
3.1、Scaled Dot-Product Attention
4、Position-wise Feed-Forward Networks
1、Attention is all you need?
1.1、摘要
当时的序列转换模型大多是基于循环或卷积的,包括编码器和解码器。通过注意力机制连接编码器和解码器取得最好的性能。本文提出一种相对简单的Transformer,完全基于注意机制,完全不需要循环和卷积。(完全依赖于一种注意机制来表示输入和输出之间的全局依赖关系)并且在两个机器翻译任务上取得不错的效果,同时支持并行、所需的训练时间显著降低。经过实验证明Transformer还可以推广到其他任务(目前已经用于computer vision等视觉任务中了VIT)。
1.2、自注意力机制
自注意力也被称为内部注意,就是将单个序列的不同位置联系起来,然后计算序列的表示。
绝大多数的序列转换模型都有Encoder-Decoder结构。其中编码器(encoder)将符号表示(x1,…,xn)的输入序列映射到z=(z1,…,zn)的连续表示序列。而对于z,解码器(decoder)一次生成一个元素的符号输出序列(y1,…,ym)。在每一步中,模型都是自回归的形式,在生成下一步时,使用之前生成的符号作为额外输入。
Transformer采用上述架构,使用堆叠的自注意力和逐点、全连接的编码器和解码器层,如下图1所示:
2、Encoder and Decoder
2.1、Encoder
编码器由N=6个相同层组成。每层有两个子层。第一个是多头自我注意机制(multi-head attention),第二个是全连接的前馈网络(feed forward)。然后在两个子层的每个层中采用残差连接(resnet)和层归一化(Norm)。
每个子层的输出是LayerNorm(x+sublayer(x)),其中sublayer(x)是由子层本身实现的函数。为了更好地使用残差连接,模型中的所有子层以及嵌入层都采用维度为512的输出。
这里有个问题:残差网络的作用?(这里可以看一下ResNet论文的介绍)
一般认为随机神经网络层数加深,神经网络的效果应该会更好,但是实际的情况却不是这样!
残差网络可以有效解决梯度消失的问题,实际上学者们已经提出很多方法,比如利用ReLU,LeakyRelu去代替Sigmoid作为激活函数。ResNet是2015年由Kaiming He提出的一种看似简单但是极为有效的网络结构,在单纯的前向传播基础上深度残差网络增加了跃层连接(Skip Connection)。实验表明ResNet的残差结构可以有效地改善“深度”网络的性能。
从上图可以简单看一下为什么残差结构可以解决梯度消失问题:
假设网络输入为 x,从 A到 D 经历两次前向传播以及一次Skip Connection,根据后向传播的链式法则
其中
此时,即使在A->B->C的后向传播中出现梯度衰减的情况,D处的梯度依然能够直接传递到A,也即是实现了梯度的跨层传播。
此处参考知乎:残差网络 - 搜索结果 - 知乎
2.2、Decoder
解码器也由N=6个相同层组成。除了每个编码器层中的两个子层之外,解码器还插入第三个子层,该子层对编码器堆栈的输出执行多头注意。与编码器类似,在每个子层周围使用残差连接,然后采用层归一化。
同时修改了解码器堆栈中的自注意子层(即图一中的Masked Multi-head Attention),以防止位置涉及后续位置。这种masking形式再加上输出嵌入偏置(一个位置),确保位置 i 的预测只能依赖于位置小于 i 的已知输出。
3、Attention
Attention可以描述为将query和一组key-value对映射到输出,其中query、keys、values和output都是向量。output为values的加权和,其中分配给每个value的权重则由query与对应的key的兼容函数计算得到。
常用的注意力机制有:加法注意力和点积注意力(Dot-Product)。点积注意力的计算速度更快,空间效率更高。
对于较小的dk值,这两种机制的表现类似,但是对于较大的dk值,加法注意力优于点积注意力。对于较大的dk值,点积的增长幅度较大,从而将softmax函数推到梯度非常小的区域。因此本文为了消除这个影响,将点积缩放。
3.1、Scaled Dot-Product Attention
如下图所示:输入包括维度的查询Q和键K,以及维度
的值V。采用所有键K计算查询Q的点积,然后每个键除以
,并应用softmax函数以获得值V的权重。
通常计算一组查询的Attention,然后将Q、K、V都打包成矩阵,计算公式(1)为:
参考知乎的scaled Dot-Product Attention,这两张图可以很直观的解释上面的公式(1):
attention具体计算过程:拿每个词a的query和所有词b的key进行缩放的点积运算,得到的值为b词value向量对应的权重,然后对权重进行softmax转换成概率。将各个词b的权重乘以其value向量,加权求和即得到每个词a的attention。
还有就是为什么要对点积进行缩放?
当维度dk很大时,点积结果会很大,会导致softmax的梯度很小。为了减轻这个影响,对点积进行缩放。
这里参考知乎: 为什么注意力机制中选择正弦函数 - 搜索结果 - 知乎
3.2、Multi-Head Attention
本文使用不同的、学习到的线性映射将Q、K和V分别线性映射到dk、dk和dv维度,而不是使用维度的K、V和Q执行单个注意函数。然后,我们在每个查询Q、键K和值V的投影版本上并行执行注意力,生成dv维输出值。然后再将输出值concat起来,并再通过一次线性映射,从而得到最终值,如下图所示:
多头注意力使模型能够关注来自不同位置的不同表征子空间的信息。由于只有一个注意力头,平均值会抑制这一点;
本文中采用h=8的并行注意力层(head),对于其中每一个注意力层采用维度。由于每个head的维数降低,总的计算成本与所有维度单头部注意力的计算成本相似。
Transformer这篇文章中主要采用了三种多头注意力:
1、在“encoder-decoder attention”层中,查询Q来自前一个解码器层,键K和值V来自编码器的输出。使得解码器中的每个位置都可以覆盖输入序列中的所有位置。这在sequence-to-sequence模型中模拟了典型的编码器-解码器注意力机制。
2、编码器包含self-attention,并且所有键、值和查询都来自同一个位置。编码器中的每个位置都可以处理编码器前一层中的所有位置。
3、为了防止解码器中的信息自动回归。通过设置Mask(设置为−∞) softmax输入中与非法连接对应的所有值。(如scaled dot-product attention中的Mask(opt))
4、Position-wise Feed-Forward Networks
除了注意力子层之外,编码器和解码器中的每一层都包含一个全连接的前馈网络(feed forward network),分别相同地应用于每个位置。其中前馈网络由两个线性变换,中间有一个ReLU激活组成。
FFN(x) = max(0, xW1 + b1)W2 + b2
虽然线性变换在不同的位置上是相同的,但它们在不同的层之间使用不同的参数。
5、Embeddings and Softmax
本文使用学习的嵌入将输入标记和输出标记转换为维度的向量。然后使用学习的线性变换和softmax函数将解码器输出转换为预测的下一个token概率。在两个嵌入层和预softmax线性变换之间共享相同的权重矩阵。在嵌入层中将这些权重乘以
。(这里可以参考上面公式(1)attention的计算公式)
6、Positional Encoding
因为Transformer没有采用循环和卷积,为了使模型能够学到序列的顺序,增加了序列中标记的相对或绝对位置信息;于是在编码器和解码器堆栈底部的输入嵌入中添加“位置编码”。位置编码与嵌入具有相同的维度,因此可以将两者相加。位置编码同样有很多选择,既有学习的,也有固定的。
本文采用不同频率的正弦和余弦函数:
其中pos表示位置,i表示维度,表示位置编码的每个维度对应一个正弦波;波长从2π到10000·2π的几何级数。它可以让模型通过相对位置轻松学习,并且对于任何固定偏移量k,PEpos+k可以表示为PEpos的线性函数。
同时文中还对比了学习的位置嵌入和位置编码的效果,基本差不多。最终选择正弦版本是因为它可以让模型推断出比训练期间遇到的序列长度更长的序列。
这里为啥会采用正弦函数?
词序信息的表示方法很丰富,但究其根本,需要的是对不同维度的不同位置生成合理的数值表示。这里的合理,理解为不同位置的同一维度的位置向量之间,含有相对位置信息,而相对位置信息可以通过函数的周期性实现。
Transformer 使用的解决方案是三角函数实现相对位置信息的表示。原回答已经对论文中的公式做了细致的解释,实质上就是对不同维度使用不同频率的正/余弦公式进而生成不同位置的高维位置向量。
7、模型多样性测试
为了评估Transformer不同部分的重要性,本文以不同的方式改变了基础模型,在newstest2013数据集上测量了英语到德语翻译的性能变化。结果如下表所示:
在表3(A)行中,改变了注意力头的数量以及键和值的维度,保持计算量不变。虽然单头注意力比最佳设置差0.9个BLEU,但如果head过多,质量也会下降。
在表3(B)行中,减少注意键大小dk会影响模型质量。
从(C)行和(D)行中可以看出模型越大越好,而且采用dropout可以有助于避免模型过拟合。
在第(E)行中,我们将正弦位置编码替换为学习的位置嵌入,可以看到效果基本没有发生变化。
8、结论
本文提出的Transformer,是第一个完全基于注意力机制的序列转换模型,采用多头注意力机制取代了编码器-解码器架构中最常用的循环层。
并且在翻译任务上,Transformer比基于循环和卷积的模型的训练速度明显提升;
同时展望了在未来可以将Transformer不仅局限于文本,在图像、音频和视频中都能采用。
确实如此!现在transformer在CV领域也取得了不错的效果!感兴趣的可以看一下最近的paper,但是工业上目前没有接触到transformer的落地!
注:这里对于模型的训练部分没有描述,注意针对transformer的模型进行陈述,具体可以看一下原文!
代码部分:
# code by Tae Hwan Jung @graykode
import torch
import torch.nn as nn
import torch.optim as optim
def make_batch():
input_batch = []
target_batch = []
for sen in sentences:
word = sen.split() # space tokenizer
input = [word_dict[n] for n in word[:-1]] # create (1~n-1) as input
target = word_dict[word[-1]] # create (n) as target, We usually call this 'casual language model'
input_batch.append(input)
target_batch.append(target)
return input_batch, target_batch
# Model
class NNLM(nn.Module):
def __init__(self):
super(NNLM, self).__init__()
self.C = nn.Embedding(n_class, m)
self.H = nn.Linear(n_step * m, n_hidden, bias=False)
self.d = nn.Parameter(torch.ones(n_hidden))
self.U = nn.Linear(n_hidden, n_class, bias=False)
self.W = nn.Linear(n_step * m, n_class, bias=False)
self.b = nn.Parameter(torch.ones(n_class))
def forward(self, X):
X = self.C(X) # X : [batch_size, n_step, m]
X = X.view(-1, n_step * m) # [batch_size, n_step * m]
tanh = torch.tanh(self.d + self.H(X)) # [batch_size, n_hidden]
output = self.b + self.W(X) + self.U(tanh) # [batch_size, n_class]
return output
if __name__ == '__main__':
n_step = 2 # number of steps, n-1 in paper
n_hidden = 2 # number of hidden size, h in paper
m = 2 # embedding size, m in paper
sentences = ["i like dog", "i love coffee", "i hate milk"]
word_list = " ".join(sentences).split()
word_list = list(set(word_list))
word_dict = {w: i for i, w in enumerate(word_list)}
number_dict = {i: w for i, w in enumerate(word_list)}
n_class = len(word_dict) # number of Vocabulary
model = NNLM()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
input_batch, target_batch = make_batch()
input_batch = torch.LongTensor(input_batch)
target_batch = torch.LongTensor(target_batch)
# Training
for epoch in range(5000):
optimizer.zero_grad()
output = model(input_batch)
# output : [batch_size, n_class], target_batch : [batch_size]
loss = criterion(output, target_batch)
if (epoch + 1) % 1000 == 0:
print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
loss.backward()
optimizer.step()
# Predict
predict = model(input_batch).data.max(1, keepdim=True)[1]
# Test
print([sen.split()[:2] for sen in sentences], '->', [number_dict[n.item()] for n in predict.squeeze()])
输出结果:
Epoch: 1000 cost = 0.045058
Epoch: 2000 cost = 0.006512
Epoch: 3000 cost = 0.002669
Epoch: 4000 cost = 0.001322
Epoch: 5000 cost = 0.000704
[['i', 'like'], ['i', 'love'], ['i', 'hate']] -> ['dog', 'coffee', 'milk']