CIFAR-10でstate of the artのスコアが出せる、インターネットに落ちている中で最強のコード

DecMeg2014をやっているときにCUDA使いて〜と思うことがあったので、最近、GTX760というGPUを購入して、Kaggle PlaygroundのCIFAR-10(有名な物体認識のデータセット)で試していたのですが、CIFAR-10のstate of the artである0.912を微妙に超える精度(0.9173)が出せるようになったのでソースコードを公開します。

nagadomi/kaggle-cifar10-torch7 · GitHub

この結果は、"ベンチマークサイト Kaggle"で現在3位にランキングされています。

内容的には特に面白いことはしていなくて、cropping,scaling,horizontal reflectionで学習データを180万件(36x)まで増殖させたあとでNIN(Network In Network)というConvolutional Neural Networkの畳み込み層をMLPにしたモデルを学習しているだけです(層数等、論文実装とは違います)。
データ増やすしすぎだろ、と思うかもしれませんが、自分が試した感じだと、NINを使うとデータを増やせば増やすほど精度が上がります。またNINは層数が多くなるので、データが少ないと学習がうまくいかないというのもあります。学習データが1万件程度だと全く学習できません(ほとんどのクラスの精度が0%になってしまう)。そういうわけで、めちゃくちゃデータを増やして学習するので、学習時間がめちゃくちゃかかります。CUDAを使って1 epochに90分くらいかかり、10 epochくらい回さないと精度が上がりらないので、15時間くらいはかかります。

実装は、Torch7で行なっています。Torch7は、Pylearn2やcaffe、cuda-convnetなどと比べるとマイナーなNeural Networkのライブラリですが、Neural language modelsというパッケージ(nn)を使うと複雑な構造のNeural Networkも自然なコードで書けるので、プログラミングが得意な人にとっては、最強の開発環境ではないかと個人的には思っています。DecMeg2014では、Torch7で書いた複雑奇怪なNeural Networkで5位になっているので、実用上問題無いレベルで使えると思います。
ただ、CUDAの実装は微妙なものが多く、例えば、SpatialMaxPoolingのCUDA実装はkernelとstrideのサイズが同じじゃないと動かなかったりします。kernelをstrideより大きくするoverlapping poolingは画像認識において精度が向上できることが知られていて、cuda-convnetのサンプルなど精度を重視している実装はほとんどこれを使っているのですが、Torch7が対応してないので、このコードでは使っていません。
ということで、まだまだ改善の余地はあるので、興味ある方はこのコードの知見を活かし、Kaggleで2位になったあとソースコードgithubに置いておいてください。
(1位はたぶん無視していい存在だと思う)

追記 (2014/8/28)

epoch 20まで増やしたら、0.92210で現在2位になった。
時間かかってもちゃんと実験するべきだった。

追記 (2014/11/8)

最終結果を書きました。ソースコードの内容も変更されています。
Kaggle CIFAR-10の話 - デー

Torch7で複雑なモデルを書くときに便利な技

Torch7はnnに用意されている部品を組み合わせることで複雑なモデルを作れて便利なのですが、複雑なモデルは入力ベクトル(orテンソル)を様々形に変換しながら実行するので、どの時点でどのサイズになっているか分からなくて書くのが難しいというのがあります。
たとえば、伝統的なCNNは、Torch7で以下のように書くのですが

require 'nn'
function cnn_model()
   local model = nn.Sequential()
                                                                
   -- convolution layers                                        
   model:add(nn.SpatialConvolutionMM(3, 128, 5, 5, 1, 1))
   model:add(nn.ReLU())
   model:add(nn.SpatialMaxPooling(2, 2, 2, 2))

   model:add(nn.SpatialConvolutionMM(128, 256, 5, 5, 1, 1))
   model:add(nn.ReLU())
   model:add(nn.SpatialMaxPooling(2, 2, 2, 2))

   model:add(nn.SpatialZeroPadding(1, 1, 1, 1))
   model:add(nn.SpatialConvolutionMM(256, 512, 4, 4, 1, 1))
   model:add(nn.ReLU())

   -- fully connected layers                                    
   model:add(nn.SpatialConvolutionMM(512, 1024, 2, 2, 1, 1))
   model:add(nn.ReLU())
   model:add(nn.Dropout(0.5))
   model:add(nn.SpatialConvolutionMM(1024, 10, 1, 1, 1, 1))

   model:add(nn.Reshape(10))
   model:add(nn.SoftMax())

   return model
end

このSpatialConvolutionMMの畳み込みカーネルが入力サイズをはみ出していないかとか、SpatialMaxPoolingの入力が奇数になっていて端っこが処理されていないのではないかとか気になります。
こういう場合、以下のようなデバッグプリントを入れると、各層でどのようなサイズになっているか分かります。

require 'nn'
function cnn_model()
   local model = nn.Sequential()
   local debug_input = torch.Tensor(3, 24, 24):uniform()

   -- convolution layers                                    
   model:add(nn.SpatialConvolutionMM(3, 128, 5, 5, 1, 1))
   model:add(nn.ReLU())

   -- model:cuda() -- 必要ならcudaにする
   print(model:forward(debug_input):size())
   model:add(nn.SpatialMaxPooling(2, 2, 2, 2))
   print(model:forward(debug_input):size())

   model:add(nn.SpatialConvolutionMM(128, 256, 5, 5, 1, 1))
   model:add(nn.ReLU())
   print(model:forward(debug_input):size())
   model:add(nn.SpatialMaxPooling(2, 2, 2, 2))
   print(model:forward(debug_input):size())

   model:add(nn.SpatialZeroPadding(1, 1, 1, 1))
   model:add(nn.SpatialConvolutionMM(256, 512, 4, 4, 1, 1))
   model:add(nn.ReLU())
   print(model:forward(debug_input):size())

   -- fully connected layers                                
   model:add(nn.SpatialConvolutionMM(512, 1024, 2, 2, 1, 1))
   model:add(nn.ReLU())
   model:add(nn.Dropout(0.5))
   print(model:forward(debug_input):size())
   model:add(nn.SpatialConvolutionMM(1024, 10, 1, 1, 1, 1))
   print(model:forward(debug_input):size())

   model:add(nn.Reshape(10))
   model:add(nn.SoftMax())

   return model
end
cnn_model()

実行結果

% th t.lua

 128
  20
  20
[torch.LongStorage of size 3]


 128
  10
  10
[torch.LongStorage of size 3]


 256
   6
   6
[torch.LongStorage of size 3]


 256
   3
   3
[torch.LongStorage of size 3]


 512
   2
   2
[torch.LongStorage of size 3]


 1024
    1
    1
[torch.LongStorage of size 3]


 10
  1
  1
[torch.LongStorage of size 3]

各層で出力がどのようなサイズになるかは、ちゃんと計算すれば分かるのですが、僕のように暗算を得意としない人間には計算するのが非常にだるいのでデバッグプリントを入れる技がとても便利です。

CNNで各層にzero paddingを入れる意味

いろいろなCNNの実装を見ていると、畳み込み層の前にzero padding(Torch7だとSpatialZeroPadding)を入れているものが多くて、自分も使っているのですが、これにどんな意味があるのが正直良く分かっていないので、詳しい文献などあれば紹介してください。以上。

自分で使っていてこれで終わるのもアレなので自分の理解。

悪い点: 入力にzero padding入れると変なデータを学習してしまうのではないか?

いろいろな実装参考にしたのと、自分で試した感じだと、畳み込みカーネルサイズの半分以下のpaddingは特に悪い影響はないようです。
逆に、端っこにしか出てこないパターンを識別しやすくなるのでは?とか。

良い点: 畳み込み演算の回数が増えるのでパラメーターの更新が多く実行される

weight sharingしているカーネルのパラメーターは入力画像に対する畳み込み演算の回数だけ更新されるので、paddingにより入力サイズが増えると1回のbackwardで更新されるパターンが増えて、overfittingしにくくなるのではないか。

良い点: 大きなカーネルを使ったり、層数を増やしたりできる

CNNでは畳込みとpoolingを行うごとに入力のサイズが小さくなっていくので、(特に)最初の入力画像が小さい場合に、大きなカーネルを使ったり層数を増やすことができない。zero paddingでサイズを水増していくと、大きなカーネルで畳み込んだり、層数を増やしたりできる。

以上です。