確率モデルを適用した自己符号化器の実装と異常値検知への活用

# はじめに

　自己符号化器は、教師なし学習に属するニューラルネットワークのひとつで、本来のデータが持つ特徴をうまくとらえることを目標としています。
　自己符号化器そのものの解説や実装例については、既に多くの記事はあるものの、確率モデルを適用した「自己符号化器は見当たらなかったこともあり、その実装と異常値検知への応用例を報告したいと思います。
　なお、実験の前提となる環境は以下の通りです。

OS : Windows10
Python : 3.7.6
tensorflow : 1.15
tensorflow-probability : 0.8

# 自己符号化器モデルの考え方
　最初に簡単なケースとしてmnistを対象にします。mnistのグレースケールから始めて次にカラー画像へと拡張していきます。
　自己符号化器は、元データから特徴ベクトルをニューラルネットワークを介して抽出するエンコード部と抽出した特徴ベクトルからニューラルネットワークを介して元データに変換するデコード部をつなげた構造を持ちます。学習は、元データとデコード部の出力結果の比較からコストを計算し、このコストを最小になるようにニューラルネットワークのパラメータを更新することで実現します。通常、コスト関数は各画素を数値データとみなし、元の画素値とデコード結果の画素値から二乗誤差やL2正則化値などを用いて表現する実装が一般的なようです。本実装は確率的プログラミングを用いて画素決定の仕組みを確率モデルを使って表現する方向に改良しました。
　mnistの手書き数字の場合、各画素は0から255の整数値を取ります。そこで、本論の場合、ニューラルネットワークを通じて獲得した確率を前提とした二項分布にしたがって各画素は決定されると考えます。つまり、確率が大きくなると真値が得られる確率が大きくなるので、結果として画素値そのものを大きくなるというものです。
　画素値が決定される理屈を上記のようにすると、コストは尤度を用いることで計算できます。デコード部において画素に真値が設定される確率と試行回数255回を前提に観測された値が出現する確からしさ（尤度）を最大になるようにモデルパラメータを更新することで最終的なモデルパラメータを獲得しようというものです。

# 実装内容

 実装は以下のようにしました。

```python
tf.reset_default_graph()
    
INPUT = tf.placeholder(tf.float32,[None,784])

w1 = tf.Variable(tf.truncated_normal([784,100],stddev=0.1))
b1 = tf.Variable(tf.zeros([100]))
w2 = tf.Variable(tf.truncated_normal([100,784],stddev=0.1))
b2 = tf.Variable(tf.zeros([784]))

Encode = tf.nn.softsign(tf.matmul(INPUT,w1) + b1)
Decode = tf.matmul(Encode,w2) + b2
p1 = tf.nn.sigmoid(Decode)

binom_func = tfp.distributions.Binomial(255,probs=p1)
log_prob = binom_func.log_prob(INPUT)

cost = -tf.reduce_mean(log_prob)

train = tf.train.AdamOptimizer().minimize(cost)

post_dist = binom_func.sample(1000)
```

見てわかるとおり、エンコード部・デコード部ともに隠れ層はひとつのシンプルなものとしています。エンコード部には活性化関数としてsoftsignを使用しました。これは実装にあたり、参考にした実装に用いられたものがあったため、あまり深く考えずに用いています。
binom_funcは、二項分布の確率分布のオブジェクトにあたります。その後にlog_probを付加することで対数尤度が計算する関数として機能させます。対数尤度は上に凸の関数なのでコスト関数とする場合はマイナス記号を付けています。最後のpost_distは学習後のパラメータを使ったサンプリングを表しています。ここではひとつの元画像に対して1000回のサンプリングをすることにしています。
　学習は通常のニューラルネットを同様です。

```python

sess = tf.Session()
Cost_trace = []
sess.run(tf.global_variables_initializer())

train_data = mnist.data[:50000,:]

batch = 1000
idx = len(train_data) // batch

for i in range(50) :
    cnt = 0
    for j in range(idx) :
        
        if cnt+idx < len(train_data):
            sess.run(train,feed_dict={INPUT:train_data[cnt:cnt+batch,:]})
            tmp = sess.run(cost,feed_dict={INPUT:train_data[cnt:cnt+batch,:]})
        else :
            sess.run(train,feed_dict={INPUT:train_data[cnt:,:]})
            tmp = sess.run(cost,feed_dict={INPUT:train_data[cnt:,:]})
            
        cnt = cnt + batch
        
    Cost_trace.append(tmp)
```

![自己符号化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163753/8489994b-7aa2-dc08-2c8c-88fca0d86f44.png)

上記がコスト関数の推移です。まだ、学習できそうですが、このあたりから勾配消失が起こるので50回でとどめてす。対数尤度の分布は以下のようになります。なお、各画像の対数尤度は画素単位に計算されているので平均をとっています。

![対数尤度.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163753/571d861c-4781-66ec-8a55-e088339252a6.png)

　今回の場合、学習時のほうが尤度が小さい（つまりあまり想定されない画素分布）画像が存在するようですが、全体的には学習時と評価時で同じような結果になっていることがみられます。
　実際の復元した画像を比較します。

![復元結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163753/c1436542-b84e-65a2-9a50-522c1d32ef0f.png)

　見た目からすると元の画像に近似した画像が復元できていることがわかります。一方、興味深いことに復元後の画像は元の画像の特徴を保持していながら、異なる文字の特徴を示しています。９の丸い部分は典型的で、元の画像が真円に近いが復元後の画像はやや斜めになっています。
　今回のモデルは、各画素を二項分布によって色の濃さを決定しています。よって、元の画像から抽出した特徴ベクトルを条件とした場合、実際の画素の分布は確率的にどのようになっているかが復元後の画像としてあらわされていると解釈できます。言い換えると、この学習済みモデルにとって元の画像は、復元後の画像のように見えているということになるでしょう。

# カラー画像への拡張

　mnistはグレー画像で比較的シンプルなものでした。そこでカラー画像に自己符号化器を適用し、あらためて確認することにします。
　使用するカラー画像はkaggleから取得しました。（https://www.kaggle.com/olgabelitskaya/flower-color-images）
　実装は、先のmnistの次元を拡張しただけです。

```python

tf.reset_default_graph()
    
INPUT_RGB = tf.placeholder(tf.float32,[None,100*100*3])

w1_rgb = tf.Variable(tf.truncated_normal([100*100*3,180],stddev=0.1))
b1_rgb = tf.Variable(tf.zeros([180]))

w2_rgb = tf.Variable(tf.truncated_normal([180,100*100*3],stddev=0.1))
b2_rgb = tf.Variable(tf.zeros([100*100*3]))

Encode_rgb = tf.nn.softsign(tf.matmul(INPUT_RGB,w1_rgb) + b1_rgb)
Decode_rbg = tf.matmul(Encode_rgb,w2_rgb) + b2_rgb

def log_probability(dec,INPUT) :
    p1 = tf.nn.sigmoid(dec)
    binom_func = tfp.distributions.Binomial(255,probs=p1)
    log_prob = binom_func.log_prob(INPUT)
    return log_prob

cost_rgb = -tf.reduce_mean(log_probability(Decode_rbg,INPUT_RGB))

train_rgb = tf.train.AdamOptimizer().minimize(cost_rgb)

def post_dict(dec_rgb) :
    p_rbg = tf.nn.sigmoid(dec_rgb)*255    
    return p_rbg

post_dist_mean = post_dict(Decode_rbg)
```

なお、学習後の予測ですが、mnistの時は複数回サンプリングしないと画素の期待値が得られないようにしていました。このままだと、カラー画像のように次元数が多いと計算コストが必要としすぎるので、簡易に二項分布の期待値を計算する公式をpost_dictとして実装しました。学習処理のコードは示しませんが、学習回数（300,000回）とmnistと比較してかなりの時間を必要としました。
　学習結果として、二乗誤差分布の可視化をします。

![二乗誤差分布.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163753/09ddf2b4-d54b-dc7e-5f2b-c7a6835a6194.png)

まだ、性能が出ていないケースもありますが、おおむね良好のように見えます。実際にこの分布からサンプリングして復元画像と元の画像を比較してみます。

![カラー画像復元.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163753/7a047f36-60d1-f8ed-b49c-f00cef519048.png)

これによると二乗誤差が650を超過するあたりから復元具合が悪くなり、1000を超えるとかろうじてわかる程度になっています。この1000超の画像の比率は全体の約30％なので、全体の30％ほどはもう少しモデルのチューニングが必要だったということになります。ただ、復元後の画像を見ていると、花の色合いや背景と花の色の相違によって復元の度合いが違うようで、このあたりを意識してチューニングしないとこれ以上の改善は難しい気がしています。まだ仮説でしかないのですが、RGB方式はRed/Green/Blueの三色を大きい値にして同じ程度に混合すると白色になり、小さい値にして混合すると黒色になっていきます。ということは、全体的に白っぽい画像や白色の花の場合、ベクトルの要素が一様になるため、復元が難しかったと考えています。この点については、別の機会にこうした各画素に対する三色の混合比と自己符号化器の性能を調べ、その結果をチューニングに活かしていきたいと思います。

# 自己符号化器の応用

　それなりのモデルができることがわかったので、このモデルの応用に移りたいと思います。自己符号化器の本来の目的は画像から質の高い特徴を抽出することにありました。ちょうど、PCAによって次元削減することと同じ構図です。（PCAと自己符号化器の関係は『深層学習（講談社 岡谷貴之著）』に詳しいので参照願います）
　本論では、この自己符号化器を異常値検知に適用したいと思います。再び、mnistのデータを使って、以下のような異常値検知問題を設定します。

　１．画像のうち0のものは正常、それ以外（1～9）は異常とし、異常データを正確に検知する。
　２．異常データは出現が希少で、正常1000件に対して80件出現する。
　３．よって学習に使用できるデータは、正常データ5000件に対して異常データ400件である。

この場合、二値識別モデルとして実装すると不均衡データであるため、異常データの識別性能があまり期待できません。加えて言うと、異常データ400件では、1クラスあたりの手書きデータが50件ほどとバリエーションが不十分と思われます。そこで、0の画像のみを使用して自己符号化器を実装します。これによりこの自己符号化器は過学習の状態になります。この自己符号化器に異常データを入力すると、復元データは0のときよりもうまく復元できていないはずです。この特性を活かすことで異常値を識別します。
　実装は、先のmnistのものをそのまま使います。変更点は学習データをclass=0のもののみを使いました。まず、完成したモデルに他のClassのデータ（つまり異常値）を入力し、復元画像を入手し、結果をClass=0のものと比較しました。

![クラス別復元結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163753/beb09941-9ab7-526d-c5c1-ebe25cb4b520.png)

横軸の評価指標には二乗誤差を使用しています。どのクラスも学習に使用したゼロのものよりもうまく復元できておらず、異常値検知にうまく使えそうです。
　実際の判定は、この正常値の分布と異常値の分布を使って行います。まず、学習用データを用いて正常値および異常値の二乗誤差の分布を確率分布として定義します。評価対象の画像に対して自己符号化器を通して計算された二乗誤差を取得し、正常値および異常値の確率分布から各々に属する確率密度を計算します。最後にその結果を比較してどちらに属するのかを判定します。まずパラメータ推計を実装します。

```python
#### 正常値（Class=0） の二乗誤差の確率分布パラメータ設定

result = sess.run(post_dist,feed_dict={INPUT:train_data})                  #### train_dataは学習時に使用したデータ
parameters_gamma_0 = stats.gamma.fit(((result-train_data)**2).sum(axis=1))

#### 異常値（Class!=0）の二乗誤差の確率分布パラメータ設定
train_x_abnorm = train_x[train_y!='1']
train_x_abnorm = train_x_abnorm[np.random.choice(np.arange(len(train_x_abnorm)),400)]

result_abnorm = sess.run(post_dist,feed_dict={INPUT:train_x_abnorm})
parameters_gamma_1 = stats.gamma.fit(((result_abnorm-train_x_abnorm)**2).sum(axis=1))

#### 未使用のデータを使った評価

test_x = mnist.data[50000:]
test_y = mnist.target[50000:]

test_result = sess.run(post_dist,feed_dict={INPUT:test_x})
tmp = ((test_result-test_x)**2).sum(axis=1)

positive_prob = stats.gamma.pdf(tmp,*parameters_gamma_1)     #### 異常値である確率密度
negative_prob = stats.gamma.pdf(tmp,*parameters_gamma_0)     #### 正常値である確率密度

result_class = positive_prob > negative_prob
```
 二乗誤差の分布にはガンマ分布を使用しました。今回の場合、二乗誤差は常に正の値しか取らないこと。事前の分析から左側の裾野が長い形状になっていること、からガンマ分布がうまくフィットするという判断です。
　結果を評価するため、ここまでに未使用であった2万件のデータを使用して混合行列を集計しました。

|        |       真 |    偽    |
|:-----------------|------------------:|------------------:|
| 真             |              17407 |        622        |
| 偽           |            150 |       1821       |

学習用データが不均衡にもかかわらず、なかなかの性能と言えそうです。なお、この実装の場合は正常値・異常値のどちらの確率密度が大きいのかという比較的単純な判定としています。タスクによっては真（異常値）の検出性能を高めにしたい場合もあるかと思います。こうした場合は、先のnegativ_probとpositive_probの差異を算出したうえで閾値を使って判定すれば実現できます。

# まとめ
　自己符号化器は、畳み込み層ニューラルネットワークなどと比べると後塵を拝しているとの見解もあって、それほど普及している印象はありません。一方、この自己符号化を発射台に深層信念ネットワークや敵対的生成ネットワークへと発展していることを考えると自己符号化器を理解しておくことは重要です。本論のモデルはほんのわずかではありますが確率モデルを適用しており、従来の自己符号化器より少しだけ深層信念ネットワークや敵対的生成ネットワークに実装を寄せています。よって、今後は本論をベースにモデルの拡張を考えたいと思います。
　最後に、本論は手書き文字を事例に異常値検知への応用を検討しました。他の事例としてクレジットカードの不正利用検知への適用についても検証したいと思います。こちらについては以前に「不均衡データに対するClassification」としてオーバーサンプリングのケースを報告していますので、この結果との比較という視点を検証したいと思います。