In [2]:
class SGD:
def __init__(self, lr=0.01):
self.lr = lr
def update(self, params, grads):
for key in params.keys():
params[key] -= self.lr * grads[key]
In [14]:
# cf.http://d.hatena.ne.jp/white_wheels/20100327/p3
import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D
def _numerical_gradient_no_batch(f, x, axis='x'):
h = 1e-4 # 0.0001
grad = np.zeros_like(x)
for idx in range(x.size):
tmp_val = x[idx]
x[idx] = float(tmp_val) + h
fxh1 = f(x, axis) # f(x+h)
x[idx] = tmp_val - h
fxh2 = f(x, axis) # f(x-h)
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val # 値を元に戻す
return grad
def numerical_gradient(f, X):
if X.ndim == 1:
return _numerical_gradient_no_batch(f, X)
else:
grad = np.zeros_like(X)
for idx, x in enumerate(X):
if(idx == 0):
grad[idx] = _numerical_gradient_no_batch(f, x, axis='x')
elif(idx == 1):
grad[idx] = _numerical_gradient_no_batch(f, x, axis='y')
return grad
def function_2(x, axis='x'):
if(axis == 'x'):
return np.sum(x**2 / 20)
elif(axis == 'y'):
return np.sum(x**2)
if __name__ == '__main__':
x = np.arange(-10, 10, 1)
y = np.arange(-10, 10, 1)
X, Y = np.meshgrid(x, y)
X = X.flatten()
Y = Y.flatten()
grad = numerical_gradient(function_2, np.array([X, Y]) )
plt.figure()
plt.quiver(X, Y, -grad[0], -grad[1], angles="xy",color="#666666")#,headwidth=10,scale=40,color="#444444")
plt.xlim([-10, 10])
plt.ylim([-10, 10])
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.legend()
plt.draw()
plt.show()
モーメンタム(Momentum)という手法は以下式で表される。
$$ v ← \alpha v -\eta \frac{\partial L}{\partial W} \\ W ← W + v $$$W$:更新する重みパラメータ、$\frac{\partial L}{\partial W}$:Wに関する損失関数の勾配、$\eta$:学習係数、$v$:物理で言う速度である。 $\alpha v$では何も力を受けないときに徐々に減速するための役割を担う($\alpha$は0.9などを設定する)
お椀を転がるボールのように更新していく。これは同じ方向に力を受け続けたら加速し(x軸)、逆方向の力が加えられると減速する(y軸)イメージと類似している。
In [16]:
class Momentum:
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update(self, params, grads):
if self.v is None:
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]
params[key] += self.v[key]
ニューラルネットワークの学習では学習係数が重要であり、小さすぎるとなかなか学習が進まず大きすぎると発散して学習ができない。
学習係数に関するテクニックで、「学習係数の減衰(learning rate decay)」という方法がある。学習が進むに連れて学習係数を小さくする手法である。パラメータ全体の学習係数の値を一括して下げる方法もあるが、AdaGradでは「1つ1つ」のパラメータに対して減衰を行なう。AdaGradの更新方法は以下である。
$$ h \gets h + \frac{\partial L}{\partial W} \odot \frac{\partial L}{\partial W} $$$$ W \gets W + \eta \frac{1}{\sqrt h} \frac{\partial L}{\partial W} $$$W$:更新する重みパラメータ、$\frac{\partial L}{\partial W}$:Wに関する損失関数の勾配、$\eta$:学習係数、$h$:これまでの勾配の2乗和($\odot$は行列の要素ごとの掛け算)。パラメータ更新の際に$\frac{1}{\sqrt h}$を乗算し学習のスケールを調整する。大きく更新された要素は学習係数が小さくなる。
AdaGradは過去の勾配を全て二乗和として記録するため進めるほど更新料が小さくなりつつける。そこでRMSPropという手法では過去のすべての勾配を均一に考えるのではなく、過去の勾配の影響は小さくし、直近の勾配情報を大きく影響を与えるようにする。(指数移動平均)
AdaGradの実装は以下となる。
In [ ]:
class AdaGrad:
def __init__(self, lr=0.01):
self.lr = lr
self.h = None
def update(self, params, grads):
if self.h is None:
self.h = {}
for key, val in params.items():
self.h[key] = np.zeros_like(val)
for key in params.keys():
self.h[key] += grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
In [ ]:
class Adam:
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.iter = 0
self.m = None
self.v = None
def update(self, params, grads):
if self.m is None:
self.m, self.v = {}, {}
for key, val in params.items():
self.m[key] = np.zeros_like(val)
self.v[key] = np.zeros_like(val)
self.iter += 1
lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)
for key in params.keys():
#self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
#self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
#unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
#unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
#params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)
In [17]:
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
from src.optimizer import *
def f(x, y):
return x**2 / 20.0 + y**2
def df(x, y):
return x / 10.0, 2.0*y
init_pos = (-7.0, 2.0)
params = {}
params['x'], params['y'] = init_pos[0], init_pos[1]
grads = {}
grads['x'], grads['y'] = 0, 0
optimizers = OrderedDict()
optimizers["SGD"] = SGD(lr=0.95)
optimizers["Momentum"] = Momentum(lr=0.1)
optimizers["AdaGrad"] = AdaGrad(lr=1.5)
optimizers["Adam"] = Adam(lr=0.3)
idx = 1
for key in optimizers:
optimizer = optimizers[key]
x_history = []
y_history = []
params['x'], params['y'] = init_pos[0], init_pos[1]
for i in range(30):
x_history.append(params['x'])
y_history.append(params['y'])
grads['x'], grads['y'] = df(params['x'], params['y'])
optimizer.update(params, grads)
x = np.arange(-10, 10, 0.01)
y = np.arange(-5, 5, 0.01)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)
# for simple contour line
mask = Z > 7
Z[mask] = 0
# plot
plt.subplot(2, 2, idx)
idx += 1
plt.plot(x_history, y_history, 'o-', color="red")
plt.contour(X, Y, Z)
plt.ylim(-10, 10)
plt.xlim(-10, 10)
plt.plot(0, 0, '+')
#colorbar()
#spring()
plt.title(key)
plt.xlabel("x")
plt.ylabel("y")
plt.show()
上記ではAdaGradが一番良く見えるが、扱う問題やハイパーパラメータ(学習係数など)の設定値によって結果が変わる。
In [22]:
import os
import sys
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import matplotlib.pyplot as plt
from src.mnist import load_mnist
from src.util import smooth_curve
from src.multi_layer_net import MultiLayerNet
from src.optimizer import *
# 0:MNISTデータの読み込み==========
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
train_size = x_train.shape[0]
batch_size = 128
max_iterations = 2000
# 1:実験の設定==========
optimizers = {}
optimizers['SGD'] = SGD()
optimizers['Momentum'] = Momentum()
optimizers['AdaGrad'] = AdaGrad()
optimizers['Adam'] = Adam()
#optimizers['RMSprop'] = RMSprop()
networks = {}
train_loss = {}
for key in optimizers.keys():
networks[key] = MultiLayerNet(
input_size=784, hidden_size_list=[100, 100, 100, 100],
output_size=10)
train_loss[key] = []
# 2:訓練の開始==========
for i in range(max_iterations):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
for key in optimizers.keys():
grads = networks[key].gradient(x_batch, t_batch)
optimizers[key].update(networks[key].params, grads)
loss = networks[key].loss(x_batch, t_batch)
train_loss[key].append(loss)
if i % 100 == 0:
print( "===========" + "iteration:" + str(i) + "===========")
for key in optimizers.keys():
loss = networks[key].loss(x_batch, t_batch)
print(key + ":" + str(loss))
# 3.グラフの描画==========
markers = {"SGD": "o", "Momentum": "x", "AdaGrad": "s", "Adam": "D"}
x = np.arange(max_iterations)
for key in optimizers.keys():
plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 1)
plt.legend()
plt.show()
SGD以外の手法が早く学習できている。AdaGradが最も早く学習出来ているように見える。一般にSGDよりほかの3つの手法のほうが早く学習でき、時には最終的な認識性能も高くなる。
ニューラルネットワークでは重みの初期値も重要である。
科学集を抑えて汎化性能を高める手法として「Weight decay(荷重減衰)」がある。この手法は重みパラメータの値が小さくなるように学習を行なうことを目的としている。(過学習を起きにくくする)
初期値を小さい状態からスタートするのが正攻法である(これまでもガウス分布から生成される値を0.01倍した値などを利用してきた)が、全て0に設定するのは不適当である。
全てを0(正確には重みを均一な値に設定)にすると、誤差逆伝播法において全ての重みの値が均一に更新されてしまうため。1層目も2層目も同じように重みは対象的な値を持つようになってしまい、上手く学習できない。
重みが均一になる(対象的な構造になる)ことを防ぐには、ランダムな初期値が必要。
In [4]:
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def ReLU(x):
return np.maximum(0, x)
def tanh(x):
return np.tanh(x)
input_data = np.random.randn(1000, 100) # 1000個のデータ
node_num = 100 # 各隠れ層のノード(ニューロン)の数
hidden_layer_size = 5 # 隠れ層が5層
activations = {} # ここにアクティベーションの結果を格納する
activations01 = {} # ここにアクティベーションの結果を格納する
activationsxa = {} # ここにアクティベーションの結果を格納する
x = input_data
x01 = input_data
xxa = input_data
for i in range(hidden_layer_size):
if i != 0:
x = activations[i-1]
x01 = activations01[i-1]
xxa = activationsxa[i-1]
# 初期値の値をいろいろ変えて実験しよう!
w = np.random.randn(node_num, node_num) * 1
w01 = np.random.randn(node_num, node_num) * 0.01
wxa = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)
# w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)
a = np.dot(x, w)
a01 = np.dot(x01, w01)
axa = np.dot(xxa, wxa)
# 活性化関数の種類も変えて実験しよう!
z = sigmoid(a)
z01 = sigmoid(a01)
zxa = sigmoid(axa)
# z = ReLU(a)
# z = tanh(a)
activations[i] = z
activations01[i] = z01
activationsxa[i] = zxa
# ヒストグラムを描画(標準偏差1)
for i, a in activations.items():
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer")
if i != 0: plt.yticks([], [])
# plt.xlim(0.1, 1)
# plt.ylim(0, 7000)
plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
# ヒストグラムを描画(標準偏差0.1)
for i, a in activations01.items():
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer")
if i != 0: plt.yticks([], [])
# plt.xlim(0.1, 1)
# plt.ylim(0, 7000)
plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
上記の上表を見ると、標準偏差1のガウス分布で与えられた重みの初期値において各層のアクティベーションは0や1に偏っている。 活性化関数であるシグモイド関数は出力が0や1に近づくほど微分は0に近づくため、逆伝播の際には勾配の値がどんどん小さくなって消えてしまう。これを「勾配消失問題(gradient vanishing)」と呼び、層が深くなるほど深刻になってくる。
上記の下表である標準偏差0.1のガウス分布で与えられた重みの初期値では0.5付近に集中している。この場合、勾配消失の問題は発生しないが表現力に大きな問題がある。複数のニューロンが同じ値を出力すれば、いわば1個のニューロンでも同じ結果であるため表現力が乏しいと言える。
これらのことから各層のアクティベーションの分布は適度な広がりを持つことが求められることが分かる。
次にディープラーニングのフレームワークで標準的に利用されている「Xavierの初期値」(Xavier Glorotの論文で推奨される重みの初期値)で確認してみる。
この論文では角層のアクティベーションを同じ広がりのある分布にすることを目的として、適切な重みのスケールを導いた。前層のノードの個数を$n$とした場合、$\frac{1}{\sqrt n}$の標準偏差を持つガウス分布を使うと良い。これは前層のノードの数が多ければ多いほど初期値として設定する重みは小さくなることとなる。
確認結果は以下となる。上位層につれていびつになるが、広がりを持った分布となっている。これによりシグモイド関数でも表現力を損なわずに効率的に学習が出来る。
いびつである点については活性化関数に$tanh$(双曲線関数)を用いれば改善される。tanhが原点で対象なカーブであるためである。一般的に活性化関数は減点対象であることが望ましい性質として知られる。
In [31]:
# ヒストグラムを描画(標準偏差 Xavier)
for i, a in activationsxa.items():
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer")
if i != 0: plt.yticks([], [])
# plt.xlim(0.1, 1)
# plt.ylim(0, 7000)
plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
In [23]:
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def ReLU(x):
return np.maximum(0, x)
def tanh(x):
return np.tanh(x)
input_data = np.random.randn(1000, 100) # 1000個のデータ
node_num = 100 # 各隠れ層のノード(ニューロン)の数
hidden_layer_size = 5 # 隠れ層が5層
activations001 = {} # ここにアクティベーションの結果を格納する
activationsxa = {} # ここにアクティベーションの結果を格納する
activationshe = {} # ここにアクティベーションの結果を格納する
x001 = input_data
xxa = input_data
xhe = input_data
for i in range(hidden_layer_size):
if i != 0:
x001 = activations001[i-1]
xxa = activationsxa[i-1]
xhe = activationshe[i-1]
# 初期値の値をいろいろ変えて実験しよう!
w001 = np.random.randn(node_num, node_num) * 0.01
wxa = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)
whe = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)
a001 = np.dot(x001, w001)
axa = np.dot(xxa, wxa)
ahe = np.dot(xhe, whe)
z001 = ReLU(a001)
zxa = ReLU(axa)
zhe = ReLU(ahe)
activations001[i] = z001
activationsxa[i] = zxa
activationshe[i] = zhe
# ヒストグラムを描画(標準偏差0.01)
for i, a in activations001.items():
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer")
if i != 0: plt.yticks([], [])
plt.xlim(0.1, 1)
plt.ylim(0, 7000)
plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
# ヒストグラムを描画(Xavier)
for i, a in activationsxa.items():
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer")
if i != 0: plt.yticks([], [])
plt.xlim(0.1, 1)
plt.ylim(0, 7000)
plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
# ヒストグラムを描画(He)
for i, a in activationshe.items():
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer")
if i != 0: plt.yticks([], [])
plt.xlim(0.1, 1)
plt.ylim(0, 7000)
plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
「std=0.01」の場合いずれの層でもアクティベーションは小さくなるため、逆伝播の際の勾配も小さくなる。「Xavierの初期値」の場合層が深くなるにつれて偏りが少しづつ大きくなる。これより層を深くすればアクティベーションの方よりも大きくなり、学習の際に勾配消失問題が発生してしまう。「Heの初期値」においては均一の広がりであることが見て取れる。逆伝播の際にもよい結果が得られる。
よって活性化関数にReLUを用いるときには「Heの初期値」、sigmoidやtanhを用いる時は「Xavierの初期値」を用いるのが良い。
In [14]:
import os
import sys
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
from src.mnist import load_mnist
from src.util import smooth_curve
from src.multi_layer_net import MultiLayerNet
from src.optimizer import SGD
# 0:MNISTデータの読み込み==========
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
train_size = x_train.shape[0]
batch_size = 128
max_iterations = 2000
# 1:実験の設定==========
weight_init_types = {'std=0.01': 0.01, 'Xavier': 'sigmoid', 'He': 'relu'}
optimizer = SGD(lr=0.01)
networks = {}
train_loss = {}
for key, weight_type in weight_init_types.items():
networks[key] = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100],
output_size=10, weight_init_std=weight_type)
train_loss[key] = []
# 2:訓練の開始==========
for i in range(max_iterations):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
for key in weight_init_types.keys():
grads = networks[key].gradient(x_batch, t_batch)
optimizer.update(networks[key].params, grads)
loss = networks[key].loss(x_batch, t_batch)
train_loss[key].append(loss)
if i % 100 == 0:
print("===========" + "iteration:" + str(i) + "===========")
for key in weight_init_types.keys():
loss = networks[key].loss(x_batch, t_batch)
print(key + ":" + str(loss))
# 3.グラフの描画==========
markers = {'std=0.01': 'o', 'Xavier': 's', 'He': 'D'}
x = np.arange(max_iterations)
for key in weight_init_types.keys():
plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 2.5)
plt.legend()
plt.show()
「std=0.01」ではほとんど学習が進んでいない。アクティベーションが0に偏っており、勾配が殆どないため重みの更新がほとんど行われていないためと考えられる。
「Xavierの初期値」「Heの初期値」においては学習が上手く進んでおり、後者のほうが学習の進みが早いことが分かる。
これらのことから重みの初期値は学習において重要であることが分かる。
重みの初期値では各層のアクティベーションの分布を確認したが、各層で適度な広がりを持つように強制的に分布を調整するのがBatch Normalizationである。
Batch Normには次の利点がある。
Batch Normは学習を行う際のミニバッチを単位として正規化を行なう。具体的にはデータ分布の平均が0で分散が1になるようにする。数式は以下となる。
$$ \mu_{B} \gets \frac{1}{m} \sum_{i=1}^{m} x_{i} \\ \sigma_{B}^{2} \gets \frac{1}{m} \sum_{i=1}^{m} (x_{i}-\mu_{B})^2 \\ \hat{x}_{i} \gets \frac{x_{i}-\mu_{B}}{\sqrt{\sigma_{B}^{2}+\epsilon}} $$ミニバッチ:$B=\{x_{1},x_{2},・・・,x_{m}\}$
平均:$\mu_{B}$
分散:$\sigma_{B}^{2}$
$\epsilon$は小さな値(10e-7)などであり、0で除算することを防ぐために加えるものである。 上記式はミニバッチの入力データ$\{x_{1},x_{2},・・・,x_{m}\}$を平均0、分散1のデータ$\{\hat{x}_{1},\hat{x}_{2},・・・,\hat{x}_{m}\}$に変換している。
正規化の処理は活性化関数の前もしくは後ろに挿入する。(どちらが良いかは議論の余地あり) Batch Normレイヤではこの正規化されたデータに対して固有のスケールとシフトで変換する。
$$ y_{i} = \gamma \hat{x}_{i} + \beta $$$\gamma$と$\beta$はパラメータであり、最初は$\gamma=1$,$\beta=0$からスタートして学習によって適した値に調整されていく。
これは順伝播の場合であるが、逆伝播の値もこちらから導出することが出来る。
In [42]:
import sys, os
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
from src.mnist import load_mnist
from src.multi_layer_net_extend import MultiLayerNetExtend
from src.optimizer import SGD, Adam
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 学習データを削減
x_train = x_train[:1000]
t_train = t_train[:1000]
max_epochs = 20
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.01
def __train(weight_init_std):
bn_network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100], output_size=10,
weight_init_std=weight_init_std, use_batchnorm=True)
network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100], output_size=10,
weight_init_std=weight_init_std)
optimizer = SGD(lr=learning_rate)
train_acc_list = []
bn_train_acc_list = []
iter_per_epoch = max(train_size / batch_size, 1)
epoch_cnt = 0
for i in range(1000000000):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
for _network in (bn_network, network):
grads = _network.gradient(x_batch, t_batch)
optimizer.update(_network.params, grads)
if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
bn_train_acc = bn_network.accuracy(x_train, t_train)
train_acc_list.append(train_acc)
bn_train_acc_list.append(bn_train_acc)
# print("epoch:" + str(epoch_cnt) + " | " + str(train_acc) + " - " + str(bn_train_acc))
epoch_cnt += 1
if epoch_cnt >= max_epochs:
break
return train_acc_list, bn_train_acc_list
# 3.グラフの描画==========
plt.figure(figsize=(10,9))
weight_scale_list = np.logspace(0, -4, num=16)
x = np.arange(max_epochs)
for i, w in enumerate(weight_scale_list):
# print( "============== " + str(i+1) + "/16" + " ==============")
train_acc_list, bn_train_acc_list = __train(w)
plt.subplot(4,4,i+1)
plt.title("W:" + str(w))
if i == 15:
plt.plot(x, bn_train_acc_list, label='Batch Normalization', markevery=2)
plt.plot(x, train_acc_list, linestyle = "--", label='Normal(without BatchNorm)', markevery=2)
else:
plt.plot(x, bn_train_acc_list, markevery=2)
plt.plot(x, train_acc_list, linestyle="--", markevery=2)
plt.ylim(0, 1.0)
if i % 4:
plt.yticks([])
else:
plt.ylabel("accuracy")
if i < 12:
plt.xticks([])
else:
plt.xlabel("epochs")
plt.legend(loc='lower right')
plt.show()
重みの初期値の標準偏差を変えたときの学習経過は上記になる。実践がBatch Normあり、破線がBatch Normなしである。多くのケースでBatch Normを利用したほうが学習が早く進んでいる。
このことからBatch Normを利用することで学習の進行を促進させることができ、重みの初期値にロバストになる(初期値に依存しなくなる)。
In [1]:
import os
import sys
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
from src.mnist import load_mnist
from src.multi_layer_net import MultiLayerNet
from src.optimizer import SGD
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 過学習を再現するために、学習データを削減
x_train = x_train[:300]
t_train = t_train[:300]
# weight decay(荷重減衰)の設定 =======================
weight_decay_lambda = 0 # weight decayを使用しない場合
# weight_decay_lambda = 0.1
# ====================================================
network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100], output_size=10,
weight_decay_lambda=weight_decay_lambda)
optimizer = SGD(lr=0.01)
max_epochs = 201
train_size = x_train.shape[0]
batch_size = 100
train_loss_list = []
train_acc_list = []
test_acc_list = []
iter_per_epoch = max(train_size / batch_size, 1)
epoch_cnt = 0
for i in range(1000000000):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
grads = network.gradient(x_batch, t_batch)
optimizer.update(network.params, grads)
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)
# print("epoch:" + str(epoch_cnt) + ", train acc:" + str(train_acc) + ", test acc:" + str(test_acc))
epoch_cnt += 1
if epoch_cnt >= max_epochs:
break
# 3.グラフの描画==========
markers = {'train': 'o', 'test': 's'}
x = np.arange(max_epochs)
plt.plot(x, train_acc_list, marker='o', label='train', markevery=10)
plt.plot(x, test_acc_list, marker='s', label='test', markevery=10)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
エポック(訓練データを全て見終わった単位)毎に、訓練データとテストデータの認識精度を確認した結果が上記となる。
訓練データにおいては100エポックを終えたあたりから認識精度が100%となっている。一方でテストデータにおいては80%未満であり精度が上がらなくなっている。訓練データにだけ適応していることから過学習していることが分かる。
In [4]:
import os
import sys
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
from src.mnist import load_mnist
from src.multi_layer_net import MultiLayerNet
from src.optimizer import SGD
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 過学習を再現するために、学習データを削減
x_train = x_train[:300]
t_train = t_train[:300]
# weight decay(荷重減衰)の設定 =======================
# weight_decay_lambda = 0 # weight decayを使用しない場合
weight_decay_lambda = 0.1
# ====================================================
network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100], output_size=10,
weight_decay_lambda=weight_decay_lambda)
optimizer = SGD(lr=0.01)
max_epochs = 201
train_size = x_train.shape[0]
batch_size = 100
train_loss_list = []
train_acc_list = []
test_acc_list = []
iter_per_epoch = max(train_size / batch_size, 1)
epoch_cnt = 0
for i in range(1000000000):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
grads = network.gradient(x_batch, t_batch)
optimizer.update(network.params, grads)
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)
# print("epoch:" + str(epoch_cnt) + ", train acc:" + str(train_acc) + ", test acc:" + str(test_acc))
epoch_cnt += 1
if epoch_cnt >= max_epochs:
break
# 3.グラフの描画==========
markers = {'train': 'o', 'test': 's'}
x = np.arange(max_epochs)
plt.plot(x, train_acc_list, marker='o', label='train', markevery=10)
plt.plot(x, test_acc_list, marker='s', label='test', markevery=10)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
Weight decayを加えた結果、訓練データとテストデータ間の差は縮まった事が分かる。一方で訓練データの認識性能が下がったことも注目すべきである。
ニューラルネットのモデルが複雑になるとWeight Decayだけでは過学習に対応できなくなる。そこで「Dropout」という手法を用いる。
Dropoutはニューロンをランダムに消去しながら学習する。訓練時に隠れ層のニューロンをランダムに選出し消去する。訓練の際にデータが流れる度に消去するニューロンを選出する。一方、テスト時には全てのニューロン信号を伝達するが、各ニューロンの出力に対して、訓練時に消去した割合を乗算して出力する。 訓練の際に適切な計算を行えば順伝播には単にデータを流すだけで消去した割合を乗算しなくてもよい。(ディープラーニングのフレームワークが参考になる)
In [2]:
import os
import sys
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
from src.mnist import load_mnist
from src.multi_layer_net_extend import MultiLayerNetExtend
from src.trainer import Trainer
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 過学習を再現するために、学習データを削減
x_train = x_train[:300]
t_train = t_train[:300]
# Dropuoutの有無、割り合いの設定 ========================
use_dropout = True # Dropoutなしのときの場合はFalseに
dropout_ratio = 0.2
# ====================================================
network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100],
output_size=10, use_dropout=use_dropout, dropout_ration=dropout_ratio)
trainer = Trainer(network, x_train, t_train, x_test, t_test,
epochs=301, mini_batch_size=100,
optimizer='sgd', optimizer_param={'lr': 0.01}, verbose=False)
trainer.train()
train_acc_list, test_acc_list = trainer.train_acc_list, trainer.test_acc_list
# グラフの描画==========
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, marker='o', label='train', markevery=10)
plt.plot(x, test_acc_list, marker='s', label='test', markevery=10)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
Dropout=0.2とした場合、上記結果となる。訓練データセットとテストデータセットの差が縮まってきており、上手く学習が行えている。このようにDropoutを行なうと表現力を保ったまま、過学習を抑制することが出来る。
Dropout実装のポイントとしては順伝播の度に消去するニューロンを選定するmaskを作成する。maskはニューロン毎に予め設定した確率でFalseを発生させ、入力データと掛け合わせることによって消去動作を実現する。逆伝播においてはReLUと同様に順伝播を通常通り行ったニューロンはそのまま、消去したニューロンは信号を止めるため0を掛け合わせる。
Dropoutはアンサンブル学習に類似している。アンサンブル学習では複数のモデルを個別に学習し、推論時に出力の平均を取ることで表現力を高くしているが、Dropoutはランダムにニューロンを消去することで様々なモデルを用いていると考えることが出来るためである。
ニューラルネットワークでは「ハイパーパラメータ(hyper-parameter)」を定める必要がある。各層のニューロンの数やバッチサイズ、パラメータ更新の際の学習係数やWeight decayなどがある。
ハイパーパラメータの決定には多くの試行錯誤が必要だが、効率的に探索する方法がいくつが存在する。
これまで訓練データで学習しテストデータで汎化性能を評価していたが、ハイパーパラメータの検証において、テストデータを用いて調整を行ってはいけない。もし利用した場合、ハイパーパラメータもまたテストデータに対して過学習が発生してしまう恐れがあるためである。 そのためハイパーパラメータを調整する場合には専用の確認データである「検証データ(validation data)」を用意する。まとめると以下となる。
データセットによっては訓練データとテストデータとしか分けられていない場合があるので、訓練データを訓練データと検証データ(20%程度)に分割する。その際にデータの偏りを無くすためにシャッフルすると良い。
ハイパーパラメータの調整においては良い値が含まれる範囲を大まかな広さから徐々に絞り込んで行くと良い。大まかに指定するとは$10^{-3}~10^{3}$のように対数スケールで指定することである。
ディープラーニングの学習は時間がかかるため、早めに悪いパラメータは切り捨てたほうがよい。そのため学習のためのエポックを小さくし、1回評価するための時間を短くすると良い。流れとしては以下の通り。
上記は試行錯誤的にハイパーパラメータを決定したが、「ベイズ最適化(Bayesian optimization)」を用いて決定する方法もある。
In [10]:
import sys, os
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
from src.mnist import load_mnist
from src.multi_layer_net import MultiLayerNet
from src.util import shuffle_dataset
from src.trainer import Trainer
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 高速化のため訓練データの削減
x_train = x_train[:500]
t_train = t_train[:500]
# 検証データの分離
validation_rate = 0.20
validation_num = x_train.shape[0] * validation_rate
x_train, t_train = shuffle_dataset(x_train, t_train)
x_val = x_train[:int(validation_num)]
t_val = t_train[:int(validation_num)]
x_train = x_train[int(validation_num):]
t_train = t_train[int(validation_num):]
def __train(lr, weight_decay, epocs=50):
network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100],
output_size=10, weight_decay_lambda=weight_decay)
trainer = Trainer(network, x_train, t_train, x_val, t_val,
epochs=epocs, mini_batch_size=100,
optimizer='sgd', optimizer_param={'lr': lr}, verbose=False)
trainer.train()
return trainer.test_acc_list, trainer.train_acc_list
# ハイパーパラメータのランダム探索======================================
optimization_trial = 100
results_val = {}
results_train = {}
for _ in range(optimization_trial):
# 探索したハイパーパラメータの範囲を指定===============
weight_decay = 10 ** np.random.uniform(-8, -4)
lr = 10 ** np.random.uniform(-6, -2)
# ================================================
val_acc_list, train_acc_list = __train(lr, weight_decay)
print("val acc:" + str(val_acc_list[-1]) + " | lr:" + str(lr) + ", weight decay:" + str(weight_decay))
key = "lr:" + str(lr) + ", weight decay:" + str(weight_decay)
results_val[key] = val_acc_list
results_train[key] = train_acc_list
# グラフの描画========================================================
print("=========== Hyper-Parameter Optimization Result ===========")
graph_draw_num = 20
col_num = 5
row_num = int(np.ceil(graph_draw_num / col_num))
i = 0
for key, val_acc_list in sorted(results_val.items(), key=lambda x:x[1][-1], reverse=True):
print("Best-" + str(i+1) + "(val acc:" + str(val_acc_list[-1]) + ") | " + key)
plt.subplot(row_num, col_num, i+1)
plt.title("Best-" + str(i+1))
plt.ylim(0.0, 1.0)
if i % 5: plt.yticks([])
plt.xticks([])
x = np.arange(len(val_acc_list))
plt.plot(x, val_acc_list)
plt.plot(x, results_train[key], "--")
i += 1
if i >= graph_draw_num:
break
plt.show()
プロットからBest1~5くらいが上手く学習できているように見える。 ハイパーパラメータを確認すると、学習係数が0.001〜0.1くらい、Weight Decay係数が$10^{-8}~10^{-6}$くらいと言うことが分かる。
この範囲内で更に絞込を行ない範囲を狭めてゆく。このように絞込処理を繰り返すことで最終的にパラメータを決定する。
In [ ]: