高级的深度学习最佳实践
序
本章将介绍几种强大的工具,可以让你朝着针对困难问题来开发最先进模型这一目标更近 一步。利用 Keras 函数式 API,你可以构建类图(graph-like)模型、在不同的输入之间共享某一层, 并且还可以像使用 Python 函数一样使用 Keras 模型。Keras 回调函数和 TensorBoard 基于浏览器 的可视化工具,让你可以在训练过程中监控模型。我们还会讨论其他几种最佳实践,包括批标 准化、残差连接、超参数优化和模型集成。
- Keras 函数式 API
- 使用 Keras 回调函数
- 使用 TensorBoard 可视化工具
- 开发最先进模型的重要最佳实践
1 不用 Sequential 模型的解决方案:Keras 函数式 API
到目前为止,本书介绍的所有神经网络都是用 Sequential 模型实现的。Sequential 模 型假设,网络只有一个输入和一个输出,而且网络是层的线性堆叠(见图 7-1)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DFumbPcR-1672156042004)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227205051532.png)]
这是一个经过普遍验证的假设。这种网络配置非常常见,以至于本书前面只用 Sequential 模型类就能够涵盖许多主题和实际应用。但有些情况下这种假设过于死板。有些网络需要多个独立的输入,有些网络则需要多个输出,而有些网络在层与层之间具有内部分支,这使得网络 看起来像是层构成的图(graph),而不是层的线性堆叠。
例如,有些任务需要多模态(multimodal)输入。这些任务合并来自不同输入源的数据,并使用不同类型的神经层处理不同类型的数据。假设有一个深度学习模型,试图利用下列输入来 预测一件二手衣服最可能的市场价格:用户提供的元数据(比如商品品牌、已使用年限等)、用 户提供的文本描述与商品照片。如果你只有元数据,那么可以使用 one-hot 编码,然后用密集 连接网络来预测价格。如果你只有文本描述,那么可以使用循环神经网络或一维卷积神经网络。 如果你只有图像,那么可以使用二维卷积神经网络。但怎么才能同时使用这三种数据呢?一种 朴素的方法是训练三个独立的模型,然后对三者的预测做加权平均。但这种方法可能不是最优的, 因为模型提取的信息可能存在冗余。更好的方法是使用一个可以同时查看所有可用的输入模态的模型,从而联合学习一个更加精确的数据模型——这个模型具有三个输入分支(见图 7-2)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uj2F8ghN-1672156042004)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227205322375.png)]
同样,有些任务需要预测输入数据的多个目标属性。给定一部小说的文本,你可能希望将 它按类别自动分类(比如爱情小说或惊悚小说),同时还希望预测其大致的写作日期。当然,你 可以训练两个独立的模型:一个用于划分类别,一个用于预测日期。但由于这些属性并不是统计无关的,你可以构建一个更好的模型,用这个模型来学习同时预测类别和日期。这种联合模 型将有两个输出,或者说两个头(head,见图 7-3)。因为类别和日期之间具有相关性,所以知 道小说的写作日期有助于模型在小说类别的空间中学到丰富而又准确的表示,反之亦然。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GPwOQ7Zi-1672156042005)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227205904937.png)]
此外,许多最新开发的神经架构要求非线性的网络拓扑结构,即网络结构为有向无环图。 比如,Inception 系列网络(由 Google 的 Szegedy 等人开发)a 依赖于 Inception 模块,其输入被 多个并行的卷积分支所处理,然后将这些分支的输出合并为单个张量(见图 7-4)。最近还有一 种趋势是向模型中添加残差连接(residual connection),它最早出现于 ResNet 系列网络(由微 软的何恺明等人开发)。b 残差连接是将前面的输出张量与后面的输出张量相加,从而将前面的 表示重新注入下游数据流中(见图 7-5),这有助于防止信息处理流程中的信息损失。这种类图网络还有许多其他示例。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kWG3CfCy-1672156042005)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227210510819.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SHi3iXsv-1672156042005)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227210517053.png)]
这三个重要的使用案例(多输入模型、多输出模型和类图模型),只用 Keras 中的 Sequential 模型类是无法实现的。但是还有另一种更加通用、更加灵活的使用 Keras 的方式,就是函数式 API(functional API)。本节将会详细介绍函数式 API 是什么、能做什么以及如何使用它。
1.1 函数式 API 简介
使用函数式 API,你可以直接操作张量,也可以把层当作函数来使用,接收张量并返回张量(因此得名函数式 API)。
from keras import Input, layers
input_tensor = Input(shape=(32,)) # 一个张量
dense = layers.Dense(32, activation='relu') # 一个层是一个函数
output_tensor = dense(input_tensor) # 可以在一个张量上调用一个层,它会返回一个张量
我们首先来看一个最简单的示例,并列展示一个简单的 Sequential 模型以及对应的函数式 API 实现。
from keras.models import Sequential, Model
from keras import layers
from keras import Input
# 前面学过的 Sequential 模型
seq_model = Sequential()
seq_model.add(layers.Dense(32, activation='relu', input_shape=(64,)))
seq_model.add(layers.Dense(32, activation='relu'))
seq_model.add(layers.Dense(10, activation='softmax'))
# 对应的函数式 API 实现
input_tensor = Input(shape=(64,))
x = layers.Dense(32, activation='relu')(input_tensor)
x = layers.Dense(32, activation='relu')(x)
output_tensor = layers.Dense(10, activation='softmax')(x)
# Model 类将输入张量和输出张量转换为一个模型
model = Model(input_tensor, output_tensor)
model.summary()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YyCoxYol-1672156042006)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227210835113.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q74OmOlb-1672156042006)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227210915515.png)]
这里只有一点可能看起来有点神奇,就是将 Model 对象实例化只用了一个输入张量和 一个输出张量。Keras 会在后台检索从 input_tensor 到 output_tensor 所包含的每一层, 并将这些层组合成一个类图的数据结构,即一个 Model。当然,这种方法有效的原因在于, output_tensor 是通过对 input_tensor 进行多次变换得到的。如果你试图利用不相关的输 入和输出来构建一个模型,那么会得到 RuntimeError。
对这种 Model 实例进行编译、训练或评估时,其 API 与 Sequential 模型相同。
model.compile(optimizer='rmsprop', loss='categorical_crossentropy') # 编译模型
# 生成用于训练的虚构 Numpy 数据
import numpy as np
x_train = np.random.random((1000, 64))
y_train = np.random.random((1000, 10))
# 训练 10 轮模型
model.fit(x_train, y_train, epochs=10, batch_size=128)
# 评估模型
score = model.evaluate(x_train, y_train)
1.2 多输入模型
函数式 API 可用于构建具有多个输入的模型。通常情况下,这种模型会在某一时刻用一个 可以组合多个张量的层将不同的输入分支合并,张量组合方式可能是相加、连接等。这通常利 用 Keras 的合并运算来实现,比如 keras.layers.add、keras.layers.concatenate 等。 我们来看一个非常简单的多输入模型示例——一个问答模型。
典型的问答模型有两个输入:一个自然语言描述的问题和一个文本片段(比如新闻文章), 后者提供用于回答问题的信息。然后模型要生成一个回答,在最简单的情况下,这个回答只包含一个词,可以通过对某个预定义的词表做 softmax 得到(见图 7-6)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yT4pCyiF-1672156042006)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227211240982.png)]
下面这个示例展示了如何用函数式 API 构建这样的模型。我们设置了两个独立分支,首先将文本输入和问题输入分别编码为表示向量,然后连接这些向量,最后,在连接好的表示上添加一个 softmax 分类器。
用函数式 API 实现双输入问答模型
from keras.models import Model
from keras import layers
from keras import Input
text_vocabulary_size = 10000
question_vocabulary_size = 10000
answer_vocabulary_size = 500
# 文本输入是一个长度可变的整数序列。注意,你可以选择对输入进行命名
text_input = Input(shape=(None,), dtype='int32', name='text')
# 将输入嵌入长度为 64 的向量
embedded_text = layers.Embedding(text_vocabulary_size, 64)(text_input)
# 利用 LSTM 将向量编码为单个向量
encoded_text = layers.LSTM(32)(embedded_text)
# 对问题进行相同的处理(使用不同的层实例)
question_input = Input(shape=(None,), dtype='int32', name='question')
embedded_question = layers.Embedding(question_vocabulary_size, 32)(question_input)
encoded_question = layers.LSTM(16)(embedded_question)
# 将编码后的问题和文本连接起来
concatenated = layers.concatenate([encoded_text, encoded_question],axis=-1)
# 在上面添加一个softmax 分类器
answer = layers.Dense(answer_vocabulary_size, activation='softmax')(concatenated)
# 在模型实例化时,指定两个输入和输出
model = Model([text_input, question_input], answer)
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['acc'])
接下来要如何训练这个双输入模型呢?有两个可用的 API:我们可以向模型输入一个由 Numpy 数组组成的列表,或者也可以输入一个将输入名称映射为 Numpy 数组的字典。当然,只有输入具有名称时才能使用后一种方法。
将数据输入到多输入模型中
import numpy as np
num_samples = 1000
max_length = 100
# 生成虚构的 Numpy数据
text = np.random.randint(1, text_vocabulary_size, size=(num_samples, max_length))
question = np.random.randint(1, question_vocabulary_size,
size=(num_samples, max_length))
# 回答是 one-hot 编码的,不是整数
answers = np.random.randint(answer_vocabulary_size, size=(num_samples))
answers = keras.utils.to_categorical(answers, answer_vocabulary_size)
# 使用输入组成的列表来拟合
model.fit([text, question], answers, epochs=10, batch_size=128)
# 使用输入组成的字典来拟合(只有对输入进行命名之后才能用这种方法)
model.fit({'text': text, 'question': question}, answers, epochs=10, batch_size=128)
1.3 多输出模型
利用相同的方法,我们还可以使用函数式 API 来构建具有多个输出(或多头)的模型。一 个简单的例子就是一个网络试图同时预测数据的不同性质,比如一个网络,输入某个匿名人士的一系列社交媒体发帖,然后尝试预测那个人的属性,比如年龄、性别和收入水平(见图 7-7)。
用函数式 API 实现一个三输出模型
from keras import layers
from keras import Input
from keras.models import Model
vocabulary_size = 50000
num_income_groups = 10
posts_input = Input(shape=(None,), dtype='int32', name='posts')
embedded_posts = layers.Embedding(256, vocabulary_size)(posts_input)
x = layers.Conv1D(128, 5, activation='relu')(embedded_posts)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.GlobalMaxPooling1D()(x)
x = layers.Dense(128, activation='relu')(x)
# 注意,输出层都具有名称
age_prediction = layers.Dense(1, name='age')(x)
income_prediction = layers.Dense(num_income_groups,
activation='softmax',
name='income')(x)
gender_prediction = layers.Dense(1, activation='sigmoid', name='gender')(x)
model = Model(posts_input, [age_prediction, income_prediction, gender_prediction])
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6OCFhwoc-1672156042007)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227212622636.png)]
重要的是,训练这种模型需要能够对网络的各个头指定不同的损失函数,例如,年龄预测 是标量回归任务,而性别预测是二分类任务,二者需要不同的训练过程。但是,梯度下降要求 将一个标量最小化,所以为了能够训练模型,我们必须将这些损失合并为单个标量。合并不同 损失最简单的方法就是对所有损失求和。在 Keras 中,你可以在编译时使用损失组成的列表或 字典来为不同输出指定不同损失,然后将得到的损失值相加得到一个全局损失,并在训练过程 中将这个损失最小化。
多输出模型的编译选项:多重损失
model.compile(optimizer='rmsprop',
loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'])
# 与上述写法等效(只有输出层具有名称时才能采用这种写法)
model.compile(optimizer='rmsprop',
loss={'age': 'mse', 'income': 'categorical_crossentropy', 'gender': 'binary_crossentropy'})
注意,严重不平衡的损失贡献会导致模型表示针对单个损失值最大的任务优先进行优化, 而不考虑其他任务的优化。为了解决这个问题,我们可以为每个损失值对最终损失的贡献分配 不同大小的重要性。如果不同的损失值具有不同的取值范围,那么这一方法尤其有用。比如, 用于年龄回归任务的均方误差(MSE)损失值通常在 3~5 左右,而用于性别分类任务的交叉熵损失值可能低至 0.1。在这种情况下,为了平衡不同损失的贡献,我们可以让交叉熵损失的权重取 10,而 MSE 损失的权重取 0.5。
多输出模型的编译选项:损失加权
model.compile(optimizer='rmsprop',
loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'],
loss_weights=[0.25, 1., 10.])
# 与上述写法等效(只有输出层具有名称时才能采用这种写法)
model.compile(optimizer='rmsprop',
loss={'age': 'mse',
'income': 'categorical_crossentropy',
'gender': 'binary_crossentropy'},
loss_weights={'age': 0.25,
'income': 1.,
'gender': 10.})
将数据输入到多输出模型中
model.fit(posts, [age_targets, income_targets, gender_targets],
epochs=10, batch_size=64)
model.fit(posts, {'age': age_targets,
'income': income_targets,
'gender': gender_targets},
epochs=10, batch_size=64)
1.4 层组成的有向无环图
利用函数式 API,我们不仅可以构建多输入和多输出的模型,而且还可以实现具有复杂 的内部拓扑结构的网络。Keras 中的神经网络可以是层组成的任意有向无环图(directed acyclic graph)。无环(acyclic)这个限定词很重要,即这些图不能有循环。张量 x 不能成为生成 x 的某一层的输入。唯一允许的处理循环(即循环连接)是循环层的内部循环。
一些常见的神经网络组件都以图的形式实现。两个著名的组件是 Inception 模块和残差连接。 为了更好地理解如何使用函数式 API 来构建层组成的图,我们来看一下如何用 Keras 实现这二者。
- Inception 模块
Inception 是一种流行的卷积神经网络的架构类型,它由 Google 的 Christian Szegedy 及其 同事在 2013—2014 年开发,其灵感来源于早期的 network-in-network 架构。它是模块的堆叠, 这些模块本身看起来像是小型的独立网络,被分为多个并行分支。
Inception 模块最基本的形式 包含 3~4 个分支,首先是一个 1×1 的卷积,然后是一个 3×3 的卷积,最后将所得到的特征连 接在一起。这种设置有助于网络分别学习空间特征和逐通道的特征,这比联合学习这两种特征更 加有效。Inception 模块也可能具有更复杂的形式,通常会包含池化运算、不同尺寸的空间卷积(比如在某些分支上使用 5×5 的卷积代替 3×3 的卷积)和不包含空间卷积的分支(只有一个 1×1 卷积)。图 7-8 给出了这种模块的一个示例,它来自于 Inception V3。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4rMgwWWu-1672156042007)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227213430352.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0WOWD11W-1672156042007)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227213513743.png)]
使用函数式 API 可以实现图 7-8 中的模块,其代码如下所示。这个例子假设我们有一个四 维输入张量 x。
from keras import layers
# 每个分支都有相同的步幅值(2),这对于保持所有分支输出具有相同的尺寸是很有必要的,这样你才能将它们连接在一起
branch_a = layers.Conv2D(128, 1, activation='relu', strides=2)(x)
# 在这个分支中,空间卷积层用到了步幅
branch_b = layers.Conv2D(128, 1, activation='relu')(x)
branch_b = layers.Conv2D(128, 3, activation='relu', strides=2)(branch_b)
# 在这个分支中,平均池化层用到了步幅
branch_c = layers.AveragePooling2D(3, strides=2)(x)
branch_c = layers.Conv2D(128, 3, activation='relu')(branch_c)
# 在这个分支中,空间卷积层用到了步幅
branch_d = layers.Conv2D(128, 1, activation='relu')(x)
branch_d = layers.Conv2D(128, 3, activation='relu')(branch_d)
branch_d = layers.Conv2D(128, 3, activation='relu', strides=2)(branch_d)
# 将分支输出连接在一起,得到模块输出
output = layers.concatenate([branch_a, branch_b, branch_c, branch_d], axis=-1)
注意,完整的Inception V3架构内置于Keras中,位置在keras.applications.inception_v3. InceptionV3,其中包括在 ImageNet 数据集上预训练得到的权重。与其密切相关的另一个模 型是 Xception,a 它也是 Keras 的 applications 模块的一部分。Xception 代表极端 Inception (extreme inception),它是一种卷积神经网络架构,其灵感可能来自于 Inception。Xception 将分 别进行通道特征学习与空间特征学习的想法推向逻辑上的极端,并将 Inception 模块替换为深度 可分离卷积,其中包括一个逐深度卷积(即一个空间卷积,分别对每个输入通道进行处理)和 后面的一个逐点卷积(即一个 1×1 卷积)。这个深度可分离卷积实际上是 Inception 模块的一种 极端形式,其空间特征和通道特征被完全分离。Xception 的参数个数与 Inception V3 大致相同, 但因为它对模型参数的使用更加高效,所以在 ImageNet 以及其他大规模数据集上的运行性能更 好,精度也更高。
- 残差连接
残差连接(residual connection)是一种常见的类图网络组件,在 2015 年之后的许多网络架构 (包括 Xception)中都可以见到。2015 年末,来自微软的何恺明等人在 ILSVRC ImageNet 挑战赛 中获胜 b,其中引入了这一方法。残差连接解决了困扰所有大规模深度学习模型的两个共性问题: 梯度消失和表示瓶颈。通常来说,向任何多于 10 层的模型中添加残差连接,都可能会有所帮助。
残差连接是让前面某层的输出作为后面某层的输入,从而在序列网络中有效地创造了一条 捷径。前面层的输出没有与后面层的激活连接在一起,而是与后面层的激活相加(这里假设两 个激活的形状相同)。如果它们的形状不同,我们可以用一个线性变换将前面层的激活改变成目 标形状(例如,这个线性变换可以是不带激活的 Dense 层;对于卷积特征图,可以是不带激活 1×1 卷积)。
如果特征图的尺寸相同,在 Keras 中实现残差连接的方法如下,用的是恒等残差连接(identity residual connection)。这个例子假设我们有一个四维输入张量 x。
from keras import layers
x = ...
# 对 x 进行变换
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
# 将原始 x 与输出特征相加
y = layers.add([y, x])
如果特征图的尺寸不同,实现残差连接的方法如下,用的是线性残差连接(linear residual connection)。同样,假设我们有一个四维输入张量 x。
from keras import layers
x = ...
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
y = layers.MaxPooling2D(2, strides=2)(y)
# 使用 1×1 卷积,将原始 x 张量线性下采样为与 y 具有相同的形状
residual = layers.Conv2D(128, 1, strides=2, padding='same')(x)
# 将残差张量与输出特征相加
y = layers.add([y, residual])
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f1I6gmEW-1672156042008)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227220544141.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ooi9hD95-1672156042008)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227220632679.png)]
1.5 共享层权重
函数式 API 还有一个重要特性,那就是能够多次重复使用一个层实例。如果你对一个层实例调用两次,而不是每次调用都实例化一个新层,那么每次调用可以重复使用相同的权重。这样你可以构建具有共享分支的模型,即几个分支全都共享相同的知识并执行相同的运算。也就是说,这些分支共享相同的表示,并同时对不同的输入集合学习这些表示。
举个例子,假设一个模型想要评估两个句子之间的语义相似度。这个模型有两个输入(需要比较的两个句子),并输出一个范围在 0~1 的分数,0 表示两个句子毫不相关,1 表示两个句子完全相同或只是换一种表述。这种模型在许多应用中都很有用,其中包括在对话系统中删除重复的自然语言查询。
在这种设置下,两个输入句子是可以互换的,因为语义相似度是一种对称关系,A 相对于 B 的相似度等于 B 相对于 A 的相似度。因此,学习两个单独的模型来分别处理两个输入句子是没有道理的。相反,你需要用一个 LSTM 层来处理两个句子。这个 LSTM 层的表示(即它的权重)是同时基于两个输入来学习的。我们将其称为连体 LSTM(Siamese LSTM)或共享 LSTM(shared LSTM)模型。
使用 Keras 函数式 API 中的层共享(层重复使用)可以实现这样的模型,其代码如下所示。
from keras import layers
from keras import Input
from keras.models import Model
lstm = layers.LSTM(32) # 将一个 LSTM 层实例化一次
left_input = Input(shape=(None, 128)) # 构建模型的左分支:输入是长度 128 的向量组成的变长序列
left_output = lstm(left_input)
right_input = Input(shape=(None, 128)) # 构建模型的右分支:如果调用已有的层实例,那么就会重复使用它的权重
right_output = lstm(right_input)
merged = layers.concatenate([left_output, right_output], axis=-1) # 在上面构建一个分类器
predictions = layers.Dense(1, activation='sigmoid')(merged)
model = Model([left_input, right_input], predictions) # 将模型实例化并训练:训练这种模型时,基于两个输入对LSTM层的权重进行更新
model.fit([left_data, right_data], targets)
自然地,一个层实例可能被多次重复使用,它可以被调用任意多次,每次都重复使用一组相同的权重。
1.6 将模型作为层
重要的是,在函数式 API 中,可以像使用层一样使用模型。实际上,你可以将模型看作“更大的层”。Sequential 类和 Model 类都是如此。这意味着你可以在一个输入张量上调用模型, 并得到一个输出张量。
y = model(x)
如果模型具有多个输入张量和多个输出张量,那么应该用张量列表来调用模型。
y1, y2 = model([x1, x2])
在调用模型实例时,就是在重复使用模型的权重,正如在调用层实例时,就是在重复使用层的权重。调用一个实例,无论是层实例还是模型实例,都会重复使用这个实例已经学到的表示, 这很直观。
通过重复使用模型实例可以构建一个简单的例子,就是一个使用双摄像头作为输入的视觉 模型:两个平行的摄像头,相距几厘米(一英寸)。这样的模型可以感知深度,这在很多应用中 都很有用。你不需要两个单独的模型从左右两个摄像头中分别提取视觉特征,然后再将二者合并。 这样的底层处理可以在两个输入之间共享,即通过共享层(使用相同的权重,从而共享相同的 表示)来实现。在 Keras 中实现连体视觉模型(共享卷积基)的代码如下所示。
from keras import layers
from keras import applications
from keras import Input
xception_base = applications.Xception(weights=None, include_top=False) # 图像处理基础模型是Xception 网络(只包括卷积基)
# 输入是 250×250 的 RGB 图像
left_input = Input(shape=(250, 250, 3))
right_input = Input(shape=(250, 250, 3))
# 对相同的视觉模型调用两次
left_features = xception_base(left_input)
right_features = xception_base(right_input)
# 合并后的特征包含来自左右两个视觉输入中的信息
merged_features = layers.concatenate([left_features, right_features], axis=-1)
1.7 小结
以上就是对 Keras 函数式 API 的介绍,它是构建高级深度神经网络架构的必备工具。
本节我们学习了以下内容。
-
如果你需要实现的架构不仅仅是层的线性堆叠,那么不要局限于 Sequential API。
-
如何使用 Keras 函数式 API 来构建多输入模型、多输出模型和具有复杂的内部网络拓扑结构的模型。
-
如何通过多次调用相同的层实例或模型实例,在不同的处理分支之间重复使用层或模型的权重。
2 使用 Keras 回调函数和 TensorBoard 来检查并监控深度学习模型
本节将介绍在训练过程中如何更好地访问并控制模型内部过程的方法。使用 model.fit() 或 model.fit_generator() 在一个大型数据集上启动数十轮的训练,有点类似于扔一架纸飞机,一开始给它一点推力,之后你便再也无法控制其飞行轨迹或着陆点。如果想要避免不好的结果(并避免浪费纸飞机),更聪明的做法是不用纸飞机,而是用一架无人机,它可以感知其环 境,将数据发回给操纵者,并且能够基于当前状态自主航行。我们下面要介绍的技术,可以让 model.fit() 的调用从纸飞机变为智能的自主无人机,可以自我反省并动态地采取行动。
2.1 训练过程中将回调函数作用于模型
训练模型时,很多事情一开始都无法预测。尤其是你不知道需要多少轮才能得到最佳验证 损失。前面所有例子都采用这样一种策略:训练足够多的轮次,这时模型已经开始过拟合,根 据这第一次运行来确定训练所需要的正确轮数,然后使用这个最佳轮数从头开始再启动一次新 的训练。当然,这种方法很浪费。
处理这个问题的更好方法是,当观测到验证损失不再改善时就停止训练。这可以使用 Keras 回调函数来实现。回调函数(callback)是在调用 fit 时传入模型的一个对象(即实现特定方法的类实例),它在训练过程中的不同时间点都会被模型调用。它可以访问关于模型状态与性能的所有可用数据,还可以采取行动:中断训练、保存模型、加载一组不同的权重或改变模型的状态。
回调函数的一些用法示例如下所示。
模型检查点(model checkpointing):在训练过程中的不同时间点保存模型的当前权重。
提前终止(early stopping):如果验证损失不再改善,则中断训练(当然,同时保存在训练过程中得到的最佳模型)。
在训练过程中动态调节某些参数值:比如优化器的学习率。
在训练过程中记录训练指标和验证指标,或将模型学到的表示可视化(这些表示也在不断更新):你熟悉的 Keras 进度条就是一个回调函数!
keras.callbacks 模块包含许多内置的回调函数,下面列出了其中一些,但还有很多没 有列出来。
keras.callbacks.ModelCheckpoint
keras.callbacks.EarlyStopping
keras.callbacks.LearningRateScheduler
keras.callbacks.ReduceLROnPlateau
keras.callbacks.CSVLogger
下面介绍其中几个回调函数,让你了解如何使用它们:ModelCheckpoint、EarlyStopping 和 ReduceLROnPlateau。
- ModelCheckpoint 与 EarlyStopping 回调函数
如果监控的目标指标在设定的轮数内不再改善,可以用 EarlyStopping 回调函数来中断训练。比如,这个回调函数可以在刚开始过拟合的时候就中断训练,从而避免用更少的轮次重 新训练模型。这个回调函数通常与 ModelCheckpoint 结合使用,后者可以在训练过程中持续 不断地保存模型(你也可以选择只保存目前的最佳模型,即一轮结束后具有最佳性能的模型)。
import keras
# 通过 fit 的 callbacks 参数将回调函数传入模型中,这个参数接收一个回调函数的列表。你可以传入任意个数的回调函数
callbacks_list = [
keras.callbacks.EarlyStopping( # 如果不再改善,就中断训练
monitor='acc', # 监控模型的验证精度
patience=1, # 如果精度在多于一轮的时间(即两轮)内不再改善,中断训练
),
keras.callbacks.ModelCheckpoint( # 在每轮过后保存当前权重
filepath='my_model.h5', # 目标模型文件的保存路径
monitor='val_loss', # 这两个参数的含义是,如果 val_loss 没有改善,那么不需要覆盖模型文件。这就可以始终保存在训练过程中见到的最佳模型
save_best_only=True,
)
]
model.compile(optimizer='rmsprop',
loss='binary_crossentropy',
metrics=['acc']) # 你监控精度,所以它应该是模型指标的一部分
# 注意,由于回调函数要监控验证损失和验证精度,所以在调用 fit 时需要传入 validation_data(验证数据)
model.fit(x, y,
epochs=10,
batch_size=32,
callbacks=callbacks_list,
validation_data=(x_val, y_val))
- ReduceLROnPlateau 回调函数
如果验证损失不再改善,你可以使用这个回调函数来降低学习率。在训练过程中如果出现 了损失平台(loss plateau),那么增大或减小学习率都是跳出局部最小值的有效策略。下面这个示例使用了 ReduceLROnPlateau 回调函数。
callbacks_list = [
keras.callbacks.ReduceLROnPlateau(
monitor='val_loss' # 监控模型的验证损失
factor=0.1, # 触发时将学习率除以 10
patience=10, # 如果验证损失在 10 轮内都没有改善,那么就触发这个回调函数
)
]
# 注意,因为回调函数要监控验证损失,所以你需要在调用 fit 时传入 validation_data(验证数据)
model.fit(x, y,
epochs=10,
batch_size=32,
callbacks=callbacks_list,
validation_data=(x_val, y_val))
- 编写你自己的回调函数
如果你需要在训练过程中采取特定行动,而这项行动又没有包含在内置回调函数中,那么 可以编写你自己的回调函数。回调函数的实现方式是创建 keras.callbacks.Callback 类的子类。然后你可以实现下面这些方法(从名称中即可看出这些方法的作用),它们分别在训练过程中的不同时间点被调用。
on_epoch_begin # 在每轮开始时被调用
on_epoch_end # 在每轮结束时被调用
on_batch_begin # 在处理每个批量之前被调用
on_batch_end # 在处理每个批量之后被调用
on_train_begin # 在训练开始时被调用
on_train_end # 在训练结束时被调用
这些方法被调用时都有一个 logs 参数,这个参数是一个字典,里面包含前一个批量、前 一个轮次或前一次训练的信息,即训练指标和验证指标等。此外,回调函数还可以访问下列属性。
self.model:调用回调函数的模型实例。
self.validation_data:传入 fit 作为验证数据的值。
下面是一个自定义回调函数的简单示例,它可以在每轮结束后将模型每层的激活保存到硬盘(格式为 Numpy 数组),这个激活是对验证集的第一个样本计算得到的。
import keras
import numpy as np
class ActivationLogger(keras.callbacks.Callback):
def set_model(self, model):
self.model = model # 在训练之前由父模型调用,告诉回调函数是哪个模型在调用它
layer_outputs = [layer.output for layer in model.layers]
self.activations_model = keras.models.Model(model.input, # 模型实例,返回每层的激活
layer_outputs)
def on_epoch_end(self, epoch, logs=None):
if self.validation_data is None:
raise RuntimeError('Requires validation_data.')
validation_sample = self.validation_data[0][0:1] # 获取验证数据的第一个输入样本
activations = self.activations_model.predict(validation_sample)
f = open('activations_at_epoch_' + str(epoch) + '.npz', 'w') # 将数组保存到硬盘
np.savez(f, activations)
f.close()
关于回调函数你只需要知道这么多,其他的都是技术细节,很容易就能查到。现在,你已 经可以在训练过程中对一个 Keras 模型执行任何类型的日志记录或预定程序的干预。
2.2 TensorBoard 简介:TensorFlow 的可视化框架
想要做好研究或开发出好的模型,在实验过程中你需要丰富频繁的反馈,从而知道模型内部正在发生什么。这正是运行实验的目的:获取关于模型表现好坏的信息,越多越好。取得进 展是一个反复迭代的过程(或循环):首先你有一个想法,并将其表述为一个实验,用于验证 你的想法是否正确。你运行这个实验,并处理其生成的信息。这又激发了你的下一个想法。在 这个循环中实验的迭代次数越多,你的想法也就变得越来越精确、越来越强大。Keras 可以帮你 在最短的时间内将想法转化成实验,而高速 GPU 可以帮你尽快得到实验结果。但如何处理实验 结果呢?这就需要 TensorBoard 发挥作用了(见图 7-9)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Pe8clST-1672156042008)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227230902372.png)]
本节将介绍 TensorBoard,一个内置于 TensorFlow 中的基于浏览器的可视化工具。注意,只 有当 Keras 使用 TensorFlow 后端时,这一方法才能用于 Keras 模型。TensorBoard 的主要用途是,在训练过程中帮助你以可视化的方法监控模型内部发生的一切。 如果你监控了除模型最终损失之外的更多信息,那么可以更清楚地了解模型做了什么、没做什么, 并且能够更快地取得进展。TensorBoard 具有下列巧妙的功能,都在浏览器中实现。
- 在训练过程中以可视化的方式监控指标
- 将模型架构可视化
- 将激活和梯度的直方图可视化
- 以三维的形式研究嵌入
我们用一个简单的例子来演示这些功能:在 IMDB 情感分析任务上训练一个一维卷积神经网络。
这个模型类似于 6.4 节的模型。我们将只考虑 IMDB 词表中的前 2000 个单词,这样更易于将词嵌入可视化。
使用了 TensorBoard 的文本分类模型
import keras
from keras import layers
from keras.datasets import imdb
from keras.preprocessing import sequence
max_features = 2000
max_len = 500
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
x_train = sequence.pad_sequences(x_train, maxlen=max_len)
x_test = sequence.pad_sequences(x_test, maxlen=max_len)
model = keras.models.Sequential()
model.add(layers.Embedding(max_features, 128,
input_length=max_len,
name='embed'))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.MaxPooling1D(5))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1))
model.summary()
model.compile(optimizer='rmsprop',
loss='binary_crossentropy',
metrics=['acc'])
在开始使用 TensorBoard 之前,我们需要创建一个目录,用于保存它生成的日志文件。
我们用一个 TensorBoard 回调函数实例来启动训练。这个回调函数会将日志事件写入硬 盘的指定位置。
callbacks = [
keras.callbacks.TensorBoard(
log_dir='my_log_dir', # 日志文件将被写入这个位置
histogram_freq=1, # 每一轮之后记录激活直方图
embeddings_freq=1, # 每一轮之后记录嵌入数据
)
]
history = model.fit(x_train, y_train,
epochs=20,
batch_size=128,
validation_split=0.2,
callbacks=callbacks)
现在,你可以在命令行启动 TensorBoard 服务器,指示它读取回调函数当前正在写入的日志。 在安装 TensorFlow 时(比如通过 pip),tensorboard 程序应该已经自动安装到计算机里了。
tensorboard --logdir=my_log_dir
然后可以用浏览器打开 http://localhost:6006,并查看模型的训练过程(见图 7-10)。除了训练指标和验证指标的实时图表之外,你还可以访问 HISTOGRAMS(直方图)标签页,并查看美观的直方图可视化,直方图中是每层的激活值(见图 7-11)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DUxlhRDC-1672156042009)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227232232707.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hS1sQV1E-1672156042009)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227232245491.png)]
EMBEDDINGS(嵌入)标签页让你可以查看输入词表中 2000 个单词的嵌入位置和空间关系, 它们都是由第一个 Embedding 层学到的。因为嵌入空间是 128 维的,所以 TensorBoard 会使用 你选择的降维算法自动将其降至二维或三维,可选的降维算法有主成分分析(PCA)和 t-分布 随机近邻嵌入(t-SNE)。在图 7-12 所示的点状云中,可以清楚地看到两个簇:正面含义的词和负面含义的词。从可视化图中可以立刻明显地看出,将嵌入与特定目标联合训练得到的模型是完全针对这个特定任务的,这也是为什么使用预训练的通用词嵌入通常不是一个好主意。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-35M8rY7o-1672156042009)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227232424985.png)]
GRAPHS(图)标签页显示的是 Keras 模型背后的底层 TensorFlow 运算图的交互式可视化 (见图 7-13)。可见,图中的内容比之前想象的要多很多。对于你刚刚构建的模型,在 Keras 中 定义模型时可能看起来很简单,只是几个基本层的堆叠;但在底层,你需要构建相当复杂的图 结构来使其生效。其中许多内容都与梯度下降过程有关。你所见到的内容与你所操作的内容之 间存在这种复杂度差异,这正是你选择使用 Keras 来构建模型、而不是使用原始 TensorFlow 从 头开始定义所有内容的主要动机。Keras 让工作流程变得非常简单。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yyot6nbN-1672156042009)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227232616639.png)]
注意,Keras 还提供了另一种更简洁的方法——keras.utils.plot_model 函数,它可以将模型绘制为层组成的图,而不是 TensorFlow 运算组成的图。使用这个函数需要安装 Python 的 pydot 库和 pydot-ng 库,还需要安装 graphviz 库。我们来快速看一下。
from keras.utils import plot_model
plot_model(model, to_file='model.png')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lhsOHToi-1672156042010)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227232718903.png)]
你还可以选择在层组成的图中显示形状信息。下面这个例子使用 plot_model 函数及 show_shapes 选项将模型拓扑结构可视化(见图 7-15)。
from keras.utils import plot_model
plot_model(model, show_shapes=True, to_file='model.png')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YzBsAifv-1672156042010)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227232752988.png)]
2.3 小结
- Keras 回调函数提供了一种简单方法,可以在训练过程中监控模型并根据模型状态自动采取行动。
- 使用 TensorFlow 时,TensorBoard 是一种在浏览器中将模型活动可视化的好方法。在 Keras 模型中你可以通过 TensorBoard 回调函数来使用这种方法。
3 让模型性能发挥到极致
如果你只是想要让模型具有不错的性能,那么盲目地尝试网络架构足以达到目的。本节中, 我们将为你提供一套用于构建最先进深度学习模型的必备技术的快速指南,从而让模型由“具 有不错的性能”上升到“性能卓越且能够赢得机器学习竞赛”。
3.1 高级架构模式
7.1.4 节详细介绍过一种重要的设计模式——残差连接。还有另外两种设计模式你也应该知 道:标准化和深度可分离卷积。这些模式在构建高性能深度卷积神经网络时特别重要,但在其 他许多类型的架构中也很常见。
- 批标准化
标准化(normalization)是一大类方法,用于让机器学习模型看到的不同样本彼此之间更加 相似,这有助于模型的学习与对新数据的泛化。最常见的数据标准化形式就是你已经在本书中 多次见到的那种形式:将数据减去其平均值使其中心为 0,然后将数据除以其标准差使其标准 差为 1。实际上,这种做法假设数据服从正态分布(也叫高斯分布),并确保让该分布的中心为 0, 同时缩放到方差为 1。
normalized_data = (data - np.mean(data, axis=...)) / np.std(data, axis=...)
前面的示例都是在将数据输入模型之前对数据做标准化。但在网络的每一次变换之后都应 该考虑数据标准化。即使输入 Dense 或 Conv2D 网络的数据均值为 0、方差为 1,也没有理由 假定网络输出的数据也是这样。
批标准化(batch normalization)是 Ioffe 和 Szegedy 在 2015 年提出的一种层的类型 a(在 Keras 中是 BatchNormalization),即使在训练过程中均值和方差随时间发生变化,它也可以 适应性地将数据标准化。批标准化的工作原理是,训练过程中在内部保存已读取每批数据均值 和方差的指数移动平均值。批标准化的主要效果是,它有助于梯度传播(这一点和残差连接很 像),因此允许更深的网络。对于有些特别深的网络,只有包含多个 BatchNormalization 层 时才能进行训练。例如,BatchNormalization 广泛用于 Keras 内置的许多高级卷积神经网络 架构,比如 ResNet50、Inception V3 和 Xception。
BatchNormalization 层通常在卷积层或密集连接层之后使用。
conv_model.add(layers.Conv2D(32, 3, activation='relu'))
conv_model.add(layers.BatchNormalization()) # 在卷积层之后使用
dense_model.add(layers.Dense(32, activation='relu'))
dense_model.add(layers.BatchNormalization()) # 在 Dense 层之后使用
BatchNormalization 层接收一个 axis 参数,它指定应该对哪个特征轴做标准化。这 个参数的默认值是 -1,即输入张量的最后一个轴。对于 Dense 层、Conv1D 层、RNN 层和将 data_format 设为 “channels_last”(通道在后)的 Conv2D 层,这个默认值都是正确的。 但有少数人使用将 data_format 设为 “channels_first”(通道在前)的 Conv2D 层,这时 特征轴是编号为 1 的轴,因此 BatchNormalization 的 axis 参数应该相应地设为 1。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PSHnjWa0-1672156042010)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227233148473.png)]
- 深度可分离卷积
如果我告诉你,有一个层可以替代 Conv2D,并可以让模型更加轻量(即更少的可训练权 重参数)、速度更快(即更少的浮点数运算),还可以让任务性能提高几个百分点,你觉得怎么 样?我说的正是深度可分离卷积(depthwise separable convolution)层(SeparableConv2D)的 作用。这个层对输入的每个通道分别执行空间卷积,然后通过逐点卷积(1×1 卷积)将输出通道混合,如图 7-16 所示。这相当于将空间特征学习和通道特征学习分开,如果你假设输入中的空间位置高度相关,但不同的通道之间相对独立,那么这么做是很有意义的。它需要的参数要少很多,计算量也更小,因此可以得到更小、更快的模型。因为它是一种执行卷积更高效的方法, 所以往往能够使用更少的数据学到更好的表示,从而得到性能更好的模型。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IH7TA6fD-1672156042010)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221227233305594.png)]
如果只用有限的数据从头开始训练小型模型,这些优点就变得尤为重要。例如,下面这个示例是在小型数据集上构建一个轻量的深度可分离卷积神经网络,用于图像分类任务(softmax 多分类)。
from keras.models import Sequential, Model
from keras import layers
height = 64
width = 64
channels = 3
num_classes = 10
model = Sequential()
model.add(layers.SeparableConv2D(32, 3,
activation='relu',
input_shape=(height, width, channels,)))
model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))
model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))
model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.GlobalAveragePooling2D())
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(num_classes, activation='softmax'))
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
对于规模更大的模型,深度可分离卷积是 Xception 架构的基础,Xception 是一个高性能的 卷积神经网络,内置于 Keras 中。在我的论文“Xception: deep learning with depthwise separable convolutions”中,你可以进一步了解深度可分离卷积和 Xception 的理论基础。
3.2 超参数优化
构建深度学习模型时,你必须做出许多看似随意的决定:应该堆叠多少层?每层应该 包含多少个单元或过滤器?激活应该使用 relu 还是其他函数?在某一层之后是否应该使用 BatchNormalization ?应该使用多大的 dropout 比率?还有很多。这些在架构层面的参数叫 作超参数(hyperparameter),以便将其与模型参数区分开来,后者通过反向传播进行训练。
在实践中,经验丰富的机器学习工程师和研究人员会培养出直觉,能够判断上述选择哪些 可行、哪些不可行。也就是说,他们学会了调节超参数的技巧。但是调节超参数并没有正式成 文的规则。如果你想要在某项任务上达到最佳性能,那么就不能满足于一个容易犯错的人随意 做出的选择。即使你拥有很好的直觉,最初的选择也几乎不可能是最优的。你可以手动调节你 的选择、重新训练模型,如此不停重复来改进你的选择,这也是机器学习工程师和研究人员大 部分时间都在做的事情。但是,整天调节超参数不应该是人类的工作,最好留给机器去做。
因此,你需要制定一个原则,系统性地自动探索可能的决策空间。你需要搜索架构空间, 并根据经验找到性能最佳的架构。这正是超参数自动优化领域的内容。这个领域是一个完整的研究领域,而且很重要。
超参数优化的过程通常如下所示。
- 选择一组超参数(自动选择)。
- 构建相应的模型。
- 将模型在训练数据上拟合,并衡量其在验证数据上的最终性能。
- 选择要尝试的下一组超参数(自动选择)。
- 重复上述过程。
- 最后,衡量模型在测试数据上的性能。
这个过程的关键在于,给定许多组超参数,使用验证性能的历史来选择下一组需要评估的 超参数的算法。有多种不同的技术可供选择:贝叶斯优化、遗传算法、简单随机搜索等。
训练模型权重相对简单:在小批量数据上计算损失函数,然后用反向传播算法让权重向正 确的方向移动。与此相反,更新超参数则非常具有挑战性。我们来考虑以下两点。
计算反馈信号(这组超参数在这个任务上是否得到了一个高性能的模型)的计算代价可 能非常高,它需要在数据集上创建一个新模型并从头开始训练。
超参数空间通常由许多离散的决定组成,因而既不是连续的,也不是可微的。因此,你 通常不能在超参数空间中做梯度下降。相反,你必须依赖不使用梯度的优化方法,而这 些方法的效率比梯度下降要低很多。
这些挑战非常困难,而这个领域还很年轻,因此我们目前只能使用非常有限的工具来优 化模型。通常情况下,随机搜索(随机选择需要评估的超参数,并重复这一过程)就是最好的 解决方案,虽然这也是最简单的解决方案。但我发现有一种工具确实比随机搜索更好,它就是 Hyperopt。它是一个用于超参数优化的 Python 库,其内部使用 Parzen 估计器的树来预测哪组超 参数可能会得到好的结果。另一个叫作 Hyperas 的库将 Hyperopt 与 Keras 模型集成在一起。一 定要试试。
在进行大规模超参数自动优化时,有一个重要的问题需要牢记,那就是验证集过拟合。 因为你是使用验证数据计算出一个信号,然后根据这个信号更新超参数,所以你实际上是在验证数据上训练超参数,很快会对验证数据过拟合。请始终记住这一点。
总之,超参数优化是一项强大的技术,想要在任何任务上获得最先进的模型或者赢得机器 学习竞赛,这项技术都必不可少。思考一下:曾经人们手动设计特征,然后输入到浅层机器学 习模型中,这肯定不是最优的。现在,深度学习能够自动完成分层特征工程的任务,这些特征 都是利用反馈信号学到的,而不是手动调节的,事情本来就应该如此。同样,你也不应该手动 设计模型架构,而是应该按照某种原则对其进行最优化。在写作本书时,超参数自动优化还是 一个非常年轻且不成熟的领域,正如几年前的深度学习,但我预计这一领域会在未来数年内蓬 勃发展。
3.3 模型集成
想要在一项任务上获得最佳结果,另一种强大的技术是模型集成(model ensembling)。集成是指将一系列不同模型的预测结果汇集到一起,从而得到更好的预测结果。观察机器学习竞赛, 特别是 Kaggle 上的竞赛,你会发现优胜者都是将很多模型集成到一起,它必然可以打败任何单 个模型,无论这个模型的表现多么好。
集成依赖于这样的假设,即对于独立训练的不同良好模型,它们表现良好可能是因为不同 的原因:每个模型都从略有不同的角度观察数据来做出预测,得到了“真相”的一部分,但不 是全部真相。你可能听说过盲人摸象的古代寓言:一群盲人第一次遇到大象,想要通过触摸来 了解大象。每个人都摸到了大象身体的不同部位,但只摸到了一部分,比如鼻子或一条腿。这 些人描述的大象是这样的,“它像一条蛇”“像一根柱子或一棵树”,等等。这些盲人就好比机器 学习模型,每个人都试图根据自己的假设(这些假设就是模型的独特架构和独特的随机权重初 始化)并从自己的角度来理解训练数据的多面性。每个人都得到了数据真相的一部分,但不是 全部真相。将他们的观点汇集在一起,你可以得到对数据更加准确的描述。大象是多个部分的 组合,每个盲人说的都不完全准确,但综合起来就成了一个相当准确的故事。
我们以分类问题为例。想要将一组分类器的预测结果汇集在一起[即分类器集成(ensemble the classifiers)],最简单的方法就是将它们的预测结果取平均值作为预测结果。
# 使用 4 个不同的模型来计算初始预测
preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)
# 这个新的预测数组应该比任何一个初始预测都更加准确
final_preds = 0.25 * (preds_a + preds_b + preds_c + preds_d)
只有这组分类器中每一个的性能差不多一样好时,这种方法才奏效。如果其中一个分类器 性能比其他的差很多,那么最终预测结果可能不如这一组中的最佳分类器那么好。
将分类器集成有一个更聪明的做法,即加权平均,其权重在验证数据上学习得到。通常来说,更好的分类器被赋予更大的权重,而较差的分类器则被赋予较小的权重。为了找到一组好的集成权重,你可以使用随机搜索或简单的优化算法(比如 Nelder-Mead 方法)。
preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)
# 假设 (0.5, 0.25, 0.1, 0.15)这些权重是根据经验学到的
final_preds = 0.5 * preds_a + 0.25 * preds_b + 0.1 * preds_c + 0.15 * preds_d
还有许多其他变体,比如你可以对预测结果先取指数再做平均。一般来说,简单的加权平均, 其权重在验证数据上进行最优化,这是一个很强大的基准方法。
想要保证集成方法有效,关键在于这组分类器的多样性(diversity)。多样性就是力量。如 果所有盲人都只摸到大象的鼻子,那么他们会一致认为大象像蛇,并且永远不会知道大象的真 实模样。是多样性让集成方法能够取得良好效果。用机器学习的术语来说,如果所有模型的偏 差都在同一个方向上,那么集成也会保留同样的偏差。如果各个模型的偏差在不同方向上,那 么这些偏差会彼此抵消,集成结果会更加稳定、更加准确。
因此,集成的模型应该尽可能好,同时尽可能不同。这通常意味着使用非常不同的架构, 甚至使用不同类型的机器学习方法。有一件事情基本上是不值得做的,就是对相同的网络,使用不同的随机初始化多次独立训练,然后集成。如果模型之间的唯一区别是随机初始化和训练数据的读取顺序,那么集成的多样性很小,与单一模型相比只会有微小的改进。
我发现有一种方法在实践中非常有效(但这一方法还没有推广到所有问题领域),就是将基 于树的方法(比如随机森林或梯度提升树)和深度神经网络进行集成。2014 年,合作者 Andrei Kolev 和我使用多种树模型和深度神经网络的集成,在 Kaggle 希格斯玻色子衰变探测挑战赛中 获得第四名。值得一提的是,集成中的某一个模型来源于与其他模型都不相同的方法(它是正 则化的贪婪森林),并且得分也远远低于其他模型。不出所料,它在集成中被赋予了一个很小的 权重。但出乎我们的意料,它极大地改进了总体的集成结果,因为它和其他所有模型都完全不同, 提供了其他模型都无法获得的信息。这正是集成方法的关键之处。集成不在于你的最佳模型有 多好,而在于候选模型集合的多样性。
3.4 小结
构建高性能的深度卷积神经网络时,你需要使用残差连接、批标准化和深度可分离卷积。 未来,无论是一维、二维还是三维应用,深度可分离卷积很可能会完全取代普通卷积, 因为它的表示效率更高。
构建深度网络需要选择许多超参数和架构,这些选择共同决定了模型的性能。与其将这些选择建立在直觉或随机性之上,不如系统性地搜索超参数空间,以找到最佳选择。目前, 这个搜索过程的计算代价还很高,使用的工具也不是很好。但 Hyperopt 和 Hyperas 这两 个库可能会对你有所帮助。进行超参数优化时,一定要小心验证集过拟合!
想要在机器学习竞赛中获胜,或者想要在某项任务上获得最佳结果,只能通过多个模型的集成来实现。利用加权平均(权重已经过优化)进行集成通常已经能取得足够好的效 果。请记住,多样性就是力量。将非常相似的模型集成基本上是没有意义的。最好的集 成方法是将尽可能不同的一组模型集成(这组模型还需要具有尽可能高的预测能力)。
4 本章总结
本章我们学习了以下内容。
- 如何将模型构建为层组成的图、层的重复使用(层权重共享)与将模型用作 Python 函 数(模型模板)。
- 你可以使用 Keras 回调函数在训练过程中监控模型,并根据模型状态采取行动。
- TensorBoard 可以将指标、激活直方图甚至嵌入空间可视化。
- 什么是批标准化、深度可分离卷积和残差连接。
- 为什么应该使用超参数优化和模型集成。
借助这些新工具,你可以在现实世界中更好地利用深度学习,并可以开始构建具有高度竞争力的深度学习模型。