Embedding 实战:闭源 vs 开源

做检索这件事

先聚类,再做语义搜索 | 图:作者

如果你不是会员但想读这篇,这是免费阅读链接

embedding 是 NLP 的基石之一。它能干的事不少,但最常见的一件就是检索场景里的语义搜索。

虽然整个圈子最近都在聊知识图谱检索 pipeline,但传统的向量检索其实并没过时。

网上能找到一堆文章讲怎么把语义搜索里不相关的结果过滤掉——这篇也会聊这个,用聚类和重排(re-ranking)这两招。

不过这篇真正想做的事,是把开源和闭源 embedding 模型放一起对比——还要看不同体量。

本文要看的几个模型——市面上还有更多 | 图:作者

我们会对比 9 个 embedding 模型,全是 MTEB 排行榜上排名靠前的。这样你大概能感觉到大模型 vs 小模型的差距,以及规模上去之后成本会怎么涨。

如果你用过 OpenAI 生成 embedding,估计也好奇过它到底竞争力够不够。

简单回顾一下 embedding——如果你刚接触:我们给每段文本生成 embedding(一组向量),就是把文本转成计算机能理解的东西。

文本到 embedding | 图:作者

具体到语义搜索,我们是把不同文本的 embedding 拿来比,看它们语义上有多像。这样查询就可以走"模糊搜索"——按关系找,不是按关键词精确匹配。

query embedding 跟其它 embedding 的语义相似度 | 图:作者

引言里我会讲 embedding 怎么工作,重点是怎么算语义相似度。

我每写一篇都喜欢拿一个具体场景练手,这次也不例外。这个想法来自一位咨询公司老板——他问我能不能做一个应用,把岗位描述自动匹配到 LinkedIn profile。

如果真做生产用,肯定要上百万真实 profile,但这篇只是演示,我用合成数据做了 6,900 份 LinkedIn profile。

数据集在这里

这场景算简单的:profile 不需要切块(一份就一段),领域也不复杂——一眼就能看出模型有没有抓对关系。

但思路是通用的,可以套到类似问题上。

如果你想直接跳过引言、用 LinkedIn 数据集动手玩不同模型,往下翻就行。

引言

这篇里 profile 数量不多,可以把聚类当成一种无监督的分类方法用在整个数据集上。

下面是聚类长什么样的示意图。

LinkedIn profile 分成 4 类的简化示意 | 图:作者

聚类还能让我们看出不同模型怎么理解事物之间的关联。

看模型表现,我们可以先把正确的那一组隔离出来,再在簇内做语义搜索。

把 query 匹到对的簇——简化图 | 图:作者

这样能过滤掉不相关的结果,比如模型把 product manager 和 product marketing manager 搞混的情况。

最后还能加一步:用 LLM 做重排,确保最相关的结果排在最上面。

为了省事也省钱,我把每个待评测模型的 embedding 都已经预先算好放进数据集里了,匿名岗位描述的 embedding 也算好了。

提醒一下:想直接动手可以跳到用例那一节,但成本那部分别跳过。

Embedding

前面提过,embedding 是文本的数值表示,把含义编码下来——这样计算机就能处理和理解自然语言。

文本到 embedding | 图:作者

到了 transformer 这一代模型,它们能理解整句的 context,从而抓住一个词或一句话的多重含义——这事几年前还做不到。

我们其实可以把 embedding 画在图上,当成几何空间里的点。embedding 之间的语义关系,就体现为几何上的远近。

embedding 在图上是几何空间里的点 | 图:作者

不同模型为不同任务训练,但稍大一些的模型一般都是通才,能干检索聚类分类这类活。

语义搜索用在检索里——它就是用图上的"近"来判断 query 应该匹到哪些 embedding,本质上是在算图上 embedding 之间的距离。

要在语义搜索里算 embedding 之间的相似度有好几种方法,但最常用的是余弦相似度。

用语义相似度找最佳匹配 | 图:作者

你选哪个模型,直接决定语义搜索的结果——这归根结底取决于它是怎么训练的。

训练用的数据集、目标、架构都很关键,会直接影响模型对各种文本的理解和关联能力。

聚类则不太一样——它是把数据组织成一组组(簇),让同一簇内的相似度高于跨簇相似度。聚类更擅长把 embedding 之间的相似关系挖出来,让我们能干净地隔离出某一组。

把 embedding 聚类好把相似 profile 分组,并把 query 落位 | 图:作者

这一步让我们能在做语义搜索之前先过滤掉不相关的匹配——相当于一道降噪工序。

起码思路是这样。

不是每个模型都能按我们想要的方式做聚类。有的擅长,有的拉胯,全看它怎么训出来的。

Embedding 模型

那怎么知道选哪个?这时候 MTEB 排行榜就该出场了——它按各种任务上的表现给 embedding 模型排名。

我从中挑了几个出来对比:OpenAI 那几款用得多的,再加一个 fine-tune 过的 Mistral-7B,还有更小更新的模型比如 Mixedbread AI 的 Mxbai

差不多都是过去两年里发布的。

本文要用的模型——这份名单不算定稿 | 图:作者

如果你刚接触开源模型,可能会惊讶——好多开源模型其实排得很靠前。如果你已经玩过这些模型,看一眼哪个在这个任务上跑得最好也挺有意思。

下面这张表列了每个模型的体量、最大 token 数、以及检索和聚类的排名。

本文要用的几个模型在 MTEB 排行榜的指标 | 图:作者

很多人都用过 OpenAI 的 Ada-002,它在我们的列表里垫底。不过 OpenAI 后来出了 text-embed-3 的 small 和 large 两版——表现更好,价格也更便宜。

那你可能会问:开源模型在排行榜上排得这么高,干嘛还有人用商业模型?

开源模型的成本账

用开源模型听起来挺好的,隐私上也是首选。排行榜上靠前的不少,但你得算一笔账:自己托管 vs 调 API,到底哪个划算。

我看了一下成本——把小一些(约 350M)和大一些(7B)的开源模型放 GPU 上自托管,对比按 token 付费的几款主流商业模型。

1 万、10 万、100 万、250 万条文本的算力成本——不含存储 | 图:作者

前提是每段文本 400 token——这样 334M 模型在单张 L4 GPU 上每秒能处理 75–90 段,7B 模型在单张 A100 上每秒约 30–40 段(也可能更多)。

你会发现:一旦要 embed 几百万段文本,text-embed-3-large 或 ada-002 这种模型成本就会飙——而且这还没算存储。

如果你是企业客户,又在看 Nvidia 的 embedding 模型比如 nv-embed-v1,他们的 API 挺好用的——我用它测过几个模型。

不过用小模型(500M 以下)肯定是最稳的选择。能上小一点的开源模型就上——算力成本能直接砍掉 90%。

我也算了一下小模型和大模型放单张 GPU 上要跑多久。

单张 GPU 估算的处理时间 | 图:作者

调 API 也要花时间,而且有推理速率限制——所以不管选哪条路,都要算把整份数据集 embed 完要多久。

开源模型嘛,可以多堆几张 GPU。但这至少给你点感觉——选小一点的可能更省电。

不过测几个看看哪个适合你的任务总没错——接下来就这么干。

量化

上面看到了,7B 这种规模的大模型还是又贵又费电。我们只算了 250 万条 embedding 的成本——一旦再往上 scale,量化就值得看一看了。

量化是用更少的位数表示模型的数据,从而把模型压小。思路是:用 4-bit、8-bit 这类量化手段,让原本带不动大模型的硬件也能跑起来。

也有几个人测过量化对各种指标的影响——我最近看到的一篇说整体性能下降约 12%。

这个以后我得专门写一篇——这种"经济性 vs 性能损失"的话题我特别喜欢挖。

用例

我不知道你怎么样——反正我喜欢直接拿不同模型测一测,而不是只看指标。这样我能直观感受到:小模型和大模型在文本关系上理解力差多少。

合成 LinkedIn profile 数据集这里,要匹配的岗位描述数据集也在。

要用的 Colab notebook 在这里

导入数据

要跟着做就得打开那个 notebook——打开后你会看到我们从 Hugging Face 拉了两个数据集。

# 带 embedding 的合成 LinkedIn profile
dataset = load_dataset("ilsilfverskiold/linkedin_profiles_synthetic")
profiles = dataset['train']
# 带 embedding 的匿名岗位描述
dataset = load_dataset("ilsilfverskiold/linkedin_recruitment_questions_embedded")
applications = dataset['train']

这两个数据集让我们能在 6,900 份合成 LinkedIn profile 上对比不同 embedding 模型。

合成数据嘛,毕竟是合成的,要打折听。这批是 Llama 3.1 生成的——而且确实有点"过度对齐"的味道:profile 里反复出现 "results-driven"、"seasoned"、"dedicated" 这种词。

embedding 已经预先算好放进 profiles 里了——你点进去就能看到。

# profiles 数据集
Dataset({
    features: [...,'embeddings_nv-embed-v1', 'embeddings_nv-embedqa-e5-v5', 'embeddings_bge-m3', 'embeddings_arctic-embed-l', 'embeddings_mistral-7b-v2', 'embeddings_gte-large-en-v1.5', 'embeddings_text-embedding-ada-002', 'embeddings_text-embedding-3-small', 'embeddings_voyage-3', 'embeddings_mxbai-embed-large-v1 '],
    num_rows: 6904
})

P.S. embeddings_gte-large-en-v1.5 这个跑不通——我尝试过自己托管但没把所有 embedding 都跑齐,所以别用它。

接下来你需要选一份要匹配的岗位描述。

看下面代码——我选了第 2 份,你可以换别的编号。

application = applications[1] # 选第 2 份——一个 product marketing manager 岗位
application_text = application['natural_language']
print("application we're looking for: ", application_text)

懒得跑代码也行,直接在 Hugging Face 上看数据集

Hugging Face 上的岗位描述数据集预览 | 图:作者

这一步你可以挑想用的 embedding 模型。我大部分都试过了,这次用 embeddings_mxbai-embed-large-v1

这是一个 334M 的开源模型——在前面那张表里它的检索和聚类排名都挺靠前。

想换别的模型只要改一下名字就行。看上面那个数据集就知道有哪些选项。

# 拿这个 embedding 模型对应的 query embedding——这里挑的是 mxbai-embed-large-v1
query_embedding_vector = np.array(application['embeddings_mxbai-embed-large-v1'])
embeddings_list = [np.array(emb) for emb in profiles['embeddings_mxbai-embed-large-v1 ']] # 注意这里多了个空格
texts = profiles['text']

语义搜索

我们先不上聚类,直接做一次语义搜索——看看裸跑能到什么效果。

要算 profile 跟 query(也就是岗位描述)之间的语义相似度,跑下面这段代码。

# 先单独算一下余弦相似度(不带聚类)
def cosine_similarity(a, b):
    a = np.array(a)
    b = np.array(b)
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

similarities = []
for idx, emb in enumerate(embeddings_list):
    sim = cosine_similarity(query_embedding_vector, emb)
    similarities.append(sim)

然后按相似度从高到低排序,显示前 30 条。

results = list(zip(range(1, len(texts) + 1), similarities, texts))
sorted_results = sorted(results, key=lambda x: x[1], reverse=True)
# 把结果打出来
print("\nSimilarity Results (sorted from highest to lowest):")
for idx, sim, text in sorted_results[:30]: # 想看更多就调这里
    percentage = (sim + 1) / 2 * 100
    text_preview = ' '.join(text.split()[:10])
    print(f"Text {idx} similarity: {percentage:.2f}% - Preview: {text_preview}...")

结果大概长这样——具体看你选的是哪一份岗位描述。

Similarity Results (sorted from highest to lowest):
Text 3615 similarity: 89.59% - Preview: Product Marketing Manager | Building Go-to-Market Strategies for Growth Results-driven...
Text 6299 similarity: 89.56% - Preview: Product Marketing Manager | Driving Growth & Customer Engagement Results-driven...
Text 3232 similarity: 89.09% - Preview: Product Marketing Manager | Driving Product Growth through Data-Driven Strategies...
Text 5959 similarity: 88.90% - Preview: Product Marketing Manager | Data-Driven Growth Expert Results-driven Product Marketing...
Text 5635 similarity: 88.84% - Preview: Product Marketing Manager | Driving Growth through Data-Driven Marketing Strategies...
Text 5835 similarity: 88.74% - Preview: Product Marketing Manager | Cloud-Based SaaS Results-driven Product Marketing Manager...
Text 139 similarity: 88.66% - Preview: Product Marketing Manager | Scaling Growth through Data-Driven Strategies Experienced...
Text 6688 similarity: 88.48% - Preview: Product Marketing Manager | Driving Business Growth through Data-Driven Insights...
Text 6405 similarity: 88.27% - Preview: Product Marketing Manager | Scaling SaaS Products for Global Markets...
Text 3439 similarity: 88.11% - Preview: Product Manager | Focused on delivering innovative products that drive...
Text 5958 similarity: 88.00% - Preview: Product Manager Office | Growth Driven by Customer Centricity Highly...
Text 5183 similarity: 87.86% - Preview: Product Marketing Manager | B2B SaaS Experienced Product Marketing Manager...
Text 1329 similarity: 87.81% - Preview: Product Marketing Manager | Scaling Growth for Emerging Tech Startups...
Text 130 similarity: 87.81% - Preview: Product Marketing Manager | Growth Strategies & Launches Results-driven Product...
Text 3423 similarity: 87.78% - Preview: Product Marketing Manager | Scaling B2B SaaS Solutions Experienced Product...
Text 4234 similarity: 87.72% - Preview: Product Manager | Leading Cross-Functional Teams to Drive Business Growth...

mxbai 这个模型——还有不少其他模型也一样——明显能看出它把 product marketing manager 跟 product manager 一起返回了,这不是我们想要的。

看上面 88.11% – Preview: Product Manager 和 88.00% – Preview: Product Manager Office 这两条。

那就上聚类——看看它能不能帮上忙。

聚类

第一步从 profile 的 embedding 里搭聚类——你得先决定要分多少簇。

我选了 10。

embeddings_array = np.array(embeddings_list)
num_clusters = 10 # 这里数字可以换
kmeans = KMeans(n_clusters=num_clusters, random_state=42)
kmeans.fit(embeddings_array)
cluster_labels = kmeans.labels_

pca = PCA(n_components=2)
reduced_embeddings = pca.fit_transform(embeddings_array)

接下来要看 query(岗位描述)落在哪一簇。

# 看 query 落到哪个簇里
query_embedding_array = np.array(query_embedding_vector).reshape(1, -1)
reduced_query_embedding = pca.transform(query_embedding_array)
# 预测 query 应该归到哪一簇
query_cluster_label = kmeans.predict(query_embedding_array)[0]
print(f"The query belongs to cluster {query_cluster_label}")

然后我们可以把簇画在 2 维图上——记得这是降维后的结果,所以有些簇可能叠在一起。

把 LinkedIn profile 聚类画出来 - 截自 Colab notebook

你可以鼠标悬停到不同 embedding 上看 profile 内容。

看第 5 簇的 embedding - 截自 Colab notebook

也可以把 query(图里的 X)单独标出来,看模型把它判到哪一簇。

把第 5 簇连同 query X 一起隔离出来 - 截自 Colab notebook

可以清楚看到,模型把 marketing 相关的人都正确归到同一簇——SEO specialist 和 growth hacker 也在里面,但 Office Product Manager 和 Product Manager 都不在。

漂亮。

到这一步,我们可以把聚类和语义搜索两招拼起来用,效果会更好。

记得换不同模型试试看哪个效果更好——你会发现大模型在"把相似 profile 分到一组"这件事上按理说更强,但有些小模型也能跑得不错。

聚类 + 语义搜索

既然 query 能正确归到对的簇里,我们就可以把两招拼起来用。

# 在正确的簇内做语义搜索
cluster_indices = np.where(cluster_labels == query_cluster_label)[0]
cluster_embeddings = embeddings_array[cluster_indices]
cluster_texts = [texts[i] for i in cluster_indices]

similarities_in_cluster = []
for idx, emb in zip(cluster_indices, cluster_embeddings):
    sim = cosine_similarity(query_embedding_vector, emb)
    similarities_in_cluster.append((idx, sim))

similarities_in_cluster.sort(key=lambda x: x[1], reverse=True)
top_n = 40 # 想看更多结果就调这个
top_matches = similarities_in_cluster[:top_n]

print(f"\nTop {top_n} similar texts in the same cluster as the query:")
for idx, sim in top_matches:
    percentage = (sim + 1) / 2 * 100
    text_preview = ' '.join(texts[idx].split()[:10])
    print(f"Text {idx+1} similarity: {percentage:.2f}% - Preview: {text_preview}...")

跑完上面这段就能看到——结果里 Product Manager 不见了,换成了 Marketing Manager 这类更对路的人选。

Top 40 similar texts in the same cluster as the query:
Text 3615 similarity: 89.59% - Preview: Product Marketing Manager | Building Go-to-Market Strategies for Growth Results-driven...
Text 3232 similarity: 89.09% - Preview: Product Marketing Manager | Driving Product Growth through Data-Driven Strategies...
Text 5959 similarity: 88.90% - Preview: Product Marketing Manager | Data-Driven Growth Expert Results-driven Product Marketing...
Text 5635 similarity: 88.84% - Preview: Product Marketing Manager | Driving Growth through Data-Driven Marketing Strategies...
Text 5835 similarity: 88.74% - Preview: Product Marketing Manager | Cloud-Based SaaS Results-driven Product Marketing Manager...
Text 139 similarity: 88.66% - Preview: Product Marketing Manager | Scaling Growth through Data-Driven Strategies Experienced...
Text 6688 similarity: 88.48% - Preview: Product Marketing Manager | Driving Business Growth through Data-Driven Insights...
Text 6405 similarity: 88.27% - Preview: Product Marketing Manager | Scaling SaaS Products for Global Markets...
Text 5183 similarity: 87.86% - Preview: Product Marketing Manager | B2B SaaS Experienced Product Marketing Manager...
Text 1329 similarity: 87.81% - Preview: Product Marketing Manager | Scaling Growth for Emerging Tech Startups...
Text 130 similarity: 87.81% - Preview: Product Marketing Manager | Growth Strategies & Launches Results-driven Product...
Text 3423 similarity: 87.78% - Preview: Product Marketing Manager | Scaling B2B SaaS Solutions Experienced Product...
Text 5945 similarity: 87.63% - Preview: Marketing Manager | Driving Growth through Data-Driven Strategies Results-driven marketing...
Text 2664 similarity: 87.59% - Preview: Product Marketing Manager | Driving Growth & Innovation Results-driven Product...
Text 3368 similarity: 87.54% - Preview: Product Marketing Manager | Scaling Growth through Data-Driven Strategies Highly...
Text 5794 similarity: 87.48% - Preview: Product Marketing Manager | Driving Growth through Data-Driven Insights Results-driven...
Text 5685 similarity: 86.71% - Preview: Performance Marketing Manager | Driving Business Growth through Data-Driven Strategies...
Text 5818 similarity: 86.37% - Preview: Digital Marketing Manager | Driving Business Growth through Data-Driven Strategies...

真实生产场景里,最好在做语义搜索之前先对数据做一遍过滤和分类。

这一节的核心就是让你自己对比不同模型——尤其是小模型 vs 大模型——看你愿意为更快更便宜的推理牺牲多少质量。

别只是为了上大模型而上大模型,除非你真的需要。

想继续评估这些模型,可以用 RAGAs 看看不同模型下检索应用的表现。

关于模型表现的几点观察

要测性能总得挑个标准——我选的是看模型能不能把 product manager 和 product marketing manager 分清楚。

大模型在不上聚类的时候,更容易直接给出对的结果——但所有模型一开始都没把这两个职位分干净。

Ada-002 因为体量大不少,不上聚类的裸跑也表现不错;OpenAI 那个更小更新的 text-embed-3-small 反而差一些。

给一个岗位 profile 设我们自己的评测指标 | 图:作者

不过有些模型连聚类都聚不对——具体来说,fine-tune 过的 7B Mistral 和 E5 这次没跑好。这可能跟它们怎么训出来有关。

剩下那些在这个具体岗位上表现差不多。

让我意外的是 mxbai——只有 335M 居然这么能打。这恰好说明:简单任务上,大模型可能就是杀鸡用牛刀。

这只是针对这一件小事的评测——你自己的任务该看什么指标得自己定。

不过从这里可以继续往前走——比如再叠一层重排,把最好的结果挑给 LLM 评。

重排(Re-Ranking)

RAG pipeline 里要修正不相关结果的招数有不少,重排是其中一种。

重排说白了就是把结果重新排序,让相关的排到最上面。一种常见做法是 Pairwise Ranking(成对排序)

具体怎么做:把一对 profile 喂给一个模型(可以是 LLM),让它根据岗位描述判断哪一个更合适。

用 LLM 做 Pairwise Ranking——简化图 | 图:作者

具体到你的场景,得把几种方法组合起来才能跑得好。

如果你刚接触 embedding,希望这篇有点收获;如果不算新手,希望这次至少给你扒了点小模型 vs 大模型的成本账——开源闭源都聊到了。

至于更大的 LLM——闭源那边确实领先;但 embedding 模型这块不是这样。

就记一句:给小一些、算力更友好的模型一个机会。