StatsFragments

Python, R, Rust, 統計, 機械学習とか

Theano で Deep Learning <3> : 畳み込みニューラルネットワーク

Python Theano を使って Deep Learning の理論とアルゴリズムを学ぶ会、第三回。今回で教師あり学習の部分はひと段落。

目次

DeepLearning 0.1 について、対応する記事のリンクを記載。

第一回 MNIST データをロジスティック回帰で判別する
第二回 多層パーセプトロン
第三回 畳み込みニューラルネットワーク (今回)
第四回 Denoising オートエンコーダ
第五回 多層 Denoising オートエンコーダ
第六回の準備1 networkx でマルコフ確率場 / 確率伝搬法を実装する -
第六回の準備2 ホップフィールドネットワーク -
第六回 制約付きボルツマンマシン
Deep Belief Networks
Hybrid Monte-Carlo Sampling
Recurrent Neural Networks with Word Embeddings
LSTM Networks for Sentiment Analysis
Modeling and generating sequences of polyphonic music with the RNN-RBM

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

多層パーセプトロンでは、入力 ( 2次元の画像 ) を 1次元のベクトルとして扱っていた。例えば MNIST データでは 縦横 28 px なので、1レコードは 28 x 28 = 784次元のベクトルとして処理される。そのため、入力画像の形状が同一であっても、その位置がずれているとまったく別のデータに見えてしまう。結果、このような入力に対しては誤判別 / 過学習が起きることがある。

f:id:sinhrks:20141207164244p:plain

畳み込みニューラルネットワークでは、入力を2次元のまま扱って特徴抽出を行い、最終的に 1次元化して多層パーセプトロンに渡す。つまり多層パーセプトロンに以下のような前処理工程が加わったもの。これは生物の視覚野の動きを模したものと考えられる。

  • 入力をいくつかの領域に分割する ( 受容野 に対応)
    • 単純な受容野 (Simpe cells) では、分割した領域から各部分の特徴、例えば物体の境界 (エッジ) などを抽出する
    • 複雑な受容野 (Complex cells) では、物体の位置の移動ずれなんかを吸収する
  • これらを統合して入力の特徴をとらえ、多層パーセプトロンで判別する

前提知識

畳み込みニューラルネットワークは 2次元の画像から特徴抽出を行うため、画像処理の基礎知識があると理解しやすい。

畳み込み (Convolution)

そもそも畳み込みってなあに?という場合はこちら。

画像への畳み込み

画像への畳み込みは、適当な大きさの領域に含まれる各値を重み付けして足し合わせる (この重みを重み行列という) ことになる。つまり畳み込み = フィルタ。畳み込みによって、画像の平滑化 / エッジ検出 (一次微分 / 二次微分 ) なんかができる。

また、もう少し複雑なフィルタもある。例えば Gabor フィルタも哺乳類の視覚野をモデル化するものとして知られている。

今回の畳み込みニューラルネットワークでは フィルタの値を直接触ることはない (モデルが勝手に学習する) ので、画像を重み付けして足し合わせるとそれに応じた特徴がとれる、ということがわかっていれば大丈夫。

疎な結合 (Sparce Connectivity)

ここからの3セクションは具体的な処理を見てからのほうがわかりやすいと思う。

多層パーセプトロンでは、上記のような位置ずれした画像が入力されると、そのずれは誤差として学習されてしまう。これは、 入力層 / 隠れ層 / 出力層 それぞれで、お互いの全ユニットが接続されているために誤差が逆伝播することに起因する。なら 特徴抽出部分では 全ユニット接続しなければよくね?というのがこのセクションの内容。

後述の Max Pooling が疎結合に対応する。サブサンプリングする = 入力の一部を捨てる = ユニット間の接続を切る、ということ。

重み行列の共有 (Shared Weights)

m-1層の特徴マップから m層の特徴マップへの変換は、畳み込み処理に対応した重み行列によって行われる。この重み行列は、ユニット単位 つまり 入力の各領域ごとに変更せず、ひとつの重み行列を入力全体に対して使うようにする。

共有、というと特殊な処理を行っているように聞こえるが、要するに同じフィルタ (重み行列) を画像全体に対して適用して特徴量をとる、と言っているだけ。これは 特徴マップ (後述) 単位で考えると自然なことに感じる。

特徴マップ (Feature Map)

入力から抽出できる特徴は、1通りとは限らない。入力から抽出される複数の特徴を特徴マップと呼ぶ。m層目の特徴マップは、 m-1層目の各特徴マップからの畳み込み + サブサンプリングで表現される (イメージは後述)。

畳み込み演算 (The Convolution Operator)

ここから具体的な処理。

チュートリアルの画像だとよくわからないので、ネットでみつけた別の画像を使う。かわいいですね。

f:id:sinhrks:20141207072036j:plain

補足 元ドキュメントとは入力画像のサイズが異なる。また、ところどころで途中のアウトプットを確認したいためスクリプトは少し書き加えた。が、処理内容は元文書と同じ。

テンソルの定義

  • input : 入力画像に対応する4階テンソル。次元は ( Minibatchのサイズ, 入力の特徴マップ数, 画像の縦幅, 画像の横幅 )。
    • Minibatch のサイズ : 定義はこちらと同じで、ひとかたまりとして処理するデータ数 (レコード数)。この例で入力する画像は 1つだけなので、当然 Minibatch も 1。
    • 入力の特徴マップ数 : カラーの入力に対して、R, G, B に対応する値を特徴マップとする。よって 3。
  • W : 係数テンソル。次元は、( 出力の特徴マップ数, 入力の特徴マップ数, フィルタの縦幅, フィルタの横幅 )。
    • 出力の特徴マップ数 : ここでは 2。
  • b : バイアスベクトル。次元は (出力の特徴マップ数)。

まずは関連パッケージのインポートと、入力テンソルを受けとるシンボル input の定義。

import numpy
import theano
import theano.tensor as T

from theano.tensor.nnet import conv
rng = numpy.random.RandomState(23455)

# 入力画像を受けとるシンボルを作成
input = T.tensor4(name='input')

ここから 係数テンソル W の定義。次元は (2, 3, 9, 9) なので、 9 x 9 のフィルタが 2 x 3 = 6 種類 使われることになる。つまり、W では 入力となる特徴マップ と 出力となる特徴マップの組み合わせの数だけ フィルタが用意され、組み合わせごとに同じ重み行列を使う (重み行列を共有する)。

# 係数テンソル 
# W の次元を指定 ( Minibatchの大きさ, 特徴マップの数, 畳み込み(フィルタ)の縦幅, 畳み込み(フィルタ)の横幅)
w_shp = (2, 3, 9, 9)

# 発生させる一様乱数の範囲を指定
w_bound = numpy.sqrt(3 * 9 * 9)
-1.0 / w_bound, 1.0 / w_bound
# (-0.064150029909958411, 0.064150029909958411)

# 係数テンソルを初期化
W = theano.shared( numpy.asarray(
            rng.uniform(
                low=-1.0 / w_bound,
                high=1.0 / w_bound,
                size=w_shp),
            dtype=input.dtype), name ='W')

続けて、バイアスベクトルを定義。 dimshuffleテンソルに対する演算を定義 = 演算をブロードキャストする次元を指定する。

# バイアスベクトルを初期化
b_shp = (2,)
b = theano.shared(numpy.asarray(
            rng.uniform(low=-.5, high=.5, size=b_shp),
            dtype=input.dtype), name ='b')
b.get_value()
# [-0.3943425   0.16818965]

b.dimshuffle('x', 0, 'x', 'x').eval()
# [[[[-0.3943425 ]]
# 
#   [[ 0.16818965]]]]

入力 input に対して、畳み込み とバイアスの足し合わせを定義。

conv_out = conv.conv2d(input, W)
output = T.nnet.sigmoid(conv_out + b.dimshuffle('x', 0, 'x', 'x'))
f = theano.function([input], output)

画像の読み込み

画像は縦横に対応する次元がわかるよう、縦 130 px * 横 120 px とした。入力画像は numpy.array として処理されるので、元文書のように PIL.Image として読み込む意味はない。記法がシンプルな scipy.imread を使う。

また、swapaxes の繰り返しで 入力画像の次元がどのように変換されているか出力した。

import numpy
from scipy.misc import imread

# 読み込むファイル名を指定
img = imread('images/01.jpg')
img = img / 256.

# 元画像の次元 (縦幅, 横幅, RGB)
img.shape
# (130, 120, 3)

# 1軸目と3軸目を入替 (RGB, 横幅, 縦幅)
img.swapaxes(0, 2).shape
# (3, 120, 130)

# 2軸目と3軸目を入替 (RGB, 縦幅, 横幅)
img.swapaxes(0, 2).swapaxes(1, 2).shape
# (3, 130, 120)

# 1軸目にダミーの軸を追加 (dummy, RGB, 縦幅, 横幅)
img.swapaxes(0, 2).swapaxes(1, 2).reshape(1, 3, img.shape[0], img.shape[1]).shape
# (1, 3, 130, 120)

img_ = img.swapaxes(0, 2).swapaxes(1, 2).reshape(1, 3, img.shape[0], img.shape[1])

この画像に対して 係数テンソル W と バイアスベクトル b を適用する。縦横 9 x 9 のフィルタが適用されるため、上下左右の 4 px ずつがクロップされる。したがって、適用後の次元は縦横で 8 px ずつ減って、

# 係数テンソル + バイアスベクトルを適用
filtered_img = f(img_)
filtered_img.shape
# (1, 2, 122, 112)

各層からの出力の描画

この部分の処理をまとめると以下のようになる。

  • 入力層: オリジナルの画像を受け取る。
  • m-1 層: 入力画像を 特徴マップに分解する。特徴マップ数は 3 で、それぞれ R, G, B の 3 色の入力に対応。
  • m 層: m-1 層の特徴マップに対して 係数テンソル, バイアスベクトルを適用する。適用後の特徴マップ数は 2。

それぞれの層で出力される画像を、特徴マップごとに描画した。下図では すべての画像が同じサイズで描画されているが、出力画像の実際のサイズは上記のとおり各層によって異なる。各層 左側のラベルに次元を記載した。

f:id:sinhrks:20141207072444p:plain

Max Pooling

Max Pooling 法とは、あるウィンドウサイズの中で 最大の値を代表値としてサブサンプリングする方法。ウィンドウの中から最も特徴が大きいと考えられる値を拾うため、多少 位置がずれた複数の入力に対しても同じ / 似た 特徴を拾ってくることが期待できる。

Theano では downsample.max_pool_2d を使って Max Pooling を行うことができる。

補足 元文書には 境界値の処理方法を指定する ignore_border オプションについて 2通りの出力が記載されているが、このあと ignore_border=True しか使わないので省略。

from theano.tensor.signal import downsample
input = T.dtensor4('input')

# Max Pooling のウィンドウサイズ
maxpool_shape = (2, 2)
pool_out = downsample.max_pool_2d(input, maxpool_shape, ignore_border=True)
f = theano.function([input],pool_out)

invals = numpy.random.RandomState(1).rand(3, 2, 5, 5)
print 'invals[0, 0, :, :] =\n', invals[0, 0, :, :]
print 'output[0, 0, :, :] =\n', f(invals)[0, 0, :, :]

# invals[0, 0, :, :] =
# [[  4.17022005e-01   7.20324493e-01   1.14374817e-04   3.02332573e-01   1.46755891e-01]
#  [  9.23385948e-02   1.86260211e-01   3.45560727e-01   3.96767474e-01   5.38816734e-01]
#  [  4.19194514e-01   6.85219500e-01   2.04452250e-01   8.78117436e-01   2.73875932e-02]
#  [  6.70467510e-01   4.17304802e-01   5.58689828e-01   1.40386939e-01   1.98101489e-01]
#  [  8.00744569e-01   9.68261576e-01   3.13424178e-01   6.92322616e-01   8.76389152e-01]]
# output[0, 0, :, :] =
# [[ 0.72032449  0.39676747]
#  [ 0.6852195   0.87811744]]

入力 invals と 出力 output について比較すると、各 invals を 2 x 2 ごとに切り取った範囲での最大値が output の対応する位置に入っていることがわかる。例えば一番左上の 2 x 2 ブロックについてみると、

invals[0, 0, 0:2, 0:2]
# array([[ 0.417022  ,  0.72032449],
#        [ 0.09233859,  0.18626021]])

invals[0, 0, 0:2, 0:2].max()
# 0.7203244934421581

各層からの出力の描画

元文書にはないが、ここまでの例に Max Pooling を適用した結果を追加してみる。この結果は、m+1層への入力に相当する。ここでの処理は、

  • 入力層: オリジナルの画像を受け取る。
  • m-1 層: 入力画像を 特徴マップに分解する。特徴マップ数は 3 で、それぞれ R, G, B の 3 色の入力に対応。
  • m 層: m-1 層の特徴マップに対して 係数テンソル, バイアスベクトルを適用する。適用後の特徴マップ数は 2。
  • m+1 層への入力: m 層の特徴マップに対して 2 x 2 の Max Pooling を適用。適用後の特徴マップ数は 2。

Max Pooling は ウィンドウサイズ 2 x 2 で行うため、m+1 層へ入力される画像の縦横サイズはそれぞれ 1/2 ( 1レコードのデータ量 = 面積は 1/4 )になる。

pool_img = f(filtered_img)
pool_img.shape
# (1, 2, 61, 56)

f:id:sinhrks:20141207072514p:plain

モデル (LeNet)

最初に元文書のこの図をみたときは、わかんねえ、、、って感じだったが、上で描画したような畳み込み / サブサンプリングを入力に対して順次実行していることを示している。このとき、各畳み込み層での係数テンソル / バイアスベクトルについて学習をかけることによって うまいこと特徴を残せるようにしよう、というのが 畳み込みニューラルネットワーク

自分の理解をイメージにした。上段がニューラルネットワークのノードとして描いた場合、下段が特徴マップの組み合わせとして描いた場合 (クリックで拡大)。

f:id:sinhrks:20141214220532p:plain

元文書でちょっとわかりにくいな、と思うのは、"サブサンプリング層はニューラルネットワークのユニットを含む層ではない" (ニューラル層間の疎結合をあらわしている) ということ。単なる定義の話なので 畳み込み層 / サブサンプリング層を全結合 - 疎結合する別個のニューラル層と捉えても間違っていないと思うが、続く例では前者の考え方をしている。

全部まとめて

LeNetConvPoolLayer は 畳み込み処理 + サブサンプリング処理を行う1層を作る。これは 特徴マップに対する処理で考えると 畳み込み層 + サブサンプリング層 ふたつの働きをする。LeNetConvPoolLayer 内の処理は、上記の 畳み込み演算 / Max Pooling でやっていることと同じなので省略。

続けて、evaluate_lenet5 関数にて、以下 5 層 (インスタンスとしては4つ) の畳み込みニューラルネットワークを定義 & 学習している。

  • input: 入力層。白黒画像のため、特徴マップ数は 1。
  • layer0: 入力の特徴マップ数は 1。出力の特徴マップ数 は nkerns[0]=20
    • 畳み込み層: フィルタは 5 x 5 のため、適用後の画像サイズは 24 x 24。
    • サブサンプリング層: ウィンドウは 2 x 2 のため、適用後の画像サイズは 12 x 12。
  • layer1: 入力の特徴マップ数は nkerns[0]=20。出力の特徴マップ数 は nkerns[1]=50
    • 畳み込み層: フィルタは 5 x 5 のため、適用後の画像サイズは 8 x 8。
    • サブサンプリング層: ウィンドウは 2 x 2 のため、適用後の画像サイズは 4 x 4。
  • layer2: 多層パーセプトロンの隠れ層
  • layer3: 出力層。ロジスティック回帰を行う
    layer0 = LeNetConvPoolLayer(
        rng,
        input=layer0_input,
        image_shape=(batch_size, 1, 28, 28),
        filter_shape=(nkerns[0], 1, 5, 5),
        poolsize=(2, 2)
    )

    layer1 = LeNetConvPoolLayer(
        rng,
        input=layer0.output,
        image_shape=(batch_size, nkerns[0], 12, 12),
        filter_shape=(nkerns[1], nkerns[0], 5, 5),
        poolsize=(2, 2)
    )

    # 入力を 4次元 -> 2次元に変換 (レコード単位でみると 2次元画像 -> 1次元ベクトルへ変換)
    layer2_input = layer1.output.flatten(2)

    # 多層パーセプトロンの隠れ層
    layer2 = HiddenLayer(
        rng,
        input=layer2_input,
        n_in=nkerns[1] * 4 * 4,
        n_out=500,
        activation=T.tanh
    )
    # ロジスティック回帰
    layer3 = LogisticRegression(input=layer2.output, n_in=500, n_out=10)

損失関数、勾配計算についての考え方は多層パーセプトロンの場合と同様。定義に layer0, layer1 での係数テンソル、バイアスベクトルを含めることで、それぞれに対して偏導関数を求めて学習ができる。

これまでと同じく、最後に置いてあるスクリプトを前回と同じディレクトリに入れて実行すればよい。

が、自分の MacBook Pro (Core2 Duo 2.4GHz, メモリ 4GB) でCPU処理させると 1 Minibatch の処理に 10 秒くらいかかる。元文書にも処理時間の目安が記載されているが、CPU処理だとCore i7 でも380 分くらいかかるようだ (GPU処理なら 45分くらい)。

これはGPU処理できるようにしないと話にならないな、、、。

パラメータ調整のコツ

また、元文書では補足的に以下のパラメータ調整についてカッコ内のようなことが記載されている。

  • フィルタの数 = 特徴マップの数 (計算コストを考えて選べ)
  • フィルタのサイズ (データセットによる。MNIST では 1層目は 5 x 5 がよいが、一般の画像ではもっと大きいほうがよい)
  • Max Pooling のウィンドウサイズ (典型的には 2 x 2、もしくは Max Pooling しない)

12/15追記 続きはこちら。

深層学習 (機械学習プロフェッショナルシリーズ)

深層学習 (機械学習プロフェッショナルシリーズ)