前回の記事でfashion mnistデータセットのサンダルを正常値、ブーツを異常値として、簡単な異常検知をしました。結果としてはまったくうまくいきませんでした。
今回は、単純に画像をOneClassSVMを使って1クラス分類するのではなく、画像をオートエンコーダーを使って二次元空間にマッピングをし、マッピングした結果を使って1クラス分類をしてみようと思います。
ソースコードはgithubにあげてあります
https://github.com/tocom242242/anomaly_detection/blob/main/fashion_mnist/simple_ae.ipynb
データセットと正常値・異常値
前回と同様ですが、Fashion MNISTを使います。
Fashin MNISTはMNISTっていう数字のデータセットを靴とか衣服とかにしたデータセットです。
詳細はこの記事は参考にしてください。
今回はこのFashin MNISTのサンダルを正常値、靴を異常値として扱います。
ちなみにサンダルと靴は以下のような感じです。
実装していく
今回は、単純にOneClassSVMを使って、異常検知をしていきます。
必要なモジュールのインポート
まずは必要なモジュール群をimportしていきます。
from sklearn.metrics import roc_auc_score
import matplotlib.pyplot as plt
import numpy as np
from tensorflow.keras import datasets, layers, models
from sklearn.svm import OneClassSVM
from sklearn.metrics import roc_curve
学習・評価用データの作成
前回と同じですが、
正常データ(学習用)、正常・異常データ(評価用)のデータセットを作成します。
まずは、fashion mnistデータセットを取得します。
(x_train, y_train), (x_test, y_test) = datasets.fashion_mnist.load_data()
画像データは正規化しておきます。
x_train = x_train / 255
x_test = x_test / 255
正常データはサンダル、異常データはブーツとし、データを作成します。
normal_idx = 5
abnormal_idx = 7
x_normal_train = x_train[np.where(y_train==normal_idx,True,False)]
x_normal_test = x_test[np.where(y_test==normal_idx,True,False)]
x_abnormal_train = x_train[np.where(y_train==abnormal_idx,True,False)]
x_abnormal_test = x_test[np.where(y_test==abnormal_idx,True,False)]
評価用の入力データを作成します。
x_test = np.concatenate((x_normal_test,x_abnormal_test))
今回は、各ピクセルの値を1次元にして使うので、以下のようにreshapeします。
x_test = x_test.reshape(x_test.shape[0], 28*28)
x_normal_train = x_normal_train.reshape(x_normal_train.shape[0], 28*28)
評価用にラベルデータを作成します。
# normalが1,異常が0
y_test = np.concatenate((np.ones(x_normal_test.shape[0]),np.zeros(x_abnormal_test.shape[0])))
ここまでは前回と同様です。
オートエンコーダーによる特徴抽出
今回は前回と異なり画像を単純に28✖28 = 784次元のベクトルとして分類するのではなく、784次元のベクトルをオートエンコーダーを使って2次元空間にマッピングした後にワンクラス分類をしてみようと思います。
オートエンコーダーの構築
オートエンコーダーを構築します。tensorflow 2.0を使うと以下のように簡単に実装できます。
import tensorflow as tf
from tensorflow.keras.datasets import fashion_mnist
from tensorflow.keras.models import Model
latent_dim = 2
class Autoencoder(Model):
def __init__(self, latent_dim):
super(Autoencoder, self).__init__()
self.latent_dim = latent_dim
self.encoder = tf.keras.Sequential([
layers.Dense(128, activation='relu'),
layers.Dense(64, activation='relu'),
layers.Dense(32, activation='relu'),
layers.Dense(latent_dim, activation='relu'),
])
self.decoder = tf.keras.Sequential([
layers.Dense(32, activation='relu'),
layers.Dense(64, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dense(784, activation='sigmoid'),
])
def call(self, x):
encoded = self.encoder(x)
decoded = self.decoder(encoded)
return decoded
autoencoder = Autoencoder(latent_dim)
autoencoder.compile(optimizer='adam', loss=losses.MeanSquaredError())
history = autoencoder.fit(x_normal_train, x_normal_train,
epochs=50,
shuffle=True)
本来はvalidationと分けるべきですが、ちょっと面倒くさかったのでわけてません(雑ですいません)
上記のコードを実行すると正常データだけでオートエンコーダーを学習します。
one class svmの学習
画像データを二次元に次元圧縮を行うエンコーダーができたので、
このエンコーダーを使って学習データを次元圧縮し、one class svmを学習します。
svm = OneClassSVM()
x_normal_train = autoencoder.encoder(x_normal_train).numpy()
svm.fit(x_normal_train)
x_test = autoencoder.encoder(x_test).numpy()
評価
では、評価していきます。
前回と同じようにROCカーブとAUCを見ていきます。
(baselineは前回のコードの結果になります。今回は計算してません。前回のと組み合わせて使ってください)
y_score = svm.decision_function(x_test)
auc_ae = roc_auc_score(y_test,y_score)
fpr_ae, tpr_ae, thresholds = roc_curve(y_test, y_score)
plt.plot(fpr_baseline, tpr_baseline, label='baseline(AUC = %.2f)'%auc_baseline)
plt.plot(fpr_ae, tpr_ae, label='Auto Encoder(AUC = %.2f)'%auc_ae)
plt.plot([0,1],[0,1],'k--')
plt.legend()
plt.title('ROC curve')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.grid(True)
plt.show()
前回より良い結果が出てますね。
この数値はオートエンコーダーの学習結果によって毎回変わると思いますが、次元圧縮しない時に比べてはちょっとは良くなると思います。
(おまけ)テストデータを二次元空間にマッピングしてみた結果をグラフ化
テストデータを二次元空間にマッピングしてみた結果を散布図としてプロットしてみます。
まず、正解データを見てみます。正常が1(黄色)、異常が0(紺色?)になります。
で、予想したデータ。
まぁあまり綺麗には分類出来てないですし、そもそも綺麗に分離できそうなデータになってないのでしょうがないです。
終わりに
今回は、画像をオートエンコーダーを使って二次元空間にマッピングをし、マッピングした結果を使って1クラス分類をしてみました。
結果としては前回に比べて多少性能が改善することがわかりました。
しかしながら、マッピングした結果を見てみるとまだまだ、1クラス分類をするには難しい状況であることがわかりました。
次回はもう少しいろいろ改善していこうと思います。