不管是机器学习模型还是神经网络模型,超参的调整都是费神费力的。一般调参方法有如下三种:
- 网格搜索调参
- 随机搜索调参
- 贝叶斯优化调参
第一种最为粗暴,基本上就是枚举所有参数的可能选择了,第二种可以理解为随机的枚举,依旧难登大雅之堂,从原理来讲,这两种方式最大的问题在于,在调参的过程中,之前选择的参数对后来要选择的参数没有任何帮助,相当于在整个调参的过程中每一次都是“无脑”的选择,那自然要选择的次数比较多了,一次费神费力。
贝叶斯优化一般用来解决目标函数未知且每一次评估都代价高昂的优化问题,细想一下,模型的超参调整不就是这样的一个问题吗?模型本身本已经很复杂了,超参对训练之后的模型是黑盒状态的,且训练一次模型也是消耗相当多的资源的,尤其是神经网络模型。
与随机或网格搜索的不同之处在于,贝叶斯优化在尝试下一组超参数时,会参考之前的评估结果,因此可以省去很多无用功。至于如何做到的呢?首先要理解贝叶斯优化背后的思路。
贝叶斯优化的思路如下:用代理模型来拟合真实模型(真实函数),在优化的过程种,根据当前拟合的结果去选择下一个最有可能的采样点,避免了不必要的采样,然后每一次采样都可以用来更新这个代理模型,使之更为接近真实模型。所以贝叶斯优化主要包括两个要点:
- 代理模型
- 采样函数
这里代理模型可以选择的包括:贝叶斯神经网络、随机过程等,即为概率模型。采样函数用来选择下一次采样的点,一般贝叶斯优化的主要研究也在该函数上。
hyperopt便是一个用贝叶斯优化来调参的框架,当然,此类框架还有很多。选择hyperopt也仅仅是因为使用简单。其使用方式可以概括为:
- 定义如下对象
- **目标函数(Objective Function)**这个函数返回的数值就是最终需要的最小值。一般这个函数内所做操作包括:训练模型,在测试集上预测,返回测试集上的结果。
- **搜索空间(Domain)**定义待优化的超参数的空间。字典形式,定义每一个超参数的概率分布。可以通过嵌套字典来设置搜索条件。
- 优化算法(Optimization Algorithm) 优化算法在该框架中仅需要定义一句话:
from hyperopt import tpe
# 创建优化
tpe_algorithm = tpe.suggest
4. **历史结果(Result History)**定义`Trials()`对象来存放搜索历史结果。
trials = Trials()
- 开始搜索:
bst = fmin(fn=objective, space=space, algo=tpe.suggest, trials=trials, max_evals=MAX_EVALS)
bst返回给我们的是最优的参数组合
在Lightgbm上的实践
一些需要注意的点:
- Lightgbm需要调整的超参数包括两大类,一种是控制模型整体结构的,如迭代次数,另外一种是控制每个弱学习器的参数。通常将迭代次数(numboostround)设置为1000或者更大,但实际上不会达到这个数字,因为会使用earlystopping_rounds来停止训练,当连续100轮迭代效果都没有提升时,则提前停止,并选择模型。因此,迭代次数并不是我们需要设置的超参数。因此,仅仅需要对控制每个弱学习器的参数定义搜索空间。
- 设置搜索空间时候,通常在自己认为最佳值的位置放置更大范围的概率分布来告知我们对超参数分布的选择。在我们不确定最佳值的情况下,可以将范围设定的大一点,让贝叶斯算法为做推理。一般首次调整模型时,通常会创建一个以默认值为中心的宽域空间,然后在后续搜索中对其进行细化。
贝叶斯优化在LightGBM模型上的调参模板
from lightgbm import log_evaluation, early_stopping
from hyperopt import fmin, hp, Trials, space_eval, rand, tpe, anneal
import lightgbm as lgb
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from hyperopt.pyll.stochastic import sample
class LgbHyperOpt:
def __init__(self, train_df, valid_df, cat_features, num_features, target_name: str):
self.train_df = train_df
self.valid_df = valid_df
if self.valid_df is None:
self.train_df, self.valid_df = self._split_train_valid(self.train_df)
self.cat_features = cat_features
self.num_features = num_features
self.target_name = target_name
self.best_params = None
def _split_train_valid(self, df, ratio=0.8):
train_num = int(len(df) * ratio)
return df.iloc[:train_num], df.iloc[train_num:]
def rmse_score(self, true, pred):
return np.sqrt(mean_squared_error(true, pred))
def _build_data(self):
lgb_train = lgb.Dataset(
self.train_df[self.num_features + self.cat_features],
label=self.train_df[self.target_name],
feature_name=self.num_features + self.cat_features,
free_raw_data=False,
categorical_feature=self.cat_features,
)
lgb_valid = lgb.Dataset(
self.valid_df[self.num_features + self.cat_features],
label=self.valid_df[self.target_name],
feature_name=self.num_features + self.cat_features,
categorical_feature=self.cat_features,
reference=lgb_train,
)
return lgb_train, lgb_valid
def transfer_param(self, source_param):
params = source_param
boosting_type = params["boosting_type"]["boosting_type"]
if boosting_type == "gbdt":
subsample = params["boosting_type"].get("gbdt_subsample", 1.0)
else:
subsample = params["boosting_type"].get("dart_subsample", 1.0)
params["boosting_type"] = boosting_type
params["subsample"] = subsample
for parameter_name in ["num_leaves", "subsample_for_bin", "min_data_in_leaf"]:
if parameter_name in params:
params[parameter_name] = int(params[parameter_name])
return params
def _objective(self, hyper_param):
params = self.transfer_param(hyper_param)
boost_round = 1000
early_stop_rounds = 50
callbacks = [
log_evaluation(period=False),
early_stopping(stopping_rounds=early_stop_rounds),
]
self.lgb_train, self.lgb_valid = self._build_data()
gbm = lgb.train(
params=params,
train_set=self.lgb_train,
num_boost_round=boost_round,
valid_sets=(self.lgb_valid, self.lgb_train),
valid_names=("valid", "train"),
categorical_feature=self.cat_features,
callbacks=callbacks,
)
y_test_pred = gbm.predict(
self.valid_df[self.num_features + self.cat_features],
num_iteration=gbm.best_iteration,
)
score = self.rmse_score(self.valid_df[self.target_name].values, y_test_pred)
return score
def _build_lgb_hypter_space(self):
space = {
"boosting_type": hp.choice(
"boosting_type",
[
{
"boosting_type": "gbdt",
"subsample": hp.uniform("gbdt_subsample", 0.5, 1),
},
],
),
"objective": "regression",
"metric": "rmse",
"max_depth": hp.choice("max_depth", range(3, 11)),
"num_leaves": hp.quniform("num_leaves", 15, 150, 1),
"learning_rate": hp.loguniform("learning_rate", np.log(0.005), np.log(0.5)),
"subsample_for_bin": hp.quniform(
"subsample_for_bin", 20000, 300000, 20000
),
"feature_fraction": hp.uniform(
"feature_fraction", 0.6, 1.0
),
"min_data_in_leaf": hp.qloguniform(
"min_data_in_leaf", 0, 6, 1
),
"min_child_weight": hp.loguniform("min_child_weight", -16, 5),
"reg_alpha": hp.uniform("reg_alpha", 0.0, 1.0),
"reg_lambda": hp.uniform("reg_lambda", 0.0, 1.0),
"verbose": -1,
}
return space
def _test_hyper_param(self):
origin_param = self._build_lgb_hypter_space()
origin_param = sample(origin_param)
target_param = self.transfer_param(origin_param)
print(f"Origin param from space:{origin_param}")
print(f"Target param from space:{target_param}")
def find_param(self, max_evals):
space = self._build_lgb_hypter_space()
trials = Trials()
best = fmin(
fn=self._objective,
space=space,
algo=tpe.suggest,
max_evals=max_evals,
trials=trials,
)
self.best_params = self.transfer_param(space_eval(space, best))
return trials
# 导数据分析常用包
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 导包获取糖尿病数据集
from sklearn.datasets import load_diabetes
data_diabetes = load_diabetes()
data = data_diabetes["data"]
target = data_diabetes["target"]
feature_names = data_diabetes["feature_names"]
# 现在三个数据都是numpy的一维数据形式,将她们组合成dataframe,可以更直观地观察数据
df = pd.DataFrame(data, columns=feature_names)
df["target"] = target
lgb_hyper_param = LgbHyperOpt(train_df=df, valid_df=None, num_features=feature_names, cat_features=[], target_name="target")
lgb_hyper_param.find_param(100)
总结
贝叶斯优化好归好,但是也有缺陷,比如贝叶斯优化无法保证找到最好的超参数,并且可能陷入目标函数的局部最小值。