目录
Self-Attention
1.键值对注意力
2.加权求和
3.Q K V 矩阵
4.的意义
5.补充
6.Self-Attention的代码实现
在介绍基础知识之前,我们先抛出两个问题:
(1)向量的内积是什么,如何计算,它的几何意义是什么?
(2)一个矩阵W与其转置相乘,得到的结果有何意义?
Transformer中最核心的部分就是键值对注意力了,键值对注意力最核心的公式就是下面这个公式了,这个公式其实蕴含了很多点,这些点都要Get到。
首先上面这个公式可能看起来挺难懂,但是我们可以看看下面这个公式,下面这个公式的意义是什么?先抛开Q,K,V矩阵不谈,self-Attention机制最原始的形态其实长下面这个样。
显然就是一个矩阵与其自身的转置相乘了,可是这有什么意义呢?我们知道,矩阵可以看作由一些向量组成,一个矩阵乘以它自己转置的矩阵,其实可以看成这些向量分别与其他向量计算内积,我们知道矩阵转置以后第一列就是原来的第一行了,其实就是在计算第一行向量与第一行向量的内积,第一行乘以第二列是计算第一个行向量与第二个行向量的内积,第一行乘第三列是计算第一行向量乘第三行向量的内积。
回到我们刚开始提出的问题,向量的内积,其几何意义是什么?
答:表征两个向量的夹角,表征一个向量在另一个向量上的投影。特别的,如果一个向量如a是某个坐标轴的单位坐标向量,那么,两个向量的内积就是向量b在此坐标轴上的坐标值。这个结论非常重要,这是傅里叶分析的理论基础。
其他几何意义:从内积数值上我们可以看出两个向量的在方向上的接近程度。当内积值为正值时,两个向量大致指向相同的方向(方向夹角小于90度),当内积值为负值时,两个向量大致指向相反的方向(方向角大于90°)。当内积值为0时,两个向量互相垂直。
另外如果两个向量长度相等,或者将两个向量化为其所在方向的单位向量(如:,)两个向量的点积得到的结果为两向量的夹角,可以通过这个夹角的大小来判断两个向量的相似性,即,当两个向量为单位向量时,他们点击的几何意义也可以理解为他们的相似性(越大越相似,越小越不相似,这个原来常被用于判断文本的相似性)。
我们假设X=,其中X为一个二维矩阵,为一个行向量,对应下面的图,对应“早”字embedding之后的结果,以此类推。
下面的运算模拟了一个过程,即。我们来看看结果究竟有什么意义?
图1 行向量运算的结果首先,行向量分别与自己和其他两个行向量做内积,得到了一个新的向量。我们回想前文提到的向量的内积表征两个向量的夹角,表征一个向量在另一个向量上的投影。那么新的向量有什么意义?是行向量在自己和其他两个行向量上的投影。投影的值大小有什么意义?
投影的值大,说明两个向量相关度高(因为在两个向量长度相等的情况下,投影的值越大,说明角度越小,而且如果投影的值为负数的话,那么角度必然就>90°,投影的值为正数的话,角度必然<90°)。
我们考虑,如果两个向量夹角是九十度,那么这两个向量线性无关,完全没有相似性。
更进一步,这个向量是词向量,是词在高维空间的数值映射。词向量之间相关度高表示什么?
是不是在一定程度上(不是完全)表示,在关注词A的时候,应当给予词B更多的关注?
上图只是展示了一个行向量运算的结果,那么矩阵代表着什么?
矩阵是一个方阵,我们以行向量的角度理解,里面保存了每个向量与自己和其他向量进行内积运算的结果。
至此,我们理解了中的意义,我们进一步,Softmax的意义何在呢? 我们知道softmax的公式如下所示,其实Softmax就是起了一个归一化的作用。
也就是下图所示:
那我们想,Attention机制的核心是什么?
那么权重从何而来呢?就是这些归一化之后的数字。当我们关注“早”这个字的时候,我们应当分配0.4的注意力给它本身,剩下0.4关注“上”,0.2关注“好”。当然具体到我们的Transformer,就是对应向量的运算了,这是后话了。
接下来我们再看下面的这个热力图Heatmap,其中的矩阵是不是也保存了相似度的结果?
那么我们再回到公式,最后一个X有何意义?完整的公式究竟代表什么?我们继续之前的计算,下图所示:
图4 乘以X之后的结果我们取的一个行向量举例。这一行向量与X的一个列向量相乘,表示什么?
观察图4,行向量与X的第一个列向量相乘,得到了一个新的行向量,并且这个行向量与X的维度相同。
在新的向量中,每一个维度的数值都是由三个词向量在这一点维度的数值加权求和得来的,这个新的行向量就是“早”字词经过注意力机制加权求和之后的表示。其实我们可以这样理解,就相当于我们已经有了早对早、上、好这仨字的注意力权重向量了(一般是两个词向量相关度越高,需要给的关注注意力就越多),那么我们就需要用这个注意力权重向量乘以原来的早上好词向量,我们就能得到早这个词向量在关注了早、上、好这三个字之后的带有注意力的词向量了。
一张更形象的图是这样的,图中右半部分的颜色深浅,其实就是我们上图中黄色数值的大小,意义就是就是单词之间的相关度(相关度其本质是由向量的内积度量的)
接下来我们再来解释一下原始公式中一些细枝末节的问题。
在我们之前的例子当中并没有出现Q K V的字眼,因为它其实并不是公式中最本质的内容,那么Q K V究竟是什么,请看下图。
其实,许多文章中所谓的QKV矩阵。查询向量之类的字眼,其来源是X与矩阵的乘积,本质上都是X的线性变换。
为什么不直接使用X而要对其进行线性变换呢?当然是为了提升模型的拟合能力,矩阵W都是可以训练的,起到一个缓冲的效果。如果真正读懂了这个矩阵的意义,那么我们就可以理解好所谓查询向量一类的字眼了。
、 假设里的元素均值为0,方差为1,那么中元素的均值为0,方差为d。当d变得很大时,A中的元素的方差也会变得很大,如果A中的元素方差很大,那么的分布会趋于陡峭(分布的方差大,分布集中在绝对值大的区域)。总结一下就是的分布会和d有关。因此A中没有给元素除以后,方差又变为1.这使得的分布“陡峭”程度与d解耦,从而使得训练过程中梯度值保持稳定。
对self-attention来说,它跟每一个input vector都做attention,所以没有考虑到input sequence的顺序。更通俗来讲,我们可以发现前文的计算没有给词向量都与其他词向量计算内积,得到的结果丢失了我们原来文本的顺序信息。对比来说,LSTM是对于文本顺序信息的解释是输出词向量的先后顺序,而上文的计算对sequence的顺序这一部分则完全没有提及,你打乱词向量的顺序,得到的结果仍然是相同的。这就牵扯到Transformer的位置编码了,详细请看Attention is all you Need这篇论文。
# self-Attention 机制的实现
from math import sqrt
import torch
import torch.nn as nn
import numpy as np
class Self_Attention(nn.Module):
# input : batch_size * seq_len * input_dim
# q : batch_size * input_dim * dim_k
# k : batch_size * input_dim * dim_k
# v : batch_size * input_dim * dim_v
def __init__(self, input_dim, dim_k, dim_v):
super(Self_Attention, self).__init__() #在init方法中添加super继承父类的属性和方法
self.q = nn.Linear(input_dim, dim_k) #相当于添加Wq,变换为具有新dim的Q
self.k = nn.Linear(input_dim, dim_k) #相当于添加Wk,变换为具有新dim的k
self.v = nn.Linear(input_dim, dim_v) #相当于添加Wv,变换为具有新dim的v
self._norm_fact = 1 / sqrt(dim_k) #相当于计算根号下Dk分之一
def forward(self, x):
Q = self.q(x) # Q: batch_size * seq_len * dim_k
K = self.k(x) # K: batch_size * seq_len * dim_k
V = self.v(x) # V: batch_size * seq_len * dim_v
atten = nn.Softmax(dim=-1)(
torch.bmm(Q, K.permute(0, 2, 1))) * self._norm_fact # Q * K.T() # batch_size * seq_len * seq_len
output = torch.bmm(atten, V) # Q * K.T() * V # batch_size * seq_len * dim_v
return output
if __name__ == '__main__':
X = torch.randn(4,3,2) #相当于4批需要计算的3个长度为2的词向量
print(X)
print(X.size())
self_attention = Self_Attention(2,4,5) #input_dim, dim_k, dim_v
res = self_attention(X)
print(res)
print(res.size())
结果:
tensor([[[ 0.2913, 0.0801],
[-0.8322, 2.2337],
[-0.2029, -0.8321]],
[[-1.8118, -1.2393],
[ 0.9505, 0.4140],
[-0.1931, 0.9636]],
[[-0.9641, -0.8094],
[-0.1577, 0.0490],
[-1.0628, -0.4931]],
[[ 2.6129, 0.2948],
[-0.9158, -0.5430],
[ 0.3036, -0.1576]]])
torch.Size([4, 3, 2])
tensor([[[-5.1425e-01, -2.8415e-01, 1.2320e-01, -4.0364e-01, -1.5285e-01],
[-2.2058e-01, -6.0590e-02, -4.8856e-02, -1.3084e-01, -3.6364e-01],
[-6.5576e-01, -3.6036e-01, 1.9014e-01, -5.1060e-01, -6.3155e-02]],
[[-3.2100e-01, -2.8709e-01, 8.5985e-02, -3.4073e-01, -2.3504e-01],
[-3.9653e-01, -2.7573e-01, 9.5358e-02, -3.5738e-01, -2.0677e-01],
[-4.9031e-01, -1.5591e-02, -1.7624e-02, -1.8686e-01, -2.6432e-01]],
[[-4.4803e-01, -7.7552e-02, 5.2922e-03, -2.2075e-01, -2.5921e-01],
[-4.5434e-01, -6.4217e-02, -1.9912e-04, -2.1251e-01, -2.6151e-01],
[-4.5307e-01, -6.4322e-02, -4.0101e-04, -2.1216e-01, -2.6202e-01]],
[[ 1.2217e-01, -3.7245e-01, 4.0454e-02, -2.5756e-01, -3.9392e-01],
[ 1.2678e-01, -3.7442e-01, 4.0530e-02, -2.5754e-01, -3.9517e-01],
[ 1.0950e-01, -3.6501e-01, 3.9226e-02, -2.5606e-01, -3.9126e-01]]],
grad_fn=)
torch.Size([4, 3, 5])