chainerで独自画像データのクラス分類と画像の分類結果取得について

# 概要
しばらくの間に進化し続けているchainerさんで、少し見ない間にだいぶ変わっていたので、独自データを利用したクラス分類をMNISTのサンプルを参考にしてやってみました。

コードはgithub上においてます。

-
 https://github.com/komorin0521/chainer_myexample/tree/master/image_classify/01_src

なお、古いデータもありますが、今回用いたのは以下のコードです
- `util.py`
- `network.py`
- `train_chainer_310.py`
- `predict_chainer_310.py`

# 環境
- Ubuntu 16.04(CPU環境:windows上のVM)
- python 3.6.2
- chainer 3.1.0

# 実行例
pipで`chainer`と`pillow`をインストールしていることを前提

## トレーニング
```bash
$ python train_chainer_310.py -lf ../02_inputdata/labels.txt -f ../02_inputdata/filepath.txt -df ../02_inputdata/ 
```

標準出力にロス値や精度が出ます

## 予測結果の取得
```
$ python predict_chainer_310.py -lf ../02_inputdata/labels.txt -mf result/model_675.npz -if ../02_inputdata/01_a/01_colour/04_a.png
```

トレーニング時に`${workdir}/result/model_*npz`にトレーニング済みのモデルが生成されるので、それを利用します。
`-if`は画像分類を行いたい対象のファイルです。
なお、標準出力には、確率、インデックス、ラベル名が表示されます。

```
index: 0, prob: 0.999291, label: a
```


# ちょっとした説明
基本的にはMNISTのトレーニングのサンプルを用いてやっているので、
以下の3点にまとを絞って説明を書いています。

- 独自データの定義部分
- モデルの保存
- ある1枚の画像の結果を取得する


## 独自データの定義
chainerに限らないが、だいたいのサンプルコードって、MNISTの画像をDLしたついでに
いい感じに前処理して、ひょいってトレーニングしている気がする（笑）
なので、どういう風に自前のデータを学習すればよいかわからないこと多い！

`chainer`では、[`datasets`](http://docs.chainer.org/en/stable/reference/datasets.html)を用いて簡単にデータセットを定義できる。

画像分類であれば画像情報、ラベル、という`TupleDataset`を用いてやればよい。
なお、chainerでは画像情報は`np.float32`で扱う必要があるので、
`util.py`の`load_data`内部で上記を指定してます。

トレーニングのバッチサイズとかは、データセットと別に`chainer.iterators.SerialIterator`で定義してあげる。

`train_chainer_310.py` 内部の下記の部分

```py3
...
def load_my_dataset(filepath, datafolderpath, labelfilepath, test_ratio=0.9):
    """
    loading and make train and test dataests
    filepath: the list of imagefilenamelist
    datafolderpath: the datafolder of inputting image
    labelfilepath: the data of label
    for this example the label is "a", "i", "u", "e", "o"
    """
    labellist = load_label(labelfilepath)

    with open(filepath, "r") as readfile:
        filepathlist = [os.path.join(datafolderpath, line.strip())
                        for line in readfile.readlines()]

    datasets = list()
    for imagefilepath in filepathlist:
        img = load_data(imagefilepath)
        filename = imagefilepath.split(os.path.sep)[-1]
        label = filename.split(".")[0].split("_")[1]
        index = labellist.index(label)
        datasets.append([img, index])
    random.shuffle(datasets)

    index = int(len(datasets) * test_ratio)
    traindatasets = datasets[:index]
    testdatasets = datasets[index:]

    train_imgs = [train_data[0] for train_data in traindatasets]
    train_labels = [train_data[1] for train_data in traindatasets]

    test_imgs = [test_data[0] for test_data in testdatasets]
    test_labels = [test_data[1] for test_data in testdatasets]

    return chainer.datasets.tuple_dataset.TupleDataset(train_imgs, train_labels), \
            chainer.datasets.tuple_dataset.TupleDataset(test_imgs, test_labels)
...
```

## モデルのセーブについて
トレーニング中にトリガーを使って、ロス値や精度を出すのに`trainer.extend`にて定義してあげればよい
MNISTの例でもいろいろと利用されているが、モデルのセーブがない...!!
ちょっと見てみると、`chainer.training.extensions.snapshot_object`というものを用いれば、対象のオブジェクトファイルをセーブしてくれるみたい。
中身を見るとどうやら`*{.updater.iteration}.npz`のように定義してあげていると、
いい感じにフォーマットしてくれるとのこと。

なので、上記のように下記のように途中でモデルをセーブするのを追加してみた。

もしかしたら`trainer`がセーブされているので、そこから拾い出せるのかもしれないが、
最終予測結果のみを得るのであれば、`model`のみエクスポートする方が、なにかと便利な気がする。


`train_chainer_310.py`内部の下記の部分

```py33
   # save the modelfile
    trainer.extend(chainer.training.extensions.snapshot_object(
        model, 'model_{.updater.iteration}.npz'), trigger=(10, 'epoch'))
```

## 予測を行う部分について
`predict_chainer_310.py`内部に書いてます。

処理の流れは、おおざっぱには下記の通り。
1. 訓練済みモデルをロード
2. 予測したいイメージファイルを読み込み
3. 訓練済みモデルを用いて予測

モデルロードは、`chainer.serializers.load_npz(対象ファイル,読み込み対象オブジェクト)`関数であっさりできる

2は、`np.float32`の配列で読み込んであげることだけ注意する

3が少し注意が必要で、`predict`関数の部分にあたる

基本的に複数のイメージを処理することを前提としているので、イメージファイル一個でもちゃんと画像ファイルリストにしてあげてから、`Variable`型に変換してあげる

```py3
x = Variable(np.asarray([inputdata]))
```

そして、順伝播は`model.predictor(x)`で行ってあげる。
`model.predictor`の実態は`L.Classifier(LinearNet(n_units_h1, n_units_h2, n_out))`の引数の`LinearNet`にあたるので、それを呼び出すと`LinearNet.__call__(x)`が呼び出されるようになっている。

ただ、ネットワークで定義した最終出力層は、ソフトマックス関数にかける前の値なので、
`F.sotmax`を行うことで、確率にしてあげないといけない。
`y`も結果の配列になっているので、画像が一個の場合は`y.data[0]`が入力画像1つに対する各ラベルの確率となっている。

なので、一番おっきい確率になっている`index`を探しています。
（たぶんforを使わなくてもできるが、ひとまず）


以上、ちょっとした解説です。

# 所感
`tensorflow`を使って独自データセットを用いて同じことやろうとしたけど、
けっこうデータセットの定義の部分とかいろいろとややこしいが、
chainerでは簡単にできたなあと
（chainerをもう少し古いバージョンのときに少し勉強したおかげもある）

ドロップアウトとかCNNとかその辺ももう少しやってみようと思いました。

# 参考
- [chainer](http://docs.chainer.org/en/stable/)