1. 概述
卷积算子是深度神经网络的核心算子, 为了加深对 卷积算子 的理解, 遂尝试自己用 C/C++ 来实现它, 花了一番功夫, 目前完成了初版且已在 GitHub 上开源, 欢迎使用, Star 和 PR.
https://github.com/quarrying/diyconv
一些说明:
1) 目前版本还不追求运行效率, 且没有完备的参数检查, 且只实现了二维卷积算子.
2) 为了便于使用, 还实现了卷积算子的 Python 接口 (通过 ctypes 调用 C/C++ 生成的库文件).
3) 自实现的卷积算子已与 PyToch 的卷积算子进行了一致性比较, 项目中附测试代码.
4) 项目代码不止包含完整版的卷积算子 (指的是包含 stride, padding, dilation 和 groups 四个超参数的卷积算子), 还包含了非完整版的卷积算子. 读者或许可以通过比较不同版本的卷积算法, 来帮助代码的理解.
2. 实现细节
上面说的非完整版的卷积算子和完整版的卷积算子, 在项目代码中具体指的是:
conv2d_with_stride
conv2d_with_stride_padding
conv2d_with_stride_padding_dilation
conv2d_with_stride_padding_dilation_groups
由它们的名字可以看出它们之间的区别, 在详细介绍之前, 先进行函数参数说明
input
,kernel
,output
: 输入数据, 卷积核, 输出数据stride_h
,stride_w
: 卷积步长padding_h_begin
,padding_h_end
: 上侧填充尺寸, 下侧填充尺寸padding_w_begin
,padding_w_end
: 左侧填充尺寸, 右侧填充尺寸dilation_h
,dilation_w
: 膨胀系数groups
: 分组数
其中 input
, kernel
, output
用自定义的结构体 Tensor4f
来表示.
typedef struct Tensor
{
float* data;
int dims[4];
} Tensor4f;
2.1. conv2d_with_stride
conv2d_with_stride
是其他函数的基础, 在此完整列出:
void conv2d_with_stride(
const Tensor4f* input, const Tensor4f* kernel, Tensor4f* output,
int stride_h, int stride_w)
{
assert (input->dims[0] == output->dims[0]);
assert (input->dims[1] == kernel->dims[1]);
assert (kernel->dims[0] == output->dims[1]);
int batch_size = input->dims[0];
int in_channels = input->dims[1];
int input_height = input->dims[2];
int input_width = input->dims[3];
int out_channels = kernel->dims[0];
int kernel_height = kernel->dims[2];
int kernel_width = kernel->dims[3];
int output_height = output->dims[2];
int output_width = output->dims[3];
int input_numel = in_channels * input_height * input_width;
int output_numel = out_channels * output_height * output_width;
int kernel_numel = in_channels * kernel_height * kernel_width;
for (int batch_idx = 0; batch_idx < batch_size; ++batch_idx)
{
int input_offset = batch_idx * input_numel;
int output_offset = batch_idx * output_numel;
const float* input_ptr = input->data + input_offset;
float* output_ptr = output->data + output_offset;
for (int output_c_idx = 0; output_c_idx < out_channels; ++output_c_idx)
{
int kernel_offset = output_c_idx * kernel_numel;
const float* kernel_ptr = kernel->data + kernel_offset;
for (int output_h_idx = 0; output_h_idx < output_height; ++output_h_idx)
{
for (int output_w_idx = 0; output_w_idx < output_width; ++output_w_idx)
{
float val = 0.0f;
for (int kernel_c_idx = 0; kernel_c_idx < in_channels; ++kernel_c_idx)
{
for (int kernel_h_idx = 0; kernel_h_idx < kernel_height; ++kernel_h_idx)
{
int input_h_idx = kernel_h_idx + output_h_idx * stride_h;
for (int kernel_w_idx = 0; kernel_w_idx < kernel_width; ++kernel_w_idx)
{
int input_w_idx = kernel_w_idx + output_w_idx * stride_w;
int input_idx =
input_w_idx +
input_h_idx * input_width +
kernel_c_idx * input_width * input_height;
int kernel_idx =
kernel_w_idx +
kernel_h_idx * kernel_width +
kernel_c_idx * kernel_width * kernel_height;
val += input_ptr[input_idx] * kernel_ptr[kernel_idx];
}
}
}
int output_idx =
output_w_idx +
output_h_idx * output_width +
output_c_idx * output_width * output_height;
output_ptr[output_idx] = val;
}
}
}
}
}
2.2. conv2d_with_stride_padding
可以复用 conv2d_with_stride
函数的代码, 只需将
int input_h_idx = kernel_h_idx + output_h_idx * stride_h;
修改为
int input_h_idx = kernel_h_idx + output_h_idx * stride_h - padding_h_begin;
if ((input_h_idx < 0) || (input_h_idx >= input_height))
{
continue;
}
且将
int input_w_idx = kernel_w_idx + output_w_idx * stride_w;
修改为
int input_w_idx = kernel_w_idx + output_w_idx * stride_w - padding_w_begin;
if ((input_w_idx < 0) || (input_w_idx >= input_width))
{
continue;
}
2.3. conv2d_with_stride_padding_dilation
可以复用 conv2d_with_stride_padding
函数的代码, 只需将
int input_h_idx = kernel_h_idx + output_h_idx * stride_h;
修改为
int input_h_idx = kernel_h_idx * dilation_h + output_h_idx * stride_h - padding_h_begin;
且将
int input_w_idx = kernel_w_idx + output_w_idx * stride_w - padding_w_begin;
修改为
int input_w_idx = kernel_w_idx * dilation_w + output_w_idx * stride_w - padding_w_begin;
2.4. conv2d_with_stride_padding_dilation_groups
可以复用 conv2d_with_stride_padding_dilation
函数的代码, 但改动之处比较多, 在此不一一列举, 仅列出关键变动, 其他改动可以参考项目代码.
1)
改动之 1
int kernel_numel = in_channels * kernel_height * kernel_width;
修改为
int kernel_numel = in_channels_per_group * kernel_height * kernel_width;
2)
改动之 2
在
for (int batch_idx = 0; batch_idx < batch_size; ++batch_idx)
之后新增一层循环
for (int group_idx = 0; group_idx < groups; ++group_idx)
3)
改动之 3
int input_offset = batch_idx * input_numel;
int output_offset = batch_idx * output_numel;
修改为
int input_offset = batch_idx * input_numel +
group_idx * in_channels_per_group * input_height * input_width;
int output_offset = batch_idx * output_numel +
group_idx * out_channels_per_group * output_height * output_width;
4)
改动之 4
int kernel_offset = output_c_idx * kernel_numel;
修改为
int kernel_offset = (output_c_idx + group_idx * out_channels_per_group) * kernel_numel;
5)
改动之 5
for (int output_c_idx = 0; output_c_idx < out_channels; ++output_c_idx)
修改为
for (int output_c_idx = 0; output_c_idx < out_channels_per_group; ++output_c_idx)
6)
改动之 6
for (int kernel_c_idx = 0; kernel_c_idx < in_channels; ++kernel_c_idx)
修改为
for (int kernel_c_idx = 0; kernel_c_idx < in_channels_per_group; ++kernel_c_idx)
笔者还直接借用 Caffe 中的 im2col_cpu
再利用自实现的矩阵乘函数 matmul
或 np.dot
实现了一版 conv2d_with_stride_padding_dilation_groups
, 详见项目, 在此就不赘述了.
2.5. output 的宽高
上面函数中的 output
的内存是事先申请好的, 其宽高可由下面的函数来算得:
def compute_output_size(input_size, kernel_size, stride=1,
padding_begin=0, padding_end=0, dilation=1):
kernel_extent = dilation * (kernel_size - 1) + 1
return (input_size - kernel_extent + padding_begin + padding_end) // stride + 1
更新记录
- 20211225, 发布