誤差逆伝播を1から解説 — 連鎖律だけで全部わかる
1000万個のパラメータの勾配を、なぜ順伝播1回ぶんの手間で全部求められるのか。連鎖律の復習から計算グラフ、2層ネットの手計算1ステップ、そして勾配消失が起きる理由までを、記号を1つずつ説明しながら追います。
要点
- 数値微分はパラメータ1個につき順伝播が1回必要で、1000万個なら1ステップに1000万回の順伝播が要る。
- 誤差逆伝播は順伝播1回ぶんと同じオーダーの手間で、連鎖律により全パラメータの勾配を一度に出す。
- 各頂点は「上流から降ってきた勾配 × 自分の局所微分」を下流へ渡し、そのために順伝播の中間値を保持する必要がある。
- 逆向きが効率的なのは損失がスカラーで、各頂点の感度1個をそのパラメータ全体で使い回せるからである。
- 連鎖律は掛け算なので、項が1未満なら勾配消失、1超なら勾配爆発となり、シグモイド微分は最大0.25で10層なら0.25^10になる。
スープが塩辛い。犯人はどの調味料か
作ったスープが塩辛すぎたとします。原因は塩か、醤油か、煮詰めすぎか。あなたが本当に知りたいのは「どれをどれだけ減らせば、しょっぱさがどれだけ和らぐか」です。
素朴なやり方は、塩だけをほんの少し減らしてもう一度作り、味を比べること。次は醤油だけを変えてもう一度。調味料が20種類なら20回作り直します。これを数式にしたのが数値微分です。
記号を1つずつ説明します。 は損失(モデルの間違い具合を表す1つの数値)、 は全パラメータをまとめたもの、 はそのうち 番目の1個、(イプシロン)はごく小さい数、 は「 番目だけ1で他は全部0」というベクトルです。(ラウンド・ディー)は、他を固定して1つの変数だけで微分する記号です。
この式が言っているのは要するに、つまみを1個だけほんの少し回して、損失がどれだけ変わったかを測るということ。正しい方法ですが、致命的に遅い。パラメータ1個につき順伝播(入力を通して出力を出す計算)が1回必要なので、1000万個なら1回の学習ステップに1000万回の順伝播が要ります。これでは終わりません。
誤差逆伝播(backpropagation)は、レシピの手順を逆向きに辿ることで、順伝播1回ぶんと同じオーダーの手間で全パラメータの勾配を一度に出す方法です。魔法ではなく、高校で習う連鎖律をひたすら適用しているだけです。
なお本記事はニューラルネットワークの基礎——ニューロンが を計算していること、順伝播が行列積の連鎖であること——を前提にします。
連鎖律 — 微分は「倍率」、連鎖律は「倍率の掛け算」
連鎖律は次の1行です。
が変われば が変わり、 が変われば が変わる、という3段の関係を考えています。
この式が言っているのは要するに、倍率は掛け算で繋がるということです。 を1だけ動かすと が3動く(倍率3)、 が1動くと が2動く(倍率2)。なら を1動かしたとき は6動く。歯車を噛み合わせたときの回転比と同じ理屈です。
微分とは、この「局所的な倍率」のことです。ある点でほんの少し入力をずらしたら出力が何倍ずれるか。それだけの意味しかありません。そして深いネットワークは、この歯車が何十段も噛み合った装置にすぎません。
計算グラフ — 順方向で値、逆方向で勾配
ネットワークを、演算を頂点・値の流れを辺とする計算グラフとして見ます。 の掛け算が1つの頂点、 の足し算が次の頂点、ReLU がその次、という具合です。
このグラフの上で、逆伝播は驚くほど機械的な作業になります。各頂点が知っている必要があるのは自分の局所微分——自分の入力を少し動かしたら自分の出力がどれだけ動くか——だけです。あとは、
上流から降ってきた勾配 × 自分の局所微分 = 下流へ渡す勾配
を計算して隣に渡す。これを出力側から入力側へ繰り返すだけで、巨大な式を展開する必要はありません。頂点が自分の足元しか知らなくても全体の勾配が正しく求まる——ここが逆伝播の美しいところです。
ただし条件が1つあります。局所微分を計算するには順伝播のときの値が必要なので、順伝播で通った中間の値を捨てずに覚えておく必要があります。学習時にメモリを大量に食う理由はここにあります。
そして「自分の局所微分」の代表格が、活性化関数の傾きです。次の図で傾きの大きさを確かめておいてください。ここで見た傾きの値が、そのまま逆伝播で掛け算される数字になります。
2層ネットを、実際の数値で1ステップ追う
言葉だけでは腹に落ちないので、最小のネットワークで手を動かします。入力1個、隠れ層1ユニット(ReLU)、出力1個。パラメータは4つだけです。
初期値は 、、、、、正解は 。損失は二乗誤差 を使います( は微分したとき係数2が消えて式がきれいになるための慣習です)。
順伝播(値を1つずつ記録しながら進みます)
ここで は活性化関数を通す前の値、 は通した後の値です。この区別が逆伝播では重要になります。
逆伝播(出力側から遡ります)
- — 二乗誤差を微分するとこの形になります
- 出力層に活性化関数がないので、そのまま 、
- — 隠れ層へ責任を渡す。渡す量は重みに比例します
- — なので傾きは1
- 、
重みの勾配が「上流から来た勾配 × その重みに入ってきた値」になっている点に注目してください。 には が、 には が掛かっています。よく使われる入力ほど、その重みの責任は大きい——直感どおりです。
更新
は任意のパラメータ、(イータ)は学習率、 は代入です。この式が言っているのは要するに、損失が増える向きの逆へ、勾配の大きさに比例して動かすということ。 で更新すると、、、、 になります。
もう一度順伝播すると 、、、損失は 。 から1ステップで大きく下がりました。ただし予測は (正解より上)から (正解より下)へ飛び越えています。誤差の符号が反転した——これが学習率を上げすぎたときに起こることの芽です。
一般の層に拡張する
同じ手続きを行列で書くと、任意の層数に一般化できます。
記号の説明です。(デルタ)は 層目の事前活性化 に対する損失の感度——さきほど手計算した の一般形です。 は次の層の重み行列の転置(行と列を入れ替えたもの)。 は要素ごとの積で、行列積ではなく同じ位置の成分同士を掛けます。 は1つ手前の層の出力です。
1本目の式が言っているのは要するに、次の層の責任を重み行列で振り分け直し、自分の活性化関数の傾きを掛ける。2本目は、重みの勾配は「その層の責任 × その重みに入ってきた値」。手計算でやったことと、字面が違うだけで中身は同じです。
なぜ「逆」向きだと効率的なのか
鍵は、損失がスカラー(数値1個)であることです。
出力から遡ると、各頂点には「この値が動いたら損失がどれだけ動くか」という感度がただ1つ定まります。そして一度求めたその1個の数値を、その頂点にぶら下がるすべてのパラメータで使い回せます。さきほどの手計算でも、 を1回求めたあと、 の勾配にも の勾配にも同じ値を使いました。層が深くなるほど、この共有が効いてきます。
逆に前向きに求めようとすると、パラメータごとに別々の追跡が必要になり、パラメータの数だけパスが要ります。数値微分が遅かったのと同じ理由です。
出力が1個で入力(パラメータ)が大量にあるときは、出力から遡るほうが圧倒的に安い。 逆伝播が普遍的に使われているのは、機械学習の損失が常にスカラー1個だからです。
勾配消失は、掛け算の連鎖から自動的に生まれる
連鎖律が掛け算である以上、避けられない副作用があります。各層1ユニットの鎖で書くと本質が見えます。
(パイ)は「並んだ項を全部掛ける」記号、 は層の総数です。この式が言っているのは要するに、入り口の層に届く勾配は、途中の「重み × 活性化関数の傾き」を全部掛け合わせたものだということ。
掛け算なので、挙動は極端です。各項が1より小さければ結果は指数的に0へ潰れ(勾配消失)、1より大きければ指数的に膨らみます(勾配爆発)。前回見たとおりシグモイドの微分は最大でも0.25なので、10層あれば 、およそ100万分の1。入り口付近の層はほとんど学習しません。
対策はすべて「掛け算の鎖を守る」という一点に向いています。ReLU は正側の傾きを1にして減衰をなくし、残差接続は勾配に「1」で通り抜ける抜け道を作り、正規化層は各層の入力スケールを揃えて項が極端になるのを防ぎます。爆発側には勾配クリッピング(大きさに上限を設ける)が使われます。
numpyで書く逆伝播
さきほど手で追った計算が、そのままコードになります。
def backward(x, y, w1, b1, w2, b2):
z1 = w1 * x + b1
a1 = np.maximum(0, z1) # ReLU
y_hat = w2 * a1 + b2 # 順伝播: 中間値を保存しておく
d_yhat = y_hat - y # dL/dŷ
d_w2, d_b2 = d_yhat * a1, d_yhat
d_a1 = d_yhat * w2 # 重みで責任を振り分け直す
d_z1 = d_a1 * (z1 > 0) # 活性化の傾きを掛ける(ReLUの微分)
d_w1, d_b1 = d_z1 * x, d_z1
return d_w1, d_b1, d_w2, d_b2
(z1 > 0) が ReLU の微分です。真偽値が1と0として扱われるので、正なら傾き1、負なら0が掛かります。逆伝播が順伝播の裏返しであること、順伝播の値(a1、z1)を使い回していることが一目で分かります。
実務で効く3つのポイント
1. 自前実装なら必ず勾配チェックをする 解析的に求めた勾配と数値微分した勾配を突き合わせます。倍精度ならごく小さい相対誤差に収まるはずで、桁が合わなければバグがあります。逆伝播のバグは「学習が少し遅い」という形で現れて気づきにくいので、実装直後に潰すのが得です。
2. 学習時のメモリの正体は中間値 逆伝播には順伝播の中間値が要るので、バッチサイズを2倍にすれば保存すべき活性化も概ね2倍。「推論は動くのに学習でメモリが足りない」の主因はこれです。gradient checkpointing は中間値を捨てて必要時に再計算する手法で、メモリを計算時間と交換します。
3. NaN が出たら、勾配ではなく順伝播を先に疑う 勾配が NaN になったとき、原因は逆伝播そのものより順伝播側にあることが多い。、0除算、学習率が大きすぎて値が発散した、などです。勾配クリッピングは症状を抑えますが原因は消しません。
まとめ
- 逆伝播は連鎖律の機械的な適用。各頂点は自分の局所微分だけを知っていればよい
- 「上流の勾配 × 局所微分」を出力から入力へ配るだけで、全パラメータの勾配が1パスで揃う
- 逆向きが速いのは、損失がスカラー1個で、遡ると部分計算を共有できるから
- 掛け算の連鎖なので、項が1未満なら勾配消失、1超なら勾配爆発。ReLU・残差接続・正規化はこの鎖を守るための道具
PyTorch や JAX の autograd は、ここまでの手続きを計算グラフから自動生成しているだけです。detach() や no_grad() は「ここで鎖を切る」という宣言だと理解すれば挙動に迷いません。次は、この土台の上に立つAttention機構へ進んでください。
コメント
コメントにはログインが必要です