Logistic Regression

개요

이 글은 Andrew Ng. 교수님 강의 중 "Logistic Regression" 부분을 제 나름의 이해방식으로 정리해 기록하는 글입니다.

 

Logistic Regression

로지스틱 회귀는 선형 회귀의 결과를 시그모이드 함수(Sigmoid function) 에 적용하여 확률 값을 출력하는 모델입니다.

이 함수는 입력값이 뭐든 항상 0과 1 사이의 값을 출력하므로, 이진 분류(Classification) 문제에서 확률 값으로 해석할 수 있습니다. 

Source : Andrew Ng. Cousera

따라서 분류문제에서 시그모이드함수의 결과값이 0.5보다 크거나 같으면 1(참), 아니면 0(거짓)으로 생각할 수 있습니다.

 

여러분이 공무원 시험준비를 한다고 가정해 볼까요. 🤔

공부시간(x0), 시도횟수(x1) 등의 따른 합격여부 데이터(Y)가 있다면, 대략 3시간 이상, 3회 이상 공부하니 합격, 아니면 불합격하더라 라는 데이터가 있습니다.

Source : Andrew Ng. Cousera

붉은색 X는 합격, 파란색 O는 불합격이라면, 

어렵지 않게 경계선, 결정경계를 발견할 수 있습니다.

(3회 이상 응시하고 3시간 이상 공부하면 합격)

Source : Andrew Ng. Cousera
 

그런데 이 선은 선형회귀에서와 마찬가지로 인간이 그려보는것이 아닙니다.

데이터만 주어지면 모델을 학습시켜 x0시간 공부하고 x1회 응시한 친구는 합격인지 불합격인지를 분류하여 예측하고 싶은 것이 목적입니다.

로지스틱회귀는 이러한 목적에 부합하며, 이는 데이터를 가장 잘 분류하는 결정경계(Decision boundary)를 학습하는 과정이라고 볼 수 있습니다.

 

Model

로지스틱 회귀의 모델은 아래와 같이 정의된다.

다양한 입력특징에 대응하기 위해 벡터, 행렬로 처리.

fw,b(x)=WX+b

 

시그모이드함수는 아래와 같다.

g(z)=11+ez

 

로지스틱 회귀는 선형 회귀의 결과를 시그모이드함수에 적용하므로

fw,b(x)=g(WX+b)=11+e(WX+b)

 

 

Logistic Loss Function

로지스틱 회귀는 선형회귀처럼 MSE(평균 제곱 오차)를 사용하면 Non Convex 함수가 되어 학습이 어려움.

(미분해서 가장 낮은 지점을 찾아야 하는데 Global 이 아닌 Local Minimum에 도달)

Source : Andrew Ng. Cousera

그래서 로그 손실 함수(Log Loss) 를 사용.

이런걸 어떻게 아냐면 First Mover인 Andrew Ng. 교수님 같은 선구자 덕분이다. 🙂‍↕️

 

로지스틱 손실 함수는 다음과 같이 정의된다.

L(fw,b(x(i)),y(i))={log(fw,b(x(i)))if y(i)=1log(1fw,b(x(i)))if y(i)=0


만약 y(i)=1 이라면 log(f) 이므로 위 그림의 좌측 로그함수.

  •  1에 가까우면 Loss
  •  0에 가까우면 Loss 
  • 손실이 1일때 줄어들고 0이면 무한대로 커지니 손실함수로 적합.

     

    만약 y(i)=0 이라면 log(1f) 이므로 위 그림의 우측 로그함수.

    • 1에 가까우면 Loss 
    • 0에 가까우면 Loss 
    • 손실이 0일때 줄어들고 1이면 무한대로 커지니 손실함수로 적합.

     

    Cost Function

    로지스틱회귀의 비용함수 J(W,b)는 모든 샘플의 손실(L) 평균.

    J(W,b)=1mi=1mL(fW,b(x(i)),y(i))

     

    Simplified Loss Function

    위에서 언급한 두가지 Loss 함수를 하나로 합치면 아래와 같다.

    L=y(i)log(fW,b(x(i)))(1y(i))log(1fW,b(x(i)))

     

    위 손실함수 앞부분 y(i)log(fW,b(x(i))) 은, 

    y(i)=1 인경우 사라지고,

    위 손실함수 뒷부분 (1y(i))log(1fW,b(x(i))) 은, 

    y(i)=0 인경우 사라진다.

     

    따라서 위의 둘을 합친 Simplified Loss Function 형태가 가능하다.

     

    Simplified Cost Function

    이제 비용함수(J)는 위의 단순화된 손실함수 L 로 대체하면, 최종적으로 아래와 같다.

    J=1mi=1m[y(i)log(fW,b(x(i)))+(1y(i))log(1fW,b(x(i)))]

    위 비용 함수는 최대 가능도, 또는 최대 우도 추청 (Maximum Likelihood Estimation, MLE) 방법에서 유도된다고 한다. 즉, 모델이 실제 데이터를 가장 잘 설명하는 파라미터 W,b를 찾는 과정이다. 

    통계수학을 깊이 공부한 적은 없어 사실 이부분은 잘 모르겠다.😓

     

    이제 정규화 (Regularization) 를 제외하고 대부분의 수식 정리가 완료되었다.

    아래는 위 비용함수 유도과정을 직접 필기한 그림이다.


    이제 이론적인 내용은 마치고 파이썬 코드로 구현할 차례이다. 


    1. Dataset

    아래와 같은 데이터를 가상으로 생성. (chatGPT 활용)

    [나이, 흡연기간, 흡연량에 따른 폐암여부 자료]
     

    • Input Features : 흡연자 나이(x0), 흡연기간(x1), 흡연량(x2)

    • Output : 폐암여부(y) 

     

    총 200명분이므로 200(행) X 4(열)로 구성 : lung_cancer_data.csv

    예를 들면,

    1번 사람은 78세, 20년흡연, 하루 1.68갑 흡연이지만 정상 😧

    2번 사람은 68세, 50년흡연, 하루 2.96갑 흡연이라서 폐암 😰

     

    먼저, csv 파일을 불러들이자.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import numpy as np
    import matplotlib.pyplot as plt
    import seaborn as sns
    import pandas as pd
    import copy
     
    # load the dataset
    path = 'lung_cancer_data.csv'
    df = pd.read_csv(path)
     
    np_array = df.to_numpy()
    print(np_array.shape)
     
    fig, axes = plt.subplots(1, 2, figsize=(10, 4))
    fig.suptitle('Scatter plot between specific features', fontsize=16)
     
    sns.scatterplot(x='Age', y='SmokingDuration', hue='LungCancer', data=df, ax=axes[0], alpha=0.7)
    sns.scatterplot(x='SmokingDuration', y='SmokingAmount', hue='LungCancer', data=df, ax=axes[1], alpha=0.7)
     
    plt.show()

     

    어떤 데이터인지 시각적으로 확인해보자.

    [좌 : 나이와 흡연기간, 우: 흡연기간과 흡연량]
     

    시각적으로 나이, 흡연기간, 흡연량의 상관관계를 알 수 있다. 

    나이가 많거나, 흡연기간이 길거나, 흡연량이 많을 수록 폐암진단 확률이 높다.

     

    2. Cost Function

    위에서 설명한 수식을 토대로 작성된 Sigmoid, Cost Function.

    입력 특징들(xi) 과 가중치의 내적을 시그모이드 함수로 전달.

    이후 앞서 설명한 Simplified Cost Function 으로 비용 계산. 

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    def sigmoid(z):
        g = 1 / (1 + np.exp(-z))
        return g
     
    def compute_cost_logistic(X, y, w, b):
        m = X.shape[0]
        cost = 0
     
        for i in range(m):
            z_i = np.dot(X[i], w) + b
            f_wb_i = sigmoid(z_i)
     
            cost += -y[i] * np.log(f_wb_i) - (1- y[i]) * np.log(1-f_wb_i)
        cost /= m
        return cost


     

    3. Gradient Descent

    위에서 설명한 수식을 토대로 작성된 경사하강법 구현.

    미분하며 w, b를 update하는 방식은 선형회귀와 동일하다.

    다만 로지스틱회귀의 손실함수는 평균제곱오차가 아닌 Log 손실함수.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    def compute_gradient_logistic(X, y, w, b):
        m,n = X.shape
        dj_dw = np.zeros((n, ))
        dj_db = 0
     
        for i in range(m):
            z_i = np.dot(X[i], w) + b
            f_wb_i = sigmoid(z_i)
            err_i = f_wb_i - y[i]
     
            for j in range(n):
                dj_dw[j] += err_i * X[i][j]
            dj_db += err_i
     
        dj_dw /= m
        dj_db /= m
        return dj_dw, dj_db
     
    def gradient_descent(X, y, w_in, b_in, alpha, num_iters):
        J_history = []
        w = copy.deepcopy(w_in)
        b = b_in
     
        for i in range(num_iters):
            dj_dw, dj_db = compute_gradient_logistic(X, y, w, b)
            w = w - alpha * dj_dw
            b = b - alpha * dj_db
     
            cost = compute_cost_logistic(X, y, w, b)
            J_history.append(cost)
     
            if i%100 == 0:
                print(f'Iteration {i}: w = {w}, b = {b:.3f}, cost = {J_history[-1]:.3f}')
     
        return w, b, J_history
        

     

    4. Machine Learning

    이제 초기값 설정 후 학습시작.

    가중치 초기값은 Input Feature의 수(3개) 만큼 0으로 초기화. 

     

    입력값(X) np_array[ : , :-1] 에서 쉼표 앞 : 은 모든 행(0~199)을, 

    :-1은 마지막 열을 제외한 (0~2) 까지를 의미.

    따라서 X는 200x3 2D 행렬 이다.

     

    y로 사용되는 np_array[ : , -1] 에서 쉼표 앞 : 은 모든 행(0~199), 

    -1은 마지막 값(열 3)인 폐암여부를 의미.

    따라서 y는 200개의 1D 벡터이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # init parameters
    w = np.zeros_like(np_array[0, :-1])
    b = 0
    alpha = 0.001
    num_iters = 1000
     
    # machine learning
    w_final, b_final, J_history = gradient_descent(np_array[:, :-1], np_array[:, -1], w, b, alpha, num_iters)
    print(f'Final w: {w_final}, Final b: {b_final}')
     
    # cost function convergence plot
    plt.plot(J_history)
    plt.xlabel("Iteration")
    plt.ylabel("Cost")
    plt.title("Cost Function Convergence")
    plt.show()

     

    경사하강법을 진행하며 w, b가 업데이트되고 비용이 감소하는 것을 확인.

    아래 그림의 마지막 Final 가중치(wi) 세가지 w1(나이), w2(흡연기간), w3(흡연량) 중 가장 큰 값인 w2, 즉 흡연기간이 폐암 여부에 미치는 영향이 큰것을 확인. (약 0.075)

    (테스트를 위해 가상으로 만들어진 데이터 임을 유의)

    단, 음수 가중치인 나이(w1)는 필요없는 값이 아니라 출력확률을 낮추는 중요한 역할을 함.


    J_history 리스트에 저장해둔 Cost값이 작아지며 학습이 잘 이루어짐을 확인.



    5. Prediction

    최종적으로 얻어진 w_final, b_final을 이용해 예측.

    학습에 사용한 Training dataset과 예측용 Validation dataset을 구분하는 것이 좋지만,

    예시에서 편의상 Training dataset 일부를 예측에 사용.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # prediction
    def predict(w, b, x):
        z = np.dot(x, w) + b
        g = sigmoid(z)
        if g >= 0.5:
            return 1
        else:
            return 0
     
    accuracy = 0
    sample_cnt = 30
         
    for x in np_array[:sample_cnt, :]:
        x_test = x[:-1]
        y_test = int(x[-1])
        y_pred = predict(w_final, b_final, x_test)
        accuracy += (y_pred == y_test)
        print(f'Input {x_test} :\tPred:{y_pred} True:{y_test}')
     
    print(f'Accuracy: {accuracy/sample_cnt:.2%}')

     

    Pred는 학습 후 예측값이고 True는 실제 폐암 확진데이터.

    둘이 일치하면 모델이 정확하다는 것.

    30개를 비교해 보니 일부 불일치해서 약 83%의 정확도.


     

    Full Code 

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    import numpy as np
    import matplotlib.pyplot as plt
    import seaborn as sns
    import pandas as pd
    import copy
     
    # load the dataset
    path = 'lung_cancer_data.csv'
    df = pd.read_csv(path)
     
    np_array = df.to_numpy()
    print(np_array.shape)
     
    fig, axes = plt.subplots(1, 2, figsize=(10, 4))
    fig.suptitle('Scatter plot between specific features', fontsize=16)
     
    sns.scatterplot(x='Age', y='SmokingDuration', hue='LungCancer', data=df, ax=axes[0], alpha=0.7)
    sns.scatterplot(x='SmokingDuration', y='SmokingAmount', hue='LungCancer', data=df, ax=axes[1], alpha=0.7)
     
    plt.show()
     
     
    def sigmoid(z):
        g = 1 / (1 + np.exp(-z))
        return g
     
    def compute_cost_logistic(X, y, w, b):
        m = X.shape[0]
        cost = 0
     
        for i in range(m):
            z_i = np.dot(X[i], w) + b
            f_wb_i = sigmoid(z_i)
     
            cost += -y[i] * np.log(f_wb_i) - (1- y[i]) * np.log(1-f_wb_i)
        cost /= m
        return cost
     
    def compute_gradient_logistic(X, y, w, b):
        m,n = X.shape
        dj_dw = np.zeros((n, ))
        dj_db = 0
     
        for i in range(m):
            z_i = np.dot(X[i], w) + b
            f_wb_i = sigmoid(z_i)
            err_i = f_wb_i - y[i]
     
            for j in range(n):
                dj_dw[j] += err_i * X[i][j]
            dj_db += err_i
     
        dj_dw /= m
        dj_db /= m
        return dj_dw, dj_db
     
    def gradient_descent(X, y, w_in, b_in, alpha, num_iters):
        J_history = []
        w = copy.deepcopy(w_in)
        b = b_in
     
        for i in range(num_iters):
            dj_dw, dj_db = compute_gradient_logistic(X, y, w, b)
            w = w - alpha * dj_dw
            b = b - alpha * dj_db
     
            cost = compute_cost_logistic(X, y, w, b)
            J_history.append(cost)
     
            if i%100 == 0:
                print(f'Iteration {i}: w = {w}, b = {b:.3f}, cost = {J_history[-1]:.3f}')
     
        return w, b, J_history
     
    # init parameters
    w = np.zeros_like(np_array[0, :-1])
    b = 0
    alpha = 0.001
    num_iters = 1000
     
    # machine learning
    w_final, b_final, J_history = gradient_descent(np_array[:, :-1], np_array[:, -1], w, b, alpha, num_iters)
    print(f'Final w: {w_final}, Final b: {b_final}')
     
    # cost function convergence plot
    plt.plot(J_history)
    plt.xlabel("Iteration")
    plt.ylabel("Cost")
    plt.title("Cost Function Convergence")
    plt.show()
     
    # prediction
    def predict(w, b, x):
        z = np.dot(x, w) + b
        g = sigmoid(z)
        if g >= 0.5:
            return 1
        else:
            return 0
     
    accuracy = 0
    sample_cnt = 30
         
    for x in np_array[:sample_cnt, :]:
        x_test = x[:-1]
        y_test = int(x[-1])
        y_pred = predict(w_final, b_final, x_test)
        accuracy += (y_pred == y_test)
        print(f'Input {x_test} :\tPred:{y_pred} True:{y_test}')
     
    print(f'Accuracy: {accuracy/sample_cnt:.2%}')

     

    느낀점

    scikit-learn, tensorflow, pytorch 등 라이브러리 사용보다 순수 파이썬으로 구현해 보는 것이 이해, 공부에 도움이 된다는 Andrew Ng. 교수님 말씀에 전적으로 동의합니다.

    이상으로 모든 설명을 마칩니다.

     

    Reference

    https://www.coursera.org/specializations/machine-learning-introduction

     

     

    댓글

    이 블로그의 인기 게시물

    Qt Designer 설치하기

    PyQt5 기반 동영상 플레이어앱 만들기