現(xiàn)腹部CT多臟器5分割:數(shù)據(jù)預(yù)處理與模型調(diào)參實(shí)戰(zhàn))
簡介這是一套面向醫(yī)學(xué)圖像分割學(xué)習(xí)者的FCN腹部多臟器五分割完整實(shí)戰(zhàn)資源。項目基于ResNet50/ResNet101作為特征提取網(wǎng)絡(luò)配合余弦退火學(xué)習(xí)率、交叉熵?fù)p失與收斂更快的Adam優(yōu)化器在僅測試20個epoch時全局像素準(zhǔn)確度即達(dá)0.99、mIoU達(dá)0.80若加大訓(xùn)練輪次性能還有進(jìn)一步提升空間。壓縮包共1025個文件、約456.22MB其中991張PNG圖像覆蓋數(shù)據(jù)集與可視化結(jié)果7個Python腳本承擔(dān)訓(xùn)練與推理流程3個pth權(quán)重文件可直接加載另有README、日志與工程配置文件等目錄結(jié)構(gòu)清晰小白也能快速上手。目前已有221人學(xué)習(xí)下載。資源內(nèi)包含完整訓(xùn)練與預(yù)測代碼、預(yù)處理后的數(shù)據(jù)集及訓(xùn)練日志日志中可查看各類別的iou、recall、precision和全局準(zhǔn)確率等指標(biāo)將待推理圖像放入inference目錄后直接運(yùn)行predict.py即可輸出結(jié)果無需額外調(diào)參適合具備一定深度學(xué)習(xí)基礎(chǔ)、希望快速復(fù)現(xiàn)FCN分割任務(wù)的讀者。1. 用 FCN 網(wǎng)絡(luò)做腹部多臟器 5 分割一個模型同時分清四種器官腹部多臟器分割和單器官分割完全是兩碼事FCN 要在同一張 CT 上同時把肝臟、脾臟、左右腎或胃、胰腺取決于標(biāo)簽定義逐像素分開。器官之間密度接近、邊界粘連模型稍有不慎就把兩個器官糊成一片。全卷積網(wǎng)絡(luò)FCN是最早把分類網(wǎng)絡(luò)端到端變成像素級分類的語義分割模型用全卷積化加轉(zhuǎn)置卷積恢復(fù)分辨率天然適合這種“整圖上下文決定邊界”的任務(wù)。這篇實(shí)戰(zhàn)文章面向準(zhǔn)備用 FCN 跑腹部多臟器 5 分割、卻被 NIfTI 預(yù)處理和訓(xùn)練細(xì)節(jié)卡住的工程師從數(shù)據(jù)整理、模型搭建到常見坑完整走一遍。2. 從 NIfTI 到模型輸入腹部 CT 數(shù)據(jù)整理與預(yù)處理細(xì)節(jié)2.1 FCN 的分割原理以及 5 分類任務(wù)的定義FCN 語義分割的完整流程可以拆成兩件事把 VGG 這類分類網(wǎng)絡(luò)里最后幾個全連接層替換成卷積層讓網(wǎng)絡(luò)接受任意尺寸的圖像并輸出二維空間上的類別得分再用轉(zhuǎn)置卷積也叫反卷積把 stride 不斷減小的特征圖一步步放大回輸入分辨率。經(jīng)典版本按上采樣融合深度分成 FCN-32s、FCN-16s、FCN-8s其中 FCN-8s 同時融合了第三、四、五個池化層的輸出淺層細(xì)節(jié)和深層語義都照顧到了在器官邊界這種灰度漸變區(qū)域表現(xiàn)比 32s 穩(wěn)得多。這里說的“5 分割”常規(guī)定義是背景加上四種目標(biāo)器官。比如 Synapse / BTCV 這類公開腹部 CT 多器官數(shù)據(jù)集原始標(biāo)簽覆蓋脾臟、右腎、左腎、膽囊、肝臟、胃、主動脈、胰腺共 8 類你需要把其中 4 個器官映射成類別 14其余器官以及所有非目標(biāo)組織一律歸為背景 0。選哪四個器官取決于臨床目的常見做法是選體積最大、最容易出現(xiàn)邊界粘連的肝、脾、雙腎想提高難度就再加一個胃或胰腺。類別編號順序直接影響后續(xù)混淆矩陣的解讀建議在代碼里用顯式映射表寫清楚不要靠記憶。2.2 讀取 NIfTI 與標(biāo)簽合并保持空間對齊是底線腹部 CT 公開數(shù)據(jù)集大多是 NIfTI 格式一個 volume 是一個三維數(shù)組加一個 affine 矩陣。第一步是把 CT 和分割標(biāo)簽成對讀進(jìn)來做強(qiáng)度歸一化、尺寸縮放和標(biāo)簽合并。最容易翻車的地方是CT 和 seg 的 voxel spacing 不一致時直接 resize 會讓兩個空間錯位訓(xùn)練出的模型邊界會整體偏移一塊。常見做法是先讀兩個文件的 header算出目標(biāo) spacing再用同一套 zoom 因子同時作用于 CT 和標(biāo)簽標(biāo)簽部分必須用最近鄰插值不能引入新的灰度值。import nibabel as nib import numpy as np from scipy.ndimage import zoom # 標(biāo)簽合并規(guī)則Synapse/BTCV 的 8 器官映射到 4 類 背景 LABEL_MAP {1: 1, 2: 2, 3: 3, 4: 4} # 這里把原始標(biāo)簽 1/2/3/4 分別映射成類別 1/2/3/4 # 用不到的 5/6/7/8 以及 0 都保留在背景類里 def load_and_resample(ct_path, seg_path, target_spacing(1.0, 1.0, 1.0)): ct_img nib.load(ct_path) seg_img nib.load(seg_path) ct ct_img.get_fdata().astype(np.float32) seg seg_img.get_fdata().astype(np.int16) # 1) 強(qiáng)度歸一化窗寬窗位把非目標(biāo)灰度壓掉 hu_min, hu_max -200, 200 # 腹部實(shí)質(zhì)器官常用窗位 40、窗寬 400 ct np.clip(ct, hu_min, hu_max) ct (ct - hu_min) / (hu_max - hu_min) # 2) 重采樣按 spacing 比例計算 zoom 因子 ct_spacing ct_img.header.get_zooms()[:3] seg_spacing seg_img.header.get_zooms()[:3] ct_zoom [orig / target for orig, target in zip(ct_spacing, target_spacing)] seg_zoom [orig / target for orig, target in zip(seg_spacing, target_spacing)] ct zoom(ct, zoomct_zoom, order3) # 圖像用三階樣條 seg zoom(seg, zoomseg_zoom, order0) # 標(biāo)簽必須最近鄰 # 3) 標(biāo)簽合并 merged np.zeros_like(seg) for src, dst in LABEL_MAP.items(): merged[seg src] dst return ct, merged邏輯說明代碼先讀雙模態(tài)文件用 clip 把 Hounsfield 值限制在 -200200 HU這是腹部實(shí)質(zhì)器官對比度較明顯的常用范圍然后按兩個文件各自的實(shí)際 spacing 計算 zoom 因子統(tǒng)一到 1mm 各向同性采樣。圖像用三階樣條會帶來亞像素平滑標(biāo)簽用零階最近鄰避免插值產(chǎn)生類別 2.7 這種非法值。最后把 8 類標(biāo)簽合并成 4 類目標(biāo)其余值歸零。參數(shù)說明窗寬窗位不是死的分割胃和腸道時空氣多視野要放寬到 -300300只做肝脾時 -150250 更聚焦。重采樣目標(biāo)建議 1.0mm 或 1.5mm太細(xì)顯存撐不住太粗小器官比如胰腺尾部直接丟像素。zoom 之后 CT 和 seg 可能出現(xiàn) 1 像素尺寸差穩(wěn)妥做法是用切片對齊兩數(shù)組形狀。注意zoom的order0是最近鄰order3是三階樣條。標(biāo)簽絕不能跟著圖像一起用三階否則類別值會被插成小數(shù)交叉熵計算直接報錯。2.3 按病人劃分訓(xùn)練集而不是按切片隨機(jī)分另一個反復(fù)踩的點(diǎn)是數(shù)據(jù)劃分。CT 是三維 volume切片之間高度相關(guān)如果直接用隨機(jī) shuffle 把二維切片分開同一病人的相鄰切片會同時進(jìn)訓(xùn)練集和驗證集模型等于提前“見過”驗證圖像Dice 虛高 5 到 10 個點(diǎn)。正確做法是先按病人編號分組把一個病人的所有層整體分進(jìn)訓(xùn)練/驗證/測試。腹部數(shù)據(jù)集中一個病人動輒上百層按病人劃分后驗證集通常還有幾百張圖足夠評估。增強(qiáng)這里我一般只做輕度隨機(jī)水平翻轉(zhuǎn)、旋轉(zhuǎn) ±10 度、亮度擾動。腹部器官解剖位置固定上下翻轉(zhuǎn)會把肝臟翻到左邊違背生理結(jié)構(gòu)模型反而學(xué)偏。彈性形變對醫(yī)學(xué)圖像有用但早期不建議用FCN 上它會破壞連續(xù)邊界。增強(qiáng)操作必須在同一個隨機(jī)狀態(tài)下同時作用于 CT 和標(biāo)簽否則增強(qiáng)完標(biāo)簽就對不上。import numpy as np from scipy.ndimage import rotate def augment(img_slice, seg_slice, angle_range10): # img_slice: (1, H, W) 單通道圖像seg_slice: (H, W) 標(biāo)簽 seed np.random.randint(0, 2**32) rng np.random.default_rng(seed) if rng.random() 0.5: img_slice np.flip(img_slice, axis2) # 水平翻轉(zhuǎn) seg_slice np.flip(seg_slice, axis1) angle rng.uniform(-angle_range, angle_range) img_slice rotate(img_slice, angle, axes(2, 1), reshapeFalse, order1) seg_slice rotate(seg_slice, angle, axes(1, 0), reshapeFalse, order0) return img_slice.copy(), seg_slice.copy()邏輯說明同一個seed生成的隨機(jī)數(shù)同時決定翻轉(zhuǎn)和旋轉(zhuǎn)圖像和標(biāo)簽永遠(yuǎn)不會錯位。圖像旋轉(zhuǎn)用一階線性插值標(biāo)簽用零階最近鄰原因和重采樣相同。reshapeFalse保證旋轉(zhuǎn)后尺寸不變不會產(chǎn)生裁剪對齊問題。參數(shù)說明angle_range10對腹部 CT 夠用超過 15 度會把器官形態(tài)拉得太畸形。如果你覺得手寫增強(qiáng)麻煩直接用 albumentations 的RandomHorizontalFlip和Rotate同時傳入image和mask兩個參數(shù)效果一樣。2.4 三維體積切成二維切片后的 Dataset 封裝FCN 的輸入是單張二維圖像CT 是幾十上百層的三維體積所以訓(xùn)練管線里必須有一個“切層”的動作。最常見做法是沿軸狀位axial腳往頭方向看逐層切把每一層 CT 和對應(yīng)標(biāo)簽當(dāng)成一個樣本。注意要保留病人 ID 和層號否則后期做三維后處理時無法把概率圖拼回完整體積。from torch.utils.data import Dataset class AbdominalDataset(Dataset): def __init__(self, case_list, transformNone): self.items [] for case_id in case_list: ct, seg load_and_resample( fdata/ct/{case_id}.nii.gz, fdata/seg/{case_id}.nii.gz ) for z in range(ct.shape[2]): self.items.append((case_id, z, ct[:, :, z], seg[:, :, z])) self.transform transform def __len__(self): return len(self.items) def __getitem__(self, idx): _, z, ct_slice, seg_slice self.items[idx] ct_slice ct_slice[None, ...].astype(np.float32) # (1, H, W) seg_slice seg_slice.astype(np.int64) # (H, W) if self.transform is not None: ct_slice, seg_slice self.transform(ct_slice, seg_slice) return ct_slice, seg_slice邏輯說明構(gòu)造函數(shù)里提前把每個病人的每層切好放入內(nèi)存適合單機(jī)小數(shù)據(jù)量數(shù)據(jù)量大時應(yīng)該改成__init__只存路徑__getitem__再去nibabel讀取。seg_slice的 dtype 必須是int64PyTorch 的交叉熵不接受 float 類型的標(biāo)簽索引這個細(xì)節(jié)讓不少人訓(xùn)練時報錯。3. 搭一個 FCN-8sVGG16 主干、轉(zhuǎn)置卷積與損失函數(shù)3.1 為什么選 VGG16 做主干而不是白手起家或直接上 ResNetFCN 的原始實(shí)現(xiàn)基于 VGG16后續(xù)很多工作換成 ResNet 主干能拿到更高精度。但對腹部多臟器 5 分割這個任務(wù)我的建議是先把 VGG16 版本跑通。原因有三一是 VGG16 的 ImageNet 預(yù)訓(xùn)練權(quán)重遷移到醫(yī)學(xué)灰度圖上前幾層提取的低級邊緣紋理特征依然可靠二是 VGG16 結(jié)構(gòu)規(guī)整各 stage 邊界清晰做跳躍連接時不容易數(shù)錯層數(shù)三是顯存占用相對可控256 輸入、batch 8 在單張 11G 顯卡上能跑。ResNet 的殘差結(jié)構(gòu)理論上更好但 FCN 的框架本身就是“先學(xué)會怎么上采樣”主干換得越晚調(diào)試成本越低。FCN-32s 直接對第五個池化層的輸出做 32 倍上采樣邊界非常糊FCN-16s 融合了 pool4 的 2 倍上采樣結(jié)果FCN-8s 再融合 pool3額外補(bǔ)上 4 像素級別的細(xì)節(jié)。腹部器官邊界處的灰度差本來就小我直接用 FCN-8s代價只是多兩條跳躍連接訓(xùn)練收益卻很明顯。3.2 模型定義PyTorch 手寫 FCN-8s下面是基于 torchvision VGG16 features 構(gòu)造 FCN-8s 的完整代碼。注意features是一個Sequential內(nèi)部每個模塊的下標(biāo)是固定的拆成五個 stage 時不要數(shù)錯。import torch import torch.nn as nn from torchvision.models import vgg16 class FCN8s(nn.Module): def __init__(self, n_class5, pretrainedTrue): super().__init__() features list(vgg16(pretrainedpretrained).features.children()) # VGG16 features 共 31 層按 pool 位置拆成 5 段 self.stage1 nn.Sequential(*features[:5]) # conv1 到 pool1 self.stage2 nn.Sequential(*features[5:10]) # conv2 到 pool2 self.stage3 nn.Sequential(*features[10:17]) # conv3 到 pool3 self.stage4 nn.Sequential(*features[17:24]) # conv4 到 pool4 self.stage5 nn.Sequential(*features[24:31]) # conv5 到 pool5 # 用卷積替換原 FC6 / FC7保持 4096 通道 self.conv6 nn.Conv2d(512, 4096, kernel_size7, padding3) self.relu6 nn.ReLU(inplaceTrue) self.drop6 nn.Dropout2d(0.5) self.conv7 nn.Conv2d(4096, 4096, kernel_size1) self.relu7 nn.ReLU(inplaceTrue) self.drop7 nn.Dropout2d(0.5) # 三個下采樣層各出一個 1x1 分類頭 self.score_pool3 nn.Conv2d(256, n_class, 1) self.score_pool4 nn.Conv2d(512, n_class, 1) self.score_pool5 nn.Conv2d(4096, n_class, 1) # 兩個 2 倍轉(zhuǎn)置卷積 一個 8 倍轉(zhuǎn)置卷積 self.upsample2 nn.ConvTranspose2d(n_class, n_class, kernel_size4, stride2, padding1) self.upsample_pool4 nn.ConvTranspose2d(n_class, n_class, kernel_size4, stride2, padding1) self.upsample8 nn.ConvTranspose2d(n_class, n_class, kernel_size16, stride8, padding4) def forward(self, x): pool1 self.stage1(x) # H/2 pool2 self.stage2(pool1) # H/4 pool3 self.stage3(pool2) # H/8 pool4 self.stage4(pool3) # H/16 pool5 self.stage5(pool4) # H/32 v self.relu6(self.conv6(pool5)) v self.drop6(v) v self.relu7(self.conv7(v)) v self.drop7(v) s5 self.score_pool5(v) # H/32, n_class s5 self.upsample2(s5) # H/16, n_class s4 self.score_pool4(pool4) # H/16, n_class # 對齊最后一像素 s5 s5 s4[:, :, :s5.size(2), :s5.size(3)] s4 self.upsample_pool4(s5) # H/8, n_class s3 self.score_pool3(pool3) # H/8, n_class s4 s4 s3[:, :, :s4.size(2), :s4.size(3)] out self.upsample8(s4) # H, n_class return out邏輯說明前向里所有尺寸按輸入 256 假定如果輸入不是 256 的整數(shù)倍最后一兩個像素會出現(xiàn) 1 像素差切片對齊是通用保險手段。stage 拆分嚴(yán)格對應(yīng) VGG16 features 的 31 層想換 ResNet 時把 features 換成 resnet50 的 layer1layer4再在 score head 上做對應(yīng)通道數(shù)替換。參數(shù)說明conv6的kernel_size7保留了原 FC6 的大感受野padding3保證特征圖尺寸不變?nèi)齻€ score head 都是 1x1 卷積本質(zhì)是在每個空間位置對特征做類別打分。upsample8的kernel_size16與stride8配合輸出尺寸滿足out (in - 1) * stride kernel - 2 * padding。輸入改成 512 時 kernel 和 padding 不用改顯存會翻 4 倍。3.3 損失函數(shù)交叉熵極易偏向大器官腹部多臟器分割最典型的現(xiàn)象是“肝臟分割很好胃和胰腺幾乎為零”。原因是背景、肝臟、脾臟占了訓(xùn)練樣本里絕大多數(shù)像素普通交叉熵會把梯度集中在大類別上。我通常用 Dice Loss 和交叉熵的組合并對小器官類別額外加權(quán)重這個思路在 fcn 語義分割模型里同樣適用。import torch.nn.functional as F class DiceCEWithWeights(nn.Module): def __init__(self, n_class5, class_weightsNone, smooth1.0): super().__init__() if class_weights is None: class_weights torch.tensor([0.1, 1.0, 1.0, 1.5, 1.5]) self.class_weights class_weights self.smooth smooth self.n_class n_class def forward(self, pred, target): # pred: (B, C, H, W) logitstarget: (B, H, W) long prob F.softmax(pred, dim1) target_onehot F.one_hot(target, self.n_class).permute(0, 3, 1, 2).float() inter (prob * target_onehot).sum(dim(2, 3)) union prob.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) dice (2.0 * inter self.smooth) / (union self.smooth) # 按類別權(quán)重累加背景 0 給最小權(quán)重 dice_loss (self.class_weights.to(pred.device) * (1 - dice)).mean() ce F.cross_entropy(pred, target, weightself.class_weights.to(pred.device)) return ce dice_loss邏輯說明one-hot 在通道維展開Dice 逐類計算背景權(quán)重給 0.1避免背景主導(dǎo)肝臟、脾臟給 1.0胃和胰腺給 1.5。交叉熵同時用 weight 加權(quán)。這個設(shè)計在訓(xùn)練初期能明顯改善小器官的召回率。參數(shù)說明class_weights要根據(jù)實(shí)際標(biāo)簽里各類像素占比重新統(tǒng)計通用做法是統(tǒng)計占比后取倒數(shù)再歸一化。smooth1.0是平滑項防止分母為零。target不能是 one-hot 后的 float交叉熵只接受類索引的 long 張量。訓(xùn)練循環(huán)部分和優(yōu)化器選擇核心就三行optimizer torch.optim.SGD(model.parameters(), lr0.001, momentum0.9, weight_decay1e-4) criterion DiceCEWithWeights(n_class5) model FCN8s(n_class5).cuda()SGD比 Adam 更容易穩(wěn)定收斂到平坦最優(yōu)點(diǎn)配合 poly 學(xué)習(xí)率衰減lr * (1 - iter/total)^0.9用 60 到 100 個 epoch。如果發(fā)現(xiàn)訓(xùn)練震蕩先把lr降到 3e-4別的先別動。4. 訓(xùn)練、調(diào)參與排查從顯存爆炸到標(biāo)簽錯位的四個翻車現(xiàn)場4.1 訓(xùn)練參數(shù)速查表下面這組默認(rèn)參數(shù)在單張 11G 顯存上能穩(wěn)定跑通參數(shù)取值說明輸入尺寸256x256保留腹部實(shí)質(zhì)器官輪廓的最小尺寸batch size8小于 4 時 BN 不穩(wěn)定優(yōu)化器SGD, momentum0.9比 Adam 更容易收斂初始學(xué)習(xí)率1e-3配 poly 衰減訓(xùn)練輪數(shù)60~100驗證集 Dice 不再上升即停數(shù)據(jù)增強(qiáng)水平翻轉(zhuǎn) / 旋轉(zhuǎn)±10° / 亮度±20%禁止上下翻轉(zhuǎn)損失權(quán)重0.1 / 1.0 / 1.0 / 1.5 / 1.5按各類像素比例調(diào)整訓(xùn)練驗證劃分按病人嚴(yán)禁按切片隨機(jī) shuffle監(jiān)控指標(biāo)建議每 epoch 打印每個類別的 Dice而不是只看平均 Dice。平均 Dice 會被肝脾拉高小器官翻車時根本看不出來。訓(xùn)練節(jié)奏上前 10 個 epoch 總 loss 應(yīng)明顯下降Dice 從 0.2 附近爬到 0.7 左右如果 loss 降而 Dice 不動先檢查標(biāo)簽是不是大部分都是背景。4.2 顯存不足batch 調(diào)到 2 仍然爆顯存現(xiàn)象輸入 512、batch 2加載模型后直接報 CUDA out of memory。原因VGG16 的conv6是 4096 通道的 7x7 卷積特征圖通道數(shù)大顯存峰值出現(xiàn)在上采樣前的分類頭附近。不是模型寫錯是容量設(shè)計超過了顯存。解決先把輸入降到 256batch 設(shè)為 8還不夠就用梯度累積每 4 個 batch 更新一次參數(shù)模擬 batch 32。最后才是換 ResNet 主干降低通道數(shù)。torch.cuda.empty_cache()釋放不了顯存中被 PyTorch 緩存分配器占用的塊換參數(shù)后重啟訓(xùn)練腳本通常更有效。4.3 驗證集 Dice 虛高換新病人就掉點(diǎn)現(xiàn)象按切片隨機(jī)劃分訓(xùn)練 10 個 epoch 驗證集肝臟 Dice 0.97換一個新病人測試掉到 0.85。原因同一病人的相鄰切片被同時分進(jìn)訓(xùn)練集和驗證集相鄰層幾乎就是復(fù)制粘貼模型提前看到了驗證分布。這是腹部數(shù)據(jù)集最容易出現(xiàn)的“虛假高分”比過擬合更難發(fā)現(xiàn)。解決在 2.3 節(jié)已經(jīng)強(qiáng)調(diào)過按case_id整體劃分。把劃分代碼放在預(yù)處理最前面打印每個集合的病人 ID 確認(rèn)沒有重疊。我第一版訓(xùn)練時驗證集“完美”后來按病人重劃分直接掉了 5 個點(diǎn)那以后我再也不敢隨機(jī)切了。4.4 小器官 Dice 接近 0胃和胰腺全是噪點(diǎn)現(xiàn)象背景、肝臟、脾臟 Dice 正常胃和胰腺的預(yù)測結(jié)果全是散落噪點(diǎn)。原因小器官像素占比可能只有 1%普通交叉熵的梯度被大器官和背景淹沒模型根本不敢預(yù)測小類別。解決換成 3.3 節(jié)的 DiceCE 加權(quán)損失batch size 調(diào)大讓每個 batch 盡量包含多個含小器官的切片訓(xùn)練前統(tǒng)計每類像素占比如果胃和胰腺確實(shí)太低對小器官切片做 3 倍過采樣比單純調(diào)權(quán)重更直接。4.5 標(biāo)簽錯位所有預(yù)測邊界整體外擴(kuò)現(xiàn)象預(yù)測邊界比真實(shí)標(biāo)簽向外擴(kuò)一圈每個器官都一樣。原因CT 和 seg 的zoom因子不一致或者重采樣后沒有重新對齊尺寸。訓(xùn)練初期如果不做可視化這類系統(tǒng)性誤差很難發(fā)現(xiàn)。解決檢查load_and_resample里兩個文件是否用了同一組 zoom 因子在每個 epoch 開始前隨機(jī)抽 3 張圖把 CT 和標(biāo)簽疊加可視化??床坏竭@一步后面的調(diào)參全是玄學(xué)??梢暬梢院唵斡胮lt.imshow(ct_slice, cmapgray)再疊一條標(biāo)簽輪廓。4.6 上采樣出現(xiàn)棋盤格紋理現(xiàn)象輸出概率圖有規(guī)則的棋盤格狀紋理疊加在器官邊界上。原因轉(zhuǎn)置卷積的 kernel 重疊不均勻stride 和 kernel 大小搭配不當(dāng)尤其是最后 8 倍上采樣時放大倍數(shù)大棋盤格更明顯。解決把最后的upsample8替換成nn.Upsample(scale_factor8, modebilinear, align_cornersTrue)前兩個 2 倍上采樣保留轉(zhuǎn)置卷積。棋盤格對 Dice 影響不大但會干擾后期三維重建和臨床展示。5. 評估指標(biāo)、CRF 后處理與遷移到自己的數(shù)據(jù)5.1 用每類 Dice 和 mIoU 代替“整體準(zhǔn)確率”像素準(zhǔn)確率在這種嚴(yán)重類別不平衡的任務(wù)里沒有任何參考價值背景占 90% 的時候一直預(yù)測背景也有 90% 的準(zhǔn)確率。我習(xí)慣每類分別算 Dice再算排除背景后的 mIoUdef compute_dice(pred, target, n_class5): dice_per_class [] for c in range(1, n_class): p (pred c) t (target c) inter (p t).sum() union p.sum() t.sum() dice_per_class.append((2.0 * inter 1e-6) / (union 1e-6)) return dice_per_class def compute_miou(pred, target, n_class5): ious [] for c in range(1, n_class): p (pred c) t (target c) inter (p t).sum() union (p | t).sum() ious.append(inter / (union 1e-6)) return np.mean(ious)邏輯說明背景類不參與計算因為它占比太高會稀釋指標(biāo)。評估時用測試集所有切片的預(yù)測結(jié)果在像素級累計再計算每類 Dice而不是對每張圖算完再取平均后者會被切片數(shù)量平衡掉權(quán)重。5.2 CRF 后處理在腹部 CT 上收益有限早期語義分割教程普遍推薦 DenseCRF 做后處理但我在腹部 CT 上的經(jīng)驗是收益很小甚至翻車。腹部器官邊界不是顏色突變而是灰度漸變CRF 的依據(jù)是“相鄰像素強(qiáng)度接近則標(biāo)簽一致”它會把本來就是漸變的器官邊界“糊”得更平。如果預(yù)測圖有破碎噪聲先試 3x3 或 5x5 中值濾波無效再考慮 CRF做 CRF 時 pairwise 項權(quán)重從 3 起步別一上來就調(diào)到 10。5.3 遷移到新數(shù)據(jù)集的最小改動路徑換一個腹部數(shù)據(jù)集時改動的順序應(yīng)該是標(biāo)簽映射、窗寬窗位、重采樣 spacing、類別權(quán)重統(tǒng)計backbone 預(yù)訓(xùn)練權(quán)重可以保留。數(shù)據(jù)量少時先凍結(jié) stage1stage3 只訓(xùn)練 score head 和上采樣層驗證集 Dice 開始上升后再解凍全部層學(xué)習(xí)率同步降到 1e-4。一個有用的習(xí)慣是把每個病人的預(yù)處理結(jié)果CT 和標(biāo)簽疊加導(dǎo)出一批 PNG 存到debug/目錄換數(shù)據(jù)集時先看這幾十張圖再動手訓(xùn)練。評估時把每個 case 的每層 Dice 畫成曲線能直觀看出模型是在哪一層、哪個器官開始壞的。這個方向值不值得投入取決于你手頭數(shù)據(jù)的標(biāo)注質(zhì)量。FCN 8s 不是精度天花板但它是把數(shù)據(jù)管線、損失設(shè)計和評估流程扎穩(wěn)的底座底座穩(wěn)了后面換 U-Net、換 Transformer 都是一兩天的事。我第一版訓(xùn)練吃了按切片隨機(jī)分的虧得到一個虛假的 0.93 Dice后來按病人重劃分掉到 0.87從那以后預(yù)處理每一步都必須可視化看不見的邊界偏移和標(biāo)簽錯位一定會變成訓(xùn)練后的事后后悔藥。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取