diff --git a/hyppopy/CandidateDescriptor.py b/hyppopy/CandidateDescriptor.py index 2023510..53f4bda 100644 --- a/hyppopy/CandidateDescriptor.py +++ b/hyppopy/CandidateDescriptor.py @@ -1,59 +1,108 @@ class CandidateDescriptor(object): """ Descriptor that defines an candidate the solver wants to be checked. It is used to lable/identify the candidates and their results in the case of batch processing. """ def __init__(self, **definingValues): """ @param definingValues Class assumes that all variables passed to the computer are parameters of the candidate the instance should represent. """ import uuid self._definingValues = definingValues self._definingStr = str() for item in sorted(definingValues.items()): self._definingStr = self._definingStr + "'" + str(item[0]) + "':'" + str(item[1]) + "'," self.ID = str(uuid.uuid4()) def __missing__(self, key): return None def __len__(self): return len(self._definingValues) def __contains__(self, key): return key in self._definingValues def __eq__(self, other): if isinstance(other, self.__class__): return self._definingValues == other._definingValues else: return False def __hash__(self): return hash(self._definingStr) def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return 'EvalInstanceDescriptor(%s)' % (self._definingValues) def __str__(self): return '(%s)' % (self._definingValues) def keys(self): return self._definingValues.keys() def __getitem__(self, key): if key in self._definingValues: return self._definingValues[key] raise KeyError('Unkown defining value key was requested. Key: {}; self: {}'.format(key, self)) def get_values(self): return self._definingValues + + +class CandicateDescriptorWrapper: + + class InternalCandidateValueWrapper: + def __init__(self, value_list): + self._value_list = value_list + + def __gt__(self, other): + boundary_condition = True + for value in self._value_list: + if value > other: + continue + else: + boundary_condition = False + break + return boundary_condition + + def __lt__(self, other): + boundary_condition = True + for value in self._value_list: + if value < other: + continue + else: + boundary_condition = False + break + return boundary_condition + + def get(self): + return self._value_list + + def __init__(self, keys): + self._cand = None + self._keys = keys + + def __iter__(self): + return self._cand + + def __getitem__(self, key): + return self.InternalCandidateValueWrapper([x[key] for x in self._cand]) + + def keys(self): + return self._keys + + def set(self, obj): + self._cand = obj + + def get(self): + return self._cand diff --git a/hyppopy/solvers/OptunitySolver.py b/hyppopy/solvers/OptunitySolver.py index bb0f8a8..072fb1a 100644 --- a/hyppopy/solvers/OptunitySolver.py +++ b/hyppopy/solvers/OptunitySolver.py @@ -1,173 +1,193 @@ # Hyppopy - A Hyper-Parameter Optimization Toolbox # # Copyright (c) German Cancer Research Center, # Division of Medical Image Computing. # All rights reserved. # # This software is distributed WITHOUT ANY WARRANTY; without # even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. # # See LICENSE import os import logging import optunity from pprint import pformat -from hyppopy.CandidateDescriptor import CandidateDescriptor +from hyppopy.CandidateDescriptor import CandidateDescriptor, CandicateDescriptorWrapper from hyppopy.globals import DEBUGLEVEL LOG = logging.getLogger(os.path.basename(__file__)) LOG.setLevel(DEBUGLEVEL) from hyppopy.solvers.HyppopySolver import HyppopySolver class OptunitySolver(HyppopySolver): def __init__(self, project=None): """ The constructor accepts a HyppopyProject. :param project: [HyppopyProject] project instance, default=None """ HyppopySolver.__init__(self, project) def define_interface(self): """ This function is called when HyppopySolver.__init__ function finished. Child classes need to define their individual parameter here by calling the _add_member function for each class member variable need to be defined. Using _add_hyperparameter_signature the structure of a hyperparameter the solver expects must be defined. Both, members and hyperparameter signatures are later get checked, before executing the solver, ensuring settings passed fullfill solver needs. """ self._add_member("max_iterations", int) self._add_hyperparameter_signature(name="domain", dtype=str, options=["uniform", "categorical"]) self._add_hyperparameter_signature(name="data", dtype=list) self._add_hyperparameter_signature(name="type", dtype=type) def loss_function_call(self, params): """ This function is called within the function loss_function and encapsulates the actual blackbox function call in each iteration. The function loss_function takes care of the iteration driving and reporting, but each solver lib might need some special treatment between the parameter set selection and the calling of the actual blackbox function, e.g. parameter converting. :param params: [dict] hyperparameter space sample e.g. {'p1': 0.123, 'p2': 3.87, ...} :return: [float] loss """ for key in params.keys(): if self.project.get_typeof(key) is int: params[key] = int(round(params[key])) return self.blackbox(**params) def loss_function_batch(self, **candidates): """ This function is called with a list of candidates. This list is driven by the solver lib itself. The purpose of this function is to take care of the iteration reporting and the calling of the callback_func if available. As a developer you might want to overwrite this function (or the 'non-batch'-version completely (e.g. HyperoptSolver) but then you need to take care for iteration reporting for yourself. The alternative is to only implement loss_function_call (e.g. OptunitySolver). :param candidates: [list of CandidateDescriptors] :return: [dict] result e.g. {'loss': 0.5, 'book_time': ..., 'refresh_time': ...} """ - # candidate_list = candidates['cand_list'] - print('hello') - can = [CandidateDescriptor(**candidates)] - result = super(OptunitySolver, self).loss_function_batch(can) - # result = HyppopySolver.loss_function_batch(self, can) + candidate_list = [] + keysValue = candidates.keys() + temp = {} + for key in keysValue: + temp[key] = candidates[key].get() + + for i, pack in enumerate(zip(*temp.values())): + candidate_list.append(CandidateDescriptor(**(dict(zip(keysValue, pack))))) + + result = super(OptunitySolver, self).loss_function_batch(candidate_list) self.best = self._trials.argmin - return list(result.values())[0]['loss'] + + return [x['loss'] for x in result.values()] def my_pmap(self, f, seq): + keys = seq[0].keys() + candidates = [] for elem in seq: can = CandidateDescriptor(**elem) candidates.append(can) #{'x': can}) + cand_list = CandicateDescriptorWrapper(keys=seq[0].keys()) + cand_list.set(candidates) + + f_result = f(cand_list) - # candidates = [candidates] - return map(f, candidates) + try: + for x in f_result: + continue + except: + bla = f_result + f_result = [] + for x in candidates: + f_result.append(bla) + + return f_result def execute_solver(self, searchspace): """ This function is called immediately after convert_searchspace and get the output of the latter as input. It's purpose is to call the solver libs main optimization function. :param searchspace: converted hyperparameter space """ LOG.debug("execute_solver using solution space:\n\n\t{}\n".format(pformat(searchspace))) try: optunity.minimize_structured(f=self.loss_function_batch, num_evals=self.max_iterations, search_space=searchspace, pmap=self.my_pmap) print('bla') except Exception as e: LOG.error("internal error in optunity.minimize_structured occured. {}".format(e)) raise BrokenPipeError("internal error in optunity.minimize_structured occured. {}".format(e)) def split_categorical(self, pdict): """ This function splits the incoming dict into two parts, categorical only entries and other. :param pdict: [dict] input parameter description dict :return: [dict],[dict] categorical only, others """ categorical = {} uniform = {} for name, pset in pdict.items(): for key, value in pset.items(): if key == 'domain' and value == 'categorical': categorical[name] = pset elif key == 'domain': uniform[name] = pset return categorical, uniform def convert_searchspace(self, hyperparameter): """ This function gets the unified hyppopy-like parameterspace description as input and, if necessary, should convert it into a solver lib specific format. The function is invoked when run is called and what it returns is passed as searchspace argument to the function execute_solver. :param hyperparameter: [dict] nested parameter description dict e.g. {'name': {'domain':'uniform', 'data':[0,1], 'type':'float'}, ...} :return: [object] converted hyperparameter space """ LOG.debug("convert input parameter\n\n\t{}\n".format(pformat(hyperparameter))) # split input in categorical and non-categorical data cat, uni = self.split_categorical(hyperparameter) # build up dictionary keeping all non-categorical data uniforms = {} for key, value in uni.items(): for key2, value2 in value.items(): if key2 == 'data': if len(value2) == 3: uniforms[key] = value2[0:2] elif len(value2) == 2: uniforms[key] = value2 else: raise AssertionError("precondition violation, optunity searchspace needs list with left and right range bounds!") if len(cat) == 0: return uniforms # build nested categorical structure inner_level = uniforms for key, value in cat.items(): tmp = {} optunity_space = {} for key2, value2 in value.items(): if key2 == 'data': for elem in value2: tmp[elem] = inner_level optunity_space[key] = tmp inner_level = optunity_space return optunity_space