基础卷积算子的C++实现与NCNN卷积层基于CPU的加速策略

卷积原理

图像的卷积过程就相当于一个自定义大小的滑块在输入图像上不断移动定义步长并与输入图像对应位置点乘求和,并且一个卷积核对应一层输出特征图,也就是说卷积的最外层是图像深度。

二维卷积层的基础实现(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帮助处理大量矩阵乘法和加法运算,提高计算速度。

inter的AVX2指令集

ARM的NEON指令集

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ANIMZLS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值