为什么 1024 个数字能装下一段话的语义
为什么再写一篇 Embedding
第 05 篇我们已经会用 embedding 做语义搜索了——调一下 API,向量进,相似度出。但很多人用着用着会卡在一个朴素的问题上:这些"向量"到底是怎么来的?为什么"猫"和"狗"在向量空间里就一定会靠近?是谁定义的"靠近"?又凭什么 1024 个数字就能塞进一段话的语义?
如果你只想做一个能跑的 demo,第 05 篇足够。但要让你在选模型、调维度、定阈值、debug 检索效果的时候心里有数,就得回到根子上:embedding 解决的本来是哪个问题,它是从一系列更朴素的尝试里怎么"长"出来的。这一篇我们从最原始的"如何让计算机理解类别"讲起。
类别数据的烦恼
机器学习模型的输入终究是数字。但生活里大量的特征是类别——城市名、商品种类、用户 ID、词。怎么把"北京"喂进一个神经网络?
最直接的办法叫 独热编码(one-hot encoding)。假设你是一个食谱推荐应用的工程师,菜单里有 5000 种菜:
- 第 1 种"宫保鸡丁" →
[1, 0, 0, ..., 0](5000 维,第 1 位是 1) - 第 2 种"披萨" →
[0, 1, 0, ..., 0] - 第 3 种"沙瓦玛" →
[0, 0, 1, ..., 0] - ...
每道菜对应一个 5000 维的向量,只有一位是 1,其余全是 0。这就是稀疏向量。
看起来挺干净,但只要打算把它喂进一个神经网络,问题就接踵而至:
- 参数爆炸:第一层如果有 256 个神经元,光是输入到第一层的权重矩阵就是 5000 × 256 = 128 万个参数。如果你的"类别"是英语词汇(10 万词级别),第一层就要 2500 万参数,整个网络很快不可训练
- 数据匮乏:参数多就要更多训练数据来约束,否则极易过拟合
- 存储和计算开销:每个样本都要带着一个绝大部分为零的长向量参与运算,纯属浪费
但这些都是工程层面的烦恼。更要命的是另一个语义层面的根本问题:任意两道菜之间的独热向量距离都是相同的。"宫保鸡丁"和"麻婆豆腐"的余弦相似度是 0,"宫保鸡丁"和"提拉米苏"的余弦相似度也是 0。一个川菜爱好者点过宫保鸡丁,你就没法用这个表示判断推麻婆豆腐和推提拉米苏哪个更靠谱——一切类别在 one-hot 空间里都是等距离的孤岛。
人类对菜的理解显然不是这样:宫保鸡丁和麻婆豆腐是"川菜兄弟",提拉米苏是"完全不同的甜点"。我们脑子里的"菜"似乎天然带着几个轴向——咸 / 甜、热 / 凉、菜系、主料。如果能让计算机也按几个有意义的轴去摆放这些菜,那同类的菜自然会靠近,距离就能反映相似度。
这就是 embedding 的核心动机。
用几个轴重新摆放:从 1D 到 3D 的直观
借用 Google 教程里的经典例子。我们手上有这几样食物:热狗、披萨、汉堡、沙拉、沙瓦玛、罗宋汤、苹果卷、汤圆。如果只能给每样食物分配一个数轴上的位置,要让"相似的食物挨得近",你会怎么放?
一个合理的尝试是按"三明治程度"排——左边是"标准三明治",右边是"完全不像三明治":
汉堡 ─ 热狗 ─ 沙瓦玛 ─── 披萨 ─── 沙拉 ─── 罗宋汤 ─── 苹果卷 ─── 汤圆
<- 越来越不像三明治
这一维已经能解释很多直觉:汉堡和热狗的距离非常近,汤圆和热狗的距离非常远。但显然信息丢得太多——苹果卷和汤圆并不"相似",它们只是同样"不像三明治"。
加一根轴——甜点性:
苹果卷 汤圆
● ●
甜
汉堡 热狗
● ● 披萨 罗宋汤
咸 ● ●
三明治 不像三明治
现在苹果卷和汤圆都在"甜"的一极,靠近了;罗宋汤和披萨都在"咸"的一极,与甜点拉开了距离。但是罗宋汤和披萨的相似度被高估了——前者是液态汤,后者是烤的。
再加第三根轴——液体性。罗宋汤往"液态"那一极拉,汤圆也带着"汤"的属性靠过去,但因为它在"甜"那一维已经远离罗宋汤了,所以三维空间里两者依然是合理的"近邻但不重合"。
到这里有几件事值得强调:
第一,每加一根轴,我们就是在多用一个数字描述每样食物。3 维向量看起来很少,但承载的信息已经远比 5000 维 one-hot 多——后者只能告诉你"这是第几个食物",前者告诉你"它在三个有意义的属性上长什么样"。
第二,同一组食物,如果你的目标变了,需要的轴也会变。如果你做的是"素食 / 非素食分类器",刚才的"三明治性"完全不重要,应该换成"含肉量"。embedding 永远是为某个任务服务的,没有放之四海而皆准的一组向量。
第三,真实的文本 embedding 不是 3 维,是几百到几千维,且每一维通常不可解释。你打开 OpenAI text-embedding-3-small 输出的 1536 个数字,没人能告诉你第 273 维代表什么——它是模型在大规模数据上自己"长"出来的特征轴,可能是"政治倾向 + 一点动物性 + 一点二战相关"的某种混合,对人类毫无意义,但对模型而言它是有效的内部坐标。
这件事为什么能成立:从离散到连续的跃迁
讲到这里得停下来想一个很容易被略过的问题:为什么"用向量代替类别"这件事行得通?
它行得通的真正原因,是把一个离散问题转化成了连续问题。
5000 道菜本质上是 5000 个孤立的符号,相互之间没有任何天生的"加减关系"——你没法说"宫保鸡丁 + 0.5 = 麻辣香锅"。但一旦把每道菜变成一个 32 维的实数向量,整个空间就有了几何结构:你可以求两点的距离、可以沿一个方向移动、可以做加权平均。
这一步看起来简单,影响是颠覆性的:所有依赖梯度下降、连续优化、距离度量的工具都瞬间变得可用。机器学习这一整套数学武器的前提就是"输入空间是连续的、损失函数是可导的",one-hot 满足不了这个前提(任意两个 one-hot 之间的"路径"上没有有意义的中间点),但 embedding 满足。
换句话说,embedding 不是为了节省存储,是为了把"类别"这种东西放进数学的世界——节省存储只是顺带的好处。
这些向量是怎么"长"出来的
要回答"机器怎么自己找到合适的那几个数字",最好的办法是看一遍这件事在历史上是怎么走过来的——它不是某个研究者灵光一现拍脑袋发明的,而是几代人在解决具体问题的过程中慢慢沉淀下来的范式。
最早的尝试是纯统计的:构建一个巨大的"词-上下文"共现矩阵,每个词配一行,统计它和别的词共同出现的次数,再做 SVD 降维。这条线叫 LSA(潜在语义分析),1990 年代就在用,思想已经很对头:"经常和一样的词一起出现的两个词,语义上多半相近。"但缺陷也很明显——它只用统计量,没有考虑词序,也没有任何"任务驱动"的信号让模型学得更精。
转折点是 2013 年的 Word2Vec。Mikolov 团队的洞察是:与其手工算共现矩阵,不如设计一个简单的预测任务——用一个词预测它左右几个词(skip-gram)或反过来(CBOW)——然后让一个浅层神经网络去学。训练完成后丢掉外层网络,保留中间那层 lookup table,每一行就是一个词的向量。这样训出来的向量出人意料地强:它不仅让相似的词靠近,还让"国王 - 男 + 女 ≈ 女王"这种关系在向量空间里成立。Word2Vec 真正把"embedding 由神经网络在解决具体任务时顺带产出"这个范式确立了下来。同一时期,推荐系统也在做相同的事——在"看了 A 菜的人会不会点 B 菜"这种监督任务里,让一个 5000×32 的权重矩阵自己长成"每行就是某道菜的 32 维 embedding",机制是一样的:模型为了把任务做好,被迫把数据里的语义结构编码进它的内部表示。
但 Word2Vec 还有个老问题——一个词只有一个向量。"苹果"无论指水果还是公司,拿到的都是同一个点。2018 年起,BERT、GPT 这一代基于 Transformer 的模型用 self-attention 把一个词的表示动态地拌上它周围词的信息:句子里的"苹果"在它出现的具体位置上,拿到一个专属的、被上下文调制过的向量。从静态 embedding 进化到上下文 embedding,是文本表示能力的又一次大跃迁。
现代的 sentence embedding 模型(像 bge-m3、OpenAI 的 text-embedding-3 系列)顺着这条路再走一步——它们在 Transformer 主干上加一个"把一段话所有 token 的向量池化成一个总结向量"的训练目标,然后用 对比学习(contrastive learning) 训练这个总结向量:让"语义相同的句子对"在向量空间里靠近、不相干的句子对远离。这个范式在工程上趋于成熟,是当前所有主流 embedding API 背后的共同方法。
理解了这条历史线,回头看 OpenAI 或 BAAI 那些 embedding 模型就不会觉得它们神秘——它们是 30 年来"如何把符号变向量"这件事的最新工程产物,本质思想从 LSA 起就没变过:让模型在解决一个具体任务的过程中,被迫把数据里隐藏的结构编码进它的内部表示。
一个值得停下来想的问题:训练的是 A 任务,为什么得到的 embedding 在 B 任务上也有用
这其实是 embedding 最神奇也最值得理解的地方。
Word2Vec 训练的目标是"用一个词预测它周围的词"——这看起来只是个语言任务,但训练出来的词向量丢进完全不同的下游任务(情感分类、机器翻译、命名实体识别)也都能 work。为什么?
朴素的回答是"因为模型学到了语义"。但严格讲,模型并不"知道"什么是语义,它只在追求把损失函数压低。真正的原因是:为了把"周围的词"预测准,模型不得不在内部把"会出现在相似上下文里的词"放到同一区域——而"出现在相似上下文里"几乎就是人类对"语义相近"的工作定义。语言学里管这叫分布假设(distributional hypothesis):"你只要看一个词的伙伴,就知道这个词是什么。"
把同一个逻辑搬到食物推荐:用户行为这个监督信号迫使模型把"被同一个用户喜欢"的菜放近,而能被同一批用户喜欢的菜,往往在口味、菜系、价格区间上确实存在系统性的共性。模型是借由解决一个具体任务,被动地发现了数据里那些恒定不变的结构——而那些结构对其他任务也往往是有用的。
这个洞察对实战非常重要:
- embedding 模型的好坏,本质上是"训练任务 + 训练数据"和"你的下游任务 + 你的数据"匹不匹配。在通用网页数据上训出来的 embedding,扔到金融研报检索上效果会打折扣,不是模型不好,是它见过的"上下文"和你的领域不对路
- 这也是为什么有"领域适配"这一说——同一个基座模型,在你自己的领域语料上再做一轮对比学习微调,往往比换一个更大的通用模型更有效
不只是点的位置:方向也带着含义
第 05 篇提过一句:"vec(国王) - vec(男) + vec(女) ≈ vec(女王)",当时只当作一个有趣的现象。但理解了"分布式表示"之后,这件事就不再神奇——它是必然。
这么想:如果一个 embedding 空间真的把"性别"这一属性沿某个方向编码了(不一定是某一根坐标轴,而是空间里的某条直线),那么"国王 → 女王"这个变换,在向量空间里就是沿这个"性别方向"做一次平移;同理"男 → 女"也是沿同一方向的平移。两个等价的平移当然给出等价的结果——国王 - 男 + 女 ≈ 女王 在数学上是恒等式。
这告诉我们一件深刻的事:embedding 空间里有意义的不只是"哪些点靠近",还有"沿哪个方向移动"。复数 / 单数、过去式 / 现在时、首都 / 国家,这类语义关系都被编码成了空间里的某些一致方向。这就是 embedding 比传统类别表示强大的根本原因——它同时编码了对象本身和对象之间的关系。
工程上这个现象有一些直接的应用:你可以用"我喜欢这个、不喜欢那个"两个向量的差,作为"用户偏好方向",去给其他物品打分。它本质上就是在利用 embedding 空间的方向语义。
工程上两个绕不开的细节:怎么比、要多长
核心思想理顺了,落到工程上还有两件小事——怎么比较两个向量、向量该多长——看起来琐碎,但选错了直接影响系统效果。
先说怎么比较。常见有三种方式:欧氏距离(直接的几何距离)、点积(同时反映方向和长度)、余弦相似度(先归一化,只比较夹角)。文本 embedding 几乎一律用余弦相似度,原因不是它"数学上更优",而是匹配训练时的目标——主流 embedding 模型用对比学习训练时关心的就是"语义方向",向量长度本身不带任务信息。一个高频词不应该因为模型给它分配了更长的向量就被判定为和谁都相似。归一化之后用点积等价于余弦相似度,前面 05 章见过的 normalize_embeddings=True 做的就是这件事——一句话的差别,但选错了的代价是召回排序整体偏向高频词。
再说该多长。直觉上加一根轴就能多塞一点信息,但维度并不是越高越好。维度太低会发生容量瓶颈——把 1000 万句话塞进 16 维向量,必然有大量语义不相干的句子被挤到同一区域,模型再聪明也救不回来。维度太高同样有反作用:存储和检索成本线性上升(1024 维向量库占的存储是 256 维的 4 倍);高维空间的"体积"指数增长,训练数据稀疏会让真正有意义的方向学不充分;更深一层是 维度灾难——在极高维空间里,几乎所有点对的距离都趋于相同,"近邻"的区分度反而变弱。
主流的 sentence embedding 模型选 768 / 1024 / 1536 这几个量级,是在质量、成本、训练难度之间长期妥协的结果。OpenAI text-embedding-3-large 提供的"手动降维"用的是一个叫 Matryoshka Representation Learning 的训练技巧——训练时让前 k 维就已经是一个可用的低维 embedding,于是用户可以根据预算自由截取,不用换模型。这是近年这个领域少数能让你"鱼和熊掌兼得"的训练 trick,遇到检索预算紧张的场景值得记住。
回到 RAG:embedding 在系统中扮演的角色
说了这么多理论,落到 RAG 这种应用里 embedding 实际承担什么?
整个 RAG 系统是这样运作的:你有一万份文档,先全部切片,每个切片用 embedding 模型编码成 1024 维向量,存进向量库;当用户提问"如何重置密码"时,先把问题也编码成 1024 维向量,去库里找余弦相似度最高的几个切片,把它们和问题一起塞进 LLM 的上下文,让 LLM 基于这些切片生成回答。
embedding 在这里扮演的是召回器——它不需要回答任何问题,只需要在海量切片里挑出"和问题相关的那一小撮"。LLM 才是负责"基于这些切片说人话"的那一环。
理解了这个分工你就会发现:
- embedding 模型选得不够好,召回阶段就漏掉了真正相关的文档,后面 LLM 再强也无米下锅
- 切片切得不够好(太长或太碎),embedding 表达不了切片的核心语义,召回效果同样会差
- 余弦相似度只是一个粗略的代理。切片可能在字面上和问题相似,但实际答案不在里面——这就是为什么生产级 RAG 通常会在 embedding 召回之上再加一层 reranker 模型做精排
第 06 篇 RAG 实战里我们会真正动手把这条链路搭起来。
检索效果不好时,沿着这条链往回查
有了前面所有的底子,做 RAG / 语义搜索碰到"明明该被找到的文档没被召回",可以按一条由近到远的链路去定位。这里给的不是一份 checklist,是一个排查的次序——大多数实战 bug 在前两步就能解决,不要一上来就怀疑模型本身。
最先要看的是切片。最常见也最容易忽视的问题就在这里。把一份 5000 字的 PDF 整篇喂给一个 512 token 上下文的 embedding 模型,超出部分会被静默截断;或者反过来按 50 字切片,每片只剩半句话。前一种情况下模型只看到了文档前面的导言,后一种情况下单个切片信息量太低、向量基本是噪声。最简单的诊断方式是用人眼随机抽几个切片读一下,问自己:这是不是一个"有信息量的独立段落"?
第二层是看 **query 和文档的"语言风格"**是否匹配。embedding 是按训练数据的分布学出来的。如果用户敲的是"咋办、咋整",文档是工整的"如何处理 X 异常",模型在训练时如果没见过太多这种"口语 ↔ 书面"配对,相似度就会被低估。这种情况要么先改写 query(让 LLM 把用户问题翻译成"标准问法"),要么用 HyDE(Hypothetical Document Embedding)——先让 LLM 假设性地生成一段"答案应该长什么样",再用这段假答案的向量去检索。前两层加起来能解决我见过的大多数 RAG 召回问题。
再往后是模型的训练域和你的领域是否对路。医疗、法律、金融、代码这些领域的术语和缩写,通用 embedding 见得不够。一个简单诊断方式:抽几对你认为"显然语义相同"的短语(比如医学里的"心梗"和"心肌梗死"),手动算一下余弦相似度。如果连这种业内常识对都拿不到 0.7 以上,说明模型根本没学透你的领域,得换领域适配过的模型,或者用自己的语料再做一轮对比学习微调。
第四层是阈值。很多人会想"超过 0.8 才算相关"——但不同 embedding 模型的相似度分布范围差别非常大,有的模型输出普遍在 0.4~0.7,有的在 0.7~0.95,直接套同一个阈值会大量漏召或误召。更稳的做法是用相对排名(取 top-k)而不是绝对阈值;如果非要阈值,在自己的数据上先跑一次小评测把它标定出来。
只有当上面四层都查过、确实是"模型已经召回不了真正相关的文档",才轮到最后一招——上 reranker。embedding 是双塔模型,query 和文档分别过模型再算相似度,这种"先各自浓缩成向量再比较"的方式天然会丢失精细信息。reranker 是 cross-encoder,把 query 和文档拼在一起过一次模型,输出"它俩相关吗"的分数。reranker 准但慢,所以工业实践是 embedding 召回 top 100、reranker 精排到 top 10,两层各司其职。生产级 RAG 系统几乎都是这样组的。
这条排查链有个一以贯之的潜台词:不要把"embedding 模型不行"当成第一直觉。多数时候问题在它前后的环节里——切片、query 改写、领域不匹配、阈值——而不是模型本身。
Embedding 也有它看不见的盲区
讲了一路它的强大,最后必须诚实地说一件事——embedding 不是万能的相似度工具。它擅长 capture 的,是它训练目标里被定义为"相似"的那种关系,而那种关系不一定是你脑子里的"相似"。
举个具体例子:通用 embedding 模型看到"我喜欢这件红色的裙子"和"我妻子喜欢那件蓝色的裙子",会判定它们高度相似——同样讲衣服喜好。但在一个推荐场景里,你要捕捉的可能是"用户的具体偏好",颜色、款式的差异恰恰是关键信息,embedding 在这个维度上是"看不见"的。
类似的盲区还有几类常见的:精确数字("1234"和"1235"在文本 embedding 里几乎一样)、时间先后("先做 A 再做 B"和"先做 B 再做 A"语义截然相反但向量很近)、否定("我喜欢猫"和"我不喜欢猫"在很多 embedding 里相似度依然很高,因为大部分词都一样)。这些都需要在系统层面用别的手段补上——把数字提到结构化字段里精确比对、在 prompt 里显式让 LLM 校验否定与时序、检索时叠一层关键词过滤。
记住这一点你就不会过度依赖 embedding:它是一个强但不全能的语义代理,给你的系统画了一条"语义粗筛"的边界,剩下的精细判断要交给后续模块(reranker、规则、LLM 本身)去做。一个好的 RAG 系统不是"embedding 模型选得好就万事大吉",而是知道 embedding 的能力边界在哪,并且在边界之外用对工具补齐。
把视野拉远:Embedding 是现代 AI 看世界的方式
回到开头的那个问题——为什么 1024 个数字能装下一段话的语义?
答案是:不止一段话。文字、图像、音频、视频,所有人类创造的内容,在现代 AI 系统里最终都被翻译成同一种东西——一个高维实数向量。GPT-4V 看一张图,是把图切成 patch、每个 patch embedding 成向量再过 attention;Whisper 听一段音频,是先切成 spectrogram patch 再 embedding;多模态模型让"一张猫的图片"和"一只猫"这段文字在同一个向量空间里靠近,本质就是让两套 embedding 学会对齐。
embedding 不是 RAG 的某个零件,它是整个现代 AI 能跨任务、跨模态、能做迁移学习的共同地基。所有把现实世界塞进神经网络的努力,最后都变成"把这个东西编码成一个向量"。这个范式从 1990 年代的 LSA 萌芽,2013 年因 Word2Vec 而被广泛意识到,2018 年随 Transformer 进入新阶段,到今天的多模态大模型仍在延续——它已经不再是某个具体技术,而是深度学习处理一切非数值信号的通用接口。
理解了这件事,你就不只是理解了 embedding——你理解了过去十年深度学习为什么会变成今天这个样子:不是因为某一个模型架构特别厉害,而是因为整个领域学会了用统一的向量语言去描述世界,于是文本、图像、声音、行为这些原本风马牛不相及的信号,第一次有了在同一个数学空间里彼此对话的可能。
回到第 05 篇里那一行 model.encode(...)——它现在对你应该是另一种东西:一行简短的代码,背后是 30 年从 LSA 到 Word2Vec 到 Transformer 的演化、是把"类别"从离散世界搬进连续世界的根本性思想跃迁、是整个现代 AI 系统赖以运转的核心范式。
下一篇 RAG 实战里,我们就把这套底子真正落地成代码。
参考资料
- Google ML Crash Course: Embeddings — 本篇主要参考
- Word2Vec 原始论文:Efficient Estimation of Word Representations in Vector Space
- Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks
- Matryoshka Representation Learning — 多维度 embedding 的训练技巧
- The Illustrated Word2vec (Jay Alammar) — 图示直觉极佳
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:番外 9:Embedding 的来龙去脉
本文链接:https://www.sshipanoo.com/blog/ai/ai-for-python/番外09-Embedding来龙去脉/
本文最后一次更新为 天前,文章中的某些内容可能已过时!