-
-
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$trafoslot. ParamSetgets a methodps$transform(x, context = list(), terminal = TRUE)that takes a named listxthat is a valid parameter configuration according to theParamSet, and returns a named list with transformed parameter values. (Ignorecontextandterminalfor now).ParamSetgets a methodps$image(terminal = TRUE)that returns aParamSet. This is theParamSetthat all values ofps$transform()will conform to. (In fact,ParamSetchecks that the return value oftransform()conforms to this image and throws an error if it doesn't).- The
ps$get_values()method ofParamSetis extended with the parametertransformed = TRUEandcontext = 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 thetransformedparameter ParamSetgets a methodps$add_trafo(trafo, new_ps).trafois afunction(x, param_set, context),new_psis aParamSet. What it does is that it "transmutes"psinto theParamSetgiven in thenew_psargument. Given thatpsdoes not have a "trafo" yet, thetrafofunction then takes inputs according tonew_psand gives outputs according tops, i.e. the oldpsbecomes 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
psalready 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 differentParamSets 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 aParamSetin 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 thetrafofunction 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 thetrafocould then make use of this information to transform a parameter value. This will work together with a convention that eachLearnerwill 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 theParamSetlooks to the user at the outside. For example, maybe the newParamSetcontains amtry_pexpparameter, while theLearner's originalParamSetonly had anmtryparameter. - When the
Learnernow callsps$get_values(transformed = TRUE, [...]), the result will be conforming to theParamSetthat theLearnerwas created with (becauseget_valuesin 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_valueswith a differentcontextargument. How they callget_valuesshould 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 allLearners contextis basically what I calledenvin 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
Learnerwith 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 theParamSethas the parametersmtry,n.treeand 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)))
trafofunction 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