深度学习入门四----过拟合与欠拟合


回想一下上一篇文章中的示例,Keras 能保留它训练模型过程中训练和验证损失的历史记录,并且可以画出来。在本文中,我们将学习如何解释这些学习曲线以及如何使用它们来指导模型开发。特别是,我们将在学习曲线上寻找模型欠拟合(underfittiing)或者过拟合(overfitting)的证据,并查看几种防止过拟合或者欠拟合的策略。

Interpreting the Learning Curves

我们可能认为训练数据中的信息有两种:信号(signal)和噪声(noise)。信号是概括的部分,即可以帮助我们的模型根据新数据进行预测的部分。噪声是仅适用于训练数据的那部分;噪声是来自现实世界中的数据的所有随机波动,或者是所有无法实际帮助模型做出预测的偶然的、非信息性的模式。噪音是部分可能看起来有用但实际上不是。 我们通过选择最小化训练集损失的权重或参数来训练模型。但是,要准确评估模型的性能,我们需要在一组新数据(验证数据)上对其进行评估。 当我们训练模型时,我们一直在逐个epoch绘制训练集时期的损失。为此,我们也将添加一条绘制验证数据损失的曲线。这些图我们称之为学习曲线。为了有效地训练深度学习模型,我们需要能够解释它们。

现在,当模型学习信号或学习噪声时,训练损失会下降。但是只有当模型学习到信号时,验证损失才会下降。 (模型从训练集中学到的任何噪声都不会推广到新数据。)因此,当模型学习到信号时,两条曲线都会下降,但是当它学习到噪声时,曲线中就会产生间隙。差距的大小告诉会告诉你模型学习了多少噪声。 理想情况下,我们将创建学习所有信号而不学习噪声的模型。这实际上永远不会发生。事实上,我们会做出权衡。我们可以让模型以学习更多噪声为代价来学习更多信号。只要这种让步对我们有利,验证损失就会继续减少。然而,在某一点之后,这种让步可能对我们不利,成本超过收益,验证损失开始上升。

这种权衡表明在训练模型时可能会出现两个问题:信号不足或噪声过多。训练集欠拟合(Underfitting)是指由于模型没有学习到足够的信号导致损失没有达到应有的水平。过拟合(Overfitting)是指由于模型学习了太多噪声造成损失没有达到应有的水平。训练深度学习模型的诀窍是在两者之间找到最佳平衡。 总之,在构建机器学习算法时,我们会利用样本数据集来训练模型。但是,当模型在样本数据上训练时间过长或模型过于复杂时,模型就会开始学习数据集中的“噪声”或不相关信息。当模型记住噪声并且与训练集过于接近时,模型就会变得“过拟合”,并且无法很好地泛化到新数据。如果模型不能很好地泛化到新数据,那么它将无法执行其预期的分类或预测任务。 我们将研究从训练数据中获取更多信号同时减少噪声量的几种方法。

Capacity

模型的capacity是指它能够学习的模式的大小和复杂性。对于神经网络,这在很大程度上取决于它有多少神经元以及它们如何连接在一起。 我们可以通过增加模型的宽度(向现有层增加更多单元)或增加模型的深度(添加更多层)来增加神经网络模型的capacity。越宽的网络越容易学习到更多的线性关系,而越深的网络更倾向于学习非线性关系。哪个更好只取决于数据集。

model = keras.Sequential([
    layers.Dense(16, activation='relu'),
    layers.Dense(1),
])

wider = keras.Sequential([
    layers.Dense(32, activation='relu'),
    layers.Dense(1),
])

deeper = keras.Sequential([
    layers.Dense(16, activation='relu'),
    layers.Dense(16, activation='relu'),
    layers.Dense(1),
])

我们将在后边的练习中探索网络容量如何影响其性能。

Early Stopping

我们提到当模型过于急切地学习噪声时,在训练期间验证损失(validation loss)可能会开始增加。为了防止这种情况,我们可以在验证损失(validation loss)不再减少时停止训练。以这种方式中断训练称为early stopping。

一旦我们检测到验证损失开始再次上升,我们就可以将权重重置为最小值出现的位置。这可确保模型不会继续学习噪声和过度拟合数据。 提前停止训练也意味着我们不太可能在网络完成学习信号之前过早停止训练。所以除了防止过拟合训练时间过长之外,提前停止还可以防止欠拟合训练时间不够长。只需将您的训练时期设置为一个较大的数字(比您需要的多),早期停止将处理其余部分。

Adding Early Stopping

在 Keras 中,我们利用回调函数(callback)将early stopping包含在我们的训练过程中。回调函数只是您希望在网络训练时经常运行的函数。early stopping callback 将在每个 epoch 之后运行。 (Keras 预定义了各种有用的回调,但您也可以定义自己的回调。)

from tensorflow.keras.callbacks import EarlyStopping

early_stopping = EarlyStopping(
    min_delta=0.001, # minimium amount of change to count as an improvement
    patience=20, # how many epochs to wait before stopping
    restore_best_weights=True,
)

这些参数表示:“如果在 20 个epoch中,验证损失中没有哪怕是 0.001 的改进,那么停止训练并保留找到的最佳模型。”有时很难判断验证损失是由于过度拟合还是仅仅由于随机批次变化而上升。这些参数允许我们设置一些关于何时停止的余量。 我们继续根据前边教程中的示例来开发模型。我们将增加该网络的容量,但也会添加一个提前停止的回调以防止过度拟合。

# 准备数据
import pandas as pd
from IPython.display import display

# 加载红酒数据集
red_wine = pd.read_csv('datasets/red-wine.csv')

# Create training and validation splits
df_train = red_wine.sample(frac=0.7, random_state=0)
df_valid = red_wine.drop(df_train.index)
display(df_train.head(4))

# Scale to [0, 1]
max_ = df_train.max(axis=0)
min_ = df_train.min(axis=0)
df_train = (df_train - min_) / (max_ - min_)
df_valid = (df_valid - min_) / (max_ - min_)

# Split features and target
X_train = df_train.drop('quality', axis=1)
X_valid = df_valid.drop('quality', axis=1)
y_train = df_train['quality']
y_valid = df_valid['quality']

现在,我们要增加网络的容量。我们将使用一个相当大的网络,一旦验证损失显示出增加的迹象,就依靠回调来停止训练。

from tensorflow import keras
from tensorflow.keras import layers, callbacks

early_stopping = callbacks.EarlyStopping(
    min_delta=0.001, # minimium amount of change to count as an improvement
    patience=20, # how many epochs to wait before stopping
    restore_best_weights=True,
)

model = keras.Sequential([
    layers.Dense(512, activation='relu', input_shape=[11]),
    layers.Dense(512, activation='relu'),
    layers.Dense(512, activation='relu'),
    layers.Dense(1),
])
model.compile(
    optimizer='adam',
    loss='mae',
)

定义回调后,为fit函数添加合适的参数(可以有多个,因此将其放入列表中)。使用提前停止时我们会设置比较多的 epoch个数,比你需要的多。

history = model.fit(
    X_train, y_train,
    validation_data=(X_valid, y_valid),
    batch_size=256,
    epochs=500,
    callbacks=[early_stopping], # put your callbacks in a list
    verbose=0,  # turn off training log
)

history_df = pd.DataFrame(history.history)
history_df.loc[:, ['loss', 'val_loss']].plot();
print("Minimum validation loss: {}".format(history_df['val_loss'].min()))

练习

在本练习中,您将学习如何通过包含提前停止回调以防止过度拟合来改善训练结果。 我们的任务是根据各种音频特征(如“节奏”(tempo)、“可舞性”(danceability)和“模式”(mode))预测歌曲的流行度。

import pandas as pd
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import make_column_transformer
from sklearn.model_selection import GroupShuffleSplit

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

spotify = pd.read_csv('datasets/spotify.csv')

X = spotify.copy().dropna()
y = X.pop('track_popularity')
artists = X['track_artist']

features_num = ['danceability', 'energy', 'key', 'loudness', 'mode',
                'speechiness', 'acousticness', 'instrumentalness',
                'liveness', 'valence', 'tempo', 'duration_ms']
features_cat = ['playlist_genre']

preprocessor = make_column_transformer(
    (StandardScaler(), features_num),
    (OneHotEncoder(), features_cat),
)

# We'll do a "grouped" split to keep all of an artist's songs in one
# split or the other. This is to help prevent signal leakage.
def group_split(X, y, group, train_size=0.75):
    splitter = GroupShuffleSplit(train_size=train_size)
    train, test = next(splitter.split(X, y, groups=group))
    return (X.iloc[train], X.iloc[test], y.iloc[train], y.iloc[test])

X_train, X_valid, y_train, y_valid = group_split(X, y, artists)

X_train = preprocessor.fit_transform(X_train)
X_valid = preprocessor.transform(X_valid)
y_train = y_train / 100 # popularity is on a scale 0-100, so this rescales to 0-1.
y_valid = y_valid / 100

input_shape = [X_train.shape[1]]
print("Input shape: {}".format(input_shape))

让我们从最简单的网络开始,一个线性模型。

model = keras.Sequential([
    layers.Dense(1, input_shape=input_shape),
])
model.compile(
    optimizer='adam',
    loss='mae',
)
history = model.fit(
    X_train, y_train,
    validation_data=(X_valid, y_valid),
    batch_size=512,
    epochs=50,
    verbose=0, # suppress output since we'll plot the curves
)
history_df = pd.DataFrame(history.history)
history_df.loc[0:, ['loss', 'val_loss']].plot()
print("Minimum Validation Loss: {:0.4f}".format(history_df['val_loss'].min()));

输出如下: 曲线遵循“曲棍球棒”图案的情况并不少见,就像您在此处看到的那样。这使得训练的最后部分很难看到,所以让我们从第 10 个epoch开始:

# Start the plot at epoch 10
history_df.loc[10:, ['loss', 'val_loss']].plot()
print("Minimum Validation Loss: {:0.4f}".format(history_df['val_loss'].min()));

你觉得这个模型怎么样,是欠拟合,过拟合,刚刚好? 这些曲线之间的差距非常小,验证损失永远不会增加,因此网络欠拟合而不是过拟合的可能性更大。值得尝试更大容量的模型,看看是否是这种情况。 现在让我们为我们的网络添加一些容量。我们将添加三个隐藏层,每个隐藏层有 128 个单元。运行下一个单元来训练网络并查看学习曲线。

model = keras.Sequential([
    layers.Dense(128, activation='relu', input_shape=input_shape),
    layers.Dense(64, activation='relu'),
    layers.Dense(1)
])
model.compile(
    optimizer='adam',
    loss='mae',
)
history = model.fit(
    X_train, y_train,
    validation_data=(X_valid, y_valid),
    batch_size=512,
    epochs=50,
)
history_df = pd.DataFrame(history.history)
history_df.loc[:, ['loss', 'val_loss']].plot()
print("Minimum Validation Loss: {:0.4f}".format(history_df['val_loss'].min()));

部分输出如下:

您如何评价这些曲线?欠拟合,过拟合,刚刚好? 现在验证损失很早就开始上升,而训练损失继续下降。这表明网络已经开始过拟合。在这一点上,我们需要尝试一些方法来防止它,或者通过减少单元数量或通过提前停止等方法。 现在定义一个提前停止回调,用 5 个 epochs(耐心)来等待至少 0.001 (min_delta) 的验证损失变化,并保持具有最佳损失的权重 (restore_best_weights)。

from tensorflow.keras import callbacks
from tensorflow.keras.callbacks import EarlyStopping
# YOUR CODE HERE: define an early stopping callback
early_stopping = EarlyStopping(min_delta=0.001,
                              patience=5,
                              restore_best_weights=True)

现在运行这个单元来训练模型并获得学习曲线。请注意 model.fit 中的回调(callbacks)参数。

model = keras.Sequential([
    layers.Dense(128, activation='relu', input_shape=input_shape),
    layers.Dense(64, activation='relu'),    
    layers.Dense(1)
])
model.compile(
    optimizer='adam',
    loss='mae',
)
history = model.fit(
    X_train, y_train,
    validation_data=(X_valid, y_valid),
    batch_size=512,
    epochs=50,
    callbacks=[early_stopping]
)
history_df = pd.DataFrame(history.history)
history_df.loc[:, ['loss', 'val_loss']].plot()
print("Minimum Validation Loss: {:0.4f}".format(history_df['val_loss'].min()));

与没有提前停止的训练相比,这是一种改进吗? 一旦网络开始过度拟合,early stopping callback确实停止了训练。此外,通过包含 restore_best_weights 我们仍然可以保持模型的验证损失最低。