- AI ⚙️
誤差逆伝播法とは|連鎖律の手計算・numpyで自作・自動微分まで
結論
誤差逆伝播法(バックプロパゲーション)とは、出力の誤差から入力側へ向かって、連鎖律(合成関数の微分の掛け算)で全部の重みの勾配を一度に求める方法です。求めた勾配を勾配降下法に渡して重みを直すことで、ニューラルネットワークが学習します。
この記事でわかること
- 2層のネットワークを数値つきで手計算し、数値微分と小数第10位まで一致させた手順
- numpy で自作した逆伝播と自作の自動微分が、数値微分・MLPClassifier と誤差 1e-10 以下で一致した結果
- 勾配消失・勾配爆発を層ごとの勾配で測った表と、転置・ブロードキャスト・平均の取り忘れを再現した実装ミス集
この記事で実際に動かしたもの:手計算と同じ2層ネットワーク(numpy)、手書き数字を学習する 64-32-10 のネットワーク(numpy で自作)、41行の自作の自動微分(スカラーの計算グラフ)、20層のネットワークでの勾配消失・爆発の測定、scikit-learn の MLPClassifier。
誤差逆伝播法は、ニューラルネットワークの「どの重みを、どちらへ、どれだけ直せば誤差が減るか」を計算する手順です。下の図の実線が順伝播(予測を出す計算)、点線が逆伝播(誤差の責任を入力側へ配る計算)です。
flowchart TD
X["入力 x(1.0, 0.5)"] --> Z["z = W1·x + b1"]
Z --> H["h = σ(z)"]
H --> Y["y = W2·h + b2"]
Y --> L["損失 L = ½(y − t)²"]
L -. "∂L/∂y = y − t" .-> Y
Y -. "× W2" .-> H
H -. "× h(1 − h)" .-> Z
Z -. "× x で ∂L/∂W1" .-> X
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| 誤差逆伝播法とは | 出力の誤差から逆向きに、全部の重みの勾配を連鎖律で求める方法 | 誤差逆伝播法とは |
| バックプロパゲーションとの違い | 同じもの。英語の Backpropagation の訳が誤差逆伝播法 | 誤差逆伝播法とは |
| 連鎖律とは | 合成関数の微分は、各段の微分の掛け算になるという規則 | 連鎖律とは |
| わかりやすく言うと | 足し算は勾配をそのまま流し、掛け算は相手の値を掛けて流す | 最初の5分 |
| 手計算の例 | 2層・重み6個+バイアス3個で、全部の勾配を数値で求めた | 2層ネットワークで手計算 |
| 勾配降下法との違い | 逆伝播は勾配を「求める」方法、勾配降下法はそれで重みを「直す」方法 | 誤差逆伝播法とは |
| なぜ逆向きなのか | 出力が1つなら、逆向きは1回で全部の勾配が出る。数値微分の約2,500倍速かった | なぜ「逆」向きなのか |
| 勾配消失とは | 入力に近い層ほど勾配が小さくなり学習が進まない現象。20層で 1兆分の1以下になった | 勾配消失・勾配爆発 |
| 正しく実装できたか確かめたい | 数値微分と比べる勾配チェック。相対誤差 1e-7 未満が目安 | 数値微分で答え合わせ |
確認環境:Ubuntu 24.04.5 LTS(CPU 2コア)の上で、Python 3.13.18、NumPy 2.5.3、scikit-learn 1.9.1 を使い、numpy の計算は OPENBLAS_NUM_THREADS=1(1スレッド)で実行しました。データは scikit-learn 同梱の手書き数字(load_digits、1,797枚・64画素)と、make_moons で作った2次元の80点、それに手計算用の自作の数値です。逆伝播・数値微分・自動微分はすべて numpy と標準の Python で自作し、scikit-learn の MLPClassifier と1回の更新の結果を突き合わせました。時間は同じマシンで他の処理と並行して測った値なので、桁の比較に使ってください。MLPClassifier の損失と学習の仕組みは scikit-learn 公式ユーザーガイド(1.17. Neural network models (supervised))、計算グラフの各ノードの勾配の流れ方は Stanford CS231n の講義ノートで確かめました。図は mermaid 11 で描画を確かめています(確認日 2026年11月5日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「誤差逆伝播法とは|連鎖律の手計算・numpyで自作・自動微分まで」
誤差逆伝播法とは(バックプロパゲーションの意味)
この節では、誤差逆伝播法が学習のどの部分を受け持つのかと、言葉のふだんの意味と機械学習での意味の違いが分かります。
誤差逆伝播法(ごさぎゃくでんぱほう、Backpropagation・バックプロパゲーション)は、損失(予測と正解のずれを1つの数にしたもの)を、ネットワークの全部の重みでそれぞれ微分した値=勾配を、まとめて効率よく求める方法です。略して「逆伝播」「BP」とも呼ばれます。誤差逆伝搬法と書かれることもありますが、同じものです。
| 言葉 | ふだんの意味 | ニューラルネットワークでの意味 |
|---|---|---|
| 誤差 | 測った値と本当の値のずれ | 予測と正解のずれ。学習では損失関数(二乗誤差・交差エントロピー)の値として扱う |
| 伝播 | 波や噂が次々に伝わり広がること | 計算の結果を層から層へ順に渡すこと |
| 逆 | 向きが反対であること | 予測を出すときと反対に、出力層から入力層へ向かって計算すること |
| 勾配 | 坂の傾き | 損失を各重みで偏微分した値。重みを少し増やすと損失がどれだけ増えるか |
| 連鎖律 | (ふだんは使わない言葉) | 合成関数の微分は各段の微分の掛け算になる、という微分の規則 |
ニューラルネットワークの学習は、次の4つを繰り返します。誤差逆伝播法はこのうち3番目だけを受け持ちます。
1順伝播
入力から層を順にたどって予測を出す。途中の値(z・h)を逆伝播のために覚えておく。
2損失を計算
予測と正解のずれを1つの数にする。回帰なら二乗誤差、分類なら交差エントロピー。
3逆伝播
出力側から入力側へ、連鎖律で全部の重みの勾配を求める。ここが誤差逆伝播法。
4重みを更新
重み ← 重み − 学習率 × 勾配。これは勾配降下法の仕事。
「逆伝播」と「勾配降下法」は別のものです。逆伝播は勾配を求める計算、勾配降下法(SGD・Adam など)はその勾配で重みを動かす方法です。学習率や Adam の仕組みは、第47弾で用意している記事「勾配降下法とは|学習率・SGD・Adamを自作して収束を比較」で掘り下げます。
歴史:1986年の論文で広まった
多層のネットワークの学習方法として広く知られるきっかけは、1986年に Nature に載った Rumelhart・Hinton・Williams の論文「Learning representations by back-propagating errors」です。考え方そのもの(計算の流れを逆にたどって微分を求める方法)は、それ以前の Linnainmaa(1970年)の自動微分の研究や Werbos(1974年)の博士論文にも現れています。今の PyTorch・TensorFlow・JAX などが持つ「自動微分」は、この逆向きの計算を一般の計算グラフに広げたものです。
「ニューラルネットワークとは」との役割の分け方
ニューロンの計算・層・活性化関数・学習の4手順の全体像と、XOR や手書き数字を numpy で学習させる入口は、関連記事「ニューラルネットワークとは|仕組み・学習・AttentionをNumPyで実演」で扱っています。この記事は、その中の「逆伝播」の1行がなぜその形になるのかを、手計算・連鎖律・自動微分まで分解して確かめる続編の位置づけです。
最初の5分:計算グラフの足し算と掛け算で逆伝播する
この節では、いちばん小さな計算グラフで、逆伝播の2つの規則(足し算と掛け算)を手で動かせます。
f = (a + b) × c を、a = 2・b = 3・c = 4 で計算します。計算グラフとは、計算を「ノード(演算)」と「矢印(値の流れ)」で描いた図です。
flowchart TD
A["a = 2"] --> S["s = a + b = 5"]
B["b = 3"] --> S
S --> F["f = s × c = 20"]
C["c = 4"] --> F
F -. "∂f/∂s = c = 4" .-> S
F -. "∂f/∂c = s = 5" .-> C
S -. "そのまま 4" .-> A
S -. "そのまま 4" .-> B
順伝播で値を求める
s = a + b = 5、f = s × c = 20。途中の s = 5 を覚えておく。
出口の勾配を 1 にする
f を f で微分すると 1。ここから逆向きに配っていく。
掛け算ノードは「相手の値」を掛ける
f = s × c なので、s 側へは c = 4、c 側へは s = 5 を、上流の勾配 1 に掛けて渡す。
足し算ノードは「そのまま」流す
s = a + b を a で微分しても b で微分しても 1。上流の 4 をそのまま a と b へ渡す。
a, b, c = 2.0, 3.0, 4.0
s = a + b # 順伝播
f = s * c
df = 1.0 # 逆伝播:出口の勾配は 1
ds = df * c # 掛け算:相手の値を掛ける
dc = df * s
da = ds * 1.0 # 足し算:そのまま流す
db = ds * 1.0
print("f =", f, " da =", da, " db =", db, " dc =", dc)
f = 20.0 da = 4.0 db = 4.0 dc = 5.0
答え合わせをします。a を 2 から 2.001 にすると f は (5.001) × 4 = 20.004 になり、0.001 の変化で 0.004 増えるので、傾きは 4 です。逆伝播の結果「∂f/∂a = 4」は「a を少し増やすと f はその4倍増える」という意味です。ニューラルネットワークの逆伝播も、この2つの規則に「活性化関数の微分を掛ける」が加わるだけです。
連鎖律とは(合成関数の微分を掛け算のリレーで求める)
この節では、連鎖律の意味と、同じ変数を2回使ったときに勾配を足す理由が分かります。
連鎖律(chain rule)は、「y が u の関数で、u が x の関数なら、dy/dx = dy/du × du/dx」という規則です。途中に何段あっても、各段の微分(局所的な傾き)を掛けていけば、入口から出口までの傾きが出ます。たとえば y = (3x + 1)² なら、u = 3x + 1 とおいて dy/du = 2u、du/dx = 3 なので、dy/dx = 2(3x + 1) × 3 です。x = 1 なら 24 になります。
逆伝播は、この掛け算を出口側から順に行い、途中の結果(上流から届いた勾配)を使い回します。だから層が何十あっても、1回の逆向きの計算で全部の勾配が出ます。
同じ変数を2回使うと勾配は「足す」
g = x × y + y(x = 3・y = 2)では、y が掛け算と足し算の2か所に入っています。y への勾配は、掛け算の経路から x = 3、足し算の経路から 1 が届き、合計の 4 が答えです(式で微分しても ∂g/∂y = x + 1 = 4)。あとで作る自動微分でも、この「経路ごとに足す」を += で書かないと答えが変わります。
計算グラフのノードは、勾配の流し方で次のように分けられます。
足し算(+)
上流の勾配を、全部の入力へそのまま配る。局所的な傾きは常に 1。
バイアスの勾配はこれ
掛け算(×)
入力の片方へは、もう片方の値を掛けて渡す。入れ替えて掛けるのが特徴。
重みの勾配は「入力 × 上流」
活性化関数
シグモイドなら h(1 − h)、tanh なら 1 − h²、ReLU なら 0 か 1 を掛ける。
勾配消失の原因になる所
max(最大値)
順伝播で最大だった入力にだけ勾配を渡し、ほかは 0。ReLU とプーリングはこの形。
選ばれた所だけが学習する
行列で書くときは、掛け算ノードの「相手の値を掛ける」が「転置した行列を掛ける」になります。順伝播が z = x @ W なら、逆伝播は dW = x.T @ dz、dx = dz @ W.T です。形(shape)が合う並べ方は1通りしかないので、迷ったら形で確かめられます。
2層ニューラルネットワークで逆伝播を手計算する(数値つき)
この節では、入力2個・隠れ層2個・出力1個の小さなネットワークで、全部の勾配を電卓で追える数値で求めます。
入力 x = (1.0, 0.5)、正解 t = 1 とし、隠れ層はシグモイド、出力は活性化なし、損失は L = ½(y − t)² です。重みは手で決めた値にしました。
| 記号 | 値 | 意味 |
|---|---|---|
| W1 | [[0.4, −0.2], [0.3, 0.6]] | 入力→隠れ層の重み(行が隠れ層の各ニューロン) |
| b1 | (0.1, −0.1) | 隠れ層のバイアス |
| W2 | (0.7, −0.5) | 隠れ層→出力の重み |
| b2 | 0.2 | 出力のバイアス |
順伝播(左から右へ)
| 手順 | 式 | 値 |
|---|---|---|
| ① 隠れ層の入力 z1 | 0.4×1.0 + (−0.2)×0.5 + 0.1 | 0.4 |
| ① 隠れ層の入力 z2 | 0.3×1.0 + 0.6×0.5 − 0.1 | 0.5 |
| ② 隠れ層の出力 h | σ(0.4)、σ(0.5) | 0.598688、0.622459 |
| ③ 出力 y | 0.7×0.598688 − 0.5×0.622459 + 0.2 | 0.307852 |
| ④ 損失 L | ½ × (0.307852 − 1)² | 0.239535 |
逆伝播(右から左へ)
| 求めるもの | 連鎖律の式 | 値 |
|---|---|---|
| ∂L/∂y | y − t | −0.692148 |
| ∂L/∂W2 | ∂L/∂y × h | (−0.414381, −0.430834) |
| ∂L/∂b2 | ∂L/∂y × 1 | −0.692148 |
| ∂L/∂h | ∂L/∂y × W2 | (−0.484504, 0.346074) |
| σ の微分 | h × (1 − h) | (0.240261, 0.235004) |
| ∂L/∂z | ∂L/∂h × h(1 − h) | (−0.116407, 0.081329) |
| ∂L/∂W1 | ∂L/∂z と x の外積 | [[−0.116407, −0.058204], [0.081329, 0.040664]] |
| ∂L/∂b1 | ∂L/∂z × 1 | (−0.116407, 0.081329) |
読み方の要点は3つです。1つ目、∂L/∂W2 が負なので、W2 を増やすと損失が減ります(y が正解の 1 より小さいので、y を上げる向き)。2つ目、∂L/∂h の2つ目が正なのは、W2 の2つ目が −0.5 で、h2 が増えると y が下がるからです。3つ目、∂L/∂z は ∂L/∂h に σ の微分(0.24 前後)を掛けたので、約4分の1に縮んでいます。シグモイドの微分は最大でも 0.25 なので、層を重ねるたびに勾配が4分の1以下になります。これが後の節の勾配消失の正体です。
同じ計算を numpy で書くと、逆伝播は6行です。
import numpy as np
x = np.array([1.0, 0.5]); t = 1.0 # 入力と正解
W1 = np.array([[0.4, -0.2], [0.3, 0.6]]); b1 = np.array([0.1, -0.1])
W2 = np.array([0.7, -0.5]); b2 = 0.2
sig = lambda z: 1 / (1 + np.exp(-z))
z = W1 @ x + b1; h = sig(z) # 順伝播
y = W2 @ h + b2; L = 0.5 * (y - t) ** 2
dy = y - t # 逆伝播
dW2 = dy * h; db2 = dy
dh = dy * W2
dz = dh * h * (1 - h) # シグモイドの微分
dW1 = np.outer(dz, x); db1 = dz
print("L =", round(L, 6), " dW1 =", dW1.round(6).tolist(), " dW2 =", dW2.round(6).tolist())
L = 0.239535 dW1 = [[-0.116407, -0.058204], [0.081329, 0.040664]] dW2 = [-0.414381, -0.430834]
この勾配で学習率 0.5 の更新を1回行うと、損失は 0.239535 → 0.001438、出力 y は 0.307852 → 0.946381 になり、正解の 1 に一気に近づきました。
数値微分で答え合わせする(勾配チェック)
この節では、逆伝播の結果が正しいかを確かめる「勾配チェック」の方法と、刻み幅の選び方が分かります。
数値微分は、重みを1つだけ ±ε 動かして損失の差を測る方法です。中心差分 (L(w + ε) − L(w − ε)) ÷ 2ε で求めます。式の導出に頼らないので、逆伝播の答え合わせに使えます。手計算のネットワークで ε = 1e-6 として比べました。
| 重み | 数値微分 | 逆伝播 |
|---|---|---|
| W1[0,0] | −0.1164072473 | −0.1164072473 |
| W1[0,1] | −0.0582036236 | −0.0582036236 |
| W1[1,0] | 0.0813287103 | 0.0813287104 |
| W1[1,1] | 0.0406643552 | 0.0406643552 |
| b1[0] | −0.1164072473 | −0.1164072473 |
| W2[0] | −0.4143806483 | −0.4143806483 |
| W2[1] | −0.4308341701 | −0.4308341701 |
小数第10位で1だけずれたのは、数値微分の側の丸め誤差です。数値微分は正確ですが、重み1つにつき損失を2回計算するので遅く、学習には使いません。「実装の確かめには数値微分、学習には逆伝播」と使い分けます。
刻み幅 ε は 1e-5 前後が最も合う
次の節の 2,410 個の重みを持つネットワーク(W2 の 320 個)で、ε を変えて相対誤差(差の大きさ ÷ 2つの大きさの和)を測りました。
| ε | 相対誤差(float64) | 相対誤差(float32) | 原因 |
|---|---|---|---|
| 1e-1 | 2.02e-03 | ― | 大きすぎて曲がりの影響が入る |
| 1e-2 | ― | 6.75e-05 | float32 ではこれが最良 |
| 1e-3 | 2.04e-07 | 6.60e-04 | ― |
| 1e-5 | 1.18e-10 | 6.24e-02 | float64 の最良。float32 は桁落ち |
| 1e-7 | 1.24e-08 | ― | 小さすぎて丸め誤差が増える |
| 1e-9 | 1.19e-06 | ― | 同上 |
| 1e-11 | 1.18e-04 | ― | 同上 |
ε は大きすぎても小さすぎても誤差が増え、谷の形になります。float64 なら ε = 1e-5 前後、相対誤差 1e-7 未満なら正しいと判断できます。float32 で勾配チェックをすると、正しい実装でも 1e-2 台の誤差が出て「間違っている」と誤解しやすいので、確かめるときは float64 に直します。
numpyで2層ニューラルネットワークの逆伝播を自作する
この節では、手書き数字を分類する 64-32-10 のネットワークを numpy で書き、行列のまま逆伝播する形と、勾配チェックの結果が分かります。
手計算との違いは、データを何件もまとめて(行列で)流すことと、出力がソフトマックス、損失が交差エントロピーになることです。隠れ層は ReLU にしました。
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X, y = load_digits(return_X_y=True) # 8×8 の手書き数字 1,797 枚
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=0, stratify=y)
sc = StandardScaler().fit(Xtr); Xtr, Xte = sc.transform(Xtr), sc.transform(Xte)
Ytr = np.eye(10)[ytr] # one-hot
def init(d, h, k, seed=0):
r = np.random.default_rng(seed)
return dict(W1=r.normal(0, np.sqrt(2 / d), (d, h)), b1=np.zeros(h),
W2=r.normal(0, np.sqrt(1 / h), (h, k)), b2=np.zeros(k))
def forward(p, X):
z1 = X @ p["W1"] + p["b1"]; h = np.maximum(z1, 0) # ReLU
z2 = h @ p["W2"] + p["b2"]; z2 = z2 - z2.max(1, keepdims=True)
e = np.exp(z2); P = e / e.sum(1, keepdims=True) # softmax
return z1, h, P
def loss(p, X, Y):
P = forward(p, X)[2]
return -np.mean(np.sum(Y * np.log(P + 1e-300), 1)) # 交差エントロピーの平均
def backward(p, X, Y):
n = len(X); z1, h, P = forward(p, X)
d2 = (P - Y) / n # softmax+交差エントロピーの勾配は P − Y
g = dict(W2=h.T @ d2, b2=d2.sum(0))
dh = d2 @ p["W2"].T # 重みの転置で1層戻る
d1 = dh * (z1 > 0) # ReLU の微分(0 か 1)
g["W1"] = X.T @ d1; g["b1"] = d1.sum(0)
return g
ソフトマックス+交差エントロピーの勾配は「P − Y」
出力の勾配が P − Y(予測確率 − 正解の one-hot)という簡単な形になるのは、ソフトマックスの微分と交差エントロピーの微分を連鎖律でつなぐと、複雑な項が打ち消し合うためです。正解クラスの確率が 0.7 なら勾配は −0.3、不正解クラスの確率が 0.2 ならその勾配は 0.2 になり、「正解を上げ、不正解を下げる」向きがそのまま出ます。交差エントロピーそのものの意味と計算は、関連記事「エントロピーとは|意味・計算・交差エントロピーをPythonで実演」で扱っています。
損失は件数 n の平均なので、勾配も n で割ります(/ n)。バイアスの勾配は、件数の方向(axis=0)に足し合わせます。バイアスは全部のデータに同じ値が足される「足し算ノード」なので、各データから届いた勾配を合計するのが答えです。
2,410個すべてを数値微分と比べる
p = init(64, 32, 10); Xs, Ys = Xtr[:50], Ytr[:50]
g = backward(p, Xs, Ys)
eps = 1e-5
for k, A in p.items():
G = np.zeros_like(A)
for i in np.ndindex(A.shape): # 2,410 個を1つずつ動かす
o = A[i]
A[i] = o + eps; lp = loss(p, Xs, Ys)
A[i] = o - eps; lm = loss(p, Xs, Ys)
A[i] = o; G[i] = (lp - lm) / (2 * eps) # 中心差分
rel = np.linalg.norm(g[k] - G) / (np.linalg.norm(g[k]) + np.linalg.norm(G))
print(k, A.shape, "相対誤差 %.2e" % rel)
W1 (64, 32) 相対誤差 3.05e-10
b1 (32,) 相対誤差 3.06e-10
W2 (32, 10) 相対誤差 1.18e-10
b2 (10,) 相対誤差 1.49e-10
4つの塊すべてで 1e-10 台になり、逆伝播の式とコードが正しいと確かめられました。この勾配で学習率 0.1・ミニバッチ32件・30周の学習を行った経過は次のとおりです。
| 周回 | 訓練の損失 | テストの正解率 |
|---|---|---|
| 1 | 0.6229 | 83.06% |
| 5 | 0.1267 | 93.89% |
| 10 | 0.0641 | 95.56% |
| 20 | 0.0282 | 96.39% |
| 30 | 0.0161 | 96.67% |
学習は 0.29 秒で終わりました。テスト360枚の分け方と初期値を変えると正解率は数%動くので、ほかの記事の数字と比べるときは分け方をそろえます。numpy の行列の掛け算(@)・形の変え方・keepdims は、関連記事「NumPyの使い方 基本一覧|配列の作成・演算・形状変更」で扱っています。
なぜ「逆」向きなのか:数値微分より約2,500倍速い理由
この節では、出力側から計算する理由を、計算の回数と実測の時間で確かめます。
損失は1つの数で、重みはたくさんあります。出力側から逆向きに計算すると、上流の勾配を使い回せるので、順伝播1回と逆伝播1回で全部の重みの勾配が出ます。入力側から順向きに微分する方法(フォワードモード)や数値微分では、重み1つごとに計算をやり直すことになります。
| 求め方 | 2,410個の勾配に要る計算 | 実測(50件) |
|---|---|---|
| 数値微分(中心差分) | 順伝播 4,820回 | 0.545 秒 |
| 誤差逆伝播法 | 順伝播1回+逆伝播1回 | 0.000222 秒 |
| 倍率 | ― | 約2,458倍 |
倍率がほぼ「重みの数 × 2」になっているのは、数値微分が重み1つにつき2回の順伝播をしているためです。重みが数億あるモデルでは、数値微分では学習ができません。
逆伝播を加えても、計算は順伝播の2〜3倍に収まります。その代わり、順伝播の途中の値(z1・h・P)を逆伝播まで覚えておくため、メモリを使います。大きなモデルで学習時のメモリが推論時より大きいのはこのためです。逆に、入力が1つで出力がたくさんある計算では、順向き(フォワードモード)のほうが速くなります。
リバースモード(逆伝播)
出力1つ・入力(重み)が多いときに速い
1回の逆向きの計算で全部の勾配が出る
途中の値を覚えておくメモリが要る
フォワードモード・数値微分
入力が少なく出力が多いときに向く
入力1つごとに計算をやり直す
数値微分は答え合わせ専用にする
自動微分を41行で自作する(スカラーの計算グラフ)
この節では、PyTorch の autograd などの自動微分が中で何をしているかを、41行の Python で作って確かめます。
自動微分は、順伝播の計算をしながら計算グラフを記録し、あとでそのグラフを逆順にたどって連鎖律を当てはめる仕組みです。各演算は「自分の局所的な微分」だけを知っていればよく、全体の式をまとめて微分しなくても勾配が出ます。
import math
class Value:
def __init__(s, data, parents=(), op=''):
s.data, s.grad = data, 0.0
s._parents, s._op = parents, op
s._backward = lambda: None
def __add__(s, o):
o = o if isinstance(o, Value) else Value(o)
out = Value(s.data + o.data, (s, o), '+')
def bw():
s.grad += out.grad # 足し算:そのまま流す
o.grad += out.grad
out._backward = bw; return out
def __mul__(s, o):
o = o if isinstance(o, Value) else Value(o)
out = Value(s.data * o.data, (s, o), '*')
def bw():
s.grad += o.data * out.grad # 掛け算:相手の値を掛ける
o.grad += s.data * out.grad
out._backward = bw; return out
def __pow__(s, k):
out = Value(s.data ** k, (s,), f'**{k}')
def bw(): s.grad += k * s.data ** (k - 1) * out.grad
out._backward = bw; return out
def sigmoid(s):
y = 1 / (1 + math.exp(-s.data)); out = Value(y, (s,), 'sigmoid')
def bw(): s.grad += y * (1 - y) * out.grad
out._backward = bw; return out
__radd__ = __add__; __rmul__ = __mul__
def __neg__(s): return s * -1
def __sub__(s, o): return s + (-o)
def backward(s):
order, seen = [], set()
def visit(v): # トポロジカル順に並べる
if v not in seen:
seen.add(v)
for p in v._parents: visit(p)
order.append(v)
visit(s)
s.grad = 1.0
for v in reversed(order): v._backward()
ポイントは3つです。1つ目、各演算が _backward に「上流の勾配 out.grad に局所的な微分を掛けて入力へ足す」処理を登録します。2つ目、backward() はグラフをトポロジカル順(入力が先、出力が後の順)に並べ、逆順に呼びます。これで、あるノードの勾配が全部の経路から集まり終わってから、その先へ流れます。3つ目、勾配は = ではなく += で足します。
a, b, c = Value(2.0), Value(3.0), Value(4.0)
f = (a + b) * c; f.backward()
x, y = Value(3.0), Value(2.0)
g = x * y + y; g.backward()
f 20.0 da 4.0 db 4.0 dc 5.0
g 8.0 dx 2.0 dy 4.0
最初の5分の例と、連鎖律の節の「経路ごとに足す」例が、どちらも手計算と同じ値になりました。手計算の2層ネットワークも Value で組んで L.backward() を呼ぶと、9個の勾配がすべて小数第10位まで一致し、グラフのノードは32個でした。
自作の自動微分で学習させる(numpy と損失が完全に一致)
make_moons の80点を、2-8-1(隠れ層・出力ともシグモイド、二乗誤差、学習率 2.0)のネットワークで200周学習させました。同じ初期値・同じ式を numpy の行列で書いたものと並べます。
| 周回 | 自作の自動微分の損失 | numpy の損失 | 正解率 |
|---|---|---|---|
| 1 | 0.2604 | 0.2604 | 65.0% |
| 50 | 0.1366 | 0.1366 | 77.5% |
| 100 | 0.1014 | 0.1014 | 85.0% |
| 200 | 0.0886 | 0.0886 | 87.5% |
| かかった時間 | 31.31 秒 | 0.0256 秒 | 約1,200倍の差 |
結果は同じで、速さは約1,200倍違いました。スカラー1個ずつのノードを Python で作ると、掛け算1回ごとにオブジェクトと関数が生まれるためです。実用の自動微分(PyTorch・JAX など)は、ノードを「行列1つ」の単位にし、中の計算を C++ や GPU で行うので速くなります。仕組みは同じで、単位の大きさが違うだけです。
scikit-learnのMLPClassifierと1回の更新を一致させる
この節では、自作の逆伝播が scikit-learn の MLPClassifier と同じ計算をしていることを、1回の更新で確かめます。
MLPClassifier は、公式ユーザーガイドに「勾配は誤差逆伝播法で計算する」と書かれており、多クラスでは出力にソフトマックス、損失に交差エントロピーを使います。モメンタムと L2 正則化(alpha)を0にし、全件を1つのバッチにすれば、自作の1回の更新と同じになるはずです。
from sklearn.neural_network import MLPClassifier
m = MLPClassifier(hidden_layer_sizes=(32,), activation="relu", solver="sgd",
learning_rate_init=0.1, momentum=0.0, nesterovs_momentum=False,
alpha=0.0, batch_size=len(Xtr), shuffle=False, random_state=0)
m.partial_fit(Xtr, ytr, classes=np.arange(10)) # 初期化して1回更新
p = dict(W1=m.coefs_[0].copy(), b1=m.intercepts_[0].copy(),
W2=m.coefs_[1].copy(), b2=m.intercepts_[1].copy()) # 同じ重みを自作側へ
g = backward(p, Xtr, Ytr)
for k in p: p[k] -= 0.1 * g[k] # 自作で1回更新
m.partial_fit(Xtr, ytr) # scikit-learn で1回更新
for k, a in [("W1", m.coefs_[0]), ("b1", m.intercepts_[0]),
("W2", m.coefs_[1]), ("b2", m.intercepts_[1])]:
print(k, "差の最大 %.2e" % np.abs(p[k] - a).max())
W1 差の最大 2.78e-17
b1 差の最大 2.78e-17
W2 差の最大 5.55e-17
b2 差の最大 8.67e-18
差は 1e-17 以下で、浮動小数点の丸めの範囲で一致しました。重みを MLPClassifier から自作側へ写してから1回ずつ更新しているので、初期化の方法の違いは入りません。
正解率と時間を比べる(3つの乱数の種の平均)
| 方法 | 設定 | テスト正解率 | 時間(1回) |
|---|---|---|---|
| numpy で自作 | SGD・学習率0.1・32件・30周 | 96.85% | 約0.3 秒 |
| MLPClassifier | 同じ SGD の設定・30周 | 97.41% | 0.68 秒 |
| MLPClassifier | 既定の Adam・200周 | 96.94% | 1.71 秒 |
| MLPClassifier | 既定の Adam・30周 | 92.50% | 0.22 秒 |
同じ設定の SGD どうしの差(96.85% と 97.41%)は、初期値の決め方とデータを回す順番の違いによるもので、1回の更新の計算は上のとおり一致しています。MLPClassifier は max_iter に達すると次の警告を出します。
ConvergenceWarning: Stochastic Optimizer: Maximum iterations (30) reached and the optimization hasn't converged yet.
業務では逆伝播を自作せず、MLPClassifier や PyTorch などの自動微分を使います。自作は仕組みの理解と、ライブラリの挙動を確かめる物差しとして役に立ちます。
勾配消失・勾配爆発を層ごとの勾配で見る
この節では、層を深くしたときに勾配が消えたり爆発したりする様子を、20層のネットワークで層ごとに測った数値で確かめます。
逆伝播では、1層戻るたびに「活性化関数の微分」と「重み」が掛かります。その掛け算が1より小さければ入力側へ行くほど勾配が0に近づき(勾配消失)、1より大きければ大きくなり続けます(勾配爆発)。幅64・20層のネットワークに手書き数字200件を流し、各層の重みの勾配の大きさ(ノルム)を測りました。
def grad_norms(f, df, scale, L=20, w=64, seed=0):
r = np.random.default_rng(seed); dims = [64] + [w] * L + [10]
Ws = [r.normal(0, scale(dims[i]), (dims[i], dims[i + 1])) for i in range(L + 1)]
a, cache = X, []
for i in range(L): # 順伝播
z = a @ Ws[i]; an = f(z); cache.append((a, z, an)); a = an
z = a @ Ws[L]; z -= z.max(1, keepdims=True)
P = np.exp(z); P /= P.sum(1, keepdims=True)
d = (P - Y) / len(X); norms = [0] * (L + 1)
norms[L] = np.linalg.norm(a.T @ d); d = d @ Ws[L].T
for i in reversed(range(L)): # 逆伝播しながら層ごとの勾配の大きさを記録
a_in, z, an = cache[i]
d = d * df(z, an); norms[i] = np.linalg.norm(a_in.T @ d); d = d @ Ws[i].T
return norms
| 活性化・初期値 | 1層目 | 10層目 | 20層目 | 1層目 ÷ 20層目 |
|---|---|---|---|---|
| シグモイド・Xavier | 4.47e-13 | 1.10e-07 | 1.82e-01 | 2.5e-12(消失) |
| シグモイド・N(0, 1) | 5.82e-02 | 1.20e+00 | 2.72e+00 | 2.1e-02(消失) |
| tanh・N(0, 0.01) | 2.34e-22 | 1.32e-22 | 1.40e-22 | 1.7(全層で消失) |
| tanh・Xavier | 3.45e-01 | 1.84e-01 | 1.88e-01 | 1.8(安定) |
| ReLU・N(0, 0.01) | 4.87e-26 | 4.31e-26 | 3.31e-26 | 1.5(全層で消失) |
| ReLU・N(0, 1) | 7.66e+14 | 1.68e+15 | 1.52e+15 | 0.50(全層で爆発) |
| ReLU・He | 4.36e-01 | 4.85e-01 | 3.46e-01 | 1.3(安定) |
Xavier は標準偏差 √(1/入力数)、He は √(2/入力数) で重みを作る初期化です。表から3つのことが読み取れます。
- シグモイドは Xavier でも20層で1兆分の1以下に消えます。入力が0付近だと微分が0.25前後になり、1層ごとに約4分の1になるためです(0.25 の19乗はおよそ 4e-12)。手計算の節で σ の微分を掛けたときに4分の1に縮んだのと同じ現象です。
- 重みが小さすぎると、層の位置に関係なく全部の勾配が 1e-22 台になります。順伝播の時点で信号が消えている(最後の層の出力の標準偏差が 1e-22 台)ので、逆伝播で届く勾配も消えます。
- ReLU に N(0, 1) を使うと 1e+15 まで爆発し、He 初期化なら全層で 0.3〜0.6 に収まりました。ReLU は半分を0にするので、分散を2倍にする He 初期化と相性が合います。
深さを変えて「1層目 ÷ 最後の隠れ層」を測ると、シグモイド・Xavier は 2層 0.58 → 5層 5.0e-03 → 10層 4.6e-06 → 20層 2.5e-12 → 30層 1.6e-18 と指数的に小さくなり、ReLU・He は 0.45〜1.3 の範囲にとどまりました。
勾配消失の対策は、ReLU 系の活性化関数、Xavier・He の初期化、バッチ正規化、残差接続(ResNet)、勾配爆発には勾配クリッピングが代表です。活性化関数ごとの微分の形は、第47弾で用意している記事「活性化関数とは|ReLU・シグモイド・ソフトマックスと勾配消失を実測」で詳しく比べます。時系列を扱う RNN では、時間方向に逆伝播(BPTT)するため同じ問題が起き、それをゲートで和らげたのが LSTM です。関連記事「LSTMとは|仕組み・3つのゲート・RNNとの違いをPythonで実演」で扱っています。
逆伝播のよくある実装ミス5つを再現する
この節では、逆伝播を自作したときに起きやすい誤りを実際に入れて、エラーになるもの・黙って間違うものを見分けられます。
d2 = (P - Y) # 誤り1:損失は平均なのに n で割っていない
g["b1"] = d1.sum() # 誤り2:axis=0 を忘れ、スカラーになる(黙って全要素に足される)
g["W1"] = d1.T @ X # 誤り3:転置の向きが逆(正方行列だとエラーにならない)
pred - y # 誤り4:(100, 1) − (100,) が (100, 100) に広がる
s.grad = out.grad # 誤り5:自動微分で += ではなく =(2回使った変数の勾配が消える)
| 誤り | 起きたこと | 勾配チェックの相対誤差 | エラー |
|---|---|---|---|
| 1. 平均なのに n で割らない | 勾配が50倍(バッチの件数倍)になる | 9.61e-01(全部の重み) | 出ない |
| 2. バイアスの sum の axis 忘れ | b1 の勾配が形 () のスカラーになり、全要素に同じ値が足される | 8.54e-01(b1 だけ) | 出ない |
| 3. 転置の向きが逆(64×32) | 更新の引き算で形が合わない | ― | ValueError |
| 3'. 同じ誤りで 32×32 の層 | 形が合ってしまい、間違った勾配で学習する | 6.97e-01 | 出ない |
| 4. (n,1) と (n,) の引き算 | 差が (100, 100) に広がり、二乗誤差が 0.0000 のはずが 16.66 になる | ― | 出ない |
| 5. 自動微分で += を = にする | g = x×y + y の ∂g/∂y が 4 ではなく 3 になる | ― | 出ない |
誤り3のエラー原文は次のとおりです。
ValueError: operands could not be broadcast together with shapes (64,32) (32,64) (64,32)
6つのうちエラーになったのは1つだけでした。残りは黙って間違った勾配を返し、学習も「それなりに」進むことがあるので、正解率だけ見ていると気づけません。誤り1の 0.961 は 49 ÷ 51(勾配が50倍になったときの相対誤差)で、学習率を50倍にしたのと同じになります。
自作の逆伝播を書き終えたとき
平均の取り忘れ・axis 忘れ・正方行列での転置ミスは、どれもエラーを出さずに学習が進んだ
もう1つの落とし穴:勾配を0に戻し忘れる
自作の自動微分で、毎回の更新の前に grad = 0 に戻さずに backward() を3回呼ぶと、∂g/∂y は 4 → 8 → 12 と前の回の分が足され続けました。PyTorch の optimizer.zero_grad() が必要なのも、勾配を += でためる仕組みだからです。
よくあるエラー・つまずきと判断表
この節では、逆伝播まわりで出るエラーの原文と直し方、自作するかライブラリを使うかの判断が分かります。
| 症状・エラー原文 | 原因 | 直し方 |
|---|---|---|
| ValueError: operands could not be broadcast together with shapes (64,32) (32,64) (64,32) | dW の転置の向きが逆 | 順伝播が x @ W なら dW = x.T @ dz。dW と W の形が同じか assert で確かめる |
| RuntimeWarning: overflow encountered in exp と invalid value encountered in divide、出力が nan | ソフトマックスで exp(1000) があふれた | 各行の最大値を引いてから exp する。[[nan 0. 0.]] が [[1. 0. 0.]] になった |
| RecursionError: maximum recursion depth exceeded | 自作の自動微分の再帰が、1,200段の計算グラフで上限1,000を超えた | トポロジカル順の並べ方を、再帰ではなくスタックを使う書き方にする |
| ConvergenceWarning: Stochastic Optimizer: Maximum iterations (30) reached … | MLPClassifier が max_iter までに収束しなかった | max_iter を増やすか、学習率・標準化を見直す。警告だけで結果は返る |
| 損失が下がらない・最初から変わらない | 勾配消失(シグモイドの多層、重みが小さすぎる) | ReLU と He 初期化にする。層ごとの勾配のノルムを出して確かめる |
| 損失が inf・nan になる | 勾配爆発か学習率が大きすぎる | 学習率を10分の1にする。勾配クリッピングを入れる |
| 勾配チェックが 1e-2 前後で合わない | float32 で数値微分した | 確かめるときだけ float64 にし、ε = 1e-5 前後で比べる |
判断表:逆伝播をどう扱うか
| 場面 | 選ぶもの | 理由 |
|---|---|---|
| 仕組みを理解したい・試験の勉強 | 手計算 → numpy で自作 → 数値微分で確かめる | 式とコードが1対1で対応し、一致で正しさを確かめられる |
| 表データで小さなネットワークを試す | MLPClassifier・MLPRegressor(標準化を Pipeline に入れる) | 逆伝播と最適化が組み込み済み。自作と1回の更新で一致した |
| 画像・文章・独自の層を使う | PyTorch・JAX などの自動微分 | 行列単位のノードと GPU で速い。自作のスカラー版の約1,200倍以上 |
| 独自の層・損失を書いた | その部分だけ数値微分で勾配チェック | 自動微分を使っても、手で書いた backward は誤りうる |
| 深いネットワークで学習が止まる | 層ごとの勾配のノルムを記録して見る | 消失(1e-10 以下)か爆発(1e+5 以上)かで対策が変わる |
自作の逆伝播を確かめる順番は次のとおりです。
- 1形を確かめるdW と W、db と b の shape が同じか assert する
- 2小さなバッチで勾配チェックfloat64・ε = 1e-5・相対誤差 1e-7 未満
- 3数件のデータで損失が0近くまで下がるか丸暗記できなければ逆伝播か学習率に誤り
- 4ライブラリと1回の更新を比べるMLPClassifier と 1e-17 以下で一致した
- 5層ごとの勾配のノルムを見る深くしたときの消失・爆発を早めに見つける
- 1損失を平均にしたら、勾配も件数で割る割り忘れると学習率がバッチの件数倍になったのと同じ
- 2正解ラベルの形を予測とそろえる(n,1) と (n,) は黙って (n,n) に広がる。reshape か [:, 0] でそろえる
- 3勾配は += でためて、更新ごとに0に戻す= だと分岐の勾配が消え、0に戻さないと前の回の分が残る
よくある質問
Q. 誤差逆伝播法とバックプロパゲーションは違うものですか? A. 同じものです。英語の Backpropagation(back-propagation of errors)の訳が誤差逆伝播法で、誤差逆伝搬法・逆伝播法・BP と書かれることもあります。
Q. 誤差逆伝播法と勾配降下法の違いは何ですか? A. 逆伝播は勾配を「求める」計算、勾配降下法は求めた勾配で重みを「動かす」方法です。学習ではこの2つを毎回続けて行います。Adam などを使っても、勾配を求める部分は同じ逆伝播です。
Q. わかりやすく一言で言うと何ですか? A. 「出力の誤差の責任を、計算をさかのぼって各重みに配る計算」です。足し算はそのまま、掛け算は相手の値を掛けて、活性化関数はその微分を掛けて配ります。
Q. 連鎖律が分からなくても使えますか? A. ライブラリを使うだけなら、逆伝播は自動で行われます。ただ、勾配消失・nan・学習が進まないときの原因を探すには、「1層戻るたびに微分が掛かる」ことを知っていると見当がつけやすくなります。
Q. 手計算はどこまで必要ですか? A. この記事の2層ネットワーク(勾配9個)を一度追えば、層が増えても同じ規則の繰り返しです。試験対策なら、シグモイド・ReLU の微分、ソフトマックス+交差エントロピーの P − Y、行列の転置の位置を押さえます。
Q. なぜ活性化関数の微分を掛けるのですか? A. 順伝播で h = σ(z) という計算をしているので、連鎖律で ∂L/∂z = ∂L/∂h × σ'(z) になるためです。シグモイドの微分は h(1 − h)、tanh は 1 − h²、ReLU は z > 0 なら 1、それ以外は 0 です。
Q. ReLU は z = 0 で微分できないのに、なぜ使えるのですか? A. ちょうど0になることはまれで、実装では 0 か 1 のどちらかに決めて扱います(この記事の自作は z > 0 のときだけ 1)。勾配チェックで z が0のすぐ近くにあると数値微分とずれることがあるので、その要素は比べる対象から外します。
Q. 自動微分と数値微分と記号微分の違いは何ですか? A. 数値微分は値を少し動かして差を測る方法で、遅く誤差があります。記号微分は式そのものを微分した式を作る方法で、式が膨らみやすくなります。自動微分は計算の記録をたどって連鎖律を当てはめる方法で、速く、丸め誤差の範囲で正確です。逆伝播は自動微分のリバースモードにあたります。
Q. 誤差逆伝播法は脳でも行われているのですか? A. 脳が同じ計算をしているとは確かめられておらず、研究の途中の話題です。誤差逆伝播法は、脳の仕組みの再現ではなく、微分を効率よく求める数学の手順として理解するのが確実です。
Q. E資格・G検定ではどこが問われますか? A. 連鎖律、計算グラフの足し算・掛け算ノードの逆伝播、活性化関数の微分、勾配消失とその対策(ReLU・初期化・バッチ正規化)がよく出ます。E資格の出題範囲と numpy での解き方は、関連記事「E資格とは|試験内容・合格率・G検定との違いとNumPyで解く要点」で扱っています。
まとめ
- 誤差逆伝播法は、出力の誤差から逆向きに、連鎖律で全部の重みの勾配を一度に求める方法で、重みを動かすのは勾配降下法の仕事です。
- 規則は「足し算はそのまま、掛け算は相手の値を掛ける、活性化関数は微分を掛ける、分岐は足す」の4つです。2層ネットワークの9個の勾配を手計算し、数値微分と小数第10位まで一致しました。
- 逆向きに計算するのは速いからです。2,410個の勾配で数値微分の約2,458倍速く、逆伝播込みでも順伝播の2.24倍の計算でした。
- 自作の逆伝播と41行の自動微分は、数値微分と 1e-10 台、MLPClassifier と 1e-17 以下で一致しました。自動微分は計算グラフを記録して逆順にたどる仕組みで、実用のものは行列単位で速くしています。
- 深くすると勾配が消えたり爆発したりします(シグモイド20層で1兆分の1以下、ReLU と N(0,1) で 1e+15)。実装ミスの多くはエラーを出さないので、勾配チェックで確かめてから学習させます。
参考・出典
いずれも確認日は 2026年11月5日です。
- David E. Rumelhart, Geoffrey E. Hinton, Ronald J. Williams「Learning representations by back-propagating errors」Nature 323, 533–536(1986):https://ui.adsabs.harvard.edu/abs/1986Natur.323..533R/abstract
- scikit-learn「1.17. Neural network models (supervised)」(User Guide):https://scikit-learn.org/stable/modules/neural_networks_supervised.html
- scikit-learn「MLPClassifier」:https://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPClassifier.html
- Stanford CS231n「Backpropagation, Intuitions」:https://cs231n.github.io/optimization-2/
- Xavier Glorot, Yoshua Bengio「Understanding the difficulty of training deep feedforward neural networks」(AISTATS 2010):https://proceedings.mlr.press/v9/glorot10a.html
- Kaiming He ほか「Delving Deep into Rectifiers」:https://arxiv.org/abs/1502.01852
- PyTorch「Automatic differentiation package - torch.autograd」:https://docs.pytorch.org/docs/stable/autograd.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「誤差逆伝播法とは|連鎖律の手計算・numpyで自作・自動微分まで」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。