Parametrizing your optimization
Please note that parametrization is still a work in progress and changes are on their way (including for this documentation)! We are trying to update it to make it simpler and simpler to use (all feedbacks are welcome ;) ), with the side effect that there will be breaking changes.
The aim of parametrization is to specify what are the parameters that the optimization should be performed upon. The parametrization subpackage will help you do thanks to:
the
parameter
modules (accessed by the shortcutnevergrad.p
) providing classes that should be used to specify each parameter.the
FolderFunction
which helps transform any code into a Python function in a few lines. This can be especially helpful to optimize parameters in non-Python 3.6+ code (C++, Octave, etc…) or parameters in scripts.
Preliminary examples
The code below defines a parametrization that creates a dict
with 3 elements:
a log-distributed scalar.
an array of size 2.
a random letter, which is either
a
,b
orc
,
The main attribute for users is the value
attribute, which provides the value of the defined dict
:
import nevergrad as ng
# build a parameter providing a dict value:
param = ng.p.Dict(
# logarithmically distributed float
log=ng.p.Log(lower=0.01, upper=1.0),
# one-dimensional array of length 2
array=ng.p.Array(shape=(2,)),
# character, either "a" or "b or "c".
char=ng.p.Choice(["a", "b", "c"]),
)
print(param.value)
# {'log': 0.01,
# 'array': array([0., 0.]),
# 'char': 'a'}
Parametrization is central in nevergrad
since it is the interface between the optimization domain
defined by the users, and the standardized representations used by the optimizers. The snippet below
shows how to duplicate the parameter (i.e. spawn a child from the parameter), manually updating the value,
and export to the standardized space. Unless implementing an algorithm, users should not have any need
for this export. However, setting the value
manually can be used to provide an initial prior to
the optimizer.
# create a new instance
child = param.spawn_child()
# update its value
child.value = {"log": 0.2, "array": np.array([12.0, 13.0]), "char": "c"}
# export to standardized space
data = child.get_standardized_data(reference=param)
print(data)
# np.array([12., 13., 0., 0., 0.69, 0.90])
Similarly, optimizers can use mutate
and recombine
methods to update the value of parameters.
You can easily check how parameters mutate, and mutation of Array
variables can be adapted to your need:
param.mutate()
print(param.value)
# {'log': 0.155,
# 'array': np.array([-0.966, 0.045]),
# 'char': 'a'}
# increase the step/sigma for array
# (note that it's advised to to this during the creation
# of the variable:
# array=ng.p.Array(shape=(2,)).set_mutation(sigma=10))
param["array"].set_mutation(sigma=10) # type: ignore
param.mutate()
print(param.value)
# {'log': 0.155,
# 'array': np.array([-9.47, 8.38]), # larger mutation
# 'char': 'a'}
Note that optimizer freeze
candidates to avoid modification their modification and border effects, however you can always spawn new parameters from them.
Parametrization is also responsible for the randomness in nevergrad. They have a random_state
which can be set for reproducibility
param.random_state.seed(12)
Parameters
7 types of parameters are currently provided:
Choice(items)
: describes a parameter which can take values within the provided list of (usually unordered categorical) items, and for which transitions are global (from one item to any other item). The returned element will be sampled as the softmax of the values on these dimensions. Be cautious: this process is non-deterministic and makes the function evaluation noisy.TransitionChoice(items)
: describes a parameter which can take values within the provided list of (usually ordered) items, and for which transitions are local (from one item to close items).Array(shape=shape)
: describes anp.ndarray
of any shape. The bounds of the array and the mutation of this array can be specified (seeset_bounds
,set_mutation
). This makes it a very flexible type of parameter. Eg.Array(shape=(2, 3)).set_bounds(0, 2)
encodes for an array of shape(2, 3)
, with values bounded between 0 and 2. It can be also set to an array of integers (seeset_integer_casting
)Scalar()
: describes a scalar. This parameter inherits from allArray
methods, so it can be bounded, projected to integers and mutation rate can be customized.Log(lower, upper)
: describes log distributed data between two bounds. Under the hood this uses anScalar
with appropriate specifications for bounds and mutations.Instrumentation(*args, **kwargs)
: a container for other parameters. Values of parameters in theargs
will be returned as atuple
byparam.args
, and values of parameters in thekwargs
will be returned as adict
byparam.kwargs
(in practice,param.value == (param.args, param.kwargs)
). This serves to parametrize functions taking multiple arguments, since you can then call the function withfunc(*param.args, **param.kwargs)
.
Follow the link to the API reference for more details and initialization options:
|
|
|
Parameter representing a scalar. |
|
Parameter representing a positive variable, mutated by Gaussian mutation in log-scale. |
|
Dictionary-valued parameter. |
|
Tuple-valued parameter. |
|
Container of parameters available through args and kwargs attributes. |
|
Unordered categorical parameter, randomly choosing one of the provided choice options as a value. |
|
Categorical parameter, choosing one of the provided choice options as a value, with continuous transitions. |
Parametrization
Parametrization helps you define the parameters you want to optimize upon. Currently most algorithms make use of it to help convert the parameters into the “standardized data” space (a vector space spanning all the real values), where it is easier to define operations.
Let’s define the parametrization for a function taking 3 positional arguments and one keyword argument value
.
arg1 = ng.p.Choice([["Helium", "Nitrogen", "Oxygen"]])
is the first positional argument, which can take 3 possible values, without any order, the selection is made stochasticly through the sampling of a softmax. It is encoded by 3 values (the softmax weights) in the “standardized space”.arg2 = ng.p.TransitionChoice(["Solid", "Liquid", "Gas"])
is the second one, it encodes the choice (i.e. 1 dimension) through a single index which can mutate in a continuous way.third argument will be kept constant to
blublu
values = ng.p.Tuple(ng.p.Scalar().set_integer_casting(), ng.p.Scalar())
which represents a tuple of two scalars with different numeric types in the parameter space, and in the “standardized space”
We then define a parameter holding all these parameters, with a standardized space of dimension 6 (as the sum of the dimensions above):
arg1 = ng.p.Choice(["Helium", "Nitrogen", "Oxygen"])
arg2 = ng.p.TransitionChoice(["Solid", "Liquid", "Gas"])
values = ng.p.Tuple(ng.p.Scalar().set_integer_casting(), ng.p.Scalar())
instru = ng.p.Instrumentation(arg1, arg2, "blublu", amount=values)
print(instru.dimension)
# >>> 6
You can then directly perform optimization on a function given its parametrization:
def myfunction(arg1, arg2, arg3, amount=(2, 2)):
print(arg1, arg2, arg3)
return amount[0] ** 2 + amount[1] ** 2
optimizer = ng.optimizers.NGOpt(parametrization=instru, budget=100)
recommendation = optimizer.minimize(myfunction)
print(recommendation.value)
# >>> (('Helium', 'Gas', 'blublu'), {'value': (0, 0.0006602471804655007)})
Here is a glimpse of what happens on the optimization space:
instru2 = instru.spawn_child().set_standardized_data([-80, 80, -80, 0, 3, 5.0])
assert instru2.args == ("Nitrogen", "Liquid", "blublu")
assert instru2.kwargs == {"amount": (3, 5.0)}
With this code:
Nitrogen
is selected because proba(e) = exp(80) / (exp(80) + exp(-80) + exp(-80)) = 1Liquid
is selected because the index for Liquid is around 0 in the standardized space.amount=(3, 5.0)
because the last two values of the standardized space (i.e. 3.0, 5.0) corresponds to the value of the last kwargs.
Parametrizing external code
Sometimes it is completely impractical or impossible to have a simple Python3.6+ function to optimize. This may happen when the code you want to optimize is a script. Even more so if the code you want to optimize is not Python3.6+.
We provide tooling for this situation but this is hacky, so if you can avoid it, do avoid it. Otherwise, go through these steps to instrument your code:
identify the variables (parameters, constants…) you want to optimize.
add placeholders to your code. Placeholders are just tokens of the form
NG_ARG{name|comment}
where you can modify the name and comment. The name you set will be the one you will need to use as your function argument. In order to avoid breaking your code, the line containing the placeholders can be commented. To notify that the line should be uncommented for parametrization, you’ll need to add “@nevergrad@” at the start of the comment. Here is an example in C which will notify that we want to obtain a function with astep
argument which will inject values into thestep_size
variable of the code:
int step_size = 0.1
// @nevergrad@ step_size = NG_ARG{step|any comment}
prepare the command to execute that will run your code. Make sure that the last printed line is just a float, which is the value to base the optimization upon. We will be doing minimization here, so this value must decrease for better results.
instantiate your code into a function using the
FolderFunction
class:
import sys
from pathlib import Path
import nevergrad as ng
from nevergrad.parametrization import FolderFunction
# nevergrad/parametrization/examples contains a script
example_folder = Path(ng.__file__).parent / "parametrization" / "examples"
python = sys.executable
command = [python, "examples/script.py"] # command to run from right outside the provided folder
# create a function from the folder
func = FolderFunction(example_folder, command, clean_copy=True)
# print the number of variables of the function:
print(func.placeholders)
# prints: [Placeholder('value1', 'this is a comment'), Placeholder('value2', None), Placeholder('string', None)]
# and run it (the script prints 12 at the end)
assert func(value1=2, value2=3, string="blublu") == 12.0
parametrize the function (see Parametrization section just above).
Tips and caveats
using
FolderFunction
argumentclean_copy=True
will copy your folder so that tempering with it during optimization will run different versions of your code.under the hood, with or without
clean_copy=True
, when calling the function,FolderFunction
will create symlink copy of the initial folder, remove the files that have tokens, and create new ones with appropriate values. Symlinks are used in order to avoid duplicating large projects, but they have some drawbacks, see next point ;)one can add a compilation step to
FolderFunction
(the compilation just has to be included in the script). However, be extra careful that if the initial folder contains some build files, they could be modified by the compilation step, because of the symlinks. Make sure that during compilation, you remove the build symlinks first! This feature has not been fool proofed yet!!!the following external file types are registered by default:
[".c", ".h", ".cpp", ".hpp", ".py", ".m"]
. Custom file types can be registered usingFolderFunction.register_file_type
by providing the relevant file suffix as well as the characters that indicate a comment. However, for now, parameters which provide a vector or values (Array
) will inject code with a Python format (list) by default, which may not be suitable.