YAOTU INSIGHTS

肾脏肿瘤语义分割实战:2800张数据集全流程指南

肾脏肿瘤语义分割实战:2800张数据集全流程指南
简介面向医学图像分割研究者的肾脏肿瘤语义分割数据集包含约2800张已划分的图片与标签覆盖背景、肾脏、肿瘤三类分割目标像素级标签信息清晰省去自行整理标注的繁琐步骤。训练集约2000张验证集约800张可直接用于深度学习模型的训练与评估适合开展肾脏肿瘤分割实验、医学图像分析课程设计或作为论文基准数据集。资源包共2000个文件以PNG格式图像和mask为主另附类别说明文档txt和可视化脚本py压缩包整体约88.6MB文件目录结构清楚便于直接加载与二次处理。可视化脚本支持随机提取一张原始图像并生成原图、GT以及GT叠加蒙版对比图方便快速核查标注效果。该数据集已有175人学习下载适合正在研究U-Net、SwinUNet、TransUNet等医学图像分割网络的开发者直接使用配套官方专栏还可了解网络改进思路便于进一步拓展实验。1. 肾脏肿瘤语义分割数据集2800张图能撑起一个什么样的医学AI起点医院里让医生圈出CT上的肾脏肿瘤新手通常会拉一个矩形框但临床真正要的是肿瘤的精确边界——它长在肾皮质还是肾盂旁、直径多少、边缘是否光滑这决定了手术方式。这套肾脏肿瘤语义分割数据集约2800张数据和标签就是为这个任务准备的原始CT切片和逐像素的语义标签成对出现标签标出背景、肾脏组织和肿瘤区域。2800张放在自然图像任务里不算多但医学图像分割本身就是高标注成本的小样本场景这个规模足够你把一条语义分割流水线完整跑通还能把数据划分、预处理、损失函数和评估指标这些基本功练扎实。适合刚转医学影像的算法工程师、相关方向的研究生也适合那些想把手头检测框架迁到像素级任务的团队。2. 从医学图像分割到肾脏肿瘤数据集为什么2800张图值得用语义分割来处理2.1 语义分割 vs. 目标检测肿瘤边界不是框能解决的语义分割算法最早被FCN带火后来UNet在医学图像分割这个方向上统治了很多年。它的核心是逐像素分类输入一张CT切片输出一张同尺寸的概率图每个像素都被贴上“背景”“肾脏”或“肿瘤”的类别标签。这和目标检测有本质区别检测输出的是包围框只能给出位置和大致范围。肾脏肿瘤在CT上的形态非常不规则有的分叶状向外突出有的内生型肿瘤几乎不改变肾脏轮廓还有一部分肿瘤和正常肾实质密度接近视觉上边界就是模糊的。我见过一个团队直接拿yolov8训练自己的数据集去做肾肿瘤定位然后从框里裁出区域再二次分类。yolo做目标检测没问题但它的损失函数回归的是框参数不是像素边界分叶肿瘤的边缘会被框的“最小外接矩形”抹掉内生性肿瘤甚至可能因为框的面积被正常组织主导而漏检。医学这边真正的问题是肿瘤体积多大、侵犯到哪一层这些信息框给不了你只有像素级标签给得了。从FCN到UNet最大的改进是跳连接。编码器一路下采样感受野变大但边缘细节丢失解码器上采样后把分辨率拉回来再用跳连接把编码器里的边缘纹理拼上去边界精度明显提升。医学图像分割数据集通常只有几百到几千张UNet这种局部卷积加多尺度融合的结构比视觉Transformer更容易在小数据上收敛。现在当然有TransUNet、nnUNet这些变体但我给新手的建议始终是第一个落地方案从UNet开始它结构简单、训练稳定、标签错配时也更容易排查。2.2 数据与标签图像、掩码和类别编号拿到这套约2800张的数据先别急着训练。第一步是搞清楚图像和标签到底是什么格式。常见情况是原始图像为CT轴向切片分辨率512x512左右灰度值不是普通RGB而是HUHounsfield Unit值。标签则是一张和原图像素一一对应的掩码图每个像素存一个整数类别ID常见定义为0背景、1肾脏、2肿瘤有的数据集只区分0背景、1肿瘤。这两个定义在损失函数里完全不同因为类别数量影响模型输出通道数。一个很容易翻车的地方是标签文件常以彩色PNG形式存储。医生看可视化时往往把肿瘤标红、肾脏标绿然后程序员图省事直接把这种伪彩色图存成了调色板PNG。调色板PNG在PIL里读出来是mode‘P’每个像素存的是调色板索引不是RGB可如果你用cv2.imread去读它默认转成BGR三通道类别就从几个整数炸成三个通道的像素组合网络输出通道怎么设都对不上。我拿到陌生医学数据集的第一个动作永远是做唯一值检查import numpy as np from PIL import Image mask_path labels/001.png mask np.array(Image.open(mask_path)) print(mask.dtype, mask.shape, np.unique(mask)) # 期望输出: dtypeuint8, shape(512, 512), unique[0 1 2] # 如果shape是(512, 512, 3)说明读成了RGB需要转灰度或按调色板映射 # 如果unique里出现255说明背景用的是255而不是0后续要重映射这里的关键是优先用PIL而不是cv2读医学标签因为cv2对调色板PNG的默认通道转换会直接破坏索引值。np.unique告诉你实际类别数如果出现预期以外的值先查数据集自带的说明文档没有文档就自己做一个像素值到类别名的映射表这一步偷懒后面全得还。除了通道问题还要确认图像和标签是否对齐。有的数据流水线里图像做过翻转或转置标签却没做同样操作肉眼可能看不出来训练时Dice就卡在某个值上不去。我一般会写一个快速可视化脚本把原图灰度和标签半透明叠加显示import matplotlib.pyplot as plt img np.array(Image.open(images/001.png)) mask np.array(Image.open(labels/001.png)) assert img.shape[:2] mask.shape[:2], image and mask shape mismatch plt.imshow(img, cmapgray) plt.imshow(mask, cmapjet, alpha0.4, vmin0, vmax2) plt.show()assert不是摆设它能在训练前暴露形状不匹配半透明叠加则能人工核对肿瘤边界和CT影像上的低密度区是否吻合。如果发现标签整体偏向某个方向多半是原始DICOM的坐标轴和图像保存顺序不一致。2.3 2800张够不够医学小数据的样本边界先说结论2800张2D切片对于训练一个中小型UNet是够用的前提是数据多样性够。如果这2800张来自60个病人每个病人约40-50张切片模型可以学到不同体型、不同增强扫描期相下的肿瘤特征如果来自10个病人那么模型只是在背诵这10个人的解剖结构验证集表现再好也不能说明泛化能力。收到数据后我会先看文件命名。常见命名是“病人编号_切片序号”例如001_023.png代表第1个病人的第23张切片。看到这类命名就应该立刻想到划分数据时要按病人编号分而不是按文件随机分。CT相邻切片的形态变化很小同一病人的第20张和第21张几乎像同一张图如果随机划分训练集和验证集里会出现大量“近亲”样本指标虚高到没有参考价值。另一个需要统计的是肿瘤区域的像素占比。肾脏肿瘤通常只占整张切片的1%到5%背景占绝大比例。如果模型全预测背景像素准确率也能到95%以上但显然是一张废掉的图。所以后面训练和评估都不能看准确率要看按类别计算的Dice或IoU。肿瘤占比还直接影响损失函数设计类别不平衡严重时需要在损失里加权重或者直接用Dice Loss。医学数据还有一点和遥感图像语义分割很不一样。遥感影像也是像素级分类但类别多、目标尺度变化大常用随机裁剪大块来增强医学CT灰度有物理含义HU值代表组织密度裁剪尺寸和灰度窗口都不能照搬自然图像那套。不同医院的扫描协议也会带来域偏移同一模型换一个数据源Dice可能掉十几个点。这也是为什么2800张数据真正考验的不是网络有多深而是你对数据分布的理解有多准。2.4 标签格式统一从PNG/NIfTI到npy数组不同来源的数据集标签格式参差不齐训练前最好统一成npy或numpy数组省得每个epoch都在处理格式。以PNG为例统一流程是先读入再处理类别值最后存成稠密掩码数组。用代码可以一次搞定def load_mask(mask_path): mask np.array(Image.open(mask_path)) if mask.ndim 3: # 假设是调色板索引被误读为RGB这里取第一个通道 mask mask[..., 0] # 把可能的255背景重映射为0 mask[mask 255] 0 return mask.astype(np.int64)如果数据是NIfTI格式则要用nibabel.load读取注意其数组轴序是(z, y, x)而我们在PyTorch里习惯(B, C, H, W)。读取后先np.transpose成(x, y, z)或直接切片训练不要用原始轴序直接训练否则图像会旋转90度。统一成npy后建议再做一次全量唯一值检查确保没有漏网的类别。这样第二章的落点就清楚了先搞懂语义分割为什么匹配这个任务再摸清标签格式和样本分布最后把数据清洗成统一形式。2800张听起来不多但干净程度决定了后续所有训练环节的上限。3. 把2800张肾脏肿瘤数据跑进语义分割模型从预处理到训练的完整闭环3.1 数据集划分按病人ID切分而不是按文件随机切数据划分是整个流程里最容易被低估的一步。很多人写代码时习惯用random.shuffle然后把文件名按比例切三份这在医学图像分割数据集上是个雷。相邻CT切片之间高度相似同一个病人的几十张图如果被拆进训练和验证两个集合验证集就不是“没见过”的数据Dice会虚高到失真。我常用的划分方式是把文件名里的病人ID提取出来先按病人分组再以病人为单位划分训练、验证和测试集。代码结构如下import os import random from collections import defaultdict image_dir images mask_dir masks patient_to_files defaultdict(list) for fname in os.listdir(image_dir): patient_id fname.split(_)[0] # 例如 001_023.png - 001 patient_to_files[patient_id].append(fname) patients sorted(patient_to_files.keys()) random.seed(42) random.shuffle(patients) n_train int(len(patients) * 0.7) n_val int(len(patients) * 0.15) train_patients set(patients[:n_train]) val_patients set(patients[n_train:n_train n_val]) test_patients set(patients[n_train n_val:]) train_files [] for p in train_patients: train_files.extend(patient_to_files[p]) # 同理生成 val_files, test_files print(ftrain patients: {len(train_patients)}, train slices: {len(train_files)})这段代码的核心是按patient_id分组而不是直接对文件列表切分。random.seed(42)保证结果可复现7:1.5:1.5的比例在两百例规模下通常够用。如果数据集中只有一个病人那只能退化为切片级划分但要在实验记录里明确标注“切片级划分测试集与训练集存在相关性”否则论文或汇报里很容易被审稿人质疑。3.2 预处理管线窗宽窗位、归一化与数据增强CT图像的HU值范围很大从-1000的空气到1000以上的骨骼如果不做窗宽窗位处理直接归一化肿瘤和肾脏在0到255的线性映射里会被压成一段几乎不可分的灰度。肾脏常规扫描常用的窗宽大约300-400HU窗位40HU左右所以常见做法是把灰度值裁剪到[-50, 150]再归一化到[0,1]。def preprocess_ct(image, window_min-50, window_max150): # 图片来源: 原始CT切片值为HU image np.clip(image, window_min, window_max) # 线性拉伸到 [0, 1] image (image - window_min) / (window_max - window_min) return image.astype(np.float32)np.clip把窗外的灰度截断窗内的组织对比度保留归一化后再输入网络数值稳定有利于BatchNorm收敛。窗口参数需要根据数据集的扫描协议微调如果发现肾皮质和肿瘤灰度重叠严重可以在验证集上人工看几张直方图再定。数据增强方面医学分割的增强必须保证图像和标签做同样的几何变换。我常用的组合是随机水平翻转、随机旋转10度以内、随机缩放0.9到1.1再加上一点亮度扰动。弹性形变在肿瘤分割里有效但需谨慎控制形变幅度否则会把边界拉得不像真实解剖结构。这里不引入第三方增强库用PyTorch的torchvision.transforms也能组合import torchvision.transforms as T from PIL import Image train_transform T.Compose([ T.RandomHorizontalFlip(p0.5), T.RandomRotation(degrees10, fill0, interpolationT.InterpolationMode.NEAREST), ])注意RandomRotation对mask要使用NEAREST插值否则类别边界会出现插值产生的中间值比如0.5这种不存在的类别ID对图像使用BILINEAR保持灰度平滑。翻转和旋转这类几何变换自动同步应用在img和mask上但需要分别创建transforms调用同一组随机参数或者自己实现一个SameTransform否则图像和mask可能用了不同随机种子。3.3 用UNet训练一个能落地的PyTorch脚本这里给一个不依赖外部医学库的训练骨架重点是让模型能先跑起来网络结构用最简单的UNet变体。假设数据集目录下是images/和masks/每张图读进来经过预处理后变成(1, H, W)的输入和(1, H, W)的标签。import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader class KidneyDataset(Dataset): def __init__(self, file_list, image_dir, mask_dir, window(-50, 150)): self.file_list file_list self.image_dir image_dir self.mask_dir mask_dir self.window window def __len__(self): return len(self.file_list) def __getitem__(self, idx): fname self.file_list[idx] img np.array(Image.open(f{self.image_dir}/{fname})).astype(np.float32) mask np.array(Image.open(f{self.mask_dir}/{fname.replace(_img, _mask)})).astype(np.int64) # 如果有通道维度则去掉 if img.ndim 3: img img[..., 0] img np.clip(img, *self.window) img (img - self.window[0]) / (self.window[1] - self.window[0]) # 转成 torch tensor形状 (C, H, W) img_tensor torch.from_numpy(img).float().unsqueeze(0) mask_tensor torch.from_numpy(mask).long().unsqueeze(0) return img_tensor, mask_tensor dataset KidneyDataset(train_files, images, masks) loader DataLoader(dataset, batch_size4, shuffleTrue, num_workers4)这里标签用long()类型是因为PyTorch的交叉熵损失要求类别索引是整数。unsqueeze(0)把H×W变成1×H×W对应单通道灰度输入。如果你的图像本身是彩色或三通道序列最后unsqueeze的维度要相应调整。训练循环可以用一个最简写法。损失函数用CrossEntropyLoss加类别权重类别权重根据训练集中背景、肾脏、肿瘤的像素占比算出来比如[0.1, 0.5, 2.0]让模型更关注肿瘤类。这个权重不是随便拍的是按“总像素数/类别像素数×类别数”做归一化后得到的。from torch.utils.data import DataLoader import torch.optim as optim model UNet(in_channels1, num_classes3) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) class_weights torch.tensor([0.1, 0.5, 2.0], dtypetorch.float32, devicedevice) criterion nn.CrossEntropyLoss(weightclass_weights) optimizer optim.Adam(model.parameters(), lr1e-4) for epoch in range(100): model.train() for img, mask in loader: img img.to(device) mask mask.squeeze(1).to(device) # (B, H, W) pred model(img) # (B, 3, H, W) loss criterion(pred, mask) optimizer.zero_grad() loss.backward() optimizer.step() print(fepoch {epoch} loss {loss.item():.4f})mask.squeeze(1)去掉通道维度因为CrossEntropyLoss期望的target形状是(B, H, W)而不是(B, 1, H, W)。如果忘记squeezePyTorch不会立刻报错但会把target维度当成类别维度来广播损失曲线会非常奇怪。3.4 损失函数与训练参数Dice Loss还是CrossEntropy3.3里用的是加权交叉熵它对类别不平衡有改善但对边缘分割任务有个天然问题它孤立看待每个像素没有直接优化区域重叠度。Dice Loss则是直接在类别区域上计算重叠对肿瘤这类小目标更友好。常见做法是把两者结合总损失等于0.5倍交叉熵加0.5倍Dice Loss。一个不容易踩到的参数是学习率。医学小数据集上Adam配合1e-4通常稳定换成SGD的话一般需要0.01到0.1还要配合动量。Batch size方面512×512的输入显存允许的情况下取4到8如果显存只有8G可以把输入resize到256×256batch size取8到16。UNet的batch size不需要特别大因为BatchNorm在batch很小的时候统计量不稳定至少保证一个batch里有肿瘤样本。训练轮数我一般设100到150同时配合“验证集Dice连续10轮不提升就保存当前模型”的早停。数据增强把训练分布拓宽验证集只用原始图片不做增强。这样得到的模型在测试集上的指标才是真实水平而不是和数据增强玩出来的拟合。4. 评估肾脏肿瘤分割结果mIoU、Dice还有Hausdorff距离4.1 三个核心指标Dice、IoU、Hausdorff距离训练完不能只看loss曲线要真正评估分割质量。医学图像分割公认的基础指标是Dice系数、IoUJaccard和Hausdorff距离。Dice和IoU都在度量区域重叠程度但侧重点略有不同。指标公式关注点适合场景Dice2|A∩B| / (|A||B|)重叠相对面积对小目标更敏感肿瘤这种小区域对比IoU|A∩B| / |A∪B|交并比更严格多类别语义分割通用Hausdorff距离max(min(distance))边界最大偏差边界不规则、手术规划表格里Dice和IoU只差一个倍率关系但Dice在小目标上显得更乐观。比如肿瘤只占图1%时预测结果稍微偏一点Dice可能还有0.8IoU已经掉到0.6说明重叠质量并不好。Hausdorff距离计算预测边界到真实边界的最大距离单位是像素或毫米它直接反映“边界最差的地方差多远”这正是临床最关心的——一个5毫米的边界偏差可能决定保肾还是全切。4.2 一个可跑的评估脚本逐类统计Dice、IoU与类别像素占比评估时不能只看肿瘤类背景类毫无意义但占比极大所以一定要逐类算指标然后汇总。下面这个脚本读入预测和真值统计每个类别的Dice和IoUimport numpy as np def dice_iou_per_class(pred, gt, num_classes3, eps1e-6): pred: (H, W) 每个像素为类别ID gt: (H, W) 每个像素为类别ID 返回每个类别的 dice 和 iou 列表 dice_list [] iou_list [] for cls in range(num_classes): pred_mask (pred cls) gt_mask (gt cls) intersection (pred_mask gt_mask).sum() pred_sum pred_mask.sum() gt_sum gt_mask.sum() dice (2.0 * intersection eps) / (pred_sum gt_sum eps) union pred_sum gt_sum - intersection iou (intersection eps) / (union eps) dice_list.append(dice) iou_list.append(iou) return dice_list, iou_listeps防止除零尤其在某个类别完全不存在时。实际评价时要同时输出每个类别的像素占比因为如果一个类别在验证集里只有几十个像素Dice稍微变动0.1不代表模型真实变化。for cls in range(num_classes): print(fclass {cls}: dice{dice_list[cls]:.4f}, iou{iou_list[cls]:.4f})如果发现肿瘤类Dice不错但Hausdorff距离很大说明整体重叠还行但最外圈某个局部突出或凹陷严重这种问题在重叠指标里很难暴露必须额外看边界差。4.3 小肿瘤、边界模糊和“看起来不错”的假象肾脏肿瘤分割最容易出现的假象是整个验证集Dice很高但单独看小肿瘤切片时Dice只有0.3。原因是大肿瘤占的像素多主导了平均指标。所以一个合格的评估报告必须按肿瘤大小分层肿瘤像素小于500、500到5000、大于5000三类分别计算Dice。直径小于1厘米的肿瘤在CT上可能只有几十个像素模型预测稍微偏移一点Dice就崩。边界模糊同样会干扰指标。靠近肾门的肿瘤和肾盂、血管灰度接近医生标注时本身也有主观性不同标注者之间Dice可能只有0.85左右。如果模型在这个区域达到0.82已经很接近标注一致性上限不要盲目追求0.95。我在项目里会留一个肿瘤中心到边界的平均距离指标用来判断分割结果是不是整体偏向某个方向。还有一种“看起来不错”的情况是模型把肾囊肿也当成了肿瘤。囊肿边界清晰、无强化但CT平扫上和部分低密度肿瘤很相似。如果你只算肿瘤Dice可能因为这类假阳性没有和真值重叠而自动惩罚惩罚幅度却不够大因为假阳性面积相对整个背景占比太小。这种情况需要用临床相关的召回率来衡量在真值肿瘤区域里预测了多少而不是只看全局Dice。评估环节必须保存几个可视化样例至少包括一张边界好、一张边界差、一张假阳性的图这样后续调参才有对照。我一般把预测边缘叠加在原图上用红色描真值边界绿色描预测边界人工一眼就能看出问题出在轮廓还是区域。5. 避坑与排查肾脏肿瘤语义分割数据集的5个血泪教训5.1 现象loss不降、预测全黑原因标签被读成了RGB三通道第一次跑肾脏肿瘤数据集时我发现训练loss在第一个epoch后降了一点就再也不动验证集预测结果全黑。后来打印np.unique(mask)才发现标签shape是(512, 512, 3)因为读取时用了OpenCV的默认BGR模式把本来就应该是索引值的PNG读成了三通道彩色图。模型把三通道当成一个“类别”输出概率自然乱掉。解决办法是统一用PIL读取mask并强制转成单通道同时检查mode。如果数据集的标签是调色板PNG可以用PIL.Image.open(mask_path).convert(L)转灰度再把非零像素映射回正确的类别ID。这类问题只能在数据加载函数里加断言比如assert mask.ndim 2让训练在第一时间报错而不是悄悄演下去。5.2 现象肿瘤区域Dice一直在0.5上下原因类别不平衡且未加权背景像素占整张图95%以上肾脏还占一部分肿瘤往往只有几千像素。如果不处理类别不平衡损失函数会被背景类主导模型即使把肿瘤整团漏掉总损失也几乎不变。我的验证集Dice从0.5爬到0.55就停住肉眼检查发现预测的肿瘤边界比真值小一圈。后来改成加权交叉熵权重按“类别像素占比的倒数”归一化肿瘤权重约10肾脏权重约3背景权重约0.1。这一步让模型开始关注小目标。更稳健的做法是直接把Dice Loss加入总损失因为Dice是区域级度量对像素占比不敏感。两条路可以同时用让交叉熵提供稳定梯度Dice Loss专注重叠度。5.3 现象预处理窗口设错导致肿瘤和正常肾分不开有一版模型在验证集上肿瘤Dice只有0.2我以为是网络结构问题。后来把训练样本里的图像灰度直方图拉出来发现原始CT值范围是-1024到3071而我用0到255做了线性归一化导致实际肿瘤和肾都落在灰度值80到90之间对比度极低。模型看到的分辨率远低于标签能提供的信息自然学不到边界。解决办法是回到HU语义先把图像裁剪到窗宽窗位范围内再做线性拉伸。对肾脏肿瘤我通常把窗口设在-50到150。如果这个数据集的肿瘤灰度更低可以统计肿瘤区域真值掩码下的像素均值来反向调窗口。记得在预处理前后各存一张可视化对比图灰度分布对不上就是窗口选错了。5.4 现象验证集Dice高达0.95测试集掉到0.5原因数据泄漏第一次做这个项目时我直接用文件名随机划分数据集得到的验证Dice令人振奋。最后上测试集发现相差巨大我的第一反应是过拟合但查来查去发现验证集里混进了同一患者的相邻切片。同一个患者相邻两张CT在解剖结构上几乎一模一样模型等于把“答案”背下来了。后果就是所有验证指标失去意义。后来我改成按patient_id划分验证集只包含训练阶段完全没有见过的患者指标立刻掉到真实水平但也终于能反映泛化表现。现在我会在划分代码里加一层检查打印验证集和训练集的patient_id交集如果有交叉就报错。5.5 现象加载数据和标签时维度对不上训练直接崩溃原因图像是(x, y)顺序mask是(y, x)顺序另一个让我排查了半天的坑是图像数组和mask数组的维度方向不一致。DICOM转PNG时如果原图以(列, 行)顺序输出而mask保存时用了(行, 列)图像与标签就会呈转置关系。肉眼单看一张图不明显叠加可视化时才看到边界错位90度。解决方式是在数据加载函数里用assert img.shape mask.shape如果不相等就对其中一个做转置。医学图像最怕的就是“看着像没问题但方向错”。我在项目里会额外做一次自动旋转测试把一张已知的mask旋转90度后和原mask计算Dice如果Dice很低说明方向处理有问题尽早暴露。6. 在2800张样本上再往前一步K折交叉验证、伪标签与测试时增强6.1 按患者做K折交叉验证让小数据得到更可信的指标2800张切片看起来不少但真正的独立样本是患者数量不是切片数量。如果患者数量小于50单次7:1.5:1.5的划分会引入较大的随机性。我习惯在这个规模上做5折按患者交叉验证每一折用不同的患者做测试最终报告5折Dice的均值和标准差。均值反映模型水平标准差反映数据差异敏感性。同一套数据单次划分Dice可能跳5个点交叉验证得出的结论才敢写进汇报。具体做法是把第三章的划分循环5次记录每一折在测试患者上的Dice和Hausdorff距离最后汇总。代价是训练时间变为5倍但2800张图规模下的UNet训练每折几十个epoch现代显卡上完全可以接受。6.2 测试时增强TTA推理时不增加训练成本的白嫖增益TTA在医学分割里的常见实现是推理时将输入做水平翻转得到两个预测概率图平均后再取argmax。肾脏肿瘤边界左右不对称的情况翻转平均能抑制单一方向的偏移。另一个有用的TTA是轻微旋转比如±10度但计算量会翻几倍我在肿瘤数据集上一般只做翻转一次TTA大概能提升1-2个Dice点同时提升边界稳定性。6.3 伪标签半监督用测试数据反哺模型当标注的2800张不够时可以尝试伪标签半监督。做法是用当前模型对一批无标签的肾脏CT切片做预测只把置信度高的区域作为伪标签加入训练集。肿瘤区域往往置信度不高所以这个方向需要谨慎但我见过有团队用明显的大肿瘤预测结果做伪标签让模型先学会大肿瘤再慢慢往小肿瘤迁移。如果你只有2800张半监督不一定能带来质变不过值得当作后期优化手段。我在这个数据集上的最后一条教训是永远把“按患者划分”和“多折评估”当作默认操作而不是某次实验的特殊处理数据量越小评估方法越要保守。现在无论多着急出结果我拿到数据的第一天就会把患者ID统计清楚哪怕这个动作要花掉半天时间也比训练两周后发现验证集虚高值得多。肾脏肿瘤分割的边界问题没有银弹但把数据、预处理和评估这三个地基打牢2800张图也足够做出可信的落地模型。希望帮到你。本文还有配套的精品资源点击获取