博客:blog.shinelee.me | 博客園 | CSDNgithub
如何將卷積運算轉爲矩陣相乘?直接看下面這張圖,如下圖片來自論文High Performance Convolutional Neural Networks for Document Processing:ide
上圖爲3D卷積的傳統計算方式與矩陣乘法計算方式的對比,傳統卷積運算是將卷積核以滑動窗口的方式在輸入圖上滑動,當前窗口內對應元素相乘而後求和獲得結果,一個窗口一個結果。相乘而後求和剛好也是向量內積的計算方式,因此能夠將每一個窗口內的元素拉成向量,經過向量內積進行運算,多個窗口的向量放在一塊兒就成了矩陣,每一個卷積核也拉成向量,多個卷積核的向量排在一塊兒也成了矩陣,因而,卷積運算轉化成了矩陣運算。.net
下圖爲轉化後的矩陣尺寸,padding爲0:
代碼上怎麼實現呢?這裏參看一下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
缺點呢?這是一種空間換時間的方法,消耗了更多的內存——轉化的過程當中數據被冗餘存儲。圖片