课堂行为数据集双格式解析:从VOC到YOLO的完整训练指南
简介面向目标检测与课堂行为分析场景的数据集收录671张课堂实景图像包含玩手机、阅读、写字、低头、睡觉、举手6类行为框标注适合用于监督学生课堂状态、构建智慧课堂行为识别模型。资源同时提供Pascal VOC与YOLO两种格式标注图像、xml、txt一一对应使用LabelImg手工画框共19768个实例框。压缩包内共2000个文件以jpg图片、xml标注文件、txt标签文件为主整体约189.74MB解压后可直接接入主流检测框架训练。目前已有401人学习下载。数据集中不同行为类别样本量差异明显阅读、玩手机等类别框数充足举手类别仅6框可进一步研究长尾类别检测、样本均衡或数据增强策略。整体标注尺度统一、定位准确既适合目标检测初学者快速上手也可为课堂行为分析算法研发提供可靠训练数据。1. 课堂行为数据集671张双格式标注先别急着训练做过课堂行为检测的人都知道最耗时间的不是模型调参而是标注数据。拿到一份现成的671张Pascal VOCYOLO双格式课堂行为数据集意味着你可以直接跳过 labelImg 画框那几周时间把精力放在训练和迭代上。这份数据集包含了playphone、reading、write、bowhead、sleep、risehand六类行为共 19768 个矩形框其中 playphone 和 reading 的框数都接近八千而 risehand 只有 6 个框——类别极度不平衡这就是第一个要正视的问题。适合谁一是做学生专注度、课堂纪律识别相关项目的研究生二是要在 YOLOv8 / YOLOv5 上快速跑通 baseline 的算法工程人员三是想验证数据增强和难例挖掘策略的人。本文会从文件结构、格式转换、训练配置到踩坑记录做一次完整拆解。2. 数据集内部结构labelImg 产出的两种格式如何一一对应2.1 解压后的目录与文件构成这份资源打包为7z压缩包解压后不再套多层目录。我用7z x解压到classroom_behavior目录得到的是一排并排的jpg、xml、txt文件而不是常见的images/labels子目录结构。看图名是firc_kt_xxx.jpg这种统一编号共 671 组即每张图配套一个 VOC 的xml文件和一个 YOLO 的txt文件。先列一下核心文件种类文件后缀数量作用.jpg671原图尺寸不统一需要训练时统一 resize.xml671Pascal VOC 标注记录对象名和xmin, ymin, xmax, ymax.txt671YOLO 格式标注每行是一个类别名和归一化后的中心点坐标没有 segmentation 相关的 txt说明这份数据只做检测不涉及分割任务。txt文件是 YOLO 官方训练时直接读取的格式如果后续用ultralytics框架只需要把图片和 txt 分别放到images和labels两个目录下即可不需要再做任何转换。但要注意当前所有文件混在同一层训练前最好写个脚本按比例拆分训练集和验证集而且拆的时候必须图、xml、txt 三个文件同步移动否则训练时找不到对应标注。标签类别从xml里读取后我确认了一下顺序。VOC 格式里每个对象名是英文字符串YOLO 格式里每个对象对应一个整数类别 id而这个 id 的顺序取决于你如何定义类别列表。这份资源的真实标注名称为[playphone,reading,write,bowhead,sleep,risehand]也就是把playphone当作 id 0依此类推。常见的一个坑是不同人整理数据时类别顺序不一致可能导致训练时模型把playphone当成reading。拿到资源后先读一遍所有 xml确认类别名和你要训练的顺序一致。2.2 VOC 绝对坐标与 YOLO 归一化坐标的换算逻辑VOC 的xml里记录的是绝对像素坐标而 YOLO 的txt里记录的是归一化相对坐标。两者换算的核心公式很简单对于给定图片宽度W和高度H# VOC - YOLO x_center (xmin xmax) / 2 / W y_center (ymin ymax) / 2 / H box_width (xmax - xmin) / W box_height (ymax - ymin) / H这份数据集里的xml和txt是同一标注工具 labelImg 同时导出的理论上完全一致但我还是建议写一个校验脚本跑一遍防止打包或传输过程中出现文件错位。校验思路是从xml算出x_center等数值再和txt里的四列浮点数做对比允许 1e-3 的浮点误差。下面这段代码是我常用的校验方式import glob import xml.etree.ElementTree as ET xml_files glob.glob(classroom_behavior/*.xml) txt_files glob.glob(classroom_behavior/*.txt) print(xml数量:, len(xml_files), txt数量:, len(txt_files)) for xml_path in xml_files: txt_path xml_path.replace(.xml, .txt) tree ET.parse(xml_path) root tree.getroot() img_width int(root.find(size/width).text) img_height int(root.find(size/height).text) voc_boxes [] for obj in root.findall(object): name obj.find(name).text xmin int(obj.find(bndbox/xmin).text) ymin int(obj.find(bndbox/ymin).text) xmax int(obj.find(bndbox/xmax).text) ymax int(obj.find(bndbox/ymax).text) x_center (xmin xmax) / 2 / img_width y_center (ymin ymax) / 2 / img_height box_w (xmax - xmin) / img_width box_h (ymax - ymin) / img_height voc_boxes.append((name, x_center, y_center, box_w, box_h)) with open(txt_path, r) as f: yolo_lines f.readlines() if len(voc_boxes) ! len(yolo_lines): print(f数量不一致: {xml_path}) continue for voc_box, yolo_line in zip(voc_boxes, yolo_lines): parts yolo_line.strip().split() yolo_cls int(parts[0]) yolo_vals list(map(float, parts[1:5])) # 只比较坐标数值类别名先在统计阶段校验 if abs(voc_box[1] - yolo_vals[0]) 1e-3 or abs(voc_box[2] - yolo_vals[1]) 1e-3: print(f坐标不一致: {xml_path} {voc_box[0]} vs {parts})这段脚本的逻辑很直接用ElementTree读取 xml 的尺寸和边界框换算成 YOLO 格式后与 txt 对比。如果某个 xml 的对象数量和 txt 行数对不上说明标注文件有缺失或多余如果坐标差超过 1e-3说明存在轻微量化误差正常情况不会出现。我跑下来 671 份全部通过说明数据本身干净。值得说明的一点是关于txt文件里类别 id 的顺序脚本里暂时只比较坐标类别映射单独处理。建议你在训练前把每个txt的类别 id 分布统计一遍确认与 xml 的类别名分布一致。因为 labelImg 导出 txt 时需要依赖一个classes.txt或者预先定义的标签顺序不同电脑导出顺序可能不同所以在别人机器上标注的数据课堂上老师给的时候往往没有附classes.txt这时候就要靠统计去反推。3. 拆分训练集与验证集写脚本保证图、xml、txt 三件套同步3.1 按比例拆分的自动脚本YOLO 训练前必须把数据拆成 train/val 两个集合。如果不拆模型会在训练集上直接推理验证指标完全失真。这里我不用随机 shuffle 后手动拖文件而是写一个 Python 脚本用random.seed固定随机种子保证每次拆完的结果可复现。关键逻辑是先把三套文件路径绑定成元组再对元组列表做 shuffle最后按比例切片并把文件移动到对应的目录。import os import random import shutil import glob random.seed(42) # 固定随机种子反复拆结果一致 base_dir classroom_behavior images glob.glob(os.path.join(base_dir, *.jpg)) data [] for img_path in images: stem os.path.splitext(os.path.basename(img_path))[0] xml_path os.path.join(base_dir, stem .xml) txt_path os.path.join(base_dir, stem .txt) if os.path.exists(xml_path) and os.path.exists(txt_path): data.append((img_path, xml_path, txt_path)) else: print(缺少配套文件:, stem) random.shuffle(data) val_ratio 0.2 val_count int(len(data) * val_ratio) val_data data[:val_count] train_data data[val_count:] for split_name, split_data in [(train, train_data), (val, val_data)]: img_dir fdataset/{split_name}/images label_dir fdataset/{split_name}/labels os.makedirs(img_dir, exist_okTrue) os.makedirs(label_dir, exist_okTrue) for img_path, xml_path, txt_path in split_data: shutil.copy(img_path, os.path.join(img_dir, os.path.basename(img_path))) shutil.copy(txt_path, os.path.join(label_dir, os.path.basename(txt_path))) # xml 也可以一并保留方便后续逆向检查 shutil.copy(xml_path, os.path.join(split_name _xml, os.path.basename(xml_path)))这里我把xml复制到train_xml/val_xml目录虽然 YOLO 训练时用不到但后续做格式转换或者人工检查时有用。xml是地面真值的最初来源txt 只是它的派生品保留 xml 等于留了一条后悔药路径。如果最后发现某个 txt 有问题可以直接从 xml 重新生成。3.2 为什么我不直接改文件名而不动内容很多同学拿到资源后第一件事就是改图片文件名比如加上前缀、去掉firc_kt_。我不建议这么干因为 xml 和 txt 内容里都没有记录文件名标注靠的是「同名的三个文件」来关联。你一旦改了图片名却忘了同步改 xml 和 txt 的文件名labelImg 能打开那套老的 xml但你的训练脚本会由于找不到 txt 而报错。上面脚本使用了stem来统一关联只要三个文件在同一目录且 base name 相同就永远不存在错位。这里额外提一下拆分时的正确做法先绑定后拆而不是把图片列表 shuffle 后分别去 glob 对应的标注。后者如果你没有做幂等校验很容易因为一个小写扩展名差异造成图片有标注、标注没有图片的情况。我这套脚本还做了一个防御对每个图片都检查对应 xml 和 txt 是否存在缺失的 print 出来不加入数据集。这是从源头避免训练时「image not found」的报错。3.3 验证集里每类至少有几张看类别分布拆完训练集和验证集后还应该统计验证集里每个类别的框数量。尤其是risehand只有 6 个框随机拆分时极有可能全部分到训练集验证集里一个正样本都没有。那验证 mAP 里该类就没有意义甚至计算平均 mAP 时会除零。我写了一个统计脚本输出验证集里每个类别的框数import os from collections import Counter label_dir dataset/val/labels counter Counter() for txt_file in os.listdir(label_dir): with open(os.path.join(label_dir, txt_file), r) as f: for line in f: cls_id int(line.strip().split()[0]) counter[cls_id] 1 class_names [playphone, reading, write, bowhead, sleep, risehand] for idx in range(len(class_names)): print(f{class_names[idx]}: {counter.get(idx, 0)} 框)如果你跑下来发现risehand在验证集里是 0那就必须手动把一张包含risehand的图从训练集挪到验证集。YOLOv8 的 val 阶段会遍历整个验证集图片如果某张图里没有该类标注它并不会报错但该类在最终 mAP 统计里会显示None。这种情况宁可少一张训练图也要保证验证集覆盖全部六个类别。另外还要注意验证集的图片尺寸和原始图比例可能各不相同YOLO 训练时默认会做 letterbox 缩放这个不会影响框的归一化坐标但会影响可视化和混淆矩阵中框面积的可比性。4. YOLO 训练前的配置策略处理类别不平衡和超参数4.1 重新组织目录并写 data.yaml用 YOLOv5 或 YOLOv8 训练都需要一个data.yaml来描述类别和路径。这份数据集的类别顺序是固定的我直接按原始顺序写死避免随意排序。下面是我的classroom.yaml# 数据集根目录下面有 train/images 和 val/images path: ./dataset train: train/images val: val/images # 类别名必须和 txt 中 id 的顺序一一对应 names: 0: playphone 1: reading 2: write 3: bowhead 4: sleep 5: risehand注意train和val如果是相对路径必须相对于path字段。这个坑我踩过之前把path写错成绝对路径换机器训练时直接失效。建议path使用相对路径整个dataset目录和项目文件放在一起。如果你用的是ultralytics包训练命令一般是yolo train dataclassroom.yaml modelyolov8n.pt epochs100。但这只是启动训练效果好坏全看后续参数调整。4.2 针对六类样本量严重不均的调参思路先看原始统计playphone 6976、reading 7826、write 2984、bowhead 947、sleep 1029、risehand 6。这个比例是夸张的前两类占了四分之三risehand几乎可以忽略。如果不处理模型会倾向把一切检测框都预测为reading或playphone因为整体 loss 被这两类主导。我能想到的处理方式有三条路。第一是直接在data.yaml里给每个类别设置权重YOLOv5 支持loss_gain也就是给cls_loss按类别加权。但 YOLOv8 的官方配置里没有直接暴露每个类别的 weight 参数需要改损失函数源码对新手不友好。第二是简单重采样让每张图在__getitem__里按类别概率被抽样但这也意味着大多数reading图会被丢弃信息损失大。第三是我更常用的不做重采样、不做类别加权而是用数据增强让少数类目标在训练时被裁切、翻转、缩放等效扩大risehand的样本量。如果坚持用 YOLOv8我会把epochs适当增加到 200同时开启mosaic1.0、mixup0.2、flipud0.5。mosaic 会把四张图拼接成一张相当于一次性让模型看到四个不同场景但对小目标risehand来说拼接后目标可能变得更小需要配合scale参数。下面是一个接近实战的启动命令yolo train \ dataclassroom.yaml \ modelyolov8n.pt \ epochs200 \ imgsz640 \ mosaic1.0 \ mixup0.2 \ fliplr0.5 \ flipud0.3 \ scale0.5 \ patience50这里scale0.5表示允许随机缩放变化flipud是我额外开启的垂直翻转因为课堂场景里倒置目标不算违背物理规律值得一试。patience50表示50轮指标不涨就提前结束省时间。当然跑完后你得看各类别的metrics/mAP50(B)如果risehand为 0说明这个类别基本没学到这时候就要去查数据增强后的可视化结果确认小目标是不是被裁切掉了。4.3 用预训练权重还是从头训如果你的显卡显存足够直接用 COCO 预训练的yolov8n.pt做初始权重。COCO 里有 person、cell phone 这类的通用目标对识别playphone和reading有一定迁移帮助。但bowhead低头和risehand举手是姿态类行为COCO 里没有类似标注预训练权重主要提供低层特征提取能力。所以我的建议是用yolov8n.pt作为起点它最大的价值不是知道什么叫bowhead而是已经学会了边缘、纹理、人体部件的通用特征这样收敛速度更快。如果你的任务只是检测头部区域和手部区域甚至可以尝试剪裁掉背景只用包含目标的区域训练。有一个细节在训练前必须明确这份数据集的图片尺寸并不统一。YOLO 训练时会把图片 resize 到imgsz如果原图是横构图而你的输入是正方形会自动填充灰边。这没问题但你要确认每个 txt 里的归一化坐标是相对于原图尺寸还是相对于已经 letterbox 后的尺寸。这份数据集中 txt 的坐标是相对原始 jpg 尺寸计算的训练读取时会先拿到原始宽高再根据imgsz做等比缩放所以你可以放心使用官方 dataloader不需要手动修改坐标。5. 避坑记录我在这个数据集上遇到过的六个典型问题5.1 框数与文件名不匹配导致训练时报错现象训练启动后提示Label file path does not exist或AssertionError: label file no found。原因我一开始图省事手工把一批 jpg 文件移动到train/images但对应的 txt 还在原目录没有同步移动。解决重新执行第 3 章的拆分脚本用程序绑定文件元组杜绝手工移动。如果已经部分移动可以写一个校验脚本遍历 images 目录对每张 jpg 检查同名的 txt 是否存在缺失的列出清单后手工补齐或从原数据重新复制。5.2 打开 xml 后发现类别名带空格或多余字符现象我用xml统计类别时发现原本单一的playphone变成了playphone末尾带空格导致 category 数量变成 7 个。原因标注时不小心输入了一个空格或者不同操作系统的换行符导致解析问题。解决用脚本对所有 xml 里的name做 strip 处理再重新生成 txt。注意这类问题在 YOLO 训练时容易忽略因为 txt 里是数字 id而 id 的映射由类别列表决定如果类别列表顺序和原始类别名不一致就会出现模型把playphone识别为reading的情况。遇到类似问题先打印所有 xml 中的类别集合人工确认后再做映射。5.3 验证集没有升手类别现象训练结束mAP 表格里risehand那一行是None其他类正常。原因第 3 章里随机拆分后整个验证集只有 134 张图其中包含risehand的图片总共 6 张概率上很容易全部落到训练集。解决我单独检查验证集每个标签文件如果没有 id5 的标注就从训练集里手动挑选一张包含risehand的图移到验证集同时移动它的 txt 和 xml。这里需要注意的是risehand的 6 个框分布在多少张图里如果一张图有多个框那么只要移动一张图就够。我在实际操作中挑了一张框数最多的保证验证集至少有 1-2 个正样本。5.4 类别顺序混乱导致训练 label 错位现象训练输出的 loss 很低但val阶段画出的 PR 曲线一团糟查看测试集的预测框发现模型把手机标签标成了写字。原因我使用的 txt 是老师给的而你自己的data.yaml里类别顺序是按playphone在前排的。但是如果这个 txt 是用另一台机器上的 labelImg 导出而 labelImg 的 classes.txt 顺序和你定义的不同就会导致 id 错位。解决最稳妥的方法是用 xml 重新生成 txt确保类别名与 id 的映射严格一致。重新生成的脚本我放在第 2 章用了但这里要再提醒一下不要在原有 txt 基础上只改数字 id要完全从 xml 的bndbox坐标和对象名重新计算。我踩过那个坑后再拿到任何数据集第一步永远是回归 xml 生成 txt除非你能确定原始 classes 顺序。5.5 图片尺寸过大导致 dataloader OOM现象训练启动后爆显存CUDA out of memory。原因默认imgsz640但原始 jpg 尺寸可能很大尤其是一边接近 4000 像素。YOLO 训练时会先读取全图再做 letterbox读图本身的内存开销在高分辨率下变得不可接受。解决我通常在transforms中强制限制输入尺寸建议直接设imgsz480或512同时给 dataloader 加pinned_memoryTrue和workers4。如果仍爆显存降低batch到 8或者改用yolov8n而不是yolov8s。这个数据集本身框多但目标不算小imgsz640不是必须的降到 512 后 mAP 下降一般在 2 个点以内。5.6 6 个正样本无法形成有效梯度现象训练完所有 epoch模型的risehandmAP 为 0或者 loss 里分类损失稳步下降但该类召回率一直为 0。原因6 个框相对于 19768 总框数占比 0.03%在损失函数里该类贡献的梯度被完全淹没。即使不做任何加权反向传播时risehand的损失占比太低模型不会学到有效的区别特征。解决我用的方案是放弃常规随机增强转而针对risehand做“过采样增强”。具体做法在训练集里找出所有包含risehand的 6 张图复制它们每次迭代时随机对复制图做随机裁剪、旋转 15 度以内、HSV 扰动让这些图的变体反复参与训练。这样实际上把risehand的样本量从 6 张等效扩到了几十张。如果你的显存允许还可以把包含risehand的图片单独做成一个小数据集用mosaic0关掉拼接只在这些图上额外微调十来个 epoch。从效果看虽然 mAP 依然低但至少能检测出少数明显的举手动作不会一出框就全丢。6. 最后的进阶习惯训练前强制走一遍数据体检流程这一章我不讲模型结构讲一个每次拿到新数据集都要做的固定流程。它不复杂但能帮你避开上面 90% 的坑。我把它叫“数据体检五步走”。第一步解压后第一时间统计xml里所有类别名而不是直接信任 txt。用set收集所有 name 字段打印再用肉眼和项目需求比对。这一步能发现拼写错误、多余空格和漏标类别。第二步用第 2 章的脚本跑一遍xml和txt的坐标一致性。不要跳过哪怕你已经被告知“双格式已对齐”。数据打包传输过程中txt 被误改、误删的事情太常见了。第三步用第 3 章的拆分脚本建立dataset/train/images和dataset/val/images随后统计验证集的类别框数确保每一个 id 都有正样本。这一步最容易被忽略但恰恰是 mAP 曲线可信度的保证。第四步训练前先可视化 20 张训练图片和 20 张验证图片用 OpenCV 画出框和类别名。看两点框是否落在目标上、框的边界是否明显超出目标边缘。如果发现很多框只框住了手机的一部分可以后续用 DFL 损失或调整坐标损失权重来修正但对这份数据集来说整体标注质量是及格的。第五步训练第一个 epoch 后保存一张验证集的预测图打印第一轮的各类 loss 和 mAP。如果box_loss和cls_loss下降异常快第一轮就低于 0.05大概率是标注缺失或类别映射错位导致模型在走捷径如果 loss 完全不降检查学习率和 batch size。从那以后我每次拿到任何数据集都强制走这五步不再跳过任何一步。尤其是遇到像risehand这种只有 6 个框的类别时不要再天真地期望随机增强能解决问题。先把数据体检做了后面训练心里才有底。希望帮到你——下次解压完别人的7z数据集一定先跑一遍体检再动手。本文还有配套的精品资源点击获取