卷积是深度学习最核心的计算之一。这节主要介绍如何使用Im2Col方式完成卷积操作,以及在kuiperCourse里的的实现方式,以及winograd是什么。
项目地址
课程地址
个人完成的作业地址
作者原文
Im2Col介绍
convolution是一个理论。im2col+gemm是一个实现方法,winograd也是一个实现方法,但是只对于少数情况有效。
gemm是通用矩阵优化,openblas做了这部分,我们只需要完成im2col就行,im2col意为把图片image转换为列形式。
卷积操作最简单实现方式逐行逐列循环遍历即可,代码如下:
def convolution(k, data):
n,m = data.shape
img_new = []
for i in range(n-3):
line = []
for j in range(m-3):
a = data[i:i+3,j:j+3]
line.append(np.sum(np.multiply(k, a)))
img_new.append(line)
return np.array(img_new)
input[C][H][W];
kernels[M][K][K][C];
output[M][H][W];
for h in 1 to H do
for w in 1 to W
do for o in 1 to M do
sum = 0;
for x in 1 to K do
for y in 1 to K do
for i in 1 to C do
sum += input[i][h+y][w+x] * kernels[o][x][y][i];
output[o][w][h] = sum;
Im2col具体含义如图:
简化问题:
- 假如只有一个通道一个卷积核,卷积核大小为(K*K)
特征图通道只有一个通道,大小为(H*W)
将卷积核展开为一个向量大小则为(1, K*K)
将特征图按照卷积核大小和卷积步长展开为矩阵(K*K,N)N为应该卷积的次数
两者进行矩阵乘法得到结果(1,N)再reshape为正常的结果格式。 其中具体展开策略,是行主序还是列主序看具体实现,只要卷积核核特征图展开方式一致即可。 - 如果有C个通道,则向量大小为(1, K*K*C),矩阵大小为(K*K*C,N)
没有什么改变,只是把通道向量按顺序拼起来即可。 - 如果有D个卷积核,则向量变成矩阵(D, K*K*C),特征图转换的矩阵不变(因为只是卷积核多了)
结果为(D,N)再reshape为正常的结果格式。
kuipercourse中代码实现
重点看ConvolutionLayer::Forwards实现。
kuipercourse中考虑了分组卷积的策略,为了简单起见,直接看成一组。
输入特征图
// 取一个输入(特征图)
const std::shared_ptr<Tensor<float>> &input = inputs.at(i);
// 拷贝一份输入, 并进行padding
std::shared_ptr<Tensor<float>> input_;
if (padding_h > 0 || padding_w > 0) {
input_ = input->Clone();
input_->Padding({padding_h, padding_h, padding_w, padding_w}, 0);
} else {
input_ = input;
}
计算输出大小
// 卷积核个数
const uint32_t kernel_count = weights.size();
uint32_t kernel_h = weights.at(0)->rows();
uint32_t kernel_w = weights.at(0)->cols();
CHECK(kernel_h > 0 && kernel_w > 0)
<< "The size of kernel size is less than zero";
uint32_t output_h = uint32_t(std::floor((input_h - kernel_h) / stride_h + 1));
uint32_t output_w = uint32_t(std::floor((input_w - kernel_w) / stride_w + 1));
CHECK(output_h > 0 && output_w > 0)
<< "The size of the output feature map is less than zero";
// 获取卷积计算所需要的一系列参数, 包括输入输出大小output_h,output_w等.
卷积核展开
// 多个卷积核,则放在一个数组里
std::vector<arma::fmat> kernel_matrix_arr(kernel_count_group);
// 一个卷积核有多个通道,则全部展开,按顺序拼一起
arma::fmat kernel_matrix_c(1, row_len * input_c_group);
for (uint32_t k = 0; k < kernel_count_group; ++k) {
// 按顺序取卷积核(kernel_count_group在分组为1时候就是卷积核个数,可以不管)
const std::shared_ptr<Tensor<float>> &kernel = weights.at(k + g * kernel_count_group);
// 一个卷积核内的多个通道数据是横向摆放的
//【通道一内容】【通道二内容】【通道三内容】排成向量
for (uint32_t ic = 0; ic < input_c_group; ++ic) {
memcpy(kernel_matrix_c.memptr() + row_len * ic,
kernel->at(ic).memptr(), row_len * sizeof(float));
}
LOG(INFO) << "kernel展开后: " << "\n" << kernel_matrix_c;
// 不同卷积核之间是竖向摆放的.
// 【卷积核1通道1内容】【卷积核1通道2内容】【卷积核1通道3内容】
// 【卷积核2通道1内容】【卷积核2通道2内容】【卷积核2通道3内容】
// 【卷积核3通道1内容】【卷积核3通道2内容】【卷积核3通道3内容】
// 【卷积核4通道1内容】【卷积核4通道2内容】【卷积核4通道3内容】
kernel_matrix_arr.at(k) = kernel_matrix_c;
}
特征图展开
// 存放展开后的输入特征图
// 长为通道乘卷积核大小,宽为需要原本卷积的次数
arma::fmat input_matrix(input_c_group * row_len, col_len);
for (uint32_t ic = 0; ic < input_c_group; ++ic) {
// 拿到输入特征图的一个通道, 一个for循环取input_c_group个通道
const arma::fmat &input_channel = input_->at(ic + g * input_c_group);
int current_col = 0;
// 在一个通道上进行滑动,窗口滑动
// 只要还有卷积核宽的空间,就能继续右边滑动
// 这里是列主序(arma里是列主序)
// 按列
for (uint32_t w = 0; w < input_w - kernel_w + 1; w += stride_w) {
// 按行
for (uint32_t r = 0; r < input_h - kernel_h + 1; r += stride_h) {
// 拷贝目标地址, 根据ic*row_len判断行位置,根据current_col找的列位置
// 如大小为27*4
// 根据current_col看在第几列,根据ic*row_len是在0还是9还是18行
// 这里是每次先遍历特征图一个通道,然后横着排开, 然后再遍历下一个通道时再补齐下面的
float *input_matrix_c_ptr =
input_matrix.colptr(current_col) + ic * row_len;
current_col += 1;// 卷积次数,
// 处理一个[卷积核窗口]内的多列数据
// 卷积核的宽度
for (uint32_t kw = 0; kw < kernel_w; ++kw) {
// 不同窗口内的数据是行相邻的
// 先判断在哪一行,在找在哪一列(因为列主序拷贝)
const float *region_ptr = input_channel.colptr(w + kw) + r;
memcpy(input_matrix_c_ptr, region_ptr, kernel_h * sizeof(float));
// 在同一个卷积核窗口中,跳转到下一列数据,下一列数据拼接到上一列的下方
input_matrix_c_ptr += kernel_h;
}
}
}
}
举例分析特征图展开
2通道,4*4
1234 hijk
5678 lmno
9abc pqrs
defg tuvw
如果卷积核大小是3*3两通道,则应该得到一个2*2的计算结果
所以展开后的输入特征图大小为(2*3*3,2*2)即(18,4)
arma::fmat input_matrix(input_c_group * row_len, col_len);
首先取一个通道
1234
5678
9abc
defg
然后在这个通道上滑动,先往下移动后往右移动
按照卷积核滑动结果为
123 567 换列 234 678
567 9ab 678 abc
9ab def abc efg
然后按照对应每一个被卷积的对象,按列主序展开
得到
1526
596a
9dae
2637
6a7b
aebf
3748
7b8c
bfcg
然后遍历第二个通道
同理,因为不同的通道按顺序排列
同样得到9*4和上序结果拼接即可
1526
596a
9dae
2637
6a7b
aebf
3748
7b8c
bfcg
hlim
lpmq
ptqu
imjn
mqnr
qurv
jnko
nros
rvsw
测试案例分析
分析test里的一个单通道单卷积核案例
卷积核展开
1.0000 1.0000 1.0000
2.0000 2.0000 2.0000
3.0000 3.0000 3.0000
展开结果(列主序)
1.0000 2.0000 3.0000 1.0000 2.0000 3.0000 1.0000 2.0000 3.0000
特征图展开
1.0000 2.0000 3.0000 4.0000
5.0000 6.0000 7.0000 8.0000
7.0000 8.0000 9.0000 10.0000
11.0000 12.0000 13.0000 14.0000
展开结果
1.0000 5.0000 2.0000 6.0000
5.0000 7.0000 6.0000 8.0000
7.0000 11.0000 8.0000 12.0000
2.0000 6.0000 3.0000 7.0000
6.0000 8.0000 7.0000 9.0000
8.0000 12.0000 9.0000 13.0000
3.0000 7.0000 4.0000 8.0000
7.0000 9.0000 8.0000 10.0000
9.0000 13.0000 10.0000 14.0000
显然第一列是左上角9个元素的按列展开
因为卷积核和特征图都是按列展开 所以两者相乘,就等价于卷积计算 (多个通道一样,多通道的计算结果也要相加的;多个卷积核,则变成矩阵乘矩阵,得到还是矩阵,则一个卷积核对应一个卷积结果)
计算结果
114 174 132 192
变形回来
114 132
174 192
TEST
目前通过的test为20个
winograd
winograd是一种很牛逼的卷积实现方式 winograd论文地址
以一维卷积举例:
输入信号为d=[d1,d2,d3,d4],卷积核大小为g=[g1,g2,g3]
则卷积计算可以写成:
d1 d2 d3 乘 g1 得到 r1
d2 d3 d4 g2 r2
g3
这个计算过程显然需要6次乘法和4次加法
winograd做法为
d1 d2 d3 乘 g1 得到 m1+m2+m3
d2 d3 d4 g2 m2-m3-m4
g3
其中
由于右边式子(只涉及卷积核的可以提前准备好)
所以只需要四次乘法和六次加法,省去两次乘法但多了两次乘法(乘法运算耗时是加法运算的好几倍)
参考文献
blog.csdn.net/qq_41398808…
zhuanlan.zhihu.com/p/63974249
leonardoaraujosantos.gitbook.io/artificial-…
www.bilibili.com/video/BV1vv…
mp.weixin.qq.com/s/_CKl1cdWH…