ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于深度学习的中文问答系统:从预训练模型微调到交付实践

基于深度学习的中文问答系统:从预训练模型微调到交付实践 简介面向毕业设计的中文问答系统完整源码包提供基于深度学习的NLP实践方案适合计算机相关专业学生完成毕设或入门自然语言处理。压缩包共26个文件以Python脚本为主9个py包含EncoderRNN、LuongAttnDecoderRNN、注意力机制、数据预处理与训练配置等模块另有7个txt数据说明、6个xml工程配置、1个README及license等整体体积16.39MB。资源覆盖语料加载、分词、模型构建、训练与评估等完整流程并涉及Transformer、BERT、RNN、LSTM等关键模型知识通过阅读源码与配置文件可掌握深度学习问答系统从数据预处理到部署的工程实现思路。已有243人学习下载适合需要快速搭建毕设框架或深入理解NLP模型原理的读者。1. 基于深度学习的中文问答系统从课题拆解到可交付的.zip看到这个标题带着.zip基本能猜到这个毕业设计的使用场景把代码、数据集、模型权重和说明文档打成一个压缩包交上去存档。基于深度学习的中文问答系统最常见的误区是以为要从零训练一个中文语言模型实际上绝大多数合格的毕业设计都走「预训练模型 微调 工程封装」这条路线评审真正在意的是你有没有把中文阅读理解任务跑通、参数设置有没有逻辑、结果能不能复现。这篇博文按“任务定义 → 模型选型 → 最小可运行代码 → 关键参数 → 交付排错”的顺序展开覆盖抽取式中文问答的完整链路。适合正在做毕业设计的本科生也适合想快速搭一个中文问答原型的一线工程师照着落一遍。2. 中文问答系统的任务形态与预训练选型理由2.1 先把任务定成抽取式还是生成式中文问答系统Chinese QA在自然语言处理里分为两大分支抽取式extractive QA也叫 MRC机器阅读理解和生成式generative QA。抽取式本质上是序列标注任务把问题和上下文一起喂给编码器模型预测答案在原文中的起始位置和结束位置。生成式则让模型自己逐字“写”出答案现在常见的开源对话大模型基本都属于生成式。毕业设计里我建议先固定到抽取式再谈优化。原因有两点第一中文公开基准 CMRC2018 采用的就是抽取式标注答案是原文中真实存在的连续片段评测用 Exact Match 和 F1指标界定清晰第二答辩时老师通常会追问“你的答案为什么和参考答案不一样”抽取式可以直接把起止位置映射回原文解释起来没有歧义。生成式虽然看起来更“智能”但答案可能写得很好却和参考答案对不上字指标方差大新手容易在两三组实验里被打乱节奏。2.1.1 抽取式 MRC 的数学形式输入上下文P和问题Q经过编码器得到 token 级别的表示矩阵H ∈ R^(L×d)。开始位置概率为p_s softmax(W_s H)结束位置概率为p_e softmax(W_e H)。训练时最小化两个位置的交叉熵之和L -log p_s(s*) - log p_e(e*)其中s*、e*是样本的真实起止位置。推理时不能独立取概率最大的起止位置因为可能出现e s的非法区间常见做法是枚举所有s ≤ e的组合选p_s(s) * p_e(e)最大的一对或者用动态规划做约束解码。2.1.2 生成式中文问答适合什么场景生成式更适合开放域问答也就是答案不在给定段落里、需要模型组织语言的情况。比如“用三句话解释什么是梯度消失”抽取式模型直接没法做因为原文根本不存在标准答案。如果毕业设计选生成式比较可靠的路径是拿开源的中文预训练模型做指令微调instruction tuning而不是从零训练一个 decoder。缺点是需要自己造或清洗问答指令数据显存消耗也比抽取式高一个量级。首版方案我一般定为抽取式但把数据接口设计成“输入文章 问题、输出答案文本 起止位置”的抽象结构后续想换成生成式只替换模型层不推翻数据管线。2.2 基座选中文 BERT 系模型的三个理由选预训练模型只需要满足三件事中文词表齐全、最大序列长度够用、PyTorch 生态里有现成加载器。BERT 在中文问答系统里仍然是性价比最高的基座。项目的第一版代码适合先跑通bert-base-chinese拿到一个真实可上报的基线再去试roberta-wwm-ext-large。bert-base-chinese的参数量约 110M最大输入长度 512 token词表直接按字切分不需要额外装分词器。相比 ALBERT 这类参数更少的变体它在中文抽取式问答上的收敛速度更快相比用英文 BERT 再挂中文分词它的字向量天然覆盖简体中文避免 OOV 问题。下表是几种常见基座的对比显存占用按单卡 12GB、batch size 8、序列长度 256 估算基座模型参数量最大长度中文抽取式表现显存占用估计bert-base-chinese约 110M512良好收敛稳定约 10GBroberta-wwm-ext约 117M512优于 BERT-base约 10.5GBalbert-base-chinese约 12M512接近训练更快约 6GBernie-1.0 中文约 110M512中文任务略好约 10GB我一般用roberta-wwm-ext做最终版用bert-base-chinese做调试期基座。前者全词掩码whole word masking在中文上的收益是公开实验验证过的后者加载速度快用来验证代码逻辑不会浪费时间。2.3 CMRC2018 数据集的字段与答案对齐CMRC2018 是中文抽取式问答最常用的数据集结构和 SQuAD 类似JSON 嵌套层级为data → paragraphs → qas。每个样本包含四个关键字段id、context、question、answers其中answers是数组首元素有text和answer_start两个属性。# 解析 CMRC2018 格式的最小实现 import json def read_cmrc(path): with open(path, r, encodingutf-8) as f: raw json.load(f) samples [] for doc in raw[data]: for para in doc[paragraphs]: context para[context] for qa in para[qas]: ans qa[answers][0] samples.append({ id: qa[id], context: context, question: qa[question], answer_text: ans[text], answer_start: ans[answer_start] }) return samplesanswer_start是答案在原始字符串里的字符偏移不是 token 偏移。用 BERT 分词后原始字符位置和 token 位置的对应关系必须通过offset_mapping转换否则训练目标完全错位。这里有个容易踩的坑BERT 的分词器会把有些连续数字切成一个小片段一个答案可能对应多个 token转换时不能只取answer_start命中的那一个 token要沿偏移表向后找直到覆盖answer_start len(answer_text)的位置。这个逻辑第 3 章会给出完整实现。3. 用 PyTorch 在本地跑通中文问答的最小可运行代码3.1 环境准备与依赖安装深度学习环境配置这部分网上教程版本很多我给出一个能直接在 CUDA 12.1 显卡上跑的固定组合conda create -n mrc python3.10 -y conda activate mrc pip install torch2.1.1 --index-url https://download.pytorch.org/whl/cu121 pip install transformers4.36.2 datasets2.16.1 pip install accelerate0.26.1这里的依赖说明transformers负责加载bert-base-chinese和BertForQuestionAnsweringdatasets用来做数据映射和缓存accelerate在单卡环境下负责 device 自动放置。如果你用的是 20GB 以上显存的显卡可以保持默认的混合精度设置如果是 8GB 卡把torch2.1.1换成 CPU 版本先调通逻辑再用小规模数据验证训练循环最后上 GPU 跑全量。3.2 数据加载与预处理数据预处理的核心是两件事把问题和上下文编码成 BERT 输入以及把原始字符偏移转成 token 起止位置。完整代码如下from transformers import BertTokenizerFast tokenizer BertTokenizerFast.from_pretrained(bert-base-chinese) def convert_example(example, max_len512): encoding tokenizer( example[question], example[context], max_lengthmax_len, paddingmax_length, truncationonly_second, return_offsets_mappingTrue, return_tensorspt ) # 答案在原始 context 中的字符区间 start_char example[answer_start] end_char start_char len(example[answer_text]) # offset_mapping 是 (input_id, token) 与原始字符位置的映射 offsets encoding.pop(offset_mapping)[0] token_start token_end None for idx, (s, e) in enumerate(offsets): if s 0 and e 0: continue # [CLS] [SEP] [PAD] 的偏移是 (0,0) if token_start is None and s start_char e: token_start idx if token_start is not None and s end_char e: token_end idx break if token_start is None or token_end is None: return None # 答案被截断丢弃该样本 encoding[start_positions] torch.tensor([token_start]) encoding[end_positions] torch.tensor([token_end]) return encoding参数说明truncationonly_second表示超长时只截断 context 而不截断 question保证问题完整return_offsets_mappingTrue是为了拿每个 token 对应原始字符串的[start, end)区间。处理极端情况时如果答案尾巴刚好落在截断边界外函数返回 None由外层filter把这个样本过滤掉。3.3 模型定义与训练循环模型部分直接用 Transformers 封装好的BertForQuestionAnswering它自带起止位置的分类头不需要自己再写 Linear 层import torch from torch.utils.data import DataLoader from transformers import BertForQuestionAnswering, AdamW model BertForQuestionAnswering.from_pretrained( bert-base-chinese, return_dictTrue ) model.train() optimizer AdamW(model.parameters(), lr3e-5) grad_clip 1.0 for epoch in range(3): for step, batch in enumerate(train_dataloader): batch {k: v.to(cuda) for k, v in batch.items()} outputs model( input_idsbatch[input_ids], token_type_idsbatch[token_type_ids], attention_maskbatch[attention_mask], start_positionsbatch[start_positions], end_positionsbatch[end_positions] ) loss outputs.loss loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip) optimizer.step() optimizer.zero_grad() if step % 100 0: print(fepoch {epoch} step {step} loss {loss.item():.4f})逻辑说明AdamW是 BERT 微调的标准优化器3e-5是中文抽取式任务常用的学习率clip_grad_norm_把梯度范数限制在 1.0防止长序列训练中偶尔出现的梯度爆炸。return_dictTrue让模型返回包含loss和start_logits、end_logits的对象方便后续推理。3.4 推理与评估推理时不再传start_positions和end_positions模型会返回原始 logitsdef predict(model, tokenizer, context, question, max_len512): model.eval() inputs tokenizer( question, context, max_lengthmax_len, truncationonly_second, return_tensorspt ).to(cuda) with torch.no_grad(): outputs model(**inputs) start_logits outputs.start_logits[0] end_logits outputs.end_logits[0] # 约束 start end取概率乘积最大的区间 best_score, best_span -float(inf), (0, 0) for s in range(len(start_logits)): for e in range(s, len(end_logits)): score start_logits[s] end_logits[e] if score best_score: best_score score best_span (s, e) tokens tokenizer.convert_ids_to_tokens(inputs[input_ids][0]) start, end best_span answer tokenizer.convert_tokens_to_string(tokens[start:end1]) return answer.replace( , )start_logits[s] end_logits[e]等价于 log 域的概率相加避免软最大计算。两层 for 循环在 512 长度下约 13 万次组合CPU 上耗时约几十毫秒瓶颈不在解码而在 BERT 前向。真正上线时用torch.no_grad()包裹即可。4. 训练与推理的关键参数与评估指标4.1 从基线开始的超参数配置bert-base-chinese在中文抽取式问答上表现不错但多数问题出在参数设置而不是模型结构。毕业设计第一版不要追求大 batch先按下面这组参数跑通参数名推荐值设置理由batch_size812GB 显存下可容纳梯度更新稳定learning_rate3e-5BERT 微调安全区间过大容易训飞num_epochs3CMRC2018 通常 3 轮收敛再多会过拟合max_length512覆盖大多数样本超过需滑窗处理warmup_ratio0.1前 10% 步数线性升温稳定前期更新weight_decay0.01对位置编码和 bias 不作用标准 AdamW 配置以上参数组合是中文问答系统的基线配置。如果 loss 在第 1 个 epoch 后没有降到 2.0 以下优先检查数据处理不要急着调学习率。实践里很多 result 异常都是answer_start转 token 位置时错位导致的。4.2 动态 padding 与数据效率固定max_length512会把短文本也拉到 512大部分 padding token 不参与 attention但显存照占。更高效的做法是让 DataLoader 按 batch 内最长样本动态 padding。两种方案都行from transformers import DataCollatorWithPadding data_collator DataCollatorWithPadding( tokenizertokenizer, paddinglongest ) train_dataloader DataLoader( ds, batch_size8, collate_fndata_collator, shuffleTrue )DataCollatorWithPadding会自动把同一个 batch 内的样本补到相同长度每个 batch 都不一样。这样训练速度能提升 20% 到 30%。需要注意的是动态 padding 后的attention_mask必须传给模型否则 padding token 会被当成有效内容计算 attention。4.3 评估指标 EM 和 F1中文问答系统的毕业设计一般用 CMRC2018 官方脚本核心是 EM完全匹配和 F1token 级重合度。EM 要求预测答案与标准答案逐字符相同F1 则是预测答案和标准答案之间的字符级精确率和召回率的调和平均。实现时先要对齐中文字符不能直接用英文空格分词def normalize_text(text): return .join(text.split()) def calc_f1(pred, gold): pred_chars set(normalize_text(pred)) gold_chars set(normalize_text(gold)) if len(gold_chars) 0: return 1.0 if len(pred_chars) 0 else 0.0 overlap len(pred_chars gold_chars) precision overlap / len(pred_chars) if pred_chars else 0.0 recall overlap / len(gold_chars) return 2 * precision * recall / (precision recall) if (precision recall) 0 else 0.0CMRC2018 官方结果中bert-base-chinese的 F1 接近 80 分如果你的模型 F1 在 60 分以下大概率是训练标签错位或评估时答案还原出了问题不是模型能力不够。答辩前可以用开发集跑一次完整评测把 EM/F1 做成表格比单独贴 loss 下降曲线更有说服力。5. 实操中的长文本、模型缓存与 zip 交付5.1 超过 512 token 的长上下文怎么处理BERT 的 512 长度限制来自位置嵌入不能简单改max_length。CMRC2018 多数样本在 200 到 400 字但真实应用里文章动辄几千字。常见做法是滑窗sliding window把 context 切成多个有重叠的窗口每个窗口单独和问题拼一起预测最终把所有窗口的start_logits和end_logits按原位置偏移量对齐取全局分数最高的 span。def predict_long(model, tokenizer, question, context, window450, stride100): all_logits [] offset_base 0 for start in range(0, len(context), stride): segment context[start:start window] inputs tokenizer( question, segment, max_length512, truncationonly_second, return_tensorspt ).to(cuda) with torch.no_grad(): outputs model(**inputs) # 每个窗口 logits 的 token 位置要加上偏移量 for span_start, logit in enumerate(outputs.start_logits[0]): all_logits.append((logit.item(), offset_base span_start)) offset_base len(tokenizer(segment, add_special_tokensFalse)[input_ids]) if offset_base len(context): break # 选 logits 最大的 token 作为答案起点再配合 end_logits 截断 best_pos max(all_logits, keylambda x: x[0])[1] return best_pos这里的stride是相邻窗口的重叠长度一般设为 80 到 120。窗口太小会导致答案横跨两个窗口被截断窗口太大则重叠区域重复计算推理变慢。若显存允许可以把window设为 480让有 token 长度的余量给 question。5.2 模型权重缓存与本地加载from_pretrained(bert-base-chinese)第一次执行会从 Hugging Face 下载约 400MB 权重后续默认缓存在~/.cache/huggingface。答辩现场如果没网这个目录很容易缺席。我一般会把权重目录公开在项目包的model/bert-base-chinese下代码改成model BertForQuestionAnswering.from_pretrained(./model/bert-base-chinese) tokenizer BertTokenizerFast.from_pretrained(./model/bert-base-chinese)这样整个.zip解压后不依赖外网from_pretrained会直接读本地目录。迁移到服务器时也只需要拷贝这一个目录避免缓存目录被系统清理。5.3 交付 zip 时容易漏掉的文件毕业设计最终交付的.zip包常见做法是包含四个部分源代码、训练日志、模型目录、数据子集。模型目录必须带上config.json否则from_pretrained无法识别结构训练日志至少要保留 loss 下降曲线对应的文本输出最好附带 TensorBoard 导出的截图。压缩包内部建议用相对路径因为评阅老师经常直接解压到桌面再运行绝对路径会立刻报FileNotFoundError。另外建议在.zip里放一个requirements.txt而不是让老师从头装环境。把本机已验证的版本写死例如transformers4.36.2不要写或等号缺失的模糊版本否则新版库改接口会导致代码跑不起来。6. 进阶从抽取式走向生成式中文问答6.1 用生成式模型替换底座如果学有余力可以在抽取式基线之上加一个生成式分支。深度学习的升级方向很明确把BertForQuestionAnswering换成支持生成的序列到序列模型比如mt5-small或开源中文对话模型。下面是最小的生成式微调入口from transformers import AutoTokenizer, AutoModelForSeq2SeqLM tokenizer AutoTokenizer.from_pretrained(google/mt5-small) model AutoModelForSeq2SeqLM.from_pretrained(google/mt5-small) # 输入格式问题 分隔符 上下文 inputs tokenizer( question /s context, max_length512, truncationTrue, return_tensorspt ) labels tokenizer( answer_text, max_length64, truncationTrue, return_tensorspt )[input_ids] outputs model(**inputs, labelslabels) loss outputs.lossmt5-small参数量只有约 3 亿单张 12GB 显卡可以微调。生成式模型的评估不能直接套用抽取式的 EM/F1因为答案表述可以不同常见做法是用 BLEU 或 ROUGE同时保留人工抽检 100 条的准确率记录。毕业设计时间有限时只需在报告里说明生成式分支的原理和效果差异不需要把训练全部跑完。6.2 最后的验证技巧模型训练完用开发集里一条样本从头到尾走一遍加载本地权重、输入问题、输出答案、人工核对 span 位置是否合理。这个可视化验证比任何 loss 曲线都更有说服力。若输出的答案落在显著不相关的位置检查方式是打印 token 级别的start_logits排名前十看模型是否把注意力放在了 [CLS] 上如果是把问题部分单独编码一遍确认token_type_ids没有在预处理中被错误覆盖。答辩前把这条验证链路跑通整个基于深度学习的中文问答系统项目就算闭环了。本文还有配套的精品资源点击获取
返回列表