>加入極市CV技..." />
<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一種更加高效的卷積計算策略:Im2Col+GEMM的改進方法MEC

          共 5500字,需瀏覽 11分鐘

           ·

          2020-10-14 22:51

          ↑ 點擊藍字?關注極市平臺

          作者丨BBuf@知乎
          來源丨h(huán)ttps://zhuanlan.zhihu.com/p/264554159
          編輯丨極市平臺

          極市導讀

          ?

          本文介紹了一種Im2Col+GEMM的改進版本——MEC,并記錄了對它進行復現嘗試后的測試結果。>>加入極市CV技術交流群,走在計算機視覺的最前沿


          大家好,國慶閑在家里偶然看到了這篇對Im2Col+GEMM實現卷積進行改進的文章,然后去讀了讀順便實現了一下,并在本文給出了一個測試結果。從結果來看,這個算法在速度和內存消耗都優(yōu)于Im2Col+GEMM方式的卷積,想更多的知識和BenchMark可以閱讀本文和原始論文。
          原論文地址:https://arxiv.org/abs/1706.06873v1
          本文復現代碼開源在:https://github.com/BBuf/Memory-efficient-Convolution-for-Deep-Neural-Network

          1. 前言

          前面介紹了Im2Col+GEMM來實現卷積以在某些條件下獲得更好的訪存和計算效率:詳解Im2Col+Pack+Sgemm策略更好的優(yōu)化卷積運算 。然后,最近偶然發(fā)現了Im2Col+GEMM的一個改進版本即MEC: Memory-efficient Convolution for Deep Neural Network ,這是發(fā)表在ICML 2017的文章,它主要優(yōu)化了Im2Col+GEMM計算策略中的內存消耗,并且也能提升一點速度,是一個不錯的卷積加速算法。所以我在這里結合論文然后復現了一下代碼實現來為分享一下。

          2. 背景介紹

          當前的CNN模型一般只在最后一層使用全連接層,也即是說CNN的主體是由卷積層構成的。因此,對卷積層的加速對整個網絡的性能非常關鍵。目前,對卷積層的計算一般有以下幾種方式:
          Im2Col+GEMM。Caffe/DarkNet/MxNet多種框架都使用了這種計算方法,因為將卷積操作轉化為矩陣運算之后就可以方便的使用很多矩陣加速庫如MKL,OpenBlas,Eigen等等。想詳細了解這個算法的讀者可以點一下上一節(jié)的鏈接。
          WinoGrad算法。前面已經詳細介紹過WinoGrad這種卷積加速算法,請點擊:詳解卷積中的Winograd加速算法
          FFT加速。時域的卷積等于頻域的乘積,我們可以把卷積運算轉換為一個簡單的乘法問題,這個并不是很多見,后面有時間我會考慮給大家分享一下如何用FFT完成卷積層計算加速的。
          例如FaceBook研發(fā)的NNPACK加速包就是將FFT和WinoGrad進行結合,來對卷積運算進行加速。
          從Im2Col+GEMM和WinoGrad的實現來看,雖然他們的執(zhí)行效率都相比原始的卷積實現有所提升,但它們的內存占用都比較高,因為要存儲中間結果比如Im2Col的轉換結果(如Figure1所示),WinoGrad中的G,V,A矩陣等。

          所以,MEC改進了Im2Col+GEMM的策略,目的是減少它的內存消耗同時提升一點速度。

          3. MEC算法原理

          在正式介紹這個算法之前先規(guī)定一些數學符號以及它代表的含義,如Table1所示:

          3.1 MEC算法初級版
          下面先介紹一下MEC算法的初級版本,我們以輸入特征圖大小為,輸入卷積核大小為以及滑動步長為1的卷積層為例,這里先不考慮BatchSize和Channel維度,即我們只需要將輸入特征圖當成一個通道數為的普通的矩陣即可,如Figure2所示:

          下面的Algorithm1展示了這個算法的流程:
          MEC算法初級版本
          我們要結合Figure2來看一下這個偽代碼,這里的意思就是說:
          因為是卷積,并且滑動步長為,所以這里循環(huán)取出A,B,C,D,E這5個子矩陣(在Figure2中看),每個子矩陣的維度都是,Figure2中代表特征圖的高度即

          將A,B,C,D,E按照行優(yōu)先展開并拼成一個大的中間矩陣的維度是:。
          從L中循環(huán)取出,,,,個子矩陣,并計算次矩陣乘法,就獲得了最終的輸出特征圖
          從上面的介紹中我們可以看到,MEC將Im2Col的過程分成了Height和Width兩部分,于是需要存儲的中間矩陣也大大減少了,因為如果是Im2Col,那么展開之后的矩陣大小是,但是現在MEC需要的矩陣大小只有,所以內存消耗降低了2倍。但是這樣做可能帶來的問題是,Im2Col+GEMM的一次矩陣乘法現在變成了多次小矩陣乘法,雖然這對并行計算是有利的,但如果使用OpenBlas庫來計算則失去了它對大矩陣乘法計算的優(yōu)勢,所以從工程實現角度來說要達到論文給出的BenchMark那么高可能還需要一些奇淫技巧。

          3.2 MEC算法高級版

          將BatchSize和Channel維度加以考慮之后就獲得了MEC算法的高級版,如Algorithm2所示:

          然后下面的Figure3是它的示例圖:

          從偽代碼里可以看到這里有2種計算方法:
          • Solution 1:Algorithm2中的第9-19行和Algorithm1中的方法完全一致,然后14-19行是對臨時結果對做排列變化,即Figure3中的上半部分。
          • Solution 2:Algorithm2中的第21-25行。每次循環(huán)處理一個樣本,不需要做額外的排列變化,即Figure3中的下半部分。
          這兩種計算方法的浮點乘法計算次數是完全一樣的。但是,在實際操作中,子矩陣的數量對性能的影響是很大的,在Solution1中執(zhí)行了次gemm,而Solution2中執(zhí)行了次gemm,如果使用Blas矩陣計算庫,那么這兩種方法在特定硬件平臺如GPU上哪一種更好是需要考慮的。因此,在Algorithm2的第8行使用了一個參數T來控制是使用Solution1還是Solution2,其中T是一個和硬件平臺有關的參數。論文指出,如果是在GPU上,那么T取是一個不錯的值。

          4. 實驗對比

          論文使用C++在CPU/GPU上分別進行了實現以及性能測試,矩陣計算庫使用了多線程OpenBlas,OpenMP,cuBLAS,數據類型為float32。下面的Table2展示了BenchMark使用的網絡結構:
          然后,下面是一些卷積加速算法和硬件平臺綁定后的簡稱:
          一些卷積加速算法和硬件平臺綁定后的簡稱
          最后,我們直接抬出實驗結果
          實驗結果
          從實驗結果可以看到,無論是從內存占用還是運算時間都相比于WinoGrad,Im2Col+GEMM,FFT有一些優(yōu)勢,不過這里并沒有給出更多的實驗對比結果例如和NNPACK以及CUDNN的對比。

          5. 復現嘗試(暫時只針對X86 CPU)

          從理論介紹來看,這個算法實現起來也相對比較簡單,接下來我就嘗試實現一下這個算法。這個算法最核心的部分就是Im2Col以及MEC這種改進后的Im2Col方式,然后這個地方我是在x86平臺上進行了復現和測試,所以選用了OpenBlas加速庫來完成Im2Col后的矩陣運算。這個地方實現的是單通道的輸入特征圖(可以看成一張灰度圖),分辨率是,然后卷積核是,然后通道數是64,這里直接隨意給輸入特征圖和卷積核賦了值,因為這里只需要對比一下這兩種實現的速度以及內存消耗。
          我們首先來看Im2Col的實現:
          // 原始的Im2Col
          void im2col_cpu(float** src, const int &inHeight, const int &intWidth, const int &kHeight,
          const int &kWidth, float* srcIm2col){
          const int outHeight = inHeight - kHeight + 1;
          const int outWidth = intWidth - kWidth + 1;
          int cnt = 0;
          for(int i = 0; i < kHeight; i++){
          for(int j = 0; j < kWidth; j++){
          int id = i * kWidth + j;
          int ii = i;
          for(int x = 0; x < outHeight; x++){
          int jj = j;
          for(int y = 0; y < outWidth; y++){
          srcIm2col[cnt] = src[ii][jj];
          jj += 1;
          cnt++;
          }
          ii += 1;
          }
          }
          }
          }
          結合一下文章開頭部分的Im2Col的圖可以更好的理解,這個地方就是將輸入特征圖通過Im2Col的方式放到一個數組里面,為什么是個數組而不是二維矩陣呢?這里只是將這個二維矩陣存成了一個數組,來方便后面調用cblas_sgemm接口,關于OpenBlas的介紹以及計算方式,函數接口可以查看參考中的資料2,這里就不過多介紹了。
          然后對卷積核同樣手動Im2Col即可:
          // 構造輸入矩陣
          float **src = new float*[inHeight];
          for(int i = 0; i < inHeight; i++){
          src[i] = new float[inWidth];
          for(int j = 0; j < inWidth; j++){
          src[i][j] = 0.1;
          }
          }
          // 構造kernel矩陣
          float **kernel[kernel_num];
          for(int i = 0; i < kernel_num; i++){
          kernel[i] = new float*[kernel_h];
          for(int j = 0; j < kernel_h; j++){
          kernel[i][j] = new float[kernel_w];
          for(int k = 0; k < kernel_w; k++){
          kernel[i][j][k] = 0.2;
          }
          }
          }

          // 開始計時
          struct timeval tstart, tend;
          gettimeofday(&tstart, NULL);

          // 對kernel進行Im2col
          float* kernel2col = new float[kernel_num*kernel_h*kernel_w];
          int cnt = 0;
          for(int i = 0; i < kernel_num; i++){
          for(int j = 0; j < kernel_h; j++){
          for(int k = 0; k < kernel_w; k++){
          kernel2col[cnt++] = kernel[i][j][k];
          }
          }
          }
          // 對輸入矩陣Im2col
          int outHeight = inHeight - kernel_h + 1;
          int outWidth = inWidth - kernel_w + 1;
          float *srcIm2col = new float[kernel_w * kernel_h * outWidth * outHeight];
          im2col_cpu(src, inHeight, inWidth, kernel_h, kernel_w, srcIm2col);
          接下來調用cblas_sgemm函數接口即可完成卷積層的計算,這個地方加入了計時函數,統(tǒng)計Im2Col+gemm的運行時間:
          // 使用Blas庫實現矩陣乘法
          float *output = new float[kernel_num * outHeight * outWidth];
          cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,kernel_num,
          outHeight*outWidth, kernel_w*kernel_h, 1,
          kernel2col, kernel_h*kernel_w,
          srcIm2col,outHeight * outWidth, 0, output, outHeight * outWidth);

          // 結束計時
          gettimeofday(&tend, NULL);
          cout<<"im2colOrigin Total time cost: "<<(tend.tv_sec-tstart.tv_sec)*1000 + (tend.tv_usec-tstart.tv_usec)/1000<<" ms"<
          在復現MEC的時候主要是對Im2Col的修改,代碼實現如下,可以結合原理介紹中的圖示進行查看,就很好理解了。
          // MEC
          void im2col_mec(float** src, const int &inHeight, const int &intWidth, const int &kHeight,
          const int &kWidth, float* srcIm2col){
          const int outHeight = inHeight - kHeight + 1;
          const int outWidth = intWidth - kWidth + 1;
          #pragma omp parallel for num_threads(THREAD_NUM)
          for(int i = 0; i < outWidth; i++){
          int outrow = 0;
          for(int j = 0; j < inHeight; j++){
          for(int k = i; k < i + kWidth; k++){
          srcIm2col[outrow * outWidth + i] = src[j][k];
          outrow++;
          }
          }
          }
          }
          然后對Kernel的Im2col過程完全一致,只是在求輸出矩陣的時候要用for循環(huán)去遍歷一下每個子矩陣(這個地方是5個,參考一下原理部分的圖),代碼如下:
          // 構造輸入矩陣
          float **src = new float*[inHeight];
          for(int i = 0; i < inHeight; i++){
          src[i] = new float[inWidth];
          for(int j = 0; j < inWidth; j++){
          src[i][j] = 0.1;
          }
          }
          // 構造kernel矩陣
          float **kernel[kernel_num];
          for(int i = 0; i < kernel_num; i++){
          kernel[i] = new float*[kernel_h];
          for(int j = 0; j < kernel_h; j++){
          kernel[i][j] = new float[kernel_w];
          for(int k = 0; k < kernel_w; k++){
          kernel[i][j][k] = 0.2;
          }
          }
          }

          // 開始計時
          struct timeval tstart, tend;
          gettimeofday(&tstart, NULL);

          // 對kernel進行Im2col
          float* kernel2col = new float[kernel_num*kernel_h*kernel_w];
          int cnt = 0;
          for(int i = 0; i < kernel_num; i++){
          for(int j = 0; j < kernel_h; j++){
          for(int k = 0; k < kernel_w; k++){
          kernel2col[cnt++] = kernel[i][j][k];
          }
          }
          }
          // 對輸入矩陣Im2col
          int outHeight = inHeight - kernel_h + 1;
          int outWidth = inWidth - kernel_w + 1;
          float *srcIm2col = new float[outWidth * inHeight * kernel_w];
          im2col_mec(src, inHeight, inWidth, kernel_h, kernel_w, srcIm2col);

          // 使用Blas庫實現矩陣乘法
          float **output = new float*[outHeight];

          #pragma omp parallel for num_threads(THREAD_NUM)
          for(int i = 0; i < outHeight; i++){
          output[i] = new float [kernel_num * outWidth];
          cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,kernel_num,
          outWidth, kernel_w * kernel_h,1,
          kernel2col, kernel_h * kernel_w,
          srcIm2col + i * outWidth, outWidth, 0, output[i], outWidth);
          }

          // 結束計時
          gettimeofday(&tend, NULL);
          cout<<"MEC Total time cost: "<<(tend.tv_sec-tstart.tv_sec)*1000 + (tend.tv_usec-tstart.tv_usec)/1000<<" ms"<
          這里實現的是MEC算法的初級版本,沒有考慮Channel維度,對這個算法感興趣的同學可以自行復現新增Channel維度的高級版算法。
          本文的完整代碼以及如何在X86編譯運行請查看github:https://github.com/BBuf/Memory-efficient-Convolution-for-Deep-Neural-Network

          6. 效果

          測試了一下復現的MEC和原始的Im2Col+GEMM的速度和內存消耗,結果如下:
          可以看到,在單線程的條件下,MEC的內存消耗明顯比原始的Im2Col+GEMM更好并且速度也有一些提升。另外如果啟用多線程那么速度也會有大幅度提升,注意在原始實現的版本中因為只有一個GEMM,所以整個算法無法并行。而在MEC的版本中拆成了5次GEMM,這對并行計算更加友好,因此獲得了可觀的加速,并且內存占用仍然優(yōu)于原始版本。

          參考資料


          https://blog.csdn.net/shuzfan/article/details/77427979
          https://blog.csdn.net/yutianzuijin/article/details/90411622
          https://github.com/CSshengxy/MEC


          推薦閱讀



          ?ACCV 2020國際細粒度網絡圖像識別競賽正式開賽!



          添加極市小助手微信(ID : cvmart2),備注:姓名-學校/公司-研究方向-城市(如:小極-北大-目標檢測-深圳),即可申請加入極市目標檢測/圖像分割/工業(yè)檢測/人臉/醫(yī)學影像/3D/SLAM/自動駕駛/超分辨率/姿態(tài)估計/ReID/GAN/圖像增強/OCR/視頻理解等技術交流群:月大咖直播分享、真實項目需求對接、求職內推、算法競賽、干貨資訊匯總、與?10000+來自港科大、北大、清華、中科院、CMU、騰訊、百度等名校名企視覺開發(fā)者互動交流~

          △長按添加極市小助手

          △長按關注極市平臺,獲取最新CV干貨

          覺得有用麻煩給個在看啦~??
          瀏覽 66
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩狠狠| 色欲综合一区二区 | 青青草视频精品 | 中文字幕无码不卡免费视频 | 精品视频免费 |