機械学習エンジニアは様々なタスクをこなさなくてはいけません。ビジネスへの影響や軸となる指標などを決める要件定義から始まり、データの収集や前処理、そしてモデル構築や調整など、機械学習の実装までのタスクをあげればきりがありません。(組織の規模にもよりますが)
本稿ではモデルの調整、さらに厳密に言うと「ハイパーパラメータチューニング」と呼ばれるタスクを機械学習初心者向けに徹底解説します。
ハイパーパラメータの概要や一般的に使用される3つのハイパーパラメータ最適化の手法を詳しく解説します。また、Kaggleで公開されている人事データを使用して、XGBoostのハイパーパラメータのチューニングを異なる手法を使って実装していきます。(参考:Kaggleとは?)
初心者向けに実装までの全コードを本記事に公開しています。是非、本稿に沿ってご自身の環境で実装してみてください。下記の環境・バージョンで動作を確認しています。Google Colabではライブラリ、Pythonのバージョンが適宜更新されますのでご留意ください。
確認済み動作環境
【Google Colab】
2020年3月30日執筆時点の構成
参照:Google Colabの使い方
【macOS】
macOS Mojvabe 10.14.6
Python 3.6.9
NumPy 1.14.6
Pandas 0.22.0
Scikit-Learn 0.20.1
XGBoost 1.0.2
この記事の目次
ハイパーパラメータとは
ハイパーパラメータ(英語:Hyperparameter)とは機械学習アルゴリズムの挙動を設定するパラメータをさします。少し乱暴な言い方をすると機械学習のアルゴリズムの「設定」です。
この設定(ハイパーパラメータの値)に応じてモデルの精度やパフォーマンスが大きく変わることがあります。例えば男女を分類するモデルを構築していた場合、特に調整を行わずに初期設定のままモデリングを行なった結果、最初は90%の正解率を得ることができたとします。90%の精度では使い物にならないと上司に怒られたので、ハイパーパラメータ(モデルの設定)を調整したところ93%へ改善することがあります。ハイパーパラメータチューニング自動化の動きもありますが、一般的には「人間」が「手動」で調整を行なっていきます。
近年、日本でも大人気のKaggleもハイパーパラメータのチューニングは必須のスキルです。Kaggleのようなデータ分析競技では0.01%の差で順位を争うことも少なくありません。モデルのハイパーパラメータを適切に調整してスコアを伸ばしていくのはKaggleの基本技です。
前述した通りハイパーパラメータは機械学習アルゴリズムの「設定」です。つまり、機械学習手法ごとに異なる値のハイパーパラメータがあります。
手法によってハイパーパラメータの数は大きく異なります。ここ数年でKaggleの上位解法の常連でもあるXGBoostやLightGBMなどはハイパーパラメータの数は比較的多いです。対して、サポートベクターマシン(SVM)やランダムフォレストなどの手法は設定可能な項目(つまりハイパーパラメータの数)は比較的少ないです。
画像分析や自然言語解析などではディープラーニング(深層学習)が使われることが多いです。例えば画像認識であれば畳み込みニューラルネットワーク(CNN)、自然言語解析ではLSTMなどが使用されます。これらの深層学習の手法にも手動で設定を行うハイパーパラメータがあり、細かいチューニングにより推測精度が改善することもしばしばあります。
機械学習の実装において「ハイパーパラメータチューニング」は避けて通れない必須のタスクです。
ハイパーパラメータチューニングの3つの手法
ハイパーパラメータの調整には主に3つの手法があります。どの手法にもメリット・デメリットがあります。本記事の後半部分ではPythonを使って3つの手法の実装を行いますが、まずはそれぞれの手法の概要を解説します。
その1 グリッドサーチ(Grid Search)
グリッドサーチとは与えられたハイパーパラメータの候補の値の全パターンのモデル構築を行う手法です。例えば設定Aと設定Bのハイパーパラメータを調整する場合、設定Aには「1、3、5」の値、設定Bには「True、False」の値を候補として指定します。グリッドサーチではこれらの値の全ての組み合わせを使ってモデル構築を行い結果を検証します。この例の場合は設定Aが3つ、設定Bは2つの候補が指定されていますので全部で6回のモデル訓練が異なる設定の値(ハイパーパラメータ)を使って行われる訳です。
グリッドサーチは調整するハイパーパラメータの数が少ない場合やある程度の「あたり」がついている場合に使用することが多い手法です。全組み合わせのモデル訓練を行うので、大規模データなど訓練時間が長い場合はグリッドサーチを使うことは滅多にありません。
【メリット】
調整する値の「あたり」が付いている場合は◎
調整する値の数が少ない場合は◎
【デメリット】
モデル訓練回数が増えるので時間が掛かる
計算コストが非常に高い
その2 ランダムサーチ(Random Search)
前項のグリッドサーチは候補の値の全パターンのモデル訓練を行いました。対してランダムサーチは候補の値をランダムに組み合わせたモデル訓練を行いハイパーパラメータを検証する手法です。
ランダムサーチの場合は検証するためのモデル訓練の回数を指定します。例えば候補Aには10個の値、候補Bには5個、候補Cは30個の値があるとします。この場合、グリッドサーチでは全組み合わせ1500回(10 x 5 x 30)のモデル訓練を行います。仮に1回の訓練で30分要するとした場合、なんと31日もかかってしまいます。現実的ではありません。
そこでランダムサーチが役に立ちます。ランダムサーチで異なる組み合わせのハイパーパラメータを用いてモデル訓練を行い検証します。ランダムに検証をするのである意味「運任せ」の要素もありますが、何度か異なる値でモデル訓練を行う事で調整する方向性が見えてくることもあります。
【メリット】
調整する値が多くても対応することが可能
【デメリット】
ランダムに検証するので「運任せ」の要素あり
その3 ベイズ最適化(Bayesian Optimization)
ベイズ最適化とは不確かさを利用して次に探索を行うべき値を探していく最適化アルゴリズムの一種です。目的関数(Acquisition Function)を推定する代理モデル(Surrogate Model)にはガウス過程が使われます。
複雑に聞こえますが、ベイズ最適化をざっくりと解説すると「前回の結果を基に次に調べる値を決めていく」手法です。次に調べる値の決め方に工夫があります。
ベイズ最適化は「Exploration(探索)」と「Exploitation(活用)」の2つの戦略を使って最適化を順次的に行います。Exploitation(活用)とは以前にやってみて良い結果が出たので継続してその近辺を調べてみることです。対してExploration(探索)はもっと良い結果があると考えてあえて異なる部分を調べることをさします。
イメージしやすいように例を示します。よく行く居酒屋で飲み物を選ぶ時を想像してみてください。先週来た時は泡盛を頼み、とても美味しいのを覚えています。今回も似たような泡盛を頼めば恐らく前回と同様に楽しめることが想像できます。これがExploitation(活用)です。しかしメニューには日本酒や焼酎など、今まで頼んだことが無い飲み物もたくさんあります。もしかすると泡盛よりも自分好みのお酒があるかもしれません。そこで今回は日本酒を頼んでみることにします。これがExploration(探索)です。
このようにベイズ最適化では「前回の結果を踏まえて次をバランス良く試す」ことが可能です。ベイズ最適化を厳密に理解したい方は「機械学習スタートアップシリーズ ベイズ推論による機械学習入門」(リンク先 Amzon)がオススメです。
「グリッドサーチ」「ランダムサーチ」「ベイズ最適化」の手法を使って、実際にハイパーパラメータチューニングを行ってみましょう。
ハイパーパラメータチューニングの実装
Kaggleで公開されている「Human Resources Data Set」(以後、人事データセット)を使って、XGBoostのハイパーパラメータを3つの手法を使って調整してみましょう。
人事データセットには従業員の性別や人種、所属部署などの情報を保持しています。また各従業員の時給や評価、従業員の満足度なども含まれます。全部で35カラムを保持するデータセットですが、本稿ではその中から12カラムを抽出して使用します。
データセットは以下のKaggleのページからCSVファイルとしてダウンロード可能です。ダウンロードにはKaggleの無料会員登録が必要です。
https://www.kaggle.com/rhuebner/human-resources-data-set
Kaggleにログイン後に上記URLの「Download」からHRDataset_v13.csvが取得できます。ご自身のローカルマシンにHRDataset_v13.csvを保存してください。
本稿では人事データセットを使い、各従業員の性別や結婚歴などを特徴量として、辞職したかどうか(カラムTermdの1は辞職、0は現職)をXGBoostを利用して推測を行います。異なる手法でハイパーパラメータチューニングを行い、それぞれの手法の結果を比較してみましょう。
こちらのデータセットはNew England College of Businessの教材の一環として Drs. Rich Huebner氏とCarla Patalano氏がデザインしたデータセットです。データセットの詳細については以下のページをご参照ください。
https://rpubs.com/rhuebner/HRCodebook-13
データの読み込みと確認
まずは本稿で使用するライブラリのインポートを行います。Pandasはデータの読み込みやデータ変形・分析を行うライブラリです。詳しくはPandas 入門コースをご参照ください。
[In ] : # 基本ライブラリ import pandas as pd # Scikit-learn from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, confusion_matrix from sklearn.model_selection import GridSearchCV, RandomizedSearchCV, KFold # XGBoost import xgboost as xgb # Matplotlibのインライン表示 %matplotlib inline
KaggleからダウンロードしたHRDataset_v13.csvをpandasのデータフレームとして読み込みましょう。CSVファイルのカラムEmployee_Name(従業員名)をデータフレームのインデックスラベルとして使用します。
本稿ではCSVファイルの一部のカラムのみを利用します。必要なカラムのみ参照してデータフレームを更新後にheadメソッドで最初の5行を表示してみましょう。
[In ]: df = pd.read_csv('HRDataset_v13.csv', index_col=['Employee_Name']) df = df[['PayRate','Termd','Position','Sex','MaritalDesc','RaceDesc', 'Department','ManagerName','RecruitmentSource', 'EngagementSurvey','EmpSatisfaction','SpecialProjectsCount',]] df.head()
[Out ]: PayRate Termd Position Sex MaritalDesc RaceDesc Department ManagerName RecruitmentSource EngagementSurvey EmpSatisfaction SpecialProjectsCount Employee_Name Brown, Mia 28.50 0.0 Accountant I F Married Black or African American Admin Offices Brandon R. LeBlanc Diversity Job Fair 2.04 2.0 6.0 LaRotonda, William 23.00 0.0 Accountant I M Divorced Black or African American Admin Offices Brandon R. LeBlanc Website Banner Ads 5.00 4.0 4.0 Steans, Tyrone 29.00 0.0 Accountant I M Single White Admin Offices Brandon R. LeBlanc Internet Search 3.90 5.0 5.0 Howard, Estelle 21.50 1.0 Administrative Assistant F Married White Admin Offices Brandon R. LeBlanc Pay Per Click - Google 3.24 3.0 4.0 Singh, Nan 16.56 0.0 Administrative Assistant F Single White Admin Offices Brandon R. LeBlanc Website Banner Ads 5.00 3.0 5.0
このデータセットを初めて扱う方も多いかと思います。簡単なEDA(探索的データ解析)を行いデータを確認してみましょう。まずはサイズを確認してみます。
[In ]: df.shape
[Out ]: (401, 12)
401行12列のデータセットです。続いてisnaメソッドとsumメソッドを使って、欠損値の有無を確認してみましょう。欠損値(NaN、Not a Numberの略)とは値が欠落している箇所を意味します。
[In ]: df.isna().sum()
[Out ]: PayRate 91 Termd 91 Position 91 Sex 91 MaritalDesc 91 RaceDesc 91 Department 91 ManagerName 91 RecruitmentSource 91 EngagementSurvey 91 EmpSatisfaction 91 SpecialProjectsCount 91 dtype: int64
各カラムで91件の欠損値が存在します。次の項「前処理」でこれらの欠損値のデータの対処を行います。
次は重複するデータが存在するのか確認をしてみましょう。pandasのデータフレームのduplicatedメソッドは重複した行があればTrue、それ以外はFalseを戻します。duplicatedメソッドのkeep引数は重複した行の抽出方法を指定することが可能です。Falseと指定した場合、重複した全ての行をTrueとして戻します。重複した行の最初の3行を参照して確認してみましょう。全ての値がNaN(欠損値)なのが確認できます。
[In ]: df[df.duplicated(keep=False)].head(3)
[Out ]: PayRate Termd Position Sex MaritalDesc RaceDesc Department ManagerName RecruitmentSource EngagementSurvey EmpSatisfaction SpecialProjectsCount Employee_Name NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
人事データセットには性別(Sex)や結婚歴(MaritalDesc)、肩書き(Position)など、文字列を値としてもつカラムがあります。カラムPositionで頻度数が高い上位10個の要素の値をvalue_coutnsメソッドとplotメソッドを使い円グラフを描画してみましょう。約半数はProduction Technician I(生産技術者I類)が占めています。続いてProduction Technician II(生産技術者II類)、Area Sales Manager(地域販売管理者)の順に多いようです。
[In ]: df['Position'].value_counts()[0:10].plot(kind='pie', figsize=(8,8))
同じ要領でRecruitmentSource(採用経由)も確認してみましょう。検索エンジン(Search Engine – Google Bing Yahoo)や既存従業員紹介(Employee Referral)などの値が確認できます。
[In ]: df['RecruitmentSource'].value_counts()[0:10].plot(kind='pie', figsize=(8,8))
最後に本稿の目的変数でもある従業員の辞職の有無を示すカラムTermdの分布も確認してみましょう。こちらは1=辞職した、0=辞職していないの値を持つカラムです。value_countsメソッドでそれぞれの値の頻度数を確認してみます。dropna引数(初期値 True)は欠損値の扱いを制御する引数です。Falseを指定して欠損値を含む全要素の頻度数を確認してみます。約3割強がすでに退職していることが確認できます。
[In ]: df['Termd'].value_counts(dropna=False)
[Out ]: 0.0 207 1.0 103 NaN 91 Name: Termd, dtype: int64
データの前処理
前の項で確認した通り、人事データセットには欠損値が含まれていました。目的変数にも欠損値が含まれていることから、これらのレコードはデータセットから除外するべきです。
dropnaメソッドは欠損値を含む行を除外したデータフレームを戻します。inplace引数(初期値 False)をTrueへ変更して、データフレームに直接変更を加えることもできます。shape属性で確認をすると、CSVファイルには401行のレコードがありましたが、欠損した行を除外したため310行へ変更されているのが確認できます。
[In ]: df.dropna(inplace=True) df.shape
[Out ]: (310, 12)
重複したデータが存在するのかも確認してみましょう。欠損値を除外した後は重複した行も0なのが確認できます。
[In ]: df.duplicated().sum()
[Out ]: 0
人事データセットには文字列の値を含むカラムが存在します。機械学習で使われる多くのモデリング手法は文字列の値を訓練データとして使用することは出来ません。そこで文字列の値を「0と1」で表現するダミー変数が使われることが多いです。ダミー変数とは数字ではないデータを数字に変換する手法をさします。
pandasのget_dummies関数は第1引数へデータフレームを渡し、columns引数で指定したカラムをダミー変数へ変換したデータフレームを戻します。文字列の値を持つPosition(肩書き)、Sex(性別)、MaritalDesc(結婚歴)、RaceDesc(人種)、Department(所属部署)、ManagerName(上司の名前)、RecruitmentSource(採用経由)をcolumns引数へ渡してダミー変数へ変換してみましょう。元は12列のデータですが100列へ変形しているのが確認できます。
[In ]: dummy_cols = ['Position','Sex','MaritalDesc','RaceDesc', 'Department','ManagerName','RecruitmentSource'] df = pd.get_dummies(df, columns=dummy_cols) df.shape
[Out ]: (310, 100)
続いて人事データセットを訓練データとテストデータへ分割を行いましょう。訓練データとはモデルの学習に使用するデータです。対してテストデータとは訓練データを学習後のモデルの評価をするために使用するデータです。
まずはデータフレームdfを説明変数(X)と目的変数(y)へ分割をします。本稿では目的変数は従業員の辞職を示すカラムTermdです。それ以外は説明変数(特徴量とも呼ばれます)となります。dropメソッドとインデックス参照を使い変数Xとyを作成しましょう。念の為shape属性でデータのサイズを確認します。
[In ]: X = df.drop(['Termd'], axis=1) y = df['Termd'] X.shape, y.shape
[Out ]: ((310, 99), (310,))
続いてデータを訓練・テストデータへ分割します。分割はScikit-Learnのtrain_test_split関数が便利です。test_size引数へ0.3と指定することで訓練データ7割:テストデータ3割へ分割が可能です。またrandom_state引数へ値を指定することで乱数のシードを固定できます。シードを固定することにより、分割の処理が毎回同じ乱数が使われるため同じ結果を得ることができます。分割後のデータのサイズをshape属性で確認してみましょう。
[In ]: seed = 42 X_train, X_test, y_train, y_test = train_test_split(X,y, test_size=.3, random_state=seed) X_train.shape, y_train.shape, X_test.shape, y_test.shape
[Out ]: ((217, 99), (217,), (93, 99), (93,))
訓練データとテストデータの目的変数(カラムTermdの値)の分布を確認しておきましょう。value_countsメソッドとconcat関数を使いy_trainとy_testの値の分布をデータフレームとして戻します。訓練データ(y_train)には辞職76名、現職141名です。y_testには辞職27名、現職66名なのが確認できます。
[In ]: pd.concat([y_train.value_counts(), y_test.value_counts()], axis=1)
[Out :] Termd Termd 0.0 141 66 1.0 76 27
本稿では非常に簡易的なEDAしか行いませんでしたが、興味がある方はより深いデータ分析も行ってみましょう。EDAで行うべきポイントを2点ほど示しておきます。
- 男女別(カラムSex)間で辞職率に違いはありますか?(ヒント:GroupBy)
- 従業員の満足度(EmpSatisfaction)と辞職(Termd)に相関性はありますか?(ヒント:corrメソッド)
ベースラインのモデル訓練
本稿ではXGBoostを使い人事データセットの説明変数を用いて退職の有無(カラムTermd)を分類推測を行います。まずは適当なハイパーパラメータの値を設定して基準となるベースラインモデルの訓練と評価を行ってみましょう。
XGBoostは勾配ブースティングのフレームワークです。前述した通りハイパーパラメータの数はモデリング手法や使用するフレームワークにより大きく異なります。XGBoostは設定するべき項目(ハイパーパラメータの数)が他の手法(例えばランダムフォレスト)と比較して多いです。
XGBoostのハイパーパラメータの数は細かいのを含めると50以上ありますが、その中でも特にモデリングに影響の高い以下のハイパーパラメータの調整を行います。XGBoostのハイパーパラメータの詳細は「XGBoost入門コース」で解説をしています。
- min_child_weight
- max_depth
- colsample_bytree
- subsample
それではベースラインとなるモデル訓練を行いましょう。まずはXGBoostへ設定するハイパーパラメータを辞書型で作成します。
[In ]: params = {'metric':'error', 'objective':'binary:logistic', 'n_estimators':50000, 'booster': 'gbtree', 'learning_rate':0.01, 'min_child_weight':1, 'max_depth':5, 'random_state':seed, 'colsample_bytree':1, 'subsample':1, }
XGBoostのScikit-learn APIのXGBClassifierを使いモデル訓練を行います。set_paramsメソッドへハイパーパラメータを渡して設定を行います。fitメソッドはモデル訓練を実行するメソッドです。説明変数(X_train)、目的変数(y_train)を指定して訓練データの学習を行います。ここでは前述した通りテストデータ(X_test、y_test)が使用されていない部分に注目しましょう。
勾配ブースティングの学習プロセスは繰り返し処理(イテレーション)となります。一つ前の学習結果の誤差を繰り返し学習する手法です。(参照:XGBoost 入門)
何回繰り返すかはハイパーパラメータのn_estimatorsの値で指定します。本稿では50,000回と指定しました。これは意図的に大きな値を指定しています。
XGBoostの繰り返し回数はハイパーパラメータの値や訓練データにより変動するため、決め打ち(例えば100回)と決めてしまうと、過学習(Over Fitting)や未学習(Under Fitting)となってしまうためです。そのため事前に大きな値を指定して、early_stopping_roundsを使用するのが一般的です。
early_stopping_roundsとは毎回の学習完了時にテストデータでモデルの評価を行い、評価指標が一定の回数を改善しなくなった時点で学習をストップさせます。これにより過学習・未学習を避けて適切な繰り返し学習回数でモデルを構築することが可能です。今回はearly_stopping_rounds引数へ50と指定していますので、50回以上モデルが改善しない場合に学習プロセスが止まります。
分類問題の評価指標には様々な種類の指標がありますが、本稿では正解率(accuracy)を使用します。これは推測結果と実際の値のがどれくらい一致しているかを示す指標です。XGBoostでは評価指標として正解率が実装されていませんので、その逆のエラー率を使用します。 (正解率には弱点もありますのでご留意ください。)
それではベースラインとなるモデルの訓練を開始しましょう。
[In ]: cls = xgb.XGBClassifier() cls.set_params(**params) cls.fit(X_train, y_train, early_stopping_rounds=50, eval_set=[(X_test, y_test)], eval_metric='error', verbose=1)
[Out ]: [0] validation_0-error:0.32258 Will train until validation_0-error hasn't improved in 50 rounds. [1] validation_0-error:0.37634 [2] validation_0-error:0.37634 〜(略)〜 [50] validation_0-error:0.36559 Stopping. Best iteration: [0] validation_0-error:0.32258 XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1, colsample_bynode=1, colsample_bytree=1, gamma=0, gpu_id=-1, importance_type='gain', interaction_constraints=None, learning_rate=0.01, max_delta_step=0, max_depth=5, metric='error', min_child_weight=1, missing=nan, monotone_constraints=None, n_estimators=50000, n_jobs=0, num_parallel_tree=1, objective='binary:logistic', random_state=42, reg_alpha=0, reg_lambda=1, scale_pos_weight=1, subsample=1, tree_method=None, validate_parameters=False, verbosity=None)
best_score属性は最もスコアが良かった回のスコアを表示します。best_iteration属性は最もスコアが良かった回数を確認することができます。今回のハイパーパラメータ(設定)では一番最初の回の訓練のスコアがベストでした。(エラー率なので低いほうが推測制度が高いことを示します)
[In ]: print('best score : ', cls.best_score) print('best iter : ', cls.best_iteration)
[Out ]: best score : 0.322581 best iter : 0
実際にモデルがどのような推測を行ったのか混同行列と正解率をScikit-learnの関数を使って算出してみましょう。
[In ]: pred_1 = cls.predict(X_test) baseline = accuracy_score(y_test, pred_1) baseline
[Out ]: 0.6774193548387096
[In ]: confusion_matrix(y_test, pred_1)
[Out ]: array([[58, 8], [22, 5]])
今回使用したハイパーパラメータの値では正解率は67.74%となっているのが確認できます。混同行列の詳細はロジスティック回帰 入門コースで詳しく解説しています。
グリッドサーチ(GridSearch)
ハイパーパラメータチューニングをグリッドサーチで行ってみましょう。前述した通りグリッドサーチはハイパーパラメータの候補の値の全組み合わせのモデル訓練を行い検証する手法です。
グリッドサーチはScikit-learnのGridSearchCV関数がとても便利です。CVはCross Validationの略で交差検証を意味します。
まずはハイパーパラメータに設定する候補の値を辞書型で作成しましょう。metricやobjectiveなどの値には1つしか値が設定されていません。これらのハイパーパラメータは検証する対象ではないので1つの値のみを設定しています。min_child_weightにはリストで「1」と「5」の値が設定されています。このように、検証するハイパーパラメータには複数の候補の値を設定します。
今回はmin_child_weight、max_depth、colsample_bytree、subsampleのハイパーパラメータで全て2つの候補の値を設定しています。グリッドサーチは全組み合わせのモデリングを行いますので16通りの異なるハイパーパラメータの値を持つXGBoostの分類器の訓練が行われます。
それではグリッドサーチでハイパーパラメータチューニングをやってみましょう。
[In ]: cv_params = {'metric':['error'], 'objective':['binary:logistic'], 'n_estimators':[50000], 'random_state':[seed], 'booster': ['gbtree'], 'learning_rate':[0.01], 'min_child_weight':[1,5], 'max_depth':[1,3], 'colsample_bytree':[0.5,1.0], 'subsample':[0.5,1.0] } cls = xgb.XGBClassifier() cls_grid = GridSearchCV(cls, cv_params, cv=cv=KFold(2, random_state=seed), scoring='accuracy', iid=False) cls_grid.fit(X_train, y_train, early_stopping_rounds=50, eval_set=[(X_test, y_test)], eval_metric='error', verbose=0)
[Out ]: GridSearchCV(cv=2, error_score='raise-deprecating', estimator=XGBClassifier(base_score=None, booster=None, colsample_bylevel=None, colsample_bynode=None, colsample_bytree=None, gamma=None, gpu_id=None, importance_type='gain', interaction_constraints=None, learning_rate=None, max_delta_step=None, max_depth=None, min_child_w...pos_weight=None, subsample=None, tree_method=None, validate_parameters=False, verbosity=None), fit_params=None, iid=False, n_jobs=None, param_grid={'metric': ['error'], 'objective': ['binary:logistic'], 'n_estimators': [50000], 'random_state': [42], 'booster': ['gbtree'], 'learning_rate': [0.01], 'min_child_weight': [1, 5], 'max_depth': [1, 3], 'colsample_bytree': [0.5, 1.0], 'subsample': [0.5, 1.0]}, pre_dispatch='2*n_jobs', refit=True, return_train_score='warn', scoring='accuracy', verbose=0)
GridSearchCVのbest_params_属性は交差検証で得た最も評価スコアが良いハイパーパラメータの値を戻します。best_score_属性は最も良い評価スコアを確認可能です。ベースラインで設定したハイパーパラメータの値を使ったモデルが最も良い結果となっているのが確認できます。
[In ]: print(cls_grid.best_params_) print(cls_grid.best_score_)
[Out ]: {'booster': 'gbtree', 'colsample_bytree': 0.5, 'learning_rate': 0.01, 'max_depth': 3, 'metric': 'error', 'min_child_weight': 1, 'n_estimators': 50000, 'objective': 'binary:logistic', 'random_state': 42, 'subsample': 0.5} 0.6913013931362555
GridSearchCVのbest_estimator_属性は交差検証で最も評価の高いモデルを使用することが可能です。predictメソッドへテストデータの特徴量(X_test)を渡して正解率を算出してみましょう。ベースラインでは67.74%でしたが、今回は72.04%と改善しているのが確認できます。
[In ]: pred_2 = cls_grid.best_estimator_.predict(X_test) grid_score = accuracy_score(y_test, pred_2) grid_score
[Out ]: 0.7204301075268817
モデルがどのような推測結果を戻したのか混同行列で確認をしてみましょう。
[In ]: confusion_matrix(y_test, pred_2)
[Out ]: array([[66, 0], [26, 1]])
ランダムサーチ(RandomizedSearchCV)
ランダムサーチはハイパーパラメータの候補の値をランダムに組み合わせてデル訓練を行います。こちらもScikit-learnのRandomizedSearchCV関数を使用することで簡単に実装することが可能です。
グリッドサーチは全組み合わせのモデル訓練を行うため、候補の値を多く設定すると訓練時間が膨大にかかってしまいます。ランダムサーチは交差検証の回数を指定可能ですので、候補の値を増やしても膨大な時間がかかることはありません。
グリッドサーチで使った4つのハイパーパラメータの調整をしてみましょう。グリッドサーチでは各パラメータで2つずつの候補ですが、今回はそれぞれ10個の候補の値を指定します。
RandomizedSearchCV関数のn_iter引数でランダムサーチの繰り返し回数の指定が可能です。30を指定してランダムサーチでハイパーパラメータの最適化をしてみましょう。
[In ]: cv_params = {'metric':['error'], 'objective':['binary:logistic'], 'n_estimators':[50000], 'random_state':[seed], 'boosting_type': ['gbdt'], 'learning_rate':[0.01], 'min_child_weight':[1,2,3,4,5,6,7,8,9,10], 'max_depth':[1,2,3,4,5,6,7,8,9,10], 'colsample_bytree':[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0], 'subsample':[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0], } cls = xgb.XGBClassifier() cls_rdn = RandomizedSearchCV(cls, cv_params, cv=cv=KFold(2, random_state=seed), random_state=seed, n_iter=30, iid=False, scoring='accuracy') cls_rdn.fit(X_train, y_train, early_stopping_rounds=50, eval_set=[(X_test, y_test)], eval_metric='error', verbose=0)
[Out ]: RandomizedSearchCV(cv=2, error_score='raise-deprecating', estimator=XGBClassifier(base_score=None, booster=None, colsample_bylevel=None, colsample_bynode=None, colsample_bytree=None, gamma=None, gpu_id=None, importance_type='gain', interaction_constraints=None, learning_rate=None, max_delta_step=None, max_depth=None, min_child_w...pos_weight=None, subsample=None, tree_method=None, validate_parameters=False, verbosity=None), fit_params=None, iid=False, n_iter=30, n_jobs=None, param_distributions={'metric': ['error'], 'objective': ['binary:logistic'], 'n_estimators': [50000], 'random_state': [42], 'boosting_type': ['gbdt'], 'learning_rate': [0.01], 'min_child_weight': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'max_depth': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'colsample_bytree': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], 'subsample': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]}, pre_dispatch='2*n_jobs', random_state=42, refit=True, return_train_score='warn', scoring='accuracy', verbose=0)
最も評価スコアの高いハイパーパラメータの値と評価スコアを確認してみましょう。
[In ]: print(cls_rdn.best_params_) print(cls_rdn.best_score_)
[Out ]: {'subsample': 0.7, 'random_state': 42, 'objective': 'binary:logistic', 'n_estimators': 50000, 'min_child_weight': 3, 'metric': 'error', 'max_depth': 5, 'learning_rate': 0.01, 'colsample_bytree': 0.5, 'boosting_type': 'gbdt'} 0.6498895684675501
ベースラインは67.74%、グリッドサーチは72.04%の正解率でした。ランダムサーチで得たハイパーパラメータの値を使用して正解率と混同行列を算出してみましょう。
[In ]: pred_3 = cls_rdn.best_estimator_.predict(X_test) rdn_score = accuracy_score(y_test, pred_3) rdn_score
[Out ]: 0.7419354838709677
[In ]: confusion_matrix(y_test, pred_3)
[Out ]: array([[65, 1], [23, 4]])
ランダムサーチの正解率は74.19%でした。ベースライン・グリッドサーチと比較すると正解率が少し改善しているのが確認できます。
ベイズ最適化(BayesianOptimization)
最後はベイズ最適化を使いXGBoostのハイパーパラメータの最適化を行ってみましょう。ベイズ最適化ですがオープンソースのライブラリが複数あります。本稿では「BayesianOptimization」を利用してハイパーパラメータチューニングを行ってみましょう。
BayesianOptimizationをインストールしていない方はpipでインストールが可能です。環境に応じてインストールをお願いします。
$pip install bayesian-optimization[In ]: from bayes_opt import BayesianOptimization
BayesianOptimizationで最適化を行う関数を定義します。調整を行うハイパーパラメータを引数として持ち、モデル訓練とテストデータを使った推測値を算出して、最後い正解率の値を戻す関数を定義します。
[In ]: def xgb_evaluate(min_child_weight, subsample, colsample_bytree, max_depth): params = {'metric': 'error', 'objective':'binary:logistic', 'n_estimators':50000, 'random_state':42, 'boosting_type':'gbdt', 'learning_rate':0.01, 'min_child_weight': int(min_child_weight), 'max_depth': int(max_depth), 'colsample_bytree': colsample_bytree, 'subsample': subsample, } cls = xgb.XGBClassifier() cls.set_params(**params) cls.fit(X_train, y_train, early_stopping_rounds=50, eval_set=[(X_test, y_test)], eval_metric='error', verbose=0) pred = cls.predict(X_test) score = accuracy_score(y_test, pred) return score
ベイズ最適化ではxgb_evaluate関数が戻すscore(正解率の値)の最大化を目的としてハイパーパラメータの値を探索します。4つのハイパーパラメータに絞り、候補の値をBayesianOptimizationのクラスへ渡しましょう。min_child_weight:(1, 20)と記載がありますが、これはmin_child_weightのハイパーパラメータへ1から20に属する値を候補の値として探索することを示します。
[In ]: xgb_bo = BayesianOptimization(xgb_evaluate, {'min_child_weight': (1,20), 'subsample': (.1,1), 'colsample_bytree': (.1,1), 'max_depth': (1,50)}, random_state=10)
maximizeメソッドを実行してベイズ最適化によるハイパーパラメータの検証を行いましょう。ベイズ最適化も繰り返しの処理による最適化の手法です。繰り返し回数はn_iter引数で指定することが可能です。50回の繰り返し処理で実行してみましょう。init_points引数はランダムな探索を何回行うのかを指定する引数です。
それでは実行してみましょう!
[In ]: xgb_bo.maximize(init_points=15, n_iter=50, acq='ei')
[Out ]: | iter | target | colsam... | max_depth | min_ch... | subsample | ------------------------------------------------------------------------- | 1 | 0.7097 | 0.7942 | 2.017 | 13.04 | 0.7739 | | 2 | 0.7527 | 0.5487 | 12.02 | 4.763 | 0.7845 | | 3 | 0.7097 | 0.2522 | 5.329 | 14.02 | 0.9581 | | 4 | 0.7097 | 0.1036 | 26.1 | 16.44 | 0.6513 | 〜(略)〜 | 64 | 0.7097 | 0.1037 | 32.08 | 19.76 | 0.2378 | | 65 | 0.7097 | 0.1 | 13.09 | 20.0 | 0.1 | =========================================================================
max属性は最も評価スコアが高かった結果を取得することが可能です。ベイズ最適化により得られた最もスコアの良かったハイパーパラメータの値を変数optimized_paramsへ格納しましょう。
[In ]: optimized_params = xgb_bo.max['params'] optimized_params['max_depth'] = int(optimized_params['max_depth']) optimized_params
[Out ]: {'colsample_bytree': 0.5928275403273192, 'max_depth': 41, 'min_child_weight': 4.7800032538974335, 'subsample': 0.8711652722119598}
metricやobjectiveは固定されたハイパーパラメータですので、変数fixed_paramsへ格納します。
[In ]: fixed_params = {'metric':'error', 'objective':'binary:logistic', 'n_estimators':50000, 'random_state':seed, 'booster': 'gbtree', 'learning_rate':0.01}
新たにXGBClassifierのインスタンスを生成して、ベイズ最適化で得られたハイパーパラメータの値を使いモデル訓練を行います。
[In ]: cls = xgb.XGBClassifier() cls.set_params(**fixed_params, **optimized_params) cls.fit(X_train, y_train, early_stopping_rounds=50, eval_set=[(X_test, y_test)], eval_metric='error', verbose=0)
[Out ]: XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1, colsample_bynode=1, colsample_bytree=0.5928275403273192, gamma=0, gpu_id=-1, importance_type='gain', interaction_constraints=None, learning_rate=0.01, max_delta_step=0, max_depth=41, metric='error', min_child_weight=4.7800032538974335, missing=nan, monotone_constraints=None, n_estimators=50000, n_jobs=0, num_parallel_tree=1, objective='binary:logistic', random_state=42, reg_alpha=0, reg_lambda=1, scale_pos_weight=1, subsample=0.8711652722119598, tree_method=None, validate_parameters=False, verbosity=None)
今までと同様にテストデータの特徴量から推測結果を算出して、正解率と混同行列を出力してみましょう。
[In ]: pred_4 = cls.predict(X_test) baseline = accuracy_score(y_test, pred_4) baseline
[Out ]: 0.7634408602150538
[In ]: confusion_matrix(y_test, pred_4)
[Out ]: array([[65, 1], [21, 6]])
ベイズ最適化で導き出したハイパーパラメータの値を使った結果、正解率が76.34%へ改善しました。
まとめ
本稿ではKaggleで公開されている人事データセットを使い、各従業員の性別や人種、満足度などを特徴量として辞職の有無をXGBoostを使い推測しました。
異なる3つの手法を用いてハイパーパラメータチューニングを行った結果は以下の通りです。全て同じ訓練データの学習を行い、同じテストデータからの推測結果となります。
- ベースライン 67.74%
- グリッドサーチ 72.04%
- ランダムサーチ 74.19%
- ベイズ最適化 76.34%
本稿の前半で解説した通り、各ハイパーパラメータチューニングの手法はメリット・デメリットがあります。扱うデータセットの大きさ、使用するモデリング手法、計算コストを考慮して適した手法を採用することが重要です。
codexa(コデクサ)では無料で機械学習関連のライブラリ、基礎統計などのコースを公開しています。機械学習をこれから学ぼうとお考えの方は、是非こちらの無料コースも受講ください!
機械学習 準備編 無料講座
以上となります!