機械学習による推論をJavaScript上で簡単に実装できるml5.js

機械学習については解説記事をかじった程度の知識しかない僕でも、物体検出プログラムを簡単に作れたので紹介します。

# ml5.js
- [ml5.js](https://ml5js.org)

機械学習プラットフォームでお馴染みのTensorFlowをJavaScriptで実装した **TensorFlow.js** というライブラリがあります。

- [TensorFlow.js](https://www.tensorflow.org/js)

WebGLを利用したGPGPUで計算を行うことによりネイティブを肉薄するパフォーマンスを得た、ブラウザで動く機械学習ライブラリです。
そしてTensorFlow.jsを、専門知識がなくても使えるよう至れり尽くせりしてくれるラッパーがml5.jsというわけです。


# 使い方
学習済モデルやアルゴリズムを選択してデータを入力することで、推論結果を得ることができます。

学習済モデルはデータ量が大きいのでライブラリ本体とは別に存在しており、選択した時点で配信サーバーへAjaxする挙動になります。
巨大なものだと100MB近くにまで達するので、スマホで利用する際は注意したほうが良いかもしれません。

入力可能なデータは`HTMLMediaElement`や`ImageData`などがあります。
**MediaStream API** でカメラやマイクなどのデバイスからデータストリームを取得し`HTMLMediaElement`に紐付けることで、リアルタイム検出が可能です。

- [MediaStream API - MDN](https://developer.mozilla.org/ja/docs/Web/API/MediaStream)

# 利用可能なモデル
- [reference - ml5.js](https://learn.ml5js.org/docs/#/reference/index)

MobileNetやDarknetなどのニューラルネットワークモデルが使えます。
(ここら辺は付け焼き刃なので深い言及は避けておきます)

# 物体検出プログラム
早速、実際のプログラムを作ってみます。

物体検出には **YOLO** というアルゴリズムを使用し、映像はウェブカメラやスマホカメラから取得します。

- [YOLO (You Only Look Once)](https://pjreddie.com/darknet/yolo)

```html:HTML
<div class="overlay">
    <video id="capture" width="640" height="480"></video>
    <canvas id="detect" width="640" height="480"></canvas>
</div>
```
```css:CSS
.overlay{
    position: relative;
}
.overlay > *{
    display: block;
    position: absolute;
}
```
まずHTML/CSSです。

いたって単純で、カメラから取得したリアルタイム映像を表示させる`<video>`と、検出結果をオーバーレイで表示させる`<canvas>`から成ります。

```javascript:JavaScript
function getStream(video){
    return navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
            facingMode: "environment"
        }
    })
    .then((stream)=>{
        video.srcObject = stream;
        return video;
    });
}
```

最初にMediaStream APIを初期化します。

`navigator.mediaDevices.getUserMedia()`でデバイスのデータストリームを取得し、実際に表示させる`HTMLMediaElement`へ紐付けます。
初期化オプションは以下となります。

- `audio`: 音声を拾うか否か
- `video`: 映像関連の設定
- `facingMode`: リアカメラorフロントカメラ

`environment`がリアカメラで`user`がフロントカメラとなります。

なお、設定値が無効だった場合は有効値を順々に探索していきますが、明示した設定値のみに限定したい場合は`exact`プロパティ経由で設定します。

```javascript:JavaScript
function detectObject(video, canvas){
    const render = canvas.getContext("2d");

    render.beginPath();
    render.lineWidth = 2;
    render.strokeStyle = "#2fad09";

    render.font = "16px consolas";

    render.fillStyle = "#ffffff";
    render.fillRect(0, 0, canvas.width, canvas.height);
    render.fillStyle = "#000000";
    render.fillText("Model Loading...", 4, 14);
    render.fillStyle = "#2fad09";

    return ml5.YOLO({
        filterBoxesThreshold: 0.01,
        IOUThreshold: 0.2,
        classProbThreshold: 0.5
    }).ready
    .then((model)=>{
        render.clearRect(0, 0, canvas.width, canvas.height);
        video.play();

        return setInterval(()=>{
            if(!model.isPredicting){
                model.detect(video)
                .then((results)=>{
                    render.clearRect(0, 0, canvas.width, canvas.height);

                    for(const result of results){
                        render.strokeRect(result.x * canvas.width, result.y * canvas.height, result.w * canvas.width, result.h * canvas.height);
                        render.fillText(`${result.label}: ${Math.round(result.confidence * 100)} %`, result.x * canvas.width + 4, result.y * canvas.height + 14);
                    }
                });
            }
        }, 67);
    });
}
```
物体検出プログラム本体です。

`ml5.YOLO().ready`でYOLOモデルを取得し`model.detect()`で検出処理を実行します。
モデル取得時に閾値オプションを渡せます。

なお検出処理は入力された映像のスナップショットに対して実行されるので、物体をトラッキングするにはループさせる必要があります。
単純な実装であれば`setInterval()`でひたすら回せば良いのですが、検出処理はヘビータスクゆえ、もし前コンテキストが実行中のまま次ループへ入ってしまうと、リソースの奪い合いで時間経過とともにどんどん重くなってしまいます。

その対策として、モデルオブジェクトには "推論を実行中か" を`Boolean`で返す`isPredicting`というプロパティがあります。
これを使い、確実に処理が終了してから次のループに入るためのロック処理を入れました。
この例だと67ms毎(≒15fps)に検出処理中でないか確認してから実行しています。

結果は、検出された物体の "座標" と "ラベル" と "確率" が検出数の分だけ配列で返されます。

```javascript:JavaScript
getStream(document.getElementById("capture"))
.then(video => detectObject(video, document.getElementById("detect")))
.catch(error => alert(error.message));
```
最終的にこのようなかたちで実行します。

# 完成
作ったサンプルはGitHubPagesに上げておきました。

- [ml_recognition - GitHub Pages](https://dojyorin.github.io/experimental/ml_recognition.html)

![Screenshot_20200115-155718_Chrome.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253070/62034073-cb3a-b5d2-3a75-320d0dcdc44f.jpeg)

この通り、スマホでも問題なく実行できます。
が、前述の通りヘビータスクなので結構重くなります。

# おわりに
「機械学習で色々やってみたいけど、そもそも入口が複雑すぎてお手上げ...」
みたいな敷居の高さを取り払ってくれる、とても良いライブラリだと思いました。

もちろん、理解を深めるにはより低レベルで高度な知識が必要になると思いますが、そのきっかけ作りには丁度良いなと感じました。