戰(zhàn):四分類模型構(gòu)建與調(diào)參)
簡介這是一套面向高校學(xué)生與深度學(xué)習(xí)入門者的花卉識別實(shí)戰(zhàn)資源以 TensorFlow 構(gòu)建卷積神經(jīng)網(wǎng)絡(luò)完成郁金香、玫瑰、蒲公英、向日葵四類常見花卉的圖像分類。壓縮包總大小約兩百八十兆字節(jié)內(nèi)含完整源碼、數(shù)據(jù)集與圖片素材文件總數(shù)達(dá)兩千個其中一千九百九十一張 JPG 圖片以類別劃分可直接用于模型訓(xùn)練兩個 Python 腳本分別承擔(dān)模型構(gòu)建與預(yù)測推理另有多份文檔說明與 HTML、JS 文件用于搭建簡單的可視化展示頁面并配有演示視頻便于對照學(xué)習(xí)整體流程。資源整體結(jié)構(gòu)清晰適合畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)以及項(xiàng)目初期立項(xiàng)演示。當(dāng)前已有七十二人學(xué)習(xí)下載代碼經(jīng)過多人、多環(huán)境測試運(yùn)行穩(wěn)定配套設(shè)計(jì)文檔和演示視頻能有效降低復(fù)現(xiàn)門檻幫助讀者掌握從數(shù)據(jù)預(yù)處理、網(wǎng)絡(luò)搭建、訓(xùn)練評估到前端展示的完整鏈路還可在此基礎(chǔ)上修改擴(kuò)展實(shí)現(xiàn)更多花卉類別的識別任務(wù)。1. 為什么用 CNN 做花卉識別而不是傳統(tǒng)圖像方法花卉識別看上去是圖像分類里最親切的任務(wù)但真要區(qū)分郁金香、玫瑰、蒲公英和向日葵傳統(tǒng)方法并不輕松。郁金香有杯狀花冠玫瑰是層疊花瓣蒲公英是放射狀絨球向日葵有巨大的中央花盤顏色分布又有大量重疊單靠顏色直方圖或 HOG 特征設(shè)計(jì)分類器很容易把黃玫瑰和向日葵混淆。CNN 用卷積核逐層學(xué)習(xí)邊緣、紋理、花瓣排列等特征對這類細(xì)粒度差異的區(qū)分能力要強(qiáng)得多。這也是這個 TensorFlow 花卉識別項(xiàng)目選擇 CNN 做四分類的直接原因。資源包里除了源碼還帶四類花的圖片數(shù)據(jù)集、演示視頻和設(shè)計(jì)文檔完整跑通后對課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)來說是比較扎實(shí)的素材。適合有 Python 基礎(chǔ)、想系統(tǒng)過一遍 TensorFlow 2.x 圖像分類流程但不想一上來就啃 ResNet 和遷移學(xué)習(xí)源碼的讀者。你可以把這份代碼里的四類花換成其他類別只需要換目錄和標(biāo)簽數(shù)量整體訓(xùn)練流程不用動。2. 數(shù)據(jù)集組織與 TensorFlow 數(shù)據(jù)管道四類花卉圖片的讀取與增強(qiáng)把壓縮包解壓后你會發(fā)現(xiàn)圖片文件基本都在同一個目錄下文件名保留著 Flickr 原始照片 ID并沒有按類別放進(jìn)子目錄。資源包里還附帶一個index.html一般是圖片來源頁或預(yù)覽索引和訓(xùn)練過程沒有直接關(guān)系。TensorFlow 的image_dataset_from_directory直接依賴目錄結(jié)構(gòu)生成標(biāo)簽所以第一步是把圖片整理成下面這種結(jié)構(gòu)。如果資源包里的數(shù)據(jù)已經(jīng)分好類這一步可以跳過但多數(shù)時候拿到手還是要重新梳理一遍。2.1 數(shù)據(jù)目錄結(jié)構(gòu)與標(biāo)簽映射flower_dataset/ ├── train/ │ ├── tulip/ # 郁金香 │ ├── rose/ # 玫瑰 │ ├── dandelion/ # 蒲公英 │ └── sunflower/ # 向日葵 ├── validation/ │ ├── tulip/ │ ├── rose/ │ ├── dandelion/ │ └── sunflower/ └── test/ ├── tulip/ └── ...這里有個容易被忽略的點(diǎn)image_dataset_from_directory會按照目錄名的字母序給類別編號所以四類花的標(biāo)簽順序是 dandelion、rose、sunflower、tulip而不是你在頁面上看到的郁金香、玫瑰、蒲公英、向日葵。后面做預(yù)測時類別數(shù)組必須按這個順序?qū)懛駝t輸出索引和真實(shí)類別會對不上。整理圖片時注意不要按文件名簡單均分。Flickr 上下載的圖里經(jīng)常有同一拍攝者的連拍照片如果這些相似圖片同時進(jìn)了訓(xùn)練集和驗(yàn)證集驗(yàn)證準(zhǔn)確率會虛高。我一般會先用文件名的 ID 前綴做一次分組再按組拆分避免同源圖片串集。這個細(xì)節(jié)在課程設(shè)計(jì)答辯時提到會是加分項(xiàng)。2.2 用 image_dataset_from_directory 加載數(shù)據(jù)加載部分直接使用 TensorFlow 的高層 APIimport tensorflow as tf IMG_SIZE (224, 224) BATCH_SIZE 32 train_ds tf.keras.preprocessing.image_dataset_from_directory( flower_dataset/train, image_sizeIMG_SIZE, batch_sizeBATCH_SIZE, label_modeint, shuffleTrue, seed42, ) val_ds tf.keras.preprocessing.image_dataset_from_directory( flower_dataset/validation, image_sizeIMG_SIZE, batch_sizeBATCH_SIZE, label_modeint, shuffleFalse, )參數(shù)里image_size會把所有圖片統(tǒng)一縮放到 224×224不管原圖是橫幅還是豎幅都會直接拉伸。對于花朵這種主體居中的圖片拉伸帶來的形變影響有限但如果后續(xù)換其他數(shù)據(jù)集先做居中裁剪再縮放會更穩(wěn)妥。label_modeint表示標(biāo)簽是整數(shù)索引對應(yīng)后面損失函數(shù)SparseCategoricalCrossentropy如果這里改成categorical標(biāo)簽就變成 one-hot 編碼損失函數(shù)也必須換成CategoricalCrossentropy。shuffle對訓(xùn)練集必須開啟驗(yàn)證集建議關(guān)閉這樣每個 epoch 的驗(yàn)證順序一致指標(biāo)可比性更強(qiáng)。seed固定后數(shù)據(jù)打亂順序可復(fù)現(xiàn)調(diào)參時不會被隨機(jī)性干擾。返回的train_ds是一個tf.data.Dataset每次迭代出一個(images, labels)組合images 的形狀是(32, 224, 224, 3)labels 形狀是(32,)。如果訓(xùn)練集和驗(yàn)證集是從同一個大目錄拆出來的也可以直接在image_dataset_from_directory里傳validation_split0.2配合subsettraining和validation使用效果相同但要求數(shù)據(jù)目錄只包含訓(xùn)練樣本。2.3 數(shù)據(jù)增強(qiáng)與歸一化參數(shù)設(shè)計(jì)花卉識別里翻轉(zhuǎn)和旋轉(zhuǎn)不會改變花的類別所以數(shù)據(jù)增強(qiáng)對這個項(xiàng)目提升非常明顯。TensorFlow 2.x 推薦把增強(qiáng)層直接寫在模型里這樣訓(xùn)練時生效推理時自動關(guān)閉不需要在數(shù)據(jù)管道里額外處理。data_augmentation tf.keras.Sequential([ tf.keras.layers.RandomFlip(horizontal), tf.keras.layers.RandomRotation(0.1), tf.keras.layers.RandomZoom(0.15), tf.keras.layers.RandomContrast(0.1), ]) rescale tf.keras.layers.Rescaling(1./255)RandomFlip(horizontal)做水平翻轉(zhuǎn)郁金香和向日葵翻轉(zhuǎn)后仍然是合理照片。RandomRotation(0.1)的 0.1 表示旋轉(zhuǎn)范圍是 ±36°超過這個角度蒲公英的放射狀特征會被裁掉。RandomZoom(0.15)做 15% 的縮放模擬拍攝距離變化。RandomContrast(0.1)只給 10% 的對比度波動玫瑰花瓣紋理密集對比度擾動太大會讓卷積核學(xué)到不穩(wěn)定的高光。這里有一個很常見的矛盾點(diǎn)如果模型里已經(jīng)放了Rescaling(1./255)數(shù)據(jù)管道里就不要再除以 255如果在tf.data里用了map(lambda x: x/255)模型里就不要放Rescaling。兩種方式都能收斂但混用時像素值范圍不是模型預(yù)期訓(xùn)練曲線會變得很奇怪。我在排查別人代碼時見過最多次的錯誤就是在兩邊同時歸一化導(dǎo)致 softmax 輸出始終接近均勻分布。后面預(yù)測階段還會再踩到一次。3. 搭建 CNN 卷積神經(jīng)網(wǎng)絡(luò)從卷積層到全連接層的參數(shù)設(shè)計(jì)數(shù)據(jù)管道準(zhǔn)備好后就進(jìn)入這個項(xiàng)目的核心CNN 本身。四分類的花卉圖片不需要非常深的網(wǎng)絡(luò)一個包含四個卷積塊的模型加上數(shù)據(jù)增強(qiáng)已經(jīng)能在驗(yàn)證集上得到不錯的準(zhǔn)確率。更深的網(wǎng)絡(luò)如 ResNet50 在這個數(shù)據(jù)規(guī)模下反而容易過擬合訓(xùn)練時間也長。下面的結(jié)構(gòu)是課程設(shè)計(jì)里比較標(biāo)準(zhǔn)的一套既完整展示了卷積、池化、全連接三種核心層參數(shù)量又不至于大到 CPU 跑不動。3.1 輸入形狀與卷積核的選擇模型輸入是(224, 224, 3)對應(yīng)image_dataset_from_directory里設(shè)置的image_size。如果數(shù)據(jù)加載時改成(128, 128)這里必須同步改否則訓(xùn)練會報維度錯誤。卷積核全部使用 3×3 小卷積核好處是參數(shù)量少兩個連續(xù) 3×3 卷積的感受野等于一個 5×5 卷積非線性表達(dá)能力卻更強(qiáng)。paddingsame會讓卷積輸出保持和輸入相同的空間尺寸配合MaxPooling2D每次把特征圖縮小一半信息逐步向通道維度集中。3.2 模型結(jié)構(gòu)Conv2D MaxPooling2D Dropout Densefrom tensorflow.keras import layers, models def build_cnn(input_shape(224, 224, 3), num_classes4): model models.Sequential([ layers.Input(shapeinput_shape), layers.Rescaling(1./255), layers.Conv2D(32, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dropout(0.5), layers.Dense(128, activationrelu), layers.Dense(num_classes, activationsoftmax) ]) return model模型里第一層放了Rescaling(1./255)與數(shù)據(jù)增強(qiáng)層共用同一個Sequential時要注意增強(qiáng)層也要包含在模型最前面否則訓(xùn)練和推理的預(yù)處理邏輯會不一致。Conv2D的卷積核數(shù)量從 32 逐步增加到 128淺層偏向?qū)W習(xí)邊緣和顏色塊深層需要更多通道表達(dá)花瓣紋理。MaxPooling2D的(2, 2)窗口把特征圖寬高減半四個池化層后224×224變成14×14Flatten 后是14*14*12825088維向量所以后面接的 Dense(128) 是主要參數(shù)量來源。Dropout(0.5)放在 Flatten 之后目的是讓全連接層不要過度依賴某個局部神經(jīng)元這里是訓(xùn)練時隨機(jī)屏蔽一半神經(jīng)元推理時全部恢復(fù)。如果訓(xùn)練集很小可以把 Dropout 提高到 0.6但不要超過 0.7否則模型會欠擬合。最后一個 Dense 層用 softmax 輸出四個類別的概率。如果不追求展示 Flatten 過程可以把最后一個池化層換成GlobalAveragePooling2D參數(shù)量會從 345 萬降到幾十萬訓(xùn)練更快精度不一定下降。但課程設(shè)計(jì)通常要求體現(xiàn) CNN 的完整結(jié)構(gòu)所以我保留 Flatten Dense 的寫法。3.3 編譯損失函數(shù)、優(yōu)化器與評價指標(biāo)model build_cnn() model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losstf.keras.losses.SparseCategoricalCrossentropy(), metrics[accuracy] ) model.summary()SparseCategoricalCrossentropy對應(yīng)整數(shù)標(biāo)簽如果你的標(biāo)簽是 one-hot 編碼就換成CategoricalCrossentropy。Adam優(yōu)化器默認(rèn)學(xué)習(xí)率 1e-3對這個項(xiàng)目來說可以先用這個值跑 5 個 epoch 觀察損失下降速度如果 loss 下降非常慢檢查是不是歸一化寫重復(fù)了如果 loss 震蕩明顯把學(xué)習(xí)率降到 1e-4。summary()輸出每一層參數(shù)數(shù)量下面是這個結(jié)構(gòu)的參數(shù)量表。層輸出形狀參數(shù)量Input(224, 224, 3)0Rescaling(224, 224, 3)0Conv2D MaxPooling(112, 112, 32)896Conv2D MaxPooling(56, 56, 64)18,496Conv2D MaxPooling(28, 28, 128)73,856Conv2D MaxPooling(14, 14, 128)147,584Flatten Dropout(25088,)0Dense(128,)3,211,264Dense(4,)516合計(jì)約 3.45M從表格能直觀看到三個卷積塊的總參數(shù)量只有約 24 萬最后一個全連接層占了超過 321 萬這就是 CNN 在圖像任務(wù)上比純?nèi)B接網(wǎng)絡(luò)節(jié)省參數(shù)的根本原因。如果你想壓縮模型可以把 Dense(128) 去掉直接接 softmax準(zhǔn)確率會略降但模型文件從十幾 MB 降到不到兩 MB。這個取舍在移動端部署時很關(guān)鍵。4. 訓(xùn)練中的過擬合控制與調(diào)參學(xué)習(xí)率、Batch Size 和 Early Stopping模型定義完成后訓(xùn)練環(huán)節(jié)往往是差距最大的部分。一開始跑這個花卉項(xiàng)目最容易看到的局面是訓(xùn)練集準(zhǔn)確率很快到 95% 以上驗(yàn)證集卻卡在 80% 上下波動。這不是網(wǎng)絡(luò)結(jié)構(gòu)錯了而是過擬合控制沒跟上。四類花的數(shù)據(jù)集總量不大每類圖片可能只有幾十到幾百張模型很容易記住訓(xùn)練圖里的背景顏色和拍攝角度。下面這套訓(xùn)練配置是專門為這種小數(shù)據(jù)集設(shè)計(jì)的。4.1 訓(xùn)練參數(shù)設(shè)置與回調(diào)函數(shù)callbacks [ tf.keras.callbacks.EarlyStopping( monitorval_accuracy, patience10, restore_best_weightsTrue, ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-6, ), tf.keras.callbacks.ModelCheckpoint( best_model.h5, monitorval_accuracy, save_best_onlyTrue, ), ] history model.fit( train_ds, validation_dataval_ds, epochs50, callbackscallbacks, )EarlyStopping的patience10表示連續(xù) 10 個 epoch 驗(yàn)證集準(zhǔn)確率沒有超過歷史最佳時停止訓(xùn)練restore_best_weightsTrue會把模型權(quán)重恢復(fù)到驗(yàn)證集最優(yōu)的那個 epoch。ReduceLROnPlateau監(jiān)測驗(yàn)證集損失5 個 epoch 不下降就把學(xué)習(xí)率減半直到最低 1e-6。這兩個回調(diào)一起用時訓(xùn)練通常會在第 25 到第 40 個 epoch 之間停下來而不是真的跑滿 50。ModelCheckpoint只保存驗(yàn)證準(zhǔn)確率最高的權(quán)重文件名叫best_model.h5方便后面加載。注意這里 monitor 一個用val_accuracy一個用val_loss。因?yàn)闇?zhǔn)確率是離散的在小驗(yàn)證集上會跳躍用損失作為學(xué)習(xí)率衰減的指標(biāo)更平滑。如果你只監(jiān)測準(zhǔn)確率可能在連續(xù)多個 epoch 都沒有變化的情況下錯過學(xué)習(xí)率調(diào)整時機(jī)。4.2 過擬合判斷與典型特征訓(xùn)練過程中不要只看準(zhǔn)確率要看訓(xùn)練集和驗(yàn)證集的差距。下表是四分類花卉項(xiàng)目里最常見的幾種情況?,F(xiàn)象判斷調(diào)整方向訓(xùn)練準(zhǔn)確率 95%驗(yàn)證準(zhǔn)確率 75%~85%過擬合增大 Dropout、加強(qiáng)數(shù)據(jù)增強(qiáng)、減少 Dense 層訓(xùn)練和驗(yàn)證都低于 70%欠擬合或?qū)W習(xí)率不合適增加卷積核數(shù)量、降低學(xué)習(xí)率、檢查歸一化驗(yàn)證損失先降后升準(zhǔn)確率徘徊開始過擬合提前停止、減少訓(xùn)練輪數(shù)驗(yàn)證準(zhǔn)確率曲線劇烈抖動batch size 太小或增強(qiáng)太強(qiáng)把 batch size 從 32 調(diào)到 64減弱旋轉(zhuǎn)角度在這個項(xiàng)目里如果驗(yàn)證集準(zhǔn)確率卡在 80% 左右而訓(xùn)練集已接近 100%最直接的兩個操作是把 Dropout 從 0.5 提到 0.6同時把RandomRotation從 0.1 調(diào)到 0.15。我測試下來這兩個改動疊加通常能帶來 3~5 個百分點(diǎn)的提升。要注意的是不要因?yàn)轵?yàn)證集準(zhǔn)確率不高就盲目加深網(wǎng)絡(luò)四個卷積塊在這個數(shù)據(jù)規(guī)模下已經(jīng)足夠再加深只會讓過擬合更嚴(yán)重。batch size 的影響也比較微妙。BATCH_SIZE32是默認(rèn)選擇顯存占用小但梯度噪聲大。如果驗(yàn)證曲線抖動明顯先嘗試把 batch size 提高到 64這時每個 step 更新的方向更穩(wěn)定學(xué)習(xí)率可以相應(yīng)調(diào)大一點(diǎn)比如從 1e-3 調(diào)到 2e-3。如果顯存不足就保持 32靠 ReduceLROnPlateau 拉平訓(xùn)練曲線。4.3 訓(xùn)練曲線可視化與保存import matplotlib.pyplot as plt acc history.history[accuracy] val_acc history.history[val_accuracy] epochs range(1, len(acc) 1) plt.plot(epochs, acc, labelTrain Accuracy) plt.plot(epochs, val_acc, labelVal Accuracy) plt.xlabel(Epoch) plt.ylabel(Accuracy) plt.legend() plt.savefig(training_curve.png)畫出來的曲線要保存成圖課程設(shè)計(jì)報告里直接使用即可。我一般還會把history.history[loss]和val_loss放到另一張圖里用于觀察過擬合出現(xiàn)的具體 epoch。如果保存模型用的是ModelCheckpoint訓(xùn)練結(jié)束后不要直接用model變量而是重新load_model(best_model.h5)因?yàn)橛?xùn)練流程跑完后model里的權(quán)重是最后一個 epoch 的不一定是最優(yōu)狀態(tài)。演示視頻里通常也會把加載模型后的預(yù)測結(jié)果單列出來就是為了驗(yàn)證保存和加載的一致性。5. 用訓(xùn)練好的模型識別郁金香單張圖片預(yù)測與導(dǎo)出技巧5.1 加載模型并做推斷模型訓(xùn)練完最終要落到單張圖片預(yù)測上。用load_model加載.h5文件它會自動重建模型結(jié)構(gòu)和權(quán)重import numpy as np from tensorflow.keras.preprocessing import image model tf.keras.models.load_model(best_model.h5) img image.load_img(test_images/tulip_001.jpg, target_size(224, 224)) img_array image.img_to_array(img) img_array np.expand_dims(img_array, axis0) # 增加 batch 維度 pred model.predict(img_array, verbose0)load_img里的target_size必須和數(shù)據(jù)加載時的image_size一致。這里的expand_dims是最容易漏的一步模型訓(xùn)練時輸入是四維張量(batch, height, width, channels)單張圖片只有三維必須在最前面加一維變成(1, 224, 224, 3)。如果模型定義時已經(jīng)包含了Rescaling層這里不需要手動除以 255如果模型里沒有則要在predict前加img_array img_array / 255.0。5.2 類別映射與置信度class_names [dandelion, rose, sunflower, tulip] score tf.nn.softmax(pred[0]) top np.argmax(score) confidence float(np.max(score)) print(f預(yù)測類別: {class_names[top]}, 置信度: {confidence:.2%})class_names的順序必須和訓(xùn)練目錄的字母序一致也就是 dandelion、rose、sunflower、tulip。如果訓(xùn)練時目錄是中文名那這里的列表就要改成與之一致但建議不要用中文目錄跨平臺且容易在解碼上出問題。因?yàn)槟P妥詈笠粚邮?softmaxpred[0]已經(jīng)是概率分布直接用np.argmax取最大概率的索引。如果你的模型在定義時沒有加 softmax輸出的是 logits預(yù)測時就一定要先做 softmax 再取 argmax。提示如果你把增強(qiáng)層寫在模型里預(yù)測時它會被自動關(guān)閉不需要額外處理。5.3 常見坑圖片預(yù)處理不一致單張圖片預(yù)測時最容易出問題的是預(yù)處理鏈路和訓(xùn)練時不統(tǒng)一。第一個坑是歸一化重復(fù)模型里有Rescaling層外面又做了除以 255像素值整體縮小 255 倍softmax 輸出會變得非常平四類概率都在 0.25 附近??梢詫Ρ容斎雸D片的像素均值判斷訓(xùn)練時模型接收的像素均值在 0~1 之間如果手寫預(yù)處理后均值只有 0.0039說明除以了兩次 255。第二個坑是圖片通道順序TensorFlow 默認(rèn)是 RGB但如果你用 OpenCV 讀圖得到的是 BGR會把郁金香的紅色花和綠色葉子交換通道導(dǎo)致預(yù)測完全錯亂。檢查方法是用img_array[0, 0, :]打印第一個像素的 RGB 數(shù)值與圖片實(shí)際顏色對照。第三個坑是target_size不匹配訓(xùn)練時用 224×224預(yù)測時寫成 229×229雖然模型能跑但特征分布和訓(xùn)練分布不一致準(zhǔn)確率明顯下降。檢查時可對照模型的輸入層形狀model.input_shape[1:3]會給出訓(xùn)練時實(shí)際使用的目標(biāo)尺寸。本文還有配套的精品資源點(diǎn)擊獲取