ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

医学图像分割超经典项目复现:U-Net源码与数据集实战

医学图像分割超经典项目复现:U-Net源码与数据集实战 简介这是一套基于 Python 与深度学习技术实现的医学图像分割系统源码核心采用 U-Net 经典网络结构并附带了可直接用于训练与测试的图像数据集。资源面向计算机、通信、人工智能、自动化等相关专业的学生和从业人员既可当作毕业设计、课程大作业的完整参考也适合初学者对照学习深度学习在医学影像分割中的落地流程。压缩包共包含 136 个文件整体约 13.75MB。其中 6 个 Python 脚本为核心实现代码120 张 PNG 图片提供训练样本或可视化结果另有 XML 标注文件、docx 使用手册和工程配置文件便于快速理解项目结构。项目作者已完成调试并经过答辩验证评审得分达 98 分目前已有 316 人浏览学习。下载后既能获得一个可运行的高分项目框架也能通过源码注释和使用手册掌握 U-Net 的数据处理、模型构建与预测流程学有余力者还可以基于现有代码替换数据集或调整网络层进行二次开发扩展。1. 医学图像分割为什么这类“超经典”项目值得复现老师甩给你一套 CT 序列让你三天把肝脏轮廓自动抠出来或者毕设要求提交一个基于深度学习的医学图像分割系统还附数据集。你会发现能搜到的“超经典”方案翻来覆去就是那几样东西Python 写的训练脚本、一套 U-Net 或它的变体、若干公开医学影像数据集。这个标题指向的项目本质上是把“医学影像里逐像素找器官/病灶”这件事用深度学习做成了一套可训练可推理的流程。它适合正在找毕业设计高分项目的学生、想切入医疗方向的算法工程师也适合需要批量处理影像标注的医学研究生。下面按选型、数据、训练、排错、进阶的顺序把这套链路完整拆开。2. 选型与架构U-Net、DeepLabV3 与 nnU-Net谁才是“超经典”的正主拿到“源码数据集”的压缩包先别急着配环境。打开模型定义文件看看网络结构这比跑通一个 epoch 更重要因为标题里的“超经典”是有具体指向的。医学图像分割里能配得上这三个字的基本就是 U-Net 及其变体它在 2015 年提出后到今天仍是医学分割竞赛的默认基线。原因不难理解医学影像样本量普遍不大通常几百到几千例而 U-Net 用跳跃连接把浅层细节和深层语义打通在小数据上就能收敛得不错它输出的分割图也是像素级对齐到原图不会出现分类网络那种空间信息丢失的毛病。DeepLabV3 在自然图像分割上是强者到医学影像里反而水土不服这在后面细说。而 nnU-Net 虽然是 U-Net 的“完全体”但它把预处理和训练策略都自动化了反而失去手工复现的价值。我判断一套源码是否值得复现只看三点是不是 U-Net 路线数据预处理是否有医学影像专属步骤损失函数和评价指标是否匹配。按这个标准筛一圈市面上流传的所谓高分项目中真正达标的其实不多这也是为什么独立掌握实现细节比拿到源码更重要。2.1 U-Net 的编码器-解码器结构为什么长盛不衰U-Net 的左侧编码器连续做卷积和下采样分辨率逐层减半、通道数逐层翻倍右侧解码器再对称地上采样回来。跳跃连接把编码器第 n 层的高分辨率特征直接拼到解码器第 n 层相当于给深层语义加上了像素级定位的辅助信息。对 CT、MRI 这类低对比度影像这个结构几乎是天生匹配的。我以 PyTorch 为例写最小实现骨架这个写法在做医学分割的团队里几乎是标准答案import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, n_channels1, n_classes2): super().__init__() self.enc1 DoubleConv(n_channels, 64) self.enc2 DoubleConv(64, 128) self.enc3 DoubleConv(128, 256) self.pool nn.MaxPool2d(2) self.mid DoubleConv(256, 512) self.up3 nn.ConvTranspose2d(512, 256, 2, stride2) self.dec3 DoubleConv(512, 256) self.up2 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec2 DoubleConv(256, 128) self.up1 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec1 DoubleConv(128, 64) self.out nn.Conv2d(64, n_classes, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) m self.mid(self.pool(e3)) d3 self.dec3(torch.cat([self.up3(m), e3], dim1)) d2 self.dec2(torch.cat([self.up2(d3), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return self.out(d1)这段代码里的关键参数n_channels1表示输入是单通道灰度医学影像如果你用的是三通道伪彩图或三模态 MRI 拼接把它改成 3n_classes2是二分类背景加一类目标器官做多器官分割时改成对应数量编码器初始通道选 64 是一个折中值显存不够换成 32。还有两个细节第一个是Conv2d里biasFalse因为后面接了 BatchNorm卷积偏置会被 BN 抵消留着它对网络容量没有帮助第二个是解码器里torch.cat之前要确保上采样结果和跳跃连接特征的空间尺寸一致最常用ConvTranspose2d(2, stride2)做两倍上采样这个组合不会产生棋盘伪影。在实际实现中我通常会把 BatchNorm 在 batch_size 小于 8 时换成 GroupNorm。原因在于 BN 依赖当前 batch 的统计量医学分割显存有限batch 通常只有 4 甚至 2统计量抖动明显BN 的均值和方差估计不准。GroupNorm 不依赖 batch 维度在 batch1 时也能稳定训练。2.2 DeepLabV3 和它的空洞卷积为什么在医疗场景水土不服如果你是从自然图像分割转过来的很自然会把 DeepLabV3 带到医学影像上。它用空洞卷积逐层扩大感受野在 VOC、Cityscapes 这种多类别、多尺度场景上表现很强。但是医学影像的情况恰好相反目标结构相对固定难点在于边界模糊、对比度低、目标小。空洞卷积的采样网格是稀疏的会漏掉小器官的细节特征。我在同样数据上做过对比DeepLabV3 的 Dice 普遍比 U-Net 低 2 到 5 个百分点而且模型体量更大训练更慢。DeepLabV3 唯一值得保留的组件是 ASPP 模块空洞空间金字塔池化它用多个不同空洞率的并联分支捕捉上下文信息。如果你想在 U-Net 的编码器底部加入对全局信息的感知可以在 bottleneck 处并联一个轻量 ASPP这是很多高分医学分割实现里都会做的小改动。但不要把整个 backbone 都换成空洞卷积结构对 2D 切片而言性价比很低。2.3 拿到一套“超经典”源码先检查这三个文件如何区分一套源码是“真经典”还是“缝合怪”我一般用十分钟检查三个点。第一模型文件里是否有跳跃连接没有跳连的基本是 FCN 简化版复现意义不大第二transform 里是否有随机弹性形变医学分割的鲁棒性一半靠它撑起来的第三数据读取是否处理了.nii.gz格式如果只支持 png/jpg那说明项目作者对医学影像理解有限。还有一个容易被忽略的点训练脚本里是否把窗宽窗位作为可配置参数暴露出来没暴露的不是不能用只是后续在 CT 以外的数据上泛化会遇到问题。换句话说真正值得投入复现的医学分割源码模型细节其实只占四成剩下六成全在数据流水线里。3. 数据集与预处理从公开数据集到能训练的样本先过这三道坎标题里同时带“源码”和“数据集”最常见的情况是压缩包里已经放好了一份整理过的公开数据集。医学影像公开数据集的数量其实不少但来源分散、格式各异。CT 用得多的是 LiTS肝脏和肝脏肿瘤、TCIA 里各种器官数据MRI 用得最多的是 BraTS脑肿瘤多序列2D 内镜和皮肤镜方面的典型是 Kvasir-SEG 与 ISIC2018。这些数据集拿到手以后没有一个是能直接进网络的格式转换、归一化、类别过滤、切片重采样每一步都能决定模型的上限。3.1 nii.gz 三维体数据从读到切片维度方向最容易搞错基础的坑是医学影像都是三维体数据文件后缀是.nii.gzPIL 不能读。要用 SimpleITK 或 NiBabel 加载。以 LiTS 为例一个病人的 CT 是一个完整的volume-xxxx.nii.gz文件mask 是另一个segmentation-xxxx.nii.gz文件。加载后的数组维度是(z, h, w)而不是(h, w, c)这是医学影像库和自然图像库的约定差异导致很多第一次接触的人在这里翻车。import SimpleITK as sitk import numpy as np def load_nii(path): itk_img sitk.ReadImage(path) arr sitk.GetArrayFromImage(itk_img) spacing itk_img.GetSpacing() return arr, spacing ct_arr, ct_sp load_nii(volume-0.nii.gz) seg_arr, seg_sp load_nii(segmentation-0.nii.gz) print(CT volume shape:, ct_arr.shape, spacing:, ct_sp) print(Mask label values:, np.unique(seg_arr))注意 SimpleITK 按(x, y, z)读 spacing元素顺序和 numpy 数组顺序axis 2, 1, 0是反的这个顺序差异做重采样时要对齐。切片时直接取第三维索引比如ct_arr[100]就是 z100 的一张横断面切片。另一个经验是把三维体数据全部按切片平铺成 2D 训练样本时要逐体素检查对应关系CT 和 seg 的切片数在个别病例里可能不一致这种样本要单独处理。重采样是另一个不起眼但影响巨大的环节。不同设备的 z 轴层厚可能从 0.5mm 到 5mm 不等直接把不同层厚的体数据混在一起训练模型会学到一些与解剖无关的设备特征。常见做法是把所有体数据在 z 轴方向线性插值到统一层厚一般取 1mm 或 2mm。如果数据量小或者只做 2D 切片训练也可以按体数据为单位分类把层厚差异大的样本分到不同 fold 里做交叉验证至少不让同一患者的切片泄漏到训练和验证两边。3.2 窗宽窗位与归一化CT 能不能“看清”就靠这一行代码CT 影像的原始像素值是亨氏单位HU理论范围从 -1024 到 3071绝大多数软组织都集中在 -100 到 200 这个很窄的区间里。如果直接做全局 MinMax 归一化绝大部分数值会被压缩到几乎为零的区间模型只能看到一片模糊的灰。解决办法就是窗宽窗位裁剪。肝脏分割的标准窗口是窗位 40、窗宽 200等价于把 [-60, 140] 之外的像素都截断再映射到 [0, 1]。写成代码就几行def ct_window_clip(volume, window_center40, window_width200): lower window_center - window_width / 2 # -60 upper window_center window_width / 2 # 140 volume np.clip(volume, lower, upper) volume (volume - lower) / (upper - lower) return volume这段逻辑放进了几乎所有专业的预处理 pipeline 里。不同器官要用不同窗骨骼看骨窗窗位 400、窗宽 1800肺部看肺窗窗位 -500、窗宽 1500脑出血看脑窗窗位 40、窗宽 80。做多器官分割时可以每个器官的输入都用自己的窗口也可以把两个窗口下的图像拼接成双通道输入后者在不少竞赛方案里被证明能稳定提升分割效果。MRI 图像没有 HU 值不需要做窗位但要做 z-score 标准化而且最好按每个体数据分别算均值和方差不要用整个数据集的全局统计以免患者间的亮度差异被强行拉平。3.3 标签不平衡与样本过滤背景占 95% 时损失函数该怎么写医学分割的标签图里背景占比经常超过 95%CT 切片尤其明显。在这种分布下直接用交叉熵模型最省事的策略是全部预测为背景损失依然很小Dice 却为零。这也是为什么主流医学分割项目几乎全部采用 Dice Loss 或其组合。Dice Loss 直接以目标重叠率为优化对象对样本不平衡天然不敏感但单独的 Dice Loss 也有一点点问题梯度对前景像素较饱和对小目标的边界推动不足。所以常见的做法是 BCE Dice 双损失各占一半权重。写成最小实现如下import torch import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): pred torch.sigmoid(pred) intersection (pred * target).sum() return 1 - (2.0 * intersection smooth) / (pred.sum() target.sum() smooth) def bce_dice_loss(pred, target): bce F.binary_cross_entropy_with_logits(pred, target) dice dice_loss(pred, target) return bce dicesmooth 取 1.0 是默认经验值作用是同时防止分母为零和稳定梯度通常不用改。另一个常用方案是 Focal Loss它对难分类样本如小病灶边缘像素加大权重对易分类的背景样本降权gamma 取 2 是初值。在肿瘤分割这种小目标场景Focal 通常比 Dice 收敛得更稳定但前提是你有耐心调 gamma 和 alpha 两个超参否则效果还不如 BCEDice。除了损失函数样本过滤同样重要。三维体数据切出来的切片大约三分之一以上可能完全没有前景像素。这些切片对训练几乎没用。常见做法是在构建 Dataset 时统计每张 mask 的前景像素比例低于 0.5% 的切片直接丢弃。这个阈值不宜太高因为小病灶本身前景占比就只有零点几个百分点。过滤完训练集切片能减少两到三成收敛速度也会明显加快。4. 训练与调参把损失函数、学习率与评价指标一次说清模型结构选定、数据预处理完成接下来的训练阶段才是真正拉开差距的地方。一套医学分割系统在技术报告里给的 Dice 是 0.87 还是 0.79差异往往不来自模型结构而是来自训练细节损失函数的配比、学习率调度、数据增强范围、评价指标的类型。这一部分我按参数逐一展开给出能直接复制的经验值。4.1 损失函数权重BCE 与 Dice 的三种配比先给出基准配比和微调方向。BCE:Dice 0.5:0.5 是大多数情况下的起点。如果分割对象是大器官肝脏、肾脏、脾脏Dice 对整体重叠率更敏感可以把权重调成 Dice:BCE 1.0:0.5。如果对象是小而细的结构血管、胆管Dice 梯度在前景区域不稳定需要把 BCE 拉上来稳定边界配比调成 BCE:Dice 0.7:0.3。这个调节没有绝对的“最优”核心是理解Dice 的梯度信号与目标总面积强相关目标越小梯度越弱越需要 BCE 的像素级信号来兜底。配比变化对训练的最终影响可以这样验证每种配比训练 30 个 epoch对比验证集 Dice而不是看训练 Loss 的收敛速度。Loss 低不代表分割好在类别不平衡的任务里尤其如此。4.2 优化器为什么资深的医学分割复现都爱用 AdamW训练医学分割模型我不用纯 Adam而用 AdamW。AdamW 把权重衰减从梯度更新的计算里解耦出来本质上就是修掉了 Adam 做 L2 正则化时的一个数学缺陷。对于小数据、小 batch 的医学分割场景用 AdamW 配合较小的 weight_decay对过拟合的抑制更有效。学习率用 1e-4 而不是自然图像常见的 1e-3这是因为医学影像任务里 batch_size 通常很小4 或 2梯度噪声大大步长直接震荡。配合 ReduceLROnPlateau配置如下import torch.optim as optim from torch.optim.lr_scheduler import ReduceLROnPlateau optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler ReduceLROnPlateau(optimizer, modemax, factor0.5, patience10, min_lr1e-6) # 每个epoch结束后用验证集Dice作为监控指标 # scheduler.step(val_dice)这里的modemax是因为我们监控的是验证 Dice越大越好patience10表示连续 10 个 epoch 没有刷新最好 Dice 就把学习率降一半min_lr1e-6是下限防止学习率降成负值。如果用的是 BraTS 这种单 epoch 就要跑半小时以上的 3D 数据patience 建议提到 15 以上否则模型还在缓慢爬坡就被降学习率打断2D 切片任务一个 epoch 只要几分钟patience 在 8 到 10 就够。关于 batch_size我有一条检查清单显存允许范围内尽量往大了设但不要为了凑 batch_size 暴力缩小输入分辨率。512 分辨率的图配 batch4效果通常优于 256 分辨率配 batch16——空间细节信息对分割的影响远大于一次迭代里看到的样本数量。如果显存实在不够先把初始通道数从 64 改成 32而不是直接砍输入尺寸。4.3 数据增强弹性形变的三个参数调过了就是负优化医学分割与自然图像分割在数据增强上有显著区别。自然图像常用的大幅随机裁剪、色彩抖动、CutOut 这些在医学影像里多数不能直接用。大幅旋转会破坏解剖学方向色彩抖动对灰度医学影像没有意义。真正起决定性作用的是随机弹性形变。它通过一个平滑的随机位移场把图像扭曲模拟人体器官在扫描时的姿态差异。albumentations 的ElasticTransform参数有三个alpha 控制形变强度sigma 控制位移场平滑程度alpha_affine 控制仿射分量。经验值为 alpha2、sigma10、alpha_affine5。alpha 调到 5 以上器官会扭曲到解剖学上不存在的形状模型学出来的“鲁棒性”其实是假的。旋转范围应限制在 10 度以内缩放取 0.9 到 1.1 之间水平翻转对大多数器官是安全的但对左右结构有明显不对称的组织要谨慎使用。4.4 评价指标Dice 不够HD95 才是边界质量的照妖镜训练过程不能只盯着 Dice 看。Dice 反映区域重叠率但两个模型 Dice 同样为 0.90 时一个边界误差 2mm一个 8mmDice 根本区分不出来。在医学场景下边界质量直接关系到病灶定位的可靠性。业界标准做法是加一个 HD95 指标即 Hausdorff Distance 的第 95 百分位。它衡量两组点集之间的最大距离第 95 百分位是为了过滤极端离群点。验证脚本里同时输出 Dice、IoU、HD95 三张表才能完整判断一个模型的真实水平。HD95 的实现可以用 scipy 的 cKDTree我常用的版本是from scipy.spatial import cKDTree import numpy as np def hd95(pred_mask, gt_mask, spacing(1.0, 1.0)): pred_pts np.argwhere(pred_mask 0.5).astype(np.float32) * spacing gt_pts np.argwhere(gt_mask 0.5).astype(np.float32) * spacing if len(pred_pts) 0 or len(gt_pts) 0: return float(inf) tree1 cKDTree(pred_pts) tree2 cKDTree(gt_pts) d1, _ tree1.query(gt_pts, k1) d2, _ tree2.query(pred_pts, k1) return max(np.percentile(d1, 95), np.percentile(d2, 95))spacing 参数要传真实体素间距用加载数据时读到的 spacing 换算到切片平面如果不传默认是(1.0, 1.0)算出来的距离只是像素单位而不是毫米跟论文里的报告值就对不上了。这个函数的计算复杂度在点数大时会偏高验证集每个 epoch 都全量算一次 HD95 可能很慢。实操时一般只在每个 epoch 结束时对验证集做一次随机采样比如抽 20 个病例计算 HD95。5. 复现与避坑五个让新手翻车的典型问题及排查我再来写避坑章节。下面这些坑都来自真实复现“源码数据集”项目的常见现场按现象、原因、解决的顺序写每一条都配合具体的定位和修复操作。5.1 坑一训练 Loss 正常下降但验证集 Dice 一直是 0现象训练损失曲线在下降每次验证时打印的 Dice 全是 0.0000。原因prediction 和 target 的维度或语义不匹配。最常见的是 pred 输出的 logits shape 是(batch, n_classes, h, w)而 target 是单通道索引标签 shape 是(batch, h, w)当n_classes2时交叉熵把 target 当成类别索引去取实际上取到的是背景这一类的概率Dice 自然算出来是 0。还有一种情形是 Dice 损失函数里没有对 pred 做 sigmoid模型还没看到正区域Dice 公式恒为 0。解决在损失函数入口临时print(pred.shape, target.shape)逐维对清楚。交叉熵类别数大于 2 时把 target 保持为(batch, h, w)的整数索引用 Dice 时把 target 转成 one-hot(batch, n_classes, h, w)。最稳妥的做法是写一个训练循环的第一轮固定输入为单 batch手动逐步执行 forward、loss、backward把变量 shape 都打印出来跑通后再换成 DataLoader。5.2 坑二显存溢出OOM 在第二个 epoch 准时出现现象显存看起来够大第一个 epoch 跑完了第二个 epoch 一开就报 CUDA out of memory。原因最常见的是 batch_size 和输入尺寸的组合超了。512x512 的输入、batch8、初始通道 64这是最容易爆的配置。初始通道 64 的 U-Net 在 512 分辨率下显存占用非常高。还有一些实现会在 forward 里保存所有中间激活值用于反向传播解码器的 Concat 又会让通道数翻倍显存直接翻倍。解决按顺序试三个降显存手段。第一个是 batch_size 减半第二个是把初始通道从 64 改到 32通常只损失零点几个 Dice 点显存节省接近一半第三个是把输入 resize 到 384x384 而不是硬扛 512。如果这三个都试完还是爆检查数据加载环节确认训练时有没有把整卷三维数据都转成 tensor 放进显存这是排查 OOM 时容易漏掉的一点数据加载应该留在 CPU 侧GPU 只接收单批张量。5.3 坑三验证 Dice 剧烈振荡模型似乎不收敛现象训练到第 10 个 epoch 后验证 Dice 还在 0.2 到 0.7 之间来回跳上下幅度超过 0.3。原因两种典型因素一是学习率太大二是数据切分泄漏了。数据切分的泄漏很多人会低估如果按默认的 train_test_split 直接把所有切片随机分没有按患者 ID 分 fold同一个患者的相邻切片会同时出现在训练集和验证集里验证 Dice 虚高且不稳定。换一个完全不同的人做测试模型可能只在某个患者身上学了很多真实泛化场景下直接露馅。解决先按患者为单位分组用 GroupKFold 切分保证同一个患者的所有切片都只落在一侧。再把学习率从 1e-4 降到 3e-5看振荡是否收窄。如果振荡还在第三个检查点是数据增强力度过大比如弹性形变的 alpha 设到 5 以上每个 epoch 看到的样本差异太大验证集里的表现自然不稳定把 alpha 降回 2。这三步做完Dice 振荡幅度一般能压到 0.05 以内。5.4 坑四推理 mask 出现棋盘格伪影现象预测的分割 mask 边缘有规律的方块状格子像马赛克一样。原因上采样层反卷积的卷积核与 stride 组合不当。经典组合是ConvTranspose2d(2, stride2)上采样因子正好是 2。如果kernel_size3, stride2反卷积的滑窗会产生重叠和间隙交替格子就出现了。还有一种更隐蔽的来源是 pixel_shuffle 配合不正确的 block 排列。解决直接把所有上采样改成nn.Upsample(scale_factor2, modebilinear, align_cornersFalse)加 3x3 卷积的组合。这个组合显存占用略高但不会产生棋盘伪影而且更容易训练。如果想保留反卷积检查每个解码器层输出的空间尺寸是否与跳跃连接对齐尤其是处理不规则尺寸输入时卷积 padding 设置算错也会在 cat 时悄悄改掉尺寸导致输出特征错位。5.5 坑五GPU 利用率才 30%训练一个 epoch 要一小时现象nvidia-smi 显示 GPU 占用率在 20% 到 40% 徘徊显存占用正常但速度很慢。原因数据预处理在 CPU 端成为瓶颈。如果窗宽裁剪、resize、归一化、翻转这些操作都在 Dataset 的__getitem__里每次现算CPU 会疯狂计算而 GPU 只能闲等。更差的情况是num_workers0主进程串行加载多张小图训练循环直接被拖死。解决把能离线算的预处理全部提前算好存成 npy 或 h5py 文件。训练时 Dataset 只做读取、裁剪到固定尺寸、随机翻转最多再加弹性形变这个没法离线做。DataLoader 配置num_workers8、pin_memoryTrue、persistent_workersTrue。前两个很常规persistent_workersTrue是 PyTorch 1.9 以后加入的避免每个 epoch 结束 worker 被重启适合医学数据这种加载成本不低的场景。改完后一个 epoch 的时间至少能压缩一半。6. 进阶实战用 TTA 和连通域清理把 Dice 再提一档单模型跑稳定后真正想从“项目能跑”走到“高分项目”靠的是推理阶段的后处理与集成。我最常用的两个技巧是 TTA 和连通域清理成本极低收益稳定。TTA测试时增强的做法是推理时把输入图像做变换把多次预测结果融合。医学 2D 切片一般用水平翻转就够了因为人体解剖大致左右对称。实现很短但有一个容易错的点翻转后的预测要翻回来再取平均否则空间位置全反了。示例如下def tta_predict(model, img_tensor): model.eval() with torch.no_grad(): pred torch.sigmoid(model(img_tensor)) flip_img torch.flip(img_tensor, dims[3]) flip_pred torch.sigmoid(model(flip_img)) flip_pred torch.flip(flip_pred, dims[3]) pred (pred flip_pred) / 2.0 return preddims[3]是在宽度维度翻转。TTA 一次推理多一倍耗时但 Dice 通常能涨 0.5 到 1.5 个点在真实高分评比中是划算的买卖。连通域清理解决的是零散假阳性模型常把背景里几个像素误判为目标从而拉低 Dice。二分类分割的 mask 里真正的目标器官通常是一个连通的大区域保留最大的连通域即可。实现时用scipy.ndimage.label但要注意如果目标在医学上有多个连通块比如双肾、多发肺结节只保留最大一块反而会删掉真阳性要加一个面积阈值而不是硬取最大。我自己的做法是对单器官分割保留最大连通域对多病灶分割保留面积大于某个像素数的连通域。最后一点是模型集成。两个不同随机种子训练出来的 U-Net推理时把概率图平均通常比单个模型稳定不少。三模型集成的边际收益开始递减所以一般集成两个就够了。集成时需要在同一个预处理和重采样参数下推理否则概率图对齐不上。我踩过最明显的坑是一次做三模型集成时两个模型用的重采样间距一个是 1mm 一个是 2mm输出概率图一叠加边界全错位了Dice 不升反降。从那以后我养成一个习惯训练和推理的所有预处理参数写成一个单独的配置文件任何实验变更都同步更新配置而不是散落在训练脚本里。整套流程走下来你会理解所谓“高分项目”赢在数据流水线和管理纪律上的部分比赢在模型结构上的部分多得多。希望这些经验能帮你在自己复现时少走几步弯路。本文还有配套的精品资源点击获取
返回列表