본문 바로가기

Programming Project/Pytorch Tutorials

Pytorch 머신러닝 튜토리얼 강의 9 (Softmax Classifier)


투명한 기부를 하고싶다면 이 링크로 와보세요! 🥰 (클릭!)

바이낸스(₿) 수수료 평생 20% 할인받는 링크로 가입하기! 🔥 (클릭!)

2018/07/02 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 1 (Overview)

2018/07/02 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 2 (Linear Model)

2018/07/03 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 3 (Gradient Descent)

2018/07/03 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 4 (Back-propagation and Autograd)

2018/07/07 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 5 (Linear Regression in the PyTorch way)

2018/07/09 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 6 (Logistic Regression)

2018/07/10 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 7 (Wide and Deep)

2018/07/11 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 8 (PyTorch DataLoader)



우리는 이전 글에서 MNIST dataset을 어떻게 Loading하는지에 대해서 배워보았습니다.




MNIST dataset은 28*28 픽셀에 숫자 0~9까지의 손글씨를 쓴 이미지 데이터입니다.


이 것을 분별하는 것을 Multi-label Classification Problem이라고 할 수 있기 때문에 이미 이전에 배운 Logistic regression model을 이용해서 해결할 수 있을 것 같다는 생각이 듭니다.




이번 글에서는 실제로 사진 input 데이터를 가지고 어떤 숫자를 쓴 것인지 알아맞춰 보도록 하겠습니다.





하지만 지난번의 모델과는 상황이 조금 다릅니다.


우리는 10개의 label이 있습니다.


그렇기 때문에 output이 y = 0 ~ 9 중 하나인 수가 나와야 합니다.


어떻게 이것을 해결할 수 있을까요?


우리는 행렬곱을 통해서 이 문제를 해결할 수 있습니다.


이전 우리의 model은 맨 마지막 dimension이 1이었기 때문에 (그러니까, 마지막 layer의 마지막 차원이 4 * 1 이었죠) 결과값 y가 1개가 나왔습니다.


만약 마지막 layer의 차원을 10개로 맞춘다면 우리는 N개의 데이터에 대해서 10개의 결과를 받을 수 있게 될 것입니다



* 조금 이해가 가지 않을테니 부연설명을 하도록 하겠습니다.


input N*2개 output N*1개인 상황에서 우리는 input에 2*1행렬을 중간에 곱해서


N*1 행렬을 만들었습니다.


만약 N*2 에 2*10행렬을 곱한다면? 우리는 N*10 행렬을 받아볼 수 있습니다.



그러면 이제 단순히 그 각각 10개에 0~9까지의 수를 mapping 시킬 수 있을 것인데요.



과연 그렇다면 우리는 어떻게 자유분방하게 나오는 0~9까지의 y - output과 (아마도 [0.3 , -2.5 , 3.4 , 1.0 , 2.6 ..... ] 이렇게 10개의 숫자가 나오곘죠) 정답을 비교할 수 있을지 알아봅시다.






Softmax


우리는 Softmax라는 함수를 써서 이 문제를 해결 해 볼 것입니다.


Softmax 함수는 어떤 Linear layer의 output들을 한번에 묶어서 그 결과를 조정해주는 함수인데요.


output들을 한번에 묶어서 처리해주는 다른 예시를 생각해보면, 흔히 전체의 데이터를 Normalize를 하거나, 평균을 0으로 맞춰주는 등의 처리가 데이터를 한번에 묶어서 처리하는 방식 중 하나가 되겠습니다. 






Softmax 함수는 기본적으로 다음과 같은 모양을 띄고 있습니다.


여기서 z는 우리의 output 데이터이고 K는 데이터의 개수입니다.






MNIST처리를 할 우리의 경우 K 는 10이겠네요


수식을 자세히 살펴보면 모든 e^z 의 합으로 를 나눠줍니다.



그러니까, output을 자연상수의 지수로 변환한 수에 총 합을 1로 바꿔주는 것입니다.


 


총 합을 1로 변환 해 주기 때문에, 우리는 이 결과를 '확률'이라고 인식할 수 있고, 그렇기 떄문에 softmax의 의미가 있게 됩니다.




One - Hot Labels


이제 우리의 정답 y를 one hot vector로 바꿉시다.


one hot vector이란 모든 벡터의 원소들 중 하나만 1인 벡터를 의미합니다.


예를 들어, one hot vector로 9를 표현하면 [0,0,0,0,0,0,0,0,0,1] 이 될 수 있겠죠.


그리고 모든 Y의 output과 (이것 역시 10개의 output이겠죠) 각각 CROSS ENTROPY Loss를 계산해주면 우리가 알고있던 logistic regression 에서 활용하던 스킬을 모두 사용할 수 있게 됩니다.


Cross entropy Loss


Cross Entropy Loss는 다음과 같습니다.




정답 Y와 log (우리가 예측한 Y ) 값을 곱해서 뺀 것입니다.


이전 binary classification에서 본 것과 조금 다른데요, multi label classification에서는 CELoss를 이렇게 정답 label에 대해서만 (정답 label만 1이므로...) 적용시킵니다.


이렇게 되면 모든 label을 1로 예측하면  loss가 0이 아니냐! 라고 생각할 수도 있는데요, softmax를 거치기 떄문에 모든 label의 총 합이 1이라서 모든 label을 1로 예측할 수는 없습니다 !





http://ml-cheatsheet.readthedocs.io/en/latest/loss_functions.html 에서 설명이 조금 더 자세히 되어있는 사진을 가져왔습니다.






위 사진은 1 0 0 이 정답인 상황에서 두 가지의 예측에 대한 Cost를 구하는 코드인데요, 정답을 제대로 예측 할 수록 loss가 작은 것을 볼 수 있습니다.








Softmax + CELoss in pytorch


pytorch에서 softmax를 거쳐 CELoss를 구현하는 것은 조금 더 쉽습니다.


import numpy as np
import torch

loss = torch.nn.CrossEntropyLoss()

Y=torch.LongTensor([0])
Y.requires_grad=False

Y_pred1 = torch.Tensor([[2.0,1.0,0.1]])
Y_pred2 = torch.Tensor([[0.5,2.0,0.3]])

l1 = loss(Y_pred1,Y)
l2 = loss(Y_pred2,Y)

print(l1,l2)

tensor(0.4170) tensor(1.8406)


1. 일단 우리가 예측한 Y value 그대로를 CELoss에 넘겨주면 됩니다.


loss 에 Softmax를 거치지 않은 Y_pred가 그대로 들어간 것을 보시면 됩니다.


CELoss에 softmax가 포함되어 있습니다.


2. 정답을 one - hot vector로 넘겨줄 필요 없이 그냥 class label 숫자 하나만 넘겨주면 됩니다. 


조금 더 간편하죠. 우리의 경우 0,1,2중 하나를 넘기면 될 것입니다.





Batch Loss


추가적으로, 더 좋은 기능이 있는데, 우리는 batch loss를 한번에 구할 수 있습니다.


여러가지의 데이터를 한번에 구할 수 있다는 것입니다.


loss = torch.nn.CrossEntropyLoss()


Y=torch.LongTensor([2,0,1])
Y.requires_grad=False

Y_pred1 = torch.Tensor([[2.0,1.0,0.1],
                       [1.0,1.4,0.1],
                       [1.1,0.2,3.1]])
Y_pred2 = torch.Tensor([[0.5,1.0,2.5],
                       [2.2,1.1,0.3],
                       [1.5,2.0,1.3]])

l1 = loss(Y_pred1,Y)
l2 = loss(Y_pred2,Y)

print(l1,l2)
tensor(2.1518) tensor(0.4812)



두 번째 predict의 경우 세가지 경우 모두 맞추었으므로 loss가 상대적으로 낮은 것을 볼 수 있습니다.








자. 그런데 마음대로 Softmax를 함께 취해준다는 점이 마음에 안든다면,


NLL Loss를 쓸 수도 있습니다.


CrossEntropyLoss의 설명에 보면


This criterion combines :func:`nn.LogSoftmax` and :func:`nn.NLLLoss` in one single class.


이라고 나와 있는데요.

( 출처 : https://pytorch.org/docs/stable/_modules/torch/nn/modules/loss.html )


CELoss 는 결국 Softmax + NLLLoss라고 볼 수 있습니다.







Solving MNIST Problem




이제 MNIST 데이터 셋으로 돌아와 봅시다.


MNIST 는 28*28 pixel 의 0~9까지의 손글씨 데이터입니다.


총 784 pixel이죠.




모델은 그럼 어떻게 설계할 수 있을까요?


간단하게 생각 해 보면, 784 * 10의 linear model 하나를 만들어서 예측할 수 있을 것입니다.


하지만 Deep neural network를 생각해보면 몇개의 input과 output  layer사이에 Hidden Layer을 넣는 것도 생각해 볼 수 있습니다.





이렇게 모델을 설계할 경우 우리는 파이토치로



이렇게 코딩 할 수 있습니다.


적당히 layer간의 input과 output을 맞춰주고 맨 마지막 layer에서 10개의 output을 뽑아주게 되면 총 10개의 class중 하나를 맞추게 될 것입니다.









Training , Test Dataset


이번 글에서는 데이터셋을 두개로 분리해서 진행하도록 할 것입니다.


Training 데이터와 Test 데이터입니다.


training cycle에서는 모델을 학습시키며 모델의 파라미터를 조정하는 과정을 거치고, 


test cycle에서는 모델을 평가하는 작업을 진행하도록 할 것입니다.


이렇게 데이터를 나누는 이유는 train할 때는 데이터 자체를 과 학습(Over-fitting) 해버려서 이미 학습이 진행된 데이터에 대해서만 높은 accuracy가 나오는 경우가 있기 때문에, 한번도 보지 않은 테스트 데이터가 있어야 그것을 공정하게 평가할 수 있기 때문입니다.





Code


이제 본격적으로 프로그래밍을 시작하도록 하겠습니다.


참고로 이 글에서는 sung kim 님의 강의에 없지만 cuda를 통해 gpu 가속이 가능하다면 gpu 가속 또한 진행해보도록 하겠습니다.



우선 선언 부분


import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms

#마법의 단어 device~
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')


저 device 부분이 만약 cuda가 작동할 수 있는 상태라면 device를 cuda로 설정해주고, 그렇지 않다면 그냥 cpu로 설정해주는 부분입니다.


추후, 우리가 연산할 것들이 늘어남에 따라 gpu 가속이 필요해서 넣어 보았습니다.



batch_size = 64

# MNIST Dataset
train_dataset = datasets.MNIST(root='./mnist_data/',
                               train=True,
                               transform=transforms.ToTensor(),
                               download=True)

test_dataset = datasets.MNIST(root='./mnist_data/',
                              train=False,
                              transform=transforms.ToTensor())

# Data Loader (Input Pipeline)
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)

그리고 batch_size를 정하고, 파이토치 MNIST 데이터 set을 DataLoader로 불러와 보도록 하겠습니다.


잘 보면 옵션에서 train = True 와 train = False 로 설정해주는 부분이 있습니다.


그 부분이 설정에 있어서 중요합니다.



그리고 모델을 작성 해 보도록 하겠습니다

class NNModel(torch.nn.Module):
    
    def __init__(self):
        super(NNModel,self).__init__()
        self.l1 = nn.Linear(784,520)
        self.l2 = nn.Linear(520,320)
        self.l3 = nn.Linear(320,240)
        self.l4 = nn.Linear(240,120)
        self.l5 = nn.Linear(120,10)
        
    def forward(self,x):
        # input data : ( n , 1 , 28 , 28 )
        x = x.view(-1,784) # Flatten : ( n , 784 )
        x = F.relu(self.l1(x))
        x = F.relu(self.l2(x))
        x = F.relu(self.l3(x))
        x = F.relu(self.l4(x))
        return self.l5(x)

5개의 layer로 되어있는 fully - connected neural net 입니다.


이제 모델을 만들고 학습시켜보도록 하겠습니다.


중요한 것은 to(device)부분인데, 이 to device부분이 파라미터들을 앞서 가능하다면 세팅해놨던 gpu로 올리는 과정입니다.


model = NNModel()

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(),lr = 0.01, momentum = 0.5)

model= model.to(device)

def train(epoch):
    model.train()
    for batch_idx,(data,target) in enumerate(train_loader):
        
        data = data.to(device)
        target = target.to(device)
        
        output = model(data)

        optimizer.zero_grad()
        loss = criterion(output,target)
        loss.backward()
        optimizer.step()
        
        if batch_idx%50==0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),100. * batch_idx / len(train_loader), loss.data[0]))
            
def test():
    model.eval()
    test_loss=0
    correct=0
    for data,target in test_loader:
        
        data = data.to(device)
        target = target.to(device)
        
        output = model(data)
        
        test_loss += criterion(output,target).data[0]
        
        pred = output.data.max(1,keepdim=True)[1]
        correct += pred.eq(target.data.view_as(pred)).sum()
        
    test_loss /= len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))
            
            
for epoch in range(1, 9):
    train(epoch)
    test()

이전에 비해서 크게 달라진 부분만 언급하자면,


train 과 test용 함수를 나눠서 진행했고,


test loss를 따로 만들어서 평가를 진행했습니다.



torch.max 함수는 주어진 텐서 배열의 최대 값이 들어있는 index를 리턴하는 함수입니다.


Y_pred = [ [0.3,0.2,0.9,0.1] ] 의 경우 torch.max(Y_pred.data , 1 ) 의 결과는 0.9의 인덱스인 2가 됩니다.


torch.max(Y_pred.data , ) 뒤에 들어가는 1 은 dimension에 대한 것인데요, 우리는 64 * 10개의 Y_pred 값을 한번에 넣어주고  64개의 예측한 값을 받아와야 합니다.


하지만 어떤 단위로 max값을 받아올 것인지 정해주지 않으면 그냥 max 함수는 전체의 640개의 element중 최대의 인덱스를 리턴하고 말 것입니다.



pred.eq(data)는 pred배열과 data가 일치하느냐를 검사하는데요, 


그 뒤에 .sum()을 붙임으로 인해서 일치하는 것들의 개수의 합을 숫자로 출력하게 됩니다.


참고로 view_as(pred) 이게 완전 비교할 때의 꿀팁이니 검색같은거 해보셔서 그 용도를 잘 알아두시기 바랍니다.




여튼 이렇게 하면


Train Epoch: 1 [0/60000 (0%)]	Loss: 2.307084
Train Epoch: 1 [3200/60000 (5%)]	Loss: 2.305902
Train Epoch: 1 [6400/60000 (11%)]	Loss: 2.297302


... 이렇게 시작해서

Train Epoch: 8 [9600/60000 (16%)]	Loss: 0.150206
Train Epoch: 8 [12800/60000 (21%)]	Loss: 0.153663
Train Epoch: 8 [16000/60000 (27%)]	Loss: 0.050358
Train Epoch: 8 [19200/60000 (32%)]	Loss: 0.057363
Train Epoch: 8 [22400/60000 (37%)]	Loss: 0.030667
Train Epoch: 8 [25600/60000 (43%)]	Loss: 0.157177
Train Epoch: 8 [28800/60000 (48%)]	Loss: 0.106968
Train Epoch: 8 [32000/60000 (53%)]	Loss: 0.189679
Train Epoch: 8 [35200/60000 (59%)]	Loss: 0.110058
Train Epoch: 8 [38400/60000 (64%)]	Loss: 0.031125
Train Epoch: 8 [41600/60000 (69%)]	Loss: 0.065067
Train Epoch: 8 [44800/60000 (75%)]	Loss: 0.032619
Train Epoch: 8 [48000/60000 (80%)]	Loss: 0.167236
Train Epoch: 8 [51200/60000 (85%)]	Loss: 0.041331
Train Epoch: 8 [54400/60000 (91%)]	Loss: 0.100234
Train Epoch: 8 [57600/60000 (96%)]	Loss: 0.043159

Test set: Average loss: 0.0018, Accuracy: 9657/10000 (96%)



마지막에 96%정도의 정답률이 나오네욤!



고생하셨습니다 



숙제로는 다른 데이터로 한번 진행해보자고 합니다.