ニューラルネットワークの学習のしくみ:誤差逆伝播法を手で計算する
ニューラルネットワークの学習は、「損失(どれだけ間違えたか)が下がる向きに、すべての重みを少しずつ動かす」ことのくり返しです。そのためには、何百万個もある重みのそれぞれについて「少し動かすと損失がどれだけ変わるか」(勾配)を求める必要があります。これを効率よく計算する方法が 誤差逆伝播法(バックプロパゲーション)です。
画像分類 AI のしくみ では、損失関数と勾配降下法を説明し、誤差逆伝播法は「出力側から入力側へ連鎖律で勾配を計算する」と紹介するだけでした。この記事では、その中身を実際に手で計算します。ほかの機械学習の記事(アテンションと Transformer のしくみ・画像生成 AI のしくみ・強化学習のしくみ)に出てくるモデルも、すべてこの方法で学習しています。
結論を先に書くと、誤差逆伝播法は次のような考え方です。
- 計算を小さな部品に分ける: 掛け算・足し算・活性化関数のように、それぞれ微分が簡単な部品の組み合わせとして見る
- 出力側から順に勾配を掛けていく: 合成関数の微分の 連鎖律を使い、「後ろから届いた勾配 × その部品の微分」を前の部品へ渡していく
- 1 回の逆向きの計算ですべての重みの勾配が求まる: 重みを 1 個ずつ動かして調べる方法に比べ、重みが多いほど圧倒的に速い
説明の途中に出てくる計算例は、Python と NumPy で実際に動かした結果です。
環境
計算例を動かした環境です。
| 項目 | バージョン |
|---|---|
| Python | 3.13 |
| NumPy | 2.5 |
求めたいもの:損失についての重みの勾配
画像分類の記事で説明したとおり、勾配降下法では次の式で重みを更新します。
新しい重み = 重み − 学習率 × 勾配
勾配は「その重みをほんの少し増やしたとき、損失がどれだけ増えるか」の割合、つまり損失をその重みで微分した値です。
いちばん素直な求め方は、重みを本当に少しだけ動かして損失を計算し直す 数値微分です。ところが、この方法では重み 1 個ごとに損失を計算し直す必要があり、重みが 1 億個あれば 1 億回(以上)の計算になってしまいます。誤差逆伝播法は、これを「順向きに 1 回、逆向きに 1 回」の計算で済ませます。
連鎖律:計算を小さな部品に分けて考える
計算グラフ
f(x, y, z) = (x × y + z)² という関数で考えます。これを、次のように 3 つの小さな計算に分けます。
a = x × y (掛け算)
b = a + z (足し算)
f = b² (2 乗)
このように計算を部品とその間のつながりで表したものを 計算グラフと呼びます。ニューラルネットワークも、掛け算・足し算・活性化関数をたくさんつないだ、大きな計算グラフです。
出力側から勾配を伝える
f を x で微分したいとき、合成関数の微分の 連鎖律を使うと、次のように部品ごとの微分の掛け算に分けられます。
df/dx = df/db × db/da × da/dx
それぞれの部品の微分は簡単です。
| 部品 | 微分 |
|---|---|
| f = b² | df/db = 2b |
| b = a + z | db/da = 1、db/dz = 1 |
| a = x × y | da/dx = y、da/dy = x |
そこで、出力の f から始めて、「後ろから届いた勾配 × その部品の微分」を前へ前へと渡していきます。x = 2、y = −3、z = 4 で計算してみます。
# f(x, y, z) = (x * y + z) ** 2 を、小さな計算に分ける
x, y, z = 2.0, -3.0, 4.0
a = x * y # 掛け算
b = a + z # 足し算
f = b ** 2 # 2 乗
print(a, b, f)
# 逆伝播:出力側から順に「f をその値で微分したもの」を求める
df_db = 2 * b # (b²)' = 2b
df_da = df_db * 1 # b = a + z なので db/da = 1
df_dz = df_db * 1 # db/dz = 1
df_dx = df_da * y # a = x * y なので da/dx = y
df_dy = df_da * x # da/dy = x
print(df_dx, df_dy, df_dz)
# 数値微分で答え合わせ
h = 1e-5
fn = lambda x, y, z: (x * y + z) ** 2
print(round((fn(x + h, y, z) - fn(x - h, y, z)) / (2 * h), 4),
round((fn(x, y + h, z) - fn(x, y - h, z)) / (2 * h), 4),
round((fn(x, y, z + h) - fn(x, y, z - h)) / (2 * h), 4))
実行結果:
-6.0 -2.0 4.0
12.0 -8.0 -4.0
12.0 -8.0 -4.0
1 行目が順向きの計算(順伝播)の途中の値、2 行目が逆向きの計算(逆伝播)で求めた x・y・z の勾配、3 行目が数値微分で求めた勾配です。2 行目と 3 行目が一致していて、逆伝播の計算が正しいことが分かります。
部品ごとに見ると、勾配の伝わり方には決まった形があります。
- 足し算は、後ろから届いた勾配を、そのまま両方の入力に配る(b = a + z で、a にも z にも −4 が届く)
- 掛け算は、後ろから届いた勾配に「相手の入力の値」を掛けて渡す(x には y = −3 を、y には x = 2 を掛ける)
自動微分:計算の履歴を覚えて勾配を出す
PyTorch などのライブラリは、この計算を自動で行います。しくみは、「計算するたびに、どの値から作られたか(履歴)と、勾配の伝え方を覚えておき、最後に逆の順でたどる」というものです。これを 自動微分(リバースモード)と呼びます。
足し算・掛け算・tanh だけを持つ、小さな自動微分を作ってみます。
import math
class Value:
"""値と勾配を持ち、計算の履歴を覚えておく数"""
def __init__(self, data, parents=(), backward=lambda: None):
self.data = data
self.grad = 0.0
self._parents = parents
self._backward = backward
def __add__(self, other):
out = Value(self.data + other.data, (self, other))
def backward():
self.grad += out.grad
other.grad += out.grad
out._backward = backward
return out
def __mul__(self, other):
out = Value(self.data * other.data, (self, other))
def backward():
self.grad += other.data * out.grad
other.grad += self.data * out.grad
out._backward = backward
return out
def tanh(self):
t = math.tanh(self.data)
out = Value(t, (self,))
def backward():
self.grad += (1 - t * t) * out.grad
out._backward = backward
return out
def backward(self):
# 計算した順に並べ、逆の順に勾配を伝える
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(self)
self.grad = 1.0
for v in reversed(order):
v._backward()
# ニューロン 1 個:y = tanh(w1 * x1 + w2 * x2 + b)
x1, x2 = Value(2.0), Value(-1.0)
w1, w2, b = Value(0.5), Value(1.5), Value(0.25)
y = (w1 * x1 + w2 * x2 + b).tanh()
y.backward()
print(round(y.data, 4))
print(round(w1.grad, 4), round(w2.grad, 4), round(b.grad, 4))
実行結果:
-0.2449
1.88 -0.94 0.94
入力の重み付きの和は 0.5 × 2 + 1.5 × (−1) + 0.25 = −0.25 で、出力は tanh(−0.25) ≒ −0.2449 です。tanh の微分は 1 − tanh² ≒ 0.94 なので、b の勾配は 0.94、w1 の勾配はそれに入力 x1 = 2 を掛けた 1.88、w2 の勾配は x2 = −1 を掛けた −0.94 になります。手で計算した値と一致しています。
勾配を = ではなく += で足しているのは、1 つの値が何か所でも使われるときに、それぞれの道から届いた勾配を合計する必要があるからです。PyTorch の loss.backward() も、規模は違いますが同じことをしています。
2 層のネットワークで逆伝播を計算する
XOR を解くネットワーク
次は、行列でまとめて計算する、実際のニューラルネットワークの形で逆伝播を書きます。題材は XOR(排他的論理和)で、2 つの入力が違うときだけ 1 を出す問題です。
| 入力 1 | 入力 2 | 正解 |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
ネットワークは、入力 2 個 → 隠れ層 4 個(活性化関数は tanh)→ 出力 1 個(シグモイドで 0〜1 の確率にする)の 2 層です。損失は 2 値の交差エントロピー誤差を使います。
順伝播と逆伝播を書く
逆伝播は、順伝播の計算を出力側から 1 段ずつ逆にたどって書きます。出力層のシグモイドと交差エントロピー誤差を組み合わせると、出力の手前の値(z2)についての勾配は「予測 − 正解」という簡単な形になります。これは画像分類の記事の「ソフトマックス + 交差エントロピー誤差の勾配は、確率 − 正解」と同じ性質です。
import numpy as np
# XOR:入力が違うときだけ 1
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
t = np.array([[0], [1], [1], [0]], dtype=float)
rng = np.random.default_rng(1)
W1, b1 = rng.normal(0, 1, (2, 4)), np.zeros(4) # 入力 2 → 隠れ層 4
W2, b2 = rng.normal(0, 1, (4, 1)), np.zeros(1) # 隠れ層 4 → 出力 1
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def forward(W1, b1, W2, b2):
h = np.tanh(X @ W1 + b1)
y = sigmoid(h @ W2 + b2)
loss = -np.mean(t * np.log(y) + (1 - t) * np.log(1 - y)) # 2 値の交差エントロピー誤差
return h, y, loss
def backward(h, y):
n = len(X)
dz2 = (y - t) / n # シグモイド + 交差エントロピー誤差の勾配
dW2, db2 = h.T @ dz2, dz2.sum(axis=0)
dh = dz2 @ W2.T # 出力層から隠れ層へ勾配を戻す
dz1 = dh * (1 - h**2) # tanh の微分を掛ける
dW1, db1 = X.T @ dz1, dz1.sum(axis=0)
return dW1, db1, dW2, db2
h, y, loss = forward(W1, b1, W2, b2)
dW1, db1, dW2, db2 = backward(h, y)
# 数値微分で W1 の勾配を確かめる
num = np.zeros_like(W1)
eps = 1e-6
for i in range(W1.shape[0]):
for j in range(W1.shape[1]):
Wp, Wm = W1.copy(), W1.copy()
Wp[i, j] += eps
Wm[i, j] -= eps
num[i, j] = (forward(Wp, b1, W2, b2)[2] - forward(Wm, b1, W2, b2)[2]) / (2 * eps)
print(np.round(dW1, 5))
print(np.round(num, 5))
print(f"{np.abs(dW1 - num).max() / np.abs(num).max():.1e}")
実行結果:
[[-0.02928 -0.01031 0.00035 0.02804]
[-0.0009 -0.00961 0.00193 0.01291]]
[[-0.02928 -0.01031 0.00035 0.02804]
[-0.0009 -0.00961 0.00193 0.01291]]
2.0e-09
1 つ目の表が逆伝播で求めた W1 の勾配、2 つ目が数値微分で求めた勾配、最後の行が 2 つの差の最大を勾配の大きさで割った値です。差は 10 億分の 2 で、逆伝播の式が正しく書けていることが分かります。このように数値微分と比べる確かめ方を 勾配チェックと呼び、逆伝播を自分で書いたときの間違い探しに使われます。
逆伝播のコードには、自動微分の例と同じ形が行列で出てきます。
dW2 = h.T @ dz2: 掛け算の勾配は「相手の入力の値」を掛ける。行列の掛け算なので、入力 h を転置して掛けるdh = dz2 @ W2.T: 前の層へ戻すときは、今度は重み W2 を(転置して)掛けるdz1 = dh * (1 - h**2): 活性化関数のところでは、その微分を要素ごとに掛ける
学習させる
求めた勾配で、重みを 2,000 回更新してみます。上のコードの続きです。
# 学習:勾配降下法で 2,000 回更新する
lr = 1.0
for step in range(1, 2001):
h, y, loss = forward(W1, b1, W2, b2)
dW1, db1, dW2, db2 = backward(h, y)
W1 -= lr * dW1; b1 -= lr * db1
W2 -= lr * dW2; b2 -= lr * db2
if step in [1, 100, 500, 2000]:
print(step, round(float(loss), 4))
print(np.round(forward(W1, b1, W2, b2)[1].ravel(), 3))
実行結果:
1 0.6712
100 0.0385
500 0.0052
2000 0.0012
[0. 0.999 0.999 0.002]
損失は 0.6712 から 0.0012 まで下がり、4 つの入力に対する出力は 0・0.999・0.999・0.002 と、正解の 0・1・1・0 にほぼ一致しました。
ただし、いつもこうなるとは限りません。最初の重みを決める乱数の種を 0 に変えて同じ学習をすると、損失は 0.3476 で止まり、出力は 0.001・0.499・0.999・0.501 になりました。入力 (0, 1) と (1, 1) を見分けられないまま、勾配がほぼ 0 の場所で動けなくなったのです。勾配降下法は「今いる場所で下り坂の向きに進む」だけなので、どこにたどり着くかは出発点しだいです。実際の大きなネットワークでは、重みの数が多いことや、次の記事で説明する最適化手法の工夫によって、こうした場所にはまりにくくなります。
活性化関数がないと層を重ねても意味がない
隠れ層の tanh を外し、活性化関数なし(線形)にして、同じ XOR を学習させてみます。順伝播の np.tanh(X @ W1 + b1) を X @ W1 + b1 に、逆伝播の dz1 = dh * (1 - h**2) を dz1 = dh に変えるだけです。
実行結果:
1 0.7201
100 0.6931
500 0.6931
2000 0.6931
[0.5 0.5 0.5 0.5]
損失は 0.6931(= log 2、「どれも半々」と答えたときの値)で止まり、4 つの入力すべてに 0.5 を返すようになりました。
線形の層は行列を掛けるだけなので、何層重ねても 1 つの行列を掛けるのと同じで、平面を 1 本の直線で分けることしかできません。XOR の 4 つの点は 1 本の直線では分けられないので、線形のネットワークでは解けないのです。1969 年にミンスキーとパパートが、1 層のパーセプトロンでは XOR が解けないことを示したことは、ニューラルネットワークの研究が下火になるきっかけの 1 つでした。隠れ層と、曲がった(非線形の)活性化関数、そしてそれを学習できる誤差逆伝播法がそろって、この壁が越えられました。
重みの初期値:同じ値から始めると学習できない
対称性の問題
最初の重みは乱数で決めました。では、すべての重みを同じ値(0.5)から始めるとどうなるでしょうか。上のコードの続きで試してみます。
# すべての重みを同じ値 0.5 にして学習してみる
W1, b1 = np.full((2, 4), 0.5), np.zeros(4)
W2, b2 = np.full((4, 1), 0.5), np.zeros(1)
for step in range(2000):
h, y, loss = forward(W1, b1, W2, b2)
dW1, db1, dW2, db2 = backward(h, y)
W1 -= lr * dW1; b1 -= lr * db1
W2 -= lr * dW2; b2 -= lr * db2
print(round(float(loss), 4))
print(np.round(W1, 3))
print(np.round(y.ravel(), 3))
実行結果:
0.4792
[[3.886 3.886 3.886 3.886]
[3.886 3.886 3.886 3.886]]
[0.002 0.666 0.666 0.668]
2,000 回学習したあとも、W1 の 8 個の重みはすべて 3.886 のままそろっています。隠れ層の 4 個のニューロンは、同じ入力に同じ重みを掛けているので同じ値を出し、逆伝播で受け取る勾配も同じです。そのため、いつまでたっても同じように動き、4 個あっても 1 個ぶんの働きしかしません。出力も (0, 1)・(1, 0)・(1, 1) の 3 つを見分けられていません。
これを 対称性の問題と呼び、重みを乱数で初期化するのは、ニューロンごとに違う役割を持たせるためです。
乱数の大きさも大事
乱数の大きさ(ばらつき)も重要です。大きすぎると層を通るたびに値が膨らみ、小さすぎると縮んでいきます。よく使われるのは、前の層のニューロンの数 n に合わせてばらつきを決める方法です。
| 初期化 | 標準偏差 | 向いている活性化関数 |
|---|---|---|
| Xavier(Glorot)の初期化 | √(1/n) | シグモイド・tanh |
| He の初期化 | √(2/n) | ReLU |
勾配消失:深いネットワークで勾配が届かない
シグモイドの微分は最大でも 0.25
逆伝播では、層を 1 つ戻るたびに、活性化関数の微分が掛け算されます。ここで問題になるのが、シグモイドの微分の大きさです。
import numpy as np
# 活性化関数の微分の最大値
z = np.linspace(-6, 6, 10001)
s = 1 / (1 + np.exp(-z))
print(round(float((s * (1 - s)).max()), 4), round(float((1 - np.tanh(z) ** 2).max()), 4))
# 30 層の全結合のネットワークで、各層に戻ってくる勾配の大きさを比べる
def grad_norms(act, n_layers=30, width=100, seed=0):
rng = np.random.default_rng(seed)
x = rng.standard_normal((64, width))
Ws, hs = [], [x]
for _ in range(n_layers):
if act == "sigmoid":
W = rng.normal(0, np.sqrt(1 / width), (width, width)) # Xavier の初期化
hs.append(1 / (1 + np.exp(-(hs[-1] @ W))))
else:
W = rng.normal(0, np.sqrt(2 / width), (width, width)) # He の初期化
hs.append(np.maximum(0, hs[-1] @ W))
Ws.append(W)
g = np.ones_like(hs[-1]) # 出力側の勾配を 1 とする
norms = []
for i in reversed(range(n_layers)):
h = hs[i + 1]
dz = g * (h * (1 - h) if act == "sigmoid" else (h > 0))
norms.append(np.linalg.norm(hs[i].T @ dz)) # i 層目の重みの勾配の大きさ
g = dz @ Ws[i].T
return norms[::-1]
for act in ["sigmoid", "relu"]:
n = grad_norms(act)
print(act, [f"{n[i]:.1e}" for i in [29, 20, 10, 0]])
実行結果:
0.25 1.0
sigmoid ['7.7e+02', '1.8e-03', '7.7e-10', '6.4e-17']
relu ['4.3e+03', '9.6e+03', '7.7e+03', '2.1e+03']
1 行目は、シグモイドと tanh の微分の最大値です。シグモイドの微分は最大でも 0.25 なので、層を 1 つ戻るたびに勾配が 4 分の 1 以下になっていきます。
2・3 行目は、幅 100・30 層のネットワークで、出力に近い 30 層目から、21 層目・11 層目・入力に近い 1 層目までの重みの勾配の大きさを比べたものです。シグモイドでは、30 層目の 770 に対して 1 層目は 10 の −17 乗で、入力に近い層にはほとんど勾配が届いていません。これでは入力に近い層の重みが学習で変わらなくなります。これを 勾配消失と呼びます。ReLU(He の初期化)では、どの層でも 10³ 前後の勾配が届いています。
勾配消失への対策
勾配消失は、深いネットワークを学習させるうえで長く大きな壁でした。今は次のような方法を組み合わせて対策しています。
| 対策 | 内容 |
|---|---|
| ReLU | 正の入力での微分が 1 なので、層を戻っても勾配が縮まない |
| 初期値の工夫 | Xavier・He の初期化で、層を通る値と勾配の大きさを保つ |
| 正規化の層 | バッチ正規化・層正規化で、層ごとの値の大きさをそろえる |
| 残差接続 | 入力をそのまま出力に足す道を作り、勾配が足し算の道を通ってそのまま届くようにする(ResNet・Transformer) |
逆に、勾配が層を戻るたびに大きくなって発散する 勾配爆発もあります。こちらは、勾配の大きさが決めた値を超えたら縮める 勾配クリッピングでよく対策されます。
計算の手間:数値微分との比較
誤差逆伝播法がどれだけ速いかを、重みが 10,000 個(100 × 100)の層で比べてみます。
import time
import numpy as np
# 重みの数が多いときの、数値微分と誤差逆伝播法の手間の違い
rng = np.random.default_rng(0)
X = rng.standard_normal((256, 100))
t = rng.standard_normal((256, 1))
W1 = rng.standard_normal((100, 100)) * 0.1
W2 = rng.standard_normal((100, 1)) * 0.1
def loss_fn(W1):
return float(((np.tanh(X @ W1) @ W2 - t) ** 2).mean())
start = time.perf_counter()
h = np.tanh(X @ W1)
dz = 2 * (h @ W2 - t) / len(X) @ W2.T * (1 - h**2)
grad = X.T @ dz # 誤差逆伝播法:1 回の逆伝播で 10,000 個の勾配
t_bp = time.perf_counter() - start
start = time.perf_counter()
num = np.zeros_like(W1)
eps = 1e-6
for i in range(100):
for j in range(100):
Wp, Wm = W1.copy(), W1.copy()
Wp[i, j] += eps
Wm[i, j] -= eps
num[i, j] = (loss_fn(Wp) - loss_fn(Wm)) / (2 * eps) # 重み 1 個につき順伝播 2 回
t_num = time.perf_counter() - start
print(f"{np.abs(grad - num).max():.1e}", round(t_num / t_bp))
実行結果(2 回実行):
2.5e-10 1935
2.5e-10 2289
左の値は 2 つの方法で求めた勾配の差の最大で、同じ答えが出ています。右の値は、数値微分が誤差逆伝播法の何倍の時間がかかったかです。時間は実行する環境やそのときの負荷で変わりますが、この記事を書いた環境では 2,000 倍前後でした。
数値微分は重み 1 個につき順伝播を 2 回、合わせて 20,000 回計算するのに対し、誤差逆伝播法は順伝播 1 回と逆伝播 1 回で済みます。重みが何億個にもなる今のモデルでは、誤差逆伝播法なしには学習ができません。
このサイトの ニューラルネットワークの学習の可視化 では、平面上の 2 種類の点を分けるネットワークが誤差逆伝播法で学習していく様子を見られます。XOR のデータや、活性化関数を「線形」にしたときに曲がった境界が作れないことも確かめられます。
誤差逆伝播法の流れ
この記事に出てきた手法を年代順に並べます。研究者の動きは AI 開発のキープレイヤー年表 にまとめています。
| 年 | 手法・出来事 | ポイント |
|---|---|---|
| 1958 | パーセプトロン | 1 層のニューラルネットワークを学習させる |
| 1969 | 『パーセプトロン』(ミンスキー・パパート) | 1 層のパーセプトロンでは XOR が解けないことを示す |
| 1970 | リバースモードの自動微分(リンナインマー) | 計算の履歴を逆にたどって微分を求める方法 |
| 1986 | 誤差逆伝播法(ラメルハート・ヒントン・ウィリアムズ) | 多層のネットワークの学習に使えることを示し、広く知られる |
| 1991 | 勾配消失の分析(ホッフライター) | 深いネットワークや RNN で勾配が届かなくなることを示す |
| 2010 | Xavier の初期化 | 層を通る値と勾配の大きさを保つ初期値の決め方 |
| 2011 | ReLU の普及 | 深いネットワークでも勾配が届きやすい活性化関数 |
| 2015 | He の初期化・バッチ正規化・ResNet | 100 層を超えるネットワークも学習できるようになる |
| 2015〜2017 | TensorFlow・PyTorch | 自動微分を備えたライブラリで、逆伝播を手で書かなくてよくなる |
まとめ
- 学習に必要なのは、損失を各重みで微分した勾配。重みを 1 個ずつ動かす数値微分では、重みが多いと計算が終わらない
- 誤差逆伝播法は、計算を小さな部品に分け、連鎖律で出力側から「届いた勾配 × 部品の微分」を前へ渡していく
- 足し算は勾配をそのまま配り、掛け算は相手の入力の値を掛けて渡す
- 自動微分は、計算の履歴を覚えておき、逆の順にたどって勾配を求める。PyTorch の
backward()もこのしくみ - 逆伝播を自分で書いたら、数値微分と比べる勾配チェックで確かめられる
- 活性化関数がないと、層を重ねても 1 本の直線でしか分けられず、XOR も解けない
- 重みを同じ値で初期化すると、ニューロンが同じ動きをくり返して学習できない(対称性の問題)
- シグモイドを何層も重ねると勾配が消えていく。ReLU・初期値の工夫・正規化・残差接続で対策する
参考
- F. Rosenblatt, "The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain", Psychological Review 1958
- M. Minsky and S. Papert, "Perceptrons", MIT Press 1969
- D. E. Rumelhart, G. E. Hinton and R. J. Williams, "Learning representations by back-propagating errors", Nature 1986
- X. Glorot and Y. Bengio, "Understanding the difficulty of training deep feedforward neural networks", AISTATS 2010
- X. Glorot et al., "Deep Sparse Rectifier Neural Networks", AISTATS 2011
- K. He et al., "Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification", ICCV 2015(arXiv:1502.01852)
- S. Ioffe and C. Szegedy, "Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift", ICML 2015(arXiv:1502.03167)
- K. He et al., "Deep Residual Learning for Image Recognition", CVPR 2016(arXiv:1512.03385)
- A. G. Baydin et al., "Automatic Differentiation in Machine Learning: a Survey", JMLR 2018(arXiv:1502.05767)
- A. Paszke et al., "PyTorch: An Imperative Style, High-Performance Deep Learning Library", NeurIPS 2019(arXiv:1912.01703)