ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于篇章结构的K12作文自动评分系统:从count文件到可解释评分报告

基于篇章结构的K12作文自动评分系统:从count文件到可解释评分报告 简介这份资源是面向K-12教育场景的Python自动作文评分系统实现包适合NLP入门学习者、教育技术开发者及需要批量评分的教师参考。系统围绕篇章结构展开涵盖词法分析、句法分析、语义理解、段落连贯性与主题发展识别并引入SVM、决策树、随机森林及神经网络等模型学习人工评分模式同时包含特征工程、文本预处理与后处理、交叉验证评估及用户界面设计等完整流程。压缩包共112个文件约2.04MB以py脚本、txt文本、count统计文件、png图表、xml配置和xlsx数据为主另有训练与结果类文件便于复现实验与查看中间产物。目前已有265人学习下载。通过该资源读者可获取从文本清洗、特征提取到模型训练与评分输出的完整代码链路理解篇章结构特征如何影响作文得分并借助统计文件与图表分析模型表现为二次开发或教学演示提供可运行的基础方案。1. 从一份“模型结果清单”说起这套 K12 作文评分系统到底交付了什么先看一组文件名ADMResult、ADMTrainLM、BGResult、BGTrainLM、EGTrain.txt.count、REXPTrain.txt.count、BGTrain.txt.count、RTTTrain.txt.count、EEXPTrain.txt.count、RSTrain.txt.count。第一次拿到这个包的人大概率会愣一下——没有main.py没有requirements.txt只有一堆以Result、TrainLM、.count结尾的文件。这恰恰是它的真实形态这不是一个开箱即用的 Web 应用而是一套基于篇章结构的自动作文评分系统的训练产物与中间统计用 Python 把 K12 作文的篇章特征抽出来、训成语言模型、再落成分数预测结果。它能解决的问题很具体K12 阶段一个语文老师带两个班一次作文上百份人工批改一篇要 3 到 5 分钟全批完就是大半天。这套系统把「篇章结构」当作评分主线——不是只看词藻和错别字而是看段落之间怎么衔接、主题句怎么展开、过渡是否自然——然后用统计语言模型和回归模型去拟合人类评分员的打分习惯。适合谁适合已经会 Python 基础、想拿一套真实教育 NLP 数据练手的中小学信息化老师、教育科技方向的在校生以及需要给作文批改做辅助工具的教研人员。它不承诺替代人工但能把「初筛 客观维度打分」这一步自动化把老师的精力留给真正需要主观判断的部分。2. 篇章结构特征怎么抽从原始作文到.count统计文件这套系统的核心资产是那些.count文件。它们不是随便生成的日志而是把每篇作文按篇章结构切分后统计出的 n-gram 频次。理解这一步后面所有TrainLM和Result才有意义。2.1 为什么评分主线选“篇章结构”而不是“词汇丰富度”纯词汇维度的评分很容易被“堆砌华丽词藻但逻辑混乱”的作文骗过去。K12 作文评分标准里内容、结构、语言三项中结构往往是最能区分档次的一项。常见做法是先把作文按段落切开识别每段的主题句通常是段首句再计算相邻段落之间的词汇重叠度、指代衔接词密度、过渡词命中率。这些指标合起来构成“篇章连贯性”特征向量。我一般会这样组织特征特征类别具体指标作用段落级段落数、平均段长、段长方差反映结构是否匀称衔接级相邻段词汇重叠率、代词密度反映段落是否连贯过渡级过渡词命中数因此/然而/总之反映逻辑显性程度主题级段首句与标题的相似度反映是否跑题这些特征抽完之后才会进入语言模型训练。.count文件就是这些统计的落盘结果命名里的BG、EG、REXP、RTT、EEXP、RS大概率对应不同的特征子集或不同的作文分组比如按文体或按年级分桶。2.2 用 Python 把作文切成篇章单元并生成 count 文件下面这段脚本是我按这套系统的数据形态反推的生成逻辑跑之前把raw_essays/换成你的作文目录每篇一个.txt。import os import re from collections import Counter # 中文过渡词表可按学段增删 TRANSITIONS [因此, 然而, 总之, 首先, 其次, 最后, 而且, 但是, 所以] def split_paragraphs(text): # 按换行和中文全角空格切段过滤空段 paras re.split(r\n, text.strip()) return [p.strip() for p in paras if len(p.strip()) 5] def char_ngrams(text, n2): # 字符级 n-gram对中文比词级更稳不依赖分词器 text re.sub(r\s, , text) return [text[i:in] for i in range(len(text)-n1)] def extract_features(text): paras split_paragraphs(text) feats {} feats[para_num] len(paras) feats[avg_para_len] sum(len(p) for p in paras) / max(len(paras), 1) # 相邻段词汇重叠率 overlaps [] for i in range(len(paras)-1): a, b set(char_ngrams(paras[i])), set(char_ngrams(paras[i1])) if a and b: overlaps.append(len(a b) / len(a | b)) feats[adj_overlap] sum(overlaps) / max(len(overlaps), 1) # 过渡词命中 feats[transition_hit] sum(text.count(t) for t in TRANSITIONS) return feats, paras def dump_count(paras, out_path): # 把所有段落拼起来做 n-gram 统计写入 .count counter Counter() for p in paras: counter.update(char_ngrams(p, n2)) with open(out_path, w, encodingutf-8) as f: for gram, cnt in counter.most_common(): f.write(f{gram}\t{cnt}\n) if __name__ __main__: src_dir raw_essays out_dir counts os.makedirs(out_dir, exist_okTrue) for name in os.listdir(src_dir): if not name.endswith(.txt): continue with open(os.path.join(src_dir, name), encodingutf-8) as f: text f.read() feats, paras extract_features(text) stem name.replace(.txt, ) dump_count(paras, os.path.join(out_dir, f{stem}.txt.count)) print(stem, feats)逻辑说明split_paragraphs负责把整篇作文切成篇章单元这是“篇章结构”四个字的落地起点char_ngrams用字符级二元组而不是词级是因为 K12 作文里分词错误会直接污染特征字符级更抗噪。extract_features里的adj_overlap是篇章连贯性的核心指标值越低说明段落之间越“跳”。dump_count把每篇作文的 n-gram 频次写成制表符 计数的格式这正是.count文件该有的样子。参数说明n2可以改成 3 提升区分度但文件会变大TRANSITIONS表按学段调整小学低年级可以只留“首先/然后/最后”。跑完你会得到一批XXX.txt.count和资源里的命名规律对得上。2.3TrainLM与Result的关系训练产物和预测产物别搞混ADMTrainLM、BGTrainLM这类文件是训练阶段的语言模型产物通常是 n-gram 概率表或模型参数序列化结果ADMResult、BGResult是预测阶段对测试作文打分后的输出。很多人第一次用会直接打开Result想找分数结果看到一堆浮点数就懵了——那些浮点数就是每篇作文的预测分或各维度得分。判断方法很简单文件名带Train的是训练中间件带Result的是最终输出。用之前先确认你的测试集和训练集命名前缀一致否则Result里的顺序对不上作文。3. 把 count 喂给模型训练、打分与结果落盘有了.count和特征下一步是训练一个能预测分数的模型。这套系统没有限定必须用哪个算法但从它保留TrainLM的形态看走的是“统计语言模型 回归”的经典路线而不是端到端深度网络。原因也现实K12 作文标注数据量通常只有几百到几千篇上 BERT 容易过拟合SVM 或岭回归反而稳。3.1 从 count 文件还原特征矩阵import os import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import Ridge def load_counts(count_dir): docs, names [], [] for fn in sorted(os.listdir(count_dir)): if not fn.endswith(.count): continue grams [] with open(os.path.join(count_dir, fn), encodingutf-8) as f: for line in f: parts line.strip().split(\t) if len(parts) 2: gram, cnt parts[0], int(parts[1]) grams.extend([gram] * min(cnt, 5)) # 截断高频防长尾主导 docs.append( .join(grams)) names.append(fn.replace(.txt.count, )) return docs, names def train_and_predict(count_dir, score_csv): docs, names load_counts(count_dir) vec TfidfVectorizer(max_features5000, ngram_range(1, 2)) X vec.fit_transform(docs) # score_csv: 两列作文名,人工分 import csv y_map {} with open(score_csv, encodingutf-8) as f: for row in csv.reader(f): y_map[row[0]] float(row[1]) y np.array([y_map[n] for n in names]) model Ridge(alpha1.0) model.fit(X, y) preds model.predict(X) for n, p in zip(names, preds): print(f{n}\t{p:.2f}) return model, vec if __name__ __main__: train_and_predict(counts, scores.csv)逻辑说明load_counts把.count里的gram 频次还原成可被TfidfVectorizer吃的文本min(cnt, 5)是关键——n-gram 频次长尾极重不截断的话高频虚词会主导整个向量。Ridge用 L2 正则适合特征维度高、样本少的场景alpha越大正则越强。输出直接打印每篇作文的预测分这就是Result文件该有的内容。参数说明max_features5000在千篇级数据上够用数据上万可以提到 20000ngram_range(1,2)同时保留单字和双字兼顾词汇和搭配。如果你的scores.csv里作文名和 count 文件名对不上y_map[n]会直接 KeyError这是最常见的翻车点。3.2 交叉验证与评分稳定性检查单次训练集打分没有意义必须做 K 折交叉验证看泛化。常见做法是KFold(n_splits5, shuffleTrue, random_state42)每折算一次 MAE 和 Pearson 相关系数。K12 作文评分里MAE 能压到 3 分以内满分 50 分制就算可用Pearson 上 0.7 说明和人工评分趋势一致。如果某一折 MAE 突然飙高先查那折里是不是混进了字数极短或极长的异常作文而不是急着换模型。3.3 结果后处理分数平滑与异常值截断原始预测分经常出现 47.3、47.8 这种带小数的值直接展示给老师很怪。后处理两步一是按满分区间截断二是做一次简单平滑。我一般会把预测分映射到整数档位比如 5 分一档同时保留原始分用于排序。异常值处理上如果某篇作文的预测分和同题其他作文偏离超过 2 个标准差标记为“待人工复核”而不是直接采信。这一步在Result落盘前做能省掉大量事后返工。4. 避坑与排查这套资源最容易翻车的五个地方4.1 现象.count文件打开是乱码或空行原因原始作文编码不是 UTF-8或者切段后段落长度都小于阈值被过滤光了。解决读文件时显式指定encodingutf-8失败就试gbk把split_paragraphs里的len(p.strip()) 5临时调到 1看是不是过滤太狠。4.2 现象训练时KeyError或标签全为 0原因scores.csv里的作文名和.count文件名前缀不一致比如一个是ADM01一个是ADM01.txt。解决统一在load_counts里做一次name.replace(.txt.count,).replace(.count,)归一化再和标签表对齐。4.3 现象模型在训练集上 MAE 很低换一批作文就崩原因特征里混入了和分数强相关的“作弊特征”比如作文长度。长作文天然容易得高分但模型学到的是“长高分”而不是“结构好高分”。解决把para_num、总字数这类特征做标准化或直接剔除只保留结构比例类特征。4.4 现象TrainLM文件加载报序列化错误原因训练和加载用的 Python 或依赖库版本不一致pickle 协议不兼容。解决确认训练环境版本或者改用 JSON 存模型参数。这类文件不要跨大版本直接搬。4.5 现象预测分全部挤在中间档区分度差原因回归模型在样本不均衡时倾向于预测均值。解决对分数做分箱后改用分类模型或者给 Ridge 加样本权重让高分和低分样本权重更大。5. 进阶把篇章特征做成可解释的评分报告到这一步系统能打分了但老师真正想要的不是一个小数而是“为什么是这个分”。我的做法是在Ridge之上再挂一层特征贡献分析对每篇作文取model.coef_和该作文的 TF-IDF 向量做逐维乘积排序后取绝对值最大的前 10 个 n-gram再映射回它们所属的篇章特征类别衔接、过渡、主题。这样输出的报告长这样维度贡献方向代表 n-gram段落衔接正向“因此”“综上”过渡显性正向“首先”“其次”主题偏离负向与标题低重叠的段首词def explain(model, vec, doc, topk10): x vec.transform([doc]) contrib x.toarray()[0] * model.coef_ idx np.argsort(np.abs(contrib))[::-1][:topk] inv {v: k for k, v in vec.vocabulary_.items()} return [(inv[i], contrib[i]) for i in idx] # 用法explain(model, vec, docs[0])逻辑说明contrib是每个 n-gram 对最终分数的线性贡献正负号代表拉高还是拉低。inv把索引还原成 n-gram 本身。参数topk控制报告长度给老师看 10 个足够给教研分析可以放到 30。验证方法上我习惯抽 20 篇人工已评的作文把系统报告和人工评语并排看如果系统标出的“负向贡献”词恰好是老师批注里圈出的问题说明解释层可信如果系统把老师认为写得好的过渡词标成负向那多半是训练集里这类词出现太少需要补样本。从那以后我每次上线新模型前都强制走一遍这 20 篇的对照不看过一遍不敢把报告发给老师。希望这套拆解能帮到你拿到资源后先跑通.count生成再谈模型调参顺序反了会多走很多弯路。本文还有配套的精品资源点击获取
返回列表