이전 글에서는 0 또는 1, 개 혹은 고양이처럼 데이터를 두 가지로 분류하는 방법을 알아보았다.
이번 글에서는 두 가지가 아니라 더 많은 클래스로 분류하는 방법인 Multinomial Logistic Regression 을 알아보자.
Multinomial Logistic Regression
\( k \) 개의 서로 겹치지 않는 카테고리가 있다고 하고 각 카테고리 \( i \)의 확률을 \( P(i) \)라고 하자.
이제 \( k-1 \) 로지스틱 분류를 할 건데, 카테고리 \( 0 \)을 레퍼런스로 두고 레퍼런스 카테고리와 \( i \in [1, 2 \ldots k-1] \)의 카테고리를 비교해보자.
비교를 위해 linear projection, 그러니까 \( \beta\cdot x \)을 로지스틱 함수 \( \sigma(x) = 1/(1+e^{-x}) \)을 통과시키면

따라서 다음과 같은 결과가 나온다.

이제 각 카테고리의 확률은 다음과 같이 쓸 수 있고 모든 카테고리의 확률을 더한 값은 \( 1 \)이 되어야 하므로 아래와 같은 식이 나온다.

\( P(0) \)을 좌변에 두고 모든 항을 우변으로 넘기면 다음과 같이 쓸 수 있고 \( P(i) = P(0) e^{\beta_i \cdot x}\)이므로

이것을 \( P(0) \)에 대해 정리하면 \( P(0) = \frac{1}{1 + \sum_{i \neq 0} e^{\beta_i \cdot x}} \)이 나온다.
\( P(i) = P(0) e^{\beta_i \cdot x}\)이므로 결론은 아래와 같다.

이것을 multimonial logistic regression이라고 부른다.
Softmax Function
위에서 \(P(0) = \frac{1}{1 + \sum_{i \neq 0} e^{\beta_i \cdot x}} \) 이고 \( 0 \)이 아닌 \( i \)에 대해 \( P(i) = \frac{e^{\beta_i \cdot x}}{1 + \sum_{j \neq 0} e^{\beta_j \cdot x}} \) 임을 알았다.
이제 분모에서 \(1 \to e^{\beta_0\cdot x}\) 이렇게 쓰면 모든 \(i\)에 대해 다음과 같다.

이제 \(z_i\)을 도입한 다음 이것이 모델의 레이어의 아웃풋이라고 하면 모든 \(i\)에 대해 아래와 같다.

이 식을 softmax function이라고 부르고 시그모이드 함수를 \(k\) 카테고리로 일반화했다고 볼 수 있다.
Cross-Entropy Loss
마지막으로 개념 하나만 더 짚고 가자.
저번 로지스틱 회귀 포스팅에서는 말도 없이 Binary Cross-Entropy을 가져다 썼지만 이것이 무엇인지 알아보고 다중 로지스틱 회귀에서는 어떻게 써야하는지 살펴보자.
Binary Cross-Entropy Loss
먼저 이진 분류에서 주어진 데이터 \(x\)에 대해 \(y\)가 \(0\) 또는 \(1\)일 확률을 각각 \(p\), \(1-p\)로 두면 아래와 같이 쓸 수 있다.

위의 두 식을 식 하나로 간단히 쓸 수 있는데 아래와 같다.

양변에 로그를 취하면 다음과 같다.

이 것을 여러 데이터 포인트로 확장하면(단순히 곱셈을 하면 됨) 다음과 같이 쓸 수 있다.

이제 우리는 주어진 데이터에 대해 확률을 최대화 하려고 한다.
즉, \(y_i\)에서 \(y=1\)일 때, \( \log p \)을 최대화하고, \( y=0 \)일 때, \( \log (1-p) \)을 최대화 하려는 것이다.
왜냐하면 \( [0, 1] \)에서 \( \log \)가 증가하기 때문에 \( P \)을 최대화 하는 것과 \( \log P \)을 최대화 하는 것과 같기 때문이고, 이 말은 \( -\log P \)을 최소화 하는 것이라고도 할 수 있다.
이제 우리는 이진 분류에서 가장 좋은 모델을 찾고 싶고, binary cross entropy loss을 최소화하려고 한다.

이제 아래 플랏을 보자.
단일 이벤트에서 \( y=0 \)이나 \( y=1 \)이 주어졌을 때 \( BCE\ Loss \)을 보여준다.
import matplotlib.pyplot as plt
import numpy as np
eps=1e-4
x=np.linspace(eps,1-eps,100)
plt.plot(x,-np.log(x),label='y=1')
plt.plot(x,-np.log(1-x),label='y=0')
plt.legend(); plt.xlabel("p"); plt.ylabel("y log p + (1-y) log (1-p)")

그래프를 보면 모델의 출력이 참값에서 멀어질수록(예를 들어, 파란 그래프\( (y=1) \)에서 \( 0 \)으로 갈수록) 로스 값이 커지는 것을 볼 수 있다.
Cross-Entropy Loss
이제 둘이 아닌 더 많은 카테고리가 있을 때, 우리는 one-hot 인코딩을 사용해서 BCE Loss을 Cross-Entropy Loss로 간단히 확장할 수 있다.
소프트맥스 함수와 유사하게 일반화를 할 수 있다.
입력 \(x\)에 대해 그 카테고리가 \(c\)이면 로스는 \( \log p_c \)임을 알고 있다.
이때 \( p_c = p(c|x) \)이므로 모든 \( i \)에 대해 확장하면 다음과 같이 쓸 수 있다. (BCE Loss을 마치 \(i\)에 대해 쓴 다음 다 더하는 방식이다.)

데이터 미리보기 (Fisher's Iris)
이전에는 비교적 간단한 분류라서 직접 데이터를 만들어서 사용했지만 이제는 파이썬 패키지에 있는 데이터를 사용해보자.

오늘 다룰 데이터는 Fisher' Iris으로 1936년 피셔가 연구 조사한 데이터로 iris을 한국어로는 붓꽃이라고 한다.
오늘의 목표는 주어진 데이터로 세 종류의 붓꽃을 분류하는 것으로 각 종의 이름은 setosa, versicolor, virginica이다.
우리가 다룰 데이터는 위의 그림에서 나와있듯이 petal의 가로와 세로, sepal의 가로와 세로로 총 네 가지와 그 데이터가 어떤 종류인지(setosa, versicolor, virginica) 이다.

먼저 머신러닝을 모르는 사람이 데이터 열어본 다음엔 위와 같이 시각화를 해볼 수 있을 것이다.
위의 데이터로 setosa는 분류가 쉬워보이는데 versicolor, virginica는 미묘하게 어렵다.
(마치 초등학교 수학시간에 나무토막을 쌓고 위에서 본 모습, 앞에서 본 모습, 옆에서 본 모습으로 생긴 것을 유추하는 그런 느낌이다.)
이제 이 데이터를 multimonial logistic regression으로 분류해보자.
PyTorch로 Multinomial Logistic Regression 구현하기
위에서 이것저것 많이 살펴봤지만 코드는 의외로 간단하다.
바뀌는 것은 단지 모델의 아웃풋에서 숫자 하나 바꾸는 것과 loss 함수의 종류만 바꾸면 된다. (물론 좋은 성능을 위해 모델도 좀더 복잡해질 수는 있다.)
이제 코드를 통해 살펴보자.
먼저 scikit-learn 파이썬 패키지가 설치되어있지 않다면 터미널에 pip install scikit-learn을 쳐서 패키지를 설치하자.
설치가 되었다면 데이터를 로드해서 열어보자.
from sklearn import datasets
import matplotlib.pyplot as plt
# check the data format
iris = datasets.load_iris()
print("classes: ", iris.target_names)
print("features: ", iris.feature_names)
print(f"shape: data:{iris.data.shape}, target:{iris.target.shape}")
print(f"first sample: data{iris.data[0]}, target:{iris.target[0]}")
print(f"last sample: data{iris.data[-1]}, target:{iris.target[-1]}")
이제 모델을 만들자.
입력이 4개(petal의 가로, petal의 세로, sepal의 가로, sepal의 세로), 512개의 노드를 가진 한 개의 히든 레이어, 출력은 iris의 클래스의 개수인 3개로 만들어보자.
그리고 사용할 데이터도 예쁘게 만들자.
import torch
import torch.nn as nn
class FisherNet(nn.Module):
def __init__(self):
super(FisherNet, self).__init__()
self.fc1 = nn.Linear(4,512)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(512,3)
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
x = torch.tensor(iris.data).float()
y = torch.tensor(iris.target)
모델의 레이어 사이에 비선형 함수인 ReLU 활성화 함수 (active function)을 집어넣었다.
두 fully connected layer사이에 비선형 함수가 없다면 두 레이어는 사실 곱셈을 두 번 하는 것이기 때문에 한 레이어로 합칠 수 있다.
따라서 비선형 함수를 중간에 집어넣었는데 활성화 함수에 대한 이야기는 기회가 있다면 다른 글에서 다뤄보도록 하겠다.
이제 모델과 옵티마이저, 비용 함수를 정하자.
net = FisherNet()
opt = torch.optim.Adam(net.parameters(), lr=1e-2)
los = nn.CrossEntropyLoss()
옵티마이저는 Adam을 사용했는데, 요즘 유행하는 옵티마이저다.
기존에 사용했던 SGD와 같이 여러 옵티마이저에 대한 소개도 다음에 다뤄보도록 하겠다.
모든 재료의 준비가 끝났으니 이제 트레이닝 루프를 만들자.
두 번째 for문에서 다섯 줄만 훈련을 위한 코드이고 나머지는 결과를 저장하고 출력해주는 부가적인 코드이니 그냥 복붙을 해보자.
참고로 bs는 batch size인데 한 번 훈련할 때 사용하는 데이터의 개수를 말한다.
이것에 관한 이야기도 나중에 기회가 된다면 다뤄보도록 하겠다.
이번에는 데이터가 총 150개라 적당히 25개로 나누었다.
from collections import defaultdict
bs = 25
for e in range(1,11):
eloss = defaultdict(lambda: [])
for idx in range(0,150,bs):
opt.zero_grad()
p = net(x[idx:idx+bs])
loss = los(p, y[idx:idx+bs])
loss.backward()
opt.step()
# keep track of some summary statistics
eloss['loss'].append(loss.item())
eloss['n'].append(x[idx:idx+bs].size(0))
eloss['corr'].append(sum(p.max(axis=1)[1] == y[idx:idx+bs]))
print(f'epoch {e:3d} avg loss; {sum(eloss["loss"])/sum(eloss["n"]):.4f}')
print(f' acc.; {sum(eloss["corr"])/sum(eloss["n"]):.4f}')
print("done")
print(f"Final accuracy: {sum(net(x).max(axis=1)[1] == y) / 150.:.4f}")
이 트레이닝 루프의 출력은 아래와 같다.
epoch 1 avg loss; 0.4188
acc.; 0.2667
epoch 2 avg loss; 0.0254
acc.; 0.6667
epoch 3 avg loss; 0.1034
acc.; 0.1667
epoch 4 avg loss; 0.0407
acc.; 0.6667
epoch 5 avg loss; 0.0218
acc.; 0.6667
epoch 6 avg loss; 0.0208
acc.; 0.6667
epoch 7 avg loss; 0.0121
acc.; 0.8667
epoch 8 avg loss; 0.0133
acc.; 0.8200
epoch 9 avg loss; 0.0106
acc.; 0.9000
epoch 10 avg loss; 0.0078
acc.; 0.9467
done
Final accuracy: 0.9733
데이터를 10번만 학습했을 뿐인데 97%의 정확도를 보여준다.
150개 중 146개의 데이터를 올바르게 분류하고 있는 것이다.
(재미로 과적합을 무시한 채 에포크를 키우면 150개의 데이터 모두 정확하게 분류가 가능했다.)
지금까지 총 세 가지 즉, linear regression, logistic regression, multinomial logistic regression를 살펴보았다.
각각 주어진 데이터로 값을 예측하는 것, 둘 중 어느 카테고리인지 분류하는 것, 여러 카테고리로 분류 하는 것이다.
다음엔 결과에 대한 새로운 목표가 아니라 convolutional이라는 이미지 처리에 적합한 방법적인 측면(?)에 대해 다뤄보도록 하겠다.
'머신 러닝 > Machine Learning' 카테고리의 다른 글
| [Machine Learning] PyTorch로 ResNet 구현하기 (Residual Network) (1) | 2023.03.07 |
|---|---|
| [Machine Learning] PyTorch로 Regularization(정규화) 직접 해보기 (1) | 2023.03.06 |
| [Machine Learning] PyTorch로 CNN 구현하기 (합성곱 신경망, Convolutional Neural Network) (0) | 2023.03.04 |
| [Machine Learning] PyTorch로 로지스틱 회귀 구현하기 (Logistic Regression) (0) | 2023.03.02 |
| [Machine Learning] PyTorch로 선형 회귀 구현하기 (Linear Regression) (1) | 2023.02.28 |