굴러가는 분석가의 일상

[CNN] 합성곱 신경망 Pytorch 구현 본문

Computer Vision

[CNN] 합성곱 신경망 Pytorch 구현

G3LU 2024. 2. 21. 20:29

※ 본 게시물은 MNIST 데이터를 기반으로 합성곱 신경망(CNN)을 구현하도록 해보겠습니다. 앞서 CNN은 크게 아래와 같은 구성 요소로 이루어져있습니다. 

  • Convolution Layer(합성곱 층) : 이미지 특성 추출 
  • Pooling Layer(풀링 층) : 이미지의 특성 축약 → 주로 Max-Pooling 사용
  • Fully Conntected Network Layer(완전연결 신경망) : 추출 및 축약된 특징(=n차원)의 데이터를 1차원의 데이터로 변환하며, Softmax 활성화 함수를 통해 Multi-class 아웃풋 도출 

 

 

📌  Import Library

import torch 
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
#torch.device를 사용함으로써 GPU 설정이 안되어 있다면, CPU 사용

 

📌  Set Hyper-parameter

CNN을 학습할 때, 지정 해줘야하는 파라미터가 총 4가지가 아래와 같이 존재합니다. 이를 적절하게 설정하는 것이 매우 중요하므로, 여러가지의 경우의 수를 통해 설정해보시길 권합니다. 

num_epochs = 5
num_classes = 10
batch_size = 100
learning_rate = 0.001

 

📌  Load Dataset 

본 게시물에서는 MNIST 데이터를 기반으로 CNN을 구성하도록 해보겠습니다. 

# MNIST dataset
#transforms.ToTensor() : 함수를 통해 모델에 입력하기 위해 텐서로 변환
train_dataset = torchvision.datasets.MNIST(root='dataset',
                                           train=True, 
                                           transform=transforms.ToTensor(),
                                           download=True)

test_dataset = torchvision.datasets.MNIST(root='dataset',
                                          train=False, 
                                          transform=transforms.ToTensor())

# Data loader
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
                                           batch_size=batch_size, 
                                           shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
                                          batch_size=batch_size, 
                                          shuffle=False)

📌  Data  Check 

for (X_train, y_train) in train_loader:
    print('X_train:', X_train.size(), 'type:', X_train.type())
    print('y_train:', y_train.size(), 'type:', y_train.type())
    break
    
 --------- output----------  
#X_train: torch.Size([100, 1, 28, 28]) type: torch.FloatTensor
#y_train: torch.Size([100]) type: torch.LongTensor
 

📌  Data  Visualization

pltsize = 1
plt.figure(figsize=(10 * pltsize, pltsize)) #10개 plot하기 위한 figure 크기 설정

for i in range(10):
    plt.subplot(1, 10, i + 1) # plot.subplot(rows, columns, index)
    plt.axis('off')
    plt.imshow(X_train[i, :, :, :].numpy().reshape(28, 28), cmap = "gray_r")
    plt.title('Class: ' + str(y_train[i].item()))

 

📌  Define CNN Model 

먼저 class를 통해 CNN를 정의해보겠습니다. torch.nn.Module은 모든 뉴럴 네트워크 모듈의 기본 클래스이며, 이를 통해 모델을 정의하고 구축하는데 사용되는 기본 클래스입니다. nn.Module을 상속받아서 모델의 구조화, 모델 파라미터 추적 등 다양한 기본적인 기능을 사용할 수 있게 만들어줍니다.

 

ConvNet 모델을 만들 때, 아래와 같은 매서드를 사용하게 됩니다. 

 

-. super() 메서드 : 부모 클래스의 매서드를 호출할 때 사용됩니다. 이를 사용함으로써 코드의 가독성 향상이 될뿐만 아니라, 다중 클래스 상속이 필요한 상황에서 다중 상속의 복잡성을 관리할 수 있습니다. 

클래스(부모 vs. 자식) 상속을 이해하기 위해서 예를 들어보도록 하겠습니다.
예를 들어, 부모님이 만약 저에게 10억원의 현금과 강남의 10채 아파트를 상속하셨다면, 제가 돈을 벌기 위해 굳이 노력하지 않아도 됩니다. 이는 코딩에서도 동일하게 적용됩니다. 부모 클래스의 속성과 메서드를 상속받게 되면, 굳이 자식 클래스를 따로 설정하지 않아도 되는 것입니다. 

 

-. nn.Sequential() : 신경망 모델을 순차적으로 정의하는데 사용됩니다. 모델을 여러 개의 레이러로 구성할 때 유용하며, 순차적으로 레이어를 쌓아 간단하게 모델을 구축할 수 있습니다. 

 

-. init() 메서드 : 신경망 레이어의 구성요소(합성곱층, 풀링층, 완전연결층)들을 정의하며, 클래스의 초기화를 담당하게 됩니다. 이를 통해 모델이 사용할 모든 파라미터를 초기화하고, 필요한 레이어를 준비할 수 있게 됩니다

 

-. Forward() 메서드 : 말그대로 순전파를 뜻합니다. 입력 데이터를 받아서 모델 내의 각 레이어를 통과시키고, 최종적으로 출력을 생성합니다. 이를 올바르게 정의해야 순전파 과정에서 미분을 통해 그래디언트를 자동으로 계산할 수 있게 하므로 매우 중요합니다. 

 

이러한 메서드를 사용하여 CNN의 모델을 구축하도록 해보겠습니다. 

class ConvNet(nn.Module):
  def __init__(self, num_classes=10):
      super(ConvNet,self).__init__()
      self.layer1 = nn.Sequential(
          nn.Conv2d(1, 16, kernel_size=5, stride=1, padding=2),
          nn.BatchNorm2d(16),
          nn.ReLU(),
          nn.MaxPool2d(kernel_size=2, stride=2))
      self.layer2 = nn.Sequential(
          nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=2),
          nn.BatchNorm2d(32),
          nn.ReLU(),
          nn.MaxPool2d(kernel_size=2, stride=2))
      self.fc = nn.Linear(7*7*32, num_classes)


  def forward(self,x):
      out = self.layer1(x)
      out = self.layer2(out)
      out = out.reshape(out.size(0),-1)
      out = self.fc(out)
      return out 
  
model = ConvNet(num_classes).to(device)

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr = learning_rate)

 

self.fc = nn.Linear()의 크기에 대해 알아보도록 하겠습니다. n번째 합성곱 레이어의 출력 크기를 계산하는 수식은 아래와 같습니다. 

 

위의 모델에 주어진 파라미터에 따라 수식에 대입한다면, 28이라는 결과가 도출됩니다. 

  • 입력 이미지 크기: 28x28 
  • 커널 크기 : 5x5 
  • 패딩 : 2
  • 스트라이드 : 1 

이에 저희는 첫 번째 합성곱 레이어를 통과한 후의 특징 맵 크기는 28이라는 것을 알 수 있습니다. 따라서 두번 째 합성곱 레이어의 입력으로 들어가는 특징 맵의 크기는 28x28이 됩니다. 이후, 두 번째 합성곱 레이어를 통과하면 특징 맵 크기는 32가 됩니다. 

 

또한, CNN 풀링층에서는 최대 풀링을 수행하기 때문에 각 레이어를 통과할 때, 특징 맵의 크기는 절반으로 줄게 됩니다. 28x28 크기의 입력 데이터가 두번의 레이어를 통과하게 되므로, 7x7 이라는 크기로 축약이 됩니다. 

 

이에, nn.Linear(7*7*32)가 도출되는 것을 볼 수 있습니다. 

 

📌 Training CNN Model

total_step = len(train_loader)
for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):
        images = images.to(device)
        labels = labels.to(device)
        
        # Forward pass
        outputs = model(images)
        loss = criterion(outputs, labels)
        
        # Backward and optimize
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        if (i+1) % 100 == 0:
            print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
                   .format(epoch+1, num_epochs, i+1, total_step, loss.item()))
Epoch [1/5], Step [100/600], Loss: 0.0972
Epoch [1/5], Step [200/600], Loss: 0.1998
Epoch [1/5], Step [300/600], Loss: 0.0544
Epoch [1/5], Step [400/600], Loss: 0.0513
Epoch [1/5], Step [500/600], Loss: 0.0127
Epoch [1/5], Step [600/600], Loss: 0.1939
Epoch [2/5], Step [100/600], Loss: 0.0387
Epoch [2/5], Step [200/600], Loss: 0.0479
Epoch [2/5], Step [300/600], Loss: 0.0141
Epoch [2/5], Step [400/600], Loss: 0.0213
Epoch [2/5], Step [500/600], Loss: 0.0635
Epoch [2/5], Step [600/600], Loss: 0.0267
Epoch [3/5], Step [100/600], Loss: 0.0462
Epoch [3/5], Step [200/600], Loss: 0.0139
Epoch [3/5], Step [300/600], Loss: 0.0265
Epoch [3/5], Step [400/600], Loss: 0.0286
Epoch [3/5], Step [500/600], Loss: 0.0207
Epoch [3/5], Step [600/600], Loss: 0.0047
Epoch [4/5], Step [100/600], Loss: 0.0526
Epoch [4/5], Step [200/600], Loss: 0.0315
Epoch [4/5], Step [300/600], Loss: 0.0304
Epoch [4/5], Step [400/600], Loss: 0.0127
Epoch [4/5], Step [500/600], Loss: 0.0245
Epoch [4/5], Step [600/600], Loss: 0.0064
Epoch [5/5], Step [100/600], Loss: 0.0021
Epoch [5/5], Step [200/600], Loss: 0.0268
Epoch [5/5], Step [300/600], Loss: 0.0413
Epoch [5/5], Step [400/600], Loss: 0.0095
Epoch [5/5], Step [500/600], Loss: 0.0102
Epoch [5/5], Step [600/600], Loss: 0.0237

 

📌 Testing CNN Model 

# Test the model
model.eval()  # eval mode (batchnorm uses moving mean/variance instead of mini-batch mean/variance)
with torch.no_grad():
    correct = 0
    total = 0
    for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

    print('Test Accuracy of the model on the 10000 test images: {} %'.format(100 * correct / total))

 

Test Accuracy of the model on the 10000 test images: 99.05 %