728x90
반응형
SMALL
728x90
반응형
SMALL

이번 블로그는 선형 모델에 대하여 알아보도록 합시다.

선형 모델은 매우 오래전 개발된 모델입니다. 선형 모델은 입력 특성에 대한 선형 함수를 만들어 예측을 수행합니다. 먼저 회귀의 선형 모델부터 알아보겠습니다

회귀의 선형 모델을 위한 일반화된 예측 함수는 다음과 같습니다.

𝑦̂ =𝑤[0]∗𝑥[0]+𝑤[1]∗𝑥[1]+...+𝑤[𝑝]∗𝑥[𝑝]+𝑏

위의 식에서 𝑥[0]부터 𝑥[𝑝]까지는 하나의 데이터 포인트에 대한 특성을 나타내고, (특성의 수는 𝑝+1 )와 𝑏는 모델이 학습해야 할 파라미터입니다. 𝑦̂ 는 모델이 만들어낸 예측값입니다.

그렇다면 특성이 한 개인 예측 함수는 어떻게 될까요? 다음과 같습니다.

𝑦̂ =𝑤[0]∗𝑥[0]+𝑏

위의 함수를 보면 예전 수학시간에 배운 직선의 방정식이 떠오릅니다. 𝑤[0]은 직선의 기울기( 계수(cofficient)라고도 합니다 )가 되고, 𝑏는 y 축과 만나는 절편( offset 또는 intercept )이 됩니다.

특성이 많아질수록 𝑤는 각 특성에 해당하는 기울기를 모두 가지게 됩니다. 즉 예측값은 입력특성에 𝑤의 가중치를 모두 더한 가중치의 합으로 볼 수 있겠습니다.

머신러닝에서 알고리즘이 주어진 데이터로부터 학습하는 파라미터라서 𝑤와 𝑏를 모델 파라미터라고 합니다.

나중에 살펴보겠지만 모델이 학습할 수 없어 사람이 직접 설정해 줘야 하는 파라미터를 하이퍼 파라미터(Hyper Parameter)라고 합니다.


wave 데이터셋을 이용해 눈으로 확인해 보겠습니다.

 

#필요 라이브러리 임포트
from IPython.display import display
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import mglearn
plt.rcParams['axes.unicode_minus'] = False

import platform
path = 'c:/Windows/Fonts/malgun.ttf'
from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
    rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc('font', family=font_name)
else:
    print('Unknown system... sorry!')




# mglearn 샘플 그래프 보기
mglearn.plots.plot_linear_regression_wave()

 

그려진 그래프를 보면 직선의 방정식이 생각나는데요, 기울기는 대략 0.4, 절편은 0에 가깝게 나타나고 있는 것이 확인됩니다.

회귀 모델을 위한 선형 모델은 특성이 하나인 경우에는 직선, 두 개면 평면이 되며, 세 개 이상( 더 높은 차원 )이 되면 초평면(Hyperplane)이 되는 회귀 모델의 특징을 가지고 있습니다.

위의 그래프와 이전에 했었던 KNeighborsRegressor를 사용하여 만든 그래프를 비교해 보면 어떤가요? 직선을 지나는 점이 얼마 없는 것으로 보아 데이터의 상세정보를 대부분 잃어버린 것처럼 보이지 않나요?

사실 wave 같은 저 차원 데이터 셋에서는 타깃의 𝑦의 특성이 선형 조합이라고 생각하는 것은 매우 비현실 적인 가정입니다. 직선을 따라 데이터가 쭉 연결되지는 않으니까요.

하지만 1차원 데이터셋만 놓고 봐서 생긴 우리의 편견 일 수도 있습니다. 특성이 많은 데이터 셋이라면 선형 모델은 매우 훌륭한 성능을 발휘하며, 훈련 데이터보다 특성이 더 많은 경우엔 어떤 타깃 𝑦도 완벽하게 훈련 세트에 대해서 선형 함수로 모델링할 수 있습니다.

 

회귀를 위한 선형 모델은 다양합니다.

인기 있는 몇 가지 선형 모델을 살펴보려고 합니다. 이 모델들은 훈련 데이터로부터 모델 파라미터 𝑤와 𝑏학습하는 방법모델의 복잡도를 제어하는 방법에서 차이가 납니다.

선형 회귀 Linear Regression (최소제곱법)

선형 회귀(Linear Regression) 또는 최소 제곱법(OLS, Ordinary Least Squares)은 가장 간단하고 오래된 회귀용 선형 알고리즘이라고 할 수 있습니다.

선형 회귀는 훈련 세트에 있는 타깃 y 사이에 평균 제곱 오차(MSE - Mean Squared Error)를 최소화하는 모델 파라미터 𝑤와 𝑏를 찾습니다. 참고로 MSE는 다음과 같습니다.

 

위의 수식의 𝑛은 샘플의 개수입니다. 이를 𝐿2 norm을 적용했다라고 이야기 합니다.

위의 수식이 조금 불편 하긴 하지만 사실 MSE는 모델이 예측한 예측값( 𝑦𝑖^ )과 훈련 세트로 인해 훈련된 값 타깃값( 𝑦𝑖 )의 차이를 제곱하여 더한 후에 평균을 구한 것이라고 보면 됩니다. ( 샘플의 개수로 나눈 것 )


from sklearn.model_selection import train_test_split # 일반화 성능 평가를 위해 기존 데이터를 훈련 세트와 테스트 세트로 나눔

from sklearn.linear_model import LinearRegression
X, y = mglearn.datasets.make_wave(n_samples=60) # 60개의 샘플 데이터 준비
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) # 훈련, 테스트 데이터셋 준비

lr = LinearRegression().fit(X_train, y_train) # 선형 회귀 모델에 훈련!

모델이 데이터를 입력받고 훈련을 통해 알아내야 할 𝑤( 가중치 - weight 또는 계수 - cofficient )와 𝑏( 편향 - offset 또는 절편 - intercept ) 은 각각 coef와 intercept 변수에 들어 있습니다.

print("lr의 계수(weight 또는 cofficient) : {}".format(lr.coef_))
print("lr의 편향(offset 또는 intercept) : {}".format(lr.intercept_))

위의 결과에서 알 수 있는 사실 중 하나는 coef값은 NumPy 배열 형태이지만 intercept 값은 언제나 실수(float) 값 하나라는 사실입니다.

즉 coef 값은 각 특성 부여되는 가중치들의 배열이라는 사실을 알 수 있고, intercept 속성은 그 가중치에 대한 조정값이라는 점입니다.

wave 데이터 셋은 입력 특성이 하나밖에 없기 때문에 배열의 길이도 1이라는 사실을 알 수 있습니다.

print("훈련 세트 점수 : {}".format(lr.score(X_train,y_train)))
print("테스트 세트 점수 : {}".format(lr.score(X_test, y_test)))

 

단순히 선형 회귀(LinearRegression) 모델을 훈련시킨 후 점수를 확인해 보니 𝑅2 값이 66% 정도로 별로 좋지 않습니다. 또한 훈련 세트의 점수와 테스트 세트의 점수가 거의 비슷한 것을 확인할 수 있는데, 이는 과소적합인 상태를 의미합니다. 많이 생각할 필요 없이 우리가 테스트 용도로 사용한 wave 데이터 셋은 특성이 하나뿐인 1차원 데이터 셋이기 때문에 과대적합을 걱정하지 않아도 됩니다. 하지만 지금부터 사용해 볼 고차원 데이터 셋인 보스턴 주택 가격 데이터셋에 대해서는 선형 모델의 성능이 매우 높아지기 때문에 과대적합이 될 가능성이 있습니다!

X, y = mglearn.datasets.load_extended_boston()

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 0)
lr = LinearRegression().fit(X_train, y_train)

print("훈련 세트 점수 : {:.2f}".format(lr.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(lr.score(X_test, y_test)))

 

테스트 결과 훈련 세트의 𝑅2는 95%지만, 테스트 세트의 점수는 61%입니다. 이는 모델이 과대적합되었다는 확실한 신호입니다. 따라서 우리는 모델의 일반화를 위해 복잡도를 제어할 수 있어야 하겠습니다. (각 특성별 가중치를 조절합니다.) 기본 선형 회귀 방식인 LinearRegression 모델로는 파라미터를 통해 복잡도를 제어 할 수 없으니, 여러 가지 회귀 모델을 알아보도록 하겠습니다.


릿지 Ridge 회귀

릿지(Ridge)도 회귀를 위한 선형 모델이므로 최소 제곱법 같은 예측 함수를 사용합니다. 릿지 회귀에서의 가중치(𝑤) 선택은 훈련 데이터를 잘 예측하기 위해서 뿐만 아니라 추가 제약 조건을 만족시키기 위한 목적도 있습니다. 즉 𝑤의 모든 원소가 0에 가깝게 만들어 가중치의 절댓값을 가능한 한 작게 한다는 뜻입니다. 결론적으로는 모든 특성이 예측(출력)에 주는 영향을 최소화하겠다는 뜻이 됩니다. 즉 𝑤에 의한 그래프의 기울기가 작아집니다. 이러한 제약 사향들을 규제(regularzation)이라고 합니다. 규제란 과대적합이 되지 않도록 모델을 강제로 제한한다는 뜻이 됩니다. 먼저 아무런 규제의 페널티가 없는 상황부터 살펴보겠습니다.

from sklearn.linear_model import Ridge

ridge = Ridge().fit(X_train, y_train)
print("훈련 세트 점수 : {:.2f}".format(ridge.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(ridge.score(X_test, y_test)))

 

결과를 확인해 보니 훈련 세트에 대한 점수는 낮지만 테스트 세트에 대한 점수는 더 높네요. 선형 회귀(LinearRegression)는 보스턴 데이트 셋에 대해 과대적합 되지만 Ridge 회귀 방식은 더 자유로운 ( 특성에 제한이 걸리는 ) 모델이기 때문에 과대적합이 적어진다고 볼 수 있습니다.

따라서 모델의 적합도가 낮아졌다고 볼 수 있는데, 이렇게 모델의 복잡도가 낮아지면 훈련 세트에서의 성능은 나빠지지만 더욱 일반화된 모델이 됩니다.

우리는 테스트 세트에 대한 성능이기 때문에 보스턴 데이터셋에는 일반 선형 회귀보다는 Ridge 회귀가 어울리는 것 같네요


페널티

여기서 페널티에 대해 잠깐 이야기 하자면, 패널티란 예측 결과물에 대한 오차 범위 허용이라고 볼 수 있습니다. 이정도 오차는 괜찮아~ 라고 말 하는 것과 비슷합니다. 간단한 예로 택시의 예상 도착 시간을 예측 한다고 했을 때 예상 한 것 보다 1~2분 늦는 것은 별로가 문제가 되지 않으나, 10 ~ 20분 늦는 것은 예측이 매우 실패 했다 라고 판단 할 수 있습니다. 우리가 위에서 사용한 MSE(Mean Squad Error) 같은 공식이 예측값의 패널티에 대한 공식 같은 것이라고 판단하면 됩니다. MSE의 공식을 자세히 보면 오차 제곱( 타깃값 - 예측값의 제곱 )을 구하고 있는데, 이는 오차가 커지면 커질수록 더욱더 많은 페널티를 부여한다고 보면 됩니다. 보통 사용되는 다른 공식으로써 MAE(Mean Absolute Error)라는 것이 있습니다.

MSE와 다른 점은 MAE는 오차에 대한 페널티가 단순히 오차의 절대 값인 것을 확인할 수 있습니다. 이를 𝐿1 norm이라고 합니다. 즉 MSE는 오차가 커질수록 페널티가 커지고, MAE는 오차가 커질 수록 패널티가 일정 하다 라고 생각 해 볼 수 있겠습니다. 패널티를 계산하는 공식에는 RMSE, RMAE 등등의 여러가지 공식들이 있습니다.

Ridge 회귀에서의 패널티

Ridge는 모델을 단순하게 ( 계수 - 𝑤를 0에 가깝게 ) 해주고 훈련 세트와 테스트 세트 사이의 선능 사이를 절출 할 수 있는 방법을 제공합니다. 우리는 Ridge 회귀의 alpha 매개변수를 조절하여 모델의 성능을 단순화할 수 있습니다. 매개변수를 아무것도 넣지 않으면 alpha는 1.0을 기본적으로 사용하고 있었습니다. 참고로 alpha 값을 높이면 계수를 0으로 점점 가깝게 설정해 페널티의 효과가 커져 가중치가 차지하는 값이 0에 가까워져 모델이 단순해지고, alpha 값을 낮추면 점점 가중치가 높아져 모델이 복잡해집니다. Ridge 회귀를 사용할 때 최적화된 alpha 값은 사용하는 데이터셋에 따라 달라집니다. 바로 예를 들어 한번 보겠습니다. alpha 값을 10으로 조절한 경우입니다.

ridge10 = Ridge(alpha=10).fit(X_train, y_train)
print("훈련 세트 점수 : {:.2f}".format(ridge10.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(ridge10.score(X_test, y_test)))

alpha값을 10으로 높였더니 페널티의 효과가 높아져 가중치가 감소 한 것이 확인 됩니다. 즉 모델이 점점 단순화 되어 과소 적합 되어 가고 있다고 판단 할 수 있습니다.

이번엔 반대로 alpha 계수를 낮춰서 패널티의 효과를 줄이고 가중치를 증가시켜 보겠습니다.

ridge01 = Ridge(alpha=0.1).fit(X_train, y_train)
print("훈련 세트 점수 : {:.2f}".format(ridge01.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(ridge01.score(X_test, y_test)))

테스트 세트의 점수가 꽤나 높아졌습니다. 보스턴 주택가 데이터셋을 Ridge 회귀로 분석했을 때 alpha=0.1이 꽤나 좋은 성능을 내는 것 같네요.

이번엔 alpha 값에 따라서 각 모델의 coef_ ( 가중치 값 )이 어떻게 달라지는지 시각화해서 살펴보겠습니다.

예상을 해보자면 높은 alpha 값은 제약이 적은 모델이기 때문에 ( 가중치가 0에 점점 가까워지기 때문에 ) coef_의 절댓값의 크기가 작을 것 같네요. 확인해 보겠습니다.

plt.plot(ridge10.coef_, '^', label="Ridge alpha 10")
plt.plot(ridge.coef_, 's', label="Ridge alpha 1.0")
plt.plot(ridge01.coef_, 'v', label="Ridge alpha 0.1")

plt.plot(lr.coef_, 'o', label="LinearRegression")
plt.xlabel("계수 목록")
plt.ylabel("계수 크기")
plt.hlines(0, 0, len(lr.coef_))
plt.ylim(-25, 25)
plt.legend()

뭔가 굉장히 복잡해 보이네요, 먼저 X축은 coef_의 원소를 위치대로 나열한 것입니다. 즉 x=0은 첫 번째 특성, 약간 100을 넘는 값은 마지막 특성이라고 보면 될 것 같습니다.

y축은 계수의 수치를 나타내는데, alpha=10 일 때 ( 파란색 세모 ) 대부분의 계수는 -3부터 3 사이에 위치하는 것을 볼 수 있습니다. alpha=1 ( 주황색 네모 )의 계수의 크기는 조금 더 커져 가중치가 증가한 것이 확인됩니다. alpha=0.1 ( 초록색 세모 ) 같은 경우는 계수의 크기가 더 커져 가중치가 1일 때 보다 더 커진 것을 확인할 수 있고

alpha 값이 0인 ( 규제가 전혀 없는 ) LinearRegression 같은 경우는 그래프를 벗어나는 특성도 존재하는 것을 확인해 볼 수 있습니다.

 

Ridge에서 alpha 값은 고정되고, 데이터의 개수를 조절한다면?

규제의 효과를 확인해 보기 위해서 alpha를 1로 고정한 채로 훈련 데이터의 크기만 변화를 시켜 데이터 세트의 크기에 대해 훈련 효과를 확인해 보겠습니다.

보스턴 주택 가격 데이터세트에서 여러 가지 크기로 훈련시켜 LinearRegression과 Ridge(alpha=1)을 적용한 그래프를 확인 해 보겠습니다.

mglearn.plots.plot_ridge_n_samples()

릿지와 선형 회귀 모두 훈련 세트의 점수 ( 점선 )가 테스트 세트의 점수 ( 실선 ) 보다 높은 것을 확인할 수 있습니다.

여기서 살펴볼 수 있는 사실은 릿지 회귀에는 규제가 적용되기 때문에 훈련 데이터의 점수가 대체적으로 선형 회귀보다 낮은 것을 확인할 수 있습니다. 이는 데이터 세트의 개수가 적으면 적을수록 더 확연히 확인해 볼 수 있습니다.

데이터 크기가 400 미만에서는 선형 회귀는 무엇도 학습하지 못하고 있는 것이 확인됩니다.

즉 두 모델의 성능은 ( 테스트 데이터세트의 결과 ) 데이터가 많으면 많을수록 좋아지며, 마지막에는 선형 회귀가 릿지 회귀의 테스트 점수를 따라잡는 것이 확인됩니다.

정리하자면 데이터를 충분히 주게 되면 규제 자체는 덜 중요해져 릿지 회귀와 선형 회귀의 성능이 같아질 것이라는 것을 예상해 볼 수 있다는 점입니다.

또 하나 이상한 점은 주황색 점선 ( 훈련 데이터셋 점수 )가 점점 내려가는 것을 확인할 수 있는데, 이는 선형 회귀의 훈련 데이터셋에 대한 성능이 점점 감소하는 것을 의미합니다.

이는 데이터가 많아지면 많아질수록 모델이 데이터를 기억하거나 과대적합 하기 어려워진다는 것을 의미합니다.


라쏘 Lasso 회귀

라쏘 회귀 방식도 릿지와 비슷하게 계수에 규제를 걸어 계수를 0에 가깝게 만들기 위한 노력을 합니다. 릿지 회귀와의 차이점은 릿지 회귀는 L2 규제 방식을 사용하는데 비해 라쏘 회귀는 L1 규제 방식을 사용하며, 실제 어떤 계수는 실제로 0이 되기도 한다는 점입니다.

계수가 0이 되면 해당 특성은 전혀 상관이 없이 완전히 제외된다는 뜻이 됩니다.

어떻게 보면 라쏘 회귀는 특성 선택( feature selection )이 자동으로 이루어진다고도 볼 수 있습니다.

일부 계수를 0으로 만들면 모델을 이해하기가 쉬워지고 이 모델의 가장 중요한 특성이 무엇인지가 드러납니다.

보스턴 주택가격 데이터셋에 라쏘를 적용시켜 보겠습니다.

from sklearn.linear_model import Lasso

lasso = Lasso().fit(X_train, y_train)
print("훈련 세트 점수 : {:.2f}".format(lasso.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(lasso.score(X_test, y_test)))
print("사용한 특성의 수 : {}".format(np.sum(lasso.coef_ != 0)))

확인 결과 라쏘는 훈련 세트와 테스트 세트의 점수가 전부 다 좋지 않습니다. 사용한 특성도 4개 정도밖에 안 되는 것으로 보아 과소적합이라고 생각할 수 있겠습니다.

릿지와 비슷하게 라쏘도 계수를 얼마나 강하게 보낼지를 조절하는 alpha 매개변수가 있습니다. 앞서 본 릿지에서는 기본적으로 alpha=1.0을 사용했었습니다.

과소적합을 줄이기 위해 alpha 값을 줄여보겠습니다. 하지만 이때 최대 반복 횟수를 의미하는 max_iter 값을 늘려 줘야 합니다.

max_iter는 내부적으로 Lasso의 학습 과정이 진행되는 최대 횟수를 의미합니다. 한 특성 씩 좌표축을 따라 최적화되는 좌표 하강법 방식을 사용하며 학습 과정이 max_iter에 지정된 횟수만큼 반복 횟수가 설정 되게 됩니다. lasso.niter를 이용해 반복 횟수를 확인할 수 있습니다.

lasso001 = Lasso(alpha=0.01, max_iter=100000).fit(X_train, y_train)
print("훈련 세트 점수 : {:.2f}".format(lasso001.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(lasso001.score(X_test, y_test)))
print("사용한 특성의 수 : {}".format(np.sum(lasso001.coef_ != 0)))

alpha 값을 줄여 봤더니 모델의 복잡도가 증가하여 훈련 세트와 테스트 세트의 점수가 모두 좋아졌습니다. 성능 자체가 릿지보다 조금 더 낫네요!

사용된 특성 자체가 105개 중 33개뿐이어서 모델을 분석하기가 쉬워진 것 같습니다.

하지만 alpha 값을 너무 낮추면 규제가 그만큼 효과가 없어지기 때문에 과대적합이 되므로 LinearRegression의 결과와 비슷해집니다.

lasso0001 = Lasso(alpha=0.0001, max_iter=1000000).fit(X_train, y_train)

print("훈련 세트 점수 : {:.2f}".format(lasso0001.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(lasso0001.score(X_test, y_test)))
print("사용한 특성의 수 : {}".format(np.sum(lasso0001.coef_ != 0)))

마찬가지로 alpha 값에 따른 다른 모델들의 계수를 그래프로 그려 보겠습니다.

plt.plot(lasso.coef_, 's', label="Lasso alpha=1")
plt.plot(lasso001.coef_, '^', label="Lasso alpha = 0.01")
plt.plot(lasso0001.coef_, 'v', label="Lasso alpha = 0.0001")

plt.plot(ridge01.coef_, 'o', label="Ridge alpha = 0.1")
plt.legend(ncol=2, loc=(0, 1.05))
plt.ylim(-25, 25)
plt.xlabel("계수 목록")
plt.ylabel("계수 크기")

alpha가 1일 때( 파란색 )는 계수의 대부분이 0인 것을 알 수 있고, 나머지 계수들도 크기가 작다는 것이 확인됩니다. alpha를 0.01로 줄이면 ( 주황색 세모 ) alpha가 1일 때보다는 적지만 마찬가지로 계수 대부분이 1인 것이 확인 됩니다.

하지만 alpha 값이 0.0001이 되면 ( 초록색 세모 ) 계수 대부분이 0이 아니게 되고 그 값이 커지는 것이 확인 됩니다.

alpha 값이 0.1인 릿지 모델은 Lasso 모델과 비교해 성능은 비슷하지만 어떠한 계수도 0이 되지 않는 것이 확인 됩니다.

실제로 Lasso와 Ridge 중에서 릿지 회귀를 선호합니다.

하지만 입력 특성 자체가 많고 그중 일부분만 중요한 특성일 경우에는 Lasso가 더 좋은 선택이 될 수가 좋습니다. 또한 분석하기 쉬운 모델을 원한다면 Lasso가 일부 모델만 사용하므로 쉽게 해석할 수 있는 모델을 만들어 주게 될 수도 있습니다.

 

728x90
반응형
LIST
728x90
반응형
SMALL

지난 2023.04.18 - [Programming/특성 공학] - [Machine Learning] 지도 학습의 종류

 

[Machine Learning]지도 학습의 종류

2023.04.14 - [Programming/특성 공학] - [Machine Learning]지도 학습에서 언급했듯이 모델 파라미터 즉, 매개변수의 종류는 다양합니다. 그래서 이번 포스트에서는 지도 학습의 종류에 대하여 알아보도록

yuja-k.tistory.com

에 이어 먼저 붓꽃 분류를 위해 사용했었던 k-최근접 이웃(k-NN) 알고리즘부터 다시 시작 해 보겠습니다.

K-NN은 무엇인가

k-NN(k-Nearest Neighbors) 알고리즘은 제일 간단한 머신러닝 알고리즘 입니다. 단순히 훈련할 데이터셋(training data set) 저장하는 것이 머신러닝 모델을 만드는 것이 전부입니다.

새로운 데이터 포인트를 예측할 때는 훈련 데이터셋으로 만들어 놓은 k-NN 모델에서 가장 가까운 데이터 포인트, 즉 최근접 이웃을 찾습니다.

#필요 라이브러리 임포트
from IPython.display import display
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import mglearn
plt.rcParams['axes.unicode_minus'] = False

import platform
path = 'c:/Windows/Fonts/malgun.ttf'
from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
    rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc('font', family=font_name)
else:
    print('Unknown system... sorry!')
K-NN 분류 모델

mglearn 라이브러리에 이미 forge 데이터 셋을 이용해 k-NN 알고리즘을 적용하고 나서 시각화 한 내용이 있습니다. 바로 확인해 보죠!

mglearn.plots.plot_knn_classification(n_neighbors=1) # n_neighbors : 이웃의 갯수

뭔가 복잡한 것 같지만, 모양으로 한번 확인해 보겠습니다.

동그라미 모양의 데이터들은 훈련 데이터입니다. 입니다. k-NN 모델 객체에 훈련시킨 데이터가 표시되어 있습니다.

별 모양의 데이터들은 테스트 데이터로써, 선을 이용해 각 훈련된 데이터 포인트에서 가장 가까운 이웃과 선이 연결되어 있습니다.

n_neighbors 파라미터의 값을 변경하면 가장 가까운 여러 개의 이웃이 선택되고, 둘 이상의 이웃이 선택되면 레이블( 분류 결과 - 그래프에서는 색상 )을 정하기 위해 투표를 합니다.

즉 테스트 포인트 하나에 대해서 클래스 0( 파란색 )에 속한 이웃이 몇 개인지, 클래스 1( 주황색 )에 속한 이웃이 몇 개인지를 셉니다. 그리고 이웃이 더 많은 클래스를 레이블로 지정합니다.

정리하자면 테스트 데이터에 대해 k-최근접 이웃 중 다수의 클래스가 레이블이 된다고 볼 수 있습니다. 이어서 3개의 최근접 이웃을 사용한 예를 보겠습니다.

mglearn.plots.plot_knn_classification(n_neighbors=3) # 이웃이 3개

이웃의 개수 ( n_neighbors 파라미터 )를 3개로 조절했더니 그래프 상에서는 별 모양의 테스트 데이터에 대해 3개의 훈련 데이터 포인트가 연결된 것을 확인할 수 있습니다.

또한 이웃을 한 개만 이용했을 때와, 세 개의 이웃을 사용했을 때 결과가 달라지는 것도 확인할 수 있습니다.

단순한 특성 1, 특성 2를 활용한 단순한 이진 분류 문제이지만, 우리가 k-NN 모델을 사용한다면 클래스가 다수인 데이터셋에도 같은 방법을 적용할 수 있습니다.

클래스가 여러 개일 때도 각 클래스에 속한 이웃이 몇 개인지를 헤아려 가장 많은 클래스를 예측값으로 사용합니다. 본격적으로 k-NN 알고리즘 적용에 대해 알아보겠습니다.

from sklearn.model_selection import train_test_split # 일반화 성능 평가를 위해 기존 데이터를 훈련 세트와 테스트 세트로 나눔
X, y = mglearn.datasets.make_forge()

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

모델을 만드는 방법은 간단합니다. k-NN 이웃 알고리즘은 scikit-learn에서 KNeighborsClassifier로 제공되고 있습니다

from sklearn.neighbors import KNeighborsClassifier #k-NN 분류 임포트
clf = KNeighborsClassifier(n_neighbors = 3) # 이웃의 갯수를 3개 갖는 모델 생성

이어서 훈련 세트를 사용해 KNeighborsClassifier 모델 객체( clf )를 훈련( fit ) 시켜 봅시다.

clf.fit(X_train, y_train) # 훈련하기

KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=3, p=2, weights='uniform')


잘 훈련되었으면 테스트 데이터에 대해 예측( predict ) 해보겠습니다.

print("테스트 세트 예측: {}".format(clf.predict(X_test)))

테스트 세트 예측: [1 0 1 0 1 0 0]

 

방금 만든 모델이 얼마나 잘 일반화되었는가를 판단하기 위해 score 메소드에 테스트 데이터와 테스트 레이블을 넣어서 확인합니다.

print("테스트 세트 정확도: {:.2f}".format(clf.score(X_test, y_test)))

테스트 세트 정확도: 0.86


우리가 만든 모델의 정확도는 86%가 나왔습니다. 즉 모델이 테스트 데이터셋에 있는 샘플 중 86%를 정확히 예측한 것입니다.

이어서 이웃의 수에 따라서 k-NN 모델이 어떻게 데이터를 구분하는지 평면에 색을 칠해 경계를 나눠 보겠습니다.

이처럼 클래스 0과 클래스 1로 지정한 영역으로 나뉘는 경계를 결정 경계 (decision boundary)라고 합니다

fig, axes = plt.subplots(1, 5, figsize=(20,3))

for n_neighbors, ax in zip([1,3,6,9,26], axes):
    # fit 메소드는 self 반환을 하기 때문에 객체 생성과 메소드를 한줄에 사용 할 수 있습니다.
    clf = KNeighborsClassifier(n_neighbors=n_neighbors).fit(X, y)
    mglearn.plots.plot_2d_separator(clf, X, fill=True, eps=0.5, ax=ax, alpha=.4)
    mglearn.discrete_scatter(X[:,0], X[:,1],y, ax=ax)
    ax.set_title("{} 이웃".format(n_neighbors))
    ax.set_xlabel("특성 0")
    ax.set_ylabel("특성 1")
    
axes[0].legend(loc=3)

<matplotlib.legend.Legend at 0x1 c14 acab38>


위의 그래프를 한번 분석해 보겠습니다.

  • 이웃이 1개인 경우는 결정 경계가 훈련데이터에 가깝게 나눠지고 있습니다. 즉 모델의 복잡도가 증가했습니다. (과대적합 가능성)
  • 이웃이 3개인 경우는 결정 경계가 1개일 때보다 약간 부드러워졌네요. 이는 모델의 복잡도가 감소한 것을 의미합니다.
  • 이웃이 6개인 경우는 결정 경계가 클래스들이 위치한 것과 비슷한 구역을 각각 나눠 갖은것 같습니다.
  • 이웃이 9개인 경우는 결정 경계가 6개 일 때 보다 훨씬 완만해집니다. 가운데쯤에 있는 특성 1 때문에 그런 것 같습니다. 모델이 점점 더 단순해지는 것 같네요( 과소 적합 가능성)
  • 극단적으로 26개의 이웃을 가진 경우는 모든 데이터를 하나로 분석해 버립니다. k-nn은 이웃의 개수로 투표를 하기 때문인 것 같습니다.

위의 분석 내용과 그래프로 알 수 있는 사실은 모델의 개수가 너무 적으면 복잡도가 증가해 과대적합 가능성이 생겨버리고, 모델의 개수가 많으면 복잡도가 감소해 과소 적합 가능성이 생긴 다는 것을 알 수 있습니다.

 

유방암 데이터를 이용해 k-NN 알아보기

모델의 복잡도와 일반화 사이의 관계를 입증해 보도록 하겠습니다. 이웃의 개수에 따라 얼마나 k-NN이 잘 일반화되는지를 알아보겠습니다.

먼저 유방암 데이터에 대해 훈련 세트와 테스트 세트를 준비해 보겠습니다. 그러고 나서 이웃의 개수를 따로 하여 얼마나 정확도가 증가하고 알맞은 모델이 되는지 평가해 보겠습니다.

from sklearn.datasets import load_breast_cancer

cancer = load_breast_cancer() #유방암 데이터 불러오기

X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, 
                                                    stratify=cancer.target, random_state=66) # 테스트 세트와 훈련 세트 분할

training_accuracy = [] # 각 이웃 개수 별 훈련 세트에 대한 정확도를 저장할 리스트
test_accuracy = [] # 각 이웃 개수 별 테스트 세트에 대한 정확도를 저장할 리스트

#이웃의 개수 설정 (1개 ~ 10개 까지)
neighbors_settings = range(1, 11)

for n_neighbors in neighbors_settings:
    #모델 생성하기
    clf = KNeighborsClassifier(n_neighbors=n_neighbors)
    clf.fit(X_train, y_train)
    
    #훈련 세트 정확도 저장
    training_accuracy.append(clf.score(X_train, y_train))
    #일반화 정확도 저장
    test_accuracy.append(clf.score(X_test, y_test))
    
plt.plot(neighbors_settings, training_accuracy, label="훈련 정확도") # 이웃 개수에 대한 훈련 세트 정확도 선 그래프 그리기
plt.plot(neighbors_settings, test_accuracy, label="테스트 정확도") # 이웃 개수에 대한 테스트 세트 정확도 선 그래프 그리기
plt.ylabel("정확도")
plt.xlabel("n_neighbors")
plt.legend()

<matplotlib.legend.Legend at 0x102 cdf5 f8>


그래프를 보면 과대적합과 과소적합의 특성을 알 수 있습니다.

  • 이웃의 개수가 적으면 훈련세트의 정확도는 높지만 테스트 세트의 정확도는 낮고 - 과대적합
  • 이웃의 개수가 많아지면 모델이 단순해지면서 훈련 정확도와 테스트 정확도가 동시에 낮아집니다. - 과소적합

여기서 알 수 있는 사실은 이웃이 1개인 경우에는 너무 모델을 복잡하게 만들어 낸다는 사실을 알 수 있네요, 반대로 이웃을 10개 사용했을 때는 모델이 너무 단순해서 정확도가 더욱더 나빠진다는 사실을 알 수 있게 됩니다.

가장 좋은 이웃은 몇 개일까요?테스트 세트의 정확도가 가장 높은 6개 정도의 이웃을 사용했을 때 가장 일반화가 잘 됐다라고 할 수 있겠습니다.

 

K-NN Regression

k-NN 알고리즘은 회귀 분석을 위해서도 사용합니다. wave 데이터셋을 이용해 알아보도록 하죠.

분류와 마찬가지로 mglearn 패키지에 wave 데이터 셋을 이용한 회귀 시각화 그래프가 이미 존재합니다. 먼저 이웃이 한 개인 경우의 회귀 결과입니다.

mglearn.plots.plot_knn_regression(n_neighbors=1)

먼저 파란색 원은 트레이닝 데이터, 초록색 별은 입력한 특성, 마지막으로 파란색 별은 입력한 특성에 대한 회귀 분석 결과입니다.

k-NN 회귀는 이웃의 개수를 다수로 지정하면 이웃 간의 평균이 그 예측값이 됩니다.

mglearn.plots.plot_knn_regression(n_neighbors=3)

이웃의 개수가 한 개일 때는 단순하게 제일 가까운 이웃만을 따져서 파란색 별의 위치가 결정되었지만, 이웃의 개수가 3개가 되면 가장 가까운 3개의 점을 찾아 그 평균을 회귀 결괏값으로 사용하는 것을 알 수 있습니다.

k-NN 회귀는 k-NN 분류와 비슷하게 사용할 수 있습니다. KNeighborsRegressor에 구현되어 있습니다.

from sklearn.neighbors import KNeighborsRegressor # k-NN 회귀를 위한 KNeighborsRegressor 임포트

X, y = mglearn.datasets.make_wave(n_samples=40)

#wave 데이터셋을 훈련 세트와 테스트 세트로 나누기
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

# 이웃의 수를 3으로 하여 모델 객체 생성하기
reg = KNeighborsRegressor(n_neighbors = 3)

# 훈련 데이터와 타깃을 사용하여 모델 학습 시키기
reg.fit(X_train, y_train)

print("테스트 세트 예측 :\n{}".format(reg.predict(X_test)))

테스트 세트 예측 :
[-0.05396539  0.35686046  1.13671923 -1.89415682 -1.13881398 -1.63113382
  0.35686046  0.91241374 -0.44680446 -1.13881398]

 


이 역시 score 메소드를 이용해 점수를 알 수 있는데요, score 메소드는 회귀분석일 때는 $R^2$ 값을 반환합니다.

여기서 $R^2$ 값은 결정 계수 라고 하며 회귀 모델에서의 예측의 적합도를 0 ~ 1 사이의 값으로 계산한 것입니다. 공식은 다음과 같습니다. \begin {align} \\R^2=1-\frac {\sum{({y-\hat {y})^2}}}{\sum{({y-\bar {y})^2}}} \end {align}

의미하는 바는 다음과 같습니다. $y$ 는 타깃값이고, $\bar {y}$는 타깃값($y$ 들)의 평균이며, $\hat {y}$는 모델의 예측값입니다.

즉 $y$는 우리가 맞춰야 할 값( 훈련된 데이터 ), $\bar {y}$는 훈련된 데이터들의 평균값, $\hat {y}$는 우리가 만든 모델이 테스트 데이터에 대해 예측한 값입니다.

따라서 훈련데이터들(X_train)의 타깃값으로 사용한 y_train을 예측 데이터로 사용하게 되면 $R^2$의 공식에서 $\hat {y}$이 $\bar {y}$와 같게 되고, 분자와 분모가 같아져 $R^2$값이 0이 되게 됩니다.

1은 예측이 완벽한 경우, 0은 훈련 세트의 출력값인 y_train의 평균으로만 예측하는 모델의 경우입니다.

print("테스트 세트 R^2 : {:.2f}".format(reg.score(X_test, y_test)))

테스트 세트 R^2 : 0.83

 

우리가 만든 모델에서 예측한 값은 83% 정도로 비교적 잘 맞는 것 같네요!

wave 데이터셋을 계속 이용하여 이웃의 개수(n_neighbors)에 따른 테스트 세트의 성능을 판단해 보겠습니다.

fig, axes = plt.subplots(1,4, figsize=(20,4))
#-3과 3 사이에 1000개의 데이터 만들기 -> 테스트 용도로 사용함
line = np.linspace(-3, 3, 1000).reshape(-1, 1)
for n_neighbors, ax in zip([1,3,5,9], axes):
    # 이웃의 개수를 1,3,9로 하여 예측하는 모델을 만듭니다
    reg = KNeighborsRegressor(n_neighbors=n_neighbors)
    reg.fit(X_train, y_train)
    ax.plot(line, reg.predict(line)) # 테스트 용도로 만든 데이터를 예측하고 예측 결과를 선으로 표현합니다.
    ax.plot(X_train, y_train, '^', c=mglearn.cm2(0), markersize=8) # 훈련 데이터를 그래프에 표시합니다.
    ax.plot(X_test, y_test, 'v', c=mglearn.cm2(1), markersize=8)   # 테스트 데이터를 그래프에 표시합니다.
    
    # 훈련 데이터의 점수와 테스트 데이터의 점수를 제목에 표현합니다.
    ax.set_title("{} 이웃의 훈련 스코어: {:.2f} / 테스트 스코어 : {:.2f}".format(n_neighbors, reg.score(X_train, y_train), reg.score(X_test, y_test)))
    ax.set_xlabel("특성")
    ax.set_ylabel("타깃")

axes[0].legend(["모델 예측", "훈련 데이터/타깃", "테스트 데이터/타깃"], loc="best")

<matplotlib.legend.Legend at 0x1 c1744 d588>


첫 번째 그래프부터 확인해 보겠습니다.

이웃의 개수가 1개인 첫 번째 그래프는 모델을 예측한 라인들이 모두 훈련 데이터를 지나가는 것을 확인할 수 있습니다. 이는 훈련 세트의 각 데이터 포인트들이 예측에 주는 영향이 커서 매우 불안정한 예측이라고 할 수 있습니다.

이에 반에 이웃의 개수가 많아질수록 훈련 스코어가 점점 줄어드는 것을 볼 수 있는데 이는 이웃을 많이 사용할수록 훈련 데이터에는 잘 맞지 않을 수 있지만, 더 안정적으로 예측을 할 수 있게 됩니다.

이웃의 개수가 3개 일 때는 적절히 훈련 데이터와 테스트 데이터를 지나가는 모습을 볼 수 있으며, 그 이상의 이웃이 설정될수록 예측 라인들이 훈련데이터를 빗겨 지나가는 모습을 확인할 수 있습니다. (정확도가 점점 떨어집니다.)

 

장단점과 매개변수

우리는 지금까지 한 개의 매개변수(n_neighbors)만 사용해서 이웃의 개수를 조절하면서 모델의 성능을 체크해 보았습니다. 보통이면 두 개의 매개변수를 활용하는데, 다른 하나는 거리 조절 공식을 따로 준비하는 것입니다.

기본적으로 k-NN은 metic 매개변수를 활용하여 거리 측정 방식을 변경할 수 있으며, 기본값은 민코프스키 거리를 의미하는 'minkowski'가 설정되어 있습니다.

이 민코프스키 거리를 제곱하여 크기를 정하는 p의 기본값이 2일 때 k-NN의 기본 거리 측정 공식인 유클라디안 거리와 같게 됩니다.

k-NN의 장점은 이해하기가 매우 쉬운 모델이라는 점입니다. 또한 많이 조정할 필요 없이 좋은 성능을 발휘하여 k-NN 보다 더 복잡한 알고리즘을 적용하기 전에 테스트해볼 수 있는 좋은 시작점이 됩니다.

단점은 모델 자체는 매우 빠르게 만들어 볼 수 있지만, 훈련 세트가 매우 크면( 특성 또는 샘플 데이터의 개수가 많으면 ) 예측이 느려지게 됩니다. 또한 수백 개의 특성을 가진 데이터 셋에는 잘 동작하지 않고, 희소한 데이터셋 ( 특성값의 대부분이 0인 ) 데이터셋과는 특히 잘 작동할 수 없습니다.

위의 단점에 따라 k-NN은 예측이 느리고 많은 특성을 처리하는 능력이 부족하기에 현업에서는 잘 사용하지 않습니다.

선형모델을 통해 k-NN 알고리즘의 단점을 극복할 수 있습니다.

 

다음 블로그에서는 선형 회귀에 대하여 알아보도록 하겠습니다.

728x90
반응형
LIST
728x90
반응형
SMALL

2023.04.14 - [Programming/특성 공학] - [Machine Learning]지도 학습에서 언급했듯이 모델 파라미터 즉, 매개변수의 종류는 다양합니다. 그래서 이번 포스트에서는 지도 학습의 종류에 대하여 알아보도록 하겠습니다.

 

먼저, 지도학습에는 분류(Classification)와 회귀(Regression)가 있습니다.

먼저 분류는 가능성 있는 여러 클래스 레이블(label) 중 하나를 예측 하는 것입니다. 이전 장 붓꽃 예제를 살펴보면 3가지 붓꽃의 품종 중 하나를 데이터( feature )를 이용해 예측하였습니다.

 

이때 두 개만 분류 하는 것을 이진 분류(binary classification)와 셋 이상의 클래스로 분류하는 다중 분류(multiclass classification)가 있습니다.


분류 Classification

이진 분류(Binary Classification)

단순히 말해 이진 분류는 Yes / No 로만 나올 수 있도록 하는 것입니다. 예를 들어보면 "당신은 비만인가요?"에 대한 답이겠네요.
이 때 양성 클래스와 음성 클래스로 분류됩니다. (양성이라고 해서 무조건 좋은 것은 아닙니다.)

"당신은 비만인가요?"에 대한 질문의 답으로 개발자가 원하는 값이, 즉 학습하고자 하는 대상이 비만이라면 '비만'이라는 값이면 '비만'이 양성이 됩니다.
반대로 비만이 아닌 값은 '음성'이 되겠네요

 

다중 분류(Multiclass Classification)

이진 분류와 다르게 다중 분류는 3개 이상의 결과가 나오는 것을 의미합니다
다중 분류는 이전 장에서 해봤던 붓꽃 예제가 이에 해당됩니다. 다른 예를 들자면 환자들의 각종 상태를 분석해 병명을 예측하는 것도 다중 분류에 해당되겠네요.

 

회귀 Regression

회귀는 보통 연속적인 숫자, 또는 부동 소수점 수(실수)를 예측합니다.

옥수수 농장의 전년도 수확량과 날씨, 고용 인원수 등으로 올해 수확량을 예측하거나 교육 수준, 나이, 주거지 등을 바탕으로 연간 소득을 예측하는 등등 어떤 숫자 된 결과물이 될 수 있을 때 이를 회귀라고 이야기합니다.

분류와 회귀 구분하기

분류와 회귀를 구분하는 방법은 출력값에 연속성이 있는지를 확인해 보면 됩니다.

연소득을 예측했을 때 이 사람의 연소득이 40,000,000원으로 예측되어야 한다고 한다면
40,000,001원이거나 39,999,999이라고 예측해도 예측한 양은 차이가 약간은 있지만 그렇게 큰 차이는 아니기 때문에 별다른 문제가 되진 않을 것입니다.

반대로 분류는 비만도를 구해 이 사람이 비만인지 과체중인지 저체중인지 등등 확실하게 그 결과가 정해져야 할 때를 의미하게 됩니다.

 

정리를 해보자면!

결과가 Yes / No 또는 이미 정해진 결과대로 예측이 되어야 하는 경우는 분류를 사용,
여러 연속적인 숫자가 예측되어서 허용한 오차 범위 내에 값이 있어도 될 때는 회귀를 사용한다고 보면 됩니다.

일반화 Generalization

훈련 데이터를 여러 개 준비해서 훈련된(학습된) 모델이 처음 보는 데이터가 주어져도 정확하게 예측할 수 있으면 이를 일반화되었다고 합니다.

보통 훈련 데이터 세트를 준비해 머신러닝을 시키게 되면, 보통이면 훈련 세트에 대해 정확히 예측하도록 모델을 구축합니다. 훈련 세트와 테스트 세트가 매우 비슷하다면 여러분이 만들어 놓은 모델은 테스트 세트에서도 정확하게 예측할 것이라 기대합니다.

이때 주의 해야 할 상황 중 하나는 아주 복잡한 모델 만든다면 훈련세트에만 정확한 모델이 되어 버릴 수가 있습니다.

여기서 복잡한 모델이란 너무나 많은 분류 데이터들을 사용하는 경우입니다. 예를 하나 들어 볼까요?

나이 이름 성별 혼인상태 자녀수 차량 구매 여부
25 김유정 남자 미혼 0 no
45 박미영 여자 기혼 2 yes
23 김경진 여자 미혼 0 no
34 박철수 남자 기혼 1 yes
44 손맹구 남자 미혼 0 yes
39 강유리 여자 이혼 1 yes
과대 적합 Overfitting & 과소 적합 Underfitting

한 회사의 고객 데이터라고 예시를 들어 보았습니다. 위 회사에서는 자동차 구매를 위해 고객들에게 전화를 할까 하는데, 기존에 자동차를 구매하지 않았던 고객들에게는 구매를 추천해 주지 않기( 전화를 하지 않겠죠? )로 합니다.

따라서 위의 데이터를 분석하여 구매했던 사람들을 분석해야 할 것 같습니다.

먼저 규칙을 한번 찾아보겠습니다. 이 규칙이 바로 구매하지 않은 사람과 구매 한 사람을 분류하는 기준이 될 것입니다.

  • 나이 분류 : 30대 이상인 사람들이 구매했다.
  • 이름 분류 : 성이 김 씨인 사람들은 구매하지 않았다.
  • 혼인 분류 : 미혼인 사람들은 대부분 구매하지 않았지만, 40대 이상인 남자는 구매했다.
  • 자녀 분류 : 자녀가 있으면 차를 대부분 구매했다. 
    "결과적으로 나이가 30대이고, 성이 김 씨가 아니며, 혼인을 한 사람들은 대부분 우리 회사 차량을 살 것이다."라는 결론을 내렸습니다.

일단 위와 같은 기준으로 모델을 만들었다고 생각해 보면 100% 정확하게 맞아 들어가는 규칙을 정의한 것 같습니다. 하지만 사실상 위의 규칙 말고도 데이터를 만족할 만한 규칙은 얼마든지 만들어 낼 수 있습니다. 나이가 45, 34, 44, 39인 사람들이 구매했다. 같은 규칙 같은 것들이죠

아무튼 기존 데이터셋에 딱 들어맞는 규칙은 얼마든지 만들어 낼 수 있습니다. 하지만 이게 저희가 원하는 결과일까요??

답은 NONO! 왜냐하면 우리는 이 데이터에 존재하는 고객들에 대한 답은 이미 알고 있고, 더해서 새로운 고객이 차량을 구매할 것 인가 이기 때문입니다.

따라서 새로운 고객에도 잘 작동하는 규칙을 찾아야만 하며, 이미 준비된 훈련 세트에서 100% 정확도를 달성하는 것은 크게 도움이 되지 않습니다.


다시 일반화에 대한 이야기를 해보겠습니다. 일반화란, 우리가 만들어 놓은 규칙(모델)이 새로운 데이터에도 적절하게 예측할 수 있는 상태를 의미하는데, 어떻게 하면 될까요?

 

바로 간단한 모델을 만드는 것입니다. 간단한 모델일수록 일반화가 더 잘 될 것이라는 뜻인데, 예를 들어 위의 예제에서는 "30대 이상의 사람들은 차량을 구매하려고 한다."라고 규칙을 세우는 것이 좋을 것 같네요.

이렇게 규칙을 만들게 되면 모든 고객데이터를 만족할 뿐만 아니라, 나이 외에 혼인 상태, 자녀 수 등을 추가한 것보다 더 신뢰할 수 있을 것 같습니다.

하지만 바로 이전에 여러 가지 규칙을 사용해 너무 복잡한 모델을 만드는 바람에 새로운 데이터에 대해 예측이 어렵게 만드는 것을 과대적합이라고 합니다.

반대로 "김 씨가 아니면 모두 차량을 구매할 것이다."와 같이 데이터의 면면과 다양성을 잡아내지 못하는 규칙을 적용하는 것을 과소적합이라고 합니다.

 

정리를 해보자면!

  • 과대적합은 규칙이 너무 많고 복잡해지기 때문에 민감해져 일반화가 어렵습니다.
  • 과소적합은 규칙이 적용되는 범위가 너무 넓어 모델의 신뢰도가 떨어집니다.

우리는 과소적합과 과대적합에 대한 절충점을 찾아야 할 것입니다. 보통은 데이터의 양이 데이터의 다양성을 키워주게 됩니다. 한마디로 데이터가 많을수록 더 적절하게 복잡하고 신뢰성 있는 모델을 만들 수 있게 도와준 다는 것이죠


본격적으로 지도 학습에 대하여 예제를 통하여 설명해 보도록 하겠습니다!

 

예제를 위한 데이터셋 준비 1 - forge [이진 분류]

 

먼저 아주 단순한 두 개의 특성만을 가지고 있는 forge 데이터셋을 준비해 보겠습니다. 이진 분류 데이터셋이며 산점도로 데이터의 분포를 먼저 확인해 보겠습니다.

#사용 할 라이브리 임포트하기

from IPython.display import display
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import mglearn
plt.rcParams['axes.unicode_minus'] = False


import platform
path = 'c:/Windows/Fonts/malgun.ttf'
from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
    rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc('font', family=font_name)
else:
    print('Unknown system... sorry!')
# mglearn에서 forge datasets 가져오기
# 기본은 np.darray 로 되어있는데, pandas로 데이터를 다루기 위해 변환
X, y = mglearn.datasets.make_forge()
forge_data = []
for item_X, item_y in zip(X, y):
    forge_data.append(np.append(item_X, item_y))

# forge datasets를 pandas의 데이터 프레임으로 변환
forge_df = pd.DataFrame(columns=["특성1", "특성2", "분류"], data=forge_data)
forge_df.head()

mglearn.discrete_scatter(forge_df["특성1"], forge_df["특성2"], forge_df["분류"]) #Parameter : x축 데이터, y축 데이터, 사용할 분류
plt.legend(["클래스 1", "클래스 2"], loc=4)
plt.xlabel("첫 번째 특성")
plt.ylabel("두 번째 특성")
plt.show()

print("forge 데이터셋의 특성들의 모양 : {}".format(forge_df[['특성1','특성2']].shape)) #특성의 모양 확인

forge 데이터셋의 특성들의 모양 : (26, 2)

 

DataFrame의 데이터 모양을 확인해 보니 26개의 데이터 포인트와 2개의 특성을 갖는 것을 확인할 수 있습니다. 다음은 회귀 알고리즘 설명을 위한 데이터셋입니다.


예제를 위한 데이터셋 준비 2 - wave [ 저차원 회귀 알고리즘 ]

 

마찬가지로 인위적으로 만든 데이터셋이며 입력 특성 한 개와(분석해야 할 특성)과 모델링할 타깃 변수 ( 맞춰야 할 값 - 응답 ) 하나를 갖습니다.

X, y = mglearn.datasets.make_wave(n_samples=40)

wave_data = []

for item_X, item_y in zip(X, y):
    wave_data.append(np.append(item_X, item_y))
    
# forge datasets를 pandas의 데이터 프레임으로 변환
wave_df = pd.DataFrame(columns=["입력 특성", "타깃(응답)"], data=wave_data)
wave_df.head()

plt.plot(wave_df['입력 특성'], wave_df['타깃(응답)'], 'o')
plt.ylim(-3, 3)
plt.xlabel("특성")
plt.ylabel("타깃")

Text(0,0.5, '타깃')

 

매우 매우 단순한 데이터 셋입니다. 회귀에 대해서 간단히 설명하자면

1) 입력된 특성들을 이용해

2) 타깃 값을 예측

위의 두 단계라고 볼 수 있습니다. 추후에 각종 회귀에 대해 이야기하겠지만 먼저 데모 데이터셋인 wave 데이터셋은 매우 특성이 적은 저차원 데이터셋이라고 할 수 있습니다. 단순히 2차원 공간에 표현하기 쉽기 때문에 선택하였습니다.

물론 우리가 저차원 데이터셋인 wave 데이터셋을 통해 얻어낸 인사이트가 특성이 매우 많은 고차원 데이터셋에서 그대로 유지가 되지 않을 수도 있습니다만, 각종 회귀 알고리즘을 배우기 위해서 사용할 것이다라고 생각 하시면 됩니다.

wave 같은 저차원 데이터셋을 사용해서 회기 알고리즘 공부를 하고, 이어서 실제 연구 결과로 이루어진 데이터셋도 두 가지를 사용해 보겠습니다.


예제를 위한 데이터셋 준비 3 - 위스콘신 유방암 데이터셋 breast_cancer [ 고차원 분류 알고리즘 ]

 

실제 위스콘신 대학에서 유방암 종양의 임상 데이터를 기록해 놓은 데이터 셋입니다. 타깃은 유방암 데이터셋 ( 입력 특성 )을 이용해 각 종양을 구분합니다.

breast_cancer 데이터셋은 유방암 데이터셋이 입력 특성이 되고, 이에 따른 종양의 종류(타깃)는 다음과 같이 구분됩니다.

  • benign(양성) - 해롭지 않은 종양
  • malignant(악성) - 암 종양
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()

print("cancer.keys(): \n{}".format(cancer.keys()))
cancer_features_df = pd.DataFrame(data=cancer.data, columns=cancer.feature_names)
cancer_target_df   = pd.DataFrame(data=cancer.target, columns=["result"])

cancer_features_df.head()

cancer.keys(): 
dict_keys(['data', 'target', 'target_names', 'DESCR', 'feature_names'])

 

cancer_target_df.head()

cancer_df = pd.merge(cancer_features_df, cancer_target_df, right_index=True, left_index=True)
cancer_df.head()

pandas를 이용해 데이터 프레임으로 만들어 보았습니다. 제일 마지막 칼럼인 result 칼럼은 타깃이 되고, 나머지 칼럼들이 입력 특성이 됩니다.

각 타깃 별 개수를 확인해 보도록 하겠습니다.

print("클래스 별 샘플 개수:\n{}".format({n: v for n, v in zip(cancer.target_names, np.bincount(cancer.target))}))

클래스 별 샘플 개수:
{'malignant': 212, 'benign': 357}

 

양성 종양이 357개, 악성 종양이 212개가 있네요!


예제를 위한 데이터셋 준비 4 - 보스턴 주택가격 boston, extended_boston [ 고차원 회귀 알고리즘 ]

 

위스콘신 유방암 데이터셋과 마찬가지로 1970년 보스턴 주택 가격을 범죄율, 찰스강 인접도, 고속도로 접근성 등의 정보를 이용해 예측합니다.

from sklearn.datasets import load_boston
boston = load_boston()
print("데이터의 형태 : {}".format(boston.data.shape))

boston_features_df = pd.DataFrame(data=boston.data, columns=boston.feature_names)
boston_target_df   = pd.DataFrame(data=boston.target, columns=["result"])

boston_df = pd.merge(boston_features_df, boston_target_df, right_index=True, left_index=True)
boston_df.head()

데이터의 형태 : (506, 13)

먼저 확인된 boston 데이터셋의 입력 특성의 개수는 13개, 데이터셋의 갯수는 506개가 있는 것이 확인됩니다.

하지만 위의 13개 입력 특성뿐만 아니라, 특성끼리의 상호작용이 발생하기 때문에 특성끼리 곱하여 의도적으로 확장한 데이터셋(extended_boston)을 사용할 겁니다.

예를 들어 범죄율, 고속도로의 개별 특성뿐만 아니라 범죄율과 고속도로의 접근성의 상호작용( 곱 )도 개별적인 특성으로 생각하겠다는 이야기입니다.

이것처럼 특성을 유도해 내는 것을 특성 공학(feature engineering)이라고 합니다. 특성 공학에 대해서는 추후 간단히 설명하겠습니다.

X, y = mglearn.datasets.load_extended_boston()
print("X.shape: {}".format(X.shape))

X.shape: (506, 104)

13개의 원래 특성에 2개씩 짝지은 91개의 데이터셋이 추가되어 총 104개의 데이터셋이 된 것을 확인할 수 있습니다.


앞으로 여러 머신러닝 알고리즘들의 특성들을 보여주고 설명하기 위해 위의 데이터셋들을 사용할 것입니다.

 

728x90
반응형
LIST
728x90
반응형
SMALL

이번 포스팅에서는 붓꽃의 품종 분류를 통하여 지도학습에 대하여 알아보도록 하겠습니다. 

사용 데이터는 scikit-learn 라이브러리에 있는 연습용 데모 데이터로써, 붓꽃의 품종을 꽃잎과 꽃받침의 크기에 따라서 분류해 놓은 데이터셋을 활용합니다.

가설 세우기

분석에 앞서 우리는 어떻게 데이터를 분석할지 가설을 세우는 것이 매우 중요합니다.

자, 여러분이 들판에서 붓꽃을 하나 발견했다고 가정합시다.
여러분들은 붓꽃에 대해서 잘 알지 못하기 때문에, 전문 식물학자가 측정한 데이터셋(우리가 활용할 데모 데이터셋)을 활용해야 합니다.
전문 식물학자가 측정해서 결과까지 내어 놓은 데이터셋은 데이터와 데이터에 따른 결과가 있을 것입니다.

붓꽃의 품종은 보통 setosa, versicolor, virgincia 이렇게 세 분류로 되어 있습니다.
예를 들어 꽃받침의 길이가 Xcm이고, 꽃잎의 길이가 Ycm 라면 virgincia라고 말한 것을 의미합니다.

문제 해결 하기

이러한 문제를 해결하기 위해서는 어떻게 해야 할까요?

여러분은 붓꽃의 품종 자체는 잘 알지 못하지만, 전문 식물학자가 작성해 놓은 데이터셋처럼 꽃받침, 꽃잎의 크기는 구할 수 있겠네요!
즉, 여러분이 채집한 붓꽃의 꽃받침, 꽃잎의 크기 ( 특성데이터 - feature )를 사용해 어떤 품종인지( 예측결과 - label )를 예측해야 합니다.

학습과 분류의 종류

여기서 우리가 사용할 데이터셋에는 품종을 정확하게 분류한 데이터를 가지고 있습니다. 따라서 지도 학습이라고 이야기할 수 있겠습니다.

몇 가지 선택사항( 품종 ) 중 하나를 선택해야 하는 문제입니다. 따라서 지금 예제는 분류(Classification)에 해당합니다.
여기서 출력될 수 있는 값들(여기서는 setosa, versicolor, virgincia)을 클래스(class)라고 합니다. 즉 측정한 데이터( feature )를 이용해 세 개의 클래스를 분류해야 한다고 볼 수 있겠습니다.

붓꽃 하나에 대한 기대 출력은 그 꽃의 품종이 됩니다. 이런 특정 데이터 포인트에 대해 기대할 수 있는 출력( 여러분이 채집한 꽃에 대한 품종 )을 레이블( label )이라고 합니다.

데이터를 통하여 한번 알아보도록 하겠습니다!


데이터 적재하기

scikit-learn의 datasets 모듈에 있는 iris 데이터를 불러와 봅시다.

from sklearn.datasets import load_iris
iris_dataset = load_iris() # python의 dict 클래스와 유사한 Bunch 클래스의 객체

# print(iris_dataset)<-  출력이 되는지 확인하기
print('iris_dataset의 키 : \n{}'.format(iris_dataset.keys())) # iris 데이터셋의 키 확인하기

# iris 데이터셋의 DESCR 키에는 간략한 설명이 들어있습니다.
print(iris_dataset['DESCR'][:193], "\n...")

# target_names 키는 우리가 예측할 붓꽃 품종의 이름을 문자열 배열로 가지고 있습니다.
print("타깃의 이름 : {}".format(iris_dataset['target_names']))

# feature_names 키는 각 특성을 설명하는 문자열 리스트 입니다.
print('특성의 이름 : {}'.format(iris_dataset['feature_names']))

# 실제 데이터는 target과 data 필드에 들어 있습니다.
# data는 feature 로써, 꽃받침과 꽃잎의 길이의 값이 들어 있습니다.
print('data의 타입 : {}'.format(type(iris_dataset['data'])))

# data 배열의 행은 각각 꽃에 대한 데이터입니다. 즉 4개의 측정치(꽃받침의 너비와 길이, 꽃잎의 너비와 길이)
print('data의 크기 : {}'.format(iris_dataset['data'].shape))

# data 배열의 실제 데이터를 확인 합니다. 5개의 데이터만 확인 해봅니다.
print('data의 처음 다섯 행 :\n{}'.format(iris_dataset['data'][:5]))

# target 배열은 붓꽃의 품종이 담겨 있습니다.
print('target의 타입 : {}'.format(type(iris_dataset['target'])))

print('target의 크기 : {}'.format(iris_dataset['target'].shape))

# 붓꽃의 종류는  0, 1, 2 형태의 정수로 들어 있습니다.
print('target:\n{}'.format(iris_dataset['target'])) # iris['target_names']에서 숫자의 의미를 파악 할 수 있습니다.

print('target mean : {}'.format(iris_dataset['target_names'])) # 0은 setosa, 1은 versicolor, 2는 virgincia 입니다.


신뢰할 수 있는 모델과 예측인지 확인하기

위의 데이터를 토대로 머신러닝 모델을 만들어서 위에 데이터셋에는 없는 새로운 데이터를 이용해 적용하기 전에 모델이 잘 작동하는지 검증하는 작업을 해봐야 합니다. 즉, 위의 iris_dataset이 신뢰할 수 있는 데이터 인지를 확인해봐야 하겠죠.

 

그렇다면 위의 데이터는 평가의 목적으로 사용할 수 있을까요? 애석하지만 위의 데이터는 평가용이 아닌 학습용도로 만들어 놓은 데이터 이기 때문에 iris_datasets의 데이터는 새로운 데이터로써 사용할 수 없는 상태입니다. 즉, 이미 위의 iris_datasets는 훈련에 의해 컴퓨터가 이미 다 알고 있는 데이터 이기 때문에 모든 데이터를 정확하게 맞출 수 있기 때문이죠. 이처럼 이미 컴퓨터가 데이터를 기억한다는 것은 모델의 일반화가 잘 이루어져 있지 않았다( 새로운 데이터에 대해서는 잘 작동하지 않는다)라고 이야기합니다.

모델의 성능을 측정하기 위해서는( 정확도를 구하기 위해서는 ) 새로운 데이터가 필요합니다. 새로운 데이터를 당장에 만들어 내는 것은 조금은 번거롭고 어려운 작업입니다.

측정을 잘할 수 있는 성능테스트 하기

자, 우리가 만들 모델을 측정하기 위해 가장 간단한 방법은 무엇일까요? 바로 위의 데이터셋을 두 그룹으로 적절하게 나누는 것입니다.

  • 첫 번째 세트는 머신러닝 모델을 만들 때 사용 합니다. 이를 훈련 데이터 또는 훈련 세트라고 합니다.
  • 두 번째 세트는 첫 번째 세트로 만들어낸 머신러닝 모델의 테스트 용도로써 사용합니다. 이를 테스트 데이터, 테스트 세트 또는 홀드아웃 세트라고 합니다.

scikit-learn은 데이터셋을 적절하게 섞어서 나눠주는 train_test_split 함수를 제공하고 있습니다.
이 함수의 역할은 전체 행 중 75%가량을 레이블 데이터( feature에 의해 맞춰야 할 결과 값)와 함께 훈련용 데이터( feature )로 뽑습니다.
나머지 25% 정도의 데이터셋은 테스트 세트가 됩니다. 물론 상황에 따라 조절할 수 있습니다. ( test_size 매개변수 활용 )

훈련용 데이터와 테스트 데이터 나눠내기

scikit-learn에서 데이터는 X로 표기하고, 레이블은 y로 표기합니다. 수학의 f(x) = y에서 유래하였습니다.

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(iris_dataset['data'], iris_dataset['target'], random_state = 0)

print('X_train의 크기 : {}'.format(X_train.shape))
print('y_train의 크기 : {}'.format(y_train.shape))

train_test_split을 활용하여 데이터를 나눌 때는 랜덤 하게 나눠져야 합니다. iris_dataset ['target']을 이용해 확인해봤지만, 맨 뒤에 있는 데이터는 전부다 레이블이 2로 동일합니다.

세 클래스 중 하나만 포함한 테스트 세트를 사용하면 일반화가 잘 이루어지지 않겠죠. 따라서 테스트 세트를 적절하게 잘 섞어줄 수 있도록 난수(random)를 활용합니다.

기본적으로 train_test_split 함수에서 랜덤으로 셔플링(shuffle)을 해주지만, 실행할 때마다 실행 결과가 달라질 수 있기 때문에 seed의 개념을 이용해 랜덤값을 고정시킬 수 있습니다.( random_state = 0 또는 아무거나 들어가도 됩니다!)

print('X_test 크기 : {}'.format(X_test.shape))
print('y_test 크기 : {}'.format(y_test.shape))


데이터 시각화로 살펴보기

train_test_split과 같은 메소드를 이용해서 훈련용(X_train, y_train) 데이터와 테스트용(X_test, y_test) 데이터를 나눠 보았습니다.
실제 훈련과 테스트를 해보기 이전에 신뢰할 수 있는 데이터 인지 시각화를 통해 살펴보겠습니다.

이를 통해 우리는 비정상적인 값이나 특이한 값들을 눈으로 확인해 볼 수 있을 것입니다. pandas 도표 또는 jupyter notebook에서 문자로 보는 것보다 도표를 이용해 보는 것 이기 때문에 한눈에 보기도 쉽고 간편할 것입니다.

어떻게 시각화를 할까?

여러 종류의 데이터가 분포되어 있고, 여기에 대해서 결과물이 나오고 있는 형태입니다. 이때 우리가 생각해 볼 수 있는 내용은 산점도(Scatter plot)를 활용한 방법이 좋습니다.

한 특성을 x축에 놓고, 다른 하나는 y축에 놓아 각 데이터 포인트를 하나의 점으로 나타내는 그래프입니다. 하지만 우리가 사용하고 있는 특성은 총 4가지(꽃받침, 꽃잎의 가로세로) 이기 때문에 하나의 그래프에 확인하기가 힘듭니다. 따라서 산점도 행렬(Scatter matrix)을 이용해 확인해 보도록 하겠습니다.

먼저 그래프를 그리기 전에 numpy로 만들어 놓은 배열을 Pandas의 DataFrame으로 변경해야 합니다.

import matplotlib.pyplot as plt
import pandas as pd # pandas 모듈 임포트하기
import mglearn
%matplotlib inline
# 훈련용 데이터인 X_train 데이터를 이용해 데이터 프레임을 만들어 줍니다.
# y축이 될 열의 이름은 iris_datase.feature_names에 있는 문자열을 사용하겠습니다.
iris_dataframe = pd.DataFrame(X_train, columns=iris_dataset.feature_names)

pd.plotting.scatter_matrix(iris_dataframe, c=y_train, figsize = (15, 15), marker='o',
                           hist_kwds={'bins':20}, s = 60, alpha = .8, cmap=mglearn.cm3)

plt.show()

k-최근접 이웃 알고리즘

k-NN( k-Nearest Neighbors - k-최근접 이웃 ) 알고리즘은 새로운 데이터에 대한 예측이 필요할 때 새로운 데이터와 가장 가까운 k개의 훈련된 데이터 포인트들을 찾아냅니다.
즉 k는 하나의 이웃이 아닌 복수의 이웃이라는 것이죠.

이 이웃들의 클래스 중에서 빈도가 가장 높은 클래스를 예측값으로 사용하게 됩니다.

덧붙여서 scikit-learn의 머신러닝 모델은 BaseEstimator라는 파이썬 클래스를 상속받아 구현되어 있습니다.

KNeighborsClassifier 클래스를 사용할 텐데, 이때 n_neighbors 매개변수는 이웃의 개수를 지정해 줍니다.
일단 가장 가까운 이웃만 하나 골라내기 위해서 n_neighbors를 1로 지정하겠습니다.

from sklearn.neighbors import KNeighborsClassifier #KNN 분류기
knn = KNeighborsClassifier(n_neighbors=1) # 이웃의 갯수를 1개로 강제 지정

위에서 만든 knn 객체는 훈련 데이터로 모델을 만들고 새로운 데이터 포인트에 대해 예측하는 알고리즘을 캡슐화하였습니다.

또한 알고리즘이 훈련 데이터로부터 추출한 정보까지 담아내고 있습니다. KNeighborsClassifier의 경우는 훈련 데이터 자체를 저장하고 있습니다.

훈련시키기 Fit

fit 메소드를 활용하면 드디어 모델을 만들어 낼 수 있습니다.

이때 훈련해야 할 데이터는 위에서 만들어 놓은 X_train과 y_train이 되어야겠죠?

knn.fit(X_train, y_train)

fit 메소드를 활용하면 머신러닝 알고리즘 객체 자체가 반환이 됩니다.

knn 객체를 생성할 때 n_neighbors 매개변수를 1로 설정하였는데, 결과를 보니 잘 된 것 같습니다.

매개변수의 종류가 상당히 많은데, 다음 포스트에서 바로 다뤄 보도록 하겠습니다!

데이터 신뢰도를 위한 예측

시각화를 통해 신뢰할 수 있는 데이터 인지 (각 데이터에 따라서 붓꽃의 종류가 잘 구분되었는지) 알아보았고, 알맞은 데이터라는 판단이 들어서 fit 메소드를 사용해 모델을 만들어 냈습니다.

이제 본격적으로 예측을 해볼 텐데요, 먼저 여러분이 수집한 붓꽃에 대한 데이터를 임의로 지정해 보도록 하겠습니다.

X_new 객체는 여러분이 수집한 붓꽃의 데이터가 들어있는 Numpy 배열입니다.

import numpy as np
X_new = np.array([[5,2.9,1,0.2]])
print("X_new.shape : {}".format(X_new.shape))

데이터가 잘 준비되었으면 이제 예측해 봅시다. 예측할 때는 predict 메소드를 사용합니다.

prediction = knn.predict(X_new) # 예측할 데이터인 X_new의 결과를 prediction으로 저장
print("예측: {}".format(prediction))
print("예측한 타깃의 이름: {}".format(iris_dataset['target_names'][prediction]))

잘 작동은 한 것 같습니다. 하지만 이 결과를 과연 신뢰할 수 있나요? 이때 필요한 것이 모델을 평가하는 것입니다.

앞서 X_train, y_train 말고도 X_test, y_test 데이터셋을 따로 만들어어 놓았습니다. 총데이터셋의 크기 중 25% 정도를 차지하는 이 데이터 셋들은 테스트, 즉 평가의 용도로 사용하게 됩니다.

 

여기에는 4가지 데이터와 정답이라고 할 수 있는 붓꽃의 품종까지 들어 있습니다. 우리는 이제 훈련한 머신러닝 모델과 정답이 들어있는 테스트 데이터를 이용해 예측하고, 얼마만큼 우리가 만든 모델이 붓꽃의 품종을 맞췄는지 알아볼 수 있습니다.

즉, 정확도를 계산하여 모델의 성능을 평가해 낼 수 있습니다.

y_pred = knn.predict(X_test) # 테스트 데이터셋을 넣어서 예측 하기
print("테스트 세트에 대한 예측값:\n {}".format(y_pred))

print('테스트 세트의 정확도: {:.2f}'.format(np.mean(y_pred == y_test))) # 정답이 들어있는 y_test와 knn 모델이 에측한 y_pred의 일치도

97퍼센트의 정답률이 나왔습니다. 꽤나 정확하게 붓꽃들을 구분했다고 볼 수 있습니다. 보통 정확도는 70% 이상이면 적합한 성능에 포함합니다. 물론, 높으면 높을수록 분석에 높은 확률을 주는 건 당연한 이야기겠죠~

앞으로 우리는 이렇게 신뢰할 수 있는 머신러닝 모델을 만들기 위해 성능을 높이는 방법과 모델을 튜닝할 때 주의할 점들을 알아봐야 합니다.

728x90
반응형
LIST
728x90
반응형
SMALL

이번 포스팅에서는 수치 변경에 대하여 알아보도록 하겠습니다.

 

제곱항이나 세제곱 항을 추가하면 선형 회귀 모델에 도움이 되는 것이 확인되었습니다. 한편 log, exp, sin 같은 수학 함수를 적용하는 방법도 특성 변환에 유용하게 사용됩니다.

 

트리 기반 모델은 특성의 순서에만 영향을 받지만 선형 모델과 신경망은 각 특성의 스케일과 분포에 밀접하게 연관되어 있습니다.

그리고 특성과 타깃값 사이에 비선형성이 있다면, 특히 선형 회귀에서는 모델을 만들기가 어렵습니다. log와 exp 함수는 데이터의 스케일을 변경해 선형 모델과 신경망의 성능을 올리는데 도움을 줍니다.

또한, sin, cos 함수 같은 경우는 예전에 컴퓨터 메모리 가격 데이터를 사용한 예제처럼 주기적인 패턴이 들어있는 데이터를 다룰 때 편리하게 사용할 수 있습니다.

 

대부분의 모델은 각 특성이 (회귀에서는 타깃도) 정규분포와 비슷할 때 최고의 성능을 냅니다. log와 exp 같은 수학 함수를 사용하는 것은 약간의 편법이라고 할 수 있으나, 이런 정규분포 모양을 만드는데 쉽고 효율적입니다. 이런 변환이 도움 되는 전형적인 경우는 정수 카운트 데이터를 다룰 때입니다. 예를 들어 사용자가 얼마나 자주 로그인 하는가? 같은 특성들을 의미합니다. 여기서 실제 데이터의 속성과 비슷한 카운트 데이터를 만들어 사용하겠습니다.

이 특성들은 모두 정수이며 응답은 실수입니다.

from IPython.display import display
import numpy as np
import matplotlib.pyplot as plt
import mglearn
import platform
from sklearn.model_selection import train_test_split

plt.rcParams['axes.unicode_minus'] = False
%matplotlib inline
path = 'c:/Windows/Fonts/malgun.ttf'
from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
    rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc('font', family=font_name)
else:
    print('Unknown system... sorry!')
rnd = np.random.RandomState(0)
X_org = rnd.normal(size=(1000, 3)) # 3열씩 1000개의 랜덤 숫자가 들어있는 다차원 배열로 만듦
w = rnd.normal(size=3) # 각 열별 무작위 샘플 추출하기

X = rnd.poisson(10 * np.exp(X_org)) # 포아송 분포(숫자가 적은 데이터가 더 많이 배치되기 위함임)
y = np.dot(X_org, w) # 랜덤값으로 생성된 값과 포아송 분포(X)와  가우스 분포상의 무작위 샘플과의 벡터 내적을 구함

print(X[:10, 0])

첫 번째 특성의 제일 앞을 살펴보면 모두 양의 정수이지만 특정한 패턴은 보이지 않습니다. 하지만 각 값이 나타난 횟수를 세면 그 분포가 잘 드러납니다.

print("특성 출현 횟수:\n{}".format(np.bincount(X[:, 0])))

2가 68번으로 가장 많이 나타나며 큰 값의 수는 빠르게 줄어듭니다. 그러나 85나 86처럼 아주 큰 값도 약간은 있습니다. 그래프로 확인해 보겠습니다.

bins = np.bincount(X[:, 0])
plt.bar(range(len(bins)), bins, color='gray')
plt.ylabel('출현 횟수')
plt.xlabel('값')
plt.show()

X [:, 1]과 X [:,2] 특성도 비슷합니다. 이런 종류의 분포는 작은 수치가 많고 큰 수치는 몇 안 되는 실제 자주 나타나는 데이터 분포입니다. 그러나 선형 모델은 이런 데이터를 잘 처리하지 못합니다. Ridge regression로 학습시켜 보겠습니다.

from sklearn.linear_model import Ridge
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
score = Ridge().fit(X_train, y_train).score(X_test, y_test)
print("테스트 점수: {:.3f}".format(score))

낮은 R^2 점수가 나왔습니다. Ridge는 X와 y의 관계를 제대로 모델링하지 못했습니다. 하지만 로그스케일로 변환하면 많은 도움이 됩니다. 데이터에 0이 있으면 log 함수를 적용할 수가 없기 때문에 log(X + 1)을 사용합니다.

X_train_log = np.log(X_train + 1)
X_test_log  = np.log(X_test + 1)

변환 후를 살펴보면 데이터의 분포가 덜 치우쳐 있으며 매우 큰 값을 가진 이상치가 보이지 않습니다.

plt.hist(X_train_log[:, 0], bins=25, color='gray')
plt.ylabel('출현 횟수')
plt.xlabel('값')
plt.show()

이 데이터에 Ridge 모델을 만들면 훨씬 좋은 결과가 등장합니다.

score = Ridge().fit(X_train_log, y_train).score(X_test_log, y_test)
print("테스트 점수: {:.3f}".format(score))

이런 방법이 항상 들어맞는 것은 아닙니다. 모든 특성이 같은 속성을 가지고 있었기 때문에 이 예제는 잘 들어맞았지만, 항상 그런 것은 아닙니다. 따라서 일부 특성만 변환하거나 특성마다 모두 다르게 변환시키기도 합니다.

트리 모델에서는 이러한 변환자체가 불필요하지만 선형 모델에서는 필수입니다. 가끔 회귀에서 타깃 변수 y를 변환하는 것이 좋을 때도 있습니다. 카운트를 예측하는 경우가 전형적인 예로 log(y + 1)를 사용해 변환하면 도움이 많이 됩니다.

언제 사용해야 하는가?

구간 분할, 다항식, 상호작용은 데이터가 주어진 상황에서 모델의 성능에 큰 영향을 줄 수 있습니다. 특별히 선형 모델이나 나이브 베이즈 모델 같은 덜 복잡한 모델일 경우입니다.

반면에 트리 기반 모델은 스스로 중요한 상호작용을 찾아낼 수 있고 대부분의 경우 데이터를 명시적으로 변환하지 않아도 됩니다. SVM, k-NN, 신경망 같은 모델은 가끔 구간분할, 상호작용, 다항식으로 이득을 볼 수 있지만 선형모델보다는 영향이 그렇게 뚜렷하지는 않습니다.

지금까지 모델 성능 향상을 위한 수치 변경에 대하여 알아보았습니다.

728x90
반응형
LIST
728x90
반응형
SMALL

이번 포스팅에서는 일변량 분석과 모델 특성 분석에 대하여 알아보도록 하겠습니다. 

새로운 특성을 만들어 내는 방법은 많기 때문에 데이터의 차원이 원본 특성의 수 이상으로 증가하기가 쉽습니다. 하지만 특성이 추가된다는 이야기는 곧 모델이 복잡해진다는 이야기이고, 과대적합이 될 가능성이 커지게 됩니다.

보통 고차원 데이터셋을 사용할 때, 가장 유용한 특성만 선택하고 나머지는 무시해서 특성의 수를 줄이는 것이 좋습니다. 즉 모델의 복잡도를 줄이고 일반화 성능을 끌어올릴 수 있다는 이야기가 됩니다.

특성을 자동으로 선택하기 위한 방법은 총 3가지가 있습니다.

  • 일변량 통계 unvariable statistics
  • 모델 기반 선택 model-based selection
  • 반복적 선택 iterative selection

위 방법은 모두 지도학습 방법이므로 최적값을 찾기 위해서는 타깃이 필요합니다.

일변량 통계

일변량 통계에서는 개개의 특성과 타깃 사이에 중요한 통계적 관계가 있는지를 계산합니다. 그러고 나서 깊이 연관되어 있는 특성을 선택하게 됩니다. 분류에서는 분산 분석 이라고도 합니다.

이 방법의 핵심 요소는 일변량, 즉 각 특성이 독립적으로 평가된다는 점입니다. 따라서 다른 특성과 깊게 연관된 특성은 선택되지 않을 것입니다. 일변량 분석은 계산이 매우 빠르고 평가를 위해 모델을 만들 필요가 없습니다. 한편으로 이 방식은 특성을 선택한 후 적용하려는 모델이 무엇인지 상관하지 않고 사용할 수 있습니다.

scikit-learn에서 일변량 분석으로 특성을 선택하려면 분류에서는 f_classif(기본값)을 사용하고, 회귀에서는 f_regression을 보통 선택하여 테스트하고, 계산한 p-값( 특성이 선택되었을 때 그 특성을 지지하는 확률 )에 기초하여 특성을 제외하는 방식을 선택합니다.

이런 방식들은 타깃값과 연관성이 작을 것 같다는 뜻으로 받아들일 수 있는 매우 높은 p-값을 가진 특성을 제외할 수 있도록 임계값을 조정하는 매개변수를 사용합니다.

임계값을 계산하는 방법은 총 두 가지입니다.

  • SelectKBest - 고정된 K개의 특성을 선택
  • SelectPercentile - 지정된 비율만큼 특성을 선택

cancer 데이터셋의 분류를 위한 특성 선택을 적용시켜 보겠습니다. 의미가 없는 노이즈 특성(예측에 아무 쓸모없는 특성)을 추가하여 특선 선택이 의미 없는 특성을 식별해서 제거하는지 보도록 하겠습니다.

from IPython.display import display
import numpy as np
import matplotlib.pyplot as plt
import mglearn
import platform
from sklearn.model_selection import train_test_split

plt.rcParams['axes.unicode_minus'] = False
%matplotlib inline
path = 'c:/Windows/Fonts/malgun.ttf'
from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
    rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc('font', family=font_name)
else:
    print('Unknown system... sorry~~~~~')


from sklearn.datasets import load_breast_cancer
from sklearn.feature_selection import SelectPercentile

cancer = load_breast_cancer()

#난수 발생
rng = np.random.RandomState(42)
noise = rng.normal(size=(len((cancer.data)), 50))

# 위에서 발생시킨 랜덤 데이터를 노이즈 특성으로 사용, 처음 30개는 원본 특성, 다음 50개는 노이즈
X_w_noise = np.hstack([cancer.data, noise])

X_train, X_test, y_train, y_test = train_test_split(X_w_noise, cancer.target, random_state=0, test_size=.5)
# f_classif와 SelectPercentile을 이용하여 특성의 50%를 선택하기
select = SelectPercentile(percentile=50)
select.fit(X_train, y_train)

#  훈련세트에 적용하기
X_train_selected = select.transform(X_train)

print("X_train.shape: {}".format(X_train.shape))
print("X_train_selected.shape: {}".format(X_train_selected.shape))

특성의 개수가 원래 80개에서 50% 수준인 40개로 줄어든 것이 확인됩니다. get_support 메소드는 선택된 특성을 불리언 값으로 표시해 주어 어떤 특성이 선택되었는지 확인할 수 있습니다.

mask = select.get_support()
print(mask)

# True는 검은색, False는 흰색으로 마스킹
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("특성 번호")
plt.yticks([0])
plt.show()

마스킹된 그래프를 보면 제거된 특성(흰색)은 대부분 노이즈 특성이고, 선택된 특성은 대부분 원본 특성입니다. 전체 특성을 이용했을 때와 선택된 특성만을 사용했을 때의 차이점을 보겠습니다.

from sklearn.linear_model import LogisticRegression

# 테스트 데이터 변환
X_test_selected = select.transform(X_test)

lr = LogisticRegression()
lr.fit(X_train, y_train)
print("전체 특성을 사용한 점수: {:.3f}".format(lr.score(X_test, y_test)))

lr.fit(X_train_selected, y_train)
print("선택된 일부 특성을 사용한 점수 : {:.3f}".format(lr.score(X_test_selected, y_test)))

이 경우에서는 원본 특성이 몇 개 없더라도 노이즈 특성을 제거한 쪽의 성능이 더 좋다는 것을 확인할 수 있습니다. 이 예는 인위적으로 간단하게 만든 예제이고, 엇갈리는 경우도 많습니다.

하지만 너무 많은 특성 때문에 모델을 만들기가 현실적으로 어려울 때 일변량 분석을 사용하여 특성을 선택하면 큰 도움이 될 수도 있습니다. 또는 많은 특성들이 확실히 도움이 안 된다고 생각될 때 사용하면 좋습니다.

모델 기반 특성 선택

모델 기반 특성 선택은 지도 학습 머신러닝 모델을 사용하여 특성의 중요도를 평가해서 가장 중요한 특성만 선택합니다. 특성 선택에 사용하는 지도 학습 모델은 최종적으로 사용할 지도 학습 모델과 같을 필요는 없습니다. 단지 특성 선택을 위한 모델은 각 특성의 중요도를 측정하여 순서를 매길 수 있어야 합니다.

결정 트리와 이를 기반으로 한 모델은 각 특성의 중요도가 담겨있는 featureimportance 속성을 사용합니다. 선형 모델 계수의 절댓값도 특성의 중요도를 재는 데 사용할 수 있습니다.

L1 규제를 사용한 선형 모델은 일부 특성의 계수만 학습에 사용됩니다. 이를 그 모델 자체를 위해 특성이 선택된다고도 볼 수 있지만, 다른 모델의 특성 선택을 위해 전처리 단계로도 활용할 수가 있습니다.

사용된 모델이 특성 간의 상호작용을 잡아낼 수 있다면 일변량 분석과는 반대로 모델 기반 특성 선택은 한 번에 모든 특성을 고려하므로 상호작용 부분을 반영할 수 있습니다.

모델 기반 특성 선택은 SelectFromModel에 구현되어 있습니다.

from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier

select = SelectFromModel(RandomForestClassifier(n_estimators=100, random_state=42), threshold='median')

L1규제가 없는 모델을 사용할 경우 SelectFromModel은 지도 학습 모델로 계산된 중요도가 지정한 임계치(threshold 매개변수의 기본값인 mean) 보다 큰 모든 특성을 선택합니다.

위에서 만든 SelectFromModel은 일변량 분석과 비교하기 위해 동일하게 절반의 특성을 선택하기 위해 중간값인 median을 사용하였습니다. 트리 100개로 만든 RandomForest 모형을 사용해 특성 중요도를 계산하게 됩니다.

이는 매우 복잡한 모델이고 일변량 분석보다는 훨씬 강력한 방법입니다.

select.fit(X_train, y_train)
X_train_l1 = select.transform(X_train)

print("X_train.shape : {}".format(X_train.shape))
print("X_train_l1.shape : {}".format(X_train_l1.shape))

이어서 선택된 특성을 일변량 통계와 같은 방식으로 그려보겠습니다.

mask = select.get_support()
# True는 검은색, False는 흰색으로 마스킹
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("특성 번호")
plt.yticks([0])
plt.show()

이번에는 두 개를 제외한 모든 원본 특성이 선택되었습니다. 특성을 40개 선택하도록 지정했기 때문에 일부 노이즈 특성까지 선택되었습니다. 성능을 확인해 보겠습니다.

X_test_l1 = select.transform(X_test)
score = LogisticRegression().fit(X_train_l1, y_train).score(X_test_l1, y_test)
print("테스트 점수: {:.3f}".format(score))

일변량 통계를 사용했을 때보다 점수가 약간 좋아졌습니다.

반복적 특성 선택

일변량 분석에서는 모델을 사용하지 않고, 모델 기반 선택에서는 하나의 모델을 사용해 특성을 선택하였습니다.

반복적 특성 선택 Iterative Feature Selection에서는 특성의 수가 각기 다른 일련의 모델이 생성됩니다. 기본적으로 두 가지 방법이 있습니다.

  • 특성을 하나도 선택하지 않은 상태로 어떠한 종료 조건에 도달할 때까지 특성을 하나씩 추가하는 방법
  • 모든 특성을 가지고 시작하여 어떤 종료 조건이 될 때까지 특성을 하나씩 제거해 가는 방법

위 방법은 일단 모델이 만들어지고 시작하기 때문에 일변량 분석과 모델 기반 선택보다 계산 비용이 훨씬 많이 듭니다. 재귀적 특성 제거 (RFE, recursive feature elimination)가 이런 방법의 하나입니다. 이 방법은 모든 특성으로 시작해서 모델을 만들고 특성 중요도가 가장 낮은 특성을 제거합니다. 그런 다음 제거한 특성을 빼고 나머지 특성을 전체로 새로운 모델을 만들어 냅니다.

이런 식으로 미리 정의한 특성 개수가 남을 때까지 반복하게 됩니다. 이를 위해 모델 기반 선택에서처럼 특성 선택에 사용할 모델은 특성의 중요도를 결정하는 방법을 제공해야 합니다.

from sklearn.feature_selection import RFE
select = RFE(RandomForestClassifier(n_estimators=100, random_state=42), n_features_to_select=40)

select.fit(X_train, y_train)
#선택한 특성을 표시
mask = select.get_support()
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("특성 번호")
plt.yticks([0])
plt.show()

일변량 분석과 모델 분석에 비해 특성을 선택하는 것은 나아졌지만, 여전히 특성 한 개를 놓치긴 했습니다. 랜덤 포레스트 모델은 특성이 누락될 때마다( 특성이 하나씩 제거될 때마다 ) 다시 학습을 하기 때문에 시간이 조금 걸립니다. RFE를 사용해서 특성을 선택했을 때 로지스틱 회귀의 정확도를 보겠습니다.

X_train_rfe = select.transform(X_train)
X_test_rfe = select.transform(X_test)

score = LogisticRegression().fit(X_train_rfe, y_train).score(X_test_rfe, y_test)
print("테스트 점수 : {:.3f}".format(score))

또한 RFE에 사용된 모델을 이용해서도 예측을 수행할 수 있습니다. 이 경우에는 선택된 특성만 사용됩니다.

print("테스트 점수: {:.3f}".format(select.score(X_test, y_test)))

RFE 안에 있는 랜덤 포레스트의 성능이 이 모델에서 선택한 특성으로 만든 로지스틱 회귀의 성능과 같습니다. 즉 특성 선택이 제대로만 된다면 선형 모델의 성능은 랜덤 포레스트와 견줄 수 있다는 이야기입니다.

머신러닝 알고리즘에 어떤 입력값을 넣을지 확신이 안 선다면 특성 자동 선택이 도움이 될 수는 있습니다. 예측 속도를 높이거나 해석하기 더 쉬운 모델을 만드는 데 필요한 만큼 특성의 수를 줄이는데도 굉장히 효과적입니다.

대부분 실전에서는 특성 선택이 큰 성능 향상을 끌어내지는 못하나 머신러닝 엔지니어에게는 매우 중요한 도구입니다.

728x90
반응형
LIST
728x90
반응형
SMALL

이번 포스팅에서는 데이터 특성을 표현하기 위한 구간 분할(bining) or 이산화, 그리고 상호작용과 다항식에 대하여 알아보도록 하겠습니다.

아주 폭넓게 사용하는 선형 모델과 트리 기반 모델들은 특성의 표현 방식으로 인해 미치는 영향이 매우 다릅니다. "파이썬 라이브러리를 활용한 머신러닝"책 2장에서 사용된 wave 데이터셋을 사용하겠습니다. 이 데이터셋은 입력 특성이 하나뿐입니다. 이 데이터셋을 이용해 선형 회귀모델과 결정 트리 회귀를 비교해 보겠습니다.

from IPython.display import display
import numpy as np
import matplotlib.pyplot as plt
import mglearn
import platform
plt.rcParams['axes.unicode_minus'] = False
%matplotlib inline
path = 'c:/Windows/Fonts/malgun.ttf'
from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
    rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc('font', family=font_name)
else:
    print('Unknown system... sorry~~~~~')

from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor

X, y = mglearn.datasets.make_wave(n_samples=100)
line = np.linspace(-3, 3, 1000, endpoint=False).reshape(-1, 1)

reg = DecisionTreeRegressor(min_samples_split=3).fit(X, y)
plt.plot(line, reg.predict(line), label="결정 트리")

reg = LinearRegression().fit(X, y)
plt.plot(line, reg.predict(line), '--' , label="선형 회귀")

plt.plot(X[:, 0], y, 'o', c='k')
plt.ylabel("회귀 출력")
plt.xlabel("입력 특성")
plt.legend(loc='best')
plt.show()

plt. show()

구간 분할

선형 모델은 선형 관계로만 모델링하므로 특성이 하나일 땐 직선으로 나타냅니다. 결정 트리는 이 데이터로 훨씬 더 복잡한 모델을 만들어 내는 것이 확인됩니다.

그러나 이는 데이터 표현 방식에 따라 굉장히 달라지게 되는데, 연속형 데이터에 아주 강력한 선형 모델을 만드는 방법 중 하나는 한 특성을 여러 특성으로 나누는 구간 분할 bining입니다.(이산화라고도 합니다.)

 

np.linspace로 만들어낸 입력값 범위( -3 ~ 3 )가 일정하기 나뉘어 여러 구간으로, 예를 들면 10개로 되어 있다고 생각해 보겠습니다. 그럼 각 데이터 포인트가 어떤 구간에 속하는지로 나타낼 수 있습니다. 이렇게 하려면 먼저 구간을 정해야 합니다. -3과 3 사이에 일정한 간격으로 10개의 구간을 정의하겠습니다.

np.linspace 함수를 이용해 11개의 지점을 만들어 10개 구간을 만들겠습니다.

bins = np.linspace(-3, 3, 11)
print("구간: {}".format(bins))

첫 번째 구간은 -3부터 -2.4 사이의 데이터 값을 담습니다. 두 번째 구가는 -2.4부터 -1.8 사이인 모든 데이터 포인트를 포함합니다.

그다음 각 데이터 포인트가 어느 구간에 속하는지 기록합니다. np.digitize 함수를 사용하면 간단하게 계산할 수 있습니다.

which_bin = np.digitize(X, bins=bins)
print('\n데이터 포인트:\n', X[:5])
print('\n데이터 포인트의 소속구간:\n', which_bin[:5])

위의 코드를 이용해 wave 데이터셋에 있는 연속형 특성을 각 데이터 포인트가 어느 구간에 속했는지로 인코딩한 범주형 특성으로 바뀌었습니다. 이 데이터에 scikit-learn의 preprocessing 모듈에 있는 OneHotEncoder로 이산적인 이 특성을 원-핫-인코딩으로 변환하겠습니다.

 

OneHotEncoder는 pandas.get_dummies와 같지만 현재는 숫자로 된 범주형 변수에만 적용시킬 수 있습니다.

from sklearn.preprocessing import OneHotEncoder

#OneHotEncoder를 사용
encoder = OneHotEncoder(sparse=False) # 희소행렬 형태로 나타내지 않음(각 아이템의 위치값을 나타내지 않음)

#encoder.fit은 which_bin에 나타낸 유일한 값을 찾습니다.
encoder.fit(which_bin)

#One-hot-encoding으로 변환
X_binned = encoder.transform(which_bin)
print(X_binned[:5])

구간이 10개로 구성되었기 때문에 변환된 데이터셋 X_binned는 10개의 특성으로 구성됩니다.

print("X_binned.shape: {}".format(X_binned.shape))

One-Hot-Encoding 된 데이터로 선형 회귀 모델과 결정 트리모델을 다시 만들어 표현해 보겠습니다.

line_binned = encoder.transform(np.digitize(line, bins=bins))

reg = LinearRegression().fit(X_binned, y)
plt.plot(line, reg.predict(line_binned), label='구간 선형 회귀')

reg = DecisionTreeRegressor(min_samples_split=3).fit(X_binned, y)
plt.plot(line, reg.predict(line_binned), '--', label='구간 결정 트리')
plt.plot(X[:, 0], y, 'o', c='k')

plt.vlines(bins, -3, 3, linewidth=1, alpha=.2)
plt.legend(loc='best')
plt.ylabel('회귀 출력')
plt.xlabel('입력 특성')
plt.show()

선형 회귀와 결정트리가 완벽하게 겹쳐져 있는 것이 확인됩니다. 구간별로 이 두 모델이 예측한 것은 상숫값입니다. 즉 "이 구간에서의 값은 이거야"라고 하는 것과 같습니다. 따라서 각 구간 안에서는 특성의 값이 상수이므로, 어떤 모델이든 그 구간의 포인트에 대해서는 같은 값을 예측할 것입니다.

 

구간으로 나눈 특성을 사용하기 전과 비교해 보면, 결정 트리는 기존보다 모델이 단순해졌고, 선형 모델은 조금 복잡해진 것으로 확인됩니다.

트리 모델은 데이터를 애초에 자유롭게 나눠서 학습하기 때문에 특성의 값을 구간으로 나누는 것이 별 도움은 되지 않습니다. 다르게 생각해 보면 결정 트리는 데이터셋에서 예측을 위한 가장 좋은 구간을 학습한다고 볼 수 있습니다. 거기다가 구간 나누기는 특성마다 따로 해야 하지만, 결정 트리는 한 번에 여러 특성을 살펴볼 수 있습니다. 하지만 선형 모델은 구간 나누기를 통해 큰 이득을 보았습니다.

 

일부 특성과 출력이 비선형 관계이지만, 용량이 매우 크고 고차원 데이터셋이라 선형 모델을 사용해야 한다면 구간 분할이 모델 성능을 높이는데 아주 좋은 방법이 될 수 있습니다.

상호작용과 다항식

특별히 특성을 다양하게 나타내게 하는 방법은 원본 데이터에 상호작용 interaction과 다항식 polynomial을 추가하는 방법입니다. 이런 종류의 특성 공학은 통계적 모델링에서 자주 사용하지만 일반적인 머신러닝 애플리케이션에서도 많이 사용됩니다.

구간 분할을 통해 배웠었던 내용 중 선형 모델이 wave 데이터셋의 각 구간에 대해 상숫값을 학습한 것이 확인되었습니다. 그런데 선형모델은 이러한 절편만 학습하는 것이 아닌 구간별로 기울기도 학습할 수 있습니다.

선형 모델에 기울기를 추가하는 방법은 구간으로 분할된 데이터에 원래 특성을 그대로 다시 추가하는 것입니다. 이렇게 하면 11차원 (10개 구간에 대한 특성을 새로이 추가) 데이터셋이 만들어지게 됩니다.

X_combined = np.hstack([X, X_binned]) # hstack을 이용해 구간별 One-hot-encoding된 데이터 추가
print(X_combined.shape)

reg = LinearRegression().fit(X_combined, y)

line_combined = np.hstack([line, line_binned]) # 예측할 선에 대해서도 One-Hot-Encoding된 데이터를 추가함
plt.plot(line, reg.predict(line_combined), label='원본 특성을 더한 선형 회귀')

for bin in bins:
    plt.plot([bin, bin], [-3, 3], ':', c='k', linewidth=1) #x축은 각 구간(bin, bin), y축은 -3 부터 3까지 표현

plt.legend(loc='best')
plt.ylabel('회귀 출력')
plt.xlabel('입력 특성')
plt.plot(X[:, 0], y, 'o', c='k')

이 모델은 각 구간의 절편과 기울기를 학습하였습니다. 학습한 기울기는 음수이고, 모든 구간에 걸쳐서 모든 기울기가 동일합니다. 즉 x축 특성이 하나이므로 기울기도 하나입니다.

기울기가 모든 구간에서 동일하다 보니 별로 유용해 보이지는 않습니다. 오히려 각 구간에서 다른 기울기는 가지는 게 좋을 것 같네요.

 

이런 효과를 위해서 데이터 포인트가 있는 구간과 x 축 사이의 상호작용 특성을 추가할 수도 있습니다. 이 특성이 구간 특성과 원본 특성의 곱입니다.

X_product = np.hstack([X_binned, X * X_binned]) # 인코딩된 구간데이터와, 구간과 원본 특성의 곱을 구한 데이터를 추가적으로 합침
print(X_product.shape)

위 데이터셋은 이제 데이터 포인트가 속한 구간과 이 구간에 원본 특성을 곱한 값을 더해 총 20개의 특성을 가지게 되었습니다. 이 곱셈 특성을 각 구간에 대한 x축 특성의 복사본이라고 생각할 수 있습니다. 즉 이 값은 구간 안에서는 원본 특성이고 다른 곳에서는 0입니다.

 

새롭게 만들어낸 데이터 포인트를 이용해 선형 모델을 적용시켜 보겠습니다.

reg = LinearRegression().fit(X_product, y)

line_product = np.hstack([line_binned, line * line_binned]) # 예측해야 할 데이터도 훈련한 데이터와 같이 상호작용을 구함
plt.plot(line, reg.predict(line_product), label='원본 특성을 곱한 선형 회귀')

for bin in bins:
    plt.plot([bin, bin], [-3, 3], ':', c='k', linewidth=1)

plt.plot(X[:, 0], y, 'o', c='k')
plt.ylabel('회귀 출력')
plt.xlabel('입력 특성')
plt.legend(loc='best')
plt.show()

원본 특성의 다항식 추가하기

위의 그림에서 알아볼 수 있듯이 이 모델에서는 각 구간에서의 절편과 기울기가 모두 달라지게 되었습니다.

구간 나누기는 연속형 특성을 확장하는 방법 중 하나입니다. 원본 특성의 다항식을 추가하는 방법도 있습니다. 예를 들어 특성 x가 주어지면 이 특성에 대한 제곱값, 세제곱값, 네 제곱값 등을 새롭게 추가하는 것을 시도해 볼 수 있습니다. preprocessing 모듈의 PolynomialFeatures에 구현되어 있습니다.

from sklearn.preprocessing import PolynomialFeatures
# x ** 10 까지의 고차항을 추가합니다.
# 기본값인 "include_bias=True"는 절편을 위해 값이 1인 특성을 추가합니다.
poly = PolynomialFeatures(degree=10, include_bias=False)
poly.fit(X)

X_poly = poly.transform(X)

10차원을 사용했기 때문에 10개의 특성이 새로 만들어집니다. include_bias를 True로 설정하면 절편까지 고려하여 11개의 특성이 만들어집니다.

print('X_poly.shape: {}'.format(X_poly.shape))

X와 X_poly의 값을 비교해 보겠습니다.

print('X 원소:\n{}'.format(X[:5]))
print('X_poly 원소:\n{}'.format(X_poly[:5]))

각 특성의 차수를 알려주는 get_features_names 메소드를 사용해 특성의 의미를 파악할 수 있습니다.

print('항 이름:\n{}'.format(poly.get_feature_names()))

X_poly의 첫 번째 열은 X와 같고 다른 열은 첫 번째 열의 각 거듭제곱입니다. 그래서 어떤 값은 매우 크게 나오는 것이 확인됩니다.

다항식 특성을 선형 모델과 함께 사용하면 전형적인 다항 회귀 polynormial regression 모델이 됩니다.

reg = LinearRegression().fit(X_poly, y)

line_poly = poly.transform(line)

plt.plot(line, reg.predict(line_poly), label='다항 선형 회귀')
plt.plot(X[:, 0], y, 'o', c='k')
plt.ylabel('회귀 출력')
plt.xlabel('입력 특성')
plt.legend(loc='best')
plt.show()

다항식 특성은 1차원 데이터셋임에도 불구하고 매우 부드러운 곡선을 만들어 내는 것이 확인됩니다. 그러나 고차원 다항식은 데이터가 부족한 영역에서 너무 민감하게 작용하여 기울기가 급격하게 바뀌는 것이 확인됩니다.

비교를 위해 커널 SVM과 비교해 보겠습니다. 아무런 변환도 거치지 않은 원본 데이터를 학습시켜 보겠습니다.

from sklearn.svm import SVR

for gamma in [1, 10]:
    svr = SVR(gamma=gamma).fit(X, y)
    plt.plot(line, svr.predict(line), label='SVR gamma={}'.format(gamma))

plt.plot(X[:, 0], y, 'o', c='k')
plt.ylabel('회귀 출력')
plt.xlabel('입력 특성')
plt.legend(loc='best')
plt.show()

비교적 훨씬 복잡한 커널 SVM을 사용해 특성 데이터를 변환하지 않고 다항 회귀와 비슷한 복잡도를 가진 예측을 만들어 냈습니다.

 

조금 더 현실적인 차이를 보기 위해 보스턴 주택가격 데이터셋을 이용해 보도럭 하겠습니다. 이때 사용할 데이터셋은 확장된 형태의 데이터셋이 아닌, 어떠한 특성 공학도 들어가지 않은 데이터셋을 사용해 보겠습니다.

from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler

boston = load_boston()

X_train, X_test, y_train, y_test = train_test_split(boston.data, boston.target, random_state=0)

# 데이터 스케일 조정하기
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

이어서 차수를 2로 하여 다항식 특성을 마련합니다.

poly = PolynomialFeatures(degree=2).fit(X_train_scaled)
X_train_poly = poly.transform(X_train_scaled)
X_test_poly  = poly.transform(X_test_scaled)

print('X_train.shape: {}'.format(X_train.shape))
print('X_train_poly.shape: {}'.format(X_train_poly.shape))

이 데이터에는 원래 특성이 13개인데 105개의 교차 특성으로 확장되었습니다. 새로운 특성은 원래 특성의 제곱은 물론 가능한 두 토성의 조합을 모두 포함합니다. 즉 degree=2로 하면 원본 특성에서 두 개를 뽑아 만들 수 있는 모든 곱을 얻어 낼 수 있습니다.

 

어떤 원본 특성이 곱해져 새 특성이 만들어졌는지 확인하기 위해 get_feature_names 메소드를 사용해 보겠습니다.

print("다항 특성 이름:\n{}".format(poly.get_feature_names()))

첫 번째 특성은 상수항으로써 단순히 일정한 절편을 나타내기 위해 추가된 항입니다. 그다음 13개 특성은 원본특성입니다. 그다음은 원본 특성의 제곱항과 첫 번째 특성과 다른 특성 간의 조합입니다.

 

상호작용 특성이 있는 데이터와 없는 데이터에 대해 Ridge를 사용해 성능을 비교해 보겠습니다.

from sklearn.linear_model import Ridge

ridge = Ridge().fit(X_train_scaled, y_train)
print('상호작용 특성이 없을 때 점수: {:.3f}'.format(ridge.score(X_test_scaled, y_test)))
ridge = Ridge().fit(X_train_poly, y_train)
print('상호작용 특성이 있을 때 점수: {:.3f}'.format(ridge.score(X_test_poly, y_test)))

상호작용 특성이 있을 때 Ridge의 성능을 크게 높인 것이 확인됩니다. 하지만 랜덤 포레스트 같이 더 복잡한 모델을 사용하면 이야기가 달라집니다.

from sklearn.ensemble import RandomForestRegressor
rf = RandomForestRegressor(n_estimators=100, random_state=0).fit(X_train_scaled, y_train)
print("상호작용 특성이 없을 때 점수 : {:.3f}".format(rf.score(X_test_scaled, y_test)))

rf = RandomForestRegressor(n_estimators=100, random_state=0).fit(X_train_poly, y_train)
print("상호작용 특성이 있을 때 점수 : {:.3f}".format(rf.score(X_test_poly, y_test)))

특성을 추가하지 않아도 랜덤 포레스트는 Ridge의 성능과 비슷합니다. 오히려 상호작용과 다항식을 추가하면 성능이 줄어듭니다.

 

지금까지 데이터 특성을 표현하기 위한 구간 분할(bining) or 이산화, 그리고 상호작용과 다항식 표현 방법을 통하여 데이터의 전처리하는 방법을 알아보며 모델 성능을 높이는 과정을 분석해 보았습니다!

다음 포스팅에서는 일변량 통계에 대하여 알아보도록 하겠습니다.

728x90
반응형
LIST
728x90
반응형
SMALL

지난 포스팅에서 정규화의 중요성을 다뤘다면, 이번에는 연속형 특성과 범주형 특성에 대하여 알아보도록 하겠습니다.

연속형 특성과 범주형 특성이란?

지금까지 우리가 살펴보고, 사용했던 데이터는 2차원 실수형 배열로 각 열이 데이터 포인트를 설명하는 연속형 특성 - continuous feature을 살펴보았습니다.

하지만 우리가 수집하는 모든 데이터들이 연속형 특성을 띄고 있다고는 장담할 수 없습니다. 일반적인 특성의 전형적인 형태는 범주형 특성 - categorical feature입니다. 또는 이산형 특성 - discrete feature라고도 합니다.

 

이러한 범주형 특성, 이산형 특성들은 보통 숫자 값이 아닙니다. 연속적 특성의 예로 들 수 있는 것은 픽셀 밝기나 붓꽃의 측정값 등을 생각해 볼 수 있고, 범주형 특성은 옷의 브랜드, 색상, 상품 분류 등등이 있습니다. 이러한 특성들은 어떠한 상품을 묘사할 수 있는 특성이긴 하지만, 연속된 값으로 나타낼 수는 없습니다. (어디에 속하는 범주의 의미한다는 이야기)

범주형 특성의 특징은 뭔가 비교를 할 수 없다는 것입니다. 예를 들어 책은 옷보다 크거나 작지 않고, 청바지는 책과 옷 사이에 있지 않죠.

특성 종류 특성 형태
연속형 특성 (양적 - Quantitative): 몸무게, 매출액, 주가 등 5.11121
범주형 특성 (질적 - Qualitative): 성별, 지역, 만족도 등 남/여, 상/중/하

 

 

특성 공학을 이용한 데이터 표현의 중요성

데이터가 어떤 형태의 특성으로 구성되어 있는가 보다 (연속형인지, 범주형인지) 데이터를 어떻게 표현하는지가 머신러닝 모델의 성능에 영향을 더 많이 줍니다. 일전에 했었던 데이터 스케일랑 작업 같은 것들을 의미하는데요, 예를 들어 측정치가 센티미터인지, 인치로 측정을 했는지에 따라서 머신러닝 모델이 인식하는 데에 차이가 생기기 시작합니다.

또는 확장된 보스턴 데이터셋처럼 각 특성의 상호작용( 특성 간의 곱 )이나 일반적인 다항식을 추가 특성으로 넣는 것이 도움이 될 때도 있습니다.

이처럼 특성 애플리케이션에 가장 적합한 데이터 표현을 찾는 것을 특성 공학 - feature engineering이라고 합니다. 데이터 분석을 할 때 데이터 과학자와 머신러닝 기술자가 실제 문제를 풀기 위해 당면하는 주요 작업 중 하나입니다.

올바른 데이터 표현은 지도 학습 모델에서 적절한 매개변수를 선택하는 것보다 성능에 더 큰 영향을 미칠 때가 많습니다.

 

 

범주형 변수

범주형 변수를 알아보기 위해 예제 데이터셋을 판다스로 불러와서 사용해 보도록 하겠습니다. 1994년 인구조사 데이터베이스에서 추출한 미국 성인의 소득 데이터셋의 일부입니다. adult 데이터셋을 사용해 어떤 근로자의 수입이 50,000 달러를 초과하는지, 이하일지 예측하는 모델을 만드려고 합니다.

import pandas as pd

data = pd.read_csv('./data/adult.csv', encoding='utf-8')
display(data)

Result of data_set

위 데이터셋은 소득(income)이 <=50와 > 50K라는 두 클래스를 가진 분류 문제로 생각해 볼 수 있습니다. 정확한 소득을 예측해 볼 수도 있겠지만, 그것은 회귀 문제가 됩니다.

어찌 됐든 이 데이터셋에 있는 age와 hours-per-week는 우리가 다뤄봤었던 연속형 특성입니다. 하지만 workclass, education, gender, occupation은 범주형 특성입니다. 따라서 이런 특성들은 어떤 범위가 아닌 고정된 목록 중 하나를 값으로 가지며, 정량적이 아니고 정성적인 속성입니다.

 

맨 먼저 이 데이터에 로지스틱 회귀 분류기를 학습하면 지도 학습에서 배운 공식이 그대로 사용될 것입니다.

𝑦̂ =𝑤[0]∗𝑥[0]+𝑤[1]∗𝑥[1]+...+𝑤[𝑝]∗𝑥[𝑝]+𝑏

위 공식에 따라 𝑥[i]는 반드시 숫자여야 합니다. 즉 𝑥[1]은 State-gov나 Self-emp-not-inc 같은 문자열 형태의 데이터가 올 수 없다는 이야기입니다. 따라서 로지스틱 회귀를 사용하려면 위 데이터를 다른 방식으로 표현해야 할 것 같습니다. 이제부터 이 문제들을 해결하기 위한 방법에 대해 이야기해 보겠습니다.

 

 

범주형 데이터 문제열 확인하기

데이터셋을 읽고 나서 먼저 어떤 열에 어떤 의미 있는 범주형 데이터가 있는지 확인해 보는 것이 좋습니다. 입력받은 데이터를 다룰 때는 정해진 범주 밖의 값이 있을 수도 있고, 철자나 대소문자가 틀려서 데이터를 전처리 해야 할 수도 있을 것입니다. 예를 들어 사람에 따라 남성을 "male"이나 "man"처럼 다르게 표현할 수 있을 수 있기 때문에 이들을 같은 범주의 데이터로 인식시켜 보아야 합니다.

가장 좋은 방법은 pandas에서 value_counts() 메소드를 이용해 각 Series에 유일한 값이 몇 개씩 있는지를 먼저 출력해 보는 것입니다.

import os
import mglearn
#mglearn에서 adult 데이터셋 불러오기
data = pd.read_csv(
    os.path.join(mglearn.datasets.DATA_PATH, 'adult.data'),
    header=None, index_col=False,
    names=['age','workclass','fnlwgt','education','education-num','marital-status','occupation', 'relationship',
           'race','gender','capital-gain', 'capital-loss','hours-per-week', 'native-country', 'income'])
data = data[['age','workclass','education','gender','hours-per-week', 'occupation', 'income']]
print(data.gender.value_counts())

Gender data_set

다행스럽게도 위 데이터셋에는 정확하게 Male과 Female을 가지고 있어서 원-핫-인코딩으로 나타내기 굉장히 좋은 형태입니다. 실제 애플리케이션에서는 모든 열을 살펴보고 그 값들을 확인해야 합니다.

pandas에서는 get_dummies 함수를 사용해 데이터를 매우 쉽게 인코딩할 수 있습니다. get_dummies 함수는 객체 타입(object 또는 문자열 타입 같은 범주형을 가진 열을 자동으로 반환해 줍니다.

print("원본 특성:\n", list(data.columns), "\n")
data_dummies = pd.get_dummies(data)
print("get_dummies 후의 특성:\n", list(data_dummies.columns))

get dummies() method

연속형 특성인 age와 hours-per-week는 그대로이지만 범주형 특성은 값마다 새로운 특성으로 확장되었습니다. 즉 새로운 열이 추가되겠네요.

data_dummies.head()

data_dummies data_set

data_dummies의 values 속성을 이용해 DataFrame을 NumPy 배열로 바꿀 수 있으며, 이를 이용해 머신러닝 모델을 학습시킵니다. 모델을 학습시키기 전에 이 데이터로부터 우리가 예측해야 할 타깃값인 income으로 시작되는 열을 분리해야 합니다. 출력값이나 출력값으로부터 유도된 변수를 특성 표현에 포함하는 것은 지도학습 모델을 만들 때 특히 저지르기 쉬운 실수입니다.

features = data_dummies.loc[:, 'age':'occupation_ Transport-moving']
# Numpy 배열 추출하기
X = features.values
y = data_dummies['income_ >50K'].values
print('X.shape: {} y.shape: {}'.format(X.shape, y.shape))

set X, y for test

이제 이 데이터는 scikit-learn에서 사용할 수 있는 형태가 되었으므로, 이전과 같은 방식을 사용하여 예측을 해볼 수 있습니다.

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

X_train, X_test, y_train,y_test = train_test_split(X, y, random_state=0)
logreg = LogisticRegression()
logreg.fit(X_train, y_train)
print("테스트 점수: {:.2f}".format(logreg.score(X_test, y_test)))

 

숫자로 표현된 범주형 특성

데이터 취합 방식에 따라서 범주형 데이터가 숫자로 취합된 경우도 생깁니다. 범주형 변수가 숫자라고 해서 연속적으로 다뤄도 된다는 의미는 아닙니다. 여러분들이 머신러닝에 사용할 데이터셋을 봤을 때, 순서를 나타낸 숫자가 아닌 단순히 범주를 나타내기 위한 숫자라는 사실을 확인하였으면, 이 값은 이산적이기 때문에 연속형 변수로 다루면 안 된다고 생각해야 합니다.

 

pandas의 get_dummis 함수는 숫자 특성은 모두 연속형이라고 생각해서 가변수를 만들지 않습니다. 대신 어떤 열이 연속형인지 범주형인지를 지정할 수 있는 scikit-learn의 OneHotEncoder를 사용해 DataFrame에 있는 숫자로 된 열을 문자열로 바꿀 수도 있습니다. 간단한 예를 보겠습니다.

# 숫자 특성과 범주형 문자열 특성을 가진 DataFrame 만들기
demo_df = pd.DataFrame({'숫자 특성' : [0, 1, 2, 1],
                        '범주형 특성' : ['양말','여우','양말','상자']})
demo_df

단순하게 get_dummies만 사용하면 문자열 특성만 인코딩 되며 숫자 특성은 바뀌지 않습니다.

pd.get_dummies(demo_df)

숫자 특성도 가변수로 만들고 싶다면 columns 매개변수에 인코딩 하고 싶은 열을 명시해야 합니다.

# 숫자 특성을 문자열로 변환
demo_df['숫자 특성'] = demo_df['숫자 특성'].astype(str)
pd.get_dummies(demo_df, columns=['숫자 특성', '범주형 특성'])

지금까지 원 핫 인코딩에 대하여 알아보았습니다.

다음 포스팅에서는 구간분할과 이산화 그리고 상호작용과 다항식에 대하여 알아보도록 하겠습니다!

728x90
반응형
LIST

+ Recent posts