弁言
RAG作为淘汰模型幻觉和让模型分析、答复私域相干知识最简朴高效的方式,我们除了使用之外可以实验相识其是怎样实现的。在实现RAG的过程中,最重要的是包管召回的知识的准确性,不然会极大影响LLM的能力,而混合检索是一个重要的方法去提高召回RAG的准确性。
单检索的局限性
拿最常用的关键词检索举例,它通过匹配用户输入的关键词与文档中的关键词来返回相干效果。然而,关键词检索存在以下几个显着的局限性:
- 语义缺失:关键词检索无法明白词语之间的语义关系。例如,用户搜索“狗”时,系统大概无法明白“犬”与“狗”是同义词,从而导致相干文档被遗漏。
- 多义词问题:关键词检索无法处置惩罚多义词。例如,“苹果”既可以指水果,也可以指科技公司。如果用户搜索“苹果”,系统无法确定用户指的是哪一种含义,从而返回不相干的效果。
单检索例子
好比下面这段代码,我们使用text1去在text2和text3中匹配最符合的文档:
- def main():
- """
- 主函数,用于测试和演示
- """
- text1 = '把商品发到闲鱼'
- text2 = '我想将商品挂到闲鱼'
- text3 = '我想找闲鱼问下商品'
- # calculate_tfidf_similarity: 通过两个文本TF-IDF相似度计算相似度
- tfidf_similarities2 = calculate_tfidf_similarity(text1, text2)
- tfidf_similarities3 = calculate_tfidf_similarity(text1, text3)
- print(f"\n匹配句子1得分:{tfidf_similarities2[0]} \n\n匹配句子2得分: {tfidf_similarities3[0]} \n\n")
复制代码 获取到的效果:
- 匹配句子2得分:0.8164965809277259
- 匹配句子3得分:0.8164965809277259
复制代码 可以看到我们肉眼可见的text1与text2更匹配,但因为三个句子中都包罗商品和闲鱼,以是两个句子都匹配到了0.8164965809277259,我们但从关键词匹配根本无法分辨召回哪个文本更好,但是关键词检索并不是一无是处,对很多文档的检索都有关键功能,而在必要保留关键词检索的同时又能分辨这种句子,我们就必要引入语义检索,让他们两种方法工作达到混合检索的功能。
混合检索
混合检索(Hybrid Retrieval)是一种结合了多种检索方法的策略,旨在提高检索效果的质量和多样性。通过结合不同检索方法的优势,混合检索可以更好地满意用户的需求,并提供更准确、更全面的检索效果。
拆解实现
下面我们来实验同时引入关键词检索和语义检索。
- def main():
- """
- 主函数,用于测试和演示
- """
- text1 = '把商品发到闲鱼'
- text2 = '我想将商品挂到闲鱼'
- text3 = '我想找闲鱼问下商品'
- # 通过两个文本TF-IDF相似度计算相似度
- tfidf_similarities2 = calculate_tfidf_similarity(text1, text2)
- tfidf_similarities3 = calculate_tfidf_similarity(text1, text3)
- # 通过两个文本的嵌入相似度计算相似度
- embedding_similarities2 = calculate_similarity(text1, text2)
- embedding_similarities3 = calculate_similarity(text1, text3)
- print(f"\n\n语义搜索句子1 {embedding_similarities2[0]} \n\n语义搜索句子2: {embedding_similarities3[0]}")
复制代码 我们先看看语义检索的效果:
- 语义搜索句子1 ('我想将商品挂到闲鱼', 0.8553742925917707)
- 语义搜索句子2: ('我想找闲鱼问下商品', 0.6846143988983046)
复制代码 好的,很显着可以看到在关键词的比力相似的情况下,我们使用语义搜索可以清晰地分出哪个句子更符合我们的需求。接下来我们来将两个搜索结合起来,并举行加权盘算得分(让用户可以根据自己必要决定是语义搜索得分更高还是关键词搜索得分更高),从而得到终极的检索效果。
- def main():
- """
- 主函数,用于测试和演示
- """
- text1 = '把商品发到闲鱼'
- text2 = '我想将商品挂到闲鱼'
- text3 = '我想找闲鱼问下商品'
- # 通过两个文本TF-IDF相似度计算相似度
- tfidf_similarities2 = calculate_tfidf_similarity(text1, text2)
- tfidf_similarities3 = calculate_tfidf_similarity(text1, text3)
- embedding_similarities2 = calculate_similarity(text1, text2)
- embedding_similarities3 = calculate_similarity(text1, text3)
-
- Semantic_Proportio = 0.8
- Word_Proportion = 0.2
- # 根据传进来的权重计算最终得分
- final_score2 = calculate_final_score(embedding_similarities2[0][1], tfidf_similarities2[0], Semantic_Proportio, Word_Proportion)
- final_score3 = calculate_final_score(embedding_similarities3[0][1], tfidf_similarities3[0], Semantic_Proportio, Word_Proportion)
- print(f"最终语句1得分: {final_score2} \n\n最终语句2得分: {final_score3}")
复制代码 效果:
- 最终语句1得分: 0.8475987502589617
- 最终语句2得分: 0.7109908353041888
复制代码 ok,可以看到我们通过混合检索的方式,可以更准确地找到与用户输入最相干的文档,从而提高检索效果的质量和准确性。
完备代码
关键词检索和语义检索的具体实现之前我的文章已经提过了,这里不在赘述,直接贴完备代码。注意把key换成qwen中你自己的key
- import dashscope
- from http import HTTPStatus
- import numpy as np
- import jieba
- from jieba.analyse import extract_tags
- import math
- # 初始化dashscope,替换qwen的api key
- dashscope.api_key = 'sk-xxxx'
- def embed_text(text):
- """
- 使用dashscope API获取文本的嵌入向量
- :param text: 输入的文本
- :return: 文本的嵌入向量,如果失败则返回None
- """
- resp = dashscope.TextEmbedding.call(
- model=dashscope.TextEmbedding.Models.text_embedding_v2,
- input=text)
- if resp.status_code == HTTPStatus.OK:
- return resp.output['embeddings'][0]['embedding']
- else:
- print(f"Failed to get embedding: {resp.status_code}")
- return None
- def cosine_similarity(vec1, vec2):
- """
- 计算两个向量之间的余弦相似度
- :param vec1: 第一个向量
- :param vec2: 第二个向量
- :return: 余弦相似度
- """
- dot_product = np.dot(vec1, vec2)
- norm_vec1 = np.linalg.norm(vec1)
- norm_vec2 = np.linalg.norm(vec2)
- return dot_product / (norm_vec1 * norm_vec2)
- def calculate_similarity(text1, text2):
- """
- 计算两个文本之间的相似度
- :param text1: 第一个文本
- :param text2: 第二个文本,可以包含多个句子,用逗号分隔
- :return: 每个句子的相似度列表,格式为 (句子, 相似度)
- """
- embedding1 = embed_text(text1)
- if embedding1 is None:
- return []
- similarities = []
- sentences = [sentence.strip() for sentence in text2.split(',') if sentence.strip()]
- for sentence in sentences:
- embedding2 = embed_text(sentence)
- if embedding2 is None:
- continue
- similarity = cosine_similarity(embedding1, embedding2)
- similarities.append((sentence, similarity))
- return similarities
- def extract_keywords(text):
- """
- 提取文本中的关键词
- :param text: 输入的文本
- :return: 关键词列表
- """
- return extract_tags(text)
- def cosine_similarity_tfidf(vec1, vec2):
- """
- 计算两个TF-IDF向量之间的余弦相似度
- :param vec1: 第一个TF-IDF向量
- :param vec2: 第二个TF-IDF向量
- :return: 余弦相似度
- """
- intersection = set(vec1.keys()) & set(vec2.keys())
- numerator = sum(vec1[x] * vec2[x] for x in intersection)
- sum1 = sum(vec1[x] ** 2 for x in vec1)
- sum2 = sum(vec2[x] ** 2 for x in vec2)
- denominator = math.sqrt(sum1) * math.sqrt(sum2)
- return numerator / denominator if denominator else 0.0
- def calculate_tfidf_similarity(text, text2):
- """
- 计算两个文本之间的TF-IDF相似度
- :param text: 第一个文本
- :param text2: 第二个文本,可以包含多个文档,用竖线分隔
- :return: 每个文档的TF-IDF相似度列表
- """
- documents = [doc for doc in text2.split('|') if doc.strip()]
- query_keywords = extract_keywords(text)
- documents_keywords = [extract_keywords(doc) for doc in documents]
- query_keyword_counts = {kw: query_keywords.count(kw) for kw in set(query_keywords)}
- total_documents = len(documents)
- all_keywords = set(kw for doc in documents_keywords for kw in doc)
- keyword_idf = {kw: math.log((1 + total_documents) / (1 + sum(1 for doc in documents_keywords if kw in doc))) + 1 for kw in all_keywords}
- query_tfidf = {kw: count * keyword_idf.get(kw, 0) for kw, count in query_keyword_counts.items()}
- documents_tfidf = [{kw: doc.count(kw) * keyword_idf.get(kw, 0) for kw in set(doc)} for doc in documents_keywords]
- return [cosine_similarity_tfidf(query_tfidf, doc_tfidf) for doc_tfidf in documents_tfidf]
- def calculate_final_score(embedding_similarity, tfidf_similarity, w1=0.5, w2=0.5):
- """
- 计算最终得分,结合语义相似度和TF-IDF相似度
- :param embedding_similarity: 语义相似度
- :param tfidf_similarity: TF-IDF相似度
- :param w1: 语义相似度的权重
- :param w2: TF-IDF相似度的权重
- :return: 最终得分
- """
- return w1 * embedding_similarity + w2 * tfidf_similarity
- def main():
- """
- 主函数,用于测试和演示
- """
- text1 = '把商品发到闲鱼'
- text2 = '我想将商品挂到闲鱼'
- text3 = '我想找闲鱼问下商品'
- tfidf_similarities2 = calculate_tfidf_similarity(text1, text2)
- tfidf_similarities3 = calculate_tfidf_similarity(text1, text3)
- embedding_similarities2 = calculate_similarity(text1, text2)
- embedding_similarities3 = calculate_similarity(text1, text3)
- Semantic_Proportio = 0.8
- Word_Proportion = 0.2
- final_score2 = calculate_final_score(embedding_similarities2[0][1], tfidf_similarities2[0], Semantic_Proportio, Word_Proportion)
- final_score3 = calculate_final_score(embedding_similarities3[0][1], tfidf_similarities3[0], Semantic_Proportio, Word_Proportion)
- print(f"最终语句1得分: {final_score2} \n\n最终语句2得分: {final_score3}")
- if __name__ == '__main__':
- main()
复制代码 总结
混合检索技能通过结合关键词检索和语义检索的优势,实现了多路召回,从而提高了检索的准确性和全面性。把握上面我们提到的混合检索,不仅你可以根据自己的实际情况去对多种检索方式的权重举行加权,还可以根据自己的实际情况去调解对应的召回策略,对我们自建RAG检索有着极大资助,希望本文能对你有启示。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |