えんじにあのじゆうちょう

勉強したことを中心にアウトプットしていきます。

コードで理解するAutoEncoder

はじめに

前回はPCAについて書きました。
PCAは比較的レガシーなやり方だと思いますが、非常に強力な手法で応用範囲が広いということは、色々なところの記載や実際に試してみてある程度わかった気がします。
では次にということでNeuralNetworkを活用した次元削減技術であるAutoEncoderを試してみようと思います。

実は昔にKerasで書いているのですが、やっぱりnumpyで作ってこそだと思うので改めてnumpy(GPU使いたかったので正確にはcupy)で作ってみました。

単純に実装

まずは構造

今回は入力層、隠れ層1つ、出力層1つで組んでみようと思います。
f:id:marufeuillex:20200429200912p:plain

なお、活性化関数は隠れ層側はReLU, 出力層側はsigmoidです。

また、損失関数は今回は入力=出力としたいので平均二乗誤差を使います。

利用するデータ

まずはお決まり通りmnistの手書き文字を利用して次元削減->復元という流れをやってみようと思います。

実装

コードを記載します。colaboratoryで動作させています。

import cupy as cp
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.datasets import fetch_openml

mnist = fetch_openml('mnist_784', version=1,)
label = mnist.target.astype("int")
x_train = cp.asarray(mnist.data).astype('float32') / 255

# 活性化関数とその微分を定義
def relu(x):
    return cp.maximum(0, x)

def relu_d(u):
    return cp.where( u > 0, 1, 0)

def sigmoid(x):
    return 1.0 / (1.0 + cp.exp(-x))

def sigmoid_d(u):
    return sigmoid(u) * (1.0-sigmoid(u))

# init params
latent_dim = 32 # 潜在変数の次元 = 圧縮後の次元数
input_dim = 784 # 入力サイズ
max_iter = 50 # 繰り返し回数
eta = 0.001 # 学習率
batch_size=256 # ミニバッチのサイズ


output_dim = input_dim

cp.random.seed(3)
we = cp.random.normal(loc=0, scale=1, size=(input_dim, latent_dim))
wd = cp.random.normal(loc=0, scale=1, size=(latent_dim, output_dim))
be = cp.zeros((latent_dim,))
bd = cp.zeros((output_dim,))


# 活性化関数を変えたいときいちいち面倒なので、_1がencoder側、_2がdecoder側としてセットしておく
activation_1 = relu
activation_2 = sigmoid

activation_d_1 = relu_d
activation_d_2 = sigmoid_d

mses = []

# ミニバッチと言いつつ、ちょっとランダムにしている
idx = cp.array([True] * batch_size + [False] * (x_train.shape[0] - batch_size))
for i in range(max_iter):
    for j in range(x_train.shape[0] // batch_size + 1):
        train = x_train[cp.random.permutation(idx)]
        #print(train.shape)
        # forward
        h = train.dot(we) + be
        y = activation_1(h) # 潜在変数
        z = y.dot(wd) + bd
        o = activation_2(z)
        
        # error(Mean Squared Error)
        mses.append(
            cp.sum((o - train) ** 2) / (o.shape[0] * o.shape[1]*2)
        )

        # backward
        do = o - train
        
        dz =  do * activation_d_2(z)
        
        dwd = y.T.dot(dz)
        dbd = cp.sum(dz, axis=0)
        dy = dz.dot(wd.T)

        dh = dy * activation_d_1(h)

        dbe = cp.sum(dh, axis=0)
        dwe = train.T.dot(dh)
        
        # update
        we = we - eta * dwe
        wd = wd - eta * dwd
        be = be - eta * dbe
        bd = bd - eta * dbd

    if (i+1) % (max_iter//10) == 0:
        print(mses[-1])

plt.plot(mses)

結果の確認

まずは学習曲線を見ます。多少バリバリしつつもしっかり収束に向かっていることが伺えます。
f:id:marufeuillex:20200429211238p:plain

では結果を見てみましょう。

元データとデコードしたデータを並べてみてみます。

# encode & decode
h = x_train.dot(we) + be
y = activation_1(h)
z = y.dot(wd) + bd
o = activation_2(z)

# visualize
no = cp.asnumpy(o)
fig = plt.figure(figsize=(2, 15))
for i in range(10):
    plt.subplot(10, 2, 2 * i + 1)
    plt.imshow(mnist.data[i].reshape((28, 28)), cmap='gray')
    plt.subplot(10, 2, 2 * i + 2)
    plt.imshow(no[i].reshape((28, 28)), cmap='gray')
plt.show()

冒頭5つを抜粋します。

f:id:marufeuillex:20200429211404p:plain

なかなかいい感じにできてますね!!

潜在変数の次元数を2にすると・・・

PCAのときみたいに、グラフ描画に使えるかなーと思って次元数を2にして見た結果・・・

f:id:marufeuillex:20200429214646p:plain

うーん、いまいちピンとこない。
PCAは次元削減するときに、分散の大きい軸から明示的に採用していた = 少ない次元数でも大きな情報量を持つ軸を明示的に選択していたと思いますが、一方のAutoEncoderはそのあたりは明らかではないのでこういった差なのでしょうか?

ちなみに当たり前ですが、こんな状況なのでEncode/Decodeしてもだいたいの結果が同じ感じになってしまいます。

f:id:marufeuillex:20200429214714p:plain

終わりに

今回はAutoEncoderを試しました。
コードを書くとAutoEncoderそのものだけでなく、どういったことが学習に影響しているかなど感覚が掴めてきて面白いですね。

さて、ここまで来たからにはVAEもやりたいのですが、果たして実装できるかどうか・・・
乞うご期待!