精华内容
下载资源
问答
  • yolov4

    2021-02-23 18:11:12
    yolov4
  • 因为工作原因,项目中经常遇到目标检测的任务,因此对目标检测算法会经常使用和关注,比如Yolov3、Yolov4算法。 当然,实际项目中很多的第一步,也都是先进行目标检测任务,比如人脸识别、多目标追踪、REID、客流...

    因为工作原因,项目中经常遇到目标检测的任务,因此对目标检测算法会经常使用和关注,比如Yolov3Yolov4算法Yolov5算法Yolox算法

    当然,实际项目中很多的第一步,也都是先进行目标检测任务,比如人脸识别多目标追踪REID客流统计等项目。因此目标检测是计算机视觉项目中非常重要的一部分。

    从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗,然后不久又出现了Yolov5

    而到了2021年,就在大家质疑Yolo系列该如何改进时?旷视科技又发布了Yolox算法

    因为项目中Yolov3、Yolov4、Yolov5、Yolox使用很多,所以大白将项目中,需要了解的Yolov3、Yolov4、Yolov5、Yolox系列相关知识点以及相关代码进行完整的汇总,希望和大家共同学习探讨。

    同时,大白每周会整理几十个人工智能公众号的精华文章,并系统的分类,让大家对于人工智能行业每周的内容动态可以一目了然,点击查看

    版权申明:本文包含图片,都为大白使用PPT所绘制的,如需网络结构高清图模型权重,可点击查看下载

    更新提醒(2021.08.11):Yolov3&Yolov4的相关视频,已经更新上传,可点击查看

    此外大白耗尽洪荒之力,准备了包括Yolo目标检测在内的31节视频课程可以点击查看。

    求职跳槽福利:为了便于大家求职、跳槽的准备,大白将45家大厂3500篇面经,按照知识框架,整理成700多页的《人工智能算法岗江湖武林秘籍》,限时开放下载,点击查看下载


    1 论文汇总

    Yolov3论文名:《Yolov3: An Incremental Improvement
    Yolov3论文地址:https://arxiv.org/pdf/1804.02767.pdf

    Yolov4论文名:《Yolov4: Optimal Speed and Accuracy of Object Detection
    Yolov4论文地址:https://arxiv.org/pdf/2004.10934.pdf


    2 Yolov3核心基础内容

    2.1 网络结构可视化

    Yolov3是目标检测Yolo系列非常非常经典的算法。
    不过很多同学拿到Yolov3或者Yolov4cfg文件时,并不知道如何直观的可视化查看网络结构。如果纯粹看cfg里面的内容,肯定会一脸懵逼
    其实可以很方便的用netron查看Yolov3的网络结构图,一目了然。
    这里不多说,如果需要安装,可以移步大白的另一篇文章:《网络可视化工具netron详细安装流程》
    如果不想安装,也可以直接点击此链接,查看Yolov3可视化流程图

    2.2 网络结构图

    在这里插入图片描述
    绘制网络结构图受到Yolov3另一位作者文章的启发,包括下面Yolov4的结构图,从总体框架上先了解了Yolov3的流程,再针对去学习每一小块的知识点,会事半功倍。
    上图三个蓝色方框内表示Yolov3三个基本组件:
    (1)CBL:Yolov3网络结构中的最小组件,由Conv+Bn+Leaky_relu激活函数三者组成。
    (2)Res unit:借鉴Resnet网络中的残差结构,让网络可以构建的更深。
    (3)ResX:由一个CBL和X个残差组件构成,是Yolov3中的大组件。每个Res模块前面的CBL都起到下采样的作用,因此经过5次Res模块后,得到的特征图是608->304->152->76->38->19大小。

    其他基础操作:
    (1)Concat:张量拼接,会扩充两个张量的维度,例如26×26×25626×26×512两个张量拼接,结果是26×26×768。Concat和cfg文件中的route功能一样。
    (2)Add:张量相加,张量直接相加,不会扩充维度,例如104×104×128104×104×128相加,结果还是104×104×128。add和cfg文件中的shortcut功能一样。

    Backbone中卷积层的数量:
    每个ResX中包含1+2×X个卷积层,因此整个主干网络Backbone中一共包含1+(1+2×1)+(1+2×2)+(1+2×8)+(1+2×8)+(1+2×4)=52,再加上一个FC全连接层,即可以组成一个Darknet53分类网络。不过在目标检测Yolov3中,去掉FC层,不过为了方便称呼,仍然把Yolov3的主干网络叫做Darknet53结构

    2.3 核心基础内容

    Yolov32018年发明提出的,这成为了目标检测one-stage中非常经典的算法,包含Darknet-53网络结构anchor锚框FPN等非常优秀的结构。
    本文主要目的在于描述Yolov4Yolov3算法的不同及创新之处,对Yolov3的基础不过多描述。

    这里大白也准备了Yolov3算法非常浅显易懂的基础视频课程,让小白也能简单清楚的了解Yolov3的整个过程及各个算法细节。

    Yolov3及Yolov4深入浅出系列视频点击查看。

    在准备课程过程中,大白搜集查看了网络上几乎所有的Yolov3资料,在此整理几个非常不错的文章及视频,大家也可以点击查看,学习相关知识。
    (1)视频:吴恩达目标检测Yolo入门讲解
    https://www.bilibili.com/video/BV1N4411J7Y6?from=search&seid=18074481568368507115
    (2)文章:Yolo系列之Yolov3【深度解析】
    https://blog.csdn.net/leviopku/article/details/82660381
    (3)文章:一文看懂Yolov3
    https://blog.csdn.net/litt1e/article/details/88907542
    相信大家看完,对于Yolov3的基础知识点会有一定的了解。


    3 Yolov3相关代码

    3.1 python代码

    代码地址:https://github.com/ultralytics/Yolov3

    3.2 C++代码

    这里推荐Yolov4作者darknetAB代码,代码和原始作者代码相比,进行了很多的优化,如需要运行Yolov3网络,加载cfg时,使用Yolov3.cfg即可
    代码地址:https://github.com/AlexeyAB/darknet

    3.3 python版本的Tensorrt代码

    除了算法研究外,实际项目中还需要将算法落地部署到工程上使用,比如GPU服务器使用时还需要对模型进行tensorrt加速
    (1)Tensort中的加速案例
    强烈推荐tensort软件中,自带的Yolov3加速案例,路径位于tensorrt解压文件夹的TensortX/samples/python/Yolov3_onnx
    针对案例中的代码,如果有不明白的,也可参照下方文章上的详细说明:
    代码讲解文章:https://www.cnblogs.com/shouhuxianjian/p/10550262.html
    (2)Github上的tensorrt加速
    除了Tensorrt软件中的代码, Github上也有其他作者的开源代码
    代码地址:https://github.com/lewes6369/TensorRT-Yolov3

    3.4 C++版本的Tensorrt代码

    项目的工程部署上,如果使用C++版本进行Tensorrt加速,一方面可以参照Alexey的Github代码,另一方面也可以参照下面其他作者的开源代码
    代码地址:https://github.com/wang-xinyu/tensorrtx/tree/master/Yolov3


    4 YoloV4核心基础内容

    4.1 网络结构可视化

    Yolov4的网络结构也可以使用netron工具查看,大白也是对照其展示的可视化流程图绘制的下方网络结构图。
    netron可视化显示Yolov4网络结构可以参照大白的另一篇文章:《netron可视化网络结构详细安装流程
    如果不想安装,也可以直接点击此链接,查看Yolov4可视化流程图。

    4.2 网络结构图

    在这里插入图片描述
    Yolov4的结构图和Yolov3相比,因为多了CSP结构PAN结构,如果单纯看可视化流程图,会觉得很绕,但是在绘制出上面的图形后,会觉得豁然开朗,其实整体架构和Yolov3是相同的,不过使用各种新的算法思想对各个子结构都进行了改进。
    先整理下Yolov4的五个基本组件:
    1. CBM:Yolov4网络结构中的最小组件,由Conv+Bn+Mish激活函数三者组成。
    2. CBL:Conv+Bn+Leaky_relu激活函数三者组成。
    3. Res unit:借鉴Resnet网络中的残差结构,让网络可以构建的更深。
    4. CSPX:借鉴CSPNet网络结构,由卷积层和X个Res unint模块Concat组成。
    5. SPP:采用1×1,5×5,9×9,13×13的最大池化的方式,进行多尺度融合。

    其他基础操作:
    1. Concat:张量拼接,维度会扩充,和Yolov3中的解释一样,对应于cfg文件中的route操作。
    2. Add:张量相加,不会扩充维度,对应于cfg文件中的shortcut操作。

    Backbone中卷积层的数量:
    和Yolov3一样,再来数一下Backbone里面的卷积层数量。
    每个CSPX中包含5+2×X个卷积层,因此整个主干网络Backbone中一共包含1+(5+2×1)+(5+2×2)+(5+2×8)+(5+2×8)+(5+2×4)=72。
    这里大白有些疑惑,按照Yolov3设计的传统,72个卷积层,加上最后的一个全连接层,主干网络的名字不应该叫CSPDarknet73吗????

    4.3 核心基础内容

    Yolov4本质上和Yolov3相差不大,可能有些人会觉得失望。
    但我觉得算法创新分为三种方式
    (1)第一种:面目一新的创新,比如Yolov1Faster-RCNNCenternet等,开创出新的算法领域,不过这种也是最难的。
    (2)第二种:守正出奇的创新,比如将图像金字塔改进为特征金字塔。
    (3)第三种:各种先进算法集成的创新,比如不同领域发表的最新论文的tricks,集成到自己的算法中,却发现有出乎意料的改进。
    Yolov4既有第二种也有第三种创新,组合尝试了大量深度学习领域最新论文的20多项研究成果,而且不得不佩服的是作者AlexeyGithub代码库维护的频繁程度。
    目前Yolov4代码的Star数量已经1万左右,据我所了解,目前超过这个数量的,目标检测领域只有FacebookDetectron(v1-v2)、和Yolo(v1-v3)官方代码库(已停止更新)。
    所以
    Yolov4
    中的各种创新方式,大白觉得还是很值得仔细研究的。

    为了便于分析,将Yolov4的整体结构拆分成四大板块:
    在这里插入图片描述
    大白主要从以上4个部分YoloV4的创新之处进行讲解,让大家一目了然。

    (1)输入端:这里指的创新主要是训练时对输入端的改进,主要包括Mosaic数据增强、cmBN、SAT自对抗训练。
    (2)BackBone主干网络:将各种新的方式结合起来,包括:CSPDarknet53Mish激活函数Dropblock
    (3)Neck:目标检测网络在BackBone和最后的输出层之间往往会插入一些层,比如Yolov4中的SPP模块FPN+PAN结构
    (4)Prediction:输出层的锚框机制和Yolov3相同,主要改进的是训练时的损失函数CIOU_Loss,以及预测框筛选的nms变为DIOU_nms
    总体来说,Yolov4Yolov3的各个部分都进行了改进优化,下面丢上作者的算法对比图。
    在这里插入图片描述
    仅对比Yolov3Yolov4,在COCO数据集上,同样的FPS等于83左右时,Yolov4的AP是43,而Yolov3是33,直接上涨了10个百分点
    不得不服,当然可能针对具体不同的数据集效果也不一样,但总体来说,改进效果是很优秀的,下面大白对Yolov4的各个创新点继续进行深挖。

    4.3.1 输入端创新

    考虑到很多同学GPU显卡数量并不是很多,Yolov4对训练时的输入端进行改进,使得训练在单张GPU上也能有不错的成绩。比如数据增强MosaiccmBNSAT自对抗训练。
    但感觉cmBN和SAT影响并不是很大,所以这里主要讲解Mosaic数据增强

    (1)Mosaic数据增强
    Yolov4中使用的Mosaic是参考2019年底提出的CutMix数据增强的方式,但CutMix只使用了两张图片进行拼接,而Mosaic数据增强则采用了4张图片,随机缩放随机裁剪随机排布的方式进行拼接。
    在这里插入图片描述
    这里首先要了解为什么要进行Mosaic数据增强呢?
    在平时项目训练时,小目标的AP一般比中目标和大目标低很多。而Coco数据集中也包含大量的小目标,但比较麻烦的是小目标的分布并不均匀
    首先看下小、中、大目标的定义:
    2019年发布的论文《Augmentation for small object detection》对此进行了区分

    在这里插入图片描述
    可以看到小目标的定义是目标框的长宽0×0~32×32之间的物体。

    在这里插入图片描述
    但在整体的数据集中,小、中、大目标的占比并不均衡。
    如上表所示,Coco数据集中小目标占比达到41.4%,数量比中目标和大目标都要多。
    但在所有的训练集图片中,只有52.3%的图片有小目标,而中目标和大目标的分布相对来说更加均匀一些。
    针对这种状况,Yolov4的作者采用了
    Mosaic数据增强
    的方式。
    主要有几个优点:
    a. 丰富数据集:随机使用4张图片,随机缩放,再随机分布进行拼接,大大丰富了检测数据集,特别是随机缩放增加了很多小目标,让网络的鲁棒性更好。
    b. 减少GPU:可能会有人说,随机缩放,普通的数据增强也可以做,但作者考虑到很多人可能只有一个GPU。
    因此Mosaic增强训练时,可以直接计算4张图片的数据,使得Mini-batch大小并不需要很大,一个GPU就可以达到比较好的效果。
    此外,发现另一研究者的训练方式也值得借鉴,采用的数据增强和Mosaic比较类似,也是使用4张图片(不是随机分布),但训练计算loss时,采用“缺啥补啥”的思路:
    如果上一个iteration中,小物体产生的loss不足(比如小于某一个阈值),则下一个iteration就用拼接图;否则就用正常图片训练,也很有意思。
    参考链接:https://www.zhihu.com/question/390191723?rf=390194081

    4.3.2 BackBone创新

    (1)CSPDarknet53
    CSPDarknet53是在Yolov3主干网络Darknet53的基础上,借鉴2019年CSPNet的经验,产生的Backbone结构,其中包含了5个CSP模块

    在这里插入图片描述
    这里因为CSP模块比较长,不放到本处,大家也可以点击Yolov4的netron网络结构图,对比查看,一目了然。
    每个CSP模块前面的卷积核的大小都是3×3步长为2,因此可以起到下采样的作用。
    因为Backbone有5个CSP模块,输入图像是608*608,所以特征图变化的规律是:608->304->152->76->38->19
    经过5次CSP模块后得到19*19大小的特征图。
    而且作者只在Backbone中采用了Mish激活函数,网络后面仍然采用Leaky_relu激活函数
    我们再看看下作者为啥要参考2019年的CSPNet,采用CSP模块?
    CSPNet论文地址:https://arxiv.org/pdf/1911.11929.pdf
    CSPNet全称是Cross Stage Paritial Network,主要从网络结构设计的角度解决推理中从计算量很大的问题。
    CSPNet的作者认为推理计算过高的问题是由于网络优化中的梯度信息重复导致的。
    因此采用CSP模块先将基础层的特征映射划分为两部分,然后通过跨阶段层次结构将它们合并,在减少了计算量的同时可以保证准确率。
    因此Yolov4在主干网络Backbone采用CSPDarknet53网络结构,主要有三个方面的优点:
    优点一:增强CNN的学习能力,使得在轻量化的同时保持准确性。
    优点二:降低计算瓶颈
    优点三:降低内存成本

    (2)Mish激活函数
    Mish激活函数是2019年下半年提出的激活函数
    论文地址:https://arxiv.org/abs/1908.08681
    leaky_relu激活函数的图形对比如下:
    在这里插入图片描述
    Yolov4的Backbone中都使用了Mish激活函数,而后面的网络则还是使用Leaky_relu函数。
    在这里插入图片描述
    Yolov4作者实验测试时,使用CSPDarknet53网络ImageNet数据集上做图像分类任务,发现使用了Mish激活函数TOP-1TOP-5的精度比没有使用时都略高一些。
    因此在设计Yolov4目标检测任务时,主干网络Backbone还是使用Mish激活函数

    (3)Dropblock
    Yolov4中使用的Dropblock,其实和常见网络中的Dropout功能类似,也是缓解过拟合的一种正则化方式。
    Dropblock2018年提出,论文地址:https://arxiv.org/pdf/1810.12890.pdf

    传统的Dropout很简单,一句话就可以说的清:随机删除减少神经元的数量,使网络变得更简单。
    在这里插入图片描述
    DropblockDropout相似,比如下图:

    在这里插入图片描述中间Dropout的方式会随机的删减丢弃一些信息,但Dropblock的研究者认为,卷积层对于这种随机丢弃并不敏感.
    因为卷积层通常是三层连用:卷积+激活+池化层,池化层本身就是对相邻单元起作用。
    而且即使随机丢弃,卷积层仍然可以从相邻的激活单元学习到相同的信息。因此,在全连接层上效果很好的Dropout在卷积层上效果并不好
    所以右图Dropblock的研究者则干脆整个局部区域进行删减丢弃。
    这种方式其实是借鉴2017年Cutout数据增强的方式,cutout是将输入图像的部分区域清零,而Dropblock则是将Cutout应用到每一个特征图。而且并不是用固定的归零比率,而是在训练时以一个小的比率开始,随着训练过程线性的增加这个比率
    在这里插入图片描述
    Dropblock的研究者与Cutout数据增强对训练效果进行对比验证时,发现有几个优点:
    优点一:Dropblock的效果优于Cutout
    优点二:cutout只能作用于输入层,而Dropblock则是将Cutout应用到网络中的每一个特征图上
    优点三:Dropblock可以定制各种组合,在训练的不同阶段可以修改删减的概率,从空间层面和时间层面,和cutout相比都有更精细的改进。
    Yolov4中直接采用了更优的Dropblock,对网络的正则化过程进行了全面的升级改进。

    4.3.3 Neck创新

    在目标检测领域,为了更好的提取融合特征,通常在Backbone输出层,会插入一些层,这个部分称为Neck。相当于目标检测网络的颈部,也是非常关键的。
    Yolov4Neck结构主要采用了SPP模块FPN+PAN的方式。

    (1)SPP模块
    SPP模块,其实在Yolov3中已经存在了,在Yolov4C++代码文件夹中有一个Yolov3_spp版本,但有的同学估计从来没有使用过,在Yolov4中,SPP模块仍然是在Backbone主干网络之后:

    在这里插入图片描述
    作者在SPP模块中,使用k={1×1,5×5,9×9,13×13}的最大池化的方式,再将不同尺度的特征图进行Concat操作
    注意:这里最大池化采用padding操作,移动的步长为1,比如13×13的输入特征图,使用5×5大小的池化核池化,padding=2,因此池化后的特征图仍然是13×13大小。

    在这里插入图片描述
    在2019年的《DC-SPP-Yolo》文章:https://arxiv.org/ftp/arxiv/papers/1903/1903.08589.pdf
    也对Yolo目标检测的SPP模块进行了对比测试。
    和Yolov4作者的研究相同,采用SPP模块的方式,比单纯的使用k×k最大池化的方式,更有效的增加主干特征的接收范围,显著的分离了最重要的上下文特征
    Yolov4的作者在使用608×608大小的图像进行测试时发现,在COCO目标检测任务中,以0.5%的额外计算代价将AP50增加了2.7%,因此Yolov4中也采用了SPP模块

    (2)FPN+PAN
    PAN结构比较有意思,看了网上Yolov4关于这个部分的讲解,大多都是讲的比较笼统的,而PAN是借鉴**2018年图像分割领域PANet的创新点,有些同学可能不是很清楚。
    下面大白将这个部分拆解开来,看下
    Yolov3Yolov4**中是如何设计的。

    Yolov3各个网络结构
    在这里插入图片描述
    可以看到经过几次下采样,三个紫色箭头指向的地方,输出分别是76×7638×3819×19
    以及最后的Prediction中用于预测的三个特征图①19×19×255②38×38×255③76×76×255 [注:255表示80类别 (1+4+80)×3=255]
    我们将Neck部分用立体图画出来,更直观的看下两部分之间是如何通过FPN结构融合的。

    在这里插入图片描述
    如图所示,FPN是自顶向下的,将高层的特征信息通过上采样的方式进行传递融合,得到进行预测的特征图。

    Yolov4各个网络结构
    而Yolov4中Neck这部分除了使用FPN外,还在此基础上使用了PAN结构

    在这里插入图片描述
    前面CSPDarknet53中讲到,每个CSP模块前面的卷积核都是3×3大小,相当于下采样操作。
    因此可以看到三个紫色箭头处的特征图是76×7638×3819×19
    以及最后Prediction中用于预测的三个特征图:①76×76×255②38×38×255③19×19×255
    我们也看下Neck部分的立体图像,看下两部分是如何通过FPN+PAN结构进行融合的。
    在这里插入图片描述
    和Yolov3的FPN层不同,Yolov4在FPN层的后面还添加了一个自底向上的特征金字塔
    其中包含两个PAN结构
    这样结合操作,FPN层自顶向下传达强语义特征,而特征金字塔则自底向上传达强定位特征,两两联手,从不同的主干层对不同的检测层进行特征聚合,这样的操作确实很皮。
    FPN+PAN借鉴的是18年CVPR的PANet,当时主要应用于图像分割领域,但Alexey将其拆分应用到Yolov4中,进一步提高特征提取的能力。

    不过这里需要注意几点:
    注意一:
    Yolov3的FPN层输出的三个大小不一的特征图①②③直接进行预测
    但Yolov4的FPN层,只使用最后的一个76×76特征图①,而经过两次PAN结构,输出预测的特征图②和③。
    这里的不同也体现在cfg文件中,这一点有很多同学之前不太明白。
    比如Yolov3.cfg中输入时608×608,最后的三个Yolo层中,
    第一个Yolo层是最小的特征图19×19,mask=6,7,8,对应最大的anchor box
    第二个Yolo层是中等的特征图38×38,mask=3,4,5,对应中等的anchor box
    第三个Yolo层是最大的特征图76×76,mask=0,1,2,对应最小的anchor box
    而Yolov4.cfg则恰恰相反
    第一个Yolo层是最大的特征图76×76mask=0,1,2,对应最小的anchor box
    第二个Yolo层是中等的特征图38×38mask=3,4,5,对应中等的anchor box
    第三个Yolo层是最小的特征图19×19mask=6,7,8,对应最大的anchor box
    注意二:
    原本的PANet网络PAN结构中,两个特征图结合是采用shortcut操作,而Yolov4中则采用concatroute)操作,特征图融合后的尺寸发生了变化。
    在这里插入图片描述
    这里也可以对应Yolov4的netron网络图查看,很有意思。

    4.3.4 Prediction创新

    (1)CIOU_loss
    目标检测任务的损失函数一般由Classificition Loss(分类损失函数)和Bounding Box Regeression Loss(回归损失函数)两部分构成。
    Bounding Box Regeression的Loss近些年的发展过程是:Smooth L1 Loss-> IoU Loss(2016)-> GIoU Loss(2019)-> DIoU Loss(2020)->CIoU Loss(2020)
    我们从最常用的IOU_Loss开始,进行对比拆解分析,看下Yolov4为啥要选择CIOU_Loss

    a. IOU_loss

    在这里插入图片描述
    可以看到IOU的loss其实很简单,主要是交集/并集,但其实也存在两个问题。
    在这里插入图片描述
    问题1:即状态1的情况,当预测框和目标框不相交时,IOU=0,无法反应两个框距离的远近,此时损失函数不可导,IOU_Loss无法优化两个框不相交的情况。
    问题2:即状态2和状态3的情况,当两个预测框大小相同,两个IOU也相同,IOU_Loss无法区分两者相交情况的不同。
    因此2019年出现了GIOU_Loss来进行改进。

    b. GIOU_loss
    在这里插入图片描述
    可以看到右图GIOU_Loss中,增加了相交尺度的衡量方式,缓解了单纯IOU_Loss时的尴尬。
    但为什么仅仅说缓解呢?因为还存在一种不足
    在这里插入图片描述
    问题:状态1、2、3都是预测框在目标框内部且预测框大小一致的情况,这时预测框和目标框的差集都是相同的,因此这三种状态的GIOU值也都是相同的,这时GIOU退化成了IOU,无法区分相对位置关系。
    基于这个问题,2020年的AAAI又提出了DIOU_Loss

    c. DIOU_loss

    好的目标框回归函数应该考虑三个重要几何因素:重叠面积中心点距离长宽比
    针对IOU和GIOU存在的问题,作者从两个方面进行考虑
    一:如何最小化预测框和目标框之间的归一化距离?
    二:如何在预测框和目标框重叠时,回归的更准确?
    针对第一个问题,提出了DIOU_LossDistance_IOU_Loss
    在这里插入图片描述
    DIOU_Loss考虑了重叠面积中心点距离,当目标框包裹预测框的时候,直接度量2个框的距离,因此DIOU_Loss收敛的更快。
    但就像前面好的目标框回归函数所说的,这时并没有考虑到长宽比
    在这里插入图片描述
    问题:比如上面三种状态,目标框包裹预测框,本来DIOU_Loss可以起作用。
    但预测框的中心点的位置都是一样的,因此按照DIOU_Loss的计算公式,三者的值都是相同的。
    针对这个问题,又提出了CIOU_Loss,不对不说,科学总是在解决问题中,不断进步!!

    d. CIOU_loss

    CIOU_LossDIOU_Loss前面的公式都是一样的,不过在此基础上还增加了一个影响因子,将预测框和目标框的长宽比都考虑了进去。
    在这里插入图片描述
    其中v是衡量长宽比一致性的参数,我们也可以定义为:
    在这里插入图片描述
    这样CIOU_Loss就将目标框回归函数应该考虑三个重要几何因素:重叠面积、中心点距离,长宽比全都考虑进去了。

    再来综合的看下各个Loss函数不同点
    IOU_Loss:主要考虑检测框和目标框重叠面积
    GIOU_Loss:在IOU的基础上,解决边界框不重合时的问题。
    DIOU_Loss:在IOU和GIOU的基础上,考虑边界框中心点距离的信息。
    CIOU_Loss:在DIOU的基础上,考虑边界框宽高比的尺度信息。
    Yolov4中采用了CIOU_Loss的回归方式,使得预测框回归的速度和精度更高一些。

    (2)DIOU_nms

    nms主要用于预测框的筛选,常用的目标检测算法中,一般采用普通的nms的方式,Yolov4则借鉴上面D/CIOU loss的论文:https://arxiv.org/pdf/1911.08287.pdf
    将其中计算IOU的部分替换成DIOU的方式:
    再来看下实际的案例:
    在这里插入图片描述
    在上图重叠的摩托车检测中,中间的摩托车因为考虑边界框中心点的位置信息,也可以回归出来。
    因此在重叠目标的检测中,DIOU_nms的效果优于传统的nms

    注意:
    有读者会有疑问,这里为什么不用CIOU_nms,而用DIOU_nms?
    :因为前面讲到的CIOU_loss,实在DIOU_loss的基础上,添加的影响因子,包含groundtruth标注框的信息,在训练时用于回归。
    但在测试过程中,并没有groundtruth的信息,不用考虑影响因子,因此直接用DIOU_nms即可。

    总体来说,YOLOv4的论文称的上良心之作,将近几年关于深度学习领域最新研究的tricks移植到Yolov4中做验证测试,将Yolov3的精度提高了不少。
    虽然没有全新的创新,但很多改进之处都值得借鉴,借用Yolov4作者的总结。

    Yolov4 主要带来了 3 点新贡献:
    (1)提出了一种高效而强大的目标检测模型,使用 1080Ti 或 2080Ti 就能训练出超快、准确的目标检测器。
    (2)在检测器训练过程中,验证了最先进的一些研究成果对目标检测器的影响。
    (3)改进了 SOTA 方法,使其更有效、更适合单 GPU 训练。


    5 YoloV4相关代码

    5.1 python代码

    代码地址:https://github.com/Tianxiaomo/pytorch-Yolov4
    作者的训练和测试推理代码都已经完成

    5.2 C++代码

    Yolov4作者Alexey的代码,俄罗斯的大神,应该是个独立研究员,更新算法的频繁程度令人佩服。
    Yolov3作者Joseph Redmon宣布停止更新Yolo算法之后,Alexey凭借对于Yolov3算法的不断探索研究,赢得了Yolov3作者的认可,发布了Yolov4
    代码地址:https://github.com/AlexeyAB/darknet

    5.3 python版本的Tensorrt代码

    目前测试有效的有tensorflow版本:weights->pb->trt
    代码地址:https://github.com/hunglc007/tensorflow-Yolov4-tflite

    5.4 C++版本的Tensorrtrt代码

    代码地址:https://github.com/wang-xinyu/tensorrtx/tree/master/Yolov4
    作者自定义了Mish激活函数Plugin层,Tensorrt加速后速度还是挺快的。

    6 Yolov5核心基础知识完整讲解

    因为Yolov5的核心基础知识点也很多,可以查看大白的另一篇文章《深入浅出Yolov5核心基础知识完整讲解》

    7 Yolox核心基础知识完整讲解

    旷视科技发布的Yolox有很多干货,可以查看大白的另一篇Yolox文章《深入浅出Yolo系列之Yolox核心基础知识完整讲解》。


    8 相关数据集下载

    实际项目中,目标检测算法应该的非常多非常多,比如人脸识别,比如疫情期间的口罩人脸识别,比如车流统计人流统计等等。
    因此大白也会将不错的值得一试的数据集汇总到此处,方便需要的同学进行下载。

    (1)口罩遮挡人脸数据集

    数据集详情:由武汉大学多媒体研究中心发起,目前是全球最大的口罩遮挡人脸数据集。
    分为真实口罩人脸和模拟口罩人脸两部分,真实口罩人脸包含525人的5000张口罩人脸9万张正常人脸。模拟口罩人脸包含1万个人共50万张模拟人脸数据集。
    应用项目:人脸检测、人脸识别
    数据集地址https://github.com/X-zhangyang/Real-World-Masked-Face-Dataset

    (2)Wider Face人脸数据集
    数据集详情:香港中文大学发起的,包含3万张图片共40万张人脸。
    应用项目:人脸检测
    数据集地址http://shuoyang1213.me/WIDERFACE/WiderFace_Results.html

    (3)Wider Person拥挤场景行人数据集
    数据集详情:多种场景比较拥挤场景的行人检测数据集,包含13382张图片,共计40万个不同遮挡程度的人体。
    应用项目:人体检测
    数据集地址http://www.cbsr.ia.ac.cn/users/sfzhang/WiderPerson/

    因为工作原因,会搜集大量的各类公开应用场景数据集。

    如果有同学需要其他场景或者其他项目的,也可以在大白网站的数据集下载版块,目前有数百个数据集,可以挑选下载。


    9 不断更新ing

    在深度学习的图像领域,肯定会涉及目标检测,而在目标检测中,Yolov3和Yolov4是非常经典,必须要学习的算法。

    有些同学,特别新接触的同学,刚学习时会觉得Yolo算法很繁琐

    而且我发现,网上很多的教程其实讲的还是比较笼统,并不适合小白学习

    所以大白耗尽洪荒之力,准备了包括Yolo目标检测在内的31节视频课,可以点击查看。

    希望和大家一起努力,在人工智能深度学习领域一起进步,一起提升,一起变强!

    在这里插入图片描述

    展开全文
  • YOLOV4

    万次阅读 2020-04-26 20:47:00
    论文:YOLOv4: Optimal Speed and Accuracy of Object Detection Github:https://github.com/AlexeyAB/darknet 论文主要针对各种可以提升精度的trick进行了整合,加入YOLOV3中,得到最终本文的YOLOV4。最终在...

    论文:YOLOv4: Optimal Speed and Accuracy of Object Detection

    Github:https://github.com/AlexeyAB/darknet

     

    论文主要针对各种可以提升精度的trick进行了整合,加入YOLOV3中,得到最终本文的YOLOV4。最终在coco上面达到了43.5%AP ,在Tesla V100 上达到了65FPS。性能+精度,好到爆炸。

     

    主要贡献:

    1. 提出了一个高效快速的目标检测框架YOLOV4
    2. 分析验证了Bag-ofFreebies 和Bag-of-Specials 方法对检测框架训练和推理的影响。
    3. 更改方法,使得YOLOV4可以适应于单GPU训练,大大降低YOLOV4的训练门槛。

     

    检测框架对比:

    输入Input:

     Image, Patches, Image Pyramid

    骨架网络Backbones:

    VGG16 , ResNet-50 , SpineNet, EfficientNet-B0/B7 , CSPResNeXt50 ,CSPDarknet53

    颈部模块Neck:
    • Additional blocks: SPP , ASPP, RFB, SAM
    • Path-aggregation blocks: FPN , PAN ,NAS-FPN , Fully-connected FPN, BiFPN, ASFF , SFAM  

    头部模块Heads:
    • Dense Prediction (one-stage):RPN , SSD , YOLO , RetinaNet (anchor based), CornerNet, CenterNet , MatrixNet, FCOS (anchor free)
    • Sparse Prediction (two-stage):Faster R-CNN , R-FCN , Mask RCNN (anchor based)RepPoints (anchor free)

     

    Bag of freebies :

    仅仅改变训练策略,并且只增加训练的开销,不增加推理测试的开销的改进,称为Bag of freebies。

    We call these methods that only change the training strategy or only increase the training cost as “bag of freebies.”

    用到的改进包括,

    (1)数据增强data augmentation

    brightness ,contrast ,hue ,saturation ,noise ,random scaling,cropping,flipping ,rotating ,CutOut, MixUp, CutMix

    (2)正则化方法

    DropOut, DropPath ,Spatial DropOut , or DropBlock

    (3)难例挖掘

    hard negative example mining ,online hard example mining ,focal loss ,label smoothing

    (4)损失函数

    MSE, IoU, GIoU, CIoU, DIoU

     

    Bag of specials:

    只通过增加很小的计算量就可以极大的提高模型精度的方法,称为Bag of specials。

    For those plugin modules and post-processing methods that only increase the inference cost by a small amount but can significantly improve the accuracy of object detection, we call them “bag of specials”.

    用到的改进包括,

    (1)增大感受野

    SPP , ASPP, RFB , Spatial Pyramid Matching (SPM)

    (2)attention方法

    Squeeze-and-Excitation (SE), Spatial Attention Module (SAM)

    (3)跳跃连接:

    Residual connections, Weighted residual connections, Multi-input weighted residual connections,  Cross stage partial connections (CSP) , FPN ,SFAM  ,ASFF  ,BiFPN  

    (4)激活函数:

    ReLU, leaky-ReLU, parametric-ReLU, ReLU6, SELU, Swish, Mish 

    (5)NMS

    greedy NMS, soft NMS

    (6)归一化方法:

    Batch Normalization (BN) ,Cross-GPU Batch Normalization (CGBN or SyncBN), Filter Response Normalization (FRN) , Cross-Iteration Batch Normalization (CBN)

     

    网络基础结构的选择:

    CSPDarknet53比CSPResNext50 ,EfficientNet-B3具有更大的感受野,更快的速度,因此,选择CSPDarknet53作为YOLOV4的基础骨架。

     

    分类精度高的模型不一定检测精度也高,

    A reference model which is optimal for classification is not always optimal for a detector.

    检测需要的条件,

    1. 更高的输入图片分辨率,有助于检测多尺度的小物体
    2. 更多的层,可以匹配更大的网络输入
    3. 更多的参数,使得模型有更大的包容力检测不同大小的物体

     

     

    实验结果:

     

     

     

    展开全文
  • 睿智的目标检测29——Keras搭建YoloV4目标检测平台

    万次阅读 多人点赞 2020-05-12 11:34:25
    睿智的目标检测29——Keras搭建YoloV4目标检测平台学习前言什么是YOLOV4YOLOV4改进的部分(不完全)改进点解析1、主干特征提取网络Backbone2、主干特征提取网络Backbone 学习前言 哈哈哈我最喜欢的YOLO更新了! ...

    学习前言

    哈哈哈我最喜欢的YOLO更新了!
    在这里插入图片描述

    什么是YOLOV4

    在这里插入图片描述
    YOLOV4是YOLOV3的改进版,在YOLOV3的基础上结合了非常多的小Tricks。
    尽管没有目标检测上革命性的改变,但是YOLOV4依然很好的结合了速度与精度。
    根据上图也可以看出来,YOLOV4在YOLOV3的基础上,在FPS不下降的情况下,mAP达到了44,提高非常明显。

    YOLOV4整体上的检测思路和YOLOV3相比相差并不大,都是使用三个特征层进行分类与回归预测。

    请注意!

    强烈建议在学习YOLOV4之前学习YOLOV3,因为YOLOV4确实可以看作是YOLOV3结合一系列改进的版本!

    强烈建议在学习YOLOV4之前学习YOLOV3,因为YOLOV4确实可以看作是YOLOV3结合一系列改进的版本!

    强烈建议在学习YOLOV4之前学习YOLOV3,因为YOLOV4确实可以看作是YOLOV3结合一系列改进的版本!

    (重要的事情说三遍!)

    YOLOV3可参考该博客:
    https://blog.csdn.net/weixin_44791964/article/details/103276106

    代码下载

    https://github.com/bubbliiiing/yolov4-keras
    喜欢的可以给个star噢!

    哔哩哔哩视频地址:
    https://www.bilibili.com/video/BV1kt4y117G6

    YOLOV4改进的部分(不完全)

    1、主干特征提取网络:DarkNet53 => CSPDarkNet53

    2、特征金字塔:SPP,PAN

    3、分类回归层:YOLOv3(未改变)

    4、训练用到的小技巧:Mosaic数据增强、Label Smoothing平滑、CIOU、学习率余弦退火衰减

    5、激活函数:使用Mish激活函数

    以上并非全部的改进部分,还存在一些其它的改进,由于YOLOV4使用的改进实在太多了,很难完全实现与列出来,这里只列出来了一些我比较感兴趣,而且非常有效的改进。

    还有一个重要的事情:
    论文中提到的SAM,作者自己的源码也没有使用。

    还有其它很多的tricks,不是所有的tricks都有提升,我也没法实现全部的tricks

    整篇BLOG会结合YOLOV3与YOLOV4的差别进行解析

    YOLOV4结构解析

    1、主干特征提取网络Backbone

    当输入是416x416时,特征结构如下:
    在这里插入图片描述
    当输入是608x608时,特征结构如下:
    在这里插入图片描述
    主干特征提取网络Backbone的改进点有两个:
    a).主干特征提取网络:DarkNet53 => CSPDarkNet53
    b).激活函数:使用Mish激活函数

    如果大家对YOLOV3比较熟悉的话,应该知道Darknet53的结构,其由一系列残差网络结构构成。在Darknet53中,其存在如下resblock_body模块,其由一次下采样多次残差结构的堆叠构成,Darknet53便是由resblock_body模块组合而成

    def resblock_body(x, num_filters, num_blocks):
        x = ZeroPadding2D(((1,0),(1,0)))(x)
        x = DarknetConv2D_BN_Leaky(num_filters, (3,3), strides=(2,2))(x)
        for i in range(num_blocks):
            y = DarknetConv2D_BN_Leaky(num_filters//2, (1,1))(x)
            y = DarknetConv2D_BN_Leaky(num_filters, (3,3))(y)
            x = Add()([x,y])
        return x
    

    而在YOLOV4中,其对该部分进行了一定的修改。
    1、其一是将DarknetConv2D的激活函数由LeakyReLU修改成了Mish,卷积块由DarknetConv2D_BN_Leaky变成了DarknetConv2D_BN_Mish
    Mish函数的公式与图像如下:
    M i s h = x × t a n h ( l n ( 1 + e x ) ) Mish=x \times tanh(ln(1+e^x)) Mish=x×tanh(ln(1+ex))
    在这里插入图片描述
    2、其二是将resblock_body的结构进行修改,使用了CSPnet结构。此时YOLOV4当中的Darknet53被修改成了CSPDarknet53
    在这里插入图片描述
    CSPnet结构并不算复杂,就是将原来的残差块的堆叠进行了一个拆分,拆成左右两部分:
    主干部分继续进行原来的残差块的堆叠
    另一部分则像一个残差边一样,经过少量处理直接连接到最后。
    因此可以认为CSP中存在一个大的残差边。

    #--------------------------------------------------------------------#
    #   CSPdarknet的结构块
    #   首先利用ZeroPadding2D和一个步长为2x2的卷积块进行高和宽的压缩
    #   然后建立一个大的残差边shortconv、这个大残差边绕过了很多的残差结构
    #   主干部分会对num_blocks进行循环,循环内部是残差结构。
    #   对于整个CSPdarknet的结构块,就是一个大残差块+内部多个小残差块
    #--------------------------------------------------------------------#
    def resblock_body(x, num_filters, num_blocks, all_narrow=True):
        #----------------------------------------------------------------#
        #   利用ZeroPadding2D和一个步长为2x2的卷积块进行高和宽的压缩
        #----------------------------------------------------------------#
        preconv1 = ZeroPadding2D(((1,0),(1,0)))(x)
        preconv1 = DarknetConv2D_BN_Mish(num_filters, (3,3), strides=(2,2))(preconv1)
    
        #--------------------------------------------------------------------#
        #   然后建立一个大的残差边shortconv、这个大残差边绕过了很多的残差结构
        #--------------------------------------------------------------------#
        shortconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(preconv1)
    
        #----------------------------------------------------------------#
        #   主干部分会对num_blocks进行循环,循环内部是残差结构。
        #----------------------------------------------------------------#
        mainconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(preconv1)
        for i in range(num_blocks):
            y = compose(
                    DarknetConv2D_BN_Mish(num_filters//2, (1,1)),
                    DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (3,3)))(mainconv)
            mainconv = Add()([mainconv,y])
        postconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(mainconv)
    
        #----------------------------------------------------------------#
        #   将大残差边再堆叠回来
        #----------------------------------------------------------------#
        route = Concatenate()([postconv, shortconv])
    
        # 最后对通道数进行整合
        return DarknetConv2D_BN_Mish(num_filters, (1,1))(route)
    

    全部实现代码为:

    from functools import wraps
    
    from keras import backend as K
    from keras.initializers import random_normal
    from keras.layers import (Add, BatchNormalization, Concatenate, Conv2D, Layer,
                              ZeroPadding2D)
    from keras.layers.normalization import BatchNormalization
    from keras.regularizers import l2
    from utils.utils import compose
    
    
    class Mish(Layer):
        def __init__(self, **kwargs):
            super(Mish, self).__init__(**kwargs)
            self.supports_masking = True
    
        def call(self, inputs):
            return inputs * K.tanh(K.softplus(inputs))
    
        def get_config(self):
            config = super(Mish, self).get_config()
            return config
    
        def compute_output_shape(self, input_shape):
            return input_shape
    
    #------------------------------------------------------#
    #   单次卷积DarknetConv2D
    #   如果步长为2则自己设定padding方式。
    #------------------------------------------------------#
    @wraps(Conv2D)
    def DarknetConv2D(*args, **kwargs):
        darknet_conv_kwargs = {'kernel_initializer' : random_normal(stddev=0.02), 'kernel_regularizer': l2(5e-4)}
        darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides')==(2,2) else 'same'
        darknet_conv_kwargs.update(kwargs)
        return Conv2D(*args, **darknet_conv_kwargs)
    
    #---------------------------------------------------#
    #   卷积块 -> 卷积 + 标准化 + 激活函数
    #   DarknetConv2D + BatchNormalization + Mish
    #---------------------------------------------------#
    def DarknetConv2D_BN_Mish(*args, **kwargs):
        no_bias_kwargs = {'use_bias': False}
        no_bias_kwargs.update(kwargs)
        return compose(
            DarknetConv2D(*args, **no_bias_kwargs),
            BatchNormalization(),
            Mish())
    
    #--------------------------------------------------------------------#
    #   CSPdarknet的结构块
    #   首先利用ZeroPadding2D和一个步长为2x2的卷积块进行高和宽的压缩
    #   然后建立一个大的残差边shortconv、这个大残差边绕过了很多的残差结构
    #   主干部分会对num_blocks进行循环,循环内部是残差结构。
    #   对于整个CSPdarknet的结构块,就是一个大残差块+内部多个小残差块
    #--------------------------------------------------------------------#
    def resblock_body(x, num_filters, num_blocks, all_narrow=True):
        #----------------------------------------------------------------#
        #   利用ZeroPadding2D和一个步长为2x2的卷积块进行高和宽的压缩
        #----------------------------------------------------------------#
        preconv1 = ZeroPadding2D(((1,0),(1,0)))(x)
        preconv1 = DarknetConv2D_BN_Mish(num_filters, (3,3), strides=(2,2))(preconv1)
    
        #--------------------------------------------------------------------#
        #   然后建立一个大的残差边shortconv、这个大残差边绕过了很多的残差结构
        #--------------------------------------------------------------------#
        shortconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(preconv1)
    
        #----------------------------------------------------------------#
        #   主干部分会对num_blocks进行循环,循环内部是残差结构。
        #----------------------------------------------------------------#
        mainconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(preconv1)
        for i in range(num_blocks):
            y = compose(
                    DarknetConv2D_BN_Mish(num_filters//2, (1,1)),
                    DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (3,3)))(mainconv)
            mainconv = Add()([mainconv,y])
        postconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(mainconv)
    
        #----------------------------------------------------------------#
        #   将大残差边再堆叠回来
        #----------------------------------------------------------------#
        route = Concatenate()([postconv, shortconv])
    
        # 最后对通道数进行整合
        return DarknetConv2D_BN_Mish(num_filters, (1,1))(route)
    
    #---------------------------------------------------#
    #   CSPdarknet53 的主体部分
    #   输入为一张416x416x3的图片
    #   输出为三个有效特征层
    #---------------------------------------------------#
    def darknet_body(x):
        x = DarknetConv2D_BN_Mish(32, (3,3))(x)
        x = resblock_body(x, 64, 1, False)
        x = resblock_body(x, 128, 2)
        x = resblock_body(x, 256, 8)
        feat1 = x
        x = resblock_body(x, 512, 8)
        feat2 = x
        x = resblock_body(x, 1024, 4)
        feat3 = x
        return feat1,feat2,feat3
    

    2、特征金字塔

    当输入是416x416时,特征结构如下:
    在这里插入图片描述
    当输入是608x608时,特征结构如下:
    在这里插入图片描述
    在特征金字塔部分,YOLOV4结合了两种改进:
    a).使用了SPP结构。
    b).使用了PANet结构。

    如上图所示,除去CSPDarknet53和Yolo Head的结构外,都是特征金字塔的结构。
    1、SPP结构参杂在对CSPdarknet53的最后一个特征层的卷积里,在对CSPdarknet53的最后一个特征层进行三次DarknetConv2D_BN_Leaky卷积后分别利用四个不同尺度的最大池化进行处理,最大池化的池化核大小分别为13x13、9x9、5x5、1x1(1x1即无处理)

    # 使用了SPP结构,即不同尺度的最大池化后堆叠。
    maxpool1 = MaxPooling2D(pool_size=(13,13), strides=(1,1), padding='same')(P5)
    maxpool2 = MaxPooling2D(pool_size=(9,9), strides=(1,1), padding='same')(P5)
    maxpool3 = MaxPooling2D(pool_size=(5,5), strides=(1,1), padding='same')(P5)
    P5 = Concatenate()([maxpool1, maxpool2, maxpool3, P5])
    

    其可以它能够极大地增加感受野,分离出最显著的上下文特征
    在这里插入图片描述
    2、PANet是2018的一种实例分割算法,其具体结构由反复提升特征的意思。
    在这里插入图片描述
    上图为原始的PANet的结构,可以看出来其具有一个非常重要的特点就是特征的反复提取
    在(a)里面是传统的特征金字塔结构,在完成特征金字塔从下到上的特征提取后,还需要实现(b)中从上到下的特征提取。

    而在YOLOV4当中,其主要是在三个有效特征层上使用了PANet结构。
    在这里插入图片描述
    实现代码如下:

    #---------------------------------------------------#
    #   Panet网络的构建,并且获得预测结果
    #---------------------------------------------------#
    def yolo_body(input_shape, anchors_mask, num_classes):
        inputs      = Input(input_shape)
        #---------------------------------------------------#   
        #   生成CSPdarknet53的主干模型
        #   获得三个有效特征层,他们的shape分别是:
        #   52,52,256
        #   26,26,512
        #   13,13,1024
        #---------------------------------------------------#
        feat1,feat2,feat3 = darknet_body(inputs)
    
        # 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512 -> 13,13,2048 -> 13,13,512 -> 13,13,1024 -> 13,13,512
        P5 = DarknetConv2D_BN_Leaky(512, (1,1))(feat3)
        P5 = DarknetConv2D_BN_Leaky(1024, (3,3))(P5)
        P5 = DarknetConv2D_BN_Leaky(512, (1,1))(P5)
        # 使用了SPP结构,即不同尺度的最大池化后堆叠。
        maxpool1 = MaxPooling2D(pool_size=(13,13), strides=(1,1), padding='same')(P5)
        maxpool2 = MaxPooling2D(pool_size=(9,9), strides=(1,1), padding='same')(P5)
        maxpool3 = MaxPooling2D(pool_size=(5,5), strides=(1,1), padding='same')(P5)
        P5 = Concatenate()([maxpool1, maxpool2, maxpool3, P5])
        P5 = DarknetConv2D_BN_Leaky(512, (1,1))(P5)
        P5 = DarknetConv2D_BN_Leaky(1024, (3,3))(P5)
        P5 = DarknetConv2D_BN_Leaky(512, (1,1))(P5)
    
        # 13,13,512 -> 13,13,256 -> 26,26,256
        P5_upsample = compose(DarknetConv2D_BN_Leaky(256, (1,1)), UpSampling2D(2))(P5)
        # 26,26,512 -> 26,26,256
        P4 = DarknetConv2D_BN_Leaky(256, (1,1))(feat2)
        # 26,26,256 + 26,26,256 -> 26,26,512
        P4 = Concatenate()([P4, P5_upsample])
        
        # 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256
        P4 = make_five_convs(P4,256)
    
        # 26,26,256 -> 26,26,128 -> 52,52,128
        P4_upsample = compose(DarknetConv2D_BN_Leaky(128, (1,1)), UpSampling2D(2))(P4)
        # 52,52,256 -> 52,52,128
        P3 = DarknetConv2D_BN_Leaky(128, (1,1))(feat1)
        # 52,52,128 + 52,52,128 -> 52,52,256
        P3 = Concatenate()([P3, P4_upsample])
    
        # 52,52,256 -> 52,52,128 -> 52,52,256 -> 52,52,128 -> 52,52,256 -> 52,52,128
        P3 = make_five_convs(P3,128)
        
        #---------------------------------------------------#
        #   第三个特征层
        #   y3=(batch_size,52,52,3,85)
        #---------------------------------------------------#
        P3_output = DarknetConv2D_BN_Leaky(256, (3,3))(P3)
        P3_output = DarknetConv2D(len(anchors_mask[0])*(num_classes+5), (1,1))(P3_output)
    
        # 52,52,128 -> 26,26,256
        P3_downsample = ZeroPadding2D(((1,0),(1,0)))(P3)
        P3_downsample = DarknetConv2D_BN_Leaky(256, (3,3), strides=(2,2))(P3_downsample)
        # 26,26,256 + 26,26,256 -> 26,26,512
        P4 = Concatenate()([P3_downsample, P4])
        # 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256
        P4 = make_five_convs(P4,256)
        
        #---------------------------------------------------#
        #   第二个特征层
        #   y2=(batch_size,26,26,3,85)
        #---------------------------------------------------#
        P4_output = DarknetConv2D_BN_Leaky(512, (3,3))(P4)
        P4_output = DarknetConv2D(len(anchors_mask[1])*(num_classes+5), (1,1))(P4_output)
        
        # 26,26,256 -> 13,13,512
        P4_downsample = ZeroPadding2D(((1,0),(1,0)))(P4)
        P4_downsample = DarknetConv2D_BN_Leaky(512, (3,3), strides=(2,2))(P4_downsample)
        # 13,13,512 + 13,13,512 -> 13,13,1024
        P5 = Concatenate()([P4_downsample, P5])
        # 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512
        P5 = make_five_convs(P5,512)
        
        #---------------------------------------------------#
        #   第一个特征层
        #   y1=(batch_size,13,13,3,85)
        #---------------------------------------------------#
        P5_output = DarknetConv2D_BN_Leaky(1024, (3,3))(P5)
        P5_output = DarknetConv2D(len(anchors_mask[2])*(num_classes+5), (1,1))(P5_output)
    
        return Model(inputs, [P5_output, P4_output, P3_output])
    

    3、YoloHead利用获得到的特征进行预测

    当输入是416x416时,特征结构如下:
    在这里插入图片描述
    当输入是608x608时,特征结构如下:
    在这里插入图片描述
    1、在特征利用部分,YoloV4提取多特征层进行目标检测,一共提取三个特征层,分别位于中间层,中下层,底层,三个特征层的shape分别为(52,52,256)、(26,26,512)、(13,13,1024)。

    2、输出层的shape分别为(13,13,75),(26,26,75),(52,52,75),最后一个维度为75是因为该图是基于voc数据集的,它的类为20种,YoloV4只有针对每一个特征层存在3个先验框,所以最后维度为3x25;
    如果使用的是coco训练集,类则为80种,最后的维度应该为255 = 3x85
    ,三个特征层的shape为(13,13,255),(26,26,255),(52,52,255)

    实现代码如下:

    #---------------------------------------------------#
    #   特征层->最后的输出
    #---------------------------------------------------#
    def yolo_body(inputs, num_anchors, num_classes):
    # 省略了一部分,只看最后的head部分
        P3_output = DarknetConv2D_BN_Leaky(256, (3,3))(P3)
        P3_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P3_output)
    
        P4_output = DarknetConv2D_BN_Leaky(512, (3,3))(P4)
        P4_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P4_output)
    
        P5_output = DarknetConv2D_BN_Leaky(1024, (3,3))(P5)
        P5_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P5_output)
    

    4、预测结果的解码

    由第二步我们可以获得三个特征层的预测结果,shape分别为(N,13,13,255),(N,26,26,255),(N,52,52,255)的数据,对应每个图分为13x13、26x26、52x52的网格上3个预测框的位置。

    但是这个预测结果并不对应着最终的预测框在图片上的位置,还需要解码才可以完成。

    此处要讲一下yolo3的预测原理,yolo3的3个特征层分别将整幅图分为13x13、26x26、52x52的网格,每个网络点负责一个区域的检测。

    我们知道特征层的预测结果对应着三个预测框的位置,我们先将其reshape一下,其结果为(N,13,13,3,85),(N,26,26,3,85),(N,52,52,3,85)。

    最后一个维度中的85包含了4+1+80,分别代表x_offset、y_offset、h和w、置信度、分类结果。

    yolo3的解码过程就是将每个网格点加上它对应的x_offset和y_offset,加完后的结果就是预测框的中心,然后再利用 先验框和h、w结合 计算出预测框的长和宽。这样就能得到整个预测框的位置了。

    在这里插入图片描述
    当然得到最终的预测结构后还要进行得分排序与非极大抑制筛选
    这一部分基本上是所有目标检测通用的部分。不过该项目的处理方式与其它项目不同。其对于每一个类进行判别。
    1、取出每一类得分大于self.obj_threshold的框和得分。
    2、利用框的位置和得分进行非极大抑制。

    实现代码如下,当调用yolo_eval时,就会对每个特征层进行解码:

    import tensorflow as tf
    from keras import backend as K
    
    
    #---------------------------------------------------#
    #   对box进行调整,使其符合真实图片的样子
    #---------------------------------------------------#
    def yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image):
        #-----------------------------------------------------------------#
        #   把y轴放前面是因为方便预测框和图像的宽高进行相乘
        #-----------------------------------------------------------------#
        box_yx = box_xy[..., ::-1]
        box_hw = box_wh[..., ::-1]
        input_shape = K.cast(input_shape, K.dtype(box_yx))
        image_shape = K.cast(image_shape, K.dtype(box_yx))
    
        if letterbox_image:
            #-----------------------------------------------------------------#
            #   这里求出来的offset是图像有效区域相对于图像左上角的偏移情况
            #   new_shape指的是宽高缩放情况
            #-----------------------------------------------------------------#
            new_shape = K.round(image_shape * K.min(input_shape/image_shape))
            offset  = (input_shape - new_shape)/2./input_shape
            scale   = input_shape/new_shape
    
            box_yx  = (box_yx - offset) * scale
            box_hw *= scale
    
        box_mins    = box_yx - (box_hw / 2.)
        box_maxes   = box_yx + (box_hw / 2.)
        boxes  = K.concatenate([box_mins[..., 0:1], box_mins[..., 1:2], box_maxes[..., 0:1], box_maxes[..., 1:2]])
        boxes *= K.concatenate([image_shape, image_shape])
        return boxes
    
    #---------------------------------------------------#
    #   将预测值的每个特征层调成真实值
    #---------------------------------------------------#
    def get_anchors_and_decode(feats, anchors, num_classes, input_shape, calc_loss=False):
        num_anchors = len(anchors)
        #------------------------------------------#
        #   grid_shape指的是特征层的高和宽
        #------------------------------------------#
        grid_shape = K.shape(feats)[1:3]
        #--------------------------------------------------------------------#
        #   获得各个特征点的坐标信息。生成的shape为(13, 13, num_anchors, 2)
        #--------------------------------------------------------------------#
        grid_x  = K.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]), [grid_shape[0], 1, num_anchors, 1])
        grid_y  = K.tile(K.reshape(K.arange(0, stop=grid_shape[0]), [-1, 1, 1, 1]), [1, grid_shape[1], num_anchors, 1])
        grid    = K.cast(K.concatenate([grid_x, grid_y]), K.dtype(feats))
        #---------------------------------------------------------------#
        #   将先验框进行拓展,生成的shape为(13, 13, num_anchors, 2)
        #---------------------------------------------------------------#
        anchors_tensor = K.reshape(K.constant(anchors), [1, 1, num_anchors, 2])
        anchors_tensor = K.tile(anchors_tensor, [grid_shape[0], grid_shape[1], 1, 1])
    
        #---------------------------------------------------#
        #   将预测结果调整成(batch_size,13,13,3,85)
        #   85可拆分成4 + 1 + 80
        #   4代表的是中心宽高的调整参数
        #   1代表的是框的置信度
        #   80代表的是种类的置信度
        #---------------------------------------------------#
        feats           = K.reshape(feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5])
        #------------------------------------------#
        #   对先验框进行解码,并进行归一化
        #------------------------------------------#
        box_xy          = (K.sigmoid(feats[..., :2]) + grid) / K.cast(grid_shape[::-1], K.dtype(feats))
        box_wh          = K.exp(feats[..., 2:4]) * anchors_tensor / K.cast(input_shape[::-1], K.dtype(feats))
        #------------------------------------------#
        #   获得预测框的置信度
        #------------------------------------------#
        box_confidence  = K.sigmoid(feats[..., 4:5])
        box_class_probs = K.sigmoid(feats[..., 5:])
        
        #---------------------------------------------------------------------#
        #   在计算loss的时候返回grid, feats, box_xy, box_wh
        #   在预测的时候返回box_xy, box_wh, box_confidence, box_class_probs
        #---------------------------------------------------------------------#
        if calc_loss == True:
            return grid, feats, box_xy, box_wh
        return box_xy, box_wh, box_confidence, box_class_probs
    
    #---------------------------------------------------#
    #   图片预测
    #---------------------------------------------------#
    def DecodeBox(outputs,
                anchors,
                num_classes,
                image_shape,
                input_shape,
                #-----------------------------------------------------------#
                #   13x13的特征层对应的anchor是[116,90],[156,198],[373,326]
                #   26x26的特征层对应的anchor是[30,61],[62,45],[59,119]
                #   52x52的特征层对应的anchor是[10,13],[16,30],[33,23]
                #-----------------------------------------------------------#
                anchor_mask     = [[6, 7, 8], [3, 4, 5], [0, 1, 2]],
                max_boxes       = 100,
                confidence      = 0.5,
                nms_iou         = 0.3,
                letterbox_image = True):
    
        box_xy = []
        box_wh = []
        box_confidence  = []
        box_class_probs = []
        for i in range(len(outputs)):
            sub_box_xy, sub_box_wh, sub_box_confidence, sub_box_class_probs = \
                get_anchors_and_decode(outputs[i], anchors[anchor_mask[i]], num_classes, input_shape)
            box_xy.append(K.reshape(sub_box_xy, [-1, 2]))
            box_wh.append(K.reshape(sub_box_wh, [-1, 2]))
            box_confidence.append(K.reshape(sub_box_confidence, [-1, 1]))
            box_class_probs.append(K.reshape(sub_box_class_probs, [-1, num_classes]))
        box_xy          = K.concatenate(box_xy, axis = 0)
        box_wh          = K.concatenate(box_wh, axis = 0)
        box_confidence  = K.concatenate(box_confidence, axis = 0)
        box_class_probs = K.concatenate(box_class_probs, axis = 0)
    
        #------------------------------------------------------------------------------------------------------------#
        #   在图像传入网络预测前会进行letterbox_image给图像周围添加灰条,因此生成的box_xy, box_wh是相对于有灰条的图像的
        #   我们需要对其进行修改,去除灰条的部分。 将box_xy、和box_wh调节成y_min,y_max,xmin,xmax
        #   如果没有使用letterbox_image也需要将归一化后的box_xy, box_wh调整成相对于原图大小的
        #------------------------------------------------------------------------------------------------------------#
        boxes       = yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image)
        box_scores  = box_confidence * box_class_probs
    
        #-----------------------------------------------------------#
        #   判断得分是否大于score_threshold
        #-----------------------------------------------------------#
        mask             = box_scores >= confidence
        max_boxes_tensor = K.constant(max_boxes, dtype='int32')
        boxes_out   = []
        scores_out  = []
        classes_out = []
        for c in range(num_classes):
            #-----------------------------------------------------------#
            #   取出所有box_scores >= score_threshold的框,和成绩
            #-----------------------------------------------------------#
            class_boxes      = tf.boolean_mask(boxes, mask[:, c])
            class_box_scores = tf.boolean_mask(box_scores[:, c], mask[:, c])
    
            #-----------------------------------------------------------#
            #   非极大抑制
            #   保留一定区域内得分最大的框
            #-----------------------------------------------------------#
            nms_index = tf.image.non_max_suppression(class_boxes, class_box_scores, max_boxes_tensor, iou_threshold=nms_iou)
    
            #-----------------------------------------------------------#
            #   获取非极大抑制后的结果
            #   下列三个分别是:框的位置,得分与种类
            #-----------------------------------------------------------#
            class_boxes         = K.gather(class_boxes, nms_index)
            class_box_scores    = K.gather(class_box_scores, nms_index)
            classes             = K.ones_like(class_box_scores, 'int32') * c
    
            boxes_out.append(class_boxes)
            scores_out.append(class_box_scores)
            classes_out.append(classes)
        boxes_out      = K.concatenate(boxes_out, axis=0)
        scores_out     = K.concatenate(scores_out, axis=0)
        classes_out    = K.concatenate(classes_out, axis=0)
    
        return boxes_out, scores_out, classes_out
    

    5、在原图上进行绘制

    通过第四步,我们可以获得预测框在原图上的位置,而且这些预测框都是经过筛选的。这些筛选后的框可以直接绘制在图片上,就可以获得结果了。

    YOLOV4的训练

    1、YOLOV4的改进训练技巧

    a)、Mosaic数据增强

    Yolov4的mosaic数据增强参考了CutMix数据增强方式,理论上具有一定的相似性!
    CutMix数据增强方式利用两张图片进行拼接。
    在这里插入图片描述
    但是mosaic利用了四张图片,根据论文所说其拥有一个巨大的优点是丰富检测物体的背景!且在BN计算的时候一下子会计算四张图片的数据!
    就像下图这样:
    在这里插入图片描述
    实现思路如下:
    1、每次读取四张图片。

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    2、分别对四张图片进行翻转、缩放、色域变化等,并且按照四个方向位置摆好。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    3、进行图片的组合和框的组合
    在这里插入图片描述

    def merge_bboxes(self, bboxes, cutx, cuty):
        merge_bbox = []
        for i in range(len(bboxes)):
            for box in bboxes[i]:
                tmp_box = []
                x1, y1, x2, y2 = box[0], box[1], box[2], box[3]
    
                if i == 0:
                    if y1 > cuty or x1 > cutx:
                        continue
                    if y2 >= cuty and y1 <= cuty:
                        y2 = cuty
                    if x2 >= cutx and x1 <= cutx:
                        x2 = cutx
    
                if i == 1:
                    if y2 < cuty or x1 > cutx:
                        continue
                    if y2 >= cuty and y1 <= cuty:
                        y1 = cuty
                    if x2 >= cutx and x1 <= cutx:
                        x2 = cutx
    
                if i == 2:
                    if y2 < cuty or x2 < cutx:
                        continue
                    if y2 >= cuty and y1 <= cuty:
                        y1 = cuty
                    if x2 >= cutx and x1 <= cutx:
                        x1 = cutx
    
                if i == 3:
                    if y1 > cuty or x2 < cutx:
                        continue
                    if y2 >= cuty and y1 <= cuty:
                        y2 = cuty
                    if x2 >= cutx and x1 <= cutx:
                        x1 = cutx
                tmp_box.append(x1)
                tmp_box.append(y1)
                tmp_box.append(x2)
                tmp_box.append(y2)
                tmp_box.append(box[-1])
                merge_bbox.append(tmp_box)
        return merge_bbox
    
    def get_random_data_with_Mosaic(self, annotation_line, input_shape, max_boxes=100, hue=.1, sat=1.5, val=1.5):
        h, w = input_shape
        min_offset_x = self.rand(0.25, 0.75)
        min_offset_y = self.rand(0.25, 0.75)
    
        nws     = [ int(w * self.rand(0.4, 1)), int(w * self.rand(0.4, 1)), int(w * self.rand(0.4, 1)), int(w * self.rand(0.4, 1))]
        nhs     = [ int(h * self.rand(0.4, 1)), int(h * self.rand(0.4, 1)), int(h * self.rand(0.4, 1)), int(h * self.rand(0.4, 1))]
    
        place_x = [int(w*min_offset_x) - nws[0], int(w*min_offset_x) - nws[1], int(w*min_offset_x), int(w*min_offset_x)]
        place_y = [int(h*min_offset_y) - nhs[0], int(h*min_offset_y), int(h*min_offset_y), int(h*min_offset_y) - nhs[3]]
    
        image_datas = [] 
        box_datas   = []
        index       = 0
        for line in annotation_line:
            # 每一行进行分割
            line_content = line.split()
            # 打开图片
            image = Image.open(line_content[0])
            image = cvtColor(image)
            
            # 图片的大小
            iw, ih = image.size
            # 保存框的位置
            box = np.array([np.array(list(map(int,box.split(',')))) for box in line_content[1:]])
            
            # 是否翻转图片
            flip = self.rand()<.5
            if flip and len(box)>0:
                image = image.transpose(Image.FLIP_LEFT_RIGHT)
                box[:, [0,2]] = iw - box[:, [2,0]]
    
            nw = nws[index] 
            nh = nhs[index] 
            image = image.resize((nw,nh), Image.BICUBIC)
    
            # 将图片进行放置,分别对应四张分割图片的位置
            dx = place_x[index]
            dy = place_y[index]
            new_image = Image.new('RGB', (w,h), (128,128,128))
            new_image.paste(image, (dx, dy))
            image_data = np.array(new_image)
    
            index = index + 1
            box_data = []
            # 对box进行重新处理
            if len(box)>0:
                np.random.shuffle(box)
                box[:, [0,2]] = box[:, [0,2]]*nw/iw + dx
                box[:, [1,3]] = box[:, [1,3]]*nh/ih + dy
                box[:, 0:2][box[:, 0:2]<0] = 0
                box[:, 2][box[:, 2]>w] = w
                box[:, 3][box[:, 3]>h] = h
                box_w = box[:, 2] - box[:, 0]
                box_h = box[:, 3] - box[:, 1]
                box = box[np.logical_and(box_w>1, box_h>1)]
                box_data = np.zeros((len(box),5))
                box_data[:len(box)] = box
            
            image_datas.append(image_data)
            box_datas.append(box_data)
    
        # 将图片分割,放在一起
        cutx = int(w * min_offset_x)
        cuty = int(h * min_offset_y)
    
        new_image = np.zeros([h, w, 3])
        new_image[:cuty, :cutx, :] = image_datas[0][:cuty, :cutx, :]
        new_image[cuty:, :cutx, :] = image_datas[1][cuty:, :cutx, :]
        new_image[cuty:, cutx:, :] = image_datas[2][cuty:, cutx:, :]
        new_image[:cuty, cutx:, :] = image_datas[3][:cuty, cutx:, :]
    
        # 进行色域变换
        hue = self.rand(-hue, hue)
        sat = self.rand(1, sat) if self.rand()<.5 else 1/self.rand(1, sat)
        val = self.rand(1, val) if self.rand()<.5 else 1/self.rand(1, val)
        x = cv2.cvtColor(np.array(new_image/255,np.float32), cv2.COLOR_RGB2HSV)
        x[..., 0] += hue*360
        x[..., 0][x[..., 0]>1] -= 1
        x[..., 0][x[..., 0]<0] += 1
        x[..., 1] *= sat
        x[..., 2] *= val
        x[x[:, :, 0]>360, 0] = 360
        x[:, :, 1:][x[:, :, 1:]>1] = 1
        x[x<0] = 0
        new_image = cv2.cvtColor(x, cv2.COLOR_HSV2RGB)*255
    
        # 对框进行进一步的处理
        new_boxes = self.merge_bboxes(box_datas, cutx, cuty)
    
        # 将box进行调整
        box_data = np.zeros((max_boxes, 5))
        if len(new_boxes)>0:
            if len(new_boxes)>max_boxes: new_boxes = new_boxes[:max_boxes]
            box_data[:len(new_boxes)] = new_boxes
        return new_image, box_data
    

    b)、Label Smoothing平滑

    标签平滑的思想很简单,具体公式如下:

    new_onehot_labels = onehot_labels * (1 - label_smoothing) + label_smoothing / num_classes
    

    当label_smoothing的值为0.01得时候,公式变成如下所示:

    new_onehot_labels = y * (1 - 0.01) + 0.01 / num_classes
    

    其实Label Smoothing平滑就是将标签进行一个平滑,原始的标签是0、1,在平滑后变成0.005(如果是二分类)、0.995,也就是说对分类准确做了一点惩罚,让模型不可以分类的太准确,太准确容易过拟合。

    实现代码如下:

    #---------------------------------------------------#
    #   平滑标签
    #---------------------------------------------------#
    def _smooth_labels(y_true, label_smoothing):
        num_classes = K.shape(y_true)[-1],
        label_smoothing = K.constant(label_smoothing, dtype=K.floatx())
        return y_true * (1.0 - label_smoothing) + label_smoothing / num_classes
    

    c)、CIOU

    IoU是比值的概念,对目标物体的scale是不敏感的。然而常用的BBox的回归损失优化和IoU优化不是完全等价的,寻常的IoU无法直接优化没有重叠的部分。

    于是有人提出直接使用IOU作为回归优化loss,CIOU是其中非常优秀的一种想法。

    CIOU将目标与anchor之间的距离,重叠率、尺度以及惩罚项都考虑进去,使得目标框回归变得更加稳定,不会像IoU和GIoU一样出现训练过程中发散等问题。而惩罚因子把预测框长宽比拟合目标框的长宽比考虑进去。

    在这里插入图片描述
    CIOU公式如下
    C I O U = I O U − ρ 2 ( b , b g t ) c 2 − α v CIOU = IOU - \frac{\rho^2(b,b^{gt})}{c^2} - \alpha v CIOU=IOUc2ρ2(b,bgt)αv
    其中, ρ 2 ( b , b g t ) \rho^2(b,b^{gt}) ρ2(b,bgt)分别代表了预测框和真实框的中心点的欧式距离。 c代表的是能够同时包含预测框和真实框的最小闭包区域的对角线距离。

    α \alpha α v v v的公式如下
    α = v 1 − I O U + v \alpha = \frac{v}{1-IOU+v} α=1IOU+vv
    v = 4 π 2 ( a r c t a n w g t h g t − a r c t a n w h ) 2 v = \frac{4}{\pi ^2}(arctan\frac{w^{gt}}{h^{gt}}-arctan\frac{w}{h})^2 v=π24(arctanhgtwgtarctanhw)2
    把1-CIOU就可以得到相应的LOSS了。
    L O S S C I O U = 1 − I O U + ρ 2 ( b , b g t ) c 2 + α v LOSS_{CIOU} = 1 - IOU + \frac{\rho^2(b,b^{gt})}{c^2} + \alpha v LOSSCIOU=1IOU+c2ρ2(b,bgt)+αv

    def box_ciou(b1, b2):
        """
        输入为:
        ----------
        b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
        b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
    
        返回为:
        -------
        ciou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1)
        """
        #-----------------------------------------------------------#
        #   求出预测框左上角右下角
        #   b1_mins     (batch, feat_w, feat_h, anchor_num, 2)
        #   b1_maxes    (batch, feat_w, feat_h, anchor_num, 2)
        #-----------------------------------------------------------#
        b1_xy = b1[..., :2]
        b1_wh = b1[..., 2:4]
        b1_wh_half = b1_wh/2.
        b1_mins = b1_xy - b1_wh_half
        b1_maxes = b1_xy + b1_wh_half
        #-----------------------------------------------------------#
        #   求出真实框左上角右下角
        #   b2_mins     (batch, feat_w, feat_h, anchor_num, 2)
        #   b2_maxes    (batch, feat_w, feat_h, anchor_num, 2)
        #-----------------------------------------------------------#
        b2_xy = b2[..., :2]
        b2_wh = b2[..., 2:4]
        b2_wh_half = b2_wh/2.
        b2_mins = b2_xy - b2_wh_half
        b2_maxes = b2_xy + b2_wh_half
    
        #-----------------------------------------------------------#
        #   求真实框和预测框所有的iou
        #   iou         (batch, feat_w, feat_h, anchor_num)
        #-----------------------------------------------------------#
        intersect_mins = K.maximum(b1_mins, b2_mins)
        intersect_maxes = K.minimum(b1_maxes, b2_maxes)
        intersect_wh = K.maximum(intersect_maxes - intersect_mins, 0.)
        intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]
        b1_area = b1_wh[..., 0] * b1_wh[..., 1]
        b2_area = b2_wh[..., 0] * b2_wh[..., 1]
        union_area = b1_area + b2_area - intersect_area
        iou = intersect_area / K.maximum(union_area, K.epsilon())
    
        #-----------------------------------------------------------#
        #   计算中心的差距
        #   center_distance (batch, feat_w, feat_h, anchor_num)
        #-----------------------------------------------------------#
        center_distance = K.sum(K.square(b1_xy - b2_xy), axis=-1)
        enclose_mins = K.minimum(b1_mins, b2_mins)
        enclose_maxes = K.maximum(b1_maxes, b2_maxes)
        enclose_wh = K.maximum(enclose_maxes - enclose_mins, 0.0)
        #-----------------------------------------------------------#
        #   计算对角线距离
        #   enclose_diagonal (batch, feat_w, feat_h, anchor_num)
        #-----------------------------------------------------------#
        enclose_diagonal = K.sum(K.square(enclose_wh), axis=-1)
        ciou = iou - 1.0 * (center_distance) / K.maximum(enclose_diagonal ,K.epsilon())
        
        v = 4 * K.square(tf.math.atan2(b1_wh[..., 0], K.maximum(b1_wh[..., 1], K.epsilon())) - tf.math.atan2(b2_wh[..., 0], K.maximum(b2_wh[..., 1],K.epsilon()))) / (math.pi * math.pi)
        alpha = v /  K.maximum((1.0 - iou + v), K.epsilon())
        ciou = ciou - alpha * v
    
        ciou = K.expand_dims(ciou, -1)
        return ciou
    

    d)、学习率余弦退火衰减

    余弦退火衰减法,学习率会先上升再下降,这是退火优化法的思想。(关于什么是退火算法可以百度。)

    上升的时候使用线性上升,下降的时候模拟cos函数下降。执行多次。

    效果如图所示:
    在这里插入图片描述

    实现方式如下,利用Callback实现,与普通的ReduceLROnPlateau调用方式类似:

    class WarmUpCosineDecayScheduler(keras.callbacks.Callback):
        def __init__(self, T_max, eta_min=0, verbose=0):
            super(WarmUpCosineDecayScheduler, self).__init__()
            self.T_max      = T_max
            self.eta_min    = eta_min
            self.verbose    = verbose
            self.init_lr    = 0
            self.last_epoch = 0
    
        def on_train_begin(self, batch, logs=None):
            self.init_lr = K.get_value(self.model.optimizer.lr)
    
        def on_epoch_end(self, batch, logs=None):
            learning_rate = self.eta_min + (self.init_lr - self.eta_min) * (1 + math.cos(math.pi * self.last_epoch / self.T_max)) / 2
            self.last_epoch += 1
    
            K.set_value(self.model.optimizer.lr, learning_rate)
            if self.verbose > 0:
                print('Setting learning rate to %s.' % (learning_rate))
    

    2、loss组成

    a)、计算loss所需参数

    在计算loss的时候,实际上是y_pre和y_true之间的对比:
    y_pre就是一幅图像经过网络之后的输出,内部含有三个特征层的内容;其需要解码才能够在图上作画
    y_true就是一个真实图像中,它的每个真实框对应的(13,13)、(26,26)、(52,52)网格上的偏移位置、长宽与种类。其仍需要编码才能与y_pred的结构一致
    实际上y_pre和y_true内容的shape都是
    (batch_size,13,13,3,85)
    (batch_size,26,26,3,85)
    (batch_size,52,52,3,85)

    b)、y_pre是什么

    网络最后输出的内容就是三个特征层每个网格点对应的预测框及其种类,即三个特征层分别对应着图片被分为不同size的网格后,每个网格点上三个先验框对应的位置、置信度及其种类。
    对于输出的y1、y2、y3而言,[…, : 2]指的是相对于每个网格点的偏移量,[…, 2: 4]指的是宽和高,[…, 4: 5]指的是该框的置信度,[…, 5: ]指的是每个种类的预测概率。
    现在的y_pre还是没有解码的,解码了之后才是真实图像上的情况。

    c)、y_true是什么。

    y_true就是一个真实图像中,它的每个真实框对应的(13,13)、(26,26)、(52,52)网格上的偏移位置、长宽与种类。其仍需要编码才能与y_pred的结构一致
    在yolo4中,其使用了一个专门的函数用于处理读取进来的图片的框的真实情况。

    def preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes):
    

    其输入为:
    true_boxes:shape为(m, T, 5)代表m张图T个框的x_min、y_min、x_max、y_max、class_id。
    input_shape:输入的形状,此处为608、608
    anchors:代表9个先验框的大小
    num_classes:种类的数量。

    其实对真实框的处理是将真实框转化成图片中相对网格的xyhw,步骤如下:
    1、取框的真实值,获取其框的中心及其宽高,除去input_shape变成比例的模式。
    2、建立全为0的y_true,y_true是一个列表,包含三个特征层,shape分别为(batch_size,13,13,3,85)、(batch_size,26,26,3,85)、(batch_size,52,52,3,85)。
    3、对每一张图片处理,将每一张图片中的真实框的wh和先验框的wh对比,计算IOU值,选取其中IOU最高的一个,得到其所属特征层及其网格点的位置,在对应的y_true中将内容进行保存。

    for t, n in enumerate(best_anchor):
        for l in range(num_layers):
            if n in anchor_mask[l]:
    
                # 计算该目标在第l个特征层所处网格的位置
                i = np.floor(true_boxes[b,t,0]*grid_shapes[l][1]).astype('int32')
                j = np.floor(true_boxes[b,t,1]*grid_shapes[l][0]).astype('int32')
    
                # 找到best_anchor索引的索引
                k = anchor_mask[l].index(n)
                c = true_boxes[b,t, 4].astype('int32')
                
                # 保存到y_true中
                y_true[l][b, j, i, k, 0:4] = true_boxes[b,t, 0:4]
                y_true[l][b, j, i, k, 4] = 1
                y_true[l][b, j, i, k, 5+c] = 1
    

    对于最后输出的y_true而言,只有每个图里每个框最对应的位置有数据,其它的地方都为0。
    preprocess_true_boxes全部的代码如下:

    def preprocess_true_boxes(self, true_boxes, input_shape, anchors, num_classes):
        assert (true_boxes[..., 4]<num_classes).all(), 'class id must be less than num_classes'
        #-----------------------------------------------------------#
        #   获得框的坐标和图片的大小
        #-----------------------------------------------------------#
        true_boxes  = np.array(true_boxes, dtype='float32')
        input_shape = np.array(input_shape, dtype='int32')
        
        #-----------------------------------------------------------#
        #   一共有三个特征层数
        #-----------------------------------------------------------#
        num_layers  = len(self.anchors_mask)
        #-----------------------------------------------------------#
        #   m为图片数量,grid_shapes为网格的shape
        #-----------------------------------------------------------#
        m           = true_boxes.shape[0]
        grid_shapes = [input_shape // {0:32, 1:16, 2:8}[l] for l in range(num_layers)]
        #-----------------------------------------------------------#
        #   y_true的格式为(m,13,13,3,85)(m,26,26,3,85)(m,52,52,3,85)
        #-----------------------------------------------------------#
        y_true = [np.zeros((m, grid_shapes[l][0], grid_shapes[l][1], len(self.anchors_mask[l]), 5 + num_classes),
                    dtype='float32') for l in range(num_layers)]
    
        #-----------------------------------------------------------#
        #   通过计算获得真实框的中心和宽高
        #   中心点(m,n,2) 宽高(m,n,2)
        #-----------------------------------------------------------#
        boxes_xy = (true_boxes[..., 0:2] + true_boxes[..., 2:4]) // 2
        boxes_wh =  true_boxes[..., 2:4] - true_boxes[..., 0:2]
        #-----------------------------------------------------------#
        #   将真实框归一化到小数形式
        #-----------------------------------------------------------#
        true_boxes[..., 0:2] = boxes_xy / input_shape[::-1]
        true_boxes[..., 2:4] = boxes_wh / input_shape[::-1]
    
        #-----------------------------------------------------------#
        #   [9,2] -> [1,9,2]
        #-----------------------------------------------------------#
        anchors         = np.expand_dims(anchors, 0)
        anchor_maxes    = anchors / 2.
        anchor_mins     = -anchor_maxes
    
        #-----------------------------------------------------------#
        #   长宽要大于0才有效
        #-----------------------------------------------------------#
        valid_mask = boxes_wh[..., 0]>0
    
        for b in range(m):
            #-----------------------------------------------------------#
            #   对每一张图进行处理
            #-----------------------------------------------------------#
            wh = boxes_wh[b, valid_mask[b]]
            if len(wh) == 0: continue
            #-----------------------------------------------------------#
            #   [n,2] -> [n,1,2]
            #-----------------------------------------------------------#
            wh          = np.expand_dims(wh, -2)
            box_maxes   = wh / 2.
            box_mins    = - box_maxes
    
            #-----------------------------------------------------------#
            #   计算所有真实框和先验框的交并比
            #   intersect_area  [n,9]
            #   box_area        [n,1]
            #   anchor_area     [1,9]
            #   iou             [n,9]
            #-----------------------------------------------------------#
            intersect_mins  = np.maximum(box_mins, anchor_mins)
            intersect_maxes = np.minimum(box_maxes, anchor_maxes)
            intersect_wh    = np.maximum(intersect_maxes - intersect_mins, 0.)
            intersect_area  = intersect_wh[..., 0] * intersect_wh[..., 1]
    
            box_area    = wh[..., 0] * wh[..., 1]
            anchor_area = anchors[..., 0] * anchors[..., 1]
    
            iou = intersect_area / (box_area + anchor_area - intersect_area)
            #-----------------------------------------------------------#
            #   维度是[n,] 感谢 消尽不死鸟 的提醒
            #-----------------------------------------------------------#
            best_anchor = np.argmax(iou, axis=-1)
    
            for t, n in enumerate(best_anchor):
                #-----------------------------------------------------------#
                #   找到每个真实框所属的特征层
                #-----------------------------------------------------------#
                for l in range(num_layers):
                    if n in self.anchors_mask[l]:
                        #-----------------------------------------------------------#
                        #   floor用于向下取整,找到真实框所属的特征层对应的x、y轴坐标
                        #-----------------------------------------------------------#
                        i = np.floor(true_boxes[b,t,0] * grid_shapes[l][1]).astype('int32')
                        j = np.floor(true_boxes[b,t,1] * grid_shapes[l][0]).astype('int32')
                        #-----------------------------------------------------------#
                        #   k指的的当前这个特征点的第k个先验框
                        #-----------------------------------------------------------#
                        k = self.anchors_mask[l].index(n)
                        #-----------------------------------------------------------#
                        #   c指的是当前这个真实框的种类
                        #-----------------------------------------------------------#
                        c = true_boxes[b, t, 4].astype('int32')
                        #-----------------------------------------------------------#
                        #   y_true的shape为(m,13,13,3,85)(m,26,26,3,85)(m,52,52,3,85)
                        #   最后的85可以拆分成4+1+80,4代表的是框的中心与宽高、
                        #   1代表的是置信度、80代表的是种类
                        #-----------------------------------------------------------#
                        y_true[l][b, j, i, k, 0:4] = true_boxes[b, t, 0:4]
                        y_true[l][b, j, i, k, 4] = 1
                        y_true[l][b, j, i, k, 5+c] = 1
    
        return y_true
    

    d)、loss的计算过程

    在得到了y_pre和y_true后怎么对比呢?不是简单的减一下!

    loss值需要对三个特征层进行处理,这里以最小的特征层为例。
    1、利用y_true取出该特征层中真实存在目标的点的位置(m,13,13,3,1)及其对应的种类(m,13,13,3,80)。
    2、将yolo_outputs的预测值输出进行处理,得到reshape后的预测值y_pre,shape为(m,13,13,3,85)。还有解码后的xy,wh。
    3、对于每一幅图,计算其中所有真实框与预测框的IOU,如果某些预测框和真实框的重合程度大于0.5,则忽略。
    4、计算ciou作为回归的loss,这里只计算正样本的回归loss。
    5、计算置信度的loss,其有两部分构成,第一部分是实际上存在目标的,预测结果中置信度的值与1对比;第二部分是实际上不存在目标的,预测结果中置信度的值与0对比。
    6、计算预测种类的loss,其计算的是实际上存在目标的,预测类与真实类的差距。

    其实际上计算的总的loss是三个loss的和,这三个loss分别是:

    • 实际存在的框,CIOU LOSS
    • 实际存在的框,预测结果中置信度的值与1对比;实际不存在的框,预测结果中置信度的值与0对比,该部分要去除被忽略的不包含目标的框
    • 实际存在的框,种类预测结果与实际结果的对比

    其实际代码如下,使用yolo_loss就可以获得loss值:

    import math
    
    import tensorflow as tf
    from keras import backend as K
    from utils.utils_bbox import get_anchors_and_decode
    
    
    def box_ciou(b1, b2):
        """
        输入为:
        ----------
        b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
        b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
    
        返回为:
        -------
        ciou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1)
        """
        #-----------------------------------------------------------#
        #   求出预测框左上角右下角
        #   b1_mins     (batch, feat_w, feat_h, anchor_num, 2)
        #   b1_maxes    (batch, feat_w, feat_h, anchor_num, 2)
        #-----------------------------------------------------------#
        b1_xy = b1[..., :2]
        b1_wh = b1[..., 2:4]
        b1_wh_half = b1_wh/2.
        b1_mins = b1_xy - b1_wh_half
        b1_maxes = b1_xy + b1_wh_half
        #-----------------------------------------------------------#
        #   求出真实框左上角右下角
        #   b2_mins     (batch, feat_w, feat_h, anchor_num, 2)
        #   b2_maxes    (batch, feat_w, feat_h, anchor_num, 2)
        #-----------------------------------------------------------#
        b2_xy = b2[..., :2]
        b2_wh = b2[..., 2:4]
        b2_wh_half = b2_wh/2.
        b2_mins = b2_xy - b2_wh_half
        b2_maxes = b2_xy + b2_wh_half
    
        #-----------------------------------------------------------#
        #   求真实框和预测框所有的iou
        #   iou         (batch, feat_w, feat_h, anchor_num)
        #-----------------------------------------------------------#
        intersect_mins = K.maximum(b1_mins, b2_mins)
        intersect_maxes = K.minimum(b1_maxes, b2_maxes)
        intersect_wh = K.maximum(intersect_maxes - intersect_mins, 0.)
        intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]
        b1_area = b1_wh[..., 0] * b1_wh[..., 1]
        b2_area = b2_wh[..., 0] * b2_wh[..., 1]
        union_area = b1_area + b2_area - intersect_area
        iou = intersect_area / K.maximum(union_area, K.epsilon())
    
        #-----------------------------------------------------------#
        #   计算中心的差距
        #   center_distance (batch, feat_w, feat_h, anchor_num)
        #-----------------------------------------------------------#
        center_distance = K.sum(K.square(b1_xy - b2_xy), axis=-1)
        enclose_mins = K.minimum(b1_mins, b2_mins)
        enclose_maxes = K.maximum(b1_maxes, b2_maxes)
        enclose_wh = K.maximum(enclose_maxes - enclose_mins, 0.0)
        #-----------------------------------------------------------#
        #   计算对角线距离
        #   enclose_diagonal (batch, feat_w, feat_h, anchor_num)
        #-----------------------------------------------------------#
        enclose_diagonal = K.sum(K.square(enclose_wh), axis=-1)
        ciou = iou - 1.0 * (center_distance) / K.maximum(enclose_diagonal ,K.epsilon())
        
        v = 4 * K.square(tf.math.atan2(b1_wh[..., 0], K.maximum(b1_wh[..., 1], K.epsilon())) - tf.math.atan2(b2_wh[..., 0], K.maximum(b2_wh[..., 1],K.epsilon()))) / (math.pi * math.pi)
        alpha = v /  K.maximum((1.0 - iou + v), K.epsilon())
        ciou = ciou - alpha * v
    
        ciou = K.expand_dims(ciou, -1)
        return ciou
    
    #---------------------------------------------------#
    #   平滑标签
    #---------------------------------------------------#
    def _smooth_labels(y_true, label_smoothing):
        num_classes = tf.cast(K.shape(y_true)[-1], dtype=K.floatx())
        label_smoothing = K.constant(label_smoothing, dtype=K.floatx())
        return y_true * (1.0 - label_smoothing) + label_smoothing / num_classes
        
    #---------------------------------------------------#
    #   用于计算每个预测框与真实框的iou
    #---------------------------------------------------#
    def box_iou(b1, b2):
        #---------------------------------------------------#
        #   num_anchor,1,4
        #   计算左上角的坐标和右下角的坐标
        #---------------------------------------------------#
        b1              = K.expand_dims(b1, -2)
        b1_xy           = b1[..., :2]
        b1_wh           = b1[..., 2:4]
        b1_wh_half      = b1_wh/2.
        b1_mins         = b1_xy - b1_wh_half
        b1_maxes        = b1_xy + b1_wh_half
    
        #---------------------------------------------------#
        #   1,n,4
        #   计算左上角和右下角的坐标
        #---------------------------------------------------#
        b2              = K.expand_dims(b2, 0)
        b2_xy           = b2[..., :2]
        b2_wh           = b2[..., 2:4]
        b2_wh_half      = b2_wh/2.
        b2_mins         = b2_xy - b2_wh_half
        b2_maxes        = b2_xy + b2_wh_half
    
        #---------------------------------------------------#
        #   计算重合面积
        #---------------------------------------------------#
        intersect_mins  = K.maximum(b1_mins, b2_mins)
        intersect_maxes = K.minimum(b1_maxes, b2_maxes)
        intersect_wh    = K.maximum(intersect_maxes - intersect_mins, 0.)
        intersect_area  = intersect_wh[..., 0] * intersect_wh[..., 1]
        b1_area         = b1_wh[..., 0] * b1_wh[..., 1]
        b2_area         = b2_wh[..., 0] * b2_wh[..., 1]
        iou             = intersect_area / (b1_area + b2_area - intersect_area)
    
        return iou
    
    #---------------------------------------------------#
    #   loss值计算
    #---------------------------------------------------#
    def yolo_loss(args, input_shape, anchors, anchors_mask, num_classes, ignore_thresh=.5, label_smoothing=0.1, print_loss=False):
        num_layers = len(anchors_mask)
        #---------------------------------------------------------------------------------------------------#
        #   将预测结果和实际ground truth分开,args是[*model_body.output, *y_true]
        #   y_true是一个列表,包含三个特征层,shape分别为:
        #   (m,13,13,3,85)
        #   (m,26,26,3,85)
        #   (m,52,52,3,85)
        #   yolo_outputs是一个列表,包含三个特征层,shape分别为:
        #   (m,13,13,3,85)
        #   (m,26,26,3,85)
        #   (m,52,52,3,85)
        #---------------------------------------------------------------------------------------------------#
        y_true          = args[num_layers:]
        yolo_outputs    = args[:num_layers]
    
        #-----------------------------------------------------------#
        #   得到input_shpae为416,416 
        #-----------------------------------------------------------#
        input_shape = K.cast(input_shape, K.dtype(y_true[0]))
    
        #-----------------------------------------------------------#
        #   取出每一张图片
        #   m的值就是batch_size
        #-----------------------------------------------------------#
        m = K.shape(yolo_outputs[0])[0]
    
        loss    = 0
        num_pos = 0
        #---------------------------------------------------------------------------------------------------#
        #   y_true是一个列表,包含三个特征层,shape分别为(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。
        #   yolo_outputs是一个列表,包含三个特征层,shape分别为(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。
        #---------------------------------------------------------------------------------------------------#
        for l in range(num_layers):
            #-----------------------------------------------------------#
            #   以第一个特征层(m,13,13,3,85)为例子
            #   取出该特征层中存在目标的点的位置。(m,13,13,3,1)
            #-----------------------------------------------------------#
            object_mask = y_true[l][..., 4:5]
            #-----------------------------------------------------------#
            #   取出其对应的种类(m,13,13,3,80)
            #-----------------------------------------------------------#
            true_class_probs = y_true[l][..., 5:]
            if label_smoothing:
                true_class_probs = _smooth_labels(true_class_probs, label_smoothing)
    
            #-----------------------------------------------------------#
            #   将yolo_outputs的特征层输出进行处理、获得四个返回值
            #   其中:
            #   grid        (13,13,1,2) 网格坐标
            #   raw_pred    (m,13,13,3,85) 尚未处理的预测结果
            #   pred_xy     (m,13,13,3,2) 解码后的中心坐标
            #   pred_wh     (m,13,13,3,2) 解码后的宽高坐标
            #-----------------------------------------------------------#
            grid, raw_pred, pred_xy, pred_wh = get_anchors_and_decode(yolo_outputs[l],
                 anchors[anchors_mask[l]], num_classes, input_shape, calc_loss=True)
            
            #-----------------------------------------------------------#
            #   pred_box是解码后的预测的box的位置
            #   (m,13,13,3,4)
            #-----------------------------------------------------------#
            pred_box = K.concatenate([pred_xy, pred_wh])
    
            #-----------------------------------------------------------#
            #   找到负样本群组,第一步是创建一个数组,[]
            #-----------------------------------------------------------#
            ignore_mask = tf.TensorArray(K.dtype(y_true[0]), size=1, dynamic_size=True)
            object_mask_bool = K.cast(object_mask, 'bool')
            
            #-----------------------------------------------------------#
            #   对每一张图片计算ignore_mask
            #-----------------------------------------------------------#
            def loop_body(b, ignore_mask):
                #-----------------------------------------------------------#
                #   取出n个真实框:n,4
                #-----------------------------------------------------------#
                true_box = tf.boolean_mask(y_true[l][b,...,0:4], object_mask_bool[b,...,0])
                #-----------------------------------------------------------#
                #   计算预测框与真实框的iou
                #   pred_box    13,13,3,4 预测框的坐标
                #   true_box    n,4 真实框的坐标
                #   iou         13,13,3,n 预测框和真实框的iou
                #-----------------------------------------------------------#
                iou = box_iou(pred_box[b], true_box)
    
                #-----------------------------------------------------------#
                #   best_iou    13,13,3 每个特征点与真实框的最大重合程度
                #-----------------------------------------------------------#
                best_iou = K.max(iou, axis=-1)
    
                #-----------------------------------------------------------#
                #   判断预测框和真实框的最大iou小于ignore_thresh
                #   则认为该预测框没有与之对应的真实框
                #   该操作的目的是:
                #   忽略预测结果与真实框非常对应特征点,因为这些框已经比较准了
                #   不适合当作负样本,所以忽略掉。
                #-----------------------------------------------------------#
                ignore_mask = ignore_mask.write(b, K.cast(best_iou<ignore_thresh, K.dtype(true_box)))
                return b+1, ignore_mask
    
            #-----------------------------------------------------------#
            #   在这个地方进行一个循环、循环是对每一张图片进行的
            #-----------------------------------------------------------#
            _, ignore_mask = K.control_flow_ops.while_loop(lambda b,*args: b<m, loop_body, [0, ignore_mask])
    
            #-----------------------------------------------------------#
            #   ignore_mask用于提取出作为负样本的特征点
            #   (m,13,13,3)
            #-----------------------------------------------------------#
            ignore_mask = ignore_mask.stack()
            #   (m,13,13,3,1)
            ignore_mask = K.expand_dims(ignore_mask, -1)
    
            #-----------------------------------------------------------#
            #   真实框越大,比重越小,小框的比重更大。
            #-----------------------------------------------------------#
            box_loss_scale = 2 - y_true[l][...,2:3]*y_true[l][...,3:4]
    
            #-----------------------------------------------------------#
            #   计算Ciou loss
            #-----------------------------------------------------------#
            raw_true_box    = y_true[l][...,0:4]
            ciou            = box_ciou(pred_box, raw_true_box)
            ciou_loss       = object_mask * box_loss_scale * (1 - ciou)
            
            #------------------------------------------------------------------------------#
            #   如果该位置本来有框,那么计算1与置信度的交叉熵
            #   如果该位置本来没有框,那么计算0与置信度的交叉熵
            #   在这其中会忽略一部分样本,这些被忽略的样本满足条件best_iou<ignore_thresh
            #   该操作的目的是:
            #   忽略预测结果与真实框非常对应特征点,因为这些框已经比较准了
            #   不适合当作负样本,所以忽略掉。
            #------------------------------------------------------------------------------#
            confidence_loss = object_mask * K.binary_crossentropy(object_mask, raw_pred[...,4:5], from_logits=True)+ \
                (1-object_mask) * K.binary_crossentropy(object_mask, raw_pred[...,4:5], from_logits=True) * ignore_mask
            
            class_loss = object_mask * K.binary_crossentropy(true_class_probs, raw_pred[...,5:], from_logits=True)
    
            location_loss   = K.sum(ciou_loss)
            confidence_loss = K.sum(confidence_loss)
            class_loss      = K.sum(class_loss)
            #-----------------------------------------------------------#
            #   计算正样本数量
            #-----------------------------------------------------------#
            num_pos += tf.maximum(K.sum(K.cast(object_mask, tf.float32)), 1)
            loss    += location_loss + confidence_loss + class_loss
            # if print_loss:
            #   loss = tf.Print(loss, [loss, location_loss, confidence_loss, class_loss, K.sum(ignore_mask)], message='loss: ')
            
        loss = loss / num_pos
        return loss
    

    训练自己的YoloV4模型

    首先前往Github下载对应的仓库,下载完后利用解压软件解压,之后用编程软件打开文件夹。
    注意打开的根目录必须正确,否则相对目录不正确的情况下,代码将无法运行。

    一定要注意打开后的根目录是文件存放的目录。
    在这里插入图片描述

    一、数据集的准备

    本文使用VOC格式进行训练,训练前需要自己制作好数据集,如果没有自己的数据集,可以通过Github连接下载VOC12+07的数据集尝试下。
    训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的Annotation中。
    在这里插入图片描述
    训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。
    在这里插入图片描述
    此时数据集的摆放已经结束。

    二、数据集的处理

    在完成数据集的摆放之后,我们需要对数据集进行下一步的处理,目的是获得训练用的2007_train.txt以及2007_val.txt,需要用到根目录下的voc_annotation.py。

    voc_annotation.py里面有一些参数需要设置。
    分别是annotation_mode、classes_path、trainval_percent、train_percent、VOCdevkit_path,第一次训练可以仅修改classes_path

    '''
    annotation_mode用于指定该文件运行时计算的内容
    annotation_mode为0代表整个标签处理过程,包括获得VOCdevkit/VOC2007/ImageSets里面的txt以及训练用的2007_train.txt、2007_val.txt
    annotation_mode为1代表获得VOCdevkit/VOC2007/ImageSets里面的txt
    annotation_mode为2代表获得训练用的2007_train.txt、2007_val.txt
    '''
    annotation_mode     = 0
    '''
    必须要修改,用于生成2007_train.txt、2007_val.txt的目标信息
    与训练和预测所用的classes_path一致即可
    如果生成的2007_train.txt里面没有目标信息
    那么就是因为classes没有设定正确
    仅在annotation_mode为0和2的时候有效
    '''
    classes_path        = 'model_data/voc_classes.txt'
    '''
    trainval_percent用于指定(训练集+验证集)与测试集的比例,默认情况下 (训练集+验证集):测试集 = 9:1
    train_percent用于指定(训练集+验证集)中训练集与验证集的比例,默认情况下 训练集:验证集 = 9:1
    仅在annotation_mode为0和1的时候有效
    '''
    trainval_percent    = 0.9
    train_percent       = 0.9
    '''
    指向VOC数据集所在的文件夹
    默认指向根目录下的VOC数据集
    '''
    VOCdevkit_path  = 'VOCdevkit'
    

    classes_path用于指向检测类别所对应的txt,以voc数据集为例,我们用的txt为:
    在这里插入图片描述
    训练自己的数据集时,可以自己建立一个cls_classes.txt,里面写自己所需要区分的类别。

    三、开始网络训练

    通过voc_annotation.py我们已经生成了2007_train.txt以及2007_val.txt,此时我们可以开始训练了。
    训练的参数较多,大家可以在下载库后仔细看注释,其中最重要的部分依然是train.py里的classes_path。

    classes_path用于指向检测类别所对应的txt,这个txt和voc_annotation.py里面的txt一样!训练自己的数据集必须要修改!
    在这里插入图片描述
    修改完classes_path后就可以运行train.py开始训练了,在训练多个epoch后,权值会生成在logs文件夹中。
    其它参数的作用如下:

    #--------------------------------------------------------#
    #   训练前一定要修改classes_path,使其对应自己的数据集
    #--------------------------------------------------------#
    classes_path    = 'model_data/voc_classes.txt'
    #---------------------------------------------------------------------#
    #   anchors_path代表先验框对应的txt文件,一般不修改。
    #   anchors_mask用于帮助代码找到对应的先验框,一般不修改。
    #---------------------------------------------------------------------#
    anchors_path    = 'model_data/yolo_anchors.txt'
    anchors_mask    = [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
    #-------------------------------------------------------------------------------------#
    #   权值文件请看README,百度网盘下载
    #   训练自己的数据集时提示维度不匹配正常,预测的东西都不一样了自然维度不匹配
    #   预训练权重对于99%的情况都必须要用,不用的话权值太过随机,特征提取效果不明显
    #   网络训练的结果也不会好,数据的预训练权重对不同数据集是通用的,因为特征是通用的
    #------------------------------------------------------------------------------------#
    model_path      = 'model_data/yolo4_weight.h5'
    #------------------------------------------------------#
    #   输入的shape大小,一定要是32的倍数
    #------------------------------------------------------#
    input_shape     = [416, 416]
    #------------------------------------------------------#
    #   Yolov4的tricks应用
    #   mosaic 马赛克数据增强 True or False 
    #   实际测试时mosaic数据增强并不稳定,所以默认为False
    #   Cosine_scheduler 余弦退火学习率 True or False
    #   label_smoothing 标签平滑 0.01以下一般 如0.01、0.005
    #------------------------------------------------------#
    mosaic              = False
    Cosine_scheduler    = False
    label_smoothing     = 0
    
    #----------------------------------------------------#
    #   训练分为两个阶段,分别是冻结阶段和解冻阶段
    #   冻结阶段训练参数
    #   此时模型的主干被冻结了,特征提取网络不发生改变
    #   占用的显存较小,仅对网络进行微调
    #----------------------------------------------------#
    Init_Epoch          = 0
    Freeze_Epoch        = 50
    Freeze_batch_size   = 4
    Freeze_lr           = 1e-3
    #----------------------------------------------------#
    #   解冻阶段训练参数
    #   此时模型的主干不被冻结了,特征提取网络会发生改变
    #   占用的显存较大,网络所有的参数都会发生改变
    #   batch不能为1
    #----------------------------------------------------#
    UnFreeze_Epoch      = 100
    Unfreeze_batch_size = 4
    Unfreeze_lr         = 1e-4
    #------------------------------------------------------#
    #   是否进行冻结训练,默认先冻结主干训练后解冻训练。
    #------------------------------------------------------#
    Freeze_Train        = True
    #------------------------------------------------------#
    #   用于设置是否使用多线程读取数据,0代表关闭多线程
    #   开启后会加快数据读取速度,但是会占用更多内存
    #   keras里开启多线程有些时候速度反而慢了许多
    #   在IO为瓶颈的时候再开启多线程,即GPU运算速度远大于读取图片的速度。
    #------------------------------------------------------#
    num_workers         = 0
    #----------------------------------------------------#
    #   获得图片路径和标签
    #----------------------------------------------------#
    train_annotation_path   = '2007_train.txt'
    val_annotation_path     = '2007_val.txt'
    

    四、训练结果预测

    训练结果预测需要用到两个文件,分别是yolo.py和predict.py。
    我们首先需要去yolo.py里面修改model_path以及classes_path,这两个参数必须要修改。

    model_path指向训练好的权值文件,在logs文件夹里。
    classes_path指向检测类别所对应的txt。

    在这里插入图片描述
    完成修改后就可以运行predict.py进行检测了。运行后输入图片路径即可检测。

    展开全文
  • 打开VINO-YOLOV4 介绍 这是完整实现。 基于 支持机型 YOLOv4 YOLOv4-relu YOLOv4-tiny 支援的装置 英特尔CPU 英特尔GPU HDDL VPU NCS2 …… 支持的推理演示 Python演示:所有模型 C ++演示:YOLOv4YOLOv...
  • YOLOv4-pytorch(专注的YOLOv4和Mobilenetv3 YOLOv4) 这是YOLOv4架构的PyTorch重新实现,它基于正式的实现与PASCAL VOC,COCO和客户数据集 结果(更新中) 姓名 训练数据集 测试数据集 测试大小 地图 推理时间...
  • YOLOv4

    千次阅读 2020-04-25 21:55:03
    Self-adversarial-training(SAT),Mish-activation Mosaic data augmentation, DropBlock, CIoU等组合得到了爆炸性的YOLOv4,可以吊打一切的YOLOv4.在MS-COCO数据上:43.5%@AP(65.7%@AP50)同时可以达到65fps@...

    在这里插入图片描述
    paper:https://arxiv.org/pdf/2004.10934.pdf,

    code:https://github.com/AlexeyAB/darknet

    核心中的核心:作者将Weighted-Residual-Connections(WRC), Cross-Stage-Partial-connections(CSP), Cross mini-Batch Normalization(CmBN), Self-adversarial-training(SAT),Mish-activation Mosaic data augmentation, DropBlock, CIoU等组合得到了爆炸性的YOLOv4,可以吊打一切的YOLOv4.在MS-COCO数据上:43.5%@AP(65.7%@AP50)同时可以达到65fps@TeslaV100.

    在这里插入图片描述

    Contribution

    作者设计YOLO的目的之初就是设计一个快速而高效的目标检测器。该文的贡献主要有以下几点:

    • 设计了一种快速而强有力的目标检测器,它使得任何人仅需一个1080Ti或者2080Ti即可训练这样超快且精确的目标检测器你;
    • (不会翻译直接上英文)We verify the influence of SOTA bag-of-freebies and bag-of-specials methods of object detection during detector training
    • 作者对SOTA方法进行改进(含CBN、PAN,SAM)以使其更适合单GPU训练
    Method

    作者在现有实时网络的基础上提出了两种观点:

    • 对于GPU而言,在组卷积中采用小数量的groups(1-8),比如CSPResNeXt50/CSPDarknet53;
    • 对于VPU而言,采用组卷积而不采用SE模块。
    网路结构选择

    网络结构选择是为了在输入分辨率、网络层数、参数量、输出滤波器数之间寻求折中。作者研究表明:CSPResNeXt50在分类方面优于CSPDarkNet53,而在检测方面反而表现要差。

    网络主要结构确定了后,下一个目标是选择额外的模块以提升感受野、更好的特征汇聚模块(如FPN、PAN、ASFF、BiFPN)。对于分类而言最好的模型可能并不适合于检测,相反,检测模型需要具有以下特性:

    • 更高的输入分辨率,为了更好的检测小目标;
    • 更多的层,为了具有更大的感受野;
    • 更多的参数,更大的模型可以同时检测不同大小的目标。

    一句话就是:选择具有更大感受野、更大参数的模型作为backbone。下图给出了不同backbone的上述信息对比。从中可以看到:CSPResNeXt50仅仅包含16个卷积层,其感受野为425x425,包含20.6M参数;而CSPDarkNet53包含29个卷积层,725x725的感受野,27.6M参数。这从理论与实验角度表明:CSPDarkNet53更适合作为检测模型的Backbone。
    在这里插入图片描述

    在CSPDarkNet53基础上,作者添加了SPP模块,因其可以提升模型的感受野、分离更重要的上下文信息、不会导致模型推理速度的下降;与此同时,作者还采用PANet中的不同backbone级的参数汇聚方法替代FPN。

    最终的模型为:CSPDarkNet53+SPP+PANet(path-aggregation neck)+YOLOv3-head = YOLOv4.

    Tricks选择

    为更好的训练目标检测模型,CNN模型通常具有以下模块:

    • Activations:ReLU、Leaky-ReLU、PReLU、ReLU6、SELU、Swish or Mish
    • Bounding box regression Loss:MSE、IoU、GIoU、CIoU、DIoU
    • Data Augmentation:CutOut、MixUp、CutMix
    • Regularization:DropOut、DropPath、Spatial DropOut、DropBlock
    • Normalization:BN、SyncBn、FRN、CBN
    • Skip-connections:Residual connections, weighted residual connections, Cross stage partial connections

    作者从上述模块中选择如下:激活函数方面选择Mish;正则化方面选择DropBlock;由于聚焦在单GPU,故而未考虑SyncBN。

    其他改进策略

    为使得所涉及的检测器更适合于单GPU,作者还进行了其他几项额外设计与改进:

    • 引入一种新的数据增广方法:Mosaic与自对抗训练;
    • 通过GA算法选择最优超参数;
    • 对现有方法进行改进以更适合高效训练和推理:改进SAM、改进PAN,CmBN。

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    YOLOv4

    总而言之,YOLOv4包含以下信息:

    Backbone:CSPDarkNet53
    Neck:SPP,PAN
    Head:YOLOv3
    Tricks(backbone):CutMix、Mosaic、DropBlock、Label Smoothing
    Modified(backbone): Mish、CSP、MiWRC
    Tricks(detector):CIoU、CMBN、DropBlock、Mosaic、SAT、Eliminate grid sensitivity、Multiple Anchor、Cosine Annealing scheduler、Random training shape
    Modified(tector):Mish、SPP、SAM、PAN、DIoU-NMS
    
    Experiments

    在这里插入图片描述

    在这里插入图片描述

    展开全文
  • yolov4.conv.137这个文件,作者给的下载链接,是外网,所以翻墙去下载了一下,速度还不错,这玩意没必要还收费,毕竟模型都是作者的,直接免费分享了: 链接:https://pan.baidu.com/s/1XrcPHdp2_4c-dKge2Guw4w 提取...
  • yolov4.cfg_yolov4.weights.7z

    2021-02-25 23:09:03
    yolov4.cfg yolov4.weights yolov4-tiny.cfg yolov4-tiny.weights coco.names
  • yolov4-keras框架下的实现,所需环境tensorflow-gpu==1.13.1 keras==2.1.5
  • YOLOv4:kaggle的YOLOv4
  • 目录 一、yolov4中的基础结构: 1.Dark layer 2.rCSP(普通结构without SPP): 3.rCSP(SPP) ...AB大神的darknet中提供的cfg包括:yolov4yolov4-tiny和yolov4x-mish (2020/11/24) WongKinYiu的sc
  • YOLOv4-pytorch 训练自己的数据集环境新特性安装依赖库准备工作下载预训练模型训练测试 注:版权所有,转载请注明出处。 环境 Nvida GeForce RTX 2070 CUDA10.0 CUDNN7.0 Windows10 Python 3.6 代码地址: 新特性 ...
  • yolov4: yolov4-conv-137 预训练模型 + yolov4 作者训练好的模型 不想花积分的,可以去这个链接直接下载:https://blog.csdn.net/baobei0112/article/details/105971290
  • YOLOv4 YOLOv4的TensorFlow 2.0实现:物体检测的最佳速度和准确性 该实现(目前)使用的原始Darknet权重进行。 请参阅路线图部分以了解下一步。 安装 要安装此软件包,您可以运行: pip install tf2_yolov4 pip ...
  • 主要是Yolov4-tiny Yolov4的神经网络模型文件,可以直接调用,主要运用在物体识别、目标识别跟踪、人脸识别、无人机等应用领域。
  • tensorflow-yolov4-tflite YOLOv4YOLOv4-tiny在Tensorflow 2.0中实现。 将YOLO v4,YOLOv3,YOLO tiny .weights转换为.pb,.tflite和trt格式以生成tensorflow,tensorflow lite和tensorRT。 下载yolov4.weights...
  • 适合用于正在做Yolov4的群体
  • YOLOv4-vs-YOLOv4-tiny-源码

    2021-04-22 17:38:28
    YOLOv4-vs-YOLOv4-tiny 根文件夹中有两个主要的jupyter笔记本,其中包含在roboflow的数据集上运行YOLOv4YOLOv4-tiny的所有代码和指令。 我还添加了一些功能来使用您的网络摄像头捕获实时对象检测。 视频文件夹...
  • yolov3,yolov4,yolov5网络架构图 Visio 2013绘制 vsdx文件 可编辑
  • 共有两类模型,分别包含 darknet_model_yolov4,有:yolov4 / yolov4-tiny 预训练模型 yolov4.conv.137 / yolov4-tiny.conv.29 和测试模型 yolov4.weights / yolov4-tiny.weights)。还包含 ncnn_model_yolov4,有:...
  • yolov4-自定义功能 在TensorFlow,TFLite和TensorRT中实现的YOLOv4YOLOv4-tiny,YOLOv3和YOLOv3-tiny的各种自定义功能。 免责声明:此存储库与我的存储库非常相似: 。我创建此存储库是为了探索要用YOLOv4实现的...
  • 该存储库使用简化和最少的代码来重现 yolov3 / yolov4 检测网络和暗网分类网络。 亮点如下: 1、支持原版暗网模式; 2、支持“* .cfg”、“* .weights”模型的训练、推理、导入导出; 3、支持最新的yolov3、yolov4...
  • yolov4.zip

    2020-07-13 13:38:03
    YOLOv4的权重文件,yolov4.weights。YOLOv4的权重文件,yolov4.weights
  • yolov4deepsort YOLOV4 DEEPSORT 2021
  • YOLOv4 / Scaled-YOLOv4 / YOLO-用于对象检测的神经网络(Windows和Linux版本的Darknet)
  • Yolov4-QtGUI:Win10 vs2017 QT_Demo yolov4
  • YOLOv4目标检测

    2021-01-03 09:13:20
    YOLOv4目标检测,介绍YOLOv4使用方法,训练

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 39,401
精华内容 15,760
关键字:

yolov4