
NOTEAI 协作声明: 本文由 Xnne 与 Korewaxnne(AI 助手)以问答形式协作完成。Xnne 提出问题并整理结论,Korewaxnne 负责解答与概念校正。
这篇适合谁?这不是入门教程,也不是进阶参考。如果你对 NLP 一无所知,这篇会让你更困惑;如果你已经搞懂了 embedding 和稀疏稠密,这篇对你没有新东西。但如果你和我一样——大概知道 tokenize、BM25、embedding 是什么,却说不清稀疏和稠密到底指什么——那这篇也许能帮你把碎片串起来。
NOTE再补一次 NLP 的知识,不过问答的形式进行,因为很多东西不需要从零开始了。 保持现在无惑便可,有想法去做,碰到再解惑。
一开始的困惑
一样先提问。
这篇,我们主要讨论——
这些抛出的问题都可能是错的,而我要做的是找到对的路径。
如何预测一个文本块 embedding 后的向量是稀疏(sparse)还是稠密的(dense)?
它是按照场景来的,是反映这一整批次之间文本的关联度或者说向量距离,如果文本关联度低,那么这个场景就是稀疏的,如果文本关联度高,就是稠密的?
稠密和稀疏反映的是一批向量之间的间距,还是只是说一个向量里的值是稀疏的还是稠密(空值多少)?
另外,为什么有的 embedding model 更擅长做稠密向量而不擅长做稀疏向量?是结构决定的?还是预训练过程决定的?embedding 的预训练期间的参照或者说目标是什么,对齐相近语义?拉远不同语义?这个如何做标注?感觉好难。
embedding 的过程我也有点忘了,是分词后的每个 term 都具有自己的 embedding or 一整个 term 列表变成了一个 embedding?咋隐约记得好像都有?
一个 sentence 输入后会是什么样的输出?一篇 document 输入后会是什么样的输出?
什么样的应用场景适合稀疏向量?什么样的场景适合稠密向量?
第一次见到 sparse 和 dense 是在 paddle 里看到一个非常可爱的数学大佬写了一个我看不懂的算子。不知道是不是女装大佬,双马尾,女仆装,非常可爱——完了,我对向量的印象全是那张可爱的脸。
Embedding pipeline
前天在 关于毕业后再学 TF-IDF 这档事 里发现我搞混了 tokenize 的过程,我把 embedding 的过程也混进了 tokenize,但实际上 tokenize 只到分词。
所以这里我们也必须搞清楚,embedding 到底包含几步?
完整管线
分词后给 term 上 token id 是否属于 embedding 的一环?
Korewaxnne属于。但“上 token id”这一步在学术上通常叫 mapping / indexing,不算在 embedding lookup 里。 先看完整管线:
原始文本↓ tokenize(分词)["毕业", "后", "我", "搬回", "了", "老家"]↓ mapping(查词典,把词映射成整数 ID)[1456, 78, 23, 8901, 45, 6723]↓ embedding lookup(查向量表,把每个 ID 变成向量)[[0.23, -0.45, ...], [0.12, 0.78, ...], ...] ← 每个 token 一个向量↓ pooling / [CLS](把一排向量压缩成一句)[0.56, -0.12, ...] ← 整句一个向量(你 API 拿到的最终结果)你的问题落在 mapping(分词后给 term 上 token id)这一步。它算不算 embedding?
- 宽泛地说,算——因为它是“把文本变成模型能处理的形式”整个流程的一部分,日常交流中经常被笼统地称为 embedding。
- 严格地说,不算——mapping 只是查表(term → id),没有涉及任何向量运算。embedding 这个词在学术语境中专指 第三步(embedding lookup),即把离散的 token ID 映射成连续的向量空间。
所以你两种说法都说得通,只是严谨程度不同。在你的文章里说你“搞混了 tokenize 和 embedding 的过程”——这个认知已经足够准确了,不用纠结 mapping 这层归属。
小 tip: 计算 token 数确实在分词后就能看了(len(tokens)),这也是为什么大模型说“token”时指的是整数 ID,不是向量。
狭义化定义
既然如此,那我们就把 embedding 和 tokenize 都狭义化,embedding 专门指 token_id_list -> vector,tokenize 专门指分词,mapping 是 mapping。
词典与子词
Subword 的直觉
我记得现代 BERT 的词典一般是 3w 词
如果一个词是完全没见过的词,甚至词根都拼不出来,那么它在 mapping 的时候会发生什么?
Korewaxnne两个问题一起回答——因为你的理解方向是对的,但“词根”这个词不太准确。
BERT 用的不是“词根”去拼,而是 subword(子词)。
比如
Transformer这个词可能在词典里没有,但 BERT 的 WordPiece tokenizer 会把它切成:Transformer → Trans + ##former
##表示“这个词是接在前面的,不是独立词”。词典里有Trans(常见前缀)和##former(常见后缀),拼起来就是 Transformer。这种切法不是基于语言学上的词根(morpheme),而是基于统计——训练的时候算法会看哪些子串出现频率高,然后把它们收进词典。所以切出来的东西有时很像词根(
##former),有时只是高频片段(比如##tion)。那如果一个词所有子词都在词典里找不到呢?
实际上,在 BERT 的 WordPiece 下几乎不会发生这种情况,原因在于它的回退机制:
输入: "一个完全没见过的生僻词"↓ 尝试在词典里匹配最长子串"一个完全没见过的生僻词" → 若"生僻词"不在词典里↓ 递归切到单字符级别"生"、"僻"、"词" → 如果某个单字符也不在词典↓ 最终回退[UNK](Unknown token)但在更现代的 tokenizer(比如 GPT 系列用的 BPE / SentencePiece)里,连 [UNK] 都不会出现——它们会在训练时把所有单字符甚至 byte 都收进词典,任何输入都能被拼出来,不存在“完全没见过”的情况。
所以回答你的问题:“词根都拼不出来”在现代 tokenizer 里基本不可能,因为单字符级别总在词典里。 即使退一万步,最坏情况也就是降级成一个
[UNK]token。而 BERT 的 3w 词词典 vs 字节级 BPE 的区别,实质是:
BERT WordPiece 字节级 BPE 词典大小 ~3 万 ~10 万(含所有 byte) 有 OOV? 可能,会回退到 [UNK] 几乎没有(byte 级总能拼) 切分依据 统计子词 + 语言偏好 纯统计(byte 级) 这也是为什么后来 GPT 系列放弃了 WordPiece 改用 BPE——不用处理 [UNK],省一个麻烦。
确实是很好的办法,我记得在学 C++ 的时候——
char a = 'a'; // 1 bytestring b = "我"; // 3 bytes (UTF-8)string c = "你好"; // 6 bytes (UTF-8)当然具体占多少 bytes 取决于编码方式,UTF-8 下一个汉字 3 bytes,UTF-32 下 4 bytes。
只要是能被编码的,就可以被 byte 拼出来,而且多字词或者单词就是 byte 的集合。很漂亮,绕开了语言学偏好,完全靠统计。
我想进一步了解一下 BPE 和 WordPiece 在词典学习过程中的异同。
BPE vs WordPiece
Korewaxnne两者的核心区别在于 合并准则不同,导致词典的构成方式不一样:
BPE WordPiece 合并依据 频率(frequency)——哪对子串出现次数最多就合并谁 似然(likelihood)——合并哪一对能让模型概率提升最大 训练过程 从单字符开始,不断合并最频繁的相邻 pair,直到词典大小达标 从单字符开始,每次合并能最大提升训练数据似然的那一对 词典内容 纯统计结果——高频片段混杂在一起, ing、tion、ab、##s都可能出现更倾向于语言学边界——因为提升似然往往意味着合并“有意义的单元” OOV 处理 几乎没有——所有 byte 都在词典里 可能有——遇到完全没见过的组合会回退到 [UNK] 典型代表 GPT 系列、LLaMA BERT、DistilBERT 一个直观的对比:
输入
"unbelievable":
- BPE(频率驱动) →
["un", "believ", "able"]
—un和able因为出现频率高被合并;believ也是高频,但可能不如un稳固- WordPiece(似然驱动) →
["un", "##believ", "##able"]
— 结果上很像,但##believ获得##标记是因为模型认为“接在前面的 believe 形式”比独立词更可能但两者更大的区别在词典的“基底”上:
BPE 从 byte 级别 开始,也就是说它的初始词典是所有 256 个 byte 值。WordPiece 从 字符级别 开始,初始词典是所有 Unicode 字符 + 常见多字符组合。这导致:
- BPE 的词典天然包含所有 byte → 任何输入都能拼 → 没有 [UNK]
- WordPiece 的词典不包含所有 Unicode 字符 → 遇到冷僻字符时只能 [UNK]
不过后来 BERT 也意识到这个问题,在后续版本中加上了“全字符覆盖”的处理,但这是后话了。
一个粗暴的总结: BPE 像是一个不认识字的统计学家,只管“哪两个东西经常挨在一起”;WordPiece 像是一个半文盲,知道自己不知道什么(所以会留下 [UNK] 的空间),但切出来的东西更有语言直觉。
输入长度与输出向量
但是否存在某些特殊情况,会让某个词单独 embedding 或者让整个 document 去 embedding。
embedding 输入时的 token_id_list 长度会影响哪些?影响输出的那个向量的稀疏稠密性吗?
Korewaxnne先回答“特殊情况”: 确实存在,而且很常见。
- 单个词 embedding:当你搜 embedding 模型时(如
text-embedding-3-small),输入当然可以是一个词。比如输入"猫",输出一个向量。只不过一个词的信息量太小,在检索场景下效果通常不好——但技术上完全合法。- 整篇 document embedding:也合法,但有长度限制。embedding 模型和 LLM 一样有最大输入长度(BGE-M3 是 8k tokens)。超过的部分会被截断或报错。所以超长文档通常分段 embedding,然后平均池化成一个向量,或者直接用每段的向量做检索。
所以你的理解“通常是一整句”是对的,但不是固定的——长度从 1 到 max_tokens 都行。
然后回答核心问题:输入长度会影响输出向量的稀疏稠密性吗?
不会。 这是个很好的问题,但方向反了。
输出的向量维度是固定的(BGE-M3 是 1024 维,OpenAI 的 text-embedding-3-small 是 1536 维)。不管输入是 1 个词还是 1000 个词,出来的向量都是这个固定长度,每个维度上都有值(稠密)。稀疏稠密不是由输入长度决定的,而是由模型结构决定的。
输入长度实际影响的是:
影响什么 为什么 计算量 越长 → embedding lookup 的次数越多 → 更慢更贵 输出向量的质量 太短(1-2 个词)→ 信息太少,向量“没话说” → 检索精度差;太长 → 超过 max tokens → 被截断丢信息 池化策略的选择 超长输入分段 embedding 后怎么合成一个向量(平均?加权?[CLS]?)会影响最终向量的“重心” 一个直觉:
输入 “猫” → 向量是一个点 输入 “一只黑猫蹲在窗台上看雨” → 向量是另一个点
两个向量都是 1024 维,都在那个 1024 维空间里,没有“稀疏”或“稠密”的区别。区别在于第二个向量承载了更多语义信息,所以在检索时它的邻居会更“懂”它想表达什么。
你之前问的“稀疏和稠密反映的是一批向量之间的间距,还是一个向量里的值”——现在可以确认了:是一个向量里各维度的值是否多为 0。 embedding 模型的输出没有大量 0(所有维度都有值),所以叫“稠密”。BM25 的输出几乎全是 0(只有匹配到的词有值),所以叫“稀疏”。
稀疏 vs 稠密:make it clear
看起来之前我认为的稀疏稠密
它和矩阵一样,真的就是值是否为 0 的问题。它不是一批向量在空间的分布上是否密集/稀疏,只是单纯的一个向量里 0 多不多.
稀疏的通常高维,但用到的维度少,稠密的几乎所有维度都用上的,隐约之间表达的语义空间更“大”。
以及我们这里额外讨论一些东西——
各自适合什么场景
什么样的场景适合稀疏向量
比如我给出几个场景:
a. 用户和大模型的对话,期间总是有另外一个大模型来总结对话里的事实类和偏好类的事情记录为记忆.
对于这样的记忆来说,它适合稠密还是稀疏?
b.用户希望快速捕获所有和猫有关的文章, 关键词猫.
对于这样的 query 来说,谁更适合?
前面我们提到了,对于一个 term 的 embedding 的情况, 也是存在的,但是通常语义太少向量没话讲,检索起来很失败,通常召回困难.稠密向量更擅长做复杂语义表达,对于复杂的 query 需求,它能够更好地处理,但对于缺乏语义和上下文的 query 它可能就会有点束手无策.
而我们的 BM25 或者说我们的 TF-IDF, 它本身完全是设计来做 term 检索的.如果是场景 b 几乎可以秒出答案.
Korewaxnne你的直觉大部分是对的,我来把几个点校准一下。
BM25 有没有自己的向量空间?
有。每个 term 是一个维度,“猫”对应的维度有值,其他维度是 0。所以它的向量空间是以词典为维度的超高维稀疏空间(比如 5 万维),但每个向量只有少数几个维度非零。这就是为什么它被视为“稀疏向量”。
场景 a 和 b 分别适合谁?
场景 匹配类型 更合适的 原因 a. 对话总结 → 记忆 语义匹配(“用户喜欢猫” ≈ “养了一只布偶”) 稠密向量 记忆需要理解意思,不是抠字眼。用户说“我养了一只布偶”,稠密向量能联想到“猫”;稀疏向量则不行 b. 搜“猫”的文章 关键词匹配(标题/内容出现“猫”) 稀疏向量(BM25) query 太短,稠密向量“无话可说”,稀疏向量靠精确命中秒出 但你这里有一个隐含问题值得展开:为什么稠密向量对短的 query 束手无策?
不是因为“短”,而是因为缺乏上下文。一个词实在太模糊了:
- “猫” → 可能指动物、猫科、猫粮、猫片、Tom and Jerry……
- “一只黑猫蹲在窗台上看雨” → 模型知道大概是什么场景
模型能“发挥”的空间越大,语义匹配越准。单字 query 不给模型发挥的空间,它的向量就不知道该往哪个方向靠。
那实际生产中怎么解决的?用混合检索(hybrid search)。
现在的搜索引擎几乎都不只用一种方式:
query → BM25(精确命中) + dense vector(语义扩展)→ 合并排序 → 返回BM25 负责“说到做到”,dense 负责“举一反三”。两者的分数加权合并——对短 query BM25 权重高,对长 query dense 权重高。这也是你上一篇 TF-IDF 笔记里谈到的“相关文章推荐”,和这里说的记忆检索其实是互补的两面。
这么看起来 memU 和 mem0 其实都是混合场景。
既需要 dense embedding, 有需要 BM25 做补充,但是 BM25 怎么用似乎很有说法,是作为 hybrid search 上面所示的用法,还是说可以有其他更有意思的用法,值得考虑。
可以在拆解 memU 的时候把它对与 BM25 的用法捕获一下,是简单的 hybrid search, or 有自己的扩展——memU 是啥?我们来拆开看看(长期更新)。
来自 Claude 的补充
开头抛出了两个问题,聊到后面忘了问了,这里补上。
Claude为什么有的模型擅长稠密,有的擅长稀疏?
结构和预训练都有影响,但预训练目标是决定性的。
结构上,BERT、BGE 这类 Transformer encoder 经过 attention + pooling 后,输出是固定维度的向量,每个维度都有值——天然就是稠密的。而 BM25 根本不是神经网络,它在词典大小的空间里工作(比如 5 万维),每个文档只有出现过的词对应的维度非零,天然就是稀疏的。
但也有神经网络产出稀疏向量的例子——比如 SPLADE。它用的也是 Transformer,但在输出层加了一个稀疏化操作(log-saturate + ReLU),强制把大部分维度压到 0,最终输出的是词典维度上的稀疏向量。所以不是说“用了 Transformer 就一定稠密”,关键看输出层怎么设计、训练目标是什么。
Embedding 预训练的目标是什么?怎么标注?
核心思路就是你猜的那样——对齐相近语义,拉远不同语义。学术上叫对比学习(contrastive learning)。
训练时需要构造正负样本对:
样本类型 含义 来源 正样本对 语义相近的两段文本 (query, 用户点击的文档)、(问题, 答案)、(标题, 正文)、(句子, 它的改写) 负样本对 语义不相关的两段文本 随机配对,或“硬负例”——看起来像但其实不相关的文档 损失函数的作用就是:正样本对的向量距离拉近,负样本对的向量距离推远。
标注从哪来? 这确实是个难题,但实际上有很多巧妙的做法:
- 天然监督信号:搜索引擎的点击日志(用户搜了 X 点了 Y → 正样本对)、QA 网站的问答配对、论文的标题和摘要
- 人工标注:贵但质量高,比如 MS MARCO 数据集有人工标注的 query-passage 相关性
- 自监督:同一篇文章里相邻的两个段落当正样本,不同文章的段落当负样本——不需要任何人工标注
所以 embedding 模型的训练并不需要像你想的那样“给每对文本标注相似度分数”,而是通过正负样本对的对比来间接学习语义空间的结构。这也是为什么有海量无标注文本的时代,embedding 模型能越训越好。





