tidymodels 不遵守固定的 set_engine 参数

tidymodels does not respect fixed set_engine parameters

(根据 Julia 的回复在最后更新TL;DR:这似乎是底层 kknn 包,而不是 tidymodels)

我正在用 tidymodels 做一些 k 最近邻回归模型。这是通过 nearest_neighbor() 函数。我想看看使用和不使用特征标准化的结果之间有什么区别。

现在 set_engine("kknn") 在后台使用 kknn::train.kknn() 函数,它有一个规范化参数 scale = TRUE。我想比较 scale = FALSEscale = TRUE 的模型(实际上,我想在食谱中这样做,但这是不可能的,我将在下面解释)。

但我似乎无法通过 tidymodels 可靠地设置 scale = FALSE。下面是一个显示我所见的代表。

问题 这么长:我做错了什么还是这是一个错误?如果它是一个错误,它是已知的吗?我可以在某个地方读到它吗?如果有人能阐明这一点,我将不胜感激。

为 reprex 设置

这里我将使用mtcars:

library(tidymodels)
data("mtcars")

训练-测试拆分为:

set.seed(1)
mtcars_split <- initial_split(mtcars, prop = 0.7)

这是我将使用的常用食谱:

mtcars_recipe <- recipe(mpg ~ disp + wt, data = mtcars)

这是模型 1(称为 knn_FALSE),其中 scale = FALSE:

knn_FALSE <- nearest_neighbor(neighbors = 5) %>% 
  set_mode("regression") %>% 
  set_engine("kknn", scale = FALSE)

这是模型 2(称为 knn_TRUE),其中 scale = TRUE:

knn_TRUE <- nearest_neighbor(neighbors = 5) %>% 
  set_mode("regression") %>% 
  set_engine("kknn", scale = TRUE)

我将这两个模型捆绑到两个工作流程中:

## Workflow with scale = FALSE
wf_FALSE <- workflow() %>% 
  add_model(knn_FALSE) %>% 
  add_recipe(mtcars_recipe)

## Worflow with scale = TRUE
wf_TRUE <- workflow() %>% 
  add_model(knn_TRUE) %>% 
  add_recipe(mtcars_recipe)

使用fit(),可以得到scale = FALSE

当在工作流程中使用 fit() 时,似乎可以有一个 scale = TRUE 版本和一个 scale = FALSE 版本。

例如,对于 scale = TRUE 我得到:

wf_TRUE %>% fit(mtcars)
== Workflow [trained] ===============================================================================================
Preprocessor: Recipe
Model: nearest_neighbor()

-- Preprocessor -----------------------------------------------------------------------------------------------------
0 Recipe Steps

-- Model ------------------------------------------------------------------------------------------------------------

Call:
kknn::train.kknn(formula = ..y ~ ., data = data, ks = ~5, scale = ~TRUE)

Type of response variable: continuous
minimal mean absolute error: 2.09425
Minimal mean squared error: 7.219114
Best kernel: optimal
Best k: 5

scale = FALSE 我有:

wf_FALSE %>% fit(mtcars)
== Workflow [trained] ===============================================================================================
Preprocessor: Recipe
Model: nearest_neighbor()

-- Preprocessor -----------------------------------------------------------------------------------------------------
0 Recipe Steps

-- Model ------------------------------------------------------------------------------------------------------------

Call:
kknn::train.kknn(formula = ..y ~ ., data = data, ks = ~5, scale = ~FALSE)

Type of response variable: continuous
minimal mean absolute error: 2.1665
Minimal mean squared error: 6.538769
Best kernel: optimal
Best k: 5

结果明显不同,这来自于scale参数的不同。

但是情节变厚了。

last_fit()没有区别

然而,当使用 last_fit() 时,scale = TRUEscale = FALSE 的结果是相同的。

对于scale = TRUE

wf_TRUE %>% last_fit(mtcars_split) %>% collect_metrics()
# A tibble: 2 x 3
  .metric .estimator .estimate
  <chr>   <chr>          <dbl>
1 rmse    standard       3.16 
2 rsq     standard       0.663

而对于 scale = FALSE

wf_FALSE %>% last_fit(mtcars_split) %>% collect_metrics()
# A tibble: 2 x 3
  .metric .estimator .estimate
  <chr>   <chr>          <dbl>
1 rmse    standard       3.16 
2 rsq     standard       0.663

这些显然——而且出乎意料地——是一样的。

使用tune_grid()

调整也没有区别

如果我用 tune_grid()validation_split() 进行调整,scale = TRUEscale = FALSE 的结果也没有区别。

这是代码:

## Tune grid
knn_grid <- tibble(neighbors = c(5, 15))

## Tune Model 1: kNN regresson with no scaling in train.kknn
knn_FALSE_tune <- nearest_neighbor(neighbors = tune()) %>% 
  set_mode("regression") %>% 
  set_engine("kknn", scale = FALSE)

## Model 2: kNN regresson with  scaling in train.kknn
knn_TRUE_tune <- nearest_neighbor(neighbors = tune()) %>% 
  set_mode("regression") %>% 
  set_engine("kknn", scale = TRUE)

## Workflow with scale = FALSE
wf_FALSE_tune <- workflow() %>% 
  add_model(knn_FALSE_tune) %>% 
  add_recipe(mtcars_recipe)

## Worflow with scale = TRUE
wf_TRUE_tune <- workflow() %>% 
  add_model(knn_TRUE_tune) %>% 
  add_recipe(mtcars_recipe)

## Validation split
mtcars_val <- validation_split(mtcars)

## Tune results: Without scaling
wf_FALSE_tune %>% 
  tune_grid(resamples = mtcars_val, 
            grid = knn_grid) %>% 
  collect_metrics()

## Tune results: With scaling
wf_TRUE_tune %>% 
  tune_grid(resamples = mtcars_val, 
            grid = knn_grid) %>% 
  collect_metrics()

scale = FALSE时的结果:

> wf_FALSE_tune %>% 
+   tune_grid(resamples = mtcars_val, 
+             grid = knn_grid) %>% 
+   collect_metrics()
# A tibble: 4 x 7
  neighbors .metric .estimator  mean     n std_err .config
      <dbl> <chr>   <chr>      <dbl> <int>   <dbl> <chr>  
1         5 rmse    standard   1.64      1      NA Model1 
2         5 rsq     standard   0.920     1      NA Model1 
3        15 rmse    standard   2.55      1      NA Model2 
4        15 rsq     standard   0.956     1      NA Model2 

scale = TRUE时的结果:

> wf_TRUE_tune %>% 
+   tune_grid(resamples = mtcars_val, 
+             grid = knn_grid) %>% 
+   collect_metrics()
# A tibble: 4 x 7
  neighbors .metric .estimator  mean     n std_err .config
      <dbl> <chr>   <chr>      <dbl> <int>   <dbl> <chr>  
1         5 rmse    standard   1.64      1      NA Model1 
2         5 rsq     standard   0.920     1      NA Model1 
3        15 rmse    standard   2.55      1      NA Model2 
4        15 rsq     standard   0.956     1      NA Model2 

问题

我是不是误解了(或者遗漏了我自己的错误),或者 last_fit()tune_grid() 函数不尊重我对 scale 的选择?

我是 tidymodels 的新手,所以我可能错过了一些东西。非常感谢回答。

我希望在配方中使用 step_normalize() 来自己进行标准化,但由于我无法在底层引擎中可靠地设置 scale = FALSE,因此我无法对此进行试验。

Julia 回复后更新

正如 Julia 所示,来自 train.kknn() 的预测为 scale = FALSEscale = TRUE 提供了相同的预测。所以这不是 tidymodels 问题。相反,kknn:::predict.train.kknn() 函数在预测时不考虑传递给 train.kknn() 的所有参数。

考虑以下使用 kknn() 而不是 train.kknn() 的输出:

kknn::kknn(formula = mpg ~ disp + wt, train = training(mtcars_split), 
           test = testing(mtcars_split), k = 5, scale = FALSE) %>% 
  predict(newdata = testing(mtcars_split))
## [1] 21.276 21.276 16.860 16.276 21.276 16.404 29.680 15.700 16.020
kknn::kknn(formula = mpg ~ disp + wt, train = training(mtcars_split), 
           test = testing(mtcars_split), k = 5, scale = TRUE) %>% 
  predict(newdata = testing(mtcars_split))
## [1] 21.032 21.784 16.668 16.052 21.264 16.404 26.340 16.076 15.620

这些是不同的,这是应该的。问题是 kknn:::predict.train.kknn() 调用 kknn(),但没有传递 scale(和其他一些可选参数):

function (object, newdata, ...) 
{
    if (missing(newdata)) 
        return(predict(object, ...))
    res <- kknn(formula(terms(object)), object$data, newdata, 
        k = object$best.parameters$k, kernel = object$best.parameters$kernel, 
        distance = object$distance)
    return(predict(res, ...))
}
<bytecode: 0x55e2304fba10>
<environment: namespace:kknn>

我认为您没有错误或问题,只是误解了 last_fit() 和朋友预测性能的依据。

library(tidymodels)
set.seed(1)
mtcars_split <- initial_split(mtcars, prop = 0.7)

knn_FALSE <- nearest_neighbor(neighbors = 5) %>% 
  set_mode("regression") %>% 
  set_engine("kknn", scale = FALSE)

knn_FALSE %>% translate()
#> K-Nearest Neighbor Model Specification (regression)
#> 
#> Main Arguments:
#>   neighbors = 5
#> 
#> Engine-Specific Arguments:
#>   scale = FALSE
#> 
#> Computational engine: kknn 
#> 
#> Model fit template:
#> kknn::train.kknn(formula = missing_arg(), data = missing_arg(), 
#>     ks = min_rows(5, data, 5), scale = FALSE)

knn_TRUE <- nearest_neighbor(neighbors = 5) %>% 
  set_mode("regression") %>% 
  set_engine("kknn", scale = TRUE)

knn_TRUE %>% translate()
#> K-Nearest Neighbor Model Specification (regression)
#> 
#> Main Arguments:
#>   neighbors = 5
#> 
#> Engine-Specific Arguments:
#>   scale = TRUE
#> 
#> Computational engine: kknn 
#> 
#> Model fit template:
#> kknn::train.kknn(formula = missing_arg(), data = missing_arg(), 
#>     ks = min_rows(5, data, 5), scale = TRUE)

请注意,两个欧洲防风草模型都正确地将 scale 参数传递给底层引擎。

我们现在可以将这两个防风草模型添加到 workflow(),使用公式预处理器(配方也可以)。

wf_FALSE <- workflow() %>% 
  add_model(knn_FALSE) %>% 
  add_formula(mpg ~ disp + wt)

## Worflow with scale = TRUE
wf_TRUE <- workflow() %>% 
  add_model(knn_TRUE) %>% 
  add_formula(mpg ~ disp + wt)

函数last_fit() 在训练数据上拟合,在测试数据上预测。我们可以通过我们的工作流程手动完成。重要的是,请注意,对于测试集中的这些示例,预测 相同 ,因此您将获得相同的指标。

wf_TRUE %>% fit(training(mtcars_split)) %>% predict(testing(mtcars_split))
#> # A tibble: 9 x 1
#>   .pred
#>   <dbl>
#> 1  21.0
#> 2  21.8
#> 3  16.7
#> 4  16.1
#> 5  21.3
#> 6  16.4
#> 7  26.3
#> 8  16.1
#> 9  15.6
wf_FALSE %>% fit(training(mtcars_split)) %>% predict(testing(mtcars_split))
#> # A tibble: 9 x 1
#>   .pred
#>   <dbl>
#> 1  21.0
#> 2  21.8
#> 3  16.7
#> 4  16.1
#> 5  21.3
#> 6  16.4
#> 7  26.3
#> 8  16.1
#> 9  15.6

直接拟合模型也是如此:

knn_TRUE %>% 
  fit(mpg ~ disp + wt, data = training(mtcars_split)) %>% 
  predict(testing(mtcars_split))
#> # A tibble: 9 x 1
#>   .pred
#>   <dbl>
#> 1  21.0
#> 2  21.8
#> 3  16.7
#> 4  16.1
#> 5  21.3
#> 6  16.4
#> 7  26.3
#> 8  16.1
#> 9  15.6
knn_FALSE %>% 
  fit(mpg ~ disp + wt, data = training(mtcars_split)) %>% 
  predict(testing(mtcars_split))
#> # A tibble: 9 x 1
#>   .pred
#>   <dbl>
#> 1  21.0
#> 2  21.8
#> 3  16.7
#> 4  16.1
#> 5  21.3
#> 6  16.4
#> 7  26.3
#> 8  16.1
#> 9  15.6

事实上,如果我们直接拟合底层 kknn 模型,则为真:

kknn::train.kknn(formula = mpg ~ disp + wt, data = training(mtcars_split), 
                 ks = 5, scale = FALSE) %>% 
  predict(testing(mtcars_split))
#> [1] 21.032 21.784 16.668 16.052 21.264 16.404 26.340 16.076 15.620
kknn::train.kknn(formula = mpg ~ disp + wt, data = training(mtcars_split), 
                 ks = 5, scale = TRUE) %>% 
  predict(testing(mtcars_split))
#> [1] 21.032 21.784 16.668 16.052 21.264 16.404 26.340 16.076 15.620

reprex package (v0.3.0.9001)

创建于 2020-11-12

scale参数被正确传递给底层引擎;它只是不会改变这些测试用例的预测。