ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

cged_datasets实战:中文语法纠错数据解析与建模要点

cged_datasets实战:中文语法纠错数据解析与建模要点 简介历届中文句法错误诊断技术评测数据集整合了 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这个整合包把历年评测语料做成了统一格式方便研究者在同一套数据上横向比较。错误类型是这套数据的骨架。常见做法是把错误分成四类类型代码英文全称含义例子RRedundant冗余出现多余的字/词“我昨天晚上睡了很”里的“很”SMissing缺失少了必要成分“他天去学校”里的“每”WWord Selection Error用词不当“他做得很好”里“做得”与动词不匹配OWord 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 到 JSONCGED 的 XML 文件常见结构是以PARAGRAPH或DOC为根节点中间包含SENTENCE节点每个句子节点下挂零个或多个ERROR节点。ERROR节点的典型属性包括start_off错误起始字符偏移、end_off错误结束字符偏移开区间、typeR/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, encodingutf-8) as f: json.dump(parsed, f, ensure_asciiFalse, indent2) 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_off1、end_off3表示错误区间是第 1 和第 2 个字符。如果某个版本的文档把end_off定义为闭区间那么切片时就要做end1处理。解析之后打印一个句子检查是否与原文一致其实是个好习惯。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, keylambda 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] fB-{etype} for idx in range(start 1, end): labels[idx] fI-{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遍历所有测试句子预测每个四字词语中第二字是否有错。这个基线虽然故意做得很粗糙但可以在跑完整模型前验证评测脚本本身是否正确。如果连明显错误的规则都得到异常高的分数问题往往出在评测函数而不是模型。本文还有配套的精品资源点击获取
返回列表