ARTICLE DETAIL

资讯详情

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

文本纠错五层模型流水线:从KenLM到ChatGLM3的工程实践

文本纠错五层模型流水线:从KenLM到ChatGLM3的工程实践 简介这份压缩包面向中文自然语言处理开发者和算法工程师集成了Kenlm、T5、MacBERT、ChatGLM3、LLaMA等多个主流模型的文本纠错实现支持常见音近字、形近字与语法错误的自动修正基于Python开发可直接嵌入数据清洗、内容校对等流程。包体共包含192个文件其中以111个Python脚本为主要代码配合多个文本说明、Markdown文档、示例图片和配置文件便于使用者理解整体结构并快速上手压缩包总大小为10.95MB整体目录清晰可快速定位到模型配置、训练脚本与推理示例。目前已有445人浏览学习。资源中提供了多模型调用示范、在标准评测数据集上的评估配置与对比结果并附有不同模型的效果图示能够帮助使用者省去重建环境和复现模型的时间开箱即用尤其适合需要基准测试或方案选型的研发场景。无论用于搭建智能客服预处理还是处理用户生成内容均能直接参考使用也便于初学者通过对比图表快速了解不同模型在中文纠错任务上的表现差异。1. 文本纠错不是换字五层模型流水线是开箱即用的解法“文本纠错”这个需求比看起来要脏得多。我接过的几个项目——搜索Query改写、语音识别结果后处理、作文批改——坏句子远不止错别字音近字、形近字、多字漏字还有整句不通顺但每个词都认识。单靠正则和词典根本写不过来。所以我最后搭了一套五层方案KenLM做快速通顺度闸门MacBERT在候选字中打分改字T5做增删改词的生成式重写ChatGLM3和LLaMA处理前几层不敢动的长难句。每一层都能单独摘出来用模型文件放好、改一行配置就能跑训练脚本、推理脚本和评估脚本一起交付开箱即用。适合手里已有推理代码、缺纠错策略的从业者照着复现。2. KenLM先做闸门n-gram语言模型为什么是最早能上线的纠错层2.1 定位KenLM不纠错它只判断哪句话更“像人话”KenLM 是一个 n-gram 语言模型工具包输入一批文本语料输出一个 .arpa 格式的统计语言模型。这个模型本身不会给你“正确答案”它只干一件事给一串词打分。翻译成工程语言就是它能告诉你“我今天去公司”比“我今天去工司”更像人话而且快得惊人。这句话在纠错流水线里极其值钱。很多生产环境的错字并不是错得离谱而是整句话的概率被拉低了。你拿一个 3-gram 模型去算句子的对数概率错字所在的 n-gram 组合几乎一定是局部低谷。所以常见做法是用它做“闸门”先用极低的开销把可疑句子筛出来再交给更贵的模型处理而不是每句话都直接上 BERT 或大模型。选 KenLM 还有一个很现实的理由运维便宜。它是 C 实现训练吃 CPU 和内存推理时单条句子毫秒级返回不需要 GPU、不需要显存公司里随便一台机器都能跑。对比后面要上的 MacBERT、T5、ChatGLM3、LLaMA它几乎不占资源。我所有纠错项目都把 KenLM 放第一位项目黄了它也能留着做文案通顺度检查。常见误用是拿语言模型当“生成器”让模型自己“造”一个正确的词补回去。这不是 n-gram 擅长的事n-gram 只负责评判不负责生成。把候选生成和候选打分解耦才是这套方案能灵活替换各层模型的关键。2.2 训练一个最小中文 KenLM命令与参数训练语料建议用业务场景里的大规模真实文本比如历史搜索日志、评论、新闻语料。如果手里没有至少要准备几十万行通顺文本否则 n-gram 计数稀疏到没法用。清洗时要注意英文、数字、常见标点要保留它们在实际坏文本里高频出现模型必须学会给这些 token 打分。bin/lmplz -o 3 -S 30% -T ./tmp --prune 0 0 1 -t 8 \ corpus.txt model.arpa bin/build_binary model.arpa model.klm参数说明-o 3n-gram 阶数。中文纠错 3 到 5 足够阶数越高越吃内存对低频组合也更不友好。我默认先用 3 跑通效果不够再升到 5。-S 30%训练时临时内存占用上限按物理内存百分比计算。30% 是常用起步值机器内存小就降到 10%代价是训练变慢。--prune 0 0 1剪枝参数三个数字对应 1-gram、2-gram、3-gram 的最低保留频次。写成0 0 1表示 3-gram 只保留出现次数大于 1 的组合模型体积能小不少。-T ./tmp中间文件目录语料大时会写不少临时文件磁盘要留足。build_binary把文本 arpa 转成二进制加载速度从秒级降到毫秒级线上推理必须做。训练完先别急着接入服务用工具自带 query 命令扔几对句子进去看看分数方向是否正确。比如比较“我今天去公司上班”和“我今天去工司上班”前者分数应该明显更高。如果方向不对说明语料分布和业务场景偏差太大先换语料别调参硬扛。提示中文语料用字级还是词级取决于下游。我做快速闸门时直接用字级模型避免分词错误传导到 n-gram 计数里词级模型留给后面语义层用。2.3 解码候选生成 LM 重打分的最小实现把 LM 接进纠错循环核心是“谁提供候选谁做裁决”要分开。候选可以来自形近字/音近字混淆表也可以来自语音识别解码器输出的混淆网络。KenLM 只做裁决对每个候选改写后的整句重新打分比原句分数高出足够多才接受。import kenlm model kenlm.Model(model.klm) def score_sentence(tokens): return model.score( .join(tokens), bosTrue, eosTrue) # 混淆表按业务维护这里只做演示 confusion { 工: [公, 功] } def correct(text, min_gain1.5): tokens list(text) # 字级切分演示用词级需配自己的分词 base_score score_sentence(tokens) best None for i in range(len(tokens)): for cand in confusion.get(tokens[i], []): new_tokens tokens[:i] [cand] tokens[i1:] new_score score_sentence(new_tokens) gain new_score - base_score if gain min_gain: best (gain, i, cand, .join(new_tokens)) if best is None: return text return best[-1]逻辑说明把原句得分当作基线遍历每一个可能出错的位置替换候选后重新计算整句对数概率只接受增益超过阈值的修改。这里最关键的是必须有基线对比否则模型会把一切句子都往高频词方向推。参数说明min_gain1.5是经验值。KenLM 的 score 默认以 10 为底差值 1.5 意味着概率相差约 30 倍已经算保守。语料越大、阶数越高这个值要跟着调。建议在验证集上从 0.5 扫到 2.0选过改率最低但不损失召回的点。逐个位置重打分的时间复杂度是 O(N×C)N 是句子长度C 是候选数。对绝大多数业务文本完全能接受不需要一上来就做 beam 搜索。白名单必须单独维护。人名、地名、数字、英文 token 一律不参与候选生成否则第 5 章里那个“把‘小王’改成‘小五’”的翻车现场就是你的上线首日。实际项目中我不会让 KenLM 单独拍板。它的分数会带着“可疑位置”传给 MacBERT由后者在这些位置上做语义级确认。3. MacBERT与T5掩码打分和生成式改写怎么互补3.1 MacBERT 为什么适合中文错别字近义词掩码练出来的还原能力MacBERT 是 BERT 的中文预训练变体预训练任务不是随机把字换成 [MASK]而是用近义词、形近词替换原文里的词让模型根据上下文还原被替换的词。这几乎就是纠错任务本身文本里存在一个“不合理的词”模型要从上下文推断出原来的词。所以在纠错场景里我一般不用 MacBERT 直接生成整句而是把它当作位置打分器。做法是把可疑句输入模型对怀疑的位置只计算候选词的概率而不是在整个词表上找 top-1。全词表 top-1 噪声极大因为很多字在特定语境下概率都不低但候选集里通常只有一个是对的。把问题缩小成“选 A 还是选 B”准确率会高非常多。它和 KenLM 的分工也不一样。KenLM 分数反映“这句话像不像人话”MacBERT 分数反映“在语义和语法双重约束下这个位置更可能是哪个字”。前者管整句后者管局部串行使用最顺手KenLM 筛出可疑句MacBERT 逐个可疑位置给候选打分。可疑位置的获取有三个来源混淆表扫描、KenLM 低分位置、一个专门训练的二分类错字检测模型。实践中我取三者并集。宁可多送几个位置给 MacBERT也不要漏掉真正出错的地方因为 MacBERT 打分成本远低于大模型重写。3.2 用 MacBERT 给候选打分的推理代码from transformers import AutoTokenizer, AutoModelForMaskedLM import torch tokenizer AutoTokenizer.from_pretrained(权重目录/macbert) model AutoModelForMaskedLM.from_pretrained(权重目录/macbert) model.eval() def score_candidates(sentence, position, candidates): tokens list(sentence) masked tokens.copy() masked[position] [MASK] inputs tokenizer( .join(masked), return_tensorspt) with torch.no_grad(): outputs model(**inputs) mask_idx inputs[input_ids][0].tolist().index( tokenizer.mask_token_id ) probs torch.softmax(outputs.logits[0, mask_idx], dim-1) scores {} for cand in candidates: cand_id tokenizer.convert_tokens_to_ids(cand) if cand_id is not None and cand_id ! tokenizer.unk_token_id: scores[cand] probs[cand_id].item() return scores sentence 我今天去工司打卡 position 4 candidates [公, 工, 功] print(score_candidates(sentence, position, candidates))逻辑说明把可疑字替换成 [MASK]让模型根据左右上下文预测该位置的概率分布然后只取候选集里的概率。注意用了中文 char 级切分并按空格 join这是 BERT 系中文权重常见的预处理方式直接传整句中文经常和训练格式对不上。参数说明position必须由检测阶段给出不要让模型自己扫描全句找错。模型扫描全句会倾向于“没看见错”因为预训练语料里绝大多数位置都是对的。候选集建议控制在 5 个以内。候选太多概率被稀释区分度明显下降。候选词如果不在词表里convert_tokens_to_ids会返回 unk此时直接跳过不要硬算。生僻字纠错交给字音字形的规则层更靠谱。这一步产出的是每个候选的归一化概率。后续我会把它和 KenLM 的增益分数做乘法融合KenLM 保证“改完更像人话”MacBERT 保证“这个位置就该是这个字”两边都过才写回结果。3.3 T5 做生成式纠错把“改错”当作“翻译”MacBERT 的天然局限是只能做位置替换多字、少字、词序乱、整句重排都处理不了。T5 的做法完全不同它把含错句子当源语言把纠正后的句子当目标语言用 seq2seq 的方式一次性生成整句。模型不是在选字而是在理解整句语义之后重写一遍。这个思路和机器翻译高度相似训练数据构造几乎可以照搬翻译项目的经验。准备一份无错语料用混淆集随机替换其中的字或词生成错误样本同时保留一部分原句作为对照样本。训练时让模型同时见两类样本一类是“错句到对句”一类是“对句到对句”防止模型看到任何输入都强行改一遍。[ { input: 我今天去工司打卡, target: 我今天去公司打卡 }, { input: 他说他明天不堪了, target: 他说他明天不看了 }, { input: 这完全是正确的一句话, target: 这完全是正确的一句话 } ]推理时用 beam search不要用 greedy。greedy 在需要长距离重排的句子上很容易陷进局部最优。from transformers import AutoTokenizer, AutoModelForSeq2SeqLM model_name 权重目录/t5-纠错 tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSeq2SeqLM.from_pretrained(model_name) def correct_t5(text): inputs tokenizer(text, return_tensorspt, max_length64, truncationTrue) outputs model.generate( **inputs, max_new_tokens64, num_beams4, length_penalty0.8, no_repeat_ngram_size3, do_sampleFalse, ) return tokenizer.decode(outputs[0], skip_special_tokensTrue)逻辑说明no_repeat_ngram_size3防止生成时反复出现同一个三字片段这个参数在长句纠错里很关键否则模型容易循环输出。参数说明num_beams4是成本和效果之间的平衡点纠错场景通常不需要超过 8。length_penalty0.8小于 1倾向于生成更短的句子。纠错原则是尽量少改所以压制长度对“不过改”有帮助。max_new_tokens按输入长度的 1.5 倍设就够。给太大除了浪费算力还会诱导模型在句子后面续写这是生成式纠错最典型的翻车方式。3.4 两个模型怎么分工短错靠打分长改靠生成MacBERT 适合“错字密度高但长度短”的文本比如搜索 Query、语音转写结果、两三个词组成的短语。它快、稳、不越界出错也容易追查。T5 适合“句子完整但需要增删改”的文本比如评论、邮件、长 Query。在流水线里我通常把它们做成并联MacBERT 先跑凡是置信度低于某个阈值的可疑位置才让 T5 对整句做一次生成式改写。T5 生成的句子还要拿 KenLM 的增益分数做交叉验证不能直接采信。两个模型各司其职比拿两个模型对同一个字做投票靠谱得多——后者在实践里非常容易打架因为 BERT 系和 T5 系对“哪个字才是对的”判断逻辑完全不同。4. ChatGLM3与LLaMA大模型纠错不是越贵越好要看长难句4.1 大模型给纠错带来了什么增量前几层模型解决的都是局部问题但真实坏文本里还有一类难搞的东西整句语法没问题、词也没错就是语义不通或表达混乱。举例“我今天吃了一个苹果然后去了公司但我不想上班于是回来睡觉了。”每个词都对但读起来冗余、别扭传统模型拿它没办法因为它们只能在词表上做替换无法对整个句子的组织方式进行重写。ChatGLM3 和 LLaMA 这类生成式大模型的价值就在这里。它们对语义有更强的整体把握能感知到“这句话哪里别扭、哪里多余、哪里缺了宾语”并且能生成符合原意、更自然的句子。局部错字它们也能做但让大模型去给候选字打分是杀鸡用牛刀——推理慢还未必比 MacBERT 准。我一般把大模型放在流水线最末端只处理前两层没有把握的句子。4.2 ChatGLM3 接入prompt 模板和推理参数用 ChatGLM3 做纠错最关键的不是模型结构而是 prompt。它不像 T5 一样天然被训练成纠错模型需要把任务描述清楚并给出两三个示例。示例格式必须和推理时保持一致否则模型容易混淆任务边界。from transformers import AutoTokenizer, AutoModelForCausalLM model_name 权重目录/chatglm3 tokenizer AutoTokenizer.from_pretrained(model_name, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained( model_name, trust_remote_codeTrue, torch_dtypeauto ).half().cuda() def correct_glm(text): prompt ( 你是一个文本纠错助手。用户会给你一段可能有错别字、多余字、漏字或表达混乱的文本。 请直接输出纠正后的完整文本不要添加任何解释。\n\n 例子1\n输入我今天去工司打卡。\n输出我今天去公司打卡。\n\n 例子2\n输入他说的这句话什么意思我不太理解。\n输出我不太理解他说的这句话什么意思。\n\n f输入{text}\n输出 ) inputs tokenizer(prompt, return_tensorspt).to(cuda) outputs model.generate( **inputs, max_new_tokens128, temperature0.1, top_p0.9, do_sampleTrue, ) response tokenizer.decode( outputs[0][inputs[input_ids].shape[1]:], skip_special_tokensTrue ) return response.strip()逻辑说明prompt 里明确写了“不要添加任何解释”并在解码后把 prompt 部分截断。大模型很容易在答案后面补一句“已为您修改”截断是必须的。参数说明temperature0.1纠错要的是确定性最小修改温度越低越少发挥。top_p0.9配合低温度使用限制候选集合的累积概率。max_new_tokens按输入长度调整短文本给 64 就够长文本给到 256。给太长模型会在答案后面继续闲聊。注意这里用的是AutoModelForCausalLM和 T5 的AutoModelForSeq2SeqLM解码方式不同前者需要自己截断 prompt后者输入输出天然分开。如果请求量上来不建议用这个脚本直接扛服务。常见做法是前面套一层 vLLM 做动态批处理脚本只负责构造 prompt 和解析结果。4.3 LLaMA 系列接入量化推理和 LoRA 之间的选择LLaMA 系模型在纠错场景里的接入方式和 ChatGLM3 没有本质区别都是 prompt 加生成。区别在资源占用。有人问过 LLaMA 模型的源码到底多少行——说句实话源码行数不是瓶颈权重文件和显卡才是。要在一台没有大显存的机器上跑常见做法是直接下载量化权重而不是啃源码重新训练。源码阅读的意义是帮你改推理脚本而不是帮你解决部署。量化版本的选择直接影响中文纠错效果。我试过在 Windows 老机器上折腾 llama.cpp 跑 Q3 量化确实省显存但中文错字纠正的效果明显不如 Q4。后来统一用 Q4_K_M中文长句的稳定性才够看。推理命令大致长这样llama-cli -m model.Q4_K_M.gguf \ -p 你是一个文本纠错助手。直接输出纠正后的完整文本。输入我今天去工司打卡。输出 \ -n 64 --temp 0.1 --top-p 0.9参数说明-n 64生成 token 上限短文本 64 足够。--temp 0.1和 ChatGLM3 同理纠错不需要创造性。--top-p 0.9低温度下限制候选范围防止跑偏。如果想让 LLaMA 在业务上更贴近“只改错、不改写”不要迷信全参微调LoRA 性价比高得多。用 PEFT 库只微调 attention 层的低秩矩阵训练数据直接用第 3 章 T5 那套统一格式。微调后的行为会更贴合纠错而不是像通用权重那样“发挥创造力”。顺序上注意先微调出 FP16 权重再导出量化 GGUF不要用量化权重直接微调否则权重更新会失真。4.4 三层串联与降级策略什么句子才值得动用大模型完整流水线我建议这样排KenLM 先算分低分句进 MacBERT 候选打分MacBERT 的置信度在 0.3 到 0.7 之间经验阈值按验证集调的句子进 T5 做生成式改写T5 改写后与原句差异过大或 MacBERT 对改写结果依然没把握再交给大模型重写。这样设计的原因很现实大模型推理成本比 MacBERT 高两个数量级。如果每条消息都让 ChatGLM3 或 LLaMA 重写一遍延迟和显存规划都扛不住。降级的核心理念是便宜的模型有把握就让它做没把握才往上送。实际项目里能走到大模型这一层的句子通常不到总量的一成。5. 串联避坑五个模型接起来后的常见问题、现象、原因、解决5.1 KenLM 把对的句子改成更“通顺”的错句现象上线后一批原本正确的短句被改坏比如“请使用本功能”被改成“请用本功能”“公司规定”被改成“公司规则”。表面看都通顺但语义已经变了。原因n-gram 语言模型天然偏向高频词串。它不知道哪些修改是“纠错”哪些是“改写”只要新句得分更高它就认为更好。解决修改决策不能只看“新句得分更高”必须满足两个条件才动手候选来自混淆集且增益超过阈值。同时维护一个全局白名单业务专名、人名、数字、英文一律不参与替换。这个坑是我上线当天被运营反馈打回来的加上阈值和白名单之后才稳住。5.2 MacBERT 对中文标点和英文静默现象中文句子里的英文单词拼错MacBERT 完全不反应中文标点被误打成全角半角混用也一样不反应。原因中文预训练模型的词表里英文和标点是低频 token预训练时被掩码替换的概率低模型对它们的表征质量差输出概率基本接近随机。解决英文拼写交给专门的英文拼写检查库标点问题走规则校验不要指望 MacBERT 学会标点纠正。混淆集构造时只保留汉字词避免模型在异常 token 上输出随机概率干扰决策。5.3 T5 把数字和人名改掉现象T5 生成结果里“李四”变成了“李思”“10086”变成了“10086 客服”训练集里越少见的内容被改得越勤快。原因生成式模型在训练数据里见过大量“改错”样本导致它对原句过度不信任倾向于把低频内容替换成高频内容。人名和数字在语料里出现频率极低受害最重。解决训练数据构造时对数字和人名做保护标记让模型学会“这些内容不可替换”。推理阶段对生成结果做后校验凡是在原句中出现过的数字和人名如果被改变就退回原词不再采信生成结果。5.4 ChatGLM3 和 LLaMA 在低配置机器上效果反而更差现象普通显卡上跑大模型单条推理耗时数秒长句经常生成一半就断效果还不如 T5。原因显存不足导致只能用低比特量化或小尺寸权重中文纠错所需的细粒度上下文信息受损。同时 prompt 里的 few-shot 示例太长挤占了可生成长度。解决重新审视降级策略不要让大模型处理每一条文本。确实要上时优先用 Q4_K_M 量化few-shot 压缩到 2 个示例输入截断到 64 字以内。大模型这一层只做长难句重写不要做候选打分候选打分用 MacBERT 更省心。5.5 多个模型结果冲突没人知道听谁的现象KenLM 说该改成 AMacBERT 说 B 是原词T5 输出 C大模型输出 D。线上结果取决于最后一个执行的模型行为不可解释出问题也没法追责。原因各模型输出的分数不是一个标尺无法横向比较。把不同分布的分数硬拼在一起投票天然会打架。解决给每个修改动作定义一个统一的三档置信度规则。低置信只有一个模型支持不改。中置信KenLM 增益和 MacBERT 概率同时支持改。高置信T5 或大模型重写后返回来的句子依然满足 KenLM 增益阈值改。实际运营时我还会把“修改前、修改后、谁建议改、谁否决”一起记日志回看时能定位到是哪一层在乱改。6. 验证指标与回归集让“开箱即用”变得可量化6.1 三个指标准确率、召回率、过改率纠错系统的评价不能只看“改对了多少”还要看“不该改的被改了多少”。我衡量这套五层方案的标准指标有三个准确率是修改中改对的比例召回率是真实含错样本中被正确修改的比例过改率是无错样本里被改过的比例。前两个越高越好过改率必须压到 5% 以下否则用户会明显觉得系统在“自作聪明”。回归集准备两份。一份用公开纠错评测集但它偏正式书面语只用来横向对比模型能力。另一份必须从自己业务里抽样把线上用户的真实坏句子按月手工标注一批作为长期回归基准。每次改模型、调参数先跑回归集三个指标都过了再谈上线。6.2 一个最小评估脚本def evaluate(pairs, gold_error_flags): tp fp fn 0 for (raw, pred), has_error in zip(pairs, gold_error_flags): changed (raw ! pred) if changed and has_error: tp 1 elif changed and not has_error: fp 1 elif not changed and has_error: fn 1 precision tp / (tp fp) if tp fp else 0 recall tp / (tp fn) if tp fn else 0 over_correction fp / len(pairs) return precision, recall, over_correction这个脚本故意写得朴素目的是让团队在每次调参后都能快速看到三个数字的变化。真正上线前我还会加一层人工抽检对模型判定为“无错”的样本每周抽 200 条重新标注防止模型为了压低过改率而故意不纠错把召回率一起拖垮。我现在的习惯是所有模型版本更新都绑一份回归报告三个指标不达标就回滚。这个规矩救过我很多次——有一次 T5 微调后准确率涨了 3 个百分点但过改率从 3% 涨到 9%要不是回归集在上线第二天就会被用户投诉。这套从 KenLM 到大模型的分层方案如果你正在搭自己的文本纠错服务可以按这个顺序先跑通 KenLM 加 MacBERT再根据长难句比例决定要不要引 T5 和大模型。希望帮到你。本文还有配套的精品资源点击获取
返回列表