ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【YOLOv4探讨 之七】利用Darknet在YOLOv4中添加注意力机制模块 系列之SE模块

2021-07-03 18:02:08  阅读:1503  来源: 互联网

标签:之七 index layer YOLOv4 batch scale 模块 size out


利用Darknet在YOLOv4中添加注意力机制模块

在论文《YOLOv4: Optimal Speed and Accuracy of Object Detectio》中,有一个重要的trick,就是注意力机制模块。而且在Darknet框架中还增加了相关的层的设计,主要包括sam_layer层和scale_channels_layer层,分别用于处理空间注意力机制和通道注意力机制。
搜索了大量的文章,基本没找到如何在Darknet在YOLOv4中添加注意力机制模块的方法,这里进行了探索。按照基本原理实现了模块的添加,但是实现效果还需要进一步调试,这里抛砖引玉,有好的理解的小伙伴可以和我交流。

添加注意力机制模块分成添加SE模块、添加SAM模块和添加CBAM模块三篇,组成一个小系列。本篇为总体介绍和添加SE模块。

基本概念

为了便于大家理解,这里对注意力机制的基本概念进行梳理。

注意力机制(Attention Mechanism)是机器学习中的一种数据处理方法,源于NLP的学习任务,最初用于处理输入不规则的语音信息或者文本信息。由于RNN网络具有很强的遗忘性,诞生了LSTM,但是LSTM运算量过大,后来将状态加权原理进一步延伸,诞生了注意力机制模型,用于对前期输入的信息加权,突出重要内容,提升训练的准确性。基于注意力机制,还诞生了Transformer模型,计算机视觉中也引入Transformer模型,形成ViT模型,最近也比较热门。浙江大学还提出了YOLOS模型,和本文讨论的YOLO是完全不同的架构,这里说到此不再延伸。

YOLOv4中引入注意力机制,就是希望网络能够自动学出来图片需要注意的地方。比如人眼在看一幅画的时候,不会将注意力平等地分配给画中的所有像素,而是将更多注意力分配给人们关注的地方。从实现的角度来讲,注意力机制就是通过神经网络的操作生成一个掩码mask,mask上的值一个打分,重点评价当前需要关注的点。

注意力机制可以分为:

  • 通道注意力机制:对通道生成掩码mask,进行打分,代表是senet, Channel Attention Module。
  • 空间注意力机制:对空间进行掩码的生成,进行打分,代表是Spatial Attention Module 。
  • 混合域注意力机制:同时对通道注意力和空间注意力进行评价打分,代表的有BAM, CBAM。

通道注意力机制使用SE模块,在Darknet中,新添加的scale_channels_layer 层就是用于SE模块,该层在darknet.h中的定义为scale_channels.

SE模块思想简单,易于实现,并且很容易可以加载到现有的网络模型框架中。SENet主要是学习了channel之间的相关性,筛选出了针对通道的注意力,稍微增加了一点计算量,但是效果比较好。原理图如下:
在这里插入图片描述
通过上图可以理解他的实现过程,通过对卷积的到的feature map进行处理,得到一个和通道数一样的一维向量作为每个通道的评价分数,然后将改分数分别施加到对应的通道上。

在这里插入图片描述

配置实现

在Backbone后面进行添加。上面的原理图中,SE模块是对残差模型的改造,在Darknet中只需要通过配置文件的修改即可完成。本人开始在实验中,一开始没有使用残差模块,导致训练好的模型根本无法识别图像中的目标。
添加SE模块需要在配置文件中增加如下内容:

####Backbone#######

####Res###

[convolutional]
batch_normalize=1
filters=1024
size=1
stride=1
pad=1
activation=leaky

[convolutional]
batch_normalize=1
filters=1024
size=3
stride=1
pad=1
activation=leaky

####se###
[avgpool]

[convolutional]
batch_normalize=1
filters=64
size=1
stride=1
pad=1
activation=relu

[convolutional]
batch_normalize=1
filters=1024
size=1
stride=1
pad=1
activation=logistic

[scale_channels]
from = -4
activation= linear
###########

[shortcut]
from=-7
scale_wh = 1
activation=linear

###########

上面配置描述中的Global pooling选择Global average pooling,在Darknet中,avgpooling层是对每个channel进行平均,有多少个channel,就计算出含多少个元素的平均值。
FC为全连接层,在Darknet中,全连接层无法修改channels的数量,由于是一维向量,卷积层和全连接层是一样的,可以修改channels的数量(通过filters参数),还可以增加激活函数,所以FC+ReLU和FC+Sigmoid可以使用两个卷基模块代替。
Scale的实现在Darknet中通过scale_channels_layer实现,这里使用了linear激活函数,表示对scale处理后的数据不再进行激活处理。

源码分析

这里主要使用scale_channels_layer和avgpool_layer,这里对涉及到的相关源码进行简要注释:

  • avgpool_layer:
//parse_avgpool可以看出在Darknet框架中cfg文件中avgpool配置需要哪些参数,可见没有size和stride,这说明在配置文件中[avgpool]不需要设置参数,这个平均池化层就是个全局平均,这个通过forward_avgpool_layer也可以看出。
avgpool_layer parse_avgpool(list *options, size_params params)
{
    int batch,w,h,c;
    w = params.w; //参数:feature map的宽度
    h = params.h; //参数:feature map的高度
    c = params.c; //参数:feature map的通道数
    batch=params.batch;  //参数:batch的数量
    if(!(h && w && c)) error("Layer before avgpool layer must output image.");

    avgpool_layer layer = make_avgpool_layer(batch,w,h,c);
    return layer;
}
//平均池化层的前向传播函数
void forward_avgpool_layer(const avgpool_layer l, network_state state)
{
    int b,i,k;

    for(b = 0; b < l.batch; ++b){
        for(k = 0; k < l.c; ++k){
            int out_index = k + b*l.c;
            l.output[out_index] = 0;
            for(i = 0; i < l.h*l.w; ++i){
                int in_index = i + l.h*l.w*(k + b*l.c);
                l.output[out_index] += state.input[in_index];
            }
            //每个层输出一个1*1的feature map,为每个feature map的全局平均
            l.output[out_index] /= l.h*l.w;
        }
    }
}

//平均池化层的反向传播函数
void backward_avgpool_layer(const avgpool_layer l, network_state state)
{
    int b,i,k;

    for(b = 0; b < l.batch; ++b){
        for(k = 0; k < l.c; ++k){
            int out_index = k + b*l.c;
            for(i = 0; i < l.h*l.w; ++i){
                int in_index = i + l.h*l.w*(k + b*l.c);
                //对后一层传过来的微分进行全局平均,分配给feature map每个元素
                state.delta[in_index] += l.delta[out_index] / (l.h*l.w);
            }
        }
    }
}
  • scale_channels_layer
//通过解析函数可以看出,用于cfg文件的包括:from,scale_wh,activation三个参数
//from表示与之相乘的feature map在cfg文件描述的层数,本文选用-4,表示倒数4层
//scale_wh表示相乘时,每个batch不同的图像对应的feature map是否使用相同的参数
layer parse_scale_channels(list *options, size_params params, network net)
{
    char *l = option_find(options, "from");
    int index = atoi(l);
    if (index < 0) index = params.index + index;
    int scale_wh = option_find_int_quiet(options, "scale_wh", 0);

    int batch = params.batch;
    layer from = net.layers[index];

    layer s = make_scale_channels_layer(batch, index, params.w, params.h, params.c, from.out_w, from.out_h, from.out_c, scale_wh);

    char *activation_s = option_find_str_quiet(options, "activation", "linear");
    ACTIVATION activation = get_activation(activation_s);
    s.activation = activation;
    if (activation == SWISH || activation == MISH) {
        printf(" [scale_channels] layer doesn't support SWISH or MISH activations \n");
    }
    return s;
}
//scale_channels_layer的前向传播函数
void forward_scale_channels_layer(const layer l, network_state state)
{
    int size = l.batch * l.out_c * l.out_w * l.out_h;
    int channel_size = l.out_w * l.out_h;
    int batch_size = l.out_c * l.out_w * l.out_h;
    float *from_output = state.net.layers[l.index].output;

    //设置scale_wh=1时,考虑一个batch中不同的图像分别进行scale系数相乘
    if (l.scale_wh) {
        int i;
        #pragma omp parallel for
        for (i = 0; i < size; ++i) {
            int input_index = i % channel_size + (i / batch_size)*channel_size;

            l.output[i] = state.input[input_index] * from_output[i];
        }
    }
    //设置scale_wh=0或不设置时,考虑一个batch中不同的图像都使用相同的scale系数相乘
    else {
        int i;
        #pragma omp parallel for
        for (i = 0; i < size; ++i) {
            l.output[i] = state.input[i / channel_size] * from_output[i];
        }
    }

    activate_array(l.output, l.outputs*l.batch, l.activation);
}
//scale_channels_layer的反向传播函数,区分也是结合scale_wh设置,对后一层的微分对输出的feature map进行系数相乘,等于微分直接传播,如果系数scale为1,则等于微分直接原封不动传递给前一层
void backward_scale_channels_layer(const layer l, network_state state)
{
    gradient_array(l.output, l.outputs*l.batch, l.activation, l.delta);
    //axpy_cpu(l.outputs*l.batch, 1, l.delta, 1, state.delta, 1);
    //scale_cpu(l.batch, l.out_w, l.out_h, l.out_c, l.delta, l.w, l.h, l.c, state.net.layers[l.index].delta);

    int size = l.batch * l.out_c * l.out_w * l.out_h;
    int channel_size = l.out_w * l.out_h;
    int batch_size = l.out_c * l.out_w * l.out_h;
    float *from_output = state.net.layers[l.index].output;
    float *from_delta = state.net.layers[l.index].delta;

    if (l.scale_wh) {
        int i;
        #pragma omp parallel for
        for (i = 0; i < size; ++i) {
            int input_index = i % channel_size + (i / batch_size)*channel_size;

            state.delta[input_index] += l.delta[i] * from_output[i];// / l.out_c; // l.delta * from  (should be divided by l.out_c?)

            from_delta[i] += state.input[input_index] * l.delta[i]; // input * l.delta
        }
    }
    else {
        int i;
        #pragma omp parallel for
        for (i = 0; i < size; ++i) {
            state.delta[i / channel_size] += l.delta[i] * from_output[i];// / channel_size; // l.delta * from  (should be divided by channel_size?)

            from_delta[i] += state.input[i / channel_size] * l.delta[i]; // input * l.delta
        }
    }
}

小结

在实验过程中,[scale_channels]一开始没有设置scale_wh = 1效果并不好,后来增加了scale_channels =1,才能正常识别。另外,SE模块放置的位置如何调整才能达到最佳效果,也需要大量实验验证。

标签:之七,index,layer,YOLOv4,batch,scale,模块,size,out
来源: https://blog.csdn.net/qq_41736617/article/details/118424585

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有