
简介基于深度学习的公文校对系统.zip是一个面向深度学习、机器学习课程期末大作业或毕业设计的完整Python实现核心利用NLP技术对公文文本进行智能校对可辅助处理拼写错误、语法偏差及格式不规范等问题。资源包共6个文件压缩后仅8KB其中4个Python脚本构成主体分别承担公文样本下载、文本清洗预处理、深度学习模型构建与训练、主控流程调度等任务另有说明文档与Git忽略配置文件便于项目阅读和版本管理。目前已有65人浏览学习适合具备一定Python基础、希望了解深度学习文本处理项目结构的学生参考。通过研读源码可以快速掌握从数据采集、清洗、模型训练到结果展示的完整工程链路理解神经网络在具体文本任务中的调参和优化思路为课程设计或毕业答辩提供现成范本。1. 为什么我把“公文校对系统”交付成 zip而不是一个在线 API拿到「基于深度学习的公文校对系统.zip」这个包很多人的第一反应是这不就是一个文本纠错项目吗拆开看才知道它跟输入法纠错完全不一样面对的是公文这种文体——错误类型从同音错字、形近字到标点、发文字号、体例格式都有而且输出必须能解释“为什么这么改”不然内部评审那一关就过不去。我把同类系统做过三轮交付最终形态都收敛成一个 zip 包模型权重、规则库、配置文件和启动脚本放在固定目录里内网机器解压即用。这背后其实是深度学习项目从“能跑通”到“可验收”的完整链路。这篇笔记按我自己的落地经验把任务定义、数据构造、训练调参、坑位和打包交付讲一遍适合正在做深度学习毕设、或者想在真实文本场景里做校对服务的从业者。2. 把校对任务拆成模型能学的样子错误类型与方案选型2.1 公文校对不是普通文本纠错先分清四类错误做技术方案之前得先回答一个问题你要校的到底是什么公文的错误和日常文本不一样我一般把它拆成四类每一类的处理方式完全不同。错误类别典型例子规则能否覆盖深度模型职责字词类“部署”写成“布署”“再接再厉”写成“再接再励”“截止/截至”混用词典和混淆集能覆盖高频但长尾不行同音、形近字候选生成与排序句法搭配“贯彻《关于…的实施意见》的精神”这种成分嵌套混乱基本不能上下文语义判断是深度学习的核心阵地标点数字顿号与逗号混用发文字号写成〔2024〕12号却用了半角括号全是硬规则正则直接改不参与或只做辅助体例格式主送机关位置、抄送机关顺序、成文日期格式模板匹配即可无这个分类决定了整个系统不是“一个模型干到底”。常见做法是规则层在前、深度学习模型在后标点、数字、格式这类有明确规范的“硬错误”交给正则和词典字词、搭配这类“软错误”交给模型。顺序也很重要我习惯先跑规则再做模型推理最后统一拼装输出避免两边互相打架。为什么要用深度学习而不是全上规则答案在长尾。公文里同一套规范反复使用“的地得”“做作”这类问题规则能管但“厉行节约”写成“励行节约”、“一如既往”写成“一如即往”这些错只要换个句子就未必命中词典。深度学习模型能我记住的是上下文模式而不是死记硬背某个词表。2.2 两条技术路线检测式与生成式怎么选中文文本纠错目前主要两条路检测式和生成式。我两版都做过结论很直接做公文体首选检测式生成式只做兜底。检测式的思路是先把“哪里错了”找出来再对错误位置给候选字。落到模型上常见做法是用 BERT 加一个 token 级分类头每个字标记为“正确”或“错误”错误位置再做掩码预测从词表里挑几个候选字按概率排序。这个路线的最大优势是可控模型只负责提出“这个字可能要改、改成什么”最终改不改由规则和置信度决定不会出现整句被重写的情况。生成式则是用 T5、BART 这类 seq2seq 模型把错误句子整体重写一遍输出修正后的完整句子。优点是连续错误、多字、少字都能处理这恰恰是检测式的短板因为检测式要先定位位置才能改定位错了后面全错。但生成式也有麻烦它可能擅自调整句式把“现就有关事项通知如下”改成“现通知如下”这对公文是不能接受的。而且生成式没法解释自己为什么删了四个字。我的选型逻辑是先用 BERT 检测式做主体输出“错误位置 候选字 置信度”如果检测式在某个位置给出的所有候选都低于阈值才调用生成式模型做句子级重写并把重写结果用 diff 算法映射回原文位置。这样既保住可控性又能覆盖连续错误。2.3 离线可交付的工程约束参数量、推理时延与词汇表公文校对系统有个现实约束它通常在单位内网跑机器可能是旧服务器甚至没有 GPU。这意味着模型的参数量不能只盯着效果看。bert-base-chinese 有 110M 参数在 CPU 上跑一篇 3000 字的公文按 512 字窗口切分Token 化加推理单篇耗时可能到 5 到 8 秒。这对“人工校对辅助工具”来说勉强能接受但用户一旦开了 Word 插件实时检查就完全不能忍。我后面换成了中文蒸馏版的预训练模型参数量降到 28M 左右效果只掉三四个点推理速度快了差不多 4 倍。另一个工程点是词汇表。bert-base-chinese 的词表有两万多个 token但公文体用字很集中大量低频字在推理时根本选不中。我在最终模型里把候选 token 表裁剪到常用字加公文高频字约 1.4 万既减小 softmax 计算量也减少模型把“目的”改成“墓地”这类离谱输出——那些低频字根本不在候选列表里。还有环境问题。内网机器装不了外网依赖所以我的交付包里必须带着固定的依赖清单和离线安装包。LLM 是不是深度学习这个问题先放一边从工程角度说那种动辄几十 GB 的大模型在这个场景下完全不可行可解释性也差。公文体校对要求每个修改都能讲出理由一个大模型吐出一句“建议修改”没人敢用。所以别看现在大模型热这类任务真正的落地方案还是中小模型加规则配合。3. 从公开公文到训练样本语料构建与数据增强3.1 语料来源与脱敏底线深度学习模型要训得好语料是第一步。公文校对没有现成的大规模“错误-正确”平行语料我一般从三个来源凑第一是各地政府网站公开印发的政策文件、通知、批复这些文本规范程度高能当“正确句”用。第二是单位内部代拟稿这是最有价值的来源因为代拟稿里有大量真实错误改完后的版本就是天然的平行语料。第三是历年校对记录哪怕只有几百条训练时拿出来做 few-shot 也很有用。但这立刻引出安全和合规问题。公文语料涉及敏感内容脱敏是底线不能因为做一个校对系统就把内部文件带出内网。我的处理原则是涉密和不公开的原文一律不进入训练集代拟稿只提取“错误位置正确写法”的片段人名校名机构名全部替换成占位符。这个工作没有太多技术含量但必须有人盯模型训练完还要做一轮隐私审计抽查生成结果里有没有出现真实姓名和机构名。脱敏可以写一段最小处理脚本思路是识别姓名、手机号、身份证号后做替换import re def desensitize(text: str) - str: # 手机号1开头的11位数字替换为占位符 text re.sub(r(?!\d)1[3-9]\d{9}(?!\d), [手机号], text) # 身份证号18位末位可能是X text re.sub(r(?!\d)[1-9]\d{16}[\dXx](?!\d), [证件号], text) # 常见的“根据《…》”里的发文机关名用占位符替代 text re.sub(r《([^》]{2,20}?)(?:局|部|委|办公厅|办公室)(》), r《[机构]\2, text) return text这个脚本只做最小脱敏说明两点。第一正则只匹配“疑似手机号/证件号”的连续数字宁可漏也不误伤因为误伤会把正常的发文字号替换掉。第二机构名替换用的占位符是把机构名本身从训练文本里抹掉避免模型从上下文里记住某个具体单位。脱敏后的文本才能进入下一步样本构造。3.2 构造错误样本的完整脚本混淆集注入有了“正确句”怎么造出“错误句”靠人工写错不现实标准做法是混淆集加自动污染。混淆集是一组“易错词对”每个词条里放着正确写法、错误写法和错误类型。构造错误样本时在正确句上随机选中某些词按概率替换成混淆集里的错误写法从而得到“错误-正确”平行对。下面是核心脚本的精简版import random import jieba.posseg as pseg # 混淆集正确写法 - [错误候选, 错误类型] CONFUSION { 部署: [(布署, 同音), (部暑, 形近)], 再接再厉: [(再接再励, 同音)], 截至: [(截止, 语义混淆)], 账目: [(帐目, 形近)], 一如既往: [(一如即往, 同音)], } def build_pair(text, prob0.3, seed42): rng random.Random(seed) words pseg.cut(text) # 分词并保留词性避免替换错位置 output [] changed False for word, flag in words: if word in CONFUSION and rng.random() prob: wrong, err_type rng.choice(CONFUSION[word]) output.append(f[{err_type}]{wrong}) changed True else: output.append(word) return text, .join(output), changed逻辑说明jieba.posseg先做词性标注因为公文本里“部署”通常做动词直接按词表匹配就可以prob控制每篇文档的污染密度设 0.3 意味着约三成命中词被改错太高会让模型学到“到处都错”太低则正负样本不均衡。CONFUSION里的每个词对都人工核对过这是整套系统的地基——自动方法找出来的词对必须再过一遍人工审查否则会把正确用法当成错误。这里有个参数值得展开prob不是越大越好。我最初设 0.6发现模型确实把错误都改对了但到了真实数据上误报特别严重因为真实场景里一篇文章只有几十处错误模型从过密污染中学到了“有权就改”的倾向。降到 0.25 到 0.3同时保持每篇至少 3 个错误、最多 15 个错误训练出的模型才更贴近真实分布。3.3 标点数字与硬错误的规则增强深度学习模型再强标点、数字这类硬错误也不能指望它。BERT 在标点符号上的预测本身就不可靠一个逗号被改成句号从句法上是“合理”的但语义上完全不能接受。所以这一块我直接用规则增强不进入模型训练。硬错误主要指三类全角半角混用写程序的人特容易把英文逗号带进公文发文字号格式〔2024〕12号要求六角括号、年份全角日期和数字表述“2024 年”中间有空格“凌晨 0 点”应为“零时”。这些用正则就能覆盖到九成以上实现也简单def fix_hard_errors(text: str) - str: # 全角括号处理发文字号专用六角括号 text re.sub(r\[(\d{4})年?\], r〔\1〕, text) # 小心这只处理年份 # 数字和单位之间的多余空格 text re.sub(r(\d)\s(年|月|日|号|元), r\1\2, text) # 中文字符后的英文逗号 text re.sub(r([\u4e00-\u9fa5]),, r\1, text) return text这条代码的坑在第一个正则\[(\d{4})年?\]可能把正常语境里的方括号年份也改了所以规则要限定在“发文字号”段落里执行不能全文档跑。我通常会先把公文按“版头、正文、版记”切段只有正文才做全量规则。这也是整个系统的原则规则不是用来替代模型的而是用来守住模型守不住的边界的。4. 训练与调优BERT 微调与解码后处理4.1 环境与结构选型训练环境我一般用 miniconda 建独立环境。深度学习环境配置是最容易卡住新手的环节80% 的报错来自三个点Python 版本对不上、CUDA 版本对不上、依赖包互相冲突。我的固定组合是 Python 3.10 PyTorch 2.x transformers 4.xCPU 机器也能训只是慢一点。下面是一个可用的环境创建命令conda create -n gongwen python3.10 -y conda activate gongwen pip install torch transformers datasets seqeval参数说明torch默认装 CPU 或 CUDA 版本取决于你机器的情况如果后面训练时遇到“Torch not compiled with CUDA enabled”这类报错说明装的是 CPU 版不影响训练只是慢。seqeval是序列标注的评估库算 F1 用。这个命令跑通后再装 jieba、pypinyin 这类处理库。模型结构上我用一个三层方案最底层是预训练语言模型中间是 token 级分类头判断“错/不错”最上层是候选生成头对错误位置做 mask 预测。对比过直接拿预训练模型的 MLM 输出做候选效果差一截因为 MLM 学的是“这个位置最可能的字”不是“这个位置是不是错了”。必须单独训练分类头。4.2 训练主流程损失、验证指标与超参训练的核心代码如下我直接给出可以跑通的主循环片段import torch from transformers import AutoModelForTokenClassification, AutoTokenizer from torch.utils.data import DataLoader, Dataset class CorrectorDataset(Dataset): def __init__(self, pairs, tokenizer, max_len256): self.pairs pairs # [(错误句, 正确句), ...] self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.pairs) def __getitem__(self, idx): wrong, correct self.pairs[idx] enc self.tokenizer(wrong, correct, truncationTrue, max_lengthself.max_len, return_tensorspt) # 标签按字对齐1 表示该位置错误0 表示正确 labels (enc[input_ids][0] ! enc[labels][0]).long() return { input_ids: enc[input_ids][0], attention_mask: enc[attention_mask][0], labels: labels, } model AutoModelForTokenClassification.from_pretrained( hfl/chinese-roberta-wwm-ext, # 中文全词掩码比原版 BERT 更适合错字 num_labels2 )这个方案是用 encoder-decoder 式的“错误句正确句”作为输入输出做序列标注。有个细节常被忽略直接用tokenizer(wrong, correct)拼接时两个句子之间的[SEP]位置会被标错所以标签对齐时要跳过特殊 token只用labels部分去计算。betatron式对齐在这种场景下会更麻烦我最终用的是字符级偏移量手动对齐每篇样本都校验一次长度宁可丢样本也不留错位数据。训练超参我从第二批开始就没再动过直接给出来参数取值说明batch_size32显存不够就降到 16learning_rate3e-5超过 5e-5 会出现损失震荡epochs5第 5 轮效果最好再多开始过拟合max_len256公文长句多512 反而稀释注意力损失函数CrossEntropy 类别权重错误标签约占 8%正负比约 1:12优化器AdamW开 weight_decay0.01训练中我每 500 步打一次验证集指标主要看两个数错误检测 F1 和错误纠正准确率。F1 衡量“能不能发现错”纠正准确率衡量“发现的错改得对不对”。F1 到 90 以上相对容易纠正准确率到 85 就是坎因为模型容易把错字改成一个同音但语义不对的字。4.3 推理与 diff 后处理把模型输出变成校对建议模型输出不是最终结果。用户要的是“这句话哪里错了、改成什么、为什么”所以推理阶段必须做位置还原。我常用的做法是拿到 token 级标签后把连续的错误 token 合并成一个“错误片段”再到原始文本里用字符偏移量定位最后从候选生成头取 Top 3 候选字。逻辑如下def merge_error_spans(logits, tokenizer, original_text, offset_mapping): preds torch.argmax(logits, dim-1)[0].tolist() results [] cur None # 暂存当前错误片段 for i, (label, mapping) in enumerate(zip(preds, offset_mapping)): start, end mapping[0], mapping[1] if start end: # 特殊 token跳过 continue if label 1: if cur is None: cur [start, end, original_text[start:end]] else: cur[1] end # 扩展片段 else: if cur is not None: results.append(cur) cur None if cur is not None: results.append(cur) # 对每个错误片段生成候选字 for span in results: span_start, span_end, wrong_word span span[candidates] get_candidates(wrong_word, logits, span_start) # Top3 return resultsoffset_mapping是 tokenizer 提供的字符级映射它保证模型预测的 token 位置能对应回原始文本的字合并片段时用end递增而不是新建 span是为了把连续两个错字并成一个修改单位。这里最重要的参数是logits的修剪候选字必须满足两个条件一是字在公文高频词表里二是跟原字的拼音相似度或字形相似度足够高。满足这两个条件的候选才进到 Top 3不然会出现“把通的改成痛”这类同音不同义笑话。4.4 置信度校准与阈值减少“把对的改成错的”公文体校对最不能接受的是误报。一个逐字核对过的文件系统却标出 20 处“可能的错误”谁也不会信任它。所以我用了一个很土但有效的办法给每个修改建议算一个置信度置信度 候选字概率 − 原字概率。只有差值超过阈值才输出建议。这个阈值必须按错误类型分开调。标点规则类错误是“必改”阈值设为 0同音替换类错误差 0.15 就改形近字要求差 0.3 以上语义依赖类错误要 0.5 以上。这么设置之后的直接效果是误报从每篇十几个降到每篇两三个。代价是召回率下降但公文体校对工具的核心诉求本来就是“少吵一吵一个准”。5. 常见问题与避坑训练到打包的 5 个翻车点5.1 模型把对的改成错的评审直接否定整个系统现象系统在 3000 字的文上标出 27 处修改用户逐条复核后认定其中 22 处是“瞎改”最典型的是把“目的”改成“墓地”——同音但语义完全不对。第一次演示就这么翻车。原因模型训练时以“错误检测”为目标天然倾向多报候选生成时只看了概率没有做置信度差值和语义过滤。解决加两层过滤。第一层概率差值阈值必须生效不满足不发候选第二层把候选字放回句子做语言模型评分同时用混淆集里的“正确→错误”方向做反向校验。方向校验很关键混淆集里“部署→布署”是错误方向但如果模型想把“布署”改成“部署”以外的任何词直接丢弃。5.2 训练样本污染密度太高模型学出“逢词必改”现象训练集 F1 到 95一到真实公文上召回率掉一半而且经常把规范用语拆开改。原因自动注入错误时污染密度设得太高模型学到的是“句子里每几个字就该有一个错”而不是“仅在语义异常时修改”。解决把污染密度降到 0.2 到 0.3并强制每篇样本错误数不超过 15 个。还不行就做难样本挖掘先把未注入错误的干净文本交给模型保留那些被模型误报的位置作为难例加入训练集让模型见见“对的句子长什么样”。5.3 zip 包换台机器跑不起来缺库缺到怀疑人生现象压缩包在 A 机器跑通拷到 B 机器后import transformers直接报ModuleNotFoundError更离谱的是连 torch 都没有。原因交付时只打了.py脚本和模型权重没有带上依赖环境。拿一个 zip 解压出来的源码只要环境不对就没法跑。解决用 conda-pack 把整个虚拟环境打成包放进 zip 的env/目录里或者退一步至少把requirements.txt里的版本全部锁死并且显式注明 Python 版本和 torch 版本。我现在的交付 zip 结构固定成下面这样后面不会再为这种事后补依赖的坑加班。5.4 规则和模型互相打架输出一片混乱现象规则层把“截止”改成“截至”模型又基于统计把“截至”改成“截止”一篇文稿上来来回回改三遍最终输出自相矛盾。原因两条处理链路各自独立没有统一的裁决顺序。解决定死处理管线。规则层只跑标点、数字、格式等硬错误模型层只跑字词和搭配输出前做一个合并器按“硬规则 模型高置信 模型低置信”排序低置信建议默认隐藏在“更多建议”里不直接进正文。这样规则和模型永远不会对同一个问题重复修改。5.5 zip 包命名与编码Windows 解压乱码和伪加密现象交付给同事的 zip 在 Windows 上右键解压文件名全是乱码另一个包设了密码结果密码忘了整个包形同虚设。原因Windows 自带压缩工具的 zip 默认用 GBK 编码文件名在 Linux 上解压就乱码而 zip 加密本身是弱加密忘了密码基本等于数据丢失网上那些移除密码的工具大多是骗局。解决统一用 7-Zip 打包打开“UTF-8 文件名”选项这样 Windows 和 Linux 解压都不乱。不要给 zip 加密码真正的权限控制走文件服务器或内网权限密码除了增加打不开的风险没有任何意义。打包前在包内放一个SHA256SUMS文件交付后让对方先验一遍避免传输过程损坏。6. 交付形态与进阶让 zip 包开箱可用一个可复现的交付包里面至少要有这些目录内容说明model/训练好的模型权重与配置文件不传原始 checkpoint转成 ONNX 更小更稳rules/混淆集、标点数字规则、体例模板JSON 格式业务方可自行维护scripts/启动脚本、一键验证脚本Windows 下给.batLinux 下给.shsample/三篇样例公文和对应校对报告验收时直接用README.md环境依赖、启动命令、目录说明写清楚 Python 版本、离线安装包位置最后用一个命令验证交付是否成功cd sample python ../scripts/correct.py test_doc.txt --output report.json这条命令的意义不是跑通一个 demo而是确认三件事模型能加载、规则能触发、输出能落地成报告。我现在的习惯是交付前不看模型指标只看这“一命令三检查”能不能在干净机器上完成能完成说明这个包是活的。每次打包我都会模拟一次“拿到全新机器、无网络、只看 README”的流程自己走一遍。希望帮到你。本文还有配套的精品资源点击获取