在深度學習領域,卷積神經網絡(CNN)已成為圖像識別任務的首選架構。而在CNN訓練過程中,優化器的選擇對模型性能有着至關重要的影響。本文將深入探討多種優化算法的原理,並通過手寫數字識別任務對比它們的實際表現。

優化器原理詳解

1. 隨機梯度下降(SGD)

隨機梯度下降是最基礎的優化算法,其更新公式為:

基於CNN的手寫數字識別_卷積

其中$η$是學習率,$∇J(θ_t)$是損失函數在$θ_t$處的梯度。

SGD的優點是簡單易懂,計算開銷小,但缺點是容易陷入局部最優,收斂速度慢,且對學習率的選擇非常敏感。

2. Momentum(動量優化)

Momentum算法在SGD基礎上引入了動量項,模擬物理中的動量概念:

基於CNN的手寫數字識別_卷積核_02

其中$γ$是動量係數(通常設為0.9),$v_t$是當前的速度向量。

Momentum的優點:

  • 加速收斂過程
  • 減少參數更新的震盪
  • 有助於跳出局部最小值

3. Adagrad(自適應梯度)

Adagrad算法為每個參數自適應地調整學習率:

基於CNN的手寫數字識別_卷積核_03

其中$G_t$是梯度平方的累積和,$ε$是為數值穩定性添加的小常數。

Adagrad的優點:

  • 自動調整每個參數的學習率
  • 適合處理稀疏數據

缺點:

  • 學習率會持續減小,可能導致過早停止學習

4. RMSprop(均方根傳播)

RMSprop改進了Adagrad的學習率持續下降問題:

基於CNN的手寫數字識別_卷積核_04

其中$β$是衰減率(通常設為0.9),$E[g^2]_t$是梯度平方的指數移動平均。

RMSprop的優點:

  • 解決了Adagrad學習率持續下降的問題
  • 在非平穩目標函數上表現良好

5. Adam(自適應矩估計)

Adam結合了Momentum和RMSprop的優點:

基於CNN的手寫數字識別_卷積核_05

其中$β_1$和$β_2$是衰減率(通常設為0.9和0.999),$m_t$和$v_t$分別是梯度的一階矩和二階矩估計。

Adam的優點:

  • 結合了動量法和自適應學習率的優勢
  • 對超參數的選擇相對魯棒
  • 在實踐中表現優異

使用MNIST手寫數字數據集,構建一個包含三個卷積層和兩個全連接層的CNN模型,對比上述優化器的性能。


import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
import time
class CNN(object):
    def __init__(self):
        model = keras.models.Sequential()
        # 第1層卷積,卷積核大小為3x3,32個,28x28為待訓練圖片的大小
        model.add(keras.layers.Conv2D(
            filters=32,          
            kernel_size=(3,3),
            activatinotallow='relu',
            input_shape=(28,28,1) 
        ))
        model.add(keras.layers.MaxPooling2D((2,2)))
        # 第2層卷積,卷積核大小為3x3,64個
        model.add(keras.layers.Conv2D(
            filters=64,
            kernel_size=(3,3),
            activatinotallow='relu'
        ))
        model.add(keras.layers.MaxPooling2D((2,2)))
        # 第3層卷積,卷積核大小為3x3,64個
        model.add(keras.layers.Conv2D(
            filters=64,
            kernel_size=(3,3),
            activatinotallow='relu'
        ))
        model.add(keras.layers.Flatten()) # 展平層,將多維輸入一維化
        model.add(keras.layers.Dense(64, activatinotallow='relu')) # 全連接層,64個神經元
        model.add(keras.layers.Dense(10, activatinotallow='softmax')) # 輸出層
        self.model = model
    
    def get_model(self):
        return keras.models.clone_model(self.model)
class TrainCNN(object):
    def __init__(self, model, X, y, optimizer='adam', epochs=5, batch_size=64):
        self.model = model
        self.X = X
        self.y = y
        self.epochs = epochs
        self.batch_size = batch_size
        self.optimizer_name = optimizer
        self.history = None
        self.training_time = 0
        
        #配置優化器
        if optimizer == 'sgd':
            opt = keras.optimizers.SGD(learning_rate=0.01)
        elif optimizer == 'momentum':
            opt = keras.optimizers.SGD(learning_rate=0.01, momentum=0.9)
        elif optimizer == 'adam':
            opt = keras.optimizers.Adam(learning_rate=0.001)
        elif optimizer == 'rmsprop':
            opt = keras.optimizers.RMSprop(learning_rate=0.001)
        else:
            opt = keras.optimizers.Adam(learning_rate=0.001)
        
        self.model.compile(
            optimizer=opt,
            loss='sparse_categorical_crossentropy',
            metrics=['accuracy']
        )
        
        #訓練模型並保存訓練歷史
        start_time = time.time()
        self.history = self.model.fit(
            self.X,
            self.y,
            epochs=self.epochs,
            batch_size=self.batch_size,
            validation_split=0.2,
            verbose=0  
        )
        self.training_time = time.time() - start_time

對比了五種優化器在MNIST數據集上的表現:

1. 收斂速度對比


基於CNN的手寫數字識別_卷積_06

從圖中看出:

    a.最優表現:RMSprop和Adam在準確率和損失上均表現最佳,早期收斂快、最終準確率接近1.00、損失接近0,且訓練/驗證指標差異小,泛化能力強,適合需要快速穩定收斂的場景。

   b.次優選擇:Momentum(帶動量的SGD)性能介於SGD和Adam之間,適合對收斂速度有一定要求但計算資源有限的場景。

   c.最差表現:SGD(隨機梯度下降)初始效率低、收斂慢,最終損失高,僅在數據量大或需要手動調參時考慮。

整體來看,自適應學習率優化器(RMSprop、Adam)在模型訓練中綜合性能顯著優於傳統的SGD及其變種。

2. 測試準確率與訓練時間對比

在測試集上的表現:

基於CNN的手寫數字識別_卷積_07

左側:測試集準確率對比

SGD(藍色):準確率0.9805(最低);

Momentum(綠色):準確率0.9903;

Adam(紅色):準確率0.9914;

RMSprop(紫色):準確率0.9923(最高)。

結論:準確率從低到高排序為:SGD < Momentum < Adam < RMSprop,RMSprop準確率最優。

右側:訓練時間對比

SGD(藍色):訓練時間130.28s(最長);

Adam(紅色):訓練時間129.40s;

Momentum(綠色):訓練時間125.71s;

RMSprop(紫色):訓練時間124.92s(最短)。

結論:訓練時間從長到短排序為:SGD > Adam > Momentum > RMSprop,RMSprop訓練效率最高。

綜合對比

RMSprop在兩項指標中均表現最優:準確率最高(0.9923) 且 訓練時間最短(124.92s);SGD則準確率最低(0.9805)且訓練時間最長(130.28s)。Momentum和Adam性能居中,其中Momentum訓練時間較短(125.71s),Adam準確率略高於Momentum(0.9914 vs 0.9903)。

3.不同優化器下的部分樣本預測結果

基於CNN的手寫數字識別_卷積核_08



應用場景建議:

  1. 簡單任務或教學演示:使用SGD,便於理解優化過程
  2. 標準深度學習任務:優先選擇Adam,綜合表現最佳
  3. 循環神經網絡:RMSprop通常表現更好
  4. 稀疏數據:Adagrad可能更適合
  5. 需要穩定訓練過程:Momentum可以減少震盪



源碼

import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
import time
class CNN(object):
    def __init__(self):
        model = keras.models.Sequential()
        # 第1層卷積,卷積核大小為3x3,32個,28x28為待訓練圖片的大小
        model.add(keras.layers.Conv2D(
            filters=32,          
            kernel_size=(3,3),
            activatinotallow='relu',
            input_shape=(28,28,1) 
        ))
        model.add(keras.layers.MaxPooling2D((2,2)))
        # 第2層卷積,卷積核大小為3x3,64個
        model.add(keras.layers.Conv2D(
            filters=64,
            kernel_size=(3,3),
            activatinotallow='relu'
        ))
        model.add(keras.layers.MaxPooling2D((2,2)))
        # 第3層卷積,卷積核大小為3x3,64個
        model.add(keras.layers.Conv2D(
            filters=64,
            kernel_size=(3,3),
            activatinotallow='relu'
        ))
        model.add(keras.layers.Flatten()) # 展平層,將多維輸入一維化
        model.add(keras.layers.Dense(64, activatinotallow='relu')) # 全連接層,64個神經元
        model.add(keras.layers.Dense(10, activatinotallow='softmax')) # 輸出層
        self.model = model
    
    def get_model(self):
        return keras.models.clone_model(self.model)
class TrainCNN(object):
    def __init__(self, model, X, y, optimizer='adam', epochs=5, batch_size=64):
        self.model = model
        self.X = X
        self.y = y
        self.epochs = epochs
        self.batch_size = batch_size
        self.optimizer_name = optimizer
        self.history = None
        self.training_time = 0
        
        #配置優化器
        if optimizer == 'sgd':
            opt = keras.optimizers.SGD(learning_rate=0.01)
        elif optimizer == 'momentum':
            opt = keras.optimizers.SGD(learning_rate=0.01, momentum=0.9)
        elif optimizer == 'adam':
            opt = keras.optimizers.Adam(learning_rate=0.001)
        elif optimizer == 'rmsprop':
            opt = keras.optimizers.RMSprop(learning_rate=0.001)
        else:
            opt = keras.optimizers.Adam(learning_rate=0.001)
        
        self.model.compile(
            optimizer=opt,
            loss='sparse_categorical_crossentropy',
            metrics=['accuracy']
        )
        
        #訓練模型並保存訓練歷史
        start_time = time.time()
        self.history = self.model.fit(
            self.X,
            self.y,
            epochs=self.epochs,
            batch_size=self.batch_size,
            validation_split=0.2,
            verbose=0  
        )
        self.training_time = time.time() - start_time
    
    def plot_training_history(self, ax1, ax2, label):
        
        history_dict = self.history.history
        epochs = range(1, len(history_dict['accuracy']) + 1)
        
        #準確率曲線
        ax1.plot(epochs, history_dict['accuracy'], 'o-', label=f'{label}訓練準確率', alpha=0.7)
        ax1.plot(epochs, history_dict['val_accuracy'], 's--', label=f'{label}驗證準確率', alpha=0.7)
        
        #損失曲線
        ax2.plot(epochs, history_dict['loss'], 'o-', label=f'{label}訓練損失', alpha=0.7)
        ax2.plot(epochs, history_dict['val_loss'], 's--', label=f'{label}驗證損失', alpha=0.7)
        
        #最終指標
        final_train_acc = history_dict['accuracy'][-1]
        final_val_acc = history_dict['val_accuracy'][-1]
        final_train_loss = history_dict['loss'][-1]
        final_val_loss = history_dict['val_loss'][-1]
        
        print(f"\n{label}優化器結果:")
        print(f"  訓練準確率: {final_train_acc:.4f}")
        print(f"  驗證準確率: {final_val_acc:.4f}")
        print(f"  訓練損失: {final_train_loss:.4f}")
        print(f"  驗證損失: {final_val_loss:.4f}")
        print(f"  訓練時間: {self.training_time:.2f}秒")
    
    def evaluate(self, X, y):
        test_loss, test_acc = self.model.evaluate(X, y, verbose=0)
        print(f"  測試集準確率: {test_acc:.4f}")
        print(f"  測試集損失: {test_loss:.4f}")
        return test_loss, test_acc
    
    def predict(self, X):
        predictions = self.model.predict(X, verbose=0)
        predicted_classes = np.argmax(predictions, axis=1)
        return predicted_classes
def compare_optimizers(X_train, y_train, X_test, y_test, epochs=10, batch_size=64):
    """比較不同優化器的性能"""
    optimizers = ['sgd', 'momentum', 'adam', 'rmsprop']
    optimizer_names = ['SGD', 'Momentum', 'Adam', 'RMSprop']
    colors = ['blue', 'green', 'red', 'purple']
    
    # 創建畫布
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
    
    trained_models = {}
    cnn_template = CNN()
    
    print("開始比較不同優化器...")
    
    for opt, name, color in zip(optimizers, optimizer_names, colors):
        print(f"\n訓練 {name} 優化器...")
        
        #創建新的模型實例
        model = cnn_template.get_model()
        
        trained_model = TrainCNN(
            model=model, 
            X=X_train, 
            y=y_train, 
            optimizer=opt, 
            epochs=epochs, 
            batch_size=batch_size
        )
        
        #存儲訓練好的模型
        trained_models[name] = trained_model
        
        #繪製收斂曲線
        trained_model.plot_training_history(ax1, ax2, name)
    
    ax1.set_title('不同優化器的準確率對比')
    ax1.set_xlabel('Epochs')
    ax1.set_ylabel('準確率')
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    
    ax2.set_title('不同優化器的損失對比')
    ax2.set_xlabel('Epochs')
    ax2.set_ylabel('損失')
    ax2.legend()
    ax2.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()
    
    print("\n" + "="*50)
    print("測試集性能對比:")
    print("="*50)
    
    test_results = {}
    for name, model in trained_models.items():
        print(f"\n{name}優化器:")
        test_loss, test_acc = model.evaluate(X_test, y_test)
        test_results[name] = {
            'test_accuracy': test_acc,
            'test_loss': test_loss,
            'training_time': model.training_time
        }
    
    #測試集性能對比圖
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
    
    #測試準確率對比
    names = list(test_results.keys())
    accuracies = [test_results[name]['test_accuracy'] for name in names]
    times = [test_results[name]['training_time'] for name in names]
    
    bars1 = ax1.bar(names, accuracies, color=colors, alpha=0.7)
    ax1.set_title('測試集準確率對比')
    ax1.set_ylabel('準確率')
    ax1.grid(True, alpha=0.3)
    
    for bar, acc in zip(bars1, accuracies):
        height = bar.get_height()
        ax1.text(bar.get_x() + bar.get_width()/2., height + 0.01,
                f'{acc:.4f}', ha='center', va='bottom')
    
    #訓練時間對比
    bars2 = ax2.bar(names, times, color=colors, alpha=0.7)
    ax2.set_title('訓練時間對比')
    ax2.set_ylabel('時間 (秒)')
    ax2.grid(True, alpha=0.3)
    
    for bar, t in zip(bars2, times):
        height = bar.get_height()
        ax2.text(bar.get_x() + bar.get_width()/2., height + 0.1,
                f'{t:.2f}s', ha='center', va='bottom')
    
    plt.tight_layout()
    plt.show()
    
    return trained_models, test_results
def plot_sample_predictions_comparison(models, X_test, y_test, num_samples=5):
    """比較不同模型在相同樣本上的預測結果"""
    n_models = len(models)
    fig, axes = plt.subplots(n_models, num_samples, figsize=(15, 3*n_models))
    
    if n_models == 1:
        axes = [axes]
    
    for i, (name, model_obj) in enumerate(models.items()):
        predicted_classes = model_obj.predict(X_test[:num_samples])
        
        for j in range(num_samples):
            ax = axes[i][j] if n_models > 1 else axes[j]
            ax.imshow(X_test[j].reshape(28, 28), cmap='gray')
            
            #正確為綠色,錯誤為紅色
            color = 'green' if predicted_classes[j] == y_test[j] else 'red'
            ax.set_title(f'{name}\n預測: {predicted_classes[j]}\n真實: {y_test[j]}', 
                        color=color, fnotallow=10)
            ax.axis('off')
    
    plt.tight_layout()
    plt.show()
if __name__ == '__main__':
 
    plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei']
    plt.rcParams['axes.unicode_minus'] = False
    
    (x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
    
    x_train = x_train.reshape((60000, 28, 28, 1))
    x_test = x_test.reshape((10000, 28, 28, 1))
    
    X_train = x_train.astype('float32') / 255.0
    X_test = x_test.astype('float32') / 255.0
    
    #比較不同優化器
    trained_models, test_results = compare_optimizers(
        X_train, y_train, X_test, y_test, epochs=10, batch_size=64
    )
    
    #最佳模型
    best_optimizer = max(test_results.items(), key=lambda x: x[1]['test_accuracy'])
    print(f"\n最佳優化器: {best_optimizer[0]}, 測試準確率: {best_optimizer[1]['test_accuracy']:.4f}")
    
    #比較不同模型在相同樣本上的預測
    print("\n不同優化器在相同樣本上的預測對比:")
    plot_sample_predictions_comparison(trained_models, X_test, y_test, num_samples=5)