人工知能に関する断創録

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

積層自己符号化器の性能評価

Theanoによる積層自己符号化器の実装(2016/1/22)の続き。

今回は、積層自己符号化器の事前学習(pretraining)の効果が本当にあったのか検証してみたい。ついでにもう一つTheanoによる雑音除去自己符号化器の実装(2015/12/9)で書いた雑音除去(denoising)の効果も本当にあったのか合わせて検証してみたい。

自己符号化器自体は教師なし学習で特徴抽出しか行わないためその特徴が本当に分類に役立つのか直接検証できなかった。前回実装した積層自己符号化器は最後の層にロジスティック回帰をくっつけることで抽出した特徴を用いてMNISTの数字認識ができるようにしたので認識精度を使って雑音除去や事前学習の効果が比較できる。

ソースコード全体はここ

スクリプトの修正

前回の積層自己符号化器のスクリプトを少しだけ修正した。

  • メインの関数に事前学習を行うか行わないかのフラグ(pretrain_flag)を追加
  • pretrain_flag=Trueのときのみ事前学習フェーズを実行、pretrain_flag=Falseのときはランダムなパラメータから学習
  • 訓練ループをシンプルにし、training_epochs回数だけ回すようにした
  • テスト誤差を出力するファイルを引数で指定できるようにした
def test_stacked_autoencoder(finetune_lr=0.1, pretraining_epochs=15,
                             pretrain_lr=0.001, training_epochs=100,
                             dataset='mnist.pkl.gz', batch_size=1,
                             hidden_layers_sizes=[1000, 1000, 1000],
                             corruption_levels=[0.1, 0.2, 0.3],
                             pretrain_flag=True,
                             testerr_file='test_error.txt'):
    datasets = load_data(dataset)
    train_set_x = datasets[0][0]
    n_train_batches = train_set_x.get_value(borrow=True).shape[0] / batch_size
    numpy_rng = np.random.RandomState(89677)

    print "building the model ..."

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

    # Pre-training
    if pretrain_flag:
        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):
            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))

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

    print "fine-tuning the model ..."

    epoch = 0
    fp = open(testerr_file, "w")
    while (epoch < training_epochs):
        epoch = epoch + 1
        for minibatch_index in xrange(n_train_batches):
            train_model(minibatch_index)
        test_losses = test_model()
        test_score = np.mean(test_losses)
        print "Fine-tuning, epoch %d, test error %f" % (epoch, test_score * 100)
        fp.write("%d\t%f\n" % (epoch, test_score * 100))
    fp.close()

雑音除去の効果

まずは雑音除去を入れた場合と入れない場合でテスト誤差の推移を比較してみた。それ以外の条件は同じで隠れ層が3層から成るニューラルネットを構成した。

def test_denoising():
    test_stacked_autoencoder(hidden_layers_sizes=[1000, 1000, 1000],
                             corruption_levels=[0, 0, 0],
                             testerr_file="sa_error_without_denoising.txt")

    test_stacked_autoencoder(hidden_layers_sizes=[1000, 1000, 1000],
                             corruption_levels=[0.1, 0.2, 0.3],
                             testerr_file="sa_error_with_denoising.txt")

テスト誤差をプロットすると下のような結果が得られた。確かに雑音除去を入れると最終的な分類精度が高くなることがわかる。雑音除去により抽出された特徴がより良いことを示唆している。

f:id:aidiary:20160206111419p:plain

事前学習の効果

次に事前学習を入れた場合と入れない場合でテスト誤差の推移を比較してみた。それ以外の条件は同じ。

def test_pretraining():
    test_stacked_autoencoder(hidden_layers_sizes=[1000, 1000, 1000],
                             corruption_levels=[0.1, 0.2, 0.3],
                             testerr_file="sa_error_with_pretraining.txt",
                             pretrain_flag=True)

    test_stacked_autoencoder(hidden_layers_sizes=[1000, 1000, 1000],
                             corruption_levels=[0.1, 0.2, 0.3],
                             testerr_file="sa_error_without_pretraining.txt",
                             pretrain_flag=False)

テスト誤差をプロットすると下のような結果が得られた。確かに事前学習を入れると誤差が低くなることがわかる。

f:id:aidiary:20160206111615p:plain

今回は隠れ層が3層程度で比較的浅いニューラルネットだがより深くしていくとさらに事前学習の効果が出るのかもしれない(1回の実行に10時間くらいかかったので実験してないけど・・・)。ただMNISTは比較的単純なタスクなのでこれ以上モデルを複雑にしても過学習してしまう可能性が高い。もっと複雑なタスクで比較してみたほうがよいかもしれない。

『深層学習(人工知能学会編)』の3.2節でも事前学習の有用性を評価した研究がいくつか紹介されている(ただし積層自己符号化器でなく次回取り上げる深層ボルツマンマシンによる事前学習の結果)。一方で、事前学習を用いなくても十分よい性能が出せることを示す研究もあるとのこと。

まあ今回の実験でも改善は0.3%だし、前にも書いたけど事前学習フェーズはけっこう時間がかかるので費用対効果が見合うかは微妙なラインかも。

(追記)事前学習は教師なし学習なので容易に入手できる大量のラベルなしデータが使えることが利点として挙げられていた。事前学習をやっておけば少量のラベル付きデータでFine-tuningすればよい。事前学習なしでFine-tuningだけだと大量のラベル付きデータが必要になってしまう。

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の結果を使うなど。比較的シンプルなアイデアに見えるが事前学習が発見されるのにこんなに長い時間かかったのが不思議に思える。知ってみれば簡単でも最初に思いついて実行するのはやはり難しいものなのだろうか。

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

参考

Theanoによる雑音除去自己符号化器の実装

この記事はDeep Learning Advent Calendar 2015の9日目です

Theanoによる自己符号化器の実装(2015/12/3)の続き。

今回は自己符号化器を継承して雑音除去自己符号化器(Denoising autoencoder)を実装した。

ソースコード全体はここ

自己符号化器は入力をもとの入力に戻すような写像を学習する手法だったが、雑音除去自己符号化器はもっと過酷で雑音(ノイズ)を付与した入力を雑音のない状態に戻せという一見すると無茶な要求を最適化アルゴリズムにつきつける。MNISTの例だと左のようなノイズが入った画像(画像クリックで拡大)を右のようなノイズがない画像に戻す写像を学習させる。最適化アルゴリズムにこういう制約を課すだけでよりロバスト性の高い特徴が自動的に学習できるという。

f:id:aidiary:20151209201055p:plain:w320 f:id:aidiary:20151209201124p:plain:w320

ノイズ付与

ノイズ付与の方法はいろいろあるらしいがランダムにマスキングする手法が一般的なようだ。おそらく、何らかの統計的な傾向があるノイズでないと復元できないと思われる。Deep Learning Tutorialではget_corrupted_input()という汚(よご)した画像のシンボルを返すメソッドを定義している。inputが(ミニバッチ単位の)画像セットを表すシンボルでこのメソッドの戻り値が汚れた画像セットを表すシンボル。

    def get_corrupted_input(self, input, corruption_level):
        return self.theano_rng.binomial(size=input.shape, n=1,
                                        p=1-corruption_level,
                                        dtype=theano.config.floatX) * input

ここでは二項分布(binomial)に従うサンプルを画像と同じサイズだけ生成し、それを画像(input)にかけている。二項分布であるが試行回数nが1なのでいわゆるベルヌーイ分布である。これがなぜノイズ付与になるかは下のようなサンプルを書けば理解できる。ここでは画像のサイズが10x10ピクセルと想定している。

#coding:utf-8
import numpy as np
import theano
from theano.tensor.shared_randomstreams import RandomStreams

rng = np.random.RandomState(123)
theano_rng = RandomStreams(rng.randint(2 ** 30))

# 二項分布のサンプルを生成
corruption_level = 0.5
binomial = theano.function([], theano_rng.binomial(size=(10, 10), n=1, p=1-corruption_level))
print binomial()

corruption_level = 0、つまりまったく汚さないとすると

[[1 1 1 1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1 1 1 1]]

が出力される。画像の各ピクセルに1をかけるのでそのままの画像が出力される。次にcorruption_level = 1、つまり最大限汚すと

[[0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]]

が出力される。画像の各ピクセルに0をかけるので画像の全ピクセルが0になり真っ黒な画像になってしまう。corruption_level = 0.5とすると

[[0 1 0 1 1 1 1 1 0 1]
 [0 0 0 1 1 1 0 0 1 0]
 [0 0 1 0 1 1 1 0 1 0]
 [1 1 1 1 1 1 0 1 0 0]
 [1 0 1 0 1 1 1 1 1 1]
 [1 0 1 0 1 1 1 1 0 1]
 [0 0 1 1 1 1 1 0 0 0]
 [0 1 0 0 1 0 0 0 1 0]
 [1 0 0 1 1 0 0 0 1 0]
 [0 1 0 1 0 1 0 0 0 0]]

となり、0と1が大体半分ずつ出現する。画像の各ピクセルにかけると0のところだけ黒く塗りつぶされノイズになるというわけか。実験では corruption_level = 0.3としているため大体30%のピクセルが黒く塗りつぶされることになる。

コスト関数

雑音除去自己符号化器はコスト関数が少しだけ違う。ポイントは2つ。

  • ニューラルネットに入力する前に画像にノイズを付与する
  • 誤差はノイズを付与するの画像との間で計算する

この誤差が小さくなるように重みを学習するためうまく学習できればノイズを付与した画像を入れるとノイズを付与する前の画像が出力されるというわけ。

    def get_cost_updates(self, corruption_level, learning_rate):
        """コスト関数と更新式のシンボルを返す"""
        # 入力の一部にノイズを付与して汚す
        tilde_x = self.get_corrupted_input(self.x, corruption_level)

        # 入力を変換
        y = self.get_hidden_values(tilde_x)

        # 変換した値を逆変換で入力に戻す
        z = self.get_reconstructed_input(y)

        # コスト関数のシンボル
        # 汚した入力が汚す前の入力に近くなるように学習する
        L = - T.sum(self.x * T.log(z) + (1 - self.x) * T.log(1 - z), axis=1)

        # Lはミニバッチの各サンプルの交差エントロピー誤差なので全サンプルで平均を取る
        cost = T.mean(L)

        # 誤差関数の微分
        gparams = T.grad(cost, self.params)

        # 更新式のシンボル
        updates = [(param, param - learning_rate * gparam)
                   for param, gparam in zip(self.params, gparams)]

        return cost, updates

重みの可視化

自己符号化器と雑音除去自己符号化器で学習された重みを可視化して比較してみた。左が自己符号化器の重みで右が今回の雑音除去自己符号化器の重みである。重みを描画するコードは前回(2015/12/3)とほとんど同じなので省略。

f:id:aidiary:20151203104440p:plain:w320 f:id:aidiary:20151209205526p:plain:w320

重みの傾向が異なることはわかったけれどこれだけじゃロバストな特徴かはわからないかな。あとで学習した特徴を用いて数字認識するがそこで精度に差が出ることでロバスト性が示せるようだ。

入力画像の再構築

最後に前回と同様に入力画像を再構築してみよう。上が自己符号化器に入れて再構築した場合、下が雑音除去自己符号化器に入れて再構築した場合である。隠れ層のユニット数はどちらも500である。

f:id:aidiary:20151203211133p:plain f:id:aidiary:20151209210233p:plain

雑音除去の方がよりクリアに再現されることがわかる。これは予想できたことでノイズの黒いところを白く補正する傾向が強くなったためだと考えられる。描画用のコードはこちら

Deep Learning Tutorialでは画像の例しかないけれど、ノイズを工夫すれば自然言語や音声など他のドメインでも応用できるのか気になった。自然言語のノイズというとタイポとかかな?音声は普通に雑「音」なのでわかりやすい。音声の雑音除去は興味があるので余裕があったら試してみたい。

次回は積層自己符号化器(Stacked autoencoder)を実装する。ようやくDeepになる!

参考