##纯小白记录 主要是跟着帖子练习代码以及整理思路 不做任何商用##有问题可以不断更新
参考内容包括但不限于:
三万字最全解析!从零实现Transformer(小白必会版😃) - 知乎
Transformer解读(论文 + PyTorch源码)_pytorch的transformer论文-优快云博客
保姆级分析self Attention为何除根号d,看不懂算我的 - 知乎
The Annotated Transformer(访问可能需要魔法)
一.Transformer的架构
1.1编码器
首先来看Encoder部分,它是由N层方框里面的内容堆叠起来的。对于每一层来说,都由两部分构成:一部分是multi-head self-attention机制,另一部分是一个简单的全连接前馈网络。在每一部分上,都使用残差+layer normalization来进行处理。论文中,这样的方框有6个,即,模型的隐层单元数
。
Encoder(编码器)部分示意图
1.2自注意力机制
Encoder内部没有使用RNN,取而代之的是一种self-attention(自注意力)机制。
一般我们用的attention机制,可以抽象为输入一个查询(query),去查询键值对(key-value pair)中的key,然后得到一个概率分布,再据此对value进行加权相加,获取当前query下的注意力表征。而我们的query,往往是Decoder中某一个step的输出,key-value pair往往是encoder的输出。
论文里面使用的也是这种attention机制,只不过其query、key、value都是由encoder的输出经过不同的变换而来,也即self-attention,所有的东西都是自己。他们定义了一种叫“Scaled Dot-Product Attention”的计算方式,用于计算给定query、key和value下的注意力表征,如下图(左)所示:
这里的 、
和
分别表示
、
和
矩阵,它们的维度分别为
、
、
。计算公式为:
一般我们经常使用的attention计算方式有两种:一种是乘性attention,即使用内积的方式;另一种是加性attention,即使用额外一层隐藏层来计算。这两种计算方式理论上复杂度是差不多的,但乘性attention因为可以用矩阵运算,会更节省时间和空间。对照着上图(左)和公式来看,这个公式与乘性attention计算方式的唯一不同就在于使用了一个缩放因子。这里为何要进行缩放呢?论文中给出了解释:在
比较小的时候,不加缩放的效果和加性attention的效果差不多,但当
比较大的时候,不加缩放的效果就明显比加性attention的效果要差,怀疑是当
增长的时候,内积的量级也会增长,导致softmax函数会被推向梯度较小的区域,为了缓解这个问题,加上了这个缩放项进行量级缩小。
这里补充一下关于缩放因子:
保姆级分析self Attention为何除根号d,看不懂算我的 - 知乎
首先是softmax函数的求导:
所以如果不加缩放会出现维度的影响会使得数值很大,输出的或者
倾向于接近1或者0,不管哪种方式都会导致导函数的值很小,会造成梯度消失。
在transformer网络中,softmax函数的输入由key向量和query向量之间的点积组成,key向量和query向量的维度 越大,点积往往越大, 原文论中12个head对应的大小是64,作者在原论文中采用的补救措施,是将点积除以key和query维度的平方根,这个计算过程如下:我们假设key和query服从均值为0,方差为1的均匀分布, 即D(query)=D(key)=1, 维度大小为64,(其实不用假设,现在基本上都符合)那么点积后的,我们可以计算他的方差变化。
所以我们将每个分数都除以64, 将方差降到1。
ok继续:
论文里面还提到,只使用一个attention的计算方式未免太过单薄,所以他们提出了multi-head(多头)注意力机制。将注意力的计算分散到不同的子空间进行,以期能从多方面进行注意力的学习,具体做法如上图(右)所示。并行地将、
和
通过不同的映射矩阵映射到不同的空间(每个空间是一个头),再分别在这些空间中对应着进行单个“Scaled Dot-Product Attention”的学习,最后将得到的多头注意力表征进行拼接,经过一个额外的映射层映射到原来的空间。其公式如下:
这里的,
,
,
。表示第
个头的变换矩阵,
表示头的个数。
在论文里面,,并且
。可见这里虽然分了很多头去计算,但整体的维度还是不变的,因此计算量还是一样的。
1.3前馈网络
这部分是整体架构图中的Feed Forward模块,其实就是一个简单的全连接前馈网络。它由两层全连接及ReLU激活函数构成,计算公式如下:
这里的全连接是Position-wise逐位置的,即设前面的attention输出的维度为,则变换时,实际上是只针对
进行变换,对于每个位置(Length维度)上,都使用同样的变换矩阵。
在论文中,这里的仍然是512,两层全连接的中间隐层单元数为
。
1.4 add&norm
在整体架构图中,还有一个部分是add&norm,这其实是借鉴了图像中的残差思想。在self-attention和feed forward计算之后都会加上一个残差变换,同时也会加上Layer Normalization(参见: https://arxiv.org/pdf/1607.06450.pdf ,用在有循环机制的网络里面效果较好)。设输入为,则输出为
,这里的
即是self-attention或feed forward层。
1.5 解码器
接着来看Decoder部分(右半部分),它同样也是由N层(在论文中,仍取)堆叠起来,对于其中的每一层,除了与Encoder中相同的self-attention及Feed Forward之外,还在中间插入了一层传统encoder-decoder框架中的attention层,即将decoder的输出作为query去查询encoder的输出,同样用的是multi-head attention,使得在decode的时候能看到encoder的所有输出。
同时,作为decoder,在预测当前步的时候,是不能知道后面的内容的,即attention需要加上mask,将当前步之后的分数全部置为,然后再计算softmax,以防止发生数据泄露。
1.6 位置编码层
位置编码的意义:Positional Encoding就是将位置信息添加(嵌入)到Embedding词向量中,让Transformer保留词向量的位置信息,可以提高模型对序列的理解能力。
Transformer虽然摒弃了RNN的循环结构和CNN的局部相关性,但对于序列来说,最重要的其实还是先后顺序。看前面self-attention的处理方式,实际上与“词袋”模型没什么区别,这样忽略了位置信息的缺陷肯定是要通过一定的手段来弥补。
论文中提出了一个非常“smart”的方式来加入位置信息,就是这里的Positional Encoding,它对于每个位置进行编码,然后与相应位置的word embedding进行相加,构成当前位置的新word embedding。它采用如下的公式为每个
进行编码:
其中,表示embedding向量中的位置,即
中的每一维。选择这种sin函数有两种好处:1)可以不用训练,直接编码即可,而且不管什么长度,都能直接得到编码结果;2)能表示相对位置,根据
,
可以表示为
的线性变换,这样的线性组合意味着位置向量中蕴含了相对位置信息。
这里插个眼,看完再来补充。
1.7 一些Tricks和技巧
-
Embedding和Softmax:论文中将embedding层的参数与最后的Softmax层之前的变换层参数进行了共享(参见:https://arxiv.org/pdf/1608.05859.pdf ),并且在embedding层,将嵌入的结果乘上
。
-
初始化:看代码里面的初始化方式采用的是PyTorch里面的
nn.xavier_uniform
,不知道是不是必须的,这个还是要具体问题具体尝试? -
优化器:论文里面提到了他们用的优化器,是以
,
和
的Adam为基础,而后使用一种warmup的学习率调整方式来进行调节。具体公式如下:基本上就是先用一个固定的warmup_steps进行学习率的线性增长(热身),而后到达warmup_steps之后会随着step_num的增长而逐渐减小,他们用的
,这个可以针对不同的问题自己尝试。
-
正则化:论文在训练的时候采用了两种正则化的方式。1)dropout:主要用在每个SubLayer计算结束之后,比如self-attention或feed forward,然后再与输入进行add & norm,同时也作用在经过了位置编码后的embedding上,他们取的
;2)标签平滑:即Label Smoothing(参见: https://arxiv.org/pdf/1512.00567.pdf ),其实还是从图像上搬过来的,具体操作可以看下一节的代码实现。这里论文取的
,他们发现会损失困惑度,但能提升准确率和BLEU值!
二、Pytorch实现
参考:
The Annotated TransformerTransformer解读(论文 + PyTorch源码)_pytorch的transformer论文-优快云博客
2.1前置操作
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import math,copy,time
from torch.autograd import Varaiable
import matplotlib.pyplot as plt
import seaborn
seaborn.set_context(context="talk")
%matplotlib inline
2.2编码器解码器
class EncoderDecoder(nn.Module):
def __init__(self,encoder,decoder,src_embed,tgt_embed,generator):
super(EncoderDecoder,self).__init__()
self.encoder = encoder
self.decoder = decoder
self.src_embed = src_embed
self.generator = generator
def forward(self,src,tgt,src_mask,tgt_mask):
return self.decode(self.encode(src,src_mask),src_mask,tgt,tgt_mask)
def encode(selfself,src,src_mask):
return self.encoder(self.src_embed(src),src_mask)
def decode(self,memory,src_mask,tgt,tgt_mask):
return self.decoder(self.tgt_embed(tgt),memory,src_mask,tgt_mask)
2.3多头注意力机制
multi-head attention可用于三个地方,分别是Encoder和Decoder中各自的self-attention部分,还有Encoder-Decoder之间的attention部分。但其实这三个地方的不同仅仅在于query、key、value和mask的不同,因此当将这4部分作为参数传入时,模型的计算方式便可抽象为如下的形式:
class MultiHeadAttention(nn.Module):
def __init__(self,h,d_model,dropout=0.1):
super(MultiHeadAttention,self).__init__()
assert d_model % h == 0
self.d_k = d_model//h
self.h = h
self.linears = clones(nn.Linear(d_model,d_model),4)
self.attn = None
self.dropout = nn.Dropout(p=dropout)
def forward(self,query,key,value,mask=None):
if mask is not None:
mask = mask.unsqueeze(1)
nbatches = query.size(0)
query,key,value = [l(x).view(nbatches,-1,self.h,self.d_k).transpose(1,2) for l,x in zip(self.linears,(query,key,value))]
x,self.attn = attention(query,key,value,mask=mask,dropout=self.dropout)
x = x.transpose(1,2).contiguous().view(nbatched,-1,self.h*self.d_k)
return self.linears[-1](x)
def attention(query,key,value,mask=None,dropout=None):
d_k = query.size(-1)
scores = torch.matmul(query,key.transpose(-2,-1))/math.sqrt(d_k)
if mask is not None:
scores = scores.mask_fill(mask ==0,-1e9)
p_attn = F.softmax(scores,dim = -1)
if dropout is not None:
p_attn = dropout(p_attn)
return torch.matmul(p_attn,value),p_attn
2.4前馈网络
#前馈网络
class PositionwiseFeedForward(nn.Module):
def __init__(self,d_model,d_ff,dropout=0.1):
super(PositionwiseFeedForward,self).__init__()
self.w_1 = nn.Linear(d_model,d_ff)
self.w_2 = nn.Linear(d_ff,d_model)
self.dropout = nn.Dropout(dropout)
def forward(self,x):
return self.w_2(self.dropout(F.relu(self.w_1(x))))
2.5 add&norm
残差模块+LayerNormalization的实现方式:
class SublayerConnecrion(nn.Module):
def __init__(self,size,dropout):
super(SublayerConnecrion,self).__init__()
self.norm = LayerNorm(size)
self.dropout = nn.Dropout(dropout)
def forward(self,x,sublayer):
return x + self.dropout(sublayer(self.norm(x)))
class LayerNorm(nn.Module):
def __init__(self,features,eps=1e-6):
super(LayerNorm,self).__init__()
self.a_2 = nn.Parameter(torch.ones(features))
self.b_2 = nn.Parameter(torch.zeros(features))
self.eps = eps
def forward(self,x):
mean = x.mean(-1,keepdim=True)
std = x.std(-1,keepdim=True)
return self.a_2 * (x-mean) / (std + self.eps) + self.b_2
2.6 位置编码
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
# Compute the positional encodings once in log space.
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2) *
-(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
x = x + Variable(self.pe[:, :x.size(1)],
requires_grad=False)
return self.dropout(x)