讨论2024-01-11 = ###### tags: `tutorials` `Fudan` `2024` # Attention机制浅析 本次分享主要介绍注意力机制的发展历史和关键性质 ## 点乘注意力与加法注意力 假如序列输入为$X=\{x_0, x_1, \cdots, x_n \}$,早期的注意力机制和我们现在transformer里面的形式其实不同,以加法注意力为代表。 \begin{align} \alpha_{ij} = v^T \tanh(W[x_i, x_j]) \end{align} 而我们现在常用的点乘注意力为, \begin{align} \alpha_{ij} =& \frac{Q_i K_j^T}{\sqrt{d}} \\ =& \frac{x_i W_Q W_K^T x_j^T}{\sqrt{d}} \end{align} 其实两种注意力的优劣并不明确,严格来说,第一种方式的变换更加复杂,并且可以拓展成多层网络来加强非线性,但其计算更为复杂。研究者们近年来采用后者更多的主要原因是其计算更加高效。 ## 独立的二元关系 我们观察 $x_i W_Q W_K^T x_j^T$,可以发现这其实是一个典型的双线性函数的矩阵形式。 我们知道一元线性函数有 $$f(a+b) = f(a) + f(b) $$ 而二元线性函数(双线性)就是 $$f(x, a+b) = f(x, a) + f(x, b) $$ 和 $$f(a+b, y)=f(a, y)+ f(b, y) $$ 而其定义的矩阵形式就是 $A = xWy^T$,因为$A_{ij} = x_i W_{ij} y_j$,恰好满足双线性。 我们可以进一步考虑下双线性的含义就是可拆分,我们考虑把序列分为前半段和后半段$x^0, x^1$(补零),那么$f(x, x^0+x^1)=f(x, x^0)+f(x, x^1)$,也就是说双线性函数对一个序列的处理是可以拆分的。同理,我们会发现任何两个token之间的关系都是独立的,互不干扰。 这里介绍的独立二元关系和我们实际使用的attention形式还差一步,就是位置编码,实际上我们认为一对token之间的关系是一个四元组或三元组$(X_i, X_j, i, j)$或$(X_i, X_j, i-j)$,而这些四元组和三元组之间独立。 ## 非独立的二元关系 在自然语言中,我们往往认为语义理解是复杂的,或者说有非组合性,即$f(ABC) != g(f(A), f(B), f(C))$,其中g为线性变换,在实践中也可以认为g很难用简单的非线性,即浅层网络建模。那么上面介绍的独立二元关系建模明显是不足以覆盖语言的复杂性的。为此,我们可以考虑非独立的二元关系,即ab之间的关系会影响cd之间的关系。 我们目前普遍使用的注意力只考虑列相关性,即ab和ac之间的关系不独立,这便是由softmax函数引入的,$$\alpha = softmax (\alpha)$$,其中$$\alpha_i = \frac{\exp{\alpha_i}}{\sum_j \exp{\alpha_j}}$$ 而考虑ab, cd之间的关系,即行列相关性都考虑方法也是有的,只不过在注意力机制中应用的比较少,常见的代表有sinkhorn normalization 考虑行相关性的也有,比如capsule network 关于非独立二元关系的分析其实不多,因为目前学者们普遍认为自然语言有复杂性,但具体多复杂,需要什么样的数学模型来建模,其实很模糊。也就很难谈论上述构建非独立关系的方法好不好。 ## 投影说 还有一种理解是把query投影到value所在的凸区域里面。首先我们要知道归一化系数所张成的空间是凸区域,即$\sum_i \alpha_i=1,\alpha_i>=0$的情况下,$\sum_i \alpha_i V_i$构成的集合是凸区域,即任意区域内两点间的连线上的点还在区域内。设想在一个attention模块中,query有多个,但value是固定的一组,那么对应的凸区域是固定的,所以attention也可以理解为把query投影到这个凸区域里面。这样做有多方面的好处,第一是区域小且紧凑,第二是更容易泛化和拟合(凸区域往往形状更简单)。 ## 语言中的特征与模式 ### Bag of Words 词袋模型(Bag of Words)是最为基础的特征,也是语言中最简单的模式,即关键字。词袋模型描述了一段文本包含了哪些关键字,并据此给出决策。比如看到“好”,“不错”就认为是表扬。只看关键字当然是不够的,比如“不好”也有关键字好。 词袋模型有两个直接的扩展: - 第一是位置相关的词袋,即同时考虑关键字和其位置,比如模式(0,“你”),(1,“好”)就表示第一个位置是你,第二个位置是好。 - 第二是关键词的频率,即关键词出现的频率,比如“好”出现了5次,而“坏”出现了1次,那我们理应更重视“好”。 频率和位置还可以结合,也就是一段连续的关键字组合出现的频率,即N-gram特征。2-gram指的是就是两个连续的单词组成的一个组合,3-gram即三个连续单词的组合。 ### TF-IDF 语言有一个很重要的特性就是频率高的不一定是重要的,这和一般的数据有很大区别。频率反应重要性,但模式却不简单。比如“之乎者也”频率虽高但没有含义。同样的,很多高频词的概念很模糊,比如“车”就比“特斯拉”要模糊很多。为此,以TF-IDF为主的方法引入相对频率的概念,即每个单词在不同文本中有一个基准频率,如果在某一篇文章中,这个词的频率远高于基准频率,那么说明它很重要。 这里引出了一个很有趣的问题,如果一个高频的词突然在一个文章中完全不出现,那么是否也是一种特征?实际上这关系到了一个NLP经常碰到的问题,即数据量不足以充分体现统计量。很多高频词都不会在一个文章中出现,因为他们不够“高频”。假如两个词的概率为百分之一和千分之一,而一段文字只有50词,那么如何区分二者? 对TF-IDF的进一步讨论中有一个非常重要的工作,Topic Model,其核心思想是每篇文章有一个主题,比如体育,养生,时政等。不同主题下用词习惯大不相同,所以引入隐变量来分别统计。这里其实很好的体现了隐变量的作用一定是把问题分解的更简单了,也就是我们很难找到一个统计模型去同时描述体育和烹饪文章,但分别描述他们很简单。 ### PMI 要想进一步理解语言,就要面对上下文信息,其中很有代表性又非常基础的一种特征便是PMI,即$$PMI=\log \frac{P(x,y)}{P(x)P(y)}$$ 这里x,y是不同的单词,x,y共同出现即x,y同时出现在一句话中,或二者相差几个单词之内。这其实和经典的Skip-gram思想很接近。 稍微变化一下,我们考虑$P(X|C)$,这里X是单词,而C变成上下文,比如之前的N-gram,或者是上下分对应的特征。这个也可以拓展为BERT训练使用的是MLM以及word2vec中的CBOW。 这里需要明确一点,就是PMI究竟在捕捉什么信息?很多人会受词向量的几何性质,以为词向量接近的词就是同义词,这其实是不准确的。PMI描述的是可替换性,即把一个词替换为另一个有相近词向量的词,不影响语句的流畅性,但不保证意思相近。 比如 **“这是一个好(坏)人”**,这里把“好”换成“坏”,句子完全通顺,但意思相反。换言之,PMI是不能区分同义词,反义词,其他可替换词之间的区别的。 ### 依赖与作用范围 PMI往往需要配合距离信息,两个词在相近的位置出现,就很可能有关系。而依赖关系则从另一个角度考虑问题,即某些词是修饰或描述其他词的,比如“**一个方方正正,表面光滑,耐高温,价格便宜的不锈钢盒子** ” ,这里面的形容词都是形容盒子的,“方方正正”和“不锈钢”是一样重要的,不能因为距离远近决定其重要性。而这种修饰关系是可以拓展的,比如一个动词的主语,宾语都可以认为是对这个动词的修饰,状语也同理。依赖关系在同时描述多个物体的时候更有优势,比如"**张三和李四去宴会,前者一直在聊天,后者不理前者,一直认真品尝美食。**" 如果仅靠位置关系,是很难合理的建模这句话的,但依赖关系可以很好的处理。 依赖关系不仅仅局限于句子内部,根据不同的作用范围,我们可以有句子之间的依赖关系,两句话内部词语之间的依赖关系等。 ### 语义抽象化 语言的一大特点是表达多样性,一句话有很多种不同的说法。其中最基本的便是词语的多样性,比如“高兴”,“开心”,“愉快”,他们表达的意思是一样。如何独立考虑他们的含义,在与其他词语组合之后,不同的表达方式几何级数增加的。为了减少可能性,人们提出了**语义单元**的概念,即一种明确语义,没有表达差异的单元。比如上述的三个词都会用同一个单元表示而不是三个。这一过程也就称之为语义的抽象化。 ![](https://i.imgur.com/imEgkq6.png) ### 省略 机器处理语言的一大困难就是省略现象,而省略的原因又可以归结为多种情况。 - “你懂的”, “吃了嘛”,对话型的省略,信息在语言种不完备,参与方的背景,表情,语音等等信息都不包含在文字中。 - “张三新买的车,轮子不太好”,整体和局部,通常我们在连续描述整体与局部时,会省略他们的从属关系,也就需要常识了。 - “张三3年级,李四4年级,他们学习都不好”,复数指代机器也很难处理,因为这涉及到集合运算。更复杂的例子如“20个同学去秋游,3个人去做饭,5个人搬行李,其余的人中一半去爬山了。” - “没有一个人是无辜的”,“有一个人看到了”,不明确指代更为复杂,他标志了逻辑运算,所以自然语言其实时包含逻辑知识的,至少比一阶逻辑复杂。 这里一个本质问题是如果注意力机制想要访问的元素被省略了,模型还能不能正常处理问题,这就引出了两个前沿研究领域,模型空转生成和FFN的知识存储能力。 ## 单头注意力到多头的发展历史 \begin{align} \alpha_{ij} =& \frac{Q_i K_j^T}{\sqrt{d}} \\ =& \frac{x_i W_Q W_K^T x_j^T}{\sqrt{d}} \\ \alpha =& softmax(\alpha) \\ H =& \sum_i \alpha_i V_i \\ \end{align} 我们考虑以下一个情况, 假如输入序列长度的是2,即Q,K,V都只有两个token,那么不论$\alpha$如何取值,H的自由度始终只有2。 以此类推,H的自由度,变化复杂性的上限是会被输入序列长度限制的。这显然是不符合常识的,语义空间的复杂度不应该根据输入长度做出调整。 为了解决这个问题,我们可以采用多维度注意力机制,即 \begin{align} \alpha^\tau_{ij} =& Q_{\tau} K_{\tau}^T \\ =& x^\tau W_Q^\tau (W_{K}^\tau)^T (x^\tau)^T \\ \alpha^\tau =& softmax(\alpha^\tau) \\ H =& [\sum_i \alpha^\tau_i V^\tau_{i};] \text{for} \quad \tau \in [0, d) \\ \end{align} 这种方式是对每个维度都进行了softmax归一化,当然这里公式会有点奇怪,因为这类多维注意力模型早期是为加法注意力准备的,即 \begin{align} \alpha_{ij} = \tanh(W[x_i, x_j]) \end{align} 只要把$v^T$拿掉,很自然的就得到了各个维度的系数。 我们现在来看一下多维注意力的性质。当输入长度为2时,H的自由度不再是2,而是有能是维度数目(还考虑线性组合),即在每个维度上我们都在两个数字之间做差值。这就大大加强了H的表达能力和复杂性。 但是对所有维度都进行归一化又有点过于细化了,一种折中的方案就是多头注意力,即注意力机制在若干个子空间中进行,最后再合并。 ## Multi-query attention 上述内容给出了单头变多头的一种解释,即在输入序列短时,表达能力的问题,但实际生活往往是复杂的。有一些系列工作侧面证明了上述解释可能有其局限性,代表性的工作便是multi-query attention,并且这项技术在大模型中也有不少应用,比如Falcon。顾名思义,这种方法有多个query,但共享K,V。比如16个head,分为2*8,然后这两组每组有一个K,V。其提出的原因更多是为了加速decoding速度,因为我们在自回归生成是时,需要cache K和V,如果能够缩小他们的数目,对速度和内存开销都是显著有帮助的。 但另一方面,这个工作也很好的反驳了我们之前的表达能力说法。因为它只有一组K,V,多个Q,performance与正常的多头注意力相比没有显著下降。但却比单头注意力要表现好,那么新问题出现了,为什么只增加query的数量也有帮助? ## 拆分建模学说 类比单高斯模型和混合高斯模型,我们可以认为每个attention head建模了一个简单模型,而多个head组成了一个更复杂的模型。当然,这里需要注意,在神经网络的帮助下,一个head也可以建模很复杂的分布,但这有可能是不好学习的。在具体展开之前,我们可以先看一些分析, ![](https://hackmd.io/_uploads/By2bbWo92.png) ![](https://hackmd.io/_uploads/SkqnxZicn.png) ![](https://hackmd.io/_uploads/r1uGTUjq3.png) 可以看到,其实不同head之间有些行为相似,有些事明显不同的。换言之,不同head之间实现了一定的合作。 有些复杂模式很难用一个head建模,比如一种的简单情况,有些token需要看另外3个token,有些token需要看另外5个token,在一个attention head的情况下,即使实现了,也会差一个系数,即前者为三分之一,后者为五分之一。但如果是多头注意力,因为多头结果是拼接的再过MLP,这个系数很容易实现。 ## 多头注意力真的必要吗? 实际上,多头注意力的必要性是有争议的,比如有些工作指出,其实更多层的单头注意力可以拟合多头注意力的能力(总head数目相同)。当然,这不是一个严格公平的比较。 ![](https://hackmd.io/_uploads/rJvdx-oqh.png) ## 预训练的意义 一般来说,没有预训练的transformer是很可能不如RNN和CNN的。参照以下的简单例子,可以看到transformer并不好训练。 ```python= import torch import torch.nn as nn import tqdm import numpy as np BATCH_SIZE = 16 D_MODEL = 256 N_HEAD = 4 N_LAYER = 6 IN_LEN = 10 IN_DIM = 50 class Trans(nn.Module): def __init__(self): super(Trans, self).__init__() self.trans = nn.TransformerEncoder(nn.TransformerEncoderLayer(D_MODEL, N_HEAD), N_LAYER) self.in_fc = nn.Linear(IN_DIM*2, D_MODEL) self.out_fc = nn.Linear(D_MODEL, IN_DIM) def forward(self, x): h = self.in_fc(x) h = self.trans(h) y = self.out_fc(h) return y def run(model, test=False): model.train() losses = [] lr = 1e-3 optimizer = torch.optim.Adam(model.parameters(), lr=lr) with tqdm.trange(1000) as tq: for i,_ in enumerate(tq): a = torch.randn((BATCH_SIZE, IN_LEN, IN_DIM)).cuda() b = torch.randn((BATCH_SIZE, IN_LEN, IN_DIM)).cuda() tar = (a+1)**2 + b y = model(torch.cat([a,b], -1)) loss = (0.5*(y-tar)**2).mean() model.zero_grad() if not test: loss.backward() optimizer.step() tq.set_postfix({'loss':loss.item()}) losses.append(loss.item()) if test: print('AVG Loss', np.mean(losses)) if __name__ == '__main__': model = Trans() model.cuda() run(model) run(model, True) ``` |Setting|MSE| |-|-| |lr=1e-3, no warm-up| 3.37| |lr=1e-4, no warm-up| 0.56| |lr=1e-5, no warm-up| 2.31| |lr=1e-3, 1% warm-up| 0.10| |Setting|MSE| |-|-| |default| 0.097| |xavier_uniform| 0.191| |xavier_normal| 0.189| |normal 0.02| 0.66| |normal 0.002| 2.95| |normal 0.1| 3.50| |uniform +-0.05| 3.50| |uniform +-0.01| 0.424| |uniform +-0.001| 3.44| |kaiming_uniform| 3.50| |kaiming_normal| 3.50| |Setting|MSE| |-|-| |kaiming_normal 2 layer| 0.524| |kaiming_uniform 2 layer| 0.531| |xavier_uniform 2 layer| 0.124| |default 2 layer| 0.092| |uniform +-0.01 2 layer| 0.117| |Setting|MSE| |-|-| |lr=1e-3, no warm-up 2 layer| 0.086| |lr=1e-4, no warm-up 2 layer | 0.614| |Setting|MSE| |-|-| |lr=1e-3, no warm-up batch_size 64| 0.037|