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


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

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

先借用上一篇的兩組圖:


先看上邊的一組圖,隨着訓練迭代次數的增長,預測的錯誤率迅速降低。
咱們上一篇中講,達到必定迭代次數以後,驗證的錯誤率就穩定不變了。實際上你仔細觀察,訓練集的錯誤率在穩定降低,但驗證集的錯誤率還會略有上升。二者之間的差別愈來愈大,圖中的兩條曲線,顯著分離了,而且分離的趨勢還在增長。這就是過擬合的典型特徵。
這表示,模型過度適應了當前的訓練集數據,對於訓練集數據有了較好表現。對於以外的數據,反而不適應,從而效果不好。
這一般都是因爲較小的數據樣本形成的。若是數據集足夠大,較多的訓練一般都能讓模型表現的更好。過擬合對於生產環境傷害是比較大的,由於生產中大多接收到的都是新數據,而過擬合沒法對這些新數據達成較好表現。
因此若是數據集不夠的狀況下,採用適當的迭代次數多是更好的選擇。這也是上一節咱們採用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相同:code

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(綠色曲線)都有效的改善了模型的過擬合問題。
blog

(待續...)

相關文章
相關標籤/搜索