class ConcavePolyCurve(Module):
def __init__(
self,
encodings: pd.DataFrame,
hierarchy_categories: list[str | list[str]] | None = None,
hyperparameters: Hyperparams | None = None,
name: str | None = None,
):
super().__init__(hyperparameters=hyperparameters, name=name)
self.encodings = encodings
self.hierarchy_categories = hierarchy_categories
def build(self, input_shapes): # noqa: U100
self.min_exponent = self.hyperparameters.get_float(
"min_exponent",
default=0.1,
min=0.01,
max=0.99,
help="The minimum value to use for the exponent in the polynomial. Can range from this to 1.0.",
)
self.params_emb_layer = self.hyperparameters.get_submodule(
"params_emb",
module_type=HierchicalEmbedding,
kwargs=dict(
encodings=self.encodings,
shape=[1],
columns=self.hierarchy_categories,
feature_names=["exponent"],
),
help="The parameter embeddings for the concave polynomial curve layer.",
)
def __call__(
self,
batch: CurveInput,
training=False,
debug: bool = False,
skip_metrics: bool = False,
) -> ConcavePolyCurveIntermediaries: # noqa: U100
"""Computes the value of the custom function f(x) = (1-c)(x)^p + c with custom bounds."""
exponent_emb = tf.squeeze(
tf.expand_dims(
self.params_emb_layer(batch.hierarchy, training=training, skip_metrics=skip_metrics, debug=debug), 1
),
axis=3,
)
# Exponent value is lower bounded to 0.1 to avoid extreme flat curves
exponent = monotonic_sigmoid(exponent_emb) * (1 - self.min_exponent) + self.min_exponent
# Epsilon is added to the signal to avoid nan-losses
impact_by_signal = tf.math.pow(batch.spends + tf.constant(EPSILON, dtype=tf.float32), exponent)
return ConcavePolyCurveIntermediaries(
exponent_emb=exponent_emb if debug else None,
exponent=exponent if debug else None,
impact_by_signal=impact_by_signal,
)