-
-
Notifications
You must be signed in to change notification settings - Fork 404
/
ModelMultiplexer.R
142 lines (136 loc) · 5.41 KB
/
ModelMultiplexer.R
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
#' @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
#' 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.
#' }
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))
}