.. _sec_numerical_stability:
数値安定性と初期化
==================
これまでに実装してきたすべてのモデルでは、
あらかじめ指定された分布に従って パラメータを初期化する必要があった。
これまでは初期化手法を当然のものとして扱い、
その選択がどのように行われるかという詳細には触れてこなかった。
そのため、こうした選択はそれほど重要ではない
という印象を持ったかもしれない。 しかし実際には、初期化手法の選択は
ニューラルネットワークの学習において重要な役割を果たし、
数値安定性を保つうえで決定的になることもある。 さらに、これらの選択は
非線形活性化関数の選択と興味深い形で結びついている。
どの関数を選ぶか、そしてパラメータをどう初期化するかによって、
最適化アルゴリズムがどれだけ速く収束するかが決まる。
ここでの選択を誤ると、学習中に 勾配爆発や勾配消失に遭遇することがある。
この節では、これらの話題をより詳しく掘り下げ、
深層学習のキャリアを通じて役立つ いくつかの有用な経験則を紹介する。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
%matplotlib inline
from d2l import torch as d2l
import torch
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
%matplotlib inline
from d2l import mxnet as d2l
from mxnet import autograd, np, npx
npx.set_np()
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
%matplotlib inline
from d2l import jax as d2l
import jax
from jax import numpy as jnp
from jax import grad, vmap
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
%matplotlib inline
from d2l import tensorflow as d2l
import tensorflow as tf
.. raw:: html
.. raw:: html
勾配消失と勾配爆発
------------------
入力 :math:`\mathbf{x}` と出力 :math:`\mathbf{o}` を持つ、 :math:`L`
層の深いネットワークを考える。 各層 :math:`l` は重み
:math:`\mathbf{W}^{(l)}` によってパラメータ化された変換 :math:`f_l`
で定義され、その隠れ層出力を :math:`\mathbf{h}^{(l)}`
とする(\ :math:`\mathbf{h}^{(0)} = \mathbf{x}` とする)。
このとき、ネットワークは次のように表せる。
.. math:: \mathbf{h}^{(l)} = f_l (\mathbf{h}^{(l-1)}) \textrm{ and thus } \mathbf{o} = f_L \circ \cdots \circ f_1(\mathbf{x}).
すべての隠れ層出力と入力がベクトルであるとき、 :math:`\mathbf{o}`
の任意のパラメータ集合 :math:`\mathbf{W}^{(l)}`
に関する勾配は次のように書ける。
.. math:: \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}}{=}}.
言い換えると、この勾配は :math:`L-l` 個の行列
:math:`\mathbf{M}^{(L)} \cdots \mathbf{M}^{(l+1)}` と勾配ベクトル
:math:`\mathbf{v}^{(l)}` の積である。 したがって、これは
あまりにも多くの確率を掛け合わせたときにしばしば現れる
数値アンダーフローの問題と同じような影響を受ける。
確率を扱うときの一般的な工夫は、 対数空間に移ること、すなわち
数値表現の仮数部から指数部へと 負荷を移すことである。
残念ながら、上の問題はそれより深刻である。 初期状態では、行列
:math:`\mathbf{M}^{(l)}` はさまざまな固有値を持ちえる。
それらは小さいことも大きいこともあり、 その積は *非常に大きく* も
*非常に小さく* もなりえる。
不安定な勾配がもたらす危険は、 数値表現の問題にとどまりない。
予測不能な大きさの勾配は、 最適化アルゴリズムの安定性も脅かする。
パラメータ更新が (i) 過度に大きくなってモデルを壊してしまう
(\ *勾配爆発* 問題)か、 あるいは (ii) 過度に小さくなって
(\ *勾配消失* 問題)、 更新ごとにパラメータがほとんど動かず
学習が不可能になるかもしれない。
勾配消失
~~~~~~~~
勾配消失問題のよくある原因の一つは、
各層の線形演算の後に付加される活性化関数 :math:`\sigma` の選択である。
歴史的には、シグモイド関数
:math:`1/(1 + \exp(-x))`\ (:numref:`sec_mlp` で導入)
は、しきい値関数に似ているため人気があった。
初期の人工ニューラルネットワークは
生物学的ニューラルネットワークに着想を得ていたため、
生体ニューロンのように *完全に* 発火するか *まったく* 発火しないかの
どちらかであるニューロンの考え方は魅力的に見えた。
シグモイドを詳しく見て、
なぜ勾配消失を引き起こしうるのかを確認ししよう。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. figure:: output_numerical-stability-and-init_d5976f_18_0.svg
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
[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.
.. figure:: output_numerical-stability-and-init_d5976f_21_1.svg
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. figure:: output_numerical-stability-and-init_d5976f_24_0.svg
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. figure:: output_numerical-stability-and-init_d5976f_27_0.svg
.. raw:: html
.. raw:: html
ご覧のとおり、シグモイドの勾配は、入力が大きいときも小さいときも消失する**)。
さらに、多くの層を逆伝播するとき、 多くのシグモイドへの入力がゼロに近い
ちょうどよい範囲にない限り、
全体の積の勾配は消失してしまう可能性がある。
ネットワークが多層になるほど、 注意しないと、どこかの層で勾配が
途切れてしまうだろう。
実際、この問題はかつて深層ネットワークの学習を悩ませていた。
その結果、より安定している
(ただし生物学的にはあまりもっともらしくない) ReLU
が、実務家にとっての標準的な選択肢として 広く使われるようになった。
勾配爆発
~~~~~~~~
逆の問題である勾配爆発も、 同様に厄介である。
これを少し分かりやすく示すために、 100 個のガウス乱数行列を生成し、
ある初期行列と掛け合わせてむ。 私たちが選んだスケール (分散
:math:`\sigma^2=1` の選択)では、 行列積は爆発する。
これが深いネットワークの初期化によって起こると、
勾配降下法の最適化器を収束させる見込みはありない。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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)
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
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]])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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)
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
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]]
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
get_key = lambda: jax.random.PRNGKey(d2l.get_seed()) # Generate PRNG keys
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)
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
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]]
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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())
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
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]]
.. raw:: html
.. raw:: html
対称性を破る
~~~~~~~~~~~~
ニューラルネットワーク設計における
もう一つの問題は、パラメータ化に内在する対称性である。 1 つの隠れ層と 2
つのユニットを持つ 単純な MLP を考える。 この場合、最初の層の重み
:math:`\mathbf{W}^{(1)}`
を入れ替え、同様に出力層の重みも入れ替えることで、
同じ関数を得ることができる。 第 1 隠れユニットと第 2 隠れユニットを
区別する特別なものはありない。 言い換えると、各層の隠れユニットの間には
置換対称性がある。
これは単なる理論上の厄介ごとではありない。 先ほどの 1 隠れ層・2
ユニットの MLP を考えしよう。 説明のために、 出力層が 2
つの隠れユニットを 1 つの出力ユニットに変換するとする。
もし隠れ層のすべてのパラメータを
:math:`\mathbf{W}^{(1)} = c`\ (ある定数
:math:`c`\ )として初期化したら、 何が起こるだろうか。
この場合、順伝播では
どちらの隠れユニットも同じ入力とパラメータを受け取り、
同じ活性化を生成し、 それが出力ユニットに渡される。 逆伝播では、
出力ユニットをパラメータ :math:`\mathbf{W}^{(1)}` で微分すると、
すべての要素が同じ値を取る勾配が得られる。
したがって、勾配ベースの反復(たとえばミニバッチ確率的勾配降下法)を行っても、
:math:`\mathbf{W}^{(1)}` のすべての要素は 依然として同じ値のままである。
このような反復だけでは 自力で対称性を *破る* ことは決してできず、
ネットワークの表現力を 実現できないままになるかもしれない。
隠れ層は、あたかも 1 つのユニットしか持たないかのように振る舞うだろう。
ミニバッチ確率的勾配降下法ではこの対称性は破れないが、
(後で導入する)ドロップアウト正則化なら破ることができる点に注意されたい。
パラメータ初期化
----------------
上で述べた問題に対処する、あるいは少なくとも軽減する
一つの方法は、慎重な初期化である。 後で見るように、
最適化時にさらに注意を払い、
適切な正則化を行うことで、安定性をさらに高められる。
デフォルトの初期化
~~~~~~~~~~~~~~~~~~
前の節、たとえば :numref:`sec_linear_concise` では、
重みの値を初期化するために 正規分布を用いた。
初期化方法を指定しない場合、フレームワークは
デフォルトのランダム初期化方法を使いる。これは
中程度の問題規模では実際によく機能することが多いである。
.. _subsec_xavier:
Xavier 初期化
~~~~~~~~~~~~~
非線形性 *なし* のある全結合層について、 出力 :math:`o_{i}`
のスケール分布を見てみしよう。 この層に :math:`n_\textrm{in}` 個の入力
:math:`x_j` と、それに対応する重み :math:`w_{ij}` があるとすると、
出力は次のように与えられる。
.. math:: o_{i} = \sum_{j=1}^{n_\textrm{in}} w_{ij} x_j.
重み :math:`w_{ij}` はすべて 同じ分布から独立にサンプルされるとする。
さらに、この分布の平均が 0、分散が :math:`\sigma^2` であると仮定する。
ここで、分布がガウス分布である必要はなく、
平均と分散が存在すればよいことに注意されたい。
今のところ、この層への入力 :math:`x_j` も 平均 0、分散 :math:`\gamma^2`
を持ち、 :math:`w_{ij}`
と互いに独立で、かつ入力同士も独立であると仮定ししよう。
この場合、\ :math:`o_i` の平均は次のように計算できる。
.. math::
\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}
また分散は次のようになる。
.. math::
\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}
分散を一定に保つ一つの方法は、 :math:`n_\textrm{in} \sigma^2 = 1`
とすることである。 次に逆伝播を考える。 ここでも同様の問題に直面するが、
今度は勾配が出力に近い層から伝播してくる。
順伝播の場合と同じ考え方を使うと、 :math:`n_\textrm{out} \sigma^2 = 1`
でない限り、勾配の分散は爆発しうることが分かる。 ここで
:math:`n_\textrm{out}` はこの層の出力数である。
すると、私たちはジレンマに陥る。 この 2
つの条件を同時に満たすことはできない。
そこで、次を満たすことを目指する。
.. math::
\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 初期化*
の理論的根拠である。
この手法は、その考案者の第一著者にちなんで名付けられた
:cite:`Glorot.Bengio.2010`\ 。 通常、Xavier 初期化では 平均 0、分散
:math:`\sigma^2 = \frac{2}{n_\textrm{in} + n_\textrm{out}}`
のガウス分布から重みをサンプルする。
また、重みを一様分布からサンプルするときの 分散の選び方にも適用できる。
一様分布 :math:`U(-a, a)` の分散は :math:`\frac{a^2}{3}`
であることに注意されたい。 :math:`\frac{a^2}{3}` を :math:`\sigma^2`
に関する条件へ代入すると、 次のように初期化すればよいことが分かる。
.. math:: U\left(-\sqrt{\frac{6}{n_\textrm{in} + n_\textrm{out}}}, \sqrt{\frac{6}{n_\textrm{in} + n_\textrm{out}}}\right).
上の数学的な議論では 非線形性がないことを仮定しているが、
この仮定はニューラルネットワークでは簡単に破られる。 それでも、Xavier
初期化法は 実際にはうまく機能することが分かっている。
さらに先へ
~~~~~~~~~~
上の議論は、現代的なパラメータ初期化手法の ほんの入口にすぎない。
深層学習フレームワークには、しばしば十数種類もの
異なる経験則が実装されている。 さらに、パラメータ初期化は
深層学習における基礎研究の 非常に活発な分野であり続けている。
そこには、共有パラメータ、超解像、系列モデル、
その他の状況に特化した経験則も含まれる。 たとえば、
:cite:t:`Xiao.Bahri.Sohl-Dickstein.ea.2018`
は、慎重に設計された初期化法を用いることで、
アーキテクチャ上の工夫なしに 10,000
層のニューラルネットワークを学習できる可能性を示した。
この話題に興味があるなら、 このモジュールで扱う各手法を深く掘り下げ、
それぞれの経験則を提案・解析した論文を読み、
さらにこの分野の最新論文を追ってみることを勧める。
もしかすると、あなたは巧妙なアイデアを見つけたり、
あるいは発明したりして、
深層学習フレームワークに実装を貢献するかもしれない。
まとめ
------
勾配消失と勾配爆発は、深いネットワークでよく見られる問題である。勾配とパラメータが適切に制御された状態を保つためには、パラメータ初期化に細心の注意が必要である。
初期勾配が大きすぎず小さすぎもしないようにするために、初期化の経験則が必要である。
ランダム初期化は、最適化の前に対称性が破られることを保証するうえで重要である。
Xavier
初期化は、各層について、任意の出力の分散が入力数の影響を受けず、任意の勾配の分散が出力数の影響を受けないことを示唆する。
ReLU 活性化関数は勾配消失問題を緩和する。これにより収束を加速できる。
演習
----
1. MLP
の各層における置換対称性以外に、対称性を破る必要があるニューラルネットワークのケースを他に設計できるか?
2. 線形回帰や softmax
回帰では、すべての重みパラメータを同じ値に初期化してもよいだろうか?
3. 2
つの行列の積の固有値に関する解析的な上界を調べて。これは、勾配がよく条件付けられるようにすることについて何を示唆しているか?
4. ある項が発散すると分かっている場合、後から修正できるだろうか?
layerwise adaptive rate scaling に関する論文を参考にされたい
:cite:`You.Gitman.Ginsburg.2017`\ 。