別實(shí)戰(zhàn):小樣本醫(yī)學(xué)圖像分類的遷移學(xué)習(xí)與兩階段微調(diào)策略)
最近在跑一個(gè)皮膚影像分類的實(shí)戰(zhàn)項(xiàng)目主題是猴痘病識(shí)別。任務(wù)本身聽起來挺簡單——給模型一張皮膚病灶圖讓它輸出是猴痘還是其他皮膚病屬于典型的細(xì)粒度醫(yī)學(xué)圖像分類問題??烧姘蚜鞒掏暾咭槐閺臄?shù)據(jù)清洗、增強(qiáng)策略、遷移學(xué)習(xí)再到指標(biāo)評估每一步都有值得琢磨的細(xì)節(jié)甚至有幾個(gè)環(huán)節(jié)讓我返工了不止一次。這個(gè)項(xiàng)目是系列實(shí)戰(zhàn)練習(xí)里的P4周前面幾周做過自然圖像分類、垃圾圖片識(shí)別、遙感影像標(biāo)注之類的入門任務(wù)到這一周難度明顯上了一個(gè)臺(tái)階。為什么因?yàn)獒t(yī)學(xué)圖像分類和普通分類完全是兩個(gè)世界數(shù)據(jù)量小、類別相似度高、圖像噪聲大、還牽扯到隱私問題。你拿ImageNet上訓(xùn)練好的模型直接過來推理效果往往很差但真要你從零訓(xùn)練一個(gè)網(wǎng)絡(luò)數(shù)據(jù)集又遠(yuǎn)遠(yuǎn)不夠。這里面怎么平衡、怎么設(shè)計(jì)訓(xùn)練策略就是這篇文章想聊清楚的東西。文章適合誰看如果你正在做圖像分類相關(guān)的項(xiàng)目尤其是醫(yī)學(xué)影像、皮膚病理、農(nóng)業(yè)病害這類小樣本細(xì)粒度識(shí)別場景這篇內(nèi)容可以直接復(fù)用。里面涉及的數(shù)據(jù)增強(qiáng)策略、兩階段微調(diào)方案、混淆矩陣分析思路都是經(jīng)過真實(shí)項(xiàng)目驗(yàn)證過的做法。我會(huì)把代碼、參數(shù)、踩坑過程都放出來你照著改改就能用到自己的數(shù)據(jù)上。1. 從任務(wù)目標(biāo)到數(shù)據(jù)認(rèn)知猴痘病識(shí)別到底難在哪1.1 業(yè)務(wù)場景給模型一張皮膚照片它要給出什么結(jié)論先把這個(gè)任務(wù)放在真實(shí)場景里看。傳統(tǒng)診斷流程里醫(yī)生需要靠肉眼觀察皮損形態(tài)、分布位置結(jié)合接觸史和實(shí)驗(yàn)室檢測才能確診。但在疫情篩查階段大量初診病例積壓醫(yī)生不可能把所有時(shí)間都花在重復(fù)閱片上。這時(shí)候一個(gè)能自動(dòng)對皮膚照片做初步分類的模型就能幫上忙它把疑似猴痘的樣本優(yōu)先挑出來讓醫(yī)生把精力集中在高風(fēng)險(xiǎn)人群上。所以這個(gè)項(xiàng)目的實(shí)際價(jià)值不在于替代醫(yī)生而在于做預(yù)篩和分流。模型面對一張皮膚照片輸出三類結(jié)論之一猴痘病變、其他皮膚病變、正常皮膚。看清楚是三分類不是二分類。很多人第一反應(yīng)是就判斷是不是猴痘唄分兩類不就行了但真落到臨床場景只做二分類意義不大。因?yàn)閷?shí)際門診來的病人大部分不是正常皮膚而是帶著各種皮疹、水皰、過敏反應(yīng)來的。如果只訓(xùn)練猴痘 vs 正常模型見到一個(gè)水痘患者大概率會(huì)把它判成猴痘——因?yàn)樗缓秃锒坏囊曈X特征太接近了而模型從來沒學(xué)過其他皮膚病長什么樣。這就逼著我們必須引入第三類讓模型學(xué)會(huì)區(qū)分猴痘和它最相似的混淆類別。1.2 數(shù)據(jù)集特征小樣本、高相似、強(qiáng)噪聲看數(shù)據(jù)之前先潑盆冷水公開的猴痘皮膚圖像數(shù)據(jù)集數(shù)量通常只有幾千張很多是網(wǎng)絡(luò)爬蟲抓來的來源五花八門。這意味著三個(gè)現(xiàn)實(shí)問題。第一數(shù)據(jù)量不夠從零訓(xùn)練一個(gè)深度網(wǎng)絡(luò)。ResNet50這種模型光參數(shù)就有2550萬你給它喂幾千張圖訓(xùn)練集準(zhǔn)確率能輕松到99%驗(yàn)證集卻可能一直卡在80%上下這就是典型的過擬合。第二圖像質(zhì)量參差不齊。手機(jī)拍攝的照片有的人離得遠(yuǎn)有的人離得近有室內(nèi)光有室外光有的還帶毛發(fā)遮擋模型很容易抓住這些和疾病無關(guān)的噪聲特征。第三最要命的是類別間相似度極高猴痘早期的紅色斑疹和麻疹、水痘幾乎一模一樣連醫(yī)生都要結(jié)合病程和接觸史判斷單靠靜態(tài)圖像讓模型硬分上限就擺在那里。正因?yàn)檫@些特點(diǎn)P4周這個(gè)項(xiàng)目真正訓(xùn)練的其實(shí)不是怎么搭一個(gè)分類網(wǎng)絡(luò)而是怎么在小樣本高混淆的醫(yī)學(xué)圖像上把遷移學(xué)習(xí)的優(yōu)勢全部榨出來。1.3 數(shù)據(jù)劃分的一個(gè)隱藏陷阱公開數(shù)據(jù)集內(nèi)部往往隱藏著數(shù)據(jù)泄漏風(fēng)險(xiǎn)。我當(dāng)時(shí)拿到數(shù)據(jù)后第一版劃分方式很粗暴——直接把所有圖片隨機(jī)打散按8:1:1切分成訓(xùn)練、驗(yàn)證、測試集。結(jié)果測試集準(zhǔn)確率漂亮得嚇人96%。可我仔細(xì)一看數(shù)據(jù)來源文件發(fā)現(xiàn)很多圖片其實(shí)來自同一組病例同一個(gè)人的不同角度皮損照片被隨機(jī)分到了訓(xùn)練集和測試集里。這就意味著模型可能記住了這個(gè)人皮膚的背景色、紋理特征而不是真正學(xué)會(huì)了識(shí)別猴痘的病理特征。測試時(shí)遇見同一個(gè)人的不同照片相當(dāng)于直接開卷考試。后來我把數(shù)據(jù)按病例來源分組保證同一個(gè)人的所有照片只出現(xiàn)在一個(gè)集合里測試集準(zhǔn)確率立刻掉到90%左右。這才是真實(shí)水平。這個(gè)教訓(xùn)很重要醫(yī)學(xué)圖像分類里如果你的數(shù)據(jù)集中同一患者的多張圖像混在訓(xùn)練集和測試集兩邊評估結(jié)果會(huì)虛高落地時(shí)模型表現(xiàn)斷崖式下跌。按來源分組劃分應(yīng)該作為醫(yī)學(xué)影像項(xiàng)目的默認(rèn)操作。2. 模型選型與訓(xùn)練設(shè)計(jì)遷移學(xué)習(xí)為什么是首選2.1 為什么不從零訓(xùn)練也不直接拿預(yù)訓(xùn)練模型硬推理圖像分類項(xiàng)目的第一個(gè)選擇題往往就是模型怎么來從零訓(xùn)練、微調(diào)預(yù)訓(xùn)練模型、還是完全凍結(jié)主干只換分類頭先說從零訓(xùn)練。ImageNet有128萬張圖、1000個(gè)類別ResNet50還花了120個(gè)epoch才收斂到75%左右的Top-1準(zhǔn)確率。而我的手頭猴痘數(shù)據(jù)集滿打滿算幾千張從零開始訓(xùn)練一個(gè)深度CNN唯一的結(jié)局就是嚴(yán)重過擬合。你要么把網(wǎng)絡(luò)做得非常淺非常小但淺層網(wǎng)絡(luò)的特征表達(dá)力又不夠做細(xì)粒度分類兩頭難。就算加上粗暴的數(shù)據(jù)增強(qiáng)本質(zhì)上也解決不了數(shù)據(jù)分布覆蓋不足的問題。再說直接拿預(yù)訓(xùn)練模型硬推理。ImageNet的1000個(gè)類別里有猴子、有獼猴沒有猴痘模型對皮膚病變區(qū)域這個(gè)概念根本沒有認(rèn)知直接推理的結(jié)果基本等于隨機(jī)猜輸出概率分布毫無區(qū)分度。那遷移學(xué)習(xí)為什么是正解因?yàn)榫矸e神經(jīng)網(wǎng)絡(luò)的特征提取是分層遞進(jìn)的淺層學(xué)到的是邊緣、紋理、顏色塊這類通用低級特征中層學(xué)到的是形狀、局部模式深層才學(xué)到類別專屬的語義特征。ImageNet預(yù)訓(xùn)練模型已經(jīng)在百萬級通用圖像上學(xué)好了淺層和中層特征這些特征對醫(yī)學(xué)圖像同樣適用——皮膚病灶的邊緣、水皰的紋理、紅斑的色澤本質(zhì)上都是邊緣和紋理的組合。我們要做的只是把深層特征適配到猴痘這個(gè)新任務(wù)上。2.2 骨干網(wǎng)絡(luò)對比與選型邏輯既然確定用遷移學(xué)習(xí)下一個(gè)問題就是選哪個(gè)預(yù)訓(xùn)練骨干網(wǎng)絡(luò)作為特征提取器。我把當(dāng)時(shí)對比的幾個(gè)模型列了個(gè)表參數(shù)都不是我瞎寫的是實(shí)際實(shí)驗(yàn)跑出來的感受骨干網(wǎng)絡(luò)參數(shù)量推理速度在小樣本醫(yī)學(xué)數(shù)據(jù)上的表現(xiàn)適合場景ResNet5025.6M中穩(wěn)定最容易調(diào)出好效果通用首選VGG16138M慢效果不錯(cuò)但顯存占用大不推薦新項(xiàng)目使用EfficientNetB05.3M快精度不低但訓(xùn)練對超參敏感需要極致效率時(shí)考慮MobileNetV34.2M最快精度略低端側(cè)部署場景為什么最終選了ResNet50三個(gè)原因。第一是它的殘差連接結(jié)構(gòu)在微調(diào)時(shí)特別友好。深層的梯度能通過捷徑連接直接傳回淺層不容易出現(xiàn)梯度消失。第二是它中間層輸出的特征圖分辨率適中既能保留病灶的局部細(xì)節(jié)又不會(huì)像EfficientNet那樣在訓(xùn)練初期對學(xué)習(xí)率極其敏感。第三是我對ResNet系列的調(diào)參經(jīng)驗(yàn)最豐富P4周這個(gè)時(shí)間節(jié)點(diǎn)上穩(wěn)定可控比極限性能更重要。如果你后續(xù)想把模型部署到手機(jī)端切到MobileNetV3再跑一遍同樣的兩階段流程就行精度會(huì)掉一個(gè)點(diǎn)左右但速度能快好幾倍。2.3 類別設(shè)計(jì)的取舍差一個(gè)類別訓(xùn)練難度完全不同上文說過這個(gè)項(xiàng)目選的是三分類猴痘、其他皮膚病變、正常皮膚。這個(gè)選擇不是隨意決定的它對整個(gè)訓(xùn)練難度有決定性影響。如果只做二分類猴痘 vs 正常皮膚數(shù)據(jù)里每一種非猴痘的圖片都是負(fù)樣本模型很容易學(xué)會(huì)有異常就是猴痘這種粗糙的決策邊界。一旦真到了臨床場景遇到水痘、帶狀皰疹、過敏性皮炎模型全都會(huì)往猴痘那邊偏誤報(bào)率會(huì)高到讓人無法接受。加上其他皮膚病變這個(gè)類別之后模型被迫去學(xué)習(xí)猴痘區(qū)別于水痘、區(qū)別于皰疹、區(qū)別于普通皮疹的判別性特征。這是細(xì)粒度分類的核心思想——難點(diǎn)不在有沒有病而在是什么病。訓(xùn)練難度確實(shí)變大了驗(yàn)證準(zhǔn)確率會(huì)比二分類低三到五個(gè)百分點(diǎn)但這是用精度換實(shí)用性值得。3. 兩階段微調(diào)凍結(jié)骨干網(wǎng)絡(luò)與解凍精修的完整流程3.1 數(shù)據(jù)增強(qiáng)策略有限數(shù)據(jù)下的第一道防線小樣本項(xiàng)目的第一道防線永遠(yuǎn)是數(shù)據(jù)增強(qiáng)。我當(dāng)時(shí)的數(shù)據(jù)增強(qiáng)管線分兩條路走離線清洗時(shí)統(tǒng)一做尺寸歸一化在線訓(xùn)練時(shí)做隨機(jī)擾動(dòng)。在線增強(qiáng)我用了這樣一組參數(shù)from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter( brightness0.2, contrast0.2, saturation0.1, hue0.05 ), transforms.ToTensor(), transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) ])這里幾個(gè)我自己踩過坑的經(jīng)驗(yàn)。RandomResizedCrop的scale參數(shù)我建議控制在0.8到1.0之間不要用ImageNet分類常用的0.08到1.0。因?yàn)獒t(yī)學(xué)病灶是有實(shí)際尺寸意義的縮放太狠會(huì)讓模型學(xué)到錯(cuò)誤的尺度信息比如把一顆小痣放大成一個(gè)巨大的潰爛面這種增強(qiáng)對模型訓(xùn)練不但沒幫助還會(huì)干擾它學(xué)習(xí)真實(shí)的病灶形態(tài)。RandomRotation我限制在15度以內(nèi)。皮膚照片雖然有旋轉(zhuǎn)角度差異但旋轉(zhuǎn)過大生成的訓(xùn)練樣本在真實(shí)世界里根本不存在反而增加了模型的不確定性。ColorJitter我特意把hue調(diào)得很小只動(dòng)了0.05。原因很簡單皮膚病變的顏色是重要診斷線索色相偏移太大會(huì)讓紅疹變成藍(lán)疹等于給模型喂錯(cuò)誤標(biāo)簽。驗(yàn)證集的transform只做resize和歸一化不做任何隨機(jī)增強(qiáng)。這一點(diǎn)我見過不少新手搞混在驗(yàn)證集上加了隨機(jī)翻轉(zhuǎn)結(jié)果指標(biāo)忽高忽低永遠(yuǎn)看不到真實(shí)水平。3.2 模型構(gòu)建替換分類頭并凍結(jié)參數(shù)模型構(gòu)建這一步核心工作是用預(yù)訓(xùn)練權(quán)重初始化骨干網(wǎng)絡(luò)把最后的全連接層換成適配三分類任務(wù)的新分類頭。import torch import torch.nn as nn from torchvision import models model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) # 替換最后一層全連接層適配3分類任務(wù) num_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(num_features, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(256, 3) ) # 第一階段凍結(jié)所有骨干網(wǎng)絡(luò)參數(shù) for param in model.parameters(): param.requires_grad False # 只允許分類頭參數(shù)更新 for param in model.fc.parameters(): param.requires_grad True這里分類頭我用了兩層全連接結(jié)構(gòu)而不是簡單的單層替換原因是單層全連接在特征維度為2048時(shí)容量有限不足以擬合三分類的決策邊界。加了兩個(gè)Dropout層0.3的丟棄率在小樣本場景下能明顯抑制分類頭的過擬合。關(guān)于凍結(jié)策略可以把骨干網(wǎng)絡(luò)想象成一個(gè)已經(jīng)熟練掌握通用視覺特征的學(xué)生分類頭是一個(gè)剛?cè)腴T的新生。第一階段讓新生先聽課做題老生別插手等新生水平上來了再讓老生也參與討論一起適應(yīng)新題型。3.3 兩階段訓(xùn)練先訓(xùn)頭部再解凍精修訓(xùn)練策略我拆成了兩段這也是整個(gè)項(xiàng)目里最關(guān)鍵的部分。第一階段凍結(jié)骨干網(wǎng)絡(luò)全部參數(shù)只訓(xùn)練新加的分類頭優(yōu)化器用AdamW學(xué)習(xí)率定在1e-3跑10個(gè)epoch。這個(gè)階段的目標(biāo)不是追求高準(zhǔn)確率而是讓隨機(jī)初始化的分類頭先在固定特征上收斂把骨干網(wǎng)絡(luò)ReLU之前輸出的特征分布摸個(gè)底。第二階段解凍骨干網(wǎng)絡(luò)最后一部分參數(shù)。我當(dāng)時(shí)解凍了第三個(gè)殘差塊之后的全部層包括stage4和stage3的后半段把學(xué)習(xí)率降到1e-5再跑15個(gè)epoch。為什么學(xué)習(xí)率要從1e-3直接降到1e-5這個(gè)數(shù)值選擇背后的邏輯值得說清楚。ImageNet上預(yù)訓(xùn)練的特征是通用特征微調(diào)的本質(zhì)是在通用特征基礎(chǔ)上做局部調(diào)整。如果學(xué)習(xí)率設(shè)得太高比如沿用1e-3梯度更新過大會(huì)把預(yù)訓(xùn)練學(xué)到的通用特征徹底沖掉。在小樣本數(shù)據(jù)集上這種破壞是不可逆的模型會(huì)快速過擬合到訓(xùn)練集的幾百張圖上驗(yàn)證準(zhǔn)確率不升反降。而1e-5這個(gè)量級相當(dāng)于用極小的步長在預(yù)訓(xùn)練權(quán)重附近做精細(xì)搜索既保留了通用特征又讓深層特征逐漸適應(yīng)皮膚病灶的分布。整個(gè)訓(xùn)練過程我用了一個(gè)很簡單的早停機(jī)制連續(xù)5個(gè)epoch驗(yàn)證集F1沒有提升就停止訓(xùn)練同時(shí)保存驗(yàn)證集F1最高的那輪權(quán)重作為最終模型。訓(xùn)練過程代碼大致這樣from torch.optim import AdamW # 第一階段只訓(xùn)練分類頭 optimizer AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4 ) for epoch in range(10): train_one_epoch(model, train_loader, optimizer, criterion) # 第二階段解凍部分骨干網(wǎng)絡(luò)降低學(xué)習(xí)率 for name, param in model.named_parameters(): if layer4 in name or layer3.3 in name or layer3.4 in name or layer3.5 in name: param.requires_grad True else: param.requires_grad False optimizer AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-5, weight_decay1e-4 ) for epoch in range(15): train_one_epoch(model, train_loader, optimizer, criterion) val_f1 evaluate(model, val_loader) if val_f1 best_f1: best_f1 val_f1 torch.save(model.state_dict(), best_model.pth)實(shí)測下來第一階段結(jié)束時(shí)驗(yàn)證集準(zhǔn)確率大概在86%左右第二階段跑到第6個(gè)epoch時(shí)驗(yàn)證集F1開始明顯上升最終穩(wěn)定在93%上下。這個(gè)曲線非常有代表性——先快速起步然后平臺(tái)期最后小幅躍升。4. 訓(xùn)練結(jié)果復(fù)盤評估指標(biāo)、混淆矩陣與可視化4.1 評估指標(biāo)選擇準(zhǔn)確率之外還必須看召回率和F1分類項(xiàng)目最忌諱只看準(zhǔn)確率。在醫(yī)學(xué)輔助篩查場景里漏診一個(gè)猴痘患者的代價(jià)遠(yuǎn)遠(yuǎn)高于誤報(bào)一次所以召回率Recall比精確率Precision更值得關(guān)注。如果模型在100個(gè)真實(shí)猴痘樣本里只識(shí)別出80個(gè)那20個(gè)漏掉的患者就是潛在傳染源。最終我在測試集上得到的結(jié)果是這樣的類別精確率召回率F1-score樣本數(shù)猴痘病變0.940.890.92120其他皮膚病變0.910.960.93150正常皮膚0.980.940.96100整體準(zhǔn)確率94%加權(quán)F1 0.94Macro F1 0.93??催@個(gè)表里的信息量不能只盯著94%這個(gè)數(shù)字。猴痘類的召回率是0.89意味著仍有11%的真猴痘樣本被漏掉了。再看混淆矩陣漏掉的大多是被分到了其他皮膚病變這一類只有極少數(shù)被分到了正常皮膚。這個(gè)錯(cuò)誤方向其實(shí)是符合預(yù)期的——猴痘早期病變和某些水皰性疾病視覺相似度太高模型判斷不了太正常了。但反過來看被漏掉的那些樣本是什么情況我把圖片一張張翻出來發(fā)現(xiàn)一個(gè)共同特點(diǎn)病變區(qū)域占比非常小在整張224x224的圖片里只占不到十分之一。這說明模型不是學(xué)不會(huì)猴痘特征而是注意力沒有被引導(dǎo)到病灶區(qū)域上。4.2 Grad-CAM可視化看模型到底在看哪里為了驗(yàn)證這個(gè)猜想我用Grad-CAM把模型最后一層卷積的注意力熱力圖疊加到原始圖像上。結(jié)果很清楚對于判斷正確的猴痘樣本熱力圖集中在病灶中央?yún)^(qū)域圍繞水皰邊緣有一圈高亮的響應(yīng)區(qū)域。這說明模型學(xué)到的確實(shí)是病灶本身的形態(tài)特征。對于判斷錯(cuò)誤的猴痘樣本熱力圖分布非常散亂有的集中在皮膚紋理上有的甚至跑到圖片角落的背景區(qū)域。有一個(gè)典型的誤判樣本病灶在皮膚褶皺處陰影特別重?zé)崃D直接失效模型把大片陰影區(qū)域當(dāng)成了關(guān)鍵特征。這就是小樣本醫(yī)學(xué)圖像分類的經(jīng)典痛點(diǎn)模型不是不想學(xué)病灶而是數(shù)據(jù)里病灶清晰、光照正常的干凈樣本太少學(xué)了半天還是被背景干擾帶偏了。這個(gè)問題光靠調(diào)參解決不了最有效的辦法是訓(xùn)練前做一次目標(biāo)檢測定位病灶將病灶區(qū)域裁剪放大后再送入分類模型。這一步能顯著提升小病灶樣本的識(shí)別率我后續(xù)測試也驗(yàn)證了這個(gè)判斷。4.3 訓(xùn)練曲線解讀兩階段特征變化帶來的啟發(fā)我記錄了每個(gè)epoch的loss和F1曲線兩階段的差異非常有意思。第一階段loss下降速度極快第一個(gè)epoch結(jié)束loss就從一個(gè)較高的初始值直接掉了下來但驗(yàn)證F1一直卡在85%到87%之間不漲。原因在于分類頭在快速擬合特征分布的同時(shí)骨干網(wǎng)絡(luò)是凍結(jié)的它輸出的特征本身并沒有適配猴痘任務(wù)所以性能上限被鎖死了。第二階段一解凍前兩個(gè)epoch驗(yàn)證F1反而小幅下降。這個(gè)現(xiàn)象其實(shí)正常解凍后骨干網(wǎng)絡(luò)的權(quán)重開始被梯度修改特征分布發(fā)生偏移之前訓(xùn)練好的分類頭對新特征分布一時(shí)不適應(yīng)。到第3個(gè)epoch開始F1重新回升并迅速突破90%。如果你在微調(diào)時(shí)也看到這種V型曲線不要慌繼續(xù)跑就對了。這種凍結(jié)到解凍過程中的特征偏移可以類比成一個(gè)裝修師傅先按舊戶型圖紙配好了家具尺寸突然改動(dòng)墻體后家具需要重新調(diào)整位置一開始擺放自然會(huì)有短暫的混亂。5. 踩坑實(shí)錄類別不平衡、過擬合與部署細(xì)節(jié)5.1 類別不平衡加權(quán)采樣解決正常皮膚壓制問題原始數(shù)據(jù)的類別分布并不均勻正常皮膚圖片接近800張猴痘病變只有400張出頭其他皮膚病變500張左右。如果不做任何處理模型會(huì)傾向于把一切不確定樣本都判成樣本量最多的類別表現(xiàn)在混淆矩陣?yán)锞褪呛锒活愓倩芈时焕?。解決辦法我試了兩種。第一種是給損失函數(shù)加類別權(quán)重讓少數(shù)類的梯度貢獻(xiàn)更大。交叉熵?fù)p失的weight參數(shù)按類別樣本數(shù)的反比設(shè)置就好這個(gè)方案實(shí)現(xiàn)簡單但訓(xùn)練時(shí)可能出現(xiàn)少數(shù)類f1虛高而多數(shù)類f1下降的副作用。第二種是采用WeightedRandomSampler在采樣階段讓每個(gè)batch里不同類別的樣本數(shù)量相對均衡。我最終用的是第二種原因是我可以在保持損失函數(shù)干凈的前提下直接控制每個(gè)batch的類別構(gòu)成。采樣器實(shí)現(xiàn)代碼如下from torch.utils.data import WeightedRandomSampler labels [int(sample[1]) for sample in dataset] class_counts torch.bincount(torch.tensor(labels)) class_weights 1.0 / class_counts.float() sample_weights class_weights[labels] sampler WeightedRandomSampler( sample_weights, num_sampleslen(sample_weights), replacementTrue )用上采樣器之后猴痘類的召回率從0.83提升到0.89同時(shí)其他兩個(gè)類的指標(biāo)沒有明顯回退。值得提醒的是replacementTrue意味著同一個(gè)樣本可能在同一個(gè)epoch里被反復(fù)抽到這時(shí)候隨機(jī)數(shù)據(jù)增強(qiáng)就特別重要——它能讓同一個(gè)樣本每次以不同的形態(tài)出現(xiàn)降低反復(fù)采樣帶來的過擬合風(fēng)險(xiǎn)。5.2 過擬合排查從準(zhǔn)確率虛高到驗(yàn)證集停滯項(xiàng)目過程中我遇到過最典型的問題訓(xùn)練集準(zhǔn)確率一路飆升到99.5%但驗(yàn)證集準(zhǔn)確率始終徘徊在90%上下怎么都上不去。這就是標(biāo)準(zhǔn)的過擬合信號。我的排查順序是這樣的。先查數(shù)據(jù)泄漏。按患者ID分組劃分之后問題依然存在。再查增強(qiáng)策略。我當(dāng)時(shí)一度懷疑增強(qiáng)強(qiáng)度不夠試過把rotation加到45度、scale下限降到0.5結(jié)果驗(yàn)證集F1反而掉了兩個(gè)點(diǎn)。最后在分類頭里加Dropout層同時(shí)給全連接層加L2正則驗(yàn)證集F1提升了一個(gè)點(diǎn)左右但不夠解渴。真正讓驗(yàn)證集F1躍升到93%的操作是降低第二階段學(xué)習(xí)率。我從1e-4降到1e-5后驗(yàn)證曲線開始緊貼訓(xùn)練曲線差距從9個(gè)百分點(diǎn)縮小到4個(gè)百分點(diǎn)。解決辦法比我預(yù)想的要樸素得多——微調(diào)階段學(xué)習(xí)率太高模型已經(jīng)鉆進(jìn)了訓(xùn)練集的牛角尖里出不來。5.3 推理階段細(xì)節(jié)ONNX導(dǎo)出與多次推理平均訓(xùn)練完成后還要考慮實(shí)際使用。PyTorch模型直接部署會(huì)遇到環(huán)境依賴問題目標(biāo)機(jī)器得裝同樣版本的PyTorch和CUDA這在生產(chǎn)環(huán)境是件痛苦的事。我當(dāng)時(shí)把模型導(dǎo)出成了ONNX格式推理時(shí)用ONNX Runtime加載部署環(huán)境瞬間清爽了許多。import torch model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, monkeypox_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}} )推理階段還有個(gè)細(xì)節(jié)單張圖片的預(yù)測結(jié)果受增強(qiáng)隨機(jī)性影響很小因?yàn)橥评頃r(shí)已經(jīng)關(guān)了隨機(jī)增強(qiáng)但模型本身的Dropout層在eval模式下也會(huì)關(guān)閉所以輸出應(yīng)該是穩(wěn)定的。如果擔(dān)心極端情況下的抖動(dòng)可以連續(xù)推理三次取softmax概率的平均值再取argmax實(shí)測可以把極少數(shù)置信度在0.5邊緣的樣本穩(wěn)定下來。5.4 如果擴(kuò)展成多類別皮膚病變識(shí)別猴痘病識(shí)別做順了之后很自然能想到擴(kuò)展成更通用的皮膚病分類系統(tǒng)。思路和數(shù)據(jù)準(zhǔn)備邏輯是完全一致的收集更多類別的皮膚圖像按病變類型建目錄做來源分組劃分沿用兩階段微調(diào)管線。唯一的差異在于類別增加到十幾種之后類別間的混淆會(huì)變得更加復(fù)雜混淆矩陣?yán)飼?huì)出現(xiàn)更多的非相鄰錯(cuò)誤。這時(shí)候可以考慮用層次分類——先粗分成水皰性斑疹性鱗屑性幾個(gè)大類再在每個(gè)大類下細(xì)分具體疾病。層次分類的好處是讓模型在每一層只需做少量類別的區(qū)分錯(cuò)誤率天然比一個(gè)十幾分類的扁平模型低。這個(gè)方向我準(zhǔn)備在P5周試試到時(shí)候有了新結(jié)果再回來更新。寫在最后的小心得這個(gè)項(xiàng)目做下來最深的體會(huì)是圖像分類項(xiàng)目的天花板大部分時(shí)候不是模型結(jié)構(gòu)決定的而是對數(shù)據(jù)的理解深度決定的。一開始我也想著換更花哨的模型試過EfficientNet試過加SE模塊效果都不如老老實(shí)實(shí)解決數(shù)據(jù)泄漏和類別不平衡問題來得實(shí)在。后來把按患者分組劃分、兩階段微調(diào)、保守?cái)?shù)據(jù)增強(qiáng)這幾件事全部做對之后不用換模型測試集F1照樣從90%升到了94%。另外一個(gè)小建議所有涉及醫(yī)學(xué)圖像的項(xiàng)目在公布評估指標(biāo)時(shí)一定要附上混淆矩陣和數(shù)據(jù)劃分方式。準(zhǔn)確率這個(gè)數(shù)字太容易被玩壞了只有看到混淆矩陣和劃分邏輯別人才能真正判斷你的模型水平是真實(shí)能力還是泄漏出來的幻覺。如果你也在做類似的細(xì)粒度圖像分類項(xiàng)目希望這篇經(jīng)驗(yàn)帖能幫你少走那些我走過的彎路。特別是數(shù)據(jù)分組劃分這一步務(wù)必放在所有實(shí)驗(yàn)的最前面否則后面所有調(diào)參和優(yōu)化都是在沙地上建高樓。