一个真实校园 AI 项目的完整复盘:数据怎么标、模型怎么选、指标怎么读、系统怎么部署。文末附开源仓库。
一、为什么要做这件事
课堂里的「趴桌、玩手机、站立、抬头」这几类行为,靠老师巡课几乎不可能实时掌握。一个 50 人的教室,一节课 45 分钟,人工观察的采样率极低,而且主观性强。
这个项目(EduBot)来自学校真实教室场景,目标很明确:
- 用教室监控数据,自建人员检测数据集 和行为分类数据集;
- 训练一个只认教室场景的人员检测模型,再配一套行为分类模型;
- 做成可落地的全栈系统(前端 + 后端 + 模型管理),而不是一个跑在 notebook 里的 demo。
最终跑通的方案是:YOLO 找人 + 自研分类模型判行为。下面把每一步的真实数据和踩过的坑摊开讲。
二、整体架构:为什么不是「一个 YOLO 干到底」
很多人第一反应是:直接把趴桌、玩手机做成 YOLO 的检测类别不就行了。
实际做下来,这条路在教室场景有几个硬伤:
- 行为是「人身上的局部状态」,不是独立物体。同一个学生坐着和站着,检测框位置、尺度完全不同,标注一致性极差;
- 细粒度行为差异太小。「互相耳语」和「正常交流」在低分辨率监控画面里几乎同形;
- 类别不平衡严重。玩手机、趴桌是低频事件,直接当检测类会严重拉低整体 mAP。
所以架构拆成两级流水线:
摄像头帧
↓
YOLOv8n 人员检测(只输出 person)
↓
按检测框裁剪单人图像
↓
5 个自研二分类模型并行推理
↓
行为标签 + 时间戳 → MySQL
↓
前端看板 / 统计报表 / LLM 教学辅助
检测负责「是谁、在哪」,分类负责「在干什么」,两级解耦之后,数据集可以分开构建,模型可以分开迭代,出问题也容易定位。
技术栈大致是:
| 层 | 选型 |
|---|---|
| 检测 | ultralytics 8.3.x + YOLOv8n |
| 分类 | PyTorch 2.5.1 + torchvision 0.20.1,自研轻量网络 |
| 推理加速 | opencv-python 4.11 |
| 后端 | Flask 3.1 + SQLAlchemy 2.0 + PyMySQL + JWT + bcrypt |
| 模型管理 | FastAPI 风格独立服务 + langchain + openai SDK |
| 数据库 | MySQL |
| 部署 | 本地服务器 + Nginx + 花生壳内网穿透 + pyinstaller 打包 |
三、数据工程:这个项目 70% 的时间花在这里
3.1 人员检测数据集
用 LabelImg(PyCharm 里跑),YOLO 格式,只标 person 一个类。不标行为类别,就是为了让标注规则足够简单,多人协作时一致性有保证。
3.2 行为分类数据集
写了一个自研标注工具:把检测框里的人员图像裁出来,人工按 5 类打标签 ------ 抬头看、站立、使用手机、头伏桌面、互相耳语。
两个数据集都按 3:1 划分训练集 / 验证集。
3.3 标注阶段的真实问题
- 遮挡:后排学生被前排挡住,框裁出来只有半个身子;
- 分辨率:远距离目标裁出来只有几十像素,人眼都难判断,模型更别想;
- 标签边界模糊:「耳语」和「讨论」的界线,不同标注员理解不一致。
这三条直接决定了后面的模型上限 ------ 数据集的歧义,模型是学不掉的。
四、检测模型选型:8 个模型的真实对比
为了搞清楚「通用检测模型在教室场景到底能不能用」,先做了一轮密集行人检测对比实验:约 9000 张密集行人图像,3:1 划分,横向对比 Faster-RCNN 系列 4 个变体和 YOLO 系列 4 个版本。
Faster-RCNN 系列在验证集上的表现(节选):
| 模型 | 参数量 | 计算量 | mAP | mAP@50 | FPS |
|---|---|---|---|---|---|
| fasterrcnn_mobilenet_v3_large_320_fpn | 18.9M | 0.9 GFLOPs | 9.09 | 24.92 | 36 |
| fasterrcnn_mobilenet_v3_large_fpn | 18.9M | 4.5 GFLOPs | 22.33 | 45.93 | 38 |
| fasterrcnn_resnet50_fpn | 41.3M | 133.9 GFLOPs | 28.61 | 50.65 | 17 |
| fasterrcnn_resnet50_fpn_v2 | 43.3M | 280.4 GFLOPs | 23.17 | 41.81 | 12 |
几个很直观的结论:
- 精度和速度是硬 trade-off。ResNet50 backbone 的 mAP 明显更高,但 12-17 FPS 在实时监控场景基本不可用;
- 轻量 backbone 掉点严重。MobileNetV3-320 版本跑到 36 FPS,但 mAP 只有 9.09 ------ 「能跑」不等于「能用」;
- 过拟合非常明显。同一批模型在训练集上的 mAP 能到 39.52% ~ 85.71%,验证集只有 9.09% ~ 28.61%。这么大的 gap 说明训练集和验证集分布差异大,或者数据量根本不够;
- 小目标检测是短板。所有模型的 mAP_large 都远高于 mAP_small,教室后排的小目标就是最难的部分。
最终选了 YOLOv8n:在 4060 上训练和推理都能接受,精度损失在可容忍范围内,模型体积小、部署成本低,还有一个很实际的理由 ------ 后面要迁到没有独显的笔记本上跑 CPU 推理。
五、行为分类:5 个小模型,比 1 个大模型更好用
5.1 真实指标
5 个行为各训练了一个轻量二分类模型,验证集结果如下:
| 行为模型 | 体积 | 参数量 | FPS | 验证集 Acc | 验证集 Precision | 验证集 Recall |
|---|---|---|---|---|---|---|
| Look_up(抬头) | 3.30 MB | 843,362 | 349 | 98.70% | 96.93% | 98.72% |
| Stand(站立) | 0.25 MB | 53,858 | 633 | 99.31% | 98.52% | 90.50% |
| Use_phone(玩手机) | 0.87 MB | 212,578 | 477 | 94.82% | 73.68% | 60.58% |
| Head_on_desk(趴桌) | 0.87 MB | 212,578 | 453 | 94.70% | 44.86% | 44.86% |
| Whispering(耳语) | 0.87 MB | 212,578 | 457 | 94.57% | 50.00% | 27.78% |
5.2 怎么读这张表
Acc 好看不代表可用。趴桌的验证集 Acc 有 94.70%,但 Precision 和 Recall 都是 44.86% ------ 这意味着模型「大部分时候说没有」,也能拿到 94.7% 的准确率。这类指标在类别不平衡任务里极具欺骗性。
任务难度差异是数量级的:
- 抬头、站立这种姿态差异明显的行为,小模型就能做到 98%+,而且模型只有 0.25 MB,FPS 600+;
- 玩手机属于中等难度,验证集 Recall 60.58%,实际使用需要配合时间窗口过滤;
- 趴桌、耳语属于细粒度 + 严重不平衡,Recall 只有 44.86% 和 27.78%,直接拿来做「判定」会误伤大量学生。
5.3 产品层面的处置
对低召回行为,系统里做了降级处理:不作为「异常判定」,只作为「趋势提示」,并且要求连续多帧命中才计入统计。这是工程上比调模型更划算的做法 ------ 与其硬把 Recall 从 27% 拉到 60%,不如先让系统不误报。
六、工程化:让系统真的能跑起来
模型只是系统的一部分,剩下的工作量同样不小。
6.1 多线程依赖安装脚本
原来是静态的 setup() + install_requires,问题很明显:无法根据设备选择 CUDA 或 CPU 源,安装慢、易断。
改成了交互式安装脚本,核心逻辑:
python
import threading, subprocess
device = input("推理设备 (cpu / cuda): ").strip()
torch_index = (
"https://download.pytorch.org/whl/cu124"
if device == "cuda"
else "https://mirrors.aliyun.com/pypi/simple/"
)
def install(pkgs, index):
subprocess.run(["pip", "install", *pkgs, "-i", index])
threads = [threading.Thread(target=install, args=(["torch==2.5.1", "torchvision==0.20.1"], torch_index))]
for dep in ["opencv-python==4.11.0.86", "ultralytics==8.3.99", "flask==3.1.0"]:
threads.append(threading.Thread(target=install, args=([dep], "https://mirrors.aliyun.com/pypi/simple/")))
for t in threads:
t.start()
while any(t.is_alive() for t in threads):
done = sum(1 for t in threads if not t.is_alive())
print(f"\r进度 {done / len(threads):.2%}", end="")
上面是核心逻辑示意,不是完整源码。
效果:PyTorch 单独线程走官方源,其余依赖并行走阿里云镜像,同时输出进度百分比。相比单线程串行安装,等待时间明显缩短。
6.2 Nginx 子路径反向代理
前后端和模型管理要共用一个入口,于是用 Nginx 监听 9004,按路径分发:
nginx
server {
listen 9004;
server_name 10.5.8.248;
location / {
proxy_pass http://127.0.0.1:8080;
}
location /zheng_en_ci/ {
proxy_pass http://127.0.0.1:8192/;
}
}
关键点:proxy_pass 末尾的 / 会自动剥掉 /zheng_en_ci 前缀,后端服务不需要感知子路径 ------ 否则所有路由都要改一遍。
6.3 公网访问与打包
- 公网访问:本地服务器 + 花生壳内网穿透,满足演示和试用;
- 打包 :
pyinstaller==6.14.2把后端和模型管理分别编译成 exe,交给非技术用户也能一键启动; - 环境迁移:从带 4060 的机箱迁到 ThinkPad E15 Gen4 之后,模型管理模块全部改成 CPU 推理,重新验证了加载和推理链路。
七、LLM 教学辅助:让系统不只是「监控」
单做行为识别,价值天花板很低。所以接入了通义千问和中科院紫东太初,做了几个教学侧功能:
- PPTX → 思维导图:解析课件结构,生成知识脉络;
- 教案 / 题目生成:基于 PPTX + 提示词生成教案和带答案的题目,支持保存;
- 英文翻译与解析:面向双语教学场景。
实现上用 langchain + openai SDK 的兼容层,模型管理服务独立打包,切换模型只改配置。
八、踩坑复盘
- 数据集决定上限。标注歧义、遮挡、小目标,这三条在训练前就锁死了模型能达到的高度;
- 训练集指标没有参考价值。所有模型训练集 mAP 都远高于验证集,只看 train loss 会严重误判;
- Acc 在类别不平衡时是陷阱。趴桌 94.7% 的 Acc 对应 44.86% 的 Recall;
- 多模型串行推理会拖垮实时性。5 个分类模型必须并行,否则单帧耗时直接翻倍;
- 打包成 exe 后路径全变。所有资源路径必须改成相对路径或运行时解析;
- 内网穿透只适合演示。真要做产品,得正经部署到有公网 IP 的服务器上。
九、开源与后续
项目前端部分已在 Gitee 开源:
arduino
https://gitee.com/zheng-enci050704/AiKeTangJiaoXuePingCeXiTong_QianDuan
后续计划:
- 补充低召回行为的高质量标注数据,优先解决耳语和趴桌;
- 检测侧尝试 YOLOv8s / 更新版本,并导出 ONNX 做推理加速;
- 视频流改成 WebSocket 推流,降低轮询开销;
- 行为统计接入时序模型,从「单帧判断」升级为「行为片段识别」。
这个项目最大的收获不是某个模型涨了几个点,而是搞明白了一件事:AI 项目真正的难点,永远在数据、指标解读和工程落地这三件事上,模型结构反而是最简单的那一环。
如果这篇文章对你有帮助,欢迎在评论区交流教室场景下的检测和分类方案。