Torch7で最近傍探索のベンチマーク

最近、Torch7で最近傍探索を繰り返し行いたかったけど、すごく遅いのでは??という不安があったのでk-NN(k=1)でベンチマークしてみた。

設定

  • MNISTをk-NN(k=1)で評価する
  • 尺度はコサイン類似度とする
  • テストを全件評価してかかった時間を計測する

環境

  • Intel(R) Core(TM) i7-3770K CPU @ 3.50GHz
  • 32GB RAM
  • GeForce GTX 760

パッと思いついた実装

初めは難しいことは考えず、パッと思いついた方法を試してみる。

工夫としては、

  • コサイン類似度を求める際にベクトルのノルムを毎回計算したくないので、最初にノルムが1になるように正規化しておく(内積=コサイン類似度になる)
  • 各テストデータと各学習データの比較は、gemv(torch.mv)で一度に計算すれば速いのではないか
require 'optim'
require 'xlua'

torch.setdefaulttensortype('torch.FloatTensor')
local EPSILON = 1.0e-6

-- normを1に正規化
local function normalize_l2(x)
   local norm = torch.pow(x, 2):sum(2):sqrt():add(EPSILON)
   x:cdiv(torch.expand(norm, x:size(1), x:size(2)))
end

function main()
   local mnist = require 'mnist'
   local trainset = mnist.traindataset()
   local testset = mnist.testdataset()
   local train_x, train_y = trainset.data:float(), trainset.label
   local test_x, test_y = testset.data:float(), testset.label
   local classes = {0,1,2,3,4,5,6,7,8,9}
   local confusion = optim.ConfusionMatrix(classes)
   local t = sys.clock()
   
   -- データを整形
   train_x = train_x:reshape(train_x:size(1), 28 * 28)
   test_x = test_x:reshape(test_x:size(1), 28 * 28)
   train_y:add(1)
   test_y:add(1)
   
   -- L2 normを1に正規化
   normalize_l2(train_x)
   normalize_l2(test_x)
   
   -- 各テストデータについて
   for i = 1, test_x:size(1) do
      -- コサイン類似度が最も大きいインスタンスを選択
      local _, nn_index = torch.mv(train_x, test_x[i]):max(1)
      -- 結果を評価
      local y = train_y[nn_index[1]]
      confusion:add(y, test_y[i])
      if i % 100 == 0 then
	 xlua.progress(i, test_x:size(1))
      end
   end
   -- 結果を表示
   print(confusion)
   print(string.format("*** %.2fs", sys.clock() - t))
end
main()
結果
ConfusionMatrix:
[[     978       1       0       0       0       0       0       1       0       0]   99.796% 	[class: 0]
 [       0    1129       3       1       0       1       1       0       0       0]   99.471% 	[class: 1]
 [       9       0    1003       4       0       0       2      10       3       1]   97.190% 	[class: 2]
 [       0       0       1     977       0      13       0       5       9       5]   96.733% 	[class: 3]
 [       1       3       0       0     940       0       6       3       1      28]   95.723% 	[class: 4]
 [       1       1       0      17       1     852      10       1       4       5]   95.516% 	[class: 5]
 [       4       3       0       0       2       3     946       0       0       0]   98.747% 	[class: 6]
 [       2      11       5       2       2       0       0     995       0      11]   96.790% 	[class: 7]
 [       6       1       1      13       2       3       5       4     935       4]   95.996% 	[class: 8]
 [       5       6       1       4       9       3       1       8       4     968]]  95.937% 	[class: 9]
 + average row correct: 97.189832925797% 
 + average rowUcol correct (VOC measure): 94.583150148392% 
 + global correct: 97.23%
*** 95.11s	

95.11秒だった。正解率は97.23%。世の中にはMNISTをk-NNしてみたら数時間かかったとか言っている人も散見されるので、そう考えると速い気もする。

gemvではなくgemmで計算する

インスタンスひとつづずgemvしてたけど、全体をgemm(torch.mm, 行列の積)で計算すればもっと速くなるだろうと思ったので変更してみた。

require 'optim'
require 'xlua'

torch.setdefaulttensortype('torch.FloatTensor')
local EPSILON = 1.0e-6

-- normを1に正規化
local function normalize_l2(x)
   local norm = torch.pow(x, 2):sum(2):sqrt()
   norm:add(EPSILON)
   x:cdiv(torch.expand(norm, x:size(1), x:size(2)))
end

function main()
   local mnist = require 'mnist'
   local trainset = mnist.traindataset()
   local testset = mnist.testdataset()
   local train_x, train_y = trainset.data:float(), trainset.label
   local test_x, test_y = testset.data:float(), testset.label
   local classes = {0,1,2,3,4,5,6,7,8,9}
   local confusion = optim.ConfusionMatrix(classes)
   local t = sys.clock()
   
   -- データを整形
   train_x = train_x:reshape(train_x:size(1), 28 * 28)
   test_x = test_x:reshape(test_x:size(1), 28 * 28)
   train_y:add(1)
   test_y:add(1)
   
   -- L2 normを1に正規化
   normalize_l2(train_x)
   normalize_l2(test_x)
   -- 全てのテストデータについてコサイン類似度が最も大きいインスタンスを選択
   local cosine = torch.mm(train_x, test_x:t())
   local _, nn_index = cosine:max(1)
   -- 結果を評価
   for i = 1, test_x:size(1) do
      local y = train_y[nn_index[1][i]]
      confusion:add(y, test_y[i])
      if i % 100 == 0 then
	 xlua.progress(i, test_x:size(1))
      end
   end
   -- 結果を表示
   print(confusion)
   print(string.format("*** %.2fs", sys.clock() - t))
end
main()
結果
ConfusionMatrix:
[[     978       1       0       0       0       0       0       1       0       0]   99.796% 	[class: 0]
 [       0    1129       3       1       0       1       1       0       0       0]   99.471% 	[class: 1]
 [       9       0    1003       4       0       0       2      10       3       1]   97.190% 	[class: 2]
 [       0       0       1     977       0      13       0       5       9       5]   96.733% 	[class: 3]
 [       1       3       0       0     940       0       6       3       1      28]   95.723% 	[class: 4]
 [       1       1       0      17       1     852      10       1       4       5]   95.516% 	[class: 5]
 [       4       3       0       0       2       3     946       0       0       0]   98.747% 	[class: 6]
 [       2      11       5       2       2       0       0     995       0      11]   96.790% 	[class: 7]
 [       6       1       1      13       2       3       5       4     935       4]   95.996% 	[class: 8]
 [       5       6       1       4       9       3       1       8       4     968]]  95.937% 	[class: 9]
 + average row correct: 97.189832925797% 
 + average rowUcol correct (VOC measure): 94.583150148392% 
 + global correct: 97.23%
*** 12.41s

12.41秒だった。正解率は当然同じ。かなり速くなった。

CUDAでやってみる

Torch7はBLASでできるような計算なら簡単にCUDA化できるのでやってみた。

工夫として、

  • gemmは使用メモリが多すぎてGPUにメモリが確保できなかったので分割して計算するようにした
require 'cutorch'
require 'optim'
require 'xlua'

torch.setdefaulttensortype('torch.FloatTensor')
local EPSILON = 1.0e-6

-- normを1に正規化
local function normalize_l2(x)
   local norm = torch.pow(x, 2):sum(2):sqrt()
   norm:add(EPSILON)
   x:cdiv(torch.expand(norm, x:size(1), x:size(2)))
end

-- 行列の積を直接計算しようとするとGPUメモリに載らなかったので16分割して計算
local function split_mm(a, b)
   local BLOCKS = 16 -- 分割数
   local step = math.floor(b:size(1) / BLOCKS)
   local results = torch.Tensor(a:size(1), b:size(1))
   for i = 1, b:size(1), step do
      local n = step
      if i + n > b:size(1) then
	 n = b:size(1) - i
      end
      if n > 0 then
	 results:narrow(2, i, n):copy(torch.mm(a, b:narrow(1, i, n):t()))
      end
      collectgarbage()
   end
   return results
end

function main()
   local mnist = require 'mnist'
   local trainset = mnist.traindataset()
   local testset = mnist.testdataset()
   local train_x, train_y = trainset.data:float(), trainset.label
   local test_x, test_y = testset.data:float(), testset.label
   local classes = {0,1,2,3,4,5,6,7,8,9}
   local confusion = optim.ConfusionMatrix(classes)
   local t = sys.clock()

   -- データを整形
   train_x = train_x:reshape(train_x:size(1), 28 * 28)
   test_x = test_x:reshape(test_x:size(1), 28 * 28)
   train_y:add(1)
   test_y:add(1)
   
   -- L2 normを1に正規化
   normalize_l2(train_x)
   normalize_l2(test_x)
   
   -- 計算用のデータをCudaTensorに変換(GPUのデバイスメモリに転送)
   train_x = train_x:cuda()
   test_x = test_x:cuda()
   
   -- 全てのテストデータについてコサイン類似度が最も大きいインスタンスを選択
   local cosine = split_mm(train_x, test_x)
   local _, nn_index = cosine:max(1)
   -- 結果を評価
   for i = 1, test_x:size(1) do
      local y = train_y[nn_index[1][i]]
      confusion:add(y, test_y[i])
      if i % 100 == 0 then
	 xlua.progress(i, test_x:size(1))
      end
   end
   print(confusion)
   print(string.format("*** %.2fs", sys.clock() - t))
end
main()
結果
ConfusionMatrix:
[[     978       1       0       0       0       0       0       1       0       0]   99.796% 	[class: 0]
 [       0    1129       3       1       0       1       1       0       0       0]   99.471% 	[class: 1]
 [       9       0    1003       4       0       0       2      10       3       1]   97.190% 	[class: 2]
 [       0       0       1     977       0      13       0       5       9       5]   96.733% 	[class: 3]
 [       1       3       0       0     940       0       6       3       1      28]   95.723% 	[class: 4]
 [       1       1       0      17       1     852      10       1       4       5]   95.516% 	[class: 5]
 [       4       3       0       0       2       4     945       0       0       0]   98.643% 	[class: 6]
 [       2      11       5       2       2       0       0     995       0      11]   96.790% 	[class: 7]
 [       6       1       1      13       2       3       5       4     935       4]   95.996% 	[class: 8]
 [       5       6       1       4       9       3       1       8       4     968]]  95.937% 	[class: 9]
 + average row correct: 97.179394364357% 
 + average rowUcol correct (VOC measure): 94.562811851501% 
 + global correct: 97.22%
*** 5.12s	

5.12秒だった。爆速!

プランクトンの画像分類コンペが開催されています!

Kaggleでプランクトンの画像分類モデルの良さを競うコンペが開催されています。
Description - National Data Science Bowl | Kaggle

Computer VisionやDeep Learningに興味のある方、自分の力を試してみたい方、手頃なサンドバックが欲しい方などは参加されてみてはいかがでしょうか。

概要(超訳)
海の状態をプランクトンの種類や数の分布などから調べたい。プランクトンの識別を人間が行うのは大変なので水中カメラで撮影したプランクトンの画像から種類を予測するモデルを作ってもらいたい。
期間
2014/12/15〜2015/3/16
賞金総額
$175,000 (14/12/20: 約2090万円)
データ
学習画像: 約3万枚 (グレースケール/様々なサイズ/不均一), 対象クラス: 121種類(多クラス分類/確率出力/multi-class logarithmic lossで評価), テスト画像: 約13万枚 (チート対策で評価に使用されないデータもたくさん含む)

詳細は、サイトを参照ください。
注意することとして、テストデータを使っての半教師あり学習や教師なし特徴学習は許可されていますが、外部データを使うことは禁止されています。(Caffeのpre-trained ImageNet modelなどは使用不可)

賞金が多いことから、たくさん人が来ると思いきや、まだあまりいないようなので、宣伝を書きました。当然僕も参加しています。

Kaggle CIFAR-10の話

以前、Kaggle CIFAR-10 に参加していると書きましたが、これが2週間ほど前に終わりました。コンペはまだ Validating Final Results の状態なのですが、2週間たっても終わらず、いつ終わるのか謎なのと、多分結果は変わらないと思うので先に書きます。

CIFAR-10は、次のような32x32の小さな画像にネコ、犬、鳥など10種類の物体が写っているので、与えられた画像に何が写っているか当てる問題です。
f:id:ultraist:20141108185409p:plain
(Kaggle CIFAR-10のデータセットは、通常のCIFAR-10と結果の互換性がありますが、チート防止に画像のハッシュ値が変わるように改変されているのと、テストセットに29万枚のジャンクイメージが含まれています。)

自分の結果は、0.9415 (正解率94.15%)で、Classification datasets results によると、state-of-the-artが91.78%なので、それを上回って人間による精度である94%に達しているのですが、このスコアでなんと5位でした。1位はDeepCNetで、95.53%という驚異の精度を出しています。2位もDeepCNetとDropConnectの結果を合わせたものなので、DeepCNet最強だったという感じです(DeepCNetのコードはコンペ終了前に公開されていました)。3、4位は手法を公開していないので不明です。

自分の手法

kaggle-cifar10-torch7 - Github
でコードを公開しています。Torch7で実装しています。

オリジナル性が高いものはなく、よくある手法をいくつか組み合わせたのと、VGG(University of OxfordのVisual Geometry Group)がILSVRC2014で使ったモデルをCIFAR-10に調節しただけものです。

やったことは

  • 学習データを36倍に増化(Data Augmentation)
  • GCN + ZCA Whiteningで正規化
  • VGGのモデルをベースにしたConvolutional Neural Network(CNN)を学習
  • 上記のモデルを重みの初期値とMini-Batch-SGDの更新順を変えて6個学習し、各分類器の平均を予測として出力

です。

学習データを36倍に増化(Data Augmentation)

ニューラルネットワークは、経験的にはオーバーフィッティングしなければ層数や素子数が多いほどよいというのがあって、よりDeeeepしたいという思いはありますがデータが少ないとオーバーフィッティングしてしまうので、データを増して複雑なモデルでもオーバーフィッティングしにくいようにしました。
コツとしては、できるだけ"あり得る範囲"の変換により増やすということです。画像の場合、人工的なノイズや歪めたりでいくらでもパターンが増やせますが、テストセットに出てこないようなパターンで学習データの分布を歪めてしまうとよくないので、元のデータとは違うけどテストには出てくるパターンに変換できるのが理想です。
今回は次の3つのメソッドを使いました。

Cropping
CIFAR-10の学習画像は32x32ですが、これを24x24の部分画像に分解します。4px置きに切り出すと、3x3の9パターンが切り出せるのでデータを9倍に増やせます。(学習画像は小さくなります)
Scaling
Croppingでの切り出しサイズを28x28にして2px置きに切り出すと3x3の9パターンが切り出せます。この部分画像を24x24にリサイズ(ズームアウト)して学習画像とします。
Horizontal reflection
左右反転です。これまで増やした画像を左右逆の2のパターンに分けて2倍に増やします。

これで(9 + 9) * 2 = 36倍になります。学習画像は32x32ではなく、24x24になります。
予測時は、予測対象の画像を同じ方法で36倍に増やして、各画像に対して予測を行い、それらを平均して予測結果としています。当然、予測にかかる時間も増えます。
この処理によって、2〜3%くらい精度がよくなりました。

f:id:ultraist:20141108184751p:plain
1行目がCropping、2行目がCropping+Scaling、3、4行目がHorizontal reflectionです。

GCN + ZCA Whiteningで正規化

GCN(Global Contrast Normalization)は、standardizeとかz-scoreとか言われるものと同じで、データ全体から各要素の平均と標準偏差を求めて、平均を引いて標準偏差で割ります。入力の値域が-2〜2くらいに正規化されて、スケールの異なる軸があった場合でもその範囲にそろえられます。また、よく出る値は平均に近くなり、あまり出ない値は大きな絶対値を持つようになります。スケールの大きな軸の影響を抑えるのと、学習時の収束が速くなる効果があります。
2014/12/20追記
この説明は間違っていました。GCNは、画像をまたがずに、画像内での平均と分散を求めて平均を引いて分散で割るようです。これは、"An Analysis of Single-Layer Networks"の実装では、local contrast normalizationと書かれていた処理で、z-scoreはglobal standardizationと書かれていて、Maxoutの論文では、このlocal contrast normalizationがglobal contrast normalizationと書かれていたので、globalとlocalの概念がどこにあるのか混乱して勘違いしていました。ただ自分の実装では、このブログ通りのz-scoreを使っています。

ZCA Whiteningは、データ全体の分散共分散行列の固有ベクトルで主軸変換を行なって、変換後の空間でstandardizeを行なって元の空間に戻すというものです。自然画像は、あるピクセルはその近隣のピクセルと相関が強いという特徴があるので、この相関を消すことで色の情報を持ったままエッジ検出を行ったような結果が得られます。元の空間に戻すのは、CNNが元画像の構造を前提としているからです。
ZCA Whiteningは、An Analysis of Single-Layer Networks in Unsupervised Feature Learning - Andrew Ngですごくよい結果を出した前処理で、僕もこの手法を実装したことがあるのですが、この手法においてはZCA Whiteningをするかしないかで、CIFAR-10の精度が15%くらい変わります。ただ、最近のDeep CNNではほとんど差がでないので、必要なかったのではないかと思っています。その前のモデル(Network In Network)ではほんの少しだけ精度が改善できていたのと、外して変わらない精度が出るか試している余裕がなかったので、そのまま慣性で入れています。

VGGのモデルをベースにしたDeep Convolutional Neural Networkを学習

[1409.1556] Very Deep Convolutional Networks for Large-Scale Image Recognition で提案されているものをベースにしたDeep Convolutional Neural Networkで分類器を作りました。

伝統的なCNNでCIFAR-10用のアーキテクチャを作ると、conv 5x5 -> maxpool -> conv 5x5 -> maxpool -> conv 5x5 -> maxpool -> fc(fully connected) -> softmaxのようになるのですが、このconv 5x5の部分を3x3カーネルを2つか3つ並べたものに置き換えます。
これで

  • 層数が増える
  • 線形の大きなカーネル非線形(convごとにReLUを挟んでいるため)の3x3を並べたものに置き換えるので表現力が上がる
  • 大きなカーネルで一回畳み込むよりも小さなカーネル複数回畳み込んだほうが計算量が少ない(5x5 > 3x3x2, 7x7 > 3x3x3)

というような効果があります。また3x3カーネルに1pxのpaddingを加えると、畳み込み層によって画像サイズが縮小されなくなるので、理論上は無限に層数を増やせるようになります。これはCIFAR-10のような入力画像が小さい場合に嬉しいです(畳込みでサイズが減っていくと増やせる層数に限界があるため)。

最終的に使ったアーキテクチャは、

conv 3x3 -> conv 3x3 -> maxpool -> conv 3x3 -> conv 3x3 -> maxpool -> conv 3x3 -> conv 3x3 -> conv 3x3 -> conv 3x3 -> maxpool -> fc -> fc -> softmax

というDeeeepなものです。詳しくはソースコードのページに表を書いているので参照してください。

上記のモデルを重みの初期値とMini-Batch-SGDの更新順を変えて6個学習し、各識別器の平均を予測として出力

ニューラルネットワークを使ったモデルで簡単に精度を上げる方法として、いくつかのモデルを学習して平均を取るというのがあります。ニューラルネットワークは初期値依存があるのと大域最適化はされないので、乱数のseedが異なると(微妙に)異なる結果を出力するモデルが学習されます。なので、いくつか学習して平均を取ると結果が安定します。

よくやるのはBagging(Committee Network)ですが、Baggingはサンプリングの割合など調節しなければならないのと今回はこれをやろうと思ったのが終了3日前で、調節する余裕がなく一発勝負だったため、良くなることはあっても悪くなることはないだろうという考えで以下の設定で行いました。

  • 同じ学習データ
  • 異なる初期重み
  • 異なる更新順

また今回使ったモデルは学習に20時間程度かかり、単体マシンで学習していては2つしか学習できないので、EC2のSpot InstanceでGPU Instanceを6個たち上げて並行して学習しました。

結果的には、シングルモデルだと93.33%、6モデルの平均で94.15%だったので、この処理で0.85%改善できていました。

その他の話

VGGの論文が発表される前は、Network In Networkを使っていました。これは最終的には92.4%の精度を出せたので、悪くはなかったと思います。

最後の方では、このままではどうやってもDeepCNetに勝てないと思ったので、GoogLeNetを実装してみたのですが、学習がクッソ遅い上validationで88%しか出なかったので諦めました(調節が足りないのか、問題に合っていないのか、なにか間違っているのか分かっていない)。

この2つのモデルは、参考としてソースコードのディレクトリに置いてあります。(nin_model.luaとinception_model.lua)

lbpcascade_animefaceをgithubに置きました

見る前からスター !lbpcascade_animeface · GitHub

OpenCV用のアニメ顔検出器 lbpcascade_animeface をgithubに置きました。
内容は2011年から変わっていません。
妙なブログ記事ではなく参照しやすくなったのではないかと思います。
全宇宙に拡散するためスターをお願いします。
ちなにみこれはImage::AnimeFaceとは全く別のものです。

見た後にもスター !lbpcascade_animeface · GitHub

AfSIS 反省会

Kaggleで行われていたAfrica Soil Property Prediction Channgleに参加して、結果は1233チーム中148位だった。
反省会です!!

ちなみに参加していなかった人には何を言っているか全く分からないと思いますのでご了承ください。

今回のコンペは、yag_aysさんも書いているけど、public LBとprivate LBで、かなり大きな順位変動があって、public LB1位だった人が500位台まで飛んだり、上位を狙うのは極めて難しい内容だった。ただこれは結果が出る前から明らかだったことで、それを見越してprivate LBで上位に入るように考えていたんだけど、いろいろ予想が外れてしまい失敗してしまった。

オレの予想と現実の違い

予想
  • private 1位は0.46くらい
  • (この間に多くても20人くらい)
  • オレは0.48〜0.50くらい
  • Abhishekのコードは0.55以上
現実
  • private 1位は0.46892
  • (この間に150人くらい)
  • オレは0.50290
  • Abhishekのコードは0.50558

現実は厳しい!!

だいたい以下のようなことを考えていた。

  • 自分の投稿やフォーラムの話、Sentinel LandscapeベースのCVからpublic LBより0.1くらい悪いスコアになるだろう
  • public LB上位(0.36~0.38)のうち何人かは本当にうまくいっていて残るだろうから1位は0.46くらい
  • 自分の投稿は、Sentinel LandscapeベースのCVで0.48くらい、public LBの計算に使われている簡単なデータセットはprivateから除かれるのでこれよりちょっと悪くなるだろう
  • AbhishekのコードはSentinel LandscapeベースのCVで0.61、locationベースのCVで0.56くらいという話があったので(自分では検証していない!!)よくても0.55くらいだろう
  • 参加者の多くは、Abhishekのコードをベースにしているので0.5には届かないだろう

多くの人はprivate LBで爆死するので、爆死しないように気をつけておけばそれほど頑張らなくても上位に入れるだろうと考えていたんだけど、この予想が外れ、Abhishekのコードがなんと0.50というスコアを持っていたため、そこから少しでも改善できた人達150人くらいに抜かれてしまった。

反省点としては、他の人が勝手に爆死することを期待したり、Kaggleの天才データサイエンティスト達を舐めてたりといったふざけた態度でのぞまず、自分のスコアを上げる方向で頑張るべきだった。マル。

自分のソリューション

ちなみに自分の方法は以下のような感じだった。

  1. spectra特徴からCO2の領域を削除
  2. spectraにfirst derivative filterを適用
  3. spectraをPCA Whiteningで160次元に圧縮
  4. 圧縮したspectra特徴, spatial特徴(衛星から取ったやつ), depth(表層か下層か)を入力可能な特徴とする
  5. huber loss functionのneural networkで回帰
  6. neural networkのアーキテクチャやハイパーパラメーターはSentinel Landscapeに基づくCVで良い結果が出るように調節する (spatial特徴を使うか使わないか、depthを使うか使わないかの組み合わせも含めて)

1は、ホストが推薦していたことで、やったほうが結果もよかったのでやった。
2は、CVではやってもやらなくてもほぼ違いはなかったので迷ったけど、データをプロットしてみるとデータごとに異なる謎の直流成分(定数のズレ)が入っていて、どういう理由で入ったのか分からないけど、おそらくホスト側でデータを正規化したときに入ったんじゃないかと思ったのと、ホストが用意しているベースラインのベンチマークコードではfirst derivativeを使っていて、これを使うと直流成分は消えるので、消したほうが安心だろうと思って適用することにした。
3もやるか迷ったものだけど、正規化のパターンとして

  • z-socre
  • ZCA Whitening
  • PCA Whitening (512,256,160,128 component選択)

を試してみて、z-socreは一番結果が悪かったので無しで、ZCAとPCAはほんのちょっとだけZCAのほうがよかった。ただ、今回のデータは学習データが約1000件で入力が約8000次元あるので、できるなら圧縮したほうがいいだろうと思っていたのと、入力次元を少なくするとneural networkの学習時間がかなり短くなので、実験回数が増やせて、この微かな差は挽回できるのではないか、と考えて160次元に圧縮することにした。
結果からするとこれは失敗だった。ZCAバージョンも投稿していたんだけど、private LBではZCAバージョンのほうが微かによくて(0.4991)、こっちを選んでいれば95位くらいだった。(そんな変わらない)

4.はspectra特徴だけで学習するか、spatialやdepthも含めるかという話だけど、これは目的変数ごとに検証した結果以下のようにした。
Ca: spectra
P: spectra + depth
pH: spectra + spatial + depth
SoC: spectra + spatial + depth
Sand: spectra + depth

5.は、この問題には非線形回帰がよいことはちょっとやってみれば分かることで、非線形回帰ができるモデルとして自分の得意なneural networkを選んだ。重要なのは損失関数で、MSEではなくHuber lossを使うことにした。これは、学習データの目的変数に明らかに大きすぎる外れ値っぽいデータが少しだけ入っていて、MSEを最小化するとその影響がすごく出てしまうので、ロバストにしたほうがいいだろうということで選んだ。CVでもMSEよりHuber lossのほうが良い結果が出ていた。
ちなみにSupport Vector Regression(Abhishekのコード)が良い結果を出していた理由も、損失関数にsquared lossを使っていないことだと思っていて、というか、SVRで良い結果が出るという話が出たあとで、なぜSVRだけ良い結果が出るのか考えた結果、ε-sensitive lossのおかげだろうと思ったので、自分もそれに習って性質が似ていてneural networkで扱いやすそうなHuber lossを使ってみることを思いついた。

6.は一番重要なところで、CVのfoldをランダムサンプリングではなく、Sentinel Landscapeという単位を壊さないように分けるようにする。
これが重要な理由は、データページに書いてある。

The training and test data have been split along Sentinel Landscape levels because we are primarily interested in predicting soil properties at new Sentinel Landscapes.

http://www.kaggle.com/c/afsis-soil-properties/data

学習データとテストデータは、Sentinel Landscape levelsで分けられていると明確に書いてある。Sentinel Landscapeは土壌をサンプリングした時の一番大きい空間単位で、

60 Sentinel Landscapes
16 Sampling Clusters per Sentinel Landscape
10 Sampling Plots per Sampling Cluster
2 composite Soil Samples (topsoil & subsoil) per Sampling Plot

と書かれている。つまりこのコンペのデータは、階層構造を持っているので、データ単位でランダムサンプリングして同一Sentinel Landscape内のデータを学習/テストに分けてCVしてしまうと、最終スコアリングに使われる学習/テストと異なる条件になってしまって、おそらく同一Sentinel Landscapes内でオーバーフィッティングしていたほうがいいスコアを出すデータセットができてしまう。なので、CVでは、同一Sentinel Landscape内のデータが学習とテストに分かれないように注意してfoldを分ける必要がある。
ちなみに各データがどのSentinel Landscapeに属するかを表すIDは振られていないので(データの説明に書くならふれよ!!と思った)、TMAPという項目をキーとして使う。TMAPは年間降水量かなにかの変数だけど、衛星から取ったデータの空間解像度の悪さからこの値をキーとしてデータをまとめるとほぼSentinel Landscapeの単位でまとめれるという議論がフォーラムであって、その話をしている人達はかなり信頼できる人達だったので、オレもこれを使うようにした。

コンペ後のフォーラムを見ると、上位のほうがみんなSentinel LandscapeベースのCVをしていたってことはないようだけど、ただ少なくともlocationベース(同じ位置から取られたSubsoil/Topsoilのデータが学習/テストで別れないようにする)でやらないと、オーバーフィッティングしてたほうが高いスコアが出てしまうので、順位落とした人の多くは間違った条件のCVでハイパーパラメータを調節した結果、元になったAbhishekのコードよりも悪い結果になってしまったのでは、と思います。

最後にpublic LBが信頼できないだろうと思った理由。

  • 計算に使っているデータが少なすぎる(90件くらい)
  • CVに比べて異常によいスコア(public LB: 約0.40, CV: 約0.50、しかもこの超簡単なデータセットはprivate LBの計算から除外されるので、private LBはいっそう難しくなる)
  • CVと負の相関すらあるように見える!!
  • 信頼できそうな人達がみんな信頼できないと言っていた(重要)

追記

なんとかツリー系、入力が信号で説明変数間に強い相関があるのと、次元に対して学習データが少なすぎるのとで、変数選択がうまくいかなくて、決定木をベースとしたものは良い結果が出せなかったのでは、と思っているけど詳しくないので分かりません。
ただ100位以内の多くは、SVRGBMの結果を混ぜたものみたいです。

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

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

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

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

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

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

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

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

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

以上です。

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())
   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]

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