什么是句子嵌入、交叉编码器和重新排名


深入探讨嵌入并解释双编码器和交叉编码器之间的差异,然后,我们将深入研究检索和重新排名。

什么是双编码器和交叉编码器?
Sentence Transformers 支持两种类型的模型:双编码器和交叉编码器。
双编码器速度更快、可扩展性更强,但交叉编码器更准确。

尽管两者都处理类似的高级任务,但何时使用其中之一却有很大不同:
双编码器更适合搜索,交叉编码器更适合分类和高精度排名。

双编码器
双编码器是将输入文本编码为固定长度向量的模型。当计算两个句子之间的相似度时,我们通常将两个句子编码为两个向量,然后计算两个向量之间的相似度(例如,通过使用余弦相似度)。我们训练双编码器来优化查询和相关句子之间相似度的增加,并减少查询和其他句子之间的相似度。这就是为什么双编码器更适合搜索。

双编码器速度快且易于扩展。如果提供多个​​句子,双编码器将独立编码每个句子。这意味着句子嵌入是相互独立的。这对于搜索来说是一件好事,因为我们可以并行编码数百万个句子。然而,这也意味着双编码器不知道句子之间的关系。

交叉编码器
当我们使用交叉编码器时,我们会做一些不同的事情。交叉编码器同时对两个句子进行编码,然后输出分类分数。

交叉编码器速度较慢且占用内存较多,但也更加准确。

交叉编码器是比较几十个句子的绝佳选择。如果您想比较数十万个句子,双编码器是更好的选择,否则交叉编码器可能需要几个小时。
如果您关心准确性并希望有效地比较数千个句子怎么办?
这是您想要检索信息时的典型情况。

在这些情况下,一个选项是首先使用双编码器来减少候选数量(即,获取前 20 个最相关的示例),然后使用交叉编码器来获取最终结果。这称为重新排序,是信息检索中的常用技术。

鉴于交叉编码器更准确,对于细微差别很重要的任务来说,它也是一个不错的选择,例如医学或法律文档,其中措辞的细微差别可能会改变句子的含义。

交叉编码器同时对两个文本进行编码,然后输出分类标签。交叉编码器首先生成一个捕获表示及其关系的嵌入。与双编码器生成的嵌入(彼此独立)相比,跨编码器嵌入是彼此依赖的。这就是为什么交叉编码器更适合分类,而且它们的质量更高:它们可以捕获两个句子之间的关系!另一方面,如果您需要比较数千个句子,交叉编码器会很慢,因为它们需要对所有句子对进行编码。
假设您有四个句子,您需要比较所有可能的对:

  • 双编码器需要独立编码每个句子,因此需要编码四个句子。
  • 交叉编码器需要对所有可能的对进行编码,因此需要对六个句子(AB、AC、AD、BC、BD、CD)进行编码。

例如,对于段落检索(给定一个问题和一个段落,该段落与该问题相关吗?)。让我们看一下一个快速代码片段,其中包含为此训练的小型交叉编码器模型:

!pip install sentence_transformers datasets

from sentence_transformers import CrossEncoder

model = CrossEncoder('cross-encoder/ms-marco-TinyBERT-L-2-v2', max_length=512)
scores = model.predict([('How many people live in Berlin?', 'Berlin had a population of 3,520,031 registered inhabitants in an area of 891.82 square kilometers.'), 
                        ('How many people live in Berlin?', 'Berlin is well known for its museums.')])
scores

分数:
array([ 7.152365 , -6.2870445], dtype=float32)
另一个用例,与我们对双编码器所做的更相似,是使用交叉编码器来实现语义相似性。例如,给定两个句子,它们在语义上相似吗?尽管这与我们使用双编码器解决的任务相同,但请记住,交叉编码器更准确但速度更慢。

另一个用例,与我们对双编码器所做的更相似,是使用交叉编码器来实现语义相似性。例如,给定两个句子,它们在语义上相似吗?尽管这与我们使用双编码器解决的任务相同,但请记住,交叉编码器更准确但速度更慢。

model = CrossEncoder('cross-encoder/stsb-TinyBERT-L-4')
scores = model.predict([("The weather today is beautiful", "It's raining!"), 
                        (
"The weather today is beautiful", "Today is a sunny day")])
scores

array([0.46552283, 0.6350213 ], dtype=float32)

检索并重新排名
现在我们已经了解了交叉编码器和双编码器之间的差异,让我们看看如何通过两阶段检索和重新排序系统在实践中使用它们。这是信息检索中的常见技术,首先检索最相关的文档,然后使用更准确的模型对它们重新排序。这是有效比较数千个句子并关心准确性的好选择。

假设您有一个包含 100,000 个句子的语料库,并且想要找到与给定查询最相关的句子。第一步是使用双编码器来检索许多候选者(以确保召回)。然后,您使用交叉编码器对候选者重新排序并获得高精度的最终结果。

让我们通过实现论文搜索系统来试试运气吧!我们将在Pinecone的关于重新排名的优秀教程中使用AI Arxiv 数据集。目标是能够提出人工智能问题并获得相关论文部分来回答问题。

from datasets import load_dataset

dataset = load_dataset("jamescalam/ai-arxiv-chunked")
dataset[
"train"]

Found cached dataset json (/home/osanseviero/.cache/huggingface/datasets/jamescalam___json/jamescalam--ai-arxiv-chunked-0d76bdc6812ffd50/0.0.0/8bb11242116d547c741b2e8a1f18598ffdd40a1d4f2a2872c7a28b697434bc96)
Dataset({
    features: ['doi', 'chunk-id', 'chunk', 'id', 'title', 'summary', 'source', 'authors', 'categories', 'comment', 'journal_ref', 'primary_category', 'published', 'updated', 'references'],
    num_rows: 41584
})

如果您查看该数据集,您会发现它是由 400 篇 Arxiv 论文组成的分块数据集。分块意味着部分被分成更少标记的块/片段,以使模型更易于管理。这是一个示例:

dataset["train"][0]

{'doi': '1910.01108',
 'chunk-id': '0',
 'chunk': 'DistilBERT, a distilled version of BERT: smaller,\nfaster, cheaper and lighter\nVictor SANH, Lysandre DEBUT, Julien CHAUMOND, Thomas WOLF\nHugging Face\n{victor,lysandre,julien,thomas}@huggingface.co\nAbstract\nAs Transfer Learning from large-scale pre-trained models becomes more prevalent\nin Natural Language Processing (NLP), operating these large models in on-theedge and/or under constrained computational training or inference budgets remains\nchallenging. In this work, we propose a method to pre-train a smaller generalpurpose language representation model, called DistilBERT, which can then be finetuned with good performances on a wide range of tasks like its larger counterparts.\nWhile most prior work investigated the use of distillation for building task-specific\nmodels, we leverage knowledge distillation during the pre-training phase and show\nthat it is possible to reduce the size of a BERT model by 40%, while retaining 97%\nof its language understanding capabilities and being 60% faster. To leverage the\ninductive biases learned by larger models during pre-training, we introduce a triple\nloss combining language modeling, distillation and cosine-distance losses. Our\nsmaller, faster and lighter model is cheaper to pre-train and we demonstrate its',
 'id': '1910.01108',
 'title': 'DistilBERT, a distilled version of BERT: smaller, faster, cheaper and lighter',
 'summary': 'As Transfer Learning from large-scale pre-trained models becomes more\nprevalent in Natural Language Processing (NLP), operating these large models in\non-the-edge and/or under constrained computational training or inference\nbudgets remains challenging. In this work, we propose a method to pre-train a\nsmaller general-purpose language representation model, called DistilBERT, which\ncan then be fine-tuned with good performances on a wide range of tasks like its\nlarger counterparts. While most prior work investigated the use of distillation\nfor building task-specific models, we leverage knowledge distillation during\nthe pre-training phase and show that it is possible to reduce the size of a\nBERT model by 40%, while retaining 97% of its language understanding\ncapabilities and being 60% faster. To leverage the inductive biases learned by\nlarger models during pre-training, we introduce a triple loss combining\nlanguage modeling, distillation and cosine-distance losses. Our smaller, faster\nand lighter model is cheaper to pre-train and we demonstrate its capabilities\nfor on-device computations in a proof-of-concept experiment and a comparative\non-device study.',
 'source': 'http://arxiv.org/pdf/1910.01108',
 'authors': ['Victor Sanh',
  'Lysandre Debut',
  'Julien Chaumond',
  'Thomas Wolf'],
 'categories': ['cs.CL'],
 'comment': 'February 2020 - Revision: fix bug in evaluation metrics, updated\n  metrics, argumentation unchanged. 5 pages, 1 figure, 4 tables. Accepted at\n  the 5th Workshop on Energy Efficient Machine Learning and Cognitive Computing\n  - NeurIPS 2019',
 'journal_ref': None,
 'primary_category': 'cs.CL',
 'published': '20191002',
 'updated': '20200301',
 'references': [{'id': '1910.01108'}]}


让我们获取所有要编码的块:

chunks = dataset["train"]["chunk"
len(chunks)

41584

现在,我们将使用双编码器将所有块编码为嵌入。我们会将长段落截断为 512 个标记。请注意,短上下文是许多嵌入模型的缺点之一!我们将专门使用multi-qa-MiniLM-L6-cos-v1模型,这是一个小型模型,经过训练将问题和段落编码到类似的嵌入空间中。该模型是双编码器,因此速度快且可扩展。

在我的普通计算机上嵌入所有 40,000 多个段落大约需要 30 秒。请注意,我们只需要生成一次段落的嵌入,因为我们可以将它们保存到磁盘并稍后加载。在生产设置中,您可以将嵌入保存到数据库并从那里加载。

from sentence_transformers import SentenceTransformer

bi_encoder = SentenceTransformer('multi-qa-MiniLM-L6-cos-v1')
bi_encoder.max_seq_length = 256

corpus_embeddings = bi_encoder.encode(chunks, convert_to_tensor=True, show_progress_bar=True)

惊人的!现在,我们提出一个问题并搜索相关段落。为此,我们需要对问题进行编码,然后计算问题与所有段落之间的相似度。让我们这样做并看看热门歌曲!

from sentence_transformers import util

query = "what is rlhf?"
top_k = 25 # how many chunks to retrieve
query_embedding = bi_encoder.encode(query, convert_to_tensor=True).cuda()

hits = util.semantic_search(query_embedding, corpus_embeddings, top_k=top_k)[0]
hits

[{'corpus_id': 14679, 'score': 0.6097552180290222},
 {'corpus_id': 17387, 'score': 0.5659530162811279},
 {'corpus_id': 39564, 'score': 0.5590510368347168},
 {'corpus_id': 14725, 'score': 0.5585878491401672},
 {'corpus_id': 5628, 'score': 0.5296251773834229},
 {'corpus_id': 14802, 'score': 0.5075011253356934},
 {'corpus_id': 9761, 'score': 0.49943411350250244},
 {'corpus_id': 14716, 'score': 0.4931946098804474},
 {'corpus_id': 9763, 'score': 0.49280521273612976},
 {'corpus_id': 20638, 'score': 0.4884325861930847},
 {'corpus_id': 20653, 'score': 0.4873950183391571},
 {'corpus_id': 9755, 'score': 0.48562008142471313},
 {'corpus_id': 14806, 'score': 0.4792214035987854},
 {'corpus_id': 14805, 'score': 0.475425660610199},
 {'corpus_id': 20652, 'score': 0.4740477204322815},
 {'corpus_id': 20711, 'score': 0.4703512489795685},
 {'corpus_id': 20632, 'score': 0.4695567488670349},
 {'corpus_id': 14750, 'score': 0.46810320019721985},
 {'corpus_id': 14749, 'score': 0.46809980273246765},
 {'corpus_id': 35209, 'score': 0.46695172786712646},
 {'corpus_id': 14671, 'score': 0.46657535433769226},
 {'corpus_id': 14821, 'score': 0.4637290835380554},
 {'corpus_id': 14751, 'score': 0.4585301876068115},
 {'corpus_id': 14815, 'score': 0.45775431394577026},
 {'corpus_id': 35250, 'score': 0.4569615125656128}]

Let's store the IDs for later
retrieval_corpus_ids = [hit['corpus_id'] for hit in hits]

# Now let's print the top 3 results
for i, hit in enumerate(hits[:3]):
    sample = dataset["train"][hit["corpus_id"]]
    print(f
"Top {i+1} passage with score {hit['score']} from {sample['source']}:")
    print(sample[
"chunk"])
    print(
"\n")

我们根据高召回率但低精度的双编码器得到了最相似的块。

现在,让我们使用更高精度的交叉编码器模型重新排名。我们将使用交叉编码器/ms-marco-MiniLM-L-6-v2模型。该模型使用 MS MARCO Passage Retrieval 数据集进行训练,这是一个包含真实搜索问题及其相关文本段落的大型数据集。这使得该模型非常适合使用问题和段落进行预测。
我们将使用相同的问题和从双编码器获得的前 10 个块。让我们看看结果!回想一下,交叉编码器需要成对的,所以我们将创建问题和每个块的对。

from sentence_transformers import  CrossEncoder
cross_encoder = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

cross_inp = [[query, chunks[hit['corpus_id']]] for hit in hits]
cross_scores = cross_encoder.predict(cross_inp)
cross_scores

得到:

array([ 1.2227577 ,  5.048051  ,  1.2897239 ,  2.205767  ,  4.4136825 ,
        1.2272772 ,  2.5638275 ,  0.81847703,  2.35553   ,  5.590804  ,
        1.3877895 ,  2.9497519 ,  1.6762824 ,  0.7211323 ,  0.16303705,
        1.3640019 ,  2.3106787 ,  1.5849439 ,  2.9696884 , -1.1079378 ,
        0.7681126 ,  1.5945492 ,  2.2869687 ,  3.5448399 ,  2.056368  ],
      dtype=float32)

让我们用 添加一个新值cross-score并按它排序!

for idx in range(len(cross_scores)):
    hits[idx]['cross-score'] = cross_scores[idx]
hits = sorted(hits, key=lambda x: x['cross-score'], reverse=True)
msmarco_l6_corpus_ids = [hit['corpus_id'] for hit in hits] # save for later

hits

输出:
[{'corpus_id': 20638, 'score': 0.4884325861930847, 'cross-score': 5.590804},
 {'corpus_id': 17387, 'score': 0.5659530162811279, 'cross-score': 5.048051},
 {'corpus_id': 5628, 'score': 0.5296251773834229, 'cross-score': 4.4136825},
 {'corpus_id': 14815, 'score': 0.45775431394577026, 'cross-score': 3.5448399},
 {'corpus_id': 14749, 'score': 0.46809980273246765, 'cross-score': 2.9696884},
 {'corpus_id': 9755, 'score': 0.48562008142471313, 'cross-score': 2.9497519},
 {'corpus_id': 9761, 'score': 0.49943411350250244, 'cross-score': 2.5638275},
 {'corpus_id': 9763, 'score': 0.49280521273612976, 'cross-score': 2.35553},
 {'corpus_id': 20632, 'score': 0.4695567488670349, 'cross-score': 2.3106787},
 {'corpus_id': 14751, 'score': 0.4585301876068115, 'cross-score': 2.2869687},
 {'corpus_id': 14725, 'score': 0.5585878491401672, 'cross-score': 2.205767},
 {'corpus_id': 35250, 'score': 0.4569615125656128, 'cross-score': 2.056368},
 {'corpus_id': 14806, 'score': 0.4792214035987854, 'cross-score': 1.6762824},
 {'corpus_id': 14821, 'score': 0.4637290835380554, 'cross-score': 1.5945492},
 {'corpus_id': 14750, 'score': 0.46810320019721985, 'cross-score': 1.5849439},
 {'corpus_id': 20653, 'score': 0.4873950183391571, 'cross-score': 1.3877895},
 {'corpus_id': 20711, 'score': 0.4703512489795685, 'cross-score': 1.3640019},
 {'corpus_id': 39564, 'score': 0.5590510368347168, 'cross-score': 1.2897239},
 {'corpus_id': 14802, 'score': 0.5075011253356934, 'cross-score': 1.2272772},
 {'corpus_id': 14679, 'score': 0.6097552180290222, 'cross-score': 1.2227577},
 {'corpus_id': 14716, 'score': 0.4931946098804474, 'cross-score': 0.81847703},
 {'corpus_id': 14671, 'score': 0.46657535433769226, 'cross-score': 0.7681126},
 {'corpus_id': 14805, 'score': 0.475425660610199, 'cross-score': 0.7211323},
 {'corpus_id': 20652, 'score': 0.4740477204322815, 'cross-score': 0.16303705},
 {'corpus_id': 35209, 'score': 0.46695172786712646, 'cross-score': -1.1079378}]

更多点击标题

LLM 作为重新排名者
有些人使用生成式大模型作为重新排序器。例如,OpenAI 的 Coobook有一个示例,其中他们使用 GPT-3 作为重新排序器,通过构建提示要求模型确定文档是否与该文档相关。尽管这显示了大模型令人印象深刻的能力,但它通常不是该任务的最佳选择,因为它可能比交叉编码器质量更差、更昂贵且速度更慢。
进行实验,看看什么最适合您的数据。如果您的文档具有很长的上下文(基于 bert 的模型对此很困难),那么使用 LLM 作为重新排名有时会很有帮助。

SPECTRE2
如果您对科学任务的嵌入特别感兴趣,我建议您查看AllenAI 的SPECTRE2,这是一个为科学论文生成嵌入的模型系列。这些模型可用于执行诸如预测链接、查找最近的论文、查找给定查询的候选论文、使用嵌入作为特征对论文进行分类等操作!

基本模型在scirepeval上进行训练,这是一个包含数百万个科学论文引用三元组的数据集。经过训练后,作者使用适配器对模型进行了微调,适配器是一个用于参数高效微调的库(如果您不知道这是什么,请不要担心)。作者将一个称为适配器的小型神经网络附加到基本模型上。该适配器经过训练以执行特定任务,但特定任务的训练所需的数据比训练整个模型少得多。由于这些差异,人们需要使用transformers并adapters运行推理,例如通过执行类似的操作

model = AutoAdapterModel.from_pretrained('allenai/specter2_base')
model.load_adapter("allenai/specter2", source="hf", load_as="proximity", set_active=True)

增强型 SBERT
增强型 SBERT是一种收集数据以改进双编码器的技术。预训练和微调双编码器需要大量数据,因此作者建议使用交叉编码器来标记大量输入对并将其添加到训练数据中。例如,如果标记数据很少,则可以训练交叉编码器,然后标记未标记的对,这可用于训练双编码器。

你如何生成这些对?我们可以使用句子的随机组合,然后使用交叉编码器对它们进行标记。这将导致大部分为负对并扭曲标签分布。为了避免这种情况,作者探索了不同的技术:

  • 通过核密度估计(KDE),目标是在小型黄金数据集和增强数据集之间具有相似的标签分布。这是通过丢弃一些负对来实现的。当然,这将是低效的,因为您需要生成许多对才能获得一些积极的对。
  • BM25是搜索引擎中使用的基于重叠(例如词频、文档长度等)的算法。在此基础上,作者通过获取top-k个相似句子来检索k个最相似的句子,然后使用交叉编码器对它们进行标记。这是有效的,但只有在句子之间几乎没有重叠的情况下才能捕获语义相似性。
  • 语义搜索采样在黄金数据上训练双编码器,然后用于对其他相似对进行采样。
  • BM25 + 语义搜索采样结合了前面的两种方法。这有助于找到词汇和语义相似的句子。

Sentence Transformers 文档中有很好的图形和示例脚本来执行此操作。