简介SIGHAN中文纠错数据集及转换后格式.zip 面向中文自然语言处理研究者、拼写检查与语法纠错方向的开发者及学生提供汉语语法错误检测与拼音标注的权威语料。原始数据源自新加坡国立大学团队涵盖错别字、词序错误、词语搭配不当等多种人工标注错误类型适合训练与评测中文纠错算法。压缩包共78个文件约19.92MB以txt文本为主辅以sgml标注文件、readme说明、jar工具、pdf论文、xlsx表格及py脚本兼顾原始语料与转换后格式便于直接接入模型训练流程。资源内含SIGHAN 7/8及CLP14等版本并附配对数据生成脚本与简繁转换模块可帮助读者快速完成数据预处理、错误标注、训练验证测试集划分及CoNLL等格式转换。目前已有378人学习下载适合需要系统掌握中文纠错语料构建与评估流程的中高级开发者参考。1. SIGHAN中文纠错数据集从zip包到可训练格式中间隔着多少坑做中文文本纠错CSC的人绕不开 SIGHAN 这三个字。它不是一个数据集而是从 2013 年到 2015 年连续三届中文拼写纠错评测Chinese Spelling Check沉淀下来的一整套标注语料覆盖简繁体、母语者与非母语者写作场景。你手上拿到的SIGHAN中文纠错数据集及转换后格式.zip本质就是把这些原始评测文件连同已经转好的训练格式打包在一起——省掉的是你自己从零解析原始标注、对齐字级标签、再拼成 seq2seq 或序列标注输入的那几个小时。但“省掉”不等于“没有”。我见过太多人解压完直接pd.read_csv结果训练时 loss 不降、验证集 F1 卡在 0.1 上下回头才发现原始 SIGHAN 的标注是“句级 位置 替换字”三元组而转换后的格式如果没对齐字符偏移标签整体错位。这篇笔记就按“这个 zip 里到底是什么 → 怎么把它变成能喂给模型的张量 → 转换和对齐环节最容易翻车的地方”这条线走一遍适合已经跑过 BERT 或 Seq2Seq 微调、但第一次碰中文纠错数据的人。2. 拆开zip先看清SIGHAN原始标注与转换后格式的对应关系2.1 原始SIGHAN三件套source、target、位置标注SIGHAN 每一届的语料组织方式略有差异但核心结构一致一个*.sgml或*.txt存原始句子一个存纠错后的句子还有一个存错误位置和类型。以 2014 年简体中文评测为例典型文件是SIGHAN14_Train.sgml、SIGHAN14_Train.lst和SIGHAN14_Train.correct。.lst里每行格式是sid start_offset, end_offset这里的 offset 是字符级的不是字节级。很多人用 Python 读文件时默认按字节处理或者用len()去算中文长度结果偏移量对不上。正确做法是先把整句读成str再用切片取错误片段。.correct文件里每行是sid correct_sentence注意.correct给的是整句纠正后结果不是只给替换字。所以如果你要做字级序列标注每个字标 0/1需要自己用原始句和纠正句做 diff再结合.lst的位置做校验。2.2 转换后格式通常长什么样常见的转换后格式有两种一种是parallel 格式src\ttgt每行一对句子直接用于 Seq2Seq 训练另一种是char-level BIO 格式每行是字\t标签用于 BERT CRF 或 Token Classification。你拿到的 zip 里如果包含train.json、dev.json、test.json大概率是{src: ..., tgt: ...}或{text: ..., labels: [...]}结构。这里有一个容易忽略的点SIGHAN 原始数据里有些句子没有错误。转换时如果直接把所有句子都塞进训练集负样本比例会偏高模型倾向于全预测“无错”。我一般会在转换脚本里加一个过滤只保留至少有一个错误位置的句子作为正样本同时从无错句里采样等量作为负样本比例控制在 1:1 到 1:2 之间。2.3 用Python快速验证zip内文件结构拿到 zip 先别急着解压到项目根目录用zipfile列一下清单确认文件数和目录层级import zipfile with zipfile.ZipFile(SIGHAN中文纠错数据集及转换后格式.zip, r) as z: for info in z.infolist(): # 只打印文件名和大小避免解压后才发现嵌套了三层 print(f{info.filename:60s} {info.file_size:10d} bytes)逻辑说明infolist()返回每个成员的元数据file_size是压缩前大小。如果看到__MACOSX/开头的条目那是 macOS 打包时自动生成的资源分叉文件解压后可以直接删不影响数据。参数上filename可能包含中文Windows 下用cp936编码解压容易乱码建议统一用utf-8或gbk显式指定。3. 把SIGHAN转成序列标注格式对齐、标签、切分三步走3.1 字符偏移对齐为什么你的标签总是错一位原始.lst里的 offset 是从 0 开始还是从 1 开始不同年份的 SIGHAN 不一样。2013 年是从 1 开始2014 和 2015 年是从 0 开始。如果你不确认这一点直接拿 offset 去切片就会整体偏移一位。验证方法很简单取一个已知错误句用 offset 切出来的片段应该正好是错字。下面是一个对齐并生成字级标签的脚本def align_labels(src_sentence, correct_sentence, error_spans): src_sentence: 原始句子 str correct_sentence: 纠正后句子 str error_spans: list of (start, end) 字符级偏移 返回: list of (char, label) label1 表示该字错误 labels [0] * len(src_sentence) for start, end in error_spans: # 注意 end 通常是 exclusive即 [start, end) for i in range(start, min(end, len(src_sentence))): labels[i] 1 # 用纠正句做一次校验如果 src 和 correct 在非错误位置不一致说明对齐有问题 for i, (c_src, c_tgt) in enumerate(zip(src_sentence, correct_sentence)): if c_src ! c_tgt and labels[i] 0: print(f警告位置 {i} 字符不一致但未标记错误: {c_src} - {c_tgt}) return list(zip(src_sentence, labels))逻辑说明先按 error_spans 打标签再用纠正句做交叉验证。如果出现“字符不一致但标签为 0”说明 offset 解析有误或句子长度不匹配。参数上end是否包含在切片内取决于原始标注习惯SIGHAN 一般用 exclusive所以range(start, end)是对的。3.2 从parallel格式到BIO用diff自动生成标签如果你只有src\ttgt平行句对没有原始 offset可以用difflib.SequenceMatcher自动 diffimport difflib def parallel_to_bio(src, tgt): 将平行句对转为字级标签1错误0正确 matcher difflib.SequenceMatcher(None, src, tgt) labels [0] * len(src) for tag, i1, i2, j1, j2 in matcher.get_opcodes(): if tag ! equal: # 替换或删除src 中 [i1, i2) 都是错误位置 for i in range(i1, i2): labels[i] 1 return list(zip(src, labels))逻辑说明get_opcodes()返回(tag, i1, i2, j1, j2)tag 为replace、delete、insert、equal。对于inserttgt 多出字符src 没有对应位置通常忽略或标记为 0。参数上SequenceMatcher的autojunk默认 True对长句可能误判建议设为False。3.3 训练/验证/测试切分别用随机切分SIGHAN 官方已经给了 train/dev/test 划分但很多人转换后直接train_test_split(random_state42)导致同一来源的句子同时出现在训练和验证集指标虚高。正确做法是保留官方划分如果官方没给 dev就从 train 里按文档级切分而不是句子级。具体来说如果原始数据有文档 ID按文档 ID 分组后再切。from sklearn.model_selection import GroupShuffleSplit # 假设 df 有 doc_id 列 gss GroupShuffleSplit(n_splits1, test_size0.1, random_state42) train_idx, dev_idx next(gss.split(df, groupsdf[doc_id]))逻辑说明GroupShuffleSplit保证同一 doc_id 的句子只出现在一边。参数上test_size0.1是常见比例如果数据量小于 5000 句可以调到 0.15 保证验证集有足够错误样本。4. 转换后格式的坑编码、空行、简繁混用与标签泄漏4.1 编码问题GBK与UTF-8混用导致读入乱码SIGHAN 原始文件有的是 GBK有的是 UTF-8甚至同一届里不同文件编码不同。如果你用open(path, r)不指定 encodingWindows 下默认 GBKLinux 下默认 UTF-8跨平台跑脚本就会翻车。我一般统一用open(path, r, encodingutf-8, errorsreplace)遇到乱码字符先记下来再单独用gbk重读那个文件。def read_sighan_file(path): for enc in [utf-8, gbk, gb18030]: try: with open(path, r, encodingenc) as f: return f.readlines() except UnicodeDecodeError: continue raise ValueError(f无法解码文件: {path})逻辑说明按 utf-8 → gbk → gb18030 顺序尝试gb18030 是 gbk 的超集覆盖更多生僻字。参数上errorsreplace会丢失信息所以这里不用而是靠异常切换编码。4.2 空行与空白字符一个空格让标签整体偏移原始文件里经常有空行、行尾空格、全角空格。如果你用line.split(\t)后不 stripsrc末尾多一个空格和tgt长度不一致diff 就会把空格标成错误。处理方式是在读入后统一strip()但注意句首句尾的空格可能是标注的一部分strip 前先确认原始数据是否用空格表示缺失字。4.3 简繁混用SIGHAN 2013 的隐藏陷阱SIGHAN 2013 同时包含简体和繁体如果你只做简体纠错需要过滤掉繁体句子。简单方法是统计句子中繁体字符比例超过阈值就丢弃。但更稳妥的是用opencc做一次转换再和原始句对比如果转换后变化很大说明是繁体句。from opencc import OpenCC cc OpenCC(t2s) # 繁体转简体 def is_traditional(sentence, threshold0.3): converted cc.convert(sentence) diff sum(1 for a, b in zip(sentence, converted) if a ! b) return diff / max(len(sentence), 1) threshold逻辑说明t2s是繁转简配置如果转换后大量字符变化说明原句是繁体。参数上threshold0.3是经验值低于这个值可能是简繁混用或个别异体字。4.4 标签泄漏验证集里混入了训练集的纠正句如果你用平行句对做 Seq2Seqtgt是纠正后句子。切分时如果按src切分但tgt和另一边的src重复就会泄漏。检查方法把训练集的所有tgt和验证集的所有src做交集如果非空说明有泄漏。解决方式是按句对整体切分或者用src的哈希做分组。5. 避坑与排查SIGHAN转换中最容易翻车的5个地方5.1 现象训练 loss 不降验证 F1 始终为 0原因标签全为 0或者标签和输入长度不一致导致 padding 后错位。常见于用tokenizer编码后没有对齐labelstokenizer会把一个中文字拆成多个 subword而你的标签还是字级。解决用tokenizer的word_ids()方法把字级标签映射到 subword 级只保留第一个 subword 的标签其余设为 -100忽略。tokenized tokenizer(src, return_tensorspt, is_split_into_wordsTrue) word_ids tokenized.word_ids() labels [] for wid in word_ids: if wid is None: labels.append(-100) else: labels.append(char_labels[wid])5.2 现象解压后文件名乱码读不到文件原因zip 打包时用了 GBK 编码文件名解压时用 UTF-8 解码。解决用zipfile手动指定编码解压with zipfile.ZipFile(zip_path) as z: for info in z.infolist(): info.filename info.filename.encode(cp437).decode(gbk) z.extract(info, extract_dir)5.3 现象offset 切片出来的不是错字原因offset 从 1 开始或者 offset 是字节偏移而非字符偏移。解决先确认年份2013 年 offset 从 1 开始减 1 再用。如果是字节偏移先sentence.encode(utf-8)再切片然后decode。5.4 现象模型把所有字都预测为正确原因负样本比例过高或者正样本标签没有正确传播。解决检查正负样本比例控制在 1:1 到 1:2。如果正样本太少可以用WeightedRandomSampler过采样。5.5 现象验证集指标远高于测试集原因验证集和测试集同源或者验证集太小。解决确保验证集和测试集来自不同文档或不同年份。如果数据量允许验证集至少 500 句其中错误句不少于 200 句。6. 进阶技巧用SIGHAN做数据增强与跨域验证SIGHAN 数据量不大训练集通常几千句直接微调 BERT 容易过拟合。我一般会做两件事一是用同音字/形近字替换做增强二是用跨年份验证检验泛化。同音字替换的思路是从混淆集里随机选一个和原字同音或形近的字替换后作为新的错误句标签标 1。混淆集可以用pypinyin生成同音字形近字可以用zhon或手工整理。注意替换后要保证句子仍然通顺否则模型学到的是噪声。我一般只替换名词和动词不替换虚词。from pypinyin import pinyin, Style def get_homophones(char): 返回同音字列表需要预先构建拼音到字的映射 py pinyin(char, styleStyle.NORMAL)[0][0] return pinyin_dict.get(py, []) def augment_with_homophone(sentence, labels, prob0.1): 随机替换非错误位置的字为同音字生成新样本 new_sentence [] new_labels [] for ch, lab in zip(sentence, labels): if lab 0 and random.random() prob: homophones get_homophones(ch) if homophones: new_sentence.append(random.choice(homophones)) new_labels.append(1) continue new_sentence.append(ch) new_labels.append(lab) return .join(new_sentence), new_labels逻辑说明只替换原本正确的字替换后标为错误。参数上prob0.1控制增强强度太高会破坏语义。pinyin_dict需要自己从pypinyin的词典构建或者用现成的同音字表。跨年份验证更简单用 2013 年训练2014 年验证2015 年测试。如果指标下降超过 10 个点说明模型对年份风格过拟合需要加入年份无关的特征或做领域自适应。我自己的习惯是每次转换完数据先跑一个 baseline记录三个年份的 F1再决定要不要做增强。这个习惯帮我省了很多后悔药——有一次发现 2015 年测试集 F1 只有 0.3回头查才发现 2015 年数据里繁体句没过滤干净。希望帮到你。本文还有配套的精品资源点击获取