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 的核心是 RPN(Region 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 还是 FP(IoU 阈值通常取 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 评价指标选择速查表
| 指标 | 反映什么 | 适用场景 | 注意事项 |
|---|---|---|---|
| mAP@0.5 | 检测框定位精度 | 通用检测对比 | 医学检测中参考价值有限 |
| FROC | 敏感度-假阳性权衡 | 医学筛查 | IoU 阈值取 0.1–0.3 |
| AUC | 图像级分类能力 | 二分类筛查 | 不反映病灶定位 |
| 敏感度@FP=1 | 固定假阳性下的召回 | 临床可用性评估 | 最贴近实际部署 |
| 特异度 | 阴性排除能力 | 筛查 | 与敏感度权衡 |
我现在的习惯是,医学检测项目至少报三个指标:FROC AUC、敏感度@FP=1、mAP@0.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 + FPN,FPN 的 out_channels 设为 256,num_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 设为 2,pooled_size 设为 7。sampling_ratio 越大越准但越慢,2 是精度和速度的平衡点。
损失函数分类用 Focal Loss(gamma=2.0, alpha=0.25),回归用 GIoU Loss。GIoU 对尺度不敏感,适合结核病灶这种尺度跨度大的场景。
训练策略我用的是 1x schedule(12 epoch),初始学习率 0.01,在第 8 和 11 epoch 衰减。batch size 设为 4(单卡 11GB 显存)。如果显存不够,用梯度累积。
注意:医学检测数据量通常不大,别用太长的 schedule,容易过拟合。我试过 3x schedule,验证集 loss 在第 15 epoch 后开始上升,测试集 FROC 反而下降。
5.3 训练过程监控与早停
训练时我监控三个指标:训练 loss、验证集 mAP@0.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 |
| 训练慢 | 图像太大 | 看输入尺寸 | 降采样到 1024 |
| FROC 异常 | 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 行。如果你需要,可以基于论文附录的公式自己实现,比找第三方库更可控。