본문 바로가기
딥러닝/모두를 위한 딥러닝 2

06 소프트맥스 회귀 softmax regression

by 혜 림 2021. 3. 31.

 target 변수가 multi - class 인 명목형 변수일 때 사용하는 방법 

 

참고한 사이트

wikidocs.net/book/2788

 

개요

 

1. 원 - 핫 인코딩

2. 소프트맥스 회귀 구현

  - 소프트맥스 회귀 

  - 비용함수

3. 코드구현

 

 

1. 원 - 핫 인코딩

 

- 범주형 데이터를 처리할 때 레이블 표현하는 방법

 

ex. 한국 = [1, 0, 0]

    중국 = [0, 1, 0]

    일본 = [0, 0, 1]

 

=> class 의 수만큼 변수가 필요함

 

* 왜 클래스 전체에 인코딩하지 않는가?

 

ex. 한국 = 0, 중국 = 1, 일본 = 2

 

일본과 중국의 다름이, 한국과 일본의 다름과 수치상으로 비교가 되버림

실은 다를뿐, 일본과 한국 사이의 차이가 일본과 중국 사이의 차이보다 큰 것을 의미하지 않기 때문

 

이러한 점에서 원 - 핫 인코딩은 무작위성을 지닌다

 

그러나 매우 비효율적이다

왜냐하면  class 수만큼 변수가 추가로 생기기 때문

2. 소프트맥스 회귀 구현

display(iris.head())

다음과 같은 데이터셋이 있다고 가정하자 (유명한 데이터 셋인 iris) 

이때 변수는 X1, X2, X3, X4(식물 특성) 총 4개이고, 우리의 target(식물 종류) 은 클래스가 3인 명목변수다

  - 소프트맥스 회귀 

 

softmax 함수는 각각의 클래스에 속할 확률을 구하는 함수다.

그 확률은 아래와 같이 구한다.

 

위의 iris 데이터 셋의 경우, 아래와 같이 표현할 수 있겠다.

 

그런데 이상하지 않은가? 우리에게는 변수가 4개나 있는데 softmax 함수의 입력값은 3개의 scalar만 필요로 한다.

그래서 우리는 가중치를 통해 입력 벡터를 적절하게 다른 벡터로 변환한다. 아래의 z 벡터가 그것이다.

 

전체 과정을 도식화하면 다음과 같다.

 

결국 가설함수 H(X)=softmax(XW+b) 의 형태가 된다

앞서서 보았던 로지스틱 회귀의 가설함수는 H(X)=sigmoid(XW+b)이다

 

  - 비용함수

 

우리는 크로스 엔트로피 함수를 쓴다.

 

한 데이터, 전체 데이터에 대한 비용함수는 다음과 같다.

하지만 이는 결코 새로운 것이 없다. 로지스틱 함수의 경우와 똑같다.

로지스틱 함수의 경우, 단지 class가 2로 고정된 것 뿐이다. 그래서 k에 2를 대입해보자. 

 

 

어떤가 똑같지 않은가? 

 

3. 코드구현

0. 사전

import torch
import torchvision.datasets as dsets
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import torch.nn as nn
import matplotlib.pyplot as plt
import random

아래 코드는 CPU로 학습할 것인지, GPU로 학습할 것인지 정하는 부분이다.

간단하게 설명하자면 CPU는 대장인데 GPU는 CPU 부하지만, 그 중에서도 반복 연산은 잘하는 놈이라고 할 수 있다.

그러니까 간단히 반복하는 업무는 GPU에게 맡기고 CPU는 좀 복잡한 일을 수행한다고 생각하면 될 듯!

하지만 우리가 구현할 코드는 간단하기 때문에 아무래도 좋다. 다만 누구를 쓰는지 짚고만 넘어가자.

 

USE_CUDA=torch.cuda.is_available()
device=torch.device("cuda" if USE_CUDA else"cpu")
print("다음 기기로 학습합니다", device)

random.seed(777)
torch.manual_seed(777)

if device=="cuda":
    torch.cuda.manual_seed(777)
training_epochs=15 # epoch 시행 횟수
batch_size=100 # 미니 배치 사이즈

 

한편, 위에서는 iris 데이터 셋을 예로 들었지만, 코드 구현에서 사용할 데이터셋은 MNIST다

아마 머신러닝, 딥러닝 공부하는 사람이라면 한 번쯤 보거나 들어봤을 셋이다

 

https://wikidocs.net/60324

손글씨로 0-9까지 모아둔 데이터 셋이다

컴퓨터가 손글씨를 보고 무슨 숫자인지 알게 하기 위한 훈련 데이터 셋이라고 생각하면 된다

 

이 하나의 손글씨 사진은 28 pixel * 28 pixel로 이루어져 있다

그래서 원래는 (batch size, 1, 28 ,28) 로 매우 큰 차원이다. 

하지만 차원을 줄이면 (batch size, 28*28)로 바꿀 수 있을것이다. 그게 위에 있는 784  pixels의 의미다.

 

불러와보자

pytorch에 mnist 데이터 셋을 불러오는 기능이 있는데, 이를 이용해서 불러와봤다

 

mnist_train = dsets.MNIST(root='MNIST_data/',
                          train=True,
                          transform=transforms.ToTensor(),
                          download=True)


mnist_test = dsets.MNIST(root='MNIST_data/',
                         train=False,
                         transform=transforms.ToTensor(),
                         download=True)

그 다음에 미니 배치를 나눈다

한 배치당 100이다

전체 sample은 60,000이므로 

그러면 우리는 한 epoch를 위해서 size가 100인 배치를 약 600번 iteration 하게 된다

 

data_loader = DataLoader(dataset=mnist_train,
                                          batch_size=batch_size, # 배치 크기는 100
                                          shuffle=True,
                                          drop_last=True)

아래는 모델 관련 설정이다 

linear=nn.Linear(784,10,bias=True).to(device) # variable 설정

criterioin=nn.CrossEntropyLoss().to(device) # softmax 함수의 비용함수 정의
optimizer=torch.optim.SGD(linear.parameters(),lr=0.1) # 모델의 가중치를 갱신할 Optimizer를 정의

 

1. 이제 epoch를 달려보자(문법 상 맞는 말인지 아닌지 모른다 신나니까 됐다)

for epoch in range(training_epochs):
    avg_cost=0
    total_batch=len(data_loader)
    
    for X,Y in data_loader:
        X=X.view(-1,28*28).to(device)
        Y=Y.to(device)
        
        optimizer.zero_grad()
        hypothesis=linear(X)
        cost=criterioin(hypothesis,Y)
        cost.backward()
        optimizer.step()
        
        avg_cost+=cost/total_batch
        
    print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.9f}'.format(avg_cost))
    
print('Learning finished')

15번만 시행을 해서 눈에 띄게 cost가 줄지는 않는다 대신 줄고 있다는 것이 중요하다!

784라는 어마무시한 변수 크기 때문인지, 15번 시행하는 것도 만만치 않다

그러면 이렇게 만든 모델을 한 번 test 셋에 시험해보자

 

with torch.no_grad(): # torch.no_grad()를 하면 gradient 계산을 수행하지 않는다.
    X_test = mnist_test.test_data.view(-1, 28 * 28).float().to(device)
    Y_test = mnist_test.test_labels.to(device)

    prediction = linear(X_test)
    correct_prediction = torch.argmax(prediction, 1) == Y_test
    accuracy = correct_prediction.float().mean()
    print('Accuracy:', accuracy.item())

    # MNIST 테스트 데이터에서 무작위로 하나를 뽑아서 예측을 해본다
    r = random.randint(0, len(mnist_test) - 1)
    X_single_data = mnist_test.test_data[r:r + 1].view(-1, 28 * 28).float().to(device)
    Y_single_data = mnist_test.test_labels[r:r + 1].to(device)

    print('Label: ', Y_single_data.item())
    single_prediction = linear(X_single_data)
    print('Prediction: ', torch.argmax(single_prediction, 1).item())
    
    plt.imshow(mnist_test.test_data[r:r + 1].view(28, 28),
		cmap="Greys", interpolation="nearest")
    plt.show()

 

엇 혹시 당신 imshow를 시행하면 커널이 죽지 않는가?? 그럴 경우를 대비해서 접은 글을 써놓았다

 

더보기

원인은 다음의 사이트를 참고하자

jovian.ai/forum/t/jupyter-kernel-died-after-plt-imshow-tensor-obj/14830/2

 

Jupyter kernel died after plt.imshow(Tensor Obj.)

Can you check in your terminal if there’s an error?

jovian.ai

이유가 읽기 귀찮다면 아래의 코드를 실행하면 된다

import os    
os.environ['KMP_DUPLICATE_LIB_OK']='True'

 

2. 결과

기가 막히게도 나는 prediction 이 틀린 값을 찾고야 말았다

 

정말 컴퓨터를 공부하면서 느끼지만, 얘는 싫증은 안내는 착한 앤데 똑똑한 애는 아니야

 

그래서 맞는 걸 찾아보고자 했다 지금의 random num에 +1을 해줬다

 

아이구머니! 대단한 결과다 내가 봐도 좀 어려운 6이였다 그런데도 해냈다 

 

 

 

4. 부록

 

이건 별 내용이 아니여서 개요에도 안 나와있는 부록이다

우리는 원 핫 인코딩에 대해서 배우고 그다음에 softmax 함수에 대해서 배웠다

그래서 다들 당연히 softmax 함수의 output은 원 핫 인코딩이 된 형식일 줄 알았을 것이다

그런데 label을 출력해보니 얼라리요? 그냥 값이 틱 나오지 않는가! 이상하다고 생각할 만한데... 다들 아닌가?

 

그래서 볼만한 자료를 첨부한다

 

stackoverflow.com/questions/62456558/is-one-hot-encoding-required-for-using-pytorchs-cross-entropy-loss-function

 

Is One-Hot Encoding required for using PyTorch's Cross Entropy Loss Function?

For example, if I want to solve the MNIST classification problem, we have 10 output classes. With PyTorch, I would like to use the torch.nn.CrossEntropyLoss function. Do I have to format the target...

stackoverflow.com

 

내용만 추출하면

 

nn.CrossEntropyLoss expects integer labels. What it does internally is that it doesn't end up one-hot encoding the class label at all, but uses the label to index into the output probability vector to calculate the loss should you decide to use this class as the final label. This small but important detail makes computing the loss easier and is the equivalent operation to performing one-hot encoding, measuring the output loss per output neuron as every value in the output layer would be zero with the exception of the neuron indexed at the target class. Therefore, there's no need to one-hot encode your data if you have the labels already provided.

 

그렇지만 원 핫 인코딩을 쓰고 싶다면? 그때는 새로 만들면 되지!~

 

이런 데이터 셋이 있다고 하자

y 값에는 레이블 값이 들어가 있다! 

 

x_train = [[1, 2, 1, 1],
           [2, 1, 3, 2],
           [3, 1, 3, 4],
           [4, 1, 5, 5],
           [1, 7, 5, 5],
           [1, 2, 5, 6],
           [1, 6, 6, 6],
           [1, 7, 7, 7]]
y_train = [2, 2, 2, 1, 1, 1, 0, 0]
x_train = torch.FloatTensor(x_train)
y_train = torch.LongTensor(y_train)

 

그럼 아래와 같이 고칠 수 있다

scatter_ 옆의 _는 pd의 inplace=True 옵션과 같다 즉, 덮어쓰기 기능이다

1번째 인자 1은 dim=1에 대해서 scatter 하겠다는 것이고

2번째 인자 y_train.unsqueeze(1)은 scatter하는 위치를 뜻하고

3번째 인자 1 은 1을 찍어주겠다는 뜻이다

 

즉 0로 가득찬 (8, 3) 차원의 y_one_hot에

dim=1의 차원에서 2번째 인자가 지정하는 위치에 1을 찍겠다는 뜻이다 

y_one_hot=torch.zeros(8,3)
y_one_hot.scatter_(1,y_train.unsqueeze(1),1)

이해가 안 된다면 하나씩 프린트해서 해보자 얼마 걸리지도 않는다!

 

 

그런데 생각해보니 내가 까먹을 것 같아서 쓴다..

 

y_one_hot.scatter_(1,y_train.unsqueeze(1),1)

y_one_hot.scatter_(0,y_train.unsqueeze(1),1)

 

 

댓글