728x90
반응형
SMALL
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
728x90
반응형
SMALL

이번 포스팅은 데이터 정규화(Normalisation)에 대해서 알아보도록 하겠습니다.

 

정규화(Normalisation)가 중요한 이유?

머신러닝 알고리즘은 데이터가 가진 feature(특성)들을 비교하여 데이터의 패턴을 찾습니다.

그런데 여기서 주의해야 할 점은 데이터가 가진 feature의 스케일이 심하게 차이가 나는 경우 문제가 되기 때문이다.

데이터의 차이는 모델의 성능을 좌우하기 때문에 신경망 같은 알고리즘들은 데이터의 스케일에 굉장히 민감합니다.

그래서 알고리즘들에 맞게 데이터의 특성 값들을 조절하게 되는데요, 보통 특성마다 스케일을 조정하여 데이터를 변경시킵니다.

예시를 한번 보겠습니다.

우선, 기본적으로 필요한 라이브러리를 임포트 해줍니다.

 

왼편의 그림은 데이터를 전처리 하기 전인 원본 데이터, 오른쪽은 각종 전처리 방법을 사용했을 때의 4가지 모습을 보여주고 있습니다.

본격적으로 전처리 방법에 대해 간단히 이야기해보겠습니다.

 

StandardScaler

scikit-learn의 StandardScaler는 각 특성의 평균을 0, 분산을 1로 변경하여 모든 특성이 같은 크기를 가지게 합니다. 하지만 최솟값과 최댓값 크기를 제한하지는 않습니다. 공식은 다음과 같습니다.

σ는 표준편차를 의미하고, x는 특성 값, 평균을 의미합니다. 이 결과를 표준 점수 또는 z-점수(z-score)라고도 합니다.

RobustScaler

RobustScaler는 일단 특성들이 같은 스케일을 갖게 된다는 것은 StandaradScaler와 비슷하나, 평균과 분산 대신 중간값 mediummedium과 사분위 값 quartilequartile을 사용합니다.

여기서 중간값이란 x보다 작은 수가 절반이고, x보다 큰 수가 절반인 x를 의미합니다. 단순히 평균이라고도 생각할 수 있는데요, 좌표 평면을 활용하기 때문에 각 사분위 별 개수가 중요합니다.

1 사분위 값은 x보다 작은 수가 전체 개수의 1/4인 x이고, 3 사분위는 x보다 큰 수가 전체 개수의 1/4인 x를 의미합니다. 공식은 다음과 같습니다.

q2는 중간값, q1은 1 사분위 값, q3은 3 사분위 값입니다.

이러한 방식 때문에 RobustScaler는 전체 데이터와 아주 동떨어진 데이터 포인트에 영향을 받지 않습니다. 이런 이상한 (멀리 떨어진 데이터)를 이상치(outlier) 라고 합니다. 이는 다른 스케일 조정 기법에서는 문제가 될 수도 있습니다.

MinMaxScaler

MinMaxScaler는 모든 특성이 정확하게 0과 1 사이에 위치하도록 데이터를 변경합니다. 2차원 데이터셋일 경우에는 모든 데이터가 x축의 0 ~ 1, y축은 0 ~ 1 사이의 사각 영역에 담기게 됩니다. 공식은 다음과 같습니다.

Normalizer

Normalizer는 매우 다른 스케일 조정 기법입니다. 이 방식은 특성 벡터의 유클리디안 길이가 1이 되도록 데이터 포인트를 조정합니다. 다른 정규 기법인 StandardScaler, RobustScaler, MinMaxScaler 기법은 특성들의 열 (특성)의 통계치를 사용하지만, Normalizer는 행(데이터 포인트)마다 각기 정규화됩니다.

다른 말로 하면 지름이 1인 원( 3차원 일 땐 구 )에 데이터 포인트를 투영하게 됩니다. 이 말은 각 데이터 포인트가 다른 비율로 (길이에 반비례하여) 스케일이 조정된다는 뜻이 됩니다. 이러한 정규화(normalizationnormalization)는 특성 벡터의 길이는 상관없고 데이터의 방향(또는 각도) 만이 중요할 때 많이 사용합니다.

데이터 변환 적용하기

여러 종류의 변환을 알아보았습니다. cancer 데이터셋에 커널 SVM(SVC)를 적용시키고 MinMaxScaler를 적용시켜 보겠습니다.

sklearn.preprocessing 모듈은 사이킷런에서 각종 전처리 작업을 위한 파이썬 클래스를 내장하고 있습니다.

MinMaxScaler 내부에는 fit 메서드가 존재합니다.

이 메서드는 모델을 훈련시키기 위해 존재하는 메서드가 아닌 각 특성마다의 최소, 최댓값을 계산해 줍니다.

따라서 특성을 의미하는 X_train만 넘겨줍니다.(y_train)은 사용하지 않습니다.

실제 fit 메서드를 통한 변환을 적용시키려면 transform() 메서드를 사용합니다.

transform은 새로운 데이터 표현(representation)을 만들 때 사용하는 메서드입니다.

변환된 배열 크기(shape)는 원본 배열과 동일합니다.

스케일 조정 후를 살펴보면 모든 특성의 최솟값과 최댓값이 각각 0과 1로 바뀐 것이 확인됩니다.

이 데이터에 SVM 모델을 적용하려면 테스트 데이터셋도 스케일 조정이 되어야 합니다. 이번엔 X_test를 조정하겠습니다.

이상하게도 똑같이 스케일을 조정했지만 0 또는 1이 나오지 않습니다. 그리고 0과 1의 범위를 넘어가 버리는 값도 존재하는 것이 확인됩니다.

모든 스케일 모델들은 항상 훈련 세트와 테스트 세트에 같은 변환을 적용시켜야 합니다. transform 메서드는 테스트 세트의 최솟값과 범위를 사용하지 않고, 항상 훈련 세트의 최솟값을 빼고 훈련 세트의 범위로 나눕니다. 간단하게 공식으로 살펴보면 다음과 같습니다.

훈련 데이터와 테스트 데이터의 스케일을 같은 방법으로 조정하기

지도 학습 모델에서 테스트 세트를 사용하려면 훈련 세트와 테스트 세트에 같은 변환을 적용해야 한다는 점이 중요합니다.

일단 이번 예제에서는 위와 반대로, 테스트 세트의 최솟값과 범위를 사용했을 때 어떤 일들이 일어나는지 보겠습니다.

from sklearn.datasets import make_blobs
# 인위적인 데이터 생성하기
X, _ = make_blobs(n_samples=50, centers=5, random_state=4, cluster_std=2)
# 훈련 세트와 테스트 세트로 나누기
X_train, X_test = train_test_split(X, random_state=5, test_size=.1)

# 훈련 세트와 테스트 세트의 산점도 그리기
fig, axes = plt.subplots(1,3,figsize=(13, 4))
axes[0].scatter(X_train[:, 0], X_train[:, 1], c=mglearn.cm2(0), label="훈련 세트", s=60)
axes[0].scatter(X_test[:, 0], X_test[:, 1],c=mglearn.cm2(1), label="테스트 세트", s=60, marker='^')
axes[0].legend(loc='upper left')
axes[0].set_title("원본 데이터")

# MinMaxScaler를 사용해 스케일 조정하기
scaler = MinMaxScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled  = scaler.transform(X_test)

# 스케일이 조정된 데이터의 산점도 그리기
axes[1].scatter(X_train_scaled[:, 0], X_train_scaled[:, 1], c=mglearn.cm2(0), label="훈련 세트", s=60)
axes[1].scatter(X_test_scaled[:, 0], X_test_scaled[:, 1], c=mglearn.cm2(1), label="테스트 세트", s=60, marker='^')
axes[1].set_title("스케일 조정된 데이터")


# 테스트 세트의 스케일을 따로 조정하기
# 테스트 세트의 최솟값은 0, 최댓값이 1이 됨
# 절대로 이렇게 사용하지 말것!

test_scaler = MinMaxScaler()
test_scaler.fit(X_test)
X_test_scaled_badly = test_scaler.transform(X_test)

# 잘못 조정된 데이터의 산점도 그리기
axes[2].scatter(X_train_scaled[:, 0], X_train_scaled[:, 1], c=mglearn.cm2(0), label="훈련 세트", s=60)
axes[2].scatter(X_test_scaled_badly[:, 0], X_test_scaled_badly[:, 1], 
                                    marker='^', c=mglearn.cm2(1), label="테스트 세트", s=60)
axes[2].set_title("잘못 조정된 데이터")

for ax in axes:
    ax.set_xlabel("특성 0")
    ax.set_ylabel("특성 1")

첫 번째 그림은 단순히 2차원 원본 데이터셋이며, 훈련 세트는 파란 동그라미, 테스트 세트는 빨간 세모로 표시합니다.

두 번째 그래프는 같은 데이터를 MinMaxScaler를 이용해 스케일을 조절하였는데, 각 그래프의 눈금 빼고는 모양이 변한 것이 없습니다.

하지만, 테스트 데이터(세모)의 최솟값과 최댓값이 0과 1은 아닙니다.

문제는 세 번째 데이터입니다. 훈련 세트와 테스트 세트에 대해 각각 스케일을 조정한 경우, 각 데이터셋이 서로 다르게 스케일 되었기 때문에 삼각형의 배치가 뒤죽박죽 되었습니다.

지도 학습에서 데이터 전처리 효과

다시 cancer 데이터셋으로 돌아와서, SVC를 학습시킬 때 MinMaxScaler의 효과를 확인해 보도록 하겠습니다.

from sklearn.svm import SVC

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

svm = SVC(C=100)
svm.fit(X_train, y_train)
print("테스트 세트 정확도: {:.2f}".format(svm.score(X_test, y_test)))

스케일이 조정되지 않은 원본 데이터의 정확도는 63%의 정확도 밖에 표현하지 못합니다. 다음은 스케일 조정 후의 학습 결과입니다.

# MinMaxScaler 사용
scaler = MinMaxScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled  = scaler.transform(X_test)

# 조정된 데이터로 SVM 학습
svm.fit(X_train_scaled, y_train)

# 스케일 조정된 테스트 세트의 정확도
print("스케일 조정된 테스트 세트의 정확도: {:.2f}".format(svm.score(X_test_scaled, y_test)))

단순히 스케일만 조정했을 뿐인데 그 효과는 매우 큰 것이 확인됩니다.

스케일을 조정하는 모든 파이썬 클래스들은 fit과 transform을 제공하므로 손쉽게 전처리 알고리즘을 바꿔낼 수 있습니다.

# 평균 0, 분선 1을 갖도록 스케일 조정하기
from sklearn.preprocessing import StandardScaler

# StandaradScaler 사용
scaler = StandardScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled  = scaler.transform(X_test)

# 조정된 데이터로 SVM 학습
svm.fit(X_train_scaled, y_train)

# 스케일 조정된 테스트 세트의 정확도
print("스케일 조정된 테스트 세트의 정확도: {:.2f}".format(svm.score(X_test_scaled, y_test)))

지금까지 데이터 학습 훈련을 위한 스케일링에 대하여 알아보았습니다.

다음 포스팅에서는 One-Hot-Encoding에 대하여 알아보도록 하겠습니다.

728x90
반응형
LIST

+ Recent posts