はじめに
RFE(Recursive Feature Elimination)というものがあることを知ったので試してみたいと思いました。
RFEは特徴選択の手法で、その名の通り再帰的にモデルを再構築しながら特徴を選択するという特色があります。
sklearn.feature_selection.RFE — scikit-learn 0.20.2 documentation
スポンサーリンク
RFEとはなにか
RandomForestのような特徴重要度の出るestimatorがあったとして、たとえば64次元から32次元に特徴選択したいと思ったとします(元の次元数、選択後の次元数は何次元でも良いのですが、あとで実際にこの数字でやるので例として先に示します)。
真っ先に思いつくのは特徴重要度を見て重要度の低い下位32次元を捨てることですが、ゴミが大量に入った状態では特徴重要度がうまく計算できていないかもしれません。本来有用な特徴を捨ててしまったり、逆に本来ゴミな特徴を残してしまったりする可能性があります。
一方、RFEは「モデルを作って特徴重要度を計算する」→「下位n次元を捨てる」→「下位n次元を捨てた特徴量でまたモデルを作る」→「下位n次元を捨てる」→……と繰り返していき、望む次元数になるまでこれを続けます。直感的には、うまくいきそうだけど計算コストは大きそう、という気がします。
sklearnのRFEの使い方
記事の冒頭にも貼りましたが、ドキュメントはここです。
sklearn.feature_selection.RFE — scikit-learn 0.20.2 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次元まで落とすと、こうなりました。
今度は顕著な差がついた・・・ように見えますが、2箇所入れ替わっているだけです。この内容について考察することは控えたいと思いますが(よくわからないので)、とにかくこれくらいの差になります。10次元だと選択した特徴の数が少ないので、ある程度スコアの差にもなってくると思います。
まとめ
ぶっちゃけ、本当に良いの? という程度ですが、とにかく特徴選択に使えます。
stepsを大きめにすればたとえば元の10倍とかその程度の処理時間で済むようにすることもできる訳で、気楽に使ってみる分には良いんじゃないでしょうか。
すごく良い、というものではなさそうです。