Transformer 前置知识 08:位置编码

上一课,文字变成了一个个向量。但还有一个问题:

Transformer 是同时看所有词的,它并不知道谁在前、谁在后。

这一课讲模型怎么知道每个词排在第几个:位置编码。


我爱猫,还是猫爱我?

“我爱猫”和”猫爱我”,用的是同样的三个词向量,只是顺序不同。

如果不告诉模型顺序,它就分不清这两句话。可这两句话的意思,完全相反。

位置编码:给每个位置一个向量

解决办法是:给每个位置也准备一个向量,叫位置编码(Positional Encoding),然后加到词向量上:

输入词向量位置向量

这样,同一个”猫”字,放在第 1 个和放在第 3 个,加上的位置向量不同,得到的向量就不一样了,模型也就能分清”我爱猫”和”猫爱我”。

直接加编号?不行

最简单的想法,是直接用位置的编号:第 1 个词加 1,第 2 个词加 2……

但一篇文章可能有几千个词,到了第 1000 个词就要加 1000。而词向量本身的数值一般都很小,编号越来越大,就把词本身的意思淹没了。

我们需要的位置编码,最好满足:

  1. 每个位置都独一无二
  2. 数值始终很小,不管位置排到多远
  3. 能体现位置之间隔了多远

原论文的办法:正弦波和余弦波

Transformer 原论文《Attention Is All You Need》用了一个巧妙的办法:一组正弦波和余弦波。

  • :第几个位置
  • :向量的第几维(偶数维用 sin,奇数维用 cos)
  • :向量的总维度

看起来复杂,其实意思很简单:向量的每一维,用一个不同频率的波。前面几维变得很快,后面几维变得很慢:

每一维是一条不同频率的波:前面几维快,后面几维慢

而且 sin 和 cos 的值,不管位置排到多远,始终在 −1 到 1 之间。

就像时钟的指针

这就像一个时钟:

指针 转得 能分清
秒针 快 相邻的几秒
分针 慢一些 相隔几分钟
时针 最慢 相隔很远的时刻

单看哪一根指针都不够,但把所有指针合在一起,每一个时刻都独一无二。

位置编码也是这样:快的维度分辨相邻位置,慢的维度分辨相隔很远的位置,合在一起,每个位置都有一个独一无二的向量。

把 128 个位置的位置编码画成一张图,就能看到这种”下面快、上面慢”的规律:

位置编码矩阵:下面几维变化快,上面几维变化慢

用代码算一下:

1
2
3
4
5
6
7
8
9
10
11
import numpy as np

def positional_encoding(num_pos, d):
pos = np.arange(num_pos)[:, None]
i = np.arange(d)[None, :]
angle = pos / np.power(10000, (2 * (i // 2)) / d)
return np.where(i % 2 == 0, np.sin(angle), np.cos(angle))

pe = positional_encoding(128, 32)
print(pe.shape) # (128, 32)
print(pe.min(), pe.max()) # 始终在 -1 ~ 1 之间

考考你

“我爱猫”和”猫爱我”,加上位置编码以后,两个”猫”字的向量还一样吗?

答案是不一样。 “猫”的位置变了(一个在第 3 个,一个在第 1 个),加上的位置向量不同,模型就能分清这两句话了。

现在的大模型:改进版

原论文的正弦位置编码之后,又出现了很多改进版:

  • 可学习的位置向量:像词向量一样,准备一张”位置表”,在训练中学出来(GPT-2、BERT 用的就是这种)
  • 旋转位置编码 RoPE:按位置把向量转一个角度,两个词之间转角的差,正好反映它们隔了多远。现在很多开源大模型都在用

思路都一样:让模型知道每个词排在第几个,以及词和词之间隔了多远。

小结

  1. Transformer 同时看所有词,本身不知道顺序
  2. 位置编码:给每个位置一个向量,加到词向量上
  3. 直接加编号不行,数值会越来越大,淹没词义
  4. 原论文用不同频率的正弦和余弦,像时钟的秒针、分针和时针,数值始终在 −1 到 1 之间
  5. 现在常用的改进版:可学习的位置向量、RoPE

下一课,我们终于进入 Transformer 的核心:注意力机制。