AI/머신러닝

[Pytorch] 소프트맥스 회귀(Softmax Regression) 구현 & MNIST 분류 적용

daeunnniii 2023. 5. 14. 22:25
728x90
반응형

이전 게시글에서는 다중 클래스 분류, 소프트맥스 함수, 크로스엔트로피 함수에 대해 정리해보았다.

  • 이전 게시글 참고:

https://daeunnniii.tistory.com/195

 

[ML] 다중 클래스 분류(Multi-Class Classification) 정리

이전 로지스틱 회귀 게시물에서는 독립 변수 $ x $가 1개인 이진 분류(Binary Classification)를 다루었다. 이번에는 독립 변수 $ x $가 2개 이상인 다중 클래스 분류(Multi-class Classification)와 소프트맥스 회

daeunnniii.tistory.com

 

이번 게시글에서는 위에서 이론을 정리해보았던 소프트맥스 회귀(Softmax Regression)를 Pytorch로 구현해보는 과정을 정리할 것이다.

 

1. Softmax Regression 비용 함수 구현

import torch
import torch.nn.functional as F

1) softmax 함수

다음은 첫번째 차원(dim=0)에 대해 소프트맥스 함수를 적용한 결과이다. 3개의 원소 [1, 2, 3]의 값이 0과 1사이의 값을 가지는 벡터로 변환되었다. 그리고 이 원소들의 값의 합은 총 1이다.

z = torch.FloatTensor([1, 2, 3])
hypothesis = F.softmax(z, dim=0)
print(hypothesis)
print(hypothesis.sum())

>> Result:
tensor([0.0900, 0.2447, 0.6652])
tensor(1.)

다음은 임의의 3 x 5 행렬의 크기를 가진 텐서를 만들고, 두번째 차원(dim=1)에 대해 소프트맥스 함수를 적용한 결과이다.

z = torch.rand(3, 5, requires_grad=True)
hypothesis = F.softmax(z, dim=1)
print(hypothesis)
print(hypothesis[0].sum())

>> Result:
tensor([[0.2645, 0.1639, 0.1855, 0.2585, 0.1277],
        [0.2430, 0.1624, 0.2322, 0.1930, 0.1694],
        [0.2226, 0.1986, 0.2326, 0.1594, 0.1868]], grad_fn=<SoftmaxBackward>)
tensor(1., grad_fn=<SumBackward0>)

 

2) 원-핫 인코딩

각 샘플에 대한 임의의 레이블을 다음과 같이 만들었다.

y = torch.randint(5, (3,)).long()     # y = tensor([0, 2, 1])

원-핫 인코딩을 수행한다. 우선 모든 원소가 0인 3 x 5 텐서를 만든 뒤, y.unsqueeze(1)을 통해 shape이 (3, )에서 (3, 1)로 변환된다.

scatter_ 함수의 첫번째 인자는 dim으로 두번째 차원(dim=1)에 대해 수행하고 세번째 인자 1은 두번째 인자가 알려주는 위치에 숫자 1을 넣도록 한다. 최종적으로 아래와 같이 원-핫 인코딩의 결과를 얻을 수 있다.

y_unsqueeze = y.unsqueeze(1)
print(y_unsqueeze)
one_hot_enc = torch.zeros_like(hypothesis) 
one_hot_enc.scatter_(1, y_unsqueeze, 1)
print(one_hot_enc)
>> Result:
tensor([[0],
        [2],
        [1]])
tensor([[1., 0., 0., 0., 0.],
        [0., 0., 1., 0., 0.],
        [0., 1., 0., 0., 0.]])

 

scatter_(dim, index, src, reduce=None) → Tensor

  • dim (int): 인덱싱의 기준이 되는 축
  • index (LongTensor): "src" 구성요소들이 흩어질 기준이 되는 인덱스 텐서로 정수형 텐서로 구성되어야 한다. 
  • src (Tensor or float): 타겟 "tensor" 를 구성할 값들이 담겨있는 텐서이다. 하나의 실수로 선언되면 그 값만으로 채워진다.
  • reduce (str, optional): 기존의 값을 어떻게 업데이트할 것인지 정의한다. 'multiply', 'add' 두 가지 방법이 존재하며, 정의되지 않을 경우 기존의 값을 없애고 새로운 값으로 치환한다.

3) F.log_softmax() : F.softmax() + torch.log()

소프트맥스 함수의 결과에 로그를 씌우는 과정을 파이토치에서는 두 과정을 결합한 F.log_softmax()를 제공한다.

# 직접 구현한 경우
torch.log(F.softmax(z, dim=1))

# 위와 동일
F.log_softmax(z, dim=1)

 

4) F.cross_entropy() : F.log_softmax() + F.nll_loss()

Cross entropy 함수식은 다음과 같았다.

 

1번~4번은 모두 같은 결과를 낸다. 1번은 직접 구현한 식, 2번은 torch.log(F.softmax(z, dim=1))를 torch.log_softmax(z, dim=1)로 대체한 것이다.

3번F.nll_loss()를 사용하였다. F.nll_loss()를 사용할 때는 원-핫 벡터를 넣을 필요없이 바로 실제값을 인자로 사용한다. nll은 Negative Log Likelihood의 약자이며 nll.loss()는 F.log_softmax() 수행 후 남은 수식을 수행한다. 4번은 위 모든 과정을 F.cross_entropy(z, y)로 한번에 진행한 것이다.

# 1. 직접 구현
(y_one_hot * -torch.log(F.softmax(z, dim=1))).sum(dim=1).mean()

# 2.
(y_one_hot * - F.log_softmax(z, dim=1)).sum(dim=1).mean()

# 3.
F.nll_loss(F.log_softmax(z, dim=1), y)

# 4. cross_entropy() 사용
F.cross_entropy(z, y)

지금까지 F.cross_entropy() 함수는 softmax 함수와 원-핫 벡터를 곱한 과정이 모두 포함되어있음을 살펴보았다.

 

 

2. Pytorch로 소프트맥스 회귀(Softmax Regression) 구현하기

1) Softmax Regression 구현

import 진행, 훈련 데이터와 레이블을 선언한다.

원-핫 인코딩 진행 후 W와 b를 선언하고, 옵티마이저로는 경사하강법을 사용한다.

learning rate는 0.1로 설정했다. 위에서 살펴본 F.cross_entropy()를 비용함수로 적용했다.

 

2) nn.Module로 Softmax Regression 구현

선형회귀에서 사용했던 nn.Linear()를 사용하여 다음과 같이 구현할 수 있다.

# 모델 선언 및 초기화. 4개의 특성을 가지고 3개의 클래스로 분류. input_dim=4, output_dim=3.
model = nn.Linear(4, 3)
# optimizer 설정
optimizer = optim.SGD(model.parameters(), lr=0.1)

n_epochs = 1000
for epoch in range(n_epochs + 1):
    prediction = model(x_train)

    # Cost 계산
    cost = F.cross_entropy(prediction, y_train)

    # cost로 H(x) 개선
    optimizer.zero_grad()
    cost.backward()
    optimizer.step()

    # 100번마다 로그 출력
    if epoch % 100 == 0:
        print('Epoch {:4d}/{} Cost: {:.6f}'.format(
            epoch, n_epochs, cost.item()
        ))

 

3) Softmax Regression 클래스로 구현

nn.Module을 상속하여 Softmax

class SoftmaxRegression(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(4, 3) # Output이 3!

    def forward(self, x):
        return self.linear(x)
        
model = SoftmaxRegression()

# optimizer 설정
optimizer = optim.SGD(model.parameters(), lr=0.1)

n_epochs = 1000
for epoch in range(n_epochs + 1):
    # H(x) 계산
    prediction = model(x_train)
    # cost 계산
    cost = F.cross_entropy(prediction, y_train)
    # cost로 H(x) 개선
    optimizer.zero_grad()
    cost.backward()
    optimizer.step()

    # 20번마다 로그 출력
    if epoch % 100 == 0:
        print('Epoch {:4d}/{} Cost: {:.6f}'.format(
            epoch, n_epochs, cost.item()
        ))

결과는 다음과 같다.

Epoch    0/1000 Cost: 2.778284
Epoch  100/1000 Cost: 0.642843
Epoch  200/1000 Cost: 0.558534
Epoch  300/1000 Cost: 0.504019
Epoch  400/1000 Cost: 0.459650
Epoch  500/1000 Cost: 0.420177
Epoch  600/1000 Cost: 0.383222
Epoch  700/1000 Cost: 0.347260
Epoch  800/1000 Cost: 0.311140
Epoch  900/1000 Cost: 0.274520
Epoch 1000/1000 Cost: 0.244428

 

3. 소프트맥스 회귀로 MNIST 데이터 분류하기

MNIST 훈련 데이터와 테스트 데이터를 다운로드하여 가져오고, 이전 게시글에서 살펴보았던 데이터로더(DataLoader)를 사용하여 데이터 로드를 진행한다. 네번째 인자의 drop_last는 마지막 배치를 버릴 것인지 설정하는 부분이다. 즉, 데이터를 배치 크기로 나누었을 때 마지막이 배치 크기를 채우지 못하고 남을 경우 버리도록 설정하는 것이다. 이는 마지막 배치를 경사 하강법에 사용하여 상대적으로 과대 평가가 되는 현상을 방지한다.

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

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
n_epochs = 15
batch_size = 100

# MNIST dataset 가져오기
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)
                         
# 데이터 로드
data_loader = DataLoader(dataset=mnist_train,
                        batch_size=batch_size,  # 배치 크기 100
                        shuffle=True,
                        drop_last=True)

모델을 초기화하고, 비용 함수와 옵티마이저를 선언해준다. 앞서 살펴본 cross_entropy() 함수는 torch.nn.functional.cross_entropy()를 사용하였으나 아래와 같이 torch.nn.CrossEntropyLoss()를 사용해도 된다. 둘 다 동일하게 softmax 함수를 포함하고 있다.

# MNIST data image of shape 28 * 28 = 784
# input_dim=784, output_dim=10
linear = nn.Linear(784, 10).to(device)

# 비용 함수와 옵티마이저 정의
criterion = nn.CrossEntropyLoss().to(device)
optimizer = torch.optim.SGD(linear.parameters(), lr=0.1)

훈련 데이터로 학습을 진행한다.

for epoch in range(n_epochs): # n_epochs=15
    avg_cost = 0
    total_batch = len(data_loader)

    for X, Y in data_loader:
        # 배치 크기가 100이므로 아래의 연산에서 X는 (100, 784)의 텐서가 된다.
        X = X.view(-1, 28 * 28).to(device)
        # 레이블은 원-핫 인코딩이 진행되지 않은 0 ~ 9의 정수.
        Y = Y.to(device)

        optimizer.zero_grad()
        hypothesis = linear(X)
        cost = criterion(hypothesis, Y)
        cost.backward()
        optimizer.step()

        avg_cost += cost / total_batch

    print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.9f}'.format(avg_cost))

print('Learning finished')

결과는 다음과 같다.

MNIST 테스트 데이터를 사용하여 모델 테스트를 진행한다. 테스트 과정에서는 경사하강법을 수행하지 않으므로 torch.no_grad()를 해준다.

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()

 

다음은 결과 화면이다.

 

참고:

- 위키독스 Pytorch로 시작하는 딥러닝 입문

- 위키독스 딥 러닝을 이용한 자연어 처리 입문

728x90
반응형