在深度學習模型的訓練過程中,難免引入隨機因素,這就會對模型的可復現性產生不好的影響。但是對於研究人員來講,模型的可復現性是很重要的。這篇文章收集並總結了可能導致模型難以復現的原因,雖然不可能完全避免隨機因素,但是可以通過一些設置盡可能降低模型的隨機性。
1. 常規操作
PyTorch官方提供了一些關於可復現性的解釋和說明。
在PyTorch發行版中,不同的版本或不同的平台上,不能保證完全可重復的結果。此外,即使在使用相同種子的情況下,結果也不能保證在CPU和GPU上再現。
但是,為了使計算能夠在一個特定平台和PyTorch版本上確定特定問題,需要采取幾個步驟。
PyTorch中涉及兩個偽隨機數生成器,需要手動對其進行播種以使運行可重復。此外,還應確保代碼所依賴的所有其他庫以及使用隨機數的庫也使用固定種子。
常用的固定seed的方法有:
import torch
import numpy as np
import random
seed=0
random.seed(seed)
np.random.seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
# Remove randomness (may be slower on Tesla GPUs)
# https://pytorch.org/docs/stable/notes/randomness.html
if seed == 0:
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
API中也揭示了原因,PyTorch使用的CUDA實現中,有一部分是原子操作,尤其是atomicAdd
,使用這個操作就代表數據不能夠並行處理,需要串行處理,使用到atomicAdd
之后就會按照不確定的並行加法順序執行,從而引入了不確定因素。PyTorch中使用到的atomicAdd
的方法:
前向傳播時:
- torch.Tensor.index_add_()
- torch.Tensor.scatter_add_()
- torch.bincount()
反向傳播時:
- torch.nn.functional.embedding_bag()
- torch.nn.functional.ctc_loss()
- 其他pooling,padding, sampling操作
可以說由於需要並行計算,從而引入atomicAdd之后,必然會引入不確定性,目前沒有一種簡單的方法可以完全避免不確定性。
2. upsample層
upsample導致模型可復現性變差,這一點在PyTorch的官方庫issue#12207
中有提到。也有很多熱心的人提供了這個的解決方案:
import torch.nn as nn
class UpsampleDeterministic(nn.Module):
def __init__(self,upscale=2):
super(UpsampleDeterministic, self).__init__()
self.upscale = upscale
def forward(self, x):
'''
x: 4-dim tensor. shape is (batch,channel,h,w)
output: 4-dim tensor. shape is (batch,channel,self.upscale*h,self.upscale*w)
'''
return x[:, :, :, None, :, None]\
.expand(-1, -1, -1, self.upscale, -1, self.upscale)\
.reshape(x.size(0), x.size(1), x.size(2)\
*self.upscale, x.size(3)*self.upscale)
# or
def upsample_deterministic(x,upscale):
return x[:, :, :, None, :, None]\
.expand(-1, -1, -1, upscale, -1, upscale)\
.reshape(x.size(0), x.size(1), x.size(2)\
*upscale, x.size(3)*upscale)
可以將以上模塊替換掉官方的nn.Upsample函數來避免不確定性。
3. Batch Size
Batch Size這個超參數很容易被人忽視,很多時候都是看目前剩余的顯存,然后再進行設置合適的Batch Size參數。模型復現時Batch Size大小是必須相同的。
Batch Size對模型的影響很大,Batch Size決定了要經過多少對數據的學習以后,進行一次反向傳播。
Batch Size過大:
- 占用顯存過大,在很多情況下很難滿足要求。對內存的容量也有更高的要求。
- 容易陷入局部最小值或者鞍點,模型會在發生過擬合,在訓練集上表現非常好,但是測試集上表現差。
Batch Size過小:
- 假設bs=1,這就屬於在線學習,每次的修正方向以各自樣本的梯度方向修正,很可能將難以收斂。
- 訓練時間過長,難以提高資源利用率
另外,由於CUDA的原因,Batch Size設置為2的冪次的時候速度更快一些。所以嘗試修改Batch Size的時候就按照4,8,16,32,...這樣進行設置。
4. 數據在線增強
在這里參考的庫是ultralytics的yolov3實現,數據增強分為在線增強和離線增強:
- 在線增強:在獲得 batch 數據之后,然后對這個 batch 的數據進行增強,如旋轉、平移、翻折等相應的變化,由於有些數據集不能接受線性級別的增長,這種方法常常用於大的數據集。
- 離線增強:直接對數據集進行處理,數據的數目會變成增強因子 x 原數據集的數目 ,這種方法常常用於數據集很小的時候。
在yolov3中使用的就是在線增強,比如其中一部分增強方法:
if self.augment:
# 隨機左右翻轉
lr_flip = True
if lr_flip and random.random() < 0.5:
img = np.fliplr(img)
if nL:
labels[:, 1] = 1 - labels[:, 1]
# 隨機上下翻轉
ud_flip = False
if ud_flip and random.random() < 0.5:
img = np.flipud(img)
if nL:
labels[:, 2] = 1 - labels[:, 2]
可以看到,如果設置了在線增強,那么模型會以一定的概率進行增強,這樣會導致每次運行得到的訓練樣本可能是不一致的,這也就造成了模型的不可復現。為了復現,這里暫時將在線增強的功能關掉。
5. 多線程操作
FP32(或者FP16 apex)中的隨機性是由多線程引入的,在PyTorch中設置DataLoader中的num_worker參數為0,或者直接不使用GPU,通過--device cpu
指定使用CPU都可以避免程序使用多線程。但是這明顯不是一個很好的解決方案,因為兩種操作都會顯著地影響訓練速度。
任何多線程操作都可能會引入問題,甚至是對單個向量求和,因為線程求和將導致FP16 / 32的精度損失,從而執行的順序和線程數將對結果產生輕微影響。
6. 其他
-
所有模型涉及到的文件中使用到random或者np.random的部分都需要設置seed
-
dropout可能也會帶來隨機性。
-
多GPU並行訓練會帶來一定程度的隨機性。
-
可能還有一些其他問題,感興趣的話可以看一下知乎上問題: PyTorch 有哪些坑/bug?
7. 總結
上面大概梳理了一下可能導致PyTorch的模型可復現性出現問題的原因。可以看出來,有很多問題是難以避免的,比如使用到官方提及的幾個方法、涉及到atomicAdd的操作、多線程操作等等。
筆者也在yolov3基礎上修改了以上提到的內容,固定了seed,batch size,關閉了數據增強。在模型運行了10個epoch左右的時候,前后兩次訓練的結果是一模一樣的,但是隨着epoch越來越多,也會產生一定的波動。
總之,應該盡量滿足可復現性的要求,我們可以通過設置固定seed等操作,盡可能保證前后兩次相同實驗得到的結果波動不能太大,不然就很難判斷模型的提升是由於隨機性導致的還是對模型的改進導致的。
目前筆者進行了多次試驗來研究模型的可復現性,偶爾會出現兩次一模一樣的訓練結果,但是更多實驗中,兩次的訓練結果都是略有不同的,不過通過以上設置,可以讓訓練結果差距在1%以內。
在目前的實驗中還無法達到每次前后兩次完全一樣,如果有讀者有類似的經驗,歡迎來交流。
8. 參考鏈接
https://pytorch.org/docs/stable/notes/randomness.html