Deep Learning/Computer Vision

Convolutional Neural Network (CNN) 밑바닥부터 구현하기

언킴 2022. 11. 17. 20:21
반응형

Contents

     

     

    CNN은 이미지처리, 자연어처리 등 다양한 분야에서 각광받는 기술이다. 파라미터를 공유하는 개념을 통해서 기존의 연구에서 제안된 단순한 Linear 수준을 넘어서서 매우 우수한 성능을 발휘한다. 이번 글에서는 CNN을 밑바닥부터 구현하는 것을 다룬다. 

     

    Import Packages

     

    from sklearn.datasets import fetch_openml
    from sklearn.utils import check_random_state 
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import StandardScaler 
    
    import matplotlib.pyplot as plt 
    import pandas as pd 
    import numpy as np 
    import torch
    
    %matplotlib inline

     

    Load Data 

     

    X, y = fetch_openml('mnist_784', version=1, return_X_y=True)

    이번 글에서는 sklearn 패키지에서 제공하는 fetch_openml 데이터를 사용하여 CNN을 학습하고자 한다. 이미지처리에서 사용되는 가장 대표적인 MNIST 데이터를 사용한다. MNIST 데이터는 셔츠, 신발 등의 10가지 카테고리를 가지는 이미지로 구성되어 있으며, 28X28 사이즈로 되어있다.

     

    print(f'X shape: {X.shape}')
    print(f'y shape: {y.shape}')
    
    single_sample = X.loc[0,:].values.reshape(28,28)
    print(f'Single sample shape: {single_sample.shape}')
    
    # X shape: (70000, 784)
    # y shape: (70000,)
    # Single sample shape: (28, 28)

    처음 입력으로 받은 $X$는 784 차원을 가지고 있다. 이미지의 경우 가로 세로로 구성되어 있기 때문에 784 차원을 reshape함수를 통해 28X28로 변환한다.

     

    train_samples = 60,000
    
    X_train, X_test, Y_train, Y_test = train_test_split(X, y, train_size=train_samples, test_size=10000, random_state=0)
    
    scaler = StandardScaler()
    X_train = scaler.fit_transform(X_train)
    X_test = scaler.fit_transform(X_test)
    
    
    print(f'Training shape: {X_train.shape}')
    print(f'Testing shape: {X_test.shape}')
    
    
    # Training shape: (60000, 784)
    # Testing shape (10000, 784)

    데이터의 수가 많아서 학습용 이미지 데이터는 6만장, 테스트용 이미지 데이터는 1만장을 추출해서 사용한다. 그 후, StandardSaler를 사용해서 pixel 값을 표준화한다. 

     

    Convolutional Layer

     

    class ConvolutionalLayer:
        def __init__(self, num_kernels, kernel_shape):
            self.num_kernels = num_kernels 
            self.kernel_shape = kernel_shape 
            self.k = self.kernel_shape[0]
            
            self.kernel_theta = torch.randn(self.num_kernels, self.kernel_shape[0], self.kernel_shape[1])
        
        def slider(self, image):
            h, w = image.shape
            for h_idx in range(h - (self.k - 1)): # h: height, w: width
                for w_idx in range(w - (self.k - 1)):
                    single_slide_area = image[h_idx:(h_idx + self.k), w_idx:(w_idx + self.k)]
                    yield single_slide_area, h_idx, w_idx 
        
        def forward(self, images):
            
            assert single_sample.dim() == 2
            
            _, w = images.shape 
            p = 0
            o = (w - self.k) + 1
            print('Padding shape: \t', p)
            print('Output shape: \t', o)
            
            output = torch.zeros((o, o, self.num_kernels))
            
            for single_slide_area, h_idx, w_idx in self.slider(images):
                if h_idx == 0 and w_idx == 0 :
                    print('Region shape: \t', list(single_slide_area.shape))
                    print('Kernel shape: \t', list(self.kernel_theta.shape))
                    print('Single Slide: \t', list(output[h_idx, w_idx].shape))
                
                output[h_idx, w_idx] = torch.sum(single_slide_area * self.kernel_theta, axis=(1, 2))
            output = 1. / (1. + torch.exp(-output))
        
            return output

    CNN에는 stride, padding, kernel 등의 용어가 존재한다. 기본적으로 kernel이 오른쪽과 아래로 움직이면서 학습하기 때문에 slider라고 작명하였다. 오른쪽으로 먼저 움직인 후, 아래로 움직이는 방식이기에 이와 같은 구조를 지닌다. forward 부분에서 마지막 output에는 sigmoid를 함수를 사용하였다. 만약 다른 non-linear 함수를 사용하고 싶으면 사용해도 무방하다.