医学图像分割实战:从nii.gz读取到U-Net训练的直肠癌肿瘤分割全流程
简介第七届泰迪杯数据挖掘挑战赛B题直肠癌肿瘤分割完整方案包面向医学图像分析、数据挖掘与深度学习竞赛选手及相关领域研究人员。资源涵盖从原始影像数据预处理、ROI区域提取到基于纹理特征的SVM分类、双GPU版Unet训练、模型预测与结果对比的整套流程并附临床数据CSV、训练完成的FinalModel.h5权重文件以及README说明能够直接帮助理解赛题思路并快速复现实验。包内共24个文件以Python源码、PNG分割结果图、HDF5数据管理脚本、Markdown文档和模型文件为主压缩包约62MB整体代码模块划分清晰适合按步骤对照学习。目前已有171人学习下载可作为参加医学影像类竞赛的完整参考也适合想系统掌握肿瘤分割预处理、特征工程与深度学习建模的进阶开发者。1. 第七届泰迪杯B题直肠癌肿瘤分割这份zip压缩包到底在考什么拿到“第七届泰迪杯数据挖掘挑战赛-B题-直肠癌肿瘤分割.zip”这份压缩包很多第一次接触医学影像的同学会以为它和普通的数据挖掘赛题一样读表、做特征、跑分类。实际拆开zip你会发现里面是医学影像数据任务是肿瘤区域分割。这个赛题本质上是“医学图像分割”问题不是传统表格挖掘核心指标是Dice系数。它考察的是一整条链路影像读取、预处理、模型训练、后处理、结果提交。适合正在学深度学习、想接触医学影像方向的学生也适合想检验自己工程落地能力的人。我的建议是先别急着套模型把数据读明白再动手。2. 解包与数据摸底先别急着跑模型把影像数据读明白2.1 解压与文件结构检查识别zip伪加密与nii.gz组织方式压缩包刚到手里第一步是解压。这步看起来简单却是整个赛题第一个翻车点。泰迪杯的数据压缩包在历届比赛中偶尔会出现zip伪加密的情况也就是文件头部的加密标志被置位但数据本身并没有真正加密。表现是用系统自带的解压工具或者命令行unzip解压时提示需要密码或者解压出来的文件不完整但用部分第三方工具却可以正常打开。我处理这类压缩包的顺序是先看压缩包信息再尝试解压最后校验文件完整性。命令行下的操作是# 查看压缩包内文件列表和前几条信息确认有没有加密标志 zipinfo 第七届泰迪杯数据挖掘挑战赛-B题-直肠癌肿瘤分割.zip | head -20 # 直接用unzip解压观察是否报错 unzip -O gbk 第七届泰迪杯数据挖掘挑战赛-B题-直肠癌肿瘤分割.zip -d ./tumor_data第一条命令的zipinfo会输出每个文件的权限、大小、压缩方式和加密状态。如果文件条目末尾有*号说明该文件被标记为加密。注意这个*不等于数据真加密只代表加密标志位。此时再执行unzip如果系统提示password required可以先用下面这组命令绕过标志位解压# 用7z尝试直接解压7z对伪加密的容错比unzip好 7z x 第七届泰迪杯数据挖掘挑战赛-B题-直肠癌肿瘤分割.zip -o./tumor_data # 解压后校验关键目录是否存在不要只看解压成功就完事 ls -lh ./tumor_data find ./tumor_data -type f | wc -l逻辑说明unzip -O gbk是让系统按GBK编码解析中文文件名避免出现乱码目录7z在处理zip文件时对伪加密的容忍度更高经常能直接解开。最后一步find | wc -l是为了统计文件数量确认所有病例都到位防止解压中断导致后续训练时样本缺失。参数说明-d指定解压目录-O gbk只在文件名含中文时有必要如果压缩包内是纯英文名不加也不影响。做完这步你的工作目录里应该能看到一批病例文件夹每个病例通常包含若干影像序列文件最常见的格式是nii.gz或dcm。如果看到的是dcm说明数据是DICOM格式需要额外处理如果是nii.gz后面直接用nibabel读就行。2.2 读取MRI序列从nii/nii.gz到numpy数组的完整代码解压只是热身真正决定后续工程质量的是“能不能把影像正确读成数组”。直肠癌肿瘤分割赛题提供的数据通常是T2加权MRI影像文件格式多为nii.gz每个病例对应的标签是肿瘤区域的掩膜。读取这一步最常见的错误是把img_data和affine搞混或者把Mask读出来之后跟原图对不上位置。用Python读取nii.gz我一般这样写import nibabel as nib import numpy as np def load_nii(file_path): 读取nii/nii.gz文件返回图像数据和仿射矩阵 img nib.load(file_path) data img.get_fdata() # 形状通常是 (z, y, x) 或 (x, y, z)取决于数据存储方向 affine img.affine # 4x4矩阵用于把体素坐标映射到真实空间坐标 spacing img.header.get_zooms() # 每个维度的物理间距单位mm return data, affine, spacing # 以某个病例为例 img_data, img_affine, img_spacing load_nii(./tumor_data/case_001/t2.nii.gz) mask_data, mask_affine, mask_spacing load_nii(./tumor_data/case_001/tumor_mask.nii.gz) print(影像 shape:, img_data.shape, spacing:, img_spacing) print(掩膜 shape:, mask_data.shape, spacing:, mask_spacing)逻辑说明get_fdata()返回的是float64类型的numpy数组原始像素值保留适合做后续窗宽窗位处理。affine矩阵非常关键它是后续判断“原图和Mask是否对齐”的唯一凭证。打印出来的spacing表示每个体素在三个方向上的物理尺寸不同机器的spacing可能不同有的病例是(0.5, 0.5, 3.0)有的是(0.4, 0.4, 4.0)意味着层厚差异很大。参数说明img.header.get_zooms()返回的是元组顺序与数据shape对应。如果读取后img_data和mask_data的shape不一致优先检查是不是读错了文件其次检查是不是某个序列被裁过。注意MRI的存储方向在不同的采集设备上可能不同有的数据是(x, y, z)存储有的是(z, y, x)不统一时会直接影响后面训练。我习惯在读取后立即把数据统一成(z, y, x)顺序避开维度顺序混淆的坑# 统一axis顺序函数 def fix_axis_order(data, affine): if data.ndim 3: # 如果维度顺序是 (x, y, z)调整为 (z, y, x) if data.shape[0] data.shape[2]: data np.transpose(data, (2, 1, 0)) return data这段代码会根据shape大小关系判断维度顺序再通过transpose调整。它不够严谨但在赛题这种小数据量、样本来源相对固定的场景下是快速排查问题的实用办法。2.3 数据分布摸底类别不平衡、肿瘤体积统计与切块策略影像读进来了下一步不是训练而是把整个数据集的分布摸清楚。做医学图像分割最怕的就是“一张图里有肿瘤的体素只占全部体素的1%”而你没有意识到这一点。类别不平衡会让模型直接学成“全预测背景”Dice系数看起来还有几分实际一点用没有。这一步我做三件事。第一统计每个病例的肿瘤体素数量第二计算所有肿瘤的体积范围判断最大肿瘤和最小肿瘤的差异第三查看整张影像的尺寸范围决定后面是整图输入还是切块输入。示例代码如下import glob nii_files glob.glob(./tumor_data/case_*/tumor_mask.nii.gz) print(总病例数:, len(nii_files)) for mask_path in nii_files: mask_data, _, _ load_nii(mask_path) mask_bin (mask_data 0).astype(np.int16) tumor_voxels mask_bin.sum() print(f{mask_path}: 肿瘤体素数 {tumor_voxels}, 占比 {tumor_voxels / mask_bin.size:.4%})逻辑说明这段代码循环读取所有Mask文件统计每个病例的肿瘤体素数以及它在整幅图中的占比。我建议把输出重定向到文本文件里方便逐个查看。占比在10%以下的情况非常普遍说明数据高度不平衡。比值最小的病例和最大的病例之间可能相差几十倍训练时要想到这一点。参数说明mask_data 0是把Mask转成二值掩膜这里的0代表背景1或255代表肿瘤区域。有些比赛给的Mask是0和255有些直接是0和1统一转成0和1是训练前的必要操作。做完统计后依据结果决定预处理策略如果图像尺寸普遍在(200, 200, 20)这类小尺寸可以整图输入如果是(512, 512, 40)以上的尺寸我一般会按照肿瘤中心的包围盒裁剪到(96, 96, 32)或类似尺寸再训练避免背景占比过大。3. 预处理与模型选型为什么这类影像分割首选U-Net3.1 预处理管线窗口截断、归一化、重采样与ROI裁剪MRI图像的像素值不像自然图像那样是0-255它没有绝对的数值范围直接输入网络会导致训练不稳定。我常用的预处理顺序是窗口截断、归一化、重采样、ROI裁剪。前两步是通用的后两步要依据数据情况调整。先看截断和归一化def preprocess_image(img_data, mask_dataNone, window_min0, window_max800): MRI预处理先截断后归一化 # 截断到指定窗口 img_clipped np.clip(img_data, window_min, window_max) # z-score归一化减去均值除以标准差 mean_val img_clipped.mean() std_val img_clipped.std() img_norm (img_clipped - mean_val) / (std_val 1e-8) # 如果没有Mask直接返回归一化结果 if mask_data is None: return img_norm # 如果有Mask同时把Mask转成二值 mask_bin (mask_data 0).astype(np.int16) return img_norm, mask_bin逻辑说明np.clip把像素值限制在window_min和window_max范围内这一步对应MRI的“窗宽窗位”概念。直肠癌T2加权像中肿瘤区域和周围组织的灰度差异体现在一定数值区间内极限值通常是噪声直接砍掉。z-score归一化让数据变成零均值、单位方差这是深度学习模型最适应的输入分布也避免了不同机器采集参数不同带来的数值漂移。参数说明window_min0, window_max800只是我的初始参考值。直肠癌T2像的窗口设置一般在0到600-1000之间具体数值可以用np.percentile(img_data, 1)和np.percentile(img_data, 99)来动态计算代替手工指定。如果数据里有明显的高信号噪声动态百分位截断往往更稳。重采样和ROI裁剪这步需要先计算目标spacing。因为不同病例的spacing不一样直接输入网络会让模型学到错误的尺度信息。把数据统一重采样到1.0x1.0x1.0或2.0x2.0x2.0毫米是常见做法使用scipy.ndimage.zoomfrom scipy.ndimage import zoom def resample_to_spacing(data, spacing, target_spacing(1.0, 1.0, 1.0)): 重采样到目标spacing返回新数据和新spacing spacing: (z, y, x) 方向的物理间距 zoom_factor [ sp / target for sp, target in zip(spacing, target_spacing) ] resampled zoom(data, zoom_factor, order1) # 线性插值Mask用order0 return resampled逻辑说明zoom_factor根据当前spacing和目标spacing计算缩放比例。比如当前z方向层厚是3mm目标是1mmz方向的插值因子就是3.0相当于在层间插出3倍数量的切片。影像数据用order1线性插值Mask用order0最近邻插值避免在肿瘤边界产生中间值。注意Mask重采样后一定要再二值化一次否则标签里会出现0.5这种灰度值Dice计算会失真。关于ROI裁剪我的做法是先根据Mask计算肿瘤的包围盒向外扩张一定像素再裁剪。这个步骤既减少计算量又让模型专注于局部结构def crop_to_mask(img, mask, pad10): 根据mask的包围盒裁剪图像和掩膜pad为外扩像素 coords np.argwhere(mask 0) # 所有肿瘤体素的三维坐标 z_min, y_min, x_min coords.min(axis0) z_max, y_max, x_max coords.max(axis0) z_min max(0, z_min - pad) y_min max(0, y_min - pad) x_min max(0, x_min - pad) z_max min(img.shape[0], z_max pad) y_max min(img.shape[1], y_max pad) x_max min(img.shape[2], x_max pad) img_crop img[z_min:z_max, y_min:y_max, x_min:x_max] mask_crop mask[z_min:z_max, y_min:y_max, x_min:x_max] return img_crop, mask_crop逻辑说明np.argwhere拿到所有非零体素的坐标然后取每个维度的最小值和最大值构成一个三维包围盒。外扩pad10是为了让模型能看到肿瘤周围的解剖结构这有助于边界判断。裁剪后保留原spacing信息推理时把预测结果贴回原图位置即可。3.2 模型选型2D U-Net、3D U-Net与nnU-Net的适用边界模型选型是很多参赛队伍纠结的问题。我的判断标准很直接先看数据量和显存再谈模型。下面这个表是我的经验参考模型显存需求适合场景实现难度2D U-Net4-8GB薄层数据较少逐切片分割★☆☆☆☆3D U-Net12-24GB层间信息重要数据量充足★★★☆☆nnU-Net16GB自动配置管线数据量大★★☆☆☆2D U-Net是最稳妥的起点。它的输入是单张切片显存压力小训练速度快代码资料也最多。缺点是逐层预测容易在层间产生不连续的情况需要后处理修一下。3D U-Net能在三个方向上同时建模分割结果的空间连续性明显更好但参数量和显存开销成倍增加。注意如果整个数据集只有几十个病例直接上3D大网络很容易过拟合先用2D跑通流程再切换3D微调迭代效率更高。nnU-Net不是神经网络而是一套自动化配置的框架。它内置了基于数据统计的预处理参数、网络结构选择和训练策略非常适合做baseline。我的建议是如果你对这套数据还没有手感先让nnU-Net跑一版结果作为参照如果你只想用PyTorch手写那2D U-Net加上后续的3D修正已经足够撑起一个不错的名次。3.3 Patch采样与数据增强提升鲁棒性的最小配置数据增强在医学影像任务中尤为重要。因为病例数少模型很快就能把训练集背下来没有增强加持的验证集Dice会肉眼可见地低于训练集。我常用的增强组合是随机翻转、随机旋转和随机弹性形变。在3D数据上的实现我倾向于在训练循环里动态完成import random def augment_pair(img, mask): 影像对增强翻转和旋转 axes [0, 1, 2] # 随机90度旋转k表示旋转次数 k random.randint(0, 3) img np.rot90(img, k, axes(1, 2)) mask np.rot90(mask, k, axes(1, 2)) # 随机水平或垂直翻转 if random.random() 0.5: img np.flip(img, axis1) mask np.flip(mask, axis1) return img, mask逻辑说明np.rot90和np.flip都只对空间维度操作通道维度不参与变换。这里的axes(1, 2)假设数据形状是(z, y, x)把y和x平面旋转90度并随机翻转。没有对z轴做翻转因为MRI的层间方向本身有解剖学意义盲目翻转可能让肿瘤的解剖位置关系错乱对分割精度没有帮助。参数说明翻转概率0.5是比较中性的选择如果你发现验证集Dice振荡剧烈可以把概率降到0.3甚至0.2减少训练样本的随机变化幅度。4. 训练与推理的关键参数loss、学习率与后处理4.1 Loss函数Dice Loss与Cross Entropy的组合使用分割任务的Loss设计直接影响收敛速度。最常见的选择是Dice Loss和Cross Entropy的组合这两个Loss一个管“区域重叠程度”一个管“逐像素分类准确度”。单独用Dice Loss在训练初期梯度不稳定单独用Cross Entropy又很难缓解类别不平衡问题。我在这个赛题上比较常用的组合是import torch import torch.nn as nn import torch.nn.functional as F def dice_coef(y_pred, y_true, smooth1.0): 计算Dice系数y_pred为概率图y_true为独热标签 y_pred y_pred.contiguous().view(-1) y_true y_true.contiguous().view(-1) intersection (y_pred * y_true).sum() return (2. * intersection smooth) / (y_pred.sum() y_true.sum() smooth) class DiceLoss(nn.Module): def __init__(self, smooth1.0): super(DiceLoss, self).__init__() self.smooth smooth def forward(self, y_pred, y_true): # y_pred: [B, C, D, H, W]对概率取均值 probs torch.softmax(y_pred, dim1) loss 0.0 for i in range(probs.shape[1]): loss 1 - dice_coef(probs[:, i], y_true[:, i], self.smooth) return loss / probs.shape[1] class CombinedLoss(nn.Module): def __init__(self, alpha0.7): super(CombinedLoss, self).__init__() self.alpha alpha self.dice_loss DiceLoss() self.ce_loss nn.CrossEntropyLoss() def forward(self, y_pred, y_true): ce self.ce_loss(y_pred, y_true) # y_true转成独热编码用来配合dice loss y_true_onehot F.one_hot(y_true.squeeze(1), num_classesy_pred.shape[1]) y_true_onehot y_true_onehot.permute(0, 4, 1, 2, 3).float() dice self.dice_loss(y_pred, y_true_onehot) return self.alpha * dice (1 - self.alpha) * ce逻辑说明CombinedLoss里的alpha是权重我初始设为0.7让Dice Loss占主导、Cross Entropy辅助。F.one_hot把标签从[B,1,D,H,W]变成[B,D,H,W,C]再调转维度到[B,C,D,H,W]这样和网络输出的通道对齐。整个函数可以同时处理2D和3D数据只要标签形状一致。参数说明smooth是平滑系数防止分母为0也避免Loss在训练早期出现过大的波动。alpha0.7不是固定的如果训练开始后Loss下降得很慢可以调大Cross Entropy的比重如果训练后期Dice还上不去可以把alpha调到0.8或0.9。4.2 训练超参数学习率、epoch、batch size、验证集划分超参数的选择决定训练的上限也决定你调试的时间成本。我习惯先把通用配置固定下来再针对数据特性微调超参数值说明优化器AdamW相比Adam权重衰减更稳定基础学习率3e-43D网络建议降到1e-4Batch size2-8根据显存调整Epoch100-150用早停控制验证集比例20%按病例划分不能按切片划分学习率是我最先调的参数。对2D U-Net3e-4配合AdamW是稳定组合对3D U-Net显存里塞的东西更多梯度噪声更大调到1e-4更安全。注意这个赛题的数据量通常只有几十个病例训练集和验证集的划分必须按病例分不能把所有病例的切片混在一起随机分否则同一个病人的切片同时出现在训练集和验证集里验证结果会虚高。我一般按8:2比例划分用随机种子固定划分结果import random from sklearn.model_selection import train_test_split all_cases sorted([case_001, case_002, case_003, case_004, case_005]) train_cases, val_cases train_test_split(all_cases, test_size0.2, random_state42) print(训练病例:, train_cases) print(验证病例:, val_cases)逻辑说明train_test_split的random_state42保证了每次划分一致方便对比实验。如果你的病例数特别少少于20可以改用五折交叉验证每折训练4/5的数据、验证1/5最终把五折的平均Dice作为成绩单次划分的偶然性会小很多。4.3 推理与后处理连通域过滤、阈值调整、结果保存为zip要求的格式推理阶段有三个关键点一是阈值选择二是连通域过滤三是把预测结果保存成和标签一致的nii格式。先看后处理这段代码from scipy.ndimage import label def post_process(pred_prob, threshold0.5, min_volume50): 对模型输出的概率图做后处理 pred_prob: 模型sigmoid输出取值0-1 min_volume: 保留的最小肿瘤连通域体素数 pred_bin (pred_prob threshold).astype(np.int16) # 连通域标记去掉太小的高响应区域 labeled, num_features label(pred_bin) final_mask np.zeros_like(pred_bin) for i in range(1, num_features 1): component (labeled i) if component.sum() min_volume: final_mask[component] 1 return final_mask逻辑说明label函数把预测出来的二值掩膜按连通性分组每个独立区域被赋予一个编号。min_volume50会把孤立的噪声块过滤掉只保留体素数超过50的区域。这个参数需要结合实际数据调整肿瘤本身很小的病例min_volume设置太大会把真实肿瘤也删掉我一般先统计训练集肿瘤体积分布再把min_volume设为最小肿瘤体积的十分之一。阈值threshold0.5是默认选择但在交叉验证时可以针对每个fold微调。如果某个fold的验证集Dice偏低可以尝试0.4或0.6看哪个阈值下Dice更高再应用到测试集。注意最终提交的Mask文件必须保持和原图相同的affine矩阵否则评测脚本会认为你提交的数据和标注空间不一致。保存代码def save_nii(pred_data, reference_nii_path, save_path): 用参考nii文件的affine和header信息保存预测结果 ref_img nib.load(reference_nii_path) pred_affine ref_img.affine new_img nib.Nifti1Image(pred_data.astype(np.int16), affinepred_affine) nib.save(new_img, save_path)逻辑说明Nifti1Image构造时需要传入数据数组和affine数据类型要显式指定为int16和标签文件一致。这里不复制原文件的header只借affine因为预测结果的维度和原图维度已经对齐用同一个affine才能让评估系统把预测和标签放在同一坐标系下比对。5. 直肠癌肿瘤分割的常见坑与排查从伪加密到显存溢出5.1 zip伪加密解压报错却用7z能打开现象执行unzip解压时提示需要密码输入任意密码都解压失败但用7-Zip却能直接打开并解压。部分文件解压后大小与压缩包内显示不符运行读取代码直接报错。原因压缩包的文件头加密标志位被置位实际数据区没有加密属于zip伪加密。比赛数据在多次上传、转存过程中文件头信息被改动导致解压工具误判。解决直接用7-Zip或命令行7z x解压绕开加密标志位。如果7z也报错可以用16进制编辑器把zip文件头的加密标志位从01 00改为00 00再保存解压。这个操作本质上是在“修复”文件头不是破解加密。5.2 标签与影像shape不一致affine错位导致mask整体偏移现象训练时影像和Mask都能读进来但打印出来的Dice系数很低甚至接近0目视Mask却明显有重叠只是位置整体偏了几个体素。原因读取时把某些病例的Mask和影像没有按同一方向读取导致体素坐标错位。比如影像读取后是(z, y, x)方向存储Mask读取后是(x, y, z)方向如果不统一后续的裁剪和旋转就会把Mask往错误的方向移动。解决在数据读取阶段统一调用前面写的fix_axis_order函数并在每个病例读取后断言img_data.shape mask_data.shape。如果断言失败说明这个病例的Mask和影像尺寸不一致直接打印病例名并排查不要把这个样本送进训练。5.3 显存溢出3D输入过大时的降级方案现象训练启动后几秒钟程序报CUDA out of memory显存直接被占满。原因3D U-Net的输入patch太大或者batch size设置过高。医学影像的维度比自然图像多一维显存消耗是指数级增长的。解决先把batch size降为1再降低patch尺寸。比如原来设置patch_size(64, 192, 192)显存不够时换成(32, 128, 128)效果虽然打折扣但至少能跑通。等级更高的做法是开启混合精度训练PyTorch里的torch.cuda.amp可以省下约40%的显存。5.4 训练集Dice高、测试集崩过拟合与验证集划分现象训练集Dice到0.85以上验证集Dice只有0.5两者差距持续扩大测试集直接崩塌。原因数据增强太弱或没有做验证集分层。医学数据病例数少模型很容易记住训练集的特定灰度分布和位置信息而不是肿瘤的通用形态特征。解决增强翻转概率从0.5提到0.8加入随机弹性形变验证集划分改为按病例分层抽样确保验证集和训练集的病例来源不同。如果两者差距仍然很大直接减少网络参数量比如把U-Net的初始通道数从32降到16。6. 收官技巧用Dice系数做可复现验证与成果打包训练收尾阶段很多人只看验证集的总Dice。我习惯把每个病例单独算一遍Dice并全部打印出来这比一个平均分有用得多。因为平均值会被表现好的病例拉高掩盖个别病例的崩坏。比如总Dice有0.78但仔细看发现五个病例中有一个Dice只有0.3平均值被其余四个病例掩盖了。这种情况在医学分割里很常见排查时第一步就是看每个病例的预测结果。def evaluate_case(pred_mask, truth_mask): 计算单个病例的Dice系数 pred_mask: 二值预测结果 truth_mask: 二值标签 intersection np.sum(pred_mask * truth_mask) union np.sum(pred_mask) np.sum(truth_mask) if union 0: return 1.0 # 两个全为0视为完全一致 return (2.0 * intersection) / union case_dices [] for case in val_cases: pred np.load(f./preds/{case}_pred.npy) truth np.load(f./labels/{case}_mask.npy) d evaluate_case(pred, truth) case_dices.append(d) print(f{case}: Dice {d:.4f})逻辑说明evaluate_case按标准定义计算Dice交集乘以2再除以两个集合的总体素数。代码里还处理了一个边界情况如果预测和标签都是空Dice被定义为1.0因为空和空确实完全重叠。这个处理在评测和调试阶段都很关键避免把“无肿瘤病例”的Dice算成0误导判断。验证阶段的另一个实用技巧是TTATest-Time Augmentation也就是推理时对输入做轻微变换预测后再反向变换取平均。这里只做左右翻转def tta_predict(model, img_tensor): 推理时增强原图 左右翻转输出取平均 img_tensor: [B, C, D, H, W] 的torch张量 with torch.no_grad(): pred_orig torch.sigmoid(model(img_tensor)) img_flip torch.flip(img_tensor, dims[3]) # 水平翻转 pred_flip torch.sigmoid(model(img_flip)) pred_flip torch.flip(pred_flip, dims[3]) # 翻转回原来方向 pred_final (pred_orig pred_flip) / 2.0 return pred_final逻辑说明torch.flip把输入沿宽度方向翻转预测后再次翻转让结果和原图对齐最后和原始预测取平均。TTA在比赛阶段的收益往往有1-2个百分点代价只是推理时间翻倍。如果赛题允许我还会用五折交叉验证分别训练五个模型集成时把五个模型输出的概率图取平均作为最终结果这通常比单模型提升更稳定。我自己的习惯是每个实验都要留好随机种子、记录完整的预处理参数、保存每个折的best模型权重并且把最终提交的结果和对应模型配置存成独立文件夹。医学图像分割的复现性比普通任务更难保证因为预处理链条更长、依赖的库版本更敏感。养成记录的习惯调参时才有后悔药可以吃。希望这篇文章能帮你少走一些弯路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →