从初中物理到大模型原理(三)
附上前两篇的链接:
从初中物理到大模型原理(一)
从初中物理到大模型原理(二)
附上后一篇的链接:
从初中物理到大模型原理(四)
当我们在说注意力机制的时候,我们到底在说什么?注意力机制其实就是衡量距离,只不过在大模型中这个衡量很抽象,很复杂。
一、词向量
我们还是先从上一篇最后的那个例子开始说。直角坐标系中,我们取两个点 x 1 = ( 1 , 0 ) x_1=(1,0) x1=(1,0) x 2 = ( 1 , 1 ) x_2=(1,1) x2=(1,1) 。再从原点出发,得到了两个向量 x 1 = ( 1 , 0 ) \mathbf{x_1} = (1,0) x1=(1,0) x 2 = ( 1 , 1 ) \mathbf{x_2} = (1,1) x2=(1,1),用向量的点积 x 1 ⋅ x 2 \mathbf{x_1}·\mathbf{x_2} x1⋅x2 来衡量这两个向量的距离,数学上用 x 1 ⋅ x 2 = x 1 x 2 + y 1 y 2 \mathbf{x_1}·\mathbf{x_2}=x_1x_2 + y_1y_2 x1⋅x2=x1x2+y1y2 来计算。可以这样算,有几个关键的信息:
- 直角坐标系:这个是基础,所有的点都必须在这个直角坐标系中,才可以算;
- 维度:直角坐标系是二维的只有 x , y x,y x,y,相对的 x 1 = ( 1 , 0 ) x_1=(1,0) x1=(1,0) 表示在 x x x 轴上的投影是 1 1 1,在 y y y 轴上的投影是 0 0 0 ,如果是三维的,那么计算方式就变成了 x 1 ⋅ x 2 = x 1 x 2 + y 1 y 2 + z 1 z 2 \mathbf{x_1}·\mathbf{x_2}=x_1x_2 + y_1y_2 + z_1z_2 x1⋅x2=x1x2+y1y2+z1z2,以此类推;
有了这两个前提,我们来看看大模型的计算过程:假设我们有一个768维的空间(维度越高,可以携带的信息就越多,GPT-2是768维),那么我们需要把上一篇文章里面 x = ( 2 , 28 , 32 , ⋅ ⋅ ⋅ , 99 ) \mathbf{x} = (2,28,32,···,99) x=(2,28,32,⋅⋅⋅,99)的这个向量映射到这个768维的空间中(简单一些,我们后面用只包含四个token的 x = ( 2 , 28 , 32 , 99 ) \mathbf{x} = (2,28,32,99) x=(2,28,32,99)来表示)。上一篇我们假设我们的词库总共有100个词,那么每个词都需要映射到这个768维的空间中,映射后在每一维上的值,就是在这个维度上的投影,假设 2 2 2映射出来以后的 ( 0.003 , 0.023 , 0.045 , 0.06 , 0.07 , ⋅ ⋅ ⋅ , 0.037 ) (0.003,0.023,0.045,0.06,0.07,···,0.037) (0.003,0.023,0.045,0.06,0.07,⋅⋅⋅,0.037) 这是一个 1 ∗ 768 1*768 1∗768 的矩阵,也叫 词向量。那么上面的 x = ( 2 , 28 , 32 , 99 ) \mathbf{x} = (2,28,32,99) x=(2,28,32,99) 就可以映射成一个 4 ∗ 768 4*768 4∗768 的矩阵,假设长这个样子 X = ( 0.003 , 0.023 , 0.045 , 0.06 , 0.07 , ⋅ ⋅ ⋅ , 0.037 0.001 , 0.323 , 0.745 , 0.16 , 0.27 , ⋅ ⋅ ⋅ , 0.347 0.053 , 0.083 , 0.095 , 0.63 , 0.75 , ⋅ ⋅ ⋅ , 0.264 0.006 , 0.213 , 0.415 , 0.06 , 0.57 , ⋅ ⋅ ⋅ , 0.537 ) \mathbf{X} = \begin{pmatrix} 0.003,0.023,0.045,0.06,0.07,···,0.037\\ 0.001,0.323,0.745,0.16,0.27,···,0.347\\ 0.053,0.083,0.095,0.63,0.75,···,0.264 \\ 0.006,0.213,0.415,0.06,0.57,···,0.537\end{pmatrix} X= 0.003,0.023,0.045,0.06,0.07,⋅⋅⋅,0.0370.001,0.323,0.745,0.16,0.27,⋅⋅⋅,0.3470.053,0.083,0.095,0.63,0.75,⋅⋅⋅,0.2640.006,0.213,0.415,0.06,0.57,⋅⋅⋅,0.537 至此,我们就完成了从字到token再到词向量的转变。我们用代码表示一下,很简单,为了方便理解,我会在关键的代码后面,直接加上输出:
import torch
from torch import nn
import tiktoken
# gpt2 提供的从字到token的转换器,也叫分词器
tokenizer = tiktoken.get_encoding("gpt2")
start_context = "大模型原"
# 转为 token id
encoded = tokenizer.encode(start_context)
# 下面这一行是 encoded 的值
# 不是四个值的原因,我就不在这里讲了,不是重点,感兴趣的可以去看一下分词器的原理
# encoded: [32014, 162, 101, 94, 161, 252, 233, 43889, 253]
# 现在 encoded 还不是矩阵,只是个数组
# 转为 1 * token.size 的 矩阵
encoded_tensor = torch.tensor(encoded).unsqueeze(0)
#encoded_tensor: tensor([[32014, 162, 101, 94, 161, 252, 233, 43889, 253]])
#encoded_tensor.shape: torch.Size([1, 9])
#这样就得到了一个 1*9 的矩阵
#在我们的假设里,共有100个词,GPT2是 50257个
tok_emb = nn.Embedding(50257, 768)
#由矩阵转换为词向量
tok_embeds = tok_emb(encoded_tensor)
print(tok_embeds)
#这个输出是肯定打不全的啦
tensor([[[-0.7558, 0.2214, -0.0669, ..., -0.0675, 1.5860, -1.7477],
[-1.5326, 0.6995, 2.1200, ..., -0.3058, 1.6716, 0.4993],
[ 0.1497, -0.6573, 0.9820, ..., -0.6105, -0.2578, -2.4229],
...,
[ 0.4232, 0.5039, 0.3852, ..., -0.2627, -0.6688, 1.4953],
[ 2.0909, 1.6228, -1.3269, ..., 0.5683, 0.4071, 0.9331],
[ 1.4512, 0.8749, 0.3835, ..., 0.8473, -2.2383, -0.4602]]],
grad_fn=<EmbeddingBackward0>)
#我们看一下最后这个词向量的形状
print(tok_embeds.shape)
torch.Size([1, 9, 768])
#我们现在可以把结果简单的理解成 1 个 9*768 的矩阵
好,我们现在已经得到了 9个词向量,共同组成了一个矩阵 tok_embeds,那么怎么计算呢?
二、注意力机制
我想了很久,怎么通俗易懂的讲注意力机制。直到上周末,我表妹马上大学毕业了。询问我计算机相关的事情时,我才恍然大悟。原来是这个名字翻译的不好,如果翻译成集中注意力,或者集中注意力机制,真的就很好理解了。
举个例子,大学的时候,假设有两门选修的课程《西夏文字研究》,《欧洲中世纪教会史》,每节课都有12个课时,老师每天都在讲课。但是你根本不感兴趣,因为你想选的是《影视鉴赏》和《基础摄影技术》。然后《西夏文字研究》开始考试了,侥幸考的刚好及格60分,我们假设每个课时讲的东西都考到了,而且只能得5分,剩下的咱也不会做。然而《欧洲中世纪教会史》老师比较有经验,知道大家都是为了学分才选的这个,所以在第12课时的最后半小时的时候,说了一句话:给大家画一下重点啊,第5课时XXXXXX,第7课时XXXXXXXX,最后大家都考了80分。
这就是注意力机制(虽然我觉得叫集中注意力更合适,但是还是沿用注意力机制吧)。
好,接下来我们开始抽象。
最开始各有12个输入,我们称为12个key;然后《欧洲中世纪教会史》有一个划重点,我们称为query,最后得到的输出就是《欧洲中世纪教会史》80分,《西夏文字研究》60分。80分的原因是因为我们把复习的时间更多给到了第5课时和第7课时,就是说这两个的权重比其他的课时变大了。
接下来,用数学公式表达一下以上的过程。首先,我们有一个词向量
k
=
(
k
1
,
k
2
,
k
3
,
⋅
⋅
⋅
,
k
12
)
\mathbf{k} = (k_1,k_2,k_3,···,k_{12})
k=(k1,k2,k3,⋅⋅⋅,k12)然后我们有另外一个 key 对应值的词向量
v
=
(
v
1
,
v
2
,
v
3
,
⋅
⋅
⋅
,
v
12
)
\mathbf{v} = (v_1,v_2,v_3,···,v_{12})
v=(v1,v2,v3,⋅⋅⋅,v12)有一个查询的向量,帮助我们集中注意力
q
\mathbf{q}
q。接下来最后的得分用一个函数来表示
80
=
∑
i
=
1
12
α
(
q
,
k
i
)
v
i
80= \sum_{i=1}^{12} \alpha(\mathbf{q},\mathbf{k}_i)\mathbf{v}_i
80=i=1∑12α(q,ki)vi这个函数称为注意力汇聚函数(集中注意力),
α
\alpha
α 称为注意力评分函数(我觉得应该翻译成注意力得分函数)。我们知道作用到
v
\mathbf{v}
v 上的是权重,所以我们需要知道
q
\mathbf{q}
q 和
k
\mathbf{k}
k 计算以后的每一个
k
i
k_i
ki 的占比,这里使用了
s
o
f
t
m
a
x
softmax
softmax 函数
α
(
q
,
k
i
)
=
s
o
f
t
m
a
x
(
a
(
q
,
k
i
)
)
=
e
x
p
(
a
(
q
,
k
i
)
)
∑
j
=
1
m
e
x
p
(
a
(
q
,
k
i
)
)
\alpha(\mathbf{q},\mathbf{k}_i) = softmax(a(\mathbf{q},\mathbf{k}_i))=\frac{exp(a(\mathbf{q},\mathbf{k}_i))}{ \sum_{j=1}^mexp(a(\mathbf{q},\mathbf{k}_i))}
α(q,ki)=softmax(a(q,ki))=∑j=1mexp(a(q,ki))exp(a(q,ki))这里用指数函数的意义在于永不为负,用自然底数的意义在于导数好算。
三、大模型的自注力机制
接下来我们说一下,大模型是怎么用的。
对于我们输入每一个token来说,这个点原本就在词库对应的高维空间中。为了方便,我换成英文再重新输出一个
start_context_en = "Hello, I am"
en_tokenid = tokenizer.encode(start_context_en)
en_tokenid_tensor = torch.tensor(encoded).unsqueeze(0)
en_embeds = tok_emb(en_tokenid_tensor)
# en_embeds 的输出为:4个是应为标点符号也占用了一位
tensor([[[-0.5124, 0.2813, 0.2190, ..., 0.9741, 0.3482, 0.2245],
[-1.2808, 0.2001, -2.3235, ..., -1.0598, 0.4959, 0.5178],
[-1.8275, 0.1890, -1.5606, ..., 2.0024, 0.5615, 0.8081],
[ 0.4713, 1.1638, -0.4603, ..., 1.3759, -0.4100, 1.5074]]],
grad_fn=<EmbeddingBackward0>)
第一行代表"hello",第二行代表",“,第三行代表"I”,第四行代表"am"。这四个token本身是50257中的四个,所以我们把每一个token作为 q \mathbf{q} q ,再把整个整个上下文向量作为 k \mathbf{k} k ,这样我们就得到了按照每一个token计算后的权重。我们用代码实现一下
attn_scores = torch.bmm(en_embeds, en_embeds.transpose(1, 2))
print(attn_scores)
tensor([[[752.2334, 44.0126, 49.4829, -20.0411],
[ 44.0126, 769.1793, 33.2120, 1.2057],
[ 49.4829, 33.2120, 794.4288, -4.4434],
[-20.0411, 1.2057, -4.4434, 841.0673]]], grad_fn=<BmmBackward0>)
attn_weights = torch.softmax(attn_scores, dim=-1)
print(attn_weights)
tensor([[[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]]], grad_fn=<SoftmaxBackward0>)
通俗一点理解自注力机制就是没有人帮你划重点。
这个全是1的得分显然是不符合常识的,真正大模型在使用的时候,是把输入
X
\mathbf{X}
X 经过三个不同的线性层,变为
W
q
\mathbf{W_q}
Wq
W
k
\mathbf{W_k}
Wk
W
v
\mathbf{W_v}
Wv 然后再计算。线性层的参数是可学习的。简单用代码表示一下
# en_embeds 是 1 * 4 * 768 的矩阵 我们这里不做升降维
W_query = nn.Linear(768, 768, bias=False)
W_key = nn.Linear(768, 768, bias=False)
W_value = nn.Linear(768, 768, bias=False)
query = W_query(en_embeds)
key = W_key(en_embeds)
value = W_value(en_embeds)
attn_scores = query @ key.transpose(1, 2)
attn_weights = torch.softmax(attn_scores, dim=-1)
print(attn_weights)
tensor([[[9.9966e-01, 6.5504e-08, 2.5137e-06, 3.3316e-04],
[9.5424e-12, 1.9095e-04, 9.9981e-01, 3.4956e-10],
[6.4441e-01, 2.2519e-05, 2.5931e-03, 3.5298e-01],
[1.0413e-12, 4.4447e-04, 7.3512e-06, 9.9955e-01]]],
grad_fn=<SoftmaxBackward0>)
context_vec = (attn_weights @ value)
print(context_vec)
tensor([[[ 0.4057, 0.1294, 0.5982, ..., 0.2427, -0.3708, -0.4979],
[-0.6604, -0.7601, 0.4312, ..., -0.6503, 0.3912, 0.2349],
[ 0.1802, 0.0558, 0.1966, ..., 0.7259, -0.4829, -0.5026],
[-0.2259, -0.0727, -0.5388, ..., 1.6182, -0.6942, -0.5163]]],
grad_fn=<UnsafeViewBackward0>)
上面的自注力计算完成后,还有很多的其他层,然后层层递进。最后才是一个输出。
下一篇我们完整的实现一个大模型。
think twice code once