Source code for lagom.experiment.config

from itertools import product


[docs]class Grid(list): r"""A grid search over a list of values. """ def __init__(self, values): super().__init__(values)
[docs]class Sample(object): def __init__(self, f): self.f = f def __call__(self): return self.f()
[docs]class Condition(object): def __init__(self, f): assert callable(f) self.f = f def __call__(self, config): return self.f(config)
[docs]class Config(object): r"""Defines a set of configurations for the experiment. The configuration includes the following possible items: * Hyperparameters: learning rate, batch size etc. * Experiment settings: training iterations, logging directory, environment name etc. All items are stored in a dictionary. It is a good practice to semantically name each item e.g. `network.lr` indicates the learning rate of the neural network. For hyperparameter search, we support both grid search (:class:`Grid`) and random search (:class:`Sample`). Call :meth:`make_configs` to generate a list of all configurations, each is assigned with a unique ID. note:: For random search over small positive float e.g. learning rate, it is recommended to use log-uniform distribution, i.e. .. math:: \text{logU}(a, b) \sim \exp(U(\log(a), \log(b))) An example: `np.exp(np.random.uniform(low=np.log(low), high=np.log(high)))` Because direct uniform sampling is very `numerically unstable`_. .. warning:: The random seeds should not be set here. Instead, it should be handled by :class:`BaseExperimentMaster` and :class:`BaseExperimentWorker`. Example:: >>> config = Config({'log.dir': 'some path', 'network.lr': Grid([1e-3, 5e-3]), 'env.id': Grid(['CartPole-v1', 'Ant-v2'])}, num_sample=1, keep_dict_order=False) >>> import pandas as pd >>> print(pd.DataFrame(config.make_configs())) ID env.id log.dir network.lr 0 0 CartPole-v1 some path 0.001 1 1 Ant-v2 some path 0.001 2 2 CartPole-v1 some path 0.005 3 3 Ant-v2 some path 0.005 Args: items (dict): a dictionary of all configuration items. num_sample (int): number of samples for random configuration items. If grid search is also provided, then the grid will be repeated :attr:`num_sample` of times. keep_dict_order (bool): if ``True``, then each generated configuration has the same key ordering with :attr:`items`. .. _numerically unstable: http://cs231n.github.io/neural-networks-3/#hyper """ def __init__(self, items, num_sample=1, keep_dict_order=False): assert isinstance(items, dict), f'dict type expected, got {type(items)}' self.items = items self.num_sample = num_sample self.keep_dict_order = keep_dict_order
[docs] def make_configs(self): r"""Generate a list of all possible combinations of configurations, including grid search and random search. Returns: list: a list of all possible configurations """ keys_fixed = [] keys_grid = [] keys_sample = [] for key in self.items.keys(): x = self.items[key] if isinstance(x, Grid): keys_grid.append(key) elif isinstance(x, Sample): keys_sample.append(key) else: keys_fixed.append(key) if len(keys_sample) == 0: # if no random search defined, set num_sample=1 to avoid repetition self.num_sample = 1 product_grid = list(product(*[self.items[key] for key in keys_grid])) # len >= 1, [()] list_config = [] for n in range(len(product_grid)*self.num_sample): x = {'ID': n} x = {**x, **{key: self.items[key] for key in keys_fixed}} for idx, key in enumerate(keys_grid): x[key] = product_grid[n % len(product_grid)][idx] for key in keys_sample: x[key] = self.items[key]() if self.keep_dict_order: x = {**{'ID': x['ID']}, **{key: x[key] for key in self.items.keys()}} for key, value in x.items(): if isinstance(value, Condition): x[key] = value(x) list_config.append(x) return list_config