회귀분석의 네가지 방법, 선형회귀/의사결정트리/랜덤포레스트/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)