-
-
Notifications
You must be signed in to change notification settings - Fork 7
Open
Description
During an insightful conversation with Franz, Raphael and Jakob we were coming to the issue that our $trafo
is missing the information about its image, i.e. the parameter set that it maps to. This information would be useful e.g. for the "autotuner" (i.e. a tuning wrapper for a Learner), because the autotuner would like to know what parameters the user should not set (because the tuner is doing that). My idea for the solution for this would mostly be an extension of my suggestion in #215. To avoid confusion with the old $trafo
slot, I am going to use different slot names for the new things I introduce, although one of these could well be named $trafo
.
I think this is a cool design, for whatever that may be worth to you ;-)
The plan:
- Remove the current
ps$trafo
slot. ParamSet
gets a methodps$transform(x, context = list(), terminal = TRUE)
that takes a named listx
that is a valid parameter configuration according to theParamSet
, and returns a named list with transformed parameter values. (Ignorecontext
andterminal
for now).ParamSet
gets a methodps$image(terminal = TRUE)
that returns aParamSet
. This is theParamSet
that all values ofps$transform()
will conform to. (In fact,ParamSet
checks that the return value oftransform()
conforms to this image and throws an error if it doesn't).- The
ps$get_values()
method ofParamSet
is extended with the parametertransformed = TRUE
andcontext = list()
.ps$get_values(transformed = FALSE)
behaves just asps$get_values()
does currently. Ifps$get_values(transformed = TRUE, context = ctx)
is called, it returns the same asps$get_values(transformed = FALSE) %>% ps$transform(context = ctx)
. We could argue about renaming the functionget_transformed_values()
or something, or having both functions and removing thetransformed
parameter ParamSet
gets a methodps$add_trafo(trafo, new_ps)
.trafo
is afunction(x, param_set, context)
,new_ps
is aParamSet
. What it does is that it "transmutes"ps
into theParamSet
given in thenew_ps
argument. Given thatps
does not have a "trafo" yet, thetrafo
function then takes inputs according tonew_ps
and gives outputs according tops
, i.e. the oldps
becomes its image. Some examples:# given: # * ps (ParamSet) that DOES NOT HAVE A TRAFO # * new_ps (ParamSet) # * trafo (function(x, param_set, context)) # * x (named list) ps_old = ps$clone(deep = TRUE) # keep the old ps for comparison ps$add_trafo(trafo, new_ps) # from the outside, ps now looks like param_set all.equal(ps$params, new_ps$params) # TRUE # the `ps$transform()` function calls the `trafo` function all.equal(trafo(x = x, param_set = ps, context = list()), ps$transform(x = x, context = list())) # TRUE # the `ps$image()` is just the "old" ParamSet all.equal(ps$image()$params, ps_old$params) # TRUE
- What if a
ps
already has a "trafo" and another one is added? It just stacks! In that case, the trafo that was added later is called first, then the earlier trafo is called, etc. Think of the differentParamSet
s as a linked list, connected by "trafo"-functions, the image of each is the preimage of the next. This is where the "terminal
" comes in: We can choose to apply all transformations of aParamSet
in a row, or just one transformation to go one "step" ahead. Similarly, we can get the "terminal" image, i.e. of the last image, or just the image of one transformation step. In code:# given: # * ps_one, ps_two, ps_three (ParamSet that DO NOT HAVE A TRAFO) # * trafo_one_two, trafo_two_three (function(x, param_set, context)) # * x, y, z (named lists) ps = ps_three$clone(deep = TRUE) ps$add_trafo(trafo_two_three, ps_two$clone(deep = TRUE)) ps$add_trafo(trafo_one_two, ps_one$clone(deep = TRUE)) # from the outside, ps now looks like ps_one all.equal(ps$params, ps_one$params) # TRUE # images: ps_three is the "terminal" one, but ps_two is the "next" one all.equal(ps$image()$params, ps_three$params) # TRUE all.equal(ps$image(terminal = FALSE)$params, ps_two$params) # TRUE # can go along the linked list to reach terminal all.equal(ps$image(terminal = FALSE)$image(terminal = FALSE)$params, ps_three$params) # TRUE # ps_three does not have a "trafo", btw, so its image is just itself all.equal(ps_three$image()$params, ps_three$params) # TRUE # trafos: ps$transform() calls trafo_one_two, then trafo_two_three # but only if "terminal" is TRUE all.equal(ps$transform(x = x, context = list()), trafo_one_two(x = x, param_set = ps, context = list()) %>% trafo_two_three(param_set = ps$ image(terminal = FALSE), context = list())) # TRUE all.equal(ps$transform(x = x, context = list(), terminal = FALSE), trafo_one_two(x = x, param_set = ps, context = list()) # we could also go along the linked list here: all.equal(ps$transform(x = x, context = list()), ps$transform(x = x, context = list(), terminal = FALSE) %>% ps$image(terminal = FALSE)$ transform(x = x, context = list(), terminal = FALSE)) # TRUE # ps_three does not have a "trafo", so its `$transform()` is the identity all.equal(ps_three$transform(x = x, context = list()), x)
- What about the
context()
? It can optionally be given to theps$transform()
function as an argument, and it will be passed on to thetrafo
function given tops$add_trafo()
. It can contain information about how the transformation is to be performed. It could, for example, contain information about a task (number of features, number of samples), and thetrafo
could then make use of this information to transform a parameter value. This will work together with a convention that eachLearner
will always callps$get_values(transformed = TRUE, context = list(task = task))
. Now what happens is the following:- The learner is created with a vanilla
ParamSet
, sops$get_values(transformed = TRUE, [...])
when called in theLearner
's$train()
function just gives the parameter values as given by the user. - If the user wants to add a transformation to the
ParamSet
, he callslearner$param_set$add_trafo(....)
. This changes how theParamSet
looks to the user at the outside. For example, maybe the newParamSet
contains amtry_pexp
parameter, while theLearner
's originalParamSet
only had anmtry
parameter. - When the
Learner
now callsps$get_values(transformed = TRUE, [...])
, the result will be conforming to theParamSet
that theLearner
was created with (becauseget_values
in this case gives a value conforming to the$image
). - Because
context = list(task = task)
is given to the$get_values()
, and hence to thetrafo()
function, the transformation can depend on properties of the task. It could, for example, dox$mtry = context$task$nfeat ^ x$mtry_pexp`
- There may be other contexts, for example inside a prediction-aggregating
PipeOp
. These pipeops can callget_values
with a differentcontext
argument. How they callget_values
should be documented, so the user can choose to use$add_trafo()
in a way that makes use of all information available. I am not sure yet if it is possible to build this behaviour into the (Learner
,PipeOp
, ...) class in some way to make it consistent, e.g. for allLearner
s context
is basically what I calledenv
in Parameter transformations inside ParamSet #215 / Ps Transformations inside ParamSet #225
- The learner is created with a vanilla
- It should be noted that this is a transformation that can be both performed at the learner-side or at the tuner-side. I.e. if I have a
Learner
with parameters that I want to tune over, but with transformed values (saytune_ps
, and trafotune_trafo
), I can do either of the following:- Transformation happens in
Learner
learner$param_set$add_trafo(tune_trafo, tune_ps) tune_learner(lrn = learner, ps = tune_ps, [...])
- Transformation happens in the tuner
Either of these could make sense in their own right. (i) is relevant if transformation should be task-dependent, (ii) is relevant if the tuning result parameters should be in a form that is naturally understandable to someone familiar with the learner.total_tune_ps = learner$param_set$clone(deep = TRUE) total_tune_ps$add_trafo(tune_trafo, tune_ps) tune_learner(lrn = learner, ps = total_tune_ps, [...])
- Transformation happens in
- I am thinking about whether there should be an
ps$add_trafo(trafo, preimage_ps, image_ps)
function, so that we can add a transformation just on a subset of theParamSet
. E.g. if theParamSet
has the parametersmtry
,n.tree
and we just want to add a trafo formtry
, we could doAnd theps$add_trafo(function(x, ...) x$mtry = round(exp(mtry), preimage_ps = ParamSet$new(ParamDbl$new("mtry", 0, 10)), image_ps = ParamSet$new(ParamInt$new("mtry", 0, Inf)))
trafo
function would only be called with the"mtry"
part of the input parameter value. This would make subsetting easy. But that is a story for a different time :-)
Metadata
Metadata
Assignees
Labels
No labels