卷积原理
图像的卷积过程就相当于一个自定义大小的滑块在输入图像上不断移动定义步长并与输入图像对应位置点乘求和,并且一个卷积核对应一层输出特征图,也就是说卷积的最外层是图像深度。
二维卷积层的基础实现(C++)
没有考虑填充、非平方卷积核的情况。
//输入图像、输出特征图、卷积核、偏置、输出特征图层数、步长
void Conv(Img_struct &input_img, Img_struct &conv_output_img_p, Img_struct* conv_kernel,Img_struct &conv_kernel_b, int feature_map, int step)
{
//定义几个局部变量,随着步长增加,相当于滑动窗口
int rows_x = 0;
int cols_y = 0;
//根据输出的feature_map进行遍历
for(int feature_map_i = 0; feature_map_i < feature_map; feature_map_i++)
{
//切记往输出图像中填数还需要Img_struct类中的Get进行指针传参的方式
//首先获得该feature_map下的像素首地址
float* output_img_arr = conv_output_img_p.Get(feature_map_i,0,0);
//定义一个该feature_map下的滑动像素
int cnt = 0;
//每个feature_map是由一组卷积核与输入图像卷积可得(二者深度一定相同),因此还需要遍历深度
for(int depth_i = 0;depth_i < input_img.depth;depth_i++)
{
//每次深度循环都得从0开始,也就是原始图像的左上角
rows_x = 0;
cols_y = 0;
//检查卷积核是否遍历完整个图像
while(rows_x + conv_kernel[feature_map_i].rows <= input_img.rows) //判断边界
{
for(int i =0;i< conv_kernel[feature_map_i].rows;i++) //开始卷积
{
for(int j = 0;j< conv_kernel[feature_map_i].cols;j++)
{
*(output_img_arr + cnt) += *(input_img.Get(depth_i,i+rows_x,j+cols_y)) * (*(conv_kernel[feature_map_i].Get(depth_i,i,j)));
}
}
cnt++;
cols_y += step;
//检查是否需要换行
if(cols_y + conv_kernel[feature_map_i].cols > input_img.cols)
{
cols_y = 0;
rows_x += step;
}
}
//置零操作,这才是每一个深度卷积后多张卷积图变为一张feature_map的精髓!!!不同通道都加在了map同一个位置
cnt = 0;
}
//开始添加偏置项,一个卷积层共用一个偏置项
for(int i =0;i < conv_output_img_p.rows;i++)
{
for(int j =0;j<conv_output_img_p.cols;j++)
{
*(output_img_arr + cnt) = *(conv_output_img_p.Get(feature_map_i,i,j)) + *(conv_kernel_b.Get(feature_map_i,0,0));
//通过激活函数
*(output_img_arr + cnt) = Activation_tanh(*(output_img_arr + cnt));
cnt++;
}
}
}
}
NCNN源码中二维卷积核心计算部分
输入参数:输入图像、输出图像、卷积核、偏置、卷积核宽、高、水平步长、垂直步长、水平方向扩展、垂直方向扩展、激活函数类型、激活函数参数、
static int convolution(const Mat& bottom_blob, Mat& top_blob, const Mat& weight_data, const Mat& bias_data, int kernel_w, int kernel_h, int stride_w, int stride_h, int dilation_w, int dilation_h, int activation_type, const Mat& activation_params, const Option& opt)
{
const int w = bottom_blob.w;
const int inch = bottom_blob.c;
const int outw = top_blob.w;
const int outh = top_blob.h;
const int outch = top_blob.c;
//是否有偏置项存在
const int bias_term = bias_data.empty() ? 0 : 1;
//卷积核像素总量
const int maxk = kernel_w * kernel_h;
// 卷积核偏移量
std::vector<int> _space_ofs(maxk);
int* space_ofs = &_space_ofs[0];
{
int p1 = 0;
int p2 = 0;
int gap = w * dilation_h - kernel_w * dilation_w;
for (int i = 0; i < kernel_h; i++)
{
for (int j = 0; j < kernel_w; j++)
{
space_ofs[p1] = p2;
p1++;
p2 += dilation_w;
}
p2 += gap;
}
}
//卷积核点乘求和,用到了openmp并行计算
#pragma omp parallel for num_threads(opt.num_threads)
for (int p = 0; p < outch; p++)
{
float* outptr = top_blob.channel(p);
for (int i = 0; i < outh; i++)
{
for (int j = 0; j < outw; j++)
{
float sum = 0.f;
//偏置量
if (bias_term)
sum = bias_data[p];
const float* kptr = (const float*)weight_data + maxk * inch * p;
for (int q = 0; q < inch; q++)
{
const Mat m = bottom_blob.channel(q);
const float* sptr = m.row(i * stride_h) + j * stride_w;
for (int k = 0; k < maxk; k++) // 29.23
{
float val = sptr[space_ofs[k]]; // 20.72
float wt = kptr[k];
sum += val * wt; // 41.45
}
kptr += maxk;
}
//激活函数
outptr[j] = activation_ss(sum, activation_type, activation_params);
}
outptr += outw;
}
}
return 0;
}
openMP加速
#pragma omp parallel for num_threads(opt.num_threads)
for (int p = 0; p < outch; p++){...}
SIMD(高效并行计算)
SIMD,全称为Single Instruction, Multiple Data,中文翻译为单指令多数据流。它是一种并行处理技术,允许一个指令处理多个数据。SIMD通过使用特殊的寄存器和指令集,将多个数据打包在一起,并在单个CPU时钟周期内执行同一个操作。比如,假设我们有四组数据需要做相同的运算,在传统的SISD架构下,需要执行四次指令,而使用SIMD技术,只需要一次指令即可完成四组数据的运算。在神经网络的训练和推理过程中,SIMD帮助处理大量矩阵乘法和加法运算,提高计算速度。