使用 Hyperopt 和 Plotly 可视化超参数优化
最强 Python 数据可视化库,没有之一!关注"Python学习与数据挖掘"
设为“置顶或星标”,第一时间送达干货
来自数据STUDIO
本文将演示如何创建超参数设置的有效交互式可视化,使我们能够了解在超参数优化期间尝试的超参数设置之间的关系。本文的第 1 部分将使用 hyperopt 设置一个简单的超参数优化示例。在第 2 部分中,我们将展示如何使用Plotly创建由第 1 部分中的超参数优化生成的数据的交互式可视化。
至今,很多大佬对“超参数优化”算法进行了大量研究,这些算法在进行少量配置后会自动搜索最佳超参数集。这些算法可以通过各种 Python 包实现。例如hyperopt
就是其中一个广泛使用的超参数优化框架包,它允许数据科学家通过定义目标函数和声明搜索空间来利用几种强大的算法进行超参数优化。
写在前面
from functools import partial
from pprint import pprint
import numpy as np
import pandas as pd
from hyperopt import fmin, hp, space_eval, tpe, STATUS_OK, Trials
from hyperopt.pyll import scope, stochastic
from plotly import express as px
from plotly import graph_objects as go
from plotly import offline as pyo
from sklearn.datasets import load_boston
from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor
from sklearn.metrics import make_scorer, mean_squared_error
from sklearn.model_selection import cross_val_score, KFold
from sklearn.utils import check_random_state
pyo.init_notebook_mode()
使用 hyperopt 超参数优化示例
选择和加载数据集 声明超参数搜索空间 定义目标函数 运行超参数优化
选择和加载数据集
load_boston
。我们将使用此函数将数据集加载到 Pandas 数据框中,如下所示:MEDIAN_HOME_VALUE = "median_home_value"
# 使用 sklearn 的辅助函数加载波士顿数据集
boston_dataset = load_boston()
# 将数据转换为 Pandas 数据框
data = np.concatenate(
[boston_dataset["data"], boston_dataset["target"][..., np.newaxis]],
axis=1,
)
features, target = boston_dataset["feature_names"], MEDIAN_HOME_VALUE
columns = np.concatenate([features, [target]])
boston_dataset_df = pd.DataFrame(data, columns=columns)
boston_dataset_df
定义超参数搜索空间
# 定义常量字符串,我们将在下面的“search space”字典中用作键。
# 注意,我在整个过程中使用的约定是,
# 用一个匹配该字符串的变量来表示字符串中的字符,只是变量中的字符是大写的。
# 这种约定允许我们在代码中遇到这些变量时很容易解释它们的含义。
# 例如,我们知道变量' MODEL '包含字符串" MODEL "。
# 用变量表示字符串的这种模式允许我在代码中重复使用同一个字符串时避免键入错误,
# 因为在变量名中键入错误将被检查器捕获为错误。
GRADIENT_BOOSTING_REGRESSOR = "gradient_boosting_regressor"
KWARGS = "kwargs"
LEARNING_RATE = "learning_rate"
LINEAR_REGRESSION = "linear_regression"
MAX_DEPTH = "max_depth"
MODEL = "model"
MODEL_CHOICE = "model_choice"
NORMALIZE = "normalize"
N_ESTIMATORS = "n_estimators"
RANDOM_FOREST_REGRESSOR = "random_forest_regressor"
RANDOM_STATE = "random_state"
# 声明随机森林回归模型的搜索空间。
random_forest_regressor = {
MODEL: RANDOM_FOREST_REGRESSOR,
# 我将模型参数定义为一个单独的字典,以便我们可以将参数输入
# 带有字典解包的模型的`__init__`。参见 `sample_to_model` 函数
# 与目标函数一起定义以查看此操作
KWARGS: {
N_ESTIMATORS: scope.int(
hp.quniform(f"{RANDOM_FOREST_REGRESSOR}__{N_ESTIMATORS}", 50, 150, 1)
),
MAX_DEPTH: scope.int(
hp.quniform(f"{RANDOM_FOREST_REGRESSOR}__{MAX_DEPTH}", 2, 12, 1)
),
RANDOM_STATE: 0,
},
}
# 声明梯度提升回归模型的搜索空间,
# 结构与随机森林回归搜索空间相同。
gradient_boosting_regressor = {
MODEL: GRADIENT_BOOSTING_REGRESSOR,
KWARGS: {
LEARNING_RATE: scope.float(
hp.uniform(f"{GRADIENT_BOOSTING_REGRESSOR}__{LEARNING_RATE}", 0.01, 0.15,)
), # lower learning rate
N_ESTIMATORS: scope.int(
hp.quniform(f"{GRADIENT_BOOSTING_REGRESSOR}__{N_ESTIMATORS}", 50, 150, 1)
),
MAX_DEPTH: scope.int(
hp.quniform(f"{GRADIENT_BOOSTING_REGRESSOR}__{MAX_DEPTH}", 2, 12, 1)
),
RANDOM_STATE: 0,
},
}
# 将两个模型搜索空间与两个模型之间的顶级“choice”结合起来,得到最终的搜索空间。
space = {
MODEL_CHOICE: hp.choice(
MODEL_CHOICE, [random_forest_regressor, gradient_boosting_regressor,],
)
}
定义目标函数
# 定义几个额外的变量来表示字符串。注意,这段代码期望我们能够
# 访问之前在"search space"代码片段中定义的所有变量。
LOSS = "loss"
STATUS = "status"
# 从字符串名称映射到模型类定义对象,我们将使用该对象
# 从hyperopt搜索空间生成的样本创建模型的初始化版本。
MODELS = {
GRADIENT_BOOSTING_REGRESSOR: GradientBoostingRegressor,
RANDOM_FOREST_REGRESSOR: RandomForestRegressor,
}
# 创建一个我们将在目标中使用的评分函数
mse_scorer = make_scorer(mean_squared_error)
# 从hyperopt生成的示例转换为初始化模型的辅助函数。
# 注意,因为我们在搜索空间声明中将模型类型和模型关键字-参数分割成单独的键-值对,# 所以我们能够使用字典解包来创建模型的初始化版本。
def sample_to_model(sample):
kwargs = sample[MODEL_CHOICE][KWARGS]
return MODELS[sample[MODEL_CHOICE][MODEL]](**kwargs "sample[MODEL_CHOICE][MODEL]")
# 定义hyperopt的目标函数。我们将使用 `functools.partial` 修复`dataset`, `features`, 和 `target` 参数。
# 来创建这个函数的那个版本, 并将其作为参数提供给 `fmin`
def objective(sample, dataset_df, features, target):
model = sample_to_model(sample)
rng = check_random_state(0)
# 处理随机洗牌时创建折叠。在现实中,
# 我们可能需要比上述生成的固定“RandomState”实例更好的策略来管理随机性。
cv = KFold(n_splits=10, random_state=rng, shuffle=True)
# 计算每一次的平均均方误差。由于`n_splits` 是10,`mse` 将是一个大小为10的数组,
# 每个元素表示一次折叠的平均平均平方误差。
mse = cross_val_score(
model,
dataset_df.loc[:, features],
dataset_df.loc[:, target],
scoring=mse_scorer,
cv=cv,
)
# 返回所有折叠的均方误差的平均值。
return {LOSS: np.mean(mse), STATUS: STATUS_OK}
运行超参数优化
fmin
函数运行一千次试验的超参数优化。重要的是,我们将提供一个Trials
对象的实例,hyperopt 将在其中记录超参数优化的每次迭代的超参数设置。我们将从这个Trials
实例中提取可视化数据。运行以下代码执行超参数优化:# 我们自定义的目标函数是通用的数据集,
# 我们需要使用`partial` 从`functools` 模块来"fix"这个`dataset_df`, `features`, 和 `target` 的参数值,
# 希望在这个例子中,我们有一个目标函数只接受一个参数假设的“hyperopt”界面。
boston_objective = partial(
objective, dataset_df=boston_dataset_df, features=features, target=MEDIAN_HOME_VALUE
)
# `hyperopt` 跟踪这个`Trials`对象的每次迭代的结果。
# 我们将从这个对象中收集用于可视化的数据。
trials = Trials()
rng = check_random_state(0) # reproducibility!
# `fmin`搜索“minimize”我们的对象的超参数,均方误差,并返回超参数的“best”集。
best = fmin(boston_objective, space, tpe.suggest, 1000, trials=trials, rstate=rng)
超参数优化可视化
trials
变量以查看 hyperopt 为前五个试验选择了哪些设置,如下所示:pprint([t for t in trials][:5])
[{'book_time': datetime.datetime(2020, 11, 4, 0, 51, 42, 199000),
'exp_key': None,
'misc': {'cmd': ('domain_attachment', 'FMinIter_Domain'),
'idxs': {'gradient_boosting_regressor__learning_rate': [],
'gradient_boosting_regressor__max_depth': [],
'gradient_boosting_regressor__n_estimators': [],
'model_choice': [0],
'random_forest_regressor__max_depth': [0],
'random_forest_regressor__n_estimators': [0]},
'tid': 0,
'vals': {'gradient_boosting_regressor__learning_rate': [],
'gradient_boosting_regressor__max_depth': [],
'gradient_boosting_regressor__n_estimators': [],
'model_choice': [0],
'random_forest_regressor__max_depth': [5.0],
'random_forest_regressor__n_estimators': [90.0]},
'workdir': None},
'owner': None,
'refresh_time': datetime.datetime(2020, 11, 4, 0, 51, 46, 83000),
'result': {'loss': 16.359897953574603, 'status': 'ok'},
'spec': None,
'state': 2,
'tid': 0,
'version': 0},
{'book_time': datetime.datetime(2020, 11, 4, 0, 51, 46, 92000),
'exp_key': None,
'misc': {'cmd': ('domain_attachment', 'FMinIter_Domain'),
'idxs': {'gradient_boosting_regressor__learning_rate': [1],
'gradient_boosting_regressor__max_depth': [1],
'gradient_boosting_regressor__n_estimators': [1],
'model_choice': [1],
'random_forest_regressor__max_depth': [],
'random_forest_regressor__n_estimators': []},
'tid': 1,
'vals': {'gradient_boosting_regressor__learning_rate': [0.03819110609989756],
'gradient_boosting_regressor__max_depth': [8.0],
'gradient_boosting_regressor__n_estimators': [137.0],
'model_choice': [1],
'random_forest_regressor__max_depth': [],
'random_forest_regressor__n_estimators': []},
'workdir': None},
'owner': None,
'refresh_time': datetime.datetime(2020, 11, 4, 0, 51, 52, 70000),
'result': {'loss': 18.045981512632412, 'status': 'ok'},
'spec': None,
'state': 2,
'tid': 1,
'version': 0},
{'book_time': datetime.datetime(2020, 11, 4, 0, 51, 52, 81000),
'exp_key': None,
'misc': {'cmd': ('domain_attachment', 'FMinIter_Domain'),
'idxs': {'gradient_boosting_regressor__learning_rate': [2],
'gradient_boosting_regressor__max_depth': [2],
'gradient_boosting_regressor__n_estimators': [2],
'model_choice': [2],
'random_forest_regressor__max_depth': [],
'random_forest_regressor__n_estimators': []},
'tid': 2,
'vals': {'gradient_boosting_regressor__learning_rate': [0.08587985607913044],
'gradient_boosting_regressor__max_depth': [12.0],
'gradient_boosting_regressor__n_estimators': [95.0],
'model_choice': [1],
'random_forest_regressor__max_depth': [],
'random_forest_regressor__n_estimators': []},
'workdir': None},
'owner': None,
'refresh_time': datetime.datetime(2020, 11, 4, 0, 51, 57, 519000),
'result': {'loss': 21.235091223167437, 'status': 'ok'},
'spec': None,
'state': 2,
'tid': 2,
'version': 0},
{'book_time': datetime.datetime(2020, 11, 4, 0, 51, 57, 528000),
'exp_key': None,
'misc': {'cmd': ('domain_attachment', 'FMinIter_Domain'),
'idxs': {'gradient_boosting_regressor__learning_rate': [],
'gradient_boosting_regressor__max_depth': [],
'gradient_boosting_regressor__n_estimators': [],
'model_choice': [3],
'random_forest_regressor__max_depth': [3],
'random_forest_regressor__n_estimators': [3]},
'tid': 3,
'vals': {'gradient_boosting_regressor__learning_rate': [],
'gradient_boosting_regressor__max_depth': [],
'gradient_boosting_regressor__n_estimators': [],
'model_choice': [0],
'random_forest_regressor__max_depth': [2.0],
'random_forest_regressor__n_estimators': [93.0]},
'workdir': None},
'owner': None,
'refresh_time': datetime.datetime(2020, 11, 4, 0, 52, 0, 661000),
'result': {'loss': 23.582397665666413, 'status': 'ok'},
'spec': None,
'state': 2,
'tid': 3,
'version': 0},
{'book_time': datetime.datetime(2020, 11, 4, 0, 52, 0, 670000),
'exp_key': None,
'misc': {'cmd': ('domain_attachment', 'FMinIter_Domain'),
'idxs': {'gradient_boosting_regressor__learning_rate': [4],
'gradient_boosting_regressor__max_depth': [4],
'gradient_boosting_regressor__n_estimators': [4],
'model_choice': [4],
'random_forest_regressor__max_depth': [],
'random_forest_regressor__n_estimators': []},
'tid': 4,
'vals': {'gradient_boosting_regressor__learning_rate': [0.0638511443414372],
'gradient_boosting_regressor__max_depth': [5.0],
'gradient_boosting_regressor__n_estimators': [72.0],
'model_choice': [1],
'random_forest_regressor__max_depth': [],
'random_forest_regressor__n_estimators': []},
'workdir': None},
'owner': None,
'refresh_time': datetime.datetime(2020, 11, 4, 0, 52, 2, 875000),
'result': {'loss': 15.253327611719737, 'status': 'ok'},
'spec': None,
'state': 2,
'tid': 4,
'version': 0}]
# 这是一个简单的辅助函数,当一个特定的超参数与一个特定的试验无关时,
# 允许我们填充`np.nan`。
def unpack(x):
if x:
return x[0]
return np.nan
# 我们将首先将每个试验转换为一个系列,然后将这些系列堆叠在一起作为一个数据框架。
trials_df = pd.DataFrame([pd.Series(t["misc"]["vals"]).apply(unpack) for t in trials])
# 然后,我们将添加其他相关的信息到正确的行,并执行一些方便的映射
trials_df["loss"] = [t["result"]["loss"] for t in trials]
trials_df["trial_number"] = trials_df.index
trials_df[MODEL_CHOICE] = trials_df[MODEL_CHOICE].apply(
lambda x: RANDOM_FOREST_REGRESSOR if x == 0 else GRADIENT_BOOSTING_REGRESSOR
使用 Plotly Express 绘制试验数量与损失
scatter
方法并指出我们想要使用哪些列作为 x 和 y 值:# px是“express”的别名,它是按照导入“express”的约定通过运行
# “from plotly import express as px”创建的。
fig = px.scatter(trials_df, x="trial_number", y="loss")
color
在方法调用中添加一个参数,scatter
如下所示:fig = px.scatter(trials_df,
x="trial_number",
y="loss",
color=MODEL_CHOICE)
hover_data
scatter 方法的参数包含一个值来实现这一点。但是,由于我们只想为每个点包含与每种模型类型相关的超参数,因此我们需要在update_trace
调用 scatter 之后调用该方法以添加悬停数据,因为这允许我们过滤为每个点显示哪些超参数观点。看起来是这样的max_depth
每个点的参数设置为3。max_depth
设置为3以外的值,例如2、4或5。这表明在我们的数据集中,参数max_depth
可能有一些特殊之处。例如,这可能表明模型性能主要由三个特征驱动。我们将希望进一步研究为什么max_depth=3
对我们的数据集如此有效,并且我们可能希望为我们构建和部署的最终模型将max_depth
设置为3。在特征之间创建等高线图
max_depth
将超参数的值固定为 3,并绘制该数据切片的learning_rate
vs.n_estimators
loss 等值线。我们可以通过运行以下命令使用 Plotly 创建这个等高线图:# plotly express不支持轮廓图,
# 所以我们将使用'graph_objects'来代替。
# `go.Contour`自动为我们的损失插入“z”值。
fig = go.Figure(
data=go.Contour(
z=trials_df.loc[max_depth_filter, "loss"],
x=trials_df.loc[max_depth_filter, "gradient_boosting_regressor__learning_rate"],
y=trials_df.loc[max_depth_filter, "gradient_boosting_regressor__n_estimators"],
contours=dict(
showlabels=True, # 显示轮廓上的标签
labelfont=dict(size=12, color="white",),
# 标签字体属性
),
colorbar=dict(title="loss", titleside="right",),
hovertemplate="loss: %{z}
learning_rate: %{x}
n_estimators: %{y}",
)
)
fig.update_layout(
xaxis_title="learning_rate",
yaxis_title="n_estimators",
title={
"text": "learning_rate vs. n_estimators | max_depth == 3",
"xanchor": "center",
"yanchor": "top",
"x": 0.5,
},
)
n_estimators
超参数,因为损失最低的区域出现在该图的顶部。写在最后
长按或扫描下方二维码,后台回复:加群,即可申请入群。一定要备注:来源+研究方向+学校/公司,否则不拉入群中,见谅!
(长按三秒,进入后台)
推荐阅读
评论