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