超參數(hyperparameter)的意思是,不是經由模型訓練過程中學習到,而是需要在訓練前手動設定的參數,如 KNN 的鄰居數量、樹模型的最大深度、SVM 的 kernel 種類,以及神經網路的 batch size 等等。由於超參數雖然不能自動學習,但又仍會對模型的性能有影響,因此如何調整它們,是相當重要的問題。當然,如果你對於待調整的超參數,可以用過去經驗來預估大概範圍,且超參數數量不多的話,可以用手動調整;但若遇到沒有過去經驗可以參考,或者超參數數量較多等等的狀況的話,就比較難以用手動調整,並紀錄實驗結果。本篇將介紹除了手動調整以外的一些方法,以及相關注意事項。
最簡單直覺的方法,是網格式的將每種組合做暴力搜尋;而這種方式除了自己撰寫多層的迴圈以外,也可以透過 scikit-learn 的 model_selection.GridSearchCV 來進行:
import numpy as np from sklearn.datasets import make_moons from sklearn.model_selection import GridSearchCV from xgboost import XGBClassifier x_train, y_train = make_moons(n_samples=800, shuffle=True, noise=0.1) x_test, y_test = make_moons(n_samples=200, shuffle=True, noise=0.1) param_grid = { 'n_estimators': [5, 10, 20], 'max_depth': [3, 5, 7], 'learning_rate': [0.01, 0.05, 0.1], } model = GridSearchCV(XGBClassifier(), param_grid) model.fit(x_train, y_train) pred = model.predict(x_test) print(f'Best parameters: {model.best_params_}') print(f'Accuracy: {np.mean(pred == y_test)*100:.2f}%')在上述範例中:
- GridSearchCV 的「CV」,是 cross validation 的意思,預設的切分方式是將原來的訓練資料分成五份,你可以透過「cv」參數去改變,例如「cv=7」。
- 承上,超參數選擇的依據,必須要用 validation data 來進行;切分出的測試資料只能在最後使用一次,請勿使用測試集來做超參數調整。
- 被調整的超參數們,其網格刻度不一定要一致,甚是不一定要是線性的,例如 learning rate 就經常使用對數刻度。
- GridSearchCV 在找到最佳超參數後,會自動用整個訓練集(不是只用某一個 fold)重新訓練模型。因此 model.predict() 使用的是已經用最佳參數訓練好的模型。
如果你基於運算量或其他各種原因,不希望做 cross validation,而是希望使用自己切分出的 validatino set 的話,則可以透過 scikit-learn 的 model_selection.PredefinedSplit 來輔助。使用時,我們需要將訓練集跟驗證集堆疊起來;並另外傳入一個變數,以代表堆疊完成的結果中,哪些要當作訓練集(標記為 -1),哪些要當作驗證集:
import numpy as np from sklearn.datasets import make_moons from sklearn.model_selection import GridSearchCV, PredefinedSplit from xgboost import XGBClassifier x_train, y_train = make_moons(n_samples=600, shuffle=True, noise=0.1) x_val, y_val = make_moons(n_samples=200, shuffle=True, noise=0.1) x_test, y_test = make_moons(n_samples=200, shuffle=True, noise=0.1) x_train_val = np.vstack([x_train, x_val]) y_train_val = np.hstack([y_train, y_val]) test_fold = np.concatenate([ np.full(len(x_train), -1), np.zeros(len(x_val)) ]) param_grid = { 'n_estimators': [5, 10, 20], 'max_depth': [3, 5, 7], 'learning_rate': [0.01, 0.05, 0.1], } ps = PredefinedSplit(test_fold) model = GridSearchCV(XGBClassifier(), param_grid, cv=ps) model.fit(x_train_val, y_train_val) pred = model.predict(x_test) print(f'Best parameters: {model.best_params_}') print(f'Validation score: {model.best_score_*100:.2f}%') print(f'Test accuracy: {np.mean(pred == y_test)*100:.2f}%')然而,一個可想而知的情況是,當要調整的超參數量一多,或者搜尋範圍一大的時候,暴力搜尋就難以適用。這時候可以考慮的選項之一是隨機搜尋,此方法對於不太重要的超參數比較不敏感,因此在同樣的計算資源限制下,仍有機會找到效果不錯的超參數。我們可以透過 scikit-learn 的 model_selection.RandomizedSearchCV 來進行,範例如下(預設是跑 10 次,調整方法請參閱官方文件):
import numpy as np from sklearn.datasets import make_moons from sklearn.model_selection import RandomizedSearchCV from xgboost import XGBClassifier x_train, y_train = make_moons(n_samples=800, shuffle=True, noise=0.1) x_test, y_test = make_moons(n_samples=200, shuffle=True, noise=0.1) param_grid = { 'n_estimators': [5, 10, 20], 'max_depth': [3, 5, 7], 'learning_rate': [0.01, 0.05, 0.1], } model = RandomizedSearchCV(XGBClassifier(), param_grid) model.fit(x_train, y_train) pred = model.predict(x_test) print(f'Best parameters: {model.best_params_}') print(f'Accuracy: {np.mean(pred == y_test)*100:.2f}%')或者,你也可以考慮先做粗略搜尋,再做精細搜尋,例如先找 a = [0.1, 1, 10, 100, 1000],假設找到 10 最好,再接著找 a = [6, 8, 10, 12, 14];甚至是如果你的資料量也很大的時候,也可以先用少量資料進行快速評估,淘汰表現差的超參數範圍以後,再用更多的資料評估剩下的範圍。對於後者,我們可以透過 scikit-learn 的 model_selection.HalvingGridSearchCV 或 model_selection.HalvingRandomSearchCV 來進行,以下示範後者:
import numpy as np from sklearn.datasets import make_moons from sklearn.experimental import enable_halving_search_cv from sklearn.model_selection import HalvingRandomSearchCV from xgboost import XGBClassifier x_train, y_train = make_moons(n_samples=800, shuffle=True, noise=0.1) x_test, y_test = make_moons(n_samples=200, shuffle=True, noise=0.1) param_grid = { 'n_estimators': [5, 10, 20], 'max_depth': [3, 5, 7], 'learning_rate': [0.01, 0.05, 0.1], } model = HalvingRandomSearchCV(XGBClassifier(), param_grid) model.fit(x_train, y_train) pred = model.predict(x_test) print(f'Best parameters: {model.best_params_}') print(f'Accuracy: {np.mean(pred == y_test)*100:.2f}%')在上述範例中,由於截至其最後被更新(2026 年四月初)為止,model_selection.HalvingRandomSearchCV 此一功能是以實驗性質的方式存在,因此必須先使用「from sklearn.experimental import enable_halving_search_cv」,才能進行 model_selection.HalvingRandomSearchCV 的載入。
如果你的狀況處理起來更困難,例如訓練一次模型就要耗去不少時間的話,則可以考慮用一些較進階的工具,來幫你比較「聰明」的找到效果不錯的超參數。這些工具的大致原理,通常是先建立一個代理模型來預測超參數的性能,並重複地選擇預期效果最佳的超參數組合,以及用實際嘗試地結果更新代理模型。以下將示範 Optuna 這一個工具:
import numpy as np import optuna from sklearn.datasets import make_moons from xgboost import XGBClassifier x_train, y_train = make_moons(n_samples=800, shuffle=True, noise=0.1) x_val, y_val = make_moons(n_samples=200, shuffle=True, noise=0.1) x_test, y_test = make_moons(n_samples=200, shuffle=True, noise=0.1) def objective(trial): param = { 'n_estimators': trial.suggest_int('n_estimators', 5, 20), 'max_depth': trial.suggest_int('max_depth', 3, 7), 'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.1), } model = XGBClassifier(**param) model.fit(x_train, y_train) pred = model.predict(x_val) accuracy = np.mean(pred == y_val) return accuracy optuna.logging.set_verbosity(optuna.logging.WARNING) study = optuna.create_study(direction='maximize') study.optimize(objective, n_trials=20, show_progress_bar=True) print(f'Best parameters: {study.best_params}') print(f'Best accuracy: {study.best_value*100:.2f}%') # Use best parameters to train a new model model = XGBClassifier(**study.best_params) model.fit(x_train, y_train) pred = model.predict(x_test) print(f'Final test accuracy: {np.mean(pred == y_test)*100:.2f}%')在上述範例中:
- 如果你想要觀看詳細的搜尋過程,可以移除「optuna.logging.set_verbosity(optuna.logging.WARNING)」這行。
- 我們需要定義一個評估的基準,並包裝成函式。範例中使用的是將 accuracy 最大化。
- 承上,原本的 param 字典必須改為包在該函式裡面,並用「trial.suggest_*」定義搜尋範圍,其中的「trial」是該函式的輸入參數。