Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(四)(2)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(四)

Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(四)(1)https://developer.aliyun.com/article/1482417


使用子类 API 构建动态模型

顺序 API 和功能 API 都是声明式的:您首先声明要使用哪些层以及它们应该如何连接,然后才能开始向模型提供一些数据进行训练或推断。这有许多优点:模型可以很容易地被保存、克隆和共享;其结构可以被显示和分析;框架可以推断形状并检查类型,因此可以在任何数据通过模型之前尽早捕获错误。调试也相当简单,因为整个模型是一组静态图层。但是反过来也是如此:它是静态的。一些模型涉及循环、变化的形状、条件分支和其他动态行为。对于这种情况,或者如果您更喜欢更具有命令式编程风格,子类 API 适合您。

使用这种方法,您可以对Model类进行子类化,在构造函数中创建所需的层,并在call()方法中使用它们执行您想要的计算。例如,创建以下WideAndDeepModel类的实例会给我们一个与我们刚刚使用功能 API 构建的模型等效的模型:

class WideAndDeepModel(tf.keras.Model):
    def __init__(self, units=30, activation="relu", **kwargs):
        super().__init__(**kwargs)  # needed to support naming the model
        self.norm_layer_wide = tf.keras.layers.Normalization()
        self.norm_layer_deep = tf.keras.layers.Normalization()
        self.hidden1 = tf.keras.layers.Dense(units, activation=activation)
        self.hidden2 = tf.keras.layers.Dense(units, activation=activation)
        self.main_output = tf.keras.layers.Dense(1)
        self.aux_output = tf.keras.layers.Dense(1)
    def call(self, inputs):
        input_wide, input_deep = inputs
        norm_wide = self.norm_layer_wide(input_wide)
        norm_deep = self.norm_layer_deep(input_deep)
        hidden1 = self.hidden1(norm_deep)
        hidden2 = self.hidden2(hidden1)
        concat = tf.keras.layers.concatenate([norm_wide, hidden2])
        output = self.main_output(concat)
        aux_output = self.aux_output(hidden2)
        return output, aux_output
model = WideAndDeepModel(30, activation="relu", name="my_cool_model")

这个例子看起来与前一个例子相似,只是我们在构造函数中将层的创建与它们在call()方法中的使用分开。而且我们不需要创建Input对象:我们可以在call()方法中使用input参数。

现在我们有了一个模型实例,我们可以对其进行编译,调整其归一化层(例如,使用model.norm_layer_wide.adapt(...)model.norm_layer_deep.adapt(...)),拟合它,评估它,并使用它进行预测,就像我们使用功能 API 一样。

这个 API 的一个重要区别是,您可以在call()方法中包含几乎任何您想要的东西:for循环,if语句,低级别的 TensorFlow 操作——您的想象力是唯一的限制(参见第十二章)!这使得它成为一个很好的 API,特别适用于研究人员尝试新想法。然而,这种额外的灵活性是有代价的:您的模型架构被隐藏在call()方法中,因此 Keras 无法轻松地检查它;模型无法使用tf.keras.models.clone_model()进行克隆;当您调用summary()方法时,您只会得到一个层列表,而没有关于它们如何连接在一起的任何信息。此外,Keras 无法提前检查类型和形状,容易出错。因此,除非您真的需要额外的灵活性,否则您可能应该坚持使用顺序 API 或功能 API。

提示

Keras 模型可以像常规层一样使用,因此您可以轻松地将它们组合在一起构建复杂的架构。

现在您知道如何使用 Keras 构建和训练神经网络,您会想要保存它们!

保存和恢复模型

保存训练好的 Keras 模型就是这么简单:

model.save("my_keras_model", save_format="tf")

当您设置save_format="tf"时,Keras 会使用 TensorFlow 的SavedModel格式保存模型:这是一个目录(带有给定名称),包含多个文件和子目录。特别是,saved_model.pb文件包含模型的架构和逻辑,以序列化的计算图形式,因此您不需要部署模型的源代码才能在生产中使用它;SavedModel 就足够了(您将在第十二章中看到这是如何工作的)。keras_metadata.pb文件包含 Keras 所需的额外信息。variables子目录包含所有参数值(包括连接权重、偏差、归一化统计数据和优化器参数),如果模型非常大,可能会分成多个文件。最后,assets目录可能包含额外的文件,例如数据样本、特征名称、类名等。默认情况下,assets目录为空。由于优化器也被保存了,包括其超参数和可能存在的任何状态,加载模型后,您可以继续训练。

注意

如果设置save_format="h5"或使用以*.h5*、.hdf5或*.keras*结尾的文件名,则 Keras 将使用基于 HDF5 格式的 Keras 特定格式将模型保存到单个文件中。然而,大多数 TensorFlow 部署工具需要使用 SavedModel 格式。

通常会有一个脚本用于训练模型并保存它,以及一个或多个脚本(或 Web 服务)用于加载模型并用于评估或进行预测。加载模型和保存模型一样简单:

model = tf.keras.models.load_model("my_keras_model")
y_pred_main, y_pred_aux = model.predict((X_new_wide, X_new_deep))

您还可以使用save_weights()load_weights()来仅保存和加载参数值。这包括连接权重、偏差、预处理统计数据、优化器状态等。参数值保存在一个或多个文件中,例如my_weights.data-00004-of-00052,再加上一个索引文件,如my_weights.index

仅保存权重比保存整个模型更快,占用更少的磁盘空间,因此在训练过程中保存快速检查点非常完美。如果您正在训练一个大模型,需要数小时或数天,那么您必须定期保存检查点以防计算机崩溃。但是如何告诉fit()方法保存检查点呢?使用回调。

使用回调

fit()方法接受一个callbacks参数,让您可以指定一个对象列表,Keras 会在训练之前和之后、每个时代之前和之后,甚至在处理每个批次之前和之后调用它们。例如,ModelCheckpoint回调会在训练期间定期保存模型的检查点,默认情况下在每个时代结束时:

checkpoint_cb = tf.keras.callbacks.ModelCheckpoint("my_checkpoints",
                                                   save_weights_only=True)
history = model.fit([...], callbacks=[checkpoint_cb])

此外,在训练过程中使用验证集时,您可以在创建 ModelCheckpoint 时设置 save_best_only=True。在这种情况下,它只会在模型在验证集上的表现迄今为止最好时保存您的模型。这样,您就不需要担心训练时间过长和过拟合训练集:只需在训练后恢复最后保存的模型,这将是验证集上的最佳模型。这是实现提前停止的一种方式(在第四章中介绍),但它实际上不会停止训练。

另一种方法是使用 EarlyStopping 回调。当在一定数量的周期(由 patience 参数定义)内在验证集上测量不到进展时,它将中断训练,如果您设置 restore_best_weights=True,它将在训练结束时回滚到最佳模型。您可以结合这两个回调来保存模型的检查点,以防计算机崩溃,并在没有进展时提前中断训练,以避免浪费时间和资源并减少过拟合:

early_stopping_cb = tf.keras.callbacks.EarlyStopping(patience=10,
                                                     restore_best_weights=True)
history = model.fit([...], callbacks=[checkpoint_cb, early_stopping_cb])

由于训练将在没有进展时自动停止(只需确保学习率不要太小,否则可能会一直缓慢进展直到结束),所以可以将周期数设置为一个较大的值。EarlyStopping 回调将在 RAM 中存储最佳模型的权重,并在训练结束时为您恢复它们。

提示

tf.keras.callbacks中还有许多其他回调可用。

如果您需要额外的控制,您可以轻松编写自己的自定义回调。例如,以下自定义回调将在训练过程中显示验证损失和训练损失之间的比率(例如,用于检测过拟合):

class PrintValTrainRatioCallback(tf.keras.callbacks.Callback):
    def on_epoch_end(self, epoch, logs):
        ratio = logs["val_loss"] / logs["loss"]
        print(f"Epoch={epoch}, val/train={ratio:.2f}")

正如您可能期望的那样,您可以实现 on_train_begin()on_train_end()on_epoch_begin()on_epoch_end()on_batch_begin()on_batch_end()。回调也可以在评估和预测期间使用,如果您需要的话(例如,用于调试)。对于评估,您应该实现 on_test_begin()on_test_end()on_test_batch_begin()on_test_batch_end(),这些方法由 evaluate() 调用。对于预测,您应该实现 on_predict_begin()on_predict_end()on_predict_batch_begin()on_predict_batch_end(),这些方法由 predict() 调用。

现在让我们再看看在使用 Keras 时您绝对应该拥有的另一个工具:TensorBoard。

使用 TensorBoard 进行可视化

TensorBoard 是一个很棒的交互式可视化工具,您可以使用它来查看训练过程中的学习曲线,比较多次运行之间的曲线和指标,可视化计算图,分析训练统计数据,查看模型生成的图像,将复杂的多维数据投影到 3D 并自动为您进行聚类,分析您的网络(即,测量其速度以识别瓶颈),等等!

TensorBoard 在安装 TensorFlow 时会自动安装。但是,您需要一个 TensorBoard 插件来可视化分析数据。如果您按照https://homl.info/install上的安装说明在本地运行所有内容,那么您已经安装了插件,但如果您在使用 Colab,则必须运行以下命令:

%pip install -q -U tensorboard-plugin-profile

要使用 TensorBoard,必须修改程序,以便将要可视化的数据输出到称为事件文件的特殊二进制日志文件中。每个二进制数据记录称为摘要。TensorBoard 服务器将监视日志目录,并自动捕捉更改并更新可视化:这使您能够可视化实时数据(有短暂延迟),例如训练期间的学习曲线。通常,您希望将 TensorBoard 服务器指向一个根日志目录,并配置程序,使其在每次运行时写入不同的子目录。这样,同一个 TensorBoard 服务器实例将允许您可视化和比较程序的多次运行中的数据,而不会混淆一切。

让我们将根日志目录命名为my_logs,并定义一个小函数,根据当前日期和时间生成日志子目录的路径,以便在每次运行时都不同:

from pathlib import Path
from time import strftime
def get_run_logdir(root_logdir="my_logs"):
    return Path(root_logdir) / strftime("run_%Y_%m_%d_%H_%M_%S")
run_logdir = get_run_logdir()  # e.g., my_logs/run_2022_08_01_17_25_59

好消息是,Keras 提供了一个方便的TensorBoard()回调,它会为您创建日志目录(以及必要时的父目录),并在训练过程中创建事件文件并写入摘要。它将测量模型的训练和验证损失和指标(在本例中是 MSE 和 RMSE),还会对神经网络进行分析。使用起来很简单:

tensorboard_cb = tf.keras.callbacks.TensorBoard(run_logdir,
                                                profile_batch=(100, 200))
history = model.fit([...], callbacks=[tensorboard_cb])

就是这样!在这个例子中,它将在第一个时期的 100 和 200 批之间对网络进行分析。为什么是 100 和 200?嗯,神经网络通常需要几批数据来“热身”,所以你不希望太早进行分析,而且分析会使用资源,最好不要为每一批数据都进行分析。

接下来,尝试将学习率从 0.001 更改为 0.002,然后再次运行代码,使用一个新的日志子目录。你将得到一个类似于这样的目录结构:

my_logs
├── run_2022_08_01_17_25_59
│   ├── train
│   │   ├── events.out.tfevents.1659331561.my_host_name.42042.0.v2
│   │   ├── events.out.tfevents.1659331562.my_host_name.profile-empty
│   │   └── plugins
│   │       └── profile
│   │           └── 2022_08_01_17_26_02
│   │               ├── my_host_name.input_pipeline.pb
│   │               └── [...]
│   └── validation
│       └── events.out.tfevents.1659331562.my_host_name.42042.1.v2
└── run_2022_08_01_17_31_12
    └── [...]

每次运行都有一个目录,每个目录包含一个用于训练日志和一个用于验证日志的子目录。两者都包含事件文件,而训练日志还包括分析跟踪。

现在你已经准备好事件文件,是时候启动 TensorBoard 服务器了。可以直接在 Jupyter 或 Colab 中使用 TensorBoard 的 Jupyter 扩展来完成,该扩展会随 TensorBoard 库一起安装。这个扩展在 Colab 中是预安装的。以下代码加载了 TensorBoard 的 Jupyter 扩展,第二行启动了一个 TensorBoard 服务器,连接到这个服务器并直接在 Jupyter 中显示用户界面。服务器会监听大于或等于 6006 的第一个可用 TCP 端口(或者您可以使用--port选项设置您想要的端口)。

%load_ext tensorboard
%tensorboard --logdir=./my_logs
提示

如果你在自己的机器上运行所有内容,可以通过在终端中执行tensorboard --logdir=./my_logs来启动 TensorBoard。您必须首先激活安装了 TensorBoard 的 Conda 环境,并转到handson-ml3目录。一旦服务器启动,访问http://localhost:6006

现在你应该看到 TensorBoard 的用户界面。点击 SCALARS 选项卡查看学习曲线(参见图 10-16)。在左下角,选择要可视化的日志(例如第一次和第二次运行的训练日志),然后点击epoch_loss标量。注意,训练损失在两次运行期间都很好地下降了,但在第二次运行中,由于更高的学习率,下降速度稍快。


图 10-16。使用 TensorBoard 可视化学习曲线

您还可以在 GRAPHS 选项卡中可视化整个计算图,在 PROJECTOR 选项卡中将学习的权重投影到 3D 中,在 PROFILE 选项卡中查看性能跟踪。TensorBoard()回调还有选项可以记录额外的数据(请参阅文档以获取更多详细信息)。您可以点击右上角的刷新按钮(⟳)使 TensorBoard 刷新数据,也可以点击设置按钮(⚙)激活自动刷新并指定刷新间隔。

此外,TensorFlow 在tf.summary包中提供了一个较低级别的 API。以下代码使用create_file_writer()函数创建一个SummaryWriter,并将此写入器用作 Python 上下文来记录标量、直方图、图像、音频和文本,所有这些都可以使用 TensorBoard 进行可视化:

test_logdir = get_run_logdir()
writer = tf.summary.create_file_writer(str(test_logdir))
with writer.as_default():
    for step in range(1, 1000 + 1):
        tf.summary.scalar("my_scalar", np.sin(step / 10), step=step)
        data = (np.random.randn(100) + 2) * step / 100  # gets larger
        tf.summary.histogram("my_hist", data, buckets=50, step=step)
        images = np.random.rand(2, 32, 32, 3) * step / 1000  # gets brighter
        tf.summary.image("my_images", images, step=step)
        texts = ["The step is " + str(step), "Its square is " + str(step ** 2)]
        tf.summary.text("my_text", texts, step=step)
        sine_wave = tf.math.sin(tf.range(12000) / 48000 * 2 * np.pi * step)
        audio = tf.reshape(tf.cast(sine_wave, tf.float32), [1, -1, 1])
        tf.summary.audio("my_audio", audio, sample_rate=48000, step=step)

如果您运行此代码并在 TensorBoard 中点击刷新按钮,您将看到几个选项卡出现:IMAGES、AUDIO、DISTRIBUTIONS、HISTOGRAMS 和 TEXT。尝试点击 IMAGES 选项卡,并使用每个图像上方的滑块查看不同时间步的图像。同样,转到 AUDIO 选项卡并尝试在不同时间步听音频。正如您所看到的,TensorBoard 甚至在 TensorFlow 或深度学习之外也是一个有用的工具。

提示

您可以通过将结果发布到https://tensorboard.dev来在线共享您的结果。为此,只需运行!tensorboard dev upload --logdir ./my_logs。第一次运行时,它会要求您接受条款和条件并进行身份验证。然后您的日志将被上传,您将获得一个永久链接,以在 TensorBoard 界面中查看您的结果。

让我们总结一下你在本章学到的内容:你现在知道神经网络的起源,MLP 是什么以及如何将其用于分类和回归,如何使用 Keras 的顺序 API 构建 MLP,以及如何使用功能 API 或子类 API 构建更复杂的模型架构(包括 Wide & Deep 模型,以及具有多个输入和输出的模型)。您还学会了如何保存和恢复模型,以及如何使用回调函数进行检查点、提前停止等。最后,您学会了如何使用 TensorBoard 进行可视化。您已经可以开始使用神经网络来解决许多问题了!但是,您可能想知道如何选择隐藏层的数量、网络中的神经元数量以及所有其他超参数。让我们现在来看看这个问题。

微调神经网络超参数

神经网络的灵活性也是它们的主要缺点之一:有许多超参数需要调整。不仅可以使用任何想象得到的网络架构,甚至在基本的 MLP 中,您可以更改层的数量、每层中要使用的神经元数量和激活函数的类型、权重初始化逻辑、要使用的优化器类型、学习率、批量大小等。您如何知道哪种超参数组合对您的任务最好?

一种选择是将您的 Keras 模型转换为 Scikit-Learn 估计器,然后使用GridSearchCVRandomizedSearchCV来微调超参数,就像您在第二章中所做的那样。为此,您可以使用 SciKeras 库中的KerasRegressorKerasClassifier包装类(有关更多详细信息,请参阅https://github.com/adriangb/scikeras)。但是,还有一种更好的方法:您可以使用Keras Tuner库,这是一个用于 Keras 模型的超参数调整库。它提供了几种调整策略,可以高度定制,并且与 TensorBoard 有很好的集成。让我们看看如何使用它。

如果您按照https://homl.info/install中的安装说明在本地运行所有内容,那么您已经安装了 Keras Tuner,但如果您使用 Colab,则需要运行 %pip install -q -U keras-tuner。接下来,导入 keras_tuner,通常为 kt,然后编写一个函数来构建、编译并返回一个 Keras 模型。该函数必须接受一个 kt.HyperParameters 对象作为参数,它可以用来定义超参数(整数、浮点数、字符串等)以及它们可能的取值范围,这些超参数可以用来构建和编译模型。例如,以下函数构建并编译了一个用于分类时尚 MNIST 图像的 MLP,使用超参数如隐藏层的数量(n_hidden)、每层神经元的数量(n_neurons)、学习率(learning_rate)和要使用的优化器类型(optimizer):

import keras_tuner as kt
def build_model(hp):
    n_hidden = hp.Int("n_hidden", min_value=0, max_value=8, default=2)
    n_neurons = hp.Int("n_neurons", min_value=16, max_value=256)
    learning_rate = hp.Float("learning_rate", min_value=1e-4, max_value=1e-2,
                             sampling="log")
    optimizer = hp.Choice("optimizer", values=["sgd", "adam"])
    if optimizer == "sgd":
        optimizer = tf.keras.optimizers.SGD(learning_rate=learning_rate)
    else:
        optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Flatten())
    for _ in range(n_hidden):
        model.add(tf.keras.layers.Dense(n_neurons, activation="relu"))
    model.add(tf.keras.layers.Dense(10, activation="softmax"))
    model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer,
                  metrics=["accuracy"])
    return model

函数的第一部分定义了超参数。例如,hp.Int("n_hidden", min_value=0, max_value=8, default=2) 检查了名为 "n_hidden" 的超参数是否已经存在于 hpHyperParameters 对象中,如果存在,则返回其值。如果不存在,则注册一个新的整数超参数,名为 "n_hidden",其可能的取值范围从 0 到 8(包括边界),并返回默认值,在本例中默认值为 2(当未设置 default 时,返回 min_value)。 "n_neurons" 超参数以类似的方式注册。 "learning_rate" 超参数注册为一个浮点数,范围从 10^(-4) 到 10^(-2),由于 sampling="log",所有尺度的学习率将被等概率采样。最后,optimizer 超参数注册了两个可能的值:“sgd” 或 “adam”(默认值是第一个,本例中为 “sgd”)。根据 optimizer 的值,我们创建一个具有给定学习率的 SGD 优化器或 Adam 优化器。

函数的第二部分只是使用超参数值构建模型。它创建一个 Sequential 模型,从一个 Flatten 层开始,然后是请求的隐藏层数量(由 n_hidden 超参数确定)使用 ReLU 激活函数,以及一个具有 10 个神经元(每类一个)的输出层,使用 softmax 激活函数。最后,函数编译模型并返回它。

现在,如果您想进行基本的随机搜索,可以创建一个 kt.RandomSearch 调谐器,将 build_model 函数传递给构造函数,并调用调谐器的 search() 方法:

random_search_tuner = kt.RandomSearch(
    build_model, objective="val_accuracy", max_trials=5, overwrite=True,
    directory="my_fashion_mnist", project_name="my_rnd_search", seed=42)
random_search_tuner.search(X_train, y_train, epochs=10,
                           validation_data=(X_valid, y_valid))

RandomSearch 调谐器首先使用一个空的 Hyperparameters 对象调用 build_model() 一次,以收集所有超参数规范。然后,在这个例子中,它运行 5 个试验;对于每个试验,它使用在其各自范围内随机抽样的超参数构建一个模型,然后对该模型进行 10 个周期的训练,并将其保存到 my_fashion_mnist/my_rnd_search 目录的子目录中。由于 overwrite=True,在训练开始之前 my_rnd_search 目录将被删除。如果您再次运行此代码,但使用 overwrite=Falsemax_trials=10,调谐器将继续从上次停止的地方进行调谐,运行 5 个额外的试验:这意味着您不必一次性运行所有试验。最后,由于 objective 设置为 "val_accuracy",调谐器更喜欢具有更高验证准确性的模型,因此一旦调谐器完成搜索,您可以像这样获取最佳模型:

top3_models = random_search_tuner.get_best_models(num_models=3)
best_model = top3_models[0]

您还可以调用 get_best_hyperparameters() 来获取最佳模型的 kt.HyperParameters

>>> top3_params = random_search_tuner.get_best_hyperparameters(num_trials=3)
>>> top3_params[0].values  # best hyperparameter values
{'n_hidden': 5,
 'n_neurons': 70,
 'learning_rate': 0.00041268008323824807,
 'optimizer': 'adam'}

每个调谐器都由一个所谓的oracle指导:在每次试验之前,调谐器会询问 oracle 告诉它下一个试验应该是什么。RandomSearch调谐器使用RandomSearchOracle,它非常基本:就像我们之前看到的那样,它只是随机选择下一个试验。由于 oracle 跟踪所有试验,您可以要求它给出最佳试验,并显示该试验的摘要:

>>> best_trial = random_search_tuner.oracle.get_best_trials(num_trials=1)[0]
>>> best_trial.summary()
Trial summary
Hyperparameters:
n_hidden: 5
n_neurons: 70
learning_rate: 0.00041268008323824807
optimizer: adam
Score: 0.8736000061035156

这显示了最佳超参数(与之前一样),以及验证准确率。您也可以直接访问所有指标:

>>> best_trial.metrics.get_last_value("val_accuracy")
0.8736000061035156

如果您对最佳模型的性能感到满意,您可以在完整的训练集(X_train_fully_train_full)上继续训练几个时期,然后在测试集上评估它,并将其部署到生产环境(参见第十九章):

best_model.fit(X_train_full, y_train_full, epochs=10)
test_loss, test_accuracy = best_model.evaluate(X_test, y_test)

在某些情况下,您可能希望微调数据预处理超参数或model.fit()参数,比如批量大小。为此,您必须使用略有不同的技术:而不是编写一个build_model()函数,您必须子类化kt.HyperModel类并定义两个方法,build()fit()build()方法执行与build_model()函数完全相同的操作。fit()方法接受一个HyperParameters对象和一个已编译的模型作为参数,以及所有model.fit()参数,并拟合模型并返回History对象。关键是,fit()方法可以使用超参数来决定如何预处理数据,调整批量大小等。例如,以下类构建了与之前相同的模型,具有相同的超参数,但它还使用一个布尔型"normalize"超参数来控制是否在拟合模型之前标准化训练数据:

class MyClassificationHyperModel(kt.HyperModel):
    def build(self, hp):
        return build_model(hp)
    def fit(self, hp, model, X, y, **kwargs):
        if hp.Boolean("normalize"):
            norm_layer = tf.keras.layers.Normalization()
            X = norm_layer(X)
        return model.fit(X, y, **kwargs)

然后,您可以将此类的实例传递给您选择的调谐器,而不是传递build_model函数。例如,让我们基于MyClassificationHyperModel实例构建一个kt.Hyperband调谐器:

hyperband_tuner = kt.Hyperband(
    MyClassificationHyperModel(), objective="val_accuracy", seed=42,
    max_epochs=10, factor=3, hyperband_iterations=2,
    overwrite=True, directory="my_fashion_mnist", project_name="hyperband")

这个调谐器类似于我们在第二章中讨论的HalvingRandomSearchCV类:它首先为少数时期训练许多不同的模型,然后消除最差的模型,仅保留前1 / factor个模型(在这种情况下是前三分之一),重复此选择过程,直到只剩下一个模型。max_epochs参数控制最佳模型将被训练的最大时期数。在这种情况下,整个过程重复两次(hyperband_iterations=2)。每个超带迭代中所有模型的总训练时期数约为max_epochs * (log(max_epochs) / log(factor)) ** 2,因此在这个例子中大约为 44 个时期。其他参数与kt.RandomSearch相同。

现在让我们运行 Hyperband 调谐器。我们将使用TensorBoard回调,这次指向根日志目录(调谐器将负责为每个试验使用不同的子目录),以及一个EarlyStopping回调:

root_logdir = Path(hyperband_tuner.project_dir) / "tensorboard"
tensorboard_cb = tf.keras.callbacks.TensorBoard(root_logdir)
early_stopping_cb = tf.keras.callbacks.EarlyStopping(patience=2)
hyperband_tuner.search(X_train, y_train, epochs=10,
                       validation_data=(X_valid, y_valid),
                       callbacks=[early_stopping_cb, tensorboard_cb])

现在,如果您打开 TensorBoard,将--logdir指向my_fashion_mnist/hyperband/tensorboard目录,您将看到所有试验结果的展示。确保访问 HPARAMS 选项卡:它包含了所有尝试过的超参数组合的摘要,以及相应的指标。请注意,在 HPARAMS 选项卡内部有三个选项卡:表格视图、平行坐标视图和散点图矩阵视图。在左侧面板的下部,取消选中除了validation.epoch_accuracy之外的所有指标:这将使图表更清晰。在平行坐标视图中,尝试选择validation.epoch_accuracy列中的高值范围:这将仅显示达到良好性能的超参数组合。单击其中一个超参数组合,相应的学习曲线将出现在页面底部。花些时间浏览每个选项卡;这将帮助您了解每个超参数对性能的影响,以及超参数之间的相互作用。

Hyperband 比纯随机搜索更聪明,因为它分配资源的方式更为高效,但在其核心部分仍然是随机探索超参数空间;它快速,但粗糙。然而,Keras Tuner 还包括一个kt.BayesianOptimization调谐器:这种算法通过拟合一个称为高斯过程的概率模型逐渐学习哪些超参数空间区域最有前途。这使得它逐渐聚焦于最佳超参数。缺点是该算法有自己的超参数:alpha代表您在试验中期望性能指标中的噪声水平(默认为 10^(–4)),beta指定您希望算法探索而不仅仅利用已知的超参数空间中的良好区域(默认为 2.6)。除此之外,这个调谐器可以像之前的调谐器一样使用:

bayesian_opt_tuner = kt.BayesianOptimization(
    MyClassificationHyperModel(), objective="val_accuracy", seed=42,
    max_trials=10, alpha=1e-4, beta=2.6,
    overwrite=True, directory="my_fashion_mnist", project_name="bayesian_opt")
bayesian_opt_tuner.search([...])

超参数调整仍然是一个活跃的研究领域,许多其他方法正在被探索。例如,查看 DeepMind 出色的2017 年论文,其中作者使用进化算法共同优化了一组模型和它们的超参数。谷歌也采用了进化方法,不仅用于搜索超参数,还用于探索各种模型架构:它为谷歌 Vertex AI 上的 AutoML 服务提供动力(参见第十九章)。术语AutoML指的是任何系统,它负责 ML 工作流的大部分。进化算法甚至已成功用于训练单个神经网络,取代了无处不在的梯度下降!例如,查看 Uber 在2017 年发布的文章,作者介绍了他们的Deep Neuroevolution技术。

尽管有这些令人兴奋的进展和所有这些工具和服务,但仍然有必要了解每个超参数的合理值,以便您可以构建一个快速原型并限制搜索空间。以下部分提供了选择 MLP 中隐藏层和神经元数量以及选择一些主要超参数的良好值的指导方针。

隐藏层的数量

对于许多问题,您可以从一个隐藏层开始并获得合理的结果。具有一个隐藏层的 MLP 在理论上可以建模甚至最复杂的函数,只要它有足够的神经元。但对于复杂问题,深度网络比浅层网络具有更高的参数效率:它们可以使用指数级较少的神经元来建模复杂函数,从而使它们在相同数量的训练数据下达到更好的性能。

要理解为什么,假设您被要求使用绘图软件画一片森林,但是禁止复制和粘贴任何东西。这将需要大量的时间:您必须逐个绘制每棵树,一枝一枝,一叶一叶。如果您可以绘制一片叶子,复制并粘贴它以绘制一根树枝,然后复制并粘贴该树枝以创建一棵树,最后复制并粘贴这棵树以制作一片森林,您将很快完成。现实世界的数据通常以这种分层方式结构化,深度神经网络会自动利用这一事实:较低的隐藏层模拟低级结构(例如各种形状和方向的线段),中间隐藏层将这些低级结构组合起来模拟中级结构(例如正方形、圆形),最高隐藏层和输出层将这些中级结构组合起来模拟高级结构(例如人脸)。

这种分层结构不仅有助于深度神经网络更快地收敛到一个好的解决方案,而且还提高了它们对新数据集的泛化能力。例如,如果您已经训练了一个模型来识别图片中的人脸,现在想要训练一个新的神经网络来识别发型,您可以通过重用第一个网络的较低层来启动训练。而不是随机初始化新神经网络的前几层的权重和偏置,您可以将它们初始化为第一个网络较低层的权重和偏置的值。这样网络就不必从头学习出现在大多数图片中的所有低级结构;它只需要学习更高级的结构(例如发型)。这就是所谓的迁移学习

总之,对于许多问题,您可以从只有一个或两个隐藏层开始,神经网络就能正常工作。例如,您可以仅使用一个具有几百个神经元的隐藏层在 MNIST 数据集上轻松达到 97% 以上的准确率,使用两个具有相同总神经元数量的隐藏层在大致相同的训练时间内达到 98% 以上的准确率。对于更复杂的问题,您可以增加隐藏层的数量,直到开始过拟合训练集。非常复杂的任务,例如大型图像分类或语音识别,通常需要具有数十层(甚至数百层,但不是全连接的,如您将在第十四章中看到的)的网络,并且需要大量的训练数据。您很少需要从头开始训练这样的网络:更常见的做法是重用执行类似任务的预训练最先进网络的部分。这样训练速度会更快,需要的数据量也会更少(我们将在第十一章中讨论这一点)。

隐藏层中的神经元数量

输入层和输出层的神经元数量取决于您的任务所需的输入和输出类型。例如,MNIST 任务需要 28 × 28 = 784 个输入和 10 个输出神经元。

至于隐藏层,过去常见的做法是将它们大小设计成金字塔形,每一层的神经元数量越来越少——其理由是许多低级特征可以融合成远远较少的高级特征。一个典型的用于 MNIST 的神经网络可能有 3 个隐藏层,第一个有 300 个神经元,第二个有 200 个,第三个有 100 个。然而,这种做法已经被大多数人放弃,因为似乎在大多数情况下,在所有隐藏层中使用相同数量的神经元表现得同样好,甚至更好;此外,只需调整一个超参数,而不是每一层一个。尽管如此,根据数据集的不同,有时将第一个隐藏层设计得比其他隐藏层更大可能会有所帮助。

就像层数一样,您可以尝试逐渐增加神经元的数量,直到网络开始过拟合。或者,您可以尝试构建一个比实际需要的层数和神经元稍多一点的模型,然后使用提前停止和其他正则化技术来防止过度拟合。Google 的科学家 Vincent Vanhoucke 将此称为“伸展裤”方法:不要浪费时间寻找完全符合您尺寸的裤子,只需使用大号伸展裤,它们会缩小到合适的尺寸。通过这种方法,您可以避免可能破坏模型的瓶颈层。实际上,如果一层的神经元太少,它将没有足够的表征能力来保留来自输入的所有有用信息(例如,具有两个神经元的层只能输出 2D 数据,因此如果它以 3D 数据作为输入,一些信息将丢失)。无论网络的其余部分有多大和强大,该信息都将永远无法恢复。

提示

一般来说,增加层数而不是每层的神经元数量会更有效。

学习率、批量大小和其他超参数

隐藏层和神经元的数量并不是您可以在 MLP 中调整的唯一超参数。以下是一些最重要的超参数,以及如何设置它们的提示:

学习率

学习率可以说是最重要的超参数。一般来说,最佳学习率约为最大学习率的一半(即训练算法发散的学习率上限,如我们在第四章中看到的)。找到一个好的学习率的方法是训练模型几百次迭代,从非常低的学习率(例如,10^(-5))开始,逐渐增加到非常大的值(例如,10)。这是通过在每次迭代时将学习率乘以一个常数因子来完成的(例如,通过(10 / 10(-5))(1 / 500)在 500 次迭代中从 10^(-5)增加到 10)。如果将损失作为学习率的函数绘制出来(使用对数刻度的学习率),您应该会看到它一开始下降。但过一段时间,学习率将变得太大,因此损失会迅速上升:最佳学习率将略低于损失开始上升的点(通常比转折点低约 10 倍)。然后,您可以重新初始化您的模型,并使用这个好的学习率进行正常训练。我们将在第十一章中探讨更多学习率优化技术。

优化器

选择比普通的小批量梯度下降更好的优化器(并调整其超参数)也非常重要。我们将在第十一章中研究几种高级优化器。

批量大小

批量大小可能会对模型的性能和训练时间产生重大影响。使用大批量大小的主要好处是硬件加速器如 GPU 可以高效处理它们(参见第十九章),因此训练算法将每秒看到更多实例。因此,许多研究人员和从业者建议使用能够适应 GPU RAM 的最大批量大小。然而,有一个问题:在实践中,大批量大小通常会导致训练不稳定,特别是在训练开始时,由此产生的模型可能不会像使用小批量大小训练的模型那样泛化得好。2018 年 4 月,Yann LeCun 甚至在推特上发表了“朋友们不要让朋友们使用大于 32 的小批量”的言论,引用了 Dominic Masters 和 Carlo Luschi 在2018 年的一篇论文的结论,该论文认为使用小批量(从 2 到 32)更可取,因为小批量在更短的训练时间内产生更好的模型。然而,其他研究结果却指向相反的方向。例如,2017 年,Elad Hoffer 等人的论文和 Priya Goyal 等人的论文显示,可以使用非常大的批量大小(高达 8,192),并结合各种技术,如学习率预热(即从小学习率开始训练,然后逐渐增加,如第十一章中讨论的那样),以获得非常短的训练时间,而不会出现泛化差距。因此,一种策略是尝试使用大批量大小,结合学习率预热,如果训练不稳定或最终性能令人失望,则尝试改用小批量大小。

激活函数

我们在本章前面讨论了如何选择激活函数:一般来说,ReLU 激活函数将是所有隐藏层的一个很好的默认选择,但对于输出层,它真的取决于您的任务。

迭代次数

在大多数情况下,实际上不需要调整训练迭代次数:只需使用早停止即可。

提示

最佳学习率取决于其他超参数,尤其是批量大小,因此如果您修改任何超参数,请确保同时更新学习率。

有关调整神经网络超参数的最佳实践,请查看 Leslie Smith 的优秀2018 年论文

这结束了我们关于人工神经网络及其在 Keras 中的实现的介绍。在接下来的几章中,我们将讨论训练非常深的网络的技术。我们还将探讨如何使用 TensorFlow 的低级 API 自定义模型,以及如何使用 tf.data API 高效加载和预处理数据。我们将深入研究其他流行的神经网络架构:用于图像处理的卷积神经网络,用于序列数据和文本的循环神经网络和 transformers,用于表示学习的自编码器,以及用于建模和生成数据的生成对抗网络。

练习

  1. TensorFlow playground是由 TensorFlow 团队构建的一个方便的神经网络模拟器。在这个练习中,您将只需点击几下就可以训练几个二元分类器,并调整模型的架构和超参数,以便对神经网络的工作原理和超参数的作用有一些直观的认识。花一些时间来探索以下内容:
  1. 神经网络学习的模式。尝试通过点击运行按钮(左上角)训练默认的神经网络。注意到它如何快速找到分类任务的良好解决方案。第一个隐藏层中的神经元已经学会了简单的模式,而第二个隐藏层中的神经元已经学会了将第一个隐藏层的简单模式组合成更复杂的模式。一般来说,层数越多,模式就越复杂。
  2. 激活函数。尝试用 ReLU 激活函数替换 tanh 激活函数,并重新训练网络。注意到它找到解决方案的速度更快,但这次边界是线性的。这是由于 ReLU 函数的形状。
  3. 局部最小值的风险。修改网络架构,只有一个有三个神经元的隐藏层。多次训练它(要重置网络权重,点击播放按钮旁边的重置按钮)。注意到训练时间变化很大,有时甚至会卡在局部最小值上。
  4. 当神经网络太小时会发生什么。移除一个神经元,只保留两个。注意到神经网络现在无法找到一个好的解决方案,即使你尝试多次。模型参数太少,系统地欠拟合训练集。
  5. 当神经网络足够大时会发生什么。将神经元数量设置为八,并多次训练网络。注意到现在训练速度一致快速,从不卡住。这突显了神经网络理论中的一个重要发现:大型神经网络很少会卡在局部最小值上,即使卡住了,这些局部最优解通常几乎和全局最优解一样好。然而,它们仍然可能在长时间的高原上卡住。
  6. 深度网络中梯度消失的风险。选择螺旋数据集(“DATA”下方的右下数据集),并将网络架构更改为每个有八个神经元的四个隐藏层。注意到训练时间更长,经常在高原上卡住很长时间。还要注意到最高层(右侧)的神经元比最低层(左侧)的神经元进化得更快。这个问题被称为梯度消失问题,可以通过更好的权重初始化和其他技术、更好的优化器(如 AdaGrad 或 Adam)或批量归一化(在第十一章中讨论)来缓解。
  7. 更进一步。花一个小时左右的时间玩弄其他参数,了解它们的作用,建立对神经网络的直观理解。
  1. 使用原始人工神经元(如图 10-3 中的人工神经元)绘制一个 ANN,计算AB(其中 ⊕ 表示异或操作)。提示:AB = (A ∧ ¬ B) ∨ (¬ AB)。
  2. 通常更倾向于使用逻辑回归分类器而不是经典感知器(即使用感知器训练算法训练的阈值逻辑单元的单层)。如何调整感知器使其等效于逻辑回归分类器?
  3. 为什么 Sigmoid 激活函数是训练第一个 MLP 的关键因素?
  4. 列出三种流行的激活函数。你能画出它们吗?
  5. 假设你有一个 MLP,由一个具有 10 个传递神经元的输入层、一个具有 50 个人工神经元的隐藏层和一个具有 3 个人工神经元的输出层组成。所有人工神经元都使用 ReLU 激活函数。
  1. 输入矩阵X的形状是什么?
  2. 隐藏层权重矩阵W[h]和偏置向量b[h]的形状是什么?
  3. 输出层权重矩阵W[o]和偏置向量b[o]的形状是什么?
  4. 网络输出矩阵Y的形状是什么?
  5. 写出计算网络输出矩阵Y的方程,作为XW[h]、b[h]、W[o]和b[o]的函数。
  1. 如果你想将电子邮件分类为垃圾邮件或正常邮件,输出层需要多少个神经元?输出层应该使用什么激活函数?如果你想处理 MNIST 数据集,输出层需要多少个神经元,应该使用哪种激活函数?对于让你的网络预测房价,如第二章中所述,需要多少个神经元,应该使用什么激活函数?
  2. 什么是反向传播,它是如何工作的?反向传播和反向模式自动微分之间有什么区别?
  3. 在基本的 MLP 中,你可以调整哪些超参数?如果 MLP 过拟合训练数据,你可以如何调整这些超参数来尝试解决问题?
  4. 在 MNIST 数据集上训练一个深度 MLP(可以使用tf.keras.datasets.mnist.load_data()加载)。看看你是否可以通过手动调整超参数获得超过 98%的准确率。尝试使用本章介绍的方法搜索最佳学习率(即通过指数增长学习率,绘制损失曲线,并找到损失飙升的点)。接下来,尝试使用 Keras Tuner 调整超参数,包括保存检查点、使用早停止,并使用 TensorBoard 绘制学习曲线。

这些练习的解决方案可以在本章笔记本的末尾找到,网址为https://homl.info/colab3

¹ 你可以通过对生物启发开放,而不害怕创建生物不现实的模型,来获得两全其美,只要它们运行良好。

² Warren S. McCulloch 和 Walter Pitts,“神经活动中固有思想的逻辑演算”,《数学生物学公报》5 卷 4 期(1943 年):115-113。

³ 它们实际上并没有连接,只是非常接近,可以非常快速地交换化学信号。

⁴ Bruce Blaus 绘制的图像(知识共享 3.0)。来源:https://en.wikipedia.org/wiki/Neuron

⁵ 在机器学习的背景下,“神经网络”一词通常指的是人工神经网络,而不是生物神经网络。

⁶ S. Ramon y Cajal 绘制的皮层层析图(公有领域)。来源:https://en.wikipedia.org/wiki/Cerebral_cortex

⁷ 请注意,这个解决方案并不唯一:当数据点线性可分时,有无穷多个可以将它们分开的超平面。

⁸ 例如,当输入为(0,1)时,左下神经元计算 0 × 1 + 1 × 1 - 3 / 2 = -1 / 2,为负数,因此输出为 0。右下神经元计算 0 × 1 + 1 × 1 - 1 / 2 = 1 / 2,为正数,因此输出为 1。输出神经元接收前两个神经元的输出作为输入,因此计算 0 × (-1) + 1 × 1 - 1 / 2 = 1 / 2。这是正数,因此输出为 1。

⁹ 在 20 世纪 90 年代,具有两个以上隐藏层的人工神经网络被认为是深度的。如今,常见的是看到具有数十层甚至数百层的人工神经网络,因此“深度”的定义非常模糊。

¹⁰ 大卫·鲁梅尔哈特等人,“通过误差传播学习内部表示”(国防技术信息中心技术报告,1985 年 9 月)。

¹¹ 生物神经元似乎实现了一个大致呈 S 形的激活函数,因此研究人员长时间坚持使用 Sigmoid 函数。但事实证明,在人工神经网络中,ReLU 通常效果更好。这是生物类比可能误导的一个案例。

¹² ONEIROS 项目(开放式神经电子智能机器人操作系统)。Chollet 在 2015 年加入了谷歌,继续领导 Keras 项目。

¹³ PyTorch 的 API 与 Keras 的相似,因此一旦你了解了 Keras,如果你想要的话,切换到 PyTorch 并不困难。PyTorch 在 2018 年的普及程度呈指数增长,这在很大程度上要归功于其简单性和出色的文档,而这些正是 TensorFlow 1.x 当时的主要弱点。然而,TensorFlow 2 和 PyTorch 一样简单,部分原因是它已经将 Keras 作为其官方高级 API,并且开发人员大大简化和清理了其余的 API。文档也已经完全重新组织,现在更容易找到所需的内容。同样,PyTorch 的主要弱点(例如,有限的可移植性和没有计算图分析)在 PyTorch 1.0 中已经得到了很大程度的解决。健康的竞争对每个人都有益。

¹⁴ 您还可以使用 tf.keras.utils.plot_model() 生成模型的图像。

¹⁵ Heng-Tze Cheng 等人,“广泛和深度学习用于推荐系统”第一届深度学习推荐系统研讨会论文集(2016):7–10。

¹⁶ 短路径也可以用于向神经网络提供手动设计的特征。

¹⁷ Keras 模型有一个 output 属性,所以我们不能将其用作主输出层的名称,这就是为什么我们将其重命名为 main_output

¹⁸ 目前这是默认设置,但 Keras 团队正在研究一种可能成为未来默认设置的新格式,因此我更喜欢明确设置格式以保证未来兼容。

¹⁹ Hyperband 实际上比连续减半法更复杂;参见 Lisha Li 等人的论文,“Hyperband: 一种新颖的基于贝叶斯的超参数优化方法”,机器学习研究杂志 18(2018 年 4 月):1–52。

²⁰ Max Jaderberg 等人,“神经网络的基于人口的训练”,arXiv 预印本 arXiv:1711.09846(2017)。

²¹ Dominic Masters 和 Carlo Luschi,“重新审视深度神经网络的小批量训练”,arXiv 预印本 arXiv:1804.07612(2018)。

²² Elad Hoffer 等人,“训练时间更长,泛化效果更好:弥合神经网络大批量训练的泛化差距”,第 31 届国际神经信息处理系统会议论文集(2017):1729–1739。

²³ Priya Goyal 等人,“准确、大型小批量 SGD:在 1 小时内训练 ImageNet”,arXiv 预印本 arXiv:1706.02677(2017)。

²⁴ Leslie N. Smith,“神经网络超参数的纪律性方法:第 1 部分—学习率、批量大小、动量和权重衰减”,arXiv 预印本 arXiv:1803.09820(2018)。

²⁵ 在https://homl.info/extra-anns的在线笔记本中还介绍了一些额外的人工神经网络架构。

第十一章:训练深度神经网络

在第十章中,您构建、训练和微调了您的第一个人工神经网络。但它们是浅层网络,只有几个隐藏层。如果您需要解决一个复杂的问题,比如在高分辨率图像中检测数百种对象,您可能需要训练一个更深的人工神经网络,也许有 10 层或更多层,每一层包含数百个神经元,通过数十万个连接相连。训练深度神经网络并不是一件轻松的事情。以下是您可能遇到的一些问题:

  • 在训练过程中,当反向传播通过 DNN 向后流动时,您可能会面临梯度变得越来越小或越来越大的问题。这两个问题都会使得较低层非常难以训练。
  • 您可能没有足够的训练数据来训练这样一个庞大的网络,或者标记成本太高。
  • 训练可能会非常缓慢。
  • 一个拥有数百万参数的模型会严重增加过拟合训练集的风险,特别是如果训练实例不足或者太嘈杂。

在本章中,我们将逐个讨论这些问题,并提出解决方法。我们将首先探讨梯度消失和梯度爆炸问题以及它们最流行的解决方案。接下来,我们将看看迁移学习和无监督预训练,这可以帮助您解决复杂任务,即使您只有很少的标记数据。然后,我们将讨论各种优化器,可以极大地加快训练大型模型。最后,我们将介绍一些用于大型神经网络的流行正则化技术。

有了这些工具,您将能够训练非常深的网络。欢迎来到深度学习!

梯度消失/爆炸问题

正如在第十章中讨论的那样,反向传播算法的第二阶段是从输出层到输入层,沿途传播错误梯度。一旦算法计算出网络中每个参数相对于成本函数的梯度,它就会使用这些梯度来更新每个参数,进行梯度下降步骤。

不幸的是,随着算法向下进行到更低的层,梯度通常会变得越来越小。结果是,梯度下降更新几乎不会改变较低层的连接权重,训练永远不会收敛到一个好的解决方案。这被称为梯度消失问题。在某些情况下,相反的情况可能发生:梯度会变得越来越大,直到层的权重更新变得非常大,算法发散。这是梯度爆炸问题,最常出现在递归神经网络中(参见第十五章)。更一般地说,深度神经网络受到不稳定梯度的困扰;不同层可能以非常不同的速度学习。

或者在-r 和+r 之间的均匀分布,r = sqrt(3 / fan_avg)

在他们的论文中,Glorot 和 Bengio 提出了一种显著减轻不稳定梯度问题的方法。他们指出,我们需要信号在两个方向上正确地流动:在前向方向进行预测时,以及在反向方向进行反向传播梯度时。我们不希望信号消失,也不希望它爆炸和饱和。为了使信号正确地流动,作者认为每一层的输出方差应该等于其输入方差,并且在反向方向通过一层之后,梯度在前后具有相等的方差(如果您对数学细节感兴趣,请查看论文)。实际上,除非层具有相等数量的输入和输出(这些数字称为层的fan-infan-out),否则不可能保证两者都相等,但 Glorot 和 Bengio 提出了一个在实践中被证明非常有效的良好折衷方案:每层的连接权重必须随机初始化,如方程 11-1 所述,其中fan[avg] = (fan[in] + fan[out]) / 2。这种初始化策略称为Xavier 初始化Glorot 初始化,以论文的第一作者命名。

观察 Sigmoid 激活函数(参见图 11-1),您会发现当输入变大(负或正)时,函数在 0 或 1 处饱和,导数非常接近 0(即曲线在两个极端处平坦)。因此,当反向传播开始时,几乎没有梯度可以通过网络向后传播,存在的微小梯度会随着反向传播通过顶层逐渐稀释,因此对于较低层几乎没有剩余的梯度。

图 11-1。Sigmoid 激活函数饱和

Glorot 和 He 初始化

这种不幸的行为早在很久以前就被经验性地观察到,这也是深度神经网络在 2000 年代初大多被放弃的原因之一。当训练 DNN 时,梯度不稳定的原因并不清楚,但在 2010 年的一篇论文中,Xavier Glorot 和 Yoshua Bengio 揭示了一些端倪。作者发现了一些嫌疑人,包括当时最流行的 Sigmoid(逻辑)激活函数和权重初始化技术的组合(即均值为 0,标准差为 1 的正态分布)。简而言之,他们表明,使用这种激活函数和初始化方案,每一层的输出方差远大于其输入方差。在网络中前进,每一层的方差在每一层之后都会增加,直到激活函数在顶层饱和。实际上,这种饱和现象被 sigmoid 函数的均值为 0.5 而不是 0 所加剧(双曲正切函数的均值为 0,在深度网络中的表现略好于 sigmoid 函数)。

方程 11-1。Glorot 初始化(使用 Sigmoid 激活函数时)

正态分布,均值为 0,方差为σ² = 1 / fan_avg

如果您在方程式 11-1 中用fan[in]替换fan[avg],您将得到 Yann LeCun 在 1990 年代提出的初始化策略。他称之为LeCun 初始化。Genevieve Orr 和 Klaus-Robert Müller 甚至在他们 1998 年的书Neural Networks: Tricks of the Trade(Springer)中推荐了这种方法。当fan[in] = fan[out]时,LeCun 初始化等同于 Glorot 初始化。研究人员花了十多年的时间才意识到这个技巧有多重要。使用 Glorot 初始化可以显著加快训练速度,这是深度学习成功的实践之一。

一些论文提供了不同激活函数的类似策略。这些策略仅在方差的规模和它们是否使用fan[avg]或fan[in]上有所不同,如表 11-1 所示(对于均匀分布,只需使用r=3σ2)。为 ReLU 激活函数及其变体提出的初始化策略称为He 初始化Kaiming 初始化,以论文的第一作者命名。对于 SELU,最好使用 Yann LeCun 的初始化方法,最好使用正态分布。我们将很快介绍所有这些激活函数。

表 11-1。每种激活函数的初始化参数

初始化 激活函数 σ²(正态)
Glorot 无,tanh,sigmoid,softmax 1 / fan[avg]
He ReLU,Leaky ReLU,ELU,GELU,Swish,Mish 2 / fan[in]
LeCun SELU 1 / fan[in]

默认情况下,Keras 使用均匀分布的 Glorot 初始化。当您创建一个层时,您可以通过设置kernel_initializer="he_uniform"kernel_initializer="he_normal"来切换到 He 初始化。

import tensorflow as tf
dense = tf.keras.layers.Dense(50, activation="relu",
                              kernel_initializer="he_normal")

或者,您可以使用VarianceScaling初始化器获得表 11-1 中列出的任何初始化方法,甚至更多。例如,如果您想要使用均匀分布并基于fan[avg](而不是fan[in])进行 He 初始化,您可以使用以下代码:

he_avg_init = tf.keras.initializers.VarianceScaling(scale=2., mode="fan_avg",
                                                    distribution="uniform")
dense = tf.keras.layers.Dense(50, activation="sigmoid",
                              kernel_initializer=he_avg_init)

更好的激活函数

2010 年 Glorot 和 Bengio 的一篇论文中的一个见解是,不稳定梯度的问题在一定程度上是由于激活函数的选择不当。直到那时,大多数人都认为,如果自然界选择在生物神经元中使用大致为 S 形的激活函数,那么它们一定是一个很好的选择。但事实证明,其他激活函数在深度神经网络中表现得更好,特别是 ReLU 激活函数,主要是因为它对于正值不会饱和,而且计算速度非常快。

不幸的是,ReLU 激活函数并不完美。它存在一个称为dying ReLUs的问题:在训练过程中,一些神经元实际上“死亡”,意味着它们停止输出除 0 以外的任何值。在某些情况下,您可能会发现您网络的一半神经元已经死亡,尤其是如果您使用了较大的学习率。当神经元的权重被微调得使得 ReLU 函数的输入(即神经元输入的加权和加上偏置项)在训练集中的所有实例中都为负时,神经元就会死亡。当这种情况发生时,它只会继续输出零,并且梯度下降不再影响它,因为当其输入为负时,ReLU 函数的梯度为零。

为了解决这个问题,您可能希望使用 ReLU 函数的变体,比如leaky ReLU

Leaky ReLU

leaky ReLU 激活函数定义为 LeakyReLUα = max(αz, z)(参见图 11-2)。超参数α定义了函数“泄漏”的程度:它是z < 0 时函数的斜率。对于z < 0,具有斜率的 leaky ReLU 永远不会死亡;它们可能会陷入长时间的昏迷,但最终有机会苏醒。Bing Xu 等人在 2015 年的一篇论文比较了几种 ReLU 激活函数的变体,其中一个结论是,泄漏变体总是优于严格的 ReLU 激活函数。事实上,设置α=0.2(一个巨大的泄漏)似乎比α=0.01(一个小泄漏)表现更好。该论文还评估了随机泄漏 ReLU(RReLU),其中α在训练期间在给定范围内随机选择,并在测试期间固定为平均值。RReLU 表现也相当不错,并似乎作为正则化器,减少了过拟合训练集的风险。最后,该论文评估了参数泄漏 ReLU(PReLU),其中α在训练期间被授权学习:它不再是一个超参数,而是一个可以像其他参数一样通过反向传播修改的参数。据报道,PReLU 在大型图像数据集上明显优于 ReLU,但在较小的数据集上存在过拟合训练集的风险。


图 11-2. Leaky ReLU:类似于 ReLU,但对负值有一个小的斜率

Keras 在tf.keras.layers包中包含了LeakyReLUPReLU类。就像其他 ReLU 变体一样,您应该使用 He 初始化。例如:

leaky_relu = tf.keras.layers.LeakyReLU(alpha=0.2)  # defaults to alpha=0.3
dense = tf.keras.layers.Dense(50, activation=leaky_relu,
                              kernel_initializer="he_normal")

如果您愿意,您也可以在模型中将LeakyReLU作为一个单独的层来使用;对于训练和预测没有任何影响:

model = tf.keras.models.Sequential([
    [...]  # more layers
    tf.keras.layers.Dense(50, kernel_initializer="he_normal"),  # no activation
    tf.keras.layers.LeakyReLU(alpha=0.2),  # activation as a separate layer
    [...]  # more layers
])

对于 PReLU,将LeakyReLU替换为PReLU。目前在 Keras 中没有官方实现 RReLU,但您可以相当容易地实现自己的(要了解如何做到这一点,请参见第十二章末尾的练习)。

ReLU、leaky ReLU 和 PReLU 都存在一个问题,即它们不是平滑函数:它们的导数在z=0 处突然变化。正如我们在第四章中讨论 lasso 时看到的那样,这种不连续性会导致梯度下降在最优点周围反弹,并减慢收敛速度。因此,现在我们将看一些 ReLU 激活函数的平滑变体,从 ELU 和 SELU 开始。

ELU 和 SELU

2015 年,Djork-Arné Clevert 等人提出了一篇论文,提出了一种新的激活函数,称为指数线性单元(ELU),在作者的实验中表现优于所有 ReLU 变体:训练时间缩短,神经网络在测试集上表现更好。方程式 11-2 展示了这个激活函数的定义。

方程式 11-2. ELU 激活函数

ELU α ( z ) = α ( exp ( z ) - 1 ) if z < 0 z if z ≥ 0

ELU 激活函数看起来很像 ReLU 函数(参见图 11-3),但有一些主要区别:

  • z < 0 时,它会取负值,这使得单元的平均输出更接近于 0,并有助于缓解梯度消失问题。超参数α定义了当z是一个较大的负数时 ELU 函数接近的值的相反数。通常设置为 1,但您可以像调整其他超参数一样进行调整。
  • z < 0 时具有非零梯度,避免了死神经元问题。
  • 如果α等于 1,则该函数在任何地方都是平滑的,包括在z = 0 附近,这有助于加快梯度下降的速度,因为它在z = 0 的左右两侧不会反弹太多。

在 Keras 中使用 ELU 就像设置activation="elu"一样简单,与其他 ReLU 变体一样,应该使用 He 初始化。ELU 激活函数的主要缺点是它的计算速度比 ReLU 函数及其变体慢(由于使用了指数函数)。在训练期间更快的收敛速度可能会弥补这种缓慢的计算,但是在测试时,ELU 网络将比 ReLU 网络慢一点。


图 11-3. ELU 和 SELU 激活函数

不久之后,Günter Klambauer 等人在2017 年的一篇论文中介绍了缩放 ELU(SELU)激活函数:正如其名称所示,它是 ELU 激活函数的缩放变体(大约是 ELU 的 1.05 倍,使用α ≈ 1.67)。作者们表明,如果构建一个仅由一堆稠密层(即 MLP)组成的神经网络,并且所有隐藏层使用 SELU 激活函数,那么网络将自标准化:每一层的输出在训练过程中倾向于保持均值为 0,标准差为 1,从而解决了梯度消失/爆炸的问题。因此,SELU 激活函数可能在 MLP 中胜过其他激活函数,尤其是深层网络。要在 Keras 中使用它,只需设置activation="selu"。然而,自标准化发生的条件有一些(请参阅论文进行数学证明):

  • 输入特征必须标准化:均值为 0,标准差为 1。
  • 每个隐藏层的权重必须使用 LeCun 正态初始化。在 Keras 中,这意味着设置kernel_initializer="lecun_normal"
  • 只有在普通 MLP 中才能保证自标准化属性。如果尝试在其他架构中使用 SELU,如循环网络(参见第十五章)或具有跳跃连接(即跳过层的连接,例如在 Wide & Deep 网络中),它可能不会胜过 ELU。
  • 您不能使用正则化技术,如ℓ[1]或ℓ[2]正则化、最大范数、批量归一化或常规的 dropout(这些将在本章后面讨论)。

这些是重要的限制条件,因此尽管 SELU 有所承诺,但并没有获得很大的关注。此外,另外三种激活函数似乎在大多数任务上表现出色:GELU、Swish 和 Mish。

GELU、Swish 和 Mish

GELU是由 Dan Hendrycks 和 Kevin Gimpel 在2016 年的一篇论文中引入的。再次,您可以将其视为 ReLU 激活函数的平滑变体。其定义在方程 11-3 中给出,其中Φ是标准高斯累积分布函数(CDF):Φ(z)对应于从均值为 0、方差为 1 的正态分布中随机抽取的值低于z的概率。

方程 11-3. GELU 激活函数

GELU(z)=zΦ(z)

如您在图 11-4 中所见,GELU 类似于 ReLU:当其输入z非常负时,它接近 0,当z非常正时,它接近z。然而,到目前为止我们讨论的所有激活函数都是凸函数且单调递增的,而 GELU 激活函数则不是:从左到右,它开始直线上升,然后下降,达到大约-0.17 的低点(接近 z≈-0.75),最后反弹上升并最终向右上方直线前进。这种相当复杂的形状以及它在每个点上都有曲率的事实可能解释了为什么它效果如此好,尤其是对于复杂任务:梯度下降可能更容易拟合复杂模式。在实践中,它通常优于迄今讨论的任何其他激活函数。然而,它的计算成本稍高,提供的性能提升并不总是足以证明额外成本的必要性。尽管如此,可以证明它大致等于zσ(1.702 z),其中σ是 sigmoid 函数:使用这个近似也非常有效,并且计算速度更快。


图 11-4. GELU、Swish、参数化 Swish 和 Mish 激活函数

GELU 论文还介绍了sigmoid linear unit(SiLU)激活函数,它等于zσ(z),但在作者的测试中被 GELU 表现得更好。有趣的是,Prajit Ramachandran 等人在2017 年的一篇论文中重新发现了 SiLU 函数,通过自动搜索好的激活函数。作者将其命名为Swish,这个名字很受欢迎。在他们的论文中,Swish 表现优于其他所有函数,包括 GELU。Ramachandran 等人后来通过添加额外的超参数β来推广 Swish,用于缩放 sigmoid 函数的输入。推广后的 Swish 函数为 Swishβ = zσ(βz),因此 GELU 大致等于使用β = 1.702 的推广 Swish 函数。您可以像调整其他超参数一样调整β。另外,也可以将β设置为可训练的,让梯度下降来优化它:这样可以使您的模型更加强大,但也会有过拟合数据的风险。

另一个相当相似的激活函数是Mish,它是由 Diganta Misra 在2019 年的一篇论文中引入的。它被定义为 mish(z) = ztanh(softplus(z)),其中 softplus(z) = log(1 + exp(z))。就像 GELU 和 Swish 一样,它是 ReLU 的平滑、非凸、非单调变体,作者再次进行了许多实验,并发现 Mish 通常优于其他激活函数,甚至比 Swish 和 GELU 稍微好一点。图 11-4 展示了 GELU、Swish(默认β = 1 和β = 0.6)、最后是 Mish。如您所见,当z为负时,Mish 几乎完全重叠于 Swish,当z为正时,几乎完全重叠于 GELU。

提示

那么,对于深度神经网络的隐藏层,你应该使用哪种激活函数?对于简单任务,ReLU 仍然是一个很好的默认选择:它通常和更复杂的激活函数一样好,而且计算速度非常快,许多库和硬件加速器提供了 ReLU 特定的优化。然而,对于更复杂的任务,Swish 可能是更好的默认选择,甚至可以尝试带有可学习β参数的参数化 Swish 来处理最复杂的任务。Mish 可能会给出稍微更好的结果,但需要更多的计算。如果你非常关心运行时延迟,那么你可能更喜欢 leaky ReLU,或者对于更复杂的任务,可以使用参数化 leaky ReLU。对于深度 MLP,可以尝试使用 SELU,但一定要遵守之前列出的约束条件。如果你有多余的时间和计算能力,也可以使用交叉验证来评估其他激活函数。

Keras 支持 GELU 和 Swish,只需使用activation="gelu"activation="swish"。然而,它目前不支持 Mish 或广义 Swish 激活函数(但请参阅第十二章了解如何实现自己的激活函数和层)。

激活函数就介绍到这里!现在,让我们看一种完全不同的解决不稳定梯度问题的方法:批量归一化。


Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(四)(3)https://developer.aliyun.com/article/1482423


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
机器学习/深度学习 TensorFlow 算法框架/工具
深度学习之格式转换笔记(三):keras(.hdf5)模型转TensorFlow(.pb) 转TensorRT(.uff)格式
将Keras训练好的.hdf5模型转换为TensorFlow的.pb模型,然后再转换为TensorRT支持的.uff格式,并提供了转换代码和测试步骤。
87 3
深度学习之格式转换笔记(三):keras(.hdf5)模型转TensorFlow(.pb) 转TensorRT(.uff)格式
|
11天前
|
机器学习/深度学习 人工智能 算法
【手写数字识别】Python+深度学习+机器学习+人工智能+TensorFlow+算法模型
手写数字识别系统,使用Python作为主要开发语言,基于深度学习TensorFlow框架,搭建卷积神经网络算法。并通过对数据集进行训练,最后得到一个识别精度较高的模型。并基于Flask框架,开发网页端操作平台,实现用户上传一张图片识别其名称。
38 0
【手写数字识别】Python+深度学习+机器学习+人工智能+TensorFlow+算法模型
|
22天前
|
机器学习/深度学习 TensorFlow API
机器学习实战:TensorFlow在图像识别中的应用探索
【10月更文挑战第28天】随着深度学习技术的发展,图像识别取得了显著进步。TensorFlow作为Google开源的机器学习框架,凭借其强大的功能和灵活的API,在图像识别任务中广泛应用。本文通过实战案例,探讨TensorFlow在图像识别中的优势与挑战,展示如何使用TensorFlow构建和训练卷积神经网络(CNN),并评估模型的性能。尽管面临学习曲线和资源消耗等挑战,TensorFlow仍展现出广阔的应用前景。
49 5
|
1月前
|
机器学习/深度学习 人工智能 算法
【玉米病害识别】Python+卷积神经网络算法+人工智能+深度学习+计算机课设项目+TensorFlow+模型训练
玉米病害识别系统,本系统使用Python作为主要开发语言,通过收集了8种常见的玉米叶部病害图片数据集('矮花叶病', '健康', '灰斑病一般', '灰斑病严重', '锈病一般', '锈病严重', '叶斑病一般', '叶斑病严重'),然后基于TensorFlow搭建卷积神经网络算法模型,通过对数据集进行多轮迭代训练,最后得到一个识别精度较高的模型文件。再使用Django搭建Web网页操作平台,实现用户上传一张玉米病害图片识别其名称。
59 0
【玉米病害识别】Python+卷积神经网络算法+人工智能+深度学习+计算机课设项目+TensorFlow+模型训练
|
2月前
|
机器学习/深度学习 算法 TensorFlow
交通标志识别系统Python+卷积神经网络算法+深度学习人工智能+TensorFlow模型训练+计算机课设项目+Django网页界面
交通标志识别系统。本系统使用Python作为主要编程语言,在交通标志图像识别功能实现中,基于TensorFlow搭建卷积神经网络算法模型,通过对收集到的58种常见的交通标志图像作为数据集,进行迭代训练最后得到一个识别精度较高的模型文件,然后保存为本地的h5格式文件。再使用Django开发Web网页端操作界面,实现用户上传一张交通标志图片,识别其名称。
108 6
交通标志识别系统Python+卷积神经网络算法+深度学习人工智能+TensorFlow模型训练+计算机课设项目+Django网页界面
|
1月前
|
机器学习/深度学习 TensorFlow API
使用 TensorFlow 和 Keras 构建图像分类器
【10月更文挑战第2天】使用 TensorFlow 和 Keras 构建图像分类器
|
1月前
|
机器学习/深度学习 移动开发 TensorFlow
深度学习之格式转换笔记(四):Keras(.h5)模型转化为TensorFlow(.pb)模型
本文介绍了如何使用Python脚本将Keras模型转换为TensorFlow的.pb格式模型,包括加载模型、重命名输出节点和量化等步骤,以便在TensorFlow中进行部署和推理。
83 0
|
1月前
|
机器学习/深度学习 算法 数据可视化
【机器学习】决策树------迅速了解其基本思想,Sklearn的决策树API及构建决策树的步骤!!!
【机器学习】决策树------迅速了解其基本思想,Sklearn的决策树API及构建决策树的步骤!!!
|
3月前
|
持续交付 测试技术 jenkins
JSF 邂逅持续集成,紧跟技术热点潮流,开启高效开发之旅,引发开发者强烈情感共鸣
【8月更文挑战第31天】在快速发展的软件开发领域,JavaServer Faces(JSF)这一强大的Java Web应用框架与持续集成(CI)结合,可显著提升开发效率及软件质量。持续集成通过频繁的代码集成及自动化构建测试,实现快速反馈、高质量代码、加强团队协作及简化部署流程。以Jenkins为例,配合Maven或Gradle,可轻松搭建JSF项目的CI环境,通过JUnit和Selenium编写自动化测试,确保每次构建的稳定性和正确性。
62 0
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
【MM2024】阿里云 PAI 团队图像编辑算法论文入选 MM2024
阿里云人工智能平台 PAI 团队发表的图像编辑算法论文在 MM2024 上正式亮相发表。ACM MM(ACM国际多媒体会议)是国际多媒体领域的顶级会议,旨在为研究人员、工程师和行业专家提供一个交流平台,以展示在多媒体领域的最新研究成果、技术进展和应用案例。其主题涵盖了图像处理、视频分析、音频处理、社交媒体和多媒体系统等广泛领域。此次入选标志着阿里云人工智能平台 PAI 在图像编辑算法方面的研究获得了学术界的充分认可。
【MM2024】阿里云 PAI 团队图像编辑算法论文入选 MM2024
下一篇
无影云桌面