AndroidでTensorFlowを使ってアプリ作りたいけど何すりゃいいの(後編・DeepLab用独自データの作成と学習)

# 概要
当記事では[前編](https://qiita.com/j_catfish/items/c5b8d9e035f365b62af6)に引き続き、画像上の物体識別モデル"DeepLab"をAndroid上で利用する手順を軸に、Android上でのTensolFlowの利用法を説明しています(と言いつつあまり説明していない)。

Android向けのコード自体に関しては[前編](https://qiita.com/j_catfish/items/c5b8d9e035f365b62af6)を参照してください。
当記事は次のポイントにスコープしています:

* 独自データセットの用意、学習
* 学習パラメータが結果に与える影響
* 結果から推測されるDeepLabの動作

タイトルとはうらはらに、内容的にはAndroidでの実装とはあまり関係ない記事となっています。
サブタイトルをつけるなら「__DeepLab用独自データの作成と学習__」といったところです。
…わかりやすいようにタイトルに付けときました。

# 前編のあらすじ
* 写真の車のナンバーを機械学習でぼかしたい
* DeepLabでナンバー領域を抽出できそう
* Android上で動かせた
* ナンバープレートを抽出するためのモデルを用意しなければ__←今ここ__

----
DeepLabはこんなことをしてくれる:
![vis1_shrinked.png](https://qiita-image-store.s3.amazonaws.com/0/252248/806ea2c3-d903-ad48-a00a-5fc3a8d2136c.png)

[前編](https://qiita.com/j_catfish/items/c5b8d9e035f365b62af6)ではこれをAndroid上で動かした:
![Screenshot_20180902-132835_shrinked.png](https://qiita-image-store.s3.amazonaws.com/0/252248/58f01d75-f55e-26db-d2f0-02e44b2b7dfa.png)

当記事ではナンバープレートのみを抽出するための学習データ生成をがんばる。

----
実際の作業や試行錯誤を飛ばして何ができたのかを見たい人は、[成果物](#成果物)まで読み飛ばしてください。

# データセットの用意
[前編](https://qiita.com/j_catfish/items/c5b8d9e035f365b62af6)では公開されているサンプルデータを利用して動作させた。

目的達成のためには、それに代わってナンバープレートだけを抽出する学習をほどこしたデータが必要となる。
学習には、ナンバープレートの領域を指示した画像群(=データセット)を用意する必要がある。

具体的には、以下の1対の画像を複数対用意する:

* ナンバープレートを含んだ元画像
* ナンバープレートをマスクしたラベル画像

元画像とラベル画像のファイル名(拡張子を除く)は一致していなければならない。
加えて画像ファイル名を列挙したリストファイルを用意する:

* 各画像のファイル名(拡張子を除く)を列挙したテキストファイル
    * 1行に1ファイル名を記載
    * リストファイル名は任意、拡張子は".txt"
    * 学習用リストなら"train.txt",評価用なら"val.txt"などが標準的

---
元画像、ラベル画像、リストファイルはそれぞれにフォルダを用意して配置する。
典型的なファイル配置は次のようになる:

```
+img
 -元画像ファイル群
+lbl
 -ラベル画像ファイル群
+lst
 -リストファイル群
```

当記事の作例では120対の画像群を用意した。
動作確認だけなら10セットもあれば用は足りる。

----
元画像例:
![src01.png](https://qiita-image-store.s3.amazonaws.com/0/252248/6c676341-b463-3fb8-b30a-41ee3cf4e353.png)
ラベル画像例:
![lbl01.png](https://qiita-image-store.s3.amazonaws.com/0/252248/630e55fb-1343-e2dd-896f-66b17a9f9054.png)
※ナンバープレートを思いっきり晒しているが、記事作成者の車なので開き直っている。

----
普通自動車,軽自動車の画像を手動で収集して利用した。
収集には自動車サイトの試乗記事が非常に役立った。
![src_list.png](https://qiita-image-store.s3.amazonaws.com/0/252248/5dab8747-4ac4-bbec-e413-cc8b06b91277.png)
※判別できないくらいちっこいから載せてもいいよね？

## 用意する画像の要件と作成方法
当記事で使用しているデータセットは、公開されているデータセット"PASCAL VOC 2012"のフォーマットにのっとっている。

用意するべき画像の要件と作成方法については別記事を用意した。
[DeepLab学習用画像の要件](https://qiita.com/j_catfish/items/0b1075c5cd5367406dd1)を参照されたい。

## ポイント
要するに、元画像とマスクした画像を用意すればよい。気を遣う点はほとんどない。

### 画像のサイズ
任意。

### 水増し画像を用意するべきか
不要。
ランダムな左右反転、サイズ拡大縮小が学習スクリプトにより自動でおこなわれる。

### 対象物が見切れた画像を意識的に含めたほうがよいか
不要。
元画像からランダムに抽出された領域が学習に供されるので、おのずと見切れた画像で学習するケースが生じる。

### 識別対象が含まれない対照学習用画像を用意するべきか
ナンバープレートのようでそうではないものを学習させるために必要かと考えたが、不要。
元画像からランダムに抽出された領域が学習に供されるので、そこに識別対象物が含まれない場合がこれに相当する。

### マスクの精度
正確な方が望ましいと思われる。
TensorFlowは与えられたマスクの領域を推論の正解として、推論結果に照らして精度を上げるべく学習に励む。

# 学習
TensorFlow + DeepLab が動作するPC上で学習を行う。
が、そんなPCは持ってない。Atomの7インチPCで開発はすべて賄っている。お金も使いたくない。

おあつらえ向きのクラウド上のサービス、Google CoLaboratory(以降CoLabと記載)を利用する。
Googleアカウントさえ持っていれば無料で利用できる。

利用手順の詳細は他の記事に譲る。注意点は次の通り:

* 一定時間でインスタンスが終了する
* GPUを利用しないと30倍ほど処理時間がかかる
* GPUを利用しすぎると次回に割り当ててもらえなくなる
* 使い込むと自分のマシンが欲しくなる。

## 学習作業の概略
作業の大枠は次の通り:

* データセットをTensorFlow向け形式"TFRecord"に変換
* TFRecordをDeepLab付属の学習用スクリプト train.py に与えて学習させる
    * データセットを指定するためにDeepLabへのパッチが必要
    * train.py へのパラメータで学習動作の詳細が設定される

加えて今回のケースでは次の対処が必要となった:

* 適切な識別結果を得るためのDeepLabへのパッチ
    * 識別対象の種類が少ない場合に、識別物への重みづけを増す必要があった

次のようなノートブックをCoLab上に作成して実現した:

* CoLabにGoogleDriveをマウント
* CoLabの仮想マシンにDeepLabをインストール
* CoLabにデータセットをアップロード
* DeepLabにパッチを当てる
* データセットをTFRecordに変換
* train.pyのパラメータを決定
* train.py実行で学習
* 結果の画像出力
* 結果データをAndroid用にエクスポート

内容が煩雑なので全体の公開はしない。
要点のみ記載する。

## データセットのTFRecord化
deeplab/datasets/build_voc2012_data.py でTFRecordへの変換を行う。

ノートブックのコードセル例:

```python
#TFRecordの生成
!mkdir -p ./dataset/tfrecord
!rm -f ./dataset/tfrecord/*
!python ./master/models-master/research/deeplab/datasets/build_voc2012_data.py --output_dir="./dataset/tfrecord" --image_folder="./dataset/img" --semantic_segmentation_folder="./dataset/lbl" --list_folder="./dataset/lst" --image_format="jpg"
!ls -Ral ./dataset/tfrecord
print("Finished.")
```

次のようなファイルが得られる:

```
train-00000-of-00004.tfrecord  val-00000-of-00004.tfrecord
train-00001-of-00004.tfrecord  val-00001-of-00004.tfrecord
train-00002-of-00004.tfrecord  val-00002-of-00004.tfrecord
train-00003-of-00004.tfrecord  val-00003-of-00004.tfrecord
```

この例でのファイル配置は次の通り:

```
/content :カレントディレクトリ
 +master/models-master/reserch/deeplab :DeepLabインストール先
  -dataset/build_voc2012_data.py :TFRecord生成スクリプト
 +dataset :データセット配置用ディレクトリ
  +tfrecord :TFRecord生成先ディレクトリ
  +img :元画像ディレクトリ
  +lbl :ラベル画像ディレクトリ
  +lst :リストファイルディレクトリ
```

## 学習実行

### パッチ
#### 必須のパッチ
deeplab/datasets/segmentation_dataset.py にデータセット緒元を追記する必要がある。

```python:deeplab/datasets/segmentation_dataset.py
# ...略...

# 以下のブロックを追記
_ORIGINAL_INFORMATION = DatasetDescriptor(
    splits_to_sizes={
        'train': 120, # 学習に使用するデータセット数
        'val': 71, # 評価に使用するデータセット数
    },
    num_classes=2, # 識別物体種類数(背景+物体)
    ignore_label=255, # 学習時に無視する画素値
)

_DATASETS_INFORMATION = {
    'cityscapes': _CITYSCAPES_INFORMATION,
    'pascal_voc_seg': _PASCAL_VOC_SEG_INFORMATION,
    'ade20k': _ADE20K_INFORMATION,
    'original': _ORIGINAL_INFORMATION, # 追記:データセット種別"original"
}

# ...略...
```

上記の例では次のようなデータセットを定義している:

* データセット種別 = "original" :学習実行時のコマンドラインパラメータでこの名前を指定する
* 使用するTFRecord :リスト名"train" データ数120 ,リスト名"val" データ数71
* 識別物体種類数 = 2 (背景1 + ナンバープレート1)
* 無視するラベル画素値 = 255 (マスクの境界線などに用いる画素値)

リスト名はデータセットのリストファイル名(拡張子を除く)と一致する必要がある。

#### 場合によって必要なパッチ
サンプルのPASCAL VOC 2012のようなデータセットでは問題なかったが、今回のように識別物体が1種類で領域サイズが小さい場合、対象物体の識別比重を大きくとる必要があった。
参考:[DeepLabv3+ on your own dataset | DelphiFan's Blog](http://hellodfan.com/2018/07/06/DeepLabv3-with-own-dataset/)

`deeplab/utils/train_utils.py` を以下のように修正して対応した。

```python:deeplab/utils/train_utils.py
    scaled_labels = tf.reshape(scaled_labels, shape=[-1])
# コメントアウト
#    not_ignore_mask = tf.to_float(tf.not_equal(scaled_labels,
#                                               ignore_label)) * loss_weight

# 追加
    ignore_weight = 0
    label0_weight = 1
    label1_weight = 180

    not_ignore_mask = \
        tf.to_float(tf.equal(scaled_labels, 0)) * label0_weight + \
        tf.to_float(tf.equal(scaled_labels, 1)) * label1_weight + \
        tf.to_float(tf.equal(scaled_labels, ignore_label)) * ignore_weight
# 追加ここまで
    one_hot_labels = slim.one_hot_encoding(
        scaled_labels, num_classes, on_value=1.0, off_value=0.0)
```

ナンバープレート識別の重みづけ係数は180とした。この値はトライ＆エラーで経験的に求めた。

### イニシャルチェックポイントの用意
オリジナルデータを学習しようとする場合でも、元となるデータ(=イニシャルチェックポイント)が必要となる。

次のようなコードセルで、サンプルデータをダウンロード・展開してイニシャルデータとした:

```python
#シェルコマンドを実行してリアルタイムで出力を表示するメソッドの宣言
#一回実行しておく
import subprocess, time, os, sys

def shell(cmd,showcmd=False):
  if showcmd :
    print(cmd)
  p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  for line in iter(p.stdout.readline, b''):
    sys.stdout.write(line)
```

```python
#イニシャルチェックポイントの用意
INIT_DIR="./init_ckpt"
INIT_SITE="http://download.tensorflow.org/models"

#xception_65
#INIT_FILE="deeplabv3_pascal_train_aug_2018_01_04.tar.gz"

#mobilenet-v2
INIT_FILE="deeplabv3_mnv2_pascal_train_aug_2018_01_29.tar.gz"

shell("mkdir -p %s" % INIT_DIR)
shell("wget -nd -c '%s/%s' -O '%s/%s'" % (INIT_SITE,INIT_FILE,INIT_DIR,INIT_FILE))
shell("tar -C '%s' -xf '%s/%s'" % (INIT_DIR,INIT_DIR,INIT_FILE),True)

#xception_65
#INIT_MODEL_PATH=INIT_DIR+"/deeplabv3_pascal_train_aug/model.ckpt"

#mobilenet-v2
INIT_MODEL_PATH=INIT_DIR+"/deeplabv3_mnv2_pascal_train_aug/model.ckpt-30000"

#継続学習フラグクリア(最初から)
TRAIN_CONTINUOUS=False
```

上記の実行で`./init_ckpt/deeplabv3_mnv2_pascal_train_aug/`に次のファイルが展開される:

```
frozen_inference_graph.pb
model.ckpt-30000.data-00000-of-00001
model.ckpt-30000.index
```
イニシャルチェックポイントとして指定するのは `./init_ckpt/deeplabv3_mnv2_pascal_train_aug/model.ckpt-30000` 。拡張子は不要。

### 実行
こんなようなコードセルで学習を実行できる:

```
!export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/master/models-master/research/slim:`pwd`/master/models-master/research;python ./master/models-master/research/deeplab/train.py  --dataset=original --logtostderr --train_split=train --model_variant=mobilenet_v2 --output_stride=8 --train_crop_size=513 --train_crop_size=513 --train_batch_size=2 --training_number_of_steps=1000 --train_logdir='./dataset/trainlog' --dataset_dir='./dataset/tfrecord' --tf_initial_checkpoint='./init_ckpt/deeplabv3_mnv2_pascal_train_aug/model.ckpt-30000' --initialize_last_layer=False
```

この例はここまでの作業内容に対してそのまま実行できる。
長すぎるので分解して解説する。

* ```export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/master/models-master/research/slim:`pwd`/master/models-master/research```
    * train.py実行に必要なパスをPYTHONPATHに追加
* ```python ./master/models-master/research/deeplab/train.py ...```
    * 学習スクリプト train.py を実行

この例での train.py のパラメータは次の通り:

|パラメータ|意味|
|---|---|
|--dataset=original |データセット種別名<br>…segmentation_dataset.py へのパッチで指定|
|--logtostderr |ログを標準エラー出力へ出力|
|--train_split=train |データセット名<br>…リストファイル名とsegmentation_dataset.py へのパッチで指定|
|--model_variant=mobilenet_v2 |ネットワークバックボーン|
|--output_stride=8 |アウトプットストライド|
|--train_crop_size=513 |クロップサイズ(縦)|
|--train_crop_size=513 |クロップサイズ(横)|
|--train_batch_size=2 |バッチサイズ|
|--training_number_of_steps=1000 |学習回数|
|--train_logdir='./dataset/trainlog' |学習結果出力ディレクトリ<br>…なければ作られる|
|--dataset_dir='./dataset/tfrecord' |TFRecordディレクトリ|
|--tf_initial_checkpoint='./init_ckpt/deeplabv3_mnv2_pascal_train_aug/model.ckpt-30000' |イニシャルチェックポイント名<br>…ファイル名ではない。拡張子は不要|
|--initialize_last_layer=False |イニシャルチェックポイントの内容引継ぎ指定<br>…Falseなら1から学習|

----
この例で学習した結果は次のようになる:
所要時間 :591.109sec (約10分)
![vis_codecellsample.png](https://qiita-image-store.s3.amazonaws.com/0/252248/d69ebba8-d2bd-aba8-87c6-6ea6fec8bdb8.png)


# 学習パラメータのポイント
学習処理が動作し曲がりなりにも結果が得られた後には、学習パラメータを操作して良好な結果を求める反復作業に入ってゆくことになる。

ここでは学習実行にあたっての要点だけを記載する。
学習結果に与える影響については[後述](#学習パラメータの影響)する。

## 学習毎に設定するもの
###イニシャルチェックポイント
学習の元となるデータ。
1から学習しようとする場合でもイニシャルチェックポイントの指定は省略できない。
元となるチェックポイントが必要となる。

#### 1から学習するには
* --tf_initial_checkpoint=元となるチェックポイント
* --initialize_last_layer=False …学習結果を引き継がない

元となるチェックポイントと新規学習とで識別物体種類数が異なるなどの差異があっても無視される。

#### 引き継いで学習するには
* --tf_initial_checkpoint＝従前のチェックポイント
* --initialize_last_layer=True …学習結果を引き継ぐ

従前のチェックポイントと継続する学習とでは識別物体種類数などが一致していなければならない。

#### 続けて学習するには
`--train_logdir`(学習結果出力ディレクトリ) にチェックポイントがある場合、そのチェックポイントの続きから学習が行われる。
この時、`--tf_initial_checkpoint`は無視される。
また、クロップサイズやアウトプットストライドなど途中で変更可能なパラメータが変わっている場合は従前のパラメータではなく新しいものが適用される。

例えば1000回の学習を終えて、さらに `--training_number_of_steps=2000` に変えて学習を開始すると1000回の結果の続きから始まり、前回までと合わせて計2000回の学習が行われる。
また、実行を中断して再度同じパラメータで学習を開始した場合は、途中で自動でセーブされているチェックポイントから学習が再開される。

#### 備考
deeplab/train.py を修正することでイニシャルチェックポイントを無視して0から学習を実行できるらしいが、試した限りでは推論結果が得られなかった。
膨大な学習回数が必要なのかもしれないが試せる環境がないので断念した。
参考:[最強のSemantic Segmentation、Deep lab v3 plus](https://qiita.com/harmegiddo/items/b11decca0769fc108ec9)

### 学習回数
* --training_number_of_steps

学習回数×バッチサイズが学習した画像セットの数となる。
出力品質に強く影響する。
0にはできない。

これが大きいと学習終了までたっぷり待つことになる。
ただし学習中のメモリ使用量には影響を与えない。
とりあえず1000くらい回しておくと何らかの結果が見て取れる。

### バッチサイズ
* --train_batch_size

一度に学習する画像セットの数。
出力品質に影響する。
0にはできない。

これが大きくても学習終了までたっぷり待つことになる。
さらに、メモリ不足で学習に失敗する場合が出てくる。
とりあえず1でも動作する。

## 途中で変更できないもの
### ネットワークバックボーン
* --model_variant

ネットワークと言ってもLANやLTE等のことではなく、ニューラルネットワークを指す。
2種類が利用できることを確認している:

* mobilenet_v2
    * 小さい、速い、モバイル向け
    * 当記事ではこれを採用する
* xception_65
    * データが大きくなるのでAndroid端末に載らない

### デコーダ
* --decoder_output_stride

DeepLabV3+の"+"の部分に相当する処理。
出力品質が向上するはず。

`--decoder_output_stride` に数値を指定することで有効になる。
DeepLab公式のサンプルデータでは4が指定されている。

## 途中で変更できるもの
### アウトプットストライド
* --output_stride

小さいほど学習が緻密になる。
それに比例して処理所要時間と学習時のメモリ使用量が増大する。
とりあえず8くらいが無難。

### クロップサイズ
* --train_crop_size

演算時の画像サイズ。
縦横の順に2回オプションを指定する。
縦横幅画素数+1の値を与える。

学習時には元画像のランダムな位置からこのサイズが切り取られて利用される。
このサイズより小さな画像の場合は余剰部分が無効画素値で埋められる。

推論実行時にはこのサイズに収まるように元画像を縮小して与える必要がある。
すなわち、推論の解像度がこのパラメータで決定される。

大きくとるほど処理所要時間と学習時のメモリ使用量が増大する。
とりあえず513が適切。1025とかを与えるとあっぷあっぷになる。

# 学習結果の確認
deeplab/vis.py で学習結果を画像ファイルにして出力できる。
![vis_list.png](https://qiita-image-store.s3.amazonaws.com/0/252248/44e012bc-8f68-bd4e-cffd-63ccc0575af1.png)

次のようなコードセルでビジュアライズを実行できる:

```
!export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/master/models-master/research/slim:`pwd`/master/models-master/research;python ./master/models-master/research/deeplab/vis.py --dataset=original --logtostderr --vis_split=val --model_variant=mobilenet_v2 --output_stride=8 --vis_crop_size=1025 --vis_crop_size=2049 --colormap_type='pascal' --vis_logdir='./dataset/vislog' --dataset_dir='./dataset/tfrecord' --checkpoint_dir='./dataset/trainlog' --eval_interval_secs=0 --max_number_of_iterations=1
```

この例は上で挙げた学習実行コードセルの実行結果に対してそのまま実行できる。
`./dataset/vislog`以下に画像が生成されるのでZIPに固めるなりなんなりしてどうにかするとよい。

行が長すぎるので分解して解説する。

* ```export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/master/models-master/research/slim:`pwd`/master/models-master/research```
    * vis.py実行に必要なパスをPYTHONPATHに追加
* ```python ./master/models-master/research/deeplab/vis.py ...```
    * ビジュアライズスクリプト vis.py を実行

この例での vis.py のパラメータは次の通り:

|パラメータ|意味|
|---|---|
|--dataset=original |データセット種別名<br>…segmentation_dataset.py へのパッチで指定|
|--logtostderr |ログを標準エラー出力へ出力|
|--vis_split=val |データセット名<br>…リストファイル名とsegmentation_dataset.py へのパッチで指定|
|--model_variant=mobilenet_v2 |ネットワークバックボーン|
|--output_stride=8 |アウトプットストライド|
|--vis_crop_size=1025 |クロップサイズ(縦)|
|--vis_crop_size=2049 |クロップサイズ(横)|
|--colormap_type='pascal' |出力画像カラーマップタイプ|
|--vis_logdir='./dataset/vislog' |結果出力ディレクトリ|
|--dataset_dir='./dataset/tfrecord' |TFRecordディレクトリ|
|--checkpoint_dir='./dataset/trainlog'|学習結果ディレクトリ|
|--eval_interval_secs=0 |出力後待機時間|
|--max_number_of_iterations=1 |出力反復回数|

* クロップサイズ …学習時と異なり、元画像の最大サイズを設定する
* --eval_interval_secs=0, -max_number_of_iterations=1 …これを設定しておかないとvis.pyが終わらない

# 学習結果のエクスポート
学習結果であるチェックポイントデータは、凍結モデル(frozen_inference_graph.pb …オフィシャルな訳語はないのかな?凍結推論グラフ?)にエクスポートしてAndroidに搭載する。
ただしAndroidで動作させるにはエクスポート用スクリプト `deeplab/export_model.py` を改変する必要がある。

改変内容は以下を参照のこと:
[Android端末向けDeepLab用モデルのエクスポート#Android向け学習済みモデルの生成](https://qiita.com/j_catfish/items/b5cc4406c686e6852e03#android%E5%90%91%E3%81%91%E5%AD%A6%E7%BF%92%E6%B8%88%E3%81%BF%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E7%94%9F%E6%88%90)

次のコードセルでパッチを当てた `export_model-mobile.py` をカレントディレクトリに生成できる:

```python
#export_model.pyにパッチを当てたものをカレントディレクトリに生成
#元ファイルをカレントディレクトリにコピー
!cp master/models-master/research/deeplab/export_model.py ./export_model-mobile.py

#パッチ用差分ファイルを作成
diff='''131c131
<         predictions[common.OUTPUT_TYPE],
---
>         tf.cast(predictions[common.OUTPUT_TYPE], tf.int32),
'''

f = open('./patch','w')
f.write(diff)
f.close()

#パッチを適用
shell("patch ./export_model-mobile.py ./patch")

print("Finished.")
```

改変した `./export_model-mobile.py` を用いて、以下のようなコードセルで凍結モデルをエクスポートできる:

```
!export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/master/models-master/research/slim:`pwd`/master/models-master/research;python ./export_model-mobile.py --checkpoint_path='./dataset/trainlog/model.ckpt-1000' --export_path='./dataset/frozen_inference_graph.pb' --num_classes 2 --model_variant=mobilenet_v2 --output_stride=8
```
この例は上で挙げた学習実行コードセルの実行結果に対してそのまま実行できる。
長すぎるので分解して解説する。

* ```export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/master/models-master/research/slim:`pwd`/master/models-master/research```
    * export_model.py実行に必要なパスをPYTHONPATHに追加
* ```python ./export_model-mobile.py ...```
    * エクスポートスクリプト export_model-mobile.py を実行

この例での export_model-mobile.py のパラメータは次の通り:

|パラメータ|意味|
|---|---|
|--checkpoint_path='./dataset/trainlog/model.ckpt-1000' |エクスポート対象のチェックポイントポイントへのパス|
|--export_path='./dataset/frozen_inference_graph.pb' |エクスポートファイルのパス|
|--model_variant=mobilenet_v2 |ネットワークバックボーン|
|--num_classes 2 |識別物体種類数|
|--output_stride=8 |アウトプットストライド|

実行するとAndroid向けのエクスポートファイル `./dataset/frozen_inference_graph.pb` が生成される。
これを端末にコピーして利用する。

なお、ここでも以下のオプションが指定できる:

* --crop_size …指定しなければデフォルト値513が適用される

----
ここまでの例の結果をエクスポートしてAndroid上で実行したもの:
![screenshot_codecellsample.png](https://qiita-image-store.s3.amazonaws.com/0/252248/70750bff-f349-b2aa-99d0-0c0e67366722.png)
ビジュアライズの結果と若干異なるのはクロップサイズの違いによるものかもしれない。

# アプリへのモデルの配置
凍結モデルをAPKのassetsに埋め込むのであれば次のようなコードで動作する:

```java
	String ASSETFILE = "frozen_inference_graph.pb";

	final AssetManager assetManager = context.getAssets();
	InputStream is = null;
	try {
		is = assetManager.open(ASSETFILE);
	} catch (IOException e) {
		e.printStackTrace();
	}
	
	if(is==null) return false;
	
	//TensorFlowにモデルファイルを読み込む
	mTFInterface=new TensorFlowInferenceInterface(is);
	
	//モデルファイルを閉じる
	try {
		is.close();
	} catch (IOException e) {
		e.printStackTrace();
	}
```

# 学習パラメータの影響
実際のアプリで利用する場合、このあたりのパラメータを試行錯誤して適切な結果を探求することになる。

## データセット数
パラメータではないが学習の質に大きく寄与する。
多いに越したことはない。

データセット数の違いによる結果の差異を次に示す:
(バッチサイズ2,アウトプットストライド8,デコーダなし,クロップサイズ513)

__データセット数10__,学習回数1000,4000,16000の各結果:
![ds10-step1000_4000_16000.png](https://qiita-image-store.s3.amazonaws.com/0/252248/01fc1576-eae6-3ef6-1354-b390e60a278a.png)

__データセット数100__,学習回数1000,4000,16000の各結果:
![ds100-step1000_4000_16000.png](https://qiita-image-store.s3.amazonaws.com/0/252248/311f4ac2-457a-c798-8a60-c85a70b6772b.png)

……あんまかわんねーじゃん

ということはなく、データセット10の方では領域の絞り込みが進み過ぎてマスクすべき領域が虫食いになったような結果が多数見受けられる。
ネットからとってきた画像ばかりなので著作権的に元画像の掲載は控えるが、下のような状態が現れる:
![overfitting.png](https://qiita-image-store.s3.amazonaws.com/0/252248/c7a3be74-1dfa-5660-322f-0cc735eb7714.png)

緑のナンバープレートの領域に対して赤い部分が推論結果。

恐らくこれが__過学習__という現象の発現だろう。
少ない教材データで学習しまくったので、教材と同じような問題しか解けなくなっちゃってるようだ。

## 学習回数
ナンバープレートのみを検出する今回のケースにおいては、最初は広く得られる検出領域が学習回数に応じて正しい形に収束してゆく傾向にある。
学習しすぎると期待する領域より小さく検出される。これがいわゆる過学習の結果と考えられる。

結論としては、学習回数は多すぎても良い結果が出るわけではない。ちょうどいいところを見つける必要がある。

## バッチサイズ
学習回数×バッチサイズが学習した画像の総数となる。
画像の総数が等しければ学習の質も等しいかと言えば、そうでもないらしい。
学習回数4000×バッチサイズ8 と 学習回数16000×バッチサイズ2 は学習した画像の枚数は同じ32000だが、異なる結果をもたらす。らしい。
確かめてみた。

![sameepoc-32000.png](https://qiita-image-store.s3.amazonaws.com/0/252248/c41b9b3b-78b7-698e-6f58-5bdd485f2f4b.png)
左が学習回数16000×バッチサイズ2、右が学習回数4000×バッチサイズ8 の結果。
どちらも画像枚数は同じだが学習進度に違いが出ている。

![samestep-4000.png](https://qiita-image-store.s3.amazonaws.com/0/252248/46c49b33-16f3-a59d-b145-0154921231e5.png)
左がバッチサイズ2、右がバッチサイズ8、どちらも学習回数4000の結果。
近い程度の学習進度が得られている。
学習進度はおおむね学習回数によって決まることがわかる。

![batch2_8-16000.png](https://qiita-image-store.s3.amazonaws.com/0/252248/70bdddaf-fdc2-a864-cc17-5cae10b1752a.png)

左がバッチサイズ2、右がバッチサイズ8、どちらも学習回数16000の結果。
同じ学習回数でもバッチサイズが大きいほうが学習結果の収束が遅い傾向がある。

同じ学習回数でもバッチサイズが大きいほうが時間がかかる事に注意。
バッチサイズ2に対してバッチサイズ8では処理画像総数は4倍で、単純に4倍の時間がかかる。

一般にバッチサイズが大きいほど多様な入力に対する出力が好成績となるらしい。
本件でもその傾向はみられたような気がする。
が、バッチサイズは学習時に使用するメモリ量に大きく関与するため、あまり多くのバッチサイズは試すことができなかった。

----
まとめると次の指針が得られる:

* 動作確認にはバッチサイズは少なめで良い
* 最終的な製品品質を向上させる段階でバッチサイズを増やして学習

## アウトプットストライド
推論結果出力の刻み幅。
小さいほど高精度。かつ処理所要時間と使用メモリが増大する。

DeepLabのドキュメントではOSと略されているのでここでもそうする。

同じ条件でOSだけを変えた学習結果を示す。
バッチサイズ2,学習回数4000での結果:
![os4_8-4000.png](https://qiita-image-store.s3.amazonaws.com/0/252248/6cc031df-b5d9-fa11-1776-ed11d272cc40.png)
左がOS4、右がOS8。

よくわかんないので拡大する。左のOS4の方がディティールが細かい:
![os4_8-4000-enlarged.png](https://qiita-image-store.s3.amazonaws.com/0/252248/3475c423-5d29-84bb-5eab-93f222dcf6d7.png)

…たいしてかわんないじゃん
傾向が顕著な例を示す:
![os4_8-4000-enlarged2.png](https://qiita-image-store.s3.amazonaws.com/0/252248/2648e437-e898-2e5d-7403-4a861eda7653.png)
左がOS4、右がOS8。緑の部分が正解のラベル(グランドトゥルースと呼ぶらしい)。

さらに学習を進めて比較してみた。
バッチサイズ2,学習回数16000での結果:
![os4_8-16000.png](https://qiita-image-store.s3.amazonaws.com/0/252248/0a6c930f-dcb5-0906-b792-10d54c2be270.png)
左がOS4、右がOS8。

OSが小さいほうが過学習気味?な傾向を見せてきた。
顕著な例:
![os4_8-16000-overfitting.png](https://qiita-image-store.s3.amazonaws.com/0/252248/27ee6fb6-3340-ac12-8f4f-f6417d52da00.png)
左がOS4、右がOS8。

----
学習時、ビジュアライズ時、エクスポート時にそれぞれ別のOS値を指定できる。
学習時にはこの刻み幅で出力された推論結果と正解との誤差を学習してゆくことになる。んだと思う。
ビジュアライズ時にはこの刻み幅で出力された推論結果を画像化している。んだと思う。
エクスポート時にはランタイムで用いられる出力刻み幅を設定していることになる。んだと思う。

----
得られた知見を指針としてまとめると次の通り:

* OSが小さいほど望ましい結果が得られるわけでもない
* 学習をOS小で実施すると学習の質が高いが学習時間が激増する
    * OSを半分にすると学習時間は4倍近く増える
* エクスポートするときのOSは端末上での処理時間を鑑みて決めればよい
    * 学習時とは別の値を設定できる

## クロップサイズ
小さいほど低精度。かつ高速度。
推論の解像度はこれで決まる。

学習の際には、クロップサイズで指定された縦横の画素幅の矩形領域を元画像から切り取ったものが学習に供される(加えてランダムな拡大縮小も施される)。
元画像(1242×932)、それをクロップサイズ513、257で切り取った各画像は次の通り:
![cropsizes.png](https://qiita-image-store.s3.amazonaws.com/0/252248/6e2c6617-5d22-824a-19b2-7052df52f3e8.png)
なお、257、513といった数字の気持ち悪さは、設定値としては画素幅+1を与えることに由来する。これらの設定値で実際には256や512が領域サイズとして適用される。

解像度と対象物の平均的な大きさによって、対象物がはみ出しがちになる場合や対象物が小さすぎてしまう状況が発生するかもしれない。
当記事で対象としているナンバープレートの識別の場合、クロップサイズ257でも十分に収まりきる場合がほとんどと考えられる。
ので、クロップサイズ513と257で違いが生じるのかを確認してみた。

同じ条件でクロップサイズだけを変えた学習結果を示す。なお、ビジュアライズ時のクロップサイズは他の画像と同じにしてある。
バッチサイズ2,学習回数4000での結果:
![crop257_512-4000.png](https://qiita-image-store.s3.amazonaws.com/0/252248/8c2ab072-74d6-31de-102e-8b8d8a7dd91d.png)
左がクロップサイズ257、右が513。
クロップサイズ257の方が収束が早い。ただし、上の画像の範囲外では下のようなノイズが生じていた:
![crop257_noize.png](https://qiita-image-store.s3.amazonaws.com/0/252248/5c05aa8b-74c2-7a4f-6b7c-d869c8004354.png)

さらに学習回数16000での結果:
![crop257_512-16000.png](https://qiita-image-store.s3.amazonaws.com/0/252248/f314c2e3-2dc5-1b53-8630-c4c0c3f18c90.png)
クロップサイズ257の方が明らかに品質が悪い。
これ以外の結果を見ると、クロップサイズ257の方が領域の収束がなぜか遅くなっている傾向がある。

総じてクロップサイズを小さくすることで学習結果の品質は下がるようだ。
なお、処理速度とメモリ使用量はクロップサイズの面積に比例する。縦横半分にすれば4倍速くなるというメリットもある。

----
理屈は解き明かしていないが使う際の指針は得られた:

* クロップサイズは大きいほうが品質が良い

## エクスポート時のアウトプットストライドとクロップサイズ
学習時、ビジュアライズ時とは別にエクスポート時にもアウトプットストライドとクロップサイズを指定できる。
結果品質に大きく影響するので触れておく。

OS8、バッチサイズ2、学習回数4000回の学習結果を異なるOS、クロップサイズでエクスポートした場合の実行結果。

クロップサイズ513の場合:
![export_crop513.png](https://qiita-image-store.s3.amazonaws.com/0/252248/2a3143f6-9f0b-9596-2f5b-dd8072d6c249.png)

クロップサイズ257の場合:
![export_crop257.png](https://qiita-image-store.s3.amazonaws.com/0/252248/363d6812-671c-3bc5-ea27-71cb929be4d2.png)

クロップサイズ129でのエクスポートでは抽出領域が得られなかった。

----
結論としては、

* エクスポート時のクロップサイズは学習時程度のサイズがよい
* エクスポート時のアウトプットストライドは小さいほうがやや結果が細かいが、実行時間も大幅に遅くなる

## デコーダ
利用すると出力品質が向上する「はず」。
1.5倍ほど遅くなる。

推論結果をさらに正解と照らし合わせて出力を最適化してくれるモデルのようだ。たのもしい。

学習時のパラメータに`--decoder_output_stride=N`をつけることで有効になる。Nには4とか8とかを充てる。公式のサンプルでは4を適用していたのでここでもそれに倣う。
ビジュアライズやエクスポートのパラメータにもつける必要がある。つけないと真っ黒な結果が得られる。

デコーダの有無による結果の違いは次の通り。
バッチサイズ2,学習回数4000での結果:
![decoder_nodecoder-4000.png](https://qiita-image-store.s3.amazonaws.com/0/252248/9c0cc070-b200-5fca-3c5a-543962667506.png)
左がデコーダあり、右がなし。
学習進度が大幅に違う。

バッチサイズ2,学習回数16000での結果:
![decoder_nodecoder-16000.png](https://qiita-image-store.s3.amazonaws.com/0/252248/90d2226e-27c2-d37c-baed-5b8f48f10104.png)
左がデコーダあり、右がなし。
デコーダありだと過学習?気味。学習進度が速いのかもしれない。
ただ、他の結果では余計なものを検出してしまう例が多くみられた。
なお、デコーダの有無によってエクスポートしたファイルのサイズに差はなかった。

出力品質が向上するはずなのだが、今回のケースでは利点は見られなかった。
ナンバープレートという単純な形状故の結果かもしれない。

----
まとめると、

* デコーダは無効でもよいかもしれない

# 成果物
当記事の内容を実践して作成されたアプリを公開した。

[__ナンバーぼかしML (概念実証版)__](https://play.google.com/store/apps/details?id=catfish.android.chaenomeles.poc)
![chaenomeles01.png](https://qiita-image-store.s3.amazonaws.com/0/252248/9a732260-38f3-3b5e-e78b-0cf0d2389ba9.png)

## 作例
色んな車を載せたかったけど他人の車を勝手に載せるわけにもいかないので勘弁願いたい。

![imagesample01.jpg](https://qiita-image-store.s3.amazonaws.com/0/252248/42ab2d3c-dc36-b4e4-ce20-05b399716d1f.jpeg)
![imagesample02.jpg](https://qiita-image-store.s3.amazonaws.com/0/252248/1944faba-1db6-31ee-0b0c-8a27725b9f79.jpeg)
![imagesample03.jpg](https://qiita-image-store.s3.amazonaws.com/0/252248/e16754c9-181b-b237-a8ca-478fd47ffc8e.jpeg)

## 学習内容
初版での学習内容は次の通り:

* 日本の普通自動車/軽自動車のナンバープレート画像120枚のデータセット
* バッチサイズ3,学習ステップ数28000
* アウトプットストライド=4,クロップサイズ=513
* ネットワークバックボーン=mobilenet_v2
* デコーダ不使用

## 注意点
* 概念実証を目的としているので実装は甘く動作は不安定
    * ただし不安定さは画像のぼかし処理など、機械学習に関連する部分以外に起因する …__機械学習の実行部分は十二分に安定している__
* サイズがやたら大きくなっている(34MB)が、これは機械学習を用いたことに起因するものではない
    * サポートライブラリを多用したことにより膨れ上がっている
* 処理完了までに時間がかかるが、機械学習による推論の所要時間は30秒ほどで一定している。
    * 遅さはぼかし処理の実装が適当なことに起因する

## おまけ
ユーザーが独自に用意した凍結モデルを利用して動作させることを可能にしてある。

`<内部共有ストレージ>/chaenomeles` ディレクトリにDeepLabの凍結モデル(Android向けにエクスポートした"frozen_inference_graph.pb")を置くことで、それを使用した推論が行われる。

モデルを用意すれば車のナンバーに限らず、人の顔など任意の物体をぼかすことが可能になっている。
でかい画像の広い範囲をぼかすとアプリが落ちるので注意。

# 結果の観察
結果を観察することでどんなふうに推論されているのかがぼんやりわかる。

## 日本車以外のナンバープレート
今回、日本の普通自動車/軽自動車の画像のみでの学習を行った。
にもかかわらず、外国のナンバープレートもかなり正確に領域を抽出できている。
著作権的にクリアな画像が見つからないので掲載できないのが残念だが、EUの横長のプレートでもかなり正確に抽出された。

外国のナンバーだけではなく、自動車教習所の教習車においてナンバープレートの上の仮免許掲示も抽出していた。

これらの事実から、本件でのモデルは「車に張り付いた四角い文字板」を識別するように学習が進んでいると思われる。
つまり、識別対象の周辺情報も含めて推論をおこなっていることがわかる。

## ナンバープレートと形状が似たもの
道路標識などの四角い文字板であっても、自動車に付帯したもの以外は抽出されない。
ちょうどよい画像があったので掲載する。
四角い文字板が車の周りにいくつかあるが、ナンバープレート以外は抽出されていない。
![CAM00531_MASK.jpg](https://qiita-image-store.s3.amazonaws.com/0/252248/fb65de64-6945-9f7e-c0b6-21d6c858ebe7.jpeg)
やはり対象の周辺情報が重要であることがわかる。

## トラック、バイク
普通自動車/軽自動車で学習を行ったため、トラックやバイクのナンバー抽出はかなり弱くなっている。というかびっくりするほど検出しない。

ナンバープレートの周辺が学習時の教材と著しく異なるため識別できないでいると考えられる。

## ナンバープレートと同時に車体も識別対象とした場合
上述の事実からナンバープレートの周辺の識別を強化すればより精度が上がるのではないかと考え、車体とナンバープレートを識別対象としたデータセットを用いて実験してみた。

結果はこちら。どちらもバッチサイズ2、16000回の学習、ただしデータセット数10:
![body_nobody-16000.png](https://qiita-image-store.s3.amazonaws.com/0/252248/f8c6055c-07b6-d287-ad0d-8cdea828b7fc.png)

あんまり芳しくない結果となったのでこれ以上の追及はしていない。

# 所感
## アプリのプログラミングは簡単
機械学習を利用する箇所の実装は、アプリ作成中級者レベルなら難なくこなせる。

## 学習は大変
次の2点において大変:

* 多量の学習用データを用意するのが大変
    * 単純作業に心が折れそうになる。"純粋IT土方"というワードが脳裏に浮かぶことになる
* 多量の学習時間が必要なのが大変
    * 最適な結果を得られる学習パラメータを試行錯誤するための反復作業が必要になるが、この学習時間の長さがネックとなる

## 独自の推論モデルを作成することは異次元
アプリのプログラミングとは全く異なるスキルが必要となる。
逆に言えば、機械学習そのもののスキルが無くても推論モデルさえあればそれを利用するアプリは作ることができる。

## プログラミングの概念が変わる
今回ナンバープレートを抽出する処理を実現するにあたって、画像の特徴抽出などのコードは一切書かずに終わっている。
これはすごいことだと思う。

そういう画像処理ライブラリを利用することに似ているようだが、感触は違う。
アルゴリズムなどを検討する必要もまったくなく、機械学習にまかせたきりで完成している。

実際に本件の成果物のアプリでは、学習済みモデルのファイルを置き換えるだけでナンバープレート以外のものをぼかす処理が実現できる。
コードの書き換えは一切不要だ。必要なのは学習作業のみなのだ。

抽出した領域をぼかす処理は従来のように地味に画素をいじる操作を実装したが、この処理にしても適切な推論モデルを用意できれば機械学習に任せることは可能であり、画像処理のすべてを任せきりにしてしまうことも十分に現実的だ。

その場合、本件でのプログラミングは元画像を配列にして渡せば処理済みの画像が配列で返ってくるだけの簡単なお仕事となる。
そして大変なのは学習用データの用意とパラメータのチューニングだ。そこにプログラミングのスキルはあまり関係ない。

----
多量の学習用画像を作りながら、IT土方という言葉のパラダイムシフトが画面の向こうに透けて見えた。
同時に大きな可能態も確かに見えた。