资讯专栏INFORMATION COLUMN

【模型推理】谈谈 darknet yolo 的 route 算子

wangbjun / 733人阅读

摘要:为什么要做这个操作呢,有了恒等映射,网络再深我也能通过这个小道把信息给你传递下去,且的导数为,也可防止回传时梯度消失,关键还不增加额外计算量。

欢迎关注我的公众号 [极智视界],获取我的更多笔记分享

O_o>_<o_OO_o~_~o_O

  本文聊一聊 darknet yolo 网络中的 route 算子。

  yolo 是目标检测算法落地常用的网络,具有速度快、精度高的优点,相信很多同学都熟悉,route 层在 yolov1、yolov2、yolov3、yolov4 中均有出现,yolov4 tiny 中的 route 又有了一些新特性,现在的它既能做类似 concatenate 的拼接,也能做类似 slice 的切割,算子功能十分丰富。这里把 route 拿出来聊一聊,主要说一下它的功能、与 darknet 源码实现。

1、route 化作 concatenate

  上面说过 route 能化作 concatenate,来完成拼接的操作,来看看 route 是怎么样幻化成一缕 convatenate 的,下面一张图就可以看明白,看右侧 route 参数栏可知,route 层的输入为 4 和 5,即接两个大卷积输入,维度分别为 160 x 160 x 32、160 x 160 x 32,这里 route 的作用就是把这两个大卷积的输出做拼接,可以看到经 route 作用后的输出维度为 160 x 160 x 64,64 就是 32 + 32 来的。


2、route 化作 slice

  route 除了能化作 concatenate,还能化作 slice 来完成切割的操作,让我们来看看 route 是怎么样幻化成一缕 slice 的。同样来看下面这张图,这时候的 route 只有一个输入,即 2 号大卷积,然后你再仔细观察会发现还有 groups 和 group_id 两个奇怪的参数,这两个参数就是拿来做 切割 的参数。groups 的意思是把输入切成 groups 份,group_id 的意思是拿 第 group_id 索引的数据传出给下一层,所以 yolo 的 route 切割其实和 slice 又不太一样,route 切割后是要了一份丢了一份 (可能是防止过拟合),而 slice 切割后往往是两份都要,这个坑大家需要注意一下。


3、route 化作恒等映射

  这个其实是我猜的,先来说下什么是恒等映射,恒等映射是 resnet 残差结构中的一个概念,如下图,恒等映射的意思就是把输入 x 原模原样的通过一个畅通无阻的高速路给传递过去。为什么要做这个操作呢,有了恒等映射,网络再深我也能通过这个小道把信息给你传递下去,且 x 的导数为 1,也可防止回传时梯度消失,关键还不增加额外计算量。

  回到我们的 route,来看一下下面这张图,可以看到框起来这两个 route 其实啥也没做,就是原封不动的将 20 x 20 x 512 维度的数据传递下去,这是不是和 恒等映射 异曲同工。


4、darknet 源码解读

  先看下 route_layer 的头:

#ifndef ROUTE_LAYER_H#define ROUTE_LAYER_H#include "network.h"#include "layer.h"typedef layer route_layer;#ifdef __cplusplusextern "C" {#endifroute_layer make_route_layer(int batch, int n, int *input_layers, int *input_size, int groups, int group_id);void forward_route_layer(const route_layer l, network_state state);void backward_route_layer(const route_layer l, network_state state);void resize_route_layer(route_layer *l, network *net);#ifdef GPUvoid forward_route_layer_gpu(const route_layer l, network_state state);void backward_route_layer_gpu(const route_layer l, network_state state);#endif#ifdef __cplusplus}#endif#endif

  其中有几个比较关键的函数声明,forward_route_layer 和 backward_route_layer 是 cpu 上的前向和后向,forward_route_layer_gpu 和 backward_route_layer_gpu 是 gpu 上的前向和后向。make_route_layer 是用于构建 route 层的,里面会调 cpu 和 gpu 的前向和后面,当然 gpu 的话需要你开了 gpu 才会去调,resize_route_layer 是经过 route 层作用后数据维度方面的变换。

  来看一下 darknet 构建 route 的操作,darkent 里 src/ 有一个 darknet.c,里面的 main 是整个框架的入口,里面也提供了功能丰富的传参示例能够帮你快速应用 darkent 这个好用的框架。顺着 darknet.c 里的逻辑你会找到构建 route 层的代码:

else if(lt == ROUTE){            l = parse_route(options, params);            int k;            for (k = 0; k < l.n; ++k) {                net.layers[l.input_layers[k]].use_bin_output = 0;                net.layers[l.input_layers[k]].keep_delta_gpu = 1;            }

  我们来看看 parse_route 做了什么:

route_layer parse_route(list *options, size_params params){    char *l = option_find(options, "layers");    if(!l) error("Route Layer must specify input layers");    int len = strlen(l);    int n = 1;    int i;    for(i = 0; i < len; ++i){        if (l[i] == ",") ++n;    }    int* layers = (int*)xcalloc(n, sizeof(int));    int* sizes = (int*)xcalloc(n, sizeof(int));    for(i = 0; i < n; ++i){        int index = atoi(l);        l = strchr(l, ",")+1;        if(index < 0) index = params.index + index;        layers[i] = index;        sizes[i] = params.net.layers[index].outputs;    }    int batch = params.batch;    int groups = option_find_int_quiet(options, "groups", 1);    int group_id = option_find_int_quiet(options, "group_id", 0);    route_layer layer = make_route_layer(batch, n, layers, sizes, groups, group_id);    convolutional_layer first = params.net.layers[layers[0]];    layer.out_w = first.out_w;    layer.out_h = first.out_h;    layer.out_c = first.out_c;    for(i = 1; i < n; ++i){        int index = layers[i];        convolutional_layer next = params.net.layers[index];        if(next.out_w == first.out_w && next.out_h == first.out_h){            layer.out_c += next.out_c;        }else{            fprintf(stderr, " The width and height of the input layers are different. /n");            layer.out_h = layer.out_w = layer.out_c = 0;        }    }    layer.out_c = layer.out_c / layer.groups;    layer.w = first.w;    layer.h = first.h;    layer.c = layer.out_c;    if (n > 3) fprintf(stderr, " /t    ");    else if (n > 1) fprintf(stderr, " /t            ");    else fprintf(stderr, " /t/t            ");    fprintf(stderr, "           ");    if (layer.groups > 1) fprintf(stderr, "%d/%d", layer.group_id, layer.groups);    else fprintf(stderr, "   ");    fprintf(stderr, " -> %4d x%4d x%4d /n", layer.out_w, layer.out_h, layer.out_c);    return layer;}

  其中关键的是:

route_layer layer = make_route_layer(batch, n, layers, sizes, groups, group_id);

  这就来到了上面 route_layer 头里的 make_route_layer 了:

route_layer make_route_layer(int batch, int n, int *input_layers, int *input_sizes, int groups, int group_id){    fprintf(stderr,"route ");    route_layer l = { (LAYER_TYPE)0 };    l.type = ROUTE;    l.batch = batch;    l.n = n;    l.input_layers = input_layers;    l.input_sizes = input_sizes;    l.groups = groups;    l.group_id = group_id;    int i;    int outputs = 0;    for(i = 0; i < n; ++i){        fprintf(stderr," %d", input_layers[i]);        outputs += input_sizes[i];    }    outputs = outputs / groups;    l.outputs = outputs;    l.inputs = outputs;    //fprintf(stderr, " inputs = %d /t outputs = %d, groups = %d, group_id = %d /n", l.inputs, l.outputs, l.groups, l.group_id);    l.delta = (float*)xcalloc(outputs * batch, sizeof(float));    l.output = (float*)xcalloc(outputs * batch, sizeof(float));    l.forward = forward_route_layer;    l.backward = backward_route_layer;    #ifdef GPU    l.forward_gpu = forward_route_layer_gpu;    l.backward_gpu = backward_route_layer_gpu;    l.delta_gpu =  cuda_make_array(l.delta, outputs*batch);    l.output_gpu = cuda_make_array(l.output, outputs*batch);    #endif    return l;}

  来看一下这个函数,传参中 n 是输入层的个数,groups 和 group_id 就是前面提到的 route 作 slice 的参数,由下面定义可以知道 groups 默认会是 1,group_id 默认会是 0,也就是当 route 没有 groups 参数时,上面函数中的 outputs = outputs / groups 和 group_id 偏移就相当于啥都没做。

int groups = option_find_int_quiet(options, "groups", 1);int group_id = option_find_int_quiet(options, "group_id", 0);

  来看一下 forward_route_layer:

void forward_route_layer(const route_layer l, network_state state){    int i, j;    int offset = 0;    for(i = 0; i < l.n; ++i){        int index = l.input_layers[i];        float *input = state.net.layers[index].output;        int input_size = l.input_sizes[i];        int part_input_size = input_size / l.groups;        for(j = 0; j < l.batch; ++j){            //copy_cpu(input_size, input + j*input_size, 1, l.output + offset + j*l.outputs, 1);            copy_cpu(part_input_size, input + j*input_size + part_input_size*l.group_id, 1, l.output + offset + j*l.outputs, 1);        }        //offset += input_size;        offset += part_input_size;    }}

  最关键的实现很简单就是在 copy_cpu 的一个赋值语句,其实想想也是,route 并没有做什么复杂的操作,拼接 或者 切割 或者 链路 的操作用一些指针偏移和赋值就可以实现了。

void copy_cpu(int N, float *X, int INCX, float *Y, int INCY){    int i;    for(i = 0; i < N; ++i) Y[i*INCY] = X[i*INCX];}

  同样,高性能计算离不开 cuda,来看一下 forward_route_layer_gpu 的操作:

void forward_route_layer_gpu(const route_layer l, network_state state){    if (l.stream >= 0) {        switch_stream(l.stream);    }    if (l.wait_stream_id >= 0) {        wait_stream(l.wait_stream_id);    }    int i, j;    int offset = 0;    for(i = 0; i < l.n; ++i){        int index = l.input_layers[i];        float *input = state.net.layers[index].output_gpu;        int input_size = l.input_sizes[i];        int part_input_size = input_size / l.groups;        for(j = 0; j < l.batch; ++j){            //copy_ongpu(input_size, input + j*input_size, 1, l.output_gpu + offset + j*l.outputs, 1);            //simple_copy_ongpu(input_size, input + j*input_size, l.output_gpu + offset + j*l.outputs);            simple_copy_ongpu(part_input_size, input + j*input_size + part_input_size*l.group_id, l.output_gpu + offset + j*l.outputs);        }        //offset += input_size;        offset += part_input_size;    }}

  route_gpu 的操作逻辑和 route_cpu 的一样,区别在于 gpu 的数据拷贝给并行起来了,来看一下 simple_copy_ongpu:

extern "C" void simple_copy_ongpu(int size, float *src, float *dst){    const int num_blocks = size / BLOCK + 1;    simple_copy_kernel << > >(size, src, dst);    CHECK_CUDA(cudaPeekAtLastError());}

  simple_copy_ongpu 里调了 simple_copy_kernel 这个 cuda kernel,我们来看一下:

__global__ void simple_copy_kernel(int size, float *src, float *dst){    int index = blockIdx.x*blockDim.x + threadIdx.x;    if (index < size)        dst[index] = src[index];}

  很简单,就是一个 device_to_device 的数据赋值过程。


  说到这里应该是比较清晰的了,不得不感慨一句:C 语言看起来真清爽啊~


  好了,有问题欢迎讨论,收工~


 【公众号传送】

《【模型推理】谈谈 darknet yolo 的 route 算子》



扫描下方二维码即可关注我的微信公众号【极智视界】,获取更多AI经验分享,让我们用极致+极客的心态来迎接AI !

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/120947.html

相关文章

  • 从零开始PyTorch项目:YOLO v3目标检测实现

    摘要:全卷积神经网络仅使用卷积层,这就使其成为全卷积神经网络。输入图像中包含了真值对象框中心的网格会作为负责预测对象的单元格。在图像中,它是被标记为红色的单元格,其中包含了真值框的中心被标记为黄色。在过去几个月中,我一直在实验室中研究提升目标检测的方法。在这之中我获得的较大启发就是意识到:学习目标检测的较佳方法就是自己动手实现这些算法,而这正是本教程引导你去做的。 在本教程中,我们将使用 P...

    sourcenode 评论0 收藏0
  • YOLO系列阅读笔记V1 V2 V3 V4

    摘要:检测任务需要精细粒度的视觉信息,因此在检测的时候把网络的输入分辨率从变成。的缺点的物体检测精度低于系列的算法。的损失函数中,大物体误差和小物体误差对网络训练中贡献值接近虽然采用求平方根方式,但没有根本解决问题。 ...

    Yuanf 评论0 收藏0
  • 人群距离监测 DeepSOCIAL 最全汉化论文+源码导读

    摘要:英国利兹大学的研究团队开源了人群距离监测项目,通过的方式快速实现了这一应用。是通过实现行人检测,再用实现目标跟踪,并通过单目视觉逆透视映射实现距离估计,最终评估人群距离监测和风险评估。 By 超神经 内容一览:在疫情期间,公共场所中尽量避免人群聚集,可以有效控制疫情扩散。英国利兹大学的研...

    mingde 评论0 收藏0
  • 【译】让你电脑学会同时识别多个物体

    摘要:工作原理以前的检测系统通过重复利用分类器和定位器来实现目标识别。修改检测阈值缺省情况下,只显示信心大于的对象。用法如下这个,呵呵,不完美把白马识别成绵羊了,把黑狗识别成奶牛了,但确实很快。 原标题:YOLO: Real-Time Object Detection英文原文:https://pjreddie.com/darknet/... 强烈推荐(TED视频):https://www....

    duan199226 评论0 收藏0
  • 【译】让你电脑学会同时识别多个物体

    摘要:工作原理以前的检测系统通过重复利用分类器和定位器来实现目标识别。修改检测阈值缺省情况下,只显示信心大于的对象。用法如下这个,呵呵,不完美把白马识别成绵羊了,把黑狗识别成奶牛了,但确实很快。 原标题:YOLO: Real-Time Object Detection英文原文:https://pjreddie.com/darknet/... 强烈推荐(TED视频):https://www....

    leap_frog 评论0 收藏0

发表评论

0条评论

wangbjun

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<