我的模型能跑多快——神經網路模型速度調研(一)

OLDPAN發表於2018-12-17

前言

對於神經網路,我們更多談的是其精度怎麼樣,有百分之零點幾的提升。但是如果談到速度的話,深度學習神經網路相比於傳統的演算法來說,速度恐怕沒有那麼快了。

那麼我們什麼時候需要提升速度呢?假如有以下的場景:

  • 將模型執行在手機上
  • 需要實時的場景,比如高速攝像機捕捉動作
  • 在嵌入式裝置上執行

對於有桌面級顯示卡這種利器來說,速度似乎很容易得到很快,但是放到以上這些裝置時,在有效的硬體上如果速度提升不上來,那麼所設計的演算法也就沒什麼用處了。

《我的模型能跑多快——神經網路模型速度調研(一)》

所謂提升速度,不談論硬體級別的優化,對於神經網路來說無非也就兩點:

  • 網路的設計
  • 輸入資料的大小

輸入資料大小我們姑且不談,而神經網路的設計這一點就顯得比較重要了,網路的設計可以細分為:網路模型權重的大小、網路執行過程中產生的中間變數的大小、網路設計中各種計算的執行速度等等這些都會對速度產生影響,一般來說,模型引數和模型速度是成正比的。

關於速度和精度來說,這往往是一個衡量,精度和速度一般無法兼顧,正如在工業界使用很火的YOLO和在學術界名聲遠揚的Mask-Rcnn,一個追求速度一個追求精度(當然速度的前提是精度在可接受範圍之內)。

《我的模型能跑多快——神經網路模型速度調研(一)》

運算量

接觸過ACM的童鞋一定知道時間複雜度空間複雜度這兩個概念,時間複雜度即衡量一個演算法執行的時間量級,而空間負責度則衡量一個演算法所佔用的空間大小。神經網路則類似,如何判斷一個網路的速度快不快,最直觀最直接地就是看其包含多少個浮點運算(當然與記憶體頻寬也有關係)。

與這個概念密切相關的就是FLOPS(Floating-point operations per second,每秒執行的浮點運算數)。現在的桌面級顯示卡,大部分都是TFLOPs級別了,1TFLOP也就是每秒執行1,000,000,000,000次浮點運算。

矩陣乘法

在神經網路中,最常見的就是矩陣乘法:

正如下方的輸入4×4的影象,卷積核為3×3,輸出為2×2:

《我的模型能跑多快——神經網路模型速度調研(一)》

在計算機中將上述運算分解為:

《我的模型能跑多快——神經網路模型速度調研(一)》

結果中一個標量的計算過程可以用公式表示為:

y = w[0]*x[0] + w[1]*x[1] + w[2]*x[2] + ... + w[n-1]*x[n-1]
複製程式碼

w和x都是向量,w是權重,x是輸入。最終的結果是y是一個標量(scalar)。這個運算稱作multipy-accumulate operations。而其中的一個操作w[0]*x[0] + ..(先乘後加)稱為一個MACC(multipy-accumulate operation)。而上面的計算一共包含n個MACCs。

簡而言之,兩個n維向量的點乘所需要n個MACCs運算(其實可以說是n-1個,因為第一個不算,但是我們近似地認為是n個)。而n個MACCs運算包括2n-1個FLOPs(n個乘法和n-1個加法),我們近似為2n個FLOPs。

也就是說,兩個n維向量的乘積所需要的FLOPs是2n個。

當然,在很多的硬體設施中(比如顯示卡),一個MACC就可以稱作一個運算單位了,而不是將加法和乘法分開,因為硬體已經對其進行了大量的優化,我們之後在測一個卷積運算量就可以按照MACC這樣的單位來計算了。

全連線層

全連線層是除了卷積層最常見的層,在全連線層中,輸入數量為I和輸出數量為O,這些節點一一相連,然後權重W儲存在I x J的矩陣中,於是對於一個全連線層來說,其計算量為:

y = matmul(x,W) + b
複製程式碼

《我的模型能跑多快——神經網路模型速度調研(一)》
(來自:leonardoaraujosantos.gitbooks.io/artificial-…)

在上面的這個式子中(結合上圖),我們的x維數I為3,x是一個3維的向量,輸出y是二維的向量,因此權重W的數量就是3 x 2,最後加上偏置量b

那我們要計算全連線層一共執行了幾個MACC,首先看全連線層中的運算matmul。這是一個矩陣運算。

矩陣運算說白了就是一堆乘法和加法的集合,我們輸入的維度是I輸出維度是O,其次中間的W的維度為I x O(在上圖中是3x2)那麼很簡單,我們一共要做的就是I x O個MACCs,可以發現和權重矩陣的數量是一樣的。

哦,還有個bias偏置b沒有算裡頭,這個其實可以忽略不計了,在平時的計算量中這個偏置直接就不算了。

我們在看一些全連線層計算公式的時候,可能會發現計算中將偏置移到了矩陣中而不是先矩陣運算完再加偏置向量。也就是執行了一個 (I + 1) x O的矩陣運算,這個就是為了計算步驟簡便一些,對計算量沒有任何影響。

也就是說,加入我們的全連線層有100個輸入,200個輸出,那麼一共執行了100 x 200 = 20,000個MACCs。

通常,輸入I向量輸出O向量的時候,執行了I x J個MACCs和(2I - 1) x J個FLOPs。

全連線層就是向量之前的運算,通常會將全連線層放在卷積層的後面,而我們在程式設計計算這些值的時候都要對卷積後的值進行Flatten操作,相比大家應該很熟悉了,Flatten就是將一個(N,C,H,W)的張量變形為(N,I)的形狀,從而去執行全連線運算。

啟用函式

通常我們會在卷積層或者全連線層之後加一個非線性的啟用函式,比如RELU或者Sigmoid。在這裡我們使用FLOPs去衡量其計算量,因為啟用函式不涉及到點乘操作,所以用不到MACCs

對於RELU來說:

y = max(x, 0)
複製程式碼

x為輸入,這裡的輸入就是其他層的輸出,假如其它層傳遞給RELU層n個向量,那麼RELU層對這n個向量進行計算,也就是n個FLOPs。

對於sigmoid來說:

y = 1 / (1 + exp(-x))
複製程式碼

上式包含了一個加法、一個減法、一個除法和一個取冪運算,我們將這些運算都歸結為一個單獨的FLOP(還有乘法、求根號等)。因此一個sigmoid的運算量為4個FLOPs。假如輸入時n那個計算量為4 x n個FLOPs。

但一般我們只關心比較大的矩陣運算,像這種計算量一般也就忽略了。

卷積層

卷積層中主要的處理物件不是之前提到的向量,而是我們平常見到的(C,H,W)三通道的張量,其中C代表通道數,HW代表這個特徵圖的高和寬。

對於一個kernel為K的卷積層來說(這裡只說方形的卷積層,我們平時用到的也都是方形的卷積),所需要的MACCs為:

K  x  K  x  Cin  x  Hout  x  Wout  x  Cout
複製程式碼

怎麼來的:

  • 輸出的特徵圖大小為Hout x Wout,由計算中的每個畫素點組成
  • 權重(weights)和輸入特徵圖的計算的視窗大小為K x K
  • 輸入特徵圖的通道數為Cin
  • 對應每一個通道的卷積產生的通道數為Cout

這裡忽略了偏置,通常我們在計算引數時會算上偏置,但是在計算FLOPs則直接忽略。

舉個例子,假如輸入三通道256*256的影象,使用的卷積核大小為3,卷積的層數為128,計算總共的運算量為:

256 x 256 x 3 x 3 x 3 x 128 = 226,492,416 
複製程式碼

差不多226M-FLOPs,計算量還是蠻大的。

以上使用的stride為1,也就是每隔一步在特徵圖上進行卷積操作,如果上述的卷積層的strid為2,那麼相當於在一半大小的影象中進行卷積,上面的256×256則變成128×128

深度可分離卷積結構

深度可分離的卷積構架是眾多高效網路的基本結構,例如MobileNetXception。都採用了depthwise-separable convolution的網路結構,該網路結構並不複雜,可以分為兩個部分:

《我的模型能跑多快——神經網路模型速度調研(一)》

(來源於 machinethink.net/blog/mobile… )

需要注意下,下文中的深度可分離卷積對應是Depthwise Separable Convolution,它分別兩個部分,分別是深度分離(depthwise)的卷積和點(pointwise)卷積(也就是所謂的1×1卷積)。

其中深度分離的卷積運算和普通的運算類似,只不過不再將三個通道(RGB)變成一個通道了(普通卷積核一般是對影象的三通道分別進行卷積再相加化為一個通道),這次是直接三個通道輸入三個通道輸出,也就是對應三個獨立引數,不同引數內容的卷積,每一個卷積核對應一個通道(輸入一個通道,輸出一個通道)。

有一個稱之為 depthwise channel multiplier 的概念,也就是深度分離通道放大器,如果這個放大器大於1,比如為5,那麼一個卷積核就相當於輸入一個通道輸出5個通道了,這個引數就是調整模型大小的一個超引數。

執行的運算次數為:

K x K x C X Hout X Wout

注意相比之前普通的卷積運算可以說少乘了個C,運算量可以說是大大提升了。

舉個例子,比如利用3x3的深度可分離卷積去對一張112 x 112的特徵圖做卷積操作,通道為64,那麼我們所需要的MACCs為:

3 x 3 x 64 x 112 x 112 = 7,225,344
複製程式碼

對於點(pointwise)卷積運算來說,需要的運算量為:

Cin X Hout X Wout X Cout
複製程式碼

這裡的K,核大小為1。

同樣舉個例子,假如我們有個112x112x64維數的資料,我們利用點分離卷積將其投射到128維中,去建立一個112x112x128維數的資料,這時我們需要的MACCs為:

64 x 112 x 112 x 128 = 102,760,448
複製程式碼

可以看到點分離運算所需要的運算量還大於深度分離運算。

我們將上述兩個運算加起來和普通的3x3卷積操作運算相比:

3×3 depthwise          : 7,225,344
1×1 pointwise          : 102,760,448
depthwise separable    : 109,985,792 MACCs

regular 3×3 convolution: 924,844,032 MACCs
複製程式碼

可以發現速度提升了8倍(8.4)多~

但是這樣比較有點不是很公平,因為普通的3x3卷積學習到的資訊更加完整,可以學習到更多的資訊,但是我們要知道在同等的計算量下,相比傳統的3x3卷積,我們可以使用8個多的深度可分離卷積,這樣比下來差距就顯現出來了。

關於模型中的引數量計算請看這篇文章:淺談深度學習:如何計算模型以及中間變數的視訊記憶體佔用大小

我們整理一下,深度可分離具體需要的MACCs為:

(K x K x Cin X Hout X Wout) + (Cin x Hout x Wout x Cout)
複製程式碼

簡化為:

Cin x Hout x Wout X (K x K + Cout)
複製程式碼

如果我們將其跟普通的3x3卷積對比的話就會發現,上式最後的+ Cout在普通的3x3卷積中為x Cout。就這個小小的差別造成了效能上極大的差異。

深度可分離卷積核傳統的卷積的提速比例可以認為為K x K(也就是卷積越大,提速越快),上面我們按照3x3卷積舉例發現提速8.4倍,其實和3x3=9倍是相差無幾的。

其實實際上的提速比例是:K x K x Cout / (K x K + Cout)
另外需要注意的是,深度可分離卷積也可以像傳統卷積一樣,使用stride大於1,當這個時候深度可分離卷積的第一部分輸出的特徵大小會下降,而深度可分離的第二部分點卷積則保持輸入卷積的維度。

上面介紹的深度可分離卷積是MobileNet V1中的經典結構,在MobileNet V2中,這個結構稍微變化了一下下,具體來說就是多了一個擴張和縮小的部分:

  • 第一個部分是1×1卷積,這個卷積用來在輸入特徵影象上新增更多的通道(這個可以理解為擴張層-expansion_layer)
  • 第二個部分就是已經提到的3×3深度分離卷積(depthwise)
  • 第三部分又是一個1×1卷積,這個卷積用來減少輸入特徵影象上的通道(這個稱之為投射層-projection_layer,也就是所謂的瓶頸層-bottleneck convolution)

再討論下上面這個結構的計算數量:

Cexp = (Cin × expansion_factor)

expansion_layer = Cin × Hin × Win × Cexp

depthwise_layer = K × K × Cexp × Hout × Wout

projection_layer = Cexp × Hout × Wout × Cout
複製程式碼

上式中的Cexp代表擴張層擴張後的層數,雖然不論是擴張層還是瓶頸層都不會改變特徵圖的H和W,但是其中的深度分離層如果stride大於1的話會發生改變,所以這裡的Hin WinHout Wout有時候會不同。

將上式進行簡化:

Cin x Hin X Win X Cexp + (K x K + Cout) x Cexp x Hout x Wout
複製程式碼

當stride=1的時候,上式簡化為:(K x K + Cout + Cin) x Cexp x Hout x Wout

和之前MobileNet V1版的深度可分離卷積對比一下,我們同樣使用112x112x64作為輸入,取擴張引數(expansion_factor)為6,3x3的深度分離卷積的stride為1,這時V2版的計算量為:

(3 × 3 + 128 + 64) × (64 × 6) × 112 × 112 = 968,196,096
複製程式碼

可以發現,這個計算量貌似比之前的V1版大了很多,而且比普通的3x3卷積都大了不少,為什麼,原因很簡單,我們設定了擴張係數為6,這樣的話我們計算了64 x 6 = 384個通道,比之前的64 -> 128學習到更多的引數,但是計算量卻差不多。

批標準化-BatchNorm

批標準化可以說是現代神經網路中除了卷積操作之外必不可少的操作了,批標準化通常是放在卷積層或者全連線層之後,啟用函式之前。對於上一個層中輸出的y來說,批標準化採取的操作為:

z = gamma * (y - mean) / sqrt(variance + epsilon) + beta
複製程式碼

首先將上一次輸出的y進行標準化(減去其平均值並處以方差,這裡的epsilon是0.001從而避免計算問題)。但是我們又將標準化後的數於gamma相乘,加上beta。這兩個引數是可學習的。

也就是對於每個通道來說,我們需要的引數為4個,也就是對於C個通道,批標準化需要學習C x 4個引數。

看來貌似需要計算的引數還不少,但是實際中我們還可以對其進行優化,將批標準化和卷積或者全連線層合併起來,這樣的話速度會進一步提升,這裡暫時先不討論。

總之,我們在討論模型計算量的時候,一般不討論批標準化產生的計算量,因為我們在inference的時候並不使用它。

其他層

除了上述的一些基本層之外(卷積,全連線,特殊卷積,批標準化),池化層也會產生一部分計算量,但是相比卷積層和全連線層池化層產生的也可以忽略不計了,而且在新型的神經網路的設計中,池化層可以通過卷積層進行代替,所以我們一般來說對這些層並不著重討論。

下一步

這篇文章僅僅是討論了一些模型計算量的問題,一個網路執行的快否,與不僅與網路的計算量有關,網路的大小、網路引數精度的高低、中間變數的優化以及混合精度等等都可以作為提速的一部分,限於篇幅將在下一部分進行討論。

文章來源於OLDPAN部落格,歡迎來訪:Oldpan部落格

歡迎關注Oldpan部落格公眾號,持續醞釀深度學習質量文:

我的模型能跑多快——神經網路模型速度調研(一)


相關文章