Pytorch裡面多工Loss是加起來還是分別backward?


MLNLP 

機器學習演算法與自然語言處理 

)社群是國內外知名自然語言處理社群,受眾覆蓋國內外NLP碩博生、高校老師以及企業研究人員。


社群的願景 是促進國內外自然語言處理,機器學習學術界、產業界和廣大愛好者之間的交流,特別是初學者同學們的進步。

本文轉載自 | 極市平臺
作者 | 歪槓小脹@知乎
來源丨https://zhuanlan.zhihu.com/p/451441329
1
『記錄寫這篇文章的初衷』
最近在復現一篇論文的訓練程式碼時,發現原論文中的總loss由多個loss組成。如果只有一個loss,那麼直接loss.backward()即可,但是這裡不止一個。一開始看到不止一個loss時,不知道將backward()放在哪裡。
for

 j 

in

 range(len(output)):

    loss += criterion(output[j], target_var)

我們知道,一般傳統的梯度回傳步驟是這樣的:

outputs = model(images)

loss = criterion(outputs,target)
optimizer.zero_grad()  

loss.backward()

optimizer.step()

  • 首先模型會透過輸入的影像與標籤計算相應的損失函式;
  • 然後清除之前過往的梯度optimizer.zero_grad()
  • 進行梯度的回傳,並計算當前的梯度loss.backward()反向傳播,計算當前梯度;
  • 根據當前的梯度來更新網路引數。一般來說是進來一個batch的資料就會計算一次梯度,然後更新網路optimizer.step()
而現在我需要在一個for迴圈中計算loss,於是我就在想是否需要在for迴圈中進行backward()的計算呢?
for

 j 

in

 range(len(output)):

    loss += criterion(output[j], target_var)

    loss.backward()

但是當計算完一個loss之後就使用backward方法,發現報錯:Pytorch – RuntimeError: Trying to backward through the graph a second time, but the buffers have already been freed. Specify retain_graph=True when calling backward the first time.
原因是在Pytorch中,一張計算圖只允許存在一次損失的回傳計算,當每次進行梯度回傳之後,中間的變數都會被釋放掉。所以如果想在本次batch中再次計算圖的梯度時,程式會發現中間的計算圖已經沒了,那麼自然而然也就沒有辦法計算梯度。
網上看到有個解決辦法是在backward中加入retain_grad=True,也就是backward(retain_graph=True)
這句話的意思是暫時不釋放計算圖,所以在後續的訓練過程中計算圖不會被釋放掉,而是會一直累積,但是隨著訓練的進行,會出現OOM。因此,需要在最後一個loss計算時,把(retain_graph=True)去掉,也就是隻使用backward(),也就是除了最終的loss要釋放資源、計算梯度,前面若干個的loss都不進行此步驟。
for

 j 

in

 range(len(output)):

    loss += criterion(output[j], target_var)

loss.backward()

也許有同學會問,為什麼不這麼寫呢?我之前也是這樣的,可是發現loss並沒有降低,於是就開始從loss裡找原因了,但是為什麼不降低,我也沒有理解明白,希望有明白的同學可以交流下~
其實當遇到這種情況,最好的辦法就是分開寫,然後再彙總到一個總loss中計算backward計算。如:

loss1= Loss(output[

0

], target)

loss2= Loss(output[

1

], target)

loss3= Loss(output[

2

], target)

loss4= Loss(output[

3

], target)
loss = loss1 + loss2 + loss3 + loss4

loss.backward()

當我這麼寫的時候,loss就正常下降了。看到loss下降得還算是正常時,我就稍微放心了。
2
『發生錯誤的其他可能原因』
在查詢資料的時候,發現即使只計算一個loss,也可能會出現錯誤。
  • 有可能你計算的裝置一個在cpu上,一個在gpu,所以將裝置設定為同一個即可。
  • 也有可能在多次迴圈中,有一些輸入是不需要計算梯度的,這個時候就可以將輸入的require_grad設定為False
  • 關於張量tensor中的require_grad屬性:如果一個張量它的requires_grad=True,那麼在反向傳播計算梯度時呼叫backward()方法就會計算這個張量的梯度。但是需要注意的是:計算完梯度之後,這個梯度並不一定會一直儲存在屬性grad中,只有對於requires_grad=True的葉子結點才會一直儲存梯度,即將梯度一直儲存在該葉子張量的grad屬性中。而對於非葉子節點,即中間節點的張量,我們在計算完梯度之後為了更高效地利用記憶體,一般會將中間計算的梯度釋放掉。
  • 在使用LSTMGRU這一些網路時,我想是因為它們不僅會從前往後計算梯度,也會從後往前計算梯度,所以可以看做是梯度在兩個方向上進行傳播,那麼這個過程中就會有重疊的部分。因此可能就需要使用detach來進行截斷。在原始碼中,detach的註釋是:Returns a new Variable, detached from the current graph。是將某個結點變成不需要梯度的變數,將其從當前的計算圖剝離出來。因此當反向傳播經過這個結點時,梯度就不會從這個結點往前面傳播。

detach()detach_()

pytorch中有兩個函式detach()detach_(),它們兩個名字、功能都很像,都是用於切斷梯度的反向傳播。那麼什麼時候會用到呢?
當我們在訓練網路的時候可能希望保持一部分的網路引數不變,而只對網路的一部分引數進行調整;或者只訓練網路的部分分支網路,並且不想讓其梯度對主網路的梯度造成影響。這時候我們就可以使用這兩個函式來進行截斷梯度的反向傳播。
二者的區別就是detach_()是對本身進行更改,而detach()則是生成了一個新的tensor
使用detach()會返回一個新的Variable。雖然它是從當前計算圖中分離下來的,但是仍指向原變數的存放位置,也就是共享同一個記憶體區域。使用了detach後,它的requires_grad屬性為False,也就是不需要再計算它的梯度。即使之後重新將它的requires_grad變為True,它也不會具有梯度grad。這樣我們就會繼續使用這個新的變數進行計算,後面當我們進行反向傳播時,梯度會一直計算直到到達這個呼叫了detach()的結點,到達這個結點後就會停止,不會再繼續向前進行傳播。
但是返回的變數和原始的結點是共用同一個記憶體區域,所以如果使用了detach後,又對其進行修改,那麼進行呼叫backward()時,就可能會導致錯誤。
使用tensor.detach_()則會將一個tensor從建立它的圖中分離,並把它設定成葉子結點。舉個例子:
假設一開始的變數關係為:x ->m -> y,那麼這裡的葉子結點就是x,當這個時候對m進行了m.detach_()操作,首先會取消m與前一個結點x的關聯,並且grad_fnNone。此時,這裡的關係就會變成xm ->y,這個時候m就變成了葉子結點。然後再將mrequires_grad屬性設定為False,當我們對y進行backward()時就不會求m的梯度。
3
『如何編寫更能節省記憶體的backward』
說到梯度回傳,我在網上也看到有人的寫法是這樣的,目的是為了節省記憶體:
for

 i, (images, target) 

in

 enumerate(train_loader):

    images = images.cuda(non_blocking=

True

)

    target = torch.from_numpy(np.array(target)).float().cuda(non_blocking=

True

)

    outputs = model(images)

    loss = criterion(outputs, target)

    loss = loss / accumulation_steps   
    loss.backward()

if

 (i + 

1

) % accumulation_steps == 

0

:

        optimizer.step()       

        optimizer.zero_grad()

  • 首先進行正向傳播,將資料傳入網路進行推理,得到結果
  • 將預測結果與label輸入進損失函式中計算損失
  • 進行反向傳播,計算梯度
  • 重複前面的步驟,先不清空梯度,而是先將梯度進行累加,當梯度累加達到固定次數之後就更新網路引數,然後將梯度置零
梯度累加就是每次獲取1個batch的資料,計算1次梯度,但是先不進行清零,而是做梯度的累加,不斷地進行累加,當累加到一定的次數之後,再更新網路引數,然後將梯度清零,進行下一個迴圈。
透過這種引數延遲更新的手段,可以實現與採用大batch size相近的效果。在平時的實驗過程中,我一般會採用梯度累加技術,大多數情況下,採用梯度累加訓練的模型效果,要比採用小batch size訓練的模型效果要好很多。
一定條件下,batch size越大訓練效果越好,梯度累加則實現了batch size的變相擴大,如果accumulation_steps為8,則batch size就變相擴大了8倍,使用時需要注意,學習率也要適當放大:因為使用的樣本增多,梯度更加穩定了。
有人會問,在上面的程式碼中為什麼不直接對多個batchloss先求和然後再取平均、再進行梯度回傳和更新呢?
按我的理解這是為了減小記憶體的消耗。當採用多個batchloss求和平再均後再回傳的方式時,我們會進行accumulation_stepsbatch的前向計算,而前向計算後都會生成一個計算圖。也就是說,在這種方式下,會生成accumulation_steps個計算圖再進行backward計算。
而採用上述程式碼的方式時,當每次的batch前向計算結束後,就會進行backward的計算,計算結束後也就釋放了計算圖。又因為這兩者計算過程的梯度都是累加的,所以計算結果都是相同的,但是上述的方法在每一時刻中,最多隻會生成一張計算圖,所以也就減小了計算中的記憶體消耗。
4
『結語』
其實透過這次探討,只能說是瞭解地稍微深一些了,但是其中的原理還是不太明白。比如autograd的跟蹤、in-place operations的屬性,什麼時候requires_gradTrue,什麼時候又為False,什麼時候梯度會進行覆蓋等等,這一些還是一頭霧水。特別是上面那種寫法,搞不明白loss為什麼就突然下降了,所以還是得多學多用才能記住,才能深刻理解。
我也看到很多文章提到:其實大部分的寫法都十分高效了,所以除非處於非常沉重的記憶體壓力下,否則一般不會用到太多騷操作。
筆者才疏學淺,以上內容如有錯誤,歡迎各位指出,期待與大家交流探討。謝謝!
技術交流群邀請函
△長按新增小助手
掃描二維碼新增小助手微信
請備註:姓名-學校/公司-研究方向
(如:小張-哈工大-對話系統)
即可申請加入自然語言處理/Pytorch等技術交流群

關於我們

MLNLP社群  機器學習演算法與自然語言處理 ) 是由國內外自然語言處理學者聯合構建的民間學術社群,目前已經發展為國內外知名自然語言處理社群,旗下包括  萬人頂會交流群、AI臻選匯、AI英才匯  以及  AI學術匯  等知名品牌,旨在促進機器學習,自然語言處理學術界、產業界和廣大愛好者之間的進步。
社群可以為相關從業者的深造、就業及研究等方面提供開放交流平臺。歡迎大家關注和加入我們。

相關文章