WEB

TensorFlow.js 入門

WEB
この記事は約17分で読めます。

背景

以前、TeachableMachine を使って、顔認識とか、これがグーでパーでチョキで…みたいな遊びをしていたのですが、実用的に扱うにはどうするんだろ、と思ったので TensorFlow.js を学んで理解しようと思います。

TensorFlow.jsとは

今回は、JavaScript 実行環境があればどこでも使用可能なオープンソースの機械学習ライブラリ。もともとは、Python ライブラリである TensorFlow に基づいている。JavaScript エコシステム用に、開発者エクスペリエンスと API のセットを再作成することを目的としている。

使用可能なプラットフォーム

・Web ブラウザのクライアントサイド
・Node.js を使用したラズパイなどのIoTデバイス
・Electron アプリ
・ReactNative アプリ

メリット

プライバシー:サードパーティライブラリ等にデータを送信→解析する必要があったものが、クライアントだけでデータのトレーニングと分類を行うことで、情報漏洩の防止に寄与できる
速度:サーバーにデータを送る必要がないことで、データを分類する行為を高速化できる。
費用:サーバーが必要ない=クライアントサイドのモデルファイルをホストするための CDN だけ用意すれば完結する。サーバー代が浮く。

サーバー側の機能

サーバーは必要ないのだが、Node.js を使用することで、サーバーサイドで行えることがある。
具体的には、以下の機能。
1: NVIDIA CUDA サポート
サーバー側でのグラフィックカードを高速化する。

CUDA とは: リアルタイムグラフィックスの中でもゲーム用途に特化して開発を進めていたのが NVIDIA, AMD であった。プログラマブルシェーダー( DirectX8 以降使用された、頂点情報やピクセル描画方法をプログラマが独自にカスタマイズできる技術)の発展により、その高処理性能をグラフィックス以外でも活用できるように開発した技術。

グラフィックカードとは: ディスプレイモニターに映像を映し出すためのチップ( GPU )が搭載されたボードのこと

2: モデルサイズ
ブラウザタブでの実行の場合、メモリ使用量の制限などが起きるが、その制限解除。

3: IoT
ラズパイなどのサポート。

4: 速度
ケーススタディ: How Hugging Face achieved a 2x performance boost for Question Answering with DistilBERT in Node.js

事前トレーニングモデルの提供

ここからは実際に TensorFlow.js を用いて開発をしていくが
(0 から画像認識モデルを構築していくのは辛い...)
ということで、Google が用意している事前トレーニングモデルを使って進めていく。(coco-ssd)
https://github.com/tensorflow/tfjs-models/blob/master/coco-ssd/src/classes.ts
上記URLでは、既にトレーニングされた機械学習モデルが共有されている。これにより、自分で収集する時間とコストの削減につながる。トレーニング済みのものなので、学習モデルと同じオブジェクトを扱うのであれば、自作のものより精度が高くなる確率が高いだろう。(多分)

セットアップ

Google が Codepen や、Glitch で既に開発環境を用意しているのだが、筆者はもっと楽をしたいため、自前の index.html だけで進めていく。index.html に数行のスタイルと70行程の JavaScript を書いていくので、これが嫌な方は、上記の Codepen / Glitch を使用していただいて構わない。

以下を初期状態として進めていく。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>TensorFlow Tutorial</title>
  <style>
    .skyNet {position: relative;width: 100%;cursor: pointer;}
    .skyNet p {position: absolute;padding: 5px;backgr![スクリーンショット 2021-05-24 21.45.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1450823/0588324e-cc7f-e9f1-437d-becf696a73b1.png)
ound-color: "#7C7C7C";color: #fff;border: 1px solid #fff;z-index: 1;font-size: 1rem;}
    .innerSquare {border: 1px solid #fff;z-index: 1;position: absolute;}
  </style>
</head>
<body>
  <div class="skyNet">
    <img src="https://images.pexels.com/photos/2896853/pexels-photo-2896853.jpeg?auto=compress&cs=tinysrgb&h=750&w=1260" width="100%" crossorigin="anonymous"/>
  </div>
  <p>Accurate Score:
    <span id="score"></span>
  </p>
</body>
</html>

最終目標

先に最終目標を出すとこんな感じ。
1: 画像クリック時、「人」がいることを TensorFlow(coco-ssd) が認識→その精度を Accurate Score: XXXX として反映させる。
2: どこからどこまでが「X」(今回なら「人」)なのかを認識するための bbox というモノがあるため、そちらも使用して境界線を border で識別する

TensorFlow と coco-ssd の導入

glitch と codepen を使っている方のソースには既に入っているが、まず最初に TensorFlow.js とトレーニング済モデルの coco-ssd を導入する。

index.html

  <!--途中から-->
    <p>Accurate Score:
      <span id="score"></span>
    </p>

    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js" type="text/javascript"></script>

    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd"></script>
    <script>
      let modelHasLoaded = false;
      let model = undefined;

      (function cocoLoading() {
        console.log('loading...');
        cocoSsd.load().then(function (loadedModel) {
          model = loadedModel;
          modelHasLoaded = true;
          console.log(loadedModel);
          console.log('finished');
        });
      })()
    </script>
    </body>
</html>

それぞれの導入と、試運転も兼ねて即時関数で coco-ssd の挙動を確認する。画面に戻り、コンソールを開いて

console.

loading...
e
finished

となっていれば成功。

 e > _proto_ > detect() の第一引数にターゲットとなる img 要素を入れてあげると、あとは勝手にTensorFlow側で画像解析をしてくれる。

なので、次に画像をクリックした時に detect() が走るよう実装を進める。

handleClick() の実装

画像をクリックする handleClick() を作る。
最初に DOM 側をいじって、eventListener で handleClick() が走るようにする。

index.html

  <script>
    let modelHasLoaded = false;
    let model = undefined;

    (function cocoLoading() {
      console.log('loading...');
      cocoSsd.load().then(function (loadedModel) {
        model = loadedModel;
        modelHasLoaded = true;
        console.log(loadedModel);
        console.log('finished');
      });
    })()

  const holderOfImage = document.getElementsByClassName('skyNet')[0].addEventListener('click', handleClick);

  </script>

これで、画像をクリックすると、その event 位置を取得できるようになった。

次に、detect() に、実際に取得できた event の target を渡して、TensorFlow を走らせるようにする。

index.html

  <script>
    let modelHasLoaded = false;
    let model = undefined;

    (function cocoLoading() {
      console.log('loading...');
      cocoSsd.load().then(function (loadedModel) {
        model = loadedModel;
        modelHasLoaded = true;
        console.log(loadedModel);
        console.log('finished');
      });
    })()

    const holderOfImage = document.getElementsByClassName('skyNet')[0].addEventListener('click', handleClick);

    function handleClick(event) {
      console.log(event)
      // ここでは、coco-ssdがload失敗していた時のために、早期returnをしています。
      // cocoSsdがloadされていなくて、この文が無いと、Uncaught TypeError: Cannot read property 'detect' of undefined at HTMLDivElement.handleClick エラーが走ります
      if (!modelHasLoaded) {
        return;
      }

      // event.target = imgをpredictions関数として動かす
      model.detect(event.target).then(function (predictions) {
        console.log(predictions) 
      })
    }

  </script>

画面に戻り、coco-ssd のロード完了後画像をクリックすると、console では以下のような状態になる。

スクリーンショット 2021-05-24 22.17.43.png

この時点で、[{…}]内の0番目に
bbox: left, top, width, heightの順番で判別できたオブジェクトの位置を表示している。(style要素に使える)
class: 判別結果
score: 精度
が出力されていることがわかる。

あとは、bbox と score を用いて、スタイリングをしてあげれば完成。

最終形

index.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>TensorFlow Tutorial</title>
    <style>
      .skyNet {position: relative;width: 100%;cursor: pointer;}
      .skyNet p {position: absolute;padding: 5px;background-color: "#7C7C7C";color: #fff;border: 1px solid #fff;z-index: 1;font-size: 1rem;}
      .innerSquare {border: 1px solid #fff;z-index: 1;position: absolute;}
    </style>
    </head>
    <body>
        <div class="skyNet">
            <img src="https://images.pexels.com/photos/2896853/pexels-photo-2896853.jpeg?auto=compress&cs=tinysrgb&h=750&w=1260" width="100%" crossorigin="anonymous"/>
        </div>
    <p>Accurate Score:
      <span id="score"></span>
    </p>

    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js" type="text/javascript"></script>

    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd"></script>

    <script>
      let modelHasLoaded = false;
      let model = undefined;

      (function cocoLoading() {
        console.log('loading...');
        cocoSsd.load().then(function (loadedModel) {
          model = loadedModel;
          modelHasLoaded = true;
          console.log(loadedModel);
          console.log('finished');
        });
      })()

      const holderOfImage = document.getElementsByClassName('skyNet')[0].addEventListener('click', handleClick);

      function handleClick(event) {
        console.log(event)
        // ここでは、coco-ssdがload失敗していた時のために、早期returnをしています。
        // cocoSsdがloadされていなくて、この文が無いと、Uncaught TypeError: Cannot read property 'detect' of undefined
        // at HTMLDivElement.handleClick エラーが走ります
        if (!modelHasLoaded) {
          return;
        }

        // event.target = imgをpredictions関数として動かす
        model.detect(event.target).then(function (predictions) {
          console.log(predictions) //[{...}]中身は {bbox: Array(4), class:"laptop", score: 0.98....}
          for (let x = 0; x < predictions.length; x++) {
            const scoreElement = document.getElementById('score')
            const p = document.createElement('p');
            p.innerText =
              predictions[x].class +
              ' - with ' +
              Math.round(parseFloat(predictions[x].score) * 100) +
              '% confidence.';
            p.style =
              'margin-left: ' +
              predictions[x].bbox[0] +
              'px; margin-top: ' +
              (predictions[x].bbox[1] - 10) +
              'px; width: ' +
              (predictions[x].bbox[2] - 10) +
              'px; top: 0; left: 0;';

            const innerSquare = document.createElement('div');
            innerSquare.setAttribute('class', 'innerSquare');
            innerSquare.style =
              'left: ' +
              predictions[x].bbox[0] +
              'px; top: ' +
              predictions[x].bbox[1] +
              'px; width: ' +
              predictions[x].bbox[2] +
              'px; height: ' +
              predictions[x].bbox[3] +
              'px;';

            event.target.parentNode.appendChild(innerSquare);
            event.target.parentNode.appendChild(p);

            scoreElement.innerText = predictions[0].score
          }
        });
      }
    </script>
  </body>
</html>

参考

https://github.com/tensorflow/tfjs-models/blob/master/coco-ssd/src/classes.ts
https://js.tensorflow.org/api/latest/
https://www.youtube.com/watch?v=9PeFDHVM66Q