pints icon indicating copy to clipboard operation
pints copied to clipboard

Change set_hyperparameters to accept a dictionary and named arguments

Open MichaelClerx opened this issue 3 years ago • 5 comments

At the moment we've got a class TunableMethod that samplers and optimisers and anything else can implement, that provides two methods:

  n_hyper_parameters -> int
  set_hyper_parameters(x)

where x is an array of scalars. This is really useful as it means you can treat the hyper parameters as a 1d vector of parameters to optimise, and @martinjrobins has been doing this in performance testing.

For functional testing, @fcooper8472 has proposed something like this:

def test_haario_bardenet_acmc_on_two_dim_gaussian():
    problem = RunMcmcMethodOnTwoDimGaussian(
        method=pints.HaarioBardenetACMC,
        n_chains=3,
        n_iterations=4000,
        n_warmup=1000
    )

    return {
        'kld': problem.estimate_kld(),
        'mean-ess': problem.estimate_mean_ess()
    }

Here, it'd be really good if we could pass in an extra argument after n_warmup that would be e.g. an array of hyperparameters. The internal code would then (1) figure out if it's a single or multi-chain method, (2) call set_hyperparameters on each.

Solved!

But it does mean that

  1. you need to remember the order the hyperparameters are in, when writing the test; and
  2. you need to set each hyperparameter, even the ones that are always some magic number (e.g. 7 in bfgs)

So I was thinking it'd be good to have a set_hyperparameters that takes a dictionary, or named arguments, as input. That way the code above could be e.g.

    problem = RunMcmcMethodOnTwoDimGaussian(
        method=pints.HaarioBardenetACMC,
        n_chains=3,
        n_iterations=4000,
        n_warmup=1000,
        hyper_parameters={'mu': 1, 'sigma': 2}
    )

or

    problem = RunMcmcMethodOnTwoDimGaussian(
        method=pints.HaarioBardenetACMC,
        n_chains=3,
        n_iterations=4000,
        n_warmup=1000,
        hyper_parameters={'mu': 1, 'sigma': 2, 'special_number_thats_usually_six': 12}
    )

for a particularly nasty problem.

Seems to me this will be much easier to use? An additional bonus would be that we solve @ben18785 's (justified) annoyance with setting hyperparameters on a controller.

Instead of

for x in controller.samplers():
    x.set_barry(5)

we could do

controller.set_hyper_parameters(barry=5)

and

params = {'barry': 5}
controller.set_hyper_parameters(**params)

(which would work via arbitrary keyword args: def set_hyper_parameters(**kwargs) gets kwargs as a dictionary)


Proposal 1

We update tunable method to:

    def set_hyper_parameter_array(self, x):
        """
        Sets the hyper-parameters for the method with the given vector of
        values (see :class:`TunableMethod`).

        Parameters
        ----------
        x
            An array of length ``n_hyper_parameters`` used to set the
            hyper-parameters.
        """
        pass

    def set_hyper_parameters(self, **kwargs):
        """
        Sets the hyper-parameters for the method using keyword
        arguments (see :class:`TunableMethod`).

        Examples::
        
            tunable_method.set_hyper_parameters(x=3, y=4)
            
            parameters = {'x': 3}
            tunable_method.set_hyper_parameters(**parameters)

        The hyper-parameters that can be set depend on the class implementing
        :class:`TunableMethod`. Setting hyper-parameters that do not exist will
        result in a ``ValueError`` being raised.
        """
        pass

Proposal 2

Slightly more confusing, but perhaps better

    def set_hyper_parameter(self, _array=None, **kwargs):
        """
        Sets the hyper-parameters for the method with a given vector of
        values or keyword arguments (see :class:`TunableMethod`).

        Parameters
        ----------
        _array
            An optional array of length ``n_hyper_parameters`` used to set the
            hyper-parameters.
        **
            Optional keyword arguments to set specific hyper-parameters. (If both
            a vector an keyword arguments are given, the array arguments will be
            set first, and then overwritten by the keyword arguments).

        Examples::
        
            tunable_method.set_hyper_parameters([3, 4])
        
            tunable_method.set_hyper_parameters(x=3, y=4)
            
            parameters = {'x': 3}
            tunable_method.set_hyper_parameters(**parameters)

        The hyper-parameters that can be set depend on the class implementing
        :class:`TunableMethod`. Setting hyper-parameters that do not exist will
        result in a ``ValueError`` being raised.
        """
        pass

Further changes

The controller would get some extra method set_hyper_parameters(**kwargs) that gets kwargs as a dict, it can then just pass this on to the appropriate sampler or samplers via sampler.set_hyper_parameters(**kwargs) (where the ** unpacks the dict into keyword arguments again).

Similarly, the functional tests would get a constructor argument hyper_parameters that they could then pass to the controller using controller.set_hyper_parameters(**hyper_parameters)

MichaelClerx avatar Mar 16 '21 10:03 MichaelClerx

@ben18785 @martinjrobins @fcooper8472 and any others, please have a look and see if this would work for you?

I think anyone can implement it, after that :D

MichaelClerx avatar Mar 16 '21 10:03 MichaelClerx

Thanks @chonlei -- I really like this! I think this makes setting hyperparameters much neater.

My preference would be to go for proposal 1 since it's safer.

ben18785 avatar Mar 16 '21 12:03 ben18785

I agree @chonlei , you should implement this immediately!

MichaelClerx avatar Mar 16 '21 12:03 MichaelClerx

Haha. I have no idea why I wrote @chonlei there! Sorry @MichaelClerx (I think I must have written "@C" then chose "h" rather than "l).

ben18785 avatar Mar 16 '21 12:03 ben18785

I was hoping Chon would be too busy to notice and just start coding

MichaelClerx avatar Mar 16 '21 13:03 MichaelClerx