静かなる名辞

pythonとプログラミングのこと


【python】ランダムフォレストのチューニングにOOB誤り率を使う

 一般的な機械学習のアルゴリズムでは、パラメタチューニングにはグリッドサーチ・交差検証を組み合わせて使うのが割と普通だと思います。sklearnにはそれ専用のGridSearchCVというクラスまで用意されています。

 実際問題としては、GridSearchは良いとしても交差検証をやったのでは計算コストをたくさん食います。なので、他の方法で代替できるなら、そうしたいところです。

 そして、RandomForestはOOB誤り率という、いかにも強そうなスコアを計算できます。これの良いところは一回fitしただけで計算でき、交差検証と同じ(ような)結果が得られることです。

 なので、OOB誤り率を使ったパラメタチューニングを試してみたいと思います。

OOB誤り率とはなんぞ?

 OOB誤り率、OOBエラー、OOB error等と呼ばれます。これを理解するためにはランダムフォレストの学習過程を(少なくとも大雑把には)理解する必要があります。

 ランダムフォレストは木を一本作る度に、データをランダムサンプリングします(「ランダム」の所以です)。サンプリング方法はブートストラップサンプリングです。詳細はググって頂くとして、とにかくサンプリングの結果、各決定木に対して「訓練に使われなかったデータ」が存在することになります。逆に言えば、各データに対して「そのデータを使っていない決定木の集合」があります。このことを利用して、「そのデータを使っていない決定木」だけ利用して推定し、汎化性能を見ようというのがOOB誤り率のコンセプトです。

実験

 以下のようなコードを書きました。

# coding: UTF-8
import time
from itertools import product

from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.model_selection import  train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import precision_recall_fscore_support as prf

def oob_tune(clf, params, X, y):
    key_index = list(params.keys())
    
    result_dict = dict()
    for ps in product(*[params[k] for k in key_index]):
        clf.set_params(**dict(zip(key_index, ps)))
        clf.fit(X, y)
        result_dict[ps] = clf.oob_score_

    return dict(zip(key_index, 
                    sorted(result_dict.items(), 
                           key=lambda x:x[1],
                           reverse=True)[0][0]))
    
def main():
    digits = load_digits()
    X_train, X_test, y_train, y_test = train_test_split(
        digits.data, digits.target)
    
    params = {"n_estimators":[50, 100, 500],
              "max_features":[4, 8, 12],
              "min_samples_leaf":[1, 2]}

    rfc = RFC(oob_score=False, n_jobs=-1)

    t1 = time.time()
    clf = GridSearchCV(rfc, params, cv=6, n_jobs=-1)
    clf.fit(X_train, y_train)
    t2 = time.time()
    best_params = clf.best_params_
    print("GridSearchCV") 
    print("time:{0:.1f}".format(t2-t1))
    print("best params")
    print(best_params)
    rfc = RFC(**best_params)
    rfc.fit(X_train, y_train)
    y_pred = rfc.predict(X_test)
    score = prf(y_test, y_pred, average="macro")
    print("p:{0:.3f} r:{0:.3f} f:{0:.3f}".format(*score))


    rfc = RFC(oob_score=True, n_jobs=-1)

    t1 = time.time()
    best_params = oob_tune(rfc, params, X_train, y_train)
    t2 = time.time()

    print("oob")
    print("time:{0:.1f}".format(t2-t1))
    print("best params")
    print(best_params)
    rfc = RFC(**best_params)
    rfc.fit(X_train, y_train)
    y_pred = rfc.predict(X_test)
    score = prf(y_test, y_pred, average="macro")
    print("p:{0:.3f} r:{0:.3f} f:{0:.3f}".format(*score))

if __name__ == "__main__":
    main()

 sklearnの機能でありそうな気がしましたが、見つからなかったので*1、OOB誤り率を使ったパラメタチューニングのプログラムは自分で書きました。

 同じパラメタ候補に対してのグリッドサーチで、GridSearchCVと比較しています。

 細かい説明は抜きで(気になる人はプログラムを追ってください。簡単なので)、結果を貼ります。

GridSearchCV
time:41.8
best params
{'n_estimators': 500, 'max_features': 4, 'min_samples_leaf': 1}
p:0.977 r:0.977 f:0.977
oob
time:13.1
best params
{'n_estimators': 500, 'max_features': 8, 'min_samples_leaf': 1}
p:0.979 r:0.979 f:0.979

 やはりOOB誤り率を使った方が速いです。最適パラメータは困ったことに両者で食い違っています。テストデータでのスコアはOOB誤り率で計算した最適パラメータを用いた方がびみょ~に高いですが、これくらいだとほとんど誤差かもしれません(1データか2データ程度の違い)。

まとめ

 交差検証でやるのと同程度の結果が得られる・・・と言い切るには微妙な部分がありますが、とにかくやればできます。

 自分でパラメタチューニングのプログラムを書くと、sklearnのインターフェースから逸脱するデメリットがあるので、実際にやるべきかどうかは微妙。scoringの関数と実質的にcross validateしないcv(ダミー)を渡せば、GridSearchCVを利用してもやれそうではありますが。ちょっと悩ましいところです*2

 とにかくさっさとパラメタチューニングしたいんだ! というときは使えるでしょう。

*1:本当に存在しないか、存在するけど私が見つけられなかったかのどちらか。

*2:それとも、私が見つけられてないだけで、sklearnで直接これができる方法があるのだろうか? もしそうなら、ご存知の方は教えていただきたい