/ STUDY

머신러닝 정리 (5)
데이터 표현과 특성 공학

머신러닝 공부 관련 글

  1. 범주형 변수
    1. 원-핫 인코딩
  2. OneHotEncoder와 ColumnTransformer: scikit-learn으로 범주형 변수 다루기
  3. make_column_transformer로 간편하게 ColumnTransformer 만들기
  4. 구간 분할, 이산화 그리고 선형 모델, 트리 모델
  5. 상호작용과 다항식
  6. 일변량 비선형 변환
  7. 특성 자동 선택
  8. 일변량 통계(ANOVA)
  9. 모델 기반 특성 선택
  10. 반복적 특성 선택
  11. 전문가 지식 활용

머신러닝 정리 (5) 데이터 표현과 특성 공학

본 문서는 [파이썬 라이브러리를 활용한 머신러닝] 책을 공부하면서 요약한 내용입니다.
또 데이터 청년 캠퍼스 수업과 학교 수업에서 배운 내용들도 함께 정리했습니다.
글의 순서는 [파이썬 라이브러리를 활용한 머신러닝]에 따라 진행됩니다.
코드는 밑에 링크에 공개되어있기 때문에 올리지않습니다.

소스 코드: https://github.com/rickiepark/introduction_to_ml_with_python

범주형 변수

우리가 실제 사용하는 데이터의 대부분은 범주형 변수입니다.

이 데이터에서는 근로자의 수입이 50000달러를 초과하는지, 이하인지 예측하려고 합니다.

사용된 데이터셋은 다음과 같습니다.

# pip install mglearn

import pandas as pd
from preamble import *
import os
# 이 파일은 열 이름을 나타내는 헤더가 없으므로 header=None으로 지정하고
# "names" 매개변수로 열 이름을 제공합니다
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']]
# IPython.display 함수는 주피터 노트북을 위해 포맷팅된 출력을 만듭니다
display(data.head())
age workclass education gender hours-per-week occupation income
0 39 State-gov Bachelors Male 40 Adm-clerical <=50K
1 50 Self-emp-not-inc Bachelors Male 13 Exec-managerial <=50K
2 38 Private HS-grad Male 40 Handlers-cleaners <=50K
3 53 Private 11th Male 40 Handlers-cleaners <=50K
4 28 Private Bachelors Female 40 Prof-specialty <=50K

원-핫 인코딩

범주형 변수를 0 또는 1 값을 가진 하나 이상의 새로운 특성으로 바꾼 것입니다.
0과 1로 표현된 변수가 선형 이진 분류 공식에 적용할 수 있기 때문입니다.

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

데이터셋을 읽은 뒤 범주형 데이터가 있는지 확인해보는 것이 좋습니다.

print(data.gender.value_counts())
 Male      21790
 Female    10771
Name: gender, dtype: int64

get_dummies 함수를 사용해 쉽게 인코딩할 수 있습니다.

data_dummies=pd.get_dummies(data)
print(list(data_dummies.columns))
['age', 'hours-per-week', 'workclass_ ?', 'workclass_ Federal-gov', 'workclass_ Local-gov', 'workclass_ Never-worked', 'workclass_ Private', 'workclass_ Self-emp-inc', 'workclass_ Self-emp-not-inc', 'workclass_ State-gov', 'workclass_ Without-pay', 'education_ 10th', 'education_ 11th', 'education_ 12th', 'education_ 1st-4th', 'education_ 5th-6th', 'education_ 7th-8th', 'education_ 9th', 'education_ Assoc-acdm', 'education_ Assoc-voc', 'education_ Bachelors', 'education_ Doctorate', 'education_ HS-grad', 'education_ Masters', 'education_ Preschool', 'education_ Prof-school', 'education_ Some-college', 'gender_ Female', 'gender_ Male', 'occupation_ ?', 'occupation_ Adm-clerical', 'occupation_ Armed-Forces', 'occupation_ Craft-repair', 'occupation_ Exec-managerial', 'occupation_ Farming-fishing', 'occupation_ Handlers-cleaners', 'occupation_ Machine-op-inspct', 'occupation_ Other-service', 'occupation_ Priv-house-serv', 'occupation_ Prof-specialty', 'occupation_ Protective-serv', 'occupation_ Sales', 'occupation_ Tech-support', 'occupation_ Transport-moving', 'income_ <=50K', 'income_ >50K']
data_dummies.head()
age hours-per-week workclass_ ? workclass_ Federal-gov ... occupation_ Tech-support occupation_ Transport-moving income_ <=50K income_ >50K
0 39 40 0 0 ... 0 0 1 0
1 50 13 0 0 ... 0 0 1 0
2 38 40 0 0 ... 0 0 1 0
3 53 40 0 0 ... 0 0 1 0
4 28 40 0 0 ... 0 0 1 0

5 rows × 46 columns

Numpy 배열로 바꾸고 타깃 값을 분리하여 학습 모델에 적용해야합니다.

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))
X.shape: (32561, 44)  y.shape: (32561,)
# 로지스틱 회귀 분석
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(max_iter=5000)
logreg.fit(X_train, y_train)
print("테스트 점수: {:.2f}".format(logreg.score(X_test, y_test)))
테스트 점수: 0.81

숫자로 표현된 범주형 특성

0부터 시작하는 연속된 자연수로 이루어진 데이터셋은 연속형인지 범주형인지 알기 힘듭니다.
따라서 데이터셋이 의미하는 것이 무엇인지 확인해야 합니다.
숫자로 표현된 범주형 데이터셋은 get_dummies를 사용하면 숫자 특성은 바뀌지 않습니다.
따라서 columns 매개변수에 인코딩하고 싶은 열을 명시해야합니다.


OneHotEncoder와 ColumnTransformer: scikit-learn으로 범주형 변수 다루기

scikit-learn의 OneHotEncoder을 통해 모든 열에 인코딩을 수행할 수 있습니다.
sparse=False는 희소 행렬이 아닌 넘파이 배열을 반환합니다.
변환된 특성에 해당하는 원본 범주형 변수 이름을 알기 위해서는 get_feature_names 메소드를 사용합니다.
ColumnTransformer의 fit, transform 메소드를 사용할 수 있습니다.

from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler

ct = ColumnTransformer(
    [("scaling", StandardScaler(), ['age', 'hours-per-week']),
     ("onehot", OneHotEncoder(sparse=False), ['workclass', 'education', 'gender', 'occupation'])])

이렇게 변환된 모델을 LogisticRegression에 학습합니다.

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
# income을 제외한 모든 열을 추출합니다
data_features = data.drop("income", axis=1)
# 데이터프레임과 income을 분할합니다
X_train, X_test, y_train, y_test = train_test_split(
    data_features, data.income, random_state=0)

ct.fit(X_train)
X_train_trans = ct.transform(X_train)
print(X_train_trans.shape)
(24420, 44)

44개의 특성이 만들어진 것은 위의 pd.get_dummies를 사용했을 때와 마찬가지이고 스케일 조정이 된 차이가 있습니다.

logreg = LogisticRegression(max_iter=1000)
logreg.fit(X_train_trans, y_train)

X_test_trans = ct.transform(X_test)
print("테스트 점수: {:.2f}".format(logreg.score(X_test_trans, y_test)))
테스트 점수: 0.81

make_column_transformer로 간편하게 ColumnTransformer 만들기

클래스 이름을 기반으로 자동으로 각 단계에 이름을 붙여주는 make_column_transformer 함수가 있습니다.
변환된 데이터는 넘파이 배열이므로 열 이름을 가지고 있지 않습니다.

from sklearn.compose import make_column_transformer
ct = make_column_transformer(
    (StandardScaler(), ['age', 'hours-per-week']),
    (OneHotEncoder(sparse=False), ['workclass', 'education', 'gender', 'occupation']))
print(ct)
ColumnTransformer(transformers=[('standardscaler', StandardScaler(),
                                 ['age', 'hours-per-week']),
                                ('onehotencoder', OneHotEncoder(sparse=False),
                                 ['workclass', 'education', 'gender',
                                  'occupation'])])

구간 분할, 이산화 그리고 선형 모델, 트리 모델

wave 데이터 셋을 통해선형 회귀 모델과 결정 트리 회귀를 비교해보았습니다.

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

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

reg = DecisionTreeRegressor(min_samples_leaf=3).fit(X, y)
plt.plot(line, reg.predict(line), label="Decision Tree")

reg = LinearRegression().fit(X, y)
plt.plot(line, reg.predict(line), '--', label="linear regression")

plt.plot(X[:, 0], y, 'o', c='k')
plt.ylabel("output")
plt.xlabel("input")
plt.legend(loc="best")
plt.show()

output_23_0

선형 모델은 선형 관계로만 모델링하므로 특성이 하나일 땐 직선으로 나타납니다.
구간 분할을 통해 한 특성을 여러 특성으로 나누면 강력한 선형 모델을 만들 수 있습니다.
균일한 너비로 나누거나 데이터의 분위를 사용할 수도 있습니다.
KBinsDiscretizer 클래스를 이용하면 됩니다.
KBinsDiscretizer는 한 번에 여러 개의 특성에 적용할 수 있습니다.
bin_edges_는 특성별로 경계값이 저장되어 있으며 길이가 1인 넘파이 배열이 출력됩니다.
기본적으로 원-핫-인코딩을 적용하며 구간마다 새로운 특성이 생기는 희소 행렬을 만듭니다.
n개의 구간을 지정하면 n개의 차원이 생성됩니다.

from sklearn.preprocessing import KBinsDiscretizer

kb = KBinsDiscretizer(n_bins=10, strategy='uniform')
kb.fit(X)
print("bin edges: \n", kb.bin_edges_)
bin edges: 
 [array([-2.967, -2.378, -1.789, -1.2  , -0.612, -0.023,  0.566,  1.155,
        1.744,  2.333,  2.921])]
X_binned = kb.transform(X)
X_binned

print(X[:10])
X_binned.toarray()[:10]
[[-0.753]
 [ 2.704]
 [ 1.392]
 [ 0.592]
 [-2.064]
 [-2.064]
 [-2.651]
 [ 2.197]
 [ 0.607]
 [ 1.248]]





array([[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 1.],
       [0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],
       [0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
       [1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
       [0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 1., 0., 0.]])

원-핫-인코딩된 밀집 배열을 만들어보면 다음과 같습니다.

kb = KBinsDiscretizer(n_bins=10, strategy='uniform', encode='onehot-dense')
kb.fit(X)
X_binned = kb.transform(X)  
line_binned = kb.transform(line)

reg = LinearRegression().fit(X_binned, y)
plt.plot(line, reg.predict(line_binned), label='linear regression')

reg = DecisionTreeRegressor(min_samples_split=3).fit(X_binned, y)
plt.plot(line, reg.predict(line_binned), label='decision tree')
plt.plot(X[:, 0], y, 'o', c='k')
plt.vlines(kb.bin_edges_[0], -3, 3, linewidth=1, alpha=.2)
plt.legend(loc="best")
plt.ylabel("output")
plt.xlabel("input")
plt.show(

output_28_0

두 선이 완전히 겹치는 결과가 나왔습니다.
선형 모델은 훨씬 유연해졌지만 결정 트리는 덜 유연해졌습니다.

상호작용과 다항식

상호작용과 다항식을 추가하면 특성을 더 풍부하게 만들 수 있습니다.

X_combined = np.hstack([X, X_binned])
print(X_combined.shape)  

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

line_combined = np.hstack([line, line_binned])
plt.plot(line, reg.predict(line_combined), label='Linear regression with original characteristics added')

plt.vlines(kb.bin_edges_[0], -3, 3, linewidth=1, alpha=.2)
plt.legend(loc="best")
plt.ylabel("Regression output")
plt.xlabel("Input characteristics")
plt.plot(X[:, 0], y, 'o', c='k')
plt.show() 
(120, 11)

output_31_1

기울기가 모두 같으므로 원본 특성을 곱한 값을 더해 20개의 특성을 만들어줍니다.

X_product = np.hstack([X_binned, X * X_binned])
print(X_product.shape)
(120, 20)
reg = LinearRegression().fit(X_product, y)

line_product = np.hstack([line_binned, line * line_binned])
plt.plot(line, reg.predict(line_product), label='Linear regression with original characteristics added')

plt.vlines(kb.bin_edges_[0], -3, 3, linewidth=1, alpha=.2)

plt.plot(X[:, 0], y, 'o', c='k')
plt.ylabel("Regression output")
plt.xlabel("Input characteristics")
plt.legend(loc="best")
plt.show()

output_34_0

preprocessiong 모듈의 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)
print("X_poly.shape:", X_poly.shape)
X_poly.shape: (120, 10)
print("항 이름:\n", poly.get_feature_names())
항 이름:
 ['x0', 'x0^2', 'x0^3', 'x0^4', 'x0^5', 'x0^6', 'x0^7', 'x0^8', 'x0^9', 'x0^10']

이렇게 다항 회귀 모델을 만들 수 있습니다.
다항식의 특성은 1차원 데이터셋에서도 매우 부드러운 곡선을 만듭니다.
하지만 고차원 다항식은 데이터가 부족한 영역에서 너무 민감하게 동작합니다.

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

line_poly = poly.transform(line)
plt.plot(line, reg.predict(line_poly), label='Multinomial linear regression')
plt.plot(X[:, 0], y, 'o', c='k')
plt.ylabel("Regression output")
plt.xlabel("Input characteristics")
plt.legend(loc="best")
plt.show()

output_40_0

비교를 위한 커널 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("Regression output")
plt.xlabel("Input characteristics")
plt.legend(loc="best")
plt.show()

output_42_0

커널 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)
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:", X_train.shape)
print("X_train_poly.shape:", X_train_poly.shape)
X_train.shape: (379, 13)
X_train_poly.shape: (379, 105)
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)))
상호작용 특성이 없을 때 점수: 0.621
상호작용 특성이 있을 때 점수: 0.753
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)))
상호작용 특성이 없을 때 점수: 0.795
상호작용 특성이 있을 때 점수: 0.775

Ridge의 성능은 크게 높였지만 RandomForest는 상호작용 특성이 있는 Ridge만큼의 성능과 맞먹었습니다.

일변량 다항식

log, exp, sin과 같은 함수들도 특성 변환에 사용됩니다.
log, exp 함수는 데이터 스케일을 변경하는 것에 사용되며, sin과 cos은 주기적인 패턴이 들어간 데이터를 변경하는 것에 유용하게 사용됩니다.

일변량 비선형 변환을 하는 이유는 특성과 타깃 값 사이에 비선형성이 있다면 모델을 만들기 어렵기 때문입니다.
또한 대부분의 모델은 특성의 분포가 정규분포와 비슷할 때 데이터 간 편차를 줄여 성능이 상승합니다.

<변환 전=""> ![그림1](https://user-images.githubusercontent.com/54880474/141170385-38f2a372-e76e-44f8-8f05-5ffcd01b743c.png) <로그 변환="" 후=""> ![그림2](https://user-images.githubusercontent.com/54880474/141170390-e416f90a-e5d0-433a-9c24-09f89bcad250.png) 다음 분포처럼 적은 값 쪽에 분포가 몰리지만 값의 범위가 매우 넓을 때 log함수를 사용하게 된다면 log함수는 x가 커질수록 증가율이 작아지기 때문에 멀리 떨어진 값을 중간값과 가깝게 이동시켜 정규분포와 유사한 형태를 띄게 만들 수 있습니다. 트리 모델처럼 스스로 중요한 상호작용을 찾을 수 있는 모델은 괜찮지만 선형 모델과 같이 스케일과 분포에 민감한 모델은 비선형 변환을 통해 좋은 효과를 얻을 수 있습니다. ## 특성 자동 선택 특성이 많으면 모델은 복잡해지고 과대적합 가능성이 상승하는 것을 앞에서 배웠습니다. 따라서 불필요한 특성을 줄이는 것이 모델 학습에 좋은 영향을 미칩니다. 특성 선택을 사용할 때는 test 데이터셋에 영향이 가지않도록 train 데이터셋에만 적용해야하는 주의점이 있습니다. ### 일변량 통계 (ANOVA) 일변량 통계는 특성과 타깃 사이에 중요한 통계적 관계를 계산합니다. 각 특성을 독립적으로 평가하여 다른 특성과 깊게 연관된 특성은 선택하지 않습니다. SelectKBest는 K개의 특성을, SelectPercentile은 지정한 비율만큼의 특성을 선택해줍니다. 또한 분류 모델에서는 f_classif, 회귀에서는 f_regression을 사용합니다. 일변량 통계는 Y값과 특성의 통계적 유의미를 분석합니다. 귀무가설을 기각하기 위해서는 p-value가 작아야합니다. 일변량 통계의 귀무가설은 '집단간 평균은 같다'입니다. 집단간 분산을 집단 내 분산으로 나눈 것을 F-value라고 합니다. F-value가 작으면 p-value가 커지므로 이는 집단간 분산이 분모에 있으므로 집단간 분산이 작다는 의미와 같습니다. 이것은 집단간 차이가 적다는 의미이므로 타깃에 미치는 영향이 적다고 할 수 있습니다. ![그림3](https://user-images.githubusercontent.com/54880474/141170392-caac02a7-62e7-4091-a621-6705b3eb68c9.png) 따라서 타깃에 미치는 영향이 적은 특성을 제외시킬 수 있게 됩니다. ### 모델 기반 특성 선택 특성의 중요도를 측정하여 순서를 매깁니다. 결정 트리에서 중요도는 feature_importances_, 선형 모델에서는 계수의 절대값을 통해 중요도를 결정합니다. 임계치를 threshold를 통해 지정하는데 중앙값, 평균값, 1.2*평균값과 같이 지정해주면 됩니다. ### 반복적 특성 선택 특성 수가 다른 일련의 모델을 생성하여 최선의 특성을 선택합니다. 여러 개의 모델을 만들어야하기 때문에 계산비용이 증가하게 됩니다. 1. 전진(후진) 선택법 특성을 하나도 선택하지 않은 모델부터 회귀에서 결정계수(R^2)나 분류에서 정확도를 통해서 scoring 매개변수를 기준으로 종료조건을 충족시킬 때까지 하나씩 추가하는 방법입니다. 2. 재귀적 특성 제거 모든 특성을 가지고 시작해서 어떤 종료 조건이 될 때까지 특성을 하나씩 제거하는 방법이니다. 마찬가지로 중요도가 낮은 특성을 먼저 제거합니다. 제거한 다음 새로운 모델을 만들어서 정의한 특성 개수가 남을 때까지 계속합니다. 특성 자동 선택을 이용하면 특성 개수가 적어지기 때문에 모델의 예측 속도가 빨라지며 해석력이 좋아집니다. ### 전문가 지식 활용 모델에 따라 외삽 문제가 발생할 수도 있고 그렇게 된다면 결정계수가 전혀 상관없는 예측을 할 수도 있습니다. 따라서 모델을 예측할 때는 서로간의 연관관계를 확인하고 특성을 추가, 제거하여 더 좋은 모델을 만들 수 있습니다.