一、IndexIVFPQ + IndexFlatL2(粗聚类中心)实现流程

二、详细流程描述
-
利用训练样本集进行训练
- IndexFlatL2类型的量化器,进行粗聚类,粗聚类中心最后会存在量化器中
- 细聚类中心训练
- 利用a中的索引(存的就是训练得到的粗聚类中心)进行检索,为训练样本集分配粗聚类中心;
- 计算粗聚类中心与训练样本集的残差(用 d维<原始向量的xi> - d维度<粗聚类中心向量yi>,得到一个新的d维残差向量),以此作为新的向量集合进行细聚类;
- PQ量化器训练:将原始向量分成M段字向量,子向量串行训练
- 根据指定的细聚类中心点数量对第Mi段子向量进行聚类(M ≤ i ≥ 0);
- 聚类完成后,在PQ量化器中保存好Mi段的聚类中心数据 (pq.train)
- 借助粗聚类中心,细聚类中心数据预计算得到 预计算表(降低在线search的算力消耗,提升检索速度)
- precompute_list_tables_L2 search阶段,这个函数会用到训练阶段得到的预计算表
-
倒排索引构建:index.add向索引中添加原始向量
- 计算粗聚类中心与原始向量的残差
- 计算a中残差向量与细聚类中心的L2距离 (分成M段分别计算的;残差向量分段及细聚类中心向量分段)
- 每个原始向量得到的距离表是 ksub * M维的
- 利用b中计算的距离表,确定原始向量的编码id
- 将原始向量添加到倒排列表中(貌似是按顺序push的,没看到排序逻辑)
- 倒排列表的结构
- std::vector<std::vector<uint8_t>> codes; // <粗聚类中心id, <编码>>
- std::vector<std::vector<idx_t>> ids; //<粗聚类中心id, <docid>>
-
检索query向量 (找到TopK个与query向量的|| x - y_C - y_R ||^2最小的原始向量label_id)
- 在所有粗聚类中心下search, 得到距离及对应粗聚类中心id (这里拿到了term1)
- 在指定(nprobe)数量的粗聚类倒排链中检索,获取最终距离及docid
- 选出nprobe个倒排链
- 依次遍历nprobe个倒排链,检索item, 当检索的向量数超过max_code的时候,终止检索;
- 创建scanner实例:InvertedListScanner*
- scanner->set_query(x + i * d); // 计算term3
- qi 存第i个query向量;
- 填充sim_table_2:query向量分段,计算与所有细聚类中心的**内积**
- scanner->set_list(key, coarse_dis_i);
- scan_one_list
- 倒排链中的编码后的原始向量节点,逐个计算term1 + term2 + term3
- 找到最相似的TopK

// 最终检索流程梳理
scanner->set_query(x + i * d);
init_query
qi 存第i个query向量
填充sim_table_2:query向量分段,计算与所有细聚类中心的**内积**
init_result
scan_one_list(
keys[i * nprobe + ik],
coarse_dis[i * nprobe + ik],
simi,
idxi,
max_codes - nscan)
scanner->set_list(key, coarse_dis_i);
list_no记录当前的倒排链id
coarse_dis记录query向量与粗聚类中心距离
dis0 和粗聚类中心的距离 // term1
fvec_madd(
pq.M * pq.ksub,
ivfpq.precomputed_table.data() + key * pq.ksub * pq.M, // term2
-2.0,
sim_table_2,
sim_table); // 最终结果sim_table[i] = term2[i] - 2 * sim_table_2[i] // term2 - 2 * term3
scanner->scan_codes(list_size, codes, ids, simi, idxi, k)
scan_list_with_table // term1 + term2 - 2 * term3
reorder_result(simi, idxi);
三、一些常见问题记录
- coarse_quantizer 粗聚类中心点结构数据就是 x维的向量? 是的
- 如果是IndexFlat类型的,中心点向量会存在一个一维数组中
- 粗聚类量化器的作用
- 对训练向量进行粗聚类(不涉及量化),得到粗聚类中心;
- 细聚类量化器的作用
- 训练阶段:把残差向量进行聚类并保留聚类中心
- 在构建倒排索引阶段,把n个d维原始向量,量化成
- 抽样数据训练最终是为了得到什么?
- 粗聚类中心 (存在量化器中)
- 细聚类中心(存在IndexIVFPQ的成员ProductQuantizer::centroids中)
- 原始向量数据存在哪里
- 量化后的数据存在哪里
- IndexIVFPQ::invlists中,即倒排链中,包含docid,编码id数组
- 最终检索query向量过程中
- 预计算好的码表在哪里使用了? 计算最终距离的时候,直接查表
- 哪里使用了量化编码?
- 能否指定遍历的粗聚类中心点,即倒排链数量
- 可以, 可以通过IVFSearchParameters::nprobe指定;
- 残差:原始向量[i] - 所属中心点向量[i]
- 为什么用残差向量做量化?仔细想一下,样本y和其所属的聚类中心向量 c(y)肯定是距离比较小的,那么其残差一定是很小的。如原始样本向量y=[8, 9, 10],其聚类中心是[7.5, 8.5, 9.5],则残差就是 y-cy=[0.5, 0.5, 0.5]。不难发现残差向量相比原始向量y,其每个值的值域要小很多,值域小的话量化误差就会更小,因为kmeans聚类时引起的误差会更小。
- 非残差的训练及检索流程
- 倒排链归哪里持有,和细聚类中心的关系是?
- 倒排链是一个vector, 和粗聚类中心是一一对应的关系
- 倒排链中每个元素都是一个原始向量PQ量化之后的结果, 同时还保存了docid信息;
- 倒排列表的结构
- std::vector<std::vector<uint8_t>> codes; // <粗聚类中心id, <编码>>
- std::vector<std::vector<idx_t>> ids; //<粗聚类中心id, <docid>>
- 可调参数
- nprobe(检索的倒排链数量)
- max_codes (所有倒排链加一起,最多对比max_codes个向量)
- nlist (建库时,粗聚类中心点个数)
- topk(需要多少个item结果)