TensorFlow.jsを使ってjavascript上でkerasモデルを使う

javascriptでTensorFlowモデルを使えるようにするTensorFlow.jsの基本的な使い方をまとめます

公式チュートリアル
https://www.tensorflow.org/js/tutorials

tensorflowjs APIドキュメント
https://js.tensorflow.org/api/latest/

実装例
https://github.com/tensorflow/tfjs-examples

# 試行環境
Windows10

# Node.js版とブラウザ版

tensorflowjsはC++で書かれたtensorflowのwrapperですが、クライアントサイドのブラウザで動作するjavascript版とサーバーサイドで動作するNode.js版があります。Node.js版はローカル環境に最適化された追加ライブラリで高速に動作しますしCUDAを使ってGPUもフルに使えるようになっているんだそうです。ブラウザ版はCPUのみで比較してもNode.jsと比べると速度は劣りますがクライアントのリソースを使えるのが大きなメリットです。

pandasが使えるpython版と比べるとNode.js版でも使い勝手は悪いので、pythonでモデルを作ってコンバートしたものをNode.jsをバックエンドとする環境でNode.js上で使うか、サーバーサイドの負荷を低減するためにブラウザ上のjavascriptで使うというやり方が多くなるのではないかと思います。

# python版tensorflowで作ったmodelを変換する

### 1. python版のモデルを用意する

model.save()で保存したh5ファイルを変換することが出来ます。
とりあえず入力1次元、出力1次元の回帰モデルを作って保存してみます。

```python:python
import numpy as np
import pandas as pd
import tensorflow as tf

physical_devices = tf.config.list_physical_devices('GPU')
if len(physical_devices) > 0:
    tf.config.experimental.set_memory_growth(physical_devices[0], True)
    print('memory growth:', tf.config.experimental.get_memory_growth(physical_devices[0]))
    
x = np.array([1,2,3,4]).reshape(4, 1)
y = np.array([1,3,5,7]).reshape(4, 1)

inputs = tf.keras.layers.Input(shape=(1))
outputs = tf.keras.layers.Dense(1, activation='linear')(inputs)
model = tf.keras.models.Model(inputs=inputs, outputs=outputs)
opt = tf.optimizers.Adam(lr=0.1)
model.compile(optimizer=opt, loss='mse')

model.fit(x, y, epochs=100, batch_size=1, verbose=0)
print(model.predict(x))

model.save('../model.h5')
pd.DataFrame(x).to_csv('../x.csv')
```

### 2. モデルを変換するtensorflowjsパッケージをインストールする

pythonパッケージのtensorflowjsをpip installして使用します。ただし2020年2月現在、tensorflow 2.0.1～2.1.0とは依存パッケージが競合してしまいますので、tensorflowjs用のconda環境を作って専用にした方が良いと思います。なお、tensorflow 2.1.0で作ったモデルも現状は問題なく変換できるようです。

```terminal:terminal
pip install tensorflowjs
```

### 3. モデルを変換する

tensorflowjsをインストールしたpythonにPATHが通った環境で、tensorflowjs_converterを使って変換します。以下の場合、カレントディレクトリのhoge.h5ファイルを読み込んで、/model/以下にモデル構造を記述したjsonファイルと重みを記録したbinファイルを書き出してくれます。

```terminal:terminal
tensorflowjs_converter --input_format keras hoge.h5 model
```

# ブラウザ版tensorflowjsで使う

tf.loadLayersModel()でjsonファイルのPATHを指定してやると保存したモデルを読んで、tf.tensorを放り込むと推定してくれます。ただし、ブラウザ上で動作するjavascriptはローカルのファイルを読めなくなっているので、面倒ですが適当なウェブサーバーを立ててドキュメントルート以下に上記で変換したモデルをフォルダごと置いてブラウザでURLを読み込むようにします。

```:フォルダ構造
└─document root
    │  index.html
    │  
    ├─model
       model.json
       group1-shard1of1.bin
```

```html:index.html
<!DOCTYPE html>
<html lang="jp">
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script>
    <script>
      async function run(){
        // load model
        const path = "http://localhost/model/model.json";
        const model = await tf.loadLayersModel(path);

        // predict
        const xs = tf.tensor2d([1, 2, 3, 4], [4, 1]);
        y_pred = await model.predict(xs);
        y_pred.print();

        // convert to array
        const values = await y_pred.data();
        const arr = await Array.from(values);
        console.log(arr);
      }
      run();
    </script>
  </body>
</html>
```

# Node.js版で使う

### 1. Node.jsをインストールする

Node.jsがまだ入ってないようならインストールします
https://nodejs.org/ja/

インストールするとPATHが通ってコマンドプロンプトから呼べるようになります

```terminal:コマンドプロンプト
> node -v
v12.14.1

> npm -v
6.13.4
```

### 2. Node.jsにプロジェクトを作る

NodeJS環境を初期化します。適当なフォルダを作ってフォルダ内でnpm initすると必要なファイルが作成されます。package nameなどを聞かれますがすべてdefaultで大丈夫です。最後にIs this OK?にyesと回答すれば実行されます。

```terminal:コマンドプロンプト
mkdir tensorflow-demo
cd tensorflow-demo/
npm init
```

### 3. Node.jsプロジェクトにtensorflowjsをインストールする

プロジェクトのディレクトリで以下を実行すればインストールされます。tfjs-nodeはNode.jsに最適化したライブラリで、ブラウザ版で実行するより高速に動作します。

```terminal:コマンドプロンプト
npm install @tensorflow/tfjs-node
```

GPU＋CUDAがある環境ではnpm install @tensorflow/tfjs-node-gpuすればGPUを使った高速演算ができると書かれているんですが、CUDA10.0をインストールしたWin10でやってみたらMODULE_NOT_FOUNDで動きませんでした。多分PATHが通ってないとかだと思いますが未確認です。




### 4. Node.jsでpython版modelを使う

tf.loadLayersModel()でjsonファイルのPATHを指定してやると保存したモデルを読んで、tf.tensorを放り込むと推定してくれます。ブラウザ上で動作する場合とは異なりウェブサーバーを立てる必要はありません。

```:フォルダ構造
└─project folder
    │  index.js
    │  
    ├─model
       model.json
       group1-shard1of1.bin
```

```javascript:index.js
// import
const tf = require('@tensorflow/tfjs');
require('@tensorflow/tfjs-node');

async function run(){
 // load model
 const path = "http://localhost/tensorflowjs/tfjs_001/model/model.json"
 const model = await tf.loadLayersModel(path);

 // predict
 y_pred = await model.predict(tf.tensor2d([1, 2, 3, 4], [4, 1]));
 y_pred.print();

 // convert to array
 const values = await y_pred.data();
 const arr = await Array.from(values);
 console.log(arr);
}

run();
```
index.jsと同じディレクトリからindex.jsを実行するとデモコードが動きます。

```terminal:コマンドプロンプト
node index.js
```

```terminal:実行結果
Tensor
    [[0.9999999],
     [3        ],
     [5.0000005],
     [7.0000005]]
[ 0.9999998807907104, 3, 5.000000476837158, 7.000000476837158 ]
```
推定結果が表示されれば正常です。




# tensorの計算

python版のtensorflowは適当に配列を放り込めばlistだろうがnp.arrayだろうが適当に処理してくれましたが、tensorflowjsではtf.tensor型に変換してからでないと扱えません。tf.tensorにするとjavascript標準の四則演算は出来なくなりますが、tf.add()やtf.sub()などで高速な演算が出来るようになっていますので不足はないと思われます。

四則演算以外にも三角関数とか最大最小とか色々計算できます。
詳しくは以下のAPIドキュメントを参照してください。
https://js.tensorflow.org/api/latest/

```javascript:tensorとscalarの四則演算
const x = tf.tensor1d([1,2,3])
const a = tf.scalar(4)

x.add(a).print()
x.sub(a).print()
x.mul(a).print()
x.div(a).print()
```
tensor同士の計算も同じようにできます。

```javascript:tensor同士の四則演算
const x1 = tf.tensor1d([1,2,3])
const x2 = tf.tensor1d([2,3,4])

console.log('add')
x1.add(x2).print();

console.log('sub')
x1.sub(x2).print();

console.log('mul')
x1.mul(x2).print();

console.log('div')
x1.div(x2).print();
```
tf.tensorを配列に戻すにはdataSync()メソッドを使います。

```javascript:配列に戻す
const b = tf.tensor1d([1,2,3])
console.log('b')
b.print()

const b_value = b.dataSync();
console.log('b_value')
console.log(b_value)
```








# メモリの解放

### tf.dispose()

openGLでtensorflowjsを使う場合、明示的に変数を消去しないとメモリが解放されない為、明示的にメモリを解放するためにtf.dispose()が用意されています。tf.memory()でメモリが正常にリリースされたか確認できるようになっていますので、tf.dispose()した場合としない場合を比較してみましょう。

```javascript:tf.dispose()を使わない場合
for (let i = 0; i < 100; i++) {
  const a = tf.tensor1d([1,2,3]);
}
console.log(tf.memory());
```

```javascript:tf.dispose()を使った場合
for (let i = 0; i < 100; i++) {
  const a = tf.tensor1d([1,2,3]);
  a.dispose()
}
console.log(tf.memory());
```
上記をそれぞれ実行するとtf.dispose()を使った場合にはtensorflowjsが保持するメモリがゼロになっている事が確認できます。






### tf.tidy()

複数の演算を続けて行った場合に計算途中で生じる変数も残ってしまう為、tf.dity()でこれが残らないように処理するようです。

```javascript:tf.tidy()の使い方
// y = 2 ^ 2 + 1
const y = tf.tidy(() => {
  // a, b, and one will be cleaned up when the tidy ends.
  const one = tf.scalar(1);
  const a = tf.scalar(2);
  const b = a.square();

  console.log('numTensors (in tidy): ' + tf.memory().numTensors);

  // The value returned inside the tidy function will return
  // through the tidy, in this case to the variable y.
  return b.add(one);
});

console.log('numTensors (outside tidy): ' + tf.memory().numTensors);
y.print();
```



# CSVファイルの読み込み

CSVファイルはtf.data.csv(source)で読み込めます。sourceにPATHを指定して読み込みますが、ブラウザ上で動作するjavascriptはhttpで指定されたPATHしか読めないようになっていますので、ローカルでテストする際にもhttpサーバーに置いて実行する必要があります。Node.jsの場合はそういう制限はないので普通にローカルで実行できます。

take()メソットでデータの頭出し、toArray()メソッドで配列への変換ができます。

```javascript:test.js
const df = tf.data.csv("http://localhost/hoge.csv")
console.log(df.take(10).toArray())
```


# グラフを描く

javascriptにはpython勢の大好きなmatplotlibがないんですが、ありがたいことに可視化の為のメソッドを用意してくれています。tfvisをインポートすれば簡単にグラフを描画できます。

tfvis APIドキュメント
https://js.tensorflow.org/api_vis/latest/



# Node.jsサーバーを立てる

ブラウザ版のテストをする際、Node.jsを使えば簡単にhttpサーバーを立てることができます

```terminal:コマンドプロンプト
npm install http-server -g
```

```terminal:document_rootにするフォルダで以下を実行
http-server -g
```