首页 / 资讯中心 / 文章详情

基于Matlab的V-Net 3D肺肿瘤分割实战:从数据准备到Dice评估

基于Matlab的V-Net 3D肺肿瘤分割实战:从数据准备到Dice评估 ★ FEATURED ARTICLE
简介这份资源面向计算机、电子信息工程、数学等专业的大学生及医学影像处理初学者提供一套基于Matlab的V-Net神经网络完整实现用于从3D医学图像中完成肺肿瘤语义分割可服务于课程设计、期末大作业与毕业设计等场景。压缩包共12个文件约2.1MB以xml配置与关系文件、png和jpeg图像素材为主配合Matlab脚本与说明文档构成可直接运行的程序结构。代码采用参数化编程网络层结构、训练参数均可按需调整注释清晰便于理解编码器与解码器对称设计、跳跃连接、卷积与反卷积等关键环节。附赠案例数据可直接运行涵盖数据归一化、Dice损失与Adam优化器选择、过拟合缓解及Dice系数评估等流程兼容Matlab2014、2019a与2024a版本。目前已有58人学习适合希望快速上手3D医学图像分割实践、对照代码复现实验并迁移到自身课题的读者。1. 从一份 Matlab 代码包说起V-Net 在 3D 肺肿瘤分割里到底解决什么问题拿到「如何创建和训练 V-Net 神经网络并从 3D 医学图像中对肺肿瘤进行语义分割 Matlab 代码.rar」这个标题时多数人的第一反应是去找一份能跑的代码但真正卡住落地的是三件事3D 数据怎么读进来、V-Net 和普通 2D 语义分割网络差在哪、Matlab 里怎么把训练循环搭起来。肺肿瘤在 CT 里是一个三维连通体单张切片上看它可能只占几十个像素相邻切片之间却存在明显的体积连续性用 2D 卷积逐层预测再堆叠边界处经常出现「切片跳变」这就是 V-Net 这类 3D 全卷积结构存在的理由。它面向的是有 Matlab 基础、手上有 nii 或 nii.gz 格式 CT 数据、想把肺肿瘤语义分割跑通并评估 Dice 的从业者不是只想知道网络结构图的人。下面按数据准备、网络搭建、训练、评估、避坑、进阶的顺序把这条链路拆成能照着复现的步骤。2. V-Net 结构与 Matlab 落地选型为什么不用 2D U-Net 硬堆2.1 V-Net 的核心设计与 3D 语义分割的匹配点V-Net 最早是为前列腺 MRI 体积分割提出的结构上属于编码器-解码器全卷积网络和 U-Net 同源但有三处针对三维数据的改动值得注意。第一卷积核全部是 3D 卷积输入张量形状是[H, W, D, C]Matlab 里对应dlarray的SSCB或SSSCB格式通道维在最后。第二下采样用步长为 2 的 3D 卷积代替最大池化这样梯度能直接回传到降采样路径对体积数据的边界更友好。第三解码器侧使用残差连接把编码器对应层的特征加回解码器缓解 3D 网络层数一深就难训的问题。肺肿瘤分割里肿瘤体积小、与周围血管和胸膜粘连2D 网络在单切片上容易把血管断面误判成肿瘤。3D 卷积能同时看到上下切片把「这个亮块在相邻层是否连续」作为判据误检会明显下降。代价是显存和内存占用按深度线性增长一个128×128×64的 patch单精度浮点就要约 4MBbatch 一上去就爆。所以实际落地时不会整卷输入而是随机裁 patch 训练、滑窗推理。Matlab 从 R2019b 起提供trainNetwork和自定义训练循环两套接口。V-Net 这种带残差相加、非标准上采样的结构用dlnetwork加自定义循环更灵活trainingOptions那套封装反而不好塞自定义损失。选型上我一般建议数据量小于 50 例、想快速验证用dlnetwork 自定义循环数据量大、要做多卡再考虑trainNetwork配合layerGraph。2.2 数据准备把 nii 转成 Matlab 能吃的 4D 数组医学图像常见格式是 NIfTIMatlab 没有内置读取函数常见做法是用niftireadR2017b 之后 Image Processing Toolbox 提供或第三方Tools for NIfTI。读进来后要做三件事强度归一化、方向对齐、标签二值化。% 读取 CT 与标签假设已用 niftiread 得到 vol 和 lbl vol niftiread(case001_ct.nii.gz); % 尺寸 [H, W, D]int16 lbl niftiread(case001_seg.nii.gz); % 0/1 标签 % 1) 肺窗截断CT 值 HU 范围肺窗约 [-1000, 400] vol double(vol); vol(vol -1000) -1000; vol(vol 400) 400; % 2) 归一化到 [0,1]按全局而非单例避免不同病例尺度漂移 vol (vol 1000) / 1400; % 3) 标签二值化并转 single lbl single(lbl 0); % 4) 转成 dlarray格式 SSSCBH,W,D,C,B vol dlarray(reshape(vol, [size(vol,1), size(vol,2), size(vol,3), 1, 1]), SSSCB); lbl dlarray(reshape(lbl, [size(lbl,1), size(lbl,2), size(lbl,3), 1, 1]), SSSCB);这段代码的关键参数是窗宽窗位。肺窗[-1000, 400]是肺实质和肿瘤的常见显示范围如果肿瘤靠近纵隔可以放宽到[-1000, 600]。归一化用全局固定范围而不是按病例 min-max是因为推理时新病例的 min-max 不可预知固定范围能让训练和推理一致。标签转single是为了和网络输出做数值运算logical在自定义损失里容易报类型错误。提示niftiread读出的方向可能和标签不一致务必用niftiinfo检查Affine矩阵方向错了会出现「预测结果整体翻转」这种玄学现象。2.3 用 dlnetwork 搭一个最小可训的 V-Net下面是一个精简版 V-Net 编码器-解码器层数按肺肿瘤 patch 规模裁剪完整版可以按需加深。function net buildVNet(inputSize) % inputSize: [H W D C]例如 [128 128 64 1] layers [ image3dInputLayer(inputSize, Normalization, none, Name, in) % ---- 编码器 ---- conv3dLayer([5 5 5], 16, Stride, 1, Padding, same, Name, e1c) batchNormalizationLayer(Name, e1b) reluLayer(Name, e1r) conv3dLayer([2 2 2], 32, Stride, 2, Padding, same, Name, e2c) batchNormalizationLayer(Name, e2b) reluLayer(Name, e2r) conv3dLayer([2 2 2], 64, Stride, 2, Padding, same, Name, e3c) batchNormalizationLayer(Name, e3b) reluLayer(Name, e3r) % ---- 解码器 ---- transposedConv3dLayer([2 2 2], 32, Stride, 2, Cropping, same, Name, d2t) reluLayer(Name, d2r) transposedConv3dLayer([2 2 2], 16, Stride, 2, Cropping, same, Name, d1t) reluLayer(Name, d1r) conv3dLayer([1 1 1], 1, Name, out) sigmoidLayer(Name, sigm) ]; net dlnetwork(layerGraph(layers)); endconv3dLayer的Stride为 2 时承担下采样Padding设same保证尺寸可预测。transposedConv3dLayer的Cropping设same是为了让上采样后尺寸和编码器对应层对齐否则残差相加会因尺寸不匹配报错。输出层用sigmoid是因为肺肿瘤分割是二分类如果做多类肿瘤、血管、背景就换成softmax并调整输出通道数。注意dlnetwork不接受image3dInputLayer之外的输入层做自定义循环如果要用trainNetwork把image3dInputLayer换成sequenceInputLayer会破坏 3D 结构别混用。3. 训练循环与损失函数Dice 怎么在 Matlab 里写对3.1 自定义训练循环的骨架Matlab 自定义循环的核心是dlfevaldlgradient配合adamupdate。下面是一个可运行的最小骨架。numEpochs 100; learnRate 1e-3; trailingAvg []; trailingAvgSq []; net buildVNet([128 128 64 1]); for epoch 1:numEpochs shuffleIdx randperm(numel(trainList)); for i 1:numel(shuffleIdx) [X, Y] loadPatch(trainList(shuffleIdx(i)), [128 128 64]); [loss, grads] dlfeval(modelLoss, net, X, Y); [net, trailingAvg, trailingAvgSq] adamupdate(net, grads, ... trailingAvg, trailingAvgSq, i, learnRate); end fprintf(Epoch %d, loss %.4f\n, epoch, double(gather(extractdata(loss)))); end function [loss, grads] modelLoss(net, X, Y) Ypred forward(net, X); loss diceLoss(Ypred, Y); grads dlgradient(loss, net.Learnables); endloadPatch负责从整卷里随机裁128×128×64的 patch正样本 patch 和负样本 patch 按 1:1 采样否则肺肿瘤像素占比可能不到 1%网络会直接全预测背景。adamupdate的第四个参数是迭代计数传i而不是epoch学习率衰减才按步数走。3.2 Dice 损失与边界加权Dice 系数对类别不平衡比交叉熵鲁棒但纯 Dice 在训练初期梯度不稳定常见做法是 Dice 二元交叉熵加权。function loss diceLoss(Ypred, Ytrue) epsVal 1e-6; Ypred squeeze(Ypred); Ytrue squeeze(Ytrue); intersection sum(Ypred .* Ytrue, all); union sum(Ypred, all) sum(Ytrue, all); dice (2 * intersection epsVal) / (union epsVal); loss 1 - dice; endepsVal防止分母为零squeeze去掉通道和 batch 维sum(..., all)是 R2018b 之后的写法。如果要做边界加权可以在Ytrue上用imerode和imdilate生成边界带对边界像素的损失乘 2 到 5 倍。肺肿瘤边界模糊加权后 Dice 通常能涨 1 到 2 个百分点但权重超过 5 容易过拟合边界噪声。提示Dice 损失在 batch 很小时方差大batch 小于 4 建议改用 Tversky 损失把假阳性和假阴性分开调。3.3 学习率、patch 尺寸与 batch 的联动这三个参数是联动的不能单独调。patch 从64×64×32加到128×128×64显存占用约 8 倍batch 只能从 8 降到 1此时学习率要相应降到 1e-4 量级否则梯度噪声会让 loss 震荡。我一般按这个顺序定先定显存能承受的最大 patch再定 batch 为 2 到 4最后用 lr finder 找初始学习率。Matlab 里没有现成 lr finder可以手动跑 10 个 epoch学习率从 1e-5 指数增到 1e-2画 loss 曲线找下降最陡的点。4. 推理、后处理与 Dice 评估滑窗怎么拼回整卷4.1 滑窗推理与重叠融合整卷尺寸往往超过显存推理要滑窗。窗口大小和训练 patch 一致步长取窗口的 1/2 到 1/4重叠区用高斯权重融合避免拼接缝。function volPred slidingWindowInfer(net, vol, winSize, stride) [H, W, D] size(vol); volPred zeros(H, W, D, single); weightMap zeros(H, W, D, single); gauss gaussian3d(winSize); % 自定义 3D 高斯核 for h 1:stride(1):H-winSize(1)1 for w 1:stride(2):W-winSize(2)1 for d 1:stride(3):D-winSize(3)1 patch vol(h:hwinSize(1)-1, w:wwinSize(2)-1, d:dwinSize(3)-1); X dlarray(reshape(patch, [winSize, 1, 1]), SSSCB); Yp extractdata(forward(net, X)); Yp squeeze(Yp); volPred(h:hwinSize(1)-1, w:wwinSize(2)-1, d:dwinSize(3)-1) ... volPred(h:hwinSize(1)-1, w:wwinSize(2)-1, d:dwinSize(3)-1) Yp .* gauss; weightMap(h:hwinSize(1)-1, w:wwinSize(2)-1, d:dwinSize(3)-1) ... weightMap(h:hwinSize(1)-1, w:wwinSize(2)-1, d:dwinSize(3)-1) gauss; end end end volPred volPred ./ max(weightMap, 1e-6); endstride取winSize/2是精度和速度的折中取winSize/4精度略升但推理时间翻倍。高斯核让窗口中心权重高、边缘权重低拼接处不会出现硬边。max(weightMap, 1e-6)防止边缘未覆盖区域除零。4.2 连通域后处理与 Dice 计算网络输出是概率图阈值 0.5 二值化后用bwconncomp去掉小于 50 体素的孤立块这些多半是血管断面误检。bw volPred 0.5; cc bwconncomp(bw, 26); % 26 邻域3D 连通 numPix cellfun(numel, cc.PixelIdxList); bw(:, :, :) false; for k find(numPix 50) bw(cc.PixelIdxList{k}) true; end % Dice 计算 inter sum(bw(:) gtVol(:)); dice 2 * inter / (sum(bw(:)) sum(gtVol(:)));26邻域比6邻域更宽松肺肿瘤形状不规则用 26 能保住细长突起。阈值 50 体素是经验值按体素间距换算如果层厚 1mm、层内 0.7mm50 体素约 25 立方毫米小于这个的孤立块基本是噪声。注意Dice 要在原始分辨率上算不要在下采样后的标签上算否则数值会虚高。5. 避坑与排查3D 分割训练里最常见的 5 个翻车现场现象一loss 一直停在 0.69 附近不降。原因通常是标签全为背景或者正样本 patch 采样比例太低。解决打印每个 batch 的sum(Ytrue, all)如果长期为 0检查loadPatch里正样本索引是否写错把正负比例调到 1:1 或 1:2。现象二训练 loss 降但验证 Dice 不涨。多半是过拟合肺肿瘤数据量小网络参数量却按自然图像规模设。解决把编码器通道数从 16/32/64 降到 8/16/32加dropoutLayer(0.3)在编码器末端数据增强加随机旋转 ±15 度和弹性形变。现象三推理结果整体偏移或翻转。这是 NIfTI 方向没对齐的血泪经验。niftiread读出的体数据方向由头信息决定训练和推理用了不同方向就会翻车。解决统一用niftiinfo读Affine用imrotate3或permute对齐到同一方向再送网络。现象四显存够但训练报 out of memory。Matlab 的dlarray默认保留计算图dlfeval里如果忘了extractdata或循环里累积了变量内存会持续涨。解决每个 batch 结束清clear grads Ypred验证阶段用dlarray但包在noGrad语义下Matlab 里用forward而非dlfeval。现象五Dice 在 0.6 到 0.7 卡住上不去。肺肿瘤边界模糊纯 Dice 对边界不敏感。解决加边界加权损失或者把输出层从sigmoid换成softmax做三分类背景、肿瘤核心、边界再合并后处理通常能到 0.75 以上。6. 进阶技巧用 5 折交叉验证和 TTA 把 Dice 再抬一档数据量小的时候单次划分的验证 Dice 波动能到 0.05靠一次结果判断模型好坏不靠谱。我一般做 5 折交叉验证每折训练一个模型推理时对同一病例做 8 次测试时增强TTA原始、三个轴翻转、两个对角翻转概率图平均后再二值化。这套组合在肺肿瘤数据上通常比单模型单次推理高 2 到 4 个百分点。% TTA8 种变换的概率图平均 augList {(x) x, (x) flip(x,1), (x) flip(x,2), (x) flip(x,3), ... (x) flip(flip(x,1),2), (x) flip(flip(x,1),3), ... (x) flip(flip(x,2),3), (x) flip(flip(flip(x,1),2),3)}; probSum zeros(size(vol)); for a 1:numel(augList) volAug augList{a}(vol); probAug slidingWindowInfer(net, volAug, [128 128 64], [64 64 32]); probSum probSum augList{a}(probAug); % 逆变换后累加 end probAvg probSum / numel(augList);逆变换要和正变换一致翻转是对合运算再翻一次就还原。TTA 的代价是推理时间乘 8如果临床场景要求实时可以只保留原始加三个轴翻转这 4 种精度损失约 0.5 个百分点时间省一半。验证方法上除了 Dice我还会看 HD9595% 豪斯多夫距离它对边界离群点敏感能暴露 Dice 掩盖的边界崩坏。Matlab 没有内置 HD95可以用bwdist算距离变换再取分位数。参数上TTA 的权重我一般均等如果某一变换明显更差比如深度轴翻转在层厚大的数据上可以把它权重降到 0.5。最后说个习惯每次改网络结构或损失先跑 10 个 epoch 看 loss 曲线形状形状不对就别浪费算力跑满。肺肿瘤分割这方向数据质量比网络结构重要标注边界差 2 个像素Dice 就能差 0.1拿到数据先花时间核对标签比调参划算。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站