《原始论文:Attention Is All You Need》
在2017年《Attention Is All You Need》论文里第一次提出Transformer之前,常用的序列模型都是基于卷积神经网络或者循环神经网络,表现最好的模型也是基于encoder- decoder框架的基础加上attention机制。
2018年10月,Google发出一篇论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》, BERT模型横空出世, 并横扫NLP领域11项任务的最佳成绩!
而在BERT中发挥重要作用的结构就是Transformer, 之后又相继出现XLNet,RoBERT等模型击败了BERT,但是他们的核心没有变,仍然是:Transformer.
相比之前占领市场的LSTM和GRU模型,Transformer有两个显著的优势:
测评比较图:
在著名的SOTA机器翻译榜单https://paperswithcode.com/上, 几乎所有排名靠前的模型都使用Transformer,
传统Seq2Seq模型中的神经网络一般使用RNN,但是RNN有个缺点:无法并行计算,导致计算特别耗时。
Transformer 是一种基于 Encoder-Decoder 结构、Attention机制、Self-Attention机制的Seq2Seq模型。Transformer 模型中不再使用RNN或CNN做数据转换,而是使用一种Self-Attention Layer结构做数据转换,从而实现并行计算。
Transformer is a kind of Seq2Seq model with “Self-attention” Layer instead of RNN/CNN 。
Transformer 在机器翻译任务上的表现超过了 RNN,CNN,只用 encoder-decoder 和 attention 机制就能达到很好的效果,最大的优点是可以高效地并行化。
Transformer模型的作用:基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务, 如机器翻译, 文本生成等. 同时又可以构建预训练语言模型,用于不同任务的迁移学习.
在接下来的架构分析中, 我们将假设使用Transformer模型架构处理从一种语言文本到另一种语言文本的翻译工作, 因此很多命名方式遵循NLP中的规则. 比如: Embeddding层将称作文本嵌入层, Embedding层产生的张量称为词嵌入张量, 它的最后一维将称作词向量等.
对于Transformer比传统序列模型RNN/LSTM具备优势的第一大原因就是强大的并行计算能力.
对于Transformer比传统序列模型RNN/LSTM具备优势的第二大原因就是强大的特征抽取能力.
seq2seq的两大缺陷
Transformer的改进
训练上的意义: 随着词嵌入维度d_k的增大, q * k 点积后的结果也会增大, 在训练时会将softmax函数推入梯度非常小的区域, 可能出现梯度消失的现象, 造成模型收敛困难.
数学上的意义: 假设q和k的统计变量是满足标准正态分布的独立随机变量, 意味着q和k满足均值为0, 方差为1. 那么q和k的点积结果就是均值为0, 方差为d_k, 为了抵消这种方差被放大d_k倍的影响, 在计算中主动将点积缩放1/sqrt(d_k), 这样点积后的结果依然满足均值为0, 方差为1.
这里我们分3个步骤来解释softmax的梯度问题:
第一步: softmax函数的输入分布是如何影响输出的.
a = 1时, y3 = 0.5761168847658291
a = 10时, y3 = 0.9999092083843412
a = 100时, y3 = 1.0
from math import exp
from matplotlib import pyplot as plt
import numpy as np
f = lambda x: exp(x * 2) / (exp(x) + exp(x) + exp(x * 2))
x = np.linspace(0, 100, 100)
y_3 = [f(x_i) for x_i in x]
plt.plot(x, y_3)
plt.show()
得到如下的曲线:第二步: softmax函数在反向传播的过程中是如何梯度求导的.
第三步: softmax函数出现梯度消失现象的原因.
针对为什么维度会影响点积的大小, 原始论文中有这样的一点解释如下:
To illustrate why the dot products get large, assume that the components of q and k
are independent random variables with mean 0 and variance 1. Then their doct product,
q*k = (q1k1+q2k2+......+q(d_k)k(d_k)), has mean 0 and variance d_k.
我们分两步对其进行一个推导, 首先就是假设向量q和k的各个分量是相互独立的随机变量, X = q_i, Y = k_i, X和Y各自有d_k个分量, 也就是向量的维度等于d_k, 有E(X) = E(Y) = 0, 以及D(X) = D(Y) = 1.
可以得到E(XY) = E(X)E(Y) = 0 * 0 = 0
根据上面的公式, 可以很轻松的得出q*k的均值为E(qk) = 0, D(qk) = d_k.
所以方差越大, 对应的qk的点积就越大, 这样softmax的输出分布就会更偏向最大值所在的分量.
最终的结论: 通过数学上的技巧将方差控制在1, 也就有效的控制了点积结果的发散, 也就控制了对应的梯度消失的问题!
首先Transformer的并行化主要体现在Encoder模块上.
1: 上图最底层绿色的部分, 整个序列所有的token可以并行的进行Embedding操作, 这一层的处理是没有依赖关系的.
2: 上图第二层土黄色的部分, 也就是Transformer中最重要的self-attention部分, 这里对于任意一个单词比如 x 1 x_1 x1, 要计算 x 1 x_1 x1对于其他所有token的注意力分布, 得到 z 1 z_1 z1. 这个过程是具有依赖性的, 必须等到序列中所有的单词完成Embedding才可以进行. 因此这一步是不能并行处理的. 但是从另一个角度看, 我们真实计算注意力分布的时候, 采用的都是矩阵运算, 也就是可以一次性的计算出所有token的注意力张量, 从这个角度看也算是实现了并行, 只是矩阵运算的"并行"和词嵌入的"并行"概念上不同而已.
3: 上图第三层蓝色的部分, 也就是前馈全连接层, 对于不同的向量z之间也是没有依赖关系的, 所以这一层是可以实现并行化处理的. 也就是所有的向量z输入Feed Forward网络的计算可以同步进行, 互不干扰.
其次Transformer的并行化也部分的体现在Decoder模块上.
1: Decoder模块在训练阶段采用了并行化处理. 其中Self-Attention和Encoder-Decoder Attention两个子层的并行化也是在进行矩阵乘法, 和Encoder的理解是一致的. 在进行Embedding和Feed Forward的处理时, 因为各个token之间没有依赖关系, 所以也是可以完全并行化处理的, 这里和Encoder的理解也是一致的.
2: Decoder模块在预测阶段基本上不认为采用了并行化处理. 因为第一个time step的输入只是一个"SOS", 后续每一个time step的输入也只是依次添加之前所有的预测token.
3: 注意: 最重要的区别是训练阶段目标文本如果有20个token, 在训练过程中是一次性的输入给Decoder端, 可以做到一些子层的并行化处理. 但是在预测阶段, 如果预测的结果语句总共有20个token, 则需要重复处理20次循环的过程, 每次的输入添加进去一个token, 每次的输入序列比上一次多一个token, 所以不认为是并行处理.
Transformer模型在输入时采用的是固定长度序列输入,且Transformer模型的时间复杂度和句子/序列长度的平方成正比(每一层的复杂度: O ( n 2 ⋅ d ) O(n^2·d) O(n2⋅d), n n n 为句子/序列长度, d d d 为词向量维度),因此一般序列长度都限制在最大512,因为太大的长度,模型训练的时间消耗太大。
此外Transformer模型又不像RNN这种结构,可以将最后时间输出的隐层向量作为整个序列的表示,然后作为下一序列的初始化输入。所以用Transformer训练语言模型时,不同的序列之间是没有联系的,因此这样的Transformer在长距离依赖的捕获能力是不够的,此外在处理长文本的时候,若是将文本分为多个固定长度的片段,对于连续的文本,这无异于将文本的整体性破坏了,导致了文本的碎片化,这也是Transformer-XL被提出的原因。
Transformer的复杂度与输入序列的长度的平方成正比:
O ( n 2 ⋅ d ) O(n^2·d) O(n2⋅d)
所以Transformer模型的输入序列的长度不能太大,否则会导致Transformer复杂度指数级增长,训练变慢。
Transformer各类模型中, n n n 一般最长取 512。
尽管 Transformer 最初是为翻译任务而构建的,但最近的趋势表明,它在语言建模上的应用也可以带来显著的效果。但是,为了获得最佳应用,需要对其架构进行一些修改。
为什么?Transformer 有什么问题?
与 RNN 相比,Transformer 的一项重大改进是其捕获长期依赖关系的能力。但是,Transformer 需要存储的中间步骤(梯度)信息比 RNN 要多的多,并且随着序列长度的增加而指数级增加【 O ( n 2 ⋅ d ) O(n^2·d) O(n2⋅d),其中 n n n 表示输入的文本长度】。换句话说,如果你试图一次输入整个文档,内存可能会爆炸(BOOM!)
为了防止出现此问题,早期有些做法是将文档分成固定大小的文本段(Segment),一次训练一段。这虽然解决了内存问题,但是破坏了模型捕获长期依赖关系的能力。例如句子 “The daughter had a nice umbrella | that her mother gave her”,如果 “daughter” 和 “her” 属于不同段。那么在编码 “her 时将无法知晓"daughter” 的信息。
我们知道GPT就是使用Transformer来进行语言模型的建模。因为Transformer要求输入是定长的词序列(不像RNN可以处理长度不确定的输入序列),太长的截断,不足的padding,这样我们把一个语料库的字符串序列切分成固定长度的segments。它有下面一些问题:
上图做是普通的Transformer语言模型的训练过程。假设Segment的长度为4,如图中我标示的:根据红色的路径,虽然 x 8 x_8 x8的最上层是受 x 1 x_1 x1影响的,但是由于固定的segment,x_8无法利用 x 1 x_1 x1 的信息。而预测的时候的上下文也是固定的4,比如预测 x 6 x_6 x6时我们需要根据 [ x 2 , x 3 , x 4 , x 5 ] [x_2,x_3,x_4,x_5] [x2,x3,x4,x5]来计算,接着把预测的结果作为下一个时刻的输入。接着预测 x 7 x_7 x7的时候需要根据 [ x 3 , x 4 , x 5 , x 6 ] [x_3,x_4,x_5,x_6] [x3,x4,x5,x6]完全进行重新的计算。之前的计算结果一点也用不上。
如何解决这个问题呢?Transformer-XL。
Transformer总体架构可分为四个部分:
Embedding Layer(文本嵌入层)的作用:无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表示, 由一维转为多维,希望在高维空间捕捉词汇间的关系.
位置编码器的作用:因为在Transformer的编码器结构中, 并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中, 以弥补位置信息的缺失.
需要使用位置嵌入的原因也很简单,因为 Transformer 摈弃了 RNN 的结构,因此需要一个东西来标记各个字之间的时序 or 位置关系,而这个东西,就是位置嵌入。
位置编码器中使用sin()函数、cos()函数将所添加的Positional Encoding张量的值域范围控制1到-1,这很好的控制了嵌入数值的大小, 有助于梯度的快速计算.
Transformer中直接采用正弦函数和余弦函数来编码位置信息, 如下图所示:
比如: d m o d e l = 512 d_{model} = 512 dmodel=512,句子长度为20,则该句子中第3个单词的位置编码信息(所有512个维度的各个维度的位置编码)为:
[ s i n ( 3 1000 0 2 × 0 / 512 ) , c o s ( 3 1000 0 2 × 0 / 512 ) , s i n ( 3 1000 0 2 × 1 / 512 ) , c o s ( 3 1000 0 2 × 1 / 512 ) , s i n ( 3 1000 0 2 × 2 / 512 ) , c o s ( 3 1000 0 2 × 2 / 512 ) , . . . , s i n ( 3 1000 0 2 × 253 / 512 ) , c o s ( 3 1000 0 2 × 253 / 512 ) s i n ( 3 1000 0 2 × 254 / 512 ) , c o s ( 3 1000 0 2 × 254 / 512 ) ] \begin{aligned} &[\\ &\color{red}{sin(\cfrac{3}{10000^{2×0/512}})},\color{blue}{cos(\cfrac{3}{10000^{2×0/512}})},\\ &\color{red}{sin(\cfrac{3}{10000^{2×1/512}})},\color{blue}{cos(\cfrac{3}{10000^{2×1/512}})},\\ &\color{red}{sin(\cfrac{3}{10000^{2×2/512}})},\color{blue}{cos(\cfrac{3}{10000^{2×2/512}})},\\ &...,\\ &\color{red}{sin(\cfrac{3}{10000^{2×253/512}})},\color{blue}{cos(\cfrac{3}{10000^{2×253/512}})}\\ &\color{red}{sin(\cfrac{3}{10000^{2×254/512}})},\color{blue}{cos(\cfrac{3}{10000^{2×254/512}})}\\ ] \end{aligned} ][sin(100002×0/5123),cos(100002×0/5123),sin(100002×1/5123),cos(100002×1/5123),sin(100002×2/5123),cos(100002×2/5123),...,sin(100002×253/5123),cos(100002×253/5123)sin(100002×254/5123),cos(100002×254/5123)
需要注意: 三角函数应用在此处的一个重要的优点, 因为对于任意的PE(pos+k), 都可以表示为PE(pos)的线性函数, 大大方便计算. 而且周期性函数不受序列长度的限制, 也可以增强模型的泛化能力.
class PositionEncoding(nn.Module):
def __init__(self, max_seq_len, word_embedding_size): # max_seq_len: 每个句子的最大长度
super(PositionEncoding, self).__init__()
pos_enc = np.array([[pos / np.power(10000, 2.0 * (j // 2) / word_embedding_size) for j in range(word_embedding_size)] for pos in range(max_seq_len)])
pos_enc[:, 0::2] = np.sin(pos_enc[:, 0::2])
pos_enc[:, 1::2] = np.cos(pos_enc[:, 1::2])
pad_row = np.zeros([1, word_embedding_size])
pos_enc = np.concatenate([pad_row, pos_enc]).astype(np.float32)
# additional single row for PAD idx
self.pos_enc = nn.Embedding(max_seq_len + 1, word_embedding_size)
# fix positional encoding: exclude weight from grad computation
self.pos_enc.weight = nn.Parameter(torch.from_numpy(pos_enc), requires_grad=False)
def forward(self, input_len):
max_len = torch.max(input_len)
tensor = torch.cuda.LongTensor if input_len.is_cuda else torch.LongTensor
input_pos = tensor([list(range(1, len + 1)) + [0] * (max_len.item() - len) for len in input_len.cpu().numpy()])
# 前面123,后面补0
return self.pos_enc(input_pos)
什么是注意力:我们观察事物时,之所以能够快速判断一种事物(当然允许判断是错误的), 是因为我们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,而并非是从头到尾的观察一遍事物后,才能有判断结果. 正是基于这样的理论,就产生了注意力机制.
注意力计算规则需要三个指定的输入Q(query), K(key), V(value), 然后通过计算公式得到注意力的结果, 这个结果代表query在key和value作用下的注意力表示.
常见的注意力计算规则有三种:
将Q,K进行纵轴拼接, 做一次线性变化, 再使用softmax处理获得结果最后与V做张量乘法.
A t t e n t i o n ( Q , K , V ) = S o f t m a x ( L i n e a r ( [ Q , K ] ) ) ⋅ V Attention(Q,K,V)=Softmax(Linear([Q,K]))·V Attention(Q,K,V)=Softmax(Linear([Q,K]))⋅V
将Q,K进行纵轴拼接, 做一次线性变化后再使用tanh函数激活, 然后再进行内部求和, 最后使用softmax处理获得结果再与V做张量乘法.
A t t e n t i o n ( Q , K , V ) = S o f t m a x ( s u m ( t a n h ( L i n e a r ( [ Q , K ] ) ) ) ) ⋅ V Attention(Q,K,V)=Softmax(sum(tanh(Linear([Q,K]))))·V Attention(Q,K,V)=Softmax(sum(tanh(Linear([Q,K]))))⋅V
A t t e n t i o n ( Q , K , V ) = S o f t m a x ( Q ⋅ K d k ) ⋅ V \color{red}{Attention(Q,K,V)=Softmax(\cfrac{Q·K}{\sqrt{d_k}})·V} Attention(Q,K,V)=Softmax(dkQ⋅K)⋅V
Q, K, V的比喻解释,假如我们有一个问题: 给出一段文本,使用一些关键词对它进行描述!
一般注意力机制: 刚刚我们说到key和value一般情况下默认是相同,与query是不同的,这种是我们一般注意力机制; 自注意力机制:当query与key和value三者都相同时,这种情况我们称为自注意力机制
注意力机制:注意力机制是注意力计算规则能够应用的深度学习网络的载体, 除了注意力计算规则外, 还包括一些必要的全连接层以及相关张量处理, 使其与应用网络融为一体. 使用自注意力计算规则的注意力机制称为自注意力机制.
class ScaledDotProductAttention(nn.Module):
def __init__(self, d_k, dropout=.1):
super(ScaledDotProductAttention, self).__init__()
self.scale_factor = np.sqrt(d_k)
self.softmax = nn.Softmax(dim=-1)
self.dropout = nn.Dropout(dropout)
def forward(self, q, k, v, attn_mask=None):
# q、k、v的形状:
# q: [b_size x n_heads x len_q x d_k]:torch.Size([batchSize * self.num_heads, seqLen, head_dim])
# k: [b_size x n_heads x len_k x d_k]:torch.Size([batchSize * self.num_heads, seqLen, head_dim])
# v: [b_size x n_heads x len_v x d_v]:torch.Size([batchSize * self.num_heads, seqLen, head_dim])
# note: (len_k == len_v)
# attention_weight: [b_size x n_heads x len_q x len_k]
scores = torch.matmul(q, k.transpose(-1, -2)) / self.scale_factor
if attn_mask is not None:
assert attn_mask.size() == scores.size()
# 将等于1的地方设置为负无穷 attn_mask = seq_k.data.eq(0).unsqueeze(1),等于0为True
scores.masked_fill_(attn_mask, -1e9)
attention_weight = self.dropout(self.softmax(scores))
# outputs: [b_size x n_heads x len_q x d_v]
context_vector = torch.matmul(attention_weight, v)
return context_vector, attention_weight
[ k 1 k 2 k 3 k 4 ] = w k ⋅ [ a 1 a 2 a 3 a 4 ] [ q 1 q 2 q 3 q 4 ] = w q ⋅ [ a 1 a 2 a 3 a 4 ] [ v 1 v 2 v 3 v 4 ] = w v ⋅ [ a 1 a 2 a 3 a 4 ] \begin{aligned} \begin{bmatrix}k^1&k^2&k^3&k^4\end{bmatrix}=w^k·\begin{bmatrix}a^1&a^2&a^3&a^4\end{bmatrix}\\\\ \begin{bmatrix}q^1&q^2&q^3&q^4\end{bmatrix}=w^q·\begin{bmatrix}a^1&a^2&a^3&a^4\end{bmatrix}\\\\ \begin{bmatrix}v^1&v^2&v^3&v^4\end{bmatrix}=w^v·\begin{bmatrix}a^1&a^2&a^3&a^4\end{bmatrix} \end{aligned} [k1k2k3k4]=wk⋅[a1a2a3a4][q1q2q3q4]=wq⋅[a1a2a3a4][v1v2v3v4]=wv⋅[a1a2a3a4]
[ α 11 α 12 α 13 α 14 α 21 α 22 α 23 α 24 α 31 α 32 α 33 α 34 α 41 α 42 α 43 α 44 ] = { [ k 1 k 2 k 3 k 4 ] ⋅ [ q 1 q 2 q 3 q 4 ] } d \begin{aligned} \begin{bmatrix}α_{11}&α_{12}&α_{13}&α_{14}\\α_{21}&α_{22}&α_{23}&α_{24}\\α_{31}&α_{32}&α_{33}&α_{34}\\α_{41}&α_{42}&α_{43}&α_{44}\end{bmatrix} =\cfrac{\{\begin{bmatrix}k^1\\k^2\\k^3\\k^4\end{bmatrix}·\begin{bmatrix}q^1&q^2&q^3&q^4\end{bmatrix}\}}{\sqrt{d}} \end{aligned} ⎣ ⎡α11α21α31α41α12α22α32α42α13α23α33α43α14α24α34α44⎦ ⎤=d{⎣ ⎡k1k2k3k4⎦ ⎤⋅[q1q2q3q4]}
其中: d d d is the dim of q q q and k k k,一般就是词向量维度
α ^ i j = e α i j ∑ k = 1 4 e α k j \hat{α}_{ij}=\cfrac{e^{α_{ij}}}{\sum^4_{k=1}e^{α_{kj}}} α^ij=∑k=14eαkjeαij
[ b 1 b 2 b 3 b 4 ] = [ v 1 v 2 v 3 v 4 ] ⋅ [ α ^ 11 α ^ 12 α ^ 13 α ^ 14 α ^ 21 α ^ 22 α ^ 23 α ^ 24 α ^ 31 α ^ 32 α ^ 33 α ^ 34 α ^ 41 α ^ 42 α ^ 43 α ^ 44 ] \begin{aligned} \begin{bmatrix}b^1&b^2&b^3&b^4\end{bmatrix}= \begin{bmatrix}v^1&v^2&v^3&v^4\end{bmatrix}·\begin{bmatrix}\hat{α}_{11}&\hat{α}_{12}&\hat{α}_{13}&\hat{α}_{14}\\\hat{α}_{21}&\hat{α}_{22}&\hat{α}_{23}&\hat{α}_{24}\\\hat{α}_{31}&\hat{α}_{32}&\hat{α}_{33}&\hat{α}_{34}\\\hat{α}_{41}&\hat{α}_{42}&\hat{α}_{43}&\hat{α}_{44}\end{bmatrix} \end{aligned} [b1b2b3b4]=[v1v2v3v4]⋅⎣ ⎡α^11α^21α^31α^41α^12α^22α^32α^42α^13α^23α^33α^43α^14α^24α^34α^44⎦ ⎤
self-attention是一种通过自身和自身进行关联的attention机制, 从而得到更好的representation来表达自身.
self-attention是attention机制的一种特殊情况:在self-attention中, Q=K=V, 序列中的每个单词(token)都和该序列中的其他所有单词(token)进行attention规则的计算。
attention机制计算的特点在于, 可以直接跨越一句话中不同距离的token, 可以远距离的学习到序列的知识依赖和语序结构.
关于self-attention为什么要使用(Q, K, V)三元组而不是其他形式:
采用Multi-head Attention的原因
Multi-head Attention的计算方式:
多头注意力机制的作用:这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达。
实验表明多头注意力机制确实可以提升模型效果.
上述的多头self-attention, 对应的数学公式形式如下:
从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层,其实并不是,只有使用了一组线性变化层
k 1 i = w 1 k ⋅ k i k 2 i = w 2 k ⋅ k i q 1 i = w 1 q ⋅ q i q 2 i = w 2 q ⋅ q i v 1 i = w 1 v ⋅ v i v 2 i = w 2 v ⋅ v i \begin{aligned} k^i_1=w^k_1·k^i \qquad k^i_2=w^k_2·k^i\\ q^i_1=w^q_1·q^i \qquad q^i_2=w^q_2·q^i\\ v^i_1=w^v_1·v^i \qquad v^i_2=w^v_2·v^i\\ \end{aligned} k1i=w1k⋅kik2i=w2k⋅kiq1i=w1q⋅qiq2i=w2q⋅qiv1i=w1v⋅viv2i=w2v⋅vi
多头注意力机制中需要使用多个相同的线性层, 可以使用克隆函数clones来生成一个克隆线性层列表。
在Transformer中 “前馈全连接层” 就是具有两层线性层的全连接网络,中间有一个Relu激活函数, 对应的数学公式形式如下:
注意: 原版论文中的前馈全连接层, 输入和输出的维度均为 d m o d e l d_{model} dmodel = 512, 层内的连接维度 d f f d_{ff} dff = 2048, 均采用4倍的大小关系.
前馈全连接层的作用:单纯的多头注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力.
Add & Norm模块接在每一个Encoder Block和Decoder Block中的每一个子层的后面. 具体来说Add表示残差连接, Norm表示LayerNorm.
Add残差连接的作用: 和其他神经网络模型中的残差连接作用一致, 都是为了将信息传递的更深, 增强模型的拟合能力. 试验表明残差连接的确增强了模型的表现.
Norm规范化层的作用:规范化层是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢. 因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.
元素的规范化值=(元素的原始值-元素所在维度均值)/元素所在维度方差
x ^ = x − μ σ = 元素的原始值 − 元素所在维度均值 元素所在维度方差 \hat{x}=\cfrac{x-μ}{σ}=\cfrac{\text{元素的原始值}-\text{元素所在维度均值}}{\text{元素所在维度方差}} x^=σx−μ=元素所在维度方差元素的原始值−元素所在维度均值
class LayerNormalization(nn.Module):
def __init__(self, d_hid, eps=1e-6):
super(LayerNormalization, self).__init__()
self.gamma = nn.Parameter(torch.ones(d_hid))
self.beta = nn.Parameter(torch.zeros(d_hid))
self.eps = eps
def forward(self, z):
mean = z.mean(dim=-1, keepdim=True, )
std = z.std(dim=-1, keepdim=True, )
ln_out = (z - mean) / (std + self.eps)
ln_out = self.gamma * ln_out + self.beta
return ln_out
如下图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.
计算注意力权重之前,对Q/K/V的形状要进行转置操作,对第二维和第三维进行转置操作,为了让代表句子长度维度和词向量维度能够相邻,这样注意力机制才能找到词义与句子位置的关系。
import torch
import torch.nn as nn
import torch.nn.init as init
from transformer.modules import Linear
from transformer.modules import ScaledDotProductAttention
from transformer.modules import LayerNormalization
'''
编码器由N个编码器层堆叠而成(经典的Transformer结构中的Encoder模块包含6个Encoder Block, 6个一模一样的Encoder Block层层堆叠在一起, 共同组成完整的Encoder)
每个编码器层(Layer)由两个子层(SubLayer)连接结构组成:
第一个子层连接结构(SubLayer)包括一个多头自注意力子层和规范化层以及一个残差连接
第二个子层连接结构(SubLayer)包括一个前馈全连接子层和规范化层以及一个残差连接
'''
class MultiHeadAttention(nn.Module):
def __init__(self, d_k, d_v, d_model, n_heads, dropout):
super(MultiHeadAttention, self).__init__()
self.d_k = d_k
self.d_v = d_v
self.d_model = d_model
self.n_heads = n_heads
self.w_q = Linear(d_model, d_k * n_heads)
self.w_k = Linear(d_model, d_k * n_heads)
self.w_v = Linear(d_model, d_v * n_heads)
self.attention = ScaledDotProductAttention(d_k, dropout)
def forward(self, q, k, v, attention_mask):
b_size = q.size(0)
# 进入多头处理环节
# 做完线性变换后,开始为每个头分割输入,这里使用view方法对线性变换的结果进行维度重塑,多加了一个维度 n_heads,代表头数。这样就意味着每个头可以获得一部分词特征组成的句子。
# 然后对第二维和第三维进行转置操作,为了让代表句子长度维度和词向量维度能够相邻,这样注意力机制才能找到词义与句子位置的关系,
q_s = self.w_q(q).view(b_size, -1, self.n_heads, self.d_k).transpose(1, 2) # q: [b_size x len_q x d_model] ----> q_s: [b_size x len_q x n_heads x d_k] ----> q_s: [b_size x n_heads x len_q x d_k]
k_s = self.w_k(k).view(b_size, -1, self.n_heads, self.d_k).transpose(1, 2) # k: [b_size x len_k x d_model] ----> k_s: [b_size x len_k x n_heads x d_k] ----> k_s: [b_size x n_heads x len_k x d_k]
v_s = self.w_v(v).view(b_size, -1, self.n_heads, self.d_v).transpose(1, 2) # v: [b_size x len_k x d_model] ----> v_s: [b_size x len_k x n_heads x d_v] ----> v_s: [b_size x n_heads x len_k x d_v]
# 扩展 attention_mask 的维度,使之与 q_s、k_s、v_s 维度一致
if attention_mask.dim() == 3: # attention_mask: [b_size x len_q x len_k]
attention_mask = attention_mask.unsqueeze(1).repeat(1, self.n_heads, 1, 1)
# 得到每个头的输入后,接下来就是将他们传入到attention中
context_vector, attention_weight = self.attention(q_s, k_s, v_s, attention_mask=attention_mask) # context_vector: [b_size x n_heads x len_q x d_v], attention_weight: [b_size x n_heads x len_q x len_k]
# 合并多头分别计算attention的结果
context_vector = context_vector.transpose(1, 2) # 通过多头注意力计算后,得到每个头计算结果组成的(len_q x d_v)维张量,我们需要将其转换为输入的形状以方便后续的计算【进行第一步处理环节的逆操作,对第二和第三维进行转置】[b_size x n_heads x len_q x d_v]---->[b_size x len_q x n_heads x d_v]
context_vector = context_vector.contiguous().view(b_size, -1, self.n_heads * self.d_v) # # 使用view重塑形状,变成和输入形状相同,将最后一维大小恢复为embedding_dim【contiguous方法的作用就是能够让转置后的张量应用view方法,否则将无法直接使用】 context_vector: [b_size x len_q x n_heads * d_v]
return context_vector, attention_weight
class MultiHeadAttnAddNormSubLayer(nn.Module):
'''
残差链接 & 规范化层 & 多头注意力层
'''
def __init__(self, d_k, d_v, d_model, n_heads, dropout):
super(MultiHeadAttnAddNormSubLayer, self).__init__()
self.n_heads = n_heads
self.multihead_attention = MultiHeadAttention(d_k, d_v, d_model, n_heads, dropout)
self.proj = Linear(n_heads * d_v, d_model)
self.dropout = nn.Dropout(dropout)
self.layer_norm = LayerNormalization(d_model)
def forward(self, q, k, v, attention_mask): # q: [b_size x len_q x d_model]; k: [b_size x len_k x d_model]; v: [b_size x len_v x d_model] note (len_k == len_v)
residual = q
q = self.layer_norm(q)
context_vector, attention_weight = self.multihead_attention(q, k, v, attention_mask=attention_mask) # context_vector: a tensor of shape [b_size x len_q x n_heads * d_v]
output = self.dropout(self.proj(context_vector)) # project back to the residual size, outputs: [b_size x len_q x d_model]
output = self.layer_norm(residual + output)
return output, attention_weight
class FeedForwardAddNormSubLayer(nn.Module):
'''
残差链接 & 规范化层 & 前馈层
'''
def __init__(self, d_model, d_ff, dropout=0.1):
super(FeedForwardAddNormSubLayer, self).__init__()
self.relu = nn.ReLU()
self.conv1 = nn.Conv1d(in_channels=d_model, out_channels=d_ff, kernel_size=1)
self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_model, kernel_size=1)
self.dropout = nn.Dropout(dropout)
self.layer_norm = LayerNormalization(d_model)
def forward(self, inputs):
# inputs: [b_size x len_q x d_model]
residual = inputs
output = self.dropout(self.conv2(self.relu(self.conv1(inputs.transpose(1, 2)))).transpose(1, 2))
return self.layer_norm(residual + output)
编码器层的作用:作为编码器的组成单元, 每个编码器层完成一次对输入的特征提取过程, 即编码过程。
import torch.nn as nn
from transformer.sublayers import MultiHeadAttnAddNormSubLayer
from transformer.sublayers import MultiBranchAttnAddNormSubLayer
from transformer.sublayers import FeedForwardAddNormSubLayer
class EncoderLayer(nn.Module):
def __init__(self, d_k, d_v, hidden_size, d_ff, n_heads, dropout=0.1):
super(EncoderLayer, self).__init__()
self.encoder_self_attention = MultiHeadAttnAddNormSubLayer(d_k, d_v, hidden_size, n_heads, dropout)
self.feed_forward = FeedForwardAddNormSubLayer(hidden_size, d_ff, dropout)
def forward(self, encoder_inputs, encoder_self_attention_mask):
encoder_outputs, encoder_self_attention_weight = self.encoder_self_attention(encoder_inputs, encoder_inputs, encoder_inputs, attention_mask=encoder_self_attention_mask)
encoder_outputs = self.feed_forward(encoder_outputs)
# (batch_size , sen_len, hidden_size) (batch_size, len_q, len_k)
return encoder_outputs, encoder_self_attention_weight
class PositionEncoding(nn.Module):
def __init__(self, max_seq_len, word_embedding_size): # max_seq_len: 每个句子的最大长度
super(PositionEncoding, self).__init__()
pos_enc = np.array([[pos / np.power(10000, 2.0 * (j // 2) / word_embedding_size) for j in range(word_embedding_size)] for pos in range(max_seq_len)])
pos_enc[:, 0::2] = np.sin(pos_enc[:, 0::2])
pos_enc[:, 1::2] = np.cos(pos_enc[:, 1::2])
pad_row = np.zeros([1, word_embedding_size])
pos_enc = np.concatenate([pad_row, pos_enc]).astype(np.float32)
# additional single row for PAD idx
self.pos_enc = nn.Embedding(max_seq_len + 1, word_embedding_size)
# fix positional encoding: exclude weight from grad computation
self.pos_enc.weight = nn.Parameter(torch.from_numpy(pos_enc), requires_grad=False)
def forward(self, input_len):
max_len = torch.max(input_len)
tensor = torch.cuda.LongTensor if input_len.is_cuda else torch.LongTensor
input_pos = tensor([list(range(1, len + 1)) + [0] * (max_len.item() - len) for len in input_len.cpu().numpy()])
# 前面123,后面补0
return self.pos_enc(input_pos)
import torch
import torch.nn as nn
import numpy as np
from transformer.modules import PositionEncoding
from transformer.layers import EncoderLayer
def get_attention_paddiing_mask(seq_q, seq_k):
assert seq_q.dim() == 2 and seq_k.dim() == 2
b_size, len_q = seq_q.size()
b_size, len_k = seq_k.size()
pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) # b_size x 1 x len_k
return pad_attn_mask.expand(b_size, len_q, len_k) # b_size x len_q x len_k
class Encoder(nn.Module):
def __init__(self, n_layers, d_k, d_v, d_model, d_ff, n_heads, max_seq_len, src_vocab_size, dropout=0.1, weighted=False):
# n_layers: Layer层数量
# d_k,d_v:用于计算Attention的K、V的维度;
# d_model: 模型中间隐藏层的维度;
# d_ff: FeedForward网络的中间隐藏层的维度;
# n_head: 多头注意力机制的头数;
# max_seq_len: 输入文本最大长度;
# src_vocab_size: 源语言词表大小;
super(Encoder, self).__init__()
self.d_model = d_model
self.src_emb = nn.Embedding(num_embeddings=src_vocab_size, embedding_dim=d_model, padding_idx=0)
self.pos_emb = PositionEncoding(max_seq_len * 10, d_model)
self.dropout_emb = nn.Dropout(dropout)
self.encoder_layer = EncoderLayer if not weighted else WeightedEncoderLayer
self.encoder_layers = nn.ModuleList([self.encoder_layer(d_k, d_v, d_model, d_ff, n_heads, dropout) for _ in range(n_layers)])
def forward(self, encoder_input_ids, encoder_inputs_len, return_attention_weight=False): # encoder_input_ids: (batch_size, sen_len)
encoder_inputs = self.dropout_emb(self.src_emb(encoder_input_ids) + self.pos_emb(encoder_inputs_len)) # 序列化后的输入文本经过文本嵌入层、位置编码层、Dropout层
encoder_self_attention_mask = get_attention_paddiing_mask(encoder_input_ids, encoder_input_ids)
encoder_self_attention_weights = [] # 用于存放每一层Layer得到的encoder_self_attention_weight
# 将encoder_inputs依次经过多层 Encoder Layer
for layer_idx, encoder_layer in enumerate(self.encoder_layers):
if layer_idx == 0: # 第一层decoder_layer是以位置编码层的输出作为输入,之后所有的decoder_layer都是以上一层的decoder_layer的输出作为输入
encoder_inputs = encoder_inputs
else:
encoder_inputs = encoder_outputs
encoder_outputs, encoder_self_attention_weight = encoder_layer(encoder_inputs=encoder_inputs, encoder_self_attention_mask=encoder_self_attention_mask)
if return_attention_weight:
encoder_self_attention_weights.append(encoder_self_attention_weight)
# encoder_outputs: 输入文本中的所有序列化的词汇id转为词向量(batch_size, src_sen_len, d_model)
return encoder_outputs, encoder_self_attention_weights
解码器的作用:根据编码器的结果以及上一次预测的结果, 对下一次可能出现的’值’进行特征表示。
import torch
import torch.nn as nn
import numpy as np
from transformer.modules import PositionEncoding
from transformer.layers import DecoderLayer
def get_attention_paddiing_mask(seq_q, seq_k):
assert seq_q.dim() == 2 and seq_k.dim() == 2
b_size, len_q = seq_q.size()
b_size, len_k = seq_k.size()
pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) # b_size x 1 x len_k
return pad_attn_mask.expand(b_size, len_q, len_k) # b_size x len_q x len_k
def get_attention_subsequent_mask(seq):
assert seq.dim() == 2
attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
subsequent_mask = np.triu(np.ones(attn_shape), k=1)
subsequent_mask = torch.from_numpy(subsequent_mask).byte()
if seq.is_cuda:
subsequent_mask = subsequent_mask.cuda()
return subsequent_mask
class Decoder(nn.Module):
def __init__(self, n_layers, d_k, d_v, d_model, d_ff, n_heads, max_seq_len, tgt_vocab_size, dropout=0.1, weighted=False):
# n_layers: Decoder的层数;
# d_k,d_v:用于计算Attention的K、V的维度;
# d_model: 模型中间隐藏层的维度;
# d_ff: FeedForward网络的中间隐藏层的维度;
# n_head: 多头注意力机制的头数;
# max_seq_len: 输出文本最大长度;
# src_vocab_size: 目标语言词表大小;
super(Decoder, self).__init__()
self.d_model = d_model
self.tgt_emb = nn.Embedding(num_embeddings=tgt_vocab_size, embedding_dim=d_model, padding_idx=0) # 文本嵌入层
self.pos_emb = PositionEncoding(max_seq_len * 10, d_model) # 位置编码层
self.dropout_emb = nn.Dropout(dropout) # Dropout层
self.decoder_layer = DecoderLayer if not weighted else WeightedDecoderLayer # 初始化 Decoder Layer
self.decoder_layers = nn.ModuleList([self.decoder_layer(d_k, d_v, d_model, d_ff, n_heads, dropout) for _ in range(n_layers)]) # 构建多层 Decoder Layer
def forward(self, decoder_input_ids, decoder_inputs_len, encoder_input_ids, encoder_outputs, return_attention_weight=False, is_initial=False):
# 通过文本嵌入层、位置编码层、Dropout层将 decoder_input_ids 进行编码, 将Decoder的序列化的输入文本进行向量化 (batch_size, sen_len) --> (batch_size, sen_len, d_model)
decoder_inputs = self.dropout_emb(self.tgt_emb(decoder_input_ids) + self.pos_emb(decoder_inputs_len))
# 计算 Mask
decoder_self_attention_padding_mask = get_attention_paddiing_mask(decoder_input_ids, decoder_input_ids) # Transformer 的 Decoder中的 Self-Attention 都需要忽略 padding 部分的影响
decoder_self_attention_subsequent_mask = get_attention_subsequent_mask(decoder_input_ids) # Subsequent掩码张量的作用:在transformer中, 掩码张量的主要作用在应用attention时,有一些生成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,我们会进行遮掩.
decoder_self_attention_mask = torch.gt((decoder_self_attention_padding_mask + decoder_self_attention_subsequent_mask), 0)
decoder_encoder_attention_padding_mask = get_attention_paddiing_mask(decoder_input_ids, encoder_input_ids) # Transformer 的 Encoder、Decoder之间的 Attention 需要忽略 padding 部分的影响
# 用于存放每一层得到的 decoder_self_attention_weights、decoder_encoder_attention_weights
decoder_self_attention_weights, decoder_encoder_attention_weights = [], []
# 将decoder_inputs依次经过多层Decoder Layer
for layer_idx, decoder_layer in enumerate(self.decoder_layers):
# 第一层decoder_layer是以位置编码层的输出作为输入,之后所有的decoder_layer都是以上一层的decoder_layer的输出作为输入
if layer_idx == 0:
decoder_inputs = decoder_inputs
else:
decoder_inputs = decoder_outputs
# decoder_outputs: 经过当前DecoderLayer后得到的输出值 (batch_size, max_tgt_sen_len, d_model)
# decoder_self_attention_weight: 经过当前DecoderLayer后得到的decoder_inputs的多头自注意力权重 (batch_size, n_head, tgt_sen_len, tgt_sen_len)
# decoder_encoder_attention_weight: 经过当前DecoderLayer后得到的decoder_inputs与encoder_outputs的多头注意力权重 (batch_size, n_head, tgt_sen_len, src_sen_len)
decoder_outputs, decoder_self_attention_weight, decoder_encoder_attention_weight = decoder_layer(decoder_inputs=decoder_inputs, encoder_outputs=encoder_outputs, decoder_self_attention_mask=decoder_self_attention_mask, decoder_encoder_attention_padding_mask=decoder_encoder_attention_padding_mask, is_initial=is_initial)
if return_attention_weight:
decoder_self_attention_weights.append(decoder_self_attention_weight)
decoder_encoder_attention_weights.append(decoder_encoder_attention_weight)
# decoder_outputs: 经过Decoder,得到的输出(batch_size, max_tgt_sen_len, d_model)【max_tgt_sen_len是本batch中最长文本的长度】
return decoder_outputs, decoder_self_attention_weights, decoder_encoder_attention_weights
Decoder模块的多头self-attention需要做look-ahead-mask, 因为在预测的时候"不能看见未来的信息", 所以要将当前的token和之后的token全部mask.
掩代表遮掩,码就是我们张量中的数值,它的尺寸不定(大小由传入的size参数决定),里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换, 它的表现形式是一个张量.
Subsequent掩码张量的作用:在transformer中, 掩码张量的主要作用在应用attention时,有一些生成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,我们会进行遮掩.
这一层区别于自注意力机制的Q = K = V, 此处矩阵Q来源于Decoder端经过上一个Decoder Block的输出, 而矩阵K, V则来源于Encoder端的输出, 造成了Q != K = V的情况。这样设计是为了让Decoder端的token能够给予Encoder端对应的token更多的关注.
Decoder Block中有2个注意力层的作用: 多头self-attention层是为了拟合Decoder端自身的信息, 而Encoder-Decoder attention层是为了整合Encoder和Decoder的信息.
解码器层的作用:作为解码器的组成单元, 每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程。解码器层的最终输出是由”编码器层的最终输出“、”目标数据张量“一同作为解码器层的输入的特征提取结果。
import torch.nn as nn
from transformer.sublayers import MultiHeadAttnAddNormSubLayer
from transformer.sublayers import MultiBranchAttnAddNormSubLayer
from transformer.sublayers import FeedForwardAddNormSubLayer
from transformer.modules import LayerNormalization
class DecoderLayer(nn.Module):
def __init__(self, d_k, d_v, hidden_size, d_ff, n_heads, dropout=0.1):
super(DecoderLayer, self).__init__()
self.decoder_self_attention = MultiHeadAttnAddNormSubLayer(d_k, d_v, hidden_size, n_heads, dropout)
self.decoder_encoder_attention = MultiHeadAttnAddNormSubLayer(d_k, d_v, hidden_size, n_heads, dropout)
self.feed_forward = FeedForwardAddNormSubLayer(hidden_size, d_ff, dropout)
self.layer_norm = LayerNormalization(hidden_size)
def forward(self, decoder_inputs, encoder_outputs, decoder_self_attention_mask, decoder_encoder_attention_padding_mask, is_initial):
# decoder_inputs: 表示Decoder端的目标文本序列化后的所有词汇的向量表示(batch_size, tgt_sen_len, d_model)
# encoder_outputs: 表示Encoder端的最终得到的向量表示(batch_size, src_sen_len, d_model)
# 因为要做两次attention,所以分了选择
if is_initial:
decoder_outputs, decoder_self_attention_weight = self.decoder_self_attention(q=decoder_inputs, k=decoder_inputs, v=decoder_inputs, attention_mask=decoder_self_attention_mask) # 自注意力机制的Q = K = V (batch_size, sen_len, d_model)
decoder_outputs, decoder_encoder_attention_weight = self.decoder_encoder_attention(q=decoder_outputs, k=encoder_outputs, v=encoder_outputs, attention_mask=decoder_encoder_attention_padding_mask) # 这一层区别于自注意力机制的Q = K = V, 此处矩阵Q来源于Decoder端经过上一个Decoder Block的输出, 而矩阵K, V则来源于Encoder端的输出, 造成了Q != K = V的情况
else:
decoder_outputs, decoder_encoder_attention_weight = self.decoder_encoder_attention(q=decoder_inputs, k=encoder_outputs, v=encoder_outputs, attention_mask=decoder_encoder_attention_padding_mask)
decoder_self_attention_weight = None
decoder_outputs_ = self.feed_forward(decoder_outputs)
decoder_outputs_ = self.layer_norm(decoder_outputs_ + decoder_outputs) # add & self.layer_norm
# decoder_outputs_: (batch_size, max_tgt_sen_len, d_model)
# decoder_self_attention_weight:(batch_size, n_head, max_tgt_sen_len, )
return decoder_outputs_, decoder_self_attention_weight, decoder_encoder_attention_weight
import torch
import torch.nn as nn
class LayerNormalization(nn.Module):
def __init__(self, d_hid, eps=1e-6):
super(LayerNormalization, self).__init__()
self.gamma = nn.Parameter(torch.ones(d_hid))
self.beta = nn.Parameter(torch.zeros(d_hid))
self.eps = eps
def forward(self, z):
mean = z.mean(dim=-1, keepdim=True, )
std = z.std(dim=-1, keepdim=True, )
ln_out = (z - mean) / (std + self.eps)
ln_out = self.gamma * ln_out + self.beta
return ln_out
Decoder端的架构: Transformer原始论文中的Decoder模块是由N=6个相同的Decoder Block堆叠而成, 其中每一个Block是由3个子模块构成, 分别是多头self-attention模块, Encoder-Decoder attention模块, 前馈全连接层模块.
6个Block的输入不完全相同:
Decoder在训练阶段的输入解析:
Decoder在预测阶段的输入解析:
参考资料:
Transformer一统江湖:自然语言处理三大特征抽取器比较
The Transformer Family
The Illustrated Transformer
The Annotated Transformer
A Deep Dive Into the Transformer Architecture – The Development of Transformer Models
Transformer图解
Transformer代码阅读
Transformer: A Novel Neural Network Architecture for Language Understanding
图解Transformer(完整版)
NLP 中的Mask全解
Talk to Transformer
Transformer的pytorch实现
基于pytorch的transformer代码实现(包含Batch Normalization,Layer normalization,Mask等讲述)