静かなる名辞

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



【python】sklearnのRFE(Recursive Feature Elimination)を使ってみる

はじめに

 RFE(Recursive Feature Elimination)というものがあることを知ったので試してみたいと思いました。

 RFEは特徴選択の手法で、その名の通り再帰的にモデルを再構築しながら特徴を選択するという特色があります。

sklearn.feature_selection.RFE — scikit-learn 0.20.1 documentation

RFEとはなにか

 RandomForestのような特徴重要度の出るestimatorがあったとして、たとえば64次元から32次元に特徴選択したいと思ったとします(元の次元数、選択後の次元数は何次元でも良いのですが、あとで実際にこの数字でやるので例として先に示します)。

 真っ先に思いつくのは特徴重要度を見て重要度の低い下位32次元を捨てることですが、ゴミが大量に入った状態では特徴重要度がうまく計算できていないかもしれません。本来有用な特徴を捨ててしまったり、逆に本来ゴミな特徴を残してしまったりする可能性があります。

 一方、RFEは「モデルを作って特徴重要度を計算する」→「下位n次元を捨てる」→「下位n次元を捨てた特徴量でまたモデルを作る」→「下位n次元を捨てる」→……と繰り返していき、望む次元数になるまでこれを続けます。直感的には、うまくいきそうだけど計算コストは大きそう、という気がします。

sklearnのRFEの使い方

 記事の冒頭にも貼りましたが、ドキュメントはここです。

sklearn.feature_selection.RFE — scikit-learn 0.20.1 documentation

 モデルに渡せる引数としては、以下のものがあります。

  • estimator

 好きなestimatorを渡せます……と言いたいところですが、coef_かfeature_importances_を持っている必要があります。coef_のときはデータをスケーリングしないとダメじゃないのかとか、SVMのcoef_はカーネルが絡むから特殊なんじゃないのかとか、色々と微妙な懸念があります(深く追求はしません)。一番安心して使えるのはRandomForestといった決定木のアンサンブル系など、feature_importances_があるようなestimatorです。

  • n_features_to_select

 最終的に選択したい特徴量の次元数です。デフォルトはNoneで、そうすると元の次元数の半分にされます。

  • step

 一度モデルを再構築するたびにどれだけ次元数を減らすか。これを大きくすると速く計算できますが、そのぶん雑な感じの選択になりそうな気がします。デフォルトは1です。

  • verbose

 設定すると学習過程をprintしてくれます。デフォルトは0で何も表示されません。1にすると学習過程が出てきます。1にしたときと2以上にしたときとの違いは私にはわかりませんでした。

 使い方は、適当にモデル作って、fit&predictで動かすだけです。

実際にやってみる

 digitsデータセットの分類をやります。digitsデータセットは64次元なので、

  • そのまま64次元で学習&予測
  • 64次元で学習させた際の特徴重要度を用いて下位32次元を切り捨て、学習&予測
  • RFEを使って32次元まで落とし、学習&予測

 の3パターン試してみました。なお、step=4としました。

 コードを以下に示します。

from sklearn.datasets import load_digits
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report as clf_report

def main():
    digits = load_digits()
    
    X_train, X_test, y_train, y_test = train_test_split(
        digits.data, digits.target, random_state=0)

    # とりあえず普通にRFCにそのまま入れてみる
    rfc = RFC(n_estimators=100)
    rfc.fit(X_train, y_train)
    preds = rfc.predict(X_test)

    print("通常のRandomForest")
    print(clf_report(y_test, preds, digits=3))

    # 重要度の低い下位32次元を落としてみる
    idx = rfc.feature_importances_.argsort()[32:]
    X_train_32 = X_train[:,idx]
    X_test_32 = X_test[:,idx]
    
    rfc.fit(X_train_32, y_train)
    preds = rfc.predict(X_test_32)

    print("特徴重要度が低いものを32個削除")
    print(clf_report(y_test, preds, digits=3))

    # RFEを使ってみる
    rfec = RFE(rfc, step=4, verbose=1)
    rfec.fit(X_train, y_train)
    preds = rfec.predict(X_test)
    print("RFE + RFC", rfec.n_features_)
    print(clf_report(y_test, preds, digits=3))

if __name__ == "__main__":
    main()

 結果は、

通常のRandomForest
              precision    recall  f1-score   support

           0      0.974     1.000     0.987        37
           1      0.956     1.000     0.977        43
           2      1.000     0.955     0.977        44
           3      0.938     1.000     0.968        45
           4      1.000     0.974     0.987        38
           5      1.000     0.958     0.979        48
           6      1.000     1.000     1.000        52
           7      0.980     1.000     0.990        48
           8      1.000     0.958     0.979        48
           9      0.957     0.957     0.957        47

   micro avg      0.980     0.980     0.980       450
   macro avg      0.980     0.980     0.980       450
weighted avg      0.981     0.980     0.980       450

特徴重要度が低いものを32個削除
              precision    recall  f1-score   support

           0      0.974     1.000     0.987        37
           1      0.956     1.000     0.977        43
           2      1.000     0.909     0.952        44
           3      0.917     0.978     0.946        45
           4      0.974     0.974     0.974        38
           5      0.958     0.958     0.958        48
           6      1.000     0.981     0.990        52
           7      0.959     0.979     0.969        48
           8      0.957     0.938     0.947        48
           9      0.957     0.936     0.946        47

   micro avg      0.964     0.964     0.964       450
   macro avg      0.965     0.965     0.965       450
weighted avg      0.965     0.964     0.964       450

Fitting estimator with 64 features.
Fitting estimator with 60 features.
Fitting estimator with 56 features.
Fitting estimator with 52 features.
Fitting estimator with 48 features.
Fitting estimator with 44 features.
Fitting estimator with 40 features.
Fitting estimator with 36 features.
RFE + RFC 32
              precision    recall  f1-score   support

           0      0.974     1.000     0.987        37
           1      0.977     1.000     0.989        43
           2      1.000     0.909     0.952        44
           3      0.936     0.978     0.957        45
           4      1.000     0.974     0.987        38
           5      0.979     0.958     0.968        48
           6      1.000     1.000     1.000        52
           7      0.960     1.000     0.980        48
           8      0.979     0.958     0.968        48
           9      0.938     0.957     0.947        47

   micro avg      0.973     0.973     0.973       450
   macro avg      0.974     0.973     0.973       450
weighted avg      0.974     0.973     0.973       450

 微妙に単純に下位32次元切り捨てよりRFEの方が良さげにみえますが、はっきり言ってこれはほぼ誤差です。試行によって逆転するときもあります。

 何回か回した感じ、微妙にRFEの方が優秀そうな気はするので、たぶん100回くらい回して検定すれば有意差は出るんじゃないかなぁ、と思います。面倒なのでやりませんけど。

 何しろ元の分類精度が良いせいで、あまり際立った結果にならない感がありますが、元論文のアブストではベースライン86%に対して98%の精度を達成した、といった数字が示されています。実際のところ、どうなんでしょうね・・・。

 なお、16次元まで削ったところまあまあ顕著な差(それでも1%ない程度ですが、逆転する頻度は4回に1回程度まで減少した)になったことを言い添えておきます。あと、stepsに対しては性能の変化はあまりない気がしました。

 データが差が出づらいものな可能性はありますが、ちょっと微妙すぎるかな・・・と思い、step=1にして10次元まで削ったところ、やっと納得できるような差(見てわかるレベル、数%)が生じ、RFE有利になりました。大きく次元を落とす分には良さそうです。ただ、スコアそのものが0.9前後になってしまうので、絶対性能の観点からするとどうだろうという気はします。

可視化してみる

 digitsはMNISTと同様の、8*8の数字の画像のデータなので、どの特徴が有効になったのかを画像として可視化してみました。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.model_selection import train_test_split

def main():
    digits = load_digits()
    
    X_train, X_test, y_train, y_test = train_test_split(
        digits.data, digits.target, random_state=0)

    rfc = RFC(n_estimators=100)
    rfc.fit(X_train, y_train)

    idx_a = np.array([True]*64)
    idx_a[rfc.feature_importances_.argsort()[:32]] = False
    idx_a = idx_a.reshape(8, 8)

    rfec = RFE(rfc, step=4, verbose=1)
    rfec.fit(X_train, y_train)
    idx_b = rfec.support_.reshape(8, 8)

    fig, axes = plt.subplots(nrows=1, ncols=2)

    axes[0].imshow(idx_a)
    axes[1].imshow(idx_b)
    plt.savefig("result.png")

if __name__ == "__main__":
    main()

可視化した結果
可視化した結果

 左が一気に落とした場合、右がRFEですが、……気のせいレベルの差異しかないですね。ただの乱数のいたずらじゃないの? というレベル。

 step=1として10次元まで落とすと、こうなりました。

step=1で10次元まで落とした場合
step=1で10次元まで落とした場合

 今度は顕著な差がついた・・・ように見えますが、2箇所入れ替わっているだけです。この内容について考察することは控えたいと思いますが(よくわからないので)、とにかくこれくらいの差になります。10次元だと選択した特徴の数が少ないので、ある程度スコアの差にもなってくると思います。

まとめ

 ぶっちゃけ、本当に良いの? という程度ですが、とにかく特徴選択に使えます。

 stepsを大きめにすればたとえば元の10倍とかその程度の処理時間で済むようにすることもできる訳で、気楽に使ってみる分には良いんじゃないでしょうか。

 すごく良い、というものではなさそうです。