解释一个整数 token id 如何变成高维向量,以及为什么向量相近意味着 token 相关。
Transformer 需要每个 token 的连续表示,这样才能表达相似度,梯度也才能流动。
术语表 · 7 个术语
- embedding
- token id 被映射到的那个向量——嵌入矩阵的一行,网络其余部分就在它上面做计算。
- embedding matrix
- 形状为 [vocab_size, hidden_dim]。对 Qwen3.5-0.8B 来说约为 248,320 × 1,024——约 254M 个参数。
- hidden_dim
- 每个 token 的向量在模型中流动时的宽度。Qwen3.5-0.8B 为 1,024。
- tied embeddings
- 让输入嵌入矩阵与输出 lm_head 共享,使同一个向量空间既负责读取提示词,也负责写出下一个 token 的 logit。
- PCA
- 主成分分析——把高维点投影到方差最大的两个方向上,从而画出 2D 图。
- cosine similarity
- 两个向量的点积除以各自的范数。衡量方向的一致程度而忽略长度;接近 1 表示非常相似。
- semantic direction
- 像 E(woman) − E(man) 这样的向量差,近似编码某一个特征(性别、单复数、所属国家),可以加到其他嵌入上或从中减去。
词嵌入(Embeddings):把 token 变成向量
分词给了模型一串整数——每个 token 一个 id。但整数不携带模型能计算的含义:id 1234 并不比 1235 更大、更小,或在任何有用的意义上更相似。下一步是把每个 id 映射成网络其余部分能做数学运算的连续向量。这个向量就是该 token 的嵌入(embedding)。
为什么是向量?
连续的表示让模型能够表达相似的“程度”。核心想法是:向量就是空间中的一个方向,含义相近的词最终会指向相近的方向——于是“相关”变成了模型可以用夹角度量的东西。著名的(虽然有点理想化的)word2vec 例子——king − man + woman ≈ queen——表明学到的嵌入甚至能把“关系”捕捉成可以做算术的方向。现代 LLM 的嵌入结构比 word2vec 的纠缠得多(一个模型要跨多种语言和代码风格服务几十种任务),所以这种干净的类比算术变弱了——但底层思想没变:向量相近意味着 token 相关,而 Transformer 各层的整个前向传播,做的就是以符合上下文的方式挪动这些向量。
这里的“相近”指的是指向相同的方向,而不是“尺子量出来的距离短”。在把真实的 1024 维向量投影成图片之前,先在一个可以上手拖动的平面 2-D 玩具里建立核心直觉:含义住在向量的方向里,两个向量之间的夹角度量它们有多相关。
所有度量只用到两小块数学,而且都能写在一行里。点积就是“对应位置相乘,再加起来”:对 a = [3, 4] 和 b = [4, 3],a·b = 3×4 + 4×3 = 24。范数 ‖a‖ 是向量的长度:‖a‖ = √(3² + 4²) = 5,同样 ‖b‖ = 5。余弦相似度只是把长度除掉之后的点积:cos = 24 / (5 × 5) = 0.96——这两个向量几乎指向同一个方向。这一个配方——点积除以范数——就是本章里所有的相似度数字。
拖动长度,看下方的 cos θ 纹丝不动——长度不会改变夹角。
把探针转到与 cat 对齐 → cos θ → 1(“相关”)。转到直角 → cos θ → 0(“不相关”)。指向相反方向 → cos θ → −1(“相反”)。长度从不进入余弦——这正是嵌入相似度用夹角、而不用直线(欧氏)距离的原因。
仅为示意——这些是手工摆放的 2-D 教学箭头,不是真实嵌入。Qwen3.5-0.8B 的向量住在 1,024 维空间里;这张平面图只为建立直觉。
嵌入表
嵌入查询是一次矩阵乘法(或者等价地,一次按行取数):给定整数 id,取嵌入矩阵的第 id 行。矩阵的形状是 [vocab_size, hidden_dim]。对 Qwen3.5-0.8B 来说,光这一张表就有约 248,320 × 1,024 ≈ 254 million 个参数(一个参数就是一个学到的数字;整个模型一共约 853M 个)——约占整个模型的三分之一。
值得停下来想一想:这 2.54 亿个数字一开始全都是随机噪声。没有人坐下来把 tiger 安排在 lion 旁边、画一条性别轴,或者把 king − man + woman ≈ queen 接好线。本章里你会去拨弄的每一个聚类、每一条语义方向、每一个类比,都是在海量文本上拟合同一个目标——预测下一个 token——所涌现出来的副产品。这套几何结构是训练留下的化石,而不是谁手工搭起来的字典。
下面把这次查询画成可以逐步播放的图。一个 token id 进来,一行向量出去——中间什么也没有计算:
分词器递给模型一个整数:9059(token "·cat")。嵌入表只是一个很高的矩阵——248,320 行,每个词表条目一行。
这 8 个浮点数是示意性替身——右侧的实时演示会从已加载的模型里取出真实的 1,024 维行向量。机制本身才是课程重点:输入 token id,输出表中的一行。
现代 LLM(包括 Qwen3.5)常常把输入嵌入与输出“反嵌入”绑定(tie)——后者就是把最终隐藏状态变回词表 logits 的 lm_head,我们会在 LM head 一章细讲;现在你只需把它读作“给下一个 token 打分的那一层”。绑定这两个矩阵既省参数,也迫使这套表示在两个方向上都好用:读取提示词的那个向量空间,同时也要写出下一个 token 的 logit。右侧的小部件画的正是这同一个共享矩阵里的行。
| vocab_size | 248,320 |
| hidden_dim | 1024 |
PCA:通往 1024 维空间的一扇 2D 窗户
想象给一座 3-D 雕塑拍照:你会把它转到特征铺得最开的角度,好看清结构。PCA 就是为 1024 维嵌入空间挑出那个“最佳机位”——而且和照片一样,它把纵深压扁了,所以屏幕上的距离只是粗略的参考,不是精确的尺子。
我们看不见 1024 个维度,所以要投影。主成分分析找出数据中方差最大的方向,并投影到前两个(或前三个)方向上。结果是线性压扁所能做到的最好程度:投影后的点保留了一张 2D 图所能承载的最多原始分布信息。
问题在于“尽可能多”仍然很少。1024 维点云的前两个主成分通常只解释总方差的百分之几——其余都在被丢掉的方向里。所以 PCA 很适合发现聚类(在嵌入空间中总体方向相近的 token 会落在一起),但两根轴装不下 1024 根轴的几何。
你应该看到什么
点击 Load embeddings。小部件会把来自六个类别(动物、数字、颜色、国家、动词、食物)的约 110 个词分词,通过模型 worker 查出每个词第一个 token 的嵌入,并在你的浏览器里运行 PCA。每个词只用它的第一个子词 token 来画,所以被切成多块的词在图上只由开头那一块代表。可以期待看到:
- 按类别聚类。动物挨着动物,数字挨着数字,依此类推。尽管只是 1024 维空间的一个 2D 小切片,聚类通常仍然清晰。
- 聚类内部的细结构。数字常常排成大致的梯度。颜色分成暖色/冷色两个子区域。食物把咸味和甜味分开。
- 看起来“懂语义”的最近邻。点任何一个点:完整 hidden-dim 空间里的 top-5 近邻会被高亮,它们通常是同类词,再加上几个出人意料的跨类别“擦边球”。
为什么用余弦,而不是欧氏距离?
余弦相似度度量两个向量之间的夹角,而夹角正是嵌入空间中编码语义方向的东西。欧氏距离度量完整的向量差,把方向和长度混在了一起。训练得到的向量长度取决于优化动态,并没有语义含义——两个同义词的范数可以差很远,方向却几乎相同。这就是余弦成为嵌入相似度标准的原因。有些检索系统会在预先归一化的向量上用 L2,那在数学上是等价的。
散点图用来定向,不用来测量
散点图里你看到的一切都受一条前提约束:它把 1024 维向量投影到仅仅两根轴上,丢掉了 1022 个维度的结构。屏幕上看着很近的两个点,在真实空间里可能离得很远,反之亦然。颜色分组依然会聚在一起(方差最大的几个方向往往能抓住它们),但距离会骗人——所以永远不要从图上读相似度。这正是最近邻浏览器在完整的 1024 维空间、而不是投影后的 2D 坐标里计算余弦相似度的原因。把散点图当成找聚类的导航图,而不是尺子。
余弦相似度,并排看
下面的面板完全跳过投影:每根条形给出该模型家族的一个大致余弦相似度——右侧的实时散点图用的才是真实嵌入。条形的排序就是要教的结论——完全相同 > 同义词 > 相关 > 反义词 > 跨语言 > 不相关。
数值仅为示意;不同模型构建之间的实测可能相差约 ±0.1。要教的结论是*排序*:完全相同 > 同义词 > 相关 > 反义词 > 跨语言 > 不相关。
方向承载含义
到目前为止,我们一直把相似度当作两个完整向量之间的一个数字。但这个空间还有更细的结构:嵌入之间的差可以表现得像特征。如果 E(woman) − E(man) 大致捕捉了“性别轴”,那么把这个差加到别的向量上,就应该让它沿同一条轴移动——这正是经典的 word2vec 把戏:E(king) − E(man) + E(woman) 应该落在 queen附近。减掉“男性”,加上“女性”,“王室”这部分会一路跟着过来。
在 2026 年的 LLM 嵌入矩阵里,这一招还灵吗?我们在这个 checkpoint 上实际测了一遍,诚实的回答是“灵,但要打个星号”。queen 本来就是 king 的第 4 名原始近邻——排在大小写和复数变体 King、King(无前导空格)、kings 之后——所以这个算术并没有凭空召唤出 queen。它真正做到的是把 queen 提升到第 1 名,越过所有 king 变体:这个方向把“男性”成分剥掉了足够多,让女性对应词胜出。同一招还能推广:E(Paris) − E(France) + E(Japan) 让 Tokyo 排到第 1——而且由于这个 248,320 个 token 的词表是多语言的,东京和日本会和 Osaka、Kyoto 出现在同一份 top-10 里。
不是每条方向都这么上镜。复数方向 E(cats) − E(cat) 确实存在——复数名词与它的点积一致为正,单数徘徊在 0 附近——但数值非常微弱,理想中的“one < two < three”递增趋势几乎看不出来。而且这些方向本身也只是大致一致:woman − man 与 queen − king 的余弦只有 0.292,远谈不上平行。这并不意外——这个矩阵是为“帮助预测下一个 token”训练的(输入/输出权重绑定,身兼两职),不是为了通过类比测验。线性的特征方向只是副产品,被这一个矩阵必须编码的其他一切弄花了。
- #1·queen0.578
- #2queen0.493
- #3·kings0.487
- #4·KING0.469
- #5·princess0.466
- #6Queen0.465
- #7King0.457
- #8國王0.433
- #9·Queen0.424
- #10·wanita0.423
诚实核查:·queen 本来就是 king 的第 4 名原始近邻——排在大小写/复数变体 ·King、King(无前导空格)、·kings 之后。− man + woman 真正做到的,是把 queen 提升到第 1 名,越过所有 king 变体。点上面的开关对比一下。
取 plur = E(cats) − E(cat),再与其他词做点积(余弦)。复数名词的结果一致为正,单数接近 0 或为负——但数值都非常小。
数词 one → five(理想中的“复数程度递增”趋势——几乎看不出来):
所有数字均由该 checkpoint 的 embed_tokens.weight([248,320 × 1,024])离线算出:进入任何 Transformer 层之前的原始输入嵌入,在完整词表上用 fp32 计算余弦,近邻列表排除了输入词本身。这种算术是 近似的,不是精确的——这个矩阵是为“预测下一个 token”训练的(且输入/输出权重绑定),不是为类比训练的。一个症状:cosine(woman − man, queen − king) = 0.292,两条“性别方向”只是大致平行而已。
为什么需要这么多维度?
如果只在 3D 里做这件事,几乎所有概念都会撞在一起。“动物”、“国家”、“颜色”、“动词”、“句法角色”、“语域”、“语言”以及另外十几条含义轴,根本没有空间在一个三轴空间里独立变化。Qwen3.5-0.8B 使用 1,024 个嵌入维度,给了模型相当宽裕的“房间”——每个独立特征都可以占据自己的方向,而不挤到别人。
接下来(自注意力)我们将看到这些向量在 Transformer 中是如何真正移动的——在位置之间搬运信息的那个操作,正是它让 "the cat sat on the mat" 的含义不同于 "the mat sat on the cat"。
- 嵌入矩阵约占一个小模型参数量的三分之一——它不是免费的查找表。
- PCA 适合发现聚类,但它的距离会骗人,所以相似度要在完整的 hidden_dim 空间里算。
- 输入/输出嵌入绑定迫使一个矩阵身兼两职,这会约束其几何结构能编码的内容。
点击 Load embeddings,然后点一个动物,比如 'tiger'。再用输入框添加自定义词 'banana'。它的 top-5 最近邻列表偏向哪个已有类别?为什么?