対象者
前回の記事はこちらです。
本記事では学習則の実装を見ていきます。と言ってもここで既に本体は実装していますので併せてご覧ください。
次回の記事はこちら
目次
学習則
まずはいつも通りスカラで考えていきます。
ニューロンオブジェクトは変数$w, b$を持ちますね。
y = \sigma(xw + b)
ここで、入力$x$を定数だとみなすと
y = f(w, b) = \sigma(xw + b)
のように書くことができます。
つまり$w, b$の値を適切に変えて目的とする値$y^{\star}$に近づける、というのが学習則の目標になります。
学習則理論
では理論的に見ていきましょう。
y = f(w, b) = \sigma(wx + b)
において
\begin{align}
y &= y^{\star} = 0.5 \\
x &= x_0 = 1
\end{align}
とし、活性化関数をsigmoid関数にして$w, b$のパラメータ空間を図示すると以下のようになります。
コード
%matplotlib nbagg
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
x_0 = 1
y_star = 0.5
sigma = lambda x: 1/(1+np.exp(-x))
w = np.arange(-2, 2, 1e-2)
b = np.arange(-2, 2, 1e-2)
W, B = np.meshgrid(w, b)
y = 0.5*(sigma(x_0*W + B) - y_star)**2
elevation = np.arange(np.min(y), np.max(y), 1/2**8)
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.set_xlabel("w")
ax.set_ylabel("b")
ax.set_zlabel("loss")
ax.view_init(60)
ax.grid()
ax.plot_surface(W, B, y, cmap="autumn", alpha=0.8)
ax.contour(W, B, y, cmap="autumn", levels=elevation, alpha=0.8)
fig.suptitle("Loss space")
fig.show()
fig.savefig("Loss_space.png")
図は損失関数に二乗誤差を用いた場合の損失空間を示しています。
ランダムな初期値$w_0, b_0$から最適値$y^{\star}$を与える$w^{\star}, b^{\star}$へと漸近していくのが学習則の目的ですね。
この時学習則として使われるのが最急降下法から発展した勾配降下法です。
勾配降下法とは、各パラメータのある地点での勾配(偏微分)を利用して坂を下る手法のことを言います。
ここで各手法について数式とコードを載せています。
また、ここにいくつかの探索空間での降下の様子も載せています。
本記事では最もシンプルなSGDについて取りあげます。
SGDの数式は以下の通りです。
\begin{align}
g_t &= \nabla_{w_t}\mathcal{L}(w_t, b_t) \\
\Delta w_t &= - \eta g_t \\
w_{t+1} &= w_t + \Delta w_t
\end{align}
この数式では$w$についてのみしか載せていませんが、$b$と置き換えればバイアスへの学習則も同様になることがわかります。
$\mathcal{L}(w_t, b_t)$は損失関数で、$\nabla_{w_t}$は$w_t$に関する偏微分を表すと考えてください(上記の数式は行列表現になっています)。
上式を日本語で表すと
- 偏微分で勾配を求める
- 移動量を求める
- 移動する
のようになります。シンプルですね。詳しく見ていきましょう。
最初の「偏微分で勾配を求める」ところは逆伝播で紹介した誤差逆伝播法を用います。これはそれでいいでしょう。「移動する」も文字通りです。
「移動量を求める」部分についてですが、
- なぜマイナスをつけるのか
- なぜ勾配をそのまま用いずに$\eta \ll 1$を乗算するのか
の2点についてお話しします。
まず、1.についてですが、これは実際に具体的に考えるとわかりやすいかと思います。
例えば$(x, y) = (1, 1)$の地点での傾きは$2$になりますが、移動して欲しい方向はマイナス方向ですよね。もちろん逆も然りです。ということで、移動して欲しい方向と勾配は常に逆符号となるためにマイナスをつけています。
2.についても、やはりグラフを見ればわかると思いますが、傾き$2$をそのまま用いて$\Delta x = -2$などとしてしまうと$x=-1$となり、最適値を通り越してしまいます。
そのため、学習率と呼ばれる係数$\eta \ll 1$を乗算して移動量を制限し、最適値に向かって徐々に降っていくようにします。
この学習率はハイパーパラメータと呼ばれる値で、この部分を人間が設計しなければならない学習則が多いです。
大抵の場合は論文などで与えられるデフォルト値を用いれば上手く動作しますが、解きたい問題によっては色々と試してやる必要があったります。
学習則実装
まあ詳しいことはさておき、とりあえず実装しましょう。実装先はいつもの通りbaselayer.pyです。
baselayer.py
def update(self, **kwds):
"""
パラメータ学習の実装
"""
dw, db = self.opt.update(self.grad_w, self.grad_b, **kwds)
self.w += dw
self.b += db
self.opt.update(self.grad_w, self.grad_b, **kwds)
の部分はこちらに丸投げしています。例としてSGDのコードを掲載しておきます。
optimizers.py
import numpy as np
class Optimizer():
"""
最適化手法が継承するスーパークラス。
"""
def __init__(self, *args, **kwds):
pass
def update(self, *args, **kwds):
pass
class SGD(Optimizer):
def __init__(self, eta=1e-2, *args, **kwds):
super().__init__(*args, **kwds)
self.eta = eta
def update(self, grad_w, grad_b, *args, **kwds):
dw = -self.eta*grad_w
db = -self.eta*grad_b
return dw, db
コードの内容は上で紹介した数式通りですね。
外から$w, b$に関する勾配を受け取り、学習則の通り$-\eta$倍して移動量を決定し投げ返しています。
この移動量をレイヤーオブジェクトが受け取り、自身のパラメータを更新します。
さて、今回の内容は以上です。「あれ?行列の場合は?」と思う方ももしかしたらいるかもしれませんね。
実は行列の場合も全く同様のコードになります。optimizers.pyを見てみても行列積の演算などは出てきていません。なぜかというと、考えてみれば当然なのですが、例えミニバッチで学習するとしても流れてくる勾配は各パラメータごとに固有のものであるはずで、他のパラメータの勾配を絡めて演算する必然性がないためです。
ということで今回はスカラで考えて実装したら行列でも同じように計算できます。
__init__
メソッドの実装
では最後に__init__
メソッドで最適化子opt
をレイヤーオブジェクトに持たせるようにしておきましょう。
def __init__(self, *, prev=1, n=1,
name="", wb_width=5e-2,
act="ReLU", opt="Adam",
act_dic={}, opt_dic={}, **kwds):
self.prev = prev # 一つ前の層の出力数 = この層への入力数
self.n = n # この層の出力数 = 次の層への入力数
self.name = name # この層の名前
# 重みとバイアスを設定
self.w = wb_width*np.random.randn(prev, n)
self.b = wb_width*np.random.randn(n)
# 活性化関数(クラス)を取得
self.act = get_act(act, **act_dic)
# 最適化子(クラス)を取得
self.opt = get_opt(opt, **opt_dic)
おわりに
次回は活性化関数と最適化子のローカライズ、それから損失関数の紹介ですかね〜