ssd 订阅
固态驱动器(Solid State Disk或Solid State Drive,简称SSD),俗称固态硬盘,固态硬盘是用固态电子存储芯片阵列而制成的硬盘,因为台湾英语里把固体电容称之为Solid而得名。SSD由控制单元和存储单元(FLASH芯片、DRAM芯片)组成。固态硬盘在接口的规范和定义、功能及使用方法上与普通硬盘的完全相同,在产品外形和尺寸上也完全与普通硬盘一致。被广泛应用于军事、车载、工控、视频监控、网络监控、网络终端、电力、医疗、航空、导航设备等诸多领域。其芯片的工作温度范围很宽,商规产品(0~70℃)工规产品(-40~85℃)。虽然成本较高,但也正在逐渐普及到DIY市场。由于固态硬盘技术与传统硬盘技术不同,所以产生了不少新兴的存储器厂商。厂商只需购买NAND存储器,再配合适当的控制芯片,就可以制造固态硬盘了。新一代的固态硬盘普遍采用SATA-2接口、SATA-3接口、SAS接口、MSATA接口、PCI-E接口、NGFF接口、CFast接口、SFF-8639接口和M.2 NVME/SATA协议。 [1] 展开全文
固态驱动器(Solid State Disk或Solid State Drive,简称SSD),俗称固态硬盘,固态硬盘是用固态电子存储芯片阵列而制成的硬盘,因为台湾英语里把固体电容称之为Solid而得名。SSD由控制单元和存储单元(FLASH芯片、DRAM芯片)组成。固态硬盘在接口的规范和定义、功能及使用方法上与普通硬盘的完全相同,在产品外形和尺寸上也完全与普通硬盘一致。被广泛应用于军事、车载、工控、视频监控、网络监控、网络终端、电力、医疗、航空、导航设备等诸多领域。其芯片的工作温度范围很宽,商规产品(0~70℃)工规产品(-40~85℃)。虽然成本较高,但也正在逐渐普及到DIY市场。由于固态硬盘技术与传统硬盘技术不同,所以产生了不少新兴的存储器厂商。厂商只需购买NAND存储器,再配合适当的控制芯片,就可以制造固态硬盘了。新一代的固态硬盘普遍采用SATA-2接口、SATA-3接口、SAS接口、MSATA接口、PCI-E接口、NGFF接口、CFast接口、SFF-8639接口和M.2 NVME/SATA协议。 [1]
信息
英文缩写
SSD
应用领域
军事、车载、工控、电力、医疗等
外文名
Solid State Disk或Solid State Drive [2]
中文名
固态硬盘/固态驱动器
组    成
由控制单元与存储单元组成
固态硬盘分类
分类方式: 固态硬盘的存储介质分为两种,一种是采用闪存(FLASH芯片)作为存储介质,另外一种是采用DRAM作为存储介质。 基于闪存类:基于闪存的固态硬盘(IDEFLASH DISK、Serial ATA Flash Disk):采用FLASH芯片作为存储介质,这也是通常所说的SSD。它的外观可以被制作成多种模样,例如:笔记本硬盘、微硬盘、存储卡、U盘等样式。这种SSD固态硬盘最大的优点就是可以移动,而且数据保护不受电源控制,能适应于各种环境,适合于个人用户使用。 [1]  一般它擦写次数普遍为3000次左右,以常用的64G为例,在SSD的平衡写入机理下,可擦写的总数据量为64G X 3000 = 192000G,假如你是个变态视频王每天喜欢下载视频看完就删每天下载100G的话,可用天数为192000 / 100 = 1920,也就是 1920 / 366 = 5.25 年。如果你只是普通用户每天写入的数据远低于10G,就拿10G来算,可以不间断用52.5年,再如果你用的是128G的SSD的话,可以不间断用104年!这什么概念?它像普通硬盘HDD一样,理论上可以无限读写,基于DRAM类:基于DRAM的固态硬盘:采用DRAM作为存储介质,应用范围较窄。它仿效传统硬盘的设计,可被绝大部分操作系统的文件系统工具进行卷设置和管理,并提供工业标准的PCI和FC接口用于连接主机或者服务器。应用方式可分为SSD硬盘和SSD硬盘阵列两种。它是一种高性能的存储器,而且使用寿命很长,美中不足的是需要独立电源来保护数据安全。DRAM固态硬盘属于比较非主流的设备。 [1] 
收起全文
精华内容
参与话题
问答
  • 理论与实战相结合,在服务器端一步步演示如何采用YOLO、SSD针对自己的检测目标进行训练,以及其中的一些技巧和经验。同时讲解如何对YOLO、SSD进行改进和创新,将其他技术与模型引入到原有模型中,并将自己的模型移植...
  • SSD详解

    千次阅读 2019-05-17 14:31:41
    目标检测就是要找到一张图中所有的物体和它们的位置,人们通常的做法是先用一些传统视觉的方法如selective search找到proposal,即比较可能是物体的一个区域,然后再用CNN判断这个物体究竟是不是物体,是哪个物体,...

           目标检测就是要找到一张图中所有的物体和它们的位置,人们通常的做法是先用一些传统视觉的方法如selective search找到proposal,即比较可能是物体的一个区域,然后再用CNN判断这个物体究竟是不是物体,是哪个物体,以及用CNN去优化这个框的位置,这种方法最典型的代表就是RCNN和Fast-RCNN。

    selective search

            目标识别(objec recognition)是指明一幅输入图像中包含那类目标。其输入为一幅图像,输出是该图像中的目标属于哪个类别(class probability)。而目标检测(object detection)除了要告诉输入图像中包含了哪类目前外,还要框出该目标的具体位置(bounding boxes)。

          在目标检测时,为了定位到目标的具体位置,通常会把图像分成许多子块(sub-regions / patches),然后把子块作为输入,送到目标识别的模型中。分子块的最直接方法叫滑动窗口法(sliding window approach)。滑动窗口的方法就是按照子块的大小在整幅图像上穷举所有子图像块。和滑动窗口法相对的是另外一类基于区域(region proposal)的方法。selective search就是其中之一!

    selective search算法流程

    step0:生成区域集R(R中的每个元素都包含了该区域的位置,尺寸,颜色特征,纹理特征等信息)具体参见论文《Efficient Graph-Based Image Segmentation》

    step1:获得原始区域集R中相交的区域。计算两个原始区域集R中相交区域的相似度S={s1,s2,…} 
    step2:找出相似度最高的两个区域(设为(a, b)),将其合并为新集c,添加进R 
    step3:从S中移除所有与step2中有关的子集 ,即将所有包含a或b的区域对(设为(a, x),(b, y),(a, b))及对应的相似度,从S中移除。
    step4:计算新集与所有子集的相似度 ,即分别计算区域c与x,y的相似度,并将(c, x): similarity_1和(c, y): similarity_2添加到S中。

    step5:跳至step2,直至S为空,我们得到了完整的区域集R’

    论文考虑了颜色、纹理、尺寸和空间交叠这4个参数。

    颜色相似度(color similarity)

    将色彩空间转为HSV,每个通道下以bins=25计算直方图,这样每个区域的颜色直方图有25*3=75个区间。 对直方图除以区域尺寸做归一化后使用下式计算相似度:

                                                     

    纹理相似度(texture similarity)

          论文采用方差为1的高斯分布在8个方向做梯度统计,然后将统计结果(尺寸与区域大小一致)以bins=10计算直方图。直方图区间数为8*3*10=240(使用RGB色彩空间)。

                                                      
    其中,是直方图中第个bin的值。

    尺寸相似度(size similarity)

                                                  

    保证合并操作的尺度较为均匀,避免一个大区域陆续“吃掉”其他小区域。

    例:设有区域a-b-c-d-e-f-g-h。较好的合并方式是:ab-cd-ef-gh -> abcd-efgh -> abcdefgh。 不好的合并方法是:ab-c-d-e-f-g-h ->abcd-e-f-g-h ->abcdef-gh -> abcdefgh。

    交叠相似度(shape compatibility measure)

                                 

                                     

    最终的相似度:

                      

    from :https://blog.csdn.net/guoyunfei20/article/details/78723646


          Faster-RCNN解决的问题是省去了selective search,直接用CNN得到最后的结果,并且性能比之前的方法有很大提升。这篇文章主要介绍一下Faster-RCNN中替换掉selective search的RPN(Region Proposal Network)以及对RPN的改进:SSD(Single Shot MultiBox Detector)。

          Faster-RCNN由RPN和Fast-RCNN组成,RPN负责寻找proposal,Fast-RCNN负责对RPN的结果进一步优化。其实RPN已经可以找到图片中每个物体的种类和位置,如果更注重速度而不是精度的话完全可以只使用RPN。RPN是一个全卷积网络(FCN),由于没有全连接层,所以可以输入任意分辨率的图像,经过网络后就得到一个feature map,然后怎么利用这个feature map得到物体的位置和类别那?

           这里要先介绍一下文章中提到的anchor这个概念,把这个feature map上的每一个点映射回原图,得到这些点的坐标,然后着这些点周围取一些提前设定好的区域,如选取每个点周围5x5的一个区域,这些选好的区域可以用来训练RPN。假设我们对feature map上的每个点选取了K个anchor,feature map的大小为H*W*C,那么我们再对这个feature map做两次卷积操作,输出分别是H*W*num_class*K和H*W*4*K,分别对应每个点每个anchor属于每一类的概率以及它所对应的物体的坐标,那么怎么训练这个网络那?

         这个网络的loss function就是一个用于分类的softmax loss和一个用于回归的smooth L1 loss,输出对应的ground truth也很好得到,对于每个anchor,如果它和图片中某个物体的IOU(面积的交/面积的并)大于一个阈值,就认为它属于这一类,否则认为是背景,对于那些是背景的anchor回归的loss就是0,其他anchor位置的ground truth就是它们所对应的物体的位置。RPN其实也很简单,关键的地方就在于选取了一些anchor然后进行pixel-wise的学习。


           但是RPN也有缺点,最大的问题就是对小物体检测效果很差,假设输入为512*512,经过网络后得到的feature map是32*32,那么feature map上的一个点就要负责周围至少是16*16的一个区域的特征表达,那对于在原图上很小的物体它的特征就难以得到充分的表示,因此检测效果比较差。去年年底的工作SSD: Single Shot MultiBox Detector很好的解决了这个问题。

          SSD可以理解为multi-scale版本的RPN,它和RPN最大的不同在于RPN只是在最后的feature map上预测检测的结果,而最后一层的feature map往往都比较抽象,对于小物体不能很好地表达特征,而SSD允许从CNN各个level的feature map预测检测结果,这样就能很好地适应不同scale的物体,对于小物体可以由更底层的feature map做预测。这就是SSD和RPN最大的不同,其他地方几乎一样。下图是SSD的网络结构:

          SSD算法是一种直接预测目标类别和bounding box的多目标检测算法。针对不同大小的目标检测,传统的做法是先将图像转换成不同大小(图像金字塔),然后分别检测,最后将结果综合起来(NMS)。SSD算法则利用不同卷积层的 feature map 进行综合也能达到同样的效果。算法的主网络结构是VGG16,将最后两个全连接层改成卷积层,并随后增加了4个卷积层来构造网络结构。对其中5种不同的卷积层的输出feature map分别用两个不同的 3×3 的卷积核进行卷积,一个输出分类用的confidence,每个default box 生成21个类别confidence;一个输出回归用的 localization,每个 default box 生成4个坐标值(x, y, w, h)。此外,这5个feature map还经过 PriorBox 层生成 prior box(生成的是坐标)。上述5个feature map中每一层的default box的数量是给定的(8732个)。最后将前面三个计算结果分别合并然后传给loss层。

    多尺度特征图(Mult-scale Feature Map For Detection)

    在图像Base Network基础上,将Fc6,Fc7变为了Conv6,Conv7两个卷积层,添加了一些卷积层(Conv8,Conv9,Conv10,Conv11),这些层的大小逐渐减小,可以进行多尺度预测。

     卷积预测器(Convolutional Predictors For Detection)

         每个新添加的卷积层和之前的部分卷积层,使用一系列的卷积核进行预测。对于一个大小为m*n大小,p通道的卷积层,使用3*3的p通道卷积核作为基础预测元素进行预测,在某个位置上预测出一个值,该值可以是某一类别的得分,也可以是相对于Default Bounding Boxes的偏移量,并且在图像的每个位置都将产生一个值。

    默认框和比例(Default Boxes And Aspect Ratio)

         在特征图的每个位置预测K个Box。对于每个Box,预测C个类别得分,以及相对于Default Bounding Box的4个偏移值,这样需要(C+4)*k个预测器,在m*n的特征图上将产生(C+4)*k*m*n个预测值。这里,Default Bounding Box类似于Faster-RCNN中Anchors。
     

    Default box

         文章的核心之一是作者同时采用lower和upper的feature map做检测。如图所示,这里假定有8×8和4×4两种不同的feature map。第一个概念是feature map cell,feature map cell 是指feature map中每一个小格子,如图中分别有64和16个cell。

          另外有一个概念:default box,是指在feature map的每个小格(cell)上都有一系列固定大小的box,如下图有4个(下图中的虚线框,仔细看格子的中间有比格子还小的一个box)。假设每个feature map cell有k个default box,那么对于每个default box都需要预测c个类别score和4个offset,那么如果一个feature map的大小是m×n,也就是有m*n个feature map cell,那么这个feature map就一共有(c+4)*k * m*n 个输出。

        这些输出个数的含义:采用3×3的卷积核对该层的feature map卷积时卷积核的个数,包含两部分(实际code是分别用不同数量的3*3卷积核对该层featuremap进行卷积):数量c*k*m*n是confidence输出,表示每个default box的confidence,也就是类别的概率;数量4*k*m*n是localization输出,表示每个default box回归后的坐标

          训练中还有一个东西:prior box,是指实际中选择的default box(每一个feature map cell 不是k个default box都取)。也就是说default box是一种概念,prior box则是实际的选取。训练中一张完整的图片送进网络获得各个feature map,对于正样本训练来说,需要先将prior box与ground truth box做匹配,匹配成功说明这个prior box所包含的是个目标,但离完整目标的ground truth box还有段距离,训练的目的是保证default box的分类confidence的同时将prior box尽可能回归到ground truth box。 举个列子:假设一个训练样本中有2个ground truth box,所有的feature map中获取的prior box一共有8732个。那个可能分别有10、20个prior box能分别与这2个ground truth box匹配上。训练的损失包含定位损失和回归损失两部分。

    在上图中,在8*8的feature map只能检测出猫,不能检测出狗。因为在8*8 的default box尺寸中没有狗的尺寸。

    (a)SSD在训练期间仅需要每个对象的输入图像和地面实况框。以卷积方式,我们在几个具有不同形状尺寸的特征图(例如,8×8 和4×4 (b)和(c))中的每个位置处评估一组不同宽高比的小集(例如4个)。对于每个默认框,我们预测形状偏移和所有目标类别的置信度(c_{1},c_{2},...,c_{p})。在训练时,我们首先将这些默认框与地面实况框匹配。例如,我们将两个默认框一个与cat匹配,一个与dog匹配,它们被视为正例,其余的为负例(即与地面实况框匹配上的为正例,否则为负例)。模型损失是定位损失(例如smooth L1)和置信度损失(例如Softmax)之间的加权和。

    注:同一层的所有特征图共享一组默认框。

    PriorBox

          SSD网络中的PriorBox层完成的是给定一系列feature map后如何在上面生成prior box。部署特征图中每个位置(像素点)处的默认框(即计算每个默认框相对于网络输入层输入图像的归一化左上角和右下角坐标以及设置的坐标variance值

          对于输入大小是W×H的feature map,生成的prior box中心就是W×H个,均匀分布在整张图上,在每个中心上,可以生成多个不同长宽比的prior box,如[1/3, 1/2, 1, 2, 3]。所以在一个feature map上可以生成的prior box总数是W×H×length_of_aspect_ratio,对于比较大的feature map,如VGG的conv4_3,生成的prior box可以达到数千个。虽然prior box的位置是在W×H的格子上,但prior box的大小并不是跟格子一样大,而是人工指定的,原论文中随着feature map从底层到高层,prior box的大小在0.2到0.9之间均匀变化。 

         default box 和Faster RCNN中的 anchor 很像。 default box的scale(大小)和aspect ratio(横纵比)要怎么定呢?

          以feature map上每个点的中点为中心(offset=0.5),生成一些列同心的prior box(然后中心点的坐标会乘以step,相当于从feature map位置映射回原图位置)
    正方形prior box最小边长为min_size,最大边长为:
    每在prototxt设置一个aspect ratio,会生成2个长方形,长宽为 和

                       

          假设我们想要使用m个特征图进行预测。而每个特征图feature map对应de 默认框prior box的尺寸min_size和max_size由以下公式决定:

                                s_{k}=s_{min}+\frac{s_{max}-s_{min}}{m-1}(k-1), k\in [1,m]   

        其中,s_{min} 取为0.2,s_{max} 取为0.9,表示最低层的尺度为0.2,最高层的尺度为0.9,其间的所有层都是规则间隔的。我们对默认框施加不同的宽高比,记为a_{r}\in\left \{ 1,2,3,\frac{1}{2},\frac{1}{3} \right \},由此能够计算每一个默认框的宽度(w_{k}^{a}=s_{k}\sqrt{a_{r}})和高度(h_{k}^{a}=s_{k}/\sqrt{a_{r}})。对于宽高比为1时,我们还添加了一个默认框,其尺寸为 ,由此在每个特征图位置处产生6个默认框。我们设置每个默认框的中心为(\frac{i+0.5}{\left | f_{k} \right |},\frac{j+0.5}{\left | f_{k} \right |}) ,其中 \left | f_{k} \right | 表示第k个方形特征图的大小(即特征图的长/宽)。

        可以看出这种default box在不同的feature层有不同的scale,在同一个feature层又有不同的aspect ratio,因此基本上可以覆盖输入图像中的各种形状和大小的object!通过结合来自多个特征图的所有位置的具有不同尺寸和宽高比的所有默认框的预测,我们具有多种预测,涵盖各种输入目标尺寸和形状。

    • SSD 300(conv4_3、fc7、conv6_2、conv7_2、conv8_2、conv9_2都生成feature map )
    • SSD使用低层feature map检测小目标,使用高层feature map检测大目标

    具体到每一个feature map上获得prior box时,会从这6种中进行选择。如下表和图所示最后会得到(38*38*4 + 19*19*6 + 10*10*6 + 5*5*6 + 3*3*4 + 1*1*4)= 8732个prior box。

    caffe中设置的参数

    layer {
      name: "fc7_mbox_priorbox"
      type: "PriorBox"
      bottom: "fc7"
      bottom: "data"
      top: "fc7_mbox_priorbox"
      prior_box_param {
        min_size: 60.0
        max_size: 111.0
        aspect_ratio: 2
        aspect_ratio: 3
        flip: true
        clip: false
        variance: 0.1  #4个variance,这实际上是一种bounding regression中的权重
        variance: 0.1
        variance: 0.2
        variance: 0.2
        step: 16
        offset: 0.5
      }

        step参数本质上是该层的特征图相对于网络输入层输入图像的下采样率,用于计算当前特征图上某一位置处所有默认框中心坐标在网络输入层输入图像坐标系下的坐标。举个例子,就是你在缩放一幅图像时,对于缩放后的图像,其上每一像素点的像素值是通过插值得到的,那如何有效插值就需要用到后向计算,即需要找出当前像素点坐标值对应于原始图像上的坐标值,由原始图像上这一坐标值来最邻近或双线性插值,重点就在于需要找出原始图像上的坐标值。而这里的step就是用来计算特征图上某一位置处所有默认框中心坐标在网络输入层输入图像坐标系下的坐标,对应的就是下采样率,拿conv4_3而言,下采样率为38/300约为8(实际上就是8,因为经过三次最大池化操作,但由于caffe中的池化采用向上取整,导致约等于8),故step=8。

    网络结构

                      

      SSD的结构在VGG16网络的基础上进行修改,训练时同样为conv1_1,conv1_2,conv2_1,conv2_2,conv3_1,conv3_2,conv3_3,conv4_1,conv4_2,conv4_3(38*38*512),conv5_1,conv5_2,conv5_3;

    fc6经过3*3*1024的卷积(VGG16中的fc6是全连接层,这里变成卷积层,fc7层同理)fc7经过1*1*1024的卷积.

    然后一方面:针对conv4_3(4),fc7(6),conv6_2(6),conv7_2(6),conv8_2(4),conv9_2(4)(括号里数字是选取的default box种类)中的每一个featuremap再分别采用两个3*3大小的卷积核进行卷积,这两个卷积核是并列的.

        上图中SSD的数字8732表示所有prior box的数量,是这么来的38*38*4+19*19*6+10*10*6+5*5*6+3*3*4+1*1*4=8732,这两个3*3的卷积核一个是用来做localization的(回归用,如果prior box是6个,那么就有6*4=24个这样的卷积核,卷积后map的大小和卷积前一样,因为pad=1),另一个是用来做confidence的(分类用,如果prior box是6个,VOC的object类别有20个,那么就有6*(20+1)=126个这样的卷积核)。

    一开始看SSD的时候很困扰我的一点就是形状的匹配问题:SSD用卷积层做bbox的拟合,输出的不应该是feature map吗,怎么能正好输出4个坐标呢?这里的做法有点暴力,比如需要输出W×H×length_of_aspect_ratio×4个坐标,就直接用length_of_aspect_ratio×4个channel的卷积层做拟合,这样就得到length_of_aspect_ratio×4个大小为W×H的feature map,然后把feature map拉成一个长度为W×H×length_of_aspect_ratio×4的向量,用SmoothL1之类的loss去拟合,效果还意外地不错……

                                                               Fig.5 SSD 流程

        这里的permute层相当于交换caffe blob中的数据维度。在正常情况下caffe blob的顺序为:

    bottom blob = [batch_num, channel, height, width]经过conv4_3_norm_mbox_conf_perm后的caffe blob为:

    top blob = [batch_num, height, width, channel]

    比如你卷积后的维度是32×24×19×19,那么经过交换层后就变成32×19×19×24,顺序变了而已。而flatten层的作用就是将32×19×19×24变成32*8664,32是batchsize的大小。

    从图可以看到,在conv4_3 feature map网络pipeline分为了3条线路:

    • 对于conv4_3 feature map,conv4_3_norm_priorbox(priorbox层)设置了每个点共有4个prior box。由于SSD 300共有21个分类,所以conv4_3_norm_mbox_conf的channel值为num_priorbox * num_class = 4 * 21 = 84;而每个prior box都要回归出4个位置变换量,所以conv4_3_norm_mbox_loc的caffe blob channel值为4 * 4 = 16。
    • fc7每个点有6个prior box。
    • 经过一系列caffe blob shape变化后,最后拼接成mbox_conf和mbox_loc。而mbox_conf后接reshape,再进行softmax。
    • 最后这些值输出detection_out_layer,获得检测结果。

             

                                                    

         如上图,我们是将6个分支合并成一个向量:,然后对该向量进行变形成21*N的矩阵,(N=4w1h1+6w2h2+...)这个矩阵是我们的参数矩阵W,乘以特征图x,得到输出T*1的向量logits,然后求softmax函数。

    T类  N表示前一层特征层flatten后的数字   fltten后的特征      无限大小的T类                    从0-1的T类向量              

         

    训练:

    选择用于检测的default box和尺度集合、难样本挖掘、数据增广策略。

    • 匹配策略: 训练阶段建立真实标签和默认框对应关系,真实标签从默认框中选择。开始先匹配每个真实标签框与默认框最好的jaccard重叠,确保每个真实标签框有一个匹配的默认框,然后匹配与真实框jaccard重叠高于阈值0.5的默认框。这样就允许网络对多个重叠的默认框预测获得高置信度,而不是只选择最大重叠的一个。

    Matching strategy:

        上面得到的8732个目标框经过Jaccard Overlap筛选剩下几个了;其中不满足的框标记为负数,其余留下的标为正数框。紧随其后:

                            

    在训练时,groundtruth boxes 与 default boxes(就是prior boxes) 按照如下方式进行配对:

    • 首先,寻找与每一个ground truth box有最大的jaccard overlap的default box,这样就能保证每一个groundtruth box与唯一的一个default box对应起来(所谓的jaccard overlap就是IoU,如下图)。
    • SSD之后将剩余还没有配对的default box与任意一个groundtruth box尝试配对,只要两者之间的jaccard overlap大于阈值,就认为match(SSD 300 阈值为0.5)。
    • 显然配对到GT的default box就是 候选正样本集,没有配对到GT的default box就是候选负样本集

                          

    负样本获得(这是一个难例挖掘的过程)

         在目标检测中我们会事先标记好ground_truth,接下来在图片中随机提取一系列sample,与ground_truth重叠率IoU超过一定阈值的(比如0.5),则认为它是positive sample,否则为negative sample,考虑到实际负样本数>>正样本数,我们为了避免network的预测值少数服从多数而向负样本靠拢,取正样本数:负样本数大约为1:3,显而易见,用来训练网络的负样本为提取的负样本的子集,那么,我们当然选择负样本中容易被分错类的困难负样本来进行网络训练

       困难负样本是指容易被网络预测为正样本的proposal,即假阳性(false positive),如roi里有二分之一个目标时,虽然它仍是负样本,却容易被判断为正样本,这块roi即为hard negative,训练hard negative对提升网络的分类性能具有极大帮助,因为它相当于一个错题集。

        如何判断它为困难负样本呢?也很简单,我们先用初始样本集(即第一帧随机选择的正负样本)去训练网络,再用训练好的网络去预测负样本集中剩余的负样本,选择其中得分最高,即最容易被判断为正样本的负样本为困难样本,加入负样本集中,重新训练网络,循环往复,然后我们会发现:我们的网络的分类性能越来越强了。

     

        在生成 prior boxes 之后,会产生很多个符合 ground truth box 的 positive boxes(候选正样本集),但同时,不符合 ground truth boxes 也很多,而且这个 negative boxes(候选负样本集),远多于 positive boxes。这会造成 negative boxes、positive boxes 之间的不均衡。训练时难以收敛。因此对default boxes以confidence loss由高到低排序,取最高的一些值,把将正负样本控制在3:1的范围。

    将每一个GT上对应prior boxes的分类loss进行排序:

        对于候选正样本集:选择loss最高的m个 prior box 与候选正样本集匹配 (box 索引同时存在于这两个集合里则匹配成功),匹配不成功则从候选正样本集中删除这个正样本(因为这个正样本loss太低,易被识别,已经很接近 ground truth box 了,不需要再训练);

       对于候选负样本集:选择最高的m个 prior box 与候选负样本集匹配,匹配成功的则留下来作为最后的负样本,不成功剔除出这个候选负样本,因为他们loss不够大,易被识别为背景,训练起来没难度没提升空间。

         举例:假设在这 8732 个 default box 里,经过 FindMatches 后得到候选正样本 P 个,候选负样本那就有 8732−P个。将 prior box 的 prediction loss 按照从大到小顺序排列后选择最高的 M个 prior box。如果这 P 个候选正样本里有 a 个 box 不在这 M 个 prior box 里(说明这a个box误差小,与真实框很接近),将这 a个 box 从候选正样本集中踢出去。如果这 8732−P个候选负样本集中有 b个在这 M 个 prior box(说明这b个box误差大,与真实框很远),则将这b个候选负样本作为正式负样本。即删除易识别的正样本,同时留下典型的负样本,组成1:3的prior boxes样本集合。SSD 算法中通过这种方式来保证 positives、negatives 的比例。

    原文:https://blog.csdn.net/Gentleman_Qin/article/details/84403313 

    数据扩容:

    • 使用原始图像
    • 采样一个patch,与目标间最小的jaccard overlap为:0.1,0.3,0.5,0.7,0.9.
    • 随机采样一个patch.每个采样的patch大小为原始图像的[0.1,1],aspect ratio在1/2和2之间.当ground truth box的中心在patch中时,保留重叠部分.每个采样的patch被resize到固定大小,并以0.5的概率随机水平翻转。

    SSD的损失函数:

    SSD的损失函数如图3所示,由每个默认框的定位损失与分类损失构成。

                 

       N是match到GT(Ground Truth)的prior box数量;而α参数用于调整confidence loss和location loss之间的比例,默认α=1。其中代表第i个prior box匹配到了第j个class为p类别的GT box。SSD中的confidence loss是典型的softmax loss。

    而location loss是典型的smooth L1 loss:

                            

    d表示真实框的参数,g表示预测框的参数,\hat{g}表示预测的偏移量。

    from:https://blog.csdn.net/a8039974/article/details/77592395

     from:https://blog.csdn.net/qq_36269513/article/details/81782722

    from:https://blog.csdn.net/m0_37192554/article/details/81986346

    展开全文
  • SSD原理解读-从入门到精通

    万次阅读 多人点赞 2018-08-27 08:00:47
    SSD原理解读-从入门到精通

    前言

    当初写这篇博客的初衷只是记录自己学习SSD的一些心得体会,纯属学习笔记,后来由于工作上的需要,需要对小伙伴进行目标检测方面的培训,后来就基于这篇博客进行了扩展,逐渐演变成了现在的样子,本文力求从一个初学者的角度去讲述目标检测和SSD(但是需要你具备CNN的基础),尽量使用通俗的语言并结合图表的方式让更多初学者更容易理解SSD这个算法,但是一个人的时间精力有限,不可能深入理解SSD的每一个细节,加上表达能力也有限,自己理解了的东西不一定在文中能够说明白,文中有什么不妥的地方,欢迎大家批评指正,也欢迎留言一起交流讨论。



    目标检测基础

    在实际的工作中发现,一些小伙伴在学习检测的时候对一些基础的东西理解的还不够,这里总结了比较重要的几点。

    传统目标检测的基本原理

    为什么要提传统目标检测呢?因为理解传统目标检测对于理解基于深度学习的目标检测非常重要,因为学到最后你会发现,两者的本质都是一样的,都是对滑动窗口的分类。下面我们看一下传统目标检测的基本原理。
    在这里插入图片描述
    主要分为两个步骤:训练+预测,其中训练主要是用来得到分类器,比如SVM,预测就是使用训练好的分类器对图像中的滑动窗口进行特征提取然后分类,最后得到检测的结果。下面以人脸检测为例:
    在这里插入图片描述
    假设我们需要训练一个人脸检测器,那第一步就是训练一个人脸的分类器,这个分类器有什么作用呢?它的作用就是将上图左边的很多图像划分为两类:人脸和非人脸。分类器训练好了之后,就可以进行检测了。
    在这里插入图片描述
    预测阶段有两种滑动窗口策略:

    1. 策略1:使用不同大小的滑动窗口,对每个滑动窗口提取特征并分类判断是否是人脸,最后经过NMS得到最后的检测结果,本文的SSD本质上就是这种策略,不同检测层的anchor就类似于不同大小的滑动窗口
    2. 策略2:构造图像金字塔,只使用一种大小的滑动窗口在所有金字塔图像上滑动,对每个滑动窗口提取特征并分类判断是否是人脸,最后经过NMS得到最后的检测结果,MTCNN就是采用了这种策略

    传统目标检测的代表主要有:

    1. HOG+SVM的行人检测
    2. Haar+Adaboost的人脸检测

    为什么要使用全卷积神经网络做检测

    我们知道经典的深度学习目标检测算法的代表SSD,YOLOV3和FasterRCNN的RPN都使用了全卷积神经网络,那为什么要使用全卷积呢?

    分类网络

    下面是一个典型的分类网络结构:
    在这里插入图片描述
    图片来自经典的人脸识别论文DeepID2:https://arxiv.org/abs/1406.4773

    我们知道典型的分类网络,比如VGG,ResNet等最后都会使用全连接层提取特征,然后经过softmax计算每一类的概率。

    典型的分类网络有如下特点:

    1. Softmax的前一层为全连接层,且输出节点数为分类的类别数,比如imagenet有1000类,则最后的全连接层有1000个输出节点,分别对应1000类的概率,VGG16中的fc8对应了这一层,上图有10000分类,所以fc2有10000个输出节点
    2. Softmax的倒数第二层也为全连接层,这一层一般是提取特征用的,比如VGG16中的fc7层就是用来提取特征的,上图中DeepID2层也是用来进行提取特征的
    3. 由于使用全连接层提取特征,所以提取的是全图的特征,所以一张图像中只能包含一个目标,如果有多个目标,提取出来的特征就不准确,影响最后的预测
    4. 由于全连接层的存在,网络的输入大小必须是固定的

    那如何才能提取多个目标的特征呢?

    使用卷积层代替全连接层进行特征提取

    在这里插入图片描述

    我们将图1简化为图2的表示形式,其中图2中省略了Input到DeepID2中间的层,我们看到当DeepID2是全连接层的时候,感受野对应了全图,所以提取的是全图的特征,现在我们把DeepID2替换为卷积层,其中卷积层的输出通道数为160,这是为了能够提取160维的特征向量,图3中我们可以看到当使用卷积层的时候,DeepID2的输出特征图的每个位置的感受野不是对应了全图,而是对应了原图的某一个滑动窗口,这样DeepID2这一层就可以提取原图多个滑动窗口的特征了,图3中我们可以看到一共提取出了25个滑动窗口的160维特征向量,然后我们就可以对这25个滑动窗口做分类了。这样就使得检测成为了可能。

    使用卷积层代替全连接层进行分类

    我们知道在分类网络中softmax的前一层使用的是全连接层,且该全连接层的输出节点数为分类数,比如上文中的DeepID2的后面的fc2层。
    在这里插入图片描述
    但是现在DeepID2这一层变成了卷积层之后,fc2层就不适合采用全连接层了。下面我们将fc2层替换为卷积层,我们采用1x1卷积(也可以使用3x3卷积),同时卷积层的输出通道数为10000,对应了10000分类。
    在这里插入图片描述
    我们看到当fc2采用1x1卷积的时候,fc2层的特征图中的每个位置对应了一个滑动窗口的10000分类,这样一共得到25x10000的特征向量,表示对25个滑动窗口的10000分类。最后将这25x10000的特征向量输入softmax,就可以实现对这25个滑动窗口的分类了。

    这其实就是经典的OverFeat的核心思想。

    注意:这里并没有说检测网络不能使用全连接层,其实检测网络也可以使用全连接层。检测网络只是使用卷积层代替全连接层提取特征,最后对特征进行分类和回归可以使用卷积层也可以使用全连接层,YOLOV1最后就使用了全连接层对特征进行分类和回归,只是这样会有一些缺点:网络的输入大小必须是固定的,而且最后检测的效果往往没有使用卷积层做分类和回归的效果好。

    确定每个滑动窗口的类别

    上文中,我们将传统的分类网络一步一步的修改成了一个检测网络,知道了如何提取多个目标的特征以及使用卷积层代替全连接层进行分类,现在我们还差最后一步:要对多个目标进行分类,还需要知道他们的groundtruth,也就是他们的类别,那么如何确定类别呢?
    这里就要用到anchor这项技术。

    anchor就是用来确定类别的。

    在这里插入图片描述
    我们知道anchor的参数是可以手动设置的,上图中
    anchor的大小被设置为 [x1,y1,x2,y2],这个anchor就是上文中提到的滑动窗口
    groundtruth对应 [x1’,y1’,x2’,y2’]
    然后通过计算anchor和groundtruth之间的IOU就可以确定这个滑动窗口的类别了(比如IOU>0.5的为正样本,IOU<0.3的为负样本)。关于anchor在下文中会有详细讨论。

    到这里,SSD的整体框架已经基本搭建好了。其实SSD的多个检测层就等价于多个DeepID2层,不同检测层有不同大小的滑动窗口,能够检测到不同大小的目标。每个检测层后面会接2路3x3卷积用来做分类和回归,对应了fc2层。
    在这里插入图片描述
    理解了上面的内容之后,其实就很容易理解SSD了。


    SSD效果为什么这么好

    虽然SSD这个算法出来已经两年了,但是至今依旧是目标检测中应用最广泛的算法,虽然后面有很多基于SSD改进的算法,但依旧没有哪一种可以完全超越SSD。那么为什么SSD效果这么好?SSD效果好主要有三点原因:

    1. 多尺度
    2. 设置了多种宽高比的anchor
    3. 数据增强

    注:

    1. anchor,default box,prior box表示的是同一个意思,本文统一使用更加常用的anchor来表示。实际上,anchor技术的鼻祖是DeepMultiBox(2014年CVPR:Scalable Object Detection using Deep Neural Networks),这篇论文里首次提出使用prior(先验框),并且提出使用prior做匹配。后面的众多工作,比如RCNN系列,YOLO系列都使用了anchor这个术语,而在SSD中anchor又叫default box,本质上都是表示的同一个东西。

    原因1:多尺度

    这里写图片描述

    由SSD的网络结构可以看出,SSD使用6个不同特征图检测不同尺度的目标。低层预测小目标,高层预测大目标。

    通过前面的学习,其实就很容易理解SSD中的多尺度检测,这6个检测层都是卷积层,对应了上文中的6个DeepID2层,每个DeepID2层对应了不同大小的滑动窗口(低层滑动窗口较小,高层滑动窗口较大),这样就可以检测到不同尺度的目标了。
    这里写图片描述

    作者在论文中通过实验验证了,采用多个特征图做检测能够大大提高检测精度,从上面的表格可以看出,采用6个特征图检测的时候,mAP为74.3%,如果只采用conv7做检测,mAP只有62.4%。

    原因2:设置了多种宽高比的anchor

    这里写图片描述

    在特征图的每个像素点处,生成不同宽高比的anchor,论文中设置的宽高比为{1,2,3,1/2,1/3}。假设每个像素点有k个anchor,需要对每个anchor进行分类和回归,其中用于分类的卷积核个数为ck(c表示类别数),回归的卷积核个数为4k。

    SSD300中anchor的数量:(38384 + 19196 + 10106 + 556 + 334 + 114)= 8732

    对于初学者一定有以下几个问题:

    1. 为什么要设置anchor?
    2. 为什么同一个检测层可以设置不同大小的anchor?
    3. 为什么一个检测层可以设置多个anchor?
    4. 为什么要设置多种宽高比的anchor?

    理论感受野和有效感受野

    NIPS 2016论文Understanding the Effective Receptive Field in Deep Convolutional Neural Networks[1]提出了有效感受野(Effective Receptive Field, ERF)理论。有效感受野的理解对于理解anchor非常重要。

    影响某个神经元输出的输入区域就是理论感受野,也就是我们平时说的感受野,但该输入区域的每个像素点对输出的重要性不同,越靠近中心的像素点影响越大,呈高斯分布,也就是说只有中间的一小部分区域对最后的输出有重要的影响,这个中间的一小部分区域就是有效感受野

    有效感受野在训练过程中是会发生变化的,影响有效感受野的因素:

    1. 数据集
    2. 层的类型(下采样,扩张卷积,跳层连接,非线性激活函数)
    3. 卷积层参数初始化方式(Uniform(参数全部设置为1), Random)
    4. 卷积层的个数

    下图展示了不同因素对有效感受野的影响
    卷积层层数,权值初始化方式以及非线性激活对ERF的影响
    卷积层层数,权值初始化方式以及非线性激活对ERF的影响

    在这里插入图片描述
    下采样和扩张卷积可以增大感受野

    在这里插入图片描述
    不同数据集对感受野的影响

    为什么要设置anchor?

    通过前面的学习,我们知道,在分类/识别问题中,通常整张图就包含一个目标,所以只需要对一个目标直接分类就好了,所以最后直接使用全连接层提取整幅图像的特征就可以了(全连接层理论感受野大小就是输入大小)。但是检测问题中,输入图像会包含有很多个目标,所以需要对多个目标进行特征提取,这个时候就不能使用全连接层了,只能使用卷积层,卷积层的输出特征图上每个位置对应原图的一个理论感受野,该位置提取的就是这个理论感受野区域的特征,然后我们只需要对这个特征进行分类和回归就可以实现检测了。在实际训练训练过程中,要想对这个特征进行分类,我们就需要知道这个特征提取的是什么目标的特征,所以我们需要知道两个东西:

    1. 这个特征对应了原图什么区域?
    2. 这个区域的label是什么?
      但是在实际训练过程中,我们并不知道这个理论感受野的大小,这个时候就出现了anchor技术。

    anchor作用:通过anchor设置每一层实际响应的区域,使得某一层对特定大小的目标响应。这样检测层提取的就是anchor对应区域的特征了。通过anchor我们就可以知道上面两个问题的答案了。

    论文中也提到:
    Feature maps from different levels within a network are known to have different (empirical) receptive field sizes. Fortunately,
    within the SSD framework, the default boxes do not necessary need to correspond to the actual receptive fields of each layer. We design the tiling of default boxes so that specific feature maps learn to be responsive to particular scales of the objects.
    通过设计anchor的平铺可以使得特定的特征图对特定大小的目标进行响应。

    下图可以更加形象的表示anchor的作用
    在这里插入图片描述
    图a中黑色虚线区域对应图b中的理论感受野,红色实线框是手动设置的anchor大小
    图b是使用图a中的anchor参数训练出来的模型,其中整个黑色区域就是理论感受野(TRF),对应a图的黑色虚线区域,中间呈高斯分布的白色点云区域就是由于设置了anchor而实际发生响应的区域,这个区域就是有效感受野(ERF),我们用anchor等价表示这个区域。
    我们可以看到通过在图a中设置anchor可以设置该层实际响应的区域,使得对特定大小的目标响应。

    这里还有几个问题需要说明一下。

    1. 为什么anchor可以设置实际响应的区域?
      这就是CNN的神奇之处,这个问题目前我还不知道如何解释,就像目前的深度学习依然是不可解释的一样,这点就当作一个结论记住就可以了。

    2. 为什么同一个检测层可以设置不同大小的anchor
      我们知道可以通过anchor设置每一层实际响应的区域,使得某一层对特定大小的目标响应。
      是否每一层只能对理论感受野响应呢?有效感受野理论表明,每一层实际响应的区域其实是有效感受野区域,而且这个有效感受野区域在训练过程中会发生变化(比如不同数据集的影响等),正是由于有效感受野有这个特性,所以我们可以在同一个检测层设置不同大小的anchor,也就是说你既可以设置anchor大小为理论感受野大小,也可以将anchor大小设置为其他大小,最后训练出来的网络会根据你的设置对特定大小的区域响应。

    3. 为什么在同一个特征图上可以设置多个anchor检测到不同尺度的目标
      刚开始学SSD的朋友一定有这样的疑惑,同一层的感受野是一样的,为什么在同一层可以设置多个anchor,然后在分类和回归两个分支上只需要使用不同通道的3x3卷积核就可以实现对不同anchor的检测?虽然分类和回归使用的是同一个特征图,但是不同通道的3x3卷积核会学习到那块区域的不同的特征,所以不同通道对应的anchor可以检测到不同尺度的目标。

    4. anchor本身不参与网络的实际训练,anchor影响的是classification和regression分支如何进行encode box(训练阶段)和decode box(测试阶段)。测试的时候,anchor就像滑动窗口一样,在图像中滑动,对每个anchor做分类和回归得到最终的结果。

    5. 关于anchor的进一步探讨,见另一篇博客:深入理解anchor,欢迎大家畅所欲言

    anchor与滑动窗口

    上文中提到了两个概念:滑动窗口和anchor,每个检测层对应了不同大小的滑动窗口,也对应着不同大小的anchor,那这两个概念有什么区别呢?

    这里我们不严格区分这两个概念,我们可以认为这两个表示的是同一个东西。

    这里有一点要注意:由于anchor是可以手动设置的,所以某一个检测层的滑动窗口大小是会随着anchor的大小发生变化的,这就是anchor的神奇之处!

    anchor的匹配

    前面我们知道了SSD提取的是anchor对应区域的特征,实际训练的时候还需要知道每个anchor的分类和回归的label,如何确定呢?SSD通过anchor与groundtruth匹配来确定label。
    在训练阶段,SSD会先寻找与每个anchor的IOU最大的那个ground truth(大于IOU阈值0.5),这个过程叫做匹配。如果一个anchor找到了匹配的ground truth,则该anchor就是正样本,该anchor的类别就是该ground truth的类别,如果没有找到,该anchor就是负样本。图1(b)中8x8特征图中的两个蓝色的anchor匹配到了猫,该anchor的类别为猫,图1©中4x4特征图中的一个红色的anchor匹配到了狗,该anchor的类别为狗。图2显示了实际的匹配过程,两个红色的anchor分别匹配到了猫和狗,左上角的anchor没有匹配,即为负样本。

    这里写图片描述
    图1

    在这里插入图片描述
    图 2(图中红色框表示anchor,绿色框表示groundtruth)

    关于匹配更多的细节,参考Caffe源码multibox_loss_layer.cpp中的FindMatches()函数,前面的博客:SSD源码解读3-MultiBoxLossLayer中也讲到了该函数。

    为什么要设置多种宽高比的anchor?

    由于现实中的目标会有各种宽高比(比如行人),设置多个宽高比可以检测到不同宽高比的目标。
    这里写图片描述

    作者实验结果表明,增加宽高比为1/2,2,1/3,3的default box,mAP从71.6%提高到了74.3%。

    如何选择anchor的scale和aspect ratio?

    假设我们用m个feature maps做预测,那么对于每个featuer map而言其anchor的scale是按以下公式计算的。
    Sk=Smin+SmaxSminm1(k1) S_k=S_{min}+{{S_{max}-S_{min}} \over {m-1}}(k-1)
    这里SminS_{min}是0.2,表示最低层的scale是0.2,SmaxS_{max}是0.9,表示最高层的scale是0.9。宽高比αr=1,2,3,1/2,1/3{\alpha}_r={1,2,3,1/2,1/3},因此每个anchor的宽wkα=Skαrw^{\alpha}_{k}=S_k \sqrt{{\alpha}_r},高hkα=Sk/αrh^{\alpha}_{k}={S_k / \sqrt{{\alpha}_r}},当aspect ratio为1时,作者还增加一种scale的anchor:Sk=SkSk+1S^{'}_k= \sqrt{S_kS_k+1},因此,对于每个feature map cell而言,一共有6种anchor。

    示例:
    假设m=6,即使用6个特征图做预测, 则每一层的scale:0.2,0.34,0.48,0.62,0.76,0.9
    对于第一层,scale=0.2,对应的6个anchor为:

    宽高比
    1 0.200000 0.200000
    2 0.282843 0.141421
    3 0.346410 0.115470
    1/2 0.141421 0.282843
    1/3 0.115412 0.346583
    最后增加的default box 0.260768 0.260768

    注:表格中每个宽高比的anchor的实际宽和高需要乘以输入图像的大小,如SSD300,则需要使用上面的数值乘以300得到anchor实际大小。

    Caffe源码中anchor的宽高比以及scale的设置参考prior_box_layer.cpp,前面的博客:SSD源码解读2-PriorBoxLayer也对该层进行过解读。

    原因3:数据增强

    SSD中使用了两种数据增强的方式
    1. 放大操作: 随机crop,patch与任意一个目标的IOU为0.1,0.3,0.5,0.7,0.9,每个patch的大小为原图大小的[0.1,1],宽高比在1/2到2之间。能够生成更多的尺度较大的目标
    2. 缩小操作: 首先创建16倍原图大小的画布,然后将原图放置其中,然后随机crop,能够生成更多尺度较小的目标

    这里写图片描述

    这里写图片描述

    作者实验表明,增加了数据增强后,mAP从65.5提高到了74.3!

    数据增强是SSD中最大的trick,已经成为了后面众多检测算法的标配了。

    数据增强对应Caffe源码annotated_data_layer.cpp,前面的博客:SSD源码解读1-数据层AnnotatedDataLayer也对该层进行过解读。


    SSD的缺点及改进

    1. SSD主要缺点:SSD对小目标的检测效果一般,作者认为小目标在高层没有足够的信息。

    论文原文:
    This is not surprising because those small objects may not even have any information at the very top layers. Increasing the input size (e.g. from 300× 300 to 512× 512) can help improve detecting small objects, but there is still a lot of room to improve.

    对小目标检测的改进可以从下面几个方面考虑:
    1. 增大输入尺寸
    2. 使用更低的特征图做检测(比如S3FD中使用更低的conv3_3检测)
    3. FPN(已经是检测网络的标配了)

    2. 关于anchor的设置的优化

    An alternative way of improving SSD is to design a better tiling of default boxes so that its position and scale are better aligned with the receptive field of each position on a feature map. We leave this for future work. P12
    论文中提到的anchor设置没有对齐感受野,通常几个像素的中心位置偏移,对大目标来说IOU变化不会很大,但对小目标IOU变化剧烈,尤其感受野不够大的时候,anchor很可能偏移出感受野区域,影响性能。
    关于anchor的设计,作者还提到了
    In practice, one can also design a distribution of default boxes to best fit a specific dataset. How to design the optimal tiling is an open question as well
    论文提到根据特定数据集设计default box,在YOLOV2中使用聚类的方式初始化anchor,能够更好的匹配到ground truth,帮助网络更好的训练


    SSD中的Mining机制

    在视觉任务中经常遇到两个问题:
    1. 类别不均衡
    2. 简单样本和困难样本不均衡 (easy sample overwhelming)。easy sample如果太多,可能会将有效梯度稀释掉。

    为了解决上述问题,研究人员提出了一些解决方案:
    1. Online Hard Example Mining, OHEM(2016)。将所有sample根据当前loss排序,选出loss最大的N个,其余的抛弃。这个方法就只处理了easy sample的问题。
    2. Focal Loss(2017), 最近提出来的。不会像OHEM那样抛弃一部分样本,focal loss考虑了每个样本, 不同的是难易样本上的loss权重是根据样本难度计算出来的。

    SSD中采用了一种新的Mining机制,OHNM(Online Hard Negative Mining),在Focal Loss里代号为OHEM 1:3,是对OHEM的一种改进。OHNM在计算loss时, 使用所有的positive anchor, 使用OHEM选择3倍于positive anchor的negative anchor。同时考虑了类间平衡与easy sample。通过OHNM让训练更快收敛同时也更加稳定。

    注意,SSD中mining具体实现的时候,MultiBoxLoss层的 mining_type可以选择MAX_NEGATIVE或者HARD_EXAMPL

    1. MAX_NEGATIVE对应OHNM, 只计算分类loss,不计算定位loss,只针对负样本选择loss最大的3倍于正样本数量的负样本
    2. HARD_EXAMPL对应OHEM ,会同时计算分类和定位loss,选择出loss最大的前topN个样本
      具体实现参考MineHardExamples()函数。

    SSD与MTCNN

    这里为什么会提到MTCNN[2]呢?如果你了解过MTCNN这个算法,一定对PNet这个网络不陌生,仔细比较SSD与PNet,你就会发现SSD与PNet之间有着千丝万缕的联系。

    这里写图片描述
    其实我对SSD的理解就是源于MTCNN中的PNet,实际上SSD可以看成是由6个不同的PNet组合而成。

    这里用原论文中的SSD300的结构与MTCNN作比较
    这里写图片描述

    这里写图片描述

    SSD与MTCNN的不同

    1. 生成训练数据的方式不同
      MTCNN需要事先手动将所有的训练样本生成好,然后输入到网络中训练, 而SSD不需要,SSD在训练过程中自动生成所有训练样本。SSD中实际的训练样本就是所有anchor,每个anchor的label由anchor与ground truth匹配来确定。SSD实现了真正意义上端到端的训练。
    2. MTCNN和SSD采用了两种不同的多尺度检测策略
      MTCNN:首先构建图像金字塔,然后使用固定大小的滑动窗口在金字塔每一级滑动,对每个滑动窗口分类回归。
      SSD: 在原图中设置了不同大小的滑动窗口,对不同大小的滑动窗口进行分类和回归。
      不管是MTCNN,还是SSD,本质上是对所有滑动窗口的分类。这与传统的目标检测方法本质上是一样的。
    3. Mining的方式不同
      MTCNN需要手动做mining,而SSD采用了OHNM训练过程中自动完成负样本的mining,解决了简单样本的问题和类别不平衡问题。
    4. 其实MTCNN中也是有anchor的, MTCNN中的anchor就是PNet的滑动窗口
      MTCNN的训练样本就是滑动窗口图像,而生成训练样本的时候使用滑动窗口与groundtruth进行匹配,得到分类和回归的label,所以anchor就是PNet的滑动窗口。不过与SSD的区别在于这个匹配过程不是训练过程中自动完成的,而是事先手动完成。
      判断什么是anchor的方法:使用什么匹配groundtruth的,那个就是anchor

    SSD与YOLOV3

    YOLOV3和SSD在很多地方都非常相似,连YOLOV3的作者都说了,YOLOV3只是借鉴了其他人的一些思想(So here’s the deal with YOLOv3: We mostly took goodideas from other people.),看完YOLOV3,我甚至都觉得YOLOV3就是抄的SSD。但是不管怎么样,目前工业界用的最多的目标检测算法就是SSD和YOLO。YOLOV3和SSD都使用了anchor技术+多尺度检测+FPN。但是在诸多细节上稍有不同。

    SSD与YOLOV3的不同

    1. 训练数据格式不同:SSD的训练数据坐标顺序为label x1 y1 x2 y2,而YOLOV3为 label x y w h(x,y表示中心点)
    2. 网络结构方面,对坐标偏移和类别预测的结构不同:SSD在每个检测层使用了两个分支分别做分类和回归,但是YOLOV3在每个检测层只使用了一个分支,也就是说只用了一个tensor预测了3部分内容:坐标偏移+目标置信度+分类置信度(通道数为4+1+classNum)。
    3. 坐标偏移量的编码形式不同。
      SSD和FasterRCNN使用了相同的编码方式在这里插入图片描述
      而YOLOV3使用了如下的编码方式
      在这里插入图片描述
      SSD的坐标偏移很好理解,就是ground truth和anchor之间的偏移,但是YOLOV3中的偏移有点不同,YOLOV3的代码中,cx和cy表示的是检测层特征图每个像素点的坐标(实际计算的时候,做了归一化处理),其实也就是anchor的中心点,所以第1,2个公式表示的就是预测框和anchor中心点的偏移,第3,4个公式和SSD的是一样的
    4. 训练中anchor的匹配策略不同。SSD中,如果一个anchor匹配到groundtruth了,就是正样本,否则为负样本,一个groundtruth可以有多个anchor匹配到,但是YOLOV3中,一个groundtruth只能有一个anchor匹配到,这个anchor负责预测这个groundtruth,而且YOLOV3用了双阈值处理,YOLOV3会忽略掉一部分样本。但是我在阅读作者源码的时候发现,源码和论文有点出入,训练的时候,并不是只有最佳匹配的anchor参与训练,只要IOU大于阈值的都会参与训练,但是在YOLOV2中,只有最佳匹配的anchor会参与训练。
    5. SSD使用了单阈值匹配,YOLOV3使用了双阈值匹配。前面也提到了这一点,SSD在区分正负anchor的时候,只用了一个阈值,但是YOLOV3使用了两个阈值。
    6. loss的形式不同。
      如果还有不同点没有列出的,欢迎留言补充。

    SSD和YOLOV3在众多细节上有所不同,但是孰优孰劣,目前还没有定论。只要用的好,这两个算法的性能都是非常不错的,能够熟练掌握其中的一种就够用了。


    与传统目标检测的关系

    基于深度学习的检测算法,不管是SSD,YOLOV3,FasterRCNN还是MTCNN,本质上都是一样的,都是对滑动窗口的分类


    结束语

    博客最初写于2018-08-27, 2019-11-10和2020-6-9分别对原博客做了两次重大修改,随着对SSD一遍又一遍的学习和思考,每一次都能够发现以前有些地方理解还是有点问题的,现在越来越能体会到,为什么经典需要反复阅读反复思考,每次都会有新的理解,每次都能发现不一样的东西。

    最后希望这篇文章能够帮助到你。


    2018-8-27 08:08:36
    Last Updated: 2020-6-9 14:11:48


    参考文献

    [1] [2016 NIPS] Understanding the Effective Receptive Field in Deep Convolutional Neural Networks
    [2] [2016 ISPL] Joint Face Detection and Alignment using Multi-task Cascaded Convolutional Networks
    [3] [2014 NIPS] Deep Learning Face Representation by Joint Identification-Verification


    非常感谢您的阅读,如果您觉得这篇文章对您有帮助,欢迎扫码进行赞赏。
    这里写图片描述

    展开全文
  • 睿智的目标检测23——Pytorch搭建SSD目标检测平台

    千次阅读 多人点赞 2020-03-20 14:48:05
    睿智的目标检测23——Pytorch搭建SSD目标检测平台学习前言什么是SSD目标检测算法源码下载SSD实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练部分1、真实框...

    学习前言

    一起来看看SSD的Pytorch实现吧,顺便训练一下自己的数据。
    在这里插入图片描述

    什么是SSD目标检测算法

    SSD是一种非常优秀的one-stage目标检测方法,one-stage算法就是目标检测和分类是同时完成的,其主要思路是利用CNN提取特征后,均匀地在图片的不同位置进行密集抽样,抽样时可以采用不同尺度和长宽比,物体分类与预测框的回归同时进行,整个过程只需要一步,所以其优势是速度快
    但是均匀的密集采样的一个重要缺点是训练比较困难,这主要是因为正样本与负样本(背景)极其不均衡(参见Focal Loss),导致模型准确度稍低。
    SSD的英文全名是Single Shot MultiBox Detector,Single shot说明SSD算法属于one-stage方法,MultiBox说明SSD算法基于多框预测。

    源码下载

    https://github.com/bubbliiiing/ssd-pytorch
    喜欢的可以点个star噢。

    SSD实现思路

    一、预测部分

    1、主干网络介绍

    在这里插入图片描述
    SSD采用的主干网络是VGG网络,关于VGG的介绍大家可以看我的另外一篇博客https://blog.csdn.net/weixin_44791964/article/details/102779878,这里的VGG网络相比普通的VGG网络有一定的修改,主要修改的地方就是:
    1、将VGG16的FC6和FC7层转化为卷积层。
    2、去掉所有的Dropout层和FC8层;
    3、新增了Conv6、Conv7、Conv8、Conv9。

    在这里插入图片描述
    如图所示,输入的图片经过了改进的VGG网络(Conv1->fc7)和几个另加的卷积层(Conv6->Conv9),进行特征提取:
    a、输入一张图片后,被resize到300x300的shape

    b、conv1,经过两次[3,3]卷积网络,输出的特征层为64,输出为(300,300,64),再2X2最大池化,输出net为(150,150,64)。

    c、conv2,经过两次[3,3]卷积网络,输出的特征层为128,输出net为(150,150,128),再2X2最大池化,输出net为(75,75,128)。

    d、conv3,经过三次[3,3]卷积网络,输出的特征层为256,输出net为(75,75,256),再2X2最大池化,输出net为(38,38,256)。

    e、conv4,经过三次[3,3]卷积网络,输出的特征层为512,输出net为(38,38,512),再2X2最大池化,输出net为(19,19,512)。

    f、conv5,经过三次[3,3]卷积网络,输出的特征层为512,输出net为(19,19,512),再步长为1,卷积核大小为3X3最大池化,输出net为(19,19,512)。

    g、利用卷积代替全连接层,进行了一次[3,3]卷积网络和一次[1,1]卷积网络,输出的特征层为1024,因此输出的net为(19,19,1024)。(从这里往前都是VGG的结构)

    h、conv6,经过一次[1,1]卷积网络,调整通道数,一次步长为2的[3,3]卷积网络,输出的特征层为512,因此输出的net为(10,10,512)。

    i、conv7,经过一次[1,1]卷积网络,调整通道数,一次步长为2的[3,3]卷积网络,输出的特征层为256,因此输出的net为(5,5,256)。

    j、conv8,经过一次[1,1]卷积网络,调整通道数,一次padding为valid的[3,3]卷积网络,输出的特征层为256,因此输出的net为(3,3,256)。

    k、conv9,经过一次[1,1]卷积网络,调整通道数,一次padding为valid的[3,3]卷积网络,输出的特征层为256,因此输出的net为(1,1,256)。

    实现代码:

    base = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M',
                512, 512, 512]
    
    def vgg(i):
        layers = []
        in_channels = i
        for v in base:
            if v == 'M':
                layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
            elif v == 'C':
                layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]
            else:
                conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
                layers += [conv2d, nn.ReLU(inplace=True)]
                in_channels = v
        pool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6)
        conv7 = nn.Conv2d(1024, 1024, kernel_size=1)
        layers += [pool5, conv6,
                   nn.ReLU(inplace=True), conv7, nn.ReLU(inplace=True)]
        return layers
    
    def add_extras(i, batch_norm=False):
        # Extra layers added to VGG for feature scaling
        layers = []
        in_channels = i
    
        # Block 6
        # 19,19,1024 -> 10,10,512
        layers += [nn.Conv2d(in_channels, 256, kernel_size=1, stride=1)]
        layers += [nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1)]
    
        # Block 7
        # 10,10,512 -> 5,5,256
        layers += [nn.Conv2d(512, 128, kernel_size=1, stride=1)]
        layers += [nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1)]
    
        # Block 8
        # 5,5,256 -> 3,3,256
        layers += [nn.Conv2d(256, 128, kernel_size=1, stride=1)]
        layers += [nn.Conv2d(128, 256, kernel_size=3, stride=1)]
        
        # Block 9
        # 3,3,256 -> 1,1,256
        layers += [nn.Conv2d(256, 128, kernel_size=1, stride=1)]
        layers += [nn.Conv2d(128, 256, kernel_size=3, stride=1)]
    
        return layers
    

    2、从特征获取预测结果

    在这里插入图片描述
    由上图我们可以知道,我们分别取conv4的第三次卷积的特征、fc7的特征、conv6的第二次卷积的特征、conv7的第二次卷积的特征、conv8的第二次卷积的特征、conv9的第二次卷积的特征,为了和普通特征层区分,我们称之为有效特征层,来获取预测结果。

    对获取到的每一个有效特征层,我们分别对其进行一次num_priors x 4的卷积、一次num_priors x num_classes的卷积、并需要计算每一个有效特征层对应的先验框。而num_priors指的是该特征层所拥有的先验框数量。

    其中:
    num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。(为什么说是变化情况呢,这是因为ssd的预测结果需要结合先验框获得预测框,预测结果就是先验框的变化情况。)

    num_priors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测框对应的种类。

    每一个有效特征层对应的先验框对应着该特征层上 每一个网格点上 预先设定好的多个框。

    所有的特征层对应的预测结果的shape如下:
    在这里插入图片描述
    实现代码为:

    class SSD(nn.Module):
        def __init__(self, phase, base, extras, head, num_classes):
            super(SSD, self).__init__()
            self.phase = phase
            self.num_classes = num_classes
            self.cfg = Config
            self.vgg = nn.ModuleList(base)
            self.L2Norm = L2Norm(512, 20)
            self.extras = nn.ModuleList(extras)
            self.priorbox = PriorBox(self.cfg)
            with torch.no_grad():
                self.priors = Variable(self.priorbox.forward())
            self.loc = nn.ModuleList(head[0])
            self.conf = nn.ModuleList(head[1])
            if phase == 'test':
                self.softmax = nn.Softmax(dim=-1)
                self.detect = Detect(num_classes, 0, 200, 0.01, 0.45)
        def forward(self, x):
            sources = list()
            loc = list()
            conf = list()
    
            # 获得conv4_3的内容
            for k in range(23):
                x = self.vgg[k](x)
    
            s = self.L2Norm(x)
            sources.append(s)
    
            # 获得fc7的内容
            for k in range(23, len(self.vgg)):
                x = self.vgg[k](x)
            sources.append(x)
    
            # 获得后面的内容
            for k, v in enumerate(self.extras):
                x = F.relu(v(x), inplace=True)
                if k % 2 == 1:
                    sources.append(x)
    
    
    
            # 添加回归层和分类层
            for (x, l, c) in zip(sources, self.loc, self.conf):
                loc.append(l(x).permute(0, 2, 3, 1).contiguous())
                conf.append(c(x).permute(0, 2, 3, 1).contiguous())
    
            # 进行resize
            loc = torch.cat([o.view(o.size(0), -1) for o in loc], 1)
            conf = torch.cat([o.view(o.size(0), -1) for o in conf], 1)
            if self.phase == "test":
                # loc会resize到batch_size,num_anchors,4
                # conf会resize到batch_size,num_anchors,
                output = self.detect(
                    loc.view(loc.size(0), -1, 4),                   # loc preds
                    self.softmax(conf.view(conf.size(0), -1,
                                 self.num_classes)),                # conf preds
                    self.priors              
                )
            else:
                output = (
                    loc.view(loc.size(0), -1, 4),
                    conf.view(conf.size(0), -1, self.num_classes),
                    self.priors
                )
            return output
    
    mbox = [4, 6, 6, 6, 4, 4]
    
    def get_ssd(phase,num_classes):
    
        vgg, extra_layers = add_vgg(3), add_extras(1024)
    
        loc_layers = []
        conf_layers = []
        vgg_source = [21, -2]
        for k, v in enumerate(vgg_source):
            loc_layers += [nn.Conv2d(vgg[v].out_channels,
                                     mbox[k] * 4, kernel_size=3, padding=1)]
            conf_layers += [nn.Conv2d(vgg[v].out_channels,
                            mbox[k] * num_classes, kernel_size=3, padding=1)]
                            
        for k, v in enumerate(extra_layers[1::2], 2):
            loc_layers += [nn.Conv2d(v.out_channels, mbox[k]
                                     * 4, kernel_size=3, padding=1)]
            conf_layers += [nn.Conv2d(v.out_channels, mbox[k]
                                      * num_classes, kernel_size=3, padding=1)]
    
        SSD_MODEL = SSD(phase, vgg, extra_layers, (loc_layers, conf_layers), num_classes)
        return SSD_MODEL
    

    3、预测结果的解码

    我们通过对每一个特征层的处理,可以获得三个内容,分别是:

    num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。**

    num_priors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测框对应的种类。

    每一个有效特征层对应的先验框对应着该特征层上 每一个网格点上 预先设定好的多个框。

    我们利用 num_priors x 4的卷积每一个有效特征层对应的先验框 获得框的真实位置。

    每一个有效特征层对应的先验框就是,如图所示的作用:
    每一个有效特征层将整个图片分成与其长宽对应的网格,如conv4-3的特征层就是将整个图像分成38x38个网格;然后从每个网格中心建立多个先验框,如conv4-3的特征层就是建立了4个先验框;对于conv4-3的特征层来讲,整个图片被分成38x38个网格,每个网格中心对应4个先验框,一共包含了,38x38x4个,5776个先验框。
    在这里插入图片描述
    先验框虽然可以代表一定的框的位置信息与框的大小信息,但是其是有限的,无法表示任意情况,因此还需要调整,ssd利用num_priors x 4的卷积的结果对先验框进行调整。

    num_priors x 4中的num_priors表示了这个网格点所包含的先验框数量,其中的4表示了x_offset、y_offset、h和w的调整情况。

    x_offset与y_offset代表了真实框距离先验框中心的xy轴偏移情况。
    h和w代表了真实框的宽与高相对于先验框的变化情况。

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

    当然得到最终的预测结构后还要进行得分排序与非极大抑制筛选这一部分基本上是所有目标检测通用的部分。
    1、取出每一类得分大于self.obj_threshold的框和得分。
    2、利用框的位置和得分进行非极大抑制。

    实现代码如下:

    # Adapted from https://github.com/Hakuyume/chainer-ssd
    def decode(loc, priors, variances):
        boxes = torch.cat((
            priors[:, :2] + loc[:, :2] * variances[0] * priors[:, 2:],
            priors[:, 2:] * torch.exp(loc[:, 2:] * variances[1])), 1)
        boxes[:, :2] -= boxes[:, 2:] / 2
        boxes[:, 2:] += boxes[:, :2]
        return boxes
    
    class Detect(Function):
        def __init__(self, num_classes, bkg_label, top_k, conf_thresh, nms_thresh):
            self.num_classes = num_classes
            self.background_label = bkg_label
            self.top_k = top_k
            self.nms_thresh = nms_thresh
            if nms_thresh <= 0:
                raise ValueError('nms_threshold must be non negative.')
            self.conf_thresh = conf_thresh
            self.variance = Config['variance']
    
        def forward(self, loc_data, conf_data, prior_data):
            loc_data = loc_data.cpu()
            conf_data = conf_data.cpu()
            num = loc_data.size(0)  # batch size
            num_priors = prior_data.size(0)
            output = torch.zeros(num, self.num_classes, self.top_k, 5)
            conf_preds = conf_data.view(num, num_priors,
                                        self.num_classes).transpose(2, 1)
            # 对每一张图片进行处理
            for i in range(num):
                # 对先验框解码获得预测框
                decoded_boxes = decode(loc_data[i], prior_data, self.variance)
                conf_scores = conf_preds[i].clone()
    
                for cl in range(1, self.num_classes):
                    # 对每一类进行非极大抑制
                    c_mask = conf_scores[cl].gt(self.conf_thresh)
                    scores = conf_scores[cl][c_mask]
                    if scores.size(0) == 0:
                        continue
                    l_mask = c_mask.unsqueeze(1).expand_as(decoded_boxes)
                    boxes = decoded_boxes[l_mask].view(-1, 4)
                    # 进行非极大抑制
                    ids, count = nms(boxes, scores, self.nms_thresh, self.top_k)
                    output[i, cl, :count] = \
                        torch.cat((scores[ids[:count]].unsqueeze(1),
                                   boxes[ids[:count]]), 1)
            flt = output.contiguous().view(num, -1, 5)
            _, idx = flt[:, :, 0].sort(1, descending=True)
            _, rank = idx.sort(1)
            flt[(rank < self.top_k).unsqueeze(-1).expand_as(flt)].fill_(0)
            return output
    

    4、在原图上进行绘制

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

    二、训练部分

    1、真实框的处理

    从预测部分我们知道,每个特征层的预测结果,num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。

    也就是说,我们直接利用ssd网络预测到的结果,并不是预测框在图片上的真实位置,需要解码才能得到真实位置。

    而在训练的时候,我们需要计算loss函数,这个loss函数是相对于ssd网络的预测结果的。我们需要把图片输入到当前的ssd网络中,得到预测结果;同时还需要把真实框的信息,进行编码,这个编码是把真实框的位置信息格式转化为ssd预测结果的格式信息

    也就是,我们需要找到 每一张用于训练的图片每一个真实框对应的先验框,并求出如果想要得到这样一个真实框,我们的预测结果应该是怎么样的。

    从预测结果获得真实框的过程被称作解码,而从真实框获得预测结果的过程就是编码的过程。

    因此我们只需要将解码过程逆过来就是编码过程了。

    实现代码如下:

    def encode(matched, priors, variances):
        g_cxcy = (matched[:, :2] + matched[:, 2:])/2 - priors[:, :2]
        g_cxcy /= (variances[0] * priors[:, 2:])
        g_wh = (matched[:, 2:] - matched[:, :2]) / priors[:, 2:]
        g_wh = torch.log(g_wh) / variances[1]
        return torch.cat([g_cxcy, g_wh], 1) 
    

    在训练的时候我们只需要选择iou最大的先验框就行了,这个iou最大的先验框就是我们用来预测这个真实框所用的先验框。

    因此我们还要经过一次筛选,将上述代码获得的真实框对应的所有的iou较大先验框的预测结果中,iou最大的那个筛选出来。

    实现代码如下:

    def match(threshold, truths, priors, variances, labels, loc_t, conf_t, idx):
        # 计算所有的先验框和真实框的重合程度
        overlaps = jaccard(
            truths,
            point_form(priors)
        )
        # 所有真实框和先验框的最好重合程度
        # [truth_box,1]
        best_prior_overlap, best_prior_idx = overlaps.max(1, keepdim=True)
        best_prior_idx.squeeze_(1)
        best_prior_overlap.squeeze_(1)
        # 所有先验框和真实框的最好重合程度
        # [1,prior]
        best_truth_overlap, best_truth_idx = overlaps.max(0, keepdim=True)
        best_truth_idx.squeeze_(0)
        best_truth_overlap.squeeze_(0)
        # 找到与真实框重合程度最好的先验框,用于保证每个真实框都要有对应的一个先验框
        best_truth_overlap.index_fill_(0, best_prior_idx, 2)
        # 对best_truth_idx内容进行设置
        for j in range(best_prior_idx.size(0)):
            best_truth_idx[best_prior_idx[j]] = j
        
        # 找到每个先验框重合程度最好的真实框
        matches = truths[best_truth_idx]          # Shape: [num_priors,4]
        conf = labels[best_truth_idx] + 1         # Shape: [num_priors]
        # 如果重合程度小于threhold则认为是背景
        conf[best_truth_overlap < threshold] = 0  # label as background
        loc = encode(matches, priors, variances)
        loc_t[idx] = loc    # [num_priors,4] encoded offsets to learn
        conf_t[idx] = conf  # [num_priors] top class label for each prior
    

    2、利用处理完的真实框与对应图片的预测结果计算loss

    loss的计算分为三个部分:
    1、获取所有正标签的框的预测结果的回归loss。
    2、获取所有正标签的种类的预测结果的交叉熵loss。
    3、获取一定负标签的种类的预测结果的交叉熵loss。

    由于在ssd的训练过程中,正负样本极其不平衡,即 存在对应真实框的先验框可能只有十来个,但是不存在对应真实框的负样本却有几千个,这就会导致负样本的loss值极大,因此我们可以考虑减少负样本的选取,对于ssd的训练来讲,常见的情况是取三倍正样本数量的负样本用于训练。这个三倍呢,也可以修改,调整成自己喜欢的数字。

    实现代码如下:

    
    
    class MultiBoxLoss(nn.Module):
        def __init__(self, num_classes, overlap_thresh, prior_for_matching,
                     bkg_label, neg_mining, neg_pos, neg_overlap, encode_target,
                     use_gpu=True):
            super(MultiBoxLoss, self).__init__()
            self.use_gpu = use_gpu
            self.num_classes = num_classes
            self.threshold = overlap_thresh
            self.background_label = bkg_label
            self.encode_target = encode_target
            self.use_prior_for_matching = prior_for_matching
            self.do_neg_mining = neg_mining
            self.negpos_ratio = neg_pos
            self.neg_overlap = neg_overlap
            self.variance = Config['variance']
    
        def forward(self, predictions, targets):
            # 回归信息,置信度,先验框
            loc_data, conf_data, priors = predictions
            # 计算出batch_size
            num = loc_data.size(0)
            # 取出所有的先验框
            priors = priors[:loc_data.size(1), :]
            # 先验框的数量
            num_priors = (priors.size(0))
            num_classes = self.num_classes
            # 创建一个tensor进行处理
            loc_t = torch.Tensor(num, num_priors, 4)
            conf_t = torch.LongTensor(num, num_priors)
            for idx in range(num):
                # 获得框
                truths = targets[idx][:, :-1].data
                # 获得标签
                labels = targets[idx][:, -1].data
                # 获得先验框
                defaults = priors.data
                # 找到标签对应的先验框
                match(self.threshold, truths, defaults, self.variance, labels,
                      loc_t, conf_t, idx)
            if self.use_gpu:
                loc_t = loc_t.cuda()
                conf_t = conf_t.cuda()
                
            # 转化成Variable
            loc_t = Variable(loc_t, requires_grad=False)
            conf_t = Variable(conf_t, requires_grad=False)
    
            # 所有conf_t>0的地方,代表内部包含物体
            pos = conf_t > 0
            # 求和得到每一个图片内部有多少正样本
            num_pos = pos.sum(dim=1, keepdim=True)
            # 计算回归loss
            pos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)
            loc_p = loc_data[pos_idx].view(-1, 4)
            loc_t = loc_t[pos_idx].view(-1, 4)
            loss_l = F.smooth_l1_loss(loc_p, loc_t, size_average=False)
    
            # 转化形式
            batch_conf = conf_data.view(-1, self.num_classes)
            # 你可以把softmax函数看成一种接受任何数字并转换为概率分布的非线性方法
            # 获得每个框预测到真实框的类的概率
            loss_c = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))
            loss_c = loss_c.view(num, -1)
    
            loss_c[pos] = 0 
            # 获得每一张图新的softmax的结果
            _, loss_idx = loss_c.sort(1, descending=True)
            _, idx_rank = loss_idx.sort(1)
            # 计算每一张图的正样本数量
            num_pos = pos.long().sum(1, keepdim=True)
            # 限制负样本数量
            num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)
            neg = idx_rank < num_neg.expand_as(idx_rank)
    
            # 计算正样本的loss和负样本的loss
            pos_idx = pos.unsqueeze(2).expand_as(conf_data)
            neg_idx = neg.unsqueeze(2).expand_as(conf_data)
            conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1, self.num_classes)
            targets_weighted = conf_t[(pos+neg).gt(0)]
            loss_c = F.cross_entropy(conf_p, targets_weighted, size_average=False)
    
            # Sum of losses: L(x,c,l,g) = (Lconf(x, c) + αLloc(x,l,g)) / N
    
            N = num_pos.data.sum()
            loss_l /= N
            loss_c /= N
            return loss_l, loss_c
    

    训练自己的ssd模型

    ssd整体的文件夹构架如下:
    在这里插入图片描述
    本文使用VOC格式进行训练。
    训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的Annotation中。
    在这里插入图片描述
    训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。
    在这里插入图片描述
    在训练前利用voc2ssd.py文件生成对应的txt。
    在这里插入图片描述
    再运行根目录下的voc_annotation.py,运行前需要将classes改成你自己的classes。

    classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]
    

    在这里插入图片描述
    就会生成对应的2007_train.txt,每一行对应其图片位置及其真实框的位置。
    在这里插入图片描述
    在训练前需要修改model_data里面的voc_classes.txt文件,需要将classes改成你自己的classes。
    在这里插入图片描述
    还有config文件下面的Num_Classes,修改成分类的数量+1。
    在这里插入图片描述
    运行train.py即可开始训练。
    在这里插入图片描述

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

    万次阅读 多人点赞 2020-01-30 16:23:28
    睿智的目标检测16——Keras搭建SSD目标检测平台学习前言什么是SSD目标检测算法源码下载SSD实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练部分1、计算loss...

    学习前言

    一起来看看SSD的keras实现吧,顺便训练一下自己的数据。
    在这里插入图片描述

    什么是SSD目标检测算法

    SSD是一种非常优秀的one-stage目标检测方法,one-stage算法就是目标检测和分类是同时完成的,其主要思路是利用CNN提取特征后,均匀地在图片的不同位置进行密集抽样,抽样时可以采用不同尺度和长宽比,物体分类与预测框的回归同时进行,整个过程只需要一步,所以其优势是速度快
    但是均匀的密集采样的一个重要缺点是训练比较困难,这主要是因为正样本与负样本(背景)极其不均衡(参见Focal Loss),导致模型准确度稍低。
    SSD的英文全名是Single Shot MultiBox Detector,Single shot说明SSD算法属于one-stage方法,MultiBox说明SSD算法基于多框预测。

    源码下载

    https://github.com/bubbliiiing/ssd-keras
    喜欢的可以点个star噢。

    SSD实现思路

    一、预测部分

    1、主干网络介绍

    在这里插入图片描述
    SSD采用的主干网络是VGG网络,关于VGG的介绍大家可以看我的另外一篇博客https://blog.csdn.net/weixin_44791964/article/details/102779878,这里的VGG网络相比普通的VGG网络有一定的修改,主要修改的地方就是:
    1、将VGG16的FC6和FC7层转化为卷积层。
    2、去掉所有的Dropout层和FC8层;
    3、新增了Conv6、Conv7、Conv8、Conv9。

    在这里插入图片描述
    如图所示,输入的图片经过了改进的VGG网络(Conv1->fc7)和几个另加的卷积层(Conv6->Conv9),进行特征提取:
    a、输入一张图片后,被resize到300x300的shape

    b、conv1,经过两次[3,3]卷积网络,输出的特征层为64,输出为(300,300,64),再2X2最大池化,输出net为(150,150,64)。

    c、conv2,经过两次[3,3]卷积网络,输出的特征层为128,输出net为(150,150,128),再2X2最大池化,输出net为(75,75,128)。

    d、conv3,经过三次[3,3]卷积网络,输出的特征层为256,输出net为(75,75,256),再2X2最大池化,输出net为(38,38,256)。

    e、conv4,经过三次[3,3]卷积网络,输出的特征层为512,输出net为(38,38,512),再2X2最大池化,输出net为(19,19,512)。

    f、conv5,经过三次[3,3]卷积网络,输出的特征层为512,输出net为(19,19,512),再3X3最大池化,输出net为(19,19,512)。

    g、利用卷积代替全连接层,进行了一次[3,3]卷积网络和一次[1,1]卷积网络,输出的特征层为1024,因此输出的net为(19,19,1024)。(从这里往前都是VGG的结构)

    h、conv6,经过一次[1,1]卷积网络,调整通道数,一次步长为2的[3,3]卷积网络,输出的特征层为512,因此输出的net为(10,10,512)。

    i、conv7,经过一次[1,1]卷积网络,调整通道数,一次步长为2的[3,3]卷积网络,输出的特征层为256,因此输出的net为(5,5,256)。

    j、conv8,经过一次[1,1]卷积网络,调整通道数,一次padding为valid的[3,3]卷积网络,输出的特征层为256,因此输出的net为(3,3,256)。

    k、conv9,经过一次[1,1]卷积网络,调整通道数,一次padding为valid的[3,3]卷积网络,输出的特征层为256,因此输出的net为(1,1,256)。

    实现代码:

    import keras.backend as K
    from keras.layers import Activation
    from keras.layers import Conv2D
    from keras.layers import Dense
    from keras.layers import Flatten
    from keras.layers import GlobalAveragePooling2D
    from keras.layers import Input
    from keras.layers import MaxPooling2D
    from keras.layers import merge, concatenate
    from keras.layers import Reshape
    from keras.layers import ZeroPadding2D
    from keras.models import Model
    
    def VGG16(input_tensor):
        #----------------------------主干特征提取网络开始---------------------------#
        # SSD结构,net字典
        net = {} 
        # Block 1
        net['input'] = input_tensor
        # 300,300,3 -> 150,150,64
        net['conv1_1'] = Conv2D(64, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv1_1')(net['input'])
        net['conv1_2'] = Conv2D(64, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv1_2')(net['conv1_1'])
        net['pool1'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                    name='pool1')(net['conv1_2'])
    
        
        # Block 2
        # 150,150,64 -> 75,75,128
        net['conv2_1'] = Conv2D(128, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv2_1')(net['pool1'])
        net['conv2_2'] = Conv2D(128, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv2_2')(net['conv2_1'])
        net['pool2'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                    name='pool2')(net['conv2_2'])
        # Block 3
        # 75,75,128 -> 38,38,256
        net['conv3_1'] = Conv2D(256, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv3_1')(net['pool2'])
        net['conv3_2'] = Conv2D(256, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv3_2')(net['conv3_1'])
        net['conv3_3'] = Conv2D(256, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv3_3')(net['conv3_2'])
        net['pool3'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                    name='pool3')(net['conv3_3'])
        # Block 4
        # 38,38,256 -> 19,19,512
        net['conv4_1'] = Conv2D(512, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv4_1')(net['pool3'])
        net['conv4_2'] = Conv2D(512, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv4_2')(net['conv4_1'])
        net['conv4_3'] = Conv2D(512, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv4_3')(net['conv4_2'])
        net['pool4'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                    name='pool4')(net['conv4_3'])
        # Block 5
        # 19,19,512 -> 19,19,512
        net['conv5_1'] = Conv2D(512, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv5_1')(net['pool4'])
        net['conv5_2'] = Conv2D(512, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv5_2')(net['conv5_1'])
        net['conv5_3'] = Conv2D(512, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv5_3')(net['conv5_2'])
        net['pool5'] = MaxPooling2D((3, 3), strides=(1, 1), padding='same',
                                    name='pool5')(net['conv5_3'])
        # FC6
        # 19,19,512 -> 19,19,1024
        net['fc6'] = Conv2D(1024, kernel_size=(3,3), dilation_rate=(6, 6),
                                         activation='relu', padding='same',
                                         name='fc6')(net['pool5'])
    
        # x = Dropout(0.5, name='drop6')(x)
        # FC7
        # 19,19,1024 -> 19,19,1024
        net['fc7'] = Conv2D(1024, kernel_size=(1,1), activation='relu',
                                   padding='same', name='fc7')(net['fc6'])
    
        # x = Dropout(0.5, name='drop7')(x)
        # Block 6
        # 19,19,512 -> 10,10,512
        net['conv6_1'] = Conv2D(256, kernel_size=(1,1), activation='relu',
                                       padding='same',
                                       name='conv6_1')(net['fc7'])
        net['conv6_2'] = ZeroPadding2D(padding=((1, 1), (1, 1)), name='conv6_padding')(net['conv6_1'])
        net['conv6_2'] = Conv2D(512, kernel_size=(3,3), strides=(2, 2),
                                       activation='relu',
                                       name='conv6_2')(net['conv6_2'])
    
        # Block 7
        # 10,10,512 -> 5,5,256
        net['conv7_1'] = Conv2D(128, kernel_size=(1,1), activation='relu',
                                       padding='same', 
                                       name='conv7_1')(net['conv6_2'])
        net['conv7_2'] = ZeroPadding2D(padding=((1, 1), (1, 1)), name='conv7_padding')(net['conv7_1'])
        net['conv7_2'] = Conv2D(256, kernel_size=(3,3), strides=(2, 2),
                                       activation='relu', padding='valid',
                                       name='conv7_2')(net['conv7_2'])
        # Block 8
        # 5,5,256 -> 3,3,256
        net['conv8_1'] = Conv2D(128, kernel_size=(1,1), activation='relu',
                                       padding='same',
                                       name='conv8_1')(net['conv7_2'])
        net['conv8_2'] = Conv2D(256, kernel_size=(3,3), strides=(1, 1),
                                       activation='relu', padding='valid',
                                       name='conv8_2')(net['conv8_1'])
    
        # Block 9
        # 3,3,256 -> 1,1,256
        net['conv9_1'] = Conv2D(128, kernel_size=(1,1), activation='relu',
                                       padding='same',
                                       name='conv9_1')(net['conv8_2'])
        net['conv9_2'] = Conv2D(256, kernel_size=(3,3), strides=(1, 1),
                                       activation='relu', padding='valid',
                                       name='conv9_2')(net['conv9_1'])
        #----------------------------主干特征提取网络结束---------------------------#
        return net
    

    2、从特征获取预测结果

    在这里插入图片描述
    由上图我们可以知道,我们分别取conv4的第三次卷积的特征、fc7的特征、conv6的第二次卷积的特征、conv7的第二次卷积的特征、conv8的第二次卷积的特征、conv9的第二次卷积的特征,为了和普通特征层区分,我们称之为有效特征层,来获取预测结果。

    对获取到的每一个有效特征层,我们分别对其进行一次num_priors x 4的卷积、一次num_priors x num_classes的卷积、并需要计算每一个有效特征层对应的先验框。而num_priors指的是该特征层所拥有的先验框数量。

    其中:
    num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。(为什么说是变化情况呢,这是因为ssd的预测结果需要结合先验框获得预测框,预测结果就是先验框的变化情况。)

    num_priors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测框对应的种类。

    每一个有效特征层对应的先验框对应着该特征层上 每一个网格点上 预先设定好的多个框。

    所有的特征层对应的预测结果的shape如下:
    在这里插入图片描述
    实现代码为:

    def SSD300(input_shape, num_classes=21):
        # 300,300,3
        input_tensor = Input(shape=input_shape)
        img_size = (input_shape[1], input_shape[0])
    
        # SSD结构,net字典
        net = VGG16(input_tensor)
        #-----------------------将提取到的主干特征进行处理---------------------------#
        # 对conv4_3进行处理 38,38,512
        net['conv4_3_norm'] = Normalize(20, name='conv4_3_norm')(net['conv4_3'])
        num_priors = 4
        # 预测框的处理
        # num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
        net['conv4_3_norm_mbox_loc'] = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same', name='conv4_3_norm_mbox_loc')(net['conv4_3_norm'])
        net['conv4_3_norm_mbox_loc_flat'] = Flatten(name='conv4_3_norm_mbox_loc_flat')(net['conv4_3_norm_mbox_loc'])
        # num_priors表示每个网格点先验框的数量,num_classes是所分的类
        net['conv4_3_norm_mbox_conf'] = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv4_3_norm_mbox_conf')(net['conv4_3_norm'])
        net['conv4_3_norm_mbox_conf_flat'] = Flatten(name='conv4_3_norm_mbox_conf_flat')(net['conv4_3_norm_mbox_conf'])
        priorbox = PriorBox(img_size, 30.0,max_size = 60.0, aspect_ratios=[2],
                            variances=[0.1, 0.1, 0.2, 0.2],
                            name='conv4_3_norm_mbox_priorbox')
        net['conv4_3_norm_mbox_priorbox'] = priorbox(net['conv4_3_norm'])
        
        # 对fc7层进行处理 
        num_priors = 6
        # 预测框的处理
        # num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
        net['fc7_mbox_loc'] = Conv2D(num_priors * 4, kernel_size=(3,3),padding='same',name='fc7_mbox_loc')(net['fc7'])
        net['fc7_mbox_loc_flat'] = Flatten(name='fc7_mbox_loc_flat')(net['fc7_mbox_loc'])
        # num_priors表示每个网格点先验框的数量,num_classes是所分的类
        net['fc7_mbox_conf'] = Conv2D(num_priors * num_classes, kernel_size=(3,3),padding='same',name='fc7_mbox_conf')(net['fc7'])
        net['fc7_mbox_conf_flat'] = Flatten(name='fc7_mbox_conf_flat')(net['fc7_mbox_conf'])
    
        priorbox = PriorBox(img_size, 60.0, max_size=111.0, aspect_ratios=[2, 3],
                            variances=[0.1, 0.1, 0.2, 0.2],
                            name='fc7_mbox_priorbox')
        net['fc7_mbox_priorbox'] = priorbox(net['fc7'])
    
        # 对conv6_2进行处理
        num_priors = 6
        # 预测框的处理
        # num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
        x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv6_2_mbox_loc')(net['conv6_2'])
        net['conv6_2_mbox_loc'] = x
        net['conv6_2_mbox_loc_flat'] = Flatten(name='conv6_2_mbox_loc_flat')(net['conv6_2_mbox_loc'])
        # num_priors表示每个网格点先验框的数量,num_classes是所分的类
        x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv6_2_mbox_conf')(net['conv6_2'])
        net['conv6_2_mbox_conf'] = x
        net['conv6_2_mbox_conf_flat'] = Flatten(name='conv6_2_mbox_conf_flat')(net['conv6_2_mbox_conf'])
    
        priorbox = PriorBox(img_size, 111.0, max_size=162.0, aspect_ratios=[2, 3],
                            variances=[0.1, 0.1, 0.2, 0.2],
                            name='conv6_2_mbox_priorbox')
        net['conv6_2_mbox_priorbox'] = priorbox(net['conv6_2'])
    
        # 对conv7_2进行处理
        num_priors = 6
        # 预测框的处理
        # num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
        x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv7_2_mbox_loc')(net['conv7_2'])
        net['conv7_2_mbox_loc'] = x
        net['conv7_2_mbox_loc_flat'] = Flatten(name='conv7_2_mbox_loc_flat')(net['conv7_2_mbox_loc'])
        # num_priors表示每个网格点先验框的数量,num_classes是所分的类
        x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv7_2_mbox_conf')(net['conv7_2'])
        net['conv7_2_mbox_conf'] = x
        net['conv7_2_mbox_conf_flat'] = Flatten(name='conv7_2_mbox_conf_flat')(net['conv7_2_mbox_conf'])
    
        priorbox = PriorBox(img_size, 162.0, max_size=213.0, aspect_ratios=[2, 3],
                            variances=[0.1, 0.1, 0.2, 0.2],
                            name='conv7_2_mbox_priorbox')
        net['conv7_2_mbox_priorbox'] = priorbox(net['conv7_2'])
    
        # 对conv8_2进行处理
        num_priors = 4
        # 预测框的处理
        # num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
        x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv8_2_mbox_loc')(net['conv8_2'])
        net['conv8_2_mbox_loc'] = x
        net['conv8_2_mbox_loc_flat'] = Flatten(name='conv8_2_mbox_loc_flat')(net['conv8_2_mbox_loc'])
        # num_priors表示每个网格点先验框的数量,num_classes是所分的类
        x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv8_2_mbox_conf')(net['conv8_2'])
        net['conv8_2_mbox_conf'] = x
        net['conv8_2_mbox_conf_flat'] = Flatten(name='conv8_2_mbox_conf_flat')(net['conv8_2_mbox_conf'])
    
        priorbox = PriorBox(img_size, 213.0, max_size=264.0, aspect_ratios=[2],
                            variances=[0.1, 0.1, 0.2, 0.2],
                            name='conv8_2_mbox_priorbox')
        net['conv8_2_mbox_priorbox'] = priorbox(net['conv8_2'])
    
        # 对conv9_2进行处理
        num_priors = 4
        # 预测框的处理
        # num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
        x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv9_2_mbox_loc')(net['conv9_2'])
        net['conv9_2_mbox_loc'] = x
        net['conv9_2_mbox_loc_flat'] = Flatten(name='conv9_2_mbox_loc_flat')(net['conv9_2_mbox_loc'])
        # num_priors表示每个网格点先验框的数量,num_classes是所分的类
        x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv9_2_mbox_conf')(net['conv9_2'])
        net['conv9_2_mbox_conf'] = x
        net['conv9_2_mbox_conf_flat'] = Flatten(name='conv9_2_mbox_conf_flat')(net['conv9_2_mbox_conf'])
        
        priorbox = PriorBox(img_size, 264.0, max_size=315.0, aspect_ratios=[2],
                            variances=[0.1, 0.1, 0.2, 0.2],
                            name='conv9_2_mbox_priorbox')
    
        net['conv9_2_mbox_priorbox'] = priorbox(net['conv9_2'])
    
        # 将所有结果进行堆叠
        net['mbox_loc'] = concatenate([net['conv4_3_norm_mbox_loc_flat'],
                                 net['fc7_mbox_loc_flat'],
                                 net['conv6_2_mbox_loc_flat'],
                                 net['conv7_2_mbox_loc_flat'],
                                 net['conv8_2_mbox_loc_flat'],
                                 net['conv9_2_mbox_loc_flat']],
                                axis=1, name='mbox_loc')
        net['mbox_conf'] = concatenate([net['conv4_3_norm_mbox_conf_flat'],
                                  net['fc7_mbox_conf_flat'],
                                  net['conv6_2_mbox_conf_flat'],
                                  net['conv7_2_mbox_conf_flat'],
                                  net['conv8_2_mbox_conf_flat'],
                                  net['conv9_2_mbox_conf_flat']],
                                 axis=1, name='mbox_conf')
        net['mbox_priorbox'] = concatenate([net['conv4_3_norm_mbox_priorbox'],
                                      net['fc7_mbox_priorbox'],
                                      net['conv6_2_mbox_priorbox'],
                                      net['conv7_2_mbox_priorbox'],
                                      net['conv8_2_mbox_priorbox'],
                                      net['conv9_2_mbox_priorbox']],
                                      axis=1, name='mbox_priorbox')
    
        if hasattr(net['mbox_loc'], '_keras_shape'):
            num_boxes = net['mbox_loc']._keras_shape[-1] // 4
        elif hasattr(net['mbox_loc'], 'int_shape'):
            num_boxes = K.int_shape(net['mbox_loc'])[-1] // 4
        # 8732,4
        net['mbox_loc'] = Reshape((num_boxes, 4),name='mbox_loc_final')(net['mbox_loc'])
        # 8732,21
        net['mbox_conf'] = Reshape((num_boxes, num_classes),name='mbox_conf_logits')(net['mbox_conf'])
        net['mbox_conf'] = Activation('softmax',name='mbox_conf_final')(net['mbox_conf'])
    
        net['predictions'] = concatenate([net['mbox_loc'],
                                   net['mbox_conf'],
                                   net['mbox_priorbox']],
                                   axis=2, name='predictions')
        print(net['predictions'])
        model = Model(net['input'], net['predictions'])
        return model
    

    3、预测结果的解码

    我们通过对每一个特征层的处理,可以获得三个内容,分别是:

    num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。**

    num_priors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测框对应的种类。

    每一个有效特征层对应的先验框对应着该特征层上 每一个网格点上 预先设定好的多个框。

    我们利用 num_priors x 4的卷积每一个有效特征层对应的先验框 获得框的真实位置。

    每一个有效特征层对应的先验框就是,如图所示的作用:
    每一个有效特征层将整个图片分成与其长宽对应的网格,如conv4-3的特征层就是将整个图像分成38x38个网格;然后从每个网格中心建立多个先验框,如conv4-3的特征层就是建立了4个先验框;对于conv4-3的特征层来讲,整个图片被分成38x38个网格,每个网格中心对应4个先验框,一共包含了,38x38x4个,5776个先验框。
    在这里插入图片描述
    先验框虽然可以代表一定的框的位置信息与框的大小信息,但是其是有限的,无法表示任意情况,因此还需要调整,ssd利用num_priors x 4的卷积的结果对先验框进行调整。

    num_priors x 4中的num_priors表示了这个网格点所包含的先验框数量,其中的4表示了x_offset、y_offset、h和w的调整情况。

    x_offset与y_offset代表了真实框距离先验框中心的xy轴偏移情况。
    h和w代表了真实框的宽与高相对于先验框的变化情况。

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

    当然得到最终的预测结构后还要进行得分排序与非极大抑制筛选这一部分基本上是所有目标检测通用的部分。
    1、取出每一类得分大于self.obj_threshold的框和得分。
    2、利用框的位置和得分进行非极大抑制。

    实现代码如下:

    def decode_boxes(self, mbox_loc, mbox_priorbox, variances):
        # 获得先验框的宽与高
        prior_width = mbox_priorbox[:, 2] - mbox_priorbox[:, 0]
        prior_height = mbox_priorbox[:, 3] - mbox_priorbox[:, 1]
        # 获得先验框的中心点
        prior_center_x = 0.5 * (mbox_priorbox[:, 2] + mbox_priorbox[:, 0])
        prior_center_y = 0.5 * (mbox_priorbox[:, 3] + mbox_priorbox[:, 1])
    
        # 真实框距离先验框中心的xy轴偏移情况
        decode_bbox_center_x = mbox_loc[:, 0] * prior_width * variances[:, 0]
        decode_bbox_center_x += prior_center_x
        decode_bbox_center_y = mbox_loc[:, 1] * prior_height * variances[:, 1]
        decode_bbox_center_y += prior_center_y
        
        # 真实框的宽与高的求取
        decode_bbox_width = np.exp(mbox_loc[:, 2] * variances[:, 2])
        decode_bbox_width *= prior_width
        decode_bbox_height = np.exp(mbox_loc[:, 3] * variances[:, 3])
        decode_bbox_height *= prior_height
    
        # 获取真实框的左上角与右下角
        decode_bbox_xmin = decode_bbox_center_x - 0.5 * decode_bbox_width
        decode_bbox_ymin = decode_bbox_center_y - 0.5 * decode_bbox_height
        decode_bbox_xmax = decode_bbox_center_x + 0.5 * decode_bbox_width
        decode_bbox_ymax = decode_bbox_center_y + 0.5 * decode_bbox_height
    
        # 真实框的左上角与右下角进行堆叠
        decode_bbox = np.concatenate((decode_bbox_xmin[:, None],
                                        decode_bbox_ymin[:, None],
                                        decode_bbox_xmax[:, None],
                                        decode_bbox_ymax[:, None]), axis=-1)
        # 防止超出0与1
        decode_bbox = np.minimum(np.maximum(decode_bbox, 0.0), 1.0)
        return decode_bbox
    
    def detection_out(self, predictions, background_label_id=0, keep_top_k=200,
                        confidence_threshold=0.5):
        # 网络预测的结果
        mbox_loc = predictions[:, :, :4]
        # 0.1,0.1,0.2,0.2
        variances = predictions[:, :, -4:]
        # 先验框
        mbox_priorbox = predictions[:, :, -8:-4]
        # 置信度
        mbox_conf = predictions[:, :, 4:-8]
        results = []
        # 对每一个特征层进行处理
        for i in range(len(mbox_loc)):
            results.append([])
            decode_bbox = self.decode_boxes(mbox_loc[i], mbox_priorbox[i],  variances[i])
    
            for c in range(self.num_classes):
                if c == background_label_id:
                    continue
                c_confs = mbox_conf[i, :, c]
                c_confs_m = c_confs > confidence_threshold
                if len(c_confs[c_confs_m]) > 0:
                    # 取出得分高于confidence_threshold的框
                    boxes_to_process = decode_bbox[c_confs_m]
                    confs_to_process = c_confs[c_confs_m]
                    # 进行iou的非极大抑制
                    feed_dict = {self.boxes: boxes_to_process,
                                    self.scores: confs_to_process}
                    idx = self.sess.run(self.nms, feed_dict=feed_dict)
                    # 取出在非极大抑制中效果较好的内容
                    good_boxes = boxes_to_process[idx]
                    confs = confs_to_process[idx][:, None]
                    # 将label、置信度、框的位置进行堆叠。
                    labels = c * np.ones((len(idx), 1))
                    c_pred = np.concatenate((labels, confs, good_boxes),
                                            axis=1)
                    # 添加进result里
                    results[-1].extend(c_pred)
            if len(results[-1]) > 0:
                # 按照置信度进行排序
                results[-1] = np.array(results[-1])
                argsort = np.argsort(results[-1][:, 1])[::-1]
                results[-1] = results[-1][argsort]
                # 选出置信度最大的keep_top_k个
                results[-1] = results[-1][:keep_top_k]
        return results
    

    4、在原图上进行绘制

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

    二、训练部分

    1、真实框的处理

    从预测部分我们知道,每个特征层的预测结果,num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。

    也就是说,我们直接利用ssd网络预测到的结果,并不是预测框在图片上的真实位置,需要解码才能得到真实位置。

    而在训练的时候,我们需要计算loss函数,这个loss函数是相对于ssd网络的预测结果的。我们需要把图片输入到当前的ssd网络中,得到预测结果;同时还需要把真实框的信息,进行编码,这个编码是把真实框的位置信息格式转化为ssd预测结果的格式信息

    也就是,我们需要找到 每一张用于训练的图片每一个真实框对应的先验框,并求出如果想要得到这样一个真实框,我们的预测结果应该是怎么样的。

    从预测结果获得真实框的过程被称作解码,而从真实框获得预测结果的过程就是编码的过程。

    因此我们只需要将解码过程逆过来就是编码过程了。

    实现代码如下:

    def encode_box(self, box, return_iou=True):
        iou = self.iou(box)
        encoded_box = np.zeros((self.num_priors, 4 + return_iou))
    
        # 找到每一个真实框,重合程度较高的先验框
        assign_mask = iou > self.overlap_threshold
        if not assign_mask.any():
            assign_mask[iou.argmax()] = True
        if return_iou:
            encoded_box[:, -1][assign_mask] = iou[assign_mask]
        
        # 找到对应的先验框
        assigned_priors = self.priors[assign_mask]
        # 逆向编码,将真实框转化为ssd预测结果的格式
    
        # 先计算真实框的中心与长宽
        box_center = 0.5 * (box[:2] + box[2:])
        box_wh = box[2:] - box[:2]
        # 再计算重合度较高的先验框的中心与长宽
        assigned_priors_center = 0.5 * (assigned_priors[:, :2] +
                                        assigned_priors[:, 2:4])
        assigned_priors_wh = (assigned_priors[:, 2:4] -
                                assigned_priors[:, :2])
        
        # 逆向求取ssd应该有的预测结果
        encoded_box[:, :2][assign_mask] = box_center - assigned_priors_center
        encoded_box[:, :2][assign_mask] /= assigned_priors_wh
        # 除以0.1
        encoded_box[:, :2][assign_mask] /= assigned_priors[:, -4:-2]
    
        encoded_box[:, 2:4][assign_mask] = np.log(box_wh / assigned_priors_wh)
        # 除以0.2
        encoded_box[:, 2:4][assign_mask] /= assigned_priors[:, -2:]
        return encoded_box.ravel()
    

    利用上述代码我们可以获得,真实框对应的所有的iou较大先验框,并计算了真实框对应的所有iou较大的先验框应该有的预测结果。

    在训练的时候我们只需要选择iou最大的先验框就行了,这个iou最大的先验框就是我们用来预测这个真实框所用的先验框。

    因此我们还要经过一次筛选,将上述代码获得的真实框对应的所有的iou较大先验框的预测结果中,iou最大的那个筛选出来。

    通过assign_boxes我们就获得了,输入进来的这张图片,应该有的预测结果是什么样子的。

    实现代码如下:

    def assign_boxes(self, boxes):
        assignment = np.zeros((self.num_priors, 4 + self.num_classes + 8))
        assignment[:, 4] = 1.0
        if len(boxes) == 0:
            return assignment
        # 对每一个真实框都进行iou计算
        encoded_boxes = np.apply_along_axis(self.encode_box, 1, boxes[:, :4])
        # 每一个真实框的编码后的值,和iou
        encoded_boxes = encoded_boxes.reshape(-1, self.num_priors, 5)
        
        # 取重合程度最大的先验框,并且获取这个先验框的index
        best_iou = encoded_boxes[:, :, -1].max(axis=0)
        best_iou_idx = encoded_boxes[:, :, -1].argmax(axis=0)
        best_iou_mask = best_iou > 0
        best_iou_idx = best_iou_idx[best_iou_mask]
    
        assign_num = len(best_iou_idx)
        # 保留重合程度最大的先验框的应该有的预测结果
        encoded_boxes = encoded_boxes[:, best_iou_mask, :]
        assignment[:, :4][best_iou_mask] = encoded_boxes[best_iou_idx,np.arange(assign_num),:4]
        # 4代表为背景的概率,为0
        assignment[:, 4][best_iou_mask] = 0
        assignment[:, 5:-8][best_iou_mask] = boxes[best_iou_idx, 4:]
        assignment[:, -8][best_iou_mask] = 1
        # 通过assign_boxes我们就获得了,输入进来的这张图片,应该有的预测结果是什么样子的
        return assignment
    

    2、利用处理完的真实框与对应图片的预测结果计算loss

    loss的计算分为三个部分:
    1、获取所有正标签的框的预测结果的回归loss。
    2、获取所有正标签的种类的预测结果的交叉熵loss。
    3、获取一定负标签的种类的预测结果的交叉熵loss。

    由于在ssd的训练过程中,正负样本极其不平衡,即 存在对应真实框的先验框可能只有2~3个,但是不存在对应真实框的负样本却有几千个,这就会导致负样本的loss值极大,因此我们可以考虑减少负样本的选取,对于ssd的训练来讲,常见的情况是取三倍正样本数量的负样本用于训练。这个三倍呢,也可以修改,调整成自己喜欢的数字。

    实现代码如下:

    class MultiboxLoss(object):
        def __init__(self, num_classes, alpha=1.0, neg_pos_ratio=3.0,
                     background_label_id=0, negatives_for_hard=100.0):
            self.num_classes = num_classes
            self.alpha = alpha
            self.neg_pos_ratio = neg_pos_ratio
            if background_label_id != 0:
                raise Exception('Only 0 as background label id is supported')
            self.background_label_id = background_label_id
            self.negatives_for_hard = negatives_for_hard
    
        def _l1_smooth_loss(self, y_true, y_pred):
            abs_loss = tf.abs(y_true - y_pred)
            sq_loss = 0.5 * (y_true - y_pred)**2
            l1_loss = tf.where(tf.less(abs_loss, 1.0), sq_loss, abs_loss - 0.5)
            return tf.reduce_sum(l1_loss, -1)
    
        def _softmax_loss(self, y_true, y_pred):
            y_pred = tf.maximum(tf.minimum(y_pred, 1 - 1e-15), 1e-15)
            softmax_loss = -tf.reduce_sum(y_true * tf.log(y_pred),
                                          axis=-1)
            return softmax_loss
    
        def compute_loss(self, y_true, y_pred):
            batch_size = tf.shape(y_true)[0]
            num_boxes = tf.to_float(tf.shape(y_true)[1])
    
            # 计算所有的loss
            # 分类的loss
            # batch_size,8732,21 -> batch_size,8732
            conf_loss = self._softmax_loss(y_true[:, :, 4:-8],
                                           y_pred[:, :, 4:-8])
            # 框的位置的loss
            # batch_size,8732,4 -> batch_size,8732
            loc_loss = self._l1_smooth_loss(y_true[:, :, :4],
                                            y_pred[:, :, :4])
    
            # 获取所有的正标签的loss
            # 每一张图的pos的个数
            num_pos = tf.reduce_sum(y_true[:, :, -8], axis=-1)
            # 每一张图的pos_loc_loss
            pos_loc_loss = tf.reduce_sum(loc_loss * y_true[:, :, -8],
                                         axis=1)
            # 每一张图的pos_conf_loss
            pos_conf_loss = tf.reduce_sum(conf_loss * y_true[:, :, -8],
                                          axis=1)
    
            # 获取一定的负样本
            num_neg = tf.minimum(self.neg_pos_ratio * num_pos,
                                 num_boxes - num_pos)
    
            # 找到了哪些值是大于0的
            pos_num_neg_mask = tf.greater(num_neg, 0)
            # 获得一个1.0
            has_min = tf.to_float(tf.reduce_any(pos_num_neg_mask))
            num_neg = tf.concat( axis=0,values=[num_neg,
                                    [(1 - has_min) * self.negatives_for_hard]])
            # 求平均每个图片要取多少个负样本
            num_neg_batch = tf.reduce_mean(tf.boolean_mask(num_neg,
                                                          tf.greater(num_neg, 0)))
            num_neg_batch = tf.to_int32(num_neg_batch)
    
            # conf的起始
            confs_start = 4 + self.background_label_id + 1
            # conf的结束
            confs_end = confs_start + self.num_classes - 1
    
            # 找到实际上在该位置不应该有预测结果的框,求他们最大的置信度。
            max_confs = tf.reduce_max(y_pred[:, :, confs_start:confs_end],
                                      axis=2)
            
            # 取top_k个置信度,作为负样本
            _, indices = tf.nn.top_k(max_confs * (1 - y_true[:, :, -8]),
                                     k=num_neg_batch)
    
            # 找到其在1维上的索引
            batch_idx = tf.expand_dims(tf.range(0, batch_size), 1)
            batch_idx = tf.tile(batch_idx, (1, num_neg_batch))
            full_indices = (tf.reshape(batch_idx, [-1]) * tf.to_int32(num_boxes) +
                            tf.reshape(indices, [-1]))
            
    
            neg_conf_loss = tf.gather(tf.reshape(conf_loss, [-1]),
                                      full_indices)
            neg_conf_loss = tf.reshape(neg_conf_loss,
                                       [batch_size, num_neg_batch])
            neg_conf_loss = tf.reduce_sum(neg_conf_loss, axis=1)
    
            # 求loss总和
            total_loss = K.sum(pos_conf_loss + neg_conf_loss)/K.cast(batch_size,K.dtype(pos_conf_loss))
    
            total_loss +=  K.sum(self.alpha * pos_loc_loss)/K.cast(batch_size,K.dtype(pos_loc_loss))
            return total_loss
    

    训练自己的ssd模型

    ssd整体的文件夹构架如下:
    在这里插入图片描述
    本文使用VOC格式进行训练。
    训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的Annotation中。
    在这里插入图片描述
    训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。
    在这里插入图片描述
    在训练前利用voc2ssd.py文件生成对应的txt。
    在这里插入图片描述
    再运行根目录下的voc_annotation.py,运行前需要将classes改成你自己的classes。

    classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]
    

    在这里插入图片描述
    就会生成对应的2007_train.txt,每一行对应其图片位置及其真实框的位置。
    在这里插入图片描述
    在训练前需要修改model_data里面的voc_classes.txt文件,需要将classes改成你自己的classes。
    在这里插入图片描述
    运行train.py即可开始训练。
    在这里插入图片描述

    展开全文
  • 深度学习算法之-SSD(一)

    千次阅读 2019-05-16 19:51:13
    版权声明:本文为博主原创文章,转载需注明出处。 https://blog.csdn.net/qianqing13579/article/details/82106664 </div> <link rel="stylesheet" href="https://csdnimg.cn/relea...
  • [paper reading] SSD

    2020-11-07 15:34:10
    [paper reading] SSD 本来想放到GitHub的,结果GitHub不支持公式。 没办法只能放到CSDN,但是格式也有些乱 强烈建议去GitHub上下载源文件,来阅读学习!!!这样阅读体验才是最好的 当然,如果有用,希望能给个star...
  • SSD系列(SSD、DSSD、FSSD 、RefineDet)

    千次阅读 2019-08-26 10:31:49
    SSD:SingleShotMultiBoxDetector 简介 one-stage、基于回归的目标检测,74.3mAP、59FPS (on VOC2007 test) 网络结构 SSD 300中输入图像的大小是300x300,特征提取部分使用...
  • SSD算法详解

    万次阅读 多人点赞 2019-01-21 16:41:34
    SSD github : https://github.com/weiliu89/caffe/tree/ssd SSD paper :https://arxiv.org/abs/1512.02325 SSD 动态PPT:...
  • SSD详解

    万次阅读 多人点赞 2018-03-05 21:57:25
    论文题目:SSD: Single Shot MultiBox Detector 论文链接:论文链接 论文代码:Caffe代码点击此处 This results in a significant improvement in speed for high-accuracy detection(59 FPS with mAP 74.3% ...
  • 其实很简单,想要加快读写速度,其中最有效的方法之一就是你需要一块高速运转的固态硬盘(SSD)。什么样的固态硬盘,才是最好的呢?说实话,仁者见仁,不同的工作应用场景,可能有不同的配置选择,今天,我们就一起为...
  • SSD框架详细解读(一)

    万次阅读 多人点赞 2018-09-18 19:05:12
    目录 模型结构 Prior box默认框的产生 Prior box 的大小尺寸计算 输出层通道num_output计算。 ...Permute,Flatten And Concat Layers ...matching strategy:选择一系列default boxes ...技巧对比效果结...
  • SSD总结报告

    千次阅读 2018-12-07 17:00:02
    SSD算法原理、框架流程、流程图 SSD网络结构 解释SSD里的atrous 解释default bounding box生成的流程 正负样本生成: 损失函数的表达式 解释多尺度特征图的预测 Caffe训练和测试过程 与YOLO、Faster R-CNN等...
  • SSD目标检测】3:训练自己的数据集(附数据集)

    万次阅读 多人点赞 2018-07-20 14:22:34
    前言:上一章已经详细介绍了SSD——多目标定位检测识别算法,该框架是Google开源的代码,所以自然有很多前辈做改进了。我也不过是站在前辈的肩膀上,这一章就是讲解如何训练自己的数据集,让SSD框架识别。源码也无偿...
  • SSD 检测原理

    千次阅读 2018-11-26 20:53:06
    SSD,全称Single Shot MultiBox Detector,是Wei Liu在ECCV 2016上提出的一种目标检测算法,速度比Faster RCNN更快,mAP优于YOLO v1。 SSD方法基于前馈卷积网络,其产生固定大小的边界框集合和框中对象类别的分数...
  • 本文主要针对SSD的tensorflow框架下的实现的源码解读即对网络模型的理解。 【前言】 首先在github上下载tensorflow版的SSD repository:https://github.com/balancap/SSD-Tensorflow 同时附上论文地址:SSD 论文...
  • 目标检测SSD+Tensorflow 训练自己的数据集

    万次阅读 多人点赞 2018-06-04 18:32:38
    2.解压ssd_300_vgg.ckpt.zip 到checkpoint文件夹下 3.测试一下看看,在notebooks中创建demo_test.py,其实就是复制ssd_notebook.ipynb中的代码,该py文件是完成对于单张图片的测试,对Jupyter不熟,就自己...
  • 目标检测算法之SSD

    万次阅读 多人点赞 2018-04-06 15:17:33
    码字不易,欢迎给个赞! 欢迎交流与转载,文章会同步发布在公众号:机器学习算法全栈工程师(Jeemy110) 目录 ...目标检测近年来已经取得了很重要的进展,主流的算法主要分为两个类型:(1)two-stage方法,如R-CNN...
  • SSD(single shot multibox detector)算法及Caffe代码详解

    万次阅读 多人点赞 2017-05-31 22:55:49
    这篇博客主要介绍SSD算法,该算法是最近一年比较优秀的object detection算法,主要特点在于采用了特征融合。 论文:SSD single shot multibox detector 论文链接:https://arxiv.org/abs/1512.02325 算法概述:...
  • SSD的配置安装与测试

    万次阅读 多人点赞 2016-07-04 18:26:37
    继yolo-darknet后,又一个SSD的配置教程,希望可以帮助大家。 注意:1、要求Ubuntu系统预先安装opencv和cuda7.5,如果您没有安装,请参考我的博客:http://blog.csdn.net/samylee/article/details/...

空空如也

1 2 3 4 5 ... 20
收藏数 90,026
精华内容 36,010
关键字:

ssd