#1.はじめに
名著、**「ゼロから作るDeep Learning」**を読んでいます。今回は4章のメモ。
コードの実行はGithubからコード全体をダウンロードし、ch04の中で jupyter notebook にて行っています。
#2.2層ニューラルネットワークの学習
第4章の最後に、2層ニューラルネットワークを数値微分によってパラメータの勾配を計算する方法で学習するコード ( train_neuralnet.py
) があります。今回は、このコードを実行してから、細部を確認して行きます。但し、Githubのコードそのままではなく、下記の様に一部修正・追加します。
1)実行速度が遅いことへの対応
数値微分
を使うと莫大な時間が掛かるのにも関わらず精度表示が 600 iter 毎(1 epoch 毎)では、2回目の精度計算の表示が数時間後くらいになってしまいます。そこで、1時間ちょっとで結果が分かるように、精度表示を 1 iter 毎
にし、実行回数を10000から100
にし、学習率を0.1から1.0
にします。
2)外部呼び出しコードを減らして見通しよく
ゼロ作の特徴は、既に説明したコードは極力外部呼び出しにして、一度に表示するコードを出来るかぎり簡潔に表すことです。しかし、train_neuralnet.py
から two_layer_net.py
を呼び出し、two_layer_net.py
から common フォルダー
内の gradient.py
を呼び出すというのは、とても分かり難いです。なので、このあたりは、train_neuralnet.py
に追加して見通しをよくします。
但し、commonフォルダー
にある function.py
だけは外部コードとして使っています(sigmoid, sofytmax, cross_entropy_errorなど)。
では、まずは下記コードを実行してみましょう。
import sys, os
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
from common.functions import * # common フォルダーの function.py にある関数を全て使えるように設定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
class TwoLayerNet:
# パラメータの初期化
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size)
# 順伝播
def predict(self, x):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
return y
# ロス計算
def loss(self, x, t):
y = self.predict(x)
return cross_entropy_error(y, t)
# 精度計算
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=1)
t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy
# 勾配計算
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numerical_gradient2(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient2(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient2(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient2(loss_W, self.params['b2'])
return grads
# 数値微分
def numerical_gradient2(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x)
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
idx = it.multi_index
tmp_val = x[idx]
x[idx] = tmp_val + h
fxh1 = f(x) # f(x+h)
x[idx] = tmp_val - h
fxh2 = f(x) # f(x-h)
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val # 値を元に戻す
it.iternext()
return grad
# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
# TwoLayerNet をインスタンス化
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
# 初期設定
iters_num = 100 # 実行回数は 10000 → 100 に変更
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 1 # 学習率は 0.1 → 1.0 に変更
train_loss_list, train_acc_list, test_acc_list = [], [], []
iter_per_epoch = 1 # 精度表示は 1 epoch 毎から 1 iter 毎に変更
for i in range(iters_num):
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]
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
# 精度表示
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)
test_acc_list.append(test_acc)
# 表示 (iter と train_loss を追加)
print('[iter='+str(i)+'] '+'train_loss='+str(loss)+', '+'train_acc='+str(train_acc)+', '+'test_acc='+str(test_acc))
# グラフの描画
markers = {'train': 'o', 'test': 's'}
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("iter")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
私の Macbook air
で 100iter 実行するのに 75分掛かりました。数値微分の場合でも 16epoch (= 9600iter) を実行した精度グラフをテキストに載せるのは一体どうなの?とは思いますが、それはさておき、数値微分には膨大な時間が掛かるということですね。
#3.クラス TwoLayerNet
# パラメータの初期化
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size)
class をインスタンス化する時に1度だけ実行される部分で、ここでは各パラメータの初期化をしています。np.random.randn()
は、平均0, 分散1 の正規分布の乱数生成、np.zeros()
は ゼロ行列の生成です。各行列の大きさは、下記の様です。
# 順伝播
def predict(self, x):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
return y
順伝播の部分です。辞書 params に保存されている 重み'W1', 'W2' とバイアス 'b1', 'b2' の行列を読み出し、行列演算で順伝播させます。
# ロス計算
def loss(self, x, t):
y = self.predict(x)
return cross_entropy_error(y, t)
ロス計算をする部分です。先程の順伝播を呼び出して求めた y と教師データ t のクロスエントロピーを求めます。クロスエントロピーは、common フォルダー
内の function.py
の関数を使っています。一応みておくと、
y[np.arange(batch_size), t]
は、tに順番に入っている正解ラベルに従って、yの該当する要素をスライシングするという意味です。
# 精度計算
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=1)
t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy
精度計算の部分です。入力データxからyを推論し、yと正解ラベルtのインデックスをそれぞれ取り出し、2つのインデクスが同じ場合の数をxのデータ数で割って精度を計算しています。
# 勾配計算
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numerical_gradient2(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient2(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient2(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient2(loss_W, self.params['b2'])
return grads
勾配計算の部分です。この後出て来る numerical_gradient2
関数を使って、勾配の計算結果を辞書形式にまとめます。引数は、xと正解ラベルtを元にクロスエントロピーを求める先程の loss関数式
、そしてパラメータの指定
です。
ちなみに、オリジナルは、grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
となっていて、あれ?再帰利用かな?と最初思ったりしたのですが、そうではありません。
オリジナルでは、TwoLayerNet.py
の冒頭に、from common.gradient import numerical_gradient
と記載があり、common フォルダー
内の gradient.py
にある numerical_gradient 関数
がインポートされているので、使っているのはこちらです。ここは名前を変えた方が間違いが少ないので、numerical_gradient2
に名称変更しています。
# 数値微分
def numerical_gradient2(f, x):
h = 1e-4 # 0.0001
# 勾配の計算結果を保管するゼロ行列 grad(サイズはxで指定)を準備
grad = np.zeros_like(x)
# 行列 x を順次インデックス指定(行と列を指定)する
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
# 行列 x の全ての行と列を指定するまで継続
while not it.finished:
idx = it.multi_index # idxに(行,列)を代入
tmp_val = x[idx] # idxで指定したxの値をtmp_valに退避
# 微小数hを加えて順伝播しロスを計算
x[idx] = tmp_val + h
fxh1 = f(x) # f(x+h)
# 微小数hを引いて順伝播しロスを計算
x[idx] = tmp_val - h
fxh2 = f(x) # f(x-h)
# 該当インデックスの勾配を計算
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val # 退避した値を元に戻す
it.iternext() # 次のインデックスを実行
return grad
数値微分をする部分です。簡単に言うと、パラメータの1つ1つについて、小さな値hを足して順伝播・ロス計算、小さな値hを引いて順伝播・ロス計算し、2つのロス計算がどう変化したかで勾配を決めています。
今回、パラメータ数は、W1が78450=39,200個、W2が5010=500個、b1が50個、b2が10個で、合計39,760個あります。パラメータ1個あたり、順伝播・ロス計算を2回行いますので、ネットワークの1回のパラメータ更新に、なんと79,520回の順伝播・ロス計算をすることになります。動作が激遅なわけです。
さて、コードに見慣れない、np.nditer
があります。通常、行列のインデッックス指定は行指定にforループ+列指定にforループと2重ループを使うわけですが、それを1回で済ませるのが、このnp.nditer
です。
なお、先程も説明しましたが、このコードはオリジナルでは、commonフォルダー
のgradient.py
に入っています。オリジナルの関数名は、numerical_gradient
と紛らわしい名前(勾配計算のところの関数名と同一)をしているので、今回は numerical_gradient2
に変更しています。
#4.本体部分
# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
# TwoLayerNet をインスタンス化
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
# 初期設定
iters_num = 100 # 実行回数は 10000 → 100 に変更
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 1 # 学習率は 0.1 → 1.0 に変更
train_loss_list, train_acc_list, test_acc_list = [], [], []
iter_per_epoch = 1 # 精度表示は 1 epoch 毎から 1 iter 毎に変更
データを読み込んだら、クラスTwoLayerNet
をインスタンス化します。input_size=784, hidden_size=50, output_size=10なので、モデルと行列演算は下記の様です。
for i in range(iters_num):
# ミニバッチデータの取得
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]
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
# 精度表示
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)
test_acc_list.append(test_acc)
# 表示 (iter と train_loss を追加)
print('[iter='+str(i)+'] '+'train_loss='+str(loss)+', '+'train_acc='+str(train_acc)+', '+'test_acc='+str(test_acc))
まず、ミニバッチ学習用データの準備です。np.random.choice(train_size, batch_size)
は 60,000個あるtrainデータの中からランダムに100個を選んだ結果(何番目を選んだか)を batch_mask
に代入し、それを使って学習データと正解ラベルを取得します。
次に、勾配計算。TwoLayerNet クラスの numerical_gradient関数
を呼び出し、先程説明した様に、パラメータ1つ1つについて、数値微分で勾配を求めます。
そして、求めた勾配を使ってパラメータを更新し、ロスを計算して記録し、精度を表示します。
# グラフの描画
markers = {'train': 'o', 'test': 's'}
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("iter")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
学習完了後に、精度グラフを表示します。これは、特に説明は必要ないでしょう。