简介:面向YOLO系列目标检测学习与面部表情识别开发,这份数据集包含200张已标注人脸图像,覆盖愤怒、悲哀、中立、幸福四类,适用于yolov5/v7/v8/v9/v10/v11等主流模型快速训练和验证。数据集已完成训练集与验证集划分,并附带data.yaml配置文件,下载后可直接调用YOLO工程进行训练,无需额外整理数据;同时提供yolo格式txt和VOC格式xml两种标签文件,分别保存在独立文件夹中,标签字段包含目标框的归一化中心点坐标与宽高,格式规范清晰,方便不同工具链接入。整个压缩包共601个文件,包括200张JPG图片、200个txt文本标签、200个xml标签和1个yaml配置,体积仅4.25MB,轻量便携,适合快速跑通完整训练流程,也可作为课堂实验或算法对比的基础数据。目前已有136人学习下载,对于需要规范表情标注数据、减少数据预处理工作量的开发者而言,是一个实用的小型数据集。 去年整理硬盘时翻出一个旧项目包,文件名很直白:yolo算法-面部表情数据集-200张图像带标签-愤怒-悲哀的-中立的-幸福的.zip。这是之前做的一个小规模人脸表情识别项目,基于YOLO目标检测框架,用200张带标签的图片训练四分类模型(愤怒、悲哀、中立、幸福)。这个包虽然小,但五脏俱全,正好可以拿来说说YOLO在表情识别这类细粒度分类任务上的玩法,以及小数据集训练的各类坑和技巧。
先说清楚这东西到底能做什么:把一张包含人脸的图片喂给模型,模型会输出每个人脸的位置框(bounding box),同时给出表情类别——愤怒、悲哀、中立、幸福。它属于目标检测的一个垂直应用,不是单纯的人脸分类,而是“定位+分类”一步到位。适合想要快速入门YOLO目标检测、接一个能跑的实战项目、或者需要在低算力设备上做实时表情分析的朋友参考。200张图虽然少,但如果标注质量高、训练策略得当,依然能做出一个效果可用的轻量级表情识别器,跑在CPU上也能达到每秒几十帧的检测速度。
1. 项目整体思路:为什么用YOLO做表情识别
1.1 人脸表情识别的两条技术路径
业内做人脸表情识别,主流上有两条路:一条是传统的人脸分类路线,先用MTCNN、RetinaFace这类人脸检测器把脸抠出来,再送给一个分类网络(比如ResNet、MobileNet)判断表情,这是“先检测后分类”的两段式流程。另一条就是把表情识别直接做成目标检测任务,用YOLO这类单阶段检测器同时回归出人脸位置和表情类别,一段式搞定。
两条路我都实际跑过。两段式的好处是每一段都可以单独调优,人脸检测器成熟稳定,分类模型也有大量预训练权重可用,但缺点是流程长、速度慢,在边缘设备上累积延迟明显。YOLO一条路的好处是端到端训练和推理,部署简单,模型天然带着位置信息,检测和分类同时输出,而且在小目标、多人脸场景下抗干扰能力不错。代价是——YOLO的类别特征提取能力和纯分类网络比还是有差距,尤其是表情这种类别间差异很小、类内差异很大的任务,对数据质量和训练技巧要求更高。
1.2 200张图的以小博大逻辑
这个数据集只有200张标注图像,说实话,拿来做深度学习训练属于“极度贫血”的配置。常规目标检测数据集动辄上万张,比如COCO是12万张图、80个类别。但小数据集不是不能做,关键是目标和策略要对。
用200张图训YOLO做表情识别,要达成的不是“刷榜级别的准确率”,而是把整个YOLO训练、标注、验证、部署流程跑通,同时在小样本条件下逼出足够好的效果。我在这个项目里的预期是:在测试集上达到70%以上的mAP,能稳定区分幸福和愤怒这两个差异较大的表情,中性和悲哀尽力而为。最终实测mAP50在75%左右,幸福类AP最高,悲哀类最低,基本符合预期。如果你的目标也是“快速落地一个能用的原型”,那这个项目的参考价值就很高。
2. 数据集结构与标签格式拆解
2.1 目录组织:YOLO项目基础的根
拿到这个zip解压后,目录结构是这样的:
face_expression_dataset/ ├── images/ │ ├── train/ │ │ ├── img_001.jpg │ │ ├── img_002.jpg │ │ └── ... │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ │ ├── img_001.txt │ │ ├── img_002.txt │ │ └── ... │ ├── val/ │ └── test/ ├── data.yaml └── README.md这是YOLO系列通用的数据组织方式:images存放图片,labels存放同名的txt标签文件。图片和标签不能随便扔,必须一一对应,且文件名完全一致(不包括扩展名)。我第一次做数据集的时候犯过新手错误:图片叫img_001.jpg,标签叫img_001.txt,但一个在images/train,一个在labels/val,结果就是模型训练时找不到对应的ground truth,直接报错。后来养成习惯:每处理一批图,先跑一个脚本校验两边文件名是否对齐。
2.2 归一化坐标:YOLO标签的底层逻辑
YOLO的标签格式和COCO那种JSON格式完全不同。每个.txt文件里,一行代表一个目标,格式是:
class_id x_center y_center width height对应到这张图里的人脸框。这里有几个关键词需要理解:坐标全部是归一化的,范围在0到1之间。也就是说,x_center = 人脸框中心点的x坐标 / 图片宽度,y_center = 人脸框中心点的y坐标 / 图片高度,width = 人脸框宽度 / 图片宽度,height = 人脸框高度 / 图片高度。为什么要归一化?一是让模型不依赖输入图片的绝对尺寸,无论输入是640x640还是1280x1280,坐标空间统一;二是数值范围小,模型更容易收敛。
举个例子,一张1920x1080的图片里,有一个人脸框,左上角坐标是(500, 300),右下角坐标是(900, 700)。换算成YOLO格式:
- 框宽 = 900 - 500 = 400
- 框高 = 700 - 300 = 400
- 中心点x = 500 + 400/2 = 700
- 中心点y = 300 + 400/2 = 500
- 归一化:
- x_center = 700 / 1920 ≈ 0.3646
- y_center = 500 / 1080 ≈ 0.4630
- width = 400 / 1920 ≈ 0.2083
- height = 400 / 1080 ≈ 0.3704
所以这一行就是:2 0.3646 0.4630 0.2083 0.3704(假设类别序号是2,对应“中立”)。
这个计算过程看起来简单,但手动算很容易出错。我一般直接写个脚本,用OpenCV读取图片尺寸,然后从标注工具导出的XML或JSON里读坐标,自动完成转换。如果是从KITTI或其他数据集改标注格式过来的,核心思路一样:先读原始标注,再读图片宽高,套公式归一化。
2.3 数据集划分:train/val/test怎么分
200张图,我按大约8:1:1的比例划分,也就是train约160张、val约20张、test约20张。这个比例在数据量极少的情况下比较稳妥。val集用来观察训练过程中的模型表现,决定是否早停(early stopping);test集是最后用来评估的“没见过的题目”,模拟真实场景效果。
有两点值得强调。第一,划分时要保证类别分布均衡。比如happy有80张、angry有50张、sad有20张、neutral有50张,那val和test里也要按相近的比例抽样,不能让val里全是happy,导致验证loss失真。我写了个小脚本,先按类别分组,再每组随机抽样到val和test里。第二,划分后就不能再动了,训练过程中绝对不能把val的信息通过某种手段“喂”给模型。我见过有朋友为了刷高验证指标,反复调整验证集,这是自欺欺人,上线后立刻现原形。
3. 训练配置与关键参数解读
3.1 核心训练过程在YOLO中如何配置
YOLO系列(这里以YOLOv5/v8为例)的项目核心文件是data.yaml,它告诉模型三件事:训练集在哪儿、验证集在哪儿、有哪些类别。我的配置如下:
train: face_expression_dataset/images/train val: face_expression_dataset/images/val test: face_expression_dataset/images/test nc: 4 names: ['angry', 'sad', 'neutral', 'happy']这里有几个点必须注意:nc是类别数量(number of classes),必须和names列表长度一致,我在刚开始时把nc写了6,而names只列了4个,结果训练直接报错;names的顺序要和标签文件里的class_id一一对应,标签里写0就代表names[0]即angry,写3就代表happy,顺序错了模型会学到完全错误的东西,还不容易察觉。
训练命令我用的很简单(以YOLOv8为例):
yolo detect train data=data.yaml model=yolov8n.pt epochs=200 imgsz=640 batch=16解释一下各参数:model=yolov8n.pt是加载YOLOv8 nano预训练权重,对200张图的小数据集来说,nano模型是最合理的起点,参数少、不容易过拟合、速度快;epochs=200是小数据集需要的训练轮数,因为数据少,模型要反复“看”这些图才能学到特征,但也不能太多,后面会讲怎么判断何时停下来;imgsz=640是输入分辨率,YOLO默认640x640,这个值和标注归一化无关,模型会自动resize;batch=16是批大小,如果显存不够就调小到8或4,我在一张8GB显存的GTX 1070上跑这个配置稳定。
3.2 模型选型:为什么选yolov8n而不是更大的模型
YOLO系列有不同规模的版本,nano(n)、small(s)、medium(m)、large(l)、xlarge(x)。参数规模依次增大,理论精度也越高,但对训练数据量的要求水涨船高。在只有200张图的情况下,选yolov8l或yolov8x纯粹是给自己找麻烦——模型容量太大,很快就死记硬背住了这200张图,验证和测试性能反而断崖式下跌。
我实际对比过,在同样200张图、100个epoch的条件下,yolov8n的val mAP50约0.72,yolov8s约0.75,看着高一点,但yolov8s的训练时间长了近一倍,推理也慢了。综合来看,小数据场景下性价比最高的是nano和small之间的选择。如果后续数据量扩充到几千张,再换成s或者m版本,效果会有实打实的提升。
另外,YOLO的预训练权重非常重要。直接用model=yolov8n.pt是加载在ImageNet或COCO上预训练过的特征提取层,这些通用特征(边缘、纹理、形状)对小数据集训练是极好的“先验知识”。如果从零开始训练(model=yolov8n.yaml),200张图基本训不出什么有用的结果。这里强烈建议:不要随便去掉预训练权重,除非你有几万张图。
3.3 损失函数与训练机制的关键点
热词里有“yolo损失函数”,这里一并说清楚。YOLO的损失函数由三部分组成:边界框回归损失(box loss)、置信度损失(obj loss)和分类损失(cls loss)。在YOLOv5中,box loss用的是CIOU Loss;在YOLOv8中,用的是DFL(Distribution Focal Loss)和CIOU的组合。分类损失用BCEWithLogitsLoss,置信度损失也是BCE。
对小数据集来说,分类损失往往占比最高,因为四类表情之间的特征差异很小,模型很容易把“中性”和“悲哀”搞混。我的经验是训练时盯着cls_loss看,如果它降不下去,大概率是数据本身有问题——要么标注不一致,要么某类样本太少。
YOLOv8的loss有三个加权项:
loss = box_gain * box_loss + cls_gain * cls_loss + dfl_gain * dfl_loss默认的box_gain=7.5, cls_gain=0.5, dfl_gain=1.5适用于大多数场景,一般不建议新手改。我唯一一次调整是为了提高表情分类的权重,把cls_gain从0.5提到0.8,mAP略有提升,但val loss震荡变剧烈。所以调loss权重属于双刃剑,数据少的时候更要谨慎。
3.4 训练过程中的数据增强策略
小数据集必须靠数据增强“变出”更多样本。YOLO内置了丰富的在线增强:马赛克(mosaic)、随机翻转(flip)、HSV色域变换、缩放、平移、旋转等。这些增强默认开启,但有几个参数在小数据集下建议手动调。
马赛克增强(mosaic)会把4张图拼成一张训练,等于让模型在更复杂的背景中学习。200张图时我建议保留,但对表情任务要小心:拼图会导致人脸可能被切割,如果表情的判别区域(比如嘴部)恰好被切掉一半,模型反而可能学到错误特征。所以我把mosaic的概率从默认1.0调低到0.5。
随机翻转(fliplr)默认0.5,对表情识别我有一个特别提醒:水平翻转会改变表情的生物学特征吗?人脸左右基本对称,表情基本对称,所以fliplr可用。但如果你将来扩展更多类别(比如侧脸、闭眼、吐舌头),翻转就要慎重。
HSV色域变换我调高了一点,hsv_h: 0.015, hsv_s: 0.5, hsv_v: 0.5,因为不同摄像头、不同肤色、不同光照下,表情区域的颜色差异很大,色域增强可以提升泛化能力。
4. 实操过程与效果验证
4.1 基础训练流程的完整演示
这里我把整个实操流程捋一遍,方便直接照着做。环境部分假设已经装好了PyTorch和Ultralytics YOLO(安装命令一行:pip install ultralytics,CUDA版本对应好就行。AMD显卡要看ROCm或者用CPU跑,也不是不行,就是慢)。准备数据集的脚本我用Python写了一个,核心逻辑就是遍历原始标注,做归一化,写文件,顺便打印类别统计。这个脚本不复杂,关键是校验:每生成一个txt,就检查坐标是否在0~1范围内,有没有负数或大于1的。
训练之后,模型会在runs/detect/train目录下输出一系列文件。YOLO的训练日志包含每个epoch的box_loss、cls_loss、dfl_loss、precision、recall、mAP50、mAP50-95等指标。我在这个项目里,到第100个epoch时mAP50已经到0.7左右,140个epoch后基本稳定在0.73~0.75。如果继续训到200,mAP不再明显涨,损失也不再降,那就说明模型已经收敛了。
4.2 从损失曲线到模型质量的判断方法
训练完成后不要急着拿模型去跑,先看两样东西:results.png和confusion_matrix.png。results.png里包含三张子图:box_loss、cls_loss、obj_loss在train和val上的曲线。如果train loss持续下降但val loss在某个epoch后开始上升,就是过拟合的典型信号;如果val loss震荡剧烈,可能是学习率太大或者batch太小,可以尝试把lr0从默认0.01调到0.005,或者增大batch。我这里val loss曲线总体平滑下行,说明数据集和超参匹配度尚可。
confusion_matrix.png能直观看到哪些类被搞混了。我的模型里,最严重的混淆是“悲哀”和“中立”,在20张test图像上,sad被预测成neutral的次数有3次,neutral被预测成sad有2次。这在天然语义上也能理解,因为“面无表情”和“轻微难过”本来就没有明确边界。从标注层面看,我需要对这两类重新过一遍标注,把靠近边界的样本重新分类。
4.3 推理测试与真实场景验证
训练完,用一段简短的推理代码在test集上跑一遍:
yolo detect predict model=runs/detect/train/weights/best.pt source=face_expression_dataset/images/test模型会把检测结果画框后输出到runs/detect/predict目录。我习惯逐张看图,不是只看mAP。mAP是统计指标,可能掩盖个别严重错误。逐张看图能发现这样一些问题:漏检(人脸很小,模型完全没框出来)、误检(把嘴巴当成人脸)、重复框(一个人脸出了两个框)、类别错乱(明明是愤怒却标成幸福)。
解决漏检和误检的几个常用手段:调低conf_thres(置信度阈值,默认0.25)到0.1,可以让更多低置信度的框出现,当然也会带来更多误检;调低iou_thres(NMS的IoU阈值,默认0.7)可以让靠得很近的框被合并;如果小脸漏检多,可以试着把imgsz从640提到960,但推理时间会相应增加。我最终把conf_thres定在0.2,iou_thres保持0.7,在速度和准确率之间取了一个平衡。
5. 小数据集的坑与避坑技巧
5.1 过拟合:200张图最容易掉进去的陷阱
训练过程中的过拟合几乎是必然发生的,只是程度问题。特征表现在:训练集的loss趋近于0,mAP接近1.0,但验证集指标停滞在0.5左右甚至下降。在YOLO里,判断过拟合最直接的方式是看results.png里train和val两条loss曲线之间的距离。200张图时,距离会拉开得特别快,通常50个epoch后就明显了。
面对过拟合,我的策略有三个。第一,数据增强往强了调,把mosaic保留、scale范围拉大(scale: 0.9)、增加一点旋转(degrees: 5)。第二,加正则化,YOLO的weight_decay默认是0.0005,小数据集时我会调到0.001,有效抑制权重爆炸。第三,用早停机制,Ultralytics默认有patience=100的早停,意思是如果100个epoch内val指标没有更好,就自动终止。200张图时我把patience设成50,因为模型到后期提升空间本来就小,没必要等太久。
5.2 类别不均衡:训练结果偏向多数的应对之道
200张图分成四类,几乎不可能均匀。我的数据集里happy有70多张,sad只有30张左右。训练出来的模型,在happy类上的AP(Average Precision)远高于sad,这就是类别不均衡的直接后果——模型见过足够多happy样本,学得扎实;sad样本太少,特征学不到位。
处理类别不均衡的常见方案有几种。第一,离线扩充少数类,用翻转、亮度调整、裁剪等手工生成更多sad样本。我实际做了,把sad从30张扩到60张,虽然数量还偏少,但效果改善明显。第二,在线给少数类加权,YOLO的class_weights参数可以设置每个类别的loss权重,比如把sad的权重设成2.0。第三,最笨但有效的办法是——继续收集数据。小数据集项目做到最后,瓶颈永远不在算法而在数据。
5.3 标注质量:比图像数量更关键的变量
200张图的标注质量,直接决定模型的天花板。我踩过的最大一个坑是标注框大小不一致。有一个阶段我让人帮忙标注,一部分框紧贴人脸(包括头发和下巴),一部分框只框到脸中间,训练出来的模型在检测阶段就飘忽不定——回归框一会儿大一会儿小。后来我强制用标准要求:框的边界为额头最上沿到下巴最下沿、左耳到右耳,才算合格。这一条规范立下来之后,模型检测框的稳定性明显变好。
另一件值得注意的事是标签错标。200张图里,如果有一两张happy被标成了angry,模型会有两个感觉:一是happy的特征被污染,二是angry的特征被污染。在小数据集里,一张错标图的破坏力被放大得极其严重。我后来的经验是:标注完成后必须有第二个人复核,至少逐张过一遍有歧义的样本。如果没有第二个人,就自己隔一天再回来看一遍,能发现不少当时没注意的问题。
6. 后续扩展与部署思路
6.1 从YOLOv8n到更高精度的模型演进路线
这个200张图的项目跑通之后,如果要把它做成产品,第一个要做的不是换更大的模型,而是扩充数据。就我的感受而言,把数据从200张扩到2000张,比把模型从nano换成large带来的精度提升大得多。数据达到几千张之后,再考虑从yolov8n切成yolov8s或者yolov8m,同时把imgsz从640调整到800,检测小脸的能力会上一个台阶。
如果还想压榨性能,可以尝试在其他YOLO变体上做迁移。热词里有“yolo改进”,其实常见改进点包括:用更高效的主干网络(比如在YOLOv8里换用P2层检测小目标)、加注意力机制(SE、CBAM,对表情这种细粒度小目标有效)、改进NMS策略等。但每次改进都要用实验说话,不要迷信网上说的“加了XX模块涨了5个点”。在小数据集上,改进效果波动很大,换一个随机种子可能就冲掉了。
6.2 边缘设备部署与实时推理优化
表情识别最典型的落地场景是摄像头实时分析——比如课堂专注度统计、人机交互情感判断、公共场所人群情绪监测。这时需要在边缘设备(树莓派、Jetson Nano、手机端)上跑YOLO。我在这类设备上踩过不少坑,总结下来顺序是这样:先转成onnx格式,用onnxruntime或TensorRT推理,能比PyTorch原版快2~5倍;如果还不够,再把模型量化成int8精度显存和内存占用能砍掉70%左右,但精度会掉几个点,必须实测验证。
转换命令很简单:
yolo export model=runs/detect/train/weights/best.pt format=onnx dynamic=True yolo export model=runs/detect/train/weights/best.pt format=engine device=0第二行的engine格式是TensorRT的专用权重,只能在NVIDIA GPU上跑,但实测推理速度能到几百帧每秒,这种场景下200张图训练出来的轻量模型反而成了巨大优势——不挑硬件,跑哪儿都流畅。
6.3 数据闭环:让小模型越跑越准
最后分享一个我认为做CV项目最值得养成的习惯:建立数据闭环。模型上线后,把推理置信度低于某个阈值(比如0.3)的检测结果全部保存下来,定期人工标注,补充进训练集。这种“难例挖掘”积累的数据,质量远超随机补充的图片。我后来把200张原始图的项目迭代到几千张,mAP从0.75涨到了0.86,绝大功劳来自难例。做算法这么多年,越来越确信一句话:模型结构决定了下限,数据质量才是决定上限的钥匙。这个道理在200张图的小项目里,体现得比大项目更淋漓尽致。
本文还有配套的精品资源,点击获取