
简介这是一份面向医学图像分割研究与实验的胸部X光片语义分割数据集核心任务是气胸Pneumothorax区域的像素级标注与识别适用于深度学习研究者、医学影像算法开发者以及相关专业学生开展模型训练、算法对比和课程实践。气胸是胸部X光片中常见的急症表现准确分割对辅助临床诊断具有实际意义。压缩包共2000个文件以1998张PNG图像为主包含原始影像与对应的分割标签另提供类别定义txt文件和Python辅助脚本整体大小183.48MB数据已经过对比度拉伸、resize等图像增广处理并明确划分为训练集和验证集可直接用于U-Net、DeepLab等常见分割模型的训练与评估。目前已有648人学习/浏览。借助该数据集使用者可省去自行采集、清洗和标注的繁琐流程将精力集中于模型结构设计与调优配合作者提供的网络分割参考链接还能快速掌握气胸分割任务的数据组织方式与实验思路非常适合作为论文实验、毕业设计或医学图像分割入门练习的高质量基础数据。1. 气胸分割数据集拿到 2000 张图真正卡人的不是模型做医学图像分割的人第一次碰气胸Pneumothorax数据最容易被一句“超过 2000 张数据和标签”带偏以为数据量够了剩下的只是套一个 U-Net 的事。实际跑起来会发现气胸在 Chest X-Ray 上的特征和正常肺野、锁骨、肋骨边缘高度相似模型经常把整个肺野都当成目标或者干脆一张图都输出不了。这个数据集解决的是“有没有”的问题而“能不能用”取决于你怎么处理标签、怎么做预处理、怎么划定训练和验证的边界。这篇笔记适合医学影像算法工程师、刚入手语义分割的学生以及想评估“气胸分割到底值不值得做”的团队。我会从数据组织方式讲到最小训练管线再讲到验证和踩坑目标是让你照着能跑通也让你知道坑在哪里。2. 拆开这份 Chest X-Ray 数据集文件结构、标签形态与三个预处理决策2.1 从目录结构看起图像、标签和元数据各扮演什么角色气胸图像的语义分割数据集最常见的组织方式不是把所有 PNG 堆在一个文件夹里而是分为原始图像、标签图像和元数据三部分。原始图像一般是 Chest X-Ray 的 PNG 或 JPG灰度图居多标签图像是单通道 PNG像素值只有 0 和 255或 0 和 1元数据 CSV 里记录文件名、是否有气胸、气胸位置等辅助信息。先别急着训练第一步一定是把目录结构看清楚把每个文件的格式、尺寸、通道数摸一遍否则后面数据加载器会反复出问题。用下面这段命令快速盘点目录结构和文件格式tree -L 2 data/ # 期望看到两个主要子目录和一个csv文件 # data/images # data/masks # data/metadata.csv # 统计图像和标签的格式、尺寸分布 python - EOF from PIL import Image from pathlib import Path for sub in [images, masks]: files list(Path(data, sub).glob(*)) print(sub, 文件数:, len(files)) img Image.open(files[0]) print( 示例:, files[0].name, 尺寸:, img.size, 模式:, img.mode) EOF这段代码的作用有两个一是确认图像和标签的文件数量是否一一对应二是看图像是灰度模式“L”还是 RGB 模式“RGB”标签是不是单通道。常见的气胸数据集里图像是灰度图标签是二值图。参数说明mode L表示灰度mode RGB表示三通道如果看到标签是三通道说明它可能是把 mask 存成了伪彩色图需要先转灰度再二值化这一步漏了损失函数里会出现“三个通道的 logits 对三个通道的 mask”这种维度错误。再看一眼标签的像素值分布。很多翻车现场都是这里埋下的隐患import numpy as np from PIL import Image mask np.array(Image.open(data/masks/0001.png)) print(mask 取值:, np.unique(mask)) print(mask 形状:, mask.shape) print(正类像素占比:, (mask 0).mean())逻辑说明np.unique(mask)告诉你标签是不是只有两个值。如果除了 0 和 255 之外还有 127、128 这类中间值说明标签有灰度过渡带这在医学分割里是常见的“标注边缘软”问题。对于气胸这种边界模糊的目标我的建议是直接把大于 127 的像素置为 1不要保留中间值因为 U-Net 的输出是概率图训练标签必须是硬标签否则 Dice Loss 的计算结果会变得很奇怪。2.2 气胸影像的特殊性边缘模糊、肺野干扰与标注口径气胸在 Chest X-Ray 上的典型表现是肺野外带出现无肺纹理的透亮区脏层胸膜线清晰可见。听起来很好识别但对语义分割模型来说这是最麻烦的目标之一它的边缘是“一条线”而不是一片边界清晰的色块它的尺寸跨度极大可以是只有几个像素的窄条也可以是压缩整个肺野的大面积它的位置高度依赖肺野边界但 X 光片上锁骨、肋骨、纵隔阴影和正常肺纹理全都会参与干扰。很多团队第一版模型训练完Dice 在验证集上有 0.6可视化一看模型其实是在分割整个肺野气胸腔和正常肺组织被一视同仁地标成了正类。理解这个之后预处理的方向就清楚了。气胸分割不像 COCO 分割那样讲究“把物体从场景里抠出来”它更像是在一张低对比度、多结构重叠的灰度图上做精细的边缘定位。因此最常见的做法是先限制区域如果你能拿到肺野分割结果或者数据里自带肺野 mask就把预测范围限制在肺野内模型不需要学“哪里是肺”只需要学“肺里哪里是气胸”。即使没有肺野 mask也可以在数据增强阶段加入随机裁剪把注意力集中到胸腔中部和两侧而不是整张图均匀地学。2.3 三个预处理决策对比度、统一尺寸与标签形式预处理不做复杂的事但每个决策都影响后面训练和推理的一致性。我一般会固定在三个决策上不来回改。第一个决策是灰度对比度增强。X 光原图受设备、电压、患者体型影响很大直接喂给网络会让模型记住设备的“色调风格”而不是解剖结构。常见做法是用 CLAHE限制对比度自适应直方图均衡 clip limit 设置在 2.0~3.0tile grid 用 8x8。气胸区域的灰度小幅变化比整图直方图均衡更重要CLAHE 在局部做对比度拉伸更适合这种场景。第二个决策是统一输入尺寸。气胸图像尺寸差异大从 512x512 到 3000x3000 都有。归一化到固定尺寸时mask 必须用最近邻插值不能双线性。原因很直接双线性插值会在 0 和 1 之间产生小数标签变成了概率值损失函数里会出现“预测接近 0 但标签是 0.3”这种惩罚偏差而且小数标签在计算 Dice 时会把边界区域权重放大模型学到的边界会偏“胖”。第三个决策是标签形态统一。打开每一张 mask 都做一次“转灰度、二值化、重采样”而不是假设所有文件格式一样。下面是一个完整的预处理管线import cv2 import numpy as np from albumentations import CLAHE, Resize def preprocess_image_and_mask(image_path, mask_path, size(512, 512)): # 读取灰度图 image cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # mask 二值化大于127视为正类统一为0/1 mask (mask 127).astype(np.uint8) # 图像增强CLAHE aug CLAHE(clip_limit2.5, tile_grid_size(8, 8)) image aug(imageimage)[image] # 统一尺寸注意 mask 用最近邻插值 image cv2.resize(image, size, interpolationcv2.INTER_LINEAR) mask cv2.resize(mask, size, interpolationcv2.INTER_NEAREST) # 归一化到[0,1] image image.astype(np.float32) / 255.0 return image, mask逻辑说明这个函数做了四件事——读取灰度图、把 mask 转成 0/1 硬标签、做 CLAHE、统一尺寸。值得注意的点是cv2.resize的插值方式图像用INTER_LINEAR标签用INTER_NEAREST。参数说明clip_limit2.5控制对比度增强的强度数值越大对比度越强但会放大噪声胸片里如果可见噪点明显降到 2.0 比较稳size(512, 512)是平衡显存和细节的经验值气胸里的细线结构在 256x256 下容易消失在 1024x1024 下显存占用会翻四倍我建议先从 512 起步。3. 用 U-Net 跑通气胸分割从数据集划分到最小训练管线3.1 为什么第一版选 U-Net而不是 SegFormer 或 DeepLab2 千张级别的医学图像数据集第一版模型选择的标准是结构简单、容易训练、对小目标敏感、显存友好。U-Net 恰好满足这四点。它在编码器部分逐层下采样提取语义在解码器部分通过 skip connection 把浅层细节传回来这正好对上了气胸“边界细节重要、整体语义简单”的特点。DeepLab 系列在空洞卷积和 ASPP 上做文章对多尺度目标有优势但气胸的主要难点不是多尺度而是低对比度和边缘模糊U-Net 的像素级细节保留能力更直接。SegFormer 这类 Transformer 需要更多数据才能发挥优势2 千张胸片只能让它学到皮毛推理速度也慢调试周期长。从显存和速度看一个小型 U-Net 在 512x512 输入下单卡 8GB 就能跑 batch size 8训练一个 epoch 大约几十秒到几分钟。对比一下模型输入尺寸相对显存在 2 千张医学图上的表现调试成本U-Net4 层512x512低Dice 基本盘稳定低DeepLabV3ResNet50512x512中需更多调参边缘略粗中SegFormer-B2512x512中高易欠拟合依赖 pretrain中高注意这不是说 U-Net 一定最好而是“第一个能跑的方案”应该用 U-Net。等 U-Net 在验证集上稳定出结果后再换 DeepLabV3 或 Transformer 做提升对比才有意义。3.2 数据划分按患者分层别让验证集泄漏数据划分是 2 千张医学数据集里最容易被跳过、也最影响可信度的一步。胸片数据经常是同一个患者的多张片子如果随机按文件名划分同一个人的前后两次胸片很可能分别落在训练集和验证集里模型等于提前见过答案验证集指标虚高换到真实新患者身上就掉下来。正确做法是按患者 ID 划分保证同一个人的所有图像只在训练集或只在验证集里。另一个容易被忽略的问题是气胸样本占比。如果数据集里正负样本不平衡无气胸片多有气胸片少随机划分会导致验证集里气胸样本太少Dice 方差极大。常见做法是分层划分以“是否有气胸”为分层依据让训练集和验证集里正样本比例保持一致。import pandas as pd from sklearn.model_selection import StratifiedGroupKFold # metadata 必须包含 patient_id 和 pneumothorax(0/1) 两列 meta pd.read_csv(data/metadata.csv) meta[has_pneu] (meta[pneumothorax] 0).astype(int) split StratifiedGroupKFold(n_splits5, shuffleTrue, random_state42) train_idx, val_idx next(iter(split.split(meta, meta[has_pneu], groupsmeta[patient_id]))) train_meta meta.iloc[train_idx] val_meta meta.iloc[val_idx] print(训练集正样本占比:, train_meta[has_pneu].mean()) print(验证集正样本占比:, val_meta[has_pneu].mean())逻辑说明StratifiedGroupKFold同时满足两个约束——按patient_id分组避免数据泄漏按has_pneu分层保证正负样本比例一致。参数说明n_splits5表示做 5 折你可以取第一折做验证也可以用全部 5 折做交叉验证最终报告平均 Dicerandom_state42固定随机种子确保每次跑结果可比。这一步做了之后后续所有模型迭代都在同一个验证集上对比得出的结论才可信。3.3 核心训练脚本模型、损失函数与一次完整训练循环下面是一个能直接跑起来的最小训练脚本用 PyTorch 实现。模型部分直接用segmentation_models_pytorch的 U-Net损失函数用 Dice BCE 的组合这是气胸分割最常用的配置。import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset import segmentation_models_pytorch as smp class PneumothoraxDataset(Dataset): def __init__(self, meta, size512): self.meta meta self.size size def __len__(self): return len(self.meta) def __getitem__(self, idx): row self.meta.iloc[idx] # 复用上一章预处理函数这里直接返回数组 image, mask preprocess_image_and_mask( row[image_path], row[mask_path], (self.size, self.size) ) return ( torch.from_numpy(image).unsqueeze(0), # [1, H, W] torch.from_numpy(mask).unsqueeze(0), # [1, H, W] ) # 损失函数BCE DiceDice 权重更高 class DiceBCELoss(nn.Module): def __init__(self, dice_weight0.7, bce_weight0.3): super().__init__() self.dice_weight dice_weight self.bce_weight bce_weight def forward(self, logits, targets): probs torch.sigmoid(logits) bce nn.functional.binary_cross_entropy(probs, targets) smooth 1.0 intersection (probs * targets).sum() dice 1 - (2.0 * intersection smooth) / (probs.sum() targets.sum() smooth) return self.bce_weight * bce self.dice_weight * dice # 模型初始化 model smp.Unet( encoder_nameresnet34, encoder_weightsimagenet, in_channels1, # 灰度图 classes1, # 二分类 ) train_loader DataLoader(PneumothoraxDataset(train_meta), batch_size8, shuffleTrue, num_workers4) val_loader DataLoader(PneumothoraxDataset(val_meta), batch_size8, shuffleFalse, num_workers4) optimizer torch.optim.AdamW(model.parameters(), lr2e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): model.train() for images, masks in train_loader: logits model(images) loss DiceBCELoss()(logits, masks) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 每个epoch后在验证集上计算Dice model.eval() val_dice 0.0 with torch.no_grad(): for images, masks in val_loader: logits model(images) probs torch.sigmoid(logits) # 这里先按0.5二值化再算dice更贴近实际指标 preds (probs 0.5).float() intersection (preds * masks).sum(dim(1, 2, 3)) dice (2 * intersection 1e-8) / (preds.sum(dim(1, 2, 3)) masks.sum(dim(1, 2, 3)) 1e-8) val_dice dice.mean().item() print(fEpoch {epoch1}: val_dice {val_dice / len(val_loader):.4f})逻辑说明脚本按“数据集类 - 损失函数 - 模型 - 训练循环 - 验证循环”组织。DiceBCELoss里把 Dice 权重设为 0.7、BCE 权重设为 0.3因为气胸正类像素占比低纯 BCE 会偏向背景而 Dice 对不平衡不敏感适合小目标。验证循环里对预测概率做了 0.5 二值化再算 Dice这个口径和竞赛平台常用的评估方式一致避免了“用概率算 Dice 虚高”的毛病。参数说明batch_size8在 512 输入、ResNet34 编码器的 U-Net 下大约占用 6~8GB 显存如果你的卡只有 6GB改成 4lr2e-4是 AdamW 处理分割任务的常见起点如果 loss 震荡剧烈下调到 1e-4T_max30需要与总 epoch 数保持一致CosineAnnealing 的周期才会完整。这里没有做混合精度如果你的 PyTorch 版本支持可以把autocast加上训练速度能快 30% 左右。3.4 训练日志怎么看三种早期病态表现训练跑到第 3~5 个 epoch日志里出现的几种现象基本能预判这一版模型的结局。第一种是 loss 不降前几个 epoch 一直在 0.8 左右水平震荡。原因通常是学习率过高或数据加载出错比如 mask 预处理后全为 0模型没有正类梯度可学。检查方法很简单单独打印一个 batch 的 mask 统计值确认正类像素占比不是 0。第二种是验证集 Dice 一直为 0但训练 Dice 正常。最常见原因是验证集用了不同的插值方式或不同的归一化参数模型在训练时见过 0~1 归一化在验证时喂了 0~255 的值特征分布完全不同。排查预处理的唯一原则是训练和验证共用同一个函数不要写两份。第三种是训练 Dice 快速升高到 0.8但验证 Dice 在 0.2 附近不再动。这是典型的过拟合在 2 千张规模的数据集上很常见。处理方式不是盲目加数据增强而是先检查数据划分是否有泄漏确认划分没问题后再考虑减小模型容量或加 dropout。4. 调参与训练细节Dice 从 0.6 到 0.8 的几个关键操作4.1 损失函数怎么选Dice、BCE、Focal 和组合策略气胸分割的损失函数选择本质是在“像素级精度”和“区域重叠度”之间找平衡。BCE 是逐像素的交叉熵对每个像素独立惩罚梯度稳定但在正负类极不平衡时会偏向背景Dice Loss 直接优化区域重叠度对小目标更友好但容易在小目标为 0 的样本上产生波动而且对置信度不敏感导致模型输出的概率值不够有区分度Focal Loss 关注难分样本适合边界模糊问题但对类不平衡的敏感度不如 Dice 直接。我最终常用的是 Dice BCE 的组合配比按数据集大小微调。气胸这种 B 超/X 光图像建议配置如下目标特点推荐损失配比理由正类占比低、目标小Dice BCE0.7 / 0.3Dice 管重叠BCE 管梯度稳定边界极其模糊Dice Focal0.5 / 0.5Focal 加强难例学习目标大且形状固定BCE 或 Dice纯 Dice 即可背景干扰小不需要 BCE 辅助代码上Dice Focal 的组合比 Dice BCE 复杂一些但原理不复杂。Focal Loss 在标准交叉熵上增加了调制因子(1 - p_t)^gammagamma 越大对难分样本的关注越多。气胸数据集如果切片里包含大量“只有几像素细线”的样例gamma 设 2 比较好如果目标普遍明显gamma 设 1 够了。4.2 医学图像增强哪几种增强有效哪几种会破坏结构医学图像增强不能直接照搬自然场景的套路。对胸片来说随机的颜色抖动、grid distortion 这类改变灰度分布和局部几何结构的增强大概率会让肺野结构变形反而增加学习难度。真正稳定有效的是这几种水平翻转气胸在左右肺都可能出现这是最安全的增强小范围旋转±10°和缩放0.9~1.1模拟摆位差异弹性形变sigma 3~5模拟呼吸和体位变化导致的软组织结构微变Cutout / CoarseDropout模拟金属物、胸腔引流管等遮挡物用 Albumentations 实现的增强组合如下import albumentations as A train_transform A.Compose([ A.HorizontalFlip(p0.5), A.Rotate(limit10, border_mode0, p0.5), # 旋转超出部分填充0 A.RandomScale(scale_limit0.1, p0.3), # 尺度扰动 A.ElasticTransform(alpha1, sigma10, p0.2), A.CoarseDropout(max_holes4, max_height32, max_width32, fill_value0, p0.3), ]) # 使用方式在预处理之后调用 # augmented train_transform(imageimage, maskmask)逻辑说明Rotate里border_mode0表示旋转后超出边界的区域填充为 0黑色胸片背景本身是黑色这样不会引入伪结构RandomScale的scale_limit0.1控制在 10% 以内的缩放避免肺野比例失真ElasticTransform的alpha1, sigma10是一个保守参数肉眼几乎看不出形变但能增强模型对软组织变形的鲁棒性。参数说明CoarseDropout(max_holes4, max_height32, max_width32)相当于随机挖掉 4 块 32x32 的区域模拟导管、纽扣遮挡但正类区域小时不要开太强否则气胸区域可能被完整挖掉。训练结束后验证集只做与训练一致的 resize 和归一化不加任何随机增强这一点务必保持一致。4.3 训练超参设置输入尺寸、学习率、EMA 与早停2 千张图 30 个 epoch足够判断一个方案是否可行。但要稳定拿到高 Dice几个超参值得专门调整。输入尺寸是第一个要找平衡的点。512x512 适合大多数场景如果发现细小的气胸线总是被模型忽略可以尝试两阶段策略先在 384x384 上训练 20 个 epoch 收敛整体结构再用 768x768 微调 10 个 epoch 细化边缘这种 coarse-to-fine 方式在医学分割里很常用。第二个关键点是学习率调度。CosineAnnealingLR配合 30 个 epoch 是不错的选择但要注意一个常见问题如果前 5 个 epoch 验证 Dice 还在 0.1 附近说明初始化或学习率有问题早停不是先考虑的事先调 lr。第三个被很多人忽略的配置是 EMA指数移动平均维护一组训练权重的滑动平均推理时用平均权重而非最终权重往往能带来 2~3 个点的 Dice 提升。一个建议的参数参考超参推荐值调整思路输入尺寸512x512细线结构丢失时调大到 768batch size8显存不足时减半同时按比例降 lr初始学习率2e-4预热 3 个 epoch 后余弦退火训练轮数30~40超过 40 轮验证不提升基本到平台期权重衰减1e-4防止在 2 千张图上过拟合EMA 衰减0.99验证提升明显时可用 0.995以 2 千张的规模哪怕在 4GB 显存的卡上这个配置也能在 2 小时内训练完一轮。没必要为了省时间降低输入尺寸因为气胸细线结构对分辨率极其敏感。5. 评测与避坑为什么你的验证分总比想象中低5.1 本地验证指标与线上评测不一致现象本地用 5 折交叉验证平均 Dice 0.83放到评测集上只有 0.65断崖式下跌。原因大多数情况下不是模型随机性而是预处理不一致——本地验证时用了 0.5 阈值做二值化评测标准用的是最大 Dice 搜索或者本地把 mask 重采样成 512x512 算指标评测时在原始分辨率上算又或者数据划分时训练集包含评测集的近似重复样本本地验证虚高。解决固定两套口径。第一套是训练时的内部监控用 0.5 阈值、512x512 尺寸只用来判断模型收敛方向第二套是最终评测必须和平台的 mask 采样方式完全一致先查重采样插值方式再查二值化阈值。我现在的习惯是在项目里维护一个inference.py训练流程和推理流程共用里面的预处理函数而不是复制粘贴两份代码。5.2 气胸区域太小Dice 方差过大现象同一份验证集训练两次一次 Dice 0.78一次 Dice 0.61分数波动很大无法判断改动是否有效。原因气胸的正类像素占比本来就低如果验证集里包含大量“只有几十个像素”的极细线边缘 1 个像素的偏移就会导致 Dice 大幅波动。这是 DICE 指标本身在小目标上的通病不是模型不稳。解决在评估时引入“按样本大小分组”的维度。把验证集分为“大面积气胸正类占比10%”和“小面积气胸正类占比5%”分别报告 Dice。这样你能清楚知道模型是在大目标上不行还是在小目标上不行避免用一个平均值掩盖问题。对于小目标样本单看 Dice 不够要配合可视化检查边缘像素偏差。5.3 后处理缺失预测图里有大量孤立小点现象模型输出的预测图在肺野之外出现一堆孤立的噪点这些假阳性明显不是气胸但在计算 Dice 时却被计入分母拉低指标。原因U-Net 的逐像素分类天然会产生空间上零散的误检尤其是边缘高对比度区域如锁骨下缘、肋骨重叠处。这是模型后验概率的空间平滑度不够紧靠增大损失函数权重解决不了。解决在推理阶段加一个最小的后处理流程先用一个大核腐蚀或连通域分析把面积小于阈值的连通区域全部删除再用闭运算填补轮廓上的小缺口。代码实现import cv2 import numpy as np def postprocess_mask(pred_prob, threshold0.5, min_area50): # 二值化 mask (pred_prob threshold).astype(np.uint8) # 连通域分析删除小面积噪声 num_labels, labels, stats, _ cv2.connectedComponentsWithStats(mask, connectivity8) cleaned np.zeros_like(mask) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] min_area: cleaned[labels i] 1 # 闭运算填补边界小缺口 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) cleaned cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, kernel) return cleaned逻辑说明这个函数先按 0.5 阈值把概率图变成二值 mask然后用connectedComponentsWithStats找出所有连通域把面积小于 50 像素的删除最后做一次 5x5 的椭圆核闭运算。参数说明min_area50是一个经验值气胸区域的面积再小也不至于低于这个值但如果你处理的是超高分辨率图这个值可以按原图尺寸等比例放大threshold0.5是通用选择如果你想在“漏检”和“误检”之间找平衡可以把这个参数放到验证集上搜索选出使 Dice 最高的阈值。5.4 数据泄漏文件重复与训练验证重叠现象模型在验证集上对一个患者的四张片预测结果完全相同而且 Dice 特别高但换到另一个医院的片子马上失效。原因数据集在整理时同一个患者的不同拍摄时间、不同视角的胸片被当成了独立样本随机划分导致这些重复出现在训练和验证集里。更隐蔽的是图像级重复——某些平台生成的 mask 是洗不掉的“知识”模型学会了记住图像指纹而不是理解气胸形态。解决在数据加载前先做一次文件级去重用 MD5 对图像做哈希排除完全相同的图片然后按患者 ID 做分组划分这一点在第三章已经讲了做法。如果数据没有患者 ID 信息退而求其次用图像特征向量聚类把相似度超过阈值的图像归为同一组再做划分。这一步没有后悔药可吃漏检一个重复文件后续所有评测都不可信。6. 进阶验证用多折交叉与 TTA 确认模型有没有真的学会气胸训练结束后我一般不会马上提交或部署而是花 20 分钟做一组鲁棒性验证确认模型不是靠记忆样本混过验证集。做法分两步先跑 5 折交叉验证看各折 Dice 的标准差再对验证集做多尺度 TTA比较 TTA 前后的指标差距。TTATest-Time Augmentation在气胸分割里最常用的是水平翻转和双尺度。推理时把原图和水平翻转图同时输入模型把两张预测概率图取平均再按原始方向还原可以在不重新训练的情况下提升分割稳定性。代码思路如下import torch import torch.nn.functional as F def predict_with_tta(model, image): # image 形状 [1, 1, H, W]已归一化 model.eval() with torch.no_grad(): prob torch.sigmoid(model(image)) # 水平翻转 prob_flip torch.sigmoid(model(torch.flip(image, dims[-1]))) prob_flip torch.flip(prob_flip, dims[-1]) # 多尺度0.75倍与1.25倍 h, w image.shape[-2:] prob_small torch.sigmoid(model(F.interpolate(image, scale_factor0.75, modebilinear))) prob_small F.interpolate(prob_small, size(h, w), modebilinear) prob_big torch.sigmoid(model(F.interpolate(image, scale_factor1.25, modebilinear))) prob_big F.interpolate(prob_big, size(h, w), modebilinear) # 平均融合 prob_final (prob prob_flip prob_small prob_big) / 4.0 return prob_final逻辑说明torch.flip(image, dims[-1])对最后一个维度做水平翻转推理后再翻转回来保证预测结果和原图对齐。多尺度部分分别用 0.75 和 1.25 倍率重新缩放推理再插值回原始尺寸。四种结果取平均后边缘置信度会明显更平滑。参数说明TTA 不是越强越好这里的尺度范围 0.75~1.25 对胸片来说足够过大的缩放会引入畸变如果你对推理速度有要求可以只保留水平翻转放弃多尺度速度提升一倍Dice 差距通常在 0.5 个点以内。跑完 TTA 验证后再看 5 折交叉的标准差。我把这个标准差叫做“数据集下限”如果五折 Dice 在 0.78~0.86 之间波动说明模型对数据规模敏感加数据比调模型有效如果五折都在 0.80 附近波动不超过 0.02说明方案已经稳定可以放心去换更强的模型或做集成。说一个我自己的教训有次做气胸分割单折验证 Dice 0.84我以为稳了直接提交结果线上 0.61。后来查了一整天才发现线上评测的 mask 是 RLE 编码转出来的边缘自带一圈半透明过渡带我的预处理直接二值化把过渡带全算成了假阳性。从那之后我每次评测前都会先画三张图——原图、模型概率图、二值化预测图——肉眼对比一遍再谈指标。这一步多花五分钟能帮你躲掉大部分评测埋的雷。希望这篇文章能让你在气胸分割这条路上少走点弯路也希望你第一次跑通时不仅看到 Dice 数字在涨还能真正看清模型在哪里做对了、在哪里还在犯难。本文还有配套的精品资源点击获取