ここでは学習とは訓練データから最適な重みパラメータの値を自動的に獲得することを指す。
ニューラルネットワークはデータから学習できる特徴がある。これは多数の重みパラメータを人力で設定する必要がなくなるため、有用である。
人にとって分かる規則性でもアルゴリズムで実現しようとすると難しい。アルゴリズムの考案ではなく、画像空特徴量を抽出したえパターンを学習させることで実現する。特徴量とはデータから重要なデータを的確に抽出できるように設計した変換器のことである。 画像の特徴量は通常、ベクトルとして記述される。(コンピュータビジョン分野ではSIFTやSURF、HOGなどが挙げられる)。それらの特徴量を使って画像データをベクトルに変換し、機械学習で用いる識別器で学習を行なう。
アルゴリズムを考える代わりに、問題に応じて適した特徴量を持ちいらなければ良い結果は得られない。(人の手によって特徴量を考える必要がある可能性) 人力においても、機械学習と特徴量によるアプローチも人の手が介在していたが、ディープラーニングでは特徴量の選択自体も機械が学習を行なう。(「5」の認識であっても、「犬」の認識であってもアプローチ方法は変わらない)
ニューラルネットワークは、対象とする問題に関係なくデータをそのまま生データとして「end-to-end」学習することができる。(生データの入力で目的の結果出力まで得られる)
機械学習の問題では作成したモデルの汎化能力を正しく評価したい。そのため訓練データ(教師データ)とテストデータを分離する。一つのデータセットだけで学習、評価を行なうとそのデータセットにしか対応できていないものの良い結果が得られたと誤認してしまう。特定のデータセットのみにだけ過度に適合した状態を過学習(overfitting)と呼ぶ。必ず避けなければ行けない状態。
ニューラルネットワークの学習ではある指標で現在の状態を表す。損失関数(loss function)と呼ばれ、この指標を基準として最適な重みの探索を行なう。関数は任意のものが用いられるが、一般的には二乗和誤差や交差エントロピー誤差が用いられる。 損失関数は教師データからどれだけ逸れているかを表す指標であり、小さくなればなるほど良い性能を示していると言える。
二乗和誤差は以下式で表される。
$$ E = \frac{1}{2}\sum(y_{k}-t_{k})^{2} $$$y_{k}$はニューラルネットワークの出力データ、$t_{k}$は教師データ、$k$はデータの次元数を表す。
出力データはソフトマックス関数のの出力であり、確率と同義である。教師データは正解データのみ「1」、それ以外は「0」で格納されており「one-hot表現」と呼ばれる。
In [4]:
# 2乗和誤差の実装
import numpy as np
def mean_squared_error(y, t):
return 0.5 * np.sum((y-t)**2)
# 「2」が正解
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
# 「2」であると推定する場合(誤差が小さい)
y_2 = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
print(mean_squared_error(np.array(y_2), np.array(t)))
# 「7」であると推定する場合(誤差が大きい)
y_7 = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print(mean_squared_error(np.array(y_7), np.array(t)))
In [5]:
import numpy as np
import matplotlib.pyplot as plt
# データの作成
x = np.arange(0, 1.0, 0.01)
y = np.log(x)
plt.plot(x, y)
plt.show()
自然対数をプロットすると上図のようになる。xが1の時にyが0となる。 よって確率が上がれば上がるほど、誤差である自然対数は0に近づく。
交差エントロピーを実装すると以下となる。 np.logの計算時に微小値であるdeltaを加算している。np.log(0)の場合において無限小(-inf)となりエラーになることを防ぐためである。
In [6]:
def cross_entropy_error(y, t):
delta = 1e-7
return -np.sum(t * np.log(y + delta))
# 「2」が正解
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
# 「2」であると推定する場合(誤差が小さい)
y_2 = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
print(cross_entropy_error(np.array(y_2), np.array(t)))
# 「7」であると推定する場合(誤差が大きい)
y_7 = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print(cross_entropy_error(np.array(y_7), np.array(t)))
これまで算出した損失関数は一つの訓練データについて求めて、できるだけ小さくするようにパラメータを探そうとしていた。実際の学習では全ての訓練データを対象として損失関数を求め、パラメータを算出する必要がある。交差エントロピーの場合は以下式。($t_{nk}$はn個目のデータのk次元目の値を意味する。)
$$ E = - \frac{1}{N} \sum_{n}\sum_{k}t_{nk} log \;y_{nk} $$Nで割るのは訓練データ全ての平均誤差を算出するためであり、データ数に関係なく統一した指標として用いるためである。
しかし、大量の訓練データの場合は計算に時間がかかってしまうため、データの中から一部を選び出して全体の近似として利用する。このような学習方法を「ミニバッチ学習」という。
In [7]:
# ミニバッチ法の実装
import sys, os
sys.path.append(os.pardir)
import numpy as np
from src.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)
# 訓練データをランダムに10枚抜き出す
train_size = x_train.shape[0]
batch_size = 10
# ミニバッチとして選出するインデックスを配列として取得
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
print(x_batch.shape)
print(t_batch.shape)
# 選出したインデックスの配列
print(batch_mask)
In [8]:
# 教師データがone-hot表現の場合
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = t.reshape(1, t.size)
return -np.sum(t * np.log(y)) / batch_size
# 教師データがラベルの場合
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = t.reshape(1, t.size)
return -np.sum(np.log(y[np.arange(batch_size)], t)) / batch_size
なぜ認識精度を指標とせずに、損失関数を設定し精度を測るのか。それはニューラルネットワークの学習において、最適なパラメータを探索する際に損失関数を少なくするようにするためである。t探索においてはパラメータの微分(勾配)を計算し、値を手がかりにパラメータの値を徐々に更新していく。
認識精度では殆どの場所で微分した結果が0になってしまい更新できない。具体的には認識性能32%で合った状態からパラメータを少し変更しても認識性能が32%のままである可能性がある(認識精度はパラメータの微小な変化には反応せず、反応した場合は急に変わる可能性があるため更新の方向が分からない)。これはステップ関数にも同じ事が言える。 一方シグモイド関数はいずれの箇所をとっても微分が0になる箇所が無く、連続的に変化している。
勾配とはどういうものか、どういう性質があるのかを説明する。
単位時間あたりにどれだけ変化があったのかは「平均」として表す事が出来る(時速など)。一方「微分」はその瞬間の変化量を表すものである(1時間→1分→1秒→…としていく)。数式では以下で表される。 (左辺は$f(x)$のxについての微分を表す。)
$$ \frac{df(x)}{dx} = lim_{h→0}\frac{f(x+h)-f(x)}{h} $$Python実装中における微分を行なう上での工夫点
丸め誤差に留意する必要がある(1e-50など小さすぎる値ま丸められる)。$10^{-4}$くらいなら良い。
上記式で微分を行なう際には真の接線ではなく、$x$と$x+h$の間の傾きを求めている(数値微分:numerical differentiation⇔数式的に展開して解析的に解く)ために誤差が生じる。 誤差を減らす工夫として$(x+h)$と$(x-h)$の差分を計算する。この手法は「中心差分」と呼ばれる。
これらの工夫を加えた実装は以下。
In [9]:
def numerical_diff(f, x):
h = 1e-4
return (f(x+h) - f(x-h)) / (2*h)
In [10]:
def function_1(x):
return 0.01*x**2 + 0.1*x
import numpy as np
import matplotlib.pylab as plt
x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x, y)
plt.show()
In [11]:
print(numerical_diff(function_1, 5))
print(numerical_diff(function_1, 10))
In [12]:
# 数値微分における傾きの確認
def tangent_line(f, x):
d = numerical_diff(f, x)
print(d)
y = f(x) - d*x
return lambda t: d*t + y
x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")
tf = tangent_line(function_1, 5)
y2 = tf(x)
plt.plot(x, y)
plt.plot(x, y2)
plt.show()
In [14]:
# 2変数の関数
def function_2(x):
return x[0]**2 + x[1]**2 # or np.sum(x**2)
# x0=3,x1=4のときのx0に対する偏微分
def function_tmp1(x0):
return x0*x0 + 4.0**2.0
print(numerical_diff(function_tmp1, 3.0))
# x0=3,x1=4のときのx1に対する偏微分
def function_tmp2(x1):
return 3.0**2.0 + x1*x1
print(numerical_diff(function_tmp2, 4.0))
In [21]:
def numerical_gradient(f, x):
h = 1e-4
grad = np.zeros_like(x)
for idx in range(x.size):
tmp_val = x[idx]
# f(x+h)
x[idx] = tmp_val + h
fxh1 = f(x)
#f(x-h)
x[idx] = tmp_val - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val
return grad
print(numerical_gradient(function_2, np.array([3.0, 4.0])))
print(numerical_gradient(function_2, np.array([0.0, 2.0])))
print(numerical_gradient(function_2, np.array([3.0, 0.0])))
勾配の意味を確認すyるために以下関数の勾配を図で表す。
$f(x0,x1) = x^{2}_{0} + x^{2}_{1}$
勾配の結果にマイナスを付けたベクトルを描画すると、関数の一番低い場所(最小値)を指し示しているように見える。また、最小値から離れるほどベクトルも大きくなる。
ただし、勾配は必ずしも最小値を指し示すわけではないので注意。各場所において関数の値を最も減らす方向を表すものと考えられる。
In [25]:
# 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):
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) # f(x+h)
x[idx] = tmp_val - h
fxh2 = f(x) # 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):
grad[idx] = _numerical_gradient_no_batch(f, x)
return grad
def function_2(x):
if x.ndim == 1:
return np.sum(x**2)
else:
return np.sum(x**2, axis=1)
def tangent_line(f, x):
d = numerical_gradient(f, x)
print(d)
y = f(x) - d*x
return lambda t: d*t + y
if __name__ == '__main__':
x0 = np.arange(-2, 2.5, 0.25)
x1 = np.arange(-2, 2.5, 0.25)
X, Y = np.meshgrid(x0, x1)
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([-2, 2])
plt.ylim([-2, 2])
plt.xlabel('x0')
plt.ylabel('x1')
plt.grid()
plt.legend()
plt.draw()
plt.show()
機械学習の問題の多くは、学習の際に最適なパラメータを探索することである。最適なパラメータは損失関数が最小値を取る時であるが、損失関数は複雑であるため計算が難しい。そこで勾配を利用して最小値がどこであるか探すのが「勾配法」である。
ただし勾配は最小値でなく、各地点において関数の値を最も減らす方向を示すものである。実際に、複雑な関数においては勾配が指す方向は最小値でないことがほとんど。それでも関数の値を減らすことが出来るため勾配を用いることが多い。
勾配法では現在の地点から勾配方向に移動させ、その先でも同様に勾配を求める。勾配方向への移動を繰り返すことによって関数の値を徐々に減らすのが「勾配法(gradient method)」である。(最小値を探す場合を勾配降下法(gradient descent method)、最大値を探す場合を勾配上昇法(gradient ascent method)と呼ぶが、符号の反転でしかないので本質的には同義)
勾配法を数式で表すと以下となる。
$x_{0}=x_{0} - \eta \frac{\delta f}{\delta x_{0}}$
$x_{1}=x_{1} - \eta \frac{\delta f}{\delta x_{1}}$
$\eta$は更新量を表し、ニューラルネットワークでは「学習率(learning late)」と呼ばれる。1回の学習でどれだけパラメータを更新するかを定める変数。学習率は予め0.01や0.001など定めておく必要があるが、大きすぎても小さすぎても上手く学習することができない。ニューラルネットワークでは学習率の値を変更しながら正しく学習できているか確認作業を行なう。実装は以下となる。
fは最適化したい関数、init_xは初期値、lrは学習率learning rate、step_numは勾配法による繰り返しの数を意味する。
更新を繰り返し最小値へと降下していく様子は以下図となる。
In [37]:
def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x
for i in range(step_num):
grad = numerical_gradient(f, x)
x -= lr * grad
return x
# f(x0,x1)=x0^2+x1^2の最小値を勾配法で求める
def function_2(x):
return x[0]**2 + x[1]**2
init_x = np.array([-3.0, 4.0])
print(gradient_descent(f=function_2, init_x=init_x, lr=0.1, step_num=100))
In [38]:
import numpy as np
import matplotlib.pylab as plt
def gradient_descent_tmp(f, init_x, lr=0.01, step_num=100):
x = init_x
x_history = []
for i in range(step_num):
x_history.append( x.copy() )
grad = numerical_gradient(f, x)
x -= lr * grad
return x, np.array(x_history)
def function_2(x):
return x[0]**2 + x[1]**2
init_x = np.array([-3.0, 4.0])
lr = 0.1
step_num = 20
x, x_history = gradient_descent_tmp(function_2, init_x, lr=lr, step_num=step_num)
plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')
plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")
plt.show()
学習率が大きすぎる場合と小さすぎる場合には良い結果が得られない。以下で確認。
学習率のようなパラメータは「ハイパーパラメータ」と呼ばれる。ニューラルネットワークのパラメータ(重みやバイアス)とは性質が異なり学習アルゴリズムによって自動で獲得されるパラメータではなく、人の手によって設定される。
In [42]:
# 学習率が大きすぎる例(10.0)
# 大きな値へ発散してしまう
init_x = np.array([-3.0, 4.0])
print(gradient_descent(f=function_2, init_x=init_x, lr=10.0, step_num=100))
# 学習率が大きすぎる例(1e-10)
# ほとんど更新されない
init_x = np.array([-3.0, 4.0])
print(gradient_descent(f=function_2, init_x=init_x, lr=1e-10, step_num=100))
ニューラルネットワークでは重みパラメータに関する損失関数の勾配を求める。形状2×3の重み$W$だけ持つニューラルネットワークにおいて損失関数をLで表す場合、勾配は$\frac{\delta L}{\delta W}$と表せる
$$ W = \begin{pmatrix} w_{1 1} & w_{2 1} & w_{3 1} \\ w_{1 2} & w_{2 2} & w_{3 2} \end{pmatrix} $$$$ \frac{\delta L}{\delta W} = \begin{pmatrix} \frac{\delta L}{\delta w_{1 1}} & \frac{\delta L}{\delta w_{2 1}} & \frac{\delta L}{\delta w_{3 1}} \\ \frac{\delta L}{\delta w_{1 2}} & \frac{\delta L}{\delta w_{2 2}} & \frac{\delta L}{\delta w_{3 2}} \\ \end{pmatrix} $$勾配を求める実装は以下のようになる。 予測するためのメソッドpredict(x)、損失関数の値を求めるメソッドloss(x, t)が存在する。xは入力データ、tは正解ラベルである。
In [53]:
import sys, os
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from src.functions import softmax, cross_entropy_error
from src.gradient import numerical_gradient
class simpleNet:
def __init__(self):
self.W = np.random.randn(2,3)
def predict(self, x):
return np.dot(x, self.W)
def loss(self, x, t):
z = self.predict(x)
y = softmax(z)
loss = cross_entropy_error(y, t)
return loss
net = simpleNet()
print(net.W) # 重みパラメータ
x = np.array([0.6, 0.9])
p = net.predict(x)
print(p) # 予測値
print(np.argmax(p)) # 最大値のインデックス
t = np.array([0, 0, 1]) # 正解ラベル
print(net.loss(x, t))
勾配は以下のように求められる。損失関数を減らすためには勾配の逆符号の方向へ更新をかけていく必要がある。
In [55]:
def f(W):
return net.loss(x, t)
# lambda w: net.loss(x, t) と記述することもできる
dW = numerical_gradient(f, net.W)
print(dW)
ニューラルネットワークの学習手順のおさらい
前提
ニューラルネットワークは重みとバイアスが有り、訓練データに適応するように調整することを「学習」という。
ステップ1(ミニバッチ)
訓練データからランダムに一部データ(ミニバッチ)を選び出す。ミニバッチの損失関数の値を減らすことを目的とする。
ステップ2(勾配の算出)
ミニバッチの損失関数を減らすために、各重みの勾配を算出する。
ステップ3(パラメータの更新)
重みパラメータを勾配方向にわずかだけ更新する。
ステップ4(繰り返す)
ステップ1,ステップ2、ステップ3を繰り返す。
ここではミニバッチとして無作為に選ばれたデータを使用していることから、「確率的勾配降下法(stochastic gradient descent。略してSGD)」と呼ばれる。
In [3]:
import sys, os
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
from src.functions import *
from src.gradient import numerical_gradient
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
# x:入力データ, t:教師データ
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
# x:入力データ, t:教師データ
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
return grads
def gradient(self, x, t):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
grads = {}
batch_num = x.shape[0]
# forward
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
# backward
dy = (y - t) / batch_num
grads['W2'] = np.dot(z1.T, dy)
grads['b2'] = np.sum(dy, axis=0)
da1 = np.dot(dy, W2.T)
dz1 = sigmoid_grad(a1) * da1
grads['W1'] = np.dot(x.T, dz1)
grads['b1'] = np.sum(dz1, axis=0)
return grads
In [5]:
import sys, os
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
from src.mnist import load_mnist
# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
# ハイパーパラメータ
iters_num = 10000 # 繰り返しの回数を適宜設定する
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
train_loss_list = []
train_acc_list = []
test_acc_list = []
iter_per_epoch = max(train_size / batch_size, 1)
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)
grad = network.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)
print("train acc, test acc | " + str(train_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("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
訓練データとテストデータ共にエポックが進むに連れて認識精度が向上しており、また互いに認識精度似さがないため過学習は発生していないと言える。