做医疗视觉项目这些年,最深的感触是:团队常常把精力全花在模型结构上,最后发现真正的瓶颈往往在数据。疼痛检测这个方向尤其典型——翻遍公开数据集,能直接拿来训YOLO的少得可怜,要么是实验室受控环境下的样本,要么标注粒度跟实际需求对不上。所以当我们决定在真实病房场景做疼痛辅助评估时,干脆自己整理了一套数据,也就是标题里这套2200张的YOLO医疗健康数据集。这篇文章不打算只给个链接就完事,我会把数据集的构成逻辑、标注规范、YOLO训练配置、踩坑记录全部摊开讲,适合正在做医疗健康类检测、又苦于没有干净数据的同学参考。
1. 为什么疼痛检测需要一份专门的YOLO数据集
1.1 主观量表与客观检测之间的落差
临床上评估疼痛,最常用的是VAS视觉模拟评分和FLACC量表。VAS让患者在0到10的刻度上自己打分,看起来简单,但遇到术后麻醉恢复中的患者、重症监护室里的插管病人、认知障碍的老年人,这套方法基本失效——他们没法准确表达感受。
FLACC量表稍微好一些,通过脸(Face)、腿(Legs)、活动(Activity)、哭闹(Cry)、可安慰性(Consolability)五个维度打分。但问题也很明显:它依赖护士在某个时间点上的观察,而疼痛是动态变化的,护士不可能每分钟都在床边盯着。我在跟护理团队聊的时候,对方提到一个很现实的痛点:夜班巡房间隔较长,患者疼痛发作的高峰往往没被记录到,等发现时已经持续一段时间了。
这就给计算机视觉留出了空间。如果能有一个模型,对摄像头画面里的面部表情做实时分析,在疼痛表情出现时立刻提示,护理人员就能及时介入。但想做到这一点,第一步不是调模型,而是先解决数据的有无问题。
1.2 视觉模型在痛觉检测中到底学什么
很多人第一次听到"疼痛检测"会下意识觉得不靠谱——疼痛是主观感受,机器凭什么判断?这里要澄清一个概念:视觉模型判断的并不是"这个人有多疼",而是检测面部表情中与疼痛强相关的外显模式。
疼痛表情在面部动作上有相对稳定的规律。比如眉头紧锁、眼轮匝肌收紧、鼻根处出现皱纹、上唇被提升等。这些动作在面部动作编码系统(FACS)里有对应的动作单元编号,例如AU4(皱眉肌收缩)、AU6(眼轮匝肌收缩)、AU9(鼻肌收缩)、AU10(上唇提肌收缩)。临床上已经有研究证实,这些动作单元的组合与自报疼痛强度之间有显著相关性。
我们做的是把这套理论"翻译"成YOLO能读懂的监督信号。具体任务定义如下:每个样本标注一个面部核心区域矩形框(包含眉眼和口周),类别是四级疼痛强度——0级无痛、1级轻度、2级中度、3级重度。为什么要用目标检测而不是简单的图像分类?因为真实病房场景里画面中往往不止一个人,可能有护工、家属、患者同框。目标检测能把每一张人脸独立检测出来并分别分级,这是纯分类网络做不到的。
提示:疼痛检测模型的输出只能作为辅助参考,不能替代临床诊断。任何实际落地都必须经过伦理审查和临床验证,这一点后面讲数据合规时还会强调。
2. 数据从哪来:2200张图像的来源、筛选与脱敏
2.1 三类来源的构成
2200张不算多,但对医疗场景来说已经足够跑通一版可用模型。我们的数据来源分了三块,每一块都有自己的价值和缺陷。
| 来源 | 数量 | 优点 | 缺点 |
|---|---|---|---|
| 公开学术数据集(如UNBC-McMaster肩痛患者表情库) | 约800张 | 疼痛等级有临床评估佐证,标注相对可靠 | 灰度图像居多、分辨率低、拍摄环境单一 |
| 医疗机构合作脱敏视频帧 | 约900张 | 真实病房场景,光照、角度、遮挡都更贴近实际 | 伦理审批周期长,脱敏流程复杂 |
| 志愿者模拟表演 | 约500张 | 可控性强,可以覆盖各个疼痛等级和年龄组 | 表演痕迹重,真实感需要专业审核 |
很多团队做医疗数据会犯一个通病:只盯着公开数据集,觉得又权威又省事。但公开数据集用在真实场景里,效果通常不太行。我们的经验是必须混入目标场景的真实数据,哪怕数量少,对模型泛化能力的提升也比单纯堆公开数据大得多。
志愿者模拟那部分,不是找人随便做个痛苦表情就行。我们请了疼痛科医生和护理专家做现场指导,每条拍摄素材最终都要过一遍专业审核,确认表情符合该疼痛等级的表现特征,不合格的当场重拍。这也是为什么这一块的产出比预期低很多,拍了七百多条,最终只留了五百张。
2.2 纳入与排除标准
数据不是越多越好,标准不严会直接把模型带偏。我们制定了五条硬性筛选标准:
- 人脸占图像短边的比例不低于30%。低于这个比例,疼痛相关的细节(眼角皱纹、眉间纹路)在YOLO的输入分辨率下几乎不可辨。
- 人脸区域不能明显模糊。呼吸面罩、监护仪遮挡导致的边缘模糊也会影响标注和训练。
- 眼睛或口周不能被大面积遮挡。这两个区域是疼痛表情的核心信息区,遮住了基本没法标注。
- 表情多义的排除。大笑、惊讶、哭泣这些表情容易和疼痛混淆,如果两位标注员都拿不准,直接丢。
- 视频抽帧需要做相似度去重。同一段视频里连续帧太接近,模型会把同一张脸反复"背下来",造成虚高的验证集指标。我们采用每5帧抽取候选帧、再做感知哈希去重的方案,保证同一患者在同一次疼痛事件中最多贡献8到10帧。
这套标准执行下来,原本候选的接近4000张图被砍到2200张出头的规模。砍数据的过程是肉疼的,但最后模型的表现证明这是值得的。
2.3 脱敏与合规处理的底线
人脸数据属于生物识别信息,在医疗场景里更是敏感中的敏感。这个环节我只能说我们做的基本流程,具体合规要求请以当地法规和机构伦理委员会的审批意见为准。
首先是授权。无论是机构合作数据还是志愿者拍摄数据,都必须有明确的知情同意文件,写明数据用途、存储方式、可能的发布范围。志愿者数据我们允许用于开源发布,机构合作数据默认只允许内部训练,后续如果要用作其他用途需要补充协议。
其次是去标识化。所有图像文件用随机ID命名,不保留受试者姓名、病历号、住院科室等任何可直接关联到个人的信息。训练和推理全程在院内网或受控环境进行,模型权重导出前也要经过数据管理部门的审查。
最后是发布边界。即使完成了去标识化,公开一个真实患者的面部图像数据集依然有隐私风险。我们的做法是:对外发布版本主要包含志愿者数据和公开学术数据改造后的统一格式版本,机构合作数据绝不进公开包。这一点在立项阶段就要想清楚,否则等到数据收集完再处理合规问题,基本等于重新做一遍项目。
3. 标注规范:把"疼痛"翻译成YOLO能读懂的坐标框
3.1 FACS动作单元与标注逻辑
标注是这套数据集里最花时间的环节,前后用了将近两个月。标注的核心不是画框,而是理解"什么样的脸算疼"。
培训标注员时,我们不是让他们凭直觉判断表情。从FACS动作单元讲起,疼痛表情最常见的组合是AU4+AU6+AU7+AU9或AU10。落到视觉上就是:眉毛压低并向内皱、下眼睑收紧、鼻根有横向皱纹、上唇被微微提起。标注员对照一张疼痛表情参考图集反复训练,每次正式标注前先通过一套内部测试,达到准确率标准才能上岗。
标注框的选择也有讲究。我们没有框整张脸,而是框"面部核心表情区域"——从眉心到口周的矩形区域。理由是疼痛表情的有效信息都集中在这块,把额头、下巴这些非关键区域框进来只会给模型增加噪声。对于侧面角度较大的图像,我们框可见侧的眉眼与口周区域;如果侧面过大导致核心区域有超过一半不可见,这条数据直接舍弃,不强行标注。
3.2 标注工具到YOLO txt格式的生成
工具我们用的CVAT,因为团队协作标注时审核流程比较顺畅。每位标注员在CVAT里给图像创建矩形框并选择疼痛等级标签,完成后由审核员抽检。最终导出YOLO格式时,CVAT会自动生成对应的txt标注文件。
YOLO的标注格式不复杂,每个txt文件与图像同名,每一行对应一个目标框,格式是:
<类别ID> <中心点x_归一化> <中心点y_归一化> <宽度_归一化> <高度_归一化>比如一张1920x1080的图像,标注框中心在(960, 540),宽480,高360,类别是2(中度疼痛),那txt里对应的一行是:
2 0.5 0.5 0.25 0.333333所有坐标都必须归一化到0到1之间。这一步看起来简单,但也是最容易出错的地方——有不少人习惯性填了像素坐标,YOLO训练时直接报错或者莫名其妙不收敛。
3.3 双人标注与一致性校验
单人标注最大的风险是主观性太强。同一个表情,A标注员觉得是中度疼痛,B可能觉得是轻度。我们对全部数据做了双人独立标注,然后用标注一致性指标来量化分歧。
实际操作中,一致性指标用的Cohen's Kappa,算下来四级疼痛强度的kappa值在0.72左右,属于"substantial agreement"的水平。分歧主要集中在1级和2级之间,毕竟轻度和中度的边界本身就有一定主观性。所有分歧样本由疼痛科医生做最终仲裁。
除了人工仲裁,我们还加了一道模型辅助质检:用初步训练的模型对全部标注数据重新推理一遍,找出预测等级与人工标注差异大的样本,逐条人工复核。这套流程帮我们揪出了一批三周前标注的低质量框——标注员在连续工作几小时后,确实会不自觉地放松标准。
提示:标注质量管理不能只靠"培训+信任"。设置明确的抽检比例、定期重新验证标注员的一致性、隔一段时间做一次全员复核,这些流程缺一不可。医疗场景的错误标注不仅是数据集质量问题,还可能影响下游应用的判断。
4. 训练配置:小数据量下的YOLO迁移学习策略
4.1 预训练模型选择与迁移学习
2200张图训一个检测模型,从头训练肯定是不现实的。我们的方案是站在YOLOv8预训练模型的肩膀上做迁移学习。
选择预训练模型时,权衡的是参数量和数据量的匹配关系。数据量只有2000多张,直接上yolov8x这种大模型,即使有预训练权重,也大概率过拟合。我们最终以yolov8s为主力,yolov8n作为消融对比。s模型在COCO上训练过,已经具备通用的特征提取能力,尤其是边缘、纹理、局部形状这些底层特征,可以直接复用到面部表情识别上。
迁移学习策略分两阶段。第一阶段冻结backbone的前十层,只训练head部分和后面的neck层,用较小的学习率跑30个epoch,让模型先适应医疗图像的分布。第二阶段解冻全部层,用更低的学习率做全局微调。为什么要这样?因为一开始就用大学习率微调全部层,预训练权重里宝贵的底层特征很快就被冲掉了,在数据量不足的情况下这是致命的。
YOLOv8的官方预训练权重在ultralytics发布页可以直接下载,对应版本是yolov8n.pt、yolov8s.pt这些。下载后还需要确认PyTorch、CUDA版本跟当前环境匹配,这一步倒是比较常规,就不展开说了。
4.2 医疗场景的数据增强组合
数据增强是小数据集训练的救命稻草,但医疗场景不能照搬通用目标检测的增强策略,原因在于有些增强方式会破坏疼痛表情的判读特征。
Mosaic增强是YOLO系列训练里增强效果最明显的手段之一,把四张图拼成一张,能大幅提升模型对重叠目标和上下文变化的适应能力。但我们在小数据集上试的时候发现,Mosaic概率太高会让模型在早期训练阶段很难收敛——拼接的图来自不同患者、不同光照条件,边界处的纹理统计差异太大。最终方案是:前30个epoch关闭Mosaic,等模型对单图分布基本适应后再以0.5的概率开启。
HSV色彩增强也被调低了强度。疼痛表情里有很多信息在细微的肤色变化上,比如面部潮红。过度的色调、饱和度扰动会把这些信号抹掉。我们把色相扰动从默认的0.015降到0.005,饱和度和明度的扰动幅度也压缩到默认值的六成左右。
翻转增强我们没有直接开。一方面面部表情左右大体对称,水平翻转理论上可行;但另一方面,某些疼痛相关的肌肉细微活动存在不对称表现,翻转会引入不自然的分布。稳妥起见,只在偏侧性弱的轻度等级样本上启用水平翻转,中度和重度保持原样。
4.3 损失函数与超参数的实测组合
YOLOv8的损失函数由三部分构成:分类用BCE损失,边框回归用CIoU损失,外加一个DFL(Distribution Focal Loss)用于更精确的边界框分布建模。不需要像YOLOv5那样手动调整anchor参数,v8本身是anchor-free的设计,这一点对新手友好不少。
四类的类别分布并不是均匀的。无痛样本大约占35%,轻度占30%,中度占20%,重度只有15%左右。这种长尾分布如果不处理,模型会天然偏向多数类。训练配置里给1、2、3级疼痛类别分别增加了loss权重,具体是给中度设1.2、重度设1.5的权重乘子,让模型在少数类上多花一些梯度。
几个关键超参数的实际配置:
| 参数 | 取值 | 说明 |
|---|---|---|
| 输入分辨率 | 864x864 | 疼痛相关细节偏小,640下容易丢失 |
| Batch size | 16 | 显存允许范围内尽量大,后面会说BN崩溃问题 |
| 初始学习率 | 0.001 | 迁移学习场景下不宜使用默认0.01 |
| Weight decay | 0.0005 | 对中小数据集防过拟合 |
| Epochs | 200 | 配合早停,在验证集指标连续40轮不提升时自动截断 |
训练命令长这样:
yolo detect train data=pain_data.yaml model=yolov8s.pt imgsz=864 batch=16 epochs=200 lr0=0.001 mosaic=0.5 hsv_h=0.005 hsv_s=0.4 hsv_v=0.4这里data.yaml要写成YOLO格式的路径索引,标明train和val目录以及类别名。我们在实际跑的时候还加了AMP混合精度训练,显存占用降了约三成,训练速度提升明显,精度几乎没有损失。
5. 踩坑记录:从BN崩溃到混淆矩阵失衡
5.1 训练中段loss突然起飞:BN统计量崩溃的现场
整个训练过程里最让人血压飙升的一件事,是模型跑到约80个epoch时,验证集loss突然从原本平稳的0.8左右直接跳到2以上,伴随mAP大幅下跌。第一反应是学习率出问题了,但检查余弦退火的调度曲线发现正常。
后来排查锁定到BN(Batch Normalization)统计量崩溃。这件事在中小批量训练里挺常见的:batch size只有16,每个batch里同一批次图像的差异又比较大,BN层在迭代中不断调整的均值、方差统计量发生剧烈震荡,积累到一定阶段后直接失稳。典型的症状就是loss曲线不是缓慢上升,而是突然跳变,然后一路失控。
处理方案分了四步:
- 把初始学习率从0.002降到0.001,降低BN统计量更新的扰动幅度。
- 将Mosaic概率从0.5临时降为0.3,让批次内的图像分布更接近真实场景。
- 开启梯度裁剪,限制单个梯度的L2范数不超过10。
- 在训练脚本里增加了loss spike的自动监测,一旦val loss在10个epoch内上升超过15%就立即停止并回滚到最后一次稳定的checkpoint。
回滚后重新训练,没有再出现崩溃。
5.2 类别不平衡导致的"高mAP、低实用度"假象
第一版模型在验证集上的mAP@0.5高达0.86,看起来很不错。但打开每个类别的召回率一看,问题立刻暴露:0级无痛的召回率接近0.95,1级轻度也不错,2级中度还行,3级重度只有0.62。
这是一个典型的长尾陷阱——模型把大部分样本都猜成了多数类,整体指标被多数类拉高,看起来不差,但在最需要关心的重度疼痛检测上并不给力。医疗场景里漏掉重度疼痛和错判无痛的代价完全不在一个量级,评估必须逐类看,不能只看mAP。
解决方式是组合拳。训练侧做了少数类过采样,把2级和3级的样本在每轮epoch里多重复一遍,并配合前面说的类别loss权重。推理侧则对3级类别使用更低的置信度阈值(0.2),宁可多产生误报,也要减少漏检。经过这轮调整,3级召回率从0.62提到0.79,mAP整体略有下降但可用性大大提升。
5.3 混淆矩阵总和为什么对不上样本总数
用Ultralytics训练完成后会输出一个confusion_matrix.png,很多人第一次看会被它的形式吓一跳:对角线上的数字加总起来为什么小于或者大于验证集标注框总数?这个问题在"YOLO混淆矩阵"这个话题下被反复问过。
原因在于YOLO的混淆矩阵多了一个background类,并且一个真实标注框和一个预测框的匹配规则比你想的更严格。具体来说:
- 每个真实框只匹配IoU最大且超过0.5的那个预测框,匹配上记为TP,计入对角线。
- 没有匹配到的真实框会被计入background行(相当于漏检)。
- 每个预测框如果没有对应的真实框IoU超过0.5,就会被计入background列(相当于误检)。
所以矩阵的总和=TP + FN + FP,而不是简单等于真实框的数量。理解了这一点,再去看该矩阵才有意义:重点不该纠结总和,而应观察漏检集中在哪个疼痛等级、误检的框大多被预测成了哪个等级。比如我们的矩阵里,重度疼痛的框有相当一部分落在了轻度列,说明模型把重度表情误读成了轻度——这比直接漏检信号更隐蔽,需要针对性地在数据里补充轻度和重度的边界样本。
6. 从评估到落地:蒸馏轻量化与实时部署
6.1 医疗场景的评估指标权重
常规目标检测项目习惯盯着mAP@0.5和mAP@0.5:0.95,但医疗场景的指标权重逻辑不太一样。临床上更关心的是敏感性和特异性。放在疼痛检测里,敏感性就是"真有疼痛且被模型识别出来的比例",特异性是"真没疼痛且没有被误判的比例"。
我在实际评估时排了一个优先级:
- 重度疼痛召回率,这是底线指标,漏掉一次重度疼痛的后果远大于一次误报。
- 无痛样本的特异性,频繁误报会引发"狼来了"效应,护理人员很快会对模型提示产生麻木。
- 中度疼痛的F1得分,中度是干预决策的临界点,需要相对均衡。
针对这些指标,我们还做了置信度阈值的校准。默认YOLO的置信度阈值是0.25,但用这个阈值跑一遍验证集,误报率偏高。我们把0级类别的判定阈值提到0.35,重度保持在0.2,形成一个分级阈值策略。这个策略用验证集调完后,又单独拿了一组没参与训练的真实病房视频帧做盲测,确认没有明显过拟合后,才作为固定配置定下来。
6.2 用蒸馏把大模型塞进边缘设备
模型服务化的过程中,我们还要考虑一个问题:真实部署环境往往不是一台A100,而是病房走廊尽头一台小主机甚至边缘盒子。
最初训练的yolov8s模型参数量在11M左右,FP32下权重文件大约22MB,直接部署其实也不是完全不行。但对于一台要同时跑多路摄像头的设备来说,能省一点是一点。而且医疗场景往往还需要同时运行其他辅助模型,算力是稀缺资源。
蒸馏方案是先用一个更大的teacher模型去教一个小student模型。Teacher我们尝试过yolov8x,也试过在C2f模块里引入轻量Transformer注意力模块的改造版本,后者在疼痛等级这个细粒度任务上表现更好。Student就是yolov8n,参数量只有3.2M左右。
蒸馏的过程不复杂:先把teacher模型在训练集上固定权重,然后用teacher的预测输出(包括分类logits和回归分布)作为soft label,连同真实标注一起监督student的训练。这种"软标签"比单纯的真值框多了一层分布信息,能把teacher学到的类间关系知识传递下去。蒸馏后的student模型在重度疼痛召回率上只掉了约3个百分点,但权重文件压缩到了7MB左右,用TensorRT转成FP16后推理一张图在边缘设备上只需要约12毫秒。
6.3 从图片检测走向实时视频监测的扩展
模型稳定后,下一步是把它接进视频流做实时监测。这一步和图片推理有一个关键差异:不能直接把每一帧都单独扔进模型然后输出结果,因为单帧误检的噪声会很大。疼痛表情本身是持续数秒甚至更久的稳定状态,不会像眨眼一样快速跳变。
我们的做法是加了一个轻量的时序平滑层。具体逻辑是:对同一个追踪ID的目标框(用ByteTrack做跨帧关联),维护一个最近10帧的预测等级直方图,只有当某个等级出现超过6帧时才对外输出状态变更。这样既消除了单帧误检的抖动,又保证真的疼痛发生时能在1秒左右被捕捉到。
再往后扩展,有几个方向值得关注。一是从检测到实例分割,把疼痛区域从矩形框精细化到像素级分割掩码,这在评估疼痛区域面积和变化趋势时更有价值。二是多模态融合,面部表情之外再接入心率、血压、动作幅度等信号,综合评估会可靠得多。三是与Transformer时序模型的结合,YOLO负责空间定位,时序Transformer负责建模疼痛在时间维度上的演变规律。这些后续扩展都建立在数据链路扎实的基础上,如果前面的数据、标注不稳定,后面模型再先进也很难落地。
最后说一点个人体会。这套数据集从立项、采数据、脱敏、标注到训练调优,前前后后花了将近三个月,标注时间的投入远超最初预估。真正跑通之后你会发现,医疗数据项目的关键瓶颈不是模型结构有多新,而是数据从采集、脱敏、标注到验证这条链路是否扎实。如果你正准备做类似方向,我最大的建议是:先把评估场景和标注规范定死,再动手采数据。否则每换一个场景就要重新标一批数据,返工的成本会高到让你怀疑人生。