内容有点长,应该是史上最详细、最有说服力 的一篇文章。因为几乎我能想到的细节都写了。后面attention的计算流程,我使用的例子的计算结果,和调用pytorch中的attention模块的计算结果是一致的,所以是最有说服力的。
如果你是小白,最好从头看,前面写的都是原理。如果你有点基础,就从文章中间开始看,后半部分是注意力模块的计算过程的详细讲解和分析。而且分析思路是独树一帜的,和网上很多类似文章是不一样的。
五、注意力机制Attention
在讲Transformer之前,我先把注意力机制Attention单独拿出来先讲一下。
一是因为attention是Transformer中非常关键的技术,也是transformer之所以区别其他模型的关键之处。但是transformer本身的架构就非常复杂了,到真正开讲transformer时才讲attention,transformer的篇幅就会又臭又长,所以这里我单独把attention先提出来讲。
二是因为attention从2016年被transformer发扬光大后,就一直支撑着NLP领域的发展,现在也已经成为了整个大语言模型领域的根基,就连现在火热的大模型微调技术也和注意力机制相关,比如LoRA。所以要学transformer就必须要先学自注意力机制,要学大语言模型以及大模型微调也必须要学习自注意力机制。
三是因为当下注意力机制不仅与Transformer和大模型有关,还和各种各样的深度学习架构进行联用,形成深度学习的融合。比如和循环神经网络(RNN)、卷积神经网络(CNN)的结合。比如它还可以作为特征提取器,去完成在各个环节上的深度学习融合。所以现在注意力机制已经成为深度学习从业者必备的关键技能了。所以有必要单独开一个章节来说透什么是注意力机制、自注意力机制、多头自注意力机制等相关概念。
(一)注意力机制的发展历程
其实注意力机制最早不是诞生在NLP领域的,是在CV领域。很明显嘛,我们看一张图像时,是一眼就看到图片中的重点信息,而不是所有的像素都看。所以注意力机制最早的研究是在CV领域。
早期关于注意力的探索的代表作是1998年的《A model of saliency-based visual attention for rapid scene analysis》论文,论文作者提出了一种视觉注意力系统:将多尺度的图像特征组合成单一的显著性图,利用动态神经网络按显著性顺序 选择重点区域。
此后2014年,谷歌DeepMind发表的《Recurrent models of visual attention》使注意力机制受到广泛关注,该论文首次在RNN模型上应用注意力机制进行图像分类。
2015年,深度学习三巨头之一Yoshua Bengio等人在《Show, attend and tell: Neural image caption generation with visual attention》中提出了两种基于注意力机制的图像描述生成模型,即使用基本反向传播训练的soft attention方法和使用强化学习训练的hard attention方法。
而将注意力机制首次应用到NLP领域的是,2015年Yoshua Bengio等人发表的《Neural machine translation by jointly learning to align and translate》论文,论文实现了同步的对齐和翻译,解决了以往神经机器翻译(NMT)领域使用encoder-decoder架构的一个潜在问题:将信息都压缩在固定长度的向量,无法对应长句子。
然而真正把注意力机制推向风口浪尖的是,2017年谷歌机器翻译团队发表的《Attention is all you need 》这篇论文,论文提出了一个全新的架构Transformer 模型架构。这个架构目前被公认是继承MLP、CNN、RNN后的第四大基础模型结构。而Transformer架构又是完全基于自注意力机制 。也就是说注意力机制是transform与其他架构最显著区别的核心关键。此后又基于Transformer的BERT、GPT等架构直接点燃了当前全球狂热的AIGC应用。
于是不管是NLPer还是CVer都又纷纷回头深入研究注意力机制,出现了许多基于注意力机制的研究改进和跨界融合。比如通道注意力、空间注意力等也在CV领域风生水起,在多个计算机视觉任务上取得了顶尖性能,甚至一度挑战了CNN在CV领域的霸主地位。比如著名的非卷积架构ViT(Vision Transformer)就是将transformer应用到视觉领域的典型代表。
所以,目前注意力机制已经逐渐成为深度学习领域的重要组成部分,被广泛应用于自然语言处理、计算机视觉、语音识别等多个领域,以提高模型对重要信息的关注和处理能力。
也所以,以后当你再看到像什么软注意力、硬注意力、空域注意力、通道注意力等这些概念,你脑子里立马就知道这些是处理图像的。而NLP领域的注意力机制主要就是自注意力机制、交叉注意力机制、多头注意力机制等几个概念,本篇讲NLP领域中的注意力机制,不扯远,在深度学习领域很难为讲某个具体概念而讲,因为每个概念背后都有大量的背景知识,当你有太多的逻辑断点时,就很难讲清楚了。
(二)从序列->样本->词向量->embedding->attention
1、从序列说起
回到NLP领域,NLP的本质就是个序列问题 。比如我给你说三句话"我爱中国"、"爱我中国"、"我中国爱"这三句话。如果让模型看每个词,那就是每个词自身的含义,比如我字就仅仅表示自己的意思;爱字表示喜欢的意思;中国表示中华人民共和国的意思。
但我们人类都知道其实这三句话的语义大不同。其中"我中国爱"是一个病句,不知道想表达什么;"我爱中国"和"爱我中国"表示的基本意思是一致。而且把"我爱中国"和"爱我中国"这两句话放到人类的知识域中,人类按照经验,还能区分出"我爱中国"和"爱我中国"这两句话中的细微差别,比如二者可能表达的场景不一样,甚至还能继续推断出,比如爱得热烈、或者推断出这个人的国籍有可能是中国,甚至还可以推断出这个人的三观行为、场景、心态等等,这些都是这三个词有序排列 后代表的语义 。我们想让模型理解的不仅是每个词本身的含义,还想让模型理解这些词按照某种特定顺序排列起来后,其背后可能隐含的语义。
所以要使模型像人一样理解文字,它不仅要理解每个词本身的含义,它还得理解词与词之间位置代表的含义。
2、再次重申一下"样本"这个概念:
比如在时间序列数据中,一个时间点对应的一条数据就是一个样本,样本和样本之间的顺序依时间排列,不能缺失也不能乱序,比如股票历史价格数据、气温数据等。这些数据变化的本质更多的是隐藏在样本和样本的时序排序中,而不是单个样本自身特征中。
比如文本数据,一个字、或者一个词就是一条样本。所以样本和样本之间的顺序也是不能乱序,不能缺失的。样本与样本之间位置关系就是语义的表达。一旦样本之间的顺序打乱,那文本的语义就天翻地覆。
再比如语音信号、视频数据等都是序列数据。其样本和样本之间也是不能缺失不能乱序。
所以要使模型像人一样理解文字表示的语义,它不仅要理解每个词本身的含义,它还得理解样本与样本之间的联系 。
也所以序列问题的本质就是如何理解样本与样本之间的关系 。之前我们学的机器学习算法、FNN、CNN时,这些算法一般都是学习样本的特征和标签之间的关系 。但是在序列数据中,特征和标签之间的规律都隐藏在样本和样本之间的联系中 ,这就让之前的那些算法无法挖掘其中的规律。所以这也是我们要专门创造处理序列数据算法 (Sequence Models)的根本原因。
也所以序列模型(Sequence Models) 的根本诉求就是要建立样本和样本之间的关联,并借助这种关联提炼出对序列数据的理解。或者说序列模型的根本任务就是找出样本和样本之间的关联,建立起样本与样本之间的根本联系,模型才能够对序列数据实现分析、理解和预测。
3、在NLP中,一条样本对应一个词,一个词被映射成一个词向量
让模型理解语义,而模型只认识数字,所以首先要把文本转化为数字:
战略方向是:把所有样本都用向量来表示。战术实操是:先分词->把每个词映射成词向量。
关于分词 ,英文文本一般是按单词以及后缀进行分的,中文文本一般都是按字或词来分的。分词其实是个big topic,避免失焦这里不展开讨论。我们先按英文语料、空格分词,继续讲解。
输入的英文文本,被分完词后,每个单词就是一个token ,然后将这些tokens关联到一个称为嵌入向量的高维向量空间 ,这个过程我们称为词嵌入embedding。
4、怎样选择高维空间?又是如何嵌入的?
你有两条道路:
(1)从0开始,自己重新造轮子:
此时你手头上拿到的文本语料就是你的文本数据。你可以这样操作:先把你所有的文本数据分完词后->然后去重->构建成一个字典->给这个字典进行标签编码->然后再用pytorch框架中的embedding类,把标签编码映射到字典对应的高维空间。流程如下:
上图就是embedding编码技术,在NLP中又叫词嵌入技巧word embedding。embedding编码技术对于深度学习非常重要。网上有文章这样评价embeding编码技术:"它能把万物嵌入万物,是沟通两个世界的桥梁,是打破次元壁的虫洞!" 还说用数学语言来说就是:"它是单射且同构的"。我去,说得这么玄乎,主打一个让你看不懂,怀疑人生。其实从上图看,embedding仅仅就是一种映射方式。将字典中的词汇从一个标量的数字表示形式,映射为高维向量的数字表示。而且这种映射过程是通过矩阵乘的操作完成的(上图右边有计算过程),也就是说可以通过线性层来完成,或者说其实embedding层就是一个线性层,这个线性层的参数矩阵就是上图的字典矩阵B,而且在最开始是随机生成的。
(2)站在别人的肩膀上,拿别人已经训练好的词向量为己用:
你可以使用别人已经训练过的高维空间。比如如果别人已经训练过一个理解侦探小说的模型,它就会有一个已经训练好的映射关系(就类似上图中的字典矩阵B),此时你自己的任务是训练另外一本侦探小说,你就可以直接使用人家的这个已经训练好了的字典矩阵。我这样说也只是一种同理类比,就是想表达,其实现在市面上很多分词工具给我们提供的功能就是这个道理。比如Jieba分词、HanLP、THULAC\FuanNLP\LTP\SNLP:
THULAC是清华大学体提供的分词工具包。
FuanNLP是复旦大学的。据说复旦的最好用。
LTP是哈工大的。
SNLP,Sanford NLP for Chinese,是斯坦福大学开发的NLP工具,提供分词、词性标注、命名实体识别等功能,都是基于深度学习的分词方法。
我们可以直接使用它们的,就不用重复造轮子,资源浪费了。而且还能站在一个优秀的起点开始训练。
(3)为什么说"而且还能站在一个优秀的起点开始训练"?
因为你拿别人的词向量,都是别人已经训练好的词向量,也就是有一定语义表示 了的词向量。比如表示苹果梨葡萄的词向量彼此距离要近一点,表示猫狗老虎的词向量彼此近一些。而你自己造词典->embedding的词向量还是一个完全没有语义 的词向量。因为embeding层初始化的句子就是一个随机矩阵啊!所以一开始你的词向量就是一堆随机的向量。
但是,上面我又说embedding层其实就是一个线性层是因为,我们在模型训练过程中,在损失函数的牵引下,embedding层的参数是会随着损失降低的方向进行迭代的 。所以只有当我们的模型也训练完毕,此时我们的词向量才是有语义的词向量。这也是embedding层的精髓所在。
但是,训练过模型的同学都知道,模型在一个优秀的起点 开始训练,训练过程会比较丝滑,损失曲线会以一个优美漂亮的弧度下降最后水平。如果你的训练起点很糟糕,那训练过程损失曲线就是大幅上下震荡,面目可憎,一脸不可思议,而且很可能训练很久都无法收敛。而且训练模型也是有技巧的,部分理论可以参考【深度学习】第六章:模型效果评估与优化_模型评估与优化-CSDN博客 这篇文章。
所以即使我们自己搭建embedding层进行语义学习,也建议大家在一个优秀的起点上开始学习,所以也建议大家要站在别人的肩上继续前进。
5、那么词向量到底是如何携带语义的?
前面说过,embedding让词向量有语义表示是在损失函数牵引下,逐渐迭代出来的语义。我们看看词向量是如何逐渐开始携带语义的:
在所有可能的嵌入向量构成的高维空间中,方向可以对应语义 。就是不断地迭代过程中,不停地扭转各个词向量的方向,直到很多方向能表示它的语义。比如上图中的黄色箭头,就可以表示男性和女性这两种语义。在一个词向量空间中,像这种方向肯定是有很多,所以可能会携带很多的语义。
或者我们从低维角度看,从字典维度看,也就是上图在三维空间中携带语义的词向量,坍塌到一维字典中,那nephew、man、uncle、"男性"这几个词的编码都比较接近,而同时niece、woman、aunt、"女性"这几个之间的编码更接近。你可以理解成聚类吧,就是之前所有的词向量都是随机的,迭代完毕后的词向量开始按照语义聚类了,物以类聚了。
上图训练完毕后携带语义的词向量,我们称为查找表(lookup table)。也就是迭代收敛后的embedding层的参数矩阵。
6、从记忆->循环网络->attention,让模型有语义推断的能力
上面的查找表是没有上下文参照的查找表。就是embedding层的表示的语义,也仅仅是每个单词自身的语义。也就是让苹果梨葡萄的词向量彼此靠近一点,让猫狗老虎的词向量彼此近一点。但是这种语义仅仅限于词语义本身。如果给模型一个"苹果公司"或者"我的小猫咪"这种文本,那模型就推理不出啥意思了,它只知道苹果是水果,为什么这里变成了一个公司的名字?!而小猫咪也只是一个小动物,怎么会变成爱人之间的称呼了?!
遇到这样的文本,模型对"苹果"和"小猫咪"的语义理解就得从上下文的联系中来推理了。比如苹果后面跟了一个公司,那苹果大概率就是一个公司的名称,而非水果;如果小猫咪前后都是两个爱人之间在对话,那大概率就是一个昵称而非小动物了。而这种语义理解都是建立在对上下文的理解。也就是需要从上下文中来推理。而这是embedding层做不到的事情,因为embedding层对应的是没有上下文参照的查找表。
要理解上下文就得有记忆 ,就是对上下文见过的词有记忆,才能凭记忆去推理"苹果"和"小猫咪"的真正语义。如何推理呢?简单,在词向量空间中,把本来是代表某种水果的"苹果"词向量,拉到代表公式语义的方向即可。此时的"苹果"词向量就表示公司了呀。"小猫咪"词向量同理喽。那又如何拉呢?用矩阵乘啊,向量在空域中变换不就是通过矩阵变换的嘛。那如何找到那个合适的矩阵,就正好和"苹果"词向量一乘,就把"苹果"拉到公司的语义了呢?这个合适的矩阵其实就是前后文的依赖关系。聚焦最近十几年的时间跨度,NLP领域推出的RNN、LSTM、encoder-decoder架构、注意力机制等,面临的核心问题都是:如何理解文本前后文的依赖关系。
面对这个问题,在注意力机制之前一般使用的是Bag-of-words的方法,但这种方法不能很好的解决语义理解问题。所以后来提出的RNN一族(RNN、GRU、LSTM),就是让样本在时间维度上循环,在时间维度上"记住"样本和样本之间的关系。这种方法虽然一定程度上解决了样本间的依赖问题,但是模型不好训练。因为RNN模块太简单,训练时是同一套参数的n次方,训练很久都无法收敛。一句话,模型不好用!于是人们开始在空间维度上进行改进,就是让模型复杂一点,于是就诞生了基于RNN或者CNN的encoder-decoder架构,但是面对复杂场景时依旧无能为力。一是计算时是时序依赖的,无法并行处理,效率太低呀,只能应付一些小型文本数据;二是无法有效捕捉长距离依赖问题,就是样本和样本之间的距离过长时,二者之间的关系就非常微弱了,无法捕捉长距离样本之间的关系。
直到谷歌的《Attention is all you need》论文发表,注意力机制重新进入NLPers眼前。注意力机制也是解决序列中样本与样本之间依赖问题的,但是它完全不同于RNN一族那样,在时序空间下一个样本一个样本的看。注意力机制是一次性看完整个sequence中的所有样本,就是用空间换时间。我要一下看完所有的样本,这样所有的样本我都看过了。然后再计算所有样本之于其他样本之间的重要性,得到一个相关性矩阵(你也可以理解为就是一个权重矩阵,就是一个上下文信息重要性的矩阵),然后根据这个相关性矩阵扭转输入的各个词向量,直到各个新向量都能根据上下文信息指向最恰当的语义方向。
7、attention模块是如何扭动样本的?
这种就是embedding层词嵌入后的效果,embedding层训练完毕的词向量只有一个,但是很多单词都是含有多种语义的。而具体是哪个意思是得参照上下文的。mole在第一个句子中就表示是一种动物,到第二个句子中就是单位的意思了,到第三个句子中就是脸上的痣的意思。所以这是查找表无法解决的问题。也就是"没有考虑样本和样本之间关系"的问题。也是一个"如何考虑样本和样本之间关系"的问题。
attention模块是通过,生成样本与样本之间的注意力分数矩阵 attention_score(你可以理解为重要性矩阵、权重矩阵都可以),用这个矩阵*原来的词向量,生成新的词向量,让新的词向量指向新的语义方向:
可见,当数据流进入attention模块后,attention模块可以看到所有的上下文信息。attention模块就根据上下文信息生成一个注意力分数矩阵,注意力分数矩阵*输入的每个词向量。此时你可以把注意力分数矩阵理解为权重矩阵,权重矩阵乘以每个词向量后生成的新向量,新向量中就携带了大量的上下文样本的信息,也就是根据上下文含义,拉动旧向量转动到新向量的方向。而新向量方向表示的语义就是通过上下文推导出来的、携带上下文信息、变得更加准确、丰富的词向量了。
或者这样说吧:mole这个词有三种语义,但是mole对应的词向量在某一个时刻只能有一个向量表示。attention可以根据上下文,让mole在不同语境下,表示成不同的向量,也就是不同的语义。比如mole本来的向量是[1,2,3,4],大体上能表示脸上的痣的意思。当序列"One mole of carbon dioxide"流经attention模块时,attention模块计算了这句话中的所有词向量之间的相关性,生成一个各个词之间重要性的权重矩阵,就用这个权重矩阵拉动mole的词向量从[1,2,3,4]变成[6,7,8,9]了,而[6,7,8,9]的方向正是表示单位语义的方向。
因为one这个词向量很可能就和单位的语义方向就很近,而且carbon和dioxide是二氧化碳嘛,所以这两个词就也可能和单位方向的语义比较近。而attention模块算的就是一个权重矩阵,假如它算出来的是mole'=0.1mole+0.5ONE+0.2carbon+0.2dioxide,这计算出来的mole'可不就和它原来的语义就相差很远了,就好像就被扭转到表示单位的那个语义方向了。
同理,当"American shrew mole"这个序列流经attention模块时,attention模块就会根据这句话各个词之间的相关性,生成这句话的注意力分数矩阵,这个矩阵拉动着mole从[1,2,3,4]变成了[11,12,13,14],而[11,12,13,14]向量的指向正是各种小动物的聚集区。
一个训练的非常好的注意力模块,它能计算出需要给初始的泛型 嵌入加个什么向量,才能把它移动到上下文对应的具体方向上。我们再看一个例子:
比如"Tower"这个词就是一个泛型词,就是它的语义是很宽泛的,因为它是一个种类嘛。所以这个词在词向量空间中的方向也是很宽泛的,比如它可以和很多高大物体的名称的方向差不多(上左图)。
但是如果给Tower字前加一个Eiffel,那就具体表示埃菲尔铁塔了。所以此时我们就需要一个机制能更新这个向量,让"Tower"更准确地指向"埃菲尔铁塔"的方向,而且新向量也许还会和巴黎、法国、钢铁制品等词的方向也很近(上中图),就是也相关,就是语义方向大体一致。
但是如果我们再加上一个Miniature一词,那么向量就得进一步更新,就不能和高大的事物相近了,就得和小物件种类相近(上右图)。
所以注意力机制attention是逐步调整这些词嵌入,移动成一个新向量,这个新向量不单单是编码单个词,还融入了更丰富的上下文含义 。所有词向量流经许多层的注意力模块后,新嵌入的词向量的信息就加入了上下文信息,新的词向量就比原来的语义更加准确了。熟悉CV的同学,看到这里是不是会联想到,卷积网络cnn中的卷积核组,疯狂提取图片特征啊,是不是特别类似!卷积核组本身就是各种不同的特征组,比如有点、斑、线、圈等不同的特征,当原图出现角点时,那有角点的特征模板就被激活;当原图出现圈时,有圈的特征模板就被激活。这里的词向量也时类似的道理。就是attention模块中包含有大量的转换方向,当怎么转动损失函数减少最多时,那个方向的矩阵就被激活,拉动词向量往最能代表它的含义上转动。
感兴趣的可以参考【深度视觉】第十七章:卷积网络的可视化_卷积网络可视化-CSDN博客 和 【深度视觉】第三章:卷积网络诞生前:卷积、边缘、纹理、图像分类等_深度学习寻找分界线-CSDN博客
所以,attention模块一是可以精细化一个词的含义,让每个样本表示的语义更加准确;二是让每个样本都携带了上下文样本的信息,并且上下文信息还可以长距离传递,因为它是一次看完序列中的所有样本,才生成的注意力分数矩阵:
(三)自注意力机制的计算流程
上面啰啰嗦嗦一大堆的扭转词向量 ,也许真的是太抽象,无法理解。那本部分讲解数学计算流程,搭配着理解,你就会豁然开朗。
因为《Attention is all you need》论文中提出的是自注意力机制 (Self-Attention),所以这里我们先讲Self-Attention。论文中的Self-Attention的最核心的公式是:
公式很简单,其实也就是很简单。但市面上的相关博文都讲的极其晦涩,各种专业词汇、各种流程图,让你云里雾里,主打一个眼花缭乱、脑子浆糊。所以我打算反其道而行之。既然从前往后理解很费劲,那就倒着来,先看结论,再一步步反推过程,就会一步步豁然开朗了。
所以这里我先用pytorch给大家展示一下上面attention公式的计算过程,然后搭配着计算过程讲解这个公式:
上图就是pytorch框架中的注意力模块的类。我们造了一句话,这句话中有3个词,每个词有5个特征。
然后我们实例化一个attention对象,参数embed_dim表示词向量的长度,所以我们这个例子就得等于5;num_heads是MultiheadAttention的head的数量,num_heads=1表示只有一个attention模块,就是单头注意力。
因为我们求的是自注意力 分数,所以给实例化对象atten传入的query\key\value三个参数都是data,就是求这个3个词之间的注意力分数,或者说就是求这输入的数据中的三个样本之间的注意力。
pytorch的计算结果包含了2个对象,一个是数据data流经注意力模块后的输出结果output。另一个是3个样本之间的权重矩阵,这个权重矩阵其实就是这三个样本之间的注意力分数。
下面我们手动复现一下pytorch的计算流程:
上图是我们手算的流程,结果和pytorch的结果一模一样,说明我们算得过程没错。现在我们结合这个计算过程再讲一遍attention的原理。
1、输入->Q、K、V
假设我们的输入文本是"never give up"->然后对这句话按空格分词->编码成词向量->生成Q、K、V矩阵:
Transformer论文中将这个Attention公式描述为:Scaled Dot-Product Attention。其中,Q、K和V分别叫Query、Key和Value。一看名称就很懵,其实你先不用纠结名称,其实这三个矩阵一开始都是随机生成的。
(1)上图的wq、wk、wv就是随机生成的 ,仅仅是对序列中的所有样本进行了三次重复的线性变换而已。就是attention不是直接在词向量空间寻转样本与样本之间的关系,而是先将所有样本映射到另外一个空间,再开始学习样本与样本之间关系的。
(2)参数矩阵wq、wk、wv是在模型训练过程中不断迭代的。也是随着迭代次数的增加、随着损失函数逐渐减小,矩阵wq、wk、wv才逐渐具备查询、索引、内容这个功能的。
(3)上图中,Q矩阵中的词向量我们称为新词向量。那每个新词向量中的每个特征都是旧词向量(data)中所有特征的线性组合而得到的。这里不牵扯样本和样本之间关系啊!没有样本和样本之间关系!never和give和up这三个样本都是独立的,互不干涉的。因为就是一个全连接的线性层嘛,认真学过DNN的同学都知道,它只是混合每个样本自己内部各个特征,样本和样本之间是互不干涉的。
同理K矩阵和V矩阵。所以这个过程中,每个特征进行了3种不同的线性组合。也就是每个词向量的特征混合了它自己的其他特征。也表示每个词的词嵌入向量从随机数开始,是可以到逐渐迭代到接近其自身语义的。
2、计算QKt
这一步是理解注意力机制的核心,我们从各个角度来拆解这个过程:
(1)Q矩阵和K矩阵的来源是一样的,都是源数据data经过架构相同、但参数不同的线性层变换得到的,因为是两套全连接架构嘛。所以Q矩阵来源于never、give、up的词向量矩阵,K矩阵也是来源于never、give、up的词向量矩阵。所以此时Q*Kt的计算结果矩阵(上图A),就是序列(never, give, up)中样本与样本之间的关系矩阵。
(2)有的地方给A矩阵也叫问答矩阵,因为A是由Q和K相乘得来的。Q的全称是Query,查询的意思。K的全称是Key,索引的意思。你可以把查询理解为问,把索引理解为答,就不难理解为啥叫问答矩阵了。
(3)因为Q和K都来自同一份数据:序列(never, give, up)中,所以又叫自注意力机制 (Self-Attention)也被称为内部注意力机制(Intra-Attention)。以后我们学机器翻译时Q和K的来源是不同的,那个才是我们平时老说的注意力机制,也叫交叉注意力机制。那时你才需要认认真真的想想谁当Q谁当K?为啥这个输入当Q另外一个输入当K?这里先不扯远,后面会单独开一个小标题讲这个话题。
(4)看A中的红色框,就是对角线上的数字,这些数字你可以看成样本自己和自己之间的关系。你也可以理解为在序列never give up中,要理解这句话,每个单词自身含义占的比例。这是当然了,你理解这句话的前提是你每个单词都单独认识它啊,每个单词都不认识,就别谈理解句子了。这是一个锚点。也就是我前面说的要使用人家已经训练好的词嵌入,也就是站在一个优秀的起点上。
(5)对比A中黄框和绿框中的两个数字。假如黄框中的数字表示give和never的关系,那绿框中的数字就表示never和give的关系!我和你的关系好,不代表你和我的关系就也好。所以A也是一个不对称矩阵。这很合理,也很容易理解。give后面跟up的概率和up前面出现give的概率是不一样的。give依赖up,up未必也依赖give呀。所以give之于up的重要性和up之于give的重要性是不一样的。
(6)对矩阵A进行解读:
当Q说我是never,我想查询一下我和谁最相关?K是索引,K就把它里面的所有词都给never匹配一个"关注度",包括never本身。就是A矩阵的第一行三个数字。第一个数字就表示never本身的含义之于never语义的重要性。第二个数字就是give样本之于never的重要性。第三个数字就是up样本之于never的重要性。
如果Q说我是give,我想查询一下我和谁最相关?K就把它里面的所有词都给give匹配一个"关注度",就是A矩阵的第二行的三个数字。
如果Q说我是up谁和我最相关?K就把它里面的所有词都给up匹配一个"关注度",就是A矩阵的第三行的三个数字。
所以,Q矩阵是问,K矩阵是罗列出所有可能答案的"关注度"。所以A矩阵按行看,就是Q中某一个向量对K中的所有向量的"关注度"。
(7)我们再从数学的角度来理解:黄框数字是从Q中的never向量点乘K中的give向量;绿框数字是从Q中的give向量点乘K中的never向量。
向量点乘的几何意义是一个向量在另一个向量方向上的投影,点乘的结果可以衡量两个向量的相似度。当两个向量的方向特别一致,点乘的结果就越大;当两个向量的方向完全不一致,就是垂直喽,那点乘的结果就是0。所以点积越大就表示越相似,越相似就表示语义越接近,那让而且语义越接近是不是就是让二者的"关注度"越高。所以点乘是可以表示"关注度"的。
3、QKt除以根号dk->softmax变换
dk指词向量的长度 ,就是词向量的特征个数。
QKt除以根号dk,论文中叫scale 过程,也就是一个标准化的过程。
对scale再进行softmax 转化,是把样本与样本之间的关系转化成一个类概率的形式。就是attention模块中计算出来的注意力分数矩阵(Attention Score)。这个矩阵就是注意力模块推理、记忆的关键。
也就是用了scale和softmax两步,把QKt这个类似于问答矩阵,给转化成了一个真真正正的、样本与样本之间重要性的、注意力分数矩阵:
从上图看,never的语义和它自己本身的语义有57%的关联,和give有38%的关联,和up有5%的关联。
give的语义和它自己本身的语义有35%的关联,和never有62%的关联,和up有3%的关联。
up的语义和它自己本身的语义有2%的关联,和never有78%的关联,和give有20%的关联。
为什么会有scale操作呢 ?
前面已经说过QKt就是序列中样本与样本之间关系了,那我们接下来的方向就是,尽量把QKt中的数字往权重的方向映射。而最常用的方式就是softmax变换。但是为什么softmax之前又除以了一个根号dk呢?
我们现在这个例子,每个词向量是5个特征。实际项目中词向量一般都是成千上万或者数万个特征的。当词向量中的特征非常多的时候,而且还不稀疏,那么这向量之间点乘的结果的方差就会很大,即结果中的元素差距很大。就是矩阵QKt中数字间的方差比较大。
在transformer原文中描述,假设q和k的元素是相互独立维度为dk的随机变量,它们的均值是0,方差为1。那么q和k的点乘的平均值为0,方差为dk,如果将点乘的结果进行缩放操作,也就是除以dk,就可以有效控制方差从dk回到1 ,也就是有效控制梯度消失问题。
如果矩阵QKt中数字间的方差比较大,那进行softmax转化时,softmax是先用一个自然底数e将输入中的元素间差距继续"拉大",然后才归一化到权重。所以如果不除以dk,把方差从dk拉回到1,那对于QKt行中有数量级较大的数,softmax会将几乎全部的概率都分配给这个大数,大数的概率就无限接近1,同时其他相对较小的数的概率就无限接近0。而在softmax后的词向量中,不管是出现0还是出现1,反向传播求导时,softmax这里的梯度都会趋近0,导致梯度消失,无法训练:
4、根据注意力分数矩阵,扭转各个词向量
注意力分数矩阵其实就是一个权重矩阵,用权重矩阵就可以扭转各个词向量到适合上下文的方向了:
上图中从V矩阵到z矩阵就是加入了注意力分数的新的词向量,如果看得不太懂,看下图:
至此我们的新向量never"、give"、up"也学习彼此之间的重要性。这就是注意力机制如何学习样本与样本之间关系的。
5、把扭转后的词向量,通过线性层变换后输出
前面讲计算K、Q、V时,就是把输入的词向量映射到另外一个空间中计算的,计算完毕并扭转完毕后自然还要再映射回原空间:
至此,这就是单头自注意力模块的所有计算流程。
(四)注意力机制、自注意力机制
其实上面讲自注意力机制时,已经给大家科普了什么是注意力机制。自注意力机制 是注意力机制的变体,捕捉的是序列内部样本和样本之间的关系。自注意力机制的Q、K、V是同一个东西,或者三者来源于同一个序列X,三者同源。找序列X中每个样本之于其他样本的重要性,每个样本都只关注对它重要的样本,忽略不重要的样本,就是自注意力。
而注意力机制 又叫交叉注意力,就是因为它的Q和K是不同来源的:
就是交叉注意力机制计算的是,来自一个模态的查询和来自另一个模态的键之间的注意力分数。就是让模型关注一个输入中与另外一个输入中最相关的部分。交叉注意力机制是能够联合两种模态提取注意力的。所以在seq2seq任务中,也就是encoder-decoder架构中,Q就来自decoder中的内容,K来自encoder中的内容。自然V也是来自encoder了。所以自注意力机制是重点关注数据A内部的各个细节和重点的。而交叉注意力则是重点关注模态A数据和模态B数据之间相互关注的重点,是从多模态数据中提取注意力分值的。
交叉注意力和自注意力的区别比较好理解,这里我想重点讲一下多头自注意力机制。因为这里还是有几个坑的,有必要从"头"这个维度也把注意力机制也说清楚。
(五)多头自注意力机制
自注意力机制 是一次性看完一个序列中的所有样本,然后通过所有样本生成一个问答矩阵 ,再对问答矩阵进行scale和softmax归一化成注意力分数矩阵 Attention Score。注意力分数矩阵是自注意力模块具有推理、记忆的关键。但是我们可以想象一下,如果是一个整本推理小说那样的长序列的输入呢?假如推理小说有1万个单词,那注意力分数矩阵就是1万x1万的方阵!这就是注意力机制的推理代价,计算量太大!所以人们又研究出了一个多头注意力机制,让这个沉重的计算过程可以并行计算,节省时间成本。
很多讲多头注意力机制的资料都云里雾里,好像一直在说,多头就是多弄几个注意力模块就是多头了。其实不是的,因为一旦你的序列定下来了,比如前面使用的例子,输入的是一整篇推理小说,就在文章的最后一个单词was后面让你预测,这种情况就是输入序列是一定的。那样本和样本之间的关系就是定的,就是注意力分数矩阵就是定的。你不能说一个头生成一个注意力分数,多个头生成多个注意力分数,那每个头的理解不一样,预测就也不一样啊。谁杀人没杀人你不能胡说呀。所以要想有正确答案,那大家都文本序列的理解应该是一致的。比如我说苹果,你脑子里马上浮现的就是苹果水果。如果你理解成苹果公司也可以,因为我也知道苹果公司,我会纠正你的理解方向。但是如果你理解的是埃菲尔铁塔,那我们两个是无法沟通的,我纠正都无从下手。所以多头不是多个注意力模块对序列进行多次解读,也不是多个头对序列分而解读,而是多个头对输入的词向量的特征进行分而解读,而且是解读完毕后再cancat一起,映射回原向量空间 。所以你一定要明白,多头的初衷不是解决模型效果的!而是解决计算效率的!或者说多头的初衷是解决计算效率的,但是在解决计算效率时,它的解决方式一定程度上也带来了,比如说,增强了模型的表达能力和泛化能力这样的效果。
我们还是以pytorch的多头注意力模块的计算流程为标杆,看看人家的计算流程,看看人家是怎么理解的:
下面开始手动计算上右图中的两头注意力模块,看看结果一样不一样:
我想上面计算流程摆出来,其他就不必多说了,都是多余的了。现在总结一下:
1、多头自注意力机制中的QKV是来自全部的序列样本和样本的全部特征 。
2、之所以叫多头是因为,把QKV在词向量的特征维度空间给分隔了,然后分而治之。分隔几段就是几头,然后每个头就各种计算自己的注意力分数,各自求自己对应的那部分数据的z,然后把所有头的z横向拼接,再用一个线性层输出。
3、pytorch输出的注意力分数矩阵attention_score,是所有头的注意力分数,按照权重,加权求和得到的。
至此结束。