iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
生成式 AI

《AI 新手到職場應用:深度學習 30 天實戰》系列 第 20

CNN 實作:打造你的第一個圖片分類器(2/2)

  • 分享至 

  • xImage
  •  

接續昨天程式碼解說,話不多說,即刻開始。
(還沒閱讀過昨日文章者,可以先前往上一篇觀看)


5. 豐富的資料增強

train_datagen = ImageDataGenerator(
    rotation_range=20,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,
    fill_mode='nearest'
)

# 驗證集不使用資料增強
val_datagen = ImageDataGenerator()

train_datagen.fit(x_train)

在這段程式碼中,我們建立了「資料增強系統」,就像為訓練資料創造各種變化版本。

ImageDataGenerator 是 Keras 提供的資料增強工具,
我們設定了多種變換參數,以下就分開解釋一下他們的功用:

rotation_range=20 讓圖片隨機旋轉正負 20 度,模擬物體傾斜的情況;

width_shift_rangeheight_shift_range 設為 0.2,
圖片隨機水平和垂直移動 20%,模擬物體不在中央的場景;

shear_range=0.2 進行剪切變換,像把正方形拉成平行四邊形,模擬不同視角;

zoom_range=0.2 隨機縮放,讓模型適應大小不同的物體;

horizontal_flip=True 隨機水平翻轉,但要注意有些物體翻轉後意義會改變。

fill_mode='nearest' 決定變換後空白區域用最近的像素填充。

還有一個東西非常重要:
我們只對「訓練集」做資料增強,驗證集保持原樣以確保評估的客觀性。

最後用 fit() 讓資料產生器學習訓練資料的統計特性。

參考資料:
https://www.wpgdadatong.com/blog/detail/71762


6. callbacks

在看程式碼前,我們當然要先知道什麼是「callbacks」:

在 Keras 裡,callbacks(回呼函式/回呼物件)是一種「在模型訓練過程中可以被自動呼叫的功能」。
它們就像「監聽器」一樣,會在特定時機點
(例如訓練開始、每個 epoch 結束、每個 batch 結束、訓練結束)自動執行指定的程式碼。

用我的理解我會這樣形容:
他就是一個紀錄的工具,每次都會更新最優的紀錄,
然後把它儲存起來。

而在了解完callbacks後,我們就可以來看程式碼了:

def create_callbacks():
    return [
        tf.keras.callbacks.ModelCheckpoint(
            'best_cifar10_model.h5',
            monitor='val_accuracy',
            save_best_only=True,
            save_weights_only=False,
            verbose=1
        ),
        tf.keras.callbacks.ReduceLROnPlateau(
            monitor='val_loss',
            factor=0.3,
            patience=5,
            min_lr=1e-7,
            verbose=1
        ),
        tf.keras.callbacks.EarlyStopping(
            monitor='val_loss',
            patience=15,
            restore_best_weights=True,
            verbose=1
        ),
        tf.keras.callbacks.CSVLogger('training_log.csv')
    ]

callbacks = create_callbacks()

這段程式碼設定了四個智慧助手來監控訓練過程:
ModelCheckpoint 像「自動存檔功能」,監控 val_accuracy(驗證準確率),
每當發現更好的模型就自動儲存到 'best_cifar10_model.h5',確保我們永遠保留最佳版本。

ReduceLROnPlateau 是「學習率調節器」,當 val_loss(驗證損失)
連續 5 個 epoch(patience=5)沒改善時,就將學習率降為原來的 30%(factor=0.3),
最低不超過 1e-7,就像爬山接近山頂時自動放慢腳步。

EarlyStopping 防止過度訓練,當驗證損失連續 15 個 epoch 沒改善就自動停止訓練,
並恢復到最佳權重,避免過擬合。

CSVLogger 則像「訓練日記」,把每個 epoch 的所有指標記錄到 CSV 文件中,方便後續分析。


7. 訓練模型(使用 datagen):

batch_size = 128
epochs = 100
history = model.fit(
    datagen.flow(x_train, y_train, batch_size=batch_size),
    steps_per_epoch = x_train.shape[0] // batch_size,
    epochs = epochs,
    validation_data = (x_test, y_test),
    callbacks = callbacks,
    verbose = 2
)

這段程式碼就是啟動實際訓練了,這邊就會整合前面準備的所有組件並開始讓它動起來啦!!!

之前提過很多次的函數我這邊就先省略,主要介紹一些之前沒有學習過的:

model.fit() : 核心訓練函數
datagen.flow() : 會源源不絕地提供經過資料增強的圖片批次
steps_per_epoch : 計算每個 epoch 需要多少批次(50000÷128≈390批次)
verbose=2 : 顯示每個 epoch 的訓練進度。

在訓練過程中,模型會重複進行「前向傳播(預測)計算損失→反向傳播(計算梯度)→更新權重」的循環,
並且會不斷記錄並更新,逐漸變得更聰明。


8. 評估模型

def evaluate_model(model, x_test, y_test):
    print("\n=== 模型評估 ===")
    results = model.evaluate(x_test, y_test, verbose=0)
    
    # 處理不同數量的指標
    if isinstance(results, list):
        test_loss = results[0]
        test_acc = results[1]
        if len(results) > 2:
            test_top5 = results[2]
            print(f"測試集損失值: {test_loss:.4f}")
            print(f"測試集正確率: {test_acc:.4f} ({test_acc*100:.2f}%)")
            print(f"Top-5 正確率: {test_top5:.4f} ({test_top5*100:.2f}%)")
        else:
            print(f"測試集損失值: {test_loss:.4f}")
            print(f"測試集正確率: {test_acc:.4f} ({test_acc*100:.2f}%)")
    else:
        # 只有一個值的情況
        test_loss = results
        test_acc = 0.0
        print(f"測試集損失值: {test_loss:.4f}")
    
    # 預測並計算混淆矩陣
    y_pred = model.predict(x_test, verbose=0)
    y_pred_classes = np.argmax(y_pred, axis=1)
    
    return test_acc, y_pred_classes

test_acc, y_pred_classes = evaluate_model(model, x_test, y_test)

接著在模型評估部分,我們建立了彈性的模型評估系統,能適應不同的指標配置。

函數首先用 model.evaluate() 在測試集上評估模型表現,處理並返回結果:
如果是列表就依序提取損失值準確率和可能的 Top-5 準確率
如果只有單一數值就當作損失值處理。

損失值反映預測與真實答案的整體差距,準確率是最直觀的「答對百分比」,
Top-5 準確率則寬鬆一些,只要正確答案在前 5 個猜測中就算對。

接著用 model.predict() 獲得所有測試圖片的預測機率分布,
再用 np.argmax() 找出每張圖片機率最高的類別作為最終預測結果,
方便後續分析混淆矩陣或錯誤案例。

參考資料:
https://www.tensorflow.org/api_docs/python/tf/math/argmax


9. 視覺化

def plot_training_history(history):
    fig, axes = plt.subplots(2, 2, figsize=(15, 10))
    
    # Loss 曲線
    axes[0,0].plot(history.history['loss'], label='訓練 Loss', linewidth=2)
    axes[0,0].plot(history.history['val_loss'], label='驗證 Loss', linewidth=2)
    axes[0,0].set_title('Loss 曲線', fontsize=14)
    axes[0,0].set_xlabel('Epoch')
    axes[0,0].set_ylabel('Loss')
    axes[0,0].legend()
    axes[0,0].grid(True, alpha=0.3)
    
    # Accuracy 曲線
    axes[0,1].plot(history.history['accuracy'], label='訓練 Accuracy', linewidth=2)
    axes[0,1].plot(history.history['val_accuracy'], label='驗證 Accuracy', linewidth=2)
    axes[0,1].set_title('Accuracy 曲線', fontsize=14)
    axes[0,1].set_xlabel('Epoch')
    axes[0,1].set_ylabel('Accuracy')
    axes[0,1].legend()
    axes[0,1].grid(True, alpha=0.3)
    
    # Learning Rate 曲線 (如果有記錄)
    if 'lr' in history.history:
        axes[1,0].plot(history.history['lr'], linewidth=2, color='orange')
        axes[1,0].set_title('Learning Rate', fontsize=14)
        axes[1,0].set_xlabel('Epoch')
        axes[1,0].set_ylabel('Learning Rate')
        axes[1,0].set_yscale('log')
        axes[1,0].grid(True, alpha=0.3)
    
    # Top-5 Accuracy
    if 'top_5_accuracy' in history.history:
        axes[1,1].plot(history.history['top_5_accuracy'], label='訓練 Top-5', linewidth=2)
        axes[1,1].plot(history.history['val_top_5_accuracy'], label='驗證 Top-5', linewidth=2)
        axes[1,1].set_title('Top-5 Accuracy 曲線', fontsize=14)
        axes[1,1].set_xlabel('Epoch')
        axes[1,1].set_ylabel('Top-5 Accuracy')
        axes[1,1].legend()
        axes[1,1].grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('training_curves.png', dpi=300, bbox_inches='tight')
    plt.show()

plot_training_history(history)

在視覺化部分的程式碼,我們創建了一個多面向的訓練過程視覺化。

首先,使用 2×2 的子圖布局,展示四個關鍵面向:

Loss 曲線顯示訓練和驗證的錯誤程度變化,理想情況下兩條線都應該平穩下降且保持相近;

Accuracy 曲線更直觀地顯示正確率提升軌跡,幫助識別過擬合(訓練準確率遠高於驗證準確率);

Learning Rate 曲線追蹤學習率的動態變化,使用對數刻度顯示 ReduceLROnPlateau 的調整效果;

Top-5 Accuracy 曲線提供更寬鬆的評估視角。

每個圖表都加上網格線、圖例和適當的標籤,最終以高解析度儲存為 PNG 檔案。
這些視覺化工具是診斷訓練問題的重要依據,好的曲線應該顯示平滑收斂、
無劇烈震盪、訓練與驗證表現相近的特徵。

https://ithelp.ithome.com.tw/upload/images/20250929/20169196wbmSSdyH4F.pnghttps://ithelp.ithome.com.tw/upload/images/20250929/20169196D8VkkGqKwS.png

上面兩張圖片就是執行出來的結果,這邊先看形式為主,
後面會再解釋。


10. 預測展示

def show_predictions(model, x_test, y_test, num_samples=8):
    """展示預測結果和真實標籤"""
    fig, axes = plt.subplots(2, 4, figsize=(16, 8))
    axes = axes.ravel()
    
    # 隨機選擇樣本
    indices = np.random.choice(len(x_test), num_samples, replace=False)
    
    for i, idx in enumerate(indices):
        img = x_test[idx]
        pred = model.predict(img[np.newaxis, ...], verbose=0)
        pred_class = np.argmax(pred[0])
        pred_prob = np.max(pred[0])
        true_class = y_test[idx]
        
        axes[i].imshow(img)
        axes[i].axis('off')
        
        # 設定顏色:正確=綠色,錯誤=紅色
        color = 'green' if pred_class == true_class else 'red'
        axes[i].set_title(
            f'True: {class_names[true_class]}\n'
            f'Pred: {class_names[pred_class]}\n'
            f'Conf: {pred_prob:.3f}',
            color=color, fontsize=10
        )
    
    plt.tight_layout()
    plt.savefig('predictions_sample.png', dpi=300, bbox_inches='tight')
    plt.show()

show_predictions(model, x_test, y_test)

這邊就是很直觀的預測結果展示系統,讓我們能親眼看到模型的「思考過程」。
函數會隨機選擇 8 張測試圖片進行展示,使用 2×4 的網格布局。
對每張圖片,先用 img[np.newaxis, ...] 添加批次維度(因為 predict 需要批次輸入),
然後獲得模型的預測機率分布。
np.argmax() 找出機率最高的類別作為預測結果,np.max() 獲得對應的信心度分數。

接著顯示原始圖像並關閉座標軸,用顏色編碼預測結果:
綠色表示預測正確,紅色表示錯誤。
標題顯示真實類別名稱、預測類別名稱和信心度(範圍 0-1)。

這種視覺化比純數字統計更有價值,我們可以分析模型容易搞混哪些類別、
是否受圖片品質影響、高信心度預測是否更準確等問題,對於建立對模型能力的直觀理解非常重要。

https://ithelp.ithome.com.tw/upload/images/20250929/20169196WgD8K94OEZ.pnghttps://ithelp.ithome.com.tw/upload/images/20250929/20169196yo1KeYp3fP.png


11. 儲存模型

print("\n儲存最終模型...")
model.save('cifar10_improved_cnn.h5')
print("模型已儲存為 'cifar10_improved_cnn.h5'")

# 儲存訓練歷史
import pickle
with open('training_history.pkl', 'wb') as f:
    pickle.dump(history.history, f)
print("訓練歷史已儲存為 'training_history.pkl'")

# 輸出最終結果摘要
print(f"\n=== 訓練完成 ===")
print(f"最佳驗證準確率: {max(history.history['val_accuracy']):.4f}")
print(f"最終測試準確率: {test_acc:.4f}")
print(f"訓練總輪數: {len(history.history['loss'])}")

這邊就是最後的程式碼,完成了訓練成果的完整保存工作。

model.save() 將完整模型儲存為 H5 格式,包含模型架構、訓練好的權重和編譯配置,
這樣就能在任何時候重新載入完整功能的模型。

接著用 pickle 模組將 history.history(包含每個 epoch 的所有指標記錄)序列化儲存,
這些詳細的訓練歷史對未來分析、比較實驗或撰寫報告都很有價值。

最後輸出關鍵的訓練總結:最佳驗證準確率反映模型的最高潛能
最終測試準確率代表在未見過資料上的真實表現,
實際訓練輪數顯示是否因 EarlyStopping 提前結束。


成果展示:

最後當然就是成果展示啦!
因為一開始的結果不盡人意,準確率只有可憐的47%,
所以我有對程式碼做以下些為更改:

1.增加初始學習率:

我將初始學習率從一開始的 1e-3 調整成 5e-3
因為有可能是因為初始學習率 1e-3 太小,導致模型學習太慢。

initial_learning_rate = 5e-3  # 從 1e-3 改為 5e-3

2.資料增強過度

我將資料增強的幅度調低了一些,
因為當增強太激進時,模型可能會難以學習一致的特徵。

train_datagen = ImageDataGenerator(
    rotation_range=10,        # 從 20 減為 10
    width_shift_range=0.1,    # 從 0.2 減為 0.1
    height_shift_range=0.1,   # 從 0.2 減為 0.1

在更改完過後,我們就來比較一下差異吧:

版本一(初版):
https://ithelp.ithome.com.tw/upload/images/20250929/20169196NL6koJw7Ql.png
可以看到,藍線(訓練 Loss)波動很大,橘線(驗證 Loss)則幾乎是一條平線,
這顯示模型無法有效學習,驗證損失完全沒有下降趨勢。

而在Accuracy曲線中也是一樣,Accuracy值幾乎沒有因訓練次數上升而有提高,
也代表說他根本沒有學習到任何東西,他把老師的話都當耳邊風

版本二(修改版):
https://ithelp.ithome.com.tw/upload/images/20250929/20169196BqlZ4xvogu.png
相對上面的圖片應該就很明顯了,這組曲線明顯有因訓練次數上升,
而降低損失率並提高了準確率。

大家也可以看一下CIFAR-10透過訓練出來的圖片辨識圖:
https://ithelp.ithome.com.tw/upload/images/20250929/20169196n3LPu4oaQD.png

最終,我們從原本的47%準確率提高到了63%,還可以再精進,
但目前已經達到學習效果了,今天就先到這裡就好,後面還有很多實作,
下次再戰!!!

https://ithelp.ithome.com.tw/upload/images/20250929/201691969JaYZQv6Wb.png


上一篇
CNN 實作:打造你的第一個圖片分類器(1/2)
下一篇
認識時間序列與文字:RNN 的核心概念
系列文
《AI 新手到職場應用:深度學習 30 天實戰》21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言