YOLOv8人脸表情识别实战:从数据标注到模型训练与部署
发布时间:2026/10/2 2:40:14来源:尧图网络
简介面向计算机视觉与人脸表情识别应用场景压缩包内提供基于YOLOv8训练完成的人脸表情识别权重以及配套的人脸表情数据集数据涵盖生气、开心、悲伤、惊讶四类适合用于表情检测、情绪识别项目或YOLO系列算法的训练实践。资源共2000个文件包含678个jpg图像样本、602个txt格式标注、58个yaml配置文件以及Python脚本、Markdown说明文档、C推理示例等整体压缩包114.27MB数据与代码较为齐全。数据集已按train、val、test划分好目录data.yaml中路径与类别信息均已配置完成nc为4names对应anger、happy、sad、surprisetxt标签符合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格式写出去。如果暂时不想引入检测环节可以用整图作为框cx0.5、cy0.5、w1.0、h1.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_okTrue) # 写类别映射文件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_okTrue) 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_unicodeTrue) 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 modelyolov8n.pt sourcehttps://ultralytics.com/images/bus.jpgCPU训练的最大问题不是装不上而是训练速度。一张1080p人脸图imgsz640CPU上大概每轮几十秒到几分钟取决于你的核数。我的做法是先用32张图、epochs3把整个流程跑通确认数据格式没问题再全量训练。这一步是为了省掉“训练10小时后才发现数据集路径错了一半”的翻车时间。3. 用YOLOv8训练人脸表情识别预训练权重选择与关键训练参数3.1 预训练权重怎么选yolov8n和yolov8s对人脸表情任务的取舍YOLOv8的预训练权重按尺寸从n到x分档n最轻、x最重。做表情识别我一般只用n或s。理由很简单表情识别的输入是裁剪后的人脸区域目标框通常只有一个背景干扰比通用检测小得多大模型收益有限。yolov8n.pt在COCO上mAP不算高但做单目标微调足够如果后续还要部署到RK3588这类边缘设备n档的权重导出onnx后只有6MB左右非常友好。预训练权重的下载不需要手动找链接ultralytics在你指定modelyolov8n.pt时如果本地没有会自动从官方release拉取。公司内网环境拉不下来时可以提前在有网的机器上yolo detect predict modelyolov8n.pt触发下载再把权重文件拷贝到工作机~/.config/Ultralytics/目录下。3.2 训练命令与yolov8模型训练参数含义一张表说清数据集准备完毕训练命令不需要花哨关键是知道你改的每个参数会带来什么影响。下面是我常用的表情识别训练命令yolo train \ modelyolov8n.pt \ dataemotion_yolo/dataset.yaml \ epochs100 \ batch16 \ imgsz640 \ lr00.01 \ optimizerAdamW \ patience20 \ freeze10 \ device0参数含义拆开说参数作用我的建议值model指定初始权重yolov8n.pt是COCO预训练数据量小用n数据量大用sepochs最大训练轮数先20看loss走势再决定100还是200batch每批图片数显存不够就减小CPU用4~8GPU看显存16G用16~32imgsz训练输入分辨率人脸图建议640原图48x48的要先缩放lr0初始学习率默认0.01loss震荡就降到0.001optimizer优化器数据量小用AdamW收敛稳patience早停轮数验证集mAP连续不涨就停15~20合适freeze冻结前N层参数数据少时freeze10保底层特征数据多可以不冻结device训练设备0是GPUcpu写cpu常见误区是epochs开很大但batch开很小结果loss一直在抖动。表情识别数据集通常几百到几千张batch16、imgsz640在8G显存上已经比较极限如果显存不够优先降batch而不是降imgsz因为人脸小目标受分辨率影响明显。3.3 表情类别不平衡是训练里的暗雷disgust永远不够真实采集的表情数据里happy和neutral往往占大头disgust和fear可能只有几十张。YOLOv8默认不做类别加权结果就是少数类在训练中很少被看到模型到验证阶段对disgust的召回率接近零。我的处理分三档第一档是给少数类过采样即在数据划分时让disgust的样本量通过重复拷贝提升到多数类的30%左右注意拷贝后要重新划分train/val防止同图重复进验证集第二档是训练参数里调class_weightultralytics支持传一个按类别的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(dataemotion_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( sourcetest_images/, conf0.4, imgsz640, saveTrue, ) 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])这段脚本把所有测试图片跑一遍输出类别、置信度和坐标。注意saveTrue会把标注后的图片存到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标注用于YOLOv8json里的坐标和像素值害我白跑两小时现象用labelme标注人脸框后导出的json转txt训练时loss巨大且不收敛。 原因labelme的json里坐标是绝对像素值我忘了除以图片宽高直接把几百像素的x、y写进txtYOLO读到的归一化坐标全部大于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.3val_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张图、epochs3跑通确认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], labeltrain_box_loss) ax1.plot(df[epoch], df[val/box_loss], labelval_box_loss) ax1.set_xlabel(epoch) ax1.set_ylabel(box_loss) ax1.legend() ax2 ax1.twinx() ax2.plot(df[epoch], df[metrics/mAP50(B)], labelmAP50, colorgreen) ax2.set_ylabel(mAP50) ax2.legend(locupper 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”三个字猜它是干什么的。这套从数据整形到训练验证的流程我现在每次跑新表情数据集都会按这个顺序走一遍。画损失曲线的时间不算在训练时长里但它是整个训练里最值得花的五分钟。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网