改进YOLOv5实现膝关节骨关节炎KL分级:检测引导分类的医学影像实践

📅 发布时间:2026/10/17 17:58:13
改进YOLOv5实现膝关节骨关节炎KL分级:检测引导分类的医学影像实践
1. 先讲清楚一件事为什么骨关节炎分级会想到用 YOLOv5最初接触这个项目是某高校医学影像课题组找过来说想做一个膝关节骨关节炎KOAX光片的自动分级系统。他们给了一堆资料核心诉求很直白医生在临床上读膝关节正位片按照 Kellgren-LawrenceKL分级标准把关节退变程度分成 0 到 4 级0 级是健康4 级是重度。这项工作极其依赖经验同一个片子不同医生打出来的级别经常差出一档基层医院更缺有经验的影像科医生所以他们希望用深度学习给一个客观、稳定、可复现的分级结果辅助诊断。我一开始听到“用 YOLOv5 做 X 光片分级”第一反应是你是不是搞错了YOLO 是目标检测模型擅长的是框出物体位置你这个任务需要的是分级也就是分类顶多加个定位为什么不用纯分类网络后来把问题拆开才明白骨关节炎分级和日常的图像分类有个本质区别模型必须先找准膝关节的解剖结构位置尤其是关节间隙区域才能做出可靠的退变程度判断。为什么KL 分级的核心依据是关节间隙是否变窄、是否有骨赘bone spur形成、是否有硬化、是否有畸形。其中“关节间隙变窄”是最重要的判据它要求模型去比对股骨髁和胫骨平台之间的距离。如果网络没有先定位直接整图分类模型可能会去关注骨骼以外的纹理、X 光片上的伪影、甚至患者双腿摆放的角度差异这些干扰会让分类结果很不稳定。所以项目最终的技术路线确定为改进 YOLOv5 作为检测骨干先定位膝关节关键区域再结合分类分支完成 KL 0-4 级分级。也就是一个“检测引导分级”的两阶段方案。这篇博文就把整个系统的构造思路、踩坑过程、改进细节都拆开来讲涵盖数据标注、网络结构改动、训练技巧、部署体验适合做医学影像 AI、横向课题或者毕业设计的同学参考。需要先说明本文不是源码逐行注释而是从项目整体角度讲清楚“每个决定背后的原因”你可以按这个思路去复现和改进遇到工程细节问题也知道该往哪个方向排查。2. 数据是命根子KOA 分级的难点先从标注开始2.1 KL 分级标准到底在评估什么Kellgren-Lawrence 分级从 0 到 IV这里后续统一说 0-4 级判据落在四个维度上KL等级关节间隙骨赘骨质硬化/畸形0级正常无无1级可疑狭窄轻微骨赘无2级明确狭窄明确骨赘无3级中度狭窄多发骨赘轻度硬化/畸形4级重度狭窄近乎消失大量骨赘明显硬化/畸形这个分级有个致命特点相邻等级之间的视觉差异非常小。1 级和 2 级可能只是关节间隙窄了不到 1 毫米骨赘多了那么一点点3 级和 4 级的边界也很模糊。这对数据标注的要求是极高的。我们拿到的数据来自多个来源包括某医院门诊历史片子和一个公开骨关节数据集。原始标注是放射科医生做的但不同医生的标注风格不一致有的医生倾向于把“可疑”打成 1 级有的直接给 2 级。我在项目中做了一个很关键的动作筛片后请两位影像科医生对标注分歧比较大的样本重新仲裁再结合每位医生自己的置信度生成软标签soft label而不是硬性 0/1/2/3/4 的 one-hot。提示如果你做类似项目不要迷信公开数据集的原始标签尤其是 KL 分级这种主观性强的任务。至少请一位相关领域的人复看一部分数据统计标注一致性再决定是直接训练还是先清洗。2.2 数据清洗流程和我的筛选经验拿到 X 光片之后我做了四个处理步骤格式统一原始数据里有 DICOM 也有 JPEGDICOM 需要提取像素数组并做窗宽窗位归一化统一转成 8bit PNG避免 JPEG 压缩带来的边缘纹理噪声。方向校正X 光片有左右膝之分有的片子是正位有的是侧位侧位片的分级意义非常有限。我只保留正位AP片在检测阶段用一个左右翻转和方向分类的小分支做辅助判断防止模型对位置关系产生歧义。裁剪区域标准化原图尺寸从 1500×1200 到 4000×3000 都有如果直接缩放会丢失细节。我们用检测框的坐标把膝关节区域截出来再按长边等比缩放保证输入网络的区域内容基本一致。异常剔除带金属假体、严重造影剂残留、明显运动模糊的片子直接排除这类样本会让模型学到错误的纹理特征。2.3 标注格式怎么设计因为要走 YOLOv5 路线我用了两套标注第一套是检测标注标的是整个膝关节区域也就是股骨远端 胫骨近端这个矩形框。这是为了让模型把注意力集中在关节结构上而不是整张片子的其他器官或背景。第二套是分类标注同一张图附 KL 等级标签。这个“框 类”的结构让我后面在改进网络时非常灵活。比如我可以把检测分支设置为辅助任务分类分支设置为主任务也可以用检测框结果对分类分支做注意力加权。整套标注就用 YOLO 的 txt 格式存类别字段从 0 到 4 表示 KL 等级。第一次跑通的时候效果不太好检测框倒是很准但分类结果总在 1 级和 2 级之间跳。我后来发现问题不在分类器而在数据的样本分布——3 级和 4 级的片子本来就比 0 级和 1 级少得多而且很多设备拍的轻度片子都存在“看起来像一级又像二级”的模糊边界。所以在数据层面我后面专门做了重采样和困难样本挖掘这个后面详细展开。3. 改进 YOLOv5 的思路不是结构越复杂越好而是匹配任务3.1 为什么不用现成的 YOLOv5s 直接训练YOLOv5s 是一个通用目标检测模型它的设计目标是在 COCO 这类多类别数据集上取得精度和速度的平衡。直接拿来做 KOA 分级会遇到三个问题输出维度不匹配YOLOv5 的原生输出是框坐标 类别置信度但我的核心任务是分级。如果只把类别数改成 5 去训模型理解的是“整图里有什么等级的区域”而不是“某个膝关节区域处于什么等级”。感受野策略不适合KOA 分级需要同时关注局部骨赘细节和整体关节间隙形态YOLOv5 在 neck 部分的多尺度融合对这些细微结构不敏感。特征没有对齐解剖语义检测模型的输出层直接分类缺少“定位出关键语义点”的过程等效于让分类器自行找到关节间隙头部数据有限时学不稳定。所以我的方案是保留 YOLOv5 的检测预训练权重做特征提取但在多个层面加入医学任务特有的监督信息。3.2 模型骨架的四个具体改动第一处改进轻量化 Backbone 替换原版 YOLOv5 的 Backbone 是 CSPDarknet对 GPU 显存要求偏高。我换成了一个更轻量的结构只保留前 4 个 stage 的卷积输出丢弃高层语义模块然后把特征图的通道控制在 256 以内。这样做的原因是X 光片是灰度图信息密度远不如自然图像高不需要那么宽的通道去表达颜色和纹理差异。实测下来显存占用降了差不多 30%精度没有明显损失。第二处改进注意力机制插入位置选择很多改进消融实验都会在 backbone 后面加 SE 或者 CBAM但我在实际测试中发现统一加注意力模块不如分阶段加。最终选择是在检测头前两个尺度的特征图上加EMAEfficient Multi-Scale Attention模块。EMA 比 SENet 好在能在多尺度上做跨通道信息交互对骨赘这种小目标的细微特征更友好。把 EMA 加在浅层特征图而不是深层是因为浅层特征保留了更多的边缘和纹理细节而这些正是区分 2 级和 3 级骨赘的关键。第三处改进检测头改为双头结构原始 YOLOv5 只有一个输出头同时输出框坐标和类别概率。我改成两个并行分支一个分支还是回归检测框坐标无类别信息另一个分支在 RoIAlign 后的特征上做 KL 分级回归。具体实现是检测分支的候选框生成依然用 anchor-based 机制但类别置信度这一路改为输出分级结果也就是 5 个输出节点对应 0-4 级。训练时两个分支的 loss 加权合并权重配比是 0.3框: 0.7分类梯度回传的时候分类分支占主导但检测分支同时约束模型对位置的理解。第四处改进Anchor 尺度的重设YOLOv5 默认的 anchor 针对自然图像目标设计的尺寸跨度是 10 到 300 像素。膝关节区域在标准化后的 X 光片里一般占 70% 以上的面积远超默认锚框范围。我重新计算了训练集所有标注框的相对面积分布把 anchor 统一放大 2.5 倍比例从默认的 1:1、1:2、2:1 改成 0.7:1、1:1、1.3:1 附近因为膝关节是近似方形略微横长。设置完这四步之后验证集上的 mAP检测部分从 0.88 提升到了 0.94这是不改变实验设置、只做结构改动前后的对比。分级准确率变化更明显后面单独说。3.3 分类分支的 Loss 函数选择KL 分级是一个有序多分类问题2 级比 1 级更接近 3 级而 5 分类交叉熵把所有类别视为等距离这不符合医学语义。我在公共实验里测过三种 loss 的表现Loss在验证集上的 QWK加权 Kappa平均分类准确率CrossEntropyLoss0.7158.2%Ordinal Loss有序回归0.7960.5%改进版 soft-label Ordinal Loss0.8764.1%单纯用交叉熵的问题很明显模型会把 1 级样本硬分到 0 级或 2 级即使预测错它自己觉得“错误距离”是一样的。换成 ordinal loss 之后通过累积分布函数输出模型学会的是 P(等级 k) 的排序监督信息直接和医学语义对齐。但第三行的改进版是我认为最关键的一步因为标注本身存在主观性我采用带不确定性的软标签。具体做法是让每个样本的标签不是 hard one-hot而是按标注医生给出的置信率分配概率例如一位医生认为某片子 90% 是 2 级、10% 是 3 级那标签就是 [0, 0, 0.9, 0.1, 0]。这样模型不会在模糊边界样本上过拟合某一个绝对等级让决策边界更平滑。4. 训练过程与多个回合的踩坑记录4.1 数据划分分层抽样而不是随机划分医学影像数据有很强的患者相关性同一批患者的左右膝片子也不应该一个放训练集一个放测试集。我在划分数据集时按患者 ID 分组同一患者的片子全部进同一个集合避免数据泄漏。训练集、验证集、测试集的比例是 7:2:1其中训练集按 KL 等级做了分层抽样保证每个等级都有足够的样本。实际项目里数据量大概在 1.2 万张左右其中 0 级和 3 级的样本相对充足1 级和 4 级偏少。这个分布是符合临床实际的——早发现和极晚期的病例本来就少但我特意在 1 级和 2 级之间做了 oversampling因为这个分界恰恰是误诊率最高的区域。4.2 训练超参数的经验值经过多轮实验最终训练配置如下输入尺寸640×640在 512 和 768 之间折中512 虽然快但 2 级/3 级细节丢失明显768 显存吃紧且提升有限batch size32单卡 3080Ti 可跑24G 显存刚好优化器AdamW初始学习率 1e-4采用 cosine 衰减到 1e-6训练轮数120 epoch其中前 10 个 epoch 冻结检测分支参数只训分类头之后全部解冻联合微调数据增强随机旋转 ±15 度、水平翻转、随机亮度变化 ±20%、轻微随机缩放 0.8 到 1.2这里要特别提一下冻结训练的事。如果一开始就全网络联合训练检测分支会迅速收敛但分类分支的梯度会被检测分支的梯度淹没导致分级收敛极慢。先冻结检测让分类头在固定特征上快速找到一个合理解再解冻统一精调可以显著加速收敛效果也更稳定。4.3 第一次遇到滑铁卢全部片子的最低等级预测占比过高第一次完整训练完我看了测试集混淆矩阵发现 0 级样本的召回率 89%但 1 级样本的召回率只有 30%大量 1 级都被分到了 0 级。当时第一反应是 loss 权重没调好调高分类 loss 权重后改善依然有限。后来细查才知道问题出在标注很多“真·0级”的片子关节间隙看起来也挺窄的因为患者拍片时膝盖位置没摆正或者股骨髁轻微旋转导致投影变化。模型把这种投影变形学成了“正常”所以低等级样本的特征空间拉不开。解决办法有三个对训练集中的 0 级和 1 级样本做边缘框裁剪只保留关节正中央区域减少腿骨旋转的干扰。增加角度扰动增强同时引入局部仿射变换模拟投影角度偏差。调整样本权重把 1 级的损失放大 1.5 倍迫使模型对模糊边界投入更多学习能力。这一套组合下来1 级样本的召回率从 30% 提到了 61%虽然还是不算高但已经明显改善。4.4 检测框不稳定导致分级误判NMS 阈值要针对性调YOLOv5 默认的 NMS 阈值是 0.5目标检测场景没问题但我们的检测场景是单目标——一张图只有一个膝关节点。如果阈值设太高同一个目标会出现多个重叠框设太低又容易丢框。测试下来把 NMS 的 IoU 阈值调到0.7置信度阈值调到0.35可以在保证不漏检的同时基本消灭重复框。有一类特殊样本当关节变形严重4级检测框会缩得很小只框住胫骨平台而漏掉整个股骨髁。我后来在检测分支后加了一个“框扩展后处理”如果检测框宽度小于图宽 50%就按中心向外扩展 1.3 倍保证分类分支输入包含完整的关节区域。这一步看起来简单但实际把 4 级样本的分级准确率从 52% 拉到了 71%效果非常明显。X 光片分类一旦定位框不完整大量语义信息被截掉分类器再强也没用。4.5 类别不平衡的最终兜底方案光靠数据增强和调整权重类别不平衡问题解决得还是不到位。我额外用了两种方法困难样本挖掘每训练完 10 个 epoch用当前模型跑一遍训练集找出那些预测置信度在 0.4-0.7 之间的“犹豫样本”把它们单独抽出来按 1:2 的比例融合进下一轮训练批次。相当于让模型总在跟自己较劲。Focal Loss 变体对分类分支的 loss 使用 focal loss 的简化版gamma 设置为 1.2。如果一个样本的置信度已经很高比如 0.95focal loss 会把它的损失压得很低让模型把精力放在模糊样本上。这个方法比直接调权重更平滑不容易过采样逃逸出原来的封顶状态。说实话Focal Loss 参数在医学影像小数据集上非常敏感。gamma 设到 2.0 会导致严重的欠拟合我测试下来 1.2 到 1.5 是比较合适的区间。如果你复现建议先从 1.0 起步观察验证集 QWK 变化再微调。5. 模型评估只看准确率会骗了你5.1 为什么用 QWK 而不是准确率医疗影像分类任务尤其是分级任务准确率是一个不充分的指标。原因很简单样本分布不均衡。假设测试集中 0 级占 60%一个死模型把所有样本都预测成 0 级准确率就有 60%看起来“还不错”实际一点用没有。另一个问题KL 分级的临床代价是非对称的把 2 级误分为 3 级可能导致患者接受不必要的治疗把 4 级误分为 2 级可能让重度患者耽误治疗。所以评价指标必须包含“预测值与真实值之间的偏差距离”这时候 Quadratic Weighted KappaQWK就派上用场了。它计算的是预测等级与真实等级之间偏差的加权一致性预测等级离真实等级越远惩罚越大。我们整个项目的最终优化目标也一直是 QWK 而非简单准确率。5.2 最终测试结果在独立测试集从未参与训练和验证调整的 1200 张片子上的最终结果指标数值QWK0.87平均分类准确率64.1%0级召回率92%1级召回率61%2级召回率66%3级召回率74%4级召回率71%检测mAP0.94单张平均推理时间GPU42ms从表格可以看到1 级依然是最难分对的。这也是这个领域的普遍现象——轻度退变本来就没有绝对清晰的边界就算两个医生独立看也常常在 1/2 级之间打架。系统能达到 0.87 的 QWK 已经具备辅助诊断价值但绝不能宣称“替代医生”。5.3 错误样本的人工复盘我把测试集中所有预测值和真实值偏差大于等于 2 级的样本挑出来逐一人工看归纳出三类高频错误场景膝关节摆放轻微旋转引发的投影变形让关节间隙看起来比实际宽或者窄模型容易在 0/1 级之间翻车。X 光片曝光过度导致关节间隙边界模糊模型对细节的响应弱化。患者存在其他退行性病变比如骨质疏松或骨质增生背景干扰模型的局部特征判断。这三类场景的本质是医学图像中的诊断信息并不完全由解剖结构决定还高度依赖成像质量和拍摄标准。这也再次确认了我在第 2 部分强调的医学影像模型数据清洗和标准化比改网络结构更值得花精力。6. 从模型到系统实际部署和工程化遇到的问题6.1 系统的整体结构最终交付的是一个 Web 端辅助诊断系统。用户在浏览器上传 X 光片后端调用推理接口返回两个结果检测框位置的骨关节炎分级结果、和显示在每个等级上概率的直方图以及一段自动生成的诊断建议文本。系统架构不算复杂分三层前端一个基于 Vue 的简单页面支持拖拽上传、图片预览、结果渲染。后端服务基于 Python FastAPI负责接收图片、预处理、调用 PyTorch 模型、返回 JSON。推理模块把训练好的 PyTorch 模型导出成 ONNX用 ONNX Runtime 在 CPU 或者 GPU 上做推理。6.2 模型导出踩的坑Batch 维度和动态形状把 PyTorch 模型转 ONNX 时第一个遇到的问题就是 YOLOv5 输出头的动态形状。YOLO 的输出层存在一个非极大值抑制NMS过程但这个 NMS 是不适合直接前向推理的。我处理的方式是导出模型时只保留检测头和分类头的输出NMS 步骤放到 ONNX 之外的代码里实现用 NumPy 写一个轻量 NMS 函数。如果你直接导出带 NMS 的模型ONNX Runtime 会要求固定 batch 大小和 anchor 数量推理时很容易因为输入图片尺寸不同报错。我的建议是导出不含 NMS 的中间层输出NMS 放在推理代码里做。这样灵活度高排查问题也更容易。6.3 推理性能优化ONNX Runtime 默认使用的是 CPUExecutionProvider在我的测试机上单张图片推理需要约 180ms对于单用户网页版其实响应还能接受但如果我们希望同时支持多用户并发CPU 就扛不住了。我启用了 CUDAExecutionProvider 之后单张推理降低到 42ms提升了四倍多。另一个优化点是预处理放到 GPU 上做包括图像缩放、归一化。用torchvision.transforms的 GPU 版本省掉了每次 CPU-GPU 间的数据拷贝。不过要注意ONNX Runtime 的 GPU 版本对算子支持有限。一定要在导出前检查模型里有没有自定义算子。我的模型结构相对简单没有自定义算子所以顺利导出。如果你加入了一些特殊模块比如自定义激活函数可能就要走 TensorRT 或者重新实现前向逻辑了。6.4 和医生实际配合时的反馈系统交付后小范围试用了一个月收到的反馈很有价值医生最看重的不是分类结果本身而是解释性。概率直方图比单一等级更有用他们可以根据置信度判断是该采信还是存疑。检测框的稳定性让医生很惊喜。有了框之后医生能把结果和图像区域对应起来不是为了看“机器的判断”而是为了“确认机器判断的依据”。医生提出一个改进需求希望能自动对比同一患者在不同年份的片子直接显示关节间隙宽度的变化量。这是后续迭代方向但已经超出了当前的模型能力范围会增加时序建模不是简单改改结构能解决的。6.5 部署后修复的一个隐蔽 bug灰度图通道处理X 光片是单通道灰度图而 YOLOv5 预训练模型跑在 RGB 三通道输入上。我在预处理里做了单通道复制成三通道的步骤这个没问题但 ONNX 导出时输入节点名写错了导致推理时形状不匹配报错信息还特别难懂查了一个下午才定位到。这种问题很典型。模型训练时一切正常一到导出部署就出现各种形状、维度、数据类型的兼容问题。建议你在做类似医学影像项目时部署阶段不要追求一步到位先在本地写一个最小化推理 demo用同一张测试图跑通训练阶段输出和部署阶段输出是否一致再接着做界面和接口。7. 回到最初这个系统的核心价值和我的几点体会做完这个项目我从头梳理了一遍。技术选型上改进 YOLOv5 确实不是一个“热门”的选择现在大家谈到医学影像更多会提 Transformer、大模型预训练、多模态。但实际做工程模型的价值除了精度还要考虑可复现性、部署成本、计算资源。YOLOv5 的生态非常成熟预训练权重多社区工具链完善遇到问题一搜就有解决方案。这些因素在一个实际项目中远比“用了一个更新的模型”重要。从效果来看0.87 的 QWK 基本达到辅助诊断的可用线。但我也必须提一句这种等级的模型还不能独立出报告。它更适合做预筛和“第二意见”在小规模筛查场景里帮助医生提高效率把精力集中在可能有问题的那一部分片子上。如果让我再重来一次我会在项目前期花更多时间收集 1 级和 2 级的多中心数据而不是急着调网络结构。骨关节炎分级的瓶颈始终在数据和标注的边界问题上模型结构改进带来的提升有上限但数据分布的改善带来的提升没有上限。这个结论我做得越深越认同。最后给想复现的同学一个起点建议不用一上来就追求比我更多的改进点先把标准 YOLOv5 在 KOA 数据集上跑通把检测框稳定下来再加分类分支然后一步步调注意力模块。每一步改动都能对应到验证集指标的提升或下降这样做出来的模型你心里才有底出问题也知道是哪一步引入的。这个项目的完整思路到这里就告一段落如果你正在做相近的医学影像课题希望这篇能帮你少踩几个我踩过的坑。