Caffeの3D-CNNを使ったダミーデータの学習

# はじめに
一般的にCNN（Convolutional Neural Network、畳み込みニューラルネットワーク）は主に画像等の2次元配列データを入力とするが、これを3次元配列データにしたものが3D-CNN。
Caffeでは3D-CNNも使うことができるが、学習の方法についての資料が少なかったので、ダミーデータを使った学習方法をメモ。応用すれば一般のND-CNNにも適用できるとは思う。

# 環境
* OS:SUSE Linux Enterprise Server 11 SP3
* Python:2.7.10（Anaconda 2.3.0）
* Caffe:[0.13.0（本家）](https://github.com/BVLC/caffe)


# ダミーデータ
今回は横2×縦3×奥行4の3次元配列データを3D-CNNの入力とし、チャネル数が5の4次元配列のデータを使う。
データの個数は学習用に6個、テスト用に6個、計12個とする。
今回は2クラス分類問題とする。どのデータも、全ての要素が1であるか、全ての要素が0であるかのどちらかとなるようにし、全ての要素が1であるデータは正例（ラベル1）、全ての要素が0であるデータは負例（ラベル0）、というごく単純なデータセットを構築する。
[このページ](https://github.com/BVLC/caffe/pull/2049)によれば、3D-CNNを使う際はデータセットの形式としてLEVELDBやLMDBは使えず、HDF5を用いる必要があるらしい（？）。HDF5形式の学習用データセットファイルの、"data"データセットにCNNへの入力を、"label"データセットにラベルを記録することにする。テスト用データセットファイルに対しても同様に記録を行うことにする。
以上のようなデータは以下のスクリプトで作れる。このスクリプトを`make_hdf5.py`とする。

```py
# -*- coding: utf-8 -*-


import h5py
import numpy as np
import random
import sys

def save_hdf5(filename, inputs, labels):
	with h5py.File(filename, 'w') as f:
		f['data'] = inputs
		f['label'] = labels

def main():
	train_file  = sys.argv[1]
	test_file   = sys.argv[2]
	channels    = 5
	width       = 2
	height      = 3
	depth       = 4
	n_train     = 6
	n_test      = 6

	labels0 = np.zeros((n_train+n_test)/2)
	labels1 = np.zeros((n_train+n_test)/2) + 1
	labels = np.r_[labels0, labels1]

	neg = np.zeros(channels * width * height * depth) + 1
	neg = neg.reshape([channels, width, height, depth])
	pos = neg + 1
	negs = []
	poss = []
	for i in range((n_train+n_test)/2):
		negs.append(neg)
		poss.append(pos)
	negs = np.array(negs,dtype=np.float32)
	poss = np.array(poss,dtype=np.float32)
	inputs = np.r_[negs, poss]

	perm = np.random.permutation(len(inputs))
	train_inputs = inputs[perm[:n_train]]
	train_labels = labels[perm[:n_train]]
	save_hdf5(train_file, train_inputs, train_labels)

	test_inputs = inputs[perm[n_train:]]
	test_labels = labels[perm[n_train:]]
	save_hdf5(test_file, test_inputs, test_labels)


if __name__ == "__main__":
	main()
```


`make_hdf5.py`の使い方は、まず`h5py`、`numpy`パッケージを以下のようにインストールしてから、

```shell-session
$ pip install h5py
$ pip install numpy
```

`make_hdf5.py`を実行する。学習用データセットファイルを`foo_train.h5`、テスト用データセットファイルを`foo_test.h5`とすると、実行すべきコマンドは以下のようになる。

```shell-session
$ make_hdf5.py foo_train.h5 foo_test.h5
```

実行すると`foo_train.h5`と`foo_test.h5`が生成される。


# 学習の準備
`foo_train_list.txt`、`foo_test_list.txt`、`foo_solver.prototxt`、`foo_train_test.prototxt`という4つのファイルを作成する必要がある。
それぞれ、学習用データセットファイルへのパスのリスト、テスト用データセットファイルへのパスのリスト、Solverのパラメータの定義、学習・テストに用いるネットワークの定義を記述する。

## `foo_train_list.txt`、`foo_test_list.txt`の作成
`foo_train_list.txt`には`foo_train.h5`へのパスを、`foo_test_list.txt`には`foo_test.h5`へのパスを書いておく。

## `foo_solver.prototxt`の作成
`foo_solver.prototxt`を以下のように記述する。
[このページ](http://pepper.is.sci.toho-u.ac.jp/index.php?%A5%CE%A1%BC%A5%C8%2FCaffe%A4%CE%A5%E2%A5%B8%A5%E5%A1%BC%A5%EB%B5%AD%BD%D2%C8%B4%BF%E8%A1%CAcaffe.prototxt%A1%CB)に従って、「`net:`」の部分に学習用ネットのprototxtファイル名を記述する。また、「`snapshot prefix:`」の部分には、スナップショットファイル(後述) のプレフィックスを指定する。

```yaml
# reduce the learning rate after 8 epochs (4000 iters) by a factor of 10

# The train/test net protocol buffer definition
net: "foo_train_test.prototxt"
# test_iter specifies how many forward passes the test should carry out.
# In the case of MNIST, we have test batch size 100 and 100 test iterations,
# covering the full 10,000 testing images.
test_iter: 100
# Carry out testing every 500 training iterations.
test_interval: 500
# The base learning rate, momentum and the weight decay of the network.
base_lr: 0.001
momentum: 0.9
weight_decay: 0.004
# The learning rate policy
lr_policy: "fixed"
# Display every 100 iterations
display: 100
# The maximum number of iterations
max_iter: 4000
# snapshot intermediate results
snapshot: 4000
snapshot_prefix: "foo"
# solver mode: CPU or GPU
solver_mode: GPU
#solver_mode: CPU
```

## `foo_train_test.prototxt`の作成
`foo_train_test.prototxt`に以下のように記述する。
基本的なフォーマットの説明は[Caffe公式のLayer Catalogue](http://caffe.berkeleyvision.org/tutorial/layers.html)に譲るが、3D-CNNを用いる場合、40行目や66行目のように畳み込み層のパラメータ（「`convolution_param`」）の記述に「`engine:CAFFE`」の記述が必要。また、畳み込み層のカーネル（フィルタ）の大きさの指定には42-44行目、68-70行目のように「`kernel_size`」を3回書くことで指定できる。
また3D-CNNに限らないが、ソフトマックス層への入力となる層の出力フィルタ数（「`num_output`」）は判定するクラス数と同じにする必要があることに注意する。今回の場合は2クラス判定問題のため、114行目のようにソフトマックス層への入力となる`ip2`層の出力フィルタ数を2とする。

```yaml
name: "foo"
layer {
  name: "foo"
  type: "HDF5Data"
  top: "data"
  top: "label"
  include {
    phase: TRAIN
  }
  hdf5_data_param {
    source: "foo_train_list.txt"
    batch_size: 2
  }
}
layer {
  name: "foo"
  type: "HDF5Data"
  top: "data"
  top: "label"
  include {
    phase: TEST
  }
  hdf5_data_param {
    source: "foo_test_list.txt"
    batch_size: 2
  }
}
layer {
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  convolution_param {
    engine: CAFFE
    num_output: 48
    kernel_size: 1
    kernel_size: 1
    kernel_size: 1
    stride: 2
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}
layer {
  name: "conv2"
  type: "Convolution"
  bottom: "conv1"
  top: "conv2"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  convolution_param {
    engine: CAFFE
    num_output: 160
    kernel_size: 1
    kernel_size: 1
    kernel_size: 1
    stride: 2
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}
layer {
  name: "ip1"
  type: "InnerProduct"
  bottom: "conv2"
  top: "ip1"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  inner_product_param {
    num_output: 64
    weight_filler {
      type: "gaussian"
      std: 0.1
    }
    bias_filler {
      type: "constant"
    }
  }
}
layer {
  name: "ip2"
  type: "InnerProduct"
  bottom: "ip1"
  top: "ip2"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  inner_product_param {
    num_output: 2
    weight_filler {
      type: "gaussian"
      std: 0.1
    }
    bias_filler {
      type: "constant"
    }
  }
}
layer {
  name: "accuracy"
  type: "Accuracy"
  bottom: "ip2"
  bottom: "label"
  top: "accuracy"
  # include {
  #   phase: TEST
  # }
}
layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "ip2"
  bottom: "label"
  top: "loss"
}
```

# 学習
以下のコマンドを入力して学習を行う。

```shell-session
$ caffe train --solver foo_solver.prototxt
```

学習が正常に終了すると`foo_iter_4000.caffemodel`、`foo_iter_4000.solverstate`という名前の2つのファイルが生成される。


# テスト
以下のコマンドを入力してテストを行う。

```shell-session
$ caffe test -weights=foo_iter_4000.caffemodel -model=foo_train_test.prototxt
```