膜病變?cè)\斷:從環(huán)境搭建到EfficientNet模型微調(diào)實(shí)踐)
簡(jiǎn)介這是一套基于Jupyter完成的糖尿病視網(wǎng)膜疾病診斷畢業(yè)設(shè)計(jì)項(xiàng)目面向計(jì)算機(jī)、人工智能、自動(dòng)化及相關(guān)專(zhuān)業(yè)學(xué)生與從業(yè)者適用于課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)或深度學(xué)習(xí)進(jìn)階練習(xí)。資源包共30個(gè)文件以16個(gè)Jupyter notebook為主完整覆蓋數(shù)據(jù)預(yù)處理、EDA探索、EfficientNet系列模型訓(xùn)練與交叉驗(yàn)證、基于logits的模型集成等關(guān)鍵環(huán)節(jié)另含2個(gè)Python腳本、9張結(jié)果標(biāo)注圖、分類(lèi)結(jié)果CSV及說(shuō)明文檔壓縮包大小30.44MB目錄結(jié)構(gòu)清晰便于按流程查閱。目前已有58人學(xué)習(xí)瀏覽代碼全部調(diào)試通過(guò)可運(yùn)行。整套資料從數(shù)據(jù)劃分、模型搭建到預(yù)測(cè)提交均有完整記錄同時(shí)附帶模型權(quán)重和文檔說(shuō)明既能幫助初學(xué)者快速?gòu)?fù)現(xiàn)糖尿病視網(wǎng)膜病變?cè)\斷流程也能作為畢業(yè)設(shè)計(jì)的高分基線(xiàn)方案——項(xiàng)目答辯評(píng)審曾獲98分具備較高的學(xué)習(xí)與二次開(kāi)發(fā)價(jià)值。1. 基于Jupyter的糖尿病視網(wǎng)膜疾病診斷源碼、數(shù)據(jù)、模型拿到手怎么落地我之前幫人看畢業(yè)設(shè)計(jì)十個(gè)里有八個(gè)卡在Jupyter跑糖尿病視網(wǎng)膜疾病診斷時(shí)環(huán)境裝不上。其實(shí)這個(gè)方向早就是公開(kāi)數(shù)據(jù)集上的成熟任務(wù)了難點(diǎn)不在算法在于你拿到源碼數(shù)據(jù)集模型文檔后能不能一次性復(fù)現(xiàn)出來(lái)。這個(gè)項(xiàng)目是典型的“畢設(shè)全家桶”Jupyter Notebook做的完整實(shí)驗(yàn)記錄、劃分好的眼底照片數(shù)據(jù)集、預(yù)訓(xùn)練權(quán)重微調(diào)腳本、還有一冊(cè)能直接改頁(yè)眉的說(shuō)明文檔。如果你正準(zhǔn)備做醫(yī)學(xué)圖像識(shí)別方向的畢設(shè)或者想把手上的眼底照片快速跑出一個(gè)像樣的分類(lèi)模型這個(gè)包值得你花一個(gè)下午從頭到尾復(fù)現(xiàn)一遍。這一章先講它是什么、適合誰(shuí)后面幾章全部按實(shí)戰(zhàn)步驟走。2. 選型邏輯為什么是Jupyter Notebook而不是PyCharm跑全套流程2.1 畢業(yè)論文和代碼不想分離Markdown寫(xiě)推導(dǎo)代碼塊做實(shí)驗(yàn)醫(yī)療影像方向的畢業(yè)設(shè)計(jì)有個(gè)特別普遍的現(xiàn)象論文里的實(shí)驗(yàn)分析比如Loss曲線(xiàn)、混淆矩陣、數(shù)據(jù)增強(qiáng)可視化圖和代碼是割裂在Word和IDE里的。你用PyCharm寫(xiě)好訓(xùn)練腳本跑完存?zhèn)€日志再去Excel里畫(huà)曲線(xiàn)再貼回論文里面來(lái)回切換至少多花一周時(shí)間。這個(gè)項(xiàng)目干脆用Jupyter Notebook把整個(gè)流程串起來(lái)。每個(gè)章節(jié)前用Markdown寫(xiě)清楚“這一節(jié)在干什么、為什么調(diào)這個(gè)參數(shù)”下面緊跟可執(zhí)行的代碼塊。比如數(shù)據(jù)清洗那節(jié)前面會(huì)有一段說(shuō)明“眼底照片邊緣經(jīng)常有黑色背景直接縮放會(huì)導(dǎo)致特征被干擾因此我們按圓形遮罩裁剪”后面就是對(duì)應(yīng)的処理代碼。你復(fù)現(xiàn)的時(shí)候等于順著作者的思路走了一遍交畢業(yè)設(shè)計(jì)時(shí)的“工作量證明”直接截代碼塊和運(yùn)行結(jié)果就有說(shuō)服力。Jupyter另一個(gè)不可替代的優(yōu)勢(shì)是增量運(yùn)行。你不需要像跑普通Python腳本那樣每次都從頭執(zhí)行全套訓(xùn)練只要數(shù)據(jù)集加載那節(jié)跑過(guò)一次后續(xù)調(diào)參時(shí)可以只重新運(yùn)行某個(gè)單元格。訓(xùn)練出問(wèn)題也不必從頭再來(lái)直接改最后一個(gè)cell重新跑就行。這對(duì)反復(fù)刷參數(shù)做對(duì)比實(shí)驗(yàn)的畢設(shè)場(chǎng)景是剛需。2.2 環(huán)境搭建Anaconda建虛擬環(huán)境安裝依賴(lài)包拿到項(xiàng)目先把隔離環(huán)境建好社區(qū)里翻車(chē)大多是因?yàn)榘秧?xiàng)目依賴(lài)直接裝到base環(huán)境導(dǎo)致包版本沖突。進(jìn)入項(xiàng)目根目錄后我一般習(xí)慣先看一眼requirements.txt里有沒(méi)有鎖版本沒(méi)有的話(huà)再手動(dòng)去裝。# 創(chuàng)建python 3.8虛擬環(huán)境不要用3.10以上的版本 # 很多老畢設(shè)源碼在3.10以上跑會(huì)出SciPy和matplotlib的兼容性錯(cuò)誤 conda create -n retina python3.8 -y conda activate retina # 安裝核心依賴(lài)這里指定版本是為了兼容Jupyter內(nèi)核 pip install torch1.10.0 torchvision0.11.0 --extra-index-url https://download.pytorch.org/whl/cu113 pip install jupyter notebook6.4.12 pip install opencv-python4.5.5.64 pillow8.4.0 scikit-learn1.0.2 pandas1.3.5 matplotlib3.5.1代碼里的邏輯分三步先創(chuàng)建獨(dú)立的Python3.8內(nèi)核避免項(xiàng)目之間環(huán)境污染再安裝PyTorch時(shí)特意指定了cu113版本對(duì)應(yīng)你的NVIDIA驅(qū)動(dòng)CUDA版本如果你用CPU跑可以去掉--extra-index-url那行最后裝的OpenCV和scikit-learn版本都是和Torch1.10驗(yàn)證過(guò)兼容的組合直接用最新版容易碰到API改名的報(bào)錯(cuò)。參數(shù)說(shuō)明里有個(gè)細(xì)節(jié)值得注意notebook6.4.12不要用JupyterLab替代。這個(gè)項(xiàng)目的.ipynb文件是舊版格式在JupyterLab上雖然能打開(kāi)但有時(shí)會(huì)出現(xiàn)markdown渲染偏移和代碼塊輸入框重疊的情況。用回經(jīng)典N(xiāo)otebook界面最穩(wěn)妥。裝完環(huán)境后終端輸入jupyter notebook接著會(huì)自動(dòng)打開(kāi)瀏覽器頁(yè)面進(jìn)入后先找到項(xiàng)目解壓目錄再打開(kāi)主文件main.ipynb。# 確認(rèn)環(huán)境沒(méi)問(wèn)題然后啟動(dòng)notebook jupyter notebook --notebook-dir你解壓的物理路徑/retina_detection這里有個(gè)常見(jiàn)誤區(qū)不要在Anaconda Prompt里直接雙擊.ipynb文件那樣它會(huì)被系統(tǒng)默認(rèn)的文本編輯器打開(kāi)根本跑不了。先進(jìn)Jupyter再找文件是對(duì)的打開(kāi)姿勢(shì)。3. 數(shù)據(jù)集準(zhǔn)備與劃分拿到的圖片不能直接灌給模型3.1 源碼包里數(shù)據(jù)集長(zhǎng)什么樣解壓后數(shù)據(jù)目錄結(jié)構(gòu)一般是這樣的路徑內(nèi)容數(shù)量級(jí)dataset/train/0/無(wú)DR標(biāo)簽的彩色眼底圖訓(xùn)練集約800~2000張dataset/train/1/輕度DR眼底圖按原數(shù)據(jù)集比例dataset/train/2/中度DR眼底圖按原數(shù)據(jù)集比例dataset/train/3/重度DR眼底圖按原數(shù)據(jù)集比例dataset/train/4/增值性DR眼底圖按原數(shù)據(jù)集比例dataset/val/按同樣5個(gè)類(lèi)分好的驗(yàn)證集占總樣本10~15%dataset/test/未知標(biāo)簽用于最終性能測(cè)試若干張文件夾命名直接以0-4作為類(lèi)別編號(hào)對(duì)應(yīng)國(guó)際通用的糖尿病視網(wǎng)膜病變嚴(yán)重程度分級(jí)0為無(wú)病變4為最嚴(yán)重的增值性病變。這個(gè)編號(hào)順序和原論文指標(biāo)里的表格是對(duì)應(yīng)上的做混淆矩陣時(shí)不用改代碼。一個(gè)很容易忽略的坑是圖片尺寸不統(tǒng)一。源數(shù)據(jù)里有些圖片能到2000×3000像素直接resize會(huì)損失大量細(xì)節(jié)。這個(gè)項(xiàng)目里已經(jīng)把原始圖片統(tǒng)一裁到224×224了但仍需確認(rèn)每張圖的通道順序是BGR還是RGB。用OpenCV讀圖默認(rèn)是BGR如果你拿PIL讀同一張圖再混著用模型輸入顏色就會(huì)錯(cuò)亂最后驗(yàn)證集AUC很難看。3.2 手寫(xiě)數(shù)據(jù)劃分腳本防止驗(yàn)證集泄漏和類(lèi)別順序偏置源碼包里自帶一個(gè)劃分好的版本但假如你要加自己的圖片進(jìn)去或者想換數(shù)據(jù)集重跑就得知道它內(nèi)部劃分的機(jī)制。項(xiàng)目里實(shí)現(xiàn)是按“病人級(jí)別”劃分這比按“圖片級(jí)別”劃分要嚴(yán)謹(jǐn)?shù)枚唷粋€(gè)人兩只眼的多張照片會(huì)被分到同一個(gè)集合不然特征重復(fù)會(huì)導(dǎo)致驗(yàn)證集得分虛高。import os import random import shutil from collections import defaultdict random.seed(42) source_root your_raw_dataset # 原始圖片路徑按類(lèi)別0-4歸類(lèi) output_root dataset train_ratio 0.85 # 思路按圖片名前綴把同一病人的圖片分到同一個(gè)桶里 patient_buckets defaultdict(list) for class_id in range(5): cls_path os.path.join(source_root, str(class_id)) for img_name in os.listdir(cls_path): # 假設(shè)文件名格式為 “病人ID_編號(hào).jpg”比如 P001_01.jpg patient_id img_name.split(_)[0] patient_buckets[patient_id].append((class_id, img_name)) for patient_id, img_list in patient_buckets.items(): random.shuffle(img_list) split_idx int(len(img_list) * train_ratio) for img_info in img_list[:split_idx]: cls_id, img_name img_info shutil.copy( os.path.join(source_root, str(cls_id), img_name), os.path.join(output_root, train, str(cls_id), img_name) ) for img_info in img_list[split_idx:]: cls_id, img_name img_info shutil.copy( os.path.join(source_root, str(cls_id), img_name), os.path.join(output_root, val, str(cls_id), img_name) )這段代碼的核心是patient_buckets這個(gè)字典它以文件名里截取出來(lái)的病人ID為主鍵把同一病人的所有照片放進(jìn)一個(gè)桶里再統(tǒng)一做切分。這樣訓(xùn)練集和驗(yàn)證集之間不會(huì)出現(xiàn)同一個(gè)病人的不同張照片交叉污染。train_ratio0.85表示每個(gè)病人約15%的照片進(jìn)驗(yàn)證集比隨機(jī)亂切更能反映真實(shí)泛化性能。跑完后建議檢查一下dataset/val/目錄下是否有空文件夾。部分類(lèi)別樣本極少時(shí)小概率會(huì)出現(xiàn)整個(gè)桶都被分進(jìn)訓(xùn)練集的情況導(dǎo)致驗(yàn)證集缺那一類(lèi)。判斷標(biāo)準(zhǔn)是驗(yàn)證集五個(gè)類(lèi)別目錄下都要有文件否則后續(xù)計(jì)算混淆矩陣那節(jié)直接報(bào)錯(cuò)。3.3 制作分類(lèi)權(quán)重類(lèi)別不平衡是醫(yī)療圖像最大的坑糖尿病數(shù)據(jù)集里0類(lèi)無(wú)病變圖片通常占大頭如果按原始比例訓(xùn)練模型會(huì)學(xué)成“全猜0”。項(xiàng)目里在數(shù)據(jù)加載階段實(shí)現(xiàn)了權(quán)重均衡采樣這是整個(gè)畢設(shè)能過(guò)盲審的關(guān)鍵點(diǎn)之一。在main.ipynb的第二個(gè)cells能看到類(lèi)似的實(shí)現(xiàn)from torch.utils.data import WeightedRandomSampler class_counts [1200, 400, 300, 180, 120] # 實(shí)際數(shù)據(jù)集的類(lèi)別分布 total_count sum(class_counts) weights [total_count / c for c in class_counts] sample_weights [] for label_idx, count in enumerate(class_counts): sample_weights.extend([weights[label_idx]] * count) sampler WeightedRandomSampler(sample_weights, num_samplestotal_count, replacementTrue)WeightedRandomSampler的原理是讓少樣本的類(lèi)比多樣本的類(lèi)更容易被抽中。這里weights計(jì)算方式就是總樣本數(shù)除以該類(lèi)數(shù)量樣本少的類(lèi)權(quán)重自然大。replacementTrue允許同一張圖在一個(gè)epoch里出現(xiàn)多次雖然增加了訓(xùn)練時(shí)間但對(duì)5分類(lèi)中類(lèi)別極不均衡的場(chǎng)景幫助很大。每訓(xùn)練完一個(gè)epoch建議打印一下每個(gè)類(lèi)別的準(zhǔn)確率不是只看總acc你會(huì)發(fā)現(xiàn)如果沒(méi)加這個(gè)采樣器3類(lèi)和4類(lèi)的召回率會(huì)低到不能看。4. 模型架構(gòu)與訓(xùn)練流程用EfficientNet做預(yù)訓(xùn)練微調(diào)不再?gòu)牧愦頒NN4.1 選型理由為什么是EfficientNet而不是ResNet眼科圖像識(shí)別在Kaggle這類(lèi)比賽里近年基本是EfficientNet系列和ResNet系列兩分天下。源碼包選擇EfficientNet的主要原因是在同樣精度水平下它的參數(shù)量和計(jì)算量比ResNet小一個(gè)量級(jí)。以EfficientNet-b3為例推理速度在GTX 1660上大約是ResNet50的1.5倍而AUC還略微領(lǐng)先。模型Top-1 Acc (ImageNet)參數(shù)量適合的畢設(shè)場(chǎng)景ResNet5075.3%25.6M設(shè)備一般追求穩(wěn)定EfficientNet-b076.8%5.3M小而快適合快速迭代EfficientNet-b381.1%12.0M性?xún)r(jià)比高本文項(xiàng)目默認(rèn)畢設(shè)答辯時(shí)老師問(wèn)你“為什么選這個(gè)模型”答案核心是因?yàn)檠鄣渍掌直媛矢?、病灶區(qū)域小EfficientNet在相同F(xiàn)LOPS下能看得更細(xì)。同時(shí)它的復(fù)合縮放策略讓寬度、深度、分辨率三個(gè)維度同時(shí)調(diào)整很容易適配224×224輸入。如果你的顯卡顯存超過(guò)6G可以把源碼包里的模型名字改成efficientnet-b4或efficientnet-b5輸入尺寸在配置文件里同步改到300或456能再漲兩個(gè)點(diǎn)。4.2 訓(xùn)練代碼核心參數(shù)解讀凍結(jié)層數(shù)、學(xué)習(xí)率、EMA模型加載部分和訓(xùn)練循環(huán)結(jié)合在一個(gè)cell里結(jié)構(gòu)不算長(zhǎng)但參數(shù)值得一個(gè)個(gè)看import torch import torch.nn as nn import torch.optim as optim from efficientnet_pytorch import EfficientNet def create_model(num_classes5, freeze_level0): # load pretrained weights model EfficientNet.from_pretrained(efficientnet-b3, weights_pathmodel/efficientnet-b3.pth) # freeze前幾層保留低級(jí)特征微調(diào)高級(jí)特征 if freeze_level 0: for param in model.parameters(): param.requires_grad True elif freeze_level 1: for i, param in enumerate(model.parameters()): if i 250: param.requires_grad False num_features model._fc.in_features model._fc nn.Linear(num_features, num_classes) return model model create_model(freeze_level1) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr3e-4, weight_decay0.01) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30, eta_min1e-6)freeze_level1意味著前250層參數(shù)被凍結(jié)只訓(xùn)練后面接近分類(lèi)層的權(quán)重。這樣做的意義是眼底照片的紋理特征血管走向、亮光區(qū)域和ImageNet上學(xué)習(xí)到的低級(jí)特征比較接近不需要重新學(xué)只微調(diào)高級(jí)語(yǔ)義特征即可可大幅減少過(guò)擬合尤其適合幾百?gòu)埖男?shù)據(jù)集。AdamW比Adam好在weight_decay實(shí)現(xiàn)更規(guī)范配合weight_decay0.01能有效限制權(quán)重范數(shù)防止模型在噪聲樣本上記死。CosineAnnealing調(diào)度器讓學(xué)習(xí)率從3e-4余弦下降到1e-6前10個(gè)epoch做粗調(diào)后20個(gè)epoch慢慢收斂到一個(gè)平坦極小值。訓(xùn)練時(shí)如果發(fā)現(xiàn)loss下不去或者accuracy停在90%0類(lèi)拉高不動(dòng)可以考慮把凍結(jié)層數(shù)調(diào)到0這會(huì)增加訓(xùn)練時(shí)間但給模型更強(qiáng)的擬合能力。另一個(gè)實(shí)操技巧是開(kāi)啟混合精度訓(xùn)練代碼里已經(jīng)預(yù)留了torch.cuda.amp的相關(guān)邏輯記得檢查你的顯卡是否支持不支持的話(huà)不要開(kāi)否則訓(xùn)練過(guò)程會(huì)出現(xiàn)NaN。5. 避坑指南跑Jupyter畢設(shè)最容易翻車(chē)的5個(gè)場(chǎng)景5.1 跑了一下午準(zhǔn)確率停在71%驗(yàn)證集表現(xiàn)也垃圾現(xiàn)象模型總loss在下降但驗(yàn)證集混淆矩陣?yán)飵缀跞闪?類(lèi)其它類(lèi)別完全分不清。原因數(shù)據(jù)加載時(shí)忘了加歸一化處理。眼底照片原圖的像素范圍是0-255模型預(yù)訓(xùn)練時(shí)接受的是0-1范圍的tensor分布不一致導(dǎo)致特征提取器一塌糊涂。解決找到項(xiàng)目預(yù)處理cell確認(rèn)加了transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])。用ImageNet的均值和方差是通用做法不要自己另算。改完這個(gè)準(zhǔn)確率基本能直接跳到85%以上。5.2 Jupyter內(nèi)核重啟后報(bào)錯(cuò)ModuleNotFoundError現(xiàn)象剛裝完環(huán)境跑通訓(xùn)練的Cell但關(guān)瀏覽器第二天重啟內(nèi)核第一行import torch直接報(bào)紅在終端里pip list又能看到torch存在。原因Jupyter內(nèi)核和conda虛擬環(huán)境沒(méi)綁定。你在conda activate之后啟動(dòng)的jupyter理論上內(nèi)核對(duì)應(yīng)的是當(dāng)前虛擬環(huán)境但如果你是從Anaconda Navigator的界面直接點(diǎn)擊啟動(dòng)它用的是base環(huán)境。解決在虛擬環(huán)境里安裝ipykernel執(zhí)行python -m ipykernel install --user --name retina --display-name Python (retina)然后重啟Jupyter在Kernel菜單里切換內(nèi)核選擇retina。從那以后每次新開(kāi)項(xiàng)目我都會(huì)先確認(rèn)右上角kernel名而不是傻傻地重裝一遍包。5.3 Loss一路跑偏直接變成NaN現(xiàn)象訓(xùn)練到約第15個(gè)epochloss突然變成nan之后一直沒(méi)有恢復(fù)。原因余弦退火學(xué)習(xí)率降到后期AdamW的自適應(yīng)梯度更新出現(xiàn)分母極小的數(shù)值不穩(wěn)或者輸入的圖片像素出現(xiàn)0分割失敗導(dǎo)致圖片全黑在歸一化后變?yōu)樨?fù)值經(jīng)過(guò)網(wǎng)絡(luò)后導(dǎo)致梯度爆炸。解決先在數(shù)據(jù)檢查階段右鍵點(diǎn)開(kāi)幾張圖片看是否全黑再把torch.autograd.set_detect_anomaly(True)臨時(shí)打開(kāi)定位是哪個(gè)層輸出的值變成inf。一般處理方式是降低初始化學(xué)習(xí)率到1e-4或者在BatchNorm層前加一個(gè)小常數(shù)eps1e-5雙保險(xiǎn)。5.4 加載官方預(yù)訓(xùn)練權(quán)重時(shí)state_dictKey名對(duì)不上現(xiàn)象model.load_state_dict(torch.load(model/efficientnet-b3.pth))報(bào)錯(cuò)提示尺寸不匹配或頁(yè)缺少多個(gè)key。原因官方EfficientNet的權(quán)重是原模型導(dǎo)出沒(méi)有帶_fc.分類(lèi)層的參數(shù)。而代碼里已經(jīng)修改了model._fc為新的5分類(lèi)線(xiàn)性層所以新舊權(quán)重有一部分對(duì)不上。解決加載時(shí)設(shè)置strictFalse告訴PyTorch允許缺失項(xiàng)。再加關(guān)鍵一步——單獨(dú)保存修改后的模型權(quán)重后續(xù)推理直接加載微調(diào)后的文件。在項(xiàng)目里一般是model/retina_best.pth則不存在這個(gè)問(wèn)題前提是你別誤刪這個(gè)文件。5.5 數(shù)據(jù)集讀著讀著突然內(nèi)存溢出現(xiàn)象訓(xùn)練第一個(gè)epoch正常第二個(gè)跑到一半OSError報(bào)“無(wú)法分配內(nèi)存”。原因沒(méi)用DataLoader的num_workers限制并行讀寫(xiě)Jupyter里默認(rèn)是0還好有些同學(xué)看了別的博客調(diào)到8加上圖片解碼緩存8G內(nèi)存直接頂不住。解決把num_workers調(diào)回2并加上pin_memoryTrue。這個(gè)是給GPU訓(xùn)練加速用的。如果你的機(jī)器內(nèi)存只有8G建議直接在config里改掉。6. 進(jìn)階玩法用訓(xùn)練好的模型對(duì)新眼底照片做推理并固化推理結(jié)果6.1 單張圖片推理代碼輸出5類(lèi)置信度而不是只貼標(biāo)簽做畢業(yè)設(shè)計(jì)答辯時(shí)老師不只看訓(xùn)練報(bào)告還想看模型對(duì)真實(shí)世界新照片的預(yù)測(cè)效果。這個(gè)源碼包里有個(gè)inference_single.py核心邏輯是把圖片讀進(jìn)來(lái)走一遍預(yù)處理然后輸出每個(gè)類(lèi)別的概率。很多新手只取argmax返回的類(lèi)別連概率都不展示寫(xiě)論文沒(méi)有說(shuō)服力。import torch import numpy as np from PIL import Image from torchvision import transforms # 與訓(xùn)練階段完全一致的預(yù)處理流程 preprocess transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def predict_single(model, image_path): model.eval() img Image.open(image_path).convert(RGB) # 統(tǒng)一轉(zhuǎn)3通道避免灰度圖意外 img preprocess(img).unsqueeze(0) # 增加batch維度 with torch.no_grad(): logits model(img) probs torch.softmax(logits, dim1).cpu().numpy()[0] class_names [No DR, Mild, Moderate, Severe, Proliferative DR] confidences {class_names[i]: round(float(probs[i]), 4) for i in range(5)} predicted_class class_names[int(np.argmax(probs))] return predicted_class, confidences if __name__ __main__: model create_model(load_weightsmodel/retina_best.pth) category, confs predict_single(model, test_imgs/case1.jpg) print(診斷結(jié)果, category) print(置信度分布, confs)這段輸出的置信度分布很有價(jià)值如果預(yù)測(cè)結(jié)果為“No DR”但“Mild”的概率有35%論文里可以寫(xiě)“模型傾向判斷為早期病變需要進(jìn)一步隨訪(fǎng)”這讓結(jié)果落地成醫(yī)學(xué)可解釋的方式比單純0/1分類(lèi)有說(shuō)服力。注意model.eval()切到評(píng)估模式因?yàn)樵谟?xùn)練模式開(kāi)啟Dropout后推理結(jié)果每次都不一樣。6.2 批量跑完文件夾里所有圖片并自動(dòng)生成CSV報(bào)告更進(jìn)階的用法是把predict_single函數(shù)寫(xiě)進(jìn)一個(gè)循環(huán)遍歷整個(gè)test目錄把結(jié)果和概率全部匯總到一張CSV里。這個(gè)CSV可以直接作為論文附錄的實(shí)驗(yàn)證據(jù)。import os import csv import torch from tqdm import tqdm model create_model(load_weightsmodel/retina_best.pth) image_dir test_imgs/ save_path report/predict_results.csv with open(save_path, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([image_name, predicted_class, conf_no_dr, conf_mild, conf_moderate, conf_severe, conf_proliferative]) for img_name in tqdm(os.listdir(image_dir)): if not img_name.endswith((.jpg, .png, .jpeg)): continue pred_class, confs predict_single(model, os.path.join(image_dir, img_name)) writer.writerow([img_name, pred_class, confs[No DR], confs[Mild], confs[Moderate], confs[Severe], confs[Proliferative DR]])用encodingutf-8-sig生成CSV可以在Excel里直接打開(kāi)而不出現(xiàn)中文亂碼——這是一個(gè)很隱蔽的坑默認(rèn)的utf-8寫(xiě)入Excel里反而是亂碼很多。tqdm負(fù)責(zé)顯示進(jìn)度條幾十張圖跑完會(huì)有日志方便你有“模型在干活”的感覺(jué)。跑完之后逐個(gè)驗(yàn)證CSV里的置信度分布。如果發(fā)現(xiàn)大量圖片預(yù)測(cè)概率比較平均最高概率低于0.6說(shuō)明模型對(duì)這個(gè)樣本的把握度不夠要回到訓(xùn)練階段檢查數(shù)據(jù)增強(qiáng)強(qiáng)度是否過(guò)大。偶爾會(huì)有圖片因?yàn)閽呙柙肼暠慌袛喑芍囟菵R這屬于模態(tài)差異不是代碼bug。遇到這類(lèi)情況直接排除那幾張圖再出最終結(jié)果表即可。從那以后我每次在Jupyter里加載這套畢設(shè)模型做推理都會(huì)強(qiáng)制先跑一遍數(shù)據(jù)預(yù)處理檢查再確認(rèn)模型路徑存在最后才執(zhí)行批量預(yù)測(cè)確保整個(gè)流程一次出結(jié)果。這已經(jīng)成了我的條件反射希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取