ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于2200张YOLO数据集的疼痛检测实战:从数据准备到模型调优

基于2200张YOLO数据集的疼痛检测实战:从数据准备到模型调优 疼痛识别这个方向我最早接触是在做养老院跌倒监测项目的时候。当时团队想加一个老人是否处于疼痛表情的判断模块找了一圈公开数据集要么是面部关键点标注的要么是纯分类的真正能直接喂给YOLO做检测的少得可怜。后来自己动手整理了一批才发现这里面的门道比想象中多——疼痛表情和普通表情的边界在哪、标注框该框整张脸还是只框眼睛和嘴、2200张这个量级到底够不够用每一个问题都能卡住你半天。这篇就围绕2200张YOLO医疗健康疼痛检测数据集这个主题把我在数据准备、模型训练、效果调优这条链路上踩过的坑和总结的经验完整讲一遍适合正在做医疗AI、行为识别、或者单纯想拿一个垂直领域数据集练手YOLO的同行参考。1. 疼痛检测到底在检测什么先搞清楚任务定义再谈数据集很多人一看到疼痛检测数据集就默认是表情分类拿到手发现是YOLO格式的标注文件直接懵了。这里必须先把这个任务的定义掰开说清楚否则后面所有的训练策略都是空中楼阁。1.1 疼痛检测和普通表情识别的本质区别普通表情识别比如FER2013那类通常是七分类高兴、悲伤、愤怒、恐惧、惊讶、厌恶、中性。它的输出是一个类别标签输入是一张裁剪好的人脸。而疼痛检测不一样疼痛在医学上有明确的评估体系临床上常用的是PRIPain Rating Index和PPIPresent Pain Intensity但这些是主观量表机器视觉做不了。落到计算机视觉任务上疼痛检测通常拆成两个层次检测层在整张图像中定位出人脸区域判断这张脸是否处于疼痛状态。这就是YOLO格式数据集要解决的问题——输出边界框加类别。强度层进一步判断疼痛的等级无痛、轻度、中度、重度。这一层往往需要回归或者多分类单靠YOLO的检测头做起来比较吃力。2200张YOLO格式的数据集绝大多数情况下解决的是检测层的问题。也就是说它的标注文件里大概率只有一到两个类别比如pain和no_pain或者更细一点pain_face。你在拿到数据集的第一件事就是打开classes.txt或者data.yaml看清楚类别定义这决定了你后面所有的评估指标怎么算。1.2 为什么用YOLO而不是分类网络这里有个很实际的工程考量。如果只是做表情分类用ResNet、EfficientNet这类骨干网络加一个分类头就够了为什么还要用YOLO这种检测框架原因有三个第一真实场景下图像里不止一张脸。医院病房、候诊区、养老院公共区域的监控画面一帧里可能有好几个人。分类网络只能告诉你这张图里有疼痛但没法告诉你是哪个人在疼痛。YOLO天然支持多目标检测一次前向就能输出所有目标框。第二人脸检测和疼痛判断可以端到端完成。传统pipeline是先用人脸检测器如MTCNN、RetinaFace裁出人脸再送进分类网络。这个两阶段方案在部署时链路长、延迟高而且第一阶段漏检会直接导致第二阶段失效。YOLO把两步合一工程上简洁很多。第三数据标注的复用性。YOLO格式的标注class_id x_center y_center width height归一化到0-1是通用格式同一份标注可以无缝切换到YOLOv5、YOLOv8、YOLOv11甚至转成COCO格式喂给其他检测器。而分类数据集一旦标注成文件夹结构想改成检测任务就得重新标。提示如果你拿到的数据集标注文件里坐标没有归一化即数值大于1那它可能是VOC格式转过来的中间产物需要先做归一化处理否则YOLO训练时loss会直接爆炸。1.3 2200张这个量级意味着什么说实话2200张对于通用目标检测任务来说偏少。COCO数据集有十几万张VOC也有上万张。但在医疗垂直领域2200张已经算中等规模了尤其是疼痛这种需要专业标注的场景——标注员得判断这张脸是不是真的在疼这不是随便拉个人就能标的。这个量级决定了你的训练策略必须偏保守不能从头训练。必须用预训练权重COCO上训过的YOLO权重是标配。强数据增强是必须的。Mosaic、MixUp、随机裁剪、色彩抖动这些要开满。要警惕过拟合。2200张图如果类别分布不均模型很容易记住训练集。验证集和测试集的划分要格外小心最好按人划分而不是按图划分——同一个人在不同帧里的表情高度相似按图随机划分会导致验证集泄漏。2. 拿到数据集后的第一轮体检别急着开训我见过太多人拿到数据集直接yolo train一把梭跑完发现mAP只有0.3然后开始怀疑人生。问题往往出在数据本身而不是模型。这一章讲的就是开训前必须做的几项检查。2.1 类别分布统计不均衡是常态疼痛检测数据集最容易出现的问题就是正负样本极度不均衡。真实场景里处于疼痛状态的人脸本来就是少数可能2200张里只有400张是pain剩下1800张都是no_pain。这种1:4.5的比例虽然不算灾难但如果不处理模型会倾向于把所有框都预测成no_pain因为这样就能拿到80%以上的准确率。统计类别分布的脚本很简单用Python扫一遍标注文件就行import os from collections import Counter label_dir datasets/pain/labels/train counter Counter() for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname)) as f: for line in f: cls_id int(line.strip().split()[0]) counter[cls_id] 1 print(counter)跑完你会得到每个类别被标注了多少次。注意这里统计的是框的数量不是图片数量。如果一张图里有多张脸框数会大于图片数。如果发现不均衡处理手段有几个手段适用场景注意事项过采样少数类少数类样本量500容易过拟合需配合强增强类别权重训练时给少数类更高loss权重YOLO需改loss计算逻辑数据增强生成少数类做更多增强增强不能改变语义收集补充数据有渠道获取更多疼痛样本成本最高但最有效2.2 标注质量抽查框歪了比没框更可怕YOLO格式的标注是归一化坐标肉眼看不出来对不对。必须写个可视化脚本把框画到图上随机抽50-100张看一遍。我踩过的坑包括框只框了眼睛和嘴没框整张脸。这种标注在训练时会让模型学到疼痛眼睛眯起来而不是疼痛整张脸的表情组合泛化性极差。框偏移。有些标注员手抖框偏了十几个像素人眼看不出来但模型会学到错误的特征。漏标。一张图里三张脸只标了两张第三张没标。YOLO训练时会把没标的脸当成背景导致模型对类似人脸产生抑制。可视化脚本用OpenCV就能写import cv2 import os img_dir datasets/pain/images/train label_dir datasets/pain/labels/train for fname in os.listdir(img_dir)[:50]: img cv2.imread(os.path.join(img_dir, fname)) h, w img.shape[:2] label_path os.path.join(label_dir, fname.replace(.jpg, .txt)) if not os.path.exists(label_path): continue with open(label_path) as f: for line in f: cls, xc, yc, bw, bh map(float, line.split()) 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.imshow(check, img) cv2.waitKey(0)一张张翻过去遇到可疑的就记下来。这一步花半小时能省掉后面几天的调参时间。2.3 图像尺寸与长宽比分布YOLO训练时默认会把图像resize到固定尺寸比如640x640。如果你的数据集里图像长宽比差异很大——有的是手机竖拍的3:4有的是监控截图的16:9——直接resize会导致严重变形。疼痛表情的关键特征在嘴部和眼周变形会破坏这些区域的纹理。建议统计一下长宽比分布import os from PIL import Image from collections import Counter ratios [] for fname in os.listdir(img_dir): with Image.open(os.path.join(img_dir, fname)) as im: w, h im.size ratios.append(round(w/h, 1)) print(Counter(ratios).most_common(10))如果长宽比集中在1.0-1.5之间直接resize问题不大。如果跨度很大建议用letterbox保持长宽比短边补灰而不是暴力resize。YOLOv5/v8默认就是letterbox但你要确认rect参数和imgsz设置是否匹配你的数据分布。3. 从零跑通第一个baselineYOLOv8训练疼痛检测模型数据检查完接下来就是跑通一个baseline。我推荐用YOLOv8原因是它的API最简洁文档最全而且对新手最友好。YOLOv5虽然经典但仓库维护节奏慢下来了YOLOv11更新但生态还不如v8成熟。3.1 环境搭建CUDA版本是第一个坑YOLOv8依赖PyTorchPyTorch又依赖CUDA。这三者的版本匹配是新手最容易翻车的地方。我的建议是先确定你的显卡驱动支持的CUDA最高版本nvidia-smi右上角能看到。然后去PyTorch官网查对应的安装命令不要凭记忆pip install。ultralytics用pip装最新版即可它对PyTorch版本要求比较宽松。# 查看CUDA版本 nvidia-smi # 安装PyTorch以CUDA 11.8为例具体命令以官网为准 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装ultralytics pip install ultralytics装完验证一下import torch print(torch.cuda.is_available()) # 必须是True print(torch.cuda.get_device_name(0))如果is_available()返回False别急着往下走先把CUDA问题解决。CPU训练YOLOv8在2200张图上跑100轮可能要十几个小时完全不可接受。3.2 数据集配置文件data.yaml怎么写YOLOv8要求一个yaml文件描述数据集路径和类别path: /home/user/datasets/pain train: images/train val: images/val test: images/test nc: 2 names: 0: no_pain 1: pain几个容易出错的点path用绝对路径最稳相对路径在不同工作目录下会出问题。train和val是相对于path的路径不是绝对路径。nc是类别数names的键必须从0开始连续。如果只有检测没有分割不需要segments字段。3.3 训练参数2200张图的保守配置直接上我的配置这套参数在2200张医疗数据上跑出来比较稳from ultralytics import YOLO model YOLO(yolov8s.pt) # 用small版本nano可能欠拟合large容易过拟合 results model.train( datadata.yaml, epochs150, imgsz640, batch16, device0, workers4, optimizerAdamW, lr00.001, lrf0.01, momentum0.937, weight_decay0.0005, warmup_epochs3, cos_lrTrue, patience30, augmentTrue, mosaic1.0, mixup0.1, copy_paste0.1, degrees10, translate0.1, scale0.5, fliplr0.5, hsv_h0.015, hsv_s0.7, hsv_v0.4, save_period10, projectruns/pain, namebaseline_v8s )逐个解释关键参数的选择理由模型选yolov8snanoyolov8n参数量太小在医疗这种细粒度任务上容易欠拟合large和xlarge在2200张图上过拟合风险高。small是甜点区。epochs150配合patience30如果30轮验证指标不涨就早停实际可能跑到80-120轮就停了。lr00.001比默认的0.01小一个量级。小数据集上大学习率容易震荡。cos_lrTrue余弦退火后期学习率平滑下降收敛更稳。mosaic1.0Mosaic增强对检测任务提升明显但要注意它会改变目标的相对尺度对疼痛这种依赖面部整体结构的任务可以适当降到0.8。mixup0.1MixUp在医疗数据上要慎用因为两张脸叠加会产生不存在的表情可能误导模型。0.1是保守值。fliplr0.5水平翻转。注意疼痛表情通常是对称的翻转不会改变语义可以放心用。但如果你做的是左右眼区分之类的任务就不能翻。3.4 训练过程监控看什么指标训练启动后终端会实时打印loss和mAP。重点盯这几个box_loss定位损失应该稳步下降。如果震荡剧烈检查学习率。cls_loss分类损失如果一直不降可能是类别不均衡或者标注有问题。mAP50IoU阈值0.5下的平均精度这是最直观的指标。mAP50-95更严格的指标医疗任务上通常比mAP50低10-20个点。如果训练到50轮mAP50还在0.5以下大概率是数据问题不是模型问题。回头检查标注质量和类别分布。4. 疼痛检测特有的调优思路通用技巧之外的针对性手段跑通baseline只是开始真正拉开差距的是针对疼痛检测这个具体任务的调优。这一章讲几个我在实践中验证有效的方向。4.1 关键点辅助让模型关注眼部和嘴部疼痛表情的核心特征集中在三个区域眉毛下压、眼睑收紧、嘴角下拉。纯检测框标注只告诉模型这张脸是疼痛但没告诉它看哪里。一个有效的改进是引入关键点辅助监督——在训练时额外预测面部关键点推理时只用检测头。具体做法是在YOLOv8的head上加一个关键点分支用公开的人脸关键点数据集如WFLW、300W预训练这个分支然后在疼痛数据上微调。这样模型在学检测的同时被迫关注眼部和嘴部的几何变化对疼痛这种细粒度表情的判别力会明显提升。代价是标注成本增加——你需要给疼痛数据集补标关键点。如果原始数据集没有关键点可以用现成的面部关键点模型如MediaPipe FaceMesh自动生成再人工修正明显错误的。4.2 多尺度训练应对不同距离的人脸医疗场景下摄像头到人脸的距离变化很大。近的可能只有半米床旁监护远的可能五六米走廊监控。同一个人脸在图像里可能占200x200像素也可能只有40x40像素。YOLOv8默认的imgsz640在远距离小脸上表现会打折。两个应对手段多尺度训练设置imgsz为一个范围比如imgsz640配合rectTrue或者手动在dataloader里随机resize到不同尺寸。YOLOv8的multi_scale参数可以开启这个行为。切片推理SAHI推理时把大图切成小块分别检测再合并结果。这对远距离小脸特别有效但会增加推理时间。我实测下来在2200张数据上多尺度训练能让小脸面积32x32的召回率提升8-12个百分点。4.3 难例挖掘把模型反复搞错的样本找出来训练完一轮后用模型在验证集上推理把预测错误的样本挑出来单独看。疼痛检测里常见的难例有几类中性表情被误判为疼痛有些人天生嘴角下垂、眼睑下垂静态表情看起来就像在疼。疼痛被漏检轻微的疼痛表情变化很细微模型可能没捕捉到。遮挡手捂脸、口罩遮挡、侧脸都会影响判断。把这些难例单独整理成一个子集在下一轮训练时给它更高的采样权重或者干脆复制几份加入训练集。这个手段在数据量有限时特别管用往往比调模型结构见效快。4.4 损失函数调整Focal Loss缓解类别不均衡如果类别不均衡严重比如1:10以上YOLOv8默认的BCE损失可能会被多数类主导。这时候可以换成Focal Loss它通过调制因子降低易分类样本的权重让模型聚焦在难样本上。在ultralytics里改损失函数需要动源码位置在ultralytics/utils/loss.py。把分类分支的BCE换成Focal Lossimport torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alpha0.25, gamma2.0): super().__init__() self.alpha alpha self.gamma gamma def forward(self, pred, target): bce F.binary_cross_entropy_with_logits(pred, target, reductionnone) pt torch.exp(-bce) focal self.alpha * (1 - pt) ** self.gamma * bce return focal.mean()改完记得重新编译并验证loss是否正常下降。这个改动有风险建议先在小的验证集上试确认有效再上全量。5. 评估与部署mAP之外你还需要看什么模型训完mAP50到了0.85是不是就可以上线了远远不够。医疗场景对误报和漏报的容忍度和通用检测任务完全不同。5.1 混淆矩阵看清每一类错在哪YOLOv8训练完会自动生成混淆矩阵在runs/pain/baseline_v8s/目录下。重点看两个数pain被预测成no_pain的比例漏报率这个在医疗场景里是最不能接受的。漏报意味着真正在疼痛的人没被发现。no_pain被预测成pain的比例误报率误报会导致不必要的干预浪费人力但比漏报好一些。如果漏报率高说明模型对疼痛特征学得不够需要加强正样本的监督。如果误报率高说明模型对中性表情的边界学得不好需要补充难负样本。5.2 置信度阈值调优不是0.5就够YOLO默认的置信度阈值是0.25推理时但医疗场景需要根据业务需求调整。如果目标是宁可误报不可漏报就把阈值调低到0.15-0.2让更多疑似疼痛的框被保留。如果目标是减少人工复核工作量就把阈值调高到0.5以上只保留高置信度的结果。调阈值的方法是在验证集上画P-R曲线找到业务可接受的平衡点。不要拍脑袋定。5.3 部署时的推理速度优化2200张训练出来的模型部署时可能面对的是实时视频流。YOLOv8s在V100上单帧推理大约5-8ms在Jetson Xavier NX上大约30-50ms。如果部署在边缘设备上几个优化方向导出TensorRTmodel.export(formatengine, halfTrue)FP16精度下速度能提升2-3倍精度损失通常在1个点以内。降低输入分辨率从640降到416或320速度提升明显但小脸检测会受影响。需要根据实际场景的人脸像素大小权衡。批处理如果处理的是离线视频可以攒批推理吞吐量提升显著。注意TensorRT导出后的模型和原PyTorch模型在数值上会有微小差异导出后必须在验证集上重新评估一遍确认mAP没有明显下降。6. 数据集扩展与长期维护2200张只是起点如果你打算把这个疼痛检测能力真正用到生产环境2200张迟早不够。这一章讲怎么持续扩展和迭代数据集。6.1 主动学习让模型帮你挑标注样本主动学习的核心思路是不是所有未标注数据都值得标优先标那些模型最不确定的样本。具体流程用当前模型在未标注池上推理记录每个样本的置信度。挑出置信度在0.4-0.6之间的样本模型最纠结的。人工标注这批样本加入训练集。重新训练重复上述过程。这个方法能让每一份标注预算产生最大的模型提升。在疼痛检测上我实测用主动学习标500张效果相当于随机标1500张。6.2 跨域泛化不同医院、不同设备的数据差异疼痛检测模型在一个数据集上训好换到另一个场景往往掉点严重。原因包括光照差异病房暖光vs走廊冷白光肤色呈现完全不同。摄像头差异手机前置vs监控球机畸变和色彩响应不同。人群差异不同年龄段、不同肤色的疼痛表情表现有差异。应对手段是域随机化增强训练时随机调整色温、亮度、对比度、gamma模拟不同设备的成像风格。YOLOv8的HSV增强只能覆盖一部分更激进的可以用Albumentations做ColorJitter和RandomGamma。6.3 标注规范文档别让第二个人标出不一样的框如果数据集要持续扩展一份明确的标注规范文档是必须的。疼痛检测的标注规范至少要写清楚框的边界是框整张脸还是只框眉眼嘴区域建议框整张脸包含下巴和额头。疼痛判定标准参考PSPIPain Assessment in Advanced Dementia或FLACC量表给出可操作的视觉特征描述。遮挡处理口罩遮挡下半脸时如果眼部特征明显仍然标pain如果完全无法判断标ignore。边界情况打哈欠、大笑、皱眉思考这些容易被误判为疼痛规范里要明确排除。这份文档写起来费时间但能省掉后面无数次返工。7. 几个我踩过的坑和对应的解法最后这部分不讲理论纯讲我在实际项目里遇到的坑以及怎么爬出来的。7.1 验证集泄漏导致指标虚高第一次训完mAP50到了0.92我特别高兴结果一上线就崩了。排查发现验证集和训练集里有同一个人的多张照片模型其实是在认人而不是认表情。后来改成按人划分——同一个人要么全在训练集要么全在验证集mAP50掉到了0.81但这才是真实水平。解法划分数据集前先用人脸识别或者简单的图像哈希把同源图片聚类按簇划分。7.2 数据增强把疼痛表情增强没了有一次开了很强的degrees30旋转增强结果模型对正脸疼痛的检测反而变差了。原因是疼痛表情的关键特征嘴角下拉、眉毛下压在旋转后模型学到的特征和实际推理时的正脸不匹配。旋转增强对通用检测有用但对细粒度表情任务角度不宜超过15度。解法degrees控制在10以内shear和perspective也要保守。7.3 类别名写错导致训练静默失败YOLOv8的data.yaml里如果names的键和标注文件里的class_id对不上训练不会报错但模型学到的类别是乱的。我有一次把0: pain和1: no_pain写反了训完发现所有预测都是反的。解法训练前用脚本验证一遍确保标注文件里出现的所有class_id都在names的键范围内且语义对应正确。7.4 小脸检测召回率低远距离人脸的疼痛检测召回率一直上不去。试过调imgsz到1280速度掉了一半但召回只涨了3个点。后来用SAHI切片推理召回涨了15个点代价是推理时间翻倍。解法如果业务允许延迟用SAHI如果要求实时考虑在摄像头端做变焦或者部署多尺度模型集成。7.5 模型对肤色和性别有偏这个问题比较敏感但必须面对。测试时发现模型对深色皮肤人脸的疼痛检测召回率比浅色皮肤低约7个点。原因是训练数据里深色皮肤样本偏少。解法是在数据收集阶段就有意识地平衡肤色分布训练时用肤色感知的采样权重。疼痛检测这个方向数据质量的重要性远大于模型结构。2200张标注精良的数据配上YOLOv8s这种中等规模的模型在受控场景下能做到0.85以上的mAP50基本可用。但如果标注粗糙、类别混乱、验证集泄漏再大的模型也救不回来。我个人的经验是把70%的时间花在数据上20%花在训练调参上10%花在部署优化上这个比例在医疗AI项目里基本是合理的。数据集不是拿到就能用的它需要你像对待代码一样去审查、清洗、迭代。
返回列表