经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Python3 » 查看文章
Sparse稀疏检索介绍与实践
来源:cnblogs  作者:JadePeng  时间:2024/4/15 14:41:34  对本文有异议

Sparse稀疏检索介绍

在处理大规模文本数据时,我们经常会遇到一些挑战,比如如何有效地表示和检索文档,当前主要有两个主要方法,传统的文本BM25检索,以及将文档映射到向量空间的向量检索。

BM25效果是有上限的,但是文本检索在一些场景仍具备较好的鲁棒性和可解释性,因此不可或缺,那么在NN模型一统天下的今天,是否能用NN模型来增强文本检索呢,答案是有的,也就是我们今天要说的sparse 稀疏检索。

传统的BM25文本检索其实就是典型的sparse稀疏检索,在BM25检索算法中,向量维度为整个词表,但是其中大部分为0,只有出现的关键词或子词(tokens)有值,其余的值都设为零。这种表示方法不仅节省了存储空间,而且提高了检索效率。

向量的形式, 大概类似:

  1. {
  2. '19828': 0.2085,
  3. '3508': 0.2374,
  4. '7919': 0.2544,
  5. '43': 0.0897,
  6. '6': 0.0967,
  7. '79299': 0.3079
  8. }

key是term的编号,value是NN模型计算出来的权重。

稀疏向量与传统方法的比较

当前流行的sparse检索,大概是通过transformer模型,为doc中的term计算weight,这样与传统的BM25等基于频率的方法相比,sparse向量可以利用神经网络的力量,提高了检索的准确性和效率。BM25虽然能够计算文档的相关性,但它无法理解词语的含义或上下文的重要性。而稀疏向量则能够通过神经网络捕捉到这些细微的差别。

稀疏向量的优势

  1. 计算效率:稀疏向量在处理包含零元素的操作时,通常比密集向量更高效。
  2. 信息密度:稀疏向量专注于关键特征,而不是捕捉所有细微的关系,这使得它们在文本搜索等应用中更为高效。
  3. 领域适应性:稀疏向量在处理专业术语或罕见关键词时表现出色,例如在医疗领域,许多专业术语不会出现在通用词汇表中,稀疏向量能够更好地捕捉这些术语的细微差别

稀疏向量举例

SPLADE 是一款开源的transformer模型,提供sparse向量生成,下面是效果对比,可以看到sparse介于BM25和dense之间,比BM25效果好。

Model MRR@10 (MS MARCO Dev) Type
BM25 0.184 Sparse
TCT-ColBERT 0.359 Dense
doc2query-T5 link 0.277 Sparse
SPLADE 0.322 Sparse
SPLADE-max 0.340 Sparse
SPLADE-doc 0.322 Sparse
DistilSPLADE-max 0.368 Sparse

Sparse稀疏检索实践

模型介绍

国内的开源模型中,BAAI的BGE-M3提供sparse向量向量生成能力,我们用这个来进行实践。

BGE是通过RetroMAE的预训练方式训练的类似bert的预训练模型。

常规的Bert预训练采用了将输入文本随机Mask再输出完整文本这种自监督式的任务,RetroMAE采用一种巧妙的方式提高了Embedding的表征能力,具体操作是:将低掩码率的的文本A输入到Encoder种得到Embedding向量,将该Embedding向量与高掩码率的文本A输入到浅层的Decoder向量中,输出完整文本。这种预训练方式迫使Encoder生成强大的Embedding向量,在表征模型中提升效果显著。

image.png

向量生成

  • 先安装

    !pip install -U FlagEmbedding

  • 然后引入模型

  1. from FlagEmbedding import BGEM3FlagModel
  2. model = BGEM3FlagModel('BAAI/bge-m3',  use_fp16=True)

编写一个函数用于计算embedding:

  1. def embed_with_progress(model, docs, batch_size):
  2. batch_count = int(len(docs) / batch_size) + 1
  3. print("start embedding docs", batch_count)
  4. query_embeddings = []
  5. for i in tqdm(range(batch_count), desc="Embedding...", unit="batch"):
  6. start = i * batch_size
  7. end = min(len(docs), (i + 1) * batch_size)
  8. if end <= start:
  9. break
  10. output = model.encode(docs[start:end], return_dense=False, return_sparse=True, return_colbert_vecs=False)
  11. query_embeddings.extend(output['lexical_weights'])
  12. return query_embeddings

然后分别计算query和doc的:

  1. query_embeddings = embed_with_progress(model, test_sets.queries, batch_size)
  2. doc_embeddings = embed_with_progress(model, test_sets.docs, batch_size)

然后是计算query和doc的分数,model.compute_lexical_matching_score(交集的权重相乘,然后累加),注意下面的代码是query和每个doc都计算了,计算量会比较大,在工程实践中需要用类似向量索引的方案(当前qdrant、milvus等都提供sparse检索支持)

  1. # 检索topk
  2. recall_results = []
  3. import numpy as np
  4. for i in tqdm(range(len(test_sets.query_ids)), desc="recall...", unit="query"):
  5. query_embeding = query_embeddings[i]
  6. query_id = test_sets.query_ids[i]
  7. if query_id not in test_sets.relevant_docs:
  8. continue
  9. socres = [model.compute_lexical_matching_score(query_embeding, doc_embedding) for doc_embedding in doc_embeddings]
  10. topk_doc_ids = [test_sets.doc_ids[i] for i in np.argsort(socres)[-20:][::-1]]
  11. recall_results.append(json.dumps({"query": test_sets.queries[i], "topk_doc_ids": topk_doc_ids, "marked_doc_ids": list(test_sets.relevant_docs[query_id].keys())}))
  12. # recall_results 写入到文件
  13. with open("recall_results.txt", "w", encoding="utf-8") as f:
  14. f.write("\n".join(recall_results))

最后,基于测试集,我们可以计算召回率:

  1. import json
  2. # 读取 JSON line 文件
  3. topk_doc_ids_list = []
  4. marked_doc_ids_list = []
  5. with open("recall_results.txt", "r") as file:
  6. for line in file:
  7. data = json.loads(line)
  8. topk_doc_ids_list.append(data["topk_doc_ids"])
  9. marked_doc_ids_list.append(data["marked_doc_ids"])
  10. # 计算 recall@k
  11. def recall_at_k(k):
  12. recalls = []
  13. for topk_doc_ids, marked_doc_ids in zip(topk_doc_ids_list, marked_doc_ids_list):
  14. # 提取前 k 个召回结果
  15. topk = set(topk_doc_ids[:k])
  16. # 计算交集
  17. intersection = topk.intersection(set(marked_doc_ids))
  18. # 计算 recall
  19. recall = len(intersection) / min(len(marked_doc_ids), k)
  20. recalls.append(recall)
  21. # 计算平均 recall
  22. average_recall = sum(recalls) / len(recalls)
  23. return average_recall
  24. # 计算 recall@5, 10, 20
  25. recall_at_5 = recall_at_k(5)
  26. recall_at_10 = recall_at_k(10)
  27. recall_at_20 = recall_at_k(20)
  28. print("Recall@5:", recall_at_5)
  29. print("Recall@10:", recall_at_10)
  30. print("Recall@20:", recall_at_20)

在测试集中,测试结果:

  1. Recall@5: 0.7350086355785777
  2. Recall@10: 0.8035261945883735
  3. Recall@20: 0.8926130345462158

在这个测试集上,比BM25测试出来的结果要更好,但是仅凭这个尚不能否定BM25,需要综合看各自的覆盖度,综合考虑成本与效果。

参考

原文链接:https://www.cnblogs.com/xiaoqi/p/18135929/sparse_retrieval

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号