【後編】自作の誤差逆伝播学習法で手書き数字を認識させてみよう!【機械学習】
IT技術
後編〜手書き数字を認識するプログラムを作る~
今回は、機械学習の要「誤差逆伝播学習法」を解説・実装してみる【人工知能】の記事で作成したコードを元に手書き数字を認識するプログラムを作ってみるの後半です!
前編はこちら
実際に手書き数字を認識させてみよう!
それでは、実際に動かして実験してみましょう!
実験は、以下のような実験条件とコード(main関数)で行なっています。
それぞれ処理ごとにコメントを書いているので参考にしてください。
ちなみにディレクトリツリーは、以下のようになっています。
1User:project User$ tree
2.
3├── OptDigits
4│ ├── optdigits.tes
5│ └── optdigits.tra
6├── main.py
7└── myDigits
8 ├── 0.png
9 ├── 1.png
10 ├── 2.png
11 ├── 3.png
12 ├── 4.png
13 ├── 5.png
14 ├── 6.png
15 ├── 7.png
16 ├── 8.png
17 └── 9.png
18
192 directories, 13 files
実験条件
初期学習率 | 0.1 |
学習率減衰 | ×0.5 / 10 epochs |
コード
1if __name__ == '__main__':
2 # ネットワークを作成
3 net = Network(64, 100, 10)
4 net.init_weights(-0.1, 0.1)
5
6 # データセットを読み込み
7 net.load_optdigits()
8
9 # 誤差逆伝播学習法
10 net.train(50)
11
12 # 最終的な訓練精度とテスト精度を出力
13 acc = net.validate()
14 print('Training Accuracy: ' + str(acc) + '%')
15 acc = net.test()
16 print('Test Accuracy: ' + str(acc) + '%')
17
18 # 自分の手書き数字を認識させてみる
19 net.prop_my_digits('myDigits/0.png')
20 net.prop_my_digits('myDigits/1.png')
21 net.prop_my_digits('myDigits/2.png')
22 net.prop_my_digits('myDigits/3.png')
23 net.prop_my_digits('myDigits/4.png')
24 net.prop_my_digits('myDigits/5.png')
25 net.prop_my_digits('myDigits/6.png')
26 net.prop_my_digits('myDigits/7.png')
27 net.prop_my_digits('myDigits/8.png')
28 net.prop_my_digits('myDigits/9.png')
出力結果
11 / 50 epoch. [ error: 90.16479204812974 ]
22 / 50 epoch. [ error: 24.274130264190433 ]
33 / 50 epoch. [ error: 6.5655244572325415 ]
44 / 50 epoch. [ error: 5.283808527334557 ]
55 / 50 epoch. [ error: 4.786816636149609 ]
66 / 50 epoch. [ error: 4.4729270206644 ]
77 / 50 epoch. [ error: 4.1851948731362825 ]
88 / 50 epoch. [ error: 4.054407533350769 ]
99 / 50 epoch. [ error: 3.8451477896939537 ]
1010 / 50 epoch. [ error: 3.6620455139942436 ]
11# --- 省略 --- #
1246 / 50 epoch. [ error: 2.3541721161391536 ]
1347 / 50 epoch. [ error: 2.3541721161391536 ]
1448 / 50 epoch. [ error: 2.328014648182048 ]
1549 / 50 epoch. [ error: 2.301857180224957 ]
1650 / 50 epoch. [ error: 2.301857180224957 ]
17Training Accuracy: 97.69814281977504%
18Test Accuracy: 44.8339000784724%
19myDigits/0.png is 0
20myDigits/1.png is 1
21myDigits/2.png is 2
22myDigits/3.png is 3
23myDigits/4.png is 8
24myDigits/5.png is 5
25myDigits/6.png is 5
26myDigits/7.png is 7
27myDigits/8.png is 8
28myDigits/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++で書かれているからです。
さいごに
今回は、自作の機械学習プログラムを使って、少し実用的な実験をしてみました。
実装を通してわかるように、シンプルな機械学習であればゼロから構築することはさほど難しい話ではありません。
しかし、ネットワークを大きくしたり、新たな機能を加えていくと一筋縄ではいかなくなってきます。
ですので、ある程度機械学習のしくみを理解できたら、既存の優秀なフレームワークに移行して快適な機械学習ライフに切り替えてみましょう!
また別の記事で、フレームワークを使った機械学習についてお話しできればと思います。
前編はこちら
こちらの記事もオススメ!
2020.07.28機械学習 特集知識編人工知能・機械学習でよく使われるワード徹底まとめ!機械学習の元祖「パーセプトロン」とは?【人工知能】ニューラルネ...
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
コード全体
1import numpy as np
2import matplotlib.pyplot as plt
3
4from PIL import Image, ImageOps # New!
5import random # New!
6
7
8class Network:
9 rate = 0.1 # 学習率
10 decay = 0.1 # 学習率減衰
11 per = 10 # 何エポックごとに学習率を減衰させるか
12
13 epsilon = 1.0 # シグモイド関数の傾き
14
15 def __init__(self, *args):
16 self.layers = list(args) # 各層のニューロン数
17 self.weights = ([]) # 各層間の重みベクトル
18 self.patterns = ([]) # 訓練パターンベクトル
19 self.labels = ([]) # 訓練パターンの教師ニューロンインデックス
20
21 # --- New! --- #
22 self.test_patterns = ([]) # テストパターンベクトル
23 self.test_labels = ([]) # テストパターンの教師ニューロンインデックス
24 # ------------ #
25
26 self.outputs = ([]) # 各ニューロンの出力値ベクトル
27
28 def init_weights(self, a=0.0, b=1.0):
29 """
30 重みを[a, b]の一様乱数で生成
31 :param a: minimum = 0.0
32 :param b: maximum = 1.0
33 :return:
34 """
35 for i in range(len(self.layers)):
36 if i + 1 >= len(self.layers):
37 break
38 self.weights.append((b - a) * np.random.rand(self.layers[i + 1], self.layers[i]) + a)
39
40 # New!
41 def load_optdigits(self):
42 train = open('OptDigits/optdigits.tra', 'r') # 訓練データ
43 test = open('OptDigits/optdigits.tes', 'r') # テストデータ
44
45 # 訓練データ
46 lines = train.read().split()
47
48 # データをランダムにシャッフル
49 random.shuffle(lines)
50
51 dataset = ([])
52 for line in lines:
53 pattern = line.split(',')
54 dataset.append(pattern)
55 train.close()
56
57 for pat in dataset:
58 # 入力は[0,1]に正規化する
59 self.patterns.append(list(map(lambda x: float(x)*(1.0/16.0), pat[0:-1])))
60
61 # OptDigitsは最後にラベル[0,1,2,...,9]がある
62 self.labels.append(int(pat[-1]))
63
64 # テストデータ
65 lines = test.read().split()
66
67 # データをレンダムにシャッフル
68 random.shuffle(lines)
69
70 dataset = ([])
71 for line in lines:
72 pattern = line.split(',')
73 dataset.append(pattern)
74 test.close()
75
76 for pat in dataset:
77 self.test_patterns.append(list(map(lambda x: float(x)*(1.0/16.0), pat[0:-1])))
78
79 # OptDigitsは最後にラベル[0,1,2,...,9]がある
80 self.test_labels.append(int(pat[-1]))
81
82 @staticmethod
83 def input_sum(inputs, weights):
84 """
85 前ニューロンからの入力和を返す関数.式(1)
86 :param inputs:
87 :param weights:
88 :return inputs dot weights (float):
89 """
90 return np.dot(inputs, weights)
91
92 def output(self, inputs, weights):
93 """
94 ニューロンの出力を返す関数.式(2)
95 :param inputs:
96 :param weights:
97 :return (0, 1):
98 """
99 return self.sigmoid(self.input_sum(inputs, weights))
100
101 def sigmoid(self, x):
102 """
103 シグモイド関数 式(3)
104 :param x:
105 :return:
106 """
107 return 1.0/(1.0 + np.exp(-self.epsilon * x))
108
109 def forward(self, pattern):
110 """
111 順方向処理をする関数.
112 :param pattern:
113 :return 出力層ニューロンの出力値ベクトル [0,1]:
114 """
115 self.outputs.clear() # まず出力値情報をクリア
116
117 out = ([]) # 前層ニューロンの出力ベクトル
118 self.outputs.append(pattern) # まず入力層ニューロンの出力(入力パターン)を追加
119
120 for layer in range(len(self.layers)):
121 out = ([])
122 if layer == 1: # 第1中間層ならば入力パターンが前層ニューロンの出力となる
123 for n in range(self.layers[layer]):
124 out.append(self.output(pattern, self.weights[layer-1][n]))
125
126 self.outputs.append(out) # 出力値を追加
127
128 elif layer > 1: # 第1中間層以降の層では前層の出力を使う
129 for n in range(self.layers[layer]):
130 out.append(self.output(self.outputs[-1], self.weights[layer-1][n]))
131
132 self.outputs.append(out) # 出力値を追加
133
134 return out
135
136 def backward(self, pattern):
137 """
138 誤差逆伝播学習法の大枠
139 :param pattern:
140 :return:
141 """
142 deltas = self.calc_delta(pattern) # δを計算
143 for l in reversed(range(1, len(self.layers))): # ネットワークを逆順処理していく
144 for j, jj in enumerate(self.outputs[l]):
145 for i, ii in enumerate(self.outputs[l-1]):
146 self.weights[l-1][j][i] += self.rate * deltas[l-1][j] * ii # 重みを更新
147
148 # 一部変更
149 def calc_delta(self, pattern):
150 """
151 δを計算する関数
152 :param pattern: パターンインデックス
153 :return: δベクトル
154 """
155 teacher = [0.1] * 10 # 10クラス分の教師ニューロンを作成
156 teacher[self.labels[pattern]] = 0.9 # ラベルに該当する教師ニューロンの出力を0.9に
157 deltas = ([]) # 全ニューロンのδ
158
159 for l in reversed(range(1, len(self.layers))): # ネットワークを逆順処理していく
160 tmp_delta = ([])
161 for j in range(self.layers[l]):
162 if l == len(self.layers) - 1: # 最終層ならば
163 delta = self.epsilon * \
164 (teacher[j] - self.outputs[l][j]) * \
165 self.outputs[l][j] * (1 - self.outputs[l][j])
166
167 tmp_delta.append(delta)
168
169 else: # 最終層以外
170 t_weights = np.array(self.weights[l]).T
171 delta = self.epsilon * \
172 self.outputs[l][j] * (1 - self.outputs[l][j]) * \
173 np.dot(deltas[-1], t_weights[j])
174
175 tmp_delta.append(delta)
176
177 deltas.append(tmp_delta)
178
179 return deltas[::-1] # δは逆順で返す
180
181 def train(self, epoch):
182 """
183 訓練関数
184 :param epoch:
185 :return:
186 """
187 trains = ([])
188 tests = ([])
189 for e in range(epoch): # 最大学習回数で回す
190 train_acc = self.validate()
191 test_acc = self.test()
192 for p, pat in enumerate(self.patterns): # 入力パターンごとに回す
193
194 self.forward(pat) # 順伝播処理で出力値を計算
195
196 self.backward(p) # 逆伝播で重みを更新
197
198 print(str(e+1) + ' / ' + str(epoch) + ' epoch.')
199 print('[ Training Acc.: ' + str(train_acc), ', Test Acc.: ' + str(test_acc) + ' ]')
200
201 if (e+1) % self.per == 0: # 学習率減衰
202 self.rate *= self.decay
203
204 trains.append(train_acc) # 訓練精度
205 tests.append(test_acc) # テスト精度
206
207 # 精度を描画
208 plt.xlabel('epochs')
209 plt.ylabel('accuracy (%)')
210 plt.plot(trains, label='Train')
211 plt.plot(tests, label='Test')
212 plt.legend()
213 plt.savefig('error.png', dpi=300)
214 plt.close()
215
216 # 重みのヒストグラムを描画
217 ws = ([])
218 for lw in self.weights:
219 for iw in lw:
220 for w in iw:
221 ws.append(w)
222 plt.hist(ws, bins=100, ec='black')
223 plt.xlabel('Weights')
224 plt.ylabel('Frequency')
225 plt.savefig('weights_hist.png', dpi=300)
226
227 # 関数名を変更
228 def validate(self):
229 """
230 訓練精度を計算
231 :return: accuracy (%)
232 """
233 correct = 0
234 for p in range(len(self.patterns)):
235 self.forward(self.patterns[p])
236 max = 0
237 ans = -1
238 # 一番出力値の高いニューロンを取ってくる
239 for o, out in enumerate(self.outputs[len(self.layers)-1]):
240 if max < out:
241 max = out
242 ans = o
243 # もしそのニューロンの番号とラベルの番号があっていれば正解!
244 if ans == self.labels[p]:
245 correct += 1
246
247 accuracy = correct / len(self.patterns) * 100
248 return accuracy
249
250 # New!
251 def test(self):
252 """
253 テスト精度を計算
254 :return: accuracy (%)
255 """
256 correct = 0
257 for p in range(len(self.test_patterns)):
258 self.forward(self.test_patterns[p])
259 max = 0
260 ans = -1
261 # 一番出力値の高いニューロンを取ってくる
262 for o, out in enumerate(self.outputs[len(self.layers)-1]):
263 if max < out:
264 max = out
265 ans = o
266 # もしそのニューロンの番号とラベルの番号があっていれば正解!
267 if ans == self.test_labels[p]:
268 correct += 1
269
270 accuracy = correct / len(self.patterns) * 100
271 return accuracy
272
273 # New!
274 def prop_my_digits(self, img_path):
275 """
276 自分で作った画像をネットワークに流して出力を得る関数。
277 :param img_path:
278 :return:
279 """
280 img = Image.open(img_path).convert('L') # グレースケールで画像を読み込む
281 resized_img = img.resize((8, 8)) # 画像リサイズ
282 input_img = ImageOps.invert(resized_img) # ネガポジ(白黒)反転
283 array = np.array(input_img) * (1.0/255.0) # [0,1]に変換
284
285 # リサイズした画像を保存して確認する場合
286 # plt.imshow(array)
287 # name = img_path + '_resize.png'
288 # plt.savefig(name, dpi=300)
289
290 input_pattern = ([])
291 for h in array:
292 for w in h:
293 input_pattern.append(w) # 1次元の配列に変換
294 ans = np.array(self.forward(input_pattern)).argmax() # 出力値の大きいニューロンのインデックスを取得
295 print(img_path + ' is ', ans) # ネットワークの識別結果を出力
296
297
298if __name__ == '__main__':
299 # ネットワークを作成
300 net = Network(64, 10)
301 net.init_weights(-0.2, 0.2)
302
303 # データセットを読み込み
304 net.load_optdigits()
305
306 # 誤差逆伝播学習法
307 net.train(30)
308
309 # 最終的な訓練精度とテスト精度を出力
310 acc = net.validate()
311 print('Training Accuracy: ' + str(acc) + '%')
312 acc = net.test()
313 print('Test Accuracy: ' + str(acc) + '%')
314
315 # 自分の手書き数字を認識させてみる
316 net.prop_my_digits('myDigits/0.png')
317 net.prop_my_digits('myDigits/1.png')
318 net.prop_my_digits('myDigits/2.png')
319 net.prop_my_digits('myDigits/3.png')
320 net.prop_my_digits('myDigits/4.png')
321 net.prop_my_digits('myDigits/5.png')
322 net.prop_my_digits('myDigits/6.png')
323 net.prop_my_digits('myDigits/7.png')
324 net.prop_my_digits('myDigits/8.png')
325 net.prop_my_digits('myDigits/9.png')
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit