
MLNLP
(
機器學習演算法與自然語言處理
)社群是國內外知名自然語言處理社群,受眾覆蓋國內外NLP碩博生、高校老師以及企業研究人員。
社群的願景 是促進國內外自然語言處理,機器學習學術界、產業界和廣大愛好者之間的交流,特別是初學者同學們的進步。
(
機器學習演算法與自然語言處理
)社群是國內外知名自然語言處理社群,受眾覆蓋國內外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
屬性中。而對於非葉子節點,即中間節點的張量,我們在計算完梯度之後為了更高效地利用記憶體,一般會將中間計算的梯度釋放掉。 -
在使用 LSTM
、GRU
這一些網路時,我想是因為它們不僅會從前往後計算梯度,也會從後往前計算梯度,所以可以看做是梯度在兩個方向上進行傳播,那麼這個過程中就會有重疊的部分。因此可能就需要使用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_fn
為None
。此時,這裡的關係就會變成x
,m
->y
,這個時候m
就變成了葉子結點。然後再將m
的requires_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倍,使用時需要注意,學習率也要適當放大:因為使用的樣本增多,梯度更加穩定了。有人會問,在上面的程式碼中為什麼不直接對多個
batch
的loss
先求和然後再取平均、再進行梯度回傳和更新呢?按我的理解這是為了減小記憶體的消耗。當採用多個
batch
的loss
求和平再均後再回傳的方式時,我們會進行accumulation_steps
次batch
的前向計算,而前向計算後都會生成一個計算圖。也就是說,在這種方式下,會生成accumulation_steps
個計算圖再進行backward
計算。而採用上述程式碼的方式時,當每次的
batch
前向計算結束後,就會進行backward
的計算,計算結束後也就釋放了計算圖。又因為這兩者計算過程的梯度都是累加的,所以計算結果都是相同的,但是上述的方法在每一時刻中,最多隻會生成一張計算圖,所以也就減小了計算中的記憶體消耗。4
『結語』
其實透過這次探討,只能說是瞭解地稍微深一些了,但是其中的原理還是不太明白。比如
autograd
的跟蹤、in-place operations
的屬性,什麼時候requires_grad
為True
,什麼時候又為False
,什麼時候梯度會進行覆蓋等等,這一些還是一頭霧水。特別是上面那種寫法,搞不明白loss
為什麼就突然下降了,所以還是得多學多用才能記住,才能深刻理解。我也看到很多文章提到:其實大部分的寫法都十分高效了,所以除非處於非常沉重的記憶體壓力下,否則一般不會用到太多騷操作。
筆者才疏學淺,以上內容如有錯誤,歡迎各位指出,期待與大家交流探討。謝謝!

掃描二維碼新增小助手微信
關於我們
