BM25稀疏检索算法笔记

文章目录

bm25有什么用?

BM25 在 RAG/AI 里的价值就 4 点:

1、和向量检索互补,构成最强混合检索

2、精准匹配关键词、术语、编号,解决向量漂移

3、轻量、快、无GPU、中文友好

4、可解释,适合合规、专业文档

没有 BM25 的 RAG,在专业文档、中文场景、精准查询上,效果通常会明显弱一截。

bm25

bm25-手动实现示例

代码:

python 复制代码
import math
from collections import Counter


class SimpleBM25:
    def __init__(self, documents, k1=1.5, b=0.75):
        self.k1 = k1
        self.b = b
        self.documents = documents
        self.N = len(documents)

        # 1. 预处理:分词 (这里简单按空格切分,中文需先用 jieba)
        self.tokenized_docs = [doc.split() for doc in documents]

        # 2. 计算文档长度和平均长度
        self.doc_lengths = [len(doc) for doc in self.tokenized_docs]
        self.avg_dl = sum(self.doc_lengths) / self.N

        # 3. 计算每个词在所有文档中出现的次数 (用于算 IDF)
        self.freq_map = {}  # {word: count_of_docs_containing_word}
        for doc in self.tokenized_docs:
            unique_words = set(doc)
            for word in unique_words:
                self.freq_map[word] = self.freq_map.get(word, 0) + 1

        # 4. 预计算 IDF
        self.idf = {}
        for word, count in self.freq_map.items():
            # IDF 公式变种:ln((N - n + 0.5) / (n + 0.5) + 1)
            self.idf[word] = math.log((self.N - count + 0.5) / (count + 0.5) + 1)

    def get_score(self, query):
        query_tokens = query.split()
        scores = []

        for i, doc in enumerate(self.tokenized_docs):
            score = 0.0
            doc_len = self.doc_lengths[i]
            counter = Counter(doc)  # 当前文档的词频统计

            for token in query_tokens:
                if token not in counter:
                    continue

                tf = counter[token]  # 词频 f(q, D)
                idf = self.idf.get(token, 0)

                # BM25 核心公式部分
                numerator = tf * (self.k1 + 1)
                denominator = tf + self.k1 * (1 - self.b + self.b * (doc_len / self.avg_dl))

                score += idf * (numerator / denominator)

            scores.append(score)

        return scores


# --- 测试数据 ---
docs = [
    "自然语言处理 是 人工智能 的 核心",
    "人工智能 和 机器学习 都 很 重要",
    "深度学习 是 机器学习 的 一种",
    "自然语言处理 需要 大量 数据"
]

# 初始化
bm25 = SimpleBM25(docs)

# 查询
query = "自然语言处理 人工智能"
scores = bm25.get_score(query)

print(f"查询: '{query}'")
print("-" * 30)
for i, score in enumerate(scores):
    print(f"文档 {i + 1}: {docs[i]}")
    print(f"得分: {score:.4f}")
print("-" * 30)
# 排序结果
ranked_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)
print("排序后的最佳匹配:")
for idx in ranked_indices:
    print(f"[{scores[idx]:.4f}] {docs[idx]}")

输出结果:

bash 复制代码
查询: '自然语言处理 人工智能'
------------------------------
文档 1: 自然语言处理 是 人工智能 的 核心
得分: 1.3863
文档 2: 人工智能 和 机器学习 都 很 重要
得分: 0.6359
文档 3: 深度学习 是 机器学习 的 一种
得分: 0.0000
文档 4: 自然语言处理 需要 大量 数据
得分: 0.7617
------------------------------
排序后的最佳匹配:
[1.3863] 自然语言处理 是 人工智能 的 核心
[0.7617] 自然语言处理 需要 大量 数据
[0.6359] 人工智能 和 机器学习 都 很 重要
[0.0000] 深度学习 是 机器学习 的 一种

bm25-jieba分词示例

python 复制代码
from rank_bm25 import BM25Okapi
import jieba

# 1. 准备中文文档数据
documents = [
    "自然语言处理是人工智能的核心领域,广泛应用于搜索引擎。",
    "机器学习和深度学习是人工智能的重要分支。",
    "搜索引擎需要使用自然语言处理技术来理解用户查询。",
    "今天天气不错,适合出去打球。",  # 无关文档
    "自然语言处理技术在医疗领域的应用也在不断增加。"
]

# 2. 中文分词 (关键步骤!)
# 使用 jieba 进行分词,返回列表的列表
tokenized_docs = [list(jieba.cut(doc)) for doc in documents]

# 3. 初始化 BM25 模型
bm25 = BM25Okapi(tokenized_docs)

# 4. 处理查询
query = "自然语言处理 搜索引擎"
# 查询也必须分词
tokenized_query = list(jieba.cut(query))

# 5. 获取得分 (返回一个包含所有文档分数的列表)
scores = bm25.get_scores(tokenized_query)


# 6. 【修正点】手动排序并提取 Top N
def get_top_n_documents(scores, documents, n=3):
    # 将 (分数, 原始索引, 文档内容) 打包
    # enumerate(scores) 生成 (索引, 分数)
    ranked_results = sorted(
        [(score, idx, doc) for idx, (score, doc) in enumerate(zip(scores, documents))],
        key=lambda x: x[0],  # 按分数排序
        reverse=True  # 降序
    )

    # 取前 N 个
    return ranked_results[:n]


top_n = 3
top_results = get_top_n_documents(scores, documents, n=top_n)

# 7. 展示结果
print(f"查询: '{query}'")
print(f"分词后查询: {tokenized_query}")
print("-" * 50)

for rank, (score, idx, doc) in enumerate(top_results, 1):
    if score > 0:
        print(f"Rank {rank} (得分: {score:.4f}):")
        print(f"  原文: {doc}")
        print(f"  分词预览: {'/'.join(list(jieba.cut(doc))[:5])}...")
        print()
    else:
        # 如果前 N 名里有得分为 0 的,说明相关文档不足 N 个
        print(f"Rank {rank}: 无相关文档 (得分 0)")

# 如果你只需要索引列表 (模拟 top_n 的功能)
top_indices = [idx for _, idx, _ in top_results]
print(f"Top {top_n} 文档索引列表: {top_indices}")

bm25-idf示例

期待:

特别高频出现的词,如,它的得分反而低。

量子力学这样的词得分反而高。

代码:

python 复制代码
from rank_bm25 import BM25Okapi
import math

# 构造极端数据
# 文档 1-99: 都是废话 "的 的 的"
# 文档 100: 包含稀有词 "量子力学"
documents = [["的"] * 10] * 99 + [["量子力学"]]

# 注意:这里为了演示,我们没有过滤"的",看看会发生什么
tokenized_docs = documents

bm25 = BM25Okapi(tokenized_docs)

# 情况 A: 搜索常见词 "的"
query_common = ["的"]
scores_common = bm25.get_scores(query_common)
print(f"搜索 '的' (出现在 99 篇文档):")
print(f"  前 5 篇得分: {scores_common[:5]}") # 应该都很低
print(f"  最后一篇(无'的')得分: {scores_common[-1]}") # 应该是 0

# 情况 B: 搜索稀有词 "量子力学"
query_rare = ["量子力学"]
scores_rare = bm25.get_scores(query_rare)
print(f"\n搜索 '量子力学' (只出现在 1 篇文档):")
print(f"  前 99 篇得分: {scores_rare[:5]}") # 应该都是 0
print(f"  最后一篇得分: {scores_rare[-1]:.4f}") # 应该非常高!

# 对比
if scores_rare[-1] > scores_common[0]:
    print("\n✅ 验证成功:稀有词的得分远高于常见词,这就是 IDF 的作用!")
相关推荐
zzm62834 分钟前
精读KDD 2017最佳论文:通过类比挖掘加速创新 | 阅读笔记
笔记
你有哈莫吗38 分钟前
CMake构建学习笔记-iconv库的构建
笔记·学习
geats人山人海2 小时前
数据结构第六章c语言 树的存储下二叉树的概念和存储
c语言·数据结构·算法
漂流瓶jz3 小时前
UVA-12627 奇怪的气球膨胀 题解答案代码 算法竞赛入门经典第二版
算法·图论·递归·aoapc·算法竞赛入门经典·uva·12627
jarvisuni3 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
kobesdu3 小时前
流形上的优化:SO(3)与SE(3)的广义加减法在FAST-LIO中如何简化状态估计
人工智能·算法·fastlio
白狐_7983 小时前
408 数据结构算法题 01:线性表暴力求解保分指南
java·数据结构·算法
乐观勇敢坚强的老彭4 小时前
C++信奥:开关门、开关灯问题
开发语言·c++·算法
冻柠檬飞冰走茶4 小时前
PTA基础编程题目集 7-31 字符串循环左移(C语言实现)
c语言·开发语言·数据结构·算法
Hi李耶4 小时前
【LeetCode】4-寻找两个正序数组的中位数
算法·leetcode·职场和发展