混合检索(Hybrid Search)的加权策略:BM25稀疏向量与Embedding稠密向量的归一化融合

混合检索的加权策略:BM25稀疏向量与Embedding稠密向量的归一化融合

大家好,今天我们来深入探讨混合检索中的一个关键环节:加权策略,特别是针对BM25稀疏向量和Embedding稠密向量的归一化融合。混合检索旨在结合不同检索方法的优势,提升整体检索效果。而加权策略,则是将这些不同方法产生的排序结果有效融合的关键。

混合检索概述

在信息检索领域,我们通常会遇到两种主要的检索方法:

  • 基于关键词的检索(Keyword-based Retrieval): 这种方法依赖于用户查询中的关键词与文档中词项的匹配程度。经典的算法包括BM25(Best Matching 25)。
  • 基于语义的检索(Semantic-based Retrieval): 这种方法利用预训练语言模型(如BERT, Sentence-BERT等)将查询和文档编码成稠密向量,然后通过向量相似度(如余弦相似度)来衡量语义相关性。

这两种方法各有优缺点:

特性 BM25 (稀疏向量) Embedding (稠密向量)
优点 速度快,可解释性强,对精确匹配敏感 能捕捉语义相关性,对同义词、近义词有较好的处理能力
缺点 无法处理语义相关性,对拼写错误、词形变化敏感 计算成本高,依赖高质量的Embedding模型,可解释性较差
向量表示 高维稀疏向量 (Term Frequency, Inverse Document Frequency) 低维稠密向量
相似度计算 通常使用 BM25 公式 通常使用余弦相似度

混合检索的目标就是将这两种方法的优点结合起来,扬长避短,从而获得更好的检索效果。一个典型的混合检索流程如下:

  1. 查询处理: 接收用户查询。
  2. 稀疏向量检索: 使用 BM25 等算法,基于关键词进行检索,得到一个排序列表。
  3. 稠密向量检索: 使用 Embedding 模型将查询和文档编码成向量,计算相似度,得到另一个排序列表。
  4. 加权融合: 将两个排序列表根据一定的权重进行融合,生成最终的排序结果。

今天的重点就在于第4步:加权融合。

加权融合的挑战

直接将 BM25 的得分和 Embedding 的相似度进行加权平均通常效果不佳。原因在于:

  • 得分范围不一致: BM25 得分和 Embedding 相似度的范围可能差异很大。例如,BM25 得分可能在 0 到 100 之间,而余弦相似度则在 -1 到 1 之间。
  • 得分分布不一致: BM25 得分和 Embedding 相似度的分布也可能不同。例如,BM25 得分可能呈现长尾分布,而余弦相似度可能更集中在某个区间。

因此,我们需要对 BM25 得分和 Embedding 相似度进行归一化,使其具有可比性,然后再进行加权融合。

归一化方法

常用的归一化方法有以下几种:

  1. Min-Max Scaling: 将得分线性缩放到 [0, 1] 区间。

    def min_max_scaling(scores):
        min_score = min(scores)
        max_score = max(scores)
        if max_score == min_score:
            return [0.0] * len(scores) # 避免除以0
        scaled_scores = [(score - min_score) / (max_score - min_score) for score in scores]
        return scaled_scores
  2. Z-Score Normalization (Standardization): 将得分转换为标准正态分布,均值为 0,标准差为 1。

    import numpy as np
    
    def z_score_normalization(scores):
        mean = np.mean(scores)
        std = np.std(scores)
        if std == 0:
            return [0.0] * len(scores) # 避免除以0
        normalized_scores = [(score - mean) / std for score in scores]
        return normalized_scores
  3. Sigmoid Scaling: 将得分映射到 [0, 1] 区间,并具有 S 形曲线的特征。

    import numpy as np
    
    def sigmoid_scaling(scores):
        scaled_scores = [1 / (1 + np.exp(-score)) for score in scores]
        return scaled_scores
  4. Rank-Based Normalization: 基于得分的排名进行归一化。例如,可以将得分排名归一化到 [0, 1] 区间。

    def rank_based_normalization(scores):
        ranked_scores = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)
        normalized_scores = [0.0] * len(scores)
        for i, (index, score) in enumerate(ranked_scores):
            normalized_scores[index] = (len(scores) - i) / len(scores) # 将排名归一化到 [0, 1]
        return normalized_scores

选择哪种归一化方法取决于具体的应用场景和数据分布。Min-Max Scaling 简单易用,但对异常值敏感。Z-Score Normalization 对异常值不敏感,但可能将得分映射到负数。Sigmoid Scaling 可以突出高分,但对低分不敏感。Rank-Based Normalization 不受得分具体数值的影响,只关注排名,但可能会损失一些信息。

加权融合策略

在对 BM25 得分和 Embedding 相似度进行归一化之后,就可以进行加权融合了。最简单的加权融合方法是线性加权:

def linear_weighted_fusion(bm25_scores, embedding_scores, weight_bm25, weight_embedding):
    """
    线性加权融合 BM25 得分和 Embedding 相似度。

    Args:
        bm25_scores: 归一化后的 BM25 得分列表。
        embedding_scores: 归一化后的 Embedding 相似度列表。
        weight_bm25: BM25 得分的权重。
        weight_embedding: Embedding 相似度的权重。

    Returns:
        融合后的得分列表。
    """

    if len(bm25_scores) != len(embedding_scores):
        raise ValueError("BM25 scores and embedding scores must have the same length.")

    fused_scores = [weight_bm25 * bm25_score + weight_embedding * embedding_score
                    for bm25_score, embedding_score in zip(bm25_scores, embedding_scores)]
    return fused_scores

其中,weight_bm25weight_embedding 分别是 BM25 得分和 Embedding 相似度的权重,且 weight_bm25 + weight_embedding = 1

确定合适的权重是一个重要的任务。常用的方法包括:

  • 手动调整: 根据经验或通过实验,手动调整权重。
  • 学习权重: 使用机器学习算法(如 LambdaMART, RankNet 等)来学习最优的权重。

代码示例:一个完整的混合检索流程

下面是一个完整的混合检索流程的示例代码,包括 BM25 检索、Embedding 检索、归一化和加权融合:

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
from sentence_transformers import SentenceTransformer

# 模拟文档数据
documents = [
    "This is the first document about cats.",
    "This document is about dogs.",
    "The third document is about animals.",
    "Another document about pets."
]

# 模拟用户查询
query = "What are good pets?"

# 1. BM25 检索
def bm25_retrieval(query, documents):
    vectorizer = TfidfVectorizer()
    tfidf_matrix = vectorizer.fit_transform(documents)
    query_vector = vectorizer.transform([query])
    bm25_scores = cosine_similarity(query_vector, tfidf_matrix).flatten() # 使用 TF-IDF 相似度作为 BM25 的近似
    return bm25_scores

# 2. Embedding 检索
def embedding_retrieval(query, documents):
    model = SentenceTransformer('all-MiniLM-L6-v2') # 选择一个合适的 Sentence Embedding 模型
    query_embedding = model.encode(query)
    document_embeddings = model.encode(documents)
    embedding_scores = cosine_similarity(query_embedding.reshape(1, -1), document_embeddings).flatten()
    return embedding_scores

# 3. 归一化
def normalize_scores(scores, method="min_max"):
    if method == "min_max":
        return min_max_scaling(scores)
    elif method == "z_score":
        return z_score_normalization(scores)
    elif method == "sigmoid":
        return sigmoid_scaling(scores)
    elif method == "rank":
        return rank_based_normalization(scores)
    else:
        raise ValueError("Invalid normalization method.")

# 4. 加权融合
def weighted_fusion(bm25_scores, embedding_scores, weight_bm25=0.5):
    weight_embedding = 1 - weight_bm25
    fused_scores = linear_weighted_fusion(bm25_scores, embedding_scores, weight_bm25, weight_embedding)
    return fused_scores

# 主程序
if __name__ == "__main__":
    bm25_scores = bm25_retrieval(query, documents)
    embedding_scores = embedding_retrieval(query, documents)

    # 归一化
    bm25_scores_normalized = normalize_scores(bm25_scores, method="min_max")
    embedding_scores_normalized = normalize_scores(embedding_scores, method="min_max")

    # 加权融合
    fused_scores = weighted_fusion(bm25_scores_normalized, embedding_scores_normalized, weight_bm25=0.6)

    # 排序并输出结果
    ranked_results = sorted(enumerate(fused_scores), key=lambda x: x[1], reverse=True)
    print("Query:", query)
    print("Results:")
    for i, (index, score) in enumerate(ranked_results):
        print(f"Rank {i+1}: Document {index+1}, Score: {score:.4f}, Content: {documents[index]}")

代码说明:

  • BM25 检索: 使用 TfidfVectorizer 来计算 TF-IDF 向量,并使用余弦相似度作为 BM25 的近似得分。 注意:这只是一个近似,真正的BM25实现涉及更复杂的公式。 在实际应用中,应该使用专门的 BM25 实现库,例如 rank_bm25
  • Embedding 检索: 使用 SentenceTransformer 库加载一个预训练的 Sentence Embedding 模型,将查询和文档编码成向量,并计算余弦相似度。
  • 归一化: 使用 normalize_scores 函数对 BM25 得分和 Embedding 相似度进行归一化。这里使用了 Min-Max Scaling 作为示例,可以根据需要选择其他归一化方法。
  • 加权融合: 使用 weighted_fusion 函数对归一化后的 BM25 得分和 Embedding 相似度进行加权融合。
  • 排序和输出: 将融合后的得分进行排序,并输出结果。

实验和评估

为了评估混合检索的效果,我们需要进行实验。常用的评估指标包括:

  • Precision@K: 返回结果中前 K 个文档的准确率。
  • Recall@K: 返回结果中前 K 个文档的召回率。
  • Mean Average Precision (MAP): 所有查询的平均准确率的均值。
  • Normalized Discounted Cumulative Gain (NDCG): 考虑排序位置的评估指标。

在实验中,我们需要比较不同加权策略的效果,例如:

  • 不同的归一化方法。
  • 不同的权重。
  • 不同的 Embedding 模型。

通过实验,我们可以找到最优的加权策略,从而提升混合检索的效果。

总结

混合检索通过结合基于关键词和基于语义的检索方法,能够有效地提升检索效果。加权融合是混合检索中的一个关键环节,需要对 BM25 得分和 Embedding 相似度进行归一化,并选择合适的权重。常用的归一化方法包括 Min-Max Scaling、Z-Score Normalization、Sigmoid Scaling 和 Rank-Based Normalization。常用的加权融合方法是线性加权,可以通过手动调整或机器学习算法来学习最优的权重。 通过实验和评估,我们可以找到最优的加权策略,从而获得更好的检索效果。

一些思考与未来方向

  • 动态权重调整: 可以考虑根据查询的特性动态调整权重。 例如,对于包含明确关键词的查询,可以增加 BM25 的权重;对于语义模糊的查询,可以增加 Embedding 的权重。
  • 更复杂的融合模型: 可以使用更复杂的模型,例如神经网络,来学习 BM25 和 Embedding 的非线性融合方式。
  • Query Expansion: 在混合检索中,可以结合 Query Expansion 技术,例如使用同义词或相关词来扩展查询,从而提升检索效果。
  • 负样本挖掘: 在学习权重时,可以使用负样本挖掘技术,找到更难区分的负样本,从而提升模型的泛化能力。
  • 冷启动问题: 当新文档没有embedding时,如何进行有效的混合检索,也是一个值得研究的问题。可以考虑使用知识蒸馏等技术,从现有embedding模型中迁移知识。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注