diff --git a/bin/hyppopy_exe.py b/bin/hyppopy_exe.py index 887f71a..a69d792 100644 --- a/bin/hyppopy_exe.py +++ b/bin/hyppopy_exe.py @@ -1,24 +1,23 @@ # -*- coding: utf-8 -*- # # DKFZ # # # Copyright (c) German Cancer Research Center, # Division of Medical and Biological Informatics. # 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.txt or http://www.mitk.org for details. # # Author: Sven Wanner (s.wanner@dkfz.de) from hyppopy.cmdtools import * if __name__ == '__main__': - #cmd_workflow() - pass + cmd_workflow() diff --git a/hyppopy/cmdtools.py b/hyppopy/cmdtools.py new file mode 100644 index 0000000..37151d0 --- /dev/null +++ b/hyppopy/cmdtools.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +# DKFZ +# +# +# Copyright (c) German Cancer Research Center, +# Division of Medical and Biological Informatics. +# 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.txt or http://www.mitk.org for details. +# +# Author: Sven Wanner (s.wanner@dkfz.de) + +import argparse + +from sklearn.svm import SVC +from sklearn import datasets +from sklearn.model_selection import cross_val_score +from sklearn.model_selection import train_test_split + +import logging +LOG = logging.getLogger('hyppopy') + +from hyppopy.solverfactory import SolverFactory + + +def cmd_workflow(): + parser = argparse.ArgumentParser(description="") + + parser.add_argument('-v', '--verbosity', type=int, required=False, default=0, + help='') + + + args_dict = vars(parser.parse_args()) + + iris = datasets.load_iris() + X, X_test, y, y_test = train_test_split(iris.data, iris.target, test_size=0.1, random_state=42) + my_IRIS_dta = [X, y] + + my_SVC_parameter = { + 'C': {'domain': 'uniform', 'data': [0, 20]}, + 'gamma': {'domain': 'uniform', 'data': [0.0001, 20.0]}, + 'kernel': {'domain': 'categorical', 'data': ['linear', 'sigmoid', 'poly', 'rbf']} + } + + def my_SVC_loss_func(data, params): + clf = SVC(**params) + return -cross_val_score(clf, data[0], data[1], cv=3).mean() + + factory = SolverFactory() + + solver = factory.get_solver('optunity') + solver.set_data(my_IRIS_dta) + solver.set_parameters(my_SVC_parameter) + solver.set_loss_function(my_SVC_loss_func) + solver.run() + solver.get_results() + + solver = factory.get_solver('hyperopt') + solver.set_data(my_IRIS_dta) + solver.set_parameters(my_SVC_parameter) + solver.set_loss_function(my_SVC_loss_func) + solver.run() + solver.get_results() diff --git a/hyppopy/settings.py b/hyppopy/globals.py similarity index 87% rename from hyppopy/settings.py rename to hyppopy/globals.py index dd68235..819d8f4 100644 --- a/hyppopy/settings.py +++ b/hyppopy/globals.py @@ -1,25 +1,28 @@ # DKFZ # # Copyright (c) German Cancer Research Center, # Division of Medical and Biological Informatics. # 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.txt or http://www.mitk.org for details. # -*- coding: utf-8 -*- import os import sys import logging ROOT = os.path.join(os.path.dirname(__file__), "..") -LOGFILENAME = os.path.join(ROOT, 'logfile.log') -PLUGIN_DEFAULT_DIR = os.path.join(ROOT, *("hyppopy", "plugins")) sys.path.insert(0, ROOT) -logging.getLogger('hyppopy').setLevel(logging.DEBUG) -logging.basicConfig(filename=LOGFILENAME, filemode='w', format='%(name)s - %(levelname)s - %(message)s') +PLUGIN_DEFAULT_DIR = os.path.join(ROOT, *("hyppopy", "plugins")) + +LOGFILENAME = os.path.join(ROOT, 'logfile.log') +DEBUGLEVEL = logging.DEBUG +logging.basicConfig(filename=LOGFILENAME, filemode='w', format='%(levelname)s: %(name)s - %(message)s') + + diff --git a/hyppopy/plugins/hyperopt_solver_plugin.py b/hyppopy/plugins/hyperopt_solver_plugin.py index 3a20be1..f08c273 100644 --- a/hyppopy/plugins/hyperopt_solver_plugin.py +++ b/hyppopy/plugins/hyperopt_solver_plugin.py @@ -1,86 +1,90 @@ # -*- coding: utf-8 -*- # # DKFZ # # # Copyright (c) German Cancer Research Center, # Division of Medical and Biological Informatics. # 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.txt or http://www.mitk.org for details. # # Author: Sven Wanner (s.wanner@dkfz.de) +import os import logging -LOG = logging.getLogger('hyppopy') +from hyppopy.globals import DEBUGLEVEL +LOG = logging.getLogger(os.path.basename(__file__)) +LOG.setLevel(DEBUGLEVEL) + from pprint import pformat try: from hyperopt import fmin, tpe, hp, STATUS_OK, STATUS_FAIL, Trials from yapsy.IPlugin import IPlugin except: - LOG.warning("Hyperopt package not installed, will ignore this plugin!") - print("Hyperopt package not installed, will ignore this plugin!") + LOG.warning("hyperopt package not installed, will ignore this plugin!") + print("hyperopt package not installed, will ignore this plugin!") from hyppopy.solverpluginbase import SolverPluginBase class hyperopt_Solver(SolverPluginBase, IPlugin): trials = None best = None def __init__(self): - LOG.debug("hyperopt_Solver.__init__()") SolverPluginBase.__init__(self) + LOG.debug("initialized") def loss_function(self, params): try: loss = self.loss(self.data, params) status = STATUS_OK except Exception as e: status = STATUS_FAIL return {'loss': loss, 'status': status} def convert_parameter(self, params): - LOG.debug(f"convert_parameter({params})") + LOG.debug(f"convert input parameter\n\n\t{pformat(params)}\n") self.solution_space = {} for name, content in params.items(): data = None domain = None domain_fn = None for key, value in content.items(): if key == 'domain': domain = value if value == 'uniform': domain_fn = hp.uniform if value == 'categorical': domain_fn = hp.choice if key == 'data': data = value if domain == 'categorical': self.solution_space[name] = domain_fn(name, data) else: self.solution_space[name] = domain_fn(name, data[0], data[1]) def execute_solver(self): - LOG.debug(f"execute_solver using solution space -> {pformat(self.solution_space)}") + LOG.debug(f"execute_solver using solution space:\n\n\t{pformat(self.solution_space)}\n") self.trials = Trials() try: self.best = fmin(fn=self.loss_function, space=self.solution_space, algo=tpe.suggest, max_evals=50, trials=self.trials) except Exception as e: - LOG.error(f"Internal error in hyperopt.fmin occured. {e}") - raise BrokenPipeError(f"Internal error in hyperopt.fmin occured. {e}") + LOG.error(f"internal error in hyperopt.fmin occured. {e}") + raise BrokenPipeError(f"internal error in hyperopt.fmin occured. {e}") def convert_results(self): solution = dict([(k, v) for k, v in self.best.items() if v is not None]) print('Solution\n========') print("\n".join(map(lambda x: "%s \t %s" % (x[0], str(x[1])), solution.items()))) diff --git a/hyppopy/plugins/optunity_solver_plugin.py b/hyppopy/plugins/optunity_solver_plugin.py index 75d9ab3..fb4aa60 100644 --- a/hyppopy/plugins/optunity_solver_plugin.py +++ b/hyppopy/plugins/optunity_solver_plugin.py @@ -1,108 +1,112 @@ # -*- coding: utf-8 -*- # # DKFZ # # # Copyright (c) German Cancer Research Center, # Division of Medical and Biological Informatics. # 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.txt or http://www.mitk.org for details. # # Author: Sven Wanner (s.wanner@dkfz.de) +import os import logging -LOG = logging.getLogger('hyppopy') +from hyppopy.globals import DEBUGLEVEL +LOG = logging.getLogger(os.path.basename(__file__)) +LOG.setLevel(DEBUGLEVEL) + from pprint import pformat try: import optunity from yapsy.IPlugin import IPlugin except: - LOG.warning("Optunity package not installed, will ignore this plugin!") - print("Optunity package not installed, will ignore this plugin!") + LOG.warning("optunity package not installed, will ignore this plugin!") + print("optunity package not installed, will ignore this plugin!") from hyppopy.solverpluginbase import SolverPluginBase class optunity_Solver(SolverPluginBase, IPlugin): solver_info = None trials = None best = None status = None def __init__(self): - LOG.debug("optunity_Solver.__init__()") SolverPluginBase.__init__(self) + LOG.debug("initialized") def loss_function(self, **params): try: loss = self.loss(self.data, params) self.status.append('ok') return loss except Exception as e: self.status.append('fail') return 1e9 def convert_parameter(self, params): - LOG.debug(f"convert_parameter({params})") + LOG.debug(f"convert input parameter\n\n\t{pformat(params)}\n") # define function spliting input dict # into categorical and non-categorical def split_categorical(pdict): 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 self.solution_space = {} # split input in categorical and non-categorical data cat, uni = split_categorical(params) # build up dictionary keeping all non-categorical data uniforms = {} for key, value in uni.items(): for key2, value2 in value.items(): if key2 == 'data': uniforms[key] = value2 # build nested categorical structure inner_level = uniforms for key, value in cat.items(): tmp = {} tmp2 = {} for key2, value2 in value.items(): if key2 == 'data': for elem in value2: tmp[elem] = inner_level tmp2[key] = tmp inner_level = tmp2 self.solution_space = tmp2 def execute_solver(self): - LOG.debug(f"execute_solver using solution space -> {pformat(self.solution_space)}") + LOG.debug(f"execute_solver using solution space:\n\n\t{pformat(self.solution_space)}\n") self.status = [] try: self.best, self.trials, self.solver_info = optunity.minimize_structured(f=self.loss_function, num_evals=50, search_space=self.solution_space) except Exception as e: - LOG.error(f"Internal error in optunity.minimize_structured occured. {e}") - raise BrokenPipeError(f"Internal error in optunity.minimize_structured occured. {e}") + LOG.error(f"internal error in optunity.minimize_structured occured. {e}") + raise BrokenPipeError(f"internal error in optunity.minimize_structured occured. {e}") def convert_results(self): solution = dict([(k, v) for k, v in self.best.items() if v is not None]) print('Solution\n========') print("\n".join(map(lambda x: "%s \t %s" % (x[0], str(x[1])), solution.items()))) print(f"Solver used: {self.solver_info['solver_name']}") print(f"Optimum: {self.trials.optimum}") print(f"Iterations used: {self.trials.stats['num_evals']}") print(f"Duration: {self.trials.stats['time']} s") diff --git a/hyppopy/solverfactory.py b/hyppopy/solverfactory.py index 218b1a9..7e4e188 100644 --- a/hyppopy/solverfactory.py +++ b/hyppopy/solverfactory.py @@ -1,106 +1,149 @@ # -*- coding: utf-8 -*- # # DKFZ # # # Copyright (c) German Cancer Research Center, # Division of Medical and Biological Informatics. # 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.txt or http://www.mitk.org for details. # # Author: Sven Wanner (s.wanner@dkfz.de) from yapsy.PluginManager import PluginManager -from hyppopy.settings import PLUGIN_DEFAULT_DIR +from hyppopy.globals import PLUGIN_DEFAULT_DIR from hyppopy.solver import Solver import os import logging -LOG = logging.getLogger('hyppopy') +from hyppopy.globals import DEBUGLEVEL +LOG = logging.getLogger(os.path.basename(__file__)) +LOG.setLevel(DEBUGLEVEL) class SolverFactory(object): + """ + This class is responsible for grabbing all plugins from the plugin folder arranging them into a + Solver class instances. These Solver class instances can be requested from the factory via the + get_solver method. The SolverFactory class is a Singleton class, so try not to instantiate it using + SolverFactory(), the consequences will be horrific. Instead use factory = SolverFactory.instance(). + """ _instance = None + _locked = True _plugin_dirs = [] _plugins = {} def __init__(self): + if self._locked: + msg = "!!! seems you used SolverFactory() to get an instance, please don't do that, "\ + "it will kill a cute puppy anywhere close to you! SolverFactory is a "\ + "Singleton, means please use SolverFactory.instance() instead !!!" + LOG.error(msg) + raise AssertionError(msg) if SolverFactory._instance is not None: pass else: - LOG.debug("__init__()") SolverFactory._instance = self self.reset() self.load_plugins() + LOG.debug("initialized") @staticmethod def instance(): """ Singleton instance access :return: [SolverFactory] instance """ - LOG.debug("instance()") + SolverFactory._locked = False + LOG.debug("instance request") if SolverFactory._instance is None: SolverFactory() + SolverFactory._locked = True return SolverFactory._instance def load_plugins(self): """ Load plugin modules from plugin paths """ LOG.debug("load_plugins()") - LOG.debug(f"setPluginPlaces(" + " ".join(map(str, self._plugin_dirs))) manager = PluginManager() + LOG.debug(f"setPluginPlaces(" + " ".join(map(str, self._plugin_dirs))) manager.setPluginPlaces(self._plugin_dirs) manager.collectPlugins() for plugin in manager.getAllPlugins(): name_elements = plugin.plugin_object.__class__.__name__.split("_") + LOG.debug("found plugin " + " ".join(map(str, name_elements))) if len(name_elements) != 2 or ("Solver" not in name_elements and "Settings" not in name_elements): - LOG.error(f"Invalid plugin class naming for class {plugin.plugin_object.__class__.__name__}, the convention is libname_Solver or libname_Settings.") - raise NameError(f"Invalid plugin class naming for class {plugin.plugin_object.__class__.__name__}, the convention is libname_Solver or libname_Settings.") + LOG.error(f"invalid plugin class naming for class {plugin.plugin_object.__class__.__name__}, the convention is libname_Solver or libname_Settings.") + raise NameError(f"invalid plugin class naming for class {plugin.plugin_object.__class__.__name__}, the convention is libname_Solver or libname_Settings.") if name_elements[0] not in self._plugins.keys(): self._plugins[name_elements[0]] = Solver() self._plugins[name_elements[0]].name = name_elements[0] if name_elements[1] == "Solver": try: self._plugins[name_elements[0]].solver = plugin.plugin_object.__class__() - LOG.info(f"Plugin: {name_elements[0]} Solver loaded") + LOG.info(f"plugin: {name_elements[0]} Solver loaded") except Exception as e: - LOG.error(f"Failed to instanciate class {plugin.plugin_object.__class__.__name__}") + LOG.error(f"failed to instanciate class {plugin.plugin_object.__class__.__name__}") raise ImportError(f"Failed to instanciate class {plugin.plugin_object.__class__.__name__}") elif type == "Settings": try: self._plugins[name_elements[0]].settings = plugin.plugin_object.__class__() - LOG.info(f"Plugin: {name_elements[0]} ParameterSpace loaded") + LOG.info(f"plugin: {name_elements[0]} ParameterSpace loaded") except Exception as e: - LOG.error(f"Failed to instanciate class {plugin.plugin_object.__class__.__name__}") - raise ImportError(f"Failed to instanciate class {plugin.plugin_object.__class__.__name__}") + LOG.error(f"failed to instanciate class {plugin.plugin_object.__class__.__name__}") + raise ImportError(f"failed to instanciate class {plugin.plugin_object.__class__.__name__}") else: - LOG.error(f"Failed loading plugin {name_elements[0]}! Please check if naming conventions are kept!") - raise IOError(f"Failed loading plugin {name_elements[0]}! Please check if naming conventions are kept!") + LOG.error(f"failed loading plugin {name_elements[0]}, please check if naming conventions are kept!") + raise IOError(f"failed loading plugin {name_elements[0]}!, please check if naming conventions are kept!") + if len(self._plugins) == 0: + msg = "no plugins found, please check your plugin folder names or your plugin scripts for errors!" + LOG.error(msg) + raise IOError(msg) def reset(self): """ Reset solver factory """ LOG.debug("reset()") self._plugins = {} self._plugin_dirs = [] self.add_plugin_dir(os.path.abspath(PLUGIN_DEFAULT_DIR)) def add_plugin_dir(self, dir): """ Add plugin directory """ LOG.debug(f"add_plugin_dir({dir})") self._plugin_dirs.append(dir) + def list_solver(self): + """ + list all solvers available + :return: [list(str)] + """ + return list(self._plugins.keys()) + def get_solver(self, name): + """ + returns a solver by name tag + :param name: [str] solver name + :return: [Solver] instance + """ + if not isinstance(name, str): + msg = f"Invalid input, str type expected for name, got {type(name)} instead" + LOG.error(msg) + raise IOError(msg) + if name not in self.list_solver(): + msg = f"failed solver request, a solver called {name} is not available, " \ + f"check for typo or if your plugin failed while loading!" + LOG.error(msg) + raise LookupError(msg) LOG.debug(f"get_solver({name})") return self._plugins[name] diff --git a/hyppopy/tests/test_solver_factory.py b/hyppopy/tests/test_solver_factory.py index a251f74..63e5833 100644 --- a/hyppopy/tests/test_solver_factory.py +++ b/hyppopy/tests/test_solver_factory.py @@ -1,71 +1,75 @@ # -*- coding: utf-8 -*- # # DKFZ # # # Copyright (c) German Cancer Research Center, # Division of Medical and Biological Informatics. # 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.txt or http://www.mitk.org for details. # # Author: Sven Wanner (s.wanner@dkfz.de) import os import unittest from sklearn.svm import SVC from sklearn import datasets from sklearn.model_selection import cross_val_score from sklearn.model_selection import train_test_split from hyppopy.solverfactory import SolverFactory class SolverFactoryTestSuite(unittest.TestCase): def setUp(self): pass def test_solver_loading(self): - pass + factory = SolverFactory.instance() + names = factory.list_solver() + self.assertTrue("hyperopt" in names) + self.assertTrue("optunity" in names) def test_iris_solver_execution(self): iris = datasets.load_iris() X, X_test, y, y_test = train_test_split(iris.data, iris.target, test_size=0.1, random_state=42) my_IRIS_dta = [X, y] my_SVC_parameter = { 'C': {'domain': 'uniform', 'data': [0, 20]}, 'gamma': {'domain': 'uniform', 'data': [0.0001, 20.0]}, 'kernel': {'domain': 'categorical', 'data': ['linear', 'sigmoid', 'poly', 'rbf']} } def my_SVC_loss_func(data, params): clf = SVC(**params) return -cross_val_score(clf, data[0], data[1], cv=3).mean() - factory = SolverFactory() + factory = SolverFactory.instance() + names = factory.list_solver() solver = factory.get_solver('optunity') solver.set_data(my_IRIS_dta) solver.set_parameters(my_SVC_parameter) solver.set_loss_function(my_SVC_loss_func) solver.run() solver.get_results() solver = factory.get_solver('hyperopt') solver.set_data(my_IRIS_dta) solver.set_parameters(my_SVC_parameter) solver.set_loss_function(my_SVC_loss_func) solver.run() solver.get_results() if __name__ == '__main__': unittest.main()