Targets

Targets play a crucial role as the connection between observables measured in an experiment and the machine learning core behind BayBE. In general, it is expected that you create one Target object for each of your observables to inform BayBE about their existence. The way BayBE treats these targets is then controlled via the Objective.

NumericalTarget

Important

The NumericalTarget class has been redesigned from the ground up in version 0.14.0, providing a more concise and significantly more expressive interface.

For a temporary transition period, the class constructor offers full backward compatibility with the previous interface, meaning that it can be called with either the new or the legacy arguments. However, this comes at the cost of reduced typing support, meaning that you won’t get type hints (e.g. for autocompletion or static type checks) for either of the two types of constructor calls.

For this reason, we offer two additional constructors available for the duration of the deprecation period that offer full typing support: from_legacy_interface() and from_modern_interface().

Use the NumericalTarget class for optimizing real-valued quantities. Optimization with targets of this type follows two basic rules:

  1. Targets are transformed as specified by their transformation, with no transformation defined being equivalent to the identity transformation.

  2. Whenever an optimization direction is required (i.e., when the context is not active learning), the transformed targets are assumed to be maximized by default or minimized if explicitly specified via their minimize flag.

This results in a simple yet expressive interface:

from baybe.targets import NumericalTarget
from baybe.transformations import LogarithmicTransformation

target = NumericalTarget(
    name="Yield",
    transformation=LogarithmicTransformation(),  # optional transformation
    minimize=False,  # this is the default
)

Targets are Optimization Instructions

Notice how the target ingredients above declaratively specify the different aspects of the underlying optimization problem:

  • The name defines the signal “source”, i.e. the observable being measured.

  • The transformation defines the “what”, i.e. which derivative of the signal is to be optimized.

  • The minimize flag defines the “how”, i.e. the desired optimization direction.

While the second rule may seem restrictive at first, it does not limit the expressiveness of the resulting models, thanks to the transformation step applied. In fact, all types of optimization problems (e.g., minimization, matching/avoiding one or multiple set point values, or pursuing any other custom objective) are just maximization problems in disguise, hidden behind an appropriate target transformation.

For example:

  • Minimization can be achieved by negating the targets before maximizing the resulting numerical values. For more information, see here.

  • Matching a set point value can be implemented by applying a transformation that computes the “proximity” to the set point in some way (e.g. in terms of the negative absolute difference to it). Similarly, avoiding the set point can be achieved by reversing the sign of the proximity measure (or activating the minimize flag in addition). For more information, see here.

  • In general, any (potentially nonlinear) custom objective can be expressed using a transformation that assigns higher values to more desirable outcomes and lower values to less desirable outcomes. Examples can be found here.

Many cases – especially the first two described above – are so common that we offer convenient ways to directly create the corresponding target objects for many optimization workflows, eliminating the need to manually specify the necessary Transformation object yourself:

Minimization

Minimization of a target can be achieved by simply passing the minimize=True argument to the constructor:

from baybe.targets import NumericalTarget

t = NumericalTarget(
    name="Cost",
    minimize=True,  # cost is to be minimized
)

Minimization = Negated Maximization

Behind the scenes, minimization of targets is achieved by maximizing their negated values: the minimize flag is used to inform the corresponding Objective holding the NumericalTarget object to inject an appropriate negating transformation just before passing the target values to the optimization engine, allowing us to reuse the same maximization-based routines for all targets. The details of this negation step depend on the objective type being used.

However, while numerically equivalent, there is a semantic difference between minimizing a quantity and maximizing the negated signal derived from it. This difference is both reflected by the way targets are specified as well as by the resulting objects:

import numpy as np
import pandas as pd
from pandas.testing import assert_frame_equal

from baybe.targets import NumericalTarget
from baybe.transformations import AffineTransformation

# Target 1: "Minimize" cost
t1 = NumericalTarget(name="Cost", minimize=True)

# Target 2: "Maximize" the quantity obtained from negating cost measurements
t2 = NumericalTarget(name="Cost", transformation=AffineTransformation(factor=-1))

# Although both targets yield the same objective values ...
s = pd.Series(np.linspace(0, 10), name="Cost")
df = s.to_frame()
assert_frame_equal(
    t1.to_objective().transform(df),
    t2.to_objective().transform(df),
)

# ... the targets themselves are not equal ...
assert t1 != t2

# ... and the transformed signals they specify differ!
assert not t1.transform(s).equals(t2.transform(s))

Set Point Matching

For common matching transformations, we provide convenience constructors with the match_ prefix (see NumericalTarget for all options). Similar to minimization targets, these constructors inject a suitable transformation computing some form of “proximity” to the set point value.

While you can easily implement your own (potentially complex) matching logic using the CustomTransformation class, let us have a look at how we can match a single set point using built-in constructors:

Absolute Transformation

Transforms

The potentially simplest way to match a set point value is by minimizing the absolute distance to it, since it requires no configuration other than specifying the set point value itself. The match_absolute() constructor allows you to do exactly that in a single line of code.

Example

t_abs = NumericalTarget.match_absolute(name="Size", match_value=42)

Practical Considerations

✅ Simple to use: no configuration required other than the set point value itself

❌ Cannot be used in situations where normalized targets are required

Triangular Transformation

Transforms

In some cases, we want to penalize absolute distance to the set point but only up to a certain threshold, above which any further deviation does not matter. For this purpose, the match_triangular() constructor can be used, which allows us to specify these thresholds in various ways.

Example

t1 = NumericalTarget.match_triangular(name="Size", match_value=42, width=10)
t2 = NumericalTarget.match_triangular(name="Size", match_value=42, cutoffs=(37, 47))
t3 = NumericalTarget.match_triangular(name="Size", match_value=42, margins=(5, 5))
assert t1 == t2 == t3

Practical Considerations

✅ Normalized output: enables direct comparison with other normalized targets

✅ Possibility to directly specify an “acceptable range” around the set point value

❌ Outside the triangular region, the gradient is zero, which can complicate optimization if the thresholds are chosen too tight

Bell Transformation

Transforms

Bell-transformed targets created via the match_bell() constructor can be considered relaxed versions of their triangular counterparts. Unlike the latter, they have no strict cutoff points, resulting in a smooth change in the output with non-zero gradient on the entire domain.

Example

t_bell = NumericalTarget.match_bell(name="Size", match_value=42, sigma=5)

Practical Considerations

✅ Normalized output: enables direct comparison with other normalized targets

✅ Smooth gradient on the entire domain, which can be beneficial for optimization

❌ Width of the bell is sometimes not intuitive to set

Power Transformation

Transforms

If you need more precise control over how strongly deviations from the set point are penalized, you can use the match_power() constructor, which applies a power transformation to the absolute distance. For the common case of squared penalties, we also provide a separate match_quadratic() constructor.

Example

t_power = NumericalTarget.match_power(name="Size", match_value=42, exponent=2)
t_quad = NumericalTarget.match_quadratic(name="Size", match_value=42)
assert t_power == t_quad

Practical Considerations

✅ Offers control over how strongly deviations from the set point are penalized

✅ Smooth gradient on the entire domain, which can be beneficial for optimization

❌ Cannot be used in situations where normalized targets are required

Custom Transformation

If none of the built-in constructors fit your needs because you need more fine-grained control over the matching behavior (e.g. when there are multiple acceptable set points), you always have the fallback option to create a CustomTransformation that implements the corresponding logic and pass it to the regular NumericalTarget constructor.

Target Normalization

Sometimes, it is necessary to normalize targets to the interval [0, 1] – especially when multiple targets are present – in order to align them on a common scale. One situation where this can be required is when combining the targets using a DesirabilityObjective. For this purpose, we provide convenience constructors with the normalized_ prefix:

Ramp Transformation

Transforms

The normalized_ramp() constructor offers the simplest way to create a normalized target. It does so by linearly mapping the target values to the range [0, 1] inside a specified interval and clamping the output outside.

Example

t = NumericalTarget.normalized_ramp(name="Target", cutoffs=(0, 1), descending=True)

Practical Considerations

✅ Easy to interpret: output value changes linearly inside the specified range

❌ Outside the linear region, the gradient is zero, which can complicate optimization if the thresholds are chosen too tight

Sigmoid Transformation

Transforms

The normalized_sigmoid() constructor can be considered a softened version of the ramp transformation. Instead of using hard cutoffs, it smoothly interpolates the target values between 0 and 1 using a sigmoid function.

Example

t = NumericalTarget.normalized_sigmoid(name="Target", anchors=[(-1, 0.1), (1, 0.9)])

Practical Considerations

✅ Smooth gradient on the entire domain, which can be beneficial for optimization

❌ Requires more parameters to configure than the ramp transformation

Normalizing Existing Targets

You can also create a normalized version of an existing target by calling its normalize() method, provided the target already maps to a bounded domain. For brevity and demonstration purposes, we show an example using method chaining:

t = NumericalTarget(name="Target").power(2).clamp(max=1).normalize()

Creation From Existing Targets

Targets can also be quickly created from existing ones by calling certain transformation methods on them (see NumericalTarget for all options).

For example:

t1 = NumericalTarget("Target")
t2 = t1 - 1  # subtract a constant
t3 = t2 / 5  # divide by a constant
t4 = t3.abs()  # compute absolute value
t5 = t4.power(3)  # compute the cube
t6 = t5.clamp(max=10)  # upper-bound to 10 (lower bound is 0 due to abs() call above)
t7 = t6.normalize()  # normalize to [0, 1]

Limitations

Important

NumericalTarget enables many use cases due to the real-valued nature of most measurements. However, it can also be used to model categorical targets if they are ordinal.

For example: If your experimental outcome is a categorical ranking into “bad”, “mediocre” and “good”, you could use a NumericalTarget by pre-mapping the categories to the values 1, 2 and 3, respectively.

If your target category is not ordinal, the transformation into a numerical target is not straightforward, which is a current limitation of BayBE. We are looking into adding more target variants in the future.