When doing a grid-search over some hyper-parameters of an algorithm I often 
need to specify the possible value space for each parameter and then evaluate 
on every combination of these values.
In this case, the product function of the itertools module is handy:

    from itertools import product
    
    def train_and_evaluate(lr, num_layers, dataset):
        print(f'training on {dataset} with {num_layers=} and {lr=}')
    
    params = (
                (0.01, 0.1, 0.5), #learning rate
                range(10, 40, 10), #number of hidden neurons
                ('MNLI', 'SNLI', 'QNLI') # training dataset
    )
    
    for config in product(*params):
        train_and_evaluate(*config)

However, this code relies on the order of parameters in the function 
train_and_evaluate, which could be considered not very pythonic, as explicit is 
better than implicit. 
In the same way, the 'intention' for the values is only clear with the context 
of the function signature (or the comments, if someone bothered to write them).

Therefore I propose a new variant of the product() function that supports 
Mappings instead of Iterables to enable to explicitly specify the function of 
an iterables values in the product. A trivial implementation could be:

    def named_product(repeat=1, **kwargs):
        for combination in itertools.product(*kwargs.values(), repeat=repeat):
        yield dict(zip(kwargs.keys(), combination))

which could then be used like this:

    params = {
        'lr': (0.01, 0.1, 0.5), #learning rate
        'num_layers': range(10, 40, 10), #number of hidden neurons
        'dataset': ('MNLI', 'SNLI', 'QNLI') # training dataset
    }
    
    for config in named_product(**params):
        train_and_evaluate(**config)

This has the advantage that the order of the parameters in the kwargs does not 
depend on the method signature of train_and_evaluate.

I also would appreciate your input on the following questions:

- Support scalar values?
    I the example use-case it may be nice to not have each kwarg to be an 
iterable, because you may want to specify all parameters to the function, even 
if they do not vary in the product items (factors?)

        params = named_product(a=(1, 2), b=3)

    instead of

        params = named_product(a=(1, 2), b=(3, ))

    However, this may be unexpected to users when they supply a string which 
would then result in an iteration over its characters.

- Would this may be suited for the more-itertools package?
    However, I would love to have this build-in and not depend on a non-stdlib 
module

- More functionality:
    In my toolbox-package I implemented a variant with much more functionality: 
[https://py-toolbox.readthedocs.io/en/latest/modules/itertools.html](https://py-toolbox.readthedocs.io/en/latest/modules/itertools.html).
 This version offers additional functionality compared to the proposed function:
    - accept both, a dict as *arg or **kwargs and merges them
    - accepts scalar values, not only iterables but treats strings as scalars
    - if the value of any item is a dict itself (not scalar or Sequence), 
iterate over the nested dict as a seperate call to named_product would and 
update the 'outer' dict with those values
    - copies values before yielding which is useful if the returned values are 
mutable (can be excluded)

Maybe some of these features would also be useful for a general version? 
However, this version is mostly developed for my special use-case and some 
behaviour is solely based on my peculiarities

## Example usage:
Here is an example from my actual code which may clarify some decisions on 
functionality of the features of 
[https://py-toolbox.readthedocs.io/en/latest/modules/itertools.html](https://py-toolbox.readthedocs.io/en/latest/modules/itertools.html)

    configs = named_product({
        'nb_epoch': 3,
        'acquisition_iterations': 0, 
        'training_function': {
            partial(train_bert, decoder_function=create_decoder):{
                'decoder_name': 'FFNN'
            },
            partial(train_bert, decoder_function=create_cnn_decoder):{
                'decoder_name': 'CNN'
            }
        },
        'training_data': partial(load_glue, initial_training_data_size=10),
        'number_queries': 100,
        'batch_size': 64,
        'dataset_class': [QNLIDataset, MNLIDataset],
        'num_frozen_layers': 0,
        'acquire_function': {
            acquire_uncertain: {
                'certainty_function': calculate_random, 
                'predict_function': 'predict',
                'dropout_iterations': 1
            }
        }
    })
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/EVW2F3PD4O25S5QWQFRCBPFB7YO6BEEA/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to