最近把一套“基于YOLOv11DjangoDeepSeek的学生课堂行为检测系统”从训练到部署完整跑了一遍。这个项目带登录界面、Python源码、ONNX模型还有一套不花哨但足够精致的Web界面主要用来识别课堂里学生举手、玩手机、低头、趴桌这类行为并把统计结果自动整理成一段能直接发给班主任的自然语言小结。这套东西适合三类人想快速交付课堂行为分析Demo的开发者、正准备入门YOLO加Web服务组合的初学者以及在学校信息化项目里做技术选型的人。下面把我踩过的坑、实际用的方案、以及每一步为什么这么做完整记录一下。1. 整体设计与方案拆解1.1 真实需求这个系统到底要解决什么问题先说项目来源。某高校信息中心想做一个课堂行为分析辅助工具应用场景不是检查学生隐私而是帮老师从大量课堂录像中快速找到明显影响听课状态的行为比如大面积趴桌、频繁玩手机、长时间低头。传统做法是让助教一帧帧看视频效率极低而且不同人判断标准不一致。用视觉模型代替人工做第一轮筛选老师只需要看系统挑出来的异常片段压力会小很多。所以我给系统定了三条核心要求。第一识别行为要尽量中性不要给学生贴价值标签像“睡觉”这种词太主观统一叫“趴桌”更合适。第二结果要可追溯单靠一个总数没有说服力必须能回看对应时间段。第三报告要能被普通老师看懂而不是扔给老师一堆数字曲线所以后面引入了DeepSeek来做自然语言小结。这就是整个系统的“为什么”视觉模型负责感知后端负责记录和汇总语言模型负责把数据翻译成老师能理解的话。技术选型完全围绕这三个目标展开不是为了炫技。1.2 为什么是YOLOv11DjangoDeepSeek这套组合先看视觉部分。YOLOv11是目前比较好上手的实时目标检测模型训练生态成熟导出ONNX模型后可以脱离PyTorch环境独立部署这对交付很关键。你给客户一套源码总不能要求对方先装几G的深度学习框架。模型推理用ONNX格式加CPU即可运行教室里一台普通办公电脑也能带动部署成本低。后端选Django原因很实在登录认证、权限控制、后台管理、ORM这些功能全自带。课堂行为检测系统天然需要分角色登录老师看自己班级的数据管理员看全校数据如果用Flask或FastAPI这些都要自己写或者拼第三方库交付周期会拉长。Django的Admin能直接预览行为记录表调试阶段特别方便。DeepSeek在这里扮演的是“总结助理”角色。检测系统会生成一堆行为统计数据比如某节课趴桌人数在15分钟后明显上升、玩手机次数在最后10分钟密集出现。老师不一定有时间看数据和曲线DeepSeek可以基于这些结构化数据生成一段课堂观察小结。它不做判断只做归纳这样既保留AI的自然语言能力也避免模型编造事实。1.3 完整数据流从摄像头画面到一份自动生成的课堂小结整套流程可以画成一条清晰的数据链摄像头或视频文件 - 抽帧 - YOLOv11模型推理 - 行为标签与置信度 - 后端过滤和统计 - 数据库存储 - 前端图表展示 - DeepSeek生成课堂小结实际实现时我把推理和Web服务分开处理。摄像头采集线程把每一帧传入模型得到行为框和置信度后不直接写数据库而是先经过一个“去抖”逻辑。比如某一帧检测到学生低头下一帧没检测到眨眼间就消失的情况不能算数我会在连续三帧里至少两帧命中同一个行为才记录一条有效事件。这一步极大减少了误报。数据库层面按小时和天做聚合统计。原始检测记录只保留最近三天用于回看和排查行为计数表永久保存用于生成趋势图。DeepSeek调用放在统计任务末尾前端展示出来的是一个既有数字又有文字结论的页面。这套流程看起来简单但每一步都有取舍。接下来拆开讲每个核心模块的做法和参数细节。2. 视觉识别核心YOLOv11的训练与ONNX导出2.1 数据集准备先保证行为类别清晰可控做行为检测最容易犯的错是一上来就定义十几个类别最后发现标注数据根本不够模型全是漏检。我的建议是控制在8个以内而且每个类别都要想清楚“什么画面算这个行为”。我在这个项目里定义了这8类类别名称检测对象实际场景举例raise_hand举手学生主动回答问题reading阅读低头看书或讲义writing写字伏案记笔记using_phone玩手机手持手机或低头看屏幕looking_down低头长时间低头但未识别出阅读或写字sleeping趴桌头枕在桌面上standing站立起立发言或活动talking交头接耳两人以上侧身交流这里要特别提醒reading、writing、looking_down三者非常容易混淆。我的处理方式是在标注规范里写清楚手上有书或本子算reading/writing手上没东西且头部低于水平线才算looking_down。宁可标注规范复杂一点也不要让标注员自由发挥。每类至少准备了800张有效标注图。数据来源包括实际课堂录像的脱敏片段、模拟课堂场景拍摄以及公开数据集筛选。涉及学生的画面我会强制裁剪人脸区域并做模糊处理避免隐私问题。标注工具用的开源标注工具导出为YOLO格式的txt文件框的归一化坐标是[class_id, x_center, y_center, width, height]。数据划分按8:1:1分成训练集、验证集、测试集同时保证同一个学生出现的时间段尽量不横跨训练集和测试集否则模型会通过“记忆人”来刷高分数而不是真正学到行为特征。2.2 训练细节从yaml文件到评估指标数据集根目录下建一个classroom.yaml内容大致如下path: ../datasets/classroom train: images/train val: images/val nc: 8 names: 0: raise_hand 1: reading 2: writing 3: using_phone 4: looking_down 5: sleeping 6: standing 7: talking训练命令我建议直接跑Ultralytics官方CLIyolo detect train \ dataclassroom.yaml \ modelyolo11n.pt \ epochs120 \ imgsz640 \ batch16 \ device0 \ patience20 \ save_period10几个参数的权衡说一下。modelyolo11n.pt是最小版本的预训练权重单张图浮点计算量小后续导出ONNX部署最省心。如果你有GPU且数据量超过5000张可以试yolo11s.pt精度会高一些但推理延迟也上去了。imgsz640是速度和精度的平衡点教室视角下学生目标通常不会太小640足够用。patience20表示20轮验证集指标没提升就提前停止。我一开始图省事没加这个参数结果训练到第100轮模型已经在过拟合趴桌和低头的验证集精度不断波动浪费了不少时间。训练完成后看权重目录下的results.png和验证集指标。我的验收底线是mAP50不低于0.75mAP50-95不低于0.55。如果低于这个水平不建议直接上项目先检查是不是类别混淆或者数据量不足。如果有个别类别掉得很厉害比如using_phone的mAP50只有0.4我会单独给这个类别补数据或者做针对性增强比如增加手机小目标、暗光、遮挡的样本。2.3 导出ONNX模型并用onnxruntime加载训练完成后导出ONNX模型这一步很关键。命令很简单yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640 dynamicTrue simplifyTruesimplifyTrue会使用ONNX简化工具去掉一些冗余算子模型体积更小推理更快。导出后一般只有一个约20MB左右的best.onnx文件独立部署时只需要这个文件加推理代码不再需要PyTorch环境。加载ONNX模型有两种路径。第一种最省事还是用Ultralytics的接口from ultralytics import YOLO model YOLO(models/classroom_best.onnx) results model.predict(frame, conf0.35, iou0.45, verboseFalse)这种方式会自动处理ONNX输出解析和NMS适合项目快速落地。第二种是纯onnxruntime方式适合你不想依赖Ultralytics库或者要嵌入到其他C/移动端程序里。这时需要自己写预处理和后处理YOLOv11导出后的输出形状一般是[1, 84, 8400]其中84是4个框坐标加80个类别概率8400是特征图展开后的候选框数量。如果你只检测8个类别类别数不一样输出维度会是[1, 12, 8400]。纯onnxruntime推理参考逻辑大致这样import onnxruntime as ort import numpy as np import cv2 session ort.InferenceSession( models/classroom_best.onnx, providers[CPUExecutionProvider] ) def preprocess(frame): img cv2.resize(frame, (640, 640)) blob img[:, :, ::-1].transpose(2, 0, 1)[None] blob np.ascontiguousarray(blob, dtypenp.float32) blob / 255.0 return blob def postprocess(outputs, conf_thres0.35, iou_thres0.45): # 输出形状若是 [1, 84, 8400] 则先转置为 [1, 8400, 84] if outputs.shape[-1] ! 84: outputs outputs.transpose(0, 2, 1) boxes, scores, labels [], [], [] for pred in outputs[0]: cx, cy, w_box, h_box pred[:4] score pred[4:].max() if score conf_thres: continue label int(pred[4:].argmax()) x1 cx - w_box / 2.0 y1 cy - h_box / 2.0 boxes.append([x1, y1, w_box, h_box]) scores.append(float(score)) labels.append(label) # 实际项目里还要做非极大值抑制避免同一个学生出多个重叠框 return boxes, scores, labels如果你不想自己写NMS可以直接用ultralytics自带的方法处理。两种方案我都跑过从稳定性上看项目里用Ultralytics加载ONNX是最省心的如果做嵌入式部署再花时间完善纯onnxruntime路径。2.4 推理技巧让普通CPU也能跑得动学生课堂行为检测通常不需要每帧都跑10秒内能看到一次统计更新就已经够用。我用“隔帧检测”策略把推理频率控制在每秒3到5次CPU占用率可以控制在比较舒服的范围。具体做法是在读取摄像头的循环里加一个计数器第1帧检测第2到3帧跳过第4帧再检测。跳过的帧直接取上一帧结果做展示视觉上看起来依然是连续的。我在普通办公级CPU上测过yolo11n加ONNX推理单帧耗时大约80到150毫秒隔帧后完全能喂得起一条MJPEG视频流。另外一个很重要的坑是线程安全。onnxruntime会话在同一时间被多个线程调用可能出问题我建议给模型加一个全局锁或者干脆用一个独立的推理线程其他代码只从队列里取结果。后面Django后端部分会再详细说。3. Django后端登录认证与数据闭环3.1 Django在项目里的定位在这个项目里Django不是承担高并发流量的角色而是承担“业务闭环”的角色。检测系统产生的数据需要入库、需要按班级和时间查询、需要区分不同用户权限Django的ORM和Auth能很自然地覆盖这些需求。我的工程结构大致是classroom_system/ ├── manage.py ├── classroom/ # 项目配置 ├── accounts/ # 登录、用户管理 ├── detection/ # 推理服务、记录查询 ├── stats/ # 统计汇总 └── frontend/ # 模板和静态资源把推理放detection应用统计放stats应用职责分开之后后面维护轻松很多。不要在views.py里堆一大坨逻辑尤其是检测结果处理和数据库写入必须拆成service模块。3.2 登录界面与用户权限系统启动后第一个页面就是登录界面。Django自带的认证体系可以直接用但模板要自己写。我重写了一套纯净的登录页背景放班级实时预览表单只有用户名、密码和登录按钮也没有第三方验证码内部系统验证码反而增加使用成本。Django登录的部分代码如下# urls.py from django.contrib.auth.views import LoginView, LogoutView from django.urls import path urlpatterns [ path(accounts/login/, LoginView.as_view(template_nameregistration/login.html), namelogin), path(accounts/logout/, LogoutView.as_view(), namelogout), ]登录模板核心结构就三块form methodpost {% csrf_token %} {{ form.as_p }} button typesubmit登录/button /form所有需要登录的视图都加上login_required装饰器。我遇到过一个很典型的坑前端通过fetch请求接口时如果session过期Django默认会返回302重定向到登录页但fetch不会自动跳转页面会默默出错。我后来写了一个统一响应处理检测到返回状态为302或401时前端直接跳转登录页。权限方面分了普通教师和管理员两种角色。教师只能看自己摄像头下的记录管理员能看全部设备。Django自带的Group模型够用不用自己去扩展权限表。3.3 检测结果入库模型设计记录表最忌讳每帧每框写一条数据几分钟就能涨到几百万条。我采用了两级存储明细表加统计表。明细表只保留最近三天的原始事件模型设计如下class BehaviorRecord(models.Model): camera_id models.CharField(max_length50, db_indexTrue) behavior models.CharField(max_length20, db_indexTrue) confidence models.FloatField() frame_time models.DateTimeField(db_indexTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-frame_time]统计表按小时聚合class HourlyStat(models.Model): camera_id models.CharField(max_length50, db_indexTrue) stat_hour models.DateTimeField(db_indexTrue) behavior models.CharField(max_length20) count models.IntegerField(default0) class Meta: unique_together (camera_id, stat_hour, behavior)前端展示趋势图时直接查统计表查询速度非常快。原始帧图片不存数据库只保存JPEG截图的文件路径而且默认只保留24小时内的异常截图。写数据库时要注意批量插入。我实测用bulk_create比逐条save()快几十倍。推理线程每攒够50条记录就批量写入一次既减少数据库压力又不会因为进程崩溃丢太多数据。3.4 把摄像头推理服务“塞”进Django进程这个部分是初学者最容易搞混的地方。有人试图在Django的请求里直接调用YOLO检测每一帧结果推流接口卡到没法看。正确思路是Django进程和推理进程分离数据通过内存队列或数据库中转。我在demo里采用的方式是Django启动时在后台创建专用推理线程循环读取摄像头结果写入队列。核心代码大概是这样import threading import queue import cv2 from ultralytics import YOLO from django.conf import settings _model None _model_lock threading.Lock() frame_queue queue.Queue(maxsize2) def get_model(): global _model with _model_lock: if _model is None: _model YOLO(settings.ONNX_MODEL_PATH) return _model def inference_worker(camera_id): cap cv2.VideoCapture(camera_id) while True: ok, frame cap.read() if not ok: break model get_model() results model.predict(frame, conf0.35, verboseFalse) # 解析结果写入明细表这里用批量收集方式 time.sleep(0.2)Django App启动时调用线程# apps.py class DetectionConfig(AppConfig): default_auto_field django.db.models.BigAutoField name detection def ready(self): if not os.environ.get(DJANGO_PROCESS_WORKER): t threading.Thread( targetinference_worker, args(settings.DEFAULT_CAMERA_ID,), daemonTrue ) t.start()这里有两个坑。第一个是ready()在runserver重启、迁移命令、甚至单元测试里都会执行所以一定要用环境变量或启动参数做开关否则跑个迁移都会把摄像头打开。第二个是模型加载很慢不要在每次请求里YOLO(xxx.onnx)用模块级变量保存单例。如果做生产级部署我会把推理放到独立进程或Celery任务里而不是Django的守护线程。不过对这套演示系统来说后台线程的方案足够简单直观代码也容易理解。4. DeepSeek课堂小结从行为统计到自然语言建议4.1 为什么需要一个语言模型参与检测系统默认输出是“08:50-09:10之间趴桌行为累计出现37次”。这个数据对懂技术的开发人员很有意义但对班主任来说不够直接。他们想知道的是“这节课整体状态怎么样”“有没有明显异常时段”“明天上课有没有什么建议”。DeepSeek正好可以把一批结构化统计翻译成几段中文小结。这里要划清界限DeepSeek只做文本组织不做数据判断更不能让它自行编造数据。所有行为数值都从数据库读出后嵌入提示词AI只负责挑重点和调整语气。4.2 调用DeepSeek接口的封装我封装了一个非常轻量的客户端避免在视图里到处写requests重复代码import os import requests import json class DeepSeekClient: def __init__(self): self.api_key os.environ.get(DEEPSEEK_API_KEY, ) self.base_url https://api.deepseek.com/chat/completions self.model deepseek-chat def generate_classroom_summary(self, stat_payload: dict) - str: if not self.api_key: return prompt self._build_prompt(stat_payload) headers { Authorization: fBearer {self.api_key}, Content-Type: application/json } body { model: self.model, messages: [ {role: system, content: 你是课堂观察助理只依据给定数据做客观归纳。}, {role: user, content: prompt} ], temperature: 0.3, max_tokens: 300, timeout: 15 } try: resp requests.post(self.base_url, headersheaders, jsonbody, timeout(5, 20)) resp.raise_for_status() data resp.json() return data[choices][0][message][content].strip() except Exception: return 注意几点API Key不要硬编码在源码里一定要通过环境变量或单独的配置文件读取timeout要设置课堂场景下接口响应超过10秒就没意义了异常必须捕获语言模型不稳定不能让它拖垮整个课堂小结功能。4.3 提示词设计让输出可读、可控我试过一段比较长的提示词也试过非常短的实际效果差距很大。目前稳定使用的提示词模板是这样的你是课堂观察助理。请根据以下统计数据生成一段不超过150字的中文课堂小结。 数据如下 - 总人数42 - 举手次数8 - 玩手机累计次数5 - 趴桌累计次数9 - 低头累计次数12 - 读写累计次数30 - 交头接耳累计次数4 要求 1. 不补充任何数据之外的信息。 2. 指出最明显的两个行为特征。 3. 用客观、中性语气不要给学生贴负面标签。 4. 最后用一句话给任课教师一个可操作建议。生成出来的效果大概是“本节课大部分学生保持了读写状态课堂参与度整体良好。值得注意的是下课铃前十五分钟趴桌和玩手机次数明显增多可能与课程内容节奏有关。建议在该时段增加互动提问或切换教学活动形式以维持学生注意力。”这里一个重要的经验是不要问DeepSeek“你觉得这节课怎么样”因为语言模型会脑补出很多主观内容甚至给你写出学生表情细节。给它结构化数据限定边界它才像一个可靠的工作助手。4.4 失败降级与成本控制DeepSeek接口如果超时或返回异常系统不能白屏。我写了一个兜底逻辑生成一段模板式小结把各项行为计数按从高到低排序挑出数量最多的前三项再加上一条预设建议。这样即使接口完全不可用教师端也能看到基本结论。调用频率也要控制。我设置了一个简单缓存同一个摄像头同一节课的小结生成完后10分钟内不会重复调用只有当统计数据和上一次有明显变化时才重新请求。这样既减少调用成本也避免界面频繁跳动。成本控制方面我限制max_tokens为300temperature固定0.3输出内容越短越稳。实测一节课的小结大约消耗几百个token成本可以忽略不计但如果项目并发量大还是建议加上调用频率限制和月度预算提醒。5. 可视化界面的打磨5.1 页面布局一张图看懂整个课堂状态整个Web界面由一个主模板控制。页面顶部是导航栏和当前时间中央左侧是摄像头实时画面旁边是最近一小时的检测记录滚动列表右侧是统计卡片区下方是行为趋势曲线。统计卡片我设置了四块在线人数、整体参与度、举手次数、疑似玩手机人数。每张卡片同时展示当前值与环比变化比如“玩手机 5 次较上一时段下降20%”。这些数字直接来自/api/summary/接口。界面风格用的是纯HTML加CSS没有引入重型前端框架。好处是加载快、不依赖CDN坏处是图表和组件都要自己接。为了方便后续维护我单独抽了一个static/js/dashboard.js所有请求和页面渲染都集中在这里。5.2 数据刷新与图表联动页面轮询后端接口可以做到近实时的数据更新。我用了一个简单的定时器setInterval(async () { const res await fetch(/api/summary/?scoperealtime); const data await res.json(); updateCards(data.summary); updateChart(data.trend); }, 3000);每3秒轮询一次对内部系统来说足够流畅也不会给Django和数据库造成太大压力。如果使用WebSocket会让数据更实时但引入Django Channels加Redis会把部署复杂度提高不少对这套系统来说不划算。图表我用的开源库接到一个div容器里每次更新只追加最新时间点保留最近30个点超过就移除最老的。关键坑是图表数据顺序要和后端返回的时间戳一致否则曲线会来回横跳。我在Django接口里显式按时间升序排序前端不再处理排序逻辑。5.3 实时推流展示的取舍实时画面推流是整个项目里最容易遇到的性能瓶颈。演示阶段可以用Django生成MJPEG流核心视图如下def video_feed(request): def gen(): while True: frame latest_frame_queue.get() ok, jpeg cv2.imencode(.jpg, frame) yield (b--frame\r\n bContent-Type: image/jpeg\r\n\r\n jpeg.tobytes() b\r\n) return StreamingHttpResponse( gen(), content_typemultipart/x-mixed-replace; boundaryframe )这种方式实现简单浏览器直接放img src/video_feed/就能显示。但要注意它会把大量画面连续推给浏览器多人同时打开时CPU和带宽压力剧增。我的处理是给推流加一个开关默认页面只显示静态快照教师点击“开始直播”后才真正建立MJPEG流。静态快照每5秒刷新一次对90%的监控场景都够用实时推流只留给需要连续观察的老师。6. 部署后的调优与问题排查实录6.1 生产部署要点开发环境下用runserver完全没问题但交付给别人跑时要调整几个地方。第一用gunicorn启动Django不要用runserver否则性能和稳定性都会出问题。命令大致是gunicorn classroom.wsgi:application -w 2 -b 0.0.0.0:8000 --timeout 120进程数设2就够没必要开太多因为推理线程本身已经占CPU。第二静态文件用WhiteNoise或Nginx托管。第三摄像头推理线程不要和Gunicorn worker混在一起否则每个worker都会启动一次推理线程CPU直接爆掉。我用环境变量DJANGO_PROCESS_WORKER1单独跑了一个推理进程Web进程只负责查数据库和渲染页面。如果条件允许把模型推理放到独立服务里通过Redis或HTTP接口给Django提供检测结果这样Web服务和推理服务可以分开扩容但现阶段演示场景没必要。6.2 常见问题速查表把我在开发中遇到的典型问题整理成一张表方便后面参考现象原因处理方法检测框把整面墙框出来背景误检置信度阈值太低把conf从0.25提高到0.35观察误报率玩手机和低头互相混淆类别定义不够清晰检查标注规范给玩手机类别增加手机特写样本ONNX模型加载失败导出时opset版本与onnxruntime不兼容指定opset12重新导出或升级onnxruntime登录之后CSS样式丢失Django静态文件路径配置错误collectstatic后确认STATIC_URL和STATIC_ROOT设置正确fetch请求返回302后页面卡死session过期导致重定向前端统一拦截302跳回登录页多用户同时打开推流页面卡顿MJPEG流重复编码且带宽被占满改为用静态快照手动开启直播开关DeepSeek接口偶尔超时网络波动或服务端限流增加重试和降级模板不要影响主页面数据库记录增长过快原始检测事件逐条写入改用bulk_create且只保留最近3天明细摄像头画面变成灰屏视频读取被阻塞或线程崩溃在推理循环里加异常捕获和自动重连逻辑6.3 一个容易被忽略的经验不要把识别结果当“判决”最后这部分可能是整个项目里最重要的一条经验。课堂行为检测系统本质上是一个统计工具它反映的是群体趋势不是个体评价。系统里检测到某个学生“低头”或者“趴桌”很可能只是在思考题目或者身体不适模型不可能百分百理解上下文。所以在产品设计上我刻意做了两个限制。对外展示只提供“全班各类行为次数统计”不提供单个学生的连续追踪报告DeepSeek生成小结时明确要求使用客观语气只用“该时段趴桌次数较多”这类中性描述避免“这个学生上课睡觉”这种个人指向性极强的输出。我给老师端加了一个提示条“检测结果仅用于辅助教学观察不应用于学生个别评价。”这个细节在实机演示时获得了不少认可也让系统更容易通过学校的合规审核。实际操作中我的体会是这类项目最难的不是把检测精度提到多高而是数据怎么组织、结果怎么呈现、以及怎么让使用者建立合理的预期。YOLOv11、Django、DeepSeek三个组件技术上都不是新东西组合在一起后把每个环节的边界画清楚项目才能真正落地进真实的课堂环境。如果重新做一遍我大概率会先把类别定义和统计口径写成文档再动工写代码。后面训练模型和写后端时这一份文档省掉了很多返工时间。这个经验也分享给正在做类似项目的人先把“什么行为算数、什么不算数”定死再谈模型和界面。