기준 모델(Baseline Model)

기준 모델이 뭐지?

baseline
an imaginary line used as a starting point for making comparisons
- Cambridge English Dictionary

a line serving as a basis
- Merriam-Webster

AI : 사장님. 저 일. 다했습니다. 저 잘했습니다. 삐릿삐릿.
사장 : 너는 뭘 믿고 너가 잘했다고 생각하니?
AI : 제가. '저 친구'보다는. 잘했습니다. 삐릿삐릿.
- 작자 미상 (사실은 블로그 작성자 머릿속...)

기준 모델은 머신 러닝 모델을 만들 때 말 그대로 '기준'이 되는 모델이다.
위에서 AI가 '저 친구'보다는 잘했다고 말한 것처럼, 학습 및 최적화를 거친 모델이라면 최소한 기준 모델보다는 성능이 좋아야 한다.

기준 모델은 무조건 이렇게 정해야 한다는 규칙은 없다.
다만 가장 간단하면서 흔하게 정해지는 유형은 다음과 같다.

  • 회귀 문제 : 타겟의 평균값 (또는 중앙값)
  • 분류 문제 : 타겟의 값 중 가장 많은 것 (타겟의 최빈 클래스)
  • 시계열 문제 : 이전 타임스탬프의 값

앞서 말했듯이 꼭 이를 기준 모델로 삼을 필요는 없다.
회귀 문제라면 선형 회귀를, 분류 문제라면 로지스틱 회귀를 기준 모델로 정할 수도 있다.
즉, 정하는 사람 마음이다.


기준 모델은 어떻게 쓰는거지?

기준 모델이 무엇인지 알았으니 구체적인 사용 예시를 코드와 함께 살펴보겠다.

여기서는 회귀와 분류 문제 각각에서 기준 모델을 설정 후, 이를 실제로 만들 모델(이하 실제 모델)과 어떤 식으로 비교하는지 설명하겠다.

(코드에 대한 구체적인 설명은 아래 참고 자료에 포함된 Colab 링크를 보시면 확인 가능합니다.)


  1. 데이터 전처리 및 가공
# ! pip install category_encoders
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

from category_encoders import TargetEncoder, OrdinalEncoder
from sklearn.preprocessing import StandardScaler
from sklearn.compose import TransformedTargetRegressor

from sklearn.linear_model import RidgeCV, LogisticRegression

from sklearn.model_selection import train_test_split, RandomizedSearchCV
from sklearn.pipeline import make_pipeline
from sklearn.metrics import mean_absolute_error, r2_score, accuracy_score, f1_score, classification_report
from scipy.stats import randint, uniform
# 자료 출처 : 서울 열린데이터 광장 - 서울특별시 부동산 실거래가 정보
# https://data.seoul.go.kr/dataList/OA-15548/S/1/datasetView.do

df = pd.read_csv('https://docs.google.com/uc?export=download&id=1WyeDe2Ry4ohJobcbnQJXHxFEAtcg-CC_', encoding='cp949')
df = df[df['지번코드'].map(str).apply(len) == 19]
tmp_idx = df[df['층정보'] < 0]['층정보'].index
df.loc[tmp_idx, ['층정보']] = df[df['층정보'] < 0]['층정보'].mul(-1)
df = df[(df['건축년도'] != 0) & ~(df['건축년도'].isna())]
df['층정보'] = df['층정보'].fillna(2)

df = df[['지번코드',  '건물면적',  '층정보',  '건물주용도코드',  '물건금액',  '건축년도']]
df['연식'] = (2020 - df['건축년도']).astype(int)
df['주택규모'] = df['건물면적'].apply(lambda x: 'S' if x <= 60 else 'L' if x > 85 else 'M')
df['고가주택여부'] = df['물건금액'].apply(lambda x: 1 if x > 900000000 else 0)
df[['층정보', '건축년도']] = df[['층정보', '건축년도']].astype(int)
df.columns = ['addr_code', 'size', 'floor', 'type', 'price', 'built_year', 'years', 'size_grade', 'is_high_price']
target = 'price'
df = df[df[target] < np.percentile(df[target], 95)]
df = df[df['size'] < np.percentile(df['size'], 95)]
df = df[df['years'] < np.percentile(df['years'], 95)]

df = df.reset_index(drop=True)

  1. 기준 모델 설정 및 평가
    회귀 : 타겟의 평균값 (평가지표 : MAE)
    분류 : 타겟의 최빈 클래스 비율 (평가지표 : 정확도)
# 기준 모델(회귀) : 타겟(price)의 평균값
baseline = df.price.mean()
baseline

470946234.08997595

# 기준 성능(회귀) - MAE
errors = baseline - df.price
baseline_mae = errors.abs().mean()
print(f'Baseline MAE: {baseline_mae:,.0f}')

Baseline MAE: 254,535,177

# 기준 모델(분류) : 타겟(is_high_price)의 최빈 클래스 비율
sns.countplot(x=df.is_high_price);
df.is_high_price.value_counts(normalize=True) 

# 기준 성능(분류) - 정확도 = 0.895
# 모든 예측값을 0으로 찍어도 정확도가 0.895가 나옴
# 따라서 실제 모델의 정확도는 0.895보다 높아야 유효

0 0.895478
1 0.104522
Name: is_high_price, dtype: float64


  1. 실제 모델 만들기 및 평가
# 물건가격 예측은 회귀, 고가주택 여부는 분류
# 회귀 : 릿지 회귀
# 분류 : 로지스틱 회귀

# 훈련 / 테스트 셋으로 분리
train, test = train_test_split(df, test_size=0.30, random_state=2)
train.shape, test.shape

((95474, 9), (40918, 9))

# 특성과 타겟 분리하기
target_cl = 'is_high_price' # 분류 문제 타겟
targets = [target, target_cl]
features = train.drop(columns=targets).columns

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

X_test = test[features]
y_test = test[target]
y_test_cl = test[target_cl]
# 범주형 특성 인코딩을 위해 특성들을 구분
tge_col = ['addr_code', 'type'] # 범주 간의 순서가 없는 특성
ord_col = ['size_grade'] # 범주 간의 순서가 있는 특성(소/중/대)
# 1. 회귀 - 릿지 회귀
alphas = [0, 0.001, 0.01, 0.1, 1]

pipe_ridge = make_pipeline(
    TargetEncoder(cols=tge_col),
    OrdinalEncoder(cols=ord_col),
    RidgeCV(alphas=alphas, cv=5)
)

tt_ridge = TransformedTargetRegressor(regressor=pipe_ridge,
                                func=np.log1p, inverse_func=np.expm1)

tt_ridge.fit(X_train, y_train);

y_pred_ridge = tt_ridge.predict(X_test)
mae_ridge = mean_absolute_error(y_test, y_pred_ridge)
print(f'MAE: {mae_ridge:,.0f}')

MAE: 86,647,593

# 2. 분류 - 로지스틱 회귀

# class weights 계산
# n_samples / (n_classes * np.bincount(y))
custom = len(y_train_cl)/(2*np.bincount(y_train_cl))

pipe_logis = make_pipeline(
    TargetEncoder(cols=tge_col),
    OrdinalEncoder(cols=ord_col),
    StandardScaler(),    # 각 특성별 스케일을 통일시켜줌
    LogisticRegression(class_weight={False:custom[0],True:custom[1]}, random_state=2)
)

pipe_logis.fit(X_train, y_train_cl)

y_pred_logis = pipe_logis.predict(X_test)
accu_logis = accuracy_score(y_test_cl, y_pred_logis)
print(f'Accuracy Score: {accu_logis:,.3f}')

Accuracy Score: 0.953


  1. 결론
  • 회귀
    • 기준 모델 MAE = 254,535,177
    • 실제 모델 MAE = 86,647,593
  • 분류
    • 기준 모델 정확도 = 0.895
    • 실제 모델 정확도 = 0.953

회귀, 분류 모두 실제 모델이 기준 모델보다 성능이 뛰어나므로 실제 모델을 '당장 버릴 필요는 없다'고 볼 수 있다.


  1. 주의할 점

당연한 말이지만, 기준 모델과 실제 모델의 성능 비교 시 동일한 평가지표를 기준으로 하여 비교해야 한다.

나의 경우, 타겟의 최빈 클래스 비율을 실제 모델의 F1 Score와 비교를 하는 실수를 저질렀는데, 둘 모두 0에서 1 사이의 값으로 표현이 되다 보니 작업을 하는 도중 혼동이 있었다.

따라서 비교 대상이 무엇인지 명확하게 한 후에 비교를 진행해야 할 것이다.

또한 만든 모델이 기준 모델보다 성능이 좋다고 해서 항상 사용 가능하다고 볼 수는 없다.
위에서 '당장 버릴 필요는 없다'고 표현한 이유도, 당장은 기준 모델보다 성능이 좋아도 이 모델을 실전에 투입하는 것은 다른 문제이기 때문이다.

예를 들어 기준 모델의 정확도가 0.3이고 실제 모델은 0.31이 나왔다면 이를 바로 실전에서 쓸 수 있을까?
상황에 따라 다르겠지만, 아무래도 바로 쓰기에는 망설여질 것이다.


<참고 자료>

 

릿지 회귀(Ridge Regression)

릿지 회귀가 뭐지?

Ridge regression is a method of estimating the coefficients of multiple-regression models in scenarios where independent variables are highly correlated.
- Wikipedia(Ridge regression)

기존의 다중 선형 회귀선을 팽팽한 고무줄이라고 하면, 릿지 회귀는 이 고무줄을 느슨하게 만들어준 것이다.

다중 선형 회귀 모델은 특성이 많아질수록 훈련 데이터에 과적합되기 쉽다. 이는 마치 "나는 훈련에서 무조건 만점을 받겠어!"라고 하여 열심히 훈련을 해서 만점을 받았는데, 정작 실전에서는 뭐 하나 제대로 못하는 병사같다.
이에 반해 릿지 회귀는 훈련은 좀 덜 열심히 했지만, 실전에서는 꽤 쓸만한 병사라고 할 수 있겠다.


릿지 회귀는 어떻게 생겼지?

과적합된 다중 선형 회귀 모델은 단 하나의 특이값에도 회귀선의 기울기가 크게 변할 수 있다. 릿지 회귀는 어떤 값을 통해 이 기울기가 덜 민감하게 반응하게끔 만드는데, 이 값을 람다(lambda, $\lambda$)라고 한다.
릿지 회귀의 식은 아래와 같다.

$$\beta_{ridge} : argmin[\sum_{i=1}^n(y_i - \beta_0 - \beta_1x_{i1}-\dotsc-\beta_px_{ip})^2 + \lambda\sum_{j=1}^p\beta_j^2]$$

(n: 샘플 수, p: 특성 수, λ: 튜닝 파라미터(패널티))
(참고 - 람다 : lambda, alpha, regularization parameter, penalty term 모두 같은 뜻)

식의 앞 부분은 다중 선형 회귀에서의 최소제곱법(OLS, Ordinary Least Square)과 동일하다.
뒤쪽의 람다가 붙어 있는 부분이 기울기를 제어하는 패널티 부분이다.
뒷부분을 자세히 보면 회귀계수 제곱의 합으로 표현되어 있는데, 이는 L2 Loss$^*$$^1$와 같다. 이런 이유로 릿지 회귀를 L2 정규화(L2 Regularization)라고도 한다.

만약 람다가 0이면 위 식은 다중 선형 회귀와 동일하다.
반대로 람다가 커지면 커질수록 다중 회귀선의 기울기를 떨어뜨려 0으로 수렴하게 만든다. 이는 덜 중요한 특성의 개수를 줄이는 효과로도 볼 수 있다.

적합한 람다의 값을 구하는 방법은 아래에서 scikit-learn을 통해 예시를 들겠다.


scikit-learn을 이용하여 릿지 회귀 수행하기

scikit-learn을 통해 릿지 회귀를 사용하는 방법으로 Ridge와 RidgeCV가 있다.
둘 모두 같은 릿지 회귀이나, RidgeCV는 여러 alpha값(=람다)을 모델 학습 시에 한꺼번에 받아서 자기 스스로 각각의 alpha값에 대한 성능을 비교 후 가장 좋은 alpha를 선택한다. (CV : Cross Validation$^*$$^2$)

아래 예시에서는 먼저 다중 선형 회귀와 릿지 회귀의 차이를 간단히 살펴본 후, 보다 구체적인 Ridge 그리고 RidgeCV의 사용을 다루겠다.

  1. 다중 선형 회귀와 릿지 회귀 간단한 비교
    (사용 데이터 : Anscombe's quartet)
# 데이터 불러오기
import seaborn as sns

ans = sns.load_dataset('anscombe').query('dataset=="III"')
baseline = ans.y.mean() # 기준 모델
sns.lineplot(x='x', y=baseline, data=ans, color='red'); # 기준 모델 시각화
sns.scatterplot(x='x', y='y', data=ans);

 

# 다중 선형 회귀(OLS)
%matplotlib inline

ax = ans.plot.scatter('x', 'y')

# OLS 
ols = LinearRegression()
ols.fit(ans[['x']], ans['y'])

# 회귀계수와 intercept 확인
m = ols.coef_[0].round(2)
b = ols.intercept_.round(2)
title = f'Linear Regression \n y = {m}x + {b}'

# 훈련 데이터로 예측
ans['y_pred'] = ols.predict(ans[['x']])

ans.plot('x', 'y_pred', ax=ax, title=title);

 

# 릿지 회귀
import matplotlib.pyplot as plt
from sklearn.linear_model import Ridge

def ridge_anscombe(alpha):
    """
    alpha : lambda, penalty term
    """
    ans = sns.load_dataset('anscombe').query('dataset=="III"')

    ax = ans.plot.scatter('x', 'y')

    ridge = Ridge(alpha=alpha, normalize=True)
    ridge.fit(ans[['x']], ans['y'])

    # 회귀계수와 intercept 가져오기
    m = ridge.coef_[0].round(2)
    b = ridge.intercept_.round(2)
    title = f'Ridge Regression, alpha={alpha} \n y = {m}x + {b}'

    # 예측
    ans['y_pred'] = ridge.predict(ans[['x']])

    ans.plot('x', 'y_pred', ax=ax, title=title)
    plt.show()

# 여러 알파값을 넣어서 기울기의 변화 확인하기
alphas = np.arange(0, 2, 0.4)
for alpha in alphas:
    ridge_anscombe(alpha=alpha)

 

∴ 그래프를 보면 alpha = 0인 경우에는 OLS와 같은 그래프 형태로 같은 모델임을 확인할 수 있고. alpha 값이 커질수록 직선의 기울기가 0에 가까워 지면서 평균 기준모델(baseline)과 비슷해진다.


  1. Ridge & RidgeCV 구체적인 활용 방법

1) 데이터 불러오기 및 전처리
(사용할 데이터 : Melbourne Housing Market)

import pandas as pd
from sklearn.model_selection import train_test_split

# 데이터 불러오기
df = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/melbourne_house_prices/MELBOURNE_HOUSE_PRICES_LESS.csv')

# 범주형 특성 중 값의 종류가 너무 많은 특성은 제외
df.drop(columns=['Suburb','Address','SellerG','Date'], inplace=True)

# 결측치인 타겟 값 제거
df.dropna(subset=['Price'], inplace=True)

# 중복된 행 제거
df.drop_duplicates(inplace=True)
from category_encoders import OneHotEncoder

# 사용할 특성들과 타겟을 별도로 분리
target = 'Price'

data = df.drop(target, axis=1)
target = df[target]

# 훈련 / 테스트 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(data, target, train_size=0.8, test_size=0.2, random_state=2)

# 범주형 특성을 수치형으로 변환하는 인코딩 수행
# 자세한 내용은 아래 참고 자료 링크(One-hot Encoder)에 있습니다
encoder = OneHotEncoder(use_cat_names = True)
X_train = encoder.fit_transform(X_train)
X_test = encoder.transform(X_test)

2) Ridge

from sklearn.linear_model import Ridge

alphas = [0, 0.001, 0.01, 0.1, 1]

# Ridge의 경우 alpha값을 이와 같이 따로 따로 넣어주어야 함
for alpha in alphas:
  ridge = Ridge(alpha=alpha, normalize=True)
  ridge.fit(X_train, y_train)
  y_pred = ridge.predict(X_test)

  mae = mean_absolute_error(y_test, y_pred)
  r2 = r2_score(y_test, y_pred)
  print(f'Test MAE: ${mae:,.0f}')
  print(f'R2 Score: {r2:,.4f}\n')

Test MAE: $255,214
R2 Score: 0.5877

Test MAE: $255,264
R2 Score: 0.5878

Test MAE: $254,701
R2 Score: 0.5874

Test MAE: $252,997
R2 Score: 0.5794

Test MAE: $279,498
R2 Score: 0.4742


3) RidgeCV

from sklearn.linear_model import RidgeCV
from sklearn.metrics import mean_absolute_error, r2_score

alphas = [0, 0.001, 0.01, 0.1, 1]

# RidgeCV는 alpha로 넣고자 하는 값들을 리스트로 전달하면 내부적으로 최적의 alpha값을 찾아냄
ridgecv = RidgeCV(alphas=alphas, normalize=True, cv=5)
# cv : cross-validation -> 데이터를 k등분한 후 각각에 대하여 검증 진행
# 검증 결과 가장 점수가 높은 모델을 채택
ridgecv.fit(X_train, y_train)
y_pred = ridgecv.predict(X_test)

mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f'Test MAE: ${mae:,.0f}')
print(f'R2 Score: {r2:,.4f}\n')

print(f'alpha: {ridgecv.alpha_}') # 최종 결정된 alpha값
print(f'cv best score: {ridgecv.best_score_}') # 최종 alpha에서의 점수(R^2 of self.predict(X) wrt. y.)

Test MAE: $255,264
R2 Score: 0.5878

alpha: 0.001
cv best score: 0.5705823371670962


*1 L2 Loss란?
L2 Loss는 L2 Norm을 기준으로 만들어진 손실 함수이다.
다만 L2 Norm이 각 원소 제곱의 합에 루트를 씌워준 것이라면 L2 Loss는 루트를 씌우지 않는다는 차이가 있다.
$$L2\ Norm = \sqrt{\sum_{i=1}^{n} x_i^2}$$
$$L2\ Loss = \sum_{i=1}^{n} (y - \hat{y})^2$$

*2 CV : Cross Validation(교차 검증)?
교차 검증은 주어진 데이터를 동일한 크기로 (고등어 자르듯이) 여러 등분한 후, 각각에 대하여 검증을 진행하는 것이다.
이는 모델이 한 가지 경우에만 잘 맞는, 즉 과적합을 줄이고자 행하는 작업이다.


<참고 자료>

랜덤 포레스트(Random Forest)

※ 결정 트리를 모른다면?(아래 클릭 시 링크로 이동)
결정 트리(Decision Tree)

랜덤 포레스트가 뭐지?

기계 학습에서의 랜덤 포레스트(영어: random forest)는 분류, 회귀 분석 등에 사용되는 앙상블 학습 방법$^*$$^1$의 일종으로, 훈련 과정에서 구성한 다수의 결정 트리로부터 분류 또는 평균 예측치(회귀 분석)를 출력함으로써 동작한다.
- 위키백과

랜덤 포레스트는 단순하게 말하면 결정 트리 여러 개를 모아 놓은 것이다.
이름 그대로 나무(Tree)가 모여 있으니 숲(Forest)이 되는 것이다.

랜덤(Random, 무작위)이라는 말은 왜 붙어 있을까? 이는 아래 내용을 참고하면 이해 가능할 것이다.

초기 랜덤 포레스트는 단일 트리를 확장할 때 가능한 결정(available decisions)중 임의의 부분집합(random subset)에서 검색하는 아이디어를 도입한 얄리 아미트(Yali Amit)와 도널드 게먼(Donald Geman)의 연구에 영향을 받았다. 또한 임의의 부분공간(random subspace)을 선택하는 틴 캄 호(Tin Kam Ho)의 아이디어 역시 랜덤 포레스트의 디자인에 영향을 미쳤다. - 위키백과(랜덤 포레스트 - 1 도입 - 1.2 역사 中)

랜덤 포레스트는 왜 만들어졌을까?
랜덤 포레스트는 결정 트리로부터 나온 것이다. 결정 트리는 한 개의 트리만을 사용하기에 한 노드에서의 에러가 그의 하부 노드까지 계속해서 영향을 주기도 하고, 트리의 깊이에 따라 과적합되는 문제 등이 있다. 이러한 이유로 새로운 데이터의 분류에 유연하게 대응하지 못하는데, 이를 보완하기 위해 나온 것이 랜덤 포레스트다.


랜덤 포레스트는 어떻게 하는거지?

랜덤 포레스트는 아래와 같은 순서로 진행된다.

  1. 초기 데이터셋(Original Dataset)으로부터 파생된 여러 개의 데이터셋을 만든다. 이 과정은 부트스트랩 샘플링$^*$$^2$을 통해 이루어진다.
  2. 1번에서 만들어진 각각의 데이터셋에 대한 결정 트리를 만든다. 여기서 주의할 점은 트리를 만들 때 트리의 각 분기마다 해당 데이터셋의 특성 중 일부를 무작위로 가져온다는 것이다.

이렇게 해서 만들어진 결정 트리들을 모아놓은 것이 바로 랜덤 포레스트이다.

그럼 랜덤 포레스트는 타겟을 어떻게 예측할까?

만약 어떤 특성을 가진 데이터의 타겟을 예측하기 위해 랜덤 포레스트에 넣는다고 하자.
이 랜덤 포레스트 안에는 여러 개의 결정 트리가 있고, 각각의 결정 트리는 이 데이터에 대한 타겟 예측을 내놓는다. 그 후 이들의 예측 모두를 종합하여 최종 타겟 예측 값을 결정하는데, 이를 배깅(Bagging, Bootstrap Aggregating)이라고 한다.

회귀 문제에서는 예측을 종합한 값의 평균이, 분류 문제에서는 다수결로 선택된 클래스가 최종 예측 값이 된다.

잠깐, 예측하기 전에 검증은 어떻게 하지?

랜덤 포레스트를 만들 때 필요한 여러 데이터셋을 만드는 과정에서 부트스트랩 샘플링을 사용한다고 하였다. 여기서 샘플링 과정에 포함되지 않는 데이터가 생기는데, 이를 Out-Of-Bag(OOB) 데이터라고 한다.
랜덤 포레스트의 검증은 이 OOB 데이터를 통해 이루어진다. 이를 통해 OOB 데이터에 대한 예측 결과와 실제 값의 오차, 즉 OOB 오차(Out-Of-Bag Error)를 얻을 수 있다.

이후 검증 결과를 바탕으로 랜덤 포레스트의 하이퍼 파라미터(개별 트리의 최대 깊이, 노드 분기를 위한 최소한의 샘플 수 등)를 조절하여 OOB 오차를 최소화시킨다.

scikit-learn을 통해 랜덤 포레스트 사용하기

  1. 데이터 불러오기 및 전처리
# 데이터 불러오기
import pandas as pd

target = 'vacc_h1n1_f'
train = pd.merge(pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/train.csv'),
pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/train_labels.csv')[target], left_index=True, right_index=True)
test = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/test.csv')
sample_submission = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/submission.csv')
# 데이터를 훈련 / 검증 세트로 분리
from sklearn.model_selection import train_test_split

train, val = train_test_split(train, train_size=0.80, test_size=0.20,
stratify=train[target], random_state=2)

train.shape, val.shape, test.shape

((33723, 39), (8431, 39), (28104, 38))

# 데이터 전처리
def  engineer(df):
    """특성을 엔지니어링 하는 함수입니다."""
    # 새로운 특성을 생성합니다.
    behaviorals = [col for col in df.columns if  'behavioral'  in col]
    df['behaviorals'] = df[behaviorals].sum(axis=1)

    dels = [col for col in df.columns if  ('employment'  in col or  'seas'  in col)]
    df.drop(columns=dels, inplace=True)

    return df

train = engineer(train)
val = engineer(val)
test = engineer(test)
# 데이터를 특성과 타겟으로 분리
features = train.drop(columns=[target]).columns  

X_train = train[features]
y_train = train[target]
X_val = val[features]
y_val = val[target]
X_test = test[features]
  1. 랜덤 포레스트 사용하기
from category_encoders import OrdinalEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.pipeline import make_pipeline  

# 범주형 특성들의 값을 숫자로 대체하기 위하여 OrdinalEncoder 사용
# handle_missing="return_nan"은 범주형 특성에 있는 NaN을 항상 -2로 대체시킴
# 만약 handle_missing="value"(기본값)인 경우 NaN이 -2가 아닌 일반적인 범주 종류로 인식되어 값이 대체될 수도 있다.
pipe = make_pipeline(
    OrdinalEncoder(handle_missing="return_nan"),
    SimpleImputer(),
    RandomForestClassifier(max_depth=13, n_estimators=120, random_state=10, n_jobs=-1, oob_score=True)
)  
# RandomForestClassifier의 n_estimators는 랜덤 포레스트 내의 결정 트리 개수를 의미함
# 그 외 결정 트리의 과적합을 줄이기 위해 사용하는 max_depth, min_samples_split, min_samples_leaf도 설정 가능

pipe.fit(X_train, y_train)
print('훈련 정확도', pipe.score(X_train, y_train))
print('검증 정확도', pipe.score(X_val, y_val))
print('f1 score', f1_score(y_val, pipe.predict(X_val)))

훈련 정확도 0.8894523025828069
검증 정확도 0.8315739532677026
f1 score 0.5520504731861199

# OOB 스코어(OOB 오차의 반대 개념) 확인
pipe.named_steps['randomforestclassifier'].oob_score_

0.821516472437209


*1 앙상블 학습 방법?

앙상블(프랑스어: ensemble)은 전체적인 어울림이나 통일. ‘조화’로 순화한다는 의미의 프랑스어이며 음악에서 2인 이상이 하는 노래나 연주를 말한다.
- 위키백과(앙상블)

보통 우리가 아는 '앙상블'이라는 말의 의미는 위와 같다.

앙상블 방법은 한 종류의 데이터로 여러 머신러닝 학습 모델(weak base learner, 기본 모델)을 만들어 그 모델들의 예측 결과를 다수결이나 평균을 내어 예측하는 방법이다.
랜덤 포레스트는 결정 트리를 기본 모델로 사용하는 앙상블 방법이다.


*2 부트스트랩 샘플링?


부트스트랩 샘플링이란 어떠한 데이터셋에 대하여 샘플링을 할 때 복원 추출, 즉 뽑았던 것을 또 뽑을 수 있도록 한 것이다. 이렇게 샘플링을 반복하여 만들어지는 것이 부트스트랩 세트(Bootstrap Set)이다. 복원 추출을 했기 때문에 각각의 부트스트랩 세트 안에는 같은 샘플이 중복되어 나타날 수 있다.

부트스트랩 세트의 크기가 n이라 할 때 한 번의 추출 과정에서 어떤 한 샘플이 추출되지 않을 확률은 다음과 같다.

$\displaystyle \frac {n-1}{n}$

n회 복원 추출을 진행했을 때 그 샘플이 추출되지 않을 확률은 다음과 같다.

$\displaystyle \left({\frac {n-1}{n}}\right)^{n}$

n을 무한히 크게 했을 때 이 식은 다음과 같다.

$\displaystyle \lim _{{n\to \infty }}\left({1 - \frac {1}{n}}\right)^{n} = e^{-1} = 0.368$
※ 참고 : $\displaystyle e = \lim _{{n\to \infty }}\left(1+{\frac {1}{n}}\right)^{n}$

즉, 데이터가 충분히 크다고 가정했을 때 한 부트스트랩 세트는 표본의 63.2% 에 해당하는 샘플을 가진다.
여기서 추출되지 않는 36.8%의 샘플이 Out-Of-Bag 샘플이며 이것을 사용해 모델을 검증한다.


<참고 자료>

'Machine Learning > Trees' 카테고리의 다른 글

[ML] 결정 트리(Decision Tree)  (0) 2021.10.26

결정 트리(Decision Tree)

결정 트리가 뭐지?

결정 트리(decision tree)는 의사 결정 규칙과 그 결과들을 트리 구조로 도식화한 의사 결정 지원 도구의 일종이다.
- 위키백과 (결정 트리)

결정 트리 학습법(decision tree learning)은 어떤 항목에 대한 관측값과 목표값을 연결시켜주는 예측 모델로서 결정 트리를 사용한다. 이는 통계학과 데이터 마이닝, 기계 학습에서 사용하는 예측 모델링 방법 중 하나이다. 트리 모델 중 목표 변수가 유한한 수의 값을 가지는 것을 분류 트리라 한다. 이 트리 구조에서 잎(리프 노드)은 클래스 라벨을 나타내고 가지는 클래스 라벨과 관련있는 특징들의 논리곱을 나타낸다. 결정 트리 중 목표 변수가 연속하는 값, 일반적으로 실수를 가지는 것은 회귀 트리라 한다.
- 위키백과 (결정 트리 학습법)

결정 트리는 스무고개 같은 것으로 생각하면 간단하다. 어떠한 대상(표본의 특성)에 대하여 첫 질문(뿌리 노드)을 시작으로 여러 질문들(중간 노드, internal node)을 거쳐 마지막 질문(말단 노드, terminal node, leaf)을 끝으로 최종 답(타겟 예측 값)을 낸다.

다만 스무고개는 질문 횟수 20번으로 규칙이 정해져 있지만, 결정 트리는 사용자가 설정하기에 따라 20번보다 더 많을 수도, 적을 수도 있다. 여기서 이 질문 횟수를 결정 트리의 깊이(depth)라고 한다.

이름이 트리(Tree)인 이유는 말 그대로 나무처럼 생겨서 그렇다. 아래와 같이 생겼다.

출처 : 결정 트리 학습법 - 위키백과

위 예시에서 맨 위 '남자인가?'가 결정 트리의 시작점인 뿌리 노드(root node)이다.
빨강('사망')과 초록('생존')색에 아래로 더 이어진 것이 없는 것들은 말단 노드(external node, terminal node, leaf)라고 한다.
그리고 뿌리 노드와 말단 노드 사이에 있는 노드가 중간 노드(internal node)이다.
노드와 노드 사이를 연결하는 선들은 엣지(edge)라고 한다.

결정 트리 학습 알고리즘

선형 모델에서는 MSE, MAE 등을 비용 함수(Cost function)로 하여 이를 최소화한 모델을 만드는 것이 목적이었다.
결정 트리에서도 비용 함수 역할을 하는 것이 있는데, 바로 지니 불순도(Gini impurity)와 엔트로피(Entropy)이다.

  • 지니 불순도
    • '불순도'라는 이름 그대로 불순물이 얼마나 섞여 있는지를 나타낸다. 여기서 불순물과 불순물이 아닌 것의 기준은 그곳에 무엇이 더 많이 있는가이다. 즉, 더 많은 것이 주인이고 적은 쪽이 불순물이다.
    • 지니 불순도를 계산하는 식은 다음과 같다.
      • 지니불순도(Gini Impurity or Gini Index):
        $$I_G(p)=\sum_{i=1}^{J} p_i(1-p_i)=1-\sum _{i=1}^{J}{p_i}^2$$
        여기서 p는 주인이 차지한 영역의 비율(달리 말하면 확률)이다.
    • 만약 불순물이 최대, 즉 반반 치킨처럼 반반이라면 지니 불순도는 0.5이다. 반대로 모든 것이 같은, 즉 순수한 후라이드 또는 양념 치킨이라면 지니 불순도는 0이다.
    • 조금 더 구체적인 예를 들어보겠다. 결정 트리의 어느 노드에 와서 보니 빨간 휴지가 50개, 파란 휴지가 30개라고 한다. 이때 이 노드에서의 지니 불순도는
      $$1 - ( (\frac{50}{50+30})^2 + (\frac{30}{50+30})^2 ) = 0.46875$$ 이다.
  • 엔트로피
    • 엔트로피도 지니 불순도처럼 높으면 불순물이 많고 낮으면 적은 것이다.
    • 지니 불순도와의 차이점으로 엔트로피는 최대 1(반반), 최소 0(순수)이다.
    • 정보 획득(Information Gain) : 결정 트리에서 어떤 특성을 사용해 분할을 했을 때 엔트로피가 감소하는 양을 뜻한다. 예시는 아래와 같다.
      자식 노드의 가중 평균 엔트로피 = $\frac{17}{30}*0.787 + \frac{13}{30}*0.391 = 0.615$
      ∴ IG = 부모 엔트로피 - 자식 노드의 가중 평균 엔트로피 = 0.996 - 0.615 = 0.38

특성 중요도

선형 모델에서는 각 특성별로 타겟에 얼마나 영향을 주는지를 회귀 계수(Coefficient)로 표현하였다. 결정 트리에도 이와 비슷한 역할로 특성 중요도(Feature importance)가 있다.

이름 그대로 타겟 값을 구하는 과정에서 각 특성이 얼마나 중요한지를 나타낸 것으로, 구체적으로는 해당 특성이 얼마나 일찍 그리고 자주 트리의 분기에 사용되는지 결정한다.

회귀 계수와 다른 점으로 특성 중요도는 항상 양수값이다.

결정 트리의 특징

  1. 결정 트리는 회귀 문제와 분류 문제 모두에 대하여 사용 가능하다.
  2. 결정 트리는 분류 과정을 직관적으로 확인이 가능하다.
  3. 선형 모델과 달리 비선형, 비단조$^*$$^1$, 특성 상호 작용$^*$$^2$ 특징을 가진 데이터 분석이 가능하다.

결정 트리는 위와 같은 장점들이 있다.
그러나 훈련 데이터의 일반화가 제대로 되지 않으면 너무 복잡한 결정 트리가 되는 문제가 있는데, 한 마디로 과적합(Overfitting) 문제가 있다.

결정 트리 과적합 문제

scikit-learn의 DecisionTreeClassifier 또는 DecisionTreeRegressor를 이용하면 결정 트리 모델을 만들 수 있다. 이 둘에는 결정 트리의 과적합을 줄일 수 있는 파라미터들로 다음과 같은 것들이 있다.

  • max_depth : 결정 트리의 최대 깊이를 제한한다. 만약 제한을 하지 않을 경우 트리는 모든 말단 노드가 순수(불순도가 0)해질 때가지 분기를 만들 것이고, 이는 과적합 문제를 발생시킬 수 있다.
  • min_samples_split : 노드가 분할하기 위해서 필요한 최소한의 표본 데이터 수를 설정한다. 이를 통해 아주 적은 데이터만 있어도 노드가 분할하는 것을 막아 과적합을 줄일 수 있다. 만약 설정하지 않으면 작은 분기가 많이 만들어져 과적합 문제가 발생할 수도 있다.
  • min_samples_leaf : 말단 노드가 되기 위해서 필요한 최소한의 표본 데이터 수를 설정한다. 설정한 숫자보다 적은 데이터 수를 가진 노드가 생성되는 것을 막아 과적합 문제를 줄인다. 이를 설정하지 않으면 아주 적은 데이터로도 말단 노드가 생성되어 과적합 문제가 생길 수 있다.

결정 트리는 어떻게 하는거지?

위에서 언급한 scikit-learn의 DecisionTreeClassifier를 통해서 결정 트리를 만들어보겠다.

1) 데이터 불러오기 및 전처리

import pandas as pd
from sklearn.model_selection import train_test_split

target = 'vacc_h1n1_f'
train = pd.merge(pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/train.csv'),
pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/train_labels.csv')[target], left_index=True, right_index=True)
test = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/test.csv')
sample_submission = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/submission.csv')
train, val = train_test_split(train, train_size=0.80, test_size=0.20,
stratify=train[target], random_state=2)

train.shape, val.shape, test.shape

((33723, 39), (8431, 39), (28104, 38))

import numpy as np

def  engineer(df):
    """특성을 엔지니어링 하는 함수입니다."""
    # 높은 카디널리티를 가지는 특성을 제거
    selected_cols = df.select_dtypes(include=['number',  'object'])
    labels = selected_cols.nunique()  # 특성별 카디널리티 리스트
    selected_features = labels[labels <= 30].index.tolist()  # 카디널리티가 30보다 작은 특성만 선택합니다.
    df = df[selected_features]

    # 새로운 특성 생성
    behaviorals = [col for col in df.columns if  'behavioral'  in col]
    df['behaviorals'] = df[behaviorals].sum(axis=1)
    dels = [col for col in df.columns if  ('employment'  in col or  'seas'  in col)]
    df.drop(columns=dels, inplace=True)
    return df

train = engineer(train)
val = engineer(val)
test = engineer(test)
features = train.drop(columns=[target]).columns

# 훈련/검증/테스트 데이터를 특성과 타겟으로 분리합니다
X_train = train[features]
y_train = train[target]
X_val = val[features]
y_val = val[target]
X_test = test[features]

2) 결정 트리 구현

from sklearn.tree import DecisionTreeClassifier  

pipe = make_pipeline(
    OneHotEncoder(use_cat_names=True),
    SimpleImputer(),
    DecisionTreeClassifier(random_state=1, criterion='entropy')
)  

pipe.fit(X_train, y_train)
print('훈련 정확도: ', pipe.score(X_train, y_train))
print('검증 정확도: ', pipe.score(X_val, y_val))

훈련 정확도: 0.9908667674880646
검증 정확도: 0.7572055509429486

y_val.value_counts(normalize=True)

0 0.761001
1 0.238999
Name: vacc_h1n1_f, dtype: float64

∴ 결정 트리의 훈련 정확도는 99가 넘는데 검증 정확도는 타겟의 다수 범주(0) 비율과 거의 똑같다. 즉, 과적합 상태이다.

3) 과적합 해결하기

# max_depth 설정을 통해 과적합 줄이기
pipe = make_pipeline(
    OneHotEncoder(use_cat_names=True),
    SimpleImputer(),
    DecisionTreeClassifier(max_depth=6, random_state=2)
)  

pipe.fit(X_train, y_train)
print('훈련 정확도', pipe.score(X_train, y_train))
print('검증 정확도', pipe.score(X_val, y_val))

훈련 정확도 0.8283367434688491
검증 정확도 0.8269481674771676

# min_samples_leaf 설정을 통해 과적합 줄이기
pipe = make_pipeline(
OneHotEncoder(use_cat_names=True),
SimpleImputer(),
DecisionTreeClassifier(min_samples_leaf=10, random_state=2)
)  

pipe.fit(X_train, y_train)
print('훈련 정확도', pipe.score(X_train, y_train))
print('검증 정확도', pipe.score(X_val, y_val))

훈련 정확도 0.8577528689618361
검증 정확도 0.8029889692800379

∴ 과적합을 줄이기 위한 파라미터를 넣지 않았을 때와 비교하면 훈련 정확도는 낮아졌지만 검증 정확도는 높아졌다. 이전보다 과적합이 줄어들었다.
위 예시에는 담지 않았지만, min_samples_split 또한 비슷한 결과를 얻을 수 있다.

4) 특성 중요도 구하기

# 특성 중요도가 높은 순서대로 그래프에 표시하기
model_dt = pipe.named_steps['decisiontreeclassifier']
enc = pipe.named_steps['onehotencoder']
encoded_columns = enc.transform(X_val).columns

# DecisionTreeClassifier의 feature_importances_ 속성을 통해 특성 중요도를 알 수 있음
importances = pd.Series(model_dt.feature_importances_, encoded_columns)
plt.figure(figsize=(10,30))
importances.sort_values().plot.barh();

지면 관계상 일부 생략

*1 비단조(non-monotonic)란?

'단조(monotonic)'라는 말의 뜻을 알면 비단조는 바로 알 수 있다.
보통 일상 생활에서 '단조(單調)롭다'라는 말에서의 '단조'와 같은 '단조(單調)'이다.

수학적인 의미의 '단조'는 간단하게 말하면 계속해서 증가 혹은 감소하는 상태를 말한다. 중간에 올라가다가 내려가거나, 내려가다가 올라가지 않고 그냥 꾸준히 계속해서 상승 또는 하강하는 것이다.

*2 특성 상호 작용?
특성 상호 작용이란 특성 둘 이상이 만나면 타겟에 각자가 갖고 있던 영향만 끼치는 것이 아니라 여기에 추가적인 영향이 생기는 것으로 이해할 수 있다.
선형 회귀 모델의 경우 이러한 특성 상호 작용이 있으면 모델 성능이 떨어질 수 있으나, 결정 트리는 그렇지 않다.
구체적인 예시를 보겠다.

  • 기본가격 150,000

Location

  • good: +50,000
  • bad: 0

Size

  • big: +100,000
  • small: 0
# 예시 데이터 준비
> cols = ['location','size','price']
# location: 1:good, 0:bad
# size: 1:big, 0:small
# big은 small보다 100,000 비싸고, good은 bad보다 50,000 가격이 더 나갑니다.

features = [[1,  1],
[1,  0],
[0,  1],
[0,  0]]

price = [[300000],
[200000],
[250000],
[150000]]

X_house = pd.DataFrame(columns=cols[:2], data=features)
y_house = pd.DataFrame(columns=[cols[2]], data=price)
# 선형 회귀 모델
from sklearn.linear_model import LinearRegression

linear = LinearRegression()
linear.fit(X_house, y_house)
print('R2: ', linear.score(X_house, y_house))
print('Intercept: ', linear.intercept_[0])
print('Coefficients')
pd.DataFrame(columns=cols[:2], data=linear.coef_)

R2: 1.0
Intercept: 150000.0
Coefficients

  location size
0 50000.0 100000.0
# 회귀 트리 모델
import graphviz
## jupyterlab 사용시: jupyter labextension install @jupyter-widgets/jupyterlab-manager
from ipywidgets import interact
from sklearn.tree import DecisionTreeRegressor, export_graphviz

# 트리구조 그리는 함수
def  show_tree(tree, colnames):
    dot = export_graphviz(tree, feature_names=colnames, filled=True, rounded=True)
    return graphviz.Source(dot)
from sklearn.tree import DecisionTreeRegressor

tree = DecisionTreeRegressor(criterion="mae")
tree.fit(X_house, y_house)
print('R2', tree.score(X_house, y_house))
show_tree(tree, colnames=X_house.columns)

R2 1.0

good and big 인 경우 +100,000 규칙 추가(특성상호작용)

y_house.loc[0,  'price'] = 400000
y_house
  price
0 400000
1 200000
2 250000
3 150000
# 선형 회귀 모델
# 특성 상호 작용이 있는 경우 $R^2$ 점수가 떨어짐(성능 감소)
linear = LinearRegression()
linear.fit(X_house, y_house)
print('R2: ', linear.score(X_house, y_house))
print('Intercept: ', linear.intercept_[0])
print('Coefficients')
pd.DataFrame(columns=cols[:2], data=linear.coef_)

R2: 0.9285714285714286
Intercept: 125000.00000000003
Coefficients

  location size
0 100000.0 150000.0
# 회귀 트리 모델
# 특성 상호 작용이 있어도 트리 모델의 성능은 그대로임
tree = DecisionTreeRegressor(criterion="mae")
tree.fit(X_house, y_house)
print('R2', tree.score(X_house, y_house))
show_tree(tree, colnames=X_house.columns)

R2 1.0


<참고 자료>

'Machine Learning > Trees' 카테고리의 다른 글

[ML] 랜덤 포레스트(Random Forest)  (0) 2021.10.26

회귀 모델 평가 지표(Evaluation Metrics for Regression Models)

회귀 모델 평가 지표란?

Evaluation metrics are a measure of how good a model performs and how well it approximates the relationship
- towards data science

회귀 모델 평가 지표는 이 모델의 성능이 얼마나 좋은지, 모델에 사용된 특성과 타겟의 관계를 얼마나 잘 나타내는지 보여주는 값이다.

회귀 모델의 평가 지표에는 다음과 같은 것들이 있다.

  • MSE(Mean Squared Error, 평균 제곱 오차)
  • RMSE(Root MSE, 평균 제곱근 오차)
  • MAE(Mean Average Error, 평균 절대 오차)
  • $R^2$(R-squared, Coefficient of determination, 결정 계수)

MSE(Mean Squared Error, 평균 제곱 오차)

회귀 분석을 통해 얻은 예측 값과 실제 값의 차이(잔차)의 평균을 나타낸 것이다. ($y$ : 실제 값, $\hat{y}$ : 예측 값)

$$MSE = \frac {1}{n}\sum_{i=1}^{n}(y_i - \hat{y_i})^2$$

예측 값과 실제 값의 차이를 나타낸 것이므로 작으면 작을수록 좋다.

위 식에서 $(y - \hat{y})^2$를 SSE(Sum of Squares Error, 오차 제곱의 합)라고 하는데, 이는 데이터의 개수가 늘어나면 계속해서 오류의 값이 증가하는 문제가 있다. 이 문제를 해결하기 위해 평균으로 만든 것이 MSE이다.

MSE는 값 하나를 통해 모델의 성능을 파악할 수 있어 직관적이다.
동시에 MSE의 단점으로 아래와 같이 몇 가지가 있는데,

  1. 제곱을 했기 때문에 기존 값의 단위와 다른 단위를 갖는다(쉽게 말하면 값이 뻥튀기되었다).
  2. 이상치(Outlier)가 있는 경우 결과가 왜곡될 수 있다(1번과 동일 이유).
  3. 제곱을 하면 결과는 양수가 되기 때문에 예측 결과가 실제 값보다 높은 것인지, 낮은 것인지 알 수 없다.
  4. 값의 스케일에 의존적이다.$^*$$^1$

RMSE(Root MSE, 평균 제곱근 오차)

위에서 설명한 MSE의 값의 루트를 씌운 것이다.
MSE의 단점 중 제곱을 했기 때문에 발생한 단점을 보완하기 위해서 만들어졌다.

$$RMSE = \sqrt{\frac {1}{n}\sum_{i=1}^{n}(y_i - \hat{y_i})^2}$$

각각의 잔차에 루트를 씌우고서 더한 것이 아니라 전부 다 더한 값(MSE)에 루트를 씌웠기 때문에 제곱에 의한 왜곡이 여전히 존재하나, MSE보다는 덜하다.


MAE(Mean Average Error, 평균 절대 오차)

잔차에 절대값을 씌운 후 평균으로 만든 것이다.

$$MSE = \frac {1}{n}\sum_{i=1}^{n}|y_i - \hat{y_i}|$$

MSE나 RMSE와 마찬가지로 값 하나만 보고 모델의 성능을 직관적으로 파악할 수 있다.
또한 각각의 잔차에 단순히 절대값만 씌운 것의 평균이기에 기존 값의 단위를 그대로 유지하며, MSE에 비해 이상치에 의한 왜곡이 덜 하다는 장점이 있다.

하지만 절대값을 씌웠기 때문에 결과 값이 항상 양수이고, 이는 MSE와 마찬가지로 예측 결과가 실제보다 높은지 낮은지 알 수 없다.


$R^2$(R-squared, Coefficient of determination, 결정 계수)

$R^2$(이하 결정 계수)는 모델이 얼마나 예측을 잘 했는지를 0에서 1 사이로 나타낸 값이다. 1에 가까울수록 성능이 좋은 것이다.

$$1 - \frac{\sum_{i=1}^{n}(y_{i} - \hat{y_{i}})^{2}}{\sum_{i=1}^{n}(y_{i} - \bar{y_{i}})^{2}} = 1 - \frac{SSE}{SST} = \frac {SSR}{SST}$$

조금 더 구체적으로 말하자면, 실제 값의 분산과 예측 값의 분산을 비교하여 예측이 얼마나 잘 맞았는지 보는 것이다. 만약 결정 계수의 값이 0.7이라면 이는 모델이 실제 값의 분산을 70% 설명한다는 의미이다.

위의 MSE, RMSE, MAE와 달리 결정 계수는 값의 단위가 원래 무엇이었든 0부터 1 사이의 값으로 표현하기에 스케일에 의존적이지 않다.
또한 값 하나로 모델의 성능을 알아볼 수 있어 직관적이다.

그러나 과적합(Overfitting) 문제에 약하다는 단점이 있다. 특성이 많아질수록, 즉 모델이 복잡해질수록 모델은 훈련 데이터는 잘 맞추지만 테스트는 잘 망치는, 즉 과적합되는 문제가 있다. 결정 계수는 그저 자신의 값을 보여줄 뿐, 모델이 과적합인지 아닌지는 말해주지 않는다.
이러한 과적합 문제를 보완하기 위해서 Adjusted R-squared(수정된 결정 계수)가 있다.

※ 참고
- SSE(Sum of Squares Error, 관측치와 예측치 차이): $\sum_{i=1}^{n}(y_{i} - \hat{y_{i}})^{2}$
- SSR(Sum of Squares due to Regression, 예측치와 평균 차이): $\sum_{i=1}^{n}(\hat{y_{i}} - \bar{y_{i}})^{2}$
- SST(Sum of Squares Total, 관측치와 평균 차이): $\sum_{i=1}^{n}(y_{i} - \bar{y_{i}})^{2}$ , SSE + SSR

*1 스케일에 의존적이라는게 무슨 말?
예를 들어서 삼성전자와 애플의 주식 가격을 예측한다고 해보자.
여기서 한국 주식의 가격은 원화, 미국 주식의 가격은 달러이다(작성 시각 기준 삼성전자 70,200원, 애플 149.28달러).
이 둘에 대한 회귀 모델을 만들었고, MSE 값을 구했더니 둘 다 30이 나왔다고 해보자.
가격의 단위가 판이하게 다름에도 에러 값이 똑같이 나왔다.
그렇다고 해서 이 둘이 동등한 수준의 성능을 보여주는 것은 아니다.

이렇게 예측하고자 하는 값의 단위에 영향을 받는 것을 두고 스케일에 의존적이라고 한다.


<참고 자료>

다중 선형 회귀 모델(Multiple Linear Regression)

※ 단순 선형 회귀를 모른다면? (아래 클릭 시 링크로 이동)
단순 선형 회귀 모델(Simple Linear Regression Model)

다중 선형 회귀가 뭐지?

단순 선형 회귀와 다중 선형 회귀
선형 회귀의 가장 단순한 예제는 한 개의 스칼라 독립 변수 x와 한 개의 스칼라 의존 변수 y의 관계일 것이다. 이를 단순 선형 회귀라 부른다. 여기에서 독립 변수를 여러 개로 확장한 것이 다중 선형 회귀이다.

실세계의 거의 대부분의 문제는 여러 개의 독립 변수를 포함하며, 선형 회귀라 함은 보통 다중 선형 회귀를 일컫는다. 하지만 이러한 경우에도 여전히 응답 변수 y는 한 개의 스칼라 변수이다. 다변량 선형 회귀는 응답 변수 y가 벡터인 경우를 의미한다. 이러한 경우를 일반 선형 회귀라 부른다. 다중 선형 회귀와 다변량 선형 회귀는 다른 의미이므로, 혼동하지 않도록 주의해야 한다.
- 위키백과 (선형 회귀 中)

영향을 받은 결과와 영향을 준 요인의 개수를 '결과 : 요인'이라고 표현하면 단순 선형 회귀는 1:1이고 다중 선형 회귀는 1:N이다.
여기서 이 결과가 종속 변수(또는 타겟)이고, 요인을 독립 변수(특성)라고 한다.

선형 회귀에서의 가정이나 최소제곱법을 이용하여 결과값을 구하는 등의 특징은 단순 선형 회귀와 공유한다.
다만 다중 선형 회귀에서는 특성의 개수가 2개 이상이므로 다중공선성(Multicollinearity)$^*$$^1$을 주의해야 한다.


다중 선형 회귀는 어떻게 하는거지?

기본적인 원리는 단순 선형 회귀와 비슷하다.
다만 차이점이라면 특성의 개수가 2개 이상이므로 회귀 방정식(단순 선형 회귀에서는 직선을 나타내는 식)에 차이가 있다.

단순 선형 회귀에서의 회귀 방정식은 $$y=β_0+β_1x$$ 이고, 다중 선형 회귀에서의 회귀 방정식은 $$y=β_0+β_1x_1+β_2x_2+ ... +β_nx_n$$ 이다.
(자세한 계산 과정은 하단 참고 자료의 링크로)


scikit-learn을 활용한 다중 선형 회귀

  1. 데이터 불러오기 & 훈련/테스트 데이터 나누기
# 예시 데이터 불러오기 (House Sales in King County, USA)
import pandas as pd

df = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/kc_house_data/kc_house_data.csv')
df.date = pd.to_datetime(df.date) # 날짜 형식으로 변환

df.head()
  id date price bedrooms bathrooms sqft_living sqft_lot floors waterfront view condition grade sqft_above sqft_basement yr_built yr_renovated zipcode lat long sqft_living15 sqft_lot15
0 7129300520 2014-10-13 221900.0 3 1.00 1180 5650 1.0 0 0 3 7 1180 0 1955 0 98178 47.5112 -122.257 1340 5650
1 6414100192 2014-12-09 538000.0 3 2.25 2570 7242 2.0 0 0 3 7 2170 400 1951 1991 98125 47.7210 -122.319 1690 7639
2 5631500400 2015-02-25 180000.0 2 1.00 770 10000 1.0 0 0 3 6 770 0 1933 0 98028 47.7379 -122.233 2720 8062
3 2487200875 2014-12-09 604000.0 4 3.00 1960 5000 1.0 0 0 5 7 1050 910 1965 0 98136 47.5208 -122.393 1360 5000
4 1954400510 2015-02-18 510000.0 3 2.00 1680 8080 1.0 0 0 3 8 1680 0 1987 0 98074 47.6168 -122.045 1800 7503
# 2015-01-01을 기준으로 훈련/테스트 데이터 분리
train = df[df.date < '2015-01-01']
test = df[df.date >= '2015-01-01']

  1. 타겟 및 기준 모델 설정
# 타겟 설정
target  =  'price'  
y_train  =  train[target]  
y_test  =  test[target]
# price 평균값으로 예측(기준모델)
predict = y_train.mean()
predict

539181.4284152258

# 기준모델로 훈련 에러(MAE) 계산
from sklearn.metrics import mean_absolute_error

y_pred = [predict] * len(y_train)
mae = mean_absolute_error(y_train, y_pred)

print(f'훈련 에러: {mae:.2f}')

훈련 에러: 233570.83

# 테스트 에러(MAE)
y_pred = [predict] * len(y_test)
mae = mean_absolute_error(y_test, y_pred)

print(f'테스트 에러: {mae:.2f}')

테스트 에러: 233990.69


  1. 다중 선형 회귀 모델 만들기
# 다중모델 학습을 위한 특성
features = ['bathrooms', 'sqft_living']
X_train = train[features]
X_test = test[features]
# 모델 fit
from sklearn.linear_model import LinearRegression  

model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_train)
mae = mean_absolute_error(y_train, y_pred)

print(f'훈련 에러: {mae:.2f}')

훈련 에러: 170777.34

# 테스트 데이터에 적용
y_pred = model.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)

print(f'테스트 에러: {mae:.2f}')

테스트 에러: 179252.53


  1. 절편과 회귀계수 확인
# 절편(intercept)과 계수들(coefficients)
model.intercept_, model.coef_

(-50243.56279640319, array([-5158.92591411, 286.13753555]))
∵ 특성이 2개이기 때문에 회귀 계수도 2개

## 회귀식
b0 = model.intercept_
b1, b2 = model.coef_  

print(f'y = {b0:.0f} + {b1:.0f}x\u2081 + {b2:.0f}x\u2082')

y = -50244 + -5159$x_1$ + 286$x_2$


  1. 훈련/테스트 데이터에 대한 평가 지표 결과 확인
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score  

def  evaluate(features, model, train, test):
    y_train = train[target]
    y_test = test[target]
    X_train = train[features]
    X_test = test[features]
    y_pred_train = model.predict(X_train)
    y_pred_test = model.predict(X_test)  

    mse = mean_squared_error(y_train, y_pred_train)
    rmse = mse ** 0.5
    mae = mean_absolute_error(y_train, y_pred_train)
    r2 = r2_score(y_train, y_pred_train)  

    mse_t = mean_squared_error(y_test, y_pred_test)
    rmse_t = mse_t ** 0.5
    mae_t = mean_absolute_error(y_test, y_pred_test)
    r2_t = r2_score(y_test, y_pred_test)  

    display(pd.DataFrame([['MSE', mse, mse_t],['RMSE', rmse, rmse_t],['MAE', mae, mae_t],['R2', r2, r2_t]], columns=['Metric',  'Score(Training)',  'Score(Test)']))
evaluate(features, model, train, test)
  Metric Score(Training) Score(Test)
0 MSE 6.709905e+10 7.108399e+10
1 RMSE 2.590348e+05 2.666158e+05
2 MAE 1.707773e+05 1.792525e+05
3 R2 5.076086e-01 4.599930e-01

 


∗1 다중공선성(Multicollinearity)이란?
다중공선성(多重共線性)문제(Multicollinearity)는 통계학의 회귀분석에서 독립변수들 간에 강한 상관관계가 나타나는 문제이다. 독립변수들간에 정확한 선형관계가 존재하는 완전공선성의 경우와 독립변수들간에 높은 선형관계가 존재하는 다중공선성으로 구분하기도 한다. 이는 회귀분석의 전제 가정을 위배하는 것이므로 적절한 회귀분석을 위해 해결해야 하는 문제가 된다.
- 위키백과

특성(독립 변수)들끼리 강한 상관 관계가 있는 경우를 의미한다.
만약 이를 배제하지 않고 분석에 그대로 사용하게 되면 결과가 왜곡될 위험이 크다.

<참고 자료>
[통계 이론] 선형 회귀 : 다중 회귀 분석
머신러닝 - 다중선형회귀(Multiple Linear Regression)

선형 회귀(Linear Regression Model)

선형 회귀가 뭐지?

통계학에서, 선형 회귀(線型回歸, 영어: linear regression)는 종속 변수 y와 한 개 이상의 독립 변수 (또는 설명 변수) X와의 선형 상관 관계를 모델링하는 회귀분석 기법이다. 한 개의 설명 변수에 기반한 경우에는 단순 선형 회귀(simple linear regression), 둘 이상의 설명 변수에 기반한 경우에는 다중 선형 회귀라고 한다.

선형 회귀는 선형 예측 함수를 사용해 회귀식을 모델링하며, 알려지지 않은 파라미터는 데이터로부터 추정한다. 이렇게 만들어진 회귀식을 선형 모델이라고 한다.

일반적으로 최소제곱법(least square method)을 사용해 선형 회귀 모델을 세운다. 최소제곱법 외에 다른 기법으로도 선형 회귀 모델을 세울 수 있다. 손실 함수(loss fuction)를 최소화 하는 방식으로 선형 회귀 모델을 세울 수도 있다. 최소제곱법은 선형 회귀 모델 뿐 아니라, 비선형 회귀 모델에도 적용할 수 있다. 최소제곱법과 선형 회귀는 가깝게 연관되어 있지만, 그렇다고 해서 동의어는 아니다.
- 위키백과

선형 회귀란 쉽게 생각하면 어떠한 현상 A와 이와 관련되어 보이는 한 개 이상의 현상 B가 있을 때, B가 변화함에 따라 A가 어떻게 변하는지 도형(직선, 평면 등) 하나를 통해 나타낸 것이다. 여기서 B가 1개라면 단순 선형 회귀(Simple Linear Regression), 2개 이상이면 다중 선형 회귀(Multiple Linear Regression)라고 한다.


선형 회귀에서의 가정(Assumptions of Linear Regression)

선형 회귀가 성립되기 위한 가정에는 다음 4가지가 있다.

  1. 선형성(Linearity) : 독립 변수와 종속 변수의 관계는 선형적이어야 한다. 즉, 그래프로 표현했을 때 끊어지지 않고 쭈욱 이어지는 선으로 표현 가능해야 한다.
  2. 정규성(Normality) : 잔차(종속 변수에 대한 예측 값과 실제 값의 차이)$^*$$^1$가 정규 분포여야 한다.
  3. 등분산성(Homoscedasticity, Constant Variance) : 독립 변수의 모든 값에 대하여 잔차의 분산이 같아야 한다.
  4. 독립성(Independence) : 잔차는 서로 독립적이어야 한다.

단순 선형 회귀(Simple Linear Regression)

위에서 간단하게 언급한 바와 같이 단순 선형 회귀는 독립 변수 1개와 종속 변수 1개의 관계를 직선으로 나타낸 것이다.
이를 통해 독립 변수의 값에 따라 이에 해당하는 종속 변수의 값을 구할 수 있다. 즉, 기존의 데이터를 바탕으로 실제 데이터에는 존재하지 않는 값을 예측할 수 있다.

예를 들면 어느 중학교 학생들의 공부 시간과 시험 성적을 조사한 결과가 아래와 같다고 해보자.

학생 공부 시간 시험 성적
A 3 30
B 5 50
C 2 20
D 6 60
E 4 40

이를 그래프로 나타내면 아래처럼 보일 것이다.

그리고 이 그래프 위의 점들에 최대한 가깝게 지나가는 직선을 그리면 아래와 같다.

실제 데이터에는 공부 시간이 2, 3, 4, 5, 6 뿐이다. 여기서 만약 8시간 공부한 학생의 시험 성적이 궁금하다면 x값(공부 시간)이 8일 때 직선 상의 y값(시험 성적)이 무엇인지 알아보면 된다.

굳이 별도의 계산이나 그래프가 필요할 만한 예시는 아니었지만, 단순 선형 회귀를 통해 예측 값을 얻는 과정이 이와 같다.

그렇다면 실제로 저 직선은 어떻게 구하는 것일까?


최소제곱법(Least Squares Method = Ordinary Least Squares, OLS)

최소제곱법이란 예측 값과 실제 값의 차이인 잔차들의 제곱의 합을 최소화하는 직선을 찾아내는 방법이다. 이 직선을 최소 제곱 직선이라고 한다.

※ 왜 제곱을 하지?

제곱하는 이유는 실제 값이 예측 값보다 클 수도, 작을 수도 있기 때문이다.
제곱을 안 하고서 그냥 합하면 양수와 음수가 서로 상쇄되어 결과가 왜곡될 수 있다(분산 구할 때 제곱해주던 것과 마찬가지).

<예시>
아래와 같이 x와 y가 있다고 해보자.

x y
1 2
2 4
3 5
4 4
5 5
평균 3 평균 4

이를 그래프로 나타내면 아래와 같다.

여기서 직선 $\hat{y} = \beta_0 + \beta_1x$ 가 예측 값을 얻는데 쓰일 최소 제곱 직선(다른 말로는 선형 회귀선)이다.

직선 $\hat{y} = \beta_0 + \beta_1x$는 $x$와 $y$의 관계를 나타낸다($\hat{y}$는 $y$의 예측값(추정치)라는 의미).
$x$와 $y$의 관계를 최대한 잘 나타내려면 각각의 $x$값에 대하여 실제 $y$값에 최대한 가까운 예측 값을 가져야 한다.
즉, 직선 상의 $y$값(= $\hat{y}$, 예측 값)과 점으로 표시된 $y$값(실제 값)의 차이(잔차)를 최소화해야 한다.

그럼 잔차를 최소화하는 직선 $\hat{y} = \beta_0 + \beta_1x$의 기울기와 $y$ 절편을 구해보자.


1) 기울기($\beta_1$) 구하기

직선의 기울기를 구하는 식은 다음과 같다.
$$\beta_1 = \frac{\sum_{i=1}^{n} (x_i - \bar{x})(y_i - \bar{y})} {\sum_{i=1}^{n} (x_i - \bar{x})^2}$$
아래 테이블에 계산되어 있는 값을 식에 그대로 넣으면 $\beta_1 = \frac{6}{10} = 0.6$ 이다.

$x$ $y$ $x - \bar{x}$ $y - \bar{y}$ $(x - \bar{x})^2$ $(x - \bar{x})(y - \bar{y})$
1 2 -2 -2 4 4
2 4 -1 0 1 0
3 5 0 1 0 0
4 4 1 0 1 0
5 5 2 1 4 2
평균 3 평균 4     합계 10 합계 6

2) $y$ 절편($\beta_0$) 구하기

이제 기울기를 알고 있으니 $y$ 절편을 구하는건 간단하다.
직선 $\hat{y} = \beta_0 + 0.6x$에 ($\bar{x}, \bar{y}$)를 넣어주면 된다.
$∴ \beta_0 = \bar{y} - 0.6\bar{x} = 4 - 0.6*3 = 2.2$

※ $\beta_1$을 구하는 식은 왜 저렇게 되고, $\beta_0$는 왜 저렇게 구할까? (참고 자료)
최소제곱법 - 네이버캐스트 수학 산책
최소 제곱법 (Least Square Method = OLS)
#선형회귀 의 기초인 #최소제곱법 ? 공식 유도를 해보자~! (영상)


기준 모델(Baseline Model)

위처럼 예측 값을 잘 뽑아줄 직선을 구했으니 무언가 할 일은 다 한 것 같다.
그러나 사실 그 전에 해야 할 일이 있었으니, 바로 기준 모델을 세우는 것이다.

기준 모델이란 예측 모델(위에서 만든 직선 식)을 구체적으로 만들기 전에 가장 간단하면서도 직관적이면서 최소한의 성능을 나타내는 기준이 되는 모델이다.
쉽게 말하면 예측 모델을 뚝딱 만들고 봤을 때 최소한 이 기준 모델이라는 친구보다는 일을 잘 해야 한다는 것이다.

기준 모델은 문제의 유형에 따라 보통 다음과 같이 설정한다.

  • 분류 문제: 타겟의 최빈 클래스
  • 회귀 문제: 타겟의 평균값 -> 평균 절대 오차(MAE, Mean Absolute Error)로 평가
  • 시계열 회귀 문제: 이전 타임 스탬프의 값

 


scikit-learn 활용하여 단순 선형 회귀 모델 만들기

  1. 데이터 불러오기
# 예시 데이터 불러오기 (House Sales in King County, USA)
import pandas as pd
df = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/kc_house_data/kc_house_data.csv')

  1. 타겟과 이와 관련된 특성 찾아서 정하기
# 여기서는 타겟(price)과 가장 상관 관계가 높은 특성을 찾겠습니다
df.corr().price.sort_values(ascending=False)

price 1.000000
sqft_living 0.702035
grade 0.667434
sqft_above 0.605567
sqft_living15 0.585379
bathrooms 0.525138
view 0.397293
sqft_basement 0.323816
bedrooms 0.308350
lat 0.307003
waterfront 0.266369
floors 0.256794
yr_renovated 0.126434
sqft_lot 0.089661
sqft_lot15 0.082447
yr_built 0.054012
condition 0.036362
long 0.021626
id -0.016762
zipcode -0.053203
Name: price, dtype: float64

∴ 타겟과 상관 관계가 가장 높은 특성의 이름 : sqft_living


  1. 기준 모델 만들기 및 평가
predict = df.price.mean() # 기준 모델 : 타겟(price)의 평균값
print(int(predict))

sns.scatterplot(x=df.sqft_living, y=df.price)
sns.lineplot(x=df.sqft_living, y=predict, color='red'); # 기준 모델 시각화

 

# 평균값으로 예측할 때 샘플 별 평균값과의 차이(error)를 저장
errors = predict - df.price
errors

0 318188.141767
1 2088.141767
2 360088.141767
3 -63911.858233
4 30088.141767
...
21608 180088.141767
21609 140088.141767
21610 137987.141767
21611 140088.141767
21612 215088.141767
Name: price, Length: 21613, dtype: float64

# mean_absolute_error(MAE), error에 절대값을 취한 후 평균을 계산
mean_absolute_error = errors.abs().mean()
print(f'예측한 주택 가격이 ${predict:,.0f}이며 절대 평균 에러가 ${mean_absolute_error:,.0f}임을 확인할 수 있습니다.')

예측한 주택 가격이 $540,088이며 절대 평균 에러가 $233,942임을 확인할 수 있습니다.


  1. 예측 모델(선형 회귀 모델) 만들기
from sklearn.linear_model import LinearRegression

model = LinearRegression()
feature = ['sqft_living']
target = ['price']
X_train = df[feature]
y_train = df[target]
model.fit(X_train, y_train)

LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None, normalize=False)

# 예측을 잘 하는지 값 하나 던져서 테스트 해보기
X_test = [[15000]]
y_pred = model.predict(X_test)

print(f'{X_test[0][0]} sqft GrLivArea를 가지는 주택의 예상 가격은 ${round(float(y_pred))} 입니다.')

15000 sqft GrLivArea를 가지는 주택의 예상 가격은 $4165773 입니다.


  1. 예측 값과 실제 값을 그래프로 표현
X_test  =  [[x]  for  x  in  df['sqft_living']]  
y_pred  =  model.predict(X_test)

y_pred

array([[287555.06702451],
[677621.82640197],
[172499.40418656],
...,
[242655.29616092],
[405416.96554144],
[242655.29616092]])

plt.scatter(X_train, y_train, color='black', linewidth=1)
plt.scatter(X_test, y_pred, color='blue', linewidth=1);

 


  1. 예측 모델의 회귀 계수(직선의 기울기)와 y 절편 확인
print(model.coef_) # 회귀 계수
print(model.intercept_) # y 절편

[[280.6235679]]
[-43580.74309447]

$*1$ 오차(error)와 잔차(residual)의 차이가 뭐지?
오차 : 모집단이 기준
잔차 : 표본 집단이 기준
실제 데이터를 토대로 선형 회귀 등의 방식으로 회귀식을 구하면 예측 값을 얻을 수 있다. 여기서 실제 데이터가 모집단인지, 표본 집단인지에 따라 실제 값과 예측 값의 차이를 각각 오차와 잔차라고 한다.

 

<참고 자료>

● 선형 회귀

● 선형 회귀에서의 가정

● 최소제곱법

● 기준 모델

● 오차와 잔차

● 시각화

+ Recent posts