From 9ffcbc01247cea2667360afc72177971798c2d05 Mon Sep 17 00:00:00 2001 From: AnnaNzrv Date: Wed, 3 Sep 2025 13:59:39 +0200 Subject: [PATCH 1/7] zwischenstand --- R/LearnerPythonClassif.R | 199 +++++++++++++++++++++++++++++++++++++++ R/python_proc.R | 94 ++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 R/LearnerPythonClassif.R create mode 100644 R/python_proc.R diff --git a/R/LearnerPythonClassif.R b/R/LearnerPythonClassif.R new file mode 100644 index 000000000..abc4b6a66 --- /dev/null +++ b/R/LearnerPythonClassif.R @@ -0,0 +1,199 @@ +LearnerPythonClassif = R6::R6Class( + "LearnerPythonClassif", + inherit = mlr3::LearnerClassif, + + public = list( + initialize = function(id, + feature_types = c("logical","integer","numeric","factor","ordered"), + predict_types = c("response"), + param_set = paradox::ParamSet$new(), + # Python requirements + packages, # character(), e.g. c("numpy","pandas","tabpfn") + python_version, # e.g. "3.10" + method = c("auto","virtualenv","conda"), + envname = NULL, + storage = c("file","bytes"), + man = NA_character_) { + + method = match.arg(method) + storage = match.arg(storage) + + base_ps = paradox::ParamSet$new(list( + paradox::ParamUty$new("py_env", default = envname, tags = "python"), + paradox::ParamFct$new("py_method", levels = c("auto","virtualenv","conda"), + default = method, tags = "python"), + paradox::ParamLgl$new("py_preflight", default = TRUE, tags = "python") + )) + param_set = paradox::combine_ps(param_set, base_ps) + + super$initialize( + id = id, feature_types = feature_types, predict_types = predict_types, + param_set = param_set, man = man + ) + + private$.py_packages = packages + private$.py_version = python_version + private$.py_method = method + private$.py_env = envname %||% .mlr_envname(id, python_version) + private$.storage = storage + + # Register only (no downloads here) + register_python_learner( + id = id, packages = packages, python_version = python_version, + method = method, envname = private$.py_env + ) + + # R-side state only; no reticulate objects here + self$state = list( + storage = storage, + model_path = NULL, # if storage == "file" + model_raw = NULL, # if storage == "bytes" + meta = NULL # e.g., class levels + ) + }, + + prepare_env = function() { + cfg = private$.cfg() + .ensure_python_env(cfg$envname, cfg$packages, cfg$python_version, cfg$method) + invisible(TRUE) + }, + + marshal = function(dir) { + dir.create(dir, recursive = TRUE, showWarnings = FALSE) + meta = private$.marshal_meta() + jsonlite::write_json(meta, file.path(dir, "python_env.json"), auto_unbox = TRUE, pretty = TRUE) + jsonlite::write_json(self$state$meta %||% list(), file.path(dir, "model_meta.json"), + auto_unbox = TRUE, pretty = TRUE) + + if (self$state$storage == "file") { + stopifnot(!is.null(self$state$model_path) && file.exists(self$state$model_path)) + file.copy(self$state$model_path, file.path(dir, "model.pkl"), overwrite = TRUE) + } else { + stopifnot(is.raw(self$state$model_raw)) + saveRDS(self$state$model_raw, file.path(dir, "model_bytes.rds")) + } + invisible(dir) + }, + + unmarshal = function(dir) { + meta = jsonlite::read_json(file.path(dir, "python_env.json")) + .ensure_python_env(meta$envname, meta$packages, meta$python_version, meta$method) + + self$state$meta = tryCatch(jsonlite::read_json(file.path(dir, "model_meta.json")), error = function(e) NULL) + + if (file.exists(file.path(dir, "model.pkl"))) { + # file mode + tmp_dir = file.path(tempdir(), paste0("mlr3x-", self$id, "-", as.integer(runif(1,1,1e9)))) + dir.create(tmp_dir, recursive = TRUE) + file.copy(file.path(dir, "model.pkl"), file.path(tmp_dir, "model.pkl"), overwrite = TRUE) + self$state$storage = "file" + self$state$model_path = file.path(tmp_dir, "model.pkl") + self$state$model_raw = NULL + } else { + # bytes mode + raw = readRDS(file.path(dir, "model_bytes.rds")) + self$state$storage = "bytes" + self$state$model_raw = raw + self$state$model_path = NULL + } + invisible(TRUE) + } + ), + + private = list( + .py_packages = NULL, .py_version = NULL, .py_method = NULL, .py_env = NULL, + .storage = NULL, + + .cfg = function() list( + packages = private$.py_packages, + python_version = private$.py_version, + method = self$param_set$values$py_method %||% private$.py_method, + envname = self$param_set$values$py_env %||% private$.py_env + ), + + .marshal_meta = function() { + cfg = private$.cfg() + list(id = self$id, packages = unname(cfg$packages), python_version = cfg$python_version, + method = cfg$method, envname = cfg$envname, timestamp = as.character(Sys.time())) + }, + + # --- lifecycle hooks ------------------------------------------------------ + + .train = function(task) { + if (isTRUE(self$param_set$values$py_preflight)) self$prepare_env() + + x = task$data(cols = task$feature_names) + y = task$truth() + cfg = private$.cfg() + + if (self$state$storage == "file") { + # The worker will write a pickle file here + model_dir = file.path(tempdir(), paste0("mlr3x-", self$id, "-", as.integer(runif(1,1,1e9)))) + dir.create(model_dir, recursive = TRUE, showWarnings = FALSE) + model_path = file.path(model_dir, "model.pkl") + + meta = .python_callr(cfg$envname, cfg$method, + fn = private$.worker_fit_file, # subclass must provide + args = list(x = x, y = y, model_path = model_path, + learner_id = self$id, predict_types = self$predict_types) + ) + + self$state$model_path = model_path + self$state$model_raw = NULL + self$state$meta = meta + + } else { # "bytes" + ret = .python_callr(cfg$envname, cfg$method, + fn = private$.worker_fit_bytes, # subclass must provide + args = list(x = x, y = y, learner_id = self$id, predict_types = self$predict_types) + ) + # ret should be list(raw = , meta = ) + self$state$model_raw = ret$raw + self$state$model_path = NULL + self$state$meta = ret$meta + } + + invisible(NULL) + }, + + .predict = function(task) { + cfg = private$.cfg() + newdata = task$data(cols = task$feature_names) + + if (self$state$storage == "file") { + res = .python_callr(cfg$envname, cfg$method, + fn = private$.worker_predict_file, # subclass must provide + args = list(model_path = self$state$model_path, newdata = newdata, + learner_id = self$id, predict_types = self$predict_types) + ) + } else { + res = .python_callr(cfg$envname, cfg$method, + fn = private$.worker_predict_bytes, # subclass must provide + args = list(model_raw = self$state$model_raw, newdata = newdata, + learner_id = self$id, predict_types = self$predict_types) + ) + } + if ("prob" %in% self$predict_types && !is.null(res$prob)) { + return(mlr3::PredictionClassif$new(task = task, prob = res$prob)) + } else { + return(mlr3::PredictionClassif$new(task = task, response = res$response)) + } + }, + + # --- abstract worker functions (to implement in subclasses) --------------- + # Each receives only base R objects; they run inside the Python-activated worker. + # They must import the right Python packages and do the work there. + + .worker_fit_file = function(envname, method, x, y, model_path, learner_id, predict_types) + stop("Subclass must implement .worker_fit_file()"), + + .worker_predict_file = function(envname, method, model_path, newdata, learner_id, predict_types) + stop("Subclass must implement .worker_predict_file()"), + + .worker_fit_bytes = function(envname, method, x, y, learner_id, predict_types) + stop("Subclass must implement .worker_fit_bytes()"), + + .worker_predict_bytes = function(envname, method, model_raw, newdata, learner_id, predict_types) + stop("Subclass must implement .worker_predict_bytes()") + ) +) diff --git a/R/python_proc.R b/R/python_proc.R new file mode 100644 index 000000000..a0c1bd8cd --- /dev/null +++ b/R/python_proc.R @@ -0,0 +1,94 @@ +# Process runner: run any fun. in clean R session that activates some specific python env +.python_callr = function(envname, method, fn, args = list(), libpaths = .libPaths(), timeout = Inf) { + callr::r( + function(envname, method, fn_serialized, args, libpaths) { + .libPaths(libpaths) # ensures the worker can load the same R packages as the caller + if (!requireNamespace("reticulate", quietly = TRUE)) + stop("Package 'reticulate' is required.") + + # Choose env + if (method == "conda") { + reticulate::use_condaenv(envname, required = TRUE) + } else if (method == "virtualenv") { + reticulate::use_virtualenv(envname, required = TRUE) + } else { + conda_ok = tryCatch(!is.na(reticulate::conda_binary("auto")), error = function(e) FALSE) + if (conda_ok) reticulate::use_condaenv(envname, required = TRUE) + else reticulate::use_virtualenv(envname, required = TRUE) + } + + fn = eval(fn_serialized, envir = baseenv()) + do.call(fn, c(list(envname = envname, method = method), args), envir = baseenv()) + }, + args = list( + envname = envname, + method = method, + fn_serialized = substitute(fn), + args = args, + libpaths = libpaths + ), + timeout = timeout + ) +} + +# env bootstrap: build python env in clean process +# meant for preflight , not during R CMD check +.ensure_python_env = function(envname, packages, python_version, method = "auto") { + callr::r(function(envname, packages, python_version, method) { + if (!requireNamespace("reticulate", quietly = TRUE)) + stop("Package 'reticulate' is required.") + + reticulate::py_install( + packages = packages, envname = envname, + method = method, python_version = python_version, pip = TRUE + ) + # Write requiriements.txt for marshalling or debugging + req = tryCatch(reticulate::py_list_packages(envname = envname), error = function(e) NULL) + if (!is.null(req)) { + dir.create(envname, showWarnings = FALSE, recursive = TRUE) + writeLines(sprintf("%s==%s", req$package, req$version), + file.path(envname, "requirements.txt")) + } + + TRUE + }, + args = list(envname = envname, packages = packages, python_version = python_version, method = method)) +} + +# registry helpers to remember each learner’s env config +.mlr_py_registry = new.env(parent = emptyenv()) + +# builds a stable, readable env name from learner id and Python version +# id = "classif.fastai", python_version = "3.10" => mlr3x-classif-fastai-py310 +.mlr_envname = function(id, python_version) { + paste0("mlr3x-", gsub("[^a-z0-9]+", "-", tolower(id)), "-py", gsub("\\D", "", python_version)) +} + +register_python_learner = function(id, packages, python_version, + method = c("auto","virtualenv","conda"), + envname = NULL) { + method = match.arg(method) + if (is.null(envname)) { + envname = .mlr_envname(id, python_version) + + } # else pass env name if two learners should share a Python env + .mlr_py_registry[[id]] = list( + id = id, packages = packages, python_version = python_version, + method = method, envname = envname + ) + invisible(envname) # callers can capture envname if needed +} + +# Create required python envs ahead of time +# CI step before R CMD check, so (hopefully) no NOTE +prepare_python_envs = function(ids = NULL) { + # registry snapshot: + items = as.list.environment(.mlr_py_registry, all.names = TRUE) + if (!is.null(ids)) { + items = items[intersect(names(items), ids)] + } + for (cfg in items) { + .ensure_python_env(cfg$envname, cfg$packages, cfg$python_version, cfg$method) + } + invisible(TRUE) +} From 319466119bbc9257c7cb5303a412f12565082e4a Mon Sep 17 00:00:00 2001 From: AnnaNzrv Date: Sat, 13 Sep 2025 18:04:08 +0200 Subject: [PATCH 2/7] base-class-py --- NEWS.md | 1 + R/LearnerPythonClassif.R | 264 ++++++++++-------------- R/learner_tabpfn_classif_tabpfn.R | 288 +++++++++++++------------- R/py_base_class_fastai.R | 325 ++++++++++++++++++++++++++++++ R/py_base_class_tabpfn.R | 149 ++++++++++++++ R/python_proc.R | 5 + attic/LearnerPythonClassif-old.R | 199 ++++++++++++++++++ attic/demo_python_learners.R | 225 +++++++++++++++++++++ 8 files changed, 1153 insertions(+), 303 deletions(-) create mode 100644 R/py_base_class_fastai.R create mode 100644 R/py_base_class_tabpfn.R create mode 100644 attic/LearnerPythonClassif-old.R create mode 100644 attic/demo_python_learners.R diff --git a/NEWS.md b/NEWS.md index f6fd76e1f..6447cf598 100644 --- a/NEWS.md +++ b/NEWS.md @@ -16,6 +16,7 @@ * Add new `control_custom_fun` parameter in `surv.aorsf` * New function `learner_is_runnable()` to check whether the required packages to train a learner are available. +* New `LearnerPythonClassif` base class for python-powered learners. ## Bug fixes diff --git a/R/LearnerPythonClassif.R b/R/LearnerPythonClassif.R index abc4b6a66..7be659006 100644 --- a/R/LearnerPythonClassif.R +++ b/R/LearnerPythonClassif.R @@ -1,199 +1,145 @@ -LearnerPythonClassif = R6::R6Class( +# R/learner_python_classif_base.R + +LearnerPythonClassif <- R6::R6Class( "LearnerPythonClassif", inherit = mlr3::LearnerClassif, public = list( initialize = function(id, - feature_types = c("logical","integer","numeric","factor","ordered"), - predict_types = c("response"), - param_set = paradox::ParamSet$new(), - # Python requirements - packages, # character(), e.g. c("numpy","pandas","tabpfn") - python_version, # e.g. "3.10" + feature_types = c("logical","integer","numeric","factor","ordered"), + predict_types = c("response"), + param_set = paradox::ParamSet$new(), + properties = character(), + packages = "reticulate", # R packages + label = NA_character_, + man = NA_character_, + py_packages, # Python packages e.g. c("numpy","pandas","tabpfn") + python_version, # "3.10" method = c("auto","virtualenv","conda"), - envname = NULL, - storage = c("file","bytes"), - man = NA_character_) { - - method = match.arg(method) - storage = match.arg(storage) + envname = NULL) { - base_ps = paradox::ParamSet$new(list( - paradox::ParamUty$new("py_env", default = envname, tags = "python"), - paradox::ParamFct$new("py_method", levels = c("auto","virtualenv","conda"), - default = method, tags = "python"), - paradox::ParamLgl$new("py_preflight", default = TRUE, tags = "python") - )) - param_set = paradox::combine_ps(param_set, base_ps) + method <- match.arg(method) - super$initialize( - id = id, feature_types = feature_types, predict_types = predict_types, - param_set = param_set, man = man + base_ps <- ps( + py_env = p_uty(default = envname, tags = "python"), + py_method = p_fct(levels = c("auto","virtualenv","conda"), + default = method, tags = "python") ) + param_set <- ps_union(list(param_set, base_ps)) - private$.py_packages = packages - private$.py_version = python_version - private$.py_method = method - private$.py_env = envname %||% .mlr_envname(id, python_version) - private$.storage = storage - - # Register only (no downloads here) - register_python_learner( - id = id, packages = packages, python_version = python_version, - method = method, envname = private$.py_env + super$initialize( + id = id, + feature_types = feature_types, + predict_types = predict_types, + param_set = param_set, + properties = properties, + packages = union(c("mlr3", "mlr3extralearners"), packages), # keep mlr3 + your R deps + label = label, + man = man ) - # R-side state only; no reticulate objects here - self$state = list( - storage = storage, - model_path = NULL, # if storage == "file" - model_raw = NULL, # if storage == "bytes" - meta = NULL # e.g., class levels - ) - }, + private$.py_packages <- py_packages + private$.py_version <- python_version + private$.py_method <- method + private$.py_env <- envname - prepare_env = function() { - cfg = private$.cfg() - .ensure_python_env(cfg$envname, cfg$packages, cfg$python_version, cfg$method) - invisible(TRUE) + self$model <- NULL }, - marshal = function(dir) { - dir.create(dir, recursive = TRUE, showWarnings = FALSE) - meta = private$.marshal_meta() - jsonlite::write_json(meta, file.path(dir, "python_env.json"), auto_unbox = TRUE, pretty = TRUE) - jsonlite::write_json(self$state$meta %||% list(), file.path(dir, "model_meta.json"), - auto_unbox = TRUE, pretty = TRUE) - - if (self$state$storage == "file") { - stopifnot(!is.null(self$state$model_path) && file.exists(self$state$model_path)) - file.copy(self$state$model_path, file.path(dir, "model.pkl"), overwrite = TRUE) - } else { - stopifnot(is.raw(self$state$model_raw)) - saveRDS(self$state$model_raw, file.path(dir, "model_bytes.rds")) - } - invisible(dir) + py_requirements = function() { + list( + packages = private$.py_packages, + python_version = private$.py_version, + method = private$.py_method, + envname = private$.py_env + ) }, - unmarshal = function(dir) { - meta = jsonlite::read_json(file.path(dir, "python_env.json")) - .ensure_python_env(meta$envname, meta$packages, meta$python_version, meta$method) - - self$state$meta = tryCatch(jsonlite::read_json(file.path(dir, "model_meta.json")), error = function(e) NULL) - - if (file.exists(file.path(dir, "model.pkl"))) { - # file mode - tmp_dir = file.path(tempdir(), paste0("mlr3x-", self$id, "-", as.integer(runif(1,1,1e9)))) - dir.create(tmp_dir, recursive = TRUE) - file.copy(file.path(dir, "model.pkl"), file.path(tmp_dir, "model.pkl"), overwrite = TRUE) - self$state$storage = "file" - self$state$model_path = file.path(tmp_dir, "model.pkl") - self$state$model_raw = NULL - } else { - # bytes mode - raw = readRDS(file.path(dir, "model_bytes.rds")) - self$state$storage = "bytes" - self$state$model_raw = raw - self$state$model_path = NULL + ensure_deps = function() { + if (!requireNamespace("reticulate", quietly = TRUE)) { + stop("Package 'reticulate' required.") } + assert_python_packages(private$.py_packages, python_version = private$.py_version) invisible(TRUE) } ), private = list( .py_packages = NULL, .py_version = NULL, .py_method = NULL, .py_env = NULL, - .storage = NULL, - - .cfg = function() list( - packages = private$.py_packages, - python_version = private$.py_version, - method = self$param_set$values$py_method %||% private$.py_method, - envname = self$param_set$values$py_env %||% private$.py_env - ), - - .marshal_meta = function() { - cfg = private$.cfg() - list(id = self$id, packages = unname(cfg$packages), python_version = cfg$python_version, - method = cfg$method, envname = cfg$envname, timestamp = as.character(Sys.time())) - }, - - # --- lifecycle hooks ------------------------------------------------------ .train = function(task) { - if (isTRUE(self$param_set$values$py_preflight)) self$prepare_env() - + self$ensure_deps() x = task$data(cols = task$feature_names) y = task$truth() - cfg = private$.cfg() - - if (self$state$storage == "file") { - # The worker will write a pickle file here - model_dir = file.path(tempdir(), paste0("mlr3x-", self$id, "-", as.integer(runif(1,1,1e9)))) - dir.create(model_dir, recursive = TRUE, showWarnings = FALSE) - model_path = file.path(model_dir, "model.pkl") - - meta = .python_callr(cfg$envname, cfg$method, - fn = private$.worker_fit_file, # subclass must provide - args = list(x = x, y = y, model_path = model_path, - learner_id = self$id, predict_types = self$predict_types) - ) - - self$state$model_path = model_path - self$state$model_raw = NULL - self$state$meta = meta - - } else { # "bytes" - ret = .python_callr(cfg$envname, cfg$method, - fn = private$.worker_fit_bytes, # subclass must provide - args = list(x = x, y = y, learner_id = self$id, predict_types = self$predict_types) - ) - # ret should be list(raw = , meta = ) - self$state$model_raw = ret$raw - self$state$model_path = NULL - self$state$meta = ret$meta - } - - invisible(NULL) + pars = self$param_set$get_values(tags = "train") + + # fit_py returns a list with entrie "model" holding the trained Python model + # and optionally "meta" holding some extra data + fit = private$.fit_py(x = x, y = y, pars = pars, task = task) + + self$model = structure( + list( + fitted = fit$model, + meta = fit$meta %||% list(), + py_modules = private$.py_packages, + py_version = private$.py_version + ), + class = c(paste0(self$id, "_model"), "pybytes_model") + ) }, .predict = function(task) { - cfg = private$.cfg() - newdata = task$data(cols = task$feature_names) - - if (self$state$storage == "file") { - res = .python_callr(cfg$envname, cfg$method, - fn = private$.worker_predict_file, # subclass must provide - args = list(model_path = self$state$model_path, newdata = newdata, - learner_id = self$id, predict_types = self$predict_types) - ) - } else { - res = .python_callr(cfg$envname, cfg$method, - fn = private$.worker_predict_bytes, # subclass must provide - args = list(model_raw = self$state$model_raw, newdata = newdata, - learner_id = self$id, predict_types = self$predict_types) - ) - } + self$ensure_deps() + if (is.null(self$model)) stop("Model not trained yet.") + newdata = ordered_features(task, self) + res = private$.predict_py(model = self$model$fitted, + newdata = newdata, + predict_type = self$predict_type, + meta = self$model$meta, + class_labels = task$class_names) if ("prob" %in% self$predict_types && !is.null(res$prob)) { - return(mlr3::PredictionClassif$new(task = task, prob = res$prob)) + mlr3::PredictionClassif$new(task = task, prob = res$prob) } else { - return(mlr3::PredictionClassif$new(task = task, response = res$response)) + mlr3::PredictionClassif$new(task = task, response = res$response) } }, - # --- abstract worker functions (to implement in subclasses) --------------- - # Each receives only base R objects; they run inside the Python-activated worker. - # They must import the right Python packages and do the work there. + # Hooks every subclass must implement: + .fit_py = function(x, y, pars, task, ...) stop("Subclass must implement .fit_py(x, y, pars)."), + .predict_py = function(model, newdata, predict_types, meta, class_labels) + stop("Subclass must implement .predict_py(model, newdata, predict_types, meta).") + ) +) - .worker_fit_file = function(envname, method, x, y, model_path, learner_id, predict_types) - stop("Subclass must implement .worker_fit_file()"), - .worker_predict_file = function(envname, method, model_path, newdata, learner_id, predict_types) - stop("Subclass must implement .worker_predict_file()"), +#' @export +marshal_model.pybytes_model <- function(model, inplace = FALSE, ...) { + # If you don't have assert_python_packages(), use py_require() directly: + reticulate::py_require(model$py_modules, python_version = model$py_version) - .worker_fit_bytes = function(envname, method, x, y, learner_id, predict_types) - stop("Subclass must implement .worker_fit_bytes()"), + pickle <- reticulate::import("pickle") + raw <- as.raw(pickle$dumps(model$fitted)) + + structure(list( + marshaled = raw, + py_modules = model$py_modules, + py_version = model$py_version, + meta = model$meta + ), class = c("pybytes_model_marshaled", "marshaled")) +} + +#' @export +unmarshal_model.pybytes_model_marshaled <- function(model, inplace = FALSE, ...) { + reticulate::py_require(model$py_modules, python_version = model$py_version) + + pickle <- reticulate::import("pickle") + fitted <- pickle$loads(reticulate::r_to_py(model$marshaled)) + + structure(list( + fitted = fitted, + meta = model$meta %||% list(), + py_modules = model$py_modules, + py_version = model$py_version + ), class = c(paste0(self$id, "_model"), "pybytes_model")) +} - .worker_predict_bytes = function(envname, method, model_raw, newdata, learner_id, predict_types) - stop("Subclass must implement .worker_predict_bytes()") - ) -) diff --git a/R/learner_tabpfn_classif_tabpfn.R b/R/learner_tabpfn_classif_tabpfn.R index 1387266ed..1ad5b2de2 100644 --- a/R/learner_tabpfn_classif_tabpfn.R +++ b/R/learner_tabpfn_classif_tabpfn.R @@ -39,155 +39,155 @@ LearnerClassifTabPFN = R6Class("LearnerClassifTabPFN", inherit = LearnerClassif, public = list( - #' @description - #' Creates a new instance of this [R6][R6::R6Class] class. - initialize = function() { - ps = ps( - n_estimators = p_int(lower = 1L, default = 4L, tags = "train"), - categorical_features_indices = p_uty(tags = "train", custom_check = function(x) { - # R indexing is used - check_integerish(x, lower = 1, any.missing = FALSE, min.len = 1) - }), - softmax_temperature = p_dbl(default = 0.9, lower = 0, tags = "train"), - balance_probabilities = p_lgl(default = FALSE, tags = "train"), - average_before_softmax = p_lgl(default = FALSE, tags = "train"), - model_path = p_uty(default = "auto", tags = "train", custom_check = check_string), - device = p_uty(default = "auto", tags = "train", custom_check = check_string), - ignore_pretraining_limits = p_lgl(default = FALSE, tags = "train"), - inference_precision = p_fct( - c( - "auto", "autocast", - # all float dtypes - # from https://docs.pytorch.org/docs/stable/tensor_attributes.html - "torch.float32", "torch.float", - "torch.float64", "torch.double", - "torch.float16", "torch.half", - "torch.bfloat16" - ), - default = "auto", - tags = "train" - ), - fit_mode = p_fct( - c("low_memory", "fit_preprocessors", "fit_with_cache"), - default = "fit_preprocessors", - tags = "train" - ), - memory_saving_mode = p_uty(default = "auto", tags = "train", custom_check = function(x) { - if (identical(x, "auto") || test_flag(x) || test_number(x, lower = 0)) { - TRUE - } else { - "Invalid value for memory_saving_mode. Must be 'auto', a TRUE/FALSE value, or a number > 0." - } - }), - random_state = p_int(default = 0L, special_vals = list("None"), tags = "train"), - n_jobs = p_int(lower = 1L, init = 1L, special_vals = list(-1L), tags = "train") - ) - - super$initialize( - id = "classif.tabpfn", - feature_types = c("integer", "numeric", "logical"), - predict_types = c("response", "prob"), - param_set = ps, - packages = "reticulate", - properties = c("twoclass", "multiclass", "missings", "marshal"), - label = "TabPFN Classifier", - man = "mlr3extralearners::mlr_learners_classif.tabpfn" - ) - }, - #' @description - #' Marshal the learner's model. - #' @param ... (any)\cr - #' Additional arguments passed to [`mlr3::marshal_model()`][mlr3::marshaling()]. - marshal = function(...) { - learner_marshal(.learner = self, ...) - }, - #' @description - #' Unmarshal the learner's model. - #' @param ... (any)\cr - #' Additional arguments passed to [`mlr3::unmarshal_model()`][mlr3::marshaling()]. - unmarshal = function(...) { - learner_unmarshal(.learner = self, ...) - } + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. + initialize = function() { + ps = ps( + n_estimators = p_int(lower = 1L, default = 4L, tags = "train"), + categorical_features_indices = p_uty(tags = "train", custom_check = function(x) { + # R indexing is used + check_integerish(x, lower = 1, any.missing = FALSE, min.len = 1) + }), + softmax_temperature = p_dbl(default = 0.9, lower = 0, tags = "train"), + balance_probabilities = p_lgl(default = FALSE, tags = "train"), + average_before_softmax = p_lgl(default = FALSE, tags = "train"), + model_path = p_uty(default = "auto", tags = "train", custom_check = check_string), + device = p_uty(default = "auto", tags = "train", custom_check = check_string), + ignore_pretraining_limits = p_lgl(default = FALSE, tags = "train"), + inference_precision = p_fct( + c( + "auto", "autocast", + # all float dtypes + # from https://docs.pytorch.org/docs/stable/tensor_attributes.html + "torch.float32", "torch.float", + "torch.float64", "torch.double", + "torch.float16", "torch.half", + "torch.bfloat16" + ), + default = "auto", + tags = "train" + ), + fit_mode = p_fct( + c("low_memory", "fit_preprocessors", "fit_with_cache"), + default = "fit_preprocessors", + tags = "train" + ), + memory_saving_mode = p_uty(default = "auto", tags = "train", custom_check = function(x) { + if (identical(x, "auto") || test_flag(x) || test_number(x, lower = 0)) { + TRUE + } else { + "Invalid value for memory_saving_mode. Must be 'auto', a TRUE/FALSE value, or a number > 0." + } + }), + random_state = p_int(default = 0L, special_vals = list("None"), tags = "train"), + n_jobs = p_int(lower = 1L, init = 1L, special_vals = list(-1L), tags = "train") + ) + + super$initialize( + id = "classif.tabpfn", + feature_types = c("integer", "numeric", "logical"), + predict_types = c("response", "prob"), + param_set = ps, + packages = "reticulate", + properties = c("twoclass", "multiclass", "missings", "marshal"), + label = "TabPFN Classifier", + man = "mlr3extralearners::mlr_learners_classif.tabpfn" + ) + }, + #' @description + #' Marshal the learner's model. + #' @param ... (any)\cr + #' Additional arguments passed to [`mlr3::marshal_model()`][mlr3::marshaling()]. + marshal = function(...) { + learner_marshal(.learner = self, ...) + }, + #' @description + #' Unmarshal the learner's model. + #' @param ... (any)\cr + #' Additional arguments passed to [`mlr3::unmarshal_model()`][mlr3::marshaling()]. + unmarshal = function(...) { + learner_unmarshal(.learner = self, ...) + } ), active = list( - #' @field marshaled (`logical(1)`)\cr - #' Whether the learner has been marshaled. - marshaled = function() { - learner_marshaled(self) - } + #' @field marshaled (`logical(1)`)\cr + #' Whether the learner has been marshaled. + marshaled = function() { + learner_marshaled(self) + } ), private = list( - .train = function(task) { - assert_python_packages(c("torch", "tabpfn")) - tabpfn = reticulate::import("tabpfn") - torch = reticulate::import("torch") - - pars = self$param_set$get_values(tags = "train") - - inference_precision = pars$inference_precision - if (!is.null(inference_precision) && startsWith(inference_precision, "torch.")) { - dtype = strsplit(inference_precision, "\\.")[[1]][2] - pars$inference_precision = reticulate::py_get_attr(torch, dtype) - } - - if (!is.null(pars$device) && pars$device != "auto") { - torch = reticulate::import("torch") - pars$device = torch$device(pars$device) - } - - if (identical(pars$random_state, "None")) { - pars$random_state = reticulate::py_none() - } - - # x is an (n_samples, n_features) array - x = as.matrix(task$data(cols = task$feature_names)) - # force NaN to make conversion work, - # otherwise reticulate will not convert NAs in logical and integer columns to - # np.nan properly - x[is.na(x)] = NaN - # y is an (n_samples,) array - y = task$truth() - - # convert categorical_features_indices to python indexing - categ_indices = pars$categorical_features_indices - if (!is.null(categ_indices)) { - if (max(categ_indices) > ncol(x)) { - stop("categorical_features_indices must not exceed number of features") - } - pars$categorical_features_indices = as.integer(categ_indices - 1) - } - - classifier = mlr3misc::invoke(tabpfn$TabPFNClassifier, .args = pars) - x_py = reticulate::r_to_py(x) - y_py = reticulate::r_to_py(y) - fitted = mlr3misc::invoke(classifier$fit, X = x_py, y = y_py) - - structure(list(fitted = fitted), class = "tabpfn_model") - }, - - .predict = function(task) { - assert_python_packages("tabpfn") - reticulate::import("tabpfn") - model = self$model$fitted - - x = as.matrix(task$data(cols = task$feature_names)) - # NA -> NaN, same reason as in $.train - x[is.na(x)] = NaN - x_py = reticulate::r_to_py(x) - - if (self$predict_type == "response") { - response = mlr3misc::invoke(model$predict, X = x_py) - response = reticulate::py_to_r(response) - list(response = response) - } else { - prob = mlr3misc::invoke(model$predict_proba, X = x_py) - prob = reticulate::py_to_r(prob) - colnames(prob) = reticulate::py_to_r(model$classes_) - list(prob = prob) - } - } + .train = function(task) { + assert_python_packages(c("torch", "tabpfn")) + tabpfn = reticulate::import("tabpfn") + torch = reticulate::import("torch") + + pars = self$param_set$get_values(tags = "train") + + inference_precision = pars$inference_precision + if (!is.null(inference_precision) && startsWith(inference_precision, "torch.")) { + dtype = strsplit(inference_precision, "\\.")[[1]][2] + pars$inference_precision = reticulate::py_get_attr(torch, dtype) + } + + if (!is.null(pars$device) && pars$device != "auto") { + torch = reticulate::import("torch") + pars$device = torch$device(pars$device) + } + + if (identical(pars$random_state, "None")) { + pars$random_state = reticulate::py_none() + } + + # x is an (n_samples, n_features) array + x = as.matrix(task$data(cols = task$feature_names)) + # force NaN to make conversion work, + # otherwise reticulate will not convert NAs in logical and integer columns to + # np.nan properly + x[is.na(x)] = NaN + # y is an (n_samples,) array + y = task$truth() + + # convert categorical_features_indices to python indexing + categ_indices = pars$categorical_features_indices + if (!is.null(categ_indices)) { + if (max(categ_indices) > ncol(x)) { + stop("categorical_features_indices must not exceed number of features") + } + pars$categorical_features_indices = as.integer(categ_indices - 1) + } + + classifier = mlr3misc::invoke(tabpfn$TabPFNClassifier, .args = pars) + x_py = reticulate::r_to_py(x) + y_py = reticulate::r_to_py(y) + fitted = mlr3misc::invoke(classifier$fit, X = x_py, y = y_py) + + structure(list(fitted = fitted), class = "tabpfn_model") + }, + + .predict = function(task) { + assert_python_packages("tabpfn") + reticulate::import("tabpfn") + model = self$model$fitted + + x = as.matrix(task$data(cols = task$feature_names)) + # NA -> NaN, same reason as in $.train + x[is.na(x)] = NaN + x_py = reticulate::r_to_py(x) + + if (self$predict_type == "response") { + response = mlr3misc::invoke(model$predict, X = x_py) + response = reticulate::py_to_r(response) + list(response = response) + } else { + prob = mlr3misc::invoke(model$predict_proba, X = x_py) + prob = reticulate::py_to_r(prob) + colnames(prob) = reticulate::py_to_r(model$classes_) + list(prob = prob) + } + } ) ) @@ -219,4 +219,4 @@ unmarshal_model.tabpfn_model_marshaled = function(model, inplace = FALSE, ...) { # unpickle fitted = pickle$loads(reticulate::r_to_py(model$marshaled)) structure(list(fitted = fitted), class = "tabpfn_model") -} +} \ No newline at end of file diff --git a/R/py_base_class_fastai.R b/R/py_base_class_fastai.R new file mode 100644 index 000000000..84a029cec --- /dev/null +++ b/R/py_base_class_fastai.R @@ -0,0 +1,325 @@ +#' @title Classification Neural Network Learner +#' @author annanzrv +#' @name mlr_learners_classif.fastai +#' +#' @description +#' Simple and fast neural nets for tabular data classification. +#' Calls [fastai::tabular_learner()] from package \CRANpkg{fastai}. +#' +#' @templateVar id classif.fastai +#' @template learner +#' +#' @references +#' `r format_bib("howard_2020")` +#' @export +LearnerPythonClassifFastai <- R6::R6Class( + "LearnerPythonClassifFastai", + inherit = LearnerPythonClassif, + + public = list( + initialize = function() { + + p_n_epoch = p_int( + lower = 1L, + tags = c("train", "hotstart", "internal_tuning"), + init = 5L, + aggr = crate(function(x) as.integer(ceiling(mean(unlist(x)))), .parent = topenv()), + in_tune_fn = crate(function(domain, param_vals) { + if (is.null(param_vals$patience)) { + stop("Parameter 'patience' must be set to use internal tuning.") + } + assert_integerish(domain$upper, len = 1L, any.missing = FALSE) + }, .parent = topenv()), + disable_in_tune = list(n_epoch = NULL) + ) + + ps_fastai <- ps( + act_cls = p_uty(tags = "train"), # Activation type for LinBnDrop layers, e.g., fastai::nn()$ReLU(inplace = TRUE) + bn_cont = p_lgl(default = TRUE, tags = "train"), # Use BatchNorm1d on continuous variables + bn_final = p_lgl(default = FALSE, tags = "train"), # Use BatchNorm1d on final layer + drop_last = p_lgl(default = FALSE, tags = "train"), # If True, then the last incomplete batch is dropped. + embed_p = p_dbl(lower = 0L, upper = 1L, default = 0L, tags = "train"), # Dropout probability for Embedding layer + emb_szs = p_uty(default = NULL, tags = "train"), # Sequence of (num_embeddings, embedding_dim) for each categorical variable + n_epoch = p_n_epoch, + eval_metric = p_uty(tags = "train", custom_check = crate({ + function(x) check_true(is.function(x) || inherits(x, "Measure")) + })), + layers = p_uty(tags = "train"), # Sequence of ints used to specify the input and output size of each LinBnDrop layer + loss_func = p_uty(tags = "train"), # Defaults to fastai::CrossEntropyLossFlat() + lr = p_dbl(lower = 0, default = 0.001, tags = "train"), # Learning rate + metrics = p_uty(tags = "train"), # optional list of metrics, e.g, fastai::Precision() or fastai::accuracy() + n_out = p_int(tags = "train"), # ? + num_workers = p_int(default = 0L, tags = "train"), # how many subprocesses to use for data loading + opt_func = p_uty(tags = "train"), # Optimizer created when Learner.fit is called. E.g., fastai::Adam() + patience = p_int(1L, default = 1, tags = "train"), # number of epochs to wait when training has not improved model. add `depends = quote(early_stopping == TRUE`)` + pin_memory = p_lgl(default = TRUE, tags = "train"), # If True, the data loader will copy Tensors into CUDA pinned memory before returning them. + procs = p_uty(default = NULL, tags = "train"), # fastai preprocessing steps such as fastai::Categorify, fastai::Normalize, fastai::fill_missing + ps = p_uty(default = NULL, tags = "train"), # Sequence of dropout probabilities + shuffle = p_lgl(default = FALSE, tags = "train"), # If True, then data is shuffled every time dataloader is fully read/iterated. + train_bn = p_lgl(default = TRUE, tags = "train"), # controls if BatchNorm layers are trained + wd = p_int(lower = 0, tags = "train"), # weight decay used for optimization + wd_bn_bias = p_lgl(default = FALSE, tags = "train"), # controls if weight decay is applied to BatchNorm layers and bias + use_bn = p_lgl(default = TRUE, tags = "train"), # Use BatchNorm1d in LinBnDrop layers + y_range = p_uty(default = NULL, tags = "train"), # Low and high for SigmoidRange activation (see below) + bs = p_int(default = 50, tags = "train") # how many samples per batch to load + ) + + super$initialize( + id = "classif.python.fastai", + feature_types = c("logical", "integer", "numeric", "factor", "ordered"), + predict_types = c("response", "prob"), + param_set = ps_fastai, + properties = c("multiclass", "twoclass", "weights", "validation", "marshal"), + packages = c("mlr3extralearners", "fastai", "reticulate"), # R packages + label = "Fastai Tabular Classifier", + man = "mlr3extralearners::mlr_learners_classif.fastai", + + py_packages = c("fastai", "torch"), + python_version = "3.10", + method = "auto" + ) + }, + #' @description + #' Marshal the learner's model. + #' @param ... (any)\cr + #' Additional arguments passed to [`mlr3::marshal_model()`][mlr3::marshaling()]. + marshal = function(...) { + learner_marshal(.learner = self, ...) + }, + #' @description + #' Unmarshal the learner's model. + #' @param ... (any)\cr + #' Additional arguments passed to [`mlr3::marshal_model()`][mlr3::marshaling()]. + unmarshal = function(...) { + learner_unmarshal(.learner = self, ...) + } + ), + + active = list( + #' @field internal_valid_scores (named `list()` or `NULL`) + #' The validation scores extracted from `eval_protocol` which itself is set by fitting the `fastai::tab_learner`. + #' If early stopping is activated, this contains the validation scores of the model for the optimal `n_epoch`, + #' otherwise the `n_epoch` for the final model. + internal_valid_scores = function() { + self$state$internal_valid_scores + }, + + #' @field internal_tuned_values (named `list()` or `NULL`) + #' If early stopping is activated, this returns a list with `n_epoch`, + #' which is the last epoch that yielded improvement w.r.t. the `patience`, extracted by `max(eval_protocol$epoch)+1` + internal_tuned_values = function() { + self$state$internal_tuned_values + }, + + #' @field validate (`numeric(1)` or `character(1)` or `NULL`) + #' How to construct the internal validation data. This parameter can be either `NULL`, + #' a ratio, `"test"`, or `"predefined"`. + validate = function(rhs) { + if (!missing(rhs)) { + private$.validate = assert_validate(rhs) + } + private$.validate + }, + + #' @field marshaled (`logical(1)`) + #' Whether the learner has been marshaled. + marshaled = function() { + learner_marshaled(self) + } + ), + + private = list( + .validate = NULL, + + # -------------------- TRAIN HOOK -------------------- + .fit_py = function(x, y, pars, task) { + self$ensure_deps() + + # We need the task columns split into cat/cont; fastai uses names, not indices. + data = task$data() + type = NULL + cat_cols = task$feature_types[type != "numeric"]$id + num_cols = task$feature_types[type == "numeric"]$id + + # Default eval metric if none is supplied + measure = pars$eval_metric + patience = pars$patience + if (is.null(measure)) measure = fastai::accuracy() + if (length(cat_cols) && is.null(pars$procs)) pars$procs = list(fastai::Categorify()) + + # match parameters to fastai functions + fastai2 = getFromNamespace("fastai2", ns = "fastai") + args_dt = formalArgs(fastai::TabularDataTable) + args_dl = formalArgs(fastai2$data$load$DataLoader) + args_config = formalArgs(fastai::tabular_config) + args_fit = formalArgs(fastai2$learner$Learner$fit) + + pv_dt = pars[names(pars) %in% args_dt] + pv_dl = pars[names(pars) %in% args_dl] + pv_config = pars[names(pars) %in% args_config] + pv_fit = pars[names(pars) %in% args_fit] + pv_layers = pars[names(pars) == "layers"] + + # internal validation + internal_valid_task = task$internal_valid_task + if (!is.null(patience) && is.null(internal_valid_task)) { + stopf("Learner (%s): Configure field 'validate' to enable early stopping.", self$id) + } + + if (!is.null(internal_valid_task)) { + full_data = data.table::rbindlist(list(data, internal_valid_task$data())) + splits = list(seq(task$nrow), seq(task$nrow + 1, task$nrow + internal_valid_task$nrow)) + } else { + full_data = data + splits = NULL + } + + metrics = if (inherits(measure, "Measure")) { + # wrap mlr3 measure into fastai metric + params_for_metric = if ("twoclass" %in% unlist(measure$task_properties)) list(positive = "1") + invoke( + fastai::AccumMetric, + metric, + flatten = FALSE, + msr = measure, + lvl = levels(factor(as.integer(y) - 1L)), + .args = params_for_metric + ) + } else { + # measure from fastai + measure + } + + # set data into a format suitable for fastai + df_fai = invoke( + fastai::TabularDataTable, + df = full_data, + cat_names = cat_cols, + cont_names = num_cols, + y_names = task$target_names, + splits = splits, + .args = pv_dt + ) + + dls = invoke( + fastai::dataloaders, + df_fai, + .args = pv_dl + ) + + config = invoke(fastai::tabular_config, .args = pv_config) + + # observation weights (normalized), if present + weights <- private$.get_weights(task) + if (!is.null(weights)) { + dls$train$wgts = weights / sum(weights) + } + + tab_learner <- fastai::tabular_learner( + dls = dls, + layers = pv_layers, + config = config, + metrics = metrics + ) + + # early stopping / internal tuning + if (!is.null(patience)) { + monitor <- tab_learner$metrics[[0]]$name + + # direction for internal tuning + # if mlr3 measure and minimize use numpy less + comp = if (inherits(measure, "Measure") && measure$minimize) { + np = reticulate::import("numpy") + np$less + } + + tab_learner$add_cb( + fastai::EarlyStoppingCallback(monitor = monitor, comp = comp, patience = patience) + ) + } + + # avoid plot creation when internally validating + invisible(tab_learner$remove_cb(tab_learner$progress)) + + # prevent python from printing evaluation protocol + eval_protocol = NULL + invisible(reticulate::py_capture_output({ + eval_protocol = invoke( + fastai::fit, + object = tab_learner, + .args = pv_fit + ) + })) + + # Rename eval protocol in case custom metric was used + names(eval_protocol)[ + names(eval_protocol) == "python_function" + ] = if (inherits(measure, "Measure")) measure$id + + list( + model = tab_learner, + meta = list(eval_protocol = eval_protocol) + ) + }, + + # -------------------- PREDICT HOOK -------------------- + .predict_py = function(model, newdata, predict_type, meta, class_labels) { + self$ensure_deps() + + # Fastai expects same column order & types as during fit + pars_pred = self$param_set$get_values(tags = "predict") + pred = invoke(fastai::predict, model, newdata, .args = pars_pred) + + if ("prob" %in% predict_type) { + prob = as.matrix(pred[, class_labels, drop = FALSE]) + list(response = NULL, prob = prob) + } else { + response = class_labels[pred$class + 1L] + list(response = response, prob = NULL) + } + }, + + # ---------------- Validation extractors ---------------- + .extract_internal_tuned_values = function() { + ep <- self$model$meta$eval_protocol + if (is.null(self$state$param_vals$patience) || is.null(ep)) return(NULL) + list(n_epoch = max(ep$epoch) + 1L) + }, + + .extract_internal_valid_scores = function() { + ep <- self$model$meta$eval_protocol + if (is.null(ep)) return(NULL) + metric <- self$model$fitted$metrics[[0]]$name + metric_name <- if (metric == "python_function") { + self$state$param_vals$eval_metric$id + } else metric + set_names(list(ep[nrow(ep), metric_name]), metric_name) + } + ) +) + +.extralrns_dict$add("classif.python.fastai", LearnerPythonClassifFastai) + +# Wrapper for eval measure to include in fastai +metric = function(pred, dtrain, msr = NULL, lvl = NULL, ...) { + reticulate::py_require("fastai") + + # label is a vector of labels (0, 1, ..., n_classes - 1) + pred = fastai::as_array(pred) + truth = factor(as.vector(fastai::as_array(dtrain)), levels = lvl) + # transform log odds to probabilities + pred_exp = exp(pred) + pred_mat = pred_exp / rowSums(pred_exp) + colnames(pred_mat) = lvl + pred = pred_mat + # only look at the positive class + if ("twoclass" %in% unlist(msr$task_properties)) { + pred = pred_mat[, 2] + } + # transform prediction into class labels + if (msr$predict_type == "response") { + p = apply(pred_mat, MARGIN = 1, FUN = function(x) which.max(x) - 1) + pred = factor(p, levels = levels(truth)) + } + msr$fun(truth, pred, ...) +} diff --git a/R/py_base_class_tabpfn.R b/R/py_base_class_tabpfn.R new file mode 100644 index 000000000..6e6b6d0fb --- /dev/null +++ b/R/py_base_class_tabpfn.R @@ -0,0 +1,149 @@ +# R/learner_classif_tabpfn.R + +LearnerPythonClassifTabPFN <- R6::R6Class( + "LearnerPythonClassifTabPFN", + inherit = LearnerPythonClassif, + + public = list( + initialize = function() { + ps <- ps( + n_estimators = p_int(lower = 1L, default = 4L, tags = "train"), + categorical_features_indices = p_uty(tags = "train", custom_check = function(x) { + check_integerish(x, lower = 1, any.missing = FALSE, min.len = 1) + }), + softmax_temperature = p_dbl(default = 0.9, lower = 0, tags = "train"), + balance_probabilities = p_lgl(default = FALSE, tags = "train"), + average_before_softmax = p_lgl(default = FALSE, tags = "train"), + model_path = p_uty(default = "auto", tags = "train", custom_check = check_string), + device = p_uty(default = "auto", tags = "train", custom_check = check_string), + ignore_pretraining_limits = p_lgl(default = FALSE, tags = "train"), + inference_precision = p_fct( + c("auto","autocast", + "torch.float32","torch.float","torch.float64","torch.double", + "torch.float16","torch.half","torch.bfloat16"), + default = "auto", tags = "train" + ), + fit_mode = p_fct( + c("low_memory","fit_preprocessors","fit_with_cache"), + default = "fit_preprocessors", tags = "train" + ), + memory_saving_mode = p_uty(default = "auto", tags = "train", custom_check = function(x) { + if (identical(x, "auto") || test_flag(x) || test_number(x, lower = 0)) TRUE + else "Invalid value for memory_saving_mode. Must be 'auto', a TRUE/FALSE value, or a number > 0." + }), + random_state = p_int(default = 0L, special_vals = list("None"), tags = "train"), + n_jobs = p_int(lower = 1L, init = 1L, special_vals = list(-1L), tags = "train") + ) + + super$initialize( + id = "classif.python.tabpfn", + feature_types = c("integer","numeric","logical"), + predict_types = c("response","prob"), + param_set = ps, + py_packages = c("numpy","pandas","torch","tabpfn"), + python_version = "3.10", + method = "auto", + properties = c("twoclass", "multiclass", "missings", "marshal"), + man = "mlr3extralearners::mlr_learners_classif.tabpfn" + ) + }, + + #' @description + #' Marshal the learner's model. + #' @param ... (any)\cr + #' Additional arguments passed to [`mlr3::marshal_model()`][mlr3::marshaling()]. + marshal = function(...) { + learner_marshal(.learner = self, ...) + }, + #' @description + #' Unmarshal the learner's model. + #' @param ... (any)\cr + #' Additional arguments passed to [`mlr3::marshal_model()`][mlr3::marshaling()]. + unmarshal = function(...) { + learner_unmarshal(.learner = self, ...) + } + ), + + active = list( + #' @field internal_valid_scores (named `list()` or `NULL`) + #' The validation scores extracted from `eval_protocol` which itself is set by fitting the `fastai::tab_learner`. + #' If early stopping is activated, this contains the validation scores of the model for the optimal `n_epoch`, + #' otherwise the `n_epoch` for the final model. + internal_valid_scores = function() { + self$state$internal_valid_scores + }, + + #' @field internal_tuned_values (named `list()` or `NULL`) + #' If early stopping is activated, this returns a list with `n_epoch`, + #' which is the last epoch that yielded improvement w.r.t. the `patience`, extracted by `max(eval_protocol$epoch)+1` + internal_tuned_values = function() { + self$state$internal_tuned_values + }, + + #' @field validate (`numeric(1)` or `character(1)` or `NULL`) + #' How to construct the internal validation data. This parameter can be either `NULL`, + #' a ratio, `"test"`, or `"predefined"`. + validate = function(rhs) { + if (!missing(rhs)) { + private$.validate = assert_validate(rhs) + } + private$.validate + }, + + #' @field marshaled (`logical(1)`) + #' Whether the learner has been marshaled. + marshaled = function() { + learner_marshaled(self) + } + ), + + private = list( + .fit_py = function(x, y, pars, task) { + torch <- reticulate::import("torch", delay_load = FALSE) + tabpfn <- reticulate::import("tabpfn", delay_load = FALSE) + + ip <- pars$inference_precision + if (!is.null(ip) && startsWith(ip, "torch.")) { + dtype <- strsplit(ip, "\\.", fixed = TRUE)[[1]][2] + pars$inference_precision <- reticulate::py_get_attr(torch, dtype) + } + if (!is.null(pars$device) && pars$device != "auto") { + pars$device <- torch$device(pars$device) + } + if (identical(pars$random_state, "None")) { + pars$random_state <- reticulate::py_none() + } + if (!is.null(pars$categorical_features_indices)) { + pars$categorical_features_indices <- as.integer(pars$categorical_features_indices - 1L) + } + + X <- as.matrix(x); storage.mode(X) <- "double"; X[is.na(X)] <- NaN + X_py <- reticulate::r_to_py(X) + y_py <- reticulate::r_to_py(as.vector(y)) + + clf <- mlr3misc::invoke(tabpfn$TabPFNClassifier, .args = pars) + fitted <- mlr3misc::invoke(clf$fit, X = X_py, y = y_py) + + classes <- as.character(reticulate::py_to_r(fitted$classes_)) + list(model = fitted, meta = list(classes = classes)) + }, + + .predict_py = function(model, newdata, predict_types, meta, class_names) { + X <- as.matrix(newdata); storage.mode(X) <- "double"; X[is.na(X)] <- NaN + X_py <- reticulate::r_to_py(X) + + if ("prob" %in% predict_types) { + prob <- mlr3misc::invoke(model$predict_proba, X = X_py) + prob <- reticulate::py_to_r(prob) + colnames(prob) <- meta$classes %||% as.character(reticulate::py_to_r(model$classes_)) + list(response = NULL, prob = prob) + } else { + response <- mlr3misc::invoke(model$predict, X = X_py) + response <- reticulate::py_to_r(response) + list(response = as.character(response), prob = NULL) + } + } + ) +) + +.extralrns_dict$add("classif.python.tabpfn", LearnerPythonClassifTabPFN) diff --git a/R/python_proc.R b/R/python_proc.R index a0c1bd8cd..f0d4599a8 100644 --- a/R/python_proc.R +++ b/R/python_proc.R @@ -1,3 +1,8 @@ +#-------------------------- CODE CEMETERY ---------------------------------------------------------------------- + +# Helper functions to build a base class for python based learners that can run +# each instance in separated R session. + # Process runner: run any fun. in clean R session that activates some specific python env .python_callr = function(envname, method, fn, args = list(), libpaths = .libPaths(), timeout = Inf) { callr::r( diff --git a/attic/LearnerPythonClassif-old.R b/attic/LearnerPythonClassif-old.R new file mode 100644 index 000000000..abc4b6a66 --- /dev/null +++ b/attic/LearnerPythonClassif-old.R @@ -0,0 +1,199 @@ +LearnerPythonClassif = R6::R6Class( + "LearnerPythonClassif", + inherit = mlr3::LearnerClassif, + + public = list( + initialize = function(id, + feature_types = c("logical","integer","numeric","factor","ordered"), + predict_types = c("response"), + param_set = paradox::ParamSet$new(), + # Python requirements + packages, # character(), e.g. c("numpy","pandas","tabpfn") + python_version, # e.g. "3.10" + method = c("auto","virtualenv","conda"), + envname = NULL, + storage = c("file","bytes"), + man = NA_character_) { + + method = match.arg(method) + storage = match.arg(storage) + + base_ps = paradox::ParamSet$new(list( + paradox::ParamUty$new("py_env", default = envname, tags = "python"), + paradox::ParamFct$new("py_method", levels = c("auto","virtualenv","conda"), + default = method, tags = "python"), + paradox::ParamLgl$new("py_preflight", default = TRUE, tags = "python") + )) + param_set = paradox::combine_ps(param_set, base_ps) + + super$initialize( + id = id, feature_types = feature_types, predict_types = predict_types, + param_set = param_set, man = man + ) + + private$.py_packages = packages + private$.py_version = python_version + private$.py_method = method + private$.py_env = envname %||% .mlr_envname(id, python_version) + private$.storage = storage + + # Register only (no downloads here) + register_python_learner( + id = id, packages = packages, python_version = python_version, + method = method, envname = private$.py_env + ) + + # R-side state only; no reticulate objects here + self$state = list( + storage = storage, + model_path = NULL, # if storage == "file" + model_raw = NULL, # if storage == "bytes" + meta = NULL # e.g., class levels + ) + }, + + prepare_env = function() { + cfg = private$.cfg() + .ensure_python_env(cfg$envname, cfg$packages, cfg$python_version, cfg$method) + invisible(TRUE) + }, + + marshal = function(dir) { + dir.create(dir, recursive = TRUE, showWarnings = FALSE) + meta = private$.marshal_meta() + jsonlite::write_json(meta, file.path(dir, "python_env.json"), auto_unbox = TRUE, pretty = TRUE) + jsonlite::write_json(self$state$meta %||% list(), file.path(dir, "model_meta.json"), + auto_unbox = TRUE, pretty = TRUE) + + if (self$state$storage == "file") { + stopifnot(!is.null(self$state$model_path) && file.exists(self$state$model_path)) + file.copy(self$state$model_path, file.path(dir, "model.pkl"), overwrite = TRUE) + } else { + stopifnot(is.raw(self$state$model_raw)) + saveRDS(self$state$model_raw, file.path(dir, "model_bytes.rds")) + } + invisible(dir) + }, + + unmarshal = function(dir) { + meta = jsonlite::read_json(file.path(dir, "python_env.json")) + .ensure_python_env(meta$envname, meta$packages, meta$python_version, meta$method) + + self$state$meta = tryCatch(jsonlite::read_json(file.path(dir, "model_meta.json")), error = function(e) NULL) + + if (file.exists(file.path(dir, "model.pkl"))) { + # file mode + tmp_dir = file.path(tempdir(), paste0("mlr3x-", self$id, "-", as.integer(runif(1,1,1e9)))) + dir.create(tmp_dir, recursive = TRUE) + file.copy(file.path(dir, "model.pkl"), file.path(tmp_dir, "model.pkl"), overwrite = TRUE) + self$state$storage = "file" + self$state$model_path = file.path(tmp_dir, "model.pkl") + self$state$model_raw = NULL + } else { + # bytes mode + raw = readRDS(file.path(dir, "model_bytes.rds")) + self$state$storage = "bytes" + self$state$model_raw = raw + self$state$model_path = NULL + } + invisible(TRUE) + } + ), + + private = list( + .py_packages = NULL, .py_version = NULL, .py_method = NULL, .py_env = NULL, + .storage = NULL, + + .cfg = function() list( + packages = private$.py_packages, + python_version = private$.py_version, + method = self$param_set$values$py_method %||% private$.py_method, + envname = self$param_set$values$py_env %||% private$.py_env + ), + + .marshal_meta = function() { + cfg = private$.cfg() + list(id = self$id, packages = unname(cfg$packages), python_version = cfg$python_version, + method = cfg$method, envname = cfg$envname, timestamp = as.character(Sys.time())) + }, + + # --- lifecycle hooks ------------------------------------------------------ + + .train = function(task) { + if (isTRUE(self$param_set$values$py_preflight)) self$prepare_env() + + x = task$data(cols = task$feature_names) + y = task$truth() + cfg = private$.cfg() + + if (self$state$storage == "file") { + # The worker will write a pickle file here + model_dir = file.path(tempdir(), paste0("mlr3x-", self$id, "-", as.integer(runif(1,1,1e9)))) + dir.create(model_dir, recursive = TRUE, showWarnings = FALSE) + model_path = file.path(model_dir, "model.pkl") + + meta = .python_callr(cfg$envname, cfg$method, + fn = private$.worker_fit_file, # subclass must provide + args = list(x = x, y = y, model_path = model_path, + learner_id = self$id, predict_types = self$predict_types) + ) + + self$state$model_path = model_path + self$state$model_raw = NULL + self$state$meta = meta + + } else { # "bytes" + ret = .python_callr(cfg$envname, cfg$method, + fn = private$.worker_fit_bytes, # subclass must provide + args = list(x = x, y = y, learner_id = self$id, predict_types = self$predict_types) + ) + # ret should be list(raw = , meta = ) + self$state$model_raw = ret$raw + self$state$model_path = NULL + self$state$meta = ret$meta + } + + invisible(NULL) + }, + + .predict = function(task) { + cfg = private$.cfg() + newdata = task$data(cols = task$feature_names) + + if (self$state$storage == "file") { + res = .python_callr(cfg$envname, cfg$method, + fn = private$.worker_predict_file, # subclass must provide + args = list(model_path = self$state$model_path, newdata = newdata, + learner_id = self$id, predict_types = self$predict_types) + ) + } else { + res = .python_callr(cfg$envname, cfg$method, + fn = private$.worker_predict_bytes, # subclass must provide + args = list(model_raw = self$state$model_raw, newdata = newdata, + learner_id = self$id, predict_types = self$predict_types) + ) + } + if ("prob" %in% self$predict_types && !is.null(res$prob)) { + return(mlr3::PredictionClassif$new(task = task, prob = res$prob)) + } else { + return(mlr3::PredictionClassif$new(task = task, response = res$response)) + } + }, + + # --- abstract worker functions (to implement in subclasses) --------------- + # Each receives only base R objects; they run inside the Python-activated worker. + # They must import the right Python packages and do the work there. + + .worker_fit_file = function(envname, method, x, y, model_path, learner_id, predict_types) + stop("Subclass must implement .worker_fit_file()"), + + .worker_predict_file = function(envname, method, model_path, newdata, learner_id, predict_types) + stop("Subclass must implement .worker_predict_file()"), + + .worker_fit_bytes = function(envname, method, x, y, learner_id, predict_types) + stop("Subclass must implement .worker_fit_bytes()"), + + .worker_predict_bytes = function(envname, method, model_raw, newdata, learner_id, predict_types) + stop("Subclass must implement .worker_predict_bytes()") + ) +) diff --git a/attic/demo_python_learners.R b/attic/demo_python_learners.R new file mode 100644 index 000000000..5c2fe393d --- /dev/null +++ b/attic/demo_python_learners.R @@ -0,0 +1,225 @@ +skip_if_not_installed("fastai") +skip_if_not_installed("reticulate") + +test_that("autotest", { + learner = lrn("classif.python.fastai", layers = c(200, 100)) + # expect_learner(learner, check_man = FALSE) # will not work because base class has new tags + # results not replicable, because torch seed must be set in the python backend + result = run_autotest(learner, check_replicable = FALSE) + expect_true(result, info = result$error) +}) + +learner = lrn("classif.python.fastai", layers = c(200, 100)) +#learner$predict_type = "prob" +tasks = generate_tasks(learner) +task = tasks[[1L]] +learner$train(task) +p = learner$predict(task) + + +test_that("eval protocol", { + learner = lrn("classif.python.fastai") + task = tsk("sonar") + learner$train(task) + expect_true(is.list(learner$model$meta$eval_protocol)) +}) + + +test_that("validation and inner tuning works", { + task = tsk("spam") + + # with n_epoch and patience parameter + learner = lrn("classif.python.fastai", + n_epoch = 10, + patience = 1, + validate = 0.2 + ) + + learner$train(task) + expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "accuracy")) + expect_list(learner$internal_valid_scores, types = "numeric") + expect_equal(names(learner$internal_valid_scores), "accuracy") + expect_list(learner$internal_tuned_values, types = "integerish") + expect_equal(names(learner$internal_tuned_values), "n_epoch") + + # without validate parameter + learner$validate = NULL + expect_error(learner$train(task), "field 'validate'") + + # with patience parameter + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2 + ) + + learner$train(task) + expect_equal(learner$internal_tuned_values, NULL) + expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "accuracy")) + expect_list(learner$internal_valid_scores, types = "numeric") + expect_equal(names(learner$internal_valid_scores), "accuracy") + + # internal tuning + learner = lrn("classif.python.fastai", + n_epoch = to_tune(upper = 20, internal = TRUE), + validate = 0.2 + ) + s = learner$param_set$search_space() + expect_error(learner$param_set$convert_internal_search_space(s), "patience") + learner$param_set$set_values(n_epoch = 10) + learner$param_set$disable_internal_tuning("n_epoch") + expect_equal(learner$param_set$values$n_epoch, NULL) + + learner = lrn("classif.python.fastai", + n_epoch = 20, + patience = 5, + validate = 0.3 + ) + learner$train(task) + expect_equal(learner$internal_valid_scores$accuracy, learner$model$meta$eval_protocol$accuracy[learner$internal_tuned_values$n_epoch]) + + # no validation and no internal tuning + learner = lrn("classif.python.fastai") + learner$train(task) + expect_null(learner$internal_valid_scores) + expect_null(learner$internal_tuned_values) + + # no tuning without patience parameter + learner = lrn("classif.python.fastai", validate = 0.3, n_epoch = 10) + learner$train(task) + expect_equal(learner$internal_valid_scores$accuracy, learner$model$meta$eval_protocol$accuracy[10L]) + expect_null(learner$internal_tuned_values) +}) + +test_that("custom inner validation measure", { + # internal measure + task = tsk("sonar") + + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2, + patience = 1, + eval_metric = fastai::error_rate() + ) + + learner$train(task) + + expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "error_rate")) + expect_list(learner$internal_valid_scores, types = "numeric") + expect_equal(names(learner$internal_valid_scores), "error_rate") + + # binary task and mlr3 measure binary response + task = tsk("sonar") + + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2, + #patience = 1, + eval_metric = msr("classif.ce") + ) + + learner$train(task) + + expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.ce")) + expect_numeric(learner$model$meta$eval_protocol$classif.ce, len = 10) + expect_list(learner$internal_valid_scores, types = "numeric") + expect_equal(names(learner$internal_valid_scores), "classif.ce") + + # binary task and mlr3 measure binary prob + task = tsk("sonar") + + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2, + #patience = 1, + predict_type = "prob", + eval_metric = msr("classif.logloss") + ) + + learner$train(task) + + expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.logloss")) + expect_numeric(learner$model$meta$eval_protocol$classif.logloss, len = 10) + expect_list(learner$internal_valid_scores, types = "numeric") + expect_equal(names(learner$internal_valid_scores), "classif.logloss") + + # binary task and mlr3 measure multiclass prob + task = tsk("sonar") + + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2, + # patience = 1, + predict_type = "prob", + eval_metric = msr("classif.auc") + ) + + learner$train(task) + + expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.auc")) + expect_numeric(learner$model$meta$eval_protocol$classif.auc, len = 10) + expect_list(learner$internal_valid_scores, types = "numeric") + expect_equal(names(learner$internal_valid_scores), "classif.auc") + + # multiclass task and mlr3 measure multiclass response + task = tsk("iris") + + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2, + #patience = 1, + predict_type = "prob", + eval_metric = msr("classif.ce") + ) + + learner$train(task) + + expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.ce")) + expect_numeric(learner$model$meta$eval_protocol$classif.ce, len = 10) + expect_list(learner$internal_valid_scores, types = "numeric") + expect_equal(names(learner$internal_valid_scores), "classif.ce") + + # multiclass task and mlr3 measure multiclass prob + task = tsk("iris") + + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2, + # patience = 1, + predict_type = "prob", + eval_metric = msr("classif.logloss") + ) + + learner$train(task) + + expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.logloss")) + expect_numeric(learner$model$meta$eval_protocol$classif.logloss, len = 10) + expect_list(learner$internal_valid_scores, types = "numeric") + expect_equal(names(learner$internal_valid_scores), "classif.logloss") +}) + +test_that("marshaling works for classif.python.fastai", { + learner = lrn("classif.python.fastai") + task = tsk("iris") + # expect_marshalable_learner(learner, task) + + learner$train(task) + pred = learner$predict(task) + model = learner$model + class_prev = class(model) + + # checks for marshaling is the same as expect_marshalable_learner + expect_false(learner$marshaled) + expect_equal(is_marshaled_model(learner$model), learner$marshaled) + expect_invisible(learner$marshal()) + expect_equal(mlr3::is_marshaled_model(learner$model), learner$marshaled) + + # checks for unmarshaling differs -- instead of checking equality of model, + # we check equality of predictions, because expect_equal() on python objects + # checks the pointer which almost always changes after unmarshaling + expect_invisible(learner$unmarshal()) + expect_prediction(learner$predict(task)) + expect_equal(learner$predict(task), pred) + expect_false(learner$marshaled) + expect_equal(class(learner$model), class_prev) +}) + From c1d04b00a5bee0b709254d4a07a9143d5538302e Mon Sep 17 00:00:00 2001 From: AnnaNzrv Date: Wed, 17 Sep 2025 23:14:21 +0200 Subject: [PATCH 3/7] check marshalling --- R/LearnerPythonClassif.R | 155 +++++++++++++++++++++++---------------- 1 file changed, 91 insertions(+), 64 deletions(-) diff --git a/R/LearnerPythonClassif.R b/R/LearnerPythonClassif.R index 7be659006..5e67e4b7b 100644 --- a/R/LearnerPythonClassif.R +++ b/R/LearnerPythonClassif.R @@ -1,57 +1,56 @@ # R/learner_python_classif_base.R -LearnerPythonClassif <- R6::R6Class( +LearnerPythonClassif = R6::R6Class( "LearnerPythonClassif", inherit = mlr3::LearnerClassif, public = list( initialize = function(id, - feature_types = c("logical","integer","numeric","factor","ordered"), - predict_types = c("response"), - param_set = paradox::ParamSet$new(), - properties = character(), - packages = "reticulate", # R packages - label = NA_character_, - man = NA_character_, - py_packages, # Python packages e.g. c("numpy","pandas","tabpfn") - python_version, # "3.10" - method = c("auto","virtualenv","conda"), - envname = NULL) { - - method <- match.arg(method) - - base_ps <- ps( + feature_types = c("logical","integer","numeric","factor","ordered"), + predict_types = c("response"), + param_set = paradox::ParamSet$new(), + properties = character(), + packages = "reticulate", # R packages + label = NA_character_, + man = NA_character_, + # Python requirements + py_packages, + python_version, + method = c("auto","virtualenv","conda"), + envname = NULL) { + + method = match.arg(method) + + base_ps = ps( py_env = p_uty(default = envname, tags = "python"), py_method = p_fct(levels = c("auto","virtualenv","conda"), default = method, tags = "python") ) - param_set <- ps_union(list(param_set, base_ps)) + param_set = ps_union(list(param_set, base_ps)) super$initialize( id = id, feature_types = feature_types, predict_types = predict_types, - param_set = param_set, - properties = properties, - packages = union(c("mlr3", "mlr3extralearners"), packages), # keep mlr3 + your R deps + param_set = param_set, + properties = properties, + packages = union(c("mlr3", "mlr3extralearners"), packages), label = label, - man = man + man = man ) - private$.py_packages <- py_packages - private$.py_version <- python_version - private$.py_method <- method - private$.py_env <- envname - - self$model <- NULL + private$.py_packages = py_packages + private$.py_version = python_version + private$.py_method = method + private$.py_env = envname }, py_requirements = function() { list( - packages = private$.py_packages, + packages = private$.py_packages, python_version = private$.py_version, - method = private$.py_method, - envname = private$.py_env + method = private$.py_method, + envname = private$.py_env ) }, @@ -59,7 +58,8 @@ LearnerPythonClassif <- R6::R6Class( if (!requireNamespace("reticulate", quietly = TRUE)) { stop("Package 'reticulate' required.") } - assert_python_packages(private$.py_packages, python_version = private$.py_version) + # assert/install in-session + reticulate::py_require(private$.py_packages, python_version = private$.py_version) invisible(TRUE) } ), @@ -69,34 +69,40 @@ LearnerPythonClassif <- R6::R6Class( .train = function(task) { self$ensure_deps() - x = task$data(cols = task$feature_names) - y = task$truth() + + x = task$data(cols = task$feature_names) + y = task$truth() pars = self$param_set$get_values(tags = "train") - # fit_py returns a list with entrie "model" holding the trained Python model - # and optionally "meta" holding some extra data fit = private$.fit_py(x = x, y = y, pars = pars, task = task) - self$model = structure( + meta = if (is.null(fit$meta)) list() else fit$meta + + structure( list( - fitted = fit$model, - meta = fit$meta %||% list(), + fitted = fit$model, # PyObject + meta = meta, # any R metadata (e.g., classes) py_modules = private$.py_packages, - py_version = private$.py_version + py_version = private$.py_version, + learner_class = paste0(self$id, "_model") ), - class = c(paste0(self$id, "_model"), "pybytes_model") + class = c("pybytes_model", paste0(self$id, "_model")) # pybytes first for S3 ) }, .predict = function(task) { self$ensure_deps() if (is.null(self$model)) stop("Model not trained yet.") - newdata = ordered_features(task, self) - res = private$.predict_py(model = self$model$fitted, - newdata = newdata, - predict_type = self$predict_type, - meta = self$model$meta, - class_labels = task$class_names) + + newdata = ordered_features(task, self) + res = private$.predict_py( + model = self$model$fitted, + newdata = newdata, + predict_type = self$predict_type, + meta = self$model$meta, + class_labels = task$class_names + ) + if ("prob" %in% self$predict_types && !is.null(res$prob)) { mlr3::PredictionClassif$new(task = task, prob = res$prob) } else { @@ -105,41 +111,62 @@ LearnerPythonClassif <- R6::R6Class( }, # Hooks every subclass must implement: - .fit_py = function(x, y, pars, task, ...) stop("Subclass must implement .fit_py(x, y, pars)."), - .predict_py = function(model, newdata, predict_types, meta, class_labels) + .fit_py = function(x, y, pars, task, ...) { + stop("Subclass must implement .fit_py(x, y, pars, task, ...).") + }, + + .predict_py = function(model, newdata, predict_types, meta) { stop("Subclass must implement .predict_py(model, newdata, predict_types, meta).") + } ) ) +# ---- Generic bytes-only marshaling for all python learners ---- #' @export marshal_model.pybytes_model <- function(model, inplace = FALSE, ...) { - # If you don't have assert_python_packages(), use py_require() directly: + print("marshalling python model...") reticulate::py_require(model$py_modules, python_version = model$py_version) - pickle <- reticulate::import("pickle") + raw <- as.raw(pickle$dumps(model$fitted)) - structure(list( - marshaled = raw, - py_modules = model$py_modules, - py_version = model$py_version, - meta = model$meta - ), class = c("pybytes_model_marshaled", "marshaled")) + # capture any additional classes (e.g. "classif.tabpfn_model"), keep first one + learner_class <- setdiff(class(model), "pybytes_model") + learner_class <- if (length(learner_class)) learner_class[1L] else NULL + + structure( + list( + marshaled = raw, + py_modules = model$py_modules, + py_version = model$py_version, + meta = model$meta, + learner_class = learner_class + ), + class = c("pybytes_model_marshaled", "marshaled") + ) } #' @export unmarshal_model.pybytes_model_marshaled <- function(model, inplace = FALSE, ...) { + print("unmarshalling python model...") reticulate::py_require(model$py_modules, python_version = model$py_version) - pickle <- reticulate::import("pickle") fitted <- pickle$loads(reticulate::r_to_py(model$marshaled)) - structure(list( - fitted = fitted, - meta = model$meta %||% list(), - py_modules = model$py_modules, - py_version = model$py_version - ), class = c(paste0(self$id, "_model"), "pybytes_model")) + classes <- if (is.null(model$learner_class)) { + "pybytes_model" + } else { + c("pybytes_model", model$learner_class) + } + + structure( + list( + fitted = fitted, + meta = if (is.null(model$meta)) list() else model$meta, + py_modules = model$py_modules, + py_version = model$py_version + ), + class = classes + ) } - From 28248d43da38002fc9510f991d0134ffdfcac087 Mon Sep 17 00:00:00 2001 From: AnnaNzrv Date: Thu, 18 Sep 2025 12:32:30 +0200 Subject: [PATCH 4/7] docs --- NAMESPACE | 3 + man/mlr_learners_classif.AdaBoostM1.Rd | 2 +- man/mlr_learners_classif.IBk.Rd | 2 +- man/mlr_learners_classif.J48.Rd | 2 +- man/mlr_learners_classif.JRip.Rd | 2 +- man/mlr_learners_classif.LMT.Rd | 2 +- man/mlr_learners_classif.OneR.Rd | 2 +- man/mlr_learners_classif.PART.Rd | 2 +- man/mlr_learners_classif.bayes_net.Rd | 2 +- man/mlr_learners_classif.decision_stump.Rd | 2 +- man/mlr_learners_classif.decision_table.Rd | 2 +- man/mlr_learners_classif.fastai.Rd | 176 +++++++++++++++++- man/mlr_learners_classif.kstar.Rd | 2 +- man/mlr_learners_classif.logistic.Rd | 2 +- ..._learners_classif.multilayer_perceptron.Rd | 2 +- ...earners_classif.naive_bayes_multinomial.Rd | 2 +- man/mlr_learners_classif.naive_bayes_weka.Rd | 2 +- ...mlr_learners_classif.random_forest_weka.Rd | 2 +- man/mlr_learners_classif.random_tree.Rd | 2 +- man/mlr_learners_classif.reptree.Rd | 2 +- man/mlr_learners_classif.sgd.Rd | 2 +- man/mlr_learners_classif.simple_logistic.Rd | 2 +- man/mlr_learners_classif.smo.Rd | 2 +- man/mlr_learners_classif.voted_perceptron.Rd | 2 +- man/mlr_learners_dens.mixed.Rd | 2 +- man/mlr_learners_regr.IBk.Rd | 2 +- man/mlr_learners_regr.M5Rules.Rd | 2 +- man/mlr_learners_regr.decision_stump.Rd | 2 +- man/mlr_learners_regr.decision_table.Rd | 2 +- man/mlr_learners_regr.gaussian_processes.Rd | 2 +- man/mlr_learners_regr.kstar.Rd | 2 +- man/mlr_learners_regr.linear_regression.Rd | 2 +- man/mlr_learners_regr.m5p.Rd | 2 +- ...mlr_learners_regr.multilayer_perceptron.Rd | 2 +- man/mlr_learners_regr.random_forest_weka.Rd | 2 +- man/mlr_learners_regr.random_tree.Rd | 2 +- man/mlr_learners_regr.reptree.Rd | 2 +- man/mlr_learners_regr.sgd.Rd | 2 +- ..._learners_regr.simple_linear_regression.Rd | 2 +- man/mlr_learners_regr.smo_reg.Rd | 2 +- 40 files changed, 214 insertions(+), 41 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index ecaa20876..01ec4a365 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,10 +2,12 @@ S3method(marshal_model,Weka_classifier) S3method(marshal_model,fastai_model) +S3method(marshal_model,pybytes_model) S3method(marshal_model,tabpfn_model) S3method(marshal_model,xgboost_cox_model) S3method(unmarshal_model,Weka_classifier_marshaled) S3method(unmarshal_model,fastai_model_marshaled) +S3method(unmarshal_model,pybytes_model_marshaled) S3method(unmarshal_model,tabpfn_model_marshaled) S3method(unmarshal_model,xgboost_cox_model_marshaled) export(LearnerClassifAbess) @@ -77,6 +79,7 @@ export(LearnerDensNonparametric) export(LearnerDensPenalized) export(LearnerDensPlugin) export(LearnerDensSpline) +export(LearnerPythonClassifFastai) export(LearnerRegrAbess) export(LearnerRegrBart) export(LearnerRegrBst) diff --git a/man/mlr_learners_classif.AdaBoostM1.Rd b/man/mlr_learners_classif.AdaBoostM1.Rd index 436563921..9c21d6ae6 100644 --- a/man/mlr_learners_classif.AdaBoostM1.Rd +++ b/man/mlr_learners_classif.AdaBoostM1.Rd @@ -6,7 +6,7 @@ \title{Classification AdaBoostM1 Learner} \description{ Adaptive boosting algorithm for classification. -Calls \code{\link[RWeka:Weka_classifier_meta]{RWeka::AdaBoostM1()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:AdaBoostM1]{RWeka::AdaBoostM1()}} from \CRANpkg{RWeka}. } \section{Dictionary}{ diff --git a/man/mlr_learners_classif.IBk.Rd b/man/mlr_learners_classif.IBk.Rd index 2df9fc837..c1cb7b8a8 100644 --- a/man/mlr_learners_classif.IBk.Rd +++ b/man/mlr_learners_classif.IBk.Rd @@ -6,7 +6,7 @@ \title{Classification IBk Learner} \description{ Instance based algorithm: K-nearest neighbours regression. -Calls \code{\link[RWeka:Weka_classifier_lazy]{RWeka::IBk()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:IBk]{RWeka::IBk()}} from \CRANpkg{RWeka}. } \section{Dictionary}{ diff --git a/man/mlr_learners_classif.J48.Rd b/man/mlr_learners_classif.J48.Rd index 22b9f4eda..f50222b75 100644 --- a/man/mlr_learners_classif.J48.Rd +++ b/man/mlr_learners_classif.J48.Rd @@ -6,7 +6,7 @@ \title{Classification J48 Learner} \description{ Decision tree algorithm. -Calls \code{\link[RWeka:Weka_classifier_lazy]{RWeka::IBk()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:IBk]{RWeka::IBk()}} from \CRANpkg{RWeka}. } \section{Dictionary}{ diff --git a/man/mlr_learners_classif.JRip.Rd b/man/mlr_learners_classif.JRip.Rd index 9c1e37c23..1ae781454 100644 --- a/man/mlr_learners_classif.JRip.Rd +++ b/man/mlr_learners_classif.JRip.Rd @@ -6,7 +6,7 @@ \title{Classification JRip Learner} \description{ Repeated Incremental Pruning to Produce Error Reduction. -Calls \code{\link[RWeka:Weka_classifier_rules]{RWeka::JRip()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:JRip]{RWeka::JRip()}} from \CRANpkg{RWeka}. } \section{Dictionary}{ diff --git a/man/mlr_learners_classif.LMT.Rd b/man/mlr_learners_classif.LMT.Rd index 63271434b..a89d210dd 100644 --- a/man/mlr_learners_classif.LMT.Rd +++ b/man/mlr_learners_classif.LMT.Rd @@ -6,7 +6,7 @@ \title{Classification Logistic Model Trees Learner} \description{ Classification tree with logistic regression models at the leaves. -Calls \code{\link[RWeka:Weka_classifier_trees]{RWeka::LMT()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:LMT]{RWeka::LMT()}} from \CRANpkg{RWeka}. } \section{Dictionary}{ diff --git a/man/mlr_learners_classif.OneR.Rd b/man/mlr_learners_classif.OneR.Rd index 6c67254c7..7fca76463 100644 --- a/man/mlr_learners_classif.OneR.Rd +++ b/man/mlr_learners_classif.OneR.Rd @@ -6,7 +6,7 @@ \title{Classification OneR Learner} \description{ One Rule classification algorithm that yields an extremely simple model. -Calls \code{\link[RWeka:Weka_classifier_rules]{RWeka::OneR()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:OneR]{RWeka::OneR()}} from \CRANpkg{RWeka}. } \section{Dictionary}{ diff --git a/man/mlr_learners_classif.PART.Rd b/man/mlr_learners_classif.PART.Rd index 3f1c9b031..b3a4a7dae 100644 --- a/man/mlr_learners_classif.PART.Rd +++ b/man/mlr_learners_classif.PART.Rd @@ -6,7 +6,7 @@ \title{Classification PART Learner} \description{ Regression partition tree. -Calls \code{\link[RWeka:Weka_classifier_rules]{RWeka::PART()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:PART]{RWeka::PART()}} from \CRANpkg{RWeka}. } \section{Dictionary}{ diff --git a/man/mlr_learners_classif.bayes_net.Rd b/man/mlr_learners_classif.bayes_net.Rd index 2a8009027..a8ae3eb17 100644 --- a/man/mlr_learners_classif.bayes_net.Rd +++ b/man/mlr_learners_classif.bayes_net.Rd @@ -6,7 +6,7 @@ \title{Classification Bayes Network Learner} \description{ Bayes Network learning using various search algorithms. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_classif.decision_stump.Rd b/man/mlr_learners_classif.decision_stump.Rd index fcd3d194e..f1c2bd1f4 100644 --- a/man/mlr_learners_classif.decision_stump.Rd +++ b/man/mlr_learners_classif.decision_stump.Rd @@ -6,7 +6,7 @@ \title{Classification Decision Stump Learner} \description{ Decision Stump Learner. -Calls \code{\link[RWeka:Weka_classifier_trees]{RWeka::DecisionStump()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:DecisionStump]{RWeka::DecisionStump()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_classif.decision_table.Rd b/man/mlr_learners_classif.decision_table.Rd index b6f080d54..44377a130 100644 --- a/man/mlr_learners_classif.decision_table.Rd +++ b/man/mlr_learners_classif.decision_table.Rd @@ -6,7 +6,7 @@ \title{Classification Decision Table Learner} \description{ Simple Decision Table majority classifier. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Initial parameter values}{ diff --git a/man/mlr_learners_classif.fastai.Rd b/man/mlr_learners_classif.fastai.Rd index 4d360d0ea..d6397e2a3 100644 --- a/man/mlr_learners_classif.fastai.Rd +++ b/man/mlr_learners_classif.fastai.Rd @@ -1,10 +1,15 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/learner_fastai_classif_fastai.R +% Please edit documentation in R/learner_fastai_classif_fastai.R, +% R/py_base_class_fastai.R \name{mlr_learners_classif.fastai} \alias{mlr_learners_classif.fastai} \alias{LearnerClassifFastai} +\alias{LearnerPythonClassifFastai} \title{Classification Neural Network Learner} \description{ +Simple and fast neural nets for tabular data classification. +Calls \code{\link[fastai:tabular_learner]{fastai::tabular_learner()}} from package \CRANpkg{fastai}. + Simple and fast neural nets for tabular data classification. Calls \code{\link[fastai:tabular_learner]{fastai::tabular_learner()}} from package \CRANpkg{fastai}. } @@ -27,16 +32,22 @@ After loading a marshaled \code{lrn("classif.fastai")} into R again, you then ne \itemize{ \item \code{n_epoch}: -Needs to be set for \code{\link[fastai:fit]{fastai::fit()}} to work. +Needs to be set for \code{\link[fastai:reexports]{fastai::fit()}} to work. If no value is given, it is set to 5. \item \code{eval_metric}: -Needs to be set for \code{\link[fastai:predict]{fastai::predict()}} to work. +Needs to be set for \code{\link[fastai:reexports]{fastai::predict()}} to work. If no value is given, it is set to \code{fastai::accuracy()}. } } \section{Dictionary}{ +This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{lrn()}: + +\if{html}{\out{
}}\preformatted{lrn("classif.fastai") +}\if{html}{\out{
}} + + This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{lrn()}: \if{html}{\out{
}}\preformatted{lrn("classif.fastai") @@ -45,6 +56,14 @@ This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{ \section{Meta Information}{ +\itemize{ +\item Task type: \dQuote{classif} +\item Predict Types: \dQuote{response}, \dQuote{prob} +\item Feature Types: \dQuote{logical}, \dQuote{integer}, \dQuote{numeric}, \dQuote{factor}, \dQuote{ordered} +\item Required Packages: \CRANpkg{mlr3}, \CRANpkg{mlr3extralearners}, \CRANpkg{fastai}, \CRANpkg{reticulate} +} + + \itemize{ \item Task type: \dQuote{classif} \item Predict Types: \dQuote{response}, \dQuote{prob} @@ -54,6 +73,36 @@ This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{ } \section{Parameters}{ +\tabular{lllll}{ + Id \tab Type \tab Default \tab Levels \tab Range \cr + act_cls \tab untyped \tab - \tab \tab - \cr + bn_cont \tab logical \tab TRUE \tab TRUE, FALSE \tab - \cr + bn_final \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr + drop_last \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr + embed_p \tab numeric \tab 0 \tab \tab \eqn{[0, 1]}{[0, 1]} \cr + emb_szs \tab untyped \tab NULL \tab \tab - \cr + n_epoch \tab integer \tab - \tab \tab \eqn{[1, \infty)}{[1, Inf)} \cr + eval_metric \tab untyped \tab - \tab \tab - \cr + layers \tab untyped \tab - \tab \tab - \cr + loss_func \tab untyped \tab - \tab \tab - \cr + lr \tab numeric \tab 0.001 \tab \tab \eqn{[0, \infty)}{[0, Inf)} \cr + metrics \tab untyped \tab - \tab \tab - \cr + n_out \tab integer \tab - \tab \tab \eqn{(-\infty, \infty)}{(-Inf, Inf)} \cr + num_workers \tab integer \tab 0 \tab \tab \eqn{(-\infty, \infty)}{(-Inf, Inf)} \cr + opt_func \tab untyped \tab - \tab \tab - \cr + patience \tab integer \tab 1 \tab \tab \eqn{[1, \infty)}{[1, Inf)} \cr + pin_memory \tab logical \tab TRUE \tab TRUE, FALSE \tab - \cr + procs \tab untyped \tab NULL \tab \tab - \cr + ps \tab untyped \tab NULL \tab \tab - \cr + shuffle \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr + train_bn \tab logical \tab TRUE \tab TRUE, FALSE \tab - \cr + wd \tab integer \tab - \tab \tab \eqn{[0, \infty)}{[0, Inf)} \cr + wd_bn_bias \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr + use_bn \tab logical \tab TRUE \tab TRUE, FALSE \tab - \cr + y_range \tab untyped \tab NULL \tab \tab - \cr + bs \tab integer \tab 50 \tab \tab \eqn{(-\infty, \infty)}{(-Inf, Inf)} \cr +} + \tabular{lllll}{ Id \tab Type \tab Default \tab Levels \tab Range \cr act_cls \tab untyped \tab - \tab \tab - \cr @@ -86,6 +135,11 @@ This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{ } \references{ +Howard, Jeremy, Gugger, Sylvain (2020). +\dQuote{Fastai: A Layered API for Deep Learning.} +\emph{Information}, \bold{11}(2), 108. +ISSN 2078-2489, \doi{10.3390/info11020108}. + Howard, Jeremy, Gugger, Sylvain (2020). \dQuote{Fastai: A Layered API for Deep Learning.} \emph{Information}, \bold{11}(2), 108. @@ -220,3 +274,119 @@ The objects of this class are cloneable with this method. } } } +\section{Super classes}{ +\code{\link[mlr3:Learner]{mlr3::Learner}} -> \code{\link[mlr3:LearnerClassif]{mlr3::LearnerClassif}} -> \code{mlr3extralearners::LearnerPythonClassif} -> \code{LearnerPythonClassifFastai} +} +\section{Active bindings}{ +\if{html}{\out{
}} +\describe{ +\item{\code{internal_valid_scores}}{(named \code{list()} or \code{NULL}) +The validation scores extracted from \code{eval_protocol} which itself is set by fitting the \code{fastai::tab_learner}. +If early stopping is activated, this contains the validation scores of the model for the optimal \code{n_epoch}, +otherwise the \code{n_epoch} for the final model.} + +\item{\code{internal_tuned_values}}{(named \code{list()} or \code{NULL}) +If early stopping is activated, this returns a list with \code{n_epoch}, +which is the last epoch that yielded improvement w.r.t. the \code{patience}, extracted by \code{max(eval_protocol$epoch)+1}} + +\item{\code{validate}}{(\code{numeric(1)} or \code{character(1)} or \code{NULL}) +How to construct the internal validation data. This parameter can be either \code{NULL}, +a ratio, \code{"test"}, or \code{"predefined"}.} + +\item{\code{marshaled}}{(\code{logical(1)}) +Whether the learner has been marshaled.} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-LearnerPythonClassifFastai-new}{\code{LearnerPythonClassifFastai$new()}} +\item \href{#method-LearnerPythonClassifFastai-marshal}{\code{LearnerPythonClassifFastai$marshal()}} +\item \href{#method-LearnerPythonClassifFastai-unmarshal}{\code{LearnerPythonClassifFastai$unmarshal()}} +\item \href{#method-LearnerPythonClassifFastai-clone}{\code{LearnerPythonClassifFastai$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-LearnerPythonClassifFastai-new}{}}} +\subsection{Method \code{new()}}{ +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{LearnerPythonClassifFastai$new()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-LearnerPythonClassifFastai-marshal}{}}} +\subsection{Method \code{marshal()}}{ +Marshal the learner's model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{LearnerPythonClassifFastai$marshal(...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{(any)\cr +Additional arguments passed to \code{\link[mlr3:marshaling]{mlr3::marshal_model()}}.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-LearnerPythonClassifFastai-unmarshal}{}}} +\subsection{Method \code{unmarshal()}}{ +Unmarshal the learner's model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{LearnerPythonClassifFastai$unmarshal(...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{(any)\cr +Additional arguments passed to \code{\link[mlr3:marshaling]{mlr3::marshal_model()}}.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-LearnerPythonClassifFastai-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{LearnerPythonClassifFastai$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/mlr_learners_classif.kstar.Rd b/man/mlr_learners_classif.kstar.Rd index d9b5a0ea8..e8b2ca3b4 100644 --- a/man/mlr_learners_classif.kstar.Rd +++ b/man/mlr_learners_classif.kstar.Rd @@ -7,7 +7,7 @@ \description{ Instance-based classifier which differs from other instance-based learners in that it uses an entropy-based distance function. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_classif.logistic.Rd b/man/mlr_learners_classif.logistic.Rd index ed79bd991..7c4f0374b 100644 --- a/man/mlr_learners_classif.logistic.Rd +++ b/man/mlr_learners_classif.logistic.Rd @@ -6,7 +6,7 @@ \title{Classification Logistic Regression Learner} \description{ Multinomial Logistic Regression model with a ridge estimator. -Calls \code{\link[RWeka:Weka_classifier_functions]{RWeka::Logistic()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:Logistic]{RWeka::Logistic()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_classif.multilayer_perceptron.Rd b/man/mlr_learners_classif.multilayer_perceptron.Rd index d093d1589..150760e1e 100644 --- a/man/mlr_learners_classif.multilayer_perceptron.Rd +++ b/man/mlr_learners_classif.multilayer_perceptron.Rd @@ -6,7 +6,7 @@ \title{Classification MultilayerPerceptron Learner} \description{ Classifier that uses backpropagation to learn a multi-layer perceptron. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_classif.naive_bayes_multinomial.Rd b/man/mlr_learners_classif.naive_bayes_multinomial.Rd index 9b88bf9ff..8fac3c3b3 100644 --- a/man/mlr_learners_classif.naive_bayes_multinomial.Rd +++ b/man/mlr_learners_classif.naive_bayes_multinomial.Rd @@ -6,7 +6,7 @@ \title{Classification Multinomial Naive Bayes Learner From Weka} \description{ Multinomial Naive Bayes classifier. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_classif.naive_bayes_weka.Rd b/man/mlr_learners_classif.naive_bayes_weka.Rd index 754a453e2..570824648 100644 --- a/man/mlr_learners_classif.naive_bayes_weka.Rd +++ b/man/mlr_learners_classif.naive_bayes_weka.Rd @@ -6,7 +6,7 @@ \title{Classification Naive Bayes Learner From Weka} \description{ Naive Bayes Classifier Using Estimator Classes. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_classif.random_forest_weka.Rd b/man/mlr_learners_classif.random_forest_weka.Rd index 2a681b12e..fb5bf1098 100644 --- a/man/mlr_learners_classif.random_forest_weka.Rd +++ b/man/mlr_learners_classif.random_forest_weka.Rd @@ -6,7 +6,7 @@ \title{Classification Random Forest Learner from Weka} \description{ Class for constructing a random forest. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_classif.random_tree.Rd b/man/mlr_learners_classif.random_tree.Rd index 4e1a42c77..dcff89c5a 100644 --- a/man/mlr_learners_classif.random_tree.Rd +++ b/man/mlr_learners_classif.random_tree.Rd @@ -6,7 +6,7 @@ \title{Classification Random Tree Learner} \description{ Tree that considers K randomly chosen attributes at each node. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_classif.reptree.Rd b/man/mlr_learners_classif.reptree.Rd index 57635a557..9c1e76d07 100644 --- a/man/mlr_learners_classif.reptree.Rd +++ b/man/mlr_learners_classif.reptree.Rd @@ -6,7 +6,7 @@ \title{Classification Decision Tree Learner} \description{ Fast decision tree learner. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_classif.sgd.Rd b/man/mlr_learners_classif.sgd.Rd index 0bbd5a031..d842ebd86 100644 --- a/man/mlr_learners_classif.sgd.Rd +++ b/man/mlr_learners_classif.sgd.Rd @@ -6,7 +6,7 @@ \title{Classification Stochastic Gradient Descent Learner} \description{ Stochastic Gradient Descent for learning various linear models. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Initial parameter values}{ diff --git a/man/mlr_learners_classif.simple_logistic.Rd b/man/mlr_learners_classif.simple_logistic.Rd index e974cab0e..8824e76b8 100644 --- a/man/mlr_learners_classif.simple_logistic.Rd +++ b/man/mlr_learners_classif.simple_logistic.Rd @@ -6,7 +6,7 @@ \title{Classification SimpleLogistic Learner} \description{ LogitBoost with simple regression functions as base learners. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_classif.smo.Rd b/man/mlr_learners_classif.smo.Rd index 36020ec27..828383cbd 100644 --- a/man/mlr_learners_classif.smo.Rd +++ b/man/mlr_learners_classif.smo.Rd @@ -6,7 +6,7 @@ \title{Classification Support Vector Machine Learner} \description{ Support Vector classifier trained with John Platt's sequential minimal optimization algorithm. -Calls \code{\link[RWeka:Weka_classifier_functions]{RWeka::SMO()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:SMO]{RWeka::SMO()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_classif.voted_perceptron.Rd b/man/mlr_learners_classif.voted_perceptron.Rd index 636e073e6..ab0951168 100644 --- a/man/mlr_learners_classif.voted_perceptron.Rd +++ b/man/mlr_learners_classif.voted_perceptron.Rd @@ -6,7 +6,7 @@ \title{Classification Voted Perceptron Learner} \description{ Voted Perceptron Algorithm by Freund and Schapire. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_dens.mixed.Rd b/man/mlr_learners_dens.mixed.Rd index 5f881e92b..602522bc4 100644 --- a/man/mlr_learners_dens.mixed.Rd +++ b/man/mlr_learners_dens.mixed.Rd @@ -6,7 +6,7 @@ \title{Density Mixed Data Kernel Learner} \description{ Density estimator for discrete and continuous variables. -Calls \code{\link[np:npudens]{np::npudens()}} from \CRANpkg{np}. +Calls \code{\link[np:np.density]{np::npudens()}} from \CRANpkg{np}. } \section{Dictionary}{ diff --git a/man/mlr_learners_regr.IBk.Rd b/man/mlr_learners_regr.IBk.Rd index c3b6f1e17..3b3528684 100644 --- a/man/mlr_learners_regr.IBk.Rd +++ b/man/mlr_learners_regr.IBk.Rd @@ -6,7 +6,7 @@ \title{Regression IBk Learner} \description{ Instance based algorithm: K-nearest neighbours regression. -Calls \code{\link[RWeka:Weka_classifier_lazy]{RWeka::IBk()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:IBk]{RWeka::IBk()}} from \CRANpkg{RWeka}. } \section{Dictionary}{ diff --git a/man/mlr_learners_regr.M5Rules.Rd b/man/mlr_learners_regr.M5Rules.Rd index 7d31829b6..018f24c1a 100644 --- a/man/mlr_learners_regr.M5Rules.Rd +++ b/man/mlr_learners_regr.M5Rules.Rd @@ -7,7 +7,7 @@ \description{ Algorithm for inducing \href{https://en.wikipedia.org/wiki/Decision_list}{decision lists} from model trees. -Calls \code{\link[RWeka:Weka_classifier_rules]{RWeka::M5Rules()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:M5Rules]{RWeka::M5Rules()}} from \CRANpkg{RWeka}. } \section{Dictionary}{ diff --git a/man/mlr_learners_regr.decision_stump.Rd b/man/mlr_learners_regr.decision_stump.Rd index 3d34434ca..3529724a7 100644 --- a/man/mlr_learners_regr.decision_stump.Rd +++ b/man/mlr_learners_regr.decision_stump.Rd @@ -6,7 +6,7 @@ \title{Regression Decision Stump Learner} \description{ Decision Stump Learner. -Calls \code{\link[RWeka:Weka_classifier_trees]{RWeka::DecisionStump()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:DecisionStump]{RWeka::DecisionStump()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_regr.decision_table.Rd b/man/mlr_learners_regr.decision_table.Rd index 419663863..f7ba120f8 100644 --- a/man/mlr_learners_regr.decision_table.Rd +++ b/man/mlr_learners_regr.decision_table.Rd @@ -6,7 +6,7 @@ \title{Regression Decision Table Learner} \description{ Simple Decision Table majority regressor. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Initial parameter values}{ diff --git a/man/mlr_learners_regr.gaussian_processes.Rd b/man/mlr_learners_regr.gaussian_processes.Rd index 464881d68..e757df07a 100644 --- a/man/mlr_learners_regr.gaussian_processes.Rd +++ b/man/mlr_learners_regr.gaussian_processes.Rd @@ -6,7 +6,7 @@ \title{Regression Gaussian Processes Learner From Weka} \description{ Gaussian Processes. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_regr.kstar.Rd b/man/mlr_learners_regr.kstar.Rd index 1858f42bd..22ac64a75 100644 --- a/man/mlr_learners_regr.kstar.Rd +++ b/man/mlr_learners_regr.kstar.Rd @@ -7,7 +7,7 @@ \description{ Instance-based regressor which differs from other instance-based learners in that it uses an entropy-based distance function. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_regr.linear_regression.Rd b/man/mlr_learners_regr.linear_regression.Rd index 3e87a5c43..0d6521b6d 100644 --- a/man/mlr_learners_regr.linear_regression.Rd +++ b/man/mlr_learners_regr.linear_regression.Rd @@ -7,7 +7,7 @@ \description{ Linear Regression learner that uses the Akaike criterion for model selection and is able to deal with weighted instances. -Calls \code{\link[RWeka:Weka_classifier_functions]{RWeka::LinearRegression()}} \CRANpkg{RWeka}. +Calls \code{\link[RWeka:LinearRegression]{RWeka::LinearRegression()}} \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_regr.m5p.Rd b/man/mlr_learners_regr.m5p.Rd index 10bedde33..6b7a6a337 100644 --- a/man/mlr_learners_regr.m5p.Rd +++ b/man/mlr_learners_regr.m5p.Rd @@ -6,7 +6,7 @@ \title{Regression M5P Learner} \description{ Implements base routines for generating M5 Model trees and rules. -Calls \code{\link[RWeka:Weka_classifier_trees]{RWeka::M5P()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:M5P]{RWeka::M5P()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_regr.multilayer_perceptron.Rd b/man/mlr_learners_regr.multilayer_perceptron.Rd index 373df0938..2f1a01110 100644 --- a/man/mlr_learners_regr.multilayer_perceptron.Rd +++ b/man/mlr_learners_regr.multilayer_perceptron.Rd @@ -6,7 +6,7 @@ \title{Regression MultilayerPerceptron Learner} \description{ Regressor that uses backpropagation to learn a multi-layer perceptron. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_regr.random_forest_weka.Rd b/man/mlr_learners_regr.random_forest_weka.Rd index 316d5829c..74f54a283 100644 --- a/man/mlr_learners_regr.random_forest_weka.Rd +++ b/man/mlr_learners_regr.random_forest_weka.Rd @@ -6,7 +6,7 @@ \title{Regression Random Forest Learner from Weka} \description{ Class for constructing a forest of random trees. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_regr.random_tree.Rd b/man/mlr_learners_regr.random_tree.Rd index b9d19db6b..9931706f3 100644 --- a/man/mlr_learners_regr.random_tree.Rd +++ b/man/mlr_learners_regr.random_tree.Rd @@ -6,7 +6,7 @@ \title{Regression Random Tree Learner} \description{ Tree that considers K randomly chosen attributes at each node. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_regr.reptree.Rd b/man/mlr_learners_regr.reptree.Rd index 77091e75c..8e82a34e3 100644 --- a/man/mlr_learners_regr.reptree.Rd +++ b/man/mlr_learners_regr.reptree.Rd @@ -6,7 +6,7 @@ \title{Regression Decision Tree Learner} \description{ Fast decision tree learner. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_regr.sgd.Rd b/man/mlr_learners_regr.sgd.Rd index 687bed450..1f07f126e 100644 --- a/man/mlr_learners_regr.sgd.Rd +++ b/man/mlr_learners_regr.sgd.Rd @@ -6,7 +6,7 @@ \title{Regression Stochastic Gradient Descent Learner} \description{ Stochastic Gradient Descent for learning various linear models. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Initial parameter values}{ diff --git a/man/mlr_learners_regr.simple_linear_regression.Rd b/man/mlr_learners_regr.simple_linear_regression.Rd index f80815e52..edda2d15b 100644 --- a/man/mlr_learners_regr.simple_linear_regression.Rd +++ b/man/mlr_learners_regr.simple_linear_regression.Rd @@ -6,7 +6,7 @@ \title{Regression SimpleLinearRegression Learner from Weka} \description{ Simple linear regression model that picks the attribute that results in the lowest squared error. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ diff --git a/man/mlr_learners_regr.smo_reg.Rd b/man/mlr_learners_regr.smo_reg.Rd index 80a7546c3..18662e7f1 100644 --- a/man/mlr_learners_regr.smo_reg.Rd +++ b/man/mlr_learners_regr.smo_reg.Rd @@ -6,7 +6,7 @@ \title{Regression Support Vector Machine Learner} \description{ Support Vector Machine for regression. -Calls \code{\link[RWeka:Weka_interfaces]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. +Calls \code{\link[RWeka:make_Weka_classifier]{RWeka::make_Weka_classifier()}} from \CRANpkg{RWeka}. } \section{Custom mlr3 parameters}{ From 7b40dd6dd1e9cb44f487f509c8985837a4473433 Mon Sep 17 00:00:00 2001 From: AnnaNzrv Date: Thu, 18 Sep 2025 15:06:53 +0200 Subject: [PATCH 5/7] make marshalling work for py learners --- R/LearnerPythonClassif.R | 10 ++-------- attic/demo_python_learners.R | 18 +++++++++++------- man/mlr_learners_classif.C50.Rd | 2 +- man/mlr_learners_classif.abess.Rd | 2 +- man/mlr_learners_classif.fastai.Rd | 4 ++-- man/mlr_learners_regr.abess.Rd | 2 +- man/mlr_learners_regr.cubist.Rd | 2 +- man/mlr_learners_surv.bart.Rd | 2 +- man/mlr_learners_surv.blackboost.Rd | 2 +- man/mlr_learners_surv.cv_glmnet.Rd | 2 +- man/mlr_learners_surv.gamboost.Rd | 4 ++-- man/mlr_learners_surv.glmboost.Rd | 4 ++-- man/mlr_learners_surv.glmnet.Rd | 4 ++-- man/mlr_learners_surv.mboost.Rd | 6 +++--- mlr3extralearners.Rproj | 1 - 15 files changed, 31 insertions(+), 34 deletions(-) diff --git a/R/LearnerPythonClassif.R b/R/LearnerPythonClassif.R index 5e67e4b7b..30952f1d7 100644 --- a/R/LearnerPythonClassif.R +++ b/R/LearnerPythonClassif.R @@ -7,7 +7,7 @@ LearnerPythonClassif = R6::R6Class( public = list( initialize = function(id, feature_types = c("logical","integer","numeric","factor","ordered"), - predict_types = c("response"), + predict_types = c("response", "prob"), param_set = paradox::ParamSet$new(), properties = character(), packages = "reticulate", # R packages @@ -125,7 +125,6 @@ LearnerPythonClassif = R6::R6Class( #' @export marshal_model.pybytes_model <- function(model, inplace = FALSE, ...) { - print("marshalling python model...") reticulate::py_require(model$py_modules, python_version = model$py_version) pickle <- reticulate::import("pickle") @@ -138,8 +137,6 @@ marshal_model.pybytes_model <- function(model, inplace = FALSE, ...) { structure( list( marshaled = raw, - py_modules = model$py_modules, - py_version = model$py_version, meta = model$meta, learner_class = learner_class ), @@ -149,7 +146,6 @@ marshal_model.pybytes_model <- function(model, inplace = FALSE, ...) { #' @export unmarshal_model.pybytes_model_marshaled <- function(model, inplace = FALSE, ...) { - print("unmarshalling python model...") reticulate::py_require(model$py_modules, python_version = model$py_version) pickle <- reticulate::import("pickle") fitted <- pickle$loads(reticulate::r_to_py(model$marshaled)) @@ -163,9 +159,7 @@ unmarshal_model.pybytes_model_marshaled <- function(model, inplace = FALSE, ...) structure( list( fitted = fitted, - meta = if (is.null(model$meta)) list() else model$meta, - py_modules = model$py_modules, - py_version = model$py_version + meta = if (is.null(model$meta)) list() else model$meta ), class = classes ) diff --git a/attic/demo_python_learners.R b/attic/demo_python_learners.R index 5c2fe393d..a915a7439 100644 --- a/attic/demo_python_learners.R +++ b/attic/demo_python_learners.R @@ -9,13 +9,6 @@ test_that("autotest", { expect_true(result, info = result$error) }) -learner = lrn("classif.python.fastai", layers = c(200, 100)) -#learner$predict_type = "prob" -tasks = generate_tasks(learner) -task = tasks[[1L]] -learner$train(task) -p = learner$predict(task) - test_that("eval protocol", { learner = lrn("classif.python.fastai") @@ -223,3 +216,14 @@ test_that("marshaling works for classif.python.fastai", { expect_equal(class(learner$model), class_prev) }) +test_that("encapsulation works for classif.python.fastai", { + learner = lrn("classif.python.fastai") + task = tsk("irirs") + + learner$train(task) + learner_encapsulated = learner$clone(deep = TRUE) + learner_encapsulated$encapsulate("mirai", default_fallback(learner_encapsulated)) + + rr = resample(task, learner_encapsulated, rsmp("holdout"), store_models = TRUE) + log = rr$learners[[1]]$state$log +}) diff --git a/man/mlr_learners_classif.C50.Rd b/man/mlr_learners_classif.C50.Rd index 4b8c96ab7..f0b480478 100644 --- a/man/mlr_learners_classif.C50.Rd +++ b/man/mlr_learners_classif.C50.Rd @@ -6,7 +6,7 @@ \title{Classification C5.0 Learner} \description{ Decision Tree Algorithm. -Calls \code{\link[C50:C5.0]{C50::C5.0.formula()}} from \CRANpkg{C50}. +Calls \code{\link[C50:C5.0.formula]{C50::C5.0.formula()}} from \CRANpkg{C50}. } \section{Dictionary}{ diff --git a/man/mlr_learners_classif.abess.Rd b/man/mlr_learners_classif.abess.Rd index ef6e14bc9..ba0799b1a 100644 --- a/man/mlr_learners_classif.abess.Rd +++ b/man/mlr_learners_classif.abess.Rd @@ -148,7 +148,7 @@ Creates a new instance of this \link[R6:R6Class]{R6} class. \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LearnerClassifAbess-selected_features}{}}} \subsection{Method \code{selected_features()}}{ -Extract the name of selected features from the model by \code{\link[abess:extract.abess]{abess::extract()}}. +Extract the name of selected features from the model by \code{\link[abess:extract]{abess::extract()}}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LearnerClassifAbess$selected_features()}\if{html}{\out{
}} } diff --git a/man/mlr_learners_classif.fastai.Rd b/man/mlr_learners_classif.fastai.Rd index d6397e2a3..5c14520f6 100644 --- a/man/mlr_learners_classif.fastai.Rd +++ b/man/mlr_learners_classif.fastai.Rd @@ -81,7 +81,7 @@ This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{ drop_last \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr embed_p \tab numeric \tab 0 \tab \tab \eqn{[0, 1]}{[0, 1]} \cr emb_szs \tab untyped \tab NULL \tab \tab - \cr - n_epoch \tab integer \tab - \tab \tab \eqn{[1, \infty)}{[1, Inf)} \cr + n_epoch \tab integer \tab 5 \tab \tab \eqn{[1, \infty)}{[1, Inf)} \cr eval_metric \tab untyped \tab - \tab \tab - \cr layers \tab untyped \tab - \tab \tab - \cr loss_func \tab untyped \tab - \tab \tab - \cr @@ -111,7 +111,7 @@ This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{ drop_last \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr embed_p \tab numeric \tab 0 \tab \tab \eqn{[0, 1]}{[0, 1]} \cr emb_szs \tab untyped \tab NULL \tab \tab - \cr - n_epoch \tab integer \tab - \tab \tab \eqn{[1, \infty)}{[1, Inf)} \cr + n_epoch \tab integer \tab 5 \tab \tab \eqn{[1, \infty)}{[1, Inf)} \cr eval_metric \tab untyped \tab - \tab \tab - \cr layers \tab untyped \tab - \tab \tab - \cr loss_func \tab untyped \tab - \tab \tab - \cr diff --git a/man/mlr_learners_regr.abess.Rd b/man/mlr_learners_regr.abess.Rd index 5324b969b..f7705a1de 100644 --- a/man/mlr_learners_regr.abess.Rd +++ b/man/mlr_learners_regr.abess.Rd @@ -148,7 +148,7 @@ Creates a new instance of this \link[R6:R6Class]{R6} class. \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LearnerRegrAbess-selected_features}{}}} \subsection{Method \code{selected_features()}}{ -Extract the name of selected features from the model by \code{\link[abess:extract.abess]{abess::extract()}}. +Extract the name of selected features from the model by \code{\link[abess:extract]{abess::extract()}}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LearnerRegrAbess$selected_features()}\if{html}{\out{
}} } diff --git a/man/mlr_learners_regr.cubist.Rd b/man/mlr_learners_regr.cubist.Rd index daa1a1171..60f42ea0d 100644 --- a/man/mlr_learners_regr.cubist.Rd +++ b/man/mlr_learners_regr.cubist.Rd @@ -7,7 +7,7 @@ \description{ Rule-based model that is an extension of Quinlan's M5 model tree. Each tree contains linear regression models at the terminal leaves. -Calls \code{\link[Cubist:cubist.default]{Cubist::cubist()}} from \CRANpkg{Cubist}. +Calls \code{\link[Cubist:cubist]{Cubist::cubist()}} from \CRANpkg{Cubist}. } \section{Dictionary}{ diff --git a/man/mlr_learners_surv.bart.Rd b/man/mlr_learners_surv.bart.Rd index 3185d2d41..5e3493f85 100644 --- a/man/mlr_learners_surv.bart.Rd +++ b/man/mlr_learners_surv.bart.Rd @@ -6,7 +6,7 @@ \title{Survival Bayesian Additive Regression Trees Learner} \description{ Fits a Bayesian Additive Regression Trees (BART) learner to right-censored -survival data. Calls \code{\link[BART:surv.bart]{BART::mc.surv.bart()}} from \CRANpkg{BART}. +survival data. Calls \code{\link[BART:mc.surv.bart]{BART::mc.surv.bart()}} from \CRANpkg{BART}. } \section{Prediction types}{ diff --git a/man/mlr_learners_surv.blackboost.Rd b/man/mlr_learners_surv.blackboost.Rd index a6b6f3d95..45a2f2ca7 100644 --- a/man/mlr_learners_surv.blackboost.Rd +++ b/man/mlr_learners_surv.blackboost.Rd @@ -17,7 +17,7 @@ This learner returns two to three prediction types: \enumerate{ \item \code{lp}: a vector containing the linear predictors (relative risk scores), where each score corresponds to a specific test observation. -Calculated using \code{\link[mboost:methods]{mboost::predict.blackboost()}}. +Calculated using \code{\link[mboost:predict.blackboost]{mboost::predict.blackboost()}}. If the \code{family} parameter is not \code{"coxph"}, \code{-lp} is returned, since non-coxph families represent AFT-style distributions where lower \code{lp} values indicate higher risk. \item \code{crank}: same as \code{lp}. diff --git a/man/mlr_learners_surv.cv_glmnet.Rd b/man/mlr_learners_surv.cv_glmnet.Rd index a31f80b0c..70e45450f 100644 --- a/man/mlr_learners_surv.cv_glmnet.Rd +++ b/man/mlr_learners_surv.cv_glmnet.Rd @@ -105,7 +105,7 @@ This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{ If a \code{Task} contains a column with the \code{offset} role, it is automatically incorporated during training via the \code{offset} argument in \code{\link[glmnet:glmnet]{glmnet::glmnet()}}. During prediction, the offset column from the test set is used only if -\code{use_pred_offset = TRUE} (default), passed via the \code{newoffset} argument in \code{\link[glmnet:predict.glmnet]{glmnet::predict.coxnet()}}. +\code{use_pred_offset = TRUE} (default), passed via the \code{newoffset} argument in \code{\link[glmnet:predict.coxnet]{glmnet::predict.coxnet()}}. Otherwise, if the user sets \code{use_pred_offset = FALSE}, a zero offset is applied, effectively disabling the offset adjustment during prediction. } diff --git a/man/mlr_learners_surv.gamboost.Rd b/man/mlr_learners_surv.gamboost.Rd index ec01719c8..db49a77ee 100644 --- a/man/mlr_learners_surv.gamboost.Rd +++ b/man/mlr_learners_surv.gamboost.Rd @@ -53,7 +53,7 @@ This learner returns two to three prediction types: \enumerate{ \item \code{lp}: a vector containing the linear predictors (relative risk scores), where each score corresponds to a specific test observation. -Calculated using \code{\link[mboost:methods]{mboost::predict.gamboost()}}. +Calculated using \code{\link[mboost:predict.gamboost]{mboost::predict.gamboost()}}. If the \code{family} parameter is not \code{"coxph"}, \code{-lp} is returned, since non-coxph families represent AFT-style distributions where lower \code{lp} values indicate higher risk. \item \code{crank}: same as \code{lp}. @@ -152,7 +152,7 @@ Named \code{numeric()}. \if{latex}{\out{\hypertarget{method-LearnerSurvGAMBoost-selected_features}{}}} \subsection{Method \code{selected_features()}}{ Selected features are extracted with the function -\code{\link[mboost:methods]{mboost::variable.names.mboost()}}, with +\code{\link[mboost:variable.names.mboost]{mboost::variable.names.mboost()}}, with \code{used.only = TRUE}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LearnerSurvGAMBoost$selected_features()}\if{html}{\out{
}} diff --git a/man/mlr_learners_surv.glmboost.Rd b/man/mlr_learners_surv.glmboost.Rd index f55e2348f..3d5ca5df8 100644 --- a/man/mlr_learners_surv.glmboost.Rd +++ b/man/mlr_learners_surv.glmboost.Rd @@ -52,7 +52,7 @@ This learner returns two to three prediction types: \enumerate{ \item \code{lp}: a vector containing the linear predictors (relative risk scores), where each score corresponds to a specific test observation. -Calculated using \code{\link[mboost:methods]{mboost::predict.glmboost()}}. +Calculated using \code{\link[mboost:predict.glmboost]{mboost::predict.glmboost()}}. If the \code{family} parameter is not \code{"coxph"}, \code{-lp} is returned, since non-coxph families represent AFT-style distributions where lower \code{lp} values indicate higher risk. \item \code{crank}: same as \code{lp}. @@ -178,7 +178,7 @@ Named \code{numeric()}. \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LearnerSurvGLMBoost-selected_features}{}}} \subsection{Method \code{selected_features()}}{ -Selected features are extracted with the function \code{\link[mboost:methods]{mboost::coef.glmboost()}} +Selected features are extracted with the function \code{\link[mboost:coef.glmboost]{mboost::coef.glmboost()}} which by default returns features with non-zero coefficients and for the final number of boosting iterations. diff --git a/man/mlr_learners_surv.glmnet.Rd b/man/mlr_learners_surv.glmnet.Rd index d408cf6e0..2c6d58644 100644 --- a/man/mlr_learners_surv.glmnet.Rd +++ b/man/mlr_learners_surv.glmnet.Rd @@ -21,7 +21,7 @@ This learner returns three prediction types: \enumerate{ \item \code{lp}: a vector containing the linear predictors (relative risk scores), where each score corresponds to a specific test observation. -Calculated using \code{\link[glmnet:predict.glmnet]{glmnet::predict.coxnet()}}. +Calculated using \code{\link[glmnet:predict.coxnet]{glmnet::predict.coxnet()}}. \item \code{crank}: same as \code{lp}. \item \code{distr}: a survival matrix in two dimensions, where observations are represented in rows and time points in columns. @@ -53,7 +53,7 @@ custom resampling strategies (blocked design, stratification). If a \code{Task} contains a column with the \code{offset} role, it is automatically incorporated during training via the \code{offset} argument in \code{\link[glmnet:glmnet]{glmnet::glmnet()}}. During prediction, the offset column from the test set is used only if -\code{use_pred_offset = TRUE} (default), passed via the \code{newoffset} argument in \code{\link[glmnet:predict.glmnet]{glmnet::predict.coxnet()}}. +\code{use_pred_offset = TRUE} (default), passed via the \code{newoffset} argument in \code{\link[glmnet:predict.coxnet]{glmnet::predict.coxnet()}}. Otherwise, if the user sets \code{use_pred_offset = FALSE}, a zero offset is applied, effectively disabling the offset adjustment during prediction. } diff --git a/man/mlr_learners_surv.mboost.Rd b/man/mlr_learners_surv.mboost.Rd index dbf5b606f..d7c99f33d 100644 --- a/man/mlr_learners_surv.mboost.Rd +++ b/man/mlr_learners_surv.mboost.Rd @@ -6,7 +6,7 @@ \title{Boosted Generalized Additive Survival Learner} \description{ Model-based boosting for survival analysis. -Calls \code{\link[mboost:gamboost]{mboost::mboost()}} from \CRANpkg{mboost}. +Calls \code{\link[mboost:mboost]{mboost::mboost()}} from \CRANpkg{mboost}. } \details{ \code{distr} prediction made by \code{\link[mboost:survFit]{mboost::survFit()}}. @@ -17,7 +17,7 @@ This learner returns two to three prediction types: \enumerate{ \item \code{lp}: a vector containing the linear predictors (relative risk scores), where each score corresponds to a specific test observation. -Calculated using \code{\link[mboost:methods]{mboost::predict.mboost()}}. +Calculated using \code{\link[mboost:predict.mboost]{mboost::predict.mboost()}}. If the \code{family} parameter is not \code{"coxph"}, \code{-lp} is returned, since non-coxph families represent AFT-style distributions where lower \code{lp} values indicate higher risk. \item \code{crank}: same as \code{lp}. @@ -153,7 +153,7 @@ Named \code{numeric()}. \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LearnerSurvMBoost-selected_features}{}}} \subsection{Method \code{selected_features()}}{ -Selected features are extracted with the function \code{\link[mboost:methods]{mboost::variable.names.mboost()}}, with +Selected features are extracted with the function \code{\link[mboost:variable.names.mboost]{mboost::variable.names.mboost()}}, with \code{used.only = TRUE}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LearnerSurvMBoost$selected_features()}\if{html}{\out{
}} diff --git a/mlr3extralearners.Rproj b/mlr3extralearners.Rproj index 632fd57da..1440aa06d 100644 --- a/mlr3extralearners.Rproj +++ b/mlr3extralearners.Rproj @@ -1,5 +1,4 @@ Version: 1.0 -ProjectId: 518a23f9-9f72-485b-aa55-7970cd1d9a1a RestoreWorkspace: Default SaveWorkspace: Default From b4207d15923bd297ba0d6061a44ef65e6d00e7a0 Mon Sep 17 00:00:00 2001 From: AnnaNzrv Date: Wed, 1 Oct 2025 18:01:10 +0200 Subject: [PATCH 6/7] refactor python base learner --- R/LearnerPythonClassif.R | 128 +++---- R/configspace_to_paramset.R | 1 + R/learner_fastai_classif_fastai.R | 548 +++++++++++++++--------------- R/py_base_class_fastai.R | 35 +- R/py_base_class_tabpfn.R | 105 +++--- attic/demo_python_learners.R | 469 +++++++++++++------------ 6 files changed, 652 insertions(+), 634 deletions(-) create mode 100644 R/configspace_to_paramset.R diff --git a/R/LearnerPythonClassif.R b/R/LearnerPythonClassif.R index 30952f1d7..2a67ab9d8 100644 --- a/R/LearnerPythonClassif.R +++ b/R/LearnerPythonClassif.R @@ -1,5 +1,3 @@ -# R/learner_python_classif_base.R - LearnerPythonClassif = R6::R6Class( "LearnerPythonClassif", inherit = mlr3::LearnerClassif, @@ -8,25 +6,14 @@ LearnerPythonClassif = R6::R6Class( initialize = function(id, feature_types = c("logical","integer","numeric","factor","ordered"), predict_types = c("response", "prob"), - param_set = paradox::ParamSet$new(), + param_set = ps(), properties = character(), - packages = "reticulate", # R packages + packages = "reticulate", label = NA_character_, man = NA_character_, # Python requirements py_packages, - python_version, - method = c("auto","virtualenv","conda"), - envname = NULL) { - - method = match.arg(method) - - base_ps = ps( - py_env = p_uty(default = envname, tags = "python"), - py_method = p_fct(levels = c("auto","virtualenv","conda"), - default = method, tags = "python") - ) - param_set = ps_union(list(param_set, base_ps)) + python_version) { super$initialize( id = id, @@ -38,80 +25,50 @@ LearnerPythonClassif = R6::R6Class( label = label, man = man ) - private$.py_packages = py_packages private$.py_version = python_version - private$.py_method = method - private$.py_env = envname }, - py_requirements = function() { + + py_requirements = function(rhs) { + assert_ro_binding(rhs) list( packages = private$.py_packages, - python_version = private$.py_version, - method = private$.py_method, - envname = private$.py_env + python_version = private$.py_version ) - }, - - ensure_deps = function() { - if (!requireNamespace("reticulate", quietly = TRUE)) { - stop("Package 'reticulate' required.") - } - # assert/install in-session - reticulate::py_require(private$.py_packages, python_version = private$.py_version) - invisible(TRUE) } ), private = list( - .py_packages = NULL, .py_version = NULL, .py_method = NULL, .py_env = NULL, + .py_packages = NULL, .py_version = NULL, .train = function(task) { - self$ensure_deps() + py_requirements = self$py_requirements() + do.call(assert_python_packages, py_requirements) - x = task$data(cols = task$feature_names) - y = task$truth() - pars = self$param_set$get_values(tags = "train") + out = named_list() - fit = private$.fit_py(x = x, y = y, pars = pars, task = task) - - meta = if (is.null(fit$meta)) list() else fit$meta + fit = private$.train_py(task = task) + assert_list(fit, all.missing = FALSE, min.len = 1) + assert_names(names(fit), must.include = "model") structure( - list( - fitted = fit$model, # PyObject - meta = meta, # any R metadata (e.g., classes) - py_modules = private$.py_packages, - py_version = private$.py_version, - learner_class = paste0(self$id, "_model") - ), - class = c("pybytes_model", paste0(self$id, "_model")) # pybytes first for S3 + mlr3misc::insert_named(out, fit), + class = c("pybytes_model", paste0(self$id, "_model")) ) }, .predict = function(task) { - self$ensure_deps() - if (is.null(self$model)) stop("Model not trained yet.") + py_requirements = self$py_requirements() + do.call(assert_python_packages, py_requirements) newdata = ordered_features(task, self) - res = private$.predict_py( - model = self$model$fitted, - newdata = newdata, - predict_type = self$predict_type, - meta = self$model$meta, - class_labels = task$class_names - ) - - if ("prob" %in% self$predict_types && !is.null(res$prob)) { - mlr3::PredictionClassif$new(task = task, prob = res$prob) - } else { - mlr3::PredictionClassif$new(task = task, response = res$response) - } + preds = private$.predict_py(task = task, newdata = newdata, predict_types = self$predict_types) + preds }, - # Hooks every subclass must implement: - .fit_py = function(x, y, pars, task, ...) { + # ---- subclass hooks ---- + .train_py = function(x, y, pars, task, ...) { stop("Subclass must implement .fit_py(x, y, pars, task, ...).") }, @@ -126,26 +83,35 @@ LearnerPythonClassif = R6::R6Class( #' @export marshal_model.pybytes_model <- function(model, inplace = FALSE, ...) { reticulate::py_require(model$py_modules, python_version = model$py_version) - pickle <- reticulate::import("pickle") + pickle = reticulate::import("pickle") - raw <- as.raw(pickle$dumps(model$fitted)) + raw = as.raw(pickle$dumps(model$model)) - # capture any additional classes (e.g. "classif.tabpfn_model"), keep first one - learner_class <- setdiff(class(model), "pybytes_model") - learner_class <- if (length(learner_class)) learner_class[1L] else NULL + learner_class = setdiff(class(model), "pybytes_model") + if (length(learner_class) > 1L) { + stop(sprintf( + "Expected at most one learner-specific class; got: %s", + paste(learner_class, collapse = ", ") + )) + } + + raw_model = list( + marshaled = raw, + learner_class = learner_class, + py_modules = model$py_modules, + py_version = model$py_version, + ) + meta_data = setdiff(model, "model") structure( - list( - marshaled = raw, - meta = model$meta, - learner_class = learner_class - ), + mlr3misc::insert_named(raw_model, meta_data), class = c("pybytes_model_marshaled", "marshaled") ) } #' @export unmarshal_model.pybytes_model_marshaled <- function(model, inplace = FALSE, ...) { + # use python requirements stored in the marshaled object reticulate::py_require(model$py_modules, python_version = model$py_version) pickle <- reticulate::import("pickle") fitted <- pickle$loads(reticulate::r_to_py(model$marshaled)) @@ -155,12 +121,16 @@ unmarshal_model.pybytes_model_marshaled <- function(model, inplace = FALSE, ...) } else { c("pybytes_model", model$learner_class) } + meta_data = setdiff(model, "marshaled") + out = list( + model = fitted, + learner_class = learner_class, + py_modules = model$py_modules, + py_version = model$py_version, + ) structure( - list( - fitted = fitted, - meta = if (is.null(model$meta)) list() else model$meta - ), + mlr3misc::insert_named(out, meta_data), class = classes ) } diff --git a/R/configspace_to_paramset.R b/R/configspace_to_paramset.R new file mode 100644 index 000000000..9321beb9e --- /dev/null +++ b/R/configspace_to_paramset.R @@ -0,0 +1 @@ +CS = reticulate::import("ConfigSpace", delay_load = TRUE) diff --git a/R/learner_fastai_classif_fastai.R b/R/learner_fastai_classif_fastai.R index ee1eff193..c64b889f3 100644 --- a/R/learner_fastai_classif_fastai.R +++ b/R/learner_fastai_classif_fastai.R @@ -37,285 +37,285 @@ LearnerClassifFastai = R6Class("LearnerClassifFastai", inherit = LearnerClassif, public = list( - #' @description - #' Creates a new instance of this [R6][R6::R6Class] class. - initialize = function() { - - p_n_epoch = p_int(1L, - tags = c("train", "hotstart", "internal_tuning"), - default = 5L, - aggr = crate(function(x) as.integer(ceiling(mean(unlist(x)))), .parent = topenv()), - in_tune_fn = crate(function(domain, param_vals) { - if (is.null(param_vals$patience)) { - stop("Parameter 'patience' must be set to use internal tuning.") - } - assert_integerish(domain$upper, len = 1L, any.missing = FALSE) - }, .parent = topenv()), - disable_in_tune = list(patience = NULL) - ) - - param_set = ps( - act_cls = p_uty(tags = "train"), # Activation type for LinBnDrop layers, e.g., fastai::nn()$ReLU(inplace = TRUE) - bn_cont = p_lgl(default = TRUE, tags = "train"), # Use BatchNorm1d on continuous variables - bn_final = p_lgl(default = FALSE, tags = "train"), # Use BatchNorm1d on final layer - drop_last = p_lgl(default = FALSE, tags = "train"), # If True, then the last incomplete batch is dropped. - embed_p = p_dbl(lower = 0L, upper = 1L, default = 0L, tags = "train"), # Dropout probability for Embedding layer - emb_szs = p_uty(default = NULL, tags = "train"), # Sequence of (num_embeddings, embedding_dim) for each categorical variable - n_epoch = p_n_epoch, - eval_metric = p_uty(tags = "train", custom_check = crate({ - function(x) check_true(is.function(x) || inherits(x, "Measure")) - })), - layers = p_uty(tags = "train"), # Sequence of ints used to specify the input and output size of each LinBnDrop layer - loss_func = p_uty(tags = "train"), # Defaults to fastai::CrossEntropyLossFlat() - lr = p_dbl(lower = 0, default = 0.001, tags = "train"), # Learning rate - metrics = p_uty(tags = "train"), # optional list of metrics, e.g, fastai::Precision() or fastai::accuracy() - n_out = p_int(tags = "train"), # ? - num_workers = p_int(default = 0L, tags = "train"), # how many subprocesses to use for data loading - opt_func = p_uty(tags = "train"), # Optimizer created when Learner.fit is called. E.g., fastai::Adam() - patience = p_int(1L, default = 1, tags = "train"), # number of epochs to wait when training has not improved model. add `depends = quote(early_stopping == TRUE`)` - pin_memory = p_lgl(default = TRUE, tags = "train"), # If True, the data loader will copy Tensors into CUDA pinned memory before returning them. - procs = p_uty(default = NULL, tags = "train"), # fastai preprocessing steps such as fastai::Categorify, fastai::Normalize, fastai::fill_missing - ps = p_uty(default = NULL, tags = "train"), # Sequence of dropout probabilities - shuffle = p_lgl(default = FALSE, tags = "train"), # If True, then data is shuffled every time dataloader is fully read/iterated. - train_bn = p_lgl(default = TRUE, tags = "train"), # controls if BatchNorm layers are trained - wd = p_int(lower = 0, tags = "train"), # weight decay used for optimization - wd_bn_bias = p_lgl(default = FALSE, tags = "train"), # controls if weight decay is applied to BatchNorm layers and bias - use_bn = p_lgl(default = TRUE, tags = "train"), # Use BatchNorm1d in LinBnDrop layers - y_range = p_uty(default = NULL, tags = "train"), # Low and high for SigmoidRange activation (see below) - bs = p_int(default = 50, tags = "train") # how many samples per batch to load - ) - - param_set$set_values(n_epoch = 5L) - - super$initialize( - id = "classif.fastai", - packages = c("mlr3extralearners", "fastai", "reticulate"), - feature_types = c("logical", "integer", "numeric", "factor", "ordered"), - predict_types = c("response", "prob"), - param_set = param_set, - properties = c("multiclass", "twoclass", "weights", "validation", "marshal", "internal_tuning"), - man = "mlr3extralearners::mlr_learners_classif.fastai", - label = "FastAi Neural Network Tabular Classifier" - ) - }, - #' @description - #' Marshal the learner's model. - #' @param ... (any)\cr - #' Additional arguments passed to [`mlr3::marshal_model()`][mlr3::marshaling()]. - marshal = function(...) { - learner_marshal(.learner = self, ...) - }, - #' @description - #' Unmarshal the learner's model. - #' @param ... (any)\cr - #' Additional arguments passed to [`mlr3::marshal_model()`][mlr3::marshaling()]. - unmarshal = function(...) { - learner_unmarshal(.learner = self, ...) - } + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. + initialize = function() { + + p_n_epoch = p_int(1L, + tags = c("train", "hotstart", "internal_tuning"), + default = 5L, + aggr = crate(function(x) as.integer(ceiling(mean(unlist(x)))), .parent = topenv()), + in_tune_fn = crate(function(domain, param_vals) { + if (is.null(param_vals$patience)) { + stop("Parameter 'patience' must be set to use internal tuning.") + } + assert_integerish(domain$upper, len = 1L, any.missing = FALSE) + }, .parent = topenv()), + disable_in_tune = list(patience = NULL) + ) + + param_set = ps( + act_cls = p_uty(tags = "train"), # Activation type for LinBnDrop layers, e.g., fastai::nn()$ReLU(inplace = TRUE) + bn_cont = p_lgl(default = TRUE, tags = "train"), # Use BatchNorm1d on continuous variables + bn_final = p_lgl(default = FALSE, tags = "train"), # Use BatchNorm1d on final layer + drop_last = p_lgl(default = FALSE, tags = "train"), # If True, then the last incomplete batch is dropped. + embed_p = p_dbl(lower = 0L, upper = 1L, default = 0L, tags = "train"), # Dropout probability for Embedding layer + emb_szs = p_uty(default = NULL, tags = "train"), # Sequence of (num_embeddings, embedding_dim) for each categorical variable + n_epoch = p_n_epoch, + eval_metric = p_uty(tags = "train", custom_check = crate({ + function(x) check_true(is.function(x) || inherits(x, "Measure")) + })), + layers = p_uty(tags = "train"), # Sequence of ints used to specify the input and output size of each LinBnDrop layer + loss_func = p_uty(tags = "train"), # Defaults to fastai::CrossEntropyLossFlat() + lr = p_dbl(lower = 0, default = 0.001, tags = "train"), # Learning rate + metrics = p_uty(tags = "train"), # optional list of metrics, e.g, fastai::Precision() or fastai::accuracy() + n_out = p_int(tags = "train"), # ? + num_workers = p_int(default = 0L, tags = "train"), # how many subprocesses to use for data loading + opt_func = p_uty(tags = "train"), # Optimizer created when Learner.fit is called. E.g., fastai::Adam() + patience = p_int(1L, default = 1, tags = "train"), # number of epochs to wait when training has not improved model. add `depends = quote(early_stopping == TRUE`)` + pin_memory = p_lgl(default = TRUE, tags = "train"), # If True, the data loader will copy Tensors into CUDA pinned memory before returning them. + procs = p_uty(default = NULL, tags = "train"), # fastai preprocessing steps such as fastai::Categorify, fastai::Normalize, fastai::fill_missing + ps = p_uty(default = NULL, tags = "train"), # Sequence of dropout probabilities + shuffle = p_lgl(default = FALSE, tags = "train"), # If True, then data is shuffled every time dataloader is fully read/iterated. + train_bn = p_lgl(default = TRUE, tags = "train"), # controls if BatchNorm layers are trained + wd = p_int(lower = 0, tags = "train"), # weight decay used for optimization + wd_bn_bias = p_lgl(default = FALSE, tags = "train"), # controls if weight decay is applied to BatchNorm layers and bias + use_bn = p_lgl(default = TRUE, tags = "train"), # Use BatchNorm1d in LinBnDrop layers + y_range = p_uty(default = NULL, tags = "train"), # Low and high for SigmoidRange activation (see below) + bs = p_int(default = 50, tags = "train") # how many samples per batch to load + ) + + param_set$set_values(n_epoch = 5L) + + super$initialize( + id = "classif.fastai", + packages = c("mlr3extralearners", "fastai", "reticulate"), + feature_types = c("logical", "integer", "numeric", "factor", "ordered"), + predict_types = c("response", "prob"), + param_set = param_set, + properties = c("multiclass", "twoclass", "weights", "validation", "marshal", "internal_tuning"), + man = "mlr3extralearners::mlr_learners_classif.fastai", + label = "FastAi Neural Network Tabular Classifier" + ) + }, + #' @description + #' Marshal the learner's model. + #' @param ... (any)\cr + #' Additional arguments passed to [`mlr3::marshal_model()`][mlr3::marshaling()]. + marshal = function(...) { + learner_marshal(.learner = self, ...) + }, + #' @description + #' Unmarshal the learner's model. + #' @param ... (any)\cr + #' Additional arguments passed to [`mlr3::marshal_model()`][mlr3::marshaling()]. + unmarshal = function(...) { + learner_unmarshal(.learner = self, ...) + } ), active = list( - #' @field internal_valid_scores (named `list()` or `NULL`) - #' The validation scores extracted from `eval_protocol` which itself is set by fitting the `fastai::tab_learner`. - #' If early stopping is activated, this contains the validation scores of the model for the optimal `n_epoch`, - #' otherwise the `n_epoch` for the final model. - internal_valid_scores = function() { - self$state$internal_valid_scores - }, - - #' @field internal_tuned_values (named `list()` or `NULL`) - #' If early stopping is activated, this returns a list with `n_epoch`, - #' which is the last epoch that yielded improvement w.r.t. the `patience`, extracted by `max(eval_protocol$epoch)+1` - internal_tuned_values = function() { - self$state$internal_tuned_values - }, - - #' @field validate (`numeric(1)` or `character(1)` or `NULL`) - #' How to construct the internal validation data. This parameter can be either `NULL`, - #' a ratio, `"test"`, or `"predefined"`. - validate = function(rhs) { - if (!missing(rhs)) { - private$.validate = assert_validate(rhs) - } - private$.validate - }, - - #' @field marshaled (`logical(1)`) - #' Whether the learner has been marshaled. - marshaled = function() { - learner_marshaled(self) - } + #' @field internal_valid_scores (named `list()` or `NULL`) + #' The validation scores extracted from `eval_protocol` which itself is set by fitting the `fastai::tab_learner`. + #' If early stopping is activated, this contains the validation scores of the model for the optimal `n_epoch`, + #' otherwise the `n_epoch` for the final model. + internal_valid_scores = function() { + self$state$internal_valid_scores + }, + + #' @field internal_tuned_values (named `list()` or `NULL`) + #' If early stopping is activated, this returns a list with `n_epoch`, + #' which is the last epoch that yielded improvement w.r.t. the `patience`, extracted by `max(eval_protocol$epoch)+1` + internal_tuned_values = function() { + self$state$internal_tuned_values + }, + + #' @field validate (`numeric(1)` or `character(1)` or `NULL`) + #' How to construct the internal validation data. This parameter can be either `NULL`, + #' a ratio, `"test"`, or `"predefined"`. + validate = function(rhs) { + if (!missing(rhs)) { + private$.validate = assert_validate(rhs) + } + private$.validate + }, + + #' @field marshaled (`logical(1)`) + #' Whether the learner has been marshaled. + marshaled = function() { + learner_marshaled(self) + } ), private = list( - .validate = NULL, - - .train = function(task) { - assert_python_packages("fastai") - - formula = task$formula() - data = task$data() - type = NULL - cat_cols = task$feature_types[type != "numeric"]$id - num_cols = task$feature_types[type == "numeric"]$id - - pars = self$param_set$get_values(tags = "train") - measure = pars$eval_metric - patience = pars$patience - - if (length(cat_cols) && is.null(pars$procs)) pars$procs = list(fastai::Categorify()) - if (is.null(measure)) measure = fastai::accuracy() - - # match parameters to fastai functions - fastai2 = getFromNamespace("fastai2", ns = "fastai") - args_dt = formalArgs(fastai::TabularDataTable) - args_dl = formalArgs(fastai2$data$load$DataLoader) - args_config = formalArgs(fastai::tabular_config) - args_fit = formalArgs(fastai2$learner$Learner$fit) - - pv_dt = pars[names(pars) %in% args_dt] - pv_dl = pars[names(pars) %in% args_dl] - pv_config = pars[names(pars) %in% args_config] - pv_fit = pars[names(pars) %in% args_fit] - pv_layers = pars[names(pars) == "layers"] - - # internal validation - internal_valid_task = task$internal_valid_task - if (!is.null(patience) && is.null(internal_valid_task)) { - stopf("Learner (%s): Configure field 'validate' to enable early stopping.", self$id) - } - - if (!is.null(internal_valid_task)) { - full_data = data.table::rbindlist(list(data, internal_valid_task$data())) - splits = list(seq(task$nrow), seq(task$nrow + 1, task$nrow + internal_valid_task$nrow)) - } else { - full_data = data - splits = NULL - } - - metrics = if (inherits(measure, "Measure")) { - # wrap mlr3 measure into fastai metric - params_for_metric = if ("twoclass" %in% unlist(measure$task_properties)) list(positive = "1") - - invoke( - fastai::AccumMetric, - metric, - flatten = FALSE, - msr = measure, - lvl = levels(factor(as.integer(task$truth()) - 1)), - .args = params_for_metric - ) - } else { - # measure from fastai - measure - } - - # set data into a format suitable for fastai - df_fai = invoke( - fastai::TabularDataTable, - df = full_data, - cat_names = cat_cols, - cont_names = num_cols, - y_names = task$target_names, - splits = splits, - .args = pv_dt - ) - - dl = invoke( - fastai::dataloaders, - df_fai, - .args = pv_dl - ) - - config = invoke( - fastai::tabular_config, - .args = pv_config - ) - - weights = private$.get_weights(task) - if (!is.null(weights)) { - dl$train$wgts = weights / sum(weights) - } - - tab_learner = fastai::tabular_learner( - dls = dl, - layers = pv_layers, - config = config, - metrics = metrics - ) - - # internal tuning - if (!is.null(patience)) { - monitor = tab_learner$metrics[[0]]$name - - # direction for internal tuning - # if mlr3 measure and minimize use numpy less - comp = if (inherits(measure, "Measure") && measure$minimize) { - np = reticulate::import("numpy") - np$less - } - - tab_learner$add_cb( - fastai::EarlyStoppingCallback(monitor = monitor, comp = comp, patience = patience) - ) - } - - # avoid plot creation when internally validating - invisible(tab_learner$remove_cb(tab_learner$progress)) - - # prevent python from printing evaluation protocol - invisible(reticulate::py_capture_output({ - eval_protocol = invoke( - fastai::fit, - object = tab_learner, - .args = pv_fit - ) - })) - - # Rename eval protocol in case custom metric was used - names(eval_protocol)[ - names(eval_protocol) == "python_function" - ] = if (inherits(measure, "Measure")) measure$id - - structure(list(tab_learner = tab_learner, eval_protocol = eval_protocol), class = "fastai_model") - }, - - .predict = function(task) { - assert_python_packages("fastai") - - pars = self$param_set$get_values(tags = "predict") - newdata = ordered_features(task, self) - - pred = invoke(predict, self$model$tab_learner, newdata, .args = pars) - class_labels = task$class_names - - if (self$predict_type == "response") { - response = class_labels[pred$class + 1] - list(response = response) - } else { - list(prob = as.matrix(pred[, class_labels])) - } - }, - - .extract_internal_tuned_values = function() { - if (is.null(self$state$param_vals$patience) || is.null(self$model$eval_protocol)) { - return(NULL) - } - list(n_epoch = max(self$model$eval_protocol$epoch) + 1) - }, - - .extract_internal_valid_scores = function() { - if (is.null(self$model$eval_protocol)) { - return(NULL) - } - metric = self$model$tab_learner$metrics[[0]]$name - metric_name = if (metric == "python_function") { - self$state$param_vals$eval_metric$id - } else { - metric - } - set_names(list(self$model$eval_protocol[nrow(self$model$eval_protocol), metric_name]), metric_name) - } + .validate = NULL, + + .train = function(task) { + assert_python_packages("fastai") + + formula = task$formula() + data = task$data() + type = NULL + cat_cols = task$feature_types[type != "numeric"]$id + num_cols = task$feature_types[type == "numeric"]$id + + pars = self$param_set$get_values(tags = "train") + measure = pars$eval_metric + patience = pars$patience + + if (length(cat_cols) && is.null(pars$procs)) pars$procs = list(fastai::Categorify()) + if (is.null(measure)) measure = fastai::accuracy() + + # match parameters to fastai functions + fastai2 = getFromNamespace("fastai2", ns = "fastai") + args_dt = formalArgs(fastai::TabularDataTable) + args_dl = formalArgs(fastai2$data$load$DataLoader) + args_config = formalArgs(fastai::tabular_config) + args_fit = formalArgs(fastai2$learner$Learner$fit) + + pv_dt = pars[names(pars) %in% args_dt] + pv_dl = pars[names(pars) %in% args_dl] + pv_config = pars[names(pars) %in% args_config] + pv_fit = pars[names(pars) %in% args_fit] + pv_layers = pars[names(pars) == "layers"] + + # internal validation + internal_valid_task = task$internal_valid_task + if (!is.null(patience) && is.null(internal_valid_task)) { + stopf("Learner (%s): Configure field 'validate' to enable early stopping.", self$id) + } + + if (!is.null(internal_valid_task)) { + full_data = data.table::rbindlist(list(data, internal_valid_task$data())) + splits = list(seq(task$nrow), seq(task$nrow + 1, task$nrow + internal_valid_task$nrow)) + } else { + full_data = data + splits = NULL + } + + metrics = if (inherits(measure, "Measure")) { + # wrap mlr3 measure into fastai metric + params_for_metric = if ("twoclass" %in% unlist(measure$task_properties)) list(positive = "1") + + invoke( + fastai::AccumMetric, + metric, + flatten = FALSE, + msr = measure, + lvl = levels(factor(as.integer(task$truth()) - 1)), + .args = params_for_metric + ) + } else { + # measure from fastai + measure + } + + # set data into a format suitable for fastai + df_fai = invoke( + fastai::TabularDataTable, + df = full_data, + cat_names = cat_cols, + cont_names = num_cols, + y_names = task$target_names, + splits = splits, + .args = pv_dt + ) + + dl = invoke( + fastai::dataloaders, + df_fai, + .args = pv_dl + ) + + config = invoke( + fastai::tabular_config, + .args = pv_config + ) + + weights = private$.get_weights(task) + if (!is.null(weights)) { + dl$train$wgts = weights / sum(weights) + } + + tab_learner = fastai::tabular_learner( + dls = dl, + layers = pv_layers, + config = config, + metrics = metrics + ) + + # internal tuning + if (!is.null(patience)) { + monitor = tab_learner$metrics[[0]]$name + + # direction for internal tuning + # if mlr3 measure and minimize use numpy less + comp = if (inherits(measure, "Measure") && measure$minimize) { + np = reticulate::import("numpy") + np$less + } + + tab_learner$add_cb( + fastai::EarlyStoppingCallback(monitor = monitor, comp = comp, patience = patience) + ) + } + + # avoid plot creation when internally validating + invisible(tab_learner$remove_cb(tab_learner$progress)) + + # prevent python from printing evaluation protocol + invisible(reticulate::py_capture_output({ + eval_protocol = invoke( + fastai::fit, + object = tab_learner, + .args = pv_fit + ) + })) + + # Rename eval protocol in case custom metric was used + names(eval_protocol)[ + names(eval_protocol) == "python_function" + ] = if (inherits(measure, "Measure")) measure$id + + structure(list(tab_learner = tab_learner, eval_protocol = eval_protocol), class = "fastai_model") + }, + + .predict = function(task) { + assert_python_packages("fastai") + + pars = self$param_set$get_values(tags = "predict") + newdata = ordered_features(task, self) + + pred = invoke(predict, self$model$tab_learner, newdata, .args = pars) + class_labels = task$class_names + + if (self$predict_type == "response") { + response = class_labels[pred$class + 1] + list(response = response) + } else { + list(prob = as.matrix(pred[, class_labels])) + } + }, + + .extract_internal_tuned_values = function() { + if (is.null(self$state$param_vals$patience) || is.null(self$model$eval_protocol)) { + return(NULL) + } + list(n_epoch = max(self$model$eval_protocol$epoch) + 1) + }, + + .extract_internal_valid_scores = function() { + if (is.null(self$model$eval_protocol)) { + return(NULL) + } + metric = self$model$tab_learner$metrics[[0]]$name + metric_name = if (metric == "python_function") { + self$state$param_vals$eval_metric$id + } else { + metric + } + set_names(list(self$model$eval_protocol[nrow(self$model$eval_protocol), metric_name]), metric_name) + } ) ) @@ -368,4 +368,4 @@ unmarshal_model.fastai_model_marshaled = function(model, inplace = FALSE, ...) { # unpickle tab_learner = pickle$loads(reticulate::r_to_py(model$marshaled$pickled)) structure(list(tab_learner = tab_learner, eval_protocol = model$marshaled$eval_protocol), class = "fastai_model") -} +} \ No newline at end of file diff --git a/R/py_base_class_fastai.R b/R/py_base_class_fastai.R index 84a029cec..adfdfe0eb 100644 --- a/R/py_base_class_fastai.R +++ b/R/py_base_class_fastai.R @@ -73,10 +73,8 @@ LearnerPythonClassifFastai <- R6::R6Class( packages = c("mlr3extralearners", "fastai", "reticulate"), # R packages label = "Fastai Tabular Classifier", man = "mlr3extralearners::mlr_learners_classif.fastai", - py_packages = c("fastai", "torch"), - python_version = "3.10", - method = "auto" + python_version = "3.10" ) }, #' @description @@ -132,8 +130,10 @@ LearnerPythonClassifFastai <- R6::R6Class( .validate = NULL, # -------------------- TRAIN HOOK -------------------- - .fit_py = function(x, y, pars, task) { - self$ensure_deps() + .train_py = function(task) { + py_requirements = self$py_requirements() + assert_python_packages(packages = py_requirements$packages, + python_version = py_requirements$python_version) # We need the task columns split into cat/cont; fastai uses names, not indices. data = task$data() @@ -142,6 +142,7 @@ LearnerPythonClassifFastai <- R6::R6Class( num_cols = task$feature_types[type == "numeric"]$id # Default eval metric if none is supplied + pars = self$param_set$get_values(tags = "train") measure = pars$eval_metric patience = pars$patience if (is.null(measure)) measure = fastai::accuracy() @@ -182,7 +183,7 @@ LearnerPythonClassifFastai <- R6::R6Class( metric, flatten = FALSE, msr = measure, - lvl = levels(factor(as.integer(y) - 1L)), + lvl = levels(factor(as.integer(task$truth()) - 1L)), .args = params_for_metric ) } else { @@ -258,39 +259,43 @@ LearnerPythonClassifFastai <- R6::R6Class( list( model = tab_learner, - meta = list(eval_protocol = eval_protocol) + eval_protocol = eval_protocol, + class_labels = task$class_names ) }, # -------------------- PREDICT HOOK -------------------- - .predict_py = function(model, newdata, predict_type, meta, class_labels) { - self$ensure_deps() + .predict_py = function(task, newdata, predict_types) { + model = self$model$model + class_labels = self$model$class_labels # Fastai expects same column order & types as during fit pars_pred = self$param_set$get_values(tags = "predict") pred = invoke(fastai::predict, model, newdata, .args = pars_pred) - if ("prob" %in% predict_type) { + if ("prob" %in% predict_types) { prob = as.matrix(pred[, class_labels, drop = FALSE]) list(response = NULL, prob = prob) - } else { + } else if ("response" %in% predict_types) { response = class_labels[pred$class + 1L] list(response = response, prob = NULL) + } else { + list() } }, # ---------------- Validation extractors ---------------- .extract_internal_tuned_values = function() { - ep <- self$model$meta$eval_protocol + ep = self$model$eval_protocol if (is.null(self$state$param_vals$patience) || is.null(ep)) return(NULL) list(n_epoch = max(ep$epoch) + 1L) }, .extract_internal_valid_scores = function() { - ep <- self$model$meta$eval_protocol + ep = self$model$eval_protocol if (is.null(ep)) return(NULL) - metric <- self$model$fitted$metrics[[0]]$name - metric_name <- if (metric == "python_function") { + metric = self$model$fitted$metrics[[0]]$name + metric_name = if (metric == "python_function") { self$state$param_vals$eval_metric$id } else metric set_names(list(ep[nrow(ep), metric_name]), metric_name) diff --git a/R/py_base_class_tabpfn.R b/R/py_base_class_tabpfn.R index 6e6b6d0fb..e5b449806 100644 --- a/R/py_base_class_tabpfn.R +++ b/R/py_base_class_tabpfn.R @@ -40,9 +40,8 @@ LearnerPythonClassifTabPFN <- R6::R6Class( feature_types = c("integer","numeric","logical"), predict_types = c("response","prob"), param_set = ps, - py_packages = c("numpy","pandas","torch","tabpfn"), + py_packages = c("numpy","pandas","torch","tabpfn"), python_version = "3.10", - method = "auto", properties = c("twoclass", "multiclass", "missings", "marshal"), man = "mlr3extralearners::mlr_learners_classif.tabpfn" ) @@ -65,31 +64,6 @@ LearnerPythonClassifTabPFN <- R6::R6Class( ), active = list( - #' @field internal_valid_scores (named `list()` or `NULL`) - #' The validation scores extracted from `eval_protocol` which itself is set by fitting the `fastai::tab_learner`. - #' If early stopping is activated, this contains the validation scores of the model for the optimal `n_epoch`, - #' otherwise the `n_epoch` for the final model. - internal_valid_scores = function() { - self$state$internal_valid_scores - }, - - #' @field internal_tuned_values (named `list()` or `NULL`) - #' If early stopping is activated, this returns a list with `n_epoch`, - #' which is the last epoch that yielded improvement w.r.t. the `patience`, extracted by `max(eval_protocol$epoch)+1` - internal_tuned_values = function() { - self$state$internal_tuned_values - }, - - #' @field validate (`numeric(1)` or `character(1)` or `NULL`) - #' How to construct the internal validation data. This parameter can be either `NULL`, - #' a ratio, `"test"`, or `"predefined"`. - validate = function(rhs) { - if (!missing(rhs)) { - private$.validate = assert_validate(rhs) - } - private$.validate - }, - #' @field marshaled (`logical(1)`) #' Whether the learner has been marshaled. marshaled = function() { @@ -98,49 +72,70 @@ LearnerPythonClassifTabPFN <- R6::R6Class( ), private = list( - .fit_py = function(x, y, pars, task) { - torch <- reticulate::import("torch", delay_load = FALSE) - tabpfn <- reticulate::import("tabpfn", delay_load = FALSE) + .train_py = function(task) { + + torch = reticulate::import("torch", delay_load = FALSE) + tabpfn = reticulate::import("tabpfn", delay_load = FALSE) + + pars = self$param_set$get_values(tags = "train") - ip <- pars$inference_precision + ip = pars$inference_precision if (!is.null(ip) && startsWith(ip, "torch.")) { - dtype <- strsplit(ip, "\\.", fixed = TRUE)[[1]][2] - pars$inference_precision <- reticulate::py_get_attr(torch, dtype) + dtype = strsplit(ip, "\\.", fixed = TRUE)[[1]][2] + pars$inference_precision = reticulate::py_get_attr(torch, dtype) } if (!is.null(pars$device) && pars$device != "auto") { - pars$device <- torch$device(pars$device) + pars$device = torch$device(pars$device) } if (identical(pars$random_state, "None")) { - pars$random_state <- reticulate::py_none() + pars$random_state = reticulate::py_none() } - if (!is.null(pars$categorical_features_indices)) { - pars$categorical_features_indices <- as.integer(pars$categorical_features_indices - 1L) + # leave categorical_features_indices as provided (1-based R indices for now); + # convert once below after validating against current feature count + # x is an (n_samples, n_features) array + X = as.matrix(task$data(cols = task$feature_names)) + # force NaN to make conversion work, + # otherwise reticulate will not convert NAs in logical and integer columns to + # np.nan properly + X[is.na(X)] = NaN + # y is an (n_samples,) array + Y = task$truth() + X_py = reticulate::r_to_py(X) + Y_py = reticulate::r_to_py(Y) + + # convert categorical_features_indices to python indexing + categ_indices = pars$categorical_features_indices + if (!is.null(categ_indices)) { + if (max(categ_indices) > ncol(X)) { + stop("categorical_features_indices must not exceed number of features") + } + pars$categorical_features_indices = as.integer(categ_indices - 1L) } - X <- as.matrix(x); storage.mode(X) <- "double"; X[is.na(X)] <- NaN - X_py <- reticulate::r_to_py(X) - y_py <- reticulate::r_to_py(as.vector(y)) + clf = mlr3misc::invoke(tabpfn$TabPFNClassifier, .args = pars) + fitted = mlr3misc::invoke(clf$fit, X = X_py, y = Y_py) - clf <- mlr3misc::invoke(tabpfn$TabPFNClassifier, .args = pars) - fitted <- mlr3misc::invoke(clf$fit, X = X_py, y = y_py) - - classes <- as.character(reticulate::py_to_r(fitted$classes_)) - list(model = fitted, meta = list(classes = classes)) + classes = as.character(reticulate::py_to_r(fitted$classes_)) + list(model = fitted, classes = classes) }, - .predict_py = function(model, newdata, predict_types, meta, class_names) { - X <- as.matrix(newdata); storage.mode(X) <- "double"; X[is.na(X)] <- NaN - X_py <- reticulate::r_to_py(X) + .predict_py = function(task, newdata, predict_types) { + model = self$model$model + X = as.matrix(newdata) + storage.mode(X) = "double" + X[is.na(X)] = NaN + X_py = reticulate::r_to_py(X) if ("prob" %in% predict_types) { - prob <- mlr3misc::invoke(model$predict_proba, X = X_py) - prob <- reticulate::py_to_r(prob) - colnames(prob) <- meta$classes %||% as.character(reticulate::py_to_r(model$classes_)) - list(response = NULL, prob = prob) - } else { + prob = mlr3misc::invoke(model$predict_proba, X = X_py) + prob = reticulate::py_to_r(prob) + colnames(prob) = as.character(reticulate::py_to_r(model$classes_)) + list(prob = prob) + } + + if ("response" %in% predict_types) { response <- mlr3misc::invoke(model$predict, X = X_py) - response <- reticulate::py_to_r(response) - list(response = as.character(response), prob = NULL) + list(response = as.character(reticulate::py_to_r(response))) } } ) diff --git a/attic/demo_python_learners.R b/attic/demo_python_learners.R index a915a7439..84dd38551 100644 --- a/attic/demo_python_learners.R +++ b/attic/demo_python_learners.R @@ -1,229 +1,276 @@ +skip_on_os("windows") skip_if_not_installed("fastai") skip_if_not_installed("reticulate") test_that("autotest", { - learner = lrn("classif.python.fastai", layers = c(200, 100)) - # expect_learner(learner, check_man = FALSE) # will not work because base class has new tags - # results not replicable, because torch seed must be set in the python backend - result = run_autotest(learner, check_replicable = FALSE) - expect_true(result, info = result$error) + expect_true(callr::r(function() { + Sys.setenv(RETICULATE_PYTHON = "managed") + library(mlr3) + library(mlr3extralearners) + + lapply(list.files(system.file("testthat", package = "mlr3"), + pattern = "^helper.*\\.[rR]", full.names = TRUE), source) + + reticulate::py_require(c("numpy", "fastai"), python_version = "3.10") + + learner = lrn("classif.python.fastai", layers = c(200, 100)) + expect_learner(learner, check_man = FALSE) + # results not replicable, because torch seed must be set in the python backend + result = run_autotest(learner, check_replicable = FALSE) + testthat::expect_true(result, info = result$error) + TRUE + })) }) - test_that("eval protocol", { - learner = lrn("classif.python.fastai") - task = tsk("sonar") - learner$train(task) - expect_true(is.list(learner$model$meta$eval_protocol)) + expect_true(callr::r(function() { + Sys.setenv(RETICULATE_PYTHON = "managed") + library(mlr3) + library(mlr3extralearners) + + lapply(list.files(system.file("testthat", package = "mlr3"), + pattern = "^helper.*\\.[rR]", full.names = TRUE), source) + + reticulate::py_require(c("numpy", "fastai"), python_version = "3.10") + + learner = lrn("classif.python.fastai") + task = tsk("sonar") + learner$train(task) + testthat::expect_true(is.list(learner$state$eval_protocol)) + TRUE + })) }) test_that("validation and inner tuning works", { - task = tsk("spam") - - # with n_epoch and patience parameter - learner = lrn("classif.python.fastai", - n_epoch = 10, - patience = 1, - validate = 0.2 - ) - - learner$train(task) - expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "accuracy")) - expect_list(learner$internal_valid_scores, types = "numeric") - expect_equal(names(learner$internal_valid_scores), "accuracy") - expect_list(learner$internal_tuned_values, types = "integerish") - expect_equal(names(learner$internal_tuned_values), "n_epoch") - - # without validate parameter - learner$validate = NULL - expect_error(learner$train(task), "field 'validate'") - - # with patience parameter - learner = lrn("classif.python.fastai", - n_epoch = 10, - validate = 0.2 - ) - - learner$train(task) - expect_equal(learner$internal_tuned_values, NULL) - expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "accuracy")) - expect_list(learner$internal_valid_scores, types = "numeric") - expect_equal(names(learner$internal_valid_scores), "accuracy") - - # internal tuning - learner = lrn("classif.python.fastai", - n_epoch = to_tune(upper = 20, internal = TRUE), - validate = 0.2 - ) - s = learner$param_set$search_space() - expect_error(learner$param_set$convert_internal_search_space(s), "patience") - learner$param_set$set_values(n_epoch = 10) - learner$param_set$disable_internal_tuning("n_epoch") - expect_equal(learner$param_set$values$n_epoch, NULL) - - learner = lrn("classif.python.fastai", - n_epoch = 20, - patience = 5, - validate = 0.3 - ) - learner$train(task) - expect_equal(learner$internal_valid_scores$accuracy, learner$model$meta$eval_protocol$accuracy[learner$internal_tuned_values$n_epoch]) - - # no validation and no internal tuning - learner = lrn("classif.python.fastai") - learner$train(task) - expect_null(learner$internal_valid_scores) - expect_null(learner$internal_tuned_values) - - # no tuning without patience parameter - learner = lrn("classif.python.fastai", validate = 0.3, n_epoch = 10) - learner$train(task) - expect_equal(learner$internal_valid_scores$accuracy, learner$model$meta$eval_protocol$accuracy[10L]) - expect_null(learner$internal_tuned_values) + expect_true(callr::r(function() { + Sys.setenv(RETICULATE_PYTHON = "managed") + library(mlr3) + library(mlr3extralearners) + + lapply(list.files(system.file("testthat", package = "mlr3"), + pattern = "^helper.*\\.[rR]", full.names = TRUE), source) + + reticulate::py_require(c("numpy", "fastai"), python_version = "3.10") + + task = tsk("spam") + + # with n_epoch and patience parameter + learner = lrn("classif.python.fastai", + n_epoch = 10, + patience = 1, + validate = 0.2 + ) + + learner$train(task) + testthat::expect_named(learner$state$eval_protocol, c("epoch", "train_loss", "valid_loss", "accuracy")) + testthat::expect_list(learner$internal_valid_scores, types = "numeric") + testthat::expect_equal(names(learner$internal_valid_scores), "accuracy") + testthat::expect_list(learner$internal_tuned_values, types = "integerish") + testthat::expect_equal(names(learner$internal_tuned_values), "n_epoch") + + # without validate parameter + learner$validate = NULL + testthat::expect_error(learner$train(task), "field 'validate'") + + # with patience parameter + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2 + ) + + learner$train(task) + testthat::expect_equal(learner$internal_tuned_values, NULL) + testthat::expect_named(learner$state$eval_protocol, c("epoch", "train_loss", "valid_loss", "accuracy")) + testthat::expect_list(learner$internal_valid_scores, types = "numeric") + testthat::expect_equal(names(learner$internal_valid_scores), "accuracy") + + # internal tuning + learner = lrn("classif.python.fastai", + n_epoch = to_tune(upper = 20, internal = TRUE), + validate = 0.2 + ) + s = learner$param_set$search_space() + testthat::expect_error(learner$param_set$convert_internal_search_space(s), "patience") + learner$param_set$set_values(n_epoch = 10) + learner$param_set$disable_internal_tuning("n_epoch") + testthat::expect_equal(learner$param_set$values$n_epoch, NULL) + + learner = lrn("classif.python.fastai", + n_epoch = 20, + patience = 5, + validate = 0.3 + ) + learner$train(task) + testthat::expect_equal(learner$internal_valid_scores$accuracy, learner$state$eval_protocol$accuracy[learner$internal_tuned_values$n_epoch]) + + # no validation and no internal tuning + learner = lrn("classif.python.fastai") + learner$train(task) + testthat::expect_null(learner$internal_valid_scores) + testthat::expect_null(learner$internal_tuned_values) + + # no tuning without patience parameter + learner = lrn("classif.python.fastai", validate = 0.3, n_epoch = 10) + learner$train(task) + testthat::expect_equal(learner$internal_valid_scores$accuracy, learner$state$eval_protocol$accuracy[10L]) + testthat::expect_null(learner$internal_tuned_values) + TRUE + })) }) test_that("custom inner validation measure", { - # internal measure - task = tsk("sonar") - - learner = lrn("classif.python.fastai", - n_epoch = 10, - validate = 0.2, - patience = 1, - eval_metric = fastai::error_rate() - ) - - learner$train(task) - - expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "error_rate")) - expect_list(learner$internal_valid_scores, types = "numeric") - expect_equal(names(learner$internal_valid_scores), "error_rate") - - # binary task and mlr3 measure binary response - task = tsk("sonar") - - learner = lrn("classif.python.fastai", - n_epoch = 10, - validate = 0.2, - #patience = 1, - eval_metric = msr("classif.ce") - ) - - learner$train(task) - - expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.ce")) - expect_numeric(learner$model$meta$eval_protocol$classif.ce, len = 10) - expect_list(learner$internal_valid_scores, types = "numeric") - expect_equal(names(learner$internal_valid_scores), "classif.ce") - - # binary task and mlr3 measure binary prob - task = tsk("sonar") - - learner = lrn("classif.python.fastai", - n_epoch = 10, - validate = 0.2, - #patience = 1, - predict_type = "prob", - eval_metric = msr("classif.logloss") - ) - - learner$train(task) - - expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.logloss")) - expect_numeric(learner$model$meta$eval_protocol$classif.logloss, len = 10) - expect_list(learner$internal_valid_scores, types = "numeric") - expect_equal(names(learner$internal_valid_scores), "classif.logloss") - - # binary task and mlr3 measure multiclass prob - task = tsk("sonar") - - learner = lrn("classif.python.fastai", - n_epoch = 10, - validate = 0.2, - # patience = 1, - predict_type = "prob", - eval_metric = msr("classif.auc") - ) - - learner$train(task) - - expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.auc")) - expect_numeric(learner$model$meta$eval_protocol$classif.auc, len = 10) - expect_list(learner$internal_valid_scores, types = "numeric") - expect_equal(names(learner$internal_valid_scores), "classif.auc") - - # multiclass task and mlr3 measure multiclass response - task = tsk("iris") - - learner = lrn("classif.python.fastai", - n_epoch = 10, - validate = 0.2, - #patience = 1, - predict_type = "prob", - eval_metric = msr("classif.ce") - ) - - learner$train(task) - - expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.ce")) - expect_numeric(learner$model$meta$eval_protocol$classif.ce, len = 10) - expect_list(learner$internal_valid_scores, types = "numeric") - expect_equal(names(learner$internal_valid_scores), "classif.ce") - - # multiclass task and mlr3 measure multiclass prob - task = tsk("iris") - - learner = lrn("classif.python.fastai", - n_epoch = 10, - validate = 0.2, - # patience = 1, - predict_type = "prob", - eval_metric = msr("classif.logloss") - ) - - learner$train(task) - - expect_named(learner$model$meta$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.logloss")) - expect_numeric(learner$model$meta$eval_protocol$classif.logloss, len = 10) - expect_list(learner$internal_valid_scores, types = "numeric") - expect_equal(names(learner$internal_valid_scores), "classif.logloss") + expect_true(callr::r(function() { + Sys.setenv(RETICULATE_PYTHON = "managed") + library(mlr3) + library(mlr3extralearners) + + lapply(list.files(system.file("testthat", package = "mlr3"), + pattern = "^helper.*\\.[rR]", full.names = TRUE), source) + + reticulate::py_require(c("numpy", "fastai"), python_version = "3.10") + # internal measure + task = tsk("sonar") + + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2, + patience = 1, + eval_metric = fastai::error_rate() + ) + + learner$train(task) + + testthat::expect_named(learner$state$eval_protocol, c("epoch", "train_loss", "valid_loss", "error_rate")) + testthat::expect_list(learner$internal_valid_scores, types = "numeric") + testthat::expect_equal(names(learner$internal_valid_scores), "error_rate") + + # binary task and mlr3 measure binary response + task = tsk("sonar") + + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2, + #patience = 1, + eval_metric = msr("classif.ce") + ) + + learner$train(task) + + testthat::expect_named(learner$state$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.ce")) + testthat::expect_numeric(learner$state$eval_protocol$classif.ce, len = 10) + testthat::expect_list(learner$internal_valid_scores, types = "numeric") + testthat::expect_equal(names(learner$internal_valid_scores), "classif.ce") + + # binary task and mlr3 measure binary prob + task = tsk("sonar") + + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2, + #patience = 1, + predict_type = "prob", + eval_metric = msr("classif.logloss") + ) + + learner$train(task) + + testthat::expect_named(learner$state$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.logloss")) + testthat::expect_numeric(learner$state$eval_protocol$classif.logloss, len = 10) + testthat::expect_list(learner$internal_valid_scores, types = "numeric") + testthat::expect_equal(names(learner$internal_valid_scores), "classif.logloss") + + # binary task and mlr3 measure multiclass prob + task = tsk("sonar") + + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2, + # patience = 1, + predict_type = "prob", + eval_metric = msr("classif.auc") + ) + + learner$train(task) + + testthat::expect_named(learner$state$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.auc")) + testthat::expect_numeric(learner$state$eval_protocol$classif.auc, len = 10) + testthat::expect_list(learner$internal_valid_scores, types = "numeric") + testthat::expect_equal(names(learner$internal_valid_scores), "classif.auc") + + # multiclass task and mlr3 measure multiclass response + task = tsk("iris") + + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2, + #patience = 1, + predict_type = "prob", + eval_metric = msr("classif.ce") + ) + + learner$train(task) + + testthat::expect_named(learner$state$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.ce")) + testthat::expect_numeric(learner$state$eval_protocol$classif.ce, len = 10) + testthat::expect_list(learner$internal_valid_scores, types = "numeric") + testthat::expect_equal(names(learner$internal_valid_scores), "classif.ce") + + # multiclass task and mlr3 measure multiclass prob + task = tsk("iris") + + learner = lrn("classif.python.fastai", + n_epoch = 10, + validate = 0.2, + # patience = 1, + predict_type = "prob", + eval_metric = msr("classif.logloss") + ) + + learner$train(task) + + testthat::expect_named(learner$state$eval_protocol, c("epoch", "train_loss", "valid_loss", "classif.logloss")) + testthat::expect_numeric(learner$state$eval_protocol$classif.logloss, len = 10) + testthat::expect_list(learner$internal_valid_scores, types = "numeric") + testthat::expect_equal(names(learner$internal_valid_scores), "classif.logloss") + TRUE + })) }) test_that("marshaling works for classif.python.fastai", { - learner = lrn("classif.python.fastai") - task = tsk("iris") - # expect_marshalable_learner(learner, task) - - learner$train(task) - pred = learner$predict(task) - model = learner$model - class_prev = class(model) - - # checks for marshaling is the same as expect_marshalable_learner - expect_false(learner$marshaled) - expect_equal(is_marshaled_model(learner$model), learner$marshaled) - expect_invisible(learner$marshal()) - expect_equal(mlr3::is_marshaled_model(learner$model), learner$marshaled) - - # checks for unmarshaling differs -- instead of checking equality of model, - # we check equality of predictions, because expect_equal() on python objects - # checks the pointer which almost always changes after unmarshaling - expect_invisible(learner$unmarshal()) - expect_prediction(learner$predict(task)) - expect_equal(learner$predict(task), pred) - expect_false(learner$marshaled) - expect_equal(class(learner$model), class_prev) -}) - -test_that("encapsulation works for classif.python.fastai", { - learner = lrn("classif.python.fastai") - task = tsk("irirs") - - learner$train(task) - learner_encapsulated = learner$clone(deep = TRUE) - learner_encapsulated$encapsulate("mirai", default_fallback(learner_encapsulated)) - - rr = resample(task, learner_encapsulated, rsmp("holdout"), store_models = TRUE) - log = rr$learners[[1]]$state$log + expect_true(callr::r(function() { + Sys.setenv(RETICULATE_PYTHON = "managed") + library(mlr3) + library(mlr3extralearners) + + lapply(list.files(system.file("testthat", package = "mlr3"), + pattern = "^helper.*\\.[rR]", full.names = TRUE), source) + + reticulate::py_require(c("numpy", "fastai"), python_version = "3.10") + + learner = lrn("classif.python.fastai") + task = tsk("iris") + # expect_marshalable_learner(learner, task) + + learner$train(task) + pred = learner$predict(task) + model = learner$model + class_prev = class(model) + + # checks for marshaling is the same as expect_marshalable_learner + testthat::expect_false(learner$marshaled) + testthat::expect_equal(is_marshaled_model(learner$model), learner$marshaled) + testthat::expect_invisible(learner$marshal()) + testthat::expect_equal(mlr3::is_marshaled_model(learner$model), learner$marshaled) + + # checks for unmarshaling differs -- instead of checking equality of model, + # we check equality of predictions, because expect_equal() on python objects + # checks the pointer which almost always changes after unmarshaling + testthat::expect_invisible(learner$unmarshal()) + testthat::expect_prediction(learner$predict(task)) + testthat::expect_equal(learner$predict(task), pred) + testthat::expect_false(learner$marshaled) + testthat::expect_equal(class(learner$model), class_prev) + TRUE + })) }) From 1b5a0cd8decc217a3814752a088355016d51374d Mon Sep 17 00:00:00 2001 From: AnnaNzrv Date: Wed, 15 Oct 2025 11:36:20 +0200 Subject: [PATCH 7/7] feat: paramspace - configspace conversion --- NAMESPACE | 3 + R/LearnerPythonClassif.R | 8 +- R/configspace_to_paramset.R | 279 +++++++++++++++++- R/paramset_to_configspace.R | 210 +++++++++++++ R/py_base_class_fastai.R | 12 +- R/py_base_class_tabpfn.R | 48 ++- attic/demo_python_learners.R | 6 + man/configspace_to_paramset.Rd | 36 +++ man/mlr_learners_classif.fastai.Rd | 172 +---------- man/mlr_learners_classif.python.fastai.Rd | 185 ++++++++++++ man/mlr_learners_classif.python.tabpfn.Rd | 228 ++++++++++++++ man/paramset_to_configspace.Rd | 57 ++++ tests/testthat/test_paramset_to_configspace.R | 130 ++++++++ 13 files changed, 1188 insertions(+), 186 deletions(-) create mode 100644 R/paramset_to_configspace.R create mode 100644 man/configspace_to_paramset.Rd create mode 100644 man/mlr_learners_classif.python.fastai.Rd create mode 100644 man/mlr_learners_classif.python.tabpfn.Rd create mode 100644 man/paramset_to_configspace.Rd create mode 100644 tests/testthat/test_paramset_to_configspace.R diff --git a/NAMESPACE b/NAMESPACE index babf50a0c..481c0c28c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -81,6 +81,7 @@ export(LearnerDensPenalized) export(LearnerDensPlugin) export(LearnerDensSpline) export(LearnerPythonClassifFastai) +export(LearnerPythonClassifTabPFN) export(LearnerRegrAbess) export(LearnerRegrBart) export(LearnerRegrBlockForest) @@ -156,9 +157,11 @@ export(LearnerSurvRanger) export(LearnerSurvSVM) export(LearnerSurvXgboostAFT) export(LearnerSurvXgboostCox) +export(configspace_to_paramset) export(install_learners) export(learner_is_runnable) export(list_mlr3learners) +export(paramset_to_configspace) import(checkmate) import(mlr3misc) import(paradox) diff --git a/R/LearnerPythonClassif.R b/R/LearnerPythonClassif.R index 2a67ab9d8..3e9b1823c 100644 --- a/R/LearnerPythonClassif.R +++ b/R/LearnerPythonClassif.R @@ -63,7 +63,7 @@ LearnerPythonClassif = R6::R6Class( do.call(assert_python_packages, py_requirements) newdata = ordered_features(task, self) - preds = private$.predict_py(task = task, newdata = newdata, predict_types = self$predict_types) + preds = private$.predict_py(task = task, newdata = newdata, predict_type = self$predict_type) preds }, @@ -99,7 +99,7 @@ marshal_model.pybytes_model <- function(model, inplace = FALSE, ...) { marshaled = raw, learner_class = learner_class, py_modules = model$py_modules, - py_version = model$py_version, + py_version = model$py_version ) meta_data = setdiff(model, "model") @@ -124,9 +124,9 @@ unmarshal_model.pybytes_model_marshaled <- function(model, inplace = FALSE, ...) meta_data = setdiff(model, "marshaled") out = list( model = fitted, - learner_class = learner_class, + learner_class = classes, py_modules = model$py_modules, - py_version = model$py_version, + py_version = model$py_version ) structure( diff --git a/R/configspace_to_paramset.R b/R/configspace_to_paramset.R index 9321beb9e..e2773ecaf 100644 --- a/R/configspace_to_paramset.R +++ b/R/configspace_to_paramset.R @@ -1 +1,278 @@ -CS = reticulate::import("ConfigSpace", delay_load = TRUE) +#' Convert a ConfigSpace ConfigurationSpace to a paradox ParamSet +#' +#' Translates a Python [`ConfigSpace.ConfigurationSpace`] into a +#' [`paradox::ParamSet`] via **reticulate**. +#' +#' Supported hyperparameter types: +#' - `Float` / `UniformFloatHyperparameter` => `ParamDbl` (via `p_dbl`) +#' - `Integer` / `UniformIntegerHyperparameter` => `ParamInt` (via `p_int`) +#' - `Categorical(TRUE/FALSE)` => `ParamLgl` (via `p_lgl`) +#' - `Categorical` (other items) => `ParamFct` (via `p_fct`) +#' +#' Tags are restored from `hp$meta$tags` if present (silently ignored otherwise). +#' Defaults are used when present; missing defaults are left unset. +#' +#' Dependency conditions (`EqualsCondition`, `InCondition`) are mapped to a single +#' `depends = quote(...)` expression per child and combined with `&&` if multiple +#' conditions exist for the same child. +#' +#' Works with both new (>= 1.0) and old ConfigSpace APIs. +#' +#' @param cs A Python `ConfigSpace.ConfigurationSpace` object. +#' +#' @return A [`paradox::ParamSet`]. +#' @export +configspace_to_paramset = function(cs) { + stopifnot(!is.null(cs)) + + if (!requireNamespace("reticulate", quietly = TRUE)) { + stop("reticulate is required.") + } + if (!requireNamespace("paradox", quietly = TRUE)) { + stop("paradox is required.") + } + + ConfigSpace = reticulate::import("ConfigSpace", delay_load = TRUE) + + old_cs_version = + !reticulate::py_has_attr(ConfigSpace, "Float") || + !reticulate::py_has_attr(ConfigSpace, "Integer") || + !reticulate::py_has_attr(ConfigSpace, "Categorical") + + # helpers -------------------------------------------------------------------- + + py_get = function(x, nms) { + for (nm in nms) { + val = tryCatch(x[[nm]], error = function(e) NULL) + if (!is.null(val)) return(val) + } + NULL + } + + get_hps = function(cs) { + if (!is.null(cs$get_hyperparameters)) return(cs$get_hyperparameters()) + return(cs$values()) # very old API + } + + hp_name = function(hp) { + # common: $name; sometimes @_name + nm = py_get(hp, c("name", "_name")) + as.character(nm) + } + + hp_meta_tags = function(hp) { + meta = py_get(hp, c("meta", "_meta")) + if (is.null(meta)) return(NULL) + tg = meta[["tags"]] + if (is.null(tg)) return(NULL) + as.character(unlist(tg)) + } + + hp_default = function(hp) { + py_get(hp, c("default", "default_value", "_default")) + } + + hp_bounds = function(hp) { + b = py_get(hp, c("bounds")) + if (!is.null(b)) { + b = as.numeric(unlist(b)) + return(list(lower = b[1L], upper = b[2L])) + } + lower = py_get(hp, c("lower", "_lower")) + upper = py_get(hp, c("upper", "_upper")) + if (!is.null(lower) || !is.null(upper)) { + return(list(lower = as.numeric(lower), upper = as.numeric(upper))) + } + NULL + } + + hp_items = function(hp) { + it = py_get(hp, c("items", "choices", "_choices")) + if (is.null(it)) return(NULL) + unlist(it) + } + + is_bool_cat = function(items) { + if (is.null(items)) return(FALSE) + # normalize Python bools → R logical + all(sort(as.logical(items)) == sort(c(FALSE, TRUE))) + } + + # Type checks (works across API versions) + is_float_hp = function(hp) { + if (!old_cs_version) { + if (reticulate::py_is_instance(hp, ConfigSpace$Float)) return(TRUE) + } else { + hf = py_get(ConfigSpace, c("hyperparameters")) + if (!is.null(hf) && !is.null(hf$UniformFloatHyperparameter)) { + if (reticulate::py_is_instance(hp, hf$UniformFloatHyperparameter)) return(TRUE) + } + } + FALSE + } + + is_int_hp = function(hp) { + if (!old_cs_version) { + if (reticulate::py_is_instance(hp, ConfigSpace$Integer)) return(TRUE) + } else { + hf = py_get(ConfigSpace, c("hyperparameters")) + if (!is.null(hf) && !is.null(hf$UniformIntegerHyperparameter)) { + if (reticulate::py_is_instance(hp, hf$UniformIntegerHyperparameter)) return(TRUE) + } + } + FALSE + } + + is_cat_hp = function(hp) { + if (!old_cs_version) { + if (reticulate::py_is_instance(hp, ConfigSpace$Categorical)) return(TRUE) + } else { + hf = py_get(ConfigSpace, c("hyperparameters")) + if (!is.null(hf) && !is.null(hf$CategoricalHyperparameter)) { + if (reticulate::py_is_instance(hp, hf$CategoricalHyperparameter)) return(TRUE) + } + } + FALSE + } + + # conditions extraction ------------------------------------------------------ + + get_conditions = function(cs) { + if (!is.null(cs$get_conditions)) return(cs$get_conditions()) + if (!is.null(cs$conditions)) return(cs$conditions) + list() + } + + cond_child_name = function(cond) { + ch = py_get(cond, c("child", "_child")) + nm = if (is.null(ch)) NULL else py_get(ch, c("name", "_name")) + as.character(nm) + } + + cond_parent_name = function(cond) { + pa = py_get(cond, c("parent", "_parent")) + nm = if (is.null(pa)) NULL else py_get(pa, c("name", "_name")) + as.character(nm) + } + + cond_values = function(cond) { + # EqualsCondition: value / _value + v = py_get(cond, c("value", "_value", "values", "_values")) + if (is.null(v)) return(NULL) + unlist(v) + } + + is_equals_condition = function(cond) { + cond_ns = if (!is.null(ConfigSpace$conditions)) ConfigSpace$conditions else ConfigSpace + if (!is.null(cond_ns$EqualsCondition) && reticulate::py_is_instance(cond, cond_ns$EqualsCondition)) return(TRUE) + FALSE + } + + is_in_condition = function(cond) { + cond_ns = if (!is.null(ConfigSpace$conditions)) ConfigSpace$conditions else ConfigSpace + if (!is.null(cond_ns$InCondition) && reticulate::py_is_instance(cond, cond_ns$InCondition)) return(TRUE) + FALSE + } + + # Collect dependency expressions per child: child -> list(expr) + conds_raw = get_conditions(cs) + depends_map = list() + if (length(conds_raw)) { + for (cond in conds_raw) { + ch = cond_child_name(cond) + pa = cond_parent_name(cond) + if (!nzchar(ch) || !nzchar(pa)) next + vals = cond_values(cond) + + # Build an R expression like (parent == val) or (parent %in% c(...)) + if (is_equals_condition(cond) && length(vals) == 1L) { + expr = call("==", as.name(pa), vals[[1L]]) + } else if (is_in_condition(cond) || length(vals) > 1L) { + expr = call("%in%", as.name(pa), as.call(c(as.name("c"), as.list(vals)))) + } else { + # Fallback (treat as equals if single value) + expr = call("==", as.name(pa), vals[[1L]]) + } + + depends_map[[ch]] = c(depends_map[[ch]], list(expr)) + } + } + + combine_and = function(exprs) { + if (!length(exprs)) return(NULL) + out = exprs[[1L]] + if (length(exprs) == 1L) return(out) + for (k in 2:length(exprs)) { + out = call("&&", out, exprs[[k]]) + } + out + } + + # build params --------------------------------------------------------------- + + hps = get_hps(cs) + params = list() + + for (hp in hps) { + id = hp_name(hp) + if (!nzchar(id)) next + + tags = hp_meta_tags(hp) + dflt = hp_default(hp) + + if (is_float_hp(hp)) { + b = hp_bounds(hp) + if (is.null(b) || any(!is.finite(unlist(b)))) { + warning(sprintf("Float hyperparameter '%s' has missing or infinite bounds; using (-Inf, Inf).", id)) + lower = -Inf; upper = Inf + } else { + lower = b$lower; upper = b$upper + } + depends = combine_and(depends_map[[id]]) + params[[id]] = paradox::p_dbl(lower = as.numeric(lower), upper = as.numeric(upper), + default = if (!is.null(dflt)) as.numeric(dflt) else NULL, + tags = tags, depends = depends) + + } else if (is_int_hp(hp)) { + b = hp_bounds(hp) + if (is.null(b)) { + warning(sprintf("Integer hyperparameter '%s' has missing bounds; using [-.Machine$integer.max, .Machine$integer.max].", id)) + lower = - .Machine$integer.max + upper = .Machine$integer.max + } else { + lower = as.integer(b$lower); upper = as.integer(b$upper) + } + depends = combine_and(depends_map[[id]]) + params[[id]] = paradox::p_int(lower = lower, upper = upper, + default = if (!is.null(dflt)) as.integer(dflt) else NULL, + tags = tags, depends = depends) + + } else if (is_cat_hp(hp)) { + items = hp_items(hp) + if (is_bool_cat(items)) { + depends = combine_and(depends_map[[id]]) + params[[id]] = paradox::p_lgl(default = if (!is.null(dflt)) isTRUE(dflt) else NULL, + tags = tags, depends = depends) + } else { + lvls = as.character(items) + if (!length(lvls)) { + warning(sprintf("Categorical hyperparameter '%s' has no items; skipping.", id)) + next + } + def = if (!is.null(dflt)) as.character(dflt) else NULL + # ensure default is one of the levels; otherwise drop it + if (!is.null(def) && !def %in% lvls) def = NULL + depends = combine_and(depends_map[[id]]) + params[[id]] = paradox::p_fct(levels = lvls, default = def, + tags = tags, depends = depends) + } + + } else { + warning(sprintf("Unsupported hyperparameter type for '%s'; skipping.", id)) + } + } + + # assemble ParamSet ---------------------------------------------------------- + ps = do.call(paradox::ps, params) + return(ps) +} diff --git a/R/paramset_to_configspace.R b/R/paramset_to_configspace.R new file mode 100644 index 000000000..af0a1bbfc --- /dev/null +++ b/R/paramset_to_configspace.R @@ -0,0 +1,210 @@ +#' Convert a paradox ParamSet to a ConfigSpace ConfigurationSpace +#' +#' Translates a [`paradox::ParamSet`] into a Python +#' [`ConfigSpace.ConfigurationSpace`] via **reticulate**. +#' +#' Supported parameter types: +#' - `ParamDbl` => `Float` / `UniformFloatHyperparameter` +#' - `ParamInt` => `Integer` / `UniformIntegerHyperparameter` +#' - `ParamLgl` => `Categorical(TRUE/FALSE)` +#' - `ParamFct` => `Categorical` +#' +#' Utility parameters (`ParamUty`) are not representable in ConfigSpace and are skipped +#' with an explicit warning listing their IDs. +#' +#' Dependency conditions (`CondEqual`, `CondIn`) are preserved. Multiple conditions +#' on the same child are combined using `ConfigSpace.AndConjunction`. +#' +#' The function auto-detects old vs. new ConfigSpace APIs: +#' - New (>= 1.0): `ConfigSpace$Float`, `ConfigSpace$Integer`, `ConfigSpace$Categorical`, +#' `cs$add()`, `cs$add_condition()` and conditions in `ConfigSpace$conditions`. +#' - Old: `ConfigSpace$hyperparameters$*Hyperparameter`, `cs$add_hyperparameter()`, +#' `cs$add_condition()`, and conditions at top level. +#' +#' @param ps [paradox::ParamSet]\cr The parameter set to convert. +#' @param name `character(1)`\cr Optional name for the resulting ConfigurationSpace. +#' +#' @return A Python `ConfigSpace.ConfigurationSpace` object. +#' +#' @examples +#' \dontrun{ +#' ps = paradox::ps( +#' lr = p_dbl(lower = 1e-5, upper = 1, default = 0.01, tags = "train"), +#' ntree = p_int(lower = 10, upper = 500, default = 100, tags = c("train","tuning")), +#' bootstrap = p_lgl(default = TRUE, tags = "train"), +#' criterion = p_fct(levels = c("gini", "entropy", "other"), default = "gini", tags = "train"), +#' extras = p_fct(tags = "predict", levels = c("alpha","beta","gamma","delta","kappa","nu")), +#' depending = p_lgl(tags = "train", +#' depends = quote(criterion == "entropy" && extras %in% c("alpha","beta"))) +#' ) +#' cs = paramset_to_configspace(ps, name = "demo") +#' } +#' @export +paramset_to_configspace = function(ps, name = NULL) { + stopifnot(inherits(ps, "ParamSet")) + + uty_idx = which(ps$params$cls %in% c("ParamUty", "p_uty")) + if (length(uty_idx)) { + warning(sprintf( + "The following ParamUty parameter(s) cannot be converted and will be skipped: %s", + paste(ps$params$id[uty_idx], collapse = ", ") + )) + } + + # Import ConfigSpace & detect API version: + ConfigSpace = reticulate::import("ConfigSpace", delay_load = TRUE) + cs = ConfigSpace$ConfigurationSpace(name = name) + + old_cs_version = + !reticulate::py_has_attr(ConfigSpace, "Float") || + !reticulate::py_has_attr(ConfigSpace, "Integer") || + !reticulate::py_has_attr(ConfigSpace, "Categorical") + + # Helper to add entities across API versions: + add_hp = function(hp) { + if (!is.null(cs$add)) cs$add(hp) + else if (!is.null(cs$add_hyperparameter)) cs$add_hyperparameter(hp) + else if (!is.null(cs$add_hyperparameters)) cs$add_hyperparameters(list(hp)) + else stop("could not add hyperparameter to configurationspace.") + invisible(NULL) + } + + # Builders for each datatype (can handle old configspace version): + build_float = function(id, lower, upper, default, meta) { + if (old_cs_version) { + hp = ConfigSpace$hyperparameters$UniformFloatHyperparameter + hp(id, lower = as.numeric(lower), upper = as.numeric(upper), + default_value = default, meta = meta) + return(hp) + } + ConfigSpace$Float(id, bounds = c(as.numeric(lower), as.numeric(upper)), + default = default, meta = meta) + } + + build_int = function(id, lower, upper, default, meta) { + if (old_cs_version) { + hp = ConfigSpace$hyperparameters$UniformIntegerHyperparameter + hp(id, lower = as.integer(lower), upper = as.integer(upper), + default_value = if (is.null(default)) NULL else as.integer(default), + meta = meta) + return(hp) + } + ConfigSpace$Integer(id, bounds = c(as.integer(lower), as.integer(upper)), + default = if (is.null(default)) NULL else as.integer(default), + meta = meta) + } + + build_cat = function(id, choices, default, meta) { + if (old_cs_version) { + hp = ConfigSpace$hyperparameters$CategoricalHyperparameter + hp(id, choices = choices, default_value = default, meta = meta) + return(hp) + } + ConfigSpace$Categorical(id, items = choices, default = default, meta = meta) + } + + build_bool = function(id, default, meta) { + build_cat(id, c(TRUE, FALSE), default, meta) + } + + + for (i in seq_row(ps$params)) { + p = ps$params[i,] + + meta = list( + tags = p$.tags, + custom_check = if (!is.null(p$custom_check)) "present" else NULL + ) + + if (p$cls == "ParamDbl") { + if (is.null(p$lower) || is.null(p$upper) || is.infinite(p$lower) || is.infinite(p$upper)) { + warning(sprintf("ParamDbl '%s' has missing or infinite bounds; skipping.", p$id)) + next + } + if (is.null(unlist(p$default))) { + warning(sprintf("ParamDbl '%s' has no default; skipping.", p$id)) + next + } + add_hp(build_float(p$id, p$lower, p$upper, unlist(p$default), meta)) + + } else if (p$cls == "ParamInt") { + pl = if (is.infinite(p$lower)) { + warning(sprintf("ParamInt '%s' lower is infinite; using -(.Machine$integer.max). It is adviced to set the bound to a finite value", p$id)) + as.integer(-.Machine$integer.max) + } else { + p$lower + } + pu = if (is.infinite(p$upper)) { + warning(sprintf("ParamInt '%s' upper is infinite; using .Machine$integer.max. It is adviced to set the bound to a finite value", p$id)) + as.integer(.Machine$integer.max) + } else { + p$upper + } + if (is.na(pl) || is.na(pu)) { + warning(sprintf("ParamInt '%s' has missing bounds; skipping.", p$id)) + next + } + if (is.null(unlist(p$default))) { + warning(sprintf("ParamInt '%s' has no default; skipping.", p$id)) + next + } + add_hp(build_int(p$id, pl, pu, unlist(p$default), meta)) + + } else if (p$cls == "ParamLgl") { + add_hp(build_bool(p$id, as.logical(p$default), meta)) + + } else if (p$cls == "ParamFct") { + lvls = unlist(p$levels) + if (length(lvls) == 0L) { + warning(sprintf("ParamFct '%s' has no levels; skipping.", p$id)) + next + } + d = unlist(p$default) + # take first value as default if none is given in param set + # todo: make a warning here + default = if (!is.null(d) && !is.na(d)) d else { + warning(sprintf( + "ParamFct '%s' has no default; using first level '%s'.", p$id, lvls[[1]] + )) + lvls[[1]] + } + add_hp(build_cat(p$id, lvls, default, meta)) + } + } + + # Build and add dependency conditions: + if (!is.null(ps$deps) && nrow(ps$deps)) { + deps = ps$deps + combine_condition = function(x) { + assert_list(x, all.missing = FALSE) + if (length(x) < 2) return(x) + do.call(ConfigSpace$AndConjunction, unname(x)) + } + + conditions = named_list() + for (i in seq_row(deps)) { + dep = deps[i,] + # `dep$cond` is a list with only one element; see `add_dep` in paradox/r/paramset.r + # thus, we can safely extract the first element + condition = dep$cond[[1]] + + # If either HP was skipped (e.g., uty or invalid), skip its condition + if (is.null(cs[dep$id]) || is.null(cs[dep$on])) next + + if (inherits(condition, "CondEqual")) { + cond = ConfigSpace$EqualsCondition(cs[dep$id], cs[dep$on], condition$rhs) + } else { + cond = ConfigSpace$InCondition(cs[dep$id], cs[dep$on], condition$rhs) + } + conditions = c(conditions, setNames(list(cond), dep$id)) + } + + if (length(conditions)) { + grouped_conditions = split(conditions, names(conditions)) + combined_conditions = lapply(grouped_conditions, combine_condition) + add_hp(unname(combined_conditions)) + } + } + + return(cs) +} diff --git a/R/py_base_class_fastai.R b/R/py_base_class_fastai.R index adfdfe0eb..a96105e81 100644 --- a/R/py_base_class_fastai.R +++ b/R/py_base_class_fastai.R @@ -1,12 +1,12 @@ #' @title Classification Neural Network Learner #' @author annanzrv -#' @name mlr_learners_classif.fastai +#' @name mlr_learners_classif.python.fastai #' #' @description #' Simple and fast neural nets for tabular data classification. #' Calls [fastai::tabular_learner()] from package \CRANpkg{fastai}. #' -#' @templateVar id classif.fastai +#' @templateVar id classif.python.fastai #' @template learner #' #' @references @@ -17,6 +17,8 @@ LearnerPythonClassifFastai <- R6::R6Class( inherit = LearnerPythonClassif, public = list( + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. initialize = function() { p_n_epoch = p_int( @@ -265,7 +267,7 @@ LearnerPythonClassifFastai <- R6::R6Class( }, # -------------------- PREDICT HOOK -------------------- - .predict_py = function(task, newdata, predict_types) { + .predict_py = function(task, newdata, predict_type) { model = self$model$model class_labels = self$model$class_labels @@ -273,10 +275,10 @@ LearnerPythonClassifFastai <- R6::R6Class( pars_pred = self$param_set$get_values(tags = "predict") pred = invoke(fastai::predict, model, newdata, .args = pars_pred) - if ("prob" %in% predict_types) { + if ("prob" %in% predict_type) { prob = as.matrix(pred[, class_labels, drop = FALSE]) list(response = NULL, prob = prob) - } else if ("response" %in% predict_types) { + } else if ("response" %in% predict_type) { response = class_labels[pred$class + 1L] list(response = response, prob = NULL) } else { diff --git a/R/py_base_class_tabpfn.R b/R/py_base_class_tabpfn.R index e5b449806..cca7dd754 100644 --- a/R/py_base_class_tabpfn.R +++ b/R/py_base_class_tabpfn.R @@ -1,10 +1,48 @@ -# R/learner_classif_tabpfn.R - +#' @title TabPFN Classification Learner +#' @author b-zhou +#' @name mlr_learners_classif.python.tabpfn +#' +#' @description +#' Foundation model for tabular data. +#' Uses \CRANpkg{reticulate} to interface with the [`tabpfn`](https://github.com/PriorLabs/TabPFN) Python package. +#' +#' @templateVar class LearnerClassifTabPFN +#' @template sections_tabpfn +#' +#' @section Custom mlr3 parameters: +#' +#' - `categorical_feature_indices` uses R indexing instead of zero-based Python indexing. +#' +#' - `device` must be a string. +#' If set to `"auto"`, the behavior is the same as original. +#' Otherwise, the string is passed as argument to `torch.device()` to create a device. +#' +#' - `inference_precision` must be `"auto"`, `"autocast"`, +#' or a [`torch.dtype`](https://docs.pytorch.org/docs/stable/tensor_attributes.html) string, +#' e.g., `"torch.float32"`, `"torch.float64"`, etc. +#' Non-float dtypes are not supported. +#' +#' - `inference_config` is currently not supported. +#' +#' - `random_state` accepts either an integer or the special value `"None"` +#' which corresponds to `None` in Python. +#' Following the original Python implementation, the default `random_state` is `0`. +#' +#' @templateVar id classif.python.tabpfn +#' @template learner +#' +#' @references +#' `r format_bib("hollmann2025tabpfn", "hollmann2023tabpfn")` +#' +#' @template seealso_learner +#' @export LearnerPythonClassifTabPFN <- R6::R6Class( "LearnerPythonClassifTabPFN", inherit = LearnerPythonClassif, public = list( + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. initialize = function() { ps <- ps( n_estimators = p_int(lower = 1L, default = 4L, tags = "train"), @@ -119,21 +157,21 @@ LearnerPythonClassifTabPFN <- R6::R6Class( list(model = fitted, classes = classes) }, - .predict_py = function(task, newdata, predict_types) { + .predict_py = function(task, newdata, predict_type) { model = self$model$model X = as.matrix(newdata) storage.mode(X) = "double" X[is.na(X)] = NaN X_py = reticulate::r_to_py(X) - if ("prob" %in% predict_types) { + if ("prob" %in% predict_type) { prob = mlr3misc::invoke(model$predict_proba, X = X_py) prob = reticulate::py_to_r(prob) colnames(prob) = as.character(reticulate::py_to_r(model$classes_)) list(prob = prob) } - if ("response" %in% predict_types) { + if ("response" %in% predict_type) { response <- mlr3misc::invoke(model$predict, X = X_py) list(response = as.character(reticulate::py_to_r(response))) } diff --git a/attic/demo_python_learners.R b/attic/demo_python_learners.R index 84dd38551..69148d6a7 100644 --- a/attic/demo_python_learners.R +++ b/attic/demo_python_learners.R @@ -7,12 +7,18 @@ test_that("autotest", { Sys.setenv(RETICULATE_PYTHON = "managed") library(mlr3) library(mlr3extralearners) + library(checkmate) lapply(list.files(system.file("testthat", package = "mlr3"), pattern = "^helper.*\\.[rR]", full.names = TRUE), source) reticulate::py_require(c("numpy", "fastai"), python_version = "3.10") + mirai::daemons(1, .compute = "mlr3_encapsulation") + mirai::everywhere({ + Sys.setenv(RETICULATE_PYTHON = "managed") + }, .compute = "mlr3_encapsulation") + learner = lrn("classif.python.fastai", layers = c(200, 100)) expect_learner(learner, check_man = FALSE) # results not replicable, because torch seed must be set in the python backend diff --git a/man/configspace_to_paramset.Rd b/man/configspace_to_paramset.Rd new file mode 100644 index 000000000..54b299c9f --- /dev/null +++ b/man/configspace_to_paramset.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/configspace_to_paramset.R +\name{configspace_to_paramset} +\alias{configspace_to_paramset} +\title{Convert a ConfigSpace ConfigurationSpace to a paradox ParamSet} +\usage{ +configspace_to_paramset(cs) +} +\arguments{ +\item{cs}{A Python \code{ConfigSpace.ConfigurationSpace} object.} +} +\value{ +A \code{\link[paradox:ParamSet]{paradox::ParamSet}}. +} +\description{ +Translates a Python \code{\link{ConfigSpace.ConfigurationSpace}} into a +\code{\link[paradox:ParamSet]{paradox::ParamSet}} via \strong{reticulate}. +} +\details{ +Supported hyperparameter types: +\itemize{ +\item \code{Float} / \code{UniformFloatHyperparameter} => \code{ParamDbl} (via \code{p_dbl}) +\item \code{Integer} / \code{UniformIntegerHyperparameter} => \code{ParamInt} (via \code{p_int}) +\item \code{Categorical(TRUE/FALSE)} => \code{ParamLgl} (via \code{p_lgl}) +\item \code{Categorical} (other items) => \code{ParamFct} (via \code{p_fct}) +} + +Tags are restored from \code{hp$meta$tags} if present (silently ignored otherwise). +Defaults are used when present; missing defaults are left unset. + +Dependency conditions (\code{EqualsCondition}, \code{InCondition}) are mapped to a single +\code{depends = quote(...)} expression per child and combined with \code{&&} if multiple +conditions exist for the same child. + +Works with both new (>= 1.0) and old ConfigSpace APIs. +} diff --git a/man/mlr_learners_classif.fastai.Rd b/man/mlr_learners_classif.fastai.Rd index 5c14520f6..79abe069d 100644 --- a/man/mlr_learners_classif.fastai.Rd +++ b/man/mlr_learners_classif.fastai.Rd @@ -1,15 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/learner_fastai_classif_fastai.R, -% R/py_base_class_fastai.R +% Please edit documentation in R/learner_fastai_classif_fastai.R \name{mlr_learners_classif.fastai} \alias{mlr_learners_classif.fastai} \alias{LearnerClassifFastai} -\alias{LearnerPythonClassifFastai} \title{Classification Neural Network Learner} \description{ -Simple and fast neural nets for tabular data classification. -Calls \code{\link[fastai:tabular_learner]{fastai::tabular_learner()}} from package \CRANpkg{fastai}. - Simple and fast neural nets for tabular data classification. Calls \code{\link[fastai:tabular_learner]{fastai::tabular_learner()}} from package \CRANpkg{fastai}. } @@ -42,12 +37,6 @@ If no value is given, it is set to \code{fastai::accuracy()}. \section{Dictionary}{ -This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{lrn()}: - -\if{html}{\out{
}}\preformatted{lrn("classif.fastai") -}\if{html}{\out{
}} - - This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{lrn()}: \if{html}{\out{
}}\preformatted{lrn("classif.fastai") @@ -56,14 +45,6 @@ This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{ \section{Meta Information}{ -\itemize{ -\item Task type: \dQuote{classif} -\item Predict Types: \dQuote{response}, \dQuote{prob} -\item Feature Types: \dQuote{logical}, \dQuote{integer}, \dQuote{numeric}, \dQuote{factor}, \dQuote{ordered} -\item Required Packages: \CRANpkg{mlr3}, \CRANpkg{mlr3extralearners}, \CRANpkg{fastai}, \CRANpkg{reticulate} -} - - \itemize{ \item Task type: \dQuote{classif} \item Predict Types: \dQuote{response}, \dQuote{prob} @@ -73,36 +54,6 @@ This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{ } \section{Parameters}{ -\tabular{lllll}{ - Id \tab Type \tab Default \tab Levels \tab Range \cr - act_cls \tab untyped \tab - \tab \tab - \cr - bn_cont \tab logical \tab TRUE \tab TRUE, FALSE \tab - \cr - bn_final \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr - drop_last \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr - embed_p \tab numeric \tab 0 \tab \tab \eqn{[0, 1]}{[0, 1]} \cr - emb_szs \tab untyped \tab NULL \tab \tab - \cr - n_epoch \tab integer \tab 5 \tab \tab \eqn{[1, \infty)}{[1, Inf)} \cr - eval_metric \tab untyped \tab - \tab \tab - \cr - layers \tab untyped \tab - \tab \tab - \cr - loss_func \tab untyped \tab - \tab \tab - \cr - lr \tab numeric \tab 0.001 \tab \tab \eqn{[0, \infty)}{[0, Inf)} \cr - metrics \tab untyped \tab - \tab \tab - \cr - n_out \tab integer \tab - \tab \tab \eqn{(-\infty, \infty)}{(-Inf, Inf)} \cr - num_workers \tab integer \tab 0 \tab \tab \eqn{(-\infty, \infty)}{(-Inf, Inf)} \cr - opt_func \tab untyped \tab - \tab \tab - \cr - patience \tab integer \tab 1 \tab \tab \eqn{[1, \infty)}{[1, Inf)} \cr - pin_memory \tab logical \tab TRUE \tab TRUE, FALSE \tab - \cr - procs \tab untyped \tab NULL \tab \tab - \cr - ps \tab untyped \tab NULL \tab \tab - \cr - shuffle \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr - train_bn \tab logical \tab TRUE \tab TRUE, FALSE \tab - \cr - wd \tab integer \tab - \tab \tab \eqn{[0, \infty)}{[0, Inf)} \cr - wd_bn_bias \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr - use_bn \tab logical \tab TRUE \tab TRUE, FALSE \tab - \cr - y_range \tab untyped \tab NULL \tab \tab - \cr - bs \tab integer \tab 50 \tab \tab \eqn{(-\infty, \infty)}{(-Inf, Inf)} \cr -} - \tabular{lllll}{ Id \tab Type \tab Default \tab Levels \tab Range \cr act_cls \tab untyped \tab - \tab \tab - \cr @@ -135,11 +86,6 @@ This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{ } \references{ -Howard, Jeremy, Gugger, Sylvain (2020). -\dQuote{Fastai: A Layered API for Deep Learning.} -\emph{Information}, \bold{11}(2), 108. -ISSN 2078-2489, \doi{10.3390/info11020108}. - Howard, Jeremy, Gugger, Sylvain (2020). \dQuote{Fastai: A Layered API for Deep Learning.} \emph{Information}, \bold{11}(2), 108. @@ -274,119 +220,3 @@ The objects of this class are cloneable with this method. } } } -\section{Super classes}{ -\code{\link[mlr3:Learner]{mlr3::Learner}} -> \code{\link[mlr3:LearnerClassif]{mlr3::LearnerClassif}} -> \code{mlr3extralearners::LearnerPythonClassif} -> \code{LearnerPythonClassifFastai} -} -\section{Active bindings}{ -\if{html}{\out{
}} -\describe{ -\item{\code{internal_valid_scores}}{(named \code{list()} or \code{NULL}) -The validation scores extracted from \code{eval_protocol} which itself is set by fitting the \code{fastai::tab_learner}. -If early stopping is activated, this contains the validation scores of the model for the optimal \code{n_epoch}, -otherwise the \code{n_epoch} for the final model.} - -\item{\code{internal_tuned_values}}{(named \code{list()} or \code{NULL}) -If early stopping is activated, this returns a list with \code{n_epoch}, -which is the last epoch that yielded improvement w.r.t. the \code{patience}, extracted by \code{max(eval_protocol$epoch)+1}} - -\item{\code{validate}}{(\code{numeric(1)} or \code{character(1)} or \code{NULL}) -How to construct the internal validation data. This parameter can be either \code{NULL}, -a ratio, \code{"test"}, or \code{"predefined"}.} - -\item{\code{marshaled}}{(\code{logical(1)}) -Whether the learner has been marshaled.} -} -\if{html}{\out{
}} -} -\section{Methods}{ -\subsection{Public methods}{ -\itemize{ -\item \href{#method-LearnerPythonClassifFastai-new}{\code{LearnerPythonClassifFastai$new()}} -\item \href{#method-LearnerPythonClassifFastai-marshal}{\code{LearnerPythonClassifFastai$marshal()}} -\item \href{#method-LearnerPythonClassifFastai-unmarshal}{\code{LearnerPythonClassifFastai$unmarshal()}} -\item \href{#method-LearnerPythonClassifFastai-clone}{\code{LearnerPythonClassifFastai$clone()}} -} -} -\if{html}{\out{ -
Inherited methods - -
-}} -\if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-LearnerPythonClassifFastai-new}{}}} -\subsection{Method \code{new()}}{ -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{LearnerPythonClassifFastai$new()}\if{html}{\out{
}} -} - -} -\if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-LearnerPythonClassifFastai-marshal}{}}} -\subsection{Method \code{marshal()}}{ -Marshal the learner's model. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{LearnerPythonClassifFastai$marshal(...)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{...}}{(any)\cr -Additional arguments passed to \code{\link[mlr3:marshaling]{mlr3::marshal_model()}}.} -} -\if{html}{\out{
}} -} -} -\if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-LearnerPythonClassifFastai-unmarshal}{}}} -\subsection{Method \code{unmarshal()}}{ -Unmarshal the learner's model. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{LearnerPythonClassifFastai$unmarshal(...)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{...}}{(any)\cr -Additional arguments passed to \code{\link[mlr3:marshaling]{mlr3::marshal_model()}}.} -} -\if{html}{\out{
}} -} -} -\if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-LearnerPythonClassifFastai-clone}{}}} -\subsection{Method \code{clone()}}{ -The objects of this class are cloneable with this method. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{LearnerPythonClassifFastai$clone(deep = FALSE)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{deep}}{Whether to make a deep clone.} -} -\if{html}{\out{
}} -} -} -} diff --git a/man/mlr_learners_classif.python.fastai.Rd b/man/mlr_learners_classif.python.fastai.Rd new file mode 100644 index 000000000..2458ceb0c --- /dev/null +++ b/man/mlr_learners_classif.python.fastai.Rd @@ -0,0 +1,185 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/py_base_class_fastai.R +\name{mlr_learners_classif.python.fastai} +\alias{mlr_learners_classif.python.fastai} +\alias{LearnerPythonClassifFastai} +\title{Classification Neural Network Learner} +\description{ +Simple and fast neural nets for tabular data classification. +Calls \code{\link[fastai:tabular_learner]{fastai::tabular_learner()}} from package \CRANpkg{fastai}. +} +\section{Dictionary}{ + +This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{lrn()}: + +\if{html}{\out{
}}\preformatted{lrn("classif.python.fastai") +}\if{html}{\out{
}} +} + +\section{Meta Information}{ + +\itemize{ +\item Task type: \dQuote{classif} +\item Predict Types: \dQuote{response}, \dQuote{prob} +\item Feature Types: \dQuote{logical}, \dQuote{integer}, \dQuote{numeric}, \dQuote{factor}, \dQuote{ordered} +\item Required Packages: \CRANpkg{mlr3}, \CRANpkg{mlr3extralearners}, \CRANpkg{fastai}, \CRANpkg{reticulate} +} +} + +\section{Parameters}{ +\tabular{lllll}{ + Id \tab Type \tab Default \tab Levels \tab Range \cr + act_cls \tab untyped \tab - \tab \tab - \cr + bn_cont \tab logical \tab TRUE \tab TRUE, FALSE \tab - \cr + bn_final \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr + drop_last \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr + embed_p \tab numeric \tab 0 \tab \tab \eqn{[0, 1]}{[0, 1]} \cr + emb_szs \tab untyped \tab NULL \tab \tab - \cr + n_epoch \tab integer \tab - \tab \tab \eqn{[1, \infty)}{[1, Inf)} \cr + eval_metric \tab untyped \tab - \tab \tab - \cr + layers \tab untyped \tab - \tab \tab - \cr + loss_func \tab untyped \tab - \tab \tab - \cr + lr \tab numeric \tab 0.001 \tab \tab \eqn{[0, \infty)}{[0, Inf)} \cr + metrics \tab untyped \tab - \tab \tab - \cr + n_out \tab integer \tab - \tab \tab \eqn{(-\infty, \infty)}{(-Inf, Inf)} \cr + num_workers \tab integer \tab 0 \tab \tab \eqn{(-\infty, \infty)}{(-Inf, Inf)} \cr + opt_func \tab untyped \tab - \tab \tab - \cr + patience \tab integer \tab 1 \tab \tab \eqn{[1, \infty)}{[1, Inf)} \cr + pin_memory \tab logical \tab TRUE \tab TRUE, FALSE \tab - \cr + procs \tab untyped \tab NULL \tab \tab - \cr + ps \tab untyped \tab NULL \tab \tab - \cr + shuffle \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr + train_bn \tab logical \tab TRUE \tab TRUE, FALSE \tab - \cr + wd \tab integer \tab - \tab \tab \eqn{[0, \infty)}{[0, Inf)} \cr + wd_bn_bias \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr + use_bn \tab logical \tab TRUE \tab TRUE, FALSE \tab - \cr + y_range \tab untyped \tab NULL \tab \tab - \cr + bs \tab integer \tab 50 \tab \tab \eqn{(-\infty, \infty)}{(-Inf, Inf)} \cr +} +} + +\references{ +Howard, Jeremy, Gugger, Sylvain (2020). +\dQuote{Fastai: A Layered API for Deep Learning.} +\emph{Information}, \bold{11}(2), 108. +ISSN 2078-2489, \doi{10.3390/info11020108}. +} +\author{ +annanzrv +} +\section{Super classes}{ +\code{\link[mlr3:Learner]{mlr3::Learner}} -> \code{\link[mlr3:LearnerClassif]{mlr3::LearnerClassif}} -> \code{mlr3extralearners::LearnerPythonClassif} -> \code{LearnerPythonClassifFastai} +} +\section{Active bindings}{ +\if{html}{\out{
}} +\describe{ +\item{\code{internal_valid_scores}}{(named \code{list()} or \code{NULL}) +The validation scores extracted from \code{eval_protocol} which itself is set by fitting the \code{fastai::tab_learner}. +If early stopping is activated, this contains the validation scores of the model for the optimal \code{n_epoch}, +otherwise the \code{n_epoch} for the final model.} + +\item{\code{internal_tuned_values}}{(named \code{list()} or \code{NULL}) +If early stopping is activated, this returns a list with \code{n_epoch}, +which is the last epoch that yielded improvement w.r.t. the \code{patience}, extracted by \code{max(eval_protocol$epoch)+1}} + +\item{\code{validate}}{(\code{numeric(1)} or \code{character(1)} or \code{NULL}) +How to construct the internal validation data. This parameter can be either \code{NULL}, +a ratio, \code{"test"}, or \code{"predefined"}.} + +\item{\code{marshaled}}{(\code{logical(1)}) +Whether the learner has been marshaled.} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-LearnerPythonClassifFastai-new}{\code{LearnerPythonClassifFastai$new()}} +\item \href{#method-LearnerPythonClassifFastai-marshal}{\code{LearnerPythonClassifFastai$marshal()}} +\item \href{#method-LearnerPythonClassifFastai-unmarshal}{\code{LearnerPythonClassifFastai$unmarshal()}} +\item \href{#method-LearnerPythonClassifFastai-clone}{\code{LearnerPythonClassifFastai$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-LearnerPythonClassifFastai-new}{}}} +\subsection{Method \code{new()}}{ +Creates a new instance of this \link[R6:R6Class]{R6} class. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{LearnerPythonClassifFastai$new()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-LearnerPythonClassifFastai-marshal}{}}} +\subsection{Method \code{marshal()}}{ +Marshal the learner's model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{LearnerPythonClassifFastai$marshal(...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{(any)\cr +Additional arguments passed to \code{\link[mlr3:marshaling]{mlr3::marshal_model()}}.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-LearnerPythonClassifFastai-unmarshal}{}}} +\subsection{Method \code{unmarshal()}}{ +Unmarshal the learner's model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{LearnerPythonClassifFastai$unmarshal(...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{(any)\cr +Additional arguments passed to \code{\link[mlr3:marshaling]{mlr3::marshal_model()}}.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-LearnerPythonClassifFastai-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{LearnerPythonClassifFastai$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/mlr_learners_classif.python.tabpfn.Rd b/man/mlr_learners_classif.python.tabpfn.Rd new file mode 100644 index 000000000..35c4c8b91 --- /dev/null +++ b/man/mlr_learners_classif.python.tabpfn.Rd @@ -0,0 +1,228 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/py_base_class_tabpfn.R +\name{mlr_learners_classif.python.tabpfn} +\alias{mlr_learners_classif.python.tabpfn} +\alias{LearnerPythonClassifTabPFN} +\title{TabPFN Classification Learner} +\description{ +Foundation model for tabular data. +Uses \CRANpkg{reticulate} to interface with the \href{https://github.com/PriorLabs/TabPFN}{\code{tabpfn}} Python package. +} +\section{Installation}{ + +This learner relies on \CRANpkg{reticulate} to handle Python dependencies. +It is not necessary to install any Python package manually in advance or specify a Python environment +via \code{reticulate::use_python()}, \code{reticulate::use_virtualenv()}, \code{reticulate::use_condaenv()}, +or \code{reticulate::use_miniconda()}. +By calling \verb{$train()} or \verb{$predict()}, the required Python packages (\code{tapfn}, \code{torch}, etc.) will be installed +automatically, if not already. +Reticulate will then configure and initialize an ephemeral environment satisfying those requirements, +unless an existing environment (e.g., \code{"r-reticulate"}) in reticulate's +\href{https://rstudio.github.io/reticulate/articles/versions.html#order-of-discovery}{Order of Discovery} +contains all the necessary packages. + +You may also manually install \code{tabpfn} into a Python environment following the +\href{https://github.com/PriorLabs/TabPFN?tab=readme-ov-file#-quick-start}{official installation guide} +and specify the environment via \verb{reticulate::use_*()} before calling \verb{$train()} or \verb{$predict()}. +Note that the GPU version of PyTorch cannot be loaded by \code{reticulate::use_condaenv()} from a conda environment. +To use a conda environment, please install the CPU version of PyTorch. +} + +\section{Saving a Learner}{ + +In order to save a \code{LearnerClassifTabPFN} for later usage, +it is necessary to call the \verb{$marshal()} method on the \code{Learner} +before writing it to disk, as the object will otherwise not be saved correctly. +After loading a marshaled \code{LearnerClassifTabPFN} into R again, +you then need to call \verb{$unmarshal()} to transform it into a useable state. +} + +\section{Initial parameter values}{ + +\itemize{ +\item \code{n_jobs} is initialized to 1 to avoid threading conflicts with \CRANpkg{future}. +} +} + +\section{Custom mlr3 parameters}{ + +\itemize{ +\item \code{categorical_feature_indices} uses R indexing instead of zero-based Python indexing. +\item \code{device} must be a string. +If set to \code{"auto"}, the behavior is the same as original. +Otherwise, the string is passed as argument to \code{torch.device()} to create a device. +\item \code{inference_precision} must be \code{"auto"}, \code{"autocast"}, +or a \href{https://docs.pytorch.org/docs/stable/tensor_attributes.html}{\code{torch.dtype}} string, +e.g., \code{"torch.float32"}, \code{"torch.float64"}, etc. +Non-float dtypes are not supported. +\item \code{inference_config} is currently not supported. +\item \code{random_state} accepts either an integer or the special value \code{"None"} +which corresponds to \code{None} in Python. +Following the original Python implementation, the default \code{random_state} is \code{0}. +} +} + +\section{Dictionary}{ + +This \link[mlr3:Learner]{Learner} can be instantiated via \link[mlr3:mlr_sugar]{lrn()}: + +\if{html}{\out{
}}\preformatted{lrn("classif.python.tabpfn") +}\if{html}{\out{
}} +} + +\section{Meta Information}{ + +\itemize{ +\item Task type: \dQuote{classif} +\item Predict Types: \dQuote{response}, \dQuote{prob} +\item Feature Types: \dQuote{logical}, \dQuote{integer}, \dQuote{numeric} +\item Required Packages: \CRANpkg{mlr3}, \CRANpkg{mlr3extralearners}, \CRANpkg{reticulate} +} +} + +\section{Parameters}{ +\tabular{lllll}{ + Id \tab Type \tab Default \tab Levels \tab Range \cr + n_estimators \tab integer \tab 4 \tab \tab \eqn{[1, \infty)}{[1, Inf)} \cr + categorical_features_indices \tab untyped \tab - \tab \tab - \cr + softmax_temperature \tab numeric \tab 0.9 \tab \tab \eqn{[0, \infty)}{[0, Inf)} \cr + balance_probabilities \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr + average_before_softmax \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr + model_path \tab untyped \tab "auto" \tab \tab - \cr + device \tab untyped \tab "auto" \tab \tab - \cr + ignore_pretraining_limits \tab logical \tab FALSE \tab TRUE, FALSE \tab - \cr + inference_precision \tab character \tab auto \tab auto, autocast, torch.float32, torch.float, torch.float64, torch.double, torch.float16, torch.half, torch.bfloat16 \tab - \cr + fit_mode \tab character \tab fit_preprocessors \tab low_memory, fit_preprocessors, fit_with_cache \tab - \cr + memory_saving_mode \tab untyped \tab "auto" \tab \tab - \cr + random_state \tab integer \tab 0 \tab \tab \eqn{(-\infty, \infty)}{(-Inf, Inf)} \cr + n_jobs \tab integer \tab - \tab \tab \eqn{[1, \infty)}{[1, Inf)} \cr +} +} + +\references{ +Hollmann, Noah, Müller, Samuel, Purucker, Lennart, Krishnakumar, Arjun, Körfer, Max, Hoo, Bin S, Schirrmeister, Tibor R, Hutter, Frank (2025). +\dQuote{Accurate predictions on small data with a tabular foundation model.} +\emph{Nature}. +\doi{10.1038/s41586-024-08328-6}, \url{https://www.nature.com/articles/s41586-024-08328-6}. + +Hollmann, Noah, Müller, Samuel, Eggensperger, Katharina, Hutter, Frank (2023). +\dQuote{TabPFN: A transformer that solves small tabular classification problems in a second.} +In \emph{International Conference on Learning Representations 2023}. +} +\seealso{ +\itemize{ +\item \link[mlr3misc:Dictionary]{Dictionary} of \link[mlr3:Learner]{Learners}: \link[mlr3:mlr_learners]{mlr3::mlr_learners}. +\item \code{as.data.table(mlr_learners)} for a table of available \link[mlr3:Learner]{Learners} in the running session (depending on the loaded packages). +\item Chapter in the \href{https://mlr3book.mlr-org.com/}{mlr3book}: \url{https://mlr3book.mlr-org.com/basics.html#learners} +\item \CRANpkg{mlr3learners} for a selection of recommended learners. +\item \CRANpkg{mlr3cluster} for unsupervised clustering learners. +\item \CRANpkg{mlr3pipelines} to combine learners with pre- and postprocessing steps. +\item \CRANpkg{mlr3tuning} for tuning of hyperparameters, \CRANpkg{mlr3tuningspaces} for established default tuning spaces. +} +} +\author{ +b-zhou +} +\section{Super classes}{ +\code{\link[mlr3:Learner]{mlr3::Learner}} -> \code{\link[mlr3:LearnerClassif]{mlr3::LearnerClassif}} -> \code{mlr3extralearners::LearnerPythonClassif} -> \code{LearnerPythonClassifTabPFN} +} +\section{Active bindings}{ +\if{html}{\out{
}} +\describe{ +\item{\code{marshaled}}{(\code{logical(1)}) +Whether the learner has been marshaled.} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-LearnerPythonClassifTabPFN-new}{\code{LearnerPythonClassifTabPFN$new()}} +\item \href{#method-LearnerPythonClassifTabPFN-marshal}{\code{LearnerPythonClassifTabPFN$marshal()}} +\item \href{#method-LearnerPythonClassifTabPFN-unmarshal}{\code{LearnerPythonClassifTabPFN$unmarshal()}} +\item \href{#method-LearnerPythonClassifTabPFN-clone}{\code{LearnerPythonClassifTabPFN$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-LearnerPythonClassifTabPFN-new}{}}} +\subsection{Method \code{new()}}{ +Creates a new instance of this \link[R6:R6Class]{R6} class. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{LearnerPythonClassifTabPFN$new()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-LearnerPythonClassifTabPFN-marshal}{}}} +\subsection{Method \code{marshal()}}{ +Marshal the learner's model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{LearnerPythonClassifTabPFN$marshal(...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{(any)\cr +Additional arguments passed to \code{\link[mlr3:marshaling]{mlr3::marshal_model()}}.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-LearnerPythonClassifTabPFN-unmarshal}{}}} +\subsection{Method \code{unmarshal()}}{ +Unmarshal the learner's model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{LearnerPythonClassifTabPFN$unmarshal(...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{(any)\cr +Additional arguments passed to \code{\link[mlr3:marshaling]{mlr3::marshal_model()}}.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-LearnerPythonClassifTabPFN-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{LearnerPythonClassifTabPFN$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/paramset_to_configspace.Rd b/man/paramset_to_configspace.Rd new file mode 100644 index 000000000..5400ef086 --- /dev/null +++ b/man/paramset_to_configspace.Rd @@ -0,0 +1,57 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/paramset_to_configspace.R +\name{paramset_to_configspace} +\alias{paramset_to_configspace} +\title{Convert a paradox ParamSet to a ConfigSpace ConfigurationSpace} +\usage{ +paramset_to_configspace(ps, name = NULL) +} +\arguments{ +\item{ps}{\link[paradox:ParamSet]{paradox::ParamSet}\cr The parameter set to convert.} + +\item{name}{\code{character(1)}\cr Optional name for the resulting ConfigurationSpace.} +} +\value{ +A Python \code{ConfigSpace.ConfigurationSpace} object. +} +\description{ +Translates a \code{\link[paradox:ParamSet]{paradox::ParamSet}} into a Python +\code{\link{ConfigSpace.ConfigurationSpace}} via \strong{reticulate}. +} +\details{ +Supported parameter types: +\itemize{ +\item \code{ParamDbl} => \code{Float} / \code{UniformFloatHyperparameter} +\item \code{ParamInt} => \code{Integer} / \code{UniformIntegerHyperparameter} +\item \code{ParamLgl} => \code{Categorical(TRUE/FALSE)} +\item \code{ParamFct} => \code{Categorical} +} + +Utility parameters (\code{ParamUty}) are not representable in ConfigSpace and are skipped +with an explicit warning listing their IDs. + +Dependency conditions (\code{CondEqual}, \code{CondIn}) are preserved. Multiple conditions +on the same child are combined using \code{ConfigSpace.AndConjunction}. + +The function auto-detects old vs. new ConfigSpace APIs: +\itemize{ +\item New (>= 1.0): \code{ConfigSpace$Float}, \code{ConfigSpace$Integer}, \code{ConfigSpace$Categorical}, +\code{cs$add()}, \code{cs$add_condition()} and conditions in \code{ConfigSpace$conditions}. +\item Old: \verb{ConfigSpace$hyperparameters$*Hyperparameter}, \code{cs$add_hyperparameter()}, +\code{cs$add_condition()}, and conditions at top level. +} +} +\examples{ +\dontrun{ + ps = paradox::ps( + lr = p_dbl(lower = 1e-5, upper = 1, default = 0.01, tags = "train"), + ntree = p_int(lower = 10, upper = 500, default = 100, tags = c("train","tuning")), + bootstrap = p_lgl(default = TRUE, tags = "train"), + criterion = p_fct(levels = c("gini", "entropy", "other"), default = "gini", tags = "train"), + extras = p_fct(tags = "predict", levels = c("alpha","beta","gamma","delta","kappa","nu")), + depending = p_lgl(tags = "train", + depends = quote(criterion == "entropy" && extras \%in\% c("alpha","beta"))) + ) + cs = paramset_to_configspace(ps, name = "demo") +} +} diff --git a/tests/testthat/test_paramset_to_configspace.R b/tests/testthat/test_paramset_to_configspace.R new file mode 100644 index 000000000..65dfba414 --- /dev/null +++ b/tests/testthat/test_paramset_to_configspace.R @@ -0,0 +1,130 @@ +skip_if_not_installed_py("ConfigSpace") + +test_that("errors and basic input validation", { + expect_error(paramset_to_configspace(list()), "ParamSet") +}) + +test_that("converts a simple mixed ParamSet (happy path)", { + checkmate::assert_true(requireNamespace("paradox", quietly = TRUE)) + + ps = paradox::ps( + lr = paradox::p_dbl(lower = 1e-5, upper = 1, default = 0.01, tags = "train"), + ntree = paradox::p_int(lower = 10, upper = 500, default = 100, tags = c("train","tuning")), + bootstrap = paradox::p_lgl(default = TRUE, tags = "train"), + criterion = paradox::p_fct(levels = c("gini","entropy","other"), default = "gini", tags = "train") + ) + + cs = paramset_to_configspace(ps, name = "happy") + checkmate::expect_class(cs, "python.builtin.object") + + hps = cs_get_hp_names(cs) + expect_setequal(hps, c("bootstrap","criterion","lr","ntree")) +}) + + +test_that("ParamUty are skipped with explicit IDs in the warning", { + ps = paradox::ps( + a = paradox::p_dbl(lower = 0, upper = 1, default = 0.5), + u = paradox::p_uty(), + v = paradox::p_uty() + ) + expect_warning(cs <- paramset_to_configspace(ps)) +}) + + +test_that("ParamDbl with infinite or missing bounds is skipped", { + ps = paradox::ps( + good = paradox::p_dbl(lower = 0, upper = 1, default = 0.5), + infu = paradox::p_dbl(lower = 0, upper = Inf, default = 1), + infl = paradox::p_dbl(lower = -Inf, upper = 1, default = 0) + ) + cs <- suppressWarnings(paramset_to_configspace(ps)) + hps = cs$get_hyperparameter_names() + expect_true("good" %in% hps) + expect_false("infu" %in% hps) + expect_false("infl" %in% hps) +}) + + +test_that("ParamFct without default warns and takes first level", { + ps = paradox::ps( + f = paradox::p_fct(levels = c("x","y","z")) + ) + expect_warning(cs <- paramset_to_configspace(ps), "no default.*first level") + f_hp = cs$get_hyperparameter("f") + expect_setequal(f_hp$choices, c("x","y","z")) + expect_identical(f_hp$default_value, "x") +}) + + +test_that("ParamFct with zero levels warns and is skipped", { + bad = paradox::p_fct(levels = character(0)) + ps = paradox::ps(ok = paradox::p_fct(levels = c("a","b"), default = "a"), bad = bad) + expect_warning(cs <- paramset_to_configspace(ps)) + hps = cs$get_hyperparameter_names() + expect_setequal(hps, "ok") +}) + + +test_that("ParamLgl becomes a categorical {TRUE,FALSE} with appropriate default", { + ps = paradox::ps( + l = paradox::p_lgl(default = TRUE) + ) + cs = paramset_to_configspace(ps) + l_hp = cs$get_hyperparameter("l") + expect_setequal(as.logical(l_hp$choices), c(TRUE, FALSE)) + expect_identical(as.logical(l_hp$default_value), TRUE) +}) + + +test_that("Dependencies are added and combined with AndConjunction where applicable", { + ps = paradox::ps( + crit = paradox::p_fct(levels = c("gini","entropy"), default = "gini"), + extras = paradox::p_fct(levels = c("alpha","beta","gamma"), default = "alpha"), + dep = paradox::p_lgl(depends = quote(crit == "entropy" && extras %in% c("alpha","beta"))) + ) + cs = paramset_to_configspace(ps) + + conds = cs$get_conditions() + if (!is.null(conds)) { + expect_length(conds, 1L) + } + + if (!is.null(cs$sample_configuration())) { + configs = replicate(20, cs$sample_configuration(), simplify = FALSE) + ok = TRUE + for (cfg in configs) { + crit_val = as.character(cfg$get("crit")) + extras_val = as.character(cfg$get("extras")) + has_dep = !is.null(cfg$get("dep")) + cond_ok <- (crit_val == "entropy" && extras_val %in% c("alpha","beta")) + # If dep present but conditions false -> violation + if (isTRUE(has_dep) && !cond_ok) ok = FALSE + } + expect_true(ok) + } +}) + + +test_that("Warnings for missing defaults are emitted (ParamDbl) but conversion continues", { + ps <- paradox::ps( + x = paradox::p_dbl(lower = 0, upper = 1), # no default + y = paradox::p_dbl(lower = 0, upper = 1, default = 0.8) # with default + ) + expect_warning(cs <- paramset_to_configspace(ps)) + hps = cs$get_hyperparameter_names() + expect_true("y" %in% hps) +}) + + +test_that("Name argument is forwarded to ConfigurationSpace", { + ps = paradox::ps(a = paradox::p_int(lower = 0, upper = 11, default = 8)) + cs = paramset_to_configspace(ps, name = "mine") + + # Try several name attributes across several ConfigSpace versions + nms <- c("name", "_name", "configuration_space", "__name__") + got <- NULL + for (nm in nms) if (!is.null(cs[[nm]])) { got <- cs[[nm]]; break } + # Some ConfigSpace versions don't expose name, so only assert if available + if (!is.null(got)) expect_match(as.character(got), "mine") +}) \ No newline at end of file