
KerasとKivyを使って手書き数字認識アプリを作ってみた!
2021.12.20
手書き数字認識アプリを作りたい
前回の 自作の誤差逆伝播学習法で手書き数字を認識させてみよう! では、自作プログラムで自分で書いた任意の手書き数字認識を試みました。
ほとんどの手書き数字では認識がうまくできていたものの、学習が遅く、あまり精度は良いとは言えませんでした。
そこで、本記事では既存の優秀な機械学習フレームワーク「Keras(ケラス)」を使って性能の良いネットワークを作成します。
さらに、もっと使いやすいように「Kivy(キビー)」というフレームワークを用いてGUIアプリケーション化しようと思います。
最終的に作れるアプリケーション
この記事を読んで最終的に完成するのは、「手書きで数字を書くと、その数字を認識するというアプリ」です。
【図 最終的に作れるアプリケーション】
それでは、作成していきましょう!
準備するもの
まずは、環境整備です。
今回使うフレームワーク
- Keras : 機械学習
- Kivy : GUI
- NumPy : 数値計算
- Pillow (PIL) : 画像処理
以上のフレームワークがまだインストールされていなければ、次に紹介する方法でインストールしてください。
もうインストール済みの方は、読み飛ばしてください。
インストールされているか確認する方法は、ご存知のコレですね。
1 | $ pip list |
Keras(ケラス)のインストール
Kerasは、機械学習の有名なフレームワークの一つで、「Tensorflow(テンソルフロー)」などの有名な機械学習フレームワークを扱いやすく、シンプルにコードが書けるようになっています。
インストール方法
Kerasは、Tensorflowに依存しているので、Tensorflowもインストールしておきましょう。
また、 pip がver. 9.x系だとエラーを吐くことがあるらしいので、ついでに pip も最新のものにしておきましょう。
1 2 3 | $ pip install --upgrade pip # pipをバージョンアップ $ pip install --upgrade tensorflow # tensorflowをインストール $ pip install keras |
Kivy(キビー)のインストール
Kivyは、GUIフレームワークの一つで、オープンソースで開発されています。
さらに扱いやすく、さまざまなOSで動作するためKivyを使うことにしました。
Kivyは、Pythonファイルの他に「*.kv」ファイルで簡単にGUIをデザインすることができます。
インストール方法
このモジュールも pip でインストールできます。
1 | $ pip install kivy |
これで、もし動かなかったら、追加で以下のモジュールもインストールしてください。
1 2 | $ pip install cython $ pip install pygame |
Pillow(PIL)のインストール
Pillowは、とても使い勝手が良い画像処理系のモジュールです。
これも先のモジュールと同じようにインストールできます。
1 | $ pip install pillow |
いざ開発!
それでは、お楽しみの開発に移ります。
開発の流れとしては、以下の通りです。
- Kivyで「キャンバス画面・ペイント機能 」を実装
- Kerasで「機械学習部 」を実装
コードとともに解説していきますが、冗長になるため一行ずつ全て解説することはせず、重要な部分を中心に解説をしていきますのでご了承ください。
Kivy (GUI)部分について
まずは、キャンバス機能とペイント機能を作ります。
main.py と MyPaint.kv という2つのファイルを作って、これらにKivyを用いたGUIを管理するコードを書いていきます。
ここで、「*.kv」の名前は重要になってくるので、必ず同じ名前にしてください。
コード
とりあえず、コードは以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | # --- Kivy 関連 --- # from kivy.app import App from kivy.config import Config # Config関係は他のモジュールがインポートされる前に行う必要があるため、ここ記述 Config.set('graphics', 'width', '300') Config.set('graphics', 'height', '340') Config.write() from kivy.uix.widget import Widget from kivy.graphics import Color, Line from kivy.utils import get_color_from_hex from kivy.core.window import Window # ----------------- # from PIL import Image import numpy as np import learning class MyPaintWidget(Widget): line_width = 20 # 線の太さ color = get_color_from_hex('#ffffff') # 線の色 def on_touch_down(self, touch): if Widget.on_touch_down(self, touch): return with self.canvas: touch.ud['line'] = Line(points=(touch.x, touch.y), width=self.line_width) def on_touch_move(self, touch): touch.ud['line'].points += [touch.x, touch.y] def set_color(self): self.canvas.add(Color(*self.color)) class MyCanvasWidget(Widget): def clear_canvas(self): MyPaintWidget.clear_canvas(self) class MyPaintApp(App): def __init__(self, **kwargs): super(MyPaintApp, self).__init__(**kwargs) self.title = '手書き数字認識テスト' def build(self): self.painter = MyCanvasWidget() # 起動時の色の設定を行う self.painter.ids['paint_area'].set_color() return self.painter def clear_canvas(self): self.painter.ids['paint_area'].canvas.clear() self.painter.ids['paint_area'].set_color() # クリアした後に色を再びセット def predict(self): pass # ここで自分の書いた手書き数字の認識結果を表示させたい if __name__ == '__main__': Window.clearcolor = get_color_from_hex('#000000') # ウィンドウの色を黒色に変更する MyPaintApp().run() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <MyCanvasWidget>: paint_id:paint_area id: canvas_area BoxLayout: orientation: 'vertical' height: root.height width: root.width MyPaintWidget: id: paint_area size_hint_y: 0.8 BoxLayout: orientation: 'horizontal' size_hint_y: 0.1 Button: text: "Clear" ont_size: 30 on_release: app.clear_canvas() Button: text: 'Predict' color: 1, 1, 1 , 1 font_size: 30 on_release: app.predict() border: (2, 2, 2, 2) x: 0 top: root.top width: 80 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モジュールで行われています。
ウィンドウサイズ
以下のコード冒頭で、ウィンドウサイズを決めています。
1 2 3 4 5 6 | from kivy.config import Config # Config関係は他のモジュールがインポートされる前に行う必要があるため、ここ記述 Config.set('graphics', 'width', '300') Config.set('graphics', 'height', '340') Config.write() |
ウィンドウの背景色
以下コードのmain関数でコメントしてあるように、ウィンドウの背景色を指定してます。
1 2 | if __name__ == '__main__': Window.clearcolor = get_color_from_hex('#000000') # ウィンドウの色を黒色に変更する |
キャンバス全体の管理
ちなみにキャンバス全体の管理は、以下コードで行なっており、ここではキャンバスを初期化する関数だけ定義しておきます。
1 2 3 | class MyCanvasWidget(Widget): def clear_canvas(self): MyPaintWidget.clear_canvas(self) |
キャンバス内のボタンは MyPaint.kv で定義していきます。
ペイント部分
大事なペイント機能は、 class MyPaintWidget というクラスが担っています。
持つべき機能としては「クリックしてドラッグしたら線を引く」ことです。
線の太さ
このクラスでは、それに必要な「線の色」「線の太さ」をメンバ変数として保持しています。
1 2 3 | class MyPaintWidget(Widget): line_width = 20 # 線の太さ color = get_color_from_hex('#ffffff') |
そして、マウスクリックダウン時とマウスドラッグ時の機能を以下の関数で実現しています。
1 2 3 4 5 6 7 8 9 | def on_touch_down(self, touch): if Widget.on_touch_down(self, touch): return with self.canvas: touch.ud['line'] = Line(points=(touch.x, touch.y), width=self.line_width) def on_touch_move(self, touch): touch.ud['line'].points += [touch.x, touch.y] |
やっていることは、取得した座標に線を追加しているだけです。
また、キャンバスをクリアした時に色情報を再度セットするための関数があって、ペイント機能は完成です。
1 2 | def set_color(self): self.canvas.add(Color(*self.color)) |
ボタン類
ボタン類はすべて「MyPaint.kv」が担っています。
「*.kv」ファイルの書き方は、 < > でどのウィジェットに配置したいかを指定し、その中に(Pythonと同じようにインデントを下げて)ボタン類を記述していきます。
今回弄るのは、 MyCanvasWidget クラスですので、 <MyCanvasWidget> の中に記述していきます。
上下に分割
ペイント領域とボタン領域を上下に分割することにしましたが、 BoxLayout: でレイアウトの調整が可能です。
まずは、上下に分割したいので、以下のように記述します。
1 2 | BoxLayout: orientation: 'vertical' |
さらに今回は、ボタンを2つ配置するので、下部のブロックを左右に1:1で分割します。
左右の分割
左右の分割は、以下のように指定します。
1 2 | BoxLayout: orientation: 'horizontal' |
しかし、このままでは、ペイント領域とボタン領域が1:1なので、 size_hint_y: で調整しています。
今回は、0.8と0.1にしました。
また、ボタンがクリックされたときの挙動は、 on_release: で関数を指定してあげます。
Keras (機械学習)部分
今回のアプリの要である、機械学習部分と識別部分を作成していきます。
イメージとしては、このような流れで作っていきます。
- ① アプリが立ち上がる
- ② 機械学習を始める
- ③ キャンパスが表示される
- ④ 手書き数字を学習済みネットワークに流す
ここでは、Kerasの学習部は別の learning.py というファイルに実装していきます。
それでは、まず、アプリを実行した後の「②機械学習を始める」の部分から考えていきたいと思います。
学習部分の大枠
アプリ実行時、すなわち最初に呼び出されるクラスのコンストラクタで機械学習を行い、そのネットワークをメンバ変数として保持しておきます。
したがって、以下のようにコードを加筆します。
1 | import learning # New! |
1 2 3 4 5 6 7 8 | class MyPaintApp(App): def __init__(self, **kwargs): super(MyPaintApp, self).__init__(**kwargs) self.title = '手書き数字認識テスト' # New! 学習を行う self.model = learning.learn_MNIST() |
このとき、 learning.py にMNISTデータセットを学習する learn_MNIST() という関数がある前提で加筆します。
したがって、このような関数を実際に learning.py に作っていきましょう!
ネットワークを作成
今回は、以下のようなネットワークを作成していきます。
【図 ネットワーク構成】
ここでDenseとは、全結合層を指し、Dropoutとは過学習を抑制するための手法の一つを指します。
簡単に説明すると、Dropoutは訓練パターンが入力される度にランダムでニューロンを非活性にし、そのニューロンと接続しているシナプス結合は学習を行わないという手法です。
こうすることにより、学習の自由度を下げて訓練パターンへの過学習を防ぐことができます。
活性化関数
また、今回のネットワークで用いるReLUと呼ばれる活性化関数は、以下のような形をしており、画像識別ではよく使われます。
【図 ReLU関数の概形】
最終層の活性化関数であるsoftmaxは、出力値の合計を1にする関数、つまり確率を出力にする関数です。
Kerasで実装
ひとまず、ここまでをKerasで実装してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | from keras.datasets import mnist from keras.utils.np_utils import to_categorical from keras.models import Sequential from keras.layers import Dense, Dropout def learn_MNIST(): # MNISTを読み込み (x_train, y_train), (x_test, y_test) = mnist.load_data() # 前処理 # データセットを入力次元784、計60,000個分のベクトルに変換 x_train = x_train.reshape(60000, 784) x_test = x_test.reshape(10000, 784) # 画素値を[0, 1]に正規化 x_train = x_train.astype('float32') / 255 x_test = x_test.astype('float32') / 255 # 教師信号をone-hot-encoding y_train = to_categorical(y_train, 10) y_test = to_categorical(y_test, 10) # ネットワーク生成 model = Sequential() # Sequential なモデル model.add(Dense(512, activation='relu', input_shape=(784,))) model.add(Dropout(0.2)) model.add(Dense(512, activation='relu')) model.add(Dropout(0.2)) model.add(Dense(10, activation='softmax')) |
このような形になりますが、教師信号のOne-hot-encodeingとはもともとのクラスラベルを[0, 1]の配列にすることを指します。
例えばクラスラベル「3」であれば、 [0, 0, 0, 1, 0, 0, 0, 0, 0, 0] のようになります。
この方が、機械学習では扱いやすいためです。
学習部分の実装
次に、学習部分を実装していきます。
ですがその前に、どのような目的関数を使い、どのような最適化手法を使うかを明示的に設定する必要があります。
今回は、最適化手法にAdamと呼ばれる学習の収束を早める手法を使い、目的関数はクロスエントロピーという関数を用いることにします。
- 最適化手法に「Adam」
- 目的関数は、「クロスエントロピー」という関数
その次に、実際に学習をしていくのですが、Kerasには、 fit() という関数一つで学習が行えます。
実装
これらの実装例は以下のようになります。
1 2 3 4 5 6 7 8 9 | model.compile(optimizer='adam', # 最適化手法 loss='categorical_crossentropy', # 目的(誤差, 損失)関数 metrics=['accuracy']) # 計測は識別率で # 学習 model.fit(x_train, y_train, batch_size=100, # バッチサイズ epochs=10, # 学習回数 verbose=1) |
これで学習部分の実装完了です。
ちなみに verbose は学習過程をログとして出力するかを指定しています。
0ならログなし、1ならプログレスバーを表示、2ならエポックごとにログを出力させます。
あとは、これによって作られたネットワーク(model)を戻り値として返すだけです。
ですが折角なので、テスト精度も表示させるようにしましょう。
1 2 3 4 | loss, accuracy = model.evaluate(x_test, y_test) # モデルの評価 print('Test Accuracy : ', accuracy) return model |
これで学習部分は完成です!
識別部分
最後に、キャンパスに描いた手書き数字をネットワークに流してネットワークの答えを出力させるだけです。
main.py の predict() を作っていきましょう!
流れ
- ① キャンパスを画像として保存
- ② Pillowでその画像を読み込み、不要な部分(ボタン類)を切り取る
- ③ グレースケール、28×28に変換
- ④ 画像を配列に変換
- ⑤ ネットワークに流す
実装
これらを実装してみます。
1 2 3 4 5 6 7 8 9 10 11 | def predict(self): self.painter.export_to_png('canvas.png') # 画像を一旦保存する # Pillowで読み込み、余計な部分を切り取る。またグレースケールにも変換 image = Image.open('canvas.png').crop((0, 0, 600, 600)).convert('L') # リサイズ image = image.resize((28, 28)) image = np.array(image) # 配列に変換 image = image.reshape(1, 784) # 1次元配列に変換 ans = self.model.predict(image) # ネットワークで出力を得る print('This Digit is ... ', np.argmax(ans)) # 一番大きい出力値のインデックスがネットワークの答え |
以上で実装は終わりです!
お疲れ様でした!
こちらの記事もオススメ!
関連記事
最終的なコード
main.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | # --- Kivy 関連 --- # from kivy.app import App from kivy.config import Config # Config関係は他のモジュールがインポートされる前に行う必要があるため、ここ記述 Config.set('graphics', 'width', '300') Config.set('graphics', 'height', '340') Config.write() from kivy.uix.widget import Widget from kivy.graphics import Color, Line from kivy.utils import get_color_from_hex from kivy.core.window import Window # ----------------- # from PIL import Image import numpy as np import learning class MyPaintWidget(Widget): line_width = 20 # 線の太さ color = get_color_from_hex('#ffffff') def on_touch_down(self, touch): if Widget.on_touch_down(self, touch): return with self.canvas: touch.ud['line'] = Line(points=(touch.x, touch.y), width=self.line_width) def on_touch_move(self, touch): touch.ud['line'].points += [touch.x, touch.y] def set_color(self): self.canvas.add(Color(*self.color)) class MyCanvasWidget(Widget): def clear_canvas(self): MyPaintWidget.clear_canvas(self) class MyPaintApp(App): def __init__(self, **kwargs): super(MyPaintApp, self).__init__(**kwargs) self.title = '手書き数字認識テスト' # 学習を行う self.model = learning.learn_MNIST() def build(self): self.painter = MyCanvasWidget() # 起動時の色の設定を行う self.painter.ids['paint_area'].set_color() return self.painter def clear_canvas(self): self.painter.ids['paint_area'].canvas.clear() self.painter.ids['paint_area'].set_color() # クリアした後に色を再びセット def predict(self): self.painter.export_to_png('canvas.png') # 画像を一旦保存する # Pillowで読み込み、余計な部分を切り取る。またグレースケールにも変換 image = Image.open('canvas.png').crop((0, 0, 600, 600)).convert('L') # リサイズ image = image.resize((28, 28)) image = np.array(image) image = image.reshape(1, 784) ans = self.model.predict(image) print('This Digit is ... ', np.argmax(ans)) if __name__ == '__main__': Window.clearcolor = get_color_from_hex('#000000') # ウィンドウの色を黒色に変更する MyPaintApp().run() |
MyPaint.kv
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <MyCanvasWidget>: paint_id:paint_area id: canvas_area BoxLayout: orientation: 'vertical' height: root.height width: root.width MyPaintWidget: id: paint_area size_hint_y: 0.8 BoxLayout: orientation: 'horizontal' size_hint_y: 0.1 Button: text: "Clear" ont_size: 30 on_release: app.clear_canvas() Button: text: 'Predict' color: 1, 1, 1 , 1 font_size: 30 on_release: app.predict() border: (2, 2, 2, 2) x: 0 top: root.top width: 80 height: 40 |
learning.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | from keras.datasets import mnist from keras.utils.np_utils import to_categorical from keras.models import Sequential from keras.layers import Dense, Dropout def learn_MNIST(): # MNISTを読み込み (x_train, y_train), (x_test, y_test) = mnist.load_data() # 前処理 # データセットを入力次元784、計60,000個分のベクトルに変換 x_train = x_train.reshape(60000, 784) x_test = x_test.reshape(10000, 784) # 画素値を[0, 1]に正規化 x_train = x_train.astype('float32') / 255 x_test = x_test.astype('float32') / 255 # 教師信号をone-hot-encoding y_train = to_categorical(y_train, 10) y_test = to_categorical(y_test, 10) # ネットワーク生成 model = Sequential() # Sequential なモデル model.add(Dense(512, activation='relu', input_shape=(784,))) model.add(Dropout(0.2)) model.add(Dense(512, activation='relu')) model.add(Dropout(0.2)) model.add(Dense(10, activation='softmax')) model.compile(optimizer='adam', # 最適化手法 loss='categorical_crossentropy', # 目的(誤差, 損失)関数 metrics=['accuracy']) # 計測は識別率で # 学習 model.fit(x_train, y_train, batch_size=100, # バッチサイズ epochs=10, # 学習回数 verbose=1) loss, accuracy = model.evaluate(x_test, y_test) print('Test Accuracy : ', accuracy) return model |
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン
ITエンタメ7月 14, 2023【クリス・ワンストラス】GitHubが出来るまでとソフトウェアの未来