Webで手書き文字認識をしたかった…(失敗談)

この記事は[エイチームブライズ/エイチームコネクト/エイチーム引越し侍 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/hikkoshi)2日目の記事です。


## Webで手書き文字認識をしたい！
学習済みのモデルとTensorflow.jsを使って、ブラウザ上でやりましょう！
mnistの次の段階として、公開されている活字データセットを使用して、「ひらがな」の文字認識をしてみたいと思います。

## 今回のテーマ
__活字のデータセットで手書き文字認識が出来るのか！？__

## 結論
出来ませんでした。いくつかテクニックを使用したのですが、過学習に陥っているようで、何を渡しても結果が変わらず…。
活字の文字認識であればまた違った結果になったかもしれません。

### 敗因
- 手書き認識に向いているデータセットではなかった
    - こう言うと元も子もないですが、活字と手書きではデータの性質が合わなかったみたいです。
    - 深刻な枚数不足も考えられます。これはImageGeneratorでなんとかなるかも…？

以下、Tensorflow.jsの解説と、使用したテクニックをいくつか紹介させていただきます。

## Tensorflow.jsとは
>A JavaScript library for training and deploying ML models in the browser and on Node.js
[TensorFlow\.js](https://js.tensorflow.org/)より。

意訳すると、「ブラウザ上でJavaScriptを用いて、機械学習の学習や学習済みモデルの使用が出来るライブラリ」です。

## Web上でTensorflowを動かすメリット・デメリット
クラサバ構成ではなく、tensorflow.jsを使ってブラウザ上のみで済ますメリット/デメリットは以下のとおりです。

### メリット
- データをサーバーに転送する必要がない
- データを転送しない分、ほぼリアルタイムに判定が行われる

### デメリット
- モデルをダウンロードする必要がある


## 使用するデータセット

[文字画像データセット\(平仮名73文字版\)を試験公開しました \| NDLラボ](https://lab.ndl.go.jp/cms/hiragana73)のデータセットを使用させていただきました。

## GitHubのリポジトリ
[GitHub \- iwahara/hiragana\_cnn](https://github.com/iwahara/hiragana_cnn)にて、データセットのダウンロードから学習、ブラウザ上での確認までを行えるソースを一式まとめてあります。
学習や確認はDocker上で実行するようになっています。
`docker-compose build && docker-compose up`するとデータセットのダウンロードからnginxの起動までまとめてやってくれます。
環境にもよりますが、初回はダウンロードや学習などで１時間程度かかりますので、気長にお待ち下さい。

詳細はソースコードを読んでもらうとして、mnistではやらなかった点をピックアップして説明します。

## 使用したテクニック

### グレースケール化
今回使用するデータセットは、PNG画像になっており、色情報(RGB)が含まれています（まれに色情報の入ってないPNGも混ざってます）。
しかし、色情報が含まれていると次元数が増えてしまい、また文字認識に色情報は不要であるため、グレースケール化して情報量を減らします。
縦×横の要素の中にRGBの3次元データが入っていたものを、縦×横の要素の中に0〜255の数値が入っているものに変換するため、
1ピクセルに付き、2次元減らすことができます。
今回のデータセットは48×48のデータが80000あるので、

```math
48 \times 48 \times 80000 \times 2 = 368640000
```

約3億7千万回も計算回数を減らすことになります。
GPUを使えないような環境だと、これでもかなり大きな削減になります。

グレースケール化の方法はいくつかありますが、
今回はPILを使って画像を読み込み、グレースケール化する方法にしました。

基本的に、機械学習でデータを扱う際はnumpyで扱うので、
このタイミングでnumpy配列に変換しておくと良いでしょう。

```py:抜粋
from PIL import Image
import numpy as np

im = Image.open(png)
new_im = np.array(im.convert('L')) # グレースケール化してnumpy化
```

### カテゴリカル化
mnistの場合は「0〜9」までの数値でしたが、今回は合計73文字のデータなので、
それぞれに番号を振る必要があります。
今回は以下の通りにしてみました。

```py:抜粋
categorical_list = list("あいうえおかがきぎくぐけげこごさざしじすずせぜそぞただちぢつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもやゆよらりるれろわゐゑをん")
```

これは、ディレクトリ名をsortした場合の並びと一致しており、
番号を取得する際は`categorical_list.index("あ")`のようにします。
これで、0〜72まで番号が振られることになります。

### numpy配列に変換し保存
tensorflow（keras）でデータを扱う際はnumpyの方が何かと都合が良いので、numpy形式でグレースケール化した画像と、それに対応するラベルを保存します。

1ファイルごとに保存してもよいのですが、numpyはいくつかの配列をまとめて圧縮保存できるので、今回はそうします。
ただし、今回でも190MB近くのファイルサイズになるので、マシンのメモリと相談しましょう。
まとめて保存する際は、データとラベルのインデックスが同じになるようにしてください。
そうしないと、学習の際に間違って学習してしまい、全く違う結果になってしまいます。

### ラベルの偏り調査＆重み調整
mnistの場合、データの偏りはなくすべてのラベルで同じ枚数のデータが用意されているが、
今回のデータセットは大きな偏りがあります。
例えば、「あ」だと1208枚だが「ぽ」だと261枚しかなかったりします。
そのため、とあるデータを与えられたときに、そのデータが「あ」であると学習してしまう確率が高くなってしまいます。
それを調整するには、データが少ない場合でも、データが多いラベルのときと同じように学習をして貰う必要があります。
それが「重み」です。
今回は以下の式で各ラベルの重みを計算することにしました。

```math
重み = ラベルの最大枚数 \div そのラベルの枚数
```

この式の場合、「あ」のデータの重みは1.063ですが、「ぽ」のデータの重みは4.923となり、より重く扱われることになります。

### データセットの分割
データセットの分割にはscikit-learnのtrain_test_splitを使うことにします。
この関数は、データセットとラベルを指定された割合で分割し、さらにシャッフルしてくれるものです。

```py:抜粋
X_train, X_test, y_train, y_test = train_test_split(x_data, y_data, test_size=0.25)
```

# まとめ
手書き文字認識の場合は手書きのデータセットを用いると良いでしょう。
今回はいい経験になりました。

# お知らせ
エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひとも[エイチームグループ採用ページ](http://www.a-tm.co.jp/recruit/)（[Webエンジニア詳細ページ](http://www.a-tm.co.jp/recruit/requirements/career/lifestylesupport-webengineer/)）よりお問い合わせ下さい。

# 明日
エイチームブライズ/エイチームコネクト/エイチーム引越し侍アドベントカレンダー2日目の記事は、いかがでしたでしょうか。
明日は @exit_00が書いてくれるようです。

## 参考URL

[Pythonの画像処理ライブラリPillow\(PIL\)の使い方 \| note\.nkmk\.me](https://note.nkmk.me/python-pillow-basic/)

[文字画像データセット\(平仮名73文字版\)を試験公開しました \| NDLラボ](https://lab.ndl.go.jp/cms/hiragana73)

[Python, NumPyで画像処理（読み込み、演算、保存） \| note\.nkmk\.me](https://note.nkmk.me/python-numpy-image-processing/)

[JavaScirpt でマウス座標（位置）を取得する \- はしくれエンジニアもどきのメモ](https://cartman0.hatenablog.com/entry/2015/06/29/022301)

[TensorFlow\.jsでMNIST学習済モデルを読み込みブラウザで手書き文字認識をする \- Qiita](https://qiita.com/yukagil/items/ca84c4bfcb47ac53af99)

[scikit\-learnのtrain\_test\_split関数を使用してデータを分割する \- AI人工知能テクノロジー](https://newtechnologylifestyle.net/rain_test_split%E9%96%A2%E6%95%B0%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%88%86%E5%89%B2%E3%81%99%E3%82%8B/)

[第17回　TensorFlow\.jsで「感情」を認識してみよう｜Tech Book Zone Manatee](https://book.mynavi.jp/manatee/detail/id=99887)