从零开始Pytorch-YOLOv3【笔记】(二)解析配置文件

前言

上一篇:从零开始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()
  )
posted @ 2022-03-11 16:42  攻城狮?  阅读(449)  评论(0编辑  收藏  举报