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

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

im2col實現

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

im2col
上圖爲3D卷積的傳統計算方式與矩陣乘法計算方式的對比,傳統卷積運算是將卷積核以滑動窗口的方式在輸入圖上滑動,當前窗口內對應元素相乘而後求和獲得結果,一個窗口一個結果。相乘而後求和剛好也是向量內積的計算方式,因此能夠將每一個窗口內的元素拉成向量,經過向量內積進行運算,多個窗口的向量放在一塊兒就成了矩陣,每一個卷積核也拉成向量,多個卷積核的向量排在一塊兒也成了矩陣,因而,卷積運算轉化成了矩陣運算。.net

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

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)邏輯上雖然爲矩陣乘法,實現時兩個矩陣逐行內積便可orm

優缺點分析

將卷積運算轉化爲矩陣乘法,從乘法和加法的運算次數上看,二者沒什麼差異,可是轉化成矩陣後,運算時須要的數據被存在連續的內存上,這樣訪問速度大大提高(cache),同時,矩陣乘法有不少庫提供了高效的實現方法,像BLAS、MKL等,轉化成矩陣運算後能夠經過這些庫進行加速。blog

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

參考

相關文章
相關標籤/搜索