:畢設(shè)全流程思路與實踐)
每年都有人把深度學(xué)習(xí)畢設(shè)做成“調(diào)包燉蘑菇”把公開數(shù)據(jù)集往預(yù)訓(xùn)練模型里一塞訓(xùn)練幾輪就交差題目和結(jié)果兩張皮。今天我把“基于CNN識別花卉是否綻放”這個畢設(shè)從立項到答辯的完整思路捋一遍重點不在跑通代碼而在每一步怎么自圓其說怎么把“是什么”“為什么”“怎么調(diào)”講明白。這套玩法換個課題也能直接移植。1. 項目本質(zhì)拆解你以為在分類其實在擬合“綻放程度”先把題目解剖清楚。“識別花卉是否綻放”是個圖像二分類問題輸出是“綻放”或“未綻放”。但如果你把它當(dāng)成普通的貓狗分類去做大概率會掛在數(shù)據(jù)上——因為“是否綻放”不是物種級的視覺差異而是同一種花在不同生長階段的表型差異?;ò桶腴_、盛開的區(qū)別往往就是花瓣張開角度、花蕊可見性、顏色飽和度這些細微變化。這一點直接決定了后續(xù)所有選擇不能用現(xiàn)成的花卉品種分類數(shù)據(jù)集直接訓(xùn)練因為那些數(shù)據(jù)標的是“這是什么花”不是“這朵花開沒開”。模型需要更關(guān)注局部紋理和形狀比例所以卷積核的感受野設(shè)計、注意力機制的引入都有討論空間。評價指標不能只看準確率因為如果數(shù)據(jù)里“未綻放”占多數(shù)模型全猜“未綻放”也能有好看的數(shù)字必須引入精確率、召回率、F1甚至畫混淆矩陣。我當(dāng)時和朋友聊這個題他第一反應(yīng)是“這有什么難的分類嘛”。我反問了他三個問題花苞算不算未綻放半開的算不算凋謝的算不算如果這三個問題定義不清楚標注的人會瘋掉模型也會學(xué)到錯誤邊界。所以項目的第一步不是敲代碼而是寫標注規(guī)范。建議把所有圖片分成三類而不是兩類未綻放花苞、閉合狀態(tài)、綻放中花萼已裂、花瓣可見但未全開、完全綻放花瓣充分展開、花蕊暴露。訓(xùn)練時把后兩類合并成“綻放”但標注階段保留三分類標簽這樣既能處理邊界情況又能為后續(xù)做細粒度分類留余地。2. 數(shù)據(jù)集的構(gòu)建與預(yù)處理2.1 公開數(shù)據(jù)集的可用性篩查很多同學(xué)會直接去下載“花卉數(shù)據(jù)集”但我篩查了一圈常見公開數(shù)據(jù)集的標注都是品種級別包括某高校開源的花卉數(shù)據(jù)集、國際上知名的牛津花數(shù)據(jù)集里面沒有“綻放狀態(tài)”標簽。所以只能自己造數(shù)據(jù)。造數(shù)據(jù)有兩條路基于品種數(shù)據(jù)集二次標注下載一批花卉圖片自己人工篩選綻放狀態(tài)寫個小腳本重新生成標簽文件。優(yōu)點是圖片質(zhì)量高、物種多樣缺點是費眼睛且原始數(shù)據(jù)集里的花苞圖往往很少正負樣本會不平衡。爬蟲采集按“含苞待放”“花苞特寫”“盛開的花”等關(guān)鍵詞爬公開圖片。優(yōu)點是能控制樣本分布缺點是要花很多時間清洗錯誤樣本。我推薦混合方案主體用公開花卉數(shù)據(jù)集做二次標注再用爬蟲補充花苞和半開狀態(tài)的圖片最后統(tǒng)一用腳本清洗大小、壓縮格式和亮度。2.2 清洗與標注的實操細節(jié)清洗環(huán)節(jié)有個容易忽視的坑圖片文件名和標簽對不上。建議一律改成統(tǒng)一命名規(guī)則比如“Tulip_12.jpg”配上同一個CSV的id字段不要用中文文件名也不要讓標簽存在文件名里除非你想在讀取數(shù)據(jù)時被編碼折磨。標注規(guī)范要寫成文檔至少包含三條標準花苞狀態(tài)花瓣未張開外層萼片包裹明顯。綻放中花瓣開始展開但整體輪廓仍呈杯狀或半球形花蕊不可見或部分可見。完全綻放花瓣展平或外翻花蕊充分暴露雄蕊可見。標注的時候最好兩個人背靠背各標一遍再取交集遇到不一致的單獨處理。這個“背靠背標注”的流程在開題報告和論文里都能寫出方法論價值導(dǎo)師看了會加分。2.3 數(shù)據(jù)增強要“適度”花卉識別比較容易過擬合因為背景、光照、拍攝角度的影響經(jīng)常比花本身的狀態(tài)還大。數(shù)據(jù)增強我常用的組合是隨機水平翻轉(zhuǎn)、隨機旋轉(zhuǎn)10到15度、隨機亮度飽和度抖動、隨機裁剪后縮放回原尺寸。有一點要提醒不要用太激進的增強比如大角度旋轉(zhuǎn)和上下翻轉(zhuǎn)。玫瑰花倒過來就不像花了模型會把上下文背景當(dāng)成特征反而降低泛化能力。隨機裁剪裁剪范圍也不要超過原圖的15%到20%否則會切掉整個花朵。另外建議在訓(xùn)練集之外單獨留出驗證集保證驗證集里同一朵花的不同角度圖片不要同時出現(xiàn)在訓(xùn)練集和驗證集。要按“花”去分數(shù)據(jù)不要按“圖片”去分。實際寫代碼的時候你得先按文件夾或文件名前綴聚合出唯一的“花”再劃分train/val/test否則同一朵花的不同照片會泄漏進驗證集指標虛高答辯時一問就露餡。3. CNN模型設(shè)計從零訓(xùn)練還是遷移學(xué)習(xí)3.1 基礎(chǔ)CNN的結(jié)構(gòu)邏輯先看一眼基礎(chǔ)版結(jié)構(gòu)我給出PyTorch風(fēng)格的代碼但沒裝PyTorch環(huán)境也看得懂核心是理解每一層的動機import torch.nn as nn class FlowerBlossomNet(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 16, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.BatchNorm2d(16), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.BatchNorm2d(32), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.BatchNorm2d(64), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.BatchNorm2d(128), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(128, 64), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(64, 2) ) def forward(self, x): x self.features(x) x self.classifier(x) return x幾個關(guān)鍵決策點輸入統(tǒng)一為224×224的RGB圖。這個尺寸不是拍腦袋定的而是綜合了計算開銷和細節(jié)保留能力。輸入越小訓(xùn)練越快但花瓣紋理容易丟失輸入越大顯存壓力大訓(xùn)練時間成倍增長。224是多種預(yù)訓(xùn)練模型的標準輸入尺寸如果你將來打算遷移學(xué)習(xí)可以直接復(fù)用這套預(yù)處理流程。BatchNorm放在每個卷積后的ReLU之前能顯著減少“梯度消失”和“內(nèi)部協(xié)變量偏移”的影響。理解上你可以把BatchNorm理解為每一層輸出的數(shù)據(jù)分布被拉回標準范圍下一層不用費力去適應(yīng)上一層輸出范圍的變化。全連接層之前用AdaptiveAvgPool2d把特征圖壓成1×1無論輸入尺寸怎么變?nèi)B接層的輸入維度都固定。這比Flatten后接尺寸固定的全連接層要靈活很多也降低了過擬合風(fēng)險。3.2 為什么要跟遷移學(xué)習(xí)做對比畢業(yè)論文如果想有“工作量”最好不要只用自己寫的小網(wǎng)絡(luò)。我的建議是至少跑三組對照自定義淺層CNN上述結(jié)構(gòu)。微調(diào)預(yù)訓(xùn)練的ResNet18或MobileNetV2替換最后的全連接層凍結(jié)前若干層只微調(diào)后面幾個stage。線性探測只替換分類頭凍結(jié)全部卷積層作為下限參照。為什么因為從零訓(xùn)練的小網(wǎng)絡(luò)在少量數(shù)據(jù)上往往表現(xiàn)不穩(wěn)而遷移學(xué)習(xí)大概率會漲點。如果你的自定義CNN效果好于遷移學(xué)習(xí)這是一個很有說服力的結(jié)論如果略低于遷移學(xué)習(xí)也很正常你能在論文里解釋清楚“數(shù)據(jù)量不足時預(yù)訓(xùn)練特征的優(yōu)勢”引用現(xiàn)有研究支撐即可。遷移學(xué)習(xí)代碼片段PyTorch寫法示意import torchvision.models as models def build_transfer_model(num_classes2, freeze_ratio0.5): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 凍結(jié)前面的層 modules list(model.children()) freeze_num int(len(modules) * freeze_ratio) for layer in modules[:freeze_num]: for param in layer.parameters(): param.requires_grad False # 替換分類頭 model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(model.fc.in_features, num_classes) ) return model凍結(jié)比例值得聊一下。如果數(shù)據(jù)量大你可以少凍結(jié)一些如果數(shù)據(jù)量只有幾千張建議凍結(jié)一半以上避免底層卷積被少量數(shù)據(jù)帶偏。這里有個經(jīng)驗值數(shù)據(jù)量小于5000張時Bottom layers全凍結(jié)、只微調(diào)最后兩個Block和全連接層成功率最高。3.3 類別不均衡與Loss函數(shù)設(shè)計假設(shè)你最后的花苞圖只有盛開圖的五分之一直接上交叉熵模型會嚴重偏向多數(shù)類。常規(guī)做法是給loss加上class weightimport torch # class_weight [0.5, 1.0] 表示“未綻放”權(quán)重小“綻放”權(quán)重大 loss_fn nn.CrossEntropyLoss(weighttorch.tensor([0.5, 1.0]).cuda())權(quán)重的設(shè)定邏輯很簡單少數(shù)類樣本少導(dǎo)致它對梯度的貢獻小給它一個更大的權(quán)重讓模型每次更新時更“在意”少數(shù)類的分類錯誤。具體值根據(jù)“多數(shù)類樣本數(shù)/少數(shù)類樣本數(shù)”折算比如比例是3:1那權(quán)重可以取1和3。注意如果權(quán)重給太大模型會嚴重誤殺多數(shù)類建議在1到3倍之間嘗試。4. 訓(xùn)練過程的完整實操4.1 超參數(shù)怎么定我的習(xí)慣是先固定一組基線再單獨去調(diào)最敏感的參數(shù)?;€設(shè)置如下優(yōu)化器Adam初始學(xué)習(xí)率1e-3beta10.9beta20.999。批量大小16在224×224下16能讓大多數(shù)消費級顯卡跑得動也保留了梯度更新的隨機性。訓(xùn)練輪數(shù)50輪配合早停機制連續(xù)10輪驗證集F1不上升就停止。學(xué)習(xí)率衰減每20輪乘以0.1。為什么不用SGD對于畢設(shè)這種規(guī)模Adam收斂快且對學(xué)習(xí)率不那么敏感省去大量調(diào)參時間。但Adam有個特性它會把歷史梯度的二階矩累積得很大后期的更新步長偏小所以當(dāng)你發(fā)現(xiàn)驗證集指標“卡住”不再上升時往往不是模型不行而是學(xué)習(xí)率需要降下來了。4.2 訓(xùn)練循環(huán)里的三個關(guān)鍵細節(jié)第一個細節(jié)是訓(xùn)練與驗證模式切換。模型里如果用了Dropout和BatchNorm在驗證階段必須切到eval模式否則BatchNorm依然按訓(xùn)練模式統(tǒng)計批內(nèi)均值和方差驗證結(jié)果會亂掉帶來莫名其妙的波動。model.train() # 訓(xùn)練階段 ... model.eval() # 驗證/測試階段 with torch.no_grad(): outputs model(images)第二個細節(jié)是保存模型時一定要連 optimizer 的 state_dict 一起存。不然恢復(fù)訓(xùn)練時優(yōu)化器的動量信息全部丟失模型的收斂軌跡會斷掉。最簡單的做法是只存model.state_dict()用于推理但如果你想中途斷訓(xùn)續(xù)跑建議存完整checkpointcheckpoint { epoch: epoch, model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), best_f1: best_f1, } torch.save(checkpoint, checkpoint_v1.pt)第三個細節(jié)是早停的判斷標準。我建議監(jiān)控驗證集的F1指標而不是loss因為交叉熵的浮動可能迷惑人F1能更好地反映正類綻放的識別率。把patience設(shè)成10也就是連續(xù)10個epoch驗證F1沒有創(chuàng)新高就停止訓(xùn)練然后用best_f1對應(yīng)的模型權(quán)重做測試。4.3 可視化與日志訓(xùn)練過程要記錄四個東西train_loss、val_loss、train_f1、val_f1。用TensorBoard或每個epoch打印一行都行但必須保留完整記錄。答辯時老師問“你怎么判斷模型是否過擬合”你直接甩出兩張曲線圖訓(xùn)練損失持續(xù)下降、驗證損失在某個epoch后反彈這就叫過擬合。如果沒有日志你只能在臺上支支吾吾。我習(xí)慣在驗證階段額外生成一組預(yù)測正確與錯誤的可視化圖片一張錯誤樣本里同時顯示真實標簽、預(yù)測標簽、置信度。這能幫助快速定位模型是“把花苞認成綻放”還是“把半開認成未綻放”。如果是前者大概率是標注規(guī)范問題如果是后者可能是訓(xùn)練數(shù)據(jù)里半開樣本太少。5. 實驗結(jié)果的呈現(xiàn)與調(diào)優(yōu)技巧5.1 混淆矩陣是答辯利器不要只報告“準確率94%”這一點。多分類任務(wù)的答辯核心是“誰和誰混淆了”。畫一個2×2的混淆矩陣你會看清模型真正的短板。比如未綻放樣本的召回率低意味著很多花苞被漏判成綻放這對農(nóng)業(yè)生產(chǎn)里的花苞采摘提醒場景是不可接受的而綻放樣本的精確率低意味著用戶拿一張未開花圖片問系統(tǒng)系統(tǒng)容易誤報。答辯話術(shù)可以這么組織準確率反映總體水平但兩個類別的recall差距較大因此后續(xù)將采集更多花苞階段的樣本并考慮在Loss上加權(quán)。這是非常自然的改進方向。5.2 調(diào)優(yōu)順序很多人的調(diào)參順序是亂的一會兒改學(xué)習(xí)率一會兒改網(wǎng)絡(luò)層數(shù)結(jié)果模型崩了也分不清是哪步造成的。我的建議是先跑通基線確認數(shù)據(jù)管道沒問題。這一步卡很久往往是數(shù)據(jù)集路徑或標簽錯位問題代碼報錯不可怕怕的是不報錯但loss不下降。再檢查過擬合程度。如果訓(xùn)練F1接近1、驗證F1明顯低就用數(shù)據(jù)增強、Dropout和L2正則如果兩個都低那是模型容量或數(shù)據(jù)量的問題優(yōu)先增強數(shù)據(jù)或增大網(wǎng)絡(luò)。等模型“靠譜”了再動學(xué)習(xí)率和batch size。這兩個參數(shù)決定你最終能收斂到什么水平。最后才嘗試調(diào)整輸入分辨率和預(yù)處理策略。有一點容易被忽略每次只改一個變量。這個變量改完了跑一輪記下結(jié)果再改下一個。如果你同時改了網(wǎng)絡(luò)結(jié)構(gòu)和學(xué)習(xí)率后面分析實驗結(jié)果時根本說不清是誰帶來的提升審稿人和答辯老師都不會滿意。5.3 關(guān)于“準確率天花板”花卉綻放識別這個任務(wù)準確率到一定程度后會卡住卡住的水平大約在90%到95%之間。為什么因為花朵的綻放狀態(tài)本身存在灰度地帶有些花半開的狀態(tài)和全開從視覺上差異極小連人眼都判斷不穩(wěn)機器當(dāng)然也很難。你的畢設(shè)不需要做到100%但你需要可視化那些“模型判斷錯誤但人也模棱兩可”的樣例證明任務(wù)本身的挑戰(zhàn)性。這一手在答辯時非常有用能夠把“錯誤”轉(zhuǎn)化為“任務(wù)邊界分析”。6. 常見問題與排查技巧實錄6.1 loss不下降遇到loss幾乎不變化先別懷疑模型結(jié)構(gòu)查以下順序標簽是否錯位。打印幾個batch的圖片和對應(yīng)標簽人眼看一遍。是否真的在更新參數(shù)。觀察模型中某一層的權(quán)重值在一輪訓(xùn)練后是否發(fā)生變化。輸入圖片是否歸一化正確。如果像素值都是0到255而模型期望0到1初始梯度就可能是異常大或異常小導(dǎo)致訓(xùn)練不穩(wěn)定。初始化是否正常。如果用了不合適的初始化深層激活值會直接飽和梯度消失。我遇到過最詭異的情況是模型在CPU上正常上GPU之后loss是nan。排查半天發(fā)現(xiàn)是某張損壞的圖片含有inf像素值卷積一算就爆了。解決辦法是在數(shù)據(jù)加載后加一個數(shù)值剪切把像素值限幅到合法區(qū)間。6.2 GPU顯存不足224×224的輸入batch size從16起手顯存不夠就逐次減半到8、4。如果4都跑不了就把輸入尺寸縮到160×160。注意改輸入尺寸后原模型如果有預(yù)訓(xùn)練它的統(tǒng)計信息是基于224的可能在特征圖上產(chǎn)生偏差遷移學(xué)習(xí)場景要慎重。更好的做法是開啟混合精度訓(xùn)練。PyTorch里可以用自動混合精度包裝訓(xùn)練步驟顯存立刻省下百分之三四十速度還能提升。畢設(shè)階段不需要你推導(dǎo)混合精度的完整原理只需在論文的“實驗環(huán)境”一節(jié)說明采用了半精度浮點加速訓(xùn)練即可。6.3 訓(xùn)練時準確率高驗證時很低這是典型的過擬合信號要么數(shù)據(jù)量太小要么增強不夠。我的排查清單是先看訓(xùn)練集圖片數(shù)量。單類少于500張很容易過擬合。增加DataLoader里的NumWorkers會不會有影響不會它只影響吞吐不影響模型泛化。Dropout比例從0.2加到0.5看驗證F1是否回升。加上LabelSmoothing把硬標簽松弛一下。這個技巧對二分類很有效能讓模型對“邊界樣本”更溫和。LabelSmoothing的代碼不該是突然冒出來的它背后對應(yīng)一個洞察花卉綻放邊界模糊硬標簽0和1過于絕對模型被迫用無限大的logit輸出去擬合這個絕對性會導(dǎo)致泛化下降。改用0.1的平滑后模型不再追求輸出完美的0或1概率泛化更好。這個理由寫進論文里是加分的。6.4 光源和背景干擾花卉識別和一般物體識別最大的不同是背景經(jīng)常比主體還醒目。不少模型實際上是“通過背景判斷是否綻放”如果背景是花園大概率綻放如果背景是深色盆栽大概率未綻放。這就很糟。我的對策是引入隨機擦除Random Erase增強隨機遮掉圖像的一小塊區(qū)域訓(xùn)練強迫模型利用花朵本身的結(jié)構(gòu)信息另外把圖片做中心裁剪讓花朵盡量占滿畫面削弱背景信號。如果你有時間和算力也可以跑一個熱力圖可視化把模型的關(guān)注點畫出來看看模型在看花瓣還是看背景。這一步如果效果好就是論文里的亮點圖。7. 從畢設(shè)到可用的推理方案7.1 菜雞也能跑起來的推理腳本訓(xùn)練完模型最終要交付一個能對單張圖片做預(yù)測的腳本。別把推理邏輯寫在訓(xùn)練代碼里單獨拆出來import torch from PIL import Image from torchvision import transforms def predict_single(model, image_path, devicecuda): 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]), ]) image Image.open(image_path).convert(RGB) tensor transform(image).unsqueeze(0).to(device) model.eval() with torch.no_grad(): logits model(tensor) prob torch.softmax(logits, dim1) return prob.cpu().numpy()[0] # [P(未綻放), P(綻放)]歸一化參數(shù)是ImageNet數(shù)據(jù)集的統(tǒng)計值遷移學(xué)習(xí)場景直接沿用如果是純從零訓(xùn)練你可以改成自己數(shù)據(jù)集的均值和標準差差異不會特別大。7.2 TinyWeb姿態(tài)的部署思路如果畢設(shè)有拓展要求想做成一個網(wǎng)頁Demo推薦用Flask把上述預(yù)測腳本包成一個REST接口。前端傳一張圖后端返回兩個概率值。界面不需要花哨一個文件上傳按鈕加一個結(jié)果展示區(qū)域就夠了。這里有個部署的坑模型文件要加載到CPU內(nèi)存里做推理不要依賴顯存。部署機器的GPU不一定和訓(xùn)練機一樣如果服務(wù)器沒有CUDA環(huán)境就map_locationcpu保證預(yù)測能在純CPU上跑。雖然單張圖多幾十毫秒延遲但換來的是最大兼容性。8. 最后的經(jīng)驗小結(jié)與答辯切入視角做這類圖像分類畢設(shè)最后一個月能不能從容收尾取決于前面數(shù)據(jù)標注、日志記錄這些階段有沒有偷懶。我的體感是模型訓(xùn)練只占總工作量的四成數(shù)據(jù)準備占四成結(jié)果分析和可視化占兩成。很多同學(xué)前半個月就訓(xùn)練出不錯的結(jié)果后半個月卻不知道每天該干什么就是因為沒有留出充足時間做實驗對比和可視化。答辯時最容易被追問的三個問題你提前準備好答案為什么不用更大的網(wǎng)絡(luò)回答應(yīng)當(dāng)是數(shù)據(jù)量只有數(shù)萬張大網(wǎng)絡(luò)會嚴重過擬合訓(xùn)練成本也高小網(wǎng)絡(luò)已經(jīng)能捕捉花瓣紋理級特征。數(shù)據(jù)哪里來的回答應(yīng)當(dāng)是有標注規(guī)范、有清洗流程、有人工復(fù)核而不是“從網(wǎng)上下載的”。怎么證明模型真的學(xué)會了“綻放”的概念回答應(yīng)當(dāng)是用熱力圖展示模型聚焦在花蕊和花瓣邊緣以及展示錯誤樣本來說明任務(wù)的灰度邊界。最后分享一個我屢試不爽的小技巧訓(xùn)練完成后隨機找20張測試圖片自己先肉眼預(yù)測一遍再跟模型預(yù)測對照。找到模型錯但你也沒把握的圖片把它們單獨放進“邊界樣本”文件夾。答辯展示里這部分內(nèi)容最能體現(xiàn)你對任務(wù)理解的深度比單純報一個準確率數(shù)字有說服力得多。這個習(xí)慣畢業(yè)之后做任何視覺項目也都能沿用。