Pytorch でのインプレース操作に関連するエラー分析と detach() メソッドの説明

0. 序文

*この記事に対する多大なご支援、ありがとうございます。
*素晴らしいブロガーを紹介してくれたライジング スター プログラムに感謝します。

国際慣例に従って、私は最初に宣言します: この記事は私自身の理解にすぎず、他の人の貴重な洞察を参照しましたが、内容が不正確である可能性があります。文章に間違いを見つけたら、批判・修正して一緒に進歩していきたいと思っています。

最近、nn.RNN モデルと nn.RNN に基づいた nn.LSTM モデルを構築しているときに、次のような非常に厄介な幸運なエラーが発生しました。

RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation: [torch.FloatTensor [1, 1]], which is output 0 of AsStridedBackward0, is at version 3; expected version 2 instead. Hint: the backtrace further above shows the operation that failed to compute its gradient. The variable in question was changed in there or anywhere later. Good luck!

CSDN には上記のエラー メッセージに関する説明記事が多数ありますが、そのほとんどが解決策を直接説明しており (ほとんどが使用されていません)、このエラーのメカニズムの説明が不足しています。したがって、このブログは、この問題の解決プロセスと関連する理解を記録するために書かれています。

1. 背景問題の説明

nn.RNN ベースのモデルを次のコードに単純化します。

import torch

rnn = torch.nn.RNN(input_size=1, hidden_size=1, num_layers=1)

train_set_x = torch.tensor([[[1]],[[2]],[[3]],[[4]],[[5]]], dtype=torch.float32)
train_set_y = torch.tensor([[[2]],[[4]],[[6]],[[8]],[[10]]], dtype=torch.float32)

h0 = torch.tensor([[0]], dtype=torch.float32)
h_cur = h0

loss = torch.nn.MSELoss()
opt = torch.optim.Adadelta(rnn.parameters(), lr = 0.01)

with torch.autograd.set_detect_anomaly(True):
    for i in range(5):
        opt.zero_grad()
        train_output, h_next = rnn(train_set_x[i], h_cur)
        rnn_loss = loss(train_output,train_set_y[i])
        rnn_loss.backward(retain_graph=True)
        opt.step()
        print(train_output)
        h_cur = h_next

このコードを実行すると、ネットワーク モデルのトレーニング時に上記のエラー メッセージが表示されることがわかります。

元の質問リンク:トレーニング中の Pytorch フレームワーク nn.RNN バックプロパゲーション エラー

2. エラー分析:インプレース(設定)操作の理解と説明

上記のエラー メッセージ「勾配の計算に必要な変数の 1 つが変更されましたan inplace operation」は、直訳すると「勾配の計算に必要な変数の 1 つが一个置位操作変更されました」になります。

以前からこの問題に悩まされていたのは、私が集合演算をよく理解していないからで、集合演算には「x += 1」や「x -」などの演算しかないことが分かりました。 = 1"。しかし、元のコードにはそのような操作はありませんが、操作の設定エラーを報告しています。

実際、集合演算は一般的な用語です直接更改内存中的值,而不是先复制一个值再更改复制后的这个值的操作

インプレース操作は、コピーを作成せずに、特定の Tensor のコンテンツを直接変更する操作です。pytorch のインプレース操作には、.add () や .scatter_() のように、常に接尾辞が付けられます。+= や *= などの Python 演算もインプレース演算です。

一般的に使用される割り当て方法「a = b」では、a と b の値は同じですが、同じ値が 2 つの完全に異なる物理アドレスに存在するため、a が変更されても影響はありません。 b、その逆。

しかし、上記の「a = b」が集合演算である場合、a が変更されると、同じ変更が b にも行われます。これは、それらが完全に同じ物理アドレスを共有し、同じメモリ ブロックを共有するためです。

したがって、共有変数が多数存在する可能性があり、これらの変数を計算 (変更) する際に、予期しない変数の変更が発生する可能性があるため、設定操作は注意して行う必要があります。

上記の問題コードの集合演算はどこにありますか?
答え: 「h_cur = h_next」です。id() メソッドを通じて、これら 2 つの変数のアドレスが一貫していることがわかります。我们认为的赋值操作被Pytorch变成了置位操作

print(id(h_cur))
print(id(h_next))
输出-------------------------------------
2943427659952
2943427659952

上記のエラー ステートメントの後半を振り返ると、「[torch.FloatTensor [1, 1]] (AsStridedBackward0 の出力 0 です) はバージョン 3 です。代わりにバージョン 2 が期待されています。」 [1,1] テンソルがあります。 (つまり、RNN h の隠れ層出力) はすでにバージョン 3 (バージョン) であり、期待されるバージョンはバージョン 2 (バージョン) です。ここでわかるのは、集合演算の回数です。

上記の「h_cur = h_next」の操作により、もう一度 h_cur (バージョン) が操作され、値は変更されていませんが、バージョンの不一致が発生し、最終的に上記のエラーが発生しました。

では、なぜ Pytorch にはデフォルトで set 操作があるのでしょうか?
答え: 为了节省内存,提高运行速度前述したように、set 操作では、最初にデータのコピーをコピーせずに、メモリ内のデータを直接変更できます。現在、大規模なニューラルネットワークには数万、数十万、さらには数百万のパラメータが存在することが多く、設定操作がなければ、各パラメータを後方にコピーし、コピーしたパラメータで計算する必要があり、膨大なコストがかかります。これらのパラメータを保存するためのメモリの容量。

隠れ層の出力 h は勾配計算にどのような影響を与えますか?
回答: 計算式は次のとおりです:
ここに画像の説明を挿入
具体的なプロセスについては、以前に紹介した RNN 数学モデルを参照してください: Numpy に基づいて RNN モジュールを構築し、それを例に適用します (コード付き)

もちろん、数学的な導出プロセスに本当に抵抗がある場合は、次の結論を直接理解できます在RNN中隐层输出h是直接参与反向传播梯度计算过程的

インプレース操作によって引き起こされるエラーを最終的に解決するにはどうすればよいですか?
A: インプレース操作をキャンセルします。実際に「値を代入」したい変数に強制的に新しいメモリを割り当てる この目的を達成できるメソッドとしては、detach() メソッドや clone() メソッドがあります。detach() の方が広く使用されているため、以下では detach() メソッドのみを説明します。

h_cur = mid 、mid = h_next などの従来の方法では中間変数を導入することはできません。Pytorch のデフォルトは依然として set 操作であるため、id を出力すると、h_cur、mid、および h_next が依然として物理アドレスを共有していることがわかります。

3. detach() メソッドの役割

①変数に新しいメモリを代入する

上記のコードを次のように変更します。

h_cur = h_next.detach()
print(id(h_cur))
print(id(h_next))
输出---------------------------------------------
3197060036944
3197060036864

2 つの変数は完全に分離され、この問題は解決されます。

②変数をリーフノード化する

変数がリーフ ノードかどうかは、 is_leaf() メソッドで識別できます。

h_cur = h_next.detach()
print('h_next_requires_grad:',h_next.requires_grad)
print('h_cur_requires_grad:',h_cur.requires_grad)
print('h_next_is_leaf:',h_next.is_leaf)
print('h_cur_is_lear:',h_cur.is_leaf)
输出---------------------------------------------
h_next_requires_grad: True
h_cur_requires_grad: False
h_next_is_leaf: False
h_cur_is_lear: True

detach() メソッドが h_cur の逆伝播を中断し、requires_grad を False に設定し、h_cur をリーフ ノードに設定していることがわかります。

リーフ ノード/非リーフ ノードの定義と機能については、この記事の対象ではありません。非常に優れたブログをお勧めします: Pytorch リーフ テンソル リーフ テンソル (リーフ ノード) (デタッチ)

4. 修正されたコード

修正後の完全なコードは次のとおりです。

import torch

rnn = torch.nn.RNN(input_size=1, hidden_size=1, num_layers=1)

train_set_x = torch.tensor([[[[1]]],[[[2]]],[[[3]]],[[[4]]],[[[5]]]], dtype=torch.float32)
train_set_y = torch.tensor([[[[2]]],[[[4]]],[[[6]]],[[[8]]],[[[10]]]], dtype=torch.float32)

h0 = torch.tensor([[[0]]], dtype=torch.float32)
h_cur = h0

loss = torch.nn.MSELoss()
opt = torch.optim.Adadelta(rnn.parameters(), lr = 0.01)


for i in range(5):
    opt.zero_grad()
    train_output, h_next = rnn(train_set_x[0], h_cur)
    rnn_loss = loss(train_output,train_set_y[0])
    h_cur = h_next.detach()
    rnn_loss.backward()
    opt.step()
    print(train_output)


# print(id(h_cur))
# print(id(h_next))
# print('h_next_requires_grad:',h_next.requires_grad)
# print('h_cur_requires_grad:',h_cur.requires_grad)
# print('h_next_is_leaf:',h_next.is_leaf)
# print('h_cur_is_lear:',h_cur.is_leaf)

さらに、一部のバージョンでは、detach() がなくても、次のような初期コードを実行できます。
ここに画像の説明を挿入
これは、以前の Pytorch にはデフォルトのインプレース操作がないためであると推測されます。もちろん、これは実行されます。これは、前述のメモリ消費量の増加の問題につながります。

おすすめ

転載: blog.csdn.net/m0_49963403/article/details/129767497