まんぼう日記

takataka's diary

Theano で GPU 使って Convolutional Neural Net

大石東バイパスとTheanoでGPGPU - まんぼう日記 では,Theano ( Welcome — Theano 0.7 documentation ) で多層パーセプトロン(MLP)の学習プログラムを作り,GPU上で動かすとCPUのみの場合よりも速いことを確認しました.今度は,Convolutional Neural Net (CNN) です.

 

 

これまでの経緯

 

Theano で MLP & CNN (3) - まんぼう日記 で,CIFAR-10 という画像データセット( CIFAR-10 と ZCA whitening - まんぼう日記 )を用いて画像識別の実験を行いました.Theano を用いて MLP と CNN の学習プログラムを作成し,CPU上で動かしてみたところ,CNN の方が識別率はよいものの,学習に長時間かかってました.GPGPU したらだいぶ速くなるはず,ってことはわかってたけど,機材も時間もなくて中断….

 

してましたが,GPGPU用に新しいPCを購入して,Theano で書いたプログラムを GPU 上で動かせる環境を整えました(その辺の詳しいことは,続 GPGPU用マシンにCUDAをインストール - まんぼう日記 とそのリンク先へどうぞ).で,まずは CIFAR-10 じゃなくて MNIST で,かつ MLP の場合のみ実験してみて,GPGPUの効果を確認したのでした( 大石東バイパスとTheanoでGPGPU - まんぼう日記 ).

 

それから,Theano のバージョンが 0.7 になって,stride size を指定して overlap ありの pooling ができるようになったので,CIFAR-10 を学習する CNN で実験してみたりもしました( Theano 0.7 / 桜定点観測(5) - まんぼう日記 ).ところが,オーバーラップありにすると CPU ではものすごい時間がかかって使いものにならへんかったのでした.

 

さて,GPU 使ってみたらどうなるでしょう…

 

CNN に CIFAR-10 を識別させる実験

実験条件

実験に使った PC の仕様等は上記のリンク先をたどると見つかるので省略.GPUを使った実験は全て Tesla K20C 上で行いました.プログラムはこんなん:

  1. cifar10.py   実験データの読み込み.
  2. nnet150712.py   ニューラルネットとその学習のプログラム.大石東バイパスとTheanoでGPGPU - まんぼう日記 時の nnet150612.py とほぼ同じなんですが,細かい修正があります(後述の「補足」参照).
  3. convnet150712.py   2. を用いたCNNのプログラム.以前の同様のプログラム( Theano で MLP & CNN (3) - まんぼう日記 の convnet0211.py )を GPGPU 向けに修正( T.dmatrix → T.matrix とか,詳細は上記リンク先参照)し,2. に合わせたもの.
  4. cifar10_convnet150712.py   実験用プログラム. Theano で MLP & CNN (3) - まんぼう日記 の cifar10_convnet0211.py を元に,2,3,4 に合わせて修正(「補足」も参照).

で,いくつかの条件で識別率と実行時間を測り,比較してみました.使ったCNNの代表的な構造は,入力の方から順に,Convolution層ーPooling層ーReLu層ーSoftmax層というもので,各種パラメータは次の通り.

  • 入力: 3チャンネル x 32画素 x 32 画素
  • Convolution層:  カーネルサイズ 3 x 5 x 5 で1画素ずつずらしならが畳み込み,チャンネル数 16
  • Pooling層: max-pooling で縦横1/4にサブサンプリングしてから ReLu (しきい値はチャンネル毎),stride size は窓と同じ (4, 4).
  • ReLu層: 素子数 1000
  • Softmax層:  10クラス識別なので素子数 10

コスト関数は交差エントロピー,学習は Stochastic Gradient Descent + 慣性項 + weight decay.入力は,ZCA whitening してます( CIFAR-10 と ZCA whitening - まんぼう日記 ).

 

実験結果

CPU と GPU での実行時間の比較

実験条件に書いた通りのCNNを CPU と GPU (Tesla K20C) のそれぞれで動かしてみました.

 

CPUの場合:

$ time THEANO_FLAGS='device=cpu,floatX=float32' python cifar10_convnet150712.py
##### CIFAR-10 #####
# label_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
# num_vis =  3072
### Conv-Pool-ReLu-Softmax   Xdim: (3, 32, 32)
#   W1dim: (16, 5, 5)  ds1: (4, 4)  st1: None  H1: 784
#   H2: 1000
# eta =  0.01  mu =  0.9  lam =  0.0001
# ZCAwhitening =  True
### training:   NL =  40000  NV =  10000  K =  10  batchsize =  100
0 | 2.3026 8.96 | 2.3026 8.79
1 | 1.4223 50.38 | 1.4358 49.84
2 | 1.0853 62.79 | 1.1394 61.08
3 | 0.8889 69.49 | 0.9934 65.85
4 | 0.8075 71.92 | 0.9680 66.17
5 | 0.6671 77.52 | 0.8897 69.93
10 | 0.2392 93.17 | 0.9336 72.15
20 | 0.0036 100.00 | 1.3997 74.07
30 | 0.0021 100.00 | 1.4388 74.38
40 | 0.0018 100.00 | 1.4306 74.35
50 | 0.0017 100.00 | 1.4136 74.36
# NT =  10000
50 | 0.0017 100.00 | 1.4136 74.36 | 1.4586 73.69  ★

real    61m45.751s
user    140m13.956s
sys    572m17.481s

 

GPUの場合:

$ time THEANO_FLAGS='device=gpu,floatX=float32' python cifar10_convnet150712.py
Using gpu device 0: Tesla K20c
##### CIFAR-10 #####
# label_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
# num_vis =  3072
### Conv-Pool-ReLu-Softmax   Xdim: (3, 32, 32)
#   W1dim: (16, 5, 5)  ds1: (4, 4)  st1: None  H1: 784
#   H2: 1000
# eta =  0.01  mu =  0.9  lam =  0.0001
# ZCAwhitening =  True
### training:   NL =  40000  NV =  10000  K =  10  batchsize =  100
0 | 2.3026 8.96 | 2.3026 8.79
1 | 1.4229 50.40 | 1.4365 49.81
2 | 1.0855 62.82 | 1.1383 61.13
3 | 0.8846 69.64 | 0.9906 66.20
4 | 0.8061 72.12 | 0.9695 66.41
5 | 0.6656 77.41 | 0.8899 69.91
10 | 0.2346 93.19 | 0.9377 72.25
20 | 0.0036 100.00 | 1.4086 73.96
30 | 0.0021 100.00 | 1.4484 74.06
40 | 0.0018 100.00 | 1.4435 74.14
50 | 0.0017 100.00 | 1.4242 74.11
# NT =  10000
50 | 0.0017 100.00 | 1.4242 74.11 | 1.4468 73.85  ★

real    10m19.188s
user    16m0.204s
sys    2m7.652s

 

★をつけた行の右端がテストデータに対する正答率,real が実際の経過時間.速度差は歴然です.

 

Conv層のチャンネル数の影響

ここからは GPU の場合のみ.Conv層のチャンネル数を 16 から 64 と 256 に増やしてみました.

#   W1dim: (16, 5, 5)   ← チャンネル数 16
50 | 0.0017 100.00 | 1.4242 74.11 | 1.4468 73.85
real    10m19.188s

#   W1dim: (64, 5, 5) 
50 | 0.0015 100.00 | 1.2193 76.40 | 1.2745 75.92
real    13m1.046s

#   W1dim: (256, 5, 5) 
50 | 0.0014 100.00 | 1.1393 77.30 | 1.1736 75.99
real    20m53.176s

CPUで動かした時は,学習時間がチャンネル数に比例して延びてましたが,こちらは延び方がずいぶん緩やかです.GPUの並列性が活きてるってことでしょう.

 

全結合のReLu層をもう1層追加してみる

標記の通り,ReLu層をもう1つ追加して,Conv - Pool - ReLu - ReLu - Softmax としてみました.ReLu層の素子数は2層とも1000.Conv層のチャンネル数は 64 です.

### Conv-Pool-ReLu-ReLu-Softmax   Xdim: (3, 32, 32)
#   W1dim: (64, 5, 5)  ds1: (4, 4)  st1: None  H1: 3136
#   H2: 1000  H3: 1000
50 | 0.0005 100.00 | 1.5674 76.66 | 1.5969 75.51
real    13m39.069s

層を増やした効果は見えませんでした.それから,これは CPU での実験でも観察されたことですが,全結合の層を1つ追加しても学習時間はほとんど変わりません.Conv層+Pool層の計算量が支配的ってことでしょう. 

 

stride size を指定して,overlap した pooling をさせてみる

ReLu層は1つに戻して,overlap pooling するようにしてみました.max_pool_2d の st を None から (2,2) に(cf.  downsample – Down-Sampling — Theano 0.7 documentation ).すると,1 epoch に30分もかかるように….GPU使ってることになってますが,使用率はほぼ 0% ….だめだこりゃ,ってわけで実験中止.Theano 0.7 / 桜定点観測(5) - まんぼう日記 の時に CPU で見たのと同じ状況っぽいです.

 

かわりに,poolingの窓サイズを小さくしてみる

上記の条件のかわりに,poolingの窓のサイズ ds を (4,4) から (2,2) にしてみました.Pool層の出力の数がほぼ4倍になる,ってことでは上記の条件とほぼ同じです.

#   W1dim: (64, 5, 5)  ds1: (2, 2)  st1: None  H1: 12544
50 | 0.0010 100.00 | 1.3966 73.16 | 1.4216 72.72
real    15m55.100s

識別率下がっちゃいました.max-poolingによって平行移動に対する不変性を獲得してるので,窓が小さくなって大きなずれに対応できなくなったのでしょう.

 

考察のようなもの

とりあえず,予想通りですが,Theano による CNN では GPU 使うと学習が大幅に高速になることがわかりました.conv – Ops for convolutional neural nets — Theano 0.7 documentation によると,NVIDIA cuDNN – GPU Accelerated Deep Learning を使えるようにしたりすることでさらに高速化が見込めるようです.

 

overlap pooling の方は…謎です.もう少し調べてみる必要がありそう.

 

補足

今回の実験用にプログラムを書いてる途中で気づいて修正した点をメモ.

 

一つ目.MLPでもCNNでも,コスト関数には交差エントロピーを用い,Theano の categorical_crossentropy 関数で計算してます( cf. nnet – Ops for neural networks — Theano 0.7 documentation ).この関数の第2引数 true_dist には,教師信号を,(1) (データ数)x(クラス数)の2D Tensor か,(2) データ数個の整数値から成る Vector か,いずれかの形式で指定します.(1) の場合,クラス数方向にはいずれか1つの要素だけを1として他は0とします.(2) の場合,所属クラスの番号を整数値で指定します.たかたかは,これまでは (1) の方を採用し,浮動小数点の 2D Tensor に 0-1 の値を入れて使ってました.ところが,GPU上でCNNを動かせるようになって実験を繰り返していると,学習がある程度進んだ段階で,交差エントロピーが nan になってしまうケースが発生するようになりました.

 

どうやら,シグモイドの対数が \( -\infty \) になるのを防ぐ - まんぼう日記 の議論(あちらは binary_crossentropy ですが)と類似の問題のような気配.で,(2) の方を採用したら解決する気がして,やってみたら,見事解決.そういうわけで,nnet.py とか convnet.py を修正しました.

 

二つ目.cifar10_convnet.py を見るとわかりますが,学習途中に,学習データと検査データで交差エントロピーと識別率を計算して表示するようにしてます.今回実験してたら,そこで GPU のメモリが足らへんって怒られちゃいました.この部分,これまで,ネットワーク出力を計算する関数にデータ行列をまるごと渡してたんですね.そらあかんわ.データ数4万個とかあるし (^^; というわけで,この部分も学習時と同様ミニバッチに小分けして処理するように修正しました.大石東バイパスとTheanoでGPGPU - まんぼう日記 の時も同じ問題があったんやけど,あっちの方はMNISTで少しデータサイズが小さかったおかげで GPU のメモリにおさまって,顕在化せえへんかったんですね.効率の悪いことになってました.ついでに,学習データ分の交差エントロピーと識別率の計算を学習時にやってまうようにすれば二度手間んならずに済むんですが,そっちは未対処.いつか本気になったら考えます (^^;