SIGHAN中文纠错数据集转换实战:从SGML到平行句对与字符级标注
简介SIGHAN中文纠错数据集及转换后格式.zip 面向中文自然语言处理研究者、拼写检查与语法纠错方向的开发者及学生提供汉语语法错误检测与拼音标注的权威语料可用于训练和评估中文纠错算法。压缩包共78个文件约19.92MB以txt文本、sgml标注文件为主另含zip原始发布包、readme说明、jar工具、pdf论文、xlsx表格、py脚本与md文档覆盖原始语料、配对数据及简繁转换等模块。已有378人学习下载。资源包含SIGHAN原始版本及转换后格式涉及错别字、词序、搭配不当等多类错误标注并附数据预处理、错误标注、训练验证测试集划分、CoNLL等格式转换与纠正标签生成的完整流程便于读者直接用于模型训练、性能对比与二次定制是推进中文纠错系统研发的实用素材。1. 拿到 SIGHAN 中文纠错数据集压缩包之后先别急着解压你从各种渠道拿到一个叫SIGHAN中文纠错数据集及转换后格式.zip的压缩包双击解压里面大概率是一堆按年份分的文件夹夹杂着.sgml、.txt、.char、.bmes之类的文件还有几个看起来像转换脚本的东西。很多人第一反应是「先跑起来再说」结果卡在编码、对齐、格式转换上白白耗掉一整天。这个标题背后真正要解决的问题是SIGHAN 系列中文纠错数据集主要来自 CGED、SIGHAN 拼写检查评测原始格式不统一需要转成模型能直接吃的平行句对格式才能喂给序列标注或 seq2seq 纠错模型。适合谁做中文文本纠错、拼写检查、ASR 后处理、OCR 后纠错的工程师和研究生。这篇笔记就按「压缩包里有什么 → 怎么转 → 怎么验证 → 坑在哪」的顺序把这条链路讲透让你拿到包当天就能跑通第一版训练数据。2. SIGHAN 中文纠错数据集的目录结构与格式差异先认清手里是什么2.1 原始 SIGHAN 格式长什么样SIGHAN 中文纠错数据最早来自 2013-2015 年的中文拼写检查评测后来 CGEDChinese Grammatical Error Diagnosis又扩展了语法纠错任务。原始文件通常是 SGML 风格的标记文本每篇文档用DOC包裹句子用SENT标记错误位置用ERROR标注。一个典型的片段长这样DOC SENT id1 我们今天ERROR id1 typemissing去/ERROR公园玩。 /SENT /DOC这里ERROR标签的type属性告诉你错误类型missing、replace、多余等标签内的文字是「正确内容」还是「错误内容」取决于具体年份和任务定义这一点是后面转换时最容易翻车的地方。2013 年的数据里ERROR标签内放的是正确词而 2014 年部分文件放的是错误词必须逐份核对 README 或论文说明不能一刀切。2.2 转换后格式通常指什么所谓「转换后格式」在中文纠错圈子里一般指两种格式类型文件形态典型用途平行句对每行错误句\t正确句seq2seq 纠错模型训练字符级标注每行字\t标签标签为 B/M/E/S 或 0/1BiLSTM-CRF、BERT 序列标注平行句对格式最通用也最容易从 SGML 转出来。字符级标注需要额外做对齐难度高一个量级。压缩包里如果同时给了这两种优先用平行句对跑通 baseline再考虑标注格式。2.3 解压前先确认的三件事第一文件编码。SIGHAN 原始数据多为 GB2312 或 GBK直接当 UTF-8 读会乱码。第二压缩包是否嵌套。有些包解压后还有一个同名 zip需要二次解压。第三是否有 README 或 LICENSE。没有 README 的数据集转换规则只能靠试试错成本很高。# 先看压缩包内文件列表不解压 unzip -l SIGHAN中文纠错数据集及转换后格式.zip # 解压到指定目录避免中文路径问题 unzip -O GBK SIGHAN中文纠错数据集及转换后格式.zip -d sighan_raw # 查看文件编码file 命令只能猜个大概 file -i sighan_raw/*/*.sgmlunzip -O GBK指定用 GBK 解码文件名避免解压后文件名乱码。file -i输出charsetiso-8859-1或unknown-8bit时基本可以确定不是 UTF-8需要用iconv转码后再处理。3. 把 SGML 转成平行句对解析脚本与四个关键参数3.1 解析 SGML 的核心逻辑不要用正则硬怼整个文件SGML 嵌套标签用正则容易漏。稳妥做法是按行读遇到SENT开始收集遇到/SENT结束中间用状态机处理ERROR标签。下面是一个最小可用的 Python 解析器import re def parse_sgml_line(line): 解析单行 SENT 内容返回 (错误句, 正确句) # 提取 ERROR 标签及其属性 error_pattern re.compile(rERROR[^]*(.*?)/ERROR) # 错误句去掉所有标签保留标签内文字 wrong re.sub(r[^], , line) # 正确句把 ERROR 标签替换为其内部文字再去掉其他标签 correct error_pattern.sub(r\1, line) correct re.sub(r[^], , correct) return wrong.strip(), correct.strip() def convert_sgml_to_parallel(input_path, output_path, encodinggbk): with open(input_path, r, encodingencoding) as fin, \ open(output_path, w, encodingutf-8) as fout: buffer [] in_sent False for line in fin: line line.strip() if SENT in line: in_sent True buffer [line] elif /SENT in line: buffer.append(line) wrong, correct parse_sgml_line( .join(buffer)) if wrong and correct: fout.write(f{wrong}\t{correct}\n) in_sent False elif in_sent: buffer.append(line)这段代码的关键点有三个encodinggbk应对原始编码error_pattern.sub(r\1, line)把错误标签替换成正确内容输出统一用 UTF-8 和\t分隔方便后续pandas.read_csv(sep\t)直接读。3.2 四个必须确认的参数参数常见取值影响输入编码gbk / gb2312 / utf-8读错编码直接乱码后续全废ERROR 语义标签内是正确词 / 错误词决定 wrong 和 correct 谁替换谁分隔符\t/|||/ 空格影响下游读取建议统一\t空行处理跳过 / 保留保留空行会让 DataLoader 报错ERROR 语义这一项最可靠的办法是拿一条已知句子人工核对。比如原句「我今天去公园玩」如果 SGML 里写成我ERROR今天/ERROR去公园玩而正确句应该是「我今天去公园玩」那标签内就是正确内容替换逻辑用sub(r\1)没问题。反过来如果标签内是错误内容就得用「去掉标签内文字」的逻辑。3.3 批量转换与目录遍历实际数据往往按年份和训练/测试集分目录写一个批量脚本比手动一个个转靠谱import os from pathlib import Path def batch_convert(root_dir, out_dir): os.makedirs(out_dir, exist_okTrue) for sgml_file in Path(root_dir).rglob(*.sgml): # 用相对路径生成输出文件名避免重名 rel sgml_file.relative_to(root_dir) out_name str(rel).replace(/, _).replace(.sgml, .tsv) out_path os.path.join(out_dir, out_name) try: convert_sgml_to_parallel(str(sgml_file), out_path) print(fOK: {sgml_file} - {out_path}) except Exception as e: print(fFAIL: {sgml_file}, reason: {e}) batch_convert(sighan_raw, sighan_parallel)rglob(*.sgml)递归找所有 SGML 文件relative_to保留目录层级信息并压平到文件名里避免不同年份的同名文件互相覆盖。转换失败的不要静默跳过打印出来单独处理通常是编码或标签不完整导致的。4. 从平行句对到字符级标注对齐逻辑与标签体系选择4.1 为什么需要字符级标注平行句对适合 seq2seq 模型但如果你用的是 BERT CRF 做序列标注输入是单句输出是每个字的标签。这时候需要把「错误句 → 正确句」的映射拆成每个字的 0/1 标签。难点在于错误句和正确句长度可能不一致比如漏字、多字直接按位置对齐会错位。4.2 基于编辑距离的对齐方案最稳的做法是用difflib.SequenceMatcher或编辑距离算法找出错误句到正确句的最小编辑操作序列再据此打标签import difflib def align_to_char_labels(wrong, correct): 返回 (chars, labels)labels 为 0/1 列表 sm difflib.SequenceMatcher(None, wrong, correct) labels [0] * len(wrong) for tag, i1, i2, j1, j2 in sm.get_opcodes(): if tag equal: continue elif tag replace: # 替换错误句对应位置标 1 for i in range(i1, i2): labels[i] 1 elif tag delete: # 错误句多字标 1 for i in range(i1, i2): labels[i] 1 elif tag insert: # 错误句漏字在 i1 位置标 1或前一个字标 1 if i1 len(labels): labels[i1] 1 return list(wrong), labelsget_opcodes()返回五种操作equal、replace、delete、insert。replace 和 delete 直接标 1insert 表示错误句漏了字通常把插入点前一个字标 1或者单独定义一个「缺失」标签。如果下游模型只做二分类统一标 1 即可。4.3 标签体系的选择标签体系标签数适用模型备注0/1 二分类2BERT 线性层最简单漏字检测弱B/M/E/S4BiLSTM-CRF能表示错误片段边界错误类型多分类5BERT CRF需要 SGML 里的 type 属性新手建议从 0/1 二分类起步跑通全流程后再升级到 B/M/E/S。多分类标签需要原始 SGML 保留type属性很多转换后的包已经丢掉了这个信息想用也用不了。5. 避坑与排查SIGHAN 转换中最容易翻车的五个点5.1 解压后文件名乱码脚本读不到文件现象ls看到一堆????.sgmlPythonglob匹配不到。原因zip 内文件名用 GBK 编码系统按 UTF-8 解码失败。解决用unzip -O GBK重新解压或在 Python 里用os.fsdecode配合surrogateescape处理。更省事的办法是在 Windows 上用 7-Zip 指定代码页解压再拷到 Linux。5.2 转换后平行句对里错误句和正确句一模一样现象打开 TSV 发现两列完全相同模型学不到任何东西。原因ERROR 标签语义判断反了或者正则sub替换时把正确内容又替换回去了。解决拿一条含错误的句子人工核对确认wrong列确实包含错误、correct列已修正。如果原始数据里ERROR标签内是错误词替换逻辑要改成「删除标签内文字」而不是「保留」。5.3 编码转换后出现「锟斤拷」或问号现象GBK 转 UTF-8 后部分生僻字变成?。原因原始数据里混了 GB2312 不支持的字符或者文件本身是 UTF-8 但被当 GBK 读。解决先用chardet检测编码再决定用哪种解码。iconv -f GBK -t UTF-8时加//IGNORE会丢字符不如在 Python 里用errorsreplace并记录替换位置后续人工抽查。5.4 字符级标注长度和原句对不上现象labels长度比wrong短或长。原因difflib处理 insert 操作时索引越界或者原句包含空格、标点被 strip 掉了。解决对齐前不要 strip 中间空格只 strip 首尾insert 分支加if i1 len(labels)保护。转换完统一断言len(chars) len(labels)不满足的样本直接丢弃并计数。5.5 训练时 loss 不降模型输出和输入一样现象训练几个 epoch模型学会「复制输入」纠错率为零。原因平行句对里正确句和错误句差异太小或者数据里负样本无错误句占比过高。解决统计错误句和正确句的编辑距离分布过滤掉完全相同的句对对无错误样本降采样让正负样本比例控制在 1:1 到 1:3 之间。这个坑很玄学但十有八九是数据分布问题不是模型问题。6. 验证转换质量与进阶用法三个可落地的检查习惯转换完不要直接开训先做三件事。第一随机抽 20 条平行句对人工读一遍确认错误句确实有错、正确句确实改对了。第二统计句对数量、平均长度、编辑距离分布和论文里报告的数据规模对比差太多说明漏文件了。第三用一个小模型比如bert-base-chinese加线性层跑 1 个 epoch看 loss 是否从 0.6 左右开始下降如果一直卡在 0.69 附近基本是标签或数据格式有问题。进阶用法上如果你要做的不只是拼写纠错而是语法纠错SGML 里的type属性值得保留。可以在转换时额外输出一列错误类型后续做多任务学习。另外SIGHAN 数据年份跨度大2013 和 2015 的标注规范有差异混在一起训练时建议加年份特征或分年份评估否则模型在某一年的测试集上会莫名翻车。# 快速检查平行句对质量 import pandas as pd df pd.read_csv(sighan_parallel/all.tsv, sep\t, headerNone, names[wrong, correct]) df[same] df[wrong] df[correct] print(f总句对: {len(df)}) print(f完全相同: {df[same].sum()} ({df[same].mean():.2%})) print(f平均错误句长度: {df[wrong].str.len().mean():.1f}) # 过滤掉完全相同的句对 df_clean df[~df[same]].drop(columns[same]) df_clean.to_csv(sighan_parallel/clean.tsv, sep\t, indexFalse, headerFalse)这段检查脚本我每次转换完都会跑一遍完全相同比例超过 30% 就说明转换逻辑有问题别急着往下走。df_clean存成无表头的 TSV方便直接喂给 HuggingFacedatasets或自定义 DataLoader。最后说个血泪经验SIGHAN 数据集的坑不在模型在数据本身。我见过太多人花三天调模型最后发现是解压时编码错了。所以拿到压缩包先花半小时把编码、标签语义、句对数量这三件事确认清楚比后面调参省事得多。希望帮到你。本文还有配套的精品资源点击获取