로지스틱 회귀(Logistic Regression)
독립 변수 $ x $가 1개인 이진 분류(Binary Classification)와 로지스틱 회귀에 대해 정리하고 Pytorch로 구현해본 뒤, 독립 변수 $ x $가 2개 이상인 다중 분류(Multiclass Classification)와 소프트맥스 회귀에 대해 정리하고 Pytorch로 구현해볼 것이다.
이번 게시글에서는 로지스틱 회귀를 다룬다.
1. 이진 분류(Binary Classification)
이진 분류는 시험이 합격인지 불합격인지 예측하는 문제, 스팸 메일인지 정상 메일인지 분류하는 문제와 같이 둘 중 하나를 결정하는 문제를 말한다.
이러한 이진 분류를 풀기 위한 대표적인 알고리즘으로 로지스틱 회귀(Logistic Regression)이 있다. 로지스틱 회귀는 알고리즘의 이름은 회귀이지만 실제로는 분류(Classification) 작업에 사용할 수 있다.
앞서 선형 회귀에서는 직선의 방정식 $ H(x) = Wx + b $ 을 가설로 사용했다. 이번 로지스틱 회귀(Logistic Regression)에서의 가설은 직선의 방정식이 아니라 시그모이드 함수라는 특정 함수 $ f $를 추가적으로 사용하여 $ H(x) = f(Wx + b) $ 의 가설을 사용한다.
1) 시그모이드 함수(Sigmoid function)
시그모이드 함수는 활성화 함수 중 하나로 아래와 같이 0에서 1사이의 값을 반환하는 함수이다.
2) 로지스틱 회귀의 비용 함수(Cost function)
로지스틱 회귀의 가설이 $ H(x) = sigmoid(Wx + b) $라는 것을 알았다. 그런데 선형회귀에서 사용한 평균 제곱 오차(Mean Square Error, MSE)를 로지스틱 회귀의 비용함수로 사용해도 될까?
로지스틱 회귀에서 평균 제곱 오차 $ Cost(W, b) $를 미분하면 선형 회귀 때와 달리 심한 비볼록(non-convex) 형태의 그래프가 나오게 된다. 아래와 같은 그래프에 경사 하강법을 사용할 경우 미분값이 0이 나와 오차가 최소가 되는 구간이라고 판단했지만 해당 구간은 local minimum으로 실제로 오차가 완전히 최소값이 되는 구간이 아닐 수 있다. 이를 전체 함수에 걸쳐 최소값이 글로벌 미니멈(Global Minimum)이 아닌 특정 구역에서의 최소값인 로컬 미니멈(Local Minimum)에 도달했다고 한다.
시그모이드 함수는 0과 1 사이의 값을 갖는다. 즉, 실제값이 1이지만 예측값이 0에 가깝거나 실제값이 0이지만 예측값이 1에 가까우면 오차가 커져야한다. 이러한 성질을 충족하는 함수가 바로 로그 함수이다.
따라서 아래와 같이 비용함수(Cost function)를 나타낼 수 있다.
실제값이 1인 경우, 예측값인 $ H(x) $ 값이 1이면 오차가 0, 예측값인 $ H(x) $ 값이 0으로 수렴하면 cost는 무한대로 발산한다.
실제값이 0인 경우, 예측값인 $ H(x) $ 값이 0이면 오차가 0, 예측값인 $ H(x) $ 값이 1으로 수렴하면 cost는 무한대로 발산한다.
즉, 로지스틱 회귀의 비용 함수는 y의 실제값이 1일 때 $ -logH(x) $ 그래프를 사용하고, y의 실제값이 0일 때 $ -log(1-H(x) $ 그래프를 사용한다. 이를 하나의 식으로 통합하면 다음과 같다.
결과적으로 모든 예측값과 실제값에 대한 오차의 평균을 구하는 식은 다음과 같다. 아래는 로지스틱 회귀의 목적함수이다. 이제 이 비용함수에 대해서 경사하강법을 수행하면서 최적의 가중치 W를 찾아간다.
3) Pytorch로 로지스틱 회귀 구현하기
이전 게시글과 마찬가지로, 우선 Pytorch에서 제공하는 함수를 최대한 사용하지 않고 직접 구현해본 뒤에 Pytorch의 nn.Module을 활용해 로지스틱 회귀 클래스를 작성해볼 것이다.
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
x_data = [[1, 2], [2, 3], [3, 1], [4, 3], [5, 3], [6, 2]]
y_data = [[0], [0], [0], [1], [1], [1]]
x_train = torch.FloatTensor(x_data) # x_train.shape : (6, 2)
y_train = torch.FloatTensor(y_data) # y_train.shape : (6, 1)
# 2x1 크기의 W 벡터와 편향 b를 0으로 초기화
W = torch.zeros((2, 1), requires_grad=True) # 크기는 2 x 1
b = torch.zeros(1, requires_grad=True)
print(W)
print(b)
# optimizer 설정
optimizer = optim.SGD([W, b], lr=1)
epochs = 1000
for e in range(epochs + 1):
hypothesis = 1 / (1 + torch.exp(-(x_train.matmul(W) + b)))
cost = -(y_train * torch.log(hypothesis) +
(1 - y_train) * torch.log(1 - hypothesis)).mean()
# cost로 H(x) 개선
optimizer.zero_grad()
cost.backward()
optimizer.step()
# 100번마다 로그 출력
if e % 100 == 0:
print('Epoch {:4d}/{} Cost: {:.6f}'.format(
e, epochs, cost.item()
))
print(W)
print(b)
결과는 다음과 같다. 0으로 초기화했던 W와 b가 훈련된 후 W와 b가 출력되었고 Cost는 1000번의 Epoch를 반복한 뒤 0.019로 줄어든 것을 확인할 수 있다.
tensor([[0.],
[0.]], requires_grad=True)
tensor([0.], requires_grad=True)
Epoch 0/1000 Cost: 0.693147
Epoch 100/1000 Cost: 0.134722
Epoch 200/1000 Cost: 0.080643
Epoch 300/1000 Cost: 0.057900
Epoch 400/1000 Cost: 0.045300
Epoch 500/1000 Cost: 0.037261
Epoch 600/1000 Cost: 0.031673
Epoch 700/1000 Cost: 0.027556
Epoch 800/1000 Cost: 0.024394
Epoch 900/1000 Cost: 0.021888
Epoch 1000/1000 Cost: 0.019852
tensor([[3.2530],
[1.5179]], requires_grad=True)
tensor([-14.4819], requires_grad=True)
현재 W와 b를 가지고 예측값을 출력해보면 다음과 같다. 이 값들은 현재 0과 1사이의 값을 가지고 있으므로 0.5를 넘으면 True, 넘지 않으면 False로 출력하도록 해볼 것이다.
print(hypothesis)
'''
result:
tensor([[2.7711e-04],
[3.1636e-02],
[3.9014e-02],
[9.5618e-01],
[9.9823e-01],
[9.9969e-01]], grad_fn=<MulBackward0>)
'''
모두 실제값과 동일하게 False, False, False, True, True, True로 예측된 것을 볼 수 있다.
prediction = hypothesis >= torch.FloatTensor([0.5])
print(prediction)
'''
result:
tensor([[False],
[False],
[False],
[ True],
[ True],
[ True]])
'''
이번에는 Pytorch의 nn.Module을 활용해 로지스틱 회귀 클래스를 작성한 코드이다.
nn.Sequential()은 nn.Module 층을 차례로 쌓을 수 있도록 한다. 즉, 여러 함수들을 연결해주는 역할을 한다.
그리고 로지스틱 회귀의 비용 함수의 경우 Pytorch에서 이미 구현해서 제공하고 있다. import torch.nn.functional as F로 임포트 후 F.binary_cross_entropy(예측값, 실제값)으로 사용하면 된다.
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
class BinaryClassifier(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(2, 1)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
return self.sigmoid(self.linear(x))
x_data = [[1, 2], [2, 3], [3, 1], [4, 3], [5, 3], [6, 2]]
y_data = [[0], [0], [0], [1], [1], [1]]
x_train = torch.FloatTensor(x_data)
y_train = torch.FloatTensor(y_data)
model = BinaryClassifier()
optimizer = optim.SGD(model.parameters(), lr=1) # optimizer 설정
epochs = 1000
for e in range(epochs + 1):
# H(x) 계산
hypothesis = model(x_train)
# cost 계산
cost = F.binary_cross_entropy(hypothesis, y_train)
# cost로 H(x) 개선
optimizer.zero_grad()
cost.backward()
optimizer.step()
# 100번마다 로그 출력
if e % 100 == 0:
prediction = hypothesis >= torch.FloatTensor([0.5]) # 예측값이 0.5를 넘으면 True로 간주
correct_prediction = prediction.float() == y_train # 실제값과 일치하는 경우만 True로 간주
accuracy = correct_prediction.sum().item() / len(correct_prediction) # 정확도를 계산
print('Epoch {:4d}/{} Cost: {:.6f} Accuracy {:2.2f}%'.format( # 각 에포크마다 정확도를 출력
e, epochs, cost.item(), accuracy * 100,
))
참고:
- 위키독스 Pytorch로 시작하는 딥러닝 입문
- 위키독스 딥 러닝을 이용한 자연어 처리 입문
'AI > 머신러닝' 카테고리의 다른 글
[Pytorch] 소프트맥스 회귀(Softmax Regression) 구현 & MNIST 분류 적용 (0) | 2023.05.14 |
---|---|
[ML] 다중 클래스 분류(Multi-Class Classification) 정리 (0) | 2023.05.12 |
[ML] 다중 선형회귀(Multivariable Linear Regression) 정리 & Pytorch 구현 (0) | 2023.04.09 |
[ML] 선형 회귀 (Linear Regression) 정리 & Pytorch 구현 (0) | 2023.04.09 |
Microsoft Azure Machine Learning Studio(classic) 사용법과 자동차 가격 예측 (1) | 2020.08.27 |