머신러닝&딥러닝

Machine Learning #2 Logistic Regression & SVM : 정규직 여부 분류 모델

seungbeomdo 2023. 2. 7. 15:07

 

 

GitHub - SeungbeomDo/DataAnalysis: Practical Codes for Data Analysis using Machine Learning and Deep Learning

Practical Codes for Data Analysis using Machine Learning and Deep Learning - GitHub - SeungbeomDo/DataAnalysis: Practical Codes for Data Analysis using Machine Learning and Deep Learning

github.com


1. 로짓 회귀분석

1.1. 로짓 회귀분석의 도입

  • 선형회귀모델의 종속변수 $Y$는 대개 연속변수 혹은 수치형변수이다. 하지만 $Y$가 0 아니면 1의 값을 갖도록 문제를 설정해야 하는 경우도 있다. 가령 은행에서 대출을 해줄 때 이 사람이 연체를 할 사람인지(=1) 안 할 사람인지(=0)를 예측하는 문제다. 이런 류의 문제를 회귀(Regression)과 구분하여 분류(Classification)라고 이름 짓기도 한다.

 

  • 분류 문제를 해결하기 위해 선형회귀모델을 약간 수정하는 것을 고려해보자. 종속변수 $Y$가 0 아니면 1의 값을 갖는 더미변수가 되도록 하고 회귀식을 추정하자. 그러면 독립변수가 주어질 때 방정식을 통과해 나온 예측값은 대체로 0과 1 사이의 값을 가질 것이다. 0과 1 사이의 값이라면 확률이라고 부를 수 있다. 적당한 임계값(가령 0.5)을 두고 이를 넘으면 1로 분류, 넘지 못하면 0으로 분류하면 된다.

$$Y_{i} = \beta_{0} + \beta_{1}X_{i,1} + \beta_{2}X_{i,2} + ... + \beta_{k}X_{i,k} + \epsilon_{i}$$

 

  •  문제가 남아있다. 선형회귀모델은 말그대로 독립변수와 종속변수의 관계가 선형적이다. 회귀식의 기울기가 아무리 작더라도, 독립변수의 값이 양 또는 음의 방향으로 매우 커진다면 종속변수의 추정값은 0과 1 사이의 범위를 벗어나게 된다. 그럼 확률이라고 말할 수가 없다.

 

  • 이 문제를 해결하기 위해 로지스틱 함수를 도입한다. 로지스틱 함수 또는 로짓(Logit)는 다음과 같이 정의한다. 우리가 구하고자 하는 확률 $p$에 대하여

Logit = $ln\frac{p}{1-p}$

 

  • 추정된 $Y$를 로짓과 같다고 두면 다음의 식이 성립한다.

$$ln\frac{p_{i}}{1-p_{i}} = \beta_{0} + \beta_{1}X_{i,1} + \beta_{2}X_{i,2} + ... + \beta_{k}X_{i,k} + \epsilon_{i}$$

 

  • 양변을 잘 정리하면 $p$는 다음과 같다.

$$p_{i} = \frac{1}{1+e^{-(\beta_{0} + \beta_{1}X_{i,1} + \beta_{2}X_{i,2} + ... + \beta_{k}X_{i,k} + \epsilon_{i})}}$$

 

이때, $\lim_{y \rightarrow \infty}p = 1$이고 $\lim_{y \rightarrow -\infty}p = 0$

 

  • 이제 추정된 확률 p는 항상 0과 1 사이의 값을 갖도록 보장된다. 그 값이 연구자가 선택한 임계값보다 크면 1로 분류하고 작으면 0으로 분류하는 것이다.

 

  • 한편 로짓의 역함수를 시그모이드 함수(Sigmoid function)이라고도 부른다.

$$Sigmoid(x) = \frac{1}{1+e^{-x}}$$

 

1.2. 모델 훈련 및 검증

  • 2022년 경제활동인구조사 자료로부터 주어진 근로자가 정규직인지 아닌지를 분류하는 로짓 회귀모델을 만들어보자. 데이터 전처리 등은 이전 포스팅에서 했으므로 생략한다. i번째 근로자가 정규직일 확률 $p_{i}$에 대하여, 회귀식은 다음과 같다.

 

$$ln\frac{p_{i}}{1-p_{i}} = \beta_{0} +\beta_{1}성별_{i} +\beta_{2}학력_{i} + \beta_{3}기업규모_{i} +\beta_{4}임금_{i} +\beta_{5}경력_{i} +\beta_{6}나이_{i} +\beta_{7}노조보호여부_{i} + \epsilon_{i}$$

 

  • 사실 좀 어색한 모형이기는 하다. 성별이나 학력, 경력, 나이, 기업규모 등이 정규직 여부에 영향을 줄 수는 있겠지만 임금이나 노조보호여부 같은 것은 오히려 정규직 여부에 의해 설명되는 게 자연스럽다. 그냥 모델 구현 연습용이니까 찝찝해도 넘어가자.

 

  • 로짓 회귀모형에서 회귀계수를 추정할 때는 최대우도법(MLE)를 사용한다. 일반적으로 사용하는 우도(Likelihood)란

$$L(\beta) = \Pi_{y_{i}=1}p(x_{i}=1;\beta)\Pi_{y_{i}=0}(1 - p(x_{i}=1);\beta)$$

 

$p(x_{i}=1;\beta)$: 주어진 독립변수 벡터와 회귀계수 하에서 클래스 1에 속할 확률

 

  • 일반적으로는 이 값에 로그를 취해서(로그 우도) 덧셈 연산으로 바꾸고, 1계 조건을 때려박아 해석적 해를 구한다.

 

  • 한편 머신러닝 라이브러리에서 사용하는 손실함수는 이를 약간 수정한 이진 크로스 엔트로피(Binary Cross Entropy) 함수이다. 

$$\Sigma_{i}^{n} \{y_{i}ln[p(x_{i}=1;\beta)] + (1-y_{i})ln[1 - p(x_{i}=1);\beta]\}$$

 

  • train set과 test set을 분리하고, test set에 대해 성능을 측정한 결과는 아래와 같다.
from sklearn.linear_model import LogisticRegression

logit = LogisticRegression() 
logit.fit(train_X, train_y)

print("R square score for logit model : {:.5f}".format(r2_score(test_y, logit.predict(test_X))))
print("RMSE for logit model : {:.5f}".format(np.sqrt(mean_squared_error(test_y, logit.predict(test_X)))))

 

  • 변수 중요도는 아래와 같다. 노조 보호 여부와 성별, 기업 규모가 가장 중요하다고 나온다.

1.3. 스케일링 후 재훈련

  • 이전 포스팅에서도 언급했듯이 변수 간 상대적 중요도를 비교할 때는 스케일링 이슈가 치명적일 수 있다. 
  • min-max scaling을 거친 후 다시 변수 중요도를 뽑으면 임금과 경력이 가장 중요하다. 전술했듯이 임금은 정규직 여부를 설명하기보다는 정규직 여부에 의해 설명되는 변수일 것이다.

  • standard scaling을 거친 결과도 비슷하다.


2. 서포트 벡터 머신

2.2. SVM의 도입

  • SVM이란 분류 모델(때로는 회귀모델로도 쓰임)의 하나이다. 독립변수들을 차원으로 하는 초공간(Hyperspace)에서 클래스가 다른 샘플들을 분류해내는 초평면(Hyperplane)을 찾는 방법이다.

 

  • 가령 아래와 같은 2차원 평면에서 검은 돌과 하얀 돌을 분류해내는 것이다. X축, Y축은 독립변수이다. 조금 더 예를 들면, 어떤 사람이 정규직인지 알아내기 위해 그 사람의 학력과 그 사람이 속한 기업의 규모를 독립변수로 선택할 수 있다. 주어진 사람이 대졸이면 X축의 값은 16(년), 기업규모가 대기업이면 Y축의 값은 300(명)인 점을 표시한다. 이런 식으로 모든 사람들을 변수평면에 점으로 나타내고 이 점들을 정규직 여부에 따라 분리하는 선을 찾는 것이다.

 

 

2.2. SVM의 최적화

2.2.1. 마진의 최대화

  • 그렇다면 SVM은 어떤 식으로 최적의 분류 초평면을 찾아낼까? 분류 초평면이 가져야 할 성질을 생각해보면

 

  • (1) 최대한 오분류가 적어야 한다. 즉 정규직인데 비정규직으로, 비정규직인데 정규직으로 분류하는 경우가 최대한 적어야 한다.
  • (2) 분류 초평면은 서로 다른 두 클래스로부터 동시에 최대한 멀리 떨어져야 한다. 즉 분류를 똑바로 하더라도 어느 한 클래스에 가깝게 붙은 초평면이어서는 안 된다. 위 그림에서 1번 직선은 검은 돌과 흰 돌을 완벽하게 분류해낸다. 그렇지만 검은 돌 중에 조금만 독특한 녀석이 test set에 포함되면 금방 1번 직선은 오분류를 허용할 것이다. 따라서 두 클래스로부터 동시에 가장 멀리 떨어진 2번 직선이 제일 좋은 직선이다.

 

  • (1)과 (2)을 묶어서 표현하기 위해, 서포트 벡터(Support Vector)라는 개념이 필요하다. 서포트 벡터란 주어진 클래스의 샘플 중에서 분류 초평면과 가장 가깝게 위치한 샘플을 말한다. 가령 아래 그림에서 d1과 d2는 각각 흰 돌과 검은 돌의 서포트 벡터이다.

 

  • 따라서 분류 초평면은 서포트벡터로부터 가장 멀리 떨어져있어야 한다. 서포트벡터로부터 멀리 떨어지게 되면 당연히 오분류도 줄어든다. 각 서포트벡터와 분류초평면의 거리의 합을 마진(Margin)이라고 한다. 따라서 (1)과 (2)의 조건은 마진의 최대화로 집약된다. 수식으로 나타내면

$$Max (Margin) = Max \frac{||W||}{2} = Min \frac{2}{||W||}$$

 

  • 여기서 $W$는 SVM 초평면의 법선벡터를 말한다. 초평면과 서포트벡터 간의 거리(유클리드 거리)는 초평면과 직교하는 벡터의 크기와 같다는 점을 고려하면 그럴 듯하게 들린다.

 

 

2.2.2. 일반화 성능의 향상

  • 그런데 주어진 데이터셋으로부터 마진을 최대화하는 데에만 너무 집착하면, 일반화 성능이 떨어지게 된다. 가령 아래 그림에서 굵은 직선은 오분류를 모두 막아내기는 했지만, 너무 가까스로 분류를 해낸 탓에 다른 샘플이 주어지면 금방 오분류를 허용하게 될 것이다. 차라리 옅은 점선이 당장은 오분류를 허용하더라도 일반적으로는 더 안정적으로 분류를 해낼 수 있을 것이다.

 

 

  • 따라서 마진의 최대화라는 과제는 약간의 오분류를 허용하기 위해 다음과 같이 수정된다.

 $$Min \frac{2}{||W||} + C\Sigma_{i}\xi_{i}$$

 

  • 여기서 $\xi$는 샘플이 오분류되었을 때, 자신이 속해있어야 하는 클래스의 서포트벡터와의 거리를 나타낸다. 제대로 분류됐다면 $\xi=0$이고 오분류됐다면 $\xi>0$이다. 곱해진 상수 $C$는 오분류에 대해 SVM이 얼마나 관대하도록 훈련시킬지를 나타내는 파라미터이다.

 

  • 주어진 목적함수를 극대화하는 벡터 $W$는 라그랑지 승수법(Lagrangian)에 의해 해석적으로 구해진다.

 

2.3. 비선형 SVM

  • 모든 분류 문제가 두 클래스를 직선으로 구분할 만큼 단순하지는 않다. 예를 들어 다음과 같은 경우는 직선으로는 분류할 수가 없다.

 

  • 이를 해결하기 위해, 비선형 SVM은 변수 공간의 차원을 확대해 선형의 분류초평면이 그려지는 것이 가능한 상황을 만든다. 가령 아래와 같이 x축 상에서는 선형적으로 분류될 수 없던 문제가 y축을 추가해서 평면으로 옮기면 선형적으로 분류가 가능해진다.

 

 

  • 다만 차원이 확대될수록 목적함수의 식을 푸는 것이 점점 더 복잡해진다. 이 문제를 해결하기 위해 도입된 것이 커널 함수(Kernel Function)이다. 커널함수를 사용하면 데이터를 직접 변환하지 않고도 변환된 후의 결과와 동일한 값을 얻을 수 있다.

 

2.4. 다중 분류

  • 만약 분류해야 할 클래스가 3개 이상이면 어떻게 할까? 그럴 때는 초평면을 여러개 만들면 된다. 크게 두 가지 접근이 있는데

 

  • 첫번째는 OVR(One-Versus-Rest)이다. 3개의 클래스가 있다면 각각의 클래스를 다른 여타의 클래스와 구분하는 초평면을 3개 만든다. 그리고 주어진 샘플에 대해 각각의 분류초평면이 제시하는 확신의 정도가 가장 큰 클래스가 그 샘플의 클래스라고 예측한다.

 

  • 두번째는 OVO(One-Versus-One)이다. 3개의 클래스가 있다면 서로 다른 두 클래스를 구분하는 초평면을 3개 만든다. 주어진 샘플에 대해 초평면을 모두 적용해보고 가장 많이 예측된 클래스가 그 샘플의 클래스라고 예측한다.

 

2.5. 실습

  • 로지스틱 회귀분석을 적용했던 정규직 여부 분류 문제에 SVM도 적용해보자. 먼저 SVM은 거리를 계산해서 처리하는 알고리즘이므로, 스케일링이 필수적이라는 점에 유의하자. 여기서는 standard scaling을 사용했다.
from sklearn.preprocessing import StandardScaler

scale_data = data.drop(columns = ['gender', 'regular', 'union'])
remaining_data = data[['gender', 'regular', 'union']]

scaler = StandardScaler()
scaler.fit(scale_data)
trans_data = scaler.transform(scale_data)

trans_data_df = pd.DataFrame(data=trans_data, columns=scale_data.columns)
trans_data_df = pd.concat([trans_data_df.reset_index(drop=True), remaining_data.reset_index(drop=True)], axis = 1)
trans_data_df
  • train set을 분리하고
X = trans_data_df.drop(columns = 'regular')
y = trans_data_df['regular']

from sklearn.model_selection import train_test_split

train_X, test_X, train_y, test_y = train_test_split(X, y, train_size=0.8,
                                                    test_size=0.2, random_state=1)
print(train_X.shape, test_X.shape, train_y.shape, test_y.shape)
  • 하이퍼 파라미터 튜닝에 들어가야 하는데, 주어진 SVM에서 중요한 파라미터는 2가지이다. 하나는 어떤 커널함수를 사용할지, 다른 하나는 오분류에 대한 민감도이다.

 

  • 어떤 파라미터가 최적인지 알 수 없으므로 train set 내에서 validation set을 만들어서 후보 파라미터들을 훈련해보아야 한다. 두 개 이상의 파라미터에 대해 튜닝을 실시할 때는 GridSearchCV 라이브러리가 cross-val-score 라이브러리보다 코드적으로 간단하다.
from sklearn.model_selection import GridSearchCV

from sklearn.svm import SVC # SVC = SVM + Classification vs SVR = SVM + Regression
model_svm = SVC()

model_svm.fit(train_X, train_y)

parameters={'kernel':['rbf','linear','poly'],
            'C':[0.5,1.5,10],
            'random_state':[1]}

# HyperParameter를 Tuning
model_svm=GridSearchCV(estimator=model_svm, param_grid=parameters,
                       scoring='recall', cv=10, refit=True)

model_svm.best_params_
  • recall을 성능치로 사용했을 때, 가장 좋은 C값은 0.5, 가장 좋은 커널함수는 'polynomial(다항함수)'라고 한다.
from sklearn.metrics import accuracy_score,f1_score,precision_score,recall_score

model_svm.fit(train_X,train_y)

print("recall :", recall_score(test_y,model_svm.predict(test_X)))
print("precision :", precision_score(test_y,model_svm.predict(test_X)))
print("f1_score :", f1_score(test_y,model_svm.predict(test_X)))

  • 분류문제의 대표적인 성능치인 recall, precision, f1 score가 모두 높게 나와서 꽤나 괜찮은 모델임을 시사한다. recall은 실제 정규직일 때 모델이 정규직이라고 분류한 비율, precision은 모델이 정규직이라고 분류했을 때 실제로 정규직인 비율, f1 score는 recall과 precision의 조화평균이다.

 

  • 아래는 standard 스케일링을 적용한 로지스틱 회귀모형의 성능치이다. SVM이 recall에서 좀 더 우월하고 precision에서는 로지스틱 회귀모형이 좀 더 낫다는 것을 보여준다.