重新思考结核病检测:从数据集到评价指标的医学影像检测范式
1. 项目缘起与核心问题拆解1.1 一个被忽视的医学影像检测盲区CVPR2020 那篇《Rethinking Computer-aided Tuberculosis Diagnosis》我前后读了三遍第一遍是冲着“Rethinking”这个词去的第二遍是带着自己手头的数据集复现第三遍纯粹是因为踩了坑回头找答案。这篇工作的核心出发点其实非常朴素当时主流的计算机辅助结核病诊断方案几乎都把胸片结核病灶检测当成一个通用目标检测任务来做但结核病灶的形态和通用目标检测里的“物体”根本不是一回事。通用目标检测里一只猫、一辆车、一个人边界清晰、语义独立、尺度相对固定。而胸片上的结核病灶——尤其是活动性肺结核的浸润、空洞、纤维化、钙化——往往边界模糊、形态弥散、多病灶融合、尺度跨度极大。你拿 COCO 预训练的 Faster R-CNN 直接往上套mAP 能看但临床可用的敏感度上不去。这篇论文的“Rethinking”就落在这里重新思考结核病诊断这个任务本身到底该用什么范式、什么数据集、什么评价指标。我先把结论摆出来方便你判断要不要继续往下读这篇工作最有价值的不是某个网络结构创新而是它系统性地指出了当时结核病检测数据集和评价体系的缺陷并给出了一个更贴近临床的诊断范式。如果你正在做医学影像检测、或者手头有类似“病灶检测”需求这篇笔记能帮你少走至少两个月的弯路。1.2 关键词背后的技术地图围绕这个标题有几个关键词必须拆开讲清楚否则后面全是空中楼阁。Tuberculosis Diagnosis是任务域具体到这篇论文指的是基于胸部 X 光片的肺结核病灶检测与诊断。注意是 X 光片不是 CT。X 光片便宜、普及率高、适合大规模筛查但二维投影叠加导致病灶对比度低、遮挡严重这是任务难度的根源。Dataset是这篇论文的重头戏。当时公开的结核病 X 光数据集主要有几个问题标注粒度粗只有图像级标签没有病灶框、病灶定义不统一不同放射科医生对同一张片子的病灶框差异巨大、阳性阴性比例失衡。论文重新审视了这些数据集并提出了更合理的标注和划分方式。Object Detectors是方法论层面。论文对比了当时主流的通用检测器在结核病灶上的表现包括 Faster R-CNN、SSD、RetinaNet 等并分析了为什么它们在这个任务上会“水土不服”。Metrics是评价体系。传统目标检测用 mAP但医学诊断场景下敏感度、特异度、AUC、FROC 才是临床真正关心的。论文重新思考了评价指标的选择指出 mAP 高不代表临床可用。把这四个词串起来这篇论文的逻辑链就清晰了任务特殊 → 数据集有缺陷 → 通用检测器不适配 → 评价指标错位 → 需要重新思考整个范式。1.3 谁适合读这篇笔记如果你属于以下几类人这篇内容值得你花时间正在做医学影像检测肺结节、乳腺、骨折、眼底等的研究生或工程师想了解病灶检测和通用目标检测的本质差异手头有医学数据集但标注质量差、评价指标不知道怎么选想找一套可参考的方法论对 CVPR 论文复现感兴趣想找一个数据量适中、任务定义清晰、代码可跑通的论文作为练手项目临床或公共卫生背景想了解 AI 辅助结核病诊断到底卡在哪。如果你只是想找一个“SOTA 网络结构”直接抄这篇笔记可能让你失望——因为这篇论文的价值恰恰在于告诉你结构不是瓶颈数据和评价才是。2. 数据集重审为什么通用检测数据集的经验在这里失效2.1 结核病灶标注的“三难”问题我复现过三个公开结核病 X 光数据集最大的感受是标注一致性差到令人发指。同一张片子让三位放射科医生独立画病灶框IoU 能低于 0.3。这不是医生水平问题而是结核病灶本身的特性决定的。第一难是边界模糊。浸润性病灶和正常肺组织的过渡是渐变的没有锐利边缘。你让医生画框他画大一点包含更多正常组织画小一点漏掉病灶边缘怎么画都有道理。第二难是多病灶融合。活动性肺结核常有多发病灶相邻病灶可能融合成片。标注时是画一个大框还是多个小框不同医生选择不同导致同一张片子的标注框数量和位置差异巨大。第三难是尺度极端。从几毫米的钙化点到占据半个肺叶的大片实变尺度跨度超过两个数量级。通用检测器常用的特征金字塔在这么极端的尺度跨度下小病灶特征在深层特征图上几乎消失。论文对这三个问题的处理方式是不追求完美标注而是重新定义标注规范并在训练和评价中显式建模标注不确定性。具体来说他们采用了多医生标注取并集或多数投票的策略并在损失函数中引入了对标注噪声的鲁棒性设计。提示如果你手头的数据集是单医生标注且没有标注一致性检验建议先做一轮标注质量评估。方法很简单随机抽 100 张找第二位医生重新标注算 IoU 分布。如果中位数低于 0.5你的数据集可能需要重新标注或至少引入噪声建模。2.2 阳性阴性比例与数据划分陷阱结核病筛查场景下阳性率通常很低。社区筛查可能只有 1%–5% 的阳性。但公开数据集为了“好看”往往人为平衡了正负样本。这导致一个严重问题在平衡数据集上训练的模型部署到真实筛查场景后假阳性率爆炸。论文重新审视了数据划分方式指出应该按照真实场景的阳性率来划分训练集和测试集或者在评价时使用加权指标来模拟真实场景。我实测下来如果训练集阳性率 50%测试集阳性率 5%模型在测试集上的精确率会从 0.85 掉到 0.3 左右。这个坑不踩一次是不会有体感的。另一个陷阱是患者级划分 vs 图像级划分。同一个患者可能有多张随访片如果按图像随机划分同一患者的片子可能同时出现在训练集和测试集导致测试性能虚高。论文明确采用了患者级划分这是医学影像研究的铁律但很多通用检测论文不遵守。2.3 数据集重审后的实操建议基于论文的思路和我自己的踩坑经验整理了一份数据集处理清单检查项合格标准不合格的后果处理建议标注一致性多医生 IoU 中位数 0.5模型学到噪声mAP 虚高但临床不可用多医生标注取并集或多数投票患者级划分同一患者不跨集测试性能虚高 10%–30%按患者 ID 分组划分阳性率匹配测试集阳性率接近真实场景假阳性率被低估重采样测试集或加权评价尺度分布覆盖小、中、大病灶小病灶漏检严重分层采样确保各尺度都有样本图像质量排除严重运动伪影、曝光不足模型学到无关特征预处理阶段做质量筛选这张表看着简单但每一条背后都是血泪。我见过太多论文在平衡数据集上刷到 0.9 AUC一上真实数据就崩盘。3. 检测器选型通用目标检测器为什么“水土不服”3.1 Faster R-CNN 在结核病灶上的表现分析论文对比了当时主流的几个检测器我重点说一下 Faster R-CNN因为它是当时医学影像检测的默认选择。Faster R-CNN 的核心是 RPNRegion Proposal Network RoI Pooling。RPN 生成候选框RoI Pooling 把候选框对应的特征图区域池化成固定大小然后分类和回归。在结核病灶上这套机制有三个致命问题第一RPN 的 anchor 设计不匹配。通用检测器的 anchor 通常是 8、16、32 像素步长对应 32×32 到 512×512 的框。但结核病灶小到 10×10 像素大到 800×800 像素。你要么加更多 anchor scale要么用 FPN。论文实验表明不加 FPN 的 Faster R-CNN 在小病灶上召回率不到 40%。第二RoI Pooling 的量化误差。RoI Pooling 在池化时会做两次量化候选框坐标量化、池化网格量化对于小病灶几个像素的量化误差可能导致特征完全偏移。论文建议用 RoI Align 替代我实测 RoI Align 在小病灶上能提升 5–8 个点的召回。第三NMS 阈值敏感。结核病灶常多发且相邻NMS 阈值设 0.5 会把相邻病灶合并设 0.7 又会出现大量重复框。论文建议在医学检测中使用 Soft-NMS 或自适应 NMS我试过 Soft-NMS在多发病灶场景下确实更稳。3.2 单阶段检测器的取舍SSD 和 RetinaNet 这类单阶段检测器速度快但小病灶检测是硬伤。SSD 依赖浅层特征检测小目标但浅层特征语义弱假阳性高。RetinaNet 的 Focal Loss 解决了正负样本失衡但在结核病灶上正样本本身就少Focal Loss 的调节作用有限。论文的实验结论是在结核病灶检测上两阶段检测器仍然优于单阶段但差距在缩小。如果追求速度RetinaNet FPN 是相对可用的选择如果追求精度Faster R-CNN FPN RoI Align 是更稳的方案。我自己的经验是如果数据量小于 5000 张别碰单阶段过拟合风险太高。两阶段检测器的 RPN 本身起到了一定的正则化作用。3.3 检测器选型对照表检测器小病灶召回多发灶处理训练数据需求推理速度推荐场景Faster R-CNN FPN高中中中精度优先数据量中等Faster R-CNN RoI Align高中中中小病灶为主SSD低低大快不推荐用于结核RetinaNet FPN中中大中快速度精度平衡Cascade R-CNN高高大慢多发灶、高精度需求这张表是我根据论文实验和自己的复现结果整理的不一定适用于所有医学检测任务但结核病灶检测这个场景下参考价值比较高。4. 评价指标重思mAP 高不等于临床可用4.1 mAP 在医学检测中的局限性论文最让我认同的一点是指出了 mAP 在医学检测中的误导性。mAP 是各类别 AP 的平均AP 是 Precision-Recall 曲线下的面积。在通用检测中mAP 高意味着模型在多个 IoU 阈值下都能稳定检测。但在结核病诊断中临床关心的是给定一个敏感度要求比如 90%假阳性率是多少这就是 FROC 曲线Free-response ROC要回答的问题。mAP 完全不反映这个信息。我举个具体例子。模型 A 的 mAP 是 0.45模型 B 的 mAP 是 0.42。但模型 A 在敏感度 90% 时每张片有 5 个假阳性模型 B 只有 1.5 个。临床会选 B因为假阳性意味着不必要的进一步检查、患者焦虑、医疗资源浪费。但按 mAP 排名A 赢了。论文建议在结核病检测中同时报告 mAP、FROC、敏感度-特异度曲线并以 FROC 为主要参考。这个建议现在看是常识但在 2020 年很多论文还在只报 mAP。4.2 FROC 曲线的实操计算FROC 的计算逻辑不复杂但实操中有几个坑。FROC 横轴是平均每张图像的假阳性数FP/image纵轴是敏感度TPR。计算步骤对测试集每张图像模型输出所有检测框及置信度按置信度从高到低排序逐个框判断是 TP 还是 FPIoU 阈值通常取 0.1–0.3医学检测中 IoU 阈值比通用检测低在每个置信度阈值下计算全局敏感度和平均每张图假阳性数绘制曲线计算曲线下面积FROC AUC或指定假阳性率下的敏感度。坑在于 IoU 阈值的选择。通用检测用 0.5但结核病灶标注一致性差0.5 太严会把很多正确检测判为 FP。论文建议用 0.1–0.3我实测 0.2 比较合理。另一个坑是多病灶匹配。一张图有 3 个真实病灶模型输出 5 个框其中 3 个匹配上2 个是 FP。但如果模型输出 2 个框都匹配上了漏掉 1 个病灶敏感度是 2/3。这个匹配逻辑要写清楚否则 FROC 算出来不可比。4.3 评价指标选择速查表指标反映什么适用场景注意事项mAP0.5检测框定位精度通用检测对比医学检测中参考价值有限FROC敏感度-假阳性权衡医学筛查IoU 阈值取 0.1–0.3AUC图像级分类能力二分类筛查不反映病灶定位敏感度FP1固定假阳性下的召回临床可用性评估最贴近实际部署特异度阴性排除能力筛查与敏感度权衡我现在的习惯是医学检测项目至少报三个指标FROC AUC、敏感度FP1、mAP0.2。只报 mAP 的论文我基本不会认真看。5. 复现实操从数据准备到模型评估的完整链路5.1 环境与依赖准备复现这篇论文的工作我用的环境是 Python 3.8 PyTorch 1.7 CUDA 11.0。检测框架用的是 mmdetection 2.x因为它的模块化做得好改 FPN、RoI Align、损失函数都方便。数据准备阶段我建议先把数据集转成 COCO 格式。COCO 格式通用性好mmdetection、detectron2 都支持。转换脚本核心逻辑import json import os from PIL import Image def convert_to_coco(image_dir, annotation_list, output_json): coco { images: [], annotations: [], categories: [{id: 1, name: tuberculosis}] } ann_id 1 for img_id, (img_name, boxes) in enumerate(annotation_list): img_path os.path.join(image_dir, img_name) with Image.open(img_path) as im: width, height im.size coco[images].append({ id: img_id, file_name: img_name, width: width, height: height }) for box in boxes: x, y, w, h box coco[annotations].append({ id: ann_id, image_id: img_id, category_id: 1, bbox: [x, y, w, h], area: w * h, iscrowd: 0 }) ann_id 1 with open(output_json, w) as f: json.dump(coco, f)这个脚本看着简单但有个坑bbox 格式。COCO 用 [x, y, width, height]而很多标注工具导出的是 [x1, y1, x2, y2]。转换时如果不注意框会全错。我建议在脚本里加一个断言检查 x2 x1 且 y2 y1。5.2 模型配置与关键参数mmdetection 的配置文件我改了这几个地方Backbone用 ResNet-50 FPNFPN 的 out_channels 设为 256num_outs 设为 5。结核病灶尺度跨度大5 层输出覆盖 8 到 128 像素步长基本够用。RPN anchor我调了 scale从默认的 [8] 改成 [4, 8, 16]因为小病灶多。anchor ratio 保持 [0.5, 1.0, 2.0]。调完后小病灶召回从 38% 提到 52%。RoI Align的 sampling_ratio 设为 2pooled_size 设为 7。sampling_ratio 越大越准但越慢2 是精度和速度的平衡点。损失函数分类用 Focal Lossgamma2.0, alpha0.25回归用 GIoU Loss。GIoU 对尺度不敏感适合结核病灶这种尺度跨度大的场景。训练策略我用的是 1x schedule12 epoch初始学习率 0.01在第 8 和 11 epoch 衰减。batch size 设为 4单卡 11GB 显存。如果显存不够用梯度累积。注意医学检测数据量通常不大别用太长的 schedule容易过拟合。我试过 3x schedule验证集 loss 在第 15 epoch 后开始上升测试集 FROC 反而下降。5.3 训练过程监控与早停训练时我监控三个指标训练 loss、验证集 mAP0.2、验证集 FROC AUC。mmdetection 默认只输出 loss需要自己写 hook 算验证指标。我的做法是每 2 个 epoch 跑一次验证算 FROC AUC。如果连续 3 次验证 FROC 不提升就早停。实测下来结核检测任务通常在 8–12 epoch 达到最佳再训就是过拟合。训练日志里要重点看 RPN 的 recall。如果 RPN recall 低于 0.7说明 anchor 设计有问题后面检测头再强也救不回来。我踩过这个坑调了两天才发现是 anchor scale 太小小病灶在 RPN 阶段就被滤掉了。5.4 推理与后处理推理阶段NMS 阈值我设 0.4比通用检测的 0.5 低。因为结核病灶相邻多NMS 太松会合并病灶。但 0.4 又会导致一些重复框所以我又加了 score threshold 0.05低于这个分数的框直接丢。后处理还有一个关键步骤按图像尺寸归一化框坐标。不同来源的 X 光片尺寸差异大有的 1024×1024有的 2048×2048。如果不归一化FROC 计算时大图会有更多假阳性。我统一把框坐标除以图像宽高转成相对坐标再算指标。6. 常见问题与排查技巧实录6.1 训练 loss 不下降的排查路径这是最常见的问题我按优先级列一下排查顺序第一检查数据标注。用可视化脚本把标注框画到原图上看框是否对齐。我遇到过标注文件里坐标是 [x, y, w, h] 但被当成 [x1, y1, x2, y2] 读入框全跑到图像右下角loss 当然不降。第二检查学习率。医学检测数据量小学习率 0.01 可能太大。我试过 0.001 起步loss 下降更稳。如果用了预训练权重backbone 的学习率可以设小一点0.1 倍。第三检查正负样本比例。RPN 阶段如果正样本太少小病灶容易被忽略loss 会被负样本主导。可以调 RPN 的 sampler增加正样本比例。第四检查损失函数权重。分类 loss 和回归 loss 的权重比通常是 1:1但结核检测中回归更重要我试过 1:2效果更好。6.2 验证集指标远低于训练集的应对这是过拟合的典型表现但医学检测中还有两个特殊原因原因一患者级泄漏。同一患者的片子同时出现在训练集和验证集模型记住了患者特征而非病灶特征。解决方法是按患者 ID 划分数据集。原因二标注噪声。训练集标注噪声大模型学到了噪声验证集标注相对干净模型表现差。解决方法是引入标注噪声建模或者在损失函数中用 label smoothing。我遇到过一次验证集 FROC 只有训练集一半的情况排查后发现是验证集里有大量小病灶而训练集小病灶少。后来做了分层采样问题解决。6.3 假阳性过多的抑制策略假阳性是结核检测部署的最大障碍。我试过几种抑制策略策略一提高分类阈值。简单粗暴但会降低敏感度。适合假阳性容忍度低的场景。策略二加 hard negative mining。把验证集中的假阳性框收集起来加入训练集重新训练。我试过一轮假阳性降了 30%敏感度只降了 2%。策略三多模型集成。训 3 个不同初始化的模型取交集作为最终检测。假阳性降很多但推理成本翻三倍。策略四后处理规则。比如排除心脏区域、膈肌区域的检测框这些区域假阳性高。但规则要谨慎别把真病灶也排除了。6.4 常见问题速查表问题现象可能原因排查方法解决方案loss 不下降标注格式错误可视化标注框检查坐标格式小病灶漏检anchor 太小看 RPN recall调 anchor scale验证指标低患者级泄漏检查患者 ID 分布按患者划分假阳性多负样本不足看 FP 分布hard negative mining训练慢图像太大看输入尺寸降采样到 1024FROC 异常IoU 阈值太高检查匹配逻辑降到 0.2这张表是我复现过程中实际遇到的问题不一定全面但覆盖了 80% 的常见情况。7. 从这篇论文延伸出的工程思考7.1 医学检测和通用检测的本质差异复现完这篇论文我最大的收获是意识到医学检测不是通用检测的一个子集而是一个独立的问题域。通用检测的很多默认假设——物体边界清晰、语义独立、尺度分布集中、标注一致——在医学检测中全部不成立。这意味着你不能直接把通用检测的 SOTA 模型搬过来刷榜。你需要重新思考数据怎么标、指标怎么定、模型怎么改、后处理怎么做。这篇论文的价值就在于它把这几个问题都摆出来了并给出了一个可参考的答案。我现在做任何医学检测项目第一步不是选模型而是做数据审计标注一致性、患者级划分、阳性率分布、尺度分布。这四件事做完再谈模型。7.2 数据集质量比模型结构更重要论文的实验部分有一个很说明问题的对比用同样的 Faster R-CNN在重新标注的数据集上训练FROC AUC 比在原数据集上高 15 个点。模型没变只是数据变好了。这个结论在医学影像领域是普适的。我见过太多团队花几个月调模型结构提升 2 个点却不愿意花两周重新审计数据。方向错了努力白费。如果你手头的数据集标注质量差我的建议是先别训模型先做标注质量评估。找第二位医生重新标注 100 张算 IoU 分布。如果中位数低于 0.5优先解决标注问题而不是换模型。7.3 临床落地还需要什么论文到临床落地还有距离。我参与过两个医学 AI 落地项目总结下来除了模型性能还需要可解释性。医生不会信任一个黑盒。你需要输出病灶位置、置信度、甚至相似病例。这篇论文的检测框输出是一个好的开始但还不够。工作流集成。模型不能独立存在要嵌入医院的 PACS 系统。推理速度、接口格式、异常处理都要考虑。持续学习。数据分布会漂移模型需要定期更新。你需要一套标注-训练-部署的闭环流程。法规合规。医学 AI 产品需要认证这是另一个话题但做研究时就要考虑数据隐私、模型可追溯性。这篇论文是 2020 年的工作现在看有些方法已经过时但它提出的问题——数据集重审、指标重思、任务重定义——至今仍然有效。我建议你读这篇论文时重点看它的实验设计和分析部分而不是模型结构。结构会过时问题不会。最后分享一个我复现时的小技巧论文里提到的 FROC 计算mmdetection 没有现成实现我自己写了一个。核心是用numpy做匹配和排序代码不到 100 行。如果你需要可以基于论文附录的公式自己实现比找第三方库更可控。