TensorFlow從1到2(八)過擬合和欠擬合的優化



《從鍋爐工到AI專家(6)》一文中,我們把神經網絡模型降維,簡單的在二維空間中介紹了過擬合和欠擬合的現象和解決方法。但是因為條件所限,在該文中我們只介紹了理論,並沒有實際觀察現象和應對。
現在有了TensorFLow 2.0 / Keras的支持,可以非常容易的構建模型。我們可以方便的人工模擬過擬合的情形,實際來操作監控、調整模型,從而顯著改善模型指標。

從圖中識別過擬合和欠擬合

先借用上一篇的兩組圖:


先看上邊的一組圖,隨着訓練迭代次數的增加,預測的錯誤率迅速下降。
我們上一篇中講,達到一定迭代次數之后,驗證的錯誤率就穩定不變了。實際上你仔細觀察,訓練集的錯誤率在穩定下降,但驗證集的錯誤率還會略有上升。兩者之間的差異越來越大,圖中的兩條曲線,顯著分離了,並且分離的趨勢還在增加。這就是過擬合的典型特征。
這表示,模型過分適應了當前的訓練集數據,對於訓練集數據有了較好表現。對於之外的數據,反而不適應,從而效果很差。
這通常都是由於較小的數據樣本造成的。如果數據集足夠大,較多的訓練通常都能讓模型表現的更好。過擬合對於生產環境傷害是比較大的,因為生產中大多接收到的都是新數據,而過擬合無法對這些新數據達成較好表現。
所以如果數據集不夠的情況下,采用適當的迭代次數可能是更好的選擇。這也是上一節我們采用EarlyStopping機制的原因之一。最終的表現是上邊下面一組圖的樣子。
欠擬合與此相反,表示模型還有較大改善空間。上面兩組圖中,左側下降沿的曲線都可以認為是欠擬合。表現特征是無論測試集還是驗證集,都沒有足夠的正確率。當然也因此,測試集和驗證集表現類似,擬合非常緊密。
欠擬合的情況,除了訓練不足之外,模型不夠強大或者或者模型不適合業務情況都是可能的原因。

實驗模擬過擬合

我們使用IMDB影評樣本庫來做這個實驗。實驗程序主要部分來自於本系列第五篇中第二個例子,當然有較大的修改。
程序主要分為幾個部分:

  • 下載IMDB影評庫(僅第一次),載入內存,並做單詞向量化。
  • 單詞向量化編碼使用了multi-hot-sequences,這種編碼跟one-hot類似,但一句話中有多個單詞,因此會有多個'1'。一個影評就是一個0、1序列。這種編碼模型非常有用,但在本例中,數據歧義會更多,更容易出現過擬合。
  • 定義baseline/small/big三個不同規模的神經網絡模型,並分別編譯訓練,訓練時保存過程數據。
  • 使用三組過程數據繪制曲線圖,指標是binary_crossentropy,這是我們經常當做損失函數使用的指征,這個值在正常訓練的時候收斂到越小越好。

程序中,文本的編碼方式、模型都並不是很合理,因為我們不是想得到一個最優的模型,而是想演示過擬合的場景。

#!/usr/bin/env python3

from __future__ import absolute_import, division, print_function, unicode_literals

import tensorflow as tf
from tensorflow import keras

import numpy as np
import matplotlib.pyplot as plt

NUM_WORDS = 10000
# 載入IMDB樣本數據
(train_data, train_labels), (test_data, test_labels) = keras.datasets.imdb.load_data(num_words=NUM_WORDS)

# 將單詞數字化,轉化為multi-hot序列編碼方式
def multi_hot_sequences(sequences, dimension):
    # 建立一個空矩陣保存結果
    results = np.zeros((len(sequences), dimension))
    for i, word_indices in enumerate(sequences):
        results[i, word_indices] = 1.0  # 出現過的詞設置為1.0
    return results

train_data = multi_hot_sequences(train_data, dimension=NUM_WORDS)
test_data = multi_hot_sequences(test_data, dimension=NUM_WORDS)

# 建立baseline模型,並編譯訓練
baseline_model = keras.Sequential([
    # 指定`input_shape`以保證下面的.summary()可以執行,
    # 否則在模型結構無法確定
    keras.layers.Dense(16, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])
baseline_model.compile(optimizer='adam',
                       loss='binary_crossentropy',
                       metrics=['accuracy', 'binary_crossentropy'])
baseline_model.summary()
baseline_history = baseline_model.fit(train_data,
                                      train_labels,
                                      epochs=20,
                                      batch_size=512,
                                      validation_data=(test_data, test_labels),
                                      verbose=2)
# 小模型定義、編譯、訓練
smaller_model = keras.Sequential([
    keras.layers.Dense(4, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(4, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])
smaller_model.compile(optimizer='adam',
                      loss='binary_crossentropy',
                      metrics=['accuracy', 'binary_crossentropy'])
smaller_model.summary()
smaller_history = smaller_model.fit(train_data,
                                    train_labels,
                                    epochs=20,
                                    batch_size=512,
                                    validation_data=(test_data, test_labels),
                                    verbose=2)
# 大模型定義、編譯、訓練
bigger_model = keras.models.Sequential([
    keras.layers.Dense(512, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(512, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])

bigger_model.compile(optimizer='adam',
                     loss='binary_crossentropy',
                     metrics=['accuracy','binary_crossentropy'])

bigger_model.summary()
bigger_history = bigger_model.fit(train_data, train_labels,
                                  epochs=20,
                                  batch_size=512,
                                  validation_data=(test_data, test_labels),
                                  verbose=2)

# 繪圖函數
def plot_history(histories, key='binary_crossentropy'):
    plt.figure(figsize=(16,10))

    for name, history in histories:
        val = plt.plot(
            history.epoch, history.history['val_'+key],
            '--', label=name.title()+' Val')
        plt.plot(
            history.epoch, history.history[key], color=val[0].get_color(),
            label=name.title()+' Train')

    plt.xlabel('Epochs')
    plt.ylabel(key.replace('_',' ').title())
    plt.legend()

    plt.xlim([0,max(history.epoch)])
    plt.show()


# 繪制三個模型的三組曲線
plot_history([('baseline', baseline_history),
              ('smaller', smaller_history),
              ('bigger', bigger_history)])

程序在命令行的輸出就不貼出來了,除了輸出的訓練迭代過程,在之前還輸出了每個模型的summary()。這里主要看最后的binary_crossentropy曲線圖。

圖中的虛線都是驗證集數據的表現,實線是訓練集數據的表現。三個模型的訓練數據和測試數據交叉熵曲線都出現了較大的分離,代表出現了過擬合。尤其是bigger模型的兩條綠線,幾乎是一開始就出現了較大的背離。

優化過擬合

優化過擬合首先要知道過擬合產生的原因,我們借用一張前一系列講解過擬合時候用過的圖,是吳恩達老師課程的筆記:

如果一個模型產生過擬合,那這個模型的總體效果就可能是一個非常復雜的非線性方程。方程非常努力的學習所有“可見”數據,導致了復雜的權重值,使得曲線彎來彎去,變得極為復雜。多層網絡更加劇了這種復雜度,最終的復雜曲線繞開了可行的區域,只對局部的可見數據有效,對於實際數據命中率低。所以從我們程序跑的結果圖來看,也是越復雜的網絡模型,過擬合現象反而越嚴重。
這么說簡單的模型就好嘍?並非如此,太簡單的模型往往無法表達復雜的邏輯,從而產生欠擬合。其實看看成熟的那些模型比如ResNet50,都是非常復雜的結構。
過擬合既然產生的主要原因是在權重值上,我們在這方面做工作即可。

增加權重的規范化

通常有兩種方法,稱為L1規范化和L2規范化。前者為代價值增加一定比例的權重值的絕對值。后者增加一定比例權重值的平方值。具體的實現來源於公式,有興趣的可以參考一下這篇文章《L1 and L2 Regularization》
我們刪除掉上面源碼中的bigger模型和small模型的部分,包括模型的構建、編譯和訓練,添加下面的代碼:

# 構建一個L2規范化的模型
l2_model = keras.models.Sequential([
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])

l2_model.compile(optimizer='adam',
                 loss='binary_crossentropy',
                 metrics=['accuracy', 'binary_crossentropy'])

l2_model_history = l2_model.fit(train_data, train_labels,
                                epochs=20,
                                batch_size=512,
                                validation_data=(test_data, test_labels),
                                verbose=2)

這個模型的邏輯結構同baseline的模型完全一致,只是在前兩層中增加了L2規范化的設置參數。
先不着急運行,我們繼續另外一種方法。

添加DropOut

DropOut是我們在上個系列中已經講過的方法,應用的很廣泛也非常有效。
其機理非常簡單,就是在一層網絡中,“丟棄”一定比例的輸出(設置為數值0)給下一層。丟棄的比例通常設置為0.2至0.5。這個過程只在訓練過程中有效,一般會在預測過程中關閉這個機制。
我們繼續在上面代碼中,添加一組采用DropOut機制的模型,模型的基本結構依然同baseline相同:


dpt_model = keras.models.Sequential([
    keras.layers.Dense(16, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(16, activation='relu'),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(1, activation='sigmoid')
])

dpt_model.compile(optimizer='adam',
                  loss='binary_crossentropy',
                  metrics=['accuracy','binary_crossentropy'])

dpt_model_history = dpt_model.fit(train_data, train_labels,
                                  epochs=20,
                                  batch_size=512,
                                  validation_data=(test_data, test_labels),
                                  verbose=2)
		....
# 最后的繪圖函數不變,繪圖語句修改如下:
plot_history([
            ('baseline', baseline_history),
            ('l2', l2_model_history),
            ('dropout', dpt_model_history)])

現在可以執行程序了。
程序獲得的曲線圖如下,圖中可見,我們在不降低模型的復雜度的情況下,L2規范化(黃色曲線)和DropOut(綠色曲線)都有效的改善了模型的過擬合問題。

(待續...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM