做一个教学评价系统不难,但把它做成一个“有灵魂”的教学评价系统,就很难。
所谓“有灵魂”,我指的是两层意思。第一层,评价数据不能被论坛投票式的打分限制住,学生提交的上千条开放评语里,藏着“课程太难”“讲得不清楚”“给分太低”“作业布置不合理”这些真正有价值的信息,靠人工一条条看很不现实,靠关键词匹配也只能覆盖有限场景,这时候必须上深度神经网络;第二层,评价数据不能只躺在后台表单里,管理者和老师需要一眼看清全局,也就是用可视化大屏把教学质量的关键指标实时展示出来。这个项目就是围绕这两件事展开的:底层用 Python 和 Django 构建完整的评价填报、数据管理、权限控制体系,核心分析层用深度神经网络模型跑评语的情感分级和内容分类,输出端用一套可投放到大屏的数据可视化方案把结果呈现出来。
这篇内容适合谁?如果你正在用 Django 做全栈项目,但还没有把机器学习模型真正接入过业务系统;如果你已经有一个教学评价的问卷后台,但不知道怎么让评语变成可量化的指标;如果你需要给实训项目做一个“数据可视化分析大屏”来收尾,这篇文章都值得对照着看一遍。下面我会先把整体设计思路讲透,再一步步拆开核心实现,最后把我们踩过的坑按问题清单的形式整理给你。
1. 内容整体设计与思路拆解
1.1 教学评价系统的真实痛点
先聊聊我为什么觉得“教学评价系统”这个需求比想象中复杂。大多数同类项目的痛点是:问卷收集上来的打分统计方式太单薄,平均值、中位数、标准差这些描述性统计只能告诉我们“学生对课程的综合印象如何”,但完全解释不了“为什么”。一个课程模块被给了低分,可能是课程内容太难,可能是授课节奏太快,也可能是助教答疑不及时,如果没有开放文本,这些原因就全被淹没了。
所以最初设计这个系统时,我就定了三条硬性要求。第一,评价表单必须包含结构化量表和非结构化评语两个部分,缺一不可;第二,评语部分不能只做展示,要能自动归类到“教学内容”“授课方式”“作业考核”“总体感受”这些维度,并且判断情感极性是正向还是负向;第三,所有统计结果和模型推断结果,最终都要汇聚到一个 Dashboard 上,方便管理者和老师直接看趋势、看分布、看异常。整套逻辑其实和很多互联网产品的舆情分析系统很像,只不过我们分析的对象是课程评价文本而已。
这个定位直接决定了后续所有技术选型。如果只做打分统计,Excel 就能完成;如果还要做文本深层理解,就需要深度神经网络;如果还要可视化展示,就需要一套成熟的前端图表方案。项目规模不大,但技术点跨度却不小,这也是我为什么把 Django 作为总框架的原因——它在后端能力、数据操作和模板渲染之间可以找到一个很好的平衡点,省去很多拼接的烦恼。
1.2 为什么核心框架选 Django
选型这件事,我是认真比较过的。只用 Flask + 裸 SQL 不是不行,但教学评价系统天然需要用户系统、权限管理、后台管理页面,这些如果用 Flask 来做,几乎都要从零搭建。Django 内置了 User、Group、Permission,配合自带的 admin 后台,几分钟就能把管理端基础界面跑起来,这在我们实际开发时节省了大量时间。
还有一个更现实的原因:Django 的 ORM 在处理复杂聚合查询时很顺滑。大屏上最常出现的几个图表,无非就是“各学院平均分排名”“课程类型评价分布”“近五届评语情感走向”,这些在 ORM 里用 annotation 加聚合函数就能直接查出结果,不需要再单独写一堆 SQL 拼 JSON 接口,代码维护起来也干净很多。对比之下,FastAPI 虽然性能更好、对异步支持更友好,但在教学管理这种低并发、重后台管理的场景里,异步性能优势基本发挥不出来,反而失去了一套已经非常成熟的后台生态。
另外,Django 的中间件机制、模板引擎、REST Framework 扩展,都让前期搭建更高效。我用 djangorestframework 写 API,用 django-cors-headers 解决跨域,用 django-redis 做缓存,这些都是社区里验证得比较充分的东西,比起自己造轮子要稳得多。而且 Python 作为 Django 的母语,跟深度学习模型的接入也几乎可以做到零缝隙,模型训练好之后,直接用 joblib 或者 torch.load 加载,再用进程内调用就能完成预测,不需要额外起一个独立的推理服务。
1.3 深度神经网络在评价系统里的角色
在这个系统里,深度神经网络主要负责的是“评语文本的语义分析”。结构化打分的部分逻辑很清晰,问卷表里设计了十几道量表题,答案直接就是 1 到 5 分,这些数值用常规统计就够了。真正麻烦的是最后那道开放式问题:“你对本门课程有什么建议或评价?”这道题的答案五花八门,有的学生写得很长,有的就一两句抱怨,而且语言习惯差异很大。
如果做一个朴素的关键词规则,比如出现“太难”就打负向标签,那“课程内容虽然难但老师讲得很清楚”这句话就会被误判为负面,准确率很成问题。如果去收集大量人工标注数据训练一个传统的条件随机场模型,特征工程又非常耗时。因此我选择了深度神经网络来做文本分类。文本在送入模型之前先经过分词和清洗,然后映射成词向量序列,通过神经网络捕捉上下文关系,最后输出几个标签的概率分布,比如情感维度上的“正面/负面”和内容维度上的“教学/内容/作业/综合”。
这里我用的模型并不是特别复杂的大模型,而是一个带注意力机制的双向 LSTM 网络,加上一部分基于卷积的 TextCNN 做并行特征提取。训练数据来自几个学期的人工标注样本,规模在八千条左右,类别不完全均衡。在测试集上的准确率能够达到百分之八十六上下,对教学评价这种场景来说完全够用了。只要训练样本里吃进了足够多的“虽然……但是”“老师人很好但课很无聊”这类转折句型,模型对复杂语义的判断能力就会明显优于规则方案。
2. 核心细节解析与实操要点
2.1 系统架构与数据流设计
整个系统的架构,我按“采集、分析、展示”三段式来组织。采集端是 Django 的学员端问卷页面,学生在课程结束后进入自己的课程表,逐项提交评价;分析端由两个部分组成,常规统计直接走 ORM 聚合查询,评语文本则先送入深度模型推理,推理结果落库,方便后续复用;展示端是一套面向大屏的只读界面,从 Django 提供的聚合 API 中拉取数据,用图表组件渲染。
数据从学生接触到最终展示,完整会经历这样一个流程:
- 学生在问卷页面提交表单,Django 的视图函数校验并保存结构化评分数据。
- 评语文本被放到一个异步队列中,消费者进程负责调用深度模型服务,把每条评语预测出的标签写入对应的评价记录字段。
- 大屏前端每隔一定时间请求一个聚合数据接口,接口在收到请求后先检查 Redis 缓存,缓存不存在则执行 ORM 聚合和统计,生成 JSON 返回。
- 大屏上的定时器收到新数据后,自动更新图表对应的 series 值。
这套流程的关键点是第 2 步和第 3 步。第 2 步必须异步执行,因为模型推理一次要几十毫秒,如果让每个学生都同步等模型结果,并发稍微上来一点,接口就有明显卡顿。我的处理方式是使用一个简单的任务队列,生产端写任务,消费端批量处理,批量推理时一次喂入一批文本,GPU 或 CPU 的利用率会高很多。第 3 步的缓存也很重要,大屏首页包含的统计项比较多,如果每次都实时跑全表聚合,一次查询就可能需要几秒钟,完全不合适。
2.2 深度神经网络的模型选型与训练细节
这个项目里的深度模型是我认为最需要讲透的部分。模型结构上,我参考的是入门级文本分类的常见做法:输入层是预训练的词向量矩阵,然后是 Embedding 层,接着并行接两个分支,一个是双向 LSTM 加注意力机制,用来捕捉全局依赖关系,另一个是不同卷积核的 TextCNN,用来捕捉局部 n-gram 特征,最后把两个分支的输出拼接起来,经过一个全连接层和 softmax 输出。
为了照顾基础较弱的读者,这里用生活化一点的类比来解释:文本分类本质上就像读一篇文章抓重点,LSTM 是通读全文之后回顾某句话的上下文,TextCNN 是快速扫描句子中的若干关键词短语,注意力机制则是告诉模型“哪些词对判断情绪更重要”。三者结合,既能照顾局部细节,也能照顾整体语境,对于“虽然难但讲得好”这种转折句式,它知道后半句的“讲得好”是重点,不会只盯着“难”字下结论。
训练时的关键参数,我直接把一份可复现的配置列出来:
- 词向量维度:128 维
- 双向 LSTM 隐藏单元数:128
- TextCNN 卷积核大小:3、4、5,每个卷积核 64 个过滤器
- Dropout:0.5
- Batch size:32
- 学习率:0.001,Adam 优化器
- 最大文本长度:每条评语截取前 200 个字符
- 早停规则:验证集损失连续 5 轮不下降则停止
训练数据需要提前做好标注。我采用的是四分类标签:情感维度包含正面、负面,内容维度包含教学内容、授课方式、作业考核、综合评价,最终输出层一共分为六个有效类别。标注时要注意,一条评语可能同时涉及多个方面,所以我的模型输出其实是一个多标签分类结果,而不是单纯的多分类。多标签问题要用的就是 sigmoid 输出层加 binary cross entropy 损失函数,每个类别独立判断。对于类别不均衡的问题,我用了一个比较取巧的办法:在损失函数里给少数类配上略高的权重,训练效果比硬做欠采样好多了。
2.3 可视化大屏的选型与布局设计
大屏部分是整个项目的“门面”,也是答辩和汇报时最容易被评委看到的部分。可视化方案我最终选了 ECharts,原因很实在:文档中文资料多、图表类型覆盖全,特别是漏斗图、雷达图、桑基图这些教学评价里用得上的图表,ECharts 都有现成实现。如果选国外的一些图表库,社区资料少,遇到自定义需求比较吃亏。
大屏的布局我通常是按“总览、分布、趋势、明细”四个模块来规划。最上面一行放三个 KPI 卡片:课程总数、已评份数、平均综合评分;中间区域左边放各课程类别平均分雷达图,中间放一个教学质量的总体评分分布柱状图,右边放近几个学期的平均分趋势折线图;最下面一块通常是高频主题词云和最近负面评语列表。
这套布局的信息层级对管理者非常友好。举例来说,如果某学期突发式出现大量低分,管理者最想先看到的是“整体均分是否下跌”,其次是“哪个课程类别拖了后腿”,再次是“最近一个月的趋势是否持续恶化”,最后落到“负面评语主要集中在哪些主题”。大屏本质上是在做数据分析的叙事,而不是简单把图表铺满一屏,这一点我建议你在设计自己的大屏时反复琢磨。
3. 实操过程与核心环节实现
3.1 环境准备与 Django 项目初始化
前期的环境搭建是整个项目里最基础也最容易出错的一步。建议使用 Python 3.9 或 3.10 版本,虚拟环境用 venv 创建,避免系统级 Python 环境被污染。创建一个项目工作目录,在虚拟环境里安装核心依赖,命令如下:
python -m venv venv source venv/bin/activate pip install django==4.1.* pip install djangorestframework pip install django-cors-headers pip install django-redis pip install redis机器学习的依赖我单独要求一下。如果你用的是 TensorFlow,安装 tensorflow-cpu 就够了,教学评价这个场景数据量不大,CPU 推理几十毫秒完全可接受,没有必要为模型单独配 GPU 服务器。如果用的是 PyTorch,也一样,装 CPU 版本的 torch 就行。还有做文本处理会用到的 jieba 分词库和 gensim 词向量库,提前装好。模型训练时如果本地有 GPU 就更好,没有 GPU 就只把训练放到带 GPU 的云服务器上跑,训练完再下载模型文件到应用服务器。
Django 项目初始化的命令也比较固定:
django-admin startproject evaluation_system cd evaluation_system python manage.py startapp course python manage.py startapp dashboard python manage.py startapp ai_analysis我习惯把业务代码拆成三个 App,course 管理课程与评价数据,ai_analysis 负责模型推理与分析,dashboard 负责大屏接口。Django 的 App 边界如果一言难尽,后面全是坑,这里宁可拆细一点。创建完先在 settings.py 里注册 rest_framework、corsheaders、自己的三个 App,再把 CORS 规则和数据库连接配好。数据库我默认用 MySQL,因为项目负责人通常要求数据落库规范,MySQL 比 SQLite 更贴近生产环境。需要注意在 settings.py 中配置字符集为 utf8mb4,否则评语里如果出现 emoji 表情或者特殊字符,数据会写不进去。
3.2 数据模型设计与评价问卷逻辑
教学评价系统的数据模型,核心关系其实不复杂:课程和教师一对多,学生和课程多对多,评价记录是学生和课程之间的关联表,同时携带结构化量表和评语字段。为了把评语预测结果和原始评语分开管理,我单独建了一个评价明细表,每一条明细对应一个课程维度下的一条评语。
数据模型可以这样定义:
from django.db import models class Course(models.Model): name = models.CharField(max_length=128, verbose_name="课程名称") category = models.CharField(max_length=64, verbose_name="课程类别") teacher = models.CharField(max_length=64, verbose_name="授课教师") semester = models.CharField(max_length=32, verbose_name="开课学期") status = models.BooleanField(default=True, verbose_name="是否开放评价") class Meta: db_table = "course_info" class EvaluationRecord(models.Model): course = models.ForeignKey(Course, on_delete=models.CASCADE) student_no = models.CharField(max_length=32, verbose_name="学号") score_teaching = models.IntegerField(verbose_name="教学内容评分") score_method = models.IntegerField(verbose_name="授课方式评分") score_homework = models.IntegerField(verbose_name="作业考核评分") comment = models.TextField(max_length=1000, verbose_name="开放评语") sentiment = models.CharField(max_length=8, blank=True, null=True, verbose_name="情感标签") topics = models.JSONField(default=list, blank=True, verbose_name="内容维度标签") class Meta: db_table = "evaluation_record"这里有个我觉得很值得说明的设计:topics 字段用的是 JSONField,保存的是一整个标签列表,比如 [“教学内容”, “正面”] 这样合并后的结果。这样在大屏展示“主题词云”的时候,不需要再做复杂的表关联查询,直接把 JSON 里的标签拆出来统计就行,速度和可维护性都很可观。而 sentiment 单独存一个字段,是为了最方便的聚合过滤操作。
评价问卷逻辑上也要处理好重复提交的问题。同一个学生对同一门课理论上只能提交一次,所以需要在 evaluation_record 的库里加唯一约束,使用的字段是 course_id 加 student_no 的联合唯一索引。在视图里接收到重复提交时返回 400 错误,前端再弹出提示,避免刷新页面导致重复入库。
3.3 深度神经网络模型的训练与接入 Django
训练环节我把工作分成两步,第一步是离线的预处理和训练,第二步是加载模型文件到 Django 进程里完成预测。
离线处理的脚本,核心是做文本清洗和分词。评价数据里有很多无效的标点、多余的空格、网址链接,需要提前清理,然后按字粒度和词粒度做混合分词。我用 jieba 分词后,保存一个词汇表 json 文件,预测时模型根据这个词汇表把评语转成单词索引序列,超过 200 长度截断,不足则补零。这里补一个经验:如果你的评价文本里口语化表达很多,也可以直接使用字向量,也就是把每个汉字作为一个 token,用 2-gram 等方式构造序列。字向量的好处是不依赖词典,遇到新词也能有一层兜底表示,分词带来的误差可以被规避掉。
训练代码的大致结构如下,我用 PyTorch 示例展示:
import torch import torch.nn as nn import torch.nn.functional as F class HsTextModel(nn.Module): def __init__(self, vocab_size, embed_dim=128, hidden_dim=128, num_labels=6): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.lstm = nn.LSTM(embed_dim, hidden_dim, bidirectional=True, batch_first=True) self.attention = nn.Linear(hidden_dim * 2, 1) self.convs = nn.ModuleList([ nn.Sequential( nn.Conv1d(embed_dim, 64, kernel_size=k), nn.ReLU(), nn.AdaptiveMaxPool1d(1) ) for k in (3, 4, 5) ]) self.fc = nn.Linear(hidden_dim * 2 + 64 * 3, num_labels) def forward(self, x): emb = self.embedding(x) # batch, seq, embed_dim lstm_out, _ = self.lstm(emb) attn_weights = torch.softmax(self.attention(lstm_out), dim=1) attn_out = torch.sum(attn_weights * lstm_out, dim=1) conv_outs = [conv(emb.transpose(1, 2)).squeeze(-1) for conv in self.convs] conv_out = torch.cat(conv_outs, dim=1) out = self.fc(torch.cat([attn_out, conv_out], dim=1)) return out训练完成后,保存整个模型和词汇表。加载到 Django 端时,有一个非常大的性能坑:模型初始化一次要数百毫秒,你绝对不能在每次请求视图函数时重新加载。正确做法是在 Django 的 apps.py 启动钩子里把模型加载到全局进程,或者用一个懒加载的模块级全局单例。我采用了 apps ready 钩子的方式,在 App 启动阶段完成加载,后续每次预测都是直接调用已有对象。
预测接口我用 djangorestframework 的 APIView 实现。一个比较典型的改动是,解析评语时先尝试从 Redis 里查是否已有该评语的预测结果,如果缓存有就直接返回,否则再调模型推理。这部分对速度的提升非常明显,因为教学评价领域里,学生的很多表述其实是重复的,缓存命中率很高。批量预测时也要尽量让数据按 batch 的方式传入模型,比如一次处理 32 条,比一条一条循环速度快一个数量级。
3.4 大屏数据接口与可视化前端实现
大屏后端接口的核心,是把 ORM 聚合之后的结果包装成 JSON,再返回给前端。如果一个接口要同时返回平均分、分布、趋势多个维度,我就直接用多个查询集合并组装。下面是一段最常用的大屏总览接口示例:
from rest_framework.views import APIView from rest_framework.response import Response from django.db.models import Avg, Count from course.models import Course, EvaluationRecord class DashboardOverview(APIView): def get(self, request): total_course = Course.objects.count() total_eval = EvaluationRecord.objects.count() avg_overall = EvaluationRecord.objects.aggregate( avg_score=Avg("score_teaching") )["avg_score"] return Response({ "total_course": total_course, "total_eval": total_eval, "avg_score": round(avg_overall, 2), })除了总览,还需要给各类图表提供数据。雷达图接口按课程类别分组,计算每个类别在各维度上的平均分;趋势图接口按学期分组,统计平均分和负面评语数量;词云接口则是把 evaluation_record 的 topics 字段拆开,用 Counter 统计高频标签和词汇。你会发现这些接口基本上都只有一个共同点:逻辑不复杂,数据量大,所以特别适合用 Redis 缓存加速。我把带参查询结果都缓存了,缓存 key 用接口名加参数,有效期设置成 60 秒,大屏轮询的时候,前端的刷新根本不会压到数据库。
前端大屏我用的是原生 HTML 加 ECharts,不额外上 Vue 或 React,原因是大屏页面本身没有复杂的交互状态,只要图表能更新就足够了。ECharts 通过 import 或 script 标签引入,下面的代码展示了一个简单柱状图如何接收后端数据并更新:
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <div id="gtChart" style="width:100%;height:360px;"></div> <script> let gtChart = echarts.init(document.getElementById('gtChart')); gtChart.setOption({ tooltip: {}, xAxis: { data: [] }, yAxis: {}, series: [{ type: 'bar', data: [] }] }); async function fetchData() { const res = await fetch('/api/dashboard/distribution/'); const data = await res.json(); gtChart.setOption({ xAxis: { data: data.categories }, series: [{ data: data.scores }] }); } setInterval(fetchData, 60000); fetchData(); </script>轮询间隔我一般设 60 秒,够实时又不至于频繁冲击接口。大屏如果部署在会议室的电视上,还可以利用留白做背景板,用深色背景加高亮图表演示效果要远比浅色背景好。另外要注意大屏的页面滚动会被误触,用 CSS 把 overflow 禁掉,然后把页面写成无滚动的全屏布局。
4. 常见问题与排查技巧实录
4.1 大屏同时轮询导致接口响应变慢
常见问题是大屏上十几个图表,每个图表调用一个接口,刷新时同时发出十几个请求,Django 的同步服务模型下每个请求占一个 worker,并发上来后会出现响应延迟。我的解决方法是给大屏所有接口套一层 Redis 缓存,前端轮询只打到 Redis,几乎没有数据库压力。另外可以做一个“聚合接口”,把大屏需要的所有数据合并到一个 JSON 里返回,前端只请求一次,然后再分发到各个图表。合并接口虽然单次响应内容变大,但整体的网络开销和服务器压力反而更小。
如果你准备把大屏推到全校范围,还要考虑 Redis 缓存失效时的雪崩问题。我习惯给不同接口的缓存 TTL 加一点随机抖动,比如基础 60 秒加上 0 到 10 秒的随机数,避免所有缓存同时过期造成数据库瞬间被打满。
4.2 深度模型负面评语识别不准
训练初期模型对负面评语预测很不准,主要原因是训练数据分布极度不平衡。学生在评语里大多会写正面内容,真正负面且表达明确的语料占比只有不到百分之二十,模型会偏好预测成大类,把负面评语漏掉。解决起来其实就是两个动作:一是在损失函数里设置 class weight,让少数类的损失权重更大;二是对训练数据做增强,比如对负面语料做同义词替换,增加其多样性。
提供一个判断模型是否真的合理的小技巧:在训练集之外,专门挑 200 条“转折句式”的评语做测试,比如“内容很充实但节奏太快”“虽然难但受益匪浅”这类句子,看模型的预测结果是否符合人工判断。这一步能直接暴露模型是否真的学到了语义关系,而不是死记标签分布。
4.3 大屏图表数值和后台报表对不上
这个问题出现了一段时间才定位成功。图表的数据来自聚合接口,而聚合接口查的是评价明细表,后台报表模块查的是另一张统计表,两张表之间的数据因为异步模型推理的延迟存在时间差。比如某条评语的 topics 标签还没有被异步任务回填,统计接口里就不会把它算进去,导致刚开始展示时数值偏低。
解决方式是把异步回填任务改成队列里严格串行处理,并在回填完成时刷新大屏数据对应的缓存,让下次轮询一定拿到的是最新结果。同时给评价明细表加上一个“分析状态”字段,分析完成才会被统计接口纳入计算,保证指标口径一致。
4.4 跨域、字体、代理等零碎问题
开发时前端页面在 3000 端口,Django 在 8000 端口,ECharts 也使用 CDN,直接请求接口会触发跨域。解决方法是安装 django-cors-headers,在 settings.py 里配置允许的来源,以及添加所需的方法和请求头。大屏部署时,如果前后端走同一个域名,就不需要 CORS,这也能避免一部分安全隐患。
还有一个容易被忽略的点是,ECharts 的图例文字和标题在 Linux 服务器上如果缺少中文字体,中文会显示成方块。部署到生产环境时,记得在服务器上安装完整的字体包,或者在代码里强制指定一个包含中文的字体族,比如 “Microsoft YaHei, PingFang SC, sans-serif”。
5. 我的几点实操心得
这个项目真正跑通之后,我最大的感受是,教学评价系统的难点从来不在某一项技术上,而在于把数据采集、模型分析和可视化展示这三套逻辑有机地接到一条线上。Django 帮我把数据层和接口层处理得很稳,深度模型帮我把最值钱的那部分文本信息提取出来,大屏则让这些结果变得可读、可决策。
分享两个我从实战里总结的优化经验。第一,模型加载不要塞进每次请求里,启动时加载一次,用进程内单例。这是很多初接触模型部署的人第一个踩进去的坑,性能开销非常可怕。第二,大屏展示的所有聚合结果一定要走缓存,教育管理系统本身并发量有限,走缓存可以容下大屏高频轮询,还可以在汇报现场作图表演示时极速出数,不会出现尴尬的加载圈。
如果你也想复现这个项目,我的建议是先从一个最小可用的闭环开始,不需要一上来就把模型、大屏、后台全部做完整。先把 Django 表单跑起来,把评价数据存到库里;再训练一个最简单的文本分类模型挂到接口上;最后再做大屏可视化。每完成一步,都可以独立演示和验证,出问题时排查范围也小很多。这个项目后续还可以往更多方向上扩展,比如把模型换成预训练语言模型做更细粒度的建议分类,或者把大屏从教学评价延伸到学生评教、督导听课和就业反馈的一体化展示,底层架构基本不需要大改。