學(xué)影像分類實(shí)戰(zhàn):基于PyTorch的候選塊方案與避坑指南)
簡(jiǎn)介面向機(jī)器學(xué)習(xí)課程設(shè)計(jì)與大作業(yè)場(chǎng)景這份3D醫(yī)學(xué)影像分類實(shí)現(xiàn)包覆蓋數(shù)據(jù)預(yù)處理、模型訓(xùn)練到結(jié)果評(píng)估的完整流程適合計(jì)算機(jī)、電子信息及數(shù)學(xué)類專業(yè)學(xué)生完成期末項(xiàng)目或畢業(yè)設(shè)計(jì)。壓縮包共596個(gè)文件主體為582個(gè)npz格式的影像特征與樣本數(shù)據(jù)配合8個(gè)Python源碼文件實(shí)現(xiàn)分類算法另有csv標(biāo)注文件、pyc緩存及md文檔說明整體約446MB結(jié)構(gòu)清晰便于按模塊復(fù)現(xiàn)。已有360人學(xué)習(xí)下載。除完整源代碼外資料附帶運(yùn)行結(jié)果與詳細(xì)注釋采用參數(shù)化編程可靈活調(diào)整模型參數(shù)和數(shù)據(jù)路徑遇到問題還可向作者咨詢能有效降低復(fù)現(xiàn)門檻幫助讀者快速掌握3D醫(yī)學(xué)影像分類的項(xiàng)目實(shí)現(xiàn)思路。1. 機(jī)器學(xué)習(xí)大作業(yè)選3D醫(yī)學(xué)影像分類一份能直接跑通的候選塊方案機(jī)器學(xué)習(xí)大作業(yè)選課題有個(gè)反直覺的點(diǎn)2D圖像分類看著簡(jiǎn)單卻難出新意3D醫(yī)學(xué)影像分類看著復(fù)雜反而容易做成完整交付方案。原因很簡(jiǎn)單這類任務(wù)的數(shù)據(jù)已被整理成固定格式模型也有成熟的3D CNN套路可用真正考驗(yàn)人的是把數(shù)據(jù)讀到內(nèi)存、把標(biāo)簽對(duì)上、把提交文件生成對(duì)。這套3D醫(yī)學(xué)影像分類方案就圍繞這三件事組織候選塊npz存三維影像csv記錄標(biāo)簽sampleSubmission定義提交格式。適合三類人計(jì)算機(jī)、電子信息專業(yè)做期末大作業(yè)和課程設(shè)計(jì)的同學(xué)數(shù)學(xué)專業(yè)想快速跑通深度學(xué)習(xí)實(shí)驗(yàn)基準(zhǔn)的同學(xué)以及畢設(shè)想以醫(yī)學(xué)影像分類為方向的開發(fā)者。下面按數(shù)據(jù)方案、總體設(shè)計(jì)、關(guān)鍵實(shí)現(xiàn)、避坑、驗(yàn)收答辯的順序把整套方案講透。2. 需求分析與數(shù)據(jù)組織npz候選塊與三個(gè)csv各自的角色拿到這類交付資源我的習(xí)慣是先翻README.md再讀csv最后才動(dòng)npz。README會(huì)寫明依賴庫(kù)和啟動(dòng)順序是這套方案的入口train_val.csv、0617.csv、sampleSubmission.csv三個(gè)表決定了數(shù)據(jù)怎么切分、模型怎么評(píng)估、結(jié)果怎么提交candidate開頭的npz文件則是真正的三位影像內(nèi)容。如果把數(shù)據(jù)語義搞反后面的模型搭建全是在錯(cuò)誤地基上蓋樓。2.1 文件清單與字段語義npz是黑匣子csv才是說明書先在代碼層面做一次數(shù)據(jù)摸查不急著寫任何訓(xùn)練邏輯。用下面的腳本分別打開npz和csv把內(nèi)部結(jié)構(gòu)全部打印出來import numpy as np import pandas as pd # 查看npz內(nèi)部有哪些鍵以及每個(gè)鍵對(duì)應(yīng)的數(shù)組形狀 data np.load(candidate409.npz, allow_pickleTrue) print(data.files) # 可能輸出 [images] 或 [data] 等鍵名 for key in data.files: arr data[key] print(key, arr.shape, arr.dtype) # 查看標(biāo)注表結(jié)構(gòu)和類別分布 train_val pd.read_csv(train_val.csv) print(train_val.head()) print(train_val.info()) if label in train_val.columns: print(train_val[label].value_counts()) else: print(train_val.iloc[:, -1].value_counts())這段代碼解決的問題很直接npz是二進(jìn)制黑匣子不打印內(nèi)部鍵名就不知道取哪個(gè)字段。有的資源里鍵名叫images有的叫data還有的只有一個(gè)匿名鍵先打印files再寫索引能避免后面幾十處地方重復(fù)踩KeyError。csv同理先確認(rèn)哪一列是樣本ID、哪一列是類別標(biāo)簽再確認(rèn)正負(fù)樣本比例這一步直接影響評(píng)估口徑的選型。對(duì)這份資源而言train_val.csv負(fù)責(zé)訓(xùn)練和驗(yàn)證樣本的標(biāo)注0617.csv通常是與train_val結(jié)構(gòu)一致的補(bǔ)充標(biāo)注文件可以當(dāng)作額外驗(yàn)證集或者增量訓(xùn)練數(shù)據(jù)sampleSubmission.csv是提交模板里面是待填寫的樣本ID與預(yù)測(cè)概率列。落筆之前把三張表的列名對(duì)齊尤其要確認(rèn)ID的統(tǒng)一格式是純數(shù)字candidate_id還是帶前綴的文件名會(huì)給后面關(guān)聯(lián)npz省下大量麻煩。2.2 候選塊預(yù)處理尺寸統(tǒng)一與歸一化的先后順序npz里的三維塊未必是同一尺寸不同候選結(jié)節(jié)可能來自不同大小的檢測(cè)窗口直接塞進(jìn)網(wǎng)絡(luò)會(huì)報(bào)維度錯(cuò)誤。統(tǒng)一的穩(wěn)妥做法是縮放到48×48×48既保留足夠的空間信息又不至于讓三維卷積的顯存開銷失控。這一步推薦用scipy的zoom插值import numpy as np from scipy.ndimage import zoom def load_and_unify(npz_path, target_size(48, 48, 48)): data np.load(npz_path, allow_pickleTrue) # 單鍵文件直接用第一個(gè)鍵多鍵文件按實(shí)際字段名匹配 key data.files[0] if len(data.files) 1 else images vol data[key].astype(np.float32) # 按目標(biāo)尺寸計(jì)算三個(gè)軸的縮放系數(shù) factors [t / s for t, s in zip(target_size, vol.shape)] vol zoom(vol, factors, order3) # 三次插值保留邊緣結(jié)構(gòu) return volorder3的三次插值比線性插值更平滑對(duì)結(jié)節(jié)邊緣這類判別區(qū)域更友好如果追求更快速度可以降到order1但基線實(shí)驗(yàn)不要省這點(diǎn)精度??s放完成后還要做歸一化這里有個(gè)容易忽略的順序問題先統(tǒng)一尺寸、再做歸一化而不是反過來。先縮放會(huì)讓原本分布在某個(gè)區(qū)間的體素值被插值稀釋后續(xù)算出來的均值和方差會(huì)漂移。歸一化策略取決于npz內(nèi)部數(shù)值的物理含義。如果體素范圍已經(jīng)是0到1直接做z-score即可如果是原始CT值或0到255的灰度就要先統(tǒng)計(jì)min和max再?zèng)Q定用均值方差歸一化還是固定窗寬。按通常做法我會(huì)先對(duì)訓(xùn)練集全部樣本做一次分布統(tǒng)計(jì)把min、max、mean、std打印出來再選擇歸一化方式而不是憑感覺套一個(gè)公式跑到底。2.3 評(píng)估口徑為什么AUC比準(zhǔn)確率更能反映模型好壞醫(yī)學(xué)影像二分類任務(wù)里陽性結(jié)節(jié)通常只占少數(shù)準(zhǔn)確率會(huì)被多數(shù)類拉高看起來95%很漂亮實(shí)際對(duì)陽性樣本的識(shí)別能力可能很弱。因此驗(yàn)收標(biāo)準(zhǔn)應(yīng)該用AUC它不依賴閾值、對(duì)類別不平衡更魯棒也是這類任務(wù)最通用的排序指標(biāo)。劃分?jǐn)?shù)據(jù)時(shí)要保證訓(xùn)練集和驗(yàn)證集正負(fù)比例一致from sklearn.model_selection import train_test_split # stratify保證劃分后正負(fù)樣本比例與原始數(shù)據(jù)一致 train_df, val_df train_test_split( train_val, test_size0.2, stratifytrain_val[label], random_state42, )stratify參數(shù)是分層劃分的關(guān)鍵不寫的話隨機(jī)切分可能把少量正樣本全分到驗(yàn)證集導(dǎo)致訓(xùn)練時(shí)根本學(xué)不到陽性特征。0617.csv先檢查列結(jié)構(gòu)是否與train_val一致一致的話可以留在一邊做二次驗(yàn)證用來回答答辯時(shí)“你的模型泛化性怎么樣”這類問題。還有一種情況要提前排查train_val和0617.csv是否包含同一個(gè)candidate_id若有交集必須去重否則驗(yàn)證集會(huì)泄漏訓(xùn)練信息后面的指標(biāo)全是虛高這個(gè)問題在避坑章節(jié)會(huì)專門展開。3. 總體架構(gòu)設(shè)計(jì)四段式pipeline與參數(shù)化配置數(shù)據(jù)摸清之后下一步不是直接寫網(wǎng)絡(luò)而是先把整套流程拆成模塊。很多同學(xué)做課設(shè)翻車不是模型不會(huì)搭是流程亂成一團(tuán)訓(xùn)練腳本和預(yù)處理代碼耦合在一起換個(gè)數(shù)據(jù)路徑要改七八處實(shí)驗(yàn)做了一半想換參數(shù)只能全部重跑。這套方案采用四段式結(jié)構(gòu)把每個(gè)階段徹底分開。3.1 四段職責(zé)劃分?jǐn)?shù)據(jù)層、預(yù)處理層、模型層、提交層層核心職責(zé)主要產(chǎn)物數(shù)據(jù)層讀取csv、關(guān)聯(lián)npz路徑、劃分訓(xùn)練驗(yàn)證集樣本索引表、DataLoader輸入預(yù)處理層尺寸統(tǒng)一、歸一化、數(shù)據(jù)增強(qiáng)固定shape的Tensor樣本模型層3D CNN定義、訓(xùn)練循環(huán)、指標(biāo)評(píng)估訓(xùn)練日志、最優(yōu)權(quán)重文件提交層讀取模板、寫入概率、校驗(yàn)格式submission_final.csv四個(gè)層之間的依賴方向是單向的數(shù)據(jù)層只產(chǎn)出索引和標(biāo)簽預(yù)處理層不關(guān)心模型結(jié)構(gòu)模型層不關(guān)心文件路徑提交層只吃模型權(quán)重和模板。任何一層要換實(shí)現(xiàn)比如把3D CNN換成本地預(yù)訓(xùn)練的特征提取器或者把npz換成其他格式只需要改對(duì)應(yīng)模塊其他層不動(dòng)。這種解耦帶來的直接好處是同一套代碼可以快速?gòu)?fù)現(xiàn)不同實(shí)驗(yàn)配置而不需要“重寫一份腳本”。3.2 參數(shù)化配置改一個(gè)yaml就能跑一組新實(shí)驗(yàn)參數(shù)化編程是這套資源強(qiáng)調(diào)的特點(diǎn)落到實(shí)處就是弄一個(gè)獨(dú)立配置文件把容易變的值全部抽出來data: train_csv: train_val.csv extra_csv: 0617.csv submission: sampleSubmission.csv npz_dir: ./npz preprocess: image_size: 48 normalize: zscore model: base_channels: 16 num_classes: 1 train: batch_size: 8 epochs: 30 lr: 0.001 device: cuda讀取端用yaml庫(kù)加載成字典訓(xùn)練腳本只從配置取參數(shù)import yaml def load_config(config_pathconfig.yaml): with open(config_path, r, encodingutf-8) as f: return yaml.safe_load(f)這個(gè)文件的價(jià)值在實(shí)驗(yàn)管理上想換batch_size改train.batch_size即可想換輸入尺寸改preprocess.image_size預(yù)處理和模型輸入會(huì)跟隨數(shù)據(jù)加載器自動(dòng)變化想對(duì)比不同學(xué)習(xí)率復(fù)制一份config.yaml改成另一組值跑出來的結(jié)果與舊實(shí)驗(yàn)完全隔離。我一般會(huì)把config.yaml和訓(xùn)練日志、最優(yōu)權(quán)重放在同一個(gè)實(shí)驗(yàn)?zāi)夸浵逻@樣過一個(gè)月回來看還能知道當(dāng)時(shí)的實(shí)驗(yàn)條件不至于對(duì)著一個(gè)裸權(quán)重文件發(fā)愁。3.3 訓(xùn)練到提交的編排流程每一步的輸入輸出是什么整體編排在main函數(shù)里按固定順序執(zhí)行每個(gè)函數(shù)只做一件事def main(): cfg load_config(config.yaml) # 數(shù)據(jù)層讀表、關(guān)聯(lián)npz路徑、分層劃分 full_df load_all_csv(cfg[data]) train_df, val_df split_with_stratify(full_df) # 預(yù)處理層構(gòu)建按需讀取的Dataset和DataLoader train_loader build_loader(train_df, cfg, shuffleTrue) val_loader build_loader(val_df, cfg, shuffleFalse) # 模型層初始化網(wǎng)絡(luò)并訓(xùn)練 model build_model(cfg[model]) train(model, train_loader, val_loader, cfg[train]) # 提交層用最優(yōu)權(quán)重生成提交文件 make_submission(model, cfg[data][submission])把生成提交文件獨(dú)立成最后一步是因?yàn)樗妮敵鲋苯記Q定作業(yè)能不能完成閉環(huán)。訓(xùn)練中途打印的指標(biāo)再好看模板沒填對(duì)、ID順序搞亂提交結(jié)果都是廢的。在這個(gè)編排里make_submission只讀取模板、按模板順序?qū)懭腩A(yù)測(cè)概率、最后做一次空值校驗(yàn)三件事邊界非常清晰。整套pipeline跑通之后才算真正把“數(shù)據(jù)讀到內(nèi)存、標(biāo)簽對(duì)上、提交文件生成對(duì)”三個(gè)交付目標(biāo)全部落地。4. 關(guān)鍵實(shí)現(xiàn)3D CNN分類模型搭建與訓(xùn)練細(xì)節(jié)流程骨架搭好后核心就是把網(wǎng)絡(luò)和數(shù)據(jù)加載器填進(jìn)去。這里給出一套能跑通最小閉環(huán)的實(shí)現(xiàn)數(shù)據(jù)加載器按需讀npz模型用三個(gè)Conv3d塊組成基線訓(xùn)練循環(huán)以驗(yàn)證集AUC為保存權(quán)重的依據(jù)最后生成提交文件。這套實(shí)現(xiàn)覆蓋了從文件到預(yù)測(cè)結(jié)果的全鏈路。4.1 數(shù)據(jù)加載器按需讀取npz不一次性進(jìn)內(nèi)存數(shù)據(jù)加載器決定訓(xùn)練能否穩(wěn)定跑完。最容易翻車的寫法是把所有npz讀進(jìn)一個(gè)list樣本數(shù)量上來后內(nèi)存直接被占滿訓(xùn)練剛開始就被系統(tǒng)殺掉。正確做法是繼承Dataset在__getitem__里按索引讀取import os import torch from torch.utils.data import Dataset class CandidateDataset(Dataset): def __init__(self, df, npz_dir, image_size): self.df df.reset_index(dropTrue) self.npz_dir npz_dir self.image_size image_size def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] # 樣本主鍵與文件名對(duì)應(yīng)例如 candidate409.npz npz_path os.path.join(self.npz_dir, fcandidate{row[candidate_id]}.npz) vol load_and_unify(npz_path, (self.image_size,) * 3) vol normalize_volume(vol) # z-score或min-max按config選擇 label torch.tensor([row[label]], dtypetorch.float32) return torch.from_numpy(vol).unsqueeze(0), label每次按索引加載一個(gè)樣本內(nèi)存里只保留當(dāng)前batch的數(shù)據(jù)這是三維影像任務(wù)的基本功。batch_size設(shè)為8比較穩(wěn)妥48×48×48的輸入在單卡上不會(huì)太吃力如果顯存只有4G可以降到4同時(shí)把num_workers設(shè)成2到4提升數(shù)據(jù)讀取并發(fā)。load_and_unify復(fù)用2.2節(jié)里的函數(shù)normalize_volume在預(yù)處理層實(shí)現(xiàn)保持加載器代碼簡(jiǎn)潔。4.2 模型結(jié)構(gòu)三個(gè)Conv3d塊加分類頭作為基線醫(yī)學(xué)影像分類的基線不需要一上來就上3D ResNet這類重模型先把一個(gè)簡(jiǎn)單的3D CNN跑通確認(rèn)數(shù)據(jù)流沒問題再逐步加深才合理。我常用這樣的結(jié)構(gòu)import torch.nn as nn class Net3D(nn.Module): def __init__(self, in_channels1, base_channels16, num_classes1): super().__init__() self.block1 self._make_block(in_channels, base_channels) self.block2 self._make_block(base_channels, base_channels * 2) self.block3 self._make_block(base_channels * 2, base_channels * 4) self.pool nn.AdaptiveAvgPool3d(1) self.head nn.Linear(base_channels * 4, num_classes) def _make_block(self, cin, cout): return nn.Sequential( nn.Conv3d(cin, cout, kernel_size3, padding1), nn.BatchNorm3d(cout), nn.ReLU(inplaceTrue), nn.MaxPool3d(kernel_size2, stride2), ) def forward(self, x): x self.block1(x) x self.block2(x) x self.block3(x) x self.pool(x) return self.head(x.view(x.size(0), -1))輸入48×48×48經(jīng)過三層2倍池化后變成6×6×6通道數(shù)從16擴(kuò)到64特征圖尺寸縮小但語義信息加深這是卷積網(wǎng)絡(luò)提取三維特征的常規(guī)路徑。BatchNorm3d放在卷積和激活之間能顯著緩解訓(xùn)練初期梯度不穩(wěn)定AdaptiveAvgPool3d把任意尺寸的特征圖壓成1×1×1保證最后的全連接層輸入維度固定。head層不接sigmoid訓(xùn)練時(shí)配合BCEWithLogitsLoss在內(nèi)部做數(shù)值穩(wěn)定的二分類計(jì)算推理時(shí)再取sigmoid得到概率。4.3 訓(xùn)練循環(huán)與提交生成保存驗(yàn)證AUC最優(yōu)權(quán)重訓(xùn)練循環(huán)的核心不是把epoch跑完而是把驗(yàn)證集AUC最高的那一輪權(quán)重留下來。很多經(jīng)驗(yàn)不足的寫法是保存最后一個(gè)epoch的權(quán)重如果后面幾輪過擬合提交效果反而更差import torch import torch.nn as nn from sklearn.metrics import roc_auc_score criterion nn.BCEWithLogitsLoss() optimizer torch.optim.Adam(model.parameters(), lrcfg[train][lr]) best_auc 0.0 for epoch in range(cfg[train][epochs]): model.train() for x, y in train_loader: x, y x.to(device), y.to(device) pred model(x) loss criterion(pred, y) optimizer.zero_grad() loss.backward() optimizer.step() # 驗(yàn)證集AUC作為保存依據(jù) model.eval() preds, labels [], [] with torch.no_grad(): for x, y in val_loader: x x.to(device) preds.append(torch.sigmoid(model(x)).cpu().numpy()) labels.append(y.numpy()) auc roc_auc_score(labels, preds) print(fepoch {epoch1}, val_auc{auc:.4f}) if auc best_auc: best_auc auc torch.save(model.state_dict(), best_model.pth)學(xué)習(xí)率1e-3配合Adam是三維小數(shù)據(jù)集上比較通用的起點(diǎn)如果訓(xùn)練loss震蕩不下優(yōu)先把lr降到1e-4再試。訓(xùn)練結(jié)束后生成提交文件順序必須以sampleSubmission模板為準(zhǔn)import pandas as pd def make_submission(model, cfg): sub pd.read_csv(cfg[data][submission]) model.load_state_dict(torch.load(best_model.pth)) model.eval() probs [] for pid in sub[id]: npz_path fcandidate{pid}.npz vol load_and_unify(npz_path, (cfg[preprocess][image_size],) * 3) vol normalize_volume(vol) x torch.from_numpy(vol).unsqueeze(0).unsqueeze(0).to(device) with torch.no_grad(): probs.append(torch.sigmoid(model(x)).item()) sub[probability] probs assert sub[probability].notna().all(), 提交文件存在空值 sub.to_csv(submission_final.csv, indexFalse)按模板id逐個(gè)計(jì)算概率而不是對(duì)預(yù)測(cè)結(jié)果排序后直接覆蓋這個(gè)順序錯(cuò)了就全錯(cuò)。最后的notna校驗(yàn)是提交前的最后一道防線存盤前只要有一行空值就拋異常避免把殘缺文件交出去。跑通這一整個(gè)閉環(huán)模型的訓(xùn)練、評(píng)估、提交三件事才算全部落地。5. 避坑指南3D影像分類最容易翻車的五個(gè)點(diǎn)三維醫(yī)學(xué)影像分類的坑比2D任務(wù)多一層因?yàn)閿?shù)據(jù)格式不透明、內(nèi)存開銷大、標(biāo)注不平衡又常見。下面五個(gè)點(diǎn)按數(shù)據(jù)讀入、預(yù)處理與訓(xùn)練、驗(yàn)證與提交三個(gè)階段梳理每條都是實(shí)際項(xiàng)目中踩過的真實(shí)教訓(xùn)。5.1 數(shù)據(jù)讀入階段npz字段名是黑匣子現(xiàn)象按資料里描述的字段名取數(shù)一運(yùn)行就報(bào)KeyError或者打印出來的數(shù)組shape和預(yù)期完全對(duì)不上。原因npz是壓縮二進(jìn)制容器內(nèi)部鍵名不是標(biāo)準(zhǔn)化的不同來源的文件可能叫images、data、volume甚至只有一個(gè)匿名鍵。直接用寫死的鍵名索引等于把代碼押在一個(gè)沒驗(yàn)證過的假設(shè)上。解決在寫任何數(shù)據(jù)處理邏輯之前先執(zhí)行2.1節(jié)里的files打印腳本把每個(gè)鍵對(duì)應(yīng)的shape和dtype確認(rèn)一遍。我的習(xí)慣是把這個(gè)探查代碼直接放在工程入口處每位拿到項(xiàng)目的人第一眼就能看到數(shù)據(jù)結(jié)構(gòu)長(zhǎng)什么樣后面所有字段索引都從探查結(jié)果里來不再靠猜。5.2 一次性預(yù)讀所有npz導(dǎo)致內(nèi)存被殺現(xiàn)象代碼在本地小規(guī)模測(cè)試沒問題一旦加載全部樣本訓(xùn)練剛開始幾分鐘系統(tǒng)內(nèi)存就暴漲甚至直接被操作系統(tǒng)終止。原因?yàn)榱耸∈掳阉衝pz提前l(fā)oad進(jìn)list再拼成一個(gè)大數(shù)組。48×48×48的float32數(shù)組單個(gè)約0.44MB幾千個(gè)樣本累加就是幾個(gè)GB加上PyTorch的顯存開銷和DataLoader的預(yù)取緩沖內(nèi)存很容易被吃滿。解決把Dataset改成按需讀取每個(gè)__getitem__只load當(dāng)前樣本處理完就釋放再配合num_workers控制預(yù)取線程數(shù)。如果還想再省一點(diǎn)數(shù)據(jù)集加載順序用shuffle打亂即可不需要一次把全部數(shù)據(jù)搬進(jìn)內(nèi)存。5.3 歸一化順序錯(cuò)亂導(dǎo)致指標(biāo)像玄學(xué)現(xiàn)象同樣的模型結(jié)構(gòu)今天跑AUC 0.85明天換一種歸一化方式掉到0.7怎么看都覺得代碼沒問題。原因先對(duì)原始體素做歸一化再做resize插值插值過程會(huì)把歸一化后的分布重新擾動(dòng)反過來先resize再歸一化統(tǒng)計(jì)量才是真實(shí)反映縮放后數(shù)據(jù)的。另一個(gè)常見原因是沒先統(tǒng)計(jì)訓(xùn)練集分布就套用固定均值導(dǎo)致歸一化后數(shù)據(jù)偏移。解決統(tǒng)一順序?yàn)椤疤讲榉植?→ resize → 歸一化”。先用一行代碼統(tǒng)計(jì)訓(xùn)練集所有樣本的min、max、mean、std再?zèng)Q定用z-score還是固定窗寬并把統(tǒng)計(jì)結(jié)果隨config一起保存保證實(shí)驗(yàn)可復(fù)現(xiàn)。5.4 類別不平衡時(shí)準(zhǔn)確率騙人現(xiàn)象訓(xùn)練日志里準(zhǔn)確率一路漲到0.93AUC卻只有0.55模型對(duì)正樣本幾乎沒有區(qū)分能力。原因正負(fù)樣本比例懸殊模型只要全預(yù)測(cè)成多數(shù)類就能拿到高準(zhǔn)確率此時(shí)用準(zhǔn)確率做保存權(quán)重的依據(jù)選出來的最優(yōu)權(quán)重恰恰是最“偷懶”的那個(gè)。解決所有評(píng)估和模型保存都改用AUC訓(xùn)練集劃分時(shí)設(shè)置stratify保證正負(fù)比例一致如果類別差異特別大可以給loss加類別權(quán)重或者對(duì)少數(shù)類做加權(quán)采樣讓每個(gè)batch里都有足夠多的陽性樣本參與梯度計(jì)算。5.5 驗(yàn)證集泄漏與提交模板錯(cuò)位現(xiàn)象驗(yàn)證集AUC很高換到0617.csv上評(píng)估明顯變差或者提交后返回的結(jié)果異常低檢查發(fā)現(xiàn)概率行和樣本ID對(duì)不上。原因train_val與0617.csv存在重合的candidate_id同一個(gè)樣本既參與訓(xùn)練又參與驗(yàn)證指標(biāo)自然虛高提交階段如果對(duì)預(yù)測(cè)結(jié)果重新排序后再寫入模板ID順序錯(cuò)位會(huì)讓整份答案作廢。解決數(shù)據(jù)處理階段先對(duì)兩份標(biāo)注表做candidate_id交集檢查有交集就放在同一份訓(xùn)練集里0617.csv僅用作最終獨(dú)立驗(yàn)證生成提交文件時(shí)嚴(yán)格用模板的id逐行計(jì)算存盤前斷言沒有NaN并核對(duì)行數(shù)等于模板行數(shù)。這兩步不涉及任何調(diào)參技巧純粹是流程紀(jì)律但幾乎決定整個(gè)項(xiàng)目驗(yàn)收能否通過。6. 驗(yàn)收與答辯把訓(xùn)練日志和指標(biāo)講成加分項(xiàng)6.1 驗(yàn)收清單先證明可復(fù)現(xiàn)再談指標(biāo)驗(yàn)收項(xiàng)判定標(biāo)準(zhǔn)操作建議環(huán)境復(fù)現(xiàn)README中的依賴安裝后可直接運(yùn)行用干凈環(huán)境跑通一次main訓(xùn)練閉環(huán)訓(xùn)練能完整跑完一個(gè)epoch且內(nèi)存穩(wěn)定觀察系統(tǒng)監(jiān)控確認(rèn)無OOM指標(biāo)留檔保存每個(gè)epoch的AUC與最優(yōu)權(quán)重訓(xùn)練日志、config.yaml、best_model.pth放同一目錄提交合規(guī)submission_final.csv無空值、行數(shù)等于模板運(yùn)行notna斷言并核對(duì)長(zhǎng)度獨(dú)立驗(yàn)證0617.csv上的評(píng)估結(jié)果合理訓(xùn)練結(jié)束后加載最優(yōu)權(quán)重重新評(píng)估這份資源交付時(shí)自帶README和運(yùn)行結(jié)果第一輪驗(yàn)收就按清單逐項(xiàng)核對(duì)能避免答辯時(shí)被發(fā)現(xiàn)訓(xùn)練日志缺失或者權(quán)重文件與代碼不匹配。6.2 答辯技巧講一個(gè)修復(fù)過程勝過背十層網(wǎng)絡(luò)結(jié)構(gòu)答辯時(shí)常規(guī)流程是講模型結(jié)構(gòu)但這個(gè)項(xiàng)目真正體現(xiàn)工程能力的地方在數(shù)據(jù)組織與驗(yàn)證策略。可以準(zhǔn)備兩件素材一是把npz字段探查和尺寸統(tǒng)一的過程講清楚說明你拿到黑匣子數(shù)據(jù)的第一反應(yīng)是先看結(jié)構(gòu)再寫代碼二是講一次參數(shù)調(diào)整比如lr從1e-2發(fā)散、降到1e-3后loss收斂這類細(xì)節(jié)比復(fù)述Conv3d的padding更讓人信服。再補(bǔ)充0617.csv作獨(dú)立驗(yàn)證的設(shè)計(jì)證明你考慮過泛化性而不是只盯著訓(xùn)練集指標(biāo)。從那次把驗(yàn)證集混進(jìn)去導(dǎo)致答辯被追問翻車以后我每次拿到這種交付型資源都會(huì)強(qiáng)制自己先把訓(xùn)練、驗(yàn)證、提交三組數(shù)據(jù)分開再寫訓(xùn)練代碼最后核對(duì)提交模板順序整套流程走一遍才覺得踏實(shí)。希望你拿到這套方案后也按這個(gè)順序跑通一次把每個(gè)環(huán)節(jié)的產(chǎn)物都留檔答辯時(shí)自然有底氣。祝順利。本文還有配套的精品資源點(diǎn)擊獲取