博议便览:
原理分析:
《中文自然语言处理Transformer模型(一) 》(1—GitHub ipynb版 ; 2—nbviewer版)
《预训练语言模型--transformer》
《保姆级教程:硬核图解Transformer》
《nlp中的Attention注意力机制+Transformer详解》
维度分析:
《Transformer的矩阵维度分析和Mask详解》
《BERT自注意力计算过程中的张量形状变化》
0、引言
减少序列处理任务的计算量是一个很重要的问题,也是Extended Neural GPU、ByteNet和ConvS2S等网络的动机。上面提到的这些网络都以CNN为基础,并行计算所有输入和输出位置的隐藏表示。 在这些模型中,关联来自两个任意输入或输出位置的信号所需的操作数随位置间的距离增长而增长,比如ConvS2S呈线性增长,ByteNet呈现以对数形式增长,这会使学习较远距离的两个位置之间的依赖关系变得更加困难。而在Transformer中,操作次数则被减少到了常数级别。以自然语言处理为例,Transformer和LSTM的最大区别, 就是LSTM的训练是迭代的,是一个接一个字的来, 当前这个字过完LSTM单元,才可以进下一个字,而transformer的训练是并行了,就是所有字是全部同时训练的,这样就大大加快了计算效率,Transformer使用了位置嵌入(𝒑𝒐𝒔𝒊𝒕𝒊𝒐𝒏𝒂𝒍 𝒆𝒏𝒄𝒐𝒅𝒊𝒏𝒈)来理解语言的顺序,使用自注意力机制(Self-attention)和全连接层来进行计算。
Self-attention有时候也被称为Intra-attention,是在单个句子不同位置上做的Attention,并得到序列的一个表示。它能够很好地应用到很多任务中,包括阅读理解、摘要、文本蕴涵,以及独立于任务的句子表示。端到端的网络一般都是基于循环注意力机制而不是序列对齐循环,并且已经有证据表明在简单语言问答和语言建模任务上表现很好。据我们所知,Transformer是第一个完全依靠Self-attention而不使用序列对齐的RNN或卷积的方式来计算输入输出表示的转换模型。
总体工作流程的动态图为:
Transformer动态流图
工作原理直解:Transformer模型主要分为两大部分,分别是编码器和解码器,编码器负责把自然语言序列映射成为隐藏层(下图中第2步用九宫格比喻的部分), 含有自然语言序列的数学表达;然后解码器把隐藏层再映射为自然语言序列, 从而使我们可以解决各种问题, 如情感分类、命名实体识别、语义关系抽取、摘要生成、机器翻译等等,下面我们简单说一下下图的每一步都做了什么,以自然语言中的翻译为例:
- 输入自然语言序列到编码器: 𝑾𝒉𝒚 𝒅𝒐 𝒘𝒆 𝒘𝒐𝒓𝒌?(为什么要工作);
- 编码器输出的隐藏层, 再输入到解码器;
- 输入<𝒔𝒕𝒂𝒓𝒕>(起始)符号到解码器;
- 得到第一个字"为";
- 将得到的第一个字"为"落下来再输入到解码器;
- 得到第二个字"什";
- 将得到的第二字再落下来,直到解码器输出<𝒆𝒏𝒅>(终止符),即序列生成完成。
工作原理直解
再次以动图解释:
Encoding-decoding的动态过程 source: https://ai.googleblog.com/2017/08/transformer-novel-neural-network.htm
这个图的编码过程, 主要是self attention, 有三层。 接下来是解码过程, 也是有三层, 第一个预测结果 <start> 符号, 是完全通过编码器里的attention vector做出的决策,而第二个预测结果‘Je’, 是基于encoding attention vector & <start> attention vector 做出的决策。按照这个逻辑,新翻译的单词不仅仅依赖 encoding attention vector, 也依赖过去翻译好的单词的attention vector。 随着翻译出来的句子越来越多,翻译下一个单词的运算量也就会相应增加。 如果详细分析,复杂度是 (n^2d), 其中n是翻译句子的长度,d是word vector 的维度。
Transformer内部具体组件框架如下图所示:
Transformer内部具体组件
一、Transformer图解
1.1 概览
Transformer总览
模型的框架如下:
- Encoder 部分 -- 6 个 block,每个 block 中有两层,他们分别是 Multi-head self attention 和 Position-wise feed forward。1. Multi-head self attention - 多组 self attention 的连接。首先 encoder 的初始输入为 sentence embedding + position embedding,其中 position embedding 的三角函数表示挺有意思。Attention(Q,K,V)=softmax(QK^T/sqrt(d_k))V,其中 Q 与 K 均为输入,(V 为 learned value?此处存疑)。输入 size 为 [sequence_length, d_model],输出 size 不变。然后是 residual connection,即 LayerNorm(x+Sublayer(x)),输出 size 不变。2. Position-wise feed forward network,其实就是一个 MLP 网络,1 的输出中,每个 d_model 维向量 x 在此先由 xW1+b1 变为 d_ff 维的 x',再经过 max(0, x')W2+b2 回归 d_model 维。之后再是一个 residual connection。输出 size 仍是 [sequence_length, d_model]。
Encoder各步骤示例
- Decoder 部分 -- 6 个 block,每个 block 中有 3 层,他们分别是 Multi-head self attention (with mask),Multi-head attention (with encoder),Position-wise feed forward。1. Multi-head self attention (with mask) 与 encoder 部分相同,只是采用 0-1mask 消除右侧单词对当前单词 attention 的影响。2. Multi-head attention(with encoder) 引入 encoder 部分的输出在此处作为 multi-head 的其中几个 head。3. Position-wise feed forward network 与 encoder 部分相同。
Dncoder各步骤示例
- some tricks during training process:(1) residual dropout; (2) attention dropout; (3) label smoothing
(1)知识点1:注意力机制
RNN中使用注意力机制
查询(Lookup)机制图示1
查询(Lookup)机制图示2
Transformer中的注意力机制
在论文中,谷歌将注意力机制一般化了,一个注意力函数描述为将 Query 与一组键值对(Key-Value)映射到输出,其中这三者均为向量形式。
对于翻译任务,Query 可以认为是源词语向量序列,而 Key 和 Value 可以认为为目标词向量序列。即注意力通过计算 Query 和 Key 之间的相似性,并通过相似性来确定 Query 和 Value 之间的注意力关系。
a、点积attention
我们来介绍一下attention的具体计算方式。attention可以有很多种计算方式: 加性attention、点积attention,还有带参数的计算方式。着重介绍一下点积attention的公式:
下图中令
,
分别是Query和Key,其中,Query可以看作
个维度为
的向量(长度为
的sequence的向量表达)拼接而成,Key可以看作
个维度为
的向量(长度为
的sequence的向量表达)拼接而成。
Attention中Q*(K^T)矩阵计算示意图,Query和Key的维度要保持一致
- 【一个小问题】为什么有缩放因子 ?
- 先一句话回答这个问题: 缩放因子的作用是归一化。
- 假设里的元素的均值为0,方差为1,那么中元素的均值为0,方差为. 当d变得很大时, 中的元素的方差也会变得很大,如果 中的元素方差很大,那么 的分布会趋于陡峭(分布的方差大,分布集中在绝对值大的区域)。总结一下就是的分布会和有关。因此 中每一个元素乘上 后,方差又变为1。这使得 的分布“陡峭”程度与解耦,从而使得训练过程中梯度值保持稳定。
点积几何意义知识补充:
其中
是
A
到
B
的投影。
由这里的图和公式得:
A
,
B
两个向量的点积的大小是由夹角决定的,当夹角越大,𝒄𝒐𝒔值就越小,90度的时候为0,夹角越小,𝒄𝒐𝒔值越大,点积越大,当两个向量重合的时候,点积最大。也就是说两个向量越相似,
相似度越大,点积越大
。
b、Attention机制涉及到的参数
一个完整的attention层涉及到的参数有:
- 把分别映射到的线性变换矩阵:
- 把输出的表达 映射为最终输出 的线性变换矩阵:
c、Query, Key, Value
Query和Key作用得到的attention权值作用到Value上。因此它们之间的关系是:
- Query和Key的维度必须一致【即】,Value 和Query/Key的维度可以不一致。
- Key和Value的长度必须一致。Key和Value本质上对应了同一个Sequence在不同空间的表达。
- Attention得到的Output 的维度和Value的维度一致,长度和Query一致。
- Output每个位置是由Value的所有位置的vector加权平均之后的向量;而其权值是由位置为的Query和Key的所有位置经过attention计算得到的 ,权值的个数等于Key/Value的长度。
Attention全部计算示意图
在经典的Transformer结构中,我们记线性映射之前的Query, Key, Value为
,映射之后为
。那么:
- self-attention的
- 都是同一个输入, 即当前序列由上一层输出的高维表达。
- cross-attention的
- 代表当前序列,
- 是同一个输入,对应的是encoder最后一层的输出结果(对decoder端的每一层来说,保持不变)
而每一层线性映射参数矩阵都是独立的,所以经过映射后的
各不相同,模型参数优化的目标在于将
被映射到新的高维空间,使得每层的
在不同抽象层面上捕获到
之间的关系。一般来说,底层layer捕获到的更多是lexical-level的关系,而高层layer捕获到的更多是semantic-level的关系。
(2)知识点2:多头注意力机制(Multi-head Attention)
维度分析详见《Transformer的矩阵维度分析和Mask详解》
Multi-head Attention计算过程的表示框图
Attention是将Query和Key映射到同一高维空间中去计算相似度,而对应的multi-head attention把Query和Key映射到高维空间
的不同子空间
中去计算相似度。
为什么要做multi-head attention?论文原文里是这么说的:
Multi-head attention allows the model to jointly attend to information from different representation subspaces at different positions. With a single attention head, averaging inhibits this.
也就是说,这样可以在不改变参数量的情况下增强每一层attention的表现力。
Multi-head Attention计算示意图
Multi-head Attention的本质是,在参数总量保持不变的情况下,将同样的Query, Key, Value映射到原来的高维空间的不同子空间中进行attention的计算,在最后一步再合并不同子空间中的attention信息。这样降低了计算每个head的attention时每个向量的维度,在某种意义上防止了过拟合;由于Attention在不同子空间中有不同的分布,Multi-head Attention实际上是寻找了序列之间不同角度的关联关系,并在最后concat这一步骤中,将不同子空间中捕获到的关联关系再综合起来。
从上图可以看出,
和
之间的attention score从1个变成了
个,这就对应了
个子空间中它们的关联度。
(3)知识点3:Transformer 中的mask
参考文献:《Transformer -encoder mask篇》《Transformer -decoder mask篇》
对于 NLP 应用领域Transformer中 mask 的作用,先上结论(PS:padding mask 和 sequence mask非官方命名):
1. padding mask: 处理非定长序列,区分 padding 和非 padding 部分,如在 RNN 等模型和 Attention 机制中的应用等【对应的是普通的Scaled Dot-Product Attention中的mask操作】;
2. sequence mask: 防止标签泄露,如:Transformer decoder 中的 mask 矩阵,BERT 中的 [Mask] 位,XLNet 中的 mask 矩阵等【对应的是Masked Multi-Head Attention中的Masked】。
Transformer 是包括 Encoder和 Decoder的,Encoder中 self-attention 的 padding mask就是用于处理非定长序列,让padding(不够长补0)的部分不参与attention操作;而 Decoder 还需要防止标签泄露,生成当前词语的概率分布时,让程序不会注意到这个词背后的部分,即在
时刻不能看到
时刻之后的信息,因此在上述 padding mask的基础上,还要加上 sequence mask。
Encoder mask篇
在NLP中,文本一般是不定长的,所以在进行 batch训练之前,要先进行长度的统一,过长的句子可以通过truncating 截断到固定的长度,过短的句子可以通过 padding 增加到固定的长度,但是 padding 对应的字符只是为了统一长度,但是这个时候进行softmax的时候就会产生问题,回顾softmax函数:
,
是有值的,这样的话softmax中被padding的部分就参与了运算,就等于让无效的部分参与了运算,会产生很大隐患,因此希望在之后的计算中屏蔽它们,这时候就需要 Mask。这时就需要做一个mask让这些无效区域不参与运算,我们这里一股脑的给无效区域加一个很大的负数偏置,也就是:
其中
就是不合法,无效的部分。经过上式的masking我们使无效区域经过softmax计算之后得到的结果几乎为0,这样就避免了无效区域参与计算。
不定长数据输入
在 Attention 机制中,同样需要忽略 padding 部分的影响,这里以Transformer encoder中的self-attention为例:self-attention中,Q和K在点积之后,需要先经过mask再进行softmax,因此,对于要屏蔽的部分,mask之后的输出需要加一个很大的负数偏置,这样softmax之后输出才为0。
def attention(query, key, value, mask=None, dropout=None):
"Compute 'Scaled Dot Product Attention'"
d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-2, -1)) \
/ math.sqrt(d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9) # mask步骤,用 -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
Decoder mask篇
在语言模型中,常常需要从上一个词预测下一个词,但如果要在LM中应用 self attention 或者是同时使用上下文的信息,要想不泄露要预测的标签信息,就需要 mask 来“遮盖”它。
sequence mask 一般是通过生成一个上三角矩阵来实现的,上三角区域对应要mask的部分。
在Transformer 的 Decoder中,先不考虑 padding mask,一个包括四个词的句子[A,B,C,D]在计算了相似度scores之后,得到下面第一幅图,将scores的上三角区域mask掉,即替换为负无穷,再做softmax得到第三幅图。这样,比如输入 B 在self-attention之后,也只和A,B有关,而与后序信息无关。
因为在softmax之后的加权平均中: B' = 0.48*A+0.52*B,而 C,D 对 B'不做贡献。
实际应用中,Decoder 需要结合 padding mask 和 sequence mask,下面在pytorch框架下以一个很简化的例子展示 Transformer 中 的两种 Mask。
import torch
def padding_mask(seq, pad_idx):
return (seq != pad_idx).unsqueeze(-2) # [B, 1, L]
def sequence_mask(seq):
batch_size, seq_len = seq.size()
mask = 1- torch.triu(torch.ones((seq_len, seq_len), dtype=torch.uint8),diagonal=1)
mask = mask.unsqueeze(0).expand(batch_size, -1, -1) # [B, L, L]
return mask
def test():
# 以最简化的形式测试Transformer的两种mask
seq = torch.LongTensor([[1,2,0]]) # batch_size=1, seq_len=3,padding_idx=0
embedding = torch.nn.Embedding(num_embeddings=3, embedding_dim=10, padding_idx=0)
query, key = embedding(seq), embedding(seq)
scores = torch.matmul(query, key.transpose(-2, -1))
mask_p = padding_mask(seq, 0)
mask_s = sequence_mask(seq)
mask_decoder = mask_p & mask_s # 结合 padding mask 和 sequence mask
scores_encoder = scores.masked_fill(mask_p==0, -1e9) # 对于scores,在mask==0的位置填充
scores_decoder = scores.masked_fill(mask_decoder==0, -1e9)
test()
对应的各mask值为:
# mask_p
[[[1 1 0]]]
# mask_s
[[[1 0 0]
[1 1 0]
[1 1 1]]]
# mask_decoder
[[[1 0 0]
[1 1 0]
[1 1 0]]]
可以看到 mask_decoder 的第三列为0 ,对应padding mask,上三角为0,对应sequence mask。
(4)知识点4:FFN(Feed Forward Network)层
每一层经过attention之后,还会有一个FFN,这个FFN的作用就是空间变换。FFN包含了2层linear transformation层,中间的激活函数是ReLu。
曾经我在这里有一个百思不得其解的问题:attention层的output最后会和W相乘,为什么这里又要增加一个2层的FFN网络?
其实,FFN的加入引入了非线性(ReLu激活函数),变换了attention output的空间, 从而增加了模型的表现能力。把FFN去掉模型也是可以用的,但是效果差了很多。
1.2 Encoder步骤
在一个编码器内部的计算过程是:❝多头注意力机制 --> 残差连接、层归一化 --> 全连接层 --> 残差连接、层归一化❞
具有两层的编码器,并行处理包含三个元素(w1,w2和w3)的输入序列。 每个输入元素的编码器还通过其“ Self-Attention ”子层接收有关其他元素的信息,从而可以捕获句子中单词之间的关系
以自然语言处理来阐述Tranformer中的编码器阶段,下图为一个Encoding Block结构图,这里详细描述各个部分的处理步骤。注意:为方便查看,第(0)小节介绍了输入嵌入,(1)、(2)、(3)、(4)小节内容分别对应着下图第1, 2, 3, 4个方框的序号:
单个Encoding Block
(0)𝑰𝒏𝒑𝒖𝒕 𝑬𝒎𝒃𝒆𝒅𝒅𝒊𝒏𝒈
我们输入数据
维度为[𝒃𝒂𝒕𝒄𝒉_𝒔𝒊𝒛𝒆, 𝒔𝒆𝒒𝒖𝒆𝒏𝒄𝒆_𝒍𝒆𝒏𝒈𝒕𝒉]的数据,比如我们为什么工作
。𝒃𝒂𝒕𝒄𝒉_𝒔𝒊𝒛𝒆就是 batch
的大小,这里只有一句话,所以 𝒃𝒂𝒕𝒄𝒉_𝒔𝒊𝒛𝒆 为 1
,𝒔𝒆𝒒𝒖𝒆𝒏𝒄𝒆_𝒍𝒆𝒏𝒈𝒕𝒉是句子的长度,一共 7
个字,所以输入的数据维度是 [1, 7]
。我们不能直接将这句话输入到编码器中,因为 Tranformer
不认识,我们需要先进行字嵌入,即得到图中的𝑰𝒏𝒑𝒖𝒕 𝑬𝒎𝒃𝒆𝒅𝒅𝒊𝒏𝒈,即先从字向量表里查到对应的字向量,它的维度就变成了[𝒃𝒂𝒕𝒄𝒉_𝒔𝒊𝒛𝒆 , 𝑺𝒆𝒒𝒖𝒆𝒏𝒄𝒆_𝒍𝒆𝒏𝒈𝒕𝒉 , 𝒆𝒎𝒃𝒆𝒅𝒅𝒊𝒏𝒈_𝒅𝒊𝒎𝒎𝒆𝒏𝒔𝒊𝒐𝒏],这里的Input Embedding
就是从这个表中找到每一个字的数学表达。字向量表的结构:
字向量表的结构
简单点说,就是文字----->字向量的转换,这种转换是将文字转换为计算机认识的数学表示,用到的方法就是 Word2Vec
,Word2Vec
的具体细节,对于初学者暂且不用了解,这个是可以直接使用的。得到的
的维度是[𝒃𝒂𝒕𝒄𝒉_𝒔𝒊𝒛𝒆, 𝒔𝒆𝒒𝒖𝒆𝒏𝒄𝒆_𝒍𝒆𝒏𝒈𝒕𝒉, 𝒆𝒎𝒃𝒆𝒅𝒅𝒊𝒏𝒈_𝒅𝒊𝒎𝒆𝒏𝒔𝒊𝒐𝒏],𝒆𝒎𝒃𝒆𝒅𝒅𝒊𝒏𝒈_𝒅𝒊𝒎𝒆𝒏𝒔𝒊𝒐𝒏 的大小由 Word2Vec
算法决定,Tranformer
采用 512
长度的字向量。所以
的维度是 [1, 7, 512]
。至此,输入的我们为什么工作
,可以用一个矩阵来简化表示。
(1)𝒑𝒐𝒔𝒊𝒕𝒊𝒐𝒏𝒂𝒍 𝒆𝒏𝒄𝒐𝒅𝒊𝒏𝒈 (位置编码)
我们知道,文字的先后顺序,很重要。比如吃饭没
、没吃饭
、没饭吃
、饭吃没
、饭没吃
,同样三个字,顺序颠倒,所表达的含义就不同了。
文字的位置信息很重要,Tranformer
没有类似 RNN
的循环结构,没有捕捉顺序序列的能力。为了保留这种位置信息交给 Tranformer
学习,我们需要用到位置编码。现在定义一个位置编码的概念,也就是positional encoding,位置编码的维度为[𝒎𝒂𝒙_𝒔𝒆𝒒𝒖𝒆𝒏𝒄𝒆𝒍𝒆𝒏𝒈𝒕𝒉, 𝒆𝒎𝒃𝒆𝒅𝒅𝒊𝒏𝒈_𝒅𝒊𝒎𝒆𝒏𝒔𝒊𝒐𝒏],编码维度的第1维度等于词向量的embedding_size维度,第0维度的 max_sequencelength属于超参数,指的是限定的最大单个句长。注意,我们一般以字为单位训练Transformer模型,也就是说我们不用分词了,首先我们要初始化字向量为[𝒗𝒐𝒄𝒂𝒃𝒔𝒊𝒛𝒆, 𝒆𝒎𝒃𝒆𝒅𝒅𝒊𝒏𝒈_𝒅𝒊𝒎𝒆𝒏𝒔𝒊𝒐𝒏𝒍],𝒗𝒐𝒄𝒂𝒃𝒔𝒊𝒛𝒆为总共的字库数量,𝒆𝒎𝒃𝒆𝒅𝒅𝒊𝒏𝒈_𝒅𝒊𝒎𝒆𝒏𝒔𝒊𝒐𝒏𝒍为字向量的维度,也是每个字的数学表达。加入位置信息的方式非常多,最简单的可以是直接将绝对坐标 0, 1, 2
上式中pos指的是句中字的位置,取值范围是[0, max sequence length),
指的是词向量的维度, 取值范围是[0, embedding dimension), 上面有𝒔𝒊𝒏和𝒄𝒐𝒔一组公式, 也就是对应着embedding dimensionem维度的一组奇数和偶数的序号的维度,例如0,1一组,2,3一组,分别用上面的𝒔𝒊𝒏和𝒄𝒐𝒔函数做处理, 从而产生不同的周期性变化, 而位置编码在embedding dimension维度上随着维度序号增大,周期变化会越来越慢,而产生一种包含位置信息的纹理,就像论文原文中第六页讲的,位置编码函数的周期从
到
变化,而每一个位置在embedding dimension维度上都会得到不同周期的𝒔𝒊𝒏和𝒄𝒐𝒔函数的取值组合,从而产生独一的纹理位置信息,模型从而学到位置之间的依赖关系和自然语言的时序特性。
下面画一下位置编码,可见纵向观察, 随着embedding dimension增大, 位置嵌入函数呈现不同的周期变化。
# 导入依赖库
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import math
def get_positional_encoding(max_seq_len, embed_dim):
# 初始化一个positional encoding
# embed_dim: 字嵌入的维度
# max_seq_len: 最大的序列长度
positional_encoding = np.array([
[pos / np.power(10000, 2 * i / embed_dim) for i in range(embed_dim)]
if pos != 0 else np.zeros(embed_dim) for pos in range(max_seq_len)])
positional_encoding[1:, 0::2] = np.sin(positional_encoding[1:, 0::2]) # dim 2i 偶数
positional_encoding[1:, 1::2] = np.cos(positional_encoding[1:, 1::2]) # dim 2i+1 奇数
# 归一化, 用位置嵌入的每一行除以它的模长
# denominator = np.sqrt(np.sum(position_enc**2, axis=1, keepdims=True))
# position_enc = position_enc / (denominator + 1e-8)
return positional_encoding
positional_encoding = get_positional_encoding(max_seq_len=100, embed_dim=16)
plt.figure(figsize=(10,10))
sns.heatmap(positional_encoding)
plt.title("Sinusoidal Function")
plt.xlabel("hidden dimension")
plt.ylabel("sequence length")
可以看到,位置嵌入在 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛
(也是hidden dimension
plt.figure(figsize=(8, 5))
plt.plot(positional_encoding[1:, 1], label="dimension 1")
plt.plot(positional_encoding[1:, 2], label="dimension 2")
plt.plot(positional_encoding[1:, 3], label="dimension 3")
plt.legend()
plt.xlabel("Sequence length")
plt.ylabel("Period of Positional Encoding")
最后,将
和 位置编码
(2)𝒔𝒆𝒍𝒇 𝒂𝒕𝒕𝒆𝒏𝒕𝒊𝒐𝒏 𝒎𝒆𝒄𝒉𝒂𝒏𝒊𝒔𝒎(自注意力机制)
多头的意义在于,
得到的矩阵就叫注意力矩阵,它可以表示每个字与其他字的相似程度。因为,向量的点积值越大,说明两个向量越接近。
我们的目的是,让每个字都含有当前这个句子中的所有字的信息,用注意力层,我们做到了。需要注意的是,在上面 𝑠𝑒𝑙𝑓 𝑎𝑡𝑡𝑒𝑛𝑡𝑖𝑜𝑛
的计算过程中,我们通常使用 𝑚𝑖𝑛𝑖 𝑏𝑎𝑡𝑐ℎ
,也就是一次计算多句话,上文举例只用了一个句子。每个句子的长度是不一样的,需要按照最长的句子的长度统一处理。对于短的句子,进行 Padding
操作,一般我们用 0
来进行填充,具体操作示意图为:
根据第一章节的知识点3:Transformer 中的mask,在Padding操作后,需要进一步实施Attention Mask,使padding的无效区域经过softmax计算之后还几乎为0,这样就避免了无效区域参与计算(具体见第一章节知识点3)。
再看看自注意力机制代码,单头Attention 操作如下:
class ScaledDotProductAttention(nn.Module):
''' Scaled Dot-Product Attention '''
def __init__(self, temperature, attn_dropout=0.1):
super().__init__()
self.temperature = temperature
self.dropout = nn.Dropout(attn_dropout)
def forward(self, q, k, v, mask=None):
# self.temperature是论文中的d_k ** 0.5,防止梯度过大
# QxK/sqrt(dk)
attn = torch.matmul(q / self.temperature, k.transpose(2, 3))
if mask is not None:
# 屏蔽不想要的输出
attn = attn.masked_fill(mask == 0, -1e9)
# softmax+dropout
attn = self.dropout(F.softmax(attn, dim=-1))
# 概率分布xV
output = torch.matmul(attn, v)
return output, attn
Multi-Head Attention
实现在 ScaledDotProductAttention
基础上构建:
class MultiHeadAttention(nn.Module):
''' Multi-Head Attention module '''
# n_head头的个数,默认是8
# d_model编码向量长度,例如本文说的512
# d_k, d_v的值一般会设置为 n_head * d_k=d_model,
# 此时concat后正好和原始输入一样,当然不相同也可以,因为后面有fc层
# 相当于将可学习矩阵分成独立的n_head份
def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
super().__init__()
# 假设n_head=8,d_k=64
self.n_head = n_head
self.d_k = d_k
self.d_v = d_v
# d_model输入向量,n_head * d_k输出向量
# 可学习W^Q,W^K,W^V矩阵参数初始化
self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
# 最后的输出维度变换操作
self.fc = nn.Linear(n_head * d_v, d_model, bias=False)
# 单头自注意力
self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)
self.dropout = nn.Dropout(dropout)
# 层归一化
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
def forward(self, q, k, v, mask=None):
# 假设qkv输入是(b,100,512),100是训练每个样本最大单词个数
# 一般qkv相等,即自注意力
residual = q
# 将输入x和可学习矩阵相乘,得到(b,100,512)输出
# 其中512的含义其实是8x64,8个head,每个head的可学习矩阵为64维度
# q的输出是(b,100,8,64),kv也是一样
q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)
# 变成(b,8,100,64),方便后面计算,也就是8个头单独计算
q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
if mask is not None:
mask = mask.unsqueeze(1) # For head axis broadcasting.
# 输出q是(b,8,100,64),维持不变,内部计算流程是:
# q*k转置,除以d_k ** 0.5,输出维度是b,8,100,100即单词和单词直接的相似性
# 对最后一个维度进行softmax操作得到b,8,100,100
# 最后乘上V,得到b,8,100,64输出
q, attn = self.attention(q, k, v, mask=mask)
# b,100,8,64-->b,100,512
q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
q = self.dropout(self.fc(q))
# 残差计算
q += residual
# 层归一化,在512维度计算均值和方差,进行层归一化
q = self.layer_norm(q)
return q, attn
(3)残差连接 & 𝑳𝒂𝒚𝒆𝒓 𝑵𝒐𝒓𝒎𝒂𝒍𝒊𝒛𝒂𝒕𝒊𝒐𝒏
在这一部分对应着图中的第3个框:Add&Norm,Add指的是残差连接;Norm指的是𝑳𝒂𝒚𝒆𝒓 𝑵𝒐𝒓𝒎𝒂𝒍𝒊𝒛𝒂𝒕𝒊𝒐𝒏。一般的一个Block里边会有
个Add&Norm部分,一般使用Tranformer
的时候,一般是
,这样的话网络结构就比较深,使用残差连接可以避免梯度消失的情况发生。
『₳』残差连接
在上一步得到了经过注意力矩阵加权之后的
, 也就是
,我们对它进行一下转置,使其和
的维度一致, 也就是[𝒃𝒂𝒕𝒄𝒉_𝒔𝒊𝒛𝒆 , 𝑺𝒆𝒒𝒖𝒆𝒏𝒄𝒆_𝒍𝒆𝒏𝒈𝒕𝒉 , 𝒆𝒎𝒃𝒆𝒅𝒅𝒊𝒏𝒈_𝒅𝒊𝒎𝒎𝒆𝒏𝒔𝒊𝒐𝒏],然后把他们加起来做残差连接,直接进行元素相加,因为他们的维度一致:
在之后的运算里,每经过一个模块的运算,都要把运算之前的值和运算之后的值相加,从而得到残差连接,训练的时候可以使梯度直接走捷径反传到最初始层:
『฿』𝑳𝒂𝒚𝒆𝒓 𝑵𝒐𝒓𝒎𝒂𝒍𝒊𝒛𝒂𝒕𝒊𝒐𝒏 (层归一化)
𝑳𝒂𝒚𝒆𝒓 𝑵𝒐𝒓𝒎𝒂𝒍𝒊𝒛𝒂𝒕𝒊𝒐𝒏作用是把神经网络中隐藏层归一为标准正态分布,也就是 𝑖.𝑖.𝑑
独立同分布, 以起到加快训练速度, 加速收敛的作用:
上式中以矩阵的行 (𝑟𝑜𝑤) 为单位求均值:
上式中以矩阵的行 (𝑟𝑜𝑤) 为单位求方差:
然后用每一行的每一个元素减去这行的均值,再除以这行的标准差,从而得到归一化后的数值,
是为了防止除0。之后引入两个可训练参数
来弥补归一化的过程中损失掉的信息,注意
表示元素相乘而不是点积,我们一般初始化
为全1,而
为全0。
(4)𝑭𝒆𝒆𝒅 𝑭𝒐𝒓𝒘𝒂𝒓𝒅
block结构图中的第4部分,也就是𝑭𝒆𝒆𝒅 𝑭𝒐𝒓𝒘𝒂𝒓𝒅,其实就是两层线性映射并用激活函数激活, 比如说ReLU,具体代码如下:
class PositionwiseFeedForward(nn.Module):
''' A two-feed-forward-layer module '''
def __init__(self, d_in, d_hid, dropout=0.1):
super().__init__()
# 两个fc层,对最后的512维度进行变换
self.w_1 = nn.Linear(d_in, d_hid) # position-wise
self.w_2 = nn.Linear(d_hid, d_in) # position-wise
self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
residual = x
x = self.w_2(F.relu(self.w_1(x)))
x = self.dropout(x)
x += residual
x = self.layer_norm(x)
return x
(5)公式表达小结
1.3 Decoder步骤
Decoder部分和Encoder一样,也是有6层,但是每一个单独的decoder与encoder相比,在self-attention层(decoder层中叫masked self-attention)和全连接网络层之间,多了一层Encoder-Decoder Attention 层。
Decoder结构中,第一层是一个multi-head self-attention层,这个与Encoder中的区别是这里是masked multi-head self-attention。使用mask的原因是因为在预测句子的时候,当前时刻是无法获取到未来时刻的信息的。Decoder中的第二层attention层就是一个正常的multi-head attention层。但是这里Q,K,V来源不同。Q来自于上一个decoder的输出,而K,V则来自于encoder的输出。剩下的计算就没有其他的不同了。
关于这两个attention层,可以理解为 masked self-attention是计算当前翻译的内容和已经翻译的前文之间的关系,而encoder-decoder attention
最后再经过一个全连接层,输出Decoder的结果。
1.3.1 训练
可参考《【全网首发】Transformer模型详解(图解史上最完整版)》及《自然语言处理Transformer模型最详细讲解(图解版)》深入了解
Transformer的训练方式和其它模型不太一样,它在训练过程是采用了Teacher Forcing的训练模型,就是会将原始输入和正确答案都会喂给模型,然后模型进行训练,而在推理过程中,是不会给正确答案的。比如对于我们的例子" 我爱你 "来说,首先会把" 我爱你 "的Embedding嵌入送入Encoder中,然后把"<开始> I love you "的编码向量送入Decoder中,这个"<开始>"是个标志表示开始翻译,它也是个向量表示。
对于翻译" 我爱你 "来说,它是要按照顺序翻译的,就是首先把我翻译成"I",然后是翻译"love",最后是"you",但是我们在训练会把正确答案喂给模型,如果这样注意力机制就会看到所有的信息,所以要采用掩码机制来遮盖当前词后面的信息,防止模型知道之后单词的信息。
下面以 "我有一只猫" 翻译成 "I have a cat" 为例,了解一下Decoder中masked self-attention中 的Masked 操作。
(1)第一个 Multi-Head Attention
Decoder block 的第一个 Multi-Head Attention 采用了 Masked 操作,因为在翻译的过程中是顺序翻译的,即翻译完第 i 个单词,才可以翻译第 i+1 个单词。通过 Masked 操作可以防止第 i 个单词知道 i+1 个单词之后的信息。
在 Decoder 的时候,是需要根据之前的翻译,求解当前最有可能的翻译,如下图所示。首先根据输入 "<Begin>" 预测出第一个单词为 "I",然后根据输入 "<Begin> I" 预测下一个单词 "have"。
Decoder 可以在训练的过程中使用 Teacher Forcing 并且并行化训练,即将正确的单词序列 (<Begin> I have a cat) 和对应输出 (I have a cat <end>) 传递到 Decoder。那么在预测第 i 个输出时,就要将第 i+1 之后的单词掩盖住,注意 Mask 操作是在 Self-Attention 的 Softmax 之前使用的,下面用 0 1 2 3 4 5 分别表示 "<Begin> I have a cat <end>"。
第一步:是 Decoder 的输入矩阵和 Mask 矩阵,输入矩阵包含 "<Begin> I have a cat" (0, 1, 2, 3, 4) 五个单词的表示向量,Mask 是一个 5×5 的矩阵。在 Mask 可以发现单词 0 只能使用单词 0 的信息,而单词 1 可以使用单词 0, 1 的信息,即只能使用之前的信息。
第二步:接下来的操作和之前的 Self-Attention 一样,通过输入矩阵X计算得到Q,K,V矩阵。然后计算
和
的乘积
。
第三步:在得到
之后需要进行 Softmax,计算 attention score,我们在 Softmax 之前需要使用Mask矩阵遮挡住每一个单词之后的信息,遮挡操作如下:
得到 Mask
之后在 Mask
上进行 Softmax,每一行的和都为 1。但是单词 0 在单词 1, 2, 3, 4 上的 attention score 都为 0。
第四步:使用 Mask
与矩阵
相乘,得到输出
,则单词 1 的输出向量
是只包含单词 1 信息的。
第五步:通过上述步骤就可以得到一个 Mask Self-Attention 的输出矩阵
,然后和 Encoder 类似,通过 Multi-Head Attention 拼接多个输出
然后计算得到第一个 Multi-Head Attention 的输出
,
与输入
维度一样。
(2)第二个 Multi-Head Attention
Decoder block 第二个 Multi-Head Attention 变化不大, 主要的区别在于其中 Self-Attention 的 K, V矩阵不是使用 上一个 Decoder block 的输出计算的,而是使用 Encoder 的编码信息矩阵 C 计算的。
根据 Encoder 的输出 C计算得到 K, V,根据上一个 Decoder block 的输出 Z 计算 Q (如果是第一个 Decoder block 则使用输入矩阵 X 进行计算),后续的计算方法与之前描述的一致。
这样做的好处是在 Decoder 的时候,每一位单词都可以利用到 Encoder 所有单词的信息 (这些信息无需 Mask)。
(3) Softmax 预测输出单词
Decoder block 最后的部分是利用 Softmax 预测下一个单词,在之前的网络层我们可以得到一个最终的输出 Z,因为 Mask 的存在,使得单词 0 的输出 Z0 只包含单词 0 的信息,如下:
Softmax 根据输出矩阵的每一行预测下一个单词:
这就是 Decoder block 的定义,与 Encoder 一样,Decoder 是由多个 Decoder block 组合而成。
1.3.2 推理
完成Encoder知识的学习以及基本上知道Decoder是如何工作的之后,接下来看看他们是如何协同工作的吧。
但是对于推理过程,是不会输入正确答案的,而且和RNN运行差不多是一个一个的,首先会给Decoder输入开始标志,然后经过Decoder会预测出 “I” 单词,然后拿着这个 “I” 单词继续喂入Decoder去预测 “love”,但后拿着 “love” 去预测 “you” ,最后拿着 “you” 去预测结束标志。
对于翻译任务来说,这是序列到序列的问题,显然每次的输出序列的长度是不一致的,所以需要一个结束标志来表明这句话已经翻译完成,所以需要按顺序一个一个翻译,不断拿着已经翻译出的词送入模型,知道预测出结束标志为止。
Transformer 结构采用了两层的编码器/解码器。 编码器并行处理输入序列的所有三个元素(w1,w2和w3),而解码器按顺序生成每个元素(仅描绘了时间步长0和1,其中生成了输出序列元素v1和v2)。生成输出token将继续进行,直到表征句子结尾的token<EOS>出现而结束。
(1)首个输出
在完成encoder阶段之后,我们开始Decoder阶段。Decoder阶段的每个步骤从输出序列(本例中为英语翻译句子)输出一个元素。
(2)冒泡输出
以下步骤重复该过程,直到到达一个特殊符号,该符号指示Transformer的Decoder已完成其输出。 每一步的输出在下一个时间步被输入到Decoder底部,Decoder像编码器一样冒泡地解码结果。 同对Encoder输入的处理步骤一样,在Decoder输入中也嵌入并添加位置编码以指示每个单词的位置。
冒泡输出后续值
详细说明:
参数维度详解:
二、Go Forth And Transform
I hope you’ve found this a useful place to start to break the ice with the major concepts of the Transformer. If you want to go deeper, I’d suggest these next steps:
- Read the Attention Is All You Need paper, the Transformer blog post (Transformer: A Novel Neural Network Architecture for Language Understanding), and the Tensor2Tensor announcement.
- Watch Łukasz Kaiser’s talk walking through the model and its details
- Play with the Jupyter Notebook provided as part of the Tensor2Tensor repo
- Explore the Tensor2Tensor repo.
Follow-up works:
- The Annotated Transformer(哈佛大学代码实现 ,编者增)
Visualizing A Neural Machine Translation Model (Mechanics of Seq2seq Models With Attention)(动态图详解,编者增)
- Depthwise Separable Convolutions for Neural Machine Translation
- One Model To Learn Them All
- Discrete Autoencoders for Sequence Models
- Generating Wikipedia by Summarizing Long Sequences
- Image Transformer
- Training Tips for the Transformer Model
- Self-Attention with Relative Position Representations
- Fast Decoding in Sequence Models using Discrete Latent Variables
- Adafactor: Adaptive Learning Rates with Sublinear Memory Cost
🦄🤝🦄 Encoder-decoders in Transformers: a hybrid pre-trained architecture for seq2seq(编者增)
《A Comparative Study on Transformer vs RNN in Speech Applications.》Karita, S., Wang, X., Watanabe, S., Yoshimura, T., Zhang, W., Chen, N., … Yamamoto, R. (2019). (编者增)
神经机器翻译 之 谷歌 transformer 模型【简书】(编者增)
《Transformer Networks》.pdf Amir Ali Moinfar · M.Soleymani Deep Learning Sharif University of Technology Spring 2019
【斯坦福CS224N硬核课CS224n: Natural Language Processing with Deep Learning】Transformers模型详解 PDF 【pdf1:Transformers (lecture by John Hewitt)】【pdf2:More about Transformers and Pretraining (lecture by John Hewitt)】
三、应用
3.1 时间序列预测
(1) 流感流行案例
详见论文《Deep Transformer Models for Time Series Forecasting: The Influenza Prevalence Case》
(2)德克萨斯州每小时电力流量预测案例
使用德克萨斯州电力可靠性委员会(ERCOT)提供的德克萨斯州每小时电力流量的时间序列预测。
具体见Medium博文《What is a Transformer?》
(3)股票预测
详见博客《Stock Forecasting with Transformer Architecture & Attention Mechanism》
(4)基于Transformer的时间序列预测网络改进
《Enhancing the Locality and Breaking the Memory Bottleneck of Transformer on Time Series Forecasting》
四、代码实现
4.1 Transformers for Time Series
见代码文档:Transformers for Time Series