深入理解卷積與模型大小問題,解決顯存不足

目錄

前言:

GPU基礎知識

卷積神經網絡參數

參數的顯存佔用

梯度與動量的顯存佔用

輸入輸出的顯存佔用

節省顯存的方法

減少卷積層的計算量

常用模型 顯存/計算複雜度/準確率

建議


前言:

在訓練自己的模型時常常出現顯存不足等問題,這個時候我們常用的方法就是調參。一般常用的方法有以下幾點:

  1. 模型壓縮
  2. 網絡參數調整, 比如減小訓練圖像大小,降低FC output個數,使用小的conv kernel size等。
  3. 深度學習框架調整
  4. 減層

但是對於既定網絡,我們減小訓練圖像大小或者改變batchsize大小都會影響模型的性能,這點在目標檢測和語義分割中反應比較明顯。那麼我們要做的是在不改變網絡性能的情況下,儘量的壓縮模型,空出足夠的顯存進行GPU加速。


GPU基礎知識

我們利用gpustat工具包可以看到關於GPU的相關信息:

pip install gpustat

直接即可安裝,gpustat基於nvidia-smi,可以提供更美觀簡潔的展示,結合watch命令,可以動態實時監控GPU的使用情況。

watch --color -n1 gpustat -cpu

img

GPU計算單元類似於CPU中的核,用來進行數值計算。衡量計算量的單位是flop: the number of floating-point multiplication-adds,浮點數先乘後加算一個flop。計算能力越強大,速度越快。衡量計算能力的單位是flops: 每秒能執行的flop數量

1Byte = 8 bit
1K = 1024 Byte
1M = 1024 K
1G = 1024 M
1T = 1024 G

10 K = 10*1024 Byte

卷積神經網絡參數

在圖像處理中,往往把圖像表示爲像素的向量,比如一個1000×1000的圖像,可以表示爲一個1000000的向量。如果隱含層數目與輸入層一樣,即也是1000000時,那麼輸入層到隱含層的參數數據爲1000000×1000000=10^12,這樣就太多了,基本沒法訓練。卷積神經網絡通過CNN中的局部連接(Sparse Connectivity)和權值共享(Shared Weights)來減少參數。

神經網絡模型佔用的顯存包括:

  • 模型自身的參數
  • 模型的輸出

舉例來說,對於如下圖所示的一個全連接網絡(不考慮偏置項b)

                                                          img

模型的顯存佔用包括:

  • 參數:二維數組 W
  • 模型的輸出: 二維數組 Y
  • 其他

輸入X可以看成是上一層的輸出,因此把它的顯存佔用歸於上一層。


參數的顯存佔用

只有有參數的層,纔會有顯存佔用。這部份的顯存佔用和輸入無關,模型加載完成之後就會佔用。

有參數的層主要包括:

  • 卷積
  • 全連接
  • BatchNorm
  • Embedding層
  • ... ...

無參數的層

  • 多數的激活層(Sigmoid/ReLU)
  • 池化層
  • Dropout
  • ... ...

更具體的來說,模型的參數數目(這裏均不考慮偏置項b)爲:

  • Linear(M->N): 參數數目:M×N
  • Conv2d(Cin, Cout, K): 參數數目:Cin × Cout × K × K
  • BatchNorm(N): 參數數目: 2N
  • Embedding(N,W): 參數數目: N × W

參數佔用顯存 = 參數數目×n

n = 4 :float32

n = 2 : float16

n = 8 : double64

在PyTorch中,當你執行完model=MyGreatModel().cuda()之後就會佔用相應的顯存,佔用的顯存大小基本與上述分析的顯存差不多(會稍大一些,因爲其它開銷)。


梯度與動量的顯存佔用

舉例來說, 優化器如果是SGD:

                                                              

 

可以看出來,除了保存W之外還要保存對應的梯度,

                                                                            

因此顯存佔用等於參數佔用的顯存x2,

如果是帶Momentum-SGD

                                                                

這時候還需要保存動量, 因此顯存x3

如果是Adam優化器,動量佔用的顯存更多,顯存x4

總結一下,模型中與輸入無關的顯存佔用包括:

  • 參數 W
  • 梯度 dW(一般與參數一樣)
  • 優化器的動量(普通SGD沒有動量,momentum-SGD動量與梯度一樣,Adam優化器動量的數量是梯度的兩倍)

輸入輸出的顯存佔用

這部份的顯存主要看輸出的feature map 的形狀。

                                              img

比如卷積的輸入輸出滿足以下關係:

                                      img

據此可以計算出每一層輸出的Tensor的形狀,然後就能計算出相應的顯存佔用。

模型輸出的顯存佔用,總結如下:

  • 需要計算每一層的feature map的形狀(多維數組的形狀)
  • 需要保存輸出對應的梯度用以反向傳播(鏈式法則)
  • 顯存佔用與 batch size 成正比
  • 模型輸出不需要存儲相應的動量信息。

深度學習中神經網絡的顯存佔用,我們可以得到如下公式:

顯存佔用 = 模型顯存佔用 + batch_size × 每個樣本的顯存佔用

可以看出顯存不是和batch-size簡單的成正比,尤其是模型自身比較複雜的情況下:比如全連接很大,Embedding層很大

另外需要注意:

  • 輸入(數據,圖片)一般不需要計算梯度
  • 神經網絡的每一層輸入輸出都需要保存下來,用來反向傳播,但是在某些特殊的情況下,我們可以不要保存輸入。比如ReLU,在PyTorch中,使用nn.ReLU(inplace = True) 能將激活函數ReLU的輸出直接覆蓋保存於模型的輸入之中,節省不少顯存。感興趣的讀者可以思考一下,這時候是如何反向傳播的(提示:y=relu(x) -> dx = dy.copy();dx[y<=0]=0

在通過卷積獲得了特徵 (features) 之後,下一步我們希望利用這些特徵去做分類。理論上講,人們可以用所有提取得到的特徵去訓練分類器,例如 softmax 分類器,但這樣做面臨計算量的挑戰。例如:對於一個 96X96 像素的圖像,假設我們已經學習得到了400個定義在8X8輸入上的特徵,每一個特徵和圖像卷積都會得到一個 (96 − 8 + 1) × (96 − 8 + 1) = 7921 維的卷積特徵,由於有 400 個特徵,所以每個樣例 (example) 都會得到一個 7921 × 400 = 3,168,400 維的卷積特徵向量。學習一個擁有超過 3 百萬特徵輸入的分類器十分不便,並且容易出現過擬合 (over-fitting)。
爲了解決這個問題,首先回憶一下,我們之所以決定使用卷積後的特徵是因爲圖像具有一種“靜態性”的屬性,這也就意味着在一個圖像區域有用的特徵極有可能在另一個區域同樣適用。因此,爲了描述大的圖像,一個很自然的想法就是對不同位置的特徵進行聚合統計,例如,人們可以計算圖像一個區域上的某個特定特徵的平均值 (或最大值)。這些概要統計特徵不僅具有低得多的維度 (相比使用所有提取得到的特徵),同時還會改善結果(不容易過擬合)。這種聚合的操作就叫做池化 (pooling),有時也稱爲平均池化或者最大池化 (取決於計算池化的方法)。

參數細節

Convolution層:

就是卷積層,是卷積神經網絡(CNN)的核心層。
層類型:Convolution
    lr_mult: 學習率的係數,最終的學習率是這個數乘以solver.prototxt配置文件中的base_lr。如果有兩個lr_mult, 則第一個表示權值的學習率,第二個表示偏置項的學習率。一般偏置項的學習率是權值學習率的兩倍。
在後面的convolution_param中,我們可以設定卷積層的特有參數。
必須設置的參數:
    num_output: 卷積核(filter)的個數
    kernel_size: 卷積核的大小。如果卷積核的長和寬不等,需要用kernel_h和kernel_w分別設定
其它參數:
    stride: 卷積核的步長,默認爲1。也可以用stride_h和stride_w來設置。
    pad: 擴充邊緣,默認爲0,不擴充。 擴充的時候是左右、上下對稱的,比如卷積核的大小爲5*5,那麼pad設置爲2,則四個邊緣都擴充2個像素,即寬度和高度都擴充了4個像素,這樣卷積運算之後的特徵圖就不會變小。也可以通過pad_h和pad_w來分別設定。
      weight_filler: 權值初始化。 默認爲“constant",值全爲0,很多時候我們用"xavier"算法來進行初始化,也可以設置爲”gaussian"
      bias_filler: 偏置項的初始化。一般設置爲"constant",值全爲0。
      bias_term: 是否開啓偏置項,默認爲true, 開啓
      group: 分組,默認爲1組。如果大於1,我們限制卷積的連接操作在一個子集內。如果我們根據圖像的通道來分組,那麼第i個輸出分組只能與第i個輸入分組進行連接。
輸入:n*c0*w0*h0
輸出:n*c1*w1*h1
其中,c1就是參數中的num_output,生成的特徵圖個數
 w1=(w0+2*pad-kernel_size)/stride+1;
 h1=(h0+2*pad-kernel_size)/stride+1;
如果設置stride爲1,前後兩次卷積部分存在重疊。如果設置pad=(kernel_size-1)/2,則運算後,寬度和高度不變。


Pooling層
也叫池化層,爲了減少運算量和數據維度而設置的一種層。
層類型:Pooling
必須設置的參數:
       kernel_size: 池化的核大小。也可以用kernel_h和kernel_w分別設定。
其它參數:
     pool: 池化方法,默認爲MAX。目前可用的方法有MAX, AVE, 或STOCHASTIC
   pad: 和卷積層的pad的一樣,進行邊緣擴充。默認爲0
   stride: 池化的步長,默認爲1。一般我們設置爲2,即不重疊。也可以用stride_h和stride_w來設置。

pooling層的運算方法基本是和卷積層是一樣的。
輸入:n*c*w0*h0
輸出:n*c*w1*h1
和卷積層的區別就是其中的c保持不變
w1=(w0+2*pad-kernel_size)/stride+1;
h1=(h0+2*pad-kernel_size)/stride+1;
如果設置stride爲2,前後兩次卷積部分不重疊。100*100的特徵圖池化後,變成50*50.


節省顯存的方法

在深度學習中,一般佔用顯存最多的是卷積等層的輸出,模型參數佔用的顯存相對較少,而且不太好優化。

節省顯存一般有如下方法:

  • 降低batch-size
  • 下采樣(NCHW -> (1/4)*NCHW)
  • 減少全連接層(一般只留最後一層分類用的全連接層)

減少卷積層的計算量

今年穀歌提出的MobileNet,利用了一種被稱爲DepthWise Convolution的技術,將神經網絡運行速度提升許多,它的核心思想就是把一個卷積操作拆分成兩個相對簡單的操作的組合。如圖所示, 左邊是原始卷積操作,右邊是兩個特殊而又簡單的卷積操作的組合(上面類似於池化的操作,但是有權重,下面類似於全連接操作)。

                                          imgDepthwise Convolution

這種操作使得:

  • 顯存佔用變多(每一步的輸出都要保存)
  • 計算量變少了許多,變成原來的(\frac{1}{C_{out}} +\frac{1}{k^{2}})(一般爲原來的10-15%)

常用模型 顯存/計算複雜度/準確率

去年一篇論文(https://arxiv.org/abs/1605.07678)總結了當時常用模型的各項指標,橫座標是計算複雜度(越往右越慢,越耗時),縱座標是準確率(越高越好),圓的面積是參數數量(不是顯存佔用),參數量越多,保存的模型文件越大。左上角我畫了一個紅色小圓,那是最理想的模型:快,準確率高,顯存佔用小。

                     img


建議

  • 時間更寶貴,儘可能使模型變快(減少flop)
  • 顯存佔用不是和batch size簡單成正比,模型自身的參數及其延伸出來的數據也要佔據顯存
  • batch size越大,速度未必越快。在你充分利用計算資源的時候,加大batch size在速度上的提升很有限

尤其是batch-size,假定GPU處理單元已經充分利用的情況下:

  • 增大batch size能增大速度,但是很有限(主要是並行計算的優化)
  • 增大batch size能減緩梯度震盪,需要更少的迭代優化次數,收斂的更快,但是每次迭代耗時更長。
  • 增大batch size使得一個epoch所能進行的優化次數變少,收斂可能變慢,從而需要更多時間才能收斂(比如batch_size 變成全部樣本數目)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章