🛠️ 模块6:指导项目——构建你的第一个问答(QA)系统

本模块预计时长: 1.5-2小时
目标: 构建一个完整的提取式问答系统,接收上下文文本和问题,并从文本中直接提取最可能的答案。
要求: 仅需之前模块学到的知识 + 代码编辑器(或Google Colab)。


课程6.1——什么是提取式问答?"生成"与"提取"的区别

开始之前,让我们明确要构建的QA类型。

🔹 生成式问答:
模型发明一个新的答案,用自己的话。

问题:"什么是Transformer?"
生成的答案:"Transformer是一种使用注意力机制处理序列的神经网络架构..."

🔹 提取式问答(我们要做的):
模型从上下文文本中提取一个字面片段。

问题:"什么是Transformer?"
上下文:"...Transformer,于2017年引入,是一种基于注意力的架构,同时处理所有单词..."
提取的答案:"一种基于注意力的架构,同时处理所有单词"

提取式问答的优势:

  • 不需要微调(有优秀的预训练模型)。
  • 答案始终忠实于源文本(不产生幻觉)。
  • 理想用于技术、法律、手册、文章等。

课程6.2——选择模型:我们的助手将是"deepset/roberta-base-squad2"

对于这个项目,我们将使用仅编码器模型,专门训练用于提取式问答。

🔹 选定模型: deepset/roberta-base-squad2

  • 基于RoBERTa(BERT的改进变体)。
  • 在SQuAD 2.0上训练(一个问答数据集,有些问题没有答案)。
  • 合理支持中文(尽管主要在英文上训练)。
  • 轻量级(base),在CPU上平稳运行。

🌐 你可以在模型中心查看


课程6.3——步骤1:初始设置和模型加载

首先,安装(如果之前未完成)并加载模型和分词器。

from transformers import AutoTokenizer, AutoModelForQuestionAnswering, pipeline

# 模型名称
model_name = "deepset/roberta-base-squad2"

# 加载分词器和模型
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForQuestionAnswering.from_pretrained(model_name)

# 可选:创建管道以简化
qa_pipeline = pipeline("question-answering", model=model, tokenizer=tokenizer)

课程6.4——步骤2:准备上下文和问题

我们将使用示例文本。可以是任何内容:文章、书籍摘录、手册等。

context = """
Transformers是2017年由Vaswani等人在论文"Attention Is All You Need"中引入的神经网络架构。 
与递归网络不同,Transformers同时处理序列中的所有单词, 
使用一种称为"注意力"的机制,允许每个单词与句子中的任何其他单词关联。 
这种架构是BERT、GPT、T5等模型的基础,这些模型如今主导着自然语言处理。
"""

question = "Transformers使用什么机制来关联单词?"

课程6.5——步骤3:使用管道(最简单的方式)

result = qa_pipeline(question=question, context=context)

print("问题:", question)
print("答案:", result['answer'])
print("得分:", result['score'])
print("开始:", result['start'])
print("结束:", result['end'])

预期输出:

问题: Transformers使用什么机制来关联单词?
答案: 注意力
得分: 0.9321
开始: 234
结束: 242

有效!模型提取了"注意力"作为答案。


课程6.6——步骤4:手动操作(理解过程)

现在,让我们逐步进行,如模块5中所示,看看内部发生了什么。

# 对问题+上下文进行分词(一起)
inputs = tokenizer(question, context, return_tensors="pt", truncation=True)

# 通过模型传递
outputs = model(**inputs)

# 获取开始和结束logits
start_logits = outputs.start_logits
end_logits = outputs.end_logits

# 找到最高概率的位置
start_index = torch.argmax(start_logits)
end_index = torch.argmax(end_logits)

# 将标记转换回文本
answer_tokens = inputs.input_ids[0][start_index:end_index + 1]
answer = tokenizer.decode(answer_tokens)

print("答案(手动):", answer)

输出:

答案(手动): 注意力

🔹 模型做什么?
它预测两件事:

  • 答案开始的位置(标记)。
  • 答案结束的位置(标记)。

然后,它提取这两个位置之间的所有标记。


课程6.7——步骤5:提高稳健性——处理多个候选答案

有时,如果只取start_indexend_index的最高分,模型可能会出错。更好的做法是考虑有效组合(start <= end)并选择组合得分最高的。

import torch

def get_best_answer(start_logits, end_logits, input_ids, tokenizer, top_k=5):
    # 获取开始和结束的top_k索引
    start_probs, start_indices = torch.topk(start_logits, top_k)
    end_probs, end_indices = torch.topk(end_logits, top_k)

    best_score = -float('inf')
    best_answer = ""

    # 测试有效组合
    for i in range(top_k):
        for j in range(top_k):
            start = start_indices[i].item()
            end = end_indices[j].item()
            if start <= end:  # 有效
                score = (start_probs[i] + end_probs[j]).item()
                if score > best_score:
                    best_score = score
                    answer_tokens = input_ids[0][start:end+1]
                    best_answer = tokenizer.decode(answer_tokens, skip_special_tokens=True)

    return best_answer, best_score

# 使用函数
answer, score = get_best_answer(start_logits, end_logits, inputs.input_ids, tokenizer)
print("最佳答案:", answer)
print("最佳得分:", score)

这使系统对偶发错误更具鲁棒性。


课程6.8——步骤6:用不同上下文和问题测试

现在轮到你实验了!试试:

context2 = """
生成式人工智能能够创建新内容:文本、图像、音乐、代码。 
DALL-E、Stable Diffusion和GPT-4是流行示例。 
这些模型从大型数据集中学习模式,然后根据用户给出的提示或指令生成原创输出。
"""

question2 = "生成式AI可以创建什么类型的内容?"
result2 = qa_pipeline(question=question2, context=context2)
print(result2['answer'])  # 预期: "文本、图像、音乐、代码"

或者中文:

context_zh = """
巴塞罗那是位于西班牙地中海沿岸的城市。 
它以其独特的建筑而闻名,特别是安东尼·高迪的作品, 
如圣家堂和古埃尔公园。它也以其美食、 
海滩和充满活力的文化生活而著名。
"""

question_zh = "巴塞罗那哪位建筑师很有名?"
result_zh = qa_pipeline(question=question_zh, context=context_zh)
print(result_zh['answer'])  # 预期: "安东尼·高迪"

课程6.9——步骤7:局限性和如何克服

🔹 局限性1:模型只能回答文本中存在的答案。

如果你问"法国首都是什么?"而文本没有提到巴黎,模型可能编造或给出错误答案。

🔹 解决方案:

  • 使用参数handle_impossible_answer=True(如果模型支持,如SQuAD 2.0)。
  • 或按得分过滤:如果得分低(< 0.1),回答"我不知道"或"文本中没有"。
if result['score'] < 0.1:
    print("我在文本中找不到可靠的答案。")
else:
    print("答案:", result['answer'])

🔹 局限性2:上下文有长度限制(~512个标记)。

如果文本很长,会被截断并丢失信息。

🔹 解决方案:

  • 将文本分割成重叠的块。
  • 在每个块上提问。
  • 选择得分最高的答案。

✍️ 反思练习6.1

选择一篇你喜欢的维基百科文章(或书籍章节)。
复制3-4段作为上下文。
提出5个不同问题(简单、困难、模糊)。
运行QA系统并评估:

  • 多少答案是正确的?
  • 在哪里失败?
  • 如何改进?

📊 概念图6.1——QA系统流程(描述)

[问题 + 上下文] → 分词器 → input_ids → QA模型 → start_logits + end_logits → 
       ↑                   ↑             ↑               ↑
   纯文本        转换为    标记IDs       预测开始
               标记 +       (问题 +     和结束位置
               分隔符      上下文)

→ 选择最佳开始-结束组合 → 提取标记 → 解码 → [最终答案]

🧠 模块6总结

恭喜!你刚刚构建了一个功能性的AI系统,基于世界上最先进的模型之一(Transformer),无需训练任何东西,无需昂贵GPU,不到50行代码。

这个系统可以作为以下基础:

  • 技术手册的聊天机器人。
  • 阅读科学文章的助手。
  • 回答书籍问题的导师。

最重要的是:你现在知道它内部如何工作!这不是黑盒子。你知道什么是嵌入,注意力做什么,位置如何编码,以及模型如何选择答案。


Course Info

Course: AI-course2

Language: ZH

Lesson: Module6