恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

SIGHAN中文纠错数据集全解析:从原始标注到BERT训练实战

  • 首页
  • 资讯中心
  • /
  • SIGHAN中文纠错数据集全解析:从原始标注到BERT训练实战

相关资讯

SIGHAN数据集转换指南:原始格式、BIO标签与常见坑 2026/10/6 3:02:12
Unity 2D跑酷游戏实战:跳跃射击、跳板判定与音效系统全解析 2026/10/6 2:57:12
MFC屏幕取色器实战:GDI像素读取与全局交互全解析 2026/10/6 2:57:12

最新资讯

本体(Ontology)构建实战:从哲学概念到AI知识引擎
ControlNet云端部署实战:从环境配置到性能优化全指南
Jenkins+Unity在Windows下自动打包APK完整指南与踩坑实录
大数据OLAP分页实战:从OFFSET到游标分页的性能优化指南
VMware报错Device/Credential Guard不兼容?三条方案彻底解决
CSS弹性布局实战指南:Flexbox核心属性、应用场景与避坑总结

今日推荐

2026 AI 开发全家桶落地指南:TaoToken 统一 Key 打通 IDE 插件、Agent 与自动化代码审查全链路配置实测
MR25H40CDF+STM32F031C6工业级高可靠数据存储方案
MRAM+STM32工业断电数据保全实战指南

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

SIGHAN中文纠错数据集全解析:从原始标注到BERT训练实战

发布时间:2026/10/6 3:02:12
SIGHAN中文纠错数据集全解析:从原始标注到BERT训练实战 简介SIGHAN中文纠错数据集是汉语语法错误检测与拼音标注领域的权威资源由新加坡国立大学团队创建。这份压缩包在原始SIGHAN数据基础上进行了系统格式转换面向中文自然语言处理研究者、算法工程师及相关专业学生可用于中文拼写检查、语法纠错模型训练与语料库建设。包内共78个文件以txt格式原始语料与sgml标注文件为主同时包含readme说明文档、py转换脚本、jar工具、pdf技术文档及xlsx整理表格等压缩包整体约19.92MB目录结构清晰便于按SIGHAN 7/8及CLP14CSC等子集检索使用。目前已有379人学习下载。借助转换脚本与格式说明使用者可快速将原始错误标注转为适合机器学习模型批量处理的CSV或CoNLL格式并完成训练、验证、测试集划分配套文档还展示了错误类型覆盖与标注规则能有效降低数据预处理门槛帮助读者构建更精准的中文纠错系统。1. SIGHAN中文纠错数据集绕不开的“标准错题本”做中文拼写纠错Chinese Spelling CheckCSC的从业者十有八九绕不开 SIGHAN。这是 ACL 下设中文信息处理兴趣小组在历年中文纠错评测任务里发布的一组公开语料每一句都标注了“哪个字错了、错在哪、属于什么错误类型”颗粒度细到字符级所以中文纠错方向的论文几乎都拿它当基准。更关键的是这个 zip 在原始语料之外还附了一版转换后格式——不用再从零写清洗对齐管线解压之后直接就能往 BERT 类模型的数据 loader 里塞。你如果正在做中文纠错、语文作文批改相关项目或准备复现一篇纠错论文这份数据包会是你最省事的一块敲门砖。2. 读懂SIGHAN原始标注错误类型、位置字段与读取脚本2.1 错误类型只有四类位置字段却最容易看走眼SIGHAN 原始数据最常见的版式是每行五个字段tab 分隔序号、错误句、正确句、错误位置、错误类型。错误句是“带错原文”正确句是“改完之后的句子”位置字段标的是错误发生在第几个字符类型字段则用一个字母说明错误性质。我见过不少人一拿到数据就急着写转换脚本结果栽在位置字段上所以我建议第一步先把原始标注的语义拆清楚。四类错误分别是Ssubstitution替换、Ddeletion缺失、Iinsertion插入、Rreversal倒序。替换是说一个字符被写成了另一个字源句和目标句长度相等缺失是源句少了一个字目标句反而更长插入是源句多了一个字目标句更短倒序则是相邻两个字符顺序写反。部分 re-pack 版本里还有第五类 U表示标注者也无法判断的类型处理时建议不要直接丢弃而是单独归为一类。举个演示样例不是数据集原文是我随手造的错误句“学声乐可以陶治情操”正确句“学声乐可以陶冶情操”这里“治”写错应为“冶”位置在第 7 个字符类型是 S。用列表示就是学/声/乐/可/以/陶/治/情/操第 7 位标错。这个位置按字符数从 1 开始计数包含标点、数字和所有可见字符不区分全角半角。注意SIGHAN 的位置从 1 开始而 PyTorch 和大部分模型内部习惯从 0 开始。转换时统一减去 1别一半 0 一半 1 地混着用。位置字段的写法在不同发布批次里也不一样有写成纯数字的7有写成范围3-4的有多个位置用空格或逗号分隔的。类型字段同样可能是一个字母也可能是多个字母。所以读取函数不能写死分割符我一般用正则来拆import re from typing import List, Tuple def parse_sighan_positions(pos_field: str) - List[Tuple[int, int]]: 解析位置字段兼容 3、3-4、3 5、3,5 等常见写法。 out [] for token in re.split(r[\s,], pos_field.strip()): if not token: continue if - in token: left, right token.split(-, 1) out.append((int(left), int(right))) else: p int(token) out.append((p, p)) return out def parse_sighan_line(line: str): fields line.rstrip(\n).split(\t) if len(fields) 5: return None idx, wrong, correct, pos_field, type_field fields[:5] positions parse_sighan_positions(pos_field) types type_field.split() if len(types) len(positions): types types [U] * (len(positions) - len(types)) return {idx: idx, wrong: wrong, correct: correct, errors: list(zip(positions, types))}这个函数做两件关键事一是用正则把位置字段里的空格、逗号、全角逗号全当成分隔符避免因为数据格式不统一直接崩二是当类型字段数量少于位置数量时自动补U保证errors列表里每个错误点都有对应类型。实际数据里的脏行不少返回None并统计丢弃行数比直接抛异常更适合批量转换场景。2.2 位置字段基于哪一句其实是有讲究的对替换类错误源句和目标句同一位置的字都能对上位置没有歧义。但对缺失和插入类错误位置到底是按源句数还是按目标句数不同数据集的处理习惯不同。以缺失为例源句少了一个字“少了字的位置”在源句里根本不存在字符标注者只能按目标句去数位置而这个位置在转换时又不能在源句里直接取到字符。插入则正好反过来。所以拿到原始数据后我习惯先做一个断言对每条 S 类型、等长的错误检查源句和目标句在标注位置上的字符确实不同对 I/D 类型检查两个句子的长度差与错误类型自洽。这一步能把相当一部分格式理解错误挡在建模之前。def quick_check(item): src, tgt item[wrong], item[correct] for (a, b), typ in item[errors]: if len(src) len(tgt) and typ S and a b: assert src[a - 1] ! tgt[a - 1], ( f位置{a}两个字符相同: {src[a - 1]!r} ) return True这个检查看起来很基础但很多踩坑都是从“我以为类型字段是 S结果两个字符都一样”开始的。转换后格式再好原始标注读错了后面全是白做。3. 转换后格式到底转了什么三条落地路径与对齐逻辑“转换后格式”这个词没有统一标准不同作者打的包差异很大。我会按自己多次整理这份数据时的习惯把最常见的三种落地形态各讲一遍你看手头的 zip 时心里就有谱了。3.1 字符级序列标签喂给 BERT 类纠错模型的默认选择中文拼写纠错里最主流的做法是把它建模成序列标注对源句每个字符预测一个标签标签非 O 就说明这个字错了再根据标签类型决定怎么改。转换后格式如果走这条路通常是一个 JSONL 文件每一行是一个字典包含序号、源句、目标句、错误列表以及可选的对齐后的字符级标签列表。{ idx: 1, src: 学声乐可以陶治情操, tgt: 学声乐可以陶冶情操, errors: [ {start: 7, end: 7, type: S, src_char: 治, tgt_char: 冶} ], labels: [O, O, O, O, O, O, S, O, O] }其中labels与src逐字符对齐长度相同这是序列标注模型最想要的形态。注意D缺失类型在源句里没有对应字符常见做法是把缺失位置左侧的字符标成D让模型学“这个字附近少了一个字”。这个挂靠位置没有绝对标准我用的是左侧挂靠如果你看过别家的转换格式用的是右侧也不奇怪。为什么首选字符级序列标注因为中文拼写纠错的错误密度很低绝大多数句子只需要改一到两个字用生成式模型重写整句的代价太高而且错误位置信息是明确给出的直接监督一个分类头更省资源、也更容易诊断问题。3.2 平行句对喂给生成式纠错模型的前提另一条路是把数据整理成src和tgt两组平行文本直接用来微调 BART、T5 这类生成模型。转换后格式如果走这条路除了文本对还要把错误位置信息保留在一个单独字段里这样模型可以额外接入“哪些位置可能错了”的提示。{ idx: 2, src: 他喜欢打蓝球, tgt: 他喜欢打篮球, errors: [ {start: 6, end: 6, type: S, src_char: 蓝, tgt_char: 篮} ], src_length: 6, tgt_length: 6 }保留src_length和tgt_length看着多余实际上有奇效后面加载数据时可以直接用长度差判断句子是替换型错误还是插入/缺失型不需要再回头扫描错误类型字段。生成式模型的 tokenizer 是 BPE 级别字符位置没法直接对上所以转换格式里必须额外给“错误在原文里的字符偏移”否则位置信息就丢了。3.3 最小转换脚本把原始 TSV 拍平成 JSONL如果手头这份 zip 里只有原始 TSV没有现成转换结果你就需要自己写一次转换。我一般用一个极简脚本只做三件事解析原始行、按统一 schema 输出、统计成功和丢弃行数。import json from pathlib import Path def convert_sighan_tsv(source: Path, output: Path) - None: ok dropped 0 with source.open(r, encodingutf-8) as fi, \ output.open(w, encodingutf-8) as fo: for line in fi: parsed parse_sighan_line(line) if parsed is None: dropped 1 continue errors [] for (a, b), typ in parsed[errors]: errors.append({ start: a, end: b, type: typ, src_char: parsed[wrong][a - 1] if a len(parsed[wrong]) else None, tgt_char: parsed[correct][a - 1] if a len(parsed[correct]) else None, }) record { idx: parsed[idx], src: parsed[wrong], tgt: parsed[correct], errors: errors, } fo.write(json.dumps(record, ensure_asciiFalse) \n) ok 1 print(fok{ok}, dropped{dropped})这里有几个参数细节值得注意。ensure_asciiFalse会把中文直接以 UTF-8 写进文件否则 json.dumps 默认会把中文转成\u转义序列之后人工检查数据时根本没法看也容易在后续加载时徒增解码心智负担。src_char和tgt_char只按start位置取字符对缺失和插入类错误可能取到 None我在代码里用条件表达式兜了底不会抛越界异常。最后那个ok和dropped的输出不是装饰它能帮你快速判断原始文件有没有大面积脏行如果 dropped 占比超过 1%先回去检查解析逻辑别急着往下训练。4. 用转换后数据跑通最小纠错基线从 JSONL 到训练循环转换后格式的价值要落到模型上才算数。这一章我带你走一遍最小可跑的流程把 JSONL 读进来、转成 BERT 能吃的输入、配上几个关键参数。4.1 数据加载字符标签与 token 的对齐方法BERT 类模型的 tokenizer 会对句子做切分中文 BERT 下绝大多数汉字是一个 token但[CLS]、[SEP]以及个别标点会改变序列长度所以字符级标签不能直接塞给模型必须和 token 对齐。正确做法是让 tokenizer 返回offset_mapping再按字符偏移把标签映射到 token 上。import json import torch from transformers import AutoTokenizer IGNORE_INDEX -100 LABEL_O O LABEL2ID {O: 0, S: 1, D: 2, I: 3, R: 4} def load_and_align(jsonl_path, tokenizer, max_len128): encodings [] with open(jsonl_path, encodingutf-8) as f: for line in f: item json.loads(line) enc tokenizer( item[src], max_lengthmax_len, truncationTrue, paddingmax_length, return_tensorspt, return_offsets_mappingTrue, ) char_labels [LABEL2ID[LABEL_O]] * len(item[src]) for err in item[errors]: pos err[start] - 1 if err[type] in LABEL2ID: char_labels[pos] LABEL2ID[err[type]] # 按 offset_mapping 映射到 token 空间 token_labels [IGNORE_INDEX] * len(enc[input_ids][0]) for t, (start, end) in enumerate(enc[offset_mapping][0]): if start end: continue # [CLS]/[SEP] 或 padding 位置 chunk char_labels[start:end] if not chunk: continue token_labels[t] chunk[-1] # 取区间内最后一个标签 encodings.append({ input_ids: enc[input_ids][0], attention_mask: enc[attention_mask][0], labels: torch.tensor(token_labels, dtypetorch.long), }) return encodings这段代码有两个关键点。一是char_labels先按源句长度初始化成全 O再把每个错误位置覆盖成对应类型这样源句里的正常字符和错误字符就各自有了明确标签。二是token_labels通过chunk[-1]取区间最后一个标签这种取法在中文场景下基本不会出错因为一个 token 通常只覆盖一个汉字如果以后换成英文纠错数据一个 token 可能覆盖多个字符就需要改成“取最后一个非 O 的标签”或“取出现频率最高的标签”。我在加载时统一用paddingmax_length把输入打到定长训练脚本里就不用写动态 padding 的逻辑了显存稍微多花一点但换来的是代码简单不少。如果你的数据量大再改用collate_fn做动态 padding这里先保证能跑通。4.2 参数配置max_len、batch_size、学习率与类别权重参数推荐值理由max_len128SIGHAN 句子基本在百字以内128 可以覆盖绝大多数样本截断造成的标签损失很小batch_size16 或 32BERT 加一个线性分类头参数量不大显存允许就 32learning_rate2e-5 到 3e-5微调 BERT 的通用区间再大容易把预训练权重冲坏epochs3 到 5数据量只有几千句轮次太多必然过拟合类别权重O 设为 0.1S 设为 1.0O 标签占绝对多数不给权重模型会学成“全预测 O”这里的类别权重值得单独解释一下。中文纠错数据里正常字符永远是绝大多数一个句子最多错两三个字如果直接拿交叉熵训练模型只要全预测 O 就能拿到 95% 以上的准确率但 F1 会非常难看。所以损失函数里通常要对 O 类降权重让模型更重视非 O 类别的错误。PyTorch 的CrossEntropyLoss(weight...)可以直接传权重向量顺序要和LABEL2ID保持一致。训练时我还建议固定随机种子。数据本身不大模型初始化和数据 shuffle 的随机性对结果影响很明显同一条训练配置不同 seed 跑出的 F1 可能差两三个点。固定 seed 之后至少能做到任何一次改动都能在可控噪声下对比出真实效果。4.3 快速验收用验证集判断转换有没有跑偏跑第一个 epoch 之前先做一个低成本的验收加载原始 SIGHAN 验证集统计转换后 JSONL 里的错误类型分布和平均错误数。如果统计结果和已知的 SIGHAN 分布差异过大大概率是格式解析或转换逻辑出了问题。常见的异常信号包括替换类占比异常高说明位置解析把其他类型算错了、缺失和插入的比例完全为零说明这版数据格式里 D/I 的位置表达方式和你理解的不一样、平均错误数超过 2 个这不符合学生作文改错的普遍密度。from collections import Counter def statistic(jsonl_path): type_counter Counter() total_err 0 with open(jsonl_path, encodingutf-8) as f: for line in f: item json.loads(line) total_err len(item[errors]) type_counter.update(e[type] for e in item[errors]) print(type_counter, avg_err, total_err / sum(1 for _ in jsonl_path))这段统计脚本写得非常简单但每个字段都有用。type_counter让你一眼看到四类错误占比avg_err帮你判断转换后格式是否保留了完整的错误信息。等这一步确认没问题再花时间调模型参数才不浪费。5. 避坑SIGHAN与转换后格式里最容易翻车的五个细节5.1 字符位置差一永远先断言再进模型现象训练时 loss 下不去回看样本发现模型预测的错误位置普遍比真实位置偏右一位或者整体偏左一位。原因SIGHAN 位置从 1 开始很多转换代码忘了减一还有人把换行符、空字符也算进了长度。解决在数据加载函数最前面加一个断言等长句子里 S 类型必须满足src[post-1] ! tgt[post-1]有一处不满足直接抛异常。宁可让训练卡在数据加载也不要让错误标签悄悄溜进模型。5.2 插入与缺失类错误让序列标注直接对不齐现象转换后格式的labels长度和src长度不一致模型输出维度对不上报shape mismatch。原因缺失类错误在src里没有对应字符标签无处可挂插入类错误虽然src里有字符但目标句里没有对应位置简单按位置取tgt_char会取错。解决转换脚本里对 D 类标签做左右挂靠对 I 类标签直接把多余字符标成 I如果跑生成式模型则只需要保证src和tgt两个字段完整不需要强行对齐。这个挂靠规则要在 README 或注释里明确写出来否则后面换人接手时又是一轮翻车。5.3 标签与 BERT 的 tokenize 结果错位现象打印input_ids解码后得到的文本和原句一模一样但训练时正确率却很低。原因标签是按字符序列写的实际喂给模型的是 token 序列二者长度对不上又没有做offset_mapping对齐。解决加载数据时让 tokenizer 返回return_offsets_mappingTrue按偏移把字符标签映射到 token 空间特殊 token 和 padding 位置用IGNORE_INDEX屏蔽。别把“看起来一样”当成“数值对齐”打印中间张量验证是唯一的可靠方法。5.4 解压 zip 时 CRC 校验失败数据处理到一半才发现现象解压过程中报invalid zip archive、CRC check failed或者 zip 能解压但里面某几个文件为空文件。原因zip 从网络传输中断、磁盘空间不足、压缩包本身不完整。解决解压前先做完整校验拿一个 zip 包先用unzip -t跑一遍确认所有条目都通过 CRC 校验再解压解压后再用du -sh对比压缩包和解压目录的大小必要时直接看文件行数。这一步看起来浪费时间却能避免后续模型脚本在一个损坏文件上反复报错。unzip -t SIGHAN中文纠错数据集及转换后格式.zipunzip -t不带参数直接测试所有文件只要输出里出现OK之外的任何信息都说明包里文件有问题。很多项目的根因不是代码 bug而是数据文件本身缺了内容这类问题最隐蔽。5.5 繁体、简体与全角标点转换后的字符对不上现象转出来的src_char和tgt_char看着是同一个字但模型预测总是错或者评估时字符级 F1 忽高忽低。原因原始 SIGHAN 文本包含繁体字和全角标点部分转换代码做了繁简归一另一部分没做导致同一份数据里两种编码风格混着用。解决转换前明确全流程是否做繁简转换并在转换脚本里统一处理。做纠错模型的话我建议保留原字符不做繁简归一因为真实使用场景里用户输入什么就改什么但在计算字符级指标时要明确比较的是“原字符”还是“归一化后字符”两种口径的不同直接影响 F1。评估前先确认口径再确认代码。6. 进阶验证字符级F1这样算复现才有底气6.1 检测 F1 与修正 F1 分开算中文纠错评估里检测和修正是两个概念。检测只看“模型有没有发现这个字错了”修正还要看“发现之后有没有改对”。单独一个总 F1 无法体现模型是在哪一步崩的所以我会把两个指标拆开算分别对应计算机和人工排查问题时的不同线索。def detection_f1(pred_flags, gold_flags): tp fp fn 0 for p, g in zip(pred_flags, gold_flags): if g 1: if p 1: tp 1 else: fn 1 elif p 1: fp 1 prec tp / (tp fp 1e-9) rec tp / (tp fn 1e-9) f1 2 * prec * rec / (prec rec 1e-9) return prec, rec, f1pred_flags和gold_flags是等长度 0/1 列表每个位置表示该字符是否被判定为错误。这个脚本是字符级的所以位置对齐已经在你前期的offset_mapping里完成这里只做集合比较。修正 F1 则在pred_flags基础上再叠加一步只有当错误字符被替换成了正确字符才算正例否则即使识别出了错误也只算检测命中。我自己的习惯是每次跑完实验把检测 F1、修正 F1、各类错误的小类 F1 全部打印出来再和 SIGHAN 基准区间对照。如果检测 F1 明显低于 0.85先检查数据加载和标签对齐如果检测正常但修正偏低再回头查标签映射表或模型预测头。这套分层排查思路帮我避过不少玄学调参也建议你拿到这份转换后数据时按同样流程先建一条评估基线再把模型参数改起来。希望帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号