解读

14 Iterative Search | Tidy Modeling with R

1️ 数据与模型准备

我们先搭建一个基本的 SVM 工作流:

library(tidymodels)
tidymodels_prefer()
library(finetune)   # 模拟退火需要用到
 
# 数据
data(cells)
cells <- cells %>% select(-case)
 
set.seed(1304)
cell_folds <- vfold_cv(cells)
 
roc_res <- metric_set(roc_auc)
 
# 数据预处理
svm_rec <- 
  recipe(class ~ ., data = cells) %>%
  step_YeoJohnson(all_numeric_predictors()) %>%
  step_normalize(all_numeric_predictors())
 
# 模型:径向基核 SVM
svm_spec <- 
  svm_rbf(cost = tune(), rbf_sigma = tune()) %>% 
  set_engine("kernlab") %>% 
  set_mode("classification")
 
svm_wflow <- 
  workflow() %>% 
  add_model(svm_spec) %>% 
  add_recipe(svm_rec)

两个需要调的超参数:

  • cost:控制“分界线”的复杂度。越大,越不允许分错,模型越复杂。
  • rbf_sigma:决定高斯核的“宽度”。越大,每个数据点影响的范围越广。

2️ 网格搜索:像在棋盘上踩点

首先用最传统的 网格搜索。它的思想很简单:在参数空间里,像棋盘一样布点,然后把每个点都试一遍。

svm_param <- 
  svm_wflow %>% 
  extract_parameter_set_dials() %>% 
  update(rbf_sigma = rbf_sigma(c(-7, -1))) # 定义参数的大致范围
 
set.seed(1401)
start_grid <- 
  svm_param %>% 
  update(
    cost = cost(c(-6, 1)),
    rbf_sigma = rbf_sigma(c(-6, -4))
  ) %>% 
  grid_regular(levels = 2) # 生成一个非常小的、均匀的网格
 
set.seed(1402)
svm_initial <- 
  svm_wflow %>% 
  tune_grid(resamples = cell_folds, grid = start_grid, metrics = roc_res)
 
collect_metrics(svm_initial)
  • 优点:简单粗暴,绝对不会漏掉网格上的任何点。
  • 缺点:非常“笨”且“浪费”。它不会利用前一次的结果去指导下一次的选择,就像闭着眼睛把地图上的格子一个个踩一遍。如果参数多、范围大,计算量会爆炸式增长,而且很可能最好的参数组合根本不在你设定的网格点上。

3️ 贝叶斯优化

既然 svm_initial 已经搜索过一些点,为什么不能更聪明地利用这些信息呢? 于是就有了贝叶斯优化。
想象你在一个巨大的美食城里,目标是找到最好吃的餐馆。

  • 网格搜索的做法是:按地图坐标,每隔100米选一家,不管口碑好坏,全都吃一遍。费时费钱。
  • 贝叶斯优化的做法是:
    1. 先验:你随机吃了几家(这就是 svm_initial 的作用),对美食城的口味有了初步印象。
    2. 建模型:你根据吃过的餐馆(位置、口味),大脑里画出了一张 “预测美食地图” 。这张图会预测哪里可能好吃(利用),哪里因为没去过而充满不确定性(探索)。
    3. 决策:你决定下一家店要么去“预测很好吃”的地方(抓住当前最佳),要么去一个“完全没去过”的区域(说不定有惊喜)。这个权衡由一个叫 “采集函数” 的规则来决定。
    4. 更新:吃完后,你的体验会更新你的“预测美食地图”,让它更准。如此循环,用最少的次数,快速找到最美味的餐馆。

核心就是在 利用已知好结果 和 探索未知区域 之间找到智能的平衡。

ctrl <- control_bayes(verbose = TRUE) # 设置控制参数,verbose=TRUE看过程
 
set.seed(1403)
svm_bo <-
  svm_wflow %>%
  tune_bayes(
    resamples = cell_folds,
    metrics = roc_res,
    initial = svm_initial,   # 基于网格搜索结果
    param_info = svm_param,  # 告诉算法参数的完整范围
    iter = 25,               # 在初始点之后,再尝试25组参数
    control = ctrl
  )
 
show_best(svm_bo) # 查看最佳结果
autoplot(svm_bo, type = "performance") # 查看性能随迭代提升的过程
autoplot(svm_bo, type = "parameters") # 查看参数是如何被探索的

4️ 模拟退火

贝叶斯优化很聪明,但它需要维护一个概率模型,计算量有时也大。模拟退火 是另一种思路,它模仿金属冶炼中退火的物理过程。

  • 你的策略:
    1. 高温阶段(大胆探索):一开始,你“精力旺盛”(高温),可以大踏步地随机走向任何方向。即使新地点的金子比当前位置的少一点,你也有一定概率接受它跳过去。这是为了能跳出当前的小山丘,避免困在局部最优(一个铜矿很多但金矿少的小山包),从而有机会到达全局最优(真正的金山)。
    2. 降温阶段(精细收敛):随着时间推移(迭代),你慢慢“累了冷静了”(温度下降)。你的步伐越来越小,并且越来越不愿意接受更差的结果。最终,你会在认为最好的那个点(金山顶)附近小范围仔细勘探,直到确认找到最好的位置。

它的核心是:先大胆跳跃逃离局部最优,再逐渐冷静精细收敛。

ctrl_sa <- control_sim_anneal(verbose = TRUE, no_improve = 10L) 
# no_improve = 10L 表示连续10次没有更好结果就停止
 
set.seed(1404)
svm_sa <-
  svm_wflow %>%
  tune_sim_anneal(
    resamples = cell_folds,
    metrics = roc_res,
    initial = svm_initial, # 同样需要一个起点
    param_info = svm_param,
    iter = 50,             # 总迭代次数
    control = ctrl_sa
  )
 
show_best(svm_sa)
autoplot(svm_sa, type = "performance") # 你会看到曲线有时会下降,这正是其特点!