Theanoによる自己符号化器の実装
この記事はDeep Learning Advent Calendar 2015の3日目です。いつも読んでばかりで悪いし、Deep Learningの話題なら何でもよいそうなので登録してみました。
Theanoによる畳み込みニューラルネットワークの実装 (2)(2015/7/14)のつづき。
ここ最近はChainerを使ってきたけれどまた手法の勉強も兼ねてTheanoでの実装に戻りたい。ChainerやTensorFlowがあるんだからTheanoなんてもう誰も使わない?ごめんなさい・・・TheanoはDeep Learning Tutorialをはじめ、実装例が豊富にあり、絶妙な粒度で小回りもきくので手法の勉強にちょうどよいんだよね。
今回からしばらくさまざまな自己符号化器(Autoencoder)を検証していきたい。深層学習のメリットである特徴の自動学習の基礎になるところなのでしっかり理解しておきたいところ。個人的には一番興味を持っているアルゴリズムである。これまでも主成分分析やフーリエ解析などパターンの分解と再構築に関するアルゴリズムに興味を惹かれることが多かったのでこれに興味を持ったのは必然というべきか・・・
Deep Learning Tutorialにそって
- 自己符号化器(Autoencoder)
- 雑音除去自己符号化器(Denoising autoencoder)
- 積層自己符号化器(Stacked autoencoder)
- スパース自己符号化器(Sparse autoencoder)
- 縮小自己符号化器(Contractive autoencoder)
- 変分自己符号化器(Variational autoencoder)
の順に実装してみる予定。日本語だとあまり聞き慣れない訳が多いけれど深層学習: Deep Learning(人工知能学会編)に準拠した。
Deep Learning Tutorialでは自己符号化器と雑音除去自己符号化器が同じコードだけれど実装をわかりやすくするため分離した。スパース自己符号化器と変分自己符号化器はDeep Learning Tutorialに載ってないけれどいくつかTheanoでの実装例を見つけたので参考までに実装してみたい。またDeep Learning TutorialではMNISTの実験結果しかないけれど、CIFAR-10(2015/10/14)などカラー画像でも実験してみたい。
今回実装したコードはここ。
自己符号化器
ポイントをまとめると
- ラベル情報を使わない教師なし学習に属する
- データに内在する特徴を学習する
- 主成分分析と同じく情報の次元圧縮の手法(主な違いは非線形性の導入と多層化)
- 深層ニューラルネットワークの事前学習(重みの初期値決定)に使われる
事前学習の方法には自己符号化器の他に制限ボルツマンマシン(RBM: Restricted Boltzmann Machine)というアルゴリズムもある。RBMが確率モデルであるのに対し、自己符号化器は決定的モデルなのでより理解しやすいと感じた。チュートリアルで先に来るのもそのせいだろうと勝手に想像。
Deep Learning Tutorialは図が乏しいので整理のため自分で描いてみた。
自己符号化器の構造的な特徴は
- 入力層、隠れ層、出力層の三層構造が基本
- 入力層と出力層のユニット数は同じ
- 隠れ層のユニット数は入力層より小さい(砂時計型)
となる。入力データに含まれる意味のある特徴を学習するためには隠れ層のユニット数を入力層のユニット数より小さくする必要がある。ただし、例外的にスパース自己符号化はスパース制約を入れるため隠れ層が大きくてもOKとのこと。これは後でためそう。
隠れ層や出力層の出力は、多層パーセプトロン(2015/6/18)とまったく同じ。
x
をy
に変換するプロセスを符号化(encode)、y
をz
に変換するプロセスを復号化(decode)と呼ぶ。
s
はsigmoid
などの活性化関数。Deep Learning Tutorialでは両方とも同じ関数s
を使っているが、符号化と復号化で別のものを使ってもよいそうだ。
W
とW'
は多層パーセプトロンだとそれぞれ別の重み行列で管理したが、自己符号化器では という重み共有(tied weight)の制約を入れることもある。この制約を入れるとW'
はW
から計算できるため学習すべきパラメータ数が半分に減る。
多層パーセプトロン(2015/6/18)だと出力z
がクラスラベル(数字認識なら10クラス)に近づくように重み {W, b, W', b'}
を学習したけれど、自己符号化器はクラスラベルを使わず、出力z
が入力x
に近づくように重みを更新する。まあ「己が教師だ!」と言ってしまえば教師あり学習に見えないこともない。
z
がx
にどれくらい近づいたかはコスト関数(誤差関数)で定義する。このコスト関数を最小化するようにパラメータ {W, b, b'}
を誤差逆伝搬法で更新するのが自己符号化器の学習に当たる。
入力x
が [0, 1] の範囲に正規化されているならば、交差エントロピー誤差関数
が使える。この式は入力x
が1サンプルの場合だが、実際にはミニバッチ単位で誤差を計算するのでミニバッチに含まれる全サンプルの平均誤差を求める。
自己符号化器は、入力x
をより低次元の空間y
に写像し、そこから元の入力z
を再構築(reconstruct)していると解釈できる。低次元の空間y
に写像した時点で入力x
から冗長な情報が削ぎ落とされ、元のx
を再構成するのに必要な情報y
だけが抽出されるという理屈のようだ。そんなにうまくいくものかな?と疑っていたのだけれど、どうやら最適化のちからを過小評価していたようだ・・・
以下、Theanoで実装していくが、全部書くと長くなるので理解が難しかったところをかいつまんで整理した。ソースコード全体はこちら。
重み共有の実装
まず重み共有の制約だがこれは簡単に入れられる。
# パラメータ
self.W = W
self.b = bhid
self.W_prime = self.W.T
self.b_prime = bvis
self.params = [self.W, self.b, self.b_prime]
self.params
は誤差逆伝搬法で更新対象となるパラメータをまとめたリストだがここにself.W_prime
は入っていないのがポイント。self.W_prime
はself.W
の転置行列として計算できるので更新しなくてもよい。
Chainerは重みパラメータが隠されているためか重み共有を実装するのに一手間かかると聞いたことがあるが、ちゃんと実装できたという報告例もある(Chainerでtied weightなAutoencoderを作った)のでできないわけではないようだ。
符号化・復号化処理の実装
符号化と復号化を行うシンボルを返すメソッドは下のように定義できる。あくまでシンボルを返すだけでtheano.function()
で関数化しないと使えない。Theanoはここらへんがややこしい・・・
def get_hidden_values(self, input): """入力層の値を隠れ層の値に変換""" return T.nnet.sigmoid(T.dot(input, self.W) + self.b)
内積の計算が数式と逆でないか?と思ったがこれで正しかった。数式のx
は1サンプルの入力(ベクトル)なのに対し、このコードのinput
はミニバッチに含まれるサンプル集合(行列)のため。行列を使うと複数のサンプルの出力をまとめて計算できるため効率がよい。同様の理由で重み行列W
の形状も数式の場合と行と列が逆なので注意。
def get_reconstructed_input(self, hidden): """隠れ層の値を入力層の値に逆変換""" return T.nnet.sigmoid(T.dot(hidden, self.W_prime) + self.b_prime)
復号化のコードもほとんど同じ。今回のMNISTの例では入力を0-1の範囲に正規化しているため活性化関数はsigmoid
でよい。ここで入力が任意範囲の実数なのに活性化関数にsigmoid
を使ってしまうと、sigmoid
によって出力z
が [0, 1] の範囲になってしまうのでz
がいつまで経ってもx
に近づかない(収束しない)という悲惨な結果になる。これは経験済み(笑)
交差エントロピー誤差関数の実装
次に交差エントロピー誤差関数(コスト)の実装。コストとパラメータ更新式を計算するシンボルを返すメソッドが定義されている。Deep Learning Tutorialではこの章から実装方法が以前と微妙に変わっていて開発者が別の気がしている。
def get_cost_updates(self, learning_rate): """コスト関数と更新式のシンボルを返す""" # 入力を変換 y = self.get_hidden_values(self.x) # 変換した値を逆変換で入力に戻す z = self.get_reconstructed_input(y) # コスト関数のシンボル # 元の入力と再構築した入力の交差エントロピー誤差を計算 # 入力xがミニバッチのときLはベクトルになる 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
L
が交差エントロピー誤差関数の数式に対応する。ここは少し理解に手間取ったので詳細にまとめておこう。
まず、数式の、はベクトルであるのに対し、このコードのself.x
とz
はミニバッチ単位の行列になっていることに注意。print self.x.type
、print z.type
とするとTensorType(float32, matrix)
、TensorType(float32, matrix)
となり、vectorではなくmatrixであることがわかる。
模式図を描くと
わかりにくくなるためT.sum()
内の第二項とlog
の計算は無視した。この結果を見ると交差エントロピー誤差関数の定義と同じようにベクトルの各要素の積を計算してから和を取るという操作がミニバッチの全サンプルまとめて一括計算できていることがわかる。for文を使わず行列で書くのは慣れないとわかりにくい。図の具体例をTheanoのコードで書くと
実行結果は
[ 50. 250. 610.] 303.333333333
となり、意図どおり計算できていることがわかる。
モデル学習の実装
ここは多層パーセプトロン(2015/6/18)とほとんど違いがないため省略かな。収束判定が適当になり、epoch
数を固定しているのでむしろ前より単純になった。このチュートリアルでは自己符号化器はあくまで事前学習に使うという位置づけなので収束判定は適当でよいのかもしれない。
if __name__ == "__main__": learning_rate = 0.1 training_epochs = 20 batch_size = 20 # 学習データのロード datasets = load_data('mnist.pkl.gz') # 自己符号化器は教師なし学習なので訓練データのラベルは使わない train_set_x = datasets[0][0] # ミニバッチ数 n_train_batches = train_set_x.get_value(borrow=True).shape[0] / batch_size # ミニバッチのインデックスを表すシンボル index = T.lscalar() # ミニバッチの学習データを表すシンボル x = T.matrix('x') # モデル構築 rng = np.random.RandomState(123) theano_rng = RandomStreams(rng.randint(2 ** 30)) autoencoder = Autoencoder(numpy_rng=rng, theano_rng=theano_rng, input=x, n_visible=28 * 28, n_hidden=100) # コスト関数と更新式のシンボルを取得 cost, updates = autoencoder.get_cost_updates(learning_rate=learning_rate) # 訓練用の関数を定義 train_da = theano.function([index], cost, updates=updates, givens={ x: train_set_x[index * batch_size: (index + 1) * batch_size] }) # モデル訓練 fp = open("cost.txt", "w") start_time = time.clock() for epoch in xrange(training_epochs): c = [] for batch_index in xrange(n_train_batches): c.append(train_da(batch_index)) print "Training epoch %d, cost %f" % (epoch, np.mean(c)) fp.write("%d\t%f\n" % (epoch, np.mean(c))) fp.flush() end_time = time.clock() training_time = (end_time - start_time) fp.close() print "time: %ds" % (training_time)
Deep Learning Tutorialにないコードとして、Theanoチュートリアルを参考に学習したモデルパラメータを保存するコードを付け加えた。
# 学習したモデルの状態を保存 f = open('autoencoder.pkl', 'wb') cPickle.dump(autoencoder.__getstate__(), f, protocol=cPickle.HIGHEST_PROTOCOL) f.close()
class Autoencoder(object): def __getstate__(self): """パラメータの状態を返す""" return (self.W.get_value(), self.b.get_value(), self.b_prime.get_value()) def __setstate__(self, state): """パラメータの状態をセット""" self.W.set_value(state[0]) self.b.set_value(state[1]) self.b_prime.set_value(state[2])
autoencoder
のオブジェクトを丸ごとdumpするというのはあまりよくないアイデアのようだ。ここでは学習したパラメータをSharedVariable
の形式ではなく、get_value()
でnumpy.ndarray
の形式にしてからダンプしている。こうしておくと重みを描画するコードなどが書きやすい。
実験結果
MNISTデータセットで実験してみた。入力層と出力層のユニット数は28*28=784ユニット、隠れ層のユニット数が500、学習率0.1とした。GPUを使用し、20エポック回すのに63秒かかった。
訓練データのコストが下がっていき学習できていることはわかる。ただ自己符号化器単体ではテストデータでの識別精度などに基づいてモデルの良し悪しが判断できない。学習した重みを可視化したり、入力画像を再構築して正しく動作しているか確認してみたい。
重みの可視化
重みを可視化する関数はDeep Learning Tutorialのutils.pyに実装されている。ただこのコードはわかりにくい・・・
これまでと同様にmatplotlibを使って描画してみた。学習した重みはautoencoder.pkl
というファイルにダンプしたのでこれを使って以下のような描画スクリプトを書いた。
重みはとくに正規化されているわけでもなく、負の値を取ることもあるため画像として出力するには重みを [0, 255] の範囲にスケーリングする必要がある。まず、W = (W - np.min(W)) / (np.max(W) - np.min(W))
のコードで重みを [0, 1] の範囲に正規化し、これに255をかけることで [0, 255] の範囲にしている。また、隠れ層の数が500ユニットなので本来は重みが500個の画像が描けるのだが最初の100個に絞っている。
実行すると下のような画像が得られ、チュートリアルの結果ともほぼ一致する。
この気持ち悪い画像が一体何を意味しているのか?が問題だけど深層学習(MLPシリーズの方)の5.3節で理解できたので引用しておこう。
自己符号化器では、誤差関数を最小化することで、ネットワークの重みとバイアスを決定します。通常、中間層の (W, b) と出力層の (W', b') のうち、前者にのみ関心があります。これらのパラメータは、(データを表す)特徴と呼ばれます。yの計算は、重み行列Wとxの積から始まりますが、これはWの各行ベクトルとxとの内積の計算であって、そこでは入力xにWの各行ベクトルが表す成分がどれだけ含まれているかを取り出しているといえます(p.58)。
主成分分析でいうところの固有ベクトルのようなものだと理解している。
入力画像の再構築
最後に隠れ層のユニット数を変えたときにどの程度元の画像が再現できるのか検証した。まずはオリジナル画像 隠れ層のユニット数500のとき。ほぼ完璧にオリジナル画像が再現できている。 隠れ層のユニット数100のとき。情報の欠落が大きいようで少し乱れてきた。 隠れ層のユニット数10のとき。かなり乱れてきた。数字自体が変わってしまうケースもある。
これCIFAR-10の画像でやったら楽しそうだな。
再構築に使ったコードはこれ。
次回は雑音除去自己符号化器を実装してみよう。大部分は今回のコードが流用できるので短く書けそう。
参考
ChainerによるCIFAR-10の一般物体認識 (2)
ChainerによるCIFAR-10の一般物体認識 (1)(2015/11/8)のつづき。今回は畳み込みニューラルネットワークの畳込み層の数を変えたときにテスト精度がどのように変わるか調査した。前回と同じくCIFAR-10の一般物体認識をタスクとしている。
畳み込み2層(conv2)
INPUT -> (CONV -> POOL) * 2 -> FC
CONVの後のRELUは省略している。* 2
は直前の(CONV -> POOL)
を2回繰り返すことを意味している。Chainerで書くと
model = chainer.FunctionSet(conv1=F.Convolution2D(3, 32, 3, pad=1), conv2=F.Convolution2D(32, 32, 3, pad=1), l1=F.Linear(2048, 1024), l2=F.Linear(1024, 10)) def forward(x_data, y_data, train=True): x, t = chainer.Variable(x_data), chainer.Variable(y_data) h = F.max_pooling_2d(F.relu(model.conv1(x)), 2) h = F.max_pooling_2d(F.relu(model.conv2(h)), 2) h = F.dropout(F.relu(model.l1(h)), train=train) y = model.l2(h) if train: return F.softmax_cross_entropy(y, t) else: return F.accuracy(y, t)
畳込みを行っても画像サイズが小さくならないようにパディングしている。フィルタサイズが3x3のときはパディングサイズを1にすると畳み込みしても画像サイズは変わらない。たとえば、元画像サイズが32x32でフィルタサイズが3x3とするとパディングによって画像サイズは34x34になる。前回書いた式で畳み込み後の画像サイズを計算すると34-2(3/2)=32となり元画像と同じサイズになる。
同様にフィルタサイズが5x5のときはパディングサイズを2、フィルタサイズが7x7のときはパディングサイズを3、フィルタサイズが9x9のときはパディングサイズを4にすれば畳み込みしても画像サイズが変わらない。
畳み込み3層(conv3)
INPUT -> (CONV -> POOL) * 3 -> FC
model = chainer.FunctionSet(conv1=F.Convolution2D(3, 32, 3, pad=1), conv2=F.Convolution2D(32, 32, 3, pad=1), conv3=F.Convolution2D(32, 32, 3, pad=1), l1=F.Linear(512, 256), l2=F.Linear(256, 10)) def forward(x_data, y_data, train=True): x, t = chainer.Variable(x_data), chainer.Variable(y_data) h = F.max_pooling_2d(F.relu(model.conv1(x)), 2) h = F.max_pooling_2d(F.relu(model.conv2(h)), 2) h = F.max_pooling_2d(F.relu(model.conv3(h)), 2) h = F.dropout(F.relu(model.l1(h)), train=train) y = model.l2(h) if train: return F.softmax_cross_entropy(y, t) else: return F.accuracy(y, t)
畳み込み4層(conv4)
前回の構成でわかるように畳み込みによって画像サイズは変わらなくても次のプーリング層で画像サイズが半分になていく。これ以上積み重ねると非常に小さい画像になってしまうためここからプーリング層は1つおきに入れることにした。
INPUT -> CONV1 -> (CONV2 -> POOL) -> CONV3 -> (CONV4 -> POOL) -> FC
model = chainer.FunctionSet(conv1=F.Convolution2D(3, 32, 3, pad=1), conv2=F.Convolution2D(32, 32, 3, pad=1), conv3=F.Convolution2D(32, 32, 3, pad=1), conv4=F.Convolution2D(32, 32, 3, pad=1), l1=F.Linear(2048, 512), l2=F.Linear(512, 10)) def forward(x_data, y_data, train=True): x, t = chainer.Variable(x_data), chainer.Variable(y_data) h = F.relu(model.conv1(x)) h = F.max_pooling_2d(F.relu(model.conv2(h)), 2) h = F.relu(model.conv3(h)) h = F.max_pooling_2d(F.relu(model.conv4(h)), 2) h = F.dropout(F.relu(model.l1(h)), train=train) y = model.l2(h) if train: return F.softmax_cross_entropy(y, t) else: return F.accuracy(y, t)
畳み込み6層(conv6)
INPUT -> CONV1 -> (CONV2 -> POOL) -> CONV3 -> (CONV4 -> POOL) -> CONV5 -> (CONV6 -> POOL) -> FC
model = chainer.FunctionSet(conv1=F.Convolution2D(3, 32, 3, pad=1), conv2=F.Convolution2D(32, 32, 3, pad=1), conv3=F.Convolution2D(32, 32, 3, pad=1), conv4=F.Convolution2D(32, 32, 3, pad=1), conv5=F.Convolution2D(32, 32, 3, pad=1), conv6=F.Convolution2D(32, 32, 3, pad=1), l1=F.Linear(512, 512), l2=F.Linear(512, 10)) def forward(x_data, y_data, train=True): x, t = chainer.Variable(x_data), chainer.Variable(y_data) h = F.relu(model.conv1(x)) h = F.max_pooling_2d(F.relu(model.conv2(h)), 2) h = F.relu(model.conv3(h)) h = F.max_pooling_2d(F.relu(model.conv4(h)), 2) h = F.relu(model.conv5(h)) h = F.max_pooling_2d(F.relu(model.conv6(h)), 2) h = F.dropout(F.relu(model.l1(h)), train=train) y = model.l2(h) if train: return F.softmax_cross_entropy(y, t) else: return F.accuracy(y, t)
畳み込み8層(conv8)
そろそろ電気代が心配になってきた・・・これが最後だ。
INPUT -> CONV1 -> (CONV2 -> POOL) -> CONV3 -> (CONV4 -> POOL) -> CONV5 -> (CONV6 -> POOL) -> CONV7 -> (CONV8 -> POOL) -> FC
model = chainer.FunctionSet(conv1=F.Convolution2D(3, 32, 3, pad=1), conv2=F.Convolution2D(32, 32, 3, pad=1), conv3=F.Convolution2D(32, 32, 3, pad=1), conv4=F.Convolution2D(32, 32, 3, pad=1), conv5=F.Convolution2D(32, 32, 3, pad=1), conv6=F.Convolution2D(32, 32, 3, pad=1), conv7=F.Convolution2D(32, 32, 3, pad=1), conv8=F.Convolution2D(32, 32, 3, pad=1), l1=F.Linear(128, 128), l2=F.Linear(128, 10)) def forward(x_data, y_data, train=True): x, t = chainer.Variable(x_data), chainer.Variable(y_data) h = F.relu(model.conv1(x)) h = F.max_pooling_2d(F.relu(model.conv2(h)), 2) h = F.relu(model.conv3(h)) h = F.max_pooling_2d(F.relu(model.conv4(h)), 2) h = F.relu(model.conv5(h)) h = F.max_pooling_2d(F.relu(model.conv6(h)), 2) h = F.relu(model.conv7(h)) h = F.max_pooling_2d(F.relu(model.conv8(h)), 2) h = F.dropout(F.relu(model.l1(h)), train=train) y = model.l2(h) if train: return F.softmax_cross_entropy(y, t) else: return F.accuracy(y, t)
実験結果
畳み込み6層のときテスト精度77.0%が最高だった。畳み込み8層にすると悪化してしまう・・・せっかく頑張って回したのに残念。今回はエポック20で単純に学習を打ち切っているのでもっと回し続ければ上がったかもしれないけど。
同じCIFAR-10の実験結果を報告しているcuda-convnetでも単純な3層の畳み込みニューラルネットだとエラー率26%(精度74%)と報告しているため今回の結果とだいたい一致している。これ以上の精度を得るには入力画像に対する前処理が重要になってくるようだ。
Kaggle CIFAR-10に参加されたid:ultraistのアルゴリズムだと精度94.15%が得られて5位だったとのこと。1位のDeepCNetは95.53%で人間の分類精度94%を超えている。
CIFAR-10で得られたフィルタの可視化についてはまた今度。画像の前処理やスパース正則化のあり/なしによって得られるフィルタが変わるらしいのでそれらを一通り実験してからまとめたい。
参考
ChainerによるCIFAR-10の一般物体認識 (1)
Chainerによる畳み込みニューラルネットワークの実装(2015/10/7)のつづき。今回はMNISTの数字画像認識ではなく、CIFAR-10(2015/10/14)という画像データを使った一般物体認識をやってみた。画像を10個のクラスに分類するタスク。実装にはChainerを使った。
MNISTは1チャンネルの白黒画像だったけれどCIFAR-10は3チャンネル(RGB)のカラー画像なので少しだけ複雑になる。CIFAR-10(2015/10/14)でも書いたけれどCIFAR-10の提供データは、各画像サンプルがchannel(3チャンネル)、row(32ピクセル)、column(32ピクセル)のフラット形式3*32*32=3072
次元ベクトルの形で格納されている。Chainerでは画像を (nsample, channel, height, width)
の形式にする必要があるためreshape()
して次元を変換している。
# 画像を (nsample, channel, height, width) の4次元テンソルに変換 X_train = X_train.reshape((len(X_train), 3, 32, 32)) X_test = X_test.reshape((len(X_test), 3, 32, 32))
今回は初めてなので畳み込み層とプーリング層が1つずつの簡単な構成で実験した。
INPUT -> (CONV -> RELU) -> POOL -> FC
CONV
は畳み込み層、RELU
はReLU活性化関数、POOL
はプーリング層(max-pooling)、FC
は全結合層(隠れ層1つ)である。Chainerでは下のようなコードで簡潔に書ける。model
の方には学習によって変化するパラメータがある層だけまとめられている。プーリング層はパラメータがないのでmodel
には含まれない。
model = chainer.FunctionSet(conv1=F.Convolution2D(3, 32, 3, pad=0), l1=F.Linear(7200, 512), l2=F.Linear(512, 10)) def forward(x_data, y_data, train=True): x, t = chainer.Variable(x_data), chainer.Variable(y_data) h = F.max_pooling_2d(F.relu(model.conv1(x)), 2) h = F.dropout(F.relu(model.l1(h)), train=train) y = model.l2(h) if train: return F.softmax_cross_entropy(y, t) else: return F.accuracy(y, t)
畳込み層のconv1=F.Convolution2D(3, 32, 3, pad=0)
は、入力が3チャンネル(RGB)、出力の特徴マップが32チャンネル、フィルタ(カーネル)サイズが3x3、パディングサイズが0であることを意味する。
フル結合層は2層からなりl1=F.Linear(7200, 512)
は、7200ユニットから512ユニットへ変換する層、l2=F.Linear(512, 10)
は512ユニットから10ユニットへ変換する層を意味する。CIFAR-10は10クラスの画像分類なので出力ユニット数は10になる。
畳み込み層ではパディングサイズが0だと出力の特徴マップの画像サイズが入力画像より少し小さくなる。入力画像サイズがでフィルタがだと出力画像サイズはになる。ここでは小数点以下切り下げて整数化する演算。
今回の例だと入力が3x32x32
でconv1
によって32x30x30
になる。さらにmax_pooling_2d
のウィンドウサイズが2なので画像サイズは半分になって32x15x15
になる。これをフラット化してからフル結合層に入力するためフル結合層のユニット数は32x15x15=7200
となる。これがl1
の入力ユニット数が7200となっている理由。
このユニット数が間違っているとChainerが実行時に正しい値を教えてくれるので、自分で計算するのが面倒なら適当に入れておいてもよいかもしれない(笑)たとえば、適当に1000にしてみると
Actual: 7200 != 1000
というエラーが出る。これは「フル結合層のユニット数は本当は7200なのに1000になっていて違うよ!」という意味。
この構成だとテスト精度で最大67.7%だった。次は畳み込みニューラルネットワークの構成をいろいろ変えたとき精度がどのように変化するか調べてみよう。