Deep Learningアプリケーション開発 (2) TensorFlow with Python

# この記事について
機械学習、Deep Learningの専門家ではない人が、Deep Learningを応用したアプリケーションを作れるようになるのが目的です。MNIST数字識別する簡単なアプリケーションを、色々な方法で作ってみます。特に、組み込み向けアプリケーション(Edge AI)を意識しています。
モデルそのものには言及しません。数学的な話も出てきません。Deep Learningモデルをどうやって使うか(エッジ推論)、ということに重点を置いています。

1. [Kerasで簡単にMNIST数字識別モデルを作り、Pythonで確認](https://qiita.com/take-iwiw/items/796ec8560563625ace34)
2. [TensorFlowモデルに変換してPythonで使用してみる (Windows, Linux)](https://qiita.com/take-iwiw/items/80fc73ff23d8f51650f5)  <--- 今回の内容
3. [TensorFlowモデルに変換してCで使用してみる (Windows, Linux)](https://qiita.com/take-iwiw/items/d7cb2bd10a8bbb81cd8b)
4. [TensorFlow Liteモデルに変換してPythonで使用してみる (Windows, Linux)](https://qiita.com/take-iwiw/items/d63aa67e5c700fcea70a)
5. [TensorFlow Liteモデルに変換してCで使用してみる (Linux)](https://qiita.com/take-iwiw/items/d998df1981d46285df62)
6. [TensorFlow Liteモデルに変換してC++で使用してみる (Raspberry Pi)](https://qiita.com/take-iwiw/items/4d198f6203348ef7fd31)
7. [TensorFlow LiteモデルをEdge TPU上で動かしてみる (Raspberry Pi)](https://qiita.com/take-iwiw/items/6aeab468c326ecc21563) 

## Google Colaboratory版
Google Colaboratory + Tensorflow2.x版を本記事の後ろに追記しました。
[Google Colaboratory版](https://qiita.com/take-iwiw/items/80fc73ff23d8f51650f5#google-colaboratory%E3%81%A7%E8%A9%A6%E3%81%99)



## 今回の内容
- 前回作成したKeras用モデル(.h5)をTensorFlow用モデル(.pb)に変換する
- 変換したTensorFlow用モデルを使って、入力画像から数字識別するアプリケーションを作る

ソースコードとサンプル入力画像: https://github.com/take-iwiw/CNN_NumberDetector/tree/master/02_Tensorflow_Python

### なぜ必要? 想定するシナリオ
- TensorFlow用モデルしかないとき
	- 社内の機械学習エンジニア様、データサイエンティスト様がTensorFlowでモデルを作成したら、それに対応したアプリケーションを作る必要があります (後述するように変換はできますが)
- 今一番勢いがある?
	- ONNXやMMdnnを使用したら、異なるDeepLearning用フレームワーク間でのモデル変換が出来ます
	- TensorFlowに対応したアプリケーションの作り方を覚えておけば、大抵のことには対応できると思います
- 動作環境にPython, Kerasがないとき
	- C言語用APIが用意されているので、ライブラリと一緒に配布すれば実行先での環境構築は不要です
		- お客様にPythonやAnacondaをインストールしてもらうわけにはいきません!
		- 今回はPythonで試しますが、C言語での実装の前段階ととらえてください

## 環境
- OS: Windows 10 (64-bt)
- OS(on VirtualBox): Ubuntu 16.04
- CPU = Intel Core i7-6700@3.4GHz (物理コア=4、論理プロセッサ数=8)
- GPU = NVIDIA GeForce GTX 1070 (← GPUは無くても大丈夫です)
- 開発環境: Anaconda3 64-bit (Python3.6.8)
- TensorFlow 1.12.0
- パッケージ詳細はこちら [Windows用](https://gist.github.com/take-iwiw/35177989686dfe6a8e6837aa3816c100#file-windows)、[Linux用](https://gist.github.com/take-iwiw/35177989686dfe6a8e6837aa3816c100#file-linux-txt)


今回の内容は、WindowsとLinux(Ubuntu)のどちらでも動きますが、本記事の説明はWindowsメインで行います。

# Keras用モデル(.h5)をTensorFlow用モデル(*.pb)に変換する
https://medium.com/@pipidog/how-to-convert-your-keras-models-to-tensorflow-e471400b886a のサイトを参考にさせていただき、変換用スクリプトを作りました。現在、FastGFileは非推奨とのことなので、tensorflow.gfile.GFileに置き換えています。
`convert_keras_to_tensorflow` 関数を呼ぶことで、Keras用モデル(.h5)をTensorFlow用モデル(*.pb)に変換できます。また、`get_model_info` 関数を呼ぶことで、変換したTensorFlow用モデルの情報をJSONファイルに保存します。
このスクリプトをそのまま実行すると、前回作成したKeras用モデル(conv_mnist.h5)を、TensorFlow用モデル(conv_mnist.pb)に変換してくれます。

```py:keras_to_tensorflow.py
# -*- coding: utf-8 -*-
# Reference URL: https://medium.com/@pipidog/how-to-convert-your-keras-models-to-tensorflow-e471400b886a

import tensorflow as tf
from tensorflow.python.keras.models import load_model
from tensorflow.python.keras import backend as K
from tensorflow.python.framework.graph_util import convert_variables_to_constants
import numpy as np
import json

def freeze_session(sess, keep_var_names=None, output_names=None, clear_devices=True):
	graph = sess.graph
	with graph.as_default():
		freeze_var_names = list(set(v.op.name for v in tf.global_variables()).difference(keep_var_names or []))
		output_names = output_names or []
		output_names += [v.op.name for v in tf.global_variables()]
		input_graph_def = graph.as_graph_def()
		if clear_devices:
			for node in input_graph_def.node:
				node.device = ''
		frozen_graph = convert_variables_to_constants(sess, input_graph_def, output_names, freeze_var_names)
		return frozen_graph

def convert_keras_to_tensorflow(keras_model_filename, tf_model_filename):
	model = load_model(keras_model_filename)
	model.summary()
	frozen_graph = freeze_session(K.get_session(), output_names=[out.op.name for out in model.outputs])
	tf.train.write_graph(frozen_graph, './', tf_model_filename, as_text=False)


def get_model_info(tf_model_filename):
	ops = {}
	with tf.Session() as sess:
		with tf.gfile.GFile(tf_model_filename, 'rb') as f:
			graph_def = tf.GraphDef()
			graph_def.ParseFromString(f.read())
			sess.graph.as_default()
			_ = tf.import_graph_def(graph_def)
			for op in tf.get_default_graph().get_operations():
				ops[op.name] = [str(output) for output in op.outputs]
		writer = tf.summary.FileWriter('./logs')
		writer.add_graph(sess.graph)
		writer.flush()
		writer.close()

	with open(tf_model_filename+'_param.json', 'w') as f:
		f.write(json.dumps(ops))


if __name__ == '__main__':
	# convert
	keras_model_filename = 'conv_mnist.h5'
	tf_model_filename = 'conv_mnist.pb'
	convert_keras_to_tensorflow(keras_model_filename, tf_model_filename)
	get_model_info(tf_model_filename)
```

## モデル情報を確認する
上記スクリプトを実行すると、TensorFlow用モデル(conv_mnist.pb)と共に、モデル情報を記した(conv_mnist.pb_param.json)も出力されます。
このモデル情報は非常に重要です。変換したモデルを使用する際に入出力の名前が必要になります。

前回モデル作成時に、入力には`Input`を使い、出力は全結合をする`Dense`を使い`softmax`を取るようにしました。また、グラフ名はデフォルトでは`import`になるようです。このような情報をヒントに入出力のTensorを探します。
今回は以下であることが分かります:

- 入力: `import/input_1:0`
- 出力: `import/dense_1/Softmax:0`

```json:conv_mnist.pb_param.json
{
	"import/input_1": [
		"Tensor(\"import/input_1:0\", shape=(?, 28, 28, 1), dtype=float32)"
	],
	"import/conv2d_1/kernel": [
		"Tensor(\"import/conv2d_1/kernel:0\", shape=(3, 3, 1, 8), dtype=float32)"
	],
~略~
	"import/dense_1/Softmax": [
		"Tensor(\"import/dense_1/Softmax:0\", shape=(?, 10), dtype=float32)"
	],
~略~
	"import_2/training_1/Adam/Variable_11": [
		"Tensor(\"import_2/training_1/Adam/Variable_11:0\", shape=(10,), dtype=float32)"
	]
}
```

# 変換したTensorFlow用モデルを使って、入力画像から数字識別するアプリケーションを作る
Spyder上で`number_detector_tensorflow.py`というファイルを開き、以下のようなコードを実装します。
動作仕様は前回Keras用に作成した`number_detector.py` と同じです。TensorFlow呼び出しもこの中でやっているので、少し複雑になっています。
get_tensor_by_nameに先ほど確認した入出力名を設定して、入出力Tensorを取得しています。

```py:number_detector_tensorflow.py
# -*- coding: utf-8 -*-
import cv2
import tensorflow as tf
import numpy as np

if __name__ == '__main__':
	img = cv2.imread('resource/4.jpg')
	cv2.imshow('image', img)

	img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
	img = cv2.resize(img, (28, 28))
	img = 255 - img
	img = img.reshape(1, img.shape[0], img.shape[1], 1)
	img = img / 255.

	with tf.Session() as sess:
		with tf.gfile.GFile('conv_mnist.pb', 'rb') as f:
			graph_def = tf.GraphDef()
			graph_def.ParseFromString(f.read())
			sess.graph.as_default()
			_ = tf.import_graph_def(graph_def)
			tensor_input = sess.graph.get_tensor_by_name('import/input_1:0')
			tensor_output = sess.graph.get_tensor_by_name('import/dense_1/Softmax:0')
			probs = sess.run(tensor_output, {tensor_input: img})

	result = np.argmax(probs[0])
	score = probs[0][result]

	print("predicted number is {} [{:.2f}]".format(result, score))

	cv2.waitKey(0)
	cv2.destroyAllWindows()
```


# (追記)keras用モデル(h5)をTensorflow用モデル(pb)に変換する (別の方法)
saved_model を使うことで、より簡単に変換できました。
https://www.tensorflow.org/api_docs/python/tf/saved_model
(参考: https://qiita.com/t_shimmura/items/1ebd2414310f827ed608 (参考、というかほぼ使わせていただきました。ありがとうございます。))


```py:keras_to_tensorflow2.py
import tensorflow as tf
from tensorflow.python.keras.models import load_model

input_keras_model = './conv_mnist.h5'
export_dir = './conv_mnist_pb'

if __name__ == '__main__':
	old_session = tf.keras.backend.get_session()
	sess = tf.Session()
	sess.run(tf.global_variables_initializer())
	tf.keras.backend.set_session(sess)
	model = load_model(input_keras_model)
	builder = tf.saved_model.builder.SavedModelBuilder(export_dir)
	signature = tf.saved_model.predict_signature_def(inputs={t.name:t for t in model.inputs},
												  outputs={t.name:t for t in model.outputs})
	builder.add_meta_graph_and_variables(sess,
									  tags=[tf.saved_model.tag_constants.SERVING],
									  signature_def_map={'predict': signature})
	builder.save(as_text=True)
	sess.close()
	tf.keras.backend.set_session(old_session)
	
	print('output_node_names:')
	for t in model.inputs:
		print(t.name)
	
	print('output_node_names:')
	for t in model.outputs:
		print(t.name)
```

上記スクリプトを実行すると、「conv_mnist_pb」というディレクトリが出来ます。この中にモデル構造を格納したファイル(.pbtxt)とweightパラメータが格納されたファイル(variables)が作られます。
また、最後に入出力名を表示しているので、下記コマンドのパラメータやTensorflowから使う際のヒントにしてください。

その後、ターミナルで以下のように打つことで、パラメータもフリーズされたpbファイル(frozen graph)が作られます。

```sh:freezeコマンド(改行区切りはWindows用なので、linuxでは\に変更してください)
freeze_graph ^
--input_saved_model_dir=./conv_mnist_pb ^
--output_graph=conv_mnist.pb ^
--output_node_names=dense_1/Softmax ^
--clear_devices
```

ちなみに、僕はIDEとしてSpyderを使用しているのですが、上記スクリプトを実行して、load_modelをやるたびに、ノード(op)名が、`input_1`, `input_1_1`, `input_1_2`, `input_1_3`と最後に数字のプリフィックスが付けられてどんどん増えていきます。conv_mnist_pbディレクトリに作られるpbtxtやweightファイルサイズもその分だけどんどん大きくなっていきます。最終的に`freeze_graph` するときには必要なoutputだけを指定するので問題ないのですが、謎です。何かリセット的なのが必要なのでしょうか。。。
ターミナルから実行したり、Spyder上でカーネルを再起動したら問題ありません。




# Google Colaboratoryで試す
本記事の内容を、Google Colaboratoryで試します。
使用するTensorflowのバージョンは、2.1.0-rc1でした。

https://github.com/iwatake2222/colaboratory_study/blob/master/DL_tutorial/DL_tutorial_02.ipynb



## Keras用モデル(.h5)をTensorFlow用モデル(*.pb)に変換する
下記コードでは、Kerasモデルを僕のgoogleドライブからダウンロードしていますが、前回の続きからやる場合は不要です。
saved_modelの保存先ディレクトリに、`saved_model.pb` ファイルと、`assets`、`variable`ディレクトリが作られます。
最後に、生成されたsaved modelをtarにまとめています。

```python:Keras用モデル(.h5)をTensorFlow用モデル(*.pb)に変換する
%tensorflow_version 2.x
from __future__ import absolute_import, division, print_function, unicode_literals
import tensorflow as tf

print(tf.__version__)

# Download Keras H5 model
!wget -O conv_mnist.h5 "https://drive.google.com/uc?export=download&id=1OLR1n5Pq0CGPz7Bw5pad-fvYsgCnKvHh" 

# Load Keras H5 model, then save as Tensorflow saved model(pb)
loaded_model = tf.keras.models.load_model("conv_mnist.h5")
loaded_model.save("conv_mnist_saved_model", save_format="tf")
# tf.saved_model.save(model, 'saved_model')

!ls -la conv_mnist_saved_model
!tar zcvf conv_mnist_saved_model.tar.gz  conv_mnist_saved_model
```

### モデル情報を見てみる
本記事では、Tensorflowモデルの情報を見るために、色々とコードを書いてみましたが、下記コマンドで見ることが出来ます。

```python:モデル情報を見てみる
!saved_model_cli show --dir conv_mnist_saved_model  --tag_set serve --signature_def serving_default
```

```txt:実行結果
The given SavedModel SignatureDef contains the following input(s):
  inputs['input_image'] tensor_info:
      dtype: DT_FLOAT
      shape: (-1, 28, 28, 1)
      name: serving_default_input_image:0
The given SavedModel SignatureDef contains the following output(s):
  outputs['output_scores'] tensor_info:
      dtype: DT_FLOAT
      shape: (-1, 10)
      name: StatefulPartitionedCall:0
Method name is: tensorflow/serving/predict
```

## Tensorflow saved modelによる推論テスト
```python:Tensorflow_savedmodelによる推論テスト
import cv2
from google.colab.patches import cv2_imshow
import numpy as np

# Read input image
!wget -O 4.jpg  "https://drive.google.com/uc?export=download&id=1-3yb3qCrN8M6Bdj7ZZ9UMjONh34R2W_A" 
img = cv2.imread("4.jpg")
cv2_imshow(img)

# Pre process
## グレースケール化、リサイズ、白黒反転、価範囲を0～255 -> 0.0～1.0
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.resize(img, (28, 28))
img = 255 - img
img = img / 255.
img = img.astype(np.float32)
input_tensor = img.reshape(1, img.shape[0], img.shape[1], 1)
input_tensor = tf.convert_to_tensor(input_tensor)

# Load model
loaded_model = tf.saved_model.load("conv_mnist_saved_model")
infer = loaded_model.signatures["serving_default"]
output_name = list(infer.structured_outputs.keys())[0]

# Inference
output_tensor = infer(input_tensor)[output_name]

scores = output_tensor.numpy()
result = np.argmax(scores[0])
print("predicted number is {} [{:.2f}]".format(result, scores[0][result]))
```