이전에 로지스틱 회귀를 통해서 2개의 선택지 중 1개를 고르는 이진 분류를 구현해봤습니다. 이번엔 소프트맥스 회귀를 통해 3개 이상의 선택지 중에서 1개를 고르는 다중 클래스 분류에 대해 알아보겠습니다.
다중 클래스 분류는 3개 이상의 선택지 중에서 1개를 고르는 문제입니다. 다중 클래스 분류의 문제는 유명한 붓꽃 데이터를 사용하겠습니다.
붓꽃 데이터는 꽃받침 길이, 꽃받침 넓이, 꽃잎 길이, 꽃잎 넓이라는 4개의 특성(feature)으로부터 setosa, versicolor, virginica 라는 3개의 붓꽃 품종 중 어떤 품종인지 예측하는 문제입니다.
소프트맥스 회귀로 바로 가기 전에 로지스틱 회귀를 복습해보겠습니다.
로지스틱 회귀에서 시그모이드 함수는 예측값을 0과 1사이의 값으로 만듭니다.

입력값이 들어왔을 때 y = sigmoid(Wx + b) 라는 수식을 이용해서 예측값을 계산하면 임계값(threshold)을 넘으면 1, 넘지 않으면 0으로 분류합니다. 만약 예측값이 0.75라면 그렇지 않을 확률이 0.25이므로 두 확률의 총 합은 1입니다.
소프트맥스 회귀는 확률의 총 합이 1이 되는 이 아이디어를 다중 클래스 분류 문제에 적용합니다. 소프트맥스 회귀는 각 클래스마다 소수 확률을 할당합니다. 이때 확률의 총 합은 1이 되어야 합니다.

소프트맥스 회귀는 선택지의 개수만큼 차원을 가지는 벡터를 만들고 해당 벡터가 벡터의 모든 원소의 합이 1이 되도록 원소들의 값을 변환시키는 어떤 함수를 지나게 만들어야 합니다. 위의 그림은 붓꽃 품종 분류하기 문제 등과 같이 선택지의 개수가 3개일 때, 3차원 벡터가 어떤 함수를 지나 원소의 총 합이 1이 되도록 원소들의 값이 변환되는 모습을 보여줍니다. 이 함수를 소프트맥스 함수라고 합니다.
소프트맥스 함수는 분류해야하는 클래스의 개수를 k라고할 때, k차원의 벡터를 입력받아 각 클래스에 대한 확률을 추정합니다.

위에서 언급한 붓꽃 데이터로 그림과 연결해보겠습니다. 붓꽃 데이터의 입력값은 4개지만 클래스의 종류는 3개입니다. 소프트맥스 함수의 입력으로 사용되는 벡터는 벡터의 차원이 분류하고자 하는 클래스 개수인 3개가 되어야 합니다. 즉, 소프트맥스 함수의 입력차원은 3이 되어야 한다는 것입니다.

데이터의 차원을 소프트맥스 함수의 입력 차원인 3차원으로 축소하기 위해서는 가중치 곱을 하면 됩니다. 위 그림에서는 화살표가 총 12개이며 전부 다른 가중치를 가지고 학습 과정에서 점차적으로 오차를 최소화하는 가중치로 값이 변경됩니다.
그 다음 소프트맥스 함수의 출력은 분류하고자하는 클래스의 개수만큼 차원을 가지고 각 원소는 0과 1사이의 값을 가집니다. 이 값들은 특정 클래스가 정답일 확률을 나타냅니다.
그렇다면 이 예측값과 비교할 수 있는 실제값의 표현 방법이 있어야 합니다. 소프트맥스 회귀에서는 실제값을 원-핫 벡터로 표현합니다.


예를 들어서 데이터의 실제값이 setosa라면 setosa의 원-핫 벡터는 [0,1,0] 입니다. 이 경우에서는 예측값과 실제값의 오차가 0이 되려면 소프트맥스 함수의 결과가 [0,1,0] 이어야 합니다. 예측값과 실제값의 오차를 계산하기 위해서 소프트맥스 회귀는 손실 함수로 크로스 엔트로피 함수를 사용합니다.

손실 함수에서 얻은 오차로 가중치를 업데이트하는 과정입니다.

가중치 뿐만 아니라 편향b까지 포함을 시켜서 본다면 위 그림과 같습니다.
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
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)
model = nn.Linear(4,3)
optimizer = optim.SGD(model.parameters(), lr=0.1)
for epoch in range(1001):
prediction = model(x_train)
loss = F.cross_entropy(prediction, y_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
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)
class softmax_model(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(4,3)
def forward(self, x):
return self.linear(x)
model = softmax_model()
optimizer = optim.SGD(model.parameters(), lr=0.1)
for epoch in range(1001):
prediction = model(x_train)
loss = F.cross_entropy(prediction, y_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()