본문 바로가기
DL/Code

YOLOv1 Pytorch 코드 리뷰

by hits_gold 2024. 1. 25.
반응형

코드 : https://github.com/aladdinpersson/Machine-Learning-Collection/tree/master/ML/Pytorch/object_detection/YOLO

 

1. DarkNet

DarkNet은 YOLOv1이 feature map을 생성하기 위해 만들어진 독자적인 CNN이다.

## model.py

"""
Implementation of Yolo (v1) architecture
with slight modification with added BatchNorm.
"""

import torch
import torch.nn as nn

""" 
Information about architecture config:
Tuple is structured by (kernel_size, filters, stride, padding) 
"M" is simply maxpooling with stride 2x2 and kernel 2x2
List is structured by tuples and lastly int with number of repeats
"""

architecture_config = [
    (7, 64, 2, 3),
    "M",
    (3, 192, 1, 1),
    "M",
    (1, 128, 1, 0),
    (3, 256, 1, 1),
    (1, 256, 1, 0),
    (3, 512, 1, 1),
    "M",
    [(1, 256, 1, 0), (3, 512, 1, 1), 4],
    (1, 512, 1, 0),
    (3, 1024, 1, 1),
    "M",
    [(1, 512, 1, 0), (3, 1024, 1, 1), 2],
    (3, 1024, 1, 1),
    (3, 1024, 2, 1),
    (3, 1024, 1, 1),
    (3, 1024, 1, 1),
]


class CNNBlock(nn.Module):
    def __init__(self, in_channels, out_channels, **kwargs):
        super(CNNBlock, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, bias=False, **kwargs)
        self.batchnorm = nn.BatchNorm2d(out_channels)
        self.leakyrelu = nn.LeakyReLU(0.1)

    def forward(self, x):
        return self.leakyrelu(self.batchnorm(self.conv(x)))


class Yolov1(nn.Module):
    def __init__(self, in_channels=3, **kwargs):
        super(Yolov1, self).__init__()
        self.architecture = architecture_config
        self.in_channels = in_channels
        self.darknet = self._create_conv_layers(self.architecture)
        self.fcs = self._create_fcs(**kwargs)

    def forward(self, x):
        x = self.darknet(x)
        return self.fcs(torch.flatten(x, start_dim=1))

    def _create_conv_layers(self, architecture):
        layers = []
        in_channels = self.in_channels

        for x in architecture:
            if type(x) == tuple:
                layers += [
                    CNNBlock(
                        in_channels, x[1], kernel_size=x[0], stride=x[2], padding=x[3],
                    )
                ]
                in_channels = x[1]

            elif type(x) == str:
                layers += [nn.MaxPool2d(kernel_size=(2, 2), stride=(2, 2))]

            elif type(x) == list:
                conv1 = x[0]
                conv2 = x[1]
                num_repeats = x[2]

                for _ in range(num_repeats):
                    layers += [
                        CNNBlock(
                            in_channels,
                            conv1[1],
                            kernel_size=conv1[0],
                            stride=conv1[2],
                            padding=conv1[3],
                        )
                    ]
                    layers += [
                        CNNBlock(
                            conv1[1],
                            conv2[1],
                            kernel_size=conv2[0],
                            stride=conv2[2],
                            padding=conv2[3],
                        )
                    ]
                    in_channels = conv2[1]

        return nn.Sequential(*layers)

    def _create_fcs(self, split_size, num_boxes, num_classes):
        S, B, C = split_size, num_boxes, num_classes

        # In original paper this should be
        # nn.Linear(1024*S*S, 4096),
        # nn.LeakyReLU(0.1),
        # nn.Linear(4096, S*S*(B*5+C))

        return nn.Sequential(
            nn.Flatten(),
            nn.Linear(1024 * S * S, 496),
            nn.Dropout(0.0),
            nn.LeakyReLU(0.1),
            nn.Linear(496, S * S * (C + B * 5)),
        )

 

  • config는 설명처럼 (kernel_size, filters, stride, padding)을 의미하고, "M"이면 Maxpooling(stride2, kernel2)이 수행된다.
  • config의 [(), n]의 형태는 ()의 cfg가 n번 반복된다.(그림 상 3, 4번째 연산에 해당)
  • _create_conv_layers 함수를 통해 CNN 구조를 만든다.
  • DarkNet 뒷단의 fclayer를 만들기 위한 함수 _create_fcs 함수를 사용하는데, 여기 입력되는 kwargs는 기본적으로 split_size=7, num_boxes=2, num_classes=20이다. 이 kwargs에 따라 FC layer의 input과 output size를 계산하기 때문에 입력된다.

 

2. Loss

 

loss 계산에 필요한 하이퍼파라미터는 위 DarkNet의 fc layer에 입력된 split_size=7, num_boxes=2, num_classes=20가 필요하고, 추가적으로 noobj, coord를 지정해주어야 한다.

 

## loss.py
"""
Implementation of Yolo Loss Function from the original yolo paper

"""

import torch
import torch.nn as nn
from utils import intersection_over_union


class YoloLoss(nn.Module):
    """
    Calculate the loss for yolo (v1) model
    """

    def __init__(self, S=7, B=2, C=20):
        super(YoloLoss, self).__init__()
        self.mse = nn.MSELoss(reduction="sum")

        """
        S is split size of image (in paper 7),
        B is number of boxes (in paper 2),
        C is number of classes (in paper and VOC dataset is 20),
        """
        self.S = S
        self.B = B
        self.C = C

        # These are from Yolo paper, signifying how much we should
        # pay loss for no object (noobj) and the box coordinates (coord)
        self.lambda_noobj = 0.5
        self.lambda_coord = 5

    def forward(self, predictions, target):
        # predictions are shaped (BATCH_SIZE, S*S(C+B*5) when inputted
        predictions = predictions.reshape(-1, self.S, self.S, self.C + self.B * 5)

        # Calculate IoU for the two predicted bounding boxes with target bbox
        iou_b1 = intersection_over_union(predictions[..., 21:25], target[..., 21:25])
        iou_b2 = intersection_over_union(predictions[..., 26:30], target[..., 21:25])
        ious = torch.cat([iou_b1.unsqueeze(0), iou_b2.unsqueeze(0)], dim=0)

        # Take the box with highest IoU out of the two prediction
        # Note that bestbox will be indices of 0, 1 for which bbox was best
        iou_maxes, bestbox = torch.max(ious, dim=0)
        exists_box = target[..., 20].unsqueeze(3)  # in paper this is Iobj_i

        # ======================== #
        #   FOR BOX COORDINATES    #
        # ======================== #

        # Set boxes with no object in them to 0. We only take out one of the two 
        # predictions, which is the one with highest Iou calculated previously.
        box_predictions = exists_box * (
            (
                bestbox * predictions[..., 26:30]
                + (1 - bestbox) * predictions[..., 21:25]
            )
        )

        box_targets = exists_box * target[..., 21:25]

        # Take sqrt of width, height of boxes to ensure that
        box_predictions[..., 2:4] = torch.sign(box_predictions[..., 2:4]) * torch.sqrt(
            torch.abs(box_predictions[..., 2:4] + 1e-6)
        )
        box_targets[..., 2:4] = torch.sqrt(box_targets[..., 2:4])

        box_loss = self.mse(
            torch.flatten(box_predictions, end_dim=-2),
            torch.flatten(box_targets, end_dim=-2),
        )

        # ==================== #
        #   FOR OBJECT LOSS    #
        # ==================== #

        # pred_box is the confidence score for the bbox with highest IoU
        pred_box = (
            bestbox * predictions[..., 25:26] + (1 - bestbox) * predictions[..., 20:21]
        )

        object_loss = self.mse(
            torch.flatten(exists_box * pred_box),
            torch.flatten(exists_box * target[..., 20:21]),
        )

        # ======================= #
        #   FOR NO OBJECT LOSS    #
        # ======================= #

        #max_no_obj = torch.max(predictions[..., 20:21], predictions[..., 25:26])
        #no_object_loss = self.mse(
        #    torch.flatten((1 - exists_box) * max_no_obj, start_dim=1),
        #    torch.flatten((1 - exists_box) * target[..., 20:21], start_dim=1),
        #)

        no_object_loss = self.mse(
            torch.flatten((1 - exists_box) * predictions[..., 20:21], start_dim=1),
            torch.flatten((1 - exists_box) * target[..., 20:21], start_dim=1),
        )

        no_object_loss += self.mse(
            torch.flatten((1 - exists_box) * predictions[..., 25:26], start_dim=1),
            torch.flatten((1 - exists_box) * target[..., 20:21], start_dim=1)
        )

        # ================== #
        #   FOR CLASS LOSS   #
        # ================== #

        class_loss = self.mse(
            torch.flatten(exists_box * predictions[..., :20], end_dim=-2,),
            torch.flatten(exists_box * target[..., :20], end_dim=-2,),
        )

        loss = (
            self.lambda_coord * box_loss  # first two rows in paper
            + object_loss  # third row in paper
            + self.lambda_noobj * no_object_loss  # forth row
            + class_loss  # fifth row
        )

        return loss

 

 2.1. localization loss

  predictions이 입력되었을 때, 이를 (batch, 7, 7, 30)사이즈로 resize 시켜주고, num_boxes가 2개이므로 아래 box 좌표에 해당하는 값을 target과의 IoU를 계산하고, 두 tensor를 concat한다. 여기서 best box에 target과의 IoU값이 더 큰 box의 인덱스가 저장된다.

  •  exist_box는 target[..., 3]의 값으로 gt가 존재한다면 1, 존재하지 않으면 0이 된다. 
  •  코드에서 bestbox값을 활용해 predicted box 좌표를 구하는데, 이 코드는 num_boxs가 2인 경우(논문 설정)에만 적용할 수 있는 코드인 것 같다.
  • 이 후 prediction과 target 둘 다 w, h에 해당하는 좌표에 루트를 씌어준다
  • Localizatoin loss의 수식을 처음 보면 뭔가 싶었는데, 결국 w, h에는 루트를 씌운 x, y, w, h에 대한 mse이다.
  • 수식의 labmda_coord 가중치는 전체 로스 합산 시 적용된다.

  2.2 Confidence loss

  •  코드에서 수식의 첫 번째 항인 object가 존재하는 경우의 Loss를 먼저 구해준다. 여기서도 best box의 인덱스값을 활용하는데, loss를 구하기 전부터 class prob + bestbox(confidence score, x, y, w, h)를 구해놓고 시작하는게 더 낫지 않나 싶다.
  •  위에서처럼 방금 best box의 값을 미리 구해놓고 시작하면 되지 않을까 생각했는데, no_object_loss를 두 번 구하는 과정에서 왜 코드를 이렇게 작성했는지 이해되었다. 해당 grid cell의 두 bbox가 전부 배경으로 판정(exists_box=0)되었을 때,  두 bbox에 대한 no_object_loss를 전부 구해주어야 한다.
  • 여기서 object_loss와 no_object_loss를 합산하지 않은 이유는 뒤에  no_object_loss에 대한 가중치 lambda를 따로 적용시키기 위해서다.

 

  2.3 Classification loss

 Classification loss는 predictions[..., :20]에 해당하며 이는 20개 class에 대한 prob을 나타낸다. 이와 target과의 mse loss를 계산한다.

 

  이 후 세 Loss에 대한 가중치를 적용시키고 합산한다.

반응형