YOLOv3反向传播原理 之 全流程源码分析


在前两篇文章《YOLOv3反向传播原理 之 公式推导》和《Batch Normalization原理 与 反向传播公式推导》中,我们对反向传播原理进行了推导。
求导过程分两步:一是对第 l l l层网络输出结果求导;二是对权重(包括偏置)求导,两个过程交替进行。需要关注的是对第 l − 1 l-1 l1层网络输出结果求导时,需要将 l l l层的误差加权累加逆向传递到 l − 1 l-1 l1层。
对于YOLOv3,我们大致回顾一下前向传播过程,数据先通过若干卷积层(卷积层中包含卷积、标准化和ReLU激活三个过程),再通过若干两层卷积层构成的残差结构(将 l − 2 l-2 l2层的输出结果和 l l l层输出结果累加),然后进入YOLO层(YOLO层中只针对位置偏差和分类使用sigmoid函数进行处理,针对边框宽度和高度直接采用前一层卷积层输出的值,然后形成最终结果输出;后两个YOLO层需要通过路由层将前面YOLO层之前的内容先累加过来,这个叫做FPN方法(特征图金字塔网络))。
因此对于反向传播源码分析,我们分别针对YOLO层、卷积层(CNN)、路由层(route层)、残差层(shortcut层)、标准化层(batch_normalize层)进行分析,最后我们再对激活层进行简单说明。

1.YOLOv3网络训练中反向传播主体流程

反向传播存在于训练过程中,入口函数为detector.c的loss = train_network(net, train);下面是传播流程:

步骤 函数 源码文件 功能
1 train_detector(datacfg, cfg, weights, gpus, ngpus, clear); detector.c 训练数据集,参数有数据路径文件、网络配置文件、权重文件、gpu地址及个数、是否清除已训练图片数量标记
2 loss = train_network(net, train); network.c 在train_detector函数中,参数有网络数据结构network *net和data train。train通过线程函数循环按batch读入。
3 float err = train_network_datum(net); network.c 在train_network函数中,参数net中包含net->input, net->truth等数据信息,net->input为resize后的三通道数据。
4 backward_network(net); network.c 在train_network_datum函数中,参数net中更新了net->seen信息,就是每个批次训练过的图片数量
5 l.backward(l, net); darknet.h 在backward_network函数中,按照层进行倒序循环调用,这个函数功能在make_yolo_layer、make_convolutional_layer中进行定义
6 通过l.backward进行每个层具体的反向传播计算 l为当前层的layer,net中的input和delta为前一层的input和delta;更新的l.weight
7 update_network(net); network.c 次步骤在backward_network(net)执行之后,对weight和biases进行更新
8 save_weights(net, buff); parse.c 在完成一个batch的训练之后,在network结构体中更新一次weight,每训练100个batch,保存一次weights到一个新的weight文件,这里保存的除了卷积核还有每个层的偏置吧biases,这时候就算完成更新了w和b

1.1 初始化

在之前《YOLOv3反向传播原理 之 公式推导》中推导反向传播公式时,都是针对一张图片的数据处理,但是在实际的软件实现中,每一次训练都是按照一个batch的图片进行训练,在上表第3步的train_network_datum函数中(源码如下),通过紧邻的forward_network(net)和backward_network(net)实现前向传播和反向传播。

float train_network_datum(network *net)
{
   
    *net->seen += net->batch;
    net->train = 1;
    forward_network(net);//前向传播
    backward_network(net);//反向传播
    float error = *net->cost;
    /*利用update_network(net)-->l.update(在不同层定义不同,
    如在convolutional_layer.c中就是update_convolutional_layer)更新weight
    */
    //训练图片达到一个整批就可以更新一次网络参数
    if(((*net->seen)/net->batch)%net->subdivisions == 0) update_network(net);
    return error;
}

在forward_network(net)中,r如果l.delta!=0通过,对整个批次的l.delta赋值为0,源码如下,:

//前向网络操作的核心代码
void forward_network(network *netp)
{
   
......
    //#pragma omp parallel for
    for(i = 0; i < net.n; ++i){
   		//每一个网络层循环一次
        net.index = i;
        layer l = net.layers[i];//这一步隐藏玄机,在一开始解析网络的时候,就决定了l会对应那个前向函数
        if(l.delta){
   
            fill_cpu(l.outputs * l.batch, 0, l.delta, 1);//输出×批尺寸,统统填0
        }
         ......
        }
    }
......
}

1.2 batch内梯度累加

反向传播计算梯度的第一步是计算LOSS函数,LOSS函数中,是将一个batch的图片所有处理结果的误差(delta)进行累加计算的,所以在计算每个权重(weight)的梯度时,也需要在一个批次内进行累加。
前向传播的过程中,l.delta变量中存储了每个yolo层对一个批次内每个图片计算的误差,但是这些信息在这个阶段并没有累加,而是存储于内存,再最终计算weight的时候进行累加。

1.3 network和layer 中的关键变量

重点需要关注的几个输入输出变量:
network *net;

typedef struct network{
   
    int n;				    // 网络总层数(make_network()时赋值),但是直接下载的现成的weight文件有现成的
    int batch;				// parse_net_options()中赋值:一个batch含有的图片张数,批处理的图片个数,一般再训练中的batch=cfg文件中batch值/subdivision
    size_t *seen;			// 目前已经读入的图片张数(网络已经处理的图片张数)(在make_network()中动态分配内存)
    layer *layers;	   	    //存储网络所有的层的信息,在make_network()中动态分配内存
    int inputs;				//在实际操作过程中,input作为network的一部分,在network和layer两个结构体中有inputs和outputs
					        //注意,都是int型,表示一张输入图片的元素个数
					        //如果网络配置文件中未指定,则默认等于net->h * net->w * net->c,在parse_net_options()中赋值

    int outputs;			//一张输入图片对应的输出元素个数,对于一些网络,可由输入图片的尺寸及相关参数计算出
					        //比如卷积层,可以通过输入尺寸以及跨度、核大小计算出;
                        	//对于另一些尺寸,则需要通过网络配置文件指定,如未指定,取默认值1,比如全连接层
    float *delta;			//存储一个临时梯度,一般是当前层前一层输出结果的梯度
    float *workspace;		//用于存储临时计算结果
    
    float *input;			// 中间变量,用来暂存某层网络的输入(包含一个batch的输入,比如某层网络完成前向,
					        // 将其输出赋给该变量,作为下一层的输入,可以参看network.c中的forward_network()与backward_network()两个函数),
                            // 当然,也是网络接受最原始输入数据(即第一层网络接收的输入)的变量
					        //(比如在图像检测训练中,最早在train_detector()->train_network()->get_next_batch()函数中赋值)
} network;

layer l,prev;

struct layer{
   
    float * delta;			//存储针对当前层输出结果的梯度
    float * output;			//当前层的输出feature map的集合
    int w,h,c;				//当前层输出的feature map宽度,高度,每组卷积核(滤波器)的个数,c的个数为前一层输出中feature map的个数,有时候也说是输出的维度

    float * biases;			//更新的偏置
    float * bias_updates;	//偏置的梯度
    float * weights;		//权重的梯度
    float * weight_updates;	//更新的权重,不知道是什么原因,作者编程时把权重和梯度形式上反着写

    float * scales;			//标准化中的尺度变换系数
    float * scale_updates;	//标准化中的尺度变换系数的梯度

    int out_h, out_w, out_c;	//当前层输出结果的高,宽,通道数
int n;				            //卷积核的组数,决定了当前层输出的feature map的个数

    float * mean;			//feature map均值,用于标准化层
    float * variance;		//feature map均方值,用于标准化层

    float * mean_delta;		//梯度均值,用于标准化层
    float * variance_delta;	//梯度均方值,用于标准化层

    float * rolling_mean;	//weight文件中的初始均值,用于标准化层
    float * rolling_variance;	//weight文件中的初始均方值,用于标准化层

    int batch_normalize;	//该层中是否有标准化
    int batch;				//一个层包含的处理的一个批次图片数量,主要为input,output,delta,weight等保存空间和确定计算次数,一般具体为多少看batch是否除以subdivisions,当说完成一个batch图片训练时候,需要除,当更新权重时候又会乘回来

    int inputs;			    //一个层输入值的尺寸
    int outputs;			//一个层输出值的尺寸
    int nweights;			//一个层权重值的尺寸
    int nbiases;			//一个层偏置值的尺寸
};

下面我们就分析前面表格中的第6步中的内容:

2.YOLO层反向传播源码分析

YOLO层反向传播只有一个直接传播的步骤。l.delta存储的就是LOSS针对YOLO层前一层输出l.output的梯度,这个梯度求解过程中,使用平方差LOSS函数和交叉熵LOSS函数的效果都是一样的,这个推导过程在前一篇文章《YOLO中LOSS函数的计算》中有说明。YOLO反向传播的主函数(yolo_layer.c)如下:

//const layer l, network net分别对应表格第5步l.backward(l, net)中的l和net。
void backward_yolo_layer(const layer l, network net)
{
   
   //axpy_cpu在blas.c中,主要用于将l.delta传递到前一层的net.delta中。
   //这里参考make_yolo_layer函数,可知l.inputs = l.outputs = h*w*n*(classes + 4 + 1);
   axpy_cpu(l.batch*l.inputs, 1, l.delta, 1, net.delta, 1);
}

axpy_cpu函数可以完成将X中元素乘以权值ALPHA,和Y中对应位置元素相乘。那么不是说好的直接传递吗?根据1.1节初始化的分析,前一层的delta统统赋值为0,所以累加等于赋值。说实话,本人感觉直接使用blas.h中的copy_cpu函数也是一样的效果。

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];
}

好了,把YOLO层的梯度求完了,按照cfg文件,就该到卷积层了。

3.卷积层(CNN)反向传播源码分析

3.1反向传播主函数backward_convolutional_layer源码分析

convolutional_layer类型实际上就是layer,在convolutional_layer.h中有声明:

typedef layer convolutional_layer;

接着看convolutional_layer.c中源码和分析:

//反向传播,反向的关键,反向传播的各种数据通过两个参数传递,一个是当前层(l层)layer,一个是前一层(l-1层)network
//convolutional_layer 的类型还是layer
//所有的*input,*output,*delta,*truth都是在network中的,因为这些信息需要连贯性
//每一层feature map的宽、高、每组卷积核中卷积核的个数,滤波器组数等信息都是在layer中。
//反向传播的计算和前向传播一样也是要用到重排和矩阵乘法计算
// net.delta = l.delta×激活函数对权重的误差,卷积误差传递
void backward_convolutional_layer(convolutional_layer l, network net)
{
   
    //*************尺寸参数定义和初始化*****************//
    int i, j;
    int m = l.n/l.groups;//滤波器核函数的个数×组个数,这里group都是1,在parse.c中设置
    int n = l.size*l.size*l.c/l.groups;//当前层卷积核总尺寸,size是卷积核的边长,l.c为卷积核(即滤波器)个数,当前层的卷积核用于处理l-1层输出
    int k = l.out_w*l.out_h;//当前层一个feature map的尺寸,l.delta的维度为[l.n, l.out_w*l.out_h],l.delta参考l.output


    //*************计算一个CNN模块中经过激活层后的梯度*****************//
    //gradient_array在activation.c中,求当前层经过激活函数的梯度,根据配置文件这里都是ReLU函数,l.output>0,则求导为结果为1×l.output,否则为0×l.output,这里的功能是经过激活层进行梯度传递,也可认为是传递误差
    //l.delta为激活层计算得到的梯度,后续和当前层权重累乘相加完成梯度(误差)传递,l.activation为激活函数类型
    gradient_array(l.output, l.outputs*l.batch, l.activation, l.delta);

    //*************计算一个CNN模块中经过batch_normalize层后的梯度*****************//
    //如果有batch_normalize层,则计算batch_normalize层传递结果
    //如果没有,直接通过backward_bias完成计算不同图片输出结果的梯度累加
    if(l.batch_normalize){
   
        backward_batchnorm_layer(l, net);
    } else {
   
        //k为feature map尺寸,l.n为输出维度或feature map个数k×l.n=一个图片output输出的尺寸
        backward_bias(l.bias_updates, l.delta, l.batch, l.n, k);
    }

    //*************计算一个CNN模块中经过CNN层后的梯度*****************//
    //按批次图像循环,在计算权重的梯度的时候将针对前一层(l-1层)不同图片处理结果误差计算结果进行累加
    //注意,前面不是已经把不同批次的delta都计算了一遍了吗,这里怎么又搞了一遍?前面计算的是第l层的梯度,累加后用于针对前一层每张图输出结果梯度的计算。这里累加是针对每张图计算结果的累加。
    //其实我们很容易想到还有一种方法,就是前面先不累加,从一开始就逐图计算权重梯度,再最终累加。这种计算方法首先要占用很大的内存,另外还可能存在梯度消失
    for(i = 0; i < l.batch; ++i){
   
        for(j = 0; j < l.groups; ++j){
   
            //l.delta为指针传递参数
            //a是delta的矩阵,表示一个批次中第i个图处理结果的位置
            float *a = l.delta + (i*l.groups + j)*m*k;
            //创建一个空的workspace指针,用于缓存信息
            float *b = net.workspace;
            //当前层中已经更新的权重指针,l.nweights为所有卷积核中每个组的总权重尺寸,这里l.groups==1用不到
            //当前层权重的尺寸为l.nweights,make_convolutional_layer中初始化了l.nweights = c/groups*n*size*size
            float *c = l.weight_updates + j*l.nweights/l.groups;//更新的权重

            //net.input就是前一层的输出,在公式中就是y(l-1)
            float *im  = net.input + (i*l.groups + j)*l.c/l.groups*l.h*l.w;
            //net.delta就是前一层的梯度,即LOSS对前一层输出的梯度
            float *imd = net.delta + (i*l.groups + j)*l.c/l.groups*l.h*l.w;//图像误差

            //如果当前层卷积核尺寸为1,b = im,前一层的输出不用重排
            if(l.size == 1){
   
                b = im;
            //如果当前层卷积核尺寸>1,前一层的输出需要重排,便于计算对前一层的梯度时候后一层的权重与之相乘累加
            } else {
   
                im2col_cpu(im, l.c/l.groups, l.h, l.w, 
                        l.size, l.stride, l.pad, b);
            }

            
            //*************计算当前CNN层权重的的梯度*****************//
            //*****a不转置,b转置,beta = 1,则c=ab+c,表示将针对不同图片权重的梯度计算结果累加*****//
            //更新了l.weight_updates + j*l.nweights/l.groups
            //注意,这里a不转置,重排的b转置了,每一个卷积核对应的输出(每个输出平面)的尺寸l.out_w*l.out_h=重排后的列数,转置后变成行数
            //然后得到每个feature map,即每个卷积核的weight的调整梯度,就是每个batch中的几张图片的导数要合并计算
            //c = delta×图像,当前层delta×前一层输出y(即net.input),叠加更新,求出来的是weight的导数
            gemm(0,1,m,n,k,1,a,k,b,k,1,c,n);

            //这一层用于求LOSS对前一层(l-1层)输出的梯度,原理就是将当前层的delta乘以计算当前层的权重,累加后进行误差传递
            //如果net.delta ≠ 0,则进行下一步计算
            //注意delta有network的也有layer的,net.delta是前一层的,l.delta是这一层的           
            if (net.delta) {
   
                //当前层的权重指针
                a = l.weights + j*l.nweights/l.groups;
                //当前层的梯度指针l.delta
                b = l.delta + (i*l.groups + j)*m*k;
                c = net.workspace;
                //如果卷积核尺寸为1,c不变,仍旧为前一层的梯度,此时卷积核的作用只有一个变换维度
                if (l.size == 1) {
   
                    c = imd;
                }

                //*************计算针对前一层输出的的梯度*****************//
                //*****a转置,b不转置,beta = 0,计算c = a×b,LOSS针对前一层输出的梯度不累加,放到下一层计算时候累加*****//
                //b每一行行数就是l.n/groups,列数为l.w*l.h
                //a是当前层权重,也是给前一层计算当前层的权重,很小的一行,一个卷积核排起来构成矩阵,行数为卷积核数,转置后成为列数,b就是delta,列数就是l.outputs×卷积核数
                //转置后:a和c的行数为n(l.size*l.size*l.c/l.groups),列数为l.n,b和c的列数为k(l.out_w*l.out_h)
                //转置后:a的列数和b的行数为l.n,a的步长为n,b的步长为k.
                //a每个卷积核的参数行和b每一列相乘并相加,将不同通道相同位置的值加权再加在一起,这个很清楚:因为一个前一层的输出对应到不同的通道
                //c就是乘得结果,存入workspace,作为中间量,求完存起来用于下个批次求梯度时候使用
                gemm(1,0,n,k,m,1,a,n,b,k,0,c,k);
                
                if (l.size != 1) {
   //最后,再将net.workspace中的值,也就是c中的值转换成imd,存入前一层(l-1层)delta中,这是个指针变量传递,所以能够保存到layer[l-1]中
                    //计算net.delta。imd和net.delta + (i*l.groups + j)*l.c/l.groups*l.h*l.w指向相同的内存
                    col2im_cpu(net.workspace, l.c/l.groups, l.h, l.w, l.size, l.stride, l.pad,
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Linux创始人LinusTorvalds有一句名言:Talk is cheap, Show me the code.(冗谈不够,放码过来!)。 代码阅读是从入门到提高的必由之路。尤其对深度学习,许多框架隐藏了神经网络底层的实现,只能在上层调包使用,对其内部原理很难认识清晰,不利于进一步优化和创新。  YOLOv3是一种基于深度学习的端到端实时目标检测方法,以速度快见长。YOLOv3的实现Darknet是使用C语言开发的轻型开源深度学习框架,依赖少,可移植性好,可以作为很好的代码阅读案例,让我们深入探究其实现原理。  本课程将解析YOLOv3的实现原理源码,具体内容包括: YOLO目标检测原理  神经网络及Darknet的C语言实现,尤其是反向传播的梯度求解和误差计算 代码阅读工具及方法 深度学习计算的利器:BLAS和GEMM GPU的CUDA编程方法及在Darknet的应用 YOLOv3的程序流程及各层的源码解析本课程将提供注释后的Darknet的源码程序文件。  除本课程《YOLOv3目标检测:原理源码解析》外,本人推出了有关YOLOv3目标检测的系列课程,包括:   《YOLOv3目标检测实战:训练自己的数据集》  《YOLOv3目标检测实战:交通标志识别》  《YOLOv3目标检测:原理源码解析》  《YOLOv3目标检测:网络模型改进方法》 建议先学习课程《YOLOv3目标检测实战:训练自己的数据集》或课程《YOLOv3目标检测实战:交通标志识别》,对YOLOv3的使用方法了解以后再学习本课程。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北溟客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值