Python/PyTorch 공부

[Object Detection] YOLO(v3)를 PyTorch로 바닥부터 구현하기 - Part 2

AI 꿈나무 2021. 1. 11. 17:44
반응형

 안녕하세요! YOLO를 PyTorch로 바닥부터 구현하기 part 2 입니다.

 이 포스팅은 공부 목적으로 아래 게시글을 변역했습니다.

 

 

How to implement a YOLO (v3) object detector from scratch in PyTorch: Part 2

Part 2 of the tutorial series on how to implement your own YOLO v3 object detector from scratch in PyTorch.

blog.paperspace.com

 

 바닥부터 YOLO v3 detector를 구현하는 튜토리얼의 Part 2 입니다. 지난 파트에서, YOLO가 어떻게 작동하는 지 설명했고 이번 파트에서는 YOLO에서 사용되는 layers를 PyTorch로 구현해보겠습니다. 즉, 이번 파트에서 모델의 building block을 생성해 볼 것입니다.

 

 이 튜토리얼에 대한 코드는 Python 3.5와 PyTorch 0.4에서 작동되도록 설계되었습니다. 전체 코드는 여기에서 확인하실 수 있습니다.

 

 튜토리얼은 5 part로 나누어져 있습니다.

 

1. Part 1 : YOLO가 어떻게 작동하는지 이해하기

2. Part 2 : (현재) 신경망 구조의 계층 생성하기

3. Part 3 : 신경망의 순전파 구현하기

4. Part 4 : 비-최대 억제(Non-maximum suppression)와 객체 점수 임계값

5. Part 5 : 입력값과 출력값 

 

사전 지식

  • YOLO가 어떻게 작동하는지에 대한 지식, 튜토리얼 Part 1
  • nn.Module, nn.Sequential, torch.nn.parameter class로 custom architecture를 생성하는 방법을 포함한 PyTorch 기본 지식

 이전에 PyTorch를 경험해보았다고 가정하겠습니다. 만약 처음해본다면, 이번 포스팅으로 돌아오기 전에 framework를 조금이라도 다뤄보는 것을 추천합니다.

 

시작하기

 첫 번째로 detector에 대한 코드를 저장할 directory를 생성합니다.

 

 그리고나서 darknet.py 파일을 생성합니다. Darknet은 YOLO의 기본적인 구조의 이름입니다. 이 파일은 YOLO 신경망을 생성하는 코드가 포함될 것 입니다. 우리는 이 파일을 다양한 helper functions 코드가 담겨있는 util.py 파일로 보완할 것 입니다. 이 두 파일들을 detector 폴더에 저장하세요. 변경을 추척하기 위해 git을 사용해도 좋습니다.

 

구성 파일

 공식적인 코드는 신경망을 구축하기 위해 구성 파일을 사용합니다. cfg 파일은 신경망의 layout을 설명합니다.

 

 논문 저자가 공개한 공식적인 cfg 파일을 신경망을 구축하기 위해 사용할 것입니다. 여기서 다운로드 받고 detectory directory에 있는 cfg 폴더에 이것을 옮기세요.

 

 구성 파일을 열어보면 다음과 같이 확인할 수 있습니다.

 

 

 4개의 blcok를 확인할 수 있습니다. 3개의 convolutional layers 다음에 chortcut layer가 있습니다. shortcut layer는 ResNet에서 사용되는 skip connection입니다. YOLO에 사용되는 5개의 layers가 있습니다.

 

Convolutional

 

Shortcut

 

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

 

Upsample

 bilinear upsampling을 사용하여 stride 인자로 이전 layer의 feature map을 upsample 합니다.

 

Route

 rout layer는 약간의 설명이 필요합니다. 이것은 하나 또는 두개의 값을 가질 수 있는 layers 속성을 갖습니다.

 

 layers 속성이 하나의 값을 갖을 때, 이것은 값으로 index된 layer의 feature maps을 출력합니다. 우리의 예제에서 이것은 -4 입니다. 따라서 layer은 Routh layer 부터 4 번째 뒤에 있는 layer이 feature map을 출력할 것입니다.

 

 layers가 두개의 값을 가질 때, 이것은 값으로 index된 연결된 layers의 feature maps를 반환합니다. 우리의 예제에서 이것은 -1, 61이고 layer은 이전 레이어와 61번째 layer에서 깊이 차원에 따라 연결된 feature map을 출력할 것입니다.

 

YOLO

 

 Detection layer에 해당되는 YOLO layer는 part 1에서 설명했습니다. anchors는 9개의 anchors를 의미하지만 mask tag의 속성으로 index된 anchors만 사용됩니다. 여기서, mask의 값은 0,1,2이고 이는 첫 번째, 두 번째, 세 번째 anchors가 사용되는 것을 의미합니다. 이것은 detection layer에 있는 각 셀이 3개의 박스들을 예측하는 것을 의미합니다. 전체적으로, 3개의 scale에서 detection layers를 지니고 있으므로 총 9개의 anchors를 생성합니다.

 

(3개의 anchor가 사용되고 각 셀이 3개의 박스를 예측하므로 총 9개의 anchors를 사용한다는 의미인 것 같습니다.)

 

Net

 

 cfg 안에 net이라고 불리는 또다흔 block 유형이 있지만, 이것은 신경망 입력과 training parameters에 대한 정보를 설명하기 때문에 layer라고 부르지 않습니다. 이것은 YOLO에서 순전파로 사용되지 않습니다. 하지만 이것은 신경망 입력 크기와 같은 정보를 제공해주고, 순전파에서 anchors를 조정하기 위해 사용됩니다.

 

구성 파일 분석

 시작하기 전에 darknet.py 파일 맨 위에 필요한 imports를 추가합니다.

# 필요한 library 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

 

 parse_cfg로 불리는 함수를 정의합니다.

 구성 파일의 경로를 입력으로 받습니다.

# parse_cfg 함수 정의하기, 구성 파일의 경로를 입력으로 받습니다.
def parse_cfg(cfgfile):
  '''
  configuration 파일을 입력으로 받습니다.
  
  blocks의 list를 반환합니다. 각 blocks는 신경망에서 구축되어지는 block을 의미합니다.
  block는 list안에 dictionary로 나타냅니다.
  '''

 

 이 아이디어는 cfg를 분석하고 모든 block을 dict으로 저장합니다. blocks의 속성과 값들은 dictionary에 key-value 쌍으로 저장됩니다. cfg 전체를 분석하고, 이것들을 dict에 추가하고, 코드에 block 변수로 나타내고, blocks list로 변경합니다. 이 함수는 이 block를 반환할 것입니다.

 

 우리는 cfg 파일의 내용을 문자열 list로 저장하여 시작합니다. 다음의 코드는 이 list에서 전처리를 수행합니다.

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

 

 그리고 나서 blocks를 얻기 위해 결과 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

 

 cfg 파일 전처리가 잘 되었는지 colab으로 확인해보겠습니다.

 

 잘 작동되는 것을 확인할 수 있습니다!

 

building blocks 생성하기

 이제 config 파일에 있는 blocks에 대한 PyTorch 모듈을 구축하기 위해 parse_cfg 함수로 반환된 리스트를 사용할 것입니다.

 

 리스트에는 5가지 유형의 layers가 있습니다. PyTorch는 convolutionalupsample 유형의 사전 구축된 layers를 제공합니다. 이제 nn.Module class를 확장함으로써 layers의 남은 부분에 대한 module을 작성할 것입니다.

 

 create_modules 함수는 parse_cfg 함수로 반환된 리스트 blocks를 입력으로 받습니다.

# nn.Module class를 사용하여 layers에 대한 module인 create_modules 함수를 정의합니다.
# 입력은 parse_cfg 함수에서 반환된 blocks를 취합니다.
def create_modules(blocks):
    net_info = blocks[0] # 입력과 전처리에 대한 정보를 저장합니다.
    module_list = nn.ModuleList()
    prev_filters = 3
    output_filters = []

 

 blocks의 list를 반복하기 전에 신경망에 대한 정보를 저장하는 net_info 변수를 정의합니다.

 

nn.ModuleList

 위 함수는 nn.ModuleList를 반환할 것입니다. 이 class는 nn.Module 객체들을 포함하는 일반적인 리스트와 거의 동일합니다. 하지만 nn.Module 객체의 member로서 nn.ModuleList를 추가할 때, nn.ModuleList에 있는 nn.Module 객체의 모든 parameter들이 nn.Module 객체의 parameter로 추가됩니다.

 

 새로운 convolutional layer를 정의할 때, 이것의 kernel의 차원을 정의해야 합니다. kernel의 높이와 넓이는 cfg 파일에서 제공되지만, kernel은 이전 layer에 존재하는 정확한 filter의 수 입니다. 이것은 우리가 적용되어지는 convolutional layer에 있는 filter의 수를 추적해야한다는 의미입니다. 우리는 이것을 하기 위해 prev_filter 변수를 사용합니다. 이것을 3으로 초기화하고 이유는 이미지는 RGB 채널에 해당하는 3개의 filter를 갖고있기 때문입니다.

 

 route layer은 이전 layers 로부터 (가능하면 연결된) feature maps를 가져옵니다. 만약 route layer 바로 앞에 convolutional layer가 있으면, kernel은 이전 layers의 feature maps에 적용되고 정확하게 그것들은 route layer가 가져옵니다. 그러므로, 이전 layer의 filters 수를 추적할 뿐만 아니라 앞의 layer의 각각의 것도 추적해야 합니다. 우리는 반복하기 때문에 각 block의 출력 filters의 수를 output_filters 리스트에 append 합니다.

 

 이제, blocks의 리스트를 반복하고 각 block에 대해 PyTorch module을 생성합니다.

    for index, x in enumerate(blocks[1:]):
        module = nn.Sequential()
        
        # block의 type을 확인합니다.
        # block에 대한 새로운 module을 생성합니다.
        # module_list에 append 합니다.

 

 nn.Sequential class는 nn.Module 객체들의 수를 연속적으로 실행하기 위해 사용됩니다. 만약 cfg를 보면, block은 1개의 layer보다 많이 포함하고 있는 것을 확인할 것입니다. 예를 들어, convolutional 타입의 block이 convolutional layer에 추가하여 leaky ReLU activation layer 뿐만 아니라 batch norm layer를 갖고 있습니다. 우리는 nn.Sequentialadd_module 함수를 사용하여 이 layers를 함께 묶습니다. 예를 들어, 이것은 어떻게 convolutional layers와 upsample layers를 생성하는지 보여줍니다.

 

        if (x['type'] == 'convolutional'):
            # layer에 대한 정보를 얻습니다.
            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을 확인합니다.
			# YOLO에서 Leaky ReLU 또는 Linear 입니다.
			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(sacle_factor = 2, mode = 'bilinear')
			module.add_module('upsample_{}'.format(index), upsample)

 

Route Layer / Shortcut Layers

 다음에는 Route 와 Shortcut Layers를 생성하는 코드를 작성하겠습니다.

		# 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 Layer를 생성하는 코드는 약간의 설명이 필요합니다. 첫 번째로, layers 속성 값을 추출하고 integer로 변경하고 list로 저장합니다.

 

 그리고나서 empty layer를 나타내는 새로운 layer인 EmptyLayer를 갖습니다.

route = EmptyLayer()

 

 이것은 다음과 같이 정의됩니다.

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

 

empty layer는 무엇일까요?

 empty layer가 아무것도 하지 않는 것을 생각하면 이상해 보일 수 있습니다. 다른 layer 같이 Route Layer는 연산(이전 계층을 연결, 도입)을 수행합니다. PyTorch에서, 새로운 layer를 정의할 때 nn.Module의 subclass로 만들고, nn.Module 객체의 forward 함수에서 layer가 수행하는 연산을 작성합니다.

 

 Route block에 대한 layer를 설계하기 위해, 우리는 이것의 member(s)로서 layers 속성 값을 초기화 시키는 nn.Module 객체를 구축합니다. 그리고나서, forward 함수에서 feature maps를 연결/ 도입하는 코드를 작성할 수 있습니다. 마지막으로, 신경망의 forward 함수에서 이 layer를 실행시킵니다.

 

 하지만 주어진 연결 코드 상당히 짧고(feature maps에 torch.cat을 호출) 간단한 것을 고려하면, 위에 있는 layer를 설계하는 것은 단지 코드를 증가시키는 불필요한 것으로 이끌 것입니다. 대신에, 우리가 할 수 있는 것은 제안된 route layer대신에 dummy layer를 두는 것 입니다. 그리고나서 darknet을 나타내는 nn.Module 객체의 forward 함수에서 직접적으로 연결을 수행합니다.(이해가 안된다면, PyTorch에서 사용되는 nn.Module이 무엇인지 읽어보세요)

 

 route layer 바로 앞에 있는 convolutional layer는 이것의 kernel(가능하면 연결된 채로)을 이전 layers의 feature map에 적용합니다. 다음의 코드는 route layer에서 출력되는 filters의 수를 저장하기 위한 filters 변수를 갱신합니다.

if end < 0:
    #If we are concatenating maps
    filters = output_filters[index + start] + output_filters[index + end]
else:
    filters= output_filters[index + start]

 

 shortcut layer 또한 매우 간단한 연산(덧셈)을 수행하기 때문에 empty layer를 사용합니다. 단지 바로 뒤에 있는 layer에 이전 layer의 feature map을 추가하기 때문에 filters 변수를 갱신할 필요가 없습니다.

 

YOLO Layer

 마지막으로, 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)

 

 바운딩 박스를 detec하기 위해 사용되는 anchors를 저장하는 새로운 layer DetectionLayer를 정의합니다.

 

 detection layer는 다음과 같이 정의됩니다.

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

 

 loop의 마지막에, bookkeeping을 합니다.

		module_list.append(module)
		prev_filters = filters
		output_filters.append(filters)

 

 이것이 loop의 전체를 종료합니다. create_modules 함수의 끝에, net_infomodule_list를 포함한 tuple을 반환합니다.

return (net_info, module_list)

 

코드 test

 darknet.py 끝에 다음 줄들을 작성함으로써 코드를 테스트할 수 있습니다.

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

 

 정확히 106 items를 지니고 있는 긴 리스트를 확인할 수 있고, 각 요소는 다음과 같습니다.

 아래는 코드를 Colab에서 작동한 결과입니다.

 

 이제 이번 part가 끝났습니다. 다음 part에서, 이미지로부터 출력값을 생성하기 위해 생성된 building blocks를 조립할 것입니다.

 

 위 코드를 분석하고 한국어로 주석을 단 코드는 여기에서 확인하실 수 있습니다.

반응형