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:\pyprojects\object-detection-code-debug\PyTorch-YOLOv3\data\custom\labels

  我多加了一個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('\n')

  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,這種寫法是很經典的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM