據(jù)集實(shí)戰(zhàn):從預(yù)處理到U-Net訓(xùn)練與避坑)
簡(jiǎn)介一套面向醫(yī)學(xué)影像分割任務(wù)的數(shù)據(jù)集聚焦超聲乳腺良性區(qū)域分割適用于計(jì)算機(jī)視覺與醫(yī)學(xué)圖像處理方向的學(xué)習(xí)者、研究者既可作為入門分割模型訓(xùn)練與驗(yàn)證的基礎(chǔ)數(shù)據(jù)也適合用來做算法對(duì)比與效果評(píng)估。壓縮包內(nèi)共877個(gè)文件其中875個(gè)PNG格式文件圖像與掩碼、1個(gè)txt類別文件和1個(gè)Python可視化腳本整體大小約88.01MB目前已有958人下載學(xué)習(xí)。數(shù)據(jù)包含訓(xùn)練集與測(cè)試集訓(xùn)練集約300張圖像及對(duì)應(yīng)mask測(cè)試集約130張按images與masks目錄組織可直接接入常見分割流程數(shù)據(jù)采用背景、良性乳腺兩類標(biāo)注classes文件提供類別說明。可視化腳本可隨機(jī)抽取一張?jiān)紙D、GT圖及兩者疊加效果并保存便于快速直觀檢查標(biāo)注質(zhì)量與模型輸出。整體來看數(shù)據(jù)集與可視化腳本共同為乳腺超聲圖像分割相關(guān)的算法復(fù)現(xiàn)、論文實(shí)驗(yàn)或課程設(shè)計(jì)提供了扎實(shí)的數(shù)據(jù)與工具支撐適合入門至中高級(jí)實(shí)踐者使用。1. 醫(yī)學(xué)圖像分割數(shù)據(jù)集一份超聲乳腺良性病灶數(shù)據(jù)到底能幫你做什么拿到一份帶訓(xùn)練集、測(cè)試集和標(biāo)簽的超聲乳腺良性圖像分割數(shù)據(jù)集第一件事不是急著跑模型而是先搞清楚它和自然圖像分割數(shù)據(jù)有什么不同。B超圖像噪聲大、邊界模糊良性病灶與周圍組織灰度接近分割任務(wù)比純檢測(cè)更依賴像素級(jí)標(biāo)注質(zhì)量。接下來我會(huì)從數(shù)據(jù)集結(jié)構(gòu)、標(biāo)簽格式、訓(xùn)練/測(cè)試劃分講到最常用的 U-Net 落地腳本再列出真實(shí)使用中踩過的五個(gè)坑最后給一套可以復(fù)現(xiàn)的驗(yàn)證套路。這套流程適合剛?cè)胧轴t(yī)學(xué)圖像分割的研究生、算法工程師也適合想拿公開數(shù)據(jù)集驗(yàn)證預(yù)處理和訓(xùn)練管線的從業(yè)者。2. 超聲乳腺良性分割數(shù)據(jù)集的目錄與標(biāo)簽?zāi)玫绞窒茸鋈履玫揭粋€(gè)醫(yī)學(xué)圖像分割數(shù)據(jù)集尤其是超聲影像數(shù)據(jù)直接開訓(xùn)是翻車高發(fā)區(qū)。超聲圖像不像CT或MRI那樣有標(biāo)準(zhǔn)DICOM頭信息公開數(shù)據(jù)集的目錄命名、掩碼格式甚至像素值定義都可能不同。我的習(xí)慣是先做三件事確認(rèn)訓(xùn)練集和測(cè)試集是否完全分開確認(rèn)標(biāo)簽是掩碼還是多邊形坐標(biāo)確認(rèn)圖像和標(biāo)簽?zāi)芊褚灰粚?duì)應(yīng)。這三件事決定了后面的預(yù)處理代碼怎么寫也決定了評(píng)測(cè)分?jǐn)?shù)是否可信。2.1 訓(xùn)練集和測(cè)試集為什么必須分開數(shù)據(jù)劃分的底層邏輯分割模型的目標(biāo)是學(xué)會(huì)“看”病灶區(qū)域而不是“認(rèn)出”某張?zhí)囟▓D片。如果訓(xùn)練集和測(cè)試集存在重疊模型在測(cè)試時(shí)直接匹配文件名或像素模板Dice會(huì)虛高到不真實(shí)。對(duì)于超聲乳腺數(shù)據(jù)集這個(gè)問題尤其隱蔽同一患者的圖像可能連文件名都不同但病灶區(qū)域來自同一次掃描背景紋理幾乎一樣。按文件名查重只是第一道防線按患者ID查重才是關(guān)鍵。如果數(shù)據(jù)集沒有提供患者ID也可以通過文件名中的case編號(hào)或病人編號(hào)推斷。下面這段腳本會(huì)掃描訓(xùn)練集和測(cè)試集的圖像文件名輸出重合列表from pathlib import Path train_img_dir Path(data/train/images) test_img_dir Path(data/test/images) train_names {p.name for p in train_img_dir.glob(*.png)} test_names {p.name for p in test_img_dir.glob(*.png)} overlap train_names test_names print(train count:, len(train_names)) print(test count:, len(test_names)) print(overlap count:, len(overlap)) for name in sorted(overlap)[:10]: print(overlap:, name)這段代碼用set求交集train_names和test_names分別是兩個(gè)目錄下的文件集合。輸出重合數(shù)量后即使沒有重合也要再按文件名前綴分組檢查比如文件名是case_001_frame_01.png那么所有case_001開頭的圖像應(yīng)該只出現(xiàn)在一個(gè)集合里。如果同一病例的幀散落在兩邊就要手動(dòng)調(diào)整劃分或者改用后面第5章會(huì)講的按患者分折交叉驗(yàn)證。再補(bǔ)一段按患者ID檢查的代碼import re def patient_id(filename): match re.match(r(case[_-]?\d), filename) return match.group(1) if match else filename train_patients {patient_id(name) for name in train_names} test_patients {patient_id(name) for name in test_names} print(patients in both sets:, train_patients test_patients)這里用正則case[_-]?\d提取病例號(hào)。實(shí)際命名可能不是這個(gè)模式需要先print幾個(gè)文件名看一眼再寫正則。注意如果提取不出來寧可手動(dòng)生成一個(gè)patient_id映射表也不要跳過這一步。對(duì)醫(yī)學(xué)圖像分割數(shù)據(jù)集而言按患者劃分比按圖像劃分更能反映模型在真實(shí)新病人上的表現(xiàn)。2.2 標(biāo)簽格式識(shí)別掩碼是 PNG、JSON 還是 NIfTI決定預(yù)處理怎么寫標(biāo)題里的“標(biāo)簽”兩個(gè)字看著簡(jiǎn)單落地時(shí)差異很大。超聲乳腺分割數(shù)據(jù)集常見三種標(biāo)簽格式二值PNG掩碼、JSON多邊形坐標(biāo)、NIfTI體數(shù)據(jù)。PNG掩碼可以直接用OpenCV/PIL讀取JSON需要把邊界點(diǎn)填充成掩碼NIfTI需要處理放射學(xué)坐標(biāo)方向。拿到數(shù)據(jù)集后先用一段腳本統(tǒng)計(jì)標(biāo)簽?zāi)夸浀臄U(kuò)展名避免用錯(cuò)解析器。from pathlib import Path from collections import Counter label_dir Path(data/train/labels) ext_counter Counter(p.suffix.lower() for p in label_dir.iterdir() if p.is_file()) print(ext_counter) for p in sorted(label_dir.glob(*))[:5]: print(p.name)這段腳本用Counter統(tǒng)計(jì)標(biāo)簽?zāi)夸浝锼形募缶Y能立刻看出是.png還是.npy還是.nii.gz。注意.nii.gz的實(shí)際文件名后綴是.gz如果直接統(tǒng)計(jì).suffix會(huì)得到.gz所以最好用p.name.endswith((.nii.gz,))或先用.with_suffix()再判斷。打印前5個(gè)文件名稱是為了確認(rèn)命名規(guī)律給后面的文件名對(duì)齊做準(zhǔn)備。如果標(biāo)簽是PNG下一步檢查掩碼的像素值。很多數(shù)據(jù)集的背景是0前景是255但也有用1表示前景的。訓(xùn)練時(shí)如果直接把255當(dāng)成類別索引會(huì)把一個(gè)前景像素拆成兩個(gè)類別。讀掩碼后先打印唯一值import cv2 import numpy as np mask cv2.imread(data/train/labels/case_001.png, cv2.IMREAD_GRAYSCALE) print(mask shape:, mask.shape) print(unique values:, np.unique(mask))這里用cv2.imread的IMREAD_GRAYSCALE強(qiáng)制按灰度讀避免三通道干擾。如果輸出只有[0 255]訓(xùn)練時(shí)要除255再當(dāng)成背景/前景如果輸出是[0 1]直接轉(zhuǎn)long類型即可。如果還有中間值如128說明掩碼可能帶邊界標(biāo)注或類別權(quán)重要回到數(shù)據(jù)集說明確認(rèn)。超聲乳腺良性分割一般只需要背景和病灶兩類出現(xiàn)128時(shí)要小心處理。如果標(biāo)簽是JSON常見結(jié)構(gòu)是{filename: ..., regions: [{shape_attributes: {points: [...]}}]}。這種情況不能直接送進(jìn)模型要先做一次離線轉(zhuǎn)換把多邊形填充成掩碼import json import cv2 import numpy as np with open(annotation.json) as f: ann json.load(f) mask np.zeros((height, width), dtypenp.uint8) for region in ann.get(regions, []): points region[shape_attributes][points] pts np.array(points, dtypenp.int32).reshape(-1, 1, 2) cv2.fillPoly(mask, [pts], 255)這段代碼用cv2.fillPoly把多邊形頂點(diǎn)填充成白色掩碼。height和width必須與原始B超圖像一致通常可以根據(jù)圖像尺寸獲取。轉(zhuǎn)換后的掩碼要和原始圖像放在同一個(gè)目錄層級(jí)方便Dataset類讀取。2.3 目錄組織檢查清單用一段腳本驗(yàn)證文件名、尺寸和類別在寫訓(xùn)練腳本前我會(huì)先用一個(gè)檢查腳本把所有潛在問題都暴露出來。你需要確認(rèn)四點(diǎn)圖像和標(biāo)簽文件一一對(duì)應(yīng)、圖像和標(biāo)簽尺寸一致、掩碼類別只有前景和背景、訓(xùn)練集和測(cè)試集沒有文件重合。下面這段腳本會(huì)遍歷圖像目錄對(duì)每張圖找到同名標(biāo)簽并檢查 shapefrom pathlib import Path import cv2 import numpy as np img_dir Path(data/train/images) mask_dir Path(data/train/labels) img_files sorted(img_dir.glob(*.png)) mask_files sorted(mask_dir.glob(*.png)) print(images:, len(img_files), masks:, len(mask_files)) for img_path in img_files: mask_path mask_dir / img_path.name if not mask_path.exists(): print(missing mask:, img_path.name) continue img cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE) mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) if img.shape ! mask.shape: print(shape mismatch:, img_path.name, img.shape, mask.shape)這組代碼用同名配對(duì)方式檢查img_files按字典序排序后再通過mask_dir / img_path.name構(gòu)造掩碼路徑。這樣不會(huì)因?yàn)榕判蚍绞讲煌霈F(xiàn)錯(cuò)位。如果打印出missing mask或shape mismatch需要先用離線腳本修復(fù)數(shù)據(jù)而不是在訓(xùn)練時(shí)靠報(bào)錯(cuò)去猜。檢查結(jié)果可以用下面這張表記錄檢查項(xiàng)期望值失敗動(dòng)作圖像數(shù)量與掩碼數(shù)量完全一致補(bǔ)齊缺失文件或重命名圖像和掩碼尺寸每個(gè)樣本 pair 相等先統(tǒng)一裁剪/縮放再入 train掩碼唯一值0 和 255 或 0 和 1訓(xùn)練前歸一化到類別索引訓(xùn)練/測(cè)試文件交集無按患者ID重新劃分這張表是我在實(shí)際處理B超數(shù)據(jù)集時(shí)的檢查模板。超聲乳腺圖像四周通常有大片黑色背景如果發(fā)現(xiàn)圖像尺寸不統(tǒng)一不要盲目resize先用固定比例中心裁剪去掉黑邊再用cv2.resize統(tǒng)一。掩碼尺寸不一樣時(shí)更危險(xiǎn)說明標(biāo)注和原圖可能不是從同一個(gè)預(yù)處理流程出來的需要回到原始數(shù)據(jù)確認(rèn)。3. 用這份數(shù)據(jù)集跑通 U-Net預(yù)處理、訓(xùn)練腳本與參數(shù)選擇把目錄和標(biāo)簽檢查做完就可以進(jìn)入正題了。醫(yī)學(xué)圖像分割數(shù)據(jù)集最常見的落地路徑是用 U-Net 結(jié)構(gòu)訓(xùn)練自己的數(shù)據(jù)集。這里我會(huì)給一個(gè)最小可用流程灰度圖預(yù)處理、圖像與標(biāo)簽對(duì)齊、訓(xùn)練循環(huán)、曲線解讀。超聲乳腺良性病灶本身邊界偏模糊模型不需要特別復(fù)雜先跑通基線比換大模型更重要。3.1 從 B 超圖像到模型輸入灰度圖歸一化與裁剪超聲圖像通常不是標(biāo)準(zhǔn)照片而是灰度強(qiáng)度圖。直接套用 ImageNet 的三通道均值和標(biāo)準(zhǔn)差沒有意義常見做法是對(duì)整張圖做最大最小值歸一化或者除以255。更穩(wěn)的做法是先統(tǒng)計(jì)數(shù)據(jù)集的灰度分布把均值附近的范圍映射到0-1而不是讓個(gè)別高亮偽影主導(dǎo)整個(gè)輸入。另一個(gè)重要的預(yù)處理是裁剪。B超圖像四周經(jīng)常有設(shè)備信息、刻度尺、純黑邊框這些區(qū)域不參與診斷卻會(huì)讓模型在前期把注意力放在背景結(jié)構(gòu)上。我一般用閾值法先找到超聲扇形區(qū)域再做歸一化和縮放。下面這段函數(shù)展示了一個(gè)可復(fù)用的預(yù)處理def preprocess_image(image_path, target_size(256, 256)): img cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) img img.astype(np.float32) # 去掉四周黑色背景 non_bg img 5 coords np.argwhere(non_bg) y0, x0 coords.min(axis0) y1, x1 coords.max(axis0) 1 img img[y0:y1, x0:x1] # 歸一化到 0~1 img (img - img.min()) / (img.max() - img.min() 1e-6) img cv2.resize(img, target_size, interpolationcv2.INTER_CUBIC) return imgnon_bg img 5的5是灰度閾值用來區(qū)分超聲回聲區(qū)和純黑背景。如果圖像整體很暗可以降到2如果包含強(qiáng)噪聲點(diǎn)可以提到10。np.argwhere得到所有非背景像素坐標(biāo)取最小和最大值得到裁剪框。歸一化時(shí)用img.max() - img.min()做分母加1e-6防止全黑圖除零。最后 resize 到256x256這是一個(gè)速度和精度平衡的尺寸如果顯存夠大用512x512能讓邊界更清楚。標(biāo)簽掩碼要執(zhí)行同樣的裁剪和resize但插值方法必須改成最近鄰否則會(huì)引入不屬于任何一類的中間灰度值def preprocess_mask(mask_path, target_size(256, 256)): mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 使用同一個(gè)裁剪框這里簡(jiǎn)化為直接 resize mask cv2.resize(mask, target_size, interpolationcv2.INTER_NEAREST) mask (mask 127).astype(np.int64) return mask注意這里的裁剪框要和圖像預(yù)處理完全一致。實(shí)際代碼里我通常把裁剪坐標(biāo)從preprocess_image返回再傳給preprocess_mask避免各自算一遍導(dǎo)致錯(cuò)位。3.2 標(biāo)簽讀取與數(shù)據(jù)對(duì)齊防止圖像和掩碼錯(cuò)位的檢查點(diǎn)很多剛開始用醫(yī)學(xué)圖像分割數(shù)據(jù)集的人會(huì)在 Dataset 類里用os.listdir分別讀圖像和掩碼再按下標(biāo)訪問。這是最容易錯(cuò)位的寫法因?yàn)閮蓚€(gè)目錄的排序結(jié)果可能不一樣。更穩(wěn)的做法是只遍歷圖像目錄然后用圖像的文件名拼接掩碼路徑。下面是一個(gè)標(biāo)準(zhǔn) PyTorch Dataset 寫法from torch.utils.data import Dataset from pathlib import Path import cv2 import torch class UltrasoundDataset(Dataset): def __init__(self, image_dir, mask_dir, target_size(256, 256)): self.image_paths sorted(Path(image_dir).glob(*.png)) self.mask_dir Path(mask_dir) self.target_size target_size for p in self.image_paths: assert (self.mask_dir / p.name).exists(), fmissing {p.name} def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image_path self.image_paths[idx] mask_path self.mask_dir / image_path.name image preprocess_image(str(image_path), self.target_size) mask preprocess_mask(str(mask_path), self.target_size) image_tensor torch.from_numpy(image).unsqueeze(0).float() mask_tensor torch.from_numpy(mask).long() return image_tensor, mask_tensorself.image_paths使用sorted(Path.glob(*.png))掩碼路徑直接用同名拼接。assert在初始化時(shí)快速失敗如果缺掩碼會(huì)立刻報(bào)錯(cuò)而不是訓(xùn)練到中間才發(fā)現(xiàn)。torch.from_numpy(image).unsqueeze(0)給灰度圖補(bǔ)上通道維得到(1, H, W)。掩碼轉(zhuǎn)成long是 PyTorch 交叉熵?fù)p失要求的整數(shù)標(biāo)簽類型。3.3 一個(gè)最小可運(yùn)行的 U-Net 訓(xùn)練腳本PyTorch模型結(jié)構(gòu)建議先用經(jīng)典 U-Net編碼器三層、解碼器三層通道數(shù)從32開始。下面這段訓(xùn)練循環(huán)聚焦在數(shù)據(jù)加載、損失函數(shù)和優(yōu)化器配置上import torch from torch.utils.data import DataLoader from unet_model import UNet device torch.device(cuda if torch.cuda.is_available() else cpu) train_dataset UltrasoundDataset(data/train/images, data/train/labels) train_loader DataLoader(train_dataset, batch_size8, shuffleTrue, num_workers2) model UNet(in_channels1, out_channels2).to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-4) loss_fn torch.nn.CrossEntropyLoss() for epoch in range(50): model.train() running_loss 0.0 for images, masks in train_loader: images, masks images.to(device), masks.to(device) output model(images) loss loss_fn(output, masks) optimizer.zero_grad() loss.backward() optimizer.step() running_loss loss.item() avg_loss running_loss / len(train_loader) print(fepoch {epoch 1:02d} loss {avg_loss:.4f})UNet(in_channels1, out_channels2)里in_channels1對(duì)應(yīng)灰度超聲圖out_channels2對(duì)應(yīng)背景和病灶兩個(gè)類別。CrossEntropyLoss的輸出要求是(N, C, H, W)掩碼是(N, H, W)正好匹配。batch_size8是在256x256輸入、顯存8GB左右的經(jīng)驗(yàn)值如果出現(xiàn)OOM先降到4或2。lr1e-4對(duì)醫(yī)學(xué)圖像小數(shù)據(jù)集比默認(rèn)的1e-3更穩(wěn)避免剛開始幾步就把分割權(quán)重帶偏。如果掩碼只有0和255需要在preprocess_mask里做(mask 127)再轉(zhuǎn)LongTensor如果已經(jīng)是0和1也要確認(rèn)沒有浮點(diǎn)噪聲。類別索引從0開始CrossEntropyLoss會(huì)自動(dòng)忽略索引為負(fù)的值所以不要給掩碼加背景掩蔽以滿足形狀要求。注意unet_model模塊可以用你自己實(shí)現(xiàn)的 U-Net 替換關(guān)鍵是輸入通道為1、輸出通道為2且輸出尺寸與輸入一致。3.4 訓(xùn)練曲線解讀Dice 不漲、Loss 震蕩時(shí)先調(diào)什么訓(xùn)練跑起來之后不能只看loss。醫(yī)學(xué)圖像分割更常用Dice系數(shù)來衡量因?yàn)榱夹院捅尘爸g像素極度不平衡時(shí)交叉熵loss可能一直很低但Dice也不漲。下面這段代碼在驗(yàn)證集上計(jì)算Dicedef dice_score(pred, target): pred (pred 0).float() # 預(yù)測(cè)屬于病灶類的概率最大 intersection (pred * target).sum() return (2.0 * intersection 1e-6) / (pred.sum() target.sum() 1e-6)pred是模型輸出的類別索引這里把預(yù)測(cè)病灶像素和二值掩碼逐像素相乘求和。加1e-6是為了處理兩張全空掩碼時(shí)除零的問題。如果訓(xùn)練loss下降但Dice在0.1左右徘徊優(yōu)先檢查兩個(gè)方向一是數(shù)據(jù)增強(qiáng)是否過強(qiáng)比如隨機(jī)旋轉(zhuǎn)90度把超聲扇形方向打亂二是掩碼是否在resize時(shí)被最近鄰插值破壞尤其是病灶區(qū)域小的時(shí)候。如果loss來回震蕩把學(xué)習(xí)率從1e-4降到3e-5同時(shí)把batch size調(diào)小通常能讓曲線穩(wěn)定下來。還有一種常見情況是驗(yàn)證loss下降但Dice不降這時(shí)需要看模型是否把所有像素都預(yù)測(cè)為背景。打印預(yù)測(cè)圖的唯一值如果只有0沒有1說明類別不平衡已經(jīng)把訓(xùn)練推到了平凡解。可以改用Dice Loss或Focal Loss先把背景類權(quán)重降低。4. 數(shù)據(jù)集使用避坑五個(gè)讓分割模型翻車的真實(shí)場(chǎng)景醫(yī)學(xué)圖像分割數(shù)據(jù)集的坑不在網(wǎng)絡(luò)結(jié)構(gòu)而在數(shù)據(jù)管道。很多問題看起來像玄學(xué)最后定位都是讀取、劃分或預(yù)處理不一致。下面五條按“現(xiàn)象、原因、解決”展開方便照著排查。4.1 灰度圖像被當(dāng)成三通道輸入U(xiǎn)-Net 輸入通道數(shù)設(shè)成3現(xiàn)象模型能跑通loss也在下降但驗(yàn)證集上預(yù)測(cè)結(jié)果基本是全黑或全圖。 原因cv2.imread默認(rèn)會(huì)按三通道讀圖代碼又沒顯式指定IMREAD_GRAYSCALE于是灰度B超圖變成三通道的重復(fù)矩陣。模型第一層in_channels設(shè)成3確實(shí)能訓(xùn)練但學(xué)到的三通道信息完全冗余邊界特征被削弱。 解決統(tǒng)一在數(shù)據(jù)讀取時(shí)加cv2.IMREAD_GRAYSCALE并打印img.shape確認(rèn)輸出是(H, W)而不是(H, W, 3)。模型第一層in_channels1如果要用預(yù)訓(xùn)練模型遷移也建議把權(quán)重在輸入層做均值折疊而不是簡(jiǎn)單地把圖復(fù)制三通道。img cv2.imread(data/train/images/case_001.png) print(img.shape) # 如果是 (512, 512, 3)說明沒走灰度讀取這段檢查會(huì)立刻暴露問題。灰度B超圖被讀成三通道后三個(gè)通道完全相同模型等于在重復(fù)特征上做卷積參數(shù)量和計(jì)算量都白白增加。4.2 掩碼前后景比例懸殊損失函數(shù)被背景主導(dǎo)現(xiàn)象訓(xùn)練準(zhǔn)確率很高但Dice只有0.2分割結(jié)果覆蓋了一整片灰度接近的區(qū)域。 原因良性病灶在超聲圖像里通常只占幾萬像素背景占了幾十萬像素。交叉熵?fù)p失會(huì)把絕大多數(shù)梯度花在背景上模型學(xué)到的就是把大塊暗區(qū)判為背景偶爾碰中一個(gè)病灶像素就能把準(zhǔn)確率拉高。 解決訓(xùn)練損失改成Dice Loss或Focal Loss。Dice Loss直接優(yōu)化區(qū)域重疊對(duì)類別不平衡更穩(wěn)定。如果數(shù)據(jù)集里病灶區(qū)域太小還可以在預(yù)處理階段按包含前景的裁剪框切patch讓每個(gè)訓(xùn)練樣本里前景占比不低于某個(gè)閾值。def dice_loss(pred, target): pred torch.softmax(pred, dim1)[:, 1] smooth 1.0 intersection (pred * target).sum() return 1 - (2 * intersection smooth) / (pred.sum() target.sum() smooth)這個(gè)dice_loss直接取模型輸出中病灶類別的概率與二值掩碼計(jì)算重疊。smooth防止除零也避免訓(xùn)練初期梯度過大。替換CrossEntropyLoss后模型會(huì)更關(guān)注少數(shù)類像素。4.3 訓(xùn)練集和測(cè)試集來自同一病例指標(biāo)虛高現(xiàn)象訓(xùn)練時(shí)Dice到0.95測(cè)試時(shí)換一組數(shù)據(jù)就掉到0.5。 原因數(shù)據(jù)集劃分是按文件名隨機(jī)分的但同一患者的多個(gè)超聲幀出現(xiàn)在兩個(gè)集合里。因?yàn)橥换颊叩脑O(shè)備參數(shù)、探頭角度、病灶形態(tài)高度相似模型記住的是這個(gè)病人的背景紋理而不是通用病灶特征。 解決拿到數(shù)據(jù)先做患者級(jí)分組同一患者的所有圖像只能出現(xiàn)在訓(xùn)練集或測(cè)試集不能跨集合。如果標(biāo)題的數(shù)據(jù)集沒有提供患者ID也可以用文件名中的case編號(hào)提取。更穩(wěn)妥的做法是用第5章的按患者分折交叉驗(yàn)證反復(fù)評(píng)估模型穩(wěn)定性。train_patients {patient_id(p.name) for p in train_names} test_patients {patient_id(p.name) for p in test_names} shared train_patients test_patients if shared: print(患者級(jí)泄露:, shared)這段代碼把2.1里的患者檢查邏輯明確放在訓(xùn)練前執(zhí)行。出現(xiàn)共享患者時(shí)寧可減少訓(xùn)練集規(guī)模也不能保留跨集合的病例。4.4 裁剪黑邊導(dǎo)致標(biāo)簽坐標(biāo)沒同步現(xiàn)象預(yù)測(cè)掩碼在圖像邊緣出現(xiàn)固定偏移評(píng)測(cè)時(shí)邊界區(qū)域出現(xiàn)一排假正或假負(fù)。 原因預(yù)處理函數(shù)對(duì)圖像先裁剪再resize但標(biāo)簽掩碼在另一段代碼里直接resize沒有同步裁剪框。圖像縮放到目標(biāo)尺寸時(shí)失去了一部分邊緣掩碼卻保留了完整尺寸兩者自然錯(cuò)位。 解決圖像和掩碼必須共用同一個(gè)裁剪框。把preprocess_image里計(jì)算出的(y0, y1, x0, x1)返回給preprocess_mask掩碼先裁剪再resize。掩碼的resize插值統(tǒng)一用INTER_NEAREST配合代碼塊里的assert檢查尺寸一致。def crop_resize_pair(image, mask, threshold5): non_bg image threshold coords np.argwhere(non_bg) y0, x0 coords.min(axis0) y1, x1 coords.max(axis0) 1 image_crop cv2.resize(image[y0:y1, x0:x1], (256, 256), interpolationcv2.INTER_CUBIC) mask_crop cv2.resize(mask[y0:y1, x0:x1], (256, 256), interpolationcv2.INTER_NEAREST) return image_crop, mask_cropcrop_resize_pair把圖像和掩碼的裁剪框統(tǒng)一到同一個(gè)y0,x0,y1,x1掩碼用INTER_NEAREST不會(huì)產(chǎn)生新灰度值。這個(gè)函數(shù)應(yīng)作為訓(xùn)練管線的唯一入口避免兩處各寫一套。4.5 測(cè)試時(shí)沒有做與訓(xùn)練時(shí)相同的預(yù)處理現(xiàn)象離線驗(yàn)證Dice還行部署到新環(huán)境后指標(biāo)明顯下降。 原因訓(xùn)練腳本里的預(yù)處理寫在train.py測(cè)試腳本里重新寫了一份歸一化順序、裁剪閾值或resize尺寸有細(xì)微差別。超聲圖像的灰度范圍很敏感差一個(gè)閾值就可能讓病灶邊界改變。 解決把預(yù)處理函數(shù)抽成獨(dú)立模塊訓(xùn)練和推理都從同一個(gè)模塊導(dǎo)入。然后在固定一張圖像上跑訓(xùn)練管線和推理管線比較輸出數(shù)組是否完全一致。我習(xí)慣加一段單元測(cè)試比較同一個(gè)輸入在兩次調(diào)用后的numpy array是否相等。arr1 preprocess_image(data/train/images/case_001.png) arr2 preprocess_image(data/train/images/case_001.png) assert np.array_equal(arr1, arr2)這段測(cè)試只能驗(yàn)證函數(shù)自身確定性更關(guān)鍵的是確認(rèn)推理腳本沒有重新編寫一份“看起來差不多”的預(yù)處理。如果訓(xùn)練和推理的灰度閾值、目標(biāo)尺寸、插值方式不一致測(cè)試集上的微小偏差會(huì)在邊界像素上被放大。5. 在單獨(dú)一次劃分之外交叉驗(yàn)證與分割指標(biāo)的人工復(fù)核技巧5.1 用按患者分組的交叉驗(yàn)證替代單次隨機(jī)劃分如果數(shù)據(jù)集包含訓(xùn)練集和測(cè)試集直接用固定劃分訓(xùn)練即可。但超聲乳腺數(shù)據(jù)規(guī)模通常不大單次劃分對(duì)隨機(jī)種子太敏感。我會(huì)在大致確認(rèn)固定劃分之外額外做一次五折按患者交叉驗(yàn)證用下面這段代碼生成折標(biāo)簽from sklearn.model_selection import KFold import numpy as np patient_ids np.array(sorted({patient_id(p.name) for p in image_paths})) kf KFold(n_splits5, shuffleTrue, random_state42) for fold, (train_idx, val_idx) in enumerate(kf.split(patient_ids)): train_patients patient_ids[train_idx] val_patients patient_ids[val_idx] print(fold, len(train_patients), len(val_patients))KFold按患者ID列表分折而不是按圖像分折。這樣每一折的驗(yàn)證集都來自模型沒見過的患者評(píng)估結(jié)果更接近真實(shí)新數(shù)據(jù)上的效果。random_state42固定隨機(jī)種子保證每次實(shí)驗(yàn)可比較。5.2 把預(yù)測(cè)掩碼和原始 B 超圖像疊起來做人工抽檢Dice和IoU只是數(shù)值不能告訴你模型在哪個(gè)解剖位置犯錯(cuò)。我會(huì)在每輪驗(yàn)證后隨機(jī)抽5張測(cè)試圖像把預(yù)測(cè)掩碼和真實(shí)掩碼分別疊加在原始B超圖上轉(zhuǎn)成彩色對(duì)比圖保存。如果模型總把低回聲區(qū)域整片當(dāng)成病灶數(shù)值上看不出來但可視化會(huì)非常明顯。overlay cv2.cvtColor(img_bgr, cv2.COLOR_GRAY2BGR) overlay[pred_mask 1] (0, 0, 255) # 紅為預(yù)測(cè) overlay[true_mask 1] (0, 255, 0) # 綠為真實(shí) cv2.imwrite(fcheck_fold{fold}.png, overlay)紅色表示預(yù)測(cè)為病灶、綠色表示真實(shí)標(biāo)注紅綠重疊會(huì)變成黃??吹酱笃t色區(qū)域時(shí)說明模型把灰度相近的腺體組織誤判成了病灶需要回到訓(xùn)練數(shù)據(jù)里檢查是否有類似的錯(cuò)誤標(biāo)注。這個(gè)人工抽檢習(xí)慣幫我校準(zhǔn)了很多看似合理、實(shí)則結(jié)構(gòu)不完整的模型。我自己的教訓(xùn)是第一次跑這份數(shù)據(jù)時(shí)只看了Dice覺得0.87很不錯(cuò)抽檢才發(fā)現(xiàn)模型把探頭陰影也劃進(jìn)了病灶。后來把可視化抽檢加進(jìn)訓(xùn)練循環(huán)問題立刻暴露。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取