선형 회귀는 선형적인 관계를 가지는 데이터에 대한 분석을 진행하기에 적합하지만, 데이터를 두 개의 클래스로 나누는 문제에는 매우 부적합하다. 클래스를 나누는 분류 문제는 또다시 다중 클래스 분류와 이진 클래스 분류로 나눌 수 있는데, 로지스틱 회귀는 그 결과값으로 0 또는 1의 값을 가지게 하여 이진 분류 문제에서 매우 자주 활용되는 알고리즘이다. 본 포스트에서는 로지스틱 회귀를 이용하여 NBA선수들의 실적을 토대로 드래프트 참가 여부를 예측하는 알고리즘을 python을 이용하여 구현한다.
로지스틱 회귀(Logistic Regression)는 이진 분류 문제를 풀기 위한 통계적 기법이다. 스팸 메일인지 아닌지, 암 환자인지 아닌지 등의 예/아니오로 답할 수 있는 문제를 로지스틱 회귀로 해결할 수 있다.
로지스틱 회귀는 선형 회귀와 비슷하게 입력 변수들의 가중합을 구하지만, 그 결과를 바로 출력하지 않고 로지스틱 함수라는 비선형 함수를 적용하여 0과 1 사이의 값으로 변환한다. 이렇게 하면 출력 값이 확률로 해석될 수 있다. 즉, 로지스틱 회귀는 입력 변수들이 주어졌을 때 어떤 범주에 속할 확률을 예측하는 모델이다. 로지스틱 함수는 다음과 같은 식으로 정의된다.
이 함수는 가 커질수록 1에 가까워지고, 가 작아질수록 0에 가까워진다. 그리고 가 0일 때는 가 0.5가 된다.
로지스틱 회귀에서는 입력 변수들의 가중합을 라고 하고, 이를 로지스틱 함수에 넣어서 출력된 값을 라고 한다. 즉,
여기서 는 가중치이고, 는 입력 변수이다.
로지스틱 회귀로 구현한 모델을 학습시키기 위해서는 적절한 가중치를 찾아야 한다. 이를 위해 cost function을 정의하고, 이 함수를 최소화하는 방법을 찾는다. 로지스틱 회귀에서는 로그 우도 함수(NLL Loss)라는 cost function을 사용한다. 로그 우도 함수는 다음과 같이 정의된다.
여기서 은 데이터의 개수이고, 는 실제 값, 는 예측 값이다. 이 함수는 실제 값이 1일 때 예측 값이 1에 가까울수록 크고, 실제 값이 0일 때 예측 값이 0에 가까울수록 크다. 반대로 실제 값과 예측 값이 다르면 함수값은 작아진다. 그래서 이 함수를 최대화하는 것이 목표가 된다. 하지만 최적화 문제에서는 보통 최소화하는 것이 편리하기 때문에, 로그 우도 함수에 을 곱하여 부호를 바꾼다. 즉,
이제 이 비용 함수를 최소화하는 가중치를 찾기 위해 경사 하강법을 사용한다. 이때 cost function의 gradient는 다음과 같이 계산된다.
여기서 는 번째 데이터의 번째 입력 변수이다. 이제 이 식을 이용하여 각 가중치에 대해 기울기를 구하고, 경사 하강법을 적용하여 가중치를 업데이트한다. 이 과정을 여러 번 반복하면 비용 함수의 값이 줄어들고, 최적의 가중치에 수렴하게 된다.
이렇게 로지스틱 회귀 모델을 학습시킨 후에는 새로운 입력 변수들에 대해 예측 값을 구할 수 있다. 예측 값이 0.5보다 크면 1로, 작으면 0으로 분류한다. 이렇게 로지스틱 회귀를 이용하여 이진 분류 문제를 풀 수 있다.
from math import exp
from math import log
filename = 'NBA_Rookie_draft.csv'
data = []
with open(filename, 'r') as file:
lines = file.readlines()
for line in lines[1:]:
values = line.strip().split(',')
values = [float(value) for value in values]
data.append(values)
W = [[0.5], [0.5], [0.5]]
B = [[0.5]]
learning_rate = 0.001
def logistic_forward(X, Y, W, B):
pred = []
loss = 0
for i in range(len(X)):
XWB = B[0][0]
for j in range(len(X[i])-1):
XWB += X[i][j] * W[j][0]
pred_i = sigmoid(XWB)
pred.append(round(pred_i))
loss -= Y[i][0] * log(pred_i) + (1 - Y[i][0]) * log(1 - pred_i)
return pred, loss
def sigmoid(x):
if x >= 0:
return 1 / (1 + exp(-x))
else:
return exp(x) / (1 + exp(x))
def loss_gradient(X, Y, W, B):
dloss_dW = [[0.0] for _ in range(len(W))]
dloss_dB = [[0.0]]
for i in range(len(X)):
XWB = B[0][0]
for j in range(len(X[i])-1):
XWB += X[i][j] * W[j][0]
pred_i = sigmoid(XWB)
for j in range(len(W)):
dloss_dW[j][0] += X[i][j] * (pred_i - Y[i][0])
dloss_dB[0][0] += pred_i - Y[i][0]
return dloss_dW, dloss_dB
pred, loss = logistic_forward(data, [row[-1:] for row in data], W, B)
epochs = 30000
for i in range(epochs + 1):
dL_dW, dL_dB = loss_gradient(data, [row[-1:] for row in data], W, B)
for j in range(len(W)):
W[j][0] += -1 * learning_rate * dL_dW[j][0]
B[0][0] += -1 * learning_rate * dL_dB[0][0]
if i%50 == 0:
print(f"epoch : {i} / {epochs}")
pred, loss = logistic_forward(data, [row[-1:] for row in data], W, B)
print("input\t\t\toutput\t\treal index")
for i in range(len(data)):
if i%45 == 0:
print()
print("input\t\t\toutput\t\treal index")
input_data = data[i][:3]
prediction = pred[i]
target = data[i][-1]
print(f"{input_data}\t{prediction}\t\t{target}")
print("Loss:", loss)
print("Weight:", W)
print("Bias:", B)
correct_count = sum([1 for i in range(len(data)) if pred[i] == data[i][-1]])
accuracy = correct_count / len(data)
print("Accuracy:", accuracy)
모델의 학습이 완료되면, 학습된 모델을 사용하여 입력값 과 예측값 , 실제 값 을 출력하고, 손실 , 가중치 , 편향 , 그리고 정확도를 계산하여 보여주도록 하였다. 그 결과는 다음과 같았다.
본 포스트에서는 학습에 사용한 데이터를 그대로 테스트에 사용하였지만, 이를 고려하여도 약 95%의 매우 높은 예측 정확도를 보이는 것을 확인할 수 있었다.
잘봤습니다. 좋은 글 감사합니다.