※以下の企画です
前回に続いてゼロつくやっていきます。
今回も4章 -ニューラルネットワークの学習-の続きです。
前回学んだ勾配法を深堀って"学習"をしていく過程の内容っぽいです。
アウトプット難しそ〜〜〜〜
それでは頑張っていきます〜
4章 ニューラルネットワークの学習
勾配法
前回学んだ勾配を用いて、関数が最小値(必ずしも最小ではないらしいが)となるパラメータを見つけようというのが勾配法の考え方である。
数式で表すと以下になる。
x_{t+1} = x_t - \eta \nabla f(x_t)
ここで、
- $x_t$ : 現在のパラメータ
- $\eta$ : 学習率(イータ)
- $\nabla f(x_t)$ : $x_t$における関数$f$の勾配
学習率$\eta$は重要な役割を果たし、高ければ高いほどパラメータを大胆に変更するが、最小値を通り越してしまったりするリスクがある。逆に低すぎると収束に時間がかかる可能性がある。この学習率はハイパーパラメータと呼ばれるもので、機械学習の過程で自動的に導かれるものではなく、操作者が設定する必要がある。
以下はPythonで単純な勾配法を実装する例である。
import numpy as np
def function_2(x):
return x[0]**2 + x[1]**2
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
def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x
for _ in range(step_num):
grad = numerical_gradient(f, x)
x -= lr * grad
return x
# テスト実行
init_x = np.array([-3.0, 4.0]) # 初期値
result = gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100)
print("最適化結果:", result)
最適化結果: [-6.11110793e-10 8.14814391e-10]
上記コードでは2次関数$f(x) = x_0^2 + x_1^2$の最小値を求めている。
-
numerical_gradient
関数は、入力関数$f$に対して勾配を数値的に計算する -
gradient_descent
関数は勾配降下法を適用してパラメータを更新し、最小値に収束させる
というもの。
結果を見てみると、初期値(-3,4)から始めて、計算をstep_num
分繰り返すと非常に小さい値が出力された。
これは最小値(十分に小さい値)を見つけられたということになる。
ニューラルネットワークでの勾配法
ニューラルネットワークでは、勾配法を用いて誤差関数の最小化を目指す。具体的には、以下のような流れで学習を進める:
-
誤差関数(損失関数)の定義:
- ニューラルネットワークの出力と正解ラベルとの誤差を定義する
-
誤差関数の勾配を計算:
- 各パラメータ(重みやバイアス)に関する勾配を計算する
-
パラメータの更新:
- 勾配降下法を用いて、パラメータを更新する
以下に、簡単な実装例(ほぼ写経だけど)を示す。
class SimpleNet:
def __init__(self):
np.random.seed(0)
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)
return cross_entropy_error(y, t)
def softmax(x):
if x.ndim == 2:
x = x - np.max(x, axis=1, keepdims=True) # オーバーフロー防止
x = np.exp(x)
return x / np.sum(x, axis=1, keepdims=True)
x = x - np.max(x) # オーバーフロー防止
return np.exp(x) / np.sum(np.exp(x))
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = y.shape[0]
return -np.sum(t * np.log(y + 1e-7)) / batch_size
def f(W):
return net.loss(x, t)
# 2次元配列対応版
def numerical_gradient(f, x):
reshape_flg = False
# 2次元配列に変換
if x.ndim == 1: # 1次元配列の場合
x = x.reshape(1, x.size)
reshape_flg = True # Trueに変更
h = 1e-4
grad = np.zeros_like(x)
# 勾配を計算
for i in range(x.shape[0]): # 行
for j in range(x.shape[1]): # 列
tmp_val = x[i, j]
# f(x+h)を計算
x[i, j] = tmp_val + h
fxh1 = f(x[i, :])
# f(x-h)を計算
x[i, j] = tmp_val - h
fxh2 = f(x[i, :])
grad[i, j] = (fxh1 - fxh2) / (2 * h)
# 元の値に戻す
x[i, j] = tmp_val
# 勾配を出力
if reshape_flg:
return grad.reshape(grad.size)
else:
return grad
上記では、numerical_gradient
関数がやや変更されている。
というのも、前回学んだままの形式だと、サイズが合わずにエラーを吐き出すためだ。
実行結果は以下。
# 使用例
net = SimpleNet()
print(net.W) #重みパラメータ
x = np.array([0.6, 0.9]) # 入力
p = net.predict(x)
print(p)
t = np.array([0, 0, 1]) # 正解ラベル(one-hot)
net.loss(x,t)
# 勾配を計算
dW = numerical_gradient(f, net.W)
print(dW)
※改行を意図的に入れています※
[[-0.63432209 -0.36274117 -0.67246045]
[-0.35955316 -0.81314628 -1.7262826 ]]
[-0.7041911 -0.94947635 -1.95713061]
[[ 0.29011482 0.22700942 -0.51712425]
[ 0.43517224 0.34051414 -0.77568637]]
一応できてるっぽい。
ニューラルネットワークの具体的な重み更新は、より複雑なネットワークでも基本は同じ手法に基づくので、ココらへんが使えれば基本はできてるって言える…かも?
まとめ
今回は勾配を用いてパラメータを更新する方法を学んだ。
データ量やパラメータ数が増えるとさらにわけわからなくなっていくと思うので、しっかり今回の内容を押さえておきたい…!
また次回も頑張ります〜