練測試數(shù)據(jù)集全流程指南:從數(shù)據(jù)組織到推理調(diào)優(yōu))
簡介一套面向深度學(xué)習(xí)道路提取任務(wù)的nnU-Net格式訓(xùn)練與測試數(shù)據(jù)集源自馬薩諸塞道路遙感數(shù)據(jù)集Massachusetts Roads Dataset適合使用nnU-Net框架進(jìn)行遙感圖像分割的研究者與工程師。數(shù)據(jù)包共59個(gè)文件含58張PNG圖像和1份dataset.json配置壓縮后約142.02MB目錄嚴(yán)格按nnU-Net規(guī)范組織imagesTr/imagesTs存放訓(xùn)練與測試影像labelsTr/labelsTs存放道路標(biāo)注掩膜訓(xùn)練與測試集劃分清晰默認(rèn)命名符合nnU-Net預(yù)處理要求可直接被讀取使用。影像覆蓋城市、郊區(qū)和農(nóng)村等多種地形道路標(biāo)簽由OpenStreetMap道路中心線柵格化生成適合檢驗(yàn)?zāi)P驮跇淠菊趽?、陰影等?fù)雜場景下的提取能力。目前已有1772人瀏覽學(xué)習(xí)拿到后無需格式轉(zhuǎn)換即可用于道路分割實(shí)驗(yàn)也可作為遙感語義分割基準(zhǔn)數(shù)據(jù)集對比不同算法。1. nnunet訓(xùn)練測試數(shù)據(jù)集為什么這個(gè)框架能“開箱即用”“nnunet訓(xùn)練測試數(shù)據(jù)集”這幾個(gè)詞放在一起容易讓人誤以為這是某個(gè)現(xiàn)成項(xiàng)目的操作手冊。實(shí)際經(jīng)歷過的人都知道nnU-Net最反直覺的地方在于它不靠發(fā)明新網(wǎng)絡(luò)結(jié)構(gòu)而是靠一套自動(dòng)配置流程把預(yù)處理、訓(xùn)練策略和推理參數(shù)按數(shù)據(jù)集特征給你定好。你只需要按它的規(guī)矩把數(shù)據(jù)集擺放整齊、寫好dataset.json剩下的plan、train、predict三步命令就能跑起來。對做醫(yī)學(xué)影像分割、又不想在參數(shù)調(diào)優(yōu)上耗太多時(shí)間的團(tuán)隊(duì)來說這套流程省掉的不是幾分鐘是幾周。我按自己慣用的落地路徑來寫數(shù)據(jù)集怎么組織、預(yù)處理怎么跑、訓(xùn)練怎么訓(xùn)、測試推理怎么出結(jié)果以及最容易返工的那幾個(gè)坑。2. 把數(shù)據(jù)集整理成nnU-Net要的樣子任務(wù)目錄與JSON配置2.1 從原始標(biāo)注到nnU-Net的數(shù)據(jù)組織labelsTr、imagesTr與dataset.jsonnnU-Net v2的數(shù)據(jù)組織是按“任務(wù)Task”管理的。每個(gè)任務(wù)是一個(gè)獨(dú)立的文件夾放在nnUNet_raw_data目錄底下命名規(guī)則是Task序號(hào)_任務(wù)名稱比如Task001_liver或Task002_brain。序號(hào)就是后面plan和train命令里用的任務(wù)編號(hào)名稱是給你自己看的兩個(gè)部分用下劃線連接。文件夾內(nèi)部固定是imagesTr、labelsTr、imagesTs三個(gè)目錄分別放訓(xùn)練圖像、訓(xùn)練標(biāo)簽和測試圖像命名中的Tr是training的縮寫Ts是testing的縮寫。nnU-Net只吃NIfTI格式.nii.gz其他格式需要先轉(zhuǎn)這個(gè)沒得商量。文件名要按“病例名_模態(tài)標(biāo)識(shí)符_病例名”來組織。比如一個(gè)病例叫case_001的CT圖像在imagesTr里就叫case_001_0000.nii.gz其中_0000代表模態(tài)序號(hào)0。標(biāo)簽文件在labelsTr里就叫case_001.nii.gz。這里最容易翻車的是圖像文件和標(biāo)簽文件名必須完全對應(yīng)但標(biāo)簽不能帶模態(tài)后綴。你有一張case_001_0000.nii.gz就必須有一張case_001.nii.gz在labelsTr里否則訓(xùn)練時(shí)nnU-Net報(bào)“找不到配對”的錯(cuò)誤。另一個(gè)容易忽略的規(guī)則是case_001這個(gè)名稱本身不能再包含下劃線因?yàn)閚nU-Net解析文件名時(shí)靠下劃線切分字段case名字里再帶下劃線會(huì)讓解析邏輯直接混亂。下面的小腳本可以快速把散落的圖像和標(biāo)簽整理成這個(gè)結(jié)構(gòu)import shutil from pathlib import Path raw_root Path(raw_data) # 原始圖像目錄 label_root Path(raw_label) # 原始標(biāo)簽?zāi)夸?target Path(nnUNet_raw/nnUNet_raw_data/Task001_demo) (target / imagesTr).mkdir(parentsTrue, exist_okTrue) (target / labelsTr).mkdir(parentsTrue, exist_okTrue) (target / imagesTs).mkdir(parentsTrue, exist_okTrue) for i, img_path in enumerate(sorted(raw_root.glob(*.nii.gz))): case_id img_path.stem.split(.)[0] # 取文件名當(dāng)作病例名 tgt_img target / fimagesTr/{case_id}_0000.nii.gz shutil.copy(img_path, tgt_img) # 找到同名標(biāo)簽 lab_path label_root / f{case_id}.nii.gz if lab_path.exists(): shutil.copy(lab_path, target / flabelsTr/{case_id}.nii.gz) else: print(f缺少標(biāo)簽: {case_id})這段腳本做的事情遍歷原始圖像目錄把每個(gè).nii.gz文件復(fù)制到imagesTr并自動(dòng)加上_0000后綴同時(shí)檢查同名標(biāo)簽是否存在存在的復(fù)制到labelsTr不存在的單獨(dú)打印出來提醒你補(bǔ)數(shù)據(jù)。我用它處理過一次幾百例的肝臟分割數(shù)據(jù)唯一要注意的是case_id不能包含下劃線以外的特殊字符否則訓(xùn)練階段解析文件名時(shí)會(huì)異常。2.2 寫dataset.json的字段channel_names、labels與file_ending每個(gè)Task目錄下都要有一個(gè)dataset.jsonnnU-Net從plan階段開始就讀它。最核心的三個(gè)字段是channel_names、labels和file_ending。channel_names描述每個(gè)模態(tài)通道是什么單模態(tài)CT就是[CT]多模態(tài)比如MRI的T1和T2就寫[T1,T2]順序要和文件名里的_0000、_0001一一對應(yīng)。labels是字典從0開始0必須是background后面的1、2就是你要分割的解剖結(jié)構(gòu)。file_ending固定寫.nii.gz。{ channel_names: { 0: CT }, labels: { 0: background, 1: liver, 2: tumor }, numTraining: 120, file_ending: .nii.gz }這里的numTraining要和imagesTr里實(shí)際圖像數(shù)量一致不一致時(shí)plan階段會(huì)警告。labels的鍵必須是字符串值是類別名建議用英文方便在日志里識(shí)別。多模態(tài)場景常見做法是把MRI的多個(gè)序列按固定順序排列比如T1加權(quán)、T2加權(quán)、FLAIR分別作為_0000、_0001、_0002同一病例的所有模態(tài)都必須存在缺失一個(gè)模態(tài)時(shí)nnU-Net不會(huì)替你補(bǔ)全訓(xùn)練會(huì)以報(bào)錯(cuò)告終。如果你處理的是帶子區(qū)域的分割任務(wù)比如肝臟和肝臟腫瘤分別標(biāo)注還可以在dataset.json里額外寫regions字段來描述子區(qū)域歸屬但這個(gè)字段是可選的新手階段先不用管。2.3 檢查圖像與標(biāo)簽形狀跑plan之前先做自檢數(shù)據(jù)放好、json寫完之后我不會(huì)立刻跑plan_and_preprocess而是先做一個(gè)自檢把每個(gè)病例的圖像和標(biāo)簽都load進(jìn)來比對shape和spacing。因?yàn)閚nU-Net在訓(xùn)練階段會(huì)自動(dòng)把圖像裁剪到非零區(qū)域但如果你圖像的尺寸和標(biāo)簽尺寸不一致等到預(yù)處理重采樣之后才發(fā)現(xiàn)再回去改數(shù)據(jù)就晚了。下面這個(gè)腳本遍歷一遍import nibabel as nib from pathlib import Path img_dir Path(nnUNet_raw/nnUNet_raw_data/Task001_demo/imagesTr) lab_dir Path(nnUNet_raw/nnUNet_raw_data/Task001_demo/labelsTr) for img_path in sorted(img_dir.glob(*.nii.gz)): case_id img_path.name.replace(_0000.nii.gz, ) lab_path lab_dir / f{case_id}.nii.gz if not lab_path.exists(): print(f標(biāo)簽缺失: {case_id}) continue img nib.load(img_path) lab nib.load(lab_path) if img.shape ! lab.shape: print(fshape不一致: {case_id} img{img.shape} lab{lab.shape}) else: print(fOK: {case_id} {img.shape})這一步花五分鐘能避免后面plan階段報(bào)一堆形狀不匹配的錯(cuò)。shape不一致通常是因?yàn)樵紭?biāo)注軟件把標(biāo)簽裁剪過或重采樣過常見做法是把標(biāo)簽重新resample到圖像的空間建議用SimpleITK的Resample插值方式選最近鄰避免引入新的標(biāo)簽值。多模態(tài)數(shù)據(jù)還要把每個(gè)模態(tài)的shape也互相核對一遍我遇到過T1和T2序列采集范圍不同導(dǎo)致shape不一致的情況這種病例要么裁到共同區(qū)域要么干脆剔除。3. 跑通預(yù)處理與訓(xùn)練從nnUNet_plan_and_preprocess到nnUNet_train3.1 安裝與環(huán)境變量nnUNet_raw、nnUNet_preprocessed、nnUNet_results三件套nnU-Net v2用pip安裝命令是pip install nnunetv2它的命令行工具前綴是nnUNet_。要跑起來必須先設(shè)置三個(gè)環(huán)境變量。nnUNet_raw對應(yīng)上面放原始數(shù)據(jù)的目錄nnUNet_preprocessed是預(yù)處理輸出目錄plan階段生成的plans.json和預(yù)處理后的數(shù)組都落在那里nnUNet_results是訓(xùn)練結(jié)果目錄權(quán)重和日志都在里面。這三個(gè)目錄建議都放到同一塊剩余空間足夠的盤上因?yàn)轭A(yù)處理后的數(shù)據(jù)往往比原始數(shù)據(jù)大好幾倍。export nnUNet_raw/data/nnUNet/nnUNet_raw export nnUNet_preprocessed/data/nnUNet/nnUNet_preprocessed export nnUNet_results/data/nnUNet/nnUNet_results這三個(gè)環(huán)境變量在每個(gè)終端都要export一遍如果忘記設(shè)置命令會(huì)直接提示找不到路徑。我一般把它們寫進(jìn)~/.bashrc省得每次重開終端都要敲一遍。另外要注意nnunetv1和v2的命令不兼容網(wǎng)上一搜一大把舊教程里nnUNet_plan_and_preprocess的用法是v1風(fēng)格v2里已經(jīng)統(tǒng)一成nnUNet_plan_and_preprocess -t 任務(wù)ID這種寫法不需要再手動(dòng)指定數(shù)據(jù)集名稱。3.2 nnUNet_plan_and_preprocess數(shù)據(jù)指紋與自動(dòng)配置預(yù)處理階段做的事情是掃描所有數(shù)據(jù)的統(tǒng)計(jì)信息形狀、spacing、強(qiáng)度分布、前景占比然后在3d_fullres、3d_lowres、2d這些配置里選出適合當(dāng)前數(shù)據(jù)的方案同時(shí)完成重采樣、裁剪、z-score歸一化。這一步的輸出是預(yù)處理后的數(shù)據(jù)、plans.json和dataset.json副本。命令很簡單nnUNet_plan_and_preprocess -t 1 --verify_dataset_integrity-t 1對應(yīng)Task001那個(gè)序號(hào)--verify_dataset_integrity是額外做一次數(shù)據(jù)完整性校驗(yàn)會(huì)檢查每個(gè)病例的圖像和標(biāo)簽是否一一對應(yīng)、尺寸是否一致比2.3節(jié)那個(gè)自檢腳本更嚴(yán)格。跑完之后看nnUNet_preprocessed/Task001_demo目錄里面應(yīng)該出現(xiàn)一個(gè)plans.json和一堆預(yù)處理后的.npz、.npy文件。預(yù)處理時(shí)間取決于數(shù)據(jù)量和分辨率幾十例CT大概十幾分鐘幾百例高分辨率MRI可能要跑幾個(gè)小時(shí)這個(gè)速度是正常的別看到進(jìn)度條不動(dòng)就去CtrlC。plan階段不是所有配置都會(huì)跑。如果數(shù)據(jù)量和前景區(qū)域都不滿足3d_lowres的條件nnU-Net會(huì)自動(dòng)跳過低分辨率配置只跑2d和3d_fullres。這一步看起來像個(gè)黑匣子但它其實(shí)把傳統(tǒng)調(diào)參過程里“圖像應(yīng)該重采樣成什么大小”“l(fā)oss怎么配”“要不要用級(jí)聯(lián)”這些問題都自動(dòng)回答了。對新手來說不需要手動(dòng)干預(yù)對想干預(yù)的熟手plans.json里的preprocessor_name、resampling_scheme這些字段都可以改但改完要能說得清為什么否則不如不改。3.3 nnUNet_train開始訓(xùn)練配置、Trainer與fold參數(shù)預(yù)處理完成后就能訓(xùn)練了。nnUNet_train命令格式是nnUNet_train 3d_fullres nnUNetTrainerV2 1 0第一個(gè)參數(shù)是配置名3d_fullres是完整分辨率的三維模型數(shù)據(jù)少或顯存小時(shí)用2d第二個(gè)參數(shù)是Trainer類一般就寫nnUNetTrainerV2想換loss或改學(xué)習(xí)率就繼承這個(gè)類寫自己的Trainer第三個(gè)參數(shù)是任務(wù)ID第四個(gè)是fold。fold從0到4代表5折交叉驗(yàn)證的哪一折如果只想快速驗(yàn)證流程通不通可以用0這一個(gè)fold訓(xùn)完后續(xù)要出穩(wěn)定結(jié)果再訓(xùn)全5個(gè)fold。模型權(quán)重默認(rèn)寫在nnUNet_results/Task001_demo/nnUNetTrainerV2__3d_fullres/fold_0下面文件名是model_best和model_final_checkpoint。訓(xùn)練日志在同一目錄下的training_log_*.txt里每個(gè)epoch會(huì)打印loss、Dice等指標(biāo)。顯存不夠時(shí)可以在命令里加--disable_deep_supervision減少顯存占用但會(huì)犧牲一點(diǎn)精度也可以手動(dòng)把batch size調(diào)小訓(xùn)練腳本默認(rèn)會(huì)按數(shù)據(jù)大小自動(dòng)選batch size手動(dòng)改的話一般從2開始逐步往下試同時(shí)觀察GPU利用率別掉到30%以下。3.4 用--val跑驗(yàn)證集先拿到訓(xùn)練階段的客觀指標(biāo)訓(xùn)練完fold_0之后我想先看一眼這個(gè)fold在驗(yàn)證集上的表現(xiàn)再?zèng)Q定要不要把5個(gè)fold都訓(xùn)練完。做法是重新執(zhí)行一次nnUNet_train但加上--val參數(shù)nnUNet_train 3d_fullres nnUNetTrainerV2 1 0 --val這個(gè)命令不會(huì)重新訓(xùn)練而是用剛才訓(xùn)練好的權(quán)重在fold_0的驗(yàn)證集就是訓(xùn)練時(shí)留出來的那20%上做推理然后輸出一個(gè)validation文件夾里面是預(yù)測結(jié)果同時(shí)打印每個(gè)類別的Dice系數(shù)。這個(gè)驗(yàn)證指標(biāo)的意義在于它是nnU-Net自己按數(shù)據(jù)劃分算出來的和后面nnUNet_predict階段你自己拿測試集去推的結(jié)果在數(shù)據(jù)分布上是一致的可以用來判斷訓(xùn)練是否收斂、需不需要加大epochs或者換Trainer。如果你的數(shù)據(jù)類別極度不均衡這里還要關(guān)注一下每個(gè)類別的Dice而不是只看平均Dice某個(gè)類別Dice特別低通常是該類別樣本太少需要回去檢查標(biāo)注質(zhì)量或考慮用更大的學(xué)習(xí)率。4. 用nnUNet_predict做測試推理輸入輸出與后處理4.1 nnUNet_predict的標(biāo)準(zhǔn)命令與關(guān)鍵參數(shù)測試階段的核心命令是nnUNet_predict。它讀取一個(gè)輸入文件夾里所有圖像輸出對應(yīng)的分割結(jié)果。一個(gè)完整的調(diào)用是nnUNet_predict -i /data/test_images -o /data/test_pred \ -t 1 -m 3d_fullres -f 0 \ --save_npz參數(shù)含義-i是測試圖像目錄里面放和訓(xùn)練集同名的.nii.gz文件模態(tài)后綴_0000也是必需的-o是輸出目錄-t是任務(wù)ID-m是配置名-f是指定用哪幾個(gè)fold的模型多個(gè)fold時(shí)寫-f 0 1 2 3 4nnU-Net會(huì)把多個(gè)fold的結(jié)果取平均再argmax這是它比單模型更穩(wěn)的重要原因5折ensemble的預(yù)測結(jié)果通常比單折高1到2個(gè)點(diǎn)Dice--save_npz會(huì)把softmax概率保存下來占空間但后續(xù)做集成或分析方便。如果訓(xùn)練時(shí)用的自定義Trainer還要加-tr nnUNetTrainerV2指定如果plan階段生成了多個(gè)配置要選對你想用的那個(gè)。這些參數(shù)不一致時(shí)最常見的報(bào)錯(cuò)是“checkpoint not found”——因?yàn)槟阒付ǖ膄old沒有訓(xùn)練過或者Trainer名對不上。排查時(shí)先看nnUNet_results/Task001_demo目錄下有哪些配置和fold再回頭對照命令里的參數(shù)。4.2 單張與批量推理輸入目錄結(jié)構(gòu)決定成敗nnUNet_predict按目錄批量處理輸入目錄里的每個(gè)文件都會(huì)被當(dāng)成一個(gè)病例。所以如果你只測一張圖也得放在一個(gè)目錄里不能直接給文件路徑。輸出目錄不需要提前創(chuàng)建程序會(huì)自動(dòng)建。推理完成之后輸出目錄里是case名加.nii.gz后綴的分割結(jié)果標(biāo)簽值對應(yīng)dataset.json里定義的1、2等類別。結(jié)果讀進(jìn)Python里很簡單import nibabel as nib import numpy as np pred nib.load(/data/test_pred/case_001.nii.gz) data pred.get_fdata().astype(np.int16) print(np.unique(data)) # 應(yīng)該出現(xiàn)0、1、(2)這些標(biāo)簽這里一個(gè)容易踩的坑是輸出的是label還是probabilities。默認(rèn)輸出的是標(biāo)簽圖只有加了--save_npz才會(huì)額外生成npz概率文件。如果你拿到結(jié)果發(fā)現(xiàn)值全是0和1而訓(xùn)練時(shí)有兩個(gè)目標(biāo)類別多半是label id寫錯(cuò)或者模型本身沒訓(xùn)好。批量推理時(shí)建議分批拷貝輸入文件比如一次放50例跑完一批再放下批這樣萬一某個(gè)文件有問題不會(huì)整個(gè)任務(wù)失敗重來。4.3 TTA開不開、step_size怎么設(shè)nnU-Net推理時(shí)默認(rèn)會(huì)啟用test time augmentationTTA即對輸入做幾種空間變換后分別預(yù)測再取平均這個(gè)機(jī)制常被稱為測試時(shí)訓(xùn)練。TTA通常能漲0.5到1個(gè)點(diǎn)Dice代價(jià)是推理時(shí)間翻倍。顯存夠、時(shí)間不急時(shí)我不關(guān)但如果是做線上服務(wù)或處理幾百例的大批量數(shù)據(jù)加--disable_tta能省一半時(shí)間。這里我一般先開著TTA跑一遍測試集把結(jié)果存好再關(guān)掉TTA跑一遍對比指標(biāo)差異如果差異不到0.3個(gè)點(diǎn)后續(xù)就關(guān)掉TTA換速度。step_size是滑窗推理的步長比例默認(rèn)0.5表示窗口每次移動(dòng)一半。對特別大的圖像step_size調(diào)到0.8能加快推理但邊界區(qū)域的分割會(huì)略差調(diào)小到0.3會(huì)變慢但更穩(wěn)。這個(gè)參數(shù)是推理階段少數(shù)值得手動(dòng)調(diào)的因?yàn)樗苯涌刂苹爸丿B程度對分割穩(wěn)定性影響明顯。調(diào)的時(shí)候觀察一下輸出mask的連通性如果目標(biāo)區(qū)域出現(xiàn)很多細(xì)碎的碎片多半是step_size太大導(dǎo)致窗口間預(yù)測不一致。4.4 模型checkpoint的“后悔藥”中斷續(xù)訓(xùn)與權(quán)重選擇訓(xùn)練中途斷了不用從頭跑。nnUNet_train會(huì)記錄斷點(diǎn)重新執(zhí)行同樣的訓(xùn)練命令會(huì)從最近的checkpoint繼續(xù)。這里要分清兩個(gè)文件model_best是驗(yàn)證集上表現(xiàn)最好的權(quán)重model_final_checkpoint是訓(xùn)練結(jié)束時(shí)的權(quán)重兩個(gè)文件在同名目錄下。如果訓(xùn)練到一半被kill掉只有model_final_checkpoint存在可以用它繼續(xù)如果要推理優(yōu)先用model_best還是model_final_checkpoint可以看驗(yàn)證指標(biāo)常見做法是哪個(gè)在驗(yàn)證集上高用哪個(gè)。我在實(shí)際項(xiàng)目中習(xí)慣每個(gè)fold訓(xùn)完后把model_best單獨(dú)復(fù)制一份到另一個(gè)目錄避免后續(xù)誤操作把結(jié)果覆蓋。權(quán)重文件是幾百M(fèi)B級(jí)別不占空間但這個(gè)習(xí)慣救過我兩次一次是重跑訓(xùn)練時(shí)把fold_0的結(jié)果覆蓋了另一次是清理目錄時(shí)誤刪了訓(xùn)練輸出。nnU-Net的自動(dòng)化程度再高也架不住手動(dòng)操作失誤備份永遠(yuǎn)是成本最低的后悔藥。5. nnunet訓(xùn)練測試數(shù)據(jù)集的5個(gè)常見問題現(xiàn)象、原因與解決5.1 標(biāo)簽是RGB索引而不是單通道標(biāo)簽loss怎么都不降現(xiàn)象訓(xùn)練幾十個(gè)epochloss在0.6左右不動(dòng)Dice一直是0或極低。原因很多人從標(biāo)注工具比如Labelme或某些Web標(biāo)注平臺(tái)導(dǎo)出的標(biāo)簽是RGB彩色圖比如背景是(0,0,0)目標(biāo)是(255,0,0)直接存成PNG轉(zhuǎn)成NIfTI后每個(gè)像素是一個(gè)三維向量不是單一類別ID。nnU-Net讀進(jìn)去會(huì)把每個(gè)向量當(dāng)成多通道標(biāo)簽來處理模型無法學(xué)習(xí)這種離散映射loss自然降不下去。解決把RGB標(biāo)簽映射成索引標(biāo)簽。常見做法是按顏色構(gòu)建一個(gè)查找表把每個(gè)像素的RGB向量映射成整數(shù)類別import numpy as np from PIL import Image # 顏色到類別ID的映射按你的實(shí)際標(biāo)注顏色填寫 color_map { (0, 0, 0): 0, # background (255, 0, 0): 1, # liver (0, 0, 255): 2, # tumor } label_img np.array(Image.open(label.png)) # 形狀 (H, W, 3) out np.zeros((label_img.shape[0], label_img.shape[1]), dtypenp.int16) for rgb, cls_id in color_map.items(): mask (label_img[..., 0] rgb[0]) (label_img[..., 1] rgb[1]) (label_img[..., 2] rgb[2]) out[mask] cls_id nib.save(nib.Nifti1Image(out, affine), label_mapped.nii.gz)關(guān)鍵是映射后要檢查np.unique(out)只能出現(xiàn)0、1、2這些連續(xù)整數(shù)中間不能有缺口否則模型訓(xùn)練時(shí)類別數(shù)量和dataset.json定義對不上又產(chǎn)生新的報(bào)錯(cuò)。我遇到過標(biāo)注工具把兩種顏色混在一起的情況比如兩個(gè)類別都含有(255,0,0)分量這時(shí)候要回到標(biāo)注軟件里重新修正靠腳本補(bǔ)不干凈。5.2 plan_and_preprocess在計(jì)算數(shù)據(jù)指紋時(shí)中斷現(xiàn)象nnUNet_plan_and_preprocess跑到某個(gè)病例時(shí)直接報(bào)錯(cuò)或Killed日志里沒有明確報(bào)錯(cuò)信息。原因最常見的是圖像非零區(qū)域dense比如整幅圖都沒有全零切片導(dǎo)致裁剪時(shí)申請的內(nèi)存過大或者圖像spacing異常某個(gè)維度spacing為0導(dǎo)致重采樣計(jì)算溢出再就是內(nèi)存不夠被系統(tǒng)OOM kill。解決先看plans.json是否已經(jīng)生成如果生成了一部分說明是在統(tǒng)計(jì)階段之后就掛的。然后單跑一遍數(shù)據(jù)完整性校驗(yàn)nnUNet_plan_and_preprocess -t 1 --verify_dataset_integrity如果這步能過再把內(nèi)存上限放開或者分批預(yù)處理。nnU-Net支持--mode參數(shù)把plan和preprocess拆開執(zhí)行先plan再preprocess這樣能定位到底是哪一步掛的。數(shù)據(jù)本身有問題的用2.3節(jié)的自檢腳本把報(bào)錯(cuò)case找出來單獨(dú)修掉再重跑。這一步最忌諱反復(fù)直接重跑完整命令日志一大片根本看不出哪個(gè)case有問題。5.3 重采樣后目標(biāo)器官變形嚴(yán)重spacing不一致先做歸一化現(xiàn)象預(yù)處理后的圖像和標(biāo)簽里目標(biāo)器官的形狀看起來和原始圖像不一致細(xì)小的結(jié)構(gòu)扭曲、斷裂。原因nnU-Net的plan階段會(huì)計(jì)算所有病例的median spacing把所有圖像重采樣到目標(biāo)spacing。如果一部分病例是1x1x1mm體素另一部分是5x5x5mm體素重采樣到中間值后后者被放大的倍數(shù)過大細(xì)節(jié)就糊了。解決在數(shù)據(jù)準(zhǔn)備階段統(tǒng)一spacing。常見做法是先用SimpleITK把所有圖像重采樣到同一個(gè)spacing范圍比如CT統(tǒng)一到1x1x1mm然后再交給nnU-Net。注意標(biāo)簽重采樣必須用最近鄰插值用線性或三次樣條會(huì)在邊緣產(chǎn)生中間值破壞標(biāo)簽語義。重采樣后還要再跑一次自檢確認(rèn)圖像和標(biāo)簽的shape仍然一致。另外不同設(shè)備采集的同一器官數(shù)據(jù)spacing差異太大時(shí)不要硬塞進(jìn)同一個(gè)數(shù)據(jù)集訓(xùn)練先按spacing分桶處理看各自的分割效果再?zèng)Q定是否合并。5.4 訓(xùn)練完找不到model_final_checkpointfold和命令沒對應(yīng)現(xiàn)象訓(xùn)練日志正常打印但nnUNet_results對應(yīng)目錄里是空的或者只有partial_checkpoint。原因訓(xùn)練進(jìn)程被提前終止或者store_checkpoint的路徑?jīng)]寫對。nnU-Net訓(xùn)練過程中會(huì)周期性保存partial_checkpoint只有正常跑完一個(gè)fold才會(huì)落model_best和model_final_checkpoint。如果你只是快速驗(yàn)證模型能跑通訓(xùn)練被CtrlC中斷自然找不到完整權(quán)重。解決確認(rèn)訓(xùn)練日志里出現(xiàn)了類似“Epoch done”的完整周期輸出。如果只是驗(yàn)證流程建議把epochs調(diào)小、data少放點(diǎn)但最好還是讓它完整跑完一個(gè)fold不然測試階段nnUNet_predict找不到checkpoint白忙一場。也可以訓(xùn)練過程中手動(dòng)把partial_checkpoint復(fù)制出來續(xù)訓(xùn)但我不建議新手這么做續(xù)訓(xùn)的路徑和參數(shù)比較容易搞錯(cuò)我見過有人把partial_checkpoint當(dāng)成final去推理結(jié)果輸出全黑。5.5 推理結(jié)果全黑或全背景l(fā)abel IDs和JSON定義對不上現(xiàn)象測試集推理完成了結(jié)果圖打開全黑np.unique輸出只有0。原因測試圖和訓(xùn)練圖強(qiáng)度分布差太遠(yuǎn)比如訓(xùn)練數(shù)據(jù)是增強(qiáng)CT測試數(shù)據(jù)是平掃CT歸一化之后目標(biāo)區(qū)域的響應(yīng)完全不同更常見的是labels定義和標(biāo)注數(shù)據(jù)不一致比如標(biāo)注文件里類別ID是1到5dataset.json里只定義了0和1兩個(gè)類別模型從未見過那些標(biāo)簽。解決先跑驗(yàn)證集看看是否正常。如果驗(yàn)證集正常、測試集全黑優(yōu)先懷疑數(shù)據(jù)分布差異把測試圖像的強(qiáng)度分布和訓(xùn)練集對比一下看是不是范圍差太多。如果驗(yàn)證集也全黑檢查標(biāo)注數(shù)據(jù)里到底有哪些值把labels字典補(bǔ)全。不要先懷疑模型這一步排查順序能省很多時(shí)間。還要順帶檢查一下是不是測試圖像命名里的模態(tài)序號(hào)和訓(xùn)練集不一致_0000寫成了_0001模型拿到的通道根本不是它認(rèn)識(shí)的那個(gè)。6. 一個(gè)讓nnU-Net結(jié)果更穩(wěn)的習(xí)慣先讀預(yù)測圖再談指標(biāo)我見過不少同事拿到Dice 0.9就急著交付結(jié)果臨床醫(yī)生打開一看分割結(jié)果比原始圖像小了一圈邊緣完全對不上。Dice是整體重疊率對邊界偏差不敏感但對醫(yī)學(xué)分割來說邊界才是最能看出問題的。所以我現(xiàn)在有一個(gè)習(xí)慣無論訓(xùn)練還是測試推理出來的預(yù)測圖一定先疊加到原圖上肉眼看一遍再去看指標(biāo)。用SimpleITK或nibabel都能做這個(gè)疊加最快速的方法是先把預(yù)測轉(zhuǎn)成numpy然后做偽彩色覆蓋import numpy as np import nibabel as nib import matplotlib.pyplot as plt img nib.load(case_001_0000.nii.gz).get_fdata() pred nib.load(case_001_pred.nii.gz).get_fdata() plt.figure(figsize(12, 6)) plt.subplot(1, 2, 1) plt.imshow(img[:, :, img.shape[2] // 2], cmapgray) plt.subplot(1, 2, 2) plt.imshow(img[:, :, img.shape[2] // 2], cmapgray) plt.imshow(np.ma.masked_where(pred[:, :, img.shape[2] // 2] 0, pred[:, :, img.shape[2] // 2]), alpha0.5, cmapautumn) plt.savefig(overlay_check.png)這個(gè)腳本每次推理后我都會(huì)跑一遍挑幾個(gè)典型切片存成圖。如果要系統(tǒng)驗(yàn)證一個(gè)測試集我會(huì)從中隨機(jī)抽10個(gè)病例做疊加圖肉眼檢查有沒有分割區(qū)域跑到圖像外面去、目標(biāo)器官之間的邊界粘連、小結(jié)構(gòu)被漏掉。這三個(gè)問題指標(biāo)上不一定看得出來但疊加圖一眼就能發(fā)現(xiàn)。做nnU-Net這兩年我最大的教訓(xùn)就是不要迷信框架的自動(dòng)化自動(dòng)化的plan和train只是把常規(guī)參數(shù)調(diào)好了數(shù)據(jù)本身的問題它管不了。每一次拿到新數(shù)據(jù)集先把數(shù)據(jù)質(zhì)量關(guān)卡住再談?dòng)?xùn)練和推理。希望這個(gè)習(xí)慣對你也有用。本文還有配套的精品資源點(diǎn)擊獲取