勾配降下法(Gradient descent)
- 勾配降下法(Gradient descent)とは、反復学習によりパラメーターを逐次的に更新し、損失(誤差)の最小化を図るアプローチの一つ
- 最小二乗法は、微分が0になる値を解析的に求めることが可能
- これに対し、対数尤度関数では解析的に値を求めることが困難
- 定義
- $ \boldsymbol{θ}_{t+1} = \boldsymbol{θ}_t - η\dfrac{∂L}{∂\boldsymbol{θ}_t} $
- $ L $は損失関数
- $η$(エータ)は学習率というハイパーパラメーターであり、パラメーターの収束のしやすさを調整
- バッチ(データ数$ n $)の勾配降下法では、$ n $個全てのデータの和を取り、それを平均することになる
- $ \boldsymbol{θ}_{t+1} = \boldsymbol{θ}_t - \dfrac{η}{n} \displaystyle \sum _{i = 1}^{n} \dfrac{∂L}{∂\boldsymbol{θ}_t} $
- つまり、パラメーターを更新するのに$ n $個全てのデータに対する和を計算する必要があり、計算負荷が高い
- また、局所的最適解にはまりやすい
- これを回避するためには、確率的勾配降下法を利用する
- $ \boldsymbol{θ}_{t+1} = \boldsymbol{θ}_t - η\dfrac{∂L}{∂\boldsymbol{θ}_t} $
最急降下法と確率的勾配降下法
- 最急降下法(Gradient decent, steepest descent)
- バッチによる勾配降下法
- 確率的勾配降下法(SGD: Stochastic Gradient Descent)
- バッチ学習が前提である最急降下法をオンライン学習用に改良したもの
- データを一つずつランダムに選んでパラメーターを更新
- 勾配降下法でパラメータを1回更新するのと同じ計算量でパラメータをn回更新できるので効率よく最適な解を探索可能
- $ \boldsymbol{θ}_{t+1} = \boldsymbol{θ}_t - η\dfrac{∂L}{∂\boldsymbol{θ}_t} $
- 実装
# 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]
学習率最適化手法(Optimizer)
- 学習率最適化手法を利用して最適な学習率を見出す
- 学習率の値が大きい場合、発散してしまい、最適解にいつまでもたどり着かない
- 学習率の値が小さい場合、発散することはないが、小さすぎると収束するまでに時間がかかる
- また大域局所最適値に収束しづらくなる
- 基本的な考え方
- 初期の学習率を大きく設定し、徐々に学習率を小さくしていく
- パラメーター毎に学習率を可変させる
モメンタム
- 通常の勾配降下法(誤差をパラメータで微分したものと学習率の積を減算する)に、慣性項(モメンタム項、前回の移動幅に慣性パラメーターをかけたもの)を加える
- 局所的最適解にはならず、大域的最適解となる
- 谷間についてから最も低い位置(最適値)にたどり着くまでの時間が長い(動き続けてしまう)
- 定義
- $ \boldsymbol{v}_{t+1} = α\boldsymbol{v}_t - η\dfrac{∂L}{∂\boldsymbol{θ}_t} $
- $ \boldsymbol{θ}_{t+1} = \boldsymbol{θ}_t + \boldsymbol{v} _{t+1} $
- $ \boldsymbol{v} $はパラメーター$ \boldsymbol{θ} $の更新量
- つまり$ \boldsymbol{v}_t = Δ\boldsymbol{θ} = \boldsymbol{θ}_t - \boldsymbol{θ} _{t-1} $
- $ α $は慣性項のパラメータ(モメンタム係数)であり、前回の更新量にα倍して加算することでパラメータの更新をより慣性的なものにする
- $ \boldsymbol{v}_{t+1} $の計算で$ η\dfrac{∂L}{∂\boldsymbol{θ}_t} $がマイナスされているため、$ \boldsymbol{θ}$の更新はプラスになることに注意
- $ \boldsymbol{v} $はパラメーター$ \boldsymbol{θ} $の更新量
- 実装
# 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 = 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]
- ネステロフのモメンタムという亜種も存在する
- ネステロフのモメンタムの定義
- $ \boldsymbol{v}_{t+1} = α\boldsymbol{v}_t - η\dfrac{∂L}{∂(\boldsymbol{θ}_t + α\boldsymbol{v}_t)} $
- $ \boldsymbol{θ}_{t+1} = \boldsymbol{θ}_t + \boldsymbol{v} _{t+1} $
- ネステロフのモメンタムの定義
AdaGrad
- 誤差をパラメータで微分したものと再定義した学習率の積を減算する
- 最初に勾配が大きいときは学習率も大きくなり、次第に勾配が小さくになるにつれて学習率も小さくなる
- 勾配の緩やかな斜面に対して、最適値に近づける
- 反面、学習率が徐々に小さくなるので、鞍点問題を引き起こすことがある
- 勾配が緩やかなところで動きにくくなってしまう
- 定義
- $ \boldsymbol{h}_{t+1} = \boldsymbol{h}_t + \dfrac{∂L} {∂\boldsymbol{θ}_t} \odot \dfrac{∂L}{∂\boldsymbol{θ}_t} $
- $ \boldsymbol{θ}_{t+1} = \boldsymbol{θ}_t - η\dfrac{1}{ε + \sqrt{\boldsymbol{h} _{t+1}}} \odot \dfrac{∂L}{∂\boldsymbol{θ}_t} $
- $ \odot $はアダマール積(要素ごとの積)
- $ ε $はdivide by zeroを防ぐための小さい定数、ルートの中にあっても外にあっても違いはない
- 実装
# AdaGrad
class AdaGrad:
def __init__(self, lr = 0.01):
self.lr = lr
self.h = None
def update(self, params, grads):
if self.h = 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-7)
RMSProp
- AdaGradの改良版
- AdaGradの欠点だった単調的な学習率低下が改善された
- 誤差をパラメータで微分したものと再定義した学習率の積を減算する
- Decayの概念を入れ、過去のすべて勾配の平均ではなく、指数移動平均を採用することにより、古い学習率は軽視するようになる
- 局所的最適解にはならず、大域的最適解となる
- ハイパーパラメーターの調整が必要な場合が少ない
- 定義
- $ \boldsymbol{h}_{t+1} = ρ\boldsymbol{h}_t + (1-ρ)\dfrac{∂L} {∂\boldsymbol{θ}_t} \odot \dfrac{∂L}{∂\boldsymbol{θ}_t} $
- $ \boldsymbol{θ}_{t+1} = \boldsymbol{θ}_t - η\dfrac{1}{ε + \sqrt{\boldsymbol{h} _{t+1}}} \odot \dfrac{∂L}{∂\boldsymbol{θ}_t} $
- 実装
# RMSProp
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 = None:
self.h = {}
for key, val in params.items():
self.h[key] = np.zeros_like(val)
for key in params.keys():
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
- もっとも使われている現在のところ最強のOptimizer
- モメンタムの過去の勾配の指数関数的減衰平均とRMSPropの過去の勾配の2乗の指数関数的減衰平均の両方を受け継いだ最適化アルゴリズム
- モメンタムとRMSProp両方のメリットを受け継ぐ
- 定義
- $ \boldsymbol{m}_{t+1} = ρ_1\boldsymbol{m}_t + (1-ρ_1)\dfrac{∂L} {∂\boldsymbol{θ}_t} $
- $ \boldsymbol{m} $は1次モーメント、勾配の平均に相当する
- $ \boldsymbol{v}_{t+1} = ρ_2\boldsymbol{v}_t + (1-ρ_2)\dfrac{∂L} {∂\boldsymbol{θ}_t} \odot \dfrac{∂L}{∂\boldsymbol{θ}_t} $
- $ \boldsymbol{v} $は2次モーメント、勾配の分散に相当する(中心化されていない)
- $ \hat{\boldsymbol{m}}_{t+1} = \dfrac{\boldsymbol{m} _{t+1}}{1 - ρ^t_1} $
- $ \hat{\boldsymbol{v}}_{t+1} = \dfrac{\boldsymbol{v} _{t+1}}{1 - ρ^t_2} $
- $ \hat{\boldsymbol{m}}, \hat{\boldsymbol{v}} $はバイアス修正された1次/2次モーメント
- 計算初期の頃のモーメントを補正することが目的であり、計算が進行する($ t $が大きくなる)と分母は1に近づく
- $ \boldsymbol{θ}_{t+1} = \boldsymbol{θ}_t - η\dfrac{1}{\sqrt{\hat{\boldsymbol{v}} _{t+1}} + ε} \odot \hat{\boldsymbol{m}} _{t+1} $
- $ \boldsymbol{m}_{t+1} = ρ_1\boldsymbol{m}_t + (1-ρ_1)\dfrac{∂L} {∂\boldsymbol{θ}_t} $
- 実装
# Adam
class Adam:
def __init__(self, lr = 0.01, rho1 = 0.9, rho2 = 0.999):
self.lr = lr
self.rho1 = rho1
self.rho2 = rho2
self.iter = 0
self.m = None
self.v = None
self.epsilon = 1e-8
def update(self, params, grads):
if self.m = 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.rho1 * self.m[key] + (1 - self.rho1) * grads[key]
self.v[key] = self.rho2 * self.v[key] + (1 - self.rho2) * (grads[key] ** 2)
# モーメントのバイアス補正
m = self.m[key] / (1 - self.rho1 ** iter)
v = self.v[key] / (1 - self.rho2 ** iter)
params[key] -= self.lr * m / (np.sqrt(v) + self.epsilon)
初期値の設定
- 一般的には重みを乱数で初期設定する
- 乱数とすることによって、均一な見方ではなく、多様な見方をできるようにする
- もっともよく利用されるのは、標準正規分布(平均が0で分散が1)
- しかし、重みの初期値設定を工夫することによって、過学習を抑制し学習を進めることが可能になる
Xavier初期化
- Xavier初期化では、標準正規分布を前の層のノード数の平方根で割った値を用いる
- 前の層のノード数を$ n $とすると、$ \sqrt{\frac{1}{n}} $を掛けることになる
- そうすることによって、活性化関数の出力が0 or 1に極端に偏らず、表現力を保てる
- シグモイド関数など、S字カーブ形状の活性化関数の場合に有効
- 逆伝播時も考慮し、入出力のノード数の平均値の平方根で割ることもある
- 前の層のノード数を$ n^{(l)} $, その次の層のノード数を$ n^{(l+1)} $とすると、$ \sqrt{\frac{2}{n^{(l)} + n^{(l+1)}}} $を掛ける
Heの初期化
- Heの初期化では、前の層のノード数の平方根で割った値に$ \sqrt{2} $をかける
- 前の層のノード数を$ n $とすると、$ \sqrt{\frac{2}{n}} $を掛けることになる
- ReLU関数など、S字カーブ形状でない活性化関数の場合に有効
Xavier初期化とHe初期化の実装
- この実装ではXavier初期化は前の層のノード数のみを考慮している
class MultiLayerNet:
'''
input_size: 入力層のノード数
hidden_size_list: 隠れ層のノード数のリスト
output_size: 出力層のノード数
activation: 活性化関数
weight_init_std: 重みの初期化方法
'''
def __init__(self, input_size, hidden_size_list, output_size, activation='relu', weight_init_std='relu'):
# 省略
# 重みの初期化
self.__init_weight(weight_init_std)
# レイヤの生成, 活性化関数はsigmoidもしくはrelu
# 省略
def __init_weight(self, weight_init_std):
all_size_list = [self.input_size] + self.hidden_size_list + [self.output_size]
for idx in range(1, len(all_size_list)):
# 数字指定であればそのまま
scale = weight_init_std
# 'relu', 'he'の場合は、he(前の層のノード数の平方根で割った値にsqrt(2)をかける)
# 大文字で指定されても問題ないように、lower()を入れている
if str(weight_init_std).lower() in ('relu', 'he'):
scale = np.sqrt(2.0 / all_size_list[idx - 1])
# 'sigmoid', 'xavier'の場合は、xavier(前の層のノード数の平方根で割った値)
elif str(weight_init_std).lower() in ('sigmoid', 'xavier'):
scale = np.sqrt(1.0 / all_size_list[idx - 1])
self.params['W' + str(idx)] = scale * np.random.randn(all_size_list[idx-1], all_size_list[idx])
self.params['b' + str(idx)] = np.zeros(all_size_list[idx])
正則化(Regularization)
- ネットワークの自由度(層数、ノード数、パラメーターの値…)を制約することによって、過学習を抑制する
- L1, L2正則化
- 重みが大きい値をとることで、過学習が発生することがあるが、誤差に対して正則化項を加算することで重みを抑制する(重み減衰、Weight decay)
- 正則化項は、モデルの複雑さに伴う罰則を課す罰則項
- 正則化項を加えると、($\boldsymbol{w}$が大きい)本当のMSEにはならず、L2ノルムやL1ノルムを満たす($\boldsymbol{w}$が制約される)中での最小値となる
- 過学習がおこりそうな重みの大きさ以下で重みをコントロールし、かつ重みの大きさにばらつきを出す
- 重みが大きい値をとることで、過学習が発生することがあるが、誤差に対して正則化項を加算することで重みを抑制する(重み減衰、Weight decay)
- 定義
- 目的関数を$ L + λ \Vert \boldsymbol{w} \Vert _p $とする
- 本来の損失関数$ L $に、正則化項(罰則項)$ λ \Vert \boldsymbol{w} \Vert _p $を加える
- ここで、$ \Vert \boldsymbol{w} \Vert _p $はpノルム(距離)
- $ \Vert \boldsymbol{w} \Vert _p = ( \vert w_1 \vert ^p + \cdots + \vert w_n \vert ^p ) ^ {\frac{1}{p}} $
- pノルムは距離を表す
- L1ノルム($ = |x_1| + |x_2| + \cdots + |x_n| $)はマンハッタン距離
- L2ノルム($ = \sqrt{x_1^2+x_2^2+\cdots+x_n^2} $)はユークリッド距離
- $ λ $は正則化の強さを定義する正則化パラメーター
- 実装としては、純粋なpノルムではなく、$ \Vert \boldsymbol{w} \Vert _p^p $とすることが多い(e.g., L2正規化の時は、二乗和の平方根でなく、二乗和を正則化項として加算する)
- また、この際には、微分時にシンプルになるように$ \frac{1}{p} $を掛けることも一般的
- 結果的に、$ L + \dfrac{1}{p}λ \Vert \boldsymbol{w} \Vert _p^p $となる
- 目的関数を$ L + λ \Vert \boldsymbol{w} \Vert _p $とする
- L1ノルムでの実装例
weight_decay += weight_decay_lambda * np.sum(np.abs(network.params['W' + str(idx)]))
loss = network.loss(x_batch , d_batch) + weight_decay
L1正規化
- $ p = 1 $の場合、L1ノルム
- L1ノルム(Lasso推定量、$ |x_1| + |x_2| + \cdots + |x_n| $)はマンハッタン距離であり、これを用いるのがL1正則化(ラッソ回帰、Lasso)
- Lassoは角が尖っているので、いくつかのパラメーターが0になる(結果的にスパースになる)
- 誤差の勾配には、微分したweight_decay_lambda * np.sign(param)が加算される
- np.sign()は符号関数
grad += weight_decay_lambda * np.sign(param)
L2正規化
- $ p = 2 $の場合、L2ノルム
- L2ノルム(Ridge推定量、$ \sqrt{x_1^2+x_2^2+\cdots+x_n^2} $)はユークリッド距離であり、これを用いるのがL2正則化(リッジ回帰、Ridge)
- 実際には、二乗和の平方根でなく、二乗和を正則化項として採用することが一般的
- Ridgeは円なので、円の中で0に近づける(が0になるのは難しい)
- 誤差の勾配には、微分したweight_decay_lambda * 2 * paramが誤差の勾配に加算される(ただし実際には2はweight_decay_lambdaに吸収可能なので、weight_decay_lambda * paramと実装される)
grad += weight_decay_lambda * param
正則化の使い分け
- 最もよく利用されるのは、L2正規化
- 一方でL1正則化は(角があるため)重みが0のパラメーターが出やすい、つまり結果的にスパース化につながるというメリットがある
- L1正則化とL2正則化を組み合わせることもできる(Elastic Net)
ドロップアウト
- 過学習を抑制するために、一定割合のノードをランダムに不活性化させながら学習を行う
- 一定割合は定数(ハイパーパラメーター)で与えられる
- 不活性化とは重みを0にするということ
- 過学習の課題であるノードの数が多い、への対策となる
- データ量を変化させずに、複数のモデルを学習させていると解釈できる
- ドロップアウトを用いたニューラルネットワークの訓練後は、ノードが全て存在した完全なネットワークで予測を行う
- この際、各ノードの出力には存在確率$ p $を掛ける
- 実装
class Dropout:
# dropout_ratioはユニットを消去する割合
def __init(self, dropout_ratio = 0.5):
self.dropout_ratio = dropout_ratio
self.mask = None
def forward(self, x, train_flag = True):
if train_flag = True:
self.mask = np.random.rand(*x.shape) > self.dropout_ratio
return x * self.mask
else:
# 予測時には完全なネットワークを利用するが、出力には存在確率(= 1 - dropout_ratio)を掛ける
return x * (1.0 - self.dropout_ratio)
def backward(self, dout):
return dout * self.mask
バッチ正規化とその類似手法
バッチ正規化(Batch Normalization)
- ミニバッチ単位で、入力値のデータを正規化する(平均0, 標準偏差1の分布にする)
- ミニバッチに含まれる全サンプルの同一チャネルが同一分布に従う
- 初期値への依存が減る、勾配消失が起こりにくくなる、外れ値が少なくなり過学習が起こりにくくなるなど、学習が安定し、結果的に速く学習できる
- 内部共変量シフトの対策となる
- 活性化関数に値を渡す前後に、バッチ正規化の処理を孕んだ層を加える
- 数学的記述
- $ μ_t = \dfrac{1}{N_t} \displaystyle \sum_{i=1}^{N_t}x_{ni} $
- $ σ_t^2 = \dfrac{1}{N_t} \displaystyle \sum_{i=1}^{N_t}(x_{ni} - μ_t)^2 $
- $ \hat{x_{ni}} = \dfrac{x_{ni} - μ_t}{\sqrt{σ_t^2 + θ}} $
- $ y_{ni} = γx_{ni} + β $
- 1でミニバッチ$ t $の平均を、2で分散を求める
- 3で正規化($ θ $はdivide by zeroを防止するためのバイアス項)
- 4はモデルの表現力を維持するために、スケーリングパラメーターで定数倍($ γ $)し、シフトパラメーターである定数項($ β $)を足している
- なお、バッチ正規化では、訓練時には入力データから算出された平均と標準偏差を用いて正規化するが、テスト時(モデル検証時)には、訓練時の移動平均値を用いる
- しかしバッチ正規化はオンライン学習に適用しても意味をなさず、ミニバッチごとのデータ数が小さい場合も正規化を行う平均と分散の値が安定しないため、収束性が悪くなるなど、万能薬というわけではない
- ミニバッチのサイズは学習環境(演算器の性能)によって変わる
バッチ正規化以外の正規化手法
- バッチ正規化では、ミニバッチ内の全サンプルについて、チャンネルごとに平均と標準偏差を計算し、正規化する
- ミニバッチのサイズの影響を受ける
- しかし、バッチ正規化はミニバッチのサイズを大きく取れない場合には、効果が薄くなってしまう
- これに対し、レイヤー正規化やインスタンス正規化といった代替的な手法はミニバッチのサイズの影響を受けない
レイヤー正規化(Layer Normalization)
- レイヤー正規化は、それぞれのサンプルのH×W×C全てのpixelを正規化する
- 結果的に、ミニバッチ内のデータごとに正規化に用いる平均と標準偏差が異なる
- 入力データのスケールに対してロバスト
- 重み行列のスケールやシフトに関してロバスト
- 結果的に、ミニバッチ内のデータごとに正規化に用いる平均と標準偏差が異なる
インスタンス正規化(Instance Normalization)
- インスタンス正規化では、各サンプルの各チャネルごとに正規化する
- 結果的に、ミニバッチ内のデータ×チャンネルごとに正規化に用いる平均と標準偏差が異なる
- コントラストの正規化に寄与する
- 画像のスタイル転送やテクスチャ合成タスクなどで利用される
- 結果的に、ミニバッチ内のデータ×チャンネルごとに正規化に用いる平均と標準偏差が異なる
データ集合の拡張(Dataset Augmentation)
- 学習データが不足するときに人工的にデータを作り水増しする手法
- 分類タスク(特に画像認識)に効果が高い
- オフセット、ノイズ、ドロップアウト、回転、拡大・縮小など様々な手法がある
- 様々な変換を組み合わせて水増しデータを生成する
- 中間層へのノイズ注入で様々な抽象化レベルでのデータ拡張が可能
- データ拡張の結果、データセット内で混同するデータが発生しないよう注意
- 数字の9を180度回転させると6と混同する
- データ拡張の効果と性能評価
- データ拡張を行うとしばしば劇的に汎化性能が向上する
- ランダムなデータ拡張を行うときは学習データが毎度異なるため再現性に注意
- データ拡張とモデルの捉え方
- 一般的に適用可能なデータ拡張(ノイズ付加など)はモデルの一部として捉える
- 特定の作業に対してのみ適用可能なデータ拡張(クロップなど)は入力データの事前加工として捉える
- 例: 不良品検知の画像識別モデルに製品の一部だけが拡大された画像は入力されない