ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

中文 BM25 检索实测:2.6 万条语料单次查询 0.3 毫秒,分词口径只改索引体积

中文 BM25 检索实测:2.6 万条语料单次查询 0.3 毫秒,分词口径只改索引体积 中文 BM25 检索实测2.6 万条语料单次查询 0.3 毫秒分词口径只改索引体积给本机的中文技术文档搭检索功能我把五种中文分词口径放在同一批语料上比了一遍也顺手量了速度。结论有点反直觉分词口径几乎不影响命中率106 条查询的 MRR 落在 0.892 到 0.901 之间差距在噪声里真正被口径改变的是索引体积和建索引时间——词级加去停用词的索引 1.56 MB字符 bigram 要 5.64 MB。而查询速度这一项2.6 万条语料上单次 0.3 毫秒同一份语料用纯 Python 的 BM25 实现要 153 毫秒差了约 510 倍。语料和查询是从哪来的语料不是下载的数据集是本机 730 份 Markdown 和文本文件里切出来的按句号切句、三句一窗、去重英文段落和代码块在切段时就被剔掉了最后留下 5194 条中文段落合计 41.9 万汉字。这些文档里既有我自己的实验记录也有项目笔记长度 60 到 900 字不等跟一般 RAG 里的分块结果形态接近。查询集是这么构造的从语料里抽 106 条段落每段挑一句原话当查询正确答案就是那句原话所在的段落。这叫 known-item 检索测的是“同一个东西换了长度呈现还能不能找回来”不是语义检索能力。这个前提很关键后面会看到它对结论的影响。BM25 用的是 GitHub 上的 bm25s 仓库1,800 stars最新版本 0.3.1210 月 2 日发布。选它是因为它只依赖 numpy不用 Java 也不用 GPU装在 2 核机器上跑得动。五种中文分词口径的结果分词器可替换是 bm25s 的一个接口设计Tokenizer接受任意 callable 当切词函数所以 jieba、单字、bigram 都能塞进去索引本身不用改。同一批语料跑五种口径106 条查询各取 top-10分词口径词表大小平均 token/篇分词耗时建索引106 条查询H1H5MRR索引体积jieba 词级16,99771.81.095 s0.101 s0.012 s0.7831.0000.8922.42 MBjieba 词级 去停用词15,89335.41.125 s0.066 s0.009 s0.8021.0000.9011.56 MB字符 unigram2,20498.20.113 s0.116 s0.017 s0.7921.0000.8962.80 MB字符 bigram94,39697.20.194 s0.182 s0.012 s0.8021.0000.9015.64 MB词级 bigram 混合100,983169.01.385 s0.234 s0.020 s0.7921.0000.8966.82 MB三个能直接拿走的观察去停用词是性价比最高的一步。停用词表就是十几个高频虚词的、了、和、是、在、也、就、都、而……加单字处理完词表从 16,997 降到 15,893索引从 2.42 MB 压到 1.56 MB建索引时间 0.101 秒降到 0.066 秒MRR 反而略涨到 0.901。BM25 的 IDF 会把高频词自动压低权重但压低不等于不占空间那些词几乎出现在每篇文档里等于每篇都存了一份没用的倒排条目。字符 unigram 的索引比词级还大。这条我一开始判断错了以为字级一定省空间。原因是 token 数变多了词级平均 71.8 个 token 一篇字符 unigram 是 98.2 个而 BM25 的索引存的是“文档-词条”这一层条目数不是词表大小所以词表只有 2,204 个也救不回来索引 2.80 MB比词级的 2.42 MB 大。字符 bigram 是唯一一个词表爆炸的口径。94,396 个 token索引 5.64 MB是去停用词版本的 3.6 倍。换来的是同样的 H10.802和 MRR0.901。如果语料里专有名词、型号、编号多bigram 能绕开分词错误直接命中字符串没有这个需求就别用它纯粹多花 4 MB。快在哪索引阶段先把分数算完bm25s 的思路是把 BM25 的计算搬到建索引阶段索引时把每个“文档-词条”对的分数预先算出来存成稀疏矩阵查询时只做查表加权求和不做全量遍历。代码很简单importbm25s,jiebafrombm25s.tokenizationimportTokenizer jiebjieba.Tokenizer()jieb.initialize()deftok_jieba(text):return[wforwinjieb.lcut(text)ifw.strip()]tkTokenizer(splittertok_jieba,stopwords[])corpus_idstk.tokenize(docs,return_asids,show_progressFalse)# docs5194 条中文段落retrieverbm25s.BM25(methodlucene)retriever.index(corpus_ids,show_progressFalse)# 这里就把分数算完了query_idstk.tokenize([语音转写的错字率],update_vocabFalse,return_asids,show_progressFalse)resultsretriever.retrieve(query_ids,k3,show_progressFalse)五个口径都跑完之后脚本自己打出来的表就是下面这样原样粘贴没改列宽语料 5194 条查询 106 条 策略 词表 tok/篇 分词s 索引s 查106条s H1 H5 MRR 索引MB jieba 词级 16997 71.8 1.095 0.101 0.012 0.783 1.000 0.892 2.42 jieba 词级去停用词 15893 35.4 1.125 0.066 0.009 0.802 1.000 0.901 1.56 字符 unigram 2204 98.2 0.113 0.116 0.017 0.792 1.000 0.896 2.80 字符 bigram 94396 97.2 0.194 0.182 0.012 0.802 1.000 0.901 5.64 词级bigram 混合 100983 169.0 1.385 0.234 0.020 0.792 1.000 0.896 6.82同一批语料复制到 25,970 条和 103,880 条再各跑一遍单次查询耗时是这样的语料条数分词建索引106 条查询总耗时单次查询5,1941.081 s0.095 s0.012 s0.12 ms25,9705.367 s0.488 s0.032 s0.30 ms103,88022.146 s2.191 s0.109 s1.03 ms10 万条语料单次查询 1.03 毫秒索引只花了 2.2 秒——这是我在 2 核i5-1340P、4 GB 内存的 WSL 里跑出来的不是仓库宣传的数字。对照组是 rank_bm25也就是 Python 生态里最常见的那份纯 Python 实现同样用 jieba 词级切词实现语料条数建索引10 条查询单次查询rank_bm25纯 Python5,1940.056 s0.217 s21.73 msrank_bm25纯 Python25,9700.269 s1.530 s153.05 msbm25s5,1940.095 s—0.12 msbm25s25,9700.488 s—0.30 ms两个细节值得说。一是建索引阶段 bm25s 反而慢rank_bm25 在构造对象时只算 IDF词频统计留在查询时做所以建得快bm25s 把活提前干完了建索引多花 0.2 秒换来查询快两个数量级。二是 25,970 条这一档21.73 ms 和 153.05 ms 的比值会随语料变大而拉开约 510 倍因为纯 Python 实现每次查询都要遍历全部文档而这里的查询耗时几乎只跟 query 长度和命中词条数有关。我怎么用它我最后选了 jieba 词级加去停用词这一档理由不是命中率是索引体积这套索引要跟文档一起打包分发1.56 MB 和 5.64 MB 是能不能塞进一个安装包的差别。命中率上的 0.892 和 0.901我看作同一水平——106 条查询的样本量撑不起这个精度。踩到的坑有三个。update_vocab我固定写成 False。查询侧的 tokenize 如果允许更新词表查询里出现的新词会拿到新 id词表在两套 id 空间里漂移分数就对不上。默认值在词表非空时也不会写入但写死更省心。jieba 首次加载词典要 0.33 秒5194 条文本切完 1.1 秒纯字符切分只要 0.11 秒。如果你的流程是每次启动都重建索引很多本地工具的默认做法字符口径能省下近一秒的启动时间。还有一个要自己泼冷水的这张表里的 H5 全是 1.000因为查询句就是从原文里抠出来的检索难度偏低。我另外拿三条改写过的问句比如“OCR 单张图片识别要多久”试了一下五种口径的 top-1 各不相同——词级命中的是讨论 OCR 体验的那段字符口径命中的是分词流程那段。真实用户提问的分布一定比我的查询集难H1 到 0.8 这个数字不要当成线上效果。numba 后端我没测。仓库说大数据集上约有 2 倍查询提升本机没装 numba我没法验证这条只能算引用。可以直接做的三件事先拿字符 bigram 建一版索引跑通链路不用装分词器改动最小再在同一批语料上换成词级如果 MRR 差不到一个百分点就别在分词器上继续花时间。停用词在建索引前处理别指望 IDF 帮你省空间。我这份停用词表就是 jieba 词典里挑的十几个高频虚词加若干单字索引体积直接降到六成。语料涨到 10 万条以上时先量一次单次查询耗时超过 5 毫秒再考虑 numba 后端或换向量检索别一开始就上重型方案。
返回列表