☰
BERT文本纠错项目拆解:规则检测与掩码预测实战
2026/10/14 14:59:00 网站建设 项目流程

简介:基于BERT的文本纠错项目,面向自然语言处理方向的毕业设计、课程设计与期末大作业场景,提供可运行的Python源码、预训练模型使用说明及配套数据,帮助学习者快速上手BERT掩码语言模型在中文拼写纠错中的应用。压缩包内共39个文件,涵盖19个py脚本(含纠错主程序、规则纠错器、文本处理工具等)、12个txt词典与停用词数据、4个xml工程配置,以及模型文件、Markdown说明和KenLM语言模型文件等,整体仅22.37MB,轻量易部署。已有303人浏览学习。资源附带了完整的目录结构与运行命令,并给出针对垂直领域重新训练的微调方式,便于扩展。无论是入门BERT实践,还是直接作为毕设、大作业的初期演示,均具备较高参考价值。

1. BERT 文本纠错项目:从下载到跑通,先看清这套架构

中文文本纠错是个容易被低估的问题。你搜“BERT 文本纠错”,会看到大量论文和开源仓库,但能直接下载、跑通、还能当课程设计交差的并不多。这个资源属于后者:一套典型的“规则检测 + BERT 掩码预测”混合架构。它先用同音字、形近字和词频字典把可疑位置标出来,再把可疑字替换成 [mask] 交给 BERT 预测候选字,最后按置信度从候选里挑出纠错结果。压缩包里把检测器、纠错器、九个数据字典、预训练模型目录、微调脚本、新闻语料全部放在一起,适合期末大作业、课程设计,也适合拿来做毕设的基座做二次开发。下面所有分析基于我实际拆包跑通的过程,新手可以照着做,熟手可以直接跳到第 3 章看 mask 纠错的参数细节。

2. 检测层拆解:同音字、形近字与词频字典如何找出可疑字

2.1 detector.py 的检测流程:先看它怎么工作

拿到压缩包后,我先看的是detector.py。这个文件负责“找出哪里错了”,是整套系统的第一层筛子。常见做法是跑一个滑窗:对输入句子逐个取词,把每个词去查同音字表、形近字表,生成一组候选;接着用词频词典对“原文 vs 候选”所在的上下文打分,判断原文是不是可疑。单纯靠查表会误伤大量人名地名,所以项目里把place_name.txt、person_name.txt作为白名单,命中白名单的词直接跳过,这一步非常关键。

按项目文件还原的逻辑,我可以把检测流程简化成下面这段伪代码:

# 检测器核心流程示意(接口和原项目一致,内部实现做了简化) class Detector: def __init__(self, data_dir): self.same_pinyin = load_dict(data_dir / "same_pinyin.txt") # 同音字映射 self.same_stroke = load_dict(data_dir / "same_stroke.txt") # 形近字映射 self.place_name = load_set(data_dir / "place_name.txt") # 地名白名单 self.person_name = load_set(data_dir / "person_name.txt") # 人名白名单 self.word_freq = load_freq(data_dir / "word_freq.txt") # 通用词频 def detect(self, sentence): errors = [] for pos, char in enumerate(sentence): if char in self.place_name or char in self.person_name: continue # 白名单优先,避免人名地名误报 candidates = set(self.same_pinyin.get(char, [])) candidates |= set(self.same_stroke.get(char, [])) if not candidates: continue # 用词频做一次上下文粗筛,频次显著更高的候选才留下 ranked = [c for c in candidates if self.word_freq.get(c, 0) > self.word_freq.get(char, 0)] if ranked: errors.append((pos, char, ranked)) return errors

这段代码的逻辑是:先将place_name.txt和person_name.txt读成集合作为白名单;再对句子逐字取候选;最后用word_freq.txt的词频做粗筛,候选字频次高于原字的才认为“可疑”。注意这里有个参数细节:word_freq.txt里如果缺失某个字,get(c, 0)会返回 0,恰好避免 KeyError。如果你想提高召回率,可以把>改成>=,但代价是误报变多。实际调参时,我一般保持默认,靠第二层 BERT 去精排。

2.2 data 目录下的字典文件:九个 txt 各自管什么

data目录是这套资源最值钱的地方。每个 txt 的责任很清晰,拆包后我按用途整理如下:

文件名类型内容与格式约定
place_name.txt地名白名单每行一个地名,命中则跳过检测
person_name.txt人名白名单每行一个姓名,避免把人名当错字
word_freq.txt通用词频表常见格式为“字/词 空格 频次”
custom_word_freq.txt自定义词频格式同上,覆盖默认词频,优先级更高
same_stroke.txt形近字表每行一组字,形近关系互查
same_pinyin.txt同音字表每行一组同音字,全拼相同互查
custom_confusion.txt用户混淆集错词 空格 正确词,专门给常见易错词纠错
common_char_set.txt常用字集合决定哪些字参与候选生成
stopwords.txt停用词表语气词、标点等不参与检测

词频类文件注意编码,项目默认 UTF-8。实际使用中我最常改的是custom_confusion.txt,它解决的是同音和形近都覆盖不到的语义错误。比如“账户”与“帐户”这样拼音相同、但字面不完全落在同音表里的场景,你在这个文件里手工加一行“帐户 账户”就能稳定纠出来。common_char_set.txt的作用是缩小候选范围,不在常用字集里的生僻字不参与生成,能明显加快推理速度。

这几个文件相互独立但也有依赖关系:白名单要在候选生成之前查,词频表在候选生成之后做粗排,混淆集则可以跳过规则直接进入 BERT 精排。改动任何一个 txt 后不用重新训练,直接重启detector.py即可生效,这也是这个项目适合拿来二次开发的原因。

2.3 规则检测的边界:为什么不能只用同音字

纯规则的检测层有天生的盲区。第一,同音字表覆盖有限,多音字、方言发音、拼音输入漏字它都管不到;第二,形近字表容易把“己/已/巳”这类高频混淆字全列成候选,没有上下文区分能力;第三,词频粗筛对专业名词不友好,“卷秩”这种低频但正确的词会被当成错字。所以正确的打开方式是:检测层负责召回,即宁多勿漏;纠错层用 BERT 的上下文表征负责精排,即从候选里挑最合适的字。这也是这个资源把 BERT 放在第二层的原因。

实际跑的时候,我还发现一个细节:白名单的作用比想象中大。地名、人名如果不设白名单,“张三”里的“三”会被同音字表换成“伞山叁”等一批候选,误报直接爆炸。项目把place_name.txt和person_name.txt单独拆出来,就是在告诉你:不要在算法里写死白名单,要把它们做成可维护的数据文件。课程设计答辩时,这个设计点可以单独讲两分钟。另外,stopwords.txt只在检测阶段影响高频虚词,不参与 BERT 精排。

3. 纠错层拆解:把可疑位置换成 [mask],让 BERT 给出候选

3.1 bert_corrector.py 主流程:检测、mask、预测、过滤

第二层的核心是bert_corrector.py。它从data/bert_models/chinese_finetuned_lm加载 BERT 模型,拿到第 2 章检测出的可疑位置后,把每个可疑字替换成[MASK],送入模型取该位置的输出分布,然后按概率挑 top-k 候选。这里很多人把 BERT 当成黑匣子直接用,其实有一个参数值得注意:因为一个句子可能有多个错误,项目一般会逐位替换、逐位预测,而不是一次性把所有可疑位全 mask 掉。一次性全 mask 会让模型丧失上下文,预测质量明显下降。

简化的核心代码如下:

import torch from transformers import BertTokenizer, BertForMaskedLM model_dir = "data/bert_models/chinese_finetuned_lm" tokenizer = BertTokenizer.from_pretrained(model_dir) model = BertForMaskedLM.from_pretrained(model_dir) def correct_with_bert(sentence, error_positions, top_k=5): result = list(sentence) for pos, char, rule_candidates in error_positions: # 逐位替换,保留其余上下文完整 masked = sentence[:pos] + "[MASK]" + sentence[pos+1:] inputs = tokenizer(masked, return_tensors="pt") with torch.no_grad(): outputs = model(**inputs).logits # [1, seq_len, vocab] mask_index = inputs["input_ids"].tolist()[0].index( tokenizer.convert_tokens_to_ids("[MASK]")) logits = outputs[0, mask_index] # 只取 mask 位置 probs = torch.softmax(logits, dim=-1) # 取前 top_k 个候选 token,并转成汉字 top_tokens = torch.topk(probs, top_k).indices.tolist() candidates = [tokenizer.decode([t]) for t in top_tokens] # 与规则候选取交集优先,没有交集就取模型第一位 final = intersect(candidates, rule_candidates) or candidates[0] result[pos] = final return "".join(result)

逻辑说明:代码只对 mask 位置的 logits 做 softmax,避免全词表 21128 维输出干扰判断;topk(probs, top_k)的top_k=5是项目里比较常用的值,小于 3 会漏掉正确答案,大于 10 引入太多噪声。intersect这一步值得单独说:规则候选和 BERT 候选如果有交集,说明两个来源都认可,正确率最高;如果没交集,信任 BERT 的第一名,因为它的上下文表征比单纯词频粗筛可信。

另外,data/bert_models/chinese_finetuned_lm目录下必须有config.json、pytorch_model.bin、vocab.txt三者齐备才能加载成功。vocab.txt是 BERT 词表的顺序,tokenizer 依赖它做 id 到字的映射;缺了它,即使有权重文件也跑不起来。这个目录放的是已经微调过的中文模型,而不是裸的bert-base-chinese,这也是一个容易误解的点。

提示:从input_ids里用.index()查找[MASK]的位置,在中文场景基本安全,因为每个汉字是一个 token;如果换成英文组合词输入,可能被拆成多个 token,需要改用inputs["input_ids"]与tokenizer.mask_token_id逐位比对。

3.2 predict_mask.py:直接观察 mask 位置的候选字

predict_mask.py是一个调试利器。它的用法是:你手动在句子里把某个字写成[mask],运行脚本直接输出该位置模型认为最可能的若干个汉字。我拆包后第一件事就是拿它跑了一句“我今天[马]上要去开会”,观察模型能不能填回“马”。这类手测的意义在于,它把 BERT 从黑匣子变成可交互的工具,你能快速判断这份预训练模型对当前领域文本的覆盖程度。

一个很典型的判断方法是:拿十句你领域内的句子,手动挖掉一个关键字,看模型 top-1 命中率。如果命中率明显低于通用文本的预期水平,那就说明你需要第 4 章的微调,而不是盲目在规则层加字典。这一步成本极低,几分钟就能得到结论,很多人在毕设里忽略了它,直接一股脑训练,最后效果差又不知道原因。

python predict_mask.py

运行后按提示输入带[mask]的句子,脚本会打印当前 mask 位置的前五个候选字。你可以直接改脚本里的top_k来观察候选变化,这是评估模型领域适应度最直观的手段。

3.3 text_utils.py 与 langconv.py:文本预处理和简繁转换

预处理层有两个文件容易被忽略,一是text_utils.py,二是langconv.py。langconv.py负责简繁转换,配合zh_wiki.py的词条映射使用。为什么纠错要管简繁?因为开源预训练语料里繁体比例不低,如果输入是简体、模型训练时见过大量繁体,同一个字在 tokenizer 里会被切成不同 token,mask 预测结果会不稳定。常见做法是统一转成简体再送模型,输出时再转回用户需要的字型。

text_utils.py里还封装了去空白字符、统一引号、过滤停用词等操作。这里有一个容易踩的细节:停用词过滤只应该作用于检测阶段,不能作用到 BERT 输入。如果提前把“的、了”从句子中删掉,BERT 拿到的就不是完整上下文,模型在 mask 位置的预测会整体漂移。我在第一次跑的时候就犯过这个错,纠错后句子变得不通顺,后来查代码才发现是预处理顺序反了,这个放到第 5 章避坑里细说。

4. 重新训练语言模型:微调脚本参数、数据准备与验证

4.1 run_lm_finetuning.py 的两套命令行参数对比

压缩包里给了两份训练命令,分别对应两代 transformers 接口。第一份是现代 Trainer 风格:

export CUDA_VISIBLE_DEVICES=0 TRAIN_FILE=data/train.txt TEST_FILE=data/test.txt python run_lm_finetuning.py \ --output_dir=chinese_finetuned_lm \ --model_type=bert \ --model_name_or_path=bert-base-chinese \ --do_train \ --train_data_file=$TRAIN_FILE \ --do_eval \ --eval_data_file=$TEST_FILE \ --mlm \ --num_train_epochs=3

第二份是旧式的脚本式写法:

python -m run_lm_finetuning \ --bert_model bert-base-uncased \ --do_lower_case \ --do_train \ --train_file ./samples/sample_text.txt \ --output_dir ./samples/samples_out \ --num_train_epochs 5.0 \ --learning_rate 3e-5 \ --train_batch_size 16 \ --max_seq_length 128

两套命令的目标相同,都是对 BERT 做掩码语言模型继续训练,区别在参数面向的接口版本。第一套里--model_type=bert指定模型结构,--model_name_or_path=bert-base-chinese指定初始权重,--mlm是掩码语言模型任务的开关;第二套里--bert_model bert-base-uncased是英文 uncased 模型,如果你要处理中文,需要把它改成bert-base-chinese,或者干脆改成你本地微调好的模型目录。注意第二套默认是英文小写处理,--do_lower_case对中文没有实际意义,保持原样即可。

关键参数对比整理如下:

参数含义第一套写法第二套写法取值建议
初始权重model_name_or_pathbert_model中文任务用 bert-base-chinese 或本地微调目录
训练数据train_data_filetrain_file纯文本,按行切分
学习率默认3e-53e-5 常见,再低收敛慢
训练轮数num_train_epochs=3num_train_epochs 5.03-5 轮,轮数多过拟合
批大小默认1612G 显存建议 16,不足降到 8

有个参数特别值得说明:--train_batch_size 16是典型单卡(12G 显存)的配置。如果显存不足,把--train_batch_size降到 8,同时把--gradient_accumulation_steps换成 2,效果等价但不爆显存。第一次看到一个参数,我建议优先改 batch size,不要动--learning_rate 3e-5,这个数值对 BERT 微调是相对安全的范围。

4.2 准备训练数据:把新闻语料转成纯文本

数据准备是微调里面最容易被糊弄的一步。项目自带sample里有一个新闻语料压缩包,解压后是一大段纯文本新闻,拿来训练完全没有问题,但要注意三件事。第一,BERT 的 MLM 训练只接收纯文本,不要带标签、不要带 json 结构,按段落换行即可。第二,语料里如果混有繁体,要在训练前统一简繁,否则模型会把繁简两个 token 当成两个语义单元来学。第三,按 9:1 切分训练集和测试集,测试集不参与训练,只用来跑--do_eval。

数据准备的常见流程是这样:

# 解压并转码 unzip "sample/新闻语料.zip" -d sample/news_texts # 简单切分为训练和测试两份纯文本 head -n 9000 sample/news_texts/clean.txt > data/train.txt tail -n 1000 sample/news_texts/clean.txt > data/test.txt

这里我一般会在切分前先做一次清洗:把每行超过 128 字的句子截断,按句号或换行拆成短行,因为--max_seq_length 128决定了 BERT 最长只能看 128 个 token,超长的部分会被截掉。另一个建议是:train.txt里的行不要给句子加空格,中文不需要按空格分词,不加反而更符合 BERT 中文词表的分词逻辑。

注意:train.txt每行建议控制在 64 个中文字以内,留出 [CLS]、[SEP] 和随机 mask 的 token 空位。超长行进入训练会被截断,等于浪费一部分语料。

4.3 微调之后怎么判断效果有没有变好

很多人微调完只看 loss 降了就以为成功,实际上 loss 降低不代表纠错正确率提高。原因是 MLM 任务学的是词语概率分布,而纠错任务关心的是在候选集中选对字。跑完--do_eval后,拿到评估 loss 只能说明模型对语料的拟合程度;要判断有没有变好,我自己的习惯是固定 20 条垂直领域句子,每句挖掉一个关键词,分别用微调前、微调后的模型跑predict_mask.py,对比 top-1 是否更接近正确答案。

如果微调后 top-1 命中率没有提升,先检查训练数据的量和纯文本质量。说实话,这块存在一定玄学:同一个参数跑两次,结果可能因为随机种子而有细微差别。我一般会在命令里固定--seed 42,并把训练轮数控制在 3 到 5 轮之间,轮数再多容易在垂直领域上过拟合,反而降低了通用文本上的表现。毕设里想展示效果,建议把微调前后的纠错结果做成前后对比表,同时给 loss 曲线截图,答辩时比一堆抽象指标更有说服力。

5. 避坑:跑通与部署中的 5 个高频问题

5.1 现象一:from_pretrained 加载失败,报找不到文件

拆包后第一次跑,最常见的报错是Can't load config.json或pytorch_model.bin路径不存在。原因通常是压缩包解压后data/bert_models/chinese_finetuned_lm的目录层级变了两层,比如多了一层外层文件夹,from_pretrained("data/bert_models/chinese_finetuned_lm")指向的目录里缺少config.json、pytorch_model.bin、vocab.txt三个文件,加载必然报错。解决方法是打开目录确认这三个文件存在且在同一层,缺哪个补哪个;如果只缺二进制权重,可以重新解压原压缩包单独拷出来。这里提醒一句:config.json不仅记录配置,还定义了模型结构,权重和配置不匹配时加载会静默出错,所以尽量不要自己改动它。

5.2 现象二:transformers 版本不一致,tokenizer 接口报错

项目是基于较老版本的 transformers 写的,如果你用新版本跑,常见报错是tokenizer.encode参数变了,batch_decode行为不同,甚至BertForMaskedLM.from_pretrained警告权重不一致。原因很好理解:新版把很多旧接口改成了统一风格,旧代码里convert_tokens_to_ids系列还能用,但encode返回对象从 list 变成BatchEncoding。我一般会先在脚本里打印type(tokenizer("测试", return_tensors="pt")),确认是新接口后把按 key 取input_ids的写法统一,别依赖位置索引。如果实在不想改代码,就按requirements.txt里的版本装一个干净环境,这是最省事的后悔药。

5.3 现象三:繁简体混用导致检测结果全是误报

输入句子是简体,但字典里的词或预训练模型输出的是繁体,纠错后你会发现每个位置都“被纠错”,结果变成一片繁体字。原因在langconv.py的转换链路没走到,常见做法是在检测前统一调to_simplified,在输出阶段再按需要转回目标字型。我一开始图省事直接在bert_corrector.py里注释了转换调用,结果训练语料里的繁体词全部被当成候选,误报直接爆炸。排错时可以先跑一句纯简体、一句纯繁体,看转换是否对称,再跑一句简繁混合,看结果是否稳定。记住这条链路是:输入 -> 繁体转简体 -> 检测 -> BERT 纠错 -> 简体转目标字型。

5.4 现象四:通用模型对垂直领域错误视而不见

微调完,跑自己领域的句子,发现 BERT 经常把正确字换成错误字,甚至 mask 出来的候选里压根没有正确答案。这不是代码问题,是通用bert-base-chinese在微调之前并没有见过足够多的领域表达。我踩过的典型场景是:某次处理一个含大量机器术语的 demo,模型把术语里的“转矩”纠成“转举”,而且概率极高。原因就是领域词在预训练语料里出现次数太少,mask 预测的分布被通用高频词带偏了。解决方案不是反复调top_k,而是老老实实按第 4 章用领域语料做继续预训练,并把custom_confusion.txt里的领域易错词加进去。这里有个量级感觉:领域语料至少 10 万行以上,模型才可能在 mask 位置给出稳定输出,低于这个量级的大部分提升来自混淆集而非模型微调。

5.5 现象五:命令行参数复制到终端直接“翻车”

run_lm_finetuning.py给的命令里,行尾必须加反斜杠续行,如果不信直接复制粘贴到 bash,第二行--num_train_epochs=3会被解析成一条新命令,报错command not found。我第一次跑就翻了车,后来总结成习惯:凡是从 md 文件复制多行命令行,先在文本编辑器里确认每行末尾的反斜杠没有少。同时注意export CUDA_VISIBLE_DEVICES=0是 Linux 下的写法,Windows 命令行要用set CUDA_VISIBLE_DEVICES=0替代。这类问题不算项目 bug,属于平台差异,但排错时最容易让人怀疑是代码坏了。

6. 进阶:把这份资源改造成属于自己的毕设项目

跑通bert_corrector.py只是第一步。要把这套资源变成能拿出手的毕设或课程设计,我给你一个最省力的改造组合拳:先扩custom_confusion.txt做领域纠错,再用predict_mask.py做上线前的验证,最后写一张前后对比表。

先说混淆集。这个文件的格式非常简单,每行一对词,左边错词右边正确词:

转举 转矩 帐户 账户

加入后,规则层会直接把“转举”标成可疑,并优先选混淆集给定答案。这一步对垂直领域效果立竿见影,且完全不需要重训模型。其次是验证:改造完混淆集,用第 3.2 章的 predict_mask 方法,挖掉十句领域句子的关键字跑 top-1,记录命中率。命中率稳定在 70% 以上,再考虑要不要动微调;反而是模型微调的边际收益在数据量不足时往往不如混淆集来得直接。

最后是成果呈现。毕设答辩最怕你说“我跑通了代码”,答辩评委其实想听的是“你改了哪一层、为什么这么改”。我建议做一个三列表格,把领域特有错误的纠正单独摘出来:

场景原文纠错结果
同音帐户余额不足账户余额不足
形近我要买只笔我要买支笔
领域混淆转举机构异常转矩机构异常

这个表比 loss 曲线更直观,也是判断项目有没有真实工程落地能力的硬标准。

回顾我拆这个项目的全程,有两点教训值得分享。第一是永远先跑通最小案例再追求精度,我一度在规则检测上反复加字典,几天没进展,回头看其实 BERT 精排早就给出了正确候选,问题只在我的预处理顺序上;第二是每次改动数据字典前把原文件做个备份,这是个低成本后悔药,但真到模型效果被改坏了想回滚时,它比重新解压快得多。从那以后,我每次拿到这种开源资源,都强制走一遍“先跑通 demo、再逐层换数据、最后写对比表”的流程,把黑匣子一点一点打开。希望帮到你。

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

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

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

立即咨询