多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

YOLO医疗健康数据集构建与训练实践:疼痛检测2200张图像全流程解析

YOLO医疗健康数据集构建与训练实践:疼痛检测2200张图像全流程解析 做AI医疗视觉快三年了这半年几乎把全部精力都压在了“疼痛检测数据集”这件事上。从最早三百张图试水到最终整理出2200张可用的YOLO格式医疗健康数据集中间踩过的坑比早年做通用目标检测时多得多。今天把这些经验一次性写出来为什么选用YOLO而不是分类模型数据怎么从零攒到2200张标注规范怎么定训练时更常遇到的BN崩溃和混淆矩阵问题怎么处理。适合两类人一是想做医疗健康场景目标检测但没摸过医学图像标注的朋友二是已经在用YOLO跑常规项目、想了解医疗数据特殊性的工程师。这里只讲实际能落地的细节不写套话。1. 整体设计与思路拆解1.1 为什么是YOLO不是纯分类或纯关键点很多人拿到“疼痛检测”第一反应是这不就是二分类吗人脸图像进网络输出有痛或无痛。我最初也这么做过拿ImageNet预训练的EfficientNet跑了一套验证集准确率看着还挺高。结果一上可视化直接就露馅了模型把画面边角的杂物、整体亮度、肤色差异当成了关键证据真正起决定性作用的眉间肌肉收缩、眼睑紧闭、鼻唇沟加深这些局部区域它根本没有学到定位信息。问题出在分类任务天然不关心目标在哪里它只关心最终概率怎么压对。疼痛反应在脸上不是一个均匀分布的现象而是集中在多个语义区域的组合动作。YOLO这类目标检测模型恰好把任务拆成了“在哪里”和“是什么”先画框再对框内内容分类。我用这套思路重新组织项目把标注对象定为六个语义区域眼周、眉区、鼻区、口周、颊部和整体面部。每一张图输出一到多个框模型既学局部纹理也利用框之间的位置关系迁移性明显优于纯分类方案。关键点检测也不是不行但2200张图像要标点的话成本高得多而且不同受试者的面部差异对关键点的语义一致性影响很大。疼痛引起的动作单元更接近“区域事件”而不是稳定存在的解剖点所以边界框是最务实的选择。1.2 2200张的数据规模到底够不够医疗数据不是COCO动辄几十万张LIIAR这类通用数据集在专业场景下没法直接用。疼痛检测相关的公开研究数据集大多只有几百到一千张规模真正能稳定收集、清洗并完成标注的能有两千张以上已经算奢侈。用YOLO官方预训练权重启动训练时每个类别有两三百个目标框就能收敛到可用的mAP。2200张图按平均每张2.5个目标计算能提供五千多个框覆盖六个类别是够的但前提是场景多样性必须做足。这里有个致命陷阱相似帧。医学图像里同一个受试者同一个表情连拍十帧太常见了如果不去重模型会疯狂过拟合到某个人的肤色、角度和画面比例上验证集指标虚高。我最终保留的2200张是经过重采样的连续视频按间隔抽帧同一受试者不同光照下最多保留30张训练集和验证集按受试者而非按图片切分。宁可总数少一点也要保证验证集里出现的是模型没见过的人这样指标涨跌才有参考价值。1.3 数据集结构与标签体系目录结构按YOLO标准组织不需要额外改造pain_data/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/数据划分如下集合张数用途train1540模型训练与在线增强val330训练过程选参、早停判断test330最终效果评估全程不参与训练这里特意把test单独隔出来是因为医疗场景的数据收集成本太高稍不注意就会拿验证集来回调参最后对泛化能力产生虚假信心。测试集在项目初期就锁死只允许在全部实验完成后跑一次。类别定义阶段我写进pain.yaml文件的配置是names: 0: eye_area 1: brow_area 2: nose_area 3: mouth_area 4: cheek_area 5: full_face六个类别中full_face是作为全局上下文加入的它能让模型更好地区分局部特征与整体姿态避免只学到某个局部区域而丢失结构上下文。2. 数据收集、清洗与标注规范2.1 图像来源与脱敏处理疼痛检测数据涉及人的面部信息来源和脱敏必须第一位。我主要以公开学术研究库和实验室自采两部分组成公开库只使用那些明确声明可以用于研究的匿名图像并严格遵守学术许可要求。自采部分则必须征得志愿者书面知情同意只保留正脸图像拍摄设备关闭定位记录所有图片在导出时删除Exif中的个人元数据信息。很多人容易忽视一个细节图像脱敏不等于打马赛克。疼痛检测本身就要分析眼周和口周区域马赛克会让特征完全失效。正确做法是只保留人脸核心区域把背景中可能存在的姓名牌、工牌、二维码等无关信息裁掉。训练前所有图像统一缩放到目标尺寸并先做人脸检测裁剪让模型尽可能专注于面部区域而不是靠背景里的桌子椅子做决策。2.2 标注目标和标框策略六个类别的标框规则必须定得非常细否则两个标注员之间会产生大量分歧。我的最终标准是这样的眼周区域眉毛下缘到下眼睑范围包括内外眼角附近细小纹理。眉区皱眉肌收缩导致的眉头下压区域连带眉心竖纹。鼻区鼻根和鼻翼上提产生的阴影结构。口周区域嘴角横向拉伸或下拉口轮匝肌张力变化。颊部鼻唇沟加深所涉及的面颊侧区域。整体面部完整人脸边界作为关联各局部框的桥梁。实际操作中标框宁准勿大。框太大就会混进大量背景噪声框太小又会把关键纹理截断。如果一个目标在多个类别间边界模糊比如眉区和眼周区域有重叠允许两个框重叠存在但两个同类别框之间的IoU不能超过0.8超过就删掉置信度更低的一个。真正的过渡帧不要硬标单独放在unlabeled目录里留作半监督素材。每一张图的标签文件必须与图片一一对应没有目标的行不要写在txt里但如果是真的一点特征都没有的图我建议直接剔除不要为了凑数保留空标签图。2.3 标注工具与YOLO格式转换标注阶段我还是推荐从VOC格式起步。LabelImg和X-AnyLabeling都很好上手先用PASCAL VOC格式保存后期再转成YOLO的txt格式。为什么不是直接标txt因为VOC格式保留更多信息中途需要修正类别和框位置时各种脚本都能兼容YOLO格式虽然简单但很难做二次版本管理。转换脚本我已经写成固定脚本核心逻辑不复杂import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_path, classes): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in classes: continue cls_id classes.index(cls_name) box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) x_center (x1 x2) / 2.0 / img_w y_center (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_path, w) as f: f.write(\n.join(lines))转换后一定要做一次反向校验把txt生成的框画回原图肉眼抽查一遍这一步能发现约10%的错标。2.4 数据增强不能一步到位数据增强是我反复试错最多的环节。YOLOv8默认开启mosaic和mixup这在COCO等通用数据上效果很好但医疗数据不能照搬。我的实际方案是训练前80个epoch开启mosaic最后10个epoch关闭。原因很简单疼痛特征本身偏细微mosaic在一张图上拼接四张不同来源的图像会生成大量伪边界前期模型还没收敛时能起到正则化作用后期继续开着就会导致模型总是去关注拼接缝。色彩抖动我保持了默认值但把hsv_h从0.015调到0.01因为人脸肤色的偏移幅度不能太大否则模型会把肤色本身当成特征。平移、缩放、翻转可以用随机裁剪时要防止人脸区域超出一半以上。医疗场景里垂直翻转要谨慎正常拍摄的人脸很少会有倒立的情况。3. YOLO训练实操记录3.1 环境搭建与预训练权重选择软件环境我用的是一套非常常规的组合Python 3.10、PyTorch 2.1、CUDA 11.8、Ultralytics 8.1版本。这里不建议追最新的代码从Ultralytics官方仓库安装一个稳定release版本讲清楚后锁定版本后续所有复现都能保持一致。官方预训练权重可以直接下载yolov8n.pt和yolov8s.pt不需要额外魔改。为什么不从零训练医疗数据量小从零训练时卷积核大概率学到的是背景噪声而不是局部肌肉纹理。加载预训练权重后backbone已经具备完整的边缘、纹理、颜色等基础视觉能力我们要做的只是让它在疼痛语义上做迁移。训练时我冻结了前15层backbone参数batch size设成16后模型仍然有足够的学习空间。启动命令大致如下yolo \ taskdetect \ modetrain \ datapain.yaml \ modelyolov8n.pt \ epochs120 \ batch16 \ imgsz640 \ lr00.0005 \ optimizerAdamW \ patience20首次训练不要总盼着一次拿到最好结果先跑30个epoch看看收敛方向确认数据加载和标签没问题后再跑全量。训练过程要实时盯GPU利用率如果某一个卡利用率长期低于80%说明数据加载或预处理变成了瓶颈。3.2 超参数怎么调才靠谱超参数表可以直接参考这套参数使用值说明imgsz640保持适中过小会丢掉疼痛区域细节batch16显存不够就降到8并开梯度累积lr00.0005迁移学习场景不能套默认0.01optimizerAdamW收敛更平稳适合小数据量mosaic前80轮开启后期关闭避免伪边界干扰patience20连续20个epoch无提升就早停fl_gamma1.5缓解类别不平衡最值得说的是lr0。images医疗数据和ImageNet的视觉分布差异很大如果继续用默认学习率0.01前几个epoch就会把预训练权重冲毁loss直接起飞。小学习率配合warmup才是正确姿势。warmup_epochs设5个周期前5个epoch学习率从低到高线性增加让BN统计量先适应新数据分布后面再接正常衰减会平稳得多。3.3 Loss曲线与退化判断YOLOv8的loss由三部分组成box_loss、cls_loss和dfl_loss。每次训练完我都把三个曲线单独画出来看不能只盯着总loss。box_loss下降而cls_loss不降说明框的位置学得还行但类别判断仍混乱回头检查标签比继续调参更有用。train_loss持续下降但val_loss开始反弹过拟合已经发生把mosaic关掉、适当增加验证集多样性、或者调高weight decay比盲目砍网络深度有效。mAP50看着不错但mAP50-95偏低是边界框精度不足的典型表现。疼痛检测里很多目标区域就十几像素大小模型能大致框中但边界贴合度不够。遇到这种情况我一般把输入分辨率提到768batch降到8训练时间多花一半但指标提升明显。连续两轮训练如果val_loss变化都在1%以内就可以收手了再跑下去只是浪费电。4. 训练和验证中的Bug排查4.1 BN层崩溃问题YOLO训练中BN崩溃是个经典问题表现为训练到某个epoch后loss突然变成NaN或者cls_loss疯狂冲到天际。本质是BatchNorm在反向传播中遇到不稳定的统计数据。我在这个项目里遇到过三次第一次差点把训练日志直接删掉重来。排查顺序很重要按下面这张表走可能原因处理方法batch size太小小于8开梯度累积把等效batch提到16以上学习率太大把lr0降到0.0005以下同时增加warmup数据里面混有全黑或全白图像清洗环节剔除并把像素均值打印出来检查old version mosaic产生全黑填充升级ultralytics到较新版本checkpoint中断恢复直接接着训重新初始化optimizer不保留旧二阶矩除非必要遇到BN崩溃第一时间不是去改网络结构而是先把数据管线排查一遍。有一次崩溃完全是因为数据集中有一张损坏的JPEG图读进来是一张全黑的零矩阵图BN统计量瞬间被拉偏。从那以后我在数据预处理脚本里加了像素值范围检查图像方差低于阈值的直接排除。4.2 混淆矩阵总和为什么不是1“yolo混淆矩阵总合不唯一”这个问题在社区里问的人很多。首先要搞清楚混淆矩阵的行代表真实类别列代表预测类别。模型输出时有一个置信度阈值低于阈值的框不会被纳入统计所以每一行加总不一定等于该类的所有真实标签数整个矩阵所有元素加总不等于1是非常正常的它不是归一化百分比的单一矩阵。正确用法是看行方向。如果某个真实类别的所有样本数明明有300个但混淆矩阵对应行的总和只有260说明有40个样本在置信度阈值下没有被检出来模型对这类目标的置信度整体偏低。这时应该把推理的conf阈值从0.25下调到0.1或者去检查那些漏检的样本是不是标注框本身就有问题。我建议用sklearn的confusion_matrix按normalizetrue重新计算一遍再和YOLO自带的混淆矩阵图对比。如果两者差异大就是推理后处理NMS参数造成的偏差而不是类别学习有问题。后处理导致的偏差在医疗场景里一定要揪住它会让最终指标的解读完全走偏。4.3 小目标漏检与重叠框疼痛区域一个显著特点是目标小眉间竖纹可能只有40×30像素在640分辨率下确实容易被下采样丢细节。YOLOv8本身设计了多个尺度的检测头但深层特征图主要照顾大目标小目标还是要靠浅层特征。一次训练中我发现鼻区类别的召回率只有51%定位原因就是目标尺寸偏小。我采取的解决方式是切图检测。把输入图切成2×2块每块独立检测最后再用NMS把框合并回原图坐标。这种方式召回率能提升十几个点但推理时间也上去了。作为替代方案SaHi这种切片推理库可以不用自己实现合并逻辑省不少事。anchor也要重新聚类用autoanchor在自定义数据上算一遍不要沿用COCO的anchor配置。多目标重叠问题则出现在眼周和眉区同时剧烈反应的情况下。两个同类别的框高度重叠后处理里NMS的iou阈值设0.5会把其中一个判定为重复框直接吞掉。处理办法是NMS阈值调到0.6并在损失函数里允许低IoU的重叠边界框共同保留。4.4 类别不平衡与数据集正义疼痛重度样本难采集是客观事实不是模型能解决的。六分类中full_face这类全局框数量会很多鼻区局部框可能只有几十个。模型天然倾向于把提升full_face的准确率放在优先位置其他类别的损失却被淹没了。缓解手段我试过三种类别重采样、损失函数调权、以及把少数类别做copy-paste增强。最有效的是在数据加载时对少数类进行过采样让每个epoch内少数类的出现频率不低于整体样本的20%。fl_gamma从默认1.5调到2.0也能提升少数类的召回但要防止多数类被过度抑制导致f1分数下降。如果某个类别的样本量真的只有十几个那最诚实的做法是合并到相近类别里而不是硬撑一个模型学不出来的类。4.5 跨设备跨光照泛化问题医院或养老机构里摄像头型号繁多训练集如果全是实验室统一光源下的图像部署出来就会明显掉点。我见过的最大差距是一套在实验环境mAP50达到0.86的模型放到走廊监控视角后掉到0.62。问题不在算法在训练分布收得太窄。解决办法是在训练集里主动注入光照差异。Data在采集阶段就应该覆盖上午、下午、晚间和灯源不同的场景如果条件不允许就在增强阶段把hsv_s和hsv_v的范围调大模拟不同白平衡和曝光。另外训练时把人脸检测裁剪的边距稍微放大10%到15%让模型同时容忍轻微的头部偏移。验证阶段除了看mAP我还把测试视频抽100帧人工逐帧看观察模型输出框是否在连续帧之间抖动。框的抖动有时候是因为阈值偏低提高conf阈值并加入跟踪的平滑处理后能明显改善。5. 在2200张数据集上的进阶探索5.1 YOLO与Transformer结构的结合YOLOv8默认的C2f模块在局部特征提取上很扎实但疼痛动作单元之间的长距离依赖关系比如眉区和口周同时处于紧张状态用纯卷积建模效率并不高。很多人问YOLO和Transformer怎么结合我推荐一个成本低的做法把Backbone最后两层的C2f替换为带attention的混合模块比如EMA或CBAM轻量注意力机制而不是直接上一个完整的Transformer Encoder那样训练成本会高得失控。替换后要注意把学习率再降低30%因为注意力模块在训练初期比卷积更容易过拟合小数据。我自己试过在颈部和backbone中间插入一层External Attention参数量增加不到10%但对颊部与鼻区联合反应的召回提升了3个百分点。如果显存充裕也可以参考RT-DETR的混合编码器思路把Transformer编码器放进颈部让模型学框与全局语义的关系但训练轮次至少要增加到160轮才能看到效果。5.2 YOLO加CLIP做语义约束YOLOCLIP是我近期比较看好的方向。疼痛是一个语义非常强的标签文字描述“the corner of the mouth is pulled down”和图像特征之间是可以对齐的。CLIP模型正好提供这种跨模态对齐能力。实操思路是把CLIP当辅助监督而不是替换检测头把检测框内的特征投影到CLIP embedding空间和一个预定义好的文本prompt embedding计算相似度把它作为辅助loss加入训练。这个辅助损失不会直接决定检测框但会让backbone提取到的特征在语义上更接近“疼痛相关肌肉紧张”的表述而不是纯视觉纹路的拟合。我在实验里发现它对颊部类别的虚假阳性有压制效果代价是每个epoch因为CLIP特征提取多花20%时间。如果硬件条件紧张可以先固定CLIP的权重只让它输出embedding不接受梯度更新。注意CLIP模型的输入要做归一化否则颜色偏移会严重干扰最终相似度。5.3 知识蒸馏与轻量化部署2200张数据的项目最后要落地到什么硬件决定你是否需要做蒸馏。如果是在服务器上跑YOLOv8s就够了如果是嵌入式设备就要考虑把模型瘦下来。我的做法是用YOLOv8m当教师YOLOv6n或YOLOv8n当学生做logits蒸馏。教师模型先在大训练集上训到收敛然后离线把所有训练图像的输出软标签保存下来。软标签比硬标签更平滑边界框的模糊信息被保留了下来学生模型能学到教师的“犹豫”反而比直接用硬标签收敛得更好。蒸馏训练的学习率要更小lr00.0003batch适当加大到24因为软标签包含更多监督信息模型能承受更大的批量。最终部署时再把模型的尾层做成INT8量化在CPU上推理延迟可以压到百毫秒级基本能满足视频流抽帧检测。5.4 医疗场景更该关心的评估维度医疗健康数据集的评估不能只看mAP。mAP是目标检测领域的通用指标但在疼痛检测这个场景下我更关心三件事第一是敏感度和特异性。如果用来做辅助提醒工具漏掉一个疼痛反应比多报一个假信号后果严重得多所以我会先把conf阈值调低把召回率拉到最高再用特异性去约束假阳性上限。第二是校准度。检测框的置信度不能只是排序数值它要能代表真实概率否则医生不敢参考这个输出。判断校准度推荐用Expected Calibration Error越低说明模型越有自知之明。第三是连续视频帧的一致性。单帧检测再准连续帧里闪烁不定也难以上线所以输出层要配合跟踪平滑或冷却时间机制。还有一点必须强调这套数据集和模型只能作为科研工具或康复辅助不能替代专业医学诊断。任何AI检测结果在临床使用前都要经过严格的合规评估这一点不是形式主义是数据项目能否走远的底线。我把最后这段当成给自己提个醒训练了一百二十个epoch之后最值钱的不是那个权重文件而是从400张原始图一路筛到2200张过程中沉淀下来的筛选规则。那些被放弃的图片里藏着真实的边界情况模糊帧、光线骤变、受试者转头一半的瞬间这些都是模型部署后必须面对的问题。做医疗健康类数据集和通用视觉感很不一样通用数据集拼数量医疗数据集拼精度和约束条件。如果你也想复现这套过程我的建议是别一上来就铺两千张先拿一百张把标框规范磨透把评审流程跑顺再逐步扩展。数据标注的准确度提上来以后很多后续训练问题都会自动消失这句话用在这个项目上一点都不夸张。
返回列表