您的位置:首页 > 游戏 > 游戏 > 从零开始实现大语言模型(四):简单自注意力机制

从零开始实现大语言模型(四):简单自注意力机制

2024/9/24 21:25:14 来源:https://blog.csdn.net/qq_24178985/article/details/140237760  浏览:    关键词:从零开始实现大语言模型(四):简单自注意力机制

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} x0hm)会接近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向量的计算步骤如下:

  1. 计算注意力分数(attention score):简单注意力机制使用向量的点积(dot product)作为注意力分数,注意力分数可以衡量两个向量的相关性;
  2. 计算注意力权重(attention weight):将注意力分数归一化得到注意力权重,序列中每个token与序列中所有tokens之间的注意力权重之和等于1;
  3. 计算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向量。

接下来,该去看看大语言模型中真正使用到的注意力机制了!

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com