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

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

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

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の話 - デー

Kaggle Masterになりました

ビッグデータを世界の天才たちが紐解くKaggle における上位プレーヤーの称号 Kaggle Master を得ました。
Kaggle Masterを得るには、

1. 2つの公開コンペで上位10%に入る
2. そのうち1つは10位以内に入る

という条件があるのですが、4月に終った Large Scale Hierarchical Text Classification で3位、今日終った
Greek Media Monitoring Multilabel Classification (WISE 2014)で9位だったので、最短2コンペでストレート Kaggle Master を得ることになりました。

これにより、天才の中の天才、超天才データサイエンティースト悪魔男爵ultraistterさんであることが示されてしまったため、僕がここで意味不明なことを言っていた場合でも、「ハイハイワロスワロス」ではなく、「さすがKaggle Masterッス よく分からないけどマジスゲーッス」みたいな反応になるよう、よろしくお願いいたします。

ちなみに統計学は全くわかりません。

最近のautomakeでmake checkの挙動を戻す方法

開発環境をUbuntu 13.10にアップグレードしたら、自分が作っているライブラリのビルドでmake checkしたときの挙動が変わっていた。どうもautomakeのバージョンが上がったせいで挙動が変わってしまったようだ。
これまでは、テストプログラムがなんかいろいろ出力しながら実行されて、失敗するとエラー行を表示していたのが、テストプログラムの出力(stdout,stder)がファイルに書き出されて一切表示されなくなってしまった。

調べてみるとautomake 1.13からparallel test harnesとかいうのデフォルトになっていて、これについてはよく知らないけど、名前からして並列テストをするもので、並列実行しているプログラムの出力をターミナルに出すと混じってしまうので、ファイルに出すようになったのだと思う。

自分は並列テストを必要としていないので、とにかく過去のバージョンと同じになるように戻したい!

automake 1.13以降で過去のバージョンと同じようにテストプログラムを実行する方法

automakeのオプションにserial-testsを渡すと、make checkの挙動が過去のバージョンと同じになる。
これは、configure.acにおいてAM_INIT_AUTOMAKEの引数で指定すればよい。

AM_INIT_AUTOMAKE([foreign])

などとなっていた行を

AM_INIT_AUTOMAKE([foreign serial-tests])

と変更する。これでserial-test driverが使われるようになって、過去のバージョンと同じように動く。

ただし、これをautomake 1.11など古いバージョンで実行すると、

configure.ac:9: option `serial-tests' not recognized
autoreconf: automake failed with exit status: 1

などと言われてエラーになる。
serial-testsというオプションがないのだ。クッソ!

現在と過去、すべてにおいて過去のバージョンと同じようにテストプログラムを実行する方法

ひとつの方法として、automakeのバージョンを判定して、バージョンによりAM_INIT_AUTOMAKEの引数を変更するm4マクロを書くというのがある。
しかし、自分は、automakeの歴史についての知識に基づいたノウハウとか持ちたくないのと、そもそもautomakeのテスト機能を駆使してテストを書いていたわけでなく、make checkしたときに自分で書いたテストプログラムが実行されればそれでいいので、もうautomakeのテストの仕組みを使わないことにした。
ということで、TESTSでテストプログラムを指定するのをやめて、check_PROGRAMでビルドするテストプログラムを指定した後、check-localのフックでテストプログラムを実行するようにした。

Makefile.amで

check_PROGRAMS = oreore_test
TESTS = $(check_PROGRAMS)

などとしていた行を

check_PROGRAMS = oreore_test

check-local:
        $(builddir)/oreore_test$(EXEEXT)

と変更する。これでautomake 1.11と1.13で同じような感じで動くようになった。あまりいい方法とは思えないけど、ならどうすればいいのか...

L4D2の特殊感染者BOTのプレイスタイルをカスタマイズしてみた

@キモト L4D2のAI改造試したけど、なんかいろいろ方法考えた上でFakeClientのキー入力を書き換えることでBotの動きを制御する方法が簡単でいいのではと思った。server.soのdisassemble読んでExtension書くのはつらいし、いろんなcvarを参照しながらTeleportEntityで制御するのもつらそうだし、キー入力なら自分で普段やっていることであり簡単ではと。もっといい方法あるかもしれないけど。

ただNavMeshとかマップに関する情報が読めてないので、あまり高度なことはできてない。適当に動き回ったり、ターゲットの状態を調べて攻撃するか判定するくらい。

何かどれくらいできるか探りながら試しただけで、あまりよく考えてないカスタマイズです。