• トップ
  • ブログ一覧
  • KerasとKivyを使って手書き数字認識アプリを作ってみた!
  • KerasとKivyを使って手書き数字認識アプリを作ってみた!

    広告メディア事業部広告メディア事業部
    2019.06.27

    IT技術

    手書き数字認識アプリを作りたい

    前回の 自作の誤差逆伝播学習法で手書き数字を認識させてみよう! では、自作プログラムで自分で書いた任意の手書き数字認識を試みました。

    ほとんどの手書き数字では認識がうまくできていたものの、学習が遅く、あまり精度は良いとは言えませんでした。

    そこで、本記事では既存の優秀な機械学習フレームワーク「Keras(ケラス)」を使って性能の良いネットワークを作成します。

    さらに、もっと使いやすいように「Kivy(キビー)」というフレームワークを用いてGUIアプリケーション化しようと思います。

    最終的に作れるアプリケーション

    この記事を読んで最終的に完成するのは、「手書きで数字を書くと、その数字を認識するというアプリ」です。

    【図 最終的に作れるアプリケーション】

    それでは、作成していきましょう!

    準備するもの

    まずは、環境整備です。

    今回使うフレームワーク

    1. Keras : 機械学習
    2. Kivy : GUI
    3. NumPy : 数値計算
    4. Pillow (PIL) : 画像処理

    以上のフレームワークがまだインストールされていなければ、次に紹介する方法でインストールしてください。

    もうインストール済みの方は、読み飛ばしてください。

    インストールされているか確認する方法は、ご存知のコレですね。

    1$ pip list

    Keras(ケラス)のインストール

    Kerasは、機械学習の有名なフレームワークの一つで、「Tensorflow(テンソルフロー)」などの有名な機械学習フレームワークを扱いやすく、シンプルにコードが書けるようになっています。

    インストール方法

    Kerasは、Tensorflowに依存しているので、Tensorflowもインストールしておきましょう。

    また、pip がver. 9.x系だとエラーを吐くことがあるらしいので、ついでにpip も最新のものにしておきましょう。

    1$ pip install --upgrade pip  # pipをバージョンアップ
    2$ pip install --upgrade tensorflow  # tensorflowをインストール
    3$ pip install keras

    Kivy(キビー)のインストール

    Kivyは、GUIフレームワークの一つで、オープンソースで開発されています。

    さらに扱いやすく、さまざまなOSで動作するためKivyを使うことにしました。

    Kivyは、Pythonファイルの他に「*.kv」ファイルで簡単にGUIをデザインすることができます。

    インストール方法

    このモジュールもpip でインストールできます。

    1$ pip install kivy

    これで、もし動かなかったら、追加で以下のモジュールもインストールしてください。

    1$ pip install cython
    2$ pip install pygame

    Pillow(PIL)のインストール

    Pillowは、とても使い勝手が良い画像処理系のモジュールです。

    これも先のモジュールと同じようにインストールできます。

    1$ pip install pillow

    いざ開発!

    それでは、お楽しみの開発に移ります。

    開発の流れとしては、以下の通りです。

    1. Kivyで「キャンバス画面・ペイント機能 」を実装
    2. Kerasで「機械学習部 」を実装

    コードとともに解説していきますが、冗長になるため一行ずつ全て解説することはせず、重要な部分を中心に解説をしていきますのでご了承ください。

    Kivy (GUI)部分について

    まずは、キャンバス機能ペイント機能を作ります。

    main.py とMyPaint.kv という2つのファイルを作って、これらにKivyを用いたGUIを管理するコードを書いていきます。

    ここで、「*.kv」の名前は重要になってくるので、必ず同じ名前にしてください。

    コード

    とりあえず、コードは以下のようになります。

    1# --- Kivy 関連 --- #
    2from kivy.app import App
    3from kivy.config import Config
    4
    5# Config関係は他のモジュールがインポートされる前に行う必要があるため、ここ記述
    6Config.set('graphics', 'width', '300')
    7Config.set('graphics', 'height', '340')
    8Config.write()
    9
    10from kivy.uix.widget import Widget
    11from kivy.graphics import Color, Line
    12from kivy.utils import get_color_from_hex
    13from kivy.core.window import Window
    14# ----------------- #
    15
    16from PIL import Image
    17import numpy as np
    18import learning
    19
    20
    21class MyPaintWidget(Widget):
    22    line_width = 20  # 線の太さ
    23    color = get_color_from_hex('#ffffff')  # 線の色
    24
    25    def on_touch_down(self, touch):
    26        if Widget.on_touch_down(self, touch):
    27            return
    28
    29        with self.canvas:
    30            touch.ud['line'] = Line(points=(touch.x, touch.y), width=self.line_width)
    31
    32    def on_touch_move(self, touch):
    33        touch.ud['line'].points += [touch.x, touch.y]
    34
    35    def set_color(self):
    36        self.canvas.add(Color(*self.color))
    37
    38
    39class MyCanvasWidget(Widget):
    40
    41    def clear_canvas(self):
    42        MyPaintWidget.clear_canvas(self)
    43
    44
    45class MyPaintApp(App):
    46
    47    def __init__(self, **kwargs):
    48        super(MyPaintApp, self).__init__(**kwargs)
    49        self.title = '手書き数字認識テスト'
    50
    51    def build(self):
    52        self.painter = MyCanvasWidget()
    53        # 起動時の色の設定を行う
    54        self.painter.ids['paint_area'].set_color()
    55        return self.painter
    56
    57    def clear_canvas(self):
    58        self.painter.ids['paint_area'].canvas.clear()
    59        self.painter.ids['paint_area'].set_color()  # クリアした後に色を再びセット
    60
    61    def predict(self):
    62        pass  # ここで自分の書いた手書き数字の認識結果を表示させたい
    63
    64
    65if __name__ == '__main__':
    66    Window.clearcolor = get_color_from_hex('#000000')   # ウィンドウの色を黒色に変更する
    67    MyPaintApp().run()
    1<MyCanvasWidget>:
    2    paint_id:paint_area
    3    id: canvas_area
    4    BoxLayout:
    5        orientation: 'vertical'
    6        height: root.height
    7
    8        width: root.width
    9        MyPaintWidget:
    10            id: paint_area
    11            size_hint_y: 0.8
    12
    13        BoxLayout:
    14            orientation: 'horizontal'
    15            size_hint_y: 0.1
    16            Button:
    17                text: "Clear"
    18                ont_size: 30
    19                on_release: app.clear_canvas()
    20
    21            Button:
    22                text: 'Predict'
    23                color: 1, 1, 1 , 1
    24                font_size: 30
    25                on_release: app.predict()
    26                border: (2, 2, 2, 2)
    27                x: 0
    28                top: root.top
    29                width: 80
    30                height: 40

    上記のコードを$ python main.py と実行してみてください。

    黒いキャンパスに白い線で文字や数字が書けると思います。

    また「Clear」でキャンパスがリセットできることも確認できます。

    しかし、まだこの時点で「Predict」は未実装なので押してもなにも起こりません

    *.pyと*.kv

    それでは、コードを簡単に解説していきます。

    まずは「この2種類のファイルの関係」について説明します。

    「MyPaint.kv」でボタンのデザインを作っていることは、コードを見るとなんとなく理解できると思います。

    しかし、肝心の「main.py」では、「MyPaint.kv」を参照しているらしきコードは見つかりません。

    これは、「main.py」のクラス名 "***App" の名前から勝手に参照してくれる機能があるからです。

    つまり、今回の場合、「main.py」MyPaintApp というクラスが定義されているので、Kivyが勝手に「MyPaint.kv」を探し参照してくれています。

    そのため、Kivyでは、「ファイル名が重要」なのです。

    キャンバスレイアウト

    ウィンドウとキャンバスのレイアウトは、主にKivy.ConfigというモジュールWindowモジュールで行われています。

    ウィンドウサイズ

    以下のコード冒頭で、ウィンドウサイズを決めています。

    1from kivy.config import Config
    2
    3# Config関係は他のモジュールがインポートされる前に行う必要があるため、ここ記述
    4Config.set('graphics', 'width', '300')
    5Config.set('graphics', 'height', '340')
    6Config.write()

    ウィンドウの背景色

    以下コードのmain関数でコメントしてあるように、ウィンドウの背景色を指定してます。

    1if __name__ == '__main__':
    2    Window.clearcolor = get_color_from_hex('#000000')   # ウィンドウの色を黒色に変更する

    キャンバス全体の管理

    ちなみにキャンバス全体の管理は、以下コードで行なっており、ここではキャンバスを初期化する関数だけ定義しておきます。

    1class MyCanvasWidget(Widget):
    2    def clear_canvas(self):
    3        MyPaintWidget.clear_canvas(self)

    キャンバス内のボタンはMyPaint.kv で定義していきます。

    ペイント部分

    大事なペイント機能は、class MyPaintWidget というクラスが担っています。

    持つべき機能としては「クリックしてドラッグしたら線を引く」ことです。

    線の太さ

    このクラスでは、それに必要な「線の色」「線の太さ」をメンバ変数として保持しています。

    1class MyPaintWidget(Widget):
    2    line_width = 20  # 線の太さ
    3    color = get_color_from_hex('#ffffff')

    そして、マウスクリックダウン時とマウスドラッグ時の機能を以下の関数で実現しています。

    1    def on_touch_down(self, touch):
    2        if Widget.on_touch_down(self, touch):
    3            return
    4
    5        with self.canvas:
    6            touch.ud['line'] = Line(points=(touch.x, touch.y), width=self.line_width)
    7
    8    def on_touch_move(self, touch):
    9        touch.ud['line'].points += [touch.x, touch.y]

    やっていることは、取得した座標に線を追加しているだけです。

    また、キャンバスをクリアした時に色情報を再度セットするための関数があって、ペイント機能は完成です。

    1    def set_color(self):
    2        self.canvas.add(Color(*self.color))

    ボタン類

    ボタン類はすべて「MyPaint.kv」が担っています。

    「*.kv」ファイルの書き方は、< > でどのウィジェットに配置したいかを指定し、その中に(Pythonと同じようにインデントを下げて)ボタン類を記述していきます。

    今回弄るのは、MyCanvasWidget クラスですので、<MyCanvasWidget> の中に記述していきます。

    上下に分割

    ペイント領域とボタン領域を上下に分割することにしましたが、BoxLayout: でレイアウトの調整が可能です。

    まずは、上下に分割したいので、以下のように記述します。

    1BoxLayout:
    2        orientation: 'vertical'

    さらに今回は、ボタンを2つ配置するので、下部のブロックを左右に1:1で分割します。

    左右の分割

    左右の分割は、以下のように指定します。

    1BoxLayout:
    2        orientation: 'horizontal'

    しかし、このままでは、ペイント領域とボタン領域が1:1なので、size_hint_y: で調整しています。

    今回は、0.80.1にしました。

    また、ボタンがクリックされたときの挙動は、on_release: で関数を指定してあげます。

    Keras (機械学習)部分

    今回のアプリの要である、機械学習部分と識別部分を作成していきます。

    イメージとしては、このような流れで作っていきます。

    1. ① アプリが立ち上がる
    2. ② 機械学習を始める
    3. ③ キャンパスが表示される
    4. ④ 手書き数字を学習済みネットワークに流す

    ここでは、Kerasの学習部は別のlearning.py というファイルに実装していきます。

    それでは、まず、アプリを実行した後の「②機械学習を始める」の部分から考えていきたいと思います。

    学習部分の大枠

    アプリ実行時、すなわち最初に呼び出されるクラスのコンストラクタで機械学習を行い、そのネットワークをメンバ変数として保持しておきます。

    したがって、以下のようにコードを加筆します。

    1import learning  # New!
    1class MyPaintApp(App):
    2
    3    def __init__(self, **kwargs):
    4        super(MyPaintApp, self).__init__(**kwargs)
    5        self.title = '手書き数字認識テスト'
    6        
    7        # New! 学習を行う
    8        self.model = learning.learn_MNIST()

    このとき、learning.py にMNISTデータセットを学習するlearn_MNIST() という関数がある前提で加筆します。

    したがって、このような関数を実際にlearning.py に作っていきましょう!

    ネットワークを作成

    今回は、以下のようなネットワークを作成していきます。

    【図 ネットワーク構成】

    ここでDenseとは、全結合層を指し、Dropoutとは過学習を抑制するための手法の一つを指します。

    簡単に説明すると、Dropoutは訓練パターンが入力される度にランダムでニューロンを非活性にし、そのニューロンと接続しているシナプス結合は学習を行わないという手法です。

    こうすることにより、学習の自由度を下げて訓練パターンへの過学習を防ぐことができます。

    活性化関数

    また、今回のネットワークで用いるReLUと呼ばれる活性化関数は、以下のような形をしており、画像識別ではよく使われます。

    【図 ReLU関数の概形】

    最終層の活性化関数であるsoftmaxは、出力値の合計を1にする関数、つまり確率を出力にする関数です。

    Kerasで実装

    ひとまず、ここまでをKerasで実装してみます。

    1from keras.datasets import mnist
    2from keras.utils.np_utils import to_categorical
    3from keras.models import Sequential
    4from keras.layers import Dense, Dropout
    5
    6
    7def learn_MNIST():
    8    # MNISTを読み込み
    9    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    10
    11    # 前処理
    12
    13    # データセットを入力次元784、計60,000個分のベクトルに変換
    14    x_train = x_train.reshape(60000, 784)
    15    x_test = x_test.reshape(10000, 784)
    16
    17    # 画素値を[0, 1]に正規化
    18    x_train = x_train.astype('float32') / 255
    19    x_test = x_test.astype('float32') / 255
    20
    21    # 教師信号をone-hot-encoding
    22    y_train = to_categorical(y_train, 10)
    23    y_test = to_categorical(y_test, 10)
    24
    25    # ネットワーク生成
    26    model = Sequential()  # Sequential なモデル
    27    model.add(Dense(512, activation='relu', input_shape=(784,)))
    28    model.add(Dropout(0.2))
    29    model.add(Dense(512, activation='relu'))
    30    model.add(Dropout(0.2))
    31    model.add(Dense(10, activation='softmax'))

    このような形になりますが、教師信号のOne-hot-encodeingとはもともとのクラスラベルを[0, 1]の配列にすることを指します。

    例えばクラスラベル「3」であれば、[0, 0, 0, 1, 0, 0, 0, 0, 0, 0] のようになります。

    この方が、機械学習では扱いやすいためです。

    学習部分の実装

    次に、学習部分を実装していきます。

    ですがその前に、どのような目的関数を使い、どのような最適化手法を使うかを明示的に設定する必要があります。

    今回は、最適化手法にAdamと呼ばれる学習の収束を早める手法を使い、目的関数はクロスエントロピーという関数を用いることにします。

    1. 最適化手法に「Adam」
    2. 目的関数は、「クロスエントロピー」という関数

    その次に、実際に学習をしていくのですが、Kerasには、fit() という関数一つで学習が行えます。

    実装

    これらの実装例は以下のようになります。

    1    model.compile(optimizer='adam',  # 最適化手法
    2                  loss='categorical_crossentropy',  # 目的(誤差, 損失)関数
    3                  metrics=['accuracy'])  # 計測は識別率で
    4 
    5    # 学習
    6    model.fit(x_train, y_train,
    7              batch_size=100,  # バッチサイズ
    8              epochs=10,  # 学習回数
    9              verbose=1)

    これで学習部分の実装完了です。

    ちなみにverbose は学習過程をログとして出力するかを指定しています。

    0ならログなし、1ならプログレスバーを表示、2ならエポックごとにログを出力させます。

    あとは、これによって作られたネットワーク(model)を戻り値として返すだけです。

    ですが折角なので、テスト精度も表示させるようにしましょう。

    1    loss, accuracy = model.evaluate(x_test, y_test)  # モデルの評価
    2    print('Test Accuracy : ', accuracy)
    3
    4    return model

    これで学習部分は完成です!

    識別部分

    最後に、キャンパスに描いた手書き数字をネットワークに流してネットワークの答えを出力させるだけです。

    main.py のpredict() を作っていきましょう!

    流れ

    1. ① キャンパスを画像として保存
    2. ② Pillowでその画像を読み込み、不要な部分(ボタン類)を切り取る
    3. ③ グレースケール、28×28に変換
    4. ④ 画像を配列に変換
    5. ⑤ ネットワークに流す

    実装

    これらを実装してみます。

    1    def predict(self):
    2        self.painter.export_to_png('canvas.png')  # 画像を一旦保存する
    3
    4        # Pillowで読み込み、余計な部分を切り取る。またグレースケールにも変換
    5        image = Image.open('canvas.png').crop((0, 0, 600, 600)).convert('L')
    6        # リサイズ
    7        image = image.resize((28, 28))
    8        image = np.array(image)  # 配列に変換
    9        image = image.reshape(1, 784)  # 1次元配列に変換
    10        ans = self.model.predict(image)  # ネットワークで出力を得る
    11        print('This Digit is ... ', np.argmax(ans))  # 一番大きい出力値のインデックスがネットワークの答え

    以上で実装は終わりです!

    お疲れ様でした!

    こちらの記事もオススメ!

    featureImg2020.07.28機械学習 特集知識編人工知能・機械学習でよく使われるワード徹底まとめ!機械学習の元祖「パーセプトロン」とは?【人工知能】ニューラルネ...

    関連記事

    featureImg2019.06.11【前編】自作の誤差逆伝播学習法で手書き数字を認識させてみよう!【機械学習】前編〜手書き数字を認識するプログラムを作る~誤差逆伝播学習法は、教師信号とネットワークの実際の出力との誤差情報と勾配降...

    最終的なコード

    main.py

    1# --- Kivy 関連 --- #
    2from kivy.app import App
    3from kivy.config import Config
    4
    5# Config関係は他のモジュールがインポートされる前に行う必要があるため、ここ記述
    6Config.set('graphics', 'width', '300')
    7Config.set('graphics', 'height', '340')
    8Config.write()
    9
    10from kivy.uix.widget import Widget
    11from kivy.graphics import Color, Line
    12from kivy.utils import get_color_from_hex
    13from kivy.core.window import Window
    14# ----------------- #
    15
    16from PIL import Image
    17import numpy as np
    18import learning
    19
    20
    21class MyPaintWidget(Widget):
    22    line_width = 20  # 線の太さ
    23    color = get_color_from_hex('#ffffff')
    24
    25    def on_touch_down(self, touch):
    26        if Widget.on_touch_down(self, touch):
    27            return
    28
    29        with self.canvas:
    30            touch.ud['line'] = Line(points=(touch.x, touch.y), width=self.line_width)
    31
    32    def on_touch_move(self, touch):
    33        touch.ud['line'].points += [touch.x, touch.y]
    34
    35    def set_color(self):
    36        self.canvas.add(Color(*self.color))
    37
    38
    39class MyCanvasWidget(Widget):
    40    def clear_canvas(self):
    41        MyPaintWidget.clear_canvas(self)
    42
    43
    44class MyPaintApp(App):
    45
    46    def __init__(self, **kwargs):
    47        super(MyPaintApp, self).__init__(**kwargs)
    48        self.title = '手書き数字認識テスト'
    49
    50        # 学習を行う
    51        self.model = learning.learn_MNIST()
    52
    53    def build(self):
    54        self.painter = MyCanvasWidget()
    55        # 起動時の色の設定を行う
    56        self.painter.ids['paint_area'].set_color()
    57        return self.painter
    58
    59    def clear_canvas(self):
    60        self.painter.ids['paint_area'].canvas.clear()
    61        self.painter.ids['paint_area'].set_color()  # クリアした後に色を再びセット
    62
    63    def predict(self):
    64        self.painter.export_to_png('canvas.png')  # 画像を一旦保存する
    65
    66        # Pillowで読み込み、余計な部分を切り取る。またグレースケールにも変換
    67        image = Image.open('canvas.png').crop((0, 0, 600, 600)).convert('L')
    68        # リサイズ
    69        image = image.resize((28, 28))
    70        image = np.array(image)
    71        image = image.reshape(1, 784)
    72        ans = self.model.predict(image)
    73        print('This Digit is ... ', np.argmax(ans))
    74
    75
    76if __name__ == '__main__':
    77    Window.clearcolor = get_color_from_hex('#000000')   # ウィンドウの色を黒色に変更する
    78    MyPaintApp().run()

    MyPaint.kv

    1<MyCanvasWidget>:
    2    paint_id:paint_area
    3    id: canvas_area
    4    BoxLayout:
    5        orientation: 'vertical'
    6        height: root.height
    7        width: root.width
    8
    9        MyPaintWidget:
    10            id: paint_area
    11            size_hint_y: 0.8
    12
    13        BoxLayout:
    14            orientation: 'horizontal'
    15            size_hint_y: 0.1
    16            Button:
    17                text: "Clear"
    18                ont_size: 30
    19                on_release: app.clear_canvas()
    20
    21            Button:
    22                text: 'Predict'
    23                color: 1, 1, 1 , 1
    24                font_size: 30
    25                on_release: app.predict()
    26                border: (2, 2, 2, 2)
    27                x: 0
    28                top: root.top
    29                width: 80
    30                height: 40

    learning.py

    1from keras.datasets import mnist
    2from keras.utils.np_utils import to_categorical
    3from keras.models import Sequential
    4from keras.layers import Dense, Dropout
    5
    6
    7def learn_MNIST():
    8    # MNISTを読み込み
    9    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    10
    11    # 前処理
    12
    13    # データセットを入力次元784、計60,000個分のベクトルに変換
    14    x_train = x_train.reshape(60000, 784)
    15    x_test = x_test.reshape(10000, 784)
    16
    17    # 画素値を[0, 1]に正規化
    18    x_train = x_train.astype('float32') / 255
    19    x_test = x_test.astype('float32') / 255
    20
    21    # 教師信号をone-hot-encoding
    22    y_train = to_categorical(y_train, 10)
    23    y_test = to_categorical(y_test, 10)
    24
    25    # ネットワーク生成
    26    model = Sequential()  # Sequential なモデル
    27    model.add(Dense(512, activation='relu', input_shape=(784,)))
    28    model.add(Dropout(0.2))
    29    model.add(Dense(512, activation='relu'))
    30    model.add(Dropout(0.2))
    31    model.add(Dense(10, activation='softmax'))
    32
    33    model.compile(optimizer='adam',  # 最適化手法
    34                  loss='categorical_crossentropy',  # 目的(誤差, 損失)関数
    35                  metrics=['accuracy'])  # 計測は識別率で
    36
    37    # 学習
    38    model.fit(x_train, y_train,
    39              batch_size=100,  # バッチサイズ
    40              epochs=10,  # 学習回数
    41              verbose=1)
    42
    43    loss, accuracy = model.evaluate(x_test, y_test)
    44    print('Test Accuracy : ', accuracy)
    45
    46    return model

     

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    GitHubActionsのランナーに触れてみた

    こやまん(エンジニア)

    こやまん(エンジニア)

    2024.03.28

    IT技術

    Azure Data FactoryでSlackへ通知をしてみる

    たかやん(エンジニア)

    たかやん(エンジニア)

    2024.03.28

    IT技術

    GCP Secret Managerを使ってみた

    たなゆー(エンジニア)

    たなゆー(エンジニア)

    2024.03.21

    IT技術

    Bitriseのパイプラインと環境変数

    加納(エンジニア)

    加納(エンジニア)

    2024.03.11

    IT技術