反向传播
这里主要参考github上面的yolo源代码解析,如侵则删,虽然以yolov2作为分析对象,但yolov3较yolov2并没有改变多少:D
首先符号定义:
公式计算:
注意这里以单个数据为计算,如果要计算batch个数据,下面的公式结果为batch数据计算的结果和
1.先计算敏感度图,即
:
若l
层为全连接层:
若l-1
层为卷积层:
2.偏置更新需要的梯度:
全连接层:
卷积层:
3.权重更新需要的梯度,这里 为卷积符号:
全连接层:
卷积层:
最后根据上面,通过梯度下降,对偏置和权重进行更新:
具体推导过程见:
BP推导——续
CNN的反向传播
DNN反向传播算法要解决的问题
回到yolo训练的函数
//位于network.c,于network.c\train_network(network net, data d)函数调用
float train_network_datum(network net)
{
// 如果使用GPU则调用gpu版的train_network_datum
#ifdef GPU
if(gpu_index >= 0) return train_network_datum_gpu(net);
#endif
// 更新目前已经处理的图片数量,这里的net.batch为子batch,不是配置文件里的
//batch,而是:net.batch = net.batch(完整batch) / net.subdivision
*net.seen += net.batch;
// 标记处于训练阶段
net.train = 1;
forward_network(net);
backward_network(net);
float error = *net.cost;
//在训练了一个完整的batch个数据后才会调用
if(((*net.seen)/net.batch)%net.subdivisions == 0) update_network(net);
return error;
}
可见反向传播主要是两个函数:
- backward_network()
:计算每层各参数的梯度值(累加batch所有的梯度)
- update_network()
:根据计算好的梯度,进行参数更新(如偏置,权重),当然这个函数只有在训练了一个真的batch个数据后才会调用
1.梯度计算
void backward_network(network net)
{
int i;
// 在进行反向之前,先保存一下原先的net,下面会用到orig的input
network orig = net;
for(i = net.n-1; i >= 0; --i){
layer l = net.layers[i];
if(l.stopbackward) break;
/*i = 0时,也即已经到了网络的第1层(或者说第0层,看个人习惯了~)了,
就是直接与输入层相连的第一层隐含层(注意不是输入层,我理解的输入层就是指输入的图像数据,
严格来说,输入层不算一层网络,因为输入层没有训练参数,也没有激活函数),这个时候,不需要else中的赋值,
1)对于第1层来说,其前面已经没有网络层了(输入层不算),因此没有必要再计算前一层的参数,故没有必要在获取上一层;
2)第一层的输入就是图像输入,也即整个net最原始的输入,在开始进行反向传播之前,已经用orig变量保存了
最为原始的net,所以net.input就是第一层的输入,
不需要通过net.input=prev.output获取上一层的输出作为当前层的输入;
3)同1),第一层之前已经没有层了,也就不需要计算上一层的delta,即不需要再将net.delta链接到prev.delta,
此时进入到l.backward()中后,net.delta就是NULL(可以参看network.h中关于delta
的注释),也就不会再计算上一层的敏感度了(比如卷积神经网络中的backward_convolutional_layer()函数)*/
if(i == 0){
net = orig;
}else{
// 获取上一层
layer prev = net.layers[i-1];
// 上一层的输出作为当前层的输入(下面l.backward()会用到,具体是在计算当前层权重更新值时要用到)
net.input = prev.output;
// 上一层的敏感度图(l.backward()会同时计算上一层的敏感度图)
net.delta = prev.delta;
}
// 置网络当前活跃层为当前层,即第i层
net.index = i;
//进入当前层反向传播函数
l.backward(l, net);
}
}
当前层的参数的具体梯度计算过程:
设当前层为第l-1
层,那么计算其敏感度分两步:
1.在l层的backward()
函数的最后部分,会计算l-1
层的
,
2.在l-1
层调用backward函数开头部分,再计算:
完成对l-1
层敏感度的最终计算,以此方法完成每一层的敏感度计算,如图:
这里以全连接层的backward为例:
/*
** 全连接层反向传播函数
** 输入: l 当前全连接层
** net 整个网络
*/
void backward_connected_layer(connected_layer l, network net)
{
int i;
// gradient_array()函数完成激活函数对加权输入的导数,并乘以之前得到的l.delta,得到当前层最终的l.delta(敏感度图),完成当前层敏感度图的计算
//对应于上面公式p1
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中所有图片对应输出元素偏置更新值的叠加。
//对应公式(2.1)
for(i = 0; i < l.batch; ++i){
/*
** axpy是线性代数中一种基本操作,完成y= alpha*x + y操作,其中x,y为矢量,alpha为实数系数
axpy_cpu(int N, float ALPHA, float *X, int INCX, float *Y, int INCY)
*/
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);
}
// 计算当前全连接层的权重梯度值,对应公式(3.1)
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,
//c保存了对应的权重梯度值,
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)
//对应公式(p2)
if(c) gemm(0,0,m,n,k,1,a,k,b,n,1,c,n);
}
2.参数更新
对应与函数update_network(net)
void update_network(network net)
{
int i;
int update_batch = net.batch*net.subdivisions;
//当前学习率
float rate = get_current_rate(net);
for(i = 0; i < net.n; ++i){
layer l = net.layers[i];
if(l.update){
l.update(l, update_batch, rate*l.learning_rate_scale, net.momentum, net.decay);
}
}
}
这里的l.update
,还是以全连接层的更新函数为例:
void update_connected_layer(connected_layer l, int batch, float learning_rate, float momentum, float decay)
{
//更新偏置,对应公式(4),这里学习率除以batch,应该等效于batch个梯度的平均值
//对应下面的公式3
axpy_cpu(l.outputs, learning_rate/batch, l.bias_updates, 1, l.biases, 1);
//计算下次梯度需要的冲量,详见注1,对应下面的公式2
scal_cpu(l.outputs, momentum, l.bias_updates, 1);
if(l.batch_normalize){
axpy_cpu(l.outputs, learning_rate/batch, l.scale_updates, 1, l.scales, 1);
scal_cpu(l.outputs, momentum, l.scale_updates, 1);
}
axpy_cpu(l.inputs*l.outputs, -decay*batch, l.weights, 1, l.weight_updates, 1);
//更新权重,对应公式(5)
axpy_cpu(l.inputs*l.outputs, learning_rate/batch, l.weight_updates, 1, l.weights, 1);
scal_cpu(l.inputs*l.outputs, momentum, l.weight_updates, 1);
}
注:
1.冲量(详见:优化函数详解)
作用将原来的参数更新(以权重w为例):
改成:
来提高收敛速度
主要参考:
yolo源代码解析