
【前編】自作の誤差逆伝播学習法で手書き数字を認識させてみよう!【機械学習】
2021.12.20
前編〜手書き数字を認識するプログラムを作る~
誤差逆伝播学習法は、教師信号とネットワークの実際の出力との誤差情報と勾配降下法を用いてネットワークを学習させる代表的な機械学習手法です。
今回は、機械学習の要「誤差逆伝播学習法」を解説・実装してみる【人工知能】の記事で作成したコードを元に「手書き数字を認識するプログラム」を作ってみましょう!
この記事での最終的なプログラムのイメージとしては、以下のように、自分で書いた手書き数字をネットワークに判別させるといった具合です。
【成果物イメージ】
手書き数字データセット
今回用いるデータセットは、「UCI Optical Recognition of Handwritten Digits」と呼ばれる、8×8の小さな手書き数字データセットです。
よく見る28×28のMNISTデータセットと違い、小規模なので扱いやすいのが特徴です。
(※本当はMNISTを使うと学習に時間がかかってしまうからという理由もあります...)
UCI Optical Recognition of Handwritten Digits
それでは上記URLから、「optdigits.tra」(訓練データ)と「optdigits.tes」(テストデータ)をダウンロードして「OptDigits」というディレクトリに入れておきましょう!
それでは、実際にコードを加筆・修正していきましょう!
修正点1:データセット読み込み
まずは、データセットが前回とは異なるので、関数を書き換えましょう。
Optical Recognition of Handwritten Digits(以下OptDigits)は、3823枚の訓練画像、1797枚のテストデータからなります。
今回は訓練データ全てを使って学習させ、最後にテストデータ全てを使ってテスト精度を計測する形を取ろうかと思います。
また、学習が進むごとに訓練データを使って訓練精度を計測して学習進行具合も可視化してみましょう。
前回の「Irisデータセット」を読み込む関数と大きく違う部分は、2種類のデータセットを扱うことぐらいでしょうか。
また、OptDigitsは8×8で[0,16]の画素値が特徴パターンとなりますが、数値に差がないようにするために更に[0,1.0]の実数値に正規化して用いることにします。
以上の正規化は、ラムダ式を利用してコーディングしてみました。
(string型をfloat型に変更する必要もあるため)
【OptDigitsの特徴パターン】
実装
実装は以下のようになります。
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 | def load_optdigits(self): train = open('OptDigits/optdigits.tra', 'r') # 訓練データ test = open('OptDigits/optdigits.tes', 'r') # テストデータ # 訓練データ lines = train.read().split() # データをランダムにシャッフル random.shuffle(lines) dataset = ([]) for line in lines: pattern = line.split(',') dataset.append(pattern) train.close() for pat in dataset: # 入力は[0,1]に正規化する self.patterns.append(list(map(lambda x: float(x)*(1.0/16.0), pat[0:-1]))) # OptDigitsは最後にラベル[0,1,2,...,9]がある self.labels.append(int(pat[-1])) # テストデータ lines = test.read().split() # データをレンダムにシャッフル random.shuffle(lines) dataset = ([]) for line in lines: pattern = line.split(',') dataset.append(pattern) test.close() for pat in dataset: self.test_patterns.append(list(map(lambda x: float(x)*(1.0/16.0), pat[0:-1]))) # OptDigitsは最後にラベル[0,1,2,...,9]がある self.test_labels.append(int(pat[-1])) |
修正点2:自分で書いた手書き数字をネットワークに流すための関数
次に、自分で書いた手書き数字をネットワークに流すために、単純な画像処理を施す必要があります。
今回は、「おえかきボード - ブラウザでかんたんお絵かき -」というサイトで200×200のキャンパスに以下のように数字を書いて、pngファイルで保存した画像を使うことにしましょう。

【おえかきボードで手書き数字を書く】
このような方法ですので、もちろん自分で書いた手書き数字は200×200のサイズでネットワークには流せません。
したがって画像をリサイズします。
また、一応カラー画像扱いなので、グレースケールにも変換します。
これらの処理は、Pillow(PIL)という画像処理ライブラリを使います。
(未インストールであれば、pip等でインストールしてください。 pip install pillow )
まずは、ライブラリのインポートを書き加えます。
1 | from PIL import Image, ImageOps |
実装
そして、以下の工程を実装します。
1、画像のグレースケール化
2、画像のリサイズ
3、ネガポジ(白黒)反転
4、ネットワークに流すためにList化
ネガポジ変換する理由は、OptDigits(背景: 黒 数字: 白)に合わせるためです。
実装は以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def prop_my_digits(self, img_path): """ 自分で作った画像をネットワークに流して出力を得る関数。 :param img_path: :return: """ img = Image.open(img_path).convert('L') # グレースケールで画像を読み込む resized_img = img.resize((8, 8)) # 画像リサイズ input_img = ImageOps.invert(resized_img) # ネガポジ(白黒)反転 array = np.array(input_img) * (1.0/255.0) # [0,1]に変換 input_pattern = ([]) for h in array: for w in h: input_pattern.append(w) # 1次元の配列に変換 ans = np.array(self.forward(input_pattern)).argmax() # 出力値の大きいニューロンのインデックスを取得 print(img_path + ' is ', ans) # ネットワークの識別結果を出力 |
修正点3:部分修正
あとは少しだけコードを修正します。
まず、δを計算する calc_delta() で、教師ニューロンをIrisのときは3つでしたが、10クラス分に変更します。
1 2 | # teacher = ([0.1, 0.1, 0.1]) teacher = [0.1] * 10 # 10クラス分の教師ニューロンを作成 |
次に、 test() は検証(訓練)精度を計測する関数になるので、名前を validate() に変更します。
そして新たに、テスト精度を計測する test() 関数を作成します。
内容は、ほとんど変わりません。
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 | # 関数名を変更 def validate(self): """ 訓練精度を計算 :return: accuracy (%) """ correct = 0 for p in range(len(self.patterns)): self.forward(self.patterns[p]) max = 0 ans = -1 # 一番出力値の高いニューロンを取ってくる for o, out in enumerate(self.outputs[len(self.layers)-1]): if max < out: max = out ans = o # もしそのニューロンの番号とラベルの番号があっていれば正解! if ans == self.labels[p]: correct += 1 accuracy = correct / len(self.patterns) * 100 return accuracy # New! def test(self): """ テスト精度を計算 :return: accuracy (%) """ correct = 0 for p in range(len(self.test_patterns)): # テストパターン self.forward(self.test_patterns[p]) max = 0 ans = -1 # 一番出力値の高いニューロンを取ってくる for o, out in enumerate(self.outputs[len(self.layers)-1]): if max < out: max = out ans = o # もしそのニューロンの番号とラベルの番号があっていれば正解! if ans == self.test_labels[p]: correct += 1 accuracy = correct / len(self.patterns) * 100 return accuracy |
変更点は以上になります!
後編へつづく!
後編はこちら
こちらの記事もオススメ!
書いた人はこんな人

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