表情识别数据集详解:从YOLO标注格式到模型训练与评估
简介这份数据集专为目标检测和深度学习开发者打造定位清晰解决表情识别场景下标注数据稀缺、格式不统一的问题。资源采用YOLO与VOC双格式标注覆盖Neutral、Happy、Angry、Fear、Sad、Surprised、Disgust七类常见表情共2504张图片且已按标准比例划分为训练集、验证集和测试集无需再拆分即可直接接入YOLOv5至YOLOv10等系列算法同时兼容Faster RCNN、SSD等模型。压缩包内共2000个文件其中1999个为txt标签文件每个标签记录对应图片的目标框与类别编号另含1个yaml配置文件用于指定类别名称配套图片与xml标签也一并打包整体约90.93MB目录结构直观便于检索。目前已有340人浏览学习。对于想快速验证表情识别模型、进行多算法对比或开展课程设计的研究者来说拿到后可直接着手训练省去了大量标注转换和数据集整理时间。1. 表情识别先定位再分类YOLO 数据集解决的其实是两件事做表情识别的人最开始都会想直接训一个分类网络把图片压缩到 224×224 后塞进 ResNet然后读 softmax。但实际情况往往是摄像头对着教室、门店或会议室画面里很少只有一张正脸有人低头、有人侧身、后排人脸小到分类网络根本没法裁准。这时候真正的问题是目标检测先把人脸定位出来再判断 Neutral、Happy、Angry 这 7 种状态。这份表情识别数据集把两件事都做完整了2504 张图片图片、txt 标签、xml 标签和 data.yaml 全部给到已经按 train/val/test 分好YOLOv5 到 YOLOv10 都能直接跑。与其自己从视频里抽帧再标注不如先拿这份数据把训练链路跑通再针对自己的场景做增量样本。2. txt、xml、yaml 三套标注表情识别数据集的格式边界拿到手先别急着开训。这份数据集最容易忽略的地方是同一个样本同时存在三种不同形态的标注.txt是 YOLO 训练真正会读的标签.xml是 Pascal VOC 风格的目标框描述data.yaml则是告诉 YOLO 去哪里找图片和标签并规定类别顺序。三种文件一旦出现 class name 不一致轻则类别错位重则 mAP 一直很低却看不出原因。所以先用几分钟把文件结构和格式差异搞清楚后面模型训练时才不会背数据格式的锅。2.1 从文件名到目录结构先识别 Roboflow 导出痕迹解压后第一眼看到的全是这种超长文件名youtube-393_jpg.rf.b9b6ad4bf73b28bb415a5a2dc233b106.txt youtube-5_jpg.rf.1cea1c38d29f276f2f86b4707d88ee4e.txt images-36-_jpg.rf.88d648a2d56017e1fb52bbcf2b06a8d7.txtrf.后面的 32 位十六进制串是标注平台的资源指纹youtube-表示样本来自视频帧images-表示来自静态图像集。这种命名在 YOLO 目标检测流程中很常见不要因为文件名长又杂就手动重命名代码靠的是图片和 txt 前缀一致来配对。如果原始包目录因为传输被拆散恢复时只要保证images/train/xxx.jpg与labels/train/xxx.txt文件名相同即可。比较典型的目录结构expression_dataset/ ├── data.yaml ├── images/ │ ├── train/ │ ├── valid/ │ └── test/ ├── labels/ │ ├── train/ │ ├── valid/ │ └── test/ └── annotations/ └── 若干.xml 文件Roboflow 导出的数据集经常把 xml 单独放进annotations而 YOLO 训练并不会读 xml它只认labels下的 txt。这里要记住一个工程结论当 xml 和 txt 同时存在以 txt 为训练依据xml 只是标注源头或给其他检测算法Faster R-CNN、SSD用的格式。如果后续要转成 COCO 或跑 mmdetection再从 xml 重新生成。2.2 YOLO 的 txt 和 VOC 的 xml同一张脸两种坐标语言YOLO 标签每行描述一个人脸框五个字段依次是类别编号、归一化中心点 x、归一化中心点 y、归一化框宽、归一化框高。例如打开某个训练标签head -2 labels/train/youtube-393_jpg.rf.b9b6ad4bf73b28bb415a5a2dc233b106.txt0 0.4864 0.4302 0.3205 0.3618 5 0.8233 0.2266 0.1544 0.1227第一列0对应 yaml 里names[0]的表情没有写字符串名字后面四列全部缩放到 [0,1]与图片像素宽高无关。这行后面的四个数字分别对应 x_center、y_center、width、height而不是 xmin/ymin/xmax/ymax这是新手最容易转错的地方。而.xml则是另一套体系一个目标对应一段objectobject nameHappy/name bndbox xmin120/xmin ymin80/ymin xmax260/xmax ymax300/ymax /bndbox /objectname是字符串类名bndbox是原图像素坐标。两边转换的核心公式是x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height w (xmax - xmin) / width h (ymax - ymin) / height我用表格把这套双格式差异写出来方便后面写转换脚本时对照对比项YOLO.txtVOC.xml类标识整数 id从 0 开始字符串 class name坐标体系归一化中心点宽高像素级 xmin/ymin/xmax/ymax与图片分辨率的关系无关强相关单文件容量一行一个框每个目标一个object块读取方YOLO 系列训练直接读Faster R-CNN/SSD 常用2.3 用 Python 统计类别分布和坐标越界训练前先统计每个类别的目标数量这决定了要不要做类别重采样。用命令行或者一段 Python 脚本几分钟就能完成from pathlib import Path import numpy as np label_dir Path(labels/train) data [] for txt in label_dir.glob(*.txt): if txt.stat().st_size 0: print(empty label:, txt.name) continue for line in txt.read_text().splitlines(): parts line.split() data.append([int(parts[0])] [float(v) for v in parts[1:]]) arr np.array(data) classes, counts np.unique(arr[:, 0], return_countsTrue) for cid, cnt in zip(classes, counts): print(fclass {int(cid)}: {int(cnt)}) if arr[:, 1].min() -0.05 or arr[:, 1].max() 1.05: print(x_center 越界) if arr[:, 3].min() 0 or arr[:, 4].max() 1.05: print(width/height 越界)这段脚本先把每个labels/train下的 txt 逐行读入一行转成一个 list第一列作为类别号后四列转为浮点坐标然后用np.unique统计每个类别出现的次数。坐标越界检查放在最后虽然 YOLO 在训练时会用clip_coords修正但越界过多通常说明标签生成过程有错误最好提前发现。如果某个类别的目标数只有其他类别的十分之一后面第五章会讲对应的处理思路。2.4 Disguist 拼写差异一个容易让 VOC 转 YOLO 翻车的细节这份数据集 yaml 里的第 7 类写的是Disguist不是我们习惯的Disgust。这里要分清楚YOLO 训练只依赖 txt 里的整数 id和names字符串拼写无关所以即使拼写“错”了YOLO 也能训练。但如果有人把 xml 里的nameDisgust/name拿去和 yaml 的names做映射就会因为字符串不匹配构造不出类别表或者更隐蔽地被排到错误的 id。我的建议是把data.yaml里的names当作唯一真源任何从 VOC 转 YOLO 的脚本都直接从 yaml 加载 names而不是在 Python 里手写一份类名列表。这样即使拼写怪异至少保证源头唯一不会出现 xml、yaml、训练代码三套类名互相打架的情况。3. 训练前做三个检查data.yaml 路径、类别顺序和样本配对很多 YOLO 项目跑了一两个小时后发现 loss 不动最后定位到的问题是data.yaml里的目录名写错、类别顺序和标签不对应、或者 txt 和图片文件名不配对。这套表情识别数据集虽然已经分好 train/val/test但导出的目录到底叫val还是validyaml 里的路径是否指向正确位置都需要在训练前自己确认。我习惯把这三项检查写成一个顺序看目录、写 yaml、跑核对脚本。3.1 先确认目录再改 data.yamlRoboflow 导出的 split 目录有时候是train/valid/test有时候是train/val/test。先执行下面这条命令列出实目录find . -maxdepth 3 -type d | sort然后按实际目录写data.yaml。注意 YOLOv8 找标签的规则图片放在images/split标签必须放在labels/split目录名可以与 yaml 里 train/val 写到的不完全一致只要 yaml 指向的路径真实存在即可。如果图片和标签没有按这种同名目录组织也可以给 yaml 分别写train: images/train和val: images/valid但标签目录无法在 yaml 里单独指定必须保持 images 与 labels 兄弟目录关系。这是这份文件对应的 yaml 写法path: . train: images/train val: images/valid test: images/test names: 0: Neutral 1: Happy 2: Angry 3: Fear 4: Sad 5: surprised 6: Disguist nc: 7path: .表示以 data.yaml 所在目录为根这样整个数据集目录可以整体搬家而不用改路径。names的顺序必须和 txt 里的 class id 一致例如第 0 类 Neutral、第 1 类 Happy。nc: 7可以省略因为 names 列表长度已经告诉 YOLO 是 7 个类别但写出来有助于人工检查。3.2 图片、txt、xml 数量配对检查下一步是核对三个 split 下图片和标签数量是否一致。我自己常用这个脚本from pathlib import Path for split in [train, valid, test]: img_dir Path(fimages/{split}) lab_dir Path(flabels/{split}) if not img_dir.exists(): print(f[{split}] images dir missing) continue imgs set(p.stem for p in img_dir.glob(*.jpg)) txts set(p.stem for p in lab_dir.glob(*.txt)) print(f{split}: images{len(imgs)} txts{len(txts)} fmiss_txt{len(imgs-txts)} no_img{len(txts-imgs)})这段代码用 set 差集来找没配对的样本一个典型的输出是train: images1000 txts1000 miss_txt0 no_img0。set去掉了多余的文件名后缀所以图片名和 txt 名必须以同一个 stem 存在。如果miss_txt不为 0对应的 txt 可能在打包时丢失如果no_img不为 0那说明有多余标签YOLO 训练会报错或忽略。这时不要直接删文件先看是不是jpg和png后缀不一致导致 stem 不同。检查项期望结果异常处理images/train 与 labels/trainjpg 数量等于 txt 数量查找同名但后缀不同的文件images/valid 与 labels/validjpg 数量等于 txt 数量看 yaml 里 val 路径是否指向 validxml 与 txt 的类别名字符串与 yaml names 一致以 yaml 的 names 为唯一真源0 字节 txt数量很少数量多时移除对应图片3.3 验证 yaml 能不能被正确加载yaml 是 YAML 格式写错一个缩进或引号训练会在读取数据集信息时报错。最快的方法是放到 Python 里加载一次import yaml from pathlib import Path cfg yaml.safe_load(open(data.yaml, encodingutf-8)) base Path(cfg[path] or .).resolve() for key in [train, val, test]: target base / cfg[key] print(key, target, exists:, target.exists()) print(names:, cfg[names]) print(nc:, cfg.get(nc))这个脚本比直接开训更快暴露两类问题路径不存在以及 names 不是完整的 7 个条目。YOLOv8 在训练启动时会执行类似的校验但报错信息埋在数据加载阶段不如我们提前打印直观。3.4 空标签文件与背景图2504 张图片里如果存在没有检测到人脸的图对应 txt 可能是 0 字节。YOLO 训练会忽略空标签但有一个副作用图片仍然会被加载相当于隐式的背景负样本。这个对表情识别是好事还是坏事要看比例空标签过多会造成大量无意义的背景梯度。检查方法很简单find labels -name *.txt -size 0 | wc -l如果数量超过几十个我一般会把这些空标签对应的图片直接从训练集移除避免每次 epoch 都在无关背景上浪费时间。这个操作在 YOLOv8 训练自己的数据集场景里经常被忽略但对这套 2504 张的中小规模数据影响会比较明显。4. 用 YOLOv8 把这个表情识别数据集跑通前面检查做完就可以进入 YOLO 目标检测流程的核心训练。这里选择 YOLOv8 举例原因是标签格式和命令参数基本是 YOLOv5-v10 的共同语言。整个流程只需要五步装库、起训练、看日志、可视化抽检、验证。数据量不大先跑一个最小的 n 模型确认链路没问题再上 s 或 m。4.1 安装和环境确认先做环境准备pip install -U ultralytics安装完成后检查 GPU 是否可用如果不可用后续把 batch 调小用 CPU 也能跑但速度会慢很多python -c import torch; print(torch.cuda.is_available())True表示 CUDA 可用。如果输出False则需要检查 PyTorch 版本和驱动通常重新安装对应 CUDA 版本的 PyTorch 即可。第一次训练时 YOLOv8 会自动下载yolov8n.pt预训练权重文件不大下载失败时手动放到当前目录即可不影响后续参数。4.2 训练命令与关键参数将 data.yaml 放在数据集根目录后执行yolo detect train \ modelyolov8n.pt \ datadata.yaml \ epochs100 \ imgsz640 \ batch16 \ patience20 \ cacheTrue \ projectruns/expression \ namebaseline几个参数的取舍是这份数据的关键epochs100对 2504 张图够用配合patience20早停避免在 loss 平台期空跑imgsz640是检测模型的默认入图尺寸人脸区域如果占比小可以提到 768 或 960但显存占用会明显上升batch16在 8G 显存左右的卡上比较稳妥显存不够就先 8 或 4cacheTrue把图片一次性载入内存减少磁盘读取对中小数据集很划算。完整参数表如下参数推荐值含义与说明modelyolov8n.pt预训练模型n 是 nano先跑通再换 s/mdatadata.yaml数据集配置描述路径和类别epochs100最大训练轮数imgsz640训练/推理图片尺寸32 的倍数batch16每批图片数按显存调整patience20连续 20 轮指标不升就停止cacheTrue缓存图片到内存project/nameruns/expression/baseline保存输出的目录4.3 训练中间怎么看results.csv 和 loss 曲线训练启动后在当前目录生成runs/expression/baseline/results.csv。这是最有信息量的文件比盯着终端日志靠谱tail -10 runs/expression/baseline/results.csv每行对应一个 epoch关键列是train/box_loss、train/cls_loss、train/dfl_loss以及metrics/mAP50(B)、metrics/mAP50-95(B)。表情识别场景下重点看cls_loss如果它降不下去说明模型在区分 Fear 和 Surprised 这类相似表情上有困难box_loss下降缓慢则代表人脸框回归没学好可以检查标签中心点是不是有系统性偏移。YOLOv8 会自动在results.png里画 loss 和 mAP 曲线。训练集 loss 一直降但验证集 mAP 不涨通常是过拟合或者增强太强验证集 loss 在训练初期就震荡优先检查数据配对而不是模型。4.4 可视化标注用小脚本抽检一百张图训练开始后与其干等不如先把训练集标注抽出来用 OpenCV 画框确认人脸框和表情类别是不是真的对得上。下面这个脚本读单张图验证import cv2 from pathlib import Path img_path images/valid/youtube-5_jpg.rf.1cea1c38d29f276f2f86b4707d88ee4e.jpg label_path labels/valid/youtube-5_jpg.rf.1cea1c38d29f276f2f86b4707d88ee4e.txt img cv2.imread(img_path) h, w img.shape[:2] for line in Path(label_path).read_text().splitlines(): cls, xc, yc, bw, bh line.split() xc, yc, bw, bh float(xc), float(yc), float(bw), float(bh) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(cls), (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(check_annotation.jpg, img)这段代码把 YOLO 归一化坐标重新乘回图片宽高得到像素级矩形再用cv2.rectangle画出人脸框左上角标出类别 id。先改img_path和label_path指向同一个 basename跑通后再包一层循环抽 100 张。如果框没有贴住人脸说明标签归一的基准尺寸和当前图片不一致或者配对错了文件。4.5 验证集与测试集评估训练结束后用 best.pt 在验证集上重新评估yolo detect val \ modelruns/expression/baseline/weights/best.pt \ datadata.yaml输出里会给出Precision、Recall、mAP50、mAP50-95四项。对于 7 类表情识别mAP50 在 0.85 附近算不错mAP50-95 比 mAP50 低 0.1-0.2 很常见。随后在测试集上做一次推理yolo detect predict \ modelruns/expression/baseline/weights/best.pt \ sourceimages/test \ saveTrue \ conf0.25预测结果会写到runs/expression/predict里面有带框的图片先肉眼看一遍比任何指标都直接。注意yolo val默认读取的是 yaml 的val路径如果想用 test 集做最终 mAP临时把 yaml 里的val换成images/test不要动原始文件。5. 不看总 mAP混淆矩阵、少数类阈值和 ONNX 导出训练结束后最忌讳只报一个总体 mAP。表情识别这个任务里 Neutral 和 Happy 的样本量通常远大于 Fear、Disgust整体指标会被多数类拉高而业务场景恰恰需要把少数情绪抓出来。所以我习惯按类别拆开看指标。5.1 从混淆矩阵找视觉相似类YOLOv8 在验证阶段会生成runs/expression/baseline/confusion_matrix.png。矩阵里 Fear、Sad、Surprised 之间往往有明显混淆这不一定全是模型问题表情本身存在大量主观标注。建议把被分错的图片直接打印出来找三个人独立给标签先解决标注噪声再回头调模型。深度学习目标检测的最后瓶颈经常在数据质量而不是网络结构。5.2 少数类不能只靠降 conf对 Fear、Disgust 这类样本全局conf0.25会把低置信度的正确框全部滤掉。常见的做法是推理时把 conf 降到 0.1让更多候选框进入后处理再在业务层根据表情业务规则过滤。另一个有效手段是训练阶段调小增强强度例如mosaic0.5、fliplr0.5因为少数类本身样本就少过强的 mosaic 会让模型更难学到充分特征。如果还是不行就把 Fear、Disgust 单独抽出来做一个小模型两个模型并联反而好调。5.3 导出 ONNX 部署输出维度变了吗验证没问题后用下面命令导出推理模型yolo export \ modelruns/expression/baseline/weights/best.pt \ formatonnx \ dynamicTrue \ opset12 \ simplifyTrue导出后可以用 onnxruntime 加载做推理。这套数据集类别数为 7所以 ONNX 输出维度是[1, 11, 8400]其中 11 等于 4 个坐标参数加 7 个表情类别8400 是默认输入尺寸 640×640 下的候选框数量后处理里需要一个 NMS。用 onnxruntime 读取输出时先取每个候选框的最大类别概率作为 confidence再按类别分别做 NMS而不是把 conf 参数写死在导出模型里。实际部署时我一般把置信度阈值和 IoU 阈值都放在业务代码里方便按场景动态调整。本文还有配套的精品资源点击获取