Python-超参数调优-全-
Python 超参数调优(全)
原文:
annas-archive.org/md5/4ecab6b29f49ea32658516b579140840译者:飞龙
前言
超参数是构建有用的机器学习模型的一个重要元素。本书收集了众多针对 Python 的超参数调整方法,Python 是机器学习中最受欢迎的编程语言之一。除了深入解释每种方法的工作原理外,你还将使用一个决策图来帮助你确定最适合你需求的最优调整方法。
我们将从超参数调整的介绍开始,并解释为什么它很重要。你将学习到针对各种用例和特定算法类型进行超参数调整的最佳方法。本书不仅涵盖了常见的网格搜索或随机搜索,还涵盖了其他强大的非主流方法。个别章节将专门关注超参数调整的三个主要组:穷举搜索、启发式搜索、贝叶斯优化和多保真优化。
在本书的后续章节中,你将学习到如何使用 scikit-learn、Hyperopt、Optuna、NNI 和 DEAP 等顶级框架来实现超参数调整。最后,我们将涵盖流行算法的超参数以及有助于你高效调整超参数的最佳实践。
到本书结束时,你将具备所需技能,以完全控制你的机器学习模型,并获得最佳结果。
本书面向对象
本书旨在为使用 Python 进行工作的数据科学家和机器学习工程师提供帮助,他们希望通过使用适当的方法来进一步优化他们的机器学习模型性能。你需要对机器学习有一个基本的理解,以及如何用 Python 编程,但不需要对 Python 中的超参数调整有任何先前的知识。
本书涵盖内容
第一章,评估机器学习模型,涵盖了评估机器学习模型时我们需要了解的所有重要事项,包括过拟合的概念、将数据分成几个部分的想法、随机分割和分层分割的比较,以及如何分割数据的多种方法。
第二章,介绍超参数调整,从定义开始介绍超参数调整的概念,然后讨论目标、几个误解以及超参数的分布。
第三章,探索穷举搜索,探讨了属于四个超参数调整组中的第一个组的每种方法,以及它们的优缺点。每种方法都会有高级和详细的解释。高级解释将使用可视化策略来帮助你更容易理解,而详细解释将把数学公式摆上桌面。
第四章,探索贝叶斯优化,探讨了属于四个超参数调优组中的第二个组的每种方法,以及它们的优缺点。对于每种方法,都会有高级和详细的解释。
第五章,探索启发式搜索,探讨了属于四个超参数调优组中的第三个组的每种方法,以及它们的优缺点。对于每种方法,都会有高级和详细的解释。
第六章,探索多保真优化,探讨了属于四个超参数调优组中的第四个组的每种方法,以及它们的优缺点。对于每种方法,都会有高级和详细的解释。
第七章,通过 Scikit 进行超参数调优,涵盖了关于 scikit-learn、scikit-optimize 和 scikit-hyperband 的所有重要信息,以及如何利用它们进行超参数调优。
第八章,通过 Hyperopt 进行超参数调优,介绍了 Hyperopt 包,从其能力和局限性开始,如何利用它进行超参数调优,以及你需要了解的所有其他重要信息。
第九章,通过 Optuna 进行超参数调优,介绍了 Optuna 包,从其众多功能开始,如何利用它进行超参数调优,以及你需要了解的所有其他重要信息。
第十章,使用 DEAP 和 Microsoft NNI 进行高级超参数调优,展示了如何使用 DEAP 和 Microsoft NNI 包进行超参数调优,从熟悉这些包开始,到了解我们需要注意的重要模块和参数。
第十一章,理解流行算法的超参数,探讨了几个流行机器学习算法的超参数。对于每个算法,都会有广泛的解释,包括但不限于每个超参数的定义,当每个超参数的值改变时会产生什么影响,以及基于影响优先级的超参数列表。
第十二章,介绍超参数调优决策图,介绍了超参数调优决策图(HTDM),它将所有讨论的超参数调优方法总结为一个基于六个方面的简单决策图。还将有三个案例研究,展示如何在实际中利用 HTDM。
第十三章,跟踪超参数调整实验,涵盖了跟踪超参数调整实验的重要性以及常规做法。您还将被介绍到一些可用的开源软件包,并学习如何在实践中使用它们。
第十四章,结论与下一步计划,总结了前几章学到的所有重要课程,并介绍了您可能从中受益的几个主题或实现,这些内容在本书中没有详细阐述。
要充分利用本书
您还需要在您的计算机上安装 Python 3.7 版本(或更高版本),以及每章“技术要求”部分中提到的相关软件包。
值得注意的是,在第八章,通过 Hyperopt 进行超参数调整,和第十章,使用 DEAP 和 Microsoft NNI 进行高级超参数调整中,Hyperopt 软件包的版本要求存在冲突。您需要为第八章,通过 Hyperopt 进行超参数调整安装版本 0.2.7,为第十章,使用 DEAP 和 Microsoft NNI 进行高级超参数调整安装版本 0.1.2。
还值得注意的是,在第七章,通过 Scikit 进行超参数调整中使用的 HyperBand 实现是 scikit-hyperband 软件包的修改版本。您可以通过克隆 GitHub 仓库(下一节中提供链接)并在名为 hyperband 的文件夹中查找来使用修改后的版本。
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。
要理解本书的所有内容,您需要对机器学习(ML)和如何在 Python 中编码有一个基本了解,但不需要对 Python 中的超参数调整有先前的知识。本书结束时,您还将被介绍到一些您可能从中受益的主题或实现,这些内容我们在这本书中没有涉及。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件,链接为 github.com/PacktPublishing/Hyperparameter-Tuning-with-Python。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包,可在 github.com/PacktPublishing/ 获取。查看它们!
下载彩色图像
我们还提供包含本书中使用的截图和图表的彩色 PDF 文件。您可以从这里下载:packt.link/ExcbH。
使用的约定
本书使用了多种文本约定。
文本中的代码: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:至于criterion和max_depth,我们仍然使用与上一个搜索空间相同的配置。
代码块设置如下:
for n_est in n_estimators:
for crit in criterion:
for m_depth in max_depth:
#perform cross-validation here
小贴士或重要提示
看起来是这样的。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误: 尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/support/errata 并填写表格。
盗版: 如果您在互联网上发现我们作品的任何形式的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件发送至 copyright@packt.com 并附上材料的链接。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
分享您的想法
读完 使用 Python 进行超参数调优 后,我们非常乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面 并分享您的反馈。
您的评论对我们和科技社区都至关重要,并将帮助我们确保我们提供高质量的内容。
第一部分:方法
本初始章节涵盖了在进行超参数调整实验之前你需要了解的概念和理论。
本节包括以下章节:
-
第一章, 评估机器学习模型
-
第二章, 介绍超参数调整
-
第三章, 探索穷举搜索
-
第四章, 探索贝叶斯优化
-
第五章, 探索启发式搜索
-
第六章, 探索多保真优化
第一章:第一章:评估机器学习模型
机器学习(ML)模型需要彻底评估以确保它们在生产环境中能够工作。我们必须确保模型不是在记忆训练数据,同时也确保模型从给定的训练数据中学习到足够的信息。当我们希望在以后阶段进行超参数调整时,选择合适的评估方法也是至关重要的。
在本章中,我们将了解评估机器学习模型时需要了解的所有重要事项。首先,我们需要理解过拟合的概念。然后,我们将探讨将数据拆分为训练集、验证集和测试集的想法。此外,我们还将了解随机拆分和分层拆分的区别以及何时使用每种拆分方法。
我们将讨论交叉验证的概念及其多种策略变体:k 折重复 k 折、留一法(LOO)、留 P 法(LPO),以及处理时间序列数据时的特定策略,称为时间序列交叉验证。我们还将学习如何使用 Scikit-Learn 包实现每种评估策略。
到本章结束时,你将很好地理解为什么选择合适的评估策略在机器学习模型开发生命周期中至关重要。此外,你将了解多种评估策略,并能够根据你的情况选择最合适的一种。此外,你还将能够使用 Scikit-Learn 包实现每种评估策略。
本章我们将涵盖以下主要内容:
-
理解过拟合的概念
-
创建训练集、验证集和测试集
-
探索随机和分层拆分
-
发现 k 折交叉验证
-
发现重复 k 折交叉验证
-
发现 LOO 交叉验证
-
发现 LPO 交叉验证
-
发现时间序列交叉验证
技术要求
我们将学习如何使用 Scikit-Learn 包实现每种评估策略。为了确保你能重现本章中的代码示例,你需要以下内容:
-
Python 3(版本 3.7 或以上)
-
安装 pandas 包(版本 1.3.4 或以上)
-
安装 Scikit-Learn 包(版本 1.0.1 或以上)
本章的所有代码示例都可以在 GitHub 上找到,链接为github.com/PacktPublishing/Hyperparameter-Tuning-with-Python/blob/main/01_Evaluating-Machine-Learning-Models.ipynb。
理解过拟合的概念
过拟合发生在训练的机器学习模型从给定的训练数据中学习过多时。在这种情况下,训练模型在训练数据上成功获得高评估分数,但在新的、未见过的数据上分数远低。换句话说,训练的机器学习模型未能将训练数据中学习到的知识泛化到未见过的数据。
那么,训练的机器学习模型如何在训练数据上获得良好的性能,但在未见数据上却无法给出合理的性能呢?嗯,这发生在模型过于努力地试图在训练数据上获得高绩效,并吸收了仅适用于特定训练数据的知识。当然,这会负面影响模型的泛化能力,导致模型在未见数据上的评估表现不佳。
为了检测我们的训练机器学习模型是否面临过拟合问题,我们可以监控模型在训练数据与未见数据上的性能。性能可以定义为我们的模型损失值或我们关心的指标,例如准确率、精确度和平均绝对误差。如果训练数据的性能持续改善,而未见数据的性能开始变得停滞甚至变差,那么这就是过拟合问题的迹象(参见图 1.1):

图 1.1 – 模型在训练数据与未见数据(过拟合)上的性能
注意
前面的图表图像是根据指定的许可证复制的:commons.wikimedia.org/wiki/File:Overfitting_svg.svg.
现在你已经了解了过拟合问题,我们需要学习如何在我们的机器学习开发生命周期中防止这种情况发生。我们将在接下来的章节中讨论这个问题。
创建训练、验证和测试集
我们知道可以通过监控模型在训练数据与未见数据上的性能来检测过拟合,但未见数据究竟是什么?它仅仅是模型在训练阶段尚未见过的随机数据吗?
未见数据是我们原始完整数据的一部分,在训练阶段模型没有看到这部分数据。我们通常将这部分未见数据称为测试集。让我们假设你一开始有 100,000 个数据样本;你可以取出其中的一部分,比如说 10%,作为测试集。因此,现在我们有 90,000 个样本作为训练集,10,000 个样本作为测试集。
然而,仅仅将我们的原始数据拆分为训练集和测试集还不够,还需要拆分为一个验证集,尤其是当我们想要对模型进行超参数调整时。假设我们有 100,000 个原始样本,我们将其中的 10%保留为验证集,另外 10%作为测试集。因此,我们将有 80,000 个样本作为训练集,10,000 个样本作为验证集,10,000 个样本作为测试集。
你可能想知道为什么我们除了测试集之外还需要验证集。实际上,如果我们不想进行超参数调整或其他以模型为中心的方法,我们不需要它。这是因为拥有验证集的目的是使用训练模型的最终版本对测试集进行无偏评估。
验证集可以帮助我们获得对测试集的无偏评估,因为我们只在超参数调整阶段包含验证集。一旦我们完成超参数调整阶段并得到最终的模型配置,我们就可以在纯未见过的新数据上评估我们的模型,这被称为测试集。
重要提示
如果你打算执行任何数据预处理步骤(例如,缺失值填充、特征工程、标准化、标签编码等),你必须基于训练集构建函数,然后将其应用于验证集和测试集。不要在数据拆分之前对完整原始数据进行数据预处理(即,在数据拆分之前)。这是因为这可能会导致数据泄露问题。
在选择训练集、验证集和测试集的比例时,并没有特定的规则。你必须根据你所面临的情况自行选择拆分比例。然而,数据科学社区常用的拆分比例是训练集 8:2 或 9:1,验证集和测试集分别为 8:2 或 9:1。通常,验证集和测试集的比例为 1:1。因此,常用的拆分比例是训练集 8:1:1 或 9:0.5:0.5,分别对应训练集、验证集和测试集。
既然我们已经了解了训练集、验证集和测试集的概念,我们需要学习如何构建这些集合。我们只是随机地将原始数据拆分为三个集合吗?或者我们也可以应用一些预定义的规则?在下一节中,我们将更详细地探讨这个话题。
探索随机和分层拆分
将我们的原始完整数据直接拆分为训练集、验证集和测试集的最直接方法(但并非完全正确的方法)是选择每个集合的比例,然后根据索引的顺序直接将它们拆分为三个集合。
例如,原始完整数据有 100,000 个样本,我们希望将其按 8:1:1 的比例分割成训练集、验证集和测试集。那么,训练集将是索引从 1 到 80,000 的样本。验证集和测试集将分别是索引从 81,000 到 90,000 和 91,000 到 100,000。
那么,这种方法有什么问题吗?只要原始完整数据被随机打乱,这种方法就没有问题。如果样本索引之间存在某种模式,可能会引起问题。
例如,我们拥有包含 10,000 个样本和 3 列的数据。第一列和第二列分别包含体重和身高信息。第三列包含“体重状态”类别(例如,体重不足、正常体重、超重和肥胖)。我们的任务是构建一个机器学习分类器模型,根据一个人的体重和身高预测其“体重状态”类别。如果数据是按照第三列排序给出的,这种情况并非不可能。因此,前 80,000 行只包含体重不足和正常体重类别。相比之下,超重和肥胖类别仅位于最后 20,000 行。如果这种情况发生,并且我们应用之前的数据分割逻辑,那么我们的分类器将无法预测一个新的人具有超重或肥胖的“体重状态”类别。为什么?因为我们的分类器在训练阶段从未见过这些类别!
因此,确保原始完整数据首先被随机打乱是非常重要的,本质上,这就是我们所说的随机分割。随机分割的工作原理是首先打乱原始完整数据,然后根据索引顺序将其分割成训练集、验证集和测试集。
还有一种称为分层分割的分割逻辑。这种逻辑确保训练集、验证集和测试集将获得与原始完整数据中每个目标类别相似的样本比例。
以相同的“体重状态”类别预测案例为例,假设我们发现全原始数据中每个类别的比例分别是体重不足 3:5:1.5:0.5、正常体重、超重和肥胖。分层分割逻辑将确保我们可以在训练集、验证集和测试集中找到这些类别的相似比例。因此,在 80,000 个训练样本中,大约有 24,000 个样本属于体重不足类别,大约有 40,000 个样本属于正常体重类别,大约有 12,000 个样本属于超重,大约有 4,000 个样本属于肥胖类别。这也会应用到验证集和测试集。
剩下的问题是理解 何时是使用随机分割/分层分割逻辑的正确时机。通常,当面临不平衡类别问题时,我们会使用分层分割逻辑。然而,当我们要确保基于特定变量(不一定必须是目标类别)具有相似样本比例时,分层分割也经常被使用。如果你没有遇到这种情况,那么随机分割就是你可以始终选择的逻辑。
要实现这两种数据分割逻辑,你可以从头开始编写代码,或者利用名为 Scikit-Learn 的知名包。以下是一个以 8:1:1 的比例执行随机分割的示例:
from sklearn.model_selection import train_test_split
df_train, df_unseen = train_test_split(df, test_size=0.2, random_state=0)
df_val, df_test = train_test_split(df_unseen, test_size=0.5, random_state=0)
df 变量是我们存储在 Pandas DataFrame 对象中的完整原始数据。train_test_split 函数将 Pandas DataFrame、数组或矩阵分割成打乱的训练集和测试集。在第 2-3 行中,首先,我们根据 test_size 参数指定的比例 8:2 将原始完整数据分割成 df_train 和 df_unseen。然后,我们将 df_unseen 按比例 1:1 分割成 df_val 和 df_test。
要执行 分层分割 逻辑,你只需将 stratify 参数添加到 train_test_split 函数中,并用目标数组填充它:
df_train, df_unseen = train_test_split(df, test_size=0.2, random_state=0, stratify=df['class'])
df_val, df_test = train_test_split(df_unseen, test_size=0.5, random_state=0, stratify=df_unseen['class'])
stratify 参数将确保数据根据给定的目标数组以分层的方式进行分割。
在本节中,我们学习了在执行数据分割之前对原始完整数据进行洗牌的重要性,也理解了随机分割和分层分割之间的区别,以及何时使用每种分割。在下一节中,我们将开始学习数据分割策略的变体,以及如何使用 Scikit-learn 包实现每种策略。
探索 k 折交叉验证
交叉验证是一种通过在原始完整数据上执行多次评估来评估我们的机器学习模型的方法。这与我们在前几节中学到的标准训练-验证-测试分割有所不同。此外,随机分割和分层分割的概念也可以应用于交叉验证。
在交叉验证中,我们对训练集和验证集执行多次分割,其中每个分割通常被称为 折。那么测试集呢?嗯,它仍然作为纯未见过数据存在,我们可以用它来测试最终的模型配置。因此,在开始时,它只从训练集和验证集中分离一次。
交叉验证策略有几种变体。第一种被称为 k 折交叉验证。它通过在每个折中执行 k 次训练和评估,分别以 (k-1):1 的比例对训练集和验证集进行训练和评估来实现。为了更清楚地理解 k 折交叉验证,请参考 图 1.2:

图 1.2 – K 折交叉验证
注意
以下图表是根据指定的许可证复制的:commons.wikimedia.org/wiki/File:K-fold_cross_validation.jpg。
例如,让我们选择 k = 4 来匹配 图 1.2 中的插图。绿色和红色球对应于目标类别,在这种情况下,我们只有两个目标类别。数据在之前已经被打乱,这可以从绿色和红色球没有模式中看出。也值得提到的是,打乱之前只进行了一次。这就是为什么绿色和红色球的顺序在每次迭代(折)中总是相同的。每个折中的黑色框对应于验证集(测试数据在插图上)。
如您在 图 1.2 中所见,训练集与验证集的比例是 (k-1):1,或者在这个例子中,3:1。在每一折中,模型将在训练集上训练并在验证集上评估。请注意,训练集和验证集在每个折中都是 不同的。最终评估分数可以通过取所有折的平均分数来计算。
总结来说,k 折交叉验证的工作原理如下:
-
打乱原始完整数据。
-
保留测试数据
-
在原始完整数据集的其余部分执行 k 折多次评估策略。
-
通过取所有折的平均分数来计算最终评估分数。
-
使用最终模型配置评估测试数据。
您可能会问,为什么我们最初需要进行交叉验证?为什么传统的训练-验证-测试分割策略不够用?我们需要应用交叉验证策略有几个原因:
-
只有少量训练数据。
-
为了从评估性能中获得更自信的结论。
-
为了更清晰地了解我们模型的学习能力以及/或给定数据的复杂性。
前两个原因是相当直接的。第三个原因更有趣,应该进行讨论。交叉验证如何帮助我们更好地了解模型的学习能力以及/或数据复杂性?好吧,这发生在每个折的评估分数变化相当大的时候。例如,在 4 个折中,我们得到了准确率分数 45%、82%、64% 和 98%。这种情况应该激发我们的好奇心:我们的模型和数据有什么问题?可能是数据太难学习,或者我们的模型不能正确学习。
以下是通过 Scikit-Learn 包执行 k 折交叉验证的语法:
From sklearn.model_selection import train_test_split, Kfold
df_cv, df_test = train_test_split(df, test_size=0.2, random_state=0)
kf = Kfold(n_splits=4)
for train_index, val_index in kf.split(df_cv):
df_train, df_val = df_cv.iloc[train_index], df_cv.iloc[val_index]
#perform training or hyperparameter tuning here
注意,首先,我们在进行 k 折交叉验证时保留测试集,并且只使用df_cv。默认情况下,Kfold函数将禁用洗牌过程。然而,这对我们来说不是问题,因为我们调用train_test_split函数时数据已经预先洗牌了。如果你想再次运行洗牌过程,可以在Kfold函数中传递shuffle=True。
如果你感兴趣学习如何在 k 折交叉验证中应用分层分割的概念,这里有一个例子:
From sklearn.model_selection import train_test_split, StratifiedKFold
df_cv, df_test = train_test_split(df, test_size=0.2, random_state=0, stratify=df['class'])
skf = StratifiedKFold(n_splits=4)
for train_index, val_index in skf.split(df_cv, df_cv['class']):
df_train, df_val = df_cv.iloc[train_index], df_cv.iloc[val_index]
#perform training or hyperparameter tuning here
唯一的区别是导入StratifiedKFold而不是Kfold函数,并添加目标变量的数组,这将用于以分层的方式分割数据。
在本节中,你学习了什么是交叉验证,何时是进行交叉验证的正确时机,以及第一个(也是最广泛使用)的交叉验证策略变体,即 k 折交叉验证。在随后的章节中,我们还将学习其他交叉验证的变体以及如何使用 Scikit-Learn 包实现它们。
发现重复 k 折交叉验证
重复 k 折交叉验证涉及简单地重复执行 k 折交叉验证,N次,每次重复都有不同的随机化。最终的评估分数是所有重复中所有折的分数的平均值。这种策略将增加我们对模型自信。
那么,为什么重复 k 折交叉验证呢?为什么我们不直接增加 k 的值呢?当然,增加 k 的值将减少模型估计性能的偏差。然而,增加 k 的值会增加变异,尤其是在样本数量较少的情况下。因此,通常重复 k 折是获得对模型估计性能更高自信的更好方法。当然,这也伴随着一个缺点,那就是计算时间的增加。
要实现这一策略,我们可以简单地执行一个手动循环,在每次循环中应用 k 折交叉验证策略。幸运的是,Scikit-Learn 包为我们提供了一个特定的函数来实现这一策略:
from sklearn.model_selection import train_test_split, RepeatedKFold
df_cv, df_test = train_test_split(df, test_size=0.2, random_state=0)
rkf = RepeatedKFold(n_splits=4, n_repeats=3, random_state=0)
for train_index, val_index in rkf.split(df_cv):
df_train, df_val = df_cv.iloc[train_index], df_cv.iloc[val_index]
#perform training or hyperparameter tuning here
选择n_splits=4和n_repeats=3意味着我们将有 12 个不同的训练和验证集。最终的评估分数就是这 12 个分数的平均值。正如你所预期的,也有一个专门的功能来实现分层重复 k 折:
from sklearn.model_selection import train_test_split, RepeatedStratifiedKFold
df_cv, df_test = train_test_split(df, test_size=0.2, random_state=0, stratify=df['class'])
rskf = RepeatedStratifiedKFold(n_splits=4, n_repeats=3, random_state=0)
for train_index, val_index in rskf.split(df_cv, df_cv['class']):
df_train, df_val = df_cv.iloc[train_index], df_cv.iloc[val_index]
#perform training or hyperparameter tuning here
RepeatedStratifiedKFold函数将重复执行分层 k 折交叉验证,n_repeats次。
现在你已经学习了另一种交叉验证策略的变体,称为重复 k 折交叉验证,接下来让我们了解其他变体。
发现留一法交叉验证
实际上,Leave One Out (LOO)交叉验证只是 k-fold 交叉验证,其中 k = n,n 是样本数量。这意味着每个折叠中有 n-1 个样本用于训练集,1 个样本用于验证集(见图 1.3)。毫无疑问,这是一个非常计算密集型的策略,并将导致非常高的方差评估分数估计器:

图 1.3 – LOO 交叉验证
因此,何时选择 LOO 而不是 k-fold 交叉验证?嗯,当数据集非常小的时候,LOO 效果最好。如果你更倾向于模型性能估计的高置信度而不是计算成本的限制,那么选择 LOO 比 k-fold 更好。
从头开始实现这种策略实际上非常简单。我们只需要遍历数据的每个索引并进行一些数据处理。然而,Scikit-Learn 包也提供了 LOO 的实现,我们可以使用它:
from sklearn.model_selection import train_test_split, LeaveOneOut
df_cv, df_test = train_test_split(df, test_size=0.2, random_state=0)
loo = LeaveOneOut()
for train_index, val_index in loo.split(df_cv):
df_train, df_val = df_cv.iloc[train_index], df_cv.iloc[val_index]
#perform training or hyperparameter tuning here
注意,在LeaveOneOut函数中没有提供任何参数,因为这种策略非常直接,不涉及任何随机过程。也没有 LPO 的分层版本,因为验证集将始终包含一个样本。
既然你已经了解了 LOO 的概念,在下一节中,我们将学习 LOO 的轻微变化。
发现 LPO 交叉验证
LPO 交叉验证是 LOO 交叉验证策略的一种变体,其中每个折叠的验证集包含p个样本,而不是仅包含 1 个样本。类似于 LOO,这种策略将确保我们得到所有可能的训练-验证对组合。更精确地说,如果有n个样本在我们的数据中,将会有
个折叠。例如,如果我们想在有 50 个样本的数据上执行 Leave-5-Out 交叉验证,将会有
或 142,506 个折叠。
当样本数量较少且希望比 LOO 方法获得更高的模型估计性能置信度时,LPO 是合适的。当样本数量较多时,LPO 将导致折叠数量激增。
这种策略在验证集之间的重叠方面与 k-fold 或 LOO 略有不同。对于 P > 1,LPO 将导致重叠的验证集,而 k-fold 和 LOO 将始终导致非重叠的验证集。此外,请注意,LPO 与 k-fold 不同,因为当 K = N // P 时,k-fold 将始终创建非重叠的验证集,但 LPO 策略则不是这样:
from sklearn.model_selection import train_test_split, LeavePOut
df_cv, df_test = train_test_split(df, test_size=0.2, random_state=0)
lpo = LeavePOut(p=2)
for train_index, val_index in lpo.split(df_cv):
df_train, df_val = df_cv.iloc[train_index], df_cv.iloc[val_index]
#perform training or hyperparameter tuning here
与 LOO 不同,我们必须为 LPO 提供p参数,它指的是 LPO 中的 p 值。
在本节中,我们学习了 LOO 交叉验证策略的变体。在下一节中,我们将学习如何在时间序列数据上执行交叉验证。
发现时间序列交叉验证
时间序列数据在本质上具有独特的特征。与假设为独立同分布(IID)的“正常”数据不同,时间序列数据不遵循这一假设。实际上,每个样本都依赖于前面的样本,这意味着改变样本的顺序会导致不同的数据解释。
以下列出了几个时间序列数据的示例:
-
每日股市价格
-
每小时温度数据
-
每分钟网页点击次数
如果我们将之前的交叉验证策略(例如,k-fold、随机或分层拆分)应用于时间序列数据,将会出现前瞻偏差。前瞻偏差发生在我们使用数据当前模拟时间点后未来的值时。
例如,我们正在处理每小时温度数据。我们想要预测 2 小时后的温度,但我们使用的是下一小时或下一 3 小时的温度值,这本来应该是不可用的。如果我们应用之前的交叉验证策略,这种偏差很容易发生,因为那些策略仅设计在 IID 分布上良好工作。
时间序列交叉验证是一种专门为处理时间序列数据而设计的交叉验证策略。它在接受折叠的预定义值方面与 k-fold 类似,然后生成 k 个测试集。不同之处在于,数据在最初并未打乱,下一个迭代的训练集是前一个迭代的超集,这意味着训练集在迭代次数增加的过程中会不断变大。一旦我们完成了交叉验证并获得了最终的模型配置,我们就可以在测试数据上测试我们的最终模型(见图 1.4):

图 1.4 – 时间序列交叉验证
此外,Scikit-Learn 包为我们提供了一个很好的策略实现:
from sklearn.model_selection import train_test_split, TimeSeriesSplit
df_cv, df_test = train_test_split(df, test_size=0.2, random_state=0, shuffle=False)
tscv = TimeSeriesSplit(n_splits=5)
for train_index, val_index in tscv.split(df_cv):
df_train, df_val = df_cv.iloc[train_index], df_cv.iloc[val_index]
#perform training or hyperparameter tuning here
提供 n_splits=5 将确保生成五个测试集。值得注意的是,默认情况下,训练集将为第 i 个折叠的大小为
,而测试集将为
的大小。
然而,您可以通过TimeSeriesSplit函数的max_train_size和test_size参数来更改训练集和测试集的大小。此外,还有一个gap参数可以被利用来排除每个训练集末尾的 G 个样本,其中 G 是开发者需要指定的值。
您需要意识到,Scikit-Learn 的实现将始终确保测试集之间没有重叠,这实际上并不是必要的。目前,使用 Scikit-Learn 实现无法启用测试集之间的重叠。您需要从头编写代码来执行这种策略。
在本节中,我们学习了时间序列数据的独特特征以及如何对其执行交叉验证策略。本书中未涵盖交叉验证策略的其他变体。如果您感兴趣,您可能会在进一步阅读部分找到一些线索。
摘要
在本章中,我们学习了许多关于如何正确评估机器学习模型的重要知识。从过拟合的概念出发,探讨了多种数据拆分策略,以及如何根据具体情况选择最佳的数据拆分策略,以及如何使用 Scikit-Learn 包实现这些策略。理解这些概念非常重要,因为如果不应用适当的数据拆分策略,您无法进行良好的超参数调整过程。
在下一章中,我们将讨论超参数调整。我们将不仅讨论定义,还会讨论一些误解和超参数分布的类型。
进一步阅读
在本章中,我们涵盖了众多主题。然而,由于本书的范畴限制,仍有许多与交叉验证相关的有趣算法未被涉及。如果您想了解更多关于这些算法以及每个算法的实现细节,可以参考 Scikit-Learn 作者创建的这篇精彩的页面:scikit-learn.org/stable/modules/cross_validation.html。
第二章:第二章:介绍超参数调整
每个机器学习(ML)项目都应该有一个明确的目标和成功指标。成功指标可以是商业和/或技术指标。评估商业指标很困难,通常只能在 ML 模型投入生产后才能进行评估。另一方面,评估技术指标更为直接,可以在开发阶段进行。作为 ML 开发者,我们希望实现我们能得到的最佳技术指标,因为这是我们能够优化的。
在本章中,我们将学习优化所选技术指标(称为超参数调整)的几种方法之一。我们将从理解超参数调整是什么以及其目标开始本章。然后,我们将讨论超参数与参数之间的区别。我们还将学习超参数空间以及你可能在实践中遇到的超参数值的可能分布。
到本章结束时,你将理解超参数调整的概念以及超参数本身。理解这些概念对于你获得对下一章将要讨论的内容的更全面了解至关重要。
在本章中,我们将涵盖以下主要主题:
-
什么是超参数调整?
-
揭秘超参数与参数的区别
-
理解超参数空间和分布
什么是超参数调整?
超参数调整是一个过程,通过这个过程,我们从所有候选集中搜索 ML 模型的最佳超参数集。它是优化我们关心的技术指标的过程。超参数调整的目标仅仅是确保在验证集上获得最大的评估分数,而不引起过拟合问题。
超参数调整是优化模型性能的以模型为中心方法之一。在实践中,当涉及到优化模型性能时,建议优先考虑以数据为中心的方法而不是以模型为中心的方法。以数据为中心意味着我们专注于清理、采样、增强或修改数据,而以模型为中心意味着我们专注于模型及其配置。
为了理解为什么以数据为中心比以模型为中心更重要,让我们假设你是一家餐厅的厨师。当涉及到烹饪时,无论你的厨房设备多么昂贵和复杂,如果原料状况不佳,你就无法为顾客提供高质量的食品。在这个类比中,原料指的是数据,而厨房设备指的是模型及其配置。无论我们的模型多么复杂和花哨,如果我们最初没有良好的数据或特征,我们就无法实现最大的评估分数。这可以用著名的说法来表达,垃圾输入,垃圾输出(GIGO)。
在以模型为中心的方法中,我们在找到最合适的模型框架或架构之后进行超参数调整。因此,可以说超参数调整是优化模型性能的最终步骤。
既然你已经了解了超参数调整及其目的,让我们来讨论超参数本身。究竟什么是超参数?超参数和参数之间的区别是什么?我们将在下一节中讨论这个问题。
揭秘超参数与参数的区别
超参数和参数之间的关键区别在于其值的生成方式。参数的值是在模型训练阶段由模型生成的。换句话说,其值是从给定的数据中学习得到的,而不是由开发者提供的。另一方面,超参数的值是由开发者提供的,因为它不能从数据中估计出来。
参数就像是模型的“心脏”。参数估计不准确会导致模型表现不佳。实际上,当我们说我们在训练一个模型时,实际上意味着我们在向模型提供数据,以便模型可以估计其参数的值,这通常是通过执行某种优化算法来完成的。以下是机器学习(ML)中参数的几个例子:
-
线性回归中的系数(
) -
多层感知器(MLP)中的权重(
)
另一方面,超参数是一组支持模型训练过程的值。它们由开发者定义,而不了解它们对模型性能的确切影响。这就是为什么我们需要进行超参数调整,以从我们的模型中获得最佳效果。搜索过程可以通过穷举搜索、启发式搜索、贝叶斯优化或多保真优化来完成,这些将在接下来的章节中讨论。以下是超参数的几个例子:
-
神经网络(NN)中的 dropout 率、epoch 数和批量大小
-
决策树中的最大深度和分割标准
-
随机森林中的估计器数量
你还需要意识到,有些模型既没有超参数也没有参数,但两者都不具备。例如,线性回归模型是一个只有参数但没有超参数的模型。另一方面,K-最近邻(KNN)是一个不包含任何参数但有一个k超参数的模型实例。
当我们开始编写代码并开发机器学习模型时,可能会出现更多的混淆。在编程中,特定函数或类中的参数也经常被称为参数。如果我们使用实现机器学习模型的类,比如决策树模型,我们应该如何称呼需要传递给类的最大深度或分裂标准参数?它们是参数还是超参数?正确的答案是两者都是!它们是类的参数,同时也是决策树模型的超参数。这只是一个视角问题!
在本节中,我们学习了超参数和参数的概念,以及它们之间的区别。在下一节中,我们将更深入地探讨超参数的领域。
理解超参数空间和分布
超参数空间被定义为所有可能的超参数值组合的集合——换句话说,它是包含所有可能用作超参数调整阶段搜索空间的超参数值的集合。这就是为什么它也经常被称为超参数调整的搜索空间。这个空间在超参数调整阶段之前是预定义的,以便搜索只在这个空间内进行。
例如,我们想在神经网络上执行超参数调整。假设我们想搜索 dropout 率、epoch 数和批大小超参数的最佳值。
dropout 率在本质上是有界的。它的值只能在0和1之间,而对于 epoch 数和批大小超参数,从理论上讲,我们可以指定任何正整数值。然而,我们还需要考虑其他因素。通常,较大的批大小会产生更好的模型性能,但它将受到我们物理计算机内存大小的限制。至于 epoch 数,如果我们选择过高的值,我们更有可能遇到过拟合问题。这就是为什么我们需要为可能超参数的值设置边界,我们称之为超参数空间。
超参数可以是离散或连续值的形式。离散超参数可以是整数或字符串数据类型,而连续超参数将始终是实数或浮点数据类型。
在定义超参数空间时,对于某些超参数调整方法,仅指定我们关心的每个超参数的可能值是不够的。我们还需要定义每个超参数的潜在分布。在这里,分布充当某种策略,决定了在超参数调整阶段测试特定值的可能性。如果是一个均匀分布,那么所有可能的值都有相同的选择概率。
可以使用的概率分布类型有很多:均匀分布、对数均匀分布、正态分布、对数正态分布等等。在选择合适的分布时没有最佳实践;您只需将其视为另一个超参数。值得注意的是,有一些分布专门用于连续超参数,也有一些用于离散超参数。对于离散超参数分布,有些分布专门设计用于离散值——例如,整数均匀分布——但也有从连续分布调整而来的分布。后一种类型的离散分布通常在其名称上带有离散化或量化前缀——例如,量化均匀分布。
还值得注意的是,并非所有超参数对模型性能的影响都是同等重要的——这就是为什么建议您优先考虑。我们不必对模型的所有超参数进行超参数调优——只需关注更重要的超参数。
在本节中,我们学习了超参数空间以及超参数分布的概念,并探讨了实践中可能遇到的超参数分布的例子。
摘要
在本章中,我们学习了关于超参数调优所需了解的所有内容,从它是什么、其目标是什么以及何时应该进行超参数调优开始。我们还讨论了超参数与参数之间的区别、超参数空间的概念以及超参数分布的概念。对超参数调优的概念和超参数本身有一个清晰的了解将极大地帮助您在接下来的章节中。
如前所述,本书将讨论所有四种超参数调优方法的类别。在第三章《探索穷举搜索》中,我们将开始讨论第一组,也是实践中最广泛使用的超参数调优方法。这里将提供高级和详细的解释,以帮助您更容易地理解每种方法。
第三章:第三章:探索穷举搜索
超参数调整并不总是对应于复杂和花哨的搜索算法。实际上,一个简单的for循环或基于开发者直觉的手动搜索也可以用来实现超参数调整的目标,即在验证分数上获得最大评估分数,同时不引起过拟合问题。
在本章中,我们将讨论四个超参数调整组中的第一个,称为穷举搜索。这是实践中最广泛使用和最直接的超参数调整组。正如其名称所解释的,属于这一组超参数调整方法通过穷举搜索超参数空间来工作。除了一个方法外,这一组中的所有方法都被归类为无信息搜索算法,这意味着它们不会从之前的迭代中学习以在未来获得更好的搜索空间。本章将讨论三种方法:手动搜索、网格搜索和随机搜索。
到本章结束时,你将理解属于穷举搜索组的每种超参数调整方法的概念。当有人问你关于这些方法时,你将能够自信地解释这些方法,无论是从高层次还是详细的角度,包括它们的优缺点。更重要的是,你将能够以高信心将所有这些方法应用于实践中。你还将能够理解如果出现错误或意外结果时会发生什么,并理解如何设置方法配置以匹配你的具体问题。
本章将涵盖以下主要主题:
-
理解手动搜索
-
理解网格搜索
-
理解随机搜索
理解手动搜索
手动搜索是穷举搜索组中最直接的超参数调整方法。实际上,它甚至不是一个算法!没有明确的规则来执行这种方法。正如其名称所暗示的,手动搜索是基于你的直觉进行的。你只需调整超参数,直到你对结果满意为止。
这种方法是本章介绍中提到的一个例外。除了这种方法外,穷举搜索组中的其他方法都被归类为无信息搜索方法。你可能已经知道为什么这个方法是例外的原因。这是因为开发者自己学习到在每次迭代中改变特定或一组超参数的影响。换句话说,他们从之前的迭代中学习,以便在下一个迭代中获得(希望是)更好的“超参数空间”。
要进行手动搜索,请执行以下操作:
-
将原始完整数据分割成训练集和测试集(参见第一章**,评估机器学习模型)。
-
指定初始超参数值。
-
在训练集上执行交叉验证(见 第一章**,评估机器学习模型)。
-
获取交叉验证分数。
-
指定新的超参数值。
-
重复 步骤 3-5 直到您满意为止。
-
使用最终超参数值在完整训练集上训练。
-
在测试集上评估最终训练好的模型。
虽然这种方法看起来非常直接且容易操作,但实际上对于初学者来说却是 相反的。这是因为你需要真正理解模型的工作原理以及每个超参数的用法。还值得注意的是,当涉及到手动搜索时,没有超参数空间的明确定义。超参数空间可能非常狭窄或非常广泛,这取决于开发者愿意和主动进行实验的意愿。
这是手动搜索超参数调整方法的优缺点列表:

图 3.1 – 手动搜索:优缺点
现在您已经了解了手动搜索的工作原理,以及其优缺点,我们将学习最简单的自动超参数调整策略,这将在下一节中讨论。
理解网格搜索
for 循环测试搜索空间中所有可能超参数值。尽管许多包将网格搜索作为它们超参数调整方法实现之一,但从头开始编写自己的代码来实现这种方法非常简单。名称 grid 来自于我们必须像创建网格一样测试整个超参数空间,如下面的图示所示。

图 3.2 – 网格搜索示意图
例如,假设我们想使用网格搜索方法对随机森林进行超参数调整。我们决定只关注估计器的数量、分割标准和最大树深度超参数。然后,我们可以为每个超参数指定可能值的列表。假设我们定义超参数空间如下:
-
估计器数量:
n_estimators = [25, 50, 100, 150, 200] -
分割标准:
criterion = ["gini", "entropy"] -
最大深度:
max_depth = [3, 5, 10, 15, 20, None]
注意到对于网格搜索方法,我们不需要指定超参数的潜在分布。我们只需为每个超参数创建一个包含我们想要测试的所有值的列表。然后,我们可以从我们喜欢的包中调用网格搜索实现,或者像下面的代码片段所示,自己编写网格搜索的代码:
for n_est in n_estimators:
for crit in criterion:
for m_depth in max_depth:
#perform cross-validation here
在这个例子中,我们创建了一个包含三个层次的嵌套 for 循环,每个层次对应于搜索空间中的一个超参数。要进行一般的网格搜索,请执行以下操作:
-
将原始完整数据分割成训练集和测试集。
-
定义超参数空间。
-
构建一个 H 层的嵌套循环,其中 H 是空间中超参数的数量。
-
在每个循环中,执行以下操作:
-
在训练集上执行交叉验证
-
将交叉验证分数与超参数组合一起存储在数据结构中——例如,一个字典
-
-
使用最佳超参数组合在完整训练集上进行训练。
-
在测试集上评估最终训练好的模型。
如您从如何执行网格搜索的详细步骤中可以看到,这种方法实际上是一种暴力搜索方法,因为我们必须测试所有预定义的超参数空间的所有可能组合。这就是为什么拥有一个适当或明确定义的超参数空间非常重要。如果没有,那么我们将浪费大量时间测试所有组合。
下面是网格搜索超参数调整方法的优缺点列表:

图 3.3 – 网格搜索:优点和缺点
表 3.2中的COD表示向超参数空间添加另一个值将指数级增加实验时间。让我们用前面的例子来说明,我们在随机森林上执行了超参数调整。在我们的初始超参数空间中,有
种组合我们必须测试。如果我们只是向我们的空间添加另一个值——比如说我们向max_depth列表添加30——那么将有
种组合,或者额外的10种组合我们必须测试。这种指数行为在我们拥有更大的超参数空间时将变得更加明显!遗憾的是,也有可能在我们定义了一个大的超参数空间并花费了大量时间进行超参数调整之后,我们仍然可能错过更好的超参数值,因为它们位于预定义空间之外!
在本节中,我们学习了什么是网格搜索,它是如何工作的,以及它的优缺点。在下一节中,我们将讨论最后一种被归类为穷举搜索组的超参数调整方法:随机搜索方法。
理解随机搜索
随机搜索是穷举搜索组中的第三种也是最后一种超参数调整方法。它是一种简单但实际效果出奇的好的方法。正如其名称所暗示的,随机搜索通过在每次迭代中随机选择超参数值来工作。除此之外没有其他内容。前一次迭代中选择的超参数集不会影响该方法在下一次迭代中选择另一组超参数的方式。这就是为什么随机搜索也被归类为无信息搜索方法。
你可以在以下图表中看到随机搜索方法的示意图:

图 3.4 – 随机搜索示意图
当我们对我们的案例合适的超参数空间知之甚少或一无所知时,随机搜索通常比网格搜索更有效,这在大多数情况下都适用。与网格搜索相比,随机搜索在计算成本和寻找最佳超参数集方面也更有效率。这是因为我们不必测试每个超参数组合;我们只需让它随机运行——或者用通俗的话说,我们只需让运气发挥作用。
你可能会想知道,随机选择一组超参数如何比网格搜索在大多数情况下产生更好的调整结果。如果预定义的超参数空间与我们提供给网格搜索方法的完全相同,实际上并不是这样。我们必须提供一个更大的超参数空间,以便支持随机搜索发挥作用。更大的搜索空间并不意味着我们必须增加维度性,无论是通过扩大现有超参数的范围还是添加新的超参数。我们也可以通过增加粒度来创建更大的超参数空间。
值得注意的是,与网格搜索不同,在定义搜索空间时不需要定义超参数的分布,在随机搜索中,建议定义每个超参数的分布。在一些包的实现中,如果您没有指定分布,它将默认为均匀分布。我们将在第七章**,通过 Scikit 进行超参数调整到第十章**,使用 DEAP 和 Microsoft NNI 进行高级超参数调整中进一步讨论实现部分。
让我们用一个与我们在理解网格搜索部分看到的类似例子来更好地理解随机搜索的工作原理。除了关注估计器的数量、分割准则和最大树深度外,我们还将添加一个最小样本分割超参数到我们的空间中。与网格搜索不同,在定义搜索空间时,我们必须提供每个超参数的分布。让我们假设我们定义的超参数空间如下:
-
估计器数量:
n_estimators = randint(25,200) -
分割准则:
criterion = ["gini", "entropy"] -
最大深度:
max_depth = [3, 5, 10, 15, 20, None] -
最小样本分割:
min_samples_split = truncnorm(a=1, b=5, loc=2, scale=0.5)
如您所见,与理解网格搜索部分中的搜索空间相比,我们通过添加粒度和添加新的超参数来增加空间的大小。我们通过利用randint均匀随机整数分布,将n_estimators超参数的粒度添加到其中,范围从25到200。这意味着我们可以测试25到200之间的任何值,其中所有这些值都有相同的被测试概率。
除了通过增加粒度来增加搜索空间的大小外,我们还添加了一个新的超参数,称为min_samples_split。这个超参数具有truncnorm分布或a=1和b=5,均值为loc=2,标准差为scale=0.5。
对于criterion和max_depth,我们仍然使用与之前搜索空间相同的配置。请注意,未指定任何分布意味着我们对超参数应用均匀分布,其中所有值都有相同的概率被测试。目前,你不必担心有哪些可用的分布以及如何实现它们,因为我们将从第七章“通过 Scikit 进行超参数调整”到第十章“使用 DEAP 和 Microsoft NNI 进行高级超参数调整”进行讨论。
在随机搜索中,除了需要定义超参数空间外,我们还需要为该方法本身定义一个超参数,称为试验次数。这个超参数将控制我们希望在预定义的搜索空间上执行多少次试验或迭代。由于我们不是旨在测试空间中所有可能的组合,因此需要这个超参数;如果我们这样做,那么它将与网格搜索方法相同。还值得注意的是,由于这种方法具有随机性,我们还需要指定一个随机种子,以确保每次运行代码时都能得到完全相同的结果。
与网格搜索不同,从头实现这种方法相当繁琐,尽管这是可能的。因此,许多包支持随机搜索方法的实现。无论实现方式如何,一般来说,随机搜索的工作方式如下:
-
将原始完整数据分割成训练集和测试集。
-
定义试验次数和随机种子。
-
定义一个带有伴随分布的超参数空间。
-
生成一个迭代器,包含随机超参数组合,元素数量等于步骤 2 中定义的试验次数。
-
遍历迭代器,在每次循环中执行以下操作:
-
从迭代器中获取本次试验的超参数组合
-
在训练集上执行交叉验证
-
将交叉验证分数与超参数组合一起存储在数据结构中——例如,字典
-
-
使用最佳超参数组合在完整训练集上进行训练。
-
在测试集上评估最终训练好的模型。
请注意,在步骤 4 中生成的超参数组合中保证没有重复。
这里是随机搜索超参数调整方法的优缺点列表:
![Figure 3.5 – Random search: pros and cons
![img/B18753_03_005.jpg]
图 3.5 – 随机搜索:优点和缺点
由于无信息搜索方法的特性,随机搜索在过程中会产生高方差。随机搜索无法从过去的经验中学习,以便在下一轮迭代中更好地学习和更有效地工作。在第六章**,探索多保真优化中,我们将学习其他被归类为信息搜索方法的网格搜索和随机搜索的变体。
在本节中,我们学习了关于随机搜索所需了解的所有内容,从它是什么,如何工作,它与网格搜索的不同之处,以及这种方法的优势和劣势。
摘要
在本章中,我们讨论了四组超参数调整方法中的第一组,称为穷举搜索组。我们讨论了手动搜索、网格搜索和随机搜索。我们不仅讨论了这些方法的定义,还讨论了这些方法在高级和专业技术层面的工作原理,以及它们各自的优缺点。从现在起,当有人向你询问这些穷举搜索方法时,你应该能够自信地解释它们,并在实践中充满信心地应用所有穷举搜索方法。
在下一章中,我们将开始讨论贝叶斯优化,这是超参数调整方法的第二组。下一章的目标与本章类似,即更好地理解属于贝叶斯优化组的方法,以便你在实践中能够充满信心地利用这些方法。
第四章:第四章:探索贝叶斯优化
贝叶斯优化(BO)是四种超参数调整方法组中的第二种。与被归类为无信息搜索方法的网格搜索和随机搜索不同,属于 BO 组的所有方法都被归类为信息搜索方法,这意味着它们是从之前的迭代中学习的,以便(希望)在将来提供一个更好的搜索空间。
在本章中,我们将讨论属于 BO 组的几种方法,包括高斯过程(GP)、基于模型的算法配置序列(SMAC)、树结构帕尔森估计器(TPE)和 Metis。与第三章 探索穷举搜索类似,我们将讨论每种方法的定义、它们之间的区别、它们的工作原理以及每种方法的优缺点。
到本章结束时,你将能够解释 BO 及其变体,当有人问你时。你不仅能够解释它们是什么,而且能够以高级和技术的角度解释它们是如何工作的。你还将能够说出它们之间的区别,以及每种方法的优缺点。此外,一旦你理解了每种方法的来龙去脉,你将体验到一项关键的好处;那就是,你将能够理解如果出现错误或意外结果时发生了什么,并理解如何设置方法配置以匹配你的特定问题。
本章将涵盖以下主要内容:
-
介绍 BO
-
理解 BO GP
-
理解 SMAC
-
理解 TPE
-
理解 Metis
介绍 BO
BO 被归类为一种基于信息的搜索超参数调整方法,这意味着搜索是从之前的迭代中学习的,以便在下一个迭代中有一个(希望)更好的子空间。它也被归类为基于模型的序列优化(SMBO)组。所有 SMBO 方法都是通过顺序更新概率模型来估计一组超参数对其性能的影响,基于历史观察数据,并在接下来的试验中建议要测试的新超参数。
由于其数据高效的特性,BO 是一种流行的超参数调整方法,这意味着它需要相对较少的样本就能达到最优解。你可能想知道,BO 是如何获得这种开创性的数据高效特性的?这种特性得益于 BO 从之前的迭代中学习的能力。BO 可以通过利用概率回归模型(作为昂贵的目标函数的廉价克隆版本)和获取函数(它控制下一个迭代中应该测试哪组超参数)来学习和预测未来值得访问的子空间。
目标函数只是一个函数,它接受超参数值作为输入并返回交叉验证分数(参见第一章,评估机器学习模型)。我们不知道目标函数对所有可能超参数值的输出是什么。如果我们知道,就没有必要进行超参数调整。我们可以直接使用该函数来获取超参数值,这将导致获得最高的交叉验证分数。这就是为什么我们需要一个概率回归模型,通过拟合一组已知的超参数和交叉验证分数值对来近似目标函数(参见图 4.1)。近似的概念与基于机器学习的回归器模型的概念类似,例如随机森林、线性回归等。首先,我们将回归器拟合到独立变量和依赖变量的样本;然后,模型将尝试学习数据,最终可以用来预测新的给定数据。概率回归模型也常被称为代理模型:

图 4.1 – 概率回归模型,M 的示意图
获取函数控制我们在下一次迭代中应该搜索哪个子空间。多亏了这个函数,贝叶斯优化(BO)使我们能够从过去的经验中学习,并且与随机搜索相比,通常需要更少的超参数调整迭代。
重要提示
记住,为了获得交叉验证分数,我们需要执行多次训练和评估过程(参见第一章**,评估机器学习模型)。当你有一个大型的、复杂的模型以及大量的训练数据时,这是一个昂贵的流程*。这就是为什么获取函数在这里扮演着重要角色。
通常情况下,贝叶斯优化(BO)的工作原理如下:
-
将原始完整数据集分为训练集和测试集。(参见第一章**,评估机器学习模型*)
-
定义超参数空间,H及其伴随的分布。
-
根据训练集定义目标函数,f。
-
定义停止标准。通常,使用试验次数。然而,也可以使用时间或收敛性作为停止标准。
-
初始化一个空集合,D,该集合将用于存储初始的超参数值对和交叉验证分数,以及由获取函数,A建议的结果值对。
-
初始化几个超参数值对和交叉验证分数,并将它们存储在D中。
-
使用D中的值对拟合概率回归模型/代理模型,M。
-
通过利用获取函数,A,采样下一组超参数:
-
在代理模型M的帮助下,对获取函数A进行优化,以采样哪些超参数应该传递给获取函数。
-
根据获取函数A,获取预期的最佳超参数集。
-
-
使用基于步骤 8输出的目标函数f计算交叉验证得分。
-
将步骤 8和步骤 9中的超参数和交叉验证得分对添加到设置D中。
-
重复步骤 7到步骤 10,直到满足停止标准。
-
使用最终的超参数值在完整训练集上进行训练。
-
在测试集上评估最终训练好的模型。
你可以使用几种采样策略来初始化超参数值和交叉验证得分,如步骤 6所示。在实践中,最直接和常用的方法是进行随机采样。然而,在实验过程中,你也可以考虑其他方法,例如准随机或拉丁超立方体采样方法。
与随机搜索类似,在贝叶斯优化(BO)中,我们还需要定义每个超参数的分布。你可能想知道 BO 是否也可以用于非数值类型的超参数。答案是基于你使用的概率回归模型。你可以选择几种代理模型。这些选项将在本章接下来的三个部分中讨论,包括高斯过程(GP)、树结构帕累托估计器(TPE)、随机森林、额外树或其他基于机器学习的回归器。在这本书中,我们将讨论 SMAC 模型中实现的随机森林回归器。
值得注意的是,步骤 8中的优化过程可以被随机搜索所替代。因此,我们不必执行某种二阶优化方法,而是可以从搜索空间中随机采样一组超参数,并将它们传递给获取函数。然后,我们可以根据获取函数的输出获取最佳超参数集。当在此步骤中使用随机搜索时,我们仍然利用获取函数来指导我们在下一次迭代中应该搜索哪个子空间,但我们添加了一些随机行为,希望可以逃离局部最优并收敛到全局最优。
最先且最受欢迎的获取函数是期望改进(EI),其定义如下:
当
时,
。
当
时,
。
在这里,
、
和
分别表示标准正态分布的累积分布函数和概率密度函数。
和
分别代表由代理模型捕捉到的预期性能和不确定性。最后,
代表目标函数的当前最佳值。
暗示地,EI 获取函数使得 BO 方法具有探索与利用的权衡特性。这种特性可以通过公式内的两个术语之间的竞争来实现。当第一个术语的值很高时,意味着预期性能
高于当前最佳值
,EI 将倾向于利用过程。另一方面,当不确定性非常高时,意味着我们有一个高值
,EI 将倾向于探索过程。通过利用,这意味着获取函数将推荐可能获得目标函数f更高值的超参数集。至于探索,这意味着获取函数将推荐来自我们尚未探索的子空间的超参数集。
你可以将这种探索与利用的权衡想象成当你渴望食物的时候。比如说,你今天想和你的兄弟一起吃午饭。想象以下两种情况:
-
“嘿,兄弟,我们今天就去我们最喜欢的餐厅吃午饭吧!”
-
“嘿,兄弟,你听说过那里的新餐厅吗?我们为什么不去那里吃午饭呢?”
在第一种情况下,你选择在你最喜欢的餐厅用餐,因为你确信食物没有问题,更重要的是,你对这家餐厅的食物和整体用餐体验非常有信心。这个第一种情况最好地解释了我们所说的利用过程。在第二种情况下,你对那家新餐厅的整体用餐体验一无所知。它可能比你最喜欢的餐厅差,但也可能成为你新的最爱!这就是我们所说的探索过程。
重要提示
在某些实现中,例如在Scikit-optimize包中,有一个超参数可以让我们控制相对于探索,我们有多大的倾向于利用。在 Scikit-optimize 中,EI 函数的符号是负的。这是因为该包默认将优化问题视为最小化问题。
在我们之前的解释中,我们将优化问题视为最大化问题,因为我们想要得到尽可能高的交叉验证分数。不要将这个问题与最小化与最大化问题混淆——只需选择最能描述您在实践中将面临的问题即可!
以下是在 Scikit-optimize 包中实现的 EI 获取函数:

如您在第一项中看到的,
的值将控制我们向利用倾斜的程度与探索相比。
的值越小,我们越倾向于利用。我们将从第七章**,通过 Scikit 进行超参数调整到第十章**,使用 DEAP 和 Microsoft NNI 进行高级超参数调整中学习更多关于使用 Scikit 或其他包实现 BO 的细节。
为了更好地理解在超参数调整阶段探索与利用权衡是如何发生的,让我们看一个例子。比如说,我们正在使用 GP 代理模型来估计以下目标函数。现在不需要担心 GP 是如何工作的,我们将在下一节中详细讨论:

在这里,
是遵循标准正态分布的噪声。以下是在
范围内的该函数的绘图。请注意,在这个例子中,我们假设我们知道真正的目标函数是什么。然而,在实践中,这个函数是未知的:


图 4.2 – 目标函数 f(x)的绘图
假设我们正在使用 EI 作为获取函数,将试验次数设置为15,将初始点数设置为5,将
的值设置为0.01。您可以在以下图中看到前五次试验的拟合过程:


图 4.3 – GP 和 EI 说明,δ = 0.01
前图中每一行对应第一次到第五次试验。左侧列包含关于目标函数(红色虚线)、目标函数的 GP 代理模型近似(绿色虚线)、近似的确信度(绿色透明区域)以及到每次试验为止的观测点(红色点)。右侧列包含关于 EI 获取函数值(蓝色线)和下一个要包含在下次试验中的点(蓝色点)的信息。
让我们逐行分析图 4.3中的每一行,以便您了解它是如何工作的。在第一次试验中(参见左列顶部的第一行),我们初始化五个随机样本点——或者说是超参数调整中的超参数值,然后基于这五个点拟合 GP 模型。请记住,GP 模型不知道实际的目标函数;它所拥有的唯一信息就是那五个随机点。然后(参见右列顶部的第一行),基于拟合的 GP 模型,我们在整个空间中获取 EI 获取函数的值。在这种情况下,空间只是一个范围——也就是说,
。我们还得到了下一个试验要包含的点,在这个例子中大约是点0.5。
在第二次试验中,我们利用 EI 获取函数建议的点,并基于我们已有的六个样本点再次拟合 GP 模型(参见左列顶部的第二行)。如果您比较第二次试验的 GP 近似和第一次试验,您会看到它更接近真实的目标函数。接下来(参见右列顶部的第二行),我们重复同样的过程,即在整个空间中生成 EI 函数值和下一个试验要包含的点。在这个步骤中建议的点大约是0.7。
我们会重复进行同样的过程,直到满足停止标准,在这个例子中是 15 次试验。下面的图表显示了 15 次试验后的结果。这比第一次试验的近似要好得多(参见绿色虚线)!您还可以看到,在
的一些范围内,高斯过程近似的置信度很高,例如在点-1.5和1.6附近:


图 4.4 – 15 次试验后的结果,δ = 0.01
根据前面的图表,最终建议的点,或者说超参数值,是-1.5218,这导致目标函数的值为-1.9765。让我们也看看从第一次试验到最后一次试验的收敛图。从下面的收敛图中,我们可以看到我们的代理模型和获取函数是如何帮助我们根据所有试验得到目标函数的最小值的:


图 4.5 – 收敛图
现在,让我们尝试将
的值改为比我们之前更低的值,以看看 EI 获取函数将如何更多地偏向利用而不是探索。让我们将
的值设置为比之前低 1000 倍。请注意,我们只改变了
的值,其他设置保持不变:


图 4.6 – GP 和 EI 说明,δ = 0.00001
如您所见,EI 获取函数建议了介于 0.5 和 1.4 之间的大多数点。获取函数不建议探索
范围,尽管我们可以在该范围内获得更低的目标函数值。这是因为该范围内没有初始随机点,在这个例子中,我们非常倾向于利用。以下图表显示了 15 次试验后的最终结果。在这种情况下,当我们更倾向于利用而不是探索时,我们得到了更差的结果。然而,这并不总是如此。您必须进行实验,因为不同的数据、不同的目标函数、不同的超参数空间和不同的实现可能会导致不同的结论:

图 4.7 – 15 次试验后的结果,δ = 0.00001
现在,让我们看看如果我们将
值设置为 100,这在这种情况下意味着我们更倾向于探索而不是利用,会产生什么影响。与之前的试验相似,运行 15 次试验后,我们得到了以下结果:

图 4.8 – 15 次试验后的结果,δ = 100
如您所见,由获取函数(红色点)建议的点遍布各处。这是因为我们设置了如此高的
值。这意味着获取函数的输出将建议空间中尚未观察到的点。我们将在 第七章,通过 Scikit 进行超参数调整 中学习如何生成这里显示的图表。
除了 EI 获取函数之外,还有其他一些流行的获取函数可以考虑使用,包括 改进概率(PI)和 上置信界(UCB)。
PI 是在 EI 之前存在的获取函数。它比 EI 简单 – 事实上,
的公式是根据以下简单的 改进 定义推导出来的:

的想法是返回改进的大小,如果预期性能和当前最佳性能之间存在改进,则返回改进的大小;如果没有改进,则返回零。基于
,我们可以定义 PI 如下:
当 
当 
PI 的问题在于,只要与当前最佳值
相比有所改进,它就会为所有超参数集提供相同的奖励,无论改进有多大。在实践中,这种行为并不太可取,因为它可能会 引导我们走向局部最小值并使我们陷入其中。如果你熟悉微积分和统计学,你会意识到 EI 只是
的期望,如这里所示:

在这里,
是标准正态分布的概率密度函数。与 PI 不同,EI 收敛函数会考虑改进的大小。
对于 UCB 来说,与其他方法相比,它非常直接。我们通过
参数有权力自己控制探索和利用之间的权衡。这个收敛函数可以定义为以下:

正如你所见,UCB 并没有考虑目标函数的当前最佳值。它只考虑了代理模型的预期性能和不确定性。你可以通过改变
的值来控制探索和利用之间的权衡。如果你想偏向于探索搜索空间,那么你可以增加
的值。然而,如果你想更多地关注那些预期表现良好的超参数集,那么你可以减少
的值。
除了代理模型和收敛函数的变体之外,还有基于修改算法本身的 BO 方法变体,包括 Metis 和 贝叶斯优化和 HyperBand (BOHB)。我们将在 理解 Metis 部分讨论 Metis,并在 第六章 探索多保真优化 中讨论 BOHB。
以下是与其他超参数调整方法相比,BO 超参数调整的优缺点:
](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hparam-tun-py/img/B18753_04_009.jpg)
图 4.9 – BO 的优缺点
BO 可以处理昂贵的目标函数,并且当有良好的初始点时,比随机搜索更数据高效,可以说是更好的。你可以利用我们在本节开头提到的流程中从 步骤 6 开始使用的超参数集。然而,如果你没有这种特权访问,BO 仍然可以通过给你更多时间来超越随机搜索,因为它必须首先从头开始构建一个好的代理模型,特别是如果你有一个巨大的超参数空间。一旦 BO 构建了一个好的代理模型,它往往比随机搜索更快地找到最优的超参数集。
还有另一种方法可以加速 BO 相对较慢的预热过程。这个想法是采用元学习程序,通过从其他类似数据集的元特征中学习来初始化初始超参数集。
加速 BO 的预热过程
有关更多信息,请参阅以下论文:高效且鲁棒的自动机器学习,作者 Matthias Feurer,Aaron Klein,Katharina Eggensperger,Jost Springenberg,Manuel Blum,Frank Hutter (papers.nips.cc/paper/2015/hash/11d0e6287202fced83f79975ec59a3a6-Abstract.html)。
BO 还有一个随机搜索没有的不错特性——能够控制探索和利用之间的权衡,如本节之前所述。这个特性使得 BO 不仅仅像随机搜索那样不断探索。
既然你已经了解了什么是 BO(贝叶斯优化),它是如何工作的,它的重要组件是什么,以及这种方法的优势和劣势,我们将在接下来的章节中深入探讨 BO 的变体。
理解 BO GP
贝叶斯优化高斯过程(BOGP)是 BO 超参数调整方法的一种变体。它因其良好的描述目标函数的能力而闻名。这种变体因其代理模型的独特可分析性和即使只有少量观测点也能产生相对准确的近似而非常受欢迎。
然而,BOGP(贝叶斯优化高斯过程)有其局限性。它仅适用于连续超参数,不适用于离散或分类类型的超参数。当你需要大量迭代来获得最佳超参数集时,不建议使用 BOGP,尤其是当你有大量样本时。这是因为 BOGP 的运行时间,其中
是样本数量。如果你有超过 10 个需要优化的超参数,普遍认为 BOGP 不是适合你的超参数调整方法。
以 GP(高斯过程)作为代理模型意味着我们利用 GP 作为目标函数的先验。然后,我们可以利用先验和似然模型来计算我们关心的后验。如果我们熟悉著名的贝叶斯定理,所有这些复杂的术语都可以很容易地理解。
贝叶斯定理允许我们通过利用我们已有的知识或普遍信念来计算在特定条件下事件发生的概率。形式上,贝叶斯定理定义为如下:

在这里,
是我们想要知道概率的事件,而
指的是我们之前提到的特定条件。方程的左边,
,就是我们所说的后验。
是先验,
是我们所说的似然模型。最后,
只是一个常数,以确保这个公式的结果被限制在
的范围内。
要理解贝叶斯定理,让我们通过一个例子来解释。假设我们想知道在今天是晴天的情况下,你吃你最喜欢的餐厅的概率。在这个例子中,你吃你最喜欢的餐厅是我们感兴趣的事件。这在方程中是
。今天是晴天这一信息指的是方程中的
。
假设你中有 40 天在你的最喜欢的餐厅吃饭,这意味着在知道今天的天气之前,你的
等于
。让我们再假设在 100 天中,有 30 天是晴天。那么,
的值等于
。根据你在你最喜欢的餐厅吃饭的经验,你已经意识到在晴天条件下你吃了 40 次中的 20 次。因此,似然,
,等于
。使用所有这些信息,我们可以计算出在今天是晴天的情况下,你吃你餐厅的概率,如
所示。
现在,我们准备重新审视 GP。BOGP 利用 GP 作为代理模型。GP 作为代理模型意味着我们将其作为目标函数的先验,这意味着后验分布也是一个 GP。你可以把 GP 看作是你熟悉的高斯分布的推广。与描述随机变量分布的高斯分布不同,GP 描述的是函数的分布。与伴随随机变量均值和方差的 Gaussian 分布类似,GP 也伴随函数的均值和协方差。至于似然,我们假设目标函数f遵循带有噪声的正态似然:


然后,我们可以描述
,即所有n个样本的目标函数的值,作为一个具有均值函数
和协方差核
的 GP,大小为n x n,其定义如下:

GP 的预测分布也遵循高斯分布,可以定义为以下:

在这里,
和
的值可以从核,
,中解析地推导出来。
总结来说,GP 通过遵循正态分布假设来近似目标函数。在实践中,即使我们没有零均值过程,也可以使用 GP,正如我们之前的假设。然而,我们需要对目标函数的值进行一些预处理,以便将其中心化到零。选择 合适的协方差核,
,也是至关重要的。它极大地影响了我们的超参数调整过程。在实践中最常用的核是 Matern 核。然而,我们必须为我们的案例选择合适的核,因为每个核都有可能或可能不适合我们的目标函数。我们将在 第七章,通过 Scikit 进行超参数调整中讨论 Scikit 包中可用的核。
以下表格显示了 BOGP 与其他 BO 超参数调整方法变体相比的优缺点列表:

图 4.10 – BOGP 的优缺点
在上一节中,我们看到了 GP 在实际中的应用,其中我们讨论了探索与利用的权衡。你可以回顾那个例子,通过可视化更好地理解 GP 在实际中的应用。
在本节中,我们学习了在 BO 中使用 GP 作为代理模型,以及与其他 BO 变体相比的优缺点。在下一节中,我们将学习另一种使用随机森林作为代理模型的 BO 变体。
理解 SMAC
SMAC 是 BO 超参数调整方法组的一部分,并使用随机森林作为代理模型。这种方法经过优化以处理离散或分类超参数。如果你的超参数空间很大,且主要由离散超参数主导,那么 SMAC 是你的一个不错的选择。
与 BOGP 类似,SMAC 也通过建模目标函数来工作。具体来说,它使用随机森林作为代理模型来创建对真实目标函数的估计,然后可以将该估计传递给获取函数(有关更多详细信息,请参阅 介绍 BO 部分)。
随机森林是一种机器学习(ML)算法,可用于分类或回归任务。它基于一系列决策树,已知在处理分类类型特征时表现良好。随机森林这个名字来源于它是由多个决策树构建而成的。我们将在第十一章《理解流行算法的超参数》中更详细地讨论随机森林及其超参数。
SMAC 与 BOGP 之间的主要区别在于每种方法中使用的代理模型类型。虽然 BOGP 使用高斯过程(GP)作为代理模型,但 SMAC 使用随机森林作为代理模型。在 SMAC 的原论文中使用的获取函数是经过一些修改的EI 函数,这些修改涉及在介绍 BO部分的步骤 8中的优化过程,这也可以在下面的截图中看到:


图 4.11 – 获取函数的优化过程
在 SMAC 中,类似于 BOGP,我们也假设我们的代理模型的预测分布遵循高斯分布,如下所示:

在这里,
和
的值分别来自随机森林预测的均值和方差。
我们还可以利用随机森林对一个随机森林模型进行超参数调整!这是如何实现的?一个模型如何被用来提高同类型另一个模型的表现?
这是因为我们将一个模型作为代理模型处理,而另一个模型是实际模型,它被拟合到独立变量以预测因变量。作为代理模型,随机森林将充当回归器,其目标是学习超参数空间与相应目标函数之间的关系。因此,当我们说我们在随机森林模型上使用随机森林进行超参数调整时,存在两个具有不同目标和不同输入输出对的随机森林模型!
查看以下步骤以更好地理解这个概念。请注意,以下程序替换了介绍 BO部分的步骤 7到11:
-
(最初几步与之前看到的一样)。
-
使用D中的值对拟合第一个随机森林模型,该模型作为代理模型M。记住,D由超参数值和交叉验证分数的成对值组成。
-
通过使用获取函数A采样下一组超参数:
-
在代理模型M的帮助下对获取函数进行优化,以采样要传递给获取函数的超参数。
-
根据获取函数获取最佳超参数集。
9. 使用 步骤 8 的输出,根据目标函数 f 计算交叉验证分数。请注意,交叉验证分数是基于 第二个随机森林模型 计算的,其目标是学习原始问题中自变量和因变量之间的关系。
10. 将 步骤 8 和 步骤 9 中的超参数和交叉验证分数对添加到设置 D。
11. 重复 步骤 7 到 10,直到满足停止标准。
12. (最后几步与之前看到的是相同的)。
您可能想知道,为什么还要使用与代理模型相同的机器学习算法?为什么我们不直接进行网格搜索或随机搜索呢?记住,代理模型只是全局优化算法的一部分。还有获取函数和其他优化步骤可以帮助我们更快地获得最佳超参数集。值得注意的是,我们可以利用除了随机森林之外的任何机器学习模型。当涉及到基于树的机器学习模型时,XGBoost、CatBoost 和 LightGBM 在数据科学家中也同样受欢迎,因为它们在实际应用中表现良好。
在 介绍全局优化(BO) 部分,我们看到了 GP 如何与 EI 获取函数一起估计虚拟目标函数。让我们使用这里定义的相同虚拟目标函数,并查看使用随机森林(不一定是 SMAC 算法)作为代理模型而不是 GP 的结果。在这个例子中,我们仍然将 EI 作为获取函数,并使用 Scikit-optimize 包作为实现:

在这里,
是一个遵循标准正态分布的噪声。请参见 图 4.2 以查看此虚拟目标函数的可视化。
让我们使用 Scikit-optimize 包为随机森林代理模型提供的默认值来设置试验次数和探索与利用权衡控制器,
,分别是 100 和 0.01。您可以在以下图中看到随机森林代理模型拟合过程在前五次试验中的工作情况:

图 4.12 – 随机森林和 EI 插图;δ = 0.01;试验 1 – 5
如您所见,在前五次试验中并没有发生太多事情。即使是随机森林给出的目标函数近似(见绿色虚线)仍然非常糟糕,因为它仅仅是一条直线!让我们看看在试验 71 到 75 期间的条件是什么:

图 4.13 – 随机森林和 EI 插图;δ = 0.01;试验 71- 75
在这里,我们可以看到我们的随机森林代理模型在估计真实目标函数方面有了很大的改进。一个有趣的观点是,获取函数曲线看起来与我们使用 GP 作为代理模型时看到的非常不同。在这里,获取函数看起来更锋利,就像我们通常从可视化随机森林中看到的那样。最后,让我们看看近似函数的最终形式:

图 4.14 – 100 次试验后的结果;δ = 0.01
在这里,我们可以看到随机森林通常无法拟合真实的目标函数,但它成功地关注了目标函数的局部最小值。这是因为 随机森林需要大量的数据,或者在这种情况下,观察到的点(见红色圆点),以对目标函数进行良好的近似。您还可以在以下图中看到拟合过程的收敛图,从第一次到最后的试验。如果我们比较 图 4.15 和 图 4.5,我们可以很容易地看出,在这个例子中,当有 EI 获取函数的支持时,随机森林的学习速度比 EI 支持的 GP 慢得多:

图 4.15 – 收敛图
从 图 4.14 我们还可以看到,目前我们只关注几个范围,而忽略了虚拟目标函数的全局最小值,它位于
范围附近。让我们看看将
的值更改为 100 是否可以解决这个问题。预期 EI 获取函数可以帮助随机森林代理模型在其他值范围内 探索更多。您可以在以下图中看到前五次试验的结果:

图 4.16 – 随机森林和 EI 插图;δ = 100;试验 1 – 5
与默认值的前五次试验类似,我们仍然看不到太多学习过程。让我们看看试验 71 到 75 期间的条件:

图 4.17 – 随机森林和 EI 插图;δ = 100;试验 71 – 75
在这里,我们可以看到 图 4.17 和 图 4.13 之间有很大的差异。最后,让我们看看近似函数的最终形式:

图 4.18 – 100 次试验后的结果;δ = 100
通过将
的值改为100,似乎我们的期望已经实现。随机森林代理模型的近似(见绿色虚线)现在更多地关注特定范围之外。此外,我们甚至比 GP(见图 4.4)得到了更好的结果。再次强调,这并不总是如此——你必须自己大量实验,因为不同的数据、不同的目标函数、不同的超参数空间和不同的实现可能会导致不同的结论。我们将在第七章中学习如何实现随机森林作为代理模型以及如何生成这些图表,通过 Scikit 进行超参数调整*。
另有一种方法,称为Grove 内的贝叶斯优化(BOinG),其目标是利用随机森林和 GP 作为代理模型,从两个世界中获取最佳效果。
Grove 内的贝叶斯优化
有关更多信息,请参阅以下论文:在森林中搜索局部贝叶斯优化,作者为 Difan Deng 和 Marius Lindauer (arxiv.org/abs/2111.05834)。
BOinG 通过使用两阶段优化来工作,利用全局和局部模型来降低计算成本并更多地关注有希望的子空间。在 BOinG 中,随机森林被用作全局模型,GP 作为局部模型。全局模型负责搜索局部模型的有希望子空间。因此,全局模型应该足够灵活,能够处理具有不同类型超参数的复杂问题。由于局部模型只在有希望的子空间中搜索,因此可以使用更准确但更昂贵的模型,例如 GP。
以下表格列出了利用随机森林作为代理模型与其他 BO 超参数调整方法变体的优缺点:

图 4.19 – 利用随机森林作为代理模型的优缺点
条件超参数是指在满足特定条件时才会被利用的超参数。随机森林的树结构非常适合这种情况,因为它可以添加另一个树枝来检查条件是否满足。条件通常只是空间中其他超参数的特定值或范围。
既然你已经了解了 SMAC 以及一般性地利用随机森林作为代理模型,在下一节中,我们将讨论 BO 的另一种变体,它在近似目标函数方面有不同的方法。
理解 TPE
TPE 是 BO 的另一种变体,在一般情况下表现良好,可以用于分类和连续类型的超参数。与具有立方体时间复杂度的 BOGP 不同,TPE 以线性时间运行。如果你有一个非常大的超参数空间,并且对评估交叉验证分数的预算非常紧张,建议使用 TPE。
TPE 与 BOGP 或 SMAC 之间的主要区别在于它建模超参数与交叉验证分数之间的关系。与 BOGP 或 SMAC 不同,它们近似目标函数的值或后验概率
,TPE 采用相反的方式。它试图根据目标函数的条件或似然概率
(参见 理解 BO GP 部分的贝叶斯定理解释)来获取最佳超参数。
换句话说,与构建在目标函数上的预测分布的 BOGP 或 SMAC 不同,TPE 尝试利用目标函数的信息来 建模超参数分布。更准确地说,当优化问题以 最小化问题 的形式出现时,
定义如下:

在这里,当目标函数的值低于或高于阈值时,分别使用
和
。如何选择阈值
没有具体的规则。然而,在 Hyperopt 和 Microsoft NNI 的实现中,这个阈值是基于 TPE 的超参数
和当前试验中观察到的 D 中的点的数量来选择的。
的定义告诉我们,TPE 有两个模型,根据目标函数的值作为学习算法,由阈值
控制。
当超参数的 分布是连续的 时,TPE 将利用高斯混合模型(GMMs)以及 EI 收集函数,来建议下一组要测试的超参数。如果连续分布不是高斯分布,那么 TPE 将将其转换为模拟高斯分布。例如,如果指定的超参数分布是均匀分布,那么它将被转换为截断高斯分布。
GMM 内部多项式分布的不同可能结果的概率,以及 GMM 内部正态分布的均值和方差,是由 自适应 Parzen 估计器 生成的。此估计器负责根据正态超参数分布的均值和方差,以及 D 中所有观察到的点的超参数值,构建两个概率分布
和
。
当 分布是分类或离散的 时,TPE 将将分类分布转换为重新加权的分类分布,并使用 加权随机采样 以及 EI 收集函数来建议预期的最佳超参数集。随机采样过程中的权重是基于历史超参数值计数生成的。
TPE 中的 EI 收集函数定义与我们在 介绍 BO 部分中学到的定义略有不同。在 TPE 中,我们在推导 EI 公式时使用贝叶斯定理。TPE 中 EI 收集函数的简单公式定义如下:

这里定义的成比例关系告诉我们,为了获得高 EI 值,我们需要获得高的
比率。换句话说,当优化问题是 最小化问题 形式时,EI 收集函数必须从
中建议比从
中更多的超参数。当优化问题是 最大化问题 形式时,情况则相反。例如,当我们使用准确率来衡量我们分类模型的性能时,那么我们应该从
中采样更多的超参数,而不是从
中采样。
总结来说,TPE 的工作原理如下。请注意,以下过程描述了 TPE 在 最小化问题 中的工作方式。这个过程替换了 介绍 BO 部分中的 步骤 7 到 11:
-
(前几个步骤与之前看到的一样)。
-
将超参数值和交叉验证分数的成对数据在 D 中根据阈值
分为两组,即 低于 和 高于 组(参见 图 4.19)。 -
通过使用 EI 收集函数来采样下一组超参数:
-
对于每个组,使用自适应 Parzen 估计器(如果它是连续类型)或随机采样的权重(如果它是分类类型)来计算 GMM 的概率、均值和方差。
-
对于每个组,如果它是连续类型,则拟合 GMM(高斯混合模型),如果它是分类类型,则进行随机采样,以采样哪些超参数将传递给 EI 收集函数。
-
对于每个组,计算这些样本成为好样本(对于下面的组)或坏样本(对于上面的组)的概率。
-
根据 EI 获取函数,获取预期的最佳超参数集。
-
使用第 8 步的输出,根据目标函数f计算交叉验证分数。
-
将第 8 步和第 9 步中的超参数和交叉验证分数对添加到设置D。
-
重复步骤 7到10,直到满足停止标准。
-
(最后几步与我们之前看到的是一样的):

图 4.20 – TPE 中分组划分的示意图
根据所述程序和前面的图,我们可以看到,与构建在目标函数上的预测分布的 BOGP 或 SMAC 不同,TPE 试图利用目标函数的信息来建模超参数分布。这样,我们不仅关注试验期间观察到的最佳点——我们关注的是观察到的最佳点的分布。
你可能想知道为什么“树结构”这个词在 TPE 方法的名字中。这个词指的是我们在上一节中讨论的条件超参数。这意味着在空间中有一些超参数只有在满足一定条件时才会被使用。我们将在第八章“通过 Hyperopt 进行超参数调整”和第九章“通过 Optuna 进行超参数调整”中看到树结构或条件超参数空间的样子。
TPE 的一个缺点是,由于 Parzen 估计器是单变量工作的,它可能在某个空间中忽略超参数之间的相互依赖性。然而,对于 BOGP 或 SMAC 来说并非如此,因为代理模型是基于超参数空间中的配置构建的。因此,它们可以考虑到超参数之间的相互依赖性。幸运的是,有一种 TPE 的实现可以克服这个缺点。Optuna包提供了多元 TPE实现,它可以考虑到超参数之间的相互依赖性。
以下表格列出了与其他 BO 超参数调整方法的变体相比,利用 TPE 的优缺点:

图 4.21 – TPE 的优缺点
重要提示
一些实现支持并行调整,但需要在建议的超参数质量和墙时间之间做出权衡。Microsoft NNI 包通过constant_liar_type参数支持此功能,这将在第十章“使用 DEAP 和 Microsoft NNI 进行高级超参数调整”中更详细地讨论。
在本节中,我们了解了 TPE,以及与其他 BO 变体的优缺点。在下一节中,我们将了解另一种 BO 变体,其算法与一般的 BO 方法略有不同。
理解 Metis
与一般的 BO 方法相比,Metis 是具有几个算法修改的 BO 变体之一。Metis 在其算法中利用 GP 和 GMM。GP 用作代理模型和异常值检测器,而 GMM 用作获取函数的一部分,类似于 TPE。
与其他一般的 BO 方法相比,Metis 的不同之处在于它可以比 EI 获取函数更 数据高效地平衡探索和利用。它还可以 处理不遵循高斯分布的数据噪声,这种情况在大多数情况下都是如此。与大多数执行随机采样以初始化超参数集和交叉验证分数的方法不同,D,Metis 利用 拉丁超立方抽样(LHS),这是一种基于每个超参数等间隔的分层抽样过程。这种方法被认为比随机采样更数据高效,可以达到相同的探索覆盖率。
那么,Metis 如何在观察点的需求方面比 EI 获取函数更有效地平衡探索和利用?这是通过 Metis 拥有的 自定义获取函数 实现的,该函数由三个子获取函数组成,如下所示:
- 最低置信度(LC):此子获取函数的目标是采样具有最高不确定性的超参数。换句话说,此子获取函数的目标是 最大化探索。此函数定义如下:

-
Parzen 估计器:此子获取函数受到 TPE 方法的启发,该方法利用 GMM 估计采样超参数是 低于 或 高于 组的可能性(有关更多详细信息,请参阅 理解 TPE 部分)。此子获取函数的目标是采样具有最高概率成为最佳超参数的超参数。换句话说,它是 针对利用进行优化的。
-
2.326。
基于这三个子获取函数建议的候选人,Metis 将计算他们的 信息增益 以选择最终候选者,并将其包含在下一个试验中。此选择过程是通过利用 GP 估计置信区间的下限来完成的。Metis 将测量区间下限与 GP 预期均值之间的差异。具有最高改进的候选者将被选为最终候选者。
值得注意的是,由于诊断模型,Metis 可以处理数据中的非高斯噪声。检测到的异常值使得 Metis 能够重新采样之前测试的超参数,从而使其对非高斯噪声也具有鲁棒性。这样,Metis 可以在超参数调整过程中平衡探索、利用和重新采样。
为了更好地理解 Metis 的工作原理,请查看以下过程。请注意,以下过程替换了介绍 BO部分中的步骤 6到11。
-
(前几步与之前看到的一样)。
-
使用 LHS 方法初始化几组超参数值和交叉验证分数,并将它们存储在D中。
-
使用D中的值对拟合一个作为代理模型的 GP,即M。
-
通过使用自定义获取函数来采样下一组超参数,该函数由三个子获取函数组成:
-
获取当前最佳的超参数最优集。
-
通过 LC 子获取函数获取探索建议的超参数。
-
通过 Parzen 估计器获取利用建议的超参数。
-
根据诊断模型检测到的异常值获取建议的重新采样的超参数。
-
计算每个建议候选者的信息增益。
-
选择具有最高信息增益的候选者。
-
如果没有建议的候选者,则随机选择一个。
-
使用步骤 8的输出,根据目标函数f计算交叉验证分数。请注意,交叉验证分数是基于第二个随机森林模型计算的,其目标是学习原始问题中因变量和自变量之间的关系。
-
将步骤 8和步骤 9中的超参数和交叉验证分数对添加到集合D中。
-
重复步骤 7到10,直到满足停止标准。
-
(最后几步与之前看到的一样)。
以下表格列出了与其他 BO 超参数调整方法的变体相比,利用 Metis 的优点和缺点:

图 4.22 – Metis 的优缺点
还值得注意的是,与其他 BO 变体不同,只有一个包实现了 Metis 超参数调整方法,即Microsoft NNI。正如你可能已经注意到的,本章讨论的所有 BO 变体都有无法利用并行计算资源的缺点。那么,为什么我们没有在第一部分就提到这个缺点呢?因为有一种 BO 变体,即 BOHB,可以利用并行计算资源。我们将在第六章中更详细地讨论 BOHB,探索多保真优化。
在本节中,我们详细介绍了 Metis,包括它是什么,它是如何工作的,它与其他 BO 变体有什么不同,以及它的优缺点。
摘要
在本章中,我们讨论了四组超参数调整方法中的第二组,称为 BO 组。我们不仅讨论了 BO 的一般情况,还讨论了其几个变体,包括 BOGP、SMAC、TPE 和 Metis。我们看到了每个变体之间有什么不同,以及每个变体的优缺点。到这一点,当有人问你时,你应该能够自信地解释 BO,并且能够轻松地应用这一组中的超参数调整方法。
在下一章中,我们将开始讨论启发式搜索,这是超参数调整方法的第三组。下一章的目标与本章类似:提供对属于启发式搜索组的各种方法的更好理解。
第五章:第五章: 探索启发式搜索
启发式搜索是四组超参数调整方法中的第三组。这一组与其他组的关键区别在于,属于这一组的所有方法都是通过进行试错来达到最优解。类似于贝叶斯优化中的获取函数(参见第四章**,探索贝叶斯优化),这一组的所有方法也采用了探索与利用的概念。探索意味着在未探索的空间中进行搜索,以降低陷入局部最优的概率,而利用则意味着在已知有很大可能包含最优解的局部空间中进行搜索。
在本章中,我们将讨论属于启发式搜索组的几种方法,包括模拟退火(SA)、遗传算法(GAs)、粒子群优化(PSO)和基于种群的训练(PBT)。类似于第四章,我们将讨论每种方法的定义、它们之间的区别、它们的工作原理以及每种方法的优缺点。
到本章结束时,你将理解上述属于启发式搜索组的超参数调整方法的概念。当有人以高层次和详细的方式询问你时,你将能够自信地解释这些方法,并包括它们的优缺点。一旦你足够自信地向其他人解释它们,这意味着你已经理解了每种方法的来龙去脉。因此,在实践中,如果你遇到错误或没有得到预期的结果,你可以理解发生了什么;你还将知道如何配置该方法,使其与你的特定问题相匹配。
在本章中,我们将涵盖以下主题:
-
理解模拟退火
-
理解遗传算法
-
理解粒子群优化
-
理解基于种群的训练
理解模拟退火
SA是受冶金中金属退火过程启发的启发式搜索方法。这种方法与随机搜索超参数调整方法(参见第三章**,探索穷举搜索)类似,但存在一个指导超参数调整过程如何工作的标准。换句话说,SA 就像是一个平滑版的随机搜索。就像随机搜索一样,建议在每次试验所需时间不多且你有足够的计算资源时使用 SA。
在金属退火过程中,金属被加热到非常高的温度一段时间,然后缓慢冷却以增加其强度,降低其硬度,使其更容易加工。给予非常高的热量的目的是激发金属的原子,使它们可以自由且随机地移动。在随机的移动过程中,原子通常会倾向于形成更好的配置。然后,执行缓慢的冷却过程,以便我们可以得到材料的晶体形式。
就像在金属退火过程中一样,SA 通过随机选择要测试的超参数集来工作。在每次试验中,该方法将考虑当前集的一些“邻居”,随机地。如果满足接受准则,那么方法将改变其焦点到那个“邻居”集。接受准则不是一个确定性函数,它是一个随机函数,这意味着在过程中会涉及到概率。这种决定方式与金属退火过程中的冷却阶段相似,在那里我们接受更少的坏超参数集,因为更多的搜索空间被探索。
SA 是最受欢迎的启发式优化方法之一——随机爬山法(SHC)的改进版本。SHC 非常易于理解和实现,这意味着 SA 也是如此。一般来说,SHC 通过在预定义的边界内初始化随机点(在我们的案例中是超参数空间)并将其视为当前最佳解来工作。然后,它会在所选点的周围随机搜索下一个候选点。然后,我们需要比较所选候选点与当前最佳解。如果候选点比当前最佳解更好或相等,SHC 将候选点视为新的最佳解。这个过程会重复进行,直到满足停止准则。
以下步骤展示了 SHC 优化在一般情况下是如何工作的:
-
定义空间边界B和步长S。
-
定义停止准则。通常,它被定义为迭代次数,但其他停止准则的定义也适用。
-
在边界B内初始化随机点。
-
将从步骤 3中选定的点设置为当前点current_point,以及最佳点best_point。
-
在距离best_point的S距离内,从边界B内随机采样下一个候选点,并将其存储为candidate_point。
-
如果candidate_point比best_point更好或相等,则用candidate_point替换best_point。
-
将current_point替换为candidate_point。
-
重复步骤 5到步骤 7,直到满足停止准则。
SA(模拟退火)和 SHC(基于热力学的冷却)之间的主要区别在于 步骤 5 和 步骤 6。在 SHC 中,我们总是从 最佳点 的周围采样下一个候选点,而在 SA 中,我们从 当前点 的周围采样。在 SHC 中,我们只接受比当前最佳解更好或相等的候选点,而在 SA 中,我们 可能也会以一定的概率接受更差的候选点,这个概率由接受标准 AC 指导,该标准定义如下:

在这里,
和
是目标函数,
是具有正值的 温度。如果您不熟悉目标函数术语,请参阅 第四章。
公式的结果在 0 和 1 之间,当 候选点 比当前点更好或相等时,它总是产生 1 的值。换句话说,当 候选点 比当前点更好或相等时,我们总是接受它。值得注意的是,更好 并不一定意味着具有更大的值。如果您正在处理一个最大化问题,那么更好意味着更大。然而,如果您正在处理一个最小化问题,那么情况则相反。例如,如果您正在测量的交叉验证分数是 均方误差(MSE),其中较低的分数对应于更好的性能,那么如果
的值小于
,则认为 候选点 比当前点更好。
尽管以下公式
受到
和
的影响,但我们只能控制
的值。在实践中,
的 初始值 被视为一个超参数,通常设置为一个较高的值。在多次试验中,
的值会根据所谓的 退火计划 或冷却计划方案而降低。我们可以遵循几种退火计划方案。以下是最受欢迎的三种方案:
- 几何冷却:这种退火计划通过冷却因子
降低温度。在几何冷却中,初始温度
被冷却因子
乘以
次数,其中
是当前的迭代次数:

这可以在以下图表中看到:


图 5.1 – 几何冷却中初始温度对可接受标准的影响
- 线性冷却:这种退火计划通过冷却因子线性降低温度,
。选择
的值,使得
在
次迭代后仍然保持正值。例如,
,其中
是经过
次迭代后的预期最终温度:

下面的图表显示了这种退火计划:

图 5.2 – 线性冷却中初始温度对可接受标准的影响
- 快速 SA:这种退火计划通过按当前迭代次数成比例降低温度来实现,
:

这种退火计划可以在以下图表中看到:

图 5.3 – 快速 SA 中初始温度对可接受标准的影响
根据图 5.1 至图 5.3,我们可以看到,无论我们使用什么退火计划方案以及初始温度是多少,随着迭代次数的增加,我们总是会得到一个更低的
值,这意味着随着迭代次数的增加,我们将接受更少的坏候选者。然而,我们最初为什么要接受坏候选者呢?模拟退火(SA)方法的主要目的不是直接拒绝更差的候选者,正如在 SHC 方法中那样,而是平衡探索和利用的权衡。较高的初始温度值允许 SA 探索超参数空间的大部分区域,随着迭代次数的增加,逐渐聚焦于空间的具体部分,就像金属退火过程一样。
记住,
仅在候选点比当前点更差时才考虑
。这意味着,根据图 5.4,我们可以这样说,建议的候选者越差(
越高),
的值就越低,因此,接受建议的坏候选者的概率就越低。对于
来说,情况正好相反,
的值越高,
的值就越高,因此,接受建议的坏候选者的概率就越高(参见图 5.1 至图 5.3):

图 5.4 – Δf 对可接受标准的影响
总结来说,以下步骤展示了 SA 作为超参数调整方法的工作原理:
-
将原始完整数据分割成训练集和测试集(见 第一章**,评估机器学习模型)。
-
使用伴随的分布定义超参数空间,H。
-
定义初始温度,T0。
-
根据训练集定义目标函数,f(见 第四章)。
-
定义停止标准。通常使用试验次数。然而,也可以使用时间或收敛性作为停止标准。
-
使用 T0 的值设置当前温度,T。
-
初始化一组从超参数空间,H,中采样的随机超参数。
-
将从 步骤 7 中选择的集合设置为当前集合,current_set,以及最佳集合,best_set。
-
从超参数空间,H,的“邻居”中随机采样下一个候选集合,candidate_set。不同类型的超参数分布中“邻居”的定义可能不同。
-
从均匀分布中生成一个介于 0 和 1 之间的随机数,并将其存储为 rnd。
-
决定是否接受 candidate_set:
-
使用 T、f(candidate_set) 和 f(current_set) 的值计算
的值。 -
如果 rnd 的值小于
,则用 candidate_set 替换 current_set。 -
如果 candidate_set 比 current_set 更好或相等,则用 candidate_set 替换 best_set。
-
-
将退火计划应用于温度,T。
-
重复 步骤 9 到 12,直到满足停止标准。
-
使用 best_set 超参数在完整训练集上训练。
-
在测试集上评估最终训练的模型。
以下表格列出了 SA 作为超参数调整方法的优缺点列表:

图 5.5 – SA 的优缺点
在本节中,我们从了解 SA 是什么、它是如何工作的、它区别于 SHC 和随机搜索的特点,以及它的优缺点开始,学习了 SA。在下一节中,我们将讨论另一个有趣的启发式搜索方法,该方法受到自然选择理论的影响。
理解遗传算法
遗传算法(GAs)是受查尔斯·达尔文自然选择理论启发的流行启发式搜索方法。与被归类为单点基于启发式搜索方法的 SA 不同,GAs 被归类为基于群体的方法,因为它们在每个试验中维护一组可能的候选解,而不是只维护一个候选解。作为一个超参数调整方法,当每个试验不需要太多时间并且你有足够的计算资源,如并行计算资源时,建议使用 GA。
为了更好地理解遗传算法(GAs),让我们从一个简单的例子开始。假设我们有一个任务,基于仅由 26 个小写字母组成的单词集合生成一个预定义的目标单词。例如,目标单词是“big”,我们有一个由单词“sea”、“pig”、“dog”、“bus”和“tie”组成的集合。
基于给定的单词集合,我们应该如何生成“big”这个单词?这无疑是一个非常简单且直接的任务。我们只需从“bus”这个单词中挑选字母“b”,从“pig”或“tie”这个单词中挑选字母“i”,从“dog”这个单词中挑选字母“g”。就这样!我们得到了“big”这个单词。你可能想知道这个例子如何与 GA 方法或自然选择理论相关。这个例子是一个非常简单的任务,没有必要利用 GA 来解决问题。然而,我们需要这样的例子,以便你更好地理解 GAs 是如何工作的,因为你一开始就知道正确的答案。
要使用遗传算法(GA)解决这个问题,你必须了解与进化理论相关的 GA 中的三个关键项目。第一个关键项目是变异。想象一下,如果给定的单词集合只包含“sea”这个单词。我们无法仅基于“sea”这个单词生成“big”这个单词。这就是为什么在初始群体(在我们的例子中是单词集合)中需要变异。如果没有足够的变异,我们可能无法达到最优解(在我们的例子中是生成“big”这个单词),因为群体中没有个体(在我们的例子中是单词集合中的每个单词)能够进化到目标单词。
重要提示
人口不是超参数空间。在遗传算法(GAs)或其他基于群体的启发式搜索方法中,群体指的是最优超参数集的候选者。
第二个关键项目是选择。你可以将这个项目视为类似于现实世界中发生的自然选择的概念。这是关于选择更适合周围环境的个体(在我们的例子中是类似于“big”这个单词的单词)并且因此能够在世界上生存下来。在 GAs 中,我们需要定量指导来执行选择,这通常被称为适应度函数。这个函数帮助我们判断一个个体相对于我们想要实现的目标有多好。在我们的例子中,我们可以创建一个适应度函数,该函数衡量单词中与目标单词在相应索引中具有相同字母的索引的比例。例如,“tie”这个单词的适应度分数为
,因为只有一个索引包含与目标单词相同的字母,即索引一,它包含字母“i”。
使用这个适应度函数,我们可以评估群体中每个个体的适应度分数,然后选择哪些个体应该被添加到配对池中作为父母。配对池是一组被认为是高质量个体的集合,因此被称为父母。
第三个关键要素是遗传。这个要素指的是繁殖或传递父母的基因(在我们的例子中是单词中的每个字母)给他们的孩子或后代。在 GA 中如何进行繁殖?遵循自然选择的相同精神,在 GA 中,我们只从配对池中的父母进行繁殖步骤,这意味着我们只想让高质量个体交配,希望在下一次世代(在下一个迭代中创建一个新的群体)中得到只有高质量的后代。繁殖阶段有两个步骤,即交叉和变异步骤。交叉步骤是我们随机混合或排列父母的基因以生成后代的基因,而变异步骤是我们随机改变后代的基因值以增加基因的变异(参见图 5.6)。被变异的个体被称为突变体。变异步骤中使用的随机值应该来自相同的基因分布,这意味着在我们的例子中,我们只能使用小写字母作为随机值,不能使用浮点数或整数:
![Figure 5.6 – 遗传算法中的交叉和变异步骤]
![img/B18753_05_006.jpg]
图 5.6 – 遗传算法中的交叉和变异步骤
现在你已经了解了遗传算法(GA)中的三个关键要素,我们可以从上一个示例开始使用 GA 来解决问题。假设我们没有给出单词集合,这样我们可以学习 GA 的完整过程。目标单词仍然是“big”。
首先,我们必须使用NPOP数量初始化一个个体群体。初始化过程通常是随机进行的,以确保我们在群体中拥有足够的变异。这里的随机意味着群体中每个个体的基因都是随机生成的。假设我们想要生成一个初始群体,该群体由七个个体组成,生成的结果为“bee”、“tea”、“pie”、“bit”、“dog”、“cat”和“dig”。
现在,我们可以评估群体中每个个体的适应度分数。假设我们使用之前定义的适应度函数。因此,我们得到了每个个体的以下分数;“bee:”
,“tea:”
,“pie:”
,“bit:”
,”dog:”
,“cat:”
,和“dig:”
。
根据每个个体的适应度分数,我们可以选择哪些个体应该作为父母添加到交配池中。我们可以采用许多策略来从种群中选择最佳个体,但在这个例子中,我们只是根据适应度分数选择前三个个体,并随机选择具有相同适应度分数的个体。比如说,在执行选择策略后,我们得到了一个由“bit”、“dig”和“bee”作为父母的交配池。
下一步是执行交叉和变异步骤。然而,在此之前,我们需要指定交叉概率CXPB和变异概率MUTPB,这定义了在交配池中交叉两个父母和变异一个后代的概率。这意味着我们既不对所有父母对执行交叉,也不对所有后代执行变异——我们只会根据预定义的概率执行这些步骤。比如说,只有“dig”和“bee”选择了交叉,交叉的结果是“deg”和“bie”。所以,当前的交配池由“bit”、“deg”和“bie”组成。现在,我们需要对“deg”和“bie”执行变异。比如说,变异后,我们得到了“den”和“tie”。这意味着当前的交配池由“bit”、“den”和“tie”组成。
在执行交叉和变异步骤之后,我们需要为下一代生成一个新的种群。这个新种群将包括所有交叉的父母、变异的后代以及来自当前种群的其他个体。因此,下一个种群将包括“bit”、“den”、“tie”、“tea”、“pie”、“dog”和“cat”。
根据新种群,我们必须重复选择、交叉和变异过程。这个程序需要执行NGEN次,其中 NGEN 代表代数,由开发者预先定义。
以下步骤定义了通用遗传算法(GA)作为优化方法的工作方式:
-
定义种群大小NPOP、交叉概率CXPB、变异概率MUTPB以及代数或试验次数NGEN。
-
定义适应度函数f。
-
使用NPOP个个体初始化一个种群,其中每个个体的基因都是随机初始化的。
-
根据适应度函数f评估种群中的所有个体。
-
根据步骤 4选择最佳个体并将它们存储在交配池中。
-
以CXPB的概率对交配池中的父母执行交叉过程。
-
以MUTPB的概率对步骤 8产生的后代执行变异过程。
-
生成一个新的种群,该种群由来自步骤 6、步骤 7以及当前种群中剩余的所有个体组成。
-
用新种群替换当前种群。
-
重复步骤 6到步骤 9共NGEN次。
现在,让我们看看一个更具体的例子,说明 GA 通常是如何工作的。我们将使用与 第四章**,评估机器学习模型 中相同的目标函数,并将其视为一个最小化问题。目标函数定义如下:

这里,
是遵循标准正态分布的噪声。我们只将在
范围内进行搜索。值得注意的是,在这个例子中,我们假设我们知道真正的目标函数是什么。然而,在实践中,这个函数是未知的。在这种情况下,每个个体将只有一个基因,即
的值本身。
假设我们定义 GA 方法的超参数为 NPOP = 25, CXPB = 0.5, MUTPB = 0.15, 和 NGEN = 6。至于每个 遗传算子 的策略,我们分别使用 锦标赛、混合 和 多项式边界 策略来进行选择、交叉和变异操作。锦标赛 选择策略通过在 tournsize 个个体中选出最佳个体,以及随机选择的个体的 NPOP 倍,其中 tournsize 是参加锦标赛的个体数量。混合 交叉策略通过执行两个连续个体基因的线性组合来实现,其中线性组合的权重由 alpha 超参数控制。多项式边界 变异策略通过将连续个体基因传递到一个预定义的多项式映射中来实现。
根据您的超参数空间定义,有许多可用的策略可供选择。我们将在 第十章**,使用 DEAP 和 Microsoft NNI 进行高级超参数调整 中更多地讨论不同的策略以及如何使用 DEAP 包实现 GA 方法。现在,让我们看看对虚拟目标函数 f 应用 GA 的结果。请注意,每个图中的点对应于种群中的每个个体:

图 5.7 – GA 流程
根据前一个图表,我们可以看到在第一代,由于是随机初始化,个体分布在整个地方。在第二代,围绕点 -1.0 初始化的几个个体移动到了具有较低适应度分数的其他地方。然而,在第三代,围绕点 -1.0 又出现了新的个体。这可能是由于应用了随机变异算子。还有一些个体陷入了局部最优,大约在点 -0.5 附近。在第四代,大多数个体已经移动到了具有较低适应度分数的地方,尽管其中一些个体仍然陷入了局部最优。在第五代,个体开始在几个地方开始收敛。
最后,在第六代,所有个体都收敛到了接近全局最优解,大约在点 1.5 附近。请注意,我们仍然在第六代有 NPOP=25 个个体,但它们都位于同一个地方,这就是为什么在图表中只能看到一个点。这也适用于其他世代,如果你在图表中看到少于 25 个个体。各个世代的收敛趋势可以在以下图表中看到:
![图 5.8 – 收敛图
![img/B18753_05_008.jpg]
图 5.8 – 收敛图
前一个图表中显示的趋势与我们的先前分析相符。然而,我们可以从这个图中获得更多信息。起初,许多个体位于具有高适应度分数的地方,但一些个体已经获得了最佳的适应度分数。在各个世代中,大多数个体开始收敛,最终,在最后一代,所有个体都获得了最佳的适应度分数。值得注意的是,在实践中,并不能保证 GA 会达到全局最优解。
到目前为止,你可能想知道,如何将 GA 作为超参数调整方法采用?在超参数调整的上下文中,GA 中所有术语的对应定义是什么?当使用 GA 进行超参数调整时,“个体”意味着什么?
作为超参数调整方法,GA 方法将一组超参数视为一个个体,其中超参数的值是基因。为了更好地理解 GA 方法中每个重要术语的含义,在超参数调整的上下文中,请参考以下表格:
![图 5.9 – 超参数调整上下文中 GA 方法术语的定义
![img/B18753_05_009.jpg]
图 5.9 – 超参数调整上下文中 GA 方法术语的定义
既然你已经了解了 GA 方法中每个重要术语的相应定义,我们可以定义正式的程序来利用 GA 方法作为超参数调整方法:
-
将原始完整数据集分为训练集和测试集。
-
定义超参数空间,H,以及伴随的分布。
-
定义种群大小,NPOP。
-
定义交叉概率,CXPB,和变异概率,MUTPB。
-
将试验次数,NGEN,定义为停止标准。
-
根据训练集定义目标函数,f。
-
初始化一个包含 NPOP 组超参数的种群,每组超参数都是从超参数空间,H,中随机抽取的。
-
根据目标函数,f,评估种群中的所有超参数集。
-
根据 Step 8 选择几个最佳候选集。
-
以 CXPB 的概率对 Step 9 中的候选集进行交叉。
-
以 MUTPB 的概率对 Step 10 中的交叉候选集进行变异。
-
生成一个新的种群,该种群由 Step 10、Step 11 和当前种群中的其余超参数集的所有组合组成。新种群也将包含 NPOP 组超参数。
-
重复 Steps 8 到 12 NGEN 次。
-
使用最终的超参数值在完整的训练集上进行训练。
-
在测试集上评估最终训练好的模型。
值得注意的是,当利用 GA 作为超参数调整方法时,GA 本身有四个超参数,即 NPOP、CXPB、MUTPB 和 NGEN,它们控制超参数调整结果的表现,以及探索与利用之间的权衡。更精确地说,CXPB 和 MUTPB,即交叉和变异概率,分别负责控制 探索 速率,而选择步骤及其策略控制 利用 速率。
下表列出了使用 GA 作为超参数调整方法的优缺点:
![Figure 5.10 – Pros and Cons of the GA method
![img/B18753_05_010.jpg]
图 5.10 – GA 方法的优缺点
评估每一代中所有个体的需求意味着我们将我们目标函数的原始时间复杂度乘以 NPOP * NGEN。这非常昂贵!这就是为什么如果你有一个昂贵的目标函数和/或有限的计算资源,GA 方法可能不适合你。然而,如果你有等待实验完成的时间,并且你有大量的并行计算资源,那么 GA 方法适合你。从理论角度来看,GA 方法也可以与各种类型的超参数一起工作——我们只需要为相应的超参数选择合适的交叉和变异策略。与 SA 相比,GA 方法在拥有一个种群来指导需要更多探索的子空间部分方面更有优势。然而,值得注意的是,GA 方法仍然可能陷入局部最优。
在本节中,我们讨论了 GA 方法,从它是什么,以及它在一般设置和超参数调整环境中的工作方式,以及它的优缺点。在下一节中,我们将讨论另一个有趣的基于种群的启发式搜索方法。
理解粒子群优化
PSO也是一种基于种群的启发式搜索方法,类似于遗传算法(GA)方法。PSO 受到自然界中鱼群和鸟群社会互动的启发。作为一种超参数调整方法,如果您的搜索空间包含许多非分类超参数,每次试验所需时间不多,并且您有足够的计算资源——特别是并行计算资源,建议使用 PSO。
PSO 是更大群体智能(SI)方法组中最受欢迎的方法之一。群体智能中有各种方法,这些方法受到自然界中动物社会互动的启发,例如陆地动物的群、蚂蚁的群体、鸟群、鱼群等等。群体智能方法的共同特征是基于种群的,种群内的个体相对相似,种群能够系统性地在种群内部或外部没有单个协调者的情况下向特定方向移动。换句话说,种群可以根据个体之间以及与周围环境的局部交互来组织自己。
当一群鸟在寻找食物时,人们认为每只鸟可以通过分享它们所看到的关于信息来为群体做出贡献,从而使群体能够朝正确的方向移动。粒子群优化(PSO)是一种模拟鸟群运动以优化目标函数的方法。在 PSO 中,鸟群被称为群,每只鸟被称为粒子。
每个粒子由其位置向量和速度向量定义。每个粒子的运动由随机和确定性成分组成。换句话说,每个粒子的运动不仅基于预定义的规则,还受到随机成分的影响。每个粒子还记住自己的最佳位置,这给出了它在轨迹上经过的最佳目标函数值。然后,结合全局最佳位置,它被用来在特定时间更新每个粒子的速度和位置。全局最佳位置只是上一步中最佳粒子的位置。
假设
是群中 m 个粒子中第
个粒子在 d 维空间中的位置向量,而
是相同大小的
粒子的速度向量,如图所示:


让我们也分别定义每个粒子的最佳位置和全局最佳位置向量:


以下公式定义了每个粒子在每次迭代中位置和速度向量的更新方式:


在这里,
、
和
是控制探索与利用权衡的超参数。
的值通常在零到一之间,被称为惯性权重系数,而
和
分别被称为认知和社会系数。
和
是介于零和一之间的随机值,充当粒子运动的随机成分。请注意,位置和速度向量的 d 维数指的是我们在搜索空间中的超参数数量,而 m 个粒子指的是从超参数空间中采样的候选超参数的数量。
第一次更新速度向量可能看起来有些令人畏惧,但实际上,通过将公式视为三个独立的部分,你可以更容易地理解它。第一部分,或者公式的最左侧,旨在按比例更新下一个速度,与当前速度成正比。第二部分,或者公式的中间部分,旨在将速度更新到
粒子所具有的最佳位置的方向,同时向其中添加一个随机成分。第三部分,或者公式的最右侧,旨在将
粒子带到全局最佳位置附近,并对其应用额外的随机行为。以下图示有助于说明这一点:

图 5.11 – 更新粒子的位置和速度
前面的图示与所给出的公式并不相同,因为图中缺少了随机成分和超参数。然而,这张图可以帮助我们理解每个粒子在每个迭代中位置和速度向量的更新高级概念。我们可以看到,最终更新的速度(见橙色线)是基于三个向量计算的,即当前速度(见棕色线)、粒子最佳位置(见绿色线)和全局最佳位置(见紫色线)。基于最终更新的速度,我们可以得到粒子的更新位置 – 那就是图中的
。
现在,让我们讨论超参数如何影响公式。惯性权重系数,
,控制我们在更新速度向量时想要将多少注意力放在当前速度上。另一方面,认知系数,
,和社会系数,
,分别控制我们应该在多大程度上关注粒子的过去轨迹历史和群搜索结果。当我们设置
时,我们不考虑最佳位置
粒子的影响,这可能导致我们陷入局部最优解。当我们设置
时,我们忽略了全局最佳位置的影响,这可能导致收敛速度较慢。
现在你已经了解了群中每个粒子的位置和速度分量,请查看以下步骤,这些步骤定义了粒子群优化(PSO)作为优化方法的一般工作方式:
-
定义群大小N,惯性权重系数w,认知系数c1,社会系数c2和最大尝试次数。
-
定义适应度函数f。
-
初始化一个包含N个粒子的群,其中每个粒子的位置和速度向量都是随机初始化的。
-
将每个粒子的当前位置向量设置为它们的最佳位置向量,pbi。
-
通过从所有N个粒子中选择具有最佳适应度分数的位置向量来设置当前全局最佳位置,gb。
-
根据更新公式更新每个粒子的位置和速度向量。
-
根据适应度函数f评估群中的所有粒子。
-
更新每个粒子的最佳位置向量,pbi:
-
将每个粒子的当前适应度分数与步骤 7中的pbi适应度分数进行比较。
-
如果当前适应度分数优于pbi适应度分数,则使用当前位置向量更新pbi。
-
-
更新全局最佳位置向量,gb:
-
将每个粒子的当前适应度分数与步骤 7中的先前gb适应度分数进行比较。
-
如果当前适应度分数优于gb适应度分数,则使用当前位置向量更新gb。
-
-
根据更新公式更新每个粒子的位置和速度向量。
-
重复步骤 7到10,直到达到最大尝试次数。
-
返回最终的全球最佳位置,gb。
值得注意的是,最优适应度分数(或先前所述程序中的更好适应度分数)的定义将取决于你试图解决的优化问题类型。如果是最小化问题,则较小的适应度分数较好。如果是最大化问题,则情况相反。
为了更好地理解 PSO 的工作原理,让我们通过一个例子来分析。让我们定义适应度函数如下:

在这里,
和
仅在
范围内定义。下面的 等高线图 展示了我们的目标函数看起来是什么样子。我们将在 *第十章**,使用 DEAP 和 Microsoft NNI 进行高级超参数调整中学习如何使用 DEAP 包实现 PSO:
![图 5.12 – 显示目标函数及其全局最小值的等高线图
![图片 B18753_05_012.jpg]
图 5.12 – 显示目标函数及其全局最小值的等高线图
在这里,您可以看到全局最小值 (见红色十字标记) 位于 (0.497, 0.295),目标函数值为 –0.649。让我们尝试使用 PSO 来看看它如何估计目标函数的最小值与真实全局最小值相比。假设我们定义 PSO 的超参数为 N=20,w=0.5,c1=0.3,和 c2=0.5,并将最大尝试次数设置为 16。
您可以在以下等高线图中看到初始群体示意图。蓝色点代表每个粒子,每个粒子上的蓝色箭头代表粒子的速度向量,黑色点代表每个粒子的最佳位置向量,红色星形标记代表特定迭代时的当前全局最佳位置向量:
![图 5.13 – 一个 PSO 初始群体
![图片 B18753_05_013.jpg]
图 5.13 – 一个 PSO 初始群体
由于群体中的初始粒子是随机初始化的,因此速度向量的方向遍布各处(参见 图 5.13)。您可以看到每个粒子的位置和速度向量在每个迭代中是如何更新的,以及全局最佳位置向量,如下所示:
![图 5.14 – PSO 过程
![图片 B18753_05_014.jpg]
图 5.14 – PSO 过程
即使在第一次迭代中,每个粒子的速度向量都指向全局最小值,该最小值位于图的左下角。在每个迭代中,位置和速度向量都会更新并接近全局最小值。在迭代循环结束时,大多数粒子都位于全局最小值位置附近,最终全局最佳位置向量位于 (0.496, 0.290),适应度分数约为 –0.648。这个估计非常接近目标函数的真实全局最小值!
值得注意的是,每个粒子的速度向量包含两个分量:大小和方向。大小将影响 图 5.14 中速度向量的长度。虽然您可能看不到每个粒子速度向量长度之间的差异,但它们彼此是不同的!
重要提示
作为超参数调整方法,在 PSO 方法中,粒子和群体分别指从超参数空间和超参数集候选集合中采样的候选超参数集。每个粒子的位置向量指代粒子中每个超参数的值。最后,速度向量指用于更新粒子中每个超参数值的超参数值变化量。
以下步骤定义了PSO 作为超参数调整方法的工作原理:
-
将原始完整数据集分为训练集和测试集。
-
使用伴随分布定义超参数空间,H。
-
定义集合大小N、惯性权重系数w、认知系数c1、社会系数c2和最大尝试次数。
-
根据训练集定义目标函数f。
-
初始化一个包含N个超参数集的集合,其中每个集是从超参数空间H中随机抽取的。
-
随机初始化集合中每个超参数集的速度向量。
-
将每个集的当前超参数值设置为它们的最佳值pbi。
-
设置当前全局最佳超参数集,gb,通过从所有 N 个超参数集中选择一个具有最佳目标函数分数的集。
-
根据更新公式更新每个集的超参数值和速度向量。
-
根据目标函数f评估集合中所有超参数集。
-
更新每个集的最佳超参数值pbi:
-
将第 10 步中每个集的当前分数与它的pbi分数进行比较。
-
如果当前分数优于pbi分数,则使用当前超参数值更新pbi。
-
-
更新全局最佳超参数集gb:
-
将第 10 步中每个集的当前分数与之前的gb分数进行比较。
-
如果当前分数优于gb分数,则使用当前超参数集更新gb。
-
-
根据更新公式更新每个集的超参数值和速度向量。
-
重复步骤 10到13,直到达到最大尝试次数。
-
使用全局最佳超参数集在完整训练集上进行训练。
-
在测试集上评估最终训练好的模型。
在 PSO 方法中的更新公式有一个问题,那就是它只适用于数值变量,尤其是连续变量,这意味着如果我们的超参数空间包含离散超参数,我们就不能直接利用原始 PSO 作为超参数调整方法。受此问题的启发,有几个 PSO 的变体被设计出来,以便能够在离散空间中也能工作。第一个变体是为了专门针对二进制变量设计的,被称为二进制 PSO。在这个变体中,速度向量的更新公式是相同的,这意味着我们仍然将速度向量视为连续空间中的,但位置向量的更新公式被修改了,如下所示:

这里,
是从
,
区间内均匀分布中抽取的一个随机数,j下标指的是第i个粒子的每个分量。正如你所见,在二进制 PSO 变体中,我们可以在离散空间中工作,但我们被限制只能使用二进制变量。
当我们有一个离散和连续数值超参数的组合时怎么办?例如,我们的神经网络模型超参数空间包含学习率、dropout 率和层数。由于层数超参数期望的是整数输入,而不是连续或浮点输入,所以我们不能直接利用原始 PSO 方法。我们也不能利用二进制 PSO 变体,因为学习率和 dropout 率是连续的,而层数超参数也不是二进制的。
我们可以做的简单事情之一是四舍五入更新后的速度向量分量值,但只针对与离散位置分量相对应的分量,在将其传递给位置向量更新公式之前。这样,我们可以确保我们的离散超参数仍然始终在离散空间内。然而,这个解决方案仍然存在问题。四舍五入操作可能会使速度向量的更新过程次优。为什么?因为速度向量的更新值无论是什么,只要它们仍然在一个整数点的相似范围内,位置向量就不再更新。这将导致大量的冗余计算成本。
有另一种方法可以使 PSO 在连续和离散空间中都能良好地运行。除了四舍五入更新的速度向量分量值之外,我们还可以动态地更新惯性权重系数。动机是帮助粒子关注其过去的速度值,这样它就不会陷入局部或全局最优,这受到
或
的影响。动态惯性权重更新过程可以根据多个因素进行,例如其当前位置向量和最佳位置向量之间的相对距离,当前试验次数与最大试验次数之间的差异,等等。
在试验过程中,我们可以有多种方式动态更新惯性权重系数的变体;我们将把它留给你来选择对你特定情况效果最好的方式。
尽管我们可以修改 PSO 中的更新公式,使其不仅适用于连续变量,也适用于离散变量,但我们仍然面临几个问题,如前所述。因此,为了在连续空间中充分利用 PSO 的最大功效,还有一种 PSO 的变体,试图将 PSO 与贝叶斯优化方法相结合,称为PSO-BO。PSO-BO 的目标是将 PSO 作为贝叶斯优化获取函数优化器的替代品(参见第四章)。因此,我们不需要使用二阶优化方法来优化获取函数,而可以使用 PSO 作为优化器来帮助决定在贝叶斯优化超参数调整过程的下一个试验中要测试的超参数集。
以下表格总结了利用 PSO 作为超参数调整方法的优缺点:

图 5.15 – PSO 的优缺点
现在你已经了解了 PSO 是什么,它是如何工作的,它的几个变体以及它的优缺点,让我们来讨论另一种有趣的基于群体的启发式搜索方法。
理解基于群体的训练
PBT是一种基于群体的启发式搜索方法,就像 GA 方法和 PSO 一样。然而,PBT 不是一个像 GA 或 PSO 那样的自然启发算法。相反,它受到 GA 方法本身的启发。当你在使用基于神经网络类型的模型,并且只需要最终的训练模型而不需要知道具体选择的超参数配置时,建议使用 PBT。
PBT 是专门设计来仅与基于神经网络的模型一起工作的,例如多层感知器、深度强化学习、转换器、生成对抗网络以及任何其他基于神经网络的模型。可以说,PBT 既能进行超参数调整,也能进行模型训练,因为在过程中神经网络模型的权重会被继承。因此,PBT 不仅是为了选择最优化超参数配置,也是为了将模型的权重或参数转移到种群中的其他个体。这就是为什么 PBT 的输出不是一个超参数配置,而是一个模型。
PBT 是随机搜索和顺序搜索方法的混合方法,例如手动搜索和贝叶斯搜索(详见第三章**,探索穷举搜索和第四章**,探索贝叶斯优化以获取更多详细信息)。随机搜索是寻找对敏感超参数良好子空间的一个非常好的方法。如果我们在执行优化过程时有足够的计算资源和时间,顺序搜索方法往往比随机搜索给出更好的性能。然而,这些方法需要顺序执行的事实使得实验运行时间非常长。PBT 提供了一种解决方案,将两种方法的优点结合成一个单一的训练优化过程,这意味着模型训练和超参数调整过程被合并成一个单一的过程。
PBT 中的术语基于种群来源于它受到 GA 方法启发,即利用整个种群的知识来产生性能更好的个体。请注意,PBT 中的个体部分指的是种群中具有不同参数和超参数的每一个N 个模型,或者所有这些 N 个模型的集合。
PBT 的搜索过程首先初始化一个包含 N 个模型的种群 P,这些模型具有各自随机采样的参数,
,以及随机采样的超参数,
,
。在搜索过程的每一轮迭代中,都会为 N 个模型中的每一个触发训练步骤。训练步骤包括前向和反向传播过程,这些过程使用基于梯度的优化方法,就像基于神经网络的模型通常的训练过程一样。一旦完成训练步骤,下一步就是执行评估步骤。评估步骤的目的是评估当前模型在未见过的验证数据上的Mi性能。
一旦模型,Mi,被认为准备就绪,PBT 将触发利用和探索步骤。模型准备就绪的定义可能有所不同,但我们可以将“准备就绪”定义为通过预定义的步骤数或通过预定义的性能阈值。利用和探索步骤的目标相同,即更新模型的参数和超参数。它们之间的区别在于它们如何进行更新过程。
根据整个种群的评估结果,利用步骤将决定是否继续使用当前的一组参数和超参数,或者关注更有希望的一组。例如,利用步骤可以通过用从种群顶部 X%中随机采样的模型替换整个种群中被认为是底部 X%模型的模型来完成。请注意,一个模型由所有参数和超参数组成。另一方面,探索步骤通过提出一组新参数来更新模型的超参数集,而不是参数。您可以通过以预定义的概率随机扰动当前的超参数集或从种群顶部 X%中重新采样超参数集来提出一组新参数。请注意,此探索步骤仅在利用步骤中选择的模型上执行。
重要提示
PBT 中的探索步骤受到随机搜索的启发。此步骤可以通过从利用步骤中选择的部分训练模型来识别需要更多探索的超参数子空间。在搜索过程中进行的评估步骤也使我们能够消除顺序优化过程的缺点。
PBT 方法中的利用和探索过程使我们能够以在线方式更新模型的一组超参数,同时更加关注有希望的超参数和权重空间。对于种群中的每个N个个体,执行 train-eval-exploit-explore 的迭代过程是异步并行的,直到满足停止标准。
以下步骤总结了PBT 作为单个训练优化过程的工作原理:
-
将原始完整数据分割成训练、验证和测试集(见第一章**,评估机器学习模型)。
-
定义超参数空间,H,以及伴随的分布。
-
定义种群大小,N,探索扰动因子,perturb_fact,探索重采样概率,resample_prob,以及利用分数,frac。
-
定义模型的准备就绪标准。通常,使用 SGD 优化步骤的数量。然而,也可以使用模型性能阈值作为标准。
-
定义用于存储模型权重和超参数的检查点目录。
-
定义评估函数,f。
-
初始化一个包含N个模型的人口,P,每个模型都有自己随机采样的参数,
,以及从超参数空间H中随机采样的超参数,
,和
。 -
对于人口P中的每个模型,并行运行以下步骤:
-
使用
参数和一组超参数
对模型M*i执行训练过程的单步。 -
如果满足准备标准,请执行以下操作。如果不满足,请返回步骤 I:
-
根据验证集上的f值执行评估步骤。
-
根据预定义的利用分数frac,对模型M*i执行利用步骤。这一步骤将产生一组新的参数和超参数。
-
根据预定义的扰动因子和重采样概率,在利用步骤的超参数集上执行探索步骤。
-
根据验证集上的f值,对新的参数集和超参数集执行评估步骤。
-
更新模型M*i的新参数集和超参数集。
-
-
重复步骤 I 和 II,直到训练循环结束。通常,它由 epoch 的数量定义。
-
-
返回人口P中评估分数最高的模型。
-
在测试集上评估最终模型。
值得注意的是,在实践中,例如在NNI包的实现中(见*第十章**,使用 DEAP 和 Microsoft NNI 进行高级超参数调整),第 4 步中定义的准备标准是一个 epoch。换句话说,第 8 步中的第二步将在每个训练 epoch 结束后运行,而不是在 epoch 的中间。还值得注意的是,第 5 步中定义的检查点目录是必需的,因为在 PBT 中,我们需要从人口中的另一个模型复制权重,而其他我们之前学过的超参数调整方法并不需要这样做。
尽管原始的 PBT 算法声称我们可以异步并行地运行步骤 8,但在本书中将使用的NNI包的实现中并非如此。在 NNI 包的实现中,过程是同步运行的,这意味着一旦人口中的所有个体或模型完成了上一个 epoch,我们就可以继续到下一个 epoch。
以下表格列出了 PBT 方法的优缺点:

图 5.16 – PBT 的优缺点
在本节中,你学习了关于 PBT 所需了解的所有内容,包括它是什么,它是如何工作的,它与其他启发式搜索方法的不同之处,以及它的优缺点。
摘要
在本章中,我们讨论了四种超参数调整方法中的第三组,称为启发式搜索组。我们一般讨论了启发式搜索方法是什么,以及包括模拟退火(SA)、遗传算法(GA)方法、粒子群优化(PSO)和参数贝叶斯树(PBT)在内的几种启发式搜索方法的变体。我们看到了这些变体之间的不同之处,以及每种方法的优缺点。此时,当有人询问你时,你应该能够自信地解释启发式搜索。你也应该能够调试并设置所选方法最适合你特定问题定义的最优配置。
在下一章中,我们将开始讨论多保真优化,这是超参数调整方法的最后一组。下一章的目标与这一章类似:为了更好地理解属于多保真优化组的那些方法,以便当有人询问你时,你能自信地解释这些方法。通过这样做,你将能够为你的特定问题配置每种方法!
第六章:第六章: 探索多保真优化
多保真优化(MFO)是四组超参数调整方法中的第四组。这一组的主要特点是,这一组中的所有方法都利用了整个超参数调整管道的廉价近似,因此我们可以以更低的计算成本和更快的实验时间获得相似的性能结果。当您有一个非常大的模型或一个非常大的样本数量时,例如,当您正在开发基于神经网络的模型时,这一组是合适的。
在本章中,我们将讨论 MFO 组中的几种方法,包括从粗到细的搜索、连续减半、超带宽和贝叶斯优化与超带宽(BOHB)。与第五章**探索启发式搜索一样,我们将讨论每种方法的定义、它们之间的差异、它们的工作原理以及每种方法的优缺点。
到本章结束时,您将能够自信地解释 MFO 及其变体,以及它们在高级和技术上是如何工作的。您还将能够区分它们之间的差异,以及每种方法的优缺点。您还将体验到理解每种方法在实际操作中的关键好处:能够配置方法以匹配您自己的问题,并在方法出现错误或意外输出时知道该怎么做。
在本章中,我们将涵盖以下主要主题:
-
介绍 MFO
-
理解从粗到细的搜索
-
理解连续减半
-
理解超带宽
-
理解 BOHB
介绍 MFO
MFO 是一组超参数调整方法,通过创建整个超参数调整管道的廉价近似,我们可以以更低的计算成本和更快的实验时间获得相似的性能结果。有许多方法可以创建廉价近似。例如,我们可以在前几个步骤中仅对完整数据的子集进行操作,而不是直接对完整数据进行操作,或者我们也可以在用完整周期训练模型之前尝试使用较少的周期来训练基于神经网络的模型。换句话说,MFO 方法通过结合廉价低保真和昂贵高保真评估来工作,其中通常廉价评估的比例远大于昂贵评估,这样我们就可以实现更低的计算成本,从而更快地完成实验。然而,MFO 方法也可以归类为信息搜索类别的一部分,因为它们利用先前迭代的知识来获得(希望)更好的搜索空间。
我们在前几章中学到的所有方法都可以归类为黑盒优化方法。所有黑盒优化方法都试图在不利用 ML 模型内部发生的情况或模型使用的数据的任何信息的情况下进行超参数调整。黑盒优化器将只关注从定义的超参数空间中搜索最佳的超参数集,并将其他因素视为黑盒(见图 6.1)。这种特性有其自身的优点和缺点。它使我们能够利用黑盒优化器,这对于各种类型的模型或数据来说更加灵活,但它也让我们付出了代价,因为我们没有考虑可能加快进程的其他因素。

图 6.1 – 黑盒优化器的示意图
黑盒优化方法的成本意味着当我们处理需要非常长的训练迭代时间才能完成的一个非常大的模型或大数据时,我们无法利用它们。这就是超参数调整方法的 MFO 组出现的地方!通过考虑黑盒优化器视为黑盒的其他因素,我们可以在牺牲一点黑盒优化器所具有的通用性的情况下,拥有一个更快的进程。
通用性
通用性意味着模型能够在许多未见过的案例上执行。
此外,本组分类的大多数方法都可以很好地利用并行计算资源,这可以进一步加快超参数调整过程的速度。然而,MFO 方法提供的更快过程的好处是有代价的。我们可能会有更差的调整结果,因为我们有可能在低保真度评估步骤中排除了更好的子空间。然而,速度提升可以说是更重要的,特别是当我们处理一个非常大的模型和/或大数据时。
重要提示
超参数调整方法的 MFO 组与包括穷举搜索、贝叶斯优化和启发式搜索在内的黑盒优化方法相比,并不是一个完全不同的组。事实上,我们也可以将多保真优化方法中使用的类似程序应用到黑盒优化器中。换句话说,我们可以结合黑盒和多保真模型,从而获得两者的最佳效果。
例如,我们可以使用贝叶斯优化(BO)方法之一(见第四章,探索贝叶斯优化)并在其上应用连续减半法(见理解连续减半部分)。这样,我们将确保我们只在重要的子空间上执行 BO,而不是让 BO 自行探索整个超参数空间。通过这样做,我们可以拥有更快的实验时间,同时降低计算成本。
现在您已经了解了 MFO 是什么,它与黑盒优化方法有何不同,以及它在高层次上是如何工作的,我们将在以下几节中深入探讨几个 MFO 方法。
理解粗细搜索
粗细搜索(CFS)是网格搜索和随机搜索超参数调整方法的组合(参见*第三章,探索穷举搜索)。与被归类在无信息搜索**方法组的网格搜索和随机搜索不同,CFS 利用先前迭代的知识来拥有(希望)更好的搜索空间。换句话说,CFS 是一种 结合了顺序和并行 超参数调整方法。实际上,这是一个非常简单的方法,因为它基本上是 两种其他简单方法的组合:网格搜索和随机搜索。
CFS 可以有效地作为超参数调整方法,当你使用中等规模模型时,例如浅层神经网络(其他类型的模型也可以工作)以及适量的训练数据。
CFS 的主要思想只是从整个超参数空间开始进行 粗略 随机搜索,然后逐渐 细化 搜索的细节,无论是使用随机搜索还是网格搜索。以下图总结了 CFS 作为超参数调整方法的工作方式。
![图 6.2 – CFS 的说明
![img/B18753_06_002.jpg]
图 6.2 – CFS 的说明
如 图 6.2 所示,CFS 首先在整个预定义的超参数空间中进行随机搜索。然后,它根据第一次粗略随机搜索评估结果寻找一个有希望的子空间。有希望的子空间的定义可能不同,可以根据您的偏好进行调整。以下列表显示了您可以采用的几个有希望的子空间定义:
-
仅根据前一次试验中进行的评估,获取最佳超参数集的 最高 N 分位数。
-
对前一次试验中的不良超参数集设置一个 硬阈值 以过滤掉。
-
进行 单变量分析 以获取每个超参数的最佳值范围。
无论您使用什么定义来定义有希望的子空间,我们总会为每个超参数得到一个值列表。然后,我们可以根据每个超参数值列表中的最小值和最大值创建一个新的超参数空间。
在得到有希望的子空间后,我们可以在较小的区域内通过执行网格搜索或另一个随机搜索来继续该过程。请注意,您还可以设置条件,以确定何时继续使用随机搜索以及何时开始使用网格搜索。再次强调,选择适当的条件取决于您。然而,进行随机搜索比进行网格搜索更好,这样我们就可以与昂贵的保真度高的方法相比,有更多的基于低成本低保真度方法的评估。我们重复执行此过程,直到达到停止标准。
以下过程更详细地解释了 CFS 作为超参数调整方法的工作原理:
-
将原始完整数据集分为训练集和测试集。(参见第一章**,评估机器学习模型*。)
-
根据训练集和停止标准,定义超参数空间
H及其伴随的分布,目标函数f。 -
定义创建网格搜索超参数空间
grid_size的网格大小和随机搜索迭代次数random_iters。 -
通过利用目标函数
f定义有希望的子空间的标准。 -
定义何时开始使用网格搜索的标准。
-
将初始最佳超参数集
best_set设置为None。 -
在当前的超参数空间
H上,进行random_iters次随机搜索。 -
根据在步骤 4中定义的标准选择有希望的子空间:
-
如果当前表现最佳的超参数集比之前的
best_set差,将best_set添加到有希望的子空间中。 -
如果当前表现最佳的超参数集比之前的
best_set好,更新best_set。
-
-
如果满足
步骤 5中的标准,请执行以下操作:-
使用每个超参数的独特
grid_size值,将当前超参数空间H更新为在步骤 8中选定的有希望的子空间。 -
在更新的超参数空间
H上执行网格搜索。
-
-
如果未满足步骤 5中的标准,请执行以下操作:
-
使用每个超参数的最小值和最大值,将当前超参数空间
H更新为在步骤 8中选定的有希望的子空间。 -
在更新的超参数空间
H上,进行random_iters次随机搜索。
-
-
重复步骤 8 – 10,直到满足停止标准。
-
使用最佳超参数组合在完整训练集上进行训练。
-
在测试集上评估最终训练的模型。
在 CFS 中,多保真特性既不是基于数据量也不是基于训练轮数,而是基于在每个试验中搜索空间中执行的搜索粒度。换句话说,我们将继续使用所有数据和所有训练轮数,在每个试验中使用精细的超参数空间。
让我们看看 CFS 作为超参数调整方法在由make_classification生成的虚拟数据上的工作情况,以创建具有几个可定制配置的虚拟分类数据。在这个例子中,我们使用以下配置来生成虚拟数据:
-
类别数量。我们将数据中的目标类别数量设置为 2,通过设置
n_classes=2。 -
样本数量。我们将样本数量设置为 500,通过设置
n_samples=500。 -
特征数量。我们将特征数量或数据中的依赖变量数量设置为 25,通过设置
n_features=25。 -
信息特征数量。我们将具有高重要性以区分所有目标类的特征数量设置为 18,通过设置
n_informative=18。 -
冗余特征数量。我们将基本上只是其他特征加权求和的特征数量设置为 5,通过设置
n_redundant=5。 -
随机种子。为了确保可重复性,我们设置
random_state=0。
我们使用一个12作为停止标准。我们将每个随机搜索试验的迭代次数设置为20。最后,我们利用顶部 N 百分位数方案来定义每个试验中的具有希望的子空间,其中N=50。我们定义超参数空间如下:
-
隐藏层中的神经元数量:
hidden_layer_sizes=range(1,51) -
初始学习率:
learning_rate_init=np.linspace(0.001,0.1,50)
以下图显示了 CFS 在每个迭代或试验中的工作方式。紫色点表示当前试验中测试的超参数值,而红色矩形表示下一个试验中要搜索的具有希望的子空间。

图 6.3 – CFS 过程的说明
在图 6.3中,我们可以清楚地看到 CFS 是如何从整个超参数空间开始工作,然后逐渐在更小的子空间中搜索的。也值得注意,尽管在这个例子中我们只使用了随机搜索,但我们仍然可以看到 CFS 在试验次数增加时仍然提高了其保真度,直到在最后一个试验中得到最终的超参数集。我们还可以在以下图中看到每个试验的性能。

图 6.4 – 收敛图
图 6.4中的蓝色线反映了所有测试超参数在每个试验中的平均交叉验证分数(参见图 6.3中的紫色点)。红色线反映了每个试验中表现最佳的超参数集的交叉验证分数。我们可以看到红色线具有很好的非递减单调特性。这是因为我们总是将所有先前试验中最佳的超参数集添加回具有希望的子空间定义中,正如在先前程序中的步骤 8所定义的。我们将在第七章,通过 Scikit 进行超参数调整中学习如何实现 CFS。
下表总结了利用 CFS 作为超参数调整方法的优缺点。

图 6.5 – CFS 的优缺点
在本节中,我们讨论了 CFS,探讨了它是什么,如何工作,以及优缺点。我们将在下一节讨论另一个有趣的 MFO 方法。
理解连续减半
连续减半(SH)是一种 MFO 方法,它不仅能够专注于更有希望的超参数子空间,而且还能在每个试验中明智地分配计算成本。与 CFS 不同,CFS 在每个试验中利用所有数据,而 SH 可以为不太有希望的子空间利用较少的数据,而为更有希望的子空间利用更多的数据。可以说,SH 是 CFS 的一个变种,具有更清晰的算法定义,并且在计算成本的分配上更为明智。将 SH 作为超参数调整方法的最有效方式是在处理大型模型(例如,深度神经网络)和/或处理大量数据时。
与 CFS 类似,SH 也使用网格搜索或随机搜索来搜索最佳的超参数集。在第一次迭代中,SH 将在整个超参数空间上执行带有少量预算或资源的网格或随机搜索,然后它将逐渐增加预算,同时在每次迭代中移除最差的半数超参数候选者。换句话说,SH 在更大的搜索空间上以较低的预算进行超参数调整,在更有希望的较小子空间上以较高的预算进行超参数调整。SH 也可以被视为超参数候选者之间的锦标赛,只有最佳候选者才能在试验结束时幸存。
SH 中的预算定义
在默认的超参数调整设置中,预算定义为数据中的样本数量。然而,也可以以其他方式定义预算。例如,我们也可以将预算定义为最大训练时间、XGBoost 训练步骤中的迭代次数、随机森林中的估计器数量,或者训练神经网络模型时的 epoch 数量。
在我们讨论如何在正式程序中工作之前,为了更好地理解 SH,让我们先看看以下示例。我们使用与理解 CFS部分中使用的相同模型和相同的超参数空间定义。我们还使用类似的程序生成一个大小大一百倍的虚拟分类数据集,这意味着我们有50000个样本,而不是像 CFS 示例中那样只有500个样本。
在这个例子中,我们使用随机搜索而不是网格搜索来在每个试验中采样超参数候选者。以下图显示了超参数候选者在试验中的准确度分数。每条线代表每个超参数候选者的目标函数分数的趋势,在这种情况下是七折交叉验证准确度分数,相对于试验次数。基于从 SH 调整过程中选择的最佳超参数集,最终的目标函数分数是0.984。我们将在第七章**,通过 Scikit 进行超参数调整和第九章**,通过 Optuna 进行超参数调整中学习如何实现 SH。

图 6.6 – SH 过程的示意图
在图 6.6中,我们可以清楚地看到 SH 如何只从每个试验中选取顶部的超参数候选者(见橙色椭圆)以在下一个试验中进行进一步评估。在第一次迭代中,进行了240次随机搜索,只有50000个样本中的600个可用。这意味着我们在第一次迭代中有240个超参数候选者,n_candidates。在这些超参数候选者中,SF 只选取了前80个候选者,在第二次迭代中使用更多的样本进行评估,即1800个样本。对于第三次迭代,SF 再次只选取了前27个候选者,并在5400个样本上评估它们。
这个过程会一直持续到我们不能使用更多的样本数量,因为这将超过最初定义的最大资源,max_resources。在这个例子中,最大资源定义为数据中的样本数量。然而,它也可以根据预算或资源的定义来定义为总轮数或训练步骤。
在这个例子中,我们在第 4 次迭代时停止,需要基于48600个样本评估3个候选者。最终选择的超参数候选者是那些在48600个样本上评估出的七折交叉验证准确度分数最高的一个。
如您所注意到的,每次试验中样本数量的逐渐增加和候选人数量的逐渐减少遵循相同的乘数因子,即factor,在这个例子中是3。这就是为什么我们必须在第 4 次迭代时停止,因为如果我们继续到第 5 次迭代,我们需要48600*3=145800个样本,而我们只有50000个样本在数据中。请注意,在运行 SH 调整过程之前,我们必须自己设置乘数因子的值。换句话说,这个乘数因子是 SH 的超参数。
SH 中的乘数因子
SH 中的 halving term 指的是将乘数因子值设为二。换句话说,每个试验中只有最好的半数超参数候选者会传递到下一个试验。然而,我们也可以用另一个值来改变这一点。例如,当我们把乘数因子设为三时,这意味着我们只取每个试验中排名前三分之一的超参数候选者。在实践中,将乘数因子设为三通常比设为二效果更好。
除了乘数因子和最大资源外,SH 还具有其他超参数,例如第一次迭代中使用的最小资源量,min_resources,以及第一次迭代中要评估的候选者初始数量,n_candidates。如果在 SH 调整过程中使用了网格搜索,n_candidates 将等于搜索空间中超参数所有组合的数量。如果使用了随机搜索,那么我们必须自己设置 n_candidates 的值。在我们的例子中,由于使用了随机搜索,我们设置了 min_resources=600 和 n_candidates=240。
虽然 factor 设为三是一种常见做法,但 min_resources 和 n_candidates 并非如此。在选择 min_resources 和 n_candidates 超参数的正确值之前,需要考虑许多因素。换句话说,它们之间存在权衡,如下所述:
-
当好坏超参数可以通过较少的样本(较小的
min_resources值)轻松区分时,为n_candidates选择一个较大的值是有用的。 -
当我们需要更多的样本(更大的
min_resources值)来区分好坏超参数时,为n_candidates选择一个较小的值是有用的。
SH 还有一个超参数,即最小早期停止率,min_early_stopping。这个整型超参数的默认值为零。如果将其设置为大于零的值,它将减少迭代次数,同时增加第一次迭代中使用的资源数量。在我们之前的例子中,我们设置了 min_early_stopping=0。
总结来说,SH 作为一种超参数调整方法,其工作原理如下:
-
将原始数据集分为训练集和测试集。
-
定义基于训练集的超参数空间,
H,以及伴随的分布和目标函数,f。 -
定义预算/资源。通常,这被定义为样本数量或训练轮数。
-
定义最大资源量,
max_resources。通常,这被定义为数据中的总样本数或总轮数。 -
定义乘数因子,
factor,第一次迭代中使用的最小资源量,min_resources,以及最小早期停止率,min_early_stopping。 -
定义第一次迭代要评估的超参数候选者初始数量,
n_candidates。如果使用网格搜索,这将从搜索空间中总超参数组合数自动解析。 -
使用以下公式计算最大迭代次数niter:

-
断言
n_candidates≥
以确保在最后迭代中至少有一个候选者。 -
预热第一次迭代:
-
从超参数空间中采样
n_candidates个超参数集。如果使用网格搜索,只需返回空间中的所有超参数组合。这组候选者被称为candidates1。 -
根据目标函数f,使用
min_resources评估所有candidates1超参数集。 -
计算用于选择下一迭代顶级候选者的topK值:
-

-
对于每个迭代i,从第二次迭代开始直到niter 迭代,按以下步骤进行:
-
通过从candidatesi-1中选择topK个最优化目标函数得分的候选者来更新当前候选者集candidatesi。
-
根据以下公式更新当前分配的资源resourcesi:
-

-
根据目标函数f,使用resourcesi评估所有candidatesi超参数集。
-
根据以下公式更新topK值:

-
返回最佳超参数候选者:
-
使用分配的资源数量和目标函数
f评估最后迭代中的所有候选者。请注意,最后迭代中分配的资源可能少于max_resources。 -
选择具有最佳目标函数得分的候选者。
-
-
使用第 11 步中最佳的超参数集在完整训练集上训练。
-
在测试集上评估最终训练的模型。
根据前面的示例和所述程序,我们可以看到 SH 在前几次迭代中通过使用少量资源进行低成本的、低保真度的评估,并在最后几次迭代中通过使用大量资源开始进行更昂贵的、高保真度的评估。
与其他黑盒方法的集成
SH 除了网格搜索和随机搜索之外,还可以与其他黑盒超参数调优方法结合使用。例如,在Optuna(见第九章**,通过 Optuna 进行超参数调优)包中,我们可以将 TPE(见第四章**,探索贝叶斯优化)与 SH 结合,其中 SH 充当修剪器。请注意,在 Optuna 中,预算/资源定义为训练步数或 epoch 数,而不是样本数。
以下是一个列表,列出了 SH 作为超参数调整方法的优缺点:

图 6.7 – SH 的优缺点
在实践中,大多数时候,我们不知道如何平衡资源量和候选者数量之间的权衡,因为没有明确的定义来区分坏的和好的超参数。可以帮助我们在这种权衡中找到甜点的一点是利用之前的类似实验配置,或者基于之前类似实验的可用的元数据执行元学习。
现在你已经了解了 SH,它的运作方式,何时使用它,以及它的优缺点,在下一节中,我们将学习这个方法的扩展,它试图克服 SH 的缺点。
理解超带宽
超带宽(HB)是 SH 的一个扩展,专门设计用来克服 SH 固有的问题(参见图 6.7)。尽管我们可以执行元学习来帮助我们平衡权衡,但在实践中我们大多数时候并没有所需的元数据。此外,SH 在前几次迭代中可能会移除更好的超参数集,这也是令人担忧的,仅仅通过找到权衡的甜点是无法解决的。HB 通过迭代调用 SH 多次来尝试解决这些问题。
由于 HB 只是 SH 的一个扩展,建议你在处理大型模型(例如,深度神经网络)和/或处理大量数据时,像 SH 一样使用 HB 作为你的超参数调整方法。此外,当你没有时间或元数据来帮助你配置资源量和候选者数量之间的权衡时,使用 HB 甚至比 SH 更好,这种情况在大多数时候都会发生。
HB 和 SH 之间的主要区别在于它们的超参数。HB 具有与 SH 相同的超参数(参见理解 SH部分),除了n_candidates。在 HB 中,我们不需要选择n_candidates的最佳值,因为它在 HB 算法中自动计算。
基本上,HB 通过迭代运行 SH,每次迭代中n_candidates和min_resources都有变化,以及每个n_candidates和min_resources可能的最小值,并逐渐降低n_candidates的可能值,同时提高资源可能的最大值(参见图 6.8)。这就像是一种暴力方法,尝试几乎所有可能的n_candidates和min_resources的组合。

图 6.8 – HB 过程的示意图。这里,nj 和 rj 分别指代 bracket-j 的 n_candidates 和 min_resources
如图 6.8所示,假设我们设置factor=3,min_resources=1,max_resources=27,和min_early_stopping=0。正如你所见,HB 在第一个括号中分配了最小数量的资源,同时分配了最大数量的候选者,而在最后一个括号中分配了最大数量的资源,同时分配了最小数量的候选者。再次强调,每个括号都指的是每次 SH 运行,这意味着在这个示例中我们运行了四次 SH,其中最后一个括号基本上等同于在小的超参数空间上进行随机搜索或网格搜索。
通过测试几乎所有的n_candidates和min_resources的可能组合,HB 能够在 SH 中消除权衡,同时减少在第一次迭代中排除更好的超参数的可能性。然而,HB 的这项开创性特性并不能保证它总是比 SH 更好。为什么?因为 HB 实际上并没有尝试所有可能的组合。我们可能通过执行一次 SH 就能找到比 HB 尝试的所有可能组合更好的n_candidates和min_resources值的组合。然而,这需要时间和运气,因为我们必须手动选择n_candidates和min_resources值。
集成其他黑盒方法
在 HB 的原始论文中,作者为每次 SH 运行使用了随机搜索。然而,与 SH 一样,我们也可以将 HB 与其他黑盒方法集成。
下面的过程进一步说明了 HB 作为超参数调整方法的正式工作原理:
-
将完整原始数据集分为训练集和测试集。
-
根据训练集定义超参数空间,
H及其伴随的分布,以及目标函数,f。 -
定义
budget资源。这通常定义为样本数或训练 epoch 数。 -
定义最大资源,
max_resources。这通常定义为数据中的总样本数或总 epoch 数。 -
定义乘数因子,
factor,最小早期停止率,min_early_stopping,以及所有括号的最小资源数量,min_resources。通常,如果预算定义为样本数,则min_resources设置为 1。 -
创建一个字典,
top_candidates,它将被用来存储每次 SH 运行中表现最好的超参数集。 -
使用以下公式计算括号的数量,nbrackets:

-
对于每个括号-j,从
j=1开始,直到j=nbrackets,执行以下操作:- 使用以下公式计算 SH 第一次迭代中用于括号-j的最小资源数量,
:
- 使用以下公式计算 SH 第一次迭代中用于括号-j的最小资源数量,

- 使用以下公式计算 SH 第一次迭代中用于括号-j的初始超参数候选数量,
:

-
根据理解 SH部分给出的 SH 过程,利用
作为当前 SH 运行的min_resources,
作为n_candidates执行步骤 7 – 11。 -
将当前 SH 运行输出的最佳超参数集以及目标函数分数存储在
top_candidates字典中。 -
从
top_candidates字典中选择具有最佳目标函数分数的最佳候选者。 -
使用步骤 9中的最佳超参数集在完整训练集上训练。
-
在测试集上评估最终训练好的模型。
以下表格总结了利用 HB 作为超参数调整方法的优缺点:

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hparam-tun-py/img/B18753_06_009.jpg)
图 6.9 – HB 的优缺点
值得注意的是,尽管 HB 可以帮助我们处理 SH 的权衡,但它有更高的计算成本,因为我们必须迭代地运行几个 SH 轮次。当我们面临一个坏的和好的超参数难以通过小预算值区分的情况时,成本甚至更高。为什么?HB 的前几个使用小预算的括号会导致噪声估计,因为较小预算上的 SH 迭代中的相对排名并不反映较高预算上的实际相对排名。在最极端的情况下,最佳的超参数集将来自最后一个括号(随机搜索)。如果这种情况发生,那么 HB 将比随机搜索慢 n**括号 倍。
在本节中,我们讨论了 HB,它是什么,它是如何工作的,以及它的优缺点。我们将在下一节讨论另一个有趣的 MFO 方法。
理解 BOHB
贝叶斯优化和超带(BOHB)是 HB 的一个扩展,它在理解超参数候选者与目标函数之间的关系方面优于 CFS、SH 和 HB。如果 CFS、SH 和 HB 都是基于随机搜索的有信息搜索组,那么 BOHB 是基于 BO 方法的有信息搜索组。这意味着 BOHB 能够根据以往的经验而不是运气来决定需要搜索哪个子空间。
如其名所示,BOHB 是 BO 和 HB 方法的组合。虽然 SH 和 HB 也可以与其他黑盒方法一起使用(参见理解 SH和理解 HB部分),但 BOHB 专门设计用来以支持 HB 的方式利用 BO 方法。此外,BOHB 中的 BO 方法还跟踪所有预算上的所有先前评估,以便它可以作为未来评估的基础。请注意,BOHB 中使用的 BO 方法是多变量 TPE,它能够考虑到超参数之间的相互依赖性(参见第四章**,探索贝叶斯优化)。
BOHB 的主要卖点是其能够实现强大的初始性能和最终性能。这可以从原始 BOHB 论文的图 6.10中轻松看出(以下注释中提供详细信息)。如果没有进行元学习,BO(BO)将优于随机搜索,如果我们有更多时间让它从以往的经验中学习。如果没有时间,BO 将提供与随机搜索相似甚至更差的性能。另一方面,当时间有限时,HB 的表现远优于随机搜索,但如果允许随机搜索有更多时间探索超参数空间,HB 的表现将与随机搜索相似。通过结合两者的优点,BOHB 不仅能够在有限的时间内优于随机搜索,而且当给随机搜索足够的时间时也能做到。

图 6.10 – 随机搜索、BO、HB 和 BOHB 的比较
原始 BOHB 论文
*《大规模鲁棒且高效的超参数优化:BOHB》由 Stefan Falkner、Aaron Klein 和 Frank Hutter 著,第 35 届国际机器学习会议论文集,PMLR 80:1437-1446,2018 (proceedings.mlr.press/v80/falkner18a.html)。
以下过程进一步说明了 BOHB 作为超参数调整方法在形式上是如何工作的。请注意,BOHB 和 HB 非常相似,除了 HB 中的随机搜索被多变量 TPE 和随机搜索的组合所取代。由于 HB 只是多次迭代地执行 SH,实际的替换实际上是在 HB 的每个 SH 运行(每个括号)中执行的。
让我们从之前的说明重新开始。
6.(前六个步骤与理解 HB 部分中的相同。)
-
定义仅执行随机搜索而不是拟合多变量 TPE 的概率,random_prob。
-
定义多变量 TPE 拟合过程中良好超参数集的百分比,top_n_percent。(参见第四章**,探索贝叶斯优化。)
-
定义一个字典candidates_dict,该字典存储特定 SH 迭代中使用的预算/资源,以及超参数候选对和目标函数得分的键值对。
-
定义在开始拟合多变量 TPE 之前随机采样的超参数集的最小数量
n_min。默认情况下,我们将n_min设置为空间中超参数数量加一。 -
对于每个 bracket-j,从
j=1开始,直到j=nbrackets,执行以下操作: -
使用以下公式计算 SH 算法第一次迭代中用于 bracket-j的最低资源量!公式:

- 使用以下公式计算 SH 算法第一次迭代中用于 bracket-j 的初始超参数候选数量!公式:

-
通过利用
作为 min_resources 和
作为 n_candidates,按照理解 SH 部分中所述的 SH 过程执行步骤 7 – 11,其中步骤 9. I.被以下程序替换: -
从均匀分布中生成一个介于零和一之间的随机数,rnd。
-
如果
rnd<random_prod或models_dict为空,则执行随机搜索以采样初始超参数候选。 -
计算在
candidates_dict![公式]中采样的超参数数量,并将其存储为num_curr_candidates。 -
如果
num_curr_candidates < n_min,则执行随机搜索以采样初始超参数候选。 -
或者,利用多变量 TPE(参见*第四章**,探索贝叶斯优化)来采样初始超参数候选。请注意,我们总是在
candidates_dict中使用的最大预算上利用多变量 TPE。对于好组和坏组,超参数集的数量基于以下公式定义:


-
将采样的初始超参数候选以及目标函数得分(来自步骤 ii、iv 或 v)存储在
candidates_dict![公式``]`中。 -
将当前 SH 运行输出的最佳超参数集以及目标函数得分存储在
top_candidates字典中。 -
从
top_candidates字典中选择具有最佳目标函数得分的最佳候选者。 -
使用第 14 步中找到的最佳超参数集在完整训练集上进行训练。
-
在测试集上评估最终训练好的模型。
注意,为了确保 BOHB 跟踪所有预算上的所有评估,我们还需要在每个 HB 分组的每个 SH 迭代中,将超参数候选者及其目标函数得分存储在candidates_dict[budget]中。在这里,每个 SH 迭代中的超参数候选者指的是candidatesi,而预算指的是step 10部分中的resourcesi,这也可以在以下图中看到:

图 6.11 – BOHB 跟踪所有预算上的评估
你可能会想知道 BOHB 是否可以利用并行资源,因为它使用的是一种因无法利用并行计算资源而臭名昭著的 BO 方法。答案是是的,这是可能的!你可以利用并行资源,因为在每个 BOHB 迭代中,特别是在 HB 迭代中,我们可以利用多个工作者并行评估多组超参数。
那么,BOHB 中使用的多变量 TPE 的顺序性质如何呢?是的,TPE 模型内部可能需要执行一些顺序过程。然而,BOHB 限制了提供给多变量 TPE 的超参数组数,所以可能不会花费太多时间。此外,对超参数组数的限制实际上是 BOHB 的作者们专门设计的。以下是从 BOHB 原始论文中的直接引用:
TPE 中的并行性是通过限制优化 EI 的样本数量来实现的,故意不全面优化以获得多样性。这确保了模型连续的建议在并行评估时足够多样化,从而产生接近线性的速度提升。
还值得注意的是,我们始终在最大的预算上使用多变量 TPE,以确保它有足够的预算(高保真度)来拟合,以最小化噪声估计的机会。因此,结合对传递给 TPE 的超参数组数的限制,我们试图确保多变量 TPE 被拟合在正确的超参数组数上。
下表总结了利用 BOHB 作为超参数调整方法的优缺点:

图 6.12 – BOHB 的优缺点
就像 HB 在面临无法用小预算值轻松区分好坏超参数的情况下,与随机搜索相比可能慢* nbrackets倍一样,BOHB 在面临相同条件时,与传统的 BO 相比也会慢 nbrackets*倍。
在本节中,我们详细介绍了 BOHB,包括它是什么,如何工作,以及它的优缺点。
摘要
在本章中,我们讨论了四种超参数调整方法中的第四组,称为 MFO 组。我们一般性地讨论了 MFO,以及它区别于黑盒优化方法的特点,还讨论了包括 CFS、SH、HB 和 BOHB 在内的几个变体。我们看到了它们之间的差异以及每种方法的优缺点。从现在开始,当有人向你询问 MFO 时,你应该能够自信地解释它。你也应该能够调试并设置最适合你特定问题定义的所选方法的最佳配置。
在下一章中,我们将开始使用 scikit-learn 包实现我们迄今为止所学的各种超参数调整方法。我们将熟悉 scikit-learn 包,并学习如何在各种超参数调整方法中利用它。
第二部分:实现
在本书的这一节中,我们将学习如何利用几个强大的包来实现上一节中讨论的所有超参数调整方法。
本节包括以下章节:
-
第七章, 通过 scikit-learn 进行超参数调整
-
第八章, 通过 Hyperopt 进行超参数调整
-
第九章, 通过 Optuna 进行超参数调整
-
第十章, 使用 DEAP 和 Microsoft NNI 进行高级超参数调整
第七章:第七章:通过 Scikit 进行超参数调整
scikit-learn是数据科学家使用最多的 Python 包之一。此包提供了一系列的scikit-learn功能,还有其他针对超参数调整任务的包,这些包建立在scikit-learn之上或模仿其接口,例如scikit-optimize和scikit-hyperband。
在本章中,我们将学习与scikit-learn、scikit-optimize和scikit-hyperband相关的所有重要事项,以及如何利用它们来实现我们在前几章中学到的超参数调整方法。我们将从介绍如何安装每个包开始。然后,我们将学习如何利用这些包的默认配置,并讨论可用的配置及其使用方法。此外,我们还将讨论超参数调整方法的实现与我们在前几章中学到的理论之间的关系,因为在实现过程中可能会有一些细微的差异或调整。
最后,凭借前几章的知识,您还将能够理解如果出现错误或意外结果时会发生什么,并了解如何设置方法配置以匹配您特定的问题。
在本章中,我们将涵盖以下主要主题:
-
介绍 scikit
-
实现 Grid Search
-
实现 Random Search
-
实现 Coarse-to-Fine Search
-
实现 Successive Halving
-
实现 Hyper Band
-
实现 Bayesian Optimization Gaussian Process
-
实现 Bayesian Optimization Random Forest
-
实现 Bayesian Optimization Gradient Boosted Trees
技术要求
我们将学习如何使用scikit-learn、scikit-optimize和scikit-hyperband实现各种超参数调整方法。为了确保您能够复制本章中的代码示例,您需要以下要求:
-
Python 3(版本 3.7 或更高)
-
安装的
Pandas包(版本 1.3.4 或更高) -
安装的
NumPy包(版本 1.21.2 或更高) -
安装的
Scipy包(版本 1.7.3 或更高) -
安装的
Matplotlib包(版本 3.5.0 或更高) -
安装的
scikit-learn包(版本 1.0.1 或更高) -
安装的
scikit-optimize包(版本 0.9.0 或更高) -
安装的
scikit-hyperband包(直接从 GitHub 仓库克隆)
本章的所有代码示例都可以在 GitHub 上找到:github.com/PacktPublishing/Hyperparameter-Tuning-with-Python。
介绍 Scikit
scikit-learn,通常称为sklearn包,其接口在许多实现类中保持一致性。
例如,所有实现的 ML 模型或sklearn都有相同的fit()和predict()方法,分别用于在训练数据上拟合模型和在测试数据上评估拟合的模型。当与数据预处理程序或sklearn一起工作时,每个预处理程序都有典型的fit()、transform()和fit_transform()方法,分别用于拟合预处理程序、使用拟合的预处理程序转换新数据以及直接拟合和转换用于拟合预处理程序的数据。
在第一章**,评估机器学习模型中,我们学习了如何通过交叉验证的概念利用sklearn来评估 ML 模型的性能,其中完整的数据被分成几个部分,如训练数据、验证数据和测试数据。在第三章至第六章中,我们始终使用交叉验证分数作为我们的目标函数。虽然我们可以手动执行超参数调整并基于分割数据计算交叉验证分数,但sklearn提供了专门用于超参数调整的类,这些类在调整过程中使用交叉验证分数作为目标函数。sklearn中实现了几个超参数调整类,例如GridSearchCV、RandomizedSearchCV、HalvingGridSearchCV和HalvingRandomSearchCV。
此外,sklearn中实现的所有超参数调整类都有一个一致的接口。我们可以使用fit()方法在给定数据上执行超参数调整,其中交叉验证分数用作目标函数。然后,我们可以使用best_params_属性来获取最佳的超参数集,使用best_score_属性来获取从最佳超参数集中得到的平均交叉验证分数,以及使用cv_results_属性来获取超参数调整过程的详细信息,包括但不限于每个折中测试的超参数集的目标函数分数。
为了防止在执行数据预处理步骤时(参见第一章**,评估机器学习模型),数据泄露,sklearn也提供了一个Pipeline对象,可以与超参数调整类一起使用。这个Pipeline对象将确保任何数据预处理步骤仅在交叉验证期间基于训练集进行拟合。本质上,这个对象只是一个由几个sklearn 转换器和估计器组成的链,它具有相同的fit()和predict()方法,就像一个普通的sklearn估计器一样。
虽然sklearn可以用于许多与机器学习相关的任务,但通常被称为sklearn的scikit-optimize,可以用于实现skopt,它与sklearn具有非常相似的接口,因此一旦你已经熟悉了sklearn本身,你将很容易熟悉skopt。在skopt中实现的主要超参数调整类是BayesSearchCV类。
skopt在BayesSearchCV类中提供了四种优化器的实现,即用作优化器的sklearn。请注意,在这里,优化器指的是skopt提供了各种获取函数的实现,即期望改进(EI)、改进概率(PI)和下置信界(LCB)函数。
最后但同样重要的是,scikit-hyperband包。此外,这个包建立在sklearn之上,专门为 HB 实现设计。在这个包中实现的超参数调整类是HyperbandSearchCV。它也具有与sklearn非常相似的接口。
对于sklearn和skopt,你可以像通常安装其他包一样通过pip install轻松安装它们。至于scikit-hyperband,该包的作者没有将其放在sklearn上。幸运的是,有一个分支版本(github.com/louisowen6/scikit-hyperband)的原始仓库,它与较新版本的sklearn(1.0.1或更高版本)配合得很好。要安装scikit-hyperband,请按照以下步骤操作:
-
将
github.com/louisowen6/scikit-hyperband克隆到你的本地机器:git clone https://github.com/louisowen6/scikit-hyperband -
打开克隆的仓库:
cd scikit-hyperband -
将
hyperband文件夹移动到你的工作目录:mv hyperband "path/to/your/working/directory"
现在你已经了解了scikit-learn、scikit-optimize和scikit-hyperband包,在接下来的章节中,我们将学习如何利用它们来实现各种超参数调整方法。
实现网格搜索
要实现一个for loop,测试搜索空间中所有可能的超参数值。然而,通过使用sklearn的网格搜索实现GridSearchCV,我们可以得到更简洁的代码,因为我们只需要调用一行代码来实例化类。
让我们通过一个例子来看看我们如何利用GridSearchCV来执行网格搜索。请注意,在这个例子中,我们正在对一个 RF 模型进行超参数调整。我们将使用sklearn对 RF 的实现,即RandomForestClassifier。在这个例子中使用的数据集是 Kaggle 上提供的银行数据集 – 营销目标(www.kaggle.com/datasets/prakharrathi25/banking-dataset-marketing-targets)。
原始数据来源
这份数据最初发表在 Sérgio Moro、Paulo Cortez 和 Paulo Rita 合著的《A Data-Driven Approach to Predict the Success of Bank Telemarketing》一文中,该文发表于《Decision Support Systems》,Elsevier 出版社,第 62 卷第 22-31 页,2014 年 6 月(doi.org/10.1016/j.dss.2014.03.001)。
这是一个二元分类数据集,包含与银行机构进行的营销活动相关的 16 个特征。目标变量由两个类别组成,是或否,表示银行客户的存款是否已订阅定期存款。因此,在这个数据集上训练机器学习模型的目的是确定客户是否可能想要订阅定期存款。更多详情,请参考 Kaggle 页面上的描述:
-
提供了两个数据集,即
train.csv数据集和test.csv数据集。然而,我们不会使用提供的test.csv数据集,因为它直接从训练数据中采样。我们将使用sklearn中的train_test_split函数手动将train.csv分割成两个子集,即训练集和测试集(见第一章**,评估机器学习模型)。我们将test_size参数设置为0.1,这意味着训练集将有40,689行,测试集将有4,522行。以下代码展示了如何加载数据并执行训练集和测试集的分割:import pandas as pd from sklearn.model_selection import train_test_split df = pd.read_csv("train.csv",sep=";") df_train, df_test = train_test_split(df, test_size=0.1, random_state=0)
在提供的数据中的 16 个特征中,有 7 个是数值特征,9 个是分类特征。至于目标类别的分布,训练和测试数据集中都有 12%是是,88%是否。这意味着我们不能使用准确率作为我们的指标,因为我们有一个不平衡的类别问题——目标类别分布非常倾斜的情况。因此,在这个例子中,我们将使用 F1 分数。
-
在执行网格搜索之前,让我们看看使用默认超参数值的RandomForestClassifier是如何工作的。此外,我们现在也尝试仅使用这七个数值特征来训练我们的模型。以下代码展示了如何获取仅数值特征,在训练集上使用这些特征训练模型,并最终在测试集上评估模型:
import numpy as np from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import f1_score
X_train_numerical变量仅存储训练数据中的数值特征:
X_train_numerical = df_train.select_dtypes(include=np.number).drop(columns=['y'])
y_train = df_train['y']
X_test_numerical变量仅存储测试数据中的数值特征:
X_test_numerical = df_test.select_dtypes(include=np.number).drop(columns=['y'])
y_test = df_test['y']
在训练数据上拟合模型:
model = RandomForestClassifier(random_state=0)
model.fit(X_train_numerical,y_train)
在测试数据上评估模型:
y_pred = model.predict(X_test_numerical)
print(f1_score(y_test, y_pred))
基于前面的代码,我们在测试集上对训练好的 RF 模型进行测试时,F1-Score 大约为0.436。请记住,这个结果仅使用了数值特征和RandomForestClassifier的默认超参数。
-
在执行网格搜索之前,我们必须以列表字典格式定义超参数空间,其中键指的是超参数的名称,而列表包含我们想要为每个超参数测试的所有值。比如说,我们为
RandomForestClassifier定义超参数空间如下:hyperparameter_space = { "n_estimators": [25,50,100,150,200], "criterion": ["gini", "entropy"], "max_depth": [3, 5, 10, 15, 20, None], "class_weight": ["balanced","balanced_subsample"], "min_samples_split": [0.01,0.1,0.25,0.5,0.75,1.0], } -
一旦我们定义了超参数空间,我们就可以将
GridSearchCV类应用于训练数据,使用最佳的超参数集在全部训练数据上训练一个新的模型,然后在该最终训练模型上评估测试数据,正如我们在第 3-6 章中学到的那样。以下代码展示了如何进行:from sklearn.model_selection import GridSearchCV
初始化模型:
model = RandomForestClassifier(random_state=0)
初始化GridSearchCV类:
clf = GridSearchCV(model, hyperparameter_space,
scoring='f1', cv=5,
n_jobs=-1, refit = True)
运行GridSearchCV类:
clf.fit(X_train_numerical, y_train)
打印最佳的超参数集:
print(clf.best_params_,clf.best_score_)
在测试数据上评估最终训练模型:
print(clf.score(X_test_numerical,y_test))
通过利用sklearn对网格搜索的实现而不是从头编写代码,看看我们的代码是多么的整洁!注意,我们只需要将sklearn的估计器和超参数空间字典传递给GridSearchCV类,其余的将由sklearn处理。在这个例子中,我们还向类传递了几个额外的参数,例如scoring='f1'、cv=5、n_jobs=-1和refit=True。
如其名所示,scoring参数控制我们在交叉验证期间想要使用的评分策略。虽然我们的目标函数始终是交叉验证分数,但此参数控制我们想要用作度量标准的分数类型。在这个例子中,我们使用 F1 分数作为度量标准。然而,您也可以传递一个自定义的可调用函数作为评分策略。
Sklearn 中可用的评分策略
您可以参考scikit-learn.org/stable/modules/model_evaluation.html#scoring-parameter了解sklearn实现的所有评分策略,如果您想实现自己的自定义评分策略,请参考scikit-learn.org/stable/modules/model_evaluation.html#scoring。
cv参数表示您想要执行多少个交叉验证折。n_jobs参数控制您想要并行运行多少个作业。如果您决定使用所有处理器,您只需将n_jobs设置为-1即可,就像我们在例子中所做的那样。
最后但同样重要的是,我们有refit参数。这个布尔参数负责决定在超参数调整过程结束后,我们是否想要使用最佳的超参数集在完整的训练集上重新拟合我们的模型。在这个例子中,我们设置refit=True,这意味着sklearn将自动使用最佳的超参数集在完整的训练集上重新拟合我们的随机森林模型。在执行超参数调整后重新在完整的训练集上重新训练我们的模型非常重要,因为我们只利用了训练集的子集。当初始化GridSearchCV类时,你可以控制的其他参数有几个。更多详情,你可以参考sklearn的官方页面(scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html)。
让我们回到我们的例子。通过在预定义的超参数空间中执行网格搜索,我们在测试集上的评估中得到了0.495的 F1 分数。最佳的超参数集是{'class_weight': 'balanced', 'criterion': 'entropy', 'min_samples_split': 0.01, 'n_estimators': 150},其目标函数分数为0.493。请注意,我们可以通过best_params_和best_score_属性分别获取最佳的超参数集及其目标函数分数。还不错!我们在 F1 分数上获得了大约0.06的改进。然而,请注意,我们仍然只使用了数值特征。
接下来,我们将尝试利用不仅数值特征,还包括数据中的分类特征。为了能够利用这些分类特征,我们需要执行分类编码预处理步骤。为什么?因为机器学习模型无法理解非数值特征。因此,我们需要将这些非数值特征转换为数值,以便机器学习模型能够利用这些特征。
记住,当我们想要执行任何数据预处理步骤时,我们必须非常小心,以防止出现数据泄露问题,我们可能会将部分测试数据引入训练数据(参见第一章**,评估机器学习模型)。为了防止这个问题,我们可以利用sklearn中的Pipeline对象。因此,我们不仅可以将估计器传递给GridSearchCV类,还可以传递一个由一系列数据预处理器和估计器组成的Pipeline对象:
-
由于在这个例子中,并非所有我们的特征都是分类的,我们只想对那些非数值特征执行分类编码,我们可以利用
ColumnTransformer类来指定我们想要应用分类编码步骤的特征。假设我们还想对数值特征执行归一化预处理步骤。我们还可以将这些数值特征传递给ColumnTransformer类,以及归一化转换器。然后,它将自动仅对那些数值特征应用归一化步骤。以下代码展示了如何使用ColumnTransformer创建这样的Pipeline对象,其中我们使用StandardScaler进行归一化步骤,使用OneHotEncoder进行分类编码步骤:from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline
获取数值特征和分类特征的列表:
numerical_feats = list(df_train.drop(columns='y').select_dtypes(include=np.number).columns)
categorical_feats = list(df_train.drop(columns='y').select_dtypes(exclude=np.number).columns)
初始化数值特征和分类特征的预处理程序:
numeric_preprocessor = StandardScaler()
categorical_preprocessor = OneHotEncoder(handle_unknown="ignore")
将每个预处理程序委托给相应的特征:
preprocessor = ColumnTransformer(
transformers=[
("num", numeric_preprocessor, numerical_feats),
("cat", categorical_preprocessor, categorical_feats),
])
创建预处理程序和模型的管道。在这个例子中,我们将预处理步骤命名为“preprocessor”,建模步骤命名为“model”:
pipe = Pipeline(
steps=[("preprocessor", preprocessor),
("model", RandomForestClassifier(random_state=0))])
如前述代码块所示,ColumnTransformer类负责将每个预处理程序委托给相应的特征。然后,我们只需通过单个预处理程序变量重复使用它,以执行所有预处理步骤。最后,我们可以创建一个由预处理程序变量和RandomForestClassifier组成的管道。请注意,在ColumnTransformer类和Pipeline类中,我们还需要分别提供每个预处理程序和管道步骤的名称。
-
现在我们已经定义了管道,我们可以通过利用管道中定义的所有特征和预处理程序来查看我们的模型在测试集上的表现(不进行超参数调整)。以下代码展示了我们如何直接使用管道执行与之前相同的
fit()和predict()方法:pipe.fit(X_train_full,y_train) y_pred = pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
根据前面的代码,我们在测试训练好的管道时,F1 分数大约为0.516。
-
接下来,我们也可以开始对管道执行网格搜索。然而,在我们能够这样做之前,我们需要重新定义超参数空间。我们需要更改字典中的键,格式为
<estimator_name_in_pipeline>__<hyperparameter_name>。以下是我们重新定义的超参数空间:hyperparameter_space = { "model__n_estimators": [25,50,100,150,200], "model__criterion": ["gini", "entropy"], "model__class_weight": ["balanced", "balanced_subsample"], "model__min_samples_split": [0.01,0.1,0.25,0.5,0.75,1.0], } -
以下代码展示了如何对管道而不是估计器本身执行网格搜索。本质上,代码与上一个版本相同。唯一的区别是我们正在对管道和数据中的所有特征而不是仅数值特征执行网格搜索。
初始化GridSearchCV类:
clf = GridSearchCV(pipe, hyperparameter_space,
scoring = 'f1', cv=5,
n_jobs=-1, refit = True
运行GridSearchCV类:
clf.fit(X_train_full, y_train)
打印最佳超参数集:
print(clf.best_params_, clf.best_score_)
在测试数据上评估最终训练好的模型:
print(clf.score(X_test_full, y_test))
根据前面的代码,我们在测试最终训练好的 RF 模型时,在测试集上使用最佳超参数集得到了大约0.549的 F1 分数。最佳超参数集是{‘model__class_weight’: ‘balanced_subsample’, ‘model__criterion’: ‘gini’, ‘model__min_samples_split’: 0.01, ‘model__n_estimators’: 100},其目标函数分数为0.549。
值得注意的是,我们还可以在管道中创建管道。例如,我们可以创建一个由缺失值插补和归一化模块组成的numeric_preprocessor管道。以下代码展示了我们如何创建这样的管道。SimpleImputer类是来自sklearn的缺失值插补转换器,它可以帮助我们在存在缺失值时执行均值、中位数、众数或常数插补策略:
from sklearn.impute import SimpleImputer
numeric_preprocessor = Pipeline(
steps=[("missing_value_imputation", SimpleImputer(strategy="mean")), ("normalization", StandardScaler())]
)
在本节中,我们学习了如何在sklearn中通过GridSearchCV类实现网格搜索,从定义超参数空间开始,设置GridSearchCV类的每个重要参数,学习如何利用Pipeline和ColumnTransformer类来防止数据泄露问题,以及学习如何在管道中创建管道。
在下一节中,我们将学习如何在sklearn中通过RandomizedSearchCV实现随机搜索。
实现sklearn的随机搜索
实现sklearn与实现网格搜索非常相似。主要区别是我们必须提供试验次数或迭代次数,因为随机搜索不会尝试超参数空间中的所有可能组合。此外,在定义搜索空间时,我们必须为每个超参数提供相应的分布。在sklearn中,随机搜索是通过RandomizedSearchCV类实现的。
为了理解我们如何在sklearn中实现随机搜索,让我们使用实现网格搜索部分中的相同示例。让我们直接尝试使用数据集中可用的所有特征。所有管道创建过程都是完全相同的,所以我们将直接跳转到如何定义超参数空间和RandomizedSearchCV类的过程中。以下代码展示了如何为空间中的每个超参数定义相应的分布:
from scipy.stats import randint, truncnorm
hyperparameter_space = {
"model__n_estimators": randint(5, 200),
"model__criterion": ["gini", "entropy"],
"model__class_weight": ["balanced","balanced_subsample"],
"model__min_samples_split": truncnorm(a=0,b=0.5,loc=0.005, scale=0.01),
}
如您所见,超参数空间与我们之前在实现网格搜索部分中定义的相当不同。在这里,我们还在每个超参数上指定了分布,其中randint和truncnorm用于n_estimators和min_samples_split超参数。至于criterion和class_weight,我们仍然使用与之前搜索空间相同的配置。请注意,不指定任何分布意味着我们对超参数应用了均匀分布,其中所有值都有相同的机会被测试。
本质上,randint分布是对离散变量的均匀分布,而truncnorm代表截断正态分布,正如其名称所暗示的,它是一个在特定范围内有界的修改后的正态分布。在这个例子中,范围被限制在a=0和b=0.5之间,均值为loc=0.005,标准差为scale=0.01。
超参数分布:
有许多其他可用的分布,您可以使用。sklearn接受所有具有rvs方法的分布,就像Scipy中的分布实现一样。本质上,这个方法只是一个从指定分布中采样值的方法。有关更多详细信息,请参阅Scipy的官方文档页面(docs.scipy.org/doc/scipy/reference/stats.html#probability-distributions)。
当初始化RandomizedSearchCV类时,我们还需要定义n_iter和random_state参数,分别代表迭代次数和随机种子。以下代码展示了如何执行与实现网格搜索部分中定义的相同管道的随机搜索。与实现网格搜索部分中的示例相比,该示例仅执行120次网格搜索迭代,而在这里,我们执行200次随机搜索,因为我们设置了n_iter=200。此外,我们有一个更大的超参数空间,因为我们增加了n_estimators和min_samples_split超参数值的粒度:
from sklearn.model_selection import RandomizedSearchCV
初始化RandomizedSearchCV类:
clf = RandomizedSearchCV(pipe, hyperparameter_space,
n_iter = 200, random_state = 0,
scoring = 'f1', cv=5,
n_jobs=-1, refit = True)
运行RandomizedSearchCV类:
clf.fit(X_train_full, y_train)
打印最佳超参数集:
print(clf.best_params_, clf.best_score_)
在测试数据上评估最终训练的模型:
print(clf.score(X_test_full, y_test))
根据前面的代码,我们在测试集上使用最佳超参数集测试最终训练的 RF 模型时,F1 分数大约为0.563。最佳超参数集是{‘model__class_weight’: ‘balanced_subsample’, ‘model__criterion’: ‘entropy’, ‘model__min_samples_split’: 0.005155815445940717, ‘model__n_estimators’: 187},目标函数得分为0.562。
在本节中,我们学习了如何通过RandomizedSearchCV类在sklearn中实现随机搜索,从定义超参数空间到设置RandomizedSearchCV类的每个重要参数。在下一节中,我们将学习如何使用sklearn执行 CFS(粗到细搜索)。
实现粗到细搜索:
在技术要求部分提到的仓库中,您可以在sklearn包中找到实现的自定义类CoarseToFineSearchCV。
让我们使用与 实现随机搜索 部分相同的示例和超参数空间,以了解 CoarseToFineSearchCV 在实际中的应用。请注意,此实现中的 CFS 仅利用随机搜索,并使用前 N 百分位方案来定义每一轮中的有希望子空间,类似于在 第六章 中显示的示例。然而,你可以根据自己的偏好编辑代码,因为 CFS 是一个非常简单的可自定义模块的方法。
以下代码展示了如何使用 CoarseToFineSearchCV 类执行 CFS。值得注意的是,此类具有与 RandomizedSearchCV 类非常相似的参数,但有几个额外的参数。random_iters 参数控制每个随机搜索试验的迭代次数,top_n_percentile 控制在定义有希望子空间的前 N 百分位中的 N 值(参见 第六章),n_iter 定义要执行的 CFS 迭代次数,而 continuous_hyperparams 存储预定义空间中的连续超参数列表。
初始化 CoarseToFineSearchCV 类:
clf = CoarseToFineSearchCV(pipe, hyperparameter_space,
random_iters=25, top_n_percentile=50, n_iter=10,
continuous_hyperparams=['model__min_samples_split'],
random_state=0, scoring='f1', cv=5,
n_jobs=-1, refit=True)
运行 CoarseToFineSearchCV 类:
clf.fit(X_train_full, y_train)
打印最佳超参数集:
print(clf.best_params_, clf.best_score_)
在测试数据上评估最终训练好的模型:
y_pred = clf.predict(X_test_full)
print(f1_score(y_test, y_pred))
根据前面的代码,我们在使用最佳超参数集在测试集上测试最终训练好的 RF 模型时,F1 分数大约为 0.561。最佳超参数集是 {‘model__class_weight’: ‘balanced_subsample’,‘model__criterion’: ‘entropy’,‘model__min_samples_split’: 0.005867409821769845,‘model__n_estimators’: 106},目标函数得分为 0.560。
在本节中,我们学习了如何通过 CoarseToFineSearchCV 类在 sklearn 上实现 CFS。在下一节中,我们将学习如何使用 sklearn 执行 SH。
实现连续减半法
与 CFS 类似,sklearn 中有 HalvingGridSearchCV 和 HalvingRandomSearchCV。正如它们的名称所暗示的,前者是 SH 的实现,在每个 SH 迭代中使用网格搜索,而后者使用随机搜索。
默认情况下,sklearn 中的 SH 实现使用样本数量,即 n_samples,作为 SH 中预算或资源的定义。然而,也可以使用其他定义来定义预算。例如,我们可以使用 RF 中的 n_estimators 作为预算,而不是使用样本数量。值得注意的是,如果它是超参数空间的一部分,则不能使用 n_estimators 或任何其他超参数来定义预算。
HalvingGridSearchCV和HalvingRandomSearchCV都有类似的标准 SH 参数来控制 SH 迭代的工作方式,例如factor参数,它指的是 SH 的乘数因子,resource指的是我们想要使用的预算定义,max_resources指的是最大预算或资源,而min_resources指的是第一次迭代中要使用的最小资源数量。默认情况下,max_resources参数设置为auto,这意味着当resource=’n_samples’时,它将使用我们拥有的总样本数。另一方面,sklearn实现了一个启发式方法来定义min_resources参数的默认值,称为smallest。这个启发式方法将确保我们有一个小的min_resources值。
对于HalvingRandomSearchCV来说,还有一个n_candidates参数,它指的是第一次迭代中要评估的初始候选者数量。请注意,这个参数在HalvingGridSearchCV中不可用,因为它将自动评估预定义空间中的所有超参数候选者。值得注意的是,sklearn实现了一个名为exhaust的策略来定义n_candidates参数的默认值。这个策略确保我们在第一次迭代中评估足够的候选者,以便在最后的 SH 迭代中尽可能多地利用资源。
除了那些标准 SH 参数之外,这两个类也都有aggressive_elimination参数,当资源较少时可以利用。如果这个布尔参数设置为True,sklearn将自动重新运行第一次 SH 迭代几次,直到候选者数量足够小。这个参数的目标是确保我们只在最后的 SH 迭代中评估最多factor个候选者。请注意,这个参数只在sklearn中实现,原始 SH 没有将这个策略作为调整方法的一部分(参见第六章)。
与GridSearchCV和RandomizedSearchCV类似,HalvingGridSearchCV和HalvingRandomSearchCV也有用于超参数调整的常用sklearn参数,例如cv、scoring、refit、random_state和n_jobs。
sklearn 中 SH 的实验特性
值得注意的是,根据sklearn的1.0.2版本,SH 的实现仍然处于实验阶段。这意味着类的实现或接口可能会有变化,而无需任何降级周期。
以下代码展示了HalvingRandomSearchCV如何使用其默认的 SH 参数进行工作。请注意,我们仍然使用与实现随机搜索部分相同的示例和超参数空间。也值得注意,在这个例子中我们只使用了HalvingRandomSearchCV类,因为HalvingGridSearchCV有一个非常相似的接口:
from sklearn.experimental import enable_halving_search_cv
from sklearn.model_selection import HalvingRandomSearchCV
初始化HalvingRandomSearchCV类:
clf = HalvingRandomSearchCV(pipe, hyperparameter_space,
factor=3,
aggressive_elimination=False,
random_state = 0,
scoring = 'f1', cv=5,
n_jobs=-1, refit = True)
运行HalvingRandomSearchCV类:
clf.fit(X_train_full, y_train)
打印最佳超参数集:
print(clf.best_params_, clf.best_score_)
在测试数据上评估最终训练的模型:
print(clf.score(X_test_full, y_test))
基于前面的代码,我们在测试集上使用最佳超参数集测试最终训练好的 RF 模型时,F1 分数大约为0.556。最佳超参数集为{‘model__class_weight’: ‘balanced_subsample’, ‘model__criterion’: ‘entropy’, ‘model__min_samples_split’: 0.007286406330027324, ‘model__n_estimators’: 42},其目标函数得分为0.565。
-
以下代码展示了如何生成一个显示每个 SH 迭代中调整过程的图:
import matplotlib.pyplot as plt
获取每个试验的拟合历史:
results = pd.DataFrame(clf.cv_results_)
results["params_str"] = results.params.apply(str)
results.drop_duplicates(subset=("params_str", "iter"), inplace=True)
mean_scores = results.pivot(
index="iter", columns="params_str", values="mean_test_score")
绘制每个试验的拟合历史图:
fig, ax = plt.subplots(figsize=(16,16))
ax = mean_scores.plot(legend=False, alpha=0.6, ax=ax)
labels = [
f"Iteration {i+1}\nn_samples={clf.n_resources_[i]}\nn_candidates={clf.n_candidates_[i]}"
for i in range(clf.n_iterations_)]
ax.set_xticks(range(clf.n_iterations_))
ax.set_xticklabels(labels, rotation=0, multialignment="left",size=16)
ax.set_title("F1-Score of Candidates over Iterations",size=20)
ax.set_ylabel("5-Folds Cross Validation F1-Score", fontsize=18)
ax.set_xlabel("")
plt.tight_layout()
plt.show()
- 基于前面的代码,我们得到以下图:

图 7.1 – SH 超参数调整过程
根据图 7.1,我们可以看到在最后一次迭代中,我们只使用了大约 14,000 个样本,而我们的训练数据中有大约 40,000 个样本。实际上,这并不是一个理想的情况,因为在最后一次 SH 迭代中,有太多的样本没有被利用。我们可以通过min_resources和n_candidates参数更改sklearn设置的 SH 参数的默认值,以确保在最后一次迭代中尽可能多地利用资源。
在本节中,我们学习了如何通过HalvingRandomSearchCV和HalvingGridSearchCV类在sklearn中实现 SH。我们还学习了这两个类中所有重要的参数。在下一节中,我们将学习如何使用scikit-hyperband执行 HB。
实现 Hyper Band
Successive Halving 的扩展,scikit-hyperband包。这个包建立在sklearn之上,这意味着它也为GridSearchCV、RandomizedSearchCV、HalvingGridSearchCV和HalvingRandomSearchCV提供了非常相似的接口。
与sklearn实现中默认的 SH 预算定义相比,Scikit-Hyperband 将预算定义为树集成中的估计器数量,即n_estimators,或者使用随机梯度下降训练的估计器的迭代次数,例如 XGBoost 算法。此外,我们还可以使用估计器中存在的任何其他超参数作为预算定义。然而,scikit-hyperband不允许我们将样本数量作为预算定义。
让我们使用与实现 Successive Halving部分相同的示例,但使用不同的超参数空间。在这里,我们使用估计器数量,即n_estimators,作为资源,这意味着我们必须从搜索空间中移除这个超参数。请注意,当您将其用作资源定义时,您还必须从空间中移除任何其他超参数,就像在sklearn的 SH 实现中一样。
以下代码展示了 HyperbandSearchCV 的工作原理。resource_param 参数指的是用作预算定义的超参数。eta 参数实际上与 HalvingRandomSearchCV 或 HalvingGridSearchCV 类中的因子参数相同,它指的是每个 SH 运行的乘数因子。min_iter 和 max_iter 参数指的是所有括号的最小和最大资源。请注意,与 sklearn 实现的 SH 相比,这里没有自动策略来设置 min_iter 和 max_iter 参数的值。
剩余的 HyperbandSearchCV 参数与其他任何 sklearn 实现的超参数调整方法类似。值得注意的是,本书中使用的 HB 实现是 scikit-hyperband 包的修改版。请检查本书 GitHub 仓库中的以下文件夹(github.com/PacktPublishing/Hyperparameter-Tuning-with-Python/tree/main/hyperband):
from hyperband import HyperbandSearchCV
初始化 HyperbandSearchCV 类:
clf = HyperbandSearchCV(pipe, hyperparameter_space,
resource_param='model__n_estimators',
eta=3, min_iter=1, max_iter=100,
random_state = 0,
scoring = 'f1', cv=5,
n_jobs=-1, refit = True)
运行 HyperbandSearchCV 类:
clf.fit(X_train_full, y_train)
打印最佳超参数集:
print(clf.best_params_, clf.best_score_)
在测试数据上评估最终训练好的模型:
print(clf.score(X_test_full, y_test))
根据前面的代码,我们在测试集上使用最佳超参数集测试最终训练好的 RF 模型时,F1 分数大约为 0.569。最佳超参数集为 {‘model__class_weight’: ‘balanced’, ‘model__criterion’: ‘entropy’, ‘model__min_samples_split’: 0.0055643644642829684, model__n_estimators: 33},目标函数得分为 0.560。请注意,尽管我们从搜索空间中移除了 model__n_estimators,但 HyperbandSearchCV 仍然通过从最佳括号中选择来输出此超参数的最佳值。
在本节中,我们学习了如何借助 scikit-hyperband 包以及 HyperbandSearchCV 类的所有重要参数来实现 HB。在下一节中,我们将学习如何使用 scikit-optimize 进行贝叶斯优化。
实现贝叶斯优化高斯过程
skopt 包。与 scikit-hyperband 类似,此包也是建立在 sklearn 包之上,这意味着实现的贝叶斯优化调整类的接口与 GridSearchCV、RandomizedSearchCV、HalvingGridSearchCV、HalvingRandomSearchCV 和 HyperbandSearchCV 非常相似。
然而,与 sklearn 或 scikit-hyperband 不同,后者可以直接与 scipy 中实现的分布一起工作,在 skopt 中,我们只能在定义超参数空间时使用该包提供的包装器。包装器是在 skopt.space.Dimension 实例中定义的,包括三种类型的维度,如 Real、Integer 和 Categorical。在每个维度包装器内部,skopt 实际上使用与 scipy 包中相同的分布。
默认情况下,Real 维度仅支持 uniform 和 log-uniform 分布,并可以接受任何实数/数值作为输入。至于 Categorical 维度,这个包装器只能接受分类值作为输入,正如其名称所暗示的。它将自动将分类值转换为整数甚至实数值,这意味着我们也可以利用分类超参数进行 BOGP!虽然我们可以这样做,但请记住,BOGP 只在真实变量方面表现最佳(见第四章**,探索贝叶斯优化)。最后,我们有 Integer 维度包装器。默认情况下,此包装器仅支持用于整数格式的 uniform 和 log-uniform 分布。uniform 分布将使用 scipy 中的 randint 分布,而 log-uniform 分布与 Real 包装器中使用的完全相同。
值得注意的是,我们也可以为其他分布编写自己的包装器;例如,我们在所有早期示例中都使用的 truncnorm 分布。实际上,你可以在技术要求部分提到的仓库中找到包含 truncnorm、uniform 和 log-uniform 分布的自定义 Real 包装器。以下代码展示了我们如何为 BayesSearchCV 定义超参数空间。请注意,我们仍在使用与实现随机搜索部分相同的示例和超参数空间。在这里,Integer 和 Categorical 是 skopt 提供的原始包装器,而 Real 包装器是包含 truncnorm 分布的自定义包装器:
from skopt.space import *
hyperparameter_space = {
"model__n_estimators": Integer(low=5, high=200),
"model__criterion": Categorical(["gini", "entropy"]),
"model__class_weight": Categorical(["balanced","balanced_subsample"]),
"model__min_samples_split": Real(low=0,high=0.5,prior="truncnorm",
**{"loc":0.005,"scale":0.01})
}
BayesSearchCV 类的所有参数都与 GridSearchCV、RandomizedSearchCV、HalvingGridSearchCV、HalvingRandomSearchCV 或 HyperbandSearchCV 非常相似。BayesSearchCV 的唯一特定参数是 n_iter 和 optimizer_kwargs,分别表示要执行的总试验次数和包含 Optimizer 所有相关参数的参数。在这里,Optimizer 是一个表示每个贝叶斯优化步骤的类,从初始化初始点、拟合代理模型、使用获取函数的帮助来采样下一组超参数,以及优化获取函数(见第四章)。
有几个参数可以传递给optimizer_kwargs字典。base_estimator参数指的是要使用的代理模型类型。skopt已经准备了几个具有默认设置的代理模型,包括高斯过程或GP。n_initial_points参数指的是在开始实际贝叶斯优化步骤之前的随机初始点的数量。initial_point_generator参数指的是要使用的初始化方法。默认情况下,skopt将它们随机初始化。然而,您也可以将初始化方法更改为lhs、sobol、halton、hammersly或grid。
关于要使用的获取函数类型,默认情况下,skopt将使用gp_hedge,这是一个会自动选择acq_func参数中的LCB、EI和PI之一的获取函数。如第四章中所述,除了选择需要使用的获取函数外,我们还需要定义用于获取函数本身的优化器类型。skopt提供了两种获取函数优化器的选项,即随机采样(sampling)和lbfgs,或者第四章中提到的二阶优化策略类型。默认情况下,skopt将acq_optimizer参数设置为auto,这将自动选择何时使用sampling或lbfgs优化方法。
最后,我们还可以在optimizer_kwargs参数中传递acq_func_kwargs参数。我们可以将所有与获取函数相关的参数传递给这个acq_func_kwargs参数;例如,控制 BOGP 的探索和利用行为的xi参数,如第四章中所述。虽然xi参数负责控制 EI 和 PI 获取函数的探索与利用权衡,但还有一个名为kappa的参数,它负责与 LCB 获取函数相同的任务。xi或kappa的值越高,意味着我们更倾向于探索而非利用,反之亦然。有关BayesSearchCV类中所有可用参数的更多信息,您可以参考skopt包的官方 API 参考(scikit-optimize.github.io/stable/modules/classes.html)。
以下代码展示了我们如何利用BayesSearchCV在实现随机搜索部分相同的示例上执行 BOGP:
from skopt import BayesSearchCV
初始化BayesSearchCV类:
clf = BayesSearchCV(pipe, hyperparameter_space, n_iter=50,
optimizer_kwargs={"base_estimator":"GP",
"n_initial_points":10,
"initial_point_generator":"random",
"acq_func":"EI",
"acq_optimizer":"auto",
"n_jobs":-1,
"random_state":0,
"acq_func_kwargs": {"xi":0.01}
},
random_state = 0,
scoring = 'f1', cv=5,
n_jobs=-1, refit = True)
运行BayesSearchCV类:
clf.fit(X_train_full, y_train)
打印最佳超参数集:
print(clf.best_params_, clf.best_score_)
在测试数据上评估最终训练好的模型:
print(clf.score(X_test_full, y_test))
根据前面的代码,我们在测试集上使用最佳超参数集测试最终训练好的 RF 模型时,F1 分数大约为0.539。最佳超参数集为{‘model__class_weight’: ‘balanced’, ‘model__criterion’: ‘entropy’, ‘model__min_samples_split’: 0.02363008892366518, ‘model__n_estimators’: 94},目标函数得分为0.530。
在本节中,我们学习了如何在skopt中实现 BOGP,以及BayesSearchCV类中所有重要的参数。值得注意的是,skopt还具有实验跟踪模块,包括几个用于绘制结果的本地支持。我们将在第十三章,跟踪超参数调整实验中了解更多关于这些模块的内容。在下一节中,我们将学习如何执行贝叶斯优化的另一种变体,该变体使用 RF 作为其代理模型,并通过skopt实现。
实现贝叶斯优化随机森林
贝叶斯优化随机森林(BORF)是贝叶斯优化超参数调整方法的另一种变体,它使用 RF 作为代理模型。请注意,虽然这个变体与顺序模型算法配置(SMAC)不同,尽管两者都使用 RF 作为代理模型(参见第四章**,探索贝叶斯优化)。
使用skopt实现 BORF 实际上与上一节中讨论的实现 BOGP 非常相似。我们只需要在optimizer_kwargs中将base_estimator参数更改为RF。让我们使用与实现贝叶斯优化高斯过程节中相同的示例,但将获取函数从EI更改为LCB。此外,我们将acq_func_kwargs中的xi参数更改为kappa,因为我们使用LCB作为获取函数。请注意,我们仍然可以使用相同的获取函数。这里所做的更改只是为了展示如何与BayesSearchCV类的接口交互:
from skopt import BayesSearchCV
初始化BayesSearchCV类:
clf = BayesSearchCV(pipe, hyperparameter_space, n_iter=50,
optimizer_kwargs={"base_estimator":"RF",
"n_initial_points":10,
"initial_point_generator":"random",
"acq_func":"LCB",
"acq_optimizer":"auto",
"n_jobs":-1,
"random_state":0,
"acq_func_kwargs": {"kappa":1.96}
},
random_state = 0,
scoring = 'f1', cv=5,
n_jobs=-1, refit = True)
运行BayesSearchCV类:
clf.fit(X_train_full, y_train)
打印最佳超参数集:
print(clf.best_params_, clf.best_score_)
在测试数据上评估最终训练好的模型。
print(clf.score(X_test_full, y_test))
根据前面的代码,我们在测试集上使用最佳超参数集测试最终训练好的 RF 模型时,F1 分数大约为0.617。最佳超参数集为{‘model__class_weight’: ‘balanced_subsample’, ‘model__criterion’: ‘gini’, ‘model__min_samples_split’: 0.00043534042560206855, ‘model__n_estimators’: 85},目标函数得分为0.616。
在本节中,我们学习了如何在skopt中通过BayesSearchCV类实现 BORF。在下一节中,我们将学习如何执行贝叶斯优化的另一种变体,该变体使用梯度提升树作为其代理模型,并通过skopt实现。
实现贝叶斯优化梯度提升树
skopt允许我们传递任何其他来自sklearn的回归器作为base_estimator参数。然而,GBRT是skopt包中默认的代理模型的一部分,具有预定义的默认超参数值。
与实现贝叶斯优化随机森林部分类似,我们只需在optimizer_kwargs中将base_estimator参数更改为GBRT。以下代码展示了如何在skopt中实现 BOGBRT:
from skopt import BayesSearchCV
初始化BayesSearchCV类:
clf = BayesSearchCV(pipe, hyperparameter_space, n_iter=50,
optimizer_kwargs={"base_estimator":"GBRT",
"n_initial_points":10,
"initial_point_generator":"random",
"acq_func":"LCB",
"acq_optimizer":"auto",
"n_jobs":-1,
"random_state":0,
"acq_func_kwargs": {"kappa":1.96}
},
random_state = 0,
scoring = 'f1', cv=5,
n_jobs=-1, refit = True)
运行BayesSearchCV类:
clf.fit(X_train_full, y_train)
打印最佳超参数集:
print(clf.best_params_, clf.best_score_)
在测试数据上评估最终训练好的模型:
print(clf.score(X_test_full, y_test))
根据前面的代码,我们在测试集上使用最佳超参数集测试最终训练好的 RF 模型时,F1-Score 大约为0.611。最佳超参数集为{‘model__class_weight’: ‘balanced_subsample’, ‘model__criterion’: ‘gini’, ‘model__min_samples_split’: 0.0005745541104096049, ‘model__n_estimators’: 143},其目标函数评分为0.618。
在本节中,我们通过使用与实现贝叶斯优化随机森林部分相同的示例,学习了如何在skopt中通过BayesSearchCV类实现 BOGBRT。
摘要
在本章中,我们学习了关于scikit-learn、scikit-optimize和scikit-hyperband包在超参数调整方面的所有重要内容。此外,我们还学习了如何使用这些包实现各种超参数调整方法,并理解每个类的重要参数以及它们如何与我们在前几章中学到的理论相关。从现在开始,你应该能够利用这些包来实现你选择的超参数调整方法,并最终提高你的 ML 模型的性能。凭借第 3 至 6 章的知识,你还将能够理解如果出现错误或意外结果时会发生什么,以及如何设置方法配置以匹配你的特定问题。
在下一章中,我们将学习 Hyperopt 包及其如何用于执行各种超参数调整方法。下一章的目标与本章节类似,即能够利用该包进行超参数调整,并理解实现类中的每个参数。
第八章:第八章:通过 Hyperopt 进行超参数调整
Hyperopt 是一个 Python 优化包,提供了多种超参数调整方法的实现,包括 随机搜索、模拟退火(SA)、树结构帕累托估计器(TPE)和 自适应 TPE(ATPE)。它还支持各种类型的超参数,以及不同类型的采样分布。
在本章中,我们将介绍 Hyperopt 包,从其功能和限制开始,学习如何利用它进行超参数调整,以及你需要了解的关于 Hyperopt 的所有其他重要事项。我们将学习如何利用 Hyperopt 的默认配置进行超参数调整,并讨论可用的配置及其用法。此外,我们还将讨论超参数调整方法的实现与我们在前几章中学到的理论之间的关系,因为实现中可能有一些细微的差异或调整。
到本章结束时,你将能够了解关于 Hyperopt 的所有重要事项,并能够实现该包中提供的各种超参数调整方法。你还将能够理解它们类的重要参数以及它们与我们之前章节中学到的理论之间的关系。最后,凭借前几章的知识,你将能够理解如果出现错误或意外结果时会发生什么,以及如何设置方法配置以匹配你的特定问题。
本章将涵盖以下主题:
-
介绍 Hyperopt
-
实现随机搜索
-
实现树结构帕累托估计器
-
实现自适应树结构帕累托估计器
-
实现模拟退火
技术要求
在本章中,我们将学习如何使用 Hyperopt 实现各种超参数调整方法。为了确保你能重现本章中的代码示例,你需要以下条件:
-
Python 3(版本 3.7 或更高版本)
-
pandas包(版本 1.3.4 或更高版本) -
NumPy包(版本 1.21.2 或更高版本) -
Matplotlib包(版本 3.5.0 或更高版本) -
scikit-learn包(版本 1.0.1 或更高版本) -
Hyperopt包(版本 0.2.7 或更高版本) -
LightGBM包(版本 3.3.2 或更高版本)
本章的所有代码示例都可以在 GitHub 上找到,链接为 github.com/PacktPublishing/Hyperparameter-Tuning-with-Python。
介绍 Hyperopt
Hyperopt包中实现的全部优化方法都假设我们正在处理一个最小化问题。如果你的目标函数被分类为最大化问题,例如,当你使用准确率作为目标函数得分时,你必须对你的目标函数添加一个负号。
利用Hyperopt包进行超参数调整非常简单。以下步骤展示了如何执行Hyperopt包中提供的任何超参数调整方法。更详细的步骤,包括代码实现,将在接下来的章节中通过各种示例给出:
-
定义要最小化的目标函数。
-
定义超参数空间。
-
(可选) 初始化
Trials()对象并将其传递给fmin()函数。 -
通过调用
fmin()函数进行超参数调整。 -
使用从
fmin()函数输出中找到的最佳超参数集在全部训练数据上训练模型。 -
在测试数据上测试最终训练好的模型。
目标函数的最简单情况是我们只返回目标函数得分的浮点类型。然而,我们也可以将其他附加信息添加到目标函数的输出中,例如评估时间或我们想要用于进一步分析的任何其他统计数据。当我们向目标函数得分的输出添加附加信息时,Hyperopt期望目标函数的输出形式为 Python 字典,该字典至少包含两个强制性的键值对——即status和loss。前者键存储运行的状态值,而后者键存储我们想要最小化的目标函数。
Hyperopt 中最简单的超参数空间形式是 Python 字典的形式,其中键指的是超参数的名称,值包含从其中采样的超参数分布。以下示例展示了我们如何在Hyperopt中定义一个非常简单的超参数空间:
import numpy as np
from hyperopt import hp
hyperparameter_space = {
“criterion”: hp.choice(“criterion”, [“gini”, “entropy”]),
“n_estimators”: 5 + hp.randint(“n_estimators”, 195),
“min_samples_split” : hp.loguniform(“min_samples_split”, np.log(0.0001), np.log(0.5))
}
如您所见,hyperparameter_space字典的值是伴随空间中每个超参数的分布。Hyperopt提供了许多采样分布,我们可以利用,例如hp.choice、hp.randint、hp.uniform、hp.loguniform、hp.normal和hp.lognormal。hp.choice分布将随机从几个给定选项中选择一个。hp.randint分布将在[0, high)范围内随机选择一个整数,其中high是我们输入的值。在先前的示例中,我们传递了195作为high值并添加了5的值。这意味着Hyperopt将在[5,200)范围内随机选择一个整数。
剩余的分布都是针对实数/浮点超参数值的。请注意,Hyperopt 还提供了针对整数超参数值的分布,这些分布模仿了上述四个分布的分布情况——即hp.quniform、hp.qloguniform、hp.qnormal和hp.qlognormal。有关 Hyperopt 提供的采样分布的更多信息,请参阅其官方维基页面(github.com/hyperopt/hyperopt/wiki/FMin#21-parameter-expressions)。
值得注意的是,Hyperopt 使我们能够定义一个条件超参数空间(参见第四章**,贝叶斯优化),以满足我们的需求。以下代码示例展示了我们如何定义这样的搜索空间:
hyperparameter_space =
hp.choice(“class_weight_type”, [
{“class_weight”: None,
“n_estimators”: 5 + hp.randint(“none_n_estimators”, 45),
},
{“class_weight”: “balanced”,
“n_estimators”: 5 + hp.randint(“balanced_n_estimators”, 195),
}
])
如你所见,条件超参数空间和非条件超参数空间之间的唯一区别是在定义每个条件的超参数之前添加了hp.choice。在这个例子中,当class_weight为None时,我们只会在范围[5,50)内搜索最佳的n_estimators超参数。另一方面,当class_weight为“balanced”时,范围变为[5,200)。
一旦定义了超参数空间,我们就可以通过fmin()函数开始超参数调整过程。该函数的输出是从调整过程中找到的最佳超参数集。此函数中提供了几个重要的参数,你需要了解它们。fn参数指的是我们试图最小化的目标函数,space参数指的是我们实验中将要使用的超参数空间,algo参数指的是我们想要利用的超参数调整算法,rstate参数指的是调整过程的随机种子,max_evals参数指的是基于试验次数的调整过程停止标准,而timeout参数指的是基于秒数时间限制的停止标准。另一个重要的参数是trials参数,它期望接收Hyperopt的Trials()对象。
Hyperopt中的Trials()对象在调整过程中记录所有相关信息。此对象还负责存储我们放入目标函数字典输出中的所有附加信息。我们可以利用此对象进行调试或直接将其传递给Hyperopt内置的绘图模块。
Hyperopt包中实现了几个内置的绘图模块,例如main_plot_history、main_plot_histogram和main_plot_vars模块。第一个绘图模块可以帮助我们理解损失值与执行时间之间的关系。第二个绘图模块显示了所有试验中所有损失的直方图。第三个绘图模块对于理解每个超参数相对于损失值的热图非常有用。
最后但同样重要的是,值得注意的是,Hyperopt 还通过利用Trials()到MongoTrials()支持并行搜索过程。如果我们想使用 Spark 而不是 MongoDB,我们可以从Trials()切换到SparkTrials()。请参阅 Hyperopt 的官方文档以获取有关并行计算更多信息(github.com/hyperopt/hyperopt/wiki/Parallelizing-Evaluations-During-Search-via-MongoDB 和 hyperopt.github.io/hyperopt/scaleout/spark/)。
在本节中,你已了解了Hyperopt包的整体功能,以及使用此包进行超参数调优的一般步骤。在接下来的几节中,我们将通过示例学习如何实现Hyperopt中可用的每种超参数调优方法。
实现随机搜索
要在 Hyperopt 中实现随机搜索(见第三章),我们可以简单地遵循上一节中解释的步骤,并将rand.suggest对象传递给fmin()函数中的algo参数。让我们学习如何利用Hyperopt包来执行随机搜索。我们将使用与第七章**,通过 Scikit 进行超参数调优相同的相同数据和sklearn管道定义,但使用稍有不同的超参数空间定义。让我们遵循上一节中介绍的步骤:
-
定义要最小化的目标函数。在这里,我们利用定义的管道
pipe,通过使用sklearn中的cross_val_score函数来计算5 折交叉验证分数。我们将使用F1 分数作为评估指标:import numpy as np from sklearn.base import clone from sklearn.model_selection import cross_val_score from hyperopt import STATUS_OK def objective(space): estimator_clone = clone(pipe).set_params(**space) return {‘loss’: -1 * np.mean(cross_val_score(estimator_clone, X_train_full, y_train, cv=5, scoring=’f1’, n_jobs=-1)), ‘status’: STATUS_OK}
注意,定义的objective函数只接收一个输入,即预定义的超参数空间space,并输出一个包含两个强制性的键值对——即status和loss。还值得注意的是,我们之所以将平均交叉验证分数输出乘以-1,是因为Hyperopt始终假设我们正在处理一个最小化问题,而在这个例子中我们并非如此。
-
定义超参数空间。由于我们使用
sklearn管道作为我们的估计器,我们仍然需要遵循定义空间内超参数的命名约定(参见第七章)。请注意,命名约定只需应用于搜索空间字典键中的超参数名称,而不是采样分布对象内的名称:from hyperopt import hp hyperparameter_space = { “model__n_estimators”: 5 + hp.randint(“n_estimators”, 195), “model__criterion”: hp.choice(“criterion”, [“gini”, “entropy”]), “model__class_weight”: hp.choice(“class_weight”, [“balanced”,”balanced_subsample”]), “model__min_samples_split”: hp.loguniform(“min_samples_split”, np.log(0.0001), np.log(0.5)) } -
初始化
Trials()对象。在这个例子中,我们将在调整过程完成后利用此对象进行绘图:from hyperopt import Trials trials = Trials() -
通过调用
fmin()函数进行超参数调整。在这里,我们通过传递定义的目标函数和超参数空间进行随机搜索。我们将algo参数设置为rand.suggest对象,并将试验次数设置为100作为停止标准。我们还设置了随机状态以确保可重复性。最后但同样重要的是,我们将定义的Trials()对象传递给trials参数:from hyperopt import fmin, rand best = fmin(objective, space=hyperparameter_space, algo=rand.suggest, max_evals=100, rstate=np.random.default_rng(0), trials=trials ) print(best)
根据前面的代码,我们得到目标函数分数大约为-0.621,这指的是平均 5 折交叉验证 F--分数的0.621。我们还得到一个包含最佳超参数集的字典,如下所示:
{‘class_weight’: 0, ‘criterion’: 1, ‘min_samples_split’: 0.00047017001935242104, ‘n_estimators’: 186}
如所示,当我们使用hp.choice作为采样分布时,Hyperopt将仅返回超参数值的索引。在这里,通过参考预定义的超参数空间,class_weight的0表示平衡,而criterion的1表示熵。因此,最佳超参数集是{‘model__class_weight’: ‘balanced’, ‘model__criterion’: ‘entropy’, ‘model__min_samples_split’: 0.0004701700193524210, ‘model__n_estimators’: 186}。
-
使用
fmin()函数输出中找到的最佳超参数集在全部训练数据上训练模型:pipe = pipe.set_params(**{‘model__class_weight’: “balanced”, ‘model__criterion’: “entropy”, ‘model__min_samples_split’: 0.00047017001935242104, ‘model__n_estimators’: 186}) pipe.fit(X_train_full,y_train) -
在测试数据上测试最终训练的模型:
from sklearn.metrics import f1_score y_pred = pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
根据前面的代码,当我们在测试集上使用最佳超参数集测试最终训练的随机森林模型时,我们得到大约0.624的 F1 分数。
-
最后但同样重要的是,我们还可以利用
Hyperopt中实现的内置绘图模块。以下代码展示了如何进行这一操作。请注意,我们需要将调整过程中的trials对象传递给绘图模块,因为所有调整过程日志都存储在其中:from hyperopt import plotting
现在,我们必须绘制损失值与执行时间的关系:
plotting.main_plot_history(trials)
我们将得到以下输出:

图 8.1 – 损失值与执行时间的关系
现在,我们必须绘制所有试验的目标函数分数的直方图:
plotting.main_plot_histogram(trials)
我们将得到以下输出。

图 8.2 – 所有试验中目标函数分数的直方图
现在,我们必须绘制空间中每个超参数相对于损失值的热图:
Plotting.main_plot_vars(trials)
我们将得到以下输出。
![图 8.3 – 空间中每个超参数相对于损失值的热图(越暗,越好)
![img/B18753_08_003.jpg]
图 8.3 – 空间中每个超参数相对于损失值的热图(越暗,越好)
在本节中,我们通过查看与第七章中展示的类似示例相同的示例,学习了如何在Hyperopt中执行随机搜索。我们还看到了通过利用 Hyperopt 内置的绘图模块,我们可以得到什么样的图形。
值得注意的是,我们不仅限于使用sklearn模型的实现来使用Hyperopt进行超参数调整。我们还可以使用来自其他包的实现,例如PyTorch、Tensorflow等。需要记住的一点是在进行交叉验证时要注意数据泄露问题(参见第一章,“评估机器学习模型”)。我们必须将所有数据预处理方法拟合到训练数据上,并将拟合的预处理程序应用于验证数据。
在下一节中,我们将学习如何利用Hyperopt通过可用的贝叶斯优化方法之一进行超参数调整。
实现树结构帕累托估计器
Hyperopt包。要使用此方法进行超参数调整,我们可以遵循与上一节类似的程序,只需将步骤 4中的algo参数更改为tpe.suggest。以下代码显示了如何在Hyperopt中使用 TPE 进行超参数调整:
from hyperopt import fmin, tpe
best = fmin(objective,
space=hyperparameter_space,
algo=tpe.suggest,
max_evals=100,
rstate=np.random.default_rng(0),
trials=trials
)
print(best)
使用相同的数据、超参数空间和fmin()函数的参数,我们得到了大约-0.620的目标函数分数,这相当于平均 5 折交叉验证 F1 分数的0.620。我们还得到了一个包含最佳超参数集的字典,如下所示:
{‘class_weight’: 1, ‘criterion’: 1, ‘min_samples_split’: 0.0005245304932726025, ‘n_estimators’: 138}
一旦使用最佳超参数集在全部数据上训练了模型,我们在测试数据上测试训练好的最终随机森林模型时,F1 分数大约为0.621。
在本节中,我们学习了如何使用Hyperopt中的 TPE 方法进行超参数调整。在下一节中,我们将学习如何使用Hyperopt包实现 TPE 的一个变体,称为自适应 TPE。
实现自适应 TPE
自适应 TPE(ATPE)是 TPE 超参数调优方法的变体,它基于与 TPE 相比的几个改进而开发,例如根据我们拥有的数据自动调整 TPE 方法的几个超参数。有关此方法的更多信息,请参阅原始白皮书。这些可以在作者的 GitHub 仓库中找到(github.com/electricbrainio/hypermax)。
虽然您可以直接使用 ATPE 的原始 GitHub 仓库来实验这种方法,但Hyperopt也已将其作为包的一部分包含在内。您只需遵循实现随机搜索部分中的类似程序,只需在步骤 4中将algo参数更改为atpe.suggest即可。以下代码展示了如何在Hyperopt中使用 ATPE 进行超参数调优。请注意,在Hyperopt中使用 ATPE 进行超参数调优之前,我们需要安装LightGBM包:
from hyperopt import fmin, atpe
best = fmin(objective,
space=hyperparameter_space,
algo=atpe.suggest,
max_evals=100,
rstate=np.random.default_rng(0),
trials=trials
)
print(best)
使用相同的fmin()函数数据、超参数空间和参数,我们得到目标函数得分为约-0.621,这相当于平均 5 折交叉验证 F1 分数的0.621。我们还得到一个包含最佳超参数集的字典,如下所示:
{‘class_weight’: 1, ‘criterion’: 1, ‘min_samples_split’: 0.0005096354197481012, ‘n_estimators’: 157}
一旦使用最佳超参数集在全部数据上训练了模型,我们在测试数据上测试最终训练的随机森林模型时,F1 分数大约为0.622。
在本节中,我们学习了如何使用Hyperopt中的 ATPE 方法进行超参数调优。在下一节中,我们将学习如何使用Hyperopt包实现属于启发式搜索组的超参数调优方法。
实现模拟退火
Hyperopt包。类似于 TPE 和 ATPE,要使用此方法进行超参数调优,我们只需遵循实现随机搜索部分中显示的程序;我们只需要在步骤 4中将algo参数更改为anneal.suggest。以下代码展示了如何在Hyperopt中使用 SA 进行超参数调优:
from hyperopt import fmin, anneal
best = fmin(objective,
space=hyperparameter_space,
algo=anneal.suggest,
max_evals=100,
rstate=np.random.default_rng(0),
trials=trials
)
print(best)
使用相同的fmin()函数数据、超参数空间和参数,我们得到目标函数得分为约-0.620,这相当于平均 5 折交叉验证 F1 分数的0.620。我们还得到一个包含最佳超参数集的字典,如下所示:
{‘class_weight’: 1, ‘criterion’: 1, ‘min_samples_split’: 0.00046660708302994583, ‘n_estimators’: 189}
一旦使用最佳超参数集在全部数据上训练了模型,我们在测试数据上测试最终训练的随机森林模型时,F1 分数大约为0.625。
虽然Hyperopt具有内置的绘图模块,但我们也可以通过利用Trials()对象来创建自定义的绘图函数。以下代码展示了如何可视化每个超参数在试验次数中的分布:
-
获取每个试验中每个超参数的值:
plotting_data = np.array([[x[‘result’][‘loss’], x[‘misc’][‘vals’][‘class_weight’][0], x[‘misc’][‘vals’][‘criterion’][0], x[‘misc’][‘vals’][‘min_samples_split’][0], x[‘misc’][‘vals’][‘n_estimators’][0], ] for x in trials.trials]) -
将值转换为 pandas DataFrame:
import pandas as pd plotting_data = pd.DataFrame(plotting_data, columns=[‘score’, ‘class_weight’, ‘criterion’, ‘min_samples_split’,’n_estimators’]) -
绘制每个超参数分布与试验次数之间的关系图:
import matplotlib.pyplot as plt plotting_data.plot(subplots=True,figsize=(12, 12)) plt.xlabel(“Iterations”) plt.show()
基于前面的代码,我们将得到以下输出:

图 8.4 – 每个超参数分布与试验次数之间的关系
在本节中,我们学习了如何通过使用与“实现随机搜索”部分相同的示例来在Hyperopt中实现模拟退火(SA)。我们还学习了如何创建一个自定义绘图函数来可视化每个超参数分布与试验次数之间的关系。
摘要
在本章中,我们学习了关于Hyperopt包的所有重要内容,包括其功能和限制,以及如何利用它来进行超参数调整。我们了解到Hyperopt支持各种类型的采样分布方法,但只能与最小化问题一起工作。我们还学习了如何借助这个包实现各种超参数调整方法,这有助于我们理解每个类的重要参数以及它们与我们在前几章中学到的理论之间的关系。此时,你应该能够利用Hyperopt来实现你选择的超参数调整方法,并最终提高你的机器学习(ML)模型的性能。凭借从第三章到第六章的知识,你应该能够理解如果出现错误或意外结果时会发生什么,以及如何设置方法配置以匹配你的具体问题。
在下一章中,我们将学习关于Optuna包以及如何利用它来执行各种超参数调整方法。下一章的目标与本章节类似——即能够利用该包进行超参数调整,并理解实现类中的每个参数。
第九章:第九章: 通过 Optuna 进行超参数调优
scikit-optimize。
在本章中,您将了解Optuna包,从其众多功能开始,学习如何利用它进行超参数调优,以及您需要了解的关于Optuna的所有其他重要事项。我们不仅将学习如何利用Optuna及其默认配置进行超参数调优,还将讨论可用的配置及其用法。此外,我们还将讨论超参数调优方法的实现与我们在前几章中学到的理论之间的关系,因为实现中可能会有一些细微的差异或调整。
到本章结束时,您将能够了解关于Optuna的所有重要事项,并实现该包中提供的各种超参数调优方法。您还将能够理解每个类的重要参数以及它们与我们之前章节中学到的理论之间的关系。最后,凭借前几章的知识,您还将能够理解如果出现错误或意外结果时会发生什么,并了解如何设置方法配置以匹配您特定的难题。
本章将讨论以下主要主题:
-
介绍 Optuna
-
实现 TPE
-
实现随机搜索
-
实现网格搜索
-
实现模拟退火
-
实现逐次减半
-
实现 Hyperband
技术要求
我们将学习如何使用Optuna实现各种超参数调优方法。为确保您能够重现本章中的代码示例,您需要以下条件:
-
Python 3(版本 3.7 或更高)
-
安装
pandas包(版本 1.3.4 或更高) -
安装
NumPy包(版本 1.21.2 或更高) -
安装
Matplotlib包(版本 3.5.0 或更高) -
安装
scikit-learn包(版本 1.0.1 或更高) -
安装
Tensorflow包(版本 2.4.1 或更高) -
安装
Optuna包(版本 2.10.0 或更高)
本章的所有代码示例都可以在 GitHub 上找到:github.com/PacktPublishing/Hyperparameter-Tuning-with-Python。
介绍 Optuna
Optuna是一个 Python 超参数调优包,提供了多种超参数调优方法的实现,例如网格搜索、随机搜索、树结构帕累托估计器(TPE)等。与假设我们始终在处理最小化问题(参见第八章**,通过 Hyperopt 进行超参数调优)的Hyperopt不同,我们可以告诉Optuna我们正在处理哪种优化问题:最小化或最大化。
Optuna有两个主要类,即采样器和剪枝器。采样器负责执行超参数调整优化,而剪枝器负责根据报告的值判断是否应该剪枝试验。换句话说,剪枝器就像早期停止方法,当我们认为继续过程没有额外好处时,我们将停止超参数调整迭代。
内置的采样器实现包括我们在第三章到第四章中学到的几种超参数调整方法,即网格搜索、随机搜索和 TPE,以及本书范围之外的其他方法,例如 CMA-ES、NSGA-II 等。我们还可以定义自己的自定义采样器,例如模拟退火(SA),这将在下一节中讨论。此外,Optuna还允许我们集成来自另一个包的采样器,例如来自scikit-optimize(skopt)包,在那里我们可以利用许多基于贝叶斯优化的方法。
Optuna 的集成
除了skopt之外,Optuna还提供了许多其他集成,包括但不限于scikit-learn、Keras、PyTorch、XGBoost、LightGBM、FastAI、MLflow等。有关可用集成的更多信息,请参阅官方文档(optuna.readthedocs.io/en/v2.10.0/reference/integration.html)。
对于剪枝器,Optuna提供了基于统计和基于多保真优化(MFO)的方法。对于基于统计的组,有MedianPruner、PercentilePruner和ThresholdPruner。MedianPruner将在当前试验的最佳中间结果比前一个试验的结果中位数更差时剪枝。PercentilePruner将在当前最佳中间值是前一个试验的底部百分位数之一时进行剪枝。ThresholdPruner将简单地在任何预定义的阈值满足时进行剪枝。Optuna中实现的基于 MFO 的剪枝器是SuccessiveHalvingPruner和HyperbandPruner。两者都将资源定义为训练步骤或 epoch 的数量,而不是样本数量,如scikit-learn的实现。我们将在下一节中学习如何利用这些基于 MFO 的剪枝器。
要使用Optuna执行超参数调整,我们可以简单地执行以下简单步骤(更详细的步骤,包括代码实现,将在下一节中的各种示例中给出):
-
定义目标函数以及超参数空间。
-
通过
create_study()函数初始化study对象。 -
通过在
study对象上调用optimize()方法执行超参数调整。 -
使用找到的最佳超参数集在全部训练数据上训练模型。
-
在测试数据上测试最终训练好的模型。
在Optuna中,我们可以在objective函数本身内直接定义超参数空间。无需定义另一个专门的独立对象来存储超参数空间。这意味着在Optuna中实现条件超参数变得非常容易,因为我们只需将它们放在objective函数中的相应if-else块内。Optuna还提供了非常实用的超参数采样分布方法,包括suggest_categorical、suggest_discrete_uniform、suggest_int和suggest_float。
suggest_categorical方法将建议从分类类型的超参数中获取值,这与random.choice()方法的工作方式类似。suggest_discrete_uniform可用于离散类型的超参数,其工作方式与 Hyperopt 中的hp.quniform非常相似(参见第八章中通过 Hyperopt 进行超参数调整),通过从[low, high]范围内以q步长进行离散化均匀采样。suggest_int方法与random.randint()方法类似。最后是suggest_float方法。此方法适用于浮点类型的超参数,实际上是两个其他采样分布方法的包装,即suggest_uniform和suggest_loguniform。要使用suggest_loguniform,只需将suggest_float中的log参数设置为True。
为了更好地理解我们如何在objective函数内定义超参数空间,以下代码展示了如何使用objective函数定义一个objective函数的示例,以确保可读性并使我们能够以模块化方式编写代码。然而,您也可以直接将所有代码放在一个单独的objective函数中。本例中使用的数据和预处理步骤与第七章中相同,即通过 Scikit 进行超参数调整。然而,在本例中,我们使用的是神经网络模型而不是随机森林,如下所示:
-
创建一个函数来定义模型架构。在这里,我们创建了一个二元分类器模型,其中隐藏层的数量、单元数量、dropout 率和每层的
activation函数都是超参数空间的一部分,如下所示:import optuna from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout def create_model(trial: optuna.trial.Trial, input_size: int): model = Sequential() model.add(Dense(input_size,input_shape=(input_size,),activation='relu')) num_layers = trial.suggest_int('num_layers',low=0,high=3) for layer_i in range(num_layers): n_units = trial.suggest_int(f'n_units_layer_{layer_i}',low=10,high=100,step=5) dropout_rate = trial.suggest_float(f'dropout_rate_layer_{layer_i}',low=0,high=0.5) actv_func = trial.suggest_categorical(f'actv_func _layer_{layer_i}',['relu','tanh','elu']) model.add(Dropout(dropout_rate)) model.add(Dense(n_units,activation=actv_func)) model.add(Dense(1,activation='sigmoid')) return model -
创建一个函数来定义模型的优化器。请注意,我们在该函数中定义了条件超参数,其中针对不同选择的优化器有不同的超参数集,如下所示:
import tensorflow as tf def create_optimizer(trial: optuna.trial.Trial): opt_kwargs = {} opt_selected = trial.suggest_categorical('optimizer', ['Adam','SGD']) if opt_selected == 'SGD': opt_kwargs['lr'] = trial.suggest_float('sgd_lr',1e-5,1e-1,log=True) opt_kwargs['momentum'] = trial.suggest_float('sgd_momentum',1e-5,1e-1,log=True) else: #'Adam' opt_kwargs['lr'] = trial.suggest_float('adam_lr',1e-5,1e-1,log=True) optimizer = getattr(tf.optimizers,opt_selected)(**opt_kwargs) return optimizer -
创建
train和validation函数。请注意,预处理代码在此处未显示,但您可以在技术要求部分提到的 GitHub 仓库中看到完整的代码。与第七章中的示例一样,我们也将 F1 分数作为模型的评估指标,如下所示:def train(trial, df_train: pd.DataFrame, df_val: pd.DataFrame = None): X_train,y_train = df_train.drop(columns=['y']), df_train['y'] if df_val is not None: X_val,y_val = df_val.drop(columns=['y']), df_val['y'] #Apply pre-processing here... #... #Build model & optimizer model = create_model(trial,X_train.shape[1]) optimizer = create_optimizer(trial) model.compile(loss='binary_crossentropy', optimizer=optimizer, metrics=[f1_m]) history = model.fit(X_train,y_train, epochs=trial.suggest_int('epoch',15,50), batch_size=64, validation_data=(X_val,y_val) if df_val is not None else None) if df_val is not None: return np.mean(history.history['val_f1_m']) else: return model -
创建
objective函数。在这里,我们将原始训练数据分为用于超参数调整的训练数据df_train_hp和验证数据df_val。我们不会遵循 k 折交叉验证评估方法,因为这会在每个调整试验中花费太多时间让神经网络模型通过几个评估折(参见第一章**,评估机器学习模型)。from sklearn.model_selection import train_test_split def objective(trial: optuna.trial.Trial, df_train: pd.DataFrame): #Split into Train and Validation data df_train_hp, df_val = train_test_split(df_train, test_size=0.1, random_state=0) #Train and Validate Model val_f1_score = train(trial, df_train_hp, df_val) return val_f1_score
要在Optuna中执行超参数调整,我们需要通过create_study()函数初始化一个study对象。study对象提供了运行新的Trial对象和访问试验历史的接口。Trial对象简单地说是一个涉及评估objective函数过程的对象。此对象将被传递给objective函数,并负责管理试验的状态,在接收到参数建议时提供接口,就像我们在objective函数中之前看到的那样。以下代码展示了如何利用create_study()函数来初始化一个study对象:
study = optuna.create_study(direction='maximize')
在create_study()函数中,有几个重要的参数。direction参数允许我们告诉Optuna我们正在处理哪种优化问题。此参数有两个有效值,即‘maximize’和‘minimize’。通过将direction参数设置为‘maximize’,这意味着我们告诉Optuna我们目前正在处理一个最大化问题。Optuna默认将此参数设置为‘minimize’。sampler参数指的是我们想要使用的超参数调整算法。默认情况下,Optuna将使用 TPE 作为采样器。pruner参数指的是我们想要使用的修剪算法,其中默认使用MedianPruner()。
Optuna 中的修剪
虽然MedianPruner()默认被选中,但除非我们明确在objective函数中告诉Optuna这样做,否则修剪过程将不会执行。以下链接展示了如何使用Optuna的默认修剪器执行简单的修剪过程:github.com/optuna/optuna-examples/blob/main/simple_pruning.py。
除了前面提到的三个参数之外,create_study()函数中还有其他参数,即storage、study_name和load_if_exists。storage参数期望一个数据库 URL 输入,它将由Optuna处理。如果我们没有传递数据库 URL,Optuna将使用内存存储。study_name参数是我们想要赋予当前study对象的名称。如果我们没有传递名称,Optuna将自动为我们生成一个随机名称。最后但同样重要的是,load_if_exists参数是一个布尔参数,用于处理可能存在冲突的实验名称的情况。如果存储中已经生成了实验名称,并且我们将load_if_exists设置为False,那么Optuna将引发错误。另一方面,如果存储中已经生成了实验名称,但我们设置了load_if_exists=True,Optuna将只加载现有的study对象而不是创建一个新的对象。
一旦初始化了study对象并设置了适当的参数,我们就可以通过调用optimize()方法开始执行超参数调优。以下代码展示了如何进行操作:
study.optimize(func=lambda trial: objective(trial, df_train),
n_trials=50, n_jobs=-1)
在optimize()方法中存在几个重要的参数。第一个也是最重要的参数是func参数。这个参数期望一个可调用的对象,该对象实现了objective函数。在这里,我们并没有直接将objective函数传递给func参数,因为我们的objective函数需要两个输入,而默认情况下,Optuna只能处理一个输入的objective函数,即Trial对象本身。这就是为什么我们需要 Python 内置的lambda函数来将第二个输入传递给我们的objective函数。如果你的objective函数有超过两个输入,你也可以使用相同的lambda函数。
第二个最重要的参数是n_trials,它指的是超参数调优过程中的试验次数或迭代次数。另一个可以作为停止标准的实现参数是timeout参数。这个参数期望以秒为单位的停止标准。默认情况下,Optuna将n_trials和timeout参数设置为None。如果我们让它保持原样,那么Optuna将运行超参数调优过程,直到接收到终止信号,例如Ctrl+C或SIGTERM。
最后但同样重要的是,Optuna还允许我们通过一个名为n_jobs的参数来利用并行资源。默认情况下,Optuna将n_jobs设置为1,这意味着它将只利用一个工作。在这里,我们将n_jobs设置为-1,这意味着我们将使用计算机上的所有 CPU 核心来执行并行计算。
Optuna 中超参数的重要性
Optuna提供了一个非常棒的模块来衡量搜索空间中每个超参数的重要性。根据 2.10.0 版本,实现了两种方法,即fANOVA和Mean Decrease Impurity方法。请参阅官方文档了解如何利用此模块以及实现方法的背后理论,文档链接如下:optuna.readthedocs.io/en/v2.10.0/reference/importance.html。
在本节中,我们了解了Optuna的一般概念,我们可以利用的功能,以及如何使用此包进行超参数调整的一般步骤。Optuna还提供了各种可视化模块,可以帮助我们跟踪我们的超参数调整实验,这将在第十三章中讨论,跟踪超参数调整实验。在接下来的章节中,我们将通过示例学习如何使用Optuna执行各种超参数调整方法。
实现 TPE
TPE 是贝叶斯优化超参数调整组(见第四章)的一种变体,是Optuna中的默认采样器。要在Optuna中使用 TPE 进行超参数调整,我们只需将optuna.samplers.TPESampler()类传递给create_study()函数的采样器参数。以下示例展示了如何在Optuna中实现 TPE。我们将使用第七章中示例中的相同数据,并按照前节中介绍的步骤进行如下操作:
-
定义
objective函数以及超参数空间。在这里,我们将使用与介绍 Optuna部分中定义的相同的函数。请记住,我们在objective函数中使用的是训练-验证分割,而不是 k 折交叉验证方法。 -
通过
create_study()函数初始化study对象,如下所示:study = optuna.create_study(direction='maximize', sampler=optuna.samplers.TPESampler(seed=0)) -
通过在
study对象上调用optimize()方法来执行超参数调整,如下所示:study.optimize(lambda trial: objective(trial, df_train), n_trials=50, n_jobs=-1) print("Best Trial:") best_trial = study.best_trial print(" Value: ", best_trial.value) print(" Hyperparameters: ") for key, value in best_trial.params.items(): print(f" {key}: {value}")
根据前面的代码,我们在验证数据上得到了大约0.563的 F1 分数。我们还得到了一个包含最佳超参数集的字典,如下所示:
{'num_layers': 2,'n_units_layer_0': 30,'dropout_rate_layer_0': 0.14068484717257745,'actv_func_layer_0': 'relu','n_units_layer_1': 20,'dropout_rate_layer_1': 0.34708586671782293,'actv_func_layer_1': 'relu','optimizer': 'Adam','adam_lr': 0.0018287924415952158,'epoch': 41}
-
使用找到的最佳超参数集在全部训练数据上训练模型。在这里,我们定义了一个名为
train_and_evaluate_final()的另一个函数,其目的是基于前一步找到的最佳超参数集在全部训练数据上训练模型,并在测试数据上对其进行评估。您可以在技术要求部分提到的 GitHub 仓库中看到实现的函数。定义函数如下:train_and_evaluate_final(df_train, df_test, **best_trial.params) -
在测试数据上测试最终训练好的模型。根据前一步的结果,当使用最佳超参数集在测试集上测试我们最终训练的神经网络模型时,F1 分数大约为
0.604。
TPESampler类有几个重要的参数。首先,是gamma参数,它指的是 TPE 中用于区分好样本和坏样本的阈值(参见第四章)。n_startup_trials参数负责控制在进行 TPE 算法之前,将有多少次试验使用随机搜索。n_ei_candidates参数负责控制用于计算预期改进获取函数的候选样本数量。最后但同样重要的是,seed参数,它控制实验的随机种子。TPESampler类还有许多其他参数,请参阅以下链接的原版文档获取更多信息:optuna.readthedocs.io/en/v2.10.0/reference/generated/optuna.samplers.TPESampler.html。
在本节中,我们学习了如何在Optuna中使用与第七章示例中相同的数据执行超参数调优。如第四章中所述,探索贝叶斯优化,Optuna也实现了多变量 TPE,能够捕捉超参数之间的相互依赖关系。要启用多变量 TPE,我们只需将optuna.samplers.TPESampler()中的multivariate参数设置为True。在下一节中,我们将学习如何使用Optuna进行随机搜索。
实现随机搜索
在Optuna中实现随机搜索与实现 TPE(Tree-based Parzen Estimator)在Optuna中非常相似。我们只需遵循前一个章节的类似步骤,并在步骤 2中更改optimize()方法中的sampler参数。以下代码展示了如何进行操作:
study = optuna.create_study(direction='maximize',
sampler=optuna.samplers.RandomSampler(seed=0))
使用完全相同的数据、预处理步骤、超参数空间和objective函数,我们在验证数据中评估的 F1 分数大约为 0.548。我们还得到了一个包含最佳超参数集的字典,如下所示:
{'num_layers': 0,'optimizer': 'Adam','adam_lr': 0.05075826567070766,'epoch': 50}
使用最佳超参数集在完整数据上训练模型后,我们在测试数据上训练的最终神经网络模型测试时,F1 分数大约为0.596。请注意,尽管我们之前定义了许多超参数(参见前一小节中的objective函数),但在这里,我们并没有在结果中得到所有这些超参数。这是因为大多数超参数都是条件超参数。例如,由于为*’num_layers’*超参数选择的值是零,因此将不存在*’n_units_layer_{layer_i}’*、*’dropout_rate_layer_{layer_i}’*或*‘actv_func _layer_{layer_i}’*,因为这些超参数只有在`’num_layers’超参数大于零时才会存在。
在本节中,我们看到了如何使用Optuna的随机搜索方法进行超参数调整。在下一节中,我们将学习如何使用Optuna包实现网格搜索。
实现网格搜索
在Optuna中实现网格搜索与实现 TPE 和随机搜索略有不同。在这里,我们还需要定义搜索空间对象并将其传递给optuna.samplers.GridSampler()。搜索空间对象只是一个 Python 字典数据结构,其键是超参数的名称,而字典的值是对应超参数的可能值。如果搜索空间中的所有组合都已评估,即使传递给optimize()方法的n_trials数量尚未达到,GridSampler也会停止超参数调整过程。此外,无论我们传递给采样分布方法(如suggest_categorical、suggest_discrete_uniform、suggest_int和suggest_float)的范围如何,GridSampler都只会获取搜索空间中声明的值。
以下代码展示了如何在Optuna中执行网格搜索。在Optuna中实现网格搜索的总体步骤与实现树结构帕累托估计器一节中所述的步骤相似。唯一的区别是我们必须定义搜索空间对象,并在步骤 2中的optimize()方法中将sampler参数更改为optuna.samplers.GridSampler(),如下所示:
search_space = {'num_layers': [0,1],
'n_units_layer_0': list(range(10,50,5)),
'dropout_rate_layer_0': np.linspace(0,0.5,5),
'actv_func_layer_0': ['relu','elu'],
'optimizer': ['Adam','SGD'],
'sgd_lr': np.linspace(1e-5,1e-1,5),
'sgd_momentum': np.linspace(1e-5,1e-1,5),
'adam_lr': np.linspace(1e-5,1e-1,5),
'epoch': list(range(15,50,5))
}
study = optuna.create_study(direction='maximize', sampler=optuna.samplers.GridSampler(search_space),
)
根据前面的代码,我们在验证数据上评估的 F1 分数大约为0.574。我们还得到了一个包含最佳超参数集的字典,如下所示:
{'num_layers': 0,'optimizer': 'Adam','adam_lr': 0.05000500000000001,'epoch': 25}
使用最佳超参数集在完整数据上训练模型后,我们在测试数据上训练的最终神经网络模型测试时,F1 分数大约为0.610。
值得注意的是,GridSampler将依赖于搜索空间来执行超参数采样。例如,在搜索空间中,我们只定义了num_layers的有效值为[0,1]。因此,尽管在objective函数中我们设置了trial.suggest_int(num_layers,low=0,high=3)(参见介绍 Optuna部分),但在调整过程中只会测试0和1。记住,在Optuna中,我们可以通过n_trials或timeout参数指定停止标准。如果我们指定了这些标准之一,GridSampler将不会测试搜索空间中的所有可能组合;一旦满足停止标准,调整过程将停止。在这个例子中,我们设置了n_trials=50,就像前一个示例部分中那样。
在本节中,我们学习了如何使用Optuna的网格搜索方法进行超参数调整。在下一节中,我们将学习如何使用Optuna包实现模拟退火(SA)。
实现模拟退火
SA 不是Optuna内置的超参数调整方法的一部分。然而,正如本章第一部分所述,我们可以在Optuna中定义自己的自定义采样器。在创建自定义采样器时,我们需要创建一个继承自BaseSampler类的类。在我们自定义类中需要定义的最重要方法是sample_relative()方法。此方法负责根据我们选择的超参数调整算法从搜索空间中采样相应的超参数。
完整的自定义SimulatedAnnealingSampler()类,包括几何退火调度计划(参见第五章),已在技术要求部分中提到的 GitHub 仓库中定义,并可以查看。以下代码仅展示了类中sample_relative()方法的实现:
class SimulatedAnnealingSampler(optuna.samplers.BaseSampler):
...
def sample_relative(self, study, trial, search_space):
if search_space == {}:
# The relative search space is empty (it means this is the first trial of a study).
return {}
prev_trial = self._get_last_complete_trial(study)
if self._rng.uniform(0, 1) <= self._transition_probability(study, prev_trial):
self._current_trial = prev_trial
params = self._sample_neighbor_params(search_space)
#Geometric Cooling Annealing Schedule
self._temperature *= self.cooldown_factor
return params
...
以下代码展示了如何在Optuna中使用 SA 进行超参数调整。在Optuna中实现 SA 的整体过程与实现树结构帕累托估计器部分中所述的过程类似。唯一的区别是我们必须在步骤 2的optimize()方法中将sampler参数更改为SimulatedAnnealingSampler(),如下所示:
study = optuna.create_study(direction='maximize',
sampler=SimulatedAnnealingSampler(seed=0),
)
使用完全相同的数据、预处理步骤、超参数空间和objective函数,我们在验证数据中得到的 F1 分数大约为0.556。我们还得到了一个包含最佳超参数集的字典,如下所示:
{'num_layers': 3,'n_units_layer_0': 30,'dropout_rate_layer_0': 0.28421697443432425,'actv_func_layer_0': 'tanh','n_units_layer_1': 20,'dropout_rate_layer_1': 0.05936385947712203,'actv_func_layer_1': 'tanh','n_units_layer_2': 25,'dropout_rate_layer_2': 0.2179324626328134,'actv_func_layer_2': 'relu','optimizer': 'Adam','adam_lr': 0.006100619734336806,'epoch': 39}
在使用最佳超参数集在全部数据上训练模型后,当我们测试在测试数据上训练的最终神经网络模型时,F1 分数大约为0.559。
在本节中,我们学习了如何使用Optuna的 SA 算法进行超参数调整。在下一节中,我们将学习如何在Optuna中利用逐次减半作为剪枝方法。
实现 Successive Halving
Optuna意味着它负责在似乎没有继续进行过程的好处时停止超参数调整迭代。由于它被实现为剪枝器,Optuna中 SH(Successive Halving)的资源定义(见第六章)指的是模型的训练步数或 epoch 数,而不是样本数,正如scikit-learn实现中那样。
我们可以利用 SH(Successive Halving)作为剪枝器,同时使用我们使用的任何采样器。本例展示了如何使用随机搜索算法作为采样器,SH 作为剪枝器来执行超参数调整。整体流程与实现 TPE部分中所述的流程类似。由于我们使用 SH 作为剪枝器,我们必须编辑我们的objective函数,以便在优化过程中使用剪枝器。在本例中,我们可以使用Optuna提供的TFKeras的回调集成,通过optuna.integration.TFKerasPruningCallback。我们只需在train函数中拟合模型时将此类传递给callbacks参数,如下面的代码所示:
def train(trial, df_train: pd.DataFrame, df_val: pd.DataFrame = None):
...
history = model.fit(X_train,y_train,
epochs=trial.suggest_int('epoch',15,50),
batch_size=64,
validation_data=(X_val,y_val) if df_val is not None else None,
callbacks=[optuna.integration.TFKerasPruningCallback(trial,'val_f1_m')],
)
...
一旦我们告诉Optuna使用剪枝器,我们还需要在实现树结构 Parzen 估计器部分的步骤 2中将optimize()方法中的pruner参数设置为optuna.pruners.SuccessiveHalvingPruner(),如下所示:
study = optuna.create_study(direction='maximize',
sampler=optuna.samplers.RandomSampler(seed=0),
pruner=optuna.pruners.SuccessiveHalvingPruner(reduction_factor=3, min_resource=5)
)
在这个例子中,我们也增加了试验次数从50到100,因为大多数试验无论如何都会被剪枝,如下所示:
study.optimize(lambda trial: objective(trial, df_train),
n_trials=100, n_jobs=-1,
)
使用完全相同的数据、预处理步骤和超参数空间,我们在验证数据中得到的 F1 分数大约是0.582。在100次试验中,有87次试验被 SH 剪枝,这意味着只有13次试验完成。我们还得到了一个包含最佳超参数集的字典,如下所示:
{'num_layers': 3,'n_units_layer_0': 10,'dropout_rate_layer_0': 0.03540368984067649,'actv_func_layer_0': 'elu','n_units_layer_1': 15,'dropout_rate_layer_1': 0.008554081181978979,'actv_func_layer_1': 'elu','n_units_layer_2': 15,'dropout_rate_layer_2': 0.4887044768096681,'actv_func_layer_2': 'relu','optimizer': 'Adam','adam_lr': 0.02763126523504823,'epoch': 28}
在使用最佳超参数集在全部数据上训练模型之后,我们在测试数据上训练的最终神经网络模型的 F1 分数大约是0.597。
值得注意的是,SuccessiveHalvingPruner有几个参数我们可以根据我们的需求进行自定义。reduction_factor参数指的是 SH(Successive Halving)的乘数因子(见第六章)。min_resource参数指的是第一次试验中要使用的最小资源数量。默认情况下,此参数设置为‘auto’,其中使用启发式算法根据第一次试验完成所需的步数来计算最合适的值。换句话说,Optuna只有在执行了min_resource训练步数或 epoch 数之后才能开始调整过程。
Optuna还提供了min_early_stopping_rate参数,其意义与我们定义在第六章中的完全相同。最后但同样重要的是,bootstrap_count参数。此参数不是原始 SH 算法的一部分。此参数的目的是控制实际 SH 迭代开始之前需要完成的试验的最小数量。
你可能会想知道,关于控制最大资源和 SH 中候选人数的参数是什么?在这里,在Optuna中,最大资源的定义将根据定义的objective函数中的总训练步骤或 epoch 数自动推导。至于控制候选人数的参数,Optuna将此责任委托给study.optimize()方法中的n_trials参数。
在本节中,我们学习了如何在参数调整过程中利用 SH 作为剪枝器。在下一节中,我们将学习如何利用 SH 的扩展算法 Hyperband 作为Optuna中的剪枝方法。
实现 Hyperband
实现Optuna与实现 Successive Halving 作为剪枝器非常相似。唯一的区别是我们必须在上一节中的步骤 2中将optimize()方法中的pruner参数设置为optuna.pruners.HyperbandPruner()。以下代码展示了如何使用随机搜索算法作为采样器,HB 作为剪枝器进行超参数调整:
study = optuna.create_study(direction='maximize',
sampler=optuna.samplers.RandomSampler(seed=0),
pruner=optuna.pruners.HyperbandPruner(reduction_factor=3, min_resource=5)
)
HyperbandPruner的所有参数都与SuccessiveHalvingPruner相同,除了这里没有min_early_stopping_rate参数,而有一个max_resource参数。min_early_stopping_rate参数被移除,因为它根据每个括号的 ID 自动设置。max_resource参数负责设置分配给试验的最大资源。默认情况下,此参数设置为‘auto’,这意味着其值将设置为第一个完成的试验中的最大步长。
使用完全相同的数据、预处理步骤和超参数空间,我们在验证数据中得到的 F1 分数大约是0.580。在进行的100次试验中,有79次试验被 SH 剪枝,这意味着只有21次试验完成。我们还得到了一个包含最佳超参数集的字典,如下所示:
{'num_layers': 0,'optimizer': 'Adam','adam_lr': 0.05584201313189952,'epoch': 37}
在使用最佳超参数集在全部数据上训练模型后,当我们测试在测试数据上训练的最终神经网络模型时,F1 分数大约是0.609。
在本节中,我们学习了如何在Optuna的参数调整过程中利用 HB 作为剪枝器。
摘要
在本章中,我们学习了Optuna包的所有重要方面。我们还学会了如何利用这个包实现各种超参数调优方法,并且理解了每个类的重要参数以及它们与我们之前章节中学到的理论之间的关系。从现在开始,你应该能够利用我们在上一章中讨论的包来实现你选择的超参数调优方法,并最终提升你的机器学习模型的性能。掌握了第三章至第六章的知识,你还将能够调试代码,如果出现错误或意外结果,你还将能够制定自己的实验配置以匹配你的特定问题。
在下一章中,我们将学习 DEAP 和 Microsoft NNI 包以及如何利用它们来执行各种超参数调优方法。下一章的目标与本章类似,即能够利用包进行超参数调优,并理解实现类中的每个参数。
第十章:第十章:使用 DEAP 和 Microsoft NNI 进行高级超参数调整
DEAP和Microsoft NNI是 Python 包,提供了其他包中未实现的多种超参数调整方法,这些包我们在第 7-9 章中讨论过。例如,遗传算法、粒子群优化、Metis、基于群体的训练以及更多。
在本章中,我们将学习如何使用 DEAP 和 Microsoft NNI 包进行超参数调整,从熟悉这些包以及我们需要注意的重要模块和参数开始。我们将学习如何利用 DEAP 和 Microsoft NNI 的默认配置进行超参数调整,并讨论其他可用的配置及其使用方法。此外,我们还将讨论超参数调整方法的实现如何与我们之前章节中学到的理论相关联,因为实现中可能会有一些细微的差异或调整。
在本章结束时,你将能够理解关于 DEAP 和 Microsoft NNI 你需要知道的所有重要事项,并能够实现这些包中可用的各种超参数调整方法。你还将能够理解每个类的重要参数以及它们与我们之前章节中学到的理论之间的关系。最后,凭借前几章的知识,你还将能够理解如果出现错误或意外结果时会发生什么,并了解如何设置方法配置以匹配你的特定问题。
本章将讨论以下主要主题:
-
介绍 DEAP
-
实现遗传算法
-
实现粒子群优化
-
介绍 Microsoft NNI
-
实现网格搜索
-
实现随机搜索
-
实现树结构 Parzen 估计器
-
实现序列模型算法配置
-
实现贝叶斯优化高斯过程
-
实现 Metis
-
实现模拟退火
-
实现 Hyper Band
-
实现贝叶斯优化 Hyper Band
-
实现基于群体的训练
技术要求
我们将学习如何使用 DEAP 和 Microsoft NNI 实现各种超参数调整方法。为了确保你能够复制本章中的代码示例,你需要以下条件:
-
Python 3(版本 3.7 或以上)
-
已安装
pandas包(版本 1.3.4 或以上) -
已安装
NumPy包(版本 1.21.2 或以上) -
已安装
SciPy包(版本 1.7.3 或以上) -
已安装
Matplotlib包(版本 3.5.0 或以上) -
已安装
scikit-learn包(版本 1.0.1 或以上) -
已安装
DEAP包(版本 1.3) -
已安装
Hyperopt包(版本 0.1.2) -
已安装
NNI包(版本 2.7) -
已安装
PyTorch包(版本 1.10.0)
本章的所有代码示例都可以在 GitHub 上找到:github.com/PacktPublishing/Hyperparameter-Tuning-with-Python/blob/main/10_Advanced_Hyperparameter-Tuning-via-DEAP-and-NNI.ipynb。
介绍 DEAP
执行pip install deap命令。
DEAP 允许你以非常灵活的方式构建进化算法的优化步骤。以下步骤展示了如何利用 DEAP 执行任何超参数调整方法。更详细的步骤,包括代码实现,将在接下来的章节中通过各种示例给出:
-
通过
creator.create()模块定义类型类。这些类负责定义在优化步骤中将使用的对象类型。 -
定义初始化器以及超参数空间,并在
base.Toolbox()容器中注册它们。初始化器负责设置在优化步骤中将使用的对象的初始值。 -
定义算子并将它们注册在
base.Toolbox()容器中。算子指的是作为优化算法一部分需要定义的进化工具或遗传算子(见第五章)。例如,遗传算法中的选择、交叉和变异算子。 -
定义目标函数并将其注册在
base.Toolbox()容器中。 -
定义你自己的超参数调整算法函数。
-
通过调用定义在步骤 5中的函数来执行超参数调整。
-
使用找到的最佳超参数集在全部训练数据上训练模型。
-
在测试数据上测试最终训练好的模型。
类型类指的是在优化步骤中使用的对象类型。这些类型类是从 DEAP 中实现的基础类继承而来的。例如,我们可以定义我们的适应度函数类型如下:
from deap import base, creator
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
base.Fitness类是 DEAP 中实现的一个基础抽象类,可以用来定义我们自己的适应度函数类型。它期望一个weights参数来理解我们正在处理的优化问题的类型。如果是最大化问题,那么我们必须放置一个正权重,反之亦然,对于最小化问题。请注意,它期望一个元组数据结构而不是浮点数。这是因为 DEAP 还允许我们将(1.0, -1.0)作为weights参数,这意味着我们有两个目标函数,我们希望第一个最大化,第二个最小化,权重相等。
creator.create()函数负责基于基类创建一个新的类。在前面的代码中,我们使用名称“FitnessMax”创建了目标函数的类型类。此creator.create()函数至少需要两个参数:具体来说,是新创建的类的名称和基类本身。传递给此函数的其他参数将被视为新创建类的属性。除了定义目标函数的类型外,我们还可以定义将要执行的进化算法中个体的类型。以下代码展示了如何创建从 Python 内置的list数据结构继承的个体类型,该类型具有fitness属性:
creator.create("Individual", list, fitness=creator.FitnessMax)
注意,fitness属性的类型为creator.FitnessMax,这是我们之前代码中刚刚创建的类型。
DEAP 中的类型定义
在 DEAP 中有许多定义类型类的方法。虽然我们已经讨论了最直接且可以说是最常用的类型类,但你可能会遇到需要其他类型类定义的情况。有关如何在 DEAP 中定义其他类型的更多信息,请参阅官方文档(deap.readthedocs.io/en/master/tutorials/basic/part1.html)。
一旦我们完成了将在优化步骤中使用的对象类型的定义,我们现在需要使用初始化器初始化这些对象的值,并在base.Toolbox()容器中注册它们。你可以将此模块视为一个盒子或容器,其中包含初始化器和将在优化步骤中使用的其他工具。以下代码展示了我们如何为个体设置随机的初始值:
import random
from deap import tools
toolbox = base.Toolbox()
toolbox.register("individual",tools.initRepeat,creator.Individual,
random.random, n=10)
前面的代码展示了如何在base.Toolbox()容器中注册"individual"对象,其中每个个体的尺寸为10。该个体是通过重复调用random.random方法 10 次生成的。请注意,在超参数调整设置中,每个个体的10尺寸实际上指的是我们在空间中拥有的超参数数量。以下展示了通过toolbox.individual()方法调用已注册个体的输出:
[0.30752039354315985,0.2491982746819209,0.8423374678316783,0.3401579175109981,0.7699302429041264,0.046433183902334974,0.5287019598616896,0.28081693679292696,0.9562244184741888,0.0008450701833065954]
如你所见,toolbox.individual()的输出只是一个包含 10 个随机值的列表,因为我们已经定义creator.Individual从 Python 内置的list数据结构继承。此外,我们在注册个体时也调用了tools.initRepeat,通过random.random方法重复 10 次。
你现在可能想知道,如何使用这个toolbox.register()方法定义实际的超参数空间?启动一串随机值显然没有意义。我们需要知道如何定义将为每个个体配备的超参数空间。为此,我们实际上可以利用 DEAP 提供的另一个工具,即tools.InitCycle。
其中tools.initRepeat将只调用提供的函数n次,在我们之前的例子中,提供的函数是random.random。在这里,tools.InitCycle期望一个函数列表,并将这些函数调用n次。以下代码展示了如何定义将为每个个体配备的超参数空间的一个示例:
-
我们需要首先注册空间中我们拥有的每个超参数及其分布。请注意,我们也可以将所有必需的参数传递给采样分布函数的
toolbox.register()。例如,在这里,我们传递了truncnorm.rvs()方法的a=0,b=0.5,loc=0.005,scale=0.01参数:from scipy.stats import randint,truncnorm,uniform toolbox.register(“param_1”, randint.rvs, 5, 200) toolbox.register(“param_2”, truncnorm.rvs, 0, 0.5, 0.005, 0.01) toolbox.register(“param_3”, uniform.rvs, 0, 1) -
一旦我们注册了所有现有的超参数,我们可以通过使用
tools.initCycle并只进行一次重复循环来注册个体:toolbox.register(“individual”,tools.initCycle,creator.Individual, ( toolbox.param_1, toolbox.param_2, toolbox.param_3 ), n=1, )
以下展示了通过toolbox.individual()方法调用已注册个体的输出:
[172, 0.005840196235159121, 0.37250162585120816]
-
一旦我们在工具箱中注册了个体,注册一个种群就非常简单。我们只需要利用
tools.initRepeat模块并将定义的toolbox.individual作为参数传递。以下代码展示了如何一般性地注册一个种群。请注意,在这里,种群只是之前定义的五个个体的列表:toolbox.register(“population”, tools.initRepeat, list, toolbox.individual, n=5)
以下展示了调用toolbox.population()方法时的输出:
[[168, 0.009384417146554462, 0.4732188841620628],
[7, 0.009356636359759574, 0.6722125618177741],
[126, 0.00927973696427319, 0.7417964302134438],
[88, 0.008112369078803545, 0.4917555243983919],
[34, 0.008615337472475908, 0.9164442190622125]]
如前所述,base.Toolbox()容器不仅负责存储初始化器,还负责存储在优化步骤中将使用的其他工具。进化算法(如 GA)的另一个重要构建块是遗传算子。幸运的是,DEAP 已经实现了我们可以通过tools模块利用的各种遗传算子。以下代码展示了如何为 GA 注册选择、交叉和变异算子的示例(参见第五章):
# selection strategy
toolbox.register("select", tools.selTournament, tournsize=3)
# crossover strategy
toolbox.register("mate", tools.cxBlend, alpha=0.5)
# mutation strategy
toolbox.register("mutate", tools.mutPolynomialBounded, eta = 0.1, low=-2, up=2, indpb=0.15)
tools.selTournament选择策略通过在随机选择的tournsize个个体中选出最佳个体,重复NPOP次来实现,其中tournsize是参加锦标赛的个体数量,而NPOP是种群中的个体数量。tools.cxBlend交叉策略通过执行两个连续个体基因的线性组合来实现,其中线性组合的权重由alpha超参数控制。tools.mutPolynomialBounded变异策略通过将连续个体基因传递给一个预定义的多项式映射来实现。
DEAP 中的进化工具
DEAP 中实现了各种内置的进化工具,我们可以根据自己的需求使用,包括初始化器、交叉、变异、选择和迁移工具。有关实现工具的更多信息,请参阅官方文档(deap.readthedocs.io/en/master/api/tools.html)。
要将预定义的目标函数注册到工具箱中,我们只需调用相同的toolbox.register()方法并传递目标函数,如下面的代码所示:
toolbox.register("evaluate", obj_func)
在这里,obj_func是一个 Python 函数,它期望接收之前定义的individual对象。我们将在接下来的章节中看到如何创建这样的目标函数,以及如何定义我们自己的超参数调整算法函数,当我们讨论如何在 DEAP 中实现 GA 和 PSO 时。
DEAP 还允许我们在调用目标函数时利用我们的并行计算资源。为此,我们只需将multiprocessing模块注册到工具箱中,如下所示:
import multiprocessing
pool = multiprocessing.Pool()
toolbox.register("map", pool.map)
一旦我们注册了multiprocessing模块,我们就可以在调用目标函数时简单地应用它,如下面的代码所示:
fitnesses = toolbox.map(toolbox.evaluate, individual)
在本节中,我们讨论了 DEAP 包及其构建块。你可能想知道如何使用 DEAP 提供的所有构建块构建一个真实的超参数调整方法。不用担心;在接下来的两个章节中,我们将学习如何利用所有讨论的构建块使用 GA 和 PSO 方法进行超参数调整。
实现遗传算法
GA 是启发式搜索超参数调整组(见第五章)的变体之一,可以通过 DEAP 包实现。为了展示我们如何使用 DEAP 包实现 GA,让我们使用随机森林分类器模型和与第七章中示例相同的数据。本例中使用的数据库是 Kaggle 上提供的Banking Dataset – Marketing Targets数据库(www.kaggle.com/datasets/prakharrathi25/banking-dataset-marketing-targets)。
目标变量由两个类别组成,yes或no,表示银行客户是否已订阅定期存款。因此,在这个数据集上训练机器学习模型的目的是确定客户是否可能想要订阅定期存款。在数据中提供的 16 个特征中,有 7 个数值特征和 9 个分类特征。至于目标类分布,训练和测试数据集中都有 12%是yes,88%是no。有关数据的更详细信息,请参阅第七章。
在执行 GA 之前,让我们看看具有默认超参数值的随机森林分类器是如何工作的。如 第七章 所示,我们在测试集上评估具有默认超参数值的随机森林分类器时,F1 分数大约为 0.436。请注意,我们仍在使用如 第七章 中解释的相同的 scikit-learn 管道定义来训练和评估随机森林分类器。
以下代码展示了如何使用 DEAP 包实现 GA。您可以在 技术要求 部分提到的 GitHub 仓库中找到更详细的代码:
-
通过
creator.create()模块定义 GA 参数和类型类:# GA Parameters NPOP = 50 #population size NGEN = 15 #number of trials CXPB = 0.5 #cross-over probability MUTPB = 0.2 #mutation probability
设置随机种子以实现可重复性:
import random
random.seed(1)
定义我们的适应度函数类型。在这里,我们正在处理一个最大化问题和一个单一目标函数,因此我们设置 weights=(1.0,):
from deap import creator, base
creator.create(“FitnessMax”, base.Fitness, weights=(1.0,))
定义从 Python 内置 list 数据结构继承的个体类型,该类型具有 fitness 作为其属性:
creator.create(“Individual”, list, fitness=creator.FitnessMax)
- 定义初始化器以及超参数空间并将它们注册在
base.Toolbox()容器中。
初始化工具箱:
toolbox = base.Toolbox()
定义超参数的命名:
PARAM_NAMES = [“model__n_estimators”,”model__criterion”,
“model__class_weight”,”model__min_samples_split”
注册空间中的每个超参数及其分布:
from scipy.stats import randint,truncnorm
toolbox.register(“model__n_estimators”, randint.rvs, 5, 200)
toolbox.register(“model__criterion”, random.choice, [“gini”, “entropy”])
toolbox.register(“model__class_weight”, random.choice, [“balanced”,”balanced_subsample”])
toolbox.register(“model__min_samples_split”, truncnorm.rvs, 0, 0.5, 0.005, 0.01)
通过使用 tools.initCycle 仅进行一次循环重复来注册个体:
from deap import tools
toolbox.register(
“individual”,
tools.initCycle,
creator.Individual,
(
toolbox.model__n_estimators,
toolbox.model__criterion,
toolbox.model__class_weight,
toolbox.model__min_samples_split,
),
)
注册种群:
toolbox.register(“population”, tools.initRepeat, list, toolbox.individual)
- 定义操作符并将它们注册在
base.Toolbox()容器中。
注册选择策略:
toolbox.register(“select”, tools.selTournament, tournsize=3)
注册交叉策略:
toolbox.register(“mate”, tools.cxUniform, indpb=CXPB)
定义一个自定义变异策略。请注意,DEAP 中实现的全部变异策略实际上并不适合超参数调整目的,因为它们只能用于浮点或二进制值,而大多数情况下,我们的超参数空间将是一组真实和离散超参数的组合。以下函数展示了如何实现这样的自定义变异策略。您可以遵循相同的结构来满足您的需求:
def mutPolynomialBoundedMix(individual, eta, low, up, is_int, indpb, discrete_params):
for i in range(len(individual)):
if discrete_params[i]:
if random.random() < indpb:
individual[i] = random.choice(discrete_params[i])
else:
individual[i] = tools.mutPolynomialBounded([individual[i]],
eta[i], low[i], up[i], indpb)[0][0]
if is_int[i]:
individual[i] = int(individual[i])
return individual,
注册自定义变异策略:
toolbox.register(“mutate”, mutPolynomialBoundedMix,
eta = [0.1,None,None,0.1],
low = [5,None,None,0],
up = [200,None,None,1],
is_int = [True,False,False,False],
indpb=MUTPB,
discrete_params=[[],[“gini”, “entropy”],[“balanced”,”balanced_subsample”],[]]
)
-
定义目标函数并将其注册在
base.Toolbox()容器中:def evaluate(individual): # convert list of parameter values into dictionary of kwargs strategy_params = {k: v for k, v in zip(PARAM_NAMES, individual)} if strategy_params['model__min_samples_split'] > 1 or strategy_params['model__min_samples_split'] <= 0: return [-np.inf] tuned_pipe = clone(pipe).set_params(**strategy_params) return [np.mean(cross_val_score(tuned_pipe,X_train_full, y_train, cv=5, scoring='f1',))]
注册目标函数:
toolbox.register(“evaluate”, evaluate)
-
定义具有并行处理的遗传算法:
import multiprocessing import numpy as np
注册 multiprocessing 模块:
pool = multiprocessing.Pool(16)
toolbox.register(“map”, pool.map)
定义空数组以存储每个试验中目标函数得分的最佳值和平均值:
mean = np.ndarray(NGEN)
best = np.ndarray(NGEN)
定义一个 HallOfFame 类,该类负责在种群中存储最新的最佳个体(超参数集):
hall_of_fame = tools.HallOfFame(maxsize=3)
定义初始种群:
pop = toolbox.population(n=NPOP)
开始 GA 迭代:
for g in range(NGEN):
选择下一代个体/孩子/后代。
offspring = toolbox.select(pop, len(pop))
复制选定的个体。
offspring = list(map(toolbox.clone, offspring))
在后代上应用交叉:
for child1, child2 in zip(offspring[::2], offspring[1::2]):
if random.random() < CXPB:
toolbox.mate(child1, child2)
del child1.fitness.values
del child2.fitness.values
在后代上应用变异。
for mutant in offspring:
if random.random() < MUTPB:
toolbox.mutate(mutant)
del mutant.fitness.values
评估具有无效适应度的个体:
invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)
for ind, fit in zip(invalid_ind, fitnesses):
ind.fitness.values = fit
种群完全由后代取代。
pop[:] = offspring
hall_of_fame.update(pop)
fitnesses = [
ind.fitness.values[0] for ind in pop if not np.isinf(ind.fitness.values[0])
]
mean[g] = np.mean(fitnesses)
best[g] = np.max(fitnesses)
-
通过运行定义的算法在步骤 5中执行超参数调整。在运行 GA 之后,我们可以根据以下代码获取最佳超参数集:
params = {} for idx_hof, param_name in enumerate(PARAM_NAMES): params[param_name] = hall_of_fame[0][idx_hof] print(params)
根据前面的代码,我们得到以下结果:
{'model__n_estimators': 101,
'model__criterion': 'entropy',
'model__class_weight': 'balanced',
'model__min_samples_split': 0.0007106340458649385}
我们也可以根据以下代码绘制试验历史或收敛图:
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()
fig, ax = plt.subplots(sharex=True, figsize=(8, 6))
sns.lineplot(x=range(NGEN), y=mean, ax=ax, label=”Average Fitness Score”)
sns.lineplot(x=range(NGEN), y=best, ax=ax, label=”Best Fitness Score”)
ax.set_title(“Fitness Score”,size=20)
ax.set_xticks(range(NGEN))
ax.set_xlabel(“Iteration”)
plt.tight_layout()
plt.show()
根据前面的代码,以下图生成。如图所示,目标函数得分或适应度得分在整个试验次数中都在增加,因为种群被更新为改进的个体:
![图 10.1 – 遗传算法收敛图]
![img/B18753_10_001.jpg]
图 10.1 – 遗传算法收敛图
-
使用找到的最佳超参数集在全部训练数据上训练模型:
from sklearn.base import clone tuned_pipe = clone(pipe).set_params(**params) tuned_pipe.fit(X_train_full,y_train) -
在测试数据上测试最终训练的模型:
y_pred = tuned_pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
根据前面的代码,当使用最佳超参数集在测试集上测试我们最终的训练随机森林模型时,F1 分数大约为0.608。
在本节中,我们学习了如何使用 DEAP 包实现遗传算法(GA),从定义必要的对象开始,到使用并行处理和自定义变异策略定义 GA 过程,再到绘制试验历史和测试测试集中最佳超参数集。在下一节中,我们将学习如何使用 DEAP 包实现 PSO 超参数调整方法。
实现粒子群优化
PSO 也是启发式搜索超参数调整组(见第五章)的一种变体,可以使用 DEAP 包实现。我们仍将使用上一节中的相同示例来查看我们如何使用 DEAP 包实现 PSO。
以下代码显示了如何使用 DEAP 包实现 PSO。你可以在技术要求部分提到的 GitHub 仓库中找到更详细的代码:
-
通过
creator.create()模块定义 PSO 参数和类型类:N = 50 #swarm size w = 0.5 #inertia weight coefficient c1 = 0.3 #cognitive coefficient c2 = 0.5 #social coefficient num_trials = 15 #number of trials
设置随机种子以实现可重复性:
import random
random.seed(1)
定义我们的适应度函数的类型。在这里,我们正在处理一个最大化问题和一个单一目标函数,这就是为什么我们设置weights=(1.0,):
from deap import creator, base
creator.create(“FitnessMax”, base.Fitness, weights=(1.0,))
定义从 Python 内置的list数据结构继承的粒子类型,该结构具有fitness、speed、smin、smax和best作为其属性。这些属性将在稍后更新每个粒子的位置时被利用(见第五章):
creator.create(“Particle”, list, fitness=creator.FitnessMax,
speed=list, smin=list, smax=list, best=None)
- 定义初始化器以及超参数空间,并在
base.Toolbox()容器中注册它们。
初始化工具箱:
toolbox = base.Toolbox()
定义超参数的命名:
PARAM_NAMES = [“model__n_estimators”,”model__criterion”,
“model__class_weight”,”model__min_samples_split”
在空间中注册我们拥有的每个超参数及其分布。记住,PSO 只与数值类型超参数一起工作。这就是为什么我们将"model__criterion"和"model__class_weight"超参数编码为整数:
from scipy.stats import randint,truncnorm
toolbox.register(“model__n_estimators”, randint.rvs, 5, 200)
toolbox.register(“model__criterion”, random.choice, [0,1])
toolbox.register(“model__class_weight”, random.choice, [0,1])
toolbox.register(“model__min_samples_split”, truncnorm.rvs, 0, 0.5, 0.005, 0.01)
通过使用tools.initCycle仅进行一次重复循环来注册个体。注意,我们还需要将speed、smin和smax值分配给每个个体。为此,让我们定义一个名为generate的函数:
from deap import tools
def generate(speed_bound):
part = tools.initCycle(creator.Particle,
[toolbox.model__n_estimators,
toolbox.model__criterion,
toolbox.model__class_weight,
toolbox.model__min_samples_split,
]
)
part.speed = [random.uniform(speed_bound[i]['smin'], speed_bound[i]['smax']) for i in range(len(part))]
part.smin = [speed_bound[i]['smin'] for i in range(len(part))]
part.smax = [speed_bound[i]['smax'] for i in range(len(part))]
return part
通过使用tools.initCycle仅进行一次重复循环来注册个体:
toolbox.register(“particle”, generate,
speed_bound=[{'smin': -2.5,'smax': 2.5},
{'smin': -1,'smax': 1},
{'smin': -1,'smax': 1},
{'smin': -0.001,'smax': 0.001}])
注册种群:
toolbox.register(“population”, tools.initRepeat, list, toolbox.particle)
-
定义操作符并将它们注册到
base.Toolbox()容器中。PSO 中的主要操作符是粒子的位置更新操作符,该操作符在updateParticle函数中定义如下:import operator import math def updateParticle(part, best, c1, c2, w, is_int): w = [w for _ in range(len(part))] u1 = (random.uniform(0, 1)*c1 for _ in range(len(part))) u2 = (random.uniform(0, 1)*c2 for _ in range(len(part))) v_u1 = map(operator.mul, u1, map(operator.sub, part.best, part)) v_u2 = map(operator.mul, u2, map(operator.sub, best, part)) part.speed = list(map(operator.add, map(operator.mul, w, part.speed), map(operator.add, v_u1, v_u2))) for i, speed in enumerate(part.speed): if abs(speed) < part.smin[i]: part.speed[i] = math.copysign(part.smin[i], speed) elif abs(speed) > part.smax[i]: part.speed[i] = math.copysign(part.smax[i], speed) part[:] = list(map(operator.add, part, part.speed)) for i, pos in enumerate(part): if is_int[i]: part[i] = int(pos)
注册操作符。注意,is_int属性负责标记哪个超参数具有整数值类型:
toolbox.register(“update”, updateParticle, c1=c1, c2=c2, w=w,
is_int=[True,True,True,False]
)
-
定义目标函数并将其注册到
base.Toolbox()容器中。注意,我们还在目标函数中解码了"model__criterion"和"model__class_weight"超参数:def evaluate(particle): # convert list of parameter values into dictionary of kwargs strategy_params = {k: v for k, v in zip(PARAM_NAMES, particle)} strategy_params[“model__criterion”] = “gini” if strategy_params[“model__criterion”]==0 else “entropy” strategy_params[“model__class_weight”] = “balanced” if strategy_params[“model__class_weight”]==0 else “balanced_subsample” if strategy_params['model__min_samples_split'] > 1 or strategy_params['model__min_samples_split'] <= 0: return [-np.inf] tuned_pipe = clone(pipe).set_params(**strategy_params) return [np.mean(cross_val_score(tuned_pipe,X_train_full, y_train, cv=5, scoring='f1',))]
注册目标函数:
toolbox.register(“evaluate”, evaluate)
-
定义具有并行处理的 PSO:
import multiprocessing import numpy as np
注册multiprocessing模块:
pool = multiprocessing.Pool(16)
toolbox.register(“map”, pool.map)
定义空数组以存储每个试验中目标函数分数的最佳和平均值:
mean_arr = np.ndarray(num_trials)
best_arr = np.ndarray(num_trials)
定义一个HallOfFame类,该类负责存储种群中的最新最佳个体(超参数集):
hall_of_fame = tools.HallOfFame(maxsize=3)
定义初始种群:
pop = toolbox.population(n=NPOP)
开始 PSO 迭代:
best = None
for g in range(num_trials):
fitnesses = toolbox.map(toolbox.evaluate, pop)
for part, fit in zip(pop, fitnesses):
part.fitness.values = fit
if not part.best or part.fitness.values > part.best.fitness.values:
part.best = creator.Particle(part)
part.best.fitness.values = part.fitness.values
if not best or part.fitness.values > best.fitness.values:
best = creator.Particle(part)
best.fitness.values = part.fitness.values
for part in pop:
toolbox.update(part, best)
hall_of_fame.update(pop)
fitnesses = [
ind.fitness.values[0] for ind in pop if not np.isinf(ind.fitness.values[0])
]
mean_arr[g] = np.mean(fitnesses)
best_arr[g] = np.max(fitnesses)
-
通过运行第 5 步中定义的算法来执行超参数调整。在运行 PSO 之后,我们可以根据以下代码获取最佳超参数集。注意,在将它们传递给最终模型之前,我们需要解码
"model__criterion"和"model__class_weight"超参数:params = {} for idx_hof, param_name in enumerate(PARAM_NAMES): if param_name == “model__criterion”: params[param_name] = “gini” if hall_of_fame[0][idx_hof]==0 else “entropy” elif param_name == “model__class_weight”: params[param_name] = “balanced” if hall_of_fame[0][idx_hof]==0 else “balanced_subsample” else: params[param_name] = hall_of_fame[0][idx_hof] print(params)
根据前面的代码,我们得到以下结果:
{'model__n_estimators': 75,
'model__criterion': 'entropy',
'model__class_weight': 'balanced',
'model__min_samples_split': 0.0037241038302412493}
-
使用找到的最佳超参数集在全部训练数据上训练模型:
from sklearn.base import clone tuned_pipe = clone(pipe).set_params(**params) tuned_pipe.fit(X_train_full,y_train) -
在测试数据上测试最终训练好的模型:
y_pred = tuned_pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
根据前面的代码,我们在测试最终训练好的随机森林模型时,在测试集上获得了大约0.569的 F1 分数,该模型使用了最佳的超参数集。
在本节中,我们学习了如何使用 DEAP 包实现 PSO,从定义必要的对象开始,将分类超参数编码为整数,并使用并行处理定义优化过程,直到在测试集上测试最佳超参数集。在下一节中,我们将开始学习另一个名为 NNI 的超参数调整包,该包由微软开发。
介绍微软 NNI
pip install nni命令。
虽然 NNI 指的是神经网络智能,但它实际上支持包括但不限于 scikit-learn、XGBoost、LightGBM、PyTorch、TensorFlow、Caffe2 和 MXNet 在内的多个机器学习框架。
NNI 实现了许多超参数调优方法;其中一些是内置的,而另一些则是从其他包如Hyperopt(见第八章)和SMAC3中封装的。在这里,NNI 中的超参数调优方法被称为调优器。由于调优器种类繁多,我们不会讨论 NNI 中实现的所有调优器。我们只会讨论在第三章至第六章中讨论过的调优器。除了调优器之外,一些超参数调优方法,如 Hyper Band 和 BOHB,在 NNI 中被视为顾问。
NNI 中的可用调优器
要查看 NNI 中所有可用调优器的详细信息,请参阅官方文档页面(nni.readthedocs.io/en/stable/hpo/tuners.html)。
与我们之前讨论的其他超参数调优包不同,在 NNI 中,我们必须准备一个包含模型定义的 Python 脚本,然后才能从笔记本中运行超参数调优过程。此外,NNI 还允许我们从命令行工具中运行超参数调优实验,在那里我们需要定义几个其他附加文件来存储超参数空间信息和其他配置。
以下步骤展示了如何使用纯 Python 代码通过 NNI 执行任何超参数调优过程:
-
在脚本中准备要调优的模型,例如,
model.py。此脚本应包括模型架构定义、数据集加载函数、训练函数和测试函数。它还必须包括三个 NNI API 调用,如下所示:-
nni.get_next_parameter()负责收集特定试验中要评估的超参数。 -
nni.report_intermediate_result()负责在每次训练迭代(epoch 或步骤)中报告评估指标。请注意,此 API 调用不是强制的;如果您无法从您的机器学习框架中获取中间评估指标,则不需要此 API 调用。 -
nni.report_final_result()负责在训练过程完成后报告最终评估指标分数。
-
-
定义超参数空间。NNI 期望超参数空间以 Python 字典的形式存在,其中第一级键存储超参数的名称。第二级键存储采样分布的类型和超参数值范围。以下是如何以预期格式定义超参数空间的示例:
hyperparameter_space = { ' n_estimators ': {'_type': 'randint', '_value': [5, 200]}, ' criterion ': {'_type': 'choice', '_value': ['gini', 'entropy']}, ' min_samples_split ': {'_type': 'uniform', '_value': [0, 0.1]}, }
关于 NNI 的更多信息
关于 NNI 支持的采样分布的更多信息,请参阅官方文档(nni.readthedocs.io/en/latest/hpo/search_space.html)。
- 接下来,我们需要通过
Experiment类设置实验配置。以下展示了在我们可以运行超参数调整过程之前设置几个配置的步骤。
加载Experiment类。在这里,我们使用的是'local'实验模式,这意味着所有训练和超参数调整过程都将在我们的本地计算机上完成。NNI 允许我们在各种平台上运行训练过程,包括但不限于Azure Machine Learning(AML)、Kubeflow 和 OpenAPI。更多信息,请参阅官方文档(nni.readthedocs.io/en/latest/reference/experiment_config.html):
from nni.experiment import Experiment
experiment = Experiment('local')
设置试验代码配置。在这里,我们需要指定运行在步骤 1中定义的脚本的命令和脚本的相对路径。以下展示了如何设置试验代码配置的示例:
experiment.config.trial_command = 'python model.py'
experiment.config.trial_code_directory = '.'
设置超参数空间配置。要设置超参数空间配置,我们只需将定义的超参数空间传递到步骤 2。以下代码展示了如何进行操作:
experiment.config.search_space = hyperparameter_space
设置要使用的超参数调整算法。以下展示了如何将 TPE 作为超参数调整算法应用于最大化问题的示例:
experiment.config.tuner.name = 'TPE'
experiment.config.tuner.class_args['optimize_mode'] = 'maximize'
设置试验次数和并发进程数。NNI 允许我们设置在单次运行中同时评估多少个超参数集。以下代码展示了如何将试验次数设置为 50,这意味着在特定时间将同时评估五个超参数集:
experiment.config.max_trial_number = 50
experiment.config.trial_concurrency = 5
值得注意的是,NNI 还允许你根据时间长度而不是试验次数来定义停止标准。以下代码展示了你如何将实验时间限制为 1 小时:
experiment.config.max_experiment_duration = '1h'
如果你没有提供max_trial_number和max_experiment_duration两个参数,那么实验将永远运行,直到你通过Ctrl + C命令强制停止它。
-
运行超参数调整实验。要运行实验,我们可以在
Experiment类上简单地调用run方法。在这里,我们还需要选择要使用的端口。我们可以通过启动的 Web 门户查看实验状态和各种有趣的统计数据。以下代码展示了如何在local模式下在端口8080上运行实验,这意味着你可以在http://localhost:8080上打开 Web 门户:experiment.run(8080)
run方法有两个可用的布尔参数,即wait_completion和debug。当我们设置wait_completion=True时,我们无法在实验完成或发现错误之前运行笔记本中的其他单元格。debug参数使我们能够选择是否以调试模式启动实验。
-
使用找到的最佳超参数集在全部训练数据上训练模型。
-
在测试数据上测试最终训练好的模型。
NNI Web Portal
关于 Web 门户中可用的更多功能,请参阅官方文档(nni.readthedocs.io/en/stable/experiment/web_portal/web_portal.html)。注意,我们将在第十三章中更详细地讨论 Web 门户,跟踪超参数调整实验。
如果你更喜欢使用命令行工具,以下步骤展示了如何使用命令行工具、JSON 和 YAML 配置文件执行任何超参数调整流程:
-
在脚本中准备要调整的模型。这一步骤与使用纯 Python 代码进行 NNI 超参数调整的前一个流程完全相同。
-
定义超参数空间。超参数空间的预期格式与使用纯 Python 代码进行任何超参数调整流程的流程完全相同。然而,在这里,我们需要将 Python 字典存储在一个 JSON 文件中,例如,
hyperparameter_space.json。 -
通过
config.yaml文件设置实验配置。需要设置的配置基本上与使用纯 Python 代码的 NNI 流程相同。然而,这里不是通过 Python 类来配置实验,而是将所有配置细节存储在一个单独的 YAML 文件中。以下是一个 YAML 文件示例:searchSpaceFile: hyperparameter_space.json trial_command: python model.py trial_code_directory: . trial_concurrency: 5 max_trial_number: 50 tuner: name: TPE class_args: optimize_mode: maximize training_service: platform: local -
运行超参数调整实验。要运行实验,我们可以简单地调用
nnictl create命令。以下代码展示了如何使用该命令在local的8080端口上运行实验:nnictl create --config config.yaml --port 8080
实验完成后,你可以通过nnictl stop命令轻松停止进程。
-
使用找到的最佳超参数集在全部训练数据上训练模型。
-
在测试数据上测试最终训练好的模型。
各种机器学习框架的示例
你可以在官方文档中找到使用你喜欢的机器学习框架通过 NNI 执行超参数调整的所有示例(github.com/microsoft/nni/tree/master/examples/trials)。
scikit-nni
此外,还有一个名为scikit-nni的包,它将自动生成所需的config.yml和search-space.json,并根据你的自定义需求构建scikit-learn管道。有关此包的更多信息,请参阅官方仓库(github.com/ksachdeva/scikit-nni)。
除了调优器或超参数调优算法之外,NNI 还提供了 nni.report_intermediate_result() API 调用。NNI 中只有两个内置评估器:中值停止 和 曲线拟合。第一个评估器将在任何步骤中,只要某个超参数集的表现不如中值,就会停止实验。后者评估器将在学习曲线可能收敛到次优结果时停止实验。
在 NNI 中设置评估器非常简单。您只需在 Experiment 类或 config.yaml 文件中添加配置即可。以下代码展示了如何在 Experiment 类上配置中值停止评估器:
experiment.config.assessor.name = 'Medianstop'
NNI 中的自定义算法
NNI 还允许我们定义自己的自定义调优器和评估器。为此,您需要继承基类 Tuner 或 Assessor,编写几个必需的函数,并在 Experiment 类或 config.yaml 文件中添加更多详细信息。有关如何定义自己的自定义调优器和评估器的更多信息,请参阅官方文档(nni.readthedocs.io/en/stable/hpo/custom_algorithm.html)。
在本节中,我们讨论了 NNI 包及其如何进行一般性的超参数调优实验。在接下来的章节中,我们将学习如何使用 NNI 实现各种超参数调优算法。
实现网格搜索
网格搜索是 NNI 包可以实现的穷举搜索超参数调优组(参见 第三章)的一种变体。为了向您展示我们如何使用 NNI 包实现网格搜索,我们将使用与上一节示例中相同的数据和管道。然而,在这里,我们将定义一个新的超参数空间,因为 NNI 只支持有限类型的采样分布。
以下代码展示了如何使用 NNI 包实现网格搜索。在这里,我们将使用 NNI 命令行工具(nnictl)而不是纯 Python 代码。更详细的代码可以在 技术要求 部分提到的 GitHub 仓库中找到:
- 在脚本中准备要调优的模型。在这里,我们将脚本命名为
model.py。该脚本中定义了几个函数,包括load_data、get_default_parameters、get_model和run。
load_data 函数加载原始数据并将其分为训练数据和测试数据。此外,它还负责返回数值和分类列名的列表:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from pathlib import Path
def load_data():
df = pd.read_csv(f”{Path(__file__).parent.parent}/train.csv”,sep=”;”)
#Convert the target variable to integer
df['y'] = df['y'].map({'yes':1,'no':0})
#Split full data into train and test data
df_train, df_test = train_test_split(df, test_size=0.1, random_state=0)
#Get list of categorical and numerical features
numerical_feats = list(df_train.drop(columns='y').select_dtypes(include=np.number).columns)
categorical_feats = list(df_train.drop(columns='y').select_dtypes(exclude=np.number).columns)
X_train = df_train.drop(columns=['y'])
y_train = df_train['y']
X_test = df_test.drop(columns=['y'])
y_test = df_test['y']
return X_train, X_test, y_train, y_test, numerical_feats, categorical_feats
get_default_parameters 函数返回实验中使用的默认超参数值:
def get_default_parameters():
params = {
'model__n_estimators': 5,
'model__criterion': 'gini',
'model__class_weight': 'balanced',
'model__min_samples_split': 0.01,
}
return params
get_model 函数定义了在此示例中使用的 sklearn 管道:
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
def get_model(PARAMS, numerical_feats, categorical_feats):
为数值特征启动归一化预处理。
numeric_preprocessor = StandardScaler()
为分类特征启动 One-Hot-Encoding 预处理。
categorical_preprocessor = OneHotEncoder(handle_unknown=”ignore”)
创建 ColumnTransformer 类以将每个预处理程序委托给相应的特征。
preprocessor = ColumnTransformer(
transformers=[
(“num”, numeric_preprocessor, numerical_feats),
(“cat”, categorical_preprocessor, categorical_feats),
]
)
创建预处理器和模型的 Pipeline。
pipe = Pipeline(
steps=[(“preprocessor”, preprocessor),
(“model”, RandomForestClassifier(random_state=0))]
)
设置超参数值。
pipe = pipe.set_params(**PARAMS)
return pipe
run 函数负责训练模型并获取交叉验证分数:
import nni
import logging
from sklearn.model_selection import cross_val_score
LOG = logging.getLogger('nni_sklearn')
def run(X_train, y_train, model):
model.fit(X_train, y_train)
score = np.mean(cross_val_score(model,X_train, y_train,
cv=5, scoring='f1')
)
LOG.debug('score: %s', score)
nni.report_final_result(score)
最后,我们可以在同一脚本中调用这些函数:
if __name__ == '__main__':
X_train, _, y_train, _, numerical_feats, categorical_feats = load_data()
try:
# get parameters from tuner
RECEIVED_PARAMS = nni.get_next_parameter()
LOG.debug(RECEIVED_PARAMS)
PARAMS = get_default_parameters()
PARAMS.update(RECEIVED_PARAMS)
LOG.debug(PARAMS)
model = get_model(PARAMS, numerical_feats, categorical_feats)
run(X_train, y_train, model)
except Exception as exception:
LOG.exception(exception)
raise
-
在名为
hyperparameter_space.json的 JSON 文件中定义超参数空间:{“model__n_estimators”: {“_type”: “randint”, “_value”: [5, 200]}, “model__criterion”: {“_type”: “choice”, “_value”: [“gini”, “entropy”]}, “model__class_weight”: {“_type”: “choice”, “_value”: [“balanced”,”balanced_subsample”]}, “model__min_samples_split”: {“_type”: “uniform”, “_value”: [0, 0.1]}} -
通过
config.yaml文件设置实验配置:searchSpaceFile: hyperparameter_space.json experimentName: nni_sklearn trial_command: python '/mnt/c/Users/Louis\ Owen/Desktop/Packt/Hyperparameter-Tuning-with-Python/nni/model.py' trial_code_directory: . trial_concurrency: 10 max_trial_number: 100 maxExperimentDuration: 1h tuner: name: GridSearch training_service: platform: local -
运行超参数调优实验。我们可以通过启动的网络门户查看实验状态和各种有趣的统计数据。以下代码展示了如何在
local模式下通过端口8080运行实验,这意味着您可以在http://localhost:8080上打开网络门户:nnictl create --config config.yaml --port 8080 -
使用找到的最佳超参数集在全部训练数据上训练模型。要获取最佳超参数集,您可以访问网络门户并在 概览 选项卡中查看。
根据在 Top trials 选项卡中显示的实验结果,以下是从实验中找到的最佳超参数值。注意,我们将在 第十三章 跟踪超参数调优实验 中更详细地讨论网络门户:
best_parameters = {
“model__n_estimators”: 27,
“model__criterion”: “entropy”,
“model__class_weight”: “balanced_subsample”,
“model__min_samples_split”: 0.05
}
我们现在可以在全部训练数据上训练模型:
from sklearn.base import clone
tuned_pipe = clone(pipe).set_params(**best_parameters)
# Fit the pipeline on train data
tuned_pipe.fit(X_train_full,y_train)
-
在测试数据上测试最终训练好的模型:
y_pred = tuned_pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
根据前面的代码,当我们在测试集上使用最佳超参数集测试我们最终的训练 Random Forest 模型时,F1 分数大约为 0.517。
在本节中,我们学习了如何通过 nnictl 使用 NNI 包实现网格搜索。在下一节中,我们将学习如何通过纯 Python 代码使用 NNI 实现 Random Search。
实现 Random Search
随机搜索是穷举搜索超参数调优组(见 第三章)的一种变体,NNI 包可以实施。让我们使用与上一节示例中相同的数据、管道和超参数空间,向您展示如何使用纯 Python 代码通过 NNI 实现 Random Search。
以下代码展示了如何使用 NNI 包实现随机搜索。在这里,我们将使用纯 Python 代码而不是像上一节那样使用 nnictl。您可以在 技术要求 部分提到的 GitHub 仓库中找到更详细的代码:
-
在脚本中准备要调优的模型。我们将使用与上一节相同的
model.py脚本。 -
以 Python 字典的形式定义超参数空间:
hyperparameter_space = { 'model__n_estimators': {'_type': 'randint', '_value': [5, 200]}, 'model__criterion': {'_type': 'choice', '_value': ['gini', 'entropy']}, 'model__class_weight': {'_type': 'choice', '_value': [“balanced”,”balanced_subsample”]}, 'model__min_samples_split': {'_type': 'uniform', '_value': [0, 0.1]}, } -
通过
Experiment类设置实验配置。注意,对于随机搜索调优器,只有一个参数,即随机的seed参数:experiment = Experiment('local') experiment.config.experiment_name = 'nni_sklearn_random_search' experiment.config.tuner.name = 'Random' experiment.config.tuner.class_args['seed'] = 0 # Boilerplate code experiment.config.trial_command = “python '/mnt/c/Users/Louis\ Owen/Desktop/Packt/Hyperparameter-Tuning-with-Python/nni/model.py'” experiment.config.trial_code_directory = '.' experiment.config.search_space = hyperparameter_space experiment.config.max_trial_number = 100 experiment.config.trial_concurrency = 10 experiment.config.max_experiment_duration = '1h' -
运行超参数调优实验:
experiment.run(8080, wait_completion = True, debug = False) -
使用找到的最佳超参数集在全部训练数据上训练模型。
获取最佳超参数集:
best_trial = sorted(experiment.export_data(),key = lambda x: x.value, reverse = True)[0]
print(best_trial.parameter)
-
根据前面的代码,我们得到了以下结果:
{'model__n_estimators': 194, 'model__criterion': 'entropy', 'model__class_weight': 'balanced_subsample', 'model__min_samples_split': 0.0014706304965369289}
我们现在可以在全部训练数据上训练模型:
from sklearn.base import clone
tuned_pipe = clone(pipe).set_params(**best_trial.parameter)
# Fit the pipeline on train data
tuned_pipe.fit(X_train_full,y_train)
-
在测试数据上测试最终训练好的模型:
y_pred = tuned_pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
根据前面的代码,当使用最佳超参数集在测试集上测试我们最终训练的随机森林模型时,F1 分数大约为0.597。
在本节中,我们学习了如何使用纯 Python 代码通过 NNI 实现随机搜索。在下一节中,我们将学习如何通过纯 Python 代码使用 NNI 实现树结构帕累托估计器。
实现树结构帕累托估计器
树结构帕累托估计器(TPEs)是贝叶斯优化超参数调整组(见第四章)中 NNI 包可以实现的变体之一。让我们使用与上一节示例中相同的数据、管道和超参数空间,使用纯 Python 代码实现 TPE 与 NNI。
以下代码展示了如何使用纯 Python 代码通过 NNI 包实现 TPE。你可以在技术要求节中提到的 GitHub 仓库中找到更详细的代码:
-
在脚本中准备要调优的模型。我们将使用与上一节相同的
model.py脚本。 -
以 Python 字典的形式定义超参数空间。我们将使用与上一节相同的超参数空间。
-
通过
Experiment类设置实验配置。请注意,TPE 调整器有三个参数:optimize_mode、seed和tpe_args。有关 TPE 调整器参数的更多信息,请参阅官方文档页面(nni.readthedocs.io/en/stable/reference/hpo.html#tpe-tuner):experiment = Experiment('local') experiment.config.experiment_name = 'nni_sklearn_tpe' experiment.config.tuner.name = 'TPE' experiment.config.tuner.class_args = {'optimize_mode': 'maximize', 'seed': 0} # Boilerplate code # same with previous section -
运行超参数调整实验:
experiment.run(8080, wait_completion = True, debug = False) -
使用找到的最佳超参数集在全部训练数据上训练模型。
获取最佳超参数集:
best_trial = sorted(experiment.export_data(),key = lambda x: x.value, reverse = True)[0]
print(best_trial.parameter)
根据前面的代码,我们得到以下结果:
{'model__n_estimators': 195, 'model__criterion': 'entropy', 'model__class_weight': 'balanced_subsample', 'model__min_samples_split': 0.0006636374717157983}
我们现在可以在全部训练数据上训练模型:
from sklearn.base import clone
tuned_pipe = clone(pipe).set_params(**best_trial.parameter)
在训练数据上拟合管道。
tuned_pipe.fit(X_train_full,y_train)
-
在测试数据上测试最终训练的模型:
y_pred = tuned_pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
根据前面的代码,当使用最佳超参数集在测试集上测试我们最终训练的随机森林模型时,F1 分数大约为0.618。
在本节中,我们学习了如何使用纯 Python 代码通过 NNI 实现 TPE。在下一节中,我们将学习如何通过纯 Python 代码使用 NNI 实现序列模型算法配置。
实现序列模型算法配置
pip install "nni[SMAC]". 让我们使用与上一节示例中相同的数据、管道和超参数空间,使用纯 Python 代码实现 SMAC 与 NNI。
以下代码展示了如何使用纯 Python 代码通过 NNI 包实现 SMAC。你可以在技术要求节中提到的 GitHub 仓库中找到更详细的代码:
-
在脚本中准备要调优的模型。我们将使用与上一节相同的
model.py脚本。 -
以 Python 字典的形式定义超参数空间。我们将使用与上一节相同的超参数空间。
-
通过
Experiment类设置实验配置。请注意,SMAC 调优器有两个参数:optimize_mode和config_dedup。有关 SMAC 调优器参数的更多信息,请参阅官方文档页面(nni.readthedocs.io/en/stable/reference/hpo.html#smac-tuner):experiment = Experiment('local') experiment.config.experiment_name = 'nni_sklearn_smac' experiment.config.tuner.name = 'SMAC' experiment.config.tuner.class_args['optimize_mode'] = 'maximize' # Boilerplate code # same with previous section -
运行超参数调优实验:
experiment.run(8080, wait_completion = True, debug = False) -
使用找到的最佳超参数集在全部训练数据上训练模型。
获取最佳的超参数组合:
best_trial = sorted(experiment.export_data(),key = lambda x: x.value, reverse = True)[0]
print(best_trial.parameter)
根据前面的代码,我们得到了以下结果:
{'model__class_weight': 'balanced', 'model__criterion': 'entropy', 'model__min_samples_split': 0.0005502416428725066, 'model__n_estimators': 199}
我们现在可以在全部训练数据上训练模型:
from sklearn.base import clone
tuned_pipe = clone(pipe).set_params(**best_trial.parameter)
# Fit the pipeline on train data
tuned_pipe.fit(X_train_full,y_train)
-
在测试数据上测试最终训练好的模型:
y_pred = tuned_pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
根据前面的代码,我们在测试集上使用最佳超参数组合测试最终训练好的随机森林模型时,F1 分数大约为0.619。
在本节中,我们学习了如何使用纯 Python 代码通过 NNI 实现 SMAC。在下一节中,我们将学习如何通过纯 Python 代码使用 NNI 实现贝叶斯优化高斯过程。
实现贝叶斯优化高斯过程
贝叶斯优化高斯过程(BOGP)是贝叶斯优化超参数调优组(见第四章)的变体之一,NNI 包可以实现。让我们使用与上一节示例中相同的数据、管道和超参数空间,使用纯 Python 代码通过 NNI 实现 BOGP。
以下代码展示了如何使用 NNI 包通过纯 Python 代码实现 BOGP。更详细的代码可以在技术要求部分提到的 GitHub 仓库中找到:
-
在脚本中准备要调优的模型。在这里,我们将使用一个新的脚本,名为
model_numeric.py。在这个脚本中,我们为非数值超参数添加了一个映射,因为 BOGP 只能处理数值超参数:non_numeric_mapping = params = { 'model__criterion': ['gini','entropy'], 'model__class_weight': ['balanced','balanced_subsample'], } -
以 Python 字典的形式定义超参数空间。我们将使用与上一节类似的超参数空间,唯一的区别在于非数值超参数。在这里,所有非数值超参数都被编码为整数值类型:
hyperparameter_space_numeric = { 'model__n_estimators': {'_type': 'randint', '_value': [5, 200]}, 'model__criterion': {'_type': 'choice', '_value': [0, 1]}, 'model__class_weight': {'_type': 'choice', '_value': [0, 1]}, 'model__min_samples_split': {'_type': 'uniform', '_value': [0, 0.1]}, } -
通过
Experiment类设置实验配置。请注意,BOGP 调优器有九个参数:optimize_mode、utility、kappa、xi、nu、alpha、cold_start_num、selection_num_warm_up和selection_num_starting_points。有关 BOGP 调优器参数的更多信息,请参阅官方文档页面(nni.readthedocs.io/en/stable/reference/hpo.html#gp-tuner):experiment = Experiment('local') experiment.config.experiment_name = 'nni_sklearn_bogp' experiment.config.tuner.name = 'GPTuner' experiment.config.tuner.class_args = { 'optimize_mode': 'maximize', 'utility': 'ei','xi': 0.01} # Boilerplate code experiment.config.trial_command = “python '/mnt/c/Users/Louis\ Owen/Desktop/Packt/Hyperparameter-Tuning-with-Python/nni/model_numeric.py'” experiment.config.trial_code_directory = '.' experiment.config.search_space = hyperparameter_space_numeric experiment.config.max_trial_number = 100 experiment.config.trial_concurrency = 10 experiment.config.max_experiment_duration = '1h' -
运行超参数调优实验:
experiment.run(8080, wait_completion = True, debug = False) -
使用找到的最佳超参数集在全部训练数据上训练模型。
获取最佳超参数集:
non_numeric_mapping = params = {
'model__criterion': ['gini','entropy'],
'model__class_weight': ['balanced','balanced_subsample'],
}
best_trial = sorted(experiment.export_data(),key = lambda x: x.value, reverse = True)[0]
for key in non_numeric_mapping:
best_trial.parameter[key] = non_numeric_mapping[key][best_trial.parameter[key]]
print(best_trial.parameter)
基于前面的代码,我们得到了以下结果:
{'model__class_weight': 'balanced_subsample', 'model__criterion': 'entropy', 'model__min_samples_split': 0.00055461211818435, 'model__n_estimators': 159}
我们现在可以在全部训练数据上训练模型:
from sklearn.base import clone
tuned_pipe = clone(pipe).set_params(**best_trial.parameter)
在训练数据上拟合管道。
tuned_pipe.fit(X_train_full,y_train)
-
在测试数据上测试最终训练好的模型:
y_pred = tuned_pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
基于前面的代码,我们在测试集上使用最佳超参数集测试最终训练好的随机森林模型时,F1 分数大约为0.619。
在本节中,我们学习了如何使用纯 Python 代码通过 NNI 实现 BOGP。在下一节中,我们将学习如何通过纯 Python 代码使用 NNI 实现 Metis。
实现 Metis
Metis是贝叶斯优化超参数调整组(参见第四章)的一个变体,NNI 包可以实现。让我们使用与上一节示例中相同的数据、管道和超参数空间,使用纯 Python 代码实现 Metis。
以下代码展示了如何使用纯 Python 代码通过 NNI 包实现 Metis。你可以在技术要求部分提到的 GitHub 仓库中找到更详细的代码:
-
在脚本中准备要调整的模型。这里,我们将使用与上一节相同的脚本
model_numeric.py,因为 Metis 只能与数值超参数一起工作。 -
以 Python 字典的形式定义超参数空间。我们将使用与上一节相同的超参数空间。
-
通过
Experiment类设置实验配置。请注意,Metis 调整器有六个参数:optimize_mode、no_resampling、no_candidates、selection_num_starting_points、cold_start_num和exploration_probability。有关 Metis 调整器参数的更多信息,请参阅官方文档页面(nni.readthedocs.io/en/stable/reference/hpo.html#metis-tuner):experiment = Experiment('local') experiment.config.experiment_name = 'nni_sklearn_metis' experiment.config.tuner.name = 'MetisTuner' experiment.config.tuner.class_args['optimize_mode'] = 'maximize' # Boilerplate code # same as previous section -
运行超参数调整实验:
experiment.run(8080, wait_completion = True, debug = False) -
使用找到的最佳超参数集在全部训练数据上训练模型。
获取最佳超参数集:
non_numeric_mapping = params = {
'model__criterion': ['gini','entropy'],
'model__class_weight': ['balanced','balanced_subsample'],
}
best_trial = sorted(experiment.export_data(),key = lambda x: x.value, reverse = True)[0]
for key in non_numeric_mapping:
best_trial.parameter[key] = non_numeric_mapping[key][best_trial.parameter[key]]
print(best_trial.parameter)
基于前面的代码,我们得到了以下结果:
{'model__n_estimators': 122, 'model__criterion': 'gini', 'model__class_weight': 'balanced', 'model__min_samples_split': 0.00173059072806428}
我们现在可以在全部训练数据上训练模型:
from sklearn.base import clone
tuned_pipe = clone(pipe).set_params(**best_trial.parameter)
# Fit the pipeline on train data
tuned_pipe.fit(X_train_full,y_train)
-
在测试数据上测试最终训练好的模型:
y_pred = tuned_pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
基于前面的代码,我们在测试集上使用最佳超参数集测试最终训练好的随机森林模型时,F1 分数大约为0.590。
在本节中,我们学习了如何使用纯 Python 代码通过 NNI 实现 Metis。在下一节中,我们将学习如何通过纯 Python 代码使用 NNI 实现模拟退火。
实现模拟退火
模拟退火是启发式搜索超参数调整组(参见第五章)的一种变体,NNI 包可以实现。让我们使用与上一节示例中相同的数据、管道和超参数空间,使用纯 Python 代码实现模拟退火。
以下代码展示了如何使用纯 Python 代码通过 NNI 包实现模拟退火。你可以在技术要求部分提到的 GitHub 仓库中找到更详细的代码:
-
在脚本中准备要调整的模型。我们将使用与实现网格搜索部分相同的
model.py脚本。 -
以 Python 字典的形式定义超参数空间。我们将使用与实现网格搜索部分相同的超参数空间。
-
通过
Experiment类设置实验配置。请注意,对于模拟退火调整器有一个参数,即optimize_mode:experiment = Experiment('local') experiment.config.experiment_name = 'nni_sklearn_anneal' experiment.config.tuner.name = 'Anneal' experiment.config.tuner.class_args['optimize_mode'] = 'maximize' # Boilerplate code experiment.config.trial_command = “python '/mnt/c/Users/Louis\ Owen/Desktop/Packt/Hyperparameter-Tuning-with-Python/nni/model.py'” experiment.config.trial_code_directory = '.' experiment.config.search_space = hyperparameter_space experiment.config.max_trial_number = 100 experiment.config.trial_concurrency = 10 experiment.config.max_experiment_duration = '1h' -
运行超参数调整实验:
experiment.run(8080, wait_completion = True, debug = False) -
使用找到的最佳超参数集在全部训练数据上训练模型。
获取最佳的超参数集:
best_trial = sorted(experiment.export_data(),key = lambda x: x.value, reverse = True)[0]
print(best_trial.parameter)
根据前面的代码,我们得到了以下结果:
{'model__n_estimators': 103, 'model__criterion': 'gini', 'model__class_weight': 'balanced_subsample', 'model__min_samples_split': 0.0010101249953063539}
我们现在可以使用全部训练数据来训练模型:
from sklearn.base import clone
tuned_pipe = clone(pipe).set_params(**best_trial.parameter)
# Fit the pipeline on train data
tuned_pipe.fit(X_train_full,y_train)
-
在测试数据上测试最终训练好的模型:
y_pred = tuned_pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
根据前面的代码,当我们在测试集上使用最佳超参数集测试最终训练好的随机森林模型时,F1 分数大约为0.600。
在本节中,我们学习了如何使用纯 Python 代码通过 NNI 实现模拟退火。在下一节中,我们将学习如何通过纯 Python 代码实现 Hyper Band。
实现 Hyper Band
Hyper Band 是多保真优化超参数调整组(参见第六章)的一种变体,NNI 包可以实现。让我们使用与上一节示例中相同的数据、管道和超参数空间,使用纯 Python 代码实现 Hyper Band。
以下代码展示了如何使用纯 Python 代码通过 NNI 包实现 Hyper Band。你可以在技术要求部分提到的 GitHub 仓库中找到更详细的代码:
-
在脚本中准备要调整的模型。在这里,我们将使用一个名为
model_advisor.py的新脚本。在这个脚本中,我们利用nni.get_next_parameter()输出的TRIAL_BUDGET值来更新'model__n_estimators'超参数。 -
以 Python 字典的形式定义超参数空间。我们将使用与实现网格搜索部分类似的超参数空间,但我们将移除
'model__n_estimators'超参数,因为它将成为 Hyper Band 的预算定义:hyperparameter_space_advisor = { 'model__criterion': {'_type': 'choice', '_value': ['gini', 'entropy']}, 'model__class_weight': {'_type': 'choice', '_value': [“balanced”,”balanced_subsample”]}, 'model__min_samples_split': {'_type': 'uniform', '_value': [0, 0.1]}, } -
通过
Experiment类设置实验配置。请注意,Hyper Band 顾问有四个参数:optimize_mode、R、eta和exec_mode。请参考官方文档页面以获取有关 Hyper Band 顾问参数的更多信息(nni.readthedocs.io/en/latest/reference/hpo.html):experiment = Experiment('local') experiment.config.experiment_name = 'nni_sklearn_hyper_band' experiment.config.advisor.name = 'Hyperband' experiment.config.advisor.class_args['optimize_mode'] = 'maximize' experiment.config.advisor.class_args['R'] = 200 experiment.config.advisor.class_args['eta'] = 3 experiment.config.advisor.class_args['exec_mode'] = 'parallelism' # Boilerplate code experiment.config.trial_command = “python '/mnt/c/Users/Louis\ Owen/Desktop/Packt/Hyperparameter-Tuning-with-Python/nni/model_advisor.py'” experiment.config.trial_code_directory = '.' experiment.config.search_space = hyperparameter_space_advisor experiment.config.max_trial_number = 100 experiment.config.trial_concurrency = 10 experiment.config.max_experiment_duration = '1h' -
运行超参数调优实验:
experiment.run(8080, wait_completion = True, debug = False) -
使用找到的最佳超参数集在全部训练数据上训练模型。
获取最佳超参数集:
best_trial = sorted(experiment.export_data(),key = lambda x: x.value, reverse = True)[0]
best_trial.parameter['model__n_estimators'] = best_trial.parameter['TRIAL_BUDGET'] * 50
del best_trial.parameter['TRIAL_BUDGET']
print(best_trial.parameter)
基于前面的代码,我们得到以下结果:
{'model__criterion': 'gini', 'model__class_weight': 'balanced_subsample', 'model__min_samples_split': 0.001676130360763284, 'model__n_estimators': 100}
我们现在可以在全部训练数据上训练模型:
from sklearn.base import clone
tuned_pipe = clone(pipe).set_params(**best_trial.parameter)
在训练数据上拟合管道。
tuned_pipe.fit(X_train_full,y_train)
-
在测试数据上测试最终训练的模型:
y_pred = tuned_pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
基于前面的代码,我们在使用最佳超参数集在测试集上测试最终训练的随机森林模型时,F1 分数大约为0.593。
在本节中,我们学习了如何使用纯 Python 代码通过 NNI 实现 Hyper Band。在下一节中,我们将学习如何通过纯 Python 代码使用 NNI 实现贝叶斯优化超参数搜索。
实现贝叶斯优化超参数搜索
贝叶斯优化超参数搜索(BOHB)是 NNI 包可以实现的 Multi-Fidelity Optimization 超参数调优组的一种变体(参见第六章)。请注意,要在 NNI 中使用 BOHB,我们需要使用以下命令安装额外的依赖项:
pip install "nni[BOHB]"
让我们使用与上一节示例中相同的数据、管道和超参数空间,使用纯 Python 代码实现 BOHB(贝叶斯优化超参数搜索)。
以下代码展示了如何使用纯 Python 代码通过 NNI 包实现 Hyper Band。更详细的代码可以在技术要求部分提到的 GitHub 仓库中找到:
-
在脚本中准备要调优的模型。我们将使用与上一节相同的
model_advisor.py脚本。 -
以 Python 字典的形式定义超参数空间。我们将使用与上一节相同的超参数空间。
-
通过
Experiment类设置实验配置。请注意,BOHB 顾问有 11 个参数:optimize_mode、min_budget、max_budget、eta、min_points_in_model、top_n_percent、num_samples、random_fraction、bandwidth_factor、min_bandwidth和config_space。请参考官方文档页面以获取有关 Hyper Band 顾问参数的更多信息(nni.readthedocs.io/en/latest/reference/hpo.html#bohb-tuner):experiment = Experiment('local') experiment.config.experiment_name = 'nni_sklearn_bohb' experiment.config.advisor.name = 'BOHB' experiment.config.advisor.class_args['optimize_mode'] = 'maximize' experiment.config.advisor.class_args['max_budget'] = 200 experiment.config.advisor.class_args['min_budget'] = 5 experiment.config.advisor.class_args['eta'] = 3 # Boilerplate code # same as previous section -
运行超参数调优实验:
experiment.run(8080, wait_completion = True, debug = False) -
使用找到的最佳超参数集在全部训练数据上训练模型。
获取最佳超参数集:
best_trial = sorted(experiment.export_data(),key = lambda x: x.value, reverse = True)[0]
best_trial.parameter['model__n_estimators'] = best_trial.parameter['TRIAL_BUDGET'] * 50
del best_trial.parameter['TRIAL_BUDGET']
print(best_trial.parameter)
基于前面的代码,我们得到以下结果:
{'model__class_weight': 'balanced', 'model__criterion': 'gini', 'model__min_samples_split': 0.000396569883631686, 'model__n_estimators': 1100}
我们现在可以在全部训练数据上训练模型:
from sklearn.base import clone
tuned_pipe = clone(pipe).set_params(**best_trial.parameter)
# Fit the pipeline on train data
tuned_pipe.fit(X_train_full,y_train)
-
在测试数据上测试最终训练好的模型:
y_pred = tuned_pipe.predict(X_test_full) print(f1_score(y_test, y_pred))
基于前面的代码,我们在测试集上使用最佳超参数集测试最终训练好的随机森林模型时,F1 分数大约为0.617。
在本节中,我们学习了如何使用纯 Python 代码实现 NNI 的贝叶斯优化超参数搜索。在下一节中,我们将学习如何通过 nnictl 使用 NNI 实现 Population-Based Training。
实现基于群体的训练
基于群体的训练(PBT)是启发式搜索超参数调整组(参见 第五章)的变体之一,NNI 包可以实现。为了向您展示如何使用纯 Python 代码通过 NNI 实现 PBT,我们将使用 NNI 包提供的相同示例。在这里,我们使用了 MNIST 数据集和卷积神经网络模型。我们将使用 PyTorch 来实现神经网络模型。有关 NNI 提供的代码示例的详细信息,请参阅 NNI GitHub 仓库(github.com/microsoft/nni/tree/1546962f83397710fe095538d052dc74bd981707/examples/trials/mnist-pbt-tuner-pytorch)。
MNIST 数据集
MNIST 是一个手写数字数据集,这些数字已经被标准化并居中在一个固定大小的图像中。在这里,我们将使用 PyTorch 包直接提供的 MNIST 数据集(pytorch.org/vision/stable/generated/torchvision.datasets.MNIST.html#torchvision.datasets.MNIST)。
以下代码展示了如何使用 NNI 包实现 PBT。在这里,我们将使用 nnictl 而不是使用纯 Python 代码。更详细的代码可以在 技术要求 部分提到的 GitHub 仓库中找到:
-
在脚本中准备要调整的模型。在这里,我们将使用来自 NNI GitHub 仓库的相同的
mnist.py脚本。请注意,我们将脚本保存为新的名称:model_pbt.py。 -
在名为
hyperparameter_space_pbt.json的 JSON 文件中定义超参数空间。在这里,我们将使用来自 NNI GitHub 仓库的相同的search_space.json文件。 -
通过
config_pbt.yaml文件设置实验配置。请注意,PBT 调优器有六个参数:optimize_mode、all_checkpoint_dir、population_size、factor、resample_probability和fraction。有关 PBT 调优器参数的更多信息,请参阅官方文档页面(nni.readthedocs.io/en/latest/reference/hpo.html#pbt-tuner):searchSpaceFile: hyperparameter_space_pbt.json trialCommand: python '/mnt/c/Users/Louis\ Owen/Desktop/Packt/Hyperparameter-Tuning-with-Python/nni/model_pbt.py' trialGpuNumber: 1 trialConcurrency: 10 maxTrialNumber: 100 maxExperimentDuration: 1h tuner: name: PBTTuner classArgs: optimize_mode: maximize trainingService: platform: local useActiveGpu: false -
运行超参数调优实验。我们可以通过启动的 Web 门户查看实验状态和各种有趣的统计数据。以下代码展示了如何在
local模式下运行端口8080上的实验,这意味着你可以在http://localhost:8080上打开 Web 门户:nnictl create --config config_pbt.yaml --port 8080
在本节中,我们学习了如何通过nnictl使用 NNI 官方文档中提供的相同示例来实现基于群体的训练。
摘要
在本章中,我们学习了关于 DEAP 和 Microsoft NNI 包的所有重要内容。我们还学习了如何借助这些包实现各种超参数调优方法,以及理解每个类的重要参数以及它们与我们之前章节中学到的理论之间的关系。从现在开始,你应该能够利用这些包来实现你选择的超参数调优方法,并最终提高你的机器学习模型性能。凭借第三章至第六章的知识,你还将能够调试代码,如果出现错误或意外结果,并且能够制定自己的实验配置以匹配你的特定问题。
在下一章中,我们将学习几种流行算法的超参数。每个算法都会有广泛的解释,包括但不限于每个超参数的定义、当每个超参数的值发生变化时将产生什么影响,以及基于影响的超参数优先级列表。
第三部分:付诸实践
在本书的最后一节,正如其名所示,我们将学习如何将我们所学的一切应用到实践中,以便我们能够拥有一个有效且强大的超参数调优实验工作流程。
本节包括以下章节:
-
第十一章, 理解流行算法的超参数
-
第十二章, 介绍超参数调优决策图
-
第十三章, 跟踪超参数调优实验
-
第十四章, 结论与下一步行动
第十一章:第十一章: 理解流行算法的超参数
大多数机器学习(ML)算法都有自己的超参数。在不了解模型超参数的情况下,知道如何实施许多花哨的超参数调整方法,就像医生在诊断病人之前就开处方一样。
在本章中,我们将学习几种流行机器学习算法的超参数。对于每个算法,都会有广泛的解释,包括但不限于每个超参数的定义、当每个超参数的值发生变化时将产生什么影响,以及基于影响的超参数优先级列表。
到本章结束时,你将了解几种流行机器学习算法的重要超参数。理解机器学习算法的超参数至关重要,因为并非所有超参数在影响模型性能时都同等重要。我们不必对模型的所有超参数进行超参数调整;我们只需要关注更关键的超参数。
本章将涵盖以下主要内容:
-
探索随机森林超参数
-
探索 XGBoost 超参数
-
探索 LightGBM 超参数
-
探索 CatBoost 超参数
-
探索 SVM 超参数
-
探索人工神经网络超参数
探索随机森林超参数
随机森林是一种基于树的模型,它使用一系列决策树构建而成。它是一个非常强大的集成机器学习模型,可以用于分类和回归任务。随机森林利用决策树集合的方式是通过执行一种称为自助聚集(bagging)的集成方法,并进行一些修改。为了理解随机森林的每个超参数如何影响模型性能,我们首先需要了解模型是如何工作的。
在讨论随机森林如何集成一系列决策树之前,让我们先讨论一下决策树在高级别是如何工作的。决策树可以通过构建一系列决策(以规则和分割点形式)来执行分类或回归任务,这些决策可以以树的形式可视化。这些决策是通过查看给定训练数据中的所有特征和特征值来做出的。决策树的目标是使每个叶节点具有高度的纯度。可以使用几种方法来衡量纯度;对于分类任务,最流行的方法是计算基尼或熵值,而对于回归任务,最流行的方法是计算均方误差值。
随机森林利用袋装法来集成决策树集合。袋装法是一种集成方法,通过结合多个机器学习模型的预测,希望生成更准确和鲁棒的预测。在这种情况下,随机森林结合了多个决策树的预测输出,这样我们就不太关注单个树的预测。这是因为决策树很可能对训练数据进行过拟合。然而,随机森林不仅仅利用了传统的袋装集成方法,它还确保只利用那些彼此之间高度不相关的决策树集合的预测输出。随机森林是如何做到这一点的呢?它不是要求每个决策树在选择分割点时查看所有特征及其值,而是随机森林定制了这个过程,使得每个决策树只查看特征的一个随机样本。
在 Python 中,最受欢迎且维护得最好的随机森林实现可以在 scikit-learn 包中找到。它包括回归(RandomForestRegressor)和分类(RandomForestClassifier)任务的实现。这两个实现具有非常相似的超参数,只有少数小的差异。以下是最重要的超参数,按照对模型性能影响从大到小排序。请注意,这个优先级列表是主观的,基于我们过去开发随机森林模型的经验:
-
n_estimators:这指定了用于构建随机森林的决策树数量。一般来说,树的数量越多,模型的性能越好,但这也意味着计算时间会更长。然而,存在一个阈值,超过这个阈值添加更多的树对模型性能的提升影响不大。甚至可能由于过拟合问题而产生负面影响。 -
max_features:这指定了随机森林用于在每个决策树中选择最佳分割点的随机采样特征的数量。值越高,方差减少越少,因此偏差增加越少。更高的值也会导致计算时间更长。scikit-learn 默认情况下,对于回归任务将使用所有特征,而对于分类任务则只使用sqrt(n_features)数量的特征。 -
criterion:这用于衡量每个决策树的同质性。scikit-learn 为回归和分类任务实现了几种方法。对于回归任务有squared_error、absolute_error和poisson,而对于分类任务有gini、entropy和log_loss。不同的方法将对模型性能产生不同的影响;对于这个超参数没有明确的经验法则。 -
max_depth: 这指定了每个决策树的最大深度。此超参数的默认值为None,意味着每个树的节点将继续分支,直到我们得到纯叶节点或直到所有叶子节点包含的样本数少于min_samples_split。值越低越好,因为这可以防止过拟合。然而,一个过低的值可能导致欠拟合问题。有一点可以肯定——值越高意味着计算时间越长。 -
min_samples_split: 这指定了树能够进一步分裂内部节点(可以分裂成子节点的节点)所需的最小样本数。值越高,越容易防止过拟合。 -
min_samples_leaf: 这指定了叶节点中所需的最小样本数。较高的值可以帮助我们防止过拟合。
scikit-learn 中的随机森林超参数
如需了解 scikit-learn 中随机森林实现的每个超参数的更多信息,请访问官方文档页面:https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html 和 scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestRegressor.html。
其他有用的模板参数可以在不同的 scikit-learn 估计器实现中找到。以下是一些您在训练 scikit-learn 估计器时需要了解的重要参数,这些参数可以帮助您:
-
class_weight: 这指定了训练数据中每个类别的权重。此参数仅适用于分类任务。当您面临类别不平衡问题时,此参数非常重要。我们需要给样本较少的类别赋予更高的权重。 -
n_jobs: 这指定了在训练估计器时使用的并行进程数。scikit-learn 在后台使用joblib包。 -
random_state: 这指定了随机种子数,以确保代码的可重复性。 -
verbose: 此参数用于控制任何日志活动。将verbose设置为大于零的整数可以让我们看到在训练估计器时发生了什么。
在本节中,我们学习了随机森林在高级别上的工作原理,并查看了一些重要的超参数,以及它们如何影响模型性能的解释。我们还了解了主要超参数。此外,我们还了解了 scikit-learn 中的一些有用参数,这些参数可以简化训练过程。在下一节中,我们将讨论 XGBoost 算法。
探索 XGBoost 超参数
极端梯度提升(XGBoost)也是一个基于树的模型,它通过一系列决策树的集合构建,类似于随机森林。它也可以用于分类和回归任务。XGBoost 与随机森林之间的区别在于它们如何进行集成。与使用袋装集成方法的随机森林不同,XGBoost 使用另一种称为提升的集成方法。
提升是一种集成算法,其目标是通过一系列单独的弱模型,通过克服先前模型的弱点(参见图 11.1)来提高性能。它不是一个特定的模型;它只是一个通用的集成算法。弱度的定义可能因不同的提升集成实现类型而异。在 XGBoost 中,它是基于先前决策树模型的梯度误差来定义的。请看以下图表:
![Figure 11.1 – Boosting ensemble algorithm]
![img/B18753_11_001.jpg]
图 11.1 – 提升集成算法
XGBoost 是一个非常流行且广泛采用的机器学习模型,它使用提升集成算法和一系列决策树构建。每个决策树都是逐个添加的,并拟合到前一个树的预测误差,以纠正这些误差。值得注意的是,由于 XGBoost 是梯度提升算法的一部分,所有弱模型(决策树)都需要使用可微分的损失函数和梯度下降优化方法进行拟合。
XGBoost 有自己的包,不仅可以在 Python 中使用,还可以在其他编程语言中使用,如 R 和 JVM。在 Python 中,您可以通过pip install xgboost安装 XGBoost。此包还实现了 scikit-learn 包装器,用于回归(XGBRegressor)和分类(XGBClassifier)任务。该包提供了许多超参数,但并非所有参数都对模型性能有重大影响。以下是最重要的超参数,按其对模型性能影响的重要性从高到低排序:
-
n_estimators:这指定了用于构建 XGBoost 模型要使用的决策树数量。它也可以解释为提升轮数,这与神经网络中的 epoch 概念相似。一般来说,值越高,模型的性能越好,但代价是计算时间会更长。然而,我们需要小心过高的值,因为它可能导致过拟合问题。 -
learning_rate:这是梯度下降优化算法的学习率。值越低,模型找到最优解的机会越高,但代价是计算时间更长。如果你在训练的最后几次迭代中没有发现过拟合的迹象,你可以增加这个超参数的值;如果有过拟合,你可以减少它。 -
max_depth:这是每个决策树的最大深度。较低的值可以帮助我们防止过拟合。然而,过低的值可能导致欠拟合问题。有一点可以肯定——较高的值会导致更长的计算时间。 -
min_child_weight:这是在子节点中需要的、使用 Hessian 计算的实例权重最小总和。这个超参数作为正则化器,确保每个树在达到一定程度的纯度后停止尝试分裂节点。换句话说,它是一个通过限制树深度来防止过拟合的正则化参数。较高的值可以帮助我们防止过拟合。然而,过高的值可能导致欠拟合问题。 -
gamma:这是一个基于损失值减少的伪正则化参数。这个超参数的值指定了在树的叶子节点上进行进一步划分所需的最小损失减少量。你可以给这个超参数一个较高的值来防止过拟合问题。然而,请小心,不要使用过高的值;它可能导致欠拟合问题。 -
colsample_bytree:这是 scikit-learn 实现的随机森林中max_features超参数的分数版本。这个超参数负责告诉 XGBoost 在每个决策树中选择最佳分裂点需要多少随机采样的特征。较低的值可以帮助我们防止过拟合并降低计算时间。然而,过低的值可能导致欠拟合问题。 -
subsample:这是colsample_bytree超参数的观察版本。这个超参数负责告诉 XGBoost 在训练每个树时需要使用多少训练样本。这个超参数可以用来防止过拟合问题。然而,如果我们使用过低的值,它也可能导致欠拟合问题。
XGBoost 超参数完整列表
如需了解 XGBoost 其他超参数的更多信息,请访问官方文档页面:https://xgboost.readthedocs.io/en/stable/python/python_api.html#module-xgboost.sklearn。
在本节中,我们讨论了 XGBoost 的高层次工作原理,并查看了一些重要的超参数,以及它们如何影响模型性能的解释。我们还看了主要超参数。在下一节中,我们将讨论 LightGBM 算法。
探索 LightGBM 超参数
轻量梯度提升机(LightGBM)也是一种基于决策树集合的增强算法,类似于 XGBoost。它既可以用于分类任务,也可以用于回归任务。然而,它在树的生长方式上与 XGBoost 不同。在 LightGBM 中,树是以叶节点的顺序生长的,而 XGBoost 是以层级的顺序生长的(见图 11.2)。我们所说的“叶节点优先”是指 LightGBM 通过优先增长那些分裂导致同质性增加最大的节点来生长树:
![Figure 11.2 – Level-wise versus leaf-wise tree growth
![img/B18753_11_002.jpg]
图 11.2 – Level-wise versus leaf-wise tree growth
除了 XGBoost 和 LightGBM 在树的生长方式上的不同之外,它们在处理分类特征方面也有不同的方法。在 XGBoost 中,我们需要在将特征传递给模型之前对分类特征进行编码。这通常是通过使用独热编码或整数编码方法来完成的。在 LightGBM 中,我们只需告诉哪些特征是分类的,它就会通过执行等值分裂自动处理这些特征。在分布式学习中进行优化时,XGBoost 和 LightGBM 在执行优化方面还有其他几个不同之处。总的来说,与 XGBoost 相比,LightGBM 的计算时间要快得多。
与 XGBoost 类似,LightGBM 也有自己的包,不仅可以在 Python 中使用,也可以在 R 语言中使用。在 Python 中,您可以通过pip install lightgbm来安装 LightGBM。此包还实现了 scikit-learn 包装器,用于回归(LGBMRegressor)和分类(LGBMClassifier)任务。以下是 LightGBM 最重要的超参数,从对模型性能影响最大到最小排序:
-
max_depth:这指定了每个决策树的最大深度。较低的值可以帮助我们防止过拟合。然而,过低的值可能导致欠拟合问题。有一点可以肯定——较高的值意味着更长的计算时间。 -
num_leaves:这指定了每棵树的最大叶节点数。它的值应该小于max_depth的 2 的幂,因为对于固定数量的叶节点,叶节点优先的树比深度优先的树要深得多。一般来说,值越高,模型的性能越好,但这也意味着更长的计算时间。然而,存在一个阈值,增加更多叶节点对模型性能的影响不会很大,甚至可能由于过拟合而产生负面影响。 -
Learning_rate:这指定了梯度下降优化算法的学习率。值越低,模型找到更优解的可能性越高,但这也意味着更长的计算时间。如果在训练的最后几次迭代中没有发现过拟合的迹象,则可以增加此超参数的值,反之亦然。 -
min_child_samples:这指定了叶节点中所需的最小样本数。较高的值可以帮助我们防止过拟合。然而,一个过高的值可能导致欠拟合问题。 -
特征分数:这与 XGBoost 中的colsample_bytree类似。这个超参数告诉 LightGBM 在每棵决策树中选择最佳分割点时需要使用多少随机采样的特征。这个超参数对于防止过拟合可能很有用。然而,如果我们使用一个过低的值,也可能导致欠拟合问题。 -
bagging_fraction:这是feature_fraction超参数的观测版本。这个超参数负责告诉 LightGBM 在每棵树的训练过程中需要使用多少训练样本。较低的值可以帮助我们防止过拟合并降低计算时间。然而,一个过低的值可能导致欠拟合问题。
LightGBM 超参数完整列表
想了解更多关于其他 LightGBM 超参数的信息,请访问官方文档页面:lightgbm.readthedocs.io/en/latest/Python-API.html#scikit-learn-api。
在本节中,我们讨论了 LightGBM 在高级别的工作方式,并查看了一些重要的超参数,以及它们如何影响模型性能的解释。我们还看了主要超参数。在下一节中,我们将讨论 CatBoost 算法。
探索 CatBoost 超参数
分类提升(CatBoost)是另一种基于决策树集合的增强算法,类似于 XGBoost 和 LightGBM。它也可以用于分类和回归任务。CatBoost 与 XGBoost 或 LightGBM 的主要区别在于其生长树的方式。在 XGBoost 和 LightGBM 中,树是非对称生长的,而在 CatBoost 中,树是对称生长的,使得所有树都是平衡的。这种平衡树特性提供了几个好处,包括控制过拟合问题的能力、降低推理时间和在 CPU 上的高效实现。CatBoost 通过在每个节点的每个分割中使用相同的条件来实现这一点,如下面的图所示:

图 11.3 – 非对称与对称树
CatBoost 的主要卖点是其自动处理多种类型特征的能力,包括数值、分类和文本,特别是对于分类特征。我们只需通过cat_features参数告诉 CatBoost 哪些特征是分类特征,它就会自动处理这些特征。默认情况下,CatBoost 会对只有两个类别的分类特征执行独热编码。对于更高基数特征,它将执行目标编码,并组合几个分类特征,甚至分类和数值特征。有关 CatBoost 如何处理分类特征的更多信息,请参阅官方文档页面:catboost.ai/en/docs/concepts/algorithm-main-stages_cat-to-numberic。
与 XGBoost 和 LightGBM 类似,CatBoost 也有自己的包,不仅可以在 Python 中使用,也可以在 R 语言中使用。在 Python 中,你可以通过pip install catboost安装 CatBoost。你可以利用实现的 scikit-learn 兼容类来处理回归(CatBoostRegressor)和分类(CatBoostClassifier)任务。以下是根据每个超参数对模型性能的重要性按降序排列的 CatBoost 最重要的超参数列表:
-
iterations: 这指定了用于构建 CatBoost 模型的决定树的数量。它也可以解释为提升轮数,类似于神经网络中的 epoch 概念。一般来说,值越高,模型的性能越好,但代价是计算时间更长。然而,存在一个阈值,增加更多树对模型性能的影响不会太大,甚至可能由于过拟合而产生负面影响。 -
depth: 这指定了每个决策树的最大深度。一个较低的值可以帮助我们防止过拟合。然而,一个过低的值可能导致欠拟合问题。有一点可以肯定——较高的值意味着更长的计算时间。 -
learning_rate: 这指定了梯度下降优化算法的学习率。值越低,模型找到更优解的机会越高,但代价是计算时间更长。如果在训练的最后几次迭代中没有发现过拟合的迹象,你可以增加这个超参数的值,反之亦然。 -
l2_leaf_reg: 这是在成本函数上的正则化参数。这个超参数可以防止过拟合问题。然而,如果我们使用一个过高的值,它也可能导致欠拟合问题。 -
one_hot_max_size:这是 CatBoost 决定何时对分类特征进行 one-hot 编码的阈值。任何基数小于或等于给定值的分类特征将通过 one-hot 编码方法转换为数值。
CatBoost 超参数完整列表
关于其他 CatBoost 超参数的更多信息,请访问官方文档页面(catboost.ai/en/docs/concepts/parameter-tuning)。
在本节中,我们讨论了 CatBoost 在高级别的工作原理,并查看了一些重要的超参数,以及它们如何影响模型性能的解释。我们还看了主要超参数。在下一节中,我们将讨论 SVM 算法。
探索 SVM 超参数
支持向量机(SVM)是一种机器学习模型,它利用线或超平面,以及一些线性代数变换,来执行分类或回归任务。前几节中讨论的所有算法都可以归类为基于树的算法,而 SVM 不属于基于树的机器学习算法组。它是基于距离的算法组的一部分。我们通常称 SVM 中的线性代数变换为内核。这负责将任何问题转化为线性问题。
Python 中最受欢迎和最维护良好的 SVM 实现可以在 scikit-learn 包中找到。它包括回归(SVR)和分类(SVC)任务的实现。它们都有非常相似的超参数,只有一些小的差异。以下是最重要的 SVM 超参数,从对模型性能影响最大到最小排序:
-
kernel:这是线性代数变换,其目标是把给定问题转化为线性问题。我们可以从五个内核中选择,包括线性(linear)、多项式(poly)、径向基函数(rbf)和 sigmoid(sigmoid)内核。不同的内核将对模型性能产生不同的影响,对于这个超参数没有明确的经验法则。 -
C:这是控制过拟合的正则化参数。值越低,正则化对模型的影响越强,因此预防过拟合的可能性越高。 -
degree:这个超参数是特定于多项式内核函数的。这个超参数的值对应于模型所使用的多项式函数的次数。 -
gamma:这是径向基、多项式和 sigmoid 内核函数的系数。scikit-learn 提供了两种选项,即scale和auto。
scikit-learn 中的 SVM 超参数
如需了解 SVM 中每个超参数在 scikit-learn 中的实现方式,您可以访问官方文档页面:https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVC.html 和 scikit-learn.org/stable/modules/generated/sklearn.svm.SVR.html。
在本节中,我们讨论了 SVM 在高级别上是如何工作的,并查看了一些重要的超参数,以及它们如何影响模型性能的解释。我们还探讨了主要超参数。在下一节中,我们将讨论人工神经网络。
探索人工神经网络超参数
人工神经网络,也称为深度学习,是一种模仿人类大脑工作方式的机器学习算法。深度学习可以用于回归和分类任务。该模型的主要卖点之一是其能够从原始数据中自动执行特征工程和选择。一般来说,为了确保此算法表现良好,我们需要向模型提供大量训练数据。神经网络的最简单形式称为感知器(见图 11.4)。感知器只是在所有特征上应用的一个线性组合,并在计算的末尾添加一个偏差:
![图 11.4 – 感知器]
![img/B18753_11_004.jpg]
图 11.4 – 感知器
如果将感知器的输出传递给一个非线性函数,这通常被称为激活函数,然后再传递给另一个感知器,那么我们可以称之为具有一层多层感知器(MLP)。神经网络的训练过程包括两个主要步骤,即前向传播和反向传播。在前向传播中,我们只是让神经网络根据定义的架构在给定的输入上执行计算。在反向传播中,模型将根据定义的损失函数使用基于梯度的优化过程更新权重和偏差参数。
除了 MLP 之外,还有其他类型的神经网络,例如卷积神经网络(CNNs)、长短期记忆网络(LSTMs)、循环神经网络(RNNs)和变换器。当我们处理图像数据时,通常会采用 CNN,但当我们处理文本数据时,也可以使用一维 CNN。RNNs 和 LSTMs 通常用于处理时间序列或自然语言数据。变换器主要用于与文本相关的项目,但最近,它们也被用于图像和声音数据。
几个包提供了在 Python 中实现神经网络的实现,包括 PyTorch、TensorFlow 和 Scikit Learn。以下是最重要的超参数,根据每个超参数对模型性能的重要性按降序排列。请注意,这个优先级列表是基于我们过去开发随机森林模型的经验而主观制定的。由于不同包中超参数的命名可能不同,我们将只使用超参数的一般名称:
-
优化器:这是将要使用的基于梯度的优化算法。我们有几个优化器可供选择。然而,可能最流行和最广泛采用的优化器是Adam。还有其他选项,包括但不限于 SGD 和 RMSProp。不同的优化器可能对模型性能有不同的影响,并且没有明确的经验法则来选择哪个是最好的。值得注意的是,每个优化器都有自己的超参数。
-
学习率:这个超参数控制优化器在优化过程中从给定的训练数据中“学习”时步的大小。在选择其他超参数之前,首先选择最佳的学习率范围非常重要。值越低,模型找到更优解的可能性越高,但会以更长的计算时间为代价。如果在训练的最后几次迭代中没有发现过拟合的迹象,则可以增加这个超参数的值,反之亦然。
-
批量大小:这指定了每个训练步骤中将传递给神经网络的训练样本数量。一般来说,值越高,模型的性能越好。然而,过大的批量大小通常会受到设备内存的限制。
-
在 XGBoost 中的
n_estimators和 CatBoost 中的iterations,高值可以导致更好的模型性能,但会以更长的计算时间为代价。然而,我们需要小心使用过高的值,因为它可能导致过拟合。 -
层数数量:值越高,模型的复杂性越高,因此过拟合的可能性也越高。通常,一或两层就足以构建一个好的模型。
-
节点数量:每个层中单元或节点的数量。值越高,模型的复杂性越高,因此过拟合的可能性也越高。
-
激活函数:非线性变换函数。有多个激活函数可供选择。实践中最广泛采用的激活函数包括修正线性激活函数(ReLU)、指数线性单元(ELU)、Sigmoid、Softmax和Tanh。
-
Dropout 率:dropout 层的比率。dropout 层是神经网络中的一个特殊层,通过随机将单元值设为零来充当正则化器。这个超参数控制了多少个单元被设为零。较高的值可以帮助我们防止过拟合。然而,过高的值可能导致欠拟合问题。
-
L1/L2 正则化:这些是应用于损失函数的正则化参数。这个超参数可以帮助防止过拟合。然而,如果其值过高,也可能导致欠拟合问题。
在本节中,我们讨论了神经网络在高级层面上的工作原理,神经网络的变体,并查看了一些重要的超参数,以及它们如何影响模型性能的解释。我们还探讨了主要超参数。现在,让我们总结本章内容。
摘要
在本章中,我们讨论了几个流行的算法在高级层面上的工作原理,解释了它们的重要超参数以及它们如何影响性能,并提供了按性能影响降序排列的超参数优先级列表。此时,你应该能够通过关注最重要的超参数来更有效地设计你的超参数调整实验。你也应该了解每个重要超参数对模型性能的影响。
在下一章中,我们将把这里讨论的超参数调整方法总结成一个简单的决策图,帮助你选择最适合你问题的方法。此外,我们还将涵盖几个案例研究,展示如何在实际中利用这个决策图。
第十二章:第十二章:介绍超参数调整决策图
获取过多信息有时会导致困惑,这反过来又可能导致采取最简单的选项。在前几章中,我们学习了众多超参数调整方法。尽管我们已经讨论了每种方法的细节,但拥有一个单一的真实来源,可以帮助我们决定在哪种情况下使用哪种方法,将非常有用。
在本章中,您将介绍超参数调整决策图(HTDM),它将所有讨论的超参数调整方法总结成一个简单的决策图,基于许多方面,包括超参数空间属性、目标函数的复杂性、训练数据大小、计算资源可用性、先验知识可用性以及我们正在处理的机器学习算法类型。还将有三个案例研究,展示如何在实际中利用 HTDM。
到本章结束时,您将能够利用 HTDM 在实际中帮助您决定在您特定情况下应采用哪种超参数调整方法。
在本章中,我们将涵盖以下主要内容:
-
熟悉 HTDM
-
案例研究 1 – 使用 HTDM 和 CatBoost 分类器
-
案例研究 2 – 使用 HTDM 和条件超参数空间
-
案例研究 3 – 使用 HTDM 和先验的超参数值知识
熟悉 HTDM
HTDM 旨在帮助您决定在特定情况下应采用哪种超参数调整方法(见图 12.1)。在这里,情况是根据六个方面定义的:
-
超参数空间属性,包括空间的大小、超参数值的类型(仅数值或混合)以及是否包含条件超参数
-
目标函数复杂性:它是一个便宜的还是昂贵的目标函数
-
计算资源可用性:您是否有足够的并行计算资源
-
训练数据大小:您是否有少量、适中或大量训练样本
-
先验知识可用性:您是否具有关于良好超参数值范围的先验知识
-
机器学习算法类型:您是处理小型、中型还是大型模型,以及您是处理传统机器学习还是深度学习类型的算法
这可以在以下图表中看到:

图 12.1 – HTDM
在 HTDM 中,“小”、“中”和“大”的定义非常主观。然而,您可以参考以下表格作为经验法则:

图 12.2 – 大小定义的经验法则
以下重要注意事项也可能帮助我们决定在特定情况下应采用哪种超参数调整方法:
.jpg)
.jpg)
图 12.3 – 每个超参数调整方法的注意事项
在本节中,我们讨论了 HTDM,以及一些额外的注意事项,以帮助您决定在特定情况下应该采用哪种超参数调整方法。在接下来的几节中,我们将通过几个有趣的研究案例学习如何在实际中利用 HTDM。
案例研究 1 – 使用 HTDM 与 CatBoost 分类器
假设我们正在基于第七章中介绍的营销活动数据训练一个分类器第七章**,通过 scikit 进行超参数调整。在这里,我们使用 CatBoost(见第十一章**,了解流行算法的超参数)作为分类器。这是我们第一次处理给定数据。我们使用的笔记本电脑只有单核 CPU,超参数空间定义如下。请注意,我们不是在处理条件超参数空间:
-
iterations:randint(5,200) -
depth:randint(3,10) -
learning_rate:np.linspace(1e-5,1e-3,20) -
l2_leaf_reg:np.linspace(1,30,30) -
one_hot_max_size:randint(2,15)
根据给定的案例描述,我们可以尝试利用 HTDM 来帮助我们选择最适合条件的超参数调整方法。首先,我们知道我们没有关于给定数据中良好超参数值的先验知识或元学习结果。这意味着我们只会关注 HTDM 中第一个节点右侧的分支,如图所示:

图 12.4 – 案例研究 1,无先验知识
我们知道我们不是在处理条件超参数空间。这意味着我们只会关注第二个节点右侧的分支,如图所示:

图 12.5 – 案例研究 1,非条件超参数空间
根据粗略估计,我们的 CatBoost 模型的大小应该在小型到中型范围内。这意味着我们只会关注第三个节点的左侧和底部分支,如图所示:

图 12.6 – 案例研究 1,小到中等模型大小
我们还有一个仅由数值组成的中等大小的超参数空间。这意味着我们的选项是粗到细搜索、随机搜索、PSO、模拟退火和遗传算法。值得注意的是,尽管我们的超参数空间仅由数值组成,我们仍然可以利用适用于混合类型值的超参数调整方法:

图 12.7 – 案例研究 1,中等大小的超参数空间,仅包含数值
那么,我们如何从所选选项中选择一个超参数调整方法呢?首先,我们知道 PSO 只在连续类型的超参数值上表现良好,而我们的超参数空间中也有整数。因此,我们可以从我们的选项中移除 PSO。这让我们剩下四个选项。选择最佳超参数调整方法的一个简单而有效的方法是选择最简单的方法,即随机搜索方法。
在本节中,我们讨论了如何在实际中利用 HTDM 的第一个案例研究。在下一节中,我们将使用另一个有趣的案例研究进行同样的操作。
案例研究 2 – 使用 HTDM 和条件超参数空间
假设我们面临与上一节类似的情况,但现在我们正在处理一个条件超参数空间,如下定义:
one_hot_max_size = randint(2,15)
iterations = randint(5,200)
If iterations < 50:
depth = randint(3,10)
learning_rate = np.linspace(5e-4,1e-3,10)
l2_leaf_reg = np.linspace(1,15,20)
elif iterations < 100:
depth = randint(3,7)
learning_rate = np.linspace(1e-5,5e-4,10)
l2_leaf_reg = np.linspace(5,20,20)
else:
depth = randint(3,5)
learning_rate = np.linspace(1e-6,5e-5,10)
l2_leaf_reg = np.linspace(5,30,20)
根据给定的案例描述,我们可以再次尝试利用 HTDM 来帮助我们选择哪种超参数调整方法最适合条件。在这里,与先前的案例研究类似,我们知道我们没有关于给定数据中良好超参数值的先验知识或元学习结果。这意味着我们只会关注 HTDM 中第一个节点右侧的分支(参见图 12.4)。然而,在这个案例中,我们现在正在处理一个条件超参数空间。这意味着我们只会关注第二个节点左侧的分支,如下所示:

图 12.8 – 案例研究 2,一个条件超参数空间
由于我们有超过 10,000 个训练数据样本(参见第七章**,通过 scikit 进行超参数调整),根据 HTDM,我们只有两种超参数调整方法可供选择,即 BOHB 或随机搜索方法(参见图 12.9)。如果我们只从实现简单性的角度比较它们,那么选择随机搜索而不是 BOHB 肯定是一个明智的选择,因为我们需要安装 Microsoft NNI 包才能采用 BOHB 方法(参见图 12.3)。
然而,我们知道我们正在处理一个不是非常小的模型,BOHB 可以根据以往的经验决定需要搜索哪个子空间,而不是基于运气。因此,从理论上讲,BOHB 将是一个更好的选择,可以节省我们寻找最佳超参数集的时间。那么,我们应该选择哪种方法呢?这取决于您的判断:

图 12.9 – 案例研究 2,大量训练数据
在本节中,我们讨论了如何在实际中利用 HTDM 的第二个案例研究。在下一节中,我们将使用另一个有趣的案例研究进行同样的操作。
案例研究 3 – 使用 HTDM 和超参数值的先验知识
假设在这种情况下,我们面临与之前案例研究类似的情况,但这次,我们由于我们团队中的数据科学家之前处理过相同的数据,因此对给定数据的良好超参数值有先验知识。这意味着我们只需关注 HTDM 中第一个节点左侧的分支,如图所示:

图 12.10 – 案例研究 3,具备先验知识
根据给定的案例描述,我们知道我们缺乏足够的并行计算资源,因为我们只有一个单核 CPU。这意味着我们只需关注第二个节点右侧的分支,如图所示:

图 12.11 – 案例研究 3,缺乏并行计算资源
我们还知道我们有一个中等大小的超参数空间,它只包含数值类型的值。这意味着我们的选项是 SMAC、TPE 和 Metis:

图 12.12 – 案例研究 3,中等大小的超参数空间,仅包含数值
根据前面的图表,我们知道当超参数空间主要由分类超参数主导时,SMAC 表现最佳,但这里并非如此。因此,我们可以从我们的选项中移除 SMAC。如果我们试图根据实现流行度来决定,那么 TPE 是我们应该选择的一个,因为它在 Hyperopt、Optuna 和 NNI 中都有实现,而 Metis 只在 NNI 中实现。然而,Metis 的一个主要卖点是其能够建议我们在下一次试验中应该测试的超参数集合。那么,我们应该选择哪种方法呢?这取决于你。
在本节中,我们讨论了如何在实践中利用 HTDM 的第三个案例研究。现在,让我们总结本章内容。
摘要
在本章中,我们将迄今为止讨论的所有超参数调整方法总结在一个简单的决策图 HTDM 中。这可以帮助你选择最适合你特定问题的方法。我们还讨论了每种超参数调整方法的重要注意事项,并展示了如何在实践中利用 HTDM。从现在起,你将能够在实践中利用 HTDM 来帮助你决定在特定情况下采用哪种超参数调整方法。
在下一章中,我们将讨论跟踪超参数调整实验的必要性,并学习如何使用几个开源软件包来做到这一点。
第十三章:第十三章:跟踪超参数调优实验
与大量实验一起工作时,有时可能会感到不知所措。需要执行许多实验迭代。当我们尝试许多机器学习模型时,这将会变得更加复杂。
在本章中,您将了解到跟踪超参数调优实验的重要性,以及常规实践。您还将了解到一些可用的开源包,并学习如何在实践中利用它们。
到本章结束时,您将能够利用您喜欢的包来跟踪您的超参数调优实验。能够跟踪您的超参数调优实验将提高您工作流程的有效性。
在本章中,我们将涵盖以下主题:
-
重温常规实践
-
探索 Neptune
-
探索 Scikit-Optimize
-
探索 Optuna
-
探索 Microsoft NNI
-
探索 MLflow
技术要求
在本章中,我们将学习如何使用各种包跟踪超参数调优实验。为了确保您能够复现本章中的代码示例,您将需要以下内容:
-
Python 3(版本 3.7 或更高)
-
pandas包(版本 1.3.4 或更高) -
NumPy包(版本 1.21.2 或更高) -
scikit-learn包(版本 1.0.1 或更高) -
matplotlib包(版本 3.5.0 或更高) -
Plotly包(版本 4.0.0 或更高) -
Neptune-client包(版本 0.16.3 或更高) -
Neptune-optuna包(版本 0.9.14 或更高) -
Scikit-Optimize 包(版本 0.9.0 或更高)
-
TensorFlow包(版本 2.4.1 或更高) -
Optuna包(版本 2.10.0 或更高) -
MLflow包(版本 1.27.0 或更高)
本章的所有代码示例都可以在 GitHub 上找到:github.com/PacktPublishing/Hyperparameter-Tuning-with-Python。
重温常规实践
在一个小规模项目中执行超参数调优实验可能看起来很简单。我们可以轻松地进行多次实验迭代,并将所有结果写入一个单独的文档中。我们可以在每个实验迭代中记录最佳超参数值集(或如果我们执行手动搜索方法,如第三章**,穷举搜索)的详细信息,以及评估指标。通过拥有实验日志,我们可以从历史中学习,并在实验的下一轮迭代中定义更好的超参数空间。
当我们采用自动超参数调整方法(除了手动搜索方法之外的所有我们之前讨论过的方法)时,我们可以直接获得最终的最佳超参数值集合。然而,当我们采用手动搜索方法时并非如此。我们需要手动测试大量的超参数集合。社区在执行手动搜索时采用了几种实践。让我们来看看。
使用内置的 Python 字典
这是最直接的方法,因为我们只需要创建一个 Python 字典来存储所有需要测试的超参数值。尽管这种做法非常简单,但它也有缺点。例如,我们可能没有注意到覆盖了一些超参数值,并且忘记记录正确的超参数值集合。以下示例展示了如何使用内置的 Python 字典来存储所有需要测试的超参数值,需要在特定的手动搜索迭代中进行测试:
hyperparameters = {
'n_estimators': 30,
'max_features': 10,
'criterion': 'gini',
'max_depth': 5,
'min_samples_split': 0.03,
'min_samples_leaf': 1,
}
接下来,让我们看看配置文件。
使用配置文件
不论是 JSON、YAML 还是 CFG 文件,配置文件都是另一种选择。我们可以在配置文件中放置所有超参数的详细信息,以及其他附加信息,包括(但不限于)项目名称、作者名称和数据预处理管道方法。一旦创建了配置文件,你就可以将其加载到你的 Python 脚本或 Jupyter 笔记本中,并像处理标准的 Python 字典一样处理它。使用配置文件的主要优势是所有重要参数都位于单个文件中,因此将非常容易重用之前保存的配置文件,并提高代码的可读性。然而,在处理大型项目或庞大的代码库时,有时使用配置文件可能会让我们感到困惑,因为我们不得不维护多个配置文件。
使用额外的模块
如果你想要通过命令行界面(CLI)指定超参数值或其他任何训练参数,argparse 和 Click 模块会很有用。这些模块可以在我们用 Python 脚本编写代码时使用,而不是在 Jupyter 笔记本中。
使用 argparse
以下代码展示了如何在 Python 脚本中利用 argparse:
import argparse
parser = argparse.ArgumentParser(description='Hyperparameter Tuning')
parser.add_argument('--n_estimators, type=int, default=30, help='number of estimators')
parser.add_argument('--max_features, type=int, default=20, help='number of randomly sampled features for choosing the best splitting point')
parser.add_argument('--criterion, type=str, default='gini', help='homogeneity measurement method')
parser.add_argument('--max_depth, type=int, default=5, help='maximum tree depth')
parser.add_argument('--min_samples_split, type=float, default=0.03, help='minimum samples to split internal node')
parser.add_argument('--min_samples_leaf, type=int, default=1, help='minimum number of samples in a leaf node')
parser.add_argument('--data_dir, type=str, required=True, help='maximum tree depth')
以下代码展示了如何从命令行界面(CLI)访问值:
args = parser.parse_args()
print(args.n_estimators)
print(args.max_features)
print(args.criterion)
print(args.max_depth)
print(args.min_samples_split)
print(args.min_samples_leaf)
print(args.data_dir)
你可以按照以下方式使用指定的参数运行 Python 脚本:
python main.py --n_estimators 35 -–criterion "entropy" -–data_dir "/path/to/my/data"
值得注意的是,如果你在调用 Python 脚本时没有指定超参数,将使用默认的超参数值。
使用 click
以下代码展示了如何在 Python 脚本中利用 click。请注意,click 与 argparse 非常相似,但实现更简单。我们只需要在特定函数上添加装饰器:
import click
@click.command()
@click.option("--n_estimators, type=int, default=30, help='number of estimators")
@click.option("--max_features, type=int, default=20, help='number of randomly sampled features for choosing the best splitting point")
@click.option("--criterion, type=str, default='gini', help='homogeneity measurement method")
@click.option("--max_depth, type=int, default=5, help='maximum tree depth")
@click.option("--min_samples_split, type=float, default=0.03, help='minimum samples to split internal node")
@click.option("--data_dir, type=str, required=True, help='maximum tree depth")
def hyperparameter_tuning(n_estimators, max_features, criterion, max_depth, min_samples_split, data_dir):
#write your code here
与 argparse 类似,你可以使用指定的参数运行 Python 脚本,如下所示。如果你在调用 Python 脚本时没有指定它们,将使用默认的超参数值:
python main.py --n_estimators 35 -–criterion "entropy" -–data_dir "/path/to/my/data"
尽管使用 argparse 或 click 进行实验非常容易,但值得注意的是,它们都不会保存任何值。因此,在每次试验中记录所有实验的超参数值需要额外的努力。
无论我们采用手动搜索还是其他自动化超参数调整方法,如果需要手动记录实验结果详情,这将需要大量的工作。特别是当我们处理更大规模的实验时,我们必须测试几个不同的机器学习模型、数据预处理管道和其他实验设置,这可能会让人感到不知所措。这就是为什么在接下来的章节中,您将了解到几个可以帮助您跟踪超参数调整实验的包,以便您拥有更有效的流程。
探索 Neptune
Neptune 是一个 Python(和 R)包,充当 MLOps 的元数据存储。此包支持许多用于处理模型构建元数据的特性。我们可以利用 Neptune 来跟踪我们的实验,不仅限于超参数调整实验,还包括其他与模型构建相关的实验。我们只需使用一个包就可以记录、可视化、组织和管理工作。此外,它还支持模型注册并实时监控我们的机器学习作业。
安装 Neptune 非常简单 – 您可以使用 pip install neptune-client 或 conda install -c conda-forge neptune-client。一旦安装完成,您需要注册一个账户以获取 API 令牌。Neptune 在个人计划配额限制内是免费的,但如果您想为商业团队使用 Neptune,则需要付费。有关注册 Neptune 的更多信息,请访问他们的官方网站:https://neptune.ai/register。
使用 Neptune 来帮助跟踪您的超参数调整实验非常简单,如下面的步骤所示:
- 从您的 Neptune 账户主页创建一个新的项目:

图 13.1 – 创建一个新的 Neptune 项目
- 为您的项目输入一个名称和描述:

图 13.2 – 输入项目详情
- 编写超参数调整实验脚本。Neptune 提供了基于您想要使用的框架的模板代码选项,包括但不限于 Optuna、PyTorch、Keras、TensorFlow、scikit-learn 和 XGBoost。您可以直接复制提供的模板代码并根据您的需求进行定制。例如,让我们使用提供的模板代码为 Optuna(见 图 13.3)并保存训练脚本为
train_optuna.py。请参阅本书 GitHub 仓库中的完整代码,该代码在 技术要求 部分提供:

图 13.3 – 创建超参数调整实验脚本
- 运行超参数调整脚本(
python train_optuna.py)并查看 Neptune 项目页面上的实验元数据。每个运行都将存储为 Neptune 中的一个新实验 ID,因此您不必担心实验版本控制,因为 Neptune 会自动为您处理:

图 13.4 – Neptune 的实验运行表
您还可以看到每个实验运行的全部元数据,包括(但不限于)测试的超参数、源代码、CPU/GPU 使用情况、指标图表、工件(数据、模型或任何其他相关文件)和图表(例如,混淆矩阵),如下面的截图所示:

图 13.5 – 存储在 Neptune 中的元数据
- 分析实验结果。Neptune 不仅可以帮助您记录每个实验运行的全部元数据,还可以使用多种比较策略比较几个不同的运行。您可以通过并行图或折线图查看超参数值比较。您还可以通过并排比较策略比较所有实验细节(见图 13.6)。此外,Neptune 还使我们能够比较每个运行之间记录的图像或工件:

图 13.6 – 比较实验运行及其结果
有关在 Neptune 中可以记录和显示的信息的更多信息,请参阅官方文档页面:https://docs.neptune.ai/you-should-know/what-can-you-log-and-display。
Neptune 的集成
Neptune 为机器学习相关实验以及特定超参数调整任务提供了许多集成。Neptune 支持三种超参数调整任务的集成:Optuna、Keras 和 Scikit-Optimize。有关更多信息,请参阅官方文档页面:https://docs.neptune.ai/integrations-and-supported-tools/intro。
更多示例
Neptune 是一个非常强大的包,可以用于其他机器学习实验相关任务。有关如何一般使用 Neptune 的更多示例,请参阅官方文档页面:https://docs.neptune.ai/getting-started/examples。
在本节中,您已了解 Neptune 及其如何帮助您跟踪超参数调整实验。在下一节中,您将学习如何利用著名的 Scikit-Optimize 包进行超参数调整实验跟踪。
探索 scikit-optimize
您在第七章中介绍了Scikit-Optimize包,通过 Scikit 进行超参数调整,以进行超参数调整实验。在本节中,我们将学习如何利用此包跟踪使用此包进行的所有超参数调整实验。
Scikit-Optimize 提供了非常棒的可视化图表,这些图表总结了测试的超参数值、目标函数分数以及它们之间的关系。本包中有三个图表可用,如上图所示。更多详细信息,请参阅本书 GitHub 仓库中的完整代码。以下图表是基于在 第七章 中提供的相同实验设置生成的,通过 Scikit 进行超参数调整,用于 BOGP 超参数调整方法:
plot_convergence:这个用于可视化每个迭代的超参数调整优化进度:

图 13.7 – 收敛图
plot_evaluations:这个用于可视化优化进化的历史过程。换句话说,它显示了在优化过程中超参数值被采样的顺序。对于每个超参数,都会生成一个探索的超参数值的直方图。对于每个超参数对,都会可视化测试的超参数值的散点图,并配备颜色作为进化历史的图例(从蓝色到黄色):

图 13.8 – 评估图
plot_objective:这个用于可视化目标函数的对应依赖图。这种可视化有助于我们了解测试的超参数值与目标函数分数之间的关系。从这张图中,你可以看到哪个子空间需要更多的关注,以及哪个子空间,甚至哪个超参数,需要在下一次试验中从原始空间中移除:

图 13.9 – 对应依赖图
与 Neptune 集成
Scikit-Optimize 提供了非常信息丰富的可视化模块。然而,它不像 Neptune 包那样支持任何实验版本化功能。为了取长补短,我们可以通过其集成模块将 Scikit-Optimize 与 Neptune 集成。有关更多信息,请参阅官方文档页面:https://docs-legacy.neptune.ai/integrations/skopt.html。
在本节中,你学习了如何利用 Scikit-Optimize 包来帮助你跟踪你的超参数调整实验。在下一节中,你将学习如何利用 Optuna 包进行超参数调整实验跟踪。
探索 Optuna
Optuna 是一个 Python 超参数调整包,它提供了几种超参数调整方法。我们在 第九章 中讨论了如何利用 Optuna 进行超参数调整实验,通过 Optuna 进行超参数调整。在这里,我们将讨论如何利用这个包来跟踪这些实验。
与 Scikit-Optimize 类似,Optuna 提供了非常优秀的可视化模块,帮助我们跟踪超参数调优实验,并作为我们决定下一次试验中搜索哪个子空间的指南。这里展示了四个可利用的可视化模块。所有这些模块都期望以study对象(见第九章,通过 Optuna 进行超参数调优)作为输入。请参阅本书 GitHub 仓库中的完整代码:
plot_contour:这个用于以等高线图的形式可视化超参数(以及目标函数分数)之间的关系:

图 13.10 – 等高线图
plot_optimization_history:这个用于可视化每个迭代的超参数调优优化进度:

图 13.11 – 优化历史图
plot_parallel_coordinate:这个用于以平行坐标图的形式可视化超参数(以及目标函数分数)之间的关系:

图 13.12 – 平行坐标图
plot_slice:这个用于可视化超参数调优方法搜索进化的过程。你可以看到实验中测试过的超参数值,以及搜索过程中哪个子空间受到了更多关注:

图 13.13 – 切片图
Optuna 中所有可视化模块的优点在于它们都是交互式图表,因为它们是使用Plotly可视化包创建的。你可以在图表中放大特定区域,并使用其他交互式功能。
与 Neptune 集成
与 Scikit-Optimize 类似,Optuna 提供了非常丰富的可视化模块。然而,它不像 Neptune 包那样支持任何实验版本化功能。我们可以通过其集成模块将 Optuna 与 Neptune 集成。有关更多信息,请参阅官方文档页面:https://docs-legacy.neptune.ai/integrations/optuna.html。
在本节中,你学习了如何利用 Optuna 包来跟踪你的超参数调优实验。在下一节中,你将学习如何利用 Microsoft NNI 包进行超参数调优实验跟踪。
探索 Microsoft NNI
神经网络智能(NNI)是一个由微软开发的包,不仅可以用于超参数调优任务,还可以用于神经架构搜索、模型压缩和特征工程。我们在第十章,使用 DEAP 和 Microsoft NNI 进行高级超参数调优中讨论了如何利用 NNI 进行超参数调优实验。
在本节中,我们将讨论如何利用此包来跟踪这些实验。NNI 提供的所有实验跟踪模块都位于门户网站。您在第十章中学习了关于门户网站的内容,使用 DEAP 和 Microsoft NNI 进行高级超参数调整。然而,我们还没有深入讨论,还有很多有用的功能您应该了解。
门户网站可以用来可视化所有超参数调整实验的元数据,包括但不限于调整和训练进度、评估指标和错误日志。它还可以用来更新实验的并发性和持续时间,以及重试失败的试验。以下是在 NNI 门户网站中可以用来帮助我们跟踪超参数调整实验的所有重要模块列表。以下图表是基于第十章中所述的相同实验设置生成的,使用 DEAP 和 Microsoft NNI 进行高级超参数调整,针对随机搜索方法。请参阅本书 GitHub 仓库中的完整代码:
- 概览页面显示了我们的超参数调整实验的概览,包括其名称和 ID、状态、开始和结束时间、最佳指标、已过持续时间、按状态分面的试验数量,以及实验路径、训练平台和调整器详情。在这里,您还可以更改最大持续时间、最大试验数量和实验的并发性。还有一个专门的模块显示表现最佳的试验:

图 13.14 – 概览页面
- 试验详情页面显示了关于实验试验的每一个细节,包括所有指标的可视化(见 图 13.15),超参数值的平行图(见 图 13.16),所有试验持续时间的条形图(见 图 13.17),以及显示每个试验在中间步骤趋势的所有中间结果的折线图。我们还可以通过试验作业模块查看每个试验的详细信息,包括但不限于试验的 ID、持续时间、状态、指标、超参数值详情和日志文件(见 图 13.18):

图 13.15 – 试验详情页面
下面的平行图显示了实验中测试过的不同超参数值:

图 13.16 – 超参数值平行图
下面的条形图包含了关于实验中所有试验持续时间的详细信息:

图 13.17 – 试验持续时间条形图
最后,还有试验作业模块:

图 13.18 – 试验作业模块
试验作业模块包括以下内容:
- 侧边栏:我们可以在侧边栏中访问与搜索空间、配置和日志文件相关的所有信息:

图 13.19 – 侧边栏
- 自动刷新按钮:我们还可以通过使用自动刷新按钮来更改 Web 门户的刷新间隔:

图 13.20 – 自动刷新按钮
- 实验摘要按钮:通过点击此按钮,你可以查看当前实验的所有摘要:

图 13.21 – 实验摘要按钮
在本节中,你学习了如何利用 Microsoft NNI 包来跟踪你的超参数调整实验。在下一节中,你将学习如何利用 MLflow 包进行超参数调整实验跟踪。
探索 MLflow
pip install mlflow命令。
要使用 MLflow 跟踪我们的超参数调整实验,我们只需在我们的代码库中添加几个日志函数。一旦我们添加了所需的日志函数,我们只需在命令行中输入mlflow ui命令并打开它,就可以进入提供的 UI 界面localhost:5000。MLflow 提供了许多日志函数,以下是一些你需要了解的主要重要日志函数。请参阅完整的示例代码。
本书 GitHub 仓库中的 ode:
-
create_experiment(): 此函数用于创建一个新的实验。你可以指定实验的名称、标签以及存储实验工件的路由。 -
set_experiment(): 此函数用于将给定的实验名称或 ID 设置为当前活动实验。 -
start_run(): 此函数用于在当前活动实验下启动一个新的 MLflow 运行。建议在with块中使用此函数作为上下文管理器。 -
log_metric(): 此函数用于在当前活动运行中记录单个指标。如果你想进行批量记录,你也可以通过传递指标字典来使用log_metrics()函数。 -
log_param(): 此函数用于在当前活动运行中记录参数或超参数。如果你想进行批量记录,你也可以通过传递指标字典来使用log_params()函数。 -
log_artifact(): 此函数用于将文件或目录记录为当前活动运行的工件。如果你想记录本地目录的所有内容,你也可以使用log_artifacts()函数。 -
set_tag(): 此函数用于为当前活动运行设置一个标签。你必须提供标签的键和值。例如,你可以将键设置为“release_version”,值设置为“1.0.0”。 -
log_figure(): 此函数用于将图形作为当前活动运行的工件进行记录。此函数支持matplotlib和pyplot图形对象类型。 -
log_image(): 此函数用于将图像作为当前活动运行的工件进行记录。此函数支持numpy.ndarray和PIL.image.image对象类型。
MLflow 记录函数
有关 MLfLow 中所有可用记录函数的更多信息,请参阅官方文档页面:https://www.mlflow.org/docs/latest/tracking.html#logging-functions。
MLflow 集成
MLflow 还支持与许多知名开源包的集成,包括但不限于 scikit-learn、TensorFlow、XGBoost、PyTorch 和 Spark。你可以通过利用提供的集成来进行自动记录。有关更多信息,请参阅官方文档页面:https://www.mlflow.org/docs/latest/tracking.html#automatic-logging。
超参数调优用例示例
MLflow 的作者为超参数调优用例提供了示例代码。有关更多信息,请参阅官方 GitHub 仓库:https://github.com/mlflow/mlflow/tree/master/examples/hyperparam。
在本节中,你学习了如何利用 MLflow 包来跟踪你的超参数调优实验。你可以自己开始探索这个包,以更好地理解这个包的工作方式和它的强大功能。
摘要
在本章中,我们讨论了跟踪超参数调优实验的重要性以及常规做法。你还介绍了几个可用的开源包,并学习了如何在实践中利用它们,包括 Neptune、Scikit-Optimize、Optuna、Microsoft NNI 和 MLflow。此时,你应该能够利用你喜欢的包来跟踪你的超参数调优实验,这将提高你工作流程的有效性。
在下一章中,我们将总结本书中讨论的所有主题。我们还将讨论你可以采取的下一步来扩展你的超参数调优知识。
第十四章:第十四章: 结论与下一步
恭喜你完成这本书!在前面的章节中,你已经接触到了许多与超参数调整相关的有趣的概念、方法和实现。本章总结了前几章中学到的重要课程,并将介绍一些你可能从中受益的话题或实现,这些内容我们在这本书中没有涉及。
本章将讨论以下主要内容:
-
重新审视超参数调整方法和包
-
重新审视 HTDM
-
接下来是什么?
重新审视超参数调整方法和包
在本书中,我们讨论了四组超参数调整方法,包括穷举搜索、贝叶斯优化、启发式搜索和多保真优化。每组方法中的所有方法都具有相似的特征。例如,手动搜索、网格搜索和随机搜索,它们都是穷举搜索组的一部分,都是通过穷举搜索超参数空间来工作的,可以归类为无信息搜索方法。
贝叶斯优化超参数调整方法被归类为信息搜索方法,其中所有方法都是通过利用代理模型和获取函数来工作的。启发式搜索组中的超参数调整方法通过试错来工作。至于多保真优化组中的超参数调整方法,它们都利用整个超参数调整管道的廉价近似,这样我们就可以以更少的计算成本和更快的实验时间获得相似的性能结果。
下表总结了本书中讨论的所有超参数调整方法,以及支持的包:
.png)
.jpg)
图 14.1 – 超参数调整方法和包总结
在本节中,我们回顾了本书中讨论的所有超参数调整方法和包。在下一节中,我们将重新审视 HTDM。
重新审视 HTDM
超参数调整决策图(HTDM)是一个你可以用来帮助你决定在特定情况下应该采用哪种超参数调整方法的图。我们在第十一章,“介绍超参数调整决策图”中详细讨论了如何利用 HTDM,以及一些用例。在这里,我们只重新审视一下以下图所示的地图:

图 14.2 – HTDM
在本节中,我们回顾了 HTDM。在下一节中,我们将讨论其他你可能感兴趣的主题,以进一步加深你的超参数调优知识。
接下来是什么?
尽管我们已经讨论了许多超参数调优方法和它们在各种包中的实现,但还有一些重要的概念你可能需要了解,这些概念在本书中没有讨论。至于超参数调优方法,你还可以了解更多关于CMA-ES方法的信息,它是启发式搜索组的一部分(cma-es.github.io/)。你还可以了解更多关于元学习概念的信息,以进一步提高你的贝叶斯优化调优结果的表现(lilianweng.github.io/posts/2018-11-30-meta-learning/)。还值得注意的是,我们可以将手动搜索方法与其他超参数调优方法相结合,以提高我们实验的效率,尤其是在我们已经对超参数值的良好范围有先验知识的情况下。
关于包,你还可以了解更多关于HpBandSter包的信息,该包实现了超带宽、BOHB 和随机搜索方法(github.com/automl/HpBandSter)。最后,还有一些包可以自动从非 scikit-learn 模型创建 scikit-learn 包装器。例如,你可以使用Skorch包从 PyTorch 模型创建 scikit-learn 包装器(skorch.readthedocs.io/en/stable/)。
摘要
在本章中,我们总结了本书所有章节中讨论的所有重要概念。你还被介绍了几种你可能想要学习以进一步加深你的超参数调优知识的新概念。从现在开始,你将拥有控制你的机器学习模型并通过超参数调优实验获得最佳模型以获得最佳结果所需的技能。
感谢你投入兴趣和时间阅读这本书。祝你在超参数调优学习之旅中好运!

订阅我们的在线数字图书馆,全面访问超过 7,000 本书和视频,以及领先的行业工具,帮助您规划个人发展并推进您的职业生涯。更多信息,请访问我们的网站。
第十五章:为什么订阅?
-
通过来自 4,000 多位行业专业人士的实用电子书和视频,节省学习时间,增加编码时间
-
通过为您量身定制的技能计划提高学习效果
-
每月免费获得一本电子书或视频
-
完全可搜索,便于轻松访问关键信息
-
复制粘贴、打印和收藏内容
您知道 Packt 为每本书都提供电子书版本,包括 PDF 和 ePub 文件吗?您可以在 packt.com 升级到电子书版本,并且作为印刷书客户,您有权获得电子书副本的折扣。有关更多详情,请联系我们 customercare@packtpub.com。
在 www.packt.com,您还可以阅读一系列免费技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。
您可能还会喜欢的其他书籍
如果您喜欢这本书,您可能对 Packt 的其他书籍也感兴趣:
)
使用 Python 进行流数据机器学习
Joos Korstanje
ISBN: 9781803248363
- 了解与流数据工作相关的挑战和优势
)
使用 PyTorch 和 Scikit-Learn 进行机器学习
Sebastian Raschka,Yuxi (Hayden) Liu,Vahid Mirjalili
ISBN: 9781801819312
- 探索机器从数据中“学习”的框架、模型和技术
Packt 正在寻找像您这样的作者
如果你有兴趣成为 Packt 的作者,请访问 authors.packtpub.com 并今天申请。我们已经与成千上万的开发者和技术专业人士合作,就像你一样,帮助他们将见解分享给全球科技社区。你可以提交一个一般性申请,申请我们正在招募作者的特定热门话题,或者提交你自己的想法。
分享你的想法
现在你已经完成了 《Python 中的超参数调优》,我们非常想听听你的想法!如果你从亚马逊购买了这本书,请点击此处直接跳转到该书的亚马逊评论页面并分享你的反馈或在该购买网站上留下评论。
你的评论对我们和科技社区都非常重要,它将帮助我们确保我们提供的是高质量的内容。
你可能还会喜欢的其他书籍


)
)
分为两组,即 低于 和 高于 组(参见 图 4.19)。
降低温度。在几何冷却中,初始温度
被冷却因子
乘以
次数,其中
。选择
的值,使得
在
次迭代后仍然保持正值。例如,
,其中
是经过
次迭代后的预期最终温度:
:
的值。
,则用 candidate_set 替换 current_set。
,以及从超参数空间H中随机采样的超参数,
,和
。
参数和一组超参数
对模型M*i执行训练过程的单步。
以确保在最后迭代中至少有一个候选者。
:
:
作为当前 SH 运行的
作为
作为 min_resources 和
作为 n_candidates,按照理解 SH 部分中所述的 SH 过程执行步骤 7 – 11,其中步骤 9. I.被以下程序替换:
浙公网安备 33010602011771号