Deep Learning

学習に関するテクニック


パラメータの更新

ニューラクネットワークにおける学習とは、損失関数の値を最小化するパラメータを見つることである。 これは「最適化 (optimization) 」問題である。

SGD

「パラメータの郊外を使って勾配方向にパラメータを更新し、より適したパラメータを見つける」方法を 確率的勾配降下法 (stochastic gradient descent、, SGD) という。

$\displaystyle \begin{equation} \mathbf{W} \leftarrow \mathbf{W} - \eta \frac{\partial L}{\partial \mathbf{W}} \end{equation}$
SGD.py
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]

クラスのコンストラクタの引数の $lr$ は learning rate (学習係数) を表す。

SGD.py
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]

ディープラーニングのフレームワークである Lasagne では、updates.py というファイルに最適化の関数がまとめて実装されている。 ユーザはその中からupdate関数を選んで使うことができる

SGDの欠点

例として次の関数の最小値を求める問題を考える。

$\displaystyle f(x, y) = \frac{1}{20} x^2 + y^2 \quad\quad\quad (6.2) $

(6.2) の関数は、
$\displaystyle \displaystyle \frac{\partial f}{\partial x} = \frac{1}{10} x \\ \displaystyle \frac{\partial f}{\partial y} = 2y \\ $
なので、初期値を $(x, y) = (-7.0, 2.0)$ として SGD を計算すると、 次のように y 座標が振動し、かなり非効率な経路となる。 これは勾配方向が本来の最小の方向ではないことが原因である。

SGD.py を使う例
>>> x, y = -7.0, 2.0
>>> for i in range(1,10):
... x, y = (x - x/10.0), (y - 2 * y)
... print(x, " ", y)
...
-6.3   -2.0
-5.67   2.0
-5.103  -2.0
-4.5927  2.0
-4.13343 -2.0
...(略)...

Momentum

$\displaystyle \mathbf{v} \leftarrow \alpha \mathbf{v} - \eta \frac{\partial L}{\partial \mathbf{W}} \quad\quad\quad (6.3) \\ \mathbf{W} \leftarrow \mathbf{W} + \mathbf{v} \quad\quad\quad (6.4) $

$\mathbf{v}$ は物理における「速度」に相当する値である 物体が勾配方向に力を受けると速度が変化する状態を表す式である。。

momentum.py
class Momentum:
  def __init__(self, lr=0.01, momentum=0.0):
    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]

式 (6.3), (6.4) を実行すると、次のようにジグザグの動きを軽減して、解に速く近づくことができる。

momentum_log.py
w = np.array([ -7, 2 ])
v = np.array([ 0, 0 ])
for i in range(1, 50):
  g = np.array([ w[0]/10, 2 * w[1]])
  v = v * 0.9 - g * 0.1
  w = w + v
  print(w[0], " ", w[1])

# execution log
-6.93   1.6
-6.7977   0.92
-6.610653   0.124
-6.37620417   -0.6172
-6.1014381813   -1.16084
-5.79313440966   -1.417948
-5.45772967108   -1.3657556
-5.10128810965   -1.04563132
-4.72947782327   -0.548393204
-4.34755378729   0.0087997412
-3.96034661704   0.50851344364
-3.57225669765   0.856553087108
-3.18725320321   0.998478148808
-2.80887752619   0.926515074576
-2.44025064161   0.676445292852
-2.08408393907   0.31609343073
-1.74269306739   -0.0714419313256
-1.41801435221   -0.405935370911
-1.11162336502   -0.625792392355
-0.824755242901   -0.698505233184
-0.558326380566   -0.624245743293
-0.312957140658   -0.432563053733
-0.0889952533349   -0.173536022382
0.11346039779   0.0942955103101
0.294535879824   0.316484787671
0.454558454856   0.453158179762
0.594033187837   0.485532596691
0.713620115641   0.417563052589
0.814112149508   0.27287785238
0.896413858494   0.0880856017151
0.961521257996   -0.0958445442259
1.01050270497   -0.242212766728
1.04448098019   -0.325501613634
1.06461661809   -0.335361253122
1.07209252602   -0.277162678038
1.0680999179   -0.169351424854
1.05382557141   -0.0384510120178
1.03044040385   0.0870495619383
0.999089349016   0.182590166111
0.960882506171   0.232058676644
0.916887522549   0.230168600796
0.868123162064   0.182433812372
0.815554006007   0.102985740317
0.760086225495   0.0108853274039
0.702564360779   -0.0741821096987
0.643769038928   -0.135906381151
0.584415558872   -0.164276949228
0.525153271233   -0.156955070652
0.466565679646   -0.118974365803

AdaGrad

ニューラルネットワークの学習では、学習係数 $\eta$ の値が重要になる。 学習係数が小さ過ぎると学習に時間がかかり、また、大き過ぎると発散して正しい値に収束しなくなる。

これに対応するには「学習係数を減衰させる (learning rate decay)」という方法を用いる。 最初は大きく学習するところかは始め、だんだん小さく学習するように変更する。 これは、パラメータ全体の学習係数の値を一括して下げることを意味する。

AdaGrad はパラメータ毎に別々に学習係数を調整する方法である。

$\displaystyle \mathbf{h} \leftarrow \mathbf{h} + \frac{\partial L}{\partial \mathbf{W}} \odot \frac{\partial L}{\partial \mathbf{W}} \quad\quad\quad (6.5) \\ \displaystyle \mathbf{W} \leftarrow \mathbf{W} - \eta \frac{1}{\sqrt{\mathbf{h}}} \frac{\partial L}{\partial \mathbf{W}} \quad\quad\quad (6.6) $

ただし $\odot$ は、要素同士の掛け算を表すものとする。 $\mathbf{h}$ は、これまでに経験した勾配の値の2乗和である。 パラメータを更新する際に、$\displaystyle \frac{1}{\sqrt{\mathbf{h}}}$ を乗算することで、 学習のスケールを調整する。大きく値が動いたパラメータの学習係数が小さくなることを意味する。

AdaGrad では $\mathbf{h}$ が単調増加するので、学習を進めるほど更新度合いが小さくなる。 この問題を改善するために RMSProp では過去の全ての勾配を加算するのではなく、 指数移動平均で計算する。

adagrad.py
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-7)

Adam

Adam は「 Momentum と AdaGrad を融合し、さらにハイパーパラメータをバイアス補正した」 ものである。

Diederik Kingma and Jimmy Ba. (2014): Adam: A Method for Stochastic Optimization.
arXiv:1412.6980 [cs] (December 2014).
https://arxiv.org/abs/1412.6980       pdf

重みの初期値

Weight decay (荷重減衰) は、過学習を抑え、汎化性能を高めるテクニックである。 重みパラメータの値を小さくすることで過学習が起きにくくなる。

重みの値を小さい値にしたい場合は初期値をできるだけ小さい値から始める方がよいと思われるが、 ただし0から始めてはいけない。初期値を0とすると、更新しても重みが均一のままになってしまうので。 したがって、重みが均一になってしまうことを防ぐため、均一ではないランダムな初期値が必要となる。

隠れ層のアクティベーション分布

活性化関数にシグモイド関数を用いた5層のニューラルネットワークを考える。 このネットワークにランダムに生成した入力データを流し、各層の アクティベーションのデータ分布を描画してみる。

それぞれの層が100個のニューロンを持つ5層からなるニューラルネットワークを考える。 入力データとして 1000 個のデータをガウス分布でランダムに生成してネットワークに流す。 各層のアクティベーションの結果を activations という変数に格納し、 ヒストグラムとして描画する。

weight_init_activation.py
import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
  return 1 / (1 + np.exp(-x))

x = np.random.randn(1000,100)

node_num = 100
hidden_layer_size = 5
activations = {}

for i in range(hidden_layer_size):
  if i != 0:
    x = activations[i-1]
  w = np.random.randn(node_num, node_num) * 1
  z = np.dot(x, w)
  a = sigmoid(z)
  activations[i] = a

for i, a in activations.items():
  plt.subplot(1, len(activations), i+1)
  plt.title(str(i+1) + "-layer")
  plt.hist(a.platten(), 30, range=(0,1))
plt.show()

Xavier の初期値

Xavier Glorot and Yoshua Bengio (2010): Understanding the difficulty of training deep feedforward neural networks.
In Proceedings of the International Conference on Artificial Intelligence and Statistics (AISTATS2010).
Society of Artificial Intelligence and Statistics.
http://machinelearning.wustl.edu/mlpapers/paper_files/AISTATS2010_GlorotB10.pdf       pdf

各層のアクティベーションを同じ広がりのある分布にすることを目的として、 適切な重みのスケールを導いた。 結論は「前層のノードの個数を $n$ として、$\displaystyle \frac{1}{\sqrt{n}}$ の標準偏差を持つ分布を使う」というもの。 これは、前層のノードが多いほど、重みの初期値を小く設定することを意味する。

図6-13 は、前層のノードの個数を $n$ として、重みの標準偏差を $\frac{1}{\sqrt{n}}$ とした例。 0.5付近が多い、広がりを持った分布になる。

[注意] 活性化関数として sigmoid 関数((0.5, 0)で点対称)ではなく tanh 関数((0,0)で点対称)を使うときれいな釣鐘型の分布になる。 分布になる。

Xavier の初期値では、活性化関数が線形であることを前提とする。 sigmoid や tanh 関数は左右対称で中央付近が線形関数と見なせるので Xavier の方法が適している。

ReLU の場合の重みの初期値

ReLUでは「左右対称でもないし、中央付近を線形と見なせない」ので、「Xavierの初期値」ではなく 「He の初期値」を使う。

Kaiming He, Xiangyu Zhang, Shaoqing Ren, and Jian Sun (2015):
Delving Deep into 
https://arxiv.org/abs/1502.01852       pdf

「He の初期値」では「前層のノード数を $n$ として、$\displaystyle \sqrt{\frac{2}{n}}$ を標準偏差とするガウス分布」を使う。 ReLU では負の領域が0になるため、「Xavierの初期値」より $\sqrt{2}$ 倍ほど広がりを持った分布が必要になると解釈できる。

図6-14 は、活性化関数として ReLU を用いた場合の分布を表している。 「標準偏差が 0.01 のガウス分布」「Xavierの初期値」「Heの初期値」のそれぞれが 示されているが、「Heの初期値」の場合は分布が一様になっているのがわかる。

MNISTデータセットによる重み初期値の比較

図6-15 は、活性化関数として ReLU を用いた場合の分布を表している。 MNIST データに対して、 「標準偏差が 0.01 のガウス分布」「Xavierの初期値」「Heの初期値」 のそれぞれで学習した結果が示されている。

「標準偏差が 0.01 のガウス分布」では学習がほとんど進んでいない。 「Xavierの初期値」と「Heの初期値」ではともに学習が進むが、 「Heの初期値」の方が優秀であることがわかる。


Batch Normalization

Sergey Ioffe and Christian Szegedy (2015): 
Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift.
arXiv:1502.03167 [cs] (February 2015).
https://arxiv.org/abs/1502.03167      pdf

各層が適度な広がりを持つように、強制的にアクティベーションの分布を調整する方法。 アフィン変換と 活性過関数 (ReLU 関数) の間に、「データ分布の正規化を行う層」を挿入する。

$\displaystyle \begin{equation} \mu_{B} \leftarrow \frac{1}{m} \sum_{i=1}^{m} x_i \\ \sigma_{B}^{2} \leftarrow \frac{1}{m} \sum_{i=1}^{m} (x_i -\mu_{B})^{2} \quad\quad\quad (6.7) \\ \hat{x} \leftarrow \frac{x_i - \mu_{B}}{\sqrt{\sigma_{B}^{2} + \epsilon}} \end{equation} $

ミニバッチとして $B=\{ x_1, x_2, \cdots, x_m \}$ という$m$個の入力データの集合に対して、 平均 $\mu_B$ , 分散 $\sigma^2_B$ を求める。 そして、入力データが平均で 0 、分散 1 のデータ $\{ \hat{x_1}, \hat{x_2}, \cdots, \hat{x_m}\}$ となるように正規化する。$\epsilon$ は$0$での除算を避けるための小さな値を表す。

Batch Norm レイヤは、この正規化されたデータに対して、固有のスケールとシフトで $y_i \leftarrow \gamma \hat{x_i} + \beta \quad\quad\quad (6.8)$ という変換を行う。 ここで $\gamma$ と $\beta$ は $\gamma = 1, \beta = 0$ からスタートして 学習によって適した値に調整されていく。

Frederik Kratzert: Understanding the backward pass through Batch Normalization Layer
https://kratzert.github.io/2016/02/12/understanding-the-gradient-flow-through-the-batch-normalization-layer.html

図 6-18 は、MNIST データに対して Batch Norm レイヤを使わない場合と使う場合の比較である。 Batch Norm を使うと、学習が速く進み、また重みの初期値に対してロバストになることがわかる。


正則化

機械学習の問題では、過学習 (overfitting) が問題になることがある。 過学習が起きる原因は、主として次のものが挙げられる。

図6-19 は、上記の過学習がおきる原因をどちらも満たしていて、 実際に過学習が起きる例である。

Weight decay

過学習抑制のために、Weight decay (荷重減衰) という手法を使うことがある。 これは、大きな重みを持つことにペナルティを与えて、過学習を抑制しようとする方法である。

たとえば、重みを $\mathbf{W}$ とすると L2 ノルム (各項目の2乗和) の Weight decay は、 $\displaystyle \frac{1}{2} \lambda \mathbf{W}^2$になり この値を損失関数に加算することで、$\mathbf{W}$ が大きくなることを抑制できる。

Weight decay は、すべての重みに対して損失関数に $\displaystyle \frac{1}{2} \lambda \mathbf{W}$ を加算する。重みの勾配を求める計算では、誤差逆伝播法による結果に、正則化学項の微分 $\lambda \mathbf{W}$ を加算する。

[注意] $\displaystyle \frac{\partial}{\partial \mathbf{W}} (\frac{1}{2} \lambda \mathbf{W}) = \lambda \mathbf{W}$ である。

Dropout

過学習を抑制するために Weight decay を使うことが多いが、ニューラルネットワークが複雑に なると Weight decay だけでは対応できなくなる。その場合は Dropout がよく使われる。

Dropout はニューロンをランダムに消去しながら学習する方法である。

訓練時には、データが流れるたびに隠れ層のニューロンをランダムに選択し、そのニューロンを消去する。 テスト時には、すべてのニューロンの信号を伝達するが、各ニューロンの出力に対して、 訓練時に消去した割合を乗算して出力する。

dropout.py
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

順伝播のたびに self.mask に消去するニューロンを False として格納する。 self.mask にxと同じ大きさの配列を生成し、 ランダムな値を生成してその値が dropout_ratio よりも大きければ True に、 小さければ False に設定する。

逆伝播の際の挙動は ReLU と同じで、 順伝播で信号を通したニューロンは逆伝播の際に伝わる信号をそのまま通し、 順伝播で信号を通さなかったニューロンは、逆伝播では信号を通さない。

図 6-25 は、訓練データとテストデータの認識精度のグラフである。 Dropout を用いると訓練データとテストデータの認識精度の差が少くなり、 また、訓練データで認識率が100% になることがなくなる。 すなわち、Dropout を用いると表現力の高いネットワークであっても過学習を避けることができる。

機械学習の一手法であるアンサンブル学習では、 複数のモデルを個別に学習させて、推論時にはその複数の出力を平均する。 Dropout はニューロンをランダムに消去することで、毎回異なるモデルを 学習させていることになるので、アンサンブル学習と似た手法だともいえる。


ハイパーパラメータの検証

ニューラルネットワークでは、重みやバイアスといったパラメータとは別に、

などといった「ハイパーパラメータ (hyper-parameter)」 が多く登場する。 できるだけ効率的にハイパーパラメータの値を探索する必要がある。

検証データ

機械学習では、訓練データで学習を行い、テストデータで汎化性能を評価してきた。 しかし、ハイパーパラメータの性能評価にテストデータを使ってはいけない (テストデータに対して単に過学習を引き起こしているだけなので)。 ハイパーパラメータを調整するためには、ハイパーパラメータ専用の確認データ (検証データ, validation data) が必要になる。

ハイパーパラメータの最適化

ハイパーパラメータの値を決定するには、良い値をとる範囲を徐々に絞り込んでいく。 最初はおおまかに範囲を設定し、 その範囲の中からランダムにハイパーパラメータの値を選び出し(サンプリングし)、 認識精度を評価し、良い評価となるハイパーパラメータの範囲を狭めていく。

  1. ステップ 0:
  2. ハイパーパラメータの範囲を設定する。

  3. ステップ 1:
  4. 設定されたハイパーパラメータの値の範囲から、ランダムにサンプリングする。

  5. ステップ 2:
  6. 学習を行い、検証データで認識精度を評価する(エポックは小さく設定する)。

  7. ステップ 3:
  8. ステップ1とステップ2をある回数 (100回程度) 繰り返し、 認識精度の結果を評価してハイパーパラメータの範囲を狭める。

James Bergstra and Yoshua Bengio (2012): Random Search for Hyper-Parameter Optimization.
Journal of Machine Learning Research 13, Feb (2012), 281-305.
bergstra12a.pdf

ハイパーパラメータの最適化において、ベイズ最適化(Bayesian optimization) を使う方法もある。

Jasper Snoek, Hugo Larochelle, and Ryan P. Adams (2012): Practical Bayesian Optimization of Machine Learning Algorithms
In F. Pereira, C.J.C. Burges, L. Bottou, & K. Q. Weinberger, eds.
Advances in Neural Information Processing Systems 25. Curran Associates, Inc., 2951-2959.
4522-practical-bayesian-optimization-of-machine-learning-algorithms.pdf

Hyper Parameter Optimization の実装

MNIST データセットを使って、ハイパーパラメータの最適化を行う。 扱うハイパーパラメータは次の2種類。

  weight_decay = 10 ** np.random.uniform(-8, -4) # 10^{-8} から 10^{-4} の間の一様分布
  lr = 10 ** np.random.uniform(-6, -2)

検証データの学習の推移を認識精度が高かった順に並べて、下位の範囲を切り捨てて、 範囲を狭めていく。


Yoshihisa Nitta

http://nw.tsuda.ac.jp/