library(tidymodels)
tidymodels_prefer()
theme_set(theme_bw())36 网格搜索
网格搜索是一种用于优化模型流程调参参数的方法。该方法生成一组预先定义的候选参数取值,并逐一计算每组参数对应的模型性能。之后既可以选取数值表现最优的候选参数,也可以考察调参参数与模型性能之间的变化关系,判断模型是否有必要开展进一步优化。
本章我们以boosted tree模型为例,演示如何使用网格搜索优化调整其中的两个超参数。
提升树是依次生成的多个独立决策树构成的集合。
- 与集成构建过程相关的一个参数是学习率:下一棵树利用上一棵树的信息进行优化。学习率定义模型学习的幅度与速度。学习率取值必须大于 0,通常区间为 \(10^{-5}\) 至 \(10^{-1}\)。由于该参数取值范围跨度很大,最适合采用对数尺度进行分析。
- 与数据集特性相关的一个参数是树的数量:树的数量越多,模型越复杂,但也越容易过拟合。因此,需要在模型复杂度和泛化能力之间找到平衡点。
- 部分数据集需要大量树并且缓慢迭代(即较小学习率),才能找到最优性能指标;而另一些数据集只需较少数量的树、快速迭代,即可获得最佳预测效果。
36.1 规则网格
- 规则网格是指在每个参数的取值范围内,均匀地划分出若干个候选参数值。
- 规则网格的优点是简单易用,缺点是可能会错过最优参数组合,尤其是在参数空间较大时。此外,随着参数数量的增加,计算量会呈指数级增长。
- 对于某些模型和预处理方法而言,即便需要调优的超参数数量较多,也可以通过规则网格搜索找到较优的参数组合。小节 36.3。
36.2 不规则网格
在超参数调优中,不规则网格搜索指的是候选参数值的排列不是均匀、等距的,而是由用户自定义的一组组合(可能随意分布或侧重某些区域)。它和规则网格(grid_regular)相对,规则网格会在每个参数轴的等分点上生成所有两两组合。
- 参数尺度差异大:比如 mtry(整数)和 penalty(对数尺度),用对数间隔比等距更合理。
- 先验知识驱动:你可能知道某些参数范围效果好,想更有针对性地测试。
- 减少浪费:避免在毫无希望的区域(如极大学习率)浪费计算。
-
空间填充设计(space-filling design):在有限预算下更均匀覆盖整个参数空间,目标是确保调参空间被覆盖,同时候选值之间的冗余最小化,胜过规则网格的“轴对齐”效应。空间填充设计有很多类型:
- 拉丁超立方设计(Latin hypercube design, LHD):是空间填充设计最常用的方法。在每个参数轴上均匀划分区间,并随机选择每个区间的一个点,保证每个参数的取值范围都被充分探索。
- 最大熵采样(Maximum entropy sampling):依据调参参数分布及其协方差矩阵相关假设选取样本点
- 均匀设计(Uniform design):通过优化分配点,使其在空间中均匀分布。
36.3 传统网格搜索的高效计算
网格搜索的计算成本可能会变得很大,具体取决于重采样策略和所考虑的候选参数数量。常用的提升网格搜索效率的方法有三种,分别是:
- 子模型:在网格搜索过程中,可以先使用一部分数据训练一个简单的模型,然后用这个模型来指导后续的参数调优。
- 并行处理:利用多核处理器或分布式计算资源,同时对多个参数组合进行评估。
- 竞赛法:通过逐步淘汰表现较差的参数组合,减少不必要的计算。
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 个近邻。
# 创建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.10 中fit_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 其他方法
- 我们可以使用
autoplot()函数来可视化网格搜索的结果,帮助我们更直观地理解不同参数组合的性能表现。
autoplot(cubist_res, metric = "rmse") # 可视化RMSE指标,不指定metric参数的话即查看所有指标
- 我们可以使用
show_best()和各类select_*()函数,用于根据指定评价指标筛选最优结果:
-
show_best():显示最优的参数组合; -
select_best():挑选单一最优参数组合; -
select_by_one_std_err():基于一倍标准差准则筛选最优参数; -
select_by_pct_loss(): 用于筛选结构最简单的模型,且该模型的性能衰减幅度不超过设定的可接受阈值。
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
[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) # 可视化竞速调参过程,每一条曲线对应一组候选参数组合