0
点赞
收藏
分享

微信扫一扫

层层剖析,让你彻底搞懂Self-Attention的机制和原理



文章目录

  • ​​本文内容​​
  • ​​为什么要使用Self-Attention​​
  • ​​直观的感受下Self-Attention​​
  • ​​Self-Attenion是如何考虑上下文的​​
  • ​​如何计算相关性分数 α \alpha α​​
  • ​​将 α \alpha α 归一化​​
  • ​​整合上述内容​​
  • ​​向量化​​
  • ​​ d k d_k dk​是什么,为什么要除以 d k \sqrt{d_k} dk​ ​​​
  • ​​参考资料​​

本文内容

本文基于李宏毅老师对 Self-Attention 的讲解,进行理解和补充,并结合Pytorch代码,最终目的是使得自己和各位读者更好的理解Self-Attention

李宏毅Self-Attention链接: ​​​https://www.youtube.com/watch?v=hYdO9CscNes​​

PPT链接见视频下方

通过本文的阅读,你可以获得以下知识:

  1. 什么是Self-Attention,为什么要用Self-Attention
  2. Self-Attention是如何做的
  3. Self-Attention这样如何设计的
  4. Self-Attention公式的细节

为什么要使用Self-Attention

假设现在一有个词性标注(POS Tags)的任务,例如:输入​​I saw a saw​​(我看到了一个锯子)这句话,目标是将每个单词的词性标注出来,最终输出为​​N, V, DET, N​​。

层层剖析,让你彻底搞懂Self-Attention的机制和原理_深度学习

这句话中,第一个​​saw​​​为动词,第二个​​saw​​(锯子)为名词。如果想做到这一点,就需要保证机器在看到一个向量时,要同时考虑其上下文,并且,要能判断出上下文中每一个元素应该考虑多少。例如,对于第一个​​saw​​​,要更多的关注​​I​​​,而第二个​​saw​​​,就应该多关注​​a​​。

这个时候,就要Attention机制来提取这种关系:如果一个任务的输入是一个Sequence(一排向量),而且各向量之间有一定关系,那么就要利用Attention机制来提取这种关系

直观的感受下Self-Attention

层层剖析,让你彻底搞懂Self-Attention的机制和原理_人工智能_02

该图描述了Self-Attention的使用。Self-Attention接受一个Sequence(一排向量,可以是输入,也可以是前面隐层的输出),然后Self-Attention输出一个长度相同的Sequence,该Sequence的每个向量都充分考虑了上下文

Self-Attenion是如何考虑上下文的

层层剖析,让你彻底搞懂Self-Attention的机制和原理_深度学习_03

如图所示,每个输入都会和其他输入计算一个相关性分数,然后基于该分数,输出包含上下文信息的新向量

对于上图,需要与 分别计算相关性分数 (需要和自己也计算一下), 的分数越高,表示两个向量的相关度越高

计算好 后,就可以求出新的包含上下文信息的向量 ,假设 ,则:

同理,对于 ,首先计算权重 , 然后进行加权求和

如果按照上面这个式子做,还有两个问题:

  1. 之和不为1,这样会将输入向量放大或缩小
  2. 直接用输入向量去乘的话,拟合能力不够好

对于问题1,通常的做法是将 过一个Softmax(当然也可以选择其他的方式)

对于问题2,通常是将 乘个矩阵(做个线性变化),然后生成 ,然后用 去乘

如何计算相关性分数 α \alpha α

首先,复习下向量相乘。两个向量相乘(做内积),公式为: , 通过公式可以很容易得出结论:

  • 两个向量夹角越小(越接近),其内积越大,相关性越高。反之,两个向量夹角越大,相关性越差,如果夹角为90°,两向量垂直,内积为0,无相关性

通过上面的结论,很容易想到,要计算 和 的相关性,直接做内积即可,即 。 但如果直接这样,显然不好,例如,句子​​​I saw a saw​​​的​​saw​​​和​​saw​​相关性一定很高,这样不就错了嘛。

为了解决上面这个问题,Self-Attention又额外“训练”了两个矩阵 和

  • 负责对“主角”进行线性变化,将其变换为 ,称为query
  • 负责对“配角”进行线性变化,将其变换为 ,称为key

有了,我们就可以计算 和 的相关分数 了,即:

上面这些内容可以汇总成如下图:

层层剖析,让你彻底搞懂Self-Attention的机制和原理_自然语言处理_04

要计算 (主角)与 (配角)的相关度,需要经历如下几步:

  1. 通过 ,计算
  2. 通过 ,计算
  3. 通过 和 , 计算


上图并没有把 画出来,但实际计算的时候,需要计算 ,即需要计算 和其自身的相关分数。


将 α \alpha α 归一化

还记得上面提到的,之和不为1,所以,在上面的到了 后,还需要过一下Softmax,将归一化。如下图:

层层剖析,让你彻底搞懂Self-Attention的机制和原理_归一化_05

最终,会将归一化后的 作为 与其它向量的相关分数。 同理, 向量与其他向量的相关分数也这么求。


不一定非要用Softmax,你开心想用什么都行,说不定效果还不错,也不一定非要归一化。 只是通常是这么做的


整合上述内容

求出了相关分数 ,就可以进行加权求和计算出包含上下文信息的向量 了。还记得上面提到过,如果直接用 与 进行加权求和,泛化性不够好,所以需要对 进行线性变换,得到向量 ,所以Self-Attention还需要训练一个矩阵 用于对 进行线性变化,即:

然后就可用 与 进行加权求和,得到 了。

将求 的整个过程可以归纳为下图:

层层剖析,让你彻底搞懂Self-Attention的机制和原理_自然语言处理_06

用更正式的话描述一下整个过程:

有一组输入序列 ,其中 为向量, 将序列 通过Self-Attention,可以将其转化为另外一个序列 ,其中向量 是由向量 结合其上下文得出的, 的求解过程如下:

  1. 求出查询向量 , 公式为
  2. 求出 ,公式为
  3. 求出 , 公式为
  4. 将 进行归一化得到 ,公式为
  5. 求出向量, 公式为:
  6. 求出 , 公式为


其中, 都是训练出来的


到这里Self-Attention的面纱已经揭开,但还没有结束,因为上面的步骤如果写成代码,需要大量的for循环,显然效率太低,所以需要进行向量化,能合并成向量的合成向量,能合并成矩阵的合成矩阵

向量化

向量 的矩阵化,假设列向量 维度为 ,显然可以将输入转化为矩阵 ,公式为:

接下来定义 矩阵的维度为 ,它们三个的维度必须一致, 是一个需要调的参数, 也是 的维度(从后面公式可以看出),定义好 的维度后,就可以将 矩阵化了,

向量 的矩阵化,公式为:

同理,向量k的矩阵化,公式为:

同理,向量v的矩阵化,公式为:

得到了矩阵和,那么就很容易得出相关分数 的矩阵了,

相关分数 的矩阵为

进一步, 的矩阵为

有了, 有了,那就可以对输出向量 进行矩阵化了,

输出向量b的矩阵化,公式为:

将上面全部整合起来,就可以的到,整合后的公式

如果你看过其他文章,你应该会看到真正的最终公式如下:

其实我们的公式和这个公式只差了一个转置和 。转置不比多说,就是表示方式不同。

d k d_k dk​是什么,为什么要除以 d k \sqrt{d_k} dk​ ​

首先,是输入向量的数量,也就是上面的 。而矩阵相乘会放大原有矩阵的标准差,放大的倍数约为,为了将标准差缩放回原来的大小,所以要除以

例如,假设 和 的均值为0,标准差为1。则矩阵 的均值为0,标准差为 ,矩阵相乘使得其标准差放大了 倍,这个 是 中的


矩阵的均值就是把所有的元素加起来除以元素数量,方差同理。


可以通过以下代码验证这个结论(数学不好,只能通过实验验证结论了,哭):

Q = np.random.normal(size=(123, 456)) # 生成均值为0,标准差为1的 Q和K
K = np.random.normal(size=(123, 456))
print("Q.std=%s, K.std=%s, \nQ·K^T.std=%s, Q·K^T/√d.std=%s"
% (Q.std(), K.std(),
Q.dot(K.T).std(), Q.dot(K.T).std() / np.sqrt(456)))
Q.std=0.9977961671085275, K.std=1.0000574599289282,
Q·K^T.std=21.240017020263437, Q·K^T/√d.std=0.9946549289466212

通过输出可以看到,Q和K的标准差都为1,但是两矩阵相乘后,标准差却变为了 21.24, 通过除以 ,标准差又重新变为了 1

再看另一个例子,该例子Q和K的标准差是随机的,更符合真实的情况:

Q = np.random.normal(loc=1.56, scale=0.36, size=(123, 456)) # 生成均值为随机,标准差为随机的 Q和K
K = np.random.normal(loc=-0.34, scale=1.2, size=(123, 456))
print("Q.std=%s, K.std=%s, \nQ·K^T.std=%s, Q·K^T/√d.std=%s"
% (Q.std(), K.std(),
Q.dot(K.T).std(), Q.dot(K.T).std() / np.sqrt(456)))
Q.std=0.357460640868945, K.std=1.204536717914841, 
Q·K^T.std=37.78368871510589, Q·K^T/√d.std=1.769383337989377

可以看到,最开始Q的标准差为 , K的标准差为 ,结果矩阵相乘后标准差达到了 , 经过缩放后,标准差又回到了。



参考资料

​​李宏毅Self-Attention​​: https://www.youtube.com/watch?v=hYdO9CscNes

​​超详细图解Self-Attention​​: https://zhuanlan.zhihu.com/p/410776234



举报

相关推荐

0 条评论