举例说明如何编写一个自定义的Keras回调以测试深度学习模型的性能

265 阅读9分钟

简介

Keras是一个高水平的API,通常与Tensorflow库一起使用,它降低了许多人的入门门槛,使深度学习模型和系统的创建民主化。

在刚开始的时候,一个抽象了大部分内部工作的高级API可以帮助人们掌握基础知识,并建立一个开始的直觉。但随着时间的推移,从业者自然希望对引擎盖下发生的事情建立更强的直觉,以获得可操作的洞察力,并更深入地了解他们的模型如何学习。

在很多情况下,看一下深度神经网络的学习过程是很有用的,测试它在每个学习阶段是如何预测数值的,并保存这些数值。

这些保存的数值可以使用Matplotlib或Seaborn等库来可视化预测,也可以保存在日志中,以便在智能系统中进一步分析,或者直接由人分析。我们通常会提取一个模型的学习曲线,以更好地了解它在一段时间内的表现--但学习曲线反映的是一段时间内的平均损失,而在模型完成训练之前,你无法看到它的表现。

Keras有一个很好的功能--回调,这是一些在训练过程中被调用的代码片段,可以用来定制训练过程。通常情况下,如果模型表现良好,你会使用回调来保存模型,如果模型过拟合,则停止训练,或者以其他方式对学习过程中的步骤做出反应或影响。

这使得回调成为在每个批次或历时中运行预测并保存结果的自然选择,在本指南中,我们将看看如何在Keras的每个训练历时中在测试集上运行预测,可视化结果,并将其保存为图像。

**注意:**在接下来的章节中,我们将使用Keras建立一个简单的深度学习模型,但不会过多地关注实现、数据集以及其准确性。这并不意味着这是一个建立回归模型的指南,但需要一个模型来正确展示回调的工作原理。

用Keras构建和评估一个深度学习模型

让我们建立一个简单的Keras模型来说明问题。我们将以最小的注意力和关注度来加速完成这一部分--这不是一个关于建立回归模型的指南。我们将使用加利福尼亚住房数据集,该数据集通过Scikit-Learn的datasets 模块获得,是一个用于回归的数据集。

让我们继续,导入我们将使用的库和静态方法。

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split

现在,让我们加载数据集,把它分成训练集和测试集(我们稍后会分出一个验证集),并把房屋的位置可视化,以检查数据是否被正确加载。

X, y = fetch_california_housing(as_frame=True, return_X_y=True)
x_train, x_test, y_train, y_test = train_test_split(x, y)

plt.figure(figsize=(12, 8))
sns.scatterplot(data=x, x='Longitude', y='Latitude', size=y, alpha=0.5, hue=y, palette='magma')
plt.show()

california dataset visualization

看起来像加利福尼亚由于数据被正确加载,我们可以定义一个简单的顺序Keras模型。

checkpoint = keras.callbacks.ModelCheckpoint("california.h5", save_best_only=True)

model = keras.Sequential([
    keras.layers.Dense(64, activation='relu', kernel_initializer='normal', kernel_regularizer="l2", input_shape=[x_train.shape[1]]),
    keras.layers.Dropout(0.2),
    keras.layers.BatchNormalization(),
    
    keras.layers.Dense(64, activation='relu', kernel_initializer='normal', kernel_regularizer="l2"),
    keras.layers.Dropout(0.2),
    keras.layers.BatchNormalization(),
  
    keras.layers.Dense(1)
])

model.compile(loss='mae',
              optimizer=keras.optimizers.RMSprop(learning_rate=1e-2, decay=0.1),
              metrics=['mae'])
              
history = model.fit(
    x_train, y_train,
    epochs=150,
    batch_size=64,
    validation_split=0.2,
    callbacks=[checkpoint]
)

在这里,我们有一个简单的MLP,用一点Dropout和Batch Normalization来对抗过度拟合,用RMSprop优化器和平均绝对误差损失来优化。我们对模型进行了150个历时的拟合,验证分割为0.2 ,并通过ModelCheckpoint 回调将权重保存在一个文件中。运行这个结果是。

...
Epoch 150/150
387/387 [==============================] - 3s 7ms/step - loss: 0.6279 - mae: 0.5976 - val_loss: 0.6346 - val_mae: 0.6042

我们可以通过可视化的学习曲线来了解训练的基本情况,但这并没有告诉我们整个故事--这些只是训练期间训练集和验证集的总平均值。

model_history = pd.DataFrame(history.history)
model_history['epoch'] = history.epoch

fig, ax = plt.subplots(1, figsize=(8,6))
num_epochs = model_history.shape[0]

ax.plot(np.arange(0, num_epochs), model_history["mae"], 
        label="Training MAE")
ax.plot(np.arange(0, num_epochs), model_history["val_mae"], 
        label="Validation MAE")
ax.legend()

plt.tight_layout()
plt.show()

这就导致了。

deep learning model learning curves

我们可以用以下方法评估我们的模型。

model.evaluate(x_test, y_test)
162/162 [==============================] - 0s 2ms/step - loss: 0.5695 - mae: 0.5451 - mape: 32.2959

由于目标变量是以100,000美元的倍数来衡量的,这意味着我们的网络最多错过了54,000美元的价格,这是一个平均绝对百分比误差~32%,这并不是很好。这里的重点不是建立一个特别准确的模型,但我们确实选择了一个模型不会很快收敛的数据集,所以我们可以观察它在目标变量周围的舞动。

一个更说明问题的方法是评估模型的工作情况,完全抛弃了总的平均绝对误差平均绝对百分比误差,我们可以绘制预测价格实际价格散点图。如果它们是相等的--绘制的标记将沿着对角线的直线轨迹。作为参考和范围--我们也可以绘制一条对角线,并评估每个标记离这条线有多近。

test_predictions = model.predict(x_test)
test_labels = y_test

fig, ax = plt.subplots(figsize=(8,4))
plt.scatter(test_labels, test_predictions, alpha=0.6, 
            color='#FF0000', lw=1, ec='black')
lims = [0, 5]

plt.plot(lims, lims, lw=1, color='#0000FF')
plt.ticklabel_format(useOffset=False, style='plain')
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
plt.xlim(lims)
plt.ylim(lims)

plt.tight_layout()
plt.show()

运行这段代码的结果是。

deep learning regression performance evaluation

网络对较便宜的房子定价过高,对较贵的房子定价过低--而且估计的范围相当宽松(右边的一些预测完全超出了范围)。这不是你从学习曲线中得到的洞察力,一个具有相反效果的网络--对较便宜的房子定价过低,对昂贵的房子定价过高,可能具有相同的MAE和MAPE,但行为完全不同。

我们还感兴趣的是,模型是如何到达这里的,以及这些预测是如何通过时间和学习过程改变的。这只是训练过程的终点,为了达到这个目的,我们进行了相当多的训练。

让我们继续写一个自定义回调,添加到训练过程中的回调列表中,它将在每个epoch上对测试集进行预测,将预测结果可视化,并将其保存为图片。

自定义预测的Keras回调与图谱

就像我们用ModelCheckpoint 回调来检查模型在每个历时中是否处于最佳表现状态,并将其保存到.h5 文件中并持久化一样--我们可以写一个自定义回调,来运行预测,将其可视化,并将图像保存到我们的磁盘上。

创建一个自定义的回调,归根结底是扩展Callback 类,并重写它所提供的任何方法--你重写的方法,保留其默认行为。

class PerformancePlotCallback(keras.callbacks.Callback):
       
    def on_train_end(self, logs=None):
      ...
    def on_epoch_begin(self, epoch, logs=None):
      ...
    def on_epoch_end(self, epoch, logs=None):
      ...
    def on_test_begin(self, logs=None):
      ...
    def on_test_end(self, logs=None):
      ...
    # Etc.

根据你想使用你的训练中模型进行预测的时间,你将选择适当的方法。衡量它的进展情况的一个很好的标准是epoch,所以在每个训练epoch结束时,我们将在测试集上测试模型。

我们需要一种方法来向回调提供测试集,因为这是外部数据。最简单的方法是定义一个构造函数,接受测试集并在其上评估当前模型,给你一个一致的结果。

class PerformancePlotCallback(keras.callbacks.Callback):
    def __init__(self, x_test, y_test):
        self.x_test = x_test
        self.y_test = y_test
        
    def on_epoch_end(self, epoch, logs=None):
        print('Evaluating Model...')
        print('Model Evaluation: ', self.model.evaluate(self.x_test))   

这个简单的回调接受测试集的房屋和相关的目标变量,并在每个周期评估自己,将结果打印到控制台,就在通常的Keras输出旁边。

如果我们将这个回调实例化并添加到模型中,并再次fit() ,我们会看到一个与之前不同的结果。

performance_simple = PerformancePlotCallback(x_test, y_test)

# Model definition and compilation...

history = model.fit(
    x_train, y_train,
    epochs=150,
    validation_split=0.2,
    callbacks=[performance_simple]
)

这个结果是

Epoch 1/150
387/387 [==============================] - 3s 7ms/step - loss: 1.0785 - mae: 1.0140 - val_loss: 0.9455 - val_mae: 0.8927
Evaluating Model...
162/162 [==============================] - 0s 1ms/step - loss: 0.0528 - mae: 0.0000e+00
Model Evaluation:  [0.05277165770530701, 0.0]
Epoch 2/150
387/387 [==============================] - 3s 7ms/step - loss: 0.9048 - mae: 0.8553 - val_loss: 0.8547 - val_mae: 0.8077
Evaluating Model...
162/162 [==============================] - 0s 1ms/step - loss: 0.0471 - mae: 0.0000e+00
Model Evaluation:  [0.04705655574798584, 0.0]
...

棒极了!该模型在每个历时中,根据我们传递给回调的数据进行自我评估。现在,让我们修改回调,让它把预测结果可视化,而不是把它们打印到已经很乱的输出中。

为了简化事情,我们将让回调把图片保存到一个文件夹里,这样我们以后就可以把它们拼接成一个视频或Gif。我们还将在构造函数中包含一个model_name ,以帮助我们在生成图像和它们的文件名时区分模型。

class PerformancePlotCallback(keras.callbacks.Callback):
    def __init__(self, x_test, y_test, model_name):
        self.x_test = x_test
        self.y_test = y_test
        self.model_name = model_name
        
    def on_epoch_end(self, epoch, logs={}):
        y_pred = self.model.predict(self.x_test)
        fig, ax = plt.subplots(figsize=(8,4))
        plt.scatter(y_test, y_pred, alpha=0.6, 
            color='#FF0000', lw=1, ec='black')
        
        lims = [0, 5]

        plt.plot(lims, lims, lw=1, color='#0000FF')
        plt.ticklabel_format(useOffset=False, style='plain')
        plt.xticks(fontsize=18)
        plt.yticks(fontsize=18)
        plt.xlim(lims)
        plt.ylim(lims)

        plt.tight_layout()
        plt.title(f'Prediction Visualization Keras Callback - Epoch: {epoch}')
        plt.savefig('model_train_images/'+self.model_name+"_"+str(epoch))
        plt.close()

在这里,我们在每个纪元上创建一个Matplotlib图,并绘制一个预测价格与实际价格的散点图。此外,我们还添加了一条对角线参考线--我们的散点图标记越接近对角线,我们模型的预测就越准确。

然后,该图被通过 plt.savefig()保存了模型的名称和纪元编号,同时还有一个信息性的标题,让你知道模型在训练过程中处于哪个纪元。

现在,让我们再次使用这个自定义回调,除了x_testy_test 集外,还提供一个模型名称。

checkpoint = keras.callbacks.ModelCheckpoint("california.h5", save_best_only=True)
performance = PerformancePlotCallback(x_test, y_test, "california_model")

# Model definition and compilation...
              
history = model.fit(
    x_train, y_train,
    epochs=150,
    validation_split=0.2,
    callbacks=[checkpoint, performance]
)

PerformancePlotCallback 全力以赴,在指定的文件夹中生成了每个epoch上的性能图像。现在model_train_images 文件夹里有150张图。

saving predictions on each epoch keras

你现在可以用你最喜欢的工具将这些图像拼接成一个视频或Gif文件,或者干脆手动浏览它们。下面是我们在这些数据上建立的模型的Gif。

animated learning process on each epoch with keras

结论

在本指南中,我们建立了一个简单的模型来预测加州住房数据集中的房屋价格,准确率还不错。然后,我们看了一下如何编写一个自定义的Keras回调,以测试深度学习模型的性能,并在训练期间的每个epoch上将其可视化。

我们接着把这些图片保存到磁盘上,并从中创建了一个Gif,让我们对训练过程有了不同的看法,而不是从分析模型的学习曲线中得到的看法。