【導讀】機器學習中,調參是一項繁瑣但至關重要的任務,因為它很大程度上影響了算法的性能。手動調參十分耗時,網格和隨機搜索不需要人力,但需要很長的運行時間。因此,誕生了許多自動調整超參數的方法。貝葉斯優化是一種用模型找到函數最小值方法,已經應用於機器學習問題中的超參數搜索,這種方法性能好,同時比隨機搜索省時。此外,現在有許多Python庫可以實現貝葉斯超參數調整。本文將使用Hyperopt庫演示梯度提升機(Gradient Boosting Machine,GBM) 的貝葉斯超參數調整的完整示例。文章由貝葉斯優化方法、優化問題的四個部分、目標函數、域空間、優化過程、及結果展示幾個部分組成。
貝葉斯優化方法
貝葉斯優化通過基於目標函數的過去評估結果建立替代函數(概率模型),來找到最小化目標函數的值。貝葉斯方法與隨機或網格搜索的不同之處在於,它在嘗試下一組超參數時,會參考之前的評估結果,因此可以省去很多無用功。
超參數的評估代價很大,因為它要求使用待評估的超參數訓練一遍模型,而許多深度學習模型動則幾個小時幾天才能完成訓練,並評估模型,因此耗費巨大。貝葉斯調參發使用不斷更新的概率模型,通過推斷過去的結果來“集中”有希望的超參數。
Python中的選擇
Python中有幾個貝葉斯優化庫,它們目標函數的替代函數不一樣。在本文中,我們將使用Hyperopt,它使用Tree Parzen Estimator(TPE)。其他Python庫包括Spearmint(高斯過程代理)和SMAC(隨機森林回歸)。
優化問題的四個部分
貝葉斯優化問題有四個部分:
-
目標函數:我們想要最小化的內容,在這里,目標函數是機器學習模型使用該組超參數在驗證集上的損失。
-
域空間:要搜索的超參數的取值范圍
-
優化算法:構造替代函數並選擇下一個超參數值進行評估的方法。
-
結果歷史記錄:來自目標函數評估的存儲結果,包括超參數和驗證集上的損失。
數據集
在本例中,我們將使用Caravan Insurance數據集,其目標是預測客戶是否購買保險單。 這是一個有監督分類問題,訓練集和測試集的大小分別為5800和4000。評估性能的指標是AUC(曲線下面積)評估准則和ROC(receiver operating characteristic,以真陽率和假陽率為坐標軸的曲線圖)曲線,ROC AUC越高表示模型越好。 數據集如下所示:
為Hyperopt最小化目標函數,我們的目標函數返回1-ROC AUC,從而提高ROC AUC。
梯度提升模型
梯度提升機(GBM)是一種基於使用弱學習器(如決策樹)組合成強學習器的模型。 GBM中有許多超參數控制整個集合和單個決策樹,如決策樹數量,決策樹深度等。簡單了解了GBM,接下來我們介紹這個問題對應的優化模型的四個部分
目標函數
目標函數是需要我們最小化的。 它的輸入為一組超參數,輸出需要最小化的值(交叉驗證損失)。Hyperopt將目標函數視為黑盒,只考慮它的輸入和輸出。 在這里,目標函數定義為:
1 def objective(hyperparameters): 2 '''Returns validation score from hyperparameters''' 3 4 model = Classifier(hyperparameters) 5 validation_loss = cross_validation(model, training_data) 6 return validation_loss
我們評估的是超參數在驗證集上的表現,但我們不將數據集划分成固定的驗證集和訓練集,而是使用K折交叉驗證。使用10折交叉驗證和提前停止的梯度提升機的完整目標函數如下所示。
1 import lightgbm as lgb 2 from hyperopt import STATUS_OK 3 4 N_FOLDS = 10 5 6 # Create the dataset 7 train_set = lgb.Dataset(train_features, train_labels) 8 9 10 def objective(params, n_folds=N_FOLDS): 11 '''Objective function for Gradient Boosting Machine Hyperparameter Tuning''' 12 13 # Perform n_fold cross validation with hyperparameters 14 # Use early stopping and evalute based on ROC AUC 15 cv_results = lgb.cv(params, train_set, nfold=n_folds, num_boost_round=10000, 16 early_stopping_rounds=100, metrics='auc', seed=50) 17 18 # Extract the best score 19 best_score = max(cv_results['auc-mean']) 20 21 # Loss must be minimized 22 loss = 1 - best_score 23 24 # Dictionary with information for evaluation 25 return {'loss': loss, 'params': params, 'status': STATUS_OK}
關鍵點是cvresults = lgb.cv(...)。為了實現提前停止的交叉驗證,我們使用LightGBM函數cv,它輸入為超參數,訓練集,用於交叉驗證的折數等。我們將迭代次數(numboostround)設置為10000,但實際上不會達到這個數字,因為我們使用earlystopping_rounds來停止訓練,當連續100輪迭代效果都沒有提升時,則提前停止,並選擇模型。因此,迭代次數並不是我們需要設置的超參數。
一旦交叉驗證完成,我們就會得到最好的分數(ROC AUC),然后,因為我們最小化目標函數,所以計算1- ROC AUC,然后返回這個值。
域空間
域空間表示我們要為每個超參數計算的值的范圍。在搜索的每次迭代中,貝葉斯優化算法將從域空間為每個超參數選擇一個值。當我們進行隨機或網格搜索時,域空間是一個網格。在貝葉斯優化中,想法是一樣的,但是不是按照順序(網格)或者隨機選擇一個超參數,而是按照每個超參數的概率分布選擇。
而確定域空間是最困難的。如果我們有機器學習方法的經驗,我們可以通過在我們認為最佳值的位置放置更大的概率來使用它來告知我們對超參數分布的選擇。然而,不同數據集之間最佳模型不一樣,並且具有高維度問題(許多超參數),超參數之間也會互相影響。在我們不確定最佳值的情況下,我們可以將范圍設定的大一點,讓貝葉斯算法為我們做推理。
首先,我們看看GBM中的所有超參數:
1 import lgb 2 # Default gradient boosting machine classifier 3 model = lgb.LGBMClassifier() 4 model 5 LGBMClassifier(boosting_type='gbdt', n_estimators=100, 6 class_weight=None, colsample_bytree=1.0, 7 learning_rate=0.1, max_depth=-1, 8 min_child_samples=20, 9 min_child_weight=0.001, min_split_gain=0.0, 10 n_jobs=-1, num_leaves=31, objective=None, 11 random_state=None, reg_alpha=0.0, reg_lambda=0.0, 12 silent=True, subsample=1.0, 13 subsample_for_bin=200000, subsample_freq=1)
其中一些我們不需要調整(例如objective和randomstate),我們將使用提前停止來找到最好的n_estimators。 但是,我們還有10個超參數要優化! 首次調整模型時,我通常會創建一個以默認值為中心的寬域空間,然后在后續搜索中對其進行細化。
例如,讓我們在Hyperopt中定義一個簡單的域,這是GBM中每棵樹中葉子數量的離散均勻分布:
1 from hyperopt import hp 2 # Discrete uniform distribution 3 num_leaves = {'num_leaves': hp.quniform('num_leaves', 30, 150, 1)}
這里選擇離散的均勻分布,因為葉子的數量必須是整數(離散),並且域中的每個值都可能(均勻)。
另一種分布選擇是對數均勻,它在對數標度上均勻分布值。 我們將使用對數統一(從0.005到0.2)來獲得學習率,因為它在幾個數量級上變化:
1 # Learning rate log uniform distribution 2 learning_rate = {'learning_rate': hp.loguniform('learning_rate', 3 np.log(0.005), 4 np.log(0.2)}
下面分別繪制了均勻分布和對數均勻分布的圖。 這些是核密度估計圖,因此y軸是密度而不是計數!
現在,讓我們定義整個域:
1 # Define the search space 2 space = { 3 'class_weight': hp.choice('class_weight', [None, 'balanced']), 4 'boosting_type': hp.choice('boosting_type', 5 [{'boosting_type': 'gbdt', 6 'subsample': hp.uniform('gdbt_subsample', 0.5, 1)}, 7 {'boosting_type': 'dart', 8 'subsample': hp.uniform('dart_subsample', 0.5, 1)}, 9 {'boosting_type': 'goss'}]), 10 'num_leaves': hp.quniform('num_leaves', 30, 150, 1), 11 'learning_rate': hp.loguniform('learning_rate', np.log(0.01), np.log(0.2)), 12 'subsample_for_bin': hp.quniform('subsample_for_bin', 20000, 300000, 20000), 13 'min_child_samples': hp.quniform('min_child_samples', 20, 500, 5), 14 'reg_alpha': hp.uniform('reg_alpha', 0.0, 1.0), 15 'reg_lambda': hp.uniform('reg_lambda', 0.0, 1.0), 16 'colsample_bytree': hp.uniform('colsample_by_tree', 0.6, 1.0) 17 }
這里我們使用了許多不同的域分發類型:
1 choice:類別變量 2 quniform:離散均勻(整數間隔均勻) 3 uniform:連續均勻(間隔為一個浮點數) 4 loguniform:連續對數均勻(對數下均勻分布) 5 # boosting type domain 6 boosting_type = {'boosting_type': hp.choice('boosting_type', 7 [{'boosting_type': 'gbdt', 8 'subsample': hp.uniform('subsample', 0.5, 1)}, 9 {'boosting_type': 'dart', 10 'subsample': hp.uniform('subsample', 0.5, 1)}, 11 {'boosting_type': 'goss', 12 'subsample': 1.0}])}
這里我們使用條件域,這意味着一個超參數的值取決於另一個超參數的值。 對於提升類型“goss”,gbm不能使用子采樣(僅選擇訓練觀察的子樣本部分以在每次迭代時使用)。 因此,如果提升類型是“goss”,則子采樣率設置為1.0(無子采樣),否則為0.5-1.0。 這是使用嵌套域實現的
定義域空間之后,我們可以從中采樣查看樣本。
1 # Sample from the full space 2 example = sample(space) 3 4 # Dictionary get method with default 5 subsample = example['boosting_type'].get('subsample', 1.0) 6 7 # Assign top-level keys 8 example['boosting_type'] = example['boosting_type']['boosting_type'] 9 example['subsample'] = subsample 10 11 example 12 {'boosting_type': 'gbdt', 13 'class_weight': 'balanced', 14 'colsample_bytree': 0.8111305579351727, 15 'learning_rate': 0.16186471096789776, 16 'min_child_samples': 470.0, 17 'num_leaves': 88.0, 18 'reg_alpha': 0.6338327001528129, 19 'reg_lambda': 0.8554826167886239, 20 'subsample_for_bin': 280000.0, 21 'subsample': 0.6318665053932255}
優化算法
雖然這是貝葉斯優化中概念上最難的部分,但在Hyperopt中創建優化算法只需一行。 要使用Tree Parzen Estimator,代碼為:
1 from hyperopt import tpe 2 # Algorithm 3 tpe_algorithm = tpe.suggest
在優化時,TPE算法根據過去的結果構建概率模型,並通過最大化預期的改進來決定下一組超參數以在目標函數中進行評估。
結果歷史
跟蹤結果並不是絕對必要的,因為Hyperopt將在內部為算法執行此操作。 但是,如果我們想知道幕后發生了什么,我們可以使用Trials對象來存儲基本的訓練信息,還可以使用從目標函數返回的字典(包括損失和范圍)。 制創建Trials對象也只要一行代碼:
1 from hyperopt import Trials 2 # Trials object to track progress 3 bayes_trials = Trials()
為了監控訓練運行進度,可以將結果歷史寫入csv文件,防止程序意外中斷導致評估結果消失。
1 import csv 2 3 # File to save first results 4 out_file = 'gbm_trials.csv' 5 of_connection = open(out_file, 'w') 6 writer = csv.writer(of_connection) 7 8 # Write the headers to the file 9 writer.writerow(['loss', 'params', 'iteration', 'estimators', 'train_time']) 10 of_connection.close()
然后在目標函數中我們可以在每次迭代時添加行寫入csv:
1 # Write to the csv file ('a' means append) 2 of_connection = open(out_file, 'a') 3 writer = csv.writer(of_connection) 4 writer.writerow([loss, params, iteration, n_estimators, run_time]) 5 of_connection.close()
優化:
一旦我們定義好了上述部分,就可以用fmin運行優化:
1 from hyperopt import fmin 2 MAX_EVALS = 500 3 # Optimize 4 best = fmin(fn = objective, space = space, algo = tpe.suggest, 5 max_evals = MAX_EVALS, trials = bayes_trials)
在每次迭代時,算法從代理函數中選擇新的超參數值,該代理函數基於先前的結果構建並在目標函數中評估這些值。 這繼續用於目標函數的MAX_EVALS評估,其中代理函數隨每個新結果不斷更新。
結果:
從fmin返回的最佳對象包含在目標函數上產生最低損失的超參數:
1 {'boosting_type': 'gbdt', 2 'class_weight': 'balanced', 3 'colsample_bytree': 0.7125187075392453, 4 'learning_rate': 0.022592570862044956, 5 'min_child_samples': 250, 6 'num_leaves': 49, 7 'reg_alpha': 0.2035211643104735, 8 'reg_lambda': 0.6455131715928091, 9 'subsample': 0.983566228071919, 10 'subsample_for_bin': 200000}
一旦我們有了這些超參數,我們就可以使用它們來訓練完整訓練數據的模型,然后評估測試數據。 最終結果如下:
The best model scores 0.72506 AUC ROC on the test set.
The best cross validation score was 0.77101 AUC ROC.
This was achieved after 413 search iterations.
作為參考,500次隨機搜索迭代返回了一個模型,該模型在測試集上評分為0.7232 ROC AUC,在交叉驗證中評分為0.76850。沒有優化的默認模型在測試集上評分為0.7143 ROC AUC。
在查看結果時,請記住一些重要的注意事項:
最佳超參數是那些在交叉驗證方面表現最佳的參數,而不一定是那些在測試數據上做得最好的參數。當我們使用交叉驗證時,我們希望這些結果可以推廣到測試數據。
即使使用10倍交叉驗證,超參數調整也會過度擬合訓練數據。交叉驗證的最佳分數顯著高於測試數據。
隨機搜索可以通過純粹的運氣返回更好的超參數(重新運行筆記本可以改變結果)。貝葉斯優化不能保證找到更好的超參數,並且可能陷入目標函數的局部最小值。
另一個重點是超參數優化的效果將隨數據集的不同而不同。相對較小的數據集(訓練集大小為6000),調整超參數,最終得到的模型的提升並不大,但數據集更大時,效果會很明顯。
因此,通過貝葉斯概率來優化超參數,我們可以:在測試集上得到更好的性能;調整超參數的迭代次數減少.
可視化結果:
繪制結果圖表是一種直觀的方式,可以了解超參數搜索過程中發生的情況。此外,通過將貝葉斯優化與隨機搜索進行比較,可以看出方法的不同之處。
首先,我們可以制作隨機搜索和貝葉斯優化中采樣的learning_rate的核密度估計圖。作為參考,我們還可以顯示采樣分布。垂直虛線表示學習率的最佳值(根據交叉驗證)。
我們將學習率定義為0.005到0.2之間的對數正態,貝葉斯優化結果看起來與采樣分布類似。 這告訴我們,我們定義的分布看起來適合於任務,盡管最佳值比我們放置最大概率的值略高。 這可用於告訴域進一步搜索。
另一個超參數是boosting_type,在隨機搜索和貝葉斯優化期間評估每種類型的條形圖。 由於隨機搜索不關注過去的結果,我們預計每種增強類型的使用次數大致相同。
根據貝葉斯算法,gdbt提升模型比dart或goss更有前途。 同樣,這可以幫助進一步搜索,貝葉斯方法或網格搜索。 如果我們想要進行更明智的網格搜索,我們可以使用這些結果來定義圍繞超參數最有希望的值的較小網格。
我們再看下其他參數的分布,隨機搜索和貝葉斯優化中的所有數值型超參數。 垂直線再次表示每次搜索的超參數的最佳值:
在大多數情況下(subsample_for_bin除外),貝葉斯優化搜索傾向於在超參數值附近集中(放置更多概率),從而產生交叉驗證中的最低損失。這顯示了使用貝葉斯方法進行超參數調整的基本思想:花費更多時間來評估有希望的超參數值。
此處還有一些有趣的結果可能會幫助我們在將來定義要搜索的域空間時。僅舉一個例子,看起來regalpha和reglambda應該相互補充:如果一個是高(接近1.0),另一個應該更低。不能保證這會解決問題,但通過研究結果,我們可以獲得可能適用於未來機器學習問題的見解!
搜索的演變
隨着優化的進展,我們期望貝葉斯方法關注超參數的更有希望的值:那些在交叉驗證中產生最低誤差的值。我們可以繪制超參數與迭代的值,以查看是否存在明顯的趨勢。
黑星表示最佳值。 colsample_bytree和learning_rate會隨着時間的推移而減少,這可能會指導我們未來的搜索。
最后,如果貝葉斯優化工作正常,我們預計平均驗證分數會隨着時間的推移而增加(相反,損失減少):
來自貝葉斯超參數優化的驗證集上的分數隨着時間的推移而增加,表明該方法正在嘗試“更好”的超參數值(應該注意,僅根據驗證集上的表現更好)。隨機搜索沒有顯示迭代的改進。
繼續搜索
如果我們對模型的性能不滿意,我們可以繼續使用Hyperopt進行搜索。我們只需要傳入相同的試驗對象,算法將繼續搜索。
隨着算法的進展,它會進行更多的挖掘 - 挑選過去表現良好的價值 , 而不是探索 - 挑選新價值。因此,開始完全不同的搜索可能是一個好主意,而不是從搜索停止的地方繼續開始。如果來自第一次搜索的最佳超參數確實是“最優的”,我們希望后續搜索專注於相同的值。
經過另外500次訓練后,最終模型在測試集上得分為0.72736 ROC AUC。 (我們實際上不應該評估測試集上的第一個模型,而只依賴於驗證分數。理想情況下,測試集應該只使用一次,以便在部署到新數據時獲得算法性能的度量)。同樣,由於數據集的小尺寸,這個問題可能導致進一步超參數優化的收益遞減,並且最終會出現驗證錯誤的平台(由於隱藏變量導致數據集上任何模型的性能存在固有限制未測量和噪聲數據,稱為貝葉斯誤差)
結論
可以使用貝葉斯優化來完成機器學習模型的超參數自動調整。與隨機或網格搜索相比,貝葉斯優化對目標函數的評估較少,測試集上的更好的泛化性能。
在本文中,我們使用Hyperopt逐步完成了Python中的貝葉斯超參數優化。除了網格和隨機搜索之外,我們還能夠提高梯度提升樹的測試集性能,盡管我們需要謹慎對待訓練數據的過度擬合。此外,我們通過可視化結果表看到隨機搜索與貝葉斯優化的不同之處,這些圖表顯示貝葉斯方法對超參數值的概率更大,導致交叉驗證損失更低。
使用優化問題的四個部分,我們可以使用Hyperopt來解決各種各樣的問題。貝葉斯優化的基本部分也適用於Python中實現不同算法的許多庫。從手動切換到隨機或網格搜索只是一小步,但要將機器學習提升到新的水平,需要一些自動形式的超參數調整。貝葉斯優化是一種易於在Python中使用的方法,並且可以比隨機搜索返回更好的結果。