原文鏈接:https://blog.csdn.net/qq_36653505/java/article/details/90026373
關於 pytorch inplace operation需要注意的問題(data和detach方法的區別)
https://zhuanlan.zhihu.com/p/69294347
PyTorch 的 Autograd
葉子張量
對於任意一個張量來說,我們可以用 tensor.is_leaf
來判斷它是否是葉子張量(leaf tensor)。在反向傳播過程中,只有 is_leaf=True
的時候,需要求導的張量的導數結果才會被最后保留下來。
對於 requires_grad=False
的 tensor 來說,我們約定俗成地把它們歸為葉子張量。但其實無論如何划分都沒有影響,因為張量的 is_leaf
屬性只有在需要求導的時候才有意義。
我們真正需要注意的是當 requires_grad=True
的時候,如何判斷是否是葉子張量:當這個 tensor 是用戶創建的時候,它是一個葉子節點,當這個 tensor 是由其他運算操作產生的時候,它就不是一個葉子節點。我們來看個例子:
1 a = torch.ones([2, 2], requires_grad=True) 2 print(a.is_leaf) 3 # True 4 5 b = a + 2 6 print(b.is_leaf) 7 # False 8 # 因為 b 不是用戶創建的,是通過計算生成的
這時有同學可能會問了,為什么要搞出這么個葉子張量的概念出來?原因是為了節省內存(或顯存)。我們來想一下,那些非葉子結點,是通過用戶所定義的葉子節點的一系列運算生成的,也就是這些非葉子節點都是中間變量,一般情況下,用戶不會去使用這些中間變量的導數,所以為了節省內存,它們在用完之后就被釋放了。
我們回頭看一下之前的反向傳播計算圖,在圖中的葉子節點我用綠色標出了。可以看出來,被叫做葉子,可能是因為游離在主干之外,沒有子節點,因為它們都是被用戶創建的,不是通過其他節點生成。對於葉子節點來說,它們的 grad_fn
屬性都為空;而對於非葉子結點來說,因為它們是通過一些操作生成的,所以它們的 grad_fn
不為空。
inplace 操作
在編寫 pytorch 代碼的時候, 如果模型很復雜, 代碼寫的很隨意, 那么很有可能就會碰到由 inplace operation 導致的問題. 所以本文將對 pytorch 的 inplace operation 做一個簡單的總結。
inplace operation引發的報錯:
1 RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation.
我們先來了解一下什么是 inplace 操作:inplace 指的是在不更改變量的內存地址的情況下,直接修改變量的值。
如 i += 1, i[10] = 0等
PyTorch 是怎么檢測 tensor 發生了 inplace 操作呢?答案是通過 tensor._version
來檢測的。我們還是來看個例子:
1 a = torch.tensor([1.0, 3.0], requires_grad=True) 2 b = a + 2 3 print(b._version) # 0 4 5 loss = (b * b).mean() 6 b[0] = 1000.0 7 print(b._version) # 1 8 9 loss.backward() 10 # RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation ...
每次 tensor 在進行 inplace 操作時,變量 _version
就會加1,其初始值為0。在正向傳播過程中,求導系統記錄的 b
的 version 是0,但是在進行反向傳播的過程中,求導系統發現 b
的 version 變成1了,所以就會報錯了。但是還有一種特殊情況不會報錯,就是反向傳播求導的時候如果沒用到 b
的值(比如 y=x+1
, y 關於 x 的導數是1,和 x 無關),自然就不會去對比 b
前后的 version 了,所以不會報錯。
上邊我們所說的情況是針對非葉子節點的,對於 requires_grad=True
的葉子節點來說,要求更加嚴格了,甚至在葉子節點被使用之前修改它的值都不行。我們來看一個報錯信息:
1 RuntimeError: leaf variable has been moved into the graph interior
這個意思通俗一點說就是你的一頓 inplace 操作把一個葉子節點變成了非葉子節點了。我們知道,非葉子節點的導數在默認情況下是不會被保存的,這樣就會出問題了。舉個小例子:
1 a = torch.tensor([10., 5., 2., 3.], requires_grad=True) 2 print(a, a.is_leaf) 3 # tensor([10., 5., 2., 3.], requires_grad=True) True 4 5 a[:] = 0 6 print(a, a.is_leaf) 7 # tensor([0., 0., 0., 0.], grad_fn=<CopySlices>) False 8 9 loss = (a*a).mean() 10 loss.backward() 11 # RuntimeError: leaf variable has been moved into the graph interior
我們看到,在進行對 a
的重新 inplace 賦值之后,表示了 a 是通過 copy operation 生成的,grad_fn
都有了,所以自然而然不是葉子節點了。本來是該有導數值保留的變量,現在成了導數會被自動釋放的中間變量了,所以 PyTorch 就給你報錯了。還有另外一種情況:
1 a = torch.tensor([10., 5., 2., 3.], requires_grad=True) 2 a.add_(10.) # 或者 a += 10. 3 # RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.
這個更厲害了,不等到你調用 backward,只要你對需要求導的葉子張量使用了這些操作,馬上就會報錯。那是不是需要求導的葉子節點一旦被初始化賦值之后,就不能修改它們的值了呢?我們如果在某種情況下需要重新對葉子變量賦值該怎么辦呢?有辦法!
1 # 方法一 2 a = torch.tensor([10., 5., 2., 3.], requires_grad=True) 3 print(a, a.is_leaf, id(a)) 4 # tensor([10., 5., 2., 3.], requires_grad=True) True 2501274822696 5 6 a.data.fill_(10.) 7 # 或者 a.detach().fill_(10.) 8 print(a, a.is_leaf, id(a)) 9 # tensor([10., 10., 10., 10.], requires_grad=True) True 2501274822696 10 11 loss = (a*a).mean() 12 loss.backward() 13 print(a.grad) 14 # tensor([5., 5., 5., 5.]) 15 16 # 方法二 17 a = torch.tensor([10., 5., 2., 3.], requires_grad=True) 18 print(a, a.is_leaf) 19 # tensor([10., 5., 2., 3.], requires_grad=True) True 20 21 with torch.no_grad(): 22 a[:] = 10. 23 print(a, a.is_leaf) 24 # tensor([10., 10., 10., 10.], requires_grad=True) True 25 26 loss = (a*a).mean() 27 loss.backward() 28 print(a.grad) 29 # tensor([5., 5., 5., 5.])
修改的方法有很多種,核心就是修改那個和變量共享內存,但 requires_grad=False
的版本的值,比如通過 tensor.data
或者 tensor.detach()
(至於這二者更詳細的介紹與比較,歡迎參照我 上一篇文章的第四部分)。我們需要注意的是,要在變量被使用之前修改,不然等計算完之后再修改,還會造成求導上的問題,會報錯的。
為什么 PyTorch 的求導不支持絕大部分 inplace 操作呢?從上邊我們也看出來了,因為真的很 tricky。比如有的時候在一個變量已經參與了正向傳播的計算,之后它的值被修改了,在做反向傳播的時候如果還需要這個變量的值的話,我們肯定不能用那個后來修改的值吧,但沒修改之前的原始值已經被釋放掉了,我們怎么辦?一種可行的辦法就是我們在 Function 做 forward 的時候每次都開辟一片空間儲存當時輸入變量的值,這樣無論之后它們怎么修改,都不會影響了,反正我們有備份在存着。但這樣有什么問題?這樣會導致內存(或顯存)使用量大大增加。因為我們不確定哪個變量可能之后會做 inplace 操作,所以我們每個變量在做完 forward 之后都要儲存一個備份,成本太高了。除此之外,inplace operation 還可能造成很多其他求導上的問題。
總之,我們在實際寫代碼的過程中,沒有必須要用 inplace operation 的情況,而且支持它會帶來很大的性能上的犧牲,所以 PyTorch 不推薦使用 inplace 操作,當求導過程中發現有 inplace 操作影響求導正確性的時候,會采用報錯的方式提醒。但這句話反過來說就是,因為只要有 inplace 操作不當就會報錯,所以如果我們在程序中使用了 inplace 操作卻沒報錯,那么說明我們最后求導的結果是正確的,沒問題的。這就是我們常聽見的沒報錯就沒有問題。
在 pytorch 中, 有兩種情況不能使用 inplace operation:
- 對於 requires_grad=True 的 葉子張量(leaf tensor) 不能使用 inplace operation
- 對於在求梯度階段需要用到的張量不能使用 inplace operation
下面將通過代碼來說明以上兩種情況:
第一種情況: requires_grad=True 的 leaf tensor
1 import torch 2 3 w = torch.FloatTensor(10) # w 是個 leaf tensor 4 w.requires_grad = True # 將 requires_grad 設置為 True 5 w.normal_() # 在執行這句話就會報錯 6 # 報錯信息為 7 # RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.
很多人可能會有疑問, 模型的參數就是 requires_grad=true 的 leaf tensor, 那么模型參數的初始化應該怎么執行呢? 如果看一下 nn.Module._apply() 的代碼, 這問題就會很清楚了
修改那個和變量共享內存,requires_grad=False
的版本的值
1 w.data = w.data.normal() # 可以使用曲線救國的方法來初始化參數
第二種情況: 求梯度階段需要用到的張量(非葉子張量)
1 import torch 2 x = torch.FloatTensor([[1., 2.]]) 3 w1 = torch.FloatTensor([[2.], [1.]]) 4 w2 = torch.FloatTensor([3.]) 5 w1.requires_grad = True 6 w2.requires_grad = True 7 8 d = torch.matmul(x, w1) 9 f = torch.matmul(d, w2) 10 d[:] = 1 # 因為這句, 代碼報錯了 RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation 11 12 f.backward()
1 import torch 2 x = torch.FloatTensor([[1., 2.]]) 3 w1 = torch.FloatTensor([[2.], [1.]]) 4 w2 = torch.FloatTensor([3.]) 5 w1.requires_grad = True 6 w2.requires_grad = True 7 8 d = torch.matmul(x, w1) 9 d[:] = 1 # 稍微調換一下位置, 就沒有問題了 10 f = torch.matmul(d, w2) 11 f.backward()