缘由

艺多不压身

碎碎念

我们继续飞行在云层和雾气之间,飞行在一个无光无影的空洞世界中。 ——《风沙星辰》

Ref

10 Resampling for Evaluating Performance | Tidy Modeling with R

解读

从简单回归到交叉验证:tidymodels 模型评估与解释

一、准备数据

使用 Ames 房价数据集,并对房价取对数变换:

library(tidymodels)
library(modeldata)
tidymodels_prefer()
 
data(ames)
ames <- mutate(ames, Sale_Price = log10(Sale_Price))
 
set.seed(502)
ames_split <- initial_split(ames, prop = 0.80, strata = Sale_Price)
ames_train <- training(ames_split)
ames_test  <- testing(ames_split)

这里我们把数据分成 训练集 (80%) 和 测试集 (20%),并且按照房价分层抽样,保证训练/测试数据的分布一致。


二、最简单的线性回归

我们先用车库面积来预测房价:

lm_model <- linear_reg() %>% set_engine("lm")
lm_fit <- fit(lm_model, Sale_Price ~ Garage_Area, data = ames_train)
 
ames_test_res <- predict(lm_fit, new_data = ames_test) %>%
  bind_cols(ames_test %>% select(Sale_Price))

这里只用一个特征(Garage_Area),模型简单但可解释性很强,缺点是可能欠拟合。


三、随机森林:建更强的模型

尝试随机森林,使用更多特征:

rf_model <- 
  rand_forest(trees = 1000) %>% 
  set_engine("ranger") %>% 
  set_mode("regression")
 
rf_wflow <- 
  workflow() %>% 
  add_formula(
    Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built + 
                  Bldg_Type + Latitude + Longitude) %>% 
  add_model(rf_model) 
 
rf_fit <- rf_wflow %>% fit(data = ames_train)
  • 这里选择 1000 棵树,避免过高的方差。
  • 相比线性回归,随机森林能建模非线性关系,但也可能学习到噪音。

四、评估函数:统一比较模型

我们写一个函数来计算 RMSE 和 R²:

estimate_perf <- function(model, dat) {
  cl <- match.call()
  obj_name <- as.character(cl$model)
  data_name <- gsub("ames_", "", as.character(cl$dat))
  
  reg_metrics <- metric_set(rmse, rsq)
  
  model %>%
    predict(dat) %>%
    bind_cols(dat %>% select(Sale_Price)) %>%
    reg_metrics(Sale_Price, .pred) %>%
    select(-.estimator) %>%
    mutate(object = obj_name, data = data_name)
}
 
estimate_perf(rf_fit, ames_train)
estimate_perf(lm_fit, ames_train)
estimate_perf(lm_fit, ames_test)

这样就能直接比较 不同模型、不同数据集 下的表现。


五、交叉验证:稳健的性能评估

单次划分可能不稳定,因此引入交叉验证:

set.seed(1001)
ames_folds <- vfold_cv(ames_train, v = 10)  # 10 折交叉验证
ames_folds

其他方法:

  • 重复交叉验证:vfold_cv(..., repeats = 5)
  • 留一法 (LOOCV):每次只留一个样本做验证
  • 蒙特卡洛采样:随机采样 9/10 的数据,重复多次
  • 自助法 (Bootstrap):对训练集有放回抽样

这些方法能帮助我们更全面地估计模型的泛化能力。


六、在交叉验证中保存预测结果

keep_pred <- control_resamples(save_pred = TRUE, save_workflow = TRUE)
 
set.seed(1003)
rf_res <- rf_wflow %>% 
  fit_resamples(resamples = ames_folds, control = keep_pred)
 
collect_metrics(rf_res)
assess_res <- collect_predictions(rf_res)
  • control_resamples():控制 resampling(重采样/交叉验证)的行为 。

    • save_pred = TRUE:作用:保存交叉验证中每折的预测结果(.pred 列)。可以后续画图(比如预测 vs 实际),而不仅仅是最终的 RMSE、R² 之类的指标。
    • save_workflow = TRUE :保存整个 workflow 对象,也就是预处理 recipe + 模型。你可以回头检查到底在交叉验证中用的是什么配方、什么参数的模型(非常方便调试或解释)。
  • fit_resamples()这是执行交叉验证的主力函数。resamples = ames_folds

    • ames_folds 是之前用 vfold_cv() 生成的 10 折交叉验证数据。
    • fit_resamples() 会自动循环:在每一折中用训练子集拟合模型,用验证子集做预测。

可视化预测效果:

assess_res %>% 
  ggplot(aes(x = Sale_Price, y = .pred)) + 
  geom_point(alpha = .15) +
  geom_abline(color = "red") + 
  coord_obs_pred() + 
  ylab("Predicted")

如果点接近红色对角线,说明模型预测较准。


七、配方 Recipe:系统化特征工程

在实际问题中,数据预处理步骤很多。Recipe 能把它们组织起来:

ames_rec <- recipe(
  Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built + 
                Bldg_Type + Latitude + Longitude, data = ames_train) %>%
  step_other(Neighborhood, threshold = 0.01) %>%   # 合并稀有类别
  step_dummy(all_nominal_predictors()) %>%         # 独热编码
  step_interact(~ Gr_Liv_Area:starts_with("Bldg_Type_")) %>% # 交互项
  step_ns(Latitude, Longitude, deg_free = 20)      # 平滑拟合经纬度

这样一来,整个预处理流程可以无缝嵌入 workflow,保证训练和预测阶段一致。


八、提取模型与解释

配方加模型放进 workflow:

lm_wflow <- workflow() %>% 
  add_recipe(ames_rec) %>% 
  add_model(linear_reg() %>% set_engine("lm")) 
 
lm_fit <- lm_wflow %>% fit(data = ames_train)

提取模型系数:

get_model <- function(x) {
  extract_fit_parsnip(x) %>% tidy()
}

在交叉验证中保存模型:

ctrl <- control_resamples(extract = get_model)
 
lm_res <- lm_wflow %>%  
  fit_resamples(resamples = ames_folds, control = ctrl)
 
# 查看第一折的模型系数
lm_res$.extracts[[1]][[1]]

这样我们不仅能得到模型性能,还能查看 每一折的模型参数,对比其稳定性,从而进行更深入的解释。