【tf.keras】basic 04: Overfit and Underfit

前面的兩個示例(對文本進行分類和預測燃油效率)中,驗證集上的準確率會在訓練了多個epoch後達到峯值,然後停滯或開始下降;損失先降低到最小,然後開始有上升的趨勢。對於這種情況,可以認爲是出現了過擬合,過擬合是模型訓練中經常遇到的問題,關乎模型的泛化能力。因此,學習如何應對過擬合非常重要。與過擬合相對應的是欠擬合。當測試數據仍有改進空間時,就會發生欠擬合。發生這種情況的原因有很多:如果模型不夠強大;模型過於規範化;訓練的epoch過少等等。這意味着網絡尚未學習訓練數據中的相關模式。

但如果訓練的epoch過多,模型將開始過擬合,從而導致泛化能力變差,在新數據上的表現不理想。上一篇文章中提到了通過設置回調,來及時停止訓練來抑制過擬合,但是這種方式是顯式的限制,沒有考慮模型容量的問題,這就需要結合正則化。通過正則化可以限制模型可存儲信息的數量和類型。即加入懲罰項(拉格朗日乘子),對假設空間中的高次項進行限制。

本文將介紹幾種正則化技術,來對分類模型進行改進。



代碼環境:

python version: 3.7.6 
tensorflow version: 2.1.0

導入必要的包:

import tensorflow as tf

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

from  IPython import display
from matplotlib import pyplot as plt
import numpy as np
import pathlib
import shutil
import tempfile

1. 數據預處理

2.1 加載數據集

UCI Higgs Dataset(粒子物理學)

Abstract: This is a classification problem to distinguish between a signal process which produces Higgs bosons and a background process which does not.

該數據集包含11000000個樣本,每個樣本具有28個特徵以及一個二進制類標籤。數據集獲取:點擊此處

注意:數據集比較大(2.6G),下載比較慢,需要下載一段時間。


1.下載數據集:

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

注意:建議手動下載,然後放到 C:\Users\34123\.keras\datasets\ 路徑下,執行以上代碼會自動讀取。

2.定義特徵數量:

FEATURES = 28

3.加載數據集:

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

說明:tf.data.experimental.CsvDataset 類可以從未解壓的gzip文件中讀取其中的CSV文件。該csv 閱讀器類返回每個記錄的標量列表。


4.將標量列表重構爲(feature_vector,label)元組。

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

當處理大量數據時,TensorFlow效率最高。因此,創建一個新的Dataset,實現批次處理。每批次10000個樣本,將pack_row函數應用於每個批次,然後將批次拆分回單獨的記錄:

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

5.查看經過批次處理後的數據情況:

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

輸出:

tf.Tensor(
[ 0.8692932  -0.6350818   0.22569026  0.32747006 -0.6899932   0.75420225
 -0.24857314 -1.0920639   0.          1.3749921  -0.6536742   0.9303491
  1.1074361   1.1389043  -1.5781983  -1.0469854   0.          0.65792954
 -0.01045457 -0.04576717  3.1019614   1.35376     0.9795631   0.97807616
  0.92000484  0.72165745  0.98875093  0.87667835], shape=(28,), dtype=float32)

在這裏插入圖片描述


1.2 訓練集驗證集劃分

本文示例僅使用數據集中前1000個樣本進行驗證,使用10000個樣本進行訓練:

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

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

說明: Dataset.cache 方法緩存數據集中的元素。第一次迭代數據集時,其元素將緩存在指定文件或內存中,隨後的迭代將使用緩存的數據。避免在每個epoch重新從文件中讀取數據。

train_ds

輸出:

<CacheDataset shapes: ((28,), ()), types: (tf.float32, tf.float32)>

由此可知,上面三種方法實現了類似迭代器的功能,每次返回一個樣本。可以使用 .batch 方法實現創建指定大小的批次進行訓練。批處理之前,還記得對數據集進行 .shuffle.repeat

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

2. 過擬合與欠擬合

防止過度擬合的最簡單方法是從一個小的模型開始:一個具有少量可學習參數(由層數和每層單位數確定)的模型。在深度學習中,模型中可學習參數的數量通常稱爲模型的“容量”。

直觀地講,具有更多參數的模型將具有更多的“記憶能力”,因此將能夠輕鬆地學習訓練樣本與其目標之間的映射關係,但不可忽略的問題是:很容易導致過擬合。過擬合的模型泛化能力很差,基本上無法在新數據上使用。深度學習模型很擅長擬合訓練數據,因此真正的挑戰是泛化問題。

另一方面,如果模型的“容量”有限,則無法輕易地學習映射。爲了最大程度地減少損失,它必須學習具有更強預測能力的壓縮表示形式。在“容量太大”和“容量不足”之間找到一個平衡是需要重點做的工作。

然而,沒有顯示解來確定模型的容量或體系結構。因此,在實際應用過程中,需要嘗試使用一系列不同的體系結構,從而找到最適合業務需求的一種模型。爲了找到合適的模型大小,最好從相對較少的層和參數開始,然後開始增加層的大小或添加新層,直到看到驗證損失不再遞減爲止。


3. 建模

首先創建簡單的模型,然後逐漸創建比較複雜的模型,一一比較。

3.1 定義優化器

在訓練過程中逐漸降低學習速度,許多模型的訓練效果會更好。使用 optimizers.schedules.InverseTimeDecay 方法實現隨着時間推移降低學習率:

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)

繪製曲線查看:

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

在這裏插入圖片描述


3.2 自定義回調函數

下文的每個模型都將使用相同的訓練配置。因此,從回調列表開始,以可重用的方式設置它們。

爲了減少輸出,自定義回調函數,實現以下功能:

  • 每100個epoch打印一次指標。
  • 使用 callbacks.EarlyStopping 方法及時停止訓練,減少過擬合風險。注意,此回調設置爲監視指標爲 val_binary_crossentropy,而不是 val_loss
  • 使用 callbacks.TensorBoard 生成TensorBoard日誌。

定義日誌文件存放路徑:

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

定義回調函數:

# 因爲訓練時間短並且訓練週期長,因此全輸出是沒必要的,以下代碼實現每100個 epoch 打印摘要,其他epoch打印點;
class EpochDots(tf.keras.callbacks.Callback):
    def __init__(self, report_every=100, dot_every=1):
        self.report_every = report_every
        self.dot_every = dot_every

    def on_epoch_end(self, epoch, logs):
        if epoch % self.report_every == 0:
            print()
            print('Epoch: {:d}, '.format(epoch), end='')
            for name, value in sorted(logs.items()):
                print('{}:{:0.4f}'.format(name, value), end=',  ')
            print()

        if epoch % self.dot_every == 0:
            print('.', end='')

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

3.3 模型編譯和訓練配置

每個模型都使用相同 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.losses.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

3.4 模型定義與訓練

定義繪製損失和準確率曲線函數:

import matplotlib.pyplot as plt
import numpy as np

prop_cycle = plt.rcParams['axes.prop_cycle']
COLOR_CYCLE = prop_cycle.by_key()['color']


def _smooth(values, std):
    width = std * 4
    x = np.linspace(-width, width, 2 * width + 1)
    kernel = np.exp(-(x / 5)**2)

    values = np.array(values)
    weights = np.ones_like(values)

    smoothed_values = np.convolve(values, kernel, mode='same')
    smoothed_weights = np.convolve(weights, kernel, mode='same')

    return smoothed_values / smoothed_weights


class HistoryPlotter(object):
    def __init__(self, metric=None, smoothing_std=None):
        self.color_table = {}
        self.metric = metric
        self.smoothing_std = smoothing_std

    def plot(self, histories, metric=None, smoothing_std=None):
        if metric is None:
            metric = self.metric
        if smoothing_std is None:
            smoothing_std = self.smoothing_std

        plt.figure(dpi=200)
        for name, history in histories.items():
            # Remember name->color asociations.
            if name in self.color_table:
                color = self.color_table[name]
            else:
                color = COLOR_CYCLE[len(self.color_table) % len(COLOR_CYCLE)]
                self.color_table[name] = color

            train_value = history.history[metric]
            val_value = history.history['val_' + metric]
            
            if smoothing_std is not None:
                train_value = _smooth(train_value, std=smoothing_std)
                val_value = _smooth(val_value, std=smoothing_std)

            plt.plot(
                history.epoch,
                train_value,
                color=color,
                label=name.title() + ' Train')
            
            plt.plot(
                history.epoch,
                val_value,
                '--',
                label=name.title() + ' Val',
                color=color)

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

        plt.xlim(
            [0, max([history.epoch[-1] for name, history in histories.items()])])
        
        plt.grid(True)

1.Tiny 模型:

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')

輸出:
在這裏插入圖片描述


2.Small 模型

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

size_histories['Small'] = compile_and_fit(small_model, 'sizes/Small')

3.medium 模型

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")

4.large 模型

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")


3.5 繪製訓練和驗證損失

實線表示訓練損失,而虛線表示驗證損失(驗證損失越小表示模型越好)。雖然構建較大的模型“容量”更大,但如果不加以限制,很容易導致過擬合。上例中,只有"Tiny"模型沒有過擬合,而每個較大的模型都會更快地過擬合。這對於"large"模型來說更嚴重。
在這裏插入圖片描述

將驗證指標與訓練指標進行比較,可以說明上述問題。注意:

  • 差異很小是正常的。
  • 如果兩個指標都朝着同一方向發展,表示模型學習的很好。
  • 如果訓練指標繼續提高,驗證指標停滯不前,則可能已經過擬合了。
  • 如果驗證指標不降反增,則表明該模型過擬合。

4.抑制過擬合策略

複製上述"Tiny"模型中的訓練日誌,用作比較的基準。

regularizer_histories = {}
regularizer_histories['Tiny'] = size_histories['Tiny']

4.1 L1 & L2 正則化

由奧卡姆剃刀原理可知:如無必要,勿增實體。即較簡單的模型比複雜的模型不太可能過擬合。

在這種情況下,“簡單模型”是參數分佈具有較小熵的模型(或如上節所述,具有總共較少參數的模型)。因此,減輕過擬合的一種通用方法是將網絡的權重強制取小的值來對網絡的複雜性施加約束,這使得權重值的分佈更加“規則”。這稱爲“權重調整”,它是通過向網絡的損失函數中添加與權重較大相關的成本(cost)來完成的。主要由兩種方法:

L1 正則化 (L1 regularization)
根據權重的絕對值的總和來懲罰權重。在依賴稀疏特徵的模型中,L1 正則化有助於使不相關或幾乎不相關的特徵的權重正好爲 0,從而將這些特徵從模型中移除。與 L2 正則化相對。

L2 正則化 (L2 regularization)
根據權重的平方和來懲罰權重。L2 正則化有助於使離羣值(具有較大正值或較小負值)權重接近於 0,但又不正好爲 0。(與 L1 正則化相對。)在線性模型中,L2 正則化始終可以改進泛化。


在large模型的基礎上添加正則化構造正則化模型:

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 爲指標的原因。因爲它沒有正則化成分。

繪圖:

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

在這裏插入圖片描述
可以看出,“L2”正則化模型現在比“Tiny”模型更具競爭力。這個“L2”模型也比它所基於的“large”模型更能抑制過度擬合,儘管參數數量相同。

這種正則化有兩點要注意:

  • 如果是自定義的訓練循環,則需要確保向模型詢問其正則化損失。
  • 將權重損失添加到模型的損失中,然後在此之後應用標準優化過程。

還有第二種方法,它只對原始損耗運行優化器,然後在應用計算出的步驟時,優化器還會應用一些權重衰減。


4.2 Dropout

Dropout是Hinton和他在多倫多大學的學生開發的最有效,最常用的神經網絡正則化技術之一。
Dropout的直觀解釋是,由於網絡中的各個節點不能依賴於其他節點的輸出,因此每個節點都必須輸出自己有用的特徵。Dropout應用於layer的過程其實是在訓練過程中隨機“丟棄”(即設置爲零)該layer的許多輸出特徵。

假設在訓練過程中,給定的圖層通常會爲給定的輸入樣本返回向量[0.2、0.5、1.3、0.8、1.1];應用Dropout後,此向量將有一些元素被隨機置零,例如[0,0.5,1.3,0,1.1]。 “dropout rate”是被置零的特徵的分數;通常設置在0.2到0.5之間。在測試時,不會丟失任何單元,而是將layers的輸出值按dropout rate進行縮小,以減少訓練時被激活神經元。

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")

輸出:
在這裏插入圖片描述

從該圖中可以看出,這兩種正則化方法都可以改善"Large"模型。但沒有超過"Tiny"模型的性能基線。


4.3 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])

輸出:
在這裏插入圖片描述


Summary

抑制神經網絡過擬合的最常見方法:

  • 擴充數據集。
  • 減少網絡容量(批量標準化)。
  • L1 L2 正則化。
  • 增加 dropout。

注意:通常將以上措施組合起來會更加有效。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章