36  网格搜索

library(tidymodels)
tidymodels_prefer()
theme_set(theme_bw())

网格搜索是一种用于优化模型流程调参参数的方法。该方法生成一组预先定义的候选参数取值,并逐一计算每组参数对应的模型性能。之后既可以选取数值表现最优的候选参数,也可以考察调参参数与模型性能之间的变化关系,判断模型是否有必要开展进一步优化。

使用外部数据评估模型至关重要(无论是通过重抽样还是验证集),网格搜索并非免费午餐:我们不能简单地对每个候选参数集拟合模型,再通过重新预测同一组数据来评估其效果。

本章我们以boosted tree模型为例,演示如何使用网格搜索优化调整其中的两个超参数。

提升树是依次生成的多个独立决策树构成的集合。

36.1 规则网格

  • 规则网格是指在每个参数的取值范围内,均匀地划分出若干个候选参数值。
  • 规则网格的优点是简单易用,缺点是可能会错过最优参数组合,尤其是在参数空间较大时。此外,随着参数数量的增加,计算量会呈指数级增长。
  • 对于某些模型和预处理方法而言,即便需要调优的超参数数量较多,也可以通过规则网格搜索找到较优的参数组合。小节 36.3

36.2 不规则网格

在超参数调优中,不规则网格搜索指的是候选参数值的排列不是均匀、等距的,而是由用户自定义的一组组合(可能随意分布或侧重某些区域)。它和规则网格(grid_regular)相对,规则网格会在每个参数轴的等分点上生成所有两两组合。

  • 参数尺度差异大:比如 mtry(整数)和 penalty(对数尺度),用对数间隔比等距更合理。
  • 先验知识驱动:你可能知道某些参数范围效果好,想更有针对性地测试。
  • 减少浪费:避免在毫无希望的区域(如极大学习率)浪费计算。
  • 空间填充设计(space-filling design):在有限预算下更均匀覆盖整个参数空间,目标是确保调参空间被覆盖,同时候选值之间的冗余最小化,胜过规则网格的“轴对齐”效应。空间填充设计有很多类型:
    • 拉丁超立方设计(Latin hypercube design, LHD):是空间填充设计最常用的方法。在每个参数轴上均匀划分区间,并随机选择每个区间的一个点,保证每个参数的取值范围都被充分探索。
    • 最大熵采样(Maximum entropy sampling):依据调参参数分布及其协方差矩阵相关假设选取样本点
    • 均匀设计(Uniform design):通过优化分配点,使其在空间中均匀分布。

36.4 偏差优化与嵌套重抽样

  • 偏差优化是指在模型调优过程中,通过调整模型复杂度、特征表示或学习算法,降低模型的系统性偏差(Bias),从而提升对真实关系的拟合能力。通过嵌套重抽样,我们可以更好地评估模型的泛化能力,从而选择最优的参数组合,减少偏差,提高模型的可靠性。
  • 嵌套重抽样是一种用于评估模型性能和选择最佳参数组合的技术。它通过在外层循环中进行重抽样来评估模型的泛化能力,而在内层循环中进行参数调优。这样可以有效地减少偏差,提高模型的可靠性。
  • 嵌套重抽样的缺点主要来自极大的计算成本(只有内部重抽样可以并行)。
  • 当训练集较小、预测变量集合非常大,或以上两者兼有时,嵌套重抽样尤其有用。它可以帮助我们更好地理解模型在不同参数组合下的表现,从而选择最优的参数设置。
  • TODO: 目前 tidymodels 提供了嵌套重采样(以及解析偏差校正)的实验版应用程序接口。待相关接口定型稳定后,我们再完善本节内容。
提示

倘若我们担心调参网格未能取得理想结果,可以采用另一种策略:扩大参数取值范围,并使用迭代方法。这类方法能够自主探索参数空间,同时依据已有实验结果指导后续空间搜索。下一章将对这类方法展开讨论。

36.5 在tidymodels框架下进行网格搜索

36.5.1 建立网格

tidymodels框架的dials包提供了多个以grid开头的网格创建函数,这些函数的主要输入参数是一个dials参数集对象,该对象可以通过model, recipe, 或 workflow 创建:

  • grid_regular():创建规则网格,适用于参数空间较小的情况。levels参数指定每个参数的候选值数量,该参数可以传入单个数值(所有调参共用该取值),也可以传入向量,分别为每个调参设定网格规模。
  • grid_random():创建不规则网格,适用于参数空间较大或对参数分布有先验知识的情况。size参数指定生成的候选参数组合数量。
  • grid_space_filling():创建空间填充设计网格,适用于需要均匀覆盖参数空间的情况。size参数指定生成的候选参数组合数量,type参数指定空间填充设计的类型(如拉丁超立方设计、最大熵采样、均匀设计等)。

我们使用Cubist模型作为示例,演示如何在tidymodels框架下进行网格搜索。Cubist模型是一种基于规则的回归模型,它结合了决策树和线性回归的优点,适用于处理复杂的非线性关系,它主要有个调参参数:

  • committees(委员会数量):模型中的委员会数量,决定了模型的复杂度和预测能力。较多的委员会数量可能提高模型的拟合能力,但也可能导致过拟合。
  • neighbors(邻居数量):事后模型校正阶段所使用的近邻数量,取值范围为 0 个近邻(即不进行校正)至 9 个近邻。
这两个参数在“Modern Rule-Based Models” 文章中有详细描述。
# 创建Cubist模型的参数集
library(rules)
cubist_spec <- cubist_rules(committees = tune(), neighbors = tune("K")) |>
  set_engine("Cubist") |>
  set_mode("regression")

cubist_params <- cubist_spec |>
  extract_parameter_set_dials()
cubist_params

# 创建包含 25 个候选模型的均匀空间填充设计
cubist_grid <- grid_space_filling(cubist_params, size = 25)
cubist_grid |>
  ggplot(aes(x = committees, y = K)) +
  geom_point(size = 3) +
  coord_fixed(ratio = 10)

以上代码中,值得注意的是:

  • 如果我们为参数设置了标签(例如 neighbors = tune("K")),该标签将用作列名。
  • 部分参数带有变换映射关系;默认情况下,先在变换尺度上生成参数值,网格输出时再转换回原始量纲。
  • size 参数代表最大样本规模;生成后会剔除冗余参数组合。举例如下:

36.5.2 使用网格调优模型

网格调参主要有三个核心函数: * tune::tune_grid(),以及 finetune 包中的两个竞速调参函数:finetune::tune_race_anova()finetune::tune_race_winloss()。 * 它们的语法几乎完全一致,并与 小节 35.10fit_resamples() 代码写法高度相近。

  • grid:一个数据框或一个整数值,指定候选参数组合的网格对象。
  • param_info:一个参数集对象,通常由 extract_parameter_set_dials() 函数生成。该参数用于指定模型的调参参数及其取值范围。仅当 grid 为整数且我们为一个或多个参数请求非标准范围/值时,才需要此对象。
# load-and-spend
set.seed(426)
concrete_split <- initial_split(concrete, prop = 3 / 4)

concrete_tr <- training(concrete_split)
concrete_te <- testing(concrete_split)
concrete_rs <- vfold_cv(concrete_tr)

# 进行网格搜索
cubist_res <- tune_grid(
  object = cubist_spec,
  preprocessor = compressive_strength ~ .,
  resamples = concrete_rs,
  grid = cubist_grid,
  control = control_grid(save_pred = TRUE, save_workflow = TRUE) # 保存预测结果和工作流程
)

cubist_res对象与fit_resamples()的输出类似,包含了每个候选参数组合在每次重抽样中的性能指标。不同的是tune_grid()返回的.metrics.predictions列中包含了更多的行。

我们童谣可以使用collect_metrics()函数来汇总每个候选参数组合的性能指标,并使用collect_predictions()函数来获取每次重抽样的预测结果。

collect_metrics(cubist_res) # 汇总性能指标
# A tibble: 50 × 8
   committees     K .metric .estimator  mean     n std_err .config         
        <int> <int> <chr>   <chr>      <dbl> <int>   <dbl> <chr>           
 1          1     4 rmse    standard   5.79     10 0.235   pre0_mod01_post0
 2          1     4 rsq     standard   0.883    10 0.0102  pre0_mod01_post0
 3          5     2 rmse    standard   5.00     10 0.267   pre0_mod02_post0
 4          5     2 rsq     standard   0.911    10 0.00954 pre0_mod02_post0
 5          9     7 rmse    standard   5.00     10 0.204   pre0_mod03_post0
 6          9     7 rsq     standard   0.911    10 0.00768 pre0_mod03_post0
 7         13     0 rmse    standard   5.44     10 0.212   pre0_mod04_post0
 8         13     0 rsq     standard   0.895    10 0.0104  pre0_mod04_post0
 9         17     3 rmse    standard   4.74     10 0.200   pre0_mod05_post0
10         17     3 rsq     standard   0.920    10 0.00705 pre0_mod05_post0
# ℹ 40 more rows
collect_predictions(cubist_res) # 获取预测结果
# A tibble: 19,300 × 7
   .pred id     compressive_strength  .row committees     K .config         
   <dbl> <chr>                 <dbl> <int>      <int> <int> <chr>           
 1 41.6  Fold01                40.8      8          1     4 pre0_mod01_post0
 2 29.8  Fold01                29.2     14          1     4 pre0_mod01_post0
 3 37.5  Fold01                37.4     27          1     4 pre0_mod01_post0
 4 25.0  Fold01                20.7     38          1     4 pre0_mod01_post0
 5 62.4  Fold01                55.2     53          1     4 pre0_mod01_post0
 6  9.84 Fold01                 9.74    71          1     4 pre0_mod01_post0
 7 41.2  Fold01                59.1     83          1     4 pre0_mod01_post0
 8 14.4  Fold01                15.0    119          1     4 pre0_mod01_post0
 9 13.2  Fold01                14.2    123          1     4 pre0_mod01_post0
10 34.9  Fold01                29.6    125          1     4 pre0_mod01_post0
# ℹ 19,290 more rows

36.5.3 其他方法

  1. 我们可以使用autoplot()函数来可视化网格搜索的结果,帮助我们更直观地理解不同参数组合的性能表现。
autoplot(cubist_res, metric = "rmse") # 可视化RMSE指标,不指定metric参数的话即查看所有指标

  1. 我们可以使用 show_best() 和各类 select_*() 函数,用于根据指定评价指标筛选最优结果:
show_best(cubist_res, metric = "rmse") # 显示评估指标为rmse的最优参数组合
# A tibble: 5 × 8
  committees     K .metric .estimator  mean     n std_err .config         
       <int> <int> <chr>   <chr>      <dbl> <int>   <dbl> <chr>           
1         95     2 rmse    standard    4.60    10   0.213 pre0_mod24_post0
2         75     3 rmse    standard    4.62    10   0.200 pre0_mod19_post0
3         54     3 rmse    standard    4.62    10   0.190 pre0_mod14_post0
4         91     4 rmse    standard    4.62    10   0.192 pre0_mod23_post0
5         29     2 rmse    standard    4.63    10   0.219 pre0_mod08_post0
show_best(cubist_res, metric = "rsq", n = 3) # 显示评估指标为rsq的前三个最优参数组合
# A tibble: 3 × 8
  committees     K .metric .estimator  mean     n std_err .config         
       <int> <int> <chr>   <chr>      <dbl> <int>   <dbl> <chr>           
1         95     2 rsq     standard   0.924    10 0.00767 pre0_mod24_post0
2         54     3 rsq     standard   0.924    10 0.00669 pre0_mod14_post0
3         75     3 rsq     standard   0.924    10 0.00697 pre0_mod19_post0
# 返回RMSE最小的参数候选值
cubist_best <- select_best(cubist_res, metric = "rmse")
cubist_best
# A tibble: 1 × 3
  committees     K .config         
       <int> <int> <chr>           
1         95     2 pre0_mod24_post0
# 以RMSE最小的参数候选值为基础,对模型结果进行筛选和操作
cubist_res |> 
  collect_predictions() |>
  nrow()
[1] 19300
# just for best
cubist_res |> 
  collect_predictions(parameters = cubist_best) |>
  nrow()
[1] 772
# augment() returns the numerically best by default:
cubist_res |> 
  augment() |> 
  nrow()
[1] 772
# also give these values for the calibration plot produced by probably:
library(probably)
cal_plot_regression(cubist_res, parameters = cubist_best)

若上述候选参数点表现最优,我们还可通过 finalize_*() 系列函数更新模型定义(或工作流):

finalize_model(cubist_spec, cubist_best)
Cubist Model Specification (regression)

Main Arguments:
  committees = 95
  neighbors = 2

Computational engine: Cubist 

如果在之前的control中使用了control_grid(save_workflow = TRUE) 控制选项,可以通过 fit_best() 函数让该模型在整个训练集上进行拟合。

fit_best(cubist_res)
══ Workflow [trained] ══════════════════════════════════════════════════════════
Preprocessor: Formula
Model: cubist_rules()

── Preprocessor ────────────────────────────────────────────────────────────────
compressive_strength ~ .

── Model ───────────────────────────────────────────────────────────────────────

Call:
cubist.default(x = x, y = y, committees = 95L)

Number of samples: 772 
Number of predictors: 8 

Number of committees: 95 
Number of rules per committee: 11, 13, 10, 10, 12, 10, 11, 12, 4, 13, 8, 16, 5, 23, 5, 14, 4, 15, 12, 13 ... 

36.5.4 竞速法

tidymodels中,这类优化方法的语法基本一致。除函数名称不同外,控制函数中有几项值得重点关注的参数设置:

  • verbose_elim:逻辑型参数,控制是否输出候选参数淘汰过程日志。
  • burn_in:整数型参数,代表最早启动参数过滤的迭代轮次。
  • num_ties:整数型参数,规定仅剩余两组候选参数时,何时触发并列结果的取舍逻辑。
  • alpha:数值参数,单侧假设检验的一类错误(假阳性)阈值。
  • randomize:布尔参数,控制重采样样本是否随机打乱顺序。

值得注意的是,用于处理竞速调参结果的各类实用函数,大多会自动过滤输出,直接舍弃中途被淘汰的候选参数组合。举个例子:如果调用 show_best()时要求展示的候选数量,多于完整跑完所有重采样流程的有效候选总数,函数会自动截断输出结果。

library(finetune)
set.seed(11)

cubist_race_res <- tune_race_anova(
  object = cubist_spec,
  preprocessor = compressive_strength ~ .,
  resamples = concrete_rs,
  grid = cubist_grid
  #control = control_race(verbose_elim = TRUE) # 输出淘汰过程日志
)

show_best(cubist_race_res, metric = "rmse", n = 10)
# A tibble: 2 × 8
  committees     K .metric .estimator  mean     n std_err .config         
       <int> <int> <chr>   <chr>      <dbl> <int>   <dbl> <chr>           
1         95     2 rmse    standard    4.60    10   0.213 pre0_mod24_post0
2         29     2 rmse    standard    4.63    10   0.219 pre0_mod08_post0
plot_race(cubist_race_res) # 可视化竞速调参过程,每一条曲线对应一组候选参数组合