貝葉斯優化原理及應用[附XGBoost、LightGBM超參數調優代碼][scikit-optimize]

近年來機器學習和深度學習算法被越來越廣泛的應用於解決對未知數據的預測問題。由於超參數的選擇對模型最終的效果可能有極大的影響,爲了使模型達到更好的效果,通常會面臨超參數調優問題。但如何選擇合適的超參數並沒有一個明確的指導,並且同一模型面對隨時間變化的數據,其超參數的選擇都可能需要隨着數據變化進行調節,更何況是原本就分佈不同的兩份數據。因此,人工指定超參數通常是根據經驗或隨機的方法進行嘗試,深度學習裏的“調參工程師”,“煉丹”等說法因此而得名。

既然調參是一個複雜並且耗費時間的工作(由於運行一次模型可能需要耗費一天甚至更長時間),有沒有什麼自動調參算法可以解放人力並且達到比經驗調參更好的效果呢?已經有許多學者在自動化機器學習方向上進行研究,不僅包括超參數自動優化,還有自動化特徵工程、自動化算法(模型)選擇、自動化的神經體系結構搜索等。目前較常用的自動調參算法有Grid Search(網格調參)和Bayesian Optimization(貝葉斯優化)。網格調參的思路很簡單,給要調節的超參數一些選擇,如果超參數調節範圍爲一個連續值則需要將其離散化(如使用等間距採樣)。之後遍歷所有的超參數組合找到這些組合中最優的方案。然而實際上這些組合中可能不包含全局最優解,並且當要調節的超參數比較多時,產生的組合數也會特別多,爲每個組合跑一遍模型驗證需要花費大量的時間。對於XGBoost這種可調節超參數很多的模型,爲了平衡時間開銷和優化結果,通常會進行分組調參,即採用控制變量的方式,先調整部分超參數,將調出的最優超參固定,繼續調節還沒有調節的超參。在我的上篇博客競賽常用集成學習框架Boosting算法總結(XGBoost、LightGBM)(附代碼)中給出了這種超參調節的代碼。這種局部優化的方式可能距離全局最優解更遠,但爲了平衡時間我們不得不這樣做。

我們發現,Grid Search對每種超參數組合都是獨立計算的,當次計算不會利用之間計算得到的信息,這就造成了計算的浪費。而貝葉斯優化算法則利用了之間輪計算的信息對之後的超參選擇給出指導,基於過去的結果搜索未知參數範圍,走向可能是全局最優的方向。貝葉斯優化可以基於不同的代理模型,分爲以下三類,並給出基於這三類算法實現的python庫:

  • TPE(Tree Parzen Estimator, 樹形 Parzen 評估器):Hyperopt, Optuna
  • SMAC(隨機森林迴歸):SMAC3
  • GP(高斯過程):scikit-optimize, GPyOpt, Botorch, spearmint, fmfn/BayesianOptimization

以上總結是Jeff Dean在ICML 2019上關於AutoML的演講上給出的,原文鏈接:An Overview of AutoML Libraries Used in Industry。綜合考慮到質量和速度,在貝葉斯優化上他推薦使用的庫是scikit-optimize。因此下文我們會給出基於高斯過程的貝葉斯優化算法的原理,並在最後給出使用scikit-optimize庫對XGBoost和LightGBM的超參數進行貝葉斯優化的代碼。

我們的優化目標是使機器學習模型的驗證誤差最小,也就是說,需要找到自變量爲超參數和因變量爲驗證誤差的函數最小值點。爲了下文方便說明(其實是懶得畫圖),我們改爲找函數的最大值點,其思想是一致的。貝葉斯優化根據前幾輪超參數組合計算出的真實的驗證誤差,通過高斯過程,得到在超參數所有取值上驗證誤差的期望均值和方差。均值越大代表該組超參數的最終期望效果越好,方差越大表示這組超參數的效果不確定性越大。因此均值大或方差大對應的那組超參數是我們下一步想要帶入模型計算驗證其效果的。

t=2表示已經通過模型計算出了兩組超參數的驗證誤差,即爲圖中的黑點。黑色實線爲假設已知的驗證誤差隨超參數變化函數,需找到其最大值。黑色虛線代表期望均值,紫色區域代表期望方差。那麼下一組超參數應該如何選擇呢?因爲前面提到均值代表期望的最終結果,當然是越大越好,但我們不能每次都挑選均值最大的,因爲有的點方差很大也有可能存在全局最優解,因此選擇均值大的點我們稱爲exploritation(開發),選擇方差大的點我們稱爲exploration(探索)。均值和方差比例通過定義acquisition function確定,對開發和探索進行權衡。簡單的acquisition function有Upper condence bound算法:

x_{t}=\arg \max _{x \in \mathcal{X}} \alpha_{t}(x)=\arg \max _{x \in \mathcal{X}} \mu_{t-1}(x)+\beta_{t}^{1 / 2} \sigma_{t-1}(x)

計算均值和方差的加權和,其中\beta_{t}的值是根據理論分析推出來的,隨時間遞增;在實際應用裏面,爲了簡便也可直接把\beta_{t}設成一個常數。除此之外還有很多複雜的acquisition function,可參考博客貝葉斯優化/Bayesian Optimization

根據應用我們選擇一個合適的acquisition function,求得其最大值,對應的這組超參數值就是貝葉斯優化算法根據之前的計算結果推薦的下一組計算的超參數值。如上圖中的綠色曲線,選擇其最大值點繼續訓練模型。

然後我們將新的計算結果加入到歷史結果中,繼續通過高斯過程計算均值方差,通過acquisition function計算下一組帶入訓練模型的超參數值。不斷重複上述步驟擬合最終的曲線,找出一組最好的值。雖然機器學習模型的超參數不一定是這麼完美的曲線,但可以從概率上找到一個較好的參數。

下文將給出使用scikit-optimize對XGBoost和LightGBM進行超參數調優的代碼。

使用pip安裝庫:

pip install scikit-optimize

在anaconda上安裝庫,運行以下任意一個即可:

conda install -c conda-forge scikit-optimize
conda install -c conda-forge/label/gcc7 scikit-optimize
conda install -c conda-forge/label/cf201901 scikit-optimize

在BayesSearchCV類中實現了貝葉斯優化,但下載的庫中這個類存在問題,會報錯TypeError: __init__() got an unexpected keyword argument 'fit_params',因此自定義FixedBayesSearchCV修復這個問題。以多分類問題爲例。

import xgboost as xgb
from xgboost.sklearn import XGBClassifier
import lightgbm as lgb

from sklearn.model_selection import train_test_split
from skopt import BayesSearchCV
from skopt.space import Real, Categorical, Integer

class FixedBayesSearchCV(BayesSearchCV):
    """
    Dirty hack to avoid compatibility issues with sklearn 0.2 and skopt.
    Credit: https://www.kaggle.com/c/home-credit-default-risk/discussion/64004

    For context, on why the workaround see:
        - https://github.com/scikit-optimize/scikit-optimize/issues/718
        - https://github.com/scikit-optimize/scikit-optimize/issues/762
    """
    def __init__(self, estimator, search_spaces, optimizer_kwargs=None,
                n_iter=50, scoring=None, fit_params=None, n_jobs=1,
                n_points=1, iid=True, refit=True, cv=None, verbose=0,
                pre_dispatch='2*n_jobs', random_state=None,
                error_score='raise', return_train_score=False):
        """
        See: https://github.com/scikit-optimize/scikit-optimize/issues/762#issuecomment-493689266
        """

        # Bug fix: Added this line
        self.fit_params = fit_params

        self.search_spaces = search_spaces
        self.n_iter = n_iter
        self.n_points = n_points
        self.random_state = random_state
        self.optimizer_kwargs = optimizer_kwargs
        self._check_search_space(self.search_spaces)

        # Removed the passing of fit_params to the parent class.
        super(BayesSearchCV, self).__init__(
                estimator=estimator, scoring=scoring, n_jobs=n_jobs, iid=iid,
                refit=refit, cv=cv, verbose=verbose, pre_dispatch=pre_dispatch,
                error_score=error_score, return_train_score=return_train_score)

    def _run_search(self, x):
        raise BaseException('Use newer skopt')

model_lgb = lgb.LGBMClassifier(
            learning_rate=0.1,   # 學習率
            n_estimators=10000,    # 樹的個數
            max_depth=10,         # 樹的最大深度
            num_leaves=31,        # 葉子節點個數 'leaf-wise'
            min_split_gain=0,     # 節點分裂所需的最小損失函數下降值
            objective='multiclass', # 多分類
            metric='multiclass',  # 評價函數
            num_class=4,          # 多分類問題類別數
            subsample=0.8,        # 樣本隨機採樣作爲訓練集的比例
            colsample_bytree=0.8, # 使用特徵比例
            seed=1)

# 若包含類別變量,將其類型設置爲category,astype('category')
# lightgbm scikit-optimize
def lgb_auto_para_tuning_bayesian(model_lgb,X,Y):
    train_x, test_x, train_y, test_y = train_test_split(X, Y, train_size=0.80, random_state=0)
    # cv:交叉驗證 n_points:並行採樣的超參組數
    opt = FixedBayesSearchCV(model_lgb,cv=3,n_points=2,n_jobs=4,verbose=1,
        search_spaces={
            'learning_rate': Real(0.008, 0.01),
            'max_depth': Integer(3, 10),
            'num_leaves': Integer(31, 127),
            'min_split_gain':Real(0.0,0.4),
            'min_child_weight':Real(0.001,0.002),
            'min_child_samples':Integer(18,22),
            'subsample':Real(0.6,1.0),
            'subsample_freq':Integer(3,5),
            'colsample_bytree':Real(0.6,1.0),
            'reg_alpha':Real(0,0.5),
            'reg_lambda':Real(0,0.5)
        },
         fit_params={
                 'eval_set':[(test_x, test_y)],
                 'eval_metric': 'multiclass',
                 'early_stopping_rounds': 50
                 })
    opt.fit(train_x,train_y)
    print("val. score: %s" % opt.best_score_)
    print("test score: %s" % opt.score(test_x, test_y))
    print("Best parameters: ", opt.best_params_)
    print("Best estimator:", opt.best_estimator_)

model_xgb = XGBClassifier(
            learning_rate =0.01,  # 學習率
            n_estimators=10000,   # 樹的個數
            max_depth=6,         # 樹的最大深度
            min_child_weight=1,  # 葉子節點樣本權重加和最小值sum(H)
            gamma=0,             # 節點分裂所需的最小損失函數下降值
            subsample=0.8,       # 樣本隨機採樣作爲訓練集的比例
            colsample_bytree=0.8, # 使用特徵比例
            objective= 'multi:softmax', # 損失函數(這裏爲多分類)
            num_class=4,         # 多分類問題類別數
            scale_pos_weight=1,  # 類別樣本不平衡
            seed=1)

# xgboost scikit-optimize
def xgb_auto_para_tuning_bayesian(model_xgb,X,Y):
    train_x, test_x, train_y, test_y = train_test_split(X, Y, train_size=0.80, random_state=0)
    opt = FixedBayesSearchCV(model_xgb,cv=3,n_points=2,n_jobs=4,verbose=1,
        search_spaces={
            'learning_rate': Real(0.008, 0.01),
            'max_depth': Integer(3, 10),
            'gamma':Real(0,0.5),
            'min_child_weight':Integer(1,8),
            'subsample':Real(0.6,1.0),
            'colsample_bytree':Real(0.6,1.0),
            'reg_alpha':Real(0,0.5),
            'reg_lambda':Real(0,0.5)
        },
         fit_params={
                 'eval_set': [(test_x, test_y)],
                 'eval_metric': 'mlogloss',
                 'early_stopping_rounds': 50
                 })
    opt.fit(train_x,y=train_y)
    print("val. score: %s" % opt.best_score_)
    print("test score: %s" % opt.score(test_x, test_y))
    print("Best parameters: ", opt.best_params_)
    print("Best estimator:", opt.best_estimator_)

scikit-optimize官方文檔:https://scikit-optimize.github.io/#skopt.BayesSearchCV

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章