1. 程式人生 > >自動機器學習超引數調整(貝葉斯優化)

自動機器學習超引數調整(貝葉斯優化)

【導讀】機器學習中,調參是一項繁瑣但至關重要的任務,因為它很大程度上影響了演算法的效能。手動調參十分耗時,網格和隨機搜尋不需要人力,但需要很長的執行時間。因此,誕生了許多自動調整超引數的方法。貝葉斯優化是一種用模型找到函式最小值方法,已經應用於機器學習問題中的超引數搜尋,這種方法效能好,同時比隨機搜尋省時。此外,現在有許多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中使用的方法,並且可以比隨機搜尋返回更好的結果。

 

參考自https://towardsdatascience.com/automated-machine-learning-hyperparameter-tuning-in-python-dfda59b72f8a