KDOG Notebook

どうも、古くからの友人です。

オートエンコーダーを用いた事前学習

はじめに

通常、多層パーセプトロンの層を深くしていけば、より複雑なパターンを認識することが期待される。しかし、層の数を増やしていくほど関数の最適化は困難になり、初期条件の選択によって学習効率が左右される。オートエンコーダーは重み関数についてのより良い初期条件を得るために提案された教師なし事前学習法である。今回はよりロバストな学習モデルを作るためにStacked denoising Autoencoderを使って事前学習を行い、層数を増やしても学習効率が下がらないことを示す。

理論

1) Denoising Autoencoder (dA)

オートエンコーダーの模式図を下に示す。

f:id:kdog08:20170602022901p:plain:w600

オートエンコーダーは与えられた入力を隠れ層に渡し(encode)、再び入力を再現する(decode)ように重み関数を最適化するネットワークである。encodeおよびdecodeは次式で表される。

 \begin{eqnarray}
&\text{Encode: }& {\bf z}_{e} = f( \tilde{{\bf x}})W + b) \\
&\text{Decode: }& {\bf x}_{rc} = \tilde{f}({\bf z}_{e}\tilde{W} + \tilde{{\bf b}})
\end{eqnarray}

ここで、  \tilde{{\bf x}}は入力 {\bf x}を一部損傷させたデータ、 {\bf x}_{rc}は損傷された入力から隠れ層を経て復元されたデータである(denoising)。また、今回は[tex: \tilde{W} = WT]として議論を進める。誤差関数として交差エントロピー誤差関数を用いると

 \begin{eqnarray}
E({\bf x}, {\bf x}_{rc}) = -{\bf x} \log{\bf x}_{rc} - ({\bf 1} - {\bf x}) \log({\bf 1} - {\bf x}_{rc})
\end{eqnarray}

で与えられる。したがって、 (W, {\bf b}, \tilde{{\bf b}})に対しての誤差関数の傾きは

 \begin{eqnarray}
\frac{\partial E}{\partial W} &=& \tilde{\bf x}^T\left(({\bf y} - {\bf t})W*{\bf z}*({\bf 1-z})\right)+\left({\bf z}^T({\bf y} - {\bf t})\right)^T \\
\frac{\partial E}{\partial {\bf b}} &=& ({\bf y} - {\bf t})W*{\bf z}*({\bf 1-z}) \\
\frac{\partial E}{\partial \tilde{{\bf b}}} &=& {\bf y} - {\bf t}
\end{eqnarray}

で与えられる(導出は下図の計算グラフを参照)。ここで、 *はベクトルの要素ごとの掛け算を表している。上式の傾きをもとに (W, {\bf b}, \tilde{{\bf b}})は更新される。

f:id:kdog08:20170602022935p:plain:w600

f:id:kdog08:20170602022956p:plain:w600

f:id:kdog08:20170602023009p:plain:w600

2) Stacked denoising Autoencoder (SdA)

SdAはdAを積み重ねて学習する方法で、多層パーセプトロンのための事前学習である。SdAの模式図を下に示す。左のdA層から1層ずつ学習を進めていく。1層目で事前学習を行い、得られた結果(encodeの結果)は2層目が入力値として受け取り、事前学習を行う。以下、3層目、4層目へと事前学習が行われる。各層で事前学習が完了したら、得られた重み関数を初期値として通常の教師あり学習が行われる。

f:id:kdog08:20170602023106p:plain:w600

実装

  • モジュール、およびデータ(MNIST)の読み込み
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from sklearn.utils import shuffle
from sklearn.model_selection import train_test_split

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data/', one_hot=True)
mnist_X, mnist_y = mnist.train.images, mnist.train.labels
train_X, test_X, train_y, test_y = train_test_split(mnist_X, mnist_y, test_size=0.1, random_state=random_state)
class Autoencoder_sig:
    def __init__(self, vis_dim, W, b):
        self.W = W
        self.b = b
        self.a = np.zeros(vis_dim).astype('float64')
        self.params = [self.W, self.b]
        
    def encode(self, x):
        u = np.dot(x, self.W) + self.b
        return sigmoid(u)
    
    def decode(self, x):
        u = np.dot(x, self.W.T) + self.a
        return sigmoid(u)
    
    def reconst_error(self, t, y):
        return -np.sum(t * np.log(y + 1.e-6) + (1.0 - t) * np.log(1.0 - y + 1.e-6), axis=1)
        
    def train(self, x, noise, eta=np.float32(0.01)):
        # forward prop
        tilde_x = x * noise
        z = self.encode(tilde_x)
        reconst_x = self.decode(z)
        
        error = self.reconst_error(x, reconst_x)
        
        # backward prop
        delta_2 = reconst_x - x
        delta_1 = np.dot(delta_2, self.W) * z * (1.0 - z)
        
        da = np.dot(np.ones(len(reconst_x)), delta_2)
        db = np.dot(np.ones(len(z)), delta_1)
        dW = np.dot(tilde_x.T, delta_1) + np.dot(z.T, delta_2).T
        
        self.W -= eta * dW
        self.a -= eta * da
        self.b -= eta * db
        
        return error, reconst_x
  • Stacked Autoencoderの実装
class Dense:
    def __init__(self, in_dim, out_dim):
        self.W = rng.uniform(low=-0.08, high=0.08, size=(in_dim, out_dim)).astype('float64')
        self.b = np.zeros([out_dim]).astype('float64')
        self.params = [self.W, self.b]
        
        self.AE = Autoencoder_sig(in_dim, self.W, self.b)
        
    def fwd_prop(self, x):
        u = np.dot(x, self.W) + self.b
        self.z = sigmoid(u)
        return self.z
    
    def pretrain(self, x, noise):
        error, reconst_x = self.AE.train(x, noise)
        return error, reconst_x
  • モデルの構築
layers = [
    Dense(784, 300),
    Dense(300, 300),
    Dense(300, 300),
    Dense(300, 300)
]
  • 事前学習
X = np.copy(train_X)

for layer in layers:
    n_epochs = 10
    batch_size = 100
    n_batches = X.shape[0] // batch_size
    
    corruption_level = np.float64(0.3)
        
    for epoch in range(n_epochs):
        X = shuffle(X)
        err_all = []
        for i in range(n_batches):
            start = i * batch_size
            end = start + batch_size

            _noise = rng.binomial(size=X[start:end].shape, n=1, p=1-corruption_level)
            err, _ = layer.pretrain(X[start:end], _noise)
            err_all.append(err)
        print('EPOCH:%d, ERROR:%lf' % (epoch+1, np.mean(err_all)))
    X = layer.fwd_prop(X)
mlp = MLP_MNIST(layers[0].W, layers[0].b, layers[1].W, layers[1].b, layers[2].W, layers[2].b, layers[3].W, layers[3].b)

n_epochs = 10
batch_size = 100
n_batches = train_X.shape[0] // batch_size
cost_history = []

for epoch in range(n_epochs):
    train_X, train_y = shuffle(train_X, train_y)
    cost = 0
    for i in range(n_batches):
        start = i * batch_size
        end = start + batch_size
        cost += mlp.train(train_X[start:end], train_y[start:end], eps=0.01)
    cost_history.append(cost/(n_batches*batch_size))

    pred_y = mlp.test(test_X)
    print(f1_score(np.argmax(test_y, 1), pred_y, average='macro'))

結果

(1) 一般的なMLPによる学習効率

SdAによる事前学習を用いない場合についてMLPの学習効率を調べた。以下にネットワークを1-4層まで変えた場合のF1値および交差エントロピー誤差 Eの変化を示す。層の数を増やすに連れてF1値が低い値をとることがわかった。10エポックの学習が終了した時には層数による影響はF1値には見られなかった(One-layer: 0.9709, Two-layer: 0.9652, Three-layer: 0.9664, Four-layer: 0.9528)。しかし、5エポックの学習時点では著しく4層のネットワークの値が悪くなっている。誤差関数においても層数を増やすに連れて収束しにくくなっている。

f:id:kdog08:20170602023903p:plain:w600

f:id:kdog08:20170602023931p:plain:w600

(2) SdAによる事前学習を用いた場合の学習効率

SdAを用いて事前学習を行い、得られた重み関数(バイアスを含む)を初期値としてMLPの学習を行った。以下に、3および4層についてのF1値および交差エントロピー誤差 Eを示す。事前学習を行ったことで、1エポックの学習終了時点でより高いF1値を示した(Three-layer: 0.9467, Four-layer: 0.9483)。層数による違いがほとんどないことから、いずれの場合でも事前学習が十分に行われたと思われる。

f:id:kdog08:20170602024028p:plain:w600

f:id:kdog08:20170602024043p:plain:w600

メモ

ネットワークの層を深くすることで学習効率が落ちる原因の一つとして勾配消失問題が挙げられる。この問題は活性化関数を変更することで避けることも可能である。今回用いているシグモイド関数の勾配は高々0.25であるから、層を深くするほど入力層に近くなる程伝搬する誤差が0に近くなり学習効率が下がるわけである。一方、tanhやReLU関数は勾配の最大値は1であるから誤差が0になるような状況を避けることが可能になる。

参考文献

岡谷貴之, 「深層学習」第5章, 講談社, 2017.