본문 바로가기
DL

YOLO 리뷰

by Kwonys 2024. 3. 12.
728x90
반응형

2-Stage Detector의 대표적인 모델인 YOLO에 대해 공부한 내용을 작성해 보겠습니다.

1. 특징

  1. 단일 신경망 구조로 구성이 단순하며 빠르다.
  2. 주변 정보까지 학습하여 이미지 전체를 처리하기 때문에 Background Error가 적다.
  3. 훈련 단계에서 보지 못한 새로운 이미지에 대해서도 정확도가 높다.
  4. 단, SOTA Detection model에 비해 mAP가 다소 떨어진다.

 

2. Unified Detection

Region Proposal, Feature Extraction, Classification, Bounding Box Regression을 단일 신경망으로 처리하는 과정

가정)

  • grid : 4 x 4 (논문에서는 7 x 7)
  • 한 grid cell이 예측하는 bbox: 2개
  • class 개수: 20

x, y는 bbox의 정 중앙의 좌표를 나타내고

w, h는 bbox의 너비와 높이로 실제 값이 아닌 Input Image로 normalize 하여 0~1 사이의 값을 가짐

Pc는 박스 내부에 물체가 존재할 확률을 말한다.

하나의 bbox에 대해 5개의 output이 나오게 된다.

이런 과정이 두 번 반복되고 20개의 클래스 각각의 확률을 포함하여 하나의 grid에 (5x2) + 20 = 30개의 output이 생긴다.

결론적으로 하나의 Image에 대해 4 x 4 x 30의 output tensor가 만들어진다.

 

 

3. YOLO Architecture

YOLO는 CNN 구조의 모델로 이미지 특징을 추출하는 24개의 Convolution Layer와 Class 확률과 bbox의 좌표(Coordinates)를 예측하는 2개의 FC Layer로 구성했다.

* GoogleNet의 inception 대신 1x1 reduction Layer와 3x3 conv Layer를 결합하여 구성했다.

 

4. Training

  • ImageNet 데이터 셋으로 YOLO의 앞단 20개의 컨볼루션 계층을 사전 훈련시킨다.
  • 사전 훈련된 20개의 컨볼루션 계층 뒤에 4개의 컨볼루션 계층 및 2개의 전결합 계층을 추가한다.
  • YOLO 신경망의 마지막 계층에는 선형 활성화 함수(linear activation function) 적용, 나머지 모든 계층에는 leaky ReLU 적용
  • 과적합(overfitting)을 막기 위해 드롭아웃(dropout)과 data augmentation을 적용한다.
  • 구조상 문제 해결을 위해 아래 3가지 개선안을 적용한다.
    1.  localization loss와 classification loss 중 localization loss의 가중치를 증가시킨다.
    2.  객체가 없는 grid cell의 confidence loss보다 객체가 존재하는 grid cell의 confidence loss의 가중치를 증가시킨다.
    3.  bounding box의 너비(widht)와 높이(height)에 square root를 취해준 값을 loss function으로 사용한다.

 

5. YOLOv8 Architecture

위의 그림이 VOLOv8 구조를 한눈에 잘 보이게 만들어둔 것 같다.

 

먼저 Detection을 기준으로 YOLOv8의 backbone과 head는 아래와 같이 구성되어 있다.

# YOLOv8.0n backbone
backbone:
  # [from, repeats, module, args]
  - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
  - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
  - [-1, 3, C2f, [128, True]]
  - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
  - [-1, 6, C2f, [256, True]]
  - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
  - [-1, 6, C2f, [512, True]]
  - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
  - [-1, 3, C2f, [1024, True]]
  - [-1, 1, SPPF, [1024, 5]] # 9

# YOLOv8.0n head
head:
  - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  - [[-1, 6], 1, Concat, [1]] # cat backbone P4
  - [-1, 3, C2f, [512]] # 12

  - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  - [[-1, 4], 1, Concat, [1]] # cat backbone P3
  - [-1, 3, C2f, [256]] # 15 (P3/8-small)

  - [-1, 1, Conv, [256, 3, 2]]
  - [[-1, 12], 1, Concat, [1]] # cat head P4
  - [-1, 3, C2f, [512]] # 18 (P4/16-medium)

  - [-1, 1, Conv, [512, 3, 2]]
  - [[-1, 9], 1, Concat, [1]] # cat head P5
  - [-1, 3, C2f, [1024]] # 21 (P5/32-large)

  - [[15, 18, 21], 1, Detect, [nc]] # Detect(P3, P4, P5)

 

위의 backbone과 head에 사용되는 Conv module의 구성

class Conv(nn.Module):
    """Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)."""

    default_act = nn.SiLU()  # default activation

    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
        """Initialize Conv layer with given arguments including activation."""
        super().__init__()
        self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
        self.bn = nn.BatchNorm2d(c2)
        self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()

    def forward(self, x):
        """Apply convolution, batch normalization and activation to input tensor."""
        return self.act(self.bn(self.conv(x)))

    def forward_fuse(self, x):
        """Perform transposed convolution of 2D data."""
        return self.act(self.conv(x))

 

head에서 Detection의 구성

class Detect(nn.Module):
    """YOLOv8 Detect head for detection models."""
    dynamic = False  # force grid reconstruction
    export = False  # export mode
    shape = None
    anchors = torch.empty(0)  # init
    strides = torch.empty(0)  # init

    def __init__(self, nc=80, ch=()):  # detection layer
        super().__init__()
        self.nc = nc  # number of classes
        self.nl = len(ch)  # number of detection layers
        self.reg_max = 16  # DFL channels (ch[0] // 16 to scale 4/8/12/16/20 for n/s/m/l/x)
        self.no = nc + self.reg_max * 4  # number of outputs per anchor
        self.stride = torch.zeros(self.nl)  # strides computed during build
        c2, c3 = max((16, ch[0] // 4, self.reg_max * 4)), max(ch[0], self.nc)  # channels
        self.cv2 = nn.ModuleList(
            nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, 4 * self.reg_max, 1)) for x in ch)
        self.cv3 = nn.ModuleList(nn.Sequential(Conv(x, c3, 3), Conv(c3, c3, 3), nn.Conv2d(c3, self.nc, 1)) for x in ch)
        self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()

    def forward(self, x):
        """Concatenates and returns predicted bounding boxes and class probabilities."""
        shape = x[0].shape  # BCHW
        for i in range(self.nl):
            x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
        if self.training:
            return x
        elif self.dynamic or self.shape != shape:
            self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))
            self.shape = shape

        x_cat = torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2)
        if self.export and self.format in ('saved_model', 'pb', 'tflite', 'edgetpu', 'tfjs'):  # avoid TF FlexSplitV ops
            box = x_cat[:, :self.reg_max * 4]
            cls = x_cat[:, self.reg_max * 4:]
        else:
            box, cls = x_cat.split((self.reg_max * 4, self.nc), 1)
        dbox = dist2bbox(self.dfl(box), self.anchors.unsqueeze(0), xywh=True, dim=1) * self.strides
        y = torch.cat((dbox, cls.sigmoid()), 1)
        return y if self.export else (y, x)

    def bias_init(self):
        """Initialize Detect() biases, WARNING: requires stride availability."""
        m = self  # self.model[-1]  # Detect() module
        # cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1
        # ncf = math.log(0.6 / (m.nc - 0.999999)) if cf is None else torch.log(cf / cf.sum())  # nominal class frequency
        for a, b, s in zip(m.cv2, m.cv3, m.stride):  # from
            a[-1].bias.data[:] = 1.0  # box
            b[-1].bias.data[:m.nc] = math.log(5 / m.nc / (640 / s) ** 2)  # cls (.01 objects, 80 classes, 640 img)

 

6. YOLOv8 vs YOLOv5

  • C3 module -> C2f module
  • Backbone에서 첫 번째 Conv Layer를 6x6 -> 3x3
  • Conv Layer 2개 삭제
  • BottleNeck에서 첫 번째 Conv Layer를 1x1 -> 3x3
  • 분리된 head(Classification, Regression) 사용
  • Anchor Free Detection

 

약 한 달 전 최신 모델을 공부하고 적용해보려 YOLOv8으로 프로젝트를 진행했는데 며칠 전  2024년 2월 27일 YOLOv9이 출시 됐다...

728x90
반응형