文档问答介绍
文档问答是自然语言处理(NLP)领域的一项重要的技术。在大模型问世之前,文档问答主要依赖检索技术,通过 ElasticSearch
等工具进行文档匹配,或者使用 query+document
的方式进行模型(模型通常依赖于多模态功能,结合文本、单词位置和图像)的训练,将文档分割后以 embedding
向量化的形式存储到 Milvus
等向量数据库中。当用户提出问题时,首先将问题向量化,再通过向量库匹配来召回 topK
文档,最后生成答案。
下图来源于langchain+chatglm.
langchain+chatglm方案:该项目实现原理如下图所示,过程包括加载文件 -> 读取文本 -> 文本分割 -> 文本向量化 -> 问句向量化 -> 在文本向量中匹配出与问句向量最相似的top k个 -> 匹配出的文本作为上下文和问题一起添加到prompt中 -> 提交给LLM生成回答。
文档问答任务,是指给定文档集和一个问题,自动分析文档内容并提供问题答案的任务。文档问答任务分为开放式和封闭式两种形式。
开放式文档问答是指问题的答案不一定出现在文档中,或出现在多篇文档中。这种方法需要NLP模型具有广泛的知识背景和推理能力。解决开放式文档问答任务的基础是信息检索技术,通常需要对问题和文档进行自然语言处理,然后将抽象问题与文档的语言内容进行比较,以查找与问题有关的段落或句子,最后从中提取答案。
封闭式文档问答是指问题的答案仅出现在给定文档中。与开放式不同,封闭式文档问答的答案具有显式的位置或者可以通过语言分析技术找到。封闭式文档问答任务需要对文档进行深入的语义分析,在检测到与问题相关的段落或句子之后,需要进行语法和语义分析来确定答案是否在该段落或句子中以及是否正确。
随着深度学习技术的发展,文档问答技术也得到了快速的发展。近年来,BERT
、GPT
等大型预训练模型的问世带来了文档问答技术的革命性变革,通过在大规模数据上进行预训练,大幅提高了模型在文档问答任务上的准确率。目前最新的技术包括 T5
、Transformer-XL
、UniLM
等超大规模的预训练模型。
文档问答基本流程
问答过程分为两个阶段。第一个阶段称为召回阶段,系统会根据用户的提问从文本库或知识库中检索相关的文本片段或知识点,利用传统的检索技术去召回可能的文档候选。第二个阶段称之为阅读理解阶段,会利用深度学习的机器阅读理解模型,从对应的候选文档里将答案抽取出来。
现在ChatGPT
的出现似乎让这种文档问答的方式多了一种选择,即让大模型记住所有的知识点和问题,然后直接将query
通过prompt
的方式投喂给大模型让其直接作答。但大模型的训练往往伴随着高成本,即便是微调也难以满足,巧妇难为无米之炊。量化(低精度推理)、p-tuning v2
, lora
等训练方式将GPU的显存使用的淋漓尽致,但也只能是作为简单的微调方式,要是想让大模型直接学会知识,还是步履维艰,难以迈出步子。
于是乎换马车,走旧路,即召回+阅读理解的方式。只是从阅读理解的方式可以通过在检索过程中拿到的文档+query+prompt的方式传给大模型进行问答,这样召回阶段显得更加重要了。
召回阶段的优化方案为:
- 优化 text_split 算法,使匹配出的结果作为上下文时能够提供更合理的推理/回答依据;
- 优化 embedding 模型,提升语义向量化的效果,使得语义匹配过程中能够匹配出最满足要求的文本段落作为上下文;
Text Split 算法
文档切割最简单的做法就是根据句子的长度来将进行切割,一般是根据embedding模型的max_seq_len
来进行设定的,即chunk_size
=max_seq_len
, 这样做的好处就是处理简单,文档内容没有重复,但是容易将答案切分到两端passages中,所以比较好的做法就是用滑动窗口的方式,Di={s1,s2,…,sn}D_i={s_1,s_2, …, s_n} 分隔为P1={s1,s2,..,si},P2={s2,s3,…,sp},…,Pk={sm,…,sn}P_1={s_1,s_2,..,s_i}, P_2={s_2, s_3, …,s_p}, …, P_k = {s_m,…,s_n}, 这样虽然数据量增加了,但是对应的召回率会提升。
下面这张图是我在医疗科普知识阅读理解数据集上做的实验。
Embedding 模型
Embedding这几年做语义匹配比较火的就是双塔模型Sentence Bert,大家可以去看看对应的论文,链接贴在下面
论文
2020-04 Dense Passage Retrieval for Open-Domain Question Answering
DPR是dense passage retrieval,是一个bi-encoder architecture。Given (question, passage/answer) pair, 方法非常直接,就是用基于BERT standard pre-trained encoding models来做一个dual encoder(文章里面用的就是two independent BERTs) :
- one encoder for questions
- one encoder for passages
并且objective就是直接maximize inner products of the question and relevant passage vector,而且是in batch of (question, passages)‘s的。
训练两个encoder分别对句子和文档进行embedding可以更好进行向量表征。因为一般情况下问句和文章的长度差距很大,如果使用同一个encoder来进行表征的话,很难将相似的句子映射到向量空间的相近位置。
- 论文:arxiv.org/abs/2004.04…
- github: github.com/facebookres…
2021-04 SimCSE: Simple Contrastive Learning of Sentence Embeddings
simces同时支持两种方式训练,对比学习的无监督方式,和二分类的监督学习。
代码调优
如果想自己调优的话,可以看看sentence_transforms
,里面有微调和training的方案,使用比较简单。
from sentence_transformers import SentenceTransformer, InputExample, losses
from torch.utils.data import DataLoader
#Define the model. Either from scratch of by loading a pre-trained model
model = SentenceTransformer('distilbert-base-nli-mean-tokens')
#Define your train examples. You need more than just two examples...
train_examples = [InputExample(texts=['My first sentence', 'My second sentence'], label=0.8),
InputExample(texts=['Another pair', 'Unrelated sentence'], label=0.3)]
#Define your train dataset, the dataloader and the train loss
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
train_loss = losses.CosineSimilarityLoss(model)
#Tune the model
model.fit(train_objectives=[(train_dataloader, train_loss)], epochs=1, warmup_steps=100)