第14章 勾配ベクトルとは何か

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

第13章までで、入力から出力へ向かう順方向の計算を扱ってきました。第14章からは、第IV部として、学習の仕組みに現れる線形代数を扱います。ここまでの説明では、重み行列の値は学習によって決まる、と述べてきました。では、その学習とは、具体的に何をしているのでしょうか。答えは、パラメータを少しずつ調整することです。そして、どちらの方向に、どれだけ調整すればよいかを教えてくれるのが、勾配です。この勾配は、実はベクトルです。この章では、勾配がベクトルであることの意味と、それが学習でどう使われるのかを確認します。微分の詳細には深入りせず、線形代数の観点から見ていきます。

学習とは何をしているのか

損失関数

まず、学習の目標を確認します。

モデルの予測が、正解からどれだけずれているかを表す指標を、損失関数と呼びます。

たとえば、予測値と正解値の差の2乗を使う方法があります。

L = (y_{pred} - y_{true})^2

損失が小さいほど、良い予測です。

重要な点は、損失が1個の数値、つまりスカラーであることです。

複数のデータ、複数の出力があっても、最終的には1個の数値にまとめられます。だからこそ、これを小さくするという明確な目標が立てられます。

損失はパラメータの関数である

ここが、理解の鍵です。

損失の値は、モデルのパラメータによって決まります。

パラメータを変えれば予測が変わり、予測が変われば損失が変わります。

L = L(w_1, w_2, \ldots, w_n)

パラメータが100万個あれば、100万個の変数を持つ関数です。

学習とは、この関数を最小にするパラメータの組み合わせを探す作業です。

1変数の場合から考える

いきなり100万変数は扱えません。まず、変数が1個の場合から考えます。

傾きが教えてくれること

パラメータが w 一つだけだとします。

損失 L(w) を、w で微分した値を、傾きと呼びます。

\frac{dL}{dw}

この値が教えてくれることは、次のとおりです。

傾きがプラスであれば、w を増やすと損失が増えます。したがって、w を減らすべきです。

傾きがマイナスであれば、w を増やすと損失が減ります。したがって、w を増やすべきです。

傾きが0であれば、その付近では損失が変化しません。

更新の規則

以上から、次の更新規則が導かれます。

w_{new} = w_{old} - \eta \frac{dL}{dw}

\eta は学習率です。どれだけ大きく動かすかを決める、小さな正の値です。

傾きがプラスなら、マイナスを掛けることで w が減ります。

傾きがマイナスなら、マイナスを掛けることで w が増えます。

いずれの場合も、損失が減る方向に動きます。

傾きの大きさの意味

傾きの絶対値が大きいということは、そのパラメータを少し動かすだけで、損失が大きく変わるということです。

したがって、大きく動かすべきです。

更新規則では、傾きそのものを掛けているため、傾きが大きいほど更新量も大きくなります。この性質が、自動的に組み込まれています。

多変数になると勾配ベクトルになる

偏微分

パラメータが複数ある場合、それぞれについて微分を考えます。

他のパラメータを固定して、このパラメータだけを動かしたら、損失はどう変わるかを計算します。

これを偏微分と呼び、次のように書きます。

\frac{\partial L}{\partial w_1}

丸みを帯びた記号を使うのが、偏微分の記法です。

勾配ベクトル

すべてのパラメータについての偏微分を、並べたものが勾配ベクトルです。

\nabla L = \begin{pmatrix} \frac{\partial L}{\partial w_1} \\ \frac{\partial L}{\partial w_2} \\ \vdots \\ \frac{\partial L}{\partial w_n} \end{pmatrix}

記号 \nabla は、ナブラと読みます。

重要な点は、勾配ベクトルの次元数が、パラメータの数と一致することです。

パラメータが100万個あれば、勾配ベクトルも100万次元です。

なぜベクトルとして扱うのか

各パラメータの偏微分を、バラバラに管理することもできます。しかし、まとめてベクトルとして扱うことには、大きな利点があります。

第一に、更新規則が簡潔に書けます。

$latex \mathbf{w}{new} = \mathbf{w}{old} - \eta \nabla L$

第5章で学んだ、ベクトルのスカラー倍と引き算です。

第二に、計算がまとめて実行できます。100万個の更新を、1回のベクトル演算で処理できます。

第三に、勾配ベクトルの長さといった、全体的な性質を扱えるようになります。

勾配ベクトルの方向が意味すること

最も急な方向

勾配ベクトルには、重要な性質があります。

勾配ベクトルの方向は、損失が最も急激に増加する方向です。

したがって、その反対方向、つまりマイナスをつけた方向が、損失が最も急激に減少する方向になります。

更新規則でマイナスをつけているのは、この理由によります。

山下りのたとえ

よく使われる比喩を紹介します。

霧のかかった山の中にいて、麓を目指しているとします。周囲が見えないため、遠くの様子は分かりません。

分かるのは、足元の傾きだけです。

そこで、最も急な下り方向に、一歩進みます。そこで再び足元の傾きを調べ、また最も急な下り方向に進みます。

これを繰り返せば、いずれ低い場所にたどり着きます。

勾配降下法と呼ばれる手法は、まさにこの戦略です。

この比喩の限界

ただし、この比喩には注意点があります。

山下りの比喩は、2次元や3次元を想定しています。実際のパラメータ空間は、100万次元です。

高次元空間では、直感が通用しない現象が起こります。

たとえば、2次元ではくぼみに落ちて抜け出せなくなる状況が想像されますが、高次元では、あらゆる方向で上りになっている点は、かなり稀です。多くの方向のうち、どれか一つでも下りがあれば、そちらに抜けられるためです。

高次元では、局所的なくぼみより、鞍点と呼ばれる、ある方向では下りで別の方向では上りという地点のほうが、問題になりやすいと言われています。

勾配ベクトルの長さ

第2章で、ベクトルの長さを扱いました。勾配ベクトルの長さにも、意味があります。

|\nabla L| = \sqrt{\left(\frac{\partial L}{\partial w_1}\right)^2 + \cdots + \left(\frac{\partial L}{\partial w_n}\right)^2}

長さが表すもの

勾配ベクトルの長さは、損失の変化の急峻さを表します。

長さが大きければ、パラメータを少し動かすだけで損失が大きく変わります。急な斜面にいる状態です。

長さが0に近ければ、平坦な場所にいます。損失の最小値付近か、あるいは学習が停滞している状態です。

勾配消失と勾配爆発

実務では、勾配ベクトルの長さが問題になることがあります。

勾配消失とは、長さが極端に小さくなり、学習が進まなくなる現象です。

深い層を持つモデルで、入力に近い層ほど起こりやすくなります。

勾配爆発とは、逆に長さが極端に大きくなり、パラメータが暴れて学習が破綻する現象です。

勾配クリッピング

勾配爆発への対策として、勾配クリッピングという手法があります。

勾配ベクトルの長さが、あらかじめ決めた閾値を超えたら、長さだけを縮小します。

\nabla L_{clipped} = \nabla L \times \frac{c}{|\nabla L|}

c が閾値です。

これは、第5章で扱ったスカラー倍です。方向は変えず、長さだけを調整します。

方向の情報は保ちつつ、更新量が大きくなりすぎるのを防ぐ、という仕組みです。

重み行列に対する勾配

ここまで、パラメータをベクトルとして扱ってきました。しかし、実際のニューラルネットワークでは、パラメータの多くは行列の形をしています。

形は元のパラメータと同じ

重み行列 W が768行3072列だとします。

この行列に対する勾配は、同じく768行3072列の行列になります。

\frac{\partial L}{\partial W}

各要素が、対応する重みについての偏微分です。

$latex \left(\frac{\partial L}{\partial W}\right){ij} = \frac{\partial L}{\partial W{ij}}$

更新も要素ごと

更新規則も、そのまま適用できます。

W_{new} = W_{old} - \eta \frac{\partial L}{\partial W}

行列どうしの引き算と、行列のスカラー倍です。第5章で扱った演算が、行列に拡張されただけです。

メモリへの影響

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

勾配は、パラメータと同じ形と大きさを持ちます。

パラメータが1億2400万個あれば、勾配も1億2400万個です。

さらに、多くの最適化手法では、過去の勾配の情報を保持します。Adamと呼ばれる代表的な手法では、パラメータ1個につき、追加で2個の値を保持します。

したがって、学習時に必要なメモリは、パラメータだけの場合の数倍になります。

float32で保持する場合、次のようになります。

パラメータが約496メガバイト。

勾配が約496メガバイト。

Adamの状態が約992メガバイト。

合計で約2ギガバイトです。これに加えて、各層の中間出力も保持する必要があります。

推論だけなら動くモデルが、学習させようとするとメモリ不足になる。この現象の理由が、ここにあります。

ミニバッチと勾配の平均

第9章で、複数のデータをまとめて処理することを扱いました。勾配についても、同様です。

各データから勾配が得られる

1件のデータについて損失を計算すれば、そのデータに対する勾配が得られます。

32件のデータがあれば、32本の勾配ベクトルが得られます。

平均を取る

これらの平均を取って、更新に使います。

\nabla L_{mean} = \frac{1}{32}\sum_{i=1}^{32} \nabla L_i

第5章で扱った、ベクトルの足し算とスカラー倍の組み合わせです。

平均を取る理由

1件のデータだけから勾配を計算すると、そのデータの特殊性に強く影響されます。

たまたま例外的なデータであれば、誤った方向にパラメータが動きます。

複数のデータの勾配を平均すると、個々のばらつきが打ち消され、より信頼できる更新方向が得られます。

一方で、平均を取りすぎると、更新が滑らかになりすぎるという側面もあります。適度なばらつきが、学習に有利に働く場合もあります。

このバランスを調整するのが、バッチサイズという設定です。

Pythonで確認する

import numpy as np

print("=== 1変数の場合:傾きが方向を教える ===")

def loss_1d(w):
    return (w - 3) ** 2

def gradient_1d(w):
    return 2 * (w - 3)

print("損失関数: L(w) = (w - 3)^2  (最小値は w=3 のとき)")
print(f"\n{'w':>8} | {'損失':>10} | {'傾き':>10} | {'進むべき方向':>14}")
print("-" * 50)
for w in [0.0, 1.0, 3.0, 5.0, 7.0]:
    g = gradient_1d(w)
    direction = "増やす" if g < 0 else ("減らす" if g > 0 else "そのまま")
    print(f"{w:>8.1f} | {loss_1d(w):>10.2f} | {g:>10.2f} | {direction:>14}")

print("\n=== 勾配降下法で最小値を探す ===")
w = 0.0
lr = 0.1
print(f"初期値: w = {w}, 学習率 = {lr}")
print(f"\n{'ステップ':>8} | {'w':>10} | {'損失':>10} | {'勾配':>10}")
print("-" * 44)
for step in range(11):
    g = gradient_1d(w)
    if step % 2 == 0:
        print(f"{step:>8} | {w:>10.4f} | {loss_1d(w):>10.4f} | {g:>10.4f}")
    w = w - lr * g

print(f"\n最終的な w: {w:.6f}  (正解は 3.0)")

print("\n=== 多変数の場合:勾配ベクトル ===")

def loss_2d(w):
    return (w[0] - 2)**2 + 3 * (w[1] + 1)**2

def gradient_2d(w):
    return np.array([2 * (w[0] - 2), 6 * (w[1] + 1)])

w = np.array([0.0, 0.0])
print("損失関数: L = (w1-2)^2 + 3(w2+1)^2")
print(f"初期値: {w}")
print(f"勾配ベクトル: {gradient_2d(w)}")
print(f"勾配の長さ: {np.linalg.norm(gradient_2d(w)):.4f}")

print("\n=== 勾配降下法(2変数)===")
w = np.array([0.0, 0.0])
lr = 0.1
print(f"{'ステップ':>8} | {'w':>20} | {'損失':>10} | {'勾配の長さ':>12}")
print("-" * 58)
for step in range(21):
    g = gradient_2d(w)
    if step % 4 == 0:
        print(f"{step:>8} | {str(w.round(4)):>20} | {loss_2d(w):>10.4f} | {np.linalg.norm(g):>12.4f}")
    w = w - lr * g

print(f"\n最終的な w: {w.round(6)}  (正解は [2, -1])")
print("最小値に近づくほど、勾配の長さが0に近づいています。")

print("\n=== 学習率の影響 ===")
for lr in [0.01, 0.1, 0.3, 0.35]:
    w = np.array([0.0, 0.0])
    for _ in range(50):
        w = w - lr * gradient_2d(w)
    print(f"学習率 {lr:>5}: 50ステップ後の w = {w.round(4)}, 損失 = {loss_2d(w):.6f}")

print("\n学習率が大きすぎると、発散する場合があります。")

print("\n=== 勾配クリッピング ===")
np.random.seed(0)
gradients = [
    np.random.randn(5) * 0.1,
    np.random.randn(5) * 1.0,
    np.random.randn(5) * 10.0
]
threshold = 1.0

print(f"閾値: {threshold}")
print(f"\n{'元の長さ':>12} | {'調整後の長さ':>14} | {'処理':>10}")
print("-" * 42)
for g in gradients:
    norm = np.linalg.norm(g)
    if norm > threshold:
        g_clipped = g * (threshold / norm)
        action = "縮小"
    else:
        g_clipped = g
        action = "そのまま"
    print(f"{norm:>12.4f} | {np.linalg.norm(g_clipped):>14.4f} | {action:>10}")

print("\n方向は変えず、長さだけを制限しています。")

print("\n=== 重み行列に対する勾配 ===")
W = np.random.randn(4, 3)
grad_W = np.random.randn(4, 3) * 0.1

print(f"重み行列の形: {W.shape}")
print(f"勾配の形: {grad_W.shape}")
print("形が完全に一致しています。")

lr = 0.01
W_new = W - lr * grad_W
print(f"\n更新前の1行目: {W[0].round(4)}")
print(f"更新後の1行目: {W_new[0].round(4)}")
print(f"変化量: {(W_new - W)[0].round(6)}")

print("\n=== メモリ量の見積もり ===")
n_params = 124_000_000
bytes_per_param = 4

param_mem = n_params * bytes_per_param / 1024**3
grad_mem = param_mem
adam_mem = param_mem * 2

print(f"パラメータ数: {n_params:,}")
print(f"パラメータ: {param_mem:.2f} GB")
print(f"勾配: {grad_mem:.2f} GB")
print(f"Adamの状態(2種類): {adam_mem:.2f} GB")
print(f"合計: {param_mem + grad_mem + adam_mem:.2f} GB")
print("\n推論だけならパラメータ分で足りますが、学習には数倍必要です。")

print("\n=== バッチサイズと勾配のばらつき ===")
np.random.seed(42)
true_grad = np.array([1.0, -0.5, 0.2, 0.8, -0.3])
print(f"{'バッチサイズ':>12} | {'真の値との差の大きさ':>22}")
print("-" * 38)
for bs in [1, 4, 16, 64, 256]:
    errors = []
    for _ in range(100):
        grads = np.random.randn(bs, 5) * 0.5 + true_grad
        mean_grad = grads.mean(axis=0)
        errors.append(np.linalg.norm(mean_grad - true_grad))
    print(f"{bs:>12} | {np.mean(errors):>22.6f}")

print("\nバッチサイズが大きいほど、真の勾配に近づきます。")

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

つまずき1:勾配の符号を取り違える

勾配は、損失が増える方向を指します。

したがって、更新ではマイナスをつけます。

$latex \mathbf{w}{new} = \mathbf{w}{old} - \eta \nabla L$

プラスにすると、損失が増える方向に進んでしまいます。

つまずき2:勾配の形を誤解する

勾配は、パラメータと同じ形を持ちます。

重み行列が768行3072列なら、勾配も768行3072列です。

勾配は1個の数値と誤解する人がいますが、それは損失の値です。勾配は、パラメータの数だけ要素を持ちます。

つまずき3:山下りの比喩を過信する

2次元や3次元での直感は、高次元では通用しないことがあります。

くぼみに落ちて抜け出せなくなるという心配は、高次元ではあまり当てはまりません。

比喩は理解の助けになりますが、そのまま高次元に持ち込むと、誤った判断につながることがあります。

演習

演習1:傾きから方向を判断する

損失関数が L(w) = (w - 5)^2 のとき、次の各点で、w を増やすべきか減らすべきかを答えてください。

w = 2

w = 5

w = 8

微分は \frac{dL}{dw} = 2(w - 5) です。

演習2:勾配ベクトルを求める

損失関数が次の形だとします。

L(w_1, w_2) = w_1^2 + 4w_2^2

\mathbf{w} = [3, 1] のときの勾配ベクトルを求めてください。

偏微分は、\frac{\partial L}{\partial w_1} = 2w_1\frac{\partial L}{\partial w_2} = 8w_2 です。

また、学習率を0.1として、1回更新した後のパラメータを求めてください。

演習3:メモリ量を計算する

パラメータ数が70億個のモデルについて、次を計算してください。float32を前提とします。

パラメータのメモリ量

勾配のメモリ量

Adamの状態を含めた合計

演習4:勾配降下法を実装する

import numpy as np

def loss(w):
    """L(w) = (w1 - 1)^2 + 10*(w2 - 2)^2"""
    return (w[0] - 1)**2 + 10 * (w[1] - 2)**2

def gradient(w):
    return np.array([2 * (w[0] - 1), 20 * (w[1] - 2)])

print("=== 学習率による挙動の違い ===")
print("損失関数: L = (w1-1)^2 + 10(w2-2)^2")
print("最小値: w = [1, 2]\n")

for lr in [0.01, 0.05, 0.09, 0.11]:
    w = np.array([0.0, 0.0])
    history = []
    for step in range(100):
        w = w - lr * gradient(w)
        history.append(loss(w))
        if np.isnan(loss(w)) or loss(w) > 1e10:
            break
    
    status = "発散" if (np.isnan(history[-1]) or history[-1] > 1e10) else "収束"
    print(f"学習率 {lr:>5}: {step+1:>3}ステップで{status}, "
          f"最終的な w = {w.round(4)}, 損失 = {history[-1]:.6e}")

print("\n=== 勾配の長さの推移 ===")
w = np.array([0.0, 0.0])
lr = 0.05
print(f"{'ステップ':>8} | {'損失':>12} | {'勾配の長さ':>12}")
print("-" * 38)
for step in range(0, 101, 20):
    if step > 0:
        for _ in range(20):
            w = w - lr * gradient(w)
    g = gradient(w)
    print(f"{step:>8} | {loss(w):>12.6f} | {np.linalg.norm(g):>12.6f}")

print("\n最小値に近づくほど、勾配の長さが0に近づきます。")

演習の解答例と解説

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

w = 2 のとき、\frac{dL}{dw} = 2(2 - 5) = -6 です。マイナスなので、w を増やすべきです。

w = 5 のとき、\frac{dL}{dw} = 2(5 - 5) = 0 です。最小値にいるため、動かす必要はありません。

w = 8 のとき、\frac{dL}{dw} = 2(8 - 5) = 6 です。プラスなので、w を減らすべきです。

いずれの場合も、w = 5 に近づく方向が示されています。

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

勾配ベクトルを計算します。

\frac{\partial L}{\partial w_1} = 2 \times 3 = 6

\frac{\partial L}{\partial w_2} = 8 \times 1 = 8

勾配ベクトルは [6, 8] です。

更新後のパラメータを計算します。

[3, 1] - 0.1 \times [6, 8] = [3 - 0.6, 1 - 0.8] = [2.4, 0.2]

なお、勾配ベクトルの長さは次のようになります。

\sqrt{6^2 + 8^2} = \sqrt{36 + 64} = 10

第2章で扱った計算です。

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

パラメータのメモリ量を計算します。

7000000000 \times 4 = 28000000000

約28ギガバイト、正確には約26.1ギビバイトです。

勾配も同じ大きさですから、約28ギガバイトです。

Adamの状態は、パラメータ1個につき2個の値を保持するため、約56ギガバイトです。

合計は、約112ギガバイトです。

一般的なGPUの搭載メモリは、24ギガバイトから80ギガバイト程度です。したがって、70億パラメータのモデルを通常の方法で学習させるには、複数のGPUが必要になります。

推論だけであれば28ギガバイトで済むため、1台のGPUでも動作します。

動かせるが学習はできないという状況が生じる理由が、この計算から分かります。

演習4では、学習率が大きすぎると発散することが確認できます。

この損失関数では、w_2 の係数が10と大きいため、w_2 方向の勾配が急です。学習率が0.1を超えると、w_2 が振動して発散します。

また、最小値に近づくにつれて、勾配の長さが0に近づくことも確認できます。これは、平坦な場所に到達したことを意味します。

講師向けの補足

この章では、微分の計算そのものには深入りしないでください。

目的は、勾配がベクトルであるという点と、学習の計算も線形代数で記述されるという点を伝えることです。

微分の詳細に立ち入ると、線形代数の連載ではなく微分の連載になってしまいます。

効果的なのは、演習4のような実験です。学習率を変えたときの挙動の違いを、数値で見せてください。実務で学習率を調整する場面が必ずあるため、この経験は直接役立ちます。

また、メモリ量の計算は、必ず扱ってください。学習にはパラメータの数倍のメモリが必要という事実は、実務で必ず直面する制約です。

山下りの比喩については、便利である一方、高次元では通用しない面もある点を、必ず添えてください。

理解度を確認する問い

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

損失関数の値は、なぜスカラーでなければならないのでしょうか。

勾配ベクトルの次元数は、何と一致するでしょうか。

パラメータの更新規則で、勾配にマイナスをつける理由を説明してください。

勾配クリッピングは、勾配ベクトルの何を変え、何を変えないのでしょうか。

学習時に必要なメモリが、パラメータだけの場合の数倍になるのは、なぜでしょうか。

まとめ

学習とは、損失関数を最小にするパラメータを探す作業です。損失は1個の数値、つまりスカラーであり、パラメータを変数とする関数として捉えられます。

1変数の場合、微分した傾きが、パラメータをどちらに動かすべきかを教えてくれます。傾きがプラスなら減らし、マイナスなら増やせば、損失が減ります。

多変数の場合、すべてのパラメータについての偏微分を並べたものが、勾配ベクトルです。次元数は、パラメータの数と一致します。勾配ベクトルの方向は損失が最も急に増える方向であり、その反対に進むことで、損失を減らせます。

更新規則は、次のように書けます。

$latex \mathbf{w}{new} = \mathbf{w}{old} - \eta \nabla L$

第5章で学んだ、ベクトルのスカラー倍と引き算です。

勾配ベクトルの長さは、損失の変化の急峻さを表します。長さが極端に小さくなる勾配消失、極端に大きくなる勾配爆発は、いずれも学習を妨げます。勾配クリッピングは、方向を保ったまま長さだけを制限する手法であり、これもスカラー倍です。

重み行列に対する勾配は、その行列と同じ形を持ちます。したがって、学習時には、パラメータと同じ量のメモリが勾配のために必要になり、最適化手法の状態も含めると、パラメータだけの場合の数倍のメモリが必要です。

複数のデータから得られた勾配は、平均して使われます。これも、ベクトルの足し算とスカラー倍です。

次の第15章では、この勾配がどのように計算されるのかを扱います。誤差逆伝播法という仕組みの中で、行列の転置が重要な役割を果たします。第10章で学んだ転置が、なぜ学習の計算に現れるのかを確認します。

第15章に進む前に、演習4のコードを実行し、学習率を変えたときの挙動の違いを確認しておいてください。発散する様子を実際に見ておくと、実務で学習率を調整する際の勘所がつかめます。

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

投稿者プロフィール

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

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