深入理解卷積與模型大小問題,解決顯存不足
在訓練自己的模型時常常出現顯存不足等問題,這個時候我們常用的方法就是調參。一般常用的方法有以下幾點:
模型壓縮
網絡參數調整, 比如減小訓練圖像大小,降低FC output個數,使用小的conv kernel size等。
深度學習框架調整
減層
但是對於既定網絡,我們減小訓練圖像大小或者改變batchsize大小都會影響模型的性能,這點在目標檢測和語義分割中反應比較明顯。那么我們要做的是在不改變網絡性能的情況下,盡量的壓縮模型,空出足夠的顯存進行GPU加速。
GPU基礎知識
我們利用gpustat工具包可以看到關於GPU的相關信息:
pip install gpustat
直接即可安裝,gpustat基於nvidia-smi,可以提供更美觀簡潔的展示,結合watch命令,可以動態實時監控GPU的使用情況。
watch --color -n1 gpustat -cpu
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)
模型的顯存占用包括:
參數:二維數組 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 的形狀。
比如卷積的輸入輸出滿足以下關系:
據此可以計算出每一層輸出的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的技術,將神經網絡運行速度提升許多,它的核心思想就是把一個卷積操作拆分成兩個相對簡單的操作的組合。如圖所示, 左邊是原始卷積操作,右邊是兩個特殊而又簡單的卷積操作的組合(上面類似於池化的操作,但是有權重,下面類似於全連接操作)。
Depthwise Convolution
這種操作使得:
顯存占用變多(每一步的輸出都要保存)
計算量變少了許多,變成原來的( +)(一般為原來的10-15%)
常用模型 顯存/計算復雜度/准確率
去年一篇論文(https://arxiv.org/abs/1605.07678)總結了當時常用模型的各項指標,橫座標是計算復雜度(越往右越慢,越耗時),縱座標是准確率(越高越好),圓的面積是參數數量(不是顯存占用),參數量越多,保存的模型文件越大。左上角我畫了一個紅色小圓,那是最理想的模型:快,准確率高,顯存占用小。
建議
時間更寶貴,盡可能使模型變快(減少flop)
顯存占用不是和batch size簡單成正比,模型自身的參數及其延伸出來的數據也要占據顯存
batch size越大,速度未必越快。在你充分利用計算資源的時候,加大batch size在速度上的提升很有限
尤其是batch-size,假定GPU處理單元已經充分利用的情況下:
增大batch size能增大速度,但是很有限(主要是並行計算的優化)
增大batch size能減緩梯度震盪,需要更少的迭代優化次數,收斂的更快,但是每次迭代耗時更長。
增大batch size使得一個epoch所能進行的優化次數變少,收斂可能變慢,從而需要更多時間才能收斂(比如batch_size 變成全部樣本數目)。