静かなる名辞

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


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

はじめに

 RandomForest(ランダムフォレスト)には木の本数という重要なパラメータがある。slearnのデフォルトは10だが、実際に使うときは1000以上にしてやらないと良い性能が得られないということをよく経験する。

 これを大きくすることで、一体どんな効果が得られるのだろうか?

  • 予想1:より複雑な形状の分離超平面を学習できるようになる
  • 予想2:汎化性能が向上する

 予想1の効果は恐らく木の本数が相対的に少ないとき(100本以下)に顕著に現れると考えられる。その後、木の本数が増えていくに従ってモデルのバリアンスが下がり、予想2の通り汎化性能は向上する方向に向かうと考えられる。

 ここで思い浮かぶ疑問は、「とにかく木の本数を増やしさえすれば、SVMみたいに高い汎化性能が得られるのか?」という点である。RandomForestは決定木なので、基本的にデータの次元軸に直交する決定境界しか引けないという弱点がある。それでも、とにかく木を増やしていけば、丸くてぬるぬるした決定境界になったりするのだろうか?

 実際にどうなるのか、やってみよう。

スポンサーリンク



実験:2次元データで木の本数を変えながら予測確率を評価する

 コードは流し読みしてください。結果の画像だけ見ればわかります。

# coding: UTF-8

import numpy as np

from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.svm import SVC

from matplotlib import pyplot as plt

def make_circle(a=1, b=1, xy=(0,0), phi=0, n=100, random_s=0.1):
    theta = np.arange(0, 2*np.pi, 2*np.pi/n)
    X = a*np.cos(theta)
    Y = b*np.sin(theta)
    data_mat = np.matrix(np.vstack([X, Y]))
    phi_d = np.deg2rad(phi)
    rot = np.matrix([[np.cos(phi_d), -np.sin(phi_d)],
                     [np.sin(phi_d), np.cos(phi_d)]])
    rot_data = rot*data_mat
    X = rot_data[0].A
    Y = rot_data[1].A

    rand1 = np.random.normal(scale=random_s, size=theta.shape)
    rand2 = np.random.normal(scale=random_s, size=theta.shape)

    return X+rand1+xy[0], Y+rand2+xy[1]

def gen_data():
    n = 150
    X1, Y1 = make_circle(a=6.5, b=4.5, n=n, random_s=0.4)
    X2, Y2 = make_circle(a=5, b=3, n=n, random_s=0.4)
    X3, Y3 = make_circle(a=2.5, b=1.5, n=n, random_s=0.4)

    X = np.hstack([X1, X2, X3])
    Y = np.hstack([Y1, Y2, Y3])
    data = np.vstack([X, Y]).T
    target = np.array([0]*n + [1]*n + [0]*n)

    return data, target

def plot_proba(data, target, clf, filename="fig.png", text=""):
    plt.figure()
    ax = plt.subplot()
    ax.set_xlim((-9, 9))
    ax.set_ylim((-6, 6))

    x = np.arange(-9, 9, 18/250)
    y = np.arange(-6, 6, 12/250)

    X, Y = np.meshgrid(x, y)
    points = np.c_[X.ravel(), Y.ravel()]

    probs = clf.predict_proba(points)
    probs = probs[:,1].reshape(X.shape)

    plt.pcolormesh(X, Y, probs, cmap=plt.cm.RdBu)
    plt.scatter(data[:,0], data[:,1], c=["rb"[t] for t in target], edgecolors='k')
    plt.title(text)

    plt.savefig(filename)

def main():
    data, target = gen_data()

    for i, n_estimators in enumerate([5, 10, 30, 50, 100, 300, 500, 1000]):
        rfc = RFC(n_estimators=n_estimators, oob_score=True, n_jobs=-1)
        rfc.fit(data, target)
        print("{0}:{1}".format(n_estimators, rfc.oob_score_))
        plot_proba(data, target, rfc,
                   "rfc_{0}.png".format(n_estimators),
                   "RFC n_estimators={0}".format(n_estimators))

    svm = SVC(C=20, gamma=0.05, probability=True)
    svm.fit(data, target)
    plot_proba(data, target, svm, "svm.png", "SVM C=20, gamma=0.05")

if __name__ == "__main__":
    main()

 昨日の記事の楕円形データ生成を使っています。
hayataka2049.hatenablog.jp

結果

f:id:hayataka2049:20180308194151p:plain
f:id:hayataka2049:20180308194158p:plain
f:id:hayataka2049:20180308194207p:plain
f:id:hayataka2049:20180308194223p:plain
f:id:hayataka2049:20180308194232p:plain
f:id:hayataka2049:20180308194241p:plain
f:id:hayataka2049:20180308194248p:plain

 そこそこ良くなるが、100以上では改善度合いは微妙かもしれない。OOB errorは、

5:0.8288888888888889
10:0.8822222222222222
30:0.9044444444444445
50:0.9133333333333333
80:0.9155555555555556
100:0.9222222222222223
300:0.92
500:0.9177777777777778
1000:0.9222222222222223

 やはり100以上の改善は微妙という、画像を見て思う感覚を裏付けるものになっている。

 では、SVMだとどんな画像が得られるだろうか?
f:id:hayataka2049:20180308194626p:plain
 これは勝てない。RandomForestだとどうしてもカクカクが残るのに。

考察

 この結果の妥当性は率直に言って判断しづらい。

 そもそも、2次元データを入力している以上、ランダムフォレストはデフォルトで( \sqrt{2})の特徴量を使って木を作ってくれている訳で、つまり1次元だけで判断してくれている。ちょっとあんまりなので、max_features=2としたのが次の画像。データが(ランダム絡みで)変わってるのでそこだけ注意。
f:id:hayataka2049:20180308200130p:plain
 まあ、これを見てもSVMみたいに滑らかな決定境界が引けてるとは言い難いものがあるけど・・・(考えようによっては1次元でやった上の画像の方が汎化性能は高い、ような気もしてこなくはない。全体的にもやっとしてて、相対的に滑らかに見える)。

 でも、SVMもSVMで、パラメタ次第ではどんな複雑怪奇な決定境界だって引けるといえば引ける。
f:id:hayataka2049:20180308200634p:plain

 こういう問題(けっきょく汎化性能が得られるかどうかはパラメタ次第)があるので、SVMの方が良いと一概に言えるかはけっこう微妙。

 もっと言えば、「それぞれの軸に意味がある」「ノイズもけっこう混ざってる」「スパース」「高次元」という性質のデータを対象とする場合、SVMの汎化性能(滑らかな決定境界を引ける)はかえって邪魔になるのではないだろうか? そのようなデータでは、とにかく重要な軸を見つけてきて、そこで判断するRandomForestの方が良い性能が得られることが多いと経験的に感じる。ちなみに自然言語処理で使うBoW(Bag of Words)はその典型例である。

 逆に、軸に意味がなくて相対的に低次元でデンスな空間を相手にする場合、SVMの方が良い結果を産むということもよく経験することである。PCAで低次元に落としてしまったデータとか、word2vecで生成される単語の空間とかが割とそんな感じである。

 なんだか話が脱線してきたのでこれくらいにするけど、けっきょく「滑らかな決定境界を引く能力はどうやってもSVMの方が高い(あたりまえ)」「滑らかだから良いというものでもない」「使い分けが重要」という当たり障りのない結論に落ち着いてしまった。

 あと、木の本数は無尽蔵に増やすわけにはいかない。ランダムフォレストは計算量は軽いけど意外とリソース消費の激しいアルゴリズムで、増やしすぎると効率が悪化する。

ランダムフォレストはサンプル数が多いとメモリ消費量が大きい - 静かなる名辞

 汎化性能はできるだけ他の方法で確保したいところ。

まとめ

 SVMの方が滑らかでした(小並感)。