ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Python으로 logistic regression 학습 구현하기
    Programming & Machine Learning/Python X 머신러닝 2018. 5. 27. 20:04

    파이썬으로 basic한 머신러닝 이론들을 구현하는 Implementation 프로젝트중 일부로(github 링크), 본 포스팅에서는 간단한 python 코드를 이용하여 logistic regression을 공부해 보았다.






    1. 로지스틱 회귀의 이해와 Hypothesis



    로지스틱 회귀모델은 y값이 범주형 변수인 경우(binary)에도 다중선형회귀모델을 적용시키기 위해 고안된 것이다.


    만약 회귀식의 결과값이 0, 1로만 도출된다면, 회귀식의 경우 이에 근사하는 직선 혹은 모델을 만들기가 어려워진다. (아래 이미지 참고 - 출처 : ratsgo's blog)





    그래서 생각해낸 것은 타겟변수를 확률로 만들자는 것이다. 이 아이디어를 통해 (0, 1)을 ≤ Pr⁡(Y=1X≤ 1 로 나타내게 된다. 하지만 이 역시 문제가 발생한다. Model : P⁡(X)= β_0+βX 의 모델은 회귀선에 의해 음수값을 추정하거나, 경우에 따라 1을 초과하는 추정값이 발생할 수도 있다. (아래 이미지 참고)




    이러한 문제점 때문에, 이항분류문제를 회귀모델을 이용하여 해결하기 위해서는 확률값 P를 -∞부터 ∞까지 확장할 필요성이 있다. 그래서 Probability 대신, 승산(Odds)이라는 개념을 도입한다. 승산이란 임의의 사건 A가 발생하지 않을 확률 대비 일어날 확률의 비율을 뜻한다. 우리가 고등학교 수업 시간에 배웠던 확률과 유사하다.



    Odds=  (p(X))/(1-p(X))



    이를 통해 계산되는 Y의 결과값은 [0,]이다. 즉, 하나의 사건이 일어날 승산이 0부터 무한대까지 표현된다는 것이다. 그러면 양수는 됐고, 이제 여기에 음수의 무한대까지 표현할 방법이 필요하다. 0부터 무한대까지 나타나는 위의 결과값 odds에 로그를 씌우면, 우리가 원하던 -∞부터 ∞까지의 결과값이 표현된다. 이렇게 오즈에 로그를 취하는 것을 로짓 변환이라고 한다.





    이제, 로짓 변환을 이용하여 위의 식 Model을 다시 표현하면 ln⁡((p(X))/(1-p(X)))= β_0+βX 가 되고, 이를 다시 p(X)에 대해 정리(아래의 그림과 수식에서는 f(X)로 표현)하면 다음과 같다. (수식 유도과정은 생략. 수식 유도과정 : 참고)



    f(X) : Logistic Function




    이를 통해 얻어낸 Hypothesis는 결과적으로, 범위가 제한되지 않은 X 입력값들을 시그모이드(로지스틱)함수를 이용하여 Y값을 [0,1]로 압축한 것이다. 즉 Y값은 사건이 발생할 확률이 된 것이고, 여기에 0 ~ 1 사이의 값을 가지는 임계값을 설정하여 사건이 발생할 확률이 어느 이상이여야 결과를 1로 할 지 결정한 뒤, 이를 최적화하는 학습계수들을 학습하면 된다.





    2. 디시전 바운더리(Decision Boundary)


    이제 Hypothesis의 결과로 나오는 Y값이 어떤 임계값을 가져야 사건 1로 분류가 되는지를 결정해야 한다. 이는 필연적으로 X 값들이 어떤 경계를 넘어감에 따라 Y의 결과값들이 바뀌는 것에 영향을 받는다. 위의 식에서 결국 로지스틱 회귀분석은 W, b의 값들을 찾는 문제이고, 따라서 임계값을 결정하는 하이퍼플레인 구분자, 디시전 바운더리 함수는 아래의 수식이 된다. 말은 어렵지만 개념은 간단하다.


    z = β_0+βX





    3. 비용 함수 정의


    비용함수는 W, b의 값을 찾기 위한 함수이다. 비용함수는 일반적으로 경사 하강법(Gradient Descent)를 사용하기에 적합한 함수 모양으로 만들어야 한다. 일반적인 Regression의 비용함수는 비용함수를 (예측값-실제값의 제곱의 합)과 같이 2차함수의 형태로 만들어 GD를 적용한다. 하지만 Logistic Regression의 비용함수는 다르게 도출해낸다. 우선 자연상수 e 때문에, 예측값과 실제값의 차이의 제곱의 합과 같은 수식은 매끈한 2차함수의 모양을 띠지 않는다. 이를 상쇄하기 위해 log를 씌워주는 과정이 필요하다.


    즉, 원래의 Regression의 cost function이 [Cost =  Sum((예측된 y값 - 실제 y값) ^ 2) / n] 이었다면, 매끈한 비용함수를 위하여 이 식을 보정해 주어야 한다. 이때, 단순히 log를 씌워 보정해주는 것이 아닌, Y의 결과값 1과 0을 구분하여 수식을 만든다. 먼저, Y가 1인 경우에는 아래를 따른다.



    cost = (1/n) * sum (-hypothesis) = (1/n) * sum (-log(sigmoid(Wx+b))



    이를 해석해보면, Hypothesis의 예측값이 0에 가까워질수록 cost의 값이 커지는 것을 알 수 있다. 이는 cost의 최저값을 찾는 모델 학습에 부합하는 개념이다. 다음으로 Y가 0인 경우에는 cost = (1/n) * sum( -log(1-sigmoid(Wx+b)))의 수식을 따른다. 이 수식 역시 예측값이 부정확할수록 cost가 커지는 것을 알 수 있다. 수학적으로 이 두가지를 하나의 수식으로 합치면(이 영역은 수학자의 영역이니 이해하기 어렵다고 겁먹지 말고, 그냥 가져다 쓰자) 다음과 같은 cost function으로 나타낼 수 있다.



    cost = Sum(-y*log(sigmoid(Wx+b)) - (1-y)*log(1-(sigmoid(Wx+b)))) / n


    사실 이 개념은, (0,1)의 값으로 나타낼 수 있는 데이터의 사후분포를 모델이 예측하는 사후 분포와 가장 비슷하도록 조정하는 통계적 방법인 최대 우도법(maximum likelihood estimation)으로부터 도출된 수식이다. 최대우도법을 이용한 통계적 분포 추정 방식에 비용(Cost 혹은 Error)함수라는 본질적인 목적(마이너스 혹은 플러스로 error와 항등한 성질을 가지는 결과값)에 충실하기 위해 로그를 씌워놨을 뿐이다.





    4. Gradient Descent로 학습하기


    그래디언트 디센트를 이용한 학습과정은 일반적인 Regression과 동일하다. 학습의 각 단계(iteration)에서 다음의 계산을 업데이트 하면 된다. 위에서 구한 cost의 편미분값을 계산해야 하는데, 역시나 수식적인 증명은 생략하겠다.



    그리고 다음은 지금까지의 전체 과정을 Python 코드로 구현한 것이다.



    import numpy as np
    from sklearn import datasets
    
    
    '''
    
    - GD를 이용하여 Logistic regression 학습
    
    '''
    class LogisticRegression:
        def __init__(self, learning_rate=0.01, threshold=0.01, max_iterations=100000, fit_intercept=True, verbose=False):
            self._learning_rate = learning_rate  # 학습 계수
            self._max_iterations = max_iterations  # 반복 횟수
            self._threshold = threshold  # 학습 중단 계수
            self._fit_intercept = fit_intercept  # 절편 사용 여부를 결정
            self._verbose = verbose  # 중간 진행사항 출력 여부
    
        # theta(W) 계수들 return
        def get_coeff(self):
            return self._W
    
        # 절편 추가
        def add_intercept(self, x_data):
            intercept = np.ones((x_data.shape[0], 1))
            return np.concatenate((intercept, x_data), axis=1)
    
        # 시그모이드 함수(로지스틱 함수)
        def sigmoid(self, z):
            return 1 / (1 + np.exp(-z))
    
        def cost(self, h, y):
            return (-y * np.log(h) - (1 - y) * np.log(1 - h)).mean()
    
        def fit(self, x_data, y_data):
            num_examples, num_features = np.shape(x_data)
    
            if self._fit_intercept:
                x_data = self.add_intercept(x_data)
    
            # weights initialization
            self._W = np.zeros(x_data.shape[1])
    
            for i in range(self._max_iterations):
                z = np.dot(x_data, self._W)
                hypothesis = self.sigmoid(z)
    
                # 실제값과 예측값의 차이
                diff = hypothesis - y_data
    
                # cost 함수
                cost = self.cost(hypothesis, y_data)
    
                # cost 함수의 편미분 : transposed X * diff / n
                # 증명 : https://stats.stackexchange.com/questions/278771/how-is-the-cost-function-from-logistic-regression-derivated
                gradient = np.dot(x_data.transpose(), diff) / num_examples
    
                # gradient에 따라 theta 업데이트
                self._W -= self._learning_rate * gradient
    
                # 판정 임계값에 다다르면 학습 중단
                if cost < self._threshold:
                    return False
    
                # 100 iter 마다 cost 출력
                if (self._verbose == True and i % 100 == 0):
                    print('cost :', cost)
    
        def predict_prob(self, x_data):
            if self._fit_intercept:
                x_data = self.add_intercept(x_data)
    
            return self.sigmoid(np.dot(x_data, self._W))
    
        def predict(self, x_data):
            # 0,1 에 대한 판정 임계값은 0.5 -> round 함수로 반올림
            return self.predict_prob(x_data).round()



    코드의 실행 및 실제 학습진행 까지의 전체 코드는 아래 깃헙 링크를 참고.

    https://github.com/yoonkt200/ml-theory-python/tree/master/02-logistic-regression

    댓글

분노의 분석실 Y.LAB