简介:这份资源是2022年江苏省研究生数学建模科研创新实践大赛B题的完整赛题文档,面向备战研究生数学建模竞赛的高校学生与指导教师,聚焦高校课堂教学质量数据的量化分析问题。压缩包内为1个PDF文件,约1.12MB,完整呈现赛题背景、问题重述与求解思路,涵盖基于LSTM的理论课与实践课二分类、文本相似度专家评语量化、K-means++课程差异性检验、熵权法与模糊综合评价的授课质量排序,以及灰色模型分析教学质量年度变化等内容。已有1501人学习下载,读者可据此了解赛题结构、建模框架与评价指标设计,为同类教育数据挖掘与综合评价类赛题提供参考。
1. 专家评语量化:为什么一段中文评语能被算成 91.63 分
一份高校课堂教学评价表里,最不值钱也最值钱的东西,是那一列自由文本。打分栏是勾选出来的,评语栏是专家一个字一个字敲的。问题在于,勾选能直接进表格做统计,评语不能。于是绝大多数教务系统里,评语最终只有一个归宿:存档,没人看。
2022 年江苏省研究生数学建模 B 题把这件事翻了过来:给定一份授课水平评分标准表,要求把专家评语转成百分制量化指标,并对 20 条样本排序。最终结果里,序号 4457 拿到 91.63 分排第一,4452 只有 50.39 分垫底。同一批专家、同一套标准,评语之间的差距被拉到了 40 分以上。
这个差距不是拍脑袋给的。它背后是一条完整的链路:先判断课程是理论课还是实践课,因为两套评分标准的指标项和权重完全不同;再把评语按句切开,逐句和标准表里的要点做文本相似度匹配;最后用相似度乘权重累加,得到一个可比较的分数。搞懂这条链路,就搞懂了「非结构化文本怎么变成可计算指标」这一类问题的通用骨架——它不只出现在教学评价里,任何需要把主观描述转成决策依据的场景,都是同一套逻辑。
2. 课程类型二分类:没有标签,怎么先造出标签
2.1 评分标准分两套,数据里却没有课程类别
题目给的理论课评分标准有四项:教学态度、教学内容、方法手段、教学成效,权重分别是 0.2、0.3、0.3、0.2。实践课标准只有三项:教学态度、教学组织、教学成效。两套标准的指标项数量不同、语义侧重不同,如果混在一起做评语匹配,理论课的「方法手段」会去和实践课的「教学组织」比相似度,结果必然是噪声。
但附件数据里根本没有「课程性质」这一列。这是一个典型的冷启动困境:下游任务依赖一个上游标签,而上游标签不存在。常见的实现思路是先用规则造一批高置信度标签,再用监督模型把剩下的补全。这里走的就是这条路。
2.2 用领域词表打种子标签
规则很简单:对评语做分词、去停用词、统计词频,含「设备」「示范」「实验」「操作」「仪器」的判为实践课,含「板书」「直播」「课件」的判为理论课。最终造出 247 条实践课标签、1465 条理论课标签,剩下 4180 条无标签数据留给模型预测。
这批标签的可靠性来自词的排他性。「实验」「仪器」几乎不会出现在纯理论课评语里,「板书」「课件」也基本不会出现在实践课评语里。用领域词做种子,本质是用人的先验知识换一批干净样本,代价是覆盖面窄——只有 1712 条,占全量约 29%。
提示:种子标签的类别极不均衡(247 比 1465,约 1:6),直接训练会让模型倾向于全判理论课。后续评估必须看召回率而不只看准确率,否则模型「什么都不做」也能拿到很高的准确率。
2.3 LSTM 为什么能吃掉这类短文本
拿到 1712 条有标签数据后,按 9:1 划分,1540 条训练、172 条验证。模型选的是 LSTM,核心是三个门控:
# LSTM 单步前向的核心结构(示意,对应题中公式 4.1-4.6) # x_t: 当前词向量, h_prev: 上一步隐状态, c_prev: 上一步记忆单元 i_t = sigmoid(W_xi @ x_t + W_hi @ h_prev + b_i) # 输入门:决定写入多少新信息 f_t = sigmoid(W_xf @ x_t + W_hf @ h_prev + b_f) # 遗忘门:决定保留多少旧记忆 o_t = sigmoid(W_xo @ x_t + W_ho @ h_prev + b_o) # 输出门:决定暴露多少记忆 c_candidate = tanh(W_xc @ x_t + W_hc @ h_prev + b_c) # 候选记忆 c_t = f_t * c_prev + i_t * c_candidate # 记忆更新 h_t = o_t * tanh(c_t) # 隐状态输出关键在遗忘门f_t。评语是变长序列,「这节课实验设备准备充分,但板书逻辑不够清晰」这种句子,前半段指向实践课、后半段指向理论课,普通 RNN 会把早期信息一路稀释掉,而 LSTM 通过f_t逐维控制旧记忆的保留比例,让「实验设备」这个强信号在读到句尾时仍然活着。对短文本分类来说,这比单纯的平均词向量更稳。
训练目标是最小化交叉熵:
# 二分类交叉熵,对应题中公式 4.7 loss = -sum(y_i * log(p_i) + (1 - y_i) * log(1 - p_i)) / Ny_i是真实标签,p_i是模型预测为正类(实践课)的概率,N是样本数。这个损失对「预测得很自信但错了」的样本惩罚极重,正好压住类别不均衡带来的偏向。
2.4 三个指标一起看,才敢信这个模型
验证集上对比了三个模型:
| 模型 | Precision | Recall | F1 |
|---|---|---|---|
| LinearSVC 类方法 | 0.543 | 0.543 | 0.543 |
| SVC | 0.778 | 0.609 | 0.683 |
| LSTM | 0.786 | 0.717 | 0.750 |
线性模型三项全是 0.543,这个数字很可疑——它基本等于多数类占比,说明模型退化成了「全判理论课」。SVC 精确度高但召回只有 0.609,漏掉了四成实践课。LSTM 的召回 0.717 是三者最高,F1 也最高。
这里的设计取舍值得说一句:在类别不均衡场景下,Precision 和 Recall 是互相拉扯的,F1 是调和平均,对短板更敏感。选 LSTM 不是因为它是「更高级的模型」,而是因为它在召回这个短板上补得最多。最终 20 条待求解样本里,4451、4458、4462 被判为实践课,其余 17 条为理论课——这个 3:17 的比例和训练集的类别分布基本吻合,是一个侧面的一致性验证。
3. 评语量化:相似度怎么变成分数,而不是变成玄学
3.1 句子级向量:先平均,再算余弦
评语量化的第一步是把「一句话」变成「一个向量」。做法是取句中每个词的词向量,做算术平均得到句向量,再算两个句向量的余弦相似度:
import numpy as np def sentence_sim(sent_a, sent_b, word2vec): # 句向量 = 词向量均值,对应题中公式 4.11 vec_a = np.mean([word2vec[w] for w in sent_a if w in word2vec], axis=0) vec_b = np.mean([word2vec[w] for w in sent_b if w in word2vec], axis=0) # 余弦相似度,对应公式 4.12 return float(vec_a @ vec_b / (np.linalg.norm(vec_a) * np.linalg.norm(vec_b)))平均词向量的好处是简单、可解释、对句子长度不敏感;坏处是丢掉了词序。对「按点评价」这类评语来说,词序信息本来就不强,专家写的是「教学内容充实、方法手段多样」这种并列结构,平均之后语义重心还在。这是典型的用表达能力换鲁棒性的取舍。
3.2 相似度到分数的分段映射:为什么不是线性
原始余弦相似度落在 [-1, 1],但实际匹配中大部分值挤在 0.3 到 0.8 之间。如果直接线性映射,区分度会被压扁。题中用了分段函数:
def sim_to_score(sim): # 对应公式 4.14:高相似度直接给满分,中间段线性拉伸,低相似度归零 if sim >= 0.7: return 1.0 elif sim > 0.4: return (sim - 0.4) / 0.3 # 把 [0.4, 0.7] 拉伸到 [0, 1] else: return 0.0这个设计有三层考虑。第一,相似度超过 0.7 基本可以认定专家确实按这条标准写了,给满分避免过度惩罚措辞差异。第二,0.4 以下视为无关,直接归零,防止「教学态度」的评语被误匹配到「教学成效」上还拿到零头分。第三,中间段做线性拉伸,把最有区分度的区间放大到完整值域。分段点的选择是经验值,换数据集需要重新标定——这是这类模型最容易被忽视的脆弱点。
3.3 逐句匹配、取最大、乘权重、再累加
完整的量化链路是四层聚合:
def quantify_review(review_sentences, standard_items, weights, word2vec): total = 0.0 for sent in review_sentences: sent_score = 0.0 for item, weight in zip(standard_items, weights): # 一句评语对一条标准:取该标准下所有参考表述的最大相似度(公式 4.13) best_sim = max(sentence_sim(sent, ref, word2vec) for ref in item) sent_score += sim_to_score(best_sim) * weight # 公式 4.15 total += sent_score return total # 公式 4.16、4.17第一层,一句评语对一条标准下的多条参考表述取最大相似度,因为专家只会用其中一种说法。第二层,乘上该标准的权重,理论课四项权重 0.2/0.3/0.3/0.2。第三层,把一句评语在所有标准上的得分累加。第四层,把所有句子的得分累加,得到整段评语的量化值。
这里有个隐含假设:评语里每一句都在评价某个标准项。实际上专家可能写「总体不错,希望继续努力」这种没有信息量的句子,它们会匹配到低相似度、贡献接近零分,不会拉高也不会拉低太多。但如果专家写了大段与标准无关的内容,累加机制会让分数虚高——这正是后面专家专业性模型要修正的问题。
3.4 排序结果说明了什么
20 条样本的量化结果里,4457(91.63)、4463(90.17)、4461(89.52)排前三,4452(50.39)、4460(48.97)垫底。分数跨度超过 40 分,说明模型确实在区分评语质量,而不是把所有评语压在一个窄区间里。值得注意的是,排名靠前的评语普遍是「逐点覆盖四项标准」的写法,排名靠后的往往只覆盖了一两项——这和「按点给分」的评价逻辑是一致的,也反过来验证了权重设计是有效的。
4. 课程差异检验:聚类之前,为什么必须先模糊化
4.1 连续分数在评价场景里是伪精确
第一问产出的是连续百分制分数,但直接拿去做聚类会翻车。原因在于:不同专家打分尺度不同,有人习惯给 88,有人习惯给 82,这 6 分的差异未必代表真实质量差异。连续值把这种评分者偏差原样带进了聚类,导致簇边界被噪声主导。
题中的处理是先模糊化,把连续分数按区间压到四个离散档位:
def fuzzify(score): # 对应公式 5.1:连续分 -> 四档模糊值 if score < 60: return 60 elif score < 67.5: return 75 elif score < 86.5: return 85 else: return 95区间边界不是等距的:60 到 67.5 只有 7.5 分宽,67.5 到 86.5 有 19 分宽,86.5 以上 13.5 分宽。这种非等距划分反映的是评价语义——「及格线附近」的区分要细,「良好到优秀」之间本来就模糊,给宽一点。模糊化的本质是主动丢弃低置信度的精度,换取对评分者偏差的鲁棒性。
4.2 K-means++ 解决了 K-means 的哪个具体毛病
模糊化之后,每门课被表示成三维向量:教师教学综合评价、专家评语量化值、学生学习综合评价。然后做二聚类。这里选 K-means++ 而不是普通 K-means,原因很具体:普通 K-means 随机初始化质心,如果两个初始质心恰好落在同一侧,迭代会收敛到局部最优,把本该分开的两类揉成一类。
K-means++ 的改进是让初始质心尽量分散:第一个质心随机选,后续每个质心按「与已选质心距离的平方」成正比的概率选取。距离越远的点越可能被选为下一个质心,从概率上保证初始点铺开。对本题这种「优秀类」和「一般类」界限本来就存在的场景,初始质心分散直接决定了能不能把界限找出来。
聚类效果用三个指标量化:紧密性 CP=1.686(类内平均距离,越低越紧)、轮廓系数 0.602(越接近 1 越好)、邓恩系数 0.443(类间最小距离除以类内最大距离,越大越好)。三个指标方向一致,说明二聚类结构是真实的,不是算法硬凑出来的。
4.3 差异到底有多大:看优秀类占比
聚类把样本分成两类后,统计每门课落在「优秀类」的比例,差异立刻显形:
| 课程 | 优秀 | 一般 | 优秀率 |
|---|---|---|---|
| 课程 0072 | 31 | 13 | 70.45% |
| 课程 0228 | 24 | 9 | 72.72% |
| 课程 0126 | 38 | 8 | 27.27% |
| 课程 0385 | 51 | 21 | 70.83% |
| 课程 0138 | 46 | 5 | 40.35% |
| 课程 0345 | 6 | 25 | 16.21% |
课程 0072 有七成评价落在优秀类,课程 0345 只有一成六。这个差距不是打分尺度能解释的,它说明「各课程教学质量存在差异」这个结论成立。注意课程 0126 的优秀率 27.27% 和它的绝对数量(38 优秀、8 一般)看起来矛盾——这是因为优秀率是按该课程被评价的总次数算的,不同课程被评价次数不同,看比例比看绝对数可靠。
5. 排序与专家画像:权重从哪来,偏差怎么量
5.1 熵权法:让数据自己决定谁更重要
七个教学团队要排序,涉及三个指标:教师教学综合评价、专家评语综合评价、学生学习综合评价。三个指标怎么加权?主观赋权(比如专家拍板 0.4/0.3/0.3)容易被质疑,题中用了熵权法,让数据自己说话。
熵权法的逻辑是:某个指标在各样本间差异越大,说明它携带的区分信息越多,权重应该越高;如果所有样本在这个指标上几乎一样,那它对排序没有贡献,权重应该低。计算上先对指标做归一化,再算信息熵:
import numpy as np def entropy_weight(matrix): # matrix: n 个样本 x m 个指标,对应公式 5.8、5.9 n, m = matrix.shape p = matrix / matrix.sum(axis=0) # 列归一化 h = -np.sum(p * np.log(p + 1e-12), axis=0) / np.log(n) # 各指标信息熵 w = (1 - h) / np.sum(1 - h) # 熵越小权重越大 return w1e-12是防止log(0)的数值保护,这类细节在实现里不能省,否则某个指标出现零值直接让整个程序崩掉。最终三个指标的权重是 0.2381、0.2464、0.5154——学生学习综合评价权重最高,说明它在七个团队之间的差异最大,区分能力最强。
5.2 模糊综合评价 + 二级评判:为什么不能只看总分
有了权重,还要把三个指标合成一个评价向量。做法是用频率法构造模糊关系矩阵:每个团队在每个指标上落在「优秀/良好/一般/合格」四档的频率,构成矩阵的行,再和权重向量做模糊合成,得到四维评价向量。
排序规则是二级的:先比等级,等级高的排前面;等级相同再比隶属度。这个规则解决了一个具体问题——如果只看加权总分,可能出现「优秀 0.44 + 良好 0.52」和「优秀 0.45 + 良好 0.50」几乎打平的情况,但前者其实更稳。二级评判强制先看最高等级,避免总分把结构性差异抹平。
最终排序:课程 0097、0138、0058、0486、0057、0322、0320。课程 0097 的优秀隶属度 0.5763 是唯一超过 0.5 的,排第一没有争议;课程 0320 优秀 0.4473、良好 0.5004,良好占优,排末位。
5.3 专家专业性:长评语不该自动得高分
评价专家时,专业性从「评语规范性」切入——专家是否按标准逐点评价。直接复用第一问的相似度累加会踩一个坑:评语越长,累加项越多,总分越高,等于奖励啰嗦。题中的修正是在累加后除以长度惩罚项:
def expert_standard_score(review_sentences, standard, word2vec): # 对应公式 6.3:累加相似度后做长度惩罚 total = sum(sentence_sim(s, ref, word2vec) for s in review_sentences for ref in standard) return total / np.log(len(review_sentences) + 1)log(len+1)而不是len,是因为惩罚要温和:完全按长度线性除,会把「写得详细且规范」的专家误伤。对数惩罚让长度的影响递减,既压住「堆字数」,又不至于让认真写长评语的专家吃亏。这个细节是整篇里最容易被忽略、但最能体现设计功力的地方。
5.4 专家客观性:和「其他人」比,而不是和「标准答案」比
客观性没有绝对标准,只能做相对度量。对每门课,算出除当前专家外其他专家打分的均值,再看当前专家偏离这个均值多少:
def expert_objectivity(expert, courses, ratings): # 对应公式 6.5-6.8:偏离群体均值越远,客观性越差 diffs = [] for c in courses: others = [ratings[e][c] for e in ratings if e != expert and c in ratings[e]] if others: diffs.append(abs(ratings[expert][c] - np.mean(others))) return -np.mean(diffs) # 取负,偏离越小得分越高这个度量有个前提假设:多数专家的判断接近真实水平。如果群体整体偏严或偏松,这个基准就偏了。题中也讨论了职称客观性——不同职称教师群体的平均得分确实有差异(正高 88.27、初职 84.57),但这是真实水平差异而非专家偏见,所以模型简化处理,认为专家在职称维度上客观。
综合得分用Score = α·专业性 + (1-α)·客观性合成,α=0.5 时专家 002 综合 15.74 最高,专家 006 只有 11.25。α 是可调参数,偏向专业性还是客观性,取决于评价目的——选评审专家时更看重客观性,做培训反馈时更看重专业性。
6. 避坑清单:这套链路在真实数据上会翻车的地方
6.1 种子标签造得太干净,模型在真实分布上失灵
现象:验证集 F1 有 0.75,但拿去做全量预测,实践课比例远低于预期。
原因:种子标签是用「实验」「仪器」这类强排他词造的,这批样本的特征分布比真实数据更极端。模型学到的是「有强信号词才是实践课」,遇到「上机操作环节安排合理」这种弱信号表述就判不准。
解决:造完种子标签后,人工抽查一批无标签样本,看模型预测的置信度分布。如果大量样本落在 0.5 附近,说明种子标签覆盖的特征空间太窄,需要补充弱信号词重新标注。
6.2 相似度分段阈值直接照搬,换数据集全线偏移
现象:换一批评语数据,量化分数整体偏低,大量评语被映射到 0 分。
原因:0.4 和 0.7 这两个分段点是针对特定词向量模型和特定语料标定的。换一个词向量模型,余弦相似度的整体分布会平移,原来的阈值就不再落在合理位置。
解决:新数据上先抽样算一批人工标注的「匹配/不匹配」句对的相似度分布,用分位数重新定阈值,而不是沿用固定值。这一步不做,后面所有权重和排序都建立在错误的映射上。
6.3 模糊化区间边界拍脑袋,聚类结果对边界极度敏感
现象:把 86.5 改成 85,优秀类占比排名发生明显变化。
原因:模糊化是硬分档,边界附近的样本会被整档平移。如果大量样本恰好聚集在边界附近,边界移动一点,簇结构就变。
解决:画一下量化分数的直方图,看边界是否落在样本稀疏区。如果边界正好切在峰值上,说明分档设计不合理,应该调整边界或改用软隶属度(比如用梯形隶属函数代替硬分档)。
6.4 熵权法遇到零值或常数列,权重计算直接崩
现象:某个指标在所有样本上取值相同,程序报ZeroDivisionError或权重出现nan。
原因:常数列的信息熵为 1,1-h为 0,归一化时分母可能为 0;零值取对数会得到负无穷。
解决:计算前先检查每列方差,方差为 0 的指标直接剔除或赋零权重;对数项加极小值保护。这两行防御代码在竞赛里经常被省略,但在真实数据上一定会遇到。
6.5 长度惩罚用错底数,长评语专家被系统性压低
现象:认真写长评语的专家专业性得分反而低于只写一两句的专家。
原因:用了len而不是log(len+1)做惩罚,惩罚力度随长度线性增长,把「详细」和「啰嗦」一起罚了。
解决:惩罚项要满足「长度增加时惩罚递增但增速递减」,对数或平方根都行。选定后拿几个已知规范的长评语样本验证,确保它们不会被压到平均线以下。
7. 从「能算」到「算得可信」:这套方法真正的边界在哪
这套链路跑通之后,最容易产生的错觉是「评语量化问题已经解决了」。实际上它解决的只是「可计算」,离「可信」还有距离。三个设计取舍值得反复掂量。
第一,相似度匹配的上限是评分标准表的质量。如果标准表本身写得含糊,比如「教学效果好」这种无法操作的表述,再好的相似度模型也只能匹配出噪声。标准表的颗粒度决定了量化的天花板,这是模型之外的事,但往往比模型本身更决定成败。
第二,模糊化和熵权法都在做「丢弃信息」的动作。模糊化丢掉精度换鲁棒,熵权法丢掉主观判断换数据驱动。丢得对不对,取决于丢掉的恰好是噪声还是恰好是信号。判断方法只有一个:拿人工标注的小样本做对照,看量化排序和人工排序的秩相关系数。这个验证步骤在竞赛里常被跳过,在真实业务里不能跳。
第三,专家评价模型里的 α 参数没有客观最优值。它编码的是「你更怕专家不专业,还是更怕专家不客观」。这个偏好应该由使用场景决定,而不是由调参决定。把 α 当成可解释的业务旋钮,而不是待优化的超参数,模型才不会被误用。
我自己的习惯是:任何一次把文本转成分数的建模,都先做一遍「反向验证」——随机抽 10 条量化结果,把分数遮住,让人工按同样标准重新打分,看排序是否一致。不一致的样本逐条看,八成能定位到相似度阈值或权重设计的问题。这个动作花不了半小时,但能挡住后面几个小时的返工。希望帮到你。
本文还有配套的精品资源,点击获取