简介:面向深度学习开发者与计算机视觉研究者的YOLOv11人脸检测与表情识别完整工程包,覆盖自定义YOLO模型改进、人脸检测、面部关键点定位与表情分类全流程,适用于智能交互、安全监控、用户行为分析等实时场景。压缩包共1022个文件,约56.71MB,包含396个Markdown说明文档、190个Python源码、97个YAML模型配置文件、59个PyTorch权重文件,以及Docker部署镜像文件、演示视频、示例图片和项目文档,可支撑从模型训练、推理到环境部署的完整链路。已有61人学习浏览,适合作为算法二次开发、实验复现与工程落地的参考。包内包含自定义YOLO网络结构(SPP空间金字塔池化、多尺度特征融合、注意力机制)与表情识别模块,可对照Python脚本和notebook理解改进细节;附带的推理代码、关键点检测脚本及多平台Docker配置,能帮助快速搭建运行环境并验证模型效果。
1. 不是又一个人脸框:YOLOv11 与表情识别这套组合的落地价值
我之前在普通笔记本上跑过一个现成的人脸检测加表情识别 Demo,图像上框得很准,一开摄像头就原形毕露——帧率掉到个位数,表情还清一色识别成“生气”,最后发现是模型把所有偏暗的人脸都判成了负面情绪。真正的问题是:很多人做表情识别只拿一个分类模型硬怼整张图,而这份资源走的是两段式路线——先用 YOLOv11 把脸稳定框出来,再把裁切后的人脸交给一个自定义 YOLO 变体做表情分类。它解决的不只是“能不能识别”,而是“在摄像头场景下还能不能保持稳定”。适合正在做毕设、课程设计,或者刚入门 CV 想做出一个完整系统的人。想复现的话,关键不在跑通 Demo,而在数据格式、模型头设计和部署三个环节。
2. 数据准备:公开数据集打底、自定义数据调优的完整处理流程
2.1 公开数据集与自定义数据怎么选
表情识别和人脸检测是两类不同的数据任务。人脸检测需要的是“整图 + 人脸框”,表情识别需要的是“人脸图 + 表情类别”。这套系统里,YOLOv11 负责检测,所以它需要的是前者;自定义 YOLO 负责表情分类,需要的是后者。常见做法是:先用公开人脸检测数据集预训练 YOLOv11 的检测权重,再拿自定义数据做场景微调。表情分类模型则直接用小规模的人脸表情数据集训练。
| 数据来源 | 规模 | 标注精度 | 场景覆盖 | 适用环节 |
|---|---|---|---|---|
| 公开通用人脸数据 | 大 | 框位准,类别粗 | 多样但不覆盖你的实际场景 | YOLOv11 预训练 |
| 公开表情数据 | 中 | 类别齐全 | 人脸基本居中、表情偏夸张 | 表情分类模型初训 |
| 自定义采集数据 | 小 | 由你控制 | 贴合摄像头真实场景 | 二次微调、域适配 |
实际训练时不要一上来就完全依赖自定义数据,量不够容易过拟合。我一般会在公开数据上把权重训到收敛,再把自定义数据混进去微调,比例控制在 3:1 到 5:1 之间。
2.2 把数据组织成 YOLO 能吃的格式
YOLOv11 和自定义 YOLO 模型的人脸检测部分都要求 YOLO 格式的标签:每张图片对应一个同名 txt,每行是“类别 id 中心点x 中心点y 框宽 框高”,坐标都是相对图片宽高的归一化值。表情分类部分不需要框,直接用裁切后的人脸图和类别索引。
import os import random from pathlib import Path IMG_EXTENSIONS = {'.jpg', '.jpeg', '.png'} dataset_root = Path('dataset') target = dataset_root / 'labels' # 标签目录 image_paths = [] for ext in IMG_EXTENSIONS: image_paths.extend(dataset_root.glob(f'images/*{ext}')) random.shuffle(image_paths) train_ratio = 0.8 split_idx = int(len(image_paths) * train_ratio) def write_split(file_list, split_name): with open(dataset_root / f'{split_name}.txt', 'w') as f: for img_path in file_list: label_path = target / (img_path.stem + '.txt') if label_path.exists() and label_path.stat().st_size > 0: f.write(str(img_path.resolve()) + '\n') write_split(image_paths[:split_idx], 'train.txt') write_split(image_paths[split_idx:], 'val.txt')这段代码做的事情很基础但非常关键:把图片路径按 8:2 拆成训练集和验证集,并且只保留那些确实有标签文件的图片。第一版我直接全量写入,结果十几个空标签图片混进训练集,模型把无脸图也学进去了,推理时频繁误检。
参数上注意train_ratio = 0.8是常见默认值,但如果自定义数据量特别小,建议调到 0.85 甚至 0.9,给训练多留样本。空标签过滤这里必须做,因为标注软件偶尔会漏存文件,漏一张就是一颗雷。
2.3 标注清洗:训练前必做的数据体检
很多人拿到标注数据就开训,结果 loss 怎么也降不下去。最常见的三个问题:标签类别 id 和模型类别配置对不上、标注框面积过小、某些表情类别样本量只有个位数。清洗这一步就是训练的后悔药。
from collections import Counter import os label_dir = 'dataset/labels' category_counter = Counter() tiny_box_count = 0 empty_label_count = 0 for label_file in os.listdir(label_dir): path = os.path.join(label_dir, label_file) with open(path) as f: lines = f.readlines() if len(lines) == 0: empty_label_count += 1 continue for line in lines: parts = line.strip().split() cls_id = int(parts[0]) w, h = float(parts[3]), float(parts[4]) category_counter[cls_id] += 1 if w * h < 0.001: # 归一化面积小于千分之一 tiny_box_count += 1 print('类别分布:', dict(category_counter)) print('空标签文件数:', empty_label_count) print('过小框数量:', tiny_box_count)这段脚本在训练前花两分钟跑一遍,能避免大多数低级翻车。类别分布是重点:如果“惊讶”只有十几张,要么去补数据,要么在训练配置里给这个类别加权。过小框我通常直接过滤掉,因为人脸检测场景下人脸占比一般不会低于千分之一,出现这种框基本都是标注错的。
3. 模型训练:YOLOv11 选择、自定义模型头与超参调节
3.1 YOLOv11 模型大小怎么选
YOLOv11 提供不同规模版本,核心区别是主干宽度和深度。n 系列最快但特征提取能力弱;s 是多数人的平衡点;m 和 l 适合显卡显存充足、对精度要求高的场景。做表情识别系统,人脸检测只是前置环节,不需要过度追求检测精度,所以优先保证推理速度。
| 模型规模 | 显存占用 | 推理速度 | 适用算力 | 本项目建议 |
|---|---|---|---|---|
| n | 小 | 快 | 无独显也能跑 | 不推荐,小脸漏检明显 |
| s | 中等 | 较快 | 入门独显即可 | 默认选择 |
| m | 较大 | 中等 | 6GB 以上显存 | 数据量大时可换 |
| l | 大 | 慢 | 8GB 以上显存 | 追求极限精度才用 |
我在这套系统里推荐用 s,因为摄像头场景下的人脸往往有一定的像素面积,s 的检测能力完全够用,省下来的算力要给后面的表情分类模型留出来。表情分类模型本身很小,两者加在一起才可能在普通设备上保持可用的帧率。
3.2 自定义 YOLO 模型头的设计方式
标题里“自定义 YOLO 模型”这个说法我拆过几种实现,最常见的不是从零写一个网络,而是复用 YOLOv11 的 backbone 和 neck 做特征提取,把检测头换成分类头。这样可以拿到 YOLOv11 预训练权重做迁移学习,又不用重新设计特征层。
# custom_expression_model.yaml backbone: name: yolov11_s_backbone pretrained: true neck: name: yolov11_s_pan out_channels: [256, 512, 1024] head: name: classification_head in_channels: 1024 # neck 输出的最大特征层 pooling: global_avg_pool # 全局平均池化 fc: [1024, 512] num_classes: 7 # 表情类别数 dropout: 0.2这个配置文件的意思是把 YOLOv11 前面的特征提取部分原样保留,但在最后不再输出预测框,而是把三个尺度的特征合并后做全局平均池化,再接两层全连接输出 7 类表情概率。num_classes必须和你实际数据集的类别数一致,差一位都会在训练时报错或者指标混乱。
这里的关键点是neck.out_channels要与 backbone 实际输出对齐。我第一次改的时候想当然写成了 512,维度对不上,训练直接崩,属于非常典型的自定义头翻车现场。建议先读取预训练权重打印结构,确认每一层的通道数再填进去。
3.3 训练超参数设置与启动命令
训练脚本和参数直接影响最终效果。我用的是一套经过多次验证的参数组合,可以直接作为起点。
from ultralytics import YOLO model = YOLO('yolov11s.pt') # 用官方预训练权重初始化 model.train( data='dataset.yaml', # 数据集配置文件 epochs=100, # 训练轮次 batch=16, # 批大小,根据显存调整 imgsz=640, # 输入分辨率,检测模型常用 lr0=0.01, # 初始学习率 lrf=0.01, # 最终学习率系数 optimizer='SGD', # 优化器 weight_decay=0.0005, # 权重衰减 warmup_epochs=3, # 预热轮次 cos_lr=True, # 余弦退火学习率 workers=4, # 数据加载线程数 device=0 # 使用第一块 GPU )逐参数说明:imgsz=640是检测模型的标准输入,不要随意调大,显存占用按平方上升;batch=16是一个比较保险的值,显存不足时先降 batch 而不是降分辨率;lr0=0.01是微调时的常用值,如果是从头训练可以适当提高到 0.02,但风险也在增加;warmup_epochs=3让学习率从 0 渐入,能有效防止前期 loss 飞掉;cos_lr=True让学习率缓慢下降,比固定学习率更容易收敛到好的局部最优点。
表情分类模型是单独训练的。把自定义模型定义好之后,训练命令和上面类似,只是输入是人脸裁切图,分辨率通常降到 112 或 128,因为表情识别不需要太高的分辨率,分辨率过大反而容易过拟合。
3.4 训练过程的监控与止损
训练不是把命令丢进终端就完事了。我每轮都会看一眼 loss 曲线和验证集 mAP。loss 在最初几个 epoch 下降很快是正常的,但如果 30 轮之后还在剧烈波动,先看学习率是不是太大。mAP 上不去但 loss 在降,大概率是过拟合,这时增加数据增强或减小模型规模比继续训练更有效。
一个比较玄学的经验是:验证集指标高但实际摄像头效果差,先检查训练集和测试集的图像分布。公开数据集大多是摄影师拍的正脸、光照均匀,摄像头场景下是斜脸、背光、动态模糊,模型自然水土不服。解决方式是混入 10% 左右的摄像头实拍数据重新微调,效果立竿见影。
4. 推理与部署:从图片检测到摄像头实时识别的完整链路
4.1 两段式推理管线的实现顺序
系统运行时的流程是:YOLOv11 首先对整帧图片做人脸检测,得到边界框;然后按边界框裁切人脸区域,缩放到固定尺寸;最后把缩放后的人脸图送进表情分类模型,得到表情类别。
import cv2 import numpy as np def run_inference(frame, detect_model, expression_model, conf_thres=0.5): results = detect_model(frame, conf=conf_thres, verbose=False)[0] boxes = results.boxes.xyxy.cpu().numpy().astype(int) output = [] for x1, y1, x2, y2 in boxes: face_crop = frame[y1:y2, x1:x2] if face_crop.size == 0: continue face_resized = cv2.resize(face_crop, (112, 112)) face_input = face_resized[:, :, ::-1].transpose(2, 0, 1) # BGR转RGB face_input = np.expand_dims(face_input, axis=0).astype(np.float32) / 255.0 pred = expression_model(face_input) expr_id = pred.argmax(axis=1)[0] output.append((x1, y1, x2, y2, int(expr_id))) return output这段代码是整套系统的核心粘合层。注意conf_thres=0.5是检测置信度阈值,摄像头场景建议降到 0.35 到 0.4,因为动态模糊会让置信度整体偏低。face_crop必须判空,检测框越界时会切出空的数组,直接送进分类模型会报错。112x112是表情分类模型的输入尺寸,和训练时保持一致,不一致会导致精度断崖式下降。
4.2 摄像头实时推理的帧率优化
实时视频流和单张图片推理有一个本质区别:连续帧之间存在大量重复信息。如果每一帧都做完整的两段式推理,多数电脑扛不住。
cap = cv2.VideoCapture(0) frame_skip = 2 # 每隔2帧处理一次 frame_count = 0 while True: ret, frame = cap.read() if not ret: break frame_count += 1 if frame_count % frame_skip != 0: continue # 可选:缩小输入尺寸加快检测 frame_small = cv2.resize(frame, (640, 480)) detections = run_inference(frame_small, detect_model, expression_model) # 在原始帧上绘制检测结果 for x1, y1, x2, y2, expr_id in detections: cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, str(expr_id), (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow('face_expression', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break参数上frame_skip=2代表每三帧处理一帧,手感基本无感,但推理负载降一半。如果还是卡,优先把输入帧从 1080p 缩到 720p,损失一点小脸检测精度,换回流畅度。注意detect_model内部也做了 resize,这种双重缩放会让标注框位置偏大,所以我在实际项目里更推荐只对检测模型输入做缩放,不在循环里额外缩小帧。
4.3 置信度阈值与 NMS 参数的现场调优
检测结果被 NMS 过滤后,能留下来的框取决于两个参数:conf_thres控制候选框的置信度底线,iou_thres控制重叠框的合并力度。这两个参数是现场调参的常客。
| 参数 | 值 | 效果 | 适用场景 |
|---|---|---|---|
| conf_thres | 0.5 | 干净但容易漏检 | 图片质量好、人脸清晰 |
| conf_thres | 0.35 | 均衡 | 摄像头、有动态模糊 |
| conf_thres | 0.25 | 召回高但误检增加 | 远距离、小脸场景 |
| iou_thres | 0.45 | 默认值 | 单人脸和多人脸均可 |
| iou_thres | 0.6 | 重叠框合并更激进 | 侧脸、遮挡场景 |
实际调参时不要只盯检测框数量。我一般会在同一帧上连续滑动 conf 值,观察框的位置是否稳定。如果框在脸上轻微抖动,说明 iou 阈值偏低;如果同一个人的脸被框了两次,说明 iou 阈值偏高。这个调节过程没有捷径,只能多看几帧。
5. 常见问题与避坑:训练和部署中最容易翻车的几个点
5.1 表情类别和索引错位,模型训练却没报错
现象:训练时 loss 正常下降,验证集准确率也不错,但跑摄像头推理时所有表情都多偏一位,或者“开心”和“难过”互换。
原因:数据集标签的类别 id 和模型配置文件里的类别顺序不对应。标注软件生成的 txt 里写的是 0 到 6,但 model.yaml 里的类别列表可能是从 1 到 7。
解决:写一个脚本读取第一张标签文件和模型配置,逐行打印类别 id 和名称的对应关系。
# 检查标签id与类别名映射 cls_names = ['anger', 'disgust', 'fear', 'happy', 'neutral', 'sad', 'surprise'] label_file = 'dataset/labels/00001.txt' with open(label_file) as f: line = f.readline().strip() cls_id = int(line.split()[0]) print(f'标签文件里的id: {cls_id}') print(f'模型配置里的对应类别: {cls_names[cls_id]}') print(f'实际表情: 根据图片人工确认')先确认一张图,再确认代码路径里 cls_names 的顺序和训练配置一致。从那以后我再也没犯过这个错,但这个坑几乎每隔一段时间就会出现在评论区里。
5.2 摄像头推理卡顿,显示帧率却没有明显降低
现象:摄像头画面看起来一顿一顿的,但代码里打印 FPS 有 20 多。
原因:打印的是检测线程的处理频率,不是画面显示频率。摄像头采集和检测是串行关系,检测偶尔慢一帧,但 FPS 统计只算处理成功了的时间,或者队列里积压了旧帧。
解决:用时间戳统计“从读到帧到画完框”的完整耗时,并且在循环里手动清空队列积压帧。具体做法是把cv2.VideoCapture的缓冲区调小,或者每次read()前循环读取并丢弃若干旧帧。
5.3 验证集 mAP 很高,一到现场就乱框
现象:公开数据上训练的模型,换到教室、办公室等真实场景,框的位置飘,表情也不准。
原因:域差异。公开数据集大多是摆拍正脸、光照充足、背景干净;现场有逆光、侧脸、低头、遮挡。
解决:在数据集中混入 20% 左右的现场实拍数据重新微调。我把实拍数据单独放一个文件夹,用 0.4 的学习率系数微调,损失函数权重不变,效果提升明显。如果没有实拍数据,先做随机亮度扰动和水平翻转也能缓解。
5.4 训练 loss 突然变 NaN,之前的权重全白费
现象:训练到某个 epoch,loss 变成 nan,之后所有指标归零,之前的训练成果等于作废。
原因:最常见是学习率过高或数据里出现了异常的标签值。比如坐标大于 1 的归一化框、类别 id 超出配置范围,让损失函数算出了无穷大。
解决:先检查标签文件里有没有坐标 >1 或 <0 的行,把异常行过滤掉。然后降低初始学习率到 0.005,并开启warmup_epochs。如果还是 nan,检查输入图片有没有全黑或全白的损坏文件,这类图片会让特征的方差为 0。
5.5 自定义模型接 YOLOv11 特征时维度对不上
现象:定义好自定义分类模型后,一前向传播就报矩阵维度错误。
原因:neck 输出的特征层通道数和分类头in_channels不一致。YOLOv11 不同版本输出通道不一样,s 和 n 的通道数差异很大,填错其中一个就对不上。
解决:先加载官方预训练权重,打印模型每一层输出的张量形状,再把打印结果填进自定义配置的in_channels。我通常会在脚本里加一段代码,直接遍历注册到模型上的所有模块并输出 shape,一步到位,不做任何猜测。
6. 进阶用法:从跑通到真正可用的三条细节技巧
6.1 用验证集做置信度校准
表情分类模型输出的概率分布往往偏保守,实际使用时很容易所有类别概率都在 0.3 到 0.5 之间,argmax 不准。常见做法是在验证集上统计每个类别的平均概率,然后算出每个类别的校准偏移。
import numpy as np calibration = {} for class_id in range(num_classes): scores = validation_scores[validation_labels == class_id] calibration[class_id] = 1.0 / (scores.mean() + 1e-6) calibrated = raw_probabilities * np.array([calibration[i] for i in range(num_classes)]) pred_class = calibrated.argmax(axis=1)这段代码把概率偏低的类别放大,偏高的类别压低,相当于给分类头做了个通道注意力。参数上注意calibration的系数不要大于 3,否则会过度放大噪声类别,反而把准的类别带偏。
6.2 把模型导出成加速引擎格式
部署阶段,PyTorch 动态图的推理开销比较大。我一般在训练收敛后,把 YOLOv11 检测模型和自定义表情模型都导出成静态图格式,再转成半精度权重跑。静态图的好处是输入尺寸固定后,网络结构被提前展开,推理路径短了很多。导出后记得重新用验证集跑一遍精度,半精度在小数点后几位有截断,通常精度损失在 0.5% 以内,超过这个范围就要检查有没有数值溢出。
6.3 用眼睛状态做表情的辅助判断
很多情况下表情识别误判来自嘴巴和眼睛的状态冲突。一个实用的技巧是在检测到人脸后,额外切出眼睛区域,用一个二分类模型判断“睁眼/闭眼”。如果嘴巴是笑的但眼睛闭着,多半是打哈欠或假笑;如果眼睛睁大且眉毛抬高,大概率是惊讶。这个小分支模型很小,单独跑一遍只增加几毫秒,但能把惊讶和开心、难过和困倦之间的混淆明显拉开。
这套系统我从训练到部署完整跑过几轮才稳定下来。最直接的教训是:换任何一批新数据,都要先跑一遍标签检查和类别分布脚本,再开始训练。每一轮自定义模型头修改后,也要先打印输出维度再训练。这些都是十分钟内能做完的事,但能省掉后面数小时的重训成本。希望帮到你。
本文还有配套的精品资源,点击获取