※以下の企画です
前回に続いてゼロつくを進めていきます。
今回は第6章「学習に関するテクニック」の内容に入ります。
要点をまとめつつ、ChatGPTなどの力も借りながら理解していきます。
それでは頑張ります〜
6章 学習に関するテクニック
パラメータの更新
ニューラルネットワークの学習において、パラメータの更新手法はモデルの性能や収束速度に大きな影響を与えるとのこと。
ここから代表的な手法を学んでいく。
SGD(確率的勾配降下法)
SGDは、最も基本的なパラメータ更新手法である。損失関数の勾配に学習率を乗じてパラメータを更新する。
W \leftarrow W - \eta \frac{\partial L}{\partial W}
ここで、$W$ はパラメータ、$\eta$ は学習率で0.01とか0.0001みたいな小さい値を前もって決めるらしい。$L$ は損失関数である。
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]
SGDの問題点は、最適化する関数の形状が等方的だと非効率な探索を行ってしまうということ。
(図にするとめちゃくちゃ分かりやすいので是非とも購入いただきたいっ!!!!!!)
その問題を解決するのがこの後説明する手法たちである。
Momentum
Momentumは、過去の勾配情報を利用して更新を滑らかにし、収束を加速させる手法である。
モーメンタムという言葉が「運動」を表すように、物体が勾配する方向に応じて加速するような動きをする。
更新式は以下。
v \leftarrow \alpha v - \eta \frac{\partial L}{\partial W}
W \leftarrow W + v
ここで、$v$ は速度、$\alpha$ はモーメンタム係数よ呼ばれるものである。
コード例は以下。
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 = {key: np.zeros_like(val) for key, val in params.items()}
for key in params.keys():
self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
params[key] += self.v[key]
Momentumを用いると、SGDのときはジグザグだった探索の動きが滑らかになる。
効率はいいらしい。
AdaGrad
パラメータの更新においては学習係数の値が重要になる。学習速度に影響があるからだ。
AdaGradは、各パラメータごとに適応的に学習率を調整する手法である。
頻繁に更新されるパラメータの学習率を減少させ、稀に更新されるパラメータの学習率を維持する。すごい。
更新式は以下。
h \leftarrow h + \left( \frac{\partial L}{\partial W} \right)^2
W \leftarrow W - \frac{\eta}{\sqrt{h + \epsilon}} \frac{\partial L}{\partial W}
ここで、$h$ は過去の勾配の二乗和、$\epsilon$ は微小な値である。
見た目がなんか凶悪すぎてスッと頭に入ってこない。
本当にざっくり理解した感じだと、学習結果の蓄積である$h$が大きく動くときは、$W$の更新時にも影響して大きく動き、学習が進むにつれて変動が小さくなるのでちゃんと最小値にたどり着く みたいな感じっぽい。
コード(写経)は以下。
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 = {key: np.zeros_like(val) for key, val in params.items()}
for key in params.keys():
self.h[key] += grads[key] ** 2
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
これも図は例に漏れず上げないのだが、今までの手法よりも早く最小値にたどり着くような挙動をする。
最初に大きく動いて最後はほぼ動かないみたいな感じ(語彙力)
6.1.4 Adam
Adamは、MomentumとAdaGradを組み合わせた手法であり、学習の高速化と安定化を両立する。
カツカレーの法則で、うまいもの×うまいもので最高にうまいものができるってやつだ。
更新式は以下。
m \leftarrow \beta_1 m + (1 - \beta_1) \frac{\partial L}{\partial W}
v \leftarrow \beta_2 v + (1 - \beta_2) \left( \frac{\partial L}{\partial W} \right)^2
\hat{m} \leftarrow \frac{m}{1 - \beta_1^t}
\hat{v} \leftarrow \frac{v}{1 - \beta_2^t}
W \leftarrow W - \eta \frac{\hat{m}}{\sqrt{\hat{v}} + \epsilon}
ここで、$m$ は一次モーメント、$v$ は二次モーメント、$\beta_1$ と $\beta_2$ は減衰率、$t$ は時間ステップである。
ははははは頭おかしくなる。
コード例は以下。
class Adam:
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.m = None
self.v = None
self.t = 0
def update(self, params, grads):
if self.m is None:
self.m = {key: np.zeros_like(val) for key, val in params.items()}
self.v = {key: np.zeros_like(val) for key, val in params.items()}
self.t += 1
lr_t = self.lr * np.sqrt(1.0 - self.beta2 ** self.t) / (1.0 - self.beta1 ** self.t)
for key in params.keys():
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)
重みの初期化
適切な重みの初期化は、学習の効率やモデルの性能に大きな影響を与える。
重みの初期値を0にする?
全ての重みを0に初期化すると、全てのニューロンが同じ更新を受けニューラルネットワークが機能しなくなる。
同じ初期値を避けるために、ランダムな値で初期化する必要がある。
Xavierの初期化
Xavierの初期化は、入力信号が層を通じて適切に伝播するように設計されている。特にシグモイド関数やtanh関数を使用する場合に有効である。らしい。
初期化式は以下。
W \sim U\left(-\sqrt{\frac{1}{n}}, \sqrt{\frac{1}{n}}\right)
ここで、$n$ は前層のノード数である。
Heの初期化
Heの初期化は、ReLUを活性化関数として使用する場合に適している。
入力信号が適切に伝播するための初期化式は以下の通り。
W \sim N(0, \sqrt{\frac{2}{n}})
以下は、XavierとHeの初期化を実装したコード例。
def xavier_init(size):
return np.random.randn(*size) * np.sqrt(1 / size[0])
def he_init(size):
return np.random.randn(*size) * np.sqrt(2 / size[0])
ここらへんは実際に具体的なテーマでNNを構築してみないと覚えられないなぁ…。
バッチ正規化(Batch Normalization)
バッチ正規化は、各層の出力を正規化することで学習を安定化させ、収束を早める手法である。
めちゃくちゃ有名でかつ実際に機械学習コンペとかでも活躍しているやつらしい。
確かに結構耳にはするかも。
バッチ正規化の数式は以下の通り。
\hat{x} = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}}
y = \gamma \hat{x} + \beta
ここで、$\mu$ は平均、$\sigma^2$ は分散、$\epsilon$ は数値安定性のための微小な値である。$\gamma$ と $\beta$ は学習可能なスケールとバイアスパラメータである。
これをミニバッチ単位で実施していくみたい。
以下はコード例。
class BatchNormalization:
def __init__(self, gamma=1.0, beta=0.0, eps=1e-7):
self.gamma = gamma
self.beta = beta
self.eps = eps
def forward(self, x):
mu = np.mean(x, axis=0)
sigma2 = np.var(x, axis=0)
self.x_hat = (x - mu) / np.sqrt(sigma2 + self.eps)
out = self.gamma * self.x_hat + self.beta
return out
正則化と過学習対策
やっかいな過学習を防ぐために、以下の正則化手法が一般的に使用される。
L1正則化
L1正則化は、損失関数にパラメータの絶対値の総和を追加する手法である。
損失関数の式は以下。
L = \text{loss} + \lambda \sum |W|
L2正則化
L2正則化は、損失関数にパラメータの二乗和を追加する手法である。
損失関数の式は以下。
L = \text{loss} + \lambda \sum W^2
Dropout
Dropoutは、学習時にランダムにノードを無効化することで、汎化性能を向上させる手法である。
これも割と耳にするかも。
コードは以下。
def dropout(x, dropout_ratio):
mask = np.random.rand(*x.shape) > dropout_ratio
return x * mask
まとめ
つかれた。つかれた。
急に手法的な話が盛り沢山になって、小手先の手法だけ集めているような気分。
いや、絶対に全部重要なんだろうけど、全部しっかり手法として身につけるには経験値が足りなさすぎると感じた。
コンペとか探してみようかな…
それでは次回も頑張ります。