ARTICLE DETAIL

资讯详情

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

气胸X光语义分割数据集实战:从数据检查到模型训练与TTA调优

气胸X光语义分割数据集实战:从数据检查到模型训练与TTA调优 简介这份资源面向医学图像分割方向的研究人员、算法工程师与相关专业学生提供气胸Pneumothorax语义分割的胸部X光数据集可用于训练和评估分割网络帮助解决病灶区域自动识别与肺部轮廓划分问题。压缩包共约2000个文件以1998张png图像及对应标签为主另含1个txt类别说明与1个py脚本整体约183.48MB图像已做对比度拉伸与尺寸调整等增广处理并预先划分训练集与验证集。目前已有648人学习下载适合作为分割模型训练与验证的实战素材。读者可据此搭建数据加载流程、复现分割实验并对比不同网络结构配合类别文件快速确认标签含义为气胸辅助诊断研究提供可直接使用的数据基础。1. 气胸 X 光语义分割数据集2000 张带标签图像能直接喂给什么模型胸部 X 光片里找气胸肉眼容易漏模型也容易学偏。这份气胸图像语义分割数据集核心就是 2000 多张 Chest X-Ray 原图加对应掩码标签标签类别只有一类——Pneumothorax也就是气胸区域。图像已经做过对比度拉伸和 resize 增广训练集、验证集也提前划好了。它适合谁做医学图像分割课程设计的学生、想跑通 U-Net / DeepLab 系列 baseline 的算法工程师、需要小规模医学数据做迁移验证的研究者。你拿到手不用再花几天做清洗和划分直接进 dataloader 就能开跑。但别急着高兴医学分割的坑不在数据量在标签质量和预处理一致性后面几章我会把能复现的流程和翻车点都拆开。2. 先看清数据长什么样文件命名、标签格式与训练验证划分2.1 从文件名反推数据组织方式项目正文里给了一串文件名像1475_train_1_.png、56_train_1_.png、4233_train_1_.png。这种命名不是随便起的_train_说明它属于训练集_1_大概率是同一张图的第 1 个版本或者原图与标签的配对标识。常见做法是原图和掩码用相同数字 ID靠后缀区分比如1475_train_1_.png是原图1475_train_1_mask.png或1475_train_1_label.png是标签。你拿到压缩包后第一件事不是写模型是先把文件列表拉出来确认原图和标签能不能一一对上。import os import re from collections import defaultdict data_dir ./pneumothorax_dataset files os.listdir(data_dir) # 按数字 ID 分组看原图和标签是否成对 id_map defaultdict(list) for f in files: match re.match(r(\d)_train_1_, f) if match: img_id match.group(1) id_map[img_id].append(f) # 打印前 5 组检查每组文件数 for img_id in list(id_map.keys())[:5]: print(img_id, id_map[img_id]) # 统计只有 1 个文件的 ID这些可能是缺标签的 single_files [k for k, v in id_map.items() if len(v) 1] print(只有单个文件的 ID 数量:, len(single_files))这段代码干的事很简单用正则把文件名里的数字 ID 抠出来按 ID 分组。如果每组只有 1 个文件说明原图和标签没配对成功要么标签在另一个文件夹要么命名规则和我猜的不一样。参数上data_dir换成你解压后的实际路径正则里的_train_1_要按真实文件名调整。跑完这一步你心里就有数了数据是不是完整、有没有孤儿文件、需不需要重命名。2.2 标签类别与掩码像素值确认摘要里写得很清楚标签类别是 Pneumothorax具体看 classes 文件。医学分割数据集的掩码常见两种存法一种是 0/1 二值图0 是背景1 是气胸另一种是 0/255方便肉眼查看。你必须在训练前把掩码的像素值分布打出来否则模型学出来的全是背景loss 降得挺好看实际 Dice 接近 0。import numpy as np from PIL import Image # 随便抽一张标签图看像素值分布 mask_path ./pneumothorax_dataset/1475_train_1_mask.png mask np.array(Image.open(mask_path).convert(L)) unique, counts np.unique(mask, return_countsTrue) print(像素值:, unique) print(各值占比:, counts / mask.size) # 如果只有 0 和 1说明是二值掩码 # 如果有 0 和 255训练前要除以 255 if set(unique) {0, 255}: mask (mask 127).astype(np.uint8) print(已转为 0/1 二值掩码)逻辑说明convert(L)保证读进来是单通道灰度图避免 RGB 三通道干扰。np.unique把掩码里所有出现过的像素值列出来你一眼就能判断是 0/1 还是 0/255。如果是 0/255后面做 loss 计算前必须归一化到 0/1否则 BCE 或 Dice 的输入范围不对梯度会炸。这个检查我每次拿到新医学数据集都强制走一遍血泪经验。2.3 训练集与验证集的划分逻辑摘要说数据已经划分好训练集和验证集从文件名里的_train_也能看出来。但你要确认验证集是不是单独的文件前缀比如_val_或_test_。如果压缩包里只有一个 train 文件夹那所谓的验证集可能是从训练集里再切出来的这时候你得自己写划分脚本并且保证同一个病人的图像不会同时出现在训练和验证里——医学图像最怕病人级泄漏同一张片子切出来的不同区域分到两边验证指标虚高上线就翻车。import random from sklearn.model_selection import train_test_split all_ids list(id_map.keys()) # 按 8:2 切分random_state 固定保证可复现 train_ids, val_ids train_test_split(all_ids, test_size0.2, random_state42) print(训练集数量:, len(train_ids)) print(验证集数量:, len(val_ids)) # 如果数据本身带 _val_ 前缀优先用官方划分 val_files [f for f in files if _val_ in f] if len(val_files) 0: print(检测到官方验证集数量:, len(val_files))参数说明test_size0.2是验证集比例医学数据量小的时候可以调到 0.15 到 0.25 之间。random_state42是为了每次跑结果一致方便对比不同模型。如果官方已经给了验证集就别自己切直接用官方的否则你没法和其他人复现的结果对齐。3. 把数据喂进分割网络Dataset 写法、增广策略与训练参数3.1 自定义 Dataset 的完整写法PyTorch 里做语义分割Dataset 类要返回原图和掩码两张 tensor。原图做归一化和 ToTensor掩码只做 Resize 和 ToTensor不能做颜色抖动、随机裁剪这类会破坏标签空间关系的增广。下面这个写法我用了很多次直接抄就行。import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image import os class PneumothoraxDataset(Dataset): def __init__(self, data_dir, ids, img_size256, is_trainTrue): self.data_dir data_dir self.ids ids self.img_size img_size self.is_train is_train # 原图增广对比度拉伸已经在数据里做过这里只做基础变换 self.img_transform transforms.Compose([ transforms.Resize((img_size, img_size)), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ]) # 掩码只做 Resize 和 ToTensor保持像素值 self.mask_transform transforms.Compose([ transforms.Resize((img_size, img_size), interpolationImage.NEAREST), transforms.ToTensor() ]) def __len__(self): return len(self.ids) def __getitem__(self, idx): img_id self.ids[idx] img_path os.path.join(self.data_dir, f{img_id}_train_1_.png) mask_path os.path.join(self.data_dir, f{img_id}_train_1_mask.png) img Image.open(img_path).convert(L) mask Image.open(mask_path).convert(L) img self.img_transform(img) mask self.mask_transform(mask) # 掩码转 0/1防止 0/255 导致 loss 异常 mask (mask 0.5).float() return img, mask逻辑说明convert(L)把 X 光片转成单通道因为原始 Chest X-Ray 就是灰度图转 RGB 反而引入冗余通道。Normalize(mean[0.5], std[0.5])把像素拉到 -1 到 1 之间这是医学图像分割里比较稳的归一化方式。掩码的 Resize 必须用Image.NEAREST最近邻插值用双线性会把 0/1 边界插出 0.3、0.7 这种中间值标签就不干净了。最后(mask 0.5).float()是兜底确保送进 loss 的标签只有 0 和 1。3.2 增广策略哪些能做哪些绝对不能做数据本身已经做了对比度拉伸和 resize这是好事但你在训练时还可以加一些安全的增广。水平翻转、小角度旋转、随机亮度调整这三类对气胸分割是安全的因为气胸区域在左右肺的分布没有严格对称性翻转不会改变病理语义。但垂直翻转要慎用胸部 X 光片有明确的上下方位倒过来之后解剖结构就不对了。随机裁剪也要小心如果裁得太狠气胸区域可能被裁掉模型学到的是背景。import albumentations as A from albumentations.pytorch import ToTensorV2 train_aug A.Compose([ A.HorizontalFlip(p0.5), A.Rotate(limit10, p0.3), A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.3), A.Resize(256, 256), A.Normalize(mean[0.5], std[0.5]), ToTensorV2() ]) val_aug A.Compose([ A.Resize(256, 256), A.Normalize(mean[0.5], std[0.5]), ToTensorV2() ])参数说明HorizontalFlip(p0.5)一半概率水平翻转这是分割任务里最安全的增广。Rotate(limit10)只转正负 10 度再大就会出现黑边黑边被卷积核看到会引入噪声。RandomBrightnessContrast的幅度控制在 0.1因为数据已经做过对比度拉伸再加太强的亮度变化会让模型对灰度分布过拟合。验证集不做任何随机增广只做 Resize 和 Normalize保证每次验证结果可复现。3.3 训练参数与 loss 选择气胸区域在整张 X 光片里占比很小典型的前景背景比可能到 1:50 甚至更极端。这时候用普通交叉熵模型会倾向于全预测背景准确率看着很高但 Dice 惨不忍睹。常见做法是 Dice Loss 加 BCE 组合Dice 负责拉回前景召回BCE 稳定梯度。import torch.nn as nn class DiceBCELoss(nn.Module): def __init__(self, weight_bce0.5): super().__init__() self.weight_bce weight_bce self.bce nn.BCEWithLogitsLoss() def forward(self, pred, target): # pred 是 logitstarget 是 0/1 bce_loss self.bce(pred, target) pred_sigmoid torch.sigmoid(pred) intersection (pred_sigmoid * target).sum() dice_loss 1 - (2. * intersection 1e-6) / (pred_sigmoid.sum() target.sum() 1e-6) return self.weight_bce * bce_loss (1 - self.weight_bce) * dice_loss逻辑说明BCEWithLogitsLoss内部做了 sigmoid数值更稳定不要在外面再手动 sigmoid。Dice 的分子乘 2 是因为 Dice 系数定义里交并比要乘 2分母加1e-6是防止除零。weight_bce0.5是经验值如果验证集 Dice 上不去可以调到 0.3让 Dice Loss 占更大权重。优化器用 Adam学习率 1e-4batch size 根据显存来8G 显存跑 256x256 可以到 16。4. 避坑与排查标签错位、显存爆炸、指标虚高怎么破4.1 原图和标签错位模型学了个寂寞现象训练 loss 能降但验证集 Dice 一直在 0.1 附近晃可视化预测结果发现掩码和原图完全对不上。原因文件名配对逻辑写错了比如原图是1475_train_1_.png标签是1475_train_1_mask.png但代码里拼成了1475_train_1_label.png打开的是另一张图或者直接报错被 try 吞了。解决在 Dataset 的__getitem__里加断言确保原图和掩码的尺寸一致并且掩码的前景像素占比在合理范围内。assert img.shape mask.shape, f尺寸不匹配: {img.shape} vs {mask.shape} foreground_ratio mask.sum() / mask.numel() assert 0.001 foreground_ratio 0.5, f前景占比异常: {foreground_ratio}4.2 显存爆炸batch size 降到 1 还 OOM现象一开训练就 CUDA out of memory把 batch size 降到 1 还是炸。原因图像 resize 到了 512x512 甚至 1024x1024U-Net 的中间层特征图太大。解决先把输入降到 256x256这是医学分割里性价比最高的尺寸如果还炸把 U-Net 的第一层通道数从 64 降到 32显存占用能少一半。另外检查 DataLoader 的num_workers设成 4 或 8别设 0否则数据加载会拖慢训练。4.3 验证集 Dice 虚高上线就翻车现象验证集 Dice 0.85换一批新数据掉到 0.4。原因训练集和验证集有病人级泄漏同一张 X 光片的不同区域被分到了两边。解决如果数据里有病人 ID 或图像 ID 的层级关系按病人 ID 划分不要按图像随机划分。这份数据集的文件名里只有数字 ID你没法直接判断病人归属但可以观察同一个 ID 前缀是否有多个文件如果有把它们绑在一起划分。4.4 掩码像素值没归一化loss 直接 NaN现象训练几个 step 后 loss 变成 NaN。原因掩码是 0/255送进 BCEWithLogitsLoss 后 target 超出 0 到 1 范围梯度爆炸。解决在 Dataset 里强制做(mask 0.5).float()或者在 loss 函数里加断言assert target.max() 1.0。这个坑我踩过不止一次后来养成习惯每个新数据集的掩码都先跑一遍np.unique。4.5 对比度拉伸过度正常组织也被当成病灶现象模型把肋骨边缘、肩胛骨轮廓也预测成气胸。原因数据预处理时对比度拉伸参数太激进正常组织的灰度被拉到了和气胸区域相近的范围。解决可视化几张原图和掩码的叠加图如果肉眼都分不清边界模型更分不清。这时候要么换预训练权重要么在 loss 里加边界权重让模型更关注难样本。5. 进阶技巧用 TTA 和阈值搜索把 Dice 再抬两个点训练完模型别急着收工推理阶段还有两个几乎零成本的涨点手段测试时增强和阈值搜索。TTA 的做法是对同一张测试图做水平翻转分别推理再把两个概率图平均。因为气胸分割对水平翻转是等变的平均之后边界更稳。阈值搜索则是把 sigmoid 输出从 0.5 开始以 0.05 为步长扫一遍找验证集 Dice 最高的那个阈值。这两个技巧叠加通常能把 Dice 从 0.78 抬到 0.82 左右。def predict_with_tta(model, img_tensor): model.eval() with torch.no_grad(): # 原始推理 pred1 torch.sigmoid(model(img_tensor)) # 水平翻转推理 img_flip torch.flip(img_tensor, dims[3]) pred2 torch.sigmoid(model(img_flip)) pred2 torch.flip(pred2, dims[3]) # 平均 pred (pred1 pred2) / 2 return pred # 阈值搜索 best_dice 0 best_thresh 0.5 for thresh in np.arange(0.3, 0.7, 0.05): dice compute_dice(val_preds, val_masks, thresh) if dice best_dice: best_dice dice best_thresh thresh print(f最佳阈值: {best_thresh}, Dice: {best_dice})逻辑说明torch.flip(img_tensor, dims[3])是对宽度维度做翻转因为输入 tensor 的形状是(B, C, H, W)dim 3 就是 W。翻转推理完再翻回来保证和原图空间对齐。阈值搜索的步长 0.05 是精度和计算量的折中如果验证集够大可以降到 0.02。注意阈值搜索必须在验证集上做不能拿测试集调阈值否则就是另一种过拟合。还有一个容易忽略的点这份数据已经做过 resize 增广你在推理时如果用了和训练不一样的尺寸比如训练 256、推理 512Dice 会掉。保持一致或者用多尺度推理再平均但多尺度会成倍增加推理时间看你的场景能不能接受。从那以后我每次拿到新的医学分割数据集都强制走一遍「文件配对检查 → 掩码像素值确认 → 前景占比统计 → 小批量过拟合测试」这四步确认没问题再开正式训练。希望帮到你。本文还有配套的精品资源点击获取
返回列表