# Copyright 2023 The TensorFlow Authors. All Rights Reserved. # # 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. """Tests for concat_feature_encoder.""" from absl.testing import parameterized import tensorflow as tf, tf_keras from official.recommendation.uplift import keras_test_case from official.recommendation.uplift.layers.encoders import concat_features class ConcatFeaturesTest(keras_test_case.KerasTestCase, parameterized.TestCase): @parameterized.named_parameters( { "testcase_name": "single_dense", "feature_names": ["feature"], "inputs": {"feature": tf.ones((3, 1))}, "expected_output": tf.ones((3, 1)), }, { "testcase_name": "single_sparse", "feature_names": ["feature"], "inputs": { "feature": tf.sparse.SparseTensor( indices=[[0, 0], [1, 2]], values=[1, 2], dense_shape=[3, 4] ) }, "expected_output": tf.constant( [[1, 0, 0, 0], [0, 0, 2, 0], [0, 0, 0, 0]] ), }, { "testcase_name": "single_ragged", "feature_names": ["feature"], "inputs": {"feature": tf.ragged.constant([[5, 7], [0, 3, 1], [6]])}, "expected_output": tf.constant([[5, 7, 0], [0, 3, 1], [6, 0, 0]]), }, { "testcase_name": "excess_features", "feature_names": ["feature3"], "inputs": { "feature1": tf.ones((3, 1)), "feature2": 2 * tf.ones((3, 1)), "feature3": 3 * tf.ones((3, 1)), }, "expected_output": 3 * tf.ones((3, 1)), }, { "testcase_name": "one_dimensional_features", "feature_names": ["feature1", "feature2"], "inputs": { "feature1": tf.ones((1, 3)), "feature2": 2 * tf.ones((1, 2)), }, "expected_output": tf.constant([1, 1, 1, 2, 2], shape=(1, 5)), }, { "testcase_name": "mixed_features", "feature_names": ["dense", "sparse", "ragged"], "inputs": { "dense": tf.constant([-1.4, 2.0], shape=(2, 1)), "sparse": tf.sparse.SparseTensor( indices=[[0, 1], [1, 0]], values=[2.718, 3.14], dense_shape=[2, 2], ), "ragged": tf.ragged.constant([[5, 7.77], [8]]), "other_feature": tf.ones((2, 5)), }, "expected_output": tf.constant( [[-1.4, 0, 2.718, 5, 7.77], [2.0, 3.14, 0, 8, 0]] ), }, ) def test_layer_correctness(self, feature_names, inputs, expected_output): layer = concat_features.ConcatFeatures(feature_names=feature_names) self.assertAllClose(expected_output, layer(inputs)) @parameterized.named_parameters( { "testcase_name": "none_dimensions", "inputs": { "x1": tf_keras.Input(shape=(2, None, 1, 3)), "x2": tf_keras.Input(shape=(2, None, 1, None)), }, "expected_shape": [None, 2, None, 1, None], }, { "testcase_name": "dense_sparse_ragged", "inputs": { "dense": tf_keras.Input(shape=(2, None, 1, 3), batch_size=10), "sparse": tf_keras.Input(shape=(2, None, 1, 3), sparse=True), "ragged": tf_keras.Input(shape=(2, None, None, 1), ragged=True), }, "expected_shape": [10, 2, None, 1, None], }, ) def test_layer_correctness_keras_inputs(self, inputs, expected_shape): layer = concat_features.ConcatFeatures(feature_names=list(inputs.keys())) output = layer(inputs) KerasTensor = tf_keras.Input(shape=(1,)).__class__ # pylint: disable=invalid-name self.assertIsInstance(output, KerasTensor) self.assertEqual(tf.TensorShape(expected_shape), output.shape) def test_layer_stability(self): layer = concat_features.ConcatFeatures( feature_names=["dense", "sparse", "ragged"] ) inputs = { "dense": tf.constant([-1.4, 2.0], shape=(2, 1)), "sparse": tf.sparse.SparseTensor( indices=[[0, 1], [1, 0]], values=[2.718, 3.14], dense_shape=[2, 2], ), "ragged": tf.ragged.constant([[5, 7.77], [8]]), "other_feature": tf.ones((2, 5)), } self.assertLayerStable(inputs=inputs, layer=layer) def test_layer_savable(self): layer = concat_features.ConcatFeatures( feature_names=["dense", "sparse", "ragged"] ) inputs = { "dense": tf.constant([-1.4, 2.0], shape=(2, 1)), "sparse": tf.sparse.SparseTensor( indices=[[0, 1], [1, 0]], values=[2.718, 3.14], dense_shape=[2, 2], ), "ragged": tf.ragged.constant([[5, 7.77], [8]]), "other_feature": tf.ones((2, 5)), } self.assertLayerSavable(inputs=inputs, layer=layer) def test_missing_input_features(self): layer = concat_features.ConcatFeatures(feature_names=["feature"]) with self.assertRaisesRegex( ValueError, "Layer inputs is missing features*" ): layer({"other_feature": tf.ones((3, 1))}) def test_unsupported_tensor_type(self): class TestType(tf.experimental.ExtensionType): tensor: tf.Tensor layer = concat_features.ConcatFeatures(feature_names=["feature"]) with self.assertRaisesRegex(TypeError, "Got unsupported tensor shape type"): layer({ "feature": TestType(tensor=tf.ones((3, 1))), "other_feature": tf.ones((3, 1)), }) def test_empty_feature_names_list(self): with self.assertRaisesRegex( ValueError, "feature_names must be a non-empty list" ): concat_features.ConcatFeatures(feature_names=[]) def test_non_string_feature_name(self): with self.assertRaisesRegex( TypeError, "feature_names must be a list of strings" ): concat_features.ConcatFeatures(feature_names=["x", 1]) @parameterized.named_parameters( { "testcase_name": "different_shapes_dense", "inputs": { "x1": tf.ones((2, 4)), "x2": tf.ones((1, 4)), }, }, { "testcase_name": "different_shapes_sparse", "inputs": { "x1": tf.ones((10, 4)), "x2": tf.sparse.SparseTensor( indices=[[0, 0], [1, 2]], values=[1, 2], dense_shape=[3, 4] ), }, }, { "testcase_name": "different_shapes_ragged", "inputs": { "x1": tf.ones((2, 2, 2)), "x2": tf.ragged.constant([[5, 7], [0, 3, 1], [6]]), }, }, { "testcase_name": "keras_input_batch_size", "inputs": { "x1": tf_keras.Input(shape=(2, 3), batch_size=10), "x2": tf_keras.Input(shape=(2, 3), batch_size=4), }, }, ) def test_shape_mismatch(self, inputs): layer = concat_features.ConcatFeatures(feature_names=list(inputs.keys())) with self.assertRaisesRegex( ValueError, ( "All features from the feature_names set must be tensors with the" " same shape except for the last dimension" ), ): layer(inputs) @parameterized.named_parameters( { "testcase_name": "different_ranks_dense", "inputs": { "x1": tf.ones((2, 4)), "x2": tf.ones((2, 4, 6)), }, }, { "testcase_name": "different_ranks_sparse", "inputs": { "x1": tf.ones((3, 4, 1)), "x2": tf.sparse.SparseTensor( indices=[[0, 0], [1, 2]], values=[1, 2], dense_shape=[3, 4] ), }, }, { "testcase_name": "different_ranks_ragged", "inputs": { "x1": tf.ones((2, 2, 2, 2)), "x2": tf.ragged.constant([[5, 7], [0, 3, 1], [6]]), }, }, { "testcase_name": "keras_input", "inputs": { "x1": tf_keras.Input(shape=(2, 3, 4), batch_size=4), "x2": tf_keras.Input(shape=(2, 3), batch_size=4), }, }, ) def test_rank_mismatch(self, inputs): layer = concat_features.ConcatFeatures(feature_names=list(inputs.keys())) with self.assertRaisesRegex( ValueError, ( "All features from the feature_names set must be tensors with the" " same shape except for the last dimension" ), ): layer(inputs) def test_layer_config(self): layer = concat_features.ConcatFeatures( feature_names=["feature1", "feature2"], name="encoder", dtype=tf.float64 ) self.assertLayerConfigurable(layer=layer, serializable=True) if __name__ == "__main__": tf.test.main()