博客: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
缺点呢?这是一种空间换时间的方法,消耗了更多的内存——转化的过程当中数据被冗余存储。图片