からっぽのしょこ

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

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

はじめに

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

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ処理を確認しながらゆっくりと組んでいきます。

 この記事は、4.5.2項「ミニバッチ学習の実装」と4.5.3項「テストデータで評価」の内容になります。前項で実装した2層ニューラルネットワークのクラスを用いて、学習を行います。また更新回数に応じて、損失関数の値が下がり、認識精度が上がる様子を可視化して確認します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

4.5.2 ミニバッチ学習の実装

 前項で実装したクラスを用いて、手書き文字の画像認識の学習を行います。バッチ処理(一部のデータを順番に取り出して処理する方法)については3.6.3項、ミニバッチ学習(一部のデータをランダムに取り出す方法)については4.2.3項をご確認ください。

 まずは、画像データを読み込みます。MNISTデータセットの読み込みについては【MNISTデータセットの読み込み【ゼロつく1のノート(Python)】 - からっぽのしょこ】をご確認ください。

# 利用するライブラリを読み込む
import numpy as np
import matplotlib.pyplot as plt

# 関数の読み込みに利用するライブラリを読み込む
import sys
import os

# ファイルパスを指定
sys.path.append("C:\\Users\\「ユーザー名」\\Documents\\・・・\\deep-learning-from-scratch-master")

# MNISTデータセット読み込み関数を読み込む
from dataset.mnist import load_mnist

# 画像データを取得
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
print(x_train.shape) # 訓練画像
print(t_train.shape) # 訓練ラベル
(60000, 784)
(60000, 10)

 6万枚の画像データから、バッチサイズ分のデータをランダムに取り出して推論処理(画像認識)を行います。

 各ハイパーパラメータを指定します。

# 試行回数を指定
iters_num = 1000

# データサイズ(画像枚数)
train_size = x_train.shape[0]

# バッチサイズ(1回当たりのデータ数)を指定
batch_size = 100

# 学習率を指定
learning_rate = 0.1


 np.random.choice()の第1引数にデータサイズを第2引数にバッチサイズを指定することで、0からデータサイズ未満の整数をランダムに生成します。それを添字とすることで、ランダムにデータを抽出することができます。(これは例なのでバッチサイズを5とします。)

# データ番号をランダムに生成
batch_mask = np.random.choice(train_size, 5, replace=False)
print(batch_mask)

# ミニバッチデータを取得
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
print(x_batch)
print(t_batch)
[ 4102  6738 59471 43525 56590]
[[0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]]
[[0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]
 [1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
 [0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]]

 これがミニバッチデータとなります。これを使って学習を行います。

 学習が終わると、また新たにデータを取得して学習を行います。これを試行回数分繰り返します。

 入力データの確認ができたので、次は2層ニューラルネットワークのインスタンス変数を作成します。

 入力データ数input_sizeはピクセルデータ数の784、出力output_sizeは0から9に対応するため10とするのでした。中間層のニューロン数hidden_sizeは自由に決められます。ここでは50とします。

# インスタンスを作成
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
print(network.params.keys())
print(network.params['W1'].shape)
print(network.params['b1'].shape)
print(network.params['W2'].shape)
print(network.params['b2'].shape)

 各パラメータを確認できました。

 各パラメータの更新では、次のようにfor文を使って行います。パラメータ(インスタンス変数)は'W1', 'b1', 'W2', 'b2'をキーとするディクショナリ型の変数でした。keyに各パラメータに対応するキー(文字列)を順番に代入し、それを.paramsのキーとして利用することで、1つずつ更新していきます。

# 繰り返し処理
for key in ('W1', 'b1', 'W2', 'b2'):
    print(network.params[key].shape)
(784, 50)
(50,)
(50, 10)
(10,)


 勾配や損失関数の計算は、それぞれメソッドを使用します。

 また損失関数の計算の度に値をtrain_loss_listに追加していきます。値を.copy()でコピーして、.append()で変数に追加します。これを後で可視化することで、学習の過程を確認します。

 この一連の処理には、かなりの時間がかかります。そこで、進行具合と時間経過を表示する処理を加えています。なくても学習自体には影響しません。

# 処理時間の計測用
import time

# 交差エントロピー誤差推移の受け皿を初期化
train_loss_list = []

# ミニバッチ学習
for i in range(iters_num):
    
    # 開始時間を記録
    start_time = time.time()
    
    # ミニバッチデータを取得
    batch_mask = np.random.choice(train_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'):
        network.params[key] -= learning_rate * grad[key] # 式(4.7)
    
    # 交差エントロピー誤差を計算
    loss = network.loss(x_batch, t_batch)
    
    # 交差エントロピー誤差を記録
    train_loss_list.append(loss.copy())
    
    # 動作確認
    print(
        str(i + 1) + "(" + str(np.round((i + 1) / iters_num * 100, 1)) + "%):" # 処理データ数
        + str(np.round(time.time() - start_time, 1)) # 処理時間
    )

# (学習の終了確認の意味も込めて)
print(loss)
1(0.1%):31.9
2(0.2%):26.8
3(0.3%):28.9
4(0.4%):27.4
5(0.5%):28.1
(中略)
996(99.6%):27.2
997(99.7%):27.2
998(99.8%):27.4
999(99.9%):27.6
1000(100.0%):27.1
0.538378472089692

 処理にすごく時間がかかります(私の環境だと1回当たり30秒弱です)。いきなり1万回は止めておいた方がいいと思います…

 では更新推移をプロットします。ただしある程度の回数の更新をしないと学習が進んでいることを確認できません…

# 交差エントロピー誤差の推移
plt.plot(np.arange(iters_num), train_loss_list)
plt.xlabel("iteration") # x軸ラベル
plt.ylabel("loss") # y軸ラベル
plt.title("Cross Entropy Error", fontsize=20) # グラフタイトル
plt.show()

f:id:anemptyarchive:20200621203817p:plain
交差エントロピー誤差の推移

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

 ただし学習に利用した訓練データでの認識精度が高くても、それは過学習によるものかもしれないのでした。様々なデータに対する精度として汎化性能が求められます。そこで次項ではテストデータも使って、認識精度を測ります。

4.5.3 テストデータで評価

 この項では汎化性能を測るために、訓練データと共にテストデータの認識精度も評価します。

 前項と同様に、各ハイパーパラメータを指定します。

# 試行回数を指定
iters_num = 1801

# バッチサイズを指定
batch_size = 100

# データサイズ(画像枚数)
train_size = x_train.shape[0]

# 学習率を指定
learning_rate = 0.1

# 1エポック当たりの繰り返し数
iter_per_epoch = max(train_size / batch_size, 1)
print(iter_per_epoch)
600.0

 データセットから一部を取り出したデータのことをバッチデータと呼ぶのでした。またこのバッチデータを変更しながら繰り返し学習を行うのでした。例えば6万枚のデータセットに対してバッチサイズを100とすると、600回繰り返したところで全てのデータ分を使ったことになります(ただしここではランダムに画像データを選ぶので全データを学習に使ったことにはなりません)。これを1エポックと言います。

 ここでは1エポック(を超える)ごとに、全ての画像に対する認識精度を測ることにします。そこで、何回繰り返したら1エポックになるのかを計算してiter_per_epochとします。ただし訓練データよりもバッチサイズが大きい(train_size / batch_sizeが1未満になる)場合は、max()を使って1とすることで1回ごとに認識精度を測ることにします。

 また次の実装方法だと、データ数を割り切れるバッチサイズを指定する必要があります。

 4.5.1項で実装したクラスを使ってインスタンスを作成します。

# インスタンスを作成
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
network.params.keys()
dict_keys(['W1', 'b1', 'W2', 'b2'])


 1エポック分の学習が済んだのかを調べるために、算術演算子%を使います。%は剰余と言い、割り算の余りを返します。

print(0 % 2)
print(1 % 2)
print(2 % 2)
print(3 % 2)
print(4 % 2)
0
1
0
1
0


 if i % iter_per_epoch == 0:とすることで、試行回数iiter_per_epochで割り切れる、つまり1エポックに達したときのみ、.accuracy()メソッドで訓練データとテストデータに対する認識精度を計算します。(iは0からカウントされるので、初回は処理されます。また最終的なiの値はiters_num - 1になることに注意しましょう。)

 ミニバッチ学習自体について前項と同様です。

# 処理時間の計測用
import time

# 交差エントロピー誤差推移の受け皿を初期化
train_loss_list = []

# 訓練データに対する認識精度の受け皿を初期化
train_acc_list = []

# テストデータに対する認識精度の受け皿を初期化
test_acc_list = []

# ミニバッチ学習
for i in range(iters_num):
    
    # 開始時間を記録
    start_time = time.time()
    
    # ミニバッチデータを取得
    batch_mask = np.random.choice(train_size, batch_size)
    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'):
        network.params[key] -= learning_rate * grad[key] # 式(4.7)
    
    # 交差エントロピー誤差を計算
    loss = network.loss(x_batch, t_batch)
    
    # 交差エントロピー誤差を記録
    train_loss_list.append(loss.copy())
    
    # 1エポックごとの認識精度を計算
    if i % 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.copy()) # 訓練データ
        test_acc_list.append(test_acc.copy()) # テストデータ
        
    # 動作確認
    print(
        str(i + 1) + "(" + str(np.round((i + 1) / iters_num * 100, 1)) + "%):" # 処理データ数
        + str(np.round(time.time() - start_time, 1)) # 処理時間
    )

# (学習の終了確認の意味も込めて)
print(loss)
print(train_acc)
print(test_acc)
1(0.1%):30.8
2(0.1%):29.8
3(0.2%):30.1
4(0.2%):29.1
5(0.3%):28.6
(中略)
1796(99.8%):28.8
1797(99.8%):28.8
1798(99.9%):28.6
1799(99.9%):28.8
1800(100.0%):29.2
0.6247978760526713
0.89925
0.9029


 ではそれぞれの推移を可視化しましょう!まずは損失関数の値の値の推移をグラフ化します。

# 交差エントロピー誤差の推移
plt.plot(np.arange(iters_num), train_loss_list)
plt.xlabel("iteration") # x軸ラベル
plt.ylabel("loss") # y軸ラベル
plt.title("Cross Entropy Error", fontsize=20) # グラフタイトル
plt.show()

f:id:anemptyarchive:20200622133910p:plain
交差エントロピー誤差の推移


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

# x軸の値を生成
x = np.arange(len(train_acc_list))

# 作図
plt.plot(x, train_acc_list, label="train acc") # 訓練データに対する認識精度
plt.plot(x, test_acc_list, label="test acc", linestyle="--") # テストデータに対する認識精度
plt.xlabel("epochs") # x軸ラベル
plt.ylabel("accuracy") # y軸ラベル
plt.legend() # 凡例
plt.title("", fontsize=20) # グラフタイトル
plt.show()

f:id:anemptyarchive:20200622133935p:plain
訓練データとテストデータに対する認識精度の推移

 試行回数が増えるに従って、訓練データとテストデータのどちらに対しても認識精度が上がっているのが分かりますね。(この図を作ってる最中に0回目(エポック目)のプロットをし忘れていることに気付きました、、コードは修正しましたが、図はそのままです。なんせこれ作るだけでも十数時間かかっているので、、、悪しからず。。)

 以上で4章の内容は終了です!これまで1つ1つ学んできた知識(ツール)を組み合わせることで、パラメータの学習を行えました。次章では、全く逆のアプローチで学習を行います。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.

おわりに

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

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

【次節の内容】

に続く。