Object Detection APIのsaved_model出力

TensorFlow Object Detection APIでGoogle Cloud MLのモデルサービス(モデルのデプロイ)をするためのsaved_model作成方法を書きます。調査が結構面倒でプログラムまで作成しました。
※Google Cloud MLでなければ[だめだった方法2 - チェックポイントファイルからエクスポート](#%E3%81%A0%E3%82%81%E3%81%A0%E3%81%A3%E3%81%9F%E6%96%B9%E6%B3%952---%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%8B%E3%82%89%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88)で大丈夫っぽいです。

# 関連記事
- [TensorFlow Object Detection APIをUbuntuにインストール](https://qiita.com/FukuharaYohei/items/5e4081ba8f7f9255a8f6)
- [Object Detection APIを使って堀川君を検知](https://qiita.com/FukuharaYohei/items/fcad2d74f2b753191548)
- [Access Tokenを取得してGoogle Cloud MLでオンライン予測](https://qiita.com/FukuharaYohei/items/5face08fa7d07bbe11fe)
- [Object Detectionのためのアノテーション(ラベリング)](https://qiita.com/FukuharaYohei/items/9bd1be8ba04430c1ebea)


#環境
|種類|バージョン|内容|
|:--|:--|:--|
|OS|Ubuntu18.04.01 LTS|Windows8.1からOracle VirtualBoxを使って動かしています|
|Python|3.5.6|Python3.5.6をpyenvで動かしています。Google Cloud MLのランタイム1.10を使うのでPythonは3.7を使いません|
|TensorFlow|1.10|Google Cloud MLのランタイム1.10を使うのでTensorFlowは1.12を使いません|
※後で気になって調べたのですが、2019/1/5時点でもTensorFlow1.12が使えました。日本語の情報だと1.10までしかなかったのですが、英語で見ると1.12も使えました。失敗したー。

# 前提
- pyenvはインストール済
- GCPのアカウントを持っていること
- Google Cloud SDKインストール済
- TensorFlow Object Detection API インストール済
- 訓練済みチェックポイントファイル作成済

※[記事「Object Detection APIを使って堀川君を検知」](https://qiita.com/FukuharaYohei/items/fcad2d74f2b753191548)で方法を書いていますが、Google Cloud ML Engineで訓練しています。ローカルPCや他サービスを使っていてもチェックポイントファイルさえあれば大丈夫です。

# 手順
##1. チェックポイントファイル準備
チェックポイントファイルをGoogle Cloud StorageからエクスポートされたモデルをPCにダウンロードします。model.ckpt-XXXXの3ファイルとcheckpointファイルが必要です(他は不要です)。

```bash
gsutil cp -r gs://cl-ml01/train_output/model ~/Documents/od/export/checkpoint
```

##2. saved_model出力プログラム作成
["How to deploy an Object Detection Model with TensorFlow serving"という記事](https://medium.freecodecamp.org/how-to-deploy-an-object-detection-model-with-tensorflow-serving-d6436e65d1d9)を参考に[exporter.py](https://github.com/tensorflow/models/blob/master/research/object_detection/exporter.py)をコピーして変更しています。
ソース全体は[GitHub](https://github.com/YoheiFukuhara/create_saved_model/blob/master/exporter_clml.py)に置いておきます。
改変ポイントを解説していきます。

### 2.1. 関数write_saved_modelのパラメータ
関数write_saved_modelのパラメータのfrozen_graph_defをtrained_checkpoint_prefixに変えます。

```python
def write_saved_model(saved_model_path,
                      # Start of change - replace trained_checkpoint_prefix from frozen_graph_def 2019/1/13
                      #frozen_graph_def,
                      trained_checkpoint_prefix,
                      # End   of change
                      inputs,
                      outputs):
```

### 2.2. 関数write_saved_modelのセッションの開始方法
関数write_saved_modelのセッションの開始方法をfrozen_graph_defからtrained_checkpoint_prefixを使ったresotreに変更。

```python
# Start of change - 2019/1/13
#  with tf.Graph().as_default():
#    with session.Session() as sess:
#
#      tf.import_graph_def(frozen_graph_def, name='')
  saver = tf.train.Saver()
  with session.Session() as sess:

    # restore from checkpoint
      saver.restore(sess, trained_checkpoint_prefix)
# End  of change
```

### 2.3. 関数write_saved_modelでのsignature追加
関数write_saved_modelでのsignatureを追加。

```python
      builder.add_meta_graph_and_variables(
          sess, [tf.saved_model.tag_constants.SERVING],
          signature_def_map={
              # Start of Insertion - 2019/1/20
              'detection_signature':
                  detection_signature,
              # End   of insertion
              signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
                  detection_signature,
          },
```

### 2.4. 関数\_export_inference_graphでの関数write_saved_model呼出パラメータ変更
1.と対応していますが、関数write_saved_modelの呼出パラメータをfrozen_graph_defからtrained_checkpoint_prefixに変えます。

```python
# Start of change - pass trained_checkpoint_prefix 2019/1/13
#  write_saved_model(saved_model_path, frozen_graph_def,
#                    placeholder_tensor, outputs)
  write_saved_model(saved_model_path, trained_checkpoint_prefix,
                  placeholder_tensor, outputs)
# End of change
```

### 2.5. 呼出メイン部分追加
単体で呼び出せるようにメイン部分を追加。ファイルパスはパラメータにできればいいのですが、面倒なのでハードコードしています。使いまわしたい場合は適宜変更してください。

```python
# Start of insertion - add for export 2019/1/18
from object_detection.utils.config_util import create_pipeline_proto_from_configs
from object_detection.utils.config_util import get_configs_from_pipeline_file

def main():
    # Configuration for model to be exported
    config_pathname = '/home/i348221/Documents/od/input/faster_rcnn_resnet101_coco.config'
    latest_filename = 'model.ckpt-1000'

    # Input checkpoint for the model to be exported
    # Path to the directory which consists of the saved model on disk (see above)
    trained_model_dir = '/home/i348221/Documents/od/export/checkpoint'

    # Create proto from model confguration
    configs = get_configs_from_pipeline_file(config_pathname)

    pipeline_proto = create_pipeline_proto_from_configs(configs=configs)

    # Read .ckpt and .meta files from model directory
    # checkpoint = tf.train.get_checkpoint_state(trained_model_dir, latest_filename)
    checkpoint = tf.train.get_checkpoint_state(trained_model_dir)
    input_checkpoint = checkpoint.model_checkpoint_path

    # Model Version
    model_version_id = 1

    # Output Directory
    output_directory = '/home/i348221/Documents/od/model/export/' + str(model_version_id)

    export_inference_graph(input_type='image_tensor', pipeline_config=pipeline_proto,
                                    trained_checkpoint_prefix=input_checkpoint, output_directory=output_directory)

# main関数呼び出し
if __name__ == "__main__":
    main()
# End   of insertion
```
##3. saved_model出力
作成したプログラムをmodels/research/object_detectionフォルダに置いて実行します。

```bash
# From tensorflow/models/research/
$ export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim
$ python object_detection/exporter_clml.py
```

これでsaved_modelとvariablesが出来ました！saved_model_cliを使って見てみます。
※saved_model_cliについては、[CLI to inspect and execute SavedModel](https://www.tensorflow.org/guide/saved_model#cli_to_inspect_and_execute_savedmodel)参照

```bash
# From ~/Documents/od/model/export/1/saved_model
$ saved_model_cli show --dir ./ --all
etaGraphDef with tag-set: 'serve' contains the following SignatureDefs:

signature_def['detection_signature']:
  The given SavedModel SignatureDef contains the following input(s):
    inputs['inputs'] tensor_info:
        dtype: DT_UINT8
        shape: (-1, -1, -1, 3)
        name: image_tensor:0
  The given SavedModel SignatureDef contains the following output(s):
    outputs['detection_boxes'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 300, 4)
        name: detection_boxes:0
    outputs['detection_classes'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 300)
        name: detection_classes:0
    outputs['detection_scores'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 300)
        name: detection_scores:0
    outputs['num_detections'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1)
        name: num_detections:0
  Method name is: tensorflow/serving/predict

signature_def['serving_default']:
  The given SavedModel SignatureDef contains the following input(s):
    inputs['inputs'] tensor_info:
        dtype: DT_UINT8
        shape: (-1, -1, -1, 3)
        name: image_tensor:0
  The given SavedModel SignatureDef contains the following output(s):
    outputs['detection_boxes'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 300, 4)
        name: detection_boxes:0
    outputs['detection_classes'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 300)
        name: detection_classes:0
    outputs['detection_scores'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 300)
        name: detection_scores:0
    outputs['num_detections'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1)
        name: num_detections:0
  Method name is: tensorflow/serving/predict
```




# NG集
よく陥るNG集です。

## だめだった方法1 - エクスポートされていたものをそのまま使う
事前に有効化venvの仮想環境を有効化し、環境変数の設定もします。

```bash
source cl-od/bin/activate

# From tensorflow/models/research/
export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim
```

saved_model_cliを使ってダウンロードしたsaved_modelを見てみます。

```bash
$ saved_model_cli show --dir ./ --all
MetaGraphDef with tag-set: 'serve' contains the following SignatureDefs:

signature_def['serving_default']:
  The given SavedModel SignatureDef contains the following input(s):
    inputs['serialized_example'] tensor_info:
        dtype: DT_STRING
        shape: ()
        name: tf_example:0
  The given SavedModel SignatureDef contains the following output(s):
    outputs['detection_boxes'] tensor_info:
        dtype: DT_FLOAT
        shape: (1, 300, 4)
        name: detection_boxes:0
    outputs['detection_classes'] tensor_info:
        dtype: DT_FLOAT
        shape: (1, 300)
        name: detection_classes:0
    outputs['detection_scores'] tensor_info:
        dtype: DT_FLOAT
        shape: (1, 300)
        name: detection_scores:0
    outputs['num_detections'] tensor_info:
        dtype: DT_FLOAT
        shape: (1)
        name: num_detections:0
  Method name is: tensorflow/serving/predict

signature_def['tensorflow/serving/predict']:
  The given SavedModel SignatureDef contains the following input(s):
    inputs['serialized_example'] tensor_info:
        dtype: DT_STRING
        shape: ()
        name: tf_example:0
  The given SavedModel SignatureDef contains the following output(s):
    outputs['detection_boxes'] tensor_info:
        dtype: DT_FLOAT
        shape: (1, 300, 4)
        name: detection_boxes:0
    outputs['detection_classes'] tensor_info:
        dtype: DT_FLOAT
        shape: (1, 300)
        name: detection_classes:0
    outputs['detection_scores'] tensor_info:
        dtype: DT_FLOAT
        shape: (1, 300)
        name: detection_scores:0
    outputs['num_detections'] tensor_info:
        dtype: DT_FLOAT
        shape: (1)
        name: num_detections:0
  Method name is: tensorflow/serving/predict
```

残念ながらこのsaved_modelはGoogle Cloud MLで使えませんでした(筆者の技術力不足かもしれません)。
ブログ記事[「Performing prediction with TensorFlow object detection models on Google Cloud Machine Learning Engine」](https://cloud.google.com/blog/products/gcp/performing-prediction-with-tensorflow-object-detection-models-on-google-cloud-machine-learning-engine)に下記のように書かれていて、shapeがマッチしないのでこのままだとうまくいきません:disappointed_relieved:
> "Note that the first dimensions of the input and output tensors should be -1 as this is required by the Cloud Machine Learning engine to support batched-inputs."

[GitHub issue](https://github.com/tensorflow/models/issues/1811#issuecomment-313242154)にも同様の記述があります。どちらも少し古いのですが、状況は変わっていないと思います。
> The current object detection example only supports reading one image at a time when doing inference. However, Cloud ML engine prediction service requires the inference graph be able to accept arbitrary number of input instances.The shape of the input tensor(s) must have None as its first dimension. 

## だめだった方法2 - チェックポイントファイルからエクスポート
チェックポイントのファイルをダウンロードします。

```bash
gsutil cp gs://cl-ml01/train_output/model/model.ckpt-1000* ~/Documents/od/model
```

[Exporting a trained model for inference](https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/exporting_models.md)に従ってsaved_modelをエクスポート。

```bash
INPUT_TYPE=image_tensor
PIPELINE_CONFIG_PATH=~/Documents/od/input/faster_rcnn_resnet101_coco.config
TRAINED_CKPT_PREFIX=~/Documents/od/model/model.ckpt-1000
EXPORT_DIR=~/Documents/od/model

python object_detection/export_inference_graph.py \
    --input_type=${INPUT_TYPE} \
    --pipeline_config_path=${PIPELINE_CONFIG_PATH} \
    --trained_checkpoint_prefix=${TRAINED_CKPT_PREFIX} \
    --output_directory=${EXPORT_DIR}
```


```bash
$ saved_model_cli show --dir ./saved_model --all
MetaGraphDef with tag-set: 'serve' contains the following SignatureDefs:

signature_def['serving_default']:
  The given SavedModel SignatureDef contains the following input(s):
    inputs['inputs'] tensor_info:
        dtype: DT_UINT8
        shape: (-1, -1, -1, 3)
        name: image_tensor:0
  The given SavedModel SignatureDef contains the following output(s):
    outputs['detection_boxes'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 300, 4)
        name: detection_boxes:0
    outputs['detection_classes'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 300)
        name: detection_classes:0
    outputs['detection_scores'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 300)
        name: detection_scores:0
    outputs['num_detections'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1)
        name: num_detections:0
  Method name is: tensorflow/serving/predict
```

今度はshapeは良さそうなのですが、variablesフォルダが空です。
[GitHub Issue](https://github.com/tensorflow/models/issues/2045#issuecomment-318421635)に以下のように書かれていて、変数はすべて固定値になってしまっているようです。うーん・・・

> This is working as intended. The exporter script converts all variables into constants when exporting the graph.