医学图像异常检测这个方向,做过几年的人都有个共同的体感:工业界那套在 MVTec AD 上跑得飞起的方法,搬到 X 光、脑 MRI、眼底图上,精度经常一下掉十几二十个点。到底是方法不行,还是评估口径压根不统一?这个问题困扰了很多人。大家各用各的数据划分、各挑各的指标,论文里报出来的数字看着都挺漂亮,可你真去复现,发现根本不是那么回事。MedIAnomaly 这篇比较研究的意义就在这里:它把医学图像异常检测这件散装的事,尽可能塞进一个统一的框架里去量。它属于无监督异常检测在医学影像上的系统性基准工作,目标是回答"现有的主流方法迁移到医学图像到底行不行、哪一类更抗打、评估该怎么定"。适合刚入坑异常检测的算法同学、做医学影像产品落地的工程师,以及正在挑 baseline 准备写论文的研究生。下面我按自己的理解,把这篇工作的设计逻辑、实现细节和坑,掰开揉碎讲一遍。
1. 医学图像异常检测为什么需要一把统一的尺子
1.1 工业异常检测和医学影像,根本不是同一个任务
先得把背景说清楚,不然后面全是空谈。工业异常检测有个标杆数据集叫 MVTec AD,它定义了这样一个设定:给你一堆正常的工业产品图像,模型要学会判断测试图里有没有缺陷,并且把缺陷区域圈出来。这个设定之所以能成立,是因为工业场景有一堆隐含前提——相机位置固定、光照稳定、产品形态高度一致,缺陷主要是划痕、脏污、缺料、变形这几类,形态相对可枚举。
医学图像把这些前提全打破了。同一个器官在不同人身上,形状、大小、纹理都不一样,"正常"本身的方差就极大。一个肺部结节是异常,一块炎症渗出也是异常,一个陈旧瘢痕严格说也能算异常,但它们在同一张胸片上可能长得完全不像。更麻烦的是,医学异常的边界往往模糊,小病灶可能就几个像素,还容易和血管横断面、组织褶皱混淆。所以你拿工业上"除正常以外皆为异常"的思路硬套医学,会立刻遇到两个问题:正常样本的分布太宽,异常样本的形态太杂,模型很难学到一条清晰的分界线。这就是为什么医学图像异常检测不能简单照搬工业方案,也是 MedIAnomaly 这类基准要做医学专用评估的根本原因。
1.2 这篇工作想回答的三个核心问题
我读完这篇工作,觉得它其实就是在替整个社区问三句话。
第一句:现成的异常检测方法,尤其是那些在工业数据集上拿到 SOTA 的,直接迁移到医学图像上,性能会掉多少?这不是一句"会掉"就能打发的,得用统一协议量出具体差距。
第二句:把大家放在同一个训练/测试协议、同一套指标下,方法之间的真实排名还和原论文一致吗?很多论文报的漂亮数字,是依赖了自己精心调过的预处理和划分,一旦统一,可能就露馅了。
第三句:医学场景里,重构类、嵌入类、合成异常类这几大流派,到底谁更合适?尤其合成异常这条路,在工业上很吃香(因为可以人为造缺陷),可医学异常你能随便"贴"吗?贴出来像不像真的?这些都得验证。
这三个问题看似朴素,但恰恰是落地时最要命的。你选 baseline、选方案,其实就是在回答它们。
1.3 评估维度拆开看,才知道差在哪
MedIAnomaly 的评估不是只报一个 AUROC 就完事,它把维度拆得比较细。首先是图像级和像素级两个层次:图像级判断"这张图有没有病",像素级判断"病在哪、圈得准不准"。这两件事在实际产品里对应完全不同的功能,图像级相当于筛查报警,像素级相当于辅助定位,不能混为一谈。
其次是单类别和多类别两种设定。单类别就是"一个模型只学一种正常模式",比如专门做脑 MRI 的;多类别是"一个模型同时覆盖多个器官/多种模态"。多类别更接近真实临床(医院设备型号多、检查项目杂),但难度也陡增,因为不同模态的正常分布差异巨大。第三是预训练权重的影响:用不用 ImageNet 预训练,结果可能天差地别。这三个维度叠在一起,才构成一个相对完整的评估画像。只看单一指标的论文,其实信息量是很有限的。
这里给个实操提醒:如果你要复现任何异常检测工作,第一件事就是确认它的数据划分和指标口径,别急着跑代码。口径不对,跑出来的数字没有可比性。
2. MedIAnomaly 的整体设计思路与数据组织
2.1 数据集的选择逻辑:覆盖多模态、多器官、多任务
一个基准好不好用,一半看数据集选得全不全。医学图像异常检测不能只用胸片一种,因为不同成像模态的异常表现差异太大,你只用一种数据得出的结论,推广性很差。从我了解到的这份基准设计看,它大致覆盖了几大类:胸部 X 光(如 RSNA 相关数据、VinDr-CXR 这类)、脑部 MRI(BraTS 系列)、眼底/视网膜图像(OCT、糖网眼底数据)、乳腺钼靶等。选择标准基本围绕三点:有公开可获取的标注、异常种类有代表性、数据量足以支撑统计意义上的比较。
下面这个表是我按常见实践整理的,帮助理解不同模态对异常检测的挑战点在哪,具体数据集归属以论文原文为准:
| 成像模态 | 典型异常 | 对检测的主要挑战 | 像素标注难度 |
|---|---|---|---|
| 胸部 X 光 | 结节、渗出、气胸、积液 | 正常变异大、病灶与组织重叠 | 高,边界模糊 |
| 脑部 MRI | 肿瘤、水肿、出血 | 多序列、病灶形态不规则 | 中,肿瘤边界相对清 |
| 视网膜 OCT | 黄斑水肿、视网膜病变 | 层状结构、细线条异常 | 高,层间微小改变 |
| 眼底彩照 | 出血点、渗出、微血管瘤 | 小目标密集、背景复杂 | 高,点状病灶极小 |
| 乳腺钼靶 | 肿块、钙化 | 低对比、钙化点极微小 | 极高,微钙化难标 |
从这张表能看出来,医学异常检测的难点是分层级的:既有"病灶太小学不到"的问题,也有"边界模糊标不准"的问题。数据集选得越杂,越能暴露方法的短板,这也是为什么这份基准强调覆盖面而不是单一刷分。
2.2 统一训练与测试协议:公平比较的前提
比较研究最核心的功夫,其实不在算法,而在协议统一。MedIAnomaly 在这块做了几件事。第一,固定数据划分。它把每个数据集按统一标准切成训练/验证/测试,通常是正常样本进训练集,异常样本只在测试阶段出现(有的设定验证集也含异常用于早停)。划分一旦固定并公开,别人复现就不会因为"你切得巧"而偏差。
第二,固定输入尺寸与预处理。医学图像原始分辨率动辄 2000 像素以上,直接喂网络显存扛不住。统一缩放到一个固定尺寸(比如 256 或 512),同时明确归一化方式。这一步看着小,其实影响很大——缩得太狠,小病灶直接消失;缩得不够,显存爆炸。第三,固定随机种子和评估脚本。异常检测里随机性来源多(初始化、采样、合成异常的随机贴图),不固定种子,你每次跑的数字都能差出好几个点。
我踩过的一个坑:早期复现某方法时没固定合成异常的随机种子,同配置连跑三次,图像级 AUROC 波动快到 2 个点,一度以为代码写错了。后来把所有随机源都固定住,结果才稳定。
2.3 方法分类:重构、嵌入、合成异常三大流派
把方法归类,是为了看清每一类的适用边界。这份基准里的方法大致能分成几大流派。
重构类:用自编码器、VAE、GAN 或者 Transformer 去重建正常图像,重建误差大的地方认为是异常。代表有 AE、VAE、GANomaly、RD4AD、UniAD 这些。逻辑是"模型只见过正常,所以重建异常会失真"。
嵌入类:不重建像素,而是用预训练网络抽特征,再对特征分布做建模,看测试特征离正常特征分布有多远。典型是 SPADE、PaDiM、PatchCore。这类方法在工业上非常强,因为预训练特征本身就带强大语义。
合成异常类:人为在正常图上贴、混、生成假的异常,把它变成监督问题来训练。代表有 DRAEM、SimpleNet 这种。工业上贴个划痕、脏污很逼真,医学上你贴块"病灶",很可能一眼假。
除此之外还有归一化流类、知识蒸馏类。分类的意义在于,你能根据自己场景的数据特点去选流派,而不是盲目追 SOTA。比如你的异常形态很接近训练分布,嵌入类可能最稳;如果你有大量正常数据但异常极少,重构类可能更好上手。
3. 核心方法与实操要点
3.1 预处理决定成败:缩放、裁剪与归一化
我先说一个很多新手忽视的点:医学图像异常检测里,预处理对结果的影响,可能比换模型还大。原因很直接——小病灶对分辨率极其敏感。一张眼底图上的微血管瘤可能就十来个像素,你缩放到 256 后,它可能就剩两三个像素,甚至直接被双线性插值抹平。这时候模型性能再强也无能为力。
实操上,我一般会做几件事。第一,评估目标病灶的最小尺寸,反推可接受的最小分辨率。如果最小病灶在原图约 15 像素,缩放后至少要保留 5-6 像素,那缩放因子就不能超过 3 倍。第二,考虑分块(patch)而不是整图缩放,尤其对超高分辨率眼底图,切块能保分辨率,代价是要处理块间拼接。第三,归一化按模态区分,CT 要用窗宽窗位(比如肺窗、纵隔窗),不能全局一刀切。CT 的 HU 值范围差异极大,不做窗处理,模型学到的大概率是伪影而不是病灶。
# CT 窗宽窗位处理的常见做法 def apply_window(image, window_center, window_width): low = window_center - window_width / 2 high = window_center + window_width / 2 image = np.clip(image, low, high) image = (image - low) / (high - low) # 归一化到 [0,1] return image # 肺窗示例 lung = apply_window(ct_slice, window_center=-600, window_width=1500)3.2 三大流派的实现要点,别只抄超参
重构类的核心在"重建目标"和"损失设计"。你要是重建原始像素,模型很容易把异常也一起重建出来,导致漏检。常见改进是用特征级重建(在预训练特征空间算误差),或者加注意力约束让模型聚焦正常区域。RD4AD 这类"教师-学生"结构,本质上是让预训练教师网络教一个学生网络在正常图上对齐特征,测试时学生无法复现异常区域的特征,误差就出来了。实现上要注意教师网络必须冻结,学生网络容量别太大,不然它会连异常都学过去。
嵌入类的关键在"记忆库怎么建"。PatchCore 的思路是把正常训练样本的特征块存进一个记忆库,测试时找最近的正常特征块,距离大就判异常。这里有个大坑:记忆库太大,推理慢到无法接受;太小,覆盖不全导致误报。实践中要做 coreset 采样,把记忆库压到一个合理规模。SPADE、PaDiM 则是假设特征服从多维高斯分布,用均值协方差来建模,计算量小但假设较强。
合成异常类最需要警惕。工业上贴缺陷之所以有效,是因为缺陷形态单一、背景干净。医学上,你贴的假病灶要同时满足"形态像真病灶""位置合理""纹理与周围组织协调"三个条件,非常难。这份基准的一个有价值发现就是,合成异常方法在医学图像上普遍表现不如工业。所以如果你的场景是医学图像,我不建议一上来就把合成异常当主力,更稳的做法是把它当作数据增广的辅助手段。
3.3 评估指标怎么选:AUROC、AP、F1、PRO
指标这块必须讲透,否则你连自己模型好坏都判断不了。图像级常用的是 AUROC,它衡量的是"随机取一个正样本和一个负样本,模型给正样本更高分的概率"。AUROC 好用的地方在于不受阈值影响,坏处是在类别极不平衡时,它会显得过于乐观。举个例子,如果 100 张图里只有 1 张异常,一个把所有图都判正常的模型 AUROC 是 0.5,但换成 precision/recall 就极难看。
所以像这类基准通常会同时报 AP(平均精度)或 F1。AP 对正类更敏感,能反映"在异常样本极少的情况下模型到底抓到了多少"。像素级定位则常用 PRO(Per-Region Overlap),它对小病灶更友好,不会因为大片正常区域被正确排除就冲淡了针对病灶的评估。我的经验是:筛查场景重点看图像级 F1 和 recall(宁可误报也别漏),辅助定位场景重点看像素级 PRO。别只盯一个 AUROC 就下结论。
| 指标 | 衡量层次 | 适用场景 | 注意点 |
|---|---|---|---|
| AUROC | 图像级 | 综合排序能力评估 | 类别不平衡时偏乐观 |
| AP | 图像级 | 异常样本极少时 | 对正类更敏感 |
| F1 | 图像级 | 需要固定阈值时 | 依赖阈值选择 |
| 像素 AUROC | 像素级 | 分割定位质量 | 对面积不敏感 |
| PRO | 像素级 | 小病灶定位 | 更能反映小目标表现 |
3.4 多类别设定的陷阱:统一模型不等于更好
多类别设定是这份基准很值得关注的部分。直觉上,一个模型学多个器官的正常模式,数据量更大,应该更强。但实测往往相反。不同模态的像素分布差异巨大,模型容量被稀释,反而每个类别都做不好;更糟的是,某些类别的正常模式会被模型误当成别的类别的异常,导致跨类干扰。
我自己的处理经验是:如果类别间差异极大(比如脑 MRI 和眼底彩照放一起),优先用独立模型或者加模态/类别条件;如果类别间差异小(同一器官不同扫描协议),可以考虑共享特征提取、分开建模正常分布。多类别不是不能做,而是要想清楚共享什么、隔离什么,不能简单地把所有数据混到一个 batch 里就完事。
4. 跑通一次 MedIAnomaly 风格的评测流程
4.1 环境准备与依赖
要把这类基准跑起来,环境不用太花哨,关键是要能稳定复现。
# 典型的依赖栈 pip install torch torchvision # 主干 pip install scikit-learn # AUROC/AP 计算 pip install opencv-python # 图像预处理 pip install pandas numpy tqdm # 数据处理 pip install timm # 预训练骨干网络GPU 显存方面,如果你用 512 分辨率加 PatchCore 的记忆库,8G 显存有点紧,建议 12G 以上。跑合成异常方法时注意数据加载器别开太多 worker,医学图像单张读取慢,IO 容易成为瓶颈。
提示:一定要在开始前把随机种子、cuDNN 的确定性开关设置好,
torch.backends.cudnn.deterministic = True,虽然会慢一点,但结果可复现。
4.2 配置文件与训练脚本的组织
比较研究的工程重点是"能批量跑、能对齐口径"。我习惯把每个数据集、每个方法拆成配置,用统一入口跑。
# 简化版配置结构示意 config = { "dataset": "brain_mri", "method": "patchcore", "image_size": 256, "backbone": "resnet18", "pretrained": True, # 是否用 ImageNet 预训练 "coreset_ratio": 0.1, # 记忆库采样比例 "seed": 42, "metrics": ["auroc", "ap", "pro"], }关键在于训练脚本和评估脚本要解耦。训练只管产出正常性分数图,评估脚本读分数图统一算指标。这样你换数据、换方法,评估环节完全不用动,避免了"换数据集时手滑改了指标算法"的低级错误。
4.3 评估执行与结果记录
评估阶段最容易出的问题是:分数图的尺寸和标注掩码对不上。预处理做了缩放,掩码也得同步缩放,而且要用最近邻插值,不能用双线性,否则边界会糊掉。记录结果时,我强烈建议把每个类别、每个指标、每次运行的曲线都落盘,别只存一个最终数字。因为当你发现某个方法异常时,需要回过头看它是图像级掉了还是像素级掉了,曲线能帮你快速定位。
流程大致是:先加载测试集,逐张过模型得到分数图,然后对所有分数图和掩码做上采样对齐,最后批量算指标。整个过程要保证"同一批测试样本、同一顺序"喂给所有方法,否则排名都不可比。
4.4 从结果里读出的三个结论
从这类基准的普遍结论看,有几条是反复被验证的。第一,绝大多数工业 SOTA 迁移到医学图像后,图像级 AUROC 会明显下降,像素级定位下降更剧烈。这说明"病在哪"比"有没有病"难得多。第二,预训练特征极其关键,用不用 ImageNet 预训练,某些方法差距能到十几个点。第三,合成异常方法在医学场景整体偏弱,不如嵌入类稳定。这些结论不是拍脑袋,而是统一协议量出来的,参考价值比单篇论文的自我报告高得多。
5. 常见问题与排查技巧实录
5.1 指标虚高:八成是数据泄漏
异常检测里最隐蔽的坑是数据泄漏。典型表现是:你啥都没改,指标突然特别高,高到不真实,比如图像级 AUROC 0.99。这时候先别高兴,去查两件事。一是训练集里是不是混进了异常样本,比如按文件名划分时把同一次检查的切片分散到了训练和测试集,造成"同源泄漏"。二是预处理时是不是用了全局统计量(比如全数据集的均值方差),把测试集信息泄露到了训练。正确做法是按受试者或检查级别划分,预处理统计量只用训练集算。
5.2 合成异常学不动:形态和真实严重不匹配
前面提过,医学合成异常容易"一眼假"。具体怎么排查?你可以把合成的异常图拿出来肉眼看看:假病灶是不是边缘太规整、颜色和周围组织反差太大、位置出现在不可能的解剖区域。如果模型在训练集上对这些假异常分类很准,一到真实异常就崩,基本就是合成分布和真实分布脱节。解决办法是降低合成权重,或者用真实异常做少量微调(但要小心重新引入泄漏),或者干脆换嵌入类方法。
5.3 小病灶漏检:先怀疑分辨率
如果你的模型大病灶都抓到了,就是小病灶漏,第一反应应该是分辨率问题,而不是模型问题。排查方法是:把分数图放大,看原图上明明有病灶的地方分数为什么低。很多时候是因为预处理缩得太狠,病灶在特征图上已经没了。解决要么提高输入分辨率,要么分块处理,要么在评估时对小目标单独统计。别急着换更复杂的模型,先把这个最可能的原因排除掉。
5.4 排查速查表
| 现象 | 最可能原因 | 排查方向 | 处理建议 |
|---|---|---|---|
| 指标异常高 | 数据泄漏 | 检查划分粒度、预处理统计量 | 按受试者划分,统计量只用训练集 |
| 小病灶全漏 | 分辨率过低 | 放大分数图核对病灶位置 | 提高分辨率或分块处理 |
| 合成异常失效 | 假异常不真实 | 肉眼检查合成图质量 | 换方法或降权重 |
| 多类别掉点 | 跨类干扰 | 检查各类别混淆矩阵 | 加类别条件或独立建模 |
| 结果不稳定 | 随机源未固定 | 检查种子、合成随机性 | 全流程固定种子 |
| 像素级很差 | 掩码未对齐 | 核对分数图与掩码尺寸 | 掩码用最近邻插值同步缩放 |
5.5 几个独家避坑技巧
再分享几个我实际踩出来的经验。第一,评估一定要做阈值敏感性分析,别只报一个最佳 F1,因为部署时你得自己定阈值,只看最优阈值会给你虚假信心。第二,跨模态比较时,先单独跑每个模态,再看多类别,否则你分不清掉点是跨类干扰还是某个模态本身太难。第三,文档里写清楚你用的预训练权重来源和版本,换一个 timm 版本,特征分布可能就变了,别人复现不出来会浪费大量时间。这些细节论文不会写,但决定你的工作能不能被别人用起来。
医学图像异常检测这条路,选对评估口径比选对模型更重要。我个人的体会是,先老老实实把数据划分、指标、预处理固定死,再谈方法对比,你会少走很多弯路。如果你想继续深入,接下来可以沿着"小病灶像素级定位""跨模态统一建模""有标注数据下的半监督"几个方向扩展,这些都是这份基准明确指向但还没完全解决的硬骨头。