简介:面向医疗图像分析与深度学习研究者或学生的三维肝脏分割实战资源,基于生成对抗网络(GAN),在Python/Jupyter Notebook环境中完成模型构建、训练与评估,适用于疾病诊断、治疗规划与手术导航等医学场景。压缩包共12个文件,含4个Python脚本、2张网络结构图、1个Jupyter Notebook及依赖与说明文档,整体仅529KB。目前已有108人学习下载。资源从数据预处理、三维卷积网络搭建,到生成器与判别器的对抗训练、损失函数与优化器选择,再到Dice/Jaccard等评估指标计算,均有完整代码与说明支撑;同时提供可直接运行的实验骨架、集群训练脚本与可视化结果,便于读者快速复现GAN医学分割流程。无论用于课程设计、毕业项目还是科研实验,都能借助这套轻量资源快速上手。
1. 使用GAN进行3D肝脏分割:先说清楚这个zip到底值不值得打开
拿到一个“使用GAN进行3D肝脏分割_Python_Jupyter Notebook_下载.zip”的项目包,你大概想知道三件事:这东西是不是又是换个皮把U-Net跑一遍?跑起来需要多大显存?以及GAN到底在这里解决了什么纯分割模型解决不了的问题。先说结论:GAN在这个任务里不是噱头,CT影像中肝脏与周围组织灰度接近、边界模糊,纯监督分割容易被模糊标注带偏,而生成对抗训练等于给分割网络配了一个“挑刺的评委”,逼着预测结果从形状和纹理上都更像真实标注。这个项目对医学图像算法工程师、做3D视觉的研究生以及想从2D分割转3D的从业者都值得过一遍。文章后面我会按“项目结构 → 数据预处理 → 生成器与判别器调参 → 踩坑记录 → 验证技巧”一条线讲清楚。
2. 读懂项目结构:从下载的zip到跑通一次训练
2.1 zip里应该有什么:核心目录与文件职责
这类GAN分割项目zip包的内部结构通常不会长得一模一样,但主干模块基本一致。我一般会用一条命令先摸清家底,再决定从哪个文件开始读:
unzip 使用GAN进行3D肝脏分割_Python_Jupyter_Notebook_下载.zip -d liver_gan cd liver_gan find . -maxdepth 2 -type f | sort | head -50find这步是为了让你在动手之前先建立文件地图。常见结构是这样的:data/放原始NIfTI和预处理产物,models/放生成器和判别器定义,configs/放超参数yaml或py,train.py是训练入口,inference.py或一个predict.ipynb负责推理和可视化。如果连requirements.txt都没有,你需要做好手动补依赖的准备。
| 目录/文件 | 典型职责 | 你要重点看什么 |
|---|---|---|
| data/raw | 原始CT和标签 | 文件是NIfTI(.nii/.nii.gz)还是H5 |
| data/preprocessed | 裁剪后的patch或归一化数组 | patch大小、归一化范围是否写死 |
| models/generator.py | 生成器网络结构 | 是3D U-Net变体还是VNet |
| models/discriminator.py | 判别器结构 | 是全局打分还是PatchGAN |
| losses/ | 组合损失 | 对抗损失权重、Dice/BCE占比 |
| train.py | 训练主循环 | 默认超参数和断点续训逻辑 |
| configs/ | 参数配置 | batch size、patch size、学习率 |
| notebook/ | 可视化或小规模demo | Jupyter Notebook里跑的推理示例 |
注意一点:很多项目的“3D”其实是假3D,内部直接调用2D卷积逐层切片处理。判断标准是看models/generator.py里有没有Conv3d、ConvTranspose3d这些算子。标题既然写的是3D分割,你应该优先找真正使用3D卷积的版本,因为肝脏CT是体数据,切片间连续性对分割精度影响很大。
2.2 环境准备:Python、CUDA、PyTorch与MONAI的版本搭配
这类医学影像项目跑不起来的原因,十有八九是环境版本互相打架。肝脏分割项目通常依赖PyTorch、NiBabel或SimpleITK做数据读写、MONAI做数据增强与评估指标。常见做法是用conda单独建环境,避免把系统Python搞乱:
conda create -n liver_gan python=3.8 -y conda activate liver_gan conda install pytorch torchvision pytorch-cuda=11.8 -c pytorch -c nvidia pip install monai nibabel simpleitk scikit-image matplotlib tensorboardPython 3.8到3.10是这类项目兼容性最稳的区间,太新的版本可能会踩到MONAI或旧版PyTorch的依赖坑。CUDA版本要看你的显卡驱动,使用nvidia-smi确认驱动支持的最高CUDA版本,再选对应的PyTorch轮子。如果只是推理不训练,CPU也能跑,但3D卷积加GAN的推理速度会让你失去耐心,建议至少要有一张8GB以上显存的卡。
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"看到True只代表CUDA可用,不代表你能跑。下一步你要确认显存是否足够,这直接决定了该项目的patch size和batch size该怎么设置。很多项目默认配置是按16GB以上显存写的,8GB卡跑python train.py大概率立刻OOM,这是最普遍的翻车点。
2.3 第一条训练命令:参数含义与最小可跑配置
在改任何代码之前,先看一眼train.py的入口参数。这类项目的启动命令通常长这样:
python train.py \ --data ./data/preprocessed \ --config ./configs/lits_gan.yaml \ --batch_size 2 \ --patch_size 96 \ --lr 1e-4 \ --lambda_adv 0.1 \ --epochs 200--lambda_adv是对抗损失权重,数值太小等于没有GAN,太大则可能让网络陷入振荡甚至生成畸变结构。--patch_size是每次喂给网络的3D块边长,不是2D图像边长,它决定了显存占用的立方级增长。--batch_size 2配合--patch_size 96是8到12GB显存的一个保守起点。
第一次跑通不要追求指标,建议把--epochs调小到10以内,确认数据加载、前向反向、权重保存整条链路能走通。等日志里出现epoch和loss稳定输出,再把它调回正常规模。这一步很重要,因为GAN训练本来就不稳定,如果链路都没通,你看到的loss数值没有任何意义。
运行过程中关注这几项:训练loss是否下降、判别器loss是否为零、每轮保存的验证Dice是否在动。如果Dice迭代几十步都纹丝不动,说明生成器可能根本没被有效训练,后面第5章的踩坑清单里会有对应的排查方法。
3. 把CT体数据喂给GAN:预处理与3D patch裁剪
3.1 从NIfTI到numpy数组:归一化、重采样与HU截断
肝脏CT数据以Hounsfield Unit(HU)表示组织密度,直接拿原始值喂网络会出问题。不同扫描设备的HU范围一致但肝脏与周围组织相对关系才是关键,所以行业通行做法是先把CT值截断到一个合理区间,再做归一化。肝脏区域大致在-100到400HU之间,常见截断范围是[-150, 250]或[-100, 300],具体看数据集标注习惯。
import SimpleITK as sitk import numpy as np def preprocess_ct(image_path, label_path, out_path, lower=-150, upper=250): itk_img = sitk.ReadImage(image_path) itk_lab = sitk.ReadImage(label_path) img = sitk.GetArrayFromImage(itk_img).astype(np.float32) lab = sitk.GetArrayFromImage(itk_lab).astype(np.uint8) # HU截断与归一化:截断后线性映射到[0,1] img = np.clip(img, lower, upper) img = (img - lower) / (upper - lower) # 标签二值化:非0视为肝脏 lab = (lab > 0).astype(np.uint8) np.savez_compressed(out_path, img=img, lab=lab) print(f"shape={img.shape}, 前景比例={(lab > 0).mean():.4f}")注释里写的前景比例是按体素算的,医学解剖中肝脏在腹部CT的占比通常只有5%到15%,这个数字你要记住,后面设计损失函数全靠它。np.savez_compressed是这类项目常用的中间格式,读写快、无需额外依赖,比直接存NIfTI再反复读取更省时间。
重采样这一步我单独说一下。很多原始CT的层间距是5mm或更厚,而层内分辨率是0.7mm左右,导致体素各向异性严重。3D卷积在特征提取时会把每个体素当成同等物理大小,这一假设在厚层数据上不成立。常见处理是重采样到各向同性,比如1.5mm或2.0mm:
def resample_to_isotropic(itk_img, target_spacing=1.5): original_spacing = itk_img.GetSpacing() original_size = itk_img.GetSize() new_size = [ int(round(original_size[0] * original_spacing[0] / target_spacing)), int(round(original_size[1] * original_spacing[1] / target_spacing)), int(round(original_size[2] * original_spacing[2] / target_spacing)), ] resampler = sitk.ResampleImageFilter() resampler.SetOutputSpacing([target_spacing] * 3) resampler.SetSize(new_size) resampler.SetInterpolator(sitk.sitkLinear) return resampler.Execute(itk_img)调用它时注意,图像用线性插值,标签必须用最近邻插值,否则肝脏边缘会被插出中间灰度值,训练时判别器会抓住这些伪影做文章。这一步你要是拿同一个重采样函数处理图像和标签,分割结果边缘会出现一圈灰色晕,这是明显的低级错误。
3.2 用滑动窗口裁patch:显存有限时的标准做法
3D体数据整卷喂进3D卷积网络是不现实的,一张512×512×300的CT体积远超显存容量。标准做法是训练时随机裁剪固定大小的patch,推理时用滑动窗口拼回全图。裁剪策略直接影响训练效果,纯随机裁剪会让大部分patch落到背景区,肝脏占比过低,判别器很容易学会“见背景就判假”。
class RandomPatchSampler: def __init__(self, img, lab, patch_size=96, foreground_fraction=0.5): self.img = img self.lab = lab self.patch_size = patch_size self.foreground_fraction = foreground_fraction # 强制50% patch包含肝脏 def sample(self): p = self.patch_size z, y, x = self.img.shape if np.random.rand() < self.foreground_fraction: # 以肝脏前景体素为中心采样 fg = np.argwhere(self.lab > 0) if len(fg) > 0: cz, cy, cx = fg[np.random.randint(len(fg))] cz = min(max(cz, p // 2), z - p // 2) cy = min(max(cy, p // 2), y - p // 2) cx = min(max(cx, p // 2), x - p // 2) else: cz, cy, cx = [np.random.randint(p//2, s-p//2) for s in (z, y, x)] else: cz, cy, cx = [np.random.randint(p//2, s-p//2) for s in (z, y, x)] return (self.img[cz-p//2:cz+p//2, cy-p//2:cy+p//2, cx-p//2:cx+p//2], self.lab[cz-p//2:cz+p//2, cy-p//2:cy+p//2, cx-p//2:cx+p//2])这段代码用foreground_fraction控制采样的偏向,0.5意味着每个batch至少一半的patch包含肝实质。实际训练中你会发现,纯随机采样时一个epoch里大量样本都是纯背景,生成器根本没机会学肝脏内部结构。加了前景引导采样后训练收敛速度能快出不少。
patch大小怎么定?96×96×96是通用起点。显存紧张时降到64,显存充裕又能承受更慢训练时用128。注意一个隐藏陷阱:patch越大,GAN判别器越容易从全局形状上指出“这是假的”,但这会让训练成本立方级上涨;patch太小,判别器只看局部纹理,又会失去对抗训练对整体形状的约束能力。所以这个参数不是越大越好,你的GPU预算决定上限,分割目标决定下限。
3.3 标签不均衡处理:前景占比只有5%时怎么办
肝脏在CT体数据中占比很小,如果直接对每个体素做二分类交叉熵,网络可以靠全部预测为背景拿到95%准确率。这时网络看似很“聪明”,实际分割结果是全黑。纯监督分割任务里常用Dice损失缓解这个问题,GAN路线里同样要处理,但方式略有不同。
def dice_loss(pred, target, smooth=1e-5): pred = torch.sigmoid(pred) intersection = (pred * target).sum(dim=(2, 3, 4)) union = pred.sum(dim=(2, 3, 4)) + target.sum(dim=(2, 3, 4)) return 1 - ((2 * intersection + smooth) / (union + smooth)).mean()这里的dice_loss按3D卷计算Dice,直接作用在logits上,配合bce一起用。训练配置里通常写成loss = dice_loss + bce_weight * bce + lambda_adv * adversarial_loss。Dice损失对前景占比不敏感,它天然关注的是重叠度而不是像素正确率。
另外你可以在配置里给标签做类别权重,比如背景权重0.1、前景权重1.0。但我不建议权重拉得过大,因为GAN的判别器会同时看到生成结果和真实标签,如果生成器被标签权重逼得过分激进,生成的肝脏形状会偏向“大而全”,把胆囊、下腔静脉这些相邻结构也包进来。遇到这种问题时,最需要调的不是类别权重,而是第4章要讲的对抗损失权重和判别器结构。
4. 生成器与判别器的关键参数:想让GAN稳定收敛该调什么
4.1 生成器选型:3D U-Net还是VNet,以及损失权重
肝脏分割的生成器几乎清一色是编码器-解码器结构,区别在细节。3D U-Net用4到5层下采样,每层特征通道翻倍,跳跃连接把浅层细节传回来,适合边界精细的分割任务。VNet在U-Net基础上加了残差连接,前向传播更稳定,梯度流动更顺滑,GAN这种对抗训练环境下崩溃概率更低。如果是自己改代码,我建议先ResNet风格的VNet起步。
class Generator(nn.Module): def __init__(self, in_channels=1, base_channels=32): super().__init__() self.enc1 = self._block(in_channels, base_channels, stride=1) self.enc2 = self._block(base_channels, base_channels * 2, stride=2) self.enc3 = self._block(base_channels * 2, base_channels * 4, stride=2) self.dec3 = self._block(base_channels * 4, base_channels * 2, stride=1) self.dec2 = self._block(base_channels * 2, base_channels, stride=1) self.out = nn.Conv3d(base_channels, 1, kernel_size=1) def _block(self, in_ch, out_ch, stride): return nn.Sequential( nn.Conv3d(in_ch, out_ch, 3, padding=1, stride=stride), nn.InstanceNorm3d(out_ch), nn.LeakyReLU(0.2, inplace=True) ) def forward(self, x): e1 = self.enc1(x) e2 = self.enc2(e1) e3 = self.enc3(e2) d3 = self.dec3(e3) d2 = self.dec2(d3 + e2) return self.out(d2)这是压缩版结构,真正跑实验时你会把跳跃连接补全。两个细节值得注意:第一,InstanceNorm3d比BatchNorm3d更适配GAN训练,因为判别器拿到的batch里的真实样本和生成样本统计量本来就不同,BN会把两组数据混在一起归一化,容易震荡。第二,生成器输出前不加Sigmoid,让nn.BCEWithLogitsLoss在数值上更稳定,也方便Dice损失直接从logits计算。
损失权重方面,lambda_adv从0.01到0.1之间起步是经验值。权重太小,输出接近纯U-Net,边界依旧模糊;权重太大,生成器为了骗过判别器可能牺牲结构准确性,出现支离破碎的空洞。这个参数每次翻倍或减半地调,观察验证集Dice的变化,别靠猜。
4.2 判别器与对抗损失:PatchGAN感受野和梯度惩罚
判别器的任务不是判断“这张图是CT还是标签图”,而是判断“分割结果和真实标注像不像”。常用的判别器有两类:整图打一个真/假分数的全局判别器,和PatchGAN这种输出一张响应图、对每个局部块分别打分的结构。3D分割场景下我几乎不用全局判别器,因为生成结果和标注的差異往往只集中在肝脏边缘这层很薄的体素上,全局打分很容易被大面积背景淹没——网络直接给全部patch判真就能把loss压到很低。
class PatchDiscriminator3D(nn.Module): def __init__(self, in_channels=2, base_channels=64): super().__init__() self.layers = nn.Sequential( nn.Conv3d(in_channels, base_channels, 4, stride=2, padding=1), nn.LeakyReLU(0.2, inplace=True), nn.Conv3d(base_channels, base_channels * 2, 4, stride=2, padding=1), nn.InstanceNorm3d(base_channels * 2), nn.LeakyReLU(0.2, inplace=True), nn.Conv3d(base_channels * 2, 1, 4, stride=1, padding=1) ) def forward(self, x): out = self.layers(x) return out.view(x.size(0), -1).mean(dim=1)判别器输入是2个通道:一个通道是真实或生成的二值分割图,另一个通道是对应的CT图像作为条件。这个“条件”非常关键,它让判别器知道当前要评判的是“这张CT下的肝脏分割图”,而不是毫无上下文的随机二值形状。
对抗损失用哪种形式,直接决定你要写的训练代码。常见做法是用WGAN-GP取代原始的log loss,因为原始BCE在判别器训练得特别好时梯度容易消失,生成器学不到东西。WGAN-GP要求判别器输出的是一个实数分值,并且对输入求梯度加入惩罚项。如果你看到项目里train.py有gradient_penalty相关代码,说明作者已经踩过梯度消失的坑。
4.3 训练节奏:先跑自监督预热再开对抗
GAN训练最让人头疼的是两个网络互相博弈导致的震荡。纯GAN从零开始训练时,判别器往往先一步收敛,生成器还在瞎猜,结果对抗损失一路飙升。这种场景下全盘训练是白费力,普遍的做法是分成两个阶段:先关掉判别器,只用Dice加BCE训练生成器几十个epoch,让分割网络先学会一个像样的初版结果;再打开对抗损失,用一个“实力接近”的判别器促使边界变得更锐利。
# 阶段一:预热,禁用对抗分支 python train.py --phase warmup --warmup_epochs 30 --lambda_adv 0 # 阶段二:正式训练,从warmup权重继续 python train.py --phase gan --resume ./checkpoints/liver_warmup.pth \ --lambda_adv 0.05 --lr 1e-4两个阶段的--lr最好都不超过1e-4。GAN里生成器更新时,判别器输出的分值大致反映了生成器的真实水平,如果判别器太弱,生成器会拿不到有效梯度,这又是另一个极端。一个实用的检查方式是打印判别器对真实样本和生成样本的平均输出分值:理想状态是真实分值略高于0但因为梯度惩罚被限制在0附近,生成分值从负往0爬。如果真实分值为1、生成分值为0,网络已经饱和,生成器已经学不到东西。
训练过程中--batch_size 2或4是3D分割的常态。显存允许时适当调大batch会让判别器梯度更稳定,但如果只能跑batch size 1,也不是不能训,只是判别器每次只看到一对样本,震荡会更明显。这种情况我一般会给判别器加谱归一化,把整个训练搅在一起的黑匣子问题留给第5章细说。
5. 训练避坑:五个让GAN分割翻车的常见问题
5.1 显存溢出:3D卷积的显存是立方级膨胀
现象:运行python train.py不到一个迭代,报错CUDA out of memory。
原因:3D卷积的特征图体积随patch边长三次方增长,96³的patch在第4层特征图已经是6×6×6,但这个过程的中间变量会同时占据显存。很多人习惯了2D网络的参数习惯,把batch size设成8或16,在3D任务里直接爆显存。
解决:先减batch size,再减patch size。--batch_size 1 --patch_size 64是大多数消费级显卡能跑的组合。还不够就用梯度累积,例如--grad_accum 4,把4个小batch的梯度累加后再更新一次参数,等效于batch size 4但不吃显存。MONAI还提供CachedDataset配合pin_memory的优化选项,能再省一点。如果改完仍然OOM,检查是不是复用了两次输入到判别器,生成和真实各一次,显存占用翻倍。
5.2 训练出现NaN:GAN训练不稳定的一大源头
现象:训练日志里loss正常,但某一步后突然变成NaN,之后一路NaN下去。
原因:最常见的两类原因,学习率过大导致loss爆炸;判别器梯度数值失控,梯度惩罚项计算过程出现除以零或无穷大。出现NaN后整段训练已经报废,这时候没有后悔药,只能恢复检查点重新来,所以模型要养成每10个epoch保存一次的习惯。
解决:把生成器和判别器的学习率降到5e-5,同时给生成器梯度做clip,常用做法是torch.nn.utils.clip_grad_norm_(g_model.parameters(), max_norm=12.0)。对WGAN-GP,检查梯度惩罚项里是否对K=1做了幂函数计算,K值过大会让惩罚项爆炸,常见范围是0.1到1.0。这类问题定位时先看log里最后一次正常迭代的loss数值,如果之前已经到几百或上千,那不是指标问题,是训练早就偏了。
5.3 肝脏边缘依旧模糊:说明对抗损失没有真正作用到边界
现象:Dice有0.85以上,但可视化切片里肝脏轮廓和背景过渡区域灰蒙蒙一片,预测概率集中在0.5附近。
原因:对抗损失的权重太低,判别器感受野过大,它只关心整体形状而不关注边界局部细节。另一个可能原因是生成器输出层用了Sigmoid加MSE,这类组合会让模型倾向于输出平滑概率,而不是锐利的0/1边界。
解决:把--lambda_adv从0.01逐步提到0.1或0.2,切换成带边缘项的判别器输入。我常用的做法是把判别器的输入从整张CT图替换为CT图的sobel梯度图与分割图的拼接,让判别器注意力直接落在高梯度区域。梯度图计算用3D Sobel,代码量不大,但它让对抗信号直接作用于边界,效果立竿见影。
5.4 判别器loss为0且保持不动:生成器已经“骗”不过去了
现象:判别器对真实样本输出总为1,对生成样本总为0,D_loss稳定在某个很低值。
原因:判别器训练得太强,生成器一开始输出的结构差距太大,判别器不需要花什么力气就能区分。这时判别器的梯度是饱和的,生成器从它那里得到的反馈几乎为零,训练进入停滞。
解决:先降低判别器学习率,比如d_lr = 0.25 * g_lr;再给判别器加标签平滑,真实标签用0.9而不是1.0;最直接做法是限制判别器的更新频率,生成器每更新2到3次,判别器才更新1次。训练脚本里通常有n_critic参数,从默认1改成3或5,让生成器有更多机会追赶。
5.5 Jupyter Notebook里跑训练直接卡死:内存叠加与默认路径两个坑
现象:在Jupyter里执行训练单元格,几十步之后页面转圈,内核无响应;或重启后模型加载不到,原来保存的路径不存在。
原因:Notebook的内核持续占用显存和内存,你反复执行单元格时旧的DataLoader和新进程叠在一起,内存堆积到系统OOM。另一个很常见但容易被忽略的问题:notebook默认保存路径和train.py读取的路径不一致,工作目录一换,相对路径全部失效,模型权重没被写到你期望的地方。
解决:训练代码不要放在Jupyter单元格里跑,用.py脚本在终端启动,notebook只做数据探索和推理可视化。如果一定要在notebook里跑,先执行gc.collect()释放旧变量,再每个epoch用torch.cuda.empty_cache()清一次缓存。路径问题用这行代码解决:
import os os.chdir(os.path.dirname(os.path.abspath(__file__)))放到脚本最顶部,确保所有相对路径都基于脚本所在目录解析,而不是随Jupyter的工作目录漂移。其实这一点也适用于所有实验,路径如果乱了,比模型不收敛更让人崩溃,因为前者是确定性的错误,要靠重复劳动来补数据。
6. 验证分割结果:Dice之外还要看什么
6.1 二维切片可视化:在Jupyter里直接对比
训练中期开始,每轮结束都用Jupyter加载验证样本,把预测结果画成切片图,比只看Dice数字靠谱得多。肝脏分割的边界质量、过分割欠分割区域都要靠肉眼判断,因为Dice只能反映重叠体积的总体比例,无法体现边缘是锯齿还是光滑、有没有漏掉小的肝段。可视化代码不复杂:
import matplotlib.pyplot as plt import numpy as np def visualize_slice(ct, label, pred, slice_idx): fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(ct[slice_idx], cmap='gray') axes[0].set_title('CT') axes[1].imshow(label[slice_idx], cmap='gray') axes[1].contour(label[slice_idx] > 0, colors='r') axes[1].set_title('Ground Truth') axes[2].imshow(pred[slice_idx], cmap='gray') axes[2].contour(pred[slice_idx] > 0.5, colors='r') axes[2].set_title('Prediction') plt.show() # 加载验证集第一个样本的预测结果 pred = np.load('./results/sample_pred.npy') ct_vol = np.load('./data/preprocessed/sample_img.npy') lab_vol = np.load('./data/preprocessed/sample_lab.npy') visualize_slice(ct_vol, lab_vol, pred, slice_idx=40)轮廓叠加用的是0.5的二值阈值,实际看结果时可以多试0.3和0.7,感受概率图的不同切分对体积的影响。边缘处的弹簧状锯齿说明patch之间存在拼接痕迹,需要在推理时加重叠融合。
6.2 全卷推理的滑动窗口实现
推理时用滑动窗口把完整CT扫描切成重叠patch,逐块预测再拼回,重叠区域取平均值,能有效避免patch边界出现明显的接缝。代码逻辑如下:
def predict_volume(model, volume, patch_size=96, stride=48): z, y, x = volume.shape output = np.zeros_like(volume, dtype=np.float32) weight_map = np.zeros_like(volume, dtype=np.float32) for cz in range(0, z - patch_size + 1, stride): for cy in range(0, y - patch_size + 1, stride): for cx in range(0, x - patch_size + 1, stride): patch = volume[cz:cz+patch_size, cy:cy+patch_size, cx:cx+patch_size] logit = model(torch.tensor(patch).unsqueeze(0).unsqueeze(0).cuda()) output[cz:cz+patch_size, cy:cy+patch_size, cx:cx+patch_size] += torch.sigmoid(logit).cpu().numpy()[0, 0] weight_map[cz:cz+patch_size, cy:cy+patch_size, cx:cx+patch_size] += 1.0 output /= np.maximum(weight_map, 1e-6) return output--stride 48表示patch之间有50%重叠,重叠区域被多次预测、取平均,拼接处的连续性会好很多。边界不足一个patch的地方需要补零或反射填充,否则边缘区域的预测结果会缺失。这个推理代码里没有做torch.no_grad(),在推理时一定要加上,否则会把中间变量全部塞进显存,照样OOM。
最后聊聊我自己的习惯。GAN分割项目跑通到成型,关键不在网络结构抄哪个模型,而在验证闭环做得到不到位——我用的是“先可视化切片、再评估Dice、最后看预测体积和标注体积的差值”这套顺序,因为一眼看上去奇怪的预测通常已经从体积上讲不清了。每次调参前我都会把数据路径、训练命令、hash过的配置参数记录到实验笔记里,防止两周后对着一个0.9的Dice不知道这个分数是哪个版本的代码产生的。如果你现在刚打开这个zip,建议先按第2章的最小命令跑通10个epoch,用第6章的切片可视化看一眼输出,再决定要不要往下投入调参;单看loss曲线或者等待别人口中“这个项目跑出来的状态很好”是不太够的。希望帮到你。
本文还有配套的精品资源,点击获取