Kuekua's blog


YesterDay you said tomorrow!


Yolo2代码解析

1. 激活层

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
26
27
28
29
30
/*
计算激活函数对加权输入的导数,并乘以delta,得到当前层最终的delta(敏感度图)
输入: x 当前层的所有输出
n l.output的维度,即为l.batch * l.out_c * l.out_w * l.out_h(包含整个batch的)
ACTIVATION 激活函数类型
delta 当前层敏感度图(与当前层输出x维度一样)
说明1: 该函数不但计算了激活函数对于加权输入的导数,还将该导数乘以了之前完成大部分计算的敏感度图delta(对应元素相乘),
因此调用改函数之后,将得到该层最终的敏感度图
说明2: 这里直接利用输出值求激活函数关于输入的导数值是因为神经网络中所使用的绝大部分激活函数,
其关于输入的导数值都可以描述为输出值的函数表达式,比如对于Sigmoid激活函数(记作f(x)),其导数值为f(x)'=f(x)*(1-f(x)),
因此如果给出y=f(x),那么f(x)'=y*(1-y),只需要输出值y就可以了,不需要输入x的值,
(暂时不确定darknet中有没有使用特殊的激活函数,以致于必须要输入值才能够求出导数值,
在activiation.c文件中,有几个激活函数暂时没看懂,也没在网上查到)。
说明3: 关于l.delta的初值,可能你有注意到在看某一类型网络层的时候,比如卷积层中的backward_convolutional_layer()函数,
没有发现在此之前对l.delta赋初值的语句,只是用calloc为其动态分配了内存,这样的l.delta其所有元素的值都为0,
那么这里使用*=运算符得到的值将恒为0。是的,如果只看某一层,或者说某一类型的层,的确有这个疑惑,但是整个网络是有很多层的,
且有多种类型,一般来说,不会以卷积层为最后一层,而回以COST或者REGION为最后一层,这些层中,会对l.delta赋初值,
又由于l.delta是由后网前逐层传播的,因此,当反向运行到某一层时,l.delta的值将都不会为0.
*/
void gradient_array(const float *x, const int n, const ACTIVATION a, float *delta)
{
int i;
for(i = 0; i < n; ++i){
delta[i] *= gradient(x[i], a);
}
}

2. Softmax层

2.1 前向传播函数

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*
** softmax层前向传播函数
** 输入: l 当前softmax层
** net 整个网络
** 说明:softmax层的前向比较简单,只需要对输入中的每个元素做softmax处理就可以,
** 但是darknet的实现引入了softmax_tree,这个参数的用法尚需要去推敲。
*/
void forward_softmax_layer(const softmax_layer l, network net)
{
if(l.softmax_tree){
int i;
int count = 0;
for (i = 0; i < l.softmax_tree->groups; ++i) {
int group_size = l.softmax_tree->group_size[i];
softmax_cpu(net.input + count, group_size, l.batch, l.inputs, 1, 0, 1, l.temperature, l.output + count);
count += group_size;
}
} else {
// 调用softmax_cpu()对输入的每一个元素进行softmax处理
softmax_cpu(net.input, l.inputs/l.groups, l.batch, l.inputs, l.groups,
l.inputs/l.groups, 1, l.temperature, l.output);
}
}
/*
** 输入: input 一组输入图片数据(含义见下面softmax_cpu()注释,下同)
** n 一组输入数据中含有的元素个数n=l.inputs/l.groups
** temp 温度参数,关于softmax的温度参数,可以搜索一下softmax with temperature,应该会有很多的
** stride 跨度
** output 这一组输入图片数据对应的输出(也即l.output中与这一组输入对应的某一部分)
** 说明:本函数实现的就是标准的softmax函数处理,唯一有点变化的就是在做指数运算之前,
将每个输入元素减去了该组输入元素中的最大值,以增加数值稳定性,
**关于此,可以参考博客:
http://freemind.pluskid.org/machine-learning/softmax-vs-softmax-loss-numerical-stability/,
**这篇博客写的不错,博客中还提到了softmax-loss,此处没有实现(此处实现的也即博客中提到的softmax函数,将softmax-loss分开实现了)。
*/
void softmax(float *input, int n, float temp, int stride, float *output)
{
int i;
float sum = 0;
// 赋初始最大值为float中的最小值-FLT_MAX(定义在float.h中)
float largest = -FLT_MAX;
// 寻找输入中的最大值,至于为什么要找出最大值,是为了数值计算上的稳定,详细请戳:
//http://freemind.pluskid.org/machine-learning/softmax-vs-softmax-loss-numerical-stability/
// 这篇博客写的不错,博客在接近尾声的时候,提到了为什么要减去输入中的最大值。
for(i = 0; i < n; ++i){
if(input[i*stride] > largest) largest = input[i*stride];
}
for(i = 0; i < n; ++i){
// 在进行指数运算之间,如上面博客所说,首先减去最大值(当然温度参数也要除)
float e = exp(input[i*stride]/temp - largest/temp);
sum += e; // 求和
output[i*stride] = e;
// 并将每一个输入的结果保存在相应的输出中
}
// 最后一步:归一化转换为概率(就是softmax函数的原型~),最后的输出结果保存在output中
for(i = 0; i < n; ++i){
output[i*stride] /= sum;
}
}

2.2 反向传播函数

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
** softmax层反向传播函数
** 输入: l 当前softmax层
** net 整个网络
** 说明:
// 下面注释理解应该有错,另外,对此处softmax反向的实现存在很大的疑问???????
** softmax层的反向很简单,由于自身没有训练参数,虽然有激活函数(即softmax函数),
** 但是又因其所处位置特殊,一般处于网络导数第二层,下一层就是cost层,
** 其自身的敏感度图l.delta已经计算完成(如果此处不太明白再说什么,可以参看卷积层的注释,
** (完成大部分计算,或者全连接层的注释,以及最大池化层的注释),
//
** 剩下要做的仅剩下利用自身的l.delta计算其上一层的敏感度图
** 还差乘以上一层激活函数关于其加权输入的导数值),
** 即将l.delta中的元素乘以对应的当前层与上一次层之间的权重,
** 而softmax层与上一层输出之间没有权重或者说权重都为1(因为是将输入直接送进softmax函数处理的,
** 并没有加权什么的),且softmax的输出与上一层的输出存在一一对应的关系,
** 所以求取上一层的敏感度图也是很简单,很直接,详见下面的注释。
*/
void backward_softmax_layer(const softmax_layer l, network net)
{
// 由当前softmax层的敏感度图l.delta计算上一层的敏感度图net.delta,调用的函数为axpy_cpu(),
// 为什么调用axpy_cpu()函数,因为softmax层的输出元素与上一层的输出元素存在一一对应的关系
//(由此可以看出softmax的stride取值为1是必然的,
// 再次照应blas.c中softmax_cpu()的注释,如果不为1,肯定不能是一一对应关系),
//所以由softmax层的敏感度图计算上一层的敏感度图,
// 可以逐个逐个元素计算,不需要类似全连接层中的矩阵乘法,更没有卷积层中的那般复杂。
axpy_cpu(l.inputs*l.batch, 1, l.delta, 1, net.delta, 1);
}
/*
** axpy是线性代数中一种基本操作,完成y= alpha*x + y操作,其中x,y为矢量,alpha为实数系数
** 可以参考:
** 输入: N X中包含的有效元素个数
** ALPHA 系数alpha
** X 参与运算的矢量X
** INCX 步长(倍数步长),即X中凡是INCX的倍数编号参与运算
** Y 参与运算的矢量,也相当于是输出
*/
void axpy_cpu(int N, float ALPHA, float *X, int INCX, float *Y, int INCY)
{
int i;
for(i = 0; i < N; ++i) Y[i*INCY] += ALPHA*X[i*INCX];
}

3. 全连接层

3.1 全连接层前向传播函数

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*
** 全连接层前向传播函数
** 输入: l 当前全连接层
** net 整个网络
** 流程: 全连接层的前向传播相对简单,首先初始化输出l.output全为0,在进行相关参数赋值之后,直接调用gemm_nt()完成Wx操作,
** 而后根据判断是否需要BN,如果需要,则进行BN操作,完了之后为每一个输出元素添加偏置得到Wx+b,最后使用激活函数处理
** 每一个输出元素,得到f(Wx+b)
*/
void forward_connected_layer(connected_layer l, network net)
{
int i;
// 初始化全连接层的所有输出(包含所有batch)为0值
fill_cpu(l.outputs*l.batch, 0, l.output, 1);
// m:全连接层接收的一个batch的图片张数
// k:全连接层单张输入图片元素个数
// n:全连接层对应单张输入图片的输出元素个数
int m = l.batch;
int k = l.inputs;
int n = l.outputs;
float *a = net.input;
float *b = l.weights;
float *c = l.output;
// a:全连接层的输入数据,维度为l.batch*l.inputs(包含整个batch的输入),可视作l.batch行,l.inputs列,每行就是一张输入图片
// b:全连接层的所有权重,维度为l.outputs*l.inputs(见make_connected_layer())
// c:全连接层的所有输出(包含所有batch),维度为l.batch*l.outputs(包含整个batch的输出)
// 根据维度匹配规则,显然需要对b进行转置,故而调用gemm_nt()函数,最终计算得到的c的维度为l.batch*l.outputs,
// 全连接层的的输出很好计算,直接矩阵相承就可以了,所谓全连接,就是全连接层的输出与输入的每一个元素都有关联(当然是同一张图片内的,
// 最中得到的c有l.batch行,l.outputs列,每行就是一张输入图片对应的输出)
// m:a的行,值为l.batch,含义为全连接层接收的一个batch的图片张数
// n:b'的列数,值为l.outputs,含义为全连接层对应单张输入图片的输出元素个数
// k:a的列数,值为l.inputs,含义为全连接层单张输入图片元素个数
gemm(0,1,m,n,k,1,a,k,b,k,1,c,n);
if(l.batch_normalize){
if(net.train){
// 计算全连接层l.output中每个元素的的均值,得到的l.mean是一个维度为l.outputs的矢量,
// 也即全连接层每一个输出元素都有一个平均值(有batch张输入图片,需要计算这batch图片对应输出元素的平均值),
// 对全连接层而言,每个输出就是一个通道,且每张特征图的维度为1*1
mean_cpu(l.output, l.batch, l.outputs, 1, l.mean);
// 计算全连接层每个输出元素的方差l,variance,其维度与l.mean一样
variance_cpu(l.output, l.mean, l.batch, l.outputs, 1, l.variance);
scal_cpu(l.outputs, .95, l.rolling_mean, 1);
axpy_cpu(l.outputs, .05, l.mean, 1, l.rolling_mean, 1);
scal_cpu(l.outputs, .95, l.rolling_variance, 1);
axpy_cpu(l.outputs, .05, l.variance, 1, l.rolling_variance, 1);
copy_cpu(l.outputs*l.batch, l.output, 1, l.x, 1);
normalize_cpu(l.output, l.mean, l.variance, l.batch, l.outputs, 1);
copy_cpu(l.outputs*l.batch, l.output, 1, l.x_norm, 1);
} else {
normalize_cpu(l.output, l.rolling_mean, l.rolling_variance, l.batch, l.outputs, 1);
}
scale_bias(l.output, l.scales, l.batch, l.outputs, 1);
}
// 前面得到的是全连接层每个输出元素的加权输入Wx,下面这个循环就是为每个元素加上偏置,最终得到每个输出元素上的加权输入:Wx+b
// 循环次数为l.batch,不是l.outputs,是因为对于全连接层来说,l.batch = l.outputs,无所谓了~
for(i = 0; i < l.batch; ++i){
// axpy_cpu()完成l.output + i*l.outputs = l.biases + (l.output + i*l.outputs)操作
// l.biases的维度为l.outputs;l.output的维度为l.batch*l.outputs,包含整个batch的输出,所以需要注意移位
axpy_cpu(l.outputs, 1, l.biases, 1, l.output + i*l.outputs, 1);
}
// 前向传播最后一步:前面得到每一个输出元素的加权输入Wx+b,这一步利用激活函数处理l.output中的每一个输出元素,
// 最终得到全连接层的输出f(Wx+b)
activate_array(l.output, l.outputs*l.batch, l.activation);
}

3.2 全连接层反向传播函数

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/*
** 全连接层反向传播函数
** 输入: l 当前全连接层
** net 整个网络
** 流程:先完成之前为完成的计算:计算当前层的敏感度图l.delta(注意是反向传播),
//而后调用axpy_cpu()函数计算当前全连接层的偏置更新值(基于完全计算完的l.delta),
** 然后判断是否进行BN,如果进行,则完成BN操作,再接着计算当前层权重更新值,最后计算上一层的敏感度图(完成大部分计算)。
//相比于卷积神经网络,全连接层很多的计算变得更为直接,不需要调用诸如im2col_cpu()或者col2im_cpu()函数
//对数据重排来重排去,直接矩阵相乘就可以搞定。
*/
void backward_connected_layer(connected_layer l, network net)
{
int i;
// 完成当前层敏感度图的计算:当前全连接层下一层不管是什么类型的网络,都会完成当前层敏感度图的绝大部分计算(上一层敏感度乘以上一层与当前层之间的权重)
// (注意是反向传播),此处只需要再将l.delta中的每一个元素乘以激活函数对加权输入的导数即可
// gradient_array()函数完成激活函数对加权输入的导数,并乘以之前得到的l.delta,得到当前层最终的l.delta(误差函数对加权输入的导数)
gradient_array(l.output, l.outputs*l.batch, l.activation, l.delta);
// 计算当前全连接层的偏置更新值
// 相比于卷积层的偏置更新值,此处更为简单(卷积层中有专门的偏置更新值计算函数,主要原因是卷积核在图像上做卷积即权值共享增加了复杂度,
//而全连接层没有权值共享),只需调用axpy_cpu()函数就可以完成。误差函数对偏置的导数实际就等于以上刚求完的敏感度值,
//因为有多张图片,需要将多张图片的效果叠加,故而循环调用axpy_cpu()函数,
// 不同于卷积层每个卷积核才有一个偏置参数,全连接层是每个输出元素就对应有一个偏置参数,共有l.outputs个,
//每次循环将求完一张图片所有输出的偏置更新值。
// l.bias_updates虽然没有明显的初始化操作,但其在make_connected_layer()中是用calloc()动态分配内存的,
//因此其已经全部初始化为0值。
// 循环结束后,最终会把每一张图的偏置更新值叠加,因此,最终l.bias_updates中每一个元素的值是batch中
//所有图片对应输出元素偏置更新值的叠加。
for(i = 0; i < l.batch; ++i){
axpy_cpu(l.outputs, 1, l.delta + i*l.outputs, 1, l.bias_updates, 1);
}
if(l.batch_normalize){
backward_scale_cpu(l.x_norm, l.delta, l.batch, l.outputs, 1, l.scale_updates);
scale_bias(l.delta, l.scales, l.batch, l.outputs, 1);
mean_delta_cpu(l.delta, l.variance, l.batch, l.outputs, 1, l.mean_delta);
variance_delta_cpu(l.x, l.delta, l.mean, l.variance, l.batch, l.outputs, 1, l.variance_delta);
normalize_delta_cpu(l.x, l.mean, l.variance, l.mean_delta, l.variance_delta, l.batch, l.outputs, 1, l.delta);
}
// 计算当前全连接层的权重更新值
int m = l.outputs;
int k = l.batch;
int n = l.inputs;
float *a = l.delta;
float *b = net.input;
float *c = l.weight_updates;
// a:当前全连接层敏感度图,维度为l.batch*l.outputs
// b:当前全连接层所有输入,维度为l.batch*l.inputs
// c:当前全连接层权重更新值,维度为l.outputs*l.inputs(权重个数)
// 由行列匹配规则可知,需要将a转置,故而调用gemm_tn()函数,转置a实际上是想把batch中所有图片的影响叠加。
// 全连接层的权重更新值的计算也相对简单,简单的矩阵乘法即可完成:当前全连接层的敏感度图乘以当前层的输入即可得到当前全连接层的权重更新值,
// (当前层的敏感度是误差函数对于加权输入的导数,所以再乘以对应输入值即可得到权重更新值)
// m:a'的行,值为l.outputs,含义为每张图片输出的元素个数
// n:b的列数,值为l.inputs,含义为每张输入图片的元素个数
// k:a’的列数,值为l.batch,含义为一个batch中含有的图片张数
// 最终得到的c维度为l.outputs*l.inputs,对应所有权重的更新值
gemm(1,0,m,n,k,1,a,m,b,n,1,c,n);
// 由当前全连接层计算上一层的敏感度图(完成绝大部分计算:当前全连接层敏感度图乘以当前层还未更新的权重)
m = l.batch;
k = l.outputs;
n = l.inputs;
a = l.delta;
b = l.weights;
c = net.delta;
// 一定注意此时的c等于net.delta,已经在network.c中的backward_network()函数中赋值为上一层的delta
// a:当前全连接层敏感度图,维度为l.batch*l.outputs
// b:当前层权重(连接当前层与上一层),维度为l.outputs*l.inputs
// c:上一层敏感度图(包含整个batch),维度为l.batch*l.inputs
// 由行列匹配规则可知,不需要转置。由全连接层敏感度图计算上一层的敏感度图也很简单,直接利用矩阵相乘,
//将当前层l.delta与当前层权重相乘就可以了,只需要注意要不要转置,拿捏好就可以,不需要像卷积层一样,需要对权重或者输入重排!
// m:a的行,值为l.batch,含义为一个batch中含有的图片张数
// n:b的列数,值为l.inputs,含义为每张输入图片的元素个数
// k:a的列数,值为l.outputs,含义为每张图片输出的元素个数
// 最终得到的c维度为l.bacth*l.inputs(包含所有batch)
if(c) gemm(0,0,m,n,k,1,a,k,b,n,1,c,n);
}

4. 卷积层

4.1 卷积运算的加速实现

将图像平铺成一行,每一段均可以直接与卷积核相乘,根据Stride大小可能有重复元素,因此元素总个数也可能变多,这样做是为了更方便快捷的并行进行卷积运算!

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/*
** 从输入的多通道数组im(存储图像数据)中获取指定行、列、、通道数处的元素值
** 输入: im 输入,所有数据存成一个一维数组,例如对于3通道的二维图像而言,
** 每一通道按行存储(每一通道所有行并成一行),三通道依次再并成一行
** height 每一通道的高度(即输入图像的真正的高度,补0之前)
** width 每一通道的宽度(即输入图像的宽度,补0之前)
** channels 输入im的通道数,比如彩色图为3通道,之后每一卷积层的输入的通道数等于上一卷积层卷积核的个数
** row 要提取的元素所在的行(二维图像补0之后的行数)
** col 要提取的元素所在的列(二维图像补0之后的列数)
** channel 要提取的元素所在的通道
** pad 图像左右上下各补0的长度(四边补0的长度一样)
** 返回: float类型数据,为im中channel通道,row-pad行,col-pad列处的元素值
而row与col则是补0之后,元素所在的行列,因此,要准确获取在im中的元素值,首先需要减去pad以获取在im中真实的行列数
*/
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存储多通道二维图像的数据的格式为:各通道所有行并成一行,再多通道依次并成一行,
// 因此width*height*channel首先移位到所在通道的起点位置,加上width*row移位到所在指定通道所在行,再加上col移位到所在列
return im[col + width*(row + height*channel)];
}
//From Berkeley Vision's Caffe!
//https://github.com/BVLC/caffe/blob/master/LICENSE
/*
** 将输入图片转为便于计算的数组格式,可以参考https://petewarden.com/2015/04/20/why-gemm-is-at-the-heart-of-deep-learning/
** 进行辅助理解(但执行方式并不同,只是用于概念上的辅助理解),由作者的注释可知,这是直接从caffe移植过来的
** 输入: data_im 输入图像
** channels 输入图像的通道数(对于第一层,一般是颜色图,3通道,中间层通道数为上一层卷积核个数)
** height 输入图像的高度(行)
** width 输入图像的宽度(列)
** ksize 卷积核尺寸
** stride 卷积核跨度
** pad 四周补0长度
** data_col 相当于输出,为进行格式重排后的输入图像数据
** 说明:1)此函数个人感觉在实现上存在不足,传入参数没有必要这么多,只需传入当前卷积层的指针即可,这样函数中的一些代码就会变的多余
** 2)输出data_col的元素个数与data_im元素个数不相等,一般比data_im的元素个数多,因为stride较小,
各个卷积核之间有很多重叠,实际data_col中的元素个数为channels*ksize*ksize*height_col* width_col,
其中channels为data_im的通道数,ksize为卷积核大小,height_col和width_col如下所注。data_col的还是按行排列,只是行数为
channels*ksize*ksize,列数为height_col*width_col,即一张特征图总的元素个数,每整列包含与某个位置处的卷积核计算的所有通道上的像素
(比如输入图像通道数为3,卷积核尺寸为3*3,则共有27行,每列有27个元素),不同列对应卷积核在图像上的不同位置做卷积
*/
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()函数已经调用
//函数参数只要传入该层网络指针就可了,没必要弄这么多参数)
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个元素的卷积核,能不能使得作用在不同通道上的卷积核有不同参数呢?
//不知道有没有这样的做法?可以思考下,当然这样做肯定会是参数剧增!!
int channels_col = channels * ksize * ksize;
// 这三层循环之间的逻辑关系,决定了输入图像重排后的格式,更为详细/形象的说明可参考博客
// 外循环次数为一个卷积核的尺寸数,循环次数即为最终得到的data_col的总行数
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;
// 中循环次数等于该层输出图像行数height_col,说明data_col中的每一行存储了一张特征图,
//这张特征图又是按行存储在data_col中的某行中
for (h = 0; h < height_col; ++h) {
// 内循环等于该层输出图像列数width_col,说明最终得到的data_col总有channels_col行,height_col*width_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列元素进行运算的像素,
// 实际在做卷积操作时,卷积核对图像逐行扫描做卷积,加上w*stride就是为了做列移位,
// 比如前一次卷积其实像素元素为(0,0),若stride=2,那么下次卷积元素起始像素位置为(0,2)(0为行号,2为列号)
int im_col = w_offset + w * stride;
// col_index为重排后图像中的像素索引,等于c * height_col * width_col + h * 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的像素值并赋值给重排后的图像,
// height和width为输入图像data_im的真实高、宽,pad为四周补0的长度(注意im_row,im_col是补0之后的行列号,
// 不是真实输入图像中的行列号,因此需要减去pad获取真实的行列号)
data_col[col_index] = im2col_get_pixel(data_im, height, width, channels,im_row, im_col, c_im, pad);
}
}
}
}

4.2 更新卷积核的偏置

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
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
** 计算每个卷积核的偏置更新值,所谓偏置更新值,就是bias = bias - alpha * bias_update中的bias_update
** 输入: bias_updates 当前层所有偏置的更新值,维度为l.n(即当前层卷积核的个数)
** delta 当前层的敏感度图(即l.delta)
** batch 一个batch含有的图片张数(即l.batch)
** n 当前层卷积核个数(即l.h)
** k 当前层输入特征图尺寸(即l.out_w*l.out_h)
** 原理:当前层的敏感度图l.delta是误差函数对加权输入的导数,也就是偏置更新值,只是其中每l.out_w*l.out_h个元素都对应同一个
偏置,因此需要将其加起来,得到的和就是误差函数对当前层各偏置的导数(l.delta的维度为l.batch*l.n*l.out_h*l.out_w,
可理解成共有l.batch行,每行有l.n*l.out_h*l.out_w列,而这一大行又可以理解成有l.n,l.out_h*l.out_w列,
这每一小行就对应同一个卷积核也即同一个偏置)
*/
void backward_bias(float *bias_updates, float *delta, int batch, int n, int size)
{
int i,b;
// 遍历batch中每张输入图片
// 注意,最后的偏置更新值是所有输入图片的总和(多张图片无非就是重复一张图片的操作,求和即可)。
// 总之:一个卷积核对应一个偏置更新值,该偏置更新值等于batch中所有输入图片累积的偏置更新值,
// 而每张图片也需要进行偏置更新值求和(因为每个卷积核在每张图片多个位置做了卷积运算,这都对偏置更新值有贡献)
//以得到每张图片的总偏置更新值。
for(b = 0; b < batch; ++b){
// 求和得一张输入图片的总偏置更新值
for(i = 0; i < n; ++i){
bias_updates[i] += sum_array(delta+size*(i+b*n), size);
}
}
}
/*
** 将以a为首地址此后n个元素相加,返回总和
*/
float sum_array(float *a, int n)
{
int i;
float sum = 0;
for(i = 0; i < n; ++i) sum += a[i];
return sum;
}

4.3 卷积层前向传播函数

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
void forward_convolutional_layer(convolutional_layer l, network net)
{
int out_h = l.out_h;
int out_w = l.out_w;
int i;
/*l.outputs = l.out_h * l.out_w * l.out_c在make各网络层函数中赋值(比如make_convolutional_layer()),
对应每张输入图片的所有输出特征图的总元素个数(每张输入图片会得到n也即l.out_c张特征图)
初始化输出l.output全为0.0;输入l.outputs*l.batch为输出的总元素个数,其中l.outputs为batch
中一个输入对应的输出的所有元素的个数,l.batch为一个batch输入包含的图片张数;0表示初始化所有输出为0;*/
fill_cpu(l.outputs*l.batch, 0, l.output, 1);
/*是否进行二值化操作(这个操作应该只有第一个卷积层使用吧?因为下面直接对net.input操作,这个理解是错误的,
因为在forward_network()含中,每进行一层都会将net.input = l.output,即下一层的输入被设置为当前层的输出)*/
if(l.xnor){
binarize_weights(l.weights, l.n, l.c*l.size*l.size, l.binary_weights);
swap_binary(&l);
binarize_cpu(net.input, l.c*l.h*l.w*l.batch, l.binary_input);
net.input = l.binary_input;
}
int m = l.n; // 该层卷积核个数
int k = l.size*l.size*l.c; // 该层每个卷积核的参数元素个数
int n = out_h*out_w; // 该层每个特征图的尺寸(元素个数)
float *a = l.weights; // 所有卷积核(也即权重),元素个数为l.n*l.c*l.size*l.size,按行存储,共有l*n行,l.c*l.size*l.size列
float *b = net.workspace; // 对输入图像进行重排之后的图像数据
float *c = l.output; // 存储一张输入图片(多通道)所有的输出特征图(输入图片是多通道的,输出图片也是多通道的,有多少个卷积核就有多少个通道,每个卷积核得到一张特征图即为一个通道)
/*该循环即为卷积计算核心代码:所有卷积核对batch中每张图片进行卷积运算!!
可以参考:https://petewarden.com/2015/04/20/why-gemm-is-at-the-heart-of-deep-learning/
进行辅助理解(主要是辅助理解,实际执行并不一样)。每次循环处理一张输入图片(所有卷积核对batch中一张图片做卷积运算)*/
for(i = 0; i < l.batch; ++i){
/*将多通道二维图像net.input变成按一定存储规则排列的数组b,以方便、高效地进行矩阵(卷积)计算,
详细查看该函数注释(比较复杂).注意net.input包含batch中所有图片的数据,但是每次循环只处理一张
(本循环最后一句对net.input进行了移位),因此在im2col_cpu仅会对其中一张图片
进行重排,l.c为每张图片的通道数,l.h为每张图片的高度,l.w为每张图片的宽度,l.size为卷积核尺寸,l.stride为跨度
得到的b为一张图片重排后的结果,也是按行存储的一维数组(共有l.c*l.size*l.size行,l.out_w*l.out_h列),*/
im2col_cpu(net.input, l.c, l.h, l.w,
l.size, l.stride, l.pad, b);
/*GEneral Matrix to Matrix Multiplication
// 此处在im2col_cpu操作基础上,利用矩阵乘法c=alpha*a*b+beta*c完成对图像卷积的操作
// 0,0表示不对输入a,b进行转置,
// m是输入a,c的行数,具体含义为每个卷积核的个数,
// n是输入b,c的列数,具体含义为每个输出特征图的元素个数(out_h*out_w),
// k是输入a的列数也是b的行数,具体含义为卷积核元素个数乘以输入图像的通道数(l.size*l.size*l.c),
// a,b,c即为三个参与运算的矩阵(用一维数组存储),alpha=beta=1为常系数,
// a为所有卷积核集合,元素个数为l.n*l.c*l.size*l.size,按行存储,共有l*n行,l.c*l.size*l.size列,
// 即a中每行代表一个可以作用在3通道上的卷积核,
// b为一张输入图像经过im2col_cpu重排后的图像数据(共有l.c*l.size*l.size行,l.out_w*l.out_h列),
// c为gemm()计算得到的值,包含一张输入图片得到的所有输出特征图(每个卷积核得到一张特征图),c中一行代表一张特征图,
// 各特征图铺排开成一行后,再将所有特征图并成一大行,存储在c中,因此c可视作有l.n行,l.out_h*l.out_w列。
// 详细查看该函数注释(比较复杂)*/
gemm(0,0,m,n,k,1,a,k,b,n,1,c,n);
/*// 对c进行指针偏移:移到batch中下一张图片对应输出的起始位置(每循环一次,将完成对一张图片的卷积操作,
// 产生的所有特征图的元素个数总和为n*m)*/
c += n*m;
// 同样,输入也进行指针偏移,移动到下一张图片元素的起始位置,以便下一次循环处理
// (batch中每张图片的元素个数为通道数*高度*宽度,即l.c*l.h*l.w)
net.input += l.c*l.h*l.w;
}
//如需要规范化(BN在非线性激活函数处理之前完成)
if(l.batch_normalize){
forward_batchnorm_layer(l, net);
} else {
add_bias(l.output, l.biases, l.batch, l.n, out_h*out_w);
}
activate_array(l.output, m*n*l.batch, l.activation);
if(l.binary || l.xnor) swap_binary(&l);
}

4.4 卷积层反向传播函数

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/*
** 卷积神经网络反向传播核心函数
** 主要流程:
1) 调用gradient_array()计算当前层l所有输出元素关于加权输入的导数值(也即激活函数关于输入的导数值),
并乘上上一次调用backward_convolutional_layer()还没计算完的l.delta,得到当前层最终的敏感度图;
2) 如果网络进行了BN,则需要进行BN的梯度计算;
3) 如果网络没有进行BN,则直接调用 backward_bias()计算当前层所有卷积核的偏置更新值;
4) 依次调用im2col_cpu(),gemm_nt()函数计算当前层权重系数更新值;
5) 如果上一层的delta已经动态分配了内存,则依次调用gemm_tn(),
col2im_cpu()计算上一层的敏感度图(并未完成所有计算,还差一个步骤);
** 强调:每次调用本函数会计算完成当前层的敏感度计算,同时计算当前层的偏置、权重更新值,除此之外,还会计算上一层的敏感度图,
但是要注意的是,并没有完全计算完,还差一步:乘上激活函数对加权输入的导数值。这一步在下一次调用本函数时完成。
*/
void backward_convolutional_layer(convolutional_layer l, network net)
{
int i;
int m = l.n; // 卷积核个数
// 每一个卷积核元素个数(包括l.c(l.c为该层网络接受的输入图片的通道数)个通道上的卷积核元素个数总数,比如卷积核尺寸为3*3,
// 输入图片有3个通道,因为要同时作用于3个通道上,所以需要额外复制两次这个卷积核,那么一个卷积核共有27个元素)
int n = l.size*l.size*l.c;
int k = l.out_w*l.out_h; // 每张输出特征图的元素个数:out_w,out_h是输出特征图的宽高
// 计算当前层激活函数对加权输入的导数值并乘以l.delta相应元素,从而彻底完成当前层敏感度图的计算,得到当前层的敏感度图l.delta。
// l.output存储了该层网络的所有输出:该层网络接受一个batch的输入图片,其中每张图片经卷积处理后得到的特征图尺寸为:l.out_w,l.out_h,
// 该层卷积网络共有l.n个卷积核,因此一张输入图片共输出l.n张宽高为l.out_w,l.out_h的特征图(l.outputs为一张图所有输出特征图的总元素个数),
// 所以所有输入图片也即l.output中的总元素个数为:l.n*l.out_w*l.out_h*l.batch;
// l.activation为该卷积层的激活函数类型,l.delta就是gradient_array()函数计算得到的
//l.output中每一个元素关于激活函数函数输入的导数值,
// 注意,这里直接利用输出值求得激活函数关于输入的导数值是因为神经网络中所使用的绝大部分激活函数关于输入的导数值
//都可以描述为输出值的函数表达式,比如对于Sigmoid激活函数(记作f(x)),其导数值为f(x)'=f(x)*(1-f(x)),
//因此如果给出y=f(x),那么f(x)'=y*(1-y),只需要输出值y就可以了,不需要输入x的值,
// (暂时不确定darknet中有没有使用特殊的激活函数,以致于必须要输入值才能够求出导数值,
//在activiation.c文件中,有几个激活函数暂时没看懂,也没在网上查到)。
// l.delta是一个一维数组,长度为l.batch * l.outputs(其中l.outputs = l.out_h * l.out_w * l.out_c),
//在make_convolutional_layer()动态分配内存;
// 再强调一次:gradient_array()不单单是完成激活函数对输入的求导运算,还完成计算当前层敏感度图的最后一步:
//l.delta中每个元素乘以激活函数对输入的导数(注意gradient_arry中使用的是*=运算符)。
// 每次调用backward_convolutional_laye时,都会完成当前层敏感度图的计算,同时会计算上一层的敏感度图,
//但对于上一层,其敏感度图并没有完全计算完成,还差一步,
// 需要等到下一次调用backward_convolutional_layer()时来完成,诚如col2im_cpu()中注释一样。
gradient_array(l.output, m*k*l.batch, l.activation, l.delta);
if(l.batch_normalize){
backward_batchnorm_layer(l, net);
} else {
// 计算偏置的更新值:每个卷积核都有一个偏置,偏置的更新值也即误差函数对偏置的导数,这个导数的计算很简单,
//实际所有的导数已经求完了,都存储在l.delta中,
// 接下来只需把l.delta中对应同一个卷积核的项加起来就可以(卷积核在图像上逐行逐列跨步移动做卷积,
//每个位置处都有一个输出,共有l.out_w*l.out_h个,
// 这些输出都与同一个偏置关联,因此将l.delta中对应同一个卷积核的项加起来即得误差函数对这个偏置的导数)
backward_bias(l.bias_updates, l.delta, l.batch, l.n, k);
}
// 遍历batch中的每张照片,对于l.delta来说,每张照片是分开存的,因此其维度会达到:l.batch*l.n*l.out_w*l.out_h,
// 对于l.weights,l.weight_updates以及上面提到的l.bias,l.bias_updates,是将所有照片对应元素叠加起来
// (循环的过程就是叠加的过程,注意gemm()这系列函数含有叠加效果,不是覆盖输入C的值,而是叠加到之前的C上),
// 因此l.weights与l.weight_updates维度为l.n*l.size*l.size,l.bias与
//l.bias_updates的维度为l.h,都与l.batch无关
for(i = 0; i < l.batch; ++i){
float *a = l.delta + i*m*k;
// net.workspace的元素个数为所有层中最大的l.workspace_size(在make_convolutional_layer()
//计算得到workspace_size的大小,在parse_network_cfg()中动态分配内存,此值对应未使用gpu时的情况),
// net.workspace充当一个临时工作空间的作用,存储临时所需要的计算参数,比如每层单张图片重排后的结果
//(这些参数马上就会参与卷积运算),一旦用完,就会被马上更新(因此该变量的值的更新频率比较大)
float *b = net.workspace;
float *c = l.weight_updates;
// 进入本函数之前,在backward_network()函数中,已经将net.input赋值为prev.output,
//也即若当前层为第l层,net.input此时已经是第l-1层的输出
float *im = net.input+i*l.c*l.h*l.w;
// 下面两步:im2col_cpu()与gemm()是为了计算当前层的权重更新值(其实也就是误差函数对当前成权重的导数)
// 将多通道二维图像net.input变成按一定存储规则排列的数组b,以方便、高效地进行矩阵(卷积)计算,详细查看该函数注释(比较复杂),
// im2col_cpu每次仅处理net.input(包含整个batch)中的一张输入图片(对于第一层,则就是读入的图片,
//对于之后的层,这些图片都是上一层的输出,通道数等于上一层卷积核个数)。
// 最终重排的b为l.c * l.size * l.size行,l.out_h * l.out_w列。
// 你会发现在前向forward_convolutional_layer()函数中,也为每层的输入进行了重排,
//但是很遗憾的是,并没有一个l.workspace把每一层的重排结果保存下来,而是统一存储到net.workspace中,
// 并被不断擦除更新,那为什么不保存呢?保存下来不是省掉一大笔额外重复计算开销?原因有两个:
//1)net.workspace中只存储了一张输入图片的重排结果,所以重排下张图片时,马上就会被擦除,
// 当然你可能会想,那为什么不弄一个l.worspaces将每层所有输入图片的结果保存呢?这引出第二个原因;
//2)计算成本是降低了,但存储空间需求急剧增加,想想每一层都有l.batch张图,且每张都是多通道的,
// 重排后其元素个数还会增多,这个存储量搁谁都受不了,如果一个batch有128张图,输入图片尺寸为400*400,
//3通道,网络有16层(假设每层输入输出尺寸及通道数都一样),那么单单为了存储这些重排结果,
// 就需要128*400*400*3*16*4/1024/1024/1024 = 3.66G,所以为了权衡,只能重复计算!
im2col_cpu(im, l.c, l.h, l.w,
l.size, l.stride, l.pad, b);
// 下面计算当前层的权重更新值,所谓权重更新值就是weight = weight - alpha * weight_update中的weight_update,
// 权重更新值等于当前层敏感度图中每个元素乘以相应的像素值,因为一个权重跟当前层多个输出有关联
//(权值共享,即卷积核在图像中跨步移动做卷积,每个位置卷积得到的值
// 都与该权值相关),所以对每一个权重更新值来说,需要在l.delta中找出所有与之相关的敏感度,
//乘以相应像素值,再求和,具体实现的方式依靠im2col_cpu()与gemm_nt()完成。
// (backward_convolutional_layer整个函数的代码非常重要,仅靠文字没有公式与图表辅助说明可能很难说清,
//所以这部分更为清晰详细的说明,请参考个人博客!)
// GEneral Matrix to Matrix Multiplication
// 此处在im2col_cpu操作基础上,利用矩阵乘法c=alpha*a*b+beta*c完成对图像卷积的操作;
// 0表示不对输入a进行转置,1表示对输入b进行转置;
// m是输入a,c的行数,具体含义为卷积核的个数(l.n);
// n是输入b,c的列数,具体含义为每个卷积核元素个数乘以输入图像的通道数(l.size*l.size*l.c);
// k是输入a的列数也是b的行数,具体含义为每个输出特征图的元素个数(l.out_w*l.out_h);
// a,b,c即为三个参与运算的矩阵(用一维数组存储),alpha=beta=1为常系数;
// a为l.delta的一大行。l.delta为本层所有输出元素(包含整个batch中每张图片的所有输出特征图)
//关于加权输入的导数(即激活函数的导数值)集合,
// 元素个数为l.batch * l.out_h * l.out_w * l.out_c(l.out_c = l.n),
//按行存储,共有l.batch行,l.out_c * l.out_h * l.out_w列,
// 即l.delta中每行包含一张图的所有输出图,故这么一大行,又可以视作有l.out_c(l.out_c=l.n)小行,
//l.out_h*l*out_w小列,而一次循环就是处理l.delta的一大行,
// 故可以将a视作l.out_c行,l.out_h*l*out_w列的矩阵;
// b为单张输入图像经过im2col_cpu重排后的图像数据;
// c为输出,按行存储,可视作有l.n行,l.c*l.size*l.size列(l.c是输入图像的通道数,l.n是卷积核个数),
// 即c就是所谓的误差项(输出关于加权输入的导数),或者敏感度(强烈推荐:https://www.zybuluo.com/hanbingtao/note/485480)
//(一个核有l.c*l.size*l.size个权重,共有l.n个核)。
// 由上可知:
// a: (l.out_c) * (l.out_h*l*out_w)
// b: (l.c * l.size * l.size) * (l.out_h * l.out_w)
// c: (l.n) * (l.c*l.size*l.size)(注意:l.n = l.out_c)
// 故要进行a * b + c计算,必须对b进行转置(否则行列不匹配),因故调用gemm_nt()函数
gemm(0,1,m,n,k,1,a,k,b,k,1,c,n);
// 接下来,用当前层的敏感度图l.delta以及权重l.weights(还未更新)来获取上一层网络的敏感度图,
//BP算法的主要流程就是依靠这种层与层之间敏感度反向递推传播关系来实现。
//而每次开始遍历某一层网络之前,都会更新net.input为这一层网络前一层的输出,即prev.output,
// 同时更新net.delta为prev.delta,因此,这里的net.delta是当前层前一层的敏感度图。
// 已经强调很多次了,再说一次:下面得到的上一层的敏感度并不完整,完整的敏感度图是损失函数对上一层的加权输入的导数,
// 而这里得到的敏感度图是损失函数对上一层输出值的导数,还差乘以一个输出值也即激活函数对加权输入的导数。
if(net.delta){
// 当前层还未更新的权重
a = l.weights;
// 每次循环仅处理一张输入图,注意移位(l.delta的维度为l.batch * l.out_c * l.out_w * l.out_h)(注意l.n = l.out_c,另外提一下,对整个网络来说,每一层的l.batch其实都是一样的)
b = l.delta + i*m*k;
// net.workspace和上面一样,还是一张输入图片的重排,不同的是,此处我们只需要这个容器,
//而里面存储的值我们并不需要,在后面的处理过程中,
// 会将其中存储的值一一覆盖掉(尺寸维持不变,还是(l.c * l.size * l.size) * (l.out_h * l.out_w)
c = net.workspace;
// 相比上一个gemm,此处的a对应上一个的c,b对应上一个的a,c对应上一个的b,即此处a,b,c的行列分别为:
// a: (l.n) * (l.c*l.size*l.size),表示当前层所有权重系数
// b: (l.out_c) * (l.out_h*l*out_w)(注意:l.n = l.out_c),表示当前层的敏感度图
// c: (l.c * l.size * l.size) * (l.out_h * l.out_w),表示上一层的敏感度图
//(其元素个数等于上一层网络单张输入图片的所有输出元素个数),
// 此时要完成a * b + c计算,必须对a进行转置(否则行列不匹配),因故调用gemm_tn()函数。
// 此操作含义是用:用当前层还未更新的权重值对敏感度图做卷积,得到包含上一层所有敏感度信息的矩阵,
//但这不是上一层最终的敏感度图,因为此时的c,也即net.workspace的尺寸为
//(l.c * l.size * l.size)*(l.out_h * l.out_w),明显不是上一层的输出尺寸l.c*l.w*l.h,
// 接下来还需要调用col2im_cpu()函数将其恢复至l.c*l.w*l.h(可视为l.c行,l.w*l.h列),
//这才是上一层的敏感度图(实际还差一个环节,这个环节需要等到下一次调用backward_convolutional_layer()才完成:
//将net.delta中每个元素乘以激活函数对加权输入的导数值)。完成gemm这一步,如col2im_cpu()中注释,
//是考虑了多个卷积核导致的一对多关系(上一层的一个输出元素会流入到下一层多个输出元素中),
// 接下来调用col2im_cpu()则是考虑卷积核重叠(步长较小)导致的一对多关系。
gemm(1,0,n,k,m,1,a,n,b,k,0,c,k);
// 对c也即net.workspace进行重排,得到的结果存储在net.delta中,每次循环只会处理一张输入图片,
//因此,此处只会得到一张输入图产生的敏感图(注意net.delta的移位),
// 整个循环结束后,net.delta的总尺寸为l.batch * l.h * l.w * l.c,这就是上一层网络整个batch的敏感度图,
//可视为有l.batch行,l.h*l.w*l.c列,每行存储了一张输入图片所有输出特征图的敏感度
// col2im_cpu()函数中会调用col2im_add_pixel()函数,该函数中使用了+=运算符,
//也即该函数要求输入的net.delta的初始值为0,而在gradient_array()中注释到l.delta的元素是不为0(也不能为0)的,
// 看上去是矛盾的,实则不然,gradient_array()使用的l.delta是当前层的敏感度图,
//而在col2im_cpu()使用的net.delta是上一层的敏感度图,正如gradient_array()中所注释的,
// 当前层l.delta之所以不为0,是因为从后面层反向传播过来的,对于上一层,显然还没有反向传播到那,
//因此net.delta的初始值都是为0的(注意,每一层在构建时,就为其delta动态分配了内存,
// 且在前向传播时,为每一层的delta都赋值为0,可以参考network.c中forward_network()函数)
col2im_cpu(net.workspace, l.c, l.h, l.w, l.size, l.stride, l.pad, net.delta+i*l.c*l.h*l.w);
}
}
}

5. Dropout层

5.1 前向传播

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
26
27
28
29
30
31
32
/*
** dropout层前向传播函数
** 输入: l 当前dropout层网络
** net 整个网络
** 说明:dropout层同样没有训练参数,因此前向传播比较简单,只完成一个事:按指定概率l.probability,
** 丢弃输入元素,并将保留下来的输入元素乘以比例因子(采用的是inverted dropout,这种方式实现更为方便,
** 且代码接口比较统一,想想看,如果采用标准的droput,则测试阶段还需要进入forward_dropout_layer(),
** 使每个输入乘以保留概率,而使用inverted dropout,测试阶段根本就不需要进入到forward_dropout_layer)。
** 说明2:dropout层输入与输出元素个数相同(即l.intputs=l.outputs)
** 说明3:关于inverted dropout,在网上随便搜索关于dropout的博客,都会讲到,这里给一个博客链接:https://yq.aliyun.com/articles/68901
*/
void forward_dropout_layer(dropout_layer l, network net)
{
int i;
// 如果当前网络不是处于训练阶段而处于测试阶段,则直接返回(使用inverted dropout带来的方便)
if (!net.train) return;
// 遍历dropout层的每一个输入元素(包含整个batch的),按照指定的概率l.probability置为0或者按l.scale缩放
for(i = 0; i < l.batch * l.inputs; ++i){
// 产生一个0~1之间均匀分布的随机数
float r = rand_uniform(0, 1);
// 每个输入元素都对应一个随机数,保存在l.rand中
l.rand[i] = r;
// 如果r小于l.probability(l.probability是舍弃概率),则舍弃该输入元素,注意,舍弃并不是删除,
// 而是将其值置为0,所以输入元素个数总数没变(因故输出元素个数l.outputs等于l.inputs)
if(r < l.probability) net.input[i] = 0;
// 否则保留该输入元素,并乘以比例因子
else net.input[i] *= l.scale;
}
}

5.2 反向传播

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
26
27
28
29
30
/*
** dropout层反向传播函数
** 输入: l 当前dropout层网络
** net 整个网络
** 说明:dropout层的反向传播相对简单,因为其本身没有训练参数,也没有激活函数,或者说激活函数就为f(x) = x,也
** 也就是激活函数关于加权输入的导数值为1,因此其自身的敏感度值已经由其下一层网络反向传播时计算完了,
** 没有必要再乘以激活函数关于加权输入的导数了。剩下要做的就是计算上一层的敏感度图net.delta,这个计算也很简单,详见下面注释。
*/
void backward_dropout_layer(dropout_layer l, network net)
{
// 如果进入backward_dropout_layer()函数,那没得说,一定是训练阶段,因为测试阶段压根就没有反向过程,只有前向过程,
// 所以就不再需要像forward_dropout_layer()函数中一样判断net.train是不是处于训练阶段了
int i;
// 如果net.delta为空,则返回(net.delta为空则说明已经反向到第一层了,此处所指第一层,是net.layers[0],
// 也是与输入层直接相连的第一层隐含层,详细参见:network.c中的forward_network()函数)
if(!net.delta) return;
// 因为dropout层的输入输出元素个数相等,所以dropout层的敏感度图的维度就为l.batch*l.inputs(每一层的敏感度值与该层的输出维度一致),
// 以下循环遍历当前层的敏感度图,并根据l.rand的指示反向计算上一层的敏感度值,由于当前dropout层与上一层之间的连接没有权重,
// 或者说连接权重为0(对于舍弃的输入)或固定的l.scale(保留的输入,这个比例因子是固定的,不需要训练),所以计算过程比较简单,
// 只需让保留输入对应输出的敏感度值乘以l.scale,其他输入(输入是针对当前dropout层而言,实际为上一层的输出)的敏感度值直接置为0即可
for(i = 0; i < l.batch * l.inputs; ++i){
float r = l.rand[i];
// 与前向过程forward_dropout_layer照应,根据l.rand指示,如果r小于l.probability,说明是舍弃的输入,其敏感度值为0;
// 反之是保留下来的输入元素,其敏感度值为当前层对应输出的敏感度值乘以l.scale
if(r < l.probability) net.delta[i] = 0;
else net.delta[i] *= l.scale;
}
}

6. Maxpool层

6.1 前向传播

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/*
** 最大池化层前向传播函数:计算l层的输出
** 输入: l 当前层(最大池化层)
** net 整个网络结构
** 说明:最大池化层处理图像的方式与卷积层类似,也是将最大池化核在图像平面上按照指定的跨度移动,
** 并取对应池化核区域中最大元素值为对应输出元素。最大池化层没有训练参数(没有权重以及偏置),
** 因此,相对与卷积来说,其前向(以及下面的反向)过程比较简单,实现上也是非常直接,不需要什么技巧。
*/
void forward_maxpool_layer(const maxpool_layer l, network net)
{
int b,i,j,k,m,n;
// 初始偏移设定为四周补0长度的负值
int w_offset = -l.pad;
int h_offset = -l.pad;
// 获取当前层的输出尺寸
int h = l.out_h;
int w = l.out_w;
// 获取当前层输入图像的通道数,为什么是输入通道数?不应该为输出通道数吗?
//实际二者没有区别,对于最大池化层来说,输入有多少通道,输出就有多少通道!
int c = l.c;
// 遍历batch中每一张输入图片,计算得到与每一张输入图片具有相同通道数的输出图
for(b = 0; b < l.batch; ++b){
// 对于每张输入图片,将得到通道数一样的输出图,以输出图为基准,按输出图通道,行,列依次遍历
// (这对应图像在l.output的存储方式,每张图片按行铺排成一大行,然后图片与图片之间再并成一行)。
// 以输出图为基准进行遍历,最终循环的总次数刚好覆盖池化核在输入图片不同位置进行池化操作。
for(k = 0; k < c; ++k){
for(i = 0; i < h; ++i){
for(j = 0; j < w; ++j){
// out_index为输出图中的索引:out_index = b * c * w * h + k * w * h + h * w + w,
//展开写可能更为清晰些
int out_index = j + w*(i + h*(k + c*b));
float max = -FLT_MAX;
// FLT_MAX为c语言中float.h定义的对大浮点数,
//此处初始化最大元素值为最小浮点数
// 最大元素值的索引初始化为-1
// 下面两个循环回到了输入图片,计算得到的cur_h以及cur_w都是在当前层所有输入元素的索引,
//内外循环的目的是找寻输入图像中,
// 以(h_offset + i*l.stride, w_offset + j*l.stride)为左上起点,
//尺寸为l.size池化区域中的最大元素值max及其在所有输入元素中的索引max_i
for(n = 0; n < l.size; ++n){
for(m = 0; m < l.size; ++m){
// cur_h,cur_w是在所有输入图像中第k通道中的cur_h行与cur_w列,
//index是在所有输入图像元素中的总索引。
// 为什么这里少一层对输入通道数的遍历循环呢?因为对于最大池化层来说
//输入与输出通道数是一样的,并在上面的通道数循环了!
int cur_h = h_offset + i*l.stride + n;
int cur_w = w_offset + j*l.stride + m;
int index = cur_w + l.w*(cur_h + l.h*(k + b*l.c));
// 边界检查:正常情况下,是不会越界的,但是如果有补0操作,就会越界了,
//这里的处理方式是直接让这些元素值为-FLT_MAX
// (注意虽然称之为补0操作,但实际不是补0),总之,这些补的元素永远不会充当最大元素值。
int valid = (cur_h >= 0 && cur_h < l.h &&
cur_w >= 0 && cur_w < l.w);
float val = (valid != 0) ? net.input[index] : -FLT_MAX;
// 记录这个池化区域中的最大的元素值及其在所有输入元素中的总索引
max_i = (val > max) ? index : max_i;
max = (val > max) ? val : max;
}
}
// 由此得到最大池化层每一个输出元素值及其在所有输入元素中的总索引。
// 为什么需要记录每个输出元素值对应在输入元素中的总索引呢?因为在下面的反向过程中需要用到,
//在计算当前最大池化层上一层网络的敏感度时,
// 需要该索引明确当前层的每个元素究竟是取上一层输出(也即上前层输入)的哪一个元素的值,
//具体见下面backward_maxpool_layer()函数的注释。
l.output[out_index] = max;
l.indexes[out_index] = max_i;
}
}
}
}
}

6.2 反向传播

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*
** 最大池化层反向传播传播函数
** 输入: l 当前最大池化层
** net 整个网络
** 说明:这个函数看上去很简单,比起backward_convolutional_layer()少了很多,这都是有原因的。实际上,在darknet中,不管是什么层,
** 其反向传播函数都会先后做两件事:
** 1)计算当前层的敏感度图l.delta、权重更新值以及偏置更新值;
** 2)计算上一层的敏感度图net.delta(部分计算,要完成计算得等到真正到了这一层再说)。
** 而这里,显然没有第一步,只有第二步,而且很简单,这是为什么呢?
** 首先回答为什么没有第一步。注意当前层l是最大池化层,最大池化层没有训练参数,
** 说的再直白一点就是没有激活函数,或者认为激活函数就是f(x)=x,所以激活函数对于加权输入的导数其实就是1,
** 正如在backward_convolutional_layer()注释的那样,每一层的反向传播函数的第一步是将之前
** (就是下一层计算得到的,注意过程是反向的)未计算完得到的l.delta乘以激活函数对加权输入的导数,
** 以最终得到当前层的敏感度图,而对于最大池化层来说,每一个输出对于加权输入的导数值都是1,
** 同时并没有权重及偏置这些需要训练的参数,自然不再需要第一步;对于第二步为什么会如此简单,可以参考:
** https://www.zybuluo.com/hanbingtao/note/485480,最大池化层它就是这么简单,剩下的参考下面的注释。
*/
void backward_maxpool_layer(const maxpool_layer l, network net)
{
int i;
// 获取当前最大池化层l的输出尺寸h,w
int h = l.out_h;
int w = l.out_w;
// 获取当前层输入的通道数,为什么是输入通道数?不应该为输出通道数吗?实际二者没有区别,
//对于最大池化层来说,输入有多少通道,输出就有多少通道!
int c = l.c;
// 计算上一层的敏感度图(未计算完全,还差一个环节,这个环节等真正反向到了那层再执行)
// 这个循环很有意思,循环总次数为当前层输出总元素个数(包含所有输入图片的输出,
//即维度为l.out_h * l.out_w * l.c * l.batch,注意此处l.c==l.out_c),
// 而不是上一层输出总元素个数,为什么呢?是因为对于最大池化层而言,
//其每个输出元素对仅受上一层输出对应池化核区域中最大值元素的影响,所以当前池化层每个输出元素
// 对于上一层输出中的很多元素的导数值为0,而对最大值元素,其导数值为1;再乘以当前层的敏感度图,
//导数值为0的还是为0,导数值为1则就等于当前层的敏感度值。
// 以输出图总元素个数进行遍历,刚好可以找出上一层输出中所有真正起作用(在某个池化区域中充当了最大元素值)
//也即敏感度值不为0的元素,而那些没有起作用的元素,
// 可以不用理会,保持其初始值0就可以了。
// 详细原理推导可以参见:https://www.zybuluo.com/hanbingtao/note/485480
for(i = 0; i < h*w*c*l.batch; ++i){
// 遍历的基准是以当前层的输出元素为基准的,l.indexes记录了当前层每一个输出元素与
//上一层哪一个输出元素有真正联系(也即上一层对应池化核区域中最大值元素的索引),
// 所以index是上一层中所有输出元素的索引,且该元素在当前层某个池化域中充当了最大值元素,
//这个元素的敏感度值将直接传承当前层对应元素的敏感度值。
// 而net.delta中,剩下没有被index按索引访问到的元素,就是那些没有真正起到作用的元素,
//这些元素的敏感度值为0(net.delta已经在前向时将所有元素值初始化为0)
// 至于为什么要用+=运算符,原因有两个,和卷积类似:一是池化核由于跨度较小,导致有重叠区域;
//二是batch中有多张图片,需要将所有图片的影响加起来。
int index = l.indexes[i];
net.delta[index] += l.delta[i];
}
}

7. RNN

darknet中的RNN是vanilla RNN,RNN层本质上是三个全连接层构成的,具体结构可以参考 https://pjreddie.com/darknet/rnns-in-darknet/

7.1 前向传播层

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/*
** RNN层前向传播
** 输入 l 当前的RNN层
** net 当前网络
**
** RNN层前向传播与其他网络不同,RNN中的全连接层的当前状态与上一个时间的状态有关,
** 所以要在每次传播后记录上一个时刻的状态
*/
void forward_rnn_layer(layer l, network net)
{
network s = net;
s.train = net.train;
int i;
layer input_layer = *(l.input_layer);
layer self_layer = *(l.self_layer);
layer output_layer = *(l.output_layer);
/* 开始训练前要将三个全连接层的错误值设置为0 */
fill_cpu(l.outputs * l.batch * l.steps, 0, output_layer.delta, 1);
fill_cpu(l.hidden * l.batch * l.steps, 0, self_layer.delta, 1);
fill_cpu(l.hidden * l.batch * l.steps, 0, input_layer.delta, 1);
/* 如果网络处于训练状态,要将state设置为0,因为初始状态的上一时刻状态是不存在的,我们只能假设它存在,并把它赋值为0 */
if(net.train) fill_cpu(l.hidden * l.batch, 0, l.state, 1);
/*
** 以下是RNN层前向传播的主要过程,该层总共要传播steps次,每次的输入batch个字符。
** Vanilla RNN具体结构参考 https://pjreddie.com/darknet/rnns-in-darknet/,
** 这里我只简单的说明一下。
** Vanilla RNN的RNN层虽然包含三个全连接层,但是只有中间一层(也就是self_layer)与传统的RNN的隐含层一致。
**
** 第一层input_layer可以理解为embedding层,它将input编码为一个hidden维的向量,
** 以darknet的字符预测问题为例,网络的input是英文字母
** 采用one-hot编码,是一个256维的向量,embedding后成为了hidden维的向量。
**
** 第二层self_layer与普通RNN的隐含层功能相同,它接收输入层和上一时刻的状态作为输入。
**
** 第三层output_layer,接收self_layer的输出为输入,需要注意的是这一层的输出并不是最终结果,
** 还需要做进一步处理,还是以darknet的字符预测为例,第三层的输出要进一步转化为一个256维的向量,
** 然后进行归一化,找到概率最大的字符作为预测结果
*/
for (i = 0; i < l.steps; ++i) {
s.input = net.input;
forward_connected_layer(input_layer, s);
s.input = l.state;
forward_connected_layer(self_layer, s);
float *old_state = l.state;
// 将当前状态存入上一时刻状态
if(net.train) l.state += l.hidden*l.batch;
// 如果网络处于训练状态,注意的是上一时刻的状态包含一个batch
// 如何设置当前状态,由shortcut的值决定
copy_cpu(l.hidden * l.batch, old_state, 1, l.state, 1);
}else{
fill_cpu(l.hidden * l.batch, 0, l.state, 1);
}
axpy_cpu(l.hidden * l.batch, 1, input_layer.output, 1, l.state, 1);
axpy_cpu(l.hidden * l.batch, 1, self_layer.output, 1, l.state, 1);
s.input = l.state;
forward_connected_layer(output_layer, s);
/* 一次传播结束,将三个层同时向前推移一步 */
net.input += l.inputs*l.batch;
increment_layer(&input_layer, 1);
increment_layer(&self_layer, 1);
increment_layer(&output_layer, 1);
}
}

7.1 后向传播层

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/*
** 误差后向传播
** 输入 l 当前RNN层
** net 当前网络
*/
void backward_rnn_layer(layer l, network net)
{
network s = net;
s.train = net.train;
int i;
layer input_layer = *(l.input_layer);
layer self_layer = *(l.self_layer);
layer output_layer = *(l.output_layer);
/* 误差传播从网络中最后一步开始 */
increment_layer(&input_layer, l.steps-1);
increment_layer(&self_layer, l.steps-1);
increment_layer(&output_layer, l.steps-1);
l.state += l.hidden*l.batch*l.steps;
for (i = l.steps-1; i >= 0; --i) {
copy_cpu(l.hidden * l.batch, input_layer.output, 1, l.state, 1);
axpy_cpu(l.hidden * l.batch, 1, self_layer.output, 1, l.state, 1);
/* 计算output_layer层的误差 */
s.input = l.state;
s.delta = self_layer.delta;
backward_connected_layer(output_layer, s);
l.state -= l.hidden*l.batch;
/*
if(i > 0){
copy_cpu(l.hidden * l.batch, input_layer.output - l.hidden*l.batch, 1, l.state, 1);
axpy_cpu(l.hidden * l.batch, 1, self_layer.output - l.hidden*l.batch, 1, l.state, 1);
}else{
fill_cpu(l.hidden * l.batch, 0, l.state, 1);
}
*/
/* 计算self_layer层的误差 */
s.input = l.state;
s.delta = self_layer.delta - l.hidden*l.batch;
if (i == 0) s.delta = 0;
backward_connected_layer(self_layer, s);
copy_cpu(l.hidden*l.batch, self_layer.delta, 1, input_layer.delta, 1);
if (i > 0 && l.shortcut) axpy_cpu(l.hidden*l.batch, 1, self_layer.delta,
1, self_layer.delta -l.hidden*l.batch, 1);
s.input = net.input + i*l.inputs*l.batch;
if(net.delta) s.delta = net.delta + i*l.inputs*l.batch;
else s.delta = 0;
/* 计算input_layer层的误差 */
backward_connected_layer(input_layer, s);
/* 误差传播一步之后,需要重新调整各个连接层, 向后移动一步 */
increment_layer(&input_layer, -1);
increment_layer(&self_layer, -1);
increment_layer(&output_layer, -1);
}
}

工程地址:
https://github.com/hgpvision/darknet

博客全站共85.5k字