회귀분석의 네가지 방법, 선형회귀/의사결정트리/랜덤포레스트/SVM

회귀분석은 다수의 특징값을 입력으로 하나의 특징값(실수값)을 산출하는 것입니다. 세가지 방법이 있는데, 선형회귀(Linear Regression)과 의사결정트리(Decision Tree) 그리고 렌덤 포레스트(Random Forest)입니다. 하나의 주제를 정하고 이 3가지 방법을 통해 회귀분석을 테스트해 보도록 하겠습니다. 이 글에서 적용한 회귀 분석 주제는 전복의 나이를 예측하는 것으로 전복의 ‘성별’, ‘키’, ‘지름’, ‘높이’, ‘전체무게’, ‘몸통무게’, ‘내장무게’, ‘껍질무게’를 입력하면 ‘껍질의 고리수’를 예측한 뒤 예측된 ‘껍질의 고리수’에 1.5를 더하면 전복의 나이가 된다고 합니다.

이에 대한 전복의 데이터셋은 kaggle에서 쉽게 다운로드 받을 수 있습니다. 일단 다운로드 받은 데이터셋을 다음과 같은 코드를 통해 전처리하여 특징과 레이블로 구분하고, 학습용과 테스트용으로 구분합니다.

# 데이터 파일 로딩
import pandas as pd
raw_data = pd.read_csv('./datasets/datasets_1495_2672_abalone.data.csv', 
        names=['sex', 'tall', 'radius', 'height', 'weg1', 'weg2', 'weg3', 'weg4', 'ring_cnt'])
    #names=['성별', '키', '지름', '높이', '전체무게', '몸통무게', '내장무게', '껍질무게', '껍질의고리수']
print(raw_data[:7])

# 레이블 분리
data_ring_cnt = raw_data[["ring_cnt"]]
data = raw_data.drop("ring_cnt", axis=1)

# 범주형 특징(sex)에 대한 원핫 인코딩
from sklearn.preprocessing import OneHotEncoder
data_cat = data[["sex"]]
onehot_encoder = OneHotEncoder()
data_cat_onehot = onehot_encoder.fit_transform(data_cat)
print(onehot_encoder.categories_)

# 범주형 특징 제거
data = data.drop("sex", axis=1)

# 범주형 필드가 제거되어 수치형 특징들에 대해 0~1 구간의 크기로 조정
#from sklearn.preprocessing import StandardScaler 
#minmax_scaler = StandardScaler()
#data = minmax_scaler.fit_transform(data)

# 원핫인코딩된 범주형 특징과 스케일링된 수치형 특징 및 레이블 결합
import numpy as np
data = np.c_[data_cat_onehot.toarray(), data, data_ring_cnt]

data = pd.DataFrame(data, columns=['sex_F', 'sex_I', 'sex_M', 'sex_''tall', 'radius', 'height', 'weg1', 'weg2', 'weg3', 'weg4', 'ring_cnt'])
print(data[:7])

# 학습 데이터와 테스트 데이터 분리 
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(data, test_size=0.1, random_state=47)

# 입력 특징과 레이블의 분리
train_data = train_set.drop("ring_cnt", axis=1)
train_data_label = train_set["ring_cnt"].copy()
test_data = test_set.drop("ring_cnt", axis=1)
test_data_label = test_set["ring_cnt"].copy()

위의 코드는 아래의 글들을 참조하여 파악할 수 있습니다.

분석가 관점에서 데이터를 개략적으로 살펴보기

계층적 샘플링(Stratified Sampling)

OneHot 인코딩(Encoding) 및 스케일링(Scaling)

다소 험난한 데이터셋의 준비가 끝났으니, 이제 이 글의 본질인 세가지 회귀분석 방식을 하나씩 살펴보겠습니다. 먼저 선형회귀입니다. 참고로 기계학습 과정에 대한 시간 대부분을 차지 하는 작업은 이러한 데이터의 수집과 가공 및 정제이며, 그 다음으로 컴퓨터(GPU)를 통한 모델의 학습입니다. 사람이 관여하는 시간은 상대적으로 매우 적습니다. 물론 모델을 직접 설계하는 경우라면 달라질 수 있겠으나, 역시 데이터 작업과 GPU의 학습 시간은 상대적으로 많이 소요됩니다.

from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(train_data, train_data_label)

from sklearn.metrics import mean_squared_error
some_predicted = model.predict(test_data)
mse = np.sqrt(mean_squared_error(some_predicted, test_data_label))
print('평균제곱근오차', mse)

학습된 모델을 테스트 데이터 셋으로 평가한 오차는 다음과 같습니다.

평균제곱근오차 1.9166262592968584

다음은 의사결정트리 방식입니다.

from sklearn.tree import DecisionTreeRegressor
model = DecisionTreeRegressor()
model.fit(train_data, train_data_label)

from sklearn.metrics import mean_squared_error
some_predicted = model.predict(test_data)
mse = np.sqrt(mean_squared_error(some_predicted, test_data_label))
print('평균제곱근오차', mse)
평균제곱근오차 2.8716565747410656

세번째로 랜덤 포레스트 방식입니다.

from sklearn.ensemble import RandomForestRegressor
model = RandomForestRegressor()
model.fit(train_data, train_data_label)

from sklearn.metrics import mean_squared_error
some_predicted = model.predict(test_data)
mse = np.sqrt(mean_squared_error(some_predicted, test_data_label))
print('평균제곱근오차', mse)
평균제곱근오차 2.0828589571564127

마지막으로 SVM(Support Vector Manchine) 방식입니다.

from sklearn import svm
model = svm.SVC()
model.fit(train_data, train_data_label)
평균제곱근오차 2.537753216441624

이 글의 데이터셋에 대해서는 선형회귀 방식이 가장 오차가 작은 것은 것을 알 수 있습니다. 하지만 이 글에서 테스트한 3가지 모델의 하이퍼파라메터는 기본값을 사용하였습니다. 하이퍼파라메터의 세부 튜팅을 수행하면 결과가 달라질 수 있습니다.

OneHot 인코딩(Encoding) 및 스케일링(Scaling)

학습 데이터의 특성들은 수치값 뿐만 아니라 ‘크다’, ‘중간’, ‘작다’ 또는 ‘여자’, ‘남자’와 같은 범주값도 존재합니다. 먼저 범주형 값을 처리하기 위해서는 이 범주형 값을 수치값으로 변환해야 합니다. 만약 범주형 값이 ‘A등급’, ‘B등급’, ‘C등급’처럼 그 의미에 순위적 연속성이 존재한다면 그냥 3, 2, 1과 같이 수치값으로 등급을 매칭하면 됩니다. 하지만 ‘여자’, ‘남자’처럼 순위도 연속성도 없다면 반드시 다른 의미로의 수치값으로 변환해야 하는데, 그 변환은 OnHot 인코딩이라고 합니다. 결론을 미리 말하면 ‘여자’라면 벡터 (1,0)으로, 남자라면 (0,1)으로 변경해야 합니다.

샘플 데이터를 통해 이 OneHot 인코딩을 하는 방법에 대해 언급하겠습니다. 샘플 데이터는 아래의 글에서 소개한 데이터를 사용합니다.

분석가 관점에서 데이터를 개략적으로 살펴보기

먼저 샘플 데이터를 불러옵니다.

import pandas as pd

raw_data = pd.read_csv('./datasets/datasets_1495_2672_abalone.data.csv', 
        names=['sex', 'tall', 'radius', 'height', 'weg1', 'weg2', 'weg3', 'weg4', 'ring_cnt'])
    #names=['성별', '키', '지름', '높이', '전체무게', '몸통무게', '내장무게', '껍질무게', '껍질의고리수']

이 중 sex 컬럼은 성별인데, 이 컬럼의 값을 아래의 코드를 통해 출력해 봅니다.

print(raw_data["sex"][:10])
0    M
1    M
2    F
3    M
4    I
5    I
6    F
7    F
8    M
9    F

범주형 값이라는 것을 알수있는데, M은 숫컷, F는 암컷, I는 유충입니다. 이 sex 컬럼에 대해 OneHot 인코딩 처리를 위해 먼저 문자형을 숫자형으로 변환해주는 OrdinalEncoder 클래스를 통해 처리합니다.

raw_data_labels = raw_data["ring_cnt"].copy()
raw_data = raw_data.drop("ring_cnt", axis=1)

raw_data_cat = raw_data[["sex"]]

from sklearn.preprocessing import OrdinalEncoder

ordinal_encoder = OrdinalEncoder()
raw_data_encoded = ordinal_encoder.fit_transform(raw_data_cat)

print(raw_data_encoded[:10])
print(ordinal_encoder.categories_)
[[2.]
 [2.]
 [0.]
 [2.]
 [1.]
 [1.]
 [0.]
 [0.]
 [2.]
 [0.]]
[array(['F', 'I', 'M'], dtype=object)]

OrdinalEncoder는 범주형 데이터를 희소행렬(Sparse Matrix)로 그 결과를 반환합니다. 다시 이 희소행렬을 OneHot 인코딩을 시키기 위해 아래의 코드를 수행합니다.

from sklearn.preprocessing import OneHotEncoder
onehot_encoder = OneHotEncoder()
raw_data_cat_onehot = onehot_encoder.fit_transform(raw_data_cat)
print(raw_data_cat_onehot.toarray()[:10])
print(onehot_encoder.categories_)
[[0. 0. 1.]
 [0. 0. 1.]
 [1. 0. 0.]
 [0. 0. 1.]
 [0. 1. 0.]
 [0. 1. 0.]
 [1. 0. 0.]
 [1. 0. 0.]
 [0. 0. 1.]
 [1. 0. 0.]]
[array(['F', 'I', 'M'], dtype=object)]

이제 범주형 컬럼인 sex 대신 OneHot 인코딩된 값을 데이터에 추가하도록 합니다.

raw_data = raw_data.drop("sex", axis=1)
raw_data = np.c_[raw_data_cat_onehot.toarray(), raw_data]
print(raw_data[:10])
[[0.0 0.0 1.0 0.455 0.365  0.095 0.514  0.2245 0.100999 0.15  4]
 [0.0 0.0 1.0 0.35  0.265  0.09  0.2255 0.0995 0.0485   0.07  2]
 [1.0 0.0 0.0 0.53  0.42   0.135 0.677  0.2565 0.1415   0.21  4]
 [0.0 0.0 1.0 0.44  0.365  0.125 0.516  0.2155 0.114    0.155 4]
 [0.0 1.0 0.0 0.33  0.255  0.08  0.205  0.0895 0.0395   0.055 2]
 [0.0 1.0 0.0 0.425 0.3    0.095 0.3515 0.141  0.0775   0.12  3]
 [1.0 0.0 0.0 0.53  0.415  0.15  0.7775 0.237  0.1415   0.33  4]
 [1.0 0.0 0.0 0.545 0.425  0.125 0.768  0.294  0.1495   0.26  4]
 [0.0 0.0 1.0 0.475 0.37   0.125 0.5095 0.2165 0.1125   0.165 4]
 [1.0 0.0 0.0 0.55  0.44   0.15  0.8945 0.3145 0.151    0.32  4]]

범주형 타입인 sex가 제거되고 이 sex에 대한 추가적인 3개의 컬럼이 추가되었습니다. 바로 이 3개의 컬럼이 OneHot 인코딩된 값입니다.

이제 수치형 데이터에 대한 스케일링입니다. 여기서 스케일링이란 서로 다른 특성들을 일정한 값의 범위로 맞춰주는 것입니다. 흔히 사용하는 방식은 Min-Max 스케일링과 표준화(Standardization)이 있습니다. 먼저 Min-Max 스케일링은 특징의 최소값과 최대값을 먼저 계산하고 이 값을 이용하여 전체 특징값들을 0~1 사이의 값으로 변경시킵니다. 표준화는 먼저 평균과 표준편차를 구하고 전체 데이터 각각에 대해 평균을 뺀 후 표준편차로 나눠 분산이 1이 되도록 데이터를 조정합니다. 각각 sklearn에서 제공하는 MinMaxScaler와 StandardScaler 클래스를 통해 수행이 가능합니다. 아래의 코드는 Min-Max 스케일링을 수행하는 코드입니다.

from sklearn.preprocessing import MinMaxScaler

minmax_scaler = MinMaxScaler()
raw_data = minmax_scaler.fit_transform(raw_data)

print(raw_data[:10])
[[0. 0.  1.   0.51351351 0.5210084  0.0840708   0.18133522 0.15030262 0.1323239  0.14798206 0.75 ]
 [0. 0.  1.   0.37162162 0.35294118 0.07964602  0.07915707 0.06624075 0.06319947 0.06826109 0.25 ]
 [1. 0.  0.   0.61486486 0.61344538 0.11946903  0.23906499 0.17182246 0.18564845 0.2077728  0.75 ]
 [0. 0.  1.   0.49324324 0.5210084  0.11061947  0.18204356 0.14425017 0.14944042 0.15296462 0.75 ]
 [0. 1.  0.   0.34459459 0.33613445 0.07079646  0.07189658 0.0595158  0.05134957 0.0533134  0.25 ]
 [0. 1.  0.   0.47297297 0.41176471 0.0840708   0.12378254 0.09414929 0.10138249 0.1180867  0.5  ]
 [1. 0.  0.   0.61486486 0.60504202 0.13274336  0.27465911 0.15870881 0.18564845 0.32735426 0.75 ]
 [1. 0.  0.   0.63513514 0.62184874 0.11061947  0.27129449 0.19704102 0.1961817  0.25759841 0.75 ]
 [0. 0.  1.   0.54054054 0.52941176 0.11061947  0.17974146 0.14492266 0.14746544 0.16292975 0.75 ]
 [1. 0.  0.   0.64189189 0.64705882 0.13274336  0.31609704 0.21082717 0.19815668 0.31738914 0.75 ]]

결과를 보면 전체 데이터가 모두 0~1 사이의 값으로 변환된 것을 알 수 있습니다.

상관관계 조사(Correlation Surveying)

상관관계는 특정 특성에 대해 다른 특성들이 얼마나 영향을 주는지에 대한 척도라고 할 수 있습니다. 가장 흔히 사용되는 상관관계 조사는 표준 상관계수(Standard Correlation Coefficient)로 판다스의 corr 매서드를 통해 쉽게 얻을 수 있습니다.

글의 진행을 위해 사용한 샘플 데이터에 대한 소개는 아래 글을 참고하기 바랍니다.

분석가 관점에서 데이터를 개략적으로 살펴보기

아래의 코드는 샘플 데이터의 특성 중 ring_cnt에 영향을 주는 다른 특성의 표준 상관계수를 구해 출력합니다.

import pandas as pd

raw_data = pd.read_csv('./datasets/datasets_1495_2672_abalone.data.csv', 
    names=['sex', 'tall', 'radius', 'height', 'weg1', 'weg2', 'weg3', 'weg4', 'ring_cnt'])

corr_matrix = raw_data.corr()
print(corr_matrix["ring_cnt"].sort_values(ascending=False))

결과는 다음과 같습니다.

표준상관계수는 특성간의 선형적인 관계를 추출해줍니다. 즉, 선형적으로 비례하면 기울기 1에 가깝고 반비례하면 기울기 -1에 가깝습니다. 선형적으로 관계가 약하면 0에 가깝게 됩니다. 위의 결과를 보면 ring_cnt 특성 중 weg4가 가장 큰 선형 관계를 가지지만 다른 특성 역시 비슷한 선형적 관계를 가지고 있습니다.

아래의 코드처럼 상관관계 조사를 위해 각 특성들을 1:1로 매칭(x축, y축)으로 분포도를 쉽게 출력하여 시각적으로 상관관계를 파악할 수 있습니다.

from pandas.plotting import scatter_matrix
scatter_matrix(raw_data)
plt.show()

결과는 다음과 같습니다.

동일한 특성에 대한 그래프는 아차피 기울기가 1인 선형이므로 히스트그램으로 표시됩니다. ring_cnt를 X축으로 하는 다른 그래프를 살펴보면 height 특성을 제외하고 매우 밀접한 상관관계를 갖고 있음을 알 수 있습니다. 이는 표준 상관계수에서 파악하지 못한 내용입니다.

계층적 샘플링(Stratified Sampling)

계층적 샘플링이란 모집단의 데이터 분포 비율을 유지하면서 데이터를 샘플링(취득)하는 것을 말합니다. 예를들어, 모집단의 남녀 성비가 각각 54%, 46%라고 한다면 이 모집단에서 취득한 샘플 데이터 역시 남녀 성비가 각각 54%, 46%가 되도록 하는 것입니다.

계층적 샘플링의 실제 활용은 학습 데이터와 테스트 데이터 또는 검증 데이터를 일정한 비율로 나눠 구분할때 반드시 적용되어야 합니다. 계층적 샘플링을 적용하지 않고 분할한다고 해도 확률적으로 비율이 유지될 수 있다고 기대하겠지만 이는 상황에 따라 적절한 안정장치가 되지 못합니다.

간단한 데이터셋을 통해 이 계층적 샘플링을 적용하는 내용을 정리하겠습니다. 데이터셋은 아래의 글에서 소개한 전복 데이터입니다.

분석가 관점에서 데이터를 개략적으로 살펴보기

위의 글에서 파악한 전복 데이터를 가져오는 코드는 다음과 같습니다.

import pandas as pd

raw_data = pd.read_csv('./datasets/datasets_1495_2672_abalone.data.csv', 
        names=['sex', 'tall', 'radius', 'height', 'weg1', 'weg2', 'weg3', 'weg4', 'ring_cnt'])

이제 이 데이터셋에서 지름(radius)를 총 5개의 계층으로 나누고, 분포를 시각화해봅니다. 지름을 계층적 샘플링의 기준으로 삼은 이유는 이 지금이 분석하고자 하는 결과에 가장 중요한 의미를 가진다는 어떤 판단(대표적으로 표준상관계수;Standard Correlation Coefficient 분석을 통함)에 의함입니다.

import numpy as np
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

raw_data["radius_cat"] = pd.cut(raw_data["radius"], bins=[0., 0.13, 0.28, 0.35, 0.56, np.inf], labels=[1,2,3,4,5])
raw_data["radius_cat"].hist()
plt.show()

[0,0.13)을 1로, [0.13,0.28]을 2로, [0.28,0.35)를 3으로, [0.35,0.56)을 4로, [0.56,inf]를 5로 계층화시킨 값을 radius_cat 컬럼에 추가하고, 각 계층별 분포 파악을 위한 히스토그램은 위 코드의 결과로써 다음과 같습니다.

이제 이 데이터셋을 학습 데이터셋과 테스트 데이터셋으로 나누는 코드는 다음과 같습니다.

from sklearn.model_selection import StratifiedShuffleSplit

split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
for train_index, test_index in split.split(raw_data, raw_data["radius_cat"]):
    strat_train_set = raw_data.loc[train_index]
    strat_test_set = raw_data.loc[test_index]

strat_train_set["radius_cat"].hist()
plt.show()
strat_test_set["radius_cat"].hist()
plt.show()

계층적 샘플링된 학습 데이터셋과 테스트 데이터셋은 각각 strat_train_set, strat_test_set 인데요. 이 두 데이터셋에 대한 분포를 히스트그램으로 표시해 보면 다음과 같습니다.

위의 결과를 보면 시각적으로도 학습 데이터셋과 테스트 데이터셋에서 지름에 대한 컬럼에 대해 원본 데이터셋과 동일 비율로 구성되고 있다는 것을 알 수 있습니다.

앞서 계층적 샘플링을 위해 추가한 radius_cat 필드는 더 이상 필요치 않으므로 다음 코드를 통해 제거할 수 있습니다.

for d in (strat_train_set, strat_test_set):
    d.drop("radius_cat", axis=1, inplace=True)

끝으로 특성간의 상관관계를 조사하기 위한 방법은 아래 글을 참고 하기 바랍니다.

상관관계 조사(Correlation Surveying)

선형 모델을 이용한 n차 다항식의 회귀

선형 모델은 1차 다항식인 직선에 대한 모델 만을 예측할 수 있습니다. 그렇다면 직선이 아닌 곡선, 즉 2차 다항식 이상의 모델을 예측하기 위해서는 선형 모델을 사용할 수 없다고 생각할 수 있습니다. 하지만 생각과는 다르게 선형 모델로도 2차 다항식 이상의 모델도 예측할 수 있는데, 이는 약간의 발상의 전환이 필요합니다. 즉, 선형 모델의 경우 특징 변수가 1차로 다항식으로만 구성되어 있습니다. 만약 x^2와 같은 거듭제곱인 2차식의 경우일때 더 이상 선형 모델이 아니게 되지만, x^2을 z라는 1차 다항식으로 취급하게 되면 선형 모델로도 2차 이상의 다항식도 회귀분석이 가능합니다.

다음의 코드는 3차 다항식에 대한 회귀분석에 대한 코드입니다.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression

np.random.seed(3224)
m = 100
X = 10 * np.random.rand(m, 1) - 5
y = (-0.8 * X**3) + (0.5 * X**2) + (2 * X) - 3 + (np.random.randn(m, 1) * 10)

poly_features = PolynomialFeatures(degree=3, include_bias=False)
X_poly = poly_features.fit_transform(X)

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

X_new = np.linspace(-5, 5, 20).reshape(20, 1)
X_new_poly = poly_features.transform(X_new)
y_new = model.predict(X_new_poly)

plt.plot(X, y, "b.")
plt.plot(X_new, y_new, "r-")
plt.show()

결과는 다음과 같습니다.

코드를 살펴보면, 6-9는 잡음이 섞인 샘플 데이터는 3차 다항식의 형태로 구성합니다. 11-12는 1개의 특성을 2차항과 3차항에 대한 독립적인 특성을 추가로 생성해 줍니다. 즉, 특성값이 2라면 4와 8이라는 특성값이 생성됩니다.