医学图像异常检测这个方向这几年在学术圈和工业界的待遇可以说是冰火两重天。工业侧MVTec AD 那一套基准把表面缺陷检测推进到了相当成熟的地步一堆开源仓库拿下来改改就能跑医学侧却始终处在一种各说各话的状态——同样一个肺 X 光数据集有人报 AUC 0.92换篇论文变成 0.78中间差着 14 个点你根本分不清是方法强还是数据划分、预处理、评价口径在打架。MedIAnomaly 这篇工作就是冲着这个痛点来的它做了一件听起来不性感但极其必要的事搭一套统一的医学图像异常检测比较框架把主流算法拉到同一张桌子上用同样的数据划分、同样的评价指标、同样的预处理流程重新跑一遍。关键词落在MedIAnomaly、医学图像、异常检测三个点上但它真正解决的是我到底该选哪个算法这个工程问题。如果你是从工业异常检测算法转到医学场景的或者正在做一个肺片/肝 CT/眼底 OCT 里找病灶的项目又或者单纯想搞清楚重建类、合成异常类、自监督特征类这几大流派到底谁更靠谱这篇内容值得往下看。我会把论文的方法分类、复现路径、踩坑记录和我自己实测下来的体会都摊开讲尽量做到看完就能上手改代码。1. 为什么医学图像异常检测需要一套统一的比较框架1.1 医学图像与工业图像的异常定义根本不在一个维度工业异常检测里的异常通常是物理性的、局部的、边界清晰的划痕、凹坑、污渍你拿个高分辨率相机拍一张缺陷区域像素和背景像素的统计分布差异明显。MVTec AD 里绝大多数类别一个正常的自编码器配上足够大的训练集就能刷到 0.95 以上的 AUROC。这种场景下重建误差大就是异常这个假设成立得相当舒服。医学图像完全是另一回事。肺 X 光里的肺炎浸润影从来不是一块边界清楚的异物而是弥漫性的、和正常组织强度重叠的、甚至需要对比双侧才能察觉的灰度变化。肺结节在 CT 上可能只有十几个像素和血管横截面长得几乎一样。眼底 OCT 里的黄斑水肿表现为视网膜层间的低反射暗区本质上是一种结构缺失而不是额外叠加。更要命的是同一个病人不同时间点的片子因为曝光、设备、体位差异全局亮度分布就能漂移一大截重建类方法会把这种全局漂移全部算成异常产生大量假阳性。从数据规模上看差距更恐怖。工业基准动辄几百上千张正常样本医学数据集里一个类别的正常样本常常只有几十到一两百例异常样本更少且标注成本极高——一个三甲医院的放射科医生标注一张 CT 切片大概需要几十秒到几分钟还要多医生交叉复核。这个现实决定了医学异常检测绝大多数场景必须走仅用正常样本训练的无监督路线而这恰恰是各家算法差距被放大到失真的地方。注意把工业基准上调好的超参数直接搬到医学数据上十有八九会崩。工业图的分辨率、对比度归一化方式、异常区域的面积占比都与医学图差异巨大尤其是异常像素占比这个量工业缺陷常常占 1%~5%肺结节在整张 X 光里可能只占 0.01%类别极度不平衡会直接把 F1、AP 这类指标拉到很低。1.2 MedIAnomaly 瞄准的三个真实痛点我把这篇工作想解决的问题归纳成三条每一条都对应着我实际做项目时被卡过的位置。第一条是评价口径不统一。不同论文的数据划分方式五花八门有的按病人划分有的按切片划分后者会造成同一病人的相邻切片同时出现在训练集和测试集里模型实质上是在背病人特征指标虚高得离谱。MedIAnomaly 把划分固定下来明确按病人级别做 split这一点看着不起眼但它直接决定了不同方法之间的数字能不能比。第二条是方法分类混乱、命名重复。同样叫基于重建有的论文指的是 AE 重建原图算残差有的指的是 VAE 用重建概率做密度估计还有的是 GAN 的判别器特征做残差。名字一样数学形式差着十万八千里。MedIAnomaly 按异常分数从哪里来这条主线重新归类而不是按网络结构归类我觉得这个视角切换很关键。第三条是缺少统一的工程基线。学术论文往往只报最好的那个数但做工程的人关心的是这个方法在正常样本只有 50 例时还能不能训、推理一张 512×512 的图要多少毫秒、需不需要预训练权重、显存吃多少。MedIAnomaly 把这些非学术指标也纳入比较虽然论文里写得比较克制但对落地的人价值很大。1.3 统一基准的构成与设计取舍框架整体是三层结构数据层负责统一六个医学数据集的目录规范、灰度归一化和分辨率处理算法层把所有方法统一到同一个接口下输入是图像张量输出是像素级异常分数图评价层统一算 AUROC、AP、DSC、F1并统一后处理流程比如高斯平滑核大小、阈值搜索策略。这里有个设计取舍值得说。医学图像的分辨率千差万别肺 X 光常见 1024×1024 甚至更高脑 MRI 切片多是 256×256肝 CT 的 HU 值范围又和自然图像完全不是一回事。很多论文为了省事统一 resize 到 256×256这会直接把小目标异常比如微小肺结节抹掉。MedIAnomaly 的做法是保留原始分辨率、只做强度归一化代价是训练显存吃紧、需要按数据集调 batch size。我个人是认同这个取舍的——在医学场景下先保证信号不丢再谈效率顺序不能反。另一处取舍是在预训练权重上。工业异常检测里 PatchCore 这类方法强依赖 ImageNet 预训练特征医学图像用 ImageNet 权重到底合不合适一直有争议自然图像的特征是否真的迁移得过来MedIAnomaly 同时提供了带预训练和不带预训练两套配置让读者自己对比。实测结论是迁移确实有效但增益幅度比工业场景小而且不同数据集差异很大这个后面细说。2. 方法分类与核心原理拆解2.1 基于重建的一类假设正常样本能被压缩再还原重建类方法的核心假设非常直白正常样本处于一个低维流形上模型学会了这个流形遇到异常就还原不好残差就大。最基础的 AE自编码器就是编码器-解码器结构损失是 MSE 重建误差推理时逐像素算 |x - x̂| 作为异常分数。听起来完美实际上有三个坑。第一个坑叫身份映射identity mapping如果网络容量足够大、训练足够久AE 会退化成恒等变换连异常也能原样重建出来这时候残差全是零方法彻底失效。经典的缓解手段是加瓶颈层、加噪声、加稀疏约束但都是治标。第二个坑是模糊导致的假阳性。MSE 损失天然倾向于输出平滑图像因为对高频细节的惩罚在梯度上表现为均值化。训练几百轮之后正常区域的边缘肺野边界、肋骨边缘、血管走行因为重建模糊都会产生不小的残差这些残差和真实病灶的残差量级相当阈值一卡就一片假阳性。我试过的最有效的缓解办法是在残差图上做各向异性高斯平滑沿血管方向平滑强、垂直方向平滑弱但实现复杂度会上升不少。VAE 在这条路上做了改进用隐空间的正态先验约束编码分布推理时用重建概率的对数似然作为分数理论上比纯 MSE 更有原则性。代价是 VAE 的重建更模糊而且存在后验坍塌的风险训练时需要调 KL 项的权重系数 β这个系数对结果极其敏感我见过同一个模型 β 从 1.0 调到 0.1AUROC 差 8 个点的情况。实操心得重建类方法不要只看训练 loss 收敛就喊完事。一定要在训练集上单独留一小部分正常样本做验证监控正常样本的残差分布。如果你的正常验证集残差方差很大说明模型在正常样本上都不稳定异常分数会失去区分度。这个检查我每次都做能省掉大量无用实验。2.2 基于合成异常的一类自己造病灶来教模型区分这条路线近两年在医学领域火得很快代表就是各类合成异常策略把正常图像做局部切除再随机贴到别处CutPaste 思路、用纹理块做泊松融合、用简单的几何形变模拟病灶或者像 DRAEM 那样直接用一个独立的合成网络学出逼真的异常。它的核心逻辑是把无监督问题转化成有监督问题既然没有真实异常标注那我就造一批假异常训练一个判别网络去区分真正常和假异常同时用一个重建分支保证正常区域的重建质量。推理时两者的输出结合得到异常分数。这条路线的魅力在于思路直观、训练稳定而且不需要复杂的密度估计。但我在实际项目里发现它的泛化性高度依赖合成策略与真实异常的相似度。CutPaste 造出来的是矩形块和肺炎的弥漫浸润影子差得太远训出来的模型对矩形块极其敏感对真实病灶反而迟钝。所以合成策略必须结合具体解剖结构去设计比如做脑 MRI 的时候可以沿着脑室形状做形变合成做肝 CT 的时候沿着肝段边界合成低密度区这些领域先验注入是提升效果的关键也是论文里比较难标准化的一部分。另一个必须警惕的点是合成异常不能出现在测试集所依赖的正常分布里。我见过有人把合成图像直接存回训练目录结果训练集里混进了假异常模型学到了错误的正常分布AUC 直接掉。工程上一定要把合成过程做成在线增强on-the-fly而不是离线写盘。2.3 基于自监督特征的一类借预训练模型的常识这一类是目前医学异常检测里表现最稳的我个人项目里默认首选。它的逻辑是自然图像上预训练好的 CNN 或 ViT已经学到了大量泛化的纹理、形状、边缘表征这些表征对医学图像也有相当程度的可迁移性。做法是把正常训练样本全部喂给冻结的骨干网络提取中间层特征然后在特征空间里做建模——可以是高斯分布拟合PaDiM 思路可以是特征记忆库加最近邻PatchCore 思路也可以是把特征袋喂给一个简单的密度估计器。为什么它稳因为骨干网络是冻结的不会过拟合到几十张正常样本上。医学数据集小这个致命短板被借用外部知识这一招对冲掉了。代价是推理时要保留特征库PatchCore 那套 coreset 采样后的记忆库动辄几万个向量配上 512×512 的输入单张推理延迟可能上到几百毫秒在需要实时处理的场景里要仔细权衡。特征层的选择也很有讲究。浅层特征分辨率高、定位准但语义性弱深层特征语义强、定位粗。PaDiM 的做法是选多个中间层拼接再随机降维到较低维度以避免维度灾难。我在肺 X 光上做对比时发现加不加随机降维对结果影响不大但对内存占用影响巨大是一个非常划算的优化点。2.4 基于归一化流与密度估计的一类归一化流Normalizing Flow在异常检测里的用法是学一个可逆变换把复杂的特征分布映射到标准正态分布推理时用负对数似然作为异常分数。它的优势是密度估计是精确的、可逆的不像 VAE 那样只是个下界。FastFlow 这类工作把它做成了 2D 特征图上的流模型既保留空间信息又能做像素级定位。我在肝 CT 上试过这类方法结论是对整体风格漂移型异常比如整个肝段密度异常相当敏感对微小局部病灶就一般。原因也不难理解流模型建模的是全局分布的形状局部小扰动在似然上体现不出来。另外这类方法的训练时间通常比 AE 长不少因为可逆结构限制了网络设计自由度不容易并行加速。2.5 四类方法的横向对比方法类别核心思想优点主要短板适合场景重建类AE/VAE/GAN正常样本重建残差作分数实现简单、无需预训练、可解释易身份映射、边缘假阳性多异常面积大、形状弥散的场景合成异常类造假病灶做判别学习训练稳定、监督信号明确合成策略依赖领域先验、泛化不稳有明确病灶形态可模拟的场景自监督特征类冻结骨干提取特征建密度模型小样本下最稳、无需训练骨干推理慢、依赖预训练权重正常样本极少、追求精度的场景归一化流类可逆变换做精确密度估计密度精确、定位有理论依据训练慢、小目标不敏感整体分布漂移型异常这张表是我自己整理的经验版本和论文里的分类大框架一致但加入了不少用起来什么感觉的判断。实际选型时不要迷信某一类而是先看你的异常是什么形态弥散的、局部的、还是全局的。3. 数据集、指标与实验协议怎么定才不出错3.1 六个数据集的覆盖逻辑MedIAnomaly 挑选的六个数据集其实覆盖了医学影像里最主要的几种模态和器官胸部 X 光、脑部 MRI、肝脏 CT、眼底 OCT、胸部 CT、组织病理切片。这个组合不是随便凑的它刻意做到了三点覆盖模态上覆盖 X 光、CT、MRI、OCT、病理五种成像原理维度上覆盖 2D 切片和 3D 体数据通过切片化处理异常形态上覆盖了弥散浸润、局部结节、结构缺失、纹理异常四大类。理解这个覆盖逻辑对你做数据集选型很有帮助。如果你想验证一个新方法最忌讳的就是只在 ChestX-ray 上刷个高分就宣布胜利——ChestX-ray 的异常是大面积浸润几乎所有方法都能刷到不错的数字区分度很低。反过来 BrainMRI 和 LiverCT 的小病灶才是真正的试金石。我自己做实验时的习惯是先在 ChestX-ray 上跑通流程、确认代码没 bug然后在 BrainMRI 和 LiverCT 上做真正的效果对比最后用 Retina OCT 做鲁棒性检验。3.2 指标不是越多越好而是要选对异常检测的评价指标看着简单实际上很容易踩坑。MedIAnomaly 主要报四个图像级 AUROC、像素级 AUROC、AP 和 DSC/F1。图像级 AUROC 衡量的是整张图判不判为异常的排序能力对类别不平衡不敏感是最好用的粗筛指标。但它有个致命盲区它只看排序不看阈值。如果你的应用需要输出这张图有没有问题的硬判断AUROC 0.95 也可能对应一个很难看的 F1。像素级 AUROC 和 AP 衡量的是定位能力。这里一定要小心AP 对正负样本极度不平衡很敏感医学图像的异常像素占比常常低于 1%这时候 AP 的数值会非常低看起来像方法完全不行其实只是数据分布的问题。我的做法是同时报像素级 AUROC对不平衡不敏感和最佳 F1反映实际可用性AP 作为补充参考。DSCDice 相似度只在有像素级真值标注的数据集上才有意义而且对阈值极其敏感。论文里通常用遍历所有阈值取最优 DSC这种偏乐观的算法实际部署时你不可能知道最优阈值是多少。所以我在工程中会额外做一件事用验证集上选定的固定阈值去测试集上评估这个数字才是你能对外承诺的性能。注意看到论文里最优 F1或者最优 DSC这种指标时先别急着对比。问清楚阈值是怎么选的——如果是在测试集上遍历得到的那这个数是有水分。真正可信的做法是训练集/验证集/测试集三级划分阈值在验证集上定测试集只用一次。3.3 划分方式决定了结果可信度这一点再怎么强调都不为过。医学数据的划分有三个层级必须严格按最高层级做病人级划分同一个病人的所有切片只能出现在一个集合里。这是底线要求。时间级划分如果有纵向随访数据同一病人的不同时间点也要注意分布避免信息泄漏。设备/中心级划分如果数据来自多个中心最好做跨中心验证这才是真实部署场景。MedIAnomaly 在协议上明确了病人级划分这一点比很多早期工作都要严谨。我自己在复现的时候会额外打印出训练集和测试集的图像均值、方差、尺寸分布做个 sanity check如果分布差异特别大说明划分可能有问题或者数据本身有采集偏移需要单独处理。4. 实验结果里那些反直觉的发现4.1 复杂结构并不必然赢过简单方法我做复现时最先看的就是最复杂的那个方法是不是最好的。答案是否定的。在很多数据集上一个冻结骨干加高斯建模的特征类方法表现不输甚至优于精心设计的 GAN 重建方法。原因其实很朴素医学数据样本量太小复杂模型的参数量带来的过拟合风险远远大于它表达能力的收益。这个结论对工程选型意义重大。不要因为某个方法论文里画了一堆漂亮的网络结构图就优先选它。先跑最简单的基线AE、PatchCore 风格的特征最近邻拿到一个数字作为基准线然后再考虑加复杂度。我踩过的坑就是一开始非要上 VAE GAN 的双分支结构调了两周参数最后发现一个朴素的特征方法在同一数据上 AUC 还高 2 个点。4.2 合成异常的增益是有条件的合成异常类方法在部分数据集上确实拿到了最好成绩但不是普遍现象。规律大致是这样的当真实异常的形态和合成策略接近时比如都是局部块状的增益明显当真实异常是弥散的、边界模糊的合成策略的收益就迅速衰减甚至因为引入误导性的监督信号而变差。这给我的启示是用合成异常方法之前先花时间做一件看起来很土的事情——把真实异常样本画出来一个个看它们的形状、大小、灰度分布、边界锐利程度然后反推该用什么样的合成策略。这个看图环节比调参数重要得多我一般会花一整个下午只做这件事。4.3 数据集难度排序与迁移性按照我复现的体感从易到难大致是胸部 X 光弥散、面积大 眼底 OCT结构层次清晰 脑部 MRI病灶小但对比度高 肝脏 CT病灶小、灰度重叠严重 组织病理纹理极其复杂、染色差异大。跨数据集迁移的表现则相当糟糕。在一个数据集上调到最优的超参数换到另一个数据集上通常需要重新调。这说明目前这些方法学到的还是数据集特有的分布特征而不是通用的异常概念。这一点在做落地项目时要特别注意千万别指望拿开源代码的默认配置直接上你的院内数据。4.4 效率与精度的真实权衡论文里的效率比较往往只报参数量和 FLOPs但实际部署关心的是另外几个数单张推理延迟、显存峰值、批处理吞吐、是否需要 GPU。我实测下来的经验是AE 类方法推理最快512×512 输入下大概几十毫秒特征记忆库类方法因为要做最近邻检索延迟会到几百毫秒如果记忆库规模大还可能上秒级归一化流类方法介于两者之间。如果做的是离线批量筛查比如夜间批量处理一天的门诊片子延迟完全不敏感可以放心用大模型如果做的是医生工作站里的实时辅助那必须压到 100ms 以内选择面会窄很多。5. 自己动手复现从环境到跑通5.1 目录结构与数据准备复现这类比较框架最大的时间消耗往往不是跑模型而是把数据整理成框架要求的格式。我的做法是先把所有数据集统一到一个目录规范下每个数据集一个文件夹训练和测试分开标签文件用 CSV 管理每行是图像路径和 0/1 标签有像素级标注的再加一列掩码路径。data/ ├── chestxray/ │ ├── train/good/ # 仅正常样本 │ ├── test/normal/*.png │ ├── test/abnormal/*.png │ └── test_masks/*.png # 像素级标注可缺 ├── brainmri/ ├── liverct/ └── ...标签 CSV 长这样image_path,label,mask_path test/normal/001.png,0, test/abnormal/002.png,1,test_masks/002.png灰度归一化建议按数据集单独算不要用全局固定的均值方差。我一般统计训练集正常样本的 1% 和 99% 分位数然后把强度线性拉伸到这个区间再减均值除标准差。这一步对重建类方法影响特别大用错了会让重建 loss 从 0.01 直接飙到 0.3。5.2 关键配置参数怎么定框架一般用 YAML 管配置下面是我常用的一个模板注释里写清楚每个参数为什么这么设dataset: name: brainmri img_size: null # 保留原始分辨率不做 resize normalize: percentile # 用 1-99 分位拉伸比 min-max 稳 batch_size: 8 # 受显存限制512x512 下 8 张约 10G model: name: feature_knn # 先用简单基线打样 backbone: resnet18 layers: [layer2, layer3] # 中层特征兼顾定位和语义 coreset_ratio: 0.1 # 记忆库采样比例10% 通常够用 n_neighbors: 3 train: epochs: 0 # 冻结骨干不需要训练 seed: 42 eval: metrics: [auroc_img, auroc_pix, f1_pix] smooth_sigma: 2.0 # 残差图高斯平滑压制边缘噪声 threshold_source: val # 阈值从验证集选绝不在测试集上选几个参数的具体考量img_size设成 null 是我强烈建议的除非显存实在不够否则不要 resizecoreset_ratio采样比例降到 0.1 甚至 0.01对精度影响通常在 1 个点以内但推理速度能快好几倍非常划算smooth_sigma这个参数对最终指标影响巨大我试过从 0 调到 4F1 能差十几个点建议在验证集上网格搜一下。5.3 评测脚本与结果对齐评测这一步最容易出问题的地方是后处理和阈值。我的评测流程固定成四步对每张测试图算出异常分数图做高斯平滑在验证集上遍历阈值找到使 F1 最大的那个阈值 t*用 t* 在测试集上算 F1、DSCAUROC 和 AP 直接用连续分数算不依赖阈值。像素级指标还要注意一个细节要不要把小于某个面积的连通域过滤掉。医学图像里几十个像素的孤立响应基本都是噪声加一个面积过滤比如小于 20 像素的连通域置零通常能提升 2-5 个点的 F1代价是可能漏掉极小的真病灶。这个权衡要结合临床需求来定做筛查可以过滤做早期微小病灶检测就不建议过滤。6. 常见问题与排查技巧实录6.1 问题速查表现象可能原因排查方法处理建议训练 loss 降到接近 0 但测试 AUC 只能到 0.5身份映射AE 学成了恒等变换可视化正常样本的重建残差加瓶颈、加噪声、换特征类方法正常测试样本残差普遍偏高预处理不一致或全局亮度漂移对比训练集与测试集强度直方图统一归一化用分位数拉伸像素级 F1 很低但 AUROC 很高阈值选得不好或后处理缺失画出验证集上的 F1-阈值曲线在验证集上定阈值加高斯平滑换数据集后性能断崖超参过拟合到原数据集检查关键超参是否与原数据集绑定逐数据集重调尤其是平滑核推理慢到不可接受记忆库太大或输入分辨率过高统计记忆库向量数与单张延迟降采样比例或换轻量方法AP 数值极低看起来像完全失败异常像素占比过低导致统计异常像素占总像素比例以像素级 AUROC 为主AP 为辅显存爆掉保留原分辨率导致 batch 太大梯度累积或降 batch用梯度累积模拟大 batch这张表里的每一条我基本都真实遇到过尤其是第一条和第三条是新手最容易卡住的地方。6.2 几个文档里不会写的避坑技巧第一个技巧用正常样本自评分作为健康检查。训练完之后先拿一批训练时完全没见过的正常样本跑一遍统计它们的异常分数分布。如果这个分布的均值本身就很高或者方差很大说明模型学到的正常分布是不稳定的这时候不管测试集 AUC 多高都不可信。我一般在正式评测前只看这一个数能挡掉八成有问题的实验。第二个技巧残差图一定要可视化而且要看至少 20 张。数字指标会骗人可视化不会。我见过不少情况是 AUC 看着不错但一看热力图模型高亮的全是肋骨边缘和图像边框根本没在病灶附近。这种指标好看但物理意义错误的模型上线就是灾难。第三个技巧随机种子至少要跑三遍。医学数据集小随机种子对结果的影响远比你想的大。我在 BrainMRI 上实测过同一个配置换三个种子像素级 AUROC 波动能到 3-4 个点。如果两个方法差距只有 1-2 个点那基本可以认为没差距。做对比实验时报平均值和标准差比报单次最优值诚实得多。第四个技巧数据泄漏排查要做成自动化脚本。我写了一个小脚本把训练集和测试集的所有图像做感知哈希pHash算一下有没有重复或高度相似的图像。这个检查帮我抓到过两次问题一次是同一个病人的不同切片被分到了两个集合另一次是数据增强后的图像被误存进了测试目录。这类错误一旦混进去指标会虚高得离谱而且很难通过看 loss 曲线发现。第五个技巧把评测流程本身当作代码来测试。我习惯用一组人造数据比如纯黑背景加一个白方块去跑评测脚本预期结果是 AUROC 1.0、DSC 接近 1.0。如果脚本在这种极端情况下的输出不对说明指标实现有 bug。这个测试花不了十分钟但能避免你把两天时间浪费在排查为什么指标算不对上。我个人在实际操作中的体会是医学图像异常检测这个方向的难点从来不在模型结构上而在数据、协议和评测这三件事上。MedIAnomaly 这类比较框架真正的价值是逼着你把这三件事做规范。模型选错了顶多差几个点数据划分错了或者阈值选错了差的可就是能用和不能用的距离。所以每次开新项目我宁愿花头两三天时间只干一件事把数据目录、划分脚本、评测脚本这三块打磨干净然后再上模型。后面再分享一个扩展方向——如果你手上的数据有少量像素级标注哪怕只有几十张把它揉进训练流程做弱监督通常能比纯无监督提升一大截这个方向目前还比较开放值得折腾。