重みの初期化の標準偏差を0.01と1.5にしただけで、50層のMLPは活性化が10^61倍潰れるか10^45倍膨張した
約15分で読めます

重みの初期化の標準偏差を0.01と1.5にしただけで、50層のMLPは活性化が10^61倍潰れるか10^45倍膨張した


深いニューラルネットワークは、少し前まで「ほぼ学習できない」ものだった。層を重ねれば重ねるほど賢くなるはずなのに、実際に10層、20層と積み上げると、なぜか全く学習が進まなくなる。この現象の主犯としてよく語られるのが重みの初期値だ。信号(活性化)と勾配は、層を1枚通過するたびに何倍かずつ拡大・縮小される。その倍率が1よりわずかでも小さければ、何十層も重ねるうちに信号は指数的にゼロへ潰れる(勾配消失)。逆にわずかでも大きければ、指数的に発散する(勾配爆発)。この「わずかな差が層を重ねるごとに掛け算で効いてくる」という話は、頭では理解していたつもりだったが、自分の手でその倍率を実測したことは一度もなかった。

そこで、自前のnumpy MLPを使って、初期化の違いが層を経るごとにどれだけの倍率で効いてくるのかを実際に測ってみた。

実験に使ったコードの全文はGitHubに置いています

実験設計: 幅64・50層のReLU MLPを3通りの初期化で

活性化関数にはReLUを使う。教科書的にはXavier/Glorot初期化はtanhやsigmoidを対象に設計されたものだが、tanhやsigmoidは出力が[1,1][-1,1]に有界なので、大きすぎる重みを使っても活性化の「絶対値」自体は爆発しない。代わりに値が±1\pm1付近に張り付く「飽和」が起き、そこでは局所的な勾配(tanh\tanh')がほぼゼロになるため、活性化は爆発していないのに勾配だけが別の理屈で消えるという、話がややこしくなる現象が起きる(これはこれでよく知られた話だが、今回見たいのは「初期化のスケールが層を経てどう掛け算で効くか」というもっとシンプルな話だ)。ReLUは出力に上限がなく、正の領域で微分が常に1なので、活性化・勾配ともに「初期化のスケールがそのまま倍率として効いてくる」様子を一番素直に見せてくれる。対応する原理的な初期化はHe初期化なので、今回はReLU + Heの組み合わせで検証する。

  • 幅64、50層の全結合MLP(バイアスは0に固定し、重み行列だけの効果を見る)
  • 入力は64次元、バッチサイズ256の固定の入力データ(全初期化方式で完全に同じものを使う)
  • 逆伝播用の損失は、固定のランダムターゲットに対するMSE(こちらも全初期化方式で共通)
  • 初期化方式は3通り
    • naive-small: 層の幅に関係なく、標準偏差0.01固定の小さすぎる初期化
    • naive-large: 層の幅に関係なく、標準偏差1.5固定の大きすぎる初期化
    • He初期化: 標準偏差を2/fan_in\sqrt{2/\text{fan\_in}}、つまり入力側の層の幅でスケールさせる原理的な初期化
def make_weights(scheme, depth, width, input_dim, seed):
    rng = np.random.default_rng(seed)
    Ws = []
    for l in range(depth):
        fan_in = input_dim if l == 0 else width
        if scheme == "naive_small":
            std = 0.01
        elif scheme == "naive_large":
            std = 1.5
        elif scheme == "he":
            std = np.sqrt(2.0 / fan_in)   # ここだけ層の幅を見ている
        Ws.append(rng.normal(0, std, size=(fan_in, width)))
    return Ws

ここで「固定の入力データ」と呼んでいるものの正体を先にはっきりさせておく。これはMNISTのような実データセットではなく、標準正規分布N(0,1)N(0,1)から生成した64次元の乱数ベクトル256本だ(逆伝播用のターゲットも同じく乱数)。今回の実験では学習は一切しない。見たいのは「初期化直後のネットワークを信号が1往復したとき、層を経るごとにスケール(標準偏差)がどう変わるか」だけなので、入力の中身に意味がある必要はなく、スケールの物差しになる「標準偏差がだいたい1の信号」でありさえすればいい(実測では層0=入力の標準偏差は0.996で、これがすべてのグラフの出発点になる)。そして今回の「成功」の定義もシンプルで、50層通過しても活性化と勾配の標準偏差が桁単位では動かず、入力と同じくらいのオーダーに留まり続けること。逆に、桁が層とともに指数的に動いてしまったら、その初期化は深いネットワークでは使えない、という判定になる。

測り方はこうだ。同じ固定入力バッチで1回だけ順伝播し、各層の活性化の標準偏差を記録する。続けて同じ順伝播結果から1回だけ逆伝播し、各層の勾配(損失を各層の活性化で微分したもの)の標準偏差を記録する。

結果1: 活性化の大きさは、50層でこれだけ変わった

初期化方式ごとの活性化の標準偏差を、層の深さ(横軸、0が入力、50が最終層)に対してlogスケールの縦軸でプロットした折れ線グラフ。3本とも層0(入力)では標準偏差1付近の同じ点から始まる。青のnaive-small(std=0.01)は右肩下がりの直線で10^0付近から10^-63付近まで一直線に潰れていく。赤のnaive-large(std=1.5)は右肩上がりの直線で10^0付近から10^46付近まで一直線に膨張していく。緑のHe初期化はほぼ水平で、50層を通じて10^0のすぐ近くに留まり続けている

数字で見るとこうなる。

                  layer 1        layer 50
naive-small(0.01)  0.0463    →   2.67e-63   (61桁潰れる, 1/1.73e61倍)
naive-large(1.5)   7.09      →   2.00e+46   (45桁膨張する, 2.83e45倍)
He                 0.820     →   0.260      (層1〜50の全体では [0.235, 1.019] の範囲に収まる)

naive-smallは、層1では活性化の標準偏差が0.0463とまだ「それなりの値」に見えるのに、層50では2.67×10⁻⁶³。61桁潰れている。 これはもう数値的にはほぼ完全にゼロで、実際の学習ではこの層より先の情報はネットワークにとって「何もない」のと同じになる。

naive-largeは逆側で、層1の7.09から層50では2.00×10⁴⁶まで、45桁膨張する。 float64がまだ扱える範囲(最大約1.8×10³⁰⁸)には収まっているが、もはや意味のある数値ではない。

He初期化だけは、層1の0.820から層50の0.260まで、全50層を通じて最小0.235・最大1.019の範囲に収まっていた。完全に一定というわけではなく(ReLUは半分のユニットをゼロにするので、層ごとに多少の揺らぎは残る)、層30あたりで一度0.396まで下がる場面もあったが、それでも「61桁潰れる」「45桁膨張する」に比べれば、誤差の範囲と言っていいレベルの揺らぎだ。

結果2: 勾配はもっと極端に効いてくる

活性化は順伝播で1方向に掛け算されていくだけだが、勾配は逆伝播でもう一段掛け算が乗る。その結果を見てみる。

初期化方式ごとの勾配(損失を各層の活性化で微分した値)の標準偏差を、層の深さ(横軸、左端の0が入力側、右端の50が出力/損失側)に対してlogスケールの縦軸でプロットした折れ線グラフ。青のnaive-smallは右端の出力側では10^-3付近にあるが、逆伝播が進む左(入力側)ほど潰れていき、左端では10^-65付近に達する(グラフ上は左から右への右肩上がりの直線に見える)。赤のnaive-largeは右端の出力側ですでに10^43付近と大きく、左(入力側)に向かってさらに膨張して左端では10^91付近に達する(グラフ上は左端が最も高い右肩下がりの直線)。緑のHe初期化は両端でほぼ同じ高さの水平線を保っている

                  layer 50(出力側)   layer 0(入力側)
naive-small(0.01)   3.90e-03      →   1.40e-65   (さらに62桁潰れる)
naive-large(1.5)    7.83e+43      →   1.34e+91   (さらに47桁膨張する)
He                  4.01e-03      →   3.64e-03   (全体では [0.00315, 0.00674] の範囲)

naive-smallは、出力に近い層50では勾配の標準偏差が0.0039とまだ普通の値なのに、そこから入力に近い層0に向かって逆伝播するにつれ、さらに62桁潰れて1.40×10⁻⁶⁵になる。つまり入力に近い層のパラメータは、実質的に勾配を一切受け取らない。 学習を回しても、そこから先の重みはほぼ動かない。

naive-largeは、逆伝播を始めた瞬間の出力側の時点で、すでに7.83×10⁴³という現実離れした大きさになっている。1層も逆伝播していないのになぜ、と思うかもしれないが、種明かしは損失関数にある。MSE損失の微分は(出力 − ターゲット)/バッチサイズで、naive-largeの出力活性化はすでに2.00×10⁴⁶まで膨張しているから、2.00×10⁴⁶ ÷ 256 ≈ 7.83×10⁴³。つまり順伝播の爆発が、損失の微分を通じてそのまま逆伝播の「初期値」に持ち込まれている。逆に、naive-smallの出力側の勾配3.90×10⁻³が普通の値なのは、出力がほぼゼロに潰れているせいで(出力 − ターゲット)がほぼターゲットそのものになり、std(ターゲット)/256 ≈ 0.0039という無傷の値から逆伝播が始まるからだ。naive-largeはこの巨大な初期値から、さらに入力側に向かって47桁膨張し、1.34×10⁹¹に達する。

He初期化は、出力側の0.00401から入力側の0.00364まで、ほぼ変わらない。全層を通じた範囲も[0.00315, 0.00674]と2倍強に収まっており、活性化よりもさらに安定していた。

naive-largeは、深さ165層で本当にoverflowした

50層時点でのnaive-largeの2.00×10⁴⁶は「巨大だが有限」な数値だった。じゃあ本当にfloat64がoverflowするところまで見てみようと、同じ構成(幅64、標準偏差1.5固定)のまま層数を400まで伸ばして、どこで壊れるか確認してみた。

naive_large を depth=400 まで伸ばして順伝播 → 層165でoverflow(inf)に到達

層165で、実際にfloat64の表現範囲を超えてinfになった。 ここまでは「桁が大きくなっていくグラフ」として眺めていたが、単なる理論上の懸念ではなく、素朴な初期化のまま深くし続けると本当に計算が壊れるところまで、手元で確認できた。

結果3: 深さを増やしながら、崩れていく分布そのものを見る

標準偏差という要約統計だけでなく、各深さでの活性化の値そのもの(ヒストグラム)がどう変わっていくかもアニメーションにしてみた。

3つの初期化方式(上からnaive-small、naive-large、He)それぞれのヒストグラムを縦に並べ、層の深さが1から50まで増えていく様子を示すアニメーション。naive-smallのヒストグラムはx軸の目盛りそのものが徐々に0に近づいていき、最終的にx軸右下の指数表示が1e-62のスケールになる。naive-largeのヒストグラムはx軸の目盛りが徐々に大きくなっていき、最終的にx軸の指数表示が1e47のスケールになる。一番下のHeのヒストグラムだけはx軸の目盛りが常に0〜数程度のままほとんど変化しない。各パネルのタイトルにはその深さでの標準偏差の実測値も表示される

このアニメーションで面白いのは、ヒストグラムの「形」そのものよりも、x軸の目盛りの数字が動いていくことだ。naive-smallは深さが増すごとにx軸の表示が1e-2、1e-10、…と際限なくゼロに近づいていき、naive-largeは逆にx軸の表示がどんどん大きな指数になっていく。一番下のHeだけは、深さが50まで進んでもx軸の目盛りがほとんど動かない。分布の形自体はReLUらしく0付近に偏った形を保ったまま、スケールだけが3者で全く違う運命をたどる。

意外だったこと

正直、He初期化がここまできれいに安定するとは思っていたものの、一番「なるほど」と思ったのは勾配側の数字の並びだった。層を1枚通るごとの崩れのペースそのものは、順伝播(61桁・45桁)と逆伝播(62桁・47桁)でほぼ同じだ。逆伝播も結局は同じ重み行列を(転置して)掛け続けているのだから、これは理屈通りとも言える。それなのに、最終的に到達する値は勾配の方がはるかに極端になり得る。naive-largeでは活性化の最大が2.00×10⁴⁶なのに対し、入力側の勾配は1.34×10⁹¹——45桁も余計に極端だ。カラクリは結果2で見た通りで、逆伝播は「無傷の値」からスタートできない。順伝播で爆発しきった出力(2.00×10⁴⁶)が損失の微分を通じてそのまま逆伝播の初期値(7.83×10⁴³)になり、その上にさらに50層分の膨張(47桁)が乗る。43桁+47桁で約90桁、という足し算になっている。一方でnaive-smallの側では、出力がゼロに潰れたおかげで逆伝播の初期値は無傷(0.0039)のままで、入力側の勾配(10⁻⁶⁵)は活性化(10⁻⁶³)と大差ない。二重の罰を受けるのは爆発側だけという非対称は、実測して初めて実感した構図だった。順伝播の崩れと逆伝播の崩れは独立に起こるのではなく、1回目の崩れの結果が、そのまま2回目の崩れの出発点になる。1回の崩れが、次の崩れの土台になる。

初期化という「最初のスケール」が持つ意味

50層のネットワークで、初期化の標準偏差をたった0.01か1.5かHe(層の幅で決まる値)かで選んだだけで、同じ入力・同じ構造のネットワークが、片方は信号がほぼゼロに消え、もう片方は現実離れした桁まで膨れ上がった。途中の層は何も間違ったことをしていない。最初の重みのスケールという、たった1つの選択が、単純に掛け算で50回積み重なっただけだ。

これは、プロジェクトや習慣、人間関係の初期の枠組みにもそのまま当てはまる話だと思う。何かを始めるとき、最初の一歩の「規模感」や「前提の置き方」は、多くの場合そこまで重要視されない。少し大きすぎても小さすぎても、あとで軌道修正すればいいと考えがちだ。でも実際には、その最初のスケールのわずかなズレは、毎回の積み重ねのたびに掛け算で効いてくる。小さすぎる前提で始めたものは、何十回とステップを重ねるうちに存在感がゼロに近づいていき、気づいた頃にはもう手を入れる余地すら残っていない。逆に大きすぎる前提で始めたものは、同じように何十回と積み重なるうちに、当初の意図から遠くかけ離れた、収拾のつかない規模まで膨れ上がってしまう。途中のどこか1回のステップで頑張って軌道修正しても、その1回の補正は、他の49回分の「最初のスケールのまま掛け算され続けた効果」には到底追いつかない。だからこそ、途中で微調整する努力よりも、最初の一歩のスケールをその場その場の幅に合わせて決めておくことの方が、割に合わない地味な作業に見えて、実は一番効くのだと思う。

まとめ

  • 幅64・50層のReLU MLP(自前numpy実装)で、初期化の標準偏差を0.01(naive-small)・1.5(naive-large)・He初期化(2/fan_in\sqrt{2/\text{fan\_in}})の3通りに変えて、固定の入力バッチで活性化と勾配の標準偏差を実測した
  • naive-smallは活性化が層1の0.0463から層50で2.67×10⁻⁶³まで61桁潰れ、勾配は入力側に向かってさらに62桁潰れて1.40×10⁻⁶⁵になった
  • naive-largeは活性化が層1の7.09から層50で2.00×10⁴⁶まで45桁膨張し、勾配はさらに入力側で47桁膨張して1.34×10⁹¹になった。同じ構成のまま深さを400まで伸ばすと、層165で実際にfloat64のoverflow(inf)に到達した
  • He初期化は活性化が[0.235, 1.019]、勾配が[0.00315, 0.00674]と、50層を通じてどちらも数倍の範囲に収まり続けた
  • 層ごとの崩れのペース自体は順伝播と逆伝播でほぼ同じ(61桁vs62桁、45桁vs47桁)だったが、naive-largeでは順伝播で爆発した出力がMSE損失の微分を通じてそのまま逆伝播の初期値(2.00×10⁴⁶ ÷ 256 ≈ 7.83×10⁴³)に持ち込まれ、入力側の勾配は活性化(10⁴⁶)よりはるかに極端な10⁹¹に達した。潰れる側のnaive-smallでは出力がゼロに潰れるぶん逆伝播が無傷の値から始まるため、この「二重の罰」は爆発側だけに効く非対称だった