備忘録とか日常とか

学んだこととかを書きます。

theanoのconv2dとmax_pool_2dが遅いので何とかする(1)

Deep learningについて勉強中である。
ライブラリは数あれど、アルゴリズムとか細部の仕組みを理解するためにtheanoで書くことを選んだ。
Bengio先生率いるLISA labが出しているTutorialもあるのでそいつで勉強中。
あとここ
Theanoの使い方 (1) シンボルと共有変数 - 人工知能に関する断創録
の方がわかりやすくまとめてくださっているのでこっちのほうが参考になります。


さてchapter6のConvolutional Neural Network まで読み進めてみた。
チュートリアルのコードは問題なく動いた。
が、conv2dでsubsampleを指定したり、max_pool_2dでstをいじったりするとGPUの使用率が下がって学習が急激に遅くなった。
cifar10のデータセットを試しに使っていたが半日回して20epochとかしか進まなかった…

ちなみにsubsampleとstはフィルタを動かすストライドのことである。
subsumpleはデフォルトで(1,1)。
stは指定しなければds(poolingのサイズを指定するパラメータ)と同じ数値が選ばれ、overlapしないようにpoolingする。
なぜ名前が違うのかは知らん。
詳しくは以下のドキュメントで
conv – Convolution — Theano 1.0.0 documentation
downsample – Down-Sampling — Theano 1.0.0 documentation


調べてみると同じ問題に出くわした人は多いみたい
とりあえず直せたので記録しておく。
参考にしたのはここ


簡潔に言えばtheanoだけではこの問題は解決できない。pylearn2のライブラリからconv2dとmax_pool_2dに相当するモジュールを引っ張ってきて使う。(あくまでtheanoでコード書いて勉強することが目的なのでpylearn2をはじめから使ったりは今のところしない。)

畳みこみ層の定義のとこで、チュートリアルでは以下のように

import theano.tensor as T
input = T.tensor4('input')
filters = T.tensor4('filters')

from theano.tensor.nnet import conv
out = conv.conv2d(input, filters, filter_shape=filter_shape,
                  image_shape=image_shape)

としている。

で、pylearn2のcuda-convnetとやらを使うには

from pylearn2.sandbox.cuda_convnet.filter_acts import FilterActs
from theano.sandbox.cuda.basic_ops import gpu_contiguous

conv_op = FilterActs()
contiguous_input = gpu_contiguous(input)
contiguous_filters = gpu_contiguous(filters)
out = conv_op(contiguous_input, contiguous_filters)

とするらしい。FilterActsというオブジェクトを生成するとこが重要っぽい。inputとfilterはC-contiguous arrayでなければならないそうで、それをチェックしてなおしてくれるのがgpu_contiguous()という関数なんだとか。よくわからんけどあったほうがいいと書いてあったのでとりあえず入れとく。。
最後の行で畳みこみをして出力を返してくれている。


またtheanoとの重要な違いに変数の形がある。conv2dでは

input: (batch size, channels, rows, columns)
filter: (number of filter, channels, rows, columns)

という形にしなければならない(この並びをbc01と呼ぶことにする)。cuda-convnetでは

input: (channels, rows, columns, batch_size)
filter: (channels, rows, columns, number of filters)

にしなければならない(同じくc01bと呼ぶことにする)。theanoのdimshuffle()というメソッドを使えば一行で入れ替えられる。numpyにもこんなのあったな。

conv_op = FilterActs()
input_shuffled = input.dimshuffle(1, 2, 3, 0)   #  bc01 -> c01b
filters_shuffled = filters.dimshuffle(1, 2, 3, 0)   #  bc01 -> c01b
contiguous_input = gpu_contiguous(input_shuffled)
contiguous_filters = gpu_contiguous(filters_shuffled)
out_shuffled = conv_op(contiguous_input, contiguous_filters)
out = out_shuffled.dimshuffle(3, 0, 1, 2)   #  c01b -> bc01

ただし一度データをコピーする操作が入るので、その分だけ実行速度に影響することになる。参考元の筆者はそこまで大きな影響はないと言っているが、どうしても気になるようならdimshuffle()を使わずに最初から c01b という並びで扱えばいいと思う。


違い二つ目、FilterActs()は正確にはConvolutionではなくcorrelationをとる(訳がわかんなかったですごめんなさい)ということなので

conv_op = FilterActs()
input_shuffled = input.dimshuffle(1, 2, 3, 0)  # bc01 -> c01b
filters_shuffled = filters.dimshuffle(1, 2, 3, 0)  # bc01 -> c01b
filters_flipped = filters_shuffled[:, ::-1, ::-1, :] # flip rows and columns
contiguous_input = gpu_contiguous(input_shuffled)
contiguous_filters = gpu_contiguous(filters_flipped)
out_shuffled = conv_op(contiguous_input, contiguous_filters)
out = out_shuffled.dimshuffle(3, 0, 1, 2)  # c01b -> bc01

とするらしい。コードを見る限り要素を反転させてるようです(見たらわかる)
ただフィルタを学習させる場合は向きとか関係ないので入れなくてよい。学習済みのモデルを使うときは必要ということか。


ここまで違いを書いてきたが、FilterActs()は使用するにあたっていくつか制限もある。conv2dとの主な違いを挙げる。

  • inputのchannelの数は3以下、もしくは4の倍数でなければならない。
  • filterは正方形でなければならない。画像データ以外を扱うときは注意?
  • filterの数は16の倍数でなければならない。
  • minibatchのサイズは何でもよいが、128の倍数の時に最もよい性能が出せる。
  • conv2dでいうところの"valid" convolutionしか対応していない。"full" convolutionしたい時は paddingで調節する。
  • FilterActs()はGPUでしか動かない。

実際に使う上で注意するのはchannel数とfilter数ぐらいか。画像データ以外を使うことも今のところないので大丈夫だと思われ。GPUしか対応してないのでコード移植時とかには気をつける。

とりあえず今回はここまで。次回にFilterActsオブジェクトの定義とかいろいろやる。