Skip to content

Diverse useful functions in TensorFlow

Diverse Tensorflow operations used in the ChoiceLearn library.

CustomCategoricalCrossEntropy

Bases: Loss

Custom Categorical Cross Entropy Loss. Handles all options in one place.

Follows structure of tf.keras.losses.CategoricalCrossentropy and its different possibilities.

Parameters:

Name Type Description Default
from_logits bool

Whether to compute the softmax from logits or probabilities, by default False

False
sparse bool

Whether the choice labels are integers(True) or one-hot encoded(False), by default False

False
label_smoothing float

Value of smoothing to apply to labels, by default 0.0 Smoothing applied is 1.0 - label_smoothing for chosen item and label_smoothing / (num_items - 1) for all other items

0.0
axis int

Axis on which to compute the softmax. Used only if from_logits is True, by default -1

-1
epsilon float

Value to apply to avoid computation issues in log, by default 1e-10

1e-10
Source code in choice_learn/tf_ops.py
class CustomCategoricalCrossEntropy(tf.keras.losses.Loss):
    """Custom Categorical Cross Entropy Loss. Handles all options in one place.

    Follows structure of tf.keras.losses.CategoricalCrossentropy and its different possibilities.

    Parameters
    ----------
    from_logits : bool, optional
        Whether to compute the softmax from logits or probabilities, by default False
    sparse : bool, optional
        Whether the choice labels are integers(True) or one-hot encoded(False), by default False
    label_smoothing : float, optional
        Value of smoothing to apply to labels, by default 0.0
        Smoothing applied is 1.0 - label_smoothing for chosen item and
        label_smoothing / (num_items - 1) for all other items
    axis : int, optional
        Axis on which to compute the softmax. Used only if from_logits is True, by default -1
    epsilon : float, optional
        Value to apply to avoid computation issues in log, by default 1e-10
    """

    def __init__(
        self,
        from_logits=False,
        sparse=False,
        label_smoothing=0.0,
        axis=-1,
        epsilon=1e-10,
        name="eps_categorical_crossentropy",
        reduction="sum_over_batch_size",
    ):
        """Initialize function.

        Follows structure of tf.keras.losses.CategoricalCrossentropy.

        Parameters
        ----------
        from_logits : bool, optional
            Whether to compute the softmax from logits or probabilities, by default False
        sparse : bool, optional
            Whether the choice labels are integers(True) or one-hot encoded(False), by default False
        label_smoothing : float, optional
            Value of smoothing to apply to labels, by default 0.0
            Smoothing applied is 1.0 - label_smoothing for chosen item and
            label_smoothing / (num_items - 1) for all other items
        axis : int, optional
            Axis on which to compute the softmax. Used only if from_logits is True, by default -1
        epsilon : float, optional
            Value to apply to avoid computation issues in log, by default 1e-10
        name: str
            Name of the loss function - here to follow tf.keras.losses.Loss signature
        reduction:
            Reduction function - here to follow tf.keras.losses.Loss signature
        """
        super().__init__(reduction=reduction, name=name)
        self.label_smoothing = label_smoothing
        self.from_logits = from_logits
        self.sparse = sparse
        self.axis = axis
        self.epsilon = epsilon

    def call(self, y_true, y_pred):
        """Compute the cross-entropy loss.

        Parameters
        ----------
        y_true : np.ndarray | tf.Tenosr
            Ground truth labels
        y_pred : np.ndarray | tf.Tenosr
            Predicted labels

        Returns
        -------
        tf.Tensor
            Average Cross-Entropy loss
        """
        if self.from_logits:  # Apply softmax if utilities are given
            y_pred = tf.nn.softmax(y_pred, axis=self.axis)
        else:
            y_pred = tf.convert_to_tensor(y_pred)
        if self.sparse:  # Create OneHot labels if sparse labels are given
            y_true = tf.one_hot(y_true, depth=tf.shape(y_pred)[self.axis])
        else:
            y_true = tf.cast(y_true, y_pred.dtype)

        # Smooth labels
        if self.label_smoothing > 0:
            label_smoothing = tf.convert_to_tensor(self.label_smoothing, dtype=y_pred.dtype)
            num_classes = tf.cast(tf.shape(y_true)[self.axis], y_pred.dtype)
            y_true = y_true * (1.0 - label_smoothing) + (label_smoothing / num_classes)

        # Apply label clipping to avoid log(0) and such issues
        y_pred = tf.clip_by_value(y_pred, self.epsilon, 1.0 - self.epsilon)
        return -tf.reduce_sum(y_true * tf.math.log(y_pred), axis=self.axis)

__init__(from_logits=False, sparse=False, label_smoothing=0.0, axis=-1, epsilon=1e-10, name='eps_categorical_crossentropy', reduction='sum_over_batch_size')

Initialize function.

Follows structure of tf.keras.losses.CategoricalCrossentropy.

Parameters:

Name Type Description Default
from_logits bool

Whether to compute the softmax from logits or probabilities, by default False

False
sparse bool

Whether the choice labels are integers(True) or one-hot encoded(False), by default False

False
label_smoothing float

Value of smoothing to apply to labels, by default 0.0 Smoothing applied is 1.0 - label_smoothing for chosen item and label_smoothing / (num_items - 1) for all other items

0.0
axis int

Axis on which to compute the softmax. Used only if from_logits is True, by default -1

-1
epsilon float

Value to apply to avoid computation issues in log, by default 1e-10

1e-10
name

Name of the loss function - here to follow tf.keras.losses.Loss signature

'eps_categorical_crossentropy'
reduction

Reduction function - here to follow tf.keras.losses.Loss signature

'sum_over_batch_size'
Source code in choice_learn/tf_ops.py
def __init__(
    self,
    from_logits=False,
    sparse=False,
    label_smoothing=0.0,
    axis=-1,
    epsilon=1e-10,
    name="eps_categorical_crossentropy",
    reduction="sum_over_batch_size",
):
    """Initialize function.

    Follows structure of tf.keras.losses.CategoricalCrossentropy.

    Parameters
    ----------
    from_logits : bool, optional
        Whether to compute the softmax from logits or probabilities, by default False
    sparse : bool, optional
        Whether the choice labels are integers(True) or one-hot encoded(False), by default False
    label_smoothing : float, optional
        Value of smoothing to apply to labels, by default 0.0
        Smoothing applied is 1.0 - label_smoothing for chosen item and
        label_smoothing / (num_items - 1) for all other items
    axis : int, optional
        Axis on which to compute the softmax. Used only if from_logits is True, by default -1
    epsilon : float, optional
        Value to apply to avoid computation issues in log, by default 1e-10
    name: str
        Name of the loss function - here to follow tf.keras.losses.Loss signature
    reduction:
        Reduction function - here to follow tf.keras.losses.Loss signature
    """
    super().__init__(reduction=reduction, name=name)
    self.label_smoothing = label_smoothing
    self.from_logits = from_logits
    self.sparse = sparse
    self.axis = axis
    self.epsilon = epsilon

call(y_true, y_pred)

Compute the cross-entropy loss.

Parameters:

Name Type Description Default
y_true ndarray | Tenosr

Ground truth labels

required
y_pred ndarray | Tenosr

Predicted labels

required

Returns:

Type Description
Tensor

Average Cross-Entropy loss

Source code in choice_learn/tf_ops.py
def call(self, y_true, y_pred):
    """Compute the cross-entropy loss.

    Parameters
    ----------
    y_true : np.ndarray | tf.Tenosr
        Ground truth labels
    y_pred : np.ndarray | tf.Tenosr
        Predicted labels

    Returns
    -------
    tf.Tensor
        Average Cross-Entropy loss
    """
    if self.from_logits:  # Apply softmax if utilities are given
        y_pred = tf.nn.softmax(y_pred, axis=self.axis)
    else:
        y_pred = tf.convert_to_tensor(y_pred)
    if self.sparse:  # Create OneHot labels if sparse labels are given
        y_true = tf.one_hot(y_true, depth=tf.shape(y_pred)[self.axis])
    else:
        y_true = tf.cast(y_true, y_pred.dtype)

    # Smooth labels
    if self.label_smoothing > 0:
        label_smoothing = tf.convert_to_tensor(self.label_smoothing, dtype=y_pred.dtype)
        num_classes = tf.cast(tf.shape(y_true)[self.axis], y_pred.dtype)
        y_true = y_true * (1.0 - label_smoothing) + (label_smoothing / num_classes)

    # Apply label clipping to avoid log(0) and such issues
    y_pred = tf.clip_by_value(y_pred, self.epsilon, 1.0 - self.epsilon)
    return -tf.reduce_sum(y_true * tf.math.log(y_pred), axis=self.axis)

ExactCategoricalCrossEntropy

Bases: Loss

Custom Categorical Cross Entropy Loss. Handles all options in one place.

Follows structure of tf.keras.losses.CategoricalCrossentropy and its different possibilities.

Parameters:

Name Type Description Default
from_logits bool

Whether to compute the softmax from logits or probabilities, by default False

False
sparse bool

Whether the choice labels are integers(True) or one-hot encoded(False), by default False

False
label_smoothing float

Value of smoothing to apply to labels, by default 0.0 Smoothing applied is 1.0 - label_smoothing for chosen item and label_smoothing / (num_items - 1)for all other items

required
axis int

Axis on which to compute the softmax. Used only if from_logits is True, by default -1

-1
epsilon float

Value to apply to avoid computation issues in log, by default 1e-10

1e-35
Source code in choice_learn/tf_ops.py
class ExactCategoricalCrossEntropy(tf.keras.losses.Loss):
    """Custom Categorical Cross Entropy Loss. Handles all options in one place.

    Follows structure of tf.keras.losses.CategoricalCrossentropy and its different possibilities.

    Parameters
    ----------
    from_logits : bool, optional
        Whether to compute the softmax from logits or probabilities, by default False
    sparse : bool, optional
        Whether the choice labels are integers(True) or one-hot encoded(False), by default False
    label_smoothing : float, optional
        Value of smoothing to apply to labels, by default 0.0
        Smoothing applied is 1.0 - label_smoothing for chosen item and
        label_smoothing / (num_items - 1)for all other items
    axis : int, optional
        Axis on which to compute the softmax. Used only if from_logits is True, by default -1
    epsilon : float, optional
        Value to apply to avoid computation issues in log, by default 1e-10
    """

    def __init__(
        self,
        from_logits=False,
        sparse=False,
        axis=-1,
        epsilon=1e-35,
        name="exact_categorical_crossentropy",
        reduction="sum_over_batch_size",
    ):
        """Initialize function.

        Follows structure of tf.keras.losses.CategoricalCrossentropy.

        Parameters
        ----------
        from_logits : bool, optional
            Whether to compute the softmax from logits or probabilities, by default False
        sparse : bool, optional
            Whether the choice labels are integers(True) or one-hot encoded(False), by default False
        axis : int, optional
            Axis on which to compute the softmax. Used only if from_logits is True, by default -1
        epsilon : float, optional
            Value to apply to avoid computation issues in log, by default 1e-10
        name: str
            Name of the loss function - here to follow tf.keras.losses.Loss signature
        reduction:
            Reduction function - here to follow tf.keras.losses.Loss signature
        """
        super().__init__(reduction=reduction, name=name)
        self.from_logits = from_logits
        self.sparse = sparse
        self.axis = axis
        self.epsilon = epsilon

    def call(self, y_true, y_pred):
        """Compute the cross-entropy loss.

        Parameters
        ----------
        y_true : np.ndarray | tf.Tensor
            Ground truth labels
        y_pred : np.ndarray | tf.Tensor
            Predicted labels

        Returns
        -------
        tf.Tensor
            Average Cross-Entropy loss
        """
        if self.from_logits:  # Apply softmax if utilities are given
            y_pred = tf.nn.softmax(y_pred, axis=self.axis)
        else:
            y_pred = tf.convert_to_tensor(y_pred)
        if self.sparse:  # Create OneHot labels if sparse labels are given
            y_true = tf.one_hot(y_true, depth=tf.shape(y_pred)[self.axis])
        else:
            y_true = tf.cast(y_true, y_pred.dtype)

        # Apply label clipping to avoid log(0) and such issues
        y_pred = tf.clip_by_value(y_pred, self.epsilon, 1.0 - self.epsilon)
        return -tf.reduce_sum(y_true * tf.math.log(y_pred), axis=self.axis)

__init__(from_logits=False, sparse=False, axis=-1, epsilon=1e-35, name='exact_categorical_crossentropy', reduction='sum_over_batch_size')

Initialize function.

Follows structure of tf.keras.losses.CategoricalCrossentropy.

Parameters:

Name Type Description Default
from_logits bool

Whether to compute the softmax from logits or probabilities, by default False

False
sparse bool

Whether the choice labels are integers(True) or one-hot encoded(False), by default False

False
axis int

Axis on which to compute the softmax. Used only if from_logits is True, by default -1

-1
epsilon float

Value to apply to avoid computation issues in log, by default 1e-10

1e-35
name

Name of the loss function - here to follow tf.keras.losses.Loss signature

'exact_categorical_crossentropy'
reduction

Reduction function - here to follow tf.keras.losses.Loss signature

'sum_over_batch_size'
Source code in choice_learn/tf_ops.py
def __init__(
    self,
    from_logits=False,
    sparse=False,
    axis=-1,
    epsilon=1e-35,
    name="exact_categorical_crossentropy",
    reduction="sum_over_batch_size",
):
    """Initialize function.

    Follows structure of tf.keras.losses.CategoricalCrossentropy.

    Parameters
    ----------
    from_logits : bool, optional
        Whether to compute the softmax from logits or probabilities, by default False
    sparse : bool, optional
        Whether the choice labels are integers(True) or one-hot encoded(False), by default False
    axis : int, optional
        Axis on which to compute the softmax. Used only if from_logits is True, by default -1
    epsilon : float, optional
        Value to apply to avoid computation issues in log, by default 1e-10
    name: str
        Name of the loss function - here to follow tf.keras.losses.Loss signature
    reduction:
        Reduction function - here to follow tf.keras.losses.Loss signature
    """
    super().__init__(reduction=reduction, name=name)
    self.from_logits = from_logits
    self.sparse = sparse
    self.axis = axis
    self.epsilon = epsilon

call(y_true, y_pred)

Compute the cross-entropy loss.

Parameters:

Name Type Description Default
y_true ndarray | Tensor

Ground truth labels

required
y_pred ndarray | Tensor

Predicted labels

required

Returns:

Type Description
Tensor

Average Cross-Entropy loss

Source code in choice_learn/tf_ops.py
def call(self, y_true, y_pred):
    """Compute the cross-entropy loss.

    Parameters
    ----------
    y_true : np.ndarray | tf.Tensor
        Ground truth labels
    y_pred : np.ndarray | tf.Tensor
        Predicted labels

    Returns
    -------
    tf.Tensor
        Average Cross-Entropy loss
    """
    if self.from_logits:  # Apply softmax if utilities are given
        y_pred = tf.nn.softmax(y_pred, axis=self.axis)
    else:
        y_pred = tf.convert_to_tensor(y_pred)
    if self.sparse:  # Create OneHot labels if sparse labels are given
        y_true = tf.one_hot(y_true, depth=tf.shape(y_pred)[self.axis])
    else:
        y_true = tf.cast(y_true, y_pred.dtype)

    # Apply label clipping to avoid log(0) and such issues
    y_pred = tf.clip_by_value(y_pred, self.epsilon, 1.0 - self.epsilon)
    return -tf.reduce_sum(y_true * tf.math.log(y_pred), axis=self.axis)

NoiseConstrastiveEstimation

Bases: Loss

Noise Contrastive Estimation Loss.

Source code in choice_learn/tf_ops.py
class NoiseConstrastiveEstimation(tf.keras.losses.Loss):
    """Noise Contrastive Estimation Loss."""

    def __init__(
        self,
        epsilon=1e-10,
        name="noise_contrastive_estimation",
        reduction="sum_over_batch_size",
    ):
        """Initialize function.

        Follows structure of tf.keras.losses.CategoricalCrossentropy.

        Parameters
        ----------
        epsilon: float
            safety net for logarithm computation
        name: str
            Name of the loss function - here to follow tf.keras.losses.Loss signature
        reduction:
            Reduction function - here to follow tf.keras.losses.Loss signature
        """
        self.epsilon = epsilon
        super().__init__(reduction=reduction, name=name)

    def __call__(self, logit_true, logit_negative, freq_true, freq_negative, sample_weight=None):
        """Overide of the tf.keras __call__ function.

        Parameters
        ----------
        logit_true : np.ndarray | tf.Tensor
            Ground truth samplelogits
        logit_negative : np.ndarray | tf.Tensor
            Negative samples logits
        freq_true : np.ndarray | tf.Tensor
            Frequency / probabilty in data of true sample
        freq_negative : np.ndarray | tf.Tensor
            Frequency / probabilty in data of negative samples
        sample_weight : np.ndarray | tf.Tensor
            samplewise weights

        Returns
        -------
        tf.Tensor
            Average Cross-Entropy loss
        """
        losses = self.call(logit_true, logit_negative, freq_true, freq_negative)
        if sample_weight is not None:
            losses = losses * sample_weight
        return tf.reduce_sum(losses)

    def call(self, logit_true, logit_negative, freq_true, freq_negative):
        """Compute NCE loss.

        Parameters
        ----------
        logit_true : np.ndarray | tf.Tensor
            Ground truth samples logit
        logit_negative : np.ndarray | tf.Tensor
            Negative samples logit
        freq_true : np.ndarray | tf.Tensor
            Frequency / probabilty in data of true sample
        freq_negative : np.ndarray | tf.Tensor
            Frequency / probabilty in data of negative samples

        Returns
        -------
        tf.Tensor
            Average Cross-Entropy loss
        """
        n_negative_samples = tf.cast(tf.shape(logit_negative)[1], tf.float32)

        true_contribution = 1 / (1 + n_negative_samples * freq_true * tf.exp(-logit_true))
        noise_contribution = 1 - (
            1 / (1 + n_negative_samples * freq_negative * tf.exp(-logit_negative))
        )
        return -tf.math.log(true_contribution + self.epsilon) - tf.reduce_sum(
            tf.math.log(noise_contribution + self.epsilon), axis=-1
        )

__call__(logit_true, logit_negative, freq_true, freq_negative, sample_weight=None)

Overide of the tf.keras call function.

Parameters:

Name Type Description Default
logit_true ndarray | Tensor

Ground truth samplelogits

required
logit_negative ndarray | Tensor

Negative samples logits

required
freq_true ndarray | Tensor

Frequency / probabilty in data of true sample

required
freq_negative ndarray | Tensor

Frequency / probabilty in data of negative samples

required
sample_weight ndarray | Tensor

samplewise weights

None

Returns:

Type Description
Tensor

Average Cross-Entropy loss

Source code in choice_learn/tf_ops.py
def __call__(self, logit_true, logit_negative, freq_true, freq_negative, sample_weight=None):
    """Overide of the tf.keras __call__ function.

    Parameters
    ----------
    logit_true : np.ndarray | tf.Tensor
        Ground truth samplelogits
    logit_negative : np.ndarray | tf.Tensor
        Negative samples logits
    freq_true : np.ndarray | tf.Tensor
        Frequency / probabilty in data of true sample
    freq_negative : np.ndarray | tf.Tensor
        Frequency / probabilty in data of negative samples
    sample_weight : np.ndarray | tf.Tensor
        samplewise weights

    Returns
    -------
    tf.Tensor
        Average Cross-Entropy loss
    """
    losses = self.call(logit_true, logit_negative, freq_true, freq_negative)
    if sample_weight is not None:
        losses = losses * sample_weight
    return tf.reduce_sum(losses)

__init__(epsilon=1e-10, name='noise_contrastive_estimation', reduction='sum_over_batch_size')

Initialize function.

Follows structure of tf.keras.losses.CategoricalCrossentropy.

Parameters:

Name Type Description Default
epsilon

safety net for logarithm computation

1e-10
name

Name of the loss function - here to follow tf.keras.losses.Loss signature

'noise_contrastive_estimation'
reduction

Reduction function - here to follow tf.keras.losses.Loss signature

'sum_over_batch_size'
Source code in choice_learn/tf_ops.py
def __init__(
    self,
    epsilon=1e-10,
    name="noise_contrastive_estimation",
    reduction="sum_over_batch_size",
):
    """Initialize function.

    Follows structure of tf.keras.losses.CategoricalCrossentropy.

    Parameters
    ----------
    epsilon: float
        safety net for logarithm computation
    name: str
        Name of the loss function - here to follow tf.keras.losses.Loss signature
    reduction:
        Reduction function - here to follow tf.keras.losses.Loss signature
    """
    self.epsilon = epsilon
    super().__init__(reduction=reduction, name=name)

call(logit_true, logit_negative, freq_true, freq_negative)

Compute NCE loss.

Parameters:

Name Type Description Default
logit_true ndarray | Tensor

Ground truth samples logit

required
logit_negative ndarray | Tensor

Negative samples logit

required
freq_true ndarray | Tensor

Frequency / probabilty in data of true sample

required
freq_negative ndarray | Tensor

Frequency / probabilty in data of negative samples

required

Returns:

Type Description
Tensor

Average Cross-Entropy loss

Source code in choice_learn/tf_ops.py
def call(self, logit_true, logit_negative, freq_true, freq_negative):
    """Compute NCE loss.

    Parameters
    ----------
    logit_true : np.ndarray | tf.Tensor
        Ground truth samples logit
    logit_negative : np.ndarray | tf.Tensor
        Negative samples logit
    freq_true : np.ndarray | tf.Tensor
        Frequency / probabilty in data of true sample
    freq_negative : np.ndarray | tf.Tensor
        Frequency / probabilty in data of negative samples

    Returns
    -------
    tf.Tensor
        Average Cross-Entropy loss
    """
    n_negative_samples = tf.cast(tf.shape(logit_negative)[1], tf.float32)

    true_contribution = 1 / (1 + n_negative_samples * freq_true * tf.exp(-logit_true))
    noise_contribution = 1 - (
        1 / (1 + n_negative_samples * freq_negative * tf.exp(-logit_negative))
    )
    return -tf.math.log(true_contribution + self.epsilon) - tf.reduce_sum(
        tf.math.log(noise_contribution + self.epsilon), axis=-1
    )

softmax_with_availabilities(items_logit_by_choice, available_items_by_choice, axis=-1, normalize_exit=False, eps=1e-05)

Compute softmax probabilities from utilities.

Takes into account availabilities (1 if the product is available, 0 otherwise) to set probabilities to 0 for unavailable products and to renormalize the probabilities of available products.

Parameters:

Name Type Description Default
items_logit_by_choice ndarray(n_choices, n_items)

Utilities / Logits on which to compute the softmax

required
available_items_by_choice ndarray(n_choices, n_items)

Matrix indicating the availability (1) or not (0) of the products

required
axis int

Axis of items_logit_by_choice on which to apply the softmax, by default -1

-1
normalize_exit bool

Whether to normalize the probabilities of available products with an exit choice of utility 1, by default False

False
eps float

Value to avoid division by 0 when a product with probability almost 1 is unavailable, by default 1e-5

1e-05

Returns:

Type Description
Tensor(n_choices, n_items)

Probabilities of each product for each choice computed from Logits

Source code in choice_learn/tf_ops.py
def softmax_with_availabilities(
    items_logit_by_choice, available_items_by_choice, axis=-1, normalize_exit=False, eps=1e-5
):
    """Compute softmax probabilities from utilities.

    Takes into account availabilities (1 if the product is available, 0 otherwise) to set
    probabilities to 0 for unavailable products and to renormalize the probabilities of
    available products.

    Parameters
    ----------
    items_logit_by_choice : np.ndarray (n_choices, n_items)
        Utilities / Logits on which to compute the softmax
    available_items_by_choice : np.ndarray (n_choices, n_items)
        Matrix indicating the availability (1) or not (0) of the products
    axis : int, optional
        Axis of items_logit_by_choice on which to apply the softmax, by default -1
    normalize_exit : bool, optional
        Whether to normalize the probabilities of available products with an exit choice of
        utility 1, by default False
    eps : float, optional
        Value to avoid division by 0 when a product with probability almost 1 is unavailable,
        by default 1e-5

    Returns
    -------
    tf.Tensor (n_choices, n_items)
        Probabilities of each product for each choice computed from Logits
    """
    # Substract max utility to avoid overflow
    large_negative = tf.constant(-1e9, dtype=items_logit_by_choice.dtype)
    masked_logits = tf.where(available_items_by_choice > 0.5, items_logit_by_choice, large_negative)

    normalizer = tf.reduce_max(masked_logits, axis=axis, keepdims=True)
    numerator = tf.exp(masked_logits - normalizer)

    # Set unavailable products utility to 0
    numerator = tf.multiply(numerator, available_items_by_choice)
    # Sum of total available utilities
    denominator = tf.reduce_sum(numerator, axis=axis, keepdims=True)

    # Add 1 to the denominator to take into account the exit choice
    if normalize_exit:
        denominator += tf.exp(-normalizer)
    # Avoid division by 0 when only unavailable items have highest utilities
    elif eps:
        denominator += eps

    # Compute softmax
    return numerator / denominator