SMAC3 icon indicating copy to clipboard operation
SMAC3 copied to clipboard

SMAC4* cannot handle functools.partial functions and passes undocumented **kwargs to the target-function (lambdas do work)

Open stheid opened this issue 4 years ago • 9 comments

Description

https://github.com/automl/SMAC3/blob/2b27e0d063bdb485e07a7b76e9ce41e05cfbbf71/examples/SMAC4HPO_svm.py#L28 and https://github.com/automl/SMAC3/blob/2b27e0d063bdb485e07a7b76e9ce41e05cfbbf71/examples/SMAC4HPO_svm.py#L105 suggests, that the target callable only needs to accept one argument.

However #546 shows, that this is not the case for SMAC4BO

Steps/Code to Reproduce

see #546

Expected Results

target-function is evaluated by passing only the configuration as argument

Actual Results

fails because the arguments seed and instance are not accepted by the target-function.

Workarround

add **kwargs argument to target-function. see refered issue for details

Versions

0.11.1

stheid avatar Oct 13 '19 22:10 stheid

Hi,

Thank you for reporting this issue. However, I cannot reproduce it on my end. Running one of the examples, e.g., the svm example (https://github.com/automl/SMAC3/blob/master/examples/SMAC4HPO_svm.py) works fine on my machine. The target function in this example, indeed only expects one argument.

Best, Marius

mlindauer avatar Oct 14 '19 09:10 mlindauer

The issue is not related to HPO but to SMAC4BO! as i write in the other issue, i cannot run HPO because of the swig issue on my machine. However it also does not happen when i simpy change the optimizer in the example you linked. I will try to hunt it down further and inform you if can create a simpler case to produce the issue.

stheid avatar Oct 14 '19 09:10 stheid

the TAE module is independent of the used SMAC mode (AC, HPO, BO). Therefore, it should work fine with all or with none. I also verified that I can run the rosenbrock example (again only one argument for the target function): https://github.com/automl/SMAC3/blob/master/examples/SMAC4BO_rosenbrock.py

mlindauer avatar Oct 14 '19 09:10 mlindauer

The issue is related to my partial function!

change target function signature

def svm_from_cfg_moreargs(cfg, x, y):

set the default arguments using functools.partial

svm_from_cfg = partial(svm_from_cfg_moreargs, x=1, y=2)

for some reason this results in the even more arguments for the function under the hood

    self._target(*self._args, **self._kwargs)
  File "/usr/lib/python3.7/site-packages/pynisher/limit_function_call.py", line 93, in subprocess_func
    return_value = ((func(*args, **kwargs), 0))
TypeError: svm_from_cfg_moreargs() got an unexpected keyword argument 'seed'

Allowing more kwargs to be added solves the issue:

def svm_from_cfg_moreargs(cfg, x, y,**kwargs):

stheid avatar Oct 14 '19 09:10 stheid

If I remember correctly, instead of using partial using lambda worked fine.

mlindauer avatar Oct 14 '19 09:10 mlindauer

I think the underlying issue here is that we assume that if the target function has more than one argument that these are either instance or seed, and that we check that via inspection. We actually changed this in the development branch (to only pass these if the argument has the correct name), but we should document this somewhere.

Current code (not on PYPI): https://github.com/automl/SMAC3/blob/development/smac/tae/execute_func.py#L74

Code on pypi: https://github.com/automl/SMAC3/blob/master/smac/tae/execute_func.py#L69

mfeurer avatar Oct 14 '19 09:10 mfeurer

can confirm that using lambda istead of partial is indeed another successful workaround.

Thanks for your blazing fast replies. I assume with the pending changes in the code this issue can be assumed closed.

stheid avatar Oct 14 '19 09:10 stheid

Kind of, but could you please leave it open so we know that we need to update the doc?

mfeurer avatar Oct 14 '19 10:10 mfeurer

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Jun 18 '22 01:06 stale[bot]

Have been looking into this. The way you would want to do this in smac 2.0, is that you no longer define the tae as a method (because the partial will treat the method as static) but instead do it like this:

def train(config: Configuration, seed: int = 0, X=None, y=None) -> float:
    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", category=ConvergenceWarning)
        
        classifier = MLPClassifier(
            hidden_layer_sizes=[config["n_neurons"]] * config["n_layer"],
            solver=config["optimizer"],
            batch_size=config["batch_size"],
            activation=config["activation"],
            learning_rate_init=config["learning_rate_init"],
            random_state=seed,
            max_iter=5,
        )

        # Returns the 5-fold cross validation accuracy
        cv = StratifiedKFold(n_splits=5, random_state=seed,
                             shuffle=True)  # to make CV splits consistent
        score = cross_val_score(classifier, X, y, cv=cv, error_score="raise")

        return 1 - np.mean(score)

Now you can do the following:

 from functools import partial

    digits = load_digits()
    tae = partial(train, X=digits.data, y=digits.target)

    tae.__code__ = tae.func.__code__  # this is a crucial step, because the facade takes the code
    # into its meta information and partial does not provide it automatically.

and you are set.

timruhkopf avatar Mar 30 '23 09:03 timruhkopf