Deep Learningアプリケーション開発 (1) Keras 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/796ec8560563625ace34#google-colaboratory%E3%81%A7%E8%A9%A6%E3%81%99)



## 今回の内容
・Deep Learningのための開発環境の構築
・Kerasを使用した、簡単なモデルの作成
・入力画像から数字識別するアプリケーションを作る

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


## 作成するアプリケーションの仕様
- 数字(0～9)が書かれた画像(jpeg)を入力する
- 数字を識別して出力する

![4.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/bf0fa7b2-60d2-c501-8728-23b36709674a.jpeg)
この場合は、「4」を出力する

本連載では簡単のため、一貫してこのアプリケーションを作ることを目的とします。

## 環境
- 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メインで行います。

# 開発環境の用意
## Anacondaのインストール
- https://www.anaconda.com/ から、Anacondaをダウンロードしてインストールします
- インストール完了後、スタートメニューからAnaconda Promptを開きます

## 仮想開発環境の作成
本アプリケーション開発用の仮想開発環境をAnaconda上で用意します。
以下のコマンドを、Anaconda Promptで実行します。

これによって、`tf_test01`という仮想環境を作成します。tensorflowなどのライブラリをインストールはこの`tf_test01`に対してのみ行われ、他の環境や標準ライブラリが汚されません。

```txt
conda create -n tf_test01
activate tf_test01
conda upgrade --all
conda install spyder
conda install opencv
conda install Pillow
conda install tensorflow
conda install tensorflow-gpu      << NVidiaのCUDA対応GPUを持っている人のみ (VirtualBox上だとダメ)
spyder			<< IDEを開きます
```

次回以降は、以下コマンドによって起動します。

```txt
activate tf_test01
cd ソースコードのある場所
spyder			<< IDEを開きます
```

### メモ
- pythonのパッケージインストールには、`pip`が有名ですが、Anaconda環境上ではできるだけ`conda`を使用するようにしてください。
- tensorflowのGPU対応のためにCUDAやcuDNNのインストール、さらにはバージョン依存などが問題となっていたようです。本記事作成の現在(2019年3月6日)においては、だいぶ改善されたようで、`conda install tensorflow-gpu`コマンドだけでCUDA関係を含む必要なパッケージ、ライブラリがインストールされるようです。
	- ちなみに、僕の環境では上記コマンドの前に、別目的でCUDA10をインストール済みでしたが、それとは別に作成した仮想環境上にCUDA9.0関係のものがインストールされているようです。
- ここではIDEとしてspyderを使っていますが、お好みに合わせてご変更ください。
- OpenCVで、後で実行時に ` Rebuild the library with Windows, GTK+ 2.x or Carbon support. ` というエラーが出る場合は、 `conda install -c menpo opencv3` を試してみてください。

# Kerasで数字識別モデルを作る
## モデル作成の方針
### モデル作成の方針 (フレームワーク)
世の中には、Deep Learningの学習、推論を行うためのフレームワークがいくつかあります。今回、モデル作成、学習のためにはKerasというフレームワークを使用します。
KerasはTensorFlowの上位API(Wrapper)で、簡単にモデル作成が行えることが利点です。また、TensorFlowをインストールしたら、自動的にKerasもインストールされます。
複雑なモデルを作ろうとしたら直接TensorFlowを使う必要があるらしいのですが、本記事ではモデル作成はお手軽にやります。

### モデル作成の方針 (学習用データ)
数字を識別するモデルを作成するためには、大量の学習データ(画像データと正解データのペア)が必要です。このために、MNISTと呼ばれているデータセットを使用します。これはtensorflowをインストールしたら一緒に入っています。

MNISTの画像フォーマットは、以下の通りです。背景が黒(輝度=0)で、数字が白(輝度=255)で書かれています。
- 学習用画像: 6000個の28x28のグレースケール画像(0～255) (配列サイズ: 6000 x 28 x 28)
- 学習用正解ラベル: 6000個の正解数字 (配列サイズ: 6000)
- テスト用画像: 1000個の28x28のグレースケール画像(0～255) (配列サイズ: 1000 x 28 x 28)
- テスト用正解ラベル: 1000個の正解数字 (配列サイズ: 1000)

### モデル作成の方針 (モデル構造)
入力画像は、28px(横) x 28px(縦) x 1チャネル(グレースケール)のデータです。画像データを精度よく処理するために、CNN (Convolutional Neural Network: 畳み込みニューラルネットワーク)を使用します。これによってある画素に対して、周辺画素の情報も使用した特徴量を抽出することが出来ます。

今回は以下のような構造にします。特に意味はないのですが、これくらいやっておけば良い感じに特徴量見つけて識別してくれるだろう、という感じです。また、後々ラズパイなどに組み込むことを考えて軽量なモデルにしています。

- 入力: 28x28x1
- 畳み込み1層目: カーネルサイズ = (3x3)、出力 = 8
- MaxPooling: 間隔 = (2, 2)
- 畳み込み2層目: カーネルサイズ = (3x3)、出力 = 4
- 全結合
- 出力: 10

## モデルを作る
先ほど、PythonのIDEであるspyderを開いていると思います。
適当に'ConvMnist.py'という名前でファイルを保存して、以下のようなコードを記載します。

Kerasでモデルを構築するには、Sequential APIを使う方法とFunctional APIを使う方法があります。ここではFunctional APIを使用しています。

ConvMnistクラスのtrain関数の中で、MNISTの学習用データの取得、モデル構築、学習、テストを行っています。また、save_trained_model関数で学習したモデルをファイルに保存できるようにしています。また、別のプロジェクトから使えるようにコンストラクタでモデルのロードをし、predict関数で識別だけを行えるようにしています。


```py:ConvMnist.py
# -*- coding: utf-8 -*-
from tensorflow.python.keras.datasets import mnist
from tensorflow.python.keras.utils import to_categorical
from tensorflow.python.keras.models import Model
from tensorflow.python.keras.layers import Input, Conv2D, MaxPooling2D, Dropout, Flatten, Dense
from tensorflow.python.keras.callbacks import TensorBoard
from tensorflow.python.keras.models import load_model
import numpy as np

class ConvMnist:
	def __init__(self, filename=None):
		'''
		学習済みモデルファイルをロードする (optional)
		'''
		self.model = None
		if filename is not None:
			print('load model: ', filename)
			self.model = load_model(filename)
			self.model.summary()

	def train(self):
		'''
		学習する
		'''
		# MNISTの学習用データ、テストデータをロードする
		(x_train, y_train), (x_test, y_test) = mnist.load_data()
		
		# 学習データの前処理
		# X: 6000x28x28x1のTensorに変換し、値を0～1.0に正規化
		# Y: one-hot化(6000x1 -> 6000x10)
		x_train = x_train.reshape(x_train.shape[0], x_train.shape[1], x_train.shape[2], 1)
		x_test = x_test.reshape(x_test.shape[0], x_test.shape[1], x_test.shape[2], 1)
		x_train = x_train / 255.
		x_test = x_test / 255.
		y_train = to_categorical(y_train, 10)
		y_test = to_categorical(y_test, 10)

		# 学習状態は悪用のTensorBoard設定
		tsb = TensorBoard(log_dir='./logs')
		
		# Convolutionモデルの作成
		input = Input(shape=(28,28,1))
		conv1 = Conv2D(
					filters=8,
					kernel_size=(3,3),
					strides=(1,1),
					padding='same',
					activation='relu'
				)(input)
		pool1 = MaxPooling2D(pool_size=(2,2))(conv1)
		conv2 = Conv2D(
					filters=4,
					kernel_size=(3,3),
					strides=(1,1),
					padding='same',
					activation='relu'
				)(pool1)
		dropout1 = Dropout(0.2)(conv2)
		flatten1 = Flatten()(dropout1)
		output = Dense(units=10, activation='softmax')(flatten1)
		self.model = Model(inputs=[input], outputs=[output])
		
		self.model.summary()
		
		self.model.compile(
			optimizer='adam',
			loss='categorical_crossentropy',
			metrics=['accuracy']
		)
		
		# Convolutionモデルの学習
		self.model.fit(
			x_train,
			y_train,
			batch_size=128,
			epochs=10,
			validation_split=0.2,
#			callbacks=[tsb],
		)
		
		# 学習したモデルを使用して、テスト用データで評価する
		score = self.model.evaluate(x_test, y_test, verbose=0)
		print("test data score: ", score)

	def save_trained_model(self, filename):
		'''
		学習済みモデルをファイル(h5)に保存する
		'''
		self.model.save(filename)


	def predict(self, input_image):
		'''
		1つのグレースケール入力画像(28x28のndarray)に対して、数字(0～9)を判定する
		ret: result, score
		'''
		if input_image.shape != (28,28):
			return -1, -1
		input_image = input_image.reshape(1, input_image.shape[0], input_image.shape[1], 1)
		input_image = input_image / 255.

		probs = self.model.predict(input_image)
		result = np.argmax(probs[0])
		return result, probs[0][result]

if __name__ == '__main__':
	conv_mnist = ConvMnist()
	conv_mnist.train()
	conv_mnist.save_trained_model(filename='conv_mnist.h5')

	'''sample code to predict'''
#	conv_mnist = ConvMnist(filename='conv_mnist.h5')
#	result = conv_mnist.predict(conv_mnist.x_test[0])
#	print("answer = {}, predict = {}" .format(conv_mnist.y_test[0], result))
```


## 実行結果
このスクリプトを直接実行すると、学習して、学習したモデルを`conv_mnist.h5`という名前で保存します。

実行中は以下のように学習の過程が見られます。なおこのモデルだとGPUだと1エポック10秒程度。CPUだと2分ほどかかりました。

```txt:実行結果
Train on 48000 samples, validate on 12000 samples
Epoch 1/10
48000/48000 [==============================]48000/48000 [==============================] - 9s 186us/step - loss: 0.6274 - acc: 0.8219 - val_loss: 0.2336 - val_acc: 0.9351

~略~

Epoch 10/10
48000/48000 [==============================]48000/48000 [==============================] - 9s 190us/step - loss: 0.0806 - acc: 0.9747 - val_loss: 0.0677 - val_acc: 0.9799

test data score:  [0.05887840811731294, 0.9812]
```

実行が完了すると、スクリプトを実行した場所に`conv_mnist.h5`ファイルが生成されているはずです。

# 数字判定をするアプリケーションを作る
先の学習途中やテスト時に、モデルを使用した数字判定は行われています。しかし、そのときの入力はMNISTのデータセットをリストに変換したものでした。これだとアプリケーションとしていまいちなので、入力画像(JPEG)から判定できるようにします。

Spyder上で`number_detector.py`というファイルを開き、以下のようなコードを実装します。
OpenCVを使用してJPEG画像を読み込み、モデルが使用できるカラーフォーマット、サイズに変換しています。また、通常数字は白い紙の上に黒いペンで書くと思いますので、白黒反転しています。

```py:number_detector.py
# -*- coding: utf-8 -*-
import cv2
from ConvMnist import ConvMnist

if __name__ == '__main__':
	conv_mnist = ConvMnist(filename='conv_mnist.h5')
	
	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
	
	result, score = conv_mnist.predict(img)
	print("predicted number is {} [{:.2f}]".format(result, score))

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

```txt:実行結果
load model:  conv_mnist.h5
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 28, 28, 1)         0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 28, 28, 8)         80        
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 14, 14, 8)         0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 14, 14, 4)         292       
_________________________________________________________________
dropout_1 (Dropout)          (None, 14, 14, 4)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 784)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 10)                7850      
=================================================================
Total params: 8,222
Trainable params: 8,222
Non-trainable params: 0
_________________________________________________________________
predicted number is 4 [0.93]
```

このスクリプトを実行すると、上記のようにモデル情報と、入力画像(ここでは「4」が書かれたJPEG)に対しての識別結果が出力されます。
93%の確率で「4」だと判定されています。


次回以降は、ここで作成したモデル`conv_mnist.h5`を、色々な環境で使用してアプリケーションを作っていこうと思います。

# おすすめの本
1. 現場で使える！TensorFlow開発入門 Kerasによる深層学習モデル構築手法
2. ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装

本記事の内容は1の本を参考に作成させていただきました。ただ、理論的な内容が少ないため、2の本で補完するのがいいかと思います。


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

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

## Kerasで数字識別モデルを作る

```python:Kerasモデル作成、学習、h5モデル保存
%tensorflow_version 2.x
from __future__ import absolute_import, division, print_function, unicode_literals
import tensorflow as tf
import numpy as np

print(tf.__version__)

# MNISTの学習用データ、テストデータをロードする
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

# 学習データの前処理
# X: 6000x28x28x1のTensorに変換し、値を0～1.0に正規化
# Y: one-hot化(6000x1 -> 6000x10)   -> no need when using SparseCategoricalCrossentropy()
x_train = x_train.reshape(x_train.shape[0], x_train.shape[1], x_train.shape[2], 1)
x_test = x_test.reshape(x_test.shape[0], x_test.shape[1], x_test.shape[2], 1)
x_train = x_train / 255.
x_test = x_test / 255.
x_train = x_train.astype(np.float32)
x_test = x_test.astype(np.float32)
# y_train = tf.keras.utils.to_categorical(y_train, 10)
# y_test = tf.keras.utils.to_categorical(y_test, 10)

# 学習状態把握用のTensorBoard設定
tsb = tf.keras.callbacks.TensorBoard(log_dir="./logs")

# Convolutionモデルの作成
input = tf.keras.layers.Input(shape=(28,28,1), name="input_image")
conv1 = tf.keras.layers.Conv2D(
  filters=8,
  kernel_size=(3,3),
  strides=(1,1),
  padding="same",
  activation="relu"
)(input)
pool1 = tf.keras.layers.MaxPooling2D(pool_size=(2,2))(conv1)
conv2 = tf.keras.layers.Conv2D(
  filters=4,
  kernel_size=(3,3),
  strides=(1,1),
  padding="same",
  activation="relu"
)(pool1)
dropout1 = tf.keras.layers.Dropout(0.2)(conv2)
flatten1 = tf.keras.layers.Flatten()(dropout1)
output = tf.keras.layers.Dense(units=10, activation="softmax", name="output_scores")(flatten1)
model = tf.keras.models.Model(inputs=[input], outputs=[output], name="ConvMnist")
model.summary()

model.compile(
  optimizer=tf.keras.optimizers.Adam(),
  loss=tf.keras.losses.SparseCategoricalCrossentropy(),
  metrics=[tf.keras.metrics.SparseCategoricalAccuracy()]
)

# Convolutionモデルの学習
model.fit(
  x_train,
  y_train,
  batch_size=32,
  epochs=20,
  validation_split=0.2,
  callbacks=[tsb],
)

# 学習したモデルを使用して、テスト用データで評価する
model.evaluate(x_test,  y_test, verbose=2)

# 学習済みモデルをファイル(h5)に保存する
model.save("conv_mnist.h5")
```

### 作成したモデルを保存する
作成したkerasモデル(h5)を、Googleドライブに保存、またはダウンロードします。

```python:作成したモデルの保存
# Mount google drive
from google.colab import drive 
drive.mount("/content/drive")

# Save to google drive
!cp conv_mnist.h5  "/content/drive/My Drive/test_data/number"

# Download to local
from google.colab import files
files.download( "./conv_mnist.h5")
```

## 数字判定をするアプリケーションを作る

```python:推論テストアプリケーション
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_org = cv2.imread("4.jpg")
cv2_imshow(img_org)

# Pre process
## グレースケール化、リサイズ、白黒判定、価範囲を0～255 -> 0.0～1.0
img = cv2.cvtColor(img_org, 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)

# Load model
loaded_model = tf.keras.models.load_model("conv_mnist.h5")

# Inference
scores = loaded_model.predict(input_tensor)

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