静かなる名辞

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



【python】ランダムフォレストの特徴重要度で特徴選択を試す

 最終更新:2018-04-02

はじめに

 RandomForestでは特徴重要度を計算できる、というのは結構有名な話です。では、これはどの程度実用的なのでしょうか?

 pythonのsklearnを使い、簡単に実験して確かめてみました。

 目次

実験条件

 実験ではsklearnのdatasetsに含まれるdigitsデータを使います。これは8*8 pixelの手書き文字画像をべたーっと64次元の1次元配列にしたものです。

 分類タスクなので交差検証を行うことが望ましいですが、簡単のためtrain:test=0.6:0.4で全データを分割しました。手抜きです。

 RandomForestのパラメータも、性能重視ならグリッドサーチなどを行って厳密にチューニングするべきですが、今回は単にn_estimators=500としました。RandomForestではとにかく木の本数を増やすのが先決で、他のパラメータは大して効かないような印象があります。

 この条件で、特徴重要度の高い次元だけ残す方法で、64次元→32, 16, 8次元まで特徴選択を行い、どの程度の性能が得られるか調べました。

実験

 まず、先に実験に使ったソースコード全体を示します。長いので、とりあえず読み飛ばすことを推奨します。ソースコードを掲載した後、サワリの部分だけ解説します。

実装

# coding: UTF-8

import seaborn as sns
import matplotlib.pyplot as plt

import numpy as np

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

def visualize_digits_importances(importances):
    im_array = np.reshape(importances, (8, 8))
    
    plt.figure()
    sns.heatmap(im_array)
    plt.savefig("importances.png")

def main():
    # 訓練データ、テストデータの生成
    digits = load_digits()
    (X_train, X_test, 
     y_train, y_test) = train_test_split(digits.data, digits.target, test_size=0.4)

    # 特徴重要度計算のためのRFの訓練
    rfc1 = RFC(n_estimators=500, n_jobs=-1)
    rfc1.fit(X_train, y_train)

    # この場合の性能を確認
    preds = rfc1.predict(X_test)
    print("64dim")
    print("precision:{0:.4f}  recall:{1:.4f}  f1:{2:.4f}".format(
        *prfs(y_test, preds, average="macro")))

    # わかりやすいように重要度を描画してみた
    visualize_digits_importances(rfc1.feature_importances_)

    # 重要さ順に並んだindexを作成
    important_index = [i for importance, i in sorted(
        zip(rfc1.feature_importances_, range(rfc1.n_features_)),
        key=lambda x:x[0], reverse=True)]

    # 次元数=48, 32, 24, 16, 8, 4の6パターンで試す
    for n_features in [48, 32, 24, 16, 8, 4]:
        index = important_index[:n_features]
    
        # 重要な特徴だけ選択した特徴量を新しく作る
        selected_X_train, selected_X_test = [x[:,index] for x in [X_train, X_test]]
 
        # 重要な特徴だけで学習
        rfc2 = RFC(n_estimators=500, n_jobs=-1)
        rfc2.fit(selected_X_train, y_train)
    
        # 重要な特徴だけ使った場合の性能を確認
        preds = rfc2.predict(selected_X_test)
        print("{0}dim".format(n_features))
        print("precision:{0:.4f}  recall:{1:.4f}  f1:{2:.4f}".format(
            *prfs(y_test, preds, average="macro")))

if __name__ == "__main__":
    main()

気を配ったこと

わかりやすいように重要度を描画してみた

 変数重要度は64次元の配列として得られますが、これを8*8次元に戻してヒートマップとして描画してみました。

f:id:hayataka2049:20180125070831p:plain

 手書き文字なので、端っこの方はそもそも真っ白なのかな? というのがとりあえず見て思うこと。この絵を見ると、全体の2/3~1/2のpixelはロクな情報を持っていないような気がします。

 実際はどうなのか、分類を行わせて確かめていきます。

重要さ順に並んだindexを作成

 綺麗に書く方法が思いつかないので、zipとsortedを駆使して作ることになりました。

important_index = [i for importance, i in sorted(
    zip(rfc1.feature_importances_, range(rfc1.n_features_)),
    key=lambda x:x[0], reverse=True)]

 ま、じっくり読めば何やってるかはわかると思いますが、「重要度とインデックス(何次元目の特徴か)をセットにしてzip」→「重要度で降順ソート」→「インデックスだけ取り出す」という処理の流れです。こういう処理を、もうちょっと綺麗に書ける手段があるとpython界隈の発展のためには良いと思いますが、zipとリスト内包表記でなんでもなんとかしてしまうのがpython言語の魅力という気もします。こういうの書いてると、キモいんだけど楽しいです。

※追記:2018/4/2
 この記事を書いたときには見落としていましたが、sklearnにはSelectFromModelというクラスがあるので、上の方法よりずっと手軽に同様の処理が行えます。

sklearn.feature_selection.SelectFromModel — scikit-learn 0.19.1 documentation

 ただし、SelectKBest的にK次元残すということはできず、特徴重要度の値がスレッショルド以上のものを取るという処理になるようです。

重要な特徴だけ選択した特徴量を新しく作る
selected_X_train, selected_X_test = [x[:,index] for x in [X_train, X_test]]

 これは1つ前の記事で書いたテクニックを使って1行で済ませています。逆に、これを使わないと難しいと思います。


 パっと見てプログラム的な面白み(=考えつくのが大変で、その書き方に到達するまでに時間がかかる処理)があるのはこれくらいでしょうか。後の処理は無難に書いてるので、解説は省略。

結果

 まず元の64次元で分類した後、次元数を48, 32, 24, 16, 8, 4の6パターンに落として、同様に学習・分類を行わせてみました。評価指標として適合率、再現率、F1値を出しています。

 結果はこんな感じになりました。なお、乱数が絡むので(RandomForestの作成とtrain_test_splitのところで)、実行ごとに0.01~0.03程度の変動があります。

64dim
precision:0.9706  recall:0.9713  f1:0.9705
48dim
precision:0.9721  recall:0.9729  f1:0.9720
32dim
precision:0.9687  recall:0.9687  f1:0.9682
24dim
precision:0.9574  recall:0.9581  f1:0.9574
16dim
precision:0.9347  recall:0.9359  f1:0.9348
8dim
precision:0.8861  recall:0.8887  f1:0.8866
4dim
precision:0.5917  recall:0.5979  f1:0.5929

 64次元で0.97強のスコアが得られているので、割と簡単な部類の問題なのがわかります。重要度の高い上位48次元に減らしたケースでは64次元より若干スコアが改善していますが、実はこれは実行ごとに上下関係がひっくり返ったりするので、あまり当てになりません。特徴選択しても、汎化性能を上げる効果はないということです(ただし、データがゴミだらけだったり、分類器の性能が違ったりする条件だと効く可能性もある。あくまでも今回の場合の話)。

 32次元まで減らしてもスコアはほぼ維持できており、16次元まで落とすと0.04ポイントほど減少しますがそれでもまだ9割強です。8次元だとさすがに9割を割りますが、それでもけっこう上手く分類できていると言って良い数字です。4次元だとちょっときついですが、それでも1/10から1/1.7くらいまで絞り込んだという見方もできるといえばできます。というか、たった4ピクセルでここまで数字が判別できるのは正直意外でした。まあ、sklearnのdigitsが分類しやすいように調整されているだけで、現実のデータだとこんなに甘くはないケースが多いかと思います。

考察

 これは元のデータがスカスカで、全体の1/8のピクセルだけ見ればだいたいどの数字が書いてあるのかわかるようなデータだった・・・ということを意味しているに過ぎませんから、どんなデータでも特徴重要度による特徴選択が効くとは限りません。それでも、特徴重要度の強さが一部の次元に集中しているようなデータが相手であれば、同様のアプローチによる特徴選択は十分有効だと考えられます。

 PCAなどの次元削減手法と比較して、どういう点が有利か? PCAは分散の大小だけ見るので、「すごく大きいノイズが乗ってるんだけど、実は分類に意味のある情報は全く入ってない」みたいな次元があると、使ったことでかえって分類精度が悪化することもままあります。RandomForestの特徴重要度は正解ラベルを参照して決まるので、そういう事態は避けられるはずです。もちろん他の手法でも特徴選択はできますが、手頃に使える方法としてRandomForestは悪くはないと思います*1

 では、実用上どんな意味があるか? 他のアルゴリズムに入れる前にRandomForestにかけてみて、ゴミ情報はさっさと除去するという応用がまず考えられます。ちょっとは汎化性能に効いてくるはずだし、処理もだいぶ軽くなるでしょう。

 あるいは、ビッグデータに使うとか、webサービスで投げられたクエリに基づいて分類を行い、応答を返すようなケースでは、予測のスピードが要求されることもあるかもしれません。「精度はカリカリにチューンしなくても良いけど、速度は欲しい」ようなとき、次元数を減らすのはどんなアルゴリズムでもダイレクトに効果が出る方法です。しかも(データにもよるとはいえ)ある程度は次元を削っても最高精度に近い数字が出せることがわかっているのですから、やらない手はありません。

まとめ

 軽く試した限りではけっこう有用です。場面さえ選べばちゃんと実用性があると思いました。

*1:ただし次元数と木の数が大きいとRandomForestは遅い印象がある。最初は木の数を少なくして、極端な話ただの決定木にして計算した方が良いのかも