Theanoによる雑音除去自己符号化器の実装
この記事はDeep Learning Advent Calendar 2015の9日目です。
Theanoによる自己符号化器の実装(2015/12/3)の続き。
今回は自己符号化器を継承して雑音除去自己符号化器(Denoising autoencoder)を実装した。
ソースコード全体はここ。
自己符号化器は入力をもとの入力に戻すような写像を学習する手法だったが、雑音除去自己符号化器はもっと過酷で雑音(ノイズ)を付与した入力を雑音のない状態に戻せという一見すると無茶な要求を最適化アルゴリズムにつきつける。MNISTの例だと左のようなノイズが入った画像(画像クリックで拡大)を右のようなノイズがない画像に戻す写像を学習させる。最適化アルゴリズムにこういう制約を課すだけでよりロバスト性の高い特徴が自動的に学習できるという。
ノイズ付与
ノイズ付与の方法はいろいろあるらしいがランダムにマスキングする手法が一般的なようだ。おそらく、何らかの統計的な傾向があるノイズでないと復元できないと思われる。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)とほとんど同じなので省略。
重みの傾向が異なることはわかったけれどこれだけじゃロバストな特徴かはわからないかな。あとで学習した特徴を用いて数字認識するがそこで精度に差が出ることでロバスト性が示せるようだ。
入力画像の再構築
最後に前回と同様に入力画像を再構築してみよう。上が自己符号化器に入れて再構築した場合、下が雑音除去自己符号化器に入れて再構築した場合である。隠れ層のユニット数はどちらも500である。
雑音除去の方がよりクリアに再現されることがわかる。これは予想できたことでノイズの黒いところを白く補正する傾向が強くなったためだと考えられる。描画用のコードはこちら。
Deep Learning Tutorialでは画像の例しかないけれど、ノイズを工夫すれば自然言語や音声など他のドメインでも応用できるのか気になった。自然言語のノイズというとタイポとかかな?音声は普通に雑「音」なのでわかりやすい。音声の雑音除去は興味があるので余裕があったら試してみたい。
次回は積層自己符号化器(Stacked autoencoder)を実装する。ようやくDeepになる!
参考
- Denoising Autoencoders (dA)
- Vincent, H. Larochelle Y. Bengio and P.A. Manzagol, Extracting and Composing Robust Features with Denoising Autoencoders, Proceedings of the Twenty-fifth International Conference on Machine Learning (ICML‘08), pages 1096 - 1103, ACM, 2008.
- Theano でDeep Learning <4> : Denoising オートエンコーダ
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で得られたフィルタの可視化についてはまた今度。画像の前処理やスパース正則化のあり/なしによって得られるフィルタが変わるらしいのでそれらを一通り実験してからまとめたい。