ニューラルネットワークによる手書き文字画像の異常検知~CNNとCAEの比較~

機械学習を用いた画像データの異常検知は、様々な分野で用いられ始めています。例えば、工場現場であれば、流れてくる製品から自動的に不良品をはじくといったことにもできますし、医療現場であれば、画像診断にも応用できるでしょう。異常検知の課題は、そもそも発生が少ない異常を学習させることが難しいところにあります。

今回は、そんな画像異常検知をシンプルな手書き文字画像でやってみます。用いる手法は、画像分類の王道手法であるCNN(Convolutional Neural Network:畳み込ニューラルネットワーク)と、画像のAuto encoderであるCAE(Convolutional Auto Encoder)です。

データの準備

まずはデータを読み込んでおきましょう。今回はkerasを使ってモデルを作成しますが、kerasにデフォルトで入っている画像分類の代名詞であるMINSTを使います。

# mnistのデータをkerasからimportする
from keras.datasets import mnist
import numpy as np
import pandas as pd
from collections import Counter
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

#データの読み込み
(X_train, y_train), (X_test,y_test) = mnist.load_data()
X_train = X_train.astype("float32")
X_test = X_test.astype("float32")

#データの概要確認
print(X_train.shape)
print(X_test.shape)
print(Counter(y_train))

>(60000, 28, 28)
>(10000, 28, 28)
>Counter({1: 6742, 7: 6265, 3: 6131, 2: 5958, 9: 5949, 0: 5923, 6: 5918, 8: 5851, 4: 5842, 5: 5421})

MINISTは、28×28ピクセルの手書き文字がtrainには60,000枚、テストには10,000枚入っています。また、各画像には0~9までの数字が一つ書かれていて、それぞれ6,000枚ずつぐらい入っています。試しに1枚表示させてみましょう。

plt.imshow(X_train[0])
plt.show()

MINIST

次に、CNNとCAEの学習用にデータを加工していきます。画像は1と書いてある画像を正常、それ以外を異常と仮定し、学習、テストデータともに異常画像を10枚にします。また、CNNでは教師ラベルが必要なため、1のときは1、それ以外の数字は0とラベルをつけておきます。

X_train = X_train/255.0
X_test = X_test/255.0

X_train = np.concatenate([X_train[y_train==1], X_train[y_train !=1][:10]])
X_train = np.expand_dims(X_train, axis=3)

y_train = np.concatenate([y_train[y_train==1], y_train[y_train !=1][:10]])
y_train = np.where(y_train==1, 1, 0)

X_test = np.concatenate([X_test[y_test==1], X_test[y_test !=1][:10]])
X_test = np.expand_dims(X_test, axis=3)
y_test = np.concatenate([y_test[y_test==1], y_test[y_test !=1][:10]])
y_test = np.where(y_test==1, 1, 0)

print(Counter(y_train))
print(Counter(y_test))
>Counter({1: 6742, 0: 10})
>Counter({1: 1135, 0: 10})

これで準備が整いました。

CNNによる異常検知

それでは早速CNNを使って画像の異常検知してみます。CNNは畳み込みニューラルネットともいい、ディープラーニング を画像分類に応用した手法です。

www.medi-08-data-06.work

本来、犬やネコといった画像分類タスクに優れており、画像の特徴を勝手に学習することで、画像を分類することができます。CNNの詳細には触れませんが、今回のモデルイメージとしては、こんな感じです。

ニューラルネット

次に、trainデータを評価用と学習用に分割し、教師ラベルを加工します。

from keras.utils import np_utils
X_train_cnn, X_valid_cnn, y_train_cnn, y_valid_cnn = train_test_split(X_train, y_train, test_size=0.3, random_state=0)

y_train_cnn = np_utils.to_categorical(y_train_cnn , 2)
y_valid_cnn = np_utils.to_categorical(y_valid_cnn , 2)
y_test_cnn = np_utils.to_categorical(y_test_cnn , 2)

最後にkerasを使って、モデルを作成します。今回は、中間層が3層のCNNを作成します。勾配アルゴリズムにはAdam、誤差評価にはcategorical_crossentropyを用いました。

from keras.models import Model, Sequential
from keras.layers import Conv2D, MaxPooling2D,Dropout,Flatten, Dense , Activation
from keras.optimizers import Adam
input_size = X_train.shape

#ニューラルネットワークの作成
model = Sequential()

#1層目
model.add(Conv2D(16, (3,3) , padding = 'same' , input_shape=(input_size[1], input_size[2], 1)))
model.add(Activation("relu"))
model.add(MaxPooling2D(2,2))

#2層目
model.add(Conv2D(8, (3,3),padding = 'same' ))
model.add(MaxPooling2D(2,2))
model.add(Activation("relu"))
model.add(Dropout(0.25))

#3層目
model.add(Flatten())
model.add(Dense(4))
model.add(Activation("relu"))

#出力
model.add(Dense(2))
model.add(Activation("softmax"))

model.compile(loss='categorical_crossentropy',
              optimizer=Adam()
             )
print(model.summary())

学習させます。

results = model.fit(X_train_cnn , y_train_cnn ,
                    batch_size = 32, 
                    epochs = 20 , 
                    shuffle=True,
                    validation_data=(X_valid_cnn , y_valid_cnn))

少し時間がかかりますが、学習が終わったあとは、trainとvalidationの学習曲線を確認してみます。

#学習曲線
loss     = results.history['loss']
val_loss = results.history['val_loss']

nb_epoch = len(loss)
plt.plot(range(nb_epoch), loss,     marker='.', label='loss')
plt.plot(range(nb_epoch), val_loss, marker='.', label='val_loss')
plt.legend(loc='best', fontsize=10)
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()

学習曲線

過学習することなくうまく学習できているように見えます。テストデータで予測して、混同行列で結果を確認してみましょう。

www.medi-08-data-06.work

from sklearn.metrics import confusion_matrix
pred_test = model.predict(X_test)
pred_test_bin=np.argmax(pred_test,axis=1)

print(pd.DataFrame(confusion_matrix(y_test, pred_test_bin), columns=['Predict_0', 'Predict_1'], index=['True_0', 'True_1']))
Predict_0 Predict_1
True_0 5 5
True_1 0 1135

テストデータの中で、1の画像、つまり正常データは正しく分類できているようです。一方で1以外の異常画像は10枚のうち5枚を、誤って正常と分類しています。このようにCNNなどの教師あり学習は、データが少ないラベルの分類が苦手です。

CAEによる異常検知

次にCAEを使ってみます。CAEはAuto Encoderの一種で、入力と出力を全く同じ形式に復元するように学習させます。つまり、正常データのみを学習させ、うまく復元できない物があれば、異常とみなす仕組みです。CAEの良いところは、学習に正常データのみを用いるため、そもそも異常データを準備する必要がないところにあります。

今回のCAEのモデルイメージはこんな感じです。

オートエンコーダー

それでは早速モデルを作っていきましょう。はじめに学習と評価データを1の画像のみで準備します。

X_train_cae, X_valid_cae, y_train_cae, y_valid_cae= train_test_split(X_train[y_train  == 1], y_train[y_train  == 1], test_size=0.2,random_state=0)

print(Counter(y_train_cae))
>Counter({1: 5393})

そして、先ほど同様にモデルを作成します。勾配アルゴリズムはAdam、誤差評価は、入力と出力のピクセルで評価するため、binary_crossentropyを使います。

from keras.models import Model, Sequential
from keras.layers import Conv2D, MaxPooling2D, UpSampling2D, Activation
from keras.optimizers import Adam
 
input_size = X_train_cae.shape

#ニューラルネットワークの作成
model = Sequential()

#1層目
model.add(Conv2D(16, (3,3) , padding = 'same' , input_shape=(input_size[1], input_size[2], 1)))
model.add(Activation("relu"))
model.add(MaxPooling2D(2,2))

#2層目
model.add(Conv2D(8, (3,3),padding = 'same' ))
model.add(MaxPooling2D(2,2))
model.add(Activation("relu"))

#3層目
model.add(UpSampling2D(size=(2,2)))
model.add(Conv2D(16 , (3,3),padding = 'same' ))

#出力
model.add(UpSampling2D(size=(2,2)))
model.add(Conv2D(1, kernel_size=(3, 3),padding='same'))
model.add(Activation("sigmoid"))

model.compile(loss='binary_crossentropy',
              optimizer=Adam()
             )

print(model.summary())

学習させて、学習曲線を確認してみます。

results = model.fit(X_train_cae, X_train_cae,
              epochs=20,
              batch_size=32,
              shuffle=True,
              validation_data=(X_valid_cae, X_valid_cae)
              )


loss     = results.history['loss']
val_loss = results.history['val_loss']

nb_epoch = len(loss)
plt.plot(range(nb_epoch), loss,     marker='.', label='loss')
plt.plot(range(nb_epoch), val_loss, marker='.', label='val_loss')
plt.legend(loc='best', fontsize=10)
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()

学習曲線

こちらもうまく学習できているように見えます。テストデータで評価してみましょう。異常の判断方法は様々ありますが、今回は学習データにおける各画像の入力と出力の誤差平方和を利用し、その平均値より誤差が大きい(つまり、復元がうまくできていない)画像を異常とみなします。

pred_train = model.predict(X_train_cae)
diff_reshape_train = ((X_train_cae - pred_train)**2).reshape(X_train_cae.shape[0], 28*28)
sumse_train = np.sum(diff_reshape_train, axis=1)
#学習データ(正常データ)におけるの誤差平方和の平均
mean_sumse1 = np.mean(sumse_train)
print(mean_sumse1)

pred_test = model.predict(X_test)
diff_reshape = ((X_test - pred_test)**2).reshape(X_test.shape[0], 28*28)
sumse = np.sum(diff_reshape, axis=1)
pred_test_bin = np.where(sumse > mean_sumse1, 0, 1)

print(pd.DataFrame(confusion_matrix(y_test, pred_test_bin), columns=['Predict_0', 'Predict_1'], index=['True_0', 'True_1']))
Predict_0 Predict_1
True_0 10 0
True_1 343 792

誤差の平均値を異常の域値としたため、正常データも半分弱は異常と分類さていますが、異常データは全て正しく分類できています。異常の域値を変えることで、用途にあわせた異常検知を行えます。このようにCAEは"俺か俺以外か"で判断できるため、学習時に異常データがない場合でも異常検知をすることができます。

復元前と後で画像を比較してみましょう。

オートエンコーダー

1の復元はうまくできているようですが、1以外の復元は、横棒や円などは途切れていたりして、不完全な復元になっているように見えます。

まとめ

今回は、白黒だったため分かりにくいですが、CAEは色も復元できるため、変色など異常検知に用いることもできます。また、入力に対して、ノイズを除いた出力の復元を学習させるとノイズ除去などにも使えます。Auto Encoderは奥が深いので、気になる方は是非とも調べてみてください。(調べます、、)

※本記事は筆者が個人的に学んだこと感じたことをまとめた記事になります。所属する組織の意見・見解とは無関係です。

参考

Building Autoencoders in Keras