把"疼"这件事交给机器来判断,听起来有点反直觉。疼痛是纯主观体验,患者说疼就是疼,可临床上就是存在大量"患者说不清疼"的场景——重症监护里的镇痛需求评估、术后苏醒期的躁动判断、婴幼儿和认知障碍患者的疼痛识别,这些场景根本没法靠一张量表解决。于是自动疼痛检测成了医疗健康AI里一个很务实的细分方向,而支撑它的第一步,就是一套靠谱的疼痛检测数据集。这篇文章我会完整复盘一个围绕"2200张YOLO医疗健康数据集"落地的实战项目,从数据怎么采集、标注方案怎么定、YOLO系列怎么选,到训练评估阶段那些文档里不会写的坑,一次说清楚。适合想用YOLO做医疗视觉检测的开发者,也适合正准备自建小规模标注数据集的团队参考。
1. 疼痛检测到底在检测什么:先明确任务边界,再选模型
1.1 从主观量表到客观视觉指标
医院的疼痛评估基本还是靠量表:VAS视觉模拟评分、NRS数字评分,让患者自己画线、自己报数字。这套流程在门诊偶尔用还行,但放到重症、术后、新生儿场景就彻底失灵了——患者要么镇静中,要么表达不清,要么根本没有表达能力。这也是过去二十多年研究者一直在做"客观疼痛评估"的原因,核心思路是捕捉疼痛引起的可测量行为信号,其中最成熟、最稳定的就是面部表情。
面部在不同疼痛程度下会触发特定的表情动作单元,比如皱眉、眯眼、鼻翼扩张、嘴角拉扯。这些动作的组合和疼痛强度有统计学相关性,所以疼痛检测数据集普遍以人脸图像为主体。机器要做的事,就是把这组面部视觉特征映射到"有没有疼"或者"疼到几级"的输出上。
我构建的这个数据集,2200张图片不是随手收集的"疼脸图",而是围绕任务边界专门设计的:检测主目标是人脸区域,类别体系是疼痛事件的有/无以及0到3级的程度分级。四个类别的比例在采集时故意做了控制,没有平均分配,因为真实病房里"轻度不适"远多于"重度疼痛",这种先验分布必须体现在数据里,不然后面训练出来的模型会严重偏向高频类。
1.2 YOLO在疼痛检测任务里的真实定位
这里先泼盆冷水:用YOLO做疼痛检测,通常不是让模型直接"一眼识别疼痛等级",而是把它放在一个更大的处理链路里。YOLO是目标检测器,强项是快速定位目标区域并给出类别置信度,它最适合承担的是"把脸框出来、把手术区域框出来"这类前置定位工作,而不是做细粒度等级判断。
我当时采用的方案是两级结构:第一级用YOLO检测图片或视频帧中的人脸,输出脸部bbox和粗类别;第二级把裁切后的脸部区域送进一个轻量分类网络,做细粒度疼痛等级打分。也有人直接用YOLO的多类输出完成"疼痛/非疼痛"的粗分类,实测效果也能用,但一旦涉及0到3级这样的分级任务,我建议还是交给专门的分支处理更稳。
这种"检测定位+分类识别"的分工,直接决定了标注阶段必须同时关注两个质量维度:bbox是否紧贴目标,类别标签是否准确。很多初学者在这里会用通用目标检测的思路标注,觉得"大概框住就行",结果训练时会暴露严重的domain bias,推理时遇到头部倾斜、手部遮挡就疯狂漏框。这个问题我在下一章展开讲。
2. 2200张数据怎么攒出来:采集、清洗、标注的全链路
2.1 数据来源与合规前提
医疗健康数据集的构建,第一关不是技术,是合规。人脸在绝大多数国家都被视为敏感生物识别信息,项目启动前就必须确认:数据有没有脱敏,有没有授权协议,能不能开放二次加工。以这个数据集为例,来源分三块:一是有公开许可的学术疼痛表情数据,允许研究用途进行二次加工;二是合作医疗场景中采集并完成去标识化的病房图片;三是为了补充特定角度和光照条件,由志愿者在签署知情同意书的前提下拍摄的模拟疼痛表情。
三块数据的价值权重也有讲究。真实病房数据最宝贵,但样本量少、场景单一;公开数据规模大,但分辨率、光照、年龄分布可能跟部署环境不匹配;模拟表情能补足角度和多样性,但"表演疼痛"和真实疼痛在微表情动态上存在差距。最终2200张的构成,我按"真实数据约700张、公开来源约1000张、模拟扩充约500张"来控制,这个比例是在真实度和覆盖度之间取的平衡。
2.2 标注方案设计:检测框和疼痛等级分开标
疼痛检测数据集的标注,关键问题是"检测目标到底是什么"。我采用的是两阶段标注方案:先标人脸bbox,再给每个人脸打上疼痛程度标签。因为后面要接分类分支,K类标签必须保持一致,这里我设计了4类:0代表无明显疼痛,1代表轻度(轻微皱眉等响应),2代表中度(表情变化明显、伴随身体紧张),3代表重度(明显的面部扭曲或发声表现)。
标注工具用的是CVAT,原因是多人协同标注时它能做任务分片和复核,比单机版LabelImg高效很多。这个环节有个特别容易被忽略的点:一致性校验。疼痛等级本质上是主观评分,两个人可能对同一张脸给出不同等级。所以每个样本至少要有两位标注者独立标注,遇到分歧再由第三人仲裁。我当时的Kappa一致性系数做到了0.82以上,才敢把数据送进训练。没有这步校验,后面所有模型指标都是虚的。
2.3 清洗、增强与数据划分
标注完成后要做的第一件事是清洗。按优先级排查四类问题:模糊(对焦不准或运动模糊)、过曝(医疗场景白平衡经常出问题)、遮挡(口罩、设备、手部遮挡超过40%)、标签错误。清洗防的是脏数据污染梯度,宁可少100张干净图,也别多留500张错误图。
数据增强方面,我用了水平翻转、小角度旋转、随机亮度对比度扰动,以及轻微的HSV色域增强。这里特别提醒一句:不要对医疗人脸数据做太夸张的几何增强。切边、大角度旋转、弹性形变虽然能在验证集上刷高指标,但会破坏临床图像的真实形态,模型在真实病房里会表现为"能识别训练分布里的脸,认不出真实环境里的脸"。增强是为了模拟真实变化,不是为了造出玄幻图片。
划分比例我自己的习惯是train:val:test=7:1.5:1.5,对应1540张训练、330张验证、330张测试。验证集和测试集必须严格同分布,测试集是"留到最后才碰"的那一部分,这个纪律直接决定最终评估的可信度。
3. 从标注文件到YOLO训练格式:格式转换与模型选型
3.1 标注格式转换的细节坑
标注完成后,CVAT导出的是COCO JSON格式,而YOLO训练流程用的是txt格式。很多人第一次跑会报"no labels found"之类的错,就是因为格式没转。转换逻辑本身不复杂:把COCO的像素XYWH转成归一化的中心点坐标,类别ID映射到YOLO连续整数编号。示意代码可以这样写:
import json from pathlib import Path def coco_to_yolo_txt(json_path, img_dir, out_dir, categories): with open(json_path) as f: data = json.load(f) for img in data["images"]: img_w, img_h = img["width"], img["height"] anns = [a for a in data["annotations"] if a["image_id"] == img["id"]] lines = [] for a in anns: cls_id = categories[a["category_id"]] x, y, w, h = a["bbox"] x_center = (x + w / 2) / img_w y_center = (y + h / 2) / img_h w_norm = w / img_w h_norm = h / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}") out_file = out_dir / (Path(img["file_name"]).stem + ".txt") out_file.write_text("\n".join(lines))这里有一个很常见的坑:COCO的bbox是左上角原点的像素坐标,YOLO要的是归一化中心坐标。有些人想当然地"除以图片宽高"就完事,结果忘了把x,y先转成中心点,训练出来的loss全程不降还要怀疑是模型的问题。转换完一定要抽查十张图,把txt里的坐标反算回图片上画框验证一遍。目录结构也要按ultralytics的习惯放好:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/3.2 YOLO系列选型:小数据量场景做什么决策
2200张的小规模医疗数据集,我最推荐YOLOv8n或YOLOv8s。原因有三:第一,模型容量和数据量匹配,n版本约300万参数,不容易把训练集学穿;第二,ultralytics生态对迁移学习、训练曲线、导出部署支持最完整,省去大量自造轮子的时间;第三,医疗场景往往要求部署到低成本设备或移动端,轻量模型在推理延迟上的收益,远大于几个百分点的mAP提升。
这里多说一句社区里很火的"efficient head"轻量检测头思路。在小数据集背景下,检测头的复杂度和训练稳定性成负相关——头越复杂,在有限样本上越容易过拟合,也越容易出梯度异常。想做模型改进的话,优先在检测头轻量化、特征融合简化上做文章,别一上来就堆注意力模块,否则训练稳定性会让你怀疑人生。
训练前确认预训练权重。从COCO预训练权重开始迁移,是当前性价比最高的做法。COCO上的检测特征已经包含"人脸轮廓、边缘、纹理"这类底层视觉模式先验,我们要做的只是微调高层语义。这个选择能直接节约60%左右的训练轮次,让模型更早进入有效收敛状态。类别映射表也要提前固定好,比如0代表pain_none,1代表pain_mild,2代表pain_moderate,3代表pain_severe,这个表一旦训练开始就不能改。
4. 训练阶段最常踩的四个坑:过拟合、BN崩溃、样本不均衡与损失不收敛
4.1 过拟合:2200张数据的第一杀手
一个300万参数的检测模型,放到2200张图上,过拟合是大概率事件。我的判断标准很简单:训练集loss持续下降,验证集mAP从某个epoch开始不再上升甚至下跌,train和val的loss曲线逐渐劈叉。
解决方案除了前面讲到的数据增强,最有效的是分期冻结训练。先用预训练权重冻结骨干层(freeze=10),只训练检测头,让模型先快速适应疼痛数据集的标签分布;跑50个epoch之后解冻骨干,用小学习率微调整个网络,学习率大约设成原来的十分之一。这样能防止模型一上来就把大量参数砸向训练集的噪声细节,尤其在医疗图像噪声偏高的情况下,这个操作几乎等于"保命"。
4.2 BN崩溃:小batch size下的隐形雷
"yolo训练中bn崩溃"是我在社区里看到的高频词,一开始没当回事,直到某次训练loss直接变成NaN才重视起来。BN崩溃的典型诱因是batch size太小,导致BatchNorm层在计算均值和方差时统计震荡剧烈;医疗图像又经常有大面积暗部,单张图内部像素分布不均匀,BN统计量更容易失控。
我的应对经验:batch size尽量不低于8。如果显存不够,优先降输入分辨率(640降到416),也不要硬把batch压到4;数据加载时对所有图片做统一的均值和方差归一化,能明显降低BN统计量的抖动。训练一旦出现NaN,立刻回滚到崩溃前最近的正常检查点,然后把当前batch里的异常样本捞出来看,别盲目调学习率,多半是数据或者归一化的问题。
4.3 样本不均衡:重度疼痛样本太少怎么补
前面提到,真实场景四个疼痛等级天然不均衡。我在YOLO训练时发现,模型对重度疼痛的recall明显偏低,因为第3级样本只有一百多张。处理方法是两步并行:一是对少数类做过采样,复制时配合增强变化;二是在损失函数层面给少数类更大权重。ultralytics支持设置class weights参数,我按[1, 1.2, 1.5, 2.0]做递增序列,让模型在梯度更新时更关注重度样本。
但这个操作有个副作用:模型会变得过度激进,把轻度疼痛也判成中度。所以调完权重之后必须回看验证集混淆矩阵,确认提升的是少数类的召回率,而不是整体乱猜。样本不均衡的解法本质是个杠杆,方向对了能撬动指标,方向错了会摧毁精确率。
4.4 损失曲线观察与超参数参考
YOLOv8的训练曲线一般看box_loss、cls_loss、dfl_loss三条。正常节奏是:前10个epoch三条loss快速下降,中间50个epoch缓慢收敛,后期小幅波动。如果cls_loss在中途突然反弹,多半是学习率过大或者数据里有标签冲突样本。前者调低lr,后者回头查标注,往往能找到两张"同一张脸被标成两个等级"的问题图。
我实测下来一组稳定的基线参数供参考:
| 参数 | 推荐值 |
|---|---|
| model | YOLOv8n |
| epochs | 100 |
| imgsz | 640 |
| batch | 16 |
| optimizer | SGD |
| lr0 | 0.01 |
| weight_decay | 0.0005 |
| freeze | 10(epoch 60前冻结骨干) |
| conf_thres | 0.5 |
| iou_thres | 0.45 |
这组参数下,2200张数据大约40到60个epoch就能让mAP50摸到0.85以上。再往后训练大概率过拟合,学会收手比学会硬训练更重要。
5. 别只盯着mAP:医疗场景下的评估指标与部署落地
5.1 看混淆矩阵,更要看错误方向
目标检测社区习惯用mAP作为主要指标,但在医疗AI场景里,mAP远远不够。mAP是平均值的艺术,它会把"把轻痛判成无痛"和"把无痛判成重痛"一视同仁地算进误差,而临床上这两类错误的影响完全不同。对疼痛检测来说,漏报意味着患者得不到镇痛干预,虚报顶多是护士多跑一趟。怕的是漏报,不是虚报。
我的评估习惯是三步走:先看mAP50和mAP50:95,确认模型整体可用;再打开混淆矩阵,统计每个疼痛等级之间的错误迁移方向;最后专门算一个"临床显著漏报率"——即实际等级大于等于二级但被预测成0级或1级的样本占比。这个自定义指标比mAP更能反映模型在真实场景里有没有用。第一次拿到YOLO混淆矩阵的人可能会疑惑,为什么行和列的总数对不上。这是NMS和置信度阈值过滤导致的,低于阈值的框不进入最终输出,所以矩阵里没有对应条目,不代表程序算错了。
5.2 从训练指标到实际部署
验证集指标只是第一步,部署环境才是真正的考场。我当时把YOLOv8n导出ONNX,再用TensorRT做FP16加速,在病房常见的低功耗主机上测试,推理单帧约35毫秒,1080p视频勉强能做到20FPS以上,够用但不算宽裕。
部署阶段有几个细节值得记下来。第一,训练时的输入尺寸和部署时的推理尺寸要保持一致,换个resize方式都会导致定位偏移;第二,实际摄像头视角和训练数据差别通常很大,必须准备一批现场采集图做二次校准;第三,医疗设备的推理结果要保守,置信度阈值宁高勿低,我用0.5,宁可漏掉一些边界case,也不让误报干扰医护判断。部署不是模型训练完就结束的事情,多跑现场才能看到训练时看不到的问题。
6. 复盘:2200张数据集的价值不在数量,而在形成闭环
6.1 小数据集的意义不在于"全",而在于"可用"
如果非要给这次项目定个性,我会说:2200张YOLO医疗健康数据集本身不该被当成"大而全"的资源,它更像一个高质量验证底座,用来证明在有限样本下,客观疼痛检测这条路可以跑通。小数据集加迁移学习加严格清洗,往往比大数据集加粗糙标注更能快速验证产品假设。
我个人的感受是,医疗AI的数据迭代一定要小步快跑。每次补充几百张真实临床样本做增量训练,比一次性追求几万张图靠谱得多。数据质量的控制是一个持续过程,模型指标其实是对数据质量的回应。
6.2 后续扩展方向与一个小技巧
模型结构上不必追新,YOLOv8n这种成熟轻量模型在现有数据规模下已经够用。真正值得投入精力的方向,是把公开数据和真实场景数据做更精细的domain mix,以及在评估体系里加上更多面向临床的指标口径。如果你也要做类似的小规模医疗检测数据集,我建议在训练时引入"少量合成样本加较强增强"的组合,把公开数据里的人物姿态迁移到不同背景上,这可以在不大幅增加标注成本的前提下提高泛化性。
最后分享一个我自己养成的小习惯:每个版本的模型训练完,我都会把验证集里预测错的样本导出成一张拼图,按错误类型分组,定期回看。这比任何loss曲线都更能提醒自己——数据里还有哪些长得像的坑没填平,下一批补充数据应该优先采什么场景。整个过程踩过不少坑,但把数据、标注、训练、评估这条链路完整走通之后,迁移到其他医疗检测任务基本就是复制经验的事了。