【TensorFlow.js】ブラウザで手話数字認識【前編：Python編】

ブラウザのGPUを利用してディープラーニングを実行できるライブラリ`Tensorflow.js`を利用して手話数字を認識させてみました。こんな感じ。

![screenshot_1.gif](https://qiita-image-store.s3.amazonaws.com/0/47128/4f828801-da52-2d7f-2db1-96242f1330f6.gif)

デモ: [Webcam Sign Language Digit Classification with TensorFlow.js](https://webcam-sign-language-digit.herokuapp.com/)

学習済みモデルをブラウザにアップロードすることで、ウェブカメラから手話画像を識別することができます。デモは`Heroku`の無料枠で公開してみました。

![Screenshot.png](https://qiita-image-store.s3.amazonaws.com/0/47128/5e07788d-28f9-9597-b9b1-3625b451b53a.png)

手話数字のデータセットは[Sign Language Digits Dataset - Kaggle](https://www.kaggle.com/ardamavi/sign-language-digits-dataset)より、0~9までの手話画像2,000枚（各200枚）を使いVGG16モデルを使い学習させています。

学習モデル作成はPython3+Kerasで行い、JavaScriptで読み込み可能な形式にコンバート（変換）します。その学習モデルを使いTensorFlow.jsとブラウザで推論をおこなう仕組みです。

今回は「前編：Python編」でデータセットを利用した学習モデルの作成・変換を、「後編：Node.js編」で学習済モデルを利用したブラウザでの推論方法をまとめます。

## 環境

- GoogleColaboratory（python3/tensorflow v1.7.0）
- Node.js v8.11.1

画像枚数はそれ程多くないのですが、６層の畳込みニューラルネットワーク（VGG16モデル）で学習させるため、TensorFlow-GPUが利用できるPythonの環境が望ましいです。

## 前編：Python編

今回は無料でクラウドGPUを使用できるGoogleColaboratoryを使います。また、モデル作成に関してはKerasの作者Francois Cholletさんの著書[^1]を参考にしました。

ノートブック: [4_webcam_sign_language_digits_classification - GitHub](https://github.com/PonDad/keras_js_examples/blob/master/4_webcam_sign_language_digits_classification/python3/4_webcam_sign_language_digits_classification.ipynb)

データセット: [Sign-Language-Digits-Dataset - GitHub](https://github.com/ardamavi/Sign-Language-Digits-Dataset)

## GoogleColaboratory

![1.png](https://qiita-image-store.s3.amazonaws.com/0/47128/b96eaf34-d9f7-80f8-731e-3ba27bce1e03.png)

[GoogleColaboratory - Google](https://colab.research.google.com/)

Colaboratoryは、機械学習の教育や研究の促進を目的に公開されているGoogle研究プロジェクトで、クラウドで実行されるJupyterノートブックを利用することができます。

機械学習に必要となるPythonライブラリのインストールが不要であり、また層の厚いディープラーニングで必要となるGPUを利用した学習をクラウドで行うことができます。なんと驚くこと無料です。

## Colaboratoryへの画像アップロード

まず大量の画像をアップロードする方法まとめます。ここではColaboratoryに直接画像をアップロードする方法を紹介します。

まずは「上メニュー>ランタイムのタイプを変更」で「ハードウェアアクセラレータ>GPU」に設定し、「接続」をクリックします。

![2.png](https://qiita-image-store.s3.amazonaws.com/0/47128/622151eb-1958-813a-54c4-3cf06251132e.png)

分かりづらいのですが、画面左にある矢印キーをクリックすると左メニューが現れます。

![3.png](https://qiita-image-store.s3.amazonaws.com/0/47128/2ccc9542-65b5-2b77-0bd7-6cfa07b81f63.png)

ランタイム接続するとColaboratoryのファイルが参照可能になります。「左メニュー>ファイル>アップロード」より画像をアップロードすることが可能になります。

![4.png](https://qiita-image-store.s3.amazonaws.com/0/47128/93bca5d4-8783-e96c-6515-e89131e6dd05.png)

100x100ピクセルの画像約2,000枚で約30MBになりました。zipファイルに圧縮後アップロードしてください。アップロード後「更新」クリックでファイルを確認することができます。

![5.png](https://qiita-image-store.s3.amazonaws.com/0/47128/110efa4c-86d2-bf3a-ea02-2b7fb65e2ec3.png)

Colaboratoryでlinuxコマンドを使用する際は`!`を頭につけて実行します。

```bash
!unzip Dataset.zip
```
これでzipファイルを解凍することができました。

## 画像の前処理

ディープラーニングで画像を学習可能にするには、RGB画素を計算可能なNumPy形式のテンソルに変換しなければなりません。Kerasでは便利なメソッド`ImageDataGenerator`が用意されており、フォルダ分けされた訓練画像セット・検証デーセットを自動的にNumPy形式のデータに変換し、学習器で読み込むことができます。

[ImageDataGenerator - Keras](https://keras.io/ja/preprocessing/image/)

そこで画像データセットをクラス毎に訓練データ・検証データに振り分ける必要があります。（学習後のテストデータは必須ではありませんが推論のテスト用に前処理段階で一緒に作成しておきましょう）

```bash
hand_sign_digit_data
├── test
│   ├── 0
│   ├── 1
│   ├── 2
│   ├── 3
│   ├── 4
│   ├── 5
│   ├── 6
│   ├── 7
│   ├── 8
│   └── 9
├── train
│   ├── 0
│   ├── 1
│   ├── 2
│   ├── 3
│   ├── 4
│   ├── 5
│   ├── 6
│   ├── 7
│   ├── 8
│   └── 9
└── validation
    ├── 0
    ├── 1
    ├── 2
    ├── 3
    ├── 4
    ├── 5
    ├── 6
    ├── 7
    ├── 8
    └── 9
```
このような画像の階層分けが必要になります。振り分けのスクリプトの例は以下のようになります。

```python
import os, shutil

original_dataset_dir = 'Dataset'
folder_name = ["0","1","2","3","4","5","6","7","8","9"]

for j in range(0, len(folder_name)):
    print (folder_name[j])

    files = os.listdir(original_dataset_dir + "/" + folder_name[j] + "/")

    for i in range(0, len(files)):
        print (files[i])
        root, extension = os.path.splitext(files[i])
        if files[i] == ".DS_Store":
            print("This is no image.")
        elif extension == ".png" or ".jpeg" or ".jpg":
            shutil.move(original_dataset_dir + "/" + folder_name[j] + "/" + files[i], original_dataset_dir + "/" + folder_name[j] + "/" + folder_name[j] + "_" + str(i + 1) + ".jpg")

print("Rename Done.")
```

まずは`shutil`モジュールを使いフォルダ内のバラバラな画像名`IMG_1118.JPG`などをフォルダごとの連番`0_153.jpg`などの様な名前に変更します。ここでは10クラスですが`folder_name`の配列を変更すれば何クラスでも自動に変更できます。

```python
mport os, shutil

original_dataset_dir = 'Dataset'
base_dir = 'hand_sign_digit_data'
os.mkdir(base_dir)

train_dir = os.path.join(base_dir, 'train')
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation')
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test')
os.mkdir(test_dir)

folder_name = ["0","1","2","3","4","5","6","7","8","9"]

for j in range(0, len(folder_name)):
    os.mkdir(os.path.join(train_dir, folder_name[j]))
    os.mkdir(os.path.join(validation_dir, folder_name[j]))
    os.mkdir(os.path.join(test_dir, folder_name[j]))

    fnames = [folder_name[j] + '_{}.jpg'.format(i) for i in range(1,101)]
    for fname in fnames:
        src = os.path.join(original_dataset_dir, folder_name[j], fname)
        dst = os.path.join(train_dir, folder_name[j], fname)
        shutil.copyfile(src, dst)

    fnames = [folder_name[j] +'_{}.jpg'.format(i) for i in range(101, 151)]
    for fname in fnames:
        src = os.path.join(original_dataset_dir, folder_name[j], fname)
        dst = os.path.join(validation_dir, folder_name[j], fname)
        shutil.copyfile(src, dst)

    fnames = [folder_name[j] +'_{}.jpg'.format(i) for i in range(151, 201)]
    for fname in fnames:
        src = os.path.join(original_dataset_dir, folder_name[j], fname)
        dst = os.path.join(test_dir, folder_name[j], fname)
        shutil.copyfile(src, dst)

print("Data generate Done.")
```
訓練データ・検証データ・テストデータの枚数を指定してフォルダごとに振り分けします。ここではデータセットを0〜9それぞれ訓練データ100枚、検証データ50枚、テストデータ50枚に振り分けしました。

ここでは10クラスですが`folder_name`の配列を変更すれば何クラスでも自動でフォルダ分けできます。また枚数の調整はループ処理`range()`の数字を変更してください。

![6.png](https://qiita-image-store.s3.amazonaws.com/0/47128/02aa277d-3e3b-f6dd-20e6-de24a6296285.png)

これで画像の前処理ができました。

## VGG16モデルでの学習

学習モデルはKerasのチュートリアルにあるVGG16モデルをそのまま使いました。GPUを使い通常の学習をおこないます。

ひとつColaboratoryでKeras.jsモデルを作成するときに注意点があります。Colaboratoryには機械学習に必要なライブラリは一通りインストールされていますが、tensorflowjsライブラリは手動でインストールしなければなりません。さらにTensorFlov 1.7.0が必要になります。

```bash
!pip3 install tensorflow==1.7.0
!pip3 install keras
!pip3 install tensorflowjs
```
この様にインストールしてください。

```python
import keras
from keras import layers
from keras import models
from keras import optimizers
from keras.preprocessing.image import ImageDataGenerator
import matplotlib.pyplot as plt
import tensorflowjs as tfjs

model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu',
                        input_shape=(100, 100, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))

model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['acc'])

classes = ['zero', 'one', 'two', 'three', 'four',
           'five', 'seven', 'eight', 'nine']

train_dir = 'hand_sign_digit_data/train'
validation_dir = 'hand_sign_digit_data/validation'

train_datagen = ImageDataGenerator(
    rescale=1./255,
    rotation_range=40,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,)

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(
        train_dir,
        target_size=(100, 100),
        batch_size=32,
        class_mode='categorical')

validation_generator = test_datagen.flow_from_directory(
        validation_dir,
        target_size=(100, 100),
        batch_size=32,
        class_mode='categorical')

history = model.fit_generator(
      train_generator,
      steps_per_epoch=50,
      epochs=70,
      validation_data=validation_generator,
      validation_steps=25)

model.save('sign_language_vgg16_1.h5')
```

今回は他クラス分類のため、`ImageDataGenerator`の`class_mode`を`'categorical'`で指定します。拡張機能を使い訓練データ1,000枚から32バッチ毎に水増しした新たな画像を生成するように指定しました。

全結合層は`layers.Dense(10, activation='softmax')`とし、10クラスを活性化関数`softmax`で分類します。

モデルの損失関数`loss`は`'categorical_crossentropy'`、オプティマイザ`optimizer`は`'adam'`としました。

モデル訓練は`steps_per_epoch=50`（バッジサイズ32で1エポック当たり50枚の訓練画像生成）、`validation_steps=25`（バッジサイズ32で1エポック当たり25枚の検証画像）と指定し、`epochs=70`で学習します。

モデルの推論用に `sign_language_vgg16_1.h5` をダウンロードします。（これはTensolFrow.jsには使いません）

![7.png](https://qiita-image-store.s3.amazonaws.com/0/47128/55f7bd58-e013-ee5e-0a2d-bcd2aa4b1bf8.png)

訓練データ、検証データの推移をグラフで確認します。

```python
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()
```

![download.png](https://qiita-image-store.s3.amazonaws.com/0/47128/7e4f3234-a474-5f88-faa1-40dec17c9f58.png)

![download-1.png](https://qiita-image-store.s3.amazonaws.com/0/47128/6a292177-9c79-0983-efb5-6d995ecc65ea.png)

検証データのロス率が上がっていないので過学習していないことが分かります。

## TensorFlow.js用コンバーターモデルの作成

ようやく本番です。TensorFlow.js用の変換モデルを作成します。`tfjs.converters.save_keras_model()`というKerasモデルを自動的に変換するメソッドを使います。

```python
save_path = 'sign_language_vgg16'
tfjs.converters.save_keras_model(model, save_path)
print("[INFO] saved tf.js vgg16 model to disk..")
```
変換されたモデルは以下のようにフォルダで生成されます。

```bash
.sign_language_vgg16
├── group1-shard1of2
├── group1-shard2of2
└── model.json
```
ひとつ注意が必要です`json`ファイルだけでなく、同時に生成された重みファイル（`group1-shard1of2`など）と合わせて使用するのでフォルダごと保存してください。

Colaboratoryからダウンロードする際はフォルダをzipファイルにしてダウンロードします。

```bash
!zip -r sign_language_vgg16.zip sign_language_vgg16
```

![8.png](https://qiita-image-store.s3.amazonaws.com/0/47128/c4a4d98d-158e-cb6a-c21f-c18c71c05d0a.png)

これでTensorFlow.jsが読み込める独自画像を利用した学習済みモデルが手に入りました。次回はこのモデルを使ってカメラ画像を元に推論する仕組みをまとめてみます。

【後編：Node.js編】は以下のサイトの５節「TensorFlow.jsモデルへのコンバート」以降を参照してください。

[Tech Book Zone Manatee - 機械学習で遊ぼう! - 第16回　TensorFlow.jsで「じゃんけん」を判別してみよう](https://book.mynavi.jp/manatee/detail/id=99768)


[^1]: [PythonとKerasによるディープラーニング](https://book.mynavi.jp/ec/products/detail/id=90124) （出版社:マイナビ出版 / 著者:Francois Chollet / 翻訳:株式会社クイープ / 監訳:巣籠悠輔）