ARTICLE DETAIL

资讯详情

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

文本纠错模型选型与工程实践:KenLM、MacBERT、T5、ChatGLM3、LLaMA一次配齐

文本纠错模型选型与工程实践:KenLM、MacBERT、T5、ChatGLM3、LLaMA一次配齐 简介中文文本纠错是NLP落地中的高频需求这一资源包收集了开源项目pycorrector的完整代码与配置面向算法工程师、NLP学习者和需要快速集成纠错能力的开发人员。项目基于Python 3.8实现涵盖KenLM、ConvSeq2Seq、BERT、MacBERT、ELECTRA、ERNIE、Transformer以及T5、ChatGLM3、LLaMA等多种模型的调用与评估支持音似、形似和语法错误纠正可开箱即用。资源共192个文件以Python源码为主辅以txt说明、Markdown文档、YAML配置、图片示例和测试数据压缩包约10.95MB其中py文件对应模型训练与推理md与txt负责使用说明jpg/png展示结果截图yml便于调整环境参数结构清晰。已有445人学习下载适合希望对比不同模型纠错效果、在SigHAN数据集上复现评估结果的读者。解压后可直接查看各模型的实现脚本、运行配置、结果截图和说明文档Dockerfile与元信息文件也一应俱全能够帮助快速搭建中文纠错实验环境或集成到自身业务中。1. 文本纠错不是单一模型这个资源把KenLM、MacBERT、T5、ChatGLM3、LLaMA一次配齐文本纠错在真实项目里不是什么高深算法题而是每天都要面对的脏数据清洗OCR出来的文档有错字、客服会话里有同音字、输入法上屏有谐音。做这类需求时最头疼的不是找不到模型而是各说各话——统计语言模型、预训练模型、大语言模型各有各的适用场景却很少有人把它们放到同一个工程框架里做对比和组合。这个资源把KenLM、MacBERT、T5、ChatGLM3、LLaMA五条技术路线整理成可直接跑的工程包覆盖从字符级替换到句子级重写再到对话式改写的完整梯度模型权重、推理脚本、加噪工具和评测代码都配齐了。适合想把纠错能力快速接进自己业务的NLP工程师也适合拿来做模型选型对比的算法同学。2. KenLM与MacBERT统计噪声过滤与掩码预测的互补组合2.1 先用KenLM给句子排困惑度训练、二值化与调用KenLM是一个n-gram语言模型工具它本身不负责找出错字而是负责回答“哪句话更像人话”。纠错场景里它最常见的用途是给候选句做排序比如MacBERT或别的手段生成了三个候选纠错句KenLM分别计算困惑度取困惑度最低的作为最终结果。优点是纯C实现训练速度快CPU就能跑缺点是严重依赖语料质量和分词粒度。训练一般分两步。先准备一份清洗过的txt语料每行一句中文用空格分词或直接按字切分。按字切分可以回避分词器的词表边界问题代价是n-gram上下文变短一般我会在按字切分时把阶数调到5。命令是这样# 训练5元字级别语言模型输出arpa格式 lmplz -o 5 --text cleaned_corpus.txt --arpa model.arpa # 转成二进制的klm格式推理速度能快一个数量级 build_binary model.arpa model.klm逻辑说明-o 5表示训练5-gram模型阶数越高对语序的约束越强但数据稀疏问题也越明显--arpa输出标准的arpa格式方便后面做模型插值或者剪枝build_binary把arpa文本格式转成内存映射的二进制加载后不必把整个模型读进内存对超大规模n-gram尤其有用。调用端我用一个最简单的打分函数来演示import kenlm model kenlm.Model(model.klm) def sentence_score(model, text): # text按空格分词因为训练时是按空格拼接后的格式 return model.score(text, bosTrue, eosTrue) / max(len(text.split()), 1) candidates [今天天气不错, 今天天气不好, 今天替气不错] scored sorted(candidates, keylambda x: sentence_score(model, x), reverseTrue) print(scored)参数说明bos和eos表示给句子补上开头和结尾标记相当于给短句子的概率做一个归一score返回的是log10概率所以之后要除以token数量否则长句子天然得分低。reverseTrue让得分最高的候选排最前。实际项目里我不会直接拿这个结果当最终纠错输出而是把它作为后置过滤器如果候选句和原句的困惑度差值小于某个阈值经过标定常见是0.5~1.0就认为这个改动不必要保持原样。这能有效减少过度纠错。2.2 MacBERT软掩码纠错混淆集构造与预测阈值MacBERT是BERT系模型它的纠错逻辑和KenLM完全不同——不是给句子打分而是把输入句子的每个位置看成潜在的错误位置用掩码预测的方式输出该位置最可能的字。它跟BERT的区别在于预训练阶段用了“软掩码”策略不是全部用[MASK]标记而是用相似词以一定概率去替换被mask的词。这个特性让它在纠错任务上比原生BERT更稳因为微调后它更习惯从相似的上下文里挑字而不是纯靠[MASK]的强特征。工程上落地MacBERT纠错需要三样东西预训练权重、混淆集、预测阈值。混淆集是核心它决定了模型能识别哪些错字。常见做法是从同音字表、形近字表、常用错词库整理出映射比如“的/地/得”、“在/再”、“做/作”这类高频易错对。混淆集既用于构造训练数据也用于推理时限定候选范围。一个可用的推理脚本长这样from transformers import AutoTokenizer, AutoModelForMaskedLM import torch model_name hfl/chinese-macbert-base tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForMaskedLM.from_pretrained(model_name) confusion {在: [再], 做: [作], 的: [地, 得]} def correct_macbert(sentence, prob_threshold0.6): tokens list(sentence) result list(sentence) for i, char in enumerate(tokens): masked .join(tokens[:i]) [MASK] .join(tokens[i1:]) inputs tokenizer(masked, return_tensorspt) with torch.no_grad(): logits model(**inputs).logits mask_idx inputs[input_ids][0].tolist().index(tokenizer.mask_token_id) probs torch.softmax(logits[0, mask_idx], dim-1) candidates confusion.get(char, []) if not candidates: continue best_prob 0.0 best_char char for cand in candidates: cand_id tokenizer.convert_tokens_to_ids(cand) prob probs[cand_id].item() if prob best_prob: best_prob, best_char prob, cand if best_prob prob_threshold: result[i] best_char return .join(result) text 今天在去公司的路上我在次遇到了他 print(correct_macbert(text))逻辑说明这段代码对句子逐字符做掩码拿每个掩码位置的预测概率去查混淆集。注意这里只用混淆集里的候选字参与比较而不是直接取top1——因为top1很可能是个形近但语义完全无关的字比如“工”预测成“公”。prob_threshold是改动阈值模型对某候选字的置信度达不到这个值就保持原文我一般从0.6起步数据越脏阈值越低。参数说明hfl/chinese-macbert-base是常用的中文MacBERT权重显存占用不到2GCPU也能推理但逐字符跑会比较慢。实际项目里我会先通过规则比如错词词典、拼音编辑距离筛出少数可疑位置只对可疑位置做掩码预测而不对整句逐字跑能把推理时间缩短一个数量级。2.3 传统路线的参数边界什么场景该用这一组KenLM和MacBERT组合起来覆盖的是“错字级别”的纠错需求比如OCR识别结果、用户输入法提交的短文本。它对以下几种错误特别有效同音字替换“在”写成“再”、形近字替换“未”写成“末”、助词误用“的地得”不分。但它的能力边界也很清晰——处理不了漏字、多字、语序颠倒这类的结构性问题因为掩码预测本质上是“单点替换”它没有能力重新组织整个句子。参数边界我按经验给一个参考表环节推荐参数说明KenLM阶数按字切分用5阶数越高越准但语料小于100万句时容易稀疏混淆集大小3000~10000组太小漏错太大模型学成“乱改”改动阈值0.6~0.7线上要求严谨就调高数据脏就调低候选句困惑度差0.5~1.0小于该差值的改动会被回滚在这个资源包里KenLM和MacBERT的权重、训练语料、混淆集是分开的可以单独替换不用整包重训练。这也是我喜欢这套结构的原因——生产环境里往往是“错误类型变了换混淆集就行”而不是把整个模型重新训一遍。3. T5生成式纠错从加噪到改写训练数据与解码参数全流程3.1 为什么生成式比判别式更适合中文口语错误MacBERT一次只能改一个位置的字但真实文本里的错误往往是连片的语音输入里“我们一起去吃饭”可能被识别成“我们一起气吃法”这里既有同音字“气/去”又有声调错误导致的“法/饭”。遇到这种情况逐个掩码就束手无策了因为模型需要在宏观上知道整句话在说什么才能决定每个字怎么改。T5走的是生成路线。输入一个带错误的句子输出一个改好的句子模型自己决定哪里动、哪里不动、怎么调整语序。它把纠错从“分类”变成“翻译”所以训练语料是成对的——错误句和正确句。在这个资源包里T5相关的内容主要包括三块数据加噪脚本、训练配置、推理脚本。我下面按这个顺序讲。3.2 构造错误句正确句数据集加噪器设计没有现成的成对错误数据最常见的做法是用正确语料反向加噪。把干净句子扰动出错误得到错误句正确句对。加噪器设计直接决定模型习得的行为边界——加噪太狠模型学会把对的也改掉加噪太轻模型学了个寂寞。常用策略是每个句子按15%~20%比例随机选择位置注入三种噪声混淆集替换、随机删除单个字、相邻字交换。一个能用的加噪器如下import random def inject_noise(sentence, confusion, max_errors3): chars list(sentence) noisy chars[:] error_count 0 positions random.sample(range(len(chars)), min(len(chars), max_errors)) for pos in positions: r random.random() if r 0.7 and chars[pos] in confusion: # 70%概率走混淆集替换学同音/形近错字 noisy[pos] random.choice(confusion[chars[pos]]) elif r 0.85 and len(noisy) 1: # 15%概率删一个字模拟漏字 del noisy[pos] elif r 1.0 and pos 1 len(noisy): # 10%概率交换相邻两个字模拟手滑 noisy[pos], noisy[pos 1] noisy[pos 1], noisy[pos] error_count 1 if error_count max_errors: break return .join(noisy), sentence # 用法读入干净语料逐行生成训练对以\t分隔 # with open(clean.txt) as f: # for line in f: # noisy, correct inject_noise(line.strip(), confusion_dict) # writer.write(noisy \t correct \n)逻辑说明真正起作用的是max_errors和随机数分支的占比。70%的噪声走混淆集替换是为了保证模型学到的是“改错字”而不是“随意改写”删除和交换各占15%和10%是为了让模型具备处理结构性错误的能力。随机采样位置时用random.sample避免全部错误挤在一段里。参数说明max_errors3对短句是合理值长句超过50字建议按长度缩放比如max(1, len(sentence) // 10)。当你想控制整体噪声密度时可以改成按泊松分布采样错误数效果差别不大但max_errors必须显式设置否则极端情况下模型会见到整句被删光的训练样本输出会变得不可控。3.3 训练与推理的关键参数训练部分资源包应该已经提供了完整的train脚本我这里重点说几个不调会翻车的参数。纠错是短文本到短文本的任务max_seq_length一般设64或128足够再长只会浪费显存。训练时用标签的交叉熵label_pad_token_id设为-100来跳过padding。推理时T5的解码参数比训练参数更值得关注from transformers import AutoTokenizer, T5ForConditionalGeneration model T5ForConditionalGeneration.from_pretrained(./t5_finetuned) tokenizer AutoTokenizer.from_pretrained(./t5_finetuned) def correct_t5(sentence): inputs tokenizer(纠错: sentence, return_tensorspt, max_length128, truncationTrue) outputs model.generate( **inputs, num_beams5, max_lengthlen(sentence) 8, min_lengthmax(1, len(sentence) // 2), no_repeat_ngram_size3, repetition_penalty1.2, early_stoppingTrue ) return tokenizer.decode(outputs[0], skip_special_tokensTrue)逻辑说明num_beams5是中文纠错里个人经验比较稳的值beam太大会让模型倾向于生成和训练集里最常见句子接近的输出beam太小又容易丢正确的候选。max_lengthlen(sentence)8限制了改写长度纠错的输出不应该比输入长出太多多出来的部分往往是模型在自言自语补话。min_length给了一个下限避免模型输出“好的”两个字来敷衍。参数说明no_repeat_ngram_size3是防重复的关键。中文纠错里模型特别喜欢重复“的的的”或“好好好”这个参数禁止3-gram重复出现repetition_penalty1.2进一步惩罚重复token的概率两个配合才能压住复读机行为。early_stoppingTrue让beam search在所有beam都到EOS时提前终止减少在长句上的推理时间。3.4 评测指标别让Precision和Recall打架T5这种生成式模型最容易翻车的评测方式是只看“句子对就是错”。实际线上场景里一个句子可能原本就对模型画蛇添足硬改成另一个字句子级的“改对”指标是看不出来的。业界比较通用的做法是错误级评测统计模型真正改对了多少个错字又冤枉了多少个正确字。实现上就是把纠错前后的字符串按字对齐统计四类结果TP确实错且改对、FP本来对但被改错、FN确实错但没改。精准率Precision TP/(TPFP)召回率Recall TP/(TPFN)F1取调和平均。这个脚本在这个资源的评测目录里应该已经有了没有的话也可以用jiwer或者简单的Levenshtein对齐来实现。我个人的经验是T5在SIGHAN这种公开评测集上句子级准确率可以做到80%以上但在真实脏数据上F1经常掉到60%以下核心原因是训练用的加噪分布和真实错误分布差太远所以微调完一定要用真实数据做一轮盲测盲测结果才是能向上汇报的数字。4. 把ChatGLM3和LLaMA当纠错引擎用Prompt模板与量化加载4.1 对话式纠错的适用边界把ChatGLM3和LLaMA塞进纠错场景不是为了炫技而是为了解决一个实际痛点传统纠错模型没有“常识”。比如“他昨天去故宫玩了一下午今天累得不行”里的“故宫”被OCR识别成“故官”MacBERT大概率不敢改因为“故官”也是一个合法词。但ChatGLM3知道故宫是一个景点结合上下文常识就能判断这是错别字。这决定了对话式纠错的适用边界适合处理那些需要世界知识、语境理解才能判断的错误比如实体名词、专有名词、跨句指代错误。反过来它不适合处理高频简单的同音字替换——大模型在这些任务上推理速度慢而且可能因为指令理解偏差而发挥不稳定。所以这个资源把LLM相关的内容定位成“会话式纠错引擎”而不是“批处理脚本”这个定位是对的。4.2 Prompt模板与解码约束大模型做纠错的输出质量一半取决于prompt一半取决于解码参数。一个常见的翻车现场是模型输出“这句话的错别字是……”而不是直接给纠错后的文本。问题就出在prompt里没有限定输出格式。我一般用下面这个模板prompt 你是文本校对助手。用户会给你一句中文文本可能包含错别字。 请你只输出纠错后的完整文本不要输出解释不要输出多余内容。 用户输入{sentence} 纠错结果 def correct_llm(sentence, model, tokenizer): inputs tokenizer(prompt.format(sentencesentence), return_tensorspt) outputs model.generate( **inputs, do_sampleFalse, temperature0.1, max_new_tokenslen(sentence) 20, num_beams1, ) return tokenizer.decode(outputs[0][inputs[input_ids].shape[1]:], skip_special_tokensTrue).strip()逻辑说明模板里“只输出纠错后的完整文本”是强制约束把模型的解释欲压住。do_sampleFalse配合temperature0.1意味着推理走贪心解码每次输出都稳定不会因为采样波动出现“这次改了、下次没改”的玄学现象。num_beams1在生成式纠错里够用LLM的上下文理解能力已经足够把候选字挑出来不需要靠beam search去枚举。参数说明max_new_tokens设置成原句长度加20即最多允许模型在句尾多写20个字。如果模型实在没法确定怎么改它倾向于在末尾补一句废话这个长度限制能兜底。如果你在用ChatGLM3的官方实现注意它内部有自带的chat模板不要手动再拼一版LLaMA家族则必须走tokenizer.apply_chat_template否则对话模板里的特殊token缺失生成结果会乱。4.3 量化推理与硬件消耗LLaMA这类开源底座在纠错场景里最常见的部署问题是显存。7B参数fp16推理大约需要14G显存很多线上机器跑不动。这个资源里推荐的方案是8bit量化加载显存占用直接砍半推理质量在纠错任务上几乎无损。一个可用的加载方式是from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig import torch quant_config BitsAndBytesConfig( load_in_8bitTrue, llm_int8_threshold6.0 ) tokenizer AutoTokenizer.from_pretrained(meta-llama/Llama-2-7b-chat-hf) model AutoModelForCausalLM.from_pretrained( meta-llama/Llama-2-7b-chat-hf, quantization_configquant_config, device_mapauto, torch_dtypetorch.float16, )逻辑说明load_in_8bitTrue把线性层权重量化成int8理论上保留了大部分表达能力llm_int8_threshold6.0控制哪些层走8bit、哪些层保留fp16阈值越高保留fp16的层越少。device_mapauto让transformers自动分配层到GPU或CPU显存不足时会把部分层放内存速度变慢但至少能跑起来。参数说明量化加载后推理速度比fp16慢20%~30%在纠错这种短输入任务上基本无感。如果你用的是ChatGLM3-6B它同样支持load_in_8bit显存需求在8G以内就能跑完整推理。这类大模型在纠错场景的瓶颈是吞吐短句批量推理的吞吐大概只有几到十几token/s所以只适合做小流量实时接口或离线批量任务不适合接日均百万级的大流量。4.4 五类模型的选型对比把这五个模型放一块很多人的第一反应是选最强的但在工程里应该选“错误类型能覆盖、延迟能接受、成本能扛住”的。我在实际项目里的对比结论如下模型推理延迟硬件需求能处理的错误典型短板KenLM微秒~毫秒CPU即可候选排序、通顺度过滤本身无纠错能力MacBERT毫秒~十毫秒CPU可跑GPU更稳单字错、同音字、形近字不能处理结构性错误T5十毫秒~百毫秒4G显存以上多字错、漏字、语序错误需要大量成对训练数据ChatGLM3百毫秒~秒级8G显存以上实体知识、语境错误延迟高、输出不稳定LLaMA秒级8G~14G显存私有化部署、可控性需要调prompt和量化这个表格不是让你选一个而是按错误类型做路由的依据。真实项目里我不会只部署其中一个而是两个模型叠加一道过滤MacBERT快速扫一遍字符级错误T5处理它搞不定的复杂错误LLM只在高置信度的实体纠错场景里上。路由框架我在最后一章给完整代码。5. 避坑指南数据泄露、混淆集噪音与解码陷阱5.1 混淆集噪音注入失控现象MacBERT或T5微调后在测试集上纠错率挺高但随便拿一句没毛病的正常句子进去输出被改得乱七八糟比如“我今天很开心”被改成“我今天很开星”。原因加噪时错误注入比例太高。一个20字的句子如果被强制注入5个错误模型在训练时看到的就是“一句错得离谱的话”它学到的映射是“不管上下文只要看到相似字就换”而不是“在确信错误时才改”。解决把每句错误数控制在1~3个且严格限制混淆集替换占比。我给MacBERT加噪时会把替换概率压回70%以内剩余30%留给删除和交换。训练完第一步不是看准确率而是拿完全正确的语料去跑一遍统计误改率误改率超过5%说明加噪器太凶直接调小max_errors重训。5.2 训练集和验证集数据同源现象模型在验证集上的F1刷到85%拿去处理线上真实客服数据掉到55%而且怎么调参都回不去。原因数据划分出问题了。很多人按文档切分训练集和验证集同一个文档里的句子大量重复或者同一批新闻稿里“今天天气不错”这种句式反复出现验证集里全是训练集的变形模型对着类似的句子自然表现出色一遇到真实分布就现原形。解决按句子粒度做hash去重后再划分同时保证验证集来自完全不同的时间窗口或来源。如果资源包自带的数据集没有这种隔离建议直接丢掉自带的dev集单独从线上日志里抽500句人工标注做盲测那才是真实水平。5.3 T5生成重复字符现象T5推理结果里频繁出现“他他他”“好的好的好的”这类复读而且句子的总长度明显超过输入。原因生成时没做重复惩罚。纠错任务的输出长度通常和输入差不多训练数据里很少出现连续重复3次以上的n-gram但beam search在概率空间里探索时容易陷入重复陷阱尤其是当训练语料里高频词聚集时。解决推理时显式开启no_repeat_ngram_size3并加repetition_penalty1.2。如果还压不住就把num_beams降下来beam越大越容易堆叠重复短语。另外检查训练标签里是否有重复字符有些加噪器会把“你好”重复生成“你好你好”这种坏样本一旦混进训练集模型会主动学坏。5.4 KenLM的未登录词惩罚现象候选句明明改对了但KenLM打分反而比原句低导致排序结果把正确纠错排到最后。原因KenLM是n-gram统计模型对训练语料里没出现过的词有天然敌意。比如语料里没有“元宇宙”这个词模型看到一个候选句里含“元宇宙”会给出极大的惩罚而原句里的错误字“原宇宙”因为每个字都在字典里反而得分更高。解决两招。第一招给KenLM喂一份专业词表把这些词在训练时显式打平成空格拼接不做切分第二招计算候选句和原句的困惑度差值时设置下限差值低于0.5就认为两者通顺度相当忽略KenLM的排序建议交给上游模型的决定。5.5 大模型输出解释性废话现象LLaMA纠错输出“这句话的错别字是‘在’应改为‘再’纠错后文本为……”而不是只给文本。原因prompt里没有约束输出格式模型默认走“解释解决方案”的助手风格。另一个常见原因是对话模板没走apply_chat_template导致模型没进入system指令所规定的角色。解决prompt里强制加“只输出纠错后的完整文本不要输出解释”推理时用do_sampleFalse消除随机性。如果模型还废话就在解析层做后处理用正则把“纠错结果”之后的部分截出来或者直接按“输出第一个完整句子”截断。这不是好办法但作为兜底值得留着。6. 进阶落地按错误类型路由模型用改动距离兜底6.1 置信度路由框架五个模型都部署好后不是让它们各跑各的而是串成一条管线。我现在的做法是按置信度分级路由MacBERT输出的改动置信度最高由它拍板字符级修改置信度不足时交给T5做句子级改写遇到实体名词或长句再上ChatGLM3补一层。路由逻辑可以落成下面这个骨架def route_correct(sentence): mac_result correct_macbert(sentence) if diff_chars(sentence, mac_result) 0: return sentence # 无改动直接返回 confidence mac_confidences(sentence, mac_result) if confidence 0.8: return mac_result elif len(sentence) 30 or has_entity_error(sentence): return correct_llm(sentence) else: return correct_t5(sentence)逻辑说明diff_chars统计逐字比较的改动位置数量如果MacBERT一个位置都没改说明它认为句子没有字符级错误不往下走。confidence取MacBERT对每个改动位置候选字的平均预测概率。实体错误检测是简化的句子中出现混淆集里没有覆盖的双字词且该词在词典中不存在就判定为疑似实体错误交给LLM做常识判断。参数说明0.8这个阈值不是拍脑袋定死的。线上反馈误改率高就往上拉到0.9漏改多就降到0.7每次调整都要对着标注集做一次A/B不要凭体感改。这个路由框架的价值在于字符级错误用最便宜的模型处理结构性错误才用贵模型整体推理成本可以压低70%。6.2 改动距离兜底路由之后还有最后一道保险就是KenLM通顺度校验。我会在最终输出前同时计算原句和结果句的困惑度如果结果句的困惑度反而更高且改动字符数小于句子长度的10%就直接回滚到原句if kenlm_score(result) kenlm_score(sentence) - 0.5: return result else: return sentence逻辑说明这个规则的本质是假设检验——如果一次改动没有让句子变得更通顺那这次改动大概率是画蛇添足。0.5的阈值是我在客服语料上标定出来的它过滤掉了大部分“改错不改对”的无效修改。这个框架搭完之后我这个项目的实施教训也就一以贯之了多模型纠错最忌一步到位每个模型负责自己擅长的错误类型再加一层成本兜底。从那以后我每次上线纠错服务都会强制走一遍“原句与结果句双写评测”确认没有把正确句子改坏才敢放量。希望帮到你。本文还有配套的精品资源点击获取
返回列表