一般によく使われる次元削減手法としてはPCA(主成分分析)がありますが、他にLDA(Linear Discriminant Analysis:線形判別分析)を使う方法もあります。
これは本来は分類に使われる判別分析という古典的なアルゴリズムで、データが一番分離しやすくなる軸を求めていくものです。つまり教師ラベルを使います。教師ラベルを使うので、PCAのような教師なしの手法と比べて有利な可能性があります。
線形判別分析の詳しい原理の説明などが欲しい方は、ググって出てくるwikipediaやqiitaなどを参考にしてください(投げやり)。この記事では、分類問題でこれを使ったとき、どのようなご利益があるのかを検証します。
実験
sklearnのdigitsデータセットを使い、次元削減→分類というタスクを行って交差検証でスコアを出します。
分類器は最初はSVMでやろうかと思ったけど、パラメタチューニングで幾らでも恣意的な結果になることに気づいたのでガウシアン・ナイーブベイズでやることにしました。
実験に使ったコードは以下に示します。
# coding: UTF-8 import warnings warnings.filterwarnings('ignore') import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_digits from sklearn.decomposition import PCA from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA from sklearn.naive_bayes import GaussianNB as GNB from sklearn.pipeline import Pipeline from sklearn.model_selection import StratifiedKFold as SKF from sklearn.metrics import precision_recall_fscore_support as prf def main(): digits = load_digits() gnb = GNB() df = pd.DataFrame([], columns=[ "n_components", "pca-gnn precision", "pca-gnn recall", "pca-gnn f1", "lda-gnn precision", "lda-gnn recall", "lda-gnn f1"]) for n_components in [5, 10, 15, 20, 25, 30, 40]: pca = PCA(n_components=n_components) lda = LDA(n_components=n_components) steps1 = list(zip(["pca", "gnb"], [pca, gnb])) steps2 = list(zip(["lda", "gnb"], [lda, gnb])) p1 = Pipeline(steps1) p2 = Pipeline(steps2) score_lst = [] for decomp_name, clf in zip(["pca", "lda"], [p1, p2]): trues = [] preds = [] for train_index, test_index in SKF( shuffle=True, random_state=0).split( digits.data, digits.target): clf.fit(digits.data[train_index], digits.target[train_index]) trues.append(digits.target[test_index]) preds.append(clf.predict(digits.data[test_index])) scores = prf(np.hstack(trues), np.hstack(preds), average="macro") score_lst.extend(scores[:-1]) df = df.append(pd.Series([n_components, *score_lst], index=df.columns), ignore_index=True) print(df) plt.figure() df.plot(x="n_components", y=["pca-gnn f1", "lda-gnn f1"]) plt.savefig("result.png") if __name__ == "__main__": main()
結果
次のようになりました。
テキスト出力
n_components pca-gnn precision pca-gnn recall pca-gnn f1 \ 0 5.0 0.847918 0.841684 0.841109 1 10.0 0.915834 0.911346 0.912563 2 15.0 0.926992 0.923032 0.924061 3 20.0 0.934522 0.930192 0.931194 4 25.0 0.941886 0.938611 0.939205 5 30.0 0.946139 0.944251 0.944669 6 40.0 0.945330 0.943644 0.943960 lda-gnn precision lda-gnn recall lda-gnn f1 0 0.917464 0.917144 0.917031 1 0.953751 0.952588 0.952950 2 0.953751 0.952588 0.952950 3 0.953751 0.952588 0.952950 4 0.953751 0.952588 0.952950 5 0.953751 0.952588 0.952950 6 0.953751 0.952588 0.952950
LDAを使った方が低い次元で、より高い分類性能が得られているようです。
まとめ
LDAは良い。
おまけ
ソースコードをちゃんと読んだ方は、最初に書かれた以下の記述に気づいたかと思います。
import warnings warnings.filterwarnings('ignore')
これを付けないとLDAはけっこうな警告(主に以下の2つ)を吐いてくれます。
UserWarning: Variables are collinear UserWarning: The priors do not sum to 1. Renormalizing
上の警告はPCAで説明変数の多重共線性を除去してやると消えます(本末転倒っぽいけど)。下の警告は、正直調べてもよくわかりませんでした。
とりあえず、警告が出てもちゃんと動いてるみたいなので別に良いか・・・。
追記
LDAのn_componentsには上限があり、クラス数-1以上のn_componentsは指定しても無意味です。
実際にやってみても、クラス数-1以上にはなりません。
>>> from sklearn.datasets import load_digits >>> from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA >>> lda = LDA(n_components=15) >>> lda.fit(digits.data, digits.target) >>> lda.explained_variance_ratio_ array([0.28912041, 0.18262788, 0.16962345, 0.1167055 , 0.08301253, 0.06565685, 0.04310127, 0.0293257 , 0.0208264 ])
決定境界をクラス数-1個引くので(SVMで言うところのone-versus-the-rest)、n_componentsも必然的にそれだけ必要になります(逆にそれ以上は必要になりません)。
上のグラフはそのつもりで眺めてください。また、LDAはけっきょくのところ線形変換なので、クラス数-1次元の線形空間にうまく張り直せないような入力に対しては無力なことも覚えておく必要があるでしょう(PCAも非線形構造はダメだが・・・カーネルでも持ってくる必要がある)。