Skip to content
BaiRuic's Blog
Go back

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

Updated:

不管是机器学习模型还是神经网络模型,超参的调整都是费神费力的。一般调参方法有如下三种:

第一种最为粗暴,基本上就是枚举所有参数的可能选择了,第二种可以理解为随机的枚举,依旧难登大雅之堂,从原理来讲,这两种方式最大的问题在于,在调参的过程中,之前选择的参数对后来要选择的参数没有任何帮助,相当于在整个调参的过程中每一次都是“无脑”的选择,那自然要选择的次数比较多了,一次费神费力。

贝叶斯优化一般用来解决目标函数未知且每一次评估都代价高昂的优化问题,细想一下,模型的超参调整不就是这样的一个问题吗?模型本身本已经很复杂了,超参对训练之后的模型是黑盒状态的,且训练一次模型也是消耗相当多的资源的,尤其是神经网络模型。

与随机或网格搜索的不同之处在于,贝叶斯优化在尝试下一组超参数时,会参考之前的评估结果,因此可以省去很多无用功。至于如何做到的呢?首先要理解贝叶斯优化背后的思路。

贝叶斯优化的思路如下:用代理模型来拟合真实模型(真实函数),在优化的过程种,根据当前拟合的结果去选择下一个最有可能的采样点,避免了不必要的采样,然后每一次采样都可以用来更新这个代理模型,使之更为接近真实模型。所以贝叶斯优化主要包括两个要点:

这里代理模型可以选择的包括:贝叶斯神经网络、随机过程等,即为概率模型。采样函数用来选择下一次采样的点,一般贝叶斯优化的主要研究也在该函数上。

hyperopt便是一个用贝叶斯优化来调参的框架,当然,此类框架还有很多。选择hyperopt也仅仅是因为使用简单。其使用方式可以概括为:

  1. 定义如下对象
    1. **目标函数(Objective Function)**这个函数返回的数值就是最终需要的最小值。一般这个函数内所做操作包括:训练模型,在测试集上预测,返回测试集上的结果。
    2. **搜索空间(Domain)**定义待优化的超参数的空间。字典形式,定义每一个超参数的概率分布。可以通过嵌套字典来设置搜索条件。
    3. 优化算法(Optimization Algorithm) 优化算法在该框架中仅需要定义一句话:
from hyperopt import tpe
# 创建优化
tpe_algorithm = tpe.suggest
4. **历史结果(Result History)**定义`Trials()`对象来存放搜索历史结果。
trials = Trials()
  1. 开始搜索:
bst = fmin(fn=objective, space=space, algo=tpe.suggest, trials=trials, max_evals=MAX_EVALS)

bst返回给我们的是最优的参数组合

在Lightgbm上的实践

一些需要注意的点:

贝叶斯优化在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)

总结

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


Share this post:

Previous Post
自动微分是什么?(续)
Next Post
我又毕业了