人工知能に関する断創録

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

Theanoによる畳み込みニューラルネットワークの実装 (2)

Theanoによる畳み込みニューラルネットワークの実装 (1)(2015/6/26)のつづき。今回は前回できなかった

  • ConvLayerとPoolingLayerの分離
  • ReLUの導入

を試してみた。

ConvLayerとPoolingLayerの分離

Deep Learning Tutorialの実装では、LeNetConvPoolLayer()というクラスで畳み込みとプーリングがセットで行われていた。

class LeNetConvPoolLayer(object):
    """畳み込みニューラルネットの畳み込み層+プーリング層"""
    def __init__(self, rng, input, image_shape, filter_shape, poolsize=(2, 2)):

        # 入力の特徴マップとフィルタの畳み込み
        conv_out = conv.conv2d(
            input=input,
            filters=self.W,
            filter_shape=filter_shape,
            image_shape=image_shape)

        # Max-poolingを用いて各特徴マップをダウンサンプリング
        pooled_out = downsample.max_pool_2d(
            input=conv_out,
            ds=poolsize,
            ignore_border=True)

        # バイアスを加える
        self.output = T.tanh(pooled_out + self.b.dimshuffle('x', 0, 'x', 'x'))

いろいろ論文を読むと畳み込みとプーリングは必ずしもセットで行う必要はなく、畳み込みを数回繰り返した後にプーリングを1回という実装もありえるみたい。このクラスのままではそのようなネットワーク構成が作れないのでまずは畳み込み層とプーリング層を分離することにした。

また、上の実装では、

  1. 畳み込み層
  2. プーリング層
  3. バイアスを加える
  4. 活性化関数(tanh)

という順番になっていたが、人工知能学会の特集論文では、

  1. 畳み込み層
  2. バイアスを加える
  3. 活性化関数
  4. プーリング層

と書いてあった。この場合、学習パラメータである重みWとバイアスbは全部畳み込み層のクラスに収まり、プーリング層には学習するパラメータがなくなる。畳み込み層とプーリング層を分離する実装ではこっちの方が都合がよさそうなので採用した。

まずは畳み込み層のクラス。

class ConvLayer(object):
    """畳み込みニューラルネットの畳み込み層"""
    def __init__(self, rng, input, image_shape, filter_shape):
        # 入力の特徴マップ数は一致する必要がある
        assert image_shape[1] == filter_shape[1]

        fan_in = np.prod(filter_shape[1:])
        fan_out = filter_shape[0] * np.prod(filter_shape[2:])

        W_bound = np.sqrt(6.0 / (fan_in + fan_out))
        self.W = theano.shared(
            np.asarray(rng.uniform(low=-W_bound, high=W_bound, size=filter_shape),
                       dtype=theano.config.floatX),
            borrow=True)

        b_values = np.zeros((filter_shape[0],), dtype=theano.config.floatX)
        self.b = theano.shared(value=b_values, borrow=T)

        # 入力の特徴マップとフィルタの畳み込み
        conv_out = conv.conv2d(
            input=input,
            filters=self.W,
            filter_shape=filter_shape,
            image_shape=image_shape)

        # バイアスを加える
        self.output = T.tanh(conv_out + self.b.dimshuffle('x', 0, 'x', 'x'))

        self.params = [self.W, self.b]

畳み込み層にバイアスを加える処理と活性化関数(tanh)を通す処理を書いている。パラメータはWbの両方が含まれる。次にプーリング層のクラス。

class PoolingLayer(object):
    """畳み込みニューラルネットのプーリング層
    この実装ではプーリング層にパラメータはない"""
    def __init__(self, rng, input, poolsize=(2, 2)):
        # Max-poolingを用いて各特徴マップをダウンサンプリング
        pooled_out = downsample.max_pool_2d(
            input=input,
            ds=poolsize,
            ignore_border=True)

        self.output = pooled_out

こちらには学習すべきパラメータはないので、self.paramsは定義しなくていい。

畳み込み層とプーリング層を分離したので見た目のレイヤの数は多くなる。前回と同じ構成の畳み込みニューラルネットを構築するには下のように書く必要がある。ConvLayerPoolingLayerを分離したので少し大変。

    # 入力
    # 入力のサイズを4Dテンソルに変換
    # batch_sizeは訓練画像の枚数
    # チャンネル数は1
    # (28, 28)はMNISTの画像サイズ
    layer0_input = x.reshape((batch_size, 1, 28, 28))

    layer0 = ConvLayer(rng,
                input=layer0_input,
                image_shape=(batch_size, 1, 28, 28),
                filter_shape=(20, 1, 5, 5))

    layer1 = PoolingLayer(rng,
                          input=layer0.output,
                          poolsize=(2, 2))

    layer2 = ConvLayer(rng,
                       input=layer1.output,
                       image_shape=(batch_size, 20, 12, 12),
                       filter_shape=(50, 20, 5, 5))

    layer3 = PoolingLayer(rng,
                          input=layer2.output,
                          poolsize=(2, 2))

    # 隠れ層への入力
    layer4_input = layer3.output.flatten(2)

    # 全結合された隠れ層
    layer4 = HiddenLayer(rng,
        input=layer4_input,
        n_in=50 * 4 * 4,
        n_out=500,
        activation=T.tanh)

    # 最終的な数字分類を行うsoftmax層
    layer5 = LogisticRegression(input=layer4.output, n_in=500, n_out=10)

また、パラメータは畳み込み層(layer0とlayer2)、隠れ層(layer4)、softmax層(layer5)にしかないので

    # パラメータ
    params = layer5.params + layer4.params + layer2.params + layer0.params

となる。GPUを使って学習してみると

Using gpu device 0: GeForce GTX 760 Ti OEM
Optimization complete.
Best validation score of 0.950000 % obtained at iteration 11700, with test performance 1.060000 %
The code for file convnet2.py ran for 42.09m

となった。学習時間は41分から43分と少し遅くなった。テストエラー率は0.93%から1.06%になったのでこちらも少し悪化している。2回繰り返してみたけどどちらも同じ傾向だったのでもしかしたらDeep Learning Tutorialの順番の方がよかったのかもしれない。

ReLUの導入

ニューラルネットの非線形な活性化関数には、sigmoidやtanhが使われることが多かったが、近年ではRectified Linear Unit、略してReLUを使うことが多いそうだ。

Rectifier (neural networks) - Wikipedia, the free encyclopedia

下のように定義が非常に単純。ReLUを使うと勾配が減衰しにくくなるため収束性や学習速度が向上するという。

 f(x) = max(x, 0)

というわけで活性化関数をtanhからReLUに変えて学習させてみよう。ReLUの実装は非常に簡単。Theanoの機能を使うと

def relu(x):
    """Rectified Linear Unit"""
    return T.switch(x < 0, 0, x)

と書ける。Ttheano.tensorの省略形。xはTensorVariableなので普通のif文では書けなかった。次に活性化関数にtanh()を使っていたところを全部reluに置換する。

    class ConvLayer(object):
        """畳み込みニューラルネットの畳み込み層"""
        def __init__(self, rng, input, image_shape, filter_shape):
            ...

            # バイアスを加える
            self.output = relu(conv_out + self.b.dimshuffle('x', 0, 'x', 'x'))

    ....

    # 全結合された隠れ層
    layer4 = HiddenLayer(rng,
        input=layer4_input,
        n_in=50 * 4 * 4,
        n_out=500,
        activation=relu)

同じくGPUで学習してみると

Using gpu device 0: GeForce GTX 760 Ti OEM
Optimization complete.
Best validation score of 1.200000 % obtained at iteration 15900, with test performance 1.060000 %
The code for file convnet2.py ran for 42.57m

となった。えー、最終的な学習時間や精度は全然改善していないようにみえる。もしかして収束が速いのかな?sigmoidとreluのエラー率の推移をグラフ化してみよう。

f:id:aidiary:20150714235442p:plain

まあ、多少ReLUの方がtanhより収束が速いけど巷で噂になっているほどほどすごいわけでもないような・・・ネットワーク構成を変えるともっと効いたりするのかな?