画像処理
機械学習
DeepLearning
Torch
CNN

初めに

この記事はTorchの基本的な使い方の解説?をします。ちょっとした応用や便利ツールなどはこちら
 caffe,chainer,theanoとdeeplearningライブラリを使ってきて最近torchに乗り換えたのでtorchについてのチュートリアルをまとめます。
コードはこちらの公式チュートリアルの2_supervisedを参考にしました。

torchについて

 日本ではdeeplearningのライブラリといえばcaffeに始まり、最近勢いのあるTensorflowや日本で開発がされているchainerなどが主流で、あまりtorchは使われていないように思えます。しかし、世界的にはtorchは主流のdeeplearningライブラリで、Facebookが使用していることでも有名です。画像処理における最新の手法の実装も多く、Fast R-CNNで使われているROIPoolingやsegmentationで最近用いられているDilatedConvolutionなどもtorchでは実装済みです。またvolumeデータに対する関数(3DConvolutionやDilatedConvolutionの3dバージョンなど)も充実しています。

 torchの特徴としては対応言語がluaであり、torchのとっつきにくさの一つでもあると思います(自分もそうでした)。自分はプログラミングの知識がないので詳しいことは言えませんが、luaはスクリプト言語であるためpythonなどに近い書き方で、jitコンパイラを用いるためpythonなどよりも動作が速いとい利点があります。また、lua自体にはpythonで言うnumpyやmatplotlibのような便利libraryはあまり多くありませんが(自分が詳しくないだけかもしれません)torchの拡張として様々なlibraryが用意されているためそこまで困ることはありません。

インストール

 インストールは公式のinstallationにあるよう以下の3行の実行でインストール可能でpathの書き込みも自動で行ってくれます(cuda周りの設定は自分で)。自分はubuntu14.04とubuntu16.04それぞれでインストールを行いました。またtitan xとgtx 1080のそれぞれに対して、cuda7.5(cuda8.0), cudnn v5をインストールしてgpu計算の動作確認できています。

git clone https://github.com/torch/distro.git ~/torch --recursive
cd ~/torch; bash install-deps;
./install.sh

注意として、torchのclone先がホームディレクトリ指定になっていることと、torchをどこにcloneしてもpathがホームディレクトリにcloneされた前提で書き込まれることが挙げられます。別なディレクトリにcloneしたい場合は1行めの~/torchを外し、./install.shを実行後のpathの書き込みの有無をnoにして自分でpathを指定してください。
 また、add-apt-repositoryがないと怒られた場合は以下を実行して解決できます。

sudo apt-get install software-properties-common python-software-properties

同様にreadline.*周りのエラーが起きた場合は

sudo apt-get install libreadline-dev

で解決できます。

torchでの行列(画像)の扱いについて

 ここからはtorchを使ったmnistのチュートリアルを解説していきます。公式のチュートリアルの2_supervisedではStreet View House Numbers Datasetというdatasetを使って数字の認識を行っていますが、このデータセットはmnistと比較するとタスクとして難しく単純なモデルではlossが落ちず精度がなかなか上がらないため、学習できているかわかりにくいです。そのためこの記事ではdatasetをmnistに差し替えます。mnistのデータセットは公式チュートリアルのA_datasetsのmnist.luaを実行することでdownloadできます。

 torchではpythonで言うnumpyのような行列演算をtorch.Tensorを用いて行います。詳しく書くと記事が長くなるため、numpyとtorch.Tensorの対応表を参照してください。またluaで画像の読み込みはimage.load()を持ちいて行います。その他luaとpythonの細かい違いは書くと長くなるので割愛します。

いよいよdeeplearning

modelの定義

torchではニューラルネットのmodelを組むのにnnライブラリを用います。まずnn.Sequential()でモデルを定義し、定義されたモデルに対しadd()を用いて畳み込みやプーリング層などを追加していきます。今回はchainerのmnistの例にならって3層の多層パーセプトロンを定義します。

model.lua
require 'nn'
model = nn.Sequential()
model:add(nn.View(32*32))
model:add(nn.Linear(32*32, 1000)):add(nn.ReLU())
model:add(nn.Linear(1000, 1000)):add(nn.ReLU())
model:add(nn.Linear(1000, 10))
model:add(nn.LogSoftMax())
model = model:float()  --modelをfloatに変換

このモデルはchainerのmnistのsampleで使われているmodelとほぼ同様のものになっています(mnist.luaでダウンロードしてくるdataが32×32の画像であるため入力が32×32になっています)。nn.View()は入力された(batchsize, channel数, width, height)次元のtorch.Tensorを(batchsize, 総入力数)次元に変換します。今回は(batchsize, 1, 32, 32)の入力が(batchsize, 32*32)に変換されます。nn.Linear()は全結合層を表し(入力ユニット数, 出力ユニット数)により定義します。nn.ReLU()は活性化関数です。また、今回出力はSoftMaxではなくLogSoftMaxにしています。理由としては今回loss関数で用いるnn.ClassNLLCriterionがnn.LogSoftMax()と対になっているためです。

 今回畳み込みやプーリングの処理は行わないためmodelにadd()していませんがnn.SpatialConvolutionやnn.SpatialMaxPoolingをadd()することでCNNが簡単に組めます。torchは他のライブラリと違い入力のtorch.Tensorを3次元にしてもforwardすることができます(一部できないものもある)。入力の次元数によりバッチ処理の判断を自動で行ってくれるため、画像1枚を入力する際にわざわざ4次元にする手間がありません。また、torchではcudnnを使うConvolutionやfacebookが実装した高速なConvolutionなど様々な種類のConvolutionがあります。詳しくはこちらの記事が解説してくれています。

パラメータ更新方法

 学習するのにはloss関数と最適化手法を決める必要があります。torchではloss関数はnnの中に用意されています(詳しくはこちら)。今回はSoftMaxを使った多クラス分類のためnn.ClassNLLCriterionを用います。また、最適化手法はoptimの中に用意されており(詳しくはこちら)今回はoptim.adagradを用います。

 loss関数と最適化手法が決まったのであとはlossの計算とbackwardの記述をするだけで学習が行えます。公式チュートリアルの4_train.luaでは学習の部分がmini-batchと言いつつ画像を一枚一枚forwardしてbackwardするよう記述されています。更新自体はまとめて行われるようになっているためmini-batch処理として問題はないと思いますがその分計算が遅くなるので今回はforwardとbackwardもmini-batchごとに行います。

trainer.lua
require optim
--loss関数の定義
criterion = nn.ClassNLLCriterion()
--confusion matrixの定義(学習するだけなら必要なし)
confusion = optim.ConfusionMatrix(#classes,classes)
--最適化手法の定義
optimizer = optim.adagrad
--最適化手法の初期値の指定(他の最適化手法の場合momentumやweightDecayなども必要となる場合がある)
config = {learningRate = 0.001}

--学習対象のパラメータを取得
param, gparam = model:getParameters()

function train(traindata, bsize)
  print('training')
  model:training() --modelを学習モードに変更(これによりdropoutの有無が決まる)
  shuffle = torch.randperm(traindata.data:size(1))


  for t = 1,traindata:size(), bsize do
    xlua.progress(t, traindata:size()) --progressbarを表示

--入力サンプルとラベルの初期化
    local inputs = torch.FloatTensor(math.min(bsize, traindata:size()-t+1), traindata.data:size(2), traindata.data:size(3), traindata.data:size(4))
    local targets = torch.DoubleTensor(math.min(bsize, traindata:size()-t+1))

    for i = t, math.min(t+bsize-1,traindata:size()) do
      local input = traindata.data[shuffle[i]]
      local target = traindata.labels[shuffle[i]]
      inputs[i-t+1] = input
      targets[i-t+1] = target
    end

    local feval = function(x)
      -- get new parameters
      if x ~= param then
        param:copy(x)
      end

      gparam:zero()
--forward
      local output = model:forward(inputs)
      local err = criterion:forward(output, targets) -- lossの計算
--backward
      local df_do = criterion:backward(output, targets) -- loss関数の微分計算

      model:backward(inputs, df_do) -- 勾配のbackward
--confusion matrixに結果を記述
      confusion:batchAdd(output:exp(), targets)

      return err, gparam
    end
    optimizer(feval, param, config)
  end
  print(confusion)
--confusion matrixを初期化
  confusion:zero()
end

同様のノリでテストのコードも書きます。

tester.lua
function test(testdata, bsize)
  model:evaluate() --評価モードに変更(dropoutの停止)
  print('testing')

  for t = 1, testdata:size(), bsize do
    xlua.progress(t, testdata:size())

    inputs = testdata.data[{{t, math.min(t+bsize-1, testdata:size())},{},{},{}}]
    targets = testdata.labels[{{t, math.min(t+bsize-1, testdata:size())}}]

    local output = model:forward(inputs)
    local err = criterion:forward(output, targets)

    confusion:batchAdd(output:exp(), targets)

  end

  print(confusion)
  confusion:zero()
end

注意として、チュートリアルでは1サンプルずつforwardしているためconfusion:add()を持ちていますが、mini-batch処理の場合はconfusion:batchAdd()を用います。

学習

学習のコードが書けたのであとはサンプルデータを読み込ませて実際に学習を行うだけです。A_datasetsのmnist.luaでtrain_32x32.t7とtest_32x32.t7に保存がしてある前提で行います。train_32x32.t7とtest_32x32.t7はtorch.ByteTensorのrgb画像と正解ラベルを持ったtableになっています。

main.lua
--confusion matrix用にクラスの定義
classes = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}
--torch.Tensorのdefaultをtorch.FloatTensorに変更
torch.setdefaulttensortype('torch.FloatTensor')

--dataの読み込み
traindata = torch.load('train_32x32.t7', 'ascii')
traindata.size = function() return traindata.data:size(1) end  --sizeを定義
traindata.data = traindata.data:float() --floatに変換

testdata = torch.load('test_32x32.t7', 'ascii')
testdata.size = function() return testdata.data:size(1) end
testdata.data = testdata.data:float()

dofile('model.lua')
dofile('trainer.lua')
dofile('tester.lua')

for epoch = 1, 50 do
 print('epoch ===> '..epoch)
 train(traindata, 256)
 test(testdata, 256)
end

これで学習が行えます。

まとめ

 以上で今回のtorchに関する記事は終わりです。まだtorchをメインで使い始めて日が浅いので自分自身わかっていない部分もあります。また、間違いやよりよい方法もあるかもしれないので気をつけて下さい。