からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

4.5.2-3:ミニバッチ学習【ゼロつく1のノート(実装)】

はじめに

 「プログラミング」初学者のための『ゼロから作るDeep Learning』攻略ノートです。『ゼロつくシリーズ』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ実行結果を見ながら処理の意図を確認していきます。

 この記事は、4.5.2項「ミニバッチ学習の実装」と4.5.3項「テストデータで評価」の内容です。前項で実装した2層のニューラルネットワークのクラスを用いて、学習を行い損失と認識精度の推移を確認します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

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として、ディクショナリ型のインスタンス変数paramsgradから各パラメータの値(配列)を取り出して勾配降下法により値を更新しています。
 ミニバッチデータの抽出については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()

f:id:anemptyarchive:20210829011035p:plain
損失の推移

 試行回数が増えるに従って、損失が下がっているのを確認できます。つまり、学習が進んでいるのが分かります。

 この項では、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のとき割り切れています。

 試行回数inumで割り切れるときだけ特定の処理を行います。

# 試行回数を指定
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_epoch0のときに)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()

f:id:anemptyarchive:20210829161830p:plain
損失の推移

 学習を繰り返すことで損失が下がっているのが分かります。

 続いて、訓練データとテストデータに対する認識精度の推移もプロットします。

# 認識精度の推移を作図
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()

f:id:anemptyarchive:20210829161843p:plain
認識精度の推移

 初期値と3エポック分の値です。訓練データとテストデータのどちらに対しても正解率が上がっています。つまり、学習に利用したデータのみに過剰に適合していないことを確認できました。

 以上で、4章の内容は終了です!これまでに学んできた知識(ツール)を組み合わせて、パラメータの学習を行えました。ただし、数値微分の計算に非常に時間がかかりました。次章では、効率よく微分(勾配)を計算できる誤差逆伝播法を学びます。

参考文献

おわりに

 4章終了です!お疲れ様でしたー。5章の資料はこれから作っていくので、ある程度溜まったらまたブログも更新していきます。

 なーーんか損失関数の値の推移の仕方が違うよーな、1万回分プロットすればその辺はぎゅっとなって近づくのかもしれませんが、そもそもの初期値が低いよーな気も?

  • 2021.08.28:加筆修正しました。


【次節の内容】

www.anarchive-beta.com