Python 超参数调优(二)
原文:
annas-archive.org/md5/4ecab6b29f49ea32658516b579140840译者:飞龙
第八章:第八章:通过 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 – 损失值与执行时间的关系
图 8.1 – 损失值与执行时间的关系
现在,我们必须绘制所有试验的目标函数分数的直方图:
plotting.main_plot_histogram(trials)
我们将得到以下输出。
![图 8.2 – 所有试验的目标函数分数直方图
图 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 – 每个超参数分布与试验次数之间的关系
图 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 包的所有重要内容。我们还学习了如何借助这些包实现各种超参数调优方法,以及理解每个类的重要参数以及它们与我们之前章节中学到的理论之间的关系。从现在开始,你应该能够利用这些包来实现你选择的超参数调优方法,并最终提高你的机器学习模型性能。凭借第三章至第六章的知识,你还将能够调试代码,如果出现错误或意外结果,并且能够制定自己的实验配置以匹配你的特定问题。
在下一章中,我们将学习几种流行算法的超参数。每个算法都会有广泛的解释,包括但不限于每个超参数的定义、当每个超参数的值发生变化时将产生什么影响,以及基于影响的超参数优先级列表。