Embedding:机器是怎么"理解"语义的
这一篇回答一个听起来很玄的问题:机器凭什么知道"苹果"和"香蕉"比"苹果"和"汽车"更像?答案不玄,甚至有点朴素——它把"像不像"换算成了"远不远",然后交给几何去算。
本文的关系图:
文字 "小猫" ↓ Token 化 Token ID [23391] ↓ ★ Embedding:查表,把编号换成一串数字 ★ 向量 [0, 0, 1, 1, 0, 2, 1, ...] ← 本篇讲的就是这一步 ↓ 送进 Transformer(注意力层在这里让向量随上下文变化) 上下文相关的表示 ↓ "语义"这件事,就是在这个空间里被几何化的一、先说那个流传很广的"魔法"
你可能见过下面这个例子:
国王 - 男人 + 女人 ≈ 王后词还能做加减法?这看起来像什么黑魔法。
其实一点不神秘。前提是:每个词都已经变成了空间里的一个坐标点。一旦有了坐标,加减法就是普通的向量运算——跟"从北京往东走 100 公里到天津"是同一种算术。
而 Embedding 干的事,就是给每个词(或者一段文本、一张图、一个用户)在地图上钉一个坐标点。
钉完之后,两件事就自动成立了:
- 像不像 → 变成 近不近,可以用距离算;
- 语义关系 → 变成 方向,可以用向量的差算。
"国王"和"男人"的差,大致等于"王后"和"女人"的差——都是"性别"这一个方向上的位移。所以把那一段位移挪到"女人"身上,就落到"王后"附近了。
补一句诚实话:这个例子是经典的理想化演示,真实模型里它并不总是成立,很多词算出来会飘。但它说明的原理是对的:语义关系确实被编码成了空间里的方向。
二、为什么非得这么做
回想上一层的处境:分词之后,每个 Token 换来一个整数编号。
编号这个东西,最大的问题是它只有顺序,没有距离。
23391 和 62028,谁大谁小?没有任何意义。把"苹果"的编号加上"香蕉"的编号?更是胡闹。编号就是个身份证号,它能告诉你"这两个是不同的东西",但绝不告诉你"这两个有多像"。
而"像不像"恰恰是语义的全部。
所以必须换一种表示法:把身份证号换成坐标。坐标可以算距离,可以算方向,可以算平均值——这才让"语义"这件事变得可计算。
这一步是整栋楼的地基。后面所有关于"理解"的讨论,追到底都是在这一层。
三、一个更好用的类比:社交圈
"高维坐标系"这个说法太抽象了。换个说法:
一个词是什么含义,看它平时跟谁混。
“咖啡"这个词,你总在"喝”“早上”“提神”“星巴克"旁边见到它。而"螺丝刀"总在"拧”“五金”"维修"旁边出现。两个词的社交圈几乎没有交集——所以它们不像。
这就是 NLP 里最重要的一条假设,叫分布假设:一个词的含义,由它的上下文决定。
人也是这么认识生僻词的。你第一次见到"糗"字不知道念什么,但看到它出现在"糗事""出糗"里,就大致猜出这不是什么好词。你用的就是这个方法。
而机器用的方法,和你一模一样,只是它数得比你认真。
四、那就现场数一遍给你看
下面这段代码不用任何模型、不下载任何东西,只用 Python 标准库,从一段小语料里硬"数"出词向量来。
它做的事,就是第三节那个社交圈类比的字面实现:一个词的向量 = 它跟词表里每个词一起出现过多少次。
"""语义是从「邻居」里数出来的:不用任何模型,纯统计造出词向量。"""fromcollectionsimportCounterfrommathimportsqrt# 一段迷你语料。故意分成两族:宠物族 和 车辆族CORPUS=["小猫 喜欢 吃 鱼","小狗 喜欢 吃 肉","小猫 喜欢 喝 牛奶","小狗 喜欢 啃 骨头","小猫 和 小狗 都 是 宠物","汽车 需要 汽油 才能 跑","汽车 需要 定期 保养","卡车 需要 柴油 才能 跑",]WINDOW=2# 只看前后 2 个词defbuild_context_counts(corpus,window):"""统计「谁和谁一起出现」,得到共现计数表。"""counts=Counter()forlineincorpus:tokens=line.split()fori,wordinenumerate(tokens):lo,hi=max(0,i-window),min(len(tokens),i+window+1)forjinrange(lo,hi):ifi!=j:counts[(word,tokens[j])]+=1returncountsdefvector_of(word,counts,vocabulary):"""一个词的向量 = 它和词表里每个词共现了多少次。"""return[counts[(word,c)]forcinvocabulary]defcosine(a,b):"""余弦相似度:只看向量方向是否一致,不受长度影响。"""na=sqrt(sum(x*xforxina))nb=sqrt(sum(x*xforxinb))ifna==0ornb==0:return0.0returnsum(x*yforx,yinzip(a,b))/(na*nb)counts=build_context_counts(CORPUS,WINDOW)vocab=sorted({tforlineinCORPUSfortinline.split()})SHOW=["小猫","小狗","鱼","汽车","卡车","汽油"]W=8defdisp_width(s):"""显示宽度:中日韩字符占 2 格,用来对齐表格。"""returnsum(2iford(c)>0x2E80else1forcins)defpad(s,n,right=False):fill=" "*max(0,n-disp_width(s))returnfill+sifrightelses+fillprint(f"词表共{len(vocab)}个词,所以每个词被表示成{len(vocab)}维向量")print(f" 小猫 →{vector_of('小猫',counts,vocab)}")print()print("两两之间的余弦相似度:")print(pad("",W)+"".join(pad(w,W,True)forwinSHOW))forwinSHOW:vw=vector_of(w,counts,vocab)print(pad(w,W)+"".join(pad(f"{cosine(vw,vector_of(o,counts,vocab)):.2f}",W,True)foroinSHOW))真实运行结果(本机实测,完整脚本见code/04-embedding-demo.py):
词表共 23 个词,所以每个词被表示成 23 维向量 小猫 → [0, 0, 1, 1, 0, 2, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 两两之间的余弦相似度: 小猫 小狗 鱼 汽车 卡车 汽油 小猫 1.00 0.67 0.75 0.00 0.00 0.00 小狗 0.67 1.00 0.67 0.00 0.00 0.00 鱼 0.75 0.67 1.00 0.00 0.00 0.00 汽车 0.00 0.00 0.00 1.00 0.58 0.41 卡车 0.00 0.00 0.00 0.58 1.00 0.35 汽油 0.00 0.00 0.00 0.41 0.35 1.00请盯着这张表看十秒。
左上角 3×3 那一块(小猫、小狗、鱼):0.67~0.75,宠物家族互相抱团。
右下角 3×3 那一块(汽车、卡车、汽油):0.35~0.58,车辆家族自成一体。
两个块之间:清一色的0.00——宠物和汽车没有任何共同语言。
没有任何一行代码告诉程序"小猫是动物"“汽车是交通工具”。它只是老老实实数了谁跟谁一起出现,语义就自己浮出来了。
这就是"理解语义"的真相:不是模型懂了什么,而是它把统计结构摆进了一个可以用几何运算的空间。
顺便看一个有意思的瑕疵:跟"小猫"最像的词排出来是鱼、肉、牛奶——全是 0.75。"肉"明明是狗吃的,怎么也排进来了?因为在这段语料里,"肉"和"鱼"占的是同一个语法位置(喜欢 吃 ___),共现模式一模一样。它学到了"位置相同",但没学到"猫吃鱼、狗吃肉"。这一点后面讲误区时还会用到。
五、三个反直觉的性质
性质一:相似度看的是夹角,不是距离
算相似度最常用的方法是余弦相似度——比较两个向量的夹角。
为什么要看夹角,不直接看直线距离?因为向量的长度往往和语义无关,只和"出现频率"有关。一个词出现得多,向量就长。如果比直线距离,高频词会平白无故离所有人更远。
比夹角就没这个问题:方向一致就是像,跟长短无关。
性质二:语义关系变成了方向
这就是开头国王 - 男人 + 女人 ≈ 王后的原理。想一下:
- "男人"到"国王"的位移,是"地位"这个方向;
- 把同样的位移加到"女人"身上,就得到"女王/王后"。
“性别”“时态”“单复数”"国家-首都"这类关系,在好一点的向量空间里,都会表现成稳定的方向。这不是设计出来的,是训练中自然涌现的。
性质三:维度高到人类无法想象
真实向量通常有768、1024、1536、3072甚至更多维。而关键在于:
每一个维度都没有人类能读懂的名字。
你没法指着第 137 维说"这就是’性别’维度"。语义是分布在整个空间里的,不是一格一格分好的。
这带来一个很实际的后果:Embedding 这东西,你能用,但你别指望能"看懂"它。调试它得靠实验数据,不能靠直觉。
六、最容易混的一件事:Embedding 有两个完全不同的意思
这是本篇最想讲清的一点。业内说"Embedding",指的可能是两个毫不相干的东西:
| 输入 Embedding(Embedding 层) | Embedding 模型(向量模型) | |
|---|---|---|
| 它是什么 | 模型内部的一层,一张查表 | 一个独立的完整模型 |
| 输入 | 一个 Token ID | 一整段文本 |
| 输出 | 一个 Token 的向量 | 整段文本的一个向量 |
| 怎么得到 | 查表,一步到位 | 跑完整个网络才出来 |
| 谁用 | 大模型自己用 | 检索、聚类、去重、分类 |
| 典型例子 | Qwen 的 151643 × 896 那张表 | text-embedding-3、BGE、M3E |
| 你要操心吗 | 基本不用,它是模型的零件 | 要,选型不当直接影响效果 |
第一类:输入 Embedding。它就是大模型的第一层,形状是"词表大小 × 隐藏维度"。作用是把 Token ID 查表变成向量,喂给后面的 Transformer。它跟模型一起训练出来,是模型的一部分,你平时根本不会单独碰它。
第二类:Embedding 模型。这是一个独立的小模型(本身通常也是个 Transformer)。你给它一段文本,它回你一个向量——整段话的"语义指纹"。做检索、去重、聚类都是用它。
为什么必须分清?因为最常见的困惑是:
“Embedding 到底是 896 维还是 1536 维?”
答案取决于你在说哪个——896 是那个大模型的输入层维度,1536 是某个 Embedding 模型的输出维度。两个数说的根本不是一件事。
再补一个关键差别:输入 Embedding 是静态的,一个 Token ID 永远对应同一个初始向量。那为什么模型还能理解"苹果发布了新手机"里的"苹果"是公司?
因为真正的"理解"发生在下一层:这些静态向量进入注意力层之后,会根据邻居被动态改写。输入 Embedding 提供的是起点,注意力层才把它变成"上下文相关的表示"。
所以准确的说法是:这一层负责"把文字变成可计算的几何对象",真正赋予上下文含义的是再上一层。
七、从"一个词一个向量"到"看人下菜碟"
Embedding 的技术史,其实就是一句话:从静态走向动态。
第一代:静态词向量(Word2Vec、GloVe)
思路就是本文第四节演示的那套——统计共现,每个词得到一个固定的向量。Word2Vec 有两个经典训练方式:用上下文猜中心词(CBOW),或用中心词猜上下文(Skip-gram)。
它有个硬伤:“苹果"只有一个向量。可"苹果发布了新手机"和"苹果很甜”,这两个"苹果"压根不是一回事。一个词一张脸,遇到多义词就抓瞎。
第二代:上下文相关
从 ELMo 开始,到 BERT,再到现在的大模型——同一个词在不同句子里,最终得到的表示不一样。"苹果"在两种句子里会自动漂向不同的位置。
所以今天你听到"Embedding"时,要先问一句:指的是那个静态的输入层,还是那个能给整段文本算指纹的独立模型?这两个问题的答案,技术内涵完全不同。
八、四个常见误区
误区一:以为 Embedding 在"理解"语义。
它没有理解任何东西。它做的是把统计结构投影成几何结构,让"像不像"变得可计算。说它"懂"是一种方便的拟人化,但一旦当真,后面全都会想歪。
误区二:以为能把向量"读"出来。
经常有人问"这个维度代表什么"。答案是:没有哪个维度单独代表什么。语义分散在整个空间里,人类可读的解释基本不存在。
误区三:以为"意思相近"就是"适合检索"。
不完全是。检索场景下,你要的是任务相关的相似——查"如何退款",理想结果里有"申请退货流程",这两个在通用语义上未必最接近,但在任务上必须匹配。所以 Embedding 模型要按场景选,不能只看排行榜总分。
误区四:把两个 Embedding 混着聊。
第六节那张表建议再看一遍。把"输入层维度"和"Embedding 模型输出维度"拿来比较,是这类讨论里最高频的鸡同鸭讲。
九、把这件事放回地图上
把大模型的概念按依赖关系从下往上摞成几层——最底下是算力和数据,往上依次是文字的表示、模型结构、训练、调用,最顶上才是增强和智能体:
L6 智能体层 Agent / Function Calling / ReAct / 记忆 / MCP L5 增强层 RAG / 向量检索 / Rerank / GraphRAG / 微调 L4 调用层 Prompt / Few-shot / CoT / 温度 / Top-p L3 训练层 预训练 / SFT / RLHF / DPO / LoRA / Scaling Law L2 结构层 Transformer / 注意力 / 参数量 / 上下文窗口 L1 表示层 Token / 分词 / 词表 / ★ Embedding ★ ← 本篇在这里 L0 底座 算力 / 数据 / 成本它和 Token 同属最底层的表示层,但两者分工不同:
- Token 负责"切"——把文字拆成模型能处理的单位;
- Embedding 负责"摆"——把每个单位摆进一个可以算距离的空间。
先切,再摆,然后几何开始工作。后面所有的"理解"“推理”“检索”,都建立在这两步之上。
而这篇留下的那个窟窿——向量又多又高维,怎么快速找出最像的那几个——正是向量数据库和 RAG 那一整套技术要填的坑。
三条可以带走的结论
- 语义 = 统计结构的几何化。机器不懂意思,但它会数邻居,然后把"像不像"变成"近不近"。
- 相似度看夹角,不看距离。向量长度往往只反映词频,方向才承载语义。
- "Embedding"有两个意思:模型内部的输入层,和独立的向量模型。混着聊,必吵架。
《大模型概念地图》专栏 · 第 4 篇