F@N Ad-Tech Blog

株式会社ファンコミュニケーションズ nend・nex8・viidleのエンジニア・技術ブログ

無料のeラーニング「Aidemy」をやってみて、ディープラーニングでひらがなを分類してみた。

皆様新年あけましておめでとうございます。冬場は毎日在宅勤務したいh_matsumotoです。最近無料でディープラーニングを学べるという「Aidemy」をやってみました。とても勉強熱心なので、12月24日と25日を丸々費やしてみました。決して予定が空いていた訳ではないと思います。

aidemy.net

Aidemyではディープラーニングを利用して、0~9までの数字を分類する方法を学ぶ事が出来ます。
f:id:fan_h_matsumoto:20171226162033p:plain

今回はそれを応用して「ひらがな」を分類してみたいと思います。

【目次】

1 ひらがなデータの入手

「ひらがな 画像 データセット」等でググって探してみると、こちらのサイトが見つかりましたので利用させて頂きました。

NDL Lab
文字画像データセット(平仮名73文字版)を試験公開しました | NDLラボ

f:id:fan_h_matsumoto:20171226163128p:plain

画像数は数がバラバラですが、大体一つのひらがなにつき1000枚以上画像があります。
1枚の画像サイズは48 x 48 [pixels]です。

2 画像データを配列にする

Aidemyの数字判別用のデータは最初から配列に変換されたデータですが、こちらは画像データなので1枚ずつ画像データを配列に変換する必要があります。Aidemyではその方法もレクチャーとしてありました。

https://aidemy.net/courses/4050

f:id:fan_h_matsumoto:20171226163642p:plain

使用言語

python3.5

使用ライブラリ

import numpy as np
import pandas as pd
%matplotlib inline #jupyter note bookで実行する場合
import matplotlib.pyplot as plt
from keras.layers import Activation, Dense, Dropout
from keras.models import Sequential
from keras import optimizers
from keras.utils.np_utils import to_categorical
from keras.callbacks import EarlyStopping
import requests
import zipfile
import cv2
import os
from sklearn.model_selection import train_test_split

zipファイルをダウンロードし展開。
画像ファイルを読み込み、特徴量とラベルデータを作る。

#フォルダ名:{ひらがな:ラベル(番号)}
aiueo = {"U3042":{"あ":1},
        "U3044":{"い":2},
        "U3046":{"う":3},
        "U3048":{"え":4},
        "U304A":{"お":5},
        "U304B":{"か":6},
        "U304C":{"が":7},
        "U304D":{"き":8},
        "U304E":{"ぎ":9},
        "U304F":{"く":10},
        "U3050":{"ぐ":11},
        "U3051":{"け":12},
        "U3052":{"げ":13},
        "U3053":{"こ":14},
        "U3054":{"ご":15},
        "U3055":{"さ":16},
        "U3056":{"ざ":17},
        "U3057":{"し":18},
        "U3058":{"じ":19},
        "U3059":{"す":20},
        "U305A":{"ず":21},
        "U305B":{"せ":22},
        "U305C":{"ぜ":23},
        "U305D":{"そ":24},
        "U305E":{"ぞ":25},
        "U305F":{"た":26},
        "U3060":{"だ":27},
        "U3061":{"ち":28},
        "U3062":{"ぢ":29},
        "U3064":{"つ":30},
        "U3065":{"づ":31},
        "U3066":{"て":32},
        "U3067":{"で":33},
        "U3068":{"と":34},
        "U3069":{"ど":35},
        "U306A":{"な":36},
        "U306B":{"に":37},
        "U306C":{"ぬ":38},
        "U306D":{"ね":39},
        "U306E":{"の":40},
        "U306F":{"は":41},
        "U3070":{"ば":42},
        "U3071":{"ぱ":43},
        "U3072":{"ひ":44},
        "U3073":{"び":45},
        "U3074":{"ぴ":46},
        "U3075":{"ふ":47},
        "U3076":{"ぶ":48},
        "U3077":{"ぷ":49},
        "U3078":{"へ":50},
        "U3079":{"べ":51},
        "U307A":{"ぺ":52},
        "U307B":{"ほ":53},
        "U307C":{"ぼ":54},
        "U307D":{"ぽ":55},
        "U307E":{"ま":56},
        "U307F":{"み":57},
        "U3080":{"む":58},
        "U3081":{"め":59},
        "U3082":{"も":60},
        "U3084":{"や":61},
        "U3086":{"ゆ":62},
        "U3088":{"よ":63},
        "U3089":{"ら":64},
        "U308A":{"り":65},
        "U308B":{"る":66},
        "U308C":{"れ":67},
        "U308D":{"ろ":68},
        "U308F":{"わ":69},
        "U3090":{"ゐ":70},
        "U3091":{"ゑ":71},
        "U3092":{"を":72},
        "U3093":{"ん":73}}

def download_file(url):
    filename = url.split('/')[-1]
    r = requests.get(url, stream=True)
    with open(filename, 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024):
            if chunk:
                f.write(chunk)
                f.flush()
        return filename
    
    return False

def zip_extract(filename):
    zfile = zipfile.ZipFile(filename)
    zfile.extractall('.')
    return zfile

file_name = download_file('http://lab.ndl.go.jp/dataset/hiragana73.zip')
zfile = zip_extract(file_name)

d1 = zfile.filename.replace('.zip','')
#特徴量
hiragana_array = []
#ラベル
hiragana_label = []
for d2 in os.listdir('./' + d1):
    for picture in os.listdir('./' + d1 + '/' + d2):
        #ここで画像を読み込んで配列(48,48,3)を取得する
        img = cv2.imread('./' + d1 + '/' + d2 + '/' + picture)
        #画像をモノクロ画像にする(48,48,3)→ (48,48)になって計算が早くなる
        img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
        #閾値処理(二値化)する
        retval, my_img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
        #画像の配列を1次元にする(48×48)⇒ 2304
        hiragana_array.append(my_img.reshape(-1))

        hiragana_label.append(list(aiueo[d2].values())[0])

閾値処理(二値化)について

retval, my_img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)

上記の処理ですが、大津の二値化というアルゴリズムを利用して画像毎に閾値を自動で検出させています。
algorithm.joho.info

3 分類してみる

訓練データと検証データの分類には「sklearnのtrain_test_split」を使いました。
多分kerasにも、同じようなメソッドがあると思います。

#訓練データと検証データに分ける
x_train,x_test,y_train,y_test = train_test_split(np.array(hiragana_array),hiragana_label, test_size=0.2)

#ひらがなに振った番号を0,1のarray形式にする
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

#データの最小値が0、最大値が1になるように正規化する
x_train = x_train/255
x_test = x_test/255

#ここからいよいよディープラーニングの設定
model = Sequential()
model.add(Dense(256, input_dim=2304))
model.add(Activation("sigmoid"))
model.add(Dropout(p=0.1))
model.add(Dense(74))
model.add(Activation("softmax"))

sgd = optimizers.SGD(lr=0.1)
model.compile(optimizer=sgd, loss="categorical_crossentropy", metrics=["accuracy"])

#Aidemyには無かった「EarlyStopping」を追加
es_cb = EarlyStopping(monitor='val_loss', patience=0, verbose=0, mode='auto')

history = model.fit(np.array(x_train),
                    np.array(y_train),
                    batch_size = 500,
                    nb_epoch = 100,
                    verbose = 1,
                    validation_data = (x_test, y_test),
                    callbacks = [es_cb])

基本的にはAidemyの数字分類のコードと一緒ですが、過学習を防ぐために「EarlyStopping」を追加してあります。
参考にさせて頂いたサイト
blog.shoby.jp

ハイパーパラメーターについては正直私も勉強不足でよくわかりませんが、何度か数字を変えて試したところデータ数が少ないせいか上記の設定が一番精度が良かったです。

実行すると、以下のように徐々に精度が向上していく過程が見れます。

Train on 64000 samples, validate on 16000 samples
Epoch 1/100
64000/64000 [==============================] - 13s - loss: 3.3197 - acc: 0.4270 - val_loss: 2.3735 - val_acc: 0.7947
Epoch 2/100
64000/64000 [==============================] - 13s - loss: 1.8139 - acc: 0.8273 - val_loss: 1.3459 - val_acc: 0.8924
Epoch 3/100
64000/64000 [==============================] - 12s - loss: 1.1035 - acc: 0.8992 - val_loss: 0.8846 - val_acc: 0.9276
Epoch 4/100
64000/64000 [==============================] - 12s - loss: 0.7821 - acc: 0.9212 - val_loss: 0.6636 - val_acc: 0.9342
Epoch 5/100
64000/64000 [==============================] - 13s - loss: 0.6126 - acc: 0.9309 - val_loss: 0.5355 - val_acc: 0.9402
Epoch 6/100
64000/64000 [==============================] - 12s - loss: 0.5089 - acc: 0.9379 - val_loss: 0.4552 - val_acc: 0.9459
Epoch 7/100
64000/64000 [==============================] - 12s - loss: 0.4396 - acc: 0.9422 - val_loss: 0.3987 - val_acc: 0.9477
Epoch 8/100
64000/64000 [==============================] - 14s - loss: 0.1414 - acc: 0.9701 - val_loss: 0.1438 - val_acc: 0.9686
Epoch 38/100
64000/64000 [==============================] - 15s - loss: 0.1389 - acc: 0.9703 - val_loss: 0.1423 - val_acc: 0.9683
Epoch 39/100
64000/64000 [==============================] - 14s - loss: 0.1374 - acc: 0.9700 - val_loss: 0.1402 - val_acc: 0.9701
Epoch 40/100
64000/64000 [==============================] - 13s - loss: 0.1355 - acc: 0.9705 - val_loss: 0.1389 - val_acc: 0.9695
Epoch 41/100
64000/64000 [==============================] - 14s - loss: 0.1339 - acc: 0.9711 - val_loss: 0.1379 - val_acc: 0.9698
Epoch 42/100
64000/64000 [==============================] - 14s - loss: 0.1319 - acc: 0.9713 - val_loss: 0.1368 - val_acc: 0.9696
Epoch 43/100
64000/64000 [==============================] - 12s - loss: 0.1303 - acc: 0.9714 - val_loss: 0.1354 - val_acc: 0.9692
Epoch 44/100
64000/64000 [==============================] - 13s - loss: 0.1290 - acc: 0.9715 - val_loss: 0.1345 - val_acc: 0.9693
Epoch 45/100
64000/64000 [==============================] - 13s - loss: 0.1269 - acc: 0.9721 - val_loss: 0.1328 - val_acc: 0.9696
Epoch 46/100
64000/64000 [==============================] - 12s - loss: 0.1259 - acc: 0.9722 - val_loss: 0.1319 - val_acc: 0.9701
Epoch 47/100
64000/64000 [==============================] - 14s - loss: 0.1249 - acc: 0.9719 - val_loss: 0.1307 - val_acc: 0.9703
Epoch 48/100
64000/64000 [==============================] - 14s - loss: 0.1234 - acc: 0.9730 - val_loss: 0.1305 - val_acc: 0.9703
Epoch 49/100
64000/64000 [==============================] - 13s - loss: 0.1224 - acc: 0.9725 - val_loss: 0.1280 - val_acc: 0.9712
Epoch 50/100
64000/64000 [==============================] - 15s - loss: 0.1209 - acc: 0.9729 - val_loss: 0.1268 - val_acc: 0.9706
Epoch 51/100
64000/64000 [==============================] - 15s - loss: 0.1193 - acc: 0.9732 - val_loss: 0.1279 - val_acc: 0.9704

4 結果の確認

こんな簡単にやってみたのですが、分類精度は97%と非常に高いです。

検証データの説明変数を出来たモデルに入れて予測したもの

np.argmax(model.predict(x_test[0:10]), axis=1)
>array([18, 63, 39, 37, 60, 12,  6, 34, 50, 24])


検証データのラベル

#0,1のarrayにしているため1がいる位置を取得する
print([list(y_test[i]).index(1) for i in range(10)])
>[18, 63, 39, 37, 60, 12, 6, 34, 50, 24]

始めの10件だけですが見事に一致しています。

ラベルだと何のひらがなか分からないので、配列から画像を復元してみましょう。

for i in range(5):
    plt.imshow(np.reshape(x_test[0 + i:1 + i]*255,(48,48,3)), cmap = 'gray', interpolation = 'bicubic')
    plt.xticks([]), plt.yticks([])  
    plt.show()

"U3057":{"し":18},
f:id:fan_h_matsumoto:20171226232116p:plain
"U3088":{"よ":63},
f:id:fan_h_matsumoto:20171226232131p:plain
"U306D":{"ね":39},
f:id:fan_h_matsumoto:20171226232139p:plain
"U306B":{"に":37},
f:id:fan_h_matsumoto:20171226232147p:plain
"U3082":{"も":60},
f:id:fan_h_matsumoto:20171226232203p:plain

先ほど定義した辞書のラベルとも一致していますね!

いくつか他のkerasを利用した画像分類の事例を見てみましたが、今回精度が高かった要因は画像データが大体同じような形
(極端なイタリックやボールド、拡大縮小されていない)のため精度が高かったと考えられます。

最後に

ファンコミュニケーションズでは機械学習エンジニアを募集しています。

主な開発言語はScala, PHPですが、分析作業においてはPythonやRも多用しています。
また分析環境としてTreasure Dataを導入しているため、SQLによって学習データの作成・機械学習をシームレスに行うことができ、データクレンジングなどの雑務に追われることなく非常に大きなデータを扱える環境が整っています。

興味がある方は以下のページをご覧ください。(Webアプリケーションエンジニアと書いてありますが、機械学習エンジニアも含んでいますので安心してください。)

www.fancs.com