transformer(上)论文解读+pytorch实现
- 1. 背景
- 2. 模型架构
- 2.1 scaled dot-product attention
- 2.2 multi-head attention
- 2.3 transformer使用的3种attention
- 2.4 point-wise feed-forward net
- 2.5 encoder-layer
- 2.6 decoder-layer
- 2.7 positional embedding
- 2.8 masking
- 2.9 encoder
- 2.10 decoder
- 2.11 embedding && softmax
- 2.12 为什么使用self-attention?
- 3. 模型训练
- 3.1 优化器
- 3.2 3种正则化
- 4. 搭积木:搭建一个transformer
- 5. transformer应用实现:机器翻译(pytorch)
- 6. 模型评价
- 完整代码
- 参考
论文全称:Attention Is All You Need
1. 背景
transformer一开始提出时,是解决机器翻译
任务的。在提出transformer之前,像机器翻译、语言理解模型等这类序列转导模型(sequence transduction model)
大多在encoder-decoder
的架构里,基于主流的RNN
或者CNN
实现,其中最优秀的模型则是在encoder和decoder之间引入了attention机制
(SOTA)。简单来说,就是几乎没绕开recurrent model
。
recurrent model的劣势:
是前一状态
与当前输入
的函数,这种固有的
“顺序性”(sequential nature)
阻碍了训练的并行化(parallelization)
,这在较长的序列长度上变得至关重要,因为内存约束限制了批处理的样本。虽然目前一些工作通过因式分解技巧(factorization tricks)和条件计算(conditional computation)在计算效率(computational effificiency)上也取得了巨大的提升和改善,并且后者(条件计算)也同样提升了模型性能。但顺序计算的基本约束仍然存在。
问题总结:计算效率(sequential computation)阻碍了计算并行化,也造成了序列长度限制。
attention机制:
attention几乎已经是sequence modeling and transduction models不可或缺的一部分了,attention的引入允许模型对长距离依赖建模
,不需要考虑他们在输入或输出中的距离。但其问题是:大多数场景下,只能与递归网络(recurrent network)一起使用
。
self-attention:
self-attention
,有时称为intra-attention
,是一种涉及单个序列的不同位置以计算序列表示的attention机制( an attention mechanism relating different positions of a single sequence in order to compute a representation of the sequence)。 self-attention已成功地应用于阅读理解、抽象概括、语篇蕴涵等多种任务中。transformer的核心也是self-attention的堆叠。
transformer:
一种新的模型架构,避开了循环(recurrent )
,而 完全依赖于注意机制来绘制输入和输出之间的全局依赖关系
(a model architecture eschewing recurrence and instead relying entirely on an attention mechanism to draw global dependencies between input and output)。
2. 模型架构
transformer也像大多数序列转导模型(sequence transduction model)
一样,总体上是基于encoder-decoder架构,其模型架构如下,encoder和decoder部分大致是self-attention
,point-wise feed-forward
和fc层
:
接下来,就由这个模型架构图从小到大依次介绍各个sub-layer及其细节。
2.1 scaled dot-product attention
transformer的核心就是它的multi-head self-attention,而所谓的“多头”就是下面scaled dot-product attention的concat,所以先看看scaled dot-product attention的结构:
Attention函数的本质可以被描述为一个查询(query)到一系列(键key-值value)对的映射(mapping a query and set of key-value pairs to an output)
。下面的公式可以泛化的表示这种关系:
其中Source的构成元素可以想象成是由一系列<key,value>pairs组成的。那么attention机制可以看作就是query与source中的各个key计算相似性或相关性,作为一种权重系数(一般使用softmax对权重进行归一化),最后对source中与key对应的value进行加权求和,通常key=value。计算attention时,query和每个key的相似度函数
常用的有:
- 点积(dot),
- 拼接(concat),
]
- general,
- perceptron,
transformer中的scaled dot-product attention就是使用点积进行相似度计算的一种特殊方式,它的特殊在于它使用了一个缩放因子(scaled factor)
:。其计算公式如下:
其中queries和keys的维度是:,values的维度是:
。
scaled dot-product attention的pytorch实现:
def scaled_dot_product_attention(q, k, v, mask=None):
"""
#计算注意力权重。
q, k, v 必须具有匹配的前置维度。 且dq=dk
k, v 必须有匹配的倒数第二个维度,例如:seq_len_k = seq_len_v。
#虽然 mask 根据其类型(填充或前瞻)有不同的形状,
#但是 mask 必须能进行广播转换以便求和。
#参数:
q: 请求的形状 == (..., seq_len_q, depth)
k: 主键的形状 == (..., seq_len_k, depth)
v: 数值的形状 == (..., seq_len_v, depth_v) seq_len_k = seq_len_v
mask: Float 张量,其形状能转换成
(..., seq_len_q, seq_len_k)。默认为None。
#返回值:
#输出,注意力权重
"""
# matmul(a,b)矩阵乘:a b的最后2个维度要能做乘法,即a的最后一个维度值==b的倒数第2个纬度值,
# 除此之外,其他维度值必须相等或为1(为1时会广播)
matmul_qk = torch.matmul(q, k.transpose(-1, -2)) # 矩阵乘 =>[..., seq_len_q, seq_len_k]
# 缩放matmul_qk
dk = torch.tensor(k.shape[-1], dtype=torch.float32) # k的深度dk,或叫做depth_k
scaled_attention_logits = matmul_qk / torch.sqrt(dk) # [..., seq_len_q, seq_len_k]
# print('scaled_attention_logits:', scaled_attention_logits)
# 将 mask 加入到缩放的张量上(重要!)
if mask is not None: # mask: [b, 1, 1, seq_len]
# mask=1的位置是pad,乘以-1e9(-1*10^9)成为负无穷,经过softmax后会趋于0
scaled_attention_logits += (mask * -1e9)
# softmax 在最后一个轴(seq_len_k)上归一化
attention_weights = torch.nn.functional.softmax(scaled_attention_logits, dim=-1) # [..., seq_len_q, seq_len_k]
# print('attention_weights:', attention_weights, attention_weights.dtype)
output = torch.matmul(attention_weights, v) # =>[..., seq_len_q, depth_v]
return output, attention_weights # [..., seq_len_q, depth_v], [..., seq_len_q, seq_len_k]
实现中有用到mask,后面会细讲mask的作用和实现。
注意:为什么要使用
进行scale呢?
防止梯度消失!
论文中提到越大的会使得点积的数量级越大,这就会使得softmax的值陷入到梯度极小的区域内(可以联想sigmoid函数曲线图,有类似的特性,在|x|很大时其函数值非0即1),为了抵消这个影响,遂对点积做一个scale:乘上一个
。
那为什么又偏偏是维度开根号来进行scale呢?
可将点积的方差稳定在1!
论文的脚注里有一点提示:假设
和
是均值为0方差为1的独立的随机变量,那么他们的点积
的均值是0,方差就是
。
我们知道当两个随机变量,
独立时,
的方差
,且有
。也就是说
,那么
。即点积后的方差是
,那么除以
就可以将点积后的方差稳定在1,
使得后面进行softmax时能更gentler(或者更加“soft”)
。
2.2 multi-head attention
multi-head attention就是上面的scaled dot-product attention做多次计算,论文中是h=8,做8次计算,即8个头,其模型结构如下:
具体的Query,Key,Value先经过线性变换
,然后计算scaled dot-product attention
,这个过程做h次
(包括线性变换),每次线性变换的参数也是不一样的。然后对h次的scaled dot-product attention做拼接concat
,最后再经过一次线性变换层
得到最终整个multi-head attention的结果,其计算过程可用如下公式表示: 其中投影维参数矩阵,
,
,
。论文对应的实验中
。
注意实现时的trick:
这里要提一嘴的是看公式和图,直观上觉得实现时就是h=8组,做8次linear变换,也就是3x8=24次矩阵乘法运算,这里
维度分别是
,
和
(具体实现时
也等于
),然后再计算scaled dot-product attention。但事实上由于矩阵乘法的底层实现已经做了很多优化,计算起来非常高效和方便,所以这里具体实现时(参考tensorflow2官方代码),是只用了一组
,即经过3次矩阵乘法运算,这里
的维度分别是
,所以经过linear变换后
在维度上没有变化,但是后面在做一次“拆分”,即将最后一维
平均拆分到8个头里,维度变为:
。
具体看下面multi-head attention的实现就更清楚了:
class MultiHeadAttention(torch.nn.Module):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
self.d_model = d_model
assert d_model % self.num_heads == 0 # 因为输入要被(平均?)split到不同的head
self.depth = d_model // self.num_heads # 512/8=64,所以在scaled dot-product atten中dq=dk=64,dv也是64
self.wq = torch.nn.Linear(d_model, d_model)
self.wk = torch.nn.Linear(d_model, d_model)
self.wv = torch.nn.Linear(d_model, d_model)
self.final_linear = torch.nn.Linear(d_model, d_model)
def split_heads(self, x, batch_size): # x [b, seq_len, d_model]
x = x.view(batch_size, -1, self.num_heads,
self.depth) # [b, seq_len, d_model=512]=>[b, seq_len, num_head=8, depth=64]
return x.transpose(1, 2) # [b, seq_len, num_head=8, depth=64]=>[b, num_head=8, seq_len, depth=64]
def forward(self, q, k, v, mask): # q=k=v=x [b, seq_len, embedding_dim] embedding_dim其实也=d_model
batch_size = q.shape[0]
q = self.wq(q) # =>[b, seq_len, d_model]
k = self.wk(k) # =>[b, seq_len, d_model]
v = self.wv(v) # =>[b, seq_len, d_model]
q = self.split_heads(q, batch_size) # =>[b, num_head=8, seq_len, depth=64]
k = self.split_heads(k, batch_size) # =>[b, num_head=8, seq_len, depth=64]
v = self.split_heads(v, batch_size) # =>[b, num_head=8, seq_len, depth=64]
scaled_attention, attention_weights = scaled_dot_product_attention(q, k, v, mask)
# => [b, num_head=8, seq_len_q, depth=64], [b, num_head=8, seq_len_q, seq_len_k]
scaled_attention = scaled_attention.transpose(1, 2) # =>[b, seq_len_q, num_head=8, depth=64]
# 转置操作让张量存储结构扭曲,直接使用view方法会失败,可以使用reshape方法
concat_attention = scaled_attention.reshape(batch_size, -1, self.d_model) # =>[b, seq_len_q, d_model=512]
output = self.final_linear(concat_attention) # =>[b, seq_len_q, d_model=512]
return output, attention_weights # [b, seq_len_q, d_model=512], [b, num_head=8, seq_len_q, seq_len_k]
为什么要多头呢?
论文中表示:多头注意允许模型在不同的位置联合处理来自不同表示子空间的信息。 (Multi-head attention allows the model to jointly attend to information from different representation subspaces at different positions)。为了验证此,论文还对不同头的attention weights进行了可视化展示,以说明不同头的注意力分布,表现出与句子的句法和语义结构有关的行为。
attention的可视化
图中8种不同的颜色代表不同的head,颜色越深代表attention weights越大,越“关注”。从上图可以看出self-attention可以学习到句子内部的长距离依赖"making…….more difficult"这个短语,这正是self-attention的优势。
这幅图中上面的图是一个单头的attention,可以看到单头attention中"its"这个词只能学习到与"law"的依赖关系,而下面的“双头”attention中,"its"不仅学习到了与"law"关系, 还学习到了与单词"application"的依赖关系。由此可以看出,多头能够从不同的表示子空间里学习到相关信息。
2.3 transformer使用的3种attention
transformer中使用multi-head attention的3种不同的方式:
-
在encoder和decoder之间的attention layer
,query来自于前一个decoder layer,而key和value来自于encoder的输出,这使得decoder能注意到/关注到input sequence的所有位置。
此时有Y=MultiHead(Q,K,V)=MultiHead(X_d,X_e,X_e),即K=V -
在encoder中的self-attention layer
,所有的k,v,q来自于encoder的的前一个layer的output。
此时有Y=MultiHead(Q,K,V)=MultiHead(X_e,X_e,X_e),即Q=K=V -
在decoder中的self-attention layer
,为了保持自回归(auto regressive)
的特征,阻值了左向信息流,这通过在Scaled Dot-Product Attention中加入掩码机制
来实现,mask掉softmax的input中属于非法连接的值。
此时有,即
2.4 point-wise feed-forward net
point-wise feed-forward network含有2个线性变换和一个ReLU激活:
这一层的input和output的维度都是,而内层的维度的是
其实就是。
其pytorch实现:
# 点式前馈网络
def point_wise_feed_forward_network(d_model, dff):
feed_forward_net = torch.nn.Sequential(
torch.nn.Linear(d_model, dff), # [b, seq_len, d_model]=>[b, seq_len, dff=2048]
torch.nn.ReLU(),
torch.nn.Linear(dff, d_model), # [b, seq_len, dff=2048]=>[b, seq_len, d_model=512]
)
return feed_forward_net
2.5 encoder-layer
每个encoder-layer包括以下2个sub-layer:
- 多头注意力(含padding mask)+ Add&Norm
- 点式前馈网络(Point wise feed forward networks)+ Add&Norm
注意:
每个子层还伴随着一个残差连接
,然后进行“层归一化”(LayerNorm)
。残差连接有助于避免深度网络中的梯度消失问题。
每个子层的输出是 LayerNorm(x + Sublayer(x))。归一化是在 (最后一个)维度完成的。
注意:
实际具体实现时在每个sub layer 之后加入了dropout层,再才进行add the sub layer input 和 normalized,即
Transformer 中的encoder
由N=6个encoder layer堆叠
而成。
其pytorch实现:
class EncoderLayer(torch.nn.Module):
def __init__(self, d_model, num_heads, dff, rate=0.1):
super(EncoderLayer, self).__init__()
self.mha = MultiHeadAttention(d_model, num_heads) # 多头注意力(padding mask)(self-attention)
self.ffn = point_wise_feed_forward_network(d_model, dff)
self.layernorm1 = torch.nn.LayerNorm(normalized_shape=d_model, eps=1e-6)
self.layernorm2 = torch.nn.LayerNorm(normalized_shape=d_model, eps=1e-6)
self.dropout1 = torch.nn.Dropout(rate)
self.dropout2 = torch.nn.Dropout(rate)
# x [b, inp_seq_len, embedding_dim] embedding_dim其实也=d_model
# mask [b,1,1,inp_seq_len]
def forward(self, x, mask):
attn_output, _ = self.mha(x, x, x, mask) # =>[b, seq_len, d_model]
attn_output = self.dropout1(attn_output)
out1 = self.layernorm1(x + attn_output) # 残差&层归一化 =>[b, seq_len, d_model]
ffn_output = self.ffn(out1) # =>[b, seq_len, d_model]
ffn_output = self.dropout2(ffn_output)
out2 = self.layernorm2(out1 + ffn_output) # 残差&层归一化 =>[b, seq_len, d_model]
return out2 # [b, seq_len, d_model]
2.6 decoder-layer
每个decoder-layer包括以下3个sub-layer:
- masked的多头注意力即
self-attention
(含look ahead mask 和 padding mask)+ Add&Norm - 多头注意力即
encoder-decoder attention
(含padding mask)+ Add&Norm
V和 K接收encoder的输出作为输入。Q接收masked的多头注意力子层的输出。 - 点式前馈网络(Point wise feed forward networks)+ Add&Norm
每个子层还伴随着一个残差连接,然后进行“层归一化”(LayerNorm)。
每个子层的输出是 LayerNorm(x + Sublayer(x))。归一化是在 d_model(最后一个)维度完成的。
Transformer 中共有 N 个解码器层。
当 Q 接收到decoder的第一个注意力块的输出,并且 K 接收到encoder的输出时,注意力权重表示根据encoder的输出赋予decoder输入的重要性。
换一种说法,decoder通过查看encoder输出和对其自身输出的自注意力,预测下一个词。
其pytorch实现:
class DecoderLayer(torch.nn.Module):
def __init__(self, d_model, num_heads, dff, rate=0.1):
super(DecoderLayer, self).__init__()
self.mha1 = MultiHeadAttention(d_model,
num_heads) # masked的多头注意力(look ahead mask 和 padding mask)(self-attention)
self.mha2 = MultiHeadAttention(d_model, num_heads) # 多头注意力(padding mask)(encoder-decoder attention)
self.ffn = point_wise_feed_forward_network(d_model, dff)
self.layernorm1 = torch.nn.LayerNorm(normalized_shape=d_model, eps=1e-6)
self.layernorm2 = torch.nn.LayerNorm(normalized_shape=d_model, eps=1e-6)
self.layernorm3 = torch.nn.LayerNorm(normalized_shape=d_model, eps=1e-6)
self.dropout1 = torch.nn.Dropout(rate)
self.dropout2 = torch.nn.Dropout(rate)
self.dropout3 = torch.nn.Dropout(rate)
# x [b, targ_seq_len, embedding_dim] embedding_dim其实也=d_model=512
# look_ahead_mask [b, 1, targ_seq_len, targ_seq_len] 这里传入的look_ahead_mask应该是已经结合了look_ahead_mask和padding mask的mask
# enc_output [b, inp_seq_len, d_model]
# padding_mask [b, 1, 1, inp_seq_len]
def forward(self, x, enc_output, look_ahead_mask, padding_mask):
attn1, attn_weights_block1 = self.mha1(x, x, x,
look_ahead_mask) # =>[b, targ_seq_len, d_model], [b, num_heads, targ_seq_len, targ_seq_len]
attn1 = self.dropout1(attn1)
out1 = self.layernorm1(x + attn1) # 残差&层归一化 [b, targ_seq_len, d_model]
# Q: receives the output from decoder's first attention block,即 masked multi-head attention sublayer
# K V: V (value) and K (key) receive the encoder output as inputs
attn2, attn_weights_block2 = self.mha2(out1, enc_output, enc_output,
padding_mask) # =>[b, targ_seq_len, d_model], [b, num_heads, targ_seq_len, inp_seq_len]
attn2 = self.dropout2(attn2)
out2 = self.layernorm2(out1 + attn2) # 残差&层归一化 [b, targ_seq_len, d_model]
ffn_output = self.ffn(out2) # =>[b, targ_seq_len, d_model]
ffn_output = self.dropout3(ffn_output)
out3 = self.layernorm3(out2 + ffn_output) # 残差&层归一化 =>[b, targ_seq_len, d_model]
return out3, attn_weights_block1, attn_weights_block2
# [b, targ_seq_len, d_model], [b, num_heads, targ_seq_len, targ_seq_len], [b, num_heads, targ_seq_len, inp_seq_len]
2.7 positional embedding
为什么需要位置编码?
到现在为止,transformer的核心multi-head attention就介绍完了,仔细观察multi-head attention的计算过程,不难发现如果K,V按行打乱顺序(相当于句子的词序打乱),计算的attention的结果是一样的,也就是说这样计算的attention其实也仅仅是一样非常精妙的词袋(bag of words)模型而已。而如果要表示像文本这类拥有temporal/spatial relationship的input,就必须加入位置编码信息(positional encoding)来表示token之间的序列关系。
简单来说,就是由于model中不含有任何recurrence or convolution,所以句子中token的相对位置关系无法体现,所以就需要在embedding vector中加入position encoding vector(维度相同)。这样每个词的词向量在
论文中采用的位置编码:
pos表示position,表示
维度的第
个位置。该位置编码很容易学习到相对位置关系,因为对于任意的位移k,
都能表示成
的线性函数。下面推导一下这种线性关系:
由三角函数公式知:
那么:
即求
时,其总能表示为
的线性组合,当
给定时,
只于偏移量k有关,所以它能表征相对距离k。
还位置编码的pytorch实现:
# 计算角度:pos * 1/(10000^(2i/d))
def get_angles(pos, i, d_model):
# 2*(i//2)保证了2i,这部分计算的是1/10000^(2i/d)
angle_rates = 1 / np.power(10000, 2 * (i // 2) / np.float32(d_model)) # => [1, 512]
return pos * angle_rates # [50,1]*[1,512]=>[50, 512]
# np.arange()函数返回一个有终点和起点的固定步长的排列,如[1,2,3,4,5],起点是1,终点是5,步长为1
# 注意:起点终点是左开右闭区间,即start=1,end=6,才会产生[1,2,3,4,5]
# 只有一个参数时,参数值为终点,起点取默认值0,步长取默认值1。
def positional_encoding(position, d_model): # d_model是位置编码的长度,相当于position encoding的embedding_dim?
angle_rads = get_angles(np.arange(position)[:, np.newaxis], # [50, 1]
np.arange(d_model)[np.newaxis, :], # [1, d_model=512]
d_model)
angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2]) # 2i
angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2]) # 2i+2
pos_encoding = angle_rads[np.newaxis, ...] # [50,512]=>[1,50,512]
return torch.tensor(pos_encoding, dtype=torch.float32)
将此位置编码绘制出来看一下它的特点:
from matplotlib import pyplot as plt
def draw_pos_encoding(pos_encoding):
plt.pcolormesh(pos_encoding[0], cmap='RdBu') # 绘制分类图
plt.xlabel('Depth')
plt.xlim((0, 512))
plt.ylabel('Position')
plt.colorbar() # 条形bar颜色图例
# plt.savefig('./imgs/pos_encoding.png')
plt.show()
draw_pos_encoding(pos_encoding)
输出:
上图纵坐标表示句子长度为50,每个位置表示句子中的每个token,横坐标表示位置编码的深度,即position embedding_dim(也就是)。从图上也可以看出
后面位置是前面位置的线性组合,即保证了即使位置不是相邻的,也可能有关系。此外图上也表明了每个位置的编码又是独特的
。
另外对每个位置的encoding两两互相做点积然后绘制热图,如下:
可以看出position encoding的点积值是“对称”的,并且位置相距越远,点积的值越小,而自己和自己点积值最大,这进一步表明这种位置编码方式能表示不同位置的token的相对位置关系
。
总结起来,这种使用sinusoidal正弦方法的位置编码有如下特点:
- (1)后面位置是前面位置的线性组合,保证了即使位置不是相邻的,也可能有关系
- (2)每个位置的编码又是独特的
- (3)每两个位置的encoding互相做点积,位置越远,点积的值越小,自己和自己点积,值最大
- (4)该位置编码不需要学习得到,可直接计算得出
【注意1:是否一定要使用此种位置编码?】
不一定!
论文中提到,在实验中也试过learned position embeding ,这种方法与上面的sinusoidal正弦方法效果上非常相近,但选择上面的sinusoidal方法是因为:这种表示方法在测试阶段接受长度超过训练集实例的情况!
(We chose the sinusoidal version because it may allow the model to extrapolate to sequence lengths longer than the ones encountered during training.)
【注意2:为什么position encoding与embedding是直接相加呢?】
当然也可以concat,但是相加也可以make sense!
公式选择sin和cos函数的话,这样不论句子有多长,叠加的每个元素都会在[-1,1]之间
,这样加到词向量中也不会对原向量的方向、长度造成很大的影响
,可以有效保持词向量之间的距离关系。经过和position encoding的相加,对于一个词向量,它在句子中的不同位置都会得到不同的新的词向量,但是新的词向量又和原来的词向量不会相差太远,在这种情况下,只要训练数据足够多,模型就能学习到词在不同的位置中表达语义的微妙区别,同时也学习到词与词之间的相对位置对于语义表达的影响。(这部分参考了知乎某回答,文末贴有链接)
2.8 masking
模型中用到的mask有2种:
- padding mask:mask pad,即句子中为pad的位置处其mask值为1
- look-ahead mask:mask future token,将当前token后面的词mask掉,只让看到前面的词,即future token位置的mask值为1
其中在encoder-layer中只使用到padding mask
,用于遮挡input中的pad。而在decoder-layer的第一个self multi-head attention
中需要同时使用padding mask和look-ahead mask
,在decoder-layer的第二个encoder-decoder multi-head attention
中仅使用padding mask
,用于遮挡 encoder douput中的pad。
其pytorch实现:
def create_padding_mask(seq): # seq [b, seq_len]
# seq = torch.eq(seq, torch.tensor(0)).float() # pad=0的情况
seq = torch.eq(seq, torch.tensor(pad)).float() # pad!=0
return seq[:, np.newaxis, np.newaxis, :] # =>[b, 1, 1, seq_len]
# torch.triu(tensor, diagonal=0) 求上三角矩阵,diagonal默认为0表示主对角线的上三角矩阵
# diagonal>0,则主对角上面的第|diagonal|条次对角线的上三角矩阵
# diagonal<0,则主对角下面的第|diagonal|条次对角线的上三角矩阵
def create_look_ahead_mask(size): # seq_len
mask = torch.triu(torch.ones((size, size)), diagonal=1)
# mask = mask.device() #
return mask # [seq_len, seq_len]
2.9 encoder
encoder 包括:
- 输入嵌入(Input Embedding)
- 位置编码(Positional Encoding)
- N 个编码器层(encoder layers)
输入经过嵌入(embedding)后,该嵌入与位置编码相加。该加法结果的输出是encoder的输入。encoder的输出是decoder的输入之一。
encoder的pytorch实现:
class Encoder(torch.nn.Module):
def __init__(self,
num_layers, # N个encoder layer
d_model,
num_heads,
dff, # 点式前馈网络内层fn的维度
input_vocab_size, # 输入词表大小(源语言(法语))
maximun_position_encoding,
rate=0.1):
super(Encoder, self).__init__()
self.num_layers = num_layers
self.d_model = d_model
self.embedding = torch.nn.Embedding(num_embeddings=input_vocab_size, embedding_dim=d_model)
self.pos_encoding = positional_encoding(maximun_position_encoding,
d_model) # =>[1, max_pos_encoding, d_model=512]
# self.enc_layers = [EncoderLayer(d_model, num_heads, dff, rate).cuda() for _ in range(num_layers)] # 不行
self.enc_layers = torch.nn.ModuleList([EncoderLayer(d_model, num_heads, dff, rate) for _ in range(num_layers)])
self.dropout = torch.nn.Dropout(rate)
# x [b, inp_seq_len]
# mask [b, 1, 1, inp_sel_len]
def forward(self, x, mask):
inp_seq_len = x.shape[-1]
# adding embedding and position encoding
x = self.embedding(x) # [b, inp_seq_len]=>[b, inp_seq_len, d_model]
# 缩放 embedding 原始论文的3.4节有提到: In the embedding layers, we multiply those weights by \sqrt{d_model}.
x *= torch.sqrt(torch.tensor(self.d_model, dtype=torch.float32))
pos_encoding = self.pos_encoding[:, :inp_seq_len, :]
pos_encoding = pos_encoding.cuda() # ###############
x += pos_encoding # [b, inp_seq_len, d_model]
x = self.dropout(x)
for i in range(self.num_layers):
x = self.enc_layers[i](x, mask) # [b, inp_seq_len, d_model]=>[b, inp_seq_len, d_model]
return x # [b, inp_seq_len, d_model]
2.10 decoder
decoder包括:
- 输出嵌入(Output Embedding)
- 位置编码(Positional Encoding)
N 个解码器层(decoder layers)
目标(target)经过一个嵌入embedding后,该嵌入和位置编码相加。该加法结果是decoder的输入。decoder的输出是最后的linear线性层的输入。
decoder的pytorch实现:
class Decoder(torch.nn.Module):
def __init__(self,
num_layers, # N个encoder layer
d_model,
num_heads,
dff, # 点式前馈网络内层fn的维度
target_vocab_size, # target词表大小(目标语言(英语))
maximun_position_encoding,
rate=0.1):
super(Decoder, self).__init__()
self.num_layers = num_layers
self.d_model = d_model
self.embedding = torch.nn.Embedding(num_embeddings=target_vocab_size, embedding_dim=d_model)
self.pos_encoding = positional_encoding(maximun_position_encoding,
d_model) # =>[1, max_pos_encoding, d_model=512]
# self.dec_layers = [DecoderLayer(d_model, num_heads, dff, rate).cuda() for _ in range(num_layers)] # 不行
self.dec_layers = torch.nn.ModuleList([DecoderLayer(d_model, num_heads, dff, rate) for _ in range(num_layers)])
self.dropout = torch.nn.Dropout(rate)
# x [b, targ_seq_len]
# look_ahead_mask [b, 1, targ_seq_len, targ_seq_len] 这里传入的look_ahead_mask应该是已经结合了look_ahead_mask和padding mask的mask
# enc_output [b, inp_seq_len, d_model]
# padding_mask [b, 1, 1, inp_seq_len]
def forward(self, x, enc_output, look_ahead_mask, padding_mask):
targ_seq_len = x.shape[-1]
attention_weights = {}
# adding embedding and position encoding
x = self.embedding(x) # [b, targ_seq_len]=>[b, targ_seq_len, d_model]
# 缩放 embedding 原始论文的3.4节有提到: In the embedding layers, we multiply those weights by \sqrt{d_model}.
x *= torch.sqrt(torch.tensor(self.d_model, dtype=torch.float32))
# x += self.pos_encoding[:, :targ_seq_len, :] # [b, targ_seq_len, d_model]
pos_encoding = self.pos_encoding[:, :targ_seq_len, :] # [b, targ_seq_len, d_model]
pos_encoding = pos_encoding.cuda() # ###############
x += pos_encoding # [b, inp_seq_len, d_model]
x = self.dropout(x)
for i in range(self.num_layers):
x, attn_block1, attn_block2 = self.dec_layers[i](x, enc_output, look_ahead_mask, padding_mask)
# => [b, targ_seq_len, d_model], [b, num_heads, targ_seq_len, targ_seq_len], [b, num_heads, targ_seq_len, inp_seq_len]
attention_weights[f'decoder_layer{i + 1}_block1'] = attn_block1
attention_weights[f'decoder_layer{i + 1}_block2'] = attn_block2
return x, attention_weights
# => [b, targ_seq_len, d_model],
# {'..block1': [b, num_heads, targ_seq_len, targ_seq_len],
# '..block2': [b, num_heads, targ_seq_len, inp_seq_len], ...}
2.11 embedding && softmax
在输入端
,可以使用预训练的embeddings将输入token转换为向量,维度是,也可以让模型自己学习token embedding。
从整个transformer模型架构图可以看到,在整个输出端
(decoder的最后)还使用linear transformation和softmax 来转化decoder的输出以预测next word的概率:
【注意1:】缩放 embedding
原始论文的3.4节Embeddings and Softmax最后一句有提到: In the embedding layers, we multiply those weights by
# adding embedding and position encoding
x = self.embedding(x) # [b, targ_seq_len]=>[b, targ_seq_len, d_model]
# 缩放 embedding 原始论文的3.4节有提到: In the embedding layers, we multiply those weights by \sqrt{d_model}.
x *= torch.sqrt(torch.tensor(self.d_model, dtype=torch.float32))
x += self.pos_encoding[:, :targ_seq_len, :] # [b, targ_seq_len, d_model]
这在前面的encoder和decoder的实现中都可以看到这个细节。
【注意2:】shared weights
共享权重
原论文中提到:In our model, we share the same weight matrix between the two embedding layers and the pre-softmax linear transformation, similar to [30]
意思是: 最后的decoder的output [b, targ_seq_len, d_model]
会经过最后一个 learned linear transformation and softmax function,这个linear的in_features=d_model
, out_features=targ_vocab_size
, 其权重shape为[d_model, target_vocab_size]
,那么decoder的output [b, targ_seq_len, d_model]
经过这个linear后得到[b, targ_seq_len, targ_vocab_size ]
。
而decoder的输入token embedding 的 num_embeddings=target_vocab_size
, embedding_dim=d_model
,其权重shape是[d_model, target_vocab_size ]
,这与final linear 的权重shape是一样的,所以论文这里的linear的权重设置成是与embedding的 权重共享
其实这里共享权重也是make sense的,因为embedding层是在vocab_size上映射到高维特征,而最后的linear层就是embedding的一个逆向操作,从高维特征重新映射回vocab_size上。
(不过我自己在使用pytorch实现时没有共享,我用的是让模型自己学习的方法)
2.12 为什么使用self-attention?
论文中提到促使他们使用self-attention有3个方面:
- One is the
total computational complexity per layer
. 即每层的总计算量,即每一层的复杂度
。
如果输入序列n小于表示维度d的话,每一层的时间复杂度self-attention是比较有优势的。当n比较大时,作者也给出了一种解决方案:restricted self-attention
,即每个词不是和所有词计算attention,而是只与限制的r个词去计算attention。 - Another is the amount of computation that can be parallelized, as measured by the minimum number of sequential operations required.
可以并行化的计算的数量
,使用需要的最小顺序计算的数量来衡量,self-attention是级别,因为self-attention不像RNN那样需要依赖前一时刻的计算。
- The third is the path length between long-range dependencies in the network. 网络中远程依赖的路径长度,即
长距离依赖学习
,因为self-attention是每个词和所有词都要计算attention,所以不管他们中间有多长距离,最大的路径长度也都只是级别。可以捕获长距离依赖关系。
3. 模型训练
关于模型的训练我主要想说2点,就是其训练使用的优化器,以及模型中使用到的3种正则化手段来防止训练过拟合的出现。
3.1 优化器
根据论文中的公式,是将Adam 优化器
与自定义的学习速率
配合使用的,其自定义的学习率计算表达式如下:
而pytorch中自带的learning scheduler都是基于epoch 变化的
,例如LambdaLR
、StepLR
等,没有基于step 变化
的,像tensorflow2中tf.keras.optimizers.schedules.LearningRateSchedule就是基于step的,可以实现更细粒度的调度。所以这里我pytorch实现Scheduler是继承基类torch.optim.lr_scheduler._LRScheduler
自己实现的:
class CustomSchedule(torch.optim.lr_scheduler._LRScheduler):
def __init__(self, optimizer, d_model, warm_steps=4):
self.optimizer = optimizer
self.d_model = d_model
self.warmup_steps = warm_steps
super(CustomSchedule, self).__init__(optimizer)
def get_lr(self):
"""
# rsqrt 函数用于计算 x 元素的平方根的倒数. 即= 1 / sqrt{x}
arg1 = torch.rsqrt(torch.tensor(self._step_count, dtype=torch.float32))
arg2 = torch.tensor(self._step_count * (self.warmup_steps ** -1.5), dtype=torch.float32)
dynamic_lr = torch.rsqrt(self.d_model) * torch.minimum(arg1, arg2)
"""
# print('*'*27, self._step_count)
arg1 = self._step_count ** (-0.5)
arg2 = self._step_count * (self.warmup_steps ** -1.5)
dynamic_lr = (self.d_model ** (-0.5)) * min(arg1, arg2)
# print('dynamic_lr:', dynamic_lr)
return [dynamic_lr for group in self.optimizer.param_groups]
绘制学习率查看一下其变化曲线:
optimizer = torch.optim.Adam(model.parameters(), lr=0, betas=(0.9, 0.98), eps=1e-9)
learning_rate = CustomSchedule(optimizer, d_model, warm_steps=4000)
lr_list = []
for i in range(1, 20000):
learning_rate.step()
lr_list.append(learning_rate.get_lr()[0])
plt.figure()
plt.plot(np.arange(1, 20000), lr_list)
plt.legend(['warmup=4000 steps'])
plt.ylabel("Learning Rate")
plt.xlabel("Train Step")
plt.show()
输出:
3.2 3种正则化
在训练中使用了3种正则化手段:
-
Residual Dropout
,在每个multi-head attention 或者point-wise feed forward sub-layer的output之后,在 add&norm 之前,即;
- 另外在encoder或者decoder的embeddings 和 the positional encodings 计算sum时会加入
dropout
,即:
# adding embedding and position encoding
x = self.embedding(x) # [b, inp_seq_len]=>[b, inp_seq_len, d_model]
# 缩放 embedding 原始论文的3.4节有提到: In the embedding layers, we multiply those weights by \sqrt{d_model}.
x *= torch.sqrt(torch.tensor(self.d_model, dtype=torch.float32))
x += self.pos_encoding[:, :inp_seq_len, :] # [b, inp_seq_len, d_model]
x = self.dropout(x)
Label Smoothing 标签平滑
其中K表示最后的linear layer & softmax进行多分类投影时的类别数,表示一个很小的数,那么标签平滑就是不要让target的表示
太“硬(hard)”了
,不要是那种非0即1的表示,而使用调整一下,使其更像一个
“分布”的表示,即更平滑
,这样能让模型更好的学习使模型输出与target更好的拟合。
4. 搭积木:搭建一个transformer
前面我们已经将transformer各个子模块实现了,下面我们就利用前面的模块,来搭建一个完整的transformer吧!
真是激动人心的时刻呢!
class Transformer(torch.nn.Module):
def __init__(self,
num_layers, # N个encoder layer
d_model,
num_heads,
dff, # 点式前馈网络内层fn的维度
input_vocab_size, # input此表大小(源语言(法语))
target_vocab_size, # target词表大小(目标语言(英语))
pe_input, # input max_pos_encoding
pe_target, # input max_pos_encoding
rate=0.1):
super(Transformer, self).__init__()
self.encoder = Encoder(num_layers,
d_model,
num_heads,
dff,
input_vocab_size,
pe_input,
rate)
self.decoder = Decoder(num_layers,
d_model,
num_heads,
dff,
target_vocab_size,
pe_target,
rate)
self.final_layer = torch.nn.Linear(d_model, target_vocab_size)
# inp [b, inp_seq_len]
# targ [b, targ_seq_len]
# enc_padding_mask [b, 1, 1, inp_seq_len]
# look_ahead_mask [b, 1, targ_seq_len, targ_seq_len]
# dec_padding_mask [b, 1, 1, inp_seq_len] # 注意这里的维度是inp_seq_len
def forward(self, inp, targ, enc_padding_mask, look_ahead_mask, dec_padding_mask):
enc_output = self.encoder(inp, enc_padding_mask) # =>[b, inp_seq_len, d_model]
dec_output, attention_weights = self.decoder(targ, enc_output, look_ahead_mask, dec_padding_mask)
# => [b, targ_seq_len, d_model],
# {'..block1': [b, num_heads, targ_seq_len, targ_seq_len],
# '..block2': [b, num_heads, targ_seq_len, inp_seq_len], ...}
final_output = self.final_layer(dec_output) # =>[b, targ_seq_len, target_vocab_size]
return final_output, attention_weights
# [b, targ_seq_len, target_vocab_size]
# {'..block1': [b, num_heads, targ_seq_len, targ_seq_len],
# '..block2': [b, num_heads, targ_seq_len, inp_seq_len], ...}
5. transformer应用实现:机器翻译(pytorch)
transformer的全部细节就已经讲解清楚了,如果您能坚持看到这里,相信一定有所收获。
接下来就使用我们自己的实现的transformer,来实现一个真实的机器翻译的任务,数据是:英语=>法语,在 《transformer(下)机器翻译+pytorch实现》
6. 模型评价
transformer的核心:self multi-head attention
transformer 优点:
- 无需对跨数据的时间/空间域的关系作出假设。It make no assumptions about the temporal/spatial relationships across the data. This is ideal for processing a set of objects
- 并行计算。Layer outputs can be calculated in parallel, instead of a series like an RNN
- 相距较远的词也能互相影响彼此的输出。Distant items can affect each other’s output without passing through many RNN-steps, or convolution layers
- 可以学习到长距离依赖关系。It can learn long-range dependencies. This is a challenge in many sequence tasks
transformer 缺点:
- 对于每一个step的
的输出,是由整个历史信息计算得出,而不再是当前输入和hidden,这可能效率较低。For a time-series, the output for a time-step is calculated from the entire history instead of only the inputs and current hidden-state. This may be less efficient
- 如果输入具有时间/空间域的关系,则需要加入位置编码,否则整个model也只能看作是一个词袋模型。If the input does have a temporal/spatial relationship, like text, some positional encoding must be added or the model will effectively see a bag of words
完整代码
完整代码请移步至: 我的github https:///qingyujean/Magic-NLPer,找到对应博客的完整代码,求赞求星求鼓励~~~
最后:如果本文中出现任何错误,请您一定要帮忙指正,感激~
参考
[1] Attention Is All You Need https://arxiv.org/abs/1706.03762
[2] The Illustrated Transformer http://jalammar.github.io/illustrated-transformer/
[3] https://tensorflow.google.cn/tutorials/text/transformer https://tensorflow.google.cn/tutorials/text/transformer
[4] 自然语言处理中的自注意力机制(Self-attention Mechanism)
[5] 深度学习中的注意力机制(2017版)
[6] 为什么 Bert 的三个 Embedding 可以进行相加? https://www.zhihu.com/question/374835153/answer/1526025182