본문 바로가기

ML, DL/머신러닝

로지스틱 회귀(Logistic Regression)

원글 출처 : https://wikidocs.net/22881

 

05) 로지스틱 회귀(Logistic Regression)

일상 속 풀고자하는 많은 문제 중에서는 두 개의 선택지 중에서 정답을 고르는 문제가 많습니다. 예를 들어 시험을 봤는데 이 시험 점수가 합격인지 불합격인지가 궁금할 수도 있 ...

wikidocs.net

 

1. 이진 분류(Binary Classification)

앞서 선형 회귀를 설명하며 공부 시간과 성적 간의 관계를 직선의 방정식으로 표현한다는 가설 하에, 주어진 데이터로부터 가중치 w와 편향 b를 찾아 데이터를 가장 잘 표현하는 직선을 찾았습니다. 그런데 이번에 배울 둘 중 하나의 선택지 중에서 정답을 고르는 이진 분류 문제는 직선으로 표현하는 것이 적절하지 않습니다.

학생들이 시험 성적에 따라서 합격, 불합격이 기재된 데이터가 있다고 가정해봅시다. 시험 성적이 x라면, 합불 결과는 y입니다. 이 데이터로부터 특정 점수를 얻었을 때의 합격, 불합격 여부를 판정하는 모델을 만들고자 합시다.

score(x)result(y)

45 불합격
50 불합격
55 불합격
60 합격
65 합격
70 합격

위 데이터에서 합격을 1, 불합격을 0이라고 하였을 때 그래프를 그려보면 아래와 같습니다.

이러한 점들을 표현하는 그래프는 알파벳의 S자 형태로 표현됩니다. 이러한 x와 y의 관계를 표현하기 위해서는 직선을 표현하는 함수가 아니라 S자 형태로 표현할 수 있는 함수가 필요합니다. 직선을 사용할 경우 보통 분류 작업이 제대로 동작하지 않습니다.

이번 예제의 경우 실제값. 즉, 레이블에 해당하는 y가 0 또는 1이라는 두 가지 값만을 가지므로, 이 문제를 풀기 위해서 예측값은 0과 1사이의 값을 가지도록 합니다. 0과 1사이의 값을 확률로 해석하면 문제를 풀기가 훨씬 용이해집니다. 최종 예측값이 0.5보다 작으면 0으로 예측했다고 판단하고, 0.5보다 크면 1로 예측했다고 판단합니다. 만약 y=wx+b의 직선을 사용할 경우, y값이 음의 무한대부터 양의 무한대와 같은 큰 수들도 가질 수 있는데 이는 직선이 분류 문제에 적합하지 않은 두번째 이유입니다.

출력이 0과 1사이의 값을 가지면서 S자 형태로 그려지는 함수로 시그모이드 함수(Sigmoid function)가 있습니다.

 

2. 시그모이드 함수(Sigmoid function)

시그모이드 함수는 종종 σ로 축약해서 표현하기도 합니다. 로지스틱 회귀를 풀기 위한 가설을 세워봅시다.

H(x)=11+e−(wx+b)=sigmoid(wx+b)=σ(wx+b)

여기서 e(e=2.718281...)는 자연 상수라 불리는 숫자입니다. 여기서 구해야할 것은 여전히 주어진 데이터에 가장 적합한 가중치 w(weight)와 편향 b(bias)입니다. 인공 지능 알고리즘이 하는 것은 결국 주어진 데이터에 적합한 가중치 w와 b를 구하는 것입니다.

시그모이드 함수를 그래프로 시각화해봅시다.

import numpy as np
import matplotlib.pyplot as plt

아래의 그래프는 w는 1, b는 0임을 가정한 그래프입니다.

def sigmoid(x):
    return 1/(1+np.exp(-x))

x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)

plt.plot(x, y, 'g')
plt.plot([0,0],[1.0,0.0], ':') # 가운데 점선 추가
plt.title('Sigmoid Function')
plt.show()

위의 그래프에서 시그모이드 함수는 출력값을 0과 1사이의 값으로 조정하여 반환합니다. 마치 S자의 모양을 연상시킵니다. x가 0일 때 출력값은 0.5의 값을 가집니다. x가 증가하면 1에 수렴합니다. 가중치 w와 편향 b이 출력값에 어떤 영향을 미치는지 시각화를 통해 알아보겠습니다. 우선 w의 값을 변화시키고 이에 따른 그래프를 확인해봅시다.

def sigmoid(x):
    return 1/(1+np.exp(-x))

x = np.arange(-5.0, 5.0, 0.1)
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()

위 그래프는 w의 값이 0.5일때 빨간색선, w의 값이 1일때는 초록색선, w의 값이 2일때 파란색선이 나오도록 하였습니다. w의 값에 따라 그래프의 경사도가 변합니다. 선형 회귀에서 직선을 표현할 때, 가중치 w는 직선의 기울기를 의미했지만 여기서는 그래프의 경사도를 결정합니다. w의 값이 커지면 경사가 커지고 w의 값이 작아지면 경사가 작아집니다.

b의 값에 따라서 그래프가 어떻게 변하는지 보겠습니다.

def sigmoid(x):
    return 1/(1+np.exp(-x))

x = np.arange(-5.0, 5.0, 0.1)
y1 = sigmoid(x+0.5)
y2 = sigmoid(x+1)
y3 = sigmoid(x+1.5)

plt.plot(x, y1, 'r', linestyle='--') # x + 0.5
plt.plot(x, y2, 'g') # x + 1
plt.plot(x, y3, 'b', linestyle='--') # x + 1.5
plt.plot([0,0],[1.0,0.0], ':') # 가운데 점선 추가
plt.title('Sigmoid Function')
plt.show()

위 그래프는 b값에 따라서 그래프가 이동하는 것을 보여줍니다. 시그모이드 함수는 입력값이 커지면 1에 수렴하고, 입력값이 작아지면 0에 수렴합니다. 0부터의 1까지의 값을 가지는데 출력값이 0.5 이상이면 1(True), 0.5이하면 0(False)로 만들면 이진 분류 문제를 풀기 위해서 사용할 수 있습니다.

 

3. 비용 함수(Cost function)

로지스틱 회귀 또한 경사 하강법을 사용하여 가중치 w를 찾아내지만, 비용 함수로는 평균 제곱 오차를 사용하지 않습니다. 평균 제곱 오차를 로지스틱 회귀의 비용 함수로 사용했을 때는 좋지 않은 로컬 미니멈에 빠질 가능성이 지나치게 높아 문제 해결이 어렵습니다.

로지스틱 회귀에서 평균 제곱 오차를 비용 함수로 사용하면, 경사 하강법을 사용하였을때 찾고자 하는 최소값이 아닌 잘못된 최소값에 빠질 가능성이 매우 높습니다. 이를 전체 함수에 걸쳐 최소값인 글로벌 미니멈(Global Minimum) 이 아닌 특정 구역에서의 최소값인 로컬 미니멈(Local Minimum) 에 도달했다고 합니다. 로컬 미니멈에 지나치게 쉽게 빠지는 비용 함수는 cost가 가능한한 최소가 되는 가중치 w를 찾는다는 목적에는 좋지 않은 선택입니니다. 그리고 로지스틱 회귀에서의 평균 제곱 오차는 바로 그 좋지 않은 선택에 해당합니다.

로지스틱 회귀라는 문제에서 가중치 w를 최소로 만드는 적절한 새로운 비용 함수를 찾아야 합니다. 가중치를 최소화하는 아래의 어떤 함수를 목적 함수라고 합시다. J는 목적 함수(objective function)를 의미합니다.

아직 완성된 식이 아닙니다. 위의 식에서 샘플 데이터의 개수가 n개이고, 어떤 함수 f가 실제값 yi와 예측값 H(xi)의 오차를 나타내는 함수라고 할 때, 여기서 새로운 함수 f를 어떻게 정의하느냐에 따라서 가중치를 최소화하는 적절한 목적 함수가 완성됩니다. 목적 함수는 전체 데이터에 대해서 어떤 함수 f의 값의 평균을 계산하고 있습니다. 적절한 가중치를 찾기 위해서 결과적으로 실제값과 예측값에 대한 오차를 줄여야 하므로 여기서 이 f는 비용 함수(cost function)라고 하겠습니다. 식을 다시 쓰면 아래와 같습니다.

시그모이드 함수는 0과 1사이의 y값을 반환합니다. 이는 실제값이 0일 때 y값이 1에 가까워지면 오차가 커지며 실제값이 1일 때 y값이 0에 가까워지면 오차가 커짐을 의미합니다. 그리고 이를 반영할 수 있는 함수는 로그 함수를 통해 표현 가능합니다.

y의 실제값이 1일 때 −logH(x) 그래프를 사용하고 y의 실제값이 0일 때 −log(1−H(x)) 그래프를 사용해야 합니다. 위의 두 식을 그래프 상으로 표현하면 아래와 같습니다.

실제값 y가 1일 때의 그래프를 파란색 선으로 표현하였으며, 실제값 y가 0일 때의 그래프를 빨간색 선으로 표현하였습니다. 위의 그래프를 간략히 설명하면, 실제값이 1일 때, 예측값인 H(x)의 값이 1이면 오차가 0이므로 당연히 cost는 0이 됩니다. 반면, 실제값이 1일 때, H(x)가 0으로 수렴하면 cost는 무한대로 발산합니다. 실제값이 0인 경우는 그 반대로 이해하면 됩니다. 이는 다음과 같이 하나의 식으로 표현할 수 있습니다.

자세히 보면 y와 (1−y)가 식 중간에 들어갔고, 두 개의 식을 -로 묶은 것 외에는 기존의 두 식이 들어가있는 것을 볼 수 있습니다. y가 0이면 ylogH(x)가 없어지고, y가 1이면 (1−y)log(1−H(x))가 없어지는데 이는 각각 y가 1일 때와 y가 0일 때의 앞서 본 식과 동일합니다.

결과적으로 로지스틱 회귀의 목적 함수는 아래와 같습니다.

이때 로지스틱 회귀에서 찾아낸 비용 함수를 크로스 엔트로피(Cross Entropy)함수라고 합니다. 결론적으로 로지스틱 회귀는 비용 함수로 크로스 엔트로피 함수를 사용하며, 가중치를 찾기 위해서 크로스 엔트로피 함수의 평균을 취한 함수를 사용합니다. 크로스 엔트로피 함수는 소프트맥스 회귀의 비용 함수이기도 하므로 뒤에서 재언급합니다.

 

 

[케라스로 구현하는 로지스틱 회귀]

독립 변수 데이터를 x, 숫자 10 이상인 경우에는 1, 미만인 경우에는 0을 부여한 레이블 데이터를 y라고 해봅시다.

이번 데이터는 앞서 배운 단순 선형 회귀때와 마찬가지로 1개의 실수 x로부터 1개의 실수인 y를 예측하는 맵핑 관계를 가지므로 Dense의 output_dim, input_dim 인자값으로 각각 1을 기재합니다. 시그모이드 함수를 사용할 것이므로 activation의 인자값으로는 sigmoid를 기재해줍니다.

옵티마이저로는 가장 기본적인 경사 하강법인 sgd를 사용하였습니다. 시그모이드 함수를 사용한 이진 분류 문제에 손실 함수로 크로스 엔트로피 함수를 사용할 경우 binary_crossentropy를 기재해주면 됩니다. 에포크는 200으로 합니다.

import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras import optimizers

x = np.array([-50, -40, -30, -20, -10, -5, 0, 5, 10, 20, 30, 40, 50])
y = np.array([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) # 숫자 10부터 1

model = Sequential()
model.add(Dense(1, input_dim=1, activation='sigmoid'))

sgd = optimizers.SGD(lr=0.01)
model.compile(optimizer=sgd, loss='binary_crossentropy', metrics=['binary_accuracy'])

model.fit(x, y, epochs=200)

총 200회에 걸쳐 전체 데이터에 대한 오차를 최소화하는 w와 b를 찾아내는 작업을 합니다. 저자의 경우 약 190회부터 정확도가 100%가 나오기 시작했습니다. 실제값과 오차를 최소화하도록 값이 변경된 w와 b의 값을 가진 모델을 이용하여 그래프를 그려보겠습니다.

plt.plot(x, model.predict(x), 'b', x,y, 'k.')

x의 값이 5와 10사이의 어떤 값일때 y값이 0.5가 넘기 시작하는 것처럼 보입니다. 정확도가 100%가 나왔었기 때문에 적어도 x의 값이 5일때는 y값이 0.5보다 작고, x의 값이 10일 때는 y값이 0.5를 넘을 것입니다. 이제 x의 값이 5보다 작은 값일 때와 x의 값이 10보다 클 때에 대해서 y값을 출력해봅시다.

print(model.predict([1, 2, 3, 4, 4.5]))
print(model.predict([11, 21, 31, 41, 500]))
[[0.21071826]
 [0.26909265]
 [0.33673897]
 [0.41180944]
 [0.45120454]]
[[0.86910886]
 [0.99398106]
 [0.99975663]
 [0.9999902 ]
 [1.        ]]

x의 값이 5보다 작을 때는 0.5보다 작은 값을, x의 값이 10보다 클 때는 0.5보다 큰 값을 출력하는 것을 볼 수 있습니다.

 

 

 

 

인공 신경망 다이어그램

다중 로지스틱 회귀를 인공 신경망의 형태로 표현하면 다음과 같습니다. 아직 인공 신경망을 배우지 않았음에도 이렇게 다이어그램으로 표현해보는 이유는 로지스틱 회귀를 일종의 인공 신경망 구조로 해석해도 무방함을 보여주기 위함입니다.