机器学习算法具有超参数,可让这些算法针对特定的数据集进行量身定制。
尽管通常可以理解超参数的影响,但是可能不知道它们对数据集的特定影响以及它们在学习期间的交互作用。因此,作为机器学习项目的一部分,调整算法超参数的值很重要。
通常使用简单的优化算法来调整超参数,例如网格搜索和随机搜索。另一种方法是使用随机优化算法,例如随机爬山算法。
在本教程中,您将发现如何手动优化机器学习算法的超参数。完成本教程后,您将知道:
- 可以使用随机优化算法代替网格和随机搜索来进行超参数优化。
- 如何使用随机爬山算法调整 Perceptron 算法的超参数。
- 如何手动优化 XGBoost 梯度提升算法的超参数。
教程概述
本教程分为三个部分:他们是:
- 手动超参数优化
- 感知器超参数优化
- XGBoost 超参数优化
手动超参数优化
机器学习模型具有必须设置的超参数,以便针对数据集自定义模型。通常,超参数对模型的一般影响是已知的,但是如何为给定的数据集最佳地设置超参数以及相互作用的超参数的组合具有挑战性。更好的方法是客观地搜索模型超参数的不同值,然后选择一个子集,以使模型在给定的数据集上获得最佳性能。这称为超参数优化或超参数调整。尽管最简单和最常见的两种方法是随机搜索和网格搜索,但是可以使用一系列不同的优化算法。
随机搜索。将搜索空间定义为超参数值的有界域,并在该域中随机采样点。
网格搜索。将搜索空间定义为超参数值的网格,并评估网格中的每个位置。
网格搜索非常适用于抽签检查组合,这些组合通常表现良好。随机搜索非常适合发现和获取您可能不会直观地猜到的超参数组合,尽管它通常需要更多时间来执行。
有关网格和随机搜索以进行超参数调整的更多信息,请参见教程:
随机搜索和网格搜索的超参数优化
https://machinelearningmastery.com/hyperparameter-optimization-with-random-search-and-grid-search/
网格和随机搜索是原始的优化算法,可以使用我们喜欢的任何优化来调整机器学习算法的性能。例如,可以使用随机优化算法。当需要良好或出色的性能并且有足够的资源可用于调整模型时,这可能是理想的。接下来,让我们看看如何使用
感知器超参数优化
Perceptron 算法是最简单的人工神经网络类型。它是单个神经元的模型,可用于两类分类问题,并为以后开发更大的网络提供了基础。在本节中,我们将探索如何手动优化 Perceptron 模型的超参数。首先,让我们定义一个综合二进制分类问题,我们可以将其用作优化模型的重点。我们可以使用make_classification()函数来定义一个包含1,000行和五个输入变量的二进制分类问题。下面的示例创建数据集并总结数据的形状。
- # define a binary classification dataset
- from sklearn.datasets import make_classification
- # define dataset
- X, y = make_classification(n_samples=1000, n_features=5, n_informative=2, n_redundant=1, random_state=1)
- # summarize the shape of the dataset
- print(X.shape, y.shape)
运行示例将打印创建的数据集的形状,从而确认我们的期望。
- (1000, 5) (1000,)
scikit-learn 通过 Perceptron 类提供了 Perceptron 模型的实现。
在调整模型的超参数之前,我们可以使用默认的超参数建立性能基准。
我们将通过 RepeatedStratifiedKFold 类使用重复分层k折交叉验证的良好实践来评估模型。下面列出了在我们的合成二进制分类数据集中使用默认超参数评估 Perceptron 模型的完整示例。
- # perceptron default hyperparameters for binary classification
- from numpy import mean
- from numpy import std
- from sklearn.datasets import make_classification
- from sklearn.model_selection import cross_val_score
- from sklearn.model_selection import RepeatedStratifiedKFold
- from sklearn.linear_model import Perceptron
- # define dataset
- X, y = make_classification(n_samples=1000, n_features=5, n_informative=2, n_redundant=1, random_state=1)
- # define model
- model = Perceptron()
- # define evaluation procedure
- cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
- # evaluate model
- scores = cross_val_score(model, X, y, scoring='accuracy', cvcv=cv, n_jobs=-1)
- # report result
- print('Mean Accuracy: %.3f (%.3f)' % (mean(scores), std(scores)))
运行示例报告将评估模型,并报告分类准确性的平均值和标准偏差。
注意:由于算法或评估程序的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑运行该示例几次并比较平均结果。
在这种情况下,我们可以看到具有默认超参数的模型实现了约78.5%的分类精度。
我们希望通过优化的超参数可以实现比此更好的性能。
- Mean Accuracy: 0.786 (0.069)
接下来,我们可以使用随机爬山算法优化 Perceptron 模型的超参数。我们可以优化许多超参数,尽管我们将重点放在可能对模型的学习行为影响最大的两个参数上。他们是:
- 学习率(eta0)
- 正则化(alpha)
学习率控制基于预测误差的模型更新量,并控制学习速度。eta的默认值为1.0。合理的值应大于零(例如,大于1e-8或1e-10),并且可能小于1.0默认情况下,Perceptron 不使用任何正则化但是我们将启用“弹性网”正则化,在学习过程中同时应用L1和L2正则化。这将鼓励模型寻求较小的模型权重,从而往往获得更好的性能。我们将调整用于控制正则化权重的“ alpha”超参数,例如它影响学习的数量。如果设置为0.0,则好像没有使用正则化。合理的值在0.0到1.0之间。首先,我们需要为优化算法定义目标函数。我们将使用平均分类精度和重复的分层k折交叉验证来评估配置。我们将努力使配置的准确性最大化。下面的 Objective() 函数实现了这一点,采用了数据集和配置值列表。将配置值(学习率和正则化权重)解压缩,用于配置模型,然后对模型进行评估,并返回平均准确度。
- # objective function
- def objective(X, y, cfg):
- # unpack config
- eta, alpha = cfg
- # define model
- model = Perceptron(penalty='elasticnet', alphaalpha=alpha, etaeta0=eta)
- # define evaluation procedure
- cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
- # evaluate model
- scores = cross_val_score(model, X, y, scoring='accuracy', cvcv=cv, n_jobs=-1)
- # calculate mean accuracy
- result = mean(scores)
- return result
接下来,我们需要一个函数来在搜索空间中迈出一步。搜索空间由两个变量(eta和alpha)定义。搜索空间中的某个步骤必须与先前的值有一定关系,并且必须绑定到合理的值(例如0到1之间)。我们将使用“步长”超参数来控制允许算法从现有配置移动多远。使用高斯分布以当前值作为分布的平均值,以步长作为分布的标准偏差来概率地选择新的配置。我们可以使用randn(),NumPy函数生成具有高斯分布的随机数。下面的step()函数实现了这一点,并将在搜索空间中迈出一步,并使用现有配置生成新配置。
- # take a step in the search space
- def step(cfg, step_size):
- # unpack the configuration
- eta, alpha = cfg
- # step eta
- new_eta = eta + randn() * step_size
- # check the bounds of eta
- if new_eta <= 0.0:
- new_eta = 1e-8
- # step alpha
- new_alpha = alpha + randn() * step_size
- # check the bounds of alpha
- if new_alpha < 0.0:
- new_alpha = 0.0
- # return the new configuration
- return [new_eta, new_alpha]
接下来,我们需要实现随机爬山算法,该算法将调用我们的Objective()函数来评估候选解,而我们的step()函数将在搜索空间中迈出一步。搜索首先生成一个随机初始解,在这种情况下,eta和alpha值在0到1范围内。然后评估初始解并将其视为当前最佳工作解。
- # starting point for the search
- solution = [rand(), rand()]
- # evaluate the initial point
- solution_eval = objective(X, y, solution)
接下来,该算法将迭代进行固定次数的迭代,作为提供给搜索的超参数。每次迭代都需要采取步骤并评估新的候选解决方案。
- # take a step
- candidate = step(solution, step_size)
- # evaluate candidate point
- candidte_eval = objective(X, y, candidate)
如果新解决方案比当前工作解决方案好,则将其视为新的当前工作解决方案。
- # check if we should keep the new point
- if candidte_eval >= solution_eval:
- # store the new point
- solution, solution_eval = candidate, candidte_eval
- # report progress
- print('>%d, cfg=%s %.5f' % (i, solution, solution_eval))
搜索结束时,将返回最佳解决方案及其性能。结合在一起,下面的hillclimbing()函数以数据集,目标函数,迭代次数和步长为参数,实现了用于调整 Perceptron 算法的随机爬山算法。
- # hill climbing local search algorithm
- def hillclimbing(X, y, objective, n_iter, step_size):
- # starting point for the search
- solution = [rand(), rand()]
- # evaluate the initial point
- solution_eval = objective(X, y, solution)
- # run the hill climb
- for i in range(n_iter):
- # take a step
- candidate = step(solution, step_size)
- # evaluate candidate point
- candidte_eval = objective(X, y, candidate)
- # check if we should keep the new point
- if candidte_eval >= solution_eval:
- # store the new point
- solution, solution_eval = candidate, candidte_eval
- # report progress
- print('>%d, cfg=%s %.5f' % (i, solution, solution_eval))
- return [solution, solution_eval]
然后,我们可以调用算法并报告搜索结果。在这种情况下,我们将运行该算法100次迭代,并使用0.1步长,这是在经过反复试验后选择的。
- # define the total iterations
- n_iter = 100
- # step size in the search space
- step_size = 0.1
- # perform the hill climbing search
- cfg, score = hillclimbing(X, y, objective, n_iter, step_size)
- print('Done!')
- print('cfg=%s: Mean Accuracy: %f' % (cfg, score))
结合在一起,下面列出了手动调整 Perceptron 算法的完整示例。
- # manually search perceptron hyperparameters for binary classification
- from numpy import mean
- from numpy.random import randn
- from numpy.random import rand
- from sklearn.datasets import make_classification
- from sklearn.model_selection import cross_val_score
- from sklearn.model_selection import RepeatedStratifiedKFold
- from sklearn.linear_model import Perceptron
- # objective function
- def objective(X, y, cfg):
- # unpack config
- eta, alpha = cfg
- # define model
- model = Perceptron(penalty='elasticnet', alphaalpha=alpha, etaeta0=eta)
- # define evaluation procedure
- cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
- # evaluate model
- scores = cross_val_score(model, X, y, scoring='accuracy', cvcv=cv, n_jobs=-1)
- # calculate mean accuracy
- result = mean(scores)
- return result
- # take a step in the search space
- def step(cfg, step_size):
- # unpack the configuration
- eta, alpha = cfg
- # step eta
- new_eta = eta + randn() * step_size
- # check the bounds of eta
- if new_eta <= 0.0:
- new_eta = 1e-8
- # step alpha
- new_alpha = alpha + randn() * step_size
- # check the bounds of alpha
- if new_alpha < 0.0:
- new_alpha = 0.0
- # return the new configuration
- return [new_eta, new_alpha]
- # hill climbing local search algorithm
- def hillclimbing(X, y, objective, n_iter, step_size):
- # starting point for the search
- solution = [rand(), rand()]
- # evaluate the initial point
- solution_eval = objective(X, y, solution)
- # run the hill climb
- for i in range(n_iter):
- # take a step
- candidate = step(solution, step_size)
- # evaluate candidate point
- candidte_eval = objective(X, y, candidate)
- # check if we should keep the new point
- if candidte_eval >= solution_eval:
- # store the new point
- solution, solution_eval = candidate, candidte_eval
- # report progress
- print('>%d, cfg=%s %.5f' % (i, solution, solution_eval))
- return [solution, solution_eval]
- # define dataset
- X, y = make_classification(n_samples=1000, n_features=5, n_informative=2, n_redundant=1, random_state=1)
- # define the total iterations
- n_iter = 100
- # step size in the search space
- step_size = 0.1
- # perform the hill climbing search
- cfg, score = hillclimbing(X, y, objective, n_iter, step_size)
- print('Done!')
- print('cfg=%s: Mean Accuracy: %f' % (cfg, score))
运行示例将在每次搜索过程中看到改进时报告配置和结果。运行结束时,将报告最佳配置和结果。
注意:由于算法或评估程序的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑运行该示例几次并比较平均结果。
在这种情况下,我们可以看到,最好的结果涉及在1.004处使用略高于1的学习率和约0.002的正则化权重,从而获得约79.1%的平均准确度,比默认配置好于约78.5%的准确度 。
- >0, cfg=[0.5827274503894747, 0.260872709578015] 0.70533
- >4, cfg=[0.5449820307807399, 0.3017271170801444] 0.70567
- >6, cfg=[0.6286475606495414, 0.17499090243915086] 0.71933
- >7, cfg=[0.5956196828965779, 0.0] 0.78633
- >8, cfg=[0.5878361167354715, 0.0] 0.78633
- >10, cfg=[0.6353507984485595, 0.0] 0.78633
- >13, cfg=[0.5690530537610675, 0.0] 0.78633
- >17, cfg=[0.6650936023999641, 0.0] 0.78633
- >22, cfg=[0.9070451625704087, 0.0] 0.78633
- >23, cfg=[0.9253366187387938, 0.0] 0.78633
- >26, cfg=[0.9966143540220266, 0.0] 0.78633
- >31, cfg=[1.0048613895650054, 0.002162219228449132] 0.79133
- Done!
- cfg=[1.0048613895650054, 0.002162219228449132]: Mean Accuracy: 0.791333
既然我们已经熟悉了如何使用随机爬山算法来调整简单的机器学习算法的超参数,那么让我们来看看如何调整更高级的算法,例如 XGBoost 。
XGBoost超参数优化
XGBoost 是 Extreme Gradient Boosting 的缩写,是随机梯度提升机器学习算法的有效实现。随机梯度增强算法(也称为梯度增强机或树增强)是一种功能强大的机器学习技术,可在各种具有挑战性的机器学习问题上表现出色,甚至表现最佳。首先,必须安装XGBoost库。您可以使用pip安装它,如下所示:
- sudo pip install xgboost
一旦安装,您可以通过运行以下代码来确认它已成功安装,并且您正在使用现代版本:
- # xgboost
- import xgboost
- print("xgboost", xgboost.__version__)
运行代码,您应该看到以下版本号或更高版本
- xgboost 1.0.1
尽管XGBoost库具有自己的 Python API,但我们可以通过 XGBClassifier 包装器类将 XGBoost 模型与 scikit-learn API 结合使用。可以实例化模型的实例,就像将其用于模型评估的任何其他 scikit-learn 类一样使用。例如:
- # define model
- model = XGBClassifier()
在调整 XGBoost 的超参数之前,我们可以使用默认的超参数建立性能基准。我们将使用与上一节相同的合成二进制分类数据集,并使用重复分层k折交叉验证的相同测试工具。下面列出了使用默认超参数评估 XGBoost 性能的完整示例。
- # xgboost with default hyperparameters for binary classification
- from numpy import mean
- from numpy import std
- from sklearn.datasets import make_classification
- from sklearn.model_selection import cross_val_score
- from sklearn.model_selection import RepeatedStratifiedKFold
- from xgboost import XGBClassifier
- # define dataset
- X, y = make_classification(n_samples=1000, n_features=5, n_informative=2, n_redundant=1, random_state=1)
- # define model
- model = XGBClassifier()
- # define evaluation procedure
- cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
- # evaluate model
- scores = cross_val_score(model, X, y, scoring='accuracy', cvcv=cv, n_jobs=-1)
- # report result
- print('Mean Accuracy: %.3f (%.3f)' % (mean(scores), std(scores)))
通过运行示例,可以评估模型并报告分类精度的平均值和标准偏差。
注意:由于算法或评估程序的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑运行该示例几次并比较平均结果。在这种情况下,我们可以看到具有默认超参数的模型实现了约84.9%的分类精度。我们希望通过优化的超参数可以实现比此更好的性能。
- Mean Accuracy: 0.849 (0.040)
接下来,我们可以采用随机爬山优化算法来调整 XGBoost 模型的超参数。我们可能要针对 XGBoost 模型优化许多超参数。
有关如何调优 XGBoost 模型的概述,请参见教程:
如何配置梯度提升算法
https://machinelearningmastery.com/configure-gradient-boosting-algorithm/
我们将关注四个关键的超参数。他们是:
- 学习率(learning_rate)
- 树数(n_estimators)
- 子样本百分比(子样本)
- 树深(最大深度)
学习速度控制着每棵树对整体的贡献。明智的值应小于1.0,而应稍高于0.0(例如1e-8)。树木的数量控制着合奏的大小,通常,越多的树木越好,以至于收益递减。合理的值在1棵树与数百或数千棵树之间。子样本百分比定义用于训练每棵树的随机样本大小,定义为原始数据集大小的百分比。值介于略高于0.0(例如1e-8)和1.0的值之间树的深度是每棵树中的级别数。较深的树更特定于训练数据集,并且可能过度拟合。较短的树通常可以更好地概括。明智的值是1到10或20之间。首先,我们必须更新Objective()函数以解包XGBoost模型的超参数,对其进行配置,然后评估平均分类精度。
- # objective function
- def objective(X, y, cfg):
- # unpack config
- lrate, n_tree, subsam, depth = cfg
- # define model
- model = XGBClassifier(learning_rate=lrate, n_estimators=n_tree, subsamsubsample=subsam, max_depth=depth)
- # define evaluation procedure
- cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
- # evaluate model
- scores = cross_val_score(model, X, y, scoring='accuracy', cvcv=cv, n_jobs=-1)
- # calculate mean accuracy
- result = mean(scores)
- return result
接下来,我们需要定义用于在搜索空间中迈出一步的step()函数。
每个超参数的范围都非常不同,因此,我们将分别为每个超参数定义步长(分布的标准偏差)。为了使事情保持简单,我们还将在线定义步长,而不是将其定义为函数的参数。
树的数量和深度是整数,因此步进值是四舍五入的。选定的步长是任意的,是在经过反复试验后选择的。下面列出了更新的步进功能。
- # take a step in the search space
- def step(cfg):
- # unpack config
- lrate, n_tree, subsam, depth = cfg
- # learning rate
- lratelrate = lrate + randn() * 0.01
- if lrate <= 0.0:
- lrate = 1e-8
- if lrate > 1:
- lrate = 1.0
- # number of trees
- n_tree = round(n_tree + randn() * 50)
- if n_tree <= 0.0:
- n_tree = 1
- # subsample percentage
- subsamsubsam = subsam + randn() * 0.1
- if subsam <= 0.0:
- subsam = 1e-8
- if subsam > 1:
- subsam = 1.0
- # max tree depth
- depth = round(depth + randn() * 7)
- if depth <= 1:
- depth = 1
- # return new config
- return [lrate, n_tree, subsam, depth]
最后,必须更新hillclimbing()算法,以定义具有适当值的初始解。在这种情况下,我们将使用合理的默认值,匹配默认的超参数或接近它们来定义初始解决方案。
- # starting point for the search
- solution = step([0.1, 100, 1.0, 7])
结合在一起,下面列出了使用随机爬山算法手动调整 XGBoost 算法的超参数的完整示例。
- # xgboost manual hyperparameter optimization for binary classification
- from numpy import mean
- from numpy.random import randn
- from numpy.random import rand
- from numpy.random import randint
- from sklearn.datasets import make_classification
- from sklearn.model_selection import cross_val_score
- from sklearn.model_selection import RepeatedStratifiedKFold
- from xgboost import XGBClassifier
- # objective function
- def objective(X, y, cfg):
- # unpack config
- lrate, n_tree, subsam, depth = cfg
- # define model
- model = XGBClassifier(learning_rate=lrate, n_estimators=n_tree, subsamsubsample=subsam, max_depth=depth)
- # define evaluation procedure
- cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
- # evaluate model
- scores = cross_val_score(model, X, y, scoring='accuracy', cvcv=cv, n_jobs=-1)
- # calculate mean accuracy
- result = mean(scores)
- return result
- # take a step in the search space
- def step(cfg):
- # unpack config
- lrate, n_tree, subsam, depth = cfg
- # learning rate
- lratelrate = lrate + randn() * 0.01
- if lrate <= 0.0:
- lrate = 1e-8
- if lrate > 1:
- lrate = 1.0
- # number of trees
- n_tree = round(n_tree + randn() * 50)
- if n_tree <= 0.0:
- n_tree = 1
- # subsample percentage
- subsamsubsam = subsam + randn() * 0.1
- if subsam <= 0.0:
- subsam = 1e-8
- if subsam > 1:
- subsam = 1.0
- # max tree depth
- depth = round(depth + randn() * 7)
- if depth <= 1:
- depth = 1
- # return new config
- return [lrate, n_tree, subsam, depth]
- # hill climbing local search algorithm
- def hillclimbing(X, y, objective, n_iter):
- # starting point for the search
- solution = step([0.1, 100, 1.0, 7])
- # evaluate the initial point
- solution_eval = objective(X, y, solution)
- # run the hill climb
- for i in range(n_iter):
- # take a step
- candidate = step(solution)
- # evaluate candidate point
- candidte_eval = objective(X, y, candidate)
- # check if we should keep the new point
- if candidte_eval >= solution_eval:
- # store the new point
- solution, solution_eval = candidate, candidte_eval
- # report progress
- print('>%d, cfg=[%s] %.5f' % (i, solution, solution_eval))
- return [solution, solution_eval]
- # define dataset
- X, y = make_classification(n_samples=1000, n_features=5, n_informative=2, n_redundant=1, random_state=1)
- # define the total iterations
- n_iter = 200
- # perform the hill climbing search
- cfg, score = hillclimbing(X, y, objective, n_iter)
- print('Done!')
- print('cfg=[%s]: Mean Accuracy: %f' % (cfg, score))
运行示例将在每次搜索过程中看到改进时报告配置和结果。运行结束时,将报告最佳配置和结果。
注意:由于算法或评估程序的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑运行该示例几次并比较平均结果。
在这种情况下,我们可以看到最好的结果涉及使用大约0.02的学习率,52棵树,大约50%的子采样率以及53个级别的较大深度。此配置产生的平均准确度约为87.3%,优于默认配置的平均准确度约为84.9%。
- >0, cfg=[[0.1058242692126418, 67, 0.9228490731610172, 12]] 0.85933
- >1, cfg=[[0.11060813799692253, 51, 0.859353656735739, 13]] 0.86100
- >4, cfg=[[0.11890247679234153, 58, 0.7135275461723894, 12]] 0.86167
- >5, cfg=[[0.10226257987735601, 61, 0.6086462443373852, 17]] 0.86400
- >15, cfg=[[0.11176962034280596, 106, 0.5592742266405146, 13]] 0.86500
- >19, cfg=[[0.09493587069112454, 153, 0.5049124222437619, 34]] 0.86533
- >23, cfg=[[0.08516531024154426, 88, 0.5895201311518876, 31]] 0.86733
- >46, cfg=[[0.10092590898175327, 32, 0.5982811365027455, 30]] 0.86867
- >75, cfg=[[0.099469211050998, 20, 0.36372573610040404, 32]] 0.86900
- >96, cfg=[[0.09021536590375884, 38, 0.4725379807796971, 20]] 0.86900
- >100, cfg=[[0.08979482274655906, 65, 0.3697395430835758, 14]] 0.87000
- >110, cfg=[[0.06792737273465625, 89, 0.33827505722318224, 17]] 0.87000
- >118, cfg=[[0.05544969684589669, 72, 0.2989721608535262, 23]] 0.87200
- >122, cfg=[[0.050102976159097, 128, 0.2043203965148931, 24]] 0.87200
- >123, cfg=[[0.031493266763680444, 120, 0.2998819062922256, 30]] 0.87333
- >128, cfg=[[0.023324201169625292, 84, 0.4017169945431015, 42]] 0.87333
- >140, cfg=[[0.020224220443108752, 52, 0.5088096815056933, 53]] 0.87367
- Done!
- cfg=[[0.020224220443108752, 52, 0.5088096815056933, 53]]: Mean Accuracy: 0.873667