2023.12.5 deepsetAI发布了最新beta版本的haystack2.0 项目整体结构大改。 而其分词分句功能目前没有加入,也许以后都不会加入。 因此本教程适用于haystack2.0版本之前的。请根据需要下载版本,并查看官方文档。 在2.0版本之前,haystack也存在很多实用性组件,不代表2.0版本会比之前功能更多。
写在开头,本系列文章,目的在将deepsetAI-haystack的nlp在中文领域应用讲解透彻,以实用角度出发,从安装部署,到实际应用,全讲。个人觉得,应该能在nlp领域广泛应用,可以用来解决训练集数据问题,大模型外接知识库问题,等。
前情提要
在上一篇内容中,我们安装好了官方版本的haystack,并测试了一个简单的例子,给了一些文档,将文档预处理之后,写入到内存中作为document_store,并通过bm25关键词检索,对用户输入的query问题检索到top_k个答案,并调用QA模型对检索出的文档进行解析。
如果还没看过的小伙伴,可以看我上一篇文章:
DeepsetAI-haystack中文场景下使用(二)haystack的安装
当然,调用QA模型有一个特点,就是回答基本都是言简意赅。 但是可以将搜索结果处理后接入大模型解析。这个会在以后的文章中更新。 最重要的是检索的精度问题。 以及检索速度的问题。
今天我们根据官方教程中,基础使用的另一个例子来看一下中文情况下,表现有多差。
并且我们会将haystack的两个py文件进行替换,让其支持中文!
下面开始
利用现有的常见问题解答,来回答问题。
官方教程链接:Utilizing Existing FAQs for Question Answering | Haystack
为了在中文语境上测试该教程,我收集了一些常见的医疗 问题—答案 的数据集。
度盘链接:
链接:https://pan.baidu.com/s/1CZ0Pthwy_jERXJSWPOceNQ
提取码:1234
里面有2个数据集,一个是官方问答数据集,是英文的。另一个是中文的医疗问答类数据集。
英文部分,自己可以跑一下。本篇主要测试中文问答。
中文医疗问答数据集大致是这个样子。
department,title,ask,answer
心血管科,高血压患者能吃党参吗?,我有高血压这两天女婿来的时候给我拿了些党参泡水喝,您好高血压可以吃党参吗?,高血压病人可以口服党参的。党参有降血脂,降血压的作用,可以彻底消除血液中的垃圾,从而对冠心病以及心血管疾病的患者都有一定的稳定预防工作作用,因此平时口服党参能远离三高的危害。另外党参除了益气养血,降低中枢神经作用,调整消化系统功能,健脾补肺的功能。感谢您的进行咨询,期望我的解释对你有所帮助。
包含了问题所属科室,问题的标题,问题详细描述,以及医生的解答。
该任务完成的是,根据语义匹配,匹配到用户输入的问题,在该数据集中最相似的问题,并把该问题的答案返还给用户。
代码如下:
import logging
logging.basicConfig(format="%(levelname)s - %(name)s - %(message)s", level=logging.WARNING)
logging.getLogger("haystack").setLevel(logging.INFO)
from haystack.nodes import EmbeddingRetriever
from haystack.document_stores import InMemoryDocumentStore
# 创建一个存储在内存中的document_store
document_store = InMemoryDocumentStore()
from haystack.nodes import EmbeddingRetriever
retriever = EmbeddingRetriever(
document_store=document_store,
embedding_model="sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
use_gpu=True,
scale_score=False,
)
# retriever = EmbeddingRetriever(
# document_store=document_store,
# embedding_model="uer/sbert-base-chinese-nli",
# use_gpu=True,
# scale_score=False,
# )
import pandas as pd
from haystack.utils import fetch_archive_from_http
# 下载数据集,这里我处理好了,记得修改好路径
doc_dir = "../data/tutorial4"
# s3_url = "https://s3.eu-central-1.amazonaws.com/deepset.ai-farm-qa/datasets/documents/small_faq_covid.csv.zip"
# fetch_archive_from_http(url=s3_url, output_dir=doc_dir)
# 读取csv文件
df = pd.read_csv(f"{doc_dir}/样例_内科5000-6000.csv")
# 去除空值
df.fillna(value="", inplace=True)
df["ask"] = df["ask"].apply(lambda x: x.strip())
print(df.head())
#将问题ask,经过embedding模型,映射为tensor,并写入dataframe的新的一列
questions = list(df["ask"].values)
df["embedding"] = retriever.embed_queries(queries=questions).tolist()
df = df.rename(columns={"ask": "content"})
# 将df数据转换为字典,因为document_store的写入接口,接收的数据是字典格式的
docs_to_index = df.to_dict(orient="records")
document_store.write_documents(docs_to_index)
# 创建FAQ管道,并添加管道组件检索器retriever
from haystack.pipelines import FAQPipeline
pipe = FAQPipeline(retriever=retriever)
from haystack.utils import print_answers
# 开始提问
prediction = pipe.run(query="癫痫用什么药物", params={"Retriever": {"top_k": 1}})
# 打印答案
print_answers(prediction, details="medium")
注意修改代码中的路径。
这里使用的embedding模型是一个文本相似度模型,支持50多种语言,包括中文。sentence-transformers/paraphrase-multilingual-mpnet-base-v2 · Hugging Face
各位也可以根据数据集中的问题来进行提问,但是注意,要换一个问法,我们需要让haystack进行多语义提问,而不是关键词搜索。 例如,在数据集中的问题是:癫痫会表现出哪些症状?,我们提问的时候可以提问,癫痫的表现是什么样的? 如果匹配到可以的答案,那么就可以了。
ps:数据embedding以及写入内存的过程比较耗时,可以将提问部分自行修改为while Ture, 等待接收问题。 这样可以省去每次执行脚本都要重新embedding写入,因为脚本执行结束,退出后,内存中存储的document_store会被释放。 本章后面会介绍一个持久化存储document_store的方法,其实也是调用数据库。
如果你已经测试了很多,发现,这不是基本可以用吗,中文的表现也不是很差啊,语义检索的精度,来自于文本相似度模型的表现啊。 根本不需要修改代码进行替换。
别急, 上一个文章说过,haystack之所以不支持中文,是因为分词分句的问题,英文的分词直接空格切分就好了,而中文要使用jieba。 在上面这个官方教程中,根本没使用到分词,分句。 并且问题ask的长度不长,如果按照title字段进行搜索,也就20个字符不到。 并且,基于已有问题搜索答案,本身实用性不是那么高,多是应用于已有固定解决方法或者答案。 比方说人工客服回复客户,常见的问题,基本都写好了。 也算是一种人工标注。
重点来了,如果只有文档,没有答案呢?我还要根据文档去找到答案。
各位首先想到的是langchain对吧。
其实haystack比langchain早了一两年。
接下来这个例子实用性较高,可扩展能力较高,请仔细阅读。
拿小板凳坐好。
官方教程 Better Retrieval with Embedding Retrieval | Haystack
任务说明:我有一大堆文档,没有人工标注,没有答案,就是一大堆文档。 我要在里面搜索一些内容的答案。
英文测试内容,去上面的链接自己跑。 如果有google_colab,可以直接点这里,链接一个免费的T4显卡,全部代码块执行,就可以。
准备一些中文数据集,百科词条
测试用的一共13个百科词条。文档长度不确定。
另外还有一个大的百科词条,一共636个文档。
度盘下载:
测试用数据data_test
链接:百度网盘 请输入提取码
提取码:znx6
解压密码:1234
完整数据:
链接:百度网盘 请输入提取码
提取码:c2g4
解压密码:1234
数据准备好了,代码如下:
import os
import logging
import time
logging.basicConfig(format="%(levelname)s - %(name)s - %(message)s", level=logging.WARNING)
logging.getLogger("haystack").setLevel(logging.INFO)
# 写入库
from haystack.document_stores import FAISSDocumentStore
from haystack.pipelines.standard_pipelines import TextIndexingPipeline
#创建库以及索引
document_store = FAISSDocumentStore(faiss_index_factory_str="Flat",embedding_dim=768)
#加载已有的document_store
# document_store = FAISSDocumentStore.load(index_path="wiki_faiss_index.faiss", config_path="wiki_faiss_index.json")
doc_dir = 'F:\MC-PROJECT\CUDA_Preject/test_haystack\wiki\data_test'
files_to_index = [doc_dir + "/" + f for f in os.listdir(doc_dir)]
indexing_pipeline = TextIndexingPipeline(document_store)
indexing_pipeline.run_batch(file_paths=files_to_index)
# print(document_store.get_all_documents())
from haystack.nodes import EmbeddingRetriever
retriever = EmbeddingRetriever(
document_store=document_store,
embedding_model="shibing624/text2vec-base-chinese",
)
document_store.update_embeddings(retriever)
document_store.save(index_path="wiki_faiss_index.faiss")
from haystack.nodes import FARMReader
reader = FARMReader(model_name_or_path="uer/roberta-base-chinese-extractive-qa", use_gpu=True,context_window_size=300,max_seq_len=512)
# 下面这个IDEA研究院的模型太大
# reader = FARMReader(model_name_or_path="IDEA-CCNL/Randeng-T5-784M-QA-Chinese", use_gpu=True)
from haystack.utils import print_answers
from haystack.pipelines import ExtractiveQAPipeline
pipe = ExtractiveQAPipeline(reader, retriever)
while True:
q = input('输入问题吧:')
st_time = time.time()
prediction = pipe.run(
query=q,
params={
"Retriever": {"top_k": 10},
"Reader": {"top_k": 5}
}
)
# print(prediction)
print_answers(prediction, details="all")
end_time = time.time()
print("计算时间为:{}".format(end_time - st_time))
说明:
- 首先,路径问题,不需要多说,自行修改。
- 使用的文本相似度模型为shibing624/text2vec-base-chinese,huggingface上开源,自行查看参数
- QA模型为uer/roberta-base-chinese-extractive-qa,同样HF上开源。
- 矢量存储库使用的是faiss,存储在本地,faiss的参数可修改地方很多,可以自行查看官方API文档,在我前面的文章有地址链接。
- 我写了while True循环,可以重复的提问。
执行成功后,应该是下面的样子:
我相信各位自行测试过的小伙伴,会发现,测试的结果有好有坏,有的还行,有的几乎不贴题。
如果你中间停止了脚本,再次执行一定会报以下错误。
注意看该脚本的同目录下,是不是多了这三个文件。
看代码这个部分:
from haystack.document_stores import FAISSDocumentStore
from haystack.pipelines.standard_pipelines import TextIndexingPipeline
#创建库以及索引
document_store = FAISSDocumentStore(faiss_index_factory_str="Flat",embedding_dim=768)
#加载已有的document_store
# document_store = FAISSDocumentStore.load(index_path="wiki_faiss_index.faiss", config_path="wiki_faiss_index.json")
解释: 创建document_store,创建的是faiss存储,因此,在同目录下,会出现后缀为db的文件,是将document_store进行本地化存储,同时另外两个,以faiss和json为后缀的文件,是faiss存储的索引以及配置文件。faiss存储也支持远程,需要填写地址以及存储名称,haystack会在远程服务器创建faiss持久化存储。
因此想要解决这个问题,你可以将这三个文件删除,删除后重新执行,就不会报错,但是又要进行一次文档处理,以及embedding的过程。如果文档量很大。 比如那个636个百科词条的文档,耗时会将近半小时。
那么另一种解决方法,就是将创建faiss的document_store代码注释掉,并把下面的document_store放开,代表不重新创建document_store,转而加载已有的document_store。
这样,就不需要等待重新写入这种漫长的过程了。
修改代码使其支持中文语境
分析一下,其中文表现效果时好时坏的原因。
分句,分词。 在文档预处理时,haystack使用了自定义的管道组件:
files_to_index = [doc_dir + "/" + f for f in os.listdir(doc_dir)]
indexing_pipeline = TextIndexingPipeline(document_store)
indexing_pipeline.run_batch(file_paths=files_to_index)
如果有兴趣ctrl跳转进TextIndexingPipeline,你会在里面看到preprocessor组件,这个就是对文档进行切分。
其规则会根据空行切分,也会根据英语的句号进行切分,也会根据回车符\n切分,等等。
但是,现在使用的是中文,因此只有\n,或者空行,生效。
看一下数据。你会发现。假设我们需要将文档切分成,300个字符的小文档。 同时尊重句子边界(也就是在不超过300字符的情况下,保证句子完整。 也就是说,如果下一个句子加进来,小文档的长度超过了300个字符,则省去下一个句子, 来保证不超过300个字符,同时不让每个小文档的最后一个句子,只说了一半话,就被截断了)
preprocessor类的接口如下,有兴趣请查一下官方接口文档,文档预处理,对于检索的准确度有一定影响。举个简单的例子,模型支持输入512个字符的话,如果文档切分长度为600,那么会被截断的。 诸如此类。
各位明显会看到language里面的参数是'en'。
别急,马上就是如何修改,使其支持中文。
我修改好了文件,并上传至github。只需要将我提供的文件,替换haystack同名原文件即可。
GitHub - mc112611/haystack-chinese: :mag: 基于deepsetAI的开源项目haystack进行修改,使其支持中文场景下的任务
里面两个preprocessor.py和txt.py都替换原文件,原文件的位置如下
preprocessor.py:你的解释器环境+\Lib\site-packages\haystack\nodes\preprocessor
txt.py:你的解释器环境\Lib\site-packages\haystack\nodes\file_converter
大家会在上面的目录下,找到同名文件,替换掉即可。
我们看一下替换后的preprocessor预处理器。
首先在语种加入了zh
参数等设置为默认。实际上,preprocessor为文档预处理器,这个组件是可以单独被调用的,用法不在官方教程中,需要自己去研究haystack的官方文档,以及接口文档。 可以作为管道组件添加到pipeline中的任意位置。本篇文章不提供这些复杂用法的教程。
替换完毕之后就可以支持中文了。
再执行一次,会发现表现效果好了很多,文档切分也更舒适了。
记得删除那三个faiss文件,因为这次我们要重新对文档进行处理。
PS:当然,文档质量也影响检索的结果,这里我提供的wiki文档,没有做数据清洗,会有一些符号等。如果有小伙伴想实际应用,注意这一点哦。
最佳化,等内容,在官方文档都有非常详细的使用说明。 个人总结起来肯定不如官方做的全面细致。 当然,我会尽可能在后面的文章中去写一些, 但是,一切请以官方文档为准。
个人提供这个修改源码的文件,基本上在我个人测试官方支持功能的时候,基本上没遇到问题。如果有问题,欢迎交流。
到这里,基本上haystack已经可以应用于中文场景了。
最大的问题解决了,剩下的就是照着官方教程,以及官方文档,去实际操作,包括接入翻译器,接入文本转语音,接入web搜索引擎,接入聊天应用程序,或者接入文本到图像的图文搜索。
下一章,我会更新如何微调模型,当应用某些特定领域时候,训练文本相似度模型,QA模型,使用哪些,或者微调这类模型。 有兴趣的可以看一下 sentence_transformer ,做文本相似度模型最好的库。