11语音与语言处理(第3版)

问答系统

智能问答

问答系统阅读理解知识图谱

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第11章:问答系统(Question Answering)

一、导读

1.1 本章学习目标

问答系统(QA)是 NLP 中最直接面向用户的应用之一。它的目标是让计算机能够理解用户的自然语言问题,并从给定的信息源中找到准确的答案。从搜索引擎的智能摘要到智能助手,问答系统是连接人与知识的关键桥梁。

本章的核心学习目标包括:

  • 理解问答系统的分类:抽取式、生成式、开放域、知识库问答
  • 掌握抽取式问答:SQuAD 数据集、BERT for QA 的架构和训练
  • 理解生成式问答:从 Seq2Seq 到大语言模型的问答范式
  • 掌握开放域问答:检索增强生成(RAG)的架构
  • 了解知识库问答:知识图谱上的问答
  • 理解问答系统的评估方法:EM、F1、BLEU 等指标

1.2 为什么本章如此重要

问答系统是 NLP 技术集大成者。一个完整的问答系统需要综合运用文本理解(理解问题)、信息检索(找到相关文档)、推理(推断答案)和语言生成(组织答案)等多种能力。

问答系统的发展历程反映了 NLP 技术的进步:

  • 早期:基于规则和模板的问答(如 ELIZA)
  • 中期:基于信息检索的问答
  • 近期:基于深度学习的抽取式问答(BERT on SQuAD)
  • 当前:基于大语言模型的生成式问答(ChatGPT、RAG)

理解问答系统,就是理解 NLP 从"理解"到"推理"到"生成"的完整能力链。

1.3 核心问题引导

问答系统有哪些类型?它们各自适用于什么场景?

抽取式问答如何定位答案?BERT 是如何解决这个问题的?

开放域问答面临什么挑战?RAG 如何解决?

生成式问答与抽取式问答有什么本质区别?

大语言模型如何改变了问答系统的设计?

问答系统如何评估?自动指标与人工评估的关系是什么?


二、核心概念详解

11.1 问答系统分类

11.1.1 抽取式问答(Extractive QA)

从给定文档中抽取连续的文本片段作为答案。

输入:问题 + 上下文文档

输出:文档中的答案片段

问题:谁创办了苹果公司?
上下文:苹果公司由史蒂夫·乔布斯、斯蒂夫·沃兹尼亚克和罗纳德·韦恩于1976年4月1日创立。
答案:史蒂夫·乔布斯、斯蒂夫·沃兹尼亚克和罗纳德·韦恩

11.1.2 生成式问答(Generative QA)

根据问题和相关信息生成自然语言答案。答案不一定是原文的片段。

输入:问题 + 上下文/知识

输出:自然语言答案

11.1.3 开放域问答(Open-Domain QA)

在大规模文档集合(如整个维基百科)中回答问题。需要结合信息检索和阅读理解。

11.1.4 知识库问答(Knowledge Base QA)

在结构化知识库(如知识图谱)上查询答案。需要将自然语言问题转换为结构化查询。

11.2 抽取式问答

11.2.1 问题定义

给定问题 $q$ 和上下文文档 $d = (w_1, w_2, \ldots, w_n)$,找到答案在文档中的起始位置 $s$ 和结束位置 $e$。

11.2.2 BERT for QA

BERT 的 QA 模型在输入端拼接问题和上下文,然后预测答案的起始和结束位置:

[CLS] 问题 [SEP] 上下文 [SEP]
python
from transformers import BertForQuestionAnswering, BertTokenizer
import torch

tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
model = BertForQuestionAnswering.from_pretrained("bert-base-uncased")

question = "Who founded Apple Inc.?"
context = "Apple Inc. was founded by Steve Jobs, Steve Wozniak, and Ronald Wayne on April 1, 1976."

inputs = tokenizer(question, context, return_tensors="pt", max_length=512, truncation=True)

with torch.no_grad():
    outputs = model(**inputs)
    start_logits = outputs.start_logits
    end_logits = outputs.end_logits

start_idx = torch.argmax(start_logits, dim=-1)
end_idx = torch.argmax(end_logits, dim=-1)

answer = tokenizer.decode(inputs["input_ids"][0][start_idx:end_idx+1])
print(f"Answer: {answer}")

11.2.3 模型架构

python
class BertQA(nn.Module):
    def __init__(self, pretrained_model_name="bert-base-uncased"):
        super().__init__()
        self.bert = AutoModel.from_pretrained(pretrained_model_name)
        self.qa_outputs = nn.Linear(self.bert.config.hidden_size, 2)  # start, end

    def forward(self, input_ids, attention_mask, token_type_ids):
        outputs = self.bert(input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        sequence_output = outputs.last_hidden_state
        logits = self.qa_outputs(sequence_output)
        start_logits, end_logits = logits.split(1, dim=-1)
        return start_logits.squeeze(-1), end_logits.squeeze(-1)

11.2.4 训练

python
def compute_loss(start_logits, end_logits, start_positions, end_positions):
    loss_fn = nn.CrossEntropyLoss()
    start_loss = loss_fn(start_logits, start_positions)
    end_loss = loss_fn(end_logits, end_positions)
    return (start_loss + end_loss) / 2

11.3 开放域问答

11.3.1 架构

开放域 QA 系统通常包含两个组件:

检索器(Retriever):从大规模文档集合中检索相关文档

阅读器(Reader):从检索到的文档中提取答案

python
# 简化的开放域 QA 流程
class OpenDomainQA:
    def __init__(self, retriever, reader):
        self.retriever = retriever
        self.reader = reader

    def answer(self, question, top_k=5):
        # 1. 检索相关文档
        documents = self.retriever.search(question, top_k=top_k)

        # 2. 从每个文档中提取答案
        answers = []
        for doc in documents:
            answer = self.reader.extract_answer(question, doc)
            answers.append(answer)

        # 3. 选择最佳答案
        best_answer = self.select_best(answers)
        return best_answer

11.3.2 DPR(Dense Passage Retrieval)

DPR 使用双编码器架构进行文档检索:

  • 问题编码器:将问题编码为向量
  • 文档编码器:将文档编码为向量
  • 通过向量相似度检索最相关的文档
python
from transformers import DPRQuestionEncoder, DPRContextEncoder, DPRTokenizer

question_encoder = DPRQuestionEncoder.from_pretrained("facebook/dpr-question_encoder-single-nq-base")
context_encoder = DPRContextEncoder.from_pretrained("facebook/dpr-ctx_encoder-single-nq-base")
tokenizer = DPRTokenizer.from_pretrained("facebook/dpr-question_encoder-single-nq-base")

# 编码问题
question = "Who founded Apple?"
q_inputs = tokenizer(question, return_tensors="pt")
q_embedding = question_encoder(**q_inputs).pooler_output

# 编码文档并计算相似度
documents = ["Apple was founded by Steve Jobs.", "Google was founded by Larry Page."]
d_inputs = tokenizer(documents, return_tensors="pt", padding=True)
d_embeddings = context_encoder(**d_inputs).pooler_output

# 计算相似度
similarities = torch.mm(q_embedding, d_embeddings.T)
print(similarities)

11.4 检索增强生成(RAG)

11.4.1 动机

大语言模型有两个主要局限:

  • 知识截止:不知道训练数据截止日期之后的事件
  • 幻觉:可能生成看似合理但事实错误的内容

RAG 通过先检索相关信息再生成答案来缓解这些问题。

11.4.2 RAG 架构

用户问题 → 检索器 → 相关文档 → 生成器(LLM + 检索结果)→ 答案
python
from transformers import RagTokenizer, RagRetriever, RagSequenceForGeneration

tokenizer = RagTokenizer.from_pretrained("facebook/rag-sequence-nq")
retriever = RagRetriever.from_pretrained("facebook/rag-sequence-nq", index_name="legacy")
model = RagSequenceForGeneration.from_pretrained("facebook/rag-sequence-nq", retriever=retriever)

question = "Who founded Apple Inc.?"
inputs = tokenizer(question, return_tensors="pt")
generated = model.generate(inputs["input_ids"])
answer = tokenizer.batch_decode(generated, skip_special_tokens=True)
print(f"Answer: {answer}")

11.4.3 使用 LLM + 向量数据库构建 RAG

python
from langchain.vectorstores import FAISS
from langchain.embeddings import OpenAIEmbeddings
from langchain.llms import OpenAI
from langchain.chains import RetrievalQA

# 构建向量数据库
documents = [
    "Apple Inc. was founded on April 1, 1976 by Steve Jobs, Steve Wozniak, and Ronald Wayne.",
    "The company is headquartered in Cupertino, California.",
    "Tim Cook has been CEO since 2011.",
]
vectorstore = FAISS.from_texts(documents, OpenAIEmbeddings())

# 构建 QA 链
qa_chain = RetrievalQA.from_chain_type(
    llm=OpenAI(),
    retriever=vectorstore.as_retriever(),
    chain_type="stuff",
)

# 问答
result = qa_chain.run("Who founded Apple?")
print(result)

11.5 知识库问答

11.5.1 任务定义

给定知识图谱和自然语言问题,找到答案。

知识图谱:(Steve Jobs, founder_of, Apple Inc.)
问题:谁创办了苹果公司?
答案:Steve Jobs

11.5.2 方法

基于语义解析:将自然语言转换为 SPARQL 查询。

基于信息检索:将问题编码为向量,在知识图谱的三元组中检索。

基于 LLM:使用大语言模型直接推理。

11.6 评估方法

11.6.1 抽取式 QA 评估

精确匹配(EM, Exact Match):预测答案与标准答案完全一致的比例。

F1 分数:基于 token 级别的精确率和召回率的调和平均。

python
def compute_f1(prediction, ground_truth):
    pred_tokens = set(prediction.lower().split())
    gt_tokens = set(ground_truth.lower().split())

    if len(pred_tokens) == 0 or len(gt_tokens) == 0:
        return float(pred_tokens == gt_tokens)

    common = pred_tokens & gt_tokens
    precision = len(common) / len(pred_tokens)
    recall = len(common) / len(gt_tokens)
    f1 = 2 * precision * recall / (precision + recall)
    return f1

# 示例
print(compute_f1("Steve Jobs", "Steve Jobs"))  # 1.0
print(compute_f1("Steve Jobs and Wozniak", "Steve Jobs"))  # 0.667

11.6.2 生成式 QA 评估

  • BLEU:n-gram 精确率
  • ROUGE:召回率导向的 n-gram 重叠
  • BERTScore:基于 BERT 嵌入的语义相似度

三、实战案例

3.1 使用 Hugging Face Pipeline 进行问答

python
from transformers import pipeline

qa_pipeline = pipeline("question-answering", model="deepset/roberta-base-squad2")

context = """
Albert Einstein was a German-born theoretical physicist.
He developed the theory of relativity. His mass-energy equivalence
formula E = mc² is one of the most famous equations in physics.
He was awarded the Nobel Prize in Physics in 1921.
"""

result = qa_pipeline(
    question="When did Einstein receive the Nobel Prize?",
    context=context,
)
print(f"Answer: {result['answer']}")
print(f"Score: {result['score']:.4f}")

3.2 构建简单的 RAG 系统

python
import numpy as np
from sentence_transformers import SentenceTransformer

class SimpleRAG:
    def __init__(self, documents, model_name="all-MiniLM-L6-v2"):
        self.model = SentenceTransformer(model_name)
        self.documents = documents
        self.embeddings = self.model.encode(documents)

    def retrieve(self, query, top_k=3):
        query_embedding = self.model.encode(query)
        similarities = np.dot(self.embeddings, query_embedding)
        top_indices = similarities.argsort()[-top_k:][::-1]
        return [(self.documents[i], similarities[i]) for i in top_indices]

    def answer(self, query):
        retrieved = self.retrieve(query)
        context = "\n".join([doc for doc, _ in retrieved])
        prompt = f"""Based on the following context, answer the question.

Context: {context}
Question: {query}
Answer:"""
        return prompt  # 实际应用中会调用 LLM 生成答案

# 使用
documents = [
    "Python was created by Guido van Rossum and first released in 1991.",
    "Python is known for its simple and readable syntax.",
    "Python supports multiple programming paradigms including OOP and functional programming.",
    "Popular Python frameworks include Django, Flask, and FastAPI.",
]
rag = SimpleRAG(documents)
print(rag.answer("Who created Python?"))

3.3 在 SQuAD 上评估 QA 模型

python
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForQuestionAnswering
import torch

# 加载 SQuAD 数据集
dataset = load_dataset("squad")
tokenizer = AutoTokenizer.from_pretrained("deepset/roberta-base-squad2")
model = AutoModelForQuestionAnswering.from_pretrained("deepset/roberta-base-squad2")

# 评估前 100 个样本
correct = 0
total = 0

for example in dataset["validation"].select(range(100)):
    question = example["question"]
    context = example["context"]
    ground_truth = example["answers"]["text"][0]

    inputs = tokenizer(question, context, return_tensors="pt", max_length=512, truncation=True)
    with torch.no_grad():
        outputs = model(**inputs)

    start_idx = torch.argmax(outputs.start_logits, dim=-1)
    end_idx = torch.argmax(outputs.end_logits, dim=-1)
    answer = tokenizer.decode(inputs["input_ids"][0][start_idx:end_idx+1])

    if answer.strip().lower() == ground_truth.strip().lower():
        correct += 1
    total += 1

print(f"Exact Match: {correct/total:.4f}")

四、本章小结

本章系统讲解了问答系统:

问答系统分类:抽取式、生成式、开放域、知识库问答各有适用场景。

抽取式问答:BERT for QA 通过预测答案的起始和结束位置实现抽取。在 SQuAD 上取得了突破性进展。

开放域问答:结合检索和阅读理解,在大规模文档集合中回答问题。DPR 使用双编码器进行高效检索。

RAG:检索增强生成结合外部知识和 LLM 的生成能力,缓解幻觉和知识截止问题。

知识库问答:在结构化知识上查询答案,需要语义解析或向量检索。

评估方法:抽取式用 EM 和 F1,生成式用 BLEU、ROUGE、BERTScore。

问答系统是 NLP 技术的集大成者。随着大语言模型的发展,问答系统正在从"检索-抽取"范式转向"检索-生成"范式,RAG 成为主流架构。


五、思考与练习

思考题

抽取式问答和生成式问答各有什么优缺点?在什么场景下你会选择哪种?

RAG 如何缓解大语言模型的幻觉问题?它有什么局限性?

开放域问答中,检索器的质量如何影响最终答案?如何优化检索器?

大语言模型(如 GPT-4)是否已经"解决"了问答问题?还有哪些挑战?

编程练习

使用 BERT 在 SQuAD 数据集上训练抽取式问答模型。比较不同学习率和训练轮数的效果。

构建一个简单的 RAG 系统:使用 FAISS 作为向量数据库,调用 GPT API 生成答案。评估答案质量。

实现一个多跳问答系统:给定多个文档,需要综合多个文档的信息才能回答问题。


11/12问答系统
0%