로지스틱 회귀(Logistic Regression)은 이름은 회귀이지만 사실 분류 문제에 쓰이는 알고리즘입니다. 그 중에서도 이진 분류(Binary Classification)에 사용됩니다.
이진 분류는 2개의 선택지 중에서 하나를 고르는 것입니다. 예를 들면 메일이 스팸인지 스팸이 아닌지를 판단하는 것입니다. 스팸이 아니면 0, 스팸이면 1이라고 했을때 그래프를 그려보면 아래와 같습니다.

이러한 점들은 표현하는 그래프는 S자 형태로 표현됩니다. 이러한 x와 y의 관계를 표현하기 위해서는 y = Wx + b와 같은 직선 함수가 아니라 S자 형태로 표현할 수 있는 함수가 필요합니다. 이런 문제에 직선 함수를 사용할 경우 분류 작업이 잘 동작하지 않습니다.
그래서 이번 로지스틱 회귀의 가설은 선형 회귀 떄의 y = Wx + b가 아니라 y = f(Wx + b)의 가설을 사용할 것입니다. 이때 f는 S자 모양의 그래프를 만들 수 있는 어떤 특정 함수 f입니다. S자 모양의 그래프를 그릴 수 있는 어떤 함수 f는 널리 알려진 시그모이드(Sigmoid) 함수입니다.
시그모이드 함수(Sigmoid Function)의 방정식은 다음과 같습니다.

선형 회귀에서 최적의 W와 b를 찾는 것이 목표였는데 로지스틱 회귀도 마찬가지입니다.
로지스틱 회귀에서는 W와 b가 함수에 어떤 영향을 주는지 보겠습니다.


W=0.5일 때 빨간색, W=1일 때 초록색, W=2일 때 파란색 선입니다.
W값에 따라 그래프의 경사도가 변하는 것을 볼 수 있습니다. 앞서 선형 회귀에서 가중치 W는 직선의 기울기를 의미했지만 로지스틱 회귀에서는 경사도입니다. W의 값이 커지면 경사가 커지고, W의 값이 작아지면 경사가 작아집니다.

b=0.5일 때 빨간색, b=1일 때 초록색, b=1.5일 때 파란색 선입니다.
b의 값에 따라 그래프가 좌우로 이동하는 것을 알 수 있습니다.
import numpy as np
import matploblib.pyplot as plt
def draw_sigmoid(x):
return 1/(1+np.exp(-x))
# 기존
x = np.arange(-5.0, 5.0, 0.1)
y = draw_sigmoid(x)
plt.plot(x,y,'g')
plt.plot([0.0], [1.0,0.0], ':')
plt.title('Sigmoid Function')
plt.show()
# W값의 변화
y1 = sigmoid(0.5*x)
y2 = sigmoid(x)
y3 = sigmoid(2*x)
plt.plot(x, y1, 'r', linestyle='--') # W의 값이 0.5일때
plt.plot(x, y2, 'g') # W의 값이 1일때
plt.plot(x, y3, 'b', linestyle='--') # W의 값이 2일때
plt.plot([0,0],[1.0,0.0], ':') # 가운데 점선 추가
plt.title('Sigmoid Function')
plt.show()
# b값의 변화
y4 = sigmoid(x+0.5)
y5 = sigmoid(x+1)
y6 = sigmoid(x+1.5)
plt.plot(x, y4, 'r', linestyle='--') # x + 0.5
plt.plot(x, y5, 'g') # x + 1
plt.plot(x, y6, 'b', linestyle='--') # x + 1.5
plt.plot([0,0],[1.0,0.0], ':') # 가운데 점선 추가
plt.title('Sigmoid Function')
plt.show()
시그모이드 함수는 입력값이 한없이 커지면 1에 수렴하고, 입력값이 한없이 작아지면 0에 수렴합니다. 시그모이드 함수의 출력값은 0과 1사이의 값을 가지는데 이 특성을 이용하여 분류 작업에 사용할 수 있습니다.
예를 들어 임계값(threshold)을 0.5라고 가정할 때, 출력값이 0.5 이상이면 1, 0.5 이하이면 0으로 판단하도록 할 수 있는 것입니다.
로지스틱 회귀의 가설이 y = sigmoid(Wx + b)인 것을 알게되었습니다. 그렇다면 이제 최적의 W와 b를 찾을 수 있는 손실 함수를 정의해야 합니다.
선형 회귀에서 사용했던 손실 함수는 평균 제곱 오차(MSE)입니다. 로지스틱 회귀에서도 사용할 수 있을까요?

평균 제곱 오차의 함수 수식에서 H(x)는 이제 sigmoid(Wx + b)입니다. 이 손실 함수를 미분하면 선형 회귀때와는 달리 비볼록(non-convex) 형태의 그래프가 생성됩니다.

위와 같은 그래프에 경사 하강법을 사용할 경우의 문제점은 경사 하강법이 오차가 최소값이 되는 구간에 도착했다고 판단한 그 구간이 실제 오차가 완전히 최소값이 되는 구간이 아닐 수가 있습니다. 즉, 로컬 미니멈(Local Minimum)에 도달하게 된 것입니다. 전체 최소값인 글로벌 미니멈(Global Minimum)에 도달해야 loss가 최소가 되는 가중치 W와 b를 찾게 되는 것입니다.
시그모이드 함수의 특징은 함수의 출력값이 0과 1사이의 값이라는 점입니다. 즉, 실제값이 1일 때 예측값이 0에 가까워지면 오차가 커져야 합니다. 그 반대도 마찬가지입니다. 이를 충족하는 함수는 바로 로그 함수입니다. 아래 그래프는 y = 0.5에 대칭하는 두 개의 로그 함수 그래프입니다.

실제값이 1일 떄의 그래프가 주황색, 실제값이 0일 때의 그래프를 초록색 선으로 표현했습니다. 실제값이 1이라고 했을 때, 예측값인 y의 값이 1이면 오차가 0이므로 당연히 loss는 0이 됩니다. 반면, y가 0으로 수렴하면 loss는 무한대로 발산합니다.
정리하면 위 손실 함수는 실제값과 예측값의 차이가 커지면 손실이 커지고, 실제값과 예측값의 차이가 작아지면 손실은 작아집니다. 이제 위의 손실 함수에 대해서 경사 하강법을 수행하며 최적의 가중치 W와 b를 찾아가면 됩니다.
로지스틱 회귀의 가설식은 y = sigmoid(Wx + b)입니다. 파이토치에서는 nn.Sigmoid()를 통해서 시그모이드 함수를 구현하므로 nn.Linear()의 결과를 nn.Sigmoid()를 거치게 하면 가설식이 완성됩니다.
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)
y_train = torch.FloatTensor(y_data)
"""
nn.Sequential()은 nn.Module 층을 차례로 쌓을 수 있도록 합니다.
즉, Wx + b와 같은 수식과 시그모이드 함수 등과 같은 여러 함수들을
연결해주는 역할을 합니다.
"""
model = nn.Sequential(
nn.Linear(2, 1), # input_dim = 2, output_dim = 1
nn.Sigmoid() # 출력은 시그모이드 함수를 거친다
)
optimizer = optim.SGD(model.parameters(), lr=1)
for epoch in range(1001):
hypothesis = model(x_train)
loss = F.binary_cross_entropy(hypothesis, 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
class BinaryClassifier(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(2,1)
self.simgoid = 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)
for epoch in range(1001):
hypothesis = model(x_train)
loss = F.binary_cross_entropy(hypothesis, y_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()