深度学习初探——yolov3经典目标检测算法

2023-11-13

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

最近刚刚接触深度学习,并且简单学习了yolov3经典目标检测算法,在这里写一写自己的感悟,其中借鉴了很多大佬的博客,有错误的地方希望大家能够帮忙指出。

一、yolov3的网络结构

 其网络结构主要分为两部分,一部分是利用Darknet-53进行特征提取,另外一部分是利用FPN特征金字塔加强特征提取,然后进行预测输出。

二、利用Darknet-53进行特征提取 

上图yolov3的网络结构的左边部分就是Darknet-53。Darknet-53是一个卷积神经网络用于特征提取,其中大量的利用的残差网络Resnet来加深层数从而提高模型的准确程度。

首先输入一个416*416*3的图片,经过3*3的卷积(步长为1)后,得到了416*416*32(增加了通道数)结果,然后经过5个残差块,每个残差块都会首先进行下采样之后,再加入残差网络进行堆叠,加深网络深度,Darknet-53中5个残差块的堆叠次数分别为【1,2,8,8,4】,然后在完成经过第3,4,5次残差块后分别输出52*52*256,26*26*512,13*13*1024大小的特征,该三个大小的特征用于后面的FPN特征金字塔加强提取,预测输出。

1.残差网络

大家都了解,越深层的网络,参数越多,所映射的模型种类越多,也就意味着层数越深,我们训练出来的模型可能就越准确。但是实际过程中,过于深的网络层数可能会导致退化问题,可能会造成关键特征丢失。

残差网络的主要作用在于残差网络应用在深层神经网络中避免了过深层数导致的退化问题,减小了训练损失,下面简单讲一下残差网络是如何能够避免退化问题的。

残差网络与经典的卷积网络不同之处在于,他从输入引出一个分支与输出结果相加,那么其实在训练和卷积的过程中,其实我们训练的是F(X)=f(x)-x这一种映射,并且加上x后就变为f(x)了。

首先我们来看为什么这种残差映射可以避免网络退化问题,我们的输入为x,因为我们在训练过程中不可避免的会产生特征丢失问题,如果我们单纯训练f(x)并且当f(x)训练不准确时,那么x经过f(x)就很有可能会造成关键信息的丢失。但是如果经过F(X)=f(x)-x这一种映射,即使F(x)中存在信息丢失,但是f(x)=F(x)+x,x中仍然存在着关键信息的保留,所以残差网络可以有效的避免深层网络的退化问题。

 

上图中为什么残差网络中的x有时候还会经过1*1的卷积层,该卷积层的作用主要是改变通道数用于与F(x)的结果进行相加。

2.代码实现

下面我们来简单的看一下Darknet-53的特征提取网络是如何进行代码实现的 。

import math
from collections import OrderedDict

import torch.nn as nn


#---------------------------------------------------------------------#
#   残差结构
#   利用一个1x1卷积下降通道数,然后利用一个3x3卷积提取特征并且上升通道数
#   最后接上一个残差边
#---------------------------------------------------------------------#
class BasicBlock(nn.Module):
    def __init__(self, inplanes, planes):
        super(BasicBlock, self).__init__()
        self.conv1  = nn.Conv2d(inplanes, planes[0], kernel_size=1, stride=1, padding=0, bias=False)
        self.bn1    = nn.BatchNorm2d(planes[0])
        self.relu1  = nn.LeakyReLU(0.1)
        
        self.conv2  = nn.Conv2d(planes[0], planes[1], kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2    = nn.BatchNorm2d(planes[1])
        self.relu2  = nn.LeakyReLU(0.1)

    def forward(self, x):
        residual = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu1(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu2(out)

        out += residual
        return out

class DarkNet(nn.Module):
    def __init__(self, layers):
        super(DarkNet, self).__init__()
        self.inplanes = 32
        # 416,416,3 -> 416,416,32
        self.conv1  = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn1    = nn.BatchNorm2d(self.inplanes)
        self.relu1  = nn.LeakyReLU(0.1)

        # 416,416,32 -> 208,208,64
        self.layer1 = self._make_layer([32, 64], layers[0])
        # 208,208,64 -> 104,104,128
        self.layer2 = self._make_layer([64, 128], layers[1])
        # 104,104,128 -> 52,52,256
        self.layer3 = self._make_layer([128, 256], layers[2])
        # 52,52,256 -> 26,26,512
        self.layer4 = self._make_layer([256, 512], layers[3])
        # 26,26,512 -> 13,13,1024
        self.layer5 = self._make_layer([512, 1024], layers[4])

        self.layers_out_filters = [64, 128, 256, 512, 1024]

        # 进行权值初始化
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()

    #---------------------------------------------------------------------#
    #   在每一个layer里面,首先利用一个步长为2的3x3卷积进行下采样
    #   然后进行残差结构的堆叠
    #---------------------------------------------------------------------#
    def _make_layer(self, planes, blocks):
        layers = []
        # 下采样,步长为2,卷积核大小为3
        layers.append(("ds_conv", nn.Conv2d(self.inplanes, planes[1], kernel_size=3, stride=2, padding=1, bias=False)))
        layers.append(("ds_bn", nn.BatchNorm2d(planes[1])))
        layers.append(("ds_relu", nn.LeakyReLU(0.1)))
        # 加入残差结构
        self.inplanes = planes[1]
        for i in range(0, blocks):
            layers.append(("residual_{}".format(i), BasicBlock(self.inplanes, planes)))
        return nn.Sequential(OrderedDict(layers))

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu1(x)

        x = self.layer1(x)
        x = self.layer2(x)
        out3 = self.layer3(x)
        out4 = self.layer4(out3)
        out5 = self.layer5(out4)

        return out3, out4, out5

def darknet53():
    model = DarkNet([1, 2, 8, 8, 4])
    return model

代码首先 建立了一个BasicBlock的类,也就是一个残差块的类,方便后面进行重复利用,残差块的类主要实现的功能就是先后经过1*1和3*3的卷积,并把其结果加上输入,作为输出返回。

class BasicBlock(nn.Module):
    def __init__(self, inplanes, planes):
        super(BasicBlock, self).__init__()
        self.conv1  = nn.Conv2d(inplanes, planes[0], kernel_size=1, stride=1, padding=0, bias=False)
        self.bn1    = nn.BatchNorm2d(planes[0])
        self.relu1  = nn.LeakyReLU(0.1)
        
        self.conv2  = nn.Conv2d(planes[0], planes[1], kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2    = nn.BatchNorm2d(planes[1])
        self.relu2  = nn.LeakyReLU(0.1)

    def forward(self, x):
        residual = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu1(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu2(out)

        out += residual
        return out

 然后定义一个函数,利用上面的BasicBlock的类,实现每一个残差块的功能,首先进行下采样,3*3的卷积(步长为2),然后进行残差网络的堆叠,堆叠的层数由输入决定。

    def _make_layer(self, planes, blocks):
        layers = []
        # 下采样,步长为2,卷积核大小为3
        layers.append(("ds_conv", nn.Conv2d(self.inplanes, planes[1], kernel_size=3, stride=2, padding=1, bias=False)))
        layers.append(("ds_bn", nn.BatchNorm2d(planes[1])))
        layers.append(("ds_relu", nn.LeakyReLU(0.1)))
        # 加入残差结构
        self.inplanes = planes[1]
        for i in range(0, blocks):
            layers.append(("residual_{}".format(i), BasicBlock(self.inplanes, planes)))
        return nn.Sequential(OrderedDict(layers))

然后通过下面的代码传入参数,并且返回经过第3,4,5个 残差块后的结果,用于后面的特征加强和预测输出。

class DarkNet(nn.Module):
    def __init__(self, layers):
        super(DarkNet, self).__init__()
        self.inplanes = 32
        # 416,416,3 -> 416,416,32
        self.conv1  = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn1    = nn.BatchNorm2d(self.inplanes)
        self.relu1  = nn.LeakyReLU(0.1)

        # 416,416,32 -> 208,208,64
        self.layer1 = self._make_layer([32, 64], layers[0])
        # 208,208,64 -> 104,104,128
        self.layer2 = self._make_layer([64, 128], layers[1])
        # 104,104,128 -> 52,52,256
        self.layer3 = self._make_layer([128, 256], layers[2])
        # 52,52,256 -> 26,26,512
        self.layer4 = self._make_layer([256, 512], layers[3])
        # 26,26,512 -> 13,13,1024
        self.layer5 = self._make_layer([512, 1024], layers[4])

        self.layers_out_filters = [64, 128, 256, 512, 1024]

        # 进行权值初始化
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()
    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu1(x)

        x = self.layer1(x)
        x = self.layer2(x)
        out3 = self.layer3(x)
        out4 = self.layer4(out3)
        out5 = self.layer5(out4)

        return out3, out4, out5

三、利用FPN特征金字塔进行特征增强和预测输出

1.利用FPN特征金字塔进行特征增强

yolov3主要利用三个特征层进行目标检测,13*13*1024,26*26*512,52*52*256,这三个特征层, 以13*13*1024为例,其有两个去向,一个去向是进行5次卷积操作(卷积核大小为1,3,1,3,1)输出为13*13*512,然后进行卷积和上采样变为26*26*256的图像与上一层特征中的26*26*512进行特征叠加叠加形成26*26*768,另外一个去向是进行7次卷积操作(卷积核大小为1,3,1,3,1,3,1)前五次操作为特征提取,后面五次操作为生成预测结果,得到13*13*75。其中75可以拆分为3*(20+4+1),3为三个先验框,20为数据集中预测物体的种类数,4为先验框调整参数(中心点位置和宽高),最后一个参数是否包含物体。

然后26*26*512,52*52*256,也经过类似的操作后就可以对特征进行增强了。首先13*13的图像感受野比较大,适合预测大物体,26*26,52*52的适合于预测中小物体。

1.1.代码实现

from collections import OrderedDict

import torch
import torch.nn as nn

from nets.darknet import darknet53

def conv2d(filter_in, filter_out, kernel_size):
    pad = (kernel_size - 1) // 2 if kernel_size else 0
    return nn.Sequential(OrderedDict([
        ("conv", nn.Conv2d(filter_in, filter_out, kernel_size=kernel_size, stride=1, padding=pad, bias=False)),
        ("bn", nn.BatchNorm2d(filter_out)),
        ("relu", nn.LeakyReLU(0.1)),
    ]))

#------------------------------------------------------------------------#
#   make_last_layers里面一共有七个卷积,前五个用于提取特征。
#   后两个用于获得yolo网络的预测结果
#------------------------------------------------------------------------#
def make_last_layers(filters_list, in_filters, out_filter):
    m = nn.Sequential(
        conv2d(in_filters, filters_list[0], 1),
        conv2d(filters_list[0], filters_list[1], 3),
        conv2d(filters_list[1], filters_list[0], 1),
        conv2d(filters_list[0], filters_list[1], 3),
        conv2d(filters_list[1], filters_list[0], 1),
        conv2d(filters_list[0], filters_list[1], 3),
        nn.Conv2d(filters_list[1], out_filter, kernel_size=1, stride=1, padding=0, bias=True)
    )
    return m

class YoloBody(nn.Module):
    def __init__(self, anchors_mask, num_classes, pretrained = False):
        super(YoloBody, self).__init__()
        #---------------------------------------------------#   
        #   生成darknet53的主干模型
        #   获得三个有效特征层,他们的shape分别是:
        #   52,52,256
        #   26,26,512
        #   13,13,1024
        #---------------------------------------------------#
        self.backbone = darknet53()
        if pretrained:
            self.backbone.load_state_dict(torch.load("model_data/darknet53_backbone_weights.pth"))

        #---------------------------------------------------#
        #   out_filters : [64, 128, 256, 512, 1024]
        #---------------------------------------------------#
        out_filters = self.backbone.layers_out_filters

        #------------------------------------------------------------------------#
        #   计算yolo_head的输出通道数,对于voc数据集而言
        #   final_out_filter0 = final_out_filter1 = final_out_filter2 = 75
        #------------------------------------------------------------------------#
        self.last_layer0            = make_last_layers([512, 1024], out_filters[-1], len(anchors_mask[0]) * (num_classes + 5))

        self.last_layer1_conv       = conv2d(512, 256, 1)
        self.last_layer1_upsample   = nn.Upsample(scale_factor=2, mode='nearest')
        self.last_layer1            = make_last_layers([256, 512], out_filters[-2] + 256, len(anchors_mask[1]) * (num_classes + 5))

        self.last_layer2_conv       = conv2d(256, 128, 1)
        self.last_layer2_upsample   = nn.Upsample(scale_factor=2, mode='nearest')
        self.last_layer2            = make_last_layers([128, 256], out_filters[-3] + 128, len(anchors_mask[2]) * (num_classes + 5))

    def forward(self, x):
        #---------------------------------------------------#   
        #   获得三个有效特征层,他们的shape分别是:
        #   52,52,256;26,26,512;13,13,1024
        #---------------------------------------------------#
        x2, x1, x0 = self.backbone(x)

        #---------------------------------------------------#
        #   第一个特征层
        #   out0 = (batch_size,255,13,13)
        #---------------------------------------------------#
        # 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512
        out0_branch = self.last_layer0[:5](x0)
        out0        = self.last_layer0[5:](out0_branch)

        # 13,13,512 -> 13,13,256 -> 26,26,256
        x1_in = self.last_layer1_conv(out0_branch)
        x1_in = self.last_layer1_upsample(x1_in)

        # 26,26,256 + 26,26,512 -> 26,26,768
        x1_in = torch.cat([x1_in, x1], 1)
        #---------------------------------------------------#
        #   第二个特征层
        #   out1 = (batch_size,255,26,26)
        #---------------------------------------------------#
        # 26,26,768 -> 26,26,256 -> 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256
        out1_branch = self.last_layer1[:5](x1_in)
        out1        = self.last_layer1[5:](out1_branch)

        # 26,26,256 -> 26,26,128 -> 52,52,128
        x2_in = self.last_layer2_conv(out1_branch)
        x2_in = self.last_layer2_upsample(x2_in)

        # 52,52,128 + 52,52,256 -> 52,52,384
        x2_in = torch.cat([x2_in, x2], 1)
        #---------------------------------------------------#
        #   第一个特征层
        #   out3 = (batch_size,255,52,52)
        #---------------------------------------------------#
        # 52,52,384 -> 52,52,128 -> 52,52,256 -> 52,52,128 -> 52,52,256 -> 52,52,128
        out2 = self.last_layer2(x2_in)
        return out0, out1, out2

2.预测输出 

那么在实际过程中,yolov3是如何对预测框进行训练和输出的呢?我们以13*13*75为例来进行一下探讨。13*13*75可以改写为13*13*3*(20+4+1) ,其中3是先验框的数量。我们对图像中的每一个像素点都会先画出三个不同大小的先验框,用于训练,由于我们在训练过程中存在我们已经标注好的真实框,这时候我们就判断真实框和这个特征点的哪个先验框重合程度最高。计算该网格点应该有怎么样的预测结果才能获得真实框,与真实框重合度最高的先验框被用于作为正样本。然后重合度低于一定阈值的会作为负样本,属于中间部分的先验框样本会进行舍弃。其原因为中间样本的重合度仍然较高,如果作为负样本出现会影响模型的拟合。

最终损失由三个部分组成:a、正样本,编码后的长宽与xy轴偏移量与预测值的差距。b、正样本,预测结果中置信度的值与1对比;负样本,预测结果中置信度的值与0对比。c、实际存在的框,种类预测结果与实际结果的对比。

 由于代码长度较长,此处暂不粘贴,然后对于预测框分析大家可以参考博客如下

史上最详细的Yolov3边框预测分析_逍遥王可爱的博客-CSDN博客_yolov3 预测框


总结

本文是在借鉴了诸多大佬们的博客后学习所得,是对yolov3的简单回顾和归纳,其中可能存在些许理解错误,错误之处请大家伙们指出,参考博客如下

睿智的目标检测26——Pytorch搭建yolo3目标检测平台_Bubbliiiing的博客-CSDN博客_睿智的目标检测26

史上最详细的Yolov3边框预测分析_逍遥王可爱的博客-CSDN博客_yolov3 预测框 

Yolov3边框预测分析_lcczzu的博客-CSDN博客

 

Pytorch机器学习(九)—— YOLO中对于锚框,预测框,产生候选区域及对候选区域进行标注详解_lzzzzzzm的博客-CSDN博客_yolo锚框 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深度学习初探——yolov3经典目标检测算法 的相关文章

  • 实力总结四类Bean注入Spring的方式

    源码 实战 一提到 Spring 大家最先想到的是啥 是 AOP 和 IOC 的两大特性 是 Spring 中 Bean 的初始化流程 还是基于 Spring 的 Spring Cloud 全家桶呢 今天我们就从 Spring 的 IOC

随机推荐

  • php密码复杂,PHP判断密码强度的方法详解

    本文实例讲述了PHP判断密码强度的方法 分享给大家供大家参考 具体如下 一 php页面 score 0 if empty GET value 接收的值 str GET value else str if preg match 0 9 str
  • 【Twinkle】2019面试总结

    人生第一次找工作 因为没把自己培养成大神直接导致找工作累成了狗 秋招已经过了一大半 虽然还没有定目标企业 但是现在也简单的谢谢自己的经历和经验 留作纪念或者分享给大家 找工作前期说实话我还是准备了一段时间的 剑指offer 牛客之类的书籍或
  • arcgis图层 GraphicsLayer与FeatureLayer

    什么是图层 图层是用来在 ArcGIS 产品套件中显示地理数据集的机制 每个图层代表一种数据集 可以是地图服务 图形或是矢量数据 并指定该数据集是如何描绘使用一组属性的 包含一个地图控件的每个应用程序是通过添加一系列图层组装的 图层以特定的
  • 输出 100~999 之间的所有素数-Python

    Python 每日一题 需求 输出 100 999 之间的所有素数 所谓素数是一个大于 1 的正整数 除了 1 和它本身 该数不能被其它的正整数整除 过程 代码 用循环语句判断质数 for i in range 100 1000 range
  • 04别人能用的代码,在vscode或cmd下运行缺少模块的解决方法

    都是踩过的坑 总结一下解决方法就是 运行npm install进行修复 修复遇到问题可以删除 node modules 文件夹再试 具体方式如下 当运行 npm run serve 命令时遇到缺少模块 Module 的错误时 可以尝试以下几
  • CrowdSourcing-众包分析

    众包指的是一个公司或机构把过去由员工执行的工作任务 以自由自愿的形式外包给非特定的 而且通常是大型的 大众网络的做法 就是原来是公司做的事情 处于资金或者工作质量的考虑 通过游戏 ImageLabeler 社区 知乎 或者维基百科 竞赛 k
  • java.math.BigDecimal用法----Add、Subtract、Multiply、Divide

    引用自 url http www cnblogs com linjiqin p 3413894 html url 一 简介 Java在java math包中提供的API类BigDecimal 用来对超过16位有效位的数进行精确的运算 双精度
  • python基础01元组 函数

    1 可变类型的数值变化 是通过方法来实现的 2 如果给一个可变类型的变量 赋值一个新的数据 引用 id 会修改 3 不可变类型数据可以作为字典的键值对key 可变类型不能 元组 小括号 只可增删不可修改 函数 def print menu
  • QT 多层视图(视图叠加显示)

    之前做Mac应用开发 视图层是可以上下叠加显示的 然后回到QT这边开发 发现QT的布局上不能叠加显示 于是写了个简单的可以叠加QWidget的小Demo 这是调用的代码 include mainform h include multilay
  • 玉柴spn码故障对照表_卡车处理故障后不烧尿素也头疼!

    之前说到车辆烧尿素太多 让很多车主头疼 但是如果车辆不烧尿素了也会让车主头疼 若长期不使用SCR系统拆掉 系统里的化学物质会很快被氧化 尿素喷嘴也会堵塞 致使整套系统报废 更换一套SCR系统的成本比消耗尿素的成本要贵得多 化学物质发生氧化
  • 常用中间件控制台默认登陆

    以下用本地127 0 0 1作为ip前缀方便本地直接点击跳转使用 实际部署时替换该ip即可 中间件名称 访问地址端口 默认账号密码 rabbitmq http 127 0 0 1 15672 guest guest activemq htt
  • 有各组方差怎么算组间平方和_组内离差平方和,组间离差平方和与总离差平方和各反映了什么?...

    展开全部 组内离差平方和反映了试验过程中各种随机因素所引起32313133353236313431303231363533e59b9ee7ad9431333431353331的试验误差 组间离差平方和反映了各组样本之间的差异程度 即由变异因
  • TortoiseGit密钥配置图文教程

    1 在 开始菜单 中找到 TortoiseGit 下的 PuTTYgen 建议直接搜索PuTTYgen 单机运行 2 点击 Generate按钮 鼠标需要在 Key区域 活动直至绿色进度条完成 切记 鼠标一定要动哦 鼠标不动的时候 绿色进度
  • 华为HCS之私有云工具FCD搭建

    华为HCS之私有云工具FCD搭建 一 FCD虚拟机创建 1 选择Vmware版本 2 选择虚拟机操作系统版本 3 虚拟机cpu配置 4 虚拟机分配内存 5 选择磁盘大小 6 指定镜像目录 7 开机安装系统 二 系统的初始配置 1 IP配置
  • Java文件的读写

    文件与流 在实际生活中 我们总要对文件进行操作 Java提供了流可以帮助我们读写文件 下面是常用的流类型结构图 在对文件的读写操作中 字节流可用于读写二进制文件 字符流用于读写文本文件 所谓二进制文件 指文件无字符编码格式 均由字节 Byt
  • SpringBoot--基础--02--配置属性

    SpringBoot 基础 02 加载配置 代码位置 https gitee com DanShenGuiZu learnDemo tree mysql mybaties DB springboot learn springboot 1 1
  • springboot整合Swagger2 被拦截问题解决

    springboot整合Swagger2 被拦截问题解决 本来是想整合swagger3 弄了3个依赖进来 就一直注入不进来 推测版本的问题 springboot公司项目是2 0 6的 就先把swagger3降到2 项目用的是OAuth2 就
  • 程序员/媛的日常——coding/debugging/查bug/改bug

    程序员 媛的日常 coding debugging 查bug 改bug 一 在面对失误而带来的指责和沮丧时 你是如何做的 二 发生了失误之后 你是如何找到问题的 如何避免只看到表面现象而忽略了根本问题 三 在工作中我们应该怎么做才能降低失误
  • 矩阵分析

    看了这个视频后觉得自己是个没有学习过线代的人啊 感谢七月算法讲解的矩阵应用 本篇博文完全用作个人学习笔记 一 我们通过图形化来描述一下矩阵的行列和转置间的关系和特性 1 矩阵的行视图 我们以矩阵的每一行为组合 会得到一个几何图案 比如下图中
  • 深度学习初探——yolov3经典目标检测算法

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 目录 前言 一 yolov3的网络结构 二 利用Darknet 53进行特征提取 1 残差网络 2 代码实现 三 利用FPN特征金字塔进行特征增强和预测输出 1 利用FPN特