• yolov3详细讲解(一)


      Part1. models.py文件里的模型创建

      1.如何更方便的准备debug环境?

      我们选取的源码是github上5.7k star的 pytorch implementation

      项目源码地址

      下面我们从models.py文件入手。在讲源码的过程中采用了debug模式,这样可以更为深入的分析整个tensor数据流的变化。默认的数据集是coco数据集,完整下载要十几G,但是作者也留下了一个小的入口,方面大家debug。

      这个入口是一张图片,需要修改train.py文件里面的一行内容:

      parser.add_argument("--data_config", type=str, default="config/custom.data", help="path to data config file")

      也就是把coco数据集改为custom数据集,custom数据集里只包含一张图片,对应的GT labels里面只包含一个target。为了看清楚target的变换,我们再多加一个target,只需修改一下custom/labels里面的train.txt。

      D:pyprojectsobject-detection-code-debugPyTorch-YOLOv3datacustomlabels

      我多加了一个target,所以改成了:

      

    修改增加一个target

      同时大家也可能看到里面的数值是5列,分别对应了target的class,x, y中心坐标, target高宽h, w,注意这里都是对应于原图进行的归一化。

      修改完之后就可以跑train.py了。我们在models.py里面增加debug断点,便于分析源码,观察数据变化。

      2.parse_model_config()函数是如何加载配置文件的?

      首先最外层是models.py里面的Darknet Class。重点的代码是下面这段:

      class Darknet(nn.Module):

      """YOLOv3 object detection model"""

      def __init__(self, config_path, img_size=416):

      super(Darknet, self).__init__()

      self.module_defs = parse_model_config(config_path) # 得到list[dict()]类型的model配置信息表

      self.hyperparams, self.module_list = create_modules(self.module_defs)

      self.yolo_layers = [layer[0] for layer in self.module_list if isinstance(layer[0], YOLOLayer)]

      self.img_size = img_size

      self.seen = 0

      self.header_info = np.array([0, 0, 0, self.seen, 0], dtype=np.int32)

      def forward(self, x, targets=None):

      img_dim = x.shape[2] # 图像的尺寸

      loss = 0

      layer_outputs, yolo_outputs = [], []

      for i, (module_def, module) in enumerate(zip(self.module_defs, self.module_list)):

      if module_def["type"] in ["convolutional", "upsample", "maxpool"]:

      x = module(x)

      elif module_def["type"] == "route":

      # torch.cat 对单个tensor相当于保持原样

      x = torch.cat([layer_outputs[int(layer_i)] for layer_i in module_def["layers"].split(",")], 1)

      elif module_def["type"] == "shortcut":

      layer_i = int(module_def["from"])

      x = layer_outputs[-1] + layer_outputs[layer_i]

      elif module_def["type"] == "yolo":

      x, layer_loss = module[0](x, targets, img_dim)

      loss += layer_loss

      yolo_outputs.append(x)

      layer_outputs.append(x)

      yolo_outputs = to_cpu(torch.cat(yolo_outputs, 1))

      return yolo_outputs if targets is None else (loss, yolo_outputs)

      我们从init函数看起,首先是:

      self.module_defs = parse_model_config(config_path)

      这句调用了parse_model_config函数,那我们首先看一下这个函数:

      def parse_model_config(path):

      """通过cfg文件加载yolov3的配置,并存储到list[dict()]的结构中

      每一层以“[”作为标记的开始

      """

      file = open(path, 'r', encoding="utf-8")

      lines = file.read().split(' ')

      lines = [x for x in lines if x and not x.startswith('#')]

      lines = [x.rstrip().lstrip() for x in lines] # get rid of fringe whitespaces

      module_defs = []

      for line in lines:

      if line.startswith('['): # This marks the start of a new block

      module_defs.append({})

      module_defs[-1]['type'] = line[1:-1].rstrip()

      # 初始化batch-normal参数 配置文件里还是加载为1

      if module_defs[-1]['type'] == 'convolutional':

      module_defs[-1]['batch_normalize'] = 0

      else:

      key, value = line.split("=")

      value = value.strip()

      module_defs[-1][key.rstrip()] = value.strip()

      return module_defs

      这个函数的作用很明显,就是加载配置文件,然后把配置文件里面的模型结构解析出来。配置文件的形式大家在项目里也是可以找到的,结尾是cfg的文件。

      打开yolov3.cfg,再结合刚才的源码,很容易知道每个[]代表一个module的开始,每个module以一个dict()的形式去存放相关的参数,仔细观察cfg配置文件可以知晓module分为以下几类:

      [net]

      # Training

      batch=16

      subdivisions=1

      width=416

      ...

      [net]:模型的参数以及一些可配置的学习策略参数

      [convolutional]

      batch_normalize=1

      filters=32

      size=3

      stride=1

      pad=1

      activation=leaky

      [convolution]:卷积层,参数指定了常见的有卷积核的kernel size、padding、stride之类的。以及是否跟随BN层,是否跟随激活层。可以注意到默认的激活函数是leaky-relu。

      [shortcut]

      from=-3

      activation=linear

      [shortcut]:这个对应的是残差结构,from=-3指代的是从当前结果和哪个层的输出进行残差结构。-3就是从当前的结果回溯三层的输出。

      [yolo]

      mask = 6,7,8

      anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326

      classes=80

      num=9

      jitter=.3

      ignore_thresh = .7

      truth_thresh = 1

      random=1

      [yolo]:yolo对应的是一个模型的最终输出,因为yolov3采用的类似FPN结构的输出,所以有3个yolo layer。

      [route]

      layers = -4

      [route]:层指代的来自不同层的特征融合。是tensor在channel维度的叠加。因为存在FPN结构,所以yolov3 在多个地方进行了上采样+特征融合。

      [upsample]

      stride=2

      [unsample]:就比较简单了,就是2倍上采样。默认的是通过双线性插值的方式来进行的。

      3.怎样更方便的debug yolov3模型结构?

      这部分最好的debug方式就是调用parse_model_config()函数去把模型的每一层打印出来,我们在parse_config.py里面添加一个main函数,解析yolov3.cfg文件:

      if __name__ == '__main__':

      path = "../config/yolov3.cfg"

      res = parse_model_config(path)

      for i in range(len(res)):

      print(f"###layer{i}###")

      print(res[i])

      观察打印出的模型结构:

      ###layer0###

      {'type': 'net', 'batch': '16', 'subdivisions': '1', 'width': '416', 'height': '416', 'channels': '3', 'momentum': '0.9', 'decay': '0.0005', 'angle': '0', 'saturation': '1.5', 'exposure': '1.5', 'hue': '.1', 'learning_rate': '0.001', 'burn_in': '1000', 'max_batches': '500200', 'policy': 'steps', 'steps': '400000,450000', 'scales': '.1,.1'}

      ###layer1###

      {'type': 'convolutional', 'batch_normalize': '1', 'filters': '32', 'size': '3', 'stride': '1', 'pad': '1', 'activation': 'leaky'}

      ###layer2###

      {'type': 'convolutional', 'batch_normalize': '1', 'filters': '64', 'size': '3', 'stride': '2', 'pad': '1', 'activation': 'leaky'}

      ###layer3###

      {'type': 'convolutional', 'batch_normalize': '1', 'filters': '32', 'size': '1', 'stride': '1', 'pad': '1', 'activation': 'leaky'}

      ###layer4###

      {'type': 'convolutional', 'batch_normalize': '1', 'filters': '64', 'size': '3', 'stride': '1', 'pad': '1', 'activation': 'leaky'}

      ...

      ...

      ...

      ###layer107###

      {'type': 'yolo', 'mask': '0,1,2', 'anchors': '10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326', 'classes': '80', 'num': '9', 'jitter': '.3', 'ignore_thresh': '.7', 'truth_thresh': '1', 'random': '1'}

      可以看出总共有107个配置信息,第一个是模型的参数,里面有3个yolo layer的信息。这个配置文件不仅包含生成Darknet53的信息,其实包含了生成整个yolov3的信息。这种配置文件生成模型的方式可以快速调整模型结构,并且十分的清晰,值得学习。

      这部分很简单,作者想表达的意思是如果想快速并细致的观测模型结构,就必须把模型结构打印出来,并且对照模型结构图逐步观察,这样基本上一次就可以完全熟悉模型结构。下面是yolov3的结构图:

      这个图用来配合刚刚打印出来的模型配置信息是非常好的,可以清晰的明白哪里是route,哪里是普通卷积层,哪里是yolo layer。

      至此我们拿到了配置文件中每一个module的配置参数,这些module串联起来就可以生成darknet53的的结构,parse_model_config()函数最终得到的是list[dict()]类型的model配置信息表。下一步代码执行了如下内容:

      self.hyperparams, self.module_list = create_modules(self.module_defs)

      下一步我们就来解析create_modules()函数。

      4.create_modules()函数如何构建模型?

      首先上这部分的代码:

      def create_modules(module_defs):

      """

      Constructs module list of layer blocks from module configuration in module_defs

      """

      hyperparams = module_defs.pop(0) # 配置信息第一项是超参数

      output_filters = [int(hyperparams["channels"])] #记录当前操作输出channel 后面输入channel根据output_filters[-1]取

      module_list = nn.ModuleList()

      for module_i, module_def in enumerate(module_defs):

      modules = nn.Sequential() # 每一层构建单独的nn.Sequential() 最后再统一加到nn.MuduleList()里

      if module_def["type"] == "convolutional":

      bn = int(module_def["batch_normalize"])

      filters = int(module_def["filters"])

      kernel_size = int(module_def["size"])

      pad = (kernel_size - 1) // 2

      modules.add_module(

      f"conv_{module_i}",

      nn.Conv2d(

      in_channels=output_filters[-1],

      out_channels=filters,

      kernel_size=kernel_size,

      stride=int(module_def["stride"]),

      padding=pad,

      bias=not bn,

      ),

      )

      if bn:

      modules.add_module(f"batch_norm_{module_i}", nn.BatchNorm2d(filters, momentum=0.9, eps=1e-5))

      if module_def["activation"] == "leaky":

      modules.add_module(f"leaky_{module_i}", nn.LeakyReLU(0.1))

      elif module_def["type"] == "maxpool":

      kernel_size = int(module_def["size"])

      stride = int(module_def["stride"])

      if kernel_size == 2 and stride == 1:

      modules.add_module(f"_debug_padding_{module_i}", nn.ZeroPad2d((0, 1, 0, 1)))

      maxpool = nn.MaxPool2d(kernel_size=kernel_size, stride=stride, padding=int((kernel_size - 1) // 2))

      modules.add_module(f"maxpool_{module_i}", maxpool)

      elif module_def["type"] == "upsample":

      upsample = Upsample(scale_factor=int(module_def["stride"]), mode="nearest")

      modules.add_module(f"upsample_{module_i}", upsample)

      elif module_def["type"] == "route": # 对应feature maps间特征融合的结构 方式是concat

      layers = [int(x) for x in module_def["layers"].split(",")]

      filters = sum([output_filters[1:][i] for i in layers])

      modules.add_module(f"route_{module_i}", EmptyLayer())

      elif module_def["type"] == "shortcut": # 对应residual的结构 方式是add

      filters = output_filters[1:][int(module_def["from"])]

      modules.add_module(f"shortcut_{module_i}", EmptyLayer())

      elif module_def["type"] == "yolo":

      anchor_idxs = [int(x) for x in module_def["mask"].split(",")]

      # Extract anchors

      anchors = [int(x) for x in module_def["anchors"].split(",")]

      anchors = [(anchors[i], anchors[i + 1]) for i in range(0, len(anchors), 2)]

      anchors = [anchors[i] for i in anchor_idxs]

      num_classes = int(module_def["classes"])

      img_size = int(hyperparams["height"])

      # Define detection layer

      yolo_layer = YOLOLayer(anchors, num_classes, img_size)

      modules.add_module(f"yolo_{module_i}", yolo_layer)

      # Register module list and number of output filters

      module_list.append(modules)

      output_filters.append(filters)

      return hyperparams, module_list

      这部分还是不难理解的。

      hyperparams = module_defs.pop(0)

      这里拿到了模型和学习策略的配置信息。

      for module_i, module_def in enumerate(module_defs):

      ...

      这里开始循环加载模型结构。

      if module_def["type"] == "convolutional":

      bn = int(module_def["batch_normalize"])

      filters = int(module_def["filters"])

      kernel_size = int(module_def["size"])

      pad = (kernel_size - 1) // 2

      modules.add_module(

      f"conv_{module_i}",

      nn.Conv2d(

      in_channels=output_filters[-1],

      out_channels=filters,

      kernel_size=kernel_size,

      stride=int(module_def["stride"]),

      padding=pad,

      bias=not bn,

      ),

      )

      if bn:

      modules.add_module(f"batch_norm_{module_i}", nn.BatchNorm2d(filters, momentum=0.9, eps=1e-5))

      if module_def["activation"] == "leaky":

      modules.add_module(f"leaky_{module_i}", nn.LeakyReLU(0.1))

      conv层都默认加了BN,激活函数默认是leaky-relu。这些都是可以调整的,作者在很多竞赛中发现leaky-relu不一定效果会比普通的relu好。

      elif module_def["type"] == "maxpool":枣庄人流医院哪家好 http://mobile.0632-3679999.com/

      kernel_size = int(module_def["size"])

      stride = int(module_def["stride"])

      if kernel_size == 2 and stride == 1:

      modules.add_module(f"_debug_padding_{module_i}", nn.ZeroPad2d((0, 1, 0, 1)))

      maxpool = nn.MaxPool2d(kernel_size=kernel_size, stride=stride, padding=int((kernel_size - 1) // 2))

      modules.add_module(f"maxpool_{module_i}", maxpool)

      elif module_def["type"] == "upsample":

      upsample = Upsample(scale_factor=int(module_def["stride"]), mode="nearest")

      modules.add_module(f"upsample_{module_i}", upsample)

      maxpool是下采样,upsample是上采样,这里都是2倍。但是实际上yolov3里面下采样用的是stride=2的卷积并没有使用maxpool。stride=2的卷积相对于maxpool保留了更多的信息,在一些论文里都有替代maxpool的作用,另外用stride conv下采样的时候也可以考虑大一点的卷积核,但这些都不是本文重点,提一句就此略过。

      elif module_def["type"] == "route": # 对应feature maps间特征融合的结构 方式是concat

      layers = [int(x) for x in module_def["layers"].split(",")]

      filters = sum([output_filters[1:][i] for i in layers])

      modules.add_module(f"route_{module_i}", EmptyLayer())

      elif module_def["type"] == "shortcut": # 对应residual的结构 方式是add

      filters = output_filters[1:][int(module_def["from"])]

      modules.add_module(f"shortcut_{module_i}", EmptyLayer())

      route和shortcut对应的分别是tensor的拼接和残差结构。EmptyLayer()是一个空层,啥也不做,具体的操作都是在Darknet的forward()函数里才去执行具体的操作。

      filters = sum([output_filters[1:][i] for i in layers])

      filters = output_filters[1:][int(module_def["from"])]

      filters都是在计算相应操作之后的channel数量。所以一个是sum所有的channel的concat,一个是channel保持不变的element-wise 加法。

      elif module_def["type"] == "yolo":

      anchor_idxs = [int(x) for x in module_def["mask"].split(",")]

      # Extract anchors

      anchors = [int(x) for x in module_def["anchors"].split(",")]

      anchors = [(anchors[i], anchors[i + 1]) for i in range(0, len(anchors), 2)]

      anchors = [anchors[i] for i in anchor_idxs]

      num_classes = int(module_def["classes"])

      img_size = int(hyperparams["height"])

      # Define detection layer

      yolo_layer = YOLOLayer(anchors, num_classes, img_size)

      modules.add_module(f"yolo_{module_i}", yolo_layer)

      yolo layer会加载YOLOLayer() module。在观察一下cfg文件里的配置信息:

      [yolo]

      mask = 0,1,2

      anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326

      classes=80

      num=9

      jitter=.3

      ignore_thresh = .7

      truth_thresh = 1

      random=1

      mask代表的是第几个anchor,比如对于这个yolo head就只采用(10,13)、(16,30)、(33,23)这三个anchor信息。我们都知道yolov3采用FPN结构作为输出的结果,拥有三个yolo head,每个head预先设置了3个不同大小的anchor,三个head分别负责大、中、小三类物体。更为具体的信息我们会在后面解析yolo layer源码的时候再分享。

      5.Darknet的前向传播过程?

      讲完create_modules()函数,我们再回到Darknet Class,剩余的__init__()里面的部分:

      def __init__(self, config_path, img_size=416):

      super(Darknet, self).__init__()

      self.module_defs = parse_model_config(config_path) # 得到list[dict()]类型的model配置信息表

      self.hyperparams, self.module_list = create_modules(self.module_defs)

      self.yolo_layers = [layer[0] for layer in self.module_list if isinstance(layer[0], YOLOLayer)]

      self.img_size = img_size

      self.seen = 0

      self.header_info = np.array([0, 0, 0, self.seen, 0], dtype=np.int32)

      其中第三句:

      self.yolo_layers = [layer[0] for layer in self.module_list if isinstance(layer[0], YOLOLayer)]

      self.module_list对应的是一个nn.ModuleList(),layer对应的是一个nn.Sequential(),layer(0)对应的是nn.Sequential()里面的第一个module。

      下面来看下forward()部分:

      def forward(self, x, targets=None):

      img_dim = x.shape[2] # 图像的尺寸

      loss = 0

      layer_outputs, yolo_outputs = [], []

      for i, (module_def, module) in enumerate(zip(self.module_defs, self.module_list)):

      if module_def["type"] in ["convolutional", "upsample", "maxpool"]:

      x = module(x)

      elif module_def["type"] == "route":

      # torch.cat 对单个tensor相当于保持原样

      x = torch.cat([layer_outputs[int(layer_i)] for layer_i in module_def["layers"].split(",")], 1)

      elif module_def["type"] == "shortcut":

      layer_i = int(module_def["from"])

      x = layer_outputs[-1] + layer_outputs[layer_i]

      elif module_def["type"] == "yolo":

      x, layer_loss = module[0](x, targets, img_dim)

      loss += layer_loss

      yolo_outputs.append(x)

      layer_outputs.append(x)

      yolo_outputs = to_cpu(torch.cat(yolo_outputs, 1))

      return yolo_outputs if targets is None else (loss, yolo_outputs)

      如果是conv、upsample、maxpool就直接执行;如果是route,就根据从cfg拿到的layer编号进行concat;如果是yolo,对应着输出就进行loss的计算。

      layer_outputs.append(x)

      这里每一层(对应一个create_modules()函数的nn.Sequential()),记录每一层的输出,可以方便的进行route和residual,这种写法是很经典的。

  • 相关阅读:
    二叉排序树的查找和插入操作
    二叉排序树(二叉查找树)- 数据结构和算法73
    线性索引查找
    斐波那契查找(黄金分割法查找)- 数据结构和算法71
    插值查找(按比例查找)- 数据结构和算法70
    序列!序列!- 零基础入门学习Python016
    字符串:格式化
    字符串:各种奇葩的内置方法
    为duilib的MenuDemo增加消息响应,优化代码和显示效果
    为duilib的MenuDemo增加消息响应,优化代码和显示效果
  • 原文地址:https://www.cnblogs.com/djw12333/p/14506083.html
Copyright © 2020-2023  润新知