静かなる名辞

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

2019/03/22:TechAcademyがteratailの質問・回答を盗用していた件
2019/03/26:TechAcademy盗用事件 公式発表と深まる疑念



ランダムフォレストで分類するときの過学習対策の検討

はじめに

 ランダムフォレストは決定木のアンサンブル学習なので、何も考えずに使うと過学習します。過学習対策はいろいろあるので(木の深さだったり、ノードあたりのサンプル数による制御だったり)、やってみます。

 まあ、その過学習した状態の方が性能良いこともあるんですが……

sklearnの場合に設定できるパラメータ

 以下のようなものがあるようですね。

 なお、以下で「ノードに属する」のような表現がたくさん出てきますが、すべて学習時の話です。

  • max_depth(default=None)

 木の最大深さです。一番よく使われる気がします。何も設定しないと伸び放題になります。

  • min_samples_split(default=2)

 中間ノードに属するサンプル数がこの数字未満になったら、分割をやめます。
 デフォルトの2は「2つまでは分割する」と言っているので、制約がきついというか過学習しやすい設定です。

  • min_samples_leaf(default=1)

 葉ノードに属するサンプル数がこの数を下回らないようにします。デフォルトの1は「分けられるだけ分ける」という方針です。
 上のmin_samples_splitとよく似ていますが、中間ノードに対してかかる制約なのか葉ノードにかかる制約なのかだけが違います。
 たとえばmin_samples_split=40, min_samples_leaf=20という設定だったとして、あるノードに39個のサンプルが入ってくるとそもそもそれ以上子を作れないことになります(そのノードが葉になる)。また、40個のサンプルが入ってきたときに[20,20]で分かれれば子ノードを作れますが、[10, 30]とかに分けてしまうとmin_samples_leafの制約を満たさなくなるので、こちらも子を作れなくなってそのノードが葉になります。

  • min_weight_fraction_leaf(default=0.0)

 あまり使用例を見ないのと、ドキュメントの説明が判然としない部分がありますが、基本的にはノードにサンプルをいくつ残すのかを比率で指定できるはずです(全サンプル数1000のとき0.1と指定すれば10以上)。fitの際にsample_weightを渡すとそれに基づいて比率を決めてくれる機能もあります。

  • min_impurity_decrease(default=0.0)

 これ以上不純度が上昇すれば、という条件を指定できます。

  • max_leaf_nodes

 葉ノードの数。

 それでどれを設定するか? という話ですが、直感的にわかりやすいのはmax_depth, min_samples_split, min_samples_leafあたりでしょう。他は少し使いづらい気がします。

 この中で、max_depthはデータによってどれくらいの深さが適当なのか予想しづらいので、少し使いづらい印象があります。min_samples_splitとmin_samples_leafはわかりやすいのと、過学習対策としては木の深さを制限するよりは理にかなっている気がするので、個人的には好んで使います。

3.2.4.3.1. sklearn.ensemble.RandomForestClassifier — scikit-learn 0.21.2 documentation

実験

 ということで、上であげた直感的にわかりやすいパラメータをいじりながら少し回してみました。例によって2次元の2クラス分類です。

 コードはお世辞にも見やすいとは言えないので、流し読みで構いません。結果の図を下に示しているので、そちらを御覧ください。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

from sklearn.datasets import make_moons, make_circles,\
    make_classification
from sklearn.ensemble import RandomForestClassifier

def main():
    rfcs = [("max_depth={}".format(max_depth),
             RandomForestClassifier(
                 max_features=2,
                 max_depth=max_depth, n_estimators=500, 
                 n_jobs=-1))
            for max_depth in [2, 4, 8]]\
            + [("min_samples_leaf={}".format(min_samples_leaf),
                RandomForestClassifier(
                max_features=2,
                min_samples_leaf=min_samples_leaf,
                    n_estimators=500, n_jobs=-1))
               for min_samples_leaf in [8, 4, 2]]\
            + [("min_samples_split={}".format(min_samples_leaf),
                RandomForestClassifier(
                max_features=2,
                min_samples_leaf=min_samples_leaf,
                    n_estimators=500, n_jobs=-1))
               for min_samples_leaf in [8, 4, 2]]\

    X, y = make_classification(
        n_features=2, n_redundant=0, n_informative=2,
        random_state=1, n_clusters_per_class=1)
    rng = np.random.RandomState(2)
    X += 2 * rng.uniform(size=X.shape)
    linearly_separable = (X, y)

    datasets = [make_moons(noise=0.3, random_state=0),
                make_circles(
                    noise=0.2, factor=0.5, random_state=1),
                linearly_separable]

    fig, axes = plt.subplots(
        nrows=3, ncols=10, figsize=(20, 9))
    plt.subplots_adjust(wspace=0.2, hspace=0.3)

    cm = plt.cm.RdBu
    cm_bright = ListedColormap(['#FF0000', '#0000FF'])
    

    for i, (X, y) in enumerate(datasets):
        x_min = X[:, 0].min()-0.5
        x_max = X[:, 0].max()+0.5
        y_min = X[:, 1].min()-0.5
        y_max = X[:, 1].max()+0.5

        xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.05),
                             np.arange(y_min, y_max, 0.05))
        axes[i,0].scatter(X[:,0], X[:,1], c=y, s=20,
                          cmap=cm_bright, edgecolors="black")


        for j, (cname, clf) in enumerate(rfcs, 1):
            clf.fit(X, y)
            Z = clf.predict_proba(
                np.c_[xx.ravel(), yy.ravel()])[:, 1]
            Z = Z.reshape(xx.shape)
            axes[i,j].contourf(xx, yy, Z, 20, cmap=cm, alpha=.8)            
            axes[i,j].set_title(cname)
    plt.tight_layout()
    plt.savefig("result.png", bbox_inches="tight")

if __name__ == "__main__":
    main()

result.png
result.png

 こんな感じで、どの方法を使ってもだいたい同じような結果が得られます(雑な結論)。

どれくらいが良いのか

 定量的なことは言えませんし、サンプルサイズによっても変わりますが、総サンプル数の1%程度をmin_samples_leafに指定するのが良さげな気がします。

 決定木系はもともとモデルの表現力がいまいちなので、あまり過学習対策を効かせても表現力が落ちて性能に結びつかない感触があります。

まとめ

 まあ、いろいろな方法でできます、という程度です。