静かなる名辞

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


AdaBoostとRandomForestの比較

はじめに

 AdaBoost(アダブースト、もしくはエイダブースト)は代表的なアンサンブル学習アルゴリズムとしてよく取り上げられるものですが、実用的に使っている事例はあまり見かけません。ランダムフォレストでいいじゃんとなっていることが多いのではないでしょうか。しかし、実際にはどのような違いがあるのでしょう?

 個人的にもAdaBoostの性質がまだよくわかっていないので、比較を行ってみようと思います。

参考にしたコード

 分類結果を二次元で可視化してプロットするため、色々なコードを参考にしました。

 大元はsklearnのこの記事です。

Classifier comparison — scikit-learn 0.21.3 documentation

 また、コードを1から書き上げるほどの情熱が今回沸かなかったので、自分の過去記事からコピペして書いています。

【python】RandomForestの木の本数を増やすとどうなるか? - 静かなる名辞

【python】線形な分類器の比較 - 静かなる名辞

 AdaBoostのパラメータの決め方については、こちらの記事を御覧ください。

【python】sklearnのAdaBoostをデフォルトパラメータで使ってはいけない - 静かなる名辞

実験:2次元データに対して、分類境界の可視化を試みる

 分類器の性質を探るために2次元で分類境界を見ます。よくやる手です。

 AdaBoostはベース分類器に決定木を使い、木の最大深さ5と10で試しています。

 コード

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 AdaBoostClassifier, RandomForestClassifier
from sklearn.tree import DecisionTreeClassifier

def main():
    ada5 = AdaBoostClassifier(
        base_estimator=DecisionTreeClassifier(max_depth=5),
        n_estimators=100)
    ada10 = AdaBoostClassifier(
        base_estimator=DecisionTreeClassifier(max_depth=10),
        n_estimators=100)

    rfc = RandomForestClassifier(n_estimators=100)

    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=3, figsize=(10,10))

    cm = plt.cm.RdBu
    cm_bright = ListedColormap(['#FF0000', '#0000FF'])
    for i, (X, y) in enumerate(datasets):
        x_min, x_max = X[:, 0].min()-0.5, X[:, 0].max()+0.5
        y_min, y_max = X[:, 1].min()-0.5, X[:, 1].max()+0.5

        xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
                             np.arange(y_min, y_max, 0.1))

        for j, (cname, clf) in enumerate([("ADA d=5", ada5),
                                          ("ADA d=10", ada10),
                                          ("RFC", rfc)]):
            clf.fit(X, y)
            if hasattr(clf, "decision_function"):
                Z = clf.decision_function(np.c_[xx.ravel(), yy.ravel()])
            else:
                Z = clf.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1]
            Z = Z.reshape(xx.shape)
            axes[j,i].contourf(xx, yy, Z, 20, cmap=cm, alpha=.8)
            axes[j,i].scatter(X[:,0], X[:,1], c=y, s=20,
                              cmap=cm_bright, edgecolors="black")
            
            axes[j,i].set_title(cname)
    plt.savefig("result.png", bbox_inches="tight")

if __name__ == "__main__":
    main()

結果

 こんなものになります。

result.png
result.png

 AdaBoostそのものには過学習を抑制するような仕組みはないので、ベース分類器の汎化性能がなければ(つまり木が深くなることを許せば)ほぼ決定木っぽい結果になることがわかります。バギング系の手法とは違います。

 最大深さ5では概ねまともですが、これでもよく見るとランダムフォレストより過学習の傾向を示しています。変なところにあるデータに引きずられる訳です。そうすると、決定木をもっと浅くするとどうなる? という疑問もありますが、実際にやったところ見るに堪えない結果になりました。性能が低い分類器ならうまくいくという感じでもないので、ある程度の識別性能のある分類器を使ったほうが良いです(見たい方は各自書き換えて実行してください)。

考察:ベース分類器のチューニングがだるい

 ランダムフォレストはバギング、ブートストラップサンプリングを使用することで、なんか適当にやっても程よい結果が得られるという素晴らしい性質を持っているのですが、アダブーストにはそれがありません。その時点で使いづらい感じになります。

 また、ブースティングなのであまりたくさんの分類器を使えない(計算時間的に)のも不利な点です。ランダムフォレストだと1000本くらいまでは増やしても改善につながるし、それはそれでいいんですが、そういう手が打ちづらい。

 ちょっと擁護しておくとすれば、今回は二次元でやっているので特徴量が2つしかなく、そもそも決定木系の手法にとっては不利といえば不利です。高次元では異なった挙動を示す可能性があるので、この結果を鵜呑みにはできません。でも、概ねの傾向はわかると思います。

結論

 気軽に使うというわけにはいかないけど、アルゴリズムの原理的にはパラメータチューニングするとランダムフォレストより良くなる可能性を秘めているようです。

 それって要するに玄人向けなんじゃ・・・かといって絶対性能で最強でもないんですが(勾配ブースティングでもいいよね)。

 とはいえ、ベース分類器の過学習さえ抑制しておけばそこそこランダムフォレストと同程度には使えそうなので、軽めのアンサンブル手法として便利ではあります。勾配ブースティングは重いんだよね。