[머신러닝] 로지스틱 회귀(Logistic Regression)란?

훈련/검증/테스트(train/validate/test) 데이터 분리

  • 캐글은 보통 데이터셋을 훈련/테스트로 나누어 제공한다.
  • 테스트 데이터는 정답 컬럼이 지워진 채로 제공된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pandas as pd
train = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/titanic/train.csv')
test = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/titanic/test.csv')


print("train features: ", train.shape[1])
print("test features: ", test.shape[1])
'''
train features:  12
test features:  11
'''


# 없는 타겟 확인
print("target col: ", train.columns.difference(test.columns)[0])
'''
target col:  Survived
'''
  • 테스트 데이터에서는 타겟 정보를 제외해 놓았다.
  • 그 이유는 모델의 일반화 성능을 올바르게 측정하기 위함이다.
  • 검증셋이 필요한 이유는,
    • 훈련셋으로 모델을 한 번에 완전하게 학습시키기 어렵고,
    • 훈련셋으 다르게 튜닝된 여러 모델을 학습 후 어떤 모델이 학습이 잘 되었는지 검증하고 선택하는 과정이 필요기 때문이다.
  • 훈련/검증셋로 좋은 모델을 만들어 낸 후 최종적으로 테스트셋에서 단 한번의 예측 테스트를 진행한다.
  • 최종 테스트 결과가 마음에 들지 않는다고 모델을 또 수정한다면 그 모델은 테스트셋에 과적합하여 일반화 성능이 떨어진다.

3개의 데이터셋로 나누는 것이 머신러닝에서 중요한 이유

  • 훈련데이터는 모델을 핏(Fit)하는데 사용된다.
  • 검증데이터는 예측 모델을 선택하기 위해 예측의 오류를 측정할 때 사용한다.
  • 테스트데이터는 일반화 오류를 평가하기 위해 선택된 모델에 한하여 마지막에 한 번만 사용한다.
    • 훈련이나 검증과정에서 사용하지 않도록 주의해야한다.
    • 테스트데이터가 유출(leak)되어 훈련/검증과정에 사용되면 모델을 잘못 평가하게 된다.

모델검증

스크린샷 2021-08-12 14 02 22

  • 학습 모델 개발 시, 모델 선택(Model Selection)을 수행해야 한다.
  • 이 때 하이퍼파라미터(HyperParameter) 튜닝을 하게 되는데 튜닝의 효과를 확인하기 위해서 검증셋이 필요하다.
    • 하이퍼파라미터는 연구자가 수정할 수 있는 값이다.
      (학습률, Optimizer, 활성화 함수, 손실 함수 등 다양한 인자)
  • 테스트셋으로는 하이퍼파라미터 튜닝을 절대로 하면 안된다.
  • 데이터가 많은 경우 세가지로 나누면 되지만, 데이터수가 적은 경우 K-fold 교차검증을 진행할 수 있다.
    (이 때도 테스트셋은 미리 떼어 놓아야 한다.)

캐글의 데이터셋 나누기

  • 훈련 데이터를 훈련/검증셋으로 나눈다.
  • 사이킷런(Sklearn)의 train_test_split을 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
from sklearn.model_selection import train_test_split


train, val = train_test_split(train, random_state=2)


print("train shape: ", train.shape)
print("val shape: ", val.shape)
'''
train shape:  (668, 12)
val shape:  (223, 12)
'''

분류(Classification) 문제

  • 분류 문제는 회귀 문제와 다른 기준으로 기준 모델을 설정한다.
  • 다수 클래스를 기준모델로 정하는 방법을 사용한다.
  • 회귀 문제: 타겟 변수의 평균값
  • 분류 문제: 타겟 변수에서 가장 빈번하게 나타나는 범주
  • 시계열 문제: 어떤 시점을 기준으로 이전 시간의 데이터
  • 분류 문제에서는 타겟 변수가 편중된 범주 비율을 가지는 경우가 많다.
    • 편중된 다수 모델을 쓰지 않고 적은 모델을 사용하는 경우 착각이 일어날 수 있다.
    • 만약 다수 모델의 비율이 90%일 경우 정확도도 90%가 될 수 있는데, 그것보다 좋은 모델을 만들기 위해 노력해야 한다.

타겟 범주 비율 확인

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 타겟 설정
# survived => 0 = No, 1 = Yes
target = 'Survived'


# 타겟 데이터 범주의 비율 확인
y_train = train[target]
y_train.value_counts(normalize=True)
'''
0    0.625749
1    0.374251
Name: Survived, dtype: float64
'''


import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
sns.countplot(x=y_train); # 시각화 확인

스크린샷 2021-08-12 14 13 05

범주 0(majority class)으로 예측 수행

1
2
3
4
5
6
# mode(): 가장 높은 벨류 값 반환
major = y_train.mode()[0]


# 타겟 샘플 수 만큼 0이 담긴 리스트 생성. 기준모델로 예측
y_pred = [major] * len(y_train)

분류의 평가지표(evaluation metrics)

  • 회귀 평가 지표를 분류에 사용할 수 없다.
  • 분류 문제에서는 정확도를 평가지표로 사용한다.
    • Accuracy = $\frac{올바르게 예측한 수} {전체 예측 수}$ = $\frac{TP + TN} {P + N}$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from sklearn.metrics import accuracy_score
print("training accuracy: ", accuracy_score(y_train, y_pred)) # 기준 모델의 정확도 산출
# 최다 클래스의 빈도가 정확도가 된다.
'''
# training accuracy:  0.625748502994012
'''


y_val = val[target] 
y_pred = [major] * len(y_val)
print("validation accuracy: ", accuracy_score(y_val, y_pred)) # 검증세트에서의 정확도 확인
'''
validation accuracy:  0.5874439461883408
'''

로지스틱 회귀(Logistic Regression)

먼저 선형회귀 모델 사용

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
from sklearn.linear_model import LinearRegression


linear_model = LinearRegression()


# 숫자형 특성
features = ['Pclass', 'Age', 'Fare']
X_train = train[features]
X_val = val[features]


# Age, Cabin의 결측치를 평균 값으로 채운다.
from sklearn.impute import SimpleImputer # 심플하게 결측치들을 평균값으로 채운다.


## default, imputing 'mean' value
imputer = SimpleImputer() 
X_train_imputed = imputer.fit_transform(X_train)
X_val_imputed = imputer.transform(X_val)


# 학습
linear_model.fit(X_train_imputed, y_train)


# 예측
pred = linear_model.predict(X_val_imputed)


# 회귀계수 수치 확인
pd.Series(linear_model.coef_, features)
'''
Pclass   -0.203810
Age      -0.007513
Fare      0.000819
dtype: float64
'''
# Pclass의 경우 높을 수록(2, 3등석) 생존률이 떨어짐
# Age의 경우 많을 수록 생존률이 떨어짐
# Fare의 경우는 수치가 작지만 높을 수록 생존률이 올라감

가상의 테스트 케이스

1
2
3
4
5
test_case = [[1, 5, 600]] # 1등급의 5살 나이에 비싼요금: 무조건 생존할 것 같다
linear_model.predict(test_case) 
'''
array([1.28916042]): 1이 넘었다.
'''
  • 회귀 모델이기 때문에 타겟 변수값이 음수에서 양수까지 나타나는데 생존인지 아닌지 분명하게 결과를 알 수 없다.
  • 게다가 회귀이기 때문에 분류모델에 사용하는 평가 지표를 사용할 수 없다.

로지스틱 회귀 모델

  • 로지스틱 회귀를 사용하면 타겟 변수의 범주로 0과 1을 사용할 수 있으며 각 범주의 예측 확률값을 얻을 수 있다.
  • 로지스틱 회귀 모델의 식은 아래와 같다.

\(\large P(X)={\frac {1}{1+e^{-(\beta _{0}+\beta _{1}X_{1}+\cdots +\beta _{p}X_{p})}}}\) \(0 \leq P(X) \leq 1\)

  • 로지스틱 회귀는 로지스틱 함수, 시그모이드 함수 형태로 표현된다.

스크린샷 2021-08-12 14 27 15

  • 기준값은 0.5이다.
  • 결과적으로 관측치가 특정 클래스에 속할 확률값으로 계산된다.
  • 분류 문제에서는 확률값을 사용하여 분류하는데, 확률값이 정해진 기준값 보다 크면 1 아니면 0으로 예측한다.

로짓 변환(Logit Transformation)

  • 로지스틱 회귀의 계수는 비선형 함수 내에 있어서 직관적으로 해석하기 어려운데, 오즈(Odds)를 사용하면 선형결합 형태로 변환 가능해 보다 쉽게 해석이 가능하다.
  • 오즈는 실패 확률에 대한 성공 확률의 비인데 예를들어 오즈값이 4면, 성공 확률이 실패 확률의 4배라는 뜻이다.
  • 분류 문제에서는 클래스 1 확률에 대한 클래스 0 확률의 비라고 해석하면 된다.
    • $Odds = \large \frac{p}{1-p}$,
    • p = 성공 확률, 1-p = 실패 확률
    • p = 1 일때 odds = $\infty$
    • p = 0 일때 odds = 0
    \[\large ln(Odds) = ln(\frac{p}{1-p}) = ln(\frac{\frac {1}{1+e^{-(\beta _{0}+\beta _{1}X_{1}+\cdots +\beta _{p}X_{p})}}}{1 - \frac {1}{1+e^{-(\beta _{0}+\beta _{1}X_{1}+\cdots +\beta _{p}X_{p})}}}) = \normalsize \beta _{0}+\beta _{1}X_{1}+\cdots +\beta _{p}X_{p}\]
  • 로짓 변환(Logit transformation)은 오즈에 로그를 취해 변환하는 것이다.
  • 로짓 변환을 통해 비선형 형태인 로지스틱 함수 형태를 선형 형태로 만들어 회귀 계수의 의미를 해석하기 쉽게 하는데, 특성 X의 증가에 따라 로짓(ln(oddx))이 얼마나 증가 또는 감소 했다고 해석할 수 있다.
    (odds 확률로 해석을 하려면 exp(계수) = p 를 계산해서 특성 1단위 증가당 확률이 p배 증가한다고 해석을 할 수 있다.)
  • 기존 로지스틱형태의 y 값은 0~1의 범위를 가졌다면 로짓은 - ∞ ~ ∞ 범위를 가진다.

스크린샷 2021-08-12 14 37 35

로지스틱 회귀 vs 선형회귀

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
from sklearn.linear_model import LogisticRegression


logistic = LogisticRegression()
logistic.fit(X_train_imputed, y_train)


print('검증세트 정확도', logistic.score(X_val_imputed, y_val)) # 분류 정확도 리턴
'''
검증세트 정확도 0.7130044843049327
'''
# 기준모델보다 정확도가 높게 나왔다.


pred = logistic.predict(X_val_imputed) # 예측 결과 확인
pred
'''
array([1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
       0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0,
       0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0,
       1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
       0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0,
       0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0,
       0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0,
       0, 1, 0])
'''


logistic.predict(test_case) # 위에서 만든 테스트 케이스 사용


logistic.predict_proba(test_case) # 클래스에 속할 확률값 확인
'''
array([[0.01749669, 0.98250331]])
'''


print(features)
print(logistic.coef_) # 로지스틱 회귀의 계수 확인
'''
['Pclass', 'Age', 'Fare']
[[-0.90248227 -0.03581619  0.00447486]]
'''
# 선형회귀분석과 비교했을 때 회귀계수 수치는 변했지만 방향은 같음을 볼 수 있다.

타이타닉 데이터 사용 모델

  • 모델에 적용하기 전 데이터 변환을 수행한다.
    • OneHotEncoder
    • SimpleImputer
    • StandardScaler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
train.columns
'''
Index(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp',
       'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked'],
      dtype='object')
'''


train['Ticket'].value_counts() # 사용가능한 범주 확인
'''
347088              6
S.O.C. 14879        5
CA. 2343            5
382652              5
3101295             5
                   ..
STON/O2. 3101271    1
2693                1
345572              1
SCO/W 1585          1
315086              1
Name: Ticket, Length: 539, dtype: int64
'''
  • 사용 가능한 모든 변수를 선택한다.
    • ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
  • PassengerId, Name, Cabin, Ticket를 사용하지 않는 이유는,
    • 아이디나 이름 같은 특성을 가진 것들은 샘플별로 모두 다르기 때문에 일반화를 하기 위한 것에 도움이 되지 않는다.
    • 캐빈은 범주의 종류 너무 많고 결측치가 많다.
    • 티켓도 범주 종류가 너무 많다.
  • 사용하고자하는 특성에 대해 vaue_counts로 범주를 확인해야 한다.
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
from category_encoders import OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler


features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
target = 'Survived'


X_train = train[features]
y_train = train[target]


X_val = val[features]
y_val = val[target]


# 원핫인코딩
encoder = OneHotEncoder(use_cat_names=True)
X_train_encoded = encoder.fit_transform(X_train) 
X_val_encoded = encoder.transform(X_val) # fit하지 않는 이유: 검증셋에서 어떠한 범주가 부족하면 문제가 생길 수 있다.


# 결측치 평균으로 변환
imputer = SimpleImputer(strategy='mean')
X_train_imputed = imputer.fit_transform(X_train_encoded)
X_val_imputed = imputer.transform(X_val_encoded)


# 특성값들을 표준화
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train_imputed)
X_val_scaled = scaler.transform(X_val_imputed)


X_train_scaled.T[0].mean(), X_train_scaled.T[0].std()
# 평균은 0, 표준편차는 1로 표준화


model = LogisticRegression(random_state=1)
model.fit(X_train_scaled, y_train) # LogisticRegression(random_state=1)


# 정확도 확인
y_pred = model.predict(X_val_scaled)
accuracy_score(y_val, y_pred) # 0.7892376681614349


# 계수 확인
coefficients = pd.Series(model.coef_[0], X_train_encoded.columns)
coefficients
'''
Pclass         -0.915833
Sex_female      0.662095
Sex_male       -0.662095
Age            -0.559957
SibSp          -0.406466
Parch          -0.015897
Fare            0.078016
Embarked_S     -0.094939
Embarked_Q      0.007684
Embarked_C      0.077224
Embarked_nan    0.188837
dtype: float64
'''


# 시각화
coefficients.sort_values().plot.barh();

스크린샷 2021-08-12 14 51 50

1
2
3
4
5
model.intercept_
'''
array([-0.71320882])
'''
# 절편(intercept)은 마이너스로 모든 특성이 0인 경우 생존하지 못할 가능성이 높다고 알려주긴 하지만 사실 관측할 수 없는 예시로 해석이 크게 유용하지 않다.

모델을 테스트셋에 적용 후 캐글에 제출

1
2
3
4
5
6
7
8
9
10
11
12
X_test = test[features]
X_test_encoded = encoder.transform(X_test)
X_test_imputed = imputer.transform(X_test_encoded)
X_test_scaled = scaler.transform(X_test_imputed)


y_pred_test = model.predict(X_test_scaled)


submission = test[['PassengerId']].copy()
submission['Survived'] = y_pred_test
submission

스크린샷 2021-08-12 14 54 37

1
submission.to_csv('submission_titanic.csv', index=False)

참조

0%