[AITech] 20220208 - CNN Basics

3 minute read


학습 내용

Convolution

일반적인 다층 신경망(MLP)은 각 뉴런들이 선형 모델과 활성 함수로 모두 연결된(fully connected) 구조이다.

Convolution 연산은 이와 달리 커널(필터)입력 벡터 상에서 움직여가면서 선형모델과 함성 함수가 적용되는 구조이다.

  • 컨볼루션 연산의 수학적 의미는 신호를 커널을 이용해 국소적으로 증폭 또는 감소시켜서 정보를 추출 또는 필터링하는 것이다.

    image-20220121111508711

  • CNN에서 사용하는 연산은 엄밀히 말하면 convolution 연산이 아니고 cross-correlation 연산이다. 하지만 그 의미에서 convolution 연산과 큰 차이가 없기 때문에 옛날부터 convolution 연산으로 통칭한다.

  • 커널은 정의역 내에서 움직여도 변하지 않고(translation invariant) 주어진 신호에 국소적(local)으로 적용한다.

  • 컨볼루션 연산은 1차원 뿐 아니라 다양한 차원에서 계산 가능하다.

    • 데이터의 성격에 따라 사용하는 커널이 달라진다.

    image-20220121111857484

  • 2D-Conv 연산은 아래와 같이 나타낼 수 있다.

    image-20220121112045046

    • 컨볼루션 연산에서 사용하는 용어로 스트라이드패딩이라는 것이 있다.
      • 스트라이드: 컨볼루션 연산을 위해 한 번에 필터(커널)를 이동시키는 칸 수
      • 패딩: 컨볼루션 결과의 크기를 조정하기 위해 입력 배열의 둘레를 확장하고 0으로 채우는 연산

    image-20220121112643098

    • 결과적으로 입력의 크기 (OH, OW), 커널(필터)의 크기(FH, FW), 패딩의 폭 P, 스트라이드 크기 S를 안다면 출력의 크기는 다음과 같이 구할 수 있다.

      image-20220121112842880

  • 채널이 여러 개인 3D-Conv 이상의 다차원 컨볼루션 연산의 경우 커널의 채널 수와 입력의 채널 수가 같아야 한다. (rank가 동일해야 함)

    • 이 경우 3차원 입력과 3차원 커널을 통해 출력의 채널 크기는 1이 되며, 채널의 크기를 Oc로 만들고 싶다면 커널을 Oc개 사용하면 된다.

    image-20220121113425774

Convolution의 역전파

  • 컨볼루션 연산은 커널이 모든 입력 데이터에 공통으로 적용되기 때문에 역전파를 계산할 때도 convolution 연산이 나오게 된다.

image-20220121114159506


CNN Architecture

앞까지의 CNN 기초 내용은 지금껏 수없이 보고 들어왔기에 어려움이 없을 것입니다.

다만, 이 강의에서는 CNN 구조의 (학습가능한) 파라미터 개수를 계산할 수 있는 것에 주안점을 두고 있습니다. 정확한 수치는 아니더라도, 십만 대, 백만 대 등과 같이 단위 정도는 가늠할 수 있어야 CNN 모델을 적절히 선택하고 사용할 수 있다는 것입니다.

CNN architecture는 다음과 같이 [Convolution-Pooling]*N - FC 층이 연결되어 있는 구조를 보입니다. 아래 구조는 AlexNet의 구조입니다.

image-20220208102830520

그리고 컨볼루션 층의 파라미터 개수는 다음과 같이 구할 수 있습니다.

image-20220208102250704

  • 3 x 3 x 128 x 64 = 73,728

그러면, 마지막으로 위의 AlexNet 모델의 각 층의 파라미터 개수를 구해보면서 포스팅 마치겠습니다. 모두들 왜 숫자가 저렇게 나오는지 계산해보세요!

image-20220208102416422


CNN 실습

이번에도 똑같이 CNN 구조를 code level에서 간단히 살펴보도록 하겠습니다.

  • Define Model

    • 모델의 깊이를 쉽게 커스터마이징 할 수 있도록 설계합니다.
    • Convolution, Batch norm, Pooling, Dropout, ReLU, Dense 층을 사용합니다.
    class ConvolutionalNeuralNetworkClass(nn.Module):
        # 깊이를 custom할 수 있도록 설계
        # cdims에는 convolution layer들의 채널 수, hdims에는 fully connected layer들의 뉴런 수 전달
        def __init__(self,name='cnn',xdim=[1,28,28],
                     ksize=3,cdims=[32,64],hdims=[1024,128],ydim=10,
                     USE_BATCHNORM=False):
            super(ConvolutionalNeuralNetworkClass,self).__init__()
            self.name = name
            self.xdim = xdim
            self.ksize = ksize
            self.cdims = cdims
            self.hdims = hdims
            self.ydim = ydim
            self.USE_BATCHNORM = USE_BATCHNORM
      
            ### 1. Convolutional layers
            self.layers = []
            prev_cdim = self.xdim[0]
            for cdim in self.cdims: # for each hidden layer
                self.layers.append(
                    nn.Conv2d( # convolution
                        in_channels=prev_cdim,  # 입력의 채널 개수
                        out_channels=cdim,      # 출력의 채널 개수(사용할 커널 개수와 동일)
                        kernel_size=self.ksize, # 커널의 (w,h)
                        stride=(1,1),           # stride
                        padding=self.ksize//2   # padding(input과 output의 (h,w)가 같도록 패딩)
                    )
                 ) 
                # BN, Dropout layer와 같은 train-test 시 동작이 다른 layer들 때문에 train/eval 모드를 구분!
                if self.USE_BATCHNORM:
                    self.layers.append(nn.BatchNorm2d(cdim))                      # batch-norm
                self.layers.append(nn.ReLU(True))                                 # activation
                self.layers.append(nn.MaxPool2d(kernel_size=(2,2), stride=(2,2))) # max-pooling 
                self.layers.append(nn.Dropout2d(p=0.5))                           # dropout
                prev_cdim = cdim
      
            ### 2. Dense layers
            self.layers.append(nn.Flatten())
            prev_hdim = prev_cdim*(self.xdim[1]//(2**len(self.cdims)))*(self.xdim[2]//(2**len(self.cdims)))
            for hdim in self.hdims:
                self.layers.append(nn.Linear(prev_hdim, hdim, bias=True)) # Fully connected
                self.layers.append(nn.ReLU(True))                         # activation
                prev_hdim = hdim
                  
            ### 3. Final layer (without activation)
            self.layers.append(nn.Linear(prev_hdim,self.ydim,bias=True))
      
            ### 4. Concatenate all layers 
            self.net = nn.Sequential()
            for l_idx,layer in enumerate(self.layers):
                layer_name = "%s_%02d"%(type(layer).__name__.lower(),l_idx)
                self.net.add_module(layer_name,layer) # add_module의 장점: 이름을 커스터마이징
                  
            ### 5. initialize parameters
            self.init_param()
              
        def init_param(self):
            for m in self.modules():
                if isinstance(m,nn.Conv2d): # init conv
                    nn.init.kaiming_normal_(m.weight)
                    nn.init.zeros_(m.bias)
                elif isinstance(m,nn.BatchNorm2d): # init BN
                    nn.init.constant_(m.weight,1)
                    nn.init.constant_(m.bias,0)
                elif isinstance(m,nn.Linear): # lnit dense
                    nn.init.kaiming_normal_(m.weight)
                    nn.init.zeros_(m.bias)
                  
        def forward(self,x):
            return self.net(x)
      
          
    C = ConvolutionalNeuralNetworkClass(
        name='cnn',
        xdim=[1,28,28],     # (28,28) grayscale
        ksize=3,            # kernel size = (3,3)
        cdims=[32,64],      # channel: 32, 64
        hdims=[32],         # FC: 32
        ydim=10).to(device) # final output: 10
    loss = nn.CrossEntropyLoss()
    optm = optim.Adam(C.parameters(),lr=1e-3)
    
  • Check parameters

    • Convolution, Dense layer에 있는 trainable parameters의 수와 형태를 나타낸 것입니다.
    np.set_printoptions(precision=3)
    n_param = 0
    '''activation function, Pooling, Dropout 등의 layer는 trainable parameter가 없음!'''
    for p_idx,(param_name,param) in enumerate(C.named_parameters()):
        if param.requires_grad:
            param_numpy = param.detach().cpu().numpy() # to numpy array 
            n_param += len(param_numpy.reshape(-1))
            print ("[%d] name:[%s] shape:[%s]."%(p_idx,param_name,param_numpy.shape))
            print ("    val:%s"%(param_numpy.reshape(-1)[:5]))
    print ("Total number of parameters:[%s]."%(format(n_param,',d')))
    '''
    [0] name:[net.conv2d_00.weight] shape:[(32, 1, 3, 3)].
        val:[-0.457 -0.491  0.172  0.296 -0.467]
    [1] name:[net.conv2d_00.bias] shape:[(32,)].
        val:[0. 0. 0. 0. 0.]
    [2] name:[net.conv2d_04.weight] shape:[(64, 32, 3, 3)].
        val:[ 0.04   0.075 -0.042  0.065 -0.011]
    [3] name:[net.conv2d_04.bias] shape:[(64,)].
        val:[0. 0. 0. 0. 0.]
    [4] name:[net.linear_09.weight] shape:[(32, 3136)].
        val:[ 0.007 -0.009  0.001  0.01  -0.033]
    [5] name:[net.linear_09.bias] shape:[(32,)].
        val:[0. 0. 0. 0. 0.]
    [6] name:[net.linear_11.weight] shape:[(10, 32)].
        val:[-0.223  0.028 -0.329  0.379 -0.05 ]
    [7] name:[net.linear_11.bias] shape:[(10,)].
        val:[0. 0. 0. 0. 0.]
    Total number of parameters:[119,530].
    '''
    
  • Training

    • Training code는 MLP와 달라진 게 없기 때문에 생략합니다.
    • 다시 말하지만, 달라지는 것은 클래스 내의 코드들입니다.



참고 자료


Categories: ,

Updated:

Leave a comment