PyTorchの神髄とも言える「微分と勾配」に関する超難解エラー。自分の考えた最強のオリジナル層や、特殊な重みの初期化を実装し、オプティマイザ(Adam等)にパラメータを渡して学習をスタートさせた瞬間に出現するこの言葉。
ValueError: can't optimize a non-leaf Tensor
「非リーフ(non-leaf)のテンソルは最適化できませんって? 木(Tree)の葉っぱ(Leaf)って何の話をしてるの? ディープラーニングのコードを書いていたはずが、急に植物学の話になったぞ…」
本記事では、このポエティック(詩的)で難解なエラーメッセージを皮切りに、PyTorchが内部で「どのように微分(グラデーション)を計算しているか」という「計算グラフ(Computational Graph)」の心臓部にメスを入れます!
1. Autograd(自動微分)の「計算グラフ」という巨大な木
PyTorchがなぜ世界を制覇できたのか?
それは、プログラムの中で足し算や掛け算を行うたびに、裏側で勝手に「AとBを足してCを作った」という歴史(計算グラフという木の枝)を記録し続けてくれるからです。この機能のおかげで、最後に Loss(損失)を backward() と唱えるだけで、裏庭で記録された計算グラフの枝を根元まで逆再生して、全ての重みの微分(勾配ベクトル)を出してくれるのです。
この巨大な木(グラフ)において、「一番端っこに生えている根源となる葉っぱ(Leaf)」が、まさに機械学習における「初期化した重みパラメータ」です。
PyTorchのオプティマイザ(AdamやSGD)は、「他の何かから計算されて作られた途中地点の枝(出力や中間活性化マップ)」は絶対に更新してくれません。
更新対象にできるのは、一番大元となる「葉っぱのテンソル(Leaf Tensor)」だけ、という絶対的な宇宙のルールがあるのです。
2. なぜあなたの重みは「葉っぱ」ではなくなったのか?
エラーが発生する原因は、あなたが「最初から用意された純粋なパラメータ(葉っぱ)」に対して、意図せず【PyTorchの計算(足し算や変換等)】を加えてしまい、その「計算結果として生まれた新しい枝(非リーフ)」をオプティマイザに渡してしまったからです。
初心者が最もやってしまいがちな、非常によくあるバグコードを見てみましょう。
import torch
import torch.optim as optim
# ================================
# 【エラーで落ちる最悪の実装例】
# ================================
# モデルの重みを定義。この時点ではWは純粋な「葉っぱ(Leaf)」です。
W = torch.randn(10, 10, requires_grad=True)
# !!! 注目 !!!
# ここで GPU (cuda) に送る操作を行っています。
# 実は「テンソルを別のデバイスに移動する」という行為自体が
# PyTorchにとっては「新しい計算(移動オペレーション)」とみなされます!
W = W.to('cuda')
# 結果として、いま W という変数に入っているのは、
# 「元の葉っぱがGPUに移動して出来上がった結果(枝=非リーフ)」に書き換わってしまいました!
# オプティマイザは激怒します。
# 「私に渡されたWはただの枝(移動結果)だ!大根元の葉っぱじゃないから最適化(更新)できん!」
optimizer = optim.Adam([W], lr=0.01) # => ValueError 大爆発!3. 葉っぱを守る「インプレース操作」と「デバイス設定の順番」
先ほどのコードを正しく動かすための防弾措置には、いくつかのアプローチがあります。
正しい解決策①:最初からGPU上で「葉っぱ」を生み出す
移動という後からの計算を発生させず、作成時に直接デバイスを指定し、そこで微分フラグ(requires_grad)を立てます。これが最も安全で速いです。
# 【防弾仕様】
# デバイス指定を生成時に一緒に行えば、移動計算は発生せず純粋な「葉っぱのまま」となる
W = torch.randn(10, 10, device='cuda', requires_grad=True)
optimizer = optim.Adam([W], lr=0.01) # 正常に学習スタート!正しい解決策②:nn.Parameter() という「神の保証」
独自レイヤーやカスタムモデルを設計する場合、ただの Tensor ではなく、必ず nn.Parameter 型で包むのがPyTorchの標準規格です。
この魔法のラッパーで包むと、「どんな移動計算が起きようと、お前は常にモデルのパラメータ(葉っぱ)である!」という属性が保持されるため、安全に model.to('cuda') で一斉移動させることができます。
import torch.nn as nn
class MyLayer(nn.Module):
def __init__(self):
super().__init__()
# nn.Parameter で包むことで、モデルのパラメータとしての地位(葉っぱ)を確立する
self.W = nn.Parameter(torch.randn(10, 10))
model = MyLayer()
# nn.Moduleの .to() メソッドは賢いので、内部の葉っぱ属性を壊さずに移動してくれる!
model = model.to('cuda')
optimizer = optim.Adam(model.parameters(), lr=0.01) # 完璧!まとめ:裏側の「計算グラフ」を意識する者だけが生き残る
non-leaf Tensor のエラーを見たとき、それを「ただのバグ」としてコピペで直す人と、「なるほど、ここで計算グラフの繋がりが途切れてしまっていたのか」と数学的に腹落ちする人との間には、計り知れない成長スピードの差が生まれます。
PyTorchの .detach()(枝を切り落としてグラフから切り離す機能)や、with torch.no_grad():(最初から歴史を記録しない機能)など、Autogradの仕組みを深く理解することは、超一流のリサーチャーやエンジニアになるための必須科目です。
この機会にぜひ、ディープラーニングの数学的基盤やPyTorchの内部構造に踏み込んだ専門書を一読してみてください!


コメント