Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ torch_transformers = kamae.get_compatible_transformers('torch')
| ListMin | Computes the listwise min of a feature, optionally calculated only on the top items based on another given feature. | [Link](src/kamae/keras/tensorflow/layers/list_min.py) | TensorFlow-only | [Link](src/kamae/spark/transformers/list_min.py) |
| ListRank | Computes the listwise rank (ordering) of a feature. | [Link](src/kamae/keras/tensorflow/layers/list_rank.py) | TensorFlow-only | [Link](src/kamae/spark/transformers/list_rank.py) |
| ListStdDev | Computes the listwise standard deviation of a feature, optionally calculated only on the top items based on another given feature. | [Link](src/kamae/keras/tensorflow/layers/list_std_dev.py) | TensorFlow-only | [Link](src/kamae/spark/transformers/list_std_dev.py) |
| ListSum | Computes the listwise sum of a feature, optionally calculated only on the top items based on another given feature. | [Link](src/kamae/keras/tensorflow/layers/list_sum.py) | TensorFlow-only | [Link](src/kamae/spark/transformers/list_sum.py) |
| Log | Applies the natural logarithm `log(alpha + x)` transform . | [Link](src/kamae/keras/core/layers/log.py) | Multi-backend | [Link](src/kamae/spark/transformers/log.py) |
| LogicalAnd | Performs an and(x, y) operation on multiple boolean features. | [Link](src/kamae/keras/core/layers/logical_and.py) | Multi-backend | [Link](src/kamae/spark/transformers/logical_and.py) |
| LogicalNot | Performs a not(x) operation on a single boolean feature. | [Link](src/kamae/keras/core/layers/logical_not.py) | Multi-backend | [Link](src/kamae/spark/transformers/logical_not.py) |
Expand Down
2 changes: 2 additions & 0 deletions src/kamae/keras/tensorflow/layers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from .list_min import ListMinLayer # noqa: F401
from .list_rank import ListRankLayer # noqa: F401
from .list_std_dev import ListStdDevLayer # noqa: F401
from .list_sum import ListSumLayer # noqa: F401
from .min_hash_index import MinHashIndexLayer # noqa: F401
from .one_hot_encode import OneHotEncodeLayer # noqa: F401
from .ordinal_array_encode import OrdinalArrayEncodeLayer # noqa: F401
Expand Down Expand Up @@ -75,6 +76,7 @@
"ListMinLayer",
"ListRankLayer",
"ListStdDevLayer",
"ListSumLayer",
"MinHashIndexLayer",
"OneHotEncodeLayer",
"OrdinalArrayEncodeLayer",
Expand Down
190 changes: 190 additions & 0 deletions src/kamae/keras/tensorflow/layers/list_sum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# Copyright [2024] Expedia, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Any, Dict, Iterable, List, Optional

import keras
import tensorflow as tf
from keras import KerasTensor

import kamae
from kamae.keras.core.backend import TENSORFLOW_ONLY
from kamae.keras.core.base import BaseLayer
from kamae.keras.core.utils.input_utils import allow_single_or_multiple_tensor_input
from kamae.keras.tensorflow.utils.list_utils import get_top_n, segmented_operation
from kamae.keras.tensorflow.utils.transform_utils import map_fn_w_axis


@tf.keras.utils.register_keras_serializable(package=kamae.__name__)
class ListSumLayer(BaseLayer):
"""
Calculate the sum across the axis dimension.
- If one tensor is passed, the transformer calculates the sum of the tensor
based on all the items in the given axis dimension.
- If inputCols is set,
- If with_segment = True: the layer calculates the sum of the first tensor
segmented by values of the second tensor.
Example: calculate the sum price of hotels within star ratings

- If with_segment = False: the layer calculates the sum of the first tensor
based on second tensor's topN items in the same given axis dimension.
By using the topN items to calculate the statistics, we can better approximate
the real statistics in production. It is suggested to use a large enough topN to
get a good approximation of the statistics, and an important feature to sort on,
such as item's past production.

Example: calculate the sum price in the same query, based only on the top N
items sorted by descending production.
Comment on lines +32 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's ensure consistent indentation here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/I think you have two separate paragraphs flowing together

"""

supported_backends = TENSORFLOW_ONLY
jit_compatible = True

def __init__(
self,
name: Optional[str] = None,
input_dtype: Optional[str] = None,
output_dtype: Optional[str] = None,
top_n: Optional[int] = None,
sort_order: str = "asc",
with_segment: bool = False,
min_filter_value: Optional[float] = None,
nan_fill_value: float = 0.0,
axis: int = 1,
**kwargs: Any,
) -> None:
"""
Initializes the Listwise Sum layer.

WARNING: The code is fully tested for axis=1 only. Further testing is needed.

WARNING: The code can be affected by the value of the padding items. Always
make sure to filter out the padding items value with min_filter_value.

:param name: Name of the layer, defaults to `None`.
:param input_dtype: The dtype to cast the input to. Defaults to `None`.
:param output_dtype: The dtype to cast the output to. Defaults to `None`.
:param top_n: The number of top items to consider when calculating the sum.
:param sort_order: The order to sort the second tensor by. Defaults to `asc`.
:param with_segment: Whether the second tensor should be used for segmentation (True)
or sorting (False). Defaults to False.
:param min_filter_value: The minimum filter value to ignore values during
calculation. Defaults to None (no filter).
:param nan_fill_value: The value to fill NaNs results with. Defaults to 0.
:param axis: The axis to calculate the statistics across. Defaults to 1.
"""
super().__init__(
name=name, input_dtype=input_dtype, output_dtype=output_dtype, **kwargs
)
self.top_n = top_n
self.sort_order = sort_order
self.min_filter_value = min_filter_value
self.nan_fill_value = nan_fill_value

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this used anywhere? let's add a test in parity check that with a non-default nan_fill_value

self.axis = axis
self.with_segment = with_segment

@property
def compatible_dtypes(self) -> Optional[List[str]]:
"""
Returns the compatible dtypes of the layer.

:returns: The compatible dtypes of the layer.
"""
return [
"bfloat16",
"float16",
"float32",
"float64",
"int8",
"int16",
"int32",
"int64",
"string",
]

@allow_single_or_multiple_tensor_input
def _call(self, inputs: Iterable[KerasTensor], **kwargs: Any) -> KerasTensor:
"""
Calculate the listwise sum, optionally sorting and
filtering based on the second input tensor, or segmenting
based on the second input tensor. Behaviour is set by with_segment.

:param inputs: The iterable tensor for the feature.
:returns: The new tensor result column.
"""
val_tensor = inputs[0]
output_shape = tf.shape(val_tensor)

# Define use of second input
if len(inputs) == 2:
if self.with_segment:
segment_tensor = inputs[1]
else:
sort_tensor = inputs[1]
if self.top_n is None:
raise ValueError("topN must be specified when using a sort column.")
val_tensor = get_top_n(
val_tensor=val_tensor,
axis=self.axis,
sort_tensor=sort_tensor,
sort_order=self.sort_order,
top_n=self.top_n,
)
else:
if self.with_segment:
raise ValueError("with_segment set to True, expected two inputs.")

# Values excluded by the min filter contribute 0 to the sum.
# Kept int/string-safe (no float-only ops), mirroring ListMaxLayer, so
# integer value columns and string segment columns both work.
if self.min_filter_value is not None:
mask = tf.greater_equal(val_tensor, self.min_filter_value)
val_tensor = tf.where(mask, val_tensor, tf.zeros_like(val_tensor))

# Apply segmented calculation
if self.with_segment:
listwise_sum = map_fn_w_axis(
elems=[val_tensor, segment_tensor],
fn=lambda x: segmented_operation(x, tf.math.unsorted_segment_sum),
axis=self.axis,
fn_output_signature=tf.TensorSpec(
shape=val_tensor.shape[self.axis :], dtype=val_tensor.dtype
),
)
listwise_sum = tf.ensure_shape(listwise_sum, val_tensor.shape)
else:
listwise_sum = tf.reduce_sum(val_tensor, axis=self.axis, keepdims=True)
listwise_sum = tf.broadcast_to(listwise_sum, output_shape)

return listwise_sum

def get_config(self) -> Dict[str, Any]:
"""
Gets the configuration of the layer.
Used for saving and loading from a model.

:returns: Dictionary of the configuration of the layer.
"""
config = super().get_config()
config.update(
{
"top_n": self.top_n,
"sort_order": self.sort_order,
"min_filter_value": self.min_filter_value,
"nan_fill_value": self.nan_fill_value,
"axis": self.axis,
"with_segment": self.with_segment,
}
)
return config
1 change: 1 addition & 0 deletions src/kamae/spark/transformers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from .list_min import ListMinTransformer # noqa: F401
from .list_rank import ListRankTransformer # noqa: F401
from .list_std_dev import ListStdDevTransformer # noqa: F401
from .list_sum import ListSumTransformer # noqa: F401
from .log import LogTransformer # noqa: F401
from .logical_and import LogicalAndTransformer # noqa: F401
from .logical_not import LogicalNotTransformer # noqa: F401
Expand Down
Loading
Loading