본문 바로가기

AI/PyTorch

PyTorch #소프트맥스 회귀(Softmax Regression)

소프트맥스 회귀

- 3개 이상의 선택지로부터 1개를 선택하는 다중 클래스 분류

 


원-핫 인코딩(One-Hot Encoding)

1. 원-핫 인코딩이란?

- 선택해야하는 선택지의 개수만큼 차원을 가지면서, 각 선택지의 인덱스에 해당하는 원소에는 1, 나머지 원소는 0의 값을 가지도록 표현하는 방법

예)

강아지 = [1, 0, 0]

고양이 = [0, 1, 0]

냉장고 = [0, 0, 1]

 

- 원-핫 인코딩으로 표현된 벡터를 원-핫 벡터(one-hot vector)라고 함

 

2. 원-핫 벡터의 무작위성

- 대부분의 다중 클래스 분류 문제가 각 클래스 간의 관계가 균등하다는 점에서 원-핫 벡터는 적절한 표현방법

- 모든 클래스에 대해 원-핫 인코딩을 통해 얻은 원-핫 벡터들은 모든 쌍에 대해 유클리드 거리를 구해도 전부 유클리드 거리가 동일함

- 원-핫 벡터의 관계 무작위성은 때로 단어의 유사성을 구할 수 없다는 단점으로 언급

 


소프트맥스 회귀(Softmax Regression) 이해

1. 다중 클래스 분류(Multi-class Classification)

- 이진 분류가 두개의 답 중 하나를 고르는 문제였다면, 세 개 이상의 답 중 하나를 고르는 문제를 다중 클래스 분류하고 함

- 소프트맥스 회귀는 확률의 총 합이 1이 되는 로지스틱 회귀를 다중 클래스 분류 문제에 적용한 것

- 소프트맥스 회귀는 각 클래스(선택지) 마다 소수 확률을 할단, 총 확률의 합은 1이 됨

- 소프트맥스 회귀는 선택지의 개수만큼 차원을 가지는 벡터를 만들고, 해당 벡터가 벡터의 모든 원소의 합이 1이 되도록 값을 변환시키는 특정 함수를 지나게 만들어야 함. 이 함수를 소프트백스 함수라고 부름

 

2. 소프트맥스 함수(Softmax function)

- 소프트맥스 함수는 분류해야하는 정답지(클래스)의 총 개수를 k라고 할 때, k차원의 벡터를 입력받아 각 클래스에 대한 확율을 추정함

 

3. 비용 함수(Cost function)

- 소프트맥스 회귀에선 비용 함수로 크로스 엔트로피 함수를 사용

 


소프트맥스 회귀의 비용 함수 구현

 

1. 도구 임포트

 

# 도구 임포트
import torch
import torch.nn.functional as F

torch.manual_seed(1)

 

 

2. 파이토치로 소프트맥스 비용 함수 구현(로우-레벨)

 

# 3개의 원소를 가진 벡터 텐서를 정의
z = torch.FloatTensor([1, 2, 3])

 

# 원소들의 값의 합이 1인지 확인
hypothesis.sum()

 

# 비용함수 구현
# 임의의 3x5 행렬 크기를 가진 텐서 생성
z = torch.rand(3, 5, requires_grad=True)

 

# 생성한 텐서에 대해 소프트맥스 함수 적용, 각 샘플에 소프트 맥스 함수를 적용해야 함으로, 두번째 차원에 적용 (dim = 1)
hypothesis = F.softmax(z, dim=1)
print(hypothesis)

 

# 각 샘플에 대해 임의의 레이블 생성
y = torch.randint(5, (3,)).long()
print(y)

 

# 각 레이블에 대해 원-핫 인코딩 수행
# 모든 원소가 0의 값을 가진 3 × 5 텐서 생성
y_one_hot = torch.zeros_like(hypothesis) 
y_one_hot.scatter_(1, y.unsqueeze(1), 1)  # 언스퀴즈: 특정 위치에 1인 차원을 추가

 

# 소프트 맥스 회귀의 비용함수 구현(크로스 엔트로피 함수)
cost = (y_one_hot * -torch.log(hypothesis)).sum(dim=1).mean()
print(cost)

 

 

 

3. 파이토치로 소프트맥스 비용 함수 구현(하이-레벨)

 

# F.log_softmax() + F.nll_loss() = F.cross_entropy()

F.cross_entropy(z, y)

* F.cross_entropy() 에는 소프트맥스 함수도 포함되어 있음

 

 


소프트맥스 회귀 구현

 

로우 레벨

 

# 도구 임포트
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)
# 훈련 데이터 및 레이블 텐서 선언
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]  # 0,1,2 총 3개의 클래스(답안) 존재
x_train = torch.FloatTensor(x_train)
y_train = torch.LongTensor(y_train)
# x_train 과 y_train 크기 확인
print(x_train.shape)
print(y_train.shape)

 

x_train의 크기는 8 × 4이며, y_train의 크기는 8 × 1. 최종 사용할 레이블은 y_train에서 원-핫 인코딩을 한 결과여야함. 클래스 개수는 3개이므로 y_trian에 원-핫 인코딩한 결과는 8x3 이어야함.

 

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

 

 

y_train에서 원-핫 인코딩을 한 결과인 y_one_hot의 크기는 8 × 3. 즉, W 행렬의 크기는 4 × 3

W와 b 선언, 옵티마이저는 SGD(경사 하강법), lr(학습률) 은 0.1로 설정

# 모델 초기화
W = torch.zeros((4, 3), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
# optimizer 설정
optimizer = optim.SGD([W, b], lr=0.1)
# 훈련 실행
nb_epochs = 1000
for epoch in range(nb_epochs + 1):

    # 가설
    hypothesis = F.softmax(x_train.matmul(W) + b, dim=1) 

    # 비용 함수
    cost = (y_one_hot * -torch.log(hypothesis)).sum(dim=1).mean()

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

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

 

 

 

하이 레벨

- F.cross_entropy()를 사용해 비용함수 구현, 소프트맥스 함수를 포함하고 있음

 

# 도구 임포트
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)
# 훈련 데이터 및 레이블 텐서 선언
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]  # 0,1,2 총 3개의 클래스(답안) 존재
x_train = torch.FloatTensor(x_train)
y_train = torch.LongTensor(y_train)
# 모델 초기화
W = torch.zeros((4, 3), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
# optimizer 설정
optimizer = optim.SGD([W, b], lr=0.1)

nb_epochs = 1000
for epoch in range(nb_epochs + 1):

    # Cost 계산
    z = x_train.matmul(W) + b
    cost = F.cross_entropy(z, y_train)

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

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

 

 

 

nn.Module로 구현

- 소프트맥스 회귀에서 output_dim 은 클래스의 개수여야 함

 

# 도구 임포트
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)

# 훈련 데이터 및 레이블 텐서 선언
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]  # 0,1,2 총 3개의 클래스(답안) 존재
x_train = torch.FloatTensor(x_train)
y_train = torch.LongTensor(y_train)
# 모델을 선언 및 초기화. 4개의 특성을 가지고 3개의 클래스로 분류. input_dim=4, output_dim=3.
model = nn.Linear(4, 3)
# optimizer 설정
optimizer = optim.SGD(model.parameters(), lr=0.1)

nb_epochs = 1000
for epoch in range(nb_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, nb_epochs, cost.item()
        ))

 

 

 

클래스로 구현

 

# 도구 임포트
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)

# 훈련 데이터 및 레이블 텐서 선언
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]  # 0,1,2 총 3개의 클래스(답안) 존재
x_train = torch.FloatTensor(x_train)
y_train = torch.LongTensor(y_train)
class SoftmaxClassifierModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(4, 3) # Output이 3!

    def forward(self, x):
        return self.linear(x)
model = SoftmaxClassifierModel()
# optimizer 설정
optimizer = optim.SGD(model.parameters(), lr=0.1)

nb_epochs = 1000
for epoch in range(nb_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, nb_epochs, cost.item()
        ))

 

 

 


소프트맥스 회귀로 MNIST 데이터 분류

- MNIST: 숫자 0부터 9까지 이미지로 구성된 손글씨 데이터셋

- 28픽셀 x 28픽셀의 흑백 이미지

- 각 이미지를 28 x 28 = 784, 784개의 원소(특성)을 가진 벡터로 변환

 

MNIST 데이터셋
28 x 28 픽셀의 이미지

 

토치비전(torchvision)

- 유명한 데이터셋, 이미 구현되어져있는 유명한 모델들, 일반적인 이미지 전처리 도구들을 포함하고 있는 패키지

- 링크: https://pytorch.org/docs/stable/torchvision/index.html

- 자연어 처리를 위한 토치 텍스트(torchtext)라는 패키지 존재

 

 

 

분류기 구현

 

사전 설정

 

# 도구 임포트

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
# 연산도구 설정
USE_CUDA = torch.cuda.is_available() # GPU를 사용가능하면 True, 아니라면 False를 리턴
device = torch.device("cuda" if USE_CUDA else "cpu") # GPU 사용 가능하면 사용하고 아니면 CPU 사용
print("다음 기기로 학습합니다:", device)
# 랜덤 시드 고정
# for reproducibility
random.seed(777)
torch.manual_seed(777)
if device == 'cuda':
    torch.cuda.manual_seed_all(777)
# 하이퍼파라미터 변수 설정
# hyperparameters
training_epochs = 15
batch_size = 100
# MNIST 데이터 로드 torchvision.datasets.dsets.MNIST를 사용
# 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)

 

 

데이터 로드 및 모델 설계, 비용 함수, 옵티마이저 정의

 

# 데이터 로드
# dataset loader
# dataset은 로드할 대상, batci_size는 배치 크기, shuffle은 매 에포크마다 미니 배치 셔플 여부, drop_last는 마지막 배치 버릴지 여부
# drop_last를 하는 이유는 1,000 개의 데이터가 있다고 할 때, 배치크기가 128이면 에포크가 7개 나오고, 104개가 남는데 그러면 128개가 안되므로
# 마지막 배치를 버리는 것. 이는 다른 미니 배치보다 개수가 적은 마지막 배치가 상대적으로 과대 평가되는 현상을 막아줌.
data_loader = DataLoader(dataset=mnist_train,
                                          batch_size=batch_size, # 배치 크기는 100
                                          shuffle=True,
                                          drop_last=True)

 

# 모델 설계, input_dim 은 784(총 특성), output_dim 은 10(구분해야할 숫자)
# MNIST data image of shape 28 * 28 = 784
linear = nn.Linear(784, 10, bias=True).to(device)

 

# 비용 함수와 옵티마이저 정의
criterion = nn.CrossEntropyLoss().to(device) # 내부적으로 소프트맥스 함수를 포함하고 있음.
optimizer = torch.optim.SGD(linear.parameters(), lr=0.1)

 

 

훈련 실행

 

# 훈련 실행
for epoch in range(training_epochs): # 앞서 training_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')

 

 

테스트 데이터로 모델 테스트

 

# 테스트 데이터를 사용하여 모델을 테스트한다.
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()

 

 

 


참고:

https://wikidocs.net/book/2788

 

PyTorch로 시작하는 딥 러닝 입문

이 책은 딥 러닝 프레임워크 PyTorch를 사용하여 딥 러닝에 입문하는 것을 목표로 합니다. 이 책은 2019년에 작성된 책으로 비영리적 목적으로 작성되어 출판 ...

wikidocs.net

 

반응형