• 文章
  • 现代稀疏神经检索:从理论到实践
返回机器学习

现代稀疏神经检索:从理论到实践

Evgeniya Sukhodolskaya

·

2024 年 10 月 23 日

Modern Sparse Neural Retrieval: From Theory to Practice

在保持生产系统运行的同时,想要抽出足够的时间研究所有现代解决方案,这几乎是不可行的。密集检索器、混合检索器、后期交互……它们是如何工作的?最适合应用在哪里?要是我们能像在亚马逊上比较商品那样轻松比较检索器就好了!

我们探索了最流行的现代稀疏神经检索模型,并为您进行了详细解读。读完本文,您将对稀疏神经检索的当前格局有一个清晰的认识,并学会如何轻松应对那些复杂、数学性强、NDCG 分数极高的研究论文,而不会感到不知所措。

本文的第一部分是理论性的,比较了现代稀疏神经检索中使用的不同方法。
本文的第二部分则更具实践性,展示了如何在 Qdrant 中使用现代稀疏神经检索中最好的模型 SPLADE++,并就何时为您的解决方案选择稀疏神经检索提出了建议。

稀疏神经检索:仿佛基于关键词的检索器理解了含义

基于关键词(词汇)的检索器,如 BM25,提供了良好的可解释性。如果文档匹配查询,很容易理解原因:查询词出现在文档中,如果这些是罕见的词,它们对检索就更重要。

Keyword-based (Lexical) Retrieval

凭借其精确词匹配机制,它们在检索时速度极快。一个简单的倒排索引,将词映射回包含该词的文档列表,从而节省了检查数百万文档的时间。

Inverted Index

词汇检索器在检索任务中仍然是一个强大的基准。然而,根据设计,它们无法弥合词汇语义不匹配的差距。想象一下在网上商店搜索“美味的奶酪”,却无法将“高达”或“布里”奶酪放入购物车。

密集检索器,基于机器学习模型,将文档和查询编码为密集向量表示,能够弥合这一差距并为您找到“一块高达奶酪”。

Dense Retrieval

然而,这里的可解释性较差:为什么这个查询表示与这个文档表示如此接近?为什么搜索“奶酪”时,我们也会看到“捕鼠器”?这个向量表示中的每个数字代表什么?它们中哪个捕捉了“奶酪味”?

缺乏扎实的理解,平衡结果质量和资源消耗就变得具有挑战性。由于理论上任何文档都可能匹配查询,依赖于精确匹配的倒排索引并不可行。这并不意味着密集检索器本质上较慢。然而,词汇检索已经存在足够长的时间,足以启发几种有效的架构选择,这些选择通常值得复用。

迟早会有人说:“等等,如果我想要像 BM25 一样经得起时间考验但又具有语义理解能力的东西怎么办?

稀疏神经检索的演进

想象一下搜索一个“令人瞠目结舌的谋杀”故事。“Flabbergasting”(令人瞠目结舌的)是一个很少使用的词,因此基于关键词的检索器(例如 BM25)会赋予它巨大的重要性。结果,很可能出现一个与任何犯罪无关但提到了“flabbergasting”的文本,并出现在顶部结果中。

如果我们不像 BM25 那样依赖文档中的词频作为词重要性的代理,而是直接预测词的重要性呢?目标是让罕见但不重要的词被赋予比同等频率的重要词小得多的权重,而在 BM25 场景中两者将被同等对待。

我们如何确定一个词是否比另一个词更重要?词的影响力与其含义相关,其含义可以从其上下文(包围特定词语的词)中推导出来。这就是密集上下文嵌入模型发挥作用的地方。

所有稀疏检索器都基于这样一个想法:采用一个能为词生成上下文密集向量表示的模型,并训练它生成稀疏表示。通常,基于 Transformer 的双向编码器表示 (BERT) 被用作基础模型,并在其之上添加一个非常简单的可训练神经网络来稀疏化表示。训练这个小型神经网络通常是通过从 MS MARCO 数据集中采样一个查询以及与其相关和不相关的文档,并朝相关性方向调整神经网络参数来完成的。

稀疏神经检索的先驱

深度上下文词权重框架 (DeepCT) 作为首批稀疏检索器之一,深度上下文词权重框架 (DeepCT) 的作者分别为文档和查询中的每个唯一词预测一个整数词影响力值。他们在基本 BERT 模型产生的上下文表示之上使用线性回归模型,模型的输出被四舍五入。

将文档上传到数据库时,经过训练的线性回归模型会预测文档中词的重要性,并以与 BM25 检索器中词频相同的方式存储在倒排索引中。然后,检索过程与 BM25 完全相同。

为什么 DeepCT 不是完美的解决方案?为了训练线性回归模型,作者需要提供每个词重要性的真实值(ground truth),这样模型才能“看到”正确的答案应该是什么。这个分数很难定义,无法真正表达查询-文档的相关性。当一个词来自一个五页的文档时,哪个分数应该代表与查询最相关的词?是第二个相关的词?还是第三个?

基于相关性目标的稀疏神经检索

DeepImpact 定义整个文档与查询是相关还是不相关要容易得多。这就是为什么 DeepImpact 稀疏神经检索器的作者直接将查询和文档之间的相关性作为训练目标。他们获取 BERT 生成的文档词语的上下文嵌入,通过一个简单的两层神经网络将其转换为一个单一的标量分数,并对每个与查询重叠的词语将这些分数相加。训练目标是使这个分数反映查询和文档之间的相关性。

为什么 DeepImpact 不是完美的解决方案?将文本转换为密集向量表示时,BERT 模型并非在词级别工作。有时,它会将词分解成部分。例如,“vector”这个词会被 BERT 作为一个整体处理,但对于 BERT 之前没见过的一些词,它会将其切分成部分,例如“Qdrant”会变成“Q”、“#dra”和“#nt”

DeepImpact 模型(像 DeepCT 模型一样)只取 BERT 为一个词生成的第一个部分,而丢弃其余部分。然而,搜索“Q”而不是“Qdrant”又能找到什么呢?

了解你的分词

词独立似然模型 v2 (TILDE v2) 为了解决 DeepImpact 架构的问题,词独立似然模型 (TILDEv2) 在 BERT 表示的级别上生成稀疏编码,而不是在词级别。除此之外,其作者使用了与 DeepImpact 模型相同的架构。

为什么 TILDEv2 不是完美的解决方案?一个单一的标量重要性得分值可能不足以捕捉一个词的所有不同含义。同音异义词(披萨、鸡尾酒、花以及女性名字“Margherita”)是信息检索中的一个难题。

理解同音异义词的稀疏神经检索器

COntextualized Inverted List (COIL)

如果词语重要性得分只有一个值不够,我们可以用向量形式描述词语的重要性!上下文倒排列表 (COIL) 模型的作者基于这个想法进行了研究。他们没有将 BERT 的 768 维上下文嵌入压缩成一个值,而是通过类似的“相关性”训练目标将其降维到 32 维。此外,为了不遗漏细节,他们还将查询词编码为向量。

对于表示查询令牌的每个向量,COIL 找到文档中同一令牌的最接近匹配向量(使用最大点积)。因此,例如,如果我们搜索“Revolut bank <金融机构>”,并且数据库中的一个文档包含句子“Vivid bank <金融机构> 已迁移到 Amstel <河流> 的岸边”,在这两个“bank”中,第一个与查询中的“bank”的点积值会更大,并计入最终分数。文档的最终相关性分数是匹配的查询词语分数的总和。

为什么 COIL 不是完美的解决方案?这种定义重要性分数的方式捕捉了更深层的语义;用于描述它的值越多,含义就越丰富。然而,为每个词存储 32 维向量成本要高得多,并且倒排索引无法直接与这种架构配合使用。

回归本源

通用上下文倒排列表 (UniCOIL) 作为 COIL 作者的后续工作,通用上下文倒排列表 (UniCOIL) 回归到生成标量值作为重要性分数而非向量,同时保留了 COIL 的所有其他设计决策。
它优化了资源消耗,但与 COIL 架构相关的深层语义理解又一次丢失了。

我们解决词汇不匹配的问题了吗?

无论预测词语重要性的方法多么复杂,基于精确匹配的检索都无法匹配那些不包含查询词的相关文档。如果你在食谱书中搜索“披萨”,你不会找到“玛格丽特”。

解决这个问题的一种方法是通过所谓的文档扩展。让我们附加可能出现在搜索此文档的潜在查询中的词语。因此,“玛格丽特”文档变成了“玛格丽特披萨”。现在,对“披萨”进行精确匹配就可以奏效了!

Document Expansion

稀疏神经检索中使用了两种类型的文档扩展:外部(一个模型负责扩展,另一个模型负责检索)和内部(所有工作由一个模型完成)。

外部文档扩展

外部文档扩展使用生成模型(Mistral 7B、Chat-GPT 和 Claude 都是生成模型,根据输入文本生成词语)在将文档转换为稀疏表示并应用精确匹配方法之前,为文档添加内容。

使用 docT5query 的外部文档扩展

使用 docT5query 的外部文档扩展 docT5query 是最常用的文档扩展模型。它基于 文本到文本转换 Transformer (T5) 模型,该模型经过训练,可以为给定的文档生成 top-k 个可能的查询,这些查询将以该文档为答案。这些预测的短查询(最多约 50-60 个词)可能包含重复词,因此如果检索器考虑词频,它也会对词的频率做出贡献。

docT5query 扩展的问题是推理时间非常长,就像任何生成模型一样:每次运行时只能生成一个 token,并且需要消耗相当一部分资源。

使用词独立似然模型 (TILDE) 的外部文档扩展

External Document Expansion with Term Independent Likelihood MODel (TILDE)

词独立似然模型 (TILDE) 是一种外部扩展方法,与 docT5query 相比,它将段落扩展时间减少了 98%。它基于文本中词语相互独立的假设(就像我们在说话时插入词语而不关注其顺序一样),这使得文档扩展可以并行化。

TILDE 不是预测查询,而是预测阅读一段文本后最有可能出现的词语(查询似然范式)。TILDE 根据文档文本获取 BERT 词汇表中所有 token 的概率分布,并将其中 top-k 个 token 添加到文档中,不重复。

外部文档扩展的问题:在许多生产场景中,外部文档扩展可能不可行,因为没有足够的时间或计算资源来扩展数据库中想要存储的每个文档,然后额外执行检索器所需的所有计算。为了解决这个问题,开发了一代模型,这些模型一次性完成所有工作,从而“内部”扩展文档。

内部文档扩展

假设我们不关心查询词语的上下文,因此我们可以将它们视为独立的词语,以随机顺序组合以获得结果。然后,对于文档中的每个上下文词语,我们可以自由地预先计算该词语如何影响我们词汇表中的每个词语。

为每个文档创建一个与词汇表长度相同的向量。为了填充这个向量,对于词汇表中的每个词,会检查文档中是否有任何词对它的影响足够大,值得考虑。否则,词汇表中的该词在文档向量中的得分将为零。例如,通过对文档“玛格丽特披萨”在包含 50,000 个最常用英语词汇的词汇表上进行向量预计算,对于这个只有两个词的小文档,我们将得到一个 50,000 维的零向量,其中非零值将对应于“pizza”、“pizzeria”、“flower”、“woman”、“girl”、“Margherita”、“cocktail”和“pizzaiolo”。

带有内部文档扩展的稀疏神经检索器

Sparse Transformer Matching (SPARTA)

稀疏 Transformer 匹配 (SPARTA) 模型的作者使用了 BERT 模型和 BERT 的词汇表(约 30,000 个 token)。对于 BERT 词汇表中的每个 token,他们找到它与文档中上下文 token 之间的最大点积,并学习一个显著(非零)影响的阈值。然后,在推理时,唯一需要做的就是将该文档中所有查询 token 的分数加总。

为什么 SPARTA 不是完美的解决方案?许多稀疏神经检索器(包括 SPARTA)在 MS MARCO 数据集上训练后,在 MS MARCO 测试数据上表现良好,但涉及泛化(处理其他数据)时,它们的性能可能不如 BM25

现代稀疏神经检索的最新进展

稀疏词汇和扩展模型 Plus Plus (SPLADE++) 稀疏词汇和扩展模型 (SPLADE)] 模型家族的作者将密集模型训练技巧添加到了内部文档扩展的思想中,这使得检索质量显著提高。

  • SPARTA 模型在构建上不够稀疏,因此 SPLADE 模型家族的作者引入了显式的稀疏性正则化,防止模型产生过多的非零值。
  • SPARTA 模型主要直接使用 BERT 模型,没有额外的神经网络来捕捉信息检索问题的特殊性,因此 SPLADE 模型在 BERT 之上引入了一个可训练的神经网络,并选择了特定的架构使其完美地适应任务。
  • 最后,SPLADE 模型家族使用知识蒸馏,即从一个更大(因此慢得多,不太适合生产任务)的模型中学习如何预测良好的表示。

SPLADE 模型家族的最新版本之一是 SPLADE++
与 SPARTA 模型不同,SPLADE++ 在推理时不仅扩展文档,还扩展查询。我们将在下一节中演示这一点。

Qdrant 中的 SPLADE++

在 Qdrant 中,您可以使用我们轻量级的嵌入库 FastEmbed 轻松使用 SPLADE++

设置

安装 FastEmbed

pip install fastembed

导入 FastEmbed 支持的稀疏文本嵌入模型。

from fastembed import SparseTextEmbedding

您可以列出当前支持的所有稀疏文本嵌入模型。

SparseTextEmbedding.list_supported_models()
包含支持模型列表的输出
[{'model': 'prithivida/Splade_PP_en_v1',
  'vocab_size': 30522,
  'description': 'Independent Implementation of SPLADE++ Model for English',
  'size_in_GB': 0.532,
  'sources': {'hf': 'Qdrant/SPLADE_PP_en_v1'},
  'model_file': 'model.onnx'},
 {'model': 'prithvida/Splade_PP_en_v1',
  'vocab_size': 30522,
  'description': 'Independent Implementation of SPLADE++ Model for English',
  'size_in_GB': 0.532,
  'sources': {'hf': 'Qdrant/SPLADE_PP_en_v1'},
  'model_file': 'model.onnx'},
 {'model': 'Qdrant/bm42-all-minilm-l6-v2-attentions',
  'vocab_size': 30522,
  'description': 'Light sparse embedding model, which assigns an importance score to each token in the text',
  'size_in_GB': 0.09,
  'sources': {'hf': 'Qdrant/all_miniLM_L6_v2_with_attentions'},
  'model_file': 'model.onnx',
  'additional_files': ['stopwords.txt'],
  'requires_idf': True},
 {'model': 'Qdrant/bm25',
  'description': 'BM25 as sparse embeddings meant to be used with Qdrant',
  'size_in_GB': 0.01,
  'sources': {'hf': 'Qdrant/bm25'},
  'model_file': 'mock.file',
  'additional_files': ['arabic.txt',
   'azerbaijani.txt',
   'basque.txt',
   'bengali.txt',
   'catalan.txt',
   'chinese.txt',
   'danish.txt',
   'dutch.txt',
   'english.txt',
   'finnish.txt',
   'french.txt',
   'german.txt',
   'greek.txt',
   'hebrew.txt',
   'hinglish.txt',
   'hungarian.txt',
   'indonesian.txt',
   'italian.txt',
   'kazakh.txt',
   'nepali.txt',
   'norwegian.txt',
   'portuguese.txt',
   'romanian.txt',
   'russian.txt',
   'slovene.txt',
   'spanish.txt',
   'swedish.txt',
   'tajik.txt',
   'turkish.txt'],
  'requires_idf': True}]

加载 SPLADE++。

sparse_model_name = "prithivida/Splade_PP_en_v1"
sparse_model = SparseTextEmbedding(model_name=sparse_model_name)

模型文件将被获取并下载,显示进度。

嵌入数据

我们将使用一个示例电影描述数据集。

电影描述数据集
descriptions = ["In 1431, Jeanne d'Arc is placed on trial on charges of heresy. The ecclesiastical jurists attempt to force Jeanne to recant her claims of holy visions.",
 "A film projectionist longs to be a detective, and puts his meagre skills to work when he is framed by a rival for stealing his girlfriend's father's pocketwatch.",
 "A group of high-end professional thieves start to feel the heat from the LAPD when they unknowingly leave a clue at their latest heist.",
 "A petty thief with an utter resemblance to a samurai warlord is hired as the lord's double. When the warlord later dies the thief is forced to take up arms in his place.",
 "A young boy named Kubo must locate a magical suit of armour worn by his late father in order to defeat a vengeful spirit from the past.",
 "A biopic detailing the 2 decades that Punjabi Sikh revolutionary Udham Singh spent planning the assassination of the man responsible for the Jallianwala Bagh massacre.",
 "When a machine that allows therapists to enter their patients' dreams is stolen, all hell breaks loose. Only a young female therapist, Paprika, can stop it.",
 "An ordinary word processor has the worst night of his life after he agrees to visit a girl in Soho whom he met that evening at a coffee shop.",
 "A story that revolves around drug abuse in the affluent north Indian State of Punjab and how the youth there have succumbed to it en-masse resulting in a socio-economic decline.",
 "A world-weary political journalist picks up the story of a woman's search for her son, who was taken away from her decades ago after she became pregnant and was forced to live in a convent.",
 "Concurrent theatrical ending of the TV series Neon Genesis Evangelion (1995).",
 "During World War II, a rebellious U.S. Army Major is assigned a dozen convicted murderers to train and lead them into a mass assassination mission of German officers.",
 "The toys are mistakenly delivered to a day-care center instead of the attic right before Andy leaves for college, and it's up to Woody to convince the other toys that they weren't abandoned and to return home.",
 "A soldier fighting aliens gets to relive the same day over and over again, the day restarting every time he dies.",
 "After two male musicians witness a mob hit, they flee the state in an all-female band disguised as women, but further complications set in.",
 "Exiled into the dangerous forest by her wicked stepmother, a princess is rescued by seven dwarf miners who make her part of their household.",
 "A renegade reporter trailing a young runaway heiress for a big story joins her on a bus heading from Florida to New York, and they end up stuck with each other when the bus leaves them behind at one of the stops.",
 "Story of 40-man Turkish task force who must defend a relay station.",
 "Spinal Tap, one of England's loudest bands, is chronicled by film director Marty DiBergi on what proves to be a fateful tour.",
 "Oskar, an overlooked and bullied boy, finds love and revenge through Eli, a beautiful but peculiar girl."]

使用 SPLADE++ 嵌入电影描述。

sparse_descriptions = list(sparse_model.embed(descriptions))

您可以检查 SPLADE++ 生成的稀疏向量在 Qdrant 中的样子。

sparse_descriptions[0]

它存储为非零权重的 BERT token 的索引以及这些权重的

SparseEmbedding(
  values=array([1.57449973, 0.90787691, ..., 1.21796167, 1.1321187]),
  indices=array([ 1040,  2001, ..., 28667, 29137])
)

将嵌入上传到 Qdrant

安装 qdrant-client

pip install qdrant-client

Qdrant 客户端有一个简单的内存模式,允许您在小数据量上进行本地实验。或者,您可以选择在 Qdrant Cloud 中使用免费层集群进行实验。

from qdrant_client import QdrantClient, models
qdrant_client = QdrantClient(":memory:") # Qdrant is running from RAM.

现在,让我们创建一个集合,用于上传我们的稀疏 SPLADE++ 嵌入。
为此,我们将使用 Qdrant 支持的稀疏向量表示。

qdrant_client.create_collection(
    collection_name="movies",
    vectors_config={},
    sparse_vectors_config={
        "film_description": models.SparseVectorParams(),
    },
)

为了使这个集合易于理解,让我们将电影元数据(名称、描述和电影时长)与嵌入一起保存。

电影元数据
metadata = [{"movie_name": "The Passion of Joan of Arc", "movie_watch_time_min": 114, "movie_description": "In 1431, Jeanne d'Arc is placed on trial on charges of heresy. The ecclesiastical jurists attempt to force Jeanne to recant her claims of holy visions."},
{"movie_name": "Sherlock Jr.", "movie_watch_time_min": 45, "movie_description": "A film projectionist longs to be a detective, and puts his meagre skills to work when he is framed by a rival for stealing his girlfriend's father's pocketwatch."},
{"movie_name": "Heat", "movie_watch_time_min": 170, "movie_description": "A group of high-end professional thieves start to feel the heat from the LAPD when they unknowingly leave a clue at their latest heist."},
{"movie_name": "Kagemusha", "movie_watch_time_min": 162, "movie_description": "A petty thief with an utter resemblance to a samurai warlord is hired as the lord's double. When the warlord later dies the thief is forced to take up arms in his place."},
{"movie_name": "Kubo and the Two Strings", "movie_watch_time_min": 101, "movie_description": "A young boy named Kubo must locate a magical suit of armour worn by his late father in order to defeat a vengeful spirit from the past."},
{"movie_name": "Sardar Udham", "movie_watch_time_min": 164, "movie_description": "A biopic detailing the 2 decades that Punjabi Sikh revolutionary Udham Singh spent planning the assassination of the man responsible for the Jallianwala Bagh massacre."},
{"movie_name": "Paprika", "movie_watch_time_min": 90, "movie_description": "When a machine that allows therapists to enter their patients' dreams is stolen, all hell breaks loose. Only a young female therapist, Paprika, can stop it."},
{"movie_name": "After Hours", "movie_watch_time_min": 97, "movie_description": "An ordinary word processor has the worst night of his life after he agrees to visit a girl in Soho whom he met that evening at a coffee shop."},
{"movie_name": "Udta Punjab", "movie_watch_time_min": 148, "movie_description": "A story that revolves around drug abuse in the affluent north Indian State of Punjab and how the youth there have succumbed to it en-masse resulting in a socio-economic decline."},
{"movie_name": "Philomena", "movie_watch_time_min": 98, "movie_description": "A world-weary political journalist picks up the story of a woman's search for her son, who was taken away from her decades ago after she became pregnant and was forced to live in a convent."},
{"movie_name": "Neon Genesis Evangelion: The End of Evangelion", "movie_watch_time_min": 87, "movie_description": "Concurrent theatrical ending of the TV series Neon Genesis Evangelion (1995)."},
{"movie_name": "The Dirty Dozen", "movie_watch_time_min": 150, "movie_description": "During World War II, a rebellious U.S. Army Major is assigned a dozen convicted murderers to train and lead them into a mass assassination mission of German officers."},
{"movie_name": "Toy Story 3", "movie_watch_time_min": 103, "movie_description": "The toys are mistakenly delivered to a day-care center instead of the attic right before Andy leaves for college, and it's up to Woody to convince the other toys that they weren't abandoned and to return home."},
{"movie_name": "Edge of Tomorrow", "movie_watch_time_min": 113, "movie_description": "A soldier fighting aliens gets to relive the same day over and over again, the day restarting every time he dies."},
{"movie_name": "Some Like It Hot", "movie_watch_time_min": 121, "movie_description": "After two male musicians witness a mob hit, they flee the state in an all-female band disguised as women, but further complications set in."},
{"movie_name": "Snow White and the Seven Dwarfs", "movie_watch_time_min": 83, "movie_description": "Exiled into the dangerous forest by her wicked stepmother, a princess is rescued by seven dwarf miners who make her part of their household."},
{"movie_name": "It Happened One Night", "movie_watch_time_min": 105, "movie_description": "A renegade reporter trailing a young runaway heiress for a big story joins her on a bus heading from Florida to New York, and they end up stuck with each other when the bus leaves them behind at one of the stops."},
{"movie_name": "Nefes: Vatan Sagolsun", "movie_watch_time_min": 128, "movie_description": "Story of 40-man Turkish task force who must defend a relay station."},
{"movie_name": "This Is Spinal Tap", "movie_watch_time_min": 82, "movie_description": "Spinal Tap, one of England's loudest bands, is chronicled by film director Marty DiBergi on what proves to be a fateful tour."},
{"movie_name": "Let the Right One In", "movie_watch_time_min": 114, "movie_description": "Oskar, an overlooked and bullied boy, finds love and revenge through Eli, a beautiful but peculiar girl."}]

将带有电影元数据的嵌入描述上传到集合中。

qdrant_client.upsert(
    collection_name="movies",
    points=[
        models.PointStruct(
            id=idx,
            payload=metadata[idx],
            vector={
                "film_description": models.SparseVector(
                    indices=vector.indices,
                    values=vector.values
                )
            },
        )
        for idx, vector in enumerate(sparse_descriptions)
    ],
)
隐式生成稀疏向量 (点击展开)
qdrant_client.upsert(
    collection_name="movies",
    points=[
        models.PointStruct(
            id=idx,
            payload=metadata[idx],
            vector={
                "film_description": models.Document(
                    text=description, model=sparse_model_name
                )
            },
        )
        for idx, description in enumerate(descriptions)
    ],
)

查询

让我们查询我们的集合!

query_embedding = list(sparse_model.embed("A movie about music"))[0]

response = qdrant_client.query_points(
    collection_name="movies",
    query=models.SparseVector(indices=query_embedding.indices, values=query_embedding.values),
    using="film_description",
    limit=1,
    with_vectors=True,
    with_payload=True
)
print(response)
隐式生成稀疏向量 (点击展开)
response = qdrant_client.query_points(
    collection_name="movies",
    query=models.Document(text="A movie about music", model=sparse_model_name),
    using="film_description",
    limit=1,
    with_vectors=True,
    with_payload=True,
)
print(response)

输出如下所示

points=[ScoredPoint(
  id=18, 
  version=0, 
  score=9.6779785, 
  payload={
    'movie_name': 'This Is Spinal Tap', 
    'movie_watch_time_min': 82, 
    'movie_description': "Spinal Tap, one of England's loudest bands, 
    is chronicled by film director Marty DiBergi on what proves to be a fateful tour."
  }, 
  vector={
    'film_description': SparseVector(
      indices=[1010, 2001, ..., 25316, 25517], 
      values=[0.49717945, 0.19760133, ..., 1.2124698, 0.58689135])
  }, 
  shard_key=None, 
  order_value=None
)]

正如您所见,查询和找到的电影描述中没有重叠的词语,尽管答案符合查询,但我们正在使用的是精确匹配
这之所以可能,是因为 SPLADE++ 对查询和文档进行了内部扩展

SPLADE++ 的内部扩展

让我们看看 SPLADE++ 如何扩展查询以及我们获得的作为答案的文档。
为此,我们将需要使用 HuggingFace 库 Tokenizers。借助它,我们将能够将 SPLADE++ 使用的词汇表中词语的索引解码回人类可读的格式。

首先,我们需要安装这个库。

pip install tokenizers

然后,我们编写一个函数,它将解码 SPLADE++ 稀疏嵌入,并返回 SPLADE++ 用于编码输入的词语。
我们希望根据 SPLADE++ 分配给它们的权重(影响力得分),按降序返回它们。

from tokenizers import Tokenizer

tokenizer = Tokenizer.from_pretrained('Qdrant/SPLADE_PP_en_v1')

def get_tokens_and_weights(sparse_embedding, tokenizer):
    token_weight_dict = {}
    for i in range(len(sparse_embedding.indices)):
        token = tokenizer.decode([sparse_embedding.indices[i]])
        weight = sparse_embedding.values[i]
        token_weight_dict[token] = weight

    # Sort the dictionary by weights
    token_weight_dict = dict(sorted(token_weight_dict.items(), key=lambda item: item[1], reverse=True))
    return token_weight_dict

首先,我们将函数应用于查询。

query_embedding = list(sparse_model.embed("A movie about music"))[0]
print(get_tokens_and_weights(query_embedding, tokenizer))

SPLADE++ 就是这样扩展查询的

{
    "music": 2.764289617538452,
    "movie": 2.674748420715332,
    "film": 2.3489091396331787,
    "musical": 2.276120901107788,
    "about": 2.124547004699707,
    "movies": 1.3825485706329346,
    "song": 1.2893378734588623,
    "genre": 0.9066758751869202,
    "songs": 0.8926399946212769,
    "a": 0.8900706768035889,
    "musicians": 0.5638002157211304,
    "sound": 0.49310919642448425,
    "musician": 0.46415239572525024,
    "drama": 0.462990403175354,
    "tv": 0.4398191571235657,
    "book": 0.38950803875923157,
    "documentary": 0.3758136034011841,
    "hollywood": 0.29099565744400024,
    "story": 0.2697228491306305,
    "nature": 0.25306591391563416,
    "concerning": 0.205053448677063,
    "game": 0.1546829640865326,
    "rock": 0.11775632947683334,
    "definition": 0.08842901140451431,
    "love": 0.08636035025119781,
    "soundtrack": 0.06807517260313034,
    "religion": 0.053535860031843185,
    "filmed": 0.025964470580220222,
    "sounds": 0.0004048719711136073
}

然后,我们将函数应用于答案。

query_embedding = list(sparse_model.embed("A movie about music"))[0]

response = qdrant_client.query_points(
    collection_name="movies",
    query=models.SparseVector(indices=query_embedding.indices, values=query_embedding.values),
    using="film_description",
    limit=1,
    with_vectors=True,
    with_payload=True
)

print(get_tokens_and_weights(response.points[0].vector['film_description'], tokenizer))
隐式生成稀疏向量 (点击展开)
response = qdrant_client.query_points(
    collection_name="movies",
    query=models.Document(text="A movie about music", model=sparse_model_name),
    using="film_description",
    limit=1,
    with_vectors=True,
    with_payload=True,
)

print(get_tokens_and_weights(response.points[0].vector["film_description"], tokenizer))

SPLADE++ 就是这样扩展答案的。

{'spinal': 2.6548674, 'tap': 2.534881, 'marty': 2.223297, '##berg': 2.0402722, 
'##ful': 2.0030282, 'fate': 1.935915, 'loud': 1.8381964, 'spine': 1.7507898, 
'di': 1.6161551, 'bands': 1.5897619, 'band': 1.589473, 'uk': 1.5385966, 'tour': 1.4758654, 
'chronicle': 1.4577943, 'director': 1.4423795, 'england': 1.4301306, '##est': 1.3025658, 
'taps': 1.2124698, 'film': 1.1069428, '##berger': 1.1044296, 'tapping': 1.0424755, 'best': 1.0327196, 
'louder': 0.9229055, 'music': 0.9056678, 'directors': 0.8887502, 'movie': 0.870712, 'directing': 0.8396196, 
'sound': 0.83609974, 'genre': 0.803052, 'dave': 0.80212915, 'wrote': 0.7849579, 'hottest': 0.7594193, 'filmed': 0.750105, 
'english': 0.72807616, 'who': 0.69502294, 'tours': 0.6833075, 'club': 0.6375339, 'vertebrae': 0.58689135, 'chronicles': 0.57296354, 
'dance': 0.57278687, 'song': 0.50987065, ',': 0.49717945, 'british': 0.4971719, 'writer': 0.495709, 'directed': 0.4875775, 
'cork': 0.475757, '##i': 0.47122696, '##band': 0.46837863, 'most': 0.44112885, '##liest': 0.44084555, 'destiny': 0.4264851, 
'prove': 0.41789067, 'is': 0.40306947, 'famous': 0.40230379, 'hop': 0.3897451, 'noise': 0.38770816, '##iest': 0.3737782, 
'comedy': 0.36903998, 'sport': 0.35883865, 'quiet': 0.3552795, 'detail': 0.3397654, 'fastest': 0.30345848, 'filmmaker': 0.3013101, 
'festival': 0.28146765, '##st': 0.28040633, 'tram': 0.27373192, 'well': 0.2599603, 'documentary': 0.24368097, 'beat': 0.22953634, 
'direction': 0.22925079, 'hardest': 0.22293334, 'strongest': 0.2018861, 'was': 0.19760133, 'oldest': 0.19532987, 
'byron': 0.19360808, 'worst': 0.18397793, 'touring': 0.17598206, 'rock': 0.17319143, 'clubs': 0.16090117, 
'popular': 0.15969758, 'toured': 0.15917331, 'trick': 0.1530599, 'celebrity': 0.14458777, 'musical': 0.13888633, 
'filming': 0.1363699, 'culture': 0.13616633, 'groups': 0.1340591, 'ski': 0.13049376, 'venue': 0.12992987, 
'style': 0.12853126, 'history': 0.12696269, 'massage': 0.11969914, 'theatre': 0.11673525, 'sounds': 0.108338095, 
'visit': 0.10516077, 'editing': 0.078659914, 'death': 0.066746496, 'massachusetts': 0.055702563, 'stuart': 0.0447934, 
'romantic': 0.041140396, 'pamela': 0.03561337, 'what': 0.016409796, 'smallest': 0.010815808, 'orchestra': 0.0020691194}

由于扩展,查询和文档在“音乐”、“电影”、“声音”等方面存在重叠,因此精确匹配有效。

关键要点:何时选择稀疏神经模型进行检索

稀疏神经检索有意义

  • 在关键词匹配至关重要但 BM25 不足以进行初始检索的领域,语义匹配(例如,同义词、同音异义词)能显著增加价值。这在医学、学术、法律和电子商务等领域尤为如此,这些领域中品牌名称和序列号起着关键作用。密集检索器往往返回许多误报(false positives),而稀疏神经检索有助于减少这些误报。

  • 稀疏神经检索是实现规模化的一个有价值的选择,尤其是在处理大型数据集时。它利用倒排索引进行精确匹配,这取决于您的数据特性,速度可以非常快。

  • 如果您正在使用传统的检索系统,稀疏神经检索与之兼容,并有助于弥合语义差距。

此页面有用吗?

感谢您的反馈!🙏

很抱歉听到这个消息。😔 您可以在 GitHub 上编辑此页面,或创建一个 GitHub issue。