本次解析src/im2col.h 与 src/im2col.c 两个。这块其实是卷积操作的底层实现。
im2col主要是完成矩阵的向量转换,为之后的 gemm.c 做矩阵乘法做准备,而im2col和gemm就是darknet卷积底层实现的核心,其实也是caffe卷积实现的核心。
img2col.h
img2col.h 中的包含的代码如下:主要就是一个函数im2col_cpu定义,在这里我们先不涉及gpu那块,先讲解cpu这块的矩阵的向量转换。
#ifndef IM2COL_H
#define IM2COL_H
//先重点关注此函数:
void im2col_cpu(float* data_im,
int channels, int height, int width,
int ksize, int stride, int pad, float* data_col);
#ifdef GPU
void im2col_gpu(float *im,
int channels, int height, int width,
int ksize, int stride, int pad,float *data_col);
#endif
#endif
im2col.c
缩写:
im2col = image to column
im2col.c 的详细分析如下:
#include "im2col.h"
#include <stdio.h>
/**
* 从输入多通道数组im(存储图像数据)中获取指定行、列、通道数处的元素值
* @param im 输入,所有数据都存成一个一维数据,例如对于3通道而言,每一个通道按行存储(每一通道所有行合并成一行),三通道依次再并成一行
* @param height 每一通道的高度(即输入图像的真正高度,补0之前)
* @param width
* @param channels 输入im的通道数,比如彩色图为3通道,之后每一次卷积的输入的通道数等于上一卷积层核的个数
* @param row 要提取的元素所在的行(二维图像补0之后的行数)
* @param col
* @param channel 要提取的元素所在的通道
* @param pad 图像左右上下各补0的长度(四个方向补0的长度一样)
* @return float类型数据,为im中channel通道,row-pad行,col-pad列处的元素值,高,宽;
* 而row与col则是补0之后,元素所在的行列,因此,要准确获取im中元素值,首先要减去pad以获取真实的行列数;
*/
float im2col_get_pixel(float *im, int height, int width, int channels,
int row, int col, int channel, int pad)
{
// 减去补0的长度,得到真实的行数和列数
row -= pad;
col -= pad;
// 如果行列数小于0,则返回0(刚好是补0的效果)
if (row < 0 || col < 0 ||
row >= height || col >= width) return 0;
// im存储多通道二维图像的数据格式为:各通道所有行并后成一行,再多通道一次并成一行;
// 所在指定通道所在行,再加上col移位到所在列
return im[col + width*(row + height*channel)];
}
//From Berkeley Vision's Caffe!
//https://github.com/BVLC/caffe/blob/master/LICENSE
/**
* 将图片转为便于计算的数组格式,这是直接从caffe移植过来的
* @param data_im 输入图像
* @param channels 输入图像的通道数(对于都一层,一般是颜色图,3通道,中间层通道数为上一层卷积核个数)
* @param height 输入图像的高度
* @param width
* @param ksize 卷积核尺寸
* @param stride 步幅
* @param pad 补0的个数
* @param data_col 相当于输出,为进行格式化重排后的输入图像数据
*
* 说明:输出data_col的元素个数与data_im元素个数补相等,一般比data_im的元素个数多。
* 因为stride较小,各个卷积核之间很很多重叠,
*/
void im2col_cpu(float* data_im,
int channels, int height, int width,
int ksize, int stride, int pad, float* data_col)
{
int c,h,w;
// 计算该层神经网络的输出图像尺寸(其实没有必要进行计算,因为在构建卷积层时,make_convolutional_layer()函数
// 已经调用convolutional_out_width(), convolutional_out_height()函数求取这两个参数,
// 此处直接调用l.out_h, l.out_w即可,函数参数只要传入该层网络指针即可)
int height_col = (height + 2*pad - ksize) / stride + 1;
int width_col = (width + 2*pad - ksize) / stride + 1;
// 卷积核大小:ksize*ksize是一个卷积核大小,之所有乘以通道数channels,是因为输入图像是多通道。每个卷积核在做卷积时
// 是同时对同一位置多通道的图像进行卷积运算,这里为了实现这一目的,将三通道上的卷积核并在一起以便于进行计算,因此卷积核
// 实际上并不是二维的,而是三维的。比如对于3通道图像,卷积核尺寸为3*3,该卷积核同时作用在三通道图像上,这样并起来就得到含有
// 27个元素的卷积核,且这27个元素都是独立的需要训练的参数。所以在计算训练参数个数时,一定要注意每一个卷积核的实际训练参数需要
// 乘以通道数
//***********这三层循环之间的逻辑关系,决定了输入图像重排后的格式 *********
// 外循环次数为一个卷积核的尺寸数,循环次数即为最终得到的data_col的总行数
int channels_col = channels * ksize * ksize;//im2col后的矩阵行数,3*3*3 = 27
for (c = 0; c < channels_col; ++c) {
// 列偏移,卷积核是一个二维矩阵,并按行存储在一维数组中,利用求余运算获取对应在卷积核的列数,比如对于
// 3*3 的卷积核(3通道),当c=0,显然在第一列,当c=5,显示在第2列,当c=9时,在第二通道的卷积核的第一列,
// 当c=26,在第三列(第三通道)。
int w_offset = c % ksize;
// 行偏移,卷积核是一个二维矩阵,且是安装(卷积核所有行并成一行)存储在一位数组中的,比如对于3*3的卷积核
// 处理3通道的图像,那么一个卷积核具有27个元素,每9个元素对应一个通道上卷积核(互为一样),每当c为3的倍数,就
// 意味着卷积核换了一行,h_offset取值为0,1,2,对应3*3卷积核中的第1,2,3行
int h_offset = (c / ksize) % ksize;
// 通道偏移,channels_col是多通道的卷积核并在一起的,比如对于3通道,3*3卷积和,每过9个元素就要换一通道数,
// 当c=0-8时候,c_im=0, c=9-17, c_im=1, c=18-26, c_im=2;
int c_im = c / ksize / ksize; // 计算目前处理第几个通道的图像
for (h = 0; h < height_col; ++h) {
// 中循环次数等于该层输出图像函数height_col, 说明data_col中的每一行存储了一张特征图,这张特征图又是按行存储在data_col中某行中
for (w = 0; w < width_col; ++w) {
// 由上面可知,对于3*3的卷积核,h_offset取值为0,1,2,当h_offset=0时,会提取出所有与卷积核第一行元素进行运算的像素,
// 依次类推;加上h×stride是对卷积核进行行移位操作,比如卷积核从图像(0,0)位置开始做卷积,那么最先涉及(0,0)——(3,3)
// 之间的像素值,若stride=2,那么卷积核进行一次行移位时,下一行的卷积操作是从元素(2, 0)(2为图像行号,0为列号)开始
int im_row = h_offset + h * stride;
// 对于3*3的卷积核,w_offset取值也为0,1,2,当w_offset=1,会提取所有与卷积核中第2列元素进行运算的像素,
// 比如前一次卷积其实像素元素(0,0),若stride=2,那么下次卷积元素是从元素(2,0)(0为行号,2为列号)
int im_col = w_offset + w * stride;
// col_index为重排后图像中的像素索引。等于c * height_col * width_col * w(还是按行存储,所有通道在合并成一行)
// 对应第c通道,h行,w列元素
int col_index = (c * height_col + h) * width_col + w;
// im2col_get_pixel函数获取输入图像data_im,第c_im通道,im_row, im_col的像素值并赋值给重排后的图像,
// 不是真实输入图像中行列号,因此需要减去pad获得真实的行列号
data_col[col_index] = im2col_get_pixel(data_im, height, width, channels,
im_row, im_col, c_im, pad);
}
}
}
}
知识点:
卷积步长 https://www.softool.cn/blog-220.html
im2col()函数图解:
感觉直接讲解代码还是比较不直观,下面我们举个例子来说明im2col()到底是怎样工作的。
输入:data_im[] 是一个 5*5【实际】单通道的特征图,卷积核大小 3*3,卷积步长 stride 为 2,补0的个数 pad为 1,则传入参数如下:
data_im = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25};
pad = 1; stride = 2; ksize = 3; height = 5; width =5; chanels=1;
而data_im实际的逻辑结构如下,是5*5的二维矩阵。如下图所示:
补0的结果如下:
卷积后输出特征图的大小(行数,列数)计算公式为:
height_col = (height + 2*pad – ksize)/stride + 1 = (5 + 2*1 -3)/2 + 1 = 3
wight_col = (width + 2*pad – ksize)/stride + 1 = (5 + 2*1 -3)/2 + 1 = 3
输入特征图转换得到的输出矩阵data_col的行数:卷积核通道数×卷积核尺寸×卷积核尺寸,大家可以想想为什么行数是这么多???
chanenls_col = channels * ksize * ksize = 1*3*3 = 9
输入特征图转换得到的输出矩阵data_col的列数:卷积后输出特征图的宽度×卷积输出特征图的高度,这里也可想想为什么列数是这么多???
height_col*weight_col= 3*3 = 9
到这里我就知道输入特征图转换得到矩阵data_col的大小为 [channels_col, height_col * weight_col] = [9, 9]
那么接下来,我们就知道data_col矩阵每个元素是从输入矩阵data_im怎么样转换过来的。如下图所示:
以下 c,h,w 指上面程序中定义的 int c,h,w;
c = 0, h = 0, w = 0,1, 2的计算过程如下:黄色部分是对应到data_im坐标【row,col】,蓝色是data_col的编号【按行存储】,红色部分表示映射得到的值。
c = 0, h = 1, w = 0,1, 2的计算过程如下:黄色部分是对应到data_im坐标【row,col】,蓝色是data_col的编号【按行存储】,红色部分表示映射得到的值。
c = 0, h = 2, w = 0,1, 2的计算过程如下:黄色部分是对应到data_im坐标【row,col】,蓝色是data_col的编号【按行存储】,红色部分表示映射得到的值。
c = 1, h = 0, w = 0,1, 2的计算过程如下:黄色部分是对应到data_im坐标【row,col】,蓝色是data_col的编号【按行存储】,红色部分表示映射得到的值。
c = 1, h = 1, w = 0,1, 2的计算过程如下:黄色部分是对应到data_im坐标【row,col】,蓝色是data_col的编号【按行存储】,红色部分表示映射得到的值。
大家看看有没有找出规律。。。最终转换结果如下,data_col矩阵:大家data_col矩阵每一列与data_im中卷积核滑动过的位置。是不是有点明白了。