史上最详细的YOLOV3 SPP结构 代码解析

推荐一个特别好的博主@太阳花的小绿豆,本人小白一枚,学习深度学习神经网络的知识全是看这个博主学习的。最近学习到了Yolov3 spp的代码,觉得代码有一定的难度在这里分享一下。我尽可能的在每一句代码都进行了注释。
本文章也是看了太阳花小绿豆博主的视频学习总结出来的,学的比较浅,希望大家批评指正。
博主的视频在B站,想看视频的同学可以去看,Yolov3 spp代码这是连接。
代码也是这个博主的代码,这个目录下的所有文件下载下来就好

准备工作

我们要下载好VOC2012数据集,下载链接在这里https://pjreddie.com/media/files/VOCtrainval_11-May-2012.tar
在Yolov3spp文件夹在,创建一个文件叫VOCdevkit,下载好的数据集放在这个文件中。
在spp文件中的README文件中,也标注了每个文件的含义,这里不再叙述。有两个转换格式和生成文件的脚本,@太阳花的小绿豆真的是太伟大了。注意更改路径即可。下面直接上代码,代码是按照我的理解顺序来的。

代码实现

1、parse_config.py 解析网络结构

先看一下cfg文件里的yolov3-spp.cfg文件,这个文件记录着我们整个spp网络每一层的参数配置。parse_config.py说白了就是将这些参数读取,解析,以便我们使用去构建网络模型。

import os
import numpy as np

def parse_model_cfg(path: str):
    # 检查文件是否存在
    if not path.endswith(".cfg") or not os.path.exists(path):
        # path.endswith判断是否存在.cfg的文件
        raise FileNotFoundError("the cfg file not exist...")

    # 读取文件信息
    with open(path, "r") as f:
        lines = f.read().split("\n")
        # f.read().split()把文件内容当成一个列表返回  通过换行符进行分割变成一行一行的

    # 去除空行和注释行
    lines = [x for x in lines if x and not x.startswith("#")]
    # startswith()判断字符串是否以指定字符或子字符串开头。
    # 如果x不为空或者不是#开头的,就存在lines列表中,否者就丢弃。 for...[if]... 就是创建List的一种写法

    lines = [x.strip() for x in lines]
    # strip() 去除每行开头和结尾的空格符,因为括号中没有元素所以为空格
    mdefs = []  # 定义一个空列表
    for line in lines:            #遍历lines
        if line.startswith("["):  # 如果列表中的内容以[开头
            mdefs.append({
    
    })      # 在mdefs这个空列表中创建一个空字典
            mdefs[-1]["type"] = line[1:-1].strip()  # 记录module类型
            # [-1]就是空字典。在这个空字典里创建一组键值,键就是["type"],值就是line[1:-1],.strip()还是去除空格
            # 这时可以打开yolov3-spp.cfg文件观察,每一个[convolutional],第一个元素[索引为0,c索引为1,l索引为-1,因为切片时左闭右开,相当于做后一个元素是l
            # 所以对应的读取到convolutional存取到type中
            # 所以说这一步的操作就是读取到卷积或池化或yolo的名字
            if mdefs[-1]["type"] == "convolutional":
                mdefs[-1]["batch_normalize"] = 0
                # 如果是卷积模块,设置默认不使用BN,(普通卷积层后面会重写成1,最后的预测层conv保持为0)
        else:
            # 如果不是[开头,读取的就是参数值了
            key, val = line.split("=")
            # 通过等号进行分割得到key和value
            key = key.strip()
            val = val.strip()

            if key == "anchors":
                # 在最后yolo层中存在anchor
                # anchors = 10,13,  16,30,  33,23,  30,61,  62,45,  59,119,  116,90,  156,198,  373,326
                val = val.replace(" ", "")  # 将空格去除
                mdefs[-1][key] = np.array([float(x) for x in val.split(",")]).reshape((-1, 2))  # np anchors
                # np.array()转化为数组,float(x)是因为有小数转为浮点型
                # arr.reshape(-1,m) 改变维度为d行、m列 (-1表示行数自动计算,d= a*b /m ),d=1*18/2=9,所以就是9行2列,9组anchor

            elif (key in ["from", "layers", "mask"]) or (key == "size" and "," in val):
                mdefs[-1][key] = [int(x) for x in val.split(",")]
                # 以逗号分割,转化为整形
            else:  #不是上面两种情况那就是到了正常卷积的参数读取了
                # TODO: .isnumeric() actually fails to get the float case
                if val.isnumeric():  # 字母读取完,如果是数值的情况,val.isnumeric()判断是否是数字
                    mdefs[-1][key] = int(val) if (int(val) - float(val)) == 0 else float(val)
                    # 判断数字是整形还是字符型,如果整形 int(val) - float(val)就会=0,那么输入int(val),否则float(val)
                else:
                    mdefs[-1][key] = val  # return string  是字符的情况

    # check all fields are supported 字典中所有的key值
    supported = ['type', 'batch_normalize', 'filters', 'size', 'stride', 'pad', 'activation', 'layers', 'groups',
                 'from', 'mask', 'anchors', 'classes', 'num', 'jitter', 'ignore_thresh', 'truth_thresh', 'random',
                 'stride_x', 'stride_y', 'weights_type', 'weights_normalization', 'scale_x_y', 'beta_nms', 'nms_kind',
                 'iou_loss', 'iou_normalizer', 'cls_normalizer', 'iou_thresh', 'probability']

    # 遍历检查每个模型的配置
    for x in mdefs[1:]:  # 0对应net配置
        # 遍历每个配置字典中的key值
        for k in x:
            if k not in supported:
                # 判断所有的key是否在这个supported列表中
                raise ValueError("Unsupported fields:{} in cfg".format(k))

    return mdefs  # 最后返回我们解析好的网络参数


def parse_data_cfg(path):
    # Parses the data configuration file
    if not os.path.exists(path) and os.path.exists('data' + os.sep + path):  # add data/ prefix if omitted
        path = 'data' + os.sep + path

    with open(path, 'r') as f:
        lines = f.readlines()

    options = dict()
    for line in lines:
        line = line.strip()
        if line == '' or line.startswith('#'):
            continue
        key, val = line.split('=')
        options[key.strip()] = val.strip()

    return options

2、models.py 建立网络模型(模型搭建和正向传播)

解析好网络的参数,我们就要构建网络的模型了,这一部分的代码除了models.py,里面的有些函数在layes.py中构建的。

代码读取步骤

整个网络应该从class Darknet(nn.Module):开始看起,读到self.module_list, self.routs = create_modules(self.module_defs, img_size),我们要看一下create_modules()这个函数如何构造,跳转到这个函数。
当运行到判断type是routeshortcut这两种类型时,用到了FeatureConcat()WeightedFeatureFusion()这两个函数,它们的建立在layers.py中,跳转到这两个函数。
当运行到判断mdef["type"] == "yolo"是yolo的时候,这句程序modules = YOLOLayer(........,我们就要去看一下YOLOLayer是如何建立的了,再跳转到这个函数, 这个函数对YOLO的输出进行处理。
之后正常一句一句阅读create_modules()函数。最后返回了module_list, routs_binary 两个值,我们再回来class Darknet(nn.Module):中。正常阅读就可以了。
注意在最开始设置了ONNX_EXPORT = False,所以涉及到ONNX_EXPORT 的语句直接不看即可。

models.py 代码如下。

from build_utils.layers import *
from build_utils.parse_config import *

ONNX_EXPORT = False


def create_modules(modules_defs: list, img_size):
    # 两个参数 第一个为一个列表,后面用到这个函数时传入的就是解析网络结构的列表,第二个参数为图片大小
    """
    Constructs module list of layer blocks from module configuration in module_defs
    :param modules_defs: 通过.cfg文件解析得到的每个层结构的列表
    :param img_size:
    :return:
    """

    img_size = [img_size] * 2 if isinstance(img_size, int) else img_size
    # pop(0)删除解析cfg列表中的第一个配置(对应[net]的配置)
    modules_defs.pop(0)  # cfg training hyperparams (unused)
    output_filters = [3]  # 记录接下来搭建每个模块时所输出特征矩阵的channel,列表中放3,是因为RGB图像通道为3
    module_list = nn.ModuleList()   # 之后每一个模块都会传入到nn.ModuleList()中
    # routs()统计哪些特征层的输出会被后续的层使用到(可能是特征融合,也可能是拼接) 这里不懂得话 后面用到就懂了
    routs = []  # list of layers which rout to deeper layers
    yolo_index = -1

    # 遍历搭建每个层结构
    for i, mdef in enumerate(modules_defs):
        # i为索引 mdef为信息
        modules = nn.Sequential()
        # 模块中的信息存入到Sequential中
        if mdef["type"] == "convolutional":
            bn = mdef["batch_normalize"]  # 1 or 0 / use or not
            filters = mdef["filters"]
            k = mdef["size"]  # kernel size
            stride = mdef["stride"] if "stride" in mdef else (mdef['stride_y'], mdef["stride_x"])  #此时就是stride而已
            if isinstance(k, int):   #isinstance()判断一个函数是否是一个已知的类型,此时判断K卷积核大小是否为整数
                # add_module这个函数可以在A.init(self)以外定义A的子模块,加一个卷积层
                modules.add_module("Conv2d", nn.Conv2d(in_channels=output_filters[-1],  # 如果是第一个卷积层,那么此时就为3
                                                       out_channels=filters,            #卷积核个数
                                                       kernel_size=k,
                                                       stride=stride,
                                                       padding=k // 2 if mdef["pad"] else 0,
                                                       bias=not bn))                    # 使用bn bias为flase 不使用为ture
            else:
                raise TypeError("conv2d filter size must be int type.")

            if bn:
                modules.add_module("BatchNorm2d", nn.BatchNorm2d(filters))   #使用bn,就再加入一个bn层,输入=上一层输出filters
            else:
                # 如果该卷积操作没有bn层,意味着该层为yolo的predictor
                routs.append(i)  # detection output (goes into yolo layer)  记录一个索引

            if mdef["activation"] == "leaky":  #除了三个predictor对应的卷积是linear 其他都是leaky,全局看就是在卷积的基础上看里面的激活函数是什么
                modules.add_module("activation", nn.LeakyReLU(0.1, inplace=True))
            else:
                pass

        elif mdef["type"] == "BatchNorm2d":
            pass

        elif mdef["type"] == "maxpool":   #只有spp结构使用maxpool
            k = mdef["size"]  # kernel size
            stride = mdef["stride"]
            modules = nn.MaxPool2d(kernel_size=k, stride=stride, padding=(k - 1) // 2)

        elif mdef["type"] == "upsample":
            if ONNX_EXPORT:  # explicitly state size, avoid scale_factor
                g = (yolo_index + 1) * 2 / 32  # gain
                modules = nn.Upsample(size=tuple(int(x * g) for x in img_size))
            else:
                modules = nn.Upsample(scale_factor=mdef["stride"])  #上采样在文件中也已经定义好

        elif mdef["type"] == "route":  # [-2],  [-1,-3,-5,-6], [-1, 61]
            layers = mdef["layers"]
            filters = sum([output_filters[l + 1 if l > 0 else l] for l in layers])
            #  filters定义为当前层输出特征矩阵深度,l遍历layers,l>0存入l+1,因为output_filters中已经存在一个元素3,它的索引为0,小于0直接写入,sum求和
            routs.extend([i + l if l < 0 else l for l in layers])
            # extend和append的区别就在于,append添加后不改变添加项的类型,之前是一个列表添加进去之后还是一个列表,extend则会存储到routs这个列表中变为其中的元素
            # 此时当前层为i,l小小于0,就说明不是此时的模块,就要倒数回去所以l+i,如果l大于0就是l本身的那一层,这个l应该是layer的值了
            # 举个例子此时i=18 layers=[-1, 61],经过上面操作 routs=[18,17, 61]
            modules = FeatureConcat(layers=layers)
            # FeatureConcat将多个特征矩阵在channel维度进行concate拼接,在layers.py写了这个函数的结构

        elif mdef["type"] == "shortcut":
            layers = mdef["from"]   #from代表与前面那一层连接 其实就是layers的意思
            filters = output_filters[-1]  # 上一层输出channel
            # routs.extend([i + l if l < 0 else l for l in layers])
            routs.append(i + layers[0])
            # 记录一下现在是哪一层的输出
            modules = WeightedFeatureFusion(layers=layers, weight="weights_type" in mdef)
            # 这个是输出特征矩阵相加,在layers.py写了这个函数的结构
        elif mdef["type"] == "yolo":
            yolo_index += 1  # 记录是第几个yolo_layer [0, 1, 2],这里加1是因为上面设置的为-1,本结构一共有3个yolo所以最后是0 1 2
            stride = [32, 16, 8]  # 预测特征层对应原图的缩放比例

            modules = YOLOLayer(anchors=mdef["anchors"][mdef["mask"]],  # 一共9组anchor使用哪一个 由mask决定
                                nc=mdef["classes"],  # number of classes
                                img_size=img_size,
                                stride=stride[yolo_index])   #每一个预测特征层有相应的stride

            # Initialize preceding Conv2d() bias (https://arxiv.org/pdf/1708.02002.pdf section 3.3)
            try:   #这里就是yolo层的上一层预测层,j=-1的意思,简单看一下
                j = -1
                # bias: shape(255,) 索引0对应Sequential中的Conv2d
                # view: shape(3, 85)
                b = module_list[j][0].bias.view(modules.na, -1)
                b.data[:, 4] += -4.5  # obj
                b.data[:, 5:] += math.log(0.6 / (modules.nc - 0.99))  # cls (sigmoid(p) = 1/nc)
                module_list[j][0].bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
            except Exception as e:
                print('WARNING: smart bias initialization failure.', e)
        else:
            print("Warning: Unrecognized Layer Type: " + mdef["type"])

        # Register module list and number of output filters
        module_list.append(modules)    # 将上面的modules都放在module_list中,上面module_list = nn.ModuleList(),ModuleList是Module的子类
        # 当添加nn.ModuleList作为nn.Module对象的一个成员时(即当我们添加模块到我们的网络时),所有nn.ModuleList内部的nn.Module的parameter也被添加作为我们网络的parameter.
        output_filters.append(filters)  #也一样每个模块的filters存入output_filters中

    routs_binary = [False] * len(modules_defs)   #建立一个列表 modules_defs有几个这个false就有多少
    for i in routs:
        routs_binary[i] = True   # routs记录的是一个索引,在有索引的地方设置为ture
    return module_list, routs_binary    # 回到了 Darknet中create_modules


class YOLOLayer(nn.Module):
    """
    对YOLO的输出进行处理
    """
    def __init__(self, anchors, nc, img_size, stride):   #nc为类别
        super(YOLOLayer, self).__init__()
        self.anchors = torch.Tensor(anchors)      #之前的anchor为numpy格式转为tensor格式
        self.stride = stride  # layer stride 特征图上一步对应原图上的步距 [32, 16, 8]
        self.na = len(anchors)  # number of anchors (3)  anchor的数量就是3
        self.nc = nc  # number of classes (80)  类别
        self.no = nc + 5  # 每个anchor预测的参数80+5 (85: x, y, w, h, obj, cls1, ...)
        self.nx, self.ny, self.ng = 0, 0, (0, 0)  # nx ny是预测特征层宽度和高度,ng预测特征层size 初始化都为0
        self.anchor_vec = self.anchors / self.stride    # 将anchors大小缩放到grid预测特征层上的尺度
        # batch_size, na, grid_h, grid_w, wh,
        # 值为1的维度对应的值不是固定值,后续操作可根据broadcast广播机制自动扩充,就是会随着输入数据不同而变化
        self.anchor_wh = self.anchor_vec.view(1, self.na, 1, 1, 2)  #缩放后的anchor进行处理,调整视图,得到了anchor相对于grid cell的宽和高就是pw和ph
        self.grid = None  #正向传播中重新赋值

        if ONNX_EXPORT:
            self.training = False
            self.create_grids((img_size[1] // stride, img_size[0] // stride))  # number x, y grid points

    def create_grids(self, ng=(13, 13), device="cpu"):
        """
        更新grids信息并生成新的grids参数
        :param ng: 特征图大小
        :param device:
        :return:
        """
        self.nx, self.ny = ng     # 特征图的宽和高
        self.ng = torch.tensor(ng, dtype=torch.float)   #转换为tensor赋值给ng

        # build xy offsets 构建每个cell处的anchor的xy偏移量(在feature map上的)
        if not self.training:  # 训练模式不需要回归到最终预测boxes,训练时只需要计算损失即可,不需要把这个框预测出来
            yv, xv = torch.meshgrid([torch.arange(self.ny, device=device),
                                     torch.arange(self.nx, device=device)])
            # arange产生的是由1-9组成的1维度张量 ,类型int
            # meshgrid得到以左上角为原点的x的坐标和y坐标分别赋值给yv和xv
            # batch_size, na, grid_h, grid_w, wh
            self.grid = torch.stack((xv, yv), 2).view((1, 1, self.ny, self.nx, 2)).float()
            # stack对xv和xy进行拼接,就是变成一个个(x,y),通过view调整视图
        if self.anchor_vec.device != device:        #判断设备类型是否一样
            self.anchor_vec = self.anchor_vec.to(device)
            self.anchor_wh = self.anchor_wh.to(device)

    def forward(self, p):  #p是预测的参数,,包括batch_size,anchor带下,图片种类,置信度一系列参数
        if ONNX_EXPORT:
            bs = 1  # batch size
        else:
            bs, _, ny, nx = p.shape  # 获取p的shape,赋值给batch_size, predict_param(255)不会用到所以_, grid(13)高, grid(13)宽
            if (self.nx, self.ny) != (nx, ny) or self.grid is None:  # 判断输入特征矩阵的宽和高是否发生变化或者说grid为none就是第一次开始就要设置grid的参数
                self.create_grids((nx, ny), p.device)   #设置grid 13*13大小

        # view: (batch_size, 255, 13, 13) -> (batch_size, 3, 85, 13, 13)
        # permute: (batch_size, 3, 85, 13, 13) -> (batch_size, 3, 13, 13, 85)
        # [bs, anchor, grid, grid, xywh + obj + classes]
        p = p.view(bs, self.na, self.no, self.ny, self.nx).permute(0, 1, 3, 4, 2).contiguous()  #  调整视图 将之前的_变为anchor的个数3和预测的参数85
        # permute调整每个参数的位置 见上面注释,改变参数位置之后在原有内存中这些参数不再连续,就使用contiguous()方法让他们连续
        if self.training: #训练模式直接返回我们处理好的p
            return p
        elif ONNX_EXPORT:
            # Avoid broadcasting for ANE operations
            m = self.na * self.nx * self.ny  # 3*
            ng = 1. / self.ng.repeat(m, 1)
            grid = self.grid.repeat(1, self.na, 1, 1, 1).view(m, 2)
            anchor_wh = self.anchor_wh.repeat(1, 1, self.nx, self.ny, 1).view(m, 2) * ng

            p = p.view(m, self.no)
            # xy = torch.sigmoid(p[:, 0:2]) + grid  # x, y
            # wh = torch.exp(p[:, 2:4]) * anchor_wh  # width, height
            # p_cls = torch.sigmoid(p[:, 4:5]) if self.nc == 1 else \
            #     torch.sigmoid(p[:, 5:self.no]) * torch.sigmoid(p[:, 4:5])  # conf
            p[:, :2] = (torch.sigmoid(p[:, 0:2]) + grid) * ng  # x, y
            p[:, 2:4] = torch.exp(p[:, 2:4]) * anchor_wh  # width, height
            p[:, 4:] = torch.sigmoid(p[:, 4:])
            p[:, 5:] = p[:, 5:self.no] * p[:, 4:5]
            return p
        else:  # inference验证时
            # [bs, anchor, grid, grid, xywh + obj + classes]
            io = p.clone()  # inference output   将p克隆给io
            io[..., :2] = torch.sigmoid(io[..., :2]) + self.grid  # xy 计算在feature map上的中心点xy坐标
            # ...是bs, anchor, grid, grid这些参数,:2是取最后的85个参数中的前两个也就是x和y,对xy进行sigmoid处理在+之前左上方grid网格坐标就得到xy坐标了
            io[..., 2:4] = torch.exp(io[..., 2:4]) * self.anchor_wh  # 2:4意味着 wh这两个参数,对应公式e的tw次方和e的th次方,计算anchor的宽和高
            io[..., :4] *= self.stride  # 换算映射回原图尺度
            torch.sigmoid_(io[..., 4:])  #将置信度和后面的类别信息sigmoid函数处理,定义在0,1之间
            return io.view(bs, -1, self.no), p  # view [1, 3, 13, 13, 85] as [1, 507, 85]   -1自动处理机制 13*13是一个举例
            #回到YOLOLayer

class Darknet(nn.Module):
    """
    YOLOv3 spp object detection model
    """
    def __init__(self, cfg, img_size=(416, 416), verbose=False):
        # 有三个参数,cfg为参数配置文件,img_size输入图像大小,训练时未用到这个参数,verbose是否打印模型每个模块详细信息
        super(Darknet, self).__init__()
        # 这里传入的img_size只在导出ONNX模型时起作用,isinstance()判断一个函数是否是一个已知的类型
        self.input_size = [img_size] * 2 if isinstance(img_size, int) else img_size
        # 解析网络对应的.cfg文件,parse_model_cfg函数是parse_config.py文件中定义的解析网络的函数。
        self.module_defs = parse_model_cfg(cfg)
        # 根据解析的网络结构通过create_modules()完成网络结构建立,返回的值传递给module_list,routs为索引
        self.module_list, self.routs = create_modules(self.module_defs, img_size)
        # 获取所有YOLOLayer层的索引
        self.yolo_layers = get_yolo_layers(self)

        # 打印下模型的信息,如果verbose为True则打印详细信息,上面设置为false了
        self.info(verbose) if not ONNX_EXPORT else None  # print model description

    def forward(self, x, verbose=False):
        return self.forward_once(x, verbose=verbose)

    def forward_once(self, x, verbose=False):
        # yolo_out收集每个yolo_layer层的输出
        # out收集每个模块的输出
        yolo_out, out = [], []
        if verbose:
            print('0', x.shape)
            str = ""

        for i, module in enumerate(self.module_list):
            name = module.__class__.__name__
            if name in ["WeightedFeatureFusion", "FeatureConcat"]:  # sum, concat
                if verbose:
                    l = [i - 1] + module.layers  # layers
                    sh = [list(x.shape)] + [list(out[i].shape) for i in module.layers]  # shapes
                    str = ' >> ' + ' + '.join(['layer %g %s' % x for x in zip(l, sh)])
                x = module(x, out)  # WeightedFeatureFusion(), FeatureConcat() 这里解释一下为什么写成module(x, out)
                                    # 可以在回顾一下WeightedFeatureFusion和FeatureConcat的正向传播过程,就是有两个参数x和output
            elif name == "YOLOLayer":
                yolo_out.append(module(x))
            else:  # run module directly, i.e. mtype = 'convolutional', 'upsample', 'maxpool', 'batchnorm2d' etc.
                x = module(x)

            out.append(x if self.routs[i] else [])
            if verbose:
                print('%g/%g %s -' % (i, len(self.module_list), name), list(x.shape), str)
                str = ''

        if self.training:  # train
            return yolo_out
        elif ONNX_EXPORT:  # export
            # x = [torch.cat(x, 0) for x in zip(*yolo_out)]
            # return x[0], torch.cat(x[1:3], 1)  # scores, boxes: 3780x80, 3780x4
            p = torch.cat(yolo_out, dim=0)

            # # 根据objectness虑除低概率目标
            # mask = torch.nonzero(torch.gt(p[:, 4], 0.1), as_tuple=False).squeeze(1)
            # # onnx不支持超过一维的索引(pytorch太灵活了)
            # # p = p[mask]
            # p = torch.index_select(p, dim=0, index=mask)
            #
            # # 虑除小面积目标,w > 2 and h > 2 pixel
            # # ONNX暂不支持bitwise_and和all操作
            # mask_s = torch.gt(p[:, 2], 2./self.input_size[0]) & torch.gt(p[:, 3], 2./self.input_size[1])
            # mask_s = torch.nonzero(mask_s, as_tuple=False).squeeze(1)
            # p = torch.index_select(p, dim=0, index=mask_s)  # width-height 虑除小目标
            #
            # if mask_s.numel() == 0:
            #     return torch.empty([0, 85])

            return p
        else:  # inference or test
            x, p = zip(*yolo_out)  # inference output, training output
            # zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。
            x = torch.cat(x, 1)  # 最后的预测结果进行拼接

            return x, p

    def info(self, verbose=False):
        """
        打印模型的信息
        :param verbose:
        :return:
        """
        torch_utils.model_info(self, verbose)


def get_yolo_layers(self):
    """
    获取网络中三个"YOLOLayer"模块对应的索引
    :param self:
    :return:
    """
    return [i for i, m in enumerate(self.module_list) if m.__class__.__name__ == 'YOLOLayer']  # [89, 101, 113]```

layes.py部分代码如下

class FeatureConcat(nn.Module):
    """
    将多个特征矩阵在channel维度进行concatenate拼接
    """
    def __init__(self, layers):
        super(FeatureConcat, self).__init__()
        self.layers = layers  # layer indices
        self.multiple = len(layers) > 1  # 如果layers中是多个参数 multiple返回ture

    def forward(self, x, outputs):
        return torch.cat([outputs[i] for i in self.layers], 1) if self.multiple else outputs[self.layers[0]]
        # C=torch.cat((A,B),1)就表示按维数1(列)拼接A和B,也就是横着拼接,A左B右
        # 如果是多个参数就将通道拼接在一起 否则就是那个通道
        # output在Darknet中建立
class WeightedFeatureFusion(nn.Module):  # weighted sum of 2 or more layers https://arxiv.org/abs/1911.09070
    """
    将多个特征矩阵的值进行融合(add操作)
    """
    def __init__(self, layers, weight=False):
        super(WeightedFeatureFusion, self).__init__()
        self.layers = layers  # layer indices
        self.weight = weight  # apply weights boolean
        self.n = len(layers) + 1  # number of layers 融合的特征矩阵个数 两个矩阵融合
        if weight:
            self.w = nn.Parameter(torch.zeros(self.n), requires_grad=True)  # layer weights

    def forward(self, x, outputs):
        # Weights
        if self.weight:
            w = torch.sigmoid(self.w) * (2 / self.n)  # sigmoid weights (0-1)
            x = x * w[0]

        # Fusion
        nx = x.shape[1]  # input channels
        for i in range(self.n - 1):  #两个融合其实n=2 n-1=1,就循环一次
            a = outputs[self.layers[i]] * w[i + 1] if self.weight else outputs[self.layers[i]]  # feature to add
            na = a.shape[1]  # feature channels

            # Adjust channels
            # 根据相加的两个特征矩阵的channel选择相加方式
            if nx == na:  # same shape 如果channel相同,直接相加 只会用到这种情况
                x = x + a
            elif nx > na:  # slice input 如果channel不同,将channel多的特征矩阵砍掉部分channel保证相加的channel一致
                x[:, :na] = x[:, :na] + a  # or a = nn.ZeroPad2d((0, 0, 0, 0, 0, dc))(a); x = x + a
            else:  # slice feature
                x = x + a[:, :nx]

        return x


class MixConv2d(nn.Module):  # MixConv: Mixed Depthwise Convolutional Kernels https://arxiv.org/abs/1907.09595
    def __init__(self, in_ch, out_ch, k=(3, 5, 7), stride=1, dilation=1, bias=True, method='equal_params'):
        super(MixConv2d, self).__init__()

        groups = len(k)
        if method == 'equal_ch':  # equal channels per group
            i = torch.linspace(0, groups - 1E-6, out_ch).floor()  # out_ch indices
            ch = [(i == g).sum() for g in range(groups)]
        else:  # 'equal_params': equal parameter count per group
            b = [out_ch] + [0] * groups
            a = np.eye(groups + 1, groups, k=-1)
            a -= np.roll(a, 1, axis=1)
            a *= np.array(k) ** 2
            a[0] = 1
            ch = np.linalg.lstsq(a, b, rcond=None)[0].round().astype(int)  # solve for equal weight indices, ax = b

        self.m = nn.ModuleList([nn.Conv2d(in_channels=in_ch,
                                          out_channels=ch[g],
                                          kernel_size=k[g],
                                          stride=stride,
                                          padding=k[g] // 2,  # 'same' pad
                                          dilation=dilation,
                                          bias=bias) for g in range(groups)])

    def forward(self, x):
        return torch.cat([m(x) for m in self.m], 1)

希望大家批评指正,不喜勿喷,下面我会根据@太阳花的小绿豆B站的视频顺序,更新关于数据读取和预处理的代码,也就是utils文件下datasets.py文件。

猜你喜欢

转载自blog.csdn.net/JiatongForever/article/details/125973726