简介:面向计算机视觉与人脸表情识别应用场景,压缩包内提供基于YOLOv8训练完成的人脸表情识别权重,以及配套的人脸表情数据集;数据涵盖生气、开心、悲伤、惊讶四类,适合用于表情检测、情绪识别项目或YOLO系列算法的训练实践。资源共2000个文件,包含678个jpg图像样本、602个txt格式标注、58个yaml配置文件,以及Python脚本、Markdown说明文档、C++推理示例等,整体压缩包114.27MB,数据与代码较为齐全。数据集已按train、val、test划分好目录,data.yaml中路径与类别信息均已配置完成,nc为4,names对应anger、happy、sad、surprise,txt标签符合YOLOv5、YOLOv7、YOLOv8、YOLOv9等主流框架的读取格式,可直接放入项目训练。此外还提供pt权重、Python训练与推理脚本、YAML配置和C++调用示例,从数据准备、模型训练、效果验证到部署环节均有对应材料,便于快速复现和二次开发。目前已有1432人学习,适合需要现成权重与完整数据集的开发者作为表情识别项目的基础参考。
1. 人脸表情识别用YOLOv8,不是取巧而是把检测和分类揉成了一件事
把人脸检测框拉出来很容易,但告诉系统框里的人现在是开心还是愤怒,是另一回事。YOLOv8算法把人脸表情识别拆成一条完整链路:检测到人脸的同时输出表情类别,省掉了“人脸检测 + 表情分类”两套模型串联的调度和时延成本。标题里的“训练权重+数据集”才是真正值钱的部分——公开的COCO权重只认识80类物体,不认识表情,你得拿表情数据微调出一份自己的权重,才能用于真实业务。这篇笔记就是沿着这条路,从数据整形、环境搭建、训练参数到踩坑记录,把整个落地过程讲清楚,适合正在准备自己表情数据、或者想在服务器上重训并部署的工程师。
2. 把表情数据喂给YOLOv8:分类目录如何变成训练集与验证集
2.1 YOLOv8要的数据长什么样:标注文件不是标签,是归一化坐标
YOLOv8训练时读的不是“某张图属于happy”这种分类标签,而是一个与图片同名的txt文件。每行对应一个目标框,五个数值依次是:类别id、中心点x坐标、中心点y坐标、框宽、框高,其中坐标全部归一化到0到1之间。对人脸表情识别来说,一张图就是一个目标框,类别id对应happy、sad、anger这些表情。
这个格式和分类数据集最大的差别在于:你得有人脸框。很多公开表情数据集,比如FER2013,给的是48x48的灰度人脸小图,没有框;自采数据往往是一个文件夹代表一类表情,也没有框。所以第一步不是急着写训练命令,而是先把“图片 + 类别”改造成“图片 + 坐标txt”。常见做法是先跑一个现成的人脸检测权重给每张图自动打框,再把框按YOLO格式写出去。如果暂时不想引入检测环节,可以用整图作为框:cx=0.5、cy=0.5、w=1.0、h=1.0,能跑通流程,但效果一般,后面会说为什么。
2.2 处理数据集用于YOLOv8训练:转换脚本是第一个分水岭
下面这个python脚本,把“文件夹名即类别、里面是表情图片”的目录结构,转换成YOLOv8能直接用的标注。它会遍历每个类别文件夹,为每张图片生成一个同名的txt文件,内容就是5个归一化数值。如果你的数据已经有框坐标,把框的来源替换成读json或xml即可。
import os from PIL import Image # 目录约定:root/happy/xxx.jpg, root/sad/xxx.jpg ... root = "emotion_raw" out_root = "emotion_yolo" class_names = ["happy", "sad", "anger", "fear", "neutral"] # 按你的实际类别改 os.makedirs(out_root, exist_ok=True) # 写类别映射文件,YOLO训练时用得上 with open(os.path.join(out_root, "classes.txt"), "w") as f: for c in class_names: f.write(c + "\n") for cls_id, cls in enumerate(class_names): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for img_name in os.listdir(cls_dir): if not img_name.lower().endswith((".jpg", ".jpeg", ".png")): continue img_path = os.path.join(cls_dir, img_name) img = Image.open(img_path) w, h = img.size # 第一个版本:直接用整张图当人脸框 # cx, cy, bw, bh 均归一化到 0~1 cx, cy = 0.5, 0.5 bw, bh = 1.0, 1.0 label_path = os.path.join(out_root, os.path.splitext(img_name)[0] + ".txt") with open(label_path, "w") as f: f.write(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n") # 图片拷贝到同目录,YOLO训练时图片和txt放在一起 img.save(os.path.join(out_root, img_name)) print("转换完成,去", out_root, "检查几张txt内容")这段脚本做了三件事:建立类别id与文件夹名的映射、遍历图片生成归一化框、把图片和txt放到同一个目录。注意bw和bh写成1.0不是偷懒,而是当你确定图片本身就是人脸特写时的合理初值;如果图片里人脸占比小,就必须换成人脸检测器输出真实框。转换完随手打开几个txt检查数值,别等到训练失败再回头看数据。
2.3 划分train/val并生成dataset.yaml,别让验证集偷看训练集
YOLOv8要求你提供一个dataset.yaml,里面写train和val路径、类别数nc、类别名names。划分时有个细节:表情数据经常按人划分,同一个人的不同表情图片如果既进了训练集又进了验证集,指标会虚高。稳妥做法是先按人分组,再整组划分,而不是简单随机打散图片。这里给一个按排序间隔划分的脚本。
import os import random import yaml out_root = "emotion_yolo" imgs = [f for f in os.listdir(out_root) if f.endswith(".jpg")] random.seed(42) random.shuffle(imgs) train_imgs = imgs[: int(len(imgs) * 0.8)] val_imgs = imgs[int(len(imgs) * 0.8):] for split, split_imgs in [("train", train_imgs), ("val", val_imgs)]: split_dir = os.path.join(out_root, split) os.makedirs(split_dir, exist_ok=True) for img in split_imgs: os.rename(os.path.join(out_root, img), os.path.join(split_dir, img)) txt = os.path.splitext(img)[0] + ".txt" if os.path.exists(os.path.join(out_root, txt)): os.rename(os.path.join(out_root, txt), os.path.join(split_dir, txt)) data_cfg = { "path": os.path.abspath(out_root), "train": "train", "val": "val", "nc": 5, "names": ["happy", "sad", "anger", "fear", "neutral"], } with open(os.path.join(out_root, "dataset.yaml"), "w") as f: yaml.dump(data_cfg, f, allow_unicode=True) print("生成 dataset.yaml,同时把图片和txt按8:2移动到了train/val")脚本里先按8:2拆图片,再同步移动同名txt。关键点是src/fix bug时rst的坑留给后面第5章,这里先确保你生成的dataset.yaml路径是绝对路径,避免训练时YOLO相对路径解析出错。yaml里names顺序必须和转换脚本里class_names顺序一致,否则类别就错位了。
2.4 Ubuntu20.04搭建YOLOv8环境CPU版本:能跑,但要控制流程
训练表情识别这类数据量不大的任务,CPU环境不是不能跑,而是要把预期放低。在Ubuntu20.04上,pip安装ultralytics包即可,不需要单独编译CUDA算子。
python3 -m venv yolov8_env source yolov8_env/bin/activate pip install --upgrade pip pip install ultralytics # 检查安装 yolo detect predict model=yolov8n.pt source=https://ultralytics.com/images/bus.jpgCPU训练的最大问题不是装不上,而是训练速度。一张1080p人脸图,imgsz=640,CPU上大概每轮几十秒到几分钟,取决于你的核数。我的做法是:先用32张图、epochs=3把整个流程跑通,确认数据格式没问题再全量训练。这一步是为了省掉“训练10小时后才发现数据集路径错了一半”的翻车时间。
3. 用YOLOv8训练人脸表情识别:预训练权重选择与关键训练参数
3.1 预训练权重怎么选:yolov8n和yolov8s对人脸表情任务的取舍
YOLOv8的预训练权重按尺寸从n到x分档,n最轻、x最重。做表情识别,我一般只用n或s。理由很简单:表情识别的输入是裁剪后的人脸区域,目标框通常只有一个,背景干扰比通用检测小得多,大模型收益有限。yolov8n.pt在COCO上mAP不算高,但做单目标微调足够;如果后续还要部署到RK3588这类边缘设备,n档的权重导出onnx后只有6MB左右,非常友好。
预训练权重的下载不需要手动找链接,ultralytics在你指定model=yolov8n.pt时如果本地没有,会自动从官方release拉取。公司内网环境拉不下来时,可以提前在有网的机器上yolo detect predict model=yolov8n.pt触发下载,再把权重文件拷贝到工作机~/.config/Ultralytics/目录下。
3.2 训练命令与yolov8模型训练参数含义:一张表说清
数据集准备完毕,训练命令不需要花哨,关键是知道你改的每个参数会带来什么影响。下面是我常用的表情识别训练命令:
yolo train \ model=yolov8n.pt \ data=emotion_yolo/dataset.yaml \ epochs=100 \ batch=16 \ imgsz=640 \ lr0=0.01 \ optimizer=AdamW \ patience=20 \ freeze=10 \ device=0参数含义拆开说:
| 参数 | 作用 | 我的建议值 |
|---|---|---|
| model | 指定初始权重,yolov8n.pt是COCO预训练 | 数据量小用n,数据量大用s |
| epochs | 最大训练轮数 | 先20看loss走势,再决定100还是200 |
| batch | 每批图片数,显存不够就减小 | CPU用4~8,GPU看显存16G用16~32 |
| imgsz | 训练输入分辨率 | 人脸图建议640,原图48x48的要先缩放 |
| lr0 | 初始学习率 | 默认0.01,loss震荡就降到0.001 |
| optimizer | 优化器 | 数据量小用AdamW收敛稳 |
| patience | 早停轮数,验证集mAP连续不涨就停 | 15~20合适 |
| freeze | 冻结前N层参数 | 数据少时freeze=10保底层特征,数据多可以不冻结 |
| device | 训练设备 | 0是GPU,cpu写cpu |
常见误区是epochs开很大但batch开很小,结果loss一直在抖动。表情识别数据集通常几百到几千张,batch=16、imgsz=640在8G显存上已经比较极限;如果显存不够,优先降batch而不是降imgsz,因为人脸小目标受分辨率影响明显。
3.3 表情类别不平衡是训练里的暗雷:disgust永远不够
真实采集的表情数据里,happy和neutral往往占大头,disgust和fear可能只有几十张。YOLOv8默认不做类别加权,结果就是少数类在训练中很少被看到,模型到验证阶段对disgust的召回率接近零。
我的处理分三档:第一档是给少数类过采样,即在数据划分时让disgust的样本量通过重复拷贝提升到多数类的30%左右,注意拷贝后要重新划分train/val,防止同图重复进验证集;第二档是训练参数里调class_weight,ultralytics支持传一个按类别的loss权重列表,给少数类更高权重;第三档最简单——找公开数据补充,比如把其他表情数据集里的fear、disgust刻意加进来。先看训练完的混淆矩阵再决定要不要重度处理,别一上来就过采样把分布搞乱。
4. 训练完了不只看best.pt:评估指标、权重文件与快速验证
4.1 训练产物:best.pt/last.pt/results.csv里到底有什么
训练结束后,runs/detect目录下会生成一个以时间戳命名的文件夹,里面weights目录放着best.pt和last.pt。best.pt是验证集上mAP最高的那一轮权重,last.pt是训练结束时那一轮。很多人直接拿last.pt去部署,结果发现效果比best.pt差一截——因为last往往已经过拟合。我一般只看best.pt,除非last正好也是best。
results.csv里按轮次记录了train_loss、val_loss、metrics/precision、metrics/recall、metrics/mAP50这些数值。这是后验训练质量的唯一依据,比终端打印的进度条可靠。训练完第一件事不应该是去测试图片,而是打开results.csv看loss曲线是否平稳下降、mAP是否还在上升。如果best出现在最后一轮,说明epochs开少了,可以加载best.pt再训几十轮。
4.2 看哪些数:P/R/mAP50/mAP50-95,以及混淆矩阵
对表情识别这种单目标任务,mAP50-95其实比mAP50更严苛,因为人脸表情边界框误差对“表情对不对”影响不大,IoU卡得越严越吃亏。我习惯重点看两类指标:
- 验证集上的混淆矩阵,看哪些表情互相混。happy和surprise经常混,因为嘴巴张大的视觉特征接近;disgust和anger也常混。
- 每个类别的recall,而不是只看总的mAP。总mAP高但某类recall极低,部署时就会频繁漏检,这在第3.3节已有伏笔。
ultralytics训练结束会自动生成confusion_matrix.png,在runs/detect目录下。看到某个类被系统性误判,优先怀疑数据量不够,而不是模型结构问题。
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.val(data="emotion_yolo/dataset.yaml") print(results.box.map) # mAP50-95 print(results.box.maps) # 每个类别的mAP print(results.box.p) # precision print(results.box.r) # recall这段代码用训练好的权重重新跑一遍验证集,输出数值指标。如果你是增量训练后想对比新旧权重的效果,val输出比肉眼瞄几张嘴可靠得多。
4.3 加载best.pt做单张图和摄像头验证,注意conf阈值
训练指标只是起点,真实场景验证要单独写脚本。表情识别的conf阈值我一般设0.4到0.5,比通用检测的0.25高,理由是人脸框内表情特征不明显时,低阈值会产生大量不稳定的跳变预测。
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="test_images/", conf=0.4, imgsz=640, save=True, ) for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) print(r.names[cls_id], conf, box.xyxy.tolist()[0])这段脚本把所有测试图片跑一遍,输出类别、置信度和坐标。注意save=True会把标注后的图片存到runs/detect/predict目录,方便人工核对。如果发现部分图片完全没框,多半不是模型没训好,而是原图人脸占比太小,YOLO的特征图感受野没覆盖住,需要把你的检测输入从整张图改成先裁剪人脸区域。
5. 人脸表情识别训练的六个踩坑记录:从标注翻车到设备部署
5.1 类别标签从1开始,训练结果全是背景
现象:训练loss下降正常,验证集mAP始终接近0,检测结果一无所有。 原因:我最初写转换脚本时,class_names里加了“background”并把happy设为1,但YOLOv8的类别id必须从0开始连续编号。模型学到的是背景类别占0号,和val标注对不上,一切指标都是乱套的。 解决:删掉background,把happy从0开始重新编号,重跑2.2节的转换脚本后训练立刻正常。这个血的教训让我现在每次转换完都随机挑一个txt文件,人工确认第一列的id数值不超过类别数减1。
5.2 labelme标注用于YOLOv8:json里的坐标和像素值害我白跑两小时
现象:用labelme标注人脸框后导出的json转txt,训练时loss巨大且不收敛。 原因:labelme的json里坐标是绝对像素值,我忘了除以图片宽高,直接把几百像素的x、y写进txt,YOLO读到的归一化坐标全部大于1,相当于把目标框推到了画面外。 解决:转txt时先读图片真实宽高,所有坐标除以宽高。如果你也是labelme转YOLO,保存前用下面这行自查:任何一个数值大于1,说明归一化没做。
import numpy as np bbox = np.array([x_min, y_min, x_max, y_max]) / [img_w, img_h, img_w, img_h] assert bbox.max() <= 1.0 and bbox.min() >= 0.05.3 训练loss很低但mAP难看:验证集和训练集“长”得太像
现象:train_loss降到0.3,val_loss也在降,但混淆矩阵显示模型把验证集里某个人的所有表情都识别成neutral。 原因:我按图片随机划分了train和val,同一个人的多张表情图同时出现在两边,模型记住的是人而不是表情;换到摄像头前没见过的真人,泛化瞬间崩塌。 解决:改成按人分组划分。先收集所有图片所属的人名,把人维度打散,再按人划入train或val。严格说表情识别数据集应该保证训练集和验证集里的人完全不重合。
5.4 表情类别不平衡导致精确率虚高:disgust只有20张还全对
现象:验证集总mAP不错,打印每类指标发现disgust的precision是1.0,但recall只有0.1。 原因:disgust样本太少,模型只学会了“看起来非常扭曲的脸才是disgust”,其余disgust全被分成了anger。precision高是因为预测侥幸撞对了寥寥几张,recall低才暴露了真实水平。 解决:先看每类recall而不是总mAP。数据层面过采样或补充公开数据,训练参数层面在loss里给少数类加权,具体做法参考3.3节。千万别拿着总mAP出去汇报,那种单类0.1的模型上线就是翻车现场。
5.5 CPU训练太慢导致反复改数据:先缩规模验证流程
现象:用完整数据集在CPU上训练,一发入魂等6小时,然后发现dataset.yaml路径写错,白白浪费时间。 原因:数据准备阶段的错误不会在训练启动时报错,而是藏在loss曲线里慢慢暴露,CPU一轮几分钟让这种“慢性错误”代价剧增。 解决:先用20张图、epochs=3跑通,确认loss真的在降、val能跑出数字,再切全量。这个“流程小样”习惯帮我省下的时间远多于多花的5分钟。
5.6 摄像头实测频繁跳变:阈值调高后稳定很多
现象:验证集mAP有0.9,摄像头前模型对同一张人脸在happy和neutral之间来回跳变。 原因:表情识别天然是时间序列问题,单帧置信度在两类边缘时,模型输出不稳定。验证集静态图测不出这种抖动。 解决:两个手段同时用——conf阈值从0.25提到0.45;对连续帧做滑动窗口投票,比如每秒取5帧,取出现次数最多的类别作为最终输出。第二种手段在部署层做,不影响训练。
6. 把训练过程收进一张图:用results.csv画loss曲线并核验训练是否正常
训练完成最好的复盘工具不是终端日志,而是用results.csv画一张损失和mAP随epoch变化的曲线图。下面这段代码读取训练过程数据,一次画出train_loss、val_loss和mAP50趋势,判断有没有过拟合、有没有欠拟合。
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/train/results.csv") # 列名是 yolov8 版本自动生成的,注意去空格 df.columns = df.columns.str.strip() fig, ax1 = plt.subplots(figsize=(10, 5)) ax1.plot(df["epoch"], df["train/box_loss"], label="train_box_loss") ax1.plot(df["epoch"], df["val/box_loss"], label="val_box_loss") ax1.set_xlabel("epoch") ax1.set_ylabel("box_loss") ax1.legend() ax2 = ax1.twinx() ax2.plot(df["epoch"], df["metrics/mAP50(B)"], label="mAP50", color="green") ax2.set_ylabel("mAP50") ax2.legend(loc="upper left") plt.title("YOLOv8 Face Expression Training Curves") plt.show()从这张图能读出三种常见状态:train_loss下降但val_loss在第30轮后反弹,是过拟合,早停参数应该更激进;两个loss同步下降但mAP50纹丝不动,是数据问题,多半是标注框偏移或类别id错位;两个loss和mAP都平稳,那这份权重基本可以进入部署环节。画完图我还会做最后一件事:复制best.pt到自己的模型目录,起一个和训练时完全一致的描述性名字,比如best_fer_7class_imgsz640.pt,避免一个月后对着“best.pt”三个字猜它是干什么的。
这套从数据整形到训练验证的流程,我现在每次跑新表情数据集都会按这个顺序走一遍。画损失曲线的时间不算在训练时长里,但它是整个训练里最值得花的五分钟。希望帮到你。
本文还有配套的精品资源,点击获取