第15章 誤差逆伝播に現れる行列の転置

こんにちは。ゆうせいです。

第14章では、勾配がベクトルであり、それを使ってパラメータを更新することを確認しました。しかし、その勾配は、どうやって計算されるのでしょうか。パラメータが1億個あるモデルで、1億個の偏微分を、一つずつ計算していたのでは、いつまでたっても終わりません。この問題を解決するのが、誤差逆伝播法です。そして、その計算の中に、第10章で学んだ行列の転置が、繰り返し現れます。なぜ転置なのでしょうか。第15章では、微分の詳細には深入りせず、線形代数の観点から、この仕組みを見ていきます。

順方向と逆方向

順方向の計算

まず、これまで扱ってきた計算を確認します。

\mathbf{h} = W\mathbf{x}

入力 \mathbf{x} に重み行列を掛けて、出力 \mathbf{h} を得ます。

情報は、入力から出力へ、一方向に流れます。これを順伝播と呼びます。

逆方向の計算

学習では、これとは逆の流れが必要になります。

損失は、出力側で計算されます。その損失を減らすために、入力側にある各層のパラメータを、どう調整すべきかを知りたいのです。

そこで、出力側から入力側へ向かって、情報を逆向きに伝えていきます。これを逆伝播と呼びます。

何が伝わるのか

逆伝播で伝わるのは、この値が損失にどれだけ影響するかという情報です。

出力 \mathbf{h} に対する勾配、つまり \frac{\partial L}{\partial \mathbf{h}} が分かっているとします。

ここから、入力 \mathbf{x} に対する勾配 \frac{\partial L}{\partial \mathbf{x}} を求めたい。これが、逆伝播の1ステップです。

転置が現れる

結論を先に示す

\mathbf{h} = W\mathbf{x} という順伝播に対して、逆伝播は次のようになります。

\frac{\partial L}{\partial \mathbf{x}} = W^T \frac{\partial L}{\partial \mathbf{h}}

順伝播では W を掛けましたが、逆伝播では W^T を掛けます。

同じ行列の転置が使われる、という点が、この章の中心です。

形から確認する

なぜ転置が必要なのか、まず形の観点から確認します。

W の形を、(m, n) とします。

順伝播では、n次元の入力を受け取り、m次元の出力を返します。

(m, n) \times (n) \rightarrow (m)

逆伝播では、逆向きです。出力に対する勾配はm次元、入力に対する勾配はn次元です。

m次元のベクトルからn次元のベクトルを作るには、(n, m) の行列が必要です。

(n, m) \times (m) \rightarrow (n)

(n, m) は、W を転置した形です。

形を合わせるという要請から、転置が必要になることが分かります。

意味から確認する

形が合うだけでなく、意味の上でも転置が正しいことを確認します。

順伝播で、W のi行j列の要素は、入力のj番目の要素が、出力のi番目にどれだけ寄与するかを表しています。

h_i = W_{i1}x_1 + W_{i2}x_2 + \cdots + W_{in}x_n

逆伝播では、逆の問いを立てます。入力のj番目の要素は、損失にどれだけ影響するか。

入力のj番目は、出力のすべての要素に影響しています。その影響の強さは、W のj列目に並んでいます。

したがって、入力のj番目に対する勾配は、W のj列目と、出力に対する勾配との内積になります。

\frac{\partial L}{\partial x_j} = W_{1j}\frac{\partial L}{\partial h_1} + W_{2j}\frac{\partial L}{\partial h_2} + \cdots + W_{mj}\frac{\partial L}{\partial h_m}

列との内積を取るということは、転置した行列の行との内積を取ることと同じです。

第6章で学んだ内積が、ここでも中心にあります。

影響が集まってくるという見方

もう一つの見方を示します。

順伝播では、入力の各要素が、出力の各要素へ散らばっていきます。1つの入力が、すべての出力に影響します。

逆伝播では、逆です。1つの入力に対する影響が、すべての出力から集まってきます。

散らばるときに使った重みと、集まるときに使う重みは、同じものです。ただし、参照する方向が逆になります。行と列が入れ替わるのです。

これが、転置が現れる理由です。

重みに対する勾配

入力に対する勾配だけでなく、重み自体に対する勾配も必要です。パラメータを更新するためです。

計算式

\mathbf{h} = W\mathbf{x} に対して、重みの勾配は次のようになります。

\frac{\partial L}{\partial W} = \frac{\partial L}{\partial \mathbf{h}} \mathbf{x}^T

ここでは、二つのベクトルの積によって、行列が作られています。

外積という演算

この計算を、外積と呼びます。第6章で扱った内積とは、異なる演算です。

内積は、二つのベクトルからスカラーを作ります。

外積は、二つのベクトルから行列を作ります。

\mathbf{a} がm次元、\mathbf{b} がn次元のとき、次のようになります。

\mathbf{a}\mathbf{b}^T = \begin{pmatrix} a_1b_1 & a_1b_2 & a_1b_3 \\ a_2b_1 & a_2b_2 & a_2b_3 \\ a_3b_1 & a_3b_2 & a_3b_3 \end{pmatrix}

上の例は3次元どうしの場合です。一般には、形は (m, n) になります。

m次元の縦ベクトルと、n次元の横ベクトルを掛けたと考えれば、第9章の行列積の規則がそのまま当てはまります。

(m, 1) \times (1, n) \rightarrow (m, n)

形の確認

重みの勾配は、重み自体と同じ形でなければなりません。第14章で確認したとおりです。

\frac{\partial L}{\partial \mathbf{h}} はm次元、\mathbf{x} はn次元です。

外積を取ると、(m, n) の行列になります。W と同じ形です。

形が正しく一致することが確認できます。

意味の確認

W_{ij} に対する勾配は、次のようになります。

\frac{\partial L}{\partial W_{ij}} = \frac{\partial L}{\partial h_i} \times x_j

これは、直感的にも納得できます。

W_{ij} は、x_j に掛けられて h_i に寄与しています。

したがって、W_{ij} を少し変えたときの影響は、入力 x_j の大きさと、出力 h_i が損失に与える影響の、両方に比例します。

入力が0であれば、その重みを変えても何も起きません。実際、x_j = 0 なら勾配も0になります。

順伝播の値を保存しておく必要がある

ここで、実装上の重要な帰結があります。

重みの勾配を計算するには、順伝播のときの入力 \mathbf{x} が必要です。

したがって、順伝播の計算中に、各層の入力を保存しておかなければなりません。

メモリへの影響

第14章で、学習にはパラメータの数倍のメモリが必要だと述べました。

その理由の一つが、これです。

各層の中間出力を、すべて保存しておく必要があります。層が深いほど、保存すべき値が増えます。

バッチサイズが大きいほど、保存すべき値も比例して増えます。

学習時にメモリ不足になる場合、バッチサイズを減らすことが有効なのは、この中間出力の保存量が減るためです。

勾配チェックポインティング

メモリを節約する手法として、勾配チェックポインティングというものがあります。

すべての中間出力を保存するのではなく、一部だけを保存しておき、逆伝播のときに必要な値を再計算する方法です。

メモリは節約できますが、計算時間は増えます。メモリと計算時間のトレードオフです。

連鎖律と層の連結

層を遡る

複数の層がある場合、逆伝播は層を順に遡っていきます。

三層のネットワークを考えます。

\mathbf{h}_1 = W_1\mathbf{x}

\mathbf{h}_2 = W_2\mathbf{h}_1

\mathbf{y} = W_3\mathbf{h}_2

損失 L は、\mathbf{y} から計算されます。

逆伝播は、次の順で進みます。

まず、\frac{\partial L}{\partial \mathbf{y}} が求まります。

次に、\frac{\partial L}{\partial \mathbf{h}_2} = W_3^T \frac{\partial L}{\partial \mathbf{y}} を計算します。

次に、\frac{\partial L}{\partial \mathbf{h}_1} = W_2^T \frac{\partial L}{\partial \mathbf{h}_2} を計算します。

最後に、\frac{\partial L}{\partial \mathbf{x}} = W_1^T \frac{\partial L}{\partial \mathbf{h}_1} を計算します。

各層で、対応する重み行列の転置を掛けていくだけです。

順伝播との対称性

順伝播では、W_1W_2W_3 の順に掛けました。

逆伝播では、W_3^TW_2^TW_1^T の順に掛けます。

順序が逆になり、それぞれが転置されています。

第10章で扱った性質を思い出してください。

(AB)^T = B^TA^T

積の転置は、順序が逆になります。この性質が、そのまま現れています。

活性化関数の扱い

実際には、層の間に活性化関数が挟まります。

\mathbf{h} = f(W\mathbf{x})

この場合、逆伝播では、活性化関数の微分を掛ける処理が追加されます。

\frac{\partial L}{\partial \mathbf{x}} = W^T \left( \frac{\partial L}{\partial \mathbf{h}} \odot f'(W\mathbf{x}) \right)

\odot は、第6章で触れたアダマール積、つまり要素ごとの掛け算です。

活性化関数は要素ごとに適用されるため、その微分も要素ごとに掛けられます。行列積ではありません。

つまり、逆伝播の計算は、行列積とアダマール積の組み合わせで構成されています。

勾配消失を線形代数で理解する

第14章で触れた勾配消失を、この章の内容から考えます。

掛け算が繰り返される

逆伝播では、層を遡るたびに、転置した重み行列を掛けます。

12層あれば、12回の掛け算が行われます。

さらに、活性化関数の微分も掛けられます。

縮小が繰り返されると

もし、各層で値が縮小されるなら、それが繰り返されて、入力に近い層では勾配がほぼ0になります。

たとえば、各層で0.5倍になるとすれば、12層後には次のようになります。

0.5^{12} \approx 0.000244

元の0.02パーセント程度です。実質的に、学習が進みません。

なぜ縮小が起きるのか

活性化関数の微分が、原因の一つです。

シグモイド関数の微分は、最大でも0.25です。したがって、層を通るたびに、少なくとも4分の1に縮小されます。

ReLUの微分は、正の領域では1です。この点が、ReLUが広く使われる理由の一つになっています。

重み行列そのものも、要因になります。値が小さい行列を掛け続ければ、縮小が起きます。

残差接続の効果

第5章で扱った残差接続を、ここで再考します。

\mathbf{y} = \mathbf{x} + F(\mathbf{x})

この式を微分すると、次のようになります。

\frac{\partial \mathbf{y}}{\partial \mathbf{x}} = I + \frac{\partial F(\mathbf{x})}{\partial \mathbf{x}}

I は単位行列です。

重要なのは、単位行列の項が必ず存在することです。

\frac{\partial F}{\partial \mathbf{x}} がどれだけ小さくなっても、単位行列の分が残ります。

単位行列を掛けても、値は変わりません。したがって、勾配が減衰せずに、入力側まで届きます。

残差接続が勾配の高速道路と呼ばれる理由が、この式から分かります。単純な足し算が、微分すると単位行列を生み、それが勾配を守るのです。

バッチ処理での逆伝播

第9章で扱ったバッチ処理では、逆伝播もまとめて計算されます。

形の確認

順伝播は、次のように書けました。

H = XW^T

X の形は (batch_size, input_dim)H の形は (batch_size, output_dim) です。

逆伝播では、次のようになります。

入力に対する勾配は、\frac{\partial L}{\partial X} = \frac{\partial L}{\partial H} W です。

重みに対する勾配は、\frac{\partial L}{\partial W} = \left(\frac{\partial L}{\partial H}\right)^T X です。

バッチ全体で足し合わされる

重みに対する勾配では、行列積によって、バッチ内のすべてのデータの寄与が自動的に足し合わされます。

(output_dim, batch_size) \times (batch_size, input_dim) \rightarrow (output_dim, input_dim)

バッチサイズの軸が、掛け算によって消えています。これが、各データからの寄与を合計する操作にあたります。

第14章で勾配を平均すると述べましたが、実装上は、この合計をバッチサイズで割ることで平均が得られます。

Pythonで確認する

import numpy as np

np.random.seed(0)

print("=== 順伝播と逆伝播の形 ===")
m, n = 4, 3   # 出力次元, 入力次元
W = np.random.randn(m, n)
x = np.random.randn(n)

h = W @ x
print(f"W の形: {W.shape}")
print(f"x の形: {x.shape}")
print(f"h = Wx の形: {h.shape}")

grad_h = np.random.randn(m)   # 出力に対する勾配(上流から来る)
grad_x = W.T @ grad_h

print(f"\ngrad_h の形: {grad_h.shape}")
print(f"W.T の形: {W.T.shape}")
print(f"grad_x = W.T @ grad_h の形: {grad_x.shape}")
print("入力と同じ形になっています。")

print("\n=== 重みに対する勾配(外積)===")
grad_W = np.outer(grad_h, x)
print(f"grad_h の形: {grad_h.shape}")
print(f"x の形: {x.shape}")
print(f"外積の形: {grad_W.shape}")
print(f"W と同じ形か: {grad_W.shape == W.shape}")

print("\n=== 外積の中身を確認 ===")
print(f"grad_h = {grad_h.round(4)}")
print(f"x = {x.round(4)}")
print(f"\ngrad_W:")
print(grad_W.round(4))
print(f"\ngrad_W[0,0] = grad_h[0] * x[0] = {grad_h[0]:.4f} * {x[0]:.4f} = {grad_h[0]*x[0]:.4f}")
print(f"grad_W[2,1] = grad_h[2] * x[1] = {grad_h[2]:.4f} * {x[1]:.4f} = {grad_h[2]*x[1]:.4f}")

print("\n=== 入力が0なら勾配も0 ===")
x_zero = np.array([1.0, 0.0, 2.0])
grad_W_zero = np.outer(grad_h, x_zero)
print(f"x = {x_zero}")
print(f"grad_W の2列目(x[1]=0に対応): {grad_W_zero[:, 1]}")
print("入力が0の成分に対応する重みの勾配は、すべて0になります。")

print("\n=== 数値微分で検証 ===")
def forward_and_loss(W, x, target):
    h = W @ x
    return ((h - target) ** 2).sum()

target = np.random.randn(m)
W_test = np.random.randn(m, n)
x_test = np.random.randn(n)

h_test = W_test @ x_test
grad_h_analytic = 2 * (h_test - target)
grad_W_analytic = np.outer(grad_h_analytic, x_test)
grad_x_analytic = W_test.T @ grad_h_analytic

eps = 1e-6
grad_W_numeric = np.zeros_like(W_test)
for i in range(m):
    for j in range(n):
        W_plus = W_test.copy()
        W_plus[i, j] += eps
        W_minus = W_test.copy()
        W_minus[i, j] -= eps
        grad_W_numeric[i, j] = (forward_and_loss(W_plus, x_test, target) - 
                                forward_and_loss(W_minus, x_test, target)) / (2 * eps)

grad_x_numeric = np.zeros_like(x_test)
for j in range(n):
    x_plus = x_test.copy()
    x_plus[j] += eps
    x_minus = x_test.copy()
    x_minus[j] -= eps
    grad_x_numeric[j] = (forward_and_loss(W_test, x_plus, target) - 
                         forward_and_loss(W_test, x_minus, target)) / (2 * eps)

print(f"重みの勾配が一致するか: {np.allclose(grad_W_analytic, grad_W_numeric, atol=1e-4)}")
print(f"入力の勾配が一致するか: {np.allclose(grad_x_analytic, grad_x_numeric, atol=1e-4)}")
print("転置と外積を使った計算が、数値微分と一致することが確認できました。")

print("\n=== 多層の逆伝播 ===")
np.random.seed(42)
W1 = np.random.randn(64, 32) * 0.1
W2 = np.random.randn(128, 64) * 0.1
W3 = np.random.randn(16, 128) * 0.1
x = np.random.randn(32)

h1 = W1 @ x
h2 = W2 @ h1
y = W3 @ h2

print("順伝播:")
print(f"  x {x.shape} → h1 {h1.shape} → h2 {h2.shape} → y {y.shape}")

grad_y = np.random.randn(16)
grad_h2 = W3.T @ grad_y
grad_h1 = W2.T @ grad_h2
grad_x = W1.T @ grad_h1

print("\n逆伝播:")
print(f"  grad_y {grad_y.shape} → grad_h2 {grad_h2.shape} → "
      f"grad_h1 {grad_h1.shape} → grad_x {grad_x.shape}")
print("\n順伝播とちょうど逆向きに、形が戻っていきます。")

print("\n=== 積の転置の性質 ===")
combined = W3 @ W2 @ W1
combined_T = combined.T
transposed_product = W1.T @ W2.T @ W3.T
print(f"(W3 W2 W1)^T の形: {combined_T.shape}")
print(f"W1^T W2^T W3^T の形: {transposed_product.shape}")
print(f"一致するか: {np.allclose(combined_T, transposed_product)}")

print("\n=== 層を増やすと勾配が消える ===")
np.random.seed(0)
n_layers_list = [5, 10, 20, 40]
for n_layers in n_layers_list:
    Ws = [np.random.randn(50, 50) * 0.1 for _ in range(n_layers)]
    grad = np.ones(50)
    for W_ in reversed(Ws):
        grad = W_.T @ grad
    print(f"{n_layers:>3}層: 勾配の長さ {np.linalg.norm(grad):.6e}")

print("\n=== 残差接続があると勾配が保たれる ===")
np.random.seed(0)
for n_layers in n_layers_list:
    Ws = [np.random.randn(50, 50) * 0.1 for _ in range(n_layers)]
    grad = np.ones(50)
    for W_ in reversed(Ws):
        # y = x + Wx の微分は I + W なので、逆伝播は grad + W.T @ grad
        grad = grad + W_.T @ grad
    print(f"{n_layers:>3}層: 勾配の長さ {np.linalg.norm(grad):.6e}")

print("\n単位行列の項があるため、勾配が消えません。")

print("\n=== 活性化関数による勾配の減衰の違い ===")
def relu(x):
    return np.maximum(0, x)

def relu_grad(x):
    return (x > 0).astype(float)

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_grad(x):
    s = sigmoid(x)
    return s * (1 - s)

dim = 50
n_layers = 30

for act_name, act, act_grad in [("シグモイド", sigmoid, sigmoid_grad),
                                  ("ReLU", relu, relu_grad)]:
    np.random.seed(0)
    Ws = [np.random.randn(dim, dim) * 0.5 for _ in range(n_layers)]
    
    x = np.random.randn(dim)
    zs = []
    h = x
    for W_ in Ws:
        z = W_ @ h
        zs.append(z)
        h = act(z)
    
    grad = np.ones(dim)
    norms = [np.linalg.norm(grad)]
    for i in reversed(range(n_layers)):
        grad = grad * act_grad(zs[i])
        grad = Ws[i].T @ grad
        norms.append(np.linalg.norm(grad))
    
    print(f"\n{act_name}:")
    for layer_idx in [0, 5, 10, 20, 30]:
        print(f"  {layer_idx:>2}層遡った時点の勾配の長さ: {norms[layer_idx]:.6e}")

print("\nシグモイドは微分の最大値が0.25のため、急速に減衰します。")
print("ReLUは正の領域で微分が1のため、減衰が緩やかです。")

print("\n=== バッチ処理での逆伝播 ===")
np.random.seed(1)
batch_size, d_in, d_out = 8, 32, 16
X = np.random.randn(batch_size, d_in)
W = np.random.randn(d_out, d_in) * 0.1

H = X @ W.T
print(f"順伝播: X{X.shape} @ W.T{W.T.shape} = H{H.shape}")

grad_H = np.random.randn(batch_size, d_out)
grad_X = grad_H @ W
grad_W = grad_H.T @ X

print(f"\n逆伝播:")
print(f"  grad_X = grad_H{grad_H.shape} @ W{W.shape} = {grad_X.shape}")
print(f"  grad_W = grad_H.T{grad_H.T.shape} @ X{X.shape} = {grad_W.shape}")
print(f"\ngrad_W が W と同じ形か: {grad_W.shape == W.shape}")

grad_W_manual = np.zeros_like(W)
for i in range(batch_size):
    grad_W_manual += np.outer(grad_H[i], X[i])
print(f"1件ずつ計算して足した結果と一致するか: {np.allclose(grad_W, grad_W_manual)}")
print("バッチサイズの軸が、行列積によって消えています。")

初学者がつまずきやすい点

つまずき1:転置をなんとなく形を合わせるためとだけ理解する

形を合わせるために必要である、というのは正しいのですが、それだけではありません。

意味の上でも、転置が正しいのです。

順伝播では、1つの入力が複数の出力に散らばります。逆伝播では、1つの入力への影響が複数の出力から集まってきます。参照する方向が逆になるため、行と列が入れ替わります。

この理解があると、単なる機械的な操作ではなく、必然的な帰結として受け取れます。

つまずき2:内積と外積を混同する

内積は、二つのベクトルからスカラーを作ります。

外積は、二つのベクトルから行列を作ります。

逆伝播では、入力に対する勾配の計算には行列積が使われ、重みに対する勾配の計算には外積が使われます。

形を確認すれば、どちらが必要かは判断できます。

つまずき3:順伝播の値を保存する必要性を見落とす

重みの勾配を計算するには、順伝播時の入力が必要です。

したがって、順伝播が終わった時点で入力を捨ててしまうと、逆伝播ができません。

PyTorchなどのフレームワークは、自動的にこれを保存しています。この保存が、メモリを消費する主な要因の一つです。

演習

演習1:形を答える

W の形が (128, 64) であるとき、次の形を答えてください。

順伝播の入力 \mathbf{x}

順伝播の出力 \mathbf{h}

出力に対する勾配 \frac{\partial L}{\partial \mathbf{h}}

入力に対する勾配 \frac{\partial L}{\partial \mathbf{x}}

重みに対する勾配 \frac{\partial L}{\partial W}

演習2:外積を計算する

次の二つのベクトルの外積を計算してください。

\mathbf{a} = [2, -1, 3]

\mathbf{b} = [1, 4]

\mathbf{a}\mathbf{b}^T の形と、各要素を求めてください。

演習3:逆伝播を手計算する

W = \begin{pmatrix} 1 & 2 \\ 0 & 3 \end{pmatrix}\mathbf{x} = [1, 2] とします。

順伝播 \mathbf{h} = W\mathbf{x} を計算してください。

出力に対する勾配が \frac{\partial L}{\partial \mathbf{h}} = [1, -1] であるとき、次を計算してください。

入力に対する勾配 \frac{\partial L}{\partial \mathbf{x}} = W^T \frac{\partial L}{\partial \mathbf{h}}

重みに対する勾配 \frac{\partial L}{\partial W} = \frac{\partial L}{\partial \mathbf{h}} \mathbf{x}^T

演習の解答例と解説

演習1の解答は、次のとおりです。

順伝播の入力 \mathbf{x} は、64次元です。W の列数と一致します。

順伝播の出力 \mathbf{h} は、128次元です。W の行数と一致します。

出力に対する勾配は、128次元です。出力と同じ形です。

入力に対する勾配は、64次元です。入力と同じ形です。

重みに対する勾配は、(128, 64) です。重みと同じ形です。

勾配は、対象と同じ形を持つ。この原則が、すべてに当てはまっています。

演習2の解答は、次のとおりです。

\mathbf{a} が3次元、\mathbf{b} が2次元ですから、外積は (3, 2) の行列になります。

\mathbf{a}\mathbf{b}^T = \begin{pmatrix} 2 & 8 \\ -1 & -4 \\ 3 & 12 \end{pmatrix}

各要素は、\mathbf{a} のi番目と \mathbf{b} のj番目の積です。

たとえば、1行1列は 2 \times 1 = 2 、3行2列は 3 \times 4 = 12 です。

演習3の解答は、次のとおりです。

順伝播を計算します。

\mathbf{h} = \begin{pmatrix} 1 & 2 \\ 0 & 3 \end{pmatrix}\begin{pmatrix} 1 \\ 2 \end{pmatrix} = \begin{pmatrix} 5 \\ 6 \end{pmatrix}

入力に対する勾配を計算します。まず転置を求めます。

W^T = \begin{pmatrix} 1 & 0 \\ 2 & 3 \end{pmatrix}

これを勾配に掛けます。

W^T\begin{pmatrix} 1 \\ -1 \end{pmatrix} = \begin{pmatrix} 1 \times 1 + 0 \times (-1) \\ 2 \times 1 + 3 \times (-1) \end{pmatrix} = \begin{pmatrix} 1 \\ -1 \end{pmatrix}

重みに対する勾配を計算します。外積です。

\begin{pmatrix} 1 \\ -1 \end{pmatrix}\begin{pmatrix} 1 & 2 \end{pmatrix} = \begin{pmatrix} 1 & 2 \\ -1 & -2 \end{pmatrix}

W と同じ (2, 2) の形になっています。

講師向けの補足

この章では、微分の詳細に立ち入らないことが重要です。

目的は、なぜ転置が現れるのかを線形代数の観点から理解してもらうことです。連鎖律の厳密な導出は、別の機会に譲ってください。

効果的な説明は、形の観点からのアプローチです。順伝播で (m, n) を使ったなら、逆向きには (n, m) が必要という説明は、直感的に納得されます。

そのうえで、意味の観点、つまり散らばると集まるの対比を示すと、理解が深まります。

勾配消失の観察は、必ず実施してください。シグモイドとReLUで桁違いに異なる結果が出るため、活性化関数の選択が学習に与える影響が実感されます。

残差接続の微分が単位行列を含むという点も、必ず触れてください。第5章で扱った単純な足し算が、なぜ重要なのかが、ここで完結します。

理解度を確認する問い

第15章の内容を、次の問いで確認してみてください。

順伝播で W を使ったとき、逆伝播で W^T を使う理由を、形の観点と意味の観点の両方から説明してください。

内積と外積の違いを、結果の形という観点から説明してください。

重みに対する勾配を計算するために、順伝播時の入力を保存しておく必要があるのは、なぜでしょうか。

残差接続の微分が単位行列を含むことが、なぜ勾配消失を防ぐのでしょうか。

シグモイド関数よりReLUのほうが、深いネットワークで有利なのはなぜでしょうか。

まとめ

学習では、出力側から入力側へ向かって、勾配の情報を伝えていきます。これを逆伝播と呼びます。

順伝播が \mathbf{h} = W\mathbf{x} であるとき、逆伝播は次のようになります。

\frac{\partial L}{\partial \mathbf{x}} = W^T \frac{\partial L}{\partial \mathbf{h}}

転置が現れる理由は、二つの観点から説明できます。形の観点では、逆向きの変換には転置した形の行列が必要だからです。意味の観点では、順伝播では1つの入力が複数の出力に散らばるのに対し、逆伝播では1つの入力への影響が複数の出力から集まってくるため、参照する方向が逆になるからです。

重みに対する勾配は、外積によって計算されます。

\frac{\partial L}{\partial W} = \frac{\partial L}{\partial \mathbf{h}} \mathbf{x}^T

外積は、二つのベクトルから行列を作る演算です。結果は、重み行列と同じ形になります。

この計算には順伝播時の入力が必要なため、各層の中間出力を保存しておく必要があります。これが、学習時に大量のメモリを消費する要因の一つです。

多層のネットワークでは、逆伝播が層を順に遡ります。順伝播で W_1, W_2, W_3 の順に掛けたなら、逆伝播では W_3^T, W_2^T, W_1^T の順に掛けます。これは、第10章で学んだ (AB)^T = B^TA^T という性質の現れです。

層を遡るたびに掛け算が繰り返されるため、各層で値が縮小されると、勾配消失が起こります。残差接続は、微分すると単位行列の項が必ず残るため、この減衰を防ぎます。

次の第16章、この連載の最終章では、なぜGPUで計算が速くなるのかを扱います。行列積という形に整理されていることが、並列処理を可能にしています。そして、この連載の次に何を学ぶべきかも示します。

第16章に進む前に、活性化関数による勾配の減衰の違いを確認しておいてください。桁違いの差が、活性化関数選択の重要性を示しています。

セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
海外放浪の末、2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。

学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。