Transformer 实现机制 · 第 4 课

词嵌入 —— 意思如何变成几何

⏱ 约 25 分钟 · 前置:第 3 课(分词与 BPE) · 主干教材:Vizuara《The Transformers》1.5 前半 · 本课目标:说清 one-hot 为什么必须被淘汰;亲手把一小块语料变成"语义地图";看懂 king−man+woman≈queen 的几何含义

第 3 课结束时,文本已经变成了一串词元 ID:[25954, 3092, …]。但停一下——ID 只是标签:「Building」的编号 25954,和它旁边的 25953、25955 毫无关系,就像身份证号不告诉你谁和谁是一家人。模型要算数学,得先让意思变成可以计算的东西。这一课的主线只有一句话:把每个词变成一个向量,让"意思相近"变成"坐标相近"。

一、one-hot:最朴素的编码,以及它的两个死穴

最直接的思路:词表里第 i 个词,就用一个"只有第 i 位是 1、其余全是 0"的向量表示。这叫 one-hot 编码。拿一个 6 词的小词表演示:

词IDone-hot 向量(词表大小 6)
cat0[1, 0, 0, 0, 0, 0]
dog1[0, 1, 0, 0, 0, 0]
fish2[0, 0, 1, 0, 0, 0]
king3[0, 0, 0, 1, 0, 0]
eats4[0, 0, 0, 0, 1, 0]
runs5[0, 0, 0, 0, 0, 1]

两个死穴立刻浮现:

死穴一:维度爆炸且极稀疏。GPT-2 词表 50,257 个词元,one-hot 向量就是 50,257 维——每个向量只有 1 个非零位,99.998% 的空间在存零。

死穴二(致命):任意两个词都正交。向量的相似度用点积衡量:对应位置相乘再求和。手算一下:

手算:one-hot 世界里没有"相近" cat·dog = [1,0,0,0,0,0]·[0,1,0,0,0,0] = 0
cat·king = [1,0,0,0,0,0]·[0,0,0,1,0,0] = 0
——「猫和狗」的相似度 = 「猫和国王」的相似度 = 0。任何两个不同的词,相似度一律为 0。

原文(1.5 节)的原话:one-hot 让 cat/dog 和 cat/quantum 一样正交——向量里没有任何语义信号。顺带一提,另一种朴素方案 bag-of-words(数词频)也不行:它把「dog bites man」和「man bites dog」算成一样,语序信息全丢。

二、分布假说:看一个词的"朋友圈",就知道它的意思

出路来自语言学的一句老话(Firth, 1957):「看一个词经常和谁在一起,你就知道了它的意思。」这叫分布假说(distributional hypothesis)。

直觉检验:「cat」经常和 cute、sleeps、eats 在一起;「dog」也经常和 cute、sleeps、eats 在一起——它俩朋友圈高度重合,所以意思相近。而「king」的朋友圈是 rules、crown、castle——和 cat 几乎不重叠。原文 1.5 节讲的 word2vec(2013)就是这个思想的工程化:训练一个小神经网络做"猜词游戏"(用上下文猜词,或用词猜上下文),训练完把网络的隐藏层权重抠出来——每个词就得到了一个向量。

这个思路能直接"算"出来。下面是一小块语料(28 句话,3 个语义组:动物/食物/皇室),数一数每对词的同句共现次数——先手算一小块:

共现次数cutesleepseatsrules
cat1110
dog1110
fish0010
king0001

看行:cat 行和 dog 行几乎一模一样(所以意思相近);fish 行与 cat 行只在"eats"上重合(食物被动物吃——中等关联);king 行和谁都不重合(无关)。把"两行有多像"算成余弦相似度,再降到 2 维画在纸上——词义就成了地图上的坐标。

三、亲手跑:从 28 句话到一张"语义地图"

# ============================================================ # 【演示目标】亲眼看到"意思变成几何":只用"数共现次数"这一种土办法, # 就能让动物聚成一团、食物聚成一团、皇室聚成一团——没人告诉模型任何词义! # 【思路】四步: # 1. 数:语料里每对词的同句共现次数 → 共现矩阵 C(C[i][j] = 词 i 和 j 同现次数) # 2. 比:两行越像 = 两词越像 → 每行归一化后算行与行的余弦相似度 → 相似度矩阵 SIM # 3. 压:22 维的行向量没法画在纸上 → 经典 MDS(多维缩放)把"相似度结构"压到 2 维 # 4. 画:左图散点(每团 = 一类意思),右图热图(亮块 = 同类词互相相似) # 对照手算: 正文第二节的共现表,cat-dog / cat-king 的相似度数字在输出里能找到 # ============================================================ import numpy as np import matplotlib.pyplot as plt import warnings warnings.filterwarnings("ignore") # AGG 后端与弃用提示会污染输出区,静音 # ---- 第 1 步·语料:28 句话。每组词有共享语境词 + 专属语境词 ---- animals = ["cat", "dog", "mouse", "bird"] foods = ["fish", "cheese", "apple", "meat"] royals = ["king", "queen", "prince", "princess"] sentences = [] uniq_a = {"cat": "the cat purrs", "dog": "the dog barks", "mouse": "the mouse squeaks", "bird": "the bird flies"} uniq_f = {"fish": "the fish is salty", "cheese": "the cheese is smelly", "apple": "the apple is sweet", "meat": "the meat is salty"} uniq_r = {"king": "the old king smiled", "queen": "the kind queen smiled", "prince": "the young prince smiled", "princess": "the tall princess smiled"} for a in animals: # 动物共享 cute/sleeps/runs/fast sentences += ["the cute %s sleeps" % a, "the %s runs fast" % a, uniq_a[a]] for f in foods: # 食物共享 tasty/fresh sentences += ["the tasty %s" % f, "the %s is fresh" % f, uniq_f[f]] for r in royals: # 皇室共享 rules/land/wears/crown sentences += ["the %s rules the land" % r, "the %s wears a crown" % r, uniq_r[r]] for a, f in zip(animals, foods): # 桥接句:动物吃食物(两类中间产生中等关联) sentences.append("the %s eats the %s" % (a, f)) stop = {"the", "a", "is"} # 停用词:信息量太低,不参与统计 words = [] # 词表(按出现顺序,方便对照) for s in sentences: for w in s.split(): if w not in stop and w not in words: words.append(w) idx = {w: i for i, w in enumerate(words)} n = len(words) # ---- 第 2 步·共现矩阵:同句出现就算一次关系 ---- C = np.zeros((n, n)) for s in sentences: ws = [w for w in s.split() if w not in stop] for i in range(len(ws)): for j in range(len(ws)): if i != j: C[idx[ws[i]], idx[ws[j]]] += 1 # 双向计数(对称) # ---- 第 3 步·行余弦相似度:两行越像 = 两词越像 ---- Nrm = np.linalg.norm(C, axis=1, keepdims=True) # 每行的长度 SIM = (C @ C.T) / (Nrm * Nrm.T + 1e-9) # 余弦 = 点积 / 长度之积 print("先验证正文手算的数字:") print(" cos(cat, dog) = %.2f ← 同类,高" % SIM[idx["cat"], idx["dog"]]) print(" cos(cat, fish) = %.2f ← 桥接(猫吃鱼),中等" % SIM[idx["cat"], idx["fish"]]) print(" cos(cat, king) = %.2f ← 无关,零" % SIM[idx["cat"], idx["king"]]) # ---- 第 4 步·经典 MDS:把相似度结构压到 2 维,好画在纸上 ---- # 做法:余弦距离 D = 1 - SIM;对"距离平方"做双重中心化后特征分解,取前 2 个方向 D2 = (1 - SIM) ** 2 J = np.eye(n) - np.ones((n, n)) / n B = -0.5 * J @ D2 @ J eigval, eigvec = np.linalg.eigh(B) coords = eigvec[:, -2:] * np.sqrt(np.maximum(eigval[-2:], 0)) # 每个词的 2 维坐标 # ---- 画图:左散点(语义地图) + 右热图(相似度矩阵) ---- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.6)) groups = [("animals", animals, "#5b5bd6"), ("foods", foods, "#1a7f37"), ("royal", royals, "#b45309")] for gname, gw, color in groups: pts = np.array([coords[idx[w]] for w in gw]) ax1.scatter(pts[:, 0], pts[:, 1], color=color, s=64, label=gname, zorder=3) for w, (x, y) in zip(gw, pts): ax1.annotate(w, (x, y), textcoords="offset points", xytext=(5, 4), fontsize=8, color=color) ax1.set_title("Word map from co-occurrence (MDS 2D)") ax1.axhline(0, color="#e5e1d5", lw=0.8); ax1.axvline(0, color="#e5e1d5", lw=0.8) ax1.legend(fontsize=8); ax1.tick_params(labelsize=7) im = ax2.imshow(SIM, cmap="viridis") ax2.set_xticks(range(n)); ax2.set_xticklabels(words, rotation=90, fontsize=6) ax2.set_yticks(range(n)); ax2.set_yticklabels(words, fontsize=6) ax2.set_title("Similarity matrix (bright = similar)") fig.colorbar(im, ax=ax2, fraction=0.046) plt.tight_layout() plt.show()
左图:没人告诉模型任何词义,三团自动按"语境朋友圈"分开;cat–fish 之间由"eats"桥接,距离中等——这正是"猫吃鱼"的语义。右图:整张相似度矩阵,亮色块 = 同类词互相相似。真正的 word2vec/GPT 用训练代替"数次数",得到的向量维度更高(768 维)、性质相同。

四、嵌入层:LLM 里的"查表",以及类比魔法

真实 LLM 的做法把上一节的思想装进一个矩阵:嵌入层(embedding layer)就是一个 词表大小 × 向量维度 的大表,GPT-2 是 50,257 × 768。词元 ID 为 i 的词,它的向量 = 表的第 i 行——查表而已(原文图 1.27)。表里每个数都是训练时学出来的:意思相近的词,训练自然把它们推到相近的位置。(衡量"相近"用的余弦相似度,数学细节见补充课:余弦相似度。)

词元 ID 25954 查第 i 行 嵌入矩阵 E(50257 × 768) ID 0 的向量 ID 1 的向量 … ID 25954「Building」的向量 ← 查到 … ID 50256 的向量 每个数都是训练学出来的 768 维向量 [0.21, -0.87, 0.05, …, 0.33] — 意思变成了坐标 语义相近的词,训练会把它们的行推得相近;整个表随模型一起端到端训练
嵌入层 = 可训练的查表:ID → 矩阵第 i 行 → 768 维向量(改绘自 Vizuara 图 1.27)。

向量一旦把意思变成了几何,就出现了原文 1.5 节展示的那著名魔法——词义关系 = 方向关系:king − man + woman 的结果向量,离 queen 最近。亲手验证(向量是手造示意的 3 维版,真实向量几百维;每一维的含义如"皇室度""性别"是训练后涌现的,不是人规定的):

# ============================================================ # 【演示目标】验证词向量的著名魔法:king - man + woman 的结果,离 queen 最近 # ——这说明"性别""皇室"这类词义关系,被表示成了向量空间里的"方向" # 【思路】三步: # 1. 造一组示意向量(3 维:皇室度/性别/动物性;真实向量几百维,3 维是为了能算给人看) # 2. 用余弦相似度比较几对词:同类应高,跨类应低 # 3. 做向量算术 king - man + woman,对全词表找最近邻 → 应该是 queen # ============================================================ import numpy as np # ---- 第 1 步·示意向量:3 维 = [皇室度, 性别(+男/−女), 动物性] ---- emb = { "king": [0.95, 0.35, 0.10], "queen": [0.92, -0.35, 0.10], "prince": [0.55, 0.35, 0.12], "princess": [0.52, -0.35, 0.12], "man": [0.05, 0.35, 0.05], "woman": [0.03, -0.35, 0.00], "cat": [0.00, 0.15, 0.95], "dog": [0.02, 0.10, 0.90], } vocab = list(emb) def cos(a, b): # 余弦相似度 = 点积 / 长度之积;衡量"方向有多一致",与向量长短无关 a, b = np.array(a), np.array(b) return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9)) # ---- 第 2 步·相似度体检:同类高、跨类低 ---- print("余弦相似度:") for a, b in [("cat", "dog"), ("king", "queen"), ("cat", "king"), ("king", "dog")]: print(" cos(%-6s, %-6s) = %+.3f" % (a, b, cos(emb[a], emb[b]))) # ---- 第 3 步·类比算术:king(皇室,男) - man(平民,男) + woman(平民,女) ---- # 皇室度: 0.95 - 0.05 + 0.03 ≈ 0.93(仍皇室);性别: +0.35 - 0.35 - 0.35 ≈ -0.35(变女) # → 结果向量应当落在"皇室 + 女"的位置,也就是 queen 附近 target = np.array(emb["king"]) - np.array(emb["man"]) + np.array(emb["woman"]) print("\nking - man + woman 的结果向量:", target.round(3)) print("对全词表找最近邻(排除参与运算的三个词):") sims = sorted(((cos(target, emb[w]), w) for w in vocab if w not in ("king", "man", "woman")), reverse=True) for s, w in sims[:3]: print(" %-9s %+.3f" % (w, s)) print("最近邻 =", sims[0][1], "← 魔法成立")
本课胜利 + 新武器提醒 ① one-hot 人人正交,没有语义;② 分布假说:朋友圈像 = 意思像;③ 28 句话的共现统计就能画出语义地图——真实模型用训练做到同样的事,且是 768 维;④ 嵌入层 = 查表,词义关系 = 方向关系。别忘了这门课刚装的调试武器:在上面两个代码块点「单步 🐛」,走到感兴趣的一行,在输入框里「求值」试试 SIM[0,1]、sims、target——每一步的中间量都能亲手查。
埋个伏笔:静态嵌入的"一词多义"困境 查表嵌入是静态的:「bank」不管出现在「river bank」还是「investment bank」,查到的都是同一行。原文 1.5 节指出,Transformer 用的是上下文嵌入——同一个词在不同句子里,向量会随语境变化。靠什么变化?靠下一课之后的主角:注意力机制(第 6–8 课)。另外,词元进模型还缺一样东西——语序。「猫咬狗」和「狗咬猫」的词元集合相同,谁来区分?下一课:位置编码。

检索练习

本课主读材料

去读/看(约 15 分钟)

Vizuara《The Transformers》1.5 节前半(one-hot 困境 → word2vec → LLM 嵌入层),重点看图 1.26–1.27:高维嵌入空间与查表结构。

图解补充:The Illustrated Word2vec — Jay Alammar——word2vec 与嵌入直觉的最佳图解,读到"Embedding"小节即可。

视频(睡前看):3Blue1Brown 深度学习第 5 章——前 20 分钟正是词嵌入与 GPT 输入,和本课完全对齐。

我是你的老师,别客气 比如"为什么余弦比点积更公平""MDS 那几行数学到底干了什么",贴回来问。单步+求值如果卡住了,把操作和报错发我。