Back to : deep-learning-study
Contents

심층 신경망의 수학적 기초 5강, 6강 (9월 16일, 23일) 에 기반합니다. 이번 내용은 대부분이 코드에 대한 내용이라서, $\LaTeX$ 노트를 변환하지 않고 여기에 바로 작성했습니다.

아직 읽지 않았다면, 최소한 Softmax(링크)MLP(링크)에 대한 포스팅 을, 되도록 링크 에 있는 포스팅 중 shallow-nn과 SVM, LR에 대한 내용을 읽으면 이론적 배경이 충분할 것으로 생각합니다 (제가 이 내용을 공부해서 이해한대로 정리했으니까요..?)

Problem / Dataset

이번에 해결하고자 하는 문제는, MNIST라는 매우 유명한 데이터셋을 이용합니다. MNIST는 Hand-written 숫자로 구성된 데이터셋으로, 널리 알려진 CNN 모델을 처음 제시한 LeCun의 연구에 사용되었던 데이터셋이기도 합니다. 실제로 작동하는 딥 러닝을 만들기에는 너무 작은 데이터셋이지만 공부하는 목적으로 주로 사용됩니다.

각 이미지는 28 by 28 grayscale image로, 편의상 $\R^{28 \times 28}$ 으로 생각하면 됩니다.

가장 먼저 해야할 일은 pytorch module을 import하고, 데이터를 받아오고 정리하는 것입니다.
Pytorch에서는 DataLoader라는 모듈을 이용하여, 편하게 데이터를 Batch로 먹인다거나 하는 작업을 할 수 있습니다.

import torch
import torch.nn as nn
from torch.optim import Optimizer
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import transforms

train_set = datasets.MNIST(root='./mnist_data/', train=True, transform=transforms.ToTensor(), download=True)
test_set = datasets.MNIST(root='./mnist_data/', train=False, transform=transforms.ToTensor(), download=True)
train_loader = DataLoader(dataset=train_set, batch_size=64, shuffle=True)
test_loader = DataLoader(dataset=test_set, batch_size=1, shuffle=False)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

device = 부분은 가능하다면 cuda GPU를 사용하도록 하는 부분인데, 사실 이번 태스크는 너무 작기 때문에 GPU를 쓰면 이득이 없거나 오히려 더 느려질 수도 있습니다.

Softmax Regression

먼저, 우리는 Softmax regression을 시도해 보겠습니다. Pytorch에서는 다음과 같은 과정으로 머신러닝 모델을 학습합니다.

  • Model 정의 : 입력 $x$를 어떤 과정을 거쳐 출력값으로 만들지를 정의합니다. 이 모델에는 훈련가능한 parameter가 있습니다.
  • Loss function 정의 : 모델이 어떤 방법으로 현재 정확도를 측정할지를 정의합니다.
  • 학습 : Training data를 이용하여 모델의 파라미터를 조정합니다.

이제, Model을 정의해야 합니다. Model은 기본적으로 softmax regression에서 공부했던 모델로, $Ax + b$ 를 10개 만들어야 합니다.

여기서, __init__ 을 이용하여 이 모델에서 쓸 Layer들을 정의하고, 그 Layer들을 forward 메서드를 통해 사용해서 넘겨주면 됩니다.

class SoftMax(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer = nn.Linear(28*28, 10, bias=True)
    def forward(self, x):
        return self.layer(x.float().view(-1, 28 * 28))

model = SoftMax()

Softmax Regression은 이렇게 정의된 model에 다음과 같은 최적화 문제를 푸는 방법입니다. \(\underset{a \in \R^{k \times n}, b \in \R^k}{\minimize}\ \frac{1}{N}\sum_{i = 1}^{N} \left(-(a_{Y_i}^T X_i + b_{Y_i} + \log\left(\sum_{j = 1}^{k} e^{a_j^TX_i + b}\right)\right)\)

pytorch에는 $Ax + b$의 결과만 받아내면 이를 계산해주는 함수가 있으므로, 모델은 $Ax + b$만 해주면 됩니다.
대신 Loss function을 다음과 같이 사용합니다. 여기서 lr 은 learning rate, $\alpha$ 값을 의미합니다. 최적화 자체에는 Stochastic Gradient Descent를 사용하겠습니다.

loss_function = torch.nn.CrossEntropyLoss()    
optimizer = torch.optim.SGD(model.parameters(), lr=0.03)   

이제, 우리에게 필요한 모델의 훈련 과정은 이렇게 진행됩니다. Epoch를 많이 돌릴수록 정확해지긴 하지만, 투입하는 시간 대비 얼만큼의 효율이 있는지는 상황마다 다르므로 적당히 판단할 필요가 있습니다.

워낙 단순한 모델이라 학습할게 많지 않으므로 여기서는 epoch=2 (즉, 전체 데이터를 두바퀴 돌립니다) 만 돌리겠습니다.

NUM_EPOCH = 2
for epoch in range(NUM_EPOCH) :
    for images, labels in train_loader :
        optimizer.zero_grad()
        train_loss = loss_function(model(images), labels)
        train_loss.backward()
        optimizer.step()

이를 뜯어보면,

  • optimizer.zero_grad() 로 기존 MLP 모델에 남아있던 gradient 값들을 다 날리고
  • train_loss 는 현재 시점에 모델이 이미지를 받아서 추측을 해보고 그 loss function 값을 확인하고,
  • .backward() 로 현재 시점의 gradient를 계산하고
  • optimizer.step() 으로 실제 optimization (여기선 SGD)를 수행합니다.

그렇다면, 이 모델은 얼마나 정확할까요?

test_loss, correct = 0, 0
for ind, (image, label) in enumerate(test_loader) :
    image, label = image.to(device), label.to(device)
    output = model(image)
    test_loss += loss_function(output, label).item()
    pred = output.max(1, keepdim=True)[1]
    correct += pred.eq(label.view_as(pred)).sum().item()

print(f'''[Test set]\nAverage loss: {test_loss /len(test_loader):.4f}, 
Accuracy: {correct}/{len(test_loader)} ({100. * correct / len(test_loader):.2f}%)''')

이 코드는 테스트셋을 모두 한바퀴 돌리면서, test loss의 값과 결과의 정확도를 확인합니다.

초기화 상황 등에 따라 조금 달라질수는 있을텐데, 저는 2번의 epoch로 92.11%의 정확도를 얻을 수 있었습니다.

Multi-Layer Perceptron

Pytorch는 굉장히 쓰기 편한 모듈인데, 위 코드에서 정말 model만 바꾸면 바로 MLP를 사용해볼수 있습니다. MLP를 수행하면서 점점 개수가 줄어들어야 하는데, 여기서는 크게 중요하지 않으므로 그냥 편의상 점점 줄어드는 이쁜 값을 몇개 적어넣겠습니다.

class MLP(nn.Module) :
    def __init__(self,) :
        super().__init__()
        self.linear = nn.Linear(784, 256, bias=True)
        self.linear2 = nn.Linear(256, 128, bias=True)
        self.linear3 = nn.Linear(128, 64, bias=True)
        self.linear4 = nn.Linear(64, 10, bias=True)
        
    def forward(self, x) :
        x = x.float().view(-1, 784)
        x = nn.functional.relu(self.linear(x))
        x = nn.functional.relu(self.linear2(x))
        x = nn.functional.relu(self.linear3(x))
        x = self.linear4(x)
        return x
    
model = MLP().to(device)

이 모델은 activation function으로 ReLU를 쓰는 depth 4짜리 MLP인데, 784 -> 256 -> 128 -> 64 -> 10으로 단계적으로 개수를 줄여나갑니다. 순서대로 Linear->ReLU->…->Linear로 끝납니다.

위 코드에서 loss_function 정의부터는 그대로 실행하면 됩니다. 저는 로컬에서 정확도가 96% 정도 나오고, 참을성을 갖고 Epoch를 10으로 바꿨을 때는 98%의 정확도를 얻을 수 있었습니다.

Q. 왜 4단계 layer를 (3단계, 5단계가 아니라…) 쓰나요?
-> 일반적으로 Layer가 깊어질수록 파라미터가 많아져서 training 시간이 오래 걸리고 capacity가 커집니다. 반대로, layer가 얕으면 non-linearity를 충분히 주지 못해서 underfitting할 우려가 있습니다. 그렇다고 해서 문제로부터 바로 몇 Layer짜리 MLP를 쓸지 결정할 수 있는 것은 아닙니다. 해보니까 저는 4 Layer 정도가 가장 적당해 보였습니다.