ギークなエンジニアを目指す男

機械学習系の知識を蓄えようとするブログ

主成分分析(PCA)の累積寄与率で見る特徴量エンジニアリング【python】

f:id:taxa_program:20191211172321j:plain

はじめに

昨日、主成分分析の累積寄与率も特徴量選択に使えますよというアドバイスをTwitterでいただいたので、忘れないうちに勉強しておきます。

主成分分析(PCA)とは

Principal Component Analysisの略であり、データセットの特徴量を相互に統計的に関連しないように回転する手法です。

回転したあとの特徴量から、データを説明するのに重要な一部の特徴量だけを抜き出すこと(次元圧縮)ができます。また、多くの特徴量を持つデータの可視化にも利用できます。

固有値と寄与率と累積寄与率

固有値

主成分分析を行うと各主成分に対応した固有値を求めることができます。この固有値は主成分の分散に対応しており、その主成分がどの程度元のデータの情報を保持しているかを表しています。

寄与率

ある主成分の固有値が表す情報が、データのすべての情報の中でどの位の割合を占めるかを表します。「主成分軸一つで、データの何割を説明することができているか」を表したものとも言えます。

累積寄与率

各主成分の寄与率を大きい順に足しあげていったもので、そこまでの主成分でデータの持っていた情報量がどのくらい説明されているかを示します。

cancerデータセットで主成分分析

今回はscikit-learnで提供されているcancerデータセットを利用します。

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import mglearn
from IPython.display import display

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# データの読み込み
cancer = load_breast_cancer()
X_df = pd.DataFrame(data=cancer.data,columns=cancer.feature_names)
X_df.head()

f:id:taxa_program:20190207234820p:plain
cancerデータセット

ロジスティック回帰でテスト

主成分分析で次元圧縮する前に、まずはそのまま30個の説明変数を使って、ロジスティック回帰で学習・テストさせて、基準となる精度を出してみます。

標準化

# 分散:1にスケーリング
scaler = StandardScaler()
scaler.fit(X_df)
X_scaled = scaler.transform(X_df)
X_scaled_df = pd.DataFrame(data=X_scaled,columns=cancer.feature_names)
X_scaled_df.head()

f:id:taxa_program:20190207235047p:plain
標準化後のcancerデータセット

学習

from sklearn.linear_model import LogisticRegressionCV
from sklearn.metrics import confusion_matrix

# 訓練用と検証用に分割
X_train, X_test, y_train, y_test = train_test_split(X_scaled_df, y, random_state=0, test_size=0.3)

# ロジスティック回帰で学習
lr = LogisticRegressionCV(cv=10, random_state=0)
y_train=y_train.values.reshape(-1)
lr.fit(X_train, y_train)

# 検証
print('Train score: {:.3f}'.format(lr.score(X_train, y_train)))
print('Test score: {:.3f}'.format(lr.score(X_test, y_test)))
print('Confustion matrix:\n{}'.format(confusion_matrix(y_true=y_test, y_pred=lr.predict(X_test))))

# -> Train score: 0.987
# -> Test score: 0.977
# -> Confustion matrix:
# -> [[ 60   3]
# ->  [  1 107]]

テストデータで精度97%というまずまずの結果になりました。

主成分分析で寄与率を確認

まずは次元圧縮せず(=特徴量:30のまま)に、寄与率を確認してみます。

from sklearn.decomposition import PCA

# 元の特徴量と同じ数で主成分分析
pca = PCA(n_components=30)
pca.fit(X_scaled)
plt.bar([n for n in range(1, len(pca.explained_variance_ratio_)+1)], pca.explained_variance_ratio_)

f:id:taxa_program:20190207235534p:plain:w400

# 寄与率の確認
np.set_printoptions(precision=5, suppress=True) # numpyの小数点以下表示桁数と、指数表記設定
print('explained variance ratio: {}'.format(pca.explained_variance_ratio_))
# -> explained variance ratio: [0.44272 0.18971 0.09393 0.06602 0.05496 0.04025 0.02251 0.01589 0.0139
# ->  0.01169 0.0098  0.00871 0.00805 0.00523 0.00314 0.00266 0.00198 0.00175
# ->  0.00165 0.00104 0.001   0.00091 0.00081 0.0006  0.00052 0.00027 0.00023
# ->  0.00005 0.00002 0.     ]

寄与率は、第1主成分で44%、第2主成分で19%、・・・といった値になっており、第4主成分までの累積で80%を超えることがわかります。 主成分分析を次元圧縮に用いる場合、累積寄与率80%の主成分のみを使い、以降の主成分は切り捨てられることが多いようです。

2次元まで圧縮してプロット

第2主成分まで次元圧縮(30次元→2次元)して、主成分を軸として散布図にプロットしてみます。

# データの最初の2つの主成分だけを維持
pca = PCA(n_components=2)
pca.fit(X_scaled)

# 最初の2つの主成分に対して、データポイントを変換
X_pca = pca.transform(X_scaled)
print('Original shape: {}'.format(str(X_scaled.shape)))
print('Reduced shape: {}'.format(str(X_pca.shape)))
# -> Original shape: (569, 30)
# -> Reduced shape: (569, 2)

確かに、2次元まで圧縮されていることがわかります。

プロットしてみます。

plt.figure(figsize=(10,10))
mglearn.discrete_scatter(X_pca[:,0], X_pca[:, 1], cancer.target)
plt.legend(cancer.target_names, loc='best')
plt.gca().set_aspect('equal')
plt.xlabel('First principal component')
plt.ylabel('Second principal component')

f:id:taxa_program:20190208000101p:plain:w450
次元圧縮後のcancerデータをプロット

上記からも分かるように、PCAは適切な回転を発見する際にはまったくクラス情報(今回で言うとtargetの悪性/良性)を用いていません。つまり、PCAは教師なし手法であると言えます。

上記の散布図では、2つのクラスが綺麗に分離できていることが見て取れると思います。これならば、線形クラス分類でもそれなりに分類できそうです。

PCAの欠点

PCAの欠点として、プロットした2つの軸の解釈が難しいことが挙げられます。2つの主成分の方向は元のデータの方向に対応しており、元の特徴量の組み合わせに過ぎませんが、これは一般的に複雑になると言われています。

下記のようにcomponents_を表示することで、主成分を確認することができます。

pca_components_df = pd.DataFrame(data=pca.components_,columns=cancer.feature_names)
pca_components_df.head()

f:id:taxa_program:20190208001117p:plain
PCA変換後の特徴量

主成分分析後のデータでロジスティック回帰

最後に、この第2主成分まで次元圧縮した説明変数を使って学習・テストさせてみます。標準化・次元圧縮・学習と処理が増えたので、Pipelineでまとめて処理を実装しています。

# パイプラインの作成
pca_pipeline = Pipeline([
    ('scale', StandardScaler()),
    ('decomposition', PCA(n_components=2)),
    ('model', LogisticRegressionCV(cv=10, random_state=0))
])

# 標準化・次元圧縮・学習
pca_pipeline.fit(X_train, y_train)

# 検証
print('Train score: {:.3f}'.format(pca_pipeline.score(X_train, y_train)))
print('Test score: {:.3f}'.format(pca_pipeline.score(X_test, y_test)))
print('Confustion matrix:\n{}'.format(confusion_matrix(y_true=y_test, y_pred=pca_pipeline.predict(X_test))))
# -> Train score: 0.965
# -> Test score: 0.947
# -> Confustion matrix:
# -> [[ 61   2]
# ->  [  7 101]]

テストデータの精度で95%となり、全ての説明変数を使った場合と比較して約3ポイント落ちています。しかし、30次元から2次元まで圧縮したことを考えれば、まずまずの精度が得られているのではないでしょうか。

今回は平面にプロットするため、次元数を2まで圧縮しましたが、上記でも述べている通り、主成分分析では累積寄与率80%の主成分のみを使い以降の主成分は切り捨てられることが多いようなので、今回のデータセットの場合はもう少し次元数を増やしても良いかもしれません。

いずれにせよ、PCAを用いて次元圧縮することで、(あまり重要ではない)特徴量を減らすことができ、可視化も行えることが分かりました。

機械学習においてはデータの次元数が大きくなり過ぎると、「次元の呪い」と呼ばれる要因によって効率的に機械学習をさせるのが難しくなります。(計算コスト、過学習、etc)

そういった場合にPCAなどはとても有効となり得そうです。