Non-Iterative Objective

Non-Iterative Objective refers to the objective functions with single iteration. They do not report progress during the execution to get a pruning decision.

Interfaceless

The simplest way to construct a Tune compatible non-iterative objective is to wirte a native python function with type annotations.

[3]:
from typing import Tuple, Dict, Any

def objective1(a, b) -> float:
    return a**2 + b**2

def objective2(a, b) -> Tuple[float, Dict[str, Any]]:
    return a**2 + b**2, {"metadata":"x"}

If you function as float or Tuple[float, Dict[str, Any]] as output annotation, they are valid non-iterative objectives for tune

Tuple[float, Dict[str, Any]] is to return both the metric and metadata.

The following code demos how it works on the backend to convert your simple functions to tune compatible objects. You normally don’t need to do that by yourself.

[5]:
from tune import to_noniterative_objective, Trial

f1 = to_noniterative_objective(objective1)
f2 = to_noniterative_objective(objective2, min_better=False)

trial = Trial("id", params=dict(a=1,b=1))
report1 = f1.safe_run(trial)
report2 = f2.safe_run(trial)

print(type(f1))
print(report1.metric, report1.sort_metric, report1.metadata)
print(report2.metric, report2.sort_metric, report2.metadata)
<class 'tune.noniterative.convert._NonIterativeObjectiveFuncWrapper'>
2.0 2.0 {}
2.0 -2.0 {'metadata': 'x'}

Decorator Approach

It is equivalent to use decorator on top of the functions. But now your functions depend on tune package.

[7]:
from tune import noniterative_objective

@noniterative_objective
def objective_3(a, b) -> float:
    return a**2 + b**2

@noniterative_objective(min_better=False)
def objective_4(a, b) -> Tuple[float, Dict[str, Any]]:
    return a**2 + b**2, {"metadata":"x"}

report3 = objective_3.safe_run(trial)
report4 = objective_4.safe_run(trial)

print(report3.metric, report3.sort_metric, report3.metadata)
print(report4.metric, report4.sort_metric, report4.metadata)
2.0 2.0 {}
2.0 -2.0 {'metadata': 'x'}

Interface Approach

With interface approach, you can access all properties of a trial. Also you can use more flexible logic to generate sort metric.

[9]:
from tune import NonIterativeObjectiveFunc, TrialReport

class Objective(NonIterativeObjectiveFunc):
    def generate_sort_metric(self, value: float) -> float:
        return - value * 10

    def run(self, trial: Trial) -> TrialReport:
        params = trial.params.simple_value
        metric = params["a"]**2 + params["b"]**2
        return TrialReport(trial, metric, metadata=dict(m="x"))

report = Objective().safe_run(trial)
print(report.metric, report.sort_metric, report.metadata)

2.0 -20.0 {'m': 'x'}

Factory Method

Almost all higher level APIs of tune are using TUNE_OBJECT_FACTORY to convert various objects to NonIterativeObjectiveFunc.

[10]:
from tune import TUNE_OBJECT_FACTORY

assert isinstance(TUNE_OBJECT_FACTORY.make_noniterative_objective(objective1), NonIterativeObjectiveFunc)
assert isinstance(TUNE_OBJECT_FACTORY.make_noniterative_objective(objective_4), NonIterativeObjectiveFunc)
assert isinstance(TUNE_OBJECT_FACTORY.make_noniterative_objective(Objective()), NonIterativeObjectiveFunc)

That is why in the higher level APIs, you can just pass in a very simple python function as objective but tune is still able to recognize.

Actually you can make it even more flexible by configuring the factory.

[11]:
def to_obj(obj):
    if obj == "test":
        return to_noniterative_objective(objective1, min_better=False)
    if isinstance(obj, NonIterativeObjectiveFunc):
        return obj
    raise NotImplementedError

TUNE_OBJECT_FACTORY.set_noniterative_objective_converter(to_obj)  # user to_obj to replace the built-in default converter

assert isinstance(TUNE_OBJECT_FACTORY.make_noniterative_objective("test"), NonIterativeObjectiveFunc)

If you customize in this way, then you can pass in test to the higher level tuning APIs, and it will be recognized as a compatible objective.

This is a common approach in Fugue projects. It enables you to use mostly primitive data types to represent what you want to do. For advanced users, if you spend some time on such configuration (one time effort), you will find the code is even simpler and less dependent on fugue and tune.

[ ]: