11  模型调整和过拟合的危险

有些参数可以直接从训练数据中估计(如线性回归模型的系数),有些参数则需要事先人工选择(如KNN中的k等),这些无法估计、需事先指定的参数被称为调优参数(tuning parameters)超参数(hyperparameters)

11.1 超参数调优的两种策略

  • 网格搜索(grid search):我们需要预先定义一组要评估的超参数值。网格搜索涉及的主要选择是如何制作网格以及要评估多少个超参数组合,适合同时调整多个超参数。在 Chapter 12 中有详细介绍。

  • 迭代搜索(interative search):或顺序搜索(sequential search),根据之前的结果顺序地发现新的超参数组合。这种方法的优点是可以快速发现最佳超参数组合,缺点是可能错过局部最优解。在 Chapter 13 中有详细介绍。

11.2 tidymodels中的超参数调节

在此前我们已经遇到了很多需要调优的超参数:

  • 将社区归为“other”的阈值。(threshold,Chapter 7, step_other())。
  • 自然样条中的自由度。(deg_free,Chapter 7, step_ns())
  • 树模型中分支时所需的最小样本数。(min_n,Chapter 5)。
  • 惩罚模型中的惩罚系数大小。(penalth,Chapter 5)。

parsnip中的模型设定,有两类超参数:

  • main arguments:这些参数经常被使用且可以在多个引擎(engine)中使用,是模型设定中的顶层参数。例如rand_forest()中的treesmin_nmtry参数,也是最常被调优的参数。

  • engine argument:引擎(engine)特有的参数,这些参数要么很少被使用,要么只适用于某些特定的引擎(engine)。随机森林中,ranger包中就包含一些特有的参数,例如增益惩罚参数,可以帮助权衡随机森林模型中使用的预测变量数量和模型性能。要想在parsnip中使用这些参数,需要在set_engine()函数中直接指定。

  • 在模型设定时,使用tune()函数标记需要被调优的超参数。注意,tune()不执行任何特定的调优操作,只是标记需要调优的参数。tune()可以在建立模型、预处理等多个过程中使用,也可同时对多个相同的预处理步骤中使用。

  • extract_parameter_set_dials()函数可以提取并展示一个对象的超参数集。对应的extract_parameter_dials()函数可以提取特定的超参数。

    • 在提取超参数后,使用update()函数可以更新超参数的值。
    • extract_parameter_set_dials()创建的超参数集可以直接传递给tidymodels中的调优函数使用。
  • recipemodel被整合至一个workflow中时,它们中设定的超参数均会被整合至workflow中。

library(dials)
library(tidymodels)
neural_net_spec <- 
  # 模型指定过程中设定需要调优的超参数
  mlp(hidden_units = tune()) %>% #隐藏层神经元数
  set_mode("regression") %>% 
  set_engine("keras")

# 预处理过程设定需要调优的超参数
ames_rec <-
  recipe(
    Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built + Bldg_Type + Latitude + Longitude,
    data = ames_train
  ) %>%
  step_log(Sale_Price, base = 10) %>%
  step_other(Neighborhood, threshold = tune()) %>% # 阈值超参数
  step_dummy(all_nominal_predictors()) %>%
  step_interact(~ Gr_Liv_Area:starts_with("Bldg_Type_")) %>%
   # 自然样条的自由度,调用两次step_ns()
  step_ns(Latitude, deg_free = tune("latitude df")) %>% 
  step_ns(Longitude, deg_free = tune("longitude df"))

# 整合workflow并提取超参数集
wflow_param <- 
  workflow() %>% 
  add_model(neural_net_spec) %>% 
  add_recipe(ames_rec) %>% 
  extract_parameter_set_dials()
wflow_param

# 提取特定的超参数
wflow_param %>% 
  extract_parameter_dials("hidden_units")

# 更新超参数
update_threshold <- wflow_param %>% 
  update(hidden_units = hidden_units(c(20, 40)))
update_threshold %>% 
  extract_parameter_dials("hidden_units")
Note
  • 每个超参数在dials包中均有一个对应的函数。绝大多数情况下,函数的名称和超参数的名称一致。
  • 直接运行超参数函数会给出该超参数的默认取值范围。

完整的超参数对象在输出时会显示[+],如果显示的符号为[?]则表示该超参数最少有一侧的范围是缺失的,在输出这个超参数集时,tidymodels会提示我们哪个超参数需要最终确定,解决该问题有两个途径:

  • recipe对象还未被整合进workflow中时,可以使用update()函数手动添加。
  • recipe对象已经被整合进workflow中后,如果recipe进行增加或减少列的预处理步骤(例如主成分分析等降维操作),直接使用update()可能就不起作用了。此时可以对workflow对象使用finalize()函数以获取超参数的默认范围。
rf_spec <- 
  rand_forest(mtry = tune()) %>% 
  set_engine("ranger", regularization.factor = tune("regularization")) %>% 
  set_mode("regression")
rf_param <- extract_parameter_set_dials(rf_spec)
rf_param
rf_param %>% 
  update(mtry = mtry(c(1, 70)))
pca_rec <- 
  recipe(Sale_Price ~ ., data = ames_train) %>% 
  # 选择包含SF的预测变量并提取主成分
  step_normalize(contains("SF")) %>% 
  # 选择能解释预测变量95%方差的主成分
  step_pca(contains("SF"), threshold = 0.95)
pca_param <- workflow() %>% 
  add_model(rf_spec) %>% 
  add_recipe(pca_rec) %>% 
  extract_parameter_set_dials()
pca_param

update_pca_param <- pca_param %>% 
  finalize(ames_train) # 根据训练集设定默认超参数范围
update_pca_param
update_pca_param %>% 
  extract_parameter_dials("mtry")