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