yolo反向传播源码分析

反向传播

这里主要参考github上面的yolo源代码解析,如侵则删,虽然以yolov2作为分析对象,但yolov3较yolov2并没有改变多少:D
这里写图片描述

首先符号定义:
      al:l a l : 第 l 层 输 出
      zl:al,:al=σ(zl) z l : a l 为 经 激 活 函 数 前 的 值 , 即 : a l = σ ( z l )
      C:,: C : 误 差 , 如 整 个 网 络 的 输 出 和 标 签 的 差 值 的 平 方 和 :
      bl:l b l : 第 l 层 偏 置
      wl:l w l : 第 l 层 权 重
      δl=Czl1:l δ l = ∂ C ∂ z l − 1 : 第 l 层 的 敏 感 度 图
      α: α : 学 习 率

公式计算:
注意这里以单个数据为计算,如果要计算batch个数据,下面的公式结果为batch数据计算的结果和
    1.先计算敏感度图,即 δ,,(Hadamard) δ , 也 叫 误 差 图 , 这 里 ⨀ 为 逐 元 素 乘 积 ( H a d a m a r d 乘 积 ) :
δl1=Czl1=Czlzlal1al1zl1=δlzlal1σ(zl1)(1) (1) δ l − 1 = ∂ C ∂ z l − 1 = ∂ C ∂ z l ∂ z l ∂ a l − 1 ∂ a l − 1 ∂ z l − 1 = δ l ∂ z l ∂ a l − 1 ⨀ σ ′ ( z l − 1 )

       若l层为全连接层:
δl1=(Wl)Tδlσ(zl1)(1.1) (1.1) δ l − 1 = ( W l ) T δ l ⨀ σ ′ ( z l − 1 )

       若l-1层为卷积层:

δl1=δlrot180(wl)σ(zl1)(1.2) (1.2) δ l − 1 = δ l ∗ r o t 180 ( w l ) ⨀ σ ′ ( z l − 1 )

   2.偏置更新需要的梯度:
       全连接层:
Cbl=δl(2.1) (2.1) ∂ C ∂ b l = δ l

       卷积层:
Cbl=w,hδw,h,u,vsize(2.2) (2.2) ∂ C ∂ b l = ∑ w , h δ w , h , 其 中 u , v 代 表 卷 积 核 输 出 的 s i z e 的 长 宽

   3.权重更新需要的梯度,这里 为卷积符号:

       全连接层:
(3.1)Cwl=(δl)Tal1
       卷积层: Cwl=al1δl(3.2) (3.2) ∂ C ∂ w l = a l − 1 ∗ δ l

       最后根据上面,通过梯度下降,对偏置和权重进行更新:
bl=blαCbl(4) (4) b l = b l − α ∂ C ∂ b l
wl=wlαCwl(5) (5) w l = w l − α ∂ C ∂ w l

具体推导过程见:
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层的
deltal1=δzll1(p1) (p1) d e l t a l − 1 = δ ∂ z l ∂ l − 1 ,
2.在l-1层调用backward函数开头部分,再计算:
δl1=deltal1σ(zl1)(p2) (p2) δ l − 1 = d e l t a l − 1 ⨀ σ ( z l − 1 )
完成对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为例):
w=wαC(1) (1) w = w − α ▽ C
改成:
vt=γvt1+αC(2) (2) v t = γ v t − 1 + α ▽ C
w=wvt(3) (3) w = w − v t
来提高收敛速度

主要参考:
yolo源代码解析

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
需要学习Windows系统YOLOv4的同学请前往《Windows版YOLOv4目标检测实战:原理与码解析》,课程链接 https://edu.csdn.net/course/detail/29865【为什么要学习这门课】 Linux创始人Linus Torvalds有一句名言:Talk is cheap. Show me the code. 冗谈不够,放码过来!  代码阅读是从基础到提高的必由之路。尤其对深度学习,许多框架隐藏了神经网络底层的实现,只能在上层调包使用,对其内部原理很难认识清晰,不利于进一步优化和创新。YOLOv4是最近推出的基于深度学习的端到端实时目标检测方法。YOLOv4的实现darknet是使用C语言开发的轻型开深度学习框架,依赖少,可移植性好,可以作为很好的代码阅读案例,让我们深入探究其实现原理。【课程内容与收获】 本课程将解析YOLOv4的实现原理和码,具体内容包括:- YOLOv4目标检测原理- 神经网络及darknet的C语言实现,尤其是反向传播的梯度求解和误差计算- 代码阅读工具及方法- 深度学习计算的利器:BLAS和GEMM- GPU的CUDA编程方法及在darknet的应用- YOLOv4的程序流程- YOLOv4各层及关键技术的码解析本课程将提供注释后的darknet的码程序文件。【相关课程】 除本课程《YOLOv4目标检测:原理与码解析》外,本人推出了有关YOLOv4目标检测的系列课程,包括:《YOLOv4目标检测实战:训练自己的数据集》《YOLOv4-tiny目标检测实战:训练自己的数据集》《YOLOv4目标检测实战:人脸口罩佩戴检测》《YOLOv4目标检测实战:中国交通标志识别》建议先学习一门YOLOv4实战课程,对YOLOv4的使用方法了解以后再学习本课程。【YOLOv4网络模型架构图】 下图由白勇老师绘制  

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值