news 2026/10/16 6:36:50

Embedding:机器是怎么“理解“语义的

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Embedding:机器是怎么“理解“语义的

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 那一整套技术要填的坑。


三条可以带走的结论

  1. 语义 = 统计结构的几何化。机器不懂意思,但它会数邻居,然后把"像不像"变成"近不近"。
  2. 相似度看夹角,不看距离。向量长度往往只反映词频,方向才承载语义。
  3. "Embedding"有两个意思:模型内部的输入层,和独立的向量模型。混着聊,必吵架。

《大模型概念地图》专栏 · 第 4 篇

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 6:36:28

PDF补丁丁 PDF 字体嵌入完整指南:方块字与缺字一次修好

PDF补丁丁 PDF 字体嵌入完整指南:方块字与缺字一次修好 【免费下载链接】PDFPatcher PDF补丁丁——PDF工具箱,可以编辑书签、剪裁旋转页面、解除限制、提取或合并文档,探查文档结构,提取图片、转成图片等等 项目地址: https://g…

作者头像 李华
网站建设 2026/10/16 6:35:41

i18n Ally Next:Monorepo中实现多语言嵌入

前言最近在打算看opencode源码,起因是在opencode桌面端设置了一个本地服务器之后,opencode起不来了😂,问了deepseek之后还是没有找到原因(最后找到原因了,跟我的服务器没关系,我的一个项目.open…

作者头像 李华
网站建设 2026/10/16 6:35:40

Rag评测-被测系统答题

上篇已经构建好问答集,可以说前半部分任务已经完成,现在执行下半部分任务了。那么下半部分首先需要执行的任务就是,用被测系统开始答题,先在models.py文件下写好被测系统的配置 System_llm_url写被测系统url def system_llm(quest…

作者头像 李华
网站建设 2026/10/16 6:35:08

FreeRTOS任务调度实战:从任务创建到状态迁移的完整解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 6:34:35

诺贝尔经济学奖回顾(1969—2026)

诺贝尔经济学奖回顾(1969—2026) 概述 瑞典中央银行纪念阿尔弗雷德诺贝尔经济学奖(The Sveriges Riksbank Prize in Economic Sciences in Memory of Alfred Nobel)自 1969 年首次颁发以来,截至 2026 年共颁发 58 次…

作者头像 李华