コンテンツにスキップ

第7章 異常検知

1 異常検知とは?

ディープラーニングの仕組みを使って異常を検知することが出来ます。例えば、画像を分析し、いつもと違う状態であることを検知し、工場での不良品検出などに応用できます。

ここではオートエンコーダーという手法を使ってみます。これは、まず、画像をニューラルネットワークに設定し、そこからニューロン数を下げて行きます。そして、また、ニューロン数を上げていき、元の画像に戻します(再構築)。

例えば28x28の画像では784の要素がありますが、これを64個までいったん下げて、再び784個にまで戻します。

これを正常な画像のみで学習させます。これにより、正常な画像は元に戻すことが出来るように学習します。

しかし、そうでない画像は元に戻せません。これにより、どれぐらい元に戻せたかを調べて、元に戻せなかったものは異常と判断できます。

2 データの準備

今回はサンプルとしてMNISTの手書き数字の画像データを使います。今回の画像は28x28のサイズです。1を正常な画像、9を異常な画像と見なします。

deep_ijou.deep に記述します。

# MNISTのデータをダウンロード
from tensorflow.keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# 画素値を0~255から0~1に変換
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0

# 学習データとして「1(正常)」のデータを抽出
x_train = x_train[y_train == 1]

# テストデータとして「1(正常)」と「9(異常)」の画像を抽出
x_test = x_test[(y_test == 1) | (y_test == 9)]
y_test = y_test[(y_test == 1) | (y_test == 9)]

print('学習データ: ', x_train.shape)
print('テストデータ: ', x_test.shape)

3 モデルの構築

モデルを構築します。まず、28x28の画像をFlattenで784個の一次元配列にします。

これを半分の392個にし、64個まで落とします(エンコーダー)。そして、再び392個に増やし、784個に戻します(デコーダー)。最後に28x28の形に戻します。

import keras
from keras import layers

model = keras.models.Sequential()

# エンコーダー
model.add(layers.Flatten(input_shape=(28, 28)))
model.add(layers.Dense(392, activation='relu'))
model.add(layers.Dense(64, activation='relu'))

# デコーダー 
model.add(layers.Dense(392, activation='relu'))
model.add(layers.Dense(784, activation='sigmoid'))
model.add(layers.Reshape((28,28)))

model.compile(optimizer='adam', loss='mean_squared_error')
model.summary()

コンパイル時の今までとの違いは誤差を評価する関数が、mean_squared_errorになり、元の値と結果の値の差を2乗して平均したものを使用します。

4 学習とグラフ表示

次に学習を行います。入力データも結果データも同じ x_train です。これは元に戻すからです。検証データもx_testを入力し、x_testに戻します。エポックは念のため50回行います。

history = model.fit(x_train, x_train, epochs=50, 
        validation_data=(x_test, x_test))

正解率は表示されません。誤差だけです。学習会数語との誤差をグラフで表示してみましょう。

import matplotlib.pyplot as plt
import seaborn as sns
sns.set(font=["Meiryo"])

plt.title("誤差")
plt.xlabel("学習回数")
plt.plot(history.history["loss"])
plt.plot(history.history["val_loss"])
plt.legend(["訓練","テスト"])
plt.show()

5 異常を検知する

では、テストデータを実際に予測し、異常検知した画像のリストを取得します。

decoded_imgs  = model.predict(x_test)

どのような結果になったか、わかりやすいように視覚化してみます。元の画像と、再構築した画像、そして、その誤差を画像化したものを表示します。

titles = ["元画像", "再構築", "差分"]

import numpy as np
    import tensorflow as tf
    import matplotlib.pyplot as plt
    import seaborn as sns
    sns.set(font=["Meiryo"])

    fig, axes = plt.subplots(3, 5, figsize=(10, 7)) # 3×5の画像表示

    for i in range(5):
        # 差分画像
        diff_img = np.abs(tf.squeeze(x_test[i]) - tf.squeeze(decoded_imgs[i]))
        anomaly_score = np.sum(diff_img) # 異常度スコア

        # 3種類の画像を表示
        images = [
            tf.squeeze(x_test[i]), # 元画像
            tf.squeeze(decoded_imgs[i]), # 再構築画像
            diff_img # 差分画像
        ]

        # それぞれの画像に対してプロット
        for j, (img, title) in enumerate(zip(images, titles)):
            ax = axes[j, i]
            ax.imshow(img, cmap='gray')
            ax.set_xticks([])
            ax.set_yticks([])
            # 差分画像の場合は異常度スコアを、それ以外はタイトルを表示
            ax.set_title(anomaly_score if j == 2 else title)

    plt.tight_layout()
    plt.show()

差分画像は元の画像と再構築画像の差分を画像化したものです。差が無ければ0になり、黒になります。差があれば白くなります。ですので、その部分を合計した値 anomaly_score が異常度ということになります。

6 結果の分析

数(0か9)と異常度の分析をしてみましょう。それを行うために全てのテストデータの異常度を計算し、データフレームに格納します。

import pandas as pd
df = pd.DataFrame(columns=["number","anomaly"])

for i in range(len(x_test)):
    # 差分画像
    diff_img = np.abs(tf.squeeze(x_test[i]) - tf.squeeze(decoded_imgs[i]))
    anomaly_score = np.sum(diff_img) # 異常度スコア
    df.loc[i] = [y_test[i], anomaly_score]

df.head()

数ごとの異常度の平均を見てみます。やはり、大きな差があります。

df.groupby('number').mean()

では、いくつ以上を異常と判断すれば良いでしょうか。そのためには全体の分布を見る必要がありますので数毎のヒストグラムを表示してみます。

sns.histplot(data=df, x="anomaly", hue="number", alpha=0.5)
plt.xlabel("異常度")
plt.ylabel("数")
plt.show()

これにより、だいたい20以上が異常(9)であることが分かります。 以下のようにして以上のものだけを抽出できます。

df2 = df[df['anomaly'] >= 20]
df2

異常データは果たして本当に異常なのか、numberごとの数を出してみましょう。

df2["number"].value_counts()

7 画像の生成

オートエンコーダーのデコーダー部分は64個の数値から画像を生成しています。 このデコーダー部分だけを見れば、数値から画像を生成していると言えます。

そこで64個のランダムな数を与えることで手書きの数字の1っぽい画像を生成することが出来ます。

まずはデコーダー部分のレイヤーを取り出し、新たなModelを作ります。

from tensorflow.keras.models import Model

decoder_input = keras.Input(shape=(64,))

# 4番目のレイヤー(エンコーダーの出力)からデコーダー部分を抽出
x = model.layers[3](decoder_input)  # Dense(392, activation='relu')
x = model.layers[4](x)              # Dense(784, activation='sigmoid')
decoder_output = model.layers[5](x) # Reshape((28, 28))
decoder = Model(decoder_input, decoder_output)

作成したモデルにランダムな数を与え、推論させ画像を表示します。

# 乱数
random_vector = np.random.normal(size=(1, 64))
generated_image = decoder.predict(random_vector)

# 画像表示
plt.imshow(generated_image[0], cmap='gray')
plt.show()