简介:历届中文句法错误诊断技术评测数据集整合了 CGED2014 至 CGED2021 多年的官方评测数据,覆盖训练集、测试集与标准答案,适合自然语言处理研究者、语言学方向学生及算法工程师用于句法错误检测与纠正任务的算法开发、对比评测和论文复现。压缩包内共72个文件,大小53.28MB,以txt数据文本、sgml/xml标注文件、pdf说明文档为核心,同时包含readme、xlsx评分表与jar工具,可满足数据解析、格式转换和结果提交等完整流程。目前已有157人学习下载。借助这份资源,使用者能免去逐届搜集与清洗数据的繁琐,直接获得带标准标注的真实语料,还可参考附带的评测论文与官方说明,深入理解任务定义、标注规范和评测指标,为改进模型或开展课程设计提供扎实基础。
1. 中文句法改错任务里,为什么绕不开 cged_datasets
如果你做过中文作文批改、文本校对或面向二语学习者的语法纠错,多半会在评测环节撞见cged_datasets这个名字。它是历届中文句法错误诊断技术评测(Chinese Grammatical Error Diagnosis)沉淀下来的公开数据集,核心内容是一批带错误类型标注的中文学习者作文句子,以及对应的修正参考。跟通用语料不同,这个数据集的价值不在于“有多少亿 token”,而在于它的错误标注是按评测任务统一设计的:冗余、缺失、选词错误、语序错误,每一类都有明确的起止位置和修正文本。
我最早接触它是为了复现一篇语法纠错论文的 baseline。当时最直观的感受是:数据不难读,但坑都在格式和评测口径上,比如同一个句子在检测、定位、纠错三个任务里评分方式完全不同。这篇文章就把这个压缩包里的东西拆开讲清楚:目录结构长什么样、XML 标注怎么解析、训练模型时怎么把标注转成序列标签、最后用什么指标才算“对齐了历届评测”。
2. cged_datasets 的来历与标签体系:历届评测定下的四类错误
2.1 评测任务怎么定义,四类错误又是什么意思
中文句法错误诊断评测的目标,是给定一个可能包含错误的中文句子,要求系统输出错误的位置、类型和修正方式。这个任务在自然语言处理领域属于“文章纠错(Grammatical Error Correction)”的子集,但 CGED 特意强调“句法错误”,即关注句子层面而非篇章层面。历届评测由不同单位组织,数据来源多为在华留学生的汉语水平作文或类似场景的语料,cged_datasets这个整合包把历年评测语料做成了统一格式,方便研究者在同一套数据上横向比较。
错误类型是这套数据的骨架。常见做法是把错误分成四类:
| 类型代码 | 英文全称 | 含义 | 例子 |
|---|---|---|---|
| R | Redundant | 冗余,出现多余的字/词 | “我昨天晚上睡了很”里的“很” |
| S | Missing | 缺失,少了必要成分 | “他天去学校”里的“每” |
| W | Word Selection Error | 用词不当 | “他做得很好”里“做得”与动词不匹配 |
| O | Word Order Error | 语序错误 | “我昨天见了他”被写成“我见了昨天他” |
有的版本会额外分出“M”表示混用或不规范表达,但cged_datasets的主流整合版以 R/S/W/O 为基准。这个分类跟 CoNLL 系列评测里的错误类型划分并不一致,原因是中文自身缺乏词形变化,冗余和缺失的判断高度依赖上下文,标注者往往需要先读懂整句才能下判断。
2.2 标注格式与评测层级的由来
数据集中的每句话通常会附带多个候选修正,因为同一个语法错误可以由不同的人改成不同但都正确的句子。评测时不能只比对“是否和某个标准答案一模一样”,而要判断“系统输出的修正是否在候选修正集合里”。这个设计直接影响后续指标的选择,也因此 CGED 专门区分了三个评测层级:
- 检测级(Detection-level):只判断整句有没有错。
- 定位级(Identification/Position-level):判断错误出现在哪个字符区间,不看类型。
- 修正级(Correction-level):判断给出的修正文本是否与标准修正一致。
这三个层级对应不同的指标计算逻辑。我在复现论文时曾直接用“句子级准确率”当唯一指标,结果某个模型在检测级上表现尚可,但一旦把评估切换到定位级,得分立刻大幅下降,因为错误位置稍微偏移一个字符就算错。cged_datasets的评测脚本虽然没有统一收录在一个单独的目录里,但历届比赛留下的格式约定是:位置以字符 offset 计算,从 0 开始;修正文本以correction属性给出。这些约定在后面的解析脚本里都会用到。
3. 解压与解析:cged_datasets 的目录结构、XML 读取和转 JSON 方法
3.1 压缩包里的目录和文件,下载后先查什么
cged_datasets.zip解压后,常见布局是每届一个子目录,比如2016、2017、2018,也可能只有一个总目录。每个子目录下通常有训练集和测试集文件,训练集分简体中文和繁体中文版本,测试集对应历届比赛实际使用的版本。文件扩展名多为.xml,偶尔也有.txt或.csv。拿到压缩包后,我建议第一件事不是写代码,而是用find命令把目录结构完整列出来,确认每个 XML 文件的行数和大小,避免后面因为漏看文件而把训练集和验证集混在一起。
unzip cged_datasets.zip -d cged_all find cged_all -type f | sort | head -50上面命令先解压到cged_all目录,再用find列出所有文件并按文件名排序。这样做的意义是先建立对语料的整体认知:哪些是训练集、哪些是测试集、有没有额外的开发集。后续写解析脚本时,就可以把文件路径直接写进配置,而不是每次手动改。
3.2 XML 标注结构解析:从 ElementTree 到 JSON
CGED 的 XML 文件,常见结构是以<PARAGRAPH>或<DOC>为根节点,中间包含<SENTENCE>节点,每个句子节点下挂零个或多个<ERROR>节点。ERROR节点的典型属性包括:start_off(错误起始字符偏移)、end_off(错误结束字符偏移,开区间)、type(R/S/W/O)、correction(修正文本)。有的版本还包含id或level属性,其中level表示错误的严重程度,但这个字段并非所有历届数据都有。下面这个脚本可以把 XML 转成 JSON,同时保留句子文本和全部错误标注:
import xml.etree.ElementTree as ET import json def parse_cged_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() # 不同版本可能使用不同的根节点名,这里统一处理 sentences = [] for sent in root.iter('SENTENCE'): text = sent.find('TEXT').text if sent.find('TEXT') is not None else '' errors = [] for err in sent.iter('ERROR'): if 'start_off' in err.attrib and 'end_off' in err.attrib: errors.append({ 'start': int(err.attrib['start_off']), 'end': int(err.attrib['end_off']), 'type': err.attrib.get('type', ''), 'correction': err.attrib.get('correction', '') }) sentences.append({'text': text, 'errors': errors}) return sentences if __name__ == '__main__': parsed = parse_cged_xml('cged_all/TrainingData/2016_Training_Set.xml') with open('cged_2016.json', 'w', encoding='utf-8') as f: json.dump(parsed, f, ensure_ascii=False, indent=2) print(f'共解析 {len(parsed)} 个句子,其中 {sum(1 for s in parsed if s["errors"])} 个含有错误')这段脚本的逻辑不复杂:先用root.iter('SENTENCE')找到所有句子节点,再在句子内部遍历ERROR子节点,把偏移量转成整数,把修正文本作为字符串保存。root.iter()比findall()更稳妥,因为不同历届数据的 XML 嵌套深度可能不一样,直接findall('.//SENTENCE')在深层文档中容易漏项。
参数上有一点需要注意:end_off在绝大多数版本里是“开区间”的语义,即它指向错误结束后的第一个字符,比如“我天天学习”中“天天”写错时,若start_off=1、end_off=3,表示错误区间是第 1 和第 2 个字符。如果某个版本的文档把end_off定义为闭区间,那么切片时就要做end+1处理。解析之后打印一个句子检查是否与原文一致,其实是个好习惯。
4. 把 cged_datasets 喂给模型:序列标注数据管线与评测指标
4.1 从句子和错误偏移构造 BIO 标签
多数中文纠错模型把任务建模成序列到序列,或者序列标注。序列标注更直观,也更接近“定位错误”这个目标。以 R/S/W/O 四类错误为例,常见做法是把错误类型和位置组合成标签集合:B-R、I-R、B-S、I-S、B-W、I-W、B-O、I-O,再加上O表示非错误。实际操作时,先按字符级把句子切开,初始化所有字符的标签为O,然后根据start_off和end_off逐段设置标签,区间内第一个字符标B-类型,后续字符标I-类型。还要注意,一个句子可能出现多个不重叠错误,遍历时要按起始位置排序,避免两个错误区间互相覆盖导致标签异常。
def make_bio_labels(text, errors): labels = ['O'] * len(text) errors = sorted(errors, key=lambda e: e['start']) for err in errors: start, end = err['start'], err['end'] etype = err['type'] if start < 0 or end > len(text) or start >= end: continue # 跳过越界的标注,防止标签序列长度错位 labels[start] = f'B-{etype}' for idx in range(start + 1, end): labels[idx] = f'I-{etype}' return labels ex_text = "我去了北平玩" ex_errors = [{'start': 3, 'end': 5, 'type': 'W', 'correction': '北京'}] print(make_bio_labels(ex_text, ex_errors)) # ['O', 'O', 'O', 'B-W', 'I-W', 'O']这段代码的关键在B-和I-的分配逻辑:区间第一个字符始终标记B-,后面的标记I-,这与常见命名实体识别任务的标签约定一致。若有多个错误紧挨着,比如前一个错误结束于索引 3,后一个错误开始于索引 3,此时索引 3 会被前一个错误的I-覆盖,所以必须对区间做冲突检测,这个校验逻辑放在errors排序之后做最稳。
4.2 评测指标:F1、准确率之外,还要看位置偏移的代价
历届 CGED 评测大多以句子为单位计算,但具体指标在不同年份有差异。检测级只看句子有无错误,所以可以用二分类的精确率、召回率、F1;定位级则要把预测的错误区间和标准错误区间做匹配,匹配成功才计为一次正确;修正级要求预测的修正文本与标准修正文本之一完全相同。整合版本里,由于不同届的评分脚本并不统一,我一般自己实现一个兼容多级的评估器,核心是下面这个定位级 F1 的计算思路:
def evaluation_identification(gold_errors, pred_errors): gold_set = set((e['start'], e['end']) for e in gold_errors) pred_set = set((p['start'], p['end']) for p in pred_errors) tp = len(gold_set & pred_set) fp = len(pred_set - gold_set) fn = len(gold_set - pred_set) precision = tp / (tp + fp) if tp + fp > 0 else 0.0 recall = tp / (tp + fn) if tp + fn > 0 else 0.0 f1 = 2 * precision * recall / (precision + recall) if precision + recall > 0 else 0.0 return {'precision': round(precision, 4), 'recall': round(recall, 4), 'f1': round(f1, 4)}这个函数使用集合运算完成匹配,前提是预测和标准都用字符偏移量表示错误位置。它的优势是逻辑直观,适合在实验初期快速评估模型质量;缺点是只允许完全匹配,预测区间比标准区间少一个字符或多一个字符都会被判为 FP 和 FN,导致分数偏低。实际训练时,我通常会额外记录“部分匹配”的数量,用来判断模型产生的偏移是系统性偏左还是偏右。
训练环节的另一个常见做法是用 BERT 类模型直接做序列标注:输入句子字符序列,输出每个字的标签概率分布,损失函数使用交叉熵。这里要注意类别不平衡问题,因为非错误字符的占比极高,模型很容易把所有字符都预测为O,导致定位级 F1 为 0。应对手段有两个:一是给B-和I-标签设置较高的损失权重,二是在训练时对不含错误的句子做降采样。两者可以组合使用,效果会比单纯调学习率更明显。
5. 验证 cged_datasets 解析结果的三个坑位与离线自查技巧
5.1 坑一:偏移量到底是字符还是字节,解析后必须对齐查看
CGED 数据的偏移量以字符数为单位,这在 Python 中对应len(text)的结果。但如果你用len(text.encode('utf-8'))去换算,就会得到完全不同的长度,造成标签序列与字符序列错位。验证方法很简单:写一个函数,用start_off和end_off从原文切片,将切出来的子串与correction做目视对比,查看每个错误子串的文本;如果切片结果明显不是完整词语,大概率是偏移量单位理解错了。
def show_error_spans(text, errors): for err in errors: span = text[err['start']:err['end']] print(f"错误片段: {span} -> 修正: {err['correction']}") show_error_spans("他天去学校", [{'start': 1, 'end': 2, 'type': 'S', 'correction': '每'}])输出结果是错误片段: 天 -> 修正: 每,这里“天”缺少前一个字符“每”,所以类型为 S。这个可视化函数虽然简单,却能快速暴露跨句子级别的解析错误,比如把上一句的末尾偏移用到下一句开头。
5.2 坑二:XML 里存在无错误句,直接把空标注句全丢会改变数据分布
有的处理脚本会在解析时只保留含错误标注的句子,理由是训练纠错模型只需要正样本。但实际上cged_datasets的测试集包含大量无错误句子,检测级评测恰恰需要模型具备“判断句子是否正确”的能力。正确做法是把无错误句保留下来,并在训练时按一定比例用于负样本,这个比例建议设置在 0.3 到 0.5 之间,因为如果太高,模型会倾向预测所有句子都有错,伤到精确率;太低则召回率不足。
5.3 坑三:测试集评测脚本要和训练数据同版本的错误类型定义对齐
不同届训练集里,某种错误的标注方式可能前后不一致,比如同一处介词误用,在 2016 年版本被标为 W,在 2017 年版本可能被拆成 S。若你直接把 2017 的数据和 2016 的数据拼在一起训练,标签冲突会明显降低模型的表现。离线自查的方法是:统计每种错误类型的数量分布,再做一次随机抽样,人工抽查 50 条类型标注是否符合直觉。如果发现大量“疑似同错不同标”的情况,可以按correction文本聚类后统一修订标签,或者干脆只在同一届数据上训练和评测。
验证最后一个技巧时,我通常写一个极小的规则分类器当作 baseline:遍历所有测试句子,预测每个四字词语中第二字是否有错。这个基线虽然故意做得很粗糙,但可以在跑完整模型前验证评测脚本本身是否正确。如果连明显错误的规则都得到异常高的分数,问题往往出在评测函数,而不是模型。
本文还有配套的精品资源,点击获取