반응형

안녕하세요. 오늘은 Deep Learning 분야에서 CNN의 BottleNeck구조에 대해 알아보겠습니다. 

 

대표적으로 ResNet에서 BottleNeck을 사용했습니다. 

 

ResNet에서 왼쪽은 BottleNeck 구조를 사용하지 않았고, 오른쪽은 BottleNeck 구조를 사용했습니다.

 

BottleNeck을 설명하기 전, Convolution의 Parameters을 계산할 줄 알아야 합니다. 이 부분은 다른 글에서 자세히 설명하겠습니다.

 

Convolution Parameters = Kernel Size x Kernel Size x Input Channel x Output Channel

 

BottleNeck의 핵심은 1x1 Convolution입니다. ( Pointwise Convolution 이라고도 합니다. 이는 Depthwise Separable Convolution에서도 똑같은 원리로 이용되기 때문에 잘 알아두면 좋습니다.)

1x1 Convolution의 Parameters는 1 x 1 x Input Channel x Output Channel입니다.

대게 1x1 Convolution은 연산량이 작기 때문에 Feature Map(Output Channel)을 줄이거나 키울 때 사용됩니다.

 

 

BottleNeck 구조

 

 

1. Input Channel = 256인 320x320  Input Image가 있다고 가정합니다. 

B=Batch SIze 

 

 

 

2. Channel Compression ( 채널 압축 )

Input Channel 256 -> Output Channel 64

256을 64로 채널을 강제로 축소한 이유는 오로지 연산량을 줄이기 위함입니다. 

 

1x1 Convolution에는 Spatial(공간적인) 특징을 가지고 있지 않습니다. Convolution 연산이 공간적인 특징을 추출하기 위해선 Kernel이 최소 2 이상 되어야 합니다. 

 

 

 

 

 

3. 특징 추출

 

Input Channel 64 -> Output Channel 64 

 

3x3 Convolution은 특성을 추출하는 역할을 합니다.

3x3 Convolution 연산은 = 3 x 3 x Input Channel x Output Channel 입니다. ( 3 x 3 x 64 x 64 )

 

3x3 Convolution은 1x1 Convolution 보다 9배 연산량이 많기 때문에, 1x1 Convolution에서 채널을 줄인 후에 

3x3 Convolution에서 특성을 추출합니다. 

 

 

 

 

4. Channel Increase( 채널 증가 )

Input Channel 64 -> Output Channel 256

 

CNN은 Feature Map의 특성이 많으면 많을수록 학습이 잘 되기 때문에, 1x1 Convolution으로 강제적으로 채널을 증가시켜줍니다. 

 

BottleNeck의 구조는 1x1 Convolution으로 장난을 치면서 연산량을 최소화하는 것입니다. 

 

하지만 강제로 채널을 줄이고 늘리는 것은 정보 손실을 일으킵니다. 

정보 손실은 모델의 정확성을 떨어뜨립니다. 

 

연산량과 정보손실은 서로 tradeoff 관계이기 때문에 서로의 합의점을 찾는 것이 중요합니다. 

ResNet이 제시한 두 구조를 Pytorch로 구현해 Parameter를 확인하겠습니다. 

 

Standard는 Channel 수가 적을지라도, 3x3 Convolution을 두 번 통과했고, 

BottleNeck은 1x1, 3x3, 1x1 순으로 Convolution을 통과하고, Channel 수는 4배 정도 많지만, Parameter가 세 배 정도 적습니다. 

 

그리고 형성된 것이 도로의 병목 현상과 비슷하다 하여 BottleNeck 구조라고 불립니다.

병목 현상

 

 

cf) Pytorch Code

# standard
class Standard(nn.Module):
    def __init__(self, in_dim=256, mid_dim=64, out_dim=64):
        super(BuildingBlock, self).__init__()
        self.building_block = nn.Sequential(
            nn.Conv2d(in_channels=in_dim, out_channels=mid_dim, kernel_size=3, padding=1, bias=False),
            nn.ReLU(),
            nn.Conv2d(in_channels=mid_dim, out_channels=out_dim, kernel_size=3, padding=1, bias=False),
        )
        self.relu = nn.ReLU()

    def forward(self, x):
        fx = self.building_block(x)  # F(x)
        out = fx + x  # F(x) + x
        out = self.relu(out)
        return out
# BottleNeck
class BottleNeck(nn.Module):
    def __init__(self, in_dim=256, mid_dim=64, out_dim=256):
        super(BottleNeck, self).__init__()
        self.bottleneck = nn.Sequential(
            nn.Conv2d(in_channels=in_dim, out_channels=mid_dim, kernel_size=1, bias=False),
            nn.ReLU(),
            nn.Conv2d(in_channels=mid_dim, out_channels=mid_dim, kernel_size=3, padding=1, bias=False),
            nn.ReLU(),
            nn.Conv2d(in_channels=mid_dim, out_channels=in_dim, kernel_size=1, bias=False),
        )

        self.relu = nn.ReLU()

    def forward(self, x):
        fx = self.bottleneck(x)  # F(x)
        out = fx + x  # F(x) + x
        out = self.relu(out)
        return out
728x90
반응형
반응형
type(nums)

pytorch1.ipynb
0.01MB

안녕하세요. 이제 파이토치로 딥러닝할 준비가 다 되었습니다.

이제 딥러닝을 하기 전에 파이토치 문법 구조에 대해서 알아보겠습니다.

 

텐서플로우는 numpyarray를 기본으로 텐서(3차 이상)를 사용한답니다. 

우리들이 공부할 파이토치는 torch를 가지고 놉니다. 

 

그냥 numpy = torch 이렇게 보시면 될 것 같습니다. 

이번 시간에 파이토치의 기본인 torch는 데이터 전처리 과정이라고 볼 수 있습니다. 

 

수천 수만 데이터를 다루기 위해서는 전처리 과정을 거쳐야 합니다. 

모델을 구상하고, 반복, 학습만큼 데이터 전처리 과정은 상당히 중요합니다. 

 

1. 기본 구조

import torch
import numpy as np

nums = torch.arange(9)
nums

 

tensor([0, 1, 2, 3, 4, 5, 6, 7, 8])

 

nums.shape

 

torch.Size([9])

 

type(nums)

 

torch.Tensor

 

 nums.numpy()

 

array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=int64)

 

nums.reshape(3,3) # 데이터 전처리하는데 가장 많이 사용

 

tensor([[0, 1, 2],

           [3, 4, 5],

           [6, 7, 8]])

 

randoms = torch.rand((3,3))
randoms

 

tensor([[0.5127, 0.8462, 0.1646],

           [0.1679, 0.7492, 0.1046],

           [0.6169, 0.6014, 0.1144]])

 

zeros = torch.zeros((3,3))
zeros

 

tensor([[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])

 

ones = torch.ones((3,3))
ones

 

tensor([[1., 1., 1.], [1., 1., 1.], [1., 1., 1.]])

 

torch.zeros_like(ones)

 

tensor([[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])

 

 

2. 연산

import torch
import numpy as np

nums = torch.arange(9)
nums * 3

 

tensor([ 0, 3, 6, 9, 12, 15, 18, 21, 24])

 

nums = nums.reshape((3,3))
nums + nums

 

tensor([[ 0, 2, 4],

          [ 6, 8, 10],

          [12, 14, 16]])

 

result = torch.add(nums, 10)

 

array([[10, 11, 12],

        [13, 14, 15],

        [16, 17, 18]], dtype=int64)

 

 

그 외 사칙연산은 이 방법과 전부 일치합니다. 

 

하지만 조금 연산에 주의해야 하는 부분은 곱셈 쪽입니다.

torch는 하나의 행렬(1, 2차)이며, 텐서(3차, dim = 3, rank = 3)로 볼 수 있습니다.

 

  • * : 스칼라 곱
  • dot : 벡터 내적
  • mv : 행렬과 벡터의 곱
  • mm : 행렬과 행렬의 곱
  • matmul : 인수의 종류에 따라서 자동으로 dot, mv, mm을 선택

곱셈의 종류가 상당합니다. 

스칼라 곱, 행렬 곱은 우리들이 공부했던 고등수학에 나오는 것과 일치합니다.

 

하지만 dot은 어떻게 할까요?

dot은 2차에서는 행렬의 곱과 똑같으나 3차 이상에서는 계산이 많이 달라집니다. 

 

수학적인 부분도 중요하나, 저는 코딩 쪽에 비중을 두면서 공부하도록 하겠습니다.  

 

좀 정리하고 보니 별로네요..

코드블럭을 처음 써봤는데 이상하기도 하고,

다음 글은 좀 더 필요하고 유용한 필요한 부분만 써야할 것 같습니다.

728x90
반응형

+ Recent posts