Post

[혼공머신] 3주차 04-1 로지스틱 회귀

[혼공머신] 3주차 04-1 로지스틱 회귀

본 게시글은 한빛미디어의 혼자 공부하는 머신러닝+딥러닝을 바탕으로 작성되었습니다.

혼공학습단

이번에 글을 작성하려고 했는데, 너무 귀찮았다. 아직 3번이 더 남았는데 벌써 귀찮으면 어떡하지라는 생각이 들었지만 어쩔 수 없었다. 그래서 미루고 미루다가 마지막날에 완성을 하게 될 것 같다. 하지만 그동안 다른 여러 가지를 시도해 보았다.

먼저, 수식을 작성할 때, $\overset{\text{def}}{=}$와 같이 등호 위에 문자로 표기를 하는 것을 자주 사용하는 편인데, 이것을 어떻게 작성해야 하는지를 몰라서 못 쓰고 있었다. 그런데 찾아보니 있어서 다시 다 바꿔야 할 것 같다. 그리고, 수식에 번호를 넣는 것도 이제 규칙을 찾아서 이전에 작성했던 글들도 다시 정리를 해야 할 것 같다. 마지막으로, MathJax 렌더링이 PC에서는 잘 되는 편이었으나, 모바일에서는 깨지는 경우가 있었다. 하지만 이것도 결국 해결해 버렸다. 물론 아직 수식 비율이 마음에 안 드는 곳이 있기는 하지만, 그건 나중에 해결하기로 하고 일단은 글부터 작성하기로 했다.

그러고 보니 이번 우수 혼공족에는 선정되지 못한 것 같다. 이번엔 와플을 줘서 받고 싶다는 생각을 하긴 했지만 어쩔 수 없다. 사실 받을 것이라고 많이 기대를 하긴 했는데, 동일한 사람에게만 계속 주게 되면 다른 사람들의 의욕이 떨어질 수도 있기에 나눠서 주는 것이라고 생각하기로 했다. 나라도 받는 사람만 받으면 하고 싶은 의욕이 떨어질 것 같긴 하다. 아무튼 이제 3주차를 시작해 보자.

확률 계산

지금까지 classification 중에서 우리는 KNN classification을 살펴보았다. 그런데 잘 생각해 보면 도미와 빙어, 둘 중 하나로 classification을 했었다. 둘 중 하나로 classify 하는 것은 쉬울 것 같은데 만약 class가 두 개보다 많으면 어떻게 될까? 그리고, 각 class에 배정될 확률을 구하려면 어떻게 해야 할까?

만약 3개의 class가 있고, $K=6$으로 가정했을 때 sample point가 어디에 속할지 그래프를 그려서 생각해 보자. 아래 그래프를 보면, 파란색 사각형이 3개, 붉은색 원 2개, 초록색 삼각형이 1개로, 일단 sample point가 파란색 사각형의 class에 classify 된다는 것을 알 수 있다. 그런데 확률은 어떻게 구할까? 각 class에 속하는 point들의 비율로 구할 수 있지 않을까?

image

아까 파란색 사각형이 3개, 붉은색 원 2개, 초록색 삼각형이 1개로, 전체 class의 포인트는 총 $n=6$이다. 이때 각각 비율은 $\frac{3}{6}=0.5$, $\frac{2}{6}\approx0.3$, $\frac{1}{6}\approx0.2$이다. 따라서 우리는 이것을 통해 sample point가 파란색 사각형 class에 속할 확률이 50%, 붉은색 원 class에 속할 확률이 30%, 초록색 삼각형 class에 속할 확률이 20%일 것이라고 생각할 수 있다.

이러한 방식으로 한빛미디어가 우리에게 준 물고기 럭키백의 확률을 구해보도록 하자. 어떤 곳인지는 모르겠지만 마케팅팀과 그것을 수락한 운영진의 생각이 비범하다고 생각되는 한 회사가 있다. 그 멋진 회사는 사은품으로 생선으로 주겠다는 원대한 계획을 세우고 있고, 그 사은품의 생선을 랜덤으로 고객에게 주겠다는 더 멋진 생각을 하고 있다. 부디 이 회사가 수산물 업체이기를 바라며, 이 회사의 고민을 들어보도록 하자. 이 회사는 사은품으로 생선을 주는데, 그래도 아무런 정보 없이 고객에게 주기는 좀 그러니 생선의 확률을 구해서 알려주고, 사은품을 선택하도록 하기로 했다. 그런데, 생선의 확률을 어떻게 구할지 고민인 것이다.

그래서 우리는 방금 배운 KNN 알고리즘을 통해 확률을 구해보기로 했다. 원래라면 데이터를 게시글에 썼겠지만, 인터넷에서 데이터를 가지고 올 수도 있다는 것을 보여주기 위해 이번 한 번만 인터넷 링크를 통해 데이터를 불러오기로 하자.

1
2
import pandas as pd
data = pd.read_csv('https://bit.ly/fish_csv_data')

아까 물고기 종류가 여러 가지가 있다고 했는데, 어떤 물고기가 있는지 확인해 보자. 총 7가지의 물고기가 있는 것을 확인하였다. 참고로 여기서 Roach는 바퀴벌레가 아닌 로치라는 물고기이다. 아무리 물고기를 랜덤으로 사은품으로 주겠다는 생각을 하는 회사라도, 사은품으로 바퀴벌레를 주지는 않으니 안심해도 된다.

1
pd.unique(data['Species'])

array([‘Bream’, ‘Roach’, ‘Whitefish’, ‘Parkki’, ‘Perch’, ‘Pike’, ‘Smelt’], dtype=object)

이제 늘 하던 대로 input data와 output data를 나눠줄 시간이다. 이 정도는 이제 따로 말하지 않아도 할 것이라고 믿는다. 그리고 당연히 train data와 test data로도 나눠준다.

1
2
3
4
5
6
from sklearn.model_selection import train_test_split

X = data[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
y = data['Species'].to_numpy()

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

그리고 이미 알고 있듯이, KNN 알고리즘은 distance, 즉 scale에 영향을 받기에, standardize를 해준다.

1
2
3
4
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.fit_transform(X_test)

그리고 KNN 알고리즘을 통해 학습시킨 후, 점수를 확인해 보자. 적당한 점수가 나왔다. Test score가 training score보다 지나치게 낮지도 않고, 높지도 않다.

1
2
3
4
from sklearn.neighbors import KNeighborsClassifier
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train_scaled, y_train)
print(knn.score(X_train_scaled, y_train), knn.score(X_test_scaled, y_test))

0.8907563025210085 0.875

그런데, 이때 궁금한 것이 하나 생긴다. 이전에 KNN classification에서 classify를 할 때, 0 또는 1로 output data가 만들어졌었다. 그럼, 이번과 같은 multiclass classification에서는 어떻게 output data가 나올까?

우리가 방금 만든 data에서 알 수 있듯이 KNN multiclass classification에선 매우 편리하게 output data로 각 class가 출력이 된다. Test data 중 처음 5개의 data에 대해서 어떻게 예측을 했는지 살펴보자.

1
knn.predict(X_test_scaled[:5])

array([‘Perch’, ‘Smelt’, ‘Pike’, ‘Perch’, ‘Perch’], dtype=object)

이제 확률을 어떻게 구했는지도 살펴보도록 하자. 그런데, 이때 어떤 class에 대한 확률인지는 알려주지 않는다. 그럼 아까 우리가 입력했던 것과 동일하지 않을까라고 생각할 수 있으나, 그렇지 않다. 굳이 랜덤으로 섞어버린다. 따라서 우리는 어떤 class에 대한 것인지도 확인을 해줘야 한다.

1
knn.predict_proba(X_test_scaled[:5])

array([[0. , 0. , 1. , 0. , 0. , 0. , 0. ], [0. , 0. , 0.33333333, 0. , 0. , 0.66666667, 0. ], [0. , 0. , 0. , 1. , 0. , 0. , 0. ], [0. , 0. , 0.66666667, 0. , 0.33333333, 0. , 0. ], [0. , 0. , 0.66666667, 0. , 0.33333333, 0. , 0. ]])

1
knn.classes_

array([‘Bream’, ‘Parkki’, ‘Perch’, ‘Pike’, ‘Roach’, ‘Smelt’, ‘Whitefish’],dtype=object)

여기서 우리는 한 가지 잘 생각해야 할 것이 있다. 우리는 KNN 알고리즘에서 $K=3$을 사용했기에 우리가 볼 수 있는 확률은 $0$, $\frac{1}{3}$, $\frac{2}{3}$, 1밖에 없다. 이렇게 성의 없게 적어놓는다면 확률형 아이템 확률 조작으로 뉴스에 나올 수도 있을 것이다. 조금 더 정확하게 확률을 구하기 위해서는 어떻게 해야 할까?

로지스틱 회귀

우리는 이전에 선형회귀에 대해서 배운 적이 있다. 그것을 바탕으로 이번 data의 linear model을 만들면 아래와 같이 만들 수 있을 것이다. 그런데, 여기서 한 가지 이상한 점이 있다. 바로 error $\varepsilon$이 보이지 않는다. 우리는 최종적으로 $z$를 예측하는 것이 목표가 아니라, sigmoid 함수를 통해 1이 될 확률을 구하는 것이므로 필요하지 않는 것이다.

\[z=\beta_0+\beta_1x_1+\beta_2x_2+\beta_3x_3+\beta_4x_4+\beta_5x_5 \tag{1}\]

시그모이드 함수(Sigmoid Function)는 다음과 같이 정의된다. 이 함수를 사용하면, input data가 무엇이든 output data의 범위를 0에서 1 사이로 제한할 수 있으며, 그 그래프는 아래와 같다. 따라서 방금 만든 $z$ 값을 이 sigmoid function에 대입한다면, 결과 값은 항상 0과 1 사이에 존재하게 된다.

\[\phi\overset{\text{def}}{=}\frac{1}{1+e^{-z}} \tag{2}\]

image

이진분류

그런데 이걸 가지고 뭘 어떻게 해야 할까? 간단한 예시로 이진 분류를 해보기로 하자. 아까 살펴본 데이터 중 도미와 빙어의 데이터만 빼내어 사용해 볼 것이다. 우선, 인덱스를 활용하여 도미와 빙어의 데이터만 빼내어 보자.

1
2
3
index = (y_train == 'Bream')|(y_train == 'Smelt')
X_train_new = X_train_scaled[index]
y_train_new = y_train[index]

이후, logistic regressor 모델을 만들고, 학습한 후 예측해 보자.

1
2
3
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(X_train_new, y_train_new)

그리고 아까 KNN 알고리즘에서 했던 것과 같이 처음 5개의 sample을 선택하여 예측하고, 그 확률을 구해보자.

1
lr.predict(X_train_new[:5])

array([‘Bream’, ‘Smelt’, ‘Bream’, ‘Bream’, ‘Bream’], dtype=object)

1
lr.predict_proba(X_train_new[:5])

array([[0.99760007, 0.00239993], [0.02737325, 0.97262675], [0.99486386, 0.00513614], [0.98585047, 0.01414953], [0.99767419, 0.00232581]])

이때, 아까 언급했듯이 class는 랜덤하게 배정되기에 어떤 class에 대한 확률인지 알기 위해서는 class를 따로 확인해 줘야 한다.

1
lr.classes_

array([‘Bream’, ‘Smelt’], dtype=object)

따라서 우리는 2개의 class가 있을 때 각 sample point에 대한 확률을 얻을 수 있었다. 그럼, 어떤 회귀식을 기반으로 학습을 했는지 확인해 보자. 그러기 위해서는 절편과 계수가 필요하다. 출력해 보자.

1
print(lr.intercept_, lr.coef_)

[-2.16172774] [[-0.40451732 -0.57582787 -0.66248158 -1.01329614 -0.73123131]]

이제 (1)에서 봤던 것을 바탕으로 우리는 다음과 같은 회귀식을 만들 수 있다. Simple linear regression model과 비슷한 모습임을 알 수 있다. 이때 $x_1$: Weight, $x_2$: Length, $x_3$: Diagonal, $x_4$: Height, $x_5$: Width이다.

\[z\approx -2.16-0.4x_1-0.58x_2-0.66x_3-x_4-0.73x_5 \tag{3}\]

그리고 회귀식이 있으면, 이것을 sigmoid function에 대입하여 그 범위를 0부터 1 사이로 바꿔줘야 한다. np.exp()로 직접 계산을 할 수는 있으나, 이미 sigmoid function이 정의가 되어 있으므로, 그것을 사용해 줄 것이다. 먼저, 각 sample point에 대한 $z$ 값을 구한 후, sigmoid function에 대입해 보자.

1
2
3
4
from scipy.special import expit

decision = lr.decision_function(X_train_new[:5])
decision

array([-6.02991358, 3.57043428, -5.26630496, -4.24382314, -6.06135688])

1
expit(decision)

array([0.00239993, 0.97262675, 0.00513614, 0.01414953, 0.00232581])

따라서 Bream은 0에 가까울 경우, Smelt는 1에 가까울 경우로 classify 됨을 알 수 있다. 이렇게 보면 logistic regression도 간단해 보인다. 그럼 multiclass classification은 어떻게 해야 할까?

다중분류

이번엔 방금 썼던 코드를 바탕으로 multiclass classification을 진행해 보자.

1
2
3
lr = LogisticRegression(C=20, max_iter=1000)
lr.fit(X_train_scaled, y_train)
print(lr.score(X_train_scaled, y_train), lr.score(X_test_scaled, y_test))

0.9327731092436975 0.925

그리고 처음 5개의 sample point의 output을 예측하고, 확률을 구해보자.

1
lr.predict(X_test_scaled[:5])

lr.predict(X_test_scaled[:5])

1
2
import numpy as np
np.round(lr.predict_proba(X_test_scaled[:5]), 3)

array([[0. , 0.013, 0.803, 0. , 0.176, 0.003, 0.004], [0. , 0.021, 0.148, 0. , 0.055, 0.776, 0. ], [0. , 0. , 0.049, 0.919, 0.022, 0.01 , 0. ], [0.005, 0.012, 0.4 , 0.007, 0.516, 0. , 0.061], [0. , 0. , 0.88 , 0.002, 0.114, 0.001, 0.002]])

1
lr.classes_

array([‘Bream’, ‘Parkki’, ‘Perch’, ‘Pike’, ‘Roach’, ‘Smelt’, ‘Whitefish’], dtype=object)

아까 했던 binary classification과 마찬가지로 각 class에 대한 확률이 나오는 것을 확인할 수 있다. 이번에도 linear function의 모습은 어떤지 coefficient와 intercept를 구해서 확인해 보자. 그런데, 이번에는 식으로 하나하나 다 쓰기엔 조금 많이 귀찮을 것이다. 각 class마다 $z$값을 계산해 가장 높은 $z$값을 출력하는 class를 최종 class로 선택하기 때문에 우리는 지금 5개의 sample point가 있고, class는 7개가 있기에 총 35개의 $z$값을 구해야 한다. 이런 건 컴퓨터가 알아서 풀게 맡기는 것이 정신건강에 좋을 것이다.

그런데 또 한 가지 의문이 드는 것이 있을 것이다. 물론 없을 수도 있다. 이전의 binary classification에서는 sample point마다 sigmoid function으로 0과 1 사이로 범위를 조절한 1개의 확률이 출력되었다. 그래서 1에 가까운 경우와 0에 가까운 경우를 가지고 분류를 했었는데, 이번에는 어떻게 해야 할까?

아까 나온 확률을 보면, 특징이 있음을 알 수 있다. 우선, 모두 0부터 1 사이의 값이라고 할 수 있을 것이다. 그리고 하나 더 특징이 있는데, 그것은 모두 다 더하면 1이 된다는 것이다. 즉, 간단히 생각해서 7개의 class에 대하여 선택될 확률을 그냥 나타낸 것이다.

그런데 $z$값을 어떻게 이렇게 변환할 수 있을까? 그것은 소프트맥스 함수(Softmax Function)를 사용하면 된다. Softmax function은 아래와 같이 정의할 수 있다. 아마 이 식도 보자마자 거부감이 드는 사람이 있으리라고 생각이 된다. 그런데, 이것도 생각보다 그렇게 어렵지는 않다.

\[g(x_i)\overset{\text{def}}{=}\frac{\exp(x_i)}{\sum^n_{j=1}\exp(x_j)}=\frac{e^{x_i}}{\sum^n_{j=1}e^{x_j}} \tag{4}\]

먼저, 7개의 class가 있고 각 class 별로 $z$ 값이 존재할 것이다. 그 $z$ 값들을 $z_1, z_2, \cdots, z_7$로 작성하고, 이 $z$를 (4)의 식에 넣어보자.

\[g(z_i)=\frac{e^{z_i}}{\sum^7_{j=1}e^{z_j}} \tag{5}\] \[g(z_1)=\frac{e^{z_1}}{\sum^7_{j=1}e^{z_j}}, g(z_2)=\frac{e^{z_1}}{\sum^7_{j=2}e^{z_j}}, \cdots, g(z_7)=\frac{e^{z_7}}{\sum^7_{j=1}e^{z_j}} \tag{6}\]

이제 이렇게 구한 값들이 모두 더하면 1이 되는지 확인해 보자. 아래와 같은 식 (7)이 성립하면 될 것이다.

\[g(z_1)+g(z_2)+g(z_3)+g(z_4)+g(z_5)+g(z_6)+g(z_7)=1 \tag{7}\]

(7)을 (6)을 통해서 다 더하면 아래와 같으며,

\[\frac{e^{z_1}+e^{z_2}+e^{z_3}+e^{z_4}+e^{z_5}+e^{z_6}+e^{z_7}}{\sum^7_{j=1}e^{z_j}} \tag{8}\] \[\sum^7_{j=1}e^{z_j}=e^{z_1}+e^{z_2}+e^{z_3}+e^{z_4}+e^{z_5}+e^{z_6}+e^{z_7} \tag{9}\]

(9)에 의해 모든 값을 더하면 1이 됨을 알 수 있다. 지금은 여기까지만 살펴보도록 하자. 어차피 나중에 인공신경망에 대해서 다룰 때 또다시 나올 것이다. 그때는 아래의 logistic regression 모형과 같은 이쁜 사진도 넣어서 설명을 해보도록 하겠다. 물론 귀찮지 않다면…

image

이제 식을 봤으니 binary classification에서 했던 것처럼 softmax function을 사용해서 확률을 구하는 코드도 작성해 보고, 확률을 살펴보자.

1
2
decision = lr.decision_function(X_test_scaled[:5])
decision

array([[-6.06763713, 0.96401561, 5.09039917, -2.9583443 , 3.57436605, -0.45286248, -0.14993692], [-9.65191575, 2.37908821, 4.33718972, -3.1567666 , 3.33853005, 5.99142119, -3.23754682], [-4.05773456, -6.27318934, 3.30845746, 6.23347698, 2.48567971, 1.74074581, -3.43743606], [-1.22123794, -0.30189847, 3.19426244, -0.91151159, 3.44834736, -5.51866484, 1.31070304], [-5.95427652, -1.98337964, 5.69935601, -0.32818047, 3.65659259, -0.77931945, -0.31079253]])

1
2
from scipy.special import softmax
np.round(softmax(decision, axis=1), 3)

array([[0. , 0.013, 0.803, 0. , 0.176, 0.003, 0.004], [0. , 0.021, 0.148, 0. , 0.055, 0.776, 0. ], [0. , 0. , 0.049, 0.919, 0.022, 0.01 , 0. ], [0.005, 0.012, 0.4 , 0.007, 0.516, 0. , 0.061], [0. , 0. , 0.88 , 0.002, 0.114, 0.001, 0.002]])

기본 숙제

이번에도 숙제가 있다. 숙제는 하라고 있는 것이기에 하기로 하자. 물론, 2번만 숙제이지만 1번부터 3번까지 다 할 것이다.

1. 2개보다 많은 클래스가 있는 분류 문제를 무엇이라 부르나요? 답: ②
우선, binary classification은 class가 2개인 경우의 classification을 말하는 것이다. 그리고 정답인 multiclass classification이 2개보다 많은 class가 있는 경우의 classification을 의미하는 것이다. 그리고 단변량 회귀(Univerate Regression)는 independent variable이 1개인 regression을 말하고, 다변량 회귀(Multivariate Regression)은 independent variable이 여러 개인 regression을 의미한다.

2. 로지스틱 회귀가 이진 분류에서 확률을 출력하기 위해 사용하는 함수는 무엇인가요?
답: ①
우리가 아까 이미 살펴본 것에 따르면, logistic regression에서 binary classification을 위해서는 sigmoid를, multiclass classification에서는 softmax를 사용한다고 말했던 것을 기억할 것이다. 이건 이전 게시글도 아니고 방금 본 것이다. 따라서 우리는 sigmoid가 정답이라는 것을 쉽게 알 수 있을 것이다.

3. decision_function() 메서드의 출력이 0일 때 시그모이드 함수의 값은 얼마인가요?
답: ③ (2)의 sigmoid funciton을 다시 한번 살펴보자. 만약 decision_funciton() 메서드의 출력이 0이라면, $z$가 0이 나왔다는 의미이다. 이때, 이 $z$를 아래의 함수에 대입하면,

\[\phi\overset{\text{def}}{=}\frac{1}{1+e^{-z}} \tag{10}\] \[\frac{1}{1+e^{-0}}=\frac{1}{1+1}=0.5 \tag{11}\]

어떤 수를 0 제곱한다면 1이 되는 것은 모두 알 것이기에 0.5가 나옴을 알 수 있다.

This post is licensed under CC BY 4.0 by the author.