ラビットチャレンジ - 機械学習 第4章 主成分分析

# 0．概要
本記事は日本ディープラーニング協会認定の講座プログラムである「ラビット・チャレンジ」が提供している科目の1つである機械学習のレポートである。
記事タイトルに記載のとおり、第4章の主成分分析について以下にまとめる。

# 1．主成分分析(PCA)
多変量のデータの持つ構造をより少数個の指標に圧縮すること(次元圧縮)。
　→情報の損失を抑えつつ高次元データの次元数を減らして圧縮する。

2次元や3次元に圧縮すればグラフで可視化ができる、計算時間の削減につながる。

数式は以下のようになる。

学習データ

```math
x_{i}=(x_{i1},x_{i2},...,x_{im})\in\mathbb{R}^{m}
```

$i$：説明変数のインデックス
$m$：データの次元数

平均(ベクトル)

```math
\bar{x}=\frac{1}{n}\sum_{i=1}^{n}{x_{i}}
```

$n$：データ数

データ行列

```math
\bar{X}=(x_{1}-\bar{x},...,x_{n}-\bar{x})^T
```

分散共分散行列

```math
\Sigma=Var(\bar{X})=\frac{1}{n}\bar{X}^{T}\bar{X}
```

線形変換後のベクトル

```math
s_{j}=(s_{1j,...,s_{nj}})^T=\bar{X}a_{j} ~~~~~ a_{j}\mathbb{R}^{m}
```

$j$：射影軸のインデックス
$a$：係数ベクトル

情報の量を分散の大きさと捉え、変換した後のデータの分散が最大となる射影軸を探索する。
分散が大きい方が元のデータの情報を持っていると考えられるため。

分散共分散行列を使ったやり方以外にも相関行列を使った手法もある。

また、深層学習における次元圧縮の手法としてオートエンコーダがある。状況に合わせて使い分ける必要がある。

## 1.1．ラグランジュ関数

目的関数

```math
arg~max~a_{j}^{T}Var(\bar{X})a_{j}
```

制約条件(制約条件がないと無限に解が出るため)

```math
a_{j}^{T}a_{j}=1
```

ラグランジュ関数を最大にする係数ベクトル(微分して0になる点)を探す。

```math
E(a_{j})=a_{j}^{T}Var(\bar{X})a_{j}-\lambda(a_{j}^{T}a_{j}-1)
```

```math
\frac{\partial{E(a_{j})}}{\partial{a_{j}}}=2Var(\bar{X})a_{j}-2\lambda{a_{j}}=0
```

```math
Var(\bar{X})a_{j}=\lambda{a_{j}}
```

元のデータの分散共分散行列の固有値と固有ベクトルと同じとなる。
$k$番目の固有値(昇順)を並べ、対応する固有ベクトルを第$k$主成分と呼ぶ。

## 1.2．寄与率
第$k$主成分の分散の全分散に対する割合(第$k$主成分が持つ情報量の割合)のこと。

```math
c_{k}=\frac{\lambda_{k}}{\sum_{i=1}^{m}{\lambda_{i}}}
```

$\lambda_{k}$：第$k$主成分の分散
$\sum_{i=1}^{m}{\lambda_{i}}$：主成分の総分散

ある次元1つだけ使うということはないため基本的に考えるべきなのは累積寄与率である。

## 1.3．累積寄与率
第$1-k$主成分まで圧縮した際の情報損失量の割合。

```math
r_{k}=\frac{\sum_{j=1}^{k}{\lambda_{i}}}{\sum_{i=1}^{m}{\lambda_{i}}}
```

$\sum_{j=1}^{k}{\lambda_{i}}$：第$1-k$主成分の分散

寄与率が高い次元のみを使用し、低い次元を切り捨てることによって、できるだけ情報を保持しつつ次元圧縮ができる。

# 2．ハンズオン

## 2.1．使用するデータ
乳がん検査データを使用する。
https://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+%28Diagnostic%29

## 2.2．課題
32次元のデータを2次元に次元圧縮した際、ロジスティック回帰モデルでうまく判別できるか。

## 2.3．演習

### 2.3.1．ライブラリのインポート

```Python:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegressionCV
from sklearn.metrics import confusion_matrix
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
%matplotlib inline
```

Google Driveのマウントもあるがこれまでにもやっているので割愛。

### 2.3.2．データ確認

```Python:
cancer_df = pd.read_csv('/content/drive/My Drive/study_ai_ml/data/cancer.csv')

# 形状を表示
print('cancer df shape: {}'.format(cancer_df.shape))

# 上位5件表示
cancer_df.head(5)
```

> cancer df shape: (569, 33)

形状を見ると569レコード、33カラム(次元)あることが分かる。

||id|diagnosis|radius_mean|texture_mean|perimeter_mean|area_mean|smoothness_mean|compactness_mean|concavity_mean|concave points_mean|symmetry_mean|fractal_dimension_mean|radius_se|texture_se|perimeter_se|area_se|smoothness_se|compactness_se|concavity_se|concave points_se|symmetry_se|fractal_dimension_se|radius_worst|texture_worst|perimeter_worst|area_worst|smoothness_worst|compactness_worst|concavity_worst|concave points_worst|symmetry_worst|fractal_dimension_worst|Unnamed: 32|
|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|
|0|842302|M|17.99|10.38|122.80|1001.0|0.11840|0.27760|0.3001|0.14710|0.2419|0.07871|1.0950|0.9053|8.589|153.40|0.006399|0.04904|0.05373|0.01587|0.03003|0.006193|25.38|17.33|184.60|2019.0|0.1622|0.6656|0.7119|0.2654|0.4601|0.11890|NaN|
|1|842517|M|20.57|17.77|132.90|1326.0|0.08474|0.07864|0.0869|0.07017|0.1812|0.05667|0.5435|0.7339|3.398|74.08|0.005225|0.01308|0.01860|0.01340|0.01389|0.003532|24.99|23.41|158.80|1956.0|0.1238|0.1866|0.2416|0.1860|0.2750|0.08902|NaN|
|2|84300903|M|19.69|21.25|130.00|1203.0|0.10960|0.15990|0.1974|0.12790|0.2069|0.05999|0.7456|0.7869|4.585|94.03|0.006150|0.04006|0.03832|0.02058|0.02250|0.004571|23.57|25.53|152.50|1709.0|0.1444|0.4245|0.4504|0.2430|0.3613|0.08758|NaN|
|3|84348301|M|11.42|20.38|77.58|386.1|0.14250|0.28390|0.2414|0.10520|0.2597|0.09744|0.4956|1.1560|3.445|27.23|0.009110|0.07458|0.05661|0.01867|0.05963|0.009208|14.91|26.50|98.87|567.7|0.2098|0.8663|0.6869|0.2575|0.6638|0.17300|NaN|
|4|84358402|M|20.29|14.34|135.10|1297.0|0.10030|0.13280|0.1980|0.10430|0.1809|0.05883|0.7572|0.7813|5.438|94.44|0.011490|0.02461|0.05688|0.01885|0.01756|0.005115|22.54|16.67|152.20|1575.0|0.1374|0.2050|0.4000|0.1625|0.2364|0.07678|NaN|

なぜあるのかは分からないが名無しのカラム「Unnamed: 32」が不要なカラムとして存在しており、課題で示した32次元ではなく33次元となっている。

```Python:
cancer_df['Unnamed: 32'].isnull().all()
```

> True

削除する前に念のためデータを確認すると全てNaNであることが分かったため安心して削除する。

```Python:
cancer_df.drop('Unnamed: 32', axis=1, inplace=True)
print('cancer df shape: {}'.format(cancer_df.shape))
```

> cancer df shape: (569, 32)

inplace=Trueなのでcancer_df自体が変更され、32次元になっていることが確認できる。

### 2.3.3．次元圧縮せずにロジスティック回帰

```Python:
def confusion_matrix_flatten(cm):
  tn, fp, fn, tp = cm.flatten()

  print('TP:', tp)
  print('FP:', fp)
  print('TN:', tn)
  print('FN:', fn)
  print()

# 目的変数の抽出
y = cancer_df.diagnosis.apply(lambda d: 1 if d == 'M' else 0)

# 説明変数の抽出
X = cancer_df.loc[:, 'radius_mean':]

# 学習用とテスト用でデータを分離
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

# 標準化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# ロジスティック回帰で学習
logistic = LogisticRegressionCV(cv=10, random_state=0)
logistic.fit(X_train_scaled, y_train)

# 検証
print('Train score: {:.3f}'.format(logistic.score(X_train_scaled, y_train)))
print('Test score: {:.3f}'.format(logistic.score(X_test_scaled, y_test)))
cm = confusion_matrix(y_true=y_test, y_pred=logistic.predict(X_test_scaled))
print('Confustion matrix:\n{}'.format(cm))
confusion_matrix_flatten(cm)
```

> Train score: 0.988
> Test score: 0.972
> Confustion matrix:
> [[89  1]
>  [ 3 50]]
> TP: 50
> FP: 1
> TN: 89
> FN: 3

目的変数はdiagnosis(診断結果)。
文字列となっているため、良性(B)を0、悪性(M)を1に置き換えて使用する。

説明変数はradius_mean(腫瘍の半径平均)以降のカラムを使用する。
つまり、ID, diagnosisは使わないため、これで次元数が32-2=30次元となる。

また、StandardScalerを使って学習データとテストデータに対して標準化を行っている(小数のような非常に小さいデータもあれば2000等の大きな値もあるためスケールを合わせる)。
学習データに対してはfit_transformなのにテストデータに対してはtransformを使用しているが、これは以下のような役割の違いによるもの。

* fit：標準化に必要な統計量の計算をする。
* transform：fitで計算した統計量を元に標準化する。
* fit_transform：fitとtransformを行う。

学習データに対してもテストデータに対しても同じように標準化をする必要があるため、学習データに対しては統計量を計算するfitを含んだfit_transformを呼び出すが、テストデータに対しては学習データで計算した統計量を使って同じように標準化するようfitは使わずtransformのみを使用している。

テストデータに対する決定係数が0.972と非常に高く、混同行列を見ても間違えて予測している数が少ないことが分かる。

#### 2.3.3.1．標準化しなかった場合

```Python:
# 学習用とテスト用でデータを分離
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

# ロジスティック回帰で学習
logistic = LogisticRegressionCV(cv=10, random_state=0)
logistic.fit(X_train_scaled, y_train)

# 検証
print('Train score: {:.3f}'.format(logistic.score(X_train, y_train)))
print('Test score: {:.3f}'.format(logistic.score(X_test, y_test)))

cm = confusion_matrix(y_true=y_test, y_pred=logistic.predict(X_test))
print('Confustion matrix:\n{}'.format(cm))
confusion_matrix_flatten(cm)
```

> Train score: 0.373
> Test score: 0.371
> Confustion matrix:
> [[ 0 90]
>  [ 0 53]]
> TP: 53
> FP: 90
> TN: 0
> FN: 0

標準化をやめると一気に決定係数の値が落ちているため、標準化することで精度向上できていた言える。

### 2.3.4．主成分の可視化

```Python:
pca = PCA(n_components=30)
pca.fit(X_train_scaled)
plt.bar([n for n in range(1, len(pca.explained_variance_ratio_) + 1)], pca.explained_variance_ratio_)
```

![pca.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389699/3f5e4c6c-eb7d-74c7-7a14-a948dd8fc3af.png)

各成分の寄与率(explained_variance_ratio_)を可視化すると上記のようになる。

第1成分だけでも半分弱の寄与率があり、第2成分も0.2ほどあるためこの2次元だけでも0.6強程度の累積寄与率になる。
第3成分も0.1あるので可視化のための次元圧縮するにしても3次元への圧縮でも良いと思われる(累積寄与率0.7強)。
可視化目的でないなら第5成分まで含めれば累積寄与率が0.9弱くらいになるので良いかもしれない。

### 2.3.5．2次元に次元圧縮

```Python:
pca = PCA(n_components=2)
X_train_pca = pca.fit_transform(X_train_scaled)
print('X_train_pca shape: {}'.format(X_train_pca.shape))

# 寄与率
print('explained variance ratio: {}'.format(pca.explained_variance_ratio_))
print('cumulative contribution ratio: {}'.format(pca.explained_variance_ratio_.sum()))

# 散布図にプロット
temp = pd.DataFrame(X_train_pca)
temp['Outcome'] = y_train.values
b = temp[temp['Outcome'] == 0]
m = temp[temp['Outcome'] == 1]
plt.scatter(x=b[0], y=b[1], marker='o') # 良性は○でマーク
plt.scatter(x=m[0], y=m[1], marker='^') # 悪性は△でマーク
plt.xlabel('PC 1') # 第1主成分をx軸
plt.ylabel('PC 2') # 第2主成分をy軸
```

> X_train_pca shape: (426, 2)
> explained variance ratio: [0.43315126 0.19586506]
> cumulative contribution ratio: 0.6290163245532953

![pca2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389699/cfc3f26c-a4af-560f-716d-5833fd545190.png)

形状を確認すると2次元になっていることが確認できる。
また、第1成分の寄与率が0.43、第1成分の寄与率が0.19で累積寄与率が約0.63となっている。

可視化すると次元圧縮しても大半のデータはきっちり分かれていることが分かる。
ただ、やはり良性と悪性の境界あたりのデータは曖昧となっている。

### 2.3.6．3次元に次元圧縮

```Python:
pca2 = PCA(n_components=3)
X_train_pca2 = pca2.fit_transform(X_train_scaled)
print('X_train_pca2 shape: {}'.format(X_train_pca2.shape))

print('explained variance ratio: {}'.format(pca2.explained_variance_ratio_))
print('cumulative contribution ratio: {}'.format(pca2.explained_variance_ratio_.sum()))

from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = Axes3D(fig)

temp = pd.DataFrame(X_train_pca2)
temp['Outcome'] = y_train.values
b = temp[temp['Outcome'] == 0]
m = temp[temp['Outcome'] == 1]
ax.scatter(xs=b[0], ys=b[1], marker='o') # 良性は○でマーク
ax.scatter(xs=m[0], ys=m[1], marker='^') # 悪性は△でマーク
ax.set_xlabel('PC 1')
ax.set_ylabel('PC 2')
ax.set_zlabel('PC 3')
```

> X_train_pca2 shape: (426, 3)
> explained variance ratio: [0.43315126 0.19586506 0.09570611]
> cumulative contribution ratio: 0.7247224331320569

![pca3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389699/58b9f690-d336-7046-427a-3ea86f2cb8fd.png)

3次元とすると累積寄与率が0.72となっている。

3次元散布図で可視化してみたもののいまいち見にくいので少なくともこのデータは2次元の可視化の方が良いかもしれない(寄与率も0.1の差なので大きな差ではないと言えると思う)。

### 2.3.7．次元圧縮したデータでロジスティック回帰

```Python:
# 2次元データで学習
X_test_pca = pca.fit_transform(X_test_scaled)
print('X_train_pca shape: {}'.format(X_train_pca.shape))
print('X_test_pca shape: {}'.format(X_test_pca.shape))

logistic1 = LogisticRegressionCV(cv=10, random_state=0)
logistic1.fit(X_train_pca, y_train)

# 検証
print('Train score: {:.3f}'.format(logistic1.score(X_train_pca, y_train)))
print('Test score: {:.3f}'.format(logistic1.score(X_test_pca, y_test)))

cm = confusion_matrix(y_true=y_test, y_pred=logistic1.predict(X_test_pca))
print('Confustion matrix:\n{}'.format(cm))
confusion_matrix_flatten(cm)
```

> X_train_pca shape: (426, 2)
> X_test_pca shape: (143, 2)
> Train score: 0.965
> Test score: 0.916
> Confustion matrix:
> [[83  7]
>  [ 5 48]]
> TP: 48
> FP: 7
> TN: 83
> FN: 5

```Python:
# 3次元データで学習
X_test_pca2 = pca2.fit_transform(X_test_scaled)
print('X_train_pca shape: {}'.format(X_train_pca2.shape))
print('X_test_pca shape: {}'.format(X_test_pca2.shape))

logistic2 = LogisticRegressionCV(cv=10, random_state=0)
logistic2.fit(X_train_pca2, y_train)

# 検証
print('Train score: {:.3f}'.format(logistic2.score(X_train_pca2, y_train)))
print('Test score: {:.3f}'.format(logistic2.score(X_test_pca2, y_test)))

cm = confusion_matrix(y_true=y_test, y_pred=logistic2.predict(X_test_pca2))
print('Confustion matrix:\n{}'.format(cm))
confusion_matrix_flatten(cm)
```

> X_train_pca2 shape: (426, 3)
> X_test_pca2 shape: (143, 3)
> Train score: 0.960
> Test score: 0.916
> Confustion matrix:
> [[83  7]
>  [ 5 48]]
> TP: 48
> FP: 7
> TN: 83
> FN: 5

こうしてみると2次元でも3次元でも精度に変わりなく、2次元でも3次元でも30次元で予測した時と大差ない精度で十分な高さとなっている。
2次元との差は学習データに対する決定係数が若干3次元より2次元の方が良いくらい。
精度に変わりないなら2次元の方が次元数が少ない分、若干ではあるが計算効率も良くなるため良いと考えられる。

# X．ラビットチャレンジとは
ラビットチャレンジとは、日本ディープラーニング協会認定の講座プログラムの1つ。
E資格を受験するためにはこのラビットチャレンジ等、いずれかの講座プログラムを修了しなければならない。

ラビットチャレンジの特徴は「現場で潰しが効くディープラーニング講座」の通学講座録画ビデオを編集した教材を使用した自習スタイルであるという点。
サポートは他の講座より少なく、受け身ではなく自主的に学んでいく姿勢でなければ進められないが、その分、他の講座に比べると安価であり、手が出しやすい。
ある程度知識がある人、自力で頑張るぞというガッツのある人向けではないかと感じる。

![bnr_jdla.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389699/bce66671-a2f5-a7f9-de6f-3f87e15c253b.png)