
简介这是一套面向自然语言处理与教育技术交叉领域的Python自动作文评分实践项目适用于高校本科生、研究生及NLP初学者开展篇章结构建模与文本评估算法研究。系统聚焦作文的逻辑连贯性、段落分布与结构完整性等深层特征而非仅依赖词频或语法错误统计可支撑教学反馈、智能阅卷原型开发及学术实验验证。压缩包共136个文件含19个核心Python脚本实现特征提取、模型训练与评分预测、29个原始文本作文样本、15个预处理后的txt中间结果以及大量.count统计文件和.trainlm语言模型文件辅以8张可视化png图表与3个xlsx评估结果表整体体积为5.42MB结构层次分明便于理解从数据预处理到评分输出的全流程。目前已有42人学习下载读者可直接复现基于篇章结构的评分 pipeline获取完整的训练配置、多维度特征计算逻辑及可扩展的模块化代码架构。 做自动作文评分AESAutomated Essay Scoring这件事我前前后后踩过不少坑。最早用 Python 写的那版评分器特征全是字数、句子总数、常用词占比这些“表面参数”评测集上跑出来的相关性和人工评分确实好看但真正把作文拿到语文老师面前对方一句话就把我问住了“这孩子通篇堆排比观点都没立起来你怎么还给他高分”从那次之后我就想明白了自动评分不能只数句子和字词得看文章是怎么组织的——也就是篇章结构。这篇就来聊聊我后面做的这个基于篇章结构的 Python 自动作文评分系统从特征设计、模型选型到训练细节、踩坑记录完整拆一遍适合正在做 NLP 落地项目、教育评测方向或者想了解作文评分原理的朋友参考。系统本身解决的痛点很明确传统作文评分只看局部语言质量对“文章有没有框架、段落之间有没有逻辑、主题是不是跑偏”这些决定作文档次的因素基本无能为力。我的目标是用可解释的篇章结构特征让机器能像阅卷老师一样先看整体框架再看逻辑衔接最后才落到语句水平。整个系统从原始文本进来到分数出去中间的每一步我都尽量做成模块化的方便替换和调试。1. 项目缘起与整体设计思路1.1 为什么一定要做篇章结构特征先说一个很多人忽视的事实近些年国内外的自动评分研究都指向同一个结论——篇章结构相关特征对作文分数的预测能力远高于词汇丰富度和句法复杂度。道理其实很好懂阅卷老师在几十分钟内批完几十篇作文真正拉开分数差距的往往不是哪个词用得华丽而是文章结构是否清晰、论证层次是否递进、段落衔接是否自然。传统评分系统把注意力放在表面特征上等于让老师只看字迹工整不工整自然不靠谱。我做的第一版系统用的就是字数和词性比例这些浅层特征。当时在自建的小规模语料上测试Spearman 相关系数能到 0.5 左右看着还行。可一旦把评分权重调高系统就会偏爱那些废话多、排比多但内容空洞的文章。后来我换了思路把评分重心挪到篇章结构上相关性和人工评分的一致性才真正提上去。这个转变的核心是从“数数”到“读结构”的转变。1.2 系统整体架构与处理流程整个系统的流水线是这样的原始作文文本先进预处理模块做完清洗、分句、分段然后特征提取模块从不同维度抽取篇章结构信号接着特征向量送入评分模型得到最终分数。中间还夹了一个特征筛选环节防止维度爆炸和冗余特征干扰模型。这个架构的设计原则是“分段解耦”。每一层只干一件事输入输出都是标准格式这样调参和替换组件都方便。特征提取模块做成了独立的类后续想加新特征只要往特征字典里塞一个新的键值对就行评分模型也用的是通用的 scikit-learn 接口哪天想从随机森林换到 XGBoost两行代码就能搞定。这种设计让我在后来的迭代实验里省了不少时间。2. 篇章结构核心特征的设计与计算2.1 预处理与篇章单元识别篇章结构分析的前提是先把文章切成正确的结构单元。我把预处理分成三个层级段落层、句子层、分句层。段落层用换行符和首行缩进识别句子层按句号、感叹号、问号切分分句层再处理逗号和分号。这里有个细节中文的省略号经常被误拆成多个句号我在切分前会先用正则把连续的点号合并掉。import re def split_sentences(text): # 先把省略号保护起来避免误切分 text re.sub(r\.{3,}, …, text) text re.sub(r。{2,}, 。, text) # 按中文句末标点切分保留标点符号 parts re.split(r(?[。]), text) return [p.strip() for p in parts if p.strip()]段落识别上我用了一个比较保守的策略如果文章自带换行直接按换行切如果没有换行就按每段开头是否有缩进或“第一”“其次”这类标记性词语来切。实际测试下来考场作文大多有明确的段落分隔真正难处理的是那些一段到底的“懒人作文”遇到这种情况我会退回到按逻辑连接词来过渡分段效果虽然不是完美但至少比一整坨丢给模型好得多。2.2 结构完整性与段落配置特征一篇结构完整的作文通常具备“开头引入—主体论证—结尾总结”的三段式框架。我设计了三个维度的特征来描述这种完整性开头段特征、结尾段特征、总段落数分布。开头段特征主要看是否为“引入型”段落方法是用段落首句是否出现“众所周知”“随着”“有人说”这类引入型表达来判断结尾段特征则看末段是否包含“总之”“因此”“综上所述”等收束型连接词。这两个特征都是布尔值计算简单但非常有效。总段落数分布我之前是作为一个单独特征丢给模型的后来发现它和评分的关系呈倒 U 型段落太少显得论证单薄段落太多显得内容琐碎4 到 7 段是最常见的舒适区。我干脆把段落数做了分箱处理转成哑变量模型稳定性明显提高了。还有一个容易被忽略的特征是“是否有标题”。别笑考场作文里真的有不少学生忘记写标题。标题与正文主题的匹配度也能反映审题能力我会把标题和正文分别转成 TF-IDF 向量算一个余弦相似度作为标题与内容一致性的量化信号。2.3 衔接连贯性与逻辑推进特征这是整套系统里我认为最有含金量的一部分。篇章衔接性好不好在作文里最直接的表现就是逻辑连接词的使用是否恰当、上下文的话题是否连贯。我做了一个逻辑连接词词典分成了转折类但是、然而、却、因果类因为、因此、所以、递进类不仅、而且、更、顺序类首先、其次、最后四组分别统计它们在全文中的出现密度。统计密度不是傻傻地数个数就完事我更关心的是“结构位置”。比如顺序类连接词如果集中出现在文章前 30% 的位置说明学生只是在开头列了个提纲后面根本没按这个逻辑展开因果类连接词如果密集出现在主体段中部论证逻辑通常更扎实。为了捕捉这种位置敏感信息我把每篇文章按句子序号归一化到 0 到 1 的区间统计每个连接词第一次出现位置和最后一次出现位置以及出现位置的标准差这些位置分布特征比单纯的总数更能反映结构组织能力。相邻段落之间的语义相似度也是一个关键指标。我用 Word2Vec 把句子向量化然后计算前一段最后一句与后一段第一句的余弦相似度。如果相邻段落的“首尾句”语义相似度高于整篇平均水平说明段间过渡自然如果所有段落的相似度都异常高反而可能是翻来覆去说同一件事这时我会额外生成一个“冗余惩罚特征”帮模型识别重复论证的作文。2.4 主题集中度与内容相关性特征作文跑题是评分的大忌所以“主题集中度”特征也放进了系统里。方法也很朴素先用 TF-IDF 抽取全文的关键词集合然后看每个段落的关键词重复程度。具体实现上我提取每段排名前三的关键词和全文排名前十的关键词做交集统计交集数量占比占比高说明各段都在围绕同一主题展开跑题风险低占比低说明段落之间各说各话文章不聚焦。这个特征虽然简单但排错能力非常强。我测试过一篇“题目是谈坚持结果写了大半篇美食描述”的作文主题集中度只有 0.12模型输出的分数比人工评分低 5 分左右方向是对的。后来又在此基础上加了主题词分布方差特征主题词在段落间分布越均匀说明文章对主题的展开越充分这个特征对区分“内容充实”和“头重脚轻”很有帮助。3. 评分模型的选择与训练过程3.1 特征工程与标准化处理特征提取完成之后我一共得到了 31 个篇章结构相关特征。这些特征量纲差异很大比如“总段落数”是整数“连接词位置标准差”是浮点数“主题集中度”是 0 到 1 之间的比例。训练模型之前我做了两步处理一是去掉方差接近 0 的常量特征它们对模型没有任何区分能力二是用 StandardScaler 做标准化让所有特征统一尺度。特征相关性检查也是个必做项。我画过相关性矩阵发现“因果连接词密度”和“转折连接词密度”的皮尔逊相关系数只有 0.35保留了“段落数”和“顺序连接词出现位置标准差”有 0.62 的相关性属于可接受范围。真正被我剔除的是一组冗余特征——开头段长度和整篇文章长度的比值这个特征和总字数强相关但又不带结构信息留着只会给模型添乱。做特征筛选时我建议优先用基于模型的特征重要性排序而不是一上来就套 PCA毕竟我们做的是可解释的评分系统特征含义比维度压缩重要得多。3.2 模型选型与训练细节模型选型我先后试过逻辑回归、支持向量机和随机森林。逻辑回归作为 baseline训练速度快特征权重可以直接解释但精度有限SVM 在样本量小的时候表现不错不过调参麻烦随机森林对特征的非线性关系拟合得最好而且天然支持特征重要性分析最终我选了它作为主模型。参数上设置树的数量为 300最大深度 8min_samples_leaf 是 5这些参数是在训练集上用 5 折交叉验证调出来的不是拍脑袋定的。训练数据的构建是整个项目里最费人力的一环。我用了两种数据来源一种是从学校教研组拿到的历史作文和人工评分大约 800 篇另一种是自己设计的模拟题邀请语文老师按统一标准评分大约 200 篇。评分标准采用 1 到 5 分的整体评分制为了降低主观性每篇作文由两位老师独立评分取平均分作为最终标签。两位老师的评分一致性我用 Cohens Kappa 系数做了校验Kappa 值 0.74属于“相当一致”的水平这说明数据的噪声在可控范围内。训练集和测试集按 8:2 划分同时保证同一学生写的多篇作文不会同时出现在训练集和测试集里否则模型会“作弊”测试分数虚高。这是做教育类模型很容易踩的坑我一共被这个坑坑过两次从此都是按学生 ID 做分组切分。3.3 评测结果与用户反馈评测阶段我主要看两个指标预测分数与人工评分的 Spearman 相关系数以及精确匹配率预测分数四舍五入后等于人工分数的比例。最终整包特征的系统在测试集上 Spearman 相关系数到了 0.81精确匹配率 0.58比只用语料表面特征的 baseline 分别提升 0.13 和 15 个百分点。对比数据我整理成了表格模型配置Spearman 相关系数精确匹配率说明表面特征 逻辑回归0.680.43字数、词性比例等传统特征表面特征 随机森林0.710.46换模型提升有限篇章结构特征 逻辑回归0.760.51特征贡献大于模型贡献篇章结构特征 随机森林0.810.58最终采用方案更让我欣慰的是使用反馈。把模型判分结果给一位资深语文老师看她随机抽了 30 篇低分作文其中 26 篇确实存在结构缺失、逻辑混乱的问题和模型捕捉到的信号能对应上。这说明我们设计的篇章结构特征至少在方向上和人类的阅读判断是一致的。4. 实操过程与关键代码实现4.1 开发环境与依赖安装整个系统我用的是 Python 3.9核心依赖没几个jieba 做分词、scikit-learn 做模型、gensim 开头的 Word2Vec 部分用来算词向量pandas 做数据处理。安装命令很简单pip install jieba scikit-learn gensim pandas数据量不大800 篇作文总共也就几十 MB所以我没有上大数据框架单机跑完全没问题。特征提取加上模型训练在普通笔记本上大约半分钟能跑完一轮实验这个体量用 Python 开发非常舒服不用一上来就考虑部署集群。如果你是想复现这个项目的新手我建议先用小规模数据把整个流程串通比如 100 篇作文跑通后再慢慢加数据。别一开始就想着把所有功能都塞进去模块化开发虽然听起来慢但后期的调试效率会高得多。4.2 核心特征提取模块实现特征提取是整个项目的核心我把它封装成一个类输入是一篇清洗后的作文文本输出是一个特征字典。下面给出部分关键代码结构完整性和衔接性特征都在里面import jieba from collections import Counter class DiscourseFeatureExtractor: def __init__(self): self.transition_words { causal: [因为, 因此, 所以, 由于, 既然], adversative: [但是, 然而, 可是, 不过, 却], progressive: [不仅, 而且, 更, 甚至, 况且], sequential: [首先, 其次, 最后, 接着, 然后] } self.opening_markers [众所周知, 随着, 有人说, 提到, 今天] self.closing_markers [总之, 综上所述, 因此, 由此可见, 总而言之] def extract(self, text): features {} paragraphs self._split_paragraphs(text) sentences split_sentences(text) # 结构完整性特征 features[paragraph_count] len(paragraphs) features[sentence_count] len(sentences) first_para paragraphs[0] if paragraphs else last_para paragraphs[-1] if paragraphs else features[has_opening] any(m in first_para for m in self.opening_markers) features[has_closing] any(m in last_para for m in self.closing_markers) # 逻辑连接词密度特征 total_sent len(sentences) for cat, words in self.transition_words.items(): count sum(len(re.findall(w, text)) for w in words) features[f{cat}_density] count / max(total_sent, 1) return features def _split_paragraphs(self, text): # 按换行切分过滤空段落 paras [p.strip() for p in text.split(\n) if p.strip()] return paras if len(paras) 1 else [text]这里有个写代码时容易忽略的细节如果原作文不分段paragraphs 列表长度为 1后面的段落位置特征会失去意义。所以我在 _split_paragraphs 里做了 fallback 处理保证至少返回一个段落块。写特征提取器的时候多处理边界情况比事后调模型参数划算得多。4.3 模型训练与预测流程模型训练这段代码相对常规但有两个地方值得单独提一下。第一个是标准化器的保存预测阶段必须使用训练阶段拟合好的 StandardScaler 做变换不能直接对预测样本重新 fit否则特征分布就变了。第二个是随机森林的随机种子要固定我设成了 42这样实验之间才有可比性不然模型结果每次都不一样你根本没法判断是特征改进带来的提升还是随机波动。from sklearn.ensemble import RandomForestRegressor from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split import joblib X feature_matrix y label_scores X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) scaler StandardScaler().fit(X_train) X_train_scaled scaler.transform(X_train) X_test_scaled scaler.transform(X_test) model RandomForestRegressor( n_estimators300, max_depth8, min_samples_leaf5, random_state42 ) model.fit(X_train_scaled, y_train) # 保存模型和标准化器 joblib.dump(model, aes_model.pkl) joblib.dump(scaler, aes_scaler.pkl)预测新作文的时候只要把新文本导入特征提取器生成特征向量标准化后丢给模型就行def predict_score(text): extractor DiscourseFeatureExtractor() features extractor.extract(text) feat_vec [features.get(f, 0) for f in feature_columns] feat_scaled scaler.transform([feat_vec]) return model.predict(feat_scaled)[0]feature_columns 这个列表是我在训练时保存的特征顺序预测时必须保持顺序一致否则特征位置错位模型结果完全不可信。这个小问题看起来很初级但我真的在迭代实验时踩过一次排查了一个多小时才发现是两个版本的代码特征顺序不一致导致的。5. 实践中的常见问题与排查技巧5.1 中文文本处理的三个典型坑中文作文和英文作文在处理上的差异很大。第一个坑是分词错误。jieba 默认词典对作文里的“躺平”“内卷”这类网络新词处理不好我就手动往自定义词典里加了约 200 个作文高频词效果立竿见影。具体做法是在项目目录下放一个自定义词典文本每行一个词加载时用 jieba.load_userdict() 导入。第二个坑是标点符号全角半角混用。很多学生作文里还有全角空格如果不预处理段落切分和句子切分都会出现乱七八糟的结果。我的处理是先把全角转半角再把所有空白字符归一化统一用 \s 匹配。第三个坑是“一句话一段”的现象。有些学生喜欢用极短段落凑字数一个段落只有一个句子。这种作文的 paragraph_count 特征值会变得很大反而干扰结构判断。我加了一道过滤逻辑如果段落少于 5 个字就不算独立段落直接并入相邻段落。这样处理之后段落数特征才有区分力。5.2 数据标注质量如何把控自动评分系统的上限是由数据标注质量决定的。模型效果不好的时候先别急着换模型先看看训练标签是不是靠谱。我在项目初期吃过一次大亏找了一位不太熟悉评分标准的同学帮忙标注结果模型训练出来的分数明显偏高后来发现是她把“结构是否清晰”这个维度误当成了“字迹是否工整”来判断。从那以后我所有作文评分都要求两位标注者独立评分不一致超过 1 分的样本交给第三方仲裁。还有一个细节是标注标准文档要先写清楚。我在项目里总结了一份“作文评分维度对照表”把结构完整、衔接连贯、内容聚焦这三个维度各拆成 3 个级别每个级别配一个例文片段。标注者看文档比听口述靠谱得多写清楚标准前后Kappa 系数从 0.61 提升到了 0.75 左右。5.3 常见问题速查表问题现象可能原因排查路径预测分数普遍偏高标注样本中高分作文占比过多检查训练集标签分布做下采样或加权长作文分数不如短作文特征计算时未做长度归一化检查所有计数类特征是否除以句子总数段落数特征区分度极低原文没有换行全部被合并为一段检查预处理模块的分段 fallback 逻辑多次运行结果不一致随机森林未固定 random_state代码中显式设置 random_state 参数新作文预测时维度报错特征顺序或特征数量与训练时不匹配打印训练与预测的特征名列表逐项比对模型对跑题作文不敏感缺少主题相关性特征补充 TF-IDF 主题一致性特征连接词密度特征无效连接词词典覆盖不全根据语料扩充词典加进自定义词表我每次实验遇到异常第一反应就是查训练集和预测集的数据分布是否一致。自动评分系统里数据路线的问题永远排在模型调参前面这个顺序定下来能少走很多弯路。5.4 从 0.7 到 0.8 的关键提升点记录最后记录一下我这套系统从 0.7 到 0.8 相关性过程中最有价值的三个改进节点供参考。第一次明显提升是把“连接词总次数”改成“连接词位置分布特征”。纯计数的特征容易受长作文影响改成位置标准差后模型能区分“全篇逻辑词均匀分布”和“逻辑词集中在前三段”这两种情况相关性和精确匹配率同步上升。第二次提升是加了段落间首尾句语义相似度这一步让系统第一次能感知到“段落之间在不在聊同一个话题”。第三次提升来自主题集中度特征这个是解决跑题作文误判最有效的一个特征单独一个特征就把精确匹配率抬了 4 个百分点。这三次改进的共同点都是往更接近“人类阅读体验”的方向靠的。阅卷老师感受到的“流畅”和“连贯”背后其实是这些可计算的篇章信号。每次涨点我都提醒自己目标不是精调模型去拟合分数而是让系统真正理解一篇作文的组织结构是否成立。本文还有配套的精品资源点击获取