RidgeCVは便利だよね
sklearn.linear_model
のRidgeCV
はfit
メソッドにより内部で交差検証を行ってハイパーパラメータのチューニングまで自動で実行してくれる便利なクラスです。
探索してくれるのは正則化の強度パラメータであるalphaだけで、かつその範囲もデフォルトでは0.1, 1.0, 10.0だけと心許ない感じではありますが、それでも様々なモデルの精度をざくっとみたいモデリングの初期には便利です。
しかしBruteforce探索の限界なのか、sklearnでbuilt-in CVが付属しているのはRidgeやLassoなどのパラメータが少なく計算も軽い線形モデルだけです。
そこで、Preferred Networks, Inc.開発のブラックボックス関数最適化のためのpythonパッケージであるoptunaを使用してsklearnのRIdgeCV風にSVRCVやXGBRegressorCVを作ってみたいと思います。
※本記事のコードは公式githubのexampleを参考に作成しました
データセット
みんな大好きいつものボストン住宅データセット
import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler from sklearn.datasets import load_boston from sklearn.model_selection import train_test_split X = pd.DataFrame(load_boston().data, columns=load_boston().feature_names) y = pd.DataFrame(load_boston().target, columns=['Price']) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3) scaler = StandardScaler() scaler.fit(X_train) X_sc_train = scaler.transform(X_train) X_sc_test = scaler.transform(X_test)
1. まずはRidgeCVから
まずはsklearn.linear_model.RIdgeCV
の再現から始めます。
from sklearn.linear_model import Ridge from sklearn.model_selection import KFold class RidgeCV(): model_cls = Ridge def __init__(self, n_trials=100): self.n_trials = n_trials def fit(self, X, y): if isinstance(X, np.ndarray): X = pd.DataFrame(X) y = pd.DataFrame(y) elif isinstance(X, pd.DataFrame): X = X.reset_index(drop=True) y = y.reset_index(drop=True) self.X = X self.y = y study = optuna.create_study(direction='maximize') study.optimize(self, n_trials=self.n_trials) self.best_trial = study.best_trial print() print("Best score:", round(self.best_trial.value, 2)) print("Best params:", self.best_trial.params) print() self.best_model = self.model_cls(**self.best_trial.params) self.best_model.fit(self.X, self.y) def predict(self, X): if isinstance(X, pd.Series): X = pd.DataFrame(X.values.reshape(1, -1)) elif isinstance(X, np.ndarray): X = pd.DataFrame(X) return self.best_model.predict(X) def score(self, X, y): if isinstance(X, np.ndarray): X = pd.DataFrame(X) y = pd.DataFrame(y) return self.best_model.score(X, y) def kfold_cv(self, model, splits=5): scores = [] kf = KFold(n_splits=splits, shuffle=True) for train_index, test_index in kf.split(self.X): X_train, X_test = self.X.iloc[train_index], self.X.iloc[test_index] y_train, y_test = self.y.iloc[train_index], self.y.iloc[test_index] model.fit(X_train, y_train) scores.append(model.score(X_test, y_test)) score = np.array(scores).mean() return score def __call__(self, trial): alpha = trial.suggest_loguniform('alpha', 1e0, 1e2) model = self.model_cls(alpha=alpha) score = self.kfold_cv(model) return score
※KFoldがインデックスが揃ってないデータフレームに対してよくわからない挙動をするのでpd.DataFrame
に変換したりreset_index()
をしたりとやや冗長なコードになってしまいました。
RidgeCV.fit(X, y)
が呼ばれるとoptunaが起動し、study.optimize(self, n_trials=self.n_trials)
でself.__call__()
によってハイパーパラメータの探索試行が行われます。試行ではあるハイパーパラメータの組み合わせにおけるmodelの精度を5分割KFoldでスコアリングします。試行はn_trials
回行い、最も結果がよかったパラメータの組み合わせでpredict()
やscore()
で使用されるモデルが訓練されます。
パラメータ最適化する場合
>>> model = RidgeCV(n_trials=30) >>> model.fit(X_sc_train, y_train) [I 2019-09-02 22:21:45,824] Finished trial#0 resulted in value: 0.697599103770344. Current best value is 0.697599103770344 with parameters: {'alpha': 62.76453278130266}. [I 2019-09-02 22:21:45,909] Finished trial#1 resulted in value: 0.6982891322971174. Current best value is 0.6982891322971174 with parameters: {'alpha': 2.850086378345985}. ~中略~ [I 2019-09-02 22:21:48,383] Finished trial#29 resulted in value: 0.7283317363854922. Current best value is 0.7290569859072776 with parameters: {'alpha': 9.503946089070118}. Best score: 0.73 Best params: {'alpha': 9.503946089070118} >>> print(model.score(X_sc_test, y_test)) 0.6985819125601394
デフォルトパラメータで訓練する場合
>>> model = Ridge() >>> model.fit(X_sc_train, y_train) >>> print(model.score(X_sc_test, y_test)) 0.6988474458318901
Ridgeでボストンデータセットだとパラメータ探索してもしなくても0.69程度のスコアでした。
2. GridSearchだとけっこうしんどいSVR
SVRは汎用性が高く個人的には回帰問題のファーストピックですが、パラメータサーチをきちんと行わないと精度がでないのが面倒なところです。
from sklearn.svm import SVR class SVRCV(RidgeCV): model_cls = SVR def __call__(self, trial): kernel = trial.suggest_categorical('kernel', ['rbf', 'linear']) C = trial.suggest_loguniform('C', 1e0, 1e2) epsilon = trial.suggest_loguniform('epsilon', 1e-1, 1e1) model = self.model_cls(kernel=kernel, C=C, epsilon=epsilon, gamma='auto') score = self.kfold_cv(model) return score
RidgeCVと構造はほぼ同じなので継承によってシンプルに書けます。
パラメータ最適化する場合
>>> model = SVRCV(n_trials=100) >>> model.fit(X_sc_train, y_train) ~略~ [I 2019-09-02 22:34:51,280] Finished trial#99 resulted in value: 0.8020224328731113. Current best value is 0.8599302680077867 with parameters: {'kernel': 'rbf', 'C': 95.38592381835898, 'epsilon': 1.6773144109569142}. Best score: 0.86 Best params: {'kernel': 'rbf', 'C': 95.38592381835898, 'epsilon': 1.6773144109569142} >>> print(model.score(X_sc_test, y_test)) 0.8794262116660849
デフォルトパラメータで訓練する場合
>>> model = SVR() >>> model.fit(X_sc_train, y_train) >>> print(model.score(X_sc_test, y_test)) 0.6667099218666681
おお、SVRの場合はパラメータサーチする/しないで大きく精度が変わりました。 デフォルトパラメータではscoreは0.66とRidge回帰以下の精度ですが、パラメータ探索によって0.87まで向上しています。 ちなみにn_trials=100で実行時間は15秒ほど。
3. 本命のXGBRegressor
とにかくパラメータが多いXGBもお気楽チューニング。
import xgboost as xgb class XGBRegressorCV(RidgeCV): model_cls = xgb.XGBRegressor def __call__(self, trial): booster = trial.suggest_categorical('booster', ['gbtree', 'dart']) alpha = trial.suggest_loguniform('alpha', 1e-8, 1.0) max_depth = trial.suggest_int('max_depth', 1, 9) eta = trial.suggest_loguniform('eta', 1e-8, 1.0) gamma = trial.suggest_loguniform('gamma', 1e-8, 1.0) grow_policy = trial.suggest_categorical( 'grow_policy', ['depthwise', 'lossguide']) if booster == 'gbtree': model = self.model_cls(silent=1, booster=booster, alpha=alpha, max_depth=max_depth, eta=eta, gamma=gamma, grow_policy=grow_policy) elif booster == 'dart': sample_type = trial.suggest_categorical('sample_type', ['uniform', 'weighted']) normalize_type = trial.suggest_categorical('normalize_type', ['tree', 'forest']) rate_drop = trial.suggest_loguniform('rate_drop', 1e-8, 1.0) skip_drop = trial.suggest_loguniform('skip_drop', 1e-8, 1.0) model = self.model_cls(silent=1, booster=booster, alpha=alpha, max_depth=max_depth, eta=eta, gamma=gamma, grow_policy=grow_policy, sample_type=sample_type, normalize_type=normalize_type, rate_drop=rate_drop, skip_drop=skip_drop) score = self.kfold_cv(model) return score
選ばれたbooster
に応じてif文で使用するパラメータを変更しています。こういう処理を楽に記述できるのがgoodですね。
パラメータ最適化する場合
>>> model = XGBRegressorCV(n_trials=100) >>> model.fit(X_sc_train, y_train) ~略~ [I 2019-09-02 22:51:05,374] Finished trial#99 resulted in value: 0.8502358779185025. Current best value is 0.8829819998433361 with parameters: {'booster': 'gbtree', 'alpha': 7.807503234311212e-06, 'max_depth': 5, 'eta': 0.09454508257094363, 'gamma': 1.1698753557081089e-07, 'grow_policy': 'depthwise'}. Best score: 0.88 Best params: {'booster': 'gbtree', 'alpha': 7.807503234311212e-06, 'max_depth': 5, 'eta': 0.09454508257094363, 'gamma': 1.1698753557081089e-07, 'grow_policy': 'depthwise'} >>> print(model.score(X_sc_test, y_test)) 0.90519430053149
デフォルトパラメータで訓練する場合
>>> model = xgb.XGBRegressor() >>> model.fit(X_sc_train, y_train) >>> print(model.score(X_sc_test, y_test)) 0.8988995910099546
パラメータチューニングする/しないに関わらずスコアは0.9程度でした。まあボストンデータセットの難易度が低いためカリカリにチューニングせずとも高精度が出るのでしょう。 試行100回で実行時間は1分程度でした。
まとめ
optunaは使いやすくなった Hyperopt という印象でとても便利!! 今回は一回の試行が軽いので使用していませんがDLモデルのチューニングのために筋の悪い試行を途中で打ち切るpruning機能もあるようです。興味のある方はgithubのexamplesを見てみましょう。
便利パッケージを公開してくれるPFNet先生に圧倒的感謝。。。