ARTICLE DETAIL

资讯详情

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

Python实现篇章结构自动作文评分:从特征到模型全流程解析

Python实现篇章结构自动作文评分:从特征到模型全流程解析 简介这是一份基于Python篇章结构自动作文评分系统的毕业设计完整资料。项目围绕作文自动评分任务提供可运行的Python源码、配套训练与测试数据集以及详细开发文档适合软件工程、计科、人工智能等专业的在校学生、教师或企业开发者用于毕业设计、课程设计或项目初期立项演示。压缩包共114个文件体积仅约2MB文件类型以Python脚本、文本数据、训练计数结果、PNG图表、XML/XLSX配置与说明文档为主并包含少量pyc编译缓存目录结构清晰按数据、模型、结果、文档等模块组织便于检索使用。资源在功能测试通过后上传能够直接运行体验完整流程也可在此基础上修改算法或界面扩展其他文本分析功能。从篇章结构分析到评分结果输出项目形成完整可运行的技术链路配合详细文档可快速理解系统设计与实现思路。目前已有359人学习下载对正在准备相关方向毕设或课设的同学来说是一份可借鉴性很强的参考素材。1. 自动作文评分为什么先看篇章结构用 Python 做篇章结构自动作文评分最容易被低估的是“结构”两个字。多数人先做词频、TF-IDF 或 BERT 向量然后直接回归分数但真正决定议论文上限的是段落关系、论证位置和连接词密度。这套源码和数据集把篇章结构拆成可计算的统计量配合语言模型概率特征做成了一条普通 CPU 就能跑完的评分流水线。适合做毕业设计、课程设计也适合快速验证自动评分思路。基础好可以直接改特征层和模型层。下面按拿到资源后第一周的拆解顺序写看数据、做特征、训练、排错、封装接口。2. 系统架构与特征设计从训练数据到评分模型2.1 先看懂源码包里的数据文件解压后能看到一批以ADM、BG、EG、REXP、RTT、EEXP、RS为前缀的文件常见后缀有两种TrainLM和.count。以ADMTrainLM和ADMTrain.txt.count举例前者是训练阶段抽出的语言模型分数后者是对文本做的计数类特征。这些前缀可以理解为不同作文题型或不同题目的语料分组命名上虽然简单但文档里通常会有一张对应表。.count文件是评分系统的主要特征输入。行为一条训练样本字段按制表符或逗号分隔第一列通常是作文 ID 或人工分后续列是词数、句子数、段落数、连接词频次等。.count的好处是体积小、加载快也方便在 Excel 里核对。而*LM文件存的是每个句子或整篇文本的语言模型对数概率通常已经是归一化后的结果。拿到这样一份数据我一般建议先不要急着训练用一个脚本把不同文件的行数核对一遍确认样本是否对齐。import glob import os for path in glob.glob(data/*.count): with open(path, r, encodingutf-8) as f: line_count len(f.readlines()) print(os.path.basename(path), line_count)这段代码遍历data目录下所有.count文件打印文件名和行数。行数不一致意味着某些作文在预处理阶段被过滤后续拼接特征时需要按作文 ID 对齐否则横向拼接会导致特征错位。如果源码里的加载脚本一次性读多个文件同样需要确认它是否按 ID 做了对齐这是后面调参是否有效的前提。2.2 篇章结构特征为什么比词频更可靠自动作文评分开始时主要看词频和句长但这类特征很容易被“写很多长句”骗过去。篇章结构特征关注的是信息在段落和句子层面的组织方式。典型的三类结构特征如下表。特征族典型特征计算方式对评分的解释篇章宏观结构段落数量、段长方差、主题词分布按\n分段落统计每段词数段落过碎或过单一都不合理句间连贯相邻句余弦相似度、主题词熵对整句向量求 TF-IDF 后计算相似度相似度过高说明重复过低说明跳脱论证标记“因此/但是/然而”等连接词密度用连接词词典做匹配计数反映逻辑显性程度与人工分相关明显其中主题词熵可以用一个很简单的方式实现把全文分词后取 top 30 高频词按段落计算这些词在段落中的分布概率再算熵值。熵越低说明主题词被集中在个别段落大概率是跑题或各部分割裂熵太高说明每段都在换话题逻辑连贯性差。这里不需要直接用 LDA原因是 LDA 在短作文上不稳定反而会增加调参负担。另一个常被忽略的特征是“论点的位置”。对议论文来说第一段末尾是否出现观点句、最后一段是否出现总结句会显著影响机器评分。可以用人工规则去匹配“我认为”、“综上所述”等模板得到一个布尔特征。这种规则特征虽然简单但和人工评分的相关度往往比词向量还要高因为评分标准本身就包含结构完整度。2.3 语言模型概率如何参与评分资源里每个题组都有*TrainLM这说明它不是用深度学习模型直接端到端打分而是把语言模型概率提取出来作为句子流利度的先验特征。原理是一个高质量的句子应该具有较高的概率病句或语序混乱概率会明显偏低。最简单也最稳定的做法是基于训练语料训练一个三元语言模型对所有作文的每句话计算 perplexity再取整篇文章的均值、中位数、90 分位数。我一般不会把原始概率直接丢给回归器而是先做 log 变换并去除绝对大小的影响。因为不同题目的文本长度不一致概率均值会随之波动如果直接拼接模型会把题目 ID 当作最强特征而不是真正关注作文质量。建议先按题目分组对概率特征做标准化。这里要给源码使用提一个关键点TrainLM文件如果已经是整理好的特征你就只需要找到它对应的列名。如果源码里用numpy.loadtxt()加载那么注意分隔符是空格还是制表符不同操作系统的换行符也可能导致最后一行读不到。2.4 文档里最值得读的并不是模型而是评分标准资源里附带详细文档很多同学会跳过文档直接跑代码。实际上这份文档通常会写明评分的维度权重比如切题占 30%、结构占 30%、语言 40%这个权重会直接影响你怎么做特征融合。假设文档里明确“结构分占到 30%”那篇章结构特征就不该只做回归器的一个输入而可以单独训练一个结构质量评分器最后再和语言质量评分器做加权平均。这种两层结构在毕设答辩时更有故事可讲代码上也更容易解释。如果你在文档的“系统设计”里看到特征被分成“宏观结构与微观结构”那么实现时最好保持同样分类不要揉在一个向量里。这么做的好处是分析每个特征的影响时不用从头做特征重要性排序直接按组查看对应模块的参数即可。这也是把整套代码讲清楚的最佳组织方式。3. 可复现的 Python 实现从语料到评分3.1 加载 count 文件并组装训练矩阵先把*.txt.count文件读成 DataFrame。每个文件的列名可能不在文件里需要从源码的config.py或文档的附录找字段顺序。假设文档给出了列名数组加载代码如下import pandas as pd feature_names [ id, score, n_char, n_word, n_sentence, n_para, conj_density, topic_entropy, first_para_view, last_para_summary ] df pd.read_csv(data/EGTrain.txt.count, sep\t, namesfeature_names) print(df.shape, df[score].describe())这段代码先指定列名再用read_csv读取。sep\t必须和文件实际分隔符一致如果文件在 Windows 下导出分隔符可能变成逗号此时把sep改为,并检查n_word这列的数值范围。score列一般是人工评分作为回归目标。n_para这类计数特征不需要额外清洗空值直接用 0 填充即可不要用均值填充因为作文里没有连接词和没有句子是两种情况。3.2 用段落相似度构建连贯性特征在 count 特征之外还需要真正意义上的篇章结构特征。下面这段代码读入原始作文文本计算相邻句子向量余弦相似度的均值和方差。import json import re import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def load_essays(path): with open(path, r, encodingutf-8) as f: return json.load(f) def sentence_similarity(text): parts re.split(r[。!?;], text) parts [p.strip() for p in parts if len(p.strip()) 2] if len(parts) 2: return 0.0, 0.0 vec TfidfVectorizer(ngram_range(1, 1), max_features2000).fit_transform(parts) sims [cosine_similarity(vec[i], vec[i 1])[0][0] for i in range(vec.shape[0] - 1)] return float(np.mean(sims)), float(np.var(sims)) essays load_essays(data/essays.json) for e in essays[:3]: mean_sim, var_sim sentence_similarity(e[text]) print(e[id], round(mean_sim, 3), round(var_sim, 3))这里先按标点把文本切成句子再用 TF-IDF 做向量化。max_features2000限制特征维度防止短文本被高频噪音词干扰。返回的均值反映句间过渡是否自然方差反映是否有某处突然断裂。使用的时候如果发现所有相似度都在 0.9 以上说明文本向量化过于稀疏可以调低max_features或在切句时过滤停顿词。实际操作里很多源码包直接把这类中间特征保存成.pkl或.npy文件目的就是避免每次训练都重新分词。你可以把上面函数输出的两个值追加到训练矩阵中和 count 特征合并对应关系如下。特征名来源含义mean_sim相邻句 TF-IDF 余弦相似度均值越高段落过渡越平缓var_sim相邻句相似度方差方差大说明某处突然断链n_paracount 文件段落数量反映结构完整度topic_entropycount 文件或自算主题词分布熵衡量聚焦程度3.3 训练岭回归并评估 QWK评分任务适合用回归模型因为人工分是连续值或有序分值。用岭回归的好处是特征维度高时依然稳定。评估指标建议用二次加权卡帕系数QWK而不是准确率。下面给出完整训练代码并把 QWK 写进 5 折交叉验证。import numpy as np from sklearn.linear_model import Ridge from sklearn.model_selection import KFold from sklearn.preprocessing import StandardScaler from sklearn.metrics import cohen_kappa_score def qwk(y_true, y_pred): y_pred np.round(y_pred).astype(int) y_true y_true.astype(int) return cohen_kappa_score(y_true, y_pred, weightsquadratic) X df.drop([id, score], axis1).values.astype(float) y df[score].values.astype(float) kf KFold(n_splits5, shuffleTrue, random_state42) scores [] for train_idx, val_idx in kf.split(X): scaler StandardScaler() X_tr scaler.fit_transform(X[train_idx]) X_val scaler.transform(X[val_idx]) model Ridge(alpha1.0, solversparse_cg) model.fit(X_tr, y[train_idx]) pred model.predict(X_val) scores.append(qwk(y[val_idx], pred)) print(QWK:, np.mean(scores), np.std(scores))每一折都重新拟合StandardScaler避免验证集信息泄漏。Ridge的alpha1.0是正则强度特征量级不一的时候可以调大到 10solversparse_cg适合稀疏矩阵如果X是 NumPy 稠密矩阵可以去掉该参数或改用默认solverauto。QWK 是 0 到 1 之间的数一般达到 0.6 以上就说明特征有效0.8 以上则接近人工评分一致性。如果只有不到 0.4优先怀疑特征对齐出了问题而不是模型参数。3.4 从 LM 概率文件提取句子级流利度最后把*TrainLM文件也读进来。假设每行是句子id 对数概率对同一道题的所有句子概率做聚合。import pandas as pd import numpy as np def load_lm_scores(fname): data {} with open(fname, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 2: continue essay_id parts[0] log_prob float(parts[-1]) if essay_id not in data: data[essay_id] [] data[essay_id].append(log_prob) feats {} for e_id, probs in data.items(): arr np.array(probs) feats[e_id] [arr.mean(), arr.std(), np.percentile(arr, 10), np.percentile(arr, 90)] return pd.DataFrame.from_dict(feats, orientindex)parts[-1]取每行最后一个字段作为概率essay_id取第一个字段。这样做的优点是只要不去动字段顺序不管中间还有多少列代码都不会被列数变化弄挂。聚合出的四个统计量分别代表整体流利度、波动程度和两头极值。如果 LM 文件是整篇一个概率而不是句子级这种聚合方式会失效所以要先看文件行数是否约等于作文篇数。行数接近句子总数时才能按句子聚合。4. 参数调优与排错把模型分拉上去的细节4.1 语言模型特征与评分目标的对齐问题跑通基线后最常见的坑是特征文件的行数不匹配。.count文件可能是 2000 条TrainLM文件可能是 1800 条中间差了 200 条通常是因为原作文文本有些为空、有些长度不足在生成 LM 特征时被过滤掉了。源码如果直接按行号拼接就会静默错位训练时 QWK 奇高验证时跌回 0.3。解决办法是按作文 ID 对齐而不是按行号。合并前先看每个文件的id列是否唯一然后用mergedf_count pd.read_csv(EGTrain.txt.count, sep\t, headerNone) df_count.columns [id, score] [ff{i} for i in range(df_count.shape[1] - 2)] df_lm load_lm_scores(EGTrainLM).reset_index().rename(columns{index: id}) df_all df_count.merge(df_lm, onid, howleft).fillna(-999)fillna(-999)是为了让缺失的 LM 特征单独成为一类而不是被默认填充干扰。如果缺失比例超过 10%应该回到 LM 生成步骤检查过滤条件而不是在这里强行补缺。用howleft保留 count 文件的全部样本避免训练集被偷偷缩小。4.2 缓解篇章结构特征稀疏篇章特征通常只有 10 到 30 列远少于词袋特征因此不太需要降维。但如果使用 n-gram 连接词模板维度也可能上千。此时用 TruncatedSVD 压缩会比 PCA 更合适因为矩阵是稀疏矩阵PCA 会先做中心化导致内存爆掉。from sklearn.decomposition import TruncatedSVD svd TruncatedSVD(n_components30, random_state7) X_svd svd.fit_transform(X_sparse)n_components30是我在中文作文特征上常用的起点保留的方差一般在 0.4 左右。压缩之后每个成分不再有明确的业务含义所以这类特征只适合并入最后的评分模型不适合用来做可解释分析。如果答辩需要解释特征就把降维前的规则特征单独保留。4.3 处理评分的区间漂移许多毕业设计的数据集评分是由两名老师打的分数带小数且不同题目的均值不一致。直接回归会出现整体预测偏高或偏低的情况QWK 反而很低。解决方法是训练时加入题目分组的偏置项。最简单的做法是在特征矩阵里加一列题目独热编码让模型学会不同题目的尺度差异。当然更稳妥的方式是每个题目单独训练一个回归器再把结果映射到公共分制上。不过这会降低训练样本量适合题目数超过 10、每组样本大于 500 的情况。如果每组就一两百条还是用单模型加独热编码更稳定。4.4 用消融实验确定哪些特征真正有效这套资源里最值得做的实验不是堆模型而是消融测试。把特征分成三组基础计数特征、篇章结构特征、语言模型概率特征。分别训练三次记录每组特征的 QWK结果用下面格式整理。特征组合5折QWK均值说明基础计数0.52词数句长为主基础计数 篇章结构0.67结构特征提升最明显全部特征0.71LM 特征带来小幅稳定提升如果实验结果是全部特征反而下降最常见原因是篇章结构特征和 LM 特征高度相关。此时可以做相关性分析把相关系数大于 0.8 的其中一列去掉。不要同时保留“句子平均长度”和“段落平均长度”这类线性相关特征岭回归虽然能处理但会让结果解释变得困难。5. 把评分模型封成接口和结构化报告5.1 用 FastAPI 暴露评分服务训练好的模型和特征提取器需要放进一个函数里。为了不让每次请求都重新加载模型可以在模块加载时把Ridge对象和TfidfVectorizer、StandardScaler全部读取到全局变量。from fastapi import FastAPI from pydantic import BaseModel import joblib app FastAPI() model joblib.load(checkpoints/scorer.joblib) scaler joblib.load(checkpoints/scaler.joblib) class EssayInput(BaseModel): text: str topic_id: str EG app.post(/score) def score_essay(req: EssayInput): text_feat extract_discourse_features(req.text) lm_feat extract_lm_agg_from_text(req.text) x scaler.transform([text_feat lm_feat]) pred float(model.predict(x)[0]) return {score: round(max(1, min(5, pred)), 2), detail: build_report(x[0])}接口里把预测分限制在 1 到 5 之间避免模型输出越界。build_report可以把各特征数值映射成自然语言例如“段落之间的相似度方差偏大意味着读起来有跳脱感”。这种结构化报告非常适合毕业设计演示比单纯输出一个分更有说服力。5.2 给前端或文档输出一张评分雷达图如果不想引入太重的前端直接用matplotlib生成图片将格式、语言、结构三个维度画成雷达图。需要说明的是雷达图只能展示相对比例所以每个维度要先除以训练集的 90 分位数。否则不同量纲的特征画出来几乎都是满格。一个小技巧是把回归模型的权重按特征分组成维度分再用 softmax 归一化成 0 到 1 的数值这样每次调用接口都能得到一致性较好的可视化。下面的代码片段示意了权重分组加和的过程。group_idx {结构: [0, 2, 5], 语言: [1, 3, 4], 内容: [6, 7, 8]} dims {} for name, idxes in group_idx.items(): dims[name] np.clip(np.abs(model.coef_[idxes]).mean() * 10, 0, 1)这里用abs取权重的绝对值再按组平均然后乘以缩放系数。实际部署时缩放系数要用验证集确定否则三个维度的分数无法拉开可视化就是一张枯萎的蜘蛛网。验证方法很简单调用接口输入两篇极端作文一篇结构严谨但词汇贫乏一篇辞藻华丽但结构混乱对比两组维度分如果差异不明显就再调缩放系数或回查特征是否进入模型。本文还有配套的精品资源点击获取
返回列表