Breadcrumb
BM25源码解读(构建稀疏矩阵)
BM25 搜索引擎包含两个主要阶段:索引阶段和检索阶段。索引阶段负责处理原始文档并构建可搜索的索引结构,检索阶段负责处理用户查询并返回最相关的文档。
BM25有多个变体,在库中也有多个不同的计算公式实现,主要公式为:
$$ \text{BM25}=\sum_{t\in\mathcal{Q}}\frac{tf_{t,d}\cdot(k+1)}{tf_{t,d}+k\cdot(1-b+b\cdot\frac{l_d}{mean(l_d)})}\cdot ln(1+\frac{N-df_t+0.5}{df_t+0.5}) $$
代码源码: https://github.com/xhluca/bm25s/blob/main/bm25s/__init__.py
bm25s库中的流程分为index和retrieve两个,我们重点介绍index部分:
index
index 方法是索引的入口方法,负责整个索引过程的协调。它接收一个 corpus 参数,这个参数可以是多种形式:可以是分词后的文档列表,也可以是已经转换为 token ID 的形式,或者是自定义的分词对象。
方法会首先判断 corpus 的类型,然后根据不同的类型调用相应的处理方法。如果是分词后的文档列表,就调用 build_index_from_tokens 方法;如果是已转换的形式,就直接调用 build_index_from_ids 方法。
build_index_from_tokens
接收一个List[List],外层List是document,内层List是document被分词之后的各个token,把这些token变为词汇表vocab_dict,然后调用build_index_from_ids函数。
首先,方法会遍历所有文档,收集所有唯一的 token,形成一个集合。然后,它会创建一个词汇表映射,这个映射是一个字典,键是 token 的字符串,值是一个唯一的整数标识符,这个整数就是 token 的 ID。比如 abstract 被分配 ID 0,activ 被分配 ID 1,这样就形成了一个像 {“abstract”: 0, “activ”: 1, …} 这样的词汇表。
接下来,方法会遍历所有文档,将每个文档中的 token 转换为对应的 token ID,最后,方法会调用 build_index_from_ids,将 token ID 列表和词汇表传递给它。
build_index_from_ids
build_index_from_ids 是核心索引构建方法,它计算 BM25 分数并构建稀疏矩阵。这个方法包含四个主要步骤。
第一步 _calculate_doc_freqs
文档频率是指每个 token 出现在多少个文档中。这里的关键是不重复计算,即使一个 token 在同一个文档中出现多次,我们也只计算一次。
比如说,如果 token 0 出现在文档 0 和文档 2 中,那么 token 0 的文档频率就是 2。如果 token 1 只出现在文档 1 中,那么 token 1 的文档频率就是 1。
计算的结果是一个字典,键是 token ID,值是该 token 出现的文档数。这个数据结构的形式类似于 {0: 2, 1: 1, 2: 1, 3: 1, 4: 2, 5: 1},其中键是 token ID,值就是出现的次数。这个信息对于后续计算 IDF 非常重要。
第二步 _build_idf_array
计算inverse document frequency,返回一个list,长度为token的数量,表示每一个token的idf。
第三步 _build_scores_and_indices_for_matrix
生成score,doc_indices,voc_indices对以便于后面构建稀疏矩阵,这三个都是相同长度的list。
- 调用_get_counts_from_token_ids,对每一个单个document,找出单个词在单个文档中的出现次数,比如tf_array=[2,1,1], voc_ind_doc=[66,179,151],表示66号词语在这个文档中出现了两次。
- 通过公式计算term frequency。
- 利用idf和tf计算bm25得分,加到score list中去。
- 最后生成的score,doc_indices,voc_indices长度为单个文档中的unique token数量和,比如doc_indices为 [0, 1, 1, 1, 2, 2, 2 …], voc_indices为 [13, 154, 9, 180, 155, 180 …],doc_indices和voc_indices是相对应的,比如第一个值就是第0个文档中的词语对应vocab_dict中的13号, 第二到四个indices是第二个文档中的词语对应vocab_dict中的154,9,180号。而score是算出来的用公式bm25分数
第四步 scipy.sparse.csc_matrix
构建稀疏矩阵,以doc_indices为row,voc_indices为col,score为value,这样就能很轻易地知道每一个document中哪一个token的bm25分数是多少,方便后面检索使用。
在检索阶段,查询被分词并转换为 token ID,然后利用保存的稀疏矩阵计算查询与所有文档的相关度分数,接着选出分数最高的 K 个文档,最后返回这些文档及其分数。比如说,查询可能是 [“python”, “tutorial”],经过转换后变成 [42, 156],然后利用稀疏矩阵计算分数,最后选出前 K 个文档。
这种设计的优点是,索引阶段只需要执行一次,而检索阶段可以执行多次。通过预先计算和存储 BM25 分数,检索阶段的速度可以大大加快。