Teachable Machine で出力した機械学習モデル（画像分類、TensorFlow.js用）を Node.js で動かすために公式情報をたどる＆実際に試す

タイトルの通りの内容で、以下の記事を書いた際にやった「ブラウザ上での実行」の部分を、「Node.js での実行」にしたものです。
Teachable Machine について等の話はこの記事では省略しますので、それらに関する情報をご覧になりたい方は以下の記事内のものをご覧ください。

●Teachable Machine で出力した機械学習モデル（画像分類、TensorFlow.js用）をダウンロードしてブラウザで動かす【2021年4月版】 - Qiita
　https://qiita.com/youtoy/items/3be502c6266cfb61e49a

## まずは公式情報をたどる
冒頭に書いた記事では、Teachable Machine で出力した画像分類用の機械学習モデル（TensorFlow.js用）をローカルにダウンロードし、それを用いた推論をブラウザ上で行いました。JavaScript の処理を含む HTMLファイルを使った形です。

今回は、推論を行うプログラムを Node.js で実行します。そのやり方については、公式で掲載された FAQ の情報からたどって探していきます。

### 公式のよくある質問から情報をたどる
Teachable Machine のページの上部メニューを見ると、右のほうに「[よくある質問](https://teachablemachine.withgoogle.com/faq)」と書かれた部分があります。そしてその中を見ていくと「[保存とエクスポート](https://teachablemachine.withgoogle.com/faq#%E4%BF%9D%E5%AD%98%E3%81%A8%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88)」と書かれた部分があり、さらにその中で「他のライブラリやプラットフォームでも Teachable Machine のモデルを使用できますか？」という質問がありました（※以下の画像の部分）。
![Teachable_Machineのよくある質問.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/d757c215-6489-8870-15ec-3f1290881d0f.jpeg)

上記の画像中の文章で、「Teachable Machine コミュニティの [Github リポジトリ](https://github.com/googlecreativelab/teachablemachine-community/)をご覧ください。」とあるので、その [Github リポジトリのページ](https://github.com/googlecreativelab/teachablemachine-community/)へと進んでいきます。

### Teachable Machine コミュニティの Github リポジトリ
上記の [Github リポジトリ](https://github.com/googlecreativelab/teachablemachine-community/)を見ると、「[Community Contributions and Projects](https://github.com/googlecreativelab/teachablemachine-community/#community-contributions-and-projects)」という項目の中で、以下の画像の記載を見つけました。
![Node.js用のライブラリの話.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/eea07b39-7af4-8f0f-e838-7f08bc716039.jpeg)
この「[Teachable Machine Node Library for image models](https://github.com/tr7zw/teachablemachine-node-example)」という部分をたどっていきます。

### プロジェクトのページとその先
上記の続きで「[tr7zw/teachablemachine-node-example](https://github.com/tr7zw/teachablemachine-node-example)」というページにたどり着きました。その説明を見ていこうとすると、以下の画像にあるように「最新版はまた別のページにある」という記載と誘導用のリンクが・・・。
![別ページを参照するようにオススメ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/0289348e-df1d-ebb0-2cc3-8f545ede5a9a.jpeg)

上記のリンク先は「[drinkspiller/teachablemachine-node-example](https://github.com/drinkspiller/teachablemachine-node-example/)」というページでした。さらにこの先へ誘導される、ということはなさそうです（笑）

このページで「[How It Works](https://github.com/drinkspiller/teachablemachine-node-example/#how-it-works)」と書かれた部分を見てみます。
![How It Works.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/45f593ac-846f-4472-d326-b21f5aa9af64.jpeg)
リクエストに対して、JSON でレスポンスを返すような動きをしてくれそうな記載がありました。

## サンプルを動かしてみる
上記の記載があった箇所の少し下を見ると、使い方などが書かれた部分がありました。
![使い方等.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/5d3f1f21-c2ab-e849-d473-8889ba3088d3.jpeg)
その手順通りに進めてみます。

### インストール
リポジトリの内容をクローンして、パッケージのインストールを行います。どこか適当なフォルダで、以下のコマンドを実行していきましょう。

```bash
# クローン
git clone https://github.com/drinkspiller/teachablemachine-node-example.git
# パッケージのインストール
cd teachablemachine-node-example/
npm install
```

自分の環境で実行したところ、以下のエラーが・・・。
![npm installのエラー.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/60bfbeaa-cf87-3bfc-8122-52122dbc2185.jpeg)
少し調べてみると、自分が Node.js の v15 を使っているのが関係していそうで、Node.js を v14 にすると良さそうでした。Node.js のバージョン管理ツールを使っていたので、サクッと v14 に変更してから再度 `npm install` を実行して、その後は無事にインストールがエラーもなく完了となりました。

なお、今回の内容の [package.json](https://github.com/drinkspiller/teachablemachine-node-example/blob/master/package.json) の中身を見てみると、dependencies は以下となっているようです。
![dependencies.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/3588d0a2-54d3-0bc9-cc17-3810c3fb5fbf.jpeg)

### app.js の書きかえ
次の手順は、[app.js](https://github.com/drinkspiller/teachablemachine-node-example/blob/master/app.js) の書きかえです。

以下の部分を見ると、モデルの URL の指定方法について書かれており、ファイルシステムのパスは利用不可と書いてあります。
![URL指定についての説明.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/7a7b75c5-4afd-70de-cd66-2645ca249b22.jpeg)
そして、デフォルトの設定として、事前に用意されたモデルの URL が別の行に書いてありました。具体的には以下の部分です。
![書きかえをするURL.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/cf7d8448-faf8-b490-12f5-1ea5ba0a20f1.jpeg)
この部分を独自の機械学習モデル用の設定に置きかえます。

Teachable Machine の機械学習モデルをクラウドにアップロードするやり方をとっていた場合は、そのアップロード先の URL を上記の部分に書くだけで大丈夫です。
そのやり方がシンプルな方法ではありましたが、今回は、[冒頭に書いた前の記事](https://qiita.com/youtoy/items/3be502c6266cfb61e49a
)と同様に、ローカルに機械学習モデルの関連ファイル一式をダウンロードするやり方ができないかを試していきました。

手順が少なくてすみそうなやり方で進めてみます。適当にフォルダを作成し、その中にダウンロードした機械学習モデル関連のファイル一式を置きましょう（ここで「関連ファイル」と書いた部分の詳細は、[冒頭に掲載した記事の「モデルのダウンロード」の部分](https://qiita.com/youtoy/items/3be502c6266cfb61e49a#%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89)をご参照ください）。
そして、そのファイル一式を置いたフォルダで、サーバーを立ち上げます。[冒頭に掲載した記事の「機械学習モデルのファイルに関する準備をする（＋ローカルでサーバーを動かす）」の部分](https://qiita.com/youtoy/items/3be502c6266cfb61e49a#%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E6%BA%96%E5%82%99%E3%82%92%E3%81%99%E3%82%8B%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%81%A7%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%92%E5%8B%95%E3%81%8B%E3%81%99)にも書いている、ワンライナーでサーバーを立ち上げるコマンドを使うのが簡単かと思います。

以下にコマンドの部分のみ記載しますが、どれを使っても `http://localhost:8080/` でローカルにサーバーが立ち上がります。

```bash
# 2.x系
$ python -m SimpleHTTPServer 8080
# 3.x系
$ python -m http.server 8080
```

```bash
# npmのバージョン5.2.0で導入された「npx」を利用
$ npx install http-server
```

このコマンドを機械学習モデル関連のファイルが置かれた場所で実行していれば、 `http://localhost:8080/` にアクセスすることで、機械学習モデル関連のファイルを参照可能になります。app.js の中の URL を指定する部分は、以下のように書きかえれば OK です。

```javascript
function configureEndPoints() {
  addEndpoint('test',
      'http://localhost:8080/');
}
```

`addEndpoint('test', ...` の部分は、この後の手順に関わってくる設定ですが、デフォルトのままで使っていきます。

以下に、ここで用いたソースコード全体を掲載します（サンプルをほぼそのまま使ったような感じですが）。

```javascript
const canvas = require("canvas");
const express = require("express");
const JSDOM = require("jsdom").JSDOM;
require("@tensorflow/tfjs-node");
const tmImage = require("@teachablemachine/image");

const app = express();

function init() {
  configureBodyParser();
  configureEndPoints();
  configureBrowserPolyFills();

  app.listen(3000, () => {
    console.log("Server running on port 3000");
  });
}

async function addEndpoint(name, baseUrl) {
  const modelURL = baseUrl + "model.json";
  const metadataURL = baseUrl + "metadata.json";
  const model = await tmImage.load(modelURL, metadataURL);
  app.post("/" + name, (request, response) => {
    const base64Image = Buffer.from(request.body).toString("base64");
    const contentType = request.get("Content-Type");
    getPrediction(model, base64Image, contentType, (output) => {
      response.send(output);
    });
  });
}

function configureBodyParser() {
  app.use(require("body-parser").raw({ type: "image/jpeg", limit: "3MB" }));
}

function configureBrowserPolyFills() {
  global.window = new JSDOM(`
  <body>
    <script>
    document.body.appendChild(document.createElement("hr"));
    </script>
  </body>`).window;
  global.document = window.document;
  global.fetch = require("node-fetch");
  global.HTMLVideoElement = class HTMLVideoElement {};
}

function configureEndPoints() {
  addEndpoint("test", "http://localhost:8080/");
}

async function getPrediction(model, imageData, contentType, responseFunction) {
  const imageCanvas = canvas.createCanvas(64, 64);
  const canvasContext = imageCanvas.getContext("2d");

  const canvasImage = new canvas.Image();
  canvasImage.onload = async () => {
    canvasContext.drawImage(canvasImage, 0, 0, 64, 64);

    const prediction = await model.predict(imageCanvas);
    console.log(prediction);
    responseFunction(prediction);
  };

  canvasImage.onerror = (error) => {
    throw error;
  };

  canvasImage.src = `data:${contentType};base64,` + imageData;
}

init();
```

### Node.js でプログラムを実行
ここまで準備ができたら、app.js を実行します。 `node app.js` を実行すると以下のような出力がされて、先ほどローカルで実行したサーバーとは別のサーバーが待ち受け状態になります（こちらのサーバーのポート番号は、app.js のデフォルト設定を変更せずにいた場合 3000番になります）。
![app.jsの実行.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/4d4c19d3-ed2f-8507-55b0-4ce8933dd35b.jpeg)

### 画像をポストして結果を得る
あとは、先ほどの app.js を実行して立ち上がったサーバーのほうに、画像を POST で送信すれば OK です。[今回参照している GitHub のリポジトリのページ](https://github.com/drinkspiller/teachablemachine-node-example/)では、curl を利用した方法のコマンドの例が掲載されています。
![POSTの例.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/91392761-2c50-0b01-cd34-ff98abe6f8ee.jpeg)

自分は Mac で試しているので、上記の上側のコマンド例を利用しました。とりあえず動作確認ができれば良かったので、そのコマンド（※以下）をそのまま流用します。

以下のコマンドで、POST する画像のパスが `'@./sample_images/person-using-iphone-1194760.jpg' ` となっていますが、これは今回クローンしたリポジトリのファイル・フォルダ一式の中で、app.js と同じ階層にあるフォルダの中に置かれた画像の 1つを指定しているようです。

```bash
$ curl -X POST -H "Content-Type: image/jpeg" --data-binary '@./sample_images/person-using-iphone-1194760.jpg' http://localhost:3000/test
```

リポジトリのほうで具体的に見ていくと、 [teachablemachine-node-example/sample_images/](https://github.com/drinkspiller/teachablemachine-node-example/tree/master/sample_images) の中にある画像のうちの 1つです。
![sample_images.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/0702ad92-89e7-f017-468b-756ea335245f.jpeg)

これをそのまま流用するために、上記の curlコマンドを実行する場所は app.js と同じ階層にします。これで既に用意されている画像を流用して POST を実行できそうです。試してみた際の出力は、以下となりました。
![POSTした結果.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/f453349b-4a07-0026-05ed-4ffc85fd4fa4.jpeg)

上記のとおり、レスポンスとして推論の結果が JSON形式で得られました。
今回使ったモデルは「Class 1、Class 2」という、Teachable Machine のデフォルト設定のクラス名を使った 2つのクラスを用意していたのですが、出力ではそれらのラベル対する confidence の値が出力されたのが確認できたと思います。

## 終わりに
今回、Teachable Machine で出力した画像分類用の機械学習モデル（TensorFlow.js用）を Node.js で動かすことができました。

個人的には「もう少しだけ手順をシンプルにできないかな」と思う部分があるので、改善の余地がないかをもう少し見ていこうと思っています。