lm + rf : 괜찮은 혼종
랜덤포레스트의 한계
부스팅의 경우도 마찬가지지만, 의사결정나무를 기반으로 한 랜덤포레스트는 반응곡선(response curve) 또는 반응곡면(response surface)가 수평선, 수평면일 수 밖에 없다. 물론 멀리서 보면 곡선으로 볼 수도 있겠지만, 아주 자세히 들어가보면 작은 수평선 구간의 모음이다.
이는 회귀: 내삽과 외삽에서도 설명했듯이 외삽에서는 엄청난 페널티로 작용할 수밖에 없다. 다시 말해 기울기가 0이 아닌 직선을 랜덤포레스트로 학습할 수도 있겠지만, 선형회귀를 사용한다면 10개 내외의 데이터로 충분히 학습할 수 있는 모형을 1000개 이상의 데이터를 써도 잘 학습하지 못할 수 있는 것이다. (예. 피처 엔지니어링 2)
이론적으로 뒷받침된 선형 회귀의 놀라운 외삽 능력과 랜덤포레스트의 안정적인 내삽능력을 합칠 순 없을까?
모형
데이터 생성 모형은 다음과 같다.
\[y = x_1 + \sin(x_2) + \cos(x_3 + \pi) + y_0(x_1, x_2, x_3) + e, \ \ e \sim \mathcal{N}(0,0.5^2)\]
library(randomForest)
library(dplyr)
library(tidyr)
library(forcats)
library(ggplot2)
x1 = rnorm(1000)
x2 = runif(1000, -3, 3)
x3 = 6*(rbeta(1000, 2, 1)-1/2)
#e = runif(1000, -1, 1)
e = rnorm(1000, 0, 0.5)
dat = data.frame(x1, x2, x3)
dat$y0 = with(dat, ifelse(x2 < -0.5, 1,
ifelse(x1 > 0.5 & x3 < -0.5, -2,
ifelse(x1 > 1 & x2 > 1.1 & (x3 > 1 & x3 <1.4), 0.5, 0))))
dat$y = with(dat, 1*x1 + sin(x2) + cos(x3+pi) + dat$y0 + e)
그래프를 그려보면 다음과 같다.
pairs(dat %>% select(x1, x2, x3, y))
선형 회귀 + 랜덤 포레스트
뚜렷한 관계 또는 이론적 관계
위의 그래프를 보면 \(y\) 와 \(x_1\) 의 선형적 관계를 뚜렷이 관찰할 수 있다. 손쉽게 생각할 수 있는 모형은 다음과 같은 선형 모형이다.
\[y = \beta_0 + \beta_1 x_1 + e\]
summary(lm(y ~ x1, dat))
## ## Call: ## lm(formula = y ~ x1, data = dat) ## ## Residuals: ## Min 1Q Median 3Q Max ## -3.5220 -0.6975 0.0033 0.6982 3.0868 ## ## Coefficients: ## Estimate Std. Error t value Pr(>|t|) ## (Intercept) 0.29561 0.03193 9.258 <2e-16 *** ## x1 0.92066 0.03177 28.979 <2e-16 *** ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## ## Residual standard error: 1.009 on 998 degrees of freedom ## Multiple R-squared: 0.457, Adjusted R-squared: 0.4564 ## F-statistic: 839.8 on 1 and 998 DF, p-value: < 2.2e-16
이제 본격적으로 ML을 하기 전에, 모형 성능을 평가하는 방법을 CV(Cross Validation)로 하겠다(참고).
createfolds = function(N, k) {
isamp <- sample(N)
split(isamp, cut(1:N,
breaks=N*seq(0, 1, length.out=k+1),
include.lowest=TRUE))
}
folds <- createfolds(1000,5)
실례
이제 첫 번째 훈련-검증셋에 대해 기계학습을 해보자. 평가는 MAE(Mean Absolute Error)를 사용하겠다.
1. 오직 랜덤포레스트
Xtrain = dat[-folds[[1]], ] %>% select(x1:x3) %>% as.matrix
ytrain = dat[-folds[[1]],] %>% pull(y)
Xtest = dat[folds[[1]], ] %>% select(x1:x3) %>% as.matrix
ytest = dat[folds[[1]],] %>% pull(y)
fitRF <- randomForest(x=Xtrain, y=ytrain)
ypred <- predict(fitRF, newdata=Xtest)
caret::MAE(ypred, ytest)
## [1] 0.4937447
2. 선형회귀 + 랜덤포레스트
앞서 얘기했듯이 랜덤포레스트는 선형적 관계에 둔감하다. 이제 뚜렷이 나타난 선형 관계를 선형 회귀를 사용하여 모델링하고, 나머지 잔차를 랜덤포레스트로 예측해보자. 구체적으로 다음과 같다. 학습은 lm
, randomForest
로 진행하고, 예측은 predict
, predict
의 값을 합하는 것으로 이루어진다. 1번보다 성능이 좋은가?
fitLM <- lm(y ~ x1, data=dat[-folds[[1]],])
fitRF2 <- randomForest(x=Xtrain[, c('x2', 'x3')], y=resid(fitLM))
ypredLM <- predict(fitLM, newdata = dat[folds[[1]],])
residLM <- predict(fitRF2, newdata = Xtest[, c('x2', 'x3')])
ypred2 <- ypredLM + residLM
caret::MAE(ypred2, ytest)
## [1] 0.5009684
CV(1 vs 2)
한두 번의 결과만으로 두 방법을 비교하기에는 무리가 있으므로 CV를 해본다.
dfMAE <- data.frame(rf = rep(NA, 5), lmrf = rep(NA,5))
for (i in 1:5) {
Xtrain = dat[-folds[[i]], ] %>% select(x1:x3) %>% as.matrix
ytrain = dat[-folds[[i]],] %>% pull(y)
Xtest = dat[folds[[i]], ] %>% select(x1:x3) %>% as.matrix
ytest = dat[folds[[i]],] %>% pull(y)
fitRF <- randomForest(x=Xtrain, y=ytrain)
ypred <- predict(fitRF, newdata=Xtest)
dfMAE[i, 1] <- caret::MAE(ypred, ytest)
fitLM <- lm(y ~ x1, data=dat[-folds[[i]],])
fitRF2 <- randomForest(x=Xtrain[, c('x2', 'x3')], y=resid(fitLM))
ypredLM <- predict(fitLM, newdata = dat[folds[[i]],])
residLM <- predict(fitRF2, newdata = Xtest[, c('x2', 'x3')])
ypred2 <- ypredLM + residLM
dfMAE[i, 2] <- caret::MAE(ypred2, ytest)
}
dfMAE %>% gather(key='key', value='value', rf:lmrf) %>%
ggplot(aes(x=key, y=value, col=key)) +
geom_boxplot() +
geom_point()
상호작용을 포함하기
우리는 참모형을 알고, 참모형에서 \(x_1\) 은 다른 예측 변수와 상호작용을 않아보이지만, 사실 \(y_0(x1, x2, x3)\) 엣 상호작용이 있음을 알 수 있다. 따라서 선형 회귀 이후 잔차 예측에 \(x_1\) 을 다시 사용한다면 성능이 증가할 수 있다. 그리고 정말 그렇다!
dfMAE <- data.frame(rf = rep(NA, 5), lmrf = rep(NA,5), `lmrf+I` = rep(NA,5))
for (i in 1:5) {
Xtrain = dat[-folds[[i]], ] %>% select(x1:x3) %>% as.matrix
ytrain = dat[-folds[[i]],] %>% pull(y)
Xtest = dat[folds[[i]], ] %>% select(x1:x3) %>% as.matrix
ytest = dat[folds[[i]],] %>% pull(y)
fitRF <- randomForest(x=Xtrain, y=ytrain)
ypred <- predict(fitRF, newdata=Xtest)
dfMAE[i, 1] <- caret::MAE(ypred, ytest)
fitLM <- lm(y ~ x1, data=dat[-folds[[i]],])
fitRF2 <- randomForest(x=Xtrain[, c('x2', 'x3')], y=resid(fitLM))
ypredLM <- predict(fitLM, newdata = dat[folds[[i]],])
residLM <- predict(fitRF2, newdata = Xtest[, c('x2', 'x3')])
ypred2 <- ypredLM + residLM
dfMAE[i, 2] <- caret::MAE(ypred2, ytest)
fitLM <- lm(y ~ x1, data=dat[-folds[[i]],])
fitRF2 <- randomForest(x=Xtrain[, c('x1', 'x2', 'x3')], y=resid(fitLM))
ypredLM <- predict(fitLM, newdata = dat[folds[[i]],])
residLM <- predict(fitRF2, newdata = Xtest[, c('x1', 'x2', 'x3')])
ypred2 <- ypredLM + residLM
dfMAE[i, 3] <- caret::MAE(ypred2, ytest)
}
dfMAE %>% gather(key='key', value='value', rf:lmrf.I) %>%
ggplot(aes(x=key, y=value, col=key)) +
geom_boxplot() +
geom_point()
dfMAE$i = 1:5
dfPlot <- dfMAE %>% gather(key='key', value='value', rf:lmrf.I)
dfPlot$key = dfPlot$key %>%
fct_reorder(dfPlot$value)
dfPlot %>%
ggplot(aes(x=key, y=value, col=key)) +
geom_line(aes(group=i), col='gray20') +
geom_point(size=5, col='white') +
geom_point(size=4)
선형 회귀 한 번 더!
우리는 산점도 행렬을 그려본 후, 선형 회귀를 하고, 랜덤포레스트를 했다. 그리고 선형 회귀를 하기 위해 뭔가 특별한 이론이 있었던 것은 아니다. 그냥 산점도 행렬을 보고 추측했다. 다시 말해 예측하고자 하는 \(y\) 와 예측 변수 \(x_1\) , \(x_2\) , \(x_3\) 의 관계에 대해 그럴싸한 가설을 만들었다는 것이다.
랜덤포레스트는 첫 번째 선형 회귀 결과로 얻은 잔차를 \(x_1\) , \(x_2\) , \(x_3\) 로 예측하려고 한다. 이들에 대해 산점도 행렬을 다시 그려 보자!
포물선이 보이지 않은가?
fitLM_all <- lm(y~x1, dat)
dat$resid <- resid(fitLM_all)
pairs(dat %>% select(resid, x1, x2, x3))
fitLM_all2 <- lm(resid ~ I(x3^2), dat)
dat$resid2 <- resid(fitLM_all2)
pairs(dat %>% select(resid2, x1, x2, x3))
그래서 랜덤 포레스트를 하기 전에 선형회귀를 한 번 더 하기로 했다. 이번에 다항회귀를 하겠다.
결과는…
dfMAE <- data.frame(rf = rep(NA, 5), lmrf = rep(NA,5),
`lmrf+I` = rep(NA,5),
'lm2rf' = rep(NA,5))
for (i in 1:5) {
Xtrain = dat[-folds[[i]], ] %>% select(x1:x3) %>% as.matrix
ytrain = dat[-folds[[i]],] %>% pull(y)
Xtest = dat[folds[[i]], ] %>% select(x1:x3) %>% as.matrix
ytest = dat[folds[[i]],] %>% pull(y)
fitRF <- randomForest(x=Xtrain, y=ytrain)
ypred <- predict(fitRF, newdata=Xtest)
dfMAE[i, 1] <- caret::MAE(ypred, ytest)
fitLM <- lm(y ~ x1, data=dat[-folds[[i]],])
fitRF2 <- randomForest(x=Xtrain[, c('x2', 'x3')], y=resid(fitLM))
ypredLM <- predict(fitLM, newdata = dat[folds[[i]],])
residLM <- predict(fitRF2, newdata = Xtest[, c('x2', 'x3')])
ypred2 <- ypredLM + residLM
dfMAE[i, 2] <- caret::MAE(ypred2, ytest)
fitLM <- lm(y ~ x1, data=dat[-folds[[i]],])
fitRF2 <- randomForest(x=Xtrain[, c('x1', 'x2', 'x3')], y=resid(fitLM))
ypredLM <- predict(fitLM, newdata = dat[folds[[i]],])
residLM <- predict(fitRF2, newdata = Xtest[, c('x1', 'x2', 'x3')])
ypred2 <- ypredLM + residLM
dfMAE[i, 3] <- caret::MAE(ypred2, ytest)
datTrain = dat[-folds[[i]],]
fitLM <- lm(y ~ x1, data=datTrain)
datTrain$resid = resid(fitLM)
fitLM2 <- lm(resid ~ I(x3^2), datTrain)
datTrain$resid2 = resid(fitLM2)
fitRF3 <- randomForest(resid2 ~ x1 + x2 + x3, datTrain)
datTest = dat[folds[[i]],]
ypred <- predict(fitLM, newdata = datTest)
residpred <- predict(fitLM2, newdata = datTest)
resid2pred <- predict(fitRF3, newdata = datTest)
ypred3 <- ypred + residpred + resid2pred
dfMAE[i, 4] <- caret::MAE(ypred3, ytest)
}
dfMAE %>% gather(key='key', value='value', rf:lm2rf) %>%
ggplot(aes(x=key, y=value, col=key)) +
geom_boxplot() +
geom_point()
dfMAE$i = 1:5
dfPlot <- dfMAE %>% gather(key='key', value='value', rf:lm2rf)
dfPlot$key = dfPlot$key %>%
fct_reorder(dfPlot$value)
dfPlot %>%
ggplot(aes(x=key, y=value, col=key)) +
geom_line(aes(group=i), col='gray20') +
geom_point(size=5, col='white') +
geom_point(size=4)
나쁘지 않다!
결론
- 선형회귀는 비선형성에 취약하고, 랜덤 포레스트는 외삽에 취약하다.
- 이 둘을 합칠 수 있다!
주의!
- 하지만 위의 방법은 전체 데이터를 통해 \(y\) 와 \(x_1\) , \(x_2\) , \(x_3\) 의 관계를 추정했으므로, 반칙이라고 볼 수 있다. 되도록 이론적인 근거를 통해 외삽 가능한 선형 관계를 생각하는 것이 좋을 것 같다.
Leave a comment