Skip to content

Commit 95d21c5

Browse files
authored
Merge branch 'main' into fix/one-se-rule
2 parents 11b7925 + deabd37 commit 95d21c5

7 files changed

Lines changed: 163 additions & 13 deletions

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
* fix: `ArchiveAsyncFSelect` pushed results with the removed `rush::Rush$push_results()` method.
44
* fix: The `mlr3fselect.one_se_rule` callback errored on archives with a single evaluation or with missing scores, and wrote the `n_features` column as a list column instead of an integer column (#174).
5+
* fix: `extract_inner_fselect_results()` added the `iteration` and `fselect_instance` columns to the result of the inner `FSelectInstance` by reference, which created a circular reference between the instance and its own result (#172).
6+
* fix: `fs("rfe")` and `fs("rfecv")` failed with an internal `data.table` error when `store_benchmark_result = FALSE` was set because the importance scores were read from the benchmark result of the archive (#169).
7+
* fix: `fs("rfecv", recursive = FALSE)` failed with an internal `data.table` error because the importance scores of all resampling iterations were written to a single archive row (#168).
8+
* fix: `fs("rfecv")` ignored the direction of the measure and selected the feature set size with the worst mean performance for minimizing measures such as `msr("classif.ce")` or `msr("regr.mse")`.
9+
Feature selection results obtained with `fs("rfecv")` and a minimizing measure are invalid and should be recomputed (#167).
510

611
# mlr3fselect 1.6.0
712

R/FSelectorBatchRFE.R

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,17 @@ rank_importance = function(learners, features) {
210210
sort(set_names(pmap_dbl(ranked_importances, function(...) mean(c(...))), names(importances[[1]])), decreasing = TRUE)
211211
}
212212

213+
# Calculates the importance scores of the feature subsets evaluated in the last batch.
214+
# The benchmark result of the objective is used instead of the one of the archive because it holds the models of the
215+
# last batch even if `store_benchmark_result` is `FALSE`.
216+
batch_importances = function(inst, aggregation) {
217+
benchmark_result = get_private(inst$objective)$.benchmark_result
218+
map(benchmark_result$uhashes, function(uhash) {
219+
rr = benchmark_result$resample_result(uhash = uhash)
220+
aggregation(rr$learners, rr$task$feature_names)
221+
})
222+
}
223+
213224
# Returns the sizes of the feature subsets
214225
rfe_subsets = function(n, n_features, feature_number, subset_sizes, feature_fraction) {
215226
subsets = if (!is.null(feature_number)) {
@@ -240,11 +251,7 @@ rfe_workhorse = function(inst, subsets, recursive, aggregation = raw_importance,
240251
inst$eval_batch(states)
241252

242253
# Calculate the variable importance on the full feature set
243-
uhashes = archive$data[list(archive$n_batch), "uhash", on = "batch_nr"][[1]]
244-
importances = map(uhashes, function(uhash) {
245-
rr = archive$benchmark_result$resample_result(uhash = uhash)
246-
aggregation(rr$learners, rr$task$feature_names)
247-
})
254+
importances = batch_importances(inst, aggregation)
248255

249256
# discard models if requested by the user
250257
if (!inst$objective$store_models) {
@@ -267,17 +274,18 @@ rfe_workhorse = function(inst, subsets, recursive, aggregation = raw_importance,
267274

268275
if (recursive) {
269276
# recalculate the variable importance on the reduced feature subset
270-
uhashes = archive$data[list(archive$n_batch), "uhash", on = "batch_nr"][[1]]
271-
importances = map(uhashes, function(uhash) {
272-
rr = archive$benchmark_result$resample_result(uhash = uhash)
273-
aggregation(rr$learners, rr$task$feature_names)
274-
})
277+
importances = batch_importances(inst, aggregation)
275278

276279
# log importance to archive
277280
archive$data[list(archive$n_batch), "importance" := importances, on = "batch_nr"]
278281
} else {
279282
# log importance to archive
280-
set(archive$data, archive$n_evals, "importance", map(importances, function(x) x[seq(j)]))
283+
# the batch holds one row per fold, so the truncated importance of each fold is assigned to the whole batch
284+
archive$data[
285+
list(archive$n_batch),
286+
"importance" := map(importances, function(x) x[seq(j)]),
287+
on = "batch_nr"
288+
]
281289
}
282290
}
283291
if (folds > 1) {

R/FSelectorBatchRFECV.R

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ FSelectorBatchRFECV = R6Class(
165165

166166
# average performance of feature numbers
167167
aggr = archive$data[, list("y" = mean(unlist(.SD))), by = "batch_nr", .SDcols = archive$cols_y]
168-
best_batch = aggr[order(get("y"), decreasing = TRUE), head(.SD, 1)]$batch_nr
168+
# the direction of the codomain determines whether the performance is minimized or maximized
169+
best_batch = aggr[which_max(aggr[["y"]] * -archive$codomain$direction, ties_method = "first")]$batch_nr
169170
n_features = rowSums(archive$data[list(best_batch), , on = "batch_nr"][1, archive$cols_x, with = FALSE])
170171

171172
# use full data set

R/extract_inner_fselect_results.R

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ extract_inner_fselect_results.ResampleResult = function(x, fselect_instance = FA
6363
return(data.table())
6464
}
6565
tab = imap_dtr(rr$learners, function(learner, i) {
66-
data = setalloccol(learner$fselect_result)
66+
# copy the result, otherwise the columns below are added to the result of the instance
67+
data = copy(learner$fselect_result)
6768
set(data, j = "iteration", value = i)
6869
if (fselect_instance) {
6970
set(data, j = "fselect_instance", value = list(learner$fselect_instance))

tests/testthat/test_FSelectorRFE.R

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,23 @@ test_that("optimal features are selected with mean", {
319319

320320
expect_equal(instance$result$features[[1]], c("x2", "x3", "x4"))
321321
})
322+
323+
test_that("rfe works without storing the benchmark result", {
324+
instance = fsi(
325+
task = TEST_MAKE_TSK(),
326+
learner = lrn("regr.rpart"),
327+
resampling = rsmp("cv", folds = 3),
328+
measures = msr("dummy"),
329+
terminator = trm("none"),
330+
store_benchmark_result = FALSE
331+
)
332+
333+
optimizer = fs("rfe", n_features = 1, feature_number = 1)
334+
optimizer$optimize(instance)
335+
data = instance$archive$data
336+
337+
expect_names(names(data), disjunct.from = "uhash")
338+
expect_feature_number(data[batch_nr == 1, 1:4], n = 4)
339+
expect_feature_number(data[batch_nr == 4, 1:4], n = 1)
340+
pwalk(data, function(x1, x2, x3, x4, importance, ...) expect_equal(x1 + x2 + x3 + x4, length(importance)))
341+
})

tests/testthat/test_FSelectorRFECV.R

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,31 @@ test_that("default parameters work", {
6868
})
6969
})
7070

71+
test_that("recursive parameter works", {
72+
instance = fsi(
73+
task = TEST_MAKE_TSK(),
74+
learner = lrn("regr.rpart"),
75+
resampling = rsmp("cv", folds = 3),
76+
measures = msr("dummy"),
77+
terminator = trm("none"),
78+
store_models = TRUE
79+
)
80+
81+
optimizer = fs("rfecv", recursive = FALSE, n_features = 1, feature_number = 1)
82+
optimizer$optimize(instance)
83+
data = instance$archive$data
84+
85+
# the importance of the first batch is reused and truncated in the following batches
86+
walk(seq(3), function(i) {
87+
importances = data[list(i), importance, on = "iteration"]
88+
walk(seq(2, length(importances)), function(j) {
89+
expect_equal(importances[[j]], importances[[1]][seq(length(importances) + 1 - j)])
90+
})
91+
})
92+
93+
pwalk(data, function(x1, x2, x3, x4, importance, ...) expect_equal(x1 + x2 + x3 + x4, length(importance)))
94+
})
95+
7196
test_that("learner without importance method throw an error", {
7297
learner = lrn("classif.rpart")
7398
learner$properties = setdiff(learner$properties, "importance")
@@ -85,6 +110,53 @@ test_that("learner without importance method throw an error", {
85110
)
86111
})
87112

113+
test_that("optimal features are selected with a minimizing measure", {
114+
LearnerRegrDebugImportance = R6Class(
115+
"LearnerRegrDebugImportance",
116+
inherit = LearnerRegrDebug,
117+
public = list(
118+
importance = function() {
119+
c(x2 = 1.4, x1 = 0.8, x3 = 1.2, x4 = 1.1)
120+
}
121+
)
122+
)
123+
124+
learner = LearnerRegrDebugImportance$new()
125+
learner$properties = c(learner$properties, "importance")
126+
127+
# the three features x2, x3 and x4 have the lowest score
128+
score_design = data.table(
129+
score = c(2, 1, 4, 3),
130+
features = list(
131+
c("x1", "x2", "x3", "x4"),
132+
c("x2", "x3", "x4"),
133+
c("x2", "x3"),
134+
"x2"
135+
)
136+
)
137+
138+
measure = msr("dummy", score_design = score_design, minimize = TRUE)
139+
140+
instance = fsi(
141+
task = TEST_MAKE_TSK(),
142+
learner = learner,
143+
resampling = rsmp("cv", folds = 3),
144+
measures = measure,
145+
terminator = trm("none"),
146+
store_models = TRUE
147+
)
148+
149+
optimizer = fs("rfecv", n_features = 1, feature_number = 1)
150+
optimizer$optimize(instance)
151+
data = as.data.table(instance$archive)
152+
153+
# number of features in the final run
154+
expect_feature_number(data[13, 1:4], n = 4)
155+
expect_feature_number(data[14, 1:4], n = 3)
156+
157+
expect_equal(instance$result$features[[1]], c("x2", "x3", "x4"))
158+
})
159+
88160
test_that("optimal features are selected", {
89161
LearnerRegrDebugImportance = R6Class(
90162
"LearnerRegrDebugImportance",
@@ -150,3 +222,22 @@ test_that("optimal features are selected", {
150222

151223
expect_equal(instance$result$features[[1]], c("x2", "x3", "x4"))
152224
})
225+
226+
test_that("rfecv works without storing the benchmark result", {
227+
instance = fsi(
228+
task = TEST_MAKE_TSK(),
229+
learner = lrn("regr.rpart"),
230+
resampling = rsmp("cv", folds = 3),
231+
measures = msr("dummy"),
232+
terminator = trm("none"),
233+
store_benchmark_result = FALSE
234+
)
235+
236+
optimizer = fs("rfecv", n_features = 1, feature_number = 1)
237+
optimizer$optimize(instance)
238+
data = instance$archive$data
239+
240+
expect_names(names(data), disjunct.from = "uhash")
241+
expect_names(names(data), must.include = c("importance", "iteration"))
242+
pwalk(data, function(x1, x2, x3, x4, importance, ...) expect_equal(x1 + x2 + x3 + x4, length(importance)))
243+
})

tests/testthat/test_extract_inner_fselect_result.R

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,3 +377,27 @@ test_that("extract_inner_fselect_results function works with benchmark and retur
377377
)
378378
expect_equal(unique(ibmr$experiment), c(1, 2))
379379
})
380+
381+
test_that("extract_inner_fselect_results does not modify the fselect result", {
382+
rr = fselect_nested(
383+
fs("random_search"),
384+
tsk("iris"),
385+
lrn("classif.rpart"),
386+
rsmp("holdout"),
387+
rsmp("cv", folds = 2),
388+
msr("classif.ce"),
389+
term_evals = 4
390+
)
391+
392+
columns = names(rr$learners[[1]]$fselect_result)
393+
394+
extract_inner_fselect_results(rr)
395+
expect_named(rr$learners[[1]]$fselect_result, columns)
396+
397+
extract_inner_fselect_results(rr, fselect_instance = TRUE)
398+
expect_named(rr$learners[[1]]$fselect_result, columns)
399+
400+
# the instance is not added again when it is not requested
401+
irr = extract_inner_fselect_results(rr)
402+
expect_names(names(irr), disjunct.from = "fselect_instance")
403+
})

0 commit comments

Comments
 (0)