diff --git a/NAMESPACE b/NAMESPACE index 3f322281a..481c0c28c 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) @@ -78,6 +80,8 @@ export(LearnerDensNonparametric) export(LearnerDensPenalized) export(LearnerDensPlugin) export(LearnerDensSpline) +export(LearnerPythonClassifFastai) +export(LearnerPythonClassifTabPFN) export(LearnerRegrAbess) export(LearnerRegrBart) export(LearnerRegrBlockForest) @@ -153,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/NEWS.md b/NEWS.md index 5f211a4de..1854d877f 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 new file mode 100644 index 000000000..3e9b1823c --- /dev/null +++ b/R/LearnerPythonClassif.R @@ -0,0 +1,136 @@ +LearnerPythonClassif = R6::R6Class( + "LearnerPythonClassif", + inherit = mlr3::LearnerClassif, + + public = list( + initialize = function(id, + feature_types = c("logical","integer","numeric","factor","ordered"), + predict_types = c("response", "prob"), + param_set = ps(), + properties = character(), + packages = "reticulate", + label = NA_character_, + man = NA_character_, + # Python requirements + py_packages, + python_version) { + + super$initialize( + id = id, + feature_types = feature_types, + predict_types = predict_types, + param_set = param_set, + properties = properties, + packages = union(c("mlr3", "mlr3extralearners"), packages), + label = label, + man = man + ) + private$.py_packages = py_packages + private$.py_version = python_version + }, + + + py_requirements = function(rhs) { + assert_ro_binding(rhs) + list( + packages = private$.py_packages, + python_version = private$.py_version + ) + } + ), + + private = list( + .py_packages = NULL, .py_version = NULL, + + .train = function(task) { + py_requirements = self$py_requirements() + do.call(assert_python_packages, py_requirements) + + out = named_list() + + fit = private$.train_py(task = task) + assert_list(fit, all.missing = FALSE, min.len = 1) + assert_names(names(fit), must.include = "model") + + structure( + mlr3misc::insert_named(out, fit), + class = c("pybytes_model", paste0(self$id, "_model")) + ) + }, + + .predict = function(task) { + py_requirements = self$py_requirements() + do.call(assert_python_packages, py_requirements) + + newdata = ordered_features(task, self) + preds = private$.predict_py(task = task, newdata = newdata, predict_type = self$predict_type) + preds + }, + + # ---- subclass hooks ---- + .train_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, ...) { + reticulate::py_require(model$py_modules, python_version = model$py_version) + pickle = reticulate::import("pickle") + + raw = as.raw(pickle$dumps(model$model)) + + 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( + 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)) + + classes <- if (is.null(model$learner_class)) { + "pybytes_model" + } else { + c("pybytes_model", model$learner_class) + } + meta_data = setdiff(model, "marshaled") + out = list( + model = fitted, + learner_class = classes, + py_modules = model$py_modules, + py_version = model$py_version + ) + + structure( + 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..e2773ecaf --- /dev/null +++ b/R/configspace_to_paramset.R @@ -0,0 +1,278 @@ +#' 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/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/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/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 new file mode 100644 index 000000000..a96105e81 --- /dev/null +++ b/R/py_base_class_fastai.R @@ -0,0 +1,332 @@ +#' @title Classification Neural Network Learner +#' @author annanzrv +#' @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.python.fastai +#' @template learner +#' +#' @references +#' `r format_bib("howard_2020")` +#' @export +LearnerPythonClassifFastai <- R6::R6Class( + "LearnerPythonClassifFastai", + inherit = LearnerPythonClassif, + + public = list( + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. + 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" + ) + }, + #' @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 -------------------- + .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() + 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 + pars = self$param_set$get_values(tags = "train") + 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(task$truth()) - 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, + eval_protocol = eval_protocol, + class_labels = task$class_names + ) + }, + + # -------------------- PREDICT HOOK -------------------- + .predict_py = function(task, newdata, predict_type) { + 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) { + prob = as.matrix(pred[, class_labels, drop = FALSE]) + list(response = NULL, prob = prob) + } else if ("response" %in% predict_type) { + response = class_labels[pred$class + 1L] + list(response = response, prob = NULL) + } else { + list() + } + }, + + # ---------------- Validation extractors ---------------- + .extract_internal_tuned_values = function() { + 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$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..cca7dd754 --- /dev/null +++ b/R/py_base_class_tabpfn.R @@ -0,0 +1,182 @@ +#' @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"), + 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", + 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 marshaled (`logical(1)`) + #' Whether the learner has been marshaled. + marshaled = function() { + learner_marshaled(self) + } + ), + + private = list( + .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 + 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() + } + # 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) + } + + 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, classes = classes) + }, + + .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_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_type) { + response <- mlr3misc::invoke(model$predict, X = X_py) + list(response = as.character(reticulate::py_to_r(response))) + } + } + ) +) + +.extralrns_dict$add("classif.python.tabpfn", LearnerPythonClassifTabPFN) diff --git a/R/python_proc.R b/R/python_proc.R new file mode 100644 index 000000000..f0d4599a8 --- /dev/null +++ b/R/python_proc.R @@ -0,0 +1,99 @@ +#-------------------------- 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( + 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) +} 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..69148d6a7 --- /dev/null +++ b/attic/demo_python_learners.R @@ -0,0 +1,282 @@ +skip_on_os("windows") +skip_if_not_installed("fastai") +skip_if_not_installed("reticulate") + +test_that("autotest", { + expect_true(callr::r(function() { + 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 + result = run_autotest(learner, check_replicable = FALSE) + testthat::expect_true(result, info = result$error) + TRUE + })) +}) + +test_that("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", { + 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", { + 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", { + 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 + })) +}) 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.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.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.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.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.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 287b3035b..79abe069d 100644 --- a/man/mlr_learners_classif.fastai.Rd +++ b/man/mlr_learners_classif.fastai.Rd @@ -62,7 +62,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_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.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/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_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.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_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}{ 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/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/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 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