
【後編】自作の誤差逆伝播学習法で手書き数字を認識させてみよう!【機械学習】
2021.12.20
後編〜手書き数字を認識するプログラムを作る~
今回は、機械学習の要「誤差逆伝播学習法」を解説・実装してみる【人工知能】の記事で作成したコードを元に手書き数字を認識するプログラムを作ってみるの後半です!
前編はこちら
実際に手書き数字を認識させてみよう!
それでは、実際に動かして実験してみましょう!
実験は、以下のような実験条件とコード(main関数)で行なっています。
それぞれ処理ごとにコメントを書いているので参考にしてください。
ちなみにディレクトリツリーは、以下のようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | User:project User$ tree . ├── OptDigits │ ├── optdigits.tes │ └── optdigits.tra ├── main.py └── myDigits ├── 0.png ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── 9.png 2 directories, 13 files |
実験条件
初期学習率 | 0.1 |
学習率減衰 | ×0.5 / 10 epochs |
コード
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 | if __name__ == '__main__': # ネットワークを作成 net = Network(64, 100, 10) net.init_weights(-0.1, 0.1) # データセットを読み込み net.load_optdigits() # 誤差逆伝播学習法 net.train(50) # 最終的な訓練精度とテスト精度を出力 acc = net.validate() print('Training Accuracy: ' + str(acc) + '%') acc = net.test() print('Test Accuracy: ' + str(acc) + '%') # 自分の手書き数字を認識させてみる net.prop_my_digits('myDigits/0.png') net.prop_my_digits('myDigits/1.png') net.prop_my_digits('myDigits/2.png') net.prop_my_digits('myDigits/3.png') net.prop_my_digits('myDigits/4.png') net.prop_my_digits('myDigits/5.png') net.prop_my_digits('myDigits/6.png') net.prop_my_digits('myDigits/7.png') net.prop_my_digits('myDigits/8.png') net.prop_my_digits('myDigits/9.png') |
出力結果
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 | 1 / 50 epoch. [ error: 90.16479204812974 ] 2 / 50 epoch. [ error: 24.274130264190433 ] 3 / 50 epoch. [ error: 6.5655244572325415 ] 4 / 50 epoch. [ error: 5.283808527334557 ] 5 / 50 epoch. [ error: 4.786816636149609 ] 6 / 50 epoch. [ error: 4.4729270206644 ] 7 / 50 epoch. [ error: 4.1851948731362825 ] 8 / 50 epoch. [ error: 4.054407533350769 ] 9 / 50 epoch. [ error: 3.8451477896939537 ] 10 / 50 epoch. [ error: 3.6620455139942436 ] # --- 省略 --- # 46 / 50 epoch. [ error: 2.3541721161391536 ] 47 / 50 epoch. [ error: 2.3541721161391536 ] 48 / 50 epoch. [ error: 2.328014648182048 ] 49 / 50 epoch. [ error: 2.301857180224957 ] 50 / 50 epoch. [ error: 2.301857180224957 ] Training Accuracy: 97.69814281977504% Test Accuracy: 44.8339000784724% myDigits/0.png is 0 myDigits/1.png is 1 myDigits/2.png is 2 myDigits/3.png is 3 myDigits/4.png is 8 myDigits/5.png is 5 myDigits/6.png is 5 myDigits/7.png is 7 myDigits/8.png is 8 myDigits/9.png is 9 |
なんと訓練精度が約97.7%で、自分の手書き数字はいくつか間違えてはいるものの、大体予測ができています!
間違えた数字を見てみると、「6」を「5」と間違えたりと確かに形が似ている数字です。
また何度か実験してみるとわかりますが、「9」と「4」も比較的間違えやすいです。
なぜ完璧に識別できなかったのか?
いくつか原因が考えられますが、ひとつあげられるとしたら過学習(または過適合: Overfitting)していることが考えられます。
過学習
訓練データの精度は良くなるものの、未知のデータに対して精度が得られない現象
今回はテスト精度、つまり学習に使っていない未知データを使った精度も計算しています。
そのテスト精度は、「約44.8%」と高い精度とは到底言えません...。
また、学習過程での訓練精度とテスト精度を見てみてもテスト精度はなかなか伸びません。
【図 30エポックまでの訓練精度とテスト精度】
過学習の対策
過学習の簡単な対策としては、
- 学習データを増やす
- 良い学習データを使う
- 正則化項を誤差関数に加える
などがよく挙げられます。
1. 2. の学習データについては、今回8×8の小さな手書き数字データセットを用いたので、文字が潰れかけていて、普段私たちが書くような数字とは少しかけ離れています。
したがって手書き数字データセットは28×28の比較的解像度が良くデータ数の多いMNISTデータセットがよく用いられます。
3. の正則化項については、過学習に陥ると重みが大きな値に発散してしまう現象がよく見られます。
【図 大きな値に分散した重みが見られる例】
この現象から、重みが大きな値に発散しないように学習を進める手法があります。
他にも、Dropoutなど過学習を抑制する手法はいくつかありますがここでは割愛します。
実験に時間がかかる上にコーディングが複雑に
これも今回の実験でわかりますが、ネットワークが大きくなるほど、膨大な計算時間がかかります。
また、先のDropoutや正則化項などの新たな手法を加えていくほどコーディングは複雑になってきます。
自分でコードを書くことは大切ですが、最適なコードをかかないと無駄な処理が積み重なって時間がかかってしまい、トータルで見て時間と労力の無駄になることも多々あります。
フレームワーク
そこで、機械学習にはたくさんのフレームワークが提供されています。
有名どころであれば、「Tensorflow」や「Keras」などが挙げられます。
これらは、基本的にPythonで動作します。
それゆえに「機械学習といえばPython」と言われているのです。
なぜ、同じPythonなのに処理速度に大きな差が出るかというと、Pythonで動く機械学習フレームワークの処理自体はCやC++で書かれているからです。
さいごに
今回は、自作の機械学習プログラムを使って、少し実用的な実験をしてみました。
実装を通してわかるように、シンプルな機械学習であればゼロから構築することはさほど難しい話ではありません。
しかし、ネットワークを大きくしたり、新たな機能を加えていくと一筋縄ではいかなくなってきます。
ですので、ある程度機械学習のしくみを理解できたら、既存の優秀なフレームワークに移行して快適な機械学習ライフに切り替えてみましょう!
また別の記事で、フレームワークを使った機械学習についてお話しできればと思います。
前編はこちら
こちらの記事もオススメ!
コード全体
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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 | import numpy as np import matplotlib.pyplot as plt from PIL import Image, ImageOps # New! import random # New! class Network: rate = 0.1 # 学習率 decay = 0.1 # 学習率減衰 per = 10 # 何エポックごとに学習率を減衰させるか epsilon = 1.0 # シグモイド関数の傾き def __init__(self, *args): self.layers = list(args) # 各層のニューロン数 self.weights = ([]) # 各層間の重みベクトル self.patterns = ([]) # 訓練パターンベクトル self.labels = ([]) # 訓練パターンの教師ニューロンインデックス # --- New! --- # self.test_patterns = ([]) # テストパターンベクトル self.test_labels = ([]) # テストパターンの教師ニューロンインデックス # ------------ # self.outputs = ([]) # 各ニューロンの出力値ベクトル def init_weights(self, a=0.0, b=1.0): """ 重みを[a, b]の一様乱数で生成 :param a: minimum = 0.0 :param b: maximum = 1.0 :return: """ for i in range(len(self.layers)): if i + 1 >= len(self.layers): break self.weights.append((b - a) * np.random.rand(self.layers[i + 1], self.layers[i]) + a) # New! 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])) @staticmethod def input_sum(inputs, weights): """ 前ニューロンからの入力和を返す関数.式(1) :param inputs: :param weights: :return inputs dot weights (float): """ return np.dot(inputs, weights) def output(self, inputs, weights): """ ニューロンの出力を返す関数.式(2) :param inputs: :param weights: :return (0, 1): """ return self.sigmoid(self.input_sum(inputs, weights)) def sigmoid(self, x): """ シグモイド関数 式(3) :param x: :return: """ return 1.0/(1.0 + np.exp(-self.epsilon * x)) def forward(self, pattern): """ 順方向処理をする関数. :param pattern: :return 出力層ニューロンの出力値ベクトル [0,1]: """ self.outputs.clear() # まず出力値情報をクリア out = ([]) # 前層ニューロンの出力ベクトル self.outputs.append(pattern) # まず入力層ニューロンの出力(入力パターン)を追加 for layer in range(len(self.layers)): out = ([]) if layer == 1: # 第1中間層ならば入力パターンが前層ニューロンの出力となる for n in range(self.layers[layer]): out.append(self.output(pattern, self.weights[layer-1][n])) self.outputs.append(out) # 出力値を追加 elif layer > 1: # 第1中間層以降の層では前層の出力を使う for n in range(self.layers[layer]): out.append(self.output(self.outputs[-1], self.weights[layer-1][n])) self.outputs.append(out) # 出力値を追加 return out def backward(self, pattern): """ 誤差逆伝播学習法の大枠 :param pattern: :return: """ deltas = self.calc_delta(pattern) # δを計算 for l in reversed(range(1, len(self.layers))): # ネットワークを逆順処理していく for j, jj in enumerate(self.outputs[l]): for i, ii in enumerate(self.outputs[l-1]): self.weights[l-1][j][i] += self.rate * deltas[l-1][j] * ii # 重みを更新 # 一部変更 def calc_delta(self, pattern): """ δを計算する関数 :param pattern: パターンインデックス :return: δベクトル """ teacher = [0.1] * 10 # 10クラス分の教師ニューロンを作成 teacher[self.labels[pattern]] = 0.9 # ラベルに該当する教師ニューロンの出力を0.9に deltas = ([]) # 全ニューロンのδ for l in reversed(range(1, len(self.layers))): # ネットワークを逆順処理していく tmp_delta = ([]) for j in range(self.layers[l]): if l == len(self.layers) - 1: # 最終層ならば delta = self.epsilon * \ (teacher[j] - self.outputs[l][j]) * \ self.outputs[l][j] * (1 - self.outputs[l][j]) tmp_delta.append(delta) else: # 最終層以外 t_weights = np.array(self.weights[l]).T delta = self.epsilon * \ self.outputs[l][j] * (1 - self.outputs[l][j]) * \ np.dot(deltas[-1], t_weights[j]) tmp_delta.append(delta) deltas.append(tmp_delta) return deltas[::-1] # δは逆順で返す def train(self, epoch): """ 訓練関数 :param epoch: :return: """ trains = ([]) tests = ([]) for e in range(epoch): # 最大学習回数で回す train_acc = self.validate() test_acc = self.test() for p, pat in enumerate(self.patterns): # 入力パターンごとに回す self.forward(pat) # 順伝播処理で出力値を計算 self.backward(p) # 逆伝播で重みを更新 print(str(e+1) + ' / ' + str(epoch) + ' epoch.') print('[ Training Acc.: ' + str(train_acc), ', Test Acc.: ' + str(test_acc) + ' ]') if (e+1) % self.per == 0: # 学習率減衰 self.rate *= self.decay trains.append(train_acc) # 訓練精度 tests.append(test_acc) # テスト精度 # 精度を描画 plt.xlabel('epochs') plt.ylabel('accuracy (%)') plt.plot(trains, label='Train') plt.plot(tests, label='Test') plt.legend() plt.savefig('error.png', dpi=300) plt.close() # 重みのヒストグラムを描画 ws = ([]) for lw in self.weights: for iw in lw: for w in iw: ws.append(w) plt.hist(ws, bins=100, ec='black') plt.xlabel('Weights') plt.ylabel('Frequency') plt.savefig('weights_hist.png', dpi=300) # 関数名を変更 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 # New! 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]に変換 # リサイズした画像を保存して確認する場合 # plt.imshow(array) # name = img_path + '_resize.png' # plt.savefig(name, dpi=300) 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) # ネットワークの識別結果を出力 if __name__ == '__main__': # ネットワークを作成 net = Network(64, 10) net.init_weights(-0.2, 0.2) # データセットを読み込み net.load_optdigits() # 誤差逆伝播学習法 net.train(30) # 最終的な訓練精度とテスト精度を出力 acc = net.validate() print('Training Accuracy: ' + str(acc) + '%') acc = net.test() print('Test Accuracy: ' + str(acc) + '%') # 自分の手書き数字を認識させてみる net.prop_my_digits('myDigits/0.png') net.prop_my_digits('myDigits/1.png') net.prop_my_digits('myDigits/2.png') net.prop_my_digits('myDigits/3.png') net.prop_my_digits('myDigits/4.png') net.prop_my_digits('myDigits/5.png') net.prop_my_digits('myDigits/6.png') net.prop_my_digits('myDigits/7.png') net.prop_my_digits('myDigits/8.png') net.prop_my_digits('myDigits/9.png') |
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の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が出来るまでとソフトウェアの未来