Tensorflow.kerasのFunctional APIを使ってSkip-Connection構造を持ったAIモデルを記述し、学習を行い、成果物をTensorflow Lite(tflite)で実行する


----

<font color="blue">本記事は、**Tensorflow.kerasのSequential APIを使ってMNISTを扱う簡単なAIモデルを定義してTensorflowによる学習手順と推論方法を理解する** の続編となる記事です。TensorflowではじめてAIモデルを組まれる方は、前編の記事にてTensorflowの使い方を詳しく説明していますので、そちらを是非ご参照ください。</font>

https://qiita.com/xtrizeShino/items/be8f405be8c3f72b10d9

----

# ゼロからわかるTensorflowとTensorflow Lite

Tensorflow Liteは、ONNXと並んで、デファクトスタンダートとも言えるエッジAIの実行環境です。 **Tensorflow LiteでAIモデルを実行するためには、まずTensorflowにてAIモデルを定義し、AIモデルの学習を実行、学習済みのAIモデルをtflite形式にてエクスポートします。** Tensorflow Liteは、ここで生成されたtflite形式のAIモデルを、組み込みデバイス上のTensorflow Liteランタイムにより実行することで、AIモデルの実行を可能としています。Tensorflow Liteランタイムは、Android/iOS/Linuxなど幅広いOSにおけるC++/Java/Swift/Objective-C/Pythonの開発環境向けに提供されており、ほぼ全てのソフトウェア開発環境で利用することが可能です。**さらにTensorflow Liteは、推論のみを行うランタイムのため、Tensorflowに比べて小さなフットプリントでAIモデルを実行することができます。**

## 本記事のゴール

本記事では、Tensorflowの使い方から、TensorflowでのAIモデルの定義方法、AIモデルの学習方法と、学習結果をtflite形式へと変換しTensorflow Liteランタイムにより実行する方法についてご紹介します。今回は簡単化のため、Tensorflow LiteランタイムはPythonのものを選択します。 **なお、AIモデルの定義方法としては、まずはじめに簡単にモデルを定義のできるSequential APIによる定義方法を紹介し、次にSkip-Connectionなども定義することのできるFunctional APIによるモデルの定義方法を紹介します。** 

https://www.tensorflow.org/guide/keras/functional?hl=ja

https://cvml-expertguide.net/terms/dl/inter-layer-connection/skip-connection/

### 前編/後編

本記事は前編と後編で構成されています。前編では、基本的なTensorflowの使い方とSequential APIを利用したAIモデルの定義方法について紹介しました。 <font color="blue">**その続きとなる本記事では、画像分類に欠かせない畳み込み層を使ったAIモデルの構築方法と、Functional APIの利用方法、そしてTensorflow Liteモデルへの置換することによるTensorflow LiteランタイムでのAIモデルの実行といった、核心部分についてご紹介します。**</font> 皆様の理解度と利用用途に応じて、前編とあわせてご活用ください。

----

- 前編
  - 開発環境のセットアップ
  - Visual Studio Codeのセットアップ
  - TensorflowでMNISTデータセットの準備
  - TensorflowのSequential APIによるAIモデルの定義
  - Tensorflowの定義したAIモデルの学習
  - 学習済みAIモデルの評価・利用
  - TensorBoardによる学習結果の可視化

https://qiita.com/xtrizeShino/items/be8f405be8c3f72b10d9

----

- 後編 <font color="red">**（本記事）**</font>
  - 畳み込みニューラルネットワークの定義
  - TensorflowのFunctional APIによるSkip-Connectionの定義
  - Functional APIにより定義したモデルの学習と評価
  - Tensorflow Lite形式への変換
  - Tensorflow Liteランタイムによる推論の実行

https://qiita.com/xtrizeShino/items/dee7990dffc3a9bb8c43

----

### 参考文献

本記事は、下記の資料を参考にさせていただきました。

https://www.amazon.co.jp/gp/product/4807920383/

----

## MNIST画像の分類を行う畳み込みニューラルネットワークをTensorflowのSequential APIを使って定義する

前編の記事では、AIモデルを構築して60000個の学習データで200epochs学習したものの、Accuracyは28%に留まる結果となってしまいました。これは、全結合層(Dense)の乱用から来るものです。Denseは全てのニューロンからの値に重みをつけて意味を見出そうとしますが、画像を認識する際は、全ピクセルの情報を統合する意味はほとんどありません。 **これはMNISTが画像であるため、画像全体でなく、ある箇所において画像の上下左右がどうなっているのか、例えば「"6"は左下端が繋がっているが"5"は左下端が開いている」といったような「ある地点を中心とした空間に関する解析」が必要である** ということです。

https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dense

### 畳み込み層(Conv2D)を利用したAIモデルを定義する

こうした課題を解決できるのが畳み込み層(Conv2D)です。畳み込み層はカーネルと呼ばれる多数のフィルタを画像に適用し、縦横斜めのピクセルと、その中心となるピクセルの依存関係を見出そうとします。 **畳み込み層を利用したAIモデルを下記に示します。** これは前編に示したソースコードの `Sequential` からはじまるAIモデルの定義箇所を置き換えるものです。

<font color="blue">**※コード全体は後述します。**</font>


https://www.tensorflow.org/api_docs/python/tf/keras/layers/Conv2D

```python
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout
from tensorflow.keras.layers import Conv2D, MaxPooling2D
from tensorflow.keras.layers import Flatten

# Sequential APIを使ってモデルを定義する
model = Sequential()
# まず1段目に32要素の畳み込み層を挿入する
model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)))
# 次に2段目にも64要素の畳み込み層を挿入する
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu'))
# MaxPoolingにより高い特徴量を抽出する
model.add(MaxPooling2D(pool_size=(2,2)))
# Dropoutで弱い特徴量を削除する
model.add(Dropout(0.25))
# 2回目のConv2D(1段目)に128要素の畳み込み層を挿入する
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu'))
# 1次元に変換
model.add(Flatten())
# 全結合層の隠れ層1層目(128要素)
model.add(Dense(128, activation='relu'))
# Dropoutで弱い特徴量を削除する
model.add(Dropout(0.5))
# 最後に確率を出力する出力層を挿入する(合計確率が1.0となるsoftmaxを使う)
model.add(Dense(n_classes, activation='softmax'))
# サマリーを表示する
model.summary()
```

#### 畳み込み層を持つAIモデルの概略

上記のコードにより生成されたニューラルネットワークは、下記の構造を持ちます。入力画像 28px x 28pxに対して、以前のアプローチでは(前記事の方法では)、入力を1次元化し全結合層で対応しようとしていましたが、 **本アプローチでは入力データを2次元として、隣り合う左右のデータに加えて、上下方向のデータの繋がりにも意味があるものとして扱います。** このAIモデルは、まず2次元の畳み込み層(Conv2D)と、MaxPooling、Dropoutにより2次元の画像を解析した後に、全結合層(Dense)で結果をまとめるという構成となっています。

![スクリーンショット 2025-01-10 18.13.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/fbeca1c1-100e-094f-767e-cf25e28525d3.png)

### 畳み込みニューラルネットワークの学習結果

このAIモデルに対してMNISTの60000個の画像で200epochsの学習を実施した結果、 **検証用データに対して実に96%ものAccuracyを達成することができました。** この学習済みのAIモデルを利用して推論すると、学習用の入力画像の先頭である"5"の画像に対して、この画像が"5"である確率を「91.6%」であると回答することができます。

![スクリーンショット 2025-01-10 18.16.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/ec13a2f2-d017-9646-168a-59bec50cd94e.png)

#### TensorBoardによる学習結果の解析

また、TensorBoardにて学習結果を確認すると、おおよそ20epochsまでにAccuracyが大きく改善し、200epochs付近では概ね収束していることを確認することができます。 **96%付近で留まっていることから、AIモデルに対する学習は十分なものであり、これ以上の精度を得るには別のアプローチが必要** だということがわかります。

```bash
# 推論を実施する
$ python3 conv_mnist.py
# ...
# test_loss: 0.00516069820150733
# test_acc: 0.9667999744415283
# 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 257ms/step
# predictions[0]: [2.49131432e-07 1.12435394e-07 9.98074665e-06 8.31116885e-02
#  7.87902188e-10 9.16857183e-01 4.41472849e-07 9.78024630e-08
#  1.08170880e-05 9.44602289e-06]
# quit()

# 学習後にTensorBoardを起動する
$ tensorboard --logdir='logs/deep-net' --port 6006
```

![スクリーンショット 2025-01-10 18.20.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/aca1496e-13ce-08c7-2ba1-1f9cdb0fead5.png)

なお、畳み込み層を導入する前の1次元でMNISTを全結合層(Dense)で分類しようとする前記事のAIモデルでは、200epochsの学習を経ても、Accuracyはわずか28%でした。 **加えて、こちらもAccuracyが低い状態ながらも収束が始まっているため、学習回数が不足しているから精度が低いというわけでもなさそうです。** このように、AIモデルの差が精度の大きな差に繋がることを確認できます。

![スクリーンショット 2025-01-10 15.27.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/9779cac8-2ced-e85d-5f9e-34a2c318b855.png)

### MNISTに対する畳み込みニューラルネットワーク(全文)

ソースコード全体は下記の通りです。

```python
import tensorflow.keras as keras
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout
from tensorflow.keras.layers import Conv2D, MaxPooling2D
from tensorflow.keras.layers import Flatten
from tensorflow.keras.optimizers import SGD
from matplotlib import pyplot as plt
import numpy as np

# MNISTの先頭を出力する画像ファイルへのパス
SAVE_FIGURE_TRAIN_HEAD = './mnist_images_head.jpg'

# MNISTのデータをロードする
(x_train, y_train), (x_valid, y_valid) = mnist.load_data()

# MNISTのデータ形状を確認する
print("x_train.shape:", x_train.shape)
print("y_train.shape:", y_train.shape)
print("x_valid.shape:", x_valid.shape)
print("y_valid.shape:", y_valid.shape)

### ネットワーク向けにデータを加工する
# 2次元のまま利用する
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1).astype('float32')
x_valid = x_valid.reshape(x_valid.shape[0], 28, 28, 1).astype('float32')
x_train /= 255 # 0~1のfloat型に変換
x_valid /= 255 # 0~1のfloat型に変換
# 出力ラベルをOne-Hotエンコーディングする
n_classes = 10
y_train = keras.utils.to_categorical(y_train, n_classes)
y_valid = keras.utils.to_categorical(y_valid, n_classes)
print("y_train[0]:",y_train[0]) # One-Hotエンコーディングできているか確認

### 推論向けに先頭のデータのみ取り出しておく
test_predict_input = x_train[0]
test_predict_input_expand = test_predict_input[np.newaxis, ...]
print("x_train[0]:", test_predict_input_expand.shape)

# Sequential APIを使ってモデルを定義する
model = Sequential()
# まず1段目に64要素の畳み込み層を挿入する
model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)))
# 次に2段目にも64要素の畳み込み層を挿入する
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu'))
# MaxPoolingにより高い特徴量を抽出する
model.add(MaxPooling2D(pool_size=(2,2)))
# Dropoutで弱い特徴量を削除する
model.add(Dropout(0.25))
# 2回目のConv2D(1段目)に128要素の畳み込み層を挿入する
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu'))
# 1次元に変換
model.add(Flatten())
# 全結合層の隠れ層1層目(128要素)
model.add(Dense(128, activation='relu'))
# Dropoutで弱い特徴量を削除する
model.add(Dropout(0.5))
# 最後に確率を出力する出力層を挿入する(合計確率が1.0となるsoftmaxを使う)
model.add(Dense(n_classes, activation='softmax'))
# サマリーを表示する
model.summary()

# モデルを学習に向けてコンパイルする
# -->> optimizerには SGD (学習率=0.01) を設定
# -->> lossは誤差の二乗の平均を選択('mean_squared_error')
model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=1e-2), metrics=['accuracy'])

# AIの学習を可視化するTensorBoardを使う
from keras.callbacks import TensorBoard
tensorboard = TensorBoard('logs/deep-net')

# AIの学習を実行する
model.fit(x_train, y_train, 
          batch_size = 256, epochs=200,
          verbose=1, validation_data=(x_valid, y_valid),
          callbacks=[tensorboard])

# AIモデルを評価する
test_loss, test_acc = model.evaluate(x_valid, y_valid, verbose=0)
print("test_loss:", str(test_loss))
print("test_acc:", str(test_acc))

# 1個目の学習用データで推論してみる "5" となるはず
predictions = model.predict(test_predict_input_expand)
print("predictions[0]:", predictions[0])

print("quit()")
quit()
```

## Tensorflow.kerasのFunctional APIを使ってみる

ここまでは、入力から出力まで、流れてくるデータに対して順次処理を適用していくAIモデルを定義してきました。そして、そうしたAIモデルではAccuracyが96%程度で伸び悩んでしまうことも確認できました。 **さらに精度を向上させるアプローチとして、ResNetをはじめとする画像認識AIでは、データを途中で分流させ、畳み込みを加えたデータと、畳み込みを加えないデータ（畳み込みをスキップしたデータ）を結合する「Skip-Connection (スキップ接続)」により高い認識精度を達成** するものがあります。

こうした「分流(Skip-Connection)」のあるネットワークはこれまでのSequential APIでは表現することができません。Tensorflow.kerasは、こうした課題に対して、 **AIを構成する機能ブロックの入出力を繋ぎ合わせて、その入出力の繋がりからAIモデルを定義することのできる「Functional API」を提供** しています。

https://www.tensorflow.org/guide/keras/functional?hl=ja

https://cvml-expertguide.net/terms/dl/inter-layer-connection/skip-connection/

実際にコードを見た方が早いので、早速ご紹介します。

### Functional APIによるAIモデルの表現方法

Functional APIでは、まずニューラルネットワークの入り口となる `inputs` を `keras.Input` で定義します。次に、AIモデルを構成する機能ブロックを `layers.*` のインスタンスとして定義します。そして、 **定義した入力と、定義したインスタンス化した各機能ブロックを、変数で繋ぐことによりAIモデルを形作ります。** 最終的な出力は変数(`outputs`)となりますので、これを最後に `inputs` と `outputs` の組みとして `keras.Model(inputs, outputs, name)` に代入し、AIモデルを生成します。

```python
###
# Functional APIを使ってモデルを定義する
###
# MNIST 28px x 28px x 1ch を入力とする
inputs = keras.Input(shape=(28, 28, 1))
# 1段目に32要素の畳み込み層を挿入する
conv2d_1st = Conv2D(32, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_1st_output = conv2d_1st(inputs)
# 2段目に64要素の畳み込み層を挿入する
conv2d_2nd = Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_2nd_output = conv2d_2nd(conv2d_1st_output)
# 3段目に64要素の畳み込み層を挿入する
conv2d_3rd = Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_3rd_output = conv2d_3rd(conv2d_2nd_output)

## 2段目の出力を3段目の出力に加える
add_1st = keras.layers.Add()
add_1st_output = add_1st([conv2d_2nd_output, conv2d_3rd_output])

# 4段目にも64要素の畳み込み層を挿入する
conv2d_4th = Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_4th_output = conv2d_4th(add_1st_output)
# 5段目にも64要素の畳み込み層を挿入する
conv2d_5th = Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_5th_output = conv2d_5th(conv2d_4th_output)

## 2段目の出力を3段目の出力に加える
add_2nd = keras.layers.Add()
add_2nd_output = add_2nd([conv2d_4th_output, conv2d_5th_output])

# 6段目にも64要素の畳み込み層を挿入する
conv2d_6th = Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_6th_output = conv2d_6th(add_2nd_output)
# MaxPoolingにより高い特徴量を抽出する
maxpool = MaxPooling2D(pool_size=(2,2))
maxpool_output = maxpool(conv2d_6th_output)
# Dropoutで弱い特徴量を削除する
dropout_1st = Dropout(0.25)
dropout_1st_output = dropout_1st(maxpool_output)
# 縮小後に畳み込みを挿入する
conv2d_7th = Conv2D(128, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_7th_output = conv2d_7th(dropout_1st_output)
# 1次元に変換
flatten = Flatten()
flatten_output = flatten(conv2d_7th_output)
# 全結合層
dense_flatten = Dense(128, activation='relu')
dense_flatten_output = dense_flatten(flatten_output)
# 全結合層
dense_last = Dense(10, activation='softmax')
dense_last_output = dense_last(dense_flatten_output)
outputs = dense_last_output

# モデルに変換する
model = keras.Model(inputs=inputs, outputs=outputs, name="mnist_model")

# サマリーを表示する
model.summary()
```

こうして構築したAIモデルは、下記のような構造となります。このSummaryでは、 `Connected to` として、ひとつのAIを構成するニューロン `Add` が、複数のニューロンからのデータを集約していることを確認することができます。

https://www.tensorflow.org/api_docs/python/tf/keras/layers/Add

![スクリーンショット 2025-01-10 22.19.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/70e560ea-2a4b-583f-550a-2185c14e7ef9.png)

#### モデルの可視化

これだけではわかりにくいので、Functional APIで定義したモデルを下記のコードにより可視化（グラフを画像化）してみましょう。 `keras.utils.plot_model` は、AIモデルのデータの流れを示すグラフを画像として保存することを可能とするAPIです。 **なお、`plot_model` の実行には `pydot` と `graphviz` が必要になります。実行前に下記のコマンドを実行してインストールしておいてください。** 

```python
# モデルの画像を保存する
keras.utils.plot_model(model, "my_first_model.png")
```

```bash
# keras.utils.plot_modelに必要なパッケージをインストールする
$ pip3 install pydot
$ pip3 install graphviz
# 忘れやすいので注意：pip3によるインストールのみでは動作しない
$ sudo apt install graphviz
```

生成されたグラフは、以下のようになります。
Skip-Connectionが形成されていますね。

![スクリーンショット 2025-01-10 22.06.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/768e64fc-2f50-f8fa-842d-b1fd64d60932.png)

#### 性能の評価

構築したAIモデルを、MNISTが提供する60000個の学習用データで、200epochs学習させると、 **Accuracy「98%」を達成することができます。AIモデルの改善により、畳み込み層のみのアプローチよりも優れた精度の実現が可能となりました。"5"の画像に対する推論も「96.8%」の確度となり、精度が向上していることを確認** することができます。

```bash
$ python3 ./functional_mnist.py
# ...
# test_loss: 0.002693937160074711
# test_acc: 0.9819999933242798
# ...
# predictions[0]: [8.9450872e-08 1.7601913e-10 3.5305588e-06 3.1478498e-02 2.3061484e-15 9.6851605e-01 6.6542358e-12 1.0044603e-06 7.7523808e-08 7.4414010e-07]
# Save Model: functional_mnist.keras
```

![スクリーンショット 2025-01-11 1.11.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/208a0d34-9e35-2bcd-b668-c626e1002762.png)

#### TensorBoardによる学習結果の確認

TensorBoardにより学習結果を確認すると、Accuracyがほとんど収束していることが確認できます（図上）。畳み込みニューラルネットワークの場合（図下）と比較すると、少しの学習回数(epochs)の段階で高い精度を達成できている様子も見ることができます。

![スクリーンショット 2025-01-11 1.10.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/4323eda3-2a6f-b3bd-ddab-d86c7ec7e4af.png)

![スクリーンショット 2025-01-10 18.20.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/5ecf2ffe-188d-6cc6-b80b-aad7b83311a0.png)

なお、これは余談ですが、学習回数を200epochsから1200epochにすると、 **学習用データに対してのみ強い精度を示し、検証用データではあまり精度を得ることのできない<font color="red">「過学習(Overfitting)」</font>の状態（下図）を見ることができます。** Overfittingに対する対策方法は、後日、別の解説記事を準備しますので、ここではそういうものとしてご理解ください。

https://www.mathworks.com/discovery/overfitting.html

![スクリーンショット 2025-01-11 7.10.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/11f734d7-db53-2aa1-0b3a-7f99a7f5b215.png)

#### 学習済みAIモデルの保存

最後に、十分な精度を得ることができた本AIモデルと、その学習結果を、他のプログラム上の推論処理で利用できるように、 **学習済みAIモデルを保存しておきましょう。** 保存方法は、Tensorflowから下記の2つの記事で紹介されていますが、今回は「モデルの保存と復元」の記事を参考にしました。 **現在のTensorflow.kerasでは保存時に拡張子 `.keras` を指定することにより「新しい高レベルの `.keras` 形式」でAIモデルとその重み（学習結果）を統合して保存することができます。**

https://www.tensorflow.org/tutorials/keras/save_and_load?hl=ja

https://www.tensorflow.org/guide/keras/save_and_serialize?hl=ja

```python 
# 学習結果を保存するファイルパス(.keras形式)
PATH_SAVE_MODEL = 'functional_mnist.keras'

# Save Model to .keras
print("Save Model:", PATH_SAVE_MODEL)
model.save(PATH_SAVE_MODEL)
```

#### 評価用の入力画像をnumpy形式で保存する

ここまでの手順では `tensorflow` を利用してMNIST画像を取得してきました。ここでは繰り返し手順を除去する意味と、Tensorflow Liteランタイムによる推論にて `tensorflow` を `import` したくない（純粋に `tflite_runtime` のみで動作していることを示す）ために `numpy.save()` 機能を利用して、画像をファイルに保存します。なお、ファイルのパスを指定していますが **勝手に拡張子が付与されますので(逆にロードの時は拡張子が付与されないので、やや不便なのですが...)、拡張子の無いファイルパスを指定してください。**

```python
# ...
# 推論に使う画像データを保存する
# -->> 実際は './mnist_head_image.npy' として保存される
SAVE_NUMPY_INPUT_IMAGE = './mnist_head_image'
# ...

# ...
### 推論向けに先頭のデータのみ取り出しておく
test_predict_input = x_train[0]
test_predict_input_expand = test_predict_input[np.newaxis, ...]
print("x_train[0]:", test_predict_input_expand.shape)

### 後の工程でも利用できるように先頭要素をnumpy形式で保存しておく
# -->> Tensorflow/Tensorflow Liteを同じ仮想環境に同居させることができない問題への
# -->> 解決策として実施、tflite_runtimeで推論する際にこのデータを利用します
np.save(SAVE_NUMPY_INPUT_IMAGE, test_predict_input_expand)

# ...
```

### 画像分類するSkip-Connectionを持ったAIモデルのソースコード全文

最後に、コード全文を紹介します。

```python
import tensorflow.keras as keras
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout
from tensorflow.keras.layers import Conv2D, MaxPooling2D
from tensorflow.keras.layers import Flatten
from tensorflow.keras.optimizers import SGD
from matplotlib import pyplot as plt
import numpy as np

# 学習結果を保存するファイルパス(.keras形式)
PATH_SAVE_MODEL = 'functional_mnist.keras'
# MNISTの先頭を出力する画像ファイルへのパス
SAVE_FIGURE_TRAIN_HEAD = './mnist_images_head.jpg'
# 推論に使う画像データを保存するファイルパス
SAVE_NUMPY_INPUT_IMAGE = './mnist_head_image'

# MNISTのデータをロードする
(x_train, y_train), (x_valid, y_valid) = mnist.load_data()

# MNISTのデータ形状を確認する
print("x_train.shape:", x_train.shape)
print("y_train.shape:", y_train.shape)
print("x_valid.shape:", x_valid.shape)
print("y_valid.shape:", y_valid.shape)

### ネットワーク向けにデータを加工する
# 2次元のまま利用する
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1).astype('float32')
x_valid = x_valid.reshape(x_valid.shape[0], 28, 28, 1).astype('float32')
x_train /= 255 # 0~1のfloat型に変換
x_valid /= 255 # 0~1のfloat型に変換
# 出力ラベルをOne-Hotエンコーディングする
n_classes = 10
y_train = keras.utils.to_categorical(y_train, n_classes)
y_valid = keras.utils.to_categorical(y_valid, n_classes)
print("y_train[0]:",y_train[0]) # One-Hotエンコーディングできているか確認

### 推論向けに先頭のデータのみ取り出しておく
test_predict_input = x_train[0]
test_predict_input_expand = test_predict_input[np.newaxis, ...]
print("x_train[0]:", test_predict_input_expand.shape)

### 後の工程でも利用できるように先頭要素をnumpy形式で保存しておく
# -->> Tensorflow/Tensorflow Liteを同じ仮想環境に同居させることができない問題への
# -->> 解決策として実施、tflite_runtimeで推論する際にこのデータを利用します
np.save(SAVE_NUMPY_INPUT_IMAGE, test_predict_input_expand)

###
# Functional APIを使ってモデルを定義する
###
# MNIST 28px x 28px x 1ch を入力とする
inputs = keras.Input(shape=(28, 28, 1))
# 1段目に32要素の畳み込み層を挿入する
conv2d_1st = Conv2D(32, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_1st_output = conv2d_1st(inputs)
# 2段目に64要素の畳み込み層を挿入する
conv2d_2nd = Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_2nd_output = conv2d_2nd(conv2d_1st_output)
# 3段目に64要素の畳み込み層を挿入する
conv2d_3rd = Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_3rd_output = conv2d_3rd(conv2d_2nd_output)

## 2段目の出力を3段目の出力に加える
add_1st = keras.layers.Add()
add_1st_output = add_1st([conv2d_2nd_output, conv2d_3rd_output])

# 4段目にも64要素の畳み込み層を挿入する
conv2d_4th = Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_4th_output = conv2d_4th(add_1st_output)
# 5段目にも64要素の畳み込み層を挿入する
conv2d_5th = Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_5th_output = conv2d_5th(conv2d_4th_output)

## 2段目の出力を3段目の出力に加える
add_2nd = keras.layers.Add()
add_2nd_output = add_2nd([conv2d_4th_output, conv2d_5th_output])

# 6段目にも64要素の畳み込み層を挿入する
conv2d_6th = Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_6th_output = conv2d_6th(add_2nd_output)
# MaxPoolingにより高い特徴量を抽出する
maxpool = MaxPooling2D(pool_size=(2,2))
maxpool_output = maxpool(conv2d_6th_output)
# Dropoutで弱い特徴量を削除する
dropout_1st = Dropout(0.25)
dropout_1st_output = dropout_1st(maxpool_output)
# 縮小後に畳み込みを挿入する
conv2d_7th = Conv2D(128, kernel_size=(3, 3), activation='relu', padding='same')
conv2d_7th_output = conv2d_7th(dropout_1st_output)
# 1次元に変換
flatten = Flatten()
flatten_output = flatten(conv2d_7th_output)
# 全結合層
dense_flatten = Dense(128, activation='relu')
dense_flatten_output = dense_flatten(flatten_output)
# 全結合層
dense_last = Dense(10, activation='softmax')
dense_last_output = dense_last(dense_flatten_output)
outputs = dense_last_output
# モデルに変換する
model = keras.Model(inputs=inputs, outputs=outputs, name="mnist_model")
# サマリーを表示する
model.summary()
# モデルの画像を保存する
keras.utils.plot_model(model, "my_first_model.png")

# モデルを学習に向けてコンパイルする
# -->> optimizerには SGD (学習率=0.01) を設定
# -->> lossは誤差の二乗の平均を選択('mean_squared_error')
model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=1e-2), metrics=['accuracy'])

# AIの学習を可視化するTensorBoardを使う
from keras.callbacks import TensorBoard
tensorboard = TensorBoard('logs/deep-net')

# AIの学習を実行する
model.fit(x_train, y_train, 
          batch_size = 256, epochs=200,
          verbose=1, validation_data=(x_valid, y_valid),
          callbacks=[tensorboard])

# AIモデルを評価する
test_loss, test_acc = model.evaluate(x_valid, y_valid, verbose=0)
print("test_loss:", str(test_loss))
print("test_acc:", str(test_acc))

# 1個目の学習用データで推論してみる "5" となるはず
predictions = model.predict(test_predict_input_expand)
print("predictions[0]:", predictions[0])

# Save Model to .keras
print("Save Model:", PATH_SAVE_MODEL)
model.save(PATH_SAVE_MODEL)

print("quit()")
quit()
```

## 保存済みのTensorflow.kerasベースのAIモデルをTensorflow Liteモデルに変換し、Tensorflow Liteランタイムにより実行する

ここまでの記事で、十分な精度を持ったMNIST画像を分類するAIモデルを構築することができましたので、**最後にこのAIモデルをポータビリティの高いTensorflow Lite形式へと変換し、Tensorflow Liteランタイムを使って推論できることを紹介したいと思います。** Tensorflow Lite形式のモデルを得るには、まずTensorflow形式の学習済みのAIモデルを準備した後に、Tensorflow Liteコンバータと呼ばれるPythonのAPIを利用して、これをtflite形式へと変換します。変換したモデルは `.tflite` ファイルとして保存できますので、これによりAIモデルを好きな環境へ持ち運ぶことができます。

### Tensorflow.kerasベースのAIモデルをTensorflow Liteモデルに変換する

以下のプログラムは、Tensorflow.kerasで定義したAIモデル(ここでは前節で保存した `.keras` 形式のAIモデル)を `.tflite` 形式のTensorflow Lite準拠のAIモデルに変換するものです。 **本来は必要ありませんが、本プログラムでは、保存されたAIモデルが正しく保存されていることを確認するために `model.summary()` の実行と、MNISTデータセットによる推論を試しています。**

https://ai.google.dev/edge/litert/models/convert_tf?hl=ja

#### convert時のパラメータ指定

なお、TensorflowのAIモデルから、Tensorflow LiteのAIモデルへと変換する際には自動的にネットワークの最適化が実行されます。 <font color="blue">**これによりconvertに対するの最適化のパラメータを特に何も指定しないと、量子化等により入出力のデータ型が変質します。この対策として `converter.optimizations` にて `tf.lite.Optimize.OPTIMIZE_FOR_SIZE` を指定して最適化を重みのみに留め、 `converter.target_spec.supported_types` を指定して入出力のデータ型を明記するようにしましょう。**</font>

https://qiita.com/f43qs9xg/items/29b9d10812dbadc5312b

#### Dropout層の扱い

**また、Tensorflow LiteランタイムはDropout層に対応していません。** Dropout層が含まれていると`tensorflow.lite` を使うことは可能ですが `tflite_runtime.interpreter` でAIモデルをロードしようとした際に下記のエラーが発生しロードに失敗します。<font color="red">**この問題に対しては `converter.target_spec.supported_ops` をコードに挿入する必要があります。**</font>

```python
# 動作しない
import tflite_runtime.interpreter as tflite
# ...
# 以下の箇所でエラーが発生する
interpreter = tflite.Interpreter(model_path=PATH_MODEL_TFLITE)
### <built-in method CreateWrapperFromFile of PyCapsule object at 0x76f004194cc0> returned a result with an exception set
###   File "/home/shino/tensorflow-tflite-work/mnist_tflite.py", line 26, in <module>
###   interpreter = tflite.Interpreter(model_path=PATH_MODEL_TFLITE)
### SystemError: <built-in method CreateWrapperFromFile of PyCapsule object at 0x76f004194cc0> returned a result with an exception set
```

**発生するエラーメッセージ**

![スクリーンショット 2025-01-11 13.08.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/cc5ba80f-c601-872b-cad5-faa1de7ab18b.png)

**参考となったWEBページ**

https://github.com/tensorflow/tensorflow/issues/44232

https://github.com/tensorflow/tensorflow/issues/39300

### ソースコード全体

以上をまとめた、Tensorflow.kerasのAIモデルからTensorflow Lite形式のAIモデルを生成するプログラムを下記に示します。 **特に気を付けるところはconverterのパラメータを指定する付近のコードです。**

```python
# AIモデルを扱うためにTensorflow.kerasをロードする
import tensorflow as tf
import tensorflow.keras as keras
# 動作確認用のデータ向けにロードする
from tensorflow.keras.datasets import mnist
import numpy as np

# 学習済みのAIモデルを格納したファイルパス(.keras形式)
PATH_MODEL_TF = 'functional_mnist.keras'
# tflite形式を出力するファイルパス(.tflite形式)
PATH_MODEL_TFLITE = 'class_mnist.tflite'

###
# 保存済みのAIモデルをロードする
###
# 保存済みのモデルをロードする
new_model = tf.keras.models.load_model(PATH_MODEL_TF)
# モデルを表示する
new_model.summary()

###
# 本箇所は任意 <念の為の動作確認>
###
# MNISTのデータをロードする
(x_train, y_train), (x_valid, y_valid) = mnist.load_data()
# 学習用データセットを変換する
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1).astype('float32')
x_train /= 255 # 0~1のfloat型に変換
### 推論向けに先頭のデータのみ取り出しておく
test_predict_input = x_train[0]
test_predict_input_expand = test_predict_input[np.newaxis, ...]
print("x_train[0]:", test_predict_input_expand.shape)
# 1個目の学習用データで推論してみる "5" となるはず
predictions = new_model.predict(test_predict_input_expand)
print("predictions[0]:", predictions[0])

###
# Tensorflow.kerasモデルを
# Tensorflow Lite形式に変換する
###
converter = tf.lite.TFLiteConverter.from_keras_model(new_model)
# 重みのみを量子化し、入出力のデータ型は維持する
converter.optimizations = [tf.lite.Optimize.OPTIMIZE_FOR_SIZE]
# 入出力のデータ型はfloat32とする
converter.target_spec.supported_types = [tf.float32]
# Dropout層をTFLiteでも利用できるようにする
converter.target_spec.supported_ops = [
                tf.lite.OpsSet.TFLITE_BUILTINS,  # enable TensorFlow Lite ops.
                tf.lite.OpsSet.SELECT_TF_OPS  # enable TensorFlow ops.
            ]
# モデルを変換する
tflite_model = converter.convert()
# 変換したTensorflow Liteモデルを保存する
with open(PATH_MODEL_TFLITE, 'wb') as f:
  f.write(tflite_model)
# 保存先は PATH_MODEL_TFLITE
print("SAVE tflite model to :", PATH_MODEL_TFLITE)
```

上記のソースコードを `tf_to_tflite.py` に保存してを実行することにより、Tensorflow.kerasにより記述した学習済みのAIモデルである `functional_mnist.keras` から`.tflite` 形式のモデル `class_mnist.tflite` を生成できます。

```bash
### 保存されているkeras AIモデルを確認
$ ls *.keras
# functional_mnist.keras

# 変換スクリプトを実行
$ python3 ./tf_to_tflite.py
# ...
# WARNING:absl:Optimization option OPTIMIZE_FOR_SIZE is deprecated, please use optimizations=[Optimize.DEFAULT] instead.
# WARNING:absl:Optimization option OPTIMIZE_FOR_SIZE is deprecated, please use optimizations=[Optimize.DEFAULT] instead.
# W0000 00:00:1736567486.513195  380000 tf_tfl_flatbuffer_helpers.cc:365] Ignored output_format.
# W0000 00:00:1736567486.513205  380000 tf_tfl_flatbuffer_helpers.cc:368] Ignored drop_control_dependency.
# 2025-01-11 12:51:26.513401: I tensorflow/cc/saved_model/reader.cc:83] Reading SavedModel from: /tmp/tmp329ot5yo
# 2025-01-11 12:51:26.513890: I tensorflow/cc/saved_model/reader.cc:52] Reading meta graph with tags { serve }
# 2025-01-11 12:51:26.513898: I tensorflow/cc/saved_model/reader.cc:147] Reading SavedModel debug info (if present) from: /tmp/tmp329ot5yo
# I0000 00:00:1736567486.518423  380000 mlir_graph_optimization_pass.cc:401] MLIR V1 optimization pass is not enabled
# 2025-01-11 12:51:26.519316: I tensorflow/cc/saved_model/loader.cc:236] Restoring SavedModel bundle.
# 2025-01-11 12:51:26.558658: I tensorflow/cc/saved_model/loader.cc:220] Running initialization op on SavedModel bundle at path: /tmp/tmp329ot5yo
# 2025-01-11 12:51:26.567541: I tensorflow/cc/saved_model/loader.cc:466] SavedModel load for tags { serve }; Status: success: OK. Took 54142 microseconds.
# SAVE tflite model to : class_mnist.tflite

### 保存されたtflite AIモデルを確認
$ ls *.tflite
# class_mnist.tflite
```
## Tensorflow LiteとTensorflowを同居させた際に仮想環境のNumpyがバージョン1.xであっても、Numpyを1.x系にするよう求めるメッセージが表示されエラーとなる <font color="red">【要対策】</font>

`tflite_runtime` でTensorflow LiteでAIモデルをロードする際、 **Tensorflowに内蔵されているTensorflow LiteのAPI(`tensorflow.lite`が提供するもの)を使えば `.tflite` 形式のファイルのAIモデルを正しく読み込めるのですが、 `tflite_runtime` が提供するAPIを使うと、Numpyのバージョン依存に関するメッセージが下記のように出力されてAIモデルのロードに失敗** してしまいます。この問題は、仮想環境に含まれるnumpyのバージョンを1.x系にしていても発生しました。

https://tech.gijukatsu.com/numpy_version_check/

```python
# 動作する
import tensorflow.lite as tflite
```

```python
# 動作しない
import tflite_runtime.interpreter as tflite
```

発生するエラーメッセージは以下の通り。

![スクリーンショット 2025-01-11 14.07.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/3ade6895-a364-3471-f74f-d5757b3c11ec.png)

```bash
# ...
# A module that was compiled using NumPy 1.x cannot be run in
# NumPy 2.0.2 as it may crash. To support both 1.x and 2.x
# versions of NumPy, modules must be compiled with NumPy 2.0.
# Some module may need to rebuild instead e.g. with 'pybind11>=2.12'.
# 
# If you are a user of the module, the easiest solution will be to
# downgrade to 'numpy<2' or try to upgrade the affected module.
# We expect that some modules will need time to support NumPy 2.

# ...
#   File "/home/shino/tensorflow-tflite-work/mnist_tflite.py", line 26, in <module>
#     interpreter = tflite.Interpreter(model_path=PATH_MODEL_TFLITE)
#   File "/home/shino/anaconda3/envs/tflite_work/lib/python3.10/site-packages/tflite_runtime/interpreter.py", line 464, in __init__
#     self._interpreter = _interpreter_wrapper.CreateWrapperFromFile(
# AttributeError: _ARRAY_API not found

# ...
```

Numpyのバージョンを確認してみるものの...

```bash
# パッと見は numpy1.x が入っているようなのですが...
# TensorflowとTensorflow Liteを同居させるとエラーになるようです
$ pip3 list | grep numpy
# numpy                             1.26.4
# numpydoc                          1.7.0
```

これに対する最も簡単な対策は、**新しい仮想環境を `conda create` により作成し、numpy=1.26.4 とTensorflow Liteランタイムだけを含む「小さな仮想環境」を構築し、この中でPythonスクリプトを実行することです。** ただしこのアプローチではTensorflowの提供するMNISTは使うことができず、MNISTのデータをTensorflowに頼らず準備しなければなりません。<font color="blue">**そこでFunctional APIを試すプログラムで、学習用データの銭湯をnumpy形式で保存し、これを本プログラムの推論で使う流れとしました（前述）。**</font>

https://tech.gijukatsu.com/numpy_version_check/

```bash
# 新しい仮想環境を作成する
$ conda create -n tflite_small_work 
# 仮想環境にログインする
$ conda activate tflite_small_work
# numpy 1.x系をインストールする
$ conda install numpy=1.26.4
# Tensorflow Liteランタイムをインストールする
$ pip3 install tflite_runtime
```

そしてVisual Studio Code上で仮想環境 `tflite_small_work` を選択します。

![スクリーンショット 2025-01-11 18.48.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/68c16db8-430c-3ab5-ca32-d46d219db44a.png)

![スクリーンショット 2025-01-11 18.52.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/92d93519-bae1-1447-7e36-8e03e4061a27.png)

## Tensorflow Lite形式のAIモデルをtflite_runtimeにより読み込み、MNISTの入力画像に対して推論を実行し結果を取得する

<font color="red">**上記の小さな仮想環境を作成する手順を実施した後に、こちらの手順をお試しください。**</font>

`.tflite` 形式で保存したTensorflow Lite準拠のAIモデルで推論を行うには、Tensorflow Liteの提供する `Interpreter` APIを使います。 **入力画像はFunctional APIを試した時のスクリプトにて生成した `SAVE_NUMPY_INPUT_IMAGE = './mnist_head_image.npy'` を利用します。**

https://www.tensorflow.org/lite/guide/inference?hl=ja

https://www.tensorflow.org/lite/guide/python?hl=ja

```python
# Tensorflow Liteランタイムをロードする
import tflite_runtime.interpreter as tflite
import numpy as np

# 利用するAIモデルを格納したファイルパス(.tflite形式)
PATH_MODEL_TFLITE = 'class_mnist.tflite'
# 推論に使う画像データを読み込むためのファイルパス
SAVE_NUMPY_INPUT_IMAGE = './mnist_head_image.npy'

###
# 推論を試すために入力データを準備する
###
test_predict_input_expand = np.load(SAVE_NUMPY_INPUT_IMAGE)

###
# .tfliteのモデルをロードして、推論する
###
interpreter = tflite.Interpreter(model_path=PATH_MODEL_TFLITE)
# 入出力テンソルを確保する
interpreter.allocate_tensors()
# AIモデルの入出力情報を取得する
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
# shapeを確認する
input_shape = input_details[0]['shape']
print("tflite input shape:", input_shape)
# input_data = np.array(np.random.random_sample(input_shape), dtype=np.float32)
input_data = test_predict_input_expand
input_data_shape = input_data.shape
print("dataset input shape:", input_data_shape)
# 入力テンソルをセットする
interpreter.set_tensor(tensor_index=0, value=input_data)

# 推論を実行
interpreter.invoke()

###
# 出力テンソルを確認する
###
output_data = interpreter.get_tensor(output_details[0]['index'])
print("tflite output:", output_data[0])
```

上記のソースコードを `mnist_tflite.py` に保存して実行することにより、`.tflite` 形式のモデルを読み込み、MNISTの先頭のデータに対してTensorflow Liteが推論、0〜9までのいずれであるかの確率を出力できます。 **MNISTの先頭のデータは"5"の手書き文字のため、配列の5番目が高い確率となっていることを確認してください。**

```bash
$ python3 ./mnist_tflite.py
# ...
# tflite input shape: [ 1 28 28  1]
# dataset input shape: (1, 28, 28, 1)
# tflite output: [8.9453636e-08 1.7602489e-10 3.5304722e-06 3.1478271e-02 2.3061404e-15　9.6851635e-01 6.6542375e-12 1.0044644e-06 7.7522643e-08 7.4418432e-07]
### -->> "5"に対応する確率が高いという結果を出せていることを確認
```

**このようにして
AIモデルを実行できました！**

----

以上が、MNIST画像を畳み込み層とSkip-Connectionにより高い精度で分類できるAIモデルを構築し、そのAIモデルをTensorflow Lite形式にエクスポートした後、Tensorflow Liteランタイムで推論を行う手順となります。 **今回紹介できなかった、Python以外の言語でTensorflow LiteのAIモデルを実行する方法についても、順次紹介していきたいと思います。**

是非、本手順を皆様の開発にご活用ください。

----