Tree における単調性制約

XGBoost における単調性制約 - 2

はじめに

前回 XGBoost での単調性制約を与える方法について紹介しましたが、そのメカニズムについて調べました。



線形モデルはデータと目的変数間の単調性(増加・減少)を捉えるモデルです。 非線形モデルを使うのは、単調性にとどまらない表現力を利用したい場合があるからです。

実データを用いていると、特定の説明変数にはすでに単調性があることが分かっていたりします。そして非線形モデルを使う場合、特定の説明変数に単調増加の制約を与えるなど、局所的にコントロールしたいときがあります。

例えば、決定木・XGBoost などでモデリングをしていると、バイアスの低さにとても助けられますが、同時に強力すぎる分割の力をなんとか制御したいと思うこともあります。
そのような、モデルの複雑さをコントロールするためにも単調性制約は使えます。
また、決定木などツリー系はロジックそのものが木で表現できるので、複雑度をうまくコントロールすると説明性が上がります。

Tree における単調性制約の考え方と実装

単調性を持った問題への分類ツリーの活用については以下の論文が理論的にあります。
http://helios.mm.di.uoa.gr/~rouvas/ssi/sigkdd/sigkdd.vol4.1/potharst.pdf

実装の考え方については、次の記事に非常に分かりやすくかつ詳しく書いてあったので、ほぼそのまま引用させてもらいました。
towardsdatascience.com

以下は、上記の記事の意訳になります。

この記事では、単調性制約の与え方を 2 段階に分けて説明しています。

1. 左右子ノードの重みで分割の是非を判断する

Tree における単調性制約の基本的な考え方は、

枝を生やしていく過程で、分割した際に単調性ではない関係が作られた場合は、その分割を取りやめる。

ことです。

例えば、特徴量 f に単調増加制約を与えたいとして、あるノードの分割の際に f がピックアップされ分割される場合を考えてみます。
この時、制約が単調増加なので、右子ノードの重み w_R は左子ノードの重み w_L よりも大きいことが期待されます。
アルゴリズムでは

w_L <= w_R の場合のみ分割し、そうでなければ分割をしない

となります。

この処理は XGBoost の以下のコード L.441,442 に相当します*1

github.com




この単調性制約の実装はとてもわかりやすい方法です。
しかしながら、単純にこの方法を実装しただけではうまく機能しないことにすぐ気づくと思います。
なぜならば、そのノード・その特徴量にのみ注目する限りに於いてはうまくいっていますが、木全体を見た場合単調性制約が守られない場合があるからです。(局所的には守られているが、大域的には破綻している)

もし、下流のノードで同じ特徴量が再度分割対象として取り上げられたらどうなるでしょう?
そのノードでも単調増加制約が守られ、左左子ノードと左右子ノードが単調増加性を持って重み w_LL <= w_LR となるように分割されたとしても、w_LR が右子ノードの重み w_R よりも高くなる場合がありえます。
このように、単調増加関係を木全体に渡って維持するためには、右子ノードの重み w_R は左子ノードの重み w_L よりも高いだけでなく、その子孫ノード { LL, LR, LLL, LLR, LRL LRR....} の重みよりも常に高くないとなりません。

それでは、どのようにして、この様なケースに対処するのでしょうか。 言い方を変えると、どのように制約を木の深さを超えて与えることができるのか、ということです。



2. 単調性制約を木全体に与える

XGBoost では次のように対処しています。

ノードを分割する時、子ノード(LL, LR)の重み(w_LL または w_LR)は親ノード(L)と親ノードの兄弟ノード(R)の重みの平均を境界とします*2
この制約を与えると、特徴 f が木の分割の中で何度も分割されたとしても、重みが祖先ノードよりも高く(或いは低く)なり、木全体に渡って単調性制約に違反しないことが保証されます。

上記のこの書き方だと少し分かりづらいので、具体的な例で考えてみます。

あるノードを単調増加性をもって左子ノード(L)と右子ノード(R)に分割し、更に左子ノードを分割(LL, LR)する場合、 分割の際に w_LL <= w_LR の条件に加えて、w_LR <= mean(w_L, w_R) の条件を課します*3

この2つの条件を課すことで、特徴 f の分割に関しての単調性制約は木全体でも保持されることとなります。

この条件の実装はXGBoost の以下のコード L.464-467 に相当します。
github.com




フロー

上記の2つの条件を、フローにしてみます。
特徴 x1 に単調増加制約を与えるとします。

  1. ルートノードでは、w_0 のみが生成されます。

  2. ルートノードの分割に於いて、特徴 x1 がピックアップされた場合、w_L <= w_R の条件に適合する分割のみ採用され、その中でゲインが最良の分割値で分割されます。*4

  3. 左子ノード(L)の分割をします。 特徴 x1 がピックアップされた際、w_LL < w_LR になる分割のみ採用されます。

  4. ルートノード以降の分割では 3 に加えて次の条件を課します。 w_LL < mean(w_L, w_R), w_LR < mean(w_L, w_R)

  5. 同様に、右子ノード(R)を分割する際にも 3,4 の制約を与えます。
    但し、4 に相当する条件は w_RL => mean(w_L, w_R), w_RR => mean(w_L, w_R) となります。

  6. 木を生成する過程で 3,4,5 の手順が守られると、x1 に対して左の部分木の重みは右の部分木の重みよりも常に小さくなります。したがって、単調増加性制約は木の全体に対して課せられます。

このフローに従った分割は下図となります。




まとめ

この記事の内容紹介をしながら、XGBoost における単調性制約の考え方と実装を見てみました。

次回は、木の数がひとつで、XGBoost よりもシンプルな決定木アルゴリズムにこの条件を実装し、動きを見てみたいと思います。

*1:条件が合わなければゲインは負の無限大に置き換えられ、その結果アルゴリズムは分割をしない、という方法で実装されています。単調減少制約の場合は逆です

*2:単調増加制約の場合は上限、単調減少制約の場合は下限

*3:mean は平均関数

*4:もちろん、他の特徴でよりよりゲインがあれば、それが採用されます

matplotlib で plt.show() するとウィンドウは開いてもグラフが描画されない

matplotlib で plt.show() するとウィンドウは開いてもグラフが描画されない、という問題が発生しました。
plt.show() で ウィンドウが開かない、というはよくあるパターンで大体が描画の際のバックエンドの指定だと思います (https://qiita.com/yoshizaki_kkgk/items/bcb45e3bc936ec49ef00)。 今回は、それとは異なった現象・原因であったため残しておきます。

結論

どうやら tk のバージョンに起因するようです。

tk 8.6.8 and tk 8.6.9 build 0 and build 1000 are fine.
tk 8.6.9 build 1001 and build 1002 are not.
macOS 10.14.5 (18F132)

github.com

github.com

対処

私は anaconda を使っており、tk のダウングレードなどで他のパッケージへの影響がわからなかったため、新たに conda 環境を作り直しました。
こういう場合は anaconda は本当に楽だなと思います。

$ conda create -n py36_base python=3.6
$ conda info -e
# conda environments:
#
base                  *  /anaconda3
py36_base                /anaconda3/envs/py36_base

$ source activate py36_base

$ conda install ipython ipdb
$ conda install pandas matplotlib seaborn
$ conda install scikit-learn

$ conda list | grep tk
tk                        8.6.8                ha441bb4_0

# 動作確認
# 以下のサンプルを実行できることを確認します。
# sklearn + matplotlib https://scikit-learn.org/stable/auto_examples/plot_multilabel.html#sphx-glr-auto-examples-plot-multilabel-py

デフォルトでは tk=8.6.8 であったことから、どうやら他のパッケージをアップグレードした拍子に tk も 8.6.9 に上げてしまったようでした。

CalibratedClassifierCV で XGBoost の学習済みモデルが ValueError: feature_names mismatch となる

はじめに

CalibratedClassifierCV に XGBoost の学習済みモデルを指定して、fit すると ValueError: feature_names mismatch となる場合があります。
これは XGBoost の学習時には pandas の column 名を変数とし、CalibratedClassifierCV では numpy.ndarray に変換されるため、説明変数名の不一致が起きてしまうことに起因しています。
XGBoost の学習済みモデルを用いて補正する場合に限定されますが、それを回避する方法を記載します。

Probability Calibration とは

Probability Calibration(確率補正)は、モデルによって予測された確率を本来の確率に補正して近づける手法です。
詳細は別途機会があれば整理したいと思いますが、とても簡単に言うと機械学習のモデルの推定する値は確率とは異なる場合があります(確率を推定するモデル以外は)。

「分類器としての性能と,確率を正しく推定できるかは分けて考えないといけないのだろうな.」https://twitter.com/nakamichi/status/797361213185335296

その推定値を確率として補正する、ということだろうと思います。

tjo.hatenablog.com

CalibratedClassifierCV で XGBoost 学習済みモデルを補正

上述のように、CalibratedClassifierCV に XGBoost の学習済みモデルを指定して、fit すると ValueError: feature_names mismatch となる場合があります。

ValueError: feature_names mismatch: ['a', 'b', ...] ['f0', 'f1', ']
expected f0 f1 .
training data did not have the following fields: a b ...

これは簡単に言うと、学習時のデータが pandas.DataFrame であった場合、CalibratedClassifierCV では numpy.ndarray に変換されるため、説明変数名の不一致が起きてしまうことに起因しています。

sklearn の XGBClassifier は pandas.DataFrame を学習データとして投入された時、feature names を column 名として保持します。
それに対して、CalibratedClassifierCV は fit 関数内部で pandas.DataFrame を numpy.ndarray に変換して学習済みの XGBClassifier Object にわたすため、feature names の不一致が起きてしまい ValueError となってしまうようです。
一方、 CalibratedClassifierCV で学習も同時に実行する場合は、XGBClassifier では numpy.ndarray が内部的に DMatrix 形式に変換され、feature names は ['f0', 'f1', '] のように設定されるため、このような不一致は起きなくなります。

これについての報告や詳細な考察は以下にあります。

github.com

calibration.py の class CalibratedClassifierCV.fit をたどると、さまざまな validation を通り抜けて、最終的に cv='prefit' (既に学習済みのモデル)である場合、_CaliratedClassifier.fit で確率補正学習します。
その vaidation の中で、dataframe のX,y は numpy.array に変換されます。
具体的には validation.py の indexable という関数にて、データ数のチェックや sparse 行列の csr 変換や non-iterable object を配列に変換する処理をしている箇所です。

def indexable(*iterables):
    """Make arrays indexable for cross-validation.

    Checks consistent length, passes through None, and ensures that everything
    can be indexed by converting sparse matrices to csr and converting
    non-interable objects to arrays.

こちらから validation 済みのデータを渡すのであれば、特にここを通す必要はなく、更に学習済みモデルを補正する場合は直接 _CalibratedClassifier で fit すればよいです(L.155のあたり)。
(一方、CrossValidation を行う場合は L.162 の交差検証実行されます。)

# L.155
calibrated_classifier = _CalibratedClassifier(base_estimator, method=self.method)
    
if sample_weight is not None:
    calibrated_classifier.fit(X, y, sample_weight)
else:
    calibrated_classifier.fit(X, y)

実行結果として適切なものがなくてコードだけになってしまいますが、動作を確認したものを簡単に記載します。

import xgboost as xgb
from _calibration import _CalibratedClassifier

#
# model は xgb.XGBClassifier の学習済みモデル
# X_test, y_test は model のテストデータ
#

sig_clf = _CalibratedClassifier(model, method='sigmoid')
sig_clf.fit(X_test, y_test)

# 補正後の値
y_prob_sig = sig_clf.predict_proba(X_test)[:, 0]

# 補正前の価
y_prob = model.predict_proba(X_test)[:, 0]

# plot ...

単語分散表現(Skipgram, CBoW)のベクトル長について

Skipgram など単語の分散表現のベクトル長について

Skipgram, CBoW などの単語分散表現は、単語をベクトル空間に配置(埋め込む)するのですが、単語同士の類似度としてはコサイン類似度が用いられます。
言い換えると、単語ベクトル同士の角度が類似度となります(類似した単語ベクトルの角度は小さくなる)。

しかし、Gensim の Word2Vec model にはベクトル .syn0 と L2norm の .syn0norm が用意されています。
この使い分けの観点は何か?という問いが stackoverflow にあり、そこから「ベクトルの長さはどういう意味を持つのか」という考察が書かれています。

「non-normalized vector と normalized vector の使い分けがわからない問題について.」
stackoverflow.com
stats.stackexchange.com

端的に言うと

ベクトル長は異なるコンテキストに出現する頻度を反映している。 - 異なるコンテキストに何回も出現する単語は短いベクトルとなる - 少ない出現頻度の言葉は長いベクトルとなる

ということで、これは何を意味しているかと言うと、

単語ベクトルの方向が意味を捉えているだけでなく、長さも重要な情報を持っている。 特に、べクトルの長さは単語の出現頻度を内包しており、単語の重要度を測るのに有用である。

ということになります。
詳細はこの論文*1に詳しいですが、この単語出現数とベクトル長のプロットを見ると

単語出現数に対して、ベクトル長は短い間隔となり出現数が増えると共に徐々に上昇する、30 回以上の出現数でその傾向は減少傾向に転じる.

となっており、それに対して著者はこのような考察をしています。

単語出現数に対して、ベクトル長は短い間隔となり出現数が増えると共に徐々に上昇する、30 回以上の出現数でその傾向は減少傾向に転じる。 少ない出現頻度の言葉は首尾一貫した使われ方をする傾向があり、その結果ベクトルは長くなる。 高い出現頻度の言葉はたくさんの異なるコンテキストで使われる傾向があるため、平均処理が増えるだけ短いベクトルとなる。 この傾向は図が明確に示している。

単語が異なるコンテキストに出現したとき、ベクトルは異なる方向に動くように更新される。 従って、最終的なベクトルは異なるコンテキストでの重み付きの平均を表現したものとなる。 このように異なるコンテキストに出現する回数が増えれば、ベクトルは平均処理される回数が増えることから短くなる。 異なるコンテキストに何回も出現する単語は、少ない意味を内包するに違いない…という仮説になる。 一番良い例としては様々なコンテキストで無差別に出現するストップワードで、これらは出現頻度の割に、短いベクトル表現となる。

以上、Skipgram、CBoW など単語分散表現のベクトル長の意味についての考察でした。

*1:Measuring Word Significance using Distributed Representations of Words by Adriaan Schakel and Benjamin Wilson. https://arxiv.org/pdf/1508.02297.pdf

XGBoost における単調性制約

XGBoost の単調性制約

XGBoost などの Tree 系アルゴリズムは表現力が大きいため、容易に bias を小さくできます。

下の例では、増加するトレンドにある特徴量と目的変数がにノイズが乗ったデータに対して XGBoost でフィッティングすると、全体的には上昇トレンドを捉えますが、局所的に振動してしまいます。

これは、XGBoost が表現力(データにフィットする能力)が高いゆえにできることなのですが、一方で過度にデータにフィットしているとも言えます。

もちろん、それはモデル自身が判断することではなく、課題設定や事前情報、ビジネス上の考慮などで決まる場合が多いです。

ただ、諸理由によりこの「増加するトレンド」であったり「減少するトレンド」を制約として加え、制約の中で最大限にフィッティングさせたい場合に、この単調性の制約(単調に増加(減少)する制約)は有効です。

一般的に、このような制約を加える理由としては「精度向上のために」というよりも「解釈しやすくするため」という動機が多いように思います。

下図が単調性制約を与えてフィッティングしたモデルの結果です。 これを見ると、大局的な傾向は捉えていながら、局所的に増減する振動が消えています。

というようなことが下のページに書いてあるのですが、簡単に内容と実際のコードを書いておきます。

xgboost.readthedocs.io

例では次のデータを用いています(上図2つも同様です)

𝑦= 5x_1+\sin(10 \pi x_1)−5x_2 − \cos( 10\pi x_2)+ Norm(0,0.01)

x_1,x_2 \in [0,1]

# jupyter notebook での実行
import numpy as np
import xgboost as xgb
import matplotlib.pylab as plt
from sklearn.cross_validation import train_test_split

%matplotlib inline

# データの作成
N = 1000
x1 = np.random.random(N)
x2 = np.random.random(N)
y = (5 * x1 + np.sin(10 * np.pi * x1)) - (5 * x2 + np.cos(10 * np.pi * x2)) + np.random.normal(0.0, 0.01)

# x1 の分布(単調増加)
plt.plot(x1, y, 'o', alpha=0.5)

# x2 の分布(単調減少)
plt.plot(x2, y, 'o', alpha=0.5)

# 学習データ
X = np.array([x1, x2]).T
X_train, X_test, y_train, y_test = train_test_split(X, y)

# 描画用
# 片方の変数を描画するときはもう片方を 0.5 で固定(影響を最小限にするため)
def plot_response_x1(model):
    
    # x1 (単調増加変数のレスポンス)
    _x1 = np.linspace(0, 1, 100)
    _x2 = np.zeros(100) + 0.5
    _X = np.array([_x1, _x2]).T
    
    _y = model.predict(_X)
    plt.plot(X_test[:,0], y_test, 'o', alpha=0.5)
    plt.plot(_x1, _y, color = 'black')

def plot_response_x2(model):

    # x2 (単調減少変数のレスポンス)
    _x1 = np.zeros(100) + 0.5
    _x2 = np.linspace(0, 1, 100)
    _X = np.array([_x1, _x2]).T
    
    _y = model.predict(_X)
    plt.plot(X_test[:, 1], y_test, 'o', alpha=0.5)
    plt.plot(_x2, _y, color = 'black')
    
# 単調性制約のない XGBoost で回帰モデルを構築します.
model = xgb.XGBRegressor()
model.fit(X_train, y_train)
plot_response_x1(model)
plot_response_x2(model)

# 単調性制約を課した XGBoost で回帰モデルを構築します.
model = xgb.XGBRegressor(monotone_constraints=(1,-1))
model.fit(X_train, y_train)
plot_response_x1(model)
plot_response_x2(model)

パラメータについて

monotone_constraints

1:単調増加制約、0:制約を加えない(デフォルト)、-1:単調減少制約、で説明変数毎にリストで設定します。例えば

  • (1, 0): 1つ目の説明変数に単調増加制約を与え、2つ目の説明変数には制約を与えない
  • (0,-1): 1つ目の説明変数に単調増加制約を与えず、2つ目の説明変数には単調減少制約を与える

となります。

tree_method

monotone_constraints を利用する場合は tree_method として次のうちのどれかを指定する必要があります。

  • tree_method='exact','hist', gpu_hist

BayesSearchCV で XGBoost の early stopping 機能を使う

BayesSearchCV で XGBoost の early stopping 機能を使う

先日の BayesSearchCV イントロダクションの続きです。

XGBoost には early stopping という、

学習データとは別に、テストデータの Validation error が少なくとも <early_stopping_rounds> 回数連続して減少しないと、学習を停止する。

という機能があります。

early stopping は過学習を防ぐ学習方法として XGBoost だけでなく DeepNeuralNetwork でもよく使われる手法です。 端的にいうと、学習データだけではなくテストデータの Validation Error もモニタリングし、学習エラー(bias)と検証エラー(variance)のトレードオフが起きるところで学習を早期に終了させ、過学習を防ぐというものです。

注意すべきは、これは学習実行時のパラメータであり、いわゆるハイパーパラメータではないというところです。

というところで、sklearn では学習実行時のパラメータは fit_params という形で estimator.fit(X, y, **fit_params) の引数として学習実行時に指定されます。 (対してハイパーパラメータは GridSearch の search_param に引き渡されます)

BayesSearchCV でも学習実行時のパラメータを指定できますが、fit の引数ではなく、コンストラクタにて search_param と同様に引数で fit_params を与えます。

サンプルコードになります。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import os
import numpy as np
import scipy as sp
import pandas as pd
from skopt import BayesSearchCV
from skopt.space import Real, Categorical, Integer
from sklearn.model_selection import train_test_split
import xgboost as xgb


# データセットを読み込み
from sklearn.datasets import load_breast_cancer
X, y = load_breast_cancer(return_X_y=True)

X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.75, random_state=0)

estimator = xgb.XGBClassifier(
    booster='gbtree',
    silent=True,
    objective='binary:logistic',
    base_score=0.5,
    eval_metric='auc',
    n_jobs=-1
)

# Early Stopping のパラメータ設定
# eval_set に モニタリングするテストデータ・セットを設定
# early_stopping_rounds 回数メトリクスが減少(増加)しなければ学習を早期終了する
# eval_metric モニタリングする指標 - 最小化(RMSE, log loss, etc.) あるいは最大化(MAP, NDCG, AUC)は自動的に選ばれる
fit_params =  {
    'early_stopping_rounds': 5,
    'eval_metric':'error',
    'verbose':1,
    'eval_set':[[X_test, y_test]]
}

# パラメータ探索範囲設定
from skopt.space import Real, Categorical, Integer
param_grid = {
    'n_estimators': Integer(1, 4),
    'learning_rate': Real(0.01, 0.5, 'log-uniform'),
    'max_depth': Integer(0, 3)
}

clf = BayesSearchCV(
    estimator=estimator,
    search_spaces=param_grid,
    scoring='roc_auc',
    cv=3,
    n_jobs=-1,
    n_iter=10,
    verbose=1,
    refit=True,
    fit_params=fit_params # コンストラクタで設定すると estimator のフィッティング実行時に渡されます
)
def status_print(optim_result):
    '''
    ベイズ最適化のイテレーションで呼ばれるコールバック
    '''
    results    = pd.DataFrame(clf.cv_results_)
    new_params = results.tail(1).params.values[0]
    new_score  = results.tail(1).mean_test_score
    print('Model #%d/%d : ROC-AUC(newest/best) = %.4f / %.4f' % (
        len(results),
        clf.n_iter,
        new_score,
        clf.best_score_
    ))

# BayesSearchCV は sklearn の他の交差検証サーチ系クラス(RandomSearchCVなど)と同じAPIを持っています
clf.fit(X, y, callback=status_print)

print(pd.DataFrame(clf.cv_results_))

# 検証データのパフォーマンス
print(clf.score(X_test, y_test))

以上、Tips でした。

scikit-optimize の BayesSearchCV を用いたベイズ最適化によるハイパーパラメータ探索

ベイズ最適化でハイパーパラメータをチューニングする

ハイパーパラメータのチューニングではパラメータを様々に振りながら学習を繰り返し、一番パフォーマンスが良いところを探し出します。 学習時間はモデルやデータサイズなどによって異なりますが、効率よく探索するかしないかで良いパラメータを得るまでの時間が大きく変わります。 探索方法には様々あり、基本的なところではグリッドサーチ(全探索)、ランダムサーチ(ランダム探索)があります。

今回紹介するものは、ベイズ最適化の手法を用いて効率よく探索する枠組みです *1

BayesSearchCV

ここで紹介するのは CrossValidation で汎化精度を確かめながら、ハイパーパラメータを探索する scikit-optimize の BayesSearchCV です。

github.com

https://scikit-optimize.github.io/#skopt.BayesSearchCV

ハイパーパラメータの汎化精度は交差検証でわかります(*2)。 そのため、交差検証で汎化精度を見ながら最適なハイパーパラメータを探索します。

手順で言うと次のようになります。

  1. ハイパーパラメータをランダムにピックアップする.
  2. 交差検証で精度を見る
  3. ベイズ最適化で次に探索するハイパーパラメータを定める(→2 へ)

これを全部やってくれるのが BayesSearchCV で、以下がサンプルコードとなります。

サンプル実装

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import os
import numpy as np
import scipy as sp
import pandas as pd
from skopt import BayesSearchCV
from skopt.space import Real, Categorical, Integer
from sklearn.model_selection import train_test_split
import xgboost as xgb


# データセットを読み込み
from sklearn.datasets import load_breast_cancer
X, y = load_breast_cancer(return_X_y=True)

X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.75, random_state=0)

estimator = xgb.XGBClassifier(
    booster='gbtree',
    silent=True,
    objective='binary:logistic',
    base_score=0.5,
    eval_metric='auc',
    n_jobs=-1
)

# パラメータ探索範囲設定
# 探索範囲 min,max,distribution を space クラスでで指定(min, max,(distribution))
from skopt.space import Real, Categorical, Integer
param_grid = {
    'n_estimators': Integer(1, 4),
    'learning_rate': Real(0.01, 0.5, 'log-uniform'),
    'max_depth': Integer(0, 3)
}

# 参考
# https://www.kaggle.com/shaz13/santander-xgboost-bayesian-optimization/code
# https://www.kaggle.com/nanomathias/bayesian-optimization-of-xgboost-lb-0-9769
#

'''
GridSearchCV 系と比べた固有の API(一部)
- search_spaces : パラメータ探索範囲設定
- optimizer_kwargs : 最適化パラメータ。探索戦略やガウス過程パラメータなど。- 
- fit_params : clf.fit(X, y) に設定するパラメータ。学習実行時のパラメータ指定.
'''
clf = BayesSearchCV(
    estimator=estimator,
    search_spaces=param_grid,
    scoring='roc_auc',
    cv=3,
    n_jobs=-1,
    n_iter=10,
    verbose=1,
    refit=True
)
def status_print(optim_result):
    '''
    ベイズ最適化のイテレーションで呼ばれるコールバック
    '''
    results    = pd.DataFrame(clf.cv_results_)
    new_params = results.tail(1).params.values[0]
    new_score  = results.tail(1).mean_test_score
    print('Model #%d/%d : ROC-AUC(newest/best) = %.4f / %.4f' % (
        len(results),
        clf.n_iter,
        new_score,
        clf.best_score_
    ))

# BayesSearchCV は sklearn の他の交差検証サーチ系クラス(RandomSearchCVなど)と同じAPIを持っています
clf.fit(X, y, callback=status_print)

print(pd.DataFrame(clf.cv_results_))

# 検証データのパフォーマンス
print(clf.score(X_test, y_test))

探索の結果は clf.cv_resuts_ フィールドに格納されていて、pd.DataFrame にすると以下のようなデータとなっています。 この中には以下のデータが入っています(一部分列挙)。

  • param_* は探索パラメータ
  • mean_test_score は交差検証スコアの平均
  • split{n}_test_score は交差検証の各分割でのスコア(これらを平均したものが mean_test_score)
  • std_test_score は交差検証スコアの偏差
mean_fit_time mean_score_time mean_test_score split0_test_score split1_test_score split2_test_score std_fit_time std_score_time std_test_score param_colsample_bylevel param_colsample_bytree
0 0.005673011 0.00219202 0.832636822 0.827494378 8.28E-01 0.842857143 0.000176986 0.00030934 0.007207978 0.432490483 0.101987568
1 0.005337397 0.002478361 0.5 0.5 0.5 0.5 0.000566812 0.000611297 0 0.45403928 0.928587396
2 0.005687555 0.002456427 0.5 5.00E-01 0.5 0.5 0.000361052 6.48E-05 0 0.471067626 0.880029631
3 0.005241315 0.00255092 0.5 5.00E-01 0.5 0.5 0.000284118 0.000263844 0 0.24114647 0.786112636
4 0.006174246 0.002198935 0.834788739 7.56E-01 0.832465381 0.915966387 0.000542449 0.000200357 0.065150251 0.699309626 0.752064985
5 0.007870754 0.002580643 0.5 0.5 0.5 0.5 0.000749173 0.000241813 0 0.932821363 0.339574308
6 0.004701535 0.00221777 0.5 0.5 0.5 0.5 0.000507002 0.000167713 0 0.818819988 0.79140138
7 0.004785299 0.002165318 0.5 0.5 5.00E-01 0.5 0.000287597 0.000235932 0 0.999741524 0.381359767
8 0.005346219 0.002468665 0.5 5.00E-01 0.5 0.5 0.000562654 0.000213206 0 0.55477075 0.31067584
9 0.005452951 0.00238506 0.5 5.00E-01 0.5 0.5 0.000326225 0.000221597 0 0.586112963 0.589381284

ベイズ最適化探索で得られた最適な結果は次のフィールドに格納されます。

  • best_estimator_ : 最適なモデル
  • best_params_ : 最適なハイパーパラメータ
  • best_index : cv_results の最適なパラメータのインデックス
  • best_score : 最適なモデルのスコア

まとめ

scikit-optimize の BayesSearchCV を用いて、ベイズ最適化によるハイパーパラメータ探索を試してみましたが、scikit-learn の RandomSearchCV や GridSearchCV と同じ使い方で、簡単にベイズ最適化を使えることがわかりました。

探索戦略のパラメータなどについては今後調べてみようと思います。