習(xí)的皮膚燒傷檢測(cè)項(xiàng)目復(fù)現(xiàn)與避坑指南)
簡(jiǎn)介一份圍繞皮膚燒傷檢測(cè)的深度學(xué)習(xí)實(shí)戰(zhàn)項(xiàng)目基于卷積神經(jīng)網(wǎng)絡(luò)自動(dòng)識(shí)別燒傷范圍、深度與愈合階段利用深度學(xué)習(xí)提升燒傷診斷的準(zhǔn)確性與效率主要面向醫(yī)療AI研究者、算法工程師與計(jì)算機(jī)視覺(jué)學(xué)習(xí)者既適合算法驗(yàn)證也便于二次開(kāi)發(fā)。壓縮包共有168個(gè)文件以43個(gè)Python腳本、38個(gè)YAML配置、16個(gè)Jupyter Notebook及多格式圖片、CSV記錄文件為主其中py為訓(xùn)練與推理工具、yaml用于模型參數(shù)配置、ipynb保留實(shí)驗(yàn)分析過(guò)程另有Dockerfile、Shell腳本輔助環(huán)境部署覆蓋數(shù)據(jù)預(yù)處理、模型搭建、訓(xùn)練調(diào)參、日志輸出與結(jié)果可視化等完整環(huán)節(jié)整體大小僅41.69MB目錄組織清晰可直接進(jìn)入訓(xùn)練流程。目前已有105人學(xué)習(xí)下載資源額外配套了從數(shù)據(jù)收集、預(yù)處理、模型設(shè)計(jì)、訓(xùn)練測(cè)試到部署驗(yàn)證的流程教程幫助初學(xué)者理解燒傷識(shí)別任務(wù)中CNN如何提取關(guān)鍵特征也可基于源碼快速?gòu)?fù)現(xiàn)實(shí)驗(yàn)結(jié)果或遷移到其他醫(yī)學(xué)圖像分類場(chǎng)景。整體是一套端到端的優(yōu)質(zhì)實(shí)戰(zhàn)案例能提供可復(fù)用框架與系統(tǒng)化工程思路。1. 皮膚燒傷檢測(cè)為什么選深度學(xué)習(xí)先把“測(cè)什么”和“能測(cè)準(zhǔn)多少”對(duì)齊“皮膚燒傷檢測(cè)”這個(gè)標(biāo)題拆到底其實(shí)是一件事讓深度學(xué)習(xí)算法在創(chuàng)面圖片上給出燒傷深度分級(jí)而不是做“有傷/無(wú)傷”的二分類。醫(yī)生在處理早期燒傷時(shí)最吃不準(zhǔn)的就是淺Ⅱ度還是深Ⅱ度這直接決定保守?fù)Q藥還是手術(shù)切痂而肉眼判斷本來(lái)就有明顯的主觀性。正因?yàn)檫@個(gè)痛點(diǎn)“基于深度學(xué)習(xí)的燒傷檢測(cè)”才成為醫(yī)療 AI 項(xiàng)目里既現(xiàn)實(shí)又有動(dòng)手空間的課題。適合誰(shuí)看想用一份帶源碼和流程教程的項(xiàng)目入門醫(yī)學(xué)圖像分類的開(kāi)發(fā)者或者手里只有幾百?gòu)垬?biāo)注圖、想在本地跑通訓(xùn)練和驗(yàn)證的工程人員。下文按我復(fù)現(xiàn)這類項(xiàng)目的習(xí)慣拆開(kāi)講任務(wù)定義、數(shù)據(jù)集組織、訓(xùn)練參數(shù)再講四個(gè)復(fù)現(xiàn)時(shí)最常見(jiàn)的坑。2. 復(fù)現(xiàn)這套皮膚燒傷檢測(cè)項(xiàng)目任務(wù)拆分、數(shù)據(jù)集來(lái)源與最小依賴環(huán)境2.1 燒傷檢測(cè)不等于一塊模型分類做分級(jí)分割做定位拿到“基于深度學(xué)習(xí)實(shí)現(xiàn)的皮膚燒傷檢測(cè)算法”這類項(xiàng)目時(shí)先別急著解壓訓(xùn)練得確定你要交付的是“整張圖的燒傷等級(jí)”還是“圖上哪一塊是幾度燒傷”。這兩個(gè)問(wèn)題對(duì)應(yīng)的模型完全不同。常見(jiàn)做法是先把任務(wù)分成兩類。第一類是分類輸入一張裁剪好的創(chuàng)面局部圖輸出四個(gè)類別——I 度、淺Ⅱ度、深Ⅱ度、Ⅲ度。這類模型實(shí)現(xiàn)簡(jiǎn)單一個(gè) ResNet 加一個(gè)分類頭就能跑訓(xùn)練腳本干凈驗(yàn)證時(shí)看混淆矩陣也直觀。第二類是分割逐像素輸出每個(gè)位置的燒傷深度適合面積大、同時(shí)存在多種深度的創(chuàng)面術(shù)前規(guī)劃更需要這種但標(biāo)注成本高模型出來(lái)也重。我的建議是如果這個(gè) zip 里的流程教程只給了 train.py 和 infer.py默認(rèn)它大概率是分類任務(wù)。先按分類把它跑通得到 baseline 之后再考慮給模型加分割分支。燒傷創(chuàng)面往往混合了紅、白、黃灰和焦痂分類模型對(duì)這個(gè)信號(hào)的抓取比想象中更直接而分割網(wǎng)絡(luò)在小數(shù)據(jù)集上反而容易學(xué)到錯(cuò)誤的邊界。源碼項(xiàng)目復(fù)現(xiàn)的第一步不是追求“模型高級(jí)”而是把任務(wù)邊界先立住。2.2 數(shù)據(jù)怎么攢成標(biāo)準(zhǔn)結(jié)構(gòu)目錄劃分與標(biāo)簽映射復(fù)現(xiàn)時(shí)最容易卡住的是數(shù)據(jù)。燒傷圖像不像 ImageNet 一條命令就能下來(lái)源一般三塊公開(kāi)醫(yī)學(xué)圖像庫(kù)、醫(yī)院脫敏數(shù)據(jù)、從公開(kāi)競(jìng)賽和教科書(shū)資料中人工整理。量通常不大幾百到兩三千張都算正常。你手里的 zip 里如果帶了數(shù)據(jù)優(yōu)先看它的目錄結(jié)構(gòu)如果沒(méi)帶自己得先按下面的方式搭一份。data/ train/ superficial/ # 淺Ⅱ度 deep/ # 深Ⅱ度 full/ # Ⅲ度 / 全層 val/ test/這種按類別分文件夾的結(jié)構(gòu)配合 torchvision 的ImageFolder直接能讀是“源碼流程教程”類項(xiàng)目最常見(jiàn)的格式。我一般會(huì)在數(shù)據(jù)目錄里再放一個(gè)label_map.json把文件夾名映射到整數(shù)避免訓(xùn)練時(shí)類別順序被 filesystem 順序搞亂{ first_degree: 0, superficial: 1, deep: 2, full: 3 }劃分?jǐn)?shù)據(jù)集時(shí)有一個(gè)關(guān)鍵原則按病人劃分而不是按圖片劃分。同一患者的多張創(chuàng)面照片高度相似如果同一患者照片同時(shí)出現(xiàn)在 train 和 val驗(yàn)證集分?jǐn)?shù)會(huì)被數(shù)據(jù)泄漏虛高等部署到新病人身上就直接“翻車”。我經(jīng)歷過(guò)一次看起來(lái) val acc 95%、換新數(shù)據(jù)掉到 60% 的情況后來(lái)反復(fù)查才發(fā)現(xiàn)是劃分時(shí)沒(méi)按patient_id做 group split。這是燒傷檢測(cè)這類小樣本醫(yī)學(xué)項(xiàng)目最典型的坑。2.3 環(huán)境固定用盡量少的依賴先把訓(xùn)練腳本跑起來(lái)不要一上來(lái)就裝一堆深度學(xué)習(xí)全家桶。復(fù)現(xiàn)源碼項(xiàng)目的核心是先讓入口腳本能跑依賴越少越好排查。我通常會(huì)先把 requirements 釘成下面這幾行torch2.0.0 torchvision0.15.0 opencv-python4.5.0 tqdm scikit-learn pandas然后新建虛擬環(huán)境安裝python -m venv .venv source .venv/bin/activate pip install -r requirements.txt邏輯說(shuō)明torch 和 torchvision 必須保持主版本對(duì)應(yīng)否則加載預(yù)訓(xùn)練權(quán)重時(shí)容易報(bào)size mismatch或直接ImportError。opencv 用于讀圖和導(dǎo)出scikit-learn 用來(lái)算混淆矩陣和分類報(bào)告這幾樣夠覆蓋整個(gè)流程。如果你機(jī)器沒(méi)有 GPUpip 會(huì)自動(dòng)裝 CPU 版還是 CUDA 版取決于 PyTorch 官方給出的安裝命令這一步在第五章會(huì)展開(kāi)這里只要保證 import 不出錯(cuò)就行。還有一條血淚經(jīng)驗(yàn)解壓路徑不要帶中文和空格。很多 zip 里的腳本用的是相對(duì)路徑路徑里一旦出現(xiàn)非 ASCII 字符opencv 讀圖和 torch 保存權(quán)重都可能直接報(bào)錯(cuò)。這種錯(cuò)誤表面上看是環(huán)境問(wèn)題實(shí)際上改一個(gè)文件夾名字就好了。3. 把訓(xùn)練腳本改造成能收斂的閉環(huán)從過(guò)擬合單batch到參數(shù)穩(wěn)定區(qū)3.1 第一口飯過(guò)擬合四十張圖先讓代碼“能學(xué)”拿到源碼后的第一個(gè)動(dòng)作不是完整訓(xùn)練而是先在幾十張圖上把 loss 打下來(lái)。這一步的目的很簡(jiǎn)單確認(rèn)數(shù)據(jù)加載、模型前向、損失計(jì)算、梯度回傳這條鏈路是通的而不是花兩小時(shí)跑完一個(gè) epoch 才發(fā)現(xiàn) loss 壓根沒(méi)動(dòng)。# 固定取一個(gè) batch看模型能不能記住它 sample_images, sample_labels next(iter(train_loader)) model.train() for step in range(200): logits model(sample_images.to(device)) loss criterion(logits, sample_labels.to(device)) optimizer.zero_grad() loss.backward() optimizer.step() if step % 20 0: print(fstep {step:3d} | loss {loss.item():.4f})這個(gè)腳本里只用一個(gè) batch比如 16 張圖跑 200 步。如果代碼沒(méi)問(wèn)題loss 應(yīng)當(dāng)在前幾十步內(nèi)明顯下降最后能逼近 0.1 以下。如果 loss 降不下去先查三件事標(biāo)簽是否傳錯(cuò)、模型參數(shù)是否被requires_gradFalse釘死、數(shù)據(jù)讀取后像素值是否異常。這一步是動(dòng)手深度學(xué)習(xí)里最廉價(jià)的“體檢”比看任何教程都管用。3.2 模型結(jié)構(gòu)選型預(yù)訓(xùn)練 ResNet 替換分類頭燒傷圖像在 ImageNet 里幾乎不存在但預(yù)訓(xùn)練權(quán)重依然值得用。原因在于燒傷深度判級(jí)依賴創(chuàng)面的顏色紋理和表皮結(jié)構(gòu)特征而這些低級(jí)視覺(jué)特征——邊緣、顏色漸變、組織塊狀紋理——和 ImageNet 上學(xué)到的通用表征是共享的。直接用隨機(jī)初始化在小數(shù)據(jù)集上從零訓(xùn)基本會(huì)被 sample size 打回原形。import torchvision from torch import nn model torchvision.models.resnet50( weightstorchvision.models.ResNet50_Weights.IMAGENET1K_V1 ) model.fc nn.Linear(model.fc.in_features, num_classes4)替換掉最后的fc是因?yàn)樵瓉?lái)的輸出是 1000 類我們的分類數(shù)是 4。in_features從model.fc.in_features拿不要手寫(xiě) 2048這樣以后換 resnet18 時(shí)不用改第二行。選定這個(gè)結(jié)構(gòu)之后我建議先全凍結(jié)骨干只訓(xùn)練分類頭跑幾個(gè) epoch 看到 val 能到基線后再解凍最后一段進(jìn)行微調(diào)。常見(jiàn)的誤用是一上來(lái)就全參數(shù)requires_gradTrue加上大學(xué)習(xí)率后面 4.2 會(huì)專門說(shuō)這個(gè)。3.3 三個(gè)開(kāi)箱參數(shù)圖像尺寸、學(xué)習(xí)率與早停訓(xùn)練參數(shù)直接決定你這個(gè)源碼項(xiàng)目能跑出多少分。下面這組是燒傷分類項(xiàng)目比較省事的起點(diǎn)我復(fù)現(xiàn)同類項(xiàng)目時(shí)基本沿用這個(gè)底座再按數(shù)據(jù)量微調(diào)參數(shù)起點(diǎn)值調(diào)整策略輸入尺寸224 x 224創(chuàng)面紋理細(xì)節(jié)不足時(shí)升到 384但訓(xùn)練時(shí)間翻倍batch size16顯存不夠降到 8同時(shí)學(xué)習(xí)率減半優(yōu)化器AdamWweight_decay 建議 0.01比 SGD 穩(wěn)學(xué)習(xí)率分類頭 3e-4骨干 1e-5解凍骨干后一定要降學(xué)習(xí)率epoch30 起加到 val loss 不再下降為止訓(xùn)練循環(huán)里最關(guān)鍵的是“按 val loss 存模型而不是按 val acc”。best_val_loss float(inf) no_improve_epochs 0 for epoch in range(max_epochs): train_loss train_one_epoch(model, train_loader, optimizer, criterion) val_loss evaluate(model, val_loader, criterion) if val_loss best_val_loss: best_val_loss val_loss no_improve_epochs 0 torch.save(model.state_dict(), best.pt) else: no_improve_epochs 1 if no_improve_epochs 5: print(fearly stop at epoch {epoch}) break邏輯說(shuō)明evaluate只返回 val loss 不返回 acc是故意的。燒傷數(shù)據(jù)類別不均衡準(zhǔn)確率在“全猜淺Ⅱ度”時(shí)也能到 70% 以上用它判斷是否保留模型會(huì)把最需要關(guān)注的深Ⅱ度和Ⅲ度樣本帶偏。val loss 不降反升持續(xù) 5 個(gè) epoch就加載歷史最優(yōu)權(quán)重這是最常見(jiàn)也最有效的早停方案。3.4 多任務(wù)和分割分支先別貪有些“優(yōu)質(zhì)項(xiàng)目實(shí)戰(zhàn)”會(huì)附一套 U-Net 風(fēng)格的分割分支同時(shí)輸出分類和像素級(jí)掩膜。做法通常是在 ResNet 骨干后并行接一個(gè)解碼器總 loss 設(shè)為0.8 * DiceLoss 0.2 * CrossEntropyLoss。原理上合理但第一次復(fù)現(xiàn)時(shí)我不建議開(kāi)雙頭訓(xùn)練。原因很實(shí)際兩個(gè)任務(wù)的收斂速度不同尤其在幾十個(gè) epoch 內(nèi)聯(lián)合訓(xùn)練很容易出現(xiàn)分類沒(méi)學(xué)好、分割也沒(méi)學(xué)好的兩頭空局面。4. 燒傷檢測(cè)復(fù)現(xiàn)避坑四個(gè)高頻問(wèn)題和排查線索4.1 類不均衡偽裝成高準(zhǔn)確率現(xiàn)象訓(xùn)練集和驗(yàn)證集準(zhǔn)確率都到了 90% 以上看起來(lái)“項(xiàng)目跑通了”但一打印分類報(bào)告深Ⅱ度和Ⅲ度的 recall 只有 20%。原因燒傷樣本里淺Ⅱ度占大頭深Ⅱ度和全層燒傷樣本少模型偏向?qū)W大類的統(tǒng)計(jì)規(guī)律。解決方法先看混淆矩陣再給損失函數(shù)加權(quán)。from sklearn.metrics import classification_report, confusion_matrix y_true, y_pred [], [] for images, labels in val_loader: logits model(images.to(device)) y_pred.extend(logits.argmax(1).cpu().tolist()) y_true.extend(labels.tolist()) print(classification_report(y_true, y_pred, digits3))加權(quán)方法是給CrossEntropyLoss傳weight權(quán)重按“類別樣本數(shù)的倒數(shù)”做平滑import torch # 手動(dòng)給一個(gè)類的權(quán)重順序與 label_map 一致 class_weights torch.tensor([1.0, 1.5, 3.0, 8.0]) criterion torch.nn.CrossEntropyLoss(weightclass_weights)權(quán)重大小不是越大越好。我試過(guò)按嚴(yán)格樣本數(shù)反比把全層燒傷權(quán)重拉到 20 倍結(jié)果模型反而把大量淺Ⅱ度誤判成Ⅲ度。一般先給 24 倍梯度觀察 confusion matrix 里低頻類的 recall 是否抬升再逐步加大。醫(yī)學(xué)判級(jí)場(chǎng)景里模型寧可過(guò)度傾向嚴(yán)重類別也不該把深Ⅱ度漏成淺Ⅱ度。4.2 遷移學(xué)習(xí)反而掉點(diǎn)預(yù)訓(xùn)練特征被大學(xué)習(xí)率打散現(xiàn)象訓(xùn)練腳本如果直接全量微調(diào)學(xué)習(xí)率設(shè)成標(biāo)準(zhǔn)的 1e-3loss 第一個(gè) epoch 就震蕩val loss 遠(yuǎn)高于隨機(jī)初始化。原因ImageNet 預(yù)訓(xùn)練的骨干特征已經(jīng)被訓(xùn)練得很好新?lián)Q上的分類頭是隨機(jī)權(quán)重。梯度剛開(kāi)始主要來(lái)自隨機(jī)頭大學(xué)習(xí)率回傳到骨干把預(yù)訓(xùn)練學(xué)到的紋理和顏色過(guò)濾器毀得七七八八。解決分兩步。第一步分類頭單獨(dú)訓(xùn)幾個(gè) epoch第二步解凍骨干把學(xué)習(xí)率降到 1e-5 左右繼續(xù)微調(diào)。# 第一階段只訓(xùn)練分類頭 for name, param in model.named_parameters(): param.requires_grad name.startswith(fc) optimizer torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr3e-4, weight_decay0.01 ) # 第二階段解凍全部骨干 for param in model.parameters(): param.requires_grad True optimizer torch.optim.AdamW( model.parameters(), lr1e-5, weight_decay0.01 )這套“先頭后干”的順序基本適用于所有醫(yī)療圖像小數(shù)據(jù)集的遷移學(xué)習(xí)場(chǎng)景。如果你發(fā)現(xiàn)某個(gè)“源碼包”里直接寫(xiě)了lr1e-3且沒(méi)有任何凍結(jié)邏輯大概率作者只在小規(guī)模 demo 上跑通并沒(méi)有仔細(xì)考慮過(guò)泛化。4.3 顏色增廣把燒傷判據(jù)“洗掉”現(xiàn)象訓(xùn)練時(shí)加入了RandomBrightnessContrast、RandomSaturation甚至隨機(jī)色調(diào)偏移然后發(fā)現(xiàn)驗(yàn)證集崩潰。原因燒傷深度分級(jí)的核心判據(jù)就是創(chuàng)面顏色——紅色區(qū)域多為淺Ⅱ度蒼白或蠟白是深Ⅱ度焦黑壞死則是Ⅲ度。一旦顏色增廣過(guò)猛模型只能退而尋找紋理和形狀信息而這些信號(hào)在真實(shí)照片里穩(wěn)定性遠(yuǎn)不如顏色。我一般把顏色增廣限制在極窄范圍from torchvision import transforms as T train_transform T.Compose([ T.RandomResizedCrop(224, scale(0.8, 1.0)), T.RandomHorizontalFlip(), T.ColorJitter(brightness0.1, contrast0.1, saturation0, hue0), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])飽和度和色調(diào)直接設(shè)為 0亮度對(duì)比度各只給 0.1這個(gè)幅度足夠應(yīng)付拍攝環(huán)境差異又不會(huì)把“紅”變成“白”。如果項(xiàng)目自帶的增廣里有RandomGrayscale建議直接刪掉灰度圖對(duì)燒傷判級(jí)基本沒(méi)有可用信息。4.4 zip 源碼跑不通的“三件套”排查現(xiàn)象解壓后運(yùn)行python train.py報(bào)錯(cuò)要么是ModuleNotFoundError要么是加載預(yù)訓(xùn)練權(quán)重時(shí)size mismatch。原因大概是三類requirements 沒(méi)裝全但不報(bào)依賴錯(cuò)、預(yù)訓(xùn)練權(quán)重文件缺失或路徑不對(duì)、Python 版本與 torch 安裝版本錯(cuò)位。排查順序固定成“三件套”。先看入口文件最前面的 import凡是第三方庫(kù)先 pip 補(bǔ)上再找模型加載那一段如果代碼寫(xiě)了torch.load(weights/resnet50.pth)而項(xiàng)目目錄里沒(méi)有這個(gè)文件那這就是第一問(wèn)題最后用一個(gè)短命令確認(rèn) torch 和 torchvision 能同時(shí)導(dǎo)入python -c import torch, torchvision; print(torch.__version__, torchvision.__version__)如果還出現(xiàn)size mismatch多半是加載的權(quán)重來(lái)自另一種模型架構(gòu)比如代碼期望 ResNet50 你卻在 torchvision 里取了 ResNet101。不要試圖硬改state_dict把加載方式改成 torchvision 自帶的weights...IMAGENET1K_V1最省事。5. 沒(méi)有GPU怎么辦用CPU把燒傷檢測(cè)源碼跑出有效結(jié)果5.1 思路轉(zhuǎn)變別用“訓(xùn)練”的思路用“特征再利用”很多人的困惑是手里只有一臺(tái)普通筆記本CPU 上跑 ResNet50 全量微調(diào)一個(gè) epoch 幾十分鐘看起來(lái)完全做不下去。解決辦法不是去買 GPU而是把任務(wù)降維成“線性探測(cè)”linear probe用預(yù)訓(xùn)練模型把圖像轉(zhuǎn)成特征向量然后用一個(gè)線性層做分類。這么做的道理在于燒傷分級(jí)數(shù)據(jù)量本來(lái)就少模型真正需要學(xué)習(xí)的是一個(gè)“如何在預(yù)訓(xùn)練特征空間里劃界”的淺層問(wèn)題沒(méi)必要重新訓(xùn)練卷積核。CPU 上跑 ResNet 推理一張 224 的圖確實(shí)要時(shí)間但如果只提取一次特征保存到內(nèi)存成本是一次性的。這比硬上剪枝算法實(shí)在得多——剪枝算法是同一模型反復(fù)迭代時(shí)用的加速手段第一次復(fù)現(xiàn)動(dòng)剪枝很容易把精度壓垮。5.2 一個(gè)能下班的 CPU 訓(xùn)練腳本import torch import torchvision from torch import nn # 主干提取特征但不做分類 base torchvision.models.resnet18( weightstorchvision.models.ResNet18_Weights.IMAGENET1K_V1 ) base.fc nn.Identity() # 去掉分類頭輸出變成 512 維特征 for param in base.parameters(): param.requires_grad False head nn.Linear(512, 4)這個(gè)base一旦加載完畢訓(xùn)練階段它完全處于“數(shù)據(jù)搬運(yùn)”狀態(tài)不參與梯度計(jì)算。整個(gè)模型的待訓(xùn)練參數(shù)只有head的權(quán)重和偏置也就 2048 個(gè)參數(shù)CPU 上跑十來(lái)個(gè) epoch 幾分鐘就能完成。配合前面的數(shù)據(jù)集劃分效果雖然不如全量微調(diào)但對(duì)幾百?gòu)垐D的燒傷分類項(xiàng)目已經(jīng)夠用。# 把特征放進(jìn)線性頭里訓(xùn)練 features base(images) # [batch, 512] logits head(features) # [batch, 4] loss criterion(logits, labels)如果嫌 ResNet18 還慢可以把輸入尺寸從 224 降到 160ResNet18 的瓶頸在最后幾個(gè) stage降分辨率帶來(lái)的速度收益遠(yuǎn)大于精度損失。160 尺寸配合隨機(jī)裁剪縮放基本不影響讀色塊。5.3 CPU 推理出口導(dǎo)出 ONNX 做輕量驗(yàn)證訓(xùn)練完成后建議導(dǎo)出 ONNX。這樣可以在不依賴 torch 的 Python 進(jìn)程里做推理也方便你把它接到 Web 端或手機(jī)端做后續(xù)項(xiàng)目演示。model.eval() dummy torch.randn(1, 3, 160, 160) torch.onnx.export( model, (dummy,), burn_classifier.onnx, input_names[image], output_names[logits], opset_version12 )導(dǎo)出后可以用 onnxruntime 加載輸入直接傳 NCHW 的 float32 張量不需要再引入 torch。邏輯說(shuō)明這個(gè)藏在“最后一章”的操作往往是整個(gè)源碼項(xiàng)目最容易被忽略的部分但實(shí)際部署價(jià)值很高因?yàn)獒t(yī)生端目標(biāo)機(jī)器通常沒(méi)有 GPUONNX 推理一次只需一次前向。6. 把“大概率對(duì)”變成“可交付”驗(yàn)證模型與不確定性設(shè)計(jì)模型在驗(yàn)證集上跑出 85% 準(zhǔn)確率不等于可以直接交給醫(yī)生。燒傷檢測(cè)的代價(jià)是非對(duì)稱的漏掉一個(gè)深Ⅱ度比多報(bào)一個(gè)淺Ⅱ度危險(xiǎn)得多。所以我最后都會(huì)再加一道不確定性過(guò)濾當(dāng)模型對(duì)某個(gè)樣本的預(yù)測(cè)概率低于閾值時(shí)輸出“無(wú)法判斷”。這個(gè)設(shè)計(jì)在醫(yī)療場(chǎng)景中叫“拒判”比硬著頭皮猜一個(gè)結(jié)果更安全。probs torch.softmax(logits, dim1) conf, pred torch.max(probs, dim1) # 低于 0.7 直接交給人工復(fù)核 if conf.item() 0.7: pred -1這個(gè)技巧對(duì)深Ⅱ度和Ⅲ度的邊界尤其有用因?yàn)檫@兩類的視覺(jué)相似度天然高強(qiáng)行細(xì)分不如主動(dòng)承認(rèn)不確定。閾值 0.7 是我常用起點(diǎn)你可以畫(huà)一條“閾值—準(zhǔn)確率”曲線來(lái)決定讓漏診率足夠低時(shí)保留多少樣本給人工。我現(xiàn)在的習(xí)慣是接觸任何一份皮膚燒傷檢測(cè)源碼到手第一件事不是看模型結(jié)構(gòu)而是跑兩個(gè) 20 行以內(nèi)的腳本一個(gè)確認(rèn)過(guò)擬合單個(gè) batch另一個(gè)打印驗(yàn)證集的 confusion matrix。兩個(gè)都不超過(guò)十分鐘但能擋掉后面大半的無(wú)效調(diào)試。這個(gè)順序幫我躲過(guò)不止一次返工希望你也能在復(fù)現(xiàn)這個(gè)項(xiàng)目時(shí)少走一段彎路希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取