ACMS ApexとChainerを組み合わせて数字画像を判定してみる

ACMS Apex 実験室 ～ 実験その２「Chainerと連携」

# はじめに
[前の記事](https://qiita.com/rytksm/items/b5da370cf629b9c95054)では、ACMS ApexとPythonを連携させて簡単な計算をさせてみました。
今回は、前回作成した環境で、Chainerのサンプルmnistを実行し、数字画像を判定して仕分けを行う仕組みを作ります。
AIで画像を分類することは最近よく行われていると思いますが、この仕組みでは分類した結果をもとに、自動的に0～9のディレクトリに仕分けします。誤判定を手動で仕分けしなおして学習データに追加すれば、サンプル数を手軽に増やすことができます。

# 注意事項

いわゆる「やってみた」系の記事ですので、無保証、無サポートです。

* ここで構築する環境は動作が保証されたものではありません。<BR>
あくまでお試し程度に「使ってみる」ことを目的とした環境を構築します。
* フリーソフトや評価版を使うので、メーカーサポートはありません。<BR>
自己解決できない場合は、本記事のコメント欄でご相談ください。<BR>
解決できるかどうかはわかりませんが…

# 前提条件
以前の記事「[PythonをACMS Apexから呼び出してみる](https://qiita.com/rytksm/items/b5da370cf629b9c95054)」で作った環境を使います。

# 環境
次のソフトウェアを使います。

* CentOS7
* ACMS Apex 1.3
* Python3.7.3
* Chainer 6.1.0

# 構築手順
まずはteraterm等を使い、acmsユーザでログインしてください。

## Python仮想環境の作成

```shell
# プロジェクトファイル格納用ディレクトリ作成
mkdir -p project/mnist
cd project/mnist
# このディレクトリ配下で使用するPythonのバージョンを指定
pyenv local 3.7.3
python --version
# 仮想環境を作成
python -m venv /home/acms/pyvenv/mnist
# 作成した仮想環境に切り替え
source /home/acms/pyvenv/mnist/bin/activate
# 前の記事のあと、pipが新しくなったようなので最新版に更新
pip install --upgrade pip
# 必要なライブラリをインストール
pip install chainer matplotlib pillow
```

## mnistを動かす
pipでインストールすると、examplesが展開されないようですので、GitHubからmnistのサンプルをダウンロードします。
svnを使ってmnistだけを持ってくるコマンド[^1]がなぜかエラーになるため、chainerを丸ごとダウンロードしてからmnistだけ抽出します。

```shell
wget https://github.com/chainer/chainer/archive/master.zip
unzip master.zip "chainer-master/examples/mnist/*"
mv chainer-master/examples/mnist/* .
rm -rf chainer-master/
```

とりあえず学習させてから推論させてみます。

```shell
# 学習
./train_mnist.py
# 推論
./inference.py --snapshot result/snapshot_iter_12000
```

実際に動かしてみると次のようになります。

```shell-session
[acms@apex-01-mst mnist]$ python ./train_mnist.py
Device: @numpy
# unit: 1000
# Minibatch-size: 100
# epoch: 20

Downloading from http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz...
Downloading from http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz...
Downloading from http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz...
Downloading from http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz...
epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
1           0.189041    0.0948515             0.943367       0.9681                    29.3833
2           0.0746062   0.0777412             0.9765         0.9755                    58.621
3           0.0469042   0.0741864             0.98535        0.9763                    89.0082
4           0.0361049   0.0718406             0.988283       0.9799                    119.898
5           0.028149    0.0790704             0.990717       0.9783                    150.946
6           0.0248317   0.0863064             0.991833       0.9776                    182.05
7           0.0206338   0.059041              0.993683       0.9848                    213.408
8           0.0182936   0.0980842             0.994          0.9778                    245.381
9           0.0137631   0.0988245             0.995817       0.9802                    277.376
10          0.0163252   0.0807811             0.99515        0.9824                    309.997
11          0.0144509   0.0907361             0.995          0.9797                    343.247
12          0.0133291   0.0825704             0.99595        0.9814                    376.936
13          0.0112155   0.0976595             0.99615        0.9803                    411.351
14          0.0128445   0.12487               0.9961         0.9776                    445.839
15          0.0108713   0.11923               0.996533       0.9782                    481.14
16          0.0097034   0.109636              0.9974         0.9796                    517.127
17          0.010692    0.102159              0.99655        0.98                      553.78
18          0.0102683   0.116668              0.996933       0.9807                    591.116
19          0.0123222   0.109196              0.996383       0.982                     628.863
20          0.00460523  0.0942767             0.998733       0.9843                    667.131
[acms@apex-01-mst mnist]$ ls
README.md     result                      train_mnist_data_parallel.py
inference.py  train_mnist.py              train_mnist_model_parallel.py
master.zip    train_mnist_custom_loop.py
[acms@apex-01-mst mnist]$ ls result/
accuracy.png  cg.dot  log  loss.png  snapshot_iter_12000
[acms@apex-01-mst mnist]$ ./inference.py --snapshot result/snapshot_iter_12000
Device: @numpy
# unit: 1000

Prediction: 7
Answer: 7
[acms@apex-01-mst mnist]$ 
```

resultというディレクトリができていて、この中にいくつかファイルができています。```snapshot_iter_12000```というファイルが学習結果を保存したファイルで、推論するときはこのデータを使って判定します。

## 判定用スクリプトを作る

サンプルにあるinference.pyを改造して、画像を読み込んで判定するPythonスクリプトと、それを呼び出すシェルスクリプトを作ります。

``` shell
cp inference.py predict_mnist.py
vi predict_mnist.py
vi predict_mnist.sh
chmod +x predict_mnist.sh
```

```python:predict_mnist.py
#!/usr/bin/env python
import argparse

import chainer
from train_mnist import MLP
from train_mnist_model_parallel import ParallelMLP
import numpy as np
from PIL import Image, ImageOps

def loadPredImg(path):
    # read image file and resize 28*28 grayscale
    img = Image.open(path)
    img = img.resize((28,28))
    img = img.convert('RGB')
    img = ImageOps.invert(img)
    img = img.convert('L')
    img = np.asarray(img).reshape( 1, 28, 28)
    # flatten
    img = img.reshape(784)
    # normalize
    img = img.astype(np.float32) / 255.0
    # label
    lbl = np.array(-1)

    return img, lbl

def main():
    parser = argparse.ArgumentParser(description='Chainer example: MNIST')
    parser.add_argument('--device', '-d', type=str, default='-1',
                        help='Device specifier. Either ChainerX device '
                        'specifier or an integer. If non-negative integer, '
                        'CuPy arrays with specified device id are used. If '
                        'negative integer, NumPy arrays are used')
    parser.add_argument('--snapshot', '-s',
                        default='result/snapshot_iter_12000',
                        help='The path to a saved snapshot (NPZ)')
    parser.add_argument('--unit', '-u', type=int, default=1000,
                        help='Number of units')
    parser.add_argument('--image', '-i', type=str, default='',
                       help='Image for prediction.')
    group = parser.add_argument_group('deprecated arguments')
    group.add_argument('--gpu', '-g', dest='device',
                       type=int, nargs='?', const=0,
                       help='GPU ID (negative value indicates CPU)')
    args = parser.parse_args()

    device = chainer.get_device(args.device)

    print('Device: {}'.format(device))
    print('# unit: {}'.format(args.unit))
    print('image file: ', args.image)
    print('')

    device.use()

    # Create a same model object as what you used for training
    if 'result_model_parallel' in args.snapshot:
        model = ParallelMLP(args.unit, 10, args.gpu, args.gpu)
    else:
        model = MLP(args.unit, 10)

    # Load saved parameters from a NPZ file of the Trainer object
    try:
        chainer.serializers.load_npz(
            args.snapshot, model, path='updater/model:main/predictor/')
    except Exception:
        chainer.serializers.load_npz(
            args.snapshot, model, path='predictor/')

    model.to_device(device)

    # Prepare data
    if len(args.image) > 0:
        x, answer = loadPredImg(args.image)
    else:
        train, test = chainer.datasets.get_mnist()
        x, answer = test[0]

    x = device.send(x)
    with chainer.using_config('train', False):
        prediction = model(x[None, ...])[0].array.argmax()

    print('Prediction:', prediction)
    print('Answer:', answer)


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

```shell:predict_mnist.sh
#!/bin/sh
# predict mnist
# $1 : image file name
# $2 : result file name
# $3 : model file name
# $4 : python script file name

source /home/acms/pyvenv/mnist/bin/activate
predict_value=`python $4 --snapshot $3 --image $1  |grep 'Prediction:' | cut -c 13`
deactivate

cp $1 newfile
echo section=data1 >> $2
echo predict_value=$predict_value >> $2
echo output_file=newfile >> $2
echo section=end >> $2
exit 0
```

predict_mnist.pyの変更点は、```loadPredImg```という画像を読み込んでmnistと同様のイメージに変換するサブルーチンと、それを呼び出す部分の追加です。
predict_mnist.shは、判定するイメージファイル、処理結果ファイル、モデルファイル、Pythonスクリプトファイルを受け取ります。判定結果は処理結果ファイルの```predict_value```に書き込まれます。

## 処理フローを作る

まずは、判定結果で画像を振り分けるためのディレクトリを予め作っておきます。

```shell
mkdir predicted_images
mkdir predicted_images/0
mkdir predicted_images/1
mkdir predicted_images/2
mkdir predicted_images/3
mkdir predicted_images/4
mkdir predicted_images/5
mkdir predicted_images/6
mkdir predicted_images/7
mkdir predicted_images/8
mkdir predicted_images/9
```

###管理画面にログイン
母艦のブラウザで下記のURLを開く
http://192.168.249.20:8080/login.html

|項目|値|
|:---|:---|
|テナント|(入力しない)|
|オペレーターID|OP_TEST|
|パスワード|<パスワード>|

###業務グループを追加する
リソース等を他と区別するために、mnistを行うための業務グループを作成します。
新しく作った業務グループの設定ができるように、ログインしているオペレーター自身と所属するオペレーターグループの権限を書き換えます。

[▼マスター] - [▼システム] - [業務グループ]
[新規作成]

|項目|値|
|:---|:---|
|業務グループ|WG_MNIST|
|アプリケーションポートグループ|(デフォルト値なし)|
|データストア名|(デフォルト値なし)|
|運用日切替時刻|(00:00)|
|完了保存日数|(7)|
|未完了保存日数|(7)|

[保存]
[OK]
[アクション▼] - [開始]

[▼マスター] - [▼システム] - [オペレーターグループ]

"OPG_TEST"の右のほうにある[詳細]
[編集]

[▼業務グループ運用権限]
[新規作成]

|項目|値|
|:---|:---|
|業務グループ|WG_MNIST|

業務グループ運用権限:

| |マスク内容閲覧|データダウンロード|
|:--|:--|:--|
|アドミニストレーター|☑|☑|

[OK]
[保存]
[OK]

[▼マスター] - [▼システム] - [オペレーター]

"OP_TEST"の右のほうにある[詳細]
[編集]

[▼業務グループ運用権限]
"WG_MNIST"の右のほうにある[詳細]

| |マスク内容閲覧|データダウンロード|
|:--|:--|:--|
|アドミニストレーター|☑|☑|

[OK]
[保存]
[OK]

###アプリケーションポートの作成
mnistを実行する専用のアプリケーションポートを作成します。

[▼マスター] - [▼アプリケーション] - [アプリケーションポートグループ]
[新規作成]

|項目|値|
|:---|:---|
|業務グループ|WG_MNIST|
|アプリケーションポートグループ|APG_MNIST|
|貸出|しない|
|デフォルトポートグループ|デフォルトポートグループにしない|

[保存]
[OK]

[▼マスター] - [▼アプリケーション] - [アプリケーションポート]
[新規作成]

|項目|値|
|:---|:---|
|業務グループ|WG_MNIST|
|アプリケーションポートグループ|APG_MNIST|
|アプリケーションポート名|1|
|ノード名|apex-01-mst|

[保存]
[OK]
[アクション▼] - [開始]

###アプリケーションを作る
先ほど作成したシェルスクリプトを呼び出すアプリケーションを作ります。
[▼マスター] - [▼アプリケーション] - [アプリケーション]
[新規作成]

|項目|値|
|:---|:---|
|業務グループ|WG_MNIST|
|アプリケーション名|APL_MNIST|
|アプリケーションタイプ|外部アプリ実行|
|貸出|しない|
|アプリケーションポートグループ|APG_MNIST|
|実行モジュールパス|/home/acms/project/mnist/predict_mnist.sh|

外部アプリ入力パラメーター
[新規作成] ×4

|パラメーター名|必須区分|パラメーター値|デフォルト値|オプション文字列|オプション文字列の出力条件|オプション文字列の連結|
|:--|:--|:--|:--|:--|:--|:--|
|image_file_name|必須|%input_file%|||値があったらオプション文字列と値を出力|しない|
|result_file_name|必須|%result_file%|||値があったらオプション文字列と値を出力|しない|
|model_file_name|必須||||値があったらオプション文字列と値を出力|しない|
|python_script_file_name|必須||||値があったらオプション文字列と値を出力|しない|

[▼ アプリ実行結果情報]

|項目|値|
|:---|:---|
|データ出力|する|

出力パラメーター
[新規作成]

|パラメーター名|必須区分|
|:--|:--|
|predict_value|必須|

[保存]
[OK]
[アクション▼] - [開始]

###フローを作る
Pythonスクリプトを呼び出して、実行結果をもとにファイルを振り分ける処理フローを作成します。
今回、フローが大きくなるため、フローエディタが狭くてアイコンが置けなくなります。そんな時は、ブラウザの表示倍率を調整してフローエディタの表示範囲を広げてください。一度アイコンを置いてしまえば、倍率を100%に戻してもスクロールできるようになります。
アイコンを配置するときの操作方法は、前回の記事を参考にしてください。

[▼マスター] - [▼アプリケーション] - [フロー]
[新規作成]

|項目|値|
|:---|:---|
|業務グループ|WG_MNIST|
|フロー名|MNIST|
|貸出|しない|
|アプリケーションポートグループ|APG_MNIST|
フロー入力パラメーター:
なし

[フローエディター]に切り替える
下図のようにアイコンを配置する

![MNISTフロー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344679/de246d9d-50bf-b19d-43f1-47b425143134.png)
配置しているアイコンは、外部アプリ実行、MATCH分岐、ファイル配備である

それぞれのアイコンは次のように設定する
設定は、外部アプリ実行→ファイル配備→MATCH分岐の順に行う

|項目|値|
|:---|:---|
|フロー名|MNIST|
|ジョブ名|JOB_MNIST|
|アプリケーション名|APL_MNIST|
|実行モジュールパス|(/home/acms/project/mnist/predict_mnist.sh)|

外部アプリ入力パラメーター:

|パラメーター名|必須区分|パラメーター値|デフォルト値|オプション文字列|オプション文字列の出力条件|オプション文字列の連結|
|:--|:--|:--|:--|:--|:--|:--|
|image_file_name|必須|%input_file%|||(値があったらオプション文字列と値を出力)|(しない)|
|result_file_name|必須|%result_file%|||(値があったらオプション文字列と値を出力)|(しない)|
|model_file_name|必須|/home/acms/project/mnist/result/snapshot_iter_12000|||(値があったらオプション文字列と値を出力)|(しない)|
|python_script_file_name|必須|/home/acms/project/mnist/predict_mnist.py|||(値があったらオプション文字列と値を出力)|(しない)|
[OK]

|項目|値|
|:---|:---|
|フロー名|MNIST|
|ジョブ名|JOB_MATCH_PREDICT_VALUE|

後続ジョブ:

|後続ジョブ名|分岐ラベル|分岐条件|
|:--|:--|:--|
|JOB_FILE0|0|\$predict_value\$=="0"|
|JOB_FILE1|1|\$predict_value\$=="1"|
|JOB_FILE2|2|\$predict_value\$=="2"|
|JOB_FILE3|3|\$predict_value\$=="3"|
|JOB_FILE4|4|\$predict_value\$=="4"|
|JOB_FILE5|5|\$predict_value\$=="5"|
|JOB_FILE6|6|\$predict_value\$=="6"|
|JOB_FILE7|7|\$predict_value\$=="7"|
|JOB_FILE8|8|\$predict_value\$=="8"|
|JOB_FILE9|9|\$predict_value\$=="9"|
[OK]

|項目|値|
|:---|:---|
|フロー名|MNIST|
|ジョブ名|JOB_FILE0|
|アプリケーション名|FileLocator|
|ファイル無し時の挙動|エラー終了|
|配備物理ノード名|apex-01-mst|
|配備ディレクトリ|/home/acms/project/mnist/predicted_images/0|
|配備ファイル名|%task_id%|
|注釈|指定しない|
[OK]
1～9に相当するジョブを同様に設定する

[保存]
[OK]
[アクション▼] - [開始]


# 動作確認
判定させる画像を母艦側に用意します。
サイズは任意で構いませんが、できるだけ正方形に近い形にしてください。
画像の形式は(ほぼ)なんでもかまいません。[^2]
下の画像をダウンロードして使っていただいてもかまいません。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344679/7686fada-d257-f50e-989e-23b50d44f589.png) ![8.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344679/aed1b414-1077-ef53-c30e-2e44ae9f415e.jpeg)
画像ファイルを用意したら、フローを起動します。

[▼データ操作] - [ロード（フロー）]

|項目|値|
|:---|:---|
|業務グループ|WG_MNIST|
|フロー名|MNIST|
|データファイル|[ファイル選択]をクリックして2.pngを選択|
|パラメーター|(空白)|
|予約登録|□する(チェックしない)|
|処理予定日時|(空白)|
|強制登録|□する(チェックしない)|

[実行]

ファイルが正しく振り分けられると、predicted_images/2の下にファイルができているはずです。

```shell-session
find predicted_images/
```

実際に実行すると次のようになります。

```shell-session
[acms@apex-01-mst mnist]$ find predicted_images/
predicted_images/
predicted_images/0
predicted_images/1
predicted_images/2
predicted_images/2/20190715133931379000
predicted_images/3
predicted_images/4
predicted_images/5
predicted_images/6
predicted_images/7
predicted_images/8
predicted_images/9
[acms@apex-01-mst mnist]$
```
画像を判定した結果、ちゃんと2のディレクトリにファイルが配置されました。

---
お疲れさまでした。以上で数字を判定して振り分ける仕組みは完成です。
拡張子がないので画像ファイルとして認識しづらいという問題はありますが、一応目的は達成できました！

最後まで読んでいただき、ありがとうございました。

[^1]: ```svn export https://github.com/chainer/chainer/trunk/examples/mnist```
[^2]: イメージファイルの読み込みにはpillowを使っています。読み込めるファイルフォーマットを確認したい方は[オンラインドキュメント](https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html)を参照してください。