Keras.jsで自作モデルに画像を入力して判別結果を出力するアプリを作る

前回、[Kerasで「笑っている犬」と「怒っている犬」を判別する機械学習モデルを作る](https://qiita.com/ariera/items/545d48c961170a784075)で作ったモデルを、犬好き、動物好きの友達に見せようと思ったのだが、プログラミングが全く分からない人が多いので、Qiitaの投稿を見せられても困るだろうと思い、Webアプリ化しようと考えた。

個人で使っているレンタルサーバーでは、pythonは入っているがkerasはデフォルトでは入っていないので、自力でインストールするのが面倒な気がして、クライアント側で動かすKeras.jsを選択した。

今回作成したプログラムは[こちら](https://pochi.qamaru.com/keras/smile-angry-dog.html)からテストできる。HTML中にJavascriptを埋め込んであるので、ブラウザでソースを表示させれば全コードも見ることができる。(Chrome、Edgeで動作確認した。IEでは動かない。)

### モデルをKeras.js用に変換する

Keras.jsの使い方を書いたサイトは日本語でも英語でもいくつかあったのだが、写経してどうにかなる感じではなかったので、[Keras.jsのドキュメント](https://transcranial.github.io/keras-js-docs/)を見ながら試行錯誤した。

まず、[Keras.jsのサイト](https://github.com/transcranial/keras-js)から、
python/encoder.py
python/model_pb2.py
の2つのファイルをダウンロードする。(model_pb2.pyはencoder.pyから参照されるファイル)

そして、[前回](https://qiita.com/ariera/items/545d48c961170a784075) ModelCheckpointコールバックで保存した HDF5形式(.h5)のモデルの中で、最も精度が高かったVGG16のモデル、model_2class120_transfer_18_0.850.h5 を選択して、コンソールから

```
> python encoder.py -q ../models/model_2class120_transfer_18_0.850.h5
```
とすると、モデルと同じフォルダに、model_2class120_transfer_18_0.850.bin というファイルができる。このbinファイルをKeras.jsに読み込ませる。

※-qオプションは、[Keras.jsのドキュメント](https://transcranial.github.io/keras-js-docs/)によると、32bit float ->8bit uint に変換してくれるもの。-qオプションを付けると、つけない時と比べて、出力されるbinファイルのサイズが約4分の1になった。

モデルを使うときに、-qオプションありとなしのモデルで比較してみると、予測結果の確率(probability)の細かい数字は違っていたが、2クラスの分類結果が変わるほどではなかったので、軽い方を使うことにした。

※[Keras.jsのドキュメント](https://transcranial.github.io/keras-js-docs/)は、ModelCheckpointコールバックを使うときは、save_weights_onlyフラグをTrueにしないように、と書いてある。デフォルトはFalseで、前回はsave_weights_onlyフラグの指定はしなかったので大丈夫なはず。

### Error: [Model] error loading weights.で止まる！モデルの読み込みでハマった！！

encoder.pyでは特にエラーは出なかったのだが、この後、入力データを作って、モデルを使う側のJavascriptコードを全部書いてからの話だが、モデルを使って予測しようとすると、
Error: [Model] error loading weights.
とか
Error: [Model] Model configuration does not contain any layers.
というエラーが出て、先に進めなくなった。

Error: [Model] Model configuration does not contain any layers.
のほうは、binファイルの名前やパスを間違えたときと、Webサーバー経由ではなくローカルでHTMLファイルを直接ブラウザに読み込ませたときに出た。
そもそもモデルのファイルを読み込めていないということだろうか。

Error: [Model] error loading weights.
こちらのほうはネットで調べてもあまり情報がなく、ドツボにハマったのだが、[Keras.jsのサイト](https://github.com/transcranial/keras-js)を見ていると、
Library version compatibility: Keras 2.1.2
と書いてあるのに気が付いた。(2019年4月30日現在)

念のため、コンソールから

```
> conda list keras
```
でKerasのバージョンを調べてみると、2.2.4だった。Keras.jsとバージョンを合わせるため、

```
>conda install keras=2.1.2
```
でKerasをダウングレード。そのときにnumpyが勝手にアップグレードされておかしくなったので、

```
>conda install numpy=1.14.2
```
で元に戻した。
以下のように、前回保存したモデルを読み込んで、Keras2.1.2で読み直せば良いと思ったのだが、

```python:
from keras.models import load_model
model = load_model('models/model_2class120_transfer_18_0.850.h5')
model.save('models/new_model.h5')
```
load_modelのところで、KeyErrorが出て読み込めない。新しいバージョンで作ったモデルは古いバージョンでは読み込めないらしい。

ということで、[前回](https://qiita.com/ariera/items/545d48c961170a784075)と同じコードで、シンプルCNNで最初から学習と、VGG16の転移学習の2種類の学習をKeras2.1.2でやり直すハメになった。

ちなみにkeras.preprocessing.imageのload_imgの引数が、Keras2.2.4ではcolor_mode = "rgb" / "grayscale" だったのを、Keras2.1.2では grayscale = False / True に変更しないとエラーになった。


シンプルCNNとVGG16で作り直したモデルを、encoder.pyでbinファイルに変換して読み込ませてみると、VGG16のほうはやはり
Error: [Model] error loading weights.
のエラーが出る。ドキュメントにはSequential()で作ったモデルもModel()で作ったモデルも読み込めると書いてあるのだが…
pythonで model.summary() で層構造を表示させてみると、最後の層がDenseではなく、Sequentialになっている。(自分で定義しなおした全結合層の中身が展開されず、Sequentialでひとまとめになっている) これが原因だろうか？
(VGG16の転移学習モデルも読み込めるようになったので、最後に追記する。2019.5.25)

シンプルCNNのほうは、エラーが出ず、うまく動作したので、少し精度は落ちるが、こちらで進めることにする。

### 入力データをKeras.js用に変換する

入力データは、Float32Arrayの形で渡す必要がある。元のモデルの入力データは(224, 224, 3)なので、これを224x224x3 = 150528個の要素を持つ配列に変換する。

今回は、ユーザーが指定したローカルの画像を入力データに変換したい。[Keras.jsのデモサイト](https://transcranial.github.io/keras-js/#/)では、canvasに手書きするか、ネットワークから読み込んだ画像を貼り付けて、canvasから読み込んだデータを入力データに変換していたので、これに倣って、ローカルの画像をcanvasに貼り付けて変換する。

```html:HTML
<input type="file" id="img_file" onChange="imgSelect(event);" accept="image/png,image/jpeg"><br>
<canvas id="file_canvas" width="224" height="224"></canvas>
```

```javascript:JavaScript
var img_rows = 224;
var img_cols = 224;

// 画像を選択したときに呼び出される関数
function imgSelect(event){
    var file = event.target.files[0]; // 選択されたファイルを1つだけ取得

    // File APIとImageオブジェクト作成
    var reader = new FileReader();
    var image = new Image();

    // キャンバスのコンテキスト取得
    var canvas = document.getElementById("file_canvas");
    var context = canvas.getContext("2d");
    context.clearRect(0, 0, img_rows, img_cols); // canvasをクリア

    // ファイル読み込み
    reader.readAsDataURL(file);

    // ファイルが読み込めたらcanvasに書き込む(img_rowsximg_colsにリサイズして書き込む)
    reader.onload = function(){
        // canvasに画像を設定
        image.src = reader.result;
        image.onload = function() {
            //canvasに画像を書き込む
            context.drawImage(image, 0, 0, img_rows, img_cols);

            // 画像データをFloat32Arrayに変換
            canvas_data = context.getImageData(0, 0, canvas.width, canvas.height).data;
            data = new Float32Array(img_rows * img_cols * 3);
            for(i=0; i<img_rows*img_cols; i++) { // RGBA -> RGB
                for(j=0; j<3; j++) {
                    data[i*3+j] = canvas_data[i*4+j] / 255;
                }
            }

            // ***************************************
            // ここに、モデルを使って予測するコードを書く
            // ***************************************
        }
    }
}
```
canvasからgetImageDataで取得した画像は、1画素あたりRGBA(RGB+透明度)の4つのデータからなるので、RGBのデータのみを抽出するところがミソ。
canvasのデータは、画素0のR、画素0のG、画素0のB、画素0のA、画素1のR、画素1のG、画素1のB、画素1のA.....という順番で並んでいるようだが、入力データは画素0のR、画素0のG、画素0のB、画素1のR、画素1のG、画素1のB.....とするのか、画素0のR、画素1のR.....画素0のG、画素1のG....画素0のB、画素1のB....とするのか、よくわからなかったので両方試してみたところ、前者のほうが予測結果がまともだったので、多分前者だと思われる。(間違っているかも？)
img_rows*img_colsの並べ方も、とりあえず、canvasのデータ並びのままにしてみたが、正しいという確証はない。

### モデルの読み込み

最初に、HTML側で、Keras.jsのライブラリを読み込んでおく。[Keras.jsのドキュメント](https://transcranial.github.io/keras-js-docs/)に書いてある通り、unpkg.comから読み込むようにした。

```html:HTML
<script src="https://unpkg.com/keras-js"></script>
```

自作モデルの読み込みは、Javascriptの最初の方(imgSelect関数の外)で行う。KerasJS.Modelのオブジェクトを作るときに、モデル(binファイル)のパスと、GPUを使うかどうかを設定する。
今回は、友達のパソコンにGPUが搭載されているか分からないので、falseとした。

着目箇所の可視化を行うときには、ここで、visualizationsオプションの設定をするらしいが、ドキュメントがまだなく、「デモサイトを見ろ」と書いてあったので、今回はやめておき、次回の課題とする。

```javascript:JavaScript
const model = new KerasJS.Model({
	filepath: './model_2class120_keras212_21_0.763.bin',
	gpu: false
});
```
※余談だが、gpuをtrueにした状態で、私のPC(GPUはオンボードのみ)で試したところ、Edgeでは特に問題なく動いたが、Chromeではどの画像を入力しても、モデルを変えても、HTMLを再読み込みしても、キャッシュを消しても、Developer ToolでDisable cacheにチェックを入れても、何をしても同じ結果しか返さなくなった。gpuをfalseにしたら正常に動くようになった。

### モデルを使った予測

予測を行うコードは、imgSelect関数内の、入力データを作成した後に、以下のように書いた。記法は[Keras.jsのドキュメント](https://transcranial.github.io/keras-js-docs/)のコピペで、よく意味が分かっていない部分もある。

```javascript:JavaScript
model
    .ready()
    .then(() => {
        // 入力データをセットして予測
        const inputData = {
            "input": data // Sequentialモデルの場合はinput、Modelの場合は入力層の名前
        }
        return model.predict(inputData);
    })
    .then(outputData => {
        // 予測結果を出力
        output = outputData["output"]; // Sequentialモデルの場合はoutput、Modelの場合は出力層の名前
        var result = "";
        if(output[0] <= output[1]) {
            result = "「笑」";
        } else {
            result = "「怒」";
        }
        result = result + "<br><br>　　　「笑っている犬」の確率：" + output[1];
        result = result + "<br>　　　「怒っている犬」の確率：" + output[0];
        document.getElementById("result").innerHTML = "　　　【判別結果】" + result;
        
    })
    .catch(err => {
        // エラーを画面表示
        alert(err);
    })
```
結果を書き込む場所は、HTMLで以下のように定義した。

```html:HTML
<span id="result"></span>
```

全コードは[こちら](https://pochi.qamaru.com/keras/smile-angry-dog.html)から、ブラウザでソースを表示させると見ることができる。(Chrome, Edgeで動作確認した。File APIを使っているため、IEでは動かない。)

なぜか、EdgeとChromeで、同じ画像を読み込ませても、予測結果の確率(probability)の数字が微妙に違う。同じブラウザだと、同じ画像に対しては同じ数値が出るので、どこかで乱数を使っているわけでもなさそう。canvasに貼り付けるときに、画像サイズ変換を行っているので、変換処理がブラウザによって違っているのだろうか？

### [追記] 転移学習モデルを読み込む

VGG16の転移学習のモデルを作るときに、全結合層をSequential() を使わないで定義すると、Keras.jsから読み込めるようになった。

```python:VGG16転移学習のモデル定義
from keras.applications.vgg16 import VGG16
from keras.layers import Input

# VGG16のモデルを読み込む(最後の全結合層を除いたモデルを読み込むのでinclude_top=False)
input_tensor = Input(shape=(img_rows, img_cols, 3))
base_model_v = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

# 全結合層を定義
from keras.models import Model
from keras.layers import Dense, Dropout, Flatten
x = base_model_v.output
x = Flatten(input_shape=base_model_v.output_shape[1:])(x)
x = Dense(512, activation='relu')(x)
x = Activation("relu")(x)
x = Dropout(0.5)(x)
predictions = Dense(nb_classes, activation='sigmoid')(x)
model = Model(inputs=base_model_v.input, outputs=predictions)

# base_model_vの各層の重みを固定する
for layer in base_model_v.layers:
    layer.trainable = False
```
(2019.5.26追記)

### [追記] import Adam でエラーになる

モデルを作るときに、学習率を変えて精度を試そうと思い、import Adamしてコンパイルし、Keras.jsで読み込もうとしたところ、
[Model] error loading weights.
のエラーになった。

```python:モデルコンパイル、エラーになる例
from keras.optimizers import Adam
model.compile(loss='binary_crossentropy',
              optimizer=Adam( 学習率等のパラメータ設定をここに書く ), 
              metrics=['accuracy'])  
```
元に戻して、再度学習しなおしたら読み込めるようになった。

```python:モデルコンパイル、エラーにならない例
model.compile(loss='binary_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])  
```

色々試してみると、model.compileの引数がoptimizer='adam'になっていても、import Adamしているだけで読み込めなくなるようだ。Jupyter Notebookで試行錯誤しているときは、一度全部リスタートして、import Adamしていない状態で学習させる必要があった。
(2019.5.26追記)