waifu2xの更新(1)

学習済みモデルファイルを更新しています。
まだ今後の準備のためにソースコードを更新するついでに手元のモデルを更新しただけなので、そのうちまた更新されます。

デモサーバーには反映しているので少し変換結果が変わっています。
サードパーティのローカルソフトウェアを使っている方は、modelsフォルダにあるjsonファイルを上書きすれば使えるのではないかと思いますが、ソフトによっては動かないかもしれません。
何が変わっているかは画像によりますが、自分の意図としては、

  • ノイズ除去でできるだけ細部が消えないようにする(ほとんど変わっていないけど、頬の///は消えにくくなった)
  • 拡大時に線画の線が太くなり過ぎないようにする(画像によってはかなり変わっている)
  • 漫画のことは諦めた(スクリーントーンは前のバージョンよりひどくなっている場合が多い)

学習データとその生成方法から間接的に調節しているので、変更は分かりにくいです。検証用ベンチマークの数値的にはほとんど変わっていません。

またcuDNNの対応をやめて、Torch標準のCUDA実装を使うようにしたので、cuDNNは不要になりました。理由としては、速度的にそれほど変わらないのと、配布時にライセンスの問題があるためです。

二次元画像を拡大したいと思ったことはありませんか?

うまくできましたか? ボヤけたり、ギザギザになったりしませんでしたか?

waifu2xをお試しください。



(ブラウザの処理に影響されないようクリックで拡大おねがいします)

waifu2xは、二次元画像を2倍に拡大するソフトウェアです。多くの二次元画像についてスゴイ級のクオリティで拡大できます。

waifu2xは、最新鋭の人工知能技術 Deep Convolutional Neural Networks を使って開発されました。
waifu2xの人工知能は、次の問に答えます。

  • いまから与える画像はある画像を半分に縮小したものである。縮小される前の画像を求めよ。

画像を拡大するのではなく、縮小される前の状態に戻します。
縮小されてないオリジナル画像を与えた場合も、やはり縮小される前の画像を答えます。
その画像は本来存在しないものですが、waifu2xはそれを想像で創ります。

二次元画像のJPEG劣化に悩まされていませんか?

JPEGノイズが気になりますか? Google画像検索を使ってもノイズのある画像しか見つかりませんでしたか?

waifu2xをお試しください。




(ブラウザの処理に影響されないようクリックで拡大おねがいします)

waifu2xは、二次元画像のJPEGノイズを取り除くソフトウェアです。多くの二次元画像についてスゴイ級のクオリティでノイズを除去できます。
waifu2xは、最新鋭の人工知能技術 Deep Convolutional Neural Networks を使って開発されました。
waifu2xの人工知能は、次の問に答えます。

  • いまから与える画像はある画像を任意の圧縮率でJPEG圧縮したものである。JPEG圧縮される前の画像を求めよ。

ノイズを取り除くのではなく、画像をJPEG圧縮される前の状態に戻します。
JPEG圧縮される前の画像がインターネット上に存在しない場合も、やはりJPEG圧縮される前の画像を答えます。
JPEG非可逆圧縮と呼ばれる一度圧縮してしまうと元の状態には戻せない形式ですが、waifu2xは二次元画像の知識を使って逆変換を行います。

waifu2xはなんだかスゴそうです。でも完璧ですか?

waifu2xは実際スゴイですが、完璧ではありません。



(ブラウザの処理に影響されないようクリックで拡大おねがいします)

画像を見て、髪飾りに注目してください。

waifu2xの出力では、髪飾りに数珠めいた模様が刻まれています。他の資料で確認したところ、初音ミクの髪飾りにそのような模様はありません。
waifu2xは、縮小される前の画像には髪飾りに数珠めいた模様があったと解釈して、その模様を復元しましたが、それは間違いだったということです。
waifu2xは、多くのケースでよい改変を行いますが、勘違いで余計なことをしてしまうことがあります。

より悪い例としてマンガのスクリーントーンがあります。
waifu2xは、マンガのスクリーントーンを謎めいた模様に変換することがあります。
ただその謎めいた模様を半分に縮小すると、不思議なことに元のスクリーントーンに戻ります。



(ブラウザの処理に影響されないようクリックで拡大おねがいします)

waifu2xは、我々人間の感覚からすると間違っているように見える変換を行うことがありますが、検証してみると間違ってはいません。
「縮小される前の画像」をどのように想像するか、waifu2xと我々人間の間で感覚の相違があるようです。この溝は今のところ埋められていません。

こういったリスクがあることもあり、waifu2xは特に画像の色合いを変えないように制限されています。代償としてヤバイ級のJPEG劣化で虹色に変化してしまった部分は修復できません。

参考

waifu2xは、SRCNN[1]にインスパイアされて開発されました。細かい違いはたくさんありますが、大まかな枠組みはSRCNNです。
初音ミクの画像は、piapro[2]よりCC BY-NCで公開されているものを使用しています。
マンガのテクスチャはスクリーントーン風にワンダラー&SAI用テクスチャー素材[3]から引用しています。

アクセス

waifu2xは以下のURLから利用できます。

waifu2x.udp.jp

現在サーバーが1台しかなく、なかなか重い処理(1件あたり1〜5秒程度)なので、多くの方が同時に試すとレスポンスが返ってこなくなるかもしれません。
レスポンスが返ってこない場合は、少し時間をあけて試してください。

またソースコードgithubで公開されています。

waifu2x - github.com

waifu2xのコードはLuaJIT/Torch7を使って開発されました。

Public AMI

EC2用の環境構築済みのイメージを公開しています。

Public AMI

追記

Firefoxだと"画像を保存"で変換後の画像が保存できていないようです。
CTRL+Sか、ALTキーを押して、ファイル→ページを保存(Save Page As..) だと保存できるようです。

イルルカの感想

ゲームしたいストレスで、e-ショップを見てたらドラゴンクエストモンスターズ2 イルとルカの不思議なふしぎな鍵があったのでやりました。
ドラクエ好きでもモンスターズシリーズはやっていない人が多い気がしますが、イルルカは前作のテリーと違って普通の広めのマップでドラクエっぽいストーリーを進めるようになっているのでお勧めできます。
難易度は、特に攻略サイトとか見なくてもストーリー上で引っかかるところはなかったし、あまり全滅もしなかったので、難しくはないと思いますが、前作はボス前に必ず回復ポイントがあったのが今作ではなくなっているので、MP回復アイテムを買うか雑魚用とボス用のパーティを分けて育てるようにしないとつらくなる気もします。
また最近のドラクエでは、クリア後の世界というのがありますが、僕はこれあまりやる意欲が沸かないなのですが、イルルカではクリア後というか前半・後半だろコレ!というくらいスタッフロール後も普通のストーリーが続いていてよかったです。たぶん前半が難しいとすぐ投げるライト層向けで、後半がレベル上げとか育成とか地道な作業をやらないとクリアできないゲーマー向け仕様じゃないかと思います。
裏ボスを倒すとさすがに対戦しかやることがないですが、毎週マスターズGP(なんでもあり)、チャレンジャーズGP(モンスターのサイズや強さなどに制限あり)というAI戦(手動入力なし)が開催されていて、今でも2000人くらいは参加しているので、AI同士の対戦とか好きな人は楽しめると思います。対人戦は、wifiで段位戦とフリー対戦というのがあって、いまでも夕方くらいにやればできます。またwifiで対戦するとすれ違いができて、すれ違った人から鍵(ボーナスマップようなもの)やモンスターが貰えるので、表クリア後くらいからやるのがいいと思います。表クリア後がいい理由として、2回ダイヤモンドスライムを倒すとレベルがカンストとするような鍵が出回っているので、これを前半でやってしまうと簡単すぎてつまらなくなるのではと思うからです(ただダイヤモンドスライム自体が結構強いので倒せないかもしれない)。後半は、レベルがカンストしててもパーティの構成などがんばらないと裏ボスは倒せないと思うので、対戦をやりながらクリアするのがいいのではないかと思います。

僕の対戦成績は、前回が、マスターズGP 600位/2300、チャレンジャーズGP 300位/1300くらいでした。あまりよくないですけど、発売して1年以上たってて、ずっとやり続けているガチ勢が多いのではと思います。wifi戦は、GP用パーティーのテストとして段位ベテランマスターまではやったのですが、手動入力なので「おまえそれはねーだろ」みたいな手を使ってくる人が多くて、AIだけだとつらいし、手動入力を考えると、GP用とは違うスキル構成のほうが強いと思うので――とか考えると、GPだけやるのがいいかなと思って、あまりやっていません。ただ、ウルトラガードSPみたいなスキルはたくさん欲しいので、たまにやっているいう感じです。

ということで、発売してかなりたちますがまだ楽しめるのでオススメです。

National Data Science Bowl 10位

プランクトン画像分類コンペこと National Data Science Bowl が終わりました。
Description - National Data Science Bowl | Kaggle

結果はタイトル通り10位(1049チーム中)でした。賞金2000万円が1円も貰えなかったショックで毎日15時間くらい寝ています。感想と投稿内容です。

感想

世界Deep Convolutional Neural Networks職人コンテストという感じでした。みんな強かったです。前半は、2〜5位くらいにいる状態だったので、これは賞金はオレのものだなHAHAHAなどと思っていたのですが、終盤は15位くらいまで押し出されるようになり、こいつらマジかよ、強すぎだろ、と思って、最後方向転換をしてどうにか10位で終われました。自分の感覚ではスコア(NLL)で0.7以下を出せれば、Deep CNNを熟知していて必要な処理を漏れなくやった上で相当チューニングしているという印象なのですが、これが最終的に50人以上いました。そんなにいるとは思わなくて、世界をなめていたと思います。
また自分でやりながら0.6くらいが限界だと思っていたのですが、上位3チームは飛び抜けて良いスコアを出していて、そこを超えているので、本当にすごかったと思います。入賞がちょうど3チームなので、まぁ...あそこにはどうやっても入れなかったな、と諦めがつくくらいです。

ということで、賞金は貰えず、ぎりぎり10位*1という微妙な結果だったので、10位だったことくらいは自慢させてください。10位!!

投稿内容

概要

前半は、ひとつのCNNのアーキテクチャをとにかくチューニングして、それを8個学習して平均するという、シンプルな手法を採用していました。
が、終了1週間前の時点で、これではTOP10にも入れないと判断したので、最後の1週間で、入力サイズの異なる3つのアーキテクチャ(48x48, 72x72, 96x96)を8ずつ計24個のCNNを20台のGPUマシンで4日くらいかけて学習して各予測値を線形モデルでアンサンブルするという、激重な手法に切り替えました。これは"色々混ぜればスゴイ"という適当な考えではなくて、データセットの画像サイズがバラバラ(幅32px〜400pxくらいがある)なので、予測時の適切な解像度は画像ごとに異り、異なる入力サイズのアーキテクチャをアンサンブルすればスコアは改善できるだろうという推測が初期の頃からありました(ただダサいのであまりやりたくなかった)。結果うまくいって、ぎりぎり10位で終われました。結局やるのなら初めからやっておけばもう少し良く出来たかもしれません。

前処理

このデータセットは、画像ごとにサイズが異なり、また正方形でもありません。一般的なCNNでは固定サイズで正方形の画像を入力とするので、以下の方法で変換しました。

  • パディングで補って元画像の2倍のサイズにする
  • 意味のある値が入っているピクセルが全て含まれる領域を矩形で囲う
  • 矩形の縦、横で長い方のサイズを1辺の長さとする正方形で切り抜く
  • 明度を反転(元画像の背景が白なのを黒にする)

入力 出力

中央寄せして、パディングで正方形にして、画素値を反転させます。画素値を反転するのはその後の画像処理で背景が0となっていたほうが扱いやすいからで、精度等とは関係ありません。

いきなりですが、中央寄せの効果については、検証していません。画像によってはプランクトンが端っこにいて、後のdata augmentationと合わせて最悪なことになりそうなので中央寄せしたほうがいいだろうと思ってやっているのですが、最初のプログラミング時点からやっていたので、これに意味があるか、または逆に悪くなっていないか等は分かりません。上位には入れているのでそんなに悪くはないと思います。

Data augmentation
  • データが少なすぎるクラスがいくつかある
  • プランクトン画像には、回転や位置ズレ、姿勢の違いなど様々な変動がありえる

といった理由から、data augmentation(学習データの人工的な拡大)でスコアが改善すると思ったので行いました。

またオフラインのdata augmentationではなくオンライン(real-time)のdata augmentationが特に効果的でした。

data augmentationには次の変形を使いました。

  • translation (位置の変形, シフト)
  • scaling (対象サイズの変形, ズームアウト/ズームイン)
  • rotation (回転)
  • horizontal reflection (左右反転)
  • parspective cropping (affine変換またはhomography変換による変形)
  • コントラスト変更

これらの変形メソッドとその変形パラメーターをランダムに組み合わせることで1枚の画像から以下のような改変バージョンをムゲンに生成して学習データとします。
https://raw.githubusercontent.com/nagadomi/kaggle-ndsb/master/figure/random_transform.png
変形が分かりやすいようグリッドで表すと次のようになります。
https://raw.githubusercontent.com/nagadomi/kaggle-ndsb/master/figure/random_transform_grid.png

real-time data augmentationでは、(オンライン)学習器にデータを与える際にこのランダムな変形を通してから渡すようにします。変形のパターンが十分あれば、学習中に同じデータは1度しか与えられないので、オーバーフィッティングしにくくなります。

自分の実験では、シングルモデルでdata augmentationのあり/なしを変えた時のスコアは以下のようになっています。

学習時のdata augmentation 予測時のdata augmentation スコア(順位)
なし なし 1.46 (400位くらい)
あり なし 0.77 (120位くらい)
あり あり 0.65 (35位くらい)

予測時のdata augmentationは、1位の方がTTA(test-time augmentation)と言っている手法で、予測対象の画像をdata augmentationで増加させた後で各パターンについて予測を行い、予測結果を平均しています。
表から分かるとおり、data augmentationは非常に効果がありました。これをやるかやらないかまたその内容によって大まかな順位が決まったと思います。

またコンペ前半に、TheanoやTorchなどプログラミングを前提としているフレームワークを使っている勢が強かったのは、この処理を早くから採用していたからだと思います(僕は最初の投稿時からやっていました)。Caffeではこの処理を入れるのに本体の改造が必要だったので、フォーラムでreal-time data augmentationが効くという話が出始めた後で本体を改造し始めた人が多かったのではと思います。

データの正規化

CNNでよく使われる正規化は、平均画像を引くというものですが、今回はGlobal Contrast Normalization(GCN)を使いました。
これは、画像ごとに画像内の平均画素値を引いて画素値の標準偏差で割るという正規化です。
データセットの平均画像はreal-time data augmentationのハイパーパラメーターによって変わるので扱いづらく、画像単位で処理できるGCNが扱いやすかったのと、収束やスコア的にも問題なかったので選択しました。

ちなみにデータの正規化は、data augmentationの後に行っています。real-timeの場合は、
1. データの選択
2. ランダム変形
3. 正規化
4. 学習器の更新
が学習のメインループになります。

アーキテクチャ

終了1週間前まで96x96を入力サイズとするシングルアーキテクチャ(cnn_96x96)をずっと調節していました。
いくつかスコアが上がった変更は、

  • OxfordNet(VGG)ベースからHe et al. のModel Cベースに変更、初期化処理も合わせて変更、ReLUをVery Leaky ReLU(negative slope = 0.333)に変更 (PReLUではない)
  • NAG で最適化するようにした
  • 学習率のスケジューリングを調節

くらいです。シングルモデルで0.7→0.65くらいの改善で、頑張っていた割にあまり改善できていませんが、0.7〜0.63くらいの密度が高かったのでコンペ的には効果があったと思います。

cnn_96x96の最終盤は次のようになっています。

require 'cunn'
require 'cudnn'
require './LeakyReLU'

-- this architecture inspired by the work of http://arxiv.org/abs/1502.01852

local ALPHA = 0.333
function cudnn.SpatialConvolution:reset(stdv)
   if stdv then
      stdv = stdv * math.sqrt(3)
   else
      stdv = math.sqrt(2 / ((1.0 + ALPHA * ALPHA) * self.kW * self.kH * self.nOutputPlane))
   end
   self.weight:normal(0, stdv)
   self.bias:normal(0, stdv)
end
function nn.Linear:reset(stdv)
   if stdv then
      stdv = stdv * math.sqrt(3)
   else
      stdv = math.sqrt(2 / ((1.0 + ALPHA * ALPHA) * self.weight:size(2)))
   end
   self.weight:normal(0, stdv)
   self.bias:normal(0, stdv)
end
local function create_model()
   local model = nn.Sequential()

   -- input: 1x96x96
   model:add(cudnn.SpatialConvolution(1, 64, 7, 7, 2, 2, 3, 3))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialMaxPooling(2, 2, 2, 2))

   model:add(cudnn.SpatialConvolution(64, 128, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(128, 128, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(128, 128, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(128, 128, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialMaxPooling(2, 2, 2, 2))
   model:add(nn.Dropout(0.25))

   model:add(cudnn.SpatialConvolution(128, 256, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(256, 256, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(256, 256, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(256, 256, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(256, 256, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(256, 256, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialMaxPooling(2, 2, 2, 2))
   model:add(nn.Dropout(0.25))
   
   model:add(cudnn.SpatialConvolution(256, 320, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(320, 320, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(320, 320, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(320, 320, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(320, 320, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialConvolution(320, 320, 3, 3, 1, 1, 1, 1))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(cudnn.SpatialMaxPooling(2, 2, 2, 2))
   model:add(nn.Dropout(0.25))
   
   model:add(nn.View(320 * 3 * 3))
   model:add(nn.Linear(320 * 3 * 3, 1024))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(nn.Dropout(0.5))
   model:add(nn.Linear(1024, 1024))
   model:add(nn.LeakyReLU(ALPHA))
   model:add(nn.Dropout(0.5))
   model:add(nn.Linear(1024, 121))
   model:add(nn.SoftMax())
   
   return model
end

return create_model

最初の頃はこんな深いネットワークは学習できませんでしたが、初期化処理を改善するのとLeakyReLUを使うのとで学習が可能になりました。
ただ悲しいことに、最後の1週間において勘で追加した入力を72x72とするアーキテクチャのほうが良いスコアを出しています。これは、入力画像サイズをチューニングしたのが最初の頃で、その後の様々な変更で適切なサイズが変わっていたのに気づかず、ずっと96x96の上で調節していたということだと思います。色々な所のハイパラーパラメーターが依存関係にあるのでもはや人類にはチューニング不可能だと思いました。

アンサンブル

上記のcnn_96x96の他に、cnn_72x72cnn_48x48を追加してアーキテクチャごとに8個バギングした予測結果をさらに重みづけてアンサンブルしました。
アンサンブルの重みは、validationの結果から解析的に求められますが、実装が面倒だったので乱数の総当りで決めました。

最終的に各モデルの結果は次のようになりました。

Model Public LB score
cnn_48x48 single model 0.6718
cnn_72x72 single model 0.6487
cnn_96x96 single model 0.6561
cnn_48x48 average of 8 models 0.6507
cnn_72x72 average of 8 models 0.6279
cnn_96x96 average of 8 models 0.6311
ensemble (cnn_48x48(x8) * 0.2292 + cnn_72x72(x8) * 0.3494 + cnn_96x96(x8) * 0.4212 + 9.828e-6) 0.6160


ソースコードは以下のURLで公開しています。
Kaggle-NDSB

まとめ

自分で書いてて、きわめて普通だなーと思いました。
上位3人の手法にはそれぞれ奇抜なものが含まれているようなので興味のある方はフォーラムを監視するのがよいと思います。

*1:KaggleではTOP10が概要ページのLeaderboardに表示される人数であり、Master statusの条件

Avazuが終わった

Avazu Click-Through Rate Predictionが終わりました。長かった。期間が長すぎる!! と言っても、僕は始めて1週間目くらいにプランクトン分類が始まったため、そっちばかりでほとんどやっていなかったのですが、ついに終わりました。
結果は72th/1604。一応、上位5%には入っているけど、僕は絶対値を気にするので、72というのはあまり良くないな、と思う。

ソリューション

以下自分の投稿内容です。

概要

0.6 * 統計量を特徴量に使ったGBDTモデル + 0.4 * 交互作用項を追加した fast_solution_v3.py
です。

統計量を特徴量に使ったGBDTモデル

まずデータを2つに分割します。

  • 統計量計算用 (14/10/30のデータ)
  • モデル学習用 (14/10/30より前のデータ)

各カテゴリ変数について、

  • 出現回数
  • クリック回数 (出現したうちクリックされたレコード数)

を統計量計算用データから計算します。

カテゴリ変数 出現回数 クリック回数
site_id=123 10 3
site_id=456 100 10
app_id=123 40 8
app_id=456 50 10
... ... ...

という感じのデータ。これらを使って、モデル学習用データの各変数を置換します。
例えば、

site_id=123のとき site_id=(10, 3)
site_id=456のとき site_id=(100, 10)
app_id=123のとき app_id=(40, 8)
app_id=456のとき app_id=(50, 10)

のように。これでたくさんあるダミー変数の次元が全て2に次元削減されます。今回の場合、22変数だったので、44次元のデータになります。

この特徴量をxgboostを使って学習しました。
単体のLBスコアは、0.3933798でした。

今回とにかく、GBDTを使ってみたいという思いが初めにあったのですが、変数が多すぎてうまく学習できないように思えたのでとにかく減らしてみました。
データの分割方法はいろんなパターンが考えられて、たとえば、統計量計算に使うデータを1日ずつズラしながら7モデル学習して平均するようなこともできるのですが、やってみたところ特に良くなりませんでした。全体をランダムに分割するというのも試しましたが、これは統計量の情報が正確すぎるせいか、めちゃくちゃオーバーフィッティングしました。また古いデータから統計量を計算すると、新しく出て来た変数の情報が無くて変換できない変数がテストデータに増えるため、一番新しいデータである30日を統計量計算用に使うのがよさそうでした。

交互作用項を追加したfast_solution_v3.py

fast_solution_v3.pyはフォーラムで共有されているコードです。これ
中身は、FTRL-Proximal online learning algorithmとかいうのを使った線形分類器です。

変数の削除は、L1正則化に期待することにして、あとはいい感じのinteractionを追加できれば精度が良くなるだろうと思ったので、変数を追加する方向で改造しました。
方法としては、ただのブルートフォースアタックです。

  • 22変数から2つ選んだ全ての組み合わせについて、交互作用項として入れたときと入れなかった時の検証スコアを計算
  • 効果があった変数順にソート
  • 効果がありそうな変数を上から5個単位で追加していってスコアが悪くなったところで追加を辞める

というコードを書いて、一晩ほど計算した結果を少し手で調節して、以下のパラメーターで学習するとよい、ということになりました。

# これは事前に改造なしで探索したパラメーター
params = { "epoch": 2, "alpha": 0.05, "beta": 1.0, "L1": 3.0, "L2": 5.0 } 
# 追加する変数
params["interaction"] = (
    ('site_id', 'C14'),
    ('site_domain', 'C14'),
    ('app_domain', 'C14'),
    ('site_domain', 'C17'),
    ('site_id', 'C17'),
    ('app_domain', 'C17'),
    ('site_category', 'C14'),
    ('site_domain', 'C19'),
    ('app_id', 'C14'),
    ('site_domain', 'C20'),          
    ('app_id', 'C20'),
    ('site_id', 'C20'),
    ('site_id', 'C19'),
    ('app_category', 'C14'),
    ('app_domain', 'C18'),
    ('site_category', 'C17'),
    ('site_domain', 'app_id'),
    ('app_id', 'C17'),
    ('site_category', 'C20'),
    ('site_id', 'app_id'),
    ('site_domain', 'app_category'),
    ('app_id', 'C21'),
    ('site_domain', 'site_category'),
    ('app_id', 'C19'),
    ('site_category', 'app_id'),
    ('site_id', 'app_category'),
    ('site_domain', 'C21'),
    ('C14', 'C19'),
    ('site_id', 'site_domain')
    )

interactionは元のコードでは全て入れるか全て入れないかの2値ですが、指定した変数の組み合わせだけ使うように改造しています。
これで、単体のLBスコアは 0.3930048 でした。

アンサンブル

0.6 * 統計量を特徴量に使ったGBDTモデル + 0.4 * 交互作用項を追加したfast_solution_v3.py
です。重みは適当に試して決めました。

これで、LBスコアは 0.3905856 でした。各モデル単体のスコアに比べてかなり良くなっています。(このコンペではスコアが0.001変わると順位が150位くらい変わるので"かなり"良くなっています)

Privateスコアは 0.3887624。

感想

あまり熱心に取り組んでいないというのもあるのですが、フォーラムを見ると、learning rate decayが効いただの書いてあって、何でオレはそんなことも試していなんだ...と思う内容が多かったです。
データが大きくてあまり手の込んだことはできないという思いが強くて、試すパターンを制限しすぎてしまっていた気もします。

Torch7の分かりにくい話

Facebookがtorchの拡張を公開したこともあり、torchを使ってみている方も出始めたようなので、自分がtorchで使う上で分かりにくかったことやハマったことなどを上げていきます。
ある程度使っている方を前提としています。

ひどいことをたくさん書きますが、torchが嫌いなわけではありません。torchはサイコーです。

リリースやバージョンについて

ezinstallはgitのリポジトリをcloneしてインストールします。torchは今のところリリースの管理などしていなくて、日々更新される開発リポジトリがあるだけなので、インストールした日によって異なるバージョンがインストールされます。
たまに以下のようなコマンドを実行して更新したほうがいいです。

#!/bin/sh
sudo luarocks install cwrap
sudo luarocks install torch
sudo luarocks install optim
sudo luarocks install nn
sudo luarocks install image
sudo luarocks install cutorch
sudo luarocks install cunn
# 他にもありますが、自分はこのあたりだけ更新しています
# 更新順に気をつけないと依存関係が壊れることがあります

バージョンの管理が必要な場合は自分で行いましょう...

nnモジュール

学習用モデルと評価(予測)用モデルの切り替え

module:training() module:evaluate() で切り替えます。これは主にDropoutの動作に影響します。module:training()(デフォルトの状態)のままforward()で予測すると、Dropoutモジュールが含まれている場合にランダムに接続を切ってしまいます。予測を行う際は、必ず module:evaluate() でモードを切り替えてからforward()を行います。

バッチモードとはなにか

nnのほとんどのモジュール(全てではない..)はバッチモードを持っています。これはminibatch更新などで一度に複数のデータをforward/backwardするためのモードです。
このモードを使いたい場合は、入力として想定されているTensorよりひとつ軸の多いTensorを入力します。
たとえば、nn.Linear()は1D Tensorを想定しているモジュールなので1D Tensorを渡した時は逐次処理、2D Tensorを渡した場合はバッチ処理になります。nn.SpatialConvolutionMM()やnn.SpatialMaxPooling()は3D Tensorを想定しているので、3D Tensorを渡した時は逐次処理、4D Tensorを渡した時はバッチ処理になります。またCUDAバッグエンドのいくつかはバッチモードにしか対応していません。CUDAを使う場合はバッチモードで使うことを想定しているようです。

Criterion

Criterionは目的関数の定義です。が、SoftMaxなどの出力関数がCriterionと切り離されているため、分かりにくくなっていると思います。
SoftMax + MSEで最適化したい場合は、nn.SoftMax() + nn.MSECriterion() を使います。この場合、教師信号は、クラス数次元の1D Tensorになります(バッチモードの場合、2DTensor)。
SoftMax + NLLで最適化したい場合は、nn.LogSoftMax() + nn.ClassNLLCriterion()を使います。この場合、教師信号は、クラス番号(1からの整数値)になります(バッチモードの場合、1D Long Tensor)。また、nn.LogSoftMax()は、SoftMaxのlog、つまりexpする前の値を出力するため、確率を得たい場合は、forward():exp() で変換する必要があります。おそらく計算効率の問題でこうなっているのだと思いますがかなり分かりにくいと思います。
自分は、最近は、nn.SoftMax() + nn.TrueNLLCriterion()を使うようにしています。TrueNLLCriterionは、SoftMaxの出力をNegative Log Lossで直接最小化するというごく普通の動きをしてくれます。このモジュールは、facebookのfbnnの一部ですが、単体で動くのでこのファイルだけコピってきて使うのがオススメです。
https://github.com/facebook/fbnn/blob/master/fbnn/TrueNLLCriterion.lua

CUDA

CUDAを使う場合、Module, Criterionに対して:cuda()メソッドを呼び出して重み等のパラメーターをCudaTensorにするのと, 入力データを(:cuda()メソッドで)CudaTensorにして実行します。またforwardの返り値はCudaTensorになるのでその後使う場合はfloat()でFloatTensorに変換してから使ったほうがいいです。

例。

require 'cutorch'
require 'cunn'

local function build_model()
   -- 適当なCNN
   local model = nn.Sequential() 
   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.View(10 * 10 * 128))
   model:add(nn.Linear(10 * 10 * 128, 1024))
   model:add(nn.ReLU())
   model:add(nn.Dropout(0.5))
   model:add(nn.Linear(1024, 10))
   model:add(nn.LogSoftMax())
   
   return model
end
local function cuda_test()
   -- Module, Criterion, x, yをCudaTensorにする
   local model = build_model():cuda()
   local criterion = nn.ClassNLLCriterion():cuda()
   local x = torch.Tensor(64, 3, 24, 24):uniform():cuda() -- 入力
   local y = torch.Tensor(64):random(1, 10):cuda() -- ラベル
   -- batch forward
   local z = model:forward(x)
   local err = criterion:forward(z, y)
   -- batch backward
   model:backward(x, criterion:backward(z, y))
   print("CUDA Test Successful!")
end
torch.setdefaulttensortype('torch.FloatTensor')
cuda_test()
Dropout

Dropoutは第2引数にtrueを入れると学習時にランダムに接続を切って、予測時は(1-接続を切る確率)倍にするという一般的なDropoutになります。デフォルトでは、学習時にランダムに接続を切って1/(1-接続を切る確率)倍にして、予測時は出力そのままを使う実装になっています。デフォルトのほうが計算効率はいいと思うのですが、出力のスケールが変わるので他の実装を参考にした場合などは注意が必要です。

Convolution moduleいろいろ

SpatialConvolution/SpatialMaxPooling/SpatialAveragePoolingには沢山の種類があります。しかも微妙に動作が異なるものがあるので使う際には注意が必要です。

nn.SpatialConvolutionMM / nn.SpatialMaxPooling / nn.SpatialAveragePooling

torch標準のものです。他の実装に比べて遅いです(と言ってもすごく遅いわけではありません)。CPU,GPU,逐次,バッチと広く対応しています。
nn.SpatialAveragePoolingは平均ではなく合計を出力します。(平均を出力するものもあり、実装を切り替えただけつもりが出力が変わることがあります)
またこれらのモジュールは、出力サイズの計算にfloor(切り捨て)を使います。(ceil(切り上げ)の実装がいくつかあるため、実装切り替えただけつもりが出力のサイズが変わることがあります)

nn.SpatialConvolution

昔からあるものでサンプルなどで使われていることがありますが、最近はnn.SpatialConvolutionMMを使うようになっているので、使わないほうがいいです。

nn.SpatialConvolutionCUDA / nn.SpatialMaxPoolingCUDA

謎の実装です。おそらく実験的な実装のまま放置されているものなので使わないほうがいいです。

cudnn.SpatialConvolution / cudnn.SpatialMaxPooling / cudnn.SpatialAveragePooling

soumith/cudnn.torch · GitHub

NVIDIA cuDNNを使う実装です。luarocks install cudnnでインストールできます。別途NVIDIA cuDNNが必要です。NVIDIA cuDNNは最近公開されたRC2-v2以外は全てバグっているので必ず新しいバージョンを使うようにします。
実行は速いです。ただ、モデル保存時に100行くらいの警告が標準出力に出てきて、とてもうざいです。

ccn2.SpatialConvolution / ccn2.SpatialMaxPooling / ccn2.SpatialAvgPooling

soumith/cuda-convnet2.torch · GitHub

cuda-convnet2のカーネルを使う実装です。luarocks install ccn2でインストールできます。cuda-convnet2のコードは含まれているため別途インストールする必要はありません。おそらく一番速いです。(fbcunnは除く..)
これらのカーネルは、torch標準とはTensorのフォーマットが違います。torch標準では(batch, depth, height, width)ですが、ccn2は(depth, height, width, batch)です。nn.Transpose()で軸を入れ替えてから使います。
CUDA&バッチモードにしか対応していません。またフィルタの数は16の倍数、バッチの数は32の倍数、などの制限があります。
自分は主にこれを使っています。

nn.SpatialConvolutionCuFFT

facebookが公開した実装です。fbcunnに入っています。
カーネルが大きい場合は、とても速いらしいですが、小さなカーネル(流行りの3x3など)の場合はcudnnやcuda-convnet2のほうが速いです。またメモリ使用量が多いためTitan BlackやK40などメモリが多いデバイスでないと大きなモデルが学習できないことがあるようです。

他にもありますが、上のやつだけ把握していればいいように思います。
soumith/convnet-benchmarks · GitHub

に各実装のベンチマークがあります。

画像関係

image.loadで読み込んだ画像の各画素は0.0-1.0にスケールされています。0-255ではありません。

image.display()

image.display()で画像が表示できると書いてありますが、thコマンドではできません。
qluaコマンドを使うとqtが使えるようになっていて、image.display()などが使えます。
ただqluaはluajitと異なるバナーが表示され、luaの実装が違う?ようなので学習など重要な処理には使わないほうがいいと思います。
またimage.display()は明るさを勝手に調節するため、画像処理結果の確認はimage.save()で保存してから見たほうがいいです。

image.crop

torch.Tensorの添字は1オリジンですが、どうもimageモジュールの一部は0オリジンで処理されているところがあるようです。
image.cropもx/yは0から始まっているように見えます。

image.rotate

thetaの符号が逆?
領域外は黒(0)で塗られます。data augmentationなどで使う場合は、可能なら背景を0にするか、不可能なら広めに取った領域を回転させたあと領域外を含まない範囲でcropするのがいいと思います。
image.warpを使うと、領域外は端のピクセルを引き伸ばすようにできますが、変なパターンが入るのは同じことなので...

image.warp

ソース画像の変換後の座標を(2(x,y), height, width)のTensorで定義して画像を変換する関数です。大概の変換処理はこの関数でできますが、変換用のデータを自分で計算する必要があります。
いまのところbicubicに対応しているのはこの関数だけなので、rotateやscaleを使わずこの関数を使っている方が多いようです。
使い方は、https://github.com/torch/image/blob/master/test/test_warp.lua

これも0オリジンだと思います。(テストは1オリジンで計算していますが端のピクセルが領域外になっている)

gfx.js

一部のチュートリアルでは gfx.js を使って画像の表示等を行うと書いてありますが、現在メンテされておらずバグっていて動かないという話なので無視しましょう。開発側としてはiTorchに移行したいようです。

optim

config

optim.sgd()にlearningRateなどの設定を渡しますが、このテーブルはただ設定を受け取るだけはなく、次の更新時に使うパラメーター等が書き込まれます。なので

optim.sgd(a, b, {learningRate = 0.1, momentum = 0.9})

のようにリテラルや一時変数で渡してしまうと、momentumやlearningRateDecayの計算ができなくなってしまいます。学習中生存している変数を介して渡すようにします。

NAG

optim.sgdにnesterovというオプションがありこれをtrueにするとNesterov momentumになると書いてありますが、一般的な実装とは違うようです。この問題が上がったあとoptim.nagが追加されたのでNAGを使いたい場合はこちらを使ったほうがいいようです。

torch.setdefaulttensortype

一部のチュートリアルでは、CUDAを使う場合はデフォルトをCudaTensorにすると書いてありますが、それをやるといろいろなところがバグるためしてはいけません。
CudaTensorはまだFloatTensorと完全な互換性がありません。努力はしているようです。
missing API against TH · Issue #70 · torch/cutorch · GitHub
デフォルトはDoubleTensorですが、Floatで十分なので(どうせCUDAはFloatしか使わない)、FloatTensorにするのがオススメです。必要なメモリが半分になり処理も速くなります。

torch.manualSeed

CUDAを使っているときは、cutorch.manualSeedも設定します。

便利情報

torchサイアクだという印象を残して終わらたないために、最後に便利情報を載せます。

Caffe関連

Caffeうらやましい!と思うところはだいたい取り込まれています。

szagoruyko/loadcaffe · GitHub
を使うと、Caffeのpretrained imagenet modelをtorchのモジュールに変換して読み込めます。(caffeのインストールは必要ありません)

szagoruyko/torch-caffe-binding · GitHub
はcaffeをtorchのモジュールとして使います(caffeのインストールが必要です)。caffeはtorchの部品のひとつであると言えます。

画像認識関連

画像認識は、ImageNet Challengeの成果が非常に参考になります。

convnet-benchmarks/torch7/imagenet_winners at master · soumith/convnet-benchmarks · GitHub
には、AlexNet[1], Overfeat[2], GoogLeNet(補助分類器なし)[3], VGG model[4]の実装例があります。(速度のベンチマーク用なので完全でないものがあります)

ImageNet-Training/Models at master · eladhoffer/ImageNet-Training · GitHub
には、AlexNet, GoogLeNet, NIN[5], OverFeatの実装例があります。

ILSVRC2014の資料は
ImageNet Large Scale Visual Recognition Competition 2014 (ILSVRC2014) Workshop
にあります。

あとオレのCIFAR-10のコードも参考になります。(オチです)
Kaggle CIFAR-10の話 - デー

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秒だった。爆速!