im2col:將卷積運算轉為矩陣相乘


博客:blog.shinelee.me | 博客園 | CSDN

im2col實現

如何將卷積運算轉為矩陣相乘?直接看下面這張圖,以下圖片來自論文High Performance Convolutional Neural Networks for Document Processing

im2col
上圖為3D卷積的傳統計算方式與矩陣乘法計算方式的對比,傳統卷積運算是將卷積核以滑動窗口的方式在輸入圖上滑動,當前窗口內對應元素相乘然后求和得到結果,一個窗口一個結果。相乘然后求和恰好也是向量內積的計算方式,所以可以將每個窗口內的元素拉成向量,通過向量內積進行運算,多個窗口的向量放在一起就成了矩陣,每個卷積核也拉成向量,多個卷積核的向量排在一起也成了矩陣,於是,卷積運算轉化成了矩陣運算。

下圖為轉化后的矩陣尺寸,padding為0:
EmzaRO.png
代碼上怎么實現呢?這里參看一下SeetaFaceEngine/FaceIdentification/src/conv_net.cpp 中的代碼,與上面的圖片對照着看比較直觀。

int dst_h = (src_h - kernel_h) / stride_h_ + 1; // int src_h = input->height(); int kernel_h = weight->height();
int dst_w = (src_w - kernel_w) / stride_w_ + 1; // int src_w = input->width(); int kernel_w = weight->width();
int end_h = src_h - kernel_h + 1;
int end_w = src_w - kernel_w + 1;
int dst_size = dst_h * dst_w;
int kernel_size = src_channels * kernel_h * kernel_w;

const int src_num_offset = src_channels * src_h * src_w; // int src_channels = input->channels();
float* const dst_head = new float[src_num * dst_size * dst_channels];
float* const mat_head = new float[dst_size * kernel_size];

const float* src_data = input->data().get();
float* dst_data = dst_head;
int didx = 0;

for (int sn = 0; sn < src_num; ++sn) {
  float* mat_data = mat_head;
  for (int sh = 0; sh < end_h; sh += stride_h_) {
    for (int sw = 0; sw < end_w; sw += stride_w_) {
      for (int sc = 0; sc < src_channels; ++sc) {
        int src_off = (sc * src_h + sh) * src_w + sw;
        for (int hidx = 0; hidx < kernel_h; ++hidx) {
          memcpy(mat_data, src_data + src_off,
                  sizeof(float) * kernel_w);
          mat_data += kernel_w;
          src_off += src_w;
        }
      } // for sc
    } // for sw
  } // for sh
  src_data += src_num_offset;

  const float* weight_head = weight->data().get();
  // int dst_channels = weight->num();
  matrix_procuct(mat_head, weight_head, dst_data, dst_size, dst_channels, 
    kernel_size, true, false);
    
  dst_data += dst_channels * dst_size;
} // for sn

src_num 個輸入,每個尺寸為 src_channels * src_h * src_w,卷積核尺寸為kernel_size = src_channels * kernel_h * kernel_w,將每個輸入轉化為二維矩陣,尺寸為(dst_h * dst_w) * (kernel_size),可以看到最內層循環在逐行拷貝當前窗口內的元素,窗口大小與卷積核大小相同,一次拷貝kernel_w個元素,一個窗口內要拷貝src_channels*kernel_h次,因此一個窗口共拷貝了kernel_size個元素,共拷貝dst_h * dst_w個窗口,因此輸入對應的二維矩陣尺寸為(dst_h * dst_w) * (kernel_size)。對於卷積核,有dst_channels= weight->num();個卷積核,因為是行有先存儲,卷積核對應的二維矩陣尺寸為dst_channels*(kernel_size)邏輯上雖然為矩陣乘法,實現時兩個矩陣逐行內積即可

優缺點分析

將卷積運算轉化為矩陣乘法,從乘法和加法的運算次數上看,兩者沒什么差別,但是轉化成矩陣后,運算時需要的數據被存在連續的內存上,這樣訪問速度大大提升(cache),同時,矩陣乘法有很多庫提供了高效的實現方法,像BLAS、MKL等,轉化成矩陣運算后可以通過這些庫進行加速。

缺點呢?這是一種空間換時間的方法,消耗了更多的內存——轉化的過程中數據被冗余存儲。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM