1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
#' @title Create model multiplexer for model selection to tune over multiple
#' possible models.
#'
#' @description
#' Combines multiple base learners by dispatching
#' on the hyperparameter \dQuote{selected.learner} to a specific model class.
#' This allows to tune not only the model class (SVM, random forest, etc) but also
#' their hyperparameters in one go. Combine this with [tuneParams] and
#' [makeTuneControlIrace] for a very powerful approach, see example below.
#'
#' The parameter set is the union of all (unique) base learners. In order to
#' avoid name clashes all parameter names are prefixed with the base learner id,
#' i.e. `learnerId.parameterName`.
#'
#' The predict.type of the Multiplexer is inherited from the predict.type of the
#' base learners.
#'
#' The getter [getLearnerProperties] returns the properties of the
#' selected base learner.
#'
#' @param base.learners ([list` of [Learner])\cr
#' List of Learners with unique IDs.
#' @return ([ModelMultiplexer]). A [Learner] specialized as `ModelMultiplexer`.
#' @aliases ModelMultiplexer
#' @family multiplexer
#' @family tune
#' @export
#' @note Note that logging output during tuning is somewhat shortened to make it
#' more readable. I.e., the artificial prefix before parameter names is
#' suppressed.
#' @examples
#' \dontshow{ if (requireNamespace("kernlab")) \{ }
#' set.seed(123)
#' \donttest{
#' library(BBmisc)
#' bls = list(
#' makeLearner("classif.ksvm"),
#' makeLearner("classif.randomForest")
#' )
#' lrn = makeModelMultiplexer(bls)
#' # simple way to contruct param set for tuning
#' # parameter names are prefixed automatically and the 'requires'
#' # element is set, too, to make all paramaters subordinate to 'selected.learner'
#' ps = makeModelMultiplexerParamSet(lrn,
#' makeNumericParam("sigma", lower = -10, upper = 10, trafo = function(x) 2^x),
#' makeIntegerParam("ntree", lower = 1L, upper = 500L)
#' )
#' print(ps)
#' rdesc = makeResampleDesc("CV", iters = 2L)
#' # to save some time we use random search. but you probably want something like this:
#' # ctrl = makeTuneControlIrace(maxExperiments = 500L)
#' ctrl = makeTuneControlRandom(maxit = 10L)
#' res = tuneParams(lrn, iris.task, rdesc, par.set = ps, control = ctrl)
#' print(res)
#'
#' df = as.data.frame(res$opt.path)
#' print(head(df[, -ncol(df)]))
#'
#' # more unique and reliable way to construct the param set
#' ps = makeModelMultiplexerParamSet(lrn,
#' classif.ksvm = makeParamSet(
#' makeNumericParam("sigma", lower = -10, upper = 10, trafo = function(x) 2^x)
#' ),
#' classif.randomForest = makeParamSet(
#' makeIntegerParam("ntree", lower = 1L, upper = 500L)
#' )
#' )
#'
#' # this is how you would construct the param set manually, works too
#' ps = makeParamSet(
#' makeDiscreteParam("selected.learner", values = extractSubList(bls, "id")),
#' makeNumericParam("classif.ksvm.sigma", lower = -10, upper = 10, trafo = function(x) 2^x,
#' requires = quote(selected.learner == "classif.ksvm")),
#' makeIntegerParam("classif.randomForest.ntree", lower = 1L, upper = 500L,
#' requires = quote(selected.learner == "classif.randomForst"))
#' )
#'
#' # all three ps-objects are exactly the same internally.
#' }
#' \dontshow{ \} }
makeModelMultiplexer = function(base.learners) {
lrn = makeBaseEnsemble(
id = "ModelMultiplexer",
base.learners = base.learners,
bls.type = NULL,
ens.type = NULL,
cl = "ModelMultiplexer"
)
# the super contructor checks that all predict.types are same
# now inherit this type from the base.learners for the MM
lrn = setPredictType(lrn, lrn$base.learners[[1L]]$predict.type)
# add extra param to parset, after we did all checks and so on in the base function
ps = makeParamSet(makeDiscreteLearnerParam("selected.learner", values = names(lrn$base.learners)))
lrn$par.set = c(lrn$par.set, ps)
lrn$par.set.ens = ps
lrn$fix.factors.prediction = TRUE
setHyperPars(lrn, selected.learner = names(lrn$base.learners)[1L])
}
#' @export
trainLearner.ModelMultiplexer = function(.learner, .task, .subset, .weights = NULL, selected.learner, ...) {
# train selected learner model and remove prefix from its param settings
bl = .learner$base.learners[[selected.learner]]
m = train(bl, task = .task, subset = .subset, weights = .weights)
makeChainModel(next.model = m, cl = "ModelMultiplexerModel")
}
#' @export
predictLearner.ModelMultiplexer = function(.learner, .model, .newdata, ...) {
# simply predict with the model
sl = .learner$par.vals$selected.learner
bl = .learner$base.learners[[sl]]
# we need to pass the changed setting of the base learner for the predict function further down
args = list(.learner = bl, .model = .model$learner.model$next.model, .newdata = .newdata)
args = c(args, getHyperPars(bl, for.fun = c("predict", "both")))
do.call(predictLearner, args)
}
#' @export
makeWrappedModel.ModelMultiplexer = function(learner, learner.model, task.desc, subset, features, factor.levels, time) {
x = NextMethod()
if (!isFailureModel(x)) {
x = addClasses(x, "ModelMultiplexerModel")
}
return(x)
}
#' @export
getLearnerModel.ModelMultiplexerModel = function(model, more.unwrap = FALSE) {
if (inherits(model$learner.model, "NoFeaturesModel")) {
return(model$learner.model)
}
if (more.unwrap) {
model$learner.model$next.model$learner.model
} else {
model$learner.model$next.model
}
}
#' @export
isFailureModel.ModelMultiplexerModel = function(model) {
NextMethod() || (!inherits(model$learner.model, "NoFeaturesModel") && isFailureModel(model$learner.model$next.model))
}
|