項(xiàng)目實(shí)戰(zhàn):從手腕骨檢測(cè)到TW3評(píng)分)
簡(jiǎn)介基于Python和YOLOv5的骨齡檢測(cè)項(xiàng)目面向畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)以及目標(biāo)檢測(cè)方向開發(fā)者尤其適合醫(yī)學(xué)影像智能分析相關(guān)課題提供從模型訓(xùn)練到推理的完整代碼框架。項(xiàng)目源碼經(jīng)過嚴(yán)格測(cè)試可以放心參考并在其基礎(chǔ)上擴(kuò)展二次開發(fā)。壓縮包共25個(gè)文件其中20個(gè)Python腳本為核心覆蓋數(shù)據(jù)加載、模型配置、訓(xùn)練驗(yàn)證、推理導(dǎo)出等流程另有YAML、TOML、TXT等配置和依賴文件便于還原運(yùn)行環(huán)境Markdown文檔則說明項(xiàng)目結(jié)構(gòu)及使用方式。整體大小約95KB輕量精簡(jiǎn)便于快速閱讀與上手目前已有107人學(xué)習(xí)資源涵蓋源碼、項(xiàng)目文檔、數(shù)據(jù)模型、數(shù)據(jù)集與權(quán)重并采用工具模塊和模型模塊等目錄化組織方便定位功能代碼數(shù)據(jù)模型與權(quán)重可用于直接推理數(shù)據(jù)集也便于重新訓(xùn)練與驗(yàn)證。讀者可根據(jù)說明文檔和配置文件構(gòu)建環(huán)境在骨齡檢測(cè)場(chǎng)景中替換或微調(diào)模型也可借鑒其工程結(jié)構(gòu)完成其他目標(biāo)檢測(cè)任務(wù)。1. 骨齡檢測(cè)項(xiàng)目不玄乎yolov5檢測(cè)手腕骨權(quán)重?cái)?shù)據(jù)集一次到位骨齡檢測(cè)在兒童生長(zhǎng)發(fā)育評(píng)估里是剛需但傳統(tǒng)TW3法要醫(yī)生逐塊讀片一個(gè)醫(yī)生一天看幾十張片子眼睛和判斷力都到極限。這份基于pythonyolov5的骨齡檢測(cè)項(xiàng)目把目標(biāo)檢測(cè)用在左手腕X光片上先檢測(cè)橈骨、尺骨、指骨等骨骺區(qū)域再按區(qū)域成熟度映射骨齡。它自帶訓(xùn)練好的權(quán)重、劃分好的數(shù)據(jù)集、預(yù)處理腳本和項(xiàng)目文檔適合畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)和想在項(xiàng)目里快速加檢測(cè)能力的開發(fā)者。拿到手先跑通推理再改數(shù)據(jù)訓(xùn)練自己的模型。我建議上手前先花二十分鐘把yolov5的檢測(cè)流程捋一遍后面改哪里、怎么改心里才有數(shù)這也是我這次拆包最花時(shí)間的地方。2. 骨齡檢測(cè)的任務(wù)拆解為什么用yolov5而不是一個(gè)分類網(wǎng)絡(luò)2.1 骨齡評(píng)估的本質(zhì)是先找區(qū)域、再評(píng)等級(jí)Tanner-Whitehouse法TW3法的核心是讀左手腕X光片對(duì)橈骨遠(yuǎn)端、尺骨遠(yuǎn)端、第一到第五指骨、腕骨等骨骺區(qū)域逐塊對(duì)照?qǐng)D譜把成熟度分成A到I等級(jí)每個(gè)等級(jí)對(duì)應(yīng)分?jǐn)?shù)最后加權(quán)算骨齡。傳統(tǒng)做法里醫(yī)生干的兩件事就是找區(qū)域、定等級(jí)。找區(qū)域這件事對(duì)神經(jīng)網(wǎng)絡(luò)來說正是目標(biāo)檢測(cè)的活兒。如果直接把整張X光片丟給ResNet回歸出一個(gè)年齡模型確實(shí)能“學(xué)出來”但你看不到它依據(jù)什么它可能同時(shí)把圖像里的金屬偽影、拍攝條件當(dāng)成特征換一臺(tái)DR設(shè)備表現(xiàn)就崩。yolov5至少先把“哪些區(qū)域參與了判斷”畫出來——框住骨骺區(qū)域醫(yī)生看到框就知道模型在看哪后面再掛評(píng)分模塊。這種先檢測(cè)后評(píng)分的兩段式結(jié)構(gòu)是骨齡方向的主流做法答辯時(shí)也站得住。再說網(wǎng)絡(luò)結(jié)構(gòu)yolov5的backbone用CSP結(jié)構(gòu)提取特征neck用PANet做多尺度融合head輸出三個(gè)尺度的檢測(cè)結(jié)果。手腕骨里指骨很小、橈骨很大這種尺度差異正好需要多尺度head去覆蓋小目標(biāo)分支負(fù)責(zé)近節(jié)指骨大目標(biāo)分支負(fù)責(zé)橈骨遠(yuǎn)端。再加上yolov5生態(tài)成熟資料多改起來有據(jù)可依這就是我推薦直接用yolov5而不是YOLOX或純分割網(wǎng)絡(luò)的原因。還有一點(diǎn)值得注意yolov5自帶的anchor先驗(yàn)是從COCO數(shù)據(jù)集聚類出來的COCO里大多數(shù)目標(biāo)是日常物體長(zhǎng)寬比和骨骺差別很大。不過這個(gè)項(xiàng)目的訓(xùn)練流程里用到了autoanchor也就是train.py啟動(dòng)時(shí)會(huì)重新對(duì)訓(xùn)練集標(biāo)簽做k-means聚類生成更適合骨骺尺度的錨框后面第四章我再展開講。2.2 項(xiàng)目文件逐一拆解這份yolov5改了什么拿到壓縮包后先別急著跑按下面這張表把文件對(duì)應(yīng)到流程里后面出問題才知道去哪翻。文件/目錄在流程里干什么什么時(shí)候需要改utils/loss.py定義損失計(jì)算yolov5的boxobjcls三項(xiàng)loss訓(xùn)練時(shí)最常改utils/autoanchor.pyk-means聚類錨框換數(shù)據(jù)集自動(dòng)運(yùn)行utils/augmentations.py在線數(shù)據(jù)增強(qiáng)灰度圖要關(guān)色彩抖動(dòng)utils/dataloaders.py加載數(shù)據(jù)集、緩存圖片報(bào)錯(cuò)高發(fā)區(qū)models/yolo.pyDetectionModel定義、前向推理組裝一般不動(dòng)models/common.pyC3、SPPF等基礎(chǔ)模塊改backbone時(shí)才動(dòng)Bone-pre.py數(shù)據(jù)預(yù)處理入口轉(zhuǎn)標(biāo)注格式用export.py導(dǎo)出ONNX/TorchScript部署用.streamlit/config.tomlstreamlit界面主題配置不影響檢測(cè)邏輯utils里最值得先看的是loss.py和augmentations.py。loss.py決定梯度怎么回傳如果你覺得模型對(duì)某個(gè)區(qū)域總學(xué)不好多半要?jiǎng)舆@里的類別權(quán)重augmentations.py決定數(shù)據(jù)增強(qiáng)是否適合醫(yī)學(xué)灰度圖X光片沒有顏色信息默認(rèn)帶HSV色彩抖動(dòng)的增強(qiáng)方案就要關(guān)掉。models/yolo.py的ModuleDict里可以看到模型的組裝順序配合官方y(tǒng)olov5網(wǎng)絡(luò)結(jié)構(gòu)圖對(duì)照看能快速定位你想改的模塊在哪一層。Bone-pre.py是作者寫的預(yù)處理入口功能是把原始X光數(shù)據(jù)整理成yolov5能吃的目錄格式常見做法是統(tǒng)一改名、裁剪、歸一化。還有一個(gè)容易被忽略的文件是.github/workflows很多派生倉(cāng)庫(kù)保留CI工作流文件內(nèi)容一般是自動(dòng)化測(cè)試和release構(gòu)建和本地跑項(xiàng)目沒關(guān)系刪掉也行。README.md里寫了數(shù)據(jù)集來源、權(quán)重復(fù)現(xiàn)說明、使用方法遇到問題先翻它別急著改代碼。2.3 從X光片到骨齡分?jǐn)?shù)整條數(shù)據(jù)流整個(gè)流程可以拆成四段。第一段是輸入左手腕X光片灰度圖分辨率通常在一千五百乘兩千以上。第二段是預(yù)處理Bone-pre.py把圖片統(tǒng)一尺寸、裁剪、歸一化盡量讓腕骨區(qū)域在畫面中央否則原圖直接進(jìn)網(wǎng)絡(luò)顯存根本扛不住。第三段是檢測(cè)yolov5模型輸出多個(gè)框每個(gè)框帶類別、置信度、坐標(biāo)類別代表這個(gè)框?qū)儆谀膫€(gè)骨骺區(qū)域。第四段是評(píng)分后處理腳本把檢測(cè)結(jié)果按區(qū)域類別聚合查TW3評(píng)分表或加權(quán)算出骨齡估計(jì)值。yolov5本身不負(fù)責(zé)“年紀(jì)”預(yù)測(cè)負(fù)責(zé)的是第三段。很多初次接觸的人以為換個(gè)訓(xùn)練數(shù)據(jù)集、把標(biāo)注改成“年齡”模型就能回歸出年齡不對(duì)。目標(biāo)檢測(cè)的數(shù)據(jù)集標(biāo)注是“框類別”類別是區(qū)域編號(hào)不是數(shù)值標(biāo)簽骨齡數(shù)字來自評(píng)分層。理解這一點(diǎn)你就明白為什么權(quán)重文件里沒有“年齡回歸頭”也明白換數(shù)據(jù)集時(shí)該保留什么標(biāo)注。這個(gè)思路也解釋了第四章的一個(gè)建議類別別用年齡。如果指導(dǎo)老師說“你加一個(gè)回歸分支直接出年齡”技術(shù)上可以做在head后面加回歸頭但你得同時(shí)準(zhǔn)備數(shù)值標(biāo)簽數(shù)據(jù)標(biāo)注量完全不同。這個(gè)項(xiàng)目里用的是先檢測(cè)后查表的方式改動(dòng)最小、解釋性最好也是我建議你沿用的方案。3. 把項(xiàng)目跑起來環(huán)境、權(quán)重、數(shù)據(jù)三步到位3.1 conda創(chuàng)建虛擬環(huán)境與依賴安裝先用conda隔離環(huán)境避免污染系統(tǒng)Python。yolov5這套代碼是在torch 1.10到1.13時(shí)代寫的python 3.8和3.9對(duì)應(yīng)最省心直接用系統(tǒng)python 3.11往上torchvision版本對(duì)不上import就報(bào)錯(cuò)。這里是我反復(fù)踩過的血淚經(jīng)驗(yàn)不要在這上面浪費(fèi)時(shí)間。# 創(chuàng)建獨(dú)立Python環(huán)境并激活 conda create -n bone python3.8 -y conda activate bone # 進(jìn)入項(xiàng)目根目錄后安裝依賴 cd Bone_Age_Predict pip install -r requirements.txt依賴裝完先驗(yàn)證torch能不能調(diào)用GPU這一步能省掉后面一半的玄學(xué)問題python -c import torch; print(torch.__version__, torch.cuda.is_available())如果輸出里cuda.is_available()是False先別懷疑代碼大概率是torch版本和顯卡驅(qū)動(dòng)不匹配。常見做法是到pytorch官網(wǎng)按你的CUDA版本重新裝torch再回來跑上面的命令。如果你的機(jī)器沒有NVIDIA顯卡CPU跑推理也夠用只是訓(xùn)練會(huì)慢很多batch-size要調(diào)小。3.2 確認(rèn)目錄和權(quán)重、數(shù)據(jù)集的落位跑通項(xiàng)目之前先確認(rèn)三樣?xùn)|西在不在權(quán)重文件、數(shù)據(jù)集目錄、入口腳本。用ls檢查根目錄結(jié)構(gòu)重點(diǎn)看權(quán)重文件放在哪個(gè)目錄通常叫best.pt或yolov5s.pt可能放在weights/下也可能直接在根目錄。入口腳本里weights參數(shù)寫的是什么路徑就得讓文件待在哪個(gè)路徑這是最蠢也最常見的啟動(dòng)失敗原因。ls -la # 確認(rèn)是否存在 weights/best.pt 或根目錄下的 .pt 文件 find . -name *.pt -maxdepth 3查數(shù)據(jù)集的路徑同理。默認(rèn)訓(xùn)練好的數(shù)據(jù)集一般放在datasets/下用find檢查images和labels目錄是否存在。best.pt是訓(xùn)練過程中val loss最低的那個(gè)checkpoint推理優(yōu)先選它last.pt是最后一個(gè)epoch的存檔復(fù)現(xiàn)訓(xùn)練過程才用到。如果你只想跑演示認(rèn)準(zhǔn)best.pt就夠了。很多人在這一步拿著只有模型沒有數(shù)據(jù)的壓縮包折騰半天結(jié)果發(fā)現(xiàn)是文件沒放全。3.3 用streamlit打開Web檢測(cè)界面這個(gè)項(xiàng)目根目錄里放了一個(gè).streamlit/config.toml說明作者用streamlit做了Web展示層。streamlit的好處是不用寫前端頁(yè)面自動(dòng)生成畢設(shè)演示時(shí)老師不用敲命令直接瀏覽器里傳圖看結(jié)果。啟動(dòng)命令的一般寫法是streamlit run app.py --server.port 8501根目錄的入口腳本名以實(shí)際倉(cāng)庫(kù)為準(zhǔn)常見叫app.py或web.py找到帶streamlit調(diào)用和title配置的那個(gè).py文件就是入口。啟動(dòng)后瀏覽器打開http://localhost:8501上傳一張左手腕X光片就能看到檢測(cè)結(jié)果頁(yè)面會(huì)把檢測(cè)框畫在圖上附帶置信度和骨齡估計(jì)。config.toml里一般是主題配置類似[theme] primaryColor #4CAF50 font sans serif這兩行只影響按鈕、背景的主題色不參與任何檢測(cè)邏輯。如果你改了不生效刷新頁(yè)面如果頁(yè)面布局亂了八成是streamlit版本和config.toml里寫的theme字段不兼容升級(jí)或降級(jí)streamlit到對(duì)應(yīng)版本就行不用改代碼。3.4 走一次命令行推理Web界面方便演示但調(diào)試問題還是命令行直接日志清晰。yolov5派生倉(cāng)庫(kù)一般保留官方detect.py入口這個(gè)包對(duì)官方代碼做了瘦身如果根目錄沒有detect.py就用作者自定義的推理腳本參數(shù)傳法一致python detect.py --weights weights/best.pt --source ./data/test/001.png --img 640 --conf-thres 0.25--weights指定權(quán)重路徑--source可以指向單張圖、一個(gè)文件夾或攝像頭設(shè)備號(hào)--img要和你訓(xùn)練時(shí)一致yolov5會(huì)在輸入時(shí)把圖縮放到這個(gè)分辨率--conf-thres是置信度閾值調(diào)低會(huì)出很多假框調(diào)高容易漏掉小骨骺。第一次跑通時(shí)控制臺(tái)會(huì)逐張打印檢測(cè)結(jié)果和耗時(shí)save目錄下生成帶框的圖。如果一張X光片上只檢測(cè)出一兩個(gè)框或者完全沒有框先不要懷疑模型壞了按這個(gè)順序排查權(quán)重路徑有沒有寫對(duì)、圖片有沒有被resize到正常范圍、置信度閾值是不是太高。這三條查完還沒解決直接看第五章的坑前兩個(gè)坑就是針對(duì)這類現(xiàn)象寫的。4. 訓(xùn)練自己的數(shù)據(jù)集換骨齡庫(kù)、換部位的關(guān)鍵參數(shù)4.1 標(biāo)注格式與目錄組織yolov5要求的數(shù)據(jù)集目錄結(jié)構(gòu)固定images和labels一一對(duì)應(yīng)同名文件匹配不要手動(dòng)復(fù)制粘貼這個(gè)結(jié)構(gòu)決定了dataloaders.py能不能正常找到每一張圖dataset/ images/ train/ val/ labels/ train/ val/images里放一張left_001.pnglabels里就要有對(duì)應(yīng)的left_001.txttxt每行格式為class x_center y_center width height五個(gè)值都是相對(duì)坐標(biāo)范圍0到1。之所以要求相對(duì)坐標(biāo)是因?yàn)檩斎雸D縮放后坐標(biāo)不用重算。很多拿公開骨齡數(shù)據(jù)集的人原始標(biāo)注是CSV或XML里面只有骨齡值沒有框這時(shí)就要用Bone-pre.py把標(biāo)注轉(zhuǎn)成txt。一個(gè)常見的做法是先用預(yù)訓(xùn)練模型出一批偽標(biāo)簽再人工修正把框轉(zhuǎn)成txt的腳本邏輯就在Bone-pre.py里轉(zhuǎn)換前先打印幾行看看格式別直接跑全量。4.2 寫data.yaml之前先理清目標(biāo)和類別data.yaml是訓(xùn)練配置的入口路徑錯(cuò)了訓(xùn)練直接報(bào)錯(cuò)內(nèi)容不多但每個(gè)字段都有講究path: ./dataset train: images/train val: images/val nc: 5 names: [radius, ulna, metacarpal, proximal_phalanx, carpal]path是數(shù)據(jù)集根目錄train和val是相對(duì)path的子目錄nc是類別數(shù)names列表的順序必須和標(biāo)注txt里的class id一一對(duì)應(yīng)順序錯(cuò)位會(huì)導(dǎo)致模型把橈骨當(dāng)成尺骨訓(xùn)練。類別應(yīng)該按照骨骺區(qū)域定義不要圖省事把class直接設(shè)為“年齡”。如果把類別設(shè)成年齡模型會(huì)把不同年齡但形態(tài)相近的骨頭混成一類框都學(xué)不準(zhǔn)。骨齡檢測(cè)的類別就是區(qū)域名年齡信息放在后處理評(píng)分階段這是我最想強(qiáng)調(diào)的一點(diǎn)。4.3 訓(xùn)練命令與超參數(shù)調(diào)優(yōu)訓(xùn)練時(shí)用COCO預(yù)訓(xùn)練權(quán)重做遷移學(xué)習(xí)收斂速度比隨機(jī)初始化快很多。基礎(chǔ)訓(xùn)練命令python train.py --data dataset/data.yaml --weights yolov5s.pt \ --img 640 --batch-size 16 --epochs 100 \ --hyp data/hyps/hyp.scratch-low.yaml--img是輸入分辨率骨齡X光片細(xì)節(jié)多有條件可以提到1280但顯存占用會(huì)翻好幾倍8G顯存建議640起。--batch-size按顯存調(diào)8G卡建議8到16之間。--hyp指定超參數(shù)文件yolov5把超參數(shù)收斂在hyp文件里而不是散落在代碼里方便復(fù)盤。骨齡場(chǎng)景建議改兩個(gè)地方一是把hsv_h和hsv_s這兩項(xiàng)色彩增強(qiáng)降到最低X光是灰度圖顏色抖動(dòng)只會(huì)引入噪聲二是degrees旋轉(zhuǎn)幅度別開大骨骺旋轉(zhuǎn)后形態(tài)會(huì)失真影響等級(jí)判斷。這兩個(gè)參數(shù)就在hyp文件里改完重跑訓(xùn)練即可。訓(xùn)練啟動(dòng)時(shí)留意日志里有沒有AutoAnchor的k-means聚類輸出如果數(shù)據(jù)里指骨很小、橈骨很大自動(dòng)聚類能明顯提升召回率想關(guān)掉就加--noautoanchor參數(shù)。4.4 從訓(xùn)練日志判斷過擬合與欠擬合訓(xùn)練時(shí)盯著兩個(gè)東西看train和val的loss曲線以及val的mAP0.5。如果train loss一直降、val loss開始回升就是過擬合骨齡公開數(shù)據(jù)集往往只有幾千張圖過擬合非常常見。緩解手段按優(yōu)先級(jí)排列調(diào)大數(shù)據(jù)增強(qiáng)、降低模型復(fù)雜度yolov5s換成yolov5n、早停參數(shù)patience調(diào)小。如果mAP0.5一直上不去先看是不是類別不平衡指骨樣本遠(yuǎn)多于橈骨可以在loss.py里調(diào)整cls損失權(quán)重給樣本少的區(qū)域更高權(quán)重。訓(xùn)練完不要只看總mAP用val.py跑一遍驗(yàn)證集看per-class的mAP哪個(gè)區(qū)域最差就針對(duì)性補(bǔ)哪個(gè)區(qū)域的數(shù)據(jù)。這一步很多人跳過結(jié)果答辯時(shí)被問“你這個(gè)模型哪類檢測(cè)最差”答不上來。per-class結(jié)果是一張表打印出來存好寫進(jìn)論文里也是一個(gè)像樣的實(shí)驗(yàn)分析。5. 避坑與排查骨齡檢測(cè)最容易翻車的五個(gè)坑5.1 現(xiàn)象訓(xùn)練loss正常下降但模型一個(gè)骨骺框都檢測(cè)不到loss曲線很漂亮mAP卻一直是0檢測(cè)結(jié)果全空白。這個(gè)現(xiàn)象我第一次遇到時(shí)也懵了后來發(fā)現(xiàn)原因在于loss是box、obj、cls三個(gè)分支的總和總loss下降不代表每個(gè)分支都收斂了。obj分支沒收斂說明錨框尺寸和目標(biāo)的真實(shí)尺寸嚴(yán)重不匹配先驗(yàn)框還是COCO的通用尺度對(duì)細(xì)長(zhǎng)的骨骺完全不適用。解決方法是確認(rèn)訓(xùn)練日志里有沒有AutoAnchor的輸出。如果日志里沒有k-means重聚類的結(jié)果說明autoanchor被關(guān)掉了或者沒跑成去掉--noautoanchor參數(shù)重新跑。要是聚類結(jié)果還是不理想手動(dòng)在hyp文件里按數(shù)據(jù)集的框尺寸分布設(shè)置初始錨框。這個(gè)問題排查路徑很固定先看日志再改參數(shù)不用動(dòng)網(wǎng)絡(luò)結(jié)構(gòu)。5.2 現(xiàn)象訓(xùn)練或推理時(shí)顯存OOM程序直接崩X光原圖分辨率動(dòng)輒2000以上--img默認(rèn)640沒問題一旦設(shè)成12808G顯存直接爆。這種情況的典型報(bào)錯(cuò)是CUDA out of memory有時(shí)還會(huì)把機(jī)器整個(gè)卡死只能重啟。原因是模型輸入尺寸乘了平方關(guān)系640變1280顯存占用翻四倍不是翻一倍。解決思路有兩個(gè)方向。一是把--img降到640batch-size降到4同時(shí)啟用amp混合精度訓(xùn)練顯存能省將近一半。二是從源頭處理用Bone-pre.py先把X光長(zhǎng)邊壓到1280以內(nèi)再做數(shù)據(jù)增強(qiáng)而不是讓yolov5在dataloader里直接縮放。我一般兩個(gè)方向一起做長(zhǎng)邊壓到1280--img設(shè)640這樣既保留細(xì)節(jié)又不爆顯存。5.3 現(xiàn)象streamlit頁(yè)面能打開但上傳X光片后一直轉(zhuǎn)圈不出結(jié)果Web界面正常顯示一上傳圖片就卡住控制臺(tái)也沒有報(bào)錯(cuò)看起來像死循環(huán)。這個(gè)問題的原因一般是兩層一是Web入口調(diào)用的推理函數(shù)里寫的權(quán)重路徑是相對(duì)路徑streamlit運(yùn)行時(shí)的工作目錄和命令行不一樣找不到權(quán)重文件二是入口腳本里模型輸入尺寸和權(quán)重訓(xùn)練尺寸不一致模型前向推理時(shí)靜默失敗。解決時(shí)先用命令行跑一遍同一張圖確認(rèn)權(quán)重本身沒問題再改Web層。把權(quán)重路徑換成絕對(duì)路徑推理函數(shù)里先打印輸入張量的shape確認(rèn)和訓(xùn)練時(shí)一致。凡是streamlit這類Web框架包檢測(cè)代碼的我都建議把推理邏輯單獨(dú)封裝成一個(gè)函數(shù)Web只做調(diào)用這樣排查時(shí)直接在命令行調(diào)函數(shù)不用反復(fù)開頁(yè)面。5.4 現(xiàn)象val集mAP很高實(shí)際對(duì)正常X光片預(yù)測(cè)的骨齡誤差卻很大驗(yàn)證集指標(biāo)漂亮得不行一上真實(shí)圖片就露餡。這個(gè)坑我見過不止一次原因是數(shù)據(jù)劃分時(shí)按文件名單純隨機(jī)切分同一個(gè)病人的多張片子同時(shí)出現(xiàn)在訓(xùn)練集和驗(yàn)證集里模型記住的是病人特征而不是骨齡特征這在醫(yī)學(xué)影像里叫數(shù)據(jù)泄漏。公共數(shù)據(jù)集經(jīng)常一個(gè)病人拍了兩三次文件名只按順序編號(hào)不按病人分組。解決方法是按病人ID分組劃分?jǐn)?shù)據(jù)集保證同一個(gè)人的所有片子只出現(xiàn)在一個(gè)集合里。拿到數(shù)據(jù)先看有沒有病人ID這一列有就先groupby再切分。這個(gè)細(xì)節(jié)在答辯時(shí)主動(dòng)講出來老師會(huì)認(rèn)為你真處理過醫(yī)療數(shù)據(jù)而不是只會(huì)跑通一個(gè)demo。5.5 現(xiàn)象Bone-pre.py轉(zhuǎn)換標(biāo)注時(shí)報(bào)KeyError或轉(zhuǎn)換后某些類別的txt是空的預(yù)處理腳本跑到一半報(bào)KeyError檢查生成結(jié)果發(fā)現(xiàn)有些類別的txt文件一個(gè)框都沒有。原因基本可以鎖定在標(biāo)注名稱沒有完全映射到類別字典上常見的情況有幾種左右手沒區(qū)分原始數(shù)據(jù)里left和right的標(biāo)注都在腕骨分塊過細(xì)一個(gè)腕骨有多個(gè)骨化中心每個(gè)都被單獨(dú)標(biāo)注了。這些名稱沒有進(jìn)到你的類別映射表腳本就報(bào)KeyError或者直接跳過。解決方法是轉(zhuǎn)換前先寫一段代碼把所有標(biāo)注名稱去重打印出來逐一核對(duì)映射字典把不需要參與評(píng)分的區(qū)域注釋掉。這個(gè)步驟不能省我一般會(huì)先跑一遍統(tǒng)計(jì)腳本輸出每個(gè)標(biāo)注名的樣本數(shù)量分布再?zèng)Q定哪些保留、哪些合并。還有一個(gè)習(xí)慣轉(zhuǎn)換完成后隨機(jī)抽三張圖把txt標(biāo)注可視化畫在圖上人工檢查一遍這一步能擋掉絕大多數(shù)標(biāo)注錯(cuò)位問題。6. 進(jìn)階用法導(dǎo)出ONNX模型用CPU跑批量推理6.1 用export.py導(dǎo)出ONNX項(xiàng)目根目錄里有export.py這是yolov5官方的導(dǎo)出腳本把PyTorch權(quán)重轉(zhuǎn)成ONNX格式導(dǎo)出后可以不依賴PyTorch環(huán)境推理python export.py --weights weights/best.pt --img 640 --include onnx導(dǎo)出時(shí)--img必須和訓(xùn)練時(shí)一致否則模型輸入輸出尺寸對(duì)不上推理結(jié)果會(huì)離譜。ONNX模型可以用onnxruntime加載純CPU也能跑單張圖在幾百毫秒級(jí)別對(duì)一個(gè)檢測(cè)任務(wù)來說完全夠用。導(dǎo)出成功后根目錄會(huì)生成best.onnx文件比.pt小不少發(fā)給別人也方便。6.2 檢測(cè)結(jié)果到骨齡分?jǐn)?shù)的映射ONNX輸出的原始結(jié)果是檢測(cè)框坐標(biāo)加上置信度和類別要得到骨齡分?jǐn)?shù)還得做一層映射。下面這段示意代碼展示了yolo輸出和骨齡評(píng)分的銜接邏輯def bone_age_from_boxes(boxes, region_weights): total_score 0.0 regions_used 0 for box in boxes: cls_id int(box[5]) conf float(box[4]) if conf 0.5: continue # 每個(gè)區(qū)域按成熟等級(jí)加權(quán)權(quán)重來自TW3評(píng)分表 total_score region_weights.get(cls_id, 0) regions_used 1 if regions_used 0: return None return total_score / regions_usedyolo的每個(gè)框是一個(gè)數(shù)組第4列是置信度第5列是類別id這個(gè)切片順序很多人寫錯(cuò)導(dǎo)致把類別當(dāng)置信度過濾。實(shí)際項(xiàng)目里region_weights會(huì)單獨(dú)存在一個(gè)py或json文件里按區(qū)域類別編號(hào)查表這里只展示思路。6.3 我的工程習(xí)慣自從有一次交付后對(duì)方問我“為什么這次跑出來的結(jié)果和上次不一樣”我開始意識(shí)到y(tǒng)olov5推理本身是確定的不確定的是預(yù)處理。從那以后我每次交付檢測(cè)類項(xiàng)目都強(qiáng)制自己走一遍固定流程干凈環(huán)境里跑通一次、導(dǎo)出ONNX、固定輸入尺寸和歸一化參數(shù)再把推理結(jié)果落盤成CSV方便回看分析。骨齡檢測(cè)這種醫(yī)療場(chǎng)景更要留痕每個(gè)框的坐標(biāo)、置信度、類別都記錄下來回查時(shí)有據(jù)可依。你自己上手時(shí)也建議從第一天就養(yǎng)成這個(gè)習(xí)慣后面省的事遠(yuǎn)不止翻半天日志。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取