[TensorFlow / C++ / CPU] Pythonで学習 & C++で推論 (2022 Ver.). III. C++で推論実行

# Pythonでネットワークを学習し、C++で推論する （2022年版）
`TenosrFlow`を用いて、**PythonとGPUでネットワークのパラメータを学習し、C++とCPUで推論を行う**ために必要な作業を紹介します。
[前回](https://qiita.com/kyaFUK/items/a4cb330b77bc3a41ac3c)は、`Python`で学習したモデル(`.ckpt`ファイル)を`C++`で読み込み可能な形式(`.pb`ファイル)に変換しました。

# 前提条件
`TensorFlow`を用いた学習は`Python`で実行します。
モデルのパラメータやネットワークを保存した`.ckpt`ファイルはすでに作成済みであるとします。

`Python`で学習したネットワークを用いて`C++`で推論を行います。
また、学習・推論で用いるアクセラレータ等の環境は`CPU only`または`CPU+GPU`を想定しています。

ネットワークの入出力層の名前はすでに分かっている（[詳しくはこちらをご参照ください](https://qiita.com/kyaFUK/items/635ef0d31e8e7eba9ab8)）とします。

`.ckpt`ファイルを`.pb`ファイルに変換できている（[詳しくはこちらをご参照ください](https://qiita.com/kyaFUK/items/a4cb330b77bc3a41ac3c)）とします。

# 環境
**学習**：tensorflow-gpu-2.4.1（Python）
**推論**：[tensorflow_cc](https://github.com/FloopCZ/tensorflow_cc)-2.4.1（C++）

# C++でTensorFlowの推論を実行する手順
</br>
[1. 学習済みモデルの入力層・出力層の名前を取得する。](https://qiita.com/kyaFUK/items/635ef0d31e8e7eba9ab8)
[2. **.ckptファイルを.pbファイルに変換する。](https://qiita.com/kyaFUK/items/a4cb330b77bc3a41ac3c)
<font color="Red">3. **.pbファイルと[tensorflow_cc](https://github.com/FloopCZ/tensorflow_cc)を用いて、C++でTensorFlowの推論を実行する。** </font>

このページでは<font color="Red">3.</font>を解説します。

# [tensorflow_cc](https://github.com/FloopCZ/tensorflow_cc)のインストール
まずは、環境構築を行います。
本番環境で実装する前に、`Docker`などで環境を使ってテストをします。
ここでは、[tensorflow_cc](https://github.com/FloopCZ/tensorflow_cc)を使います。
`clone`したら、_Running the image on CPU_のコマンドを使って`Docker Image`から`Container`を作ります。
![docker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613755/2eda7ef9-0abb-935a-39c3-e31a8a3ef87e.png)

これ以降は、この`Container`の中で作業します。

# `.pb`ファイルと[tensorflow_cc](https://github.com/FloopCZ/tensorflow_cc)を用いて、`C++`で`TensorFlow`の推論を実行する。
[@rxoxixyxd様の解説](https://qiita.com/rxoxixyxd/items/f2b8a0ddf09b6734d313)と[こちらのリポジトリ](https://github.com/ryojiysd/tf-train-cpp)を参考にして作りました。
とりあえずサンプルを動かしてみたい場合は、上記のページをおすすめします。

今回のファイル階層は以下のようになります。

```
tensorflow_cc
├─Dockerfiles
├─Test
│  ├─build
│  ├─CMakeLists.txt
│  ├─main.cpp
│  ├─numpy.hpp
│  ├─common.cpp
│  └─common.h
```
## 概要
今回利用するプログラムは以下になります。
学習データは(64, 64, 3)の次元を持つ画像を想定しています。

```c++:main.cpp
#include <iostream>
#include <string>

#include <tensorflow/core/public/session.h>
#include "tensorflow/core/lib/io/path.h"
#include "tensorflow/core/platform/env.h"
#include "tensorflow/core/platform/init_main.h"
#include "tensorflow/core/public/session.h"

#include "common.h"

using std::string;


int main(int argc, char* argv[]) {

  const string graph_def_filename = "../frozen_graph.pb";


  // Setup global state for TensorFlow.
  tensorflow::port::InitMain(argv[0], &argc, &argv);

  std::cout << "Loading graph\n";

  // Load a frozen model
  tensorflow::GraphDef graph_def;
  TF_CHECK_OK(tensorflow::ReadBinaryProto(tensorflow::Env::Default(),
                                          graph_def_filename, &graph_def));

  // Load an input data
  std::vector<int> s;
  std::vector<double> input_data; //直接の入力

  std::vector<std::vector<std::vector<float>>> \
    batch(64, \
    std::vector<std::vector<float>> (64, \
    std::vector<float> (3) ));　// reshape&batch取得された後

  int i=0;
  aoba::LoadArrayFromNumpy("../input.npy", s, input_data);

  std::cout << s[0] << " " << s[1] << " " << s[2] << std::endl; // dimension

  /* Data reshape */
  get_batch_from_input(input_data, batch)


  // Create a session
  std::unique_ptr<tensorflow::Session> session(tensorflow::NewSession(tensorflow::SessionOptions()));
  TF_CHECK_OK(session->Create(graph_def));

  

  predict(session, input);
  
  return 0;
}
```
### TensorFlowの初期化
```c++
  // Setup global state for TensorFlow.
  tensorflow::port::InitMain(argv[0], &argc, &argv);
```
### 学習済みモデルのグラフ・パラメータ読み込み

```c++
  const string graph_def_filename = "../frozen_graph.pb";
  std::cout << "Loading graph\n";

  // Load a frozen model
  tensorflow::GraphDef graph_def;
  TF_CHECK_OK(tensorflow::ReadBinaryProto(tensorflow::Env::Default(),
                                          graph_def_filename, &graph_def));
```

### Dataの入力とReshape
データの入力には`.npy`ファイルを通して`NumPy`の`ndarray`を用います。
`C++`で`.npy`ファイルを読み込み`vecotr`に変換してくれるヘッダファイル[numpy.h](https://gist.github.com/rezoo/5656056)を用います。
また、ネットワークに入力するテンソルデータの作成は、[このページ](https://memo.saitodev.com/home/tensorflow/use_graph_in_cxx/#id8　"入力テンソルの作り方")が参考になります。

```c++
  // Load an input data
  std::vector<int> s;
  std::vector<double> input_data; //直接の入力

  std::vector<std::vector<std::vector<float>>> \
    batch(64, \
    std::vector<std::vector<float>> (64, \
    std::vector<float> (3) )); // reshape&batch取得された後

  int i=0;
  aoba::LoadArrayFromNumpy("../input.npy", s, input_data);

  std::cout << s[0] << " " << s[1] << " " << s[2] << std::endl; // dimension

  /* Data reshape */
  get_batch_from_input(input_data, batch)
```


## 推論部分
推論に用いる以下の関数は`common.cpp`で定義されています。

```c++:
predict(session, input);
```


```c++:common.cpp
#include "common.h"

#include <iostream>
#include <fstream>

#include "tensorflow/core/framework/tensor.h"
#include "tensorflow/core/lib/io/path.h"
#include "tensorflow/core/platform/env.h"
#include "tensorflow/core/platform/init_main.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/types.h"

using std::string;
using std::vector;

tensorflow::Tensor MakeTensor_Placeholder(const std::vector<std::vector<std::vector<float>>>& batch) {
  tensorflow::Tensor t(tensorflow::DT_FLOAT,
                       tensorflow::TensorShape({(int)batch.size(),64,64,3}));  //(64,64,3)

  /* tにbatchを代入する操作 */

  return t;
}


void predict(const std::unique_ptr<tensorflow::Session>& session, const std::vector<std::vector<std::vector<float>>>& batch) {
  // Create an input data

  vector<std::pair<string, tensorflow::Tensor>> inputs = {

    {"Placeholder", MakeTensor_Placeholder(batch)}//入力層

  };

  std::vector<tensorflow::Tensor> out_tensors; //結果を格納する変数ベクトル(batch_size,64,64,3)

  // Predict
  TF_CHECK_OK(session->Run(inputs, {"stack"}, {}, &out_tensors));
}

void get_batch_from_input(std::vector<int>& s, std::vector<double>& input_data,  std::vector<std::vector<std::vector<float>>>& batch){
  /* Reshape data and get a batch */
}

```

```C++:common.h
#pragma once

#include <string>
#include <vector>

#include "tensorflow/core/public/session.h"

#include "numpy.hpp"

tensorflow::Tensor MakeTensor_Placeholder(const std::vector<std::vector<std::vector<float>>>& batch);
void predict(const std::unique_ptr<tensorflow::Session>& session, const std::vector<std::vector<std::vector<float>>>& batch);
void get_batch_from_input(std::vector<int>& s, std::vector<double>& input_data,  std::vector<std::vector<std::vector<float>>>& batch);
```

### 入力層の名前
`MakeTensor_Placeholder(batch)`でvectorの`batch`をtensorの`t`に変換し、入力層`Placeholder`に代入します。
入力層の名前はモデルごとに違う可能性があるので、[こちらの方法](https://qiita.com/kyaFUK/items/635ef0d31e8e7eba9ab8)で調べる必要があります。

```C++
  vector<std::pair<string, tensorflow::Tensor>> inputs = {

    {"Placeholder", MakeTensor_Placeholder(batch)}//入力層

  };
```
## buildについて
buildにはCMakeを使います。
ディレクトリ`build`の中で、`cmake .. & make`とすればbuildできます。

```cmake:CMakeLists.txt
cmake_minimum_required(VERSION 3.3)

add_executable(predict predict.cpp common.h common.cpp numpy.hpp)

find_package(TensorflowCC REQUIRED)
target_link_libraries(predict TensorflowCC::TensorflowCC)
```

# 補足
このあと起こりうる問題として、推論のためのリソースをどの程度制限する（または拡張する）かということが考えられます。
Tensorflowには、リソースを制限するパラメータとして、`config.set_inter_op_parallelism_threads(threads num)`や`config.set_intra_op_parallelism_threads(threads num)`があります。
詳しくは、[こちらの資料](#threadsについて)をご参照ください。

# 流行りの（？）エッジAIとの関係
エッジAIとは、サーバーやクラウドなどの大規模なシステムで学習を行い、学習済みモデルが搭載されたスマホなどのエッジデバイスで推論を行うことのようです。
エッジAI、エッジ推論、Edge Machine Learning (EdgeML)、Edge Intelligenceなどとも呼ばれ、Edge Computingの分野の一つとされています。
Python on GPUで大量のデータを学習し、C++ on CPUで高速に推論を行うことで、組込みシステムやアプリ開発などにも応用できそうです。
詳しくは、[参考にしたサイト](#エッジaiについて) を参照してください。


# 参考にしたページ

### TensorFlowの学習・推論をC++で行う。

https://qiita.com/rxoxixyxd/items/f2b8a0ddf09b6734d313

https://memo.saitodev.com/home/tensorflow/use_graph_in_cxx/

https://github.com/FloopCZ/tensorflow_cc

### CMakeの設定
https://qiita.com/syoyo/items/c102611ff63a6bbadc80

### NumPy.hpp
https://gist.github.com/rezoo/5656056

http://mglab.blogspot.com/2014/03/numpyhpp-npy.html

### Threadsについて
https://stackoverflow.com/questions/61361334/how-to-set-inter-op-parallelism-threads-and-intra-op-parallelism-threads-for-see

https://www.intel.com/content/dam/develop/external/us/en/documents/pdf/parallel-universe-issue-45.pdf

### エッジAIについて

https://qiita.com/tomo_makes/items/cfde51e3b632fdd09243

https://qiita.com/cvusk/items/c89db530548e3e105ecb