yusukaid's IT note

Pytorch를 이용한 YOLO v3 논문 구현 #1 - 신경망 구조의 계층 생성 (22.10.04 수정) 본문

YOLO

Pytorch를 이용한 YOLO v3 논문 구현 #1 - 신경망 구조의 계층 생성 (22.10.04 수정)

yusukaid 2022. 9. 27. 04:04

※본 포스팅은 아래 블로그를 참조해 번역하고 공부한 것입니다.

https://blog.paperspace.com/how-to-implement-a-yolo-object-detector-in-pytorch/

 

Tutorial on implementing YOLO v3 from scratch in PyTorch

Tutorial on building YOLO v3 detector from scratch detailing how to create the network architecture from a configuration file, load the weights and designing input/output pipelines.

blog.paperspace.com

※본 포스팅은 아래 논문에 기반합니다.

https://arxiv.org/pdf/1804.02767.pdf

YOLO v2부터 backbone으로 darknet을 사용한다. 이는 개발자가 자체적으로 C언어를 이용해 제작한 것으로, YOLO 신경망을 생성하는 로직을 포함한다. 또한, 다양한 유틸성 기능을 포함한 utils.py로 모델을 보완할 것이다.


구성 파일

신경망 구축을 위해 공식적으로 제공하는 cfg 파일을 다운받자. 

https://github.com/pjreddie/darknet/blob/master/cfg/yolov3.cfg

 

GitHub - pjreddie/darknet: Convolutional Neural Networks

Convolutional Neural Networks. Contribute to pjreddie/darknet development by creating an account on GitHub.

github.com

파일의 구성은 다음과 같다.


Convolutional

Shortcut

shortcut 레이어는 ResNet에서 사용하는 것과 유사한, skip connection이다. from 변수가 -3인 것은 출력이 이전 레이어의 feature maps와 shorcut late의 3번째 뒤에 있는 layer를 추가하여 얻는 것을 의미한다.

Upsample

upsampling을 사용하여 stride 인자로 이전 레이어의 feature map을 upsampling 한다.

Route

route 레이어는 하나나 두개의 값을 가질 수 있는 속성 레이어다. 

레이어가 하나의 값을 가지면, index된 레이어의 feature maps를 출력한다. 본 구현에서 변수값은 -4다. 따라서 4번째 뒤에 있는 layer가 feature map을 출력한다.

레이어가 만약 두개의 값을 가지면 index된 레이어의 feature maps를 반환한다. 본 구현에서 변수값은 -1과 61이다. 따라서 61번째 레이어에서 깊이 차원에 따라 연결된 feature map을 출력할 것이다.

YOLO

챕터 0에서 설명한 detection layer에 해당되는 레이어다. 전체적으로 3개의 scale 레이어를 포함하므로 총 9개의 anchors를 생성한다.

Net

이는 신경망 input과 training parameters에 대한 정보만 설명하기 때문에 레이어라고 부르지 않으며 순전파로 사용하지도 않는다.


코드 분석

#필요한 라이브러리 import
from __future__ import division

import torch 
import torch.nn as nn
import torch.nn.functional as F 
from torch.autograd import Variable
import numpy as np
from util import *

pytorch를 이용하기 때문에 기본적으로 torch를 import한다. 함수를 계산하기 위해 numpy도 import한다.

#구성 파일의 경로를 입력으로 받기
def parse_cfg(cfgfile):
    """
    configuration file 입력으로 받기
    
    block list 반환 (신경망에서 구축되는 block)
    block은 list안에 dictionary로 나타냄
    
    """

cfg를 분석하고 모든 block을 dict으로 저장하기 위해 함수를 정의하는 부분. 이 함수는 cfg 전체를 분석하고, dictionary에 추가하며, 코드에 block 변수로 나타내고, list로 저장한다. 최종적으로 이 함수는 block을 반환한다.

    #cfg 파일 전처리
    file = open(cfgfile, 'r')
    lines = file.read().split('\n')                        # lines를 lis로 저장
    lines = [x for x in lines if len(x) > 0]               # 빈 line 삭제 
    lines = [x for x in lines if x[0] != '#']              # 주석 삭
    lines = [x.rstrip().lstrip() for x in lines]           # 공백 제거

list 문자열을 전처리하기 위한 코드.

    #blocks를 얻기 위해 list 반복
    block = {}
    blocks = []
    
    for line in lines:
        if line[0] == "[":               # 새로운 block의 시작
            if len(block) != 0:          # block이 비어있지 않으면 이전 block의 값을 저장
                blocks.append(block)     # blocks list에 추가
                block = {}               # block 초기화
            block["type"] = line[1:-1].rstrip()     
        else:
            key,value = line.split("=") 
            block[key.rstrip()] = value.lstrip()
    blocks.append(block)
    
    return blocks

blocks를 얻기 위해 결과 list를 반복하는 코드.


building blocks 생성하기

config 파일에 있는 blocks에 대한 pytorch 모듈을 구축하기 위해 parse_cfg 함수로 반환된 리스트를 사용한다. pytorch는 convolutional과 upsample 유형의 사전 구축된 레이어를 제공한다. nn.module을 확장함으로써 레이어의 남은 부분에 대한 module 작성이 필요하다.

#nn.Module class를 사용하여 레이어에 대한 모듈 생성 함수 정
def create_modules(blocks):
    net_info = blocks[0]     #입력과 전처리에 대한 정보 저장     
    module_list = nn.ModuleList()
    prev_filters = 3
    output_filters = []

parse_cfg 함수로 반환된 blocks 리스트를 입력으로 받고, 반복을 수행하기 전에 신경망에 대한 정보를 저장하는 net_info 변수를 정의한다.

  • nn.ModuleList()는 nn.Module 객체의 멤버로서 nn.ModuleList를 추가할 때, 모든 파라미터들을 추가한다.
  • convolutional layer를 정의할 때 차원을 정의해야 하는데 이는 이전 레이어에 존재하는 필터의 수이다. prev_filter를 3으로 초기화 하는 이유는 RGB 채널에 해당하는 3개의 필터를 가지고 있기 때문이다. 
  • 우리는 blocks list 결과를 반복 수행한다. route 레이어는 이전 레이어로부터 feature maps를 가져오기 때문에 이전 레이어의 필터뿐만 아니라 각각 앞의 레이어 필터도 추적하고 가져와야 한다. 이를 위해 각 block의 필터 수를 output_filters 리스트에 저장한다.
    for index, x in enumerate(blocks[1:]):
        module = nn.Sequential()
    
        #block 타입 확인
        #block에 대한 새로운 모듈 생성
        #module_list에 append

blocks의 리스트를 반복 수행하고, 각 block에 대한 pytorch module을 생성하는 코드.

        #convolutional layer
        if (x["type"] == "convolutional"):
            #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:
                pad = (kernel_size - 1) // 2
            else:
                pad = 0
        
            #convolutional layer 추가
            conv = nn.Conv2d(prev_filters, filters, kernel_size, stride, pad, bias = bias)
            module.add_module("conv_{0}".format(index), conv)
        
            #Batch Norm Layer 추가
            if batch_normalize:
                bn = nn.BatchNorm2d(filters)
                module.add_module("batch_norm_{0}".format(index), bn)
        
            #activation 확인
            #Linear 또는 Leaky ReLU
            if activation == "leaky":
                activn = nn.LeakyReLU(0.1, inplace = True)
                module.add_module("leaky_{0}".format(index), activn)
        
            #upsampling layer
            #Bilinear2dUpsampling 사용
        elif (x["type"] == "upsample"):
            stride = int(x["stride"])
            upsample = nn.Upsample(scale_factor = 2, mode = "nearest")
            module.add_module("upsample_{}".format(index), upsample)

nn.Sequential과 all_module은 여러개의 레이어를 함께 묶기 위해 사용한다. 위 코드는 어떻게 convolutional과 upsample 레이어를 생성하는지 보여준다.

        #route layer
        elif (x["type"] == "route"):
            x["layers"] = x["layers"].split(',')
            #route 시작
            start = int(x["layers"][0])
            #1개만 존재하면 종료
            try:
                end = int(x["layers"][1])
            except:
                end = 0
            #양수인 경우
            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]
    
        #skip connection을 수행하는 shortcut
        elif x["type"] == "shortcut":
            shortcut = EmptyLayer()
            module.add_module("shortcut_{}".format(index), shortcut)

route 레이어와 shortcut 레이어를 생성하는 코드다. 

  1. Route 레이어의 경우, layers 속성 값을 추출하고 이를 integer로 변경후 list로 저장한다.
  2. 그 후 empty layer를 나타내는 새로운 레이어인 EmptyLayer를 갖는다.
class EmptyLayer(nn.Module):
    def __init__(self):
        super(EmptyLayer, self).__init__()

pytorch에서 새로운 레이어를 정의할 때 nn.Module의 subclass로 만들고, nn.Module 객체의 forward 함수에서 레이어가 수행하는 연산을 작성한다. 즉, route 레이어는 굉장히 복잡한 로직을 포함하고 있기 때문에, 코드를 간결하게 하기 위해 빈 dummy layer를 생성하는 것이다. 그리고 이 레이어에 darknet을 나타내는 nn.Module 객체의 forward 함수에서 직접적인 연결을 수행한다.


YOLO layer

        #Yolo == 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)]
            anchors = [anchors[i] for i in mask]
    
            detection = DetectionLayer(anchors)
            module.add_module("Detection_{}".format(index), detection)

 yolo 레이어를 생성하기 위한 코드. 

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

바운딩 박스를 detect하기 위해 사용되는 anchors를 저장하기 위해 새로운 레이어인 detectlayer를 정의한다.

        module_list.append(module)
        prev_filters = filters
        output_filters.append(filters)
        
    return (net_info, module_list)

루프의 마지막에 추가해야 할 코드. 이는 루프의 종료를 의미하며, net_info와 module_list를 포함한 tuple을 최종 반환한다.