はじめに
「プログラミング」初学者のための『ゼロから作るDeep Learning』攻略ノートです。『ゼロつくシリーズ』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
関数やクラスとして実装される処理の塊を細かく分解して、1つずつ実行結果を見ながら処理の意図を確認していきます。
この記事は、4.5.2項「ミニバッチ学習の実装」と4.5.3項「テストデータで評価」の内容です。前項で実装した2層のニューラルネットワークのクラスを用いて、学習を行い損失と認識精度の推移を確認します。
【前節の内容】
【他の節の内容】
【この節の内容】
4.2.3 ミニバッチデータの抽出
この項では、データセットからミニバッチデータを取り出す処理を確認します。
利用するライブラリを読み込みます。
# 4.2.3項で利用するライブラリ import numpy as np
・処理の確認
データセットの代わりに分かりやすい配列を作成しておきます。
# (仮の)データを作成 x = np.arange(1000).reshape(100, 10) print(x[:5]) # 前から5行 print(x[95:]) # 後から5行 print(x.shape)
[[ 0 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]]
[[950 951 952 953 954 955 956 957 958 959]
[960 961 962 963 964 965 966 967 968 969]
[970 971 972 973 974 975 976 977 978 979]
[980 981 982 983 984 985 986 987 988 989]
[990 991 992 993 994 995 996 997 998 999]]
(100, 10)
3
行目の要素は、30
から39
なので、3n
の形になっておりn
の部分が0
から9
までです。0
行目であれば(0は見えませんが)0n
で、95
行目であれば95n
ですね(分かりやすいはず?)。
ミニバッチデータとして利用するデータ番号(インデックス)をランダムに生成します。
# インデックスをランダムに生成 idx = np.random.choice(100, size=10, replace=False) print(idx)
[96 57 75 82 60 51 78 29 11 21]
np.random.choice()
は、0から第1引数の値未満の整数をランダムに返します。
つまり、第1引数に総データ数、size
引数(第2引数)にバッチサイズを指定することで、バッチサイズ個のインデックスをランダムに生成できます。
idx
を添字として使って、対応するデータ(行)を抽出します。
# ミニバッチデータを抽出 x_batch = x[idx] print(x_batch) print(x_batch.shape)
[[960 961 962 963 964 965 966 967 968 969]
[570 571 572 573 574 575 576 577 578 579]
[750 751 752 753 754 755 756 757 758 759]
[820 821 822 823 824 825 826 827 828 829]
[600 601 602 603 604 605 606 607 608 609]
[510 511 512 513 514 515 516 517 518 519]
[780 781 782 783 784 785 786 787 788 789]
[290 291 292 293 294 295 296 297 298 299]
[110 111 112 113 114 115 116 117 118 119]
[210 211 212 213 214 215 216 217 218 219]]
(10, 10)
実際には、入力データと教師データから同じインデックスのデータを取り出す必要があります。
replace
引数にFalse
を指定すると、重複せずに値を生成します。True
とすると重複を許します。
# 整数をランダムに生成 print(np.random.choice(10, size=10)) # 重複を許す print(np.random.choice(10, size=10, replace=False)) # 重複しない
[1 6 5 1 6 9 0 7 8 3]
[9 1 5 8 0 2 4 7 6 3]
デフォルトはTrue
なので、replace
引数を指定しなければ値が重複することがあります。
第1引数と第2引数が同じ値でありreplace=False
であれば全ての値を生成できます。しかし、100個のデータから10個取り出す操作を10回行っても全てのデータを取り出せるわけではありません。
# 繰り返し処理 for i in range(10): print(np.random.choice(100, size=10))
[15 10 13 84 45 73 67 0 68 88]
[90 70 89 35 27 27 2 53 38 65]
[17 0 2 23 51 91 8 23 56 7]
[75 18 1 15 1 54 90 56 0 93]
[21 4 1 4 56 23 82 94 56 68]
[45 77 57 67 61 84 41 83 69 79]
[92 14 75 67 51 93 93 83 27 21]
[91 34 20 77 66 65 68 60 4 44]
[58 27 58 23 68 88 32 32 45 95]
[43 1 58 14 24 39 60 11 76 44]
なので、ミニバッチ学習において数エポック分の学習を行っても全てのデータを利用したとは限りません。
以上で、ミニバッチデータの取得方法を確認できました。4.5.2項以降で利用します。
4.5.2 ミニバッチ学習
前項で実装した2層のニューラルネットワークのクラスを用いて、手書き数字の認識の学習を行います。バッチ処理(一部のデータを順番に取り出す)については3.6.3項を、ミニバッチ学習(一部のデータをランダムに取り出す)については4.2.3項を参照してください。
利用するライブラリを読み込みます。
# 4.5.2項で利用するライブラリ import numpy as np import matplotlib.pyplot as plt import time # 処理時間の計測用
また、MNISTデータセットの読み込みに利用する関数load_mnist()
を読み込みます。詳しくは「3.6.1:MNISTデータセットの読み込み【ゼロつく1のノート(Python)】 - からっぽのしょこ」を参照してください。
# 読み込み用の設定 import sys sys.path.append('../deep-learning-from-scratch-master') # MNISTデータセット読み込み関数を読み込み from dataset.mnist import load_mnist #from ch04.two_layer_net import TwoLayerNet # 2層のニューラルネットワーク:4.5.1項
「4.5.1:2層ニューラルネットワークのクラス【ゼロつく1のノート(実装)】 - からっぽのしょこ」で実装した2層のニューラルネットワークのクラスTwoLayerNet
の定義を再度実行する必要があります。上の方法で読み込んだ場合は、誤差逆伝播法によって勾配を計算するメソッドを利用できます。
・データセットの用意
load_mnist()
を使って、手書き数字の画像データとラベルデータを読み込みます。この例では、正規化normalize
、1次元化flatten
、one-hot表現化one_hot_label
の全ての引数をTrue
とします。
# MNISTデータセットを取得 (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=True) print(x_train.shape) # 訓練画像 print(t_train.shape) # 訓練ラベル
(60000, 784)
(60000, 10)
6万枚の画像データから、繰り返しバッチサイズ分のデータをランダムに取り出して学習を行います。
・ミニバッチ学習
ミニバッチデータに対する試行回数をiters_num
、1回の学習に利用するデータ数をbatch_size
、学習率(パラメータを更新する際の調整項)をlearning_rate
として値を指定します。また、総データ数をtrain_size
とします。
# バッチデータ当たりの試行回数を指定 iters_num = 600 # バッチサイズ(1試行当たりのデータ数)を指定 batch_size = 100 # データサイズ(総データ数)を取得 train_size = x_train.shape[0] print(train_size) # 学習率を指定 learning_rate = 0.1
60000
この例では、試行回数を600、バッチサイズを100としました。合計60000枚(1エポック)のデータを使うことになりますが、使用するデータはランダムに選ぶので、全てのデータを使うわけではありません。
ミニバッチ学習を行います。データセットx_train, t_train
からランダムにbatch_size
個取り出したミニバッチデータx_batch, t_batch
を使って学習します。
# 2層のニューラルネットワークのインスタンスを作成 network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10) # 損失の記録用のリストを作成 train_loss_list = [] # ミニバッチ学習 for i in range(iters_num): # 開始時間を記録 start_time = time.time() # ミニバッチデータを取得 batch_mask = np.random.choice(train_size, size=batch_size, replace=False) # データ番号を生成 x_batch = x_train[batch_mask] # 入力データ t_batch = t_train[batch_mask] # 教師データ # 勾配を計算 grad = network.numerical_gradient(x_batch, t_batch) # パラメータを更新 for key in ('W1', 'b1', 'W2', 'b2'): # 勾配降下法により値を更新:式(4.7) network.params[key] -= learning_rate * grad[key] # 損失を計算 loss = network.loss(x_batch, t_batch) # 損失の値を記録 train_loss_list.append(loss) # 途中経過を表示 print( 'iter ' + str(i + 1) + ' (' + str(np.round((i + 1) / iters_num * 100, 1)) + '%) : ' + # 繰り返し回数 str(np.round(time.time() - start_time, 1)) + '[s]' # 処理時間 )
iter 1 (0.2%) : 30.3[s]
iter 2 (0.3%) : 28.2[s]
iter 3 (0.5%) : 28.1[s]
iter 4 (0.7%) : 27.0[s]
iter 5 (0.8%) : 27.4[s]
(省略)
iter 596 (99.3%) : 26.8[s]
iter 597 (99.5%) : 27.4[s]
iter 598 (99.7%) : 26.9[s]
iter 599 (99.8%) : 27.2[s]
iter 600 (100.0%) : 26.8[s]
4.5.2項で実装した2層のニューラルネットワークのクラスTwoLayerNet
のインスタンスを作成します。
入力データの要素数input_size
はピクセルデータ数の784、クラス数output_size
は数字の種類数の10にするのでした。中間層のニューロン数hidden_size
は自由に決められます。ここでは50とします。この値が大きいほどニューラルネットワーク全体のパラメータの要素数が多くなります。パラメータの数が多いほどモデルの表現力が広がりますが、その分過学習の可能性も高まってしまいます。
各パラメータの更新では、for
文を使ってキー名'W1', 'b1', 'W2', 'b2'
を順番にkey
として、ディクショナリ型のインスタンス変数params
とgrad
から各パラメータの値(配列)を取り出して勾配降下法により値を更新しています。
ミニバッチデータの抽出については4.2.3項を、勾配降下法については4.4.1項を、TwoLayerNet
クラスのメソッドやインスタンス変数については4.5.1項を参照してください。
学習状況を確認するために、試行ごとに損失の値をリストtrain_loss_list
に記録していきます。append()
メソッドでリストに値を追加できます。
この学習処理(数値微分を伴う処理)にはすごく時間がかかります(私の環境だと1回当たり30秒弱でした)。そこで、進行具合と時間経過を表示する処理を加えています。なくても学習自体には影響しません。いきなり1万回は止ておいた方がいいと思います。この例では、学習に利用するデータ数が1エポック分となる600回にしました。
JupyterLabであれば、ツールバーの「Kernel」→「Interrupt Kernel」で処理を中断できます。
・結果の確認
損失(交差エントロピー誤差)の推移をプロットします。ただし、ある程度の回数の更新を行わないと学習が進んでいることを確認できません…
# 損失の推移を作図 plt.figure(figsize=(8, 6)) # 図の設定 plt.plot(np.arange(1, iters_num + 1), train_loss_list) # 折れ線グラフ plt.xlabel('iteration') # x軸ラベル plt.ylabel('loss') # y軸ラベル plt.title('Cross Entropy Error', fontsize=20) # タイトル plt.grid() # グリッド線 plt.show()
試行回数が増えるに従って、損失が下がっているのを確認できます。つまり、学習が進んでいるのが分かります。
この項では、MNISTデータセットに対する学習を行いました。ただし、学習に利用した訓練データへの認識精度が高くても、それは過学習によるものかもしれないのでした。様々なデータに対する精度として汎化性能が求められます。そこで次項では、テストデータへの認識精度を調べます。
4.5.3 テストデータで評価
前項では、学習を行い損失の推移を確認しました。この項では、汎化性能を見るために、訓練データとテストデータに対する認識精度も評価します。
・処理の確認
この例では、1エポックごとに認識精度を測定することにします。そのため、1エポック分の学習が済んだのかを調べる必要があります。
そこで、算術演算子%
を使います。%
は剰余と言い、割り算の余りを返します。
# 剰余の計算 print(0 % 3) print(1 % 3) print(2 % 3) print(3 % 3) print(4 % 3) print(5 % 3) print(6 % 3)
0
1
2
0
1
2
0
計算結果が0
のとき割り切れています。
試行回数i
がnum
で割り切れるときだけ特定の処理を行います。
# 試行回数を指定 iter_num = 12 # 処理を行う回数を指定 num = 3 # 指定した回数ごとに処理 for i in range(12): if i % num == 0: # 試行回数を表示 print(str(i))
0
3
6
9
指定した回数num
ごとに処理を実行できています。ただし、range(iter_num)
は0
からiter_num - 1
の整数を生成するので、0
回目(初回)で実行し、(実質12回目の)11
回目(最後)で実行されません。
これは、i + 1
とすることで対応できます。
# 指定した回数ごとに処理 for i in range(iter_num): if (i + 1) % num == 0: # 試行回数を表示 print(str(i + 1))
3
6
9
12
他にも、range(1, iter_num + 1)
とすることで1
からiter_num
までの整数を生成させることや、iter_num
を切りの良い値+1
にすることでも対応できます。
・ミニバッチ学習
ミニバッチデータに対する試行回数をiters_num
、1回の学習に利用するデータ数をbatch_size
、学習率(パラメータを更新する際の調整項)をlearning_rate
として値を指定します。また、総データ数をtrain_size
、1エポックにかかる試行回数をiter_per_epoch
とします。この例では(私のPCでは10000回はムリなので)、3エポック分(1800回)の学習を行います(これでも十数時間かかりました…)。
# バッチデータ当たりの試行回数を指定 iters_num = 600 * 3 # バッチサイズを指定 batch_size = 100 # データサイズ(総データ数)を取得 train_size = x_train.shape[0] # 1エポックに達する回数を計算 iter_per_epoch = max(train_size / batch_size, 1) print(iter_per_epoch) # 学習率を指定 learning_rate = 0.1
600.0
1エポックごとに認識精度を求めます。そこで、何回繰り返したら1エポックになるのか(総データ数割るバッチサイズtrain_size / batch_size
)を計算してiter_per_epoch
とします。
ただし、総データ数よりもバッチサイズが大きい場合はtrain_size / batch_size
が1未満になるので、max()
を使って1
とします。
ミニバッチ学習を行います。基本的な処理は前項と同じです。
# インスタンスを作成 network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10) # 損失の記録用のリストを作成 train_loss_list = [] # 精度の記録用のリストを作成 train_acc_list = [network.accuracy(x_train, t_train)] # 訓練データに対する初期値 test_acc_list = [network.accuracy(x_test, t_test)] # テストデータに対する初期値 # ミニバッチ学習 for i in range(iters_num): # 開始時間を記録 start_time = time.time() # ミニバッチデータを取得 batch_mask = np.random.choice(train_size, size=batch_size, replace=False) # データ番号を生成 x_batch = x_train[batch_mask] # 入力データ t_batch = t_train[batch_mask] # 教師データ # 勾配を計算 grad = network.numerical_gradient(x_batch, t_batch) # パラメータを更新 for key in ('W1', 'b1', 'W2', 'b2'): # 勾配降下法により値を更新:式(4.7) network.params[key] -= learning_rate * grad[key] # 損失を計算 loss = network.loss(x_batch, t_batch) train_loss_list.append(loss) # 値を記録 # 途中経過を表示 print( 'iter ' + str(i + 1) + ' (' + str(np.round((i + 1) / iters_num * 100, 1)) + '%) : ' + # 繰り返し str(np.round(time.time() - start_time, 1)) + '[s]' # 処理時間 ) print('loss : ' + str(loss)) # 1エポックごとの処理 if (i + 1) % iter_per_epoch == 0: # 認識精度を計算 train_acc = network.accuracy(x_train, t_train) # 訓練データ test_acc = network.accuracy(x_test, t_test) # テストデータ # 値を記録 train_acc_list.append(train_acc) # 訓練データ test_acc_list.append(test_acc) # テストデータ # 途中経過を表示 print('train acc : ' + str(train_acc)) print('test acc : ' + str(test_acc))
iter 1 (0.1%) : 32.1[s]
loss : 2.28899670902947
iter 2 (0.1%) : 34.7[s]
loss : 2.2978796927210614
iter 3 (0.2%) : 29.6[s]
loss : 2.297271887242288
iter 4 (0.2%) : 29.5[s]
loss : 2.2936587108247823
iter 5 (0.3%) : 29.2[s]
loss : 2.3004593306292462
(省略)
iter 1796 (99.8%) : 27.1[s]
loss : 0.25982501271076813
iter 1797 (99.8%) : 26.3[s]
loss : 0.3114888447003331
iter 1798 (99.9%) : 27.3[s]
loss : 0.4504757901224974
iter 1799 (99.9%) : 26.8[s]
loss : 0.3633107660640384
iter 1800 (100.0%) : 26.9[s]
loss : 0.4468746140751425
train acc : 0.90075
test acc : 0.9037
損失と同様に、認識精度(正解率)もリスト***_acc_list
に記録します。こちらは、学習前のパラメータでも認識精度を求めてリストに格納しておきます。
1エポックごとに((i + 1) % iter_per_epoch
が0
のときに)accuracy()
メソッドで訓練データとテストデータに対する認識精度を計算します。
JupyterLabであれば、ツールバーの「Kernel」→「Interrupt Kernel」で処理を中断できます。
・結果の確認
損失(交差エントロピー誤差)の推移をプロットします。
# 損失の推移を作図 plt.figure(figsize=(8, 6)) # 図の設定 plt.plot(np.arange(1, len(train_loss_list) + 1), train_loss_list) # 折れ線グラフ plt.xlabel('iteration') # x軸ラベル plt.ylabel('loss') # y軸ラベル plt.title('Cross Entropy Error', fontsize=20) # グラフタイトル plt.grid() # グリッド線 plt.show()
学習を繰り返すことで損失が下がっているのが分かります。
続いて、訓練データとテストデータに対する認識精度の推移もプロットします。
# 認識精度の推移を作図 plt.figure(figsize=(8, 6)) # 図の設定 plt.plot(np.arange(len(train_acc_list)), train_acc_list, label='train acc') # 訓練データ plt.plot(np.arange(len(test_acc_list)), test_acc_list, label='test acc') # テストデータ plt.xlabel('epochs') # x軸ラベル plt.ylabel('acc') # y軸ラベル plt.title('Accuracy', fontsize=20) # グラフタイトル plt.legend() # 凡例 plt.grid() # グリッド線 plt.show()
初期値と3エポック分の値です。訓練データとテストデータのどちらに対しても正解率が上がっています。つまり、学習に利用したデータのみに過剰に適合していないことを確認できました。
以上で、4章の内容は終了です!これまでに学んできた知識(ツール)を組み合わせて、パラメータの学習を行えました。ただし、数値微分の計算に非常に時間がかかりました。次章では、効率よく微分(勾配)を計算できる誤差逆伝播法を学びます。
参考文献
- 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.
- サポートページ:学習コード(https://github.com/oreilly-japan/deep-learning-from-scratch/blob/master/ch04/train_neuralnet.py)
おわりに
4章終了です!お疲れ様でしたー。5章の資料はこれから作っていくので、ある程度溜まったらまたブログも更新していきます。
なーーんか損失関数の値の推移の仕方が違うよーな、1万回分プロットすればその辺はぎゅっとなって近づくのかもしれませんが、そもそもの初期値が低いよーな気も?
- 2021.08.28:加筆修正しました。
【次節の内容】