1. 前言
理解大语言模型结构的关键在于理解自注意力机制(self-attention)。自注意力机制可以判断输入文本序列中各个token与序列中所有token之间的相关性,并生成包含这种相关性信息的context向量。
本文介绍一种不包含训练参数的简化版自注意力机制——简单自注意力机制(simplified self-attention),后续三篇文章将分别介绍缩放点积注意力机制(scaled dot-product attention),因果注意力机制(causal attention),多头注意力机制(multi-head attention),并最终实现OpenAI的GPT系列大语言模型中MultiHeadAttention
类。
2. 从循环神经网络到自注意力机制
解决机器翻译等多对多(many-to-many)自然语言处理任务最常用的模型是sequence-to-sequence模型。Sequence-to-sequence模型包含一个编码器(encoder)和一个解码器(decoder),编码器将输入序列信息编码成信息向量,解码器用于解码信息向量,生成输出序列。在Transformer模型出现之前,编码器和解码器一般都是一个循环神经网络(RNN, recurrent neural network)。
RNN是一种非常适合处理文本等序列数据的神经网络架构。Encoder RNN对输入序列进行处理,将输入序列信息压缩到一个向量中。状态向量 h 0 h_0 h0包含第一个token x 0 x_0 x0的信息, h 1 h_1 h1包含前两个tokens x 0 x_0 x0和 x 1 x_1 x1的信息。以此类推, Encoder RNN最后一个状态 h m h_m hm是整个输入序列的概要,包含了整个输入序列的信息。Decoder RNN的初始状态等于Encoder RNN最后一个状态 h m h_m hm。 h m h_m hm包含了输入序列的信息,Decoder RNN可以通过 h m h_m hm知道输入序列的信息。Decoder RNN可以将 h m h_m hm中包含的信息解码,逐个元素地生成输出序列。
RNN的神经网络结构及计算方法使Encoder RNN必须用一个隐藏状态向量 h m h_m hm记住整个输入序列的全部信息。当输入序列很长时,隐藏状态向量 h m h_m hm对输入序列中前面部分的tokens的偏导数(如对 x 0 x_0 x0的偏导数 ∂ h m x 0 \frac{\partial h_m}{x_0} x0∂hm)会接近0。输入不同的 x 0 x_0 x0,隐藏状态向量 h m h_m hm几乎不会发生变化,即RNN会遗忘输入序列前面部分的信息。
本文不会详细介绍RNN的原理,大语言模型的神经网络中没有循环结构,RNN的原理及结构与大语言模型没有关系。对RNN的原理感兴趣读者可以参见本人的博客专栏:自然语言处理。
2014年,文章Neural Machine Translation by Jointly Learning to Align and Translate提出了一种改进sequence-to-sequence模型的方法,使Decoder每次更新状态时会查看Encoder所有状态,从而避免RNN遗忘的问题,而且可以让Decoder关注Encoder中最相关的信息,这也是attention名字的由来。
2017年,文章Attention Is All You Need指出可以剥离RNN,仅保留attention,且attention并不局限于sequence-to-sequence模型,可以直接用在输入序列数据上,构建self-attention,并提出了基于attention的sequence-to-sequence架构模型Transformer。
3. 简单自注意力机制
自注意力机制的目标是计算输入文本序列中各个token与序列中所有tokens之间的相关性,并生成包含这种相关性信息的context向量。如下图所示,简单自注意力机制生成context向量的计算步骤如下:
- 计算注意力分数(attention score):简单注意力机制使用向量的点积(dot product)作为注意力分数,注意力分数可以衡量两个向量的相关性;
- 计算注意力权重(attention weight):将注意力分数归一化得到注意力权重,序列中每个token与序列中所有tokens之间的注意力权重之和等于1;
- 计算context向量:简单注意力机制将所有tokens对应Embedding向量的加权和作为context向量,每个token对应Embedding向量的权重等于其相应的注意力权重。
3.1 计算注意力分数
对输入文本序列 “Your journey starts with one step.” 做tokenization,将文本中每个单词分割成一个token,并转换成Embedding向量,得到 x 1 , x 2 , ⋯ , x 6 x_1, x_2, \cdots, x_6 x1,x2,⋯,x6。自注意力机制分别计算 x i x_i xi与 x 1 , x 2 , ⋯ , x 6 x_1, x_2, \cdots, x_6 x1,x2,⋯,x6的注意力权重,进而计算 x 1 , x 2 , ⋯ , x 6 x_1, x_2, \cdots, x_6 x1,x2,⋯,x6与其相应注意力权重的加权和,得到context向量 z i z_i zi。
如下图所示,将context向量 z i z_i zi对应的向量 x i x_i xi称为query向量,计算query向量 x 2 x_2 x2对应的context向量 z 2 z_2 z2的第一步是计算注意力分数。将query向量 x 2 x_2 x2分别点乘向量 x 1 , x 2 , ⋯ , x 6 x_1, x_2, \cdots, x_6 x1,x2,⋯,x6,得到实数 ω 21 , ω 22 , ⋯ , ω 26 \omega_{21}, \omega_{22}, \cdots, \omega_{26} ω21,ω22,⋯,ω26,其中 ω 2 i \omega_{2i} ω2i是query向量 x 2 x_2 x2与向量 x i x_i xi的注意力分数,可以衡量 x 2 x_2 x2对应token与 x i x_i xi对应token之间的相关性。
两个向量的点积等于这两个向量相同位置元素的乘积之和。假如向量 x 1 = ( x 11 , x 12 , x 13 ) x_1=(x_{11}, x_{12}, x_{13}) x1=(x11,x12,x13),向量 x 2 = ( x 21 , x 22 , x 23 ) x_2=(x_{21}, x_{22}, x_{23}) x2=(x21,x22,x23),则向量 x 1 x_1 x1与 x 2 x_2 x2的点积等于 x 11 × x 21 + x 12 × x 22 + x 13 × x 23 x_{11}\times x_{21} + x_{12}\times x_{22} + x_{13}\times x_{23} x11×x21+x12×x22+x13×x23。
可以使用如下代码计算query向量 x 2 x_2 x2与 x 1 , x 2 , ⋯ , x 6 x_1, x_2, \cdots, x_6 x1,x2,⋯,x6的注意力分数:
import torch
inputs = torch.tensor([[0.43, 0.15, 0.89], # Your (x^1)[0.55, 0.87, 0.66], # journey (x^2)[0.57, 0.85, 0.64], # starts (x^3)[0.22, 0.58, 0.33], # with (x^4)[0.77, 0.25, 0.10], # one (x^5)[0.05, 0.80, 0.55]] # step (x^6)
)query = inputs[1]
attn_scores_2 = torch.empty(inputs.shape[0])
for i, x_i in enumerate(inputs):attn_scores_2[i] = torch.dot(x_i, query)
print(attn_scores_2)
执行上面代码,打印结果如下:
tensor([0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865])
3.2 计算注意力权重
如下图所示,将注意力分数 ω 21 , ω 22 , ⋯ , ω 26 \omega_{21}, \omega_{22}, \cdots, \omega_{26} ω21,ω22,⋯,ω26归一化可得到注意力权重 α 21 , α 22 , ⋯ , α 26 \alpha_{21}, \alpha_{22}, \cdots, \alpha_{26} α21,α22,⋯,α26。每个注意力权重 α 2 i \alpha_{2i} α2i的值均介于0到1之间,所有注意力权重的和 ∑ i α 2 i = 1 \sum_i\alpha_{2i}=1 ∑iα2i=1。可以用注意力权重 α 2 i \alpha_{2i} α2i表示 x i x_i xi对当前context向量 z 2 z_2 z2的重要性占比,注意力权重 α 2 i \alpha_{2i} α2i越大,表示 x i x_i xi与 x 2 x_2 x2的相关性越强,context向量 z 2 z_2 z2中 x i x_i xi的信息量比例应该越高。使用注意力权重对 x 1 , x 2 , ⋯ , x 6 x_1, x_2, \cdots, x_6 x1,x2,⋯,x6加权求和计算context向量,可以使context向量的数值分布范围始终与 x 1 , x 2 , ⋯ , x 6 x_1, x_2, \cdots, x_6 x1,x2,⋯,x6一致。这种数值分布范围的一致性可以使大语言模型训练过程更稳定,模型更容易收敛。
可以使用softmax
函数将注意力分数归一化得到注意力权重:
attn_weights_2 = torch.softmax(attn_scores_2, dim=0)
print("Attention weights:", attn_weights_2)
print("Sum:", attn_weights_2.sum())
执行上面代码,打印结果如下:
Attention weights: tensor([0.1385, 0.2379, 0.2333, 0.1240, 0.1082, 0.1581])
Sum: tensor(1.)
3.3 计算context向量
简单注意力机制使用所有tokens对应Embedding向量的加权和作为context向量,context向量 z 2 = ∑ i α 2 i x i z_2=\sum_i\alpha_{2i}x_i z2=∑iα2ixi。
可以使用如下代码计算context向量 z 2 z_2 z2:
query = inputs[1] # 2nd input token is the query
context_vec_2 = torch.zeros(query.shape)
for i,x_i in enumerate(inputs):context_vec_2 += attn_weights_2[i] * x_i
print(context_vec_2)
执行上面代码,打印结果如下:
tensor([0.4419, 0.6515, 0.5683])
3.4 计算所有tokens对应的context向量
将向量 x 2 x_2 x2作为query向量,按照3.1所述方法,可以计算出注意力分数 ω 21 , ω 22 , ⋯ , ω 26 \omega_{21}, \omega_{22}, \cdots, \omega_{26} ω21,ω22,⋯,ω26。使用softmax
函数将注意力分数 ω 21 , ω 22 , ⋯ , ω 26 \omega_{21}, \omega_{22}, \cdots, \omega_{26} ω21,ω22,⋯,ω26归一化,可以得到注意力权重 α 21 , α 22 , ⋯ , α 26 \alpha_{21}, \alpha_{22}, \cdots, \alpha_{26} α21,α22,⋯,α26。Context向量 z 2 z_2 z2是使用注意力权重对 x 1 , x 2 , ⋯ , x 6 x_1, x_2, \cdots, x_6 x1,x2,⋯,x6的加权和。
计算所有tokens对应的context向量,可以使用矩阵乘法运算,分别将各个 x i x_i xi作为query向量,一次性批量计算注意力分数及注意力权重,并最终得到context向量 z i z_i zi。
如下面代码所示,可以使用矩阵乘法,一次性计算出所有注意力分数:
attn_scores = inputs @ inputs.T
print(attn_scores)
执行上面代码,打印结果如下:
tensor([[0.9995, 0.9544, 0.9422, 0.4753, 0.4576, 0.6310],[0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865],[0.9422, 1.4754, 1.4570, 0.8296, 0.7154, 1.0605],[0.4753, 0.8434, 0.8296, 0.4937, 0.3474, 0.6565],[0.4576, 0.7070, 0.7154, 0.3474, 0.6654, 0.2935],[0.6310, 1.0865, 1.0605, 0.6565, 0.2935, 0.9450]])
@
操作符是PyTorch中的矩阵乘法运算符号,与函数torch.matmul
运算逻辑相同。将一个 n n n行 m m m列的矩阵 A A A与另一个 m m m行 n n n列 B B B的矩阵相乘,结果 C C C是一个 n n n行 n n n列的矩阵。其中矩阵 C C C的 i i i行 j j j列元素等于矩阵 A A A的第 i i i行与矩阵 B B B的第 j j j列两个向量的内积。
如下面代码所示,使用softmax
函数注意力分数归一化,可以一次批量计算出所有注意力权重:
attn_weights = torch.softmax(attn_scores, dim=1)
print(attn_weights)
执行上面代码,打印结果如下:
tensor([[0.2098, 0.2006, 0.1981, 0.1242, 0.1220, 0.1452],[0.1385, 0.2379, 0.2333, 0.1240, 0.1082, 0.1581],[0.1390, 0.2369, 0.2326, 0.1242, 0.1108, 0.1565],[0.1435, 0.2074, 0.2046, 0.1462, 0.1263, 0.1720],[0.1526, 0.1958, 0.1975, 0.1367, 0.1879, 0.1295],[0.1385, 0.2184, 0.2128, 0.1420, 0.0988, 0.1896]])
可以同样使用矩阵乘法运算,一次性批量计算出所有context向量:
all_context_vecs = attn_weights @ inputs
print(all_context_vecs)
执行上面代码,打印结果如下:
tensor([[0.4421, 0.5931, 0.5790],[0.4419, 0.6515, 0.5683],[0.4431, 0.6496, 0.5671],[0.4304, 0.6298, 0.5510],[0.4671, 0.5910, 0.5266],[0.4177, 0.6503, 0.5645]])
4. 结束语
自注意力机制是大语言模型神经网络结构中最复杂的部分。为降低自注意力机制原理的理解门槛,本文介绍了一种不带任何训练参数的简化版自注意力机制。
自注意力机制的目标是计算输入文本序列中各个token与序列中所有tokens之间的相关性,并生成包含这种相关性信息的context向量。简单自注意力机制生成context向量共3个步骤,首先计算注意力分数,然后使用softmax
函数将注意力分数归一化得到注意力权重,最后使用注意力权重对所有tokens对应的Embedding向量加权求和得到context向量。
接下来,该去看看大语言模型中真正使用到的注意力机制了!