☰
基于深度学习的中文问答系统毕设:从数据预处理到Gradio演示的完整实战
2026/9/28 11:34:23 网站建设 项目流程

简介:这份毕业设计资源面向计算机相关专业学生与深度学习入门者,提供一套基于深度学习的中文问答系统完整源码,可用于课程设计、毕业设计或NLP方向的自学实践。压缩包共26个文件,约16.39MB,以9个Python脚本为核心,涵盖编码器、注意力解码器、数据预处理与训练入口等模块;另有7个txt语料与说明文件、6个xml工程配置、1个md说明文档及license等辅助文件,目录结构清晰,便于按模块阅读与二次开发。项目围绕自然语言处理、Transformer/BERT/RNN/LSTM等模型、分词与词向量化、损失函数与优化器选择、准确率与BLEU评估等关键环节展开,读者可据此理解问答系统从数据预处理、模型搭建到训练评估的完整链路,并参考配置与脚本组织方式完成自己的实验。目前已有243人学习,适合希望将深度学习理论落地为可运行项目的同学参考。

1. 中文问答系统毕设:从深度学习模型到可演示的最小闭环

很多同学拿到“基于深度学习的中文问答系统”这个题目时,第一反应是打开搜索引擎找现成代码,结果翻到的大多是英文 SQuAD 数据集上的 BERT 微调脚本,直接拿来跑中文语料就翻车——分词对不上、数据格式不匹配、显存爆掉、推理速度慢到演示时尴尬。这个题目的核心不是“训练一个多大的模型”,而是在有限算力和有限时间内,搭出一条从中文原始语料到可交互问答的完整链路。它适合计算机、软件工程、大数据方向的本科或硕士毕业设计,也适合想用 Python 深度学习教程里知识做实战项目的人。你需要的是能跑通、能演示、能写进论文的系统,而不是一个刷榜模型。这一章先把边界划清楚:输入是用户自然语言问题,输出是答案片段或答案文本,中间涉及数据预处理、模型选型、训练策略、服务封装四个环节,缺一个都交不了差。

2. 中文问答系统的数据从哪来:语料选型与预处理流水线

2.1 公开中文问答数据集对比与选择依据

做中文问答,第一个卡点不是模型,是数据。英文有 SQuAD、Natural Questions,中文能直接用的、规模适中、标注质量过得去的,常见的有以下几个方向:一是机器阅读理解类,比如 CMRC 2018、DRCD、WebQA;二是社区问答对类,比如 NLPCC 的 KBQA 数据集;三是自己爬取构建,但毕设周期内不建议从零标注。选数据集的判断标准有三条:是否包含答案在原文中的位置标注(抽取式需要)、问题类型是否覆盖事实型与推理型、数据量是否能在单卡 8G 显存内完成微调。

数据集任务类型规模量级答案形式适合的模型路线
CMRC 2018抽取式阅读理解约 2 万问题原文片段BERT/RoBERTa 微调
DRCD抽取式阅读理解约 3 万问题原文片段BERT 微调
WebQA开放域问答约 4 万问题短答案检索+阅读理解
NLPCC KBQA知识库问答约 1.5 万问题实体/关系语义解析或检索排序

如果导师没有指定数据集,我一般会选 CMRC 2018 做主线,因为它格式干净、社区脚本多、论文里引用率高。想加创新点,可以再叠加一个检索模块做开放域版本。

2.2 从原始 JSON 到模型输入:分词、截断与答案对齐

拿到数据集后,不能直接丢给模型。中文需要先经过分词或字级切分,再映射到预训练模型的词表。以 BERT 中文版为例,它用的是字级 WordPiece,所以“深度学习”会被切成“深”“度”“学”“习”四个 token。这一步的坑在于答案起止位置的对齐:原始数据里答案是以字符索引标注的,转成 token 索引时如果直接复用,会偏移一到两个位置,导致训练时标签错位,模型学出来全是废的。

下面是一个可复现的预处理脚本核心片段,用 Python 和 HuggingFace transformers 实现:

from transformers import BertTokenizerFast import json tokenizer = BertTokenizerFast.from_pretrained("bert-base-chinese") def preprocess_example(example, max_len=384, doc_stride=128): # example 包含 context, question, answers tokenized = tokenizer( example["question"], example["context"], truncation="only_second", max_length=max_len, stride=doc_stride, return_overflowing_tokens=True, return_offsets_mapping=True, padding="max_length" ) # 关键:用 offset_mapping 把字符级答案映射到 token 级 offset_mapping = tokenized.pop("offset_mapping") sample_map = tokenized.pop("overflow_to_sample_mapping") start_positions, end_positions = [], [] for i, offsets in enumerate(offset_mapping): input_ids = tokenized["input_ids"][i] cls_index = input_ids.index(tokenizer.cls_token_id) sequence_ids = tokenized.sequence_ids(i) # 找到 context 在 token 序列中的起止 span_start = 0 while sequence_ids[span_start] != 1: span_start += 1 span_end = len(input_ids) - 1 while sequence_ids[span_end] != 1: span_end -= 1 answer = example["answers"][0] start_char = answer["answer_start"] end_char = start_char + len(answer["text"]) # 如果答案不在当前窗口内,标为 CLS if not (offsets[span_start][0] <= start_char and offsets[span_end][1] >= end_char): start_positions.append(cls_index) end_positions.append(cls_index) else: while span_start < len(offsets) and offsets[span_start][0] <= start_char: span_start += 1 start_positions.append(span_start - 1) while offsets[span_end][1] >= end_char: span_end -= 1 end_positions.append(span_end + 1) tokenized["start_positions"] = start_positions tokenized["end_positions"] = end_positions return tokenized

这段代码的逻辑说明:return_offsets_mapping=True是核心,它返回每个 token 在原始字符串中的字符区间。overflow_to_sample_mapping处理长文本切窗后一个样本对应多个特征的情况。答案对齐时,先判断答案是否落在当前窗口内,不在就标为 CLS 位置,让模型学会“无法回答”。参数方面,max_len=384是单卡 8G 显存下 BERT-base 的安全值,doc_stride=128保证切窗之间有重叠,避免答案被切断。如果显存更小,降到 256 和 64,但召回会掉。

注意:中文预训练模型不要用bert-base-uncased,它会把中文字符全部映射成 UNK,训练 loss 根本不降。认准bert-base-chinese或hfl/chinese-roberta-wwm-ext。

3. 模型选型与训练:在毕设算力约束下做取舍

3.1 抽取式、生成式还是检索式:三条路线的成本对比

中文问答系统在毕设语境下,模型路线基本三条:抽取式(阅读理解)、生成式(Seq2Seq 或 LLM 微调)、检索式(向量匹配+排序)。抽取式的优点是答案一定来自原文,不会胡说,训练稳定,BERT-base 在 CMRC 上微调三四个 epoch 就能到 70% 左右的 EM,够写论文。生成式的优点是答案灵活,但需要更多数据和更大模型,用 T5 或 BART 中文版在毕设数据上训,很容易生成重复或无关文本,调参周期长。检索式适合 FAQ 场景,用 Sentence-BERT 做向量召回,再接一个交叉编码器精排,工程量大但可解释性强。

我的建议:主线做抽取式,加一个检索模块做开放域扩展。这样论文里有 baseline 对比,有模块创新,工作量也撑得起来。如果导师明确要求用 LLM,那就选一个 6B 或 7B 的中文模型做 LoRA 微调,但显存至少 16G,训练时间按小时算,要提前规划。

3.2 用 HuggingFace Trainer 微调 BERT 中文问答模型

选定抽取式路线后,训练脚本可以用 transformers 的 Trainer 封装,减少手写循环的出错概率。下面是一个可运行的训练配置:

from transformers import BertForQuestionAnswering, TrainingArguments, Trainer from datasets import load_dataset model = BertForQuestionAnswering.from_pretrained("bert-base-chinese") dataset = load_dataset("cmrc2018") # 假设已转为 HF datasets 格式 args = TrainingArguments( output_dir="./qa_model", evaluation_strategy="epoch", learning_rate=3e-5, per_device_train_batch_size=8, per_device_eval_batch_size=8, num_train_epochs=3, weight_decay=0.01, warmup_ratio=0.1, logging_steps=50, save_strategy="epoch", load_best_model_at_end=True, metric_for_best_model="eval_exact_match", fp16=True # 显卡支持时开启 ) trainer = Trainer( model=model, args=args, train_dataset=dataset["train"], eval_dataset=dataset["validation"], tokenizer=tokenizer, data_collator=default_data_collator ) trainer.train()

逻辑说明:learning_rate=3e-5是 BERT 微调的经典值,太大 loss 震荡,太小收敛慢。per_device_train_batch_size=8配合fp16=True在 8G 显存上刚好跑满。warmup_ratio=0.1让前 10% 步数线性升温,避免初期梯度爆炸。metric_for_best_model用 EM 而不是 loss,因为问答任务最终看的是答案匹配度。如果显存不够,把 batch size 降到 4,梯度累积设 2,等效 batch 不变。

训练过程中要盯两个信号:训练 loss 是否稳定下降,验证集 EM 是否在第二个 epoch 后还在涨。如果 loss 降但 EM 不涨,大概率是过拟合或标签对齐有问题,回去检查预处理脚本。

3.3 推理阶段的后处理:从 logits 到可读答案

模型输出的是 start logits 和 end logits,需要解码成文本片段。常见做法是取 start 和 end 概率最高的组合,限制end >= start且长度不超过 30 个 token。下面是一个推理函数:

import torch def answer_question(question, context, model, tokenizer, max_len=384): inputs = tokenizer(question, context, return_tensors="pt", max_length=max_len, truncation="only_second") with torch.no_grad(): outputs = model(**inputs) start_logits = outputs.start_logits end_logits = outputs.end_logits start_idx = torch.argmax(start_logits) end_idx = torch.argmax(end_logits) if end_idx < start_idx or end_idx - start_idx > 30: return "无法从给定文本中找到答案" tokens = inputs["input_ids"][0][start_idx:end_idx+1] return tokenizer.decode(tokens, skip_special_tokens=True)

参数说明:max_len=384要和训练时一致,否则位置编码对不上。end_idx - start_idx > 30是长度约束,防止模型把整段话当答案。返回“无法找到”是一种兜底策略,演示时比返回乱码体面得多。

4. 避坑与排查:中文问答系统毕设里最容易翻车的五件事

4.1 现象:训练 loss 正常下降,但验证集 EM 始终为 0

原因:答案位置对齐错误。中文 tokenizer 在切分时,标点、空格、数字的处理和英文不同,直接用字符索引当 token 索引会整体偏移。解决:用offset_mapping重新计算 start/end 位置,并在预处理后随机抽 5 条样本,把 token 序列 decode 回文本,肉眼确认答案片段是否被正确框出。

4.2 现象:模型推理时显存溢出,batch size 设为 1 依然 OOM

原因:max_length设得太大,或者没有用torch.no_grad()。BERT-base 在 512 长度下,单条推理约需 1.5G 显存,如果忘了关闭梯度,会额外占用计算图内存。解决:推理时加with torch.no_grad():,把max_length降到 384 或 256,必要时用 ONNX Runtime 做推理加速。

4.3 现象:演示时输入一个问题,系统返回的答案包含大量无关标点

原因:解码时没有跳过 special tokens,或者答案边界判断太宽松。解决:tokenizer.decode加skip_special_tokens=True,并在后处理里过滤掉纯标点或长度小于 2 的答案片段,返回“未找到答案”。

4.4 现象:换一个中文预训练模型后,训练直接报维度不匹配

原因:不同模型的词表大小不同,BertForQuestionAnswering的config.vocab_size和 tokenizer 不一致。解决:加载模型时用from_pretrained自动同步 config,不要手动改num_labels或vocab_size。如果换 RoBERTa,注意它没有 NSP 任务,pooler 层输出不同,但问答头不受影响。

4.5 现象:系统在测试集上 EM 不错,但实际演示时问题稍微换个说法就答错

原因:模型过拟合了训练集的问题句式,缺乏语义泛化。解决:在训练数据里做问题改写增强,比如同义词替换、语序调整;或者在推理前加一个检索模块,先用 Sentence-BERT 召回最相关的段落,再送给阅读理解模型,减少上下文噪声。

5. 把模型变成可演示系统:Gradio 封装与论文实验设计

5.1 用 Gradio 搭一个三小时能跑通的演示界面

毕设答辩时,老师不会看你训练脚本,只会看输入问题、点按钮、出答案。Gradio 是最省事的方案,几十行代码就能把模型包成 Web 界面。下面是一个最小可用示例:

import gradio as gr def qa_interface(question, context): if not question or not context: return "请输入问题和上下文" return answer_question(question, context, model, tokenizer) demo = gr.Interface( fn=qa_interface, inputs=[ gr.Textbox(label="问题", placeholder="例如:深度学习是什么?"), gr.Textbox(label="上下文", lines=6, placeholder="粘贴一段包含答案的文本") ], outputs=gr.Textbox(label="答案"), title="中文问答系统演示", description="基于 BERT 的抽取式问答,答案来自上下文" ) demo.launch(server_name="0.0.0.0", server_port=7860)

逻辑说明:qa_interface做输入校验,避免空输入导致模型报错。server_name="0.0.0.0"让同一局域网内的设备都能访问,答辩时用手机或另一台电脑演示更方便。如果要做开放域版本,把上下文输入框换成检索模块的返回结果,用户只输问题即可。

5.2 论文实验部分该跑哪些对比和消融

毕设论文里,实验章节不能只放一个最终 EM 值。至少要有三组对比:不同预训练模型(BERT-base vs RoBERTa-wwm vs ERNIE)、不同 max_length(256/384/512)、有无数据增强。消融实验可以去掉检索模块,看端到端 EM 掉多少,证明模块有效性。评价指标用 EM 和 F1,计算脚本可以直接用官方评估代码,不要自己手写,容易算错。

实验组模型max_len数据增强验证集 EM验证集 F1
baselineBERT-base-chinese384无68.279.5
换模型RoBERTa-wwm-ext384无71.482.1
调长度BERT-base-chinese512无69.080.2
加增强BERT-base-chinese384同义替换70.181.0

这张表的结构可以直接放进论文,数据根据你实际跑的结果填。注意:如果换模型后 EM 提升不到 1 个点,别硬吹,如实写“提升有限,但推理速度下降 20%”,反而显得客观。

5.3 一个我踩过的坑:别在答辩前一天换模型

我见过太多人,答辩前三天觉得 BERT 不够新,非要换 LLM 做生成式问答,结果 LoRA 微调 loss 不收敛,推理延迟高到每问一次等十秒,最后演示翻车。毕设的第一目标是稳定演示,第二目标才是创新。抽取式 BERT 方案虽然不新,但可控、可解释、可复现,论文里把数据预处理、模型对比、系统封装写清楚,工作量已经足够。真想加 LLM,可以放在“未来工作”里讨论,或者单独做一个检索模块的对比实验,不要动主线。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询