人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

Theanoによる積層自己符号化器の実装

Theanoによる雑音除去自己符号化器の実装(2015/12/9)の続き。今回は自己符号化器を積み上げて積層自己符号化器を実装した。

多層パーセプトロンは隠れ層の数を増やす(Deepにしていく)とより複雑なモデルを表現できるようになるが、誤差が伝播されずに重みが更新されなくなる勾配消失問題(Vanishing gradient problem)が生じる。この問題を解決するため多層パーセプトロンの重みの初期値をランダムに設定するのではなくより良い重みを設定しておく事前学習(pre-training)というテクニックが提案されている。

このより良い重みを自己符号化器(2015/12/3)を順に積み上げることで設定するのが積層自己符号化器(Stacked autoencoder)。自己符号化器で初期値を決めたらあとは普通に多層パーセプトロン(2015/6/18)と同じ誤差逆伝播法で重みを更新すればよい。この重み更新はfine-tuningと呼ばれている。

積層自己符号化器の2つの側面

積層自己符号化器の構造は

  1. 自己符号化器を複数積み上げたもの
  2. 複数の隠れ層を持つ多層パーセプトロン

という二つの側面がある。たとえば、下のようなニューラルネットを考えると自己符号化器が3つ(ただし、符号化部分のみで復号化部分は省略)という側面と隠れ層が3つの多層パーセプトロンという側面がある。出力層だけは少し特殊。ニューラルネットで何らかの分類をしたいときは多層パーセプトロン(2015/6/18)と同じようにロジスティック回帰層をくっつければよい。

f:id:aidiary:20160120194922p:plain

(注)入力のユニット数より隠れ層のユニット数が小さくないと自己符号化器として意味がないと理解していたがチュートリアルでは入力が784で隠れ層が1000と大きくなっていたため上の図でもそのようにしている。

積層自己符号化器の学習は、pre-trainingフェーズで重みの初期値を求めて、fine-tuningフェーズで重みを更新するという2つのフェーズからなる。最初のpre-trainingではこのニューラルネットを自己符号化器を積み上げたものとみなして入力を再現するような重みを教師なしで1層ずつ求める。次のfine-tuningでは自己符号化器で求めた重みをそのまま引き継いで、多層パーセプトロンとみなして教師ありで重みを更新する。出力層の重みは自己符号化器に含まれないためランダムに初期化し、fine-tuningでのみ更新する。

というのが私が理解した積層自己符号化器の概要。というわけでDeep Learning Tutorialを参考にTheanoによる実装を詳しくみていきたい。これまで自己符号化器と書いてきたが実装では過学習に強い雑音除去自己符号化器(2015/12/9)を使っている。

ソースコード全体はここ。以下のまとめではポイントになる部分のコードの抜粋しか載せていない。

ネットワークの構成・重みの共有

積層自己符号化器の実装では、自己符号化器と多層パーセプトロンの間で重みを共有するのが大きなポイント。たとえば、先の図の赤色の重み (W, b) はAutoencoder 0とHidden Layer 0が共有し、黄色の重み (W, b) はAutoencoder 1とHidden Layer 1が共有していることを意味する。

このように重みを共有しておけばpre-trainingの自己符号化器で求めた重みの初期値をfine-tuningの多層パーセプトロンに速やかに(というか何もしなくても)引き継げる。Deep Learning Tutorialの実装では、多層パーセプトロンの隠れ層の重みを自己符号化器が参照する方法で重みの共有が実装されている(逆でもよいか?)。

ネットワークを構成するコードは隠れ層の数(self.n_layers)を可変にするため下のようなforループで書かれている。入力層に近い方から順番に構築して積み上げていることがわかる。HiddenLayerクラスは多層パーセプトロン(2015/6/18)で実装したものをDenoisingAutoencoder雑音除去自己符号化器(2015/12/3)で実装したものを再利用している。pre-trainingではDenoisingAutoencoderのコスト関数とパラメータ更新式をfine-tuningではHiddenLayerのコスト関数とパラメータ更新式をそれぞれ使う。

# ネットワークを構築
# 隠れ層の数だけループして積み上げていく
for i in xrange(self.n_layers):
    # ユニット数
    if i == 0:
        input_size = n_ins
    else:
        input_size = hidden_layers_sizes[i - 1]

    # 隠れ層への入力データ
    if i == 0:
        layer_input = self.x
    else:
        layer_input = self.hidden_layers[-1].output

    # 多層パーセプトロンの隠れ層
    # fine-tuningで重みを更新するため
    hidden_layer = HiddenLayer(rng=numpy_rng,
                               input=layer_input,
                               n_in=input_size,
                               n_out=hidden_layers_sizes[i],
                               activation=T.nnet.sigmoid)
    self.hidden_layers.append(hidden_layer)
    self.params.extend(hidden_layer.params)

    # 自己符号化器だが重みは多層パーセプトロンの隠れ層と共有
    autoencoder_layer = DenoisingAutoencoder(numpy_rng=numpy_rng,
                                             theano_rng=theano_rng,
                                             input=layer_input,
                                             n_visible=input_size,
                                             n_hidden=hidden_layers_sizes[i],
                                             W=hidden_layer.W,
                                             bhid=hidden_layer.b)
    self.autoencoder_layers.append(autoencoder_layer)

# MNISTの分類ができるように最後にロジスティック回帰層を追加
self.log_layer = LogisticRegression(
                    input=self.hidden_layers[-1].output,
                    n_in=hidden_layers_sizes[-1],
                    n_out=n_outs)
self.params.extend(self.log_layer.params)

self.hidden_layersのリストはforループを回すたびに隠れ層のオブジェクトが追加されていく。そのためself.hidden_layers[-1]で一つ前の隠れ層にアクセスできる。自己符号化器の重みとバイアスは隠れ層の重みとバイアスを設定して共有している。そのため自己符号化器のWbself.paramsに追加する必要がない。

8.4 Tips and Tricksを読むとこの実装は効率が悪いとのこと。2つ目や3つ目の自己符号化器への入力を得たいときすでに前の層において出力を計算済みなのにそれを保存していないため再度入力から順にフィードフォワードして出力を再計算する必要がある。ただフィードフォワードの計算は高速だし、GPUのメモリ不足の方が心配の種だから実装的にこれでよいのかもしれない。

pre-training関数の作成

次はpre-training。pre-trainingでは先のニューラルネットを自己符号化器を積み上げたものとみなして浅い層から順に訓練していく。チュートリアルの実装ではpre-trainingを行う関数のリスト(自己符号化器が3つのときは3つの関数がある)を生成して返すメソッドを定義している。

    def pretraining_functions(self, train_set_x, batch_size):
        """自己符号化器を学習するpre-training用の関数リストを返す
        教師なし学習なのでxのみを渡す"""
        # 学習に使うミニバッチのインデックス
        index = T.lscalar('index')

        # 複数の自己符号化器で異なる値を指定できるようにシンボル化する
        corruption_level = T.scalar('corruption')
        learning_rate = T.scalar('lr')

        batch_begin = index * batch_size
        batch_end = batch_begin + batch_size

        # 自己符号化器を学習する関数を生成
        # 入力層に近い方から順番に追加する
        pretrain_functions = []
        for autoencoder in self.autoencoder_layers:
            # 誤差と更新式を計算するシンボルを取得
            cost, updates = autoencoder.get_cost_updates(corruption_level, learning_rate)
            fn = theano.function(
                inputs=[
                    index,
                    # Paramにした引数を関数呼び出し時に与えるときはPython変数名ではなく、
                    # Tensorの引数の名前(corruption, lr)で指定できる
                    theano.Param(corruption_level, default=0.2),
                    theano.Param(learning_rate, default=0.1)
                ],
                outputs=cost,
                updates=updates,
                givens={
                    self.x: train_set_x[batch_begin:batch_end]
                }
            )
            pretrain_functions.append(fn)

        return pretrain_functions

自己符号化器は教師なし学習なので引数にはtrain_set_xのみでラベルは必要ない。自己符号化器が複数あるだけで訓練方法はTheanoによる自己符号化器の実装(2015/12/3)と同じで目新しい部分はない。自己符号化器ごとに独立に学習率と汚染度を設定できるようにinputsに引数を与えているのが少し新しい。

Theanoのややこしい部分だがこの時点では自己符号化器の学習を行う関数リストを返すだけで実行はしない。

fine-tuning関数の作成

次にfine-tuning。自己符号化器の訓練が終わった後に実行される。ニューラルネットを多層パーセプトロンとみなして教師あり(分類ラベルを使う)で学習する。こちらも訓練を行う関数を返すだけで実行はしない。こちらも書き方が少し違うだけでTheanoによる多層パーセプトロンの実装(2015/6/18)とほぼ同じ。

    def build_finetune_functions(self, datasets, batch_size, learning_rate):
        """fine-tuning用の関数を返す"""
        train_set_x, train_set_y = datasets[0]
        valid_set_x, valid_set_y = datasets[1]
        test_set_x, test_set_y = datasets[2]

        n_valid_batches = valid_set_x.get_value(borrow=True).shape[0] / batch_size
        n_test_batches = test_set_x.get_value(borrow=True).shape[0] / batch_size

        index = T.lscalar('index')

        gparams = T.grad(self.finetune_cost, self.params)

        updates = [
            (param, param - gparam * learning_rate) for param, gparam in zip(self.params, gparams)
        ]

        train_model = theano.function(
            inputs=[index],
            outputs=self.finetune_cost,
            updates=updates,
            givens={
                self.x: train_set_x[index * batch_size: (index + 1) * batch_size],
                self.y: train_set_y[index * batch_size: (index + 1) * batch_size]
            },
            name='train')

        test_score_i = theano.function(
            inputs=[index],
            outputs=self.errors,
            givens={
                self.x: test_set_x[index * batch_size: (index + 1) * batch_size],
                self.y: test_set_y[index * batch_size: (index + 1) * batch_size]
            },
            name='test')

        valid_score_i = theano.function(
            inputs=[index],
            outputs=self.errors,
            givens={
                self.x: valid_set_x[index * batch_size: (index + 1) * batch_size],
                self.y: valid_set_y[index * batch_size: (index + 1) * batch_size]
            },
            name='validate')

        def valid_score():
            """各ミニバッチのvalid誤差のリストを返す"""
            return [valid_score_i(i) for i in xrange(n_valid_batches)]

        def test_score():
            """各ミニバッチのtest誤差のリストを返す"""
            return [test_score_i(i) for i in xrange(n_test_batches)]

        return train_model, valid_score, test_score

test_score_ivalid_score_iのように_iがついているのはindexで指定した一つのミニバッチの誤差(スコア)を返すからみたいだ。実際は全ミニバッチの誤差をまとめて計算して誤差リストを返す関数を返している。

積層自己符号化器の訓練と評価

ネットワークの構築

最後にこれまでの関数を使ってモデルを訓練する流れを整理したい。まず、積層自己符号化器のオブジェクトを作る。これで先の図のようなニューラルネットが構築される。

    sda = StackedDenoisingAutoencoder(
        numpy_rng,
        28 * 28,
        hidden_layers_sizes,
        10,
        corruption_levels)

例では、hidden_layers_sizes=[1000, 1000, 1000]corruption_levels=[0.1, 0.2, 0.3]となっている。入力が28x28=784ユニットなのに隠れ層が1000ユニットで自己符号化器が砂時計型にならないけれどよいのだろうか?

pre-training

まずはpre-trainingで重みの初期値を決める。

    # Pre-training
    print "getting the pre-training functions ..."
    pretraining_functions = sda.pretraining_functions(train_set_x=train_set_x, batch_size=batch_size)

    print "pre-training the model ..."
    for i in xrange(sda.n_layers):
        # pre-trainingのエポック数は固定
        for epoch in xrange(pretraining_epochs):
            c = []
            for batch_index in xrange(n_train_batches):
                c.append(pretraining_functions[i](index=batch_index,
                                                  corruption=corruption_levels[i],
                                                  lr=pretrain_lr))
            print "Pre-training layer %i, epoch %d, cost %f" % (i, epoch, np.mean(c))

最初のforループで浅い層から深い層へ順番に自己符号化器を学習していることがわかる。エポック数は固定でpretraining_epochs=15となっている。初期値を決めるだけなので厳密な収束判定は行わずに割と適当でよいのだろうか。実行すると

building the model ...
getting the pre-training functions ...
pre-training the model ...
Pre-training layer 0, epoch 0, cost 71.648659
Pre-training layer 0, epoch 1, cost 63.687855
・・・
Pre-training layer 0, epoch 13, cost 59.473030
Pre-training layer 0, epoch 14, cost 59.331242
Pre-training layer 1, epoch 0, cost 482.215973
Pre-training layer 1, epoch 1, cost 464.952728
・・・
Pre-training layer 1, epoch 13, cost 451.862030
Pre-training layer 1, epoch 14, cost 451.500214
Pre-training layer 2, epoch 0, cost 196.515244
Pre-training layer 2, epoch 1, cost 183.888687
・・・
Pre-training layer 2, epoch 13, cost 176.494202
Pre-training layer 2, epoch 14, cost 176.348206
The pretraining code for file stacked_autoencoder.py ran for 72.73m

こんな感じで層ごとに自己符号化器を学習してエポックが進むと誤差が低下することがわかる。層を深くして自己符号化器の数が多くなると学習にけっこう時間がかかる。たった3つの隠れ層の初期値を決めるだけでGPUでも72分かかった。事前学習の効果は本当にあるのだろうか・・・

fine-tuning

pre-trainingが終わったら最後にfine-tuning。これはTheanoによる多層パーセプトロンの実装(2015/6/18)とほとんど同じでearly-stoppingで収束判定している。

    # Fine-tuning
    print "getting the fine-tuning functions ..."
    train_model, validate_model, test_model = sda.build_finetune_functions(
        datasets=datasets,
        batch_size=batch_size,
        learning_rate=finetune_lr
    )

    print "fine-tuning the model ..."
    while (epoch < training_epochs) and (not done_looping):
        epoch = epoch + 1
        for minibatch_index in xrange(n_train_batches):
            train_model(minibatch_index)
            iter = (epoch - 1) * n_train_batches + minibatch_index

            if (iter + 1) % validation_frequency == 0:
                validation_losses = validate_model()
                this_validation_loss = np.mean(validation_losses)

                if this_validation_loss < best_validation_loss:
                    if this_validation_loss < best_validation_loss * improvement_threshold:
                        patience = max(patience, iter * patience_increase)
                    best_validation_loss = this_validation_loss
                    best_iter = iter

                    test_losses = test_model()
                    test_score = np.mean(test_losses)

            # patienceを超えたらループを終了
            if patience <= iter:
                done_looping = True
                break

Validation誤差は以下のように収束する。

f:id:aidiary:20160122224815p:plain

最終的に75エポックで収束し、テスト誤差は1.4%となった。つまり、正解率は98.6%。実行時間はGPUで162分・・・

Optimization complete with the best validation score of 1.360000 %, on iteration 1900000, with test performance 1.400000 %
The training code for file stacked_autoencoder.py ran for 162.11m

初期値をランダムではなく、なるべく良いように設定しておくと深いニューラルネットがうまく学習できるというのが事前学習のアイデアだ。ニューラルネットではないが同様のアイデアは他のアルゴリズムでもあった。たとえば、混合ガウス分布(GMM)の学習(2010/5/21)で初期クラスタの位置をランダムに設定せずにK-meansの結果を使うなど。比較的シンプルなアイデアに見えるが事前学習が発見されるのにこんなに長い時間かかったのが不思議に思える。知ってみれば簡単でも最初に思いついて実行するのはやはり難しいものなのだろうか。

今回の積層自己符号化器の実装自体は以前に実装した自己符号化器と多層パーセプトロンの組み合わせなのでそんなに難しくなかった。なかったけれど・・・うーん、事前学習で初期値を決めた効果は本当にあったのだろうか?次回はここら辺を実験で検証してみたい。

参考