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:
Targets are transformed as specified by their
transformation
, with no transformation defined being equivalent to the identity transformation.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¶
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¶
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¶
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¶
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¶
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¶
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.