• 从零开始PytorchYOLOv3【笔记】(二)解析配置文件


    前言

    上一篇:从零开始Pytorch-YOLOv3【笔记】(一)配置文件解读

    下面是解析配置文件和生成model。对应从零开始PyTorch项目:YOLO v3目标检测实现中的第二部分 创建 YOLO 网络层级

    解析配置文件

    创建 Python 文件 darknet.py。darknet.py 是构建 YOLO 底层架构的环境,这个文件将包含实现 YOLO 网络的所有代码。

    darknet.py中有如下函数和类:

    def parse_cfg(cfgfile):解析配置文件。该函数使用配置文件的路径作为输入,返回一个blocks。

    点击查看代码
    def parse_cfg(cfgfile):
        '''
        Args:
          cfgfile(str):cfg文件路径
    
        Return:
          blocks = [block, block, ..., block]
          block = {
          "type": "convolutional",
          "batch_normalize": "1",
          "filters": "32",
          "size": "3",
          "stride": "1",
          "pad": "1",
          "activation": "leaky"
          }
        '''
        # 首先将配置文件内容保存在字符串列表中。下面的代码对该列表执行预处理:
        file = open(cfgfile, 'r')
        lines = file.read().split('\n')
        lines = [x for x in lines if len(x) > 0]
        lines = [x for x in lines if x[0] != '#']
        lines = [x.strip() for x in lines]  # 原代码是lines = [x.rstrip().lstrip() for x in lines],不知道为什么不直接用strip()
    
        # 然后,我们遍历预处理后的列表,得到块。
        block = {}
        blocks = []
    
        for line in lines:
            if line[0] == "[":  # This marks the start of a new block
                if len(block) != 0:  # If block is not empty, implies it is storing values of previous block.在读到下一个
                    blocks.append(block)  # add it the blocks list
                    block = {}  # re-init the block
                block["type"] = line[1:-1].rstrip()  # block = {}时,先写入type,
            else:
                key, value = line.split("=")
                block[key.rstrip()] = value.lstrip()
        blocks.append(block)
    
        return blocks
    

    def create_modules(blocks):根据parse_cfg返回的blocks来构建pytorch模块。
    列表中有 5 种类型的层。PyTorch 为 convolutional 和 upsample 提供预置层。我们将通过扩展 nn.Module 类为其余层写自己的模块。

    点击查看代码
    def create_modules(blocks):
        '''
        根据cfg返回的block来构建pytorch模块
        '''
        net_info = blocks[0]     # 存储该网络的信息
        module_list = nn.ModuleList()
        prev_filters = 3  # 预定义卷积核的数量为3,因为初始输入图像为RGB图像,channel=3。卷积核的数量决定了这一层输出feature map的深度。
        output_filters = []  # 将每个模块的输出卷积核数量添加到 output_filters 列表上。
        '''
        路由层(route layer)从前面层得到特征图(可能是拼接的)。如果在路由层之后有一个卷积层,那么卷积核将被应用到前面层的特征图上,精确来说是路由层得到的特征图。
        因此,我们不仅需要追踪前一层的卷积核数量,还需要追踪之前每个层。随着不断地迭代,我们将每个模块的输出卷积核数量添加到 output_filters 列表上。
        '''
        for index, x in enumerate(blocks[1:]):  # enumerate,将一组数据带索引[a,b,c] -> [(0,a), (1,b), (2,c)]
            '''
            遍历后续的模块([net]之后的block)
            '''
            module = nn.Sequential()
    
            #check the type of block
            #create a new module for the block
            #append to module_list
    
            # //---有以下几种block,依次添加到model中。
            # if convolutional layer
    
            # elif upsampling layer
            
            # elif route layer
    
            # elif shortcut corresponds to skip connection
    
            # elif Yolo is the detection layer
    
            # 在这个回路结束时,我们做了一些统计(bookkeeping.)。
            module_list.append(module)
            prev_filters = filters
            output_filters.append(filters)
        
        # 这总结了此回路的主体。在 create_modules 函数后,我们获得了包含 net_info 和 module_list 的元组。
        return (net_info, module_list)
    

    上面用到了两个pytorch方法module = nn.ModuleList()module.add_module("conv_{0}".format(index), conv)我们使用 nn.Sequential 将这些层串联起来,得到 add_module 函数。

    nn.Sequential允许我们构建序列化的模块。就把Sequential当作list来看。也就是说用了Sequential的好处是我们可以通过数字访问第几层,可以通过parameters、weights等参数显示网络的参数和权重

    参考文章:Pytorch —— nn.Module类(nn.sequential)

    卷积层

    # convolutional layer
    if (x["type"] == "convolutional"):
      #Get the info about the layer
      activation = x["activation"]
      try:
          batch_normalize = int(x["batch_normalize"])
          bias = False
      except:
          batch_normalize = 0
          bias = True
    
      filters= int(x["filters"])
      padding = int(x["pad"])
      kernel_size = int(x["size"])
      stride = int(x["stride"])
    
      if padding:  # 这里的padding在配置文件里有说明:pad=1时,pad = (kernel_size - 1) // 2,pad=0时,pad取默认值0
          pad = (kernel_size - 1) // 2
      else:
          pad = 0
    
      #Add the convolutional layer
      conv = nn.Conv2d(prev_filters, filters, kernel_size, stride, pad, bias = bias)
      module.add_module("conv_{0}".format(index), conv)
    
      #Add the Batch Norm Layer
      if batch_normalize:
          bn = nn.BatchNorm2d(filters)
          module.add_module("batch_norm_{0}".format(index), bn)
    
      #Check the activation. 
      #It is either Linear or a Leaky ReLU for YOLO
      if activation == "leaky":
          activn = nn.LeakyReLU(0.1, inplace = True)
          module.add_module("leaky_{0}".format(index), activn)
    

    上采样层

    对于backbone后面的分支,假设其网格为13×13,那么要和26×26的的分支进行级联,那么必须进行上采样,即Upsample操作。

    # upsampling layer
    #We use Bilinear2dUpsampling  这里注释说用的双线性上采样,意思应该是model = 'bilinear',但其实还是用的默认的'nearest'???
    elif (x["type"] == "upsample"):
      stride = int(x["stride"])
      upsample = nn.Upsample(scale_factor = 2, mode = "nearest")
      module.add_module("upsample_{}".format(index), upsample)
    

    路由层

    路由层的作用是获取之前层的拼接。

    它的参数 layers 有一个或两个值。
    当只有一个值时,它输出这一层通过该值索引的特征图。在我们的实验中设置为了-4,所以层级将输出路由层之前第四个层的特征图。
    当层级有两个值时,它将返回由这两个值索引的拼接特征图。在我们的实验中为-1 和 61,因此该层级将输出由前一层级(-1)和第 61 层的特征图将它们按深度拼接后的特征图。

    在路由层之后的卷积层会把它的卷积核应用到之前层的特征图(可能是拼接的)上。以下的代码更新了 filters 变量以保存路由层输出的卷积核数量。

    这里可能是拼接的是指:layer只有一个值的时候就是获取之前的某一层输出的特征图,layer两个值时就是拼接的特征图。

    #If it is a route layer
    elif (x["type"] == "route"):
        x["layers"] = x["layers"].split(',')
        #Start  of a route
        start = int(x["layers"][0])
        #end, if there exists one.
        try:
            end = int(x["layers"][1])
        except:
            end = 0
        #Positive anotation
        if start > 0: 
            start = start - index
        if end > 0:
            end = end - index
        route = EmptyLayer()
        module.add_module("route_{0}".format(index), route)
        if end < 0:
            filters = output_filters[index + start] + output_filters[index + end]
        else:
            filters= output_filters[index + start]
    

    捷径层(跳跃连接)

    捷径层执行一个非常简单的操作(加)。没必要更新 filters 变量,因为它只是将前一层的特征图添加到后面的层上而已。

    #shortcut corresponds to skip connection
    elif x["type"] == "shortcut":
        shortcut = EmptyLayer()
        module.add_module("shortcut_{}".format(index), shortcut)
    

    EmptyLayer

    Route layer,shortcut layer都使用了route = EmptyLayer()

    TODO:...关于EmptyLayer的理解。
    简要来说就是nn.Model_list()对象中的元素必须为nn.Module的子类,而Route layer,shortcut layer操作都不是一个nn.Module,所以使用EmptyLayer来站位。

    EmptyLayer,因为整个backbone模型被整合成了一个nn.Model_list()对象,而route模块做的仅仅是级联(torch.cat操作),shortcut模块做的仅仅是将不同模块的结果进行相加,它们的共同特点是:未必需要上一个模块的输出,但需要上好几个模块的输出,至于需要的是第几个模块,一个模块还是多个模块,这个暂时无法知道,所以直接在create_modules方法中,很难实习这个功能。
    这个问题暂时先不处理,而是先定义一个空层来占位,由于nn.ModuleList中的元素必须为nn.Module的子类,所以在models.py中,可以这么定义EmptyLayer

    class EmptyLayer(nn.Module):
        def __init__(self):
            super(EmptyLayer, self).__init__()
    

    YOLO层

    #Yolo is the detection layer
    elif x["type"] == "yolo":
        mask = x["mask"].split(",")
        mask = [int(x) for x in mask]
    
        anchors = x["anchors"].split(",")
        anchors = [int(a) for a in anchors]
        anchors = [(anchors[i], anchors[i+1]) for i in range(0, len(anchors),2)]  # range(strat, end, stride),这代码写的,优雅!
        anchors = [anchors[i] for i in mask]
    
        detection = DetectionLayer(anchors)
        module.add_module("Detection_{}".format(index), detection)
    

    DetectionLayer

    我们定义一个新的层 DetectionLayer 保存用于检测边界框的锚点。

    检测层的定义如下:

    class DetectionLayer(nn.Module):
        def __init__(self, anchors):
            super(DetectionLayer, self).__init__()
            self.anchors = anchors
    

    测试代码

    你可以在 darknet.py 后通过输入以下命令行测试代码,运行文件。

    blocks = parse_cfg("cfg/yolov3.cfg")
    print(create_modules(blocks))
    

    你会看到一个长列表(确切来说包含 106 条),其中元素看起来如下所示:

    我测试输出是(51)条。不知道是不是我数的方式不对。

     (9): Sequential(
        (conv_9): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (batch_norm_9): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (leaky_9): LeakyReLU(negative_slope=0.1, inplace=True)
      )
      (10): Sequential(
        (conv_10): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (batch_norm_10): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (leaky_10): LeakyReLU(negative_slope=0.1, inplace=True)
      )
      (11): Sequential(
        (shortcut_11): EmptyLayer()
      )
    
  • 相关阅读:
    模拟——1031D
    线性dp——cf1032
    莫比乌斯反演——专题练习
    数论,质因数,gcd——cf1033D 好题!
    连通图,set——cf1037E
    线段树动态开点——cf1045G
    跳表上线性dp——1150D 好题!
    高斯消元求主元——模意义下的消元cf1155E
    汽车长期停放,毁车没商量?
    驾校都是错的?这才是日常驾驶正确的换挡方式
  • 原文地址:https://www.cnblogs.com/daiSir/p/15990923.html
Copyright © 2020-2023  润新知