学習の最適化手法
Optimizer
確率的勾配降下法(SGD)
最も基本的な手法。勾配に学習率をかけた値でパラメータを更新する。
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]
モーメンタム法
ボールが急斜面は加速して転がり、上り坂になると徐々に減速する、という発想を取り入れた手法。
v が物体の速度にあたり、前回の値から momentum
という値をかけ、今回の勾配を引くことで実現させている。
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]
Nesterov のモーメンタム法
モーメンタム法をより効率良くしたもの。
\[v_{t+1} = \alpha v_t - \eta \frac{dL}{d\Theta}\] \[\Theta_{t+1} = \Theta_{t} + \alpha^2 v_t - (1 + \alpha) \eta \frac{dL}{d\Theta}\]class Nesterov:
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():
params[key] += self.momentum * self.momentum * self.v[key]
params[key] -= (1 + self.momentum) * self.lr * grads[key]
self.v[key] *= self.momentum
self.v[key] -= self.lr * grads[key]
AdaGrad
各パラメータに対して、学習率を個別に適応させる手法。
更新が頻繁に行われるパラメータは学習率が低下し、更新が少ないパラメータは学習率が上がる。
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-07)
RMSProp
AdaGradが勾配の二乗和を無限に蓄積していくと、学習率が0に近づくという問題を解決する手法。
このために、過去の勾配の情報は指数的に忘れられ、最近の情報がより重視される。
AdaGrad の時と学習率の更新度合いを決める式が変わる。
\[G = γ*G + (1-γ)*∇J(θ)^2\]class RMSprop:
def __init__(self, lr=0.01, decay_rate=0.99):
self.lr = lr
self.decay_rate = decay_rate
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.key():
self.h[key] *= self.decay_rate
self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
Adam
MomentumとRMSPropのアイデアを組み合わせた手法で、一般的には最も良好なパフォーマンスを発揮する。
\[m_t = \beta_1m_{t-1} + (1-\beta_1)g_t\] \[v_t = \beta_2v_{t-1} + (1-\beta_2)g_t^2\] \[\hat{m_t} = \frac{m_t}{1-\beta_1^t}\] \[\hat{v_t} = \frac{v_t}{1-\beta_2^t}\] \[\theta_{t+1} = \theta_t - \frac{\alpha}{\sqrt{\hat{v_t}}+\epsilon}\hat{m_t}\]- \(m_t\) は1次モーメント
- \(v_t\) は2次モーメント
- \(\beta_1\) は1次モーメントの指数減衰率
- \(\beta_2\) は2次モーメントの指数減衰率
- \(\hat{m_t}\) は1次モーメントのバイアス補正
- \(\hat{v_t}\) は2次モーメントのバイアス補正
- \(\theta_t\) はt時点のパラメータ
- \(\alpha\) は学習率
- \(\epsilon\) は数値安定性のための定数
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
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)
m = self.m[key] / (1 - self.beta1 ** self.iter) # 1 次モーメント
v = self.v[key] / (1 - self.beta2 ** self.iter) # 2 次モーメント
params[key] -= self.lr * m / (np.sqrt(v) + 1e-7)
初期値
Xavierの初期値
Xavierの初期値は、ニューラルネットワークの重みを適切に初期化する手法のひとつです。 Xavierの初期値を用いることで、勾配消失や爆発を防ぎ、学習を安定化させることができます。 Xavierの初期値は、前の層のノード数をn、次の層のノード数をmとすると、以下のように計算されます。
\[W \sim N(0, \frac{1}{n})\]ここで、 \(N(0, σ^2)\) は平均0、分散 \(σ^2\) の正規分布を表します。 前後のノードがある場合は、それぞれのノード数を \(n_1\) 、 \(n_2\) とすると、
\[W \sim N(0, \frac{2}{n_1 + n_2})\]sigmoid や tahn のような活性化関数と相性が良い。
Heの初期値
Heの初期値は、Xavierの初期値の改良版で、ReLUなどの活性化関数を用いる場合に適した初期値です。 Heの初期値を用いることで、勾配消失や爆発を防ぎ、より高速な学習が可能になります。 Heの初期値は、前の層のノード数をnとすると、以下のように計算されます。
\[W \sim N(0, \frac{2}{n})\]Xavierの初期値とは異なり、Heの初期値では分散が2/nとなっています。
ReLU のような活性化関数と相性が良い。
バッチ正規化
バッチ正規化は、ニューラルネットワークの中間層の出力を正規化することで、学習を高速化し、精度を向上させる手法です。
バッチサイズごとに平均と分散を計算し、それを用いて入力を正規化することで、学習の収束を早めることができます。
バッチ正規化
def batch_normalization(data, epsilon=1e-6):
# 特徴量ごとの平均を計算
batch_means = data.mean(axis=0)
# 特徴量ごとの分散を計算
batch_vars = data.var(axis=0)
data_hat = (data - batch_means)/np.sqrt(batch_vars+epsilon)
data_output = gamma * data_hat + beta
return data_output
テスト時には、平均と標準偏差は訓練時の移動平均を用いる。
画像では、(N, H, W, C) であれば、軸(N, H, W) ([0, 1, 2]) で平均、分散をとる。
レイヤー正規化
レイヤー正規化は、バッチ正規化のように 中間層の出力 を正規化する手法ですが、バッチ正規化と異なり、バッチ内ではなく層内で正規化を行います。
そのため、バッチサイズに依存しないモデルの学習が可能になります。
ミニバッチ内のデータごとに、正規化に用いる平均と標準偏差が異なる。
レイヤー正規化
def layer_normalization(x):
input_mean = np.mean(x, axis = 1)
input_var = np.var(x, axis = 1)
normed_input = (x - input_mean[...,None])/np.sqrt(input_var[...,None]+1e-8)
return normed_input
画像では、軸 (H, W, C) ([1, 2, 3]) で平均、分散をとる。
インスタンス正規化
インスタンス正規化は、バッチ正規化やレイヤー正規化のように、中間層の出力を正規化する手法ですが、 バッチサイズや層内のグループごとではなく、各特徴マップごと に正規化を行います。
マップの形式が (N, C, W, H) とすると、 W, H で正規化される。これにより、画像の場合はコントラストが取り除かれる。
そのため、畳み込み層の特徴マップに対しても適用可能であり、画像の局所的な特徴を捉えることができます。
インスタンス正規化
def instance_normalization(x, epsilon=1e-8):
''' x.shape = (batch_size, channel, data) '''
mean = np.mean(x, axis=(2))
var = np.var(x, axis=(2))
x_normed = (x-mean[...,None])/np.sqrt(var[...,None] + epsilon)
return x_normed
インスタンス正規化は画像データに対して使うケースが多いので、データを画像の縦・横で表した4次元(4重配列)データxを引数とする関数実装は以下のようになる。
def instance_normalization(x, epsilon=1e-8):
''' x.shape = (batch_size, channel, width, height) '''
mean = np.mean(x, axis=(2,3))
var = np.var(x, axis=(2,3))
x_normed = (x-mean[...,None,None])/np.sqrt(var[...,None,None] + epsilon)
return x_normed
画像では、軸 (H, W) ([1, 2]) で平均、分散をとる。
グループ正規化
グループ正規化は、 レイヤー正規化の一種であり、層内の特徴マップをグループ単位で正規化する 手法です。
グループ正規化を用いることで、層内での正規化がうまくいかない場合でも、グループ単位で正規化することで学習を安定化させることができます。
バッチ次元に対して独立しているので、バッチサイズが変化してもサイズが変わらないという特徴がある。
グループ正規化
- 特徴マップを G 個のグループに分割します。それぞれのグループは C/G 個のチャンネルを含みます(ここで C は特徴マップの全チャンネル数です)。
- 各グループ内での平均 \(\mu_g\) と分散 \(\sigma_g^2\) を計算します。具体的には、グループ g の平均と分散は以下のように計算されます:
ここで、 \(x_{gi}\) はグループ g 内のチャンネルの値を表し、m はグループ内のチャンネル数です。
- 次に、各チャンネルを正規化します:
ここで、 \(\epsilon\) は数値的安定性を保つための小さな値(通常 \(10^{-5}\) 程度)です。
- 最後に、スケーリングとシフトを行います:
ここで、 \(\gamma_g\) と \(\beta_g\) は学習可能なパラメータで、それぞれスケールとシフトを制御します。これらはモデルの訓練プロセスの一部として学習されます。
ハイパーパラメータのチューニング
主に、グリッドサーチ、ランダムサーチ、ベイズ最適化がある。
ベイズ最適化
ベイズ最適化は、確率モデル(主に ガウス過程 )を使用してハイパーパラメータ探索を行う手法。
これにより、過去の試行結果から得られた情報を活用して、次に試すべきハイパーパラメータの組み合わせを選択する。
ベイズ最適化は、ランダムサーチやグリッドサーチよりも効率的に探索空間を探索し、少ない試行回数で最適解に収束できる場合がある。
ドロップアウト法
- 学習時、ランダムにノードを消す。 rand は 0 以上 1 未満の乱数を生成する。(似たようなメソッドの randn は標準正規分布に従って乱数を出力する)
- 逆伝播時には、消したノードの勾配は伝えたくないので、そのまま mask をかける
- 推論時は、入力に対し、 1-self.dropout_ratio をかけた値を利用する
class Dropout:
def __init__(self, dropout_ratio=0.5):
self.dropout_ratio = dropout_ratio
self.mask = None
def forward(self, x, train_flg=True):
if train_flg:
self.mask = np.random.rand(*x.shape) > self.dropout_ratio
return x * self.mask
else:
return x * (1.0 - self.dropout_ratio)
def backward(self, dout):
return dout * self.mask