5.4. 数値安定性と初期化

これまで実装してきたすべてのモデルでは、 あらかじめ定めた分布に従って パラメータを初期化する必要があった。 これまでは初期化手法を所与のものとして扱い、 その選択方法の詳細には立ち入らなかった。 そのため、こうした選択はそれほど重要ではない という印象を持ったかもしれない。 しかし実際には、初期化手法の選択は ニューラルネットワークの学習において重要な役割を果たし、 数値安定性を保つうえで決定的になることもある。 さらに、これらの選択は 非線形活性化関数の選択とも興味深い形で結びついている。 どの関数を選ぶか、そしてパラメータをどう初期化するかによって、 最適化アルゴリズムがどれだけ速く収束するかが左右される。 ここでの選択を誤ると、学習中に 勾配爆発や勾配消失に直面することがある。 この節では、これらの話題をより詳しく掘り下げ、 深層学習に取り組むうえで有用な いくつかの経験則を紹介する。

%matplotlib inline
from d2l import torch as d2l
import torch
%matplotlib inline
from d2l import mxnet as d2l
from mxnet import autograd, np, npx
npx.set_np()
%matplotlib inline
from d2l import jax as d2l
import jax
from jax import numpy as jnp
from jax import grad, vmap
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
%matplotlib inline
from d2l import tensorflow as d2l
import tensorflow as tf

5.4.1. 勾配消失と勾配爆発

入力 \(\mathbf{x}\) と出力 \(\mathbf{o}\) を持つ \(L\) 層の深いネットワークを考える。 各層 \(l\) は、重み \(\mathbf{W}^{(l)}\) によってパラメータ化された変換 \(f_l\) で定義され、その隠れ層出力を \(\mathbf{h}^{(l)}\) とする(\(\mathbf{h}^{(0)} = \mathbf{x}\) とする)。 このとき、ネットワークは次のように表せる。

(5.4.1)\[\mathbf{h}^{(l)} = f_l (\mathbf{h}^{(l-1)}) \textrm{ and thus } \mathbf{o} = f_L \circ \cdots \circ f_1(\mathbf{x}).\]

すべての隠れ層出力と入力がベクトルであるとき、 \(\mathbf{o}\) の任意のパラメータ集合 \(\mathbf{W}^{(l)}\) に関する勾配は次のように書ける。

(5.4.2)\[\partial_{\mathbf{W}^{(l)}} \mathbf{o} = \underbrace{\partial_{\mathbf{h}^{(L-1)}} \mathbf{h}^{(L)}}_{ \mathbf{M}^{(L)} \stackrel{\textrm{def}}{=}} \cdots \underbrace{\partial_{\mathbf{h}^{(l)}} \mathbf{h}^{(l+1)}}_{ \mathbf{M}^{(l+1)} \stackrel{\textrm{def}}{=}} \underbrace{\partial_{\mathbf{W}^{(l)}} \mathbf{h}^{(l)}}_{ \mathbf{v}^{(l)} \stackrel{\textrm{def}}{=}}.\]

言い換えると、この勾配は \(L-l\) 個の行列 \(\mathbf{M}^{(L)} \cdots \mathbf{M}^{(l+1)}\) と勾配ベクトル \(\mathbf{v}^{(l)}\) の積である。 したがって、 あまりに多くの確率を掛け合わせたときにしばしば生じる 数値アンダーフローと同様の影響を受ける。 確率を扱うときの一般的な工夫は、 対数空間に移ること、すなわち 数値表現の仮数部から指数部へと 負担を移すことである。 残念ながら、ここでの問題はそれより深刻である。 初期状態では、行列 \(\mathbf{M}^{(l)}\) はさまざまな固有値を持ちうる。 それらは小さいことも大きいこともあり、 その積は 非常に大きく非常に小さく もなりうる。

不安定な勾配がもたらす危険は、 数値表現上の問題にとどまらない。 予測不能な大きさの勾配は、 最適化アルゴリズムの安定性も脅かす。 パラメータ更新が (i) 過度に大きくなってモデルを壊してしまう (勾配爆発 問題)か、 あるいは (ii) 過度に小さくなって (勾配消失 問題)、 更新のたびにパラメータがほとんど動かず 学習が不可能になるかもしれない。

5.4.1.1. 勾配消失

勾配消失問題のよくある原因の一つは、 各層の線形演算の後に適用する活性化関数 \(\sigma\) の選択である。 歴史的には、シグモイド関数 \(1/(1 + \exp(-x))\)5.1 章 で導入) は、しきい値関数に似ているため広く用いられていた。 初期の人工ニューラルネットワークは 生物学的ニューラルネットワークに着想を得ていたため、 生体ニューロンのように 完全に 発火するか まったく 発火しないかの どちらかであるニューロンという考え方は魅力的に見えた。 シグモイドを詳しく見て、 なぜ勾配消失を引き起こしうるのかを確認しよう。

x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.sigmoid(x)
y.backward(torch.ones_like(x))

d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()],
         legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))
../_images/output_numerical-stability-and-init_d5976f_18_0.svg
x = np.arange(-8.0, 8.0, 0.1)
x.attach_grad()
with autograd.record():
    y = npx.sigmoid(x)
y.backward()

d2l.plot(x, [y, x.grad], legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))
[07:05:11] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
[07:05:11] ../src/base.cc:48: GPU context requested, but no GPUs found.
../_images/output_numerical-stability-and-init_d5976f_21_1.svg
x = jnp.arange(-8.0, 8.0, 0.1)
y = jax.nn.sigmoid(x)
grad_sigmoid = vmap(grad(jax.nn.sigmoid))
d2l.plot(x, [y, grad_sigmoid(x)],
         legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))
../_images/output_numerical-stability-and-init_d5976f_24_0.svg
x = tf.Variable(tf.range(-8.0, 8.0, 0.1))
with tf.GradientTape() as t:
    y = tf.nn.sigmoid(x)
d2l.plot(x.numpy(), [y.numpy(), t.gradient(y, x).numpy()],
         legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))
../_images/output_numerical-stability-and-init_d5976f_27_0.svg

図から分かるように、シグモイドの勾配は、入力が大きいときも小さいときも消失する**)。 さらに、多くの層を逆伝播するとき、 多くのシグモイドへの入力がゼロ近傍の 勾配が十分に得られる範囲にない限り、 全体の勾配の積は消失してしまう可能性がある。 ネットワークが深くなるほど、 注意しなければ、どこかの層で勾配が 途切れてしまう。 実際、この問題はかつて深層ネットワークの学習を大きく妨げていた。 その結果、より安定した (ただし生物学的にはあまりもっともらしくない) ReLU が、実務上の標準的な選択肢として 広く使われるようになった。

5.4.1.2. 勾配爆発

逆の問題である勾配爆発も、 同様に厄介である。 これを直感的に示すために、 100 個のガウス乱数行列を生成し、 ある初期行列に順に掛け合わせてみる。 ここで選んだスケール (分散 \(\sigma^2=1\))では、 行列積は爆発する。 これが深いネットワークの初期化で起これば、 勾配降下法に基づく最適化器が収束する見込みはない。

M = torch.normal(0, 1, size=(4, 4))
print('a single matrix \n',M)
for i in range(100):
    M = M @ torch.normal(0, 1, size=(4, 4))
print('after multiplying 100 matrices\n', M)
a single matrix
 tensor([[-0.4847, -0.2991, -0.8154, -0.5333],
        [-0.2325, -1.6432,  0.2363, -0.2251],
        [-0.0026, -0.4851, -0.2048, -0.5896],
        [-0.1705,  0.1220,  0.7720, -0.5366]])
after multiplying 100 matrices
 tensor([[ 1.3606e+24,  3.6219e+24, -2.1369e+24, -3.9221e+23],
        [ 9.3126e+24,  2.4791e+25, -1.4626e+25, -2.6846e+24],
        [ 2.2680e+24,  6.0374e+24, -3.5621e+24, -6.5379e+23],
        [ 1.4751e+24,  3.9267e+24, -2.3167e+24, -4.2521e+23]])
M = np.random.normal(size=(4, 4))
print('a single matrix', M)
for i in range(100):
    M = np.dot(M, np.random.normal(size=(4, 4)))
print('after multiplying 100 matrices', M)
a single matrix [[ 2.2122064   1.1630787   0.7740038   0.4838046 ]
 [ 1.0434403   0.29956347  1.1839255   0.15302546]
 [ 1.8917114  -1.1688148  -1.2347414   1.5580711 ]
 [-1.771029   -0.5459446  -0.45138445 -2.3556297 ]]
after multiplying 100 matrices [[ 3.4459747e+23 -7.8040759e+23  5.9973355e+23  4.5230040e+23]
 [ 2.5275059e+23 -5.7240258e+23  4.3988419e+23  3.3174704e+23]
 [ 1.3731275e+24 -3.1097129e+24  2.3897754e+24  1.8022945e+24]
 [-4.4951091e+23  1.0180045e+24 -7.8232368e+23 -5.9000419e+23]]
get_key = lambda: jax.random.PRNGKey(d2l.get_seed())  # PRNGキーを生成する
M = jax.random.normal(get_key(), (4, 4))
print('a single matrix \n', M)
for i in range(100):
    M = jnp.matmul(M, jax.random.normal(get_key(), (4, 4)))
print('after multiplying 100 matrices\n', M)
a single matrix
 [[-0.01468026  0.18429002  0.29529375  1.3654956 ]
 [-0.3123339   0.15577619 -1.126514   -0.279658  ]
 [-0.17748299  1.1886835   0.24319468 -0.02573   ]
 [ 0.8505334   0.4623113  -1.6157581   0.48986113]]
after multiplying 100 matrices
 [[ 1.5799222e+22 -3.6156512e+23 -5.8382020e+21 -1.8848757e+23]
 [ 3.1915137e+22 -7.3038060e+23 -1.1793621e+22 -3.8075463e+23]
 [-1.8222258e+22  4.1701538e+23  6.7335221e+21  2.1739437e+23]
 [-9.0227682e+21  2.0648444e+23  3.3340368e+21  1.0764252e+23]]
M = tf.random.normal((4, 4))
print('a single matrix \n', M)
for i in range(100):
    M = tf.matmul(M, tf.random.normal((4, 4)))
print('after multiplying 100 matrices\n', M.numpy())
a single matrix
 tf.Tensor(
[[ 1.1841711   0.6504623  -0.29817837  0.37453428]
 [-1.0665823  -0.09414102 -2.4775772   0.5071953 ]
 [-1.0149585   0.07600389  2.1217499   0.8125305 ]
 [ 1.6692833  -0.18011202 -1.1919788  -0.01561711]], shape=(4, 4), dtype=float32)
after multiplying 100 matrices
 [[ 3.6784546e+24 -5.8023196e+23  2.4111507e+24  1.9948421e+24]
 [-7.0356487e+24  1.1097884e+24 -4.6117209e+24 -3.8154634e+24]
 [-2.0623473e+24  3.2531121e+23 -1.3518255e+24 -1.1184203e+24]
 [ 4.4600454e+24 -7.0351904e+23  2.9234677e+24  2.4187031e+24]]

5.4.1.3. 対称性を破る

ニューラルネットワーク設計における もう一つの問題は、パラメータ化に内在する対称性である。 1 つの隠れ層と 2 つのユニットを持つ 単純な MLP を考える。 この場合、最初の層の重み \(\mathbf{W}^{(1)}\) を入れ替え、同様に出力層の重みも入れ替えれば、 同じ関数を得られる。 第 1 隠れユニットと第 2 隠れユニットを 区別する特別な理由はない。 言い換えると、各層の隠れユニットの間には 置換対称性がある。

これは単なる理論上の厄介ごとではない。 先ほどの 1 隠れ層・2 ユニットの MLP を考えよう。 説明のために、 出力層が 2 つの隠れユニットを 1 つの出力ユニットに写像するとする。 もし隠れ層のすべてのパラメータを \(\mathbf{W}^{(1)} = c\)(ある定数 \(c\))として初期化したら、 何が起こるだろうか。 この場合、順伝播では どちらの隠れユニットも同じ入力とパラメータを受け取り、 同じ活性化を生成し、 それが出力ユニットに渡される。 逆伝播では、 出力ユニットをパラメータ \(\mathbf{W}^{(1)}\) で微分すると、 すべての要素が同じ値を取る勾配が得られる。 したがって、勾配ベースの反復法(たとえばミニバッチ確率的勾配降下法)を行っても、 \(\mathbf{W}^{(1)}\) のすべての要素は 依然として同じ値のままである。 このような反復だけでは 自力で対称性を 破る ことは決してできず、 ネットワークの表現力を 十分に引き出せないままになるかもしれない。 隠れ層は、あたかも 1 つのユニットしか持たないかのように振る舞うだろう。 ミニバッチ確率的勾配降下法ではこの対称性は破れないが、 (後で導入する)ドロップアウト正則化なら破ることができる点に注意されたい。

5.4.2. パラメータ初期化

上で述べた問題に対処する、あるいは少なくとも軽減する 一つの方法は、慎重な初期化である。 後で見るように、 最適化時にさらに注意を払い、 適切な正則化を行うことで、安定性をさらに高められる。

5.4.2.1. デフォルトの初期化

前の節、たとえば 3.5 章 では、 重みの値を初期化するために 正規分布を用いた。 初期化方法を指定しない場合、フレームワークは デフォルトのランダム初期化を用いる。 中程度の問題規模では、これは実際によく機能することが多い。

5.4.2.2. Xavier 初期化

非線形性 なし の全結合層について、 出力 \(o_{i}\) のスケール分布を考えよう。 この層に \(n_\textrm{in}\) 個の入力 \(x_j\) と、それに対応する重み \(w_{ij}\) があるとすると、 出力は次のように与えられる。

(5.4.3)\[o_{i} = \sum_{j=1}^{n_\textrm{in}} w_{ij} x_j.\]

重み \(w_{ij}\) はすべて 同じ分布から独立にサンプルされるとする。 さらに、この分布の平均が 0、分散が \(\sigma^2\) であると仮定する。 ここで、分布がガウス分布である必要はなく、 平均と分散が存在すれば十分であることに注意されたい。 今のところ、この層への入力 \(x_j\) も 平均 0、分散 \(\gamma^2\) を持ち、 \(w_{ij}\) と互いに独立で、かつ入力同士も独立であると仮定しよう。 この場合、\(o_i\) の平均は次のように計算できる。

(5.4.4)\[\begin{split}\begin{aligned} E[o_i] & = \sum_{j=1}^{n_\textrm{in}} E[w_{ij} x_j] \\&= \sum_{j=1}^{n_\textrm{in}} E[w_{ij}] E[x_j] \\&= 0, \end{aligned}\end{split}\]

また分散は次のようになる。

(5.4.5)\[\begin{split}\begin{aligned} \textrm{Var}[o_i] & = E[o_i^2] - (E[o_i])^2 \\ & = \sum_{j=1}^{n_\textrm{in}} E[w^2_{ij} x^2_j] - 0 \\ & = \sum_{j=1}^{n_\textrm{in}} E[w^2_{ij}] E[x^2_j] \\ & = n_\textrm{in} \sigma^2 \gamma^2. \end{aligned}\end{split}\]

分散を一定に保つ一つの方法は、 \(n_\textrm{in} \sigma^2 = 1\) とすることである。 次に逆伝播を考える。 ここでも同様の問題に直面するが、 今度は勾配が出力に近い層から伝播してくる。 順伝播の場合と同じ考え方を使うと、 \(n_\textrm{out} \sigma^2 = 1\) でない限り、勾配の分散は爆発しうることが分かる。 ここで \(n_\textrm{out}\) はこの層の出力数である。 すると、ジレンマに陥る。 この 2 つの条件を同時に満たすことはできない。 そこで、次を満たすことを目指す。

(5.4.6)\[\begin{aligned} \frac{1}{2} (n_\textrm{in} + n_\textrm{out}) \sigma^2 = 1 \textrm{ or equivalently } \sigma = \sqrt{\frac{2}{n_\textrm{in} + n_\textrm{out}}}. \end{aligned}\]

これが、現在では標準的で実用上も有益な Xavier 初期化 の理論的根拠である。 この手法は、その考案者の第一著者にちなんで名付けられた (Glorot and Bengio, 2010)。 通常、Xavier 初期化では 平均 0、分散 \(\sigma^2 = \frac{2}{n_\textrm{in} + n_\textrm{out}}\) のガウス分布から重みをサンプルする。 また、重みを一様分布からサンプルするときの 分散の選び方にも適用できる。 一様分布 \(U(-a, a)\) の分散は \(\frac{a^2}{3}\) であることに注意されたい。 \(\frac{a^2}{3}\)\(\sigma^2\) に関する条件へ代入すると、 次のように初期化すればよいことが分かる。

(5.4.7)\[U\left(-\sqrt{\frac{6}{n_\textrm{in} + n_\textrm{out}}}, \sqrt{\frac{6}{n_\textrm{in} + n_\textrm{out}}}\right).\]

上の数学的な議論では 非線形性がないことを仮定しているが、 この仮定はニューラルネットワークでは容易に破られる。 それでも、Xavier 初期化は 実際にはうまく機能することが分かっている。

5.4.2.3. さらに先へ

上の議論は、現代的なパラメータ初期化手法の ほんの入口にすぎない。 深層学習フレームワークには、しばしば十数種類もの 異なる経験則が実装されている。 さらに、パラメータ初期化は 深層学習における基礎研究の 非常に活発な分野であり続けている。 そこには、共有パラメータ、超解像、系列モデル、 その他の状況に特化した経験則も含まれる。 たとえば、 Xiao et al. (2018) は、慎重に設計された初期化法を用いることで、 アーキテクチャ上の工夫なしに 10,000 層のニューラルネットワークを学習できる可能性を示した。

この話題に興味があるなら、 このモジュールで扱う各手法を深く掘り下げ、 それぞれの経験則を提案・解析した論文を読み、 さらにこの分野の最新論文を追ってみるとよい。 巧妙なアイデアを見つけたり、 あるいは自ら考案したりして、 深層学習フレームワークへの実装に貢献することもあるかもしれない。

5.4.3. まとめ

勾配消失と勾配爆発は、深いネットワークでよく見られる問題である。勾配とパラメータを適切な範囲に保つためには、パラメータ初期化に細心の注意が必要である。 初期勾配が大きすぎず小さすぎもしないようにするには、初期化に関する経験則が必要である。 ランダム初期化は、最適化の前に対称性を破ることを保証するうえで重要である。 Xavier 初期化は、各層において、任意の出力の分散が入力数の影響を受けず、任意の勾配の分散が出力数の影響を受けないようにすることを意図している。 ReLU 活性化関数は勾配消失問題を緩和し、収束を加速する。

5.4.4. 演習

  1. MLP の各層における置換対称性以外に、対称性を破る必要があるニューラルネットワークの例を設計できるか?

  2. 線形回帰や softmax 回帰では、すべての重みパラメータを同じ値に初期化してもよいだろうか?

  3. 2 つの行列の積の固有値に関する解析的な上界を調べよ。これは、勾配が良条件となるように保つことについて何を示唆しているだろうか?

  4. ある項が発散すると分かっている場合、後から修正できるだろうか? layerwise adaptive rate scaling に関する論文を参照されたい (You et al., 2017)