Embedding 与语义空间

LLM 与 Prompt 基础入门3讲解 3

Embedding 把一段文本压成定长实数向量;「相近的两句话向量就近」不是因为模型理解了语义,而是对比学习把训练目标定义成了这样。所以「相似」不等于「相关」:一句话和它的否定形式可能很近,对称相似与非对称检索是两种任务,换 embedding 模型意味着全量重建。

也叫:Embedding · 嵌入 · 向量表示 · 余弦相似度 · cosine · 语义空间 · 对比学习

原理拆解出自 T0-4

「近」的定义写在训练数据里
这一问最容易答含糊:正确答案不是「因为模型理解了语义」

训练阶段
正样本对「怎么申请年假」配「年假申请流程说明」
负样本对配「打印机维修指南」
反复优化之后
编码器学到映射被判为相关 → 方向接近
检索阶段
在线算query 编码
离线算好,存进向量库文档编码
余弦相似度只看方向夹角,不看长度
Top-K
三个必须记住的已知失真
失真为什么会这样该由谁来接
否定不敏感整段话题一致,否定只是一个低权重信号结构化过滤,别指望向量
精确匹配被稀释年份、型号、金额、人名、错误码在稠密向量里没有专属维度这才是必须搭配 BM25 的根本原因
一段多义被平均一个 chunk 讲三件事,向量落在三件事的平均位置分块策略:chunk 讲一件事(T2-2)

所以「近」等于训练数据里被标成正样本的那种关系。理解这一点,开篇三个故障就全部自解:训练数据里几乎没有「一句话和它的否定形式应该被推远」这样的样本,模型自然没学会区分;也几乎没有「查询是关系、答案要跨两篇文档」这样的样本,空间里根本没有对应的点。

归一化之后,余弦相似度等价于点积 —— 这就是向量库通常要求你先归一化、再用内积检索的原因:更快,而且等价。

为什么语义相近的两句话向量就近?

这是本篇最容易答得含糊的一问。正确答案不是「因为模型理解了语义」,而是:

原文示意
训练阶段
  正样本对:("怎么申请年假", "年假申请流程说明")  → 损失函数把它们拉近
  负样本对:("怎么申请年假", "打印机维修指南")    → 损失函数把它们推远
                      │
                      ▼
        反复优化后,编码器学到一种映射:
        "训练数据里被判为相关的文本对" → 方向接近
                      │
检索阶段              ▼
  query ──► 编码 ──► 向量 ──┐
                            ├──► 余弦相似度 ──► Top-K
  文档 ──► 编码 ──► 向量 ──┘   (文档侧离线算好,存进向量库)

所以「近」的定义,等于训练数据里被标成正样本的那种关系。理解这一点,前面三个故障就全部自解了:训练数据里几乎没有「一句话和它的否定形式应该被推远」这样的样本,模型自然没学会区分;也几乎没有「查询是关系,答案要跨两篇文档」这样的样本,模型自然给不出。

import numpy as np

def cosine(a, b):
    a, b = np.asarray(a), np.asarray(b)
    return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))

# 归一化之后,余弦相似度等价于点积——
# 这就是为什么向量库通常要求你先归一化,然后用内积检索:更快,且等价。
def normalize(v):
    v = np.asarray(v)
    return v / np.linalg.norm(v)

三个必须记住的已知失真

  1. 否定不敏感。「能报销」和「不能报销」余弦相似度极高,因为整段话的话题一致,否定只是一个低权重信号。
  2. 精确匹配被稀释。年份、型号、金额、人名、错误码这类 token,在稠密向量里没有专属维度。这正是必须搭配 BM25 的根本原因,而不只是「工程上稳一点」。
  3. 一段多义会被平均掉。如果一个 chunk 同时讲了三件事,它的向量落在三件事的「平均位置」,结果是对哪一件都不够像。这条把 Embedding 和分块策略直接绑在一起(T2-2)。

以上节选自T0-4 Embedding 与语义空间:为什么「相似」不等于「相关」,读全文能看到前后语境。

延伸阅读

考这个知识点的题1

会连带问到2