19.1. 什么是超参数优化?¶ 在 SageMaker Studio Lab 中打开 Notebook
正如我们在前面章节中看到的,深度神经网络带有大量的参数或权重,这些参数或权重是在训练过程中学习的。除此之外,每个神经网络还有额外的*超参数*需要用户配置。例如,为了确保随机梯度下降收敛到训练损失的局部最优(见 第 12 节),我们必须调整学习率和批量大小。为了避免在训练数据集上过拟合,我们可能需要设置正则化参数,例如权重衰减(见 第 3.7 节)或暂退法(见 第 5.6 节)。我们可以通过设置层数和每层的单元或过滤器数量(即有效的权重数量)来定义模型的容量和归纳偏置。
不幸的是,我们不能简单地通过最小化训练损失来调整这些超参数,因为这会导致在训练数据上过拟合。例如,将正则化参数(如暂退法或权重衰减)设置为零会导致较小的训练损失,但可能会损害泛化性能。
图 19.1.1 机器学习中的典型工作流程,包括使用不同超参数多次训练模型。¶
如果没有其他形式的自动化,超参数必须以试错的方式手动设置,这在机器学习工作流程中是耗时且困难的一部分。例如,考虑在CIFAR-10上训练一个ResNet(见 第 8.6 节),这在Amazon Elastic Cloud Compute(EC2)的g4dn.xlarge
实例上需要超过2小时。即使只是按顺序尝试十个超参数配置,这也将花费我们大约一天的时间。更糟糕的是,超参数通常不能直接在不同的架构和数据集之间转移(Bardenet et al., 2013, Feurer et al., 2022, Wistuba et al., 2018),并且需要为每个新任务重新优化。此外,对于大多数超参数,没有经验法则,需要专家知识来找到合理的值。
超参数优化(HPO)算法旨在以一种有原则和自动化的方式解决这个问题(Feurer and Hutter, 2018),将其框定为一个全局优化问题。默认的目标是在一个留出的验证数据集上的误差,但原则上也可以是任何其他业务指标。它可以与次要目标(如训练时间、推理时间或模型复杂度)相结合或受其约束。
最近,超参数优化已扩展到*神经架构搜索(NAS)*(Elsken et al., 2018, Wistuba et al., 2019),其目标是找到全新的神经网络架构。与经典的HPO相比,NAS在计算方面更加昂贵,并且需要额外的努力才能在实践中保持可行。HPO和NAS都可以被视为AutoML的子领域(Hutter et al., 2019),其旨在自动化整个机器学习流水线。
在本节中,我们将介绍HPO,并展示我们如何能自动找到在 第 4.5 节 中介绍的逻辑回归示例的最佳超参数。
19.1.1. 优化问题¶
我们将从一个简单的玩具问题开始:搜索在 第 4.5 节 中介绍的多类逻辑回归模型 SoftmaxRegression
的学习率,以最小化在 Fashion MNIST 数据集上的验证误差。虽然其他超参数如批量大小或训练轮数也值得调整,但为了简单起见,我们只关注学习率。
import numpy as np
import torch
from scipy import stats
from torch import nn
from d2l import torch as d2l
在运行HPO之前,我们首先需要定义两个要素:目标函数和配置空间。
19.1.1.1. 目标函数¶
学习算法的性能可以看作是一个函数 \(f: \mathcal{X} \rightarrow \mathbb{R}\),它将超参数空间 \(\mathbf{x} \in \mathcal{X}\) 映射到验证损失。对于 \(f(\mathbf{x})\) 的每次评估,我们都必须训练和验证我们的机器学习模型,对于在大型数据集上训练的深度神经网络来说,这可能是时间和计算密集型的。给定我们的标准 \(f(\mathbf{x})\),我们的目标是找到 \(\mathbf{x}_{\star} \in \mathrm{argmin}_{\mathbf{x} \in \mathcal{X}} f(\mathbf{x})\)。
没有简单的方法来计算 \(f\) 相对于 \(\mathbf{x}\) 的梯度,因为它需要通过整个训练过程传播梯度。虽然最近有工作 (Franceschi et al., 2017, Maclaurin et al., 2015) 通过近似的“超梯度”来驱动HPO,但目前还没有任何现有方法能与最先进的技术相媲美,我们在这里不讨论它们。此外,评估 \(f\) 的计算负担要求HPO算法以尽可能少的样本来逼近全局最优。
神经网络的训练是随机的(例如,权重是随机初始化的,小批量是随机抽样的),因此我们的观测将是有噪声的:\(y \sim f(\mathbf{x}) + \epsilon\),我们通常假设观测噪声 \(\epsilon \sim N(0, \sigma)\) 是高斯分布的。
面对所有这些挑战,我们通常会尝试快速识别一小组表现良好的超参数配置,而不是精确地达到全局最优。然而,由于大多数神经网络模型的巨大计算需求,即使这样也可能需要数天或数周的计算。我们将在 第 19.4 节 中探讨如何通过分配搜索或使用评估成本更低的目标函数近似来加速优化过程。
我们从一个计算模型验证误差的方法开始。
class HPOTrainer(d2l.Trainer): #@save
def validation_error(self):
self.model.eval()
accuracy = 0
val_batch_idx = 0
for batch in self.val_dataloader:
with torch.no_grad():
x, y = self.prepare_batch(batch)
y_hat = self.model(x)
accuracy += self.model.accuracy(y_hat, y)
val_batch_idx += 1
return 1 - accuracy / val_batch_idx
我们针对超参数配置 config
(包含 learning_rate
)来优化验证误差。对于每次评估,我们训练我们的模型 max_epochs
个轮次,然后计算并返回其验证误差。
def hpo_objective_softmax_classification(config, max_epochs=8):
learning_rate = config["learning_rate"]
trainer = d2l.HPOTrainer(max_epochs=max_epochs)
data = d2l.FashionMNIST(batch_size=16)
model = d2l.SoftmaxRegression(num_outputs=10, lr=learning_rate)
trainer.fit(model=model, data=data)
return trainer.validation_error().detach().numpy()
19.1.1.2. 配置空间¶
除了目标函数 \(f(\mathbf{x})\),我们还需要定义要优化的可行集 \(\mathbf{x} \in \mathcal{X}\),这被称为*配置空间*或*搜索空间*。对于我们的逻辑回归示例,我们将使用
config_space = {"learning_rate": stats.loguniform(1e-4, 1)}
这里我们使用 SciPy 的 loguniform
对象,它表示在对数空间中 -4 和 -1 之间的均匀分布。这个对象允许我们从该分布中采样随机变量。
每个超参数都有一个数据类型,例如 learning_rate
的 float
,以及一个封闭的有界范围(即下界和上界)。我们通常为每个超参数分配一个先验分布(例如,均匀或对数均匀)来进行采样。一些正参数,如 learning_rate
,最好在对数尺度上表示,因为最优值可能会相差几个数量级,而其他参数,如动量,则使用线性尺度。
下面我们展示了一个由多层感知机典型超参数组成的简单配置空间的例子,包括它们的类型和标准范围。
:多层感知机配置空间示例
名称 |
类型 |
超参数范围 |
对数尺度 |
---|---|---|---|
学习率 |
浮点型 |
:math:` [10^{-6},10^{-1}]` |
是 |
批量大小 |
整型 |
\([8,256]\) |
是 |
动量 |
浮点型 |
\([0,0.99]\) |
否 |
激活函数 |
类别型 |
:math:`\{\textrm{tanh}, \textrm{relu}\}` |
|
单元数 |
整型 |
\([32, 1024]\) |
是 |
层数 |
整型 |
\([1, 6]\) |
否 |
通常,配置空间 \(\mathcal{X}\) 的结构可能很复杂,并且可能与 \(\mathbb{R}^d\) 大不相同。在实践中,一些超参数可能依赖于其他超参数的值。例如,假设我们试图调整多层感知机的层数,以及每层的单元数。第 \(l\) 层的单元数仅在网络至少有 \(l+1\) 层时才相关。这些高级的HPO问题超出了本章的范围。我们建议感兴趣的读者参考 (Baptista and Poloczek, 2018, Hutter et al., 2011, Jenatton et al., 2017)。
配置空间在超参数优化中扮演着重要角色,因为任何算法都无法找到配置空间之外的东西。另一方面,如果范围太大,找到表现良好配置的计算预算可能会变得不可行。
19.1.2. 随机搜索¶
随机搜索是我们首先要考虑的超参数优化算法。随机搜索的主要思想是独立地从配置空间中采样,直到预定义的预算(例如最大迭代次数)耗尽,然后返回观察到的最佳配置。所有评估都可以独立并行执行(参见 第 19.3 节),但为了简单起见,我们在这里使用顺序循环。
errors, values = [], []
num_iterations = 5
for i in range(num_iterations):
learning_rate = config_space["learning_rate"].rvs()
print(f"Trial {i}: learning_rate = {learning_rate}")
y = hpo_objective_softmax_classification({"learning_rate": learning_rate})
print(f" validation_error = {y}")
values.append(learning_rate)
errors.append(y)
validation_error = 0.17070001363754272
最佳学习率就是验证误差最低的那个。
best_idx = np.argmin(errors)
print(f"optimal learning rate = {values[best_idx]}")
optimal learning rate = 0.09844872561810249
由于其简单性和通用性,随机搜索是最常用的HPO算法之一。它不需要任何复杂的实现,并且只要我们能为每个超参数定义某种概率分布,就可以应用于任何配置空间。
不幸的是,随机搜索也有一些缺点。首先,它不会根据迄今为止收集到的先前观察结果来调整采样分布。因此,采样到一个表现不佳的配置和一个表现较好的配置的可能性是相同的。其次,所有配置都花费相同数量的资源,即使有些配置可能在初始表现不佳,并且不太可能超过先前看到的配置。
在接下来的部分中,我们将探讨更具样本效率的超参数优化算法,这些算法通过使用模型来指导搜索,从而克服随机搜索的缺点。我们还将研究那些能自动停止表现不佳配置的评估过程以加速优化过程的算法。
19.1.3. 小结¶
在本节中,我们介绍了超参数优化(HPO)以及如何通过定义配置空间和目标函数将其表述为全局优化问题。我们还实现了我们的第一个HPO算法——随机搜索,并将其应用于一个简单的softmax分类问题。
虽然随机搜索非常简单,但它比网格搜索是更好的选择,后者只是评估一组固定的超参数。随机搜索在某种程度上减轻了维度灾难 (Bellman, 1966) 的影响,并且如果评判标准主要依赖于一小部分超参数,它可能比网格搜索效率高得多。
19.1.4. 练习¶
在本章中,我们在一个不相交的训练集上训练后,优化模型的验证误差。为简单起见,我们的代码使用
Trainer.val_dataloader
,它映射到一个围绕FashionMNIST.val
的加载器。通过查看代码,请您确信这意味着我们使用原始的FashionMNIST训练集(60000个样本)进行训练,并使用原始的*测试集*(10000个样本)进行验证。
为什么这种做法可能会有问题?提示:重读 第 3.6 节,特别是关于*模型选择*的部分。
我们应该怎么做才对?
我们前面提到,通过梯度下降进行超参数优化非常困难。考虑一个较小的问题,例如在FashionMNIST数据集上训练一个两层感知机(第 5.2 节),批量大小为256。我们希望调整SGD的学习率,以在训练一个轮次后最小化验证指标。
为什么我们不能为此目的使用验证*误差*?您会使用验证集上的什么指标?
大致画出训练一个轮次后验证指标的计算图。你可以假设初始权重和超参数(如学习率)是该图的输入节点。提示:重读 第 5.3 节 中关于计算图的内容。
粗略估计一下,在该图上进行一次前向传播需要存储多少浮点数值。提示:FashionMNIST有60000个案例。假设所需内存主要由每层之后的激活值决定,并查阅 第 5.2 节 中的层宽度。
除了巨大的计算和存储需求外,基于梯度的超参数优化还会遇到哪些其他问题?提示:重读 第 5.4 节 中关于梯度消失和爆炸的内容。
进阶:阅读 (Maclaurin et al., 2015),了解一种优雅(但仍有些不切实际)的基于梯度的HPO方法。
网格搜索是另一个HPO基准,我们为每个超参数定义一个等间距的网格,然后遍历(组合的)笛卡尔积来提出配置。
我们前面提到,对于有相当数量超参数的HPO,如果评判标准主要依赖于一小部分超参数,随机搜索可能比网格搜索效率高得多。这是为什么?提示:阅读 (Bergstra et al., 2011)。