[控场AI]
· 9 分钟阅读· 4,506 字

嵌入从原理讲起:小猫如何找猫,还跨语言找到Gato

嵌入从原理讲起:小猫如何找猫,还跨语言找到Gato

从第一性原理拆解嵌入与余弦相似度,揭示多语言 RAG 静默失败的原因。

文章从关键词搜索为何失灵出发,用颜色类比解释嵌入:把文本含义转成固定长度向量,并通过余弦相似度用夹角衡量语义远近。作者用二十行代码演示「kitten」如何匹配「cat」并让西语「gato」排第二,展现多语言模型的跨语言能力。随后通过对比纯英文模型,指出拼写重叠会制造虚假高分——纯英文模型给西语句打高分只是因为共享 el、ga、##to 等分词片段,并非真正理解。文章最后提醒两个坑:不能跨模型比较向量,换模型必须重新嵌入全部文档;同时要注意最大 token 长度,超出会被静默截断。

计算机无法直接比较含义,只能比较数字。本文从第一性原理拆解嵌入模型:如何把一段文字变成向量,如何用余弦相似度衡量语义远近,以及为什么用错模型会让多语言 RAG 悄无声息地失败。

嵌入从第一性原理出发,演示猫与小猫、gato 的语义匹配

为什么关键词搜索会失灵

关键词搜索在两个场景下必然失效:同义词和翻译。查询「kitten」时,文档里写的是「cat」;查询「cat」时,文档里写的是西语「gato」。字母层面没有重叠,倒排索引就什么也找不到。

要解决这个问题,就需要一种把含义转成数字的方法。这就是嵌入(embedding)要干的事,也是 RAG 中「R」(retrieval,检索)的第一步。

嵌入是什么:用颜色类比理解向量

嵌入模型把任意文本转成一个固定长度的数字列表,也就是向量。屏幕上每一种颜色都只是 3 个数字(R、G、B):

Orange       → [255, 165,   0]
Dark orange  → [255, 140,   0]   ← 数字与橙色接近
Blue         → [  0,   0, 255]   ← 数字离两者都远

橙色和深橙色看起来相似,数字也接近;蓝色看起来不同,数字就远。3 个数字能描述 1600 万种颜色,但 3 个数字并不等于 3 种颜色。

嵌入做的是同一件事,只是数字更多。模型不同,维度也不同:

模型每段文本的数字个数
paraphrase-multilingual-MiniLM-L12-v2384(本文使用)
BERT-base 系列768
OpenAI text-embedding-3-small1536
OpenAI text-embedding-3-large3072
Google gemini-embedding-0013072

维度越高,能捕捉的细节越细,但存储成本更高、检索更慢。用 384 维模型的效果是这样的:

"The cat sat on the mat."         → [ 0.43, -0.38, -0.23, ... ]
"El gato duerme en la alfombra."  → [ ... 相似的数字 ... ]   ← 含义相同,距离近
"Stock markets fell sharply."     → [ ... 完全不同的数字 ... ] ← 含义无关,距离远

两个关键事实容易被忽略:384 是模型的输出维度,由模型设计决定,与文本的词数、字符数无关;单独一个数字没有意义,只有向量之间互相比较时,含义才会浮现。

余弦相似度:用夹角衡量语义距离

余弦相似度衡量两个向量之间的夹角:结果为 1 表示方向一致(含义相同),为 0 表示无关。

import numpy as np

def cosine(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

它忽略向量长度,所以一个短问题仍能匹配一段长文档。

二十行代码跑通语义检索

from sentence_transformers import SentenceTransformer
import numpy as np

docs = [
    "The cat sat on the mat.",
    "Dogs are loyal pets.",
    "Stock markets fell sharply today.",
    "El gato duerme en la alfombra.",   # 西语:猫在毯子上睡觉
    "The central bank raised interest rates.",
]
query = "Where is the kitten?"

model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
doc_vecs = model.encode(docs)
q_vec = model.encode(query)

scores = [cosine(q_vec, d) for d in doc_vecs]
for score, doc in sorted(zip(scores, docs), reverse=True):
    print(f"{score:.3f}  {doc}")

运行结果:

0.560  The cat sat on the mat.
0.480  El gato duerme en la alfombra.     ← 西语句排第二
0.138  Dogs are loyal pets.
0.049  Stock markets fell sharply today.
0.046  The central bank raised interest rates.

「kitten」找到了「cat」,两个句子没有任何共享单词。西语句拿到第二名。模型从不逐字母比较「gato」和「cat」,它在数百万翻译句对上训练,目标是让每一对句子落在向量空间中相同的位置——句子落在哪里取决于含义,而不是语言。

上述代码中的 paraphrase-multilingual-MiniLM-L12-v2 是 Sentence Transformers 库提供的多语言句子嵌入模型,它把 12 层 Transformer 编码器的输出通过平均池化压缩成 384 维向量,支持约 50 种语言。Sentence Transformers 是在 BERT、XLM-R 等预训练模型之上做对比学习微调的框架,训练目标是让语义相似的句子向量彼此靠近。模型首次使用时由 encode() 自动下载权重。真正让它具备跨语言能力的是多语言 XLM-R 主干与数百万翻译句对(如英语和西语互译)的训练数据:同义的跨语言句子对在训练中被拉近到向量空间的同一区域,因此模型能在不依赖共享单词的情况下比对语义。

多语言能力翻车:拼写重叠的假象

同样的测试换成只在英文上训练的 all-MiniLM-L6-v2,结果出现反转:

模型          查询                       cat(英)   gato(西)
多语言        "Where is the kitten?"      0.560     0.480
多语言        "¿Dónde está el gatito?"    0.569     0.467
纯英文        "Where is the kitten?"      0.418     0.096
纯英文        "¿Dónde está el gatito?"   -0.069     0.616  ← 陷阱

最后一行是陷阱:纯英文模型给西语句打了最高分,但不是因为它看懂了。看它的分词器输出:

query: ['¿', 'don', '##de', 'est', '##a', 'el', 'ga', '##ti', '##to', '?']
doc:   ['el', 'ga', '##to', 'due', '##rm', '##e', 'en', 'la', 'alf', '##om', '##bra', '.']

它们共享 el、ga、##to 这几个片段,所以这是拼写重叠,不是语义理解。同时,它把英文的猫句子排到了最后。

结论很直接:当查询和文档存在拼写重叠时,模型看起来像多语言。只有跨语言测试(A 语言提问、B 语言作答)才能证明它真的多语言。这也是 RAG 选错模型后「悄无声息失败」的典型方式。

这里的 ## 是 WordPiece 分词器的子词前缀标记。BERT 系列模型先把单词拆成词汇表里存在的子词:如果一个完整单词不在词表中,就退回到更短的片段,并用 ## 标记它是某个词的后半部分。例如 gatito 被切成 ga、##ti、##to,而 gato 也包含 ##to,于是两者在查询和文档之间出现共享 token。这个机制让模型能处理未登录词,但也带来副作用:正字法相近的语言(如英语和西语的拉丁字母词汇)会因碎片重叠而产生虚假相似度,而模型实际输出的只是对拼写碎片而非完整语义的编码。

两个容易踩的坑

不要跨模型比较向量。 每个模型都有自己的「语义地图」。用模型 A 的向量和模型 B 的向量做比较,相当于拿 GPS 坐标和 RGB 值比大小。一旦换模型,所有文档都必须重新嵌入。

注意最大长度。 这个多语言模型最多读取 128 个 token,超出部分会被静默截断。这正是 RAG 要切分文档块的原因。可以从下载模型的配置文件里查最大序列长度:

cd ~/.cache/huggingface/hub/models--sentence-transformers--paraphrase-multilingual-MiniLM-L12-v2/snapshots/*/ 
cat sentence_bert_config.json

# 输出:
# {
#   "max_seq_length": 128,
#   "do_lower_case": false
# }

分词器是模型的「眼睛」。如果它把一门语言切成语义上无意义的碎片,模型就只能匹配拼写,匹配不了含义。

背景补充

max_seq_length 是模型编码时允许的最大 token 数,这里的 128 个 token 对应大约 80 到 100 个英文单词或更少的非拉丁字母语言。Sentences Transformers 在编码时默认会把超长文本截断到该长度,且不发出警告,因此较长的文档块后半部分会被静默丢弃,检索时仿佛那些内容从未出现。RAG 中常见的 256 或 512 token 分块策略正是为了匹配不同嵌入模型的这一上限。查询同样受此限制,超长查询会被截断,导致关键信息丢失。在选模型时,除了维度,还应确认 max_seq_length 是否覆盖你的典型文档块长度。

#嵌入模型#embedding#余弦相似度#RAG 检索#文本向量化#多语言嵌入#sentence-transformers#向量维度
分享:

相关推荐