教師なし学習は、2~3次元に圧縮して可視化できるようにするとか、(どう解釈すればよいのかよくわからない)クラスターをつくる、とかくらいしか頭に入っておらず、もっときちんと使いこなせるようにしたいと思って「Pythonではじめる教師なし学習」という本で勉強をはじめました。
パラパラ見ていて、PCAで異常検知する例が面白いなと思いました。たしかに画像の異常検知でオートエンコーダー使うようなのは、教師なし学習で異常検知をしていたわけで、それも1つの教師なし学習の使いみちでした。内容を咀嚼してこちらにメモを残しておこうと思います。
・PCAによる異常検知の考え方
・最適な n_components の探索
・テストデータに対するパフォーマンス評価
・異常スコアの閾値の決定
PCA に依る異常検知の考え方
PCAやオートエンコーダーのような教師なし学習を用いて異常検知をする場合の考え方は下記の通り
1. 次元削減をして、データを圧縮する(データの本質を取り出す)
2. 圧縮したデータから復元する
3. 圧縮前と復元後のデータを比較し、上手く再現できなかったものが異常であるとみなす
次元削減をした場合、データの本質的な情報は残り、本質的でない情報は失われる。復元をして、元に戻らなかったということは、そのサンプルにはデータセット全体を見た時の本質となる情報があまり含まれていなかった=異常データであるとみなす。
実際にやってみる
今回も、Kaggle のクレジットカードの不正検知のデータセットを使わせてもらいます。
ttps://www.kaggle.com/mlg-ulb/creditcardfraud
欠損値がなく、カテゴリカル変数もないきれいなデータで扱いやすいです。
前処理で標準化を施しつつ、テストデータと訓練データに分けるところまで。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import pandas as pd import numpy as np import matplotlib.pyplot as plt %matplotlib inline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split from sklearn.metrics import precision_recall_curve, average_precision_score plt.style.use('ggplot') font = {'family' : 'meiryo'} matplotlib.rc('font', **font) data = pd.read_csv("creditcard.csv") data.shape ''' (284807, 31) ''' X = data.drop(["Class"], axis=1).values t = data["Class"].values scaler = StandardScaler() X_scaled = scaler.fit_transform(X) x_train, x_test, t_train, t_test = train_test_split(X_scaled, t, test_size=0.3, random_state=1, stratify=t) |
サンプルごとに、異常の程度を定量評価したいのですが、その基準には、圧縮する前と復元した後の二乗誤差和を使います。
1 2 3 4 |
def anomalyScores(original, reduced): loss= np.sum((original - reduced) ** 2, axis=1 ) loss = (loss - np.min(loss)) / (np.max(loss) - np.min(loss)) #min-max scaling return loss |
では30の特徴量がある状態から、27に削減して、また復元を行います。
1 2 3 4 5 6 7 |
from sklearn.decomposition import PCA pca = PCA(n_components=27, whiten=False, random_state=1) x_train_pca = pca.fit_transform(x_train) # 再構成 x_train_pca_inverse = pca.inverse_transform(x_train_pca) |
復元は、pca.inverse_transform
で行えます。
上手く検知できているかどうかは、Average Precision と PRCurve でいていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
preds = pd.DataFrame([t_train, anomalyScore]).T preds.columns = ['trueLabel', 'anomalyScore'] precision, recall, thresholds = precision_recall_curve(preds['trueLabel'],preds['anomalyScore']) average_precision = average_precision_score(preds['trueLabel'],preds['anomalyScore']) plt.step(recall, precision, alpha=0.7, where='post') plt.fill_between(recall, precision, step='post', alpha=0.3) plt.xlabel('Recall') plt.ylabel('Precision') plt.ylim([0.0, 1.05]) plt.xlim([0.0, 1.0]) plt.title('Precision-Recall curve: Average Precision = {0:0.2f}'.format(average_precision)) |
7割ちょっとのRecallで7割ちょっとのPrecisionを得られるモデルができました。
今回 n_components = 27 としましたが、他の値でもっとよいパフォーマンスが出るか確認します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
for i in range(30): pca = PCA(n_components=i, whiten=False, random_state=1) x_train_pca = pca.fit_transform(x_train) x_train_pca_inverse = pca.inverse_transform(x_train_pca) anomalyScore = anomalyScores(x_train, x_train_pca_inverse) average_precision = average_precision_score(t_train, anomalyScore) print(f"n_component: {i} , Average Precision: {average_precision}") ''' n_component: 0 , Average Precision: 0.13455983988598216 n_component: 1 , Average Precision: 0.15675315218676336 n_component: 2 , Average Precision: 0.1567424478620602 n_component: 3 , Average Precision: 0.1673404762449729 n_component: 4 , Average Precision: 0.1745105594777486 n_component: 5 , Average Precision: 0.18133001694910586 n_component: 6 , Average Precision: 0.183822501139346 n_component: 7 , Average Precision: 0.19780074997630095 n_component: 8 , Average Precision: 0.20229177033715284 n_component: 9 , Average Precision: 0.1998937891733822 n_component: 10 , Average Precision: 0.2032953711739749 n_component: 11 , Average Precision: 0.19107792588697858 n_component: 12 , Average Precision: 0.19107440626605932 n_component: 13 , Average Precision: 0.18918831502567005 n_component: 14 , Average Precision: 0.20044379532950998 n_component: 15 , Average Precision: 0.20184319505830484 n_component: 16 , Average Precision: 0.21881101912014708 n_component: 17 , Average Precision: 0.22145790234039803 n_component: 18 , Average Precision: 0.2194790023662497 n_component: 19 , Average Precision: 0.2202160768571342 n_component: 20 , Average Precision: 0.22401107628019248 n_component: 21 , Average Precision: 0.22510819650846475 n_component: 22 , Average Precision: 0.2268312192477547 n_component: 23 , Average Precision: 0.25410594949656634 n_component: 24 , Average Precision: 0.2774608442170086 n_component: 25 , Average Precision: 0.355566588978797 n_component: 26 , Average Precision: 0.436359119568168 n_component: 27 , Average Precision: 0.6532268903103455 n_component: 28 , Average Precision: 0.0027231974915662154 n_component: 29 , Average Precision: 0.009225387749600756 ''' |
27が最適だったようです。
テストデータに対してのパフォーマンスもみてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# n_components=27 でモデル構築 pca = PCA(n_components=27, whiten=False, random_state=1) pca.fit(x_train) # テストデータに対して適用 x_test_pca = pca.transform(x_test) x_test_pca_inverse = pca.inverse_transform(x_test_pca) # 異常スコアを算出 anomalyScore = anomalyScores(x_test, x_test_pca_inverse) preds = pd.DataFrame([t_test, anomalyScore]).T preds.columns = ['trueLabel', 'anomalyScore'] # PR曲線の描画 precision, recall, thresholds = precision_recall_curve(preds['trueLabel'],preds['anomalyScore']) average_precision = average_precision_score(preds['trueLabel'],preds['anomalyScore']) plt.step(recall, precision, alpha=0.7, where='post') plt.fill_between(recall, precision, step='post', alpha=0.3) plt.xlabel('Recall') plt.ylabel('Precision') plt.ylim([0.0, 1.05]) plt.xlim([0.0, 1.0]) plt.title('Precision-Recall curve: Average Precision = {0:0.2f}'.format(average_precision)) |
ばっちりです。最後に異常スコアのどの値を閾値にするか決定します。
1 2 |
pr_data = pd.DataFrame([precision, recall, thresholds], index=["precision", "recall", "thresholds"]).T pr_data[pr_data["recall"] >= 0.8] |
例えばrecallが0.8以上にしたい、という場合には、anomalyScore の閾値は0.0327とすれば良いことがわかります。
というわけで、PCAによる異常検知の整理でした。
最後まで読んでいただきありがとうございました。
参考
Pythonではじめる教師なし学習