머신러닝 정리 (5)
데이터 표현과 특성 공학
머신러닝 공부 관련 글
- 머신러닝 정리 (1)-지도학습 (1)
- 머신러닝 정리 (2)-지도학습 (2)
- 머신러닝 정리 (3)-비지도학습 (1)
- 머신러닝 정리 (4)-비지도학습 (2)
- 머신러닝 정리 (5)-데이터 표현과 특성 공학
- 머신러닝 정리 (6)-모델 평가와 성능 향상
- 범주형 변수
- 원-핫 인코딩
- OneHotEncoder와 ColumnTransformer: scikit-learn으로 범주형 변수 다루기
- make_column_transformer로 간편하게 ColumnTransformer 만들기
- 구간 분할, 이산화 그리고 선형 모델, 트리 모델
- 상호작용과 다항식
- 일변량 비선형 변환
- 특성 자동 선택
- 일변량 통계(ANOVA)
- 모델 기반 특성 선택
- 반복적 특성 선택
- 전문가 지식 활용
머신러닝 정리 (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()
선형 모델은 선형 관계로만 모델링하므로 특성이 하나일 땐 직선으로 나타납니다.
구간 분할을 통해 한 특성을 여러 특성으로 나누면 강력한 선형 모델을 만들 수 있습니다.
균일한 너비로 나누거나 데이터의 분위를 사용할 수도 있습니다.
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(
두 선이 완전히 겹치는 결과가 나왔습니다.
선형 모델은 훨씬 유연해졌지만 결정 트리는 덜 유연해졌습니다.
상호작용과 다항식
상호작용과 다항식을 추가하면 특성을 더 풍부하게 만들 수 있습니다.
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)
기울기가 모두 같으므로 원본 특성을 곱한 값을 더해 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()
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()
비교를 위한 커널 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()
커널 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은 주기적인 패턴이 들어간 데이터를 변경하는 것에 유용하게 사용됩니다.
일변량 비선형 변환을 하는 이유는 특성과 타깃 값 사이에 비선형성이 있다면 모델을 만들기 어렵기 때문입니다.
또한 대부분의 모델은 특성의 분포가 정규분포와 비슷할 때 데이터 간 편차를 줄여 성능이 상승합니다.