词是坐标
第 3 章的计数模型死在一件事上:它不知道 my lord 和 my lady 很像。这一章看神经网络怎么补上这一课——每个 token 先换成一串数,一个高维空间里的坐标;训练会把用法相似的 token 推到相近的方向。于是「像」第一次变成了一件可以计算的事,而「举一反三」就从这里开始。
那个讲词向量必讲的例子:取「king」的向量,减去「man」,加上「woman」,然后在 GPT-2 的全部 50257 个 token 里找方向最接近结果的那一个。
␣queen。经典结果,向量里装着「性别」这个方向
B ␣king。结果离出发点最近
C ␣woman。加上去的那个词把结果拽了过去
D ␣princess。方向对了,但落在了旁边
答案在本章末尾。
一个编号,变成一串数
上一卷说过,模型的输入是一串编号。可是编号本身没有意义:41236 号(␣strawberry)和 41237 号之间,不比和 3 号之间更「近」。神经网络要的是能做加减乘除、能比较远近的东西。
所以 GPT-2 读入编号之后做的第一件事,是查表:它有一张 50257 行、768 列的大表,叫词嵌入表(embedding table)。编号 41236,就取出第 41236 行——一串 768 个小数。这串数,就是这个 token 在一个 768 维空间里的坐标,也叫它的向量。
这张表有 50257 × 768 = 38597376 个数,占了 GPT-2 全部参数的 31.0%。训练开始时它是随机的,训练结束时,它的每一行都被挪到了某个特别的位置上。挪到了哪?量一下就知道。
近的东西,意思也近
量两个向量有多「像」,最常用的是余弦相似度:只看两支箭头的方向夹角,不管长短。方向完全一样是 1,垂直是 0,完全相反是 −1。下面是 GPT-2 的嵌入表里,几个 token 的最近邻:
| token | 方向最接近的几个(余弦相似度) |
|---|---|
| ␣Monday | ␣Tuesday 0.890 · ␣Thursday 0.845 · ␣Wednesday 0.844 · ␣Friday 0.821 · ␣Sunday 0.806 |
| ␣seven | ␣six 0.872 · ␣eight 0.862 · ␣five 0.853 · ␣nine 0.848 · ␣four 0.802 |
| ␣king | ␣kings 0.733 · ␣King 0.696 · ␣queen 0.657 · King 0.604 · ␣prince 0.590 |
| ␣Paris | Paris 0.834 · ␣France 0.634 · ␣French 0.581 · ␣London 0.579 · France 0.571 |
| ␣Python | ␣python 0.771 · Python 0.764 · python 0.697 · ␣Django 0.614 · ␣PHP 0.570 |
没有人告诉过它星期一和星期二是一类东西,也没有人告诉过它 ␣Python 和 Python(不带空格的那个)是同一个词——第 2 章说过,那是两个毫不相干的编号。可是在 768 维空间里,它们几乎指向同一个方向。上一章留下的那个问题——「同一个词的几种拼法,模型怎么知道是一回事」——答案就在这张表里。
为什么「像」会被训出来
训练只要求它猜准下一个 token。可是想一想 ␣Monday 和 ␣Tuesday 出现在什么样的上下文里:「the meeting is on ___」「see you next ___」「every ___ morning」……几乎一模一样。它们后面跟的东西也一模一样。
既然两个 token 在任何地方都能互换而不让下一个 token 的概率变多少,训练就没有理由让它们的向量分开——相反,每一次在「Monday」上学到的东西,只要它们的向量挨在一起,「Tuesday」就能顺带用上。语言学家 J. R. Firth 在 1957 年说过一句后来被引用了无数次的话:「观其伴,知其义。」(You shall know a word by the company it keeps.)嵌入表是这句话的一个数值实现。
这就是第 3 章那堵墙的出口。计数模型里,「见过 Monday」对「Tuesday」毫无帮助;在这里,相似的 token 共享坐标附近的那片空间,在一个上面学到的东西会自动「溢」到它的邻居身上。没见过的组合,只要它的成分和见过的那些「挨得近」,模型就有东西可依。泛化不再需要「见过」,只需要「像」。
那道著名的算术题
2013 年 Google 的 word2vec 论文让「king − man + woman ≈ queen」家喻户晓:好像词向量里藏着一个干净的「性别」方向,可以像做算术一样加加减减。在 GPT-2 的嵌入表里做一遍:
king − man + woman → ⟪' king'⟫ 0.776 ' queen' 0.709 ' princess' 0.605 ' Queen' 0.596 Paris − France + Japan → ⟪' Japan'⟫ 0.700 ' Tokyo' 0.688 ' Paris' 0.634 Paris − France + Germany → ⟪' Paris'⟫ 0.732 ' Berlin' 0.666 ' Germany' 0.656
三道题,离结果最近的都是题目里本来就有的那个词。正确答案(queen、Tokyo、Berlin)都在,而且都排第二——方向确实存在,只是它比「离出发点近」这个效应弱得多。
那经典结果是怎么来的?word2vec 当年的评测规则里写着:找最近邻的时候,把题目里出现过的词排除掉。排除掉 king,queen 就是第一。这不是作弊,评测要的本来就是「排除输入之后的最近邻」;但它被转述成「向量里有一套干净的语义代数」时,那条排除规则就被省略了。这里能带走的判断是:嵌入空间里确实有一些有意义的方向,但它们是叠在一堆别的东西上的弱信号,不是一套坐标轴分明的语义系统。
768 维的空间,比你想的大得多
还有一件和直觉对不上的事。随便取两支随机方向的箭头,它们的夹角是多少?在纸上(2 维),什么角度都可能;在 768 维里,答案几乎永远是「接近 90 度」:
| 维数 | 两个随机方向的 |余弦| 平均值 | |余弦| < 0.1(几乎垂直)的比例 |
|---|---|---|
| 2 | 0.637 | 6.4% |
| 3 | 0.500 | 10.0% |
| 10 | 0.258 | 23.1% |
| 100 | 0.080 | 68.0% |
| 768(GPT-2) | 0.029 | 99.4% |
| 12288(GPT-3 最大那个) | 0.007 | 100.0% |
(每行是 1000 个随机方向两两比较的结果。)768 维里,99.4% 的随机方向对几乎互相垂直——互相不干扰。这意味着这个空间里能塞下的「几乎独立的方向」远远多于 768 个。第 13 章会回到这件事:模型正是利用这一点,在 768 维里存了远多于 768 个概念。
顺带一个怪癖:GPT-2 自己的嵌入并不是均匀散开的。随便两个 token 的余弦平均是 0.268,不是 0——所有向量都挤在一个锥里,朝着差不多的方向。这是一个被反复报告过的现象(叫各向异性),它提醒你:拿这些原始向量直接算相似度时,0.3 并不代表「有点像」,可能只是「都在那个锥里」。上面那张表里 0.89 的才算真的近。
嵌入表就是模型里的一个矩阵,直接取出来算就行:
from transformers import AutoTokenizer, AutoModelForCausalLM
tok = AutoTokenizer.from_pretrained("gpt2")
W = AutoModelForCausalLM.from_pretrained("gpt2").transformer.wte.weight.detach() # 50257 × 768
En = W / W.norm(dim=1, keepdim=True) # 只留方向
def vec(w):
return W[tok.encode(w)[0]]
def near(v, k=4, skip=()):
s = En @ (v / v.norm()) # 和每个 token 的夹角余弦
ids = [i for i in s.topk(k + len(skip)).indices.tolist() if i not in skip][:k]
return [(tok.decode(i), round(s[i].item(), 3)) for i in ids]
for w in [" king", " Monday", " seven"]:
print(f"{w!r:9} 最近的邻居 {near(vec(w), skip=[tok.encode(w)[0]])}")
print("king − man + woman →", near(vec(" king") - vec(" man") + vec(" woman")))
本机输出:
' king' 最近的邻居 [(' kings', 0.733), (' King', 0.696), (' queen', 0.657), ('King', 0.604)]
' Monday' 最近的邻居 [(' Tuesday', 0.89), (' Thursday', 0.845), (' Wednesday', 0.844), (' Friday', 0.821)]
' seven' 最近的邻居 [(' six', 0.872), (' eight', 0.862), (' five', 0.853), (' nine', 0.848)]
king − man + woman → [(' king', 0.776), (' queen', 0.709), (' princess', 0.605), (' Queen', 0.596)]
试试你自己关心的词:编程语言、城市、颜色、情绪词。也试试找「反义词」——你会发现 ␣happy 的近邻里有 ␣unhappy,因为反义词出现的上下文往往一模一样。
pip install torch transformers && python 05.py
在线跑:Google Colab。想看三维投影:projector.tensorflow.org 能把高维词向量用 PCA 投到三维里转着看。
- 语义搜索和 RAG(检索增强生成)的底层就是这一章:把每段文档变成一个向量,把问题也变成一个向量,找余弦相似度最高的那几段。所谓「向量数据库」,主要做的就是这件事的快速版本。
- 推荐系统里,用户和商品各有一个向量,「你可能喜欢」就是找离你的向量最近的商品。它们和词向量是同一个想法:用出现在什么场合来定义一个东西是什么。
- 「相似度 0.3 算不算相似」没有通用答案,取决于那个空间的形状(比如上面说的各向异性)。做检索时,先看看随便两个文档的相似度分布在哪,再决定阈值。
- 人脸识别、以图搜图、音乐指纹:把一样东西压成一个坐标,然后比距离——这个模式在 AI 里无处不在。
「词向量的每一维代表一个意思——比如第 17 维是『性别』,第 203 维是『是不是动物』。」
训练没有任何理由把意思对齐到坐标轴上。把整个嵌入表旋转一个角度,再把下一层的权重反向旋转回来,整个模型的输出一位都不会变——所以「第 17 维」本身没有意义,意义在方向上(某几维的特定组合)和邻居关系上。
而且上面那张维数表已经暗示了:768 维的空间能放下远多于 768 个几乎独立的方向。第 13 章会看到,模型确实这么干了——概念比维度多,每一维都同时参与了好几个概念。所以「找到那个代表 X 的神经元」常常是一个没有答案的问题。
正确答案是 B:最近的是 ␣king 自己,余弦 0.776;␣queen 排第二,0.709。
另外两道(Paris − France + Japan / + Germany)也一样:第一名都是题目里的词,正确答案都排第二。
␣woman 不在前五。拽得最狠的是出发点 king:减去 man、加上 woman 这两步的改动,比 king 自身的分量小。
D 「方向对了,但落在旁边」——princess 确实在榜上(第三,0.605)。这个直觉其实最接近事实:向量算术给出的是一个大致的区域,而不是一个精确的点。
《张成》第 4 章讲点积为什么是「相似度的度量衡」,第 13 章讲换基——那正是「坐标轴本身没有意义、旋转不改变任何东西」的数学版本;第 23 章讲嵌入里的低秩直觉。《拨动》会提醒你另一件事:两个词向量相近,只说明它们「出现在相似的场合」,不说明任何因果。
这一章的一句话
每个 token 先变成一个高维坐标;训练把出现在相似上下文里的 token 推到相近的方向,于是在一个上面学到的东西会溢到它的邻居身上——这就是泛化的来处。意义在方向和邻居里,不在某一根坐标轴上;而著名的向量算术,是一个真实但微弱的信号。
可是「训练把它们推过去」——到底是怎么推的?下一章打开这个黑箱。一个 13 万参数的小网络,用手写的反向传播训练:算出全部 13 万个参数「该往哪拧」,只花了相当于 2.3 次前向计算的时间;要是一个参数一个参数地去试,得花它的十一万多倍。