
简介面向课堂教学行为分析场景一套已标注的学生低头、转头目标检测数据集可直接用于YOLO系列模型的训练与评估。数据共约2400张图像标注类别为低头、转头两类并已划分好训练集与验证集降低预处理成本适合教育场景智能化开发者、算法入门者以及希望改进YOLO检测效果的工程师。包体包含2000个文件以1999个txt格式的标注文件为主每个txt对应一张图像的类别与边界框坐标信息另有1个python可视化脚本可快速预览标注效果整体压缩包约298.38MB。该资源已有130人学习下载借助标注数据和验证划分可快速训练学生行为检测模型并配合作者提供的YOLOv5改进实战专栏及主页相关项目便于进一步做模型优化、消融实验和场景适配扩展。1. 学生低头、转头行为检测2400张YOLO标注数据到底能做什么学生在课堂上的低头与转头是最容易被摄像头捕获、也最能反映课堂参与度的两类行为信号。所谓学生上课低头、转头行为检测就是通过图像目标检测模型在教室画面中实时框出头部区域并打上标签再由上层逻辑统计成低头时长、转头频次这些可量化指标。这里的2400张已标注图像是整个方案的地基它决定了一个小团队能不能在不对数据做大规模补标的情况下用YOLO系列模型快速训练出可用的行为识别器。适合两类人往下读一类是做课堂分析产品、要把行为指标落到实处的算法工程师另一类是手握教室摄像头视频、正在纠结要不要自己标数据的人。2. 读懂2400张数据的标注结构YOLO txt每行数字的含义与类别定义拿到这类数据集第一件要做的事不是急着训练而是把标注文件逐行读明白。YOLO标注格式下每张图像对应一个同名txt文件放在labels目录里txt里每一行代表一个目标实例。格式看起来简单但坐标做了归一化直接决定了后续data.yaml怎么写、可视化脚本怎么写以及训练报错时该往哪排查。2.1 标注格式逐字段拆解归一化坐标让数据与分辨率解耦打开一个txt你会看到类似0 0.5123 0.3845 0.0831 0.1582这样的行。五个数字的含义分别是类别ID、目标框中心点的x坐标、中心点的y坐标、框的宽度、框的高度其中坐标全部是0到1的归一化值不是像素绝对值。所谓归一化是指这些数字都是相对图像宽高的比例和图像原始分辨率没有关系。这个设计带来的直接好处是同一份标注可以喂给任意输入分辨率。训练时用640后续想用960或1280做推理不需要重新标注缩放图像时坐标会跟着等比换算。我在拿到一批新标注时会先写一个小脚本把所有txt扫一遍确认格式和类别分布没有明显异常import os label_dir labels/train class_count {} total_boxes 0 bad_files [] for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname), r) as f: lines f.readlines() for line in lines: parts line.strip().split() if len(parts) ! 5: bad_files.append((fname, line)) continue cls int(parts[0]) class_count[cls] class_count.get(cls, 0) 1 total_boxes 1 print(各类别目标数:, class_count) print(总目标框数:, total_boxes) print(格式异常文件:, len(bad_files))逻辑说明先用len(parts) ! 5过滤掉字段数不对的行再按类别ID统计目标数量。这一步能同时暴露两类问题——脏数据和类别分布严重失衡。如果两个类别的目标数相差三五倍以上训练时模型会偏向多数类后续要考虑调loss权重或补标。参数说明cls就是类别ID必须和data.yaml里names的顺序严格对应。如果这批数据约定0是低头、1是转头那么names第一项必须是head_down、第二项必须是head_turn。顺序写反模型训练出来就是两个类别互相调换的废品而且指标还看不出任何异常。2.2 目录组织与训练/验证划分小数据集的切分脚本和泄漏坑数据集的目录组织常见做法是images和labels分开放各自再按train和val划分。2400张属于中小偏小的规模验证集切多了训练样本不足切少了验证指标波动大。我一般按9:1左右划分训练集约2160张、验证集约240张。如果你拿到的原始数据已经是整理好的标准目录跳过切分脚本直接用如果是散装文件下面这个脚本可以快速完成划分import os import random from shutil import move random.seed(42) os.makedirs(images/train, exist_okTrue) os.makedirs(images/val, exist_okTrue) os.makedirs(labels/train, exist_okTrue) os.makedirs(labels/val, exist_okTrue) images sorted(os.listdir(images/all)) random.shuffle(images) split_idx int(len(images) * 0.9) for i, fname in enumerate(images): part train if i split_idx else val base os.path.splitext(fname)[0] move(fimages/all/{fname}, fimages/{part}/{fname}) move(flabels/all/{base}.txt, flabels/{part}/{base}.txt)逻辑说明先把所有图像路径读进来按9:1切分后图像和对应的txt一起移动。random.seed(42)保证每次运行切分结果一致方便复现。参数说明这个随机切分有个隐蔽的坑——如果原始数据是同一段视频里连续抽帧来的相邻帧内容几乎一样随机切分会导致训练集和验证集存在大量重复画面验证指标虚高。遇到这种情况要按视频片段切分而不是按单帧切分比如把前70%的视频帧都归train、后30%归val再补一点不同教室的数据做验证。切分完成后data.yaml的写法如下path: ./dataset train: images/train val: images/val names: 0: head_down 1: head_turn注意YOLOv8会自动从names的长度推断nc不写也能跑。但如果你的ultralytics版本偏旧建议补上nc: 2不报错也不影响结果。2.3 标注质量静态核查先用可视化脚本抽检再决定要不要补标标注质量决定了训练上限。2400张图在训练前值得先做一轮人工抽检否则后期返工成本更高。我通常每50张抽1张把图像和对应txt框叠加画出来用OpenCV直接可视化import cv2 img_path images/train/IMG_0001.jpg label_path labels/train/IMG_0001.txt img cv2.imread(img_path) h, w img.shape[:2] with open(label_path, r) as f: for line in f: parts list(map(float, line.strip().split())) cls, cx, cy, bw, bh parts x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) color (0, 0, 255) if int(cls) 0 else (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, str(int(cls)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) cv2.imwrite(check.jpg, img)逻辑说明把归一化坐标还原成像素坐标并绘制检测框。人工看画框结果重点检查三件事框是否贴合头部、有没有大面积漏标、类别ID是否符合直觉。如果抽检100张里有超过5张明显有问题宁可把问题样本挑出来修也不要直接训练。参数说明还原坐标最容易出错的地方是x1和y1要用中心点减半宽/半高而不是直接用cx乘w。cls只决定画框颜色红色是0类、绿色是1类这跟模型无关纯可视化习惯。3. 用YOLOv8把行为检测跑起来环境安装、训练命令与五个必调参数数据就位后训练本身不复杂。Ultralytics YOLOv8是目前跑通这类小数据集最快的路径安装、写配置、执行训练三步就能看到第一条loss曲线。选它不是因为一定比YOLOv5指标高而是生态太顺报错信息直观对2400张规模的课堂场景特别友好。3.1 用conda搭好环境Python版本、依赖与首次推理验证常见做法是用miniconda建一个独立环境避免污染系统Python。在普通PC、工作站或AGX Orin这类边缘设备上都能跑但Python版本不要低于3.9torch和ultralytics的依赖在3.8以下经常出现兼容性问题conda create -n yolo_class python3.10 -y conda activate yolo_class pip install ultralytics逻辑说明conda环境解决的是多项目依赖隔离。之前踩过的坑是系统Python里已经装了旧版numpy和opencv直接pip装ultralytics会把整个环境的依赖版本搅乱。独立环境建好后所有yolo相关包装在新环境里互不影响。参数说明python3.10是目前ultralytics全功能支持比较稳的版本。ARM架构平台比如AGX Orin上pip会自动拉取对应平台的torch版本不需要手动处理CUDA。环境装好后最快验证方式是拿一张课堂图片跑一次推理from ultralytics import YOLO model YOLO(yolov8n.pt) results model(test.jpg) print(len(results[0].boxes))逻辑说明用官方预训练权重跑一次推理没报错并输出了检测框数量说明依赖链是完整的。这一步把环境问题和数据问题分开后面训练报错时能更快定位方向。3.2 写data.yaml与训练命令行imgsz、batch、epochs、patience、mosaic环境就绪后直接用ultralytics的CLI训练。它会自动读取data.yaml下载基础权重然后开始训练循环yolo detect train \ modelyolov8n.pt \ datadataset/data.yaml \ imgsz640 \ epochs100 \ batch16 \ patience20 \ mosaic0.5 \ projectruns/train \ nameclass_behavior逻辑说明model指定预训练权重yolov8n是从COCO上训好的基础版本用它做起点比随机初始化收敛快得多。data指向data.yamlultralytics会根据path字段拼接图像和标签的相对路径。参数说明imgsz是训练输入分辨率640对课堂场景是保守选择。如果摄像头是1080P或更高后排学生的头部在画面里往往只有30到50像素这时把imgsz提到960甚至1280小目标召回率会有明显提升代价是显存占用和单epoch时间上涨约30%到50%。batch是单批次图像数16在12G显存下跑n模型是安全的8G显存就降到8或4收敛会慢一点但不是不能训。epochs100对2400张的小数据集是合理设置但要不要训满100轮主要看patience什么时候触发。patience20的意思是验证集指标连续20轮没有提升就提前结束。mosaic是默认开启的数据增强把四张图拼成一张训练。小数据集上容易让模型学到过于激进的拼接特征我一般会降到0.5。如果训练中发现验证指标抖动得很厉害把这个参数再往下降或者干脆关掉。提示如果你用的是8G显存的老卡batch降到8同时把数据加载的workers设成2避免磁盘I/O拖慢训练。3.3 训练日志与损失曲线box_loss、cls_loss和dfl_loss怎么看训练时终端会滚动输出每一轮的GPU内存、损失值和mAP。真正要盯的是验证集上的mAP50和mAP50-95以及三条loss的走势。YOLOv8的损失函数是三项加权组合box_loss负责回归检测框的位置和大小误差cls_loss负责类别判断误差dfl_loss负责边界框的分布建模让模型在框边缘附近也能输出较平滑的坐标估计。看曲线时注意三点只要mAP50-95在涨单轮抖动不用管cls_loss下降慢是正常的因为低头和转头两个类别本身非常相似特征区别只在头部角度和视线方向如果训练后期val loss开始反弹而训练loss还在降说明过拟合已经开始了这时候提前停止或降低增强强度比硬加数据更有效。训练结束后runs/train/class_behavior/weights/下会生成best.pt和last.pt。best.pt是验证集指标最好的那一轮权重后面做推理和部署都该拿它。4. 行为检测训练避坑四条从实践中踩出来的经验这章写的是我在课堂行为检测上遇到的最典型的坑。每条都按现象、原因、解决三步走。能看到现象描述是因为自己也翻过车这些经验比任何调参技巧都值钱。4.1 后排小目标全漏检mAP不低真实场景却框不住人现象训练完验证集mAP50达到0.85看起来效果不错。但拿另一间教室的实拍视频测试时后排学生几乎全部漏检检测框密集地落在前三排。原因训练数据里后排样本本身就少或者后排人头在640分辨率下只有约25到35像素。模型没见过足够多的小尺度正样本自然学不到对应特征。验证集指标好看是因为验证集跟训练集同源小目标比例一样稀缺mAP被前排样本撑高了。解决优先把imgsz提到960或1280重训一轮做对比。其次用切片推理工具比如SAHI这种思路把原图切成重叠块分别检测再合并。还有一种做法是统计训练集中小目标占比如果面积小于32×32像素的框占比不足5%人工补标100到200张后排图像这往往比调整网络结构更直接。4.2 低头和转头互相混淆边界样本决定了类别边界现象训练日志里的混淆矩阵显示head_down和head_turn之间有10%到15%的互相误判。实际观察视频发现模型把低头看手机的侧脸频繁标成转头。原因低头和转头在视觉上存在大量过渡姿态。比如头低下去但眼睛看向侧边或者转头时只有颈部偏转、身体没动标注员面对这类边界样本时往往凭感觉打标导致同一个姿态在不同图像里可能被标成两个不同类别。模型学到的类别边界是标注员主观边界的平均系统性偏差就这么来的。解决先统计全量标注里两个类别的框数如果数量差距过大说明标注口径已失衡。再抽看被标为不同类别但视觉特征接近的样本统一口径。更稳妥的做法是把分类损失的权重调高一点比如把默认的cls系数翻倍让模型更重视区分这两个容易混淆的类别但根本上还是要统一标注标准。4.3 loss曲线先降后升2400张小数据集的过拟合信号现象训练到第30轮左右训练集的box_loss持续走低验证集的box_loss开始反弹但mAP50还在缓慢上升。到第50轮验证loss已经比第20轮高出不少。原因2400张图像对YOLO来说不算多。模型早期学到的是通用特征后期开始记住训练图像里的背景和噪声模式比如特定教室的课桌椅颜色、窗户光线。验证集和训练集如果来自同一间教室这种记忆行为不会立刻让mAP崩盘但换到新教室就现原形。解决不要盲目训满100个epoch用patience提前结束是最简单的后悔药。如果确认过拟合我把mosaic降到0甚至关掉同时适当提高颜色增强参数让模型没法稳定记住背景色。如果条件允许能凑到4000到5000张并覆盖多间不同教室过拟合问题会自然缓解。4.4 训练报错loss为nan问题绝大多数出在标注文件里现象训练跑到某个batchloss输出nan后面所有指标全是nan训练直接报废。原因最常见的情况是标注文件里有宽度或高度为0的框或者坐标值大于1。这些通常是导出程序向上取整出错或者手动编辑txt时多打了一个小数点。如果txt里有空行或者只有类别ID没有坐标的行也会让损失计算遇到非法值。解决训练前用脚本把整个labels目录扫一遍定位到非法样本所在文件和行号import os for fname in os.listdir(labels/train): path os.path.join(labels/train, fname) with open(path) as f: for line_idx, line in enumerate(f.readlines()): parts line.strip().split() if len(parts) ! 5: print(f{fname}:{line_idx} 字段数异常) continue cls, cx, cy, bw, bh map(float, parts) if bw 0 or bh 0: print(f{fname}:{line_idx} 宽高异常 bw{bw} bh{bh}) if not (0 cx 1 and 0 cy 1): print(f{fname}:{line_idx} 中心坐标越界)逻辑说明脚本在训练前跑一遍把所有非法框全部暴露出来。定位到具体文件和行号后删除该样本或修正坐标即可。参数说明坐标越界的判定条件0 cx 1和0 cy 1是YOLO格式的硬性规范。如果发现少量越界是把0.9写成了9.9这类笔误直接修正坐标如果是宽高为0多半是标注时框选操作失误那张图建议整个样本重标。5. 从检测框到课堂指标模型规模取舍、推理脚本与统计逻辑训练出best.pt只是第一步。课堂行为检测真正落地的难点在于摄像头输出的是连续视频流检测框本身不能直接作为行为指标中间还有模型选型、阈值设定、结果聚合这三层逻辑。5.1 模型规模怎么选n、s还是m以及边缘部署的导出路径先看硬指标YOLOv8n参数量约300万s约1100万m约2500万。对课堂行为检测这类只有两个类别、目标尺度相对集中的任务n在常见GPU上推理一张640分辨率图片只需几毫秒在边缘设备上也能跑到实时。s精度提升有限但显存和延迟翻倍。我的经验是如果摄像头分辨率是1080P且学生数在30人以内用n做初版完全够用如果要覆盖大教室且后排占画面比例小就上s并配imgsz1280训练。m和更大版本在2400张的小数据集上收益不明显反而过拟合得更快。模型参数量640分辨率单帧延迟参考适用场景YOLOv8n约3M低小教室、实时互动提醒YOLOv8s约11M中大教室、1280推理分辨率YOLOv8m约25M较高不推荐小数据容易过拟合之后如果要把模型部署到AGX Orin这类边缘设备常见做法是先用yolo export modelbest.pt formatonnx导出ONNX再转成TensorRT engine格式推理延迟会比直接跑PyTorch低一截。5.2 推理脚本与阈值过滤把best.pt变成标签流训练收敛后用best.pt做推理只需要几行代码。这里要做两个工程化处理一是用confidence阈值过滤低质量框二是把类别ID映射为可读标签from ultralytics import YOLO model YOLO(runs/train/class_behavior/weights/best.pt) results model.predict( sourceclassroom.mp4, imgsz960, conf0.35, iou0.5, verboseFalse ) for frame_idx, r in enumerate(results): for box in r.boxes: cls int(box.cls[0]) conf float(box.conf[0]) label head_down if cls 0 else head_turn print(f帧 {frame_idx}: {label} 置信度 {conf:.2f})逻辑说明model.predict直接接受视频路径并逐帧推理r.boxes里包含当前帧的所有检测框。框的cls给出类别索引conf给出置信度。映射成标签后就能进入后续的行为统计逻辑。参数说明conf0.35是经验值。课堂场景误检主要来自投影屏幕上的文字和后排杂物误检多就上调到0.45如果出现大量漏检说明阈值偏高。iou0.5是NMS阈值决定两个重叠框是否合并。课堂场景一个人只应该有一个框保持默认的0.5足够。imgsz在推理时可以比训练时更高比如训练用640、推理用960能缓解小目标漏检但帧率会下降实时性要求高的场景要权衡。5.3 从单帧到统计检测框如何变成低头时长和转头频次单帧检测结果没有时间维度没法回答这节课学生的状态怎么样这类问题。我常用的做法是在推理循环外面加一个基于时间窗口的统计层按窗口聚合输出from collections import defaultdict window_stats defaultdict(lambda: {down_frames: 0, turn_frames: 0, total_frames: 0}) def update_window(frame_idx, label, window_id0): stats window_stats[window_id] stats[total_frames] 1 if label head_down: stats[down_frames] 1 elif label head_turn: stats[turn_frames] 1 if stats[total_frames] 300: down_ratio stats[down_frames] / stats[total_frames] turn_ratio stats[turn_frames] / stats[total_frames] print(f窗口 {window_id}: 低头占比 {down_ratio:.2f}, 转头占比 {turn_ratio:.2f}) window_stats[window_id] {down_frames: 0, turn_frames: 0, total_frames: 0}逻辑说明用固定帧数作为聚合窗口输出窗口内低头帧占比和转头帧占比。这种统计方式能平滑逐帧的检测抖动。如果要做单人行为分析还需要用IoU匹配或ByteTrack这类跟踪器把每一帧的框关联到具体座位或个人这是从画面统计升级到个人分析的关键一步。参数说明total_frames 300 是按25fps帧率算出的12秒窗口具体数值要按你的实际帧率调整。想要更快反馈就把窗口缩到150帧想要更稳的复盘数据就用600帧。6. 进阶用法检测加时序平滑把行为判断做得更贴近课堂单帧模型能输出检测框但老师真正关心的是这个学生低头了多久和转头是否频繁。落到真实课堂里单帧检测结果直接被当结论用会显得很不稳定因为模型偶尔会把低头看书的瞬间误判成转头也会因为遮挡漏掉一帧。需要一个轻量的时序记忆系统来兜底。一个简单实用的方案是用滑动窗口队列平滑帧级决策。维护一个长度为30的标签队列只有当低头标签在队列里占比超过六成时才判定为一次低头事件开始占比低到两成以下时判定事件结束。比起对单帧做阈值判断误报会少非常多from collections import deque label_history deque(maxlen30) def smooth_decision(label): label_history.append(label) if label_history.count(head_down) 18: return head_down if label_history.count(head_turn) 18: return head_turn return unknown这个做法的实质是给检测结果加了一道低通滤波不改模型结构把训练好的best.pt当黑匣子用就能明显提升后端指标的稳定性。队列长度和阈值按帧率调整30帧对应25fps下约1.2秒这个粒度刚好能过滤短促误检又不会吞掉持续1秒以上的真实行为。另一个进阶方向是引入座位区域划分。如果摄像头位置固定先在画面里人工圈出每个座位区域再把检测框中心点与区域做包含关系匹配。只要某个座位区域内低头框占比持续超过阈值就记录为该座位的一次低头事件。这个思路比起整帧统计更能回答谁在低头、低了多少次这类班主任真正关心的问题。我自己的教训是第一次做课堂行为统计时把全部精力花在了模型精度上mAP已经很高但后端反馈还是不准。后来才意识到老师看到的不是单帧框而是某个人在某个时间段是否频繁低头。这个语义比单帧检测框粗放但恰好需要时序聚合才能实现。模型指标是很好的起点但不是终点。如果你手头也有一套2400张左右的已标注数据先把第2章的数据清洗脚本跑一遍再按第3章的命令训练一轮最后把时序平滑接上基本就能得到一套可用的课堂行为检测原型。希望帮到你。本文还有配套的精品资源点击获取