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

이번 포스팅에서는 데이터 특성을 표현하기 위한 구간 분할(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