1. 程式人生 > 實用技巧 >關於pytorch中inplace運算需要注意的問題

關於pytorch中inplace運算需要注意的問題

原文連結: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()