使用深度神经网络的最大缺点之一是它们具有许多应优化的超参数,以使网络发挥最佳表现。 在前面的每个章节中,我们都遇到但没有涵盖超参数估计的挑战。 超参数优化是一个非常重要的话题。 在大多数情况下,这是一个未解决的问题,尽管我们不能涵盖本书的全部主题,但我认为它仍然值得一章。
在本章中,我将为您提供一些我认为是选择超参数的实用建议。 可以肯定的是,由于本章是基于我自己的经验,因此本章可能会有些偏颇和偏颇。 我希望经验会有所帮助,同时也带您进一步对该主题进行调查。
我们将在本章介绍以下主题:
- 是否应该将网络架构视为超参数?
- 我们应该优化哪些超参数?
- 超参数优化策略
在构建最简单的网络时,我们必须对网络架构做出各种选择。 我们应该使用 1 个隐藏层还是 1,000 个? 每层应包含多少个神经元? 他们都应该使用relu
激活函数还是tanh
? 我们应该在每个隐藏层上还是仅在第一层上使用丢弃? 在设计网络架构时,我们必须做出许多选择。
在最典型的情况下,我们穷举搜索每个超参数的最佳值。 但是,要穷举搜索网络架构并不容易。 实际上,我们可能没有时间或计算能力。 我们很少看到研究人员通过穷举搜索来寻找最佳架构,因为选择的数量非常多,而且存在不只一个正确的答案。 取而代之的是,我们看到该领域的研究人员通过实验尝试建立已知的架构,以尝试创建新的新颖架构并改善现有架构。
因此,在介绍详尽搜索超参数的策略之前,让我们看一下两种推论出合理的,甚至不是最佳的网络架构的策略。
沙特尔的伯纳德(Bernard of Chartres)被赋予了通过借鉴他人的发现来学习的概念。 但是,正是艾萨克·牛顿(Isaac Newton)说:“如果我进一步观察,那就是站在巨人的肩膀上。” 要明确的是,这正是我在这里建议的。
如果我要设计一个用于新的深度学习问题的网络架构,我要做的第一件事就是尝试找到一个令人满意的方式,以前已经解决了类似的问题。 尽管可能没有人能够解决您面临的任务,但可能存在类似的情况。
很可能存在几种可能的解决方案。 如果是这样,并且在时间允许的情况下,每次运行几次的平均结果可能会告诉您哪个运行效果最好。 当然,在这里我们发现自己很快进入了研究领域。
希望通过寻找类似问题的架构,您至少接近适合您的架构。 您如何做才能进一步优化网络架构?
- 在多个实验运行中,添加层和/或神经元,直到您的网络开始针对问题过拟合。 在深度学习中,添加单元,直到您不再具有高偏差模型为止。
- 一旦开始过拟合,您就会发现一些网络架构能够很好地拟合训练数据,甚至可能拟合得很好。 在这一点上,您应该集中精力通过使用丢弃,正则化,提早停止等方法来减少方差。
这种方法通常归因于著名的神经网络研究员 Geoffrey Hinton。 这是一个有趣的想法,因为它使过拟合不是要避免的事情,而是构建网络架构的良好第一步。
尽管没有规则可供我们选择最佳网络架构,并且可能存在许多最佳架构,但我发现这种策略在实践中对我来说非常有效。
如果您对上述内容不太了解,我同意。 这对我也不是,我也不希望那样。 您当然可以在一组预定义的配置之间搜索最佳的网络架构,这也是正确的方法。 实际上,它可以说是更正确,更严格。 此过程旨在为您提供实用的建议,以帮助您在尽可能短的时间内达到最佳状态。
即使您遵循我的建议并选择了一个足够好的架构,您也可以并且仍然应该尝试在该架构中搜索理想的超参数。 我们可能要搜索的一些超参数包括:
- 我们选择的优化器。 到目前为止,我一直在使用 Adam,但是 rmsprop 优化器或调整良好的 SGD 可能会更好。
- 每个优化器都有一组我们可能需要调整的超参数,例如学习率,动量和衰减。
- 网络权重初始化。
- 神经元激活。
- 正则化参数(例如丢弃概率)或 12 正则化中使用的正则化参数。
- 批次大小。
如上所述,这不是详尽的清单。 当然,您可以尝试更多的选择,包括在每个隐藏层中引入可变数量的神经元,每层中丢弃概率的变化等等。 就像我们一直暗示的那样,超参数的可能组合是无限的。 这些选择也很可能并非独立于网络架构,添加和删除层可能会为这些超参数中的任何一个带来新的最佳选择。
在本章的这一点上,我们建议,在大多数情况下,尝试我们可能想尝试的每个超参数组合在计算上都是不可能的,或者至少是不切实际的。 深度神经网络肯定会花费很长时间进行训练。 尽管您可以并行处理问题并投入计算资源,但搜索超参数的最大限制可能仍然是时间。
如果时间是我们最大的限制,并且我们无法合理地探索拥有的所有可能性,那么我们将必须制定一种策略,使我们在拥有的时间内获得最大的效用。
在本节的其余部分,我将介绍一些用于超参数优化的常用策略,然后向您展示如何使用我最喜欢的两种方法在 Keras 中优化超参数。
在所有机器学习模型中都有一套通用的超参数优化策略。 从总体上讲,这些策略包括:
- 网格搜索
- 随机搜索
- 贝叶斯优化
- 遗传算法
- 机器学习的超参数
网格搜索只是尝试尝试所有事物,或者至少尝试离散事物,然后报告我们用蛮力找到的最佳超参数的最佳组合。 可以保证在我们确定的参数空间中找到最佳解决方案,以及其他较差的解决方案。
网格搜索对于深度学习并不是很实用。 除了最基本的深度神经网络,我们无法现实地探索所有可能参数的每个可能值。 使用随机搜索,我们从每个参数分布中随机抽样,并尝试其中的n
,其中(n x
每个示例训练时间)是我们愿意分配给这个问题的时间预算。
贝叶斯优化方法使用以前的观察结果来预测接下来要采样的超参数集。 尽管贝叶斯优化方法通常胜过蛮力技术,但目前的研究表明,与穷举方法相比,表现提升较小。 此外,由于贝叶斯方法取决于先前的经验,因此无论如何都不会令人尴尬地并行进行。
遗传算法是机器学习中非常有趣且活跃的研究领域。 但是,我目前的观点是,它们也不是深度神经网络参数优化的理想选择,因为它们再次依赖于先前的经验。
该领域中的一些最新研究着眼于训练神经网络,该神经网络可以预测给定网络架构的最佳参数。 可以参数化模型的模型的想法当然非常有趣,这是一个值得密切关注的地方。 这也可能是我们获得天网的方式。 只有时间证明一切。
使用 scikit-learn 可以轻松实现网格搜索和随机搜索。 在此示例中,我们将使用 Keras 的KerasClassifier
类包装模型并使其与 scikit-learn API 兼容。 然后,我们将使用 scikit-learn 的RandomSearchCV
类进行超参数搜索。
为此,我们将从稍微更改现在熟悉的模型构建函数开始。 我们将使用我们要搜索的超参数对其进行参数化,如以下代码所示:
def build_network(keep_prob=0.5, optimizer='adam'):
inputs = Input(shape=(784,), name="input")
x = Dense(512, activation='relu', name="hidden1")(inputs)
x = Dropout(keep_prob)(x)
x = Dense(256, activation='relu', name="hidden2")(x)
x = Dropout(keep_prob)(x)
x = Dense(128, activation='relu', name="hidden3")(x)
x = Dropout(keep_prob)(x)
prediction = Dense(10, activation='softmax', name="output")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer=optimizer, loss='categorical_crossentropy',
metrics=["accuracy"])
return model
在此示例中,我想搜索一个理想的丢弃值,并且我想尝试几个不同的优化器。 为了实现这一点,我需要将它们作为参数包含在函数中,以便可以通过我们的随机搜索方法对其进行更改。 当然,我们可以使用相同的方法来参数化和测试许多其他网络架构选择,但是我们在这里保持简单。
接下来,我们将创建一个函数,该函数返回一个字典,其中包含我们想搜索的所有可能的超参数及其值空间,如以下代码所示:
def create_hyperparameters():
batches = [10, 20, 30, 40, 50]
optimizers = ['rmsprop', 'adam', 'adadelta']
dropout = np.linspace(0.1, 0.5, 5)
return {"batch_size": batches, "optimizer": optimizers,
"keep_prob": dropout}
剩下的就是使用RandomSearchCV
将这两部分连接在一起。 首先,我们将模型包装到keras.wrappers.scikit_learn.KerasClassifier
中,以便与 scikit-learn 兼容,如以下代码所示:
model = KerasClassifier(build_fn=build_network, verbose=0)
接下来,我们将使用以下代码获得超参数字典:
hyperparameters = create_hyperparameters()
然后,最后,我们将创建一个RandomSearchCV
对象,该对象将用于搜索模型的参数空间,如以下代码所示:
search = RandomizedSearchCV(estimator=model, param_distributions=hyperparameters, n_iter=10, n_jobs=1, cv=3, verbose=1)
拟合此RandomizedSearchCV
对象后,它将从参数分布中随机选择值并将其应用于模型。 它将执行 10 次(n_iter=10
),并且将尝试每种组合 3 次,因为我们使用了 3 倍交叉验证。 这意味着我们将总共拟合模型 30 次。 使用每次运行的平均准确率,它将返回最佳模型作为类属性.best_estimator
,并且将返回最佳参数作为.best_params_
。
为了适合它,我们只需调用它的fit
方法,就好像它是一个模型一样,如以下代码所示:
search.fit(data["train_X"], data["train_y"])
print(search.best_params_)
在 Tesla K80 GPU 实例上,在上述网格上拟合第 5 章,“使用 Keras 进行多分类”所使用的 MNIST 模型。 在完成本节之前,让我们看一下搜索的一些输出,如以下代码所示:
Using TensorFlow backend.
Fitting 3 folds for each of 10 candidates, totalling 30 fits
tensorflow/core/common_runtime/gpu/gpu_device.cc:1030] Found device 0 with properties:
name: Tesla K80 major: 3 minor: 7 memoryClockRate(GHz): 0.8235
pciBusID: 0000:00:1e.0
totalMemory: 11.17GiB freeMemory: 11.10GiB
tensorflow/core/common_runtime/gpu/gpu_device.cc:1120] Creating TensorFlow device (/device:GPU:0) -> (device: 0, name: Tesla K80, pci bus id: 0000:00:1e.0, compute capability: 3.7)
[Parallel(n_jobs=1)]: Done 30 out of 30 | elapsed: 8.8min finished
{'keep_prob': 0.20000000000000001, 'batch_size': 40, 'optimizer': 'adam'}
如您在此输出中看到的,在 10 次运行中,加粗的超参数似乎是表现最好的集合。 当然,我们当然可以运行更多的迭代,并且我们可能会找到一个更好的选择。 我们的预算仅由时间,耐心以及云帐户附带的信用卡决定。
Hyperband 是一项超参数优化技术,由 Lisha Li,Kevin Jamieson,Guilia DeSalvo,Afshin Rostamizadeh 和 Ameet Talwalker 于 2016 年在伯克利开发。 您可以在这里阅读他们的原始论文。
想象一下,就像我们在RandomSearchCV
中所做的那样,随机采样许多潜在的超参数集。 完成RandomSearchCV
后,它将选择一个单一的超参数配置作为其采样的最优值。 Hyperband 利用这样的思想,即即使经过少量迭代,最佳的超参数配置也可能会胜过其他配置。 Hyperband 中的乐队来自土匪,指的是基于多臂土匪技术(用于优化竞争选择之间的资源分配以优化表现为目标的技术)的勘探与开发。
使用 Hyperband,我们可以尝试一些可能的配置集(n
),仅训练一次迭代。 作者将迭代一词留作多种可能的用途。 但是,我将周期作为迭代。 一旦完成第一个训练循环,就将根据表现对结果进行配置。 然后,对该列表的上半部分进行大量迭代的训练。 然后重复进行减半和剔除的过程,我们得到了一些非常小的配置集,我们将针对在搜索中定义的完整迭代次数进行训练。 与在每种可能的配置中搜索最大周期相比,此过程使我们在更短的时间内获得了最佳超参数集。
在本章的 GitHub 存储库中,我在hyperband.py
中包括了hyperband
算法的实现。 此实现主要源自 FastML 的实现,您可以在这个页面中找到。 要使用它,您需要首先实例化一个hyperband
对象,如以下代码所示:
from hyperband import Hyperband
hb = Hyperband(data, get_params, try_params)
Hyperband 构造器需要三个参数:
data
:到目前为止,我在示例中一直在使用的数据字典get_params
:用于从我们正在搜索的超参数空间中采样的函数的名称try_param
:可用于评估n_iter
迭代的超参数配置并返回损失的函数的名称
在下面的示例中,我实现了get_params
以在参数空间中以统一的方式进行采样:
def get_params():
batches = np.random.choice([5, 10, 100])
optimizers = np.random.choice(['rmsprop', 'adam', 'adadelta'])
dropout = np.random.choice(np.linspace(0.1, 0.5, 10))
return {"batch_size": batches, "optimizer": optimizers,
"keep_prob": dropout}
如您所见,所选的超参数配置将作为字典返回。
接下来,可以实现try_params
以在超参数配置上针对指定的迭代次数拟合模型,如下所示:
def try_params(data, num_iters, hyperparameters):
model = build_network(keep_prob=hyperparameters["keep_prob"],
optimizer=hyperparameters["optimizer"])
model.fit(x=data["train_X"], y=data["train_y"],
batch_size=hyperparameters["batch_size"],
epochs=int(num_iters))
loss = model.evaluate(x=data["val_X"], y=data["val_y"], verbose=0)
return {"loss": loss}
try_params
函数返回一个字典,可用于跟踪任何数量的度量; 但是,由于它用于比较运行,因此需要损失。
通过在对象上调用.run()
方法,hyperband
对象将通过我们上面描述的算法运行。
results = hb.run()
在这种情况下,results
将是每次运行,其运行时间和测试的超参数的字典。 因为即使这种高度优化的搜索都需要花费大量时间,并且 GPU 时间也很昂贵,所以我将 MNIST 搜索的结果包括在本章的 GitHub 存储库的hyperband-output-mnist.txt
中,可以在以下位置找到。
超参数优化是从我们的深度神经网络获得最佳效果的重要一步。 寻找搜索超参数的最佳方法是机器学习研究的一个开放而活跃的领域。 尽管您当然可以将最新技术应用于自己的深度学习问题,但您需要在决策中权衡实现的复杂性和搜索运行时间。
有一些与网络架构有关的决策可以肯定地进行详尽地搜索,但是,如我上面提供的那样,一组启发式方法和最佳实践可能使您足够接近甚至减少搜索参数的数量。
最终,超参数搜索是一个经济问题,任何超参数搜索的第一部分都应考虑您的计算时间和个人时间预算,以试图找出最佳的超参数配置。
本章总结了深度学习的基础。 在下一章中,我们将从计算机视觉入手,介绍神经网络的一些更有趣和更高级的应用。