第 3 课结束时,文本已经变成了一串词元 ID:[25954, 3092, …]。但停一下——ID 只是标签:「Building」的编号 25954,和它旁边的 25953、25955 毫无关系,就像身份证号不告诉你谁和谁是一家人。模型要算数学,得先让意思变成可以计算的东西。这一课的主线只有一句话:把每个词变成一个向量,让"意思相近"变成"坐标相近"。
一、one-hot:最朴素的编码,以及它的两个死穴
最直接的思路:词表里第 i 个词,就用一个"只有第 i 位是 1、其余全是 0"的向量表示。这叫 one-hot 编码。拿一个 6 词的小词表演示:
| 词 | ID | one-hot 向量(词表大小 6) |
| cat | 0 | [1, 0, 0, 0, 0, 0] |
| dog | 1 | [0, 1, 0, 0, 0, 0] |
| fish | 2 | [0, 0, 1, 0, 0, 0] |
| king | 3 | [0, 0, 0, 1, 0, 0] |
| eats | 4 | [0, 0, 0, 0, 1, 0] |
| runs | 5 | [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 个语义组:动物/食物/皇室),数一数每对词的同句共现次数——先手算一小块:
| 共现次数 | cute | sleeps | eats | rules |
| cat | 1 | 1 | 1 | 0 |
| dog | 1 | 1 | 1 | 0 |
| fish | 0 | 0 | 1 | 0 |
| king | 0 | 0 | 0 | 1 |
看行: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 → 矩阵第 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 课)。另外,词元进模型还缺一样东西——语序。「猫咬狗」和「狗咬猫」的词元集合相同,谁来区分?下一课:位置编码。
检索练习
本课主读材料
我是你的老师,别客气
比如"为什么余弦比点积更公平""MDS 那几行数学到底干了什么",贴回来问。单步+求值如果卡住了,把操作和报错发我。