贝叶斯优化在ML调参中的应用

date
Oct 22, 2023
slug
bayesian-optimization-in-ml-hyperparameter-tuning
status
Published
tags
Machine Learn
Neural Network
Python
summary
总结了贝叶斯调参框架hyperopt的常规用法,然后在真实数据集上利用Lightgbm模型进行了实践
type
Post
不管是机器学习模型还是神经网络模型,超参的调整都是费神费力的。一般调参方法有如下三种:
  • 网格搜索调参
  • 随机搜索调参
  • 贝叶斯优化调参
第一种最为粗暴,基本上就是枚举所有参数的可能选择了,第二种可以理解为随机的枚举,依旧难登大雅之堂,从原理来讲,这两种方式最大的问题在于,在调参的过程中,之前选择的参数对后来要选择的参数没有任何帮助,相当于在整个调参的过程中每一次都是“无脑”的选择,那自然要选择的次数比较多了,一次费神费力。
贝叶斯优化一般用来解决目标函数未知且每一次评估都代价高昂的优化问题,细想一下,模型的超参调整不就是这样的一个问题吗?模型本身本已经很复杂了,超参对训练之后的模型是黑盒状态的,且训练一次模型也是消耗相当多的资源的,尤其是神经网络模型。
与随机或网格搜索的不同之处在于,贝叶斯优化在尝试下一组超参数时,会参考之前的评估结果,因此可以省去很多无用功。至于如何做到的呢?首先要理解贝叶斯优化背后的思路。
贝叶斯优化的思路如下:用代理模型来拟合真实模型(真实函数),在优化的过程种,根据当前拟合的结果去选择下一个最有可能的采样点,避免了不必要的采样,然后每一次采样都可以用来更新这个代理模型,使之更为接近真实模型。所以贝叶斯优化主要包括两个要点:
  • 代理模型
  • 采样函数
这里代理模型可以选择的包括:贝叶斯神经网络、随机过程等,即为概率模型。采样函数用来选择下一次采样的点,一般贝叶斯优化的主要研究也在该函数上。
hyperopt便是一个用贝叶斯优化来调参的框架,当然,此类框架还有很多。选择hyperopt也仅仅是因为使用简单。其使用方式可以概括为:
  1. 定义如下对象
    1. 目标函数(Objective Function)这个函数返回的数值就是最终需要的最小值。一般这个函数内所做操作包括:训练模型,在测试集上预测,返回测试集上的结果。
    2. 搜索空间(Domain)定义待优化的超参数的空间。字典形式,定义每一个超参数的概率分布。可以通过嵌套字典来设置搜索条件。
    3. 优化算法(Optimization Algorithm)
      1. 优化算法在该框架中仅需要定义一句话:
        from hyperopt import tpe # 创建优化 tpe_algorithm = tpe.suggest
    4. 历史结果(Result History)定义Trials()对象来存放搜索历史结果。
      1. trials = Trials()
  1. 开始搜索:
    1. 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)

总结

贝叶斯优化好归好,但是也有缺陷,比如贝叶斯优化无法保证找到最好的超参数,并且可能陷入目标函数的局部最小值。