特徴量削減が精度向上に寄与することがあるのか実際に検証してみた


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">これ300回ぐらいTwitterに書いてる。いつの時代に生きてるのかという感じ。（ただし、特徴量の分布のシフトが大きい場合は別だけど。） <a href="https://t.co/unSEqvyiWl">https://t.co/unSEqvyiWl</a></p>&mdash; mamas (@mamas16k) <a href="https://twitter.com/mamas16k/status/1600355349407883264?ref_src=twsrc%5Etfw">December 7, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

twitterで，精度向上のために特徴量削減をするのは意味がない，という話を聞いたので実際にどうなのかトイデータで試してみる

### 問題設定
データはsklearnのdatasetsで適当な人工データを作成する．
モデルとしてはsklearnのMLPClassifierを使用する．
特徴量削減のためのimportanceにはpermutation importanceを使用する．
精度の評価にはaucを使用する．
seedを変えながら複数回実験を行い，全特徴量を使用したケースと特徴量を削減したケースでどちらのaucが高いことが多いのかを調べる．
結果に影響を与えるのか調べるパラメータは，重要な特徴量数（n_informative）と削減後の特徴量数（n_use_feat）とする．
特徴量の数は20で固定する．

```
## データセット
x, y = datasets.make_classification(n_samples=1000,
                                    n_features=20,
                                    n_informative=n_informative,
                                    shuffle=True)
```
### 結果
全特徴量を使用した方が精度が良い場合もあれば，特徴量削減をした方が精度が良い場合もあった．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3034137/81a05d58-5309-570d-9862-4df19a072e92.png)
表の中の値は，全特徴量を使用した際のaucが特徴量削減をした際のaucに勝った割合．
0.5なら同点， 0.5よりも大きければ全特徴量の勝ち， 0.5より小さければ特徴量選択の勝ち．
n_informativeが大きい場合，n_use_featを減らすと精度が悪化しやすいが，
n_informativeが小さい場合には使用する特徴量数を削減した方が良い，という結果になった．

### 結論
特徴量削減が精度向上に寄与するケースが存在することを示せた．　
以下は，あくまで今回の実験設定での今回の実験範囲内での結果です． 鵜呑みにしないでください．
有益な特徴量が75%以上（n_informative=15）の場合は特徴量選択しなくていい． 削減すると有用な特徴量を削ってしまうリスクが上がるのかも．
有益な特徴量が40%を切った場合には特徴量選択した方が良さげ．

### 雑感
（優秀な人は有用な特徴量しか作らないので）精度向上のために特徴量削減をするのは意味がない，ということなのかなと思いました．

今回はmlpで実験を行いましたが，gbdtだとゴミ特徴量を完全に無視できることからmlpよりもノイズに強いと思うので同じような結果にはならないかもしれません．
（せっかくなのでtwitterでの意見と反対の結果が出やすいようにしようと思ってgbdtよりもノイズに弱そうなMLPを使用しました）


実験に使用したコード
```
import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import KFold
from sklearn.metrics import roc_auc_score
from sklearn import datasets
from tqdm.auto import tqdm
import matplotlib.pyplot as plt

import warnings
warnings.filterwarnings('ignore')

def create(n_informative=2):
    x, y = datasets.make_classification(n_samples=1000,
                                        n_features=20,
                                        n_informative=n_informative,
                                        shuffle=True)
    x_train, y_train = x[::2], y[::2]
    x_test, y_test = x[1::2], y[1::2]
    return x_train, y_train, x_test, y_test

def experiment(
        seed = 0,
        n_informative = 2,
    ):
    np.random.seed(seed)
    
    ## ２値分類データセット
    x_train, y_train, x_test, y_test = create(n_informative)
    kf = KFold(5, shuffle = True)

    ## 5 foldのcross validationを行って permutation importanceを計算する
    n_permutation_importance_rand = 3 ## permutation importanceの計算に使用するpermutationの回数
    permutation_importance_records = []
    for tr_idx, va_idx in kf.split(x_train):
        model = MLPClassifier(x_train.shape[-1])
        model.fit(x_train[tr_idx], y_train[tr_idx])
        pred = model.predict_proba(x_train[va_idx])[:,1]
        base_auc = roc_auc_score(y_train[va_idx], pred) ## 普通に推論した時のvalidationのauc

        ## permutation importanceの計算
        for i in range(x_train.shape[1]):
            x_train_copy = x_train.copy()
            for k in range(n_permutation_importance_rand):
                x_train_copy[:,i] = np.random.permutation(x_train_copy[:,i])
                pred = model.predict_proba(x_train_copy[va_idx])[:,1]
                auc = roc_auc_score(y_train[va_idx], pred)
                diff = auc - base_auc ## importance
                permutation_importance_records.append([i, diff])

    ## permutation importanceの計算結果を統合して特徴量をdropする
    pi_df = pd.DataFrame(permutation_importance_records, columns = ["col_idx", "diff"])
    pi_df_agg = pi_df.groupby("col_idx")["diff"].mean()
    pi_sort = pi_df_agg.sort_values()
#     print("dropped : ", pi_sort[-n_drop:].index)

    ## 全特徴量を使用して精度を評価
    model = MLPClassifier(x_train.shape[-1])
    model.fit(x_train, y_train)
    pred = model.predict_proba(x_test)[:,1]
    base_auc = roc_auc_score(y_test, pred)

    ## permutation importanceの上位の特徴量を使用して精度を評価
    drop_aucs = {}
    for n_use in [5, 10, 13, 15, 17]:
        use_columns = pi_sort[:n_use].index
        model = MLPClassifier(len(use_columns))
        model.fit(x_train[:,use_columns], y_train)
        pred = model.predict_proba(x_test[:,use_columns])[:,1]
        drop_auc = roc_auc_score(y_test, pred)
        drop_aucs[n_use] = drop_auc

    return base_auc, drop_aucs

## seedを変えながら実験を回して， 全特徴量を使用した場合のauc と importanceで選択した特徴量だけを使用した場合のauc を計算する
recs = []
for n_informative in [2, 5, 8, 10, 15]: ## 有用な特徴量数． 全体の特徴量数は20で固定して実験を行う
    base_auc_rec = []
    drop_aucs_rec = []
    for seed in tqdm(range(42)): ## seedを変えながら実験する
        base_auc, drop_aucs = experiment(seed, n_informative)
        base_auc_rec.append(base_auc)
        drop_aucs_rec.append(drop_aucs)

    ## 実験結果の統合
    drop_aucs_df = pd.DataFrame(drop_aucs_rec)
    for c in drop_aucs_df.columns:
        win_rate = np.mean((np.array(base_auc_rec) - drop_aucs_df[c]) > 0)
        recs.append({"n_informative" : n_informative, "n_use_feat" : c, "auc_win_rate" : win_rate})

rec_df = pd.DataFrame(recs)
display(rec_df.pivot_table(index = "n_informative", columns = "n_use_feat", values = "auc_win_rate"))
```