自注意力机制,可以说是transformer中最核心的部分之一,注意力机制,主要是在处理序列数据时,根据序列数据提供的上下文环境信息识别需要关注的特征数据,注意力机制通常用于不同序列之间的交互,表示不同序列环境的相互影响,而自注意力机制,则更侧重单个序列数据内部个元素间的相互影响关系。--这段话不理解的话,没关系,先放着,且看下面分析:
物体特征需要在环境中才能体现出来,比如"苹果和梨都是水果",这个句子中,我们知道苹果是水果,但是"用苹果打电话"这句话中我们知道苹果是手机。
我们以三维空间为例,开始学习自注意力机制:
假设三个坐标轴分别为 (手机、水果、其他) 单位矩阵 [ (1,0,0),(0,1,0),(0,0,1)] ,
纯手机可以表示为(1,0,0),纯水果可以表示(0,1,0),纯其他(0,0,1).
在transformer学习笔记-词嵌入embedding原理我们知道一个词向量需要通过一个嵌入矩阵得到,也了解了词向量在向量空间中代表的意义。现在假设我们已经基于现有的语料训练好了一个嵌入矩阵,并且得到如下词嵌入:
苹果:[ 3 ,3 , 0 ] = 3 · 手机 + 3 · 水果 +0 · 其他
雪梨:[ 0.5 ,4 , 0 ] = 0.5 · 手机 + 4 · 水果 +0 · 其他
华为:[ 4 ,0.5 , 0 ] = 4 · 手机 + 0.5 · 水果 +0 · 其他
注意: 当然也可以随机指定一个词向量,然后在通过自注意力机制,大量语料不断训练,得到一个跟输入序列上下文相关的词向量矩阵,bert就是这么干的。而上面通过嵌入矩阵得到的词向量,虽然嵌入矩阵也是通过语料训练得到,但是后续再通过嵌入矩阵得到的词向量在不同的句子环境中都是一样的。后续有机会我们再学习bert的原理。
然后我们给出一个句子: 苹果和梨都是水果 ,提取token就有【苹果,和,梨,都、是、水果】,这里我们采用内积计算不同token的词向量的相似度,这里本应还有【和,都、是、水果】的词向量的,不过这里为了图示方便,我们只关注梨和苹果 ,也就是我们把句子缩减 为苹果梨 ,此时应该如何调整词向量,以苹果开始:
计算两个矩阵A和B的内积 = A ⋅ B T A · B^T A⋅BT
通过苹果[ 3 ,3 , 0 ],分别于苹果自身和梨[ 0.5 ,4 , 0 ],分别得到内积[18 , 13.5],也就是说,苹果和苹果的相似度为18,苹果和梨的相似度为13.5,我们将相似度作为权重进行归一化: [ 18 18 + 13.5 , 13.5 18 + 13.5 ] [\frac{18}{18+13.5},\frac{13.5}{18+13.5}] [18+13.518,18+13.513.5] = [0.57,0.43]
这里的归一化操作,大致就是softmax操作:
稍后我们简要介绍softmax。这里计算出来的归一化结果代表苹果一词,在"苹果梨"这句话中注意力权重(受关注度)。
此时我们得到一个包含上下文信息的新的苹果:
新的苹果 = 0.57 · 旧的苹果 + 0.43 · 旧的梨
苹果的手机属性降低,水果的属性提高,向梨靠近,也就是说,此时的苹果包含了上下文信息,这里的旧苹果,可以通过嵌入矩阵得到,也可以随机一个点,然后通过训练移动
如果回到苹果和梨都是水果 这个句子,那么新的苹果还需要加上【和,都、是、水果】:
新的苹果 = a·旧的苹果 + b·旧的梨 +c·和 +d·都 +e·是 + f ·水果(a,b,c,d,e,f的和为1)
以同样的方式可以得到新的梨:
新的梨 = 0.45 * 旧的苹果 + 0.55 * 旧的梨
因此,我们可以得到更一般的矩阵表示 :Q中的每一行分别与K的转置做内积并通过softmax函数做归一化,再与旧的值V做加权求和得到新的值,新的值就包含了上下文的信息。
这也就得到注意力机制的式子 :
1、计算查询与所有键之间的点积,得到查询Q与序列中每个位置的相关性得分
Scores= Q K T QK ^T QKT
2、使用softmax函数将这些得分转换成0到1之间的概率值,表示当前查询与其他位置的重要性或权重:
weights = softmax( Q K T QK ^T QKT)
实际上会对内积除以 d k \sqrt{d_k} dk 得到新的概率分布:
weights = softmax( Q K T d k \frac{QK ^T}{\sqrt{d_k}} dk QKT) ( d k d_k dk 是Q和K的维度)
3、使用概率分布权重乘以V矩阵,我们就得到了注意力公式,也叫缩放点积自注意力
output = weights · V = softmax( Q K T d k \frac{QK ^T}{\sqrt{d_k}} dk QKT)·V
在上面计算苹果梨的例子中,Q、K、V都是来源于同一个句子序列,对于这种句子中某个token对同一个句子序列中的所有token求注意力权重的,我们称之为自注意力机制;而传统注意力机制(交叉注意力机制):如翻译场景,Q来自同一个序列(梨和苹果都是水果)、K、V来自另外的序列,如:Pears and apples are both fruits(早期的交叉注意力并没有QKV一说,而是序列到序列(Seq2Seq)的注意力机制,有兴趣可以了解下)。
这里就有两个疑问:为何要用除以一个数?为何是 d k \sqrt{d_k} dk ?当然过程可能涉及数学推导,这里简要说下(不是本文重点):
当Q和K的维度特别大的时候,内积也会变得很大,由于softmax通过指数计算,容易导致数值爆炸增长,同时元素间的差值和方差也变大失真,使softmax后的概率分布变得更加尖锐(即某些位置的概率接近1,而其他位置的概率接近0,大的大小的小),导致模型损失对概率小的位置的关注,通过损失函数求导过程中对softmax求导,对于接近0的元素的梯度变得极小(梯度消失),影响权重收敛。除以 d k \sqrt{d_k} dk ,可以是softmax后的概率分布更加平滑。
同时 d k \sqrt{d_k} dk 是一个经验取值,通过推导可以得到更优的值。
竟然这么麻烦,为何不直接用余弦相似函数计算向量间夹角再通过softmax计算权重? 其实在后面我们将要学习的位置编码中,有类似的原理。
注意: 细心的小伙伴可能发现,我们将句子从"苹果梨",改成"梨苹果",最终的到的新苹果和新梨,竟然是一样的,因为苹果和梨两个向量调换顺序后,对应计算的权重也换了顺序,因此最终计算出来的结果一样,有兴趣的小伙伴可以试着算一算。在大多数场景词token的位置不同含义也不同,因此得到的词向量也不该相同,比如我吃饭,和 饭吃我,肯定是不同意思,这也引出为何需要在词编码的基础上加上位置编码,位置编码一两句说不清,我们后面另外笔记学习:
4、实际上,自注意力机制中Q、K、V,是通过对输入做不同的线性变换得到,同一个输入,分别与 W Q 、 W K 、 W V {W^Q}、W^K、W^V WQ、WK、WV相乘,其中 W Q 、 W K {W^Q}、W^K WQ、WK维度 d k {d_k} dk(列数)相同,行数与输入token维度相同 ;W V W^V WV 维度 d v {d_v} dv任意,行数与输入的token维度相同。
然后将得到的Q、K、V代入softmax( Q K T d k \frac{QK ^T}{\sqrt{d_k}} dk QKT)·V
最终输出的维度与V的维度相同。
为何要 W Q 、 W K 、 W V {W^Q}、W^K、W^V WQ、WK、WV三个矩阵: 主要是为了通过线性变化,使模型更容易关注到某些特征:
图中红色表示不同的水果、蓝色表示不同的手机,通过变换1,可以将水果和手机的点区分开更容易提取特征完成分类。而变换2,所有的点聚集一起,难以区分。 W Q 、 W K 、 W V {W^Q}、W^K、W^V WQ、WK、WV就是这样的变换矩阵。
对于二维三维的空间,可能通过人工设计一个变换矩阵,但对于高维向量空间,这几乎不可能,因此 W Q 、 W K 、 W V {W^Q}、W^K、W^V WQ、WK、WV 也是通过大量的语料训练得到, W V W^V WV的将输入投射到更高维或者更低维空间,实现特征的扩展或者压缩,也直接影响output的维度,也就影响关注到的特征数目。
output的作用:
output是一个包含当前序列的其他token的信息的上下文向量,融合了来自整个输入序列的信息,帮助模型更准确地理解每个词或片段的在当前环境的语义。
例如:大模型就是根据output乘以一个解嵌入矩阵,得到下一个词的概率。
在大模型的生成任务中,这里的output通常取得是整个输入序列的最后一个token对应的output,因为他不仅包含了整个序列的上下文信息,还跟下一个需要预测的token最靠近。
通过输入序列中的最后一个token的output与解嵌入矩阵相乘,得到词库中每个词token的概率,概率最大的词就是下一个出现的词。
整体输入经过QKV处理后得到的output的行数也就是上下文的长度,在大模型中可以设置最大上下文长度,输入的多个token中,超出上下文长度限制的部分,不参与QKV计算。
softmax函数:
1、每个输入token经过 Q K T QK ^T QKT得到的内积,都会得到一个维度与key的行数相等的的单行矩阵s = [ s 1 , s 2 , . . . , s n ] [s_1,s_2,...,s_n] [s1,s2,...,sn],s中的每个si元素就是单个输入token的Q与key的每行记录的相似度,我们可以直接通过 s i s 1 + s 2 + . . . + s n \frac{s_i}{s_1+s_2+...+s_n} s1+s2+...+snsi得到每个s的权重。
注意: s元素个数是K的行数相等,也就是 K T K ^T KT的列数,不是 K T K ^T KT的行数,2、由于内积计算结果可能是负数,也可能是0,会导致 s i s 1 + s 2 + . . . + s n \frac{s_i}{s_1+s_2+...+s_n} s1+s2+...+snsi无法计算
3、因此引入softmax函数,将权重归一化到[0,1]:
p i = e s i ∑ j = 1 n e s j ( i = 1 , 2 , . . . n ) p_i = \frac{e^{s_i}}{∑_{j=1}^{n}e^{s_j}} ( i = 1,2,... n) pi=∑j=1nesjesi(i=1,2,...n)
s i s_i si表示经过 Softmax 函数转换后的第 i 个元素,e 是自然对数的底,分母是对所有输入元素 e s j e^{s_j} esj的求和,最终得到P = [ p 1 , p 2 , . . . , p n ] [p_1,p_2,...,p_n] [p1,p2,...,pn],每个p元素就是K矩阵每个token与输入token的相似度权重。
注意: 当Q和K的维度特别大的时候,内积也会变得很大,因此 e s i e^{s_i} esi的值可以出现指数爆炸的情况,导致元素间差距变大(方差变大),导致某些元素概率接近1,某些接近0,这也是上面说到为何需要除以 d k \sqrt{d_k} dk 的原因。
在大模型中,有个超参叫temperature,也是跟softmax函数有关系,带temperature的softmax函数表示如下:
p i = e s i / T ∑ j = 1 n e s j / T ( i = 1 , 2 , . . . n ) p_i = \frac{e^{s_i/T}}{∑_{j=1}^{n}e^{s_j/T}} ( i = 1,2,... n) pi=∑j=1nesj/Tesi/T(i=1,2,...n)他的作用跟 d k \sqrt{d_k} dk 差不多,也是为了控制概率分布的平滑性,由于大模型通过概率选择下一个需要出现的词token,T越小,某些元素的概率越大,越有可能被选中为下一个词,生成内容越保守,T越大,概率分布越平均,下一个词出现的概率越随机,生成的内容更有创造性,或者,胡说八道。
多头注意力:
多头注意力(multi-heads)就是将同一个输入序列,经过多组不同的 W Q 、 W K 、 W V {W^Q}、W^K、W^V WQ、WK、WV处理,最后将结果拼接。
每组不同的 W Q 、 W K 、 W V {W^Q}、W^K、W^V WQ、WK、WV,,每组不同的 W Q 、 W K 、 W V {W^Q}、W^K、W^V WQ、WK、WV相当于一个head,每个头关注不同方面的数据特征,并且可并行计算,提高模型的表达能力,表现更丰富的依赖关系。
掩码注意力
对于生成类的任务,其前序token的出现,往往不需要依赖后续token的上下文信息,为了保持训练和测试的一致性,前序token计算注意力的时候,不能包含当前token的后续token的信息。比如:"我 喜欢 学习 Python",当生成任务执行到"我 喜欢"的时候,计算"喜欢"的注意力就不应该包含"学习"和"Python"的信息。所以在用"我 喜欢 学习 Python"当语料执行训练任务时,也应该如此。
如图,"我"不跟其他词token计算注意力权重,预测得到"喜欢",到"喜欢"的时候,"喜欢"与"我"计算权重并得到新的我,然后再预测得到下一个词:"学习",到"学习"的时候,分别需要跟"我","喜欢"计算权重,最终预测下一个词:"Python"
如何实现呢,通常逐乘一个形状与 Q K T QK ^T QKT的结果相同的三角矩阵 :
输入序列的多个token,经过 Q K T QK ^T QKT的处理一定是个n*n的矩阵(上图省略了k的生成过程,因 W Q 、 W K {W^Q}、W^K WQ、WK形状相同,生成的Q和K形状也相同),n为token个数,即为输入Q的行数。
以 n * n矩阵的第一行为例,第一行的四个格子分别代表的是"我" 分别与 "我","喜欢","学习","Python"四个词token计算的内积得分,与三角矩阵逐乘后,只有第一格有得分,经过softmax计算得到权重矩阵[1,0,0,0];
n * n矩阵的第二行:是"喜欢",分别和"我","喜欢","学习","Python"四个词token计算的内积得分与三角矩阵逐乘后,只有前两个格有得分,经过softmax计算得到权重矩阵[0.4,0.6,0,0](数值随便写的);
模型的参数量:
我们经常听到说大模型参数量多少B,这个参数量主要由以下部分组成:
嵌入矩阵: n* m个参数;(n为词库token数,m为嵌入矩阵维度(列数))
W Q {W^Q} WQ: m* k个参数;(m为词嵌入的维度,k为 W Q {W^Q} WQ的维度)
W K {W^K} WK: m* k个参数;(m为词嵌入的维度,k为 W K {W^K} WK的维度)
W V {W^V} WV: m* v个参数:(m为词嵌入的维度,为 W V {W^V} WV的维度)
多头注意力层数: M,那么 W Q 、 W K 、 W V {W^Q}、W^K、W^V WQ、WK、WV的个数还需要分别再乘以M
解嵌入矩阵: v* n个参数;( v为嵌入矩阵维度(也是 W V {W^V} WV的维度),n为词库token数)
其他隐藏层权重W和偏置b: 若干假设词库token个数:50000,维度16384, W Q 、 W K 、 W V {W^Q}、W^K、W^V WQ、WK、WV的维度8192,注意力头96个,暂不计算隐藏层,得到总参数量:50000 * 16384 + 96*16384 * 8192 * 3 + 8192 * 16384 ≈ 4B(四十亿)。
下一篇我们将通过代码对上面的关键点,做简要演示。