Type something to search...
精通重排序模型:优化结果的5个关键步骤

精通重排序模型:优化结果的5个关键步骤

好久没写博客了,很高兴能再次开始!

在我之前的文章中,我们探讨了微调嵌入模型的世界——这是改进检索系统的关键一步。今天,我们将更进一步,深入研究微调重新排序模型。虽然嵌入可以帮助我们检索相关文档,但重新排序器会优化这些结果,以确保最准确和上下文相关的匹配。

在本文中,我将向您介绍我的方法,重点是准备数据和微调自定义重新排序器。

让我们开始吧。

Cross Encoders

Cross-encoders 是一种神经网络架构,主要用于自然语言处理 (NLP) 中需要理解两段文本之间关系的 任务,例如句子对。它们特别适用于语义相似性、问答和自然语言推理等任务。

Cross-Encoders 的工作原理:

它们的工作原理

输入对处理:

  • 将两段文本连接起来,通常用特殊标记(如 [SEP])分隔。例如,在 BERT 风格的模型中,格式通常是: [CLS] Text1 [SEP] Text2 [SEP]
  • 这种格式允许模型联合处理两个输入。

联合编码:

  • 与独立编码文本的双编码器不同,交叉编码器一起处理连接的序列,允许模型捕获两个文本之间的 token 级交互。
  • 这使得交叉编码器对于需要理解深层关系的任务更准确,但与双编码器相比,计算量也更大。

输出:

  • 输出通常源自 [CLS] token 的最终隐藏状态,它充当整个序列的表示。
  • 对于二元分类等任务,分数或概率可以指示关系(例如,语义相似性)。对于其他任务,额外的层可能会产生更复杂的输出。

用例:

  • 语义文本相似性 (STS):确定两个句子有多相似。
  • 自然语言推理 (NLI):对假设是蕴含、矛盾还是中立于前提进行分类。
  • 问答:根据候选答案与问题的相关性对其进行排名。
  • 信息检索:在初始检索步骤后对候选文档或段落进行重新排序。

Re-ranking

检索增强生成 (RAG) 中,重新排序是提高检索到的文档或段落的质量的关键步骤,然后再使用它们来生成最终答案。RAG 结合了基于检索的方法(从大型语料库中提取相关文档)和生成模型(根据检索到的内容生成答案)。重新排序有助于确保将最相关高质量的文档优先用于生成步骤。

为什么重新排序在 RAG 中很重要

重新排序的需求源于初始检索阶段的局限性。检索器,例如稀疏检索器(如 BM25)密集检索器(如双编码器),可能会返回大量候选文档,这些文档的与查询的相关性排名并不完美。重新排序通过使用更复杂的模型(例如交叉编码器)来改进检索到的文档的顺序来解决这个问题,以更好地评估每个文档与查询的相关性。通过将最相关的文档馈送到生成模型,最终输出(无论是答案还是摘要)都会变得更加准确和上下文相关。

重新排序在 RAG 中的工作原理

在实践中,重新排序作为多步骤流程的一部分工作。

初始检索:

  • 检索器(例如,BM25、DPR 或密集嵌入模型)根据输入查询从大型语料库中提取一组候选文档或段落。

重新排序:

  • 重新排序器(例如,交叉编码器)评估每个检索到的文档与查询的相关性。
  • 交叉编码器一起处理查询和每个文档,捕获细粒度的交互并产生相关性分数。
  • 文档根据这些分数重新排序。

生成:

  • 将排名前 k 位的重新排序文档传递给生成模型(例如,GPT、LLAMA 或 Qwen)。
  • 生成模型根据最相关的文档生成最终输出(例如,答案)。

微调重新排序模型

微调重新排序模型对于针对特定任务或领域优化其性能至关重要。虽然 BERT 或 RoBERTa 等预训练模型对语言有一般的理解,但它们可能无法针对任务的细微差别进行定制,例如根据与查询的相关性对文档进行排名。

微调 使模型适应任务提高准确性和相关性评分。它有助于模型学习特定于任务的关系、特定于领域的知识和隐式上下文连接,这对于准确的重新排序至关重要。

微调还可以使模型与目标数据分布保持一致。此过程增强了模型对输入数据中的噪声和变化的鲁棒性,例如拼写错误或释义查询。

微调对于专业领域(例如,医疗或法律)以及当有标记数据可用时尤其重要。通过微调,重新排序模型可以更好地优化检索到的文档,从而确保为RAG等系统中的问答等下游任务提供更高质量的输入。

数据集格式

在微调重排序模型时,使用两种常见的数据集格式:连续评分制离散类别制

连续评分制格式涉及句子或文本对,并带有连续的相关性评分(例如,介于 0 和 1 之间)。例如,使用 sentence-transformers 库中的 InputExample 类,您可以定义类似 ["sentence1", "sentence2"] 的对,并附带标签,如 0.30.8,表示两个文本之间的相关性或相似程度。这种格式非常适合对相关性进行分级的任务,例如语义文本相似度或文档排序。

示例:

train_samples = [
    InputExample(texts=["sentence1", "sentence2"], label=0.3),
    InputExample(texts=["Another", "pair"], label=0.8),
]

另一方面,离散类别制格式使用预定义的类别来表示句子对之间的关系。例如,在自然语言推理 (NLI) 任务中,句子对被标记为“矛盾”、“蕴含”或“中立”,这些类别被映射为整数值(例如,0、1、2)。这种格式适用于需要类别分类的任务,例如确定句子之间的逻辑关系。这两种格式都广泛用于微调重排序模型,选择哪种格式取决于具体的任务和数据的性质。

示例:

label2int = {"contradiction": 0, "entailment": 1, "neutral": 2}
train_samples = [
    InputExample(texts=["sentence1", "sentence2"], label=label2int["neutral"]),
    InputExample(texts=["Another", "pair"], label=label2int["entailment"]),
]

生成用于微调的合成数据

在微调重排序模型时,拥有高质量的训练数据至关重要。如果您可以访问来自已部署解决方案的真实数据,这是最佳选择。真实数据反映了您的系统处理的实际查询和文档,确保微调后的模型与您的特定用例保持一致。您可以从 LangfuseLangSmithLlamaTrace 等可观察性工具中收集这些数据,这些工具跟踪 LLM 应用程序中的交互,并提供对查询-文档对及其相关性的见解。

但是,如果无法获得真实数据,您可以生成合成数据来微调您的模型。我使用的方法是不寻常但有效的。以下是这个想法:

我利用一个 检索器(例如,向量存储)来获取一组查询的候选文档。然后,我使用 Cohere 的 rerank-v3.5(一种最先进的重排序模型)来优化检索到的文档并分配相关性评分。通过从这些重新排序的结果中抽样,我创建了一个数据集,该数据集模仿了高质量重排序器的行为。这种方法允许我的微调模型从一个更优越的模型的重排序模式中学习,从而有效地提炼其知识。

此外,我在抽样过程中引入了随机性,使模型能够接触到各种查询-文档对,确保它能够很好地泛化到各种场景。虽然合成数据可能无法完美地复制真实世界的分布,但当真实数据稀缺时,这种方法提供了一种实用的引导微调的方法。

Bge-reranker-base(我们的基础模型)

BAAI/bge-reranker-base 是一个高性能的交叉编码器模型,专为文本重排序任务而设计,由 北京智源人工智能研究院 (BAAI) 开发。它是 BAAI 通用嵌入 (BGE) 系列的一部分,该系列侧重于改进类似 RAG(检索增强生成)的检索增强系统,我们将使用我们的数据微调此模型,然后使用它来重新排序 llama-index 检索到的文档。

代码时间

安装必要的库

首先,您需要安装所需的库。运行以下命令以安装所有依赖项:

pip install sentence-transformers llama-index llama-index-llms-gemini  llama-index-embeddings-gemini llama-index-postprocessor-cohere-rerank

设置环境变量

在深入研究代码之前,请确保您拥有 GoogleCohere 的必要 API 密钥。这些密钥用于进行身份验证并使用它们各自的服务。将它们存储为环境变量,以便在整个脚本中安全且轻松地访问:

import os

GOOGLE_API_KEY = "xxxxxxxxx"
os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY

COHERE_API_KEY = "xxxxxxxxxxxxxxx"
os.environ["COHERE_API_KEY"] = COHERE_API_KEY

导入必要的库

接下来,导入您将在脚本中使用的库和模块。其中包括用于文档加载、文本拆分、嵌入生成、重排序和微调的工具。以下是导入列表:

from llama_index.core.evaluation import generate_question_context_pairs
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.postprocessor.cohere_rerank import CohereRerank
from sentence_transformers import CrossEncoder, InputExample
from llama_index.embeddings.gemini import GeminiEmbedding
from llama_index.core.node_parser import SentenceSplitter
from sentence_transformers import InputExample
from llama_index.llms.gemini import Gemini
from torch.utils.data import DataLoader
from llama_index.core import Settings
import pandas as pd
import random

定义模型和 cohere 重排序器

现在,定义您将使用的语言模型 (LLM) 和嵌入模型。在这种情况下,我们同时使用 Gemini 进行文本生成和嵌入。

## define the models
llm = Gemini(
    model="models/gemini-1.5-flash",
)

embed_model = GeminiEmbedding(
    model_name="models/embedding-001", api_key=GOOGLE_API_KEY
)
Settings.embed_model = embed_model
Settings.llm = llm

设置 Cohere 重排序器,它将用于优化检索到的文档。top_n 参数指定要重新排序的文档数量,而 model 参数指定 Cohere 重排序器的版本(例如,rerank-v3.5)。

## define the cohere reranker
cohere_rerank = CohereRerank(api_key=COHERE_API_KEY, top_n=5 , model="rerank-v3.5") # top_n is the number of documents to rerank

准备我们的数据

创建问答数据集

为了生成用于微调的合成数据集,我们首先需要从文档集合中创建一个问答数据集。create_qa_dataset 函数处理此过程。它接收一个文档目录,将其分割成更小的块(例如,256 个 token),并使用语言模型 (LLM) 为每个块生成问题。每个块的问题数量是可自定义的,允许您控制数据集的密度。

def create_qa_dataset(input_dir ,  llm , num_questions_per_chunk):
    """
    Create a question-answer dataset from a directory of documents
    Args:
        input_dir: str
            The directory containing the documents
        llm: llama_index.llms.gemini.Gemini
            The LLM model to use for generating questions
        num_questions_per_chunk: int
            The number of questions to generate per chunk
    Returns:
        list: A list of questions
    """
    documents = SimpleDirectoryReader(input_dir=input_dir).load_data()
    node_parser = SentenceSplitter(chunk_size=256)
    nodes = node_parser.get_nodes_from_documents(documents)
    qa_dataset = generate_question_context_pairs(
          nodes,
          llm=llm,
          num_questions_per_chunk=num_questions_per_chunk
      )
    queries = qa_dataset.queries.values()
    return list(queries) , documents

此函数输出一个查询列表和相应的文档,这些文档作为下一步的基础。

创建评分查询上下文数据集

一旦我们有了查询,我们需要将它们与相关的上下文配对并分配相关性分数。create_score_query_context_dataset 函数通过利用检索器和 Cohere 重新排序器来完成此操作。

def create_score_query_context_dataset(queries , documents , split) :
    """
    Create a dataset of queries, contexts and scores
    Args:
        queries: list
            A list of queries
        documents: list
            A list of documents
        split: str
            The split of the dataset
    Returns:
        pd.DataFrame: A DataFrame containing the queries, contexts and scores
    """
  contexts = []
  scores = []
  # create the index
  index = VectorStoreIndex.from_documents(documents)
  # create the retriever
  retriever = index.as_retriever(verbose=True, similarity_top_k=10)
  for query in queries:
      # retrieve the top 10 documents
      nodes = retriever.retrieve(query)
      # rerank the documents using cohere and get the top 5 document
      response = cohere_rerank.postprocess_nodes(
                    nodes=nodes, query_str=query
                )
      random_number = random.randint(0, len(response)-1)
      contexts.append(response[random_number].text)
      scores.append(response[random_number].score)
  assert len(queries) == len(contexts) == len(scores)
  df = pd.DataFrame({"query": queries, "context": contexts, "score": scores})
  df.to_csv(f"{split}-data.csv", index=False)
  return df

其工作原理如下:

  • 索引文档: 使用向量存储对文档进行索引,从而可以高效地检索相关块。
  • 检索和重新排序: 对于每个查询,检索器会提取前 k 个文档(例如,前 10 个)。然后使用 Cohere 重新排序器对这些文档进行重新排序以优化结果。
  • 随机抽样: 为了引入多样性,从重新排序的结果中随机选择一个文档。这确保了模型接触到各种查询-上下文对。
  • 数据集创建: 查询、上下文及其相应的相关性分数被编译成一个 DataFrame,并保存为 CSV 文件以供进一步使用。

创建训练数据集

为了准备训练数据集,我们首先使用 create_qa_dataset 函数生成一个问答数据集。该函数处理指定目录 (/content/train_data) 中的文档,将其分割成块,并使用 Gemini 语言模型为每个块生成问题。每个块的问题数量设置为 1,确保数据集的重点和可管理性。

## create the the train dataset
queries , documents = create_qa_dataset("/content/train_data", llm , 1 )
train_data = create_score_query_context_dataset(queries , documents , "train")
train_samples = [
    InputExample(texts=[row['query'], row['context']], label=row['score'])
    for _, row in train_data.iterrows()
]
train_dataloader = DataLoader(train_samples, shuffle=True, batch_size=8)

一旦查询和文档准备就绪,我们使用 create_score_query_context_dataset 函数创建评分查询上下文数据集。此函数检索并重新排序每个查询的文档,分配相关性分数,并将数据编译成结构化格式。然后,将生成的数据集转换为 InputExample 对象的列表,这些对象用于创建用于训练的 DataLoaderDataLoader 对数据进行洗牌并以批次为 8 的方式处理数据,从而优化训练过程。

创建验证数据集

验证数据集的创建过程类似。

## create the validation dataset
queries , documents = create_qa_dataset("/content/val_data", llm , 1)
val_data = create_score_query_context_dataset(queries , documents , "validation")
val_samples = [
    InputExample(texts=[row['query'], row['context']], label=row['score'])
    for _, row in val_data.iterrows()
]
val_dataloader = DataLoader(val_samples, shuffle=True, batch_size=3)

初始化模型

为了开始微调,我们初始化一个预先训练的 Cross-Encoder 模型。在这种情况下,我们使用 BAAI/bge-reranker-base model 作为基础模型。我们之前讨论过它。

## Initialize a pre-trained Cross-Encoder model
model = CrossEncoder('BAAI/bge-reranker-base')

创建自定义评估器

为了在训练期间监控模型的性能,我们定义了一个名为 MSEEval 的自定义评估器。该评估器计算模型预测的相关性分数与真实分数之间的均方误差 (MSE)。它记录结果并将其保存到 CSV 文件中,以便于跟踪。

评估器使用验证数据集 (val_dataloader) 以固定间隔评估模型的性能。

from sentence_transformers.evaluation import SentenceEvaluator
import torch
from torch.utils.data import DataLoader
import logging
from sentence_transformers.util import batch_to_device
import os
import csv
from sentence_transformers import CrossEncoder
from tqdm.autonotebook import tqdm

logger = logging.getLogger(__name__)

class MSEEval(SentenceEvaluator):
    """
    Evaluate a model based on its accuracy on a labeled dataset

This requires a model with LossFunction.SOFTMAX

The results are written in a CSV. If a CSV already exists, then values are appended.
    """

    def __init__(self,
                 dataloader: DataLoader,
                 name: str = "",
                 show_progress_bar: bool = True,
                 write_csv: bool = True):
        """
        Constructs an evaluator for the given dataset

:param dataloader:
            the data for the evaluation
        """
        self.dataloader = dataloader
        self.name = name
        self.show_progress_bar = show_progress_bar

        if name:
            name = "_"+name

        self.write_csv = write_csv
        self.csv_file = "accuracy_evaluation"+name+"_results.csv"
        self.csv_headers = ["epoch", "steps", "accuracy"]

    def __call__(self, model: CrossEncoder, output_path: str = None, epoch: int = -1, steps: int = -1) -> float:
        model.model.eval()
        total = 0
        loss_total = 0

        if epoch != -1:
            if steps == -1:
                out_txt = " after epoch {}:".format(epoch)
            else:
                out_txt = " in epoch {} after {} steps:".format(epoch, steps)
        else:
            out_txt = ":"

        loss_fnc = torch.nn.MSELoss()
        activation_fnc = torch.nn.Sigmoid()

        logger.info("Evaluation on the "+self.name+" dataset"+out_txt)
        self.dataloader.collate_fn = model.smart_batching_collate
        for features, labels in tqdm(self.dataloader,  desc="Evaluation", smoothing=0.05, disable=not self.show_progress_bar):

            with torch.no_grad():
                model_predictions = model.model(**features, return_dict=True)
                logits = activation_fnc(model_predictions.logits)
                if model.config.num_labels == 1:
                    logits = logits.view(-1)
                loss_value = loss_fnc(logits, labels)

            total += 1 # number of batches
            loss_total += loss_value.cpu().item()
        mse = loss_total/total

        logger.info("MSE: {:.4f} ({}/{})\n".format(mse, loss_total, total))

        if output_path is not None and self.write_csv:
            csv_path = os.path.join(output_path, self.csv_file)
            if not os.path.isfile(csv_path):
                with open(csv_path, newline='', mode="w", encoding="utf-8") as f:
                    writer = csv.writer(f)
                    writer.writerow(self.csv_headers)
                    writer.writerow([epoch, steps, mse])
            else:
                with open(csv_path, newline='', mode="a", encoding="utf-8") as f:
                    writer = csv.writer(f)
                    writer.writerow([epoch, steps, mse])

        return mse

定义评估器后,我们使用验证数据加载器对其进行初始化:

## create the evaluator
evaluator = MSEEval(val_dataloader,show_progress_bar=True , write_csv=True )

训练模型

准备好模型和评估器后,我们开始使用训练数据集微调模型。使用 fit 方法来训练模型,其中包含以下关键参数:

  • train_dataloader:训练数据,按批处理。
  • evaluator:用于在训练期间监控性能的自定义评估器。
  • epochs:训练轮数(在本例中设置为 1)。
  • warmup_steps:用于稳定训练的预热步数。
  • evaluation_steps:训练期间评估的频率。
  • output_path:保存微调模型的目录。
  • save_best_model:是否根据评估结果保存性能最佳的模型。
  • use_amp:是否使用自动混合精度 (AMP) 来加快训练速度。
  • scheduler:学习率调度器(例如,warmupcosine)。
  • show_progress_bar:是否在训练期间显示进度条。
## Train the model
model.fit(
    train_dataloader=train_dataloader,
    evaluator=evaluator,
    epochs=1,
    warmup_steps=100,
    evaluation_steps=3,
    output_path="my_model",
    save_best_model=True,
    use_amp=True,
    scheduler= 'warmupcosine',
    show_progress_bar=True,
)

登录并将模型推送到 Hub

对模型进行微调后,您可以通过将其推送到 Hugging Face 模型 Hub 来保存和共享它。这允许您对模型进行版本控制、与他人协作以及在生产环境中部署它。

为此,您首先需要使用 notebook_login 函数登录到您的 Hugging Face 帐户。这将提示您输入您的 Hugging Face 凭据。

from huggingface_hub import notebook_login

notebook_login()

登录后,您可以使用 push_to_hub 方法将微调后的模型推送到 Hub。将 user/bge-reranker-base-finetuned 替换为您想要的存储库名称。这将把模型权重、配置和其他必要的文件上传到 Hub。

model.push_to_hub("user/bge-reranker-base-finetuned")

使用微调模型与 llama-index

将模型上传到 Hub 后,您可以轻松地将其集成到您的 LlamaIndex 管道中进行重新排序。 SentenceTransformerRerank 类允许您加载微调模型并使用它来重新排序检索到的文档。 top_n 参数指定重新排序后要返回的文档数量。

from llama_index.core.postprocessor import SentenceTransformerRerank

rerank = SentenceTransformerRerank(
    model="user/bge-reranker-base-finetuned", top_n=3
)

在处理嵌入后,微调重新排序模型是顺理成章的下一步,它是一种改进系统理解和优先处理信息的强大方法。 从生成合成数据到训练和评估模型,每一步都带来了其自身的挑战和回报。 无论您是使用真实世界的数据还是创建自己的数据集,关键在于进行实验、迭代和改进。 我希望本指南能激励您探索在您自己的项目中进行微调的潜力。

Related Posts

结合chatgpt-o3-mini与perplexity Deep Research的3步提示:提升论文写作质量的终极指南

结合chatgpt-o3-mini与perplexity Deep Research的3步提示:提升论文写作质量的终极指南

AI 研究报告和论文写作 合并两个系统指令以获得两个模型的最佳效果 Perplexity AI 的 Deep Research 工具提供专家级的研究报告,而 OpenAI 的 ChatGPT-o3-mini-high 擅长推理。我发现你可以将它们结合起来生成令人难以置信的论文,这些论文比任何一个模型单独撰写的都要好。你只需要将这个一次性提示复制到 **

阅读更多
让 Excel 过时的 10 种 Ai 工具:实现数据分析自动化,节省手工作业时间

让 Excel 过时的 10 种 Ai 工具:实现数据分析自动化,节省手工作业时间

Non members click here作为一名软件开发人员,多年来的一个发现总是让我感到惊讶,那就是人们还在 Excel

阅读更多
使用 ChatGPT 搜索网络功能的 10 种创意方法

使用 ChatGPT 搜索网络功能的 10 种创意方法

例如,提示和输出 你知道可以使用 ChatGPT 的“搜索网络”功能来完成许多任务,而不仅仅是基本的网络搜索吗? 对于那些不知道的人,ChatGPT 新的“搜索网络”功能提供实时信息。 截至撰写此帖时,该功能仅对使用 ChatGPT 4o 和 4o-mini 的付费会员开放。 ![](https://images.weserv.nl/?url=https://cdn-im

阅读更多
掌握Ai代理:解密Google革命性白皮书的10个关键问题解答

掌握Ai代理:解密Google革命性白皮书的10个关键问题解答

10 个常见问题解答 本文是我推出的一个名为“10 个常见问题解答”的新系列的一部分。在本系列中,我旨在通过回答关于该主题的十个最常见问题来分解复杂的概念。我的目标是使用简单的语言和相关的类比,使这些想法易于理解。 图片来自 [Solen Feyissa](https://unsplash.com/@solenfeyissa?utm_source=medium&utm_medi

阅读更多
在人工智能和技术领域保持领先地位的 10 项必学技能 📚

在人工智能和技术领域保持领先地位的 10 项必学技能 📚

在人工智能和科技这样一个动态的行业中,保持领先意味着不断提升你的技能。无论你是希望深入了解人工智能模型性能、掌握数据分析,还是希望通过人工智能转变传统领域如法律,这些课程都是你成功的捷径。以下是一个精心策划的高价值课程列表,可以助力你的职业发展,并让你始终处于创新的前沿。 1. 生成性人工智能简介课程: [生成性人工智能简介](https://genai.works

阅读更多
揭开真相!深度探悉DeepSeek AI的十大误区,您被误导了吗?

揭开真相!深度探悉DeepSeek AI的十大误区,您被误导了吗?

在AI军备竞赛中分辨事实与虚构 DeepSeek AI真的是它所宣传的游戏规则改变者,还是仅仅聪明的营销和战略炒作?👀 虽然一些人将其视为AI效率的革命性飞跃,但另一些人则认为它的成功建立在借用(甚至窃取的)创新和可疑的做法之上。传言称,DeepSeek的首席执行官在疫情期间像囤积卫生纸一样囤积Nvidia芯片——这只是冰山一角。 从其声称的550万美元培训预算到使用Open

阅读更多
Type something to search...