過度擬合與擬合不足

在 TensorFlow.org 上檢視 在 Google Colab 中執行 在 GitHub 上檢視原始碼 下載筆記本

一如既往,本範例中的程式碼將使用 tf.keras API,您可以在 TensorFlow Keras 指南中瞭解更多資訊。

在先前的範例 (「文字分類」和「預測燃油效率」) 中,模型在驗證資料上的準確度會在訓練幾個週期後達到高峰,然後停滯或開始下降。

換句話說,您的模型會過度擬合訓練資料。學習如何處理過度擬合非常重要。雖然通常可以在訓練集上達到高準確度,但您真正想要的是開發能妥善泛化至測試集 (或模型之前未見過的資料) 的模型。

與過度擬合相反的是擬合不足。當訓練資料仍有改進空間時,就會發生擬合不足。發生擬合不足的原因有很多:如果模型不夠強大、過度正規化,或只是訓練時間不夠長。這表示網路尚未學習訓練資料中的相關模式。

但如果您訓練時間過長,模型就會開始過度擬合,並從訓練資料中學習無法泛化至測試資料的模式。您需要取得平衡。瞭解如何訓練適當的週期數 (如下所述) 是一項實用的技能。

為了防止過度擬合,最佳解決方案是使用更完整的訓練資料。資料集應涵蓋模型預期處理的完整輸入範圍。額外資料可能只有在涵蓋新的有趣案例時才有用。

在更完整資料上訓練的模型自然會更好地泛化。如果無法做到這一點,次佳的解決方案是使用正規化等技術。這些技術會限制模型可以儲存的資訊數量和類型。如果網路只能記住少數模式,則最佳化程序會強制網路專注於最顯著的模式,而這些模式更有可能妥善泛化。

在本筆記本中,您將探索幾種常見的正規化技術,並使用這些技術來改進分類模型。

設定

開始之前,請匯入必要的套件

import tensorflow as tf

from tensorflow.keras import layers
from tensorflow.keras import regularizers

print(tf.__version__)
!pip install git+https://github.com/tensorflow/docs

import tensorflow_docs as tfdocs
import tensorflow_docs.modeling
import tensorflow_docs.plots
from  IPython import display
from matplotlib import pyplot as plt

import numpy as np

import pathlib
import shutil
import tempfile
logdir = pathlib.Path(tempfile.mkdtemp())/"tensorboard_logs"
shutil.rmtree(logdir, ignore_errors=True)

Higgs 資料集

本教學課程的目標不是進行粒子物理學研究,因此請勿鑽研資料集的細節。它包含 11,000,000 個範例,每個範例有 28 個特徵和一個二元類別標籤。

gz = tf.keras.utils.get_file('HIGGS.csv.gz', 'http://mlphysics.ics.uci.edu/data/higgs/HIGGS.csv.gz')
FEATURES = 28

tf.data.experimental.CsvDataset 類別可用於直接從 gzip 檔案讀取 csv 記錄,而無需中間解壓縮步驟。

ds = tf.data.experimental.CsvDataset(gz,[float(),]*(FEATURES+1), compression_type="GZIP")

該 csv 讀取器類別會針對每個記錄傳回純量清單。下列函式會將純量清單重新封裝成 (特徵向量、標籤) 配對。

def pack_row(*row):
  label = row[0]
  features = tf.stack(row[1:],1)
  return features, label

TensorFlow 在處理大量資料批次時效率最高。

因此,與其個別重新封裝每個資料列,不如建立新的 tf.data.Dataset,該資料集會採用 10,000 個範例的批次,將 pack_row 函式套用至每個批次,然後將批次重新分割成個別記錄

packed_ds = ds.batch(10000).map(pack_row).unbatch()

檢查此新 packed_ds 中的一些記錄。

特徵並非完全正規化,但對於本教學課程而言已足夠。

for features,label in packed_ds.batch(1000).take(1):
  print(features[0])
  plt.hist(features.numpy().flatten(), bins = 101)

為了讓本教學課程相對簡短,請僅使用前 1,000 個樣本進行驗證,並使用接下來的 10,000 個樣本進行訓練

N_VALIDATION = int(1e3)
N_TRAIN = int(1e4)
BUFFER_SIZE = int(1e4)
BATCH_SIZE = 500
STEPS_PER_EPOCH = N_TRAIN//BATCH_SIZE

Dataset.skipDataset.take 方法讓此作業變得容易。

同時,使用 Dataset.cache 方法,確保載入器不需要在每個週期重新從檔案讀取資料

validate_ds = packed_ds.take(N_VALIDATION).cache()
train_ds = packed_ds.skip(N_VALIDATION).take(N_TRAIN).cache()
train_ds

這些資料集會傳回個別範例。使用 Dataset.batch 方法建立適用於訓練大小的批次。在批次處理之前,也請務必在訓練集上使用 Dataset.shuffleDataset.repeat

validate_ds = validate_ds.batch(BATCH_SIZE)
train_ds = train_ds.shuffle(BUFFER_SIZE).repeat().batch(BATCH_SIZE)

示範過度擬合

防止過度擬合的最簡單方法是從小型模型開始:具有少量可學習參數的模型 (由層數和每層的單元數決定)。在深度學習中,模型中可學習參數的數量通常稱為模型的「容量」。

直覺上,具有更多參數的模型將具有更多「記憶容量」,因此能夠輕鬆學習訓練樣本及其目標之間的完美字典式對應,這種對應沒有任何泛化能力,但在對先前未見過的資料進行預測時,這種對應將毫無用處。

請始終記住這一點:深度學習模型擅長擬合訓練資料,但真正的挑戰是泛化,而不是擬合。

另一方面,如果網路的記憶資源有限,則將無法輕易學習對應。為了儘量減少損失,網路必須學習具有更多預測能力的壓縮表示法。同時,如果您將模型做得太小,模型將難以擬合訓練資料。「容量過大」和「容量不足」之間存在平衡。

遺憾的是,沒有神奇的公式可以確定模型合適的大小或架構 (就層數或每層的合適大小而言)。您必須使用一系列不同的架構進行實驗。

為了找到合適的模型大小,最好從相對較少的層和參數開始,然後開始增加層的大小或新增層,直到您看到驗證損失的回報遞減為止。

從僅使用密集連接層 (tf.keras.layers.Dense) 的簡單模型開始作為基準,然後建立更大的模型,並比較這些模型。

訓練程序

如果您在訓練期間逐步降低學習率,許多模型的訓練效果會更好。使用 tf.keras.optimizers.schedules 隨著時間推移降低學習率

lr_schedule = tf.keras.optimizers.schedules.InverseTimeDecay(
  0.001,
  decay_steps=STEPS_PER_EPOCH*1000,
  decay_rate=1,
  staircase=False)

def get_optimizer():
  return tf.keras.optimizers.Adam(lr_schedule)

上述程式碼設定 tf.keras.optimizers.schedules.InverseTimeDecay,以雙曲線方式將學習率降低至 1,000 個週期時的基準率的 1/2、2,000 個週期時的 1/3,依此類推。

step = np.linspace(0,100000)
lr = lr_schedule(step)
plt.figure(figsize = (8,6))
plt.plot(step/STEPS_PER_EPOCH, lr)
plt.ylim([0,max(plt.ylim())])
plt.xlabel('Epoch')
_ = plt.ylabel('Learning Rate')

本教學課程中的每個模型都將使用相同的訓練設定。因此,請以可重複使用的方式設定這些,從回呼清單開始。

本教學課程的訓練會執行許多短週期。為了減少記錄雜訊,請使用 tfdocs.EpochDots,它只會針對每個週期列印 .,並每 100 個週期列印一組完整的指標。

接下來,納入 tf.keras.callbacks.EarlyStopping,以避免長時間且不必要的訓練時間。請注意,此回呼設定為監控 val_binary_crossentropy,而不是 val_loss。此差異稍後將很重要。

使用 callbacks.TensorBoard 為訓練產生 TensorBoard 記錄。

def get_callbacks(name):
  return [
    tfdocs.modeling.EpochDots(),
    tf.keras.callbacks.EarlyStopping(monitor='val_binary_crossentropy', patience=200),
    tf.keras.callbacks.TensorBoard(logdir/name),
  ]

同樣地,每個模型都將使用相同的 Model.compileModel.fit 設定

def compile_and_fit(model, name, optimizer=None, max_epochs=10000):
  if optimizer is None:
    optimizer = get_optimizer()
  model.compile(optimizer=optimizer,
                loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
                metrics=[
                  tf.keras.metrics.BinaryCrossentropy(
                      from_logits=True, name='binary_crossentropy'),
                  'accuracy'])

  model.summary()

  history = model.fit(
    train_ds,
    steps_per_epoch = STEPS_PER_EPOCH,
    epochs=max_epochs,
    validation_data=validate_ds,
    callbacks=get_callbacks(name),
    verbose=0)
  return history

微型模型

從訓練模型開始

tiny_model = tf.keras.Sequential([
    layers.Dense(16, activation='elu', input_shape=(FEATURES,)),
    layers.Dense(1)
])
size_histories = {}
size_histories['Tiny'] = compile_and_fit(tiny_model, 'sizes/Tiny')

現在檢查模型的執行情況

plotter = tfdocs.plots.HistoryPlotter(metric = 'binary_crossentropy', smoothing_std=10)
plotter.plot(size_histories)
plt.ylim([0.5, 0.7])

小型模型

為了檢查您是否可以超越小型模型的效能,請逐步訓練一些較大型的模型。

嘗試兩個隱藏層,每層 16 個單元

small_model = tf.keras.Sequential([
    # `input_shape` is only required here so that `.summary` works.
    layers.Dense(16, activation='elu', input_shape=(FEATURES,)),
    layers.Dense(16, activation='elu'),
    layers.Dense(1)
])
size_histories['Small'] = compile_and_fit(small_model, 'sizes/Small')

中型模型

現在嘗試三個隱藏層,每層 64 個單元

medium_model = tf.keras.Sequential([
    layers.Dense(64, activation='elu', input_shape=(FEATURES,)),
    layers.Dense(64, activation='elu'),
    layers.Dense(64, activation='elu'),
    layers.Dense(1)
])

並使用相同的資料訓練模型

size_histories['Medium']  = compile_and_fit(medium_model, "sizes/Medium")

大型模型

作為練習,您可以建立更大的模型,並檢查它開始過度擬合的速度。接下來,在此基準中新增一個容量更大的網路,其容量遠遠超出問題所需

large_model = tf.keras.Sequential([
    layers.Dense(512, activation='elu', input_shape=(FEATURES,)),
    layers.Dense(512, activation='elu'),
    layers.Dense(512, activation='elu'),
    layers.Dense(512, activation='elu'),
    layers.Dense(1)
])

並再次使用相同的資料訓練模型

size_histories['large'] = compile_and_fit(large_model, "sizes/large")

繪製訓練和驗證損失

實線顯示訓練損失,虛線顯示驗證損失 (請記住:驗證損失越低表示模型越好)。

雖然建構較大型的模型會賦予模型更多能力,但如果未以某種方式限制此能力,模型很容易過度擬合訓練集。

在本範例中,通常只有 "Tiny" 模型能夠完全避免過度擬合,而每個較大型的模型都更快地過度擬合資料。"large" 模型的過度擬合變得非常嚴重,您需要將圖表切換為對數刻度才能真正弄清楚發生了什麼事。

如果您繪製並比較驗證指標與訓練指標,這一點會很明顯。

  • 存在細微差異是正常的。
  • 如果這兩個指標都朝著相同的方向移動,則一切正常。
  • 如果驗證指標開始停滯不前,而訓練指標持續改進,則您可能即將過度擬合。
  • 如果驗證指標朝著錯誤的方向移動,則模型顯然過度擬合。
plotter.plot(size_histories)
a = plt.xscale('log')
plt.xlim([5, max(plt.xlim())])
plt.ylim([0.5, 0.7])
plt.xlabel("Epochs [Log Scale]")

在 TensorBoard 中檢視

這些模型都在訓練期間寫入了 TensorBoard 記錄。

在筆記本內開啟內嵌 TensorBoard 檢視器 (抱歉,這不會顯示在 tensorflow.org 上)

# Load the TensorBoard notebook extension
%load_ext tensorboard

# Open an embedded TensorBoard viewer
%tensorboard --logdir {logdir}/sizes

您可以在 TensorBoard.dev 上檢視本筆記本先前執行的結果

防止過度擬合的策略

在深入探討本節內容之前,請複製上述 "Tiny" 模型中的訓練記錄,以用作比較的基準。

shutil.rmtree(logdir/'regularizers/Tiny', ignore_errors=True)
shutil.copytree(logdir/'sizes/Tiny', logdir/'regularizers/Tiny')
regularizer_histories = {}
regularizer_histories['Tiny'] = size_histories['Tiny']

新增權重正規化

您可能熟悉奧卡姆剃刀原則:如果有兩種事物解釋,最有可能正確的解釋是「最簡單」的解釋,即假設最少的解釋。這也適用於神經網路學習的模型:給定一些訓練資料和網路架構,有多組權重值 (多個模型) 可以解釋資料,並且簡單的模型比複雜的模型更不容易過度擬合。

在此背景下,「簡單模型」是指參數值分佈的熵較少的模型 (或參數較少的模型,如上節所示)。因此,減輕過度擬合的常見方法是透過強制網路的權重僅採用小值來限制網路的複雜性,這使得權重值的分佈更「正規」。這稱為「權重正規化」,它透過將與具有大權重相關的成本新增至網路的損失函數來完成。此成本有兩種形式

  • L1 正規化,其中新增的成本與權重係數的絕對值成正比 (即與權重的「L1 範數」成正比)。

  • L2 正規化,其中新增的成本與權重係數值的平方成正比 (即與權重的平方「L2 範數」成正比)。在神經網路的背景下,L2 正規化也稱為權重衰減。請勿讓不同的名稱讓您感到困惑:權重衰減在數學上與 L2 正規化完全相同。

L1 正規化會將權重推向精確的零,鼓勵稀疏模型。L2 正規化會懲罰權重參數,而不會使其稀疏,因為對於小權重,懲罰會變為零,這是 L2 更常見的原因之一。

tf.keras 中,權重正規化是透過將權重正規化器執行個體作為關鍵字引數傳遞至層來新增的。新增 L2 權重正規化

l2_model = tf.keras.Sequential([
    layers.Dense(512, activation='elu',
                 kernel_regularizer=regularizers.l2(0.001),
                 input_shape=(FEATURES,)),
    layers.Dense(512, activation='elu',
                 kernel_regularizer=regularizers.l2(0.001)),
    layers.Dense(512, activation='elu',
                 kernel_regularizer=regularizers.l2(0.001)),
    layers.Dense(512, activation='elu',
                 kernel_regularizer=regularizers.l2(0.001)),
    layers.Dense(1)
])

regularizer_histories['l2'] = compile_and_fit(l2_model, "regularizers/l2")

l2(0.001) 表示層的權重矩陣中的每個係數都會將 0.001 * weight_coefficient_value**2 新增至網路的總損失

這就是我們直接監控 binary_crossentropy 的原因。因為它沒有混合此正規化元件。

因此,具有 L2 正規化懲罰的相同 "Large" 模型效能更好

plotter.plot(regularizer_histories)
plt.ylim([0.5, 0.7])

如上圖所示,"L2" 正規化模型現在比 "Tiny" 模型更具競爭力。此 "L2" 模型也比其基於的 "Large" 模型更不容易過度擬合,儘管參數數量相同。

更多資訊

關於此類正規化,有兩件重要事項需要注意

  1. 如果您要編寫自己的訓練迴圈,則需要確保向模型索取其正規化損失。
result = l2_model(features)
regularization_loss=tf.add_n(l2_model.losses)
  1. 此實作的工作方式是將權重懲罰新增至模型的損失,然後在此之後套用標準最佳化程序。

還有第二種方法,即僅在原始損失上執行最佳化工具,然後在套用計算的步驟時,最佳化工具也會套用一些權重衰減。此「解耦權重衰減」用於 tf.keras.optimizers.Ftrltfa.optimizers.AdamW 等最佳化工具中。

新增 Dropout

Dropout 是最有效且最常用的神經網路正規化技術之一,由 Hinton 及其在多倫多大學的學生開發。

Dropout 的直覺解釋是,由於網路中的個別節點無法依賴其他節點的輸出,因此每個節點都必須輸出本身有用的特徵。

Dropout 套用至層,包括在訓練期間隨機「捨棄」(即設為零) 層的多個輸出特徵。例如,給定層通常會針對訓練期間的給定輸入範例傳回向量 [0.2, 0.5, 1.3, 0.8, 1.1];在套用 Dropout 後,此向量將有一些隨機分佈的零項目,例如 [0, 0.5, 1.3, 0, 1.1]

「Dropout 率」是被歸零的特徵比例;通常設定在 0.2 到 0.5 之間。在測試時間,不會捨棄任何單元,而是將層的輸出值按等於 Dropout 率的係數縮小,以便平衡比訓練時間更活躍的單元這一事實。

在 Keras 中,您可以透過 tf.keras.layers.Dropout 層在網路中引入 Dropout,該層會套用至緊接在其之前的層的輸出。

將兩個 Dropout 層新增至您的網路,以檢查它們在減少過度擬合方面的效果如何

dropout_model = tf.keras.Sequential([
    layers.Dense(512, activation='elu', input_shape=(FEATURES,)),
    layers.Dropout(0.5),
    layers.Dense(512, activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(512, activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(512, activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(1)
])

regularizer_histories['dropout'] = compile_and_fit(dropout_model, "regularizers/dropout")
plotter.plot(regularizer_histories)
plt.ylim([0.5, 0.7])

從此圖表中可以清楚看出,這兩種正規化方法都改善了 "Large" 模型的行為。但這仍然無法擊敗 "Tiny" 基準。

接下來,一起嘗試這兩種方法,看看是否能做得更好。

結合 L2 + Dropout

combined_model = tf.keras.Sequential([
    layers.Dense(512, kernel_regularizer=regularizers.l2(0.0001),
                 activation='elu', input_shape=(FEATURES,)),
    layers.Dropout(0.5),
    layers.Dense(512, kernel_regularizer=regularizers.l2(0.0001),
                 activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(512, kernel_regularizer=regularizers.l2(0.0001),
                 activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(512, kernel_regularizer=regularizers.l2(0.0001),
                 activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(1)
])

regularizer_histories['combined'] = compile_and_fit(combined_model, "regularizers/combined")
plotter.plot(regularizer_histories)
plt.ylim([0.5, 0.7])

具有 "Combined" 正規化的此模型顯然是目前最好的模型。

在 TensorBoard 中檢視

這些模型也記錄了 TensorBoard 記錄。

若要開啟內嵌執行,請將以下內容執行到程式碼儲存格中 (抱歉,這不會顯示在 tensorflow.org 上)

%tensorboard --logdir {logdir}/regularizers

您可以在 TensorBoard.dev 上檢視本筆記本先前執行的結果

結論

總而言之,以下是防止神經網路過度擬合的最常見方法

  • 取得更多訓練資料。
  • 降低網路的容量。
  • 新增權重正規化。
  • 新增 Dropout。

本指南未涵蓋的兩種重要方法是

請記住,每種方法都可以單獨提供協助,但通常將它們結合起來會更有效。

# MIT License
#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.