自然语言处理之Attention大详解(Attention is all you need)

1. 写在前面
今天分享的论文是2017年谷歌团队发表的一篇论文,这是一篇非常经典的自然语言处理领域的文章,基于这篇文章,才有了最近非常火的bert, Albert等模型,接触这篇文章是在一次直播中看到的,因为经典,所以就想着读了读(虽然不是搞nlp的,但总感觉知识这东西都有一定的通性,多学一些肯定没有坏处,万一以后要用到呢?)。但是这篇论文本身我读了一遍之后,感觉不太懂,感觉里面有些东西并不是说的很清楚,具体流程更是别提了。可能我预备知识也不足,于是就查资料,然后结合直播中讲的理解了一下,仍然可能有不到位的地方,但是我得先把理解的这些先整理下来,以后如果再有新的见解,我还会继续补充。

虽然没有完全get,但是我貌似get到了一些很精华的部分,因为这篇文章最大的亮点就是提出了一种Transformer的结构,这种结构呢,是完全依赖注意力机制来刻画输入和输出之间的全局依赖关系,而不使用递归运算的RNN网络了。这样的好处就是第一可以有效的防止RNN存在的梯度消失的问题,第二是允许所有的字全部同时训练(RNN的训练是迭代的,一个接一个的来,当前这个字过完,才可以进下一个字),即训练并行,大大加快了计算效率。

Transformer使用了位置嵌入来理解语言的顺序,使用了多头注意力机制和全连接层等进行计算,还有跳远机制,LayerNorm机制,Encoder-Decoder架构等,我感觉这里面比较重要且难以理解的就是Multi-Head Attention机制了,而我通过学习貌似恰好懂了这些知识,所以下面我想总结一些前面的各个模块。这篇论文我花了一天的时间整理,因为我想用最简单朴素的语言写出来,顺便分析一下经历了各个模块时序列的维度变化,毕竟如果维度不懂,这些东西就没法真正的实现出来。

这篇分享会很长,但是读下来之后保证你能够明白Transformer到底做了个什么事情,甚至可以自己去实现这个结构

论文下载:https://arxiv.org/abs/1706.03762

分享大纲如下:

PART ONE : Abstract
PART TWO: Introduction
PART THREE: Model Architecture(这一部分占据很大一部分,详细剖析)
PART FROE: Training
PART FIVE: Conclusion
后面的训练就简单说一下了,因为我感觉最难懂的是这个框架的运行原理,懂了原理之后,训练也好,实验也好,就是设备和时间的问题了。

下面正式开始:

2. Abstract
摘要部分说了一下目前用于序列转换的模型依然是Encoder-Decoder结构的RNN或者CNN。效果比较好的是Encoder-Attention-Decoder这样的结构。 所以在这里作者基于Encoder-Decoder提出了一种完全依赖Attention机制的Transformer模型,并且可以并行化而减少训练时间,实验表明,效果很好。 之前的结构类似这样:


3. Introduction
说了一下上面的这种结构的弊端: 就是需要递归迭代运行,没法并行化,这样对于很长的句子来说,很可能出现梯度消失的情况,并且计算量也很大,速度比较慢。所以需要改进。

Attention是利用局部聚焦的思想去建立注意力模型,但目前这样的机制都是和RNN连接。(self-attention, 有时也称为内注意,是一种将单个序列的不同位置联系起来以计算序列表示的注意机制。)

所以提出了一种Transformer模型,这种模型不用RNN或者说CNN这种递归机制,而是完全依赖于Attention。

4. Model Architecture(主角登场)
这是本篇文章的主角,也是我想重点说的地方。下面这个就是Transformer,先看总体结构:

从这个结构的宏观角度上,我们可以看到Transformer模型也是用了Encoder-Decoder结构,编码器部分负责把自然语言序列映射成为隐藏层(就上面那个九宫格),含有自然语言序列的数学表达,然后解码器把隐藏层再映射为自然语言序列,从而使我们可以解决各种问题,比如情感分类,命名实体识别,语义关系抽取,机器翻译,摘要生成等等。

先简单说一下上面的结构的工作流程:
比如我做一个机器翻译(Why do we work?) -> 为什么要工作?

输入自然语言序列: Why do we work?
编码器输出的隐藏层是Why do we work的一种数学表示,类似于提取了每一个词的信息,然后汇总,转换成了这句话的数学向量。然后输入到解码器
输入符号到解码器
就会得到第一个字“为”
将得到的第一个字“为”落下来再输入到解码器
得到第二个字“什”
将得到的第二个字落下来输入,得到“么”,重复,直到解码器输出。 翻译完成
下面重点讲讲细节部分了。看看究竟是怎么得到数学向量的,以及怎么通过数学向量得出最终答案的?

3.1 编码器部分的工作细节

看上面结构我们发现编码器部分是由N x N_xN 
x

 个transformer block堆叠而成的,我们就拿一个transformer block来进一步观察,每一个transformer block又有两个子层,第一个是多头注意力部分,第二个是feed-forward部分。

我们输入句子:Why do we work? 的时候,它的编码流程进一步细化:

首先输入进来之后,经过Input Embedding层每个字进行embedding编码(这个后面会说),然后再编入位置信息(position Encoding),形成带有位置信息的embedding编码。
然后进入多头注意力部分,这部分是多角度的self-attention部分,在里面每个字的信息会依据权重进行交换融合,这样每一个字会带上其他字的信息(信息多少依据权重决定),然后进入feed-forward部分进行进一步的计算,最后就会得到输入句子的数学表示了。
下面再详细说一下每一部分的细节:

3.1.1 位置嵌入
由于transformer模型没有循环神经网络的迭代操作, 所以我们必须提供每个字的位置信息给transformer, 才能识别出语言中的顺序关系。

现在定义一个位置嵌入的概念,也就是现在定义一个位置嵌入的概念, 也就是𝑝𝑜𝑠𝑖𝑡𝑖𝑜𝑛𝑎𝑙 𝑒𝑛𝑐𝑜𝑑𝑖𝑛𝑔, 位置嵌入的维度为[𝑚𝑎𝑥 𝑠𝑒𝑞𝑢𝑒𝑛𝑐𝑒 𝑙𝑒𝑛𝑔𝑡ℎ, 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛], 嵌入的维度同词向量的维度, 𝑚𝑎𝑥 𝑠𝑒𝑞𝑢𝑒𝑛𝑐𝑒 𝑙𝑒𝑛𝑔𝑡ℎ属于超参数, 指的是限定的最大单个句长.

注意, 我们一般以字为单位训练transformer模型, 也就是说我们不用分词了, 首先我们要初始化字向量为[𝑣𝑜𝑐𝑎𝑏 𝑠𝑖𝑧𝑒, 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛], 𝑣𝑜𝑐𝑎𝑏 𝑠𝑖𝑧𝑒为总共的字库数量, 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛为字向量的维度, 也是每个字的数学表达.

好吧,如果这里开始不懂了, 我们就拿我们的例子来看一下子:

这里论文里面使用了sine和cosine函数的线性变换来提供给模型的位置信息:

上式中p o s pospos指的是句中字的位置, 取值范围是[ 0 ,   m a x   s e q u e n c e   l e n g t h ) [0, \ max \ sequence \ length)[0, max sequence length), i ii指的是词向量的维度, 取值范围是[ 0 ,   e m b e d d i n g   d i m e n s i o n ) [0, \ embedding \ dimension)[0, embedding dimension), 上面有s i n sinsin和c o s coscos一组公式, 也就是对应着e m b e d d i n g   d i m e n s i o n embedding \ dimensionembedding dimension维度的一组奇数和偶数的序号的维度, 例如0 , 1 0, 10,1一组, 2 , 3 2, 32,3一组, 分别用上面的s i n sinsin和c o s coscos函数做处理, 从而产生不同的周期性变化, 而位置嵌入在e m b e d d i n g   d i m e n s i o n embedding \ dimensionembedding dimension维度上随着维度序号增大, 周期变化会越来越慢, 而产生一种包含位置信息的纹理, 就像论文原文中第六页讲的

位置嵌入函数的周期从2 π 2 \pi2π到10000 ∗ 2 π 10000 * 2 \pi10000∗2π变化, 而每一个位置在e m b e d d i n g   d i m e n s i o n embedding \ dimensionembedding dimension维度上都会得到不同周期的s i n sinsin和c o s coscos函数的取值组合, 从而产生独一的纹理位置信息, 模型从而学到位置之间的依赖关系和自然语言的时序特性.

还是拿例子举例, 我们看看我们的输入Why do we work? 的位置信息怎么编码的?

这样第三个,第四个词的编码这样下去。编码实现如下:

    def get_positional_encoding(max_seq_len, embed_dim):
        # 初始化一个positional encoding
        # embed_dim: 字嵌入的维度
        # max_seq_len: 最大的序列长度
        positional_encoding = np.array([
            [np.sin(pos / np.power(10000, 2 * i / embed_dim)) if i%2==0 else 
             np.cos(pos / np.power(10000, 2*i/embed_dim))
             for i in range(embed_dim) ]
             for pos in range(max_seq_len)])
       
        return positional_encoding
    
    
    positional_encoding = get_positional_encoding(max_seq_len=100, embed_dim=16)
    
    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")
    
    
    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")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
可视化一下,最后得到这样的效果:

所以, 会得到Why do we work这四个词的位置信息, 然后Embedding矩阵和位置矩阵的加和作为带有位置信息的新X,Xembedding_pos

这里再补充两个问题, 我们可以思考下, 第一个就是为啥要用这种方式编码呢?

作者这里这么设计的原因是考虑到NLP任务中,除了单词的绝对位置, 单词的相对位置也非常重要, 根据公式s i n ( α + β ) = s i n α c o s β + c o s α s i n β sin(\alpha+\beta)=sin\alpha cos\beta+cos\alpha sin\betasin(α+β)=sinαcosβ+cosαsinβ以及c o s ( α + β ) = c o s α c o s β − s i n α s i n β cos(\alpha+\beta)=cos\alpha cos\beta-sin\alpha sin\betacos(α+β)=cosαcosβ−sinαsinβ, 这表明位置k + p k+pk+p的位置向量可以表示为位置k kk的特征向量的线性变换, 为模型捕捉单词之间的相对位置关系提供了非常大的便利。

第二个问题,就是这里为啥这里的单词embedding和位置embedding能直接相加呢? 论文里面提到了个维度相同,但是我觉得应该还有更深的原因

维度相同是基础,但能相加的原因就是不同的位置,这个embedding肯定是不一样的,而对于词语来说,不同的词语,embedding肯定也是不一样, 那么这样相加,肯定能区分开词语和位置,这就类似于, 每个位置one-hot编码,每个词One-hot编码,然后对应位置和对应词one-hot相加,然后再取相应的embedding是一个道理。

3.1.2 多头注意力机制
这一步为了学到多重语意含义的表达,进行多头注意力机制的运算。不要被这个多头注意力给吓住,其实这里面就是用到了几个矩阵运算,先不用管怎么运算的,我们先宏观看一下这个注意力机制到底在做什么? 拿单头注意力机制举例:

左边的红框就是我们现在讲的部分,右图就是单头注意力机制做的事情,拿句子:

The animal didn’t cross the street, because it was too tired.
1
我们看i t itit这个词最后得到的R矩阵里面,就会表示出这个i t itit到底是指的什么, 可以看到R 1 R_1R 
1

 和R 2 R_2R 
2

 和i t itit最相关,就可以认为i t itit表示的是The animal。

也就是说,每一个字经过映射之后都会对应一个R矩阵, 这个R矩阵就是表示这个字与其他字之间某个角度上的关联性信息,这叫做单头注意力机制。(具体怎么做到的,下面会说)

下面看一下多头注意力宏观上到底干了什么事情:

左边这个是两头的注意力机制,上面说到这个橙色的这个注意力反映了i t itit这个词指代的信息。 而这个绿色的这个注意力,反应了i t itit这个词的状态信息,可以看到i t itit经过这个绿色的注意力机制后,t i r e d tiredtired这个词与i t itit关联最大,就是说i t itit,映射过去,会更关注t i r e d tiredtired这个词,因为这个正好是它的一个状态。 它累了。

这样是不是就能明白多头注意力的意义了啊,每个字经过多头注意力机制之后会得到一个R矩阵,这个R矩阵表示这个字与其他字在N个角度上(比如指代,状态…)的一个关联信息,这个角度就是用多个头的注意力矩阵体现的。这就是每个字多重语义的含义。

那么究竟是怎么实现的呢? 其实这个过程中就是借助了三个矩阵来完成的。下面具体看一下:

我们的目标是把我们的输入Xembedding_pos通过多头注意力机制(系列线性变换)先得到Z。然后Z通过前馈神经网络得到R。这个R矩阵表示这个字与其他字在N个角度上(比如指代,状态…)的一个关联信息。

先看看怎么得到这个Z: 在Xembedding_pos->Z的过程中到底发生了什么呢?

这就是整个过程的变化,首先Xembedding_pos会做三次线性变化得到Q,K,V,三个矩阵,然后里面Attention机制,把Q,K,V三个矩阵进行运算,最后把Attention矩阵和Xembedding_pos加起来就是最后的Z。

可是为什么要这么做呢? Q,K,V又分别表示什么意思呢?

我们先说第二个问题,Q,K,V这三个矩阵分别是什么意思, Q表示Query,K表示Key,V表示Value。之所以引入了这三个矩阵,是借鉴了搜索查询的思想,比如我们有一些信息是键值对(key->value)的形式存到了数据库,(5G->华为,4G->诺基亚), 比如我们输入的Query是5G, 那么去搜索的时候,会对比一下Query和Key, 把与Query最相似的那个Key对应的值返回给我们。 这里是同样的思想,我们最后想要的Attention,就是V的一个线性组合,只不过根据Q和K的相似性加了一个权重并softmax了一下而已, 这里比较巧妙的是Q,K,V都是这个Xembedding_pos而已。下面具体来看一下:

上面图中有8个head, 我们这里拿一个head来看一下做了什么事情:(请注意这里head的个数一定要能够被embedding dimension整除才可以,上面的embedding dimension是512, head个数是8,那么每一个head的维度是(4, 512/8))

怎么得到Q 1 Q_1Q 
1

 和K 1 K_1K 
1

 的相似度呢? 我们想到了点积运算, 我们还记得点积运算的几何意义吗?两个向量越相似, 他们的点积就越大,反之就越小(因为向量a点乘向量b等与∣ ∣ a ∣ ∣ ∣ ∣ b ∣ ∣ c o s θ ||a|| ||b| |cosθ∣∣a∣∣∣∣b∣∣cosθ, 越相似,θ越小,点积就会越大)。

我们看看Q 1 ∗ K 1 Q_1*K_1Q 
1

 ∗K 
1

 的转置表达的是个什么意思:


c 1 , c 2 , . . c 6 c_1, c_2,..c_6c 
1

 ,c 
2

 ,..c 
6

 这些就代表我们的输入的每一个字,每一行代表每一个字的特征信息, 那么Q1的c1行和K1转置的各个列做点积运算得到第一个字和其他几个字的相似度。这样最后的结果每一行表示的这个字和其他哪几个字比较相关, 这个矩阵就是head1角度的注意力矩阵。自注意力的巧妙之处就在于这里, 每个词向量两两之间内积,就能得到当前词与其他词的相似关系,有了相似关系,再通过softmax映射出权重, 再把这个权重反乘到各自词语的embedding身上,再加权求和,就相当于融于了其他词的相关信息。

这里再解释下为啥会Q K T QK^TQK 
T
 然后除以d k \sqrt {d_k} 

k

 

 的操作, 这个d k d_kd 
k

 表示的是K KK矩阵的向量维度,作者这里把内积方式的这种注意力和additive attention(这个就是全连接层实现注意力的方式)对比发现,当d k d_kd 
k

 小的时候,效果差不多,但d k d_kd 
k

 维度很大的时候, 内积方式的这种注意力方式不如前者,这里之所以用内积的注意力,是因为这种方式计算要快很多。作者怀疑当d k d_kd 
k

 很大的时候, 这里的内积结果也会变大

但是我没理解为啥这俩相乘求和是均值0方差d k d_kd 
k

  ? 所以下面这段是按照我的理解,解释内积为啥会变大了,因为按照上面的说法,我看不明白:

这里假设q qq和k kk都是d k d_kd 
k

 维的向量, 并且每个向量的每一个维度上的元素都服从均值0,方差1的标准正态。而q ⋅ k = ∑ i = 1 d k q i k i q \cdot k=\sum_{i=1}^{d_{k}} q_{i} k_{i}q⋅k=∑ 
i=1

k

 

 q 
i

 k 
i

 , 我们假设一个特殊情况, q qq和k kk是一样的, 那么上面这个操作相当于q ⋅ q = ∑ i = 1 d k q i q i = ∑ i = 1 d k q i 2 q \cdot q=\sum_{i=1}^{d_{k}} q_{i} q_{i}=\sum_{i=1}^{d_{k}} q_{i}^2q⋅q=∑ 
i=1

k

 

 q 
i

 q 
i

 =∑ 
i=1

k

 

 q 
i
2

 , 根据正态分布的性质:


那么上面这个内积其实相当于服从自由度为d k d_kd 
k

 的卡方分布, 且均值为d k d_kd 
k

 , 方差为2 d k 2d_k2d 
k

 , 看这个均值d k d_kd 
k

 , 如果当d k d_kd 
k

 很大的时候, 那么显然, 这个内积操作会使得结果变大。 而内积结果变大,可能会将softmax函数推到具有极小梯度的区域(就类似于sigmoid离原点越远的地方),这时候可能会梯度消失。 所以为了反向传播的时候能够获取平衡的梯度, 有一个Q K T QK^TQK 
T
 然后除以d k \sqrt {d_k} 

k

 

 的操作,这个再放到q , k q,kq,k上面,相当于对其进行了一个缩放,相当于每个元素乘上了1 d k \frac{1}{\sqrt{d_k}} 

k

 

 
1

 , 那么q ⋅ q = ∑ i = 1 d k 1 d k q i 2 q \cdot q=\sum_{i=1}^{d_{k}} \frac{1}{d_k}q_{i}^2q⋅q=∑ 
i=1

k

 

  

k

 
1

 q 
i
2

 ,这时候的卡方分布的均值就成了1了。拉回到了较小的值, 不容易梯度消失吧。

当然这个是我自己的理解哈。

好吧, 自己的理解还真是有些问题, 还是看正确的推导吧,当然上面这个举了个特殊情况帮助自己理解, 正确的推导是这样:

假设X = q i , Y = k i X=q_i, Y=k_iX=q 
i

 ,Y=k 
i

 , 这两个都是服从标准正态, 那么E ( X Y ) = E ( X ) E ( Y ) = 0 × 0 = 0 E(X Y)=E(X) E(Y)=0 \times 0=0E(XY)=E(X)E(Y)=0×0=0, 这个很容易解,主要是方差计算:
D ( X Y ) = E ( X 2 ⋅ Y 2 ) − [ E ( X Y ) ] 2 = E ( X 2 ) E ( Y 2 ) − [ E ( X ) E ( Y ) ] 2 = E ( X 2 − 0 2 ) E ( Y 2 − 0 2 ) − [ E ( X ) E ( Y ) ] 2 = E ( X 2 − [ E ( X ) ] 2 ) E ( Y 2 − [ E ( Y ) ] 2 ) − [ E ( X ) E ( Y ) ] 2 = D ( X ) D ( Y ) − [ E ( X ) E ( Y ) ] 2 = 1 × 1 − ( 0 × 0 ) 2 = 1
D(XY)=E(X2⋅Y2)−[E(XY)]2=E(X2)E(Y2)−[E(X)E(Y)]2=E(X2−02)E(Y2−02)−[E(X)E(Y)]2=E(X2−[E(X)]2)E(Y2−[E(Y)]2)−[E(X)E(Y)]2=D(X)D(Y)−[E(X)E(Y)]2=1×1−(0×0)2=1
D(XY)=E(X2⋅Y2)−[E(XY)]2=E(X2)E(Y2)−[E(X)E(Y)]2=E(X2−02)E(Y2−02)−[E(X)E(Y)]2=E(X2−[E(X)]2)E(Y2−[E(Y)]2)−[E(X)E(Y)]2=D(X)D(Y)−[E(X)E(Y)]2=1×1−(0×0)2=1
D(XY)

  
=E(X 
2
 ⋅Y 
2
 )−[E(XY)] 
2
 
=E(X 
2
 )E(Y 
2
 )−[E(X)E(Y)] 
2
 
=E(X 
2
 −0 
2
 )E(Y 
2
 −0 
2
 )−[E(X)E(Y)] 
2
 
=E(X 
2
 −[E(X)] 
2
 )E(Y 
2
 −[E(Y)] 
2
 )−[E(X)E(Y)] 
2
 
=D(X)D(Y)−[E(X)E(Y)] 
2
 
=1×1−(0×0) 
2
 
=1

 

这样就会发现,q i , k i q_i,k_iq 
i

 ,k 
i

 相乘依然是服从0-1的正态分布, 那么d k d_kd 
k

 个相加, 根据
E ( ∑ i Z i ) = ∑ i E ( Z i ) D ( ∑ i Z i ) = ∑ i D ( Z i ) E\left(\sum_{i} Z_{i}\right)=\sum_{i} E\left(Z_{i}\right) \\ D\left(\sum_{i} Z_{i}\right)=\sum_{i} D\left(Z_{i}\right)
E( 
i


 Z 
i

 )= 
i


 E(Z 
i

 )
D( 
i


 Z 
i

 )= 
i


 D(Z 
i

 )

就能得到q ⋅ k = ∑ i = 1 d k q i k i q \cdot k=\sum_{i=1}^{d_{k}} q_{i} k_{i}q⋅k=∑ 
i=1

k

 

 q 
i

 k 
i

 的均值是0, 方差是d k d_kd 
k

 。方差越大也就说明,点积的数量级越大(以越大的概率取大值)。那么一个自然的做法就是把方差稳定到1,做法是将点积除以d k \sqrt {d_k} 

k

 

 , 此时:
D ( q ⋅ k d k ) = d k ( d k ) 2 = 1 D\left(\frac{q \cdot k}{\sqrt{d}_{k}}\right)=\frac{d_{k}}{\left(\sqrt{d}_{k}\right)^{2}}=1
D( 
d

  
k

 
q⋅k

 )= 

d

  
k

 ) 
2
 

k

 

 =1

将方差控制到1, 就能有效的防止梯度消失问题了, 那么这个是为什么呢? 那就得需要画出softmax图像来看看了: 这里假设输入是x = [ a , 2 a , 3 a ] x=[a, 2a, 3a]x=[a,2a,3a], 看看随着a的增大,softmax图像的变化:

可以看到,数量级对softmax得到的分布影响非常大。在数量级较大时,softmax将几乎全部的概率分布都分配给了最大值对应的标签。而看这个图像的梯度, 在输入值很大的时候,会发现导数几乎为0. 当然,更详细的公式推导,可以看这里

然后对每一行使用softmax归一化变成某个字与其他字的注意力的概率分布(使每一个字跟其他所有字的权重的和为1)。

这时候,我们从注意力矩阵取出一行(和为1),然后依次点乘V的列,因为矩阵V的每一行代表着每一个字向量的数学表达,这样操作,得到的正是注意力权重进行数学表达的加权线性组合,从而使每个字向量都含有当前句子的所有字向量的信息。这样就得到了新的X_attention(这个X_attention中每一个字都含有其他字的信息)。

用这个加上之前的Xembedding_pos得到残差连接,训练的时候可以使得梯度直接走捷径反传到最初层,不易消失。另外,我觉得这里用残差还有个好处就是能够保留原始的一些信息

再经过一个LayerNormlization操作就可以得到Z。 LayerNormlization的作用是把神经网络中隐藏层归一化为标准正态分布,起到加快训练速度,加速收敛的作用。类似于BatchNormlization,但是与BatchNormlization不同的是前者是以行为单位(每一行减去每一行的均值然后除以每一行的标准差),后者是一个Batch为单位(每一个元素减去Batch的均值然后除以Batch的标准差)。来个图感受下就是这样:
这里简单的考虑了下从样本角度的合理性,就是每个句子毕竟差距还是蛮大的, 既然是融合全局信息, 并归一化, 最好还是各个句子归一化自己的。


所以多头注意力机制细节总结起来就是下面这个图了:

注意,图里面有个地方表达错了,d k d_kd 
k

 不是注意力的头数, 而是拼接起来的那个最终维度,这里指的是512, 另外就是,这里多个头直接拼接的操作, 相当于默认了每个头或者说每个子空间的重要性是一样的, 在每个子空间里面学习到的相似性的重要度是一样的。

最近伙伴提出了一个问题,就是为啥这里是各个头的结果直接拼接,而不是进行堆叠,然后再过一个att加权,再汇总? 当时觉得好像这么做也合理呀,相当于每个头再进行一个重要性衡量,乘上对应权重之后再算? 但现在又想了想,貌似这个操作多次一举,因为这里的多头就是经过可学习参数矩阵w q w_qw 
q

 这种矩阵得到的, 而这里再搞个注意力权重,无非就是在这个基础上又乘上了一个常量值,这个值又是学习。那么为啥不直接交给前面这个可学习参数呢? 如果它觉得某个头重要, 那干脆让那个头对应的可学习参数大些,输出的矩阵大些,不就类似于加了个权重吗?

另外还有伙伴问我,既然K和Q矩阵是一样维度的,那为啥不直接用一个呢? 这样参数还少些? 这个问题,我在整理推荐模型autoInt的文章中也有提到过, 这么做得到的注意力矩阵是个对称阵,感觉会漏掉信息,毕竟有时候, 对于两个词语来说,A对于B的重要性,不一定和B对于A的重要性对等, 反映到具体特征上, 就是学历和职业,对于计算机行业,学历可能不是很重要,但是对于金融行业, 学历就非常重要,所以学历对于职业来讲,还是比较重要的。 而职业对于学历来讲,可能不如前者重要。 反映到生活, 通常会听到男孩子对女孩子说: 你就是我的唯一, 而女孩子也会对男孩子说: 你也是我的唯一。 但这俩人说的都是实话吗? 但愿是吧。

3.1.3 前馈神经网络(FeedForward)
这一块就比较简单了,我们上面通过多头注意力机制得到了Z,下面就是把Z再做两层线性变换,然后relu激活就得到最后的R矩阵了。(相当于一个两层的神经网络)


3.1.4 Layer Normalization和残差连接
1)残差连接:
我们在上一步得到了经过注意力矩阵加权之后的V VV, 也就是A t t e n t i o n ( Q ,   K ,   V ) Attention(Q, \ K, \ V)Attention(Q, K, V), 我们对它进行一下转置, 使其和X e m b e d d i n g X_{embedding}X 
embedding

 的维度一致, 也就是[ b a t c h   s i z e ,   s e q u e n c e   l e n g t h ,   e m b e d d i n g   d i m e n s i o n ] [batch \ size, \ sequence \ length, \ embedding \ dimension][batch size, sequence length, embedding dimension], 然后把他们加起来做残差连接, 直接进行元素相加, 因为他们的维度一致:
X e m b e d d i n g + A t t e n t i o n ( Q ,   K ,   V ) X_{embedding} + Attention(Q, \ K, \ V)

embedding

 +Attention(Q, K, V)

在之后的运算里, 每经过一个模块的运算, 都要把运算之前的值和运算之后的值相加, 从而得到残差连接, 训练的时候可以使梯度直接走捷径反传到最初始层:
X + S u b L a y e r ( X ) X + SubLayer(X)
X+SubLayer(X)


2) LayerNorm
L a y e r N o r m a l i z a t i o n Layer NormalizationLayerNormalization的作用是把神经网络中隐藏层归一为标准正态分布, 也就是i . i . d i.i.di.i.d独立同分布, 以起到加快训练速度, 加速收敛的作用:
μ i = 1 m ∑ i = 1 m x i j \mu_{i}=\frac{1}{m} \sum^{m}_{i=1}x_{ij}
μ 
i

 = 
m
1

  
i=1

m

 x 
ij

 

上式中以矩阵的行( r o w ) (row)(row)为单位求均值;
σ j 2 = 1 m ∑ i = 1 m ( x i j − μ j ) 2 \sigma^{2}_{j}=\frac{1}{m} \sum^{m}_{i=1} (x_{ij}-\mu_{j})^{2}
σ 
j
2

 = 
m
1

  
i=1

m

 (x 
ij

 −μ 
j

 ) 
2
 

上式中以矩阵的行( r o w ) (row)(row)为单位求方差;
L a y e r N o r m ( x ) = α ⊙ x i j − μ i σ i 2 + ϵ + β LayerNorm(x)=\alpha \odot \frac{x_{ij}-\mu_{i}} {\sqrt{\sigma^{2}_{i}+\epsilon}} + \beta
LayerNorm(x)=α⊙ 
σ 
i
2

 +ϵ

 

ij

 −μ 
i

 

 +β

然后用每一行的每一个元素减去这行的均值, 再除以这行的标准差, 从而得到归一化后的数值, ϵ \epsilonϵ是为了防止除0 00;
之后引入两个可训练参数α ,   β \alpha, \ \betaα, β来弥补归一化的过程中损失掉的信息, 注意⊙ \odot⊙表示元素相乘而不是点积, 我们一般初始化α \alphaα为全1 11, 而β \betaβ为全0 00.

所以一个Transformer编码块做的事情如下:


下面再说两个细节就可以把编码器的部分结束了

第一个细节就是上面只是展示了一句话经过一个Transformer编码块之后的状态和维度,但我们实际工作中,不会只有一句话和一个Transform编码块,所以对于输入来的维度一般是[batch_size, seq_len, embedding_dim], 而编码块的个数一般也是多个,不过每一个的工作过程和上面一致,无非就是第一块的输出作为第二块的输入,然后再操作。论文里面是用的6个块进行的堆叠。
Attention Mask的问题, 因为如果有多句话的时候,句子都不一定一样长,而我们的seqlen肯定是以最长的那个为标准,不够长的句子一般用0来补充到最大长度,这个过程叫做padding。

但这时在进行s o f t m a x softmaxsoftmax的时候就会产生问题, 回顾s o f t m a x softmaxsoftmax函数σ ( z ) i = e z i ∑ j = 1 K e z j \sigma (\mathbf {z} )_{i}={\frac {e^{z_{i}}}{\sum _{j=1}^{K}e^{z_{j}}}}σ(z) 
i

 = 
∑ 
j=1
K

 e 

j

 
 


i

 
 

 , e 0 e^0e 
0
 是1, 是有值的, 这样的话s o f t m a x softmaxsoftmax中被p a d d i n g paddingpadding的部分就参与了运算, 就等于是让无效的部分参与了运算, 会产生很大隐患, 这时就需要做一个m a s k maskmask让这些无效区域不参与运算, 我们一般给无效区域加一个很大的负数的偏置, 也就是:
z i l l e g a l = z i l l e g a l + b i a s i l l e g a l z_{illegal} = z_{illegal} + bias_{illegal}

illegal

 =z 
illegal

 +bias 
illegal

 

b i a s i l l e g a l → − ∞ bias_{illegal} \to -\infty
bias 
illegal

 →−∞

e z i l l e g a l → 0 e^{z_{illegal}} \to 0


illegal

 
 →0

经过上式的m a s k i n g maskingmasking我们使无效区域经过s o f t m a x softmaxsoftmax计算之后还几乎为0 00, 这样就避免了无效区域参与计算. Transformer里面有两种mask方式,分别是 padding mask 和 sequence mask, 上面这个就是padding mask, 这种mask在scaled dot-product attention 里面都需要用到,而 sequence mask 只有在 Decoder 的 self-attention 里面用到。后面会看到。

实际实现的时候, padding mask 实际上是一个张量,每个值都是一个Boolean,值为 false 的地方就是我们要进行处理的地方(加负无穷的地方)
最后通过上面的梳理,我们解决了Transformer编码器部分,下面看看Transformer Encoder的整体的计算过程:

字向量与位置编码:
X = E m b e d d i n g L o o k u p ( X ) + P o s i t i o n a l E n c o d i n g   X = EmbeddingLookup(X) + PositionalEncoding \
X=EmbeddingLookup(X)+PositionalEncoding 

X ∈ R b a t c h   s i z e   ∗   s e q .   l e n .   ∗   e m b e d .   d i m . X \in \mathbb{R}^{batch \ size \ * \ seq. \ len. \ * \ embed. \ dim.}
X∈R 
batch size ∗ seq. len. ∗ embed. dim.
 
自注意力机制:
Q = L i n e a r ( X ) = X W Q Q = Linear(X) = XW_{Q}
Q=Linear(X)=XW 
Q

 

K = L i n e a r ( X ) = X W K K = Linear(X) = XW_{K}
K=Linear(X)=XW 
K

 

V = L i n e a r ( X ) = X W V V = Linear(X) = XW_{V}
V=Linear(X)=XW 
V

 

X a t t e n t i o n = S e l f A t t e n t i o n ( Q ,   K ,   V )   X_{attention} = SelfAttention(Q, \ K, \ V) \

attention

 =SelfAttention(Q, K, V) 
残差连接与L a y e r   N o r m a l i z a t i o n Layer \ NormalizationLayer Normalization
X a t t e n t i o n = X + X a t t e n t i o n   X_{attention} = X + X_{attention} \

attention

 =X+X 
attention

  

X a t t e n t i o n = L a y e r N o r m ( X a t t e n t i o n )   X_{attention} = LayerNorm(X_{attention}) \

attention

 =LayerNorm(X 
attention

 ) 
F e e d F o r w a r d FeedForwardFeedForward, 其实就是两层线性映射并用激活函数激活, 比如说R e L U ReLUReLU:
X h i d d e n = A c t i v a t e ( L i n e a r ( L i n e a r ( X a t t e n t i o n ) ) )   X_{hidden} = Activate(Linear(Linear(X_{attention}))) \

hidden

 =Activate(Linear(Linear(X 
attention

 ))) 
重复3.:
X h i d d e n = X a t t e n t i o n + X h i d d e n X_{hidden} = X_{attention} + X_{hidden}

hidden

 =X 
attention

 +X 
hidden

 

X h i d d e n = L a y e r N o r m ( X h i d d e n ) X_{hidden} = LayerNorm(X_{hidden})

hidden

 =LayerNorm(X 
hidden

 )

X h i d d e n ∈ R b a t c h   s i z e   ∗   s e q .   l e n .   ∗   e m b e d .   d i m . X_{hidden} \in \mathbb{R}^{batch \ size \ * \ seq. \ len. \ * \ embed. \ dim.}

hidden

 ∈R 
batch size ∗ seq. len. ∗ embed. dim.
 
这样一个Transformer编码块就执行完了, 得到了X_hidden之后,就可以作为下一个Transformer编码块的输入,然后重复2-5执行,直到Nx个编码块。
好了,编码器部分结束,下面进入解码器部分:


3.2 解码器部分的工作细节
上面我们说完了编码器,看上面这张图,我们发现编码器和解码器其实差不多,只不过解码器部分多了一个Encoder-Decoder Attention, 知道编码器是怎么工作的,也基本会解码器了,但是还是来看几个细节。

编码器通过处理输入序列开启工作。顶端编码器的输出之后会变转化为一个包含向量K(键向量)和V(值向量)的注意力向量集(也就是编码器最终输出的那个从多角度集自身与其他各个字关系的矩阵,比如记为M)。这些向量将被每个解码器用于自身的“编码-解码注意力层”,而这些层可以帮助解码器关注输入序列哪些位置合适。

在完成编码阶段后,则开始解码阶段。解码阶段的每个步骤都会输出一个输出序列(在这个例子里,是英语翻译的句子)的元素(先输出为,为落下去,输出什, 什落下去输出么)

接下来的步骤重复了这个过程,直到到达一个特殊的终止符号,它表示transformer的解码器已经完成了它的输出。每个步骤的输出在下一个时间步被提供给底端解码器,并且就像编码器之前做的那样,这些解码器会输出它们的解码结果 。另外,就像我们对编码器的输入所做的那样,我们会嵌入并添加位置编码给那些解码器,来表示每个单词的位置。

而那些解码器中的自注意力层表现的模式与编码器不同:在解码器中,自注意力层只被允许处理输出序列中更靠前的那些位置。在softmax步骤前,它会把后面的位置给隐去(把它们设为-inf), 这里才是正规的mask操作, 这个mask操作的目的是不能让当前位置的词看到它之后的,只能看到它以及它之前的。 这个具体实现的时候:

拿到注意力矩阵, 用一个上三角矩阵,上三角的值全为0去这个它,相当于把注意力矩阵的山上三角部分变为0, 那么再乘V的时候,当前的位置就无法看到后面的词信息了。

所以对于 decoder 的 self-attention,里面使用到的 scaled dot-product attention,同时需要padding mask 和 sequence mask 作为 attn_mask,具体实现就是两个mask相加作为attn_mask。

举个例子:
假设最大允许的序列长度为10, 先令padding mask为: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 然后再假设当前句子一共五个单词, 在输入第三个单词的时候, 前面有一个开始标识和两个单词, 那么此刻的sequence mask为[1, 1, 1, 0, 0], 然后两个mask相加,得[1, 1, 1, 0, 0, 0, 0, 0, 0, 0]。

那么这个sequence mask到底是为什么要做呢? 难道我不mask了, 我就得不出结果了? 其实不mask是可以得出结果的,但是仅仅是在训练集上。 什么意思? 这里如果不用sequence mask的话,相当于我预测当前输出的时候, 是结合了所有的信息,也就是看到了后面的词语信息。 比如 我/爱/中国/共产党. 在输出爱的时候,如果不mask,会用到后面中国共产党的信息。 But, 我们做预测的时候, 预测当前输出,是看不多后面句子的呀, 毕竟后面句子还没有出来呢? 所以如果训练的时候不mask,就会导致训练和预测出现了一个gap,所以这个sequence mask是必须要的。

这个“编码-解码注意力层”工作方式基本就像多头自注意力层一样,只不过它是通过在它下面的层来创造查询矩阵,并且从编码器的输出中取得键/值矩阵。这个地方简单说一下细节


3.3 最终的线性变换和softmax层
解码组件最后会输出一个实数向量。我们如何把浮点数变成一个单词?这便是线性变换层要做的工作,它之后就是Softmax层。

线性变换层是一个简单的全连接神经网络,它可以把解码组件产生的向量投射到一个比它大得多的、被称作对数几率(logits)的向量里。

不妨假设我们的模型从训练集中学习一万个不同的英语单词(我们模型的“输出词表”)。因此对数几率向量为一万个单元格长度的向量——每个单元格对应某一个单词的分数。

接下来的Softmax 层便会把那些分数变成概率(都为正数、上限1.0)。概率最高的单元格被选中,并且它对应的单词被作为这个时间步的输出。

这张图片从底部以解码器组件产生的输出向量开始。之后它会转化出一个输出单词。

4. Training
我们已经过了一遍完整的transformer的前向传播过程,那我们就可以直观感受一下它的训练过程。

在训练过程中,一个未经训练的模型会通过一个完全一样的前向传播。但因为我们用有标记的训练集来训练它,所以我们可以用它的输出去与真实的输出做比较。

为了把这个流程可视化,不妨假设我们的输出词汇仅仅包含六个单词:“a”, “am”, “i”, “thanks”, “student”以及 “”(end of sentence的缩写形式)。

我们模型的输出词表在我们训练之前的预处理流程中就被设定好。

一旦我们定义了我们的输出词表,我们可以使用一个相同宽度的向量来表示我们词汇表中的每一个单词。这也被认为是一个one-hot 编码。所以,我们可以用下面这个向量来表示单词“am”:


4.1 损失函数
那么我们的损失函数是什么呢?
这里我们使用的是交叉熵损失函数,因为模型的参数(权重)都被随机的生成,(未经训练的)模型产生的概率分布在每个单元格/单词里都赋予了随机的数值。我们可以用真实的输出来比较它,然后用反向传播算法来略微调整所有模型的权重,生成更接近结果的输出。

左边就是我们想要的模型输出,右边是我们训练的模型的输出,是一些概率的形式,训练的时候,我们就先采用前向传播得到一个输出,然后采用交叉熵损失比较模型的输出和真实的期望值,得到梯度反向传播回去更新参数。

4.2 训练小技巧
L a b e l S m o o t h i n g ( r e g u l a r i z a t i o n ) Label Smoothing(regularization)LabelSmoothing(regularization)

这个是什么意思呢? 就是我们准备我们的真实标签的时候,最好也不要完全标成非0即1的这种情况,而是用一种概率的方式标记我们的答案。这是一种规范化的方式。

比如上面我们的答案

我们最好不要标成这种形式,而是比如position #1这个,我们虽然想让机器输出I
我们可以I对应的位置是0.9, 剩下的0.1其他五个地方平分,也就是

position #1   0.02  0.02   0.9  0.02  0.02   0.02

Noam Learning Rate Schedule
这是一种非常重要的方式,如果不用这种学习率的话,可能训练不出一个好的Transformer。

简单的说,就是先让学习率线性增长到某个最大的值,然后再按指数的方式衰减。相当于先有一个热启动阶段,在这个阶段呢? 先让学习率线性增大, 到一定的程度, 再开启冷启阶段,或者叫学习率衰减的方式。 这个方式对于训练Transformer来说,是一个比较重要的策略。

5. Conclusion
这篇文章最经典的核心就是transformer结构,这种结构完全依赖于注意力机制,取代了基于Encoder-Decoder的循环层,并且引入了位置嵌入,Multi-Head Attention机制。

下面分析一下Transformer的特性:

优点:
(1) 每一层的计算复杂度比较低
(2) 比较利于并行计算
(3) 模型可解释性比较高(不同单词之间的相关性有多大)
缺点:
(1) 有些RNN轻易可以解决的问题Transformer没做到,比如复制string,或者推理碰到的sequence长度比训练时更长(因为碰到了没见到过的position embedding)
(2) RNN图灵完备,Transformer不是。 图灵完备的系统理论是可以近似任意Turing计算机可以解决的算法。
这里还应该注意到的一点细节,就是Transformer的设计最大的性能提升关键是将任意两个单词的距离变成了1,也就是可以直接算任意两个词之间的相似度, 对于NLP里面的长期依赖问题有了一个很好的解决方法。 虽然依然是全连接+Attention的结合体,但这个设计已经非常的巧妙和创新。 当然, 这个模型可能会丧失捕捉局部特征的能力, 并且失去的位置信息在NLP也很重要, 虽然有个position embedding,但这个也是权宜之计,并没有改变Transformer结构上的固有缺陷。

代码实现:

https://nlp.seas.harvard.edu/2018/04/03/attention.html
https://www.tensorflow.org/tutorials/text/transformer
参考:

BERT大火却不懂Transformer?读这一篇就够了
从中文Transformer到BERT的模型精讲,以及基于BERT情感分类实战
Self-Attention与Transformer
后记
上面已经详细的说了transformer的编码器的部分, 了解到了transformer是怎样获得自然语言的位置信息的, 注意力机制是怎样的, 其实举个语言情感分类的例子, 我们已经知道, 经过自注意力机制, 一句话中的每个字都含有这句话中其他所有字的信息, 那么我们可不可以添加一个空白字符到句子最前面, 然后让句子中的所有信息向这个空白字符汇总, 然后再映射成想要分的类别呢?

这就是伟大的BERT了, BERT采用了Transformer的编码的部分,在BERT的预训练中, 我们给每句话的句头加一个特殊字符, 然后句末再加一个特殊字符, 之后模型预训练完毕之后, 我们就可以用句头的特殊字符的h i d d e n   s t a t e hidden \ statehidden state完成一些分类任务了,以后有机会再整理吧。

关于transformer, 面试的时候,也是一个非常喜欢问的模型,对于NLP来说,这个应该是必问,大致上会从位置编码,到自注意力的理解,到归一化,mask等,每个细节都是必问题目。

下面就进行一个汇总啦(灵魂20问):

Transformer为何使用多头注意力机制?(为什么不使用一个头)
Transformer为什么Q和K使用不同的权重矩阵生成,为何不能使用同一个值进行自身的点乘? (注意和第一个问题的区别)
Transformer计算attention的时候为何选择点乘而不是加法?两者计算复杂度和效果上有什么区别?
为什么在进行softmax之前需要对attention进行scaled(为什么除以dk的平方根),并使用公式推导进行讲解
在计算attention score的时候如何对padding做mask操作?
为什么在进行多头注意力的时候需要对每个head进行降维?(可以参考上面一个问题)
大概讲一下Transformer的Encoder模块?
为何在获取输入词向量之后需要对矩阵乘以embedding size的开方?意义是什么?
简单介绍一下Transformer的位置编码?有什么意义和优缺点?
你还了解哪些关于位置编码的技术,各自的优缺点是什么?
简单讲一下Transformer中的残差结构以及意义。
为什么transformer块使用LayerNorm而不是BatchNorm?LayerNorm 在Transformer的位置是哪里?
简答讲一下BatchNorm技术,以及它的优缺点。
简单描述一下Transformer中的前馈神经网络?使用了什么激活函数?相关优缺点?
Encoder端和Decoder端是如何进行交互的?(在这里可以问一下关于seq2seq的attention知识)
Decoder阶段的多头自注意力和encoder的多头自注意力有什么区别?(为什么需要decoder自注意力需要进行 sequence mask)
Transformer的并行化体现在哪个地方?Decoder端可以做并行化吗?
简单描述一下wordpiece model 和 byte pair encoding,有实际应用过吗?
Transformer训练的时候学习率是如何设定的?Dropout是如何设定的,位置在哪里?Dropout 在测试的需要有什么需要注意的吗?
 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI周红伟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值