데이터

CARET을 활용한 회귀 모형

데이터할아버지 2022. 9. 4. 16:31

본 내용은 Caret 패키지를 이용하여 Palmer Penguin 데이터의 펭귄체중을 예측하는 회귀문제이다. 지금까지 회귀를 수행할 수 있는 다양한 모델(알고리즘)이 개발되어 있으며, caret 패키지는 모델별 개별 학습보다 다양한 모델을 통합해서 머신러닝을 수행하고 결과를 비교평가 할 수 있는 장점이 있다. 최근 R 기반 머신러닝의 추세는 caret에서 tidymodels로 변화되는 추세에 있다. 그러나 ADP 실기 등 현실에서는 tidymodels의 최신 버전 패키지들을 사용하는데 제한이 많아 여전히 익숙한 caret 기반 모델링을 활용하고 있는 실정이다. 본 실습에서는 데이터 처리와 관련된 부분 즉 머신러닝 수행 이전 준비단계에서는 tdidymodel의 rsample과 recipe를 사용하여 데이터를 분할 및 전처리를 수행한 다음 caret을 이용하여 다양한 회귀 모델로 학습하고, 그 결과를 비교한 후 검증용 데이터로 예측하는 것 까지 머신러닝 전체 프로세스를 다룬다. 본 실습에서 중점을 두고자 했던 것은 데이터의 전처리에 대한 세부적인 내용이나 모델별 파라메터 튜닝 등 세부적인 내용보다 머신러닝 전체적인 프로세스의 흐름에 방점을 두고 수행하였다.

데이터 살펴보기

팔머 펭귄데이터는 남극의 팔머열도 일대의 펭귄에 대한 조사 데이터로 펭귄의 종류, 부리길이, 부리두께, 날개길이와 체중, 성별이 포함된 데이터이다. iris 데이터 보다 형식이 다양해서 데이터 분석이나 머신러닝의 예제에 많이 활용되는 데이터이다.


glimpse(penguins) 
Rows: 344
Columns: 8
$ species           fct Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Adel…
$ island            fct Torgersen, Torgersen, Torgersen, Torgersen, Torgerse…
$ bill_length_mm    dbl 39.1, 39.5, 40.3, NA, 36.7, 39.3, 38.9, 39.2, 34.1, …
$ bill_depth_mm     dbl 18.7, 17.4, 18.0, NA, 19.3, 20.6, 17.8, 19.6, 18.1, …
$ flipper_length_mm int 181, 186, 195, NA, 193, 190, 181, 195, 193, 190, 186…
$ body_mass_g       int 3750, 3800, 3250, NA, 3450, 3650, 3625, 4675, 3475, …
$ sex               fct male, female, female, NA, female, male, female, male…
$ year              int 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007…
#  문제 해결과 연관이 별로 없는 year와 island 제거하고 시작한다.   
dat <- penguins %>% select(-year, -island)

# 결측치 확인
dat %>% is.na() %>% colSums()   # 컬럼별 결측치 확인 
          species    bill_length_mm     bill_depth_mm flipper_length_mm 
                0                 2                 2                 2 
      body_mass_g               sex 
                2                11 
dat %>% skim()
Data summary
Name Piped data
Number of rows 344
Number of columns 6
_______________________  
Column type frequency:  
factor 2
numeric 4
________________________  
Group variables None

Variable type: factor

skim_variable n_missing complete_rate ordered n_unique top_counts
species 0 1.00 FALSE 3 Ade: 152, Gen: 124, Chi: 68
sex 11 0.97 FALSE 2 mal: 168, fem: 165

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
bill_length_mm 2 0.99 43.92 5.46 32.1 39.23 44.45 48.5 59.6 ▃▇▇▆▁
bill_depth_mm 2 0.99 17.15 1.97 13.1 15.60 17.30 18.7 21.5 ▅▅▇▇▂
flipper_length_mm 2 0.99 200.92 14.06 172.0 190.00 197.00 213.0 231.0 ▂▇▃▅▂
body_mass_g 2 0.99 4201.75 801.95 2700.0 3550.00 4050.00 4750.0 6300.0 ▃▇▆▃▂

탐색적 데이터 분석(Explatory Data Analysis)

탐색적 데이터 분석은 말 그대로 데이터를 만져 보면서 데이터에 대한 특성을 파악하는 과정이다. EDA를 어떻게 하는 것인가 에 대한 정답은 없다. 이 과정을 통해서 데이터 안에 숨어있는 스토리들을 찾아 낼 수 있다. EDA는 데이터의 가시화를 통해 구체적인 내용들을 파악해 나가게 된다. 머신러닝을 위한 EDA는 데이터 규모와 특성 변수들의 속성(type)과 분포, 변수별 결측치 및 이상치의 존재 여부 등 머신러닝 수행에 필요한 데이터 전처리 소요를 파악하는 과정이다.

dat %>%
  na.omit() %>%
  ggplot() +
  geom_boxplot(aes(x= species, y= body_mass_g, fill = sex)) +
  labs(title = " 펭귄 종류별 성별 체중 분포")
dat %>%    
  select_if(is.numeric) %>%
  pivot_longer(col= everything()) %>%   
  ggplot() +
  geom_boxplot(aes(x=name, y = value)) +   
  facet_wrap(~ name, scales ="free") +
  labs(title = " 연속형 변수의 BOX PLOT ")
dat %>%
  select_if(is.numeric) %>%
  pivot_longer(col= everything()) %>%
  ggplot() +
  geom_density(aes(x=value)) +  
  facet_wrap( ~ name, scale = "free")
dat %>%
  select_if(is.numeric) %>%
  na.omit() %>%
  cor() %>% 
  corrplot(method = "number")
dat %>%
  ggpairs()

데이터 분할과 전처리

일반적으로 caret에서 데이터 분할 할 때는 createDataPartition() 함수를, 전처리 할 때는 preProcess()함수를 사용한다. 여기에서는 특별히 tidymodels의 initial_split() 함수와 recipe() 함수를 사용하여 데이터를 분할하고 전처리를 수행하였다. 데이터의 분할과 전처리 관련 세부 내용은 데이터 전처리 관련 포스팅을 참고하면 된다. 데이터는 학습용과 검증용을 80:20으로 분할하여 사용하였다. 필요한 전처리는 recipe 패키지의 step_* 함수를 사용하여 수행하였으며, 결측치 제거, 팩터형 변수 더미화, 상관관계가 높은 변수를 제거하였다.
전체 데이터를 한꺼번에 전처리를 한 후 학습용과 검증용으로 나누어 머신러닝을 수행하는 것보다는 학습용 데이터와 검증용 데이터를 선 분할 한 다음 각각의 데이터를 동일한 방법으로 전처리 하는 것을 더 권장 하고 있다. recipe 패키지를 사용하는 이유도 이런 방식의 데이터 전처리에 recipe가 더 효과적이기 때문이다.

# 데이터 분할 
set.seed(6439)
splits <- initial_split(dat, prop = 0.8, strata = body_mass_g)
train  <- training(splits)
test   <- testing(splits)

# 데이터 전처리 :  step_*  
recipe_p <- train %>%
  recipe(body_mass_g ~.) %>%
  step_impute_bag(where(is.numeric), impute_with = imp_vars(all_predictors())) %>% 
  step_impute_mode(where(is.factor)) %>%
  step_dummy(all_nominal(), one_hot = F) %>%
  step_corr(threshold = 0.8) %>%     
  step_nzv() %>%
  prep()  

train <- recipe_p %>% juice()      # 학습용 데이터 
test <-  recipe_p %>% bake(test)   # 검증용 데이터 

머신러닝 수행

이제 데이터가 준비되었으니 다양한 머신러닝 알고리즘으로 으로 회귀를 수행해보자. 여기서 수행하는 회귀는 랜덤포레스트, 페털티회귀(Ridge, Lasso, Elastic회귀), SVM, 의사결정나무 모형으로 수행하였다. 앞서 언급했듯이 이 글은 caret 패키지로 머신러닝을 수행하는 프로세스 전반에 대한 리뷰로 각각의 모델에 대한 상세한 설명은 생략한다.
각 모델로 학습을 수행하기 전, 머신러닝 모형들이 학습간에 적용할 몇가지 공통된 기준을 먼저 설정하고 학습간에 모델이 동일한 기준을 적용하게 하여 공평한 성능 비교가 되도록 기준을 설정하였다. 이런 준비과정을 “테스트 하네스”를 만든다는 표현을 사용하기도 한다. 여기서는 5-Fold validation 방법을 적용하였고, 평가지표는 RMSE를 적용하였다.

데이터 준비 : 테스트 하네스 준비

caret에서 학습용 데이터와 검증용 데이터를 분할하는 방법은 trainControl() 함수의 method 인자로 지정한다. 가용한 방법에는 “cv”, “repetedcv”, “LOOCV”, “boot” 등이 있으며 여기서는 10-Cross Validation(cv, k = 10)을 사용하였다.

trControl <- trainControl(method = "cv", # cross validation 적용 
                          number = 10)   # k 값은 10
metric <- "RMSE"                         # 평가지표는 RMSE

# ---->>>> 다음과 같이 지정할 수 도 있다.     
# fitControl <- trainControl(method = "repeatedcv",  # 반복적 CV  
#                            number = 10,    
#                            repeats = 10)

데이터 준비도 되었고 테스트 하네스도 만들었으니 이제 모델별로 회귀를 수행해보자.
현실적으로 모든 모형들의 파라메터를 다 외우고 있을 수는 없으니 필요하면 caret에서 파라메터를 확인하는 함수 modleLookup()함수를 사용하여 모델별로 필요한 파라메터를 확인 할 수 있다. 머신러닝에서 튜닝을 한다는 것은 결국 모델의 파라메터 설정값을 어떻게 적용할 것인가의 문제이다. 학습을 시키기 위해 모델별로 다양한 파라라메터 값을 설정해 주어야하는데, 파라메터 종류별 설정 값의 모든 조합을 다 만들어 파라메터를 설정하는 것은 학습에 많은 시간이 소요되는 방법이다.
caret에서는 tuneGrid라는 인자를 통해 여러 가지 파라메터를 조합하여 수행하는 방법과, 랜덤하게 일정한 경우의 수만 지정하여 튜닝하는 tuneLength 방법을 사용하고 있다. 여기에서는 실행 시간 요소들 고려하여 주로 tuneLength 인자를 지정하여 튜닝을 하기로 한다.

랜덤 포레스트 모형

가장 많이 알려지도 하고, 비교의 기준이 되기도 하는 대표적 기계학습 모형이다.

modelLookup("rf")    # 모델별 파라메터를 확인해보려면  
  model parameter                         label forReg forClass probModel
1    rf      mtry #Randomly Selected Predictors   TRUE     TRUE      TRUE
set.seed(6439)

# tuneLength = 5로 수행하자.        
rf_length <- train(body_mass_g ~.,
                 data = train,
                 method = "rf",
                 trControl = trControl,
                 metric = metric,
                 tuneLength  = 5)   
rf_length
Random Forest 

273 samples
  6 predictor

No pre-processing
Resampling: Cross-Validated (10 fold) 
Summary of sample sizes: 246, 246, 246, 246, 247, 245, ... 
Resampling results across tuning parameters:

  mtry  RMSE      Rsquared   MAE     
  2     297.4695  0.8598524  236.5640
  3     302.0707  0.8551533  237.8213
  4     302.7406  0.8543613  238.0366
  5     307.0748  0.8498993  241.3914
  6     308.5088  0.8494708  242.6346

RMSE was used to select the optimal model using the smallest value.
The final value used for the model was mtry = 2.

랜덤포레스트 학습 수행결과를 보면, mtry 파라메터를 2부터 6까지 다섯번 변경시켜 가면서 랜덤포레스트로 회귀를 수행한 결과를 RMSE, Rsquared, MAE 지표로 제시하고 있다. metric을 이용하여 RMSE만을 지정했지만 디폴트로 Rsquared값이나, MAE값도 제시해줌을 알 수 있다. 또한 mtry 파라메터가 2일때 가장 낮은 RMSE 값이 나왔음을 알 수 있다.
랜덤포레스트 경우 펭귄체중을 예측하는 변수 중요도는 다음과 같이 알 수 있다.

varImp(rf_length)           # 변수별 중요도 값 
rf variable importance

                  Overall
species_Gentoo     100.00
flipper_length_mm   98.74
bill_depth_mm       55.49
bill_length_mm      37.48
sex_male            28.10
species_Chinstrap    0.00
plot(varImp(rf_length))     # 변수중요도 플롯

랜텀 포레스트로 회귀를 수행하는데 변수의 중요도는 그래프에서 보는 바와 같다.

실제 검증용 데이터로 예측하는 것은 predict() 함수에 학습 모형을 적용하여 예측을 수행한다. 검증용 데이터로 휘귀예측을 수행하여 RMSE를 구하는 과정은 다음과 같다.

pre_rf <- predict(rf_length, newdata = test)                 
rf_RMSE <- RMSE(pre_rf, test$body_mass_g)
rf_RMSE
[1] 304.0923

실제 펭귄의 체중과 모델로 예측한 값 및 그 차이을 비교하려면 다음과 같이 하면 볼 수 있다.

cbind(body_mass = test$body_mass_g, 
      prediction = round(pre_rf), 
      diff = round(test$body_mass_g-pre_rf)) %>%
head()
  body_mass prediction diff
1      3575       3381  194
2      3700       3317  383
3      3200       3506 -306
4      3700       3329  371
5      4500       4126  374
6      3950       3834  116

SVM

서포트 벡터머신과 인공신경망(Neural Network)은 대표적인 블랙박스 모형이다. 블랙박스 모형이라 함은 내부의 복잡한 수학과 계산으로 인하여 입력과 출력간의 연관성을 쉽게 설명하기 어렵기 때문에 붙여진 이름이다. 그렇지만, 블랙박스 머신이 가지고 있는 성능의 우수성 때문에 많이 사용되고 있는 모형이다.
서포트 벡터 머신은 적용하는 커널 함수에 따라 “svmPoly”와 “svmRadial” 가 있다. svm 모형에서도 train 함수에 앞서 만들어 놓은 테스트 하네스 즉 trainControl 인자와 metric 인자를 그대로 사용한다. train method 만 “svmRadial”로 바꿔서 지정하면 된다. 모델을 정교하게 튜닝하기 위해서 modelLookup()함수로 모델파라메터를 확인한 후 tuneGrid를 다양하고 섬세하게 조정해서 학습을 시킬 수 도 있다. 여기서는 랜텀포레스트와 마찬가지로 tuneLenth를 5로 지정해 학습을 시킨다.

modelLookup("svmRadial")
      model parameter label forReg forClass probModel
1 svmRadial     sigma Sigma   TRUE     TRUE      TRUE
2 svmRadial         C  Cost   TRUE     TRUE      TRUE
set.seed(6439)
svm_length <- train(body_mass_g ~.,
                   data = train,
                   method = "svmRadial",
                   trControl = trControl,
                   metric = metric,
                   tuneLength = 5, 
                   verbose = F)
svm_length
Support Vector Machines with Radial Basis Function Kernel 

273 samples
  6 predictor

No pre-processing
Resampling: Cross-Validated (10 fold) 
Summary of sample sizes: 246, 246, 246, 246, 247, 245, ... 
Resampling results across tuning parameters:

  C     RMSE      Rsquared   MAE     
  0.25  295.4202  0.8656750  241.1138
  0.50  294.6690  0.8651083  240.2304
  1.00  295.8479  0.8631958  241.6367
  2.00  299.8849  0.8576557  243.3283
  4.00  300.0451  0.8561967  242.1209

Tuning parameter 'sigma' was held constant at a value of 0.3574057
RMSE was used to select the optimal model using the smallest value.
The final values used for the model were sigma = 0.3574057 and C = 0.5.

svm의 수행의 결과를 보면 sigma 값은 0.357로 고정하고 C 값을 5가지 값으로 변화 시켜가면서 학습한 결과를 보여주고 있다. 최종 모델은 sigma = 0.357이고 C = 0.5 일때 RMSE가 최소가 됨을 보여주고 있다. 검증용 데이터로 예측을 하기 위해서는 predict() 함수를 사용한다.

pre_svm  <- predict(svm_length, newdata = test)
svm_RMSE <- RMSE(pre_svm, test$body_mass_g)
svm_RMSE
[1] 339.3566

SVM 모형의 변수 중요도 측정은 위에서 사용한 varImp()함수를 사용하는 대신 Permutation 분석을 통해 변수의 중요도를 계산하는 DALEX 패키지를 이용해서 수행한다.
Permutation 분석을 통한 변수 중요도 계산은 회귀에 참여하는 변수의 관측치 순서(Row 순서)를 랜덤하게 섞어 회귀를 수행시켜 보면서 RMSE값의 변화를 살펴보는 개념이다. 만일 설명력이 높은 변수의 관측치 값을 랜덤하게 섞어서 회귀를 수행하면 당연히 예측력이 떨어질(RMSE값은 상승) 것이고 중요하지 않은 변수는 랜덤하게 섞어서 회귀를 수행해도 예측력이 많이 떨어지지 않을 것이라는 가정을 이용하는 것이다.

exp <- DALEX::explain(model = svm_length, 
                      data = train[,-4],   # body_mass_g 제외
                      y = train$body_mass_g)
Preparation of a new explainer is initiated
  -> model label       :  train.formula  (  default  )
  -> data              :  273  rows  6  cols 
  -> data              :  tibble converted into a data.frame 
  -> target variable   :  273  values 
  -> predict function  :  yhat.train  will be used (  default  )
  -> predicted values  :  No value for predict function target column. (  default  )
  -> model_info        :  package caret , ver. 6.0.92 , task regression (  default  ) 
  -> predicted values  :  numerical, min =  3147.365 , mean =  4178.292 , max =  5635.577  
  -> residual function :  difference between y and yhat (  default  )
  -> residuals         :  numerical, min =  -862.2732 , mean =  5.220495 , max =  801.9652  
  A new explainer has been created!  
vip <- model_parts(explainer = exp, B=10) 
plot(vip)       # RMSE 손실값으로 제시 

페널티 회귀분석 개요

많은 독립 변수를 갖는 경우, 모델에 페널티를 부과하여 모델 성능에 기여하지 못하는 변수의 영향력을 축소하거나 제거하는 회귀를 페널티(Penalty)회귀라고 한다. 즉 페널티 회귀분석은 복잡한 모델에 페널티를 부과하여 간명한 회귀모델을 생성하는 것을 말한다. 표준적 선형회귀 모델의 잔차(관측값 - 예측값)의 제곱합에 페널티 항을 추가하여 잔차의 제곱합과 페널티 항의 합이 최소화가 되는 회귀계수를 추정하는 방법을 이른다. 페널티 회귀분석에는 Ridge, Lasso, Elastic 회귀분석이 있다.
Ridge 회귀분석은 회귀계수 제곱 합인 L2_norm을 페널티항으로 부과하여 모델의 설명력에 기여하지 못하는 독립변수의 회귀계수를 0에 근접하도록 축소한다.
반변 Lasso 회귀분석은 회귀계수의 절대값을 합산한 L1_norm을 페널티 항으로 부과하여 설명에 기여하지 못하는 독립변수의 회귀계수를 0으로 한다.
Elastic 회귀는 L1_norm과 L2_norm의 값을 모두 고려하기위하여 두 종류의 파라메터인 알파값과 람다값을 변화시켜가면서 회귀분석을 수행하는 페털티 회귀 방식이다.
페털티 회귀의 특성상 랜덤포레스트나, svm에서 사용한 tuneLength 인자 대신 tuneGrid를 사용하여 알파값과 람다 값을 변화 시켜가면서 회귀를 수행한다.

Ridge 회귀

# ----->>>> Ridge  Regression  <<<<--------
# alpha 값을 0으로 설정하여(위 식에서 L1_norm 제거)  
set.seed(6439)
lambda <- seq(0, 1, 0.1)  # 0에서 1까지 0.1 간격으로 
ridge_penalty <- expand.grid(alpha = 0, lambda = lambda) #알파 = 0 인걸 리지 
ridge_grid <- train( body_mass_g ~.,
                     data = train,
                     method = "glmnet",
                     metric = metric,
                     trControl = trControl,
                     tuneGrid = ridge_penalty)
ridge_grid
glmnet 

273 samples
  6 predictor

No pre-processing
Resampling: Cross-Validated (10 fold) 
Summary of sample sizes: 246, 246, 246, 246, 247, 245, ... 
Resampling results across tuning parameters:

  lambda  RMSE      Rsquared   MAE     
  0.0     293.5082  0.8658677  237.3179
  0.1     293.5082  0.8658677  237.3179
  0.2     293.5082  0.8658677  237.3179
  0.3     293.5082  0.8658677  237.3179
  0.4     293.5082  0.8658677  237.3179
  0.5     293.5082  0.8658677  237.3179
  0.6     293.5082  0.8658677  237.3179
  0.7     293.5082  0.8658677  237.3179
  0.8     293.5082  0.8658677  237.3179
  0.9     293.5082  0.8658677  237.3179
  1.0     293.5082  0.8658677  237.3179

Tuning parameter 'alpha' was held constant at a value of 0
RMSE was used to select the optimal model using the smallest value.
The final values used for the model were alpha = 0 and lambda = 1.
ridge_grid$bestTune   # 알파가 0, 람다가 1일때, RMSE 최소 
   alpha lambda
11     0      1
plot(varImp(ridge_grid)) 
pre_ridge <- predict(ridge_grid, newdata = test)
ridge_RMSE <- RMSE(pre_ridge, test$body_mass_g)
ridge_RMSE
[1] 307.9409

LASSO 회귀

모델의 설명력에 기여하지 못하는 독립변수의 회귀계수를 0으로 하여 설명력에 기여하지 못하는 변수의 회귀계수를 0으로 만들어 모델을 간명하게 한다.

# ----->>>> LASSO Regression  <<<<--------
# Lasso 회귀는 alpha = 1로 설정하여 위식에서 L2_norm 제거, lamda는 페널티 부과 값 
set.seed(6439)
lambda <- seq(0, 1, 0.1)  # 0에서 1까지 0.1 간격으로 
lasso_penalty <- expand.grid(alpha = 1, lambda = lambda) #알파 = 1 인걸 라소 

lasso_grid <- train( body_mass_g ~.,
                     data = train,
                     method = "glmnet",
                     metric = metric,
                     trControl = trControl,
                     tuneGrid = lasso_penalty)
lasso_grid
glmnet 

273 samples
  6 predictor

No pre-processing
Resampling: Cross-Validated (10 fold) 
Summary of sample sizes: 246, 246, 246, 246, 247, 245, ... 
Resampling results across tuning parameters:

  lambda  RMSE      Rsquared   MAE     
  0.0     290.2135  0.8668278  234.4075
  0.1     290.2135  0.8668278  234.4075
  0.2     290.2135  0.8668278  234.4075
  0.3     290.2135  0.8668278  234.4075
  0.4     290.2135  0.8668278  234.4075
  0.5     290.2135  0.8668278  234.4075
  0.6     290.2135  0.8668278  234.4075
  0.7     290.2135  0.8668278  234.4075
  0.8     290.2135  0.8668278  234.4075
  0.9     290.2135  0.8668278  234.4075
  1.0     290.2135  0.8668278  234.4075

Tuning parameter 'alpha' was held constant at a value of 1
RMSE was used to select the optimal model using the smallest value.
The final values used for the model were alpha = 1 and lambda = 1.
# 변수중요도 플롯 
plot(varImp(lasso_grid))

 

# 예측은 ...
pre_lasso <- predict(lasso_grid, newdata = test)
lasso_RMSE <- RMSE(pre_lasso, test$body_mass_g)
lasso_RMSE
[1] 294.8429

Elastic 회귀

#---->>> Elastic net Regression <<<<--------
# alpha 값을 0이나 1인 아닌 값으로 추정하여 alpha와 람다값의 조합으로 회귀분석 
set.seed(6439)
lambda <- 10^seq(-3,3, length =10)   # 통상 문제에서 주어짐 
alpha  <- seq(0, 1, 0.1)             # 0과 1사이 값 
elastic_penalty <- expand.grid(alpha = alpha, lambda = lambda) # 둘다 변하겠지..
elastic_grid <-  train( body_mass_g ~.,
                        data = train,
                        method = "glmnet",
                        metric = metric,
                        trControl = trControl,
                        tuneGrid = elastic_penalty,
                        verbose = FALSE)
elastic_grid
plot(varImp(elastic_grid))   # 변수중요도 
pre_elastic <- predict(elastic_grid, newdata = test)
#head(pre_elastic)
elastic_RMSE <- RMSE(pre_elastic, test$body_mass_g)
elastic_RMSE
[1] 294.8857

Decision Tree

의사결정 나무로 회귀 예측을 수행할 수 있다. 이런 경우의 모형을 회귀나무라고 칭하기도 한다. 회귀나무는 더이상 분류가 곤란하 터미널 노드에 속한 관측치의 평균 값을 예측값으로 추정하는 방식이다.

#---->>> Decision Tree   <<<<--------  
modelLookup("rpart")
  model parameter                label forReg forClass probModel
1 rpart        cp Complexity Parameter   TRUE     TRUE      TRUE
set.seed(6439)

rpart_length <- train(body_mass_g ~.,
                      data = train,
                      method = "rpart",
                      metric = metric,
                      trControl = trControl,
                      tuneLength = 5)
rpart_length
CART 

273 samples
  6 predictor

No pre-processing
Resampling: Cross-Validated (10 fold) 
Summary of sample sizes: 246, 246, 246, 246, 247, 245, ... 
Resampling results across tuning parameters:

  cp           RMSE      Rsquared   MAE     
  0.007337325  341.9583  0.8101518  276.0187
  0.009537013  340.2093  0.8108394  274.3338
  0.084539656  392.7167  0.7520833  320.5445
  0.097061430  464.6919  0.6603322  374.7653
  0.659543202  658.1660  0.5397786  550.0598

RMSE was used to select the optimal model using the smallest value.
The final value used for the model was cp = 0.009537013.
plot(varImp(rpart_length))
pre_rpart <- predict(rpart_length, newdata = test)
rpart_RMSE <- RMSE(pre_rpart, test$body_mass_g)
rpart_RMSE
[1] 320.9721

종합해서 한꺼번에 보기

지금까지 수행했던 모형들의 예측을 모아서 한꺼번에 비교해보자.

set.seed(6439)
jonghap <- resamples(list(RF = rf_length, SVM = svm_length,
                          LASSO = lasso_grid, RIDGE = ridge_grid, 
                          Elastic = elastic_grid,  DT = rpart_length))
summary(jonghap)
Call:
summary.resamples(object = jonghap)

Models: RF, SVM, LASSO, RIDGE, Elastic, DT 
Number of resamples: 10 

MAE 
            Min.  1st Qu.   Median     Mean  3rd Qu.     Max. NA's
RF      170.4687 222.1991 234.1113 236.5640 257.3223 311.7368    0
SVM     182.7281 218.8252 239.4161 240.2304 257.0922 297.6288    0
LASSO   177.1605 222.0884 229.6770 234.4075 252.5977 315.1990    0
RIDGE   185.2112 225.3853 237.3564 237.3179 246.8764 314.0771    0
Elastic 177.6741 222.2601 229.4185 234.3877 252.1960 315.0830    0
DT      211.7864 221.1848 253.5969 274.3338 287.3793 422.4269    0

RMSE 
            Min.  1st Qu.   Median     Mean  3rd Qu.     Max. NA's
RF      216.0716 281.2482 302.8795 297.4695 333.1116 358.4486    0
SVM     212.9128 277.8000 303.3701 294.6690 314.0730 346.0441    0
LASSO   213.6115 266.2800 296.1298 290.2135 320.9854 365.7865    0
RIDGE   222.4999 269.4323 307.4668 293.5082 319.9098 366.5474    0
Elastic 213.7434 266.5024 296.0783 290.1370 321.0426 365.6349    0
DT      260.1709 270.0432 326.2135 340.2093 352.7073 559.8889    0

Rsquared 
             Min.   1st Qu.    Median      Mean   3rd Qu.      Max. NA's
RF      0.8090402 0.8248879 0.8592343 0.8598524 0.8838999 0.9378070    0
SVM     0.8169917 0.8432637 0.8608232 0.8651083 0.8863110 0.9179209    0
LASSO   0.7975399 0.8348958 0.8745080 0.8668278 0.8980141 0.9413840    0
RIDGE   0.8034923 0.8356022 0.8751893 0.8658677 0.8924485 0.9340840    0
Elastic 0.7974355 0.8352464 0.8748302 0.8669054 0.8978939 0.9413714    0
DT      0.5616468 0.7862655 0.8410528 0.8108394 0.8741402 0.9095921    0
dotplot(jonghap, scale = "free", main = " 모델별 Fitting 결과 비교 ")

각 모델에 테스트 데이터를 적용하여 펭귄의 체중을 예측한 결과에 대해 평가척도인 RMSE 값의 평균을 비교한 결과 피팅에서 가장 우수한 알고리즘은 Elastic 모델이었다. 의사결정나무모델은 신뢰구간이 다른 알고리즘에 비해 넓은 특징이 있어 예측의 정확도의 편차가 큼을 예상할 수 있었다. 최종적으로 검증용 데이터로 검증을 해보자.

pred_jonghap <- data.frame(RF = rf_RMSE, SVM = svm_RMSE, 
                           LASSO = lasso_RMSE, RIDGE = ridge_RMSE, 
                           Elastic = elastic_RMSE,  DT = rpart_RMSE)
pred_jonghap
        RF      SVM    LASSO    RIDGE  Elastic       DT
1 304.0923 339.3566 294.8429 307.9409 294.8857 320.9721
pred_jonghap %>%
  pivot_longer(col = everything()) %>%
  ggplot(aes(x= fct_reorder(name, value),  y = value)) +
  geom_col() +
  ggtitle("모델별 Test 데이터 예측 결과 RMSE 값")+
  xlab (" 모델의 종류") + 
  ylab ("RMSE")  

검증용 데이터인 test 데이터로 예측을 수행했을때, RMSE 평가척도를 비교해보면, Lasso 알고리즘이 가장 우수하였으며, Lasso, Elastic, RandomForest 순을 보였다.

맺음말

caret 패키지를 활용하여 penguins 데이터의 펭귄체중을 예측하는 회귀분석 절차를 처음부터 끝까지 수행해 보았다. 머신러닝의 흐름에 중점을 두고 설명하다보니 세부적인 설명이 부족하여 각 모델에 대한 이해가 부족한 경우 이해하기 어려울 수 있겠다는 점이 마음에 걸린다. 하지만 분류분석의 경우에도 평가지표만 다를 뿐 거의 유사한 프로세스를 밟는 만큼 본 글에서 제시한 흐름에 익숙해지면 머신러닝을 수행하는데 도움이 될것이라 생각한다.