PyTorch Autograd 自動微分

ディープラーニングのトレーニングは、本質的に勾配を求め、パラメータを更新するプロセスを繰り返すことです。

各層の勾配を手動で導出するのは煩雑でエラーも起こりやすいため、PyTorch のAutograd(自動微分)エンジンはまさにこの問題を解決するために生まれました。それは任意の計算グラフの勾配を自動計算できます。そのため、微積分の導出ではなくモデル設計に集中できます。


コアコンセプト

1、自動微分とは

自動微分(Automatic Differentiation)は、数値微分(有限差分法)でも記号微分(代数導出)でもありません。次の方法によって計算過程を記録し、逆方向に連鎖律を段階的に適用することで導関数を正確に計算します。

PyTorch の Autograd は動的計算グラフ(Define-by-Run)方式を採用しています。毎回の順伝播で有向非巡回グラフ(DAG)をリアルタイムに構築し、各操作とその入出力を記録します。逆伝播時にはグラフを逆方向に辿り、各ノードの勾配を順に計算します。

2、requires_grad 属性

Tensor のrequires_grad属性は、そのテンソルに対して勾配を追跡する必要があるかどうかを制御します:

例

import torch

# 勾配の追跡が必要なテンソルを作成(デフォルトは requires_grad=False)
x = torch.tensor(3.0, requires_grad=True)
print(x)              # tensor(3., requires_grad=True)
print(x.requires_grad)  # True

# 作成後に変更することもできます
y = torch.tensor(2.0)
print(y.requires_grad)   # False
y.requires_grad_(True)   # インプレース変更(末尾にアンダースコアがあることに注意)
print(y.requires_grad)   # True

# requires_grad=True のテンソルが演算に関与した結果は、自動的に requires_grad=True を受け継ぎます
z = x * y
print(z.requires_grad)   # True

3、grad_fn と計算グラフ

演算によって生成された各テンソルは、grad_fnそれを生成した操作ノードを指す grad_fn を記録します。これが計算グラフの「骨格」です:

例

import torch

x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)

z = x ** 2 + y * 3    # z = x² + 3y

print(z)           # tensor(13., grad_fn=<AddBackward0>)
print(z.grad_fn)   # <AddBackward0 object>

# z を作成した操作チェーンを追跡
print(z.grad_fn.next_functions)
# ((<PowBackward0 object>, 0), (<MulBackward0 object>, 0))
# z は冪乗演算と乗算演算の組み合わせから生成されていることがわかります

backward() 逆伝播

1、スカラー出力での backward() 呼び出し

最終的なスカラー(損失値)に対して backward() を呼び出す.backward()と、Autograd は自動的に計算グラフに沿って逆方向にすべての葉ノードの勾配を計算し、結果は各テンソルの.grad属性に格納されます:

例

import torch

x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)

# 順伝播:z = x² + 3y
z = x ** 2 + y * 3

# 逆伝播:dz/dx と dz/dy を自動計算
z.backward()

# 勾配を確認
print(x.grad)   # tensor(4.)  ← dz/dx = 2x = 2×2 = 4
print(y.grad)   # tensor(3.)  ← dz/dy = 3

# 数学的な検証:
# z = x² + 3y
# dz/dx = 2x = 2×2 = 4  ✓
# dz/dy = 3             ✓

2、backward() を複数回呼び出したときの累積問題

Autograd の勾配は累積され、上書きではありません。呼び出すたびにbackward()勾配は.grad既存の値に加算されます。これはトレーニングループで最も陥りやすい落とし穴です:

例

import torch

x = torch.tensor(2.0, requires_grad=True)

# 1回目の逆伝播
loss = x ** 2
loss.backward()
print(x.grad)    # tensor(4.)  ← dL/dx = 2x = 4

# 2回目の逆伝播(クリアしていない!)
loss = x ** 2
loss.backward()
print(x.grad)    # tensor(8.) ← 累積された!4 ではなく 4+4=8

# ✅ 正しい方法:毎回の逆伝播前にクリアする
x.grad.zero_()   # インプレースでクリア(アンダースコアに注意)
loss = x ** 2
loss.backward()
print(x.grad)    # tensor(4.) ← 正しい

ニューラルネットワークのトレーニング時には、毎回backward()backward() の前に呼び出しoptimizer.zero_grad()て勾配をクリアする必要があります。そうしないと勾配が累積し続け、パラメータ更新が誤ってしまいます。

3、非スカラー出力での backward(gradient) 呼び出し

出力がスカラーではなくベクトルや行列の場合、backward()出力と同じ形状のgradientパラメータ(すなわち「上流勾配」)を渡す必要があります。これは本質的にベクトル-ヤコビアン積(VJP)を計算することです:

例

import torch

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)

# 順伝播:y はベクトル
y = x ** 2   # y = [1, 4, 9]

# 非スカラーの場合、gradient パラメータ(y と同じ形状)を渡す必要があります
# gradient は「損失の y に対する勾配」と理解できます
y.backward(gradient=torch.ones_like(y))   # 上流勾配がすべて 1 であると仮定

print(x.grad)
# tensor([2., 4., 6.]) ← dy/dx = 2x、要素ごとに計算

# 上流勾配がすべて 1 でない場合(例えば重み付け)
x.grad.zero_()
y.backward(gradient=torch.tensor([1.0, 0.5, 2.0]))  # 異なる重み
# 実際の計算:x.grad = 2x * gradient = [2×1, 4×0.5, 6×2]
print(x.grad)
# tensor([2., 2., 12.])

# より一般的な方法:最初に sum/mean でスカラーにしてから backward()
x.grad.zero_()
loss = (x ** 2).sum()   # ベクトルをスカラーに集約
loss.backward()
print(x.grad)
# tensor([2., 4., 6.]) ← 最初の書き方と等価

torch.no_grad() で勾配追跡を停止

モデルの推論(予測)段階では、勾配を計算する必要はありません。torch.no_grad() をtorch.no_grad()使用すると、計算グラフの構築をスキップでき、メモリと計算を大幅に節約できます:

例

import torch

x = torch.tensor(3.0, requires_grad=True)

# no_grad コンテキスト内では、すべての演算は勾配を追跡されません
with torch.no_grad():
    y = x ** 2
    print(y.requires_grad)  # False ← 勾配を追跡しない
    print(y.grad_fn)        # None ← 計算グラフのノードがない

# no_grad コンテキストを抜けると、通常の追跡に戻ります
z = x ** 2
print(z.requires_grad)  # True

# 一般的な用途:モデル評価時に推論プロセス全体をラップする
model = torch.nn.Linear(10, 1)
inputs = torch.randn(32, 10)

with torch.no_grad():
    outputs = model(inputs)   # 計算グラフを構築しないため、高速でメモリも節約できる

@torch.no_grad() デコレータの書き方

デコレータ形式でも使用でき、推論関数全体を勾配なしとしてマークするのに適しています:

例

import torch
import torch.nn as nn

model = nn.Linear(10, 1)

@torch.no_grad()
def predict(model, x):
    """推論関数。勾配を計算する必要はありません"""
    return model(x)

x = torch.randn(5, 10)
output = predict(model, x)
print(output.requires_grad)   # False

detach() で計算グラフから分離

.detach()元のテンソルとデータを共有するが、勾配を追跡しない新しいテンソルを返します。主に以下のシナリオで使用されます:

シナリオ 説明
中間結果を numpy 配列に変換する numpy は勾配を持つテンソルをサポートしていないため、まず detach() する必要があります
トレーニング損失(ログ)を記録する 計算グラフ全体を保存せず、メモリリークを防ぐ
ネットワークの一部の勾配伝播を凍結する GAN トレーニング、転移学習などのシナリオ

例

import torch

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x ** 2 + x * 3   # y には grad_fn がある

# detach 後のテンソルは y とデータを共有するが、計算グラフから切り離される
y_detached = y.detach()
print(y_detached.requires_grad)  # False
print(y_detached.grad_fn)        # None

# ✅ numpy に変換(勾配を持つテンソルは直接変換できない)
# y.numpy() # ❌ エラー:RuntimeError
y_detached.numpy()    # ✅ 正常

# ✅ 損失値を記録するときは detach する(計算グラフを保持してメモリを消費するのを避ける)
losses = []
for i in range(3):
    loss = (x ** 2).sum()
    losses.append(loss.detach().item())  # .item() はスカラーテンソルを Python の float に変換する
    loss.backward()
    x.grad.zero_()

print(losses)   # [14.0, 14.0, 14.0]

retain_graph で計算グラフを保持

デフォルトでは、backward()backward() 実行後、計算グラフは自動的に解放されます(メモリ節約のため)。同じ計算グラフに対して複数回逆伝播を行う必要がある場合(一部の GAN トレーニングなど)、retain_graph=True を渡す必要があります。retain_graph=True:

例

import torch

x = torch.tensor(2.0, requires_grad=True)
y = x ** 3   # y = x³

# 1回目の backward(計算グラフを保持)
y.backward(retain_graph=True)
print(x.grad)    # tensor(12.)  ← dy/dx = 3x² = 3×4 = 12

# 2回目の backward(計算グラフはまだ存在する)
x.grad.zero_()
y.backward(retain_graph=True)
print(x.grad)    # tensor(12.) ← 同じ結果

# 最後は保持する必要がない
x.grad.zero_()
y.backward()     # この後、計算グラフが解放される
print(x.grad)    # tensor(12.)

# 再度 backward を試みるとエラーになる(計算グラフは解放済み)
# y.backward()   # &#x274c; RuntimeError: Trying to backward through the graph a second time

不必要に使用するretain_graph=Trueメモリの持続的な増加を引き起こす。計算グラフを解放できないためである。本当に複数回の逆伝播が必要な場合にのみ使用する。


ニューラルネットワークトレーニングにおける勾配の応用

以下は、Autograd を使用して勾配降下法を手動で実装する完全な例であり、実際の訓練における Autograd の完全な流れを示している:

例

import torch

# 訓練データを構築:y = 2x + 1 にノイズを加える
torch.manual_seed(42)
X = torch.randn(100, 1)
y_true = 2 * X + 1 + 0.1 * torch.randn(100, 1)

# モデルパラメータを初期化(勾配の追跡が必要)
w = torch.zeros(1, requires_grad=True)   # 重み
b = torch.zeros(1, requires_grad=True)   # バイアス

lr = 0.1    # 学習率
epochs = 50  # 訓練エポック数

for epoch in range(epochs):
    # 1. 順伝播:予測値を計算
    y_pred = X * w + b

    # 2. 損失を計算(平均二乗誤差)
    loss = ((y_pred - y_true) ** 2).mean()

    # 3. 逆伝播:d(loss)/dw と d(loss)/db を自動計算
    loss.backward()

    # 4. パラメータを手動更新(no_grad でラップし、更新操作が計算グラフに追跡されるのを防ぐ)
    with torch.no_grad():
        w -= lr * w.grad
        b -= lr * b.grad

    # 5. 勾配をゼロにする(次の backward の前に必ずクリアする)
    w.grad.zero_()
    b.grad.zero_()

    if (epoch + 1) % 10 == 0:
        print(f"Epoch {epoch+1:3d} | Loss: {loss.item():.4f} | w={w.item():.3f}, b={b.item():.3f}")

print(f"\n訓練完了:w ≈ {w.item():.3f}(実際の値 2.0)、b ≈ {b.item():.3f}(実際の値 1.0))

上記のコードの実行結果は次のようになる:

Epoch  10 | Loss: 0.1064 | w=1.587, b=0.805
Epoch  20 | Loss: 0.0281 | w=1.876, b=0.949
Epoch  30 | Loss: 0.0152 | w=1.954, b=0.983
Epoch  40 | Loss: 0.0128 | w=1.978, b=0.993
Epoch  50 | Loss: 0.0122 | w=1.987, b=0.997

训练完成:w ≈ 1.987(真实值 2.0),b ≈ 0.997(真实值 1.0)

よく使う API 早見表

API 機能 よくある使用シーン
tensor.requires_grad_(True) インプレースで勾配追跡を有効にする 作成済みのテンソルに対して Autograd を有効にする
loss.backward() 逆伝播し、すべての葉ノードの勾配を計算する 訓練ループ内の各イテレーション
tensor.grad テンソルの勾配値にアクセスする パラメータの確認または手動更新
tensor.grad.zero_() インプレースで勾配をゼロにする 毎回 backward() の前に必ずクリアする
torch.no_grad() コンテキストマネージャ。勾配追跡を無効にする 推論段階、パラメータの手動更新
tensor.detach() 計算グラフから切り離された新しいテンソルを返す numpy への変換、ログの記録、勾配の凍結
tensor.item() スカラーテンソルを Python の数値に変換する 損失値の出力、指標の記録
loss.backward(retain_graph=True) 計算グラフを保持し、複数回の逆伝播を可能にする GAN 訓練など、複数回の backward が必要なシーン

よくある質問と注意事項

1. 葉ノードと非葉ノードの違い

のみ葉ノード(つまりユーザーが直接作成した、演算結果ではないテンソル)の勾配が保存される.gradに。中間演算によって生成された非葉ノードは、デフォルトでは勾配を保持しない(メモリ節約のため)。中間ノードの勾配を確認する必要がある場合は、呼び出す必要がある.retain_grad():

例

import torch

x = torch.tensor(2.0, requires_grad=True)   # 葉ノード

# 中間ノード(非葉ノード)
y = x ** 2      # y は中間ノード
y.retain_grad() # y の勾配を保持することを明示的に宣言

z = y * 3       # z は最終出力
z.backward()

print(x.grad)   # tensor(12.) ← 葉ノードなので、正常に保存される
print(y.grad)   # tensor(3.) ← retain_grad() により、保存された
# retain_grad() がない中間ノードでは、.grad は None

2. インプレース操作(in-place)は計算グラフを壊す可能性がある

勾配の追跡が必要なテンソルに対するインプレース操作(例:+=、.add_())は Autograd が正しく逆伝播できない可能性があるため、できる限り避けるべきである:

例

import torch

x = torch.tensor([1.0, 2.0], requires_grad=True)

# 危険:葉ノードに対するインプレース操作
# x += 1 # エラーになる可能性がある:a leaf Variable that requires grad has been used in an in-place operation

# 安全:非インプレース操作を使用する
y = x + 1   # 新しいテンソルを作成し、x を変更しない

# no_grad コンテキストでのインプレース操作は安全である(例:パラメータ更新)
with torch.no_grad():
    x += 0.01   # パラメータの手動更新に使用する場合、ここでは安全である

3. 浮動小数点型テンソルのみが勾配をサポートする

整数型(例:torch.int64)のテンソルはサポートしていないrequires_grad=True、浮動小数点型のみ(float32、float64、float16)が自動微分に参加できる。

その他の関連記事