ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLO疼痛检测实战:2200张医疗健康数据集的训练与避坑指南

YOLO疼痛检测实战:2200张医疗健康数据集的训练与避坑指南 搞目标检测这些年常见的项目无非是安全帽、小麦、违停车辆之类的说实话有点审美疲劳。但前几天一个数据资源让我来了兴趣标题很直接疼痛检测数据集 | 2200张YOLO医疗健康数据集。当时我第一反应是疼痛这玩意儿还能用YOLO检测它是检测皱眉表情还是标注哪里受伤了带着这个疑惑我下载后认真做了一遍标注分析、格式转换、模型训练和部署验证这才发现“医疗健康数据集”和平时用的公开数据集差别很大坑也比想象中多。这篇文章不打算只讲“运行一个训练命令跑通”。我会从数据集的合理性拆解、YOLO格式的细节、训练参数选择、到实际部署和避坑经验把我这几天踩过的坑和总结出的一套可复用流程全部记录下来。如果你正打算用YOLO做医疗图像相关的小目标、表情、姿态或行为检测这篇文章应该能帮你少走至少两周弯路。1. 先把项目本质拆开疼痛检测到底在测什么1.1 疼痛检测不是简单贴个“痛”标签拿到的“疼痛检测数据集”图片并不是那种教科书式的医学影像而是自然场景下的病人照片——有的是正面照有的是侧面照有些甚至是监控视角。标注框也不是给每个器官打框更接近标注出人的面部区域、头部、手部或某个身体区域并为这个框赋予一个与疼痛相关的类别ID。也就是说这个任务的核心不是“找到痛觉神经”而是训练一个视觉模型去捕捉疼痛的外在表征。比如经典的UNBC-McMaster肩痛数据集用面部表情编码来标注疼痛等级而迁移到YOLO目标检测框架里通常做法是先检测“潜在疼痛表现区域”再配合分类模型判断等级。也可以把“疼痛”直接当做一个类别比如0代表pain1代表normal模型的任务就是在画面里把人脸/身体框出来并判断这个框内是否存在疼痛表现。所以不要一上来就套用常规目标检测思路。疼痛检测本质上是一个细粒度视觉任务它对纹理、表情细节敏感对遮挡和光线非常挑剔这为后面的数据清洗和训练带来了不少麻烦。1.2 2200张照片是什么量级目标检测领域常用数据集对比下来COCO约12万张80类目标框约150万个PASCAL VOC约1.2万张20类安全帽/火情这类垂直领域已经有开源数据集动辄上万张这个疼痛检测数据集只有2200张。2200张是典型的“小型垂直数据集”。如果只检测一到两类目标且目标形态相对固定比如人脸加上预训练权重和合理的数据增强是可以做出能用的模型。但如果想同时检测多种疼痛表现、不同身体部位、多个严重程度等级2200张撑死只够摸清规律距离泛化很远。我的结论是这2200张的数据集适合做算法验证、小范围应用原型、教学Demo但直接拿去生产环境需要自行扩展数据。原文标题里的“医疗健康数据集”也提醒我们数据涉及医疗场景质量和伦理要求比一般数据高得多标注的主观性也更强。1.3 一个典型的复合任务架构想直接用YOLO输出“病人是否疼痛”其实并不明智。更合理的做法是级联用YOLO检测身体关键区域人脸、手部、关节不直接判断疼痛把检测到的区域裁出来送入一个轻量分类模型如ResNet/GiN或MLP输出疼痛等级0-3或者保留YOLO的多类别能力让类别直接包含pain_level_0、pain_level_1、pain_level_2、pain_level_3。第二种做法对数据量要求过高2200张分到4个等级里每类只有几百个样本类别平衡会很糟糕。我建议新手优先选第一种先做人脸检测再做疼痛等级分类逻辑清晰且不容易被目标检测的小样本问题卡死。2. 数据集分析与标注格式里的那些坑2.1 YOLO标注文件到底长什么样“YOLO格式”看起来简单每行对应一个目标框class_id x_center y_center width height比如0 0.5321 0.4456 0.2145 0.3221 1 0.5123 0.7698 0.1567 0.2312注意x_center、y_center、width、height 都是除以原始图片宽高后的归一化值。举个例子一张640x480的图某个框左上角位于(160, 120)框宽高为(64, 96)那么x_center (160 64/2) / 640 0.3y_center (120 96/2) / 480 0.35width 64 / 640 0.1height 96 / 480 0.2很多从公开资源下载的数据集看起来是YOLO格式但实际坐标可能是像素值也可能是Pascal VOC的左上右下格式。我拿到这个数据集后第一件事就是写脚本统计坐标范围发现部分txt里最大值超过1.0再看原图对应位置果然有标注越界。安全做法不要盲信脚本文档随机抽20张图将标注画回原图肉眼核对。这是任何数据集分析的第一步。2.2 类别定义模糊的困局这个数据集由于来源不明、没有完整说明存在类别名称混乱的情况。有些txt里类别ID为0、1但names文件缺失有些图片内容明明是一个人却标注了多个重叠框。我的处理办法先对全部类别ID做分布统计看看有几个不同类别随机抽取每个类别对应的图片人工确认类别含义如果发现有两个类别边界模糊比如“轻度疼痛”和“中度疼痛”优先合并成一两个大类。比如我最终把原有4个类别合并成了pain和no_pain两个类别疼痛等级留到后续分类模型处理。这样既保证了目标检测阶段的数据量又不会因为标注主观性破坏模型训练。2.3 清洗与去重的实操清单医疗健康数据的标注主观性强数据清洗尤其重要。以下是我实际执行的清单删除损坏图片用PIL或OpenCV尝试打开失败则删除同时移除同名txt检查图片与txt是否一一对应多余的标注文件统一删除缩放检查如果图片尺寸差异大确保没有读取宽高颠倒的问题删除尺寸小于32x32像素的目标框YOLO对极小目标学习难度大过滤掉能减少账本干扰注意重复图像用dhash算法做近似去重尤其像这种资源往往来自抓取重复率不低。from PIL import Image, ImageChops import imagehash def dedup_images(path_list, threshold5): seen [] dup [] for p in path_list: h imagehash.phash(Image.open(p)) if any(h - o threshold for o in seen): dup.append(p) else: seen.append(h) return dup这步做完2200张可能只剩1800张。虽然数量掉了但训练稳定性大幅提升。2.4 数据集扩展策略宁可少改也别乱生成疼痛检测依赖面部肌肉纹理和动作如果使用mosaic增强过度拼接很可能把不同人的皮肤纹理混杂在一起反而引入错误特征。我建议对于目标检测阶段使用轻微mosaicUltralytics默认的mosaic是0.5~1.0可调低到0.2重点使用水平翻转、尺度变化、HSV微扰不要使用强模糊或重度噪声增强医疗数据更看重原始纹理细节如果实在缺数据考虑引入更多公开的人脸/表情数据集如FER2013、RAF-DB但只能用来预训练分类器不能直接混入这个YOLO训练集因为标签体系不同。基于我在训练中的体验用小数据量训练YOLO最大的敌人不是模型复杂而是标注不一致。3. YOLO模型训练从配置到评估的完整记录3.1 环境准备与数据组织我用的基准环境是Ubuntu 22.04Python 3.10PyTorch 2.1Ultralytics YOLOv8 / YOLO11单张NVIDIA RTX 4090安装很简单pip install ultralytics数据组织按照Ultralytics要求pain_data/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/然后用一个data.yaml描述数据集path: /home/user/pain_data train: images/train val: images/val test: images/test nc: 2 names: 0: pain 1: no_pain划分比例我用了7:2:12100张训练对这个小样本数据集来说合理。需要注意划分时要按人物主体分组避免同一个人的不同图片同时出现在训练集和验证集否则模型可能记住人而并非学习疼痛特征。这一步虽然是常规操作但很多人会忽略。3.2 模型选型不要一上来就上大模型2200张图、2个类别第一原则是从最小的模型结构开始。我对比过YOLOv8n、YOLOv8s和YOLO11s最终留下YOLOv8s。原因是n参数太少对疼痛这种细节特征的表达力不够mAP50在验证集上低3~4个百分点v8m又过大训练收敛慢且容易过拟合。s刚好卡在性价比最高的位置。如果要用预训练权重不要直接用COCO训练出来的完整模型作为起点。更好的是加载COCO预训练权重但冻结backbone前10层只训练后面检测头。因为疼痛面部特征和通用物体猫狗汽车差异较大底层特征可以复用但高层特征需要重新学习。yolo train \ modelyolov8s.pt \ data./pain_data/data.yaml \ imgsz640 \ batch16 \ epochs200 \ lr00.005 \ optimizerAdamW \ freeze10 \ cacheTrue上面freeze10表示冻结模型的backbone前10层。这样在训练早期可以避免小样本把预训练权重打乱。3.3 损失函数和超参数背后的道理YOLOv8的损失由三部分组成box_lossCIoU、cls_lossBCE相关、dfl_lossDistribution Focal Loss。对疼痛检测而言分类损失是最重要的因为很多框本身是准确的人脸困难在于对“是否有疼痛”下判断。具体训练中我关注几个参数imgsz选择了640。不建议为了小目标用1024小样本下参数量和过拟合风险都上升batch16。显存够用就尽量大能帮助BN统计更稳定lr00.005。因为用了冻结层初始学习率比默认的0.01小一半减少震荡epochs200。小数据集训练很快我设了早停patience30实际在第105轮就停下了。我还启用了自动加权处理类别不平衡# ultralytics支持在data.yaml后添加权重配置 # 例如 # weight: [1.0, 2.5] # pain:no_pain 样本比约1:2.5时不过实测下来Ultralytics的类权重需要自己传。我在外部用一个加权采样器解决了pain类复制一份让正负样本比例接近1:1.2。3.4 训练曲线怎么读训练时不要只盯着终端里刷屏的loss。我当时保存了results.csv关心四个指标train/box_loss、train/cls_loss是否持续下降val/cls_loss是否在某个epoch开始反弹metrics/mAP50和metrics/mAP50-95是否同步上升recall是否因为过拟合而掉下来。我这次训练的统计大致指标第50轮第100轮最终bestmAP500.7210.8640.873mAP50-950.5030.6110.629precision0.810.920.91recall0.640.820.84看起来还行但如果只看mAP50很容易被骗。我单独看了no_pain类recall为0.95而pain类recall只有0.71。也就是说模型对正常状态判断很准对疼痛表现却容易漏检。这是小样本医疗数据的典型问题解决思路见下一章。3.5 混淆矩阵和PR曲线不要只拍一张best图Ultralytics在输出里会生成confusion_matrix.png、P_curve.png、PR_curve.png。疼痛检测里我特别建议打印按类别拆分的PR曲线判断“疼痛类”的置信度阈值是否需要单独调整。默认置信度阈值0.25对pain类偏高我最终在推理时对pain类downscale到0.12对no_pain保持0.5总体F1提升了4%。from ultralytics import YOLO model YOLO(best.pt) results model.predict(sourcexxx.jpg, conf0.12, classes[0], verboseFalse)但这里要提醒降低conf会带来大量误报适合初筛不适合自动诊断。伦理相关会在第五章展开。4. 常见问题与排查实录4.1 训练集loss下降、验证集loss上升过拟合了小数据集最容易出现这个现象。我当时第一个版本训练了300个epoch训练集mAP到0.95验证集mAP反而从0.8掉到0.75。看训练曲线val损失在120轮后持续抬头典型过拟合。对策按优先级降低epoch数或增加早停耐心值让模型在过拟合前停下来调低mosaic和mixup比例使用dropoutYOLOv8中可以设置dropout0.1换更小的模型比如从s降到n增加训练集规模和多样性。我实际用了前三种最后稳定在mAP500.87。4.2 疼痛类别的recall非常低pain类recall只有0.71主要原因有三个疼痛表现本来就少图片里疼痛区域占画面比例小标注者存在漏标部分疼痛表情与中性表情很像。排查方法是取出所有被漏检的pain样本逐张看标注框是否有问题同时统计那些框的尺寸分布。如果pain框平均面积小于no_pain框说明小目标漏检严重。解决方案降低检测置信度只适用于推理训练层面应提高pain类的loss权重以及对pain类框做过采样复制。实际操作中我还用了一个小技巧用训练好的模型预测训练集把置信度大于0.5但未匹配到ground truth的框挑出来人工确认是否为漏标。这个“正反馈预标注”帮我多找回了30多张原本标注错误的图片pain类recall提到0.78。4.3 训练中loss突然变成NaN在训练到第40轮时cls_loss突然变成NaN。排查后发现学习率调到0.005后加上batch16AMP混合精度下梯度溢出数据里有一张图片的标注框width或height等于0生成target时出现除零。简单处理关闭AMPamp: False同时过滤掉非法标注框for line in txt: cls_id, xc, yc, w, h map(float, line.split()) if w 0 or h 0: continue第二个原因更隐蔽我在数据清洗时漏掉了异常框。建议在训练前写个脚本对每个txt文件做校验任何坐标小于0或大于1、宽高为负的格式直接剔除。4.4 混淆矩阵总和不是100%如果你发现Ultralytics画出的混淆矩阵对角线数值加起来不是1.0这很正常。因为混淆矩阵中还包括背景Background类别。没有正样本的负样本会被归到“漏检”所以总和小于等于100%。这并不代表模型坏了但要学会看列方向例如No_pain列中如果大量真实pain被归类为no_pain才是真正需要关注的混淆。4.5 小样本数据验证集不稳定同样的数据换一个随机种子mAP波动3~5个百分点。不要把第一次训练结果当最终结论。解决方式是用k折交叉验证k5但代价是训练时间五倍。在预算有限的情况下至少做两次不同seed的验证一次获得评分一次确认稳定性。如果两次mAP50差异超过5%先回头检查数据质量而不是调模型。5. 部署落地与医疗场景的特殊考量5.1 导出ONNX与推理优化训练完best.pt我直接导出ONNXyolo export modelbest.pt formatonnx opset12 dynamicTrue导出后用ONNXRuntime做CPU推理import onnxruntime as ort import cv2 session ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) input_name session.get_inputs()[0].name input_shape session.get_inputs()[0].shape image cv2.imread(test.jpg) image cv2.resize(image, (640, 640)) blob image[:, :, ::-1].transpose(2, 0, 1) / 255.0 blob blob[None].astype(float32) outs session.run(None, {input_name: blob})注意Ultralytics导出的ONNX输出是(1, 4num_classes, 8400)的格式需要自己把8400个预测框做解码和NMS。如果不想写解码逻辑直接用torch推理更容易model YOLO(best.pt) results model.predict(frame, conf0.3)但生产环境用ONNXRuntime能省显存。端侧部署手机、树莓派我建议用NCNN或者TFLiteyolo export modelbest.pt formattflite实际在树莓派4B上的CPU推理FPS大约2~3勉强够离线分析实时视频流还要再做模型剪枝和量化。5.2 实时视频监控里的级联架构病房或康复场景下摄像头是连续的视频流单帧检测疼痛意义有限。我搭了一套级联推理YOLO先检测人脸框用原训练好的YOLO里no_pain/pain都会输出框但其实可以只做人脸检测裁出人脸区域标准化后送入分类模型一个简单的ResNet18输入224x224滑动窗口统计5秒内疼痛等级大于1的帧数占比超过40%才触发预警忽略置信度0.15的弱检测减少误报。这套架构的好处是目标检测阶段不用反复纠结“是否疼痛”把难题交给时序聚合。单帧误报可能很多但窗口统计能把绝大多数误报滤掉。5.3 医疗合规与隐私问题这不是套话必须写在标题下疼痛检测涉及医疗健康数据如果数据集来自临床必须确认知情同意和脱敏处理。即使自己构建数据集也要遵守数据保护原则。实际部署时要注意摄像头方案优先用边缘设备推理避免把原始图像上传到云端存储图像必须打码或模糊处理身份信息最终输出的“疼痛等级”应作为辅助参考不能替代医生判断不要对用户给出“你有疼痛”的医学建议产品文案要谨慎。我见到的很多项目失败不是算法不行而是数据合规没过关。如果你只是开源演示建议使用合成表情图片或明确标注“用于科研演示”。5.4 后续还能怎么扩展2200张的训练集只能算种子。如果你要继续做我建议在公开的UNBC-McMaster数据集或自采数据上扩充补足不同肤色、年龄、光照条件下的样本加入关键点检测用面部关键点间的距离和角度做疼痛特征比纯目标框更鲁棒换成视频输入用SlowFast或TimeSformer这类时序模型疼痛的动态变化才是关键做模型蒸馏用一个较大的YOLOv8m训练然后蒸馏到YOLOv8n兼顾精度和速度。我个人体会最深的一点是医疗健康类数据集标注者的主观性远超想象。同样一张图让我标是“轻微疼痛”换个人可能标“正常”。所以这类项目真正要花精力的不是训练脚本而是建立一套可复现的标注规则并用多个标注者交叉验证。如果你拿到手的数据集没有这份标注规则请务必自己补上。这个动作虽然无聊却是后续一切模型能力的基石。
返回列表