読者です 読者をやめる 読者になる 読者になる

人工知能に関する断創録

人工知能、認知科学、心理学、ロボティクス、生物学、ゲームAIなどに興味を持っています。このブログでは人工知能のさまざまな分野について調査したことをまとめています。最近は、機械学習、音声認識・音声合成、複雑系(カオス、力学系)、Deep Learningなど。



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

Theanoによる多層パーセプトロンの実装(2015/6/18)のつづき。今回は、Deep Learning Tutorialの畳込みニューラルネットワーク(Convolutional Neural Network: CNN, ConvNet)を実装してみる。

CNNは人間の視覚野を参考にした手法であり、画像認識に特化したDeep Learningアルゴリズムである。ImageNetの物体認識コンテストでぶっちぎりの成果を上げた手法Googleの猫認識と言われている手法もさまざまな工夫があるもののCNNをベースにしている。

本当は一般物体認識の実験をやりたいところだけどお楽しみは後に残しておいて、まずはMNISTの手書き数字認識を追試して感触をつかみたい。ソースコード全体はGithubに置いた。

畳込みニューラルネットワーク

まず今回実装する畳込みニューラルネット(CNN)の構成を図でまとめてみた(Deep Learning Tutorialの図は実装と関係ないじゃん・・・)

f:id:aidiary:20150626203849p:plain

一番左の入力から分類結果を出力する右側まで畳込み層(convolution layer)プーリング層(pooling layer)を何回か繰り返したあと最後に全結合した多層パーセプトロンが配置される構成になっている。前回実験した(2015/6/18)多層パーセプトロンは一番右側の部分にあたる。なんか一気に複雑になった・・・理解できんのこれ?

今回は上の図のlayer0の部分をチュートリアルの実装に即して自分なりにまとめてみたい。

畳込み層

畳込み層は入力画像に対してフィルタをかける(畳込む)層である。画像の畳込みによって画像内のパターンが検出できるようになる。畳込みの数式は参考文献に書いてあるがここでは省略。Theanoではtheano.tensor.nnet.conv.conv2d()という関数が提供されているので実装する上ではあまり問題にならない。

先の図では28x28ピクセルの「7」という入力画像に対して5x5のそれぞれ異なるフィルタを20個かけて24x24の20枚の画像を出力している。フィルタをかけると画像サイズが少し小さくなる。入力画像サイズがW \times WでフィルタがH \times Hだと出力画像サイズは W - 2 \lfloor H/2 \rfloor \times W - 2 \lfloor H/2 \rfloorになる。ここで\lfloor \cdot \rfloorは小数点以下切り下げて整数化する演算。今回はW=28, L=5なので上式で計算すると24になる。フィルタをかけた出力画像は特徴マップと呼ばれることが多いようだ。

フィルタは4次元テンソルW[20,1,5,5]で表せる(テンソルの扱いnumpyのndarrayとほとんど同じ)この意味は5x5のフィルタで入力画像の枚数(チャネル数)が1で出力画像の枚数が20ということを意味している。MNISTのデータは白黒画像なので入力画像は1チャネルになる。もし入力がカラー画像のときはRGBの3チャネルである。今回の実装では1チャネルのフィルタが20枚使われる。フィルタを1つでなく、複数使うのがポイント。フィルタを増やすことで入力画像のさまざまな特徴を捉えられるようになる。

CNNがすごいのはこのフィルタを開発者が手動で設計するのではなく、学習によって自動獲得できるという点にある。最初、Wはランダムな値が入っていてよくわからない特徴にしか反応しないが、学習が進むと縦線や横線など画像認識に重要な特徴に強く反応するようになっていく。これがDeep Learningの力の源で表現学習と呼ばれる。これまでの説明からわかるようにCNNで更新(学習)するパラメータはフィルタWになる。

学習する前のランダムな値が入ったフィルタを可視化すると下のようになる。5x5のサイズのフィルタが20個ある。ランダムなパターンでなんかよくわからない(笑)

f:id:aidiary:20150626220554p:plain

CNNの学習が完了した後の同じ場所のフィルタを可視化すると下のようになる。

f:id:aidiary:20150626220604p:plain

少しわかりにくいが縦線や斜めの線が強調されるフィルタパターンが学習されたのががわかる。この20個のフィルタを数字の「7」の画像に畳み込むと下のような畳込み層の出力画像が得られる。先の図で左から2つめの24x24ピクセルの20枚の画像がこれである。

f:id:aidiary:20150626220932p:plain

フィルタや畳込み層の出力を可視化するコードは次回まとめる予定。

プーリング層

プーリング層は畳込み層の直後に置かれ、抽出された特徴の位置感度を低下させる働きがある。つまり、数字の「7」が画像の真ん中に書かれていても少し左や右にずれていても同じように「7」と判定するために必要ってこと。すごく難しそうだが処理自体は畳込みよりずっと単純で2x2(poolsizeで指定)の矩形フィルタを入力画像内でずらしていき矩形内の最大の値を取り出して新しい画像を出力するような処理を行う。最大値に置き換える方法はmaxpoolingと呼ばれる。このプーリング層でも入力画像に比べて出力画像サイズは小さくなる。先の例では24x24の20枚の画像が12x12の20枚の画像になっている。画像のサイズは小さくなるが枚数は変わらない。Theanoではtheano.tensor.signal.downsample.max_pool_2d()という関数が用意されている。数式や動作の詳細は参考文献参照。なんでプーリングって名前が付いたのかよくわからないな。

LeNetConvPoolLayerクラス

Deep Learning Tutorialでは、畳込み層とプーリング層のペアをLeNetConvPoolLayerというクラスで表現している。実際はペアである必要はなく、畳込み層を3つ続けてプーリング層1つとかでもいいみたい。その場合はこのクラスは使えない。分けたほうがよいかもね。

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

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

        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),  # @UndefinedVariable
            borrow=True)

        b_values = np.zeros((filter_shape[0],), dtype=theano.config.floatX)  # @UndefinedVariable
        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)

        # 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'))

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

入力される画像がinputでフィルターはWで表現している。フィルタの値は多層パーセプトロンと同じくランダムな値で初期化される。bはバイアスでプーリングの後に各画像に対して加えられる。bはフィルタ数と同じ数の1次元配列なのでpooled_outの4Dテンソルと足し合わせるようにdimshuffleで次元を調整している。pooled_outの2つめの次元がbの次元数と一致するのがポイント。バイアスを加えたあとに活性化関数tanhを適用している。活性化関数にはtanhより高速で同じくらい高性能なReLU(Rectified Linear Unit)というのが最近ではよく使われるらしい。あとで置き換えて結果を比較してみたい。

Deep Learning Tutorialではプーリング層の出力に対してバイアスを加えて活性化関数を通すような実装になっているが、論文によっては畳込み層の出力に対してバイアスを加えて活性化関数を通した後にプーリング層へというように説明されていることもある。どちらでもよいのかな?

このクラスは下のように使う。

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

    # 最初の畳み込み層+プーリング層
    # 畳み込みに使用するフィルタサイズは5x5ピクセル
    # 畳み込みによって画像サイズは28x28ピクセルから24x24ピクセルに落ちる
    # プーリングによって画像サイズはさらに12x12ピクセルに落ちる
    # 特徴マップ数は20枚でそれぞれの特徴マップのサイズは12x12ピクセル
    # 最終的にこの層の出力のサイズは (batch_size, 20, 12, 12) になる
    layer0 = LeNetConvPoolLayer(rng,
                input=layer0_input,
                image_shape=(batch_size, 1, 28, 28),  # 入力画像のサイズを4Dテンソルで指定
                filter_shape=(20, 1, 5, 5),           # フィルタのサイズを4Dテンソルで指定
                poolsize=(2, 2))

先の図では入力画像は「7」だけでたった1つのように描いたが、学習するときはミニバッチ単位でbatch_size=500枚の画像をまとめて渡す。このミニバッチ単位でフィルタのパラメータWとバイアスbを更新するためだ。

ここまでで先の図のlayer0がやっと終わった。この層の出力は12x12ピクセルの特徴マップが20枚になる。この出力に対してもう一度畳込みとプーリングをかけるのだがこれは次回にしよう。

参考文献

はっきり言ってDeep Learning TutorialのCNNの説明はわかりにくく、これだけでは理解できなかった。次のサイト、書籍、論文で何とか自分なりに理解できたと思う。比較して確認したから勘違いはしてないと思うのだけど・・・