Repository: sql-machine-learning/elasticdl

Issue:
support LogRound preprocessing layer
We could use implement it based on Discretization layer with a default bin [0, 2, 4, 8, ...]


Discussion:

Resolving patch:
diff --git a/elasticdl_preprocessing/layers/__init__.py b/elasticdl_preprocessing/layers/__init__.py
--- a/elasticdl_preprocessing/layers/__init__.py
+++ b/elasticdl_preprocessing/layers/__init__.py
@@ -7,6 +7,7 @@
 from elasticdl_preprocessing.layers.discretization import Discretization
 from elasticdl_preprocessing.layers.hashing import Hashing
 from elasticdl_preprocessing.layers.index_lookup import IndexLookup
+from elasticdl_preprocessing.layers.log_round import LogRound
 from elasticdl_preprocessing.layers.round_identity import RoundIdentity
 from elasticdl_preprocessing.layers.to_number import ToNumber
 from elasticdl_preprocessing.layers.to_ragged import ToRagged
diff --git a/elasticdl_preprocessing/layers/log_round.py b/elasticdl_preprocessing/layers/log_round.py
new file mode 100644
--- /dev/null
+++ b/elasticdl_preprocessing/layers/log_round.py
@@ -0,0 +1,85 @@
+import tensorflow as tf
+from tensorflow.python.ops.ragged import ragged_functional_ops, ragged_tensor
+
+
+def log(x, base):
+    x = tf.cast(x, tf.float64)
+    numerator = tf.math.log(x)
+
+    if base is None:
+        return numerator
+
+    denominator = tf.math.log(tf.constant(base, dtype=numerator.dtype))
+    return numerator / denominator
+
+
+class LogRound(tf.keras.layers.Layer):
+    """Cast a numeric value into a discrete integer value by
+    `round(log(x))`.
+
+
+    Example :
+    ```python
+        layer = LogRound(num_bins=16, base=2)
+        inp = np.asarray([[1.2], [1.6], [0.2], [3.1], [100]])
+        layer(inp)
+        [[0], [1], [0], [2], [7]]
+    ```
+
+    Arguments:
+        num_bins: Range of inputs and outputs is `[0, num_bins)`.
+        **kwargs: Keyword arguments to construct a layer.
+
+    Input shape: A numeric `Tensor`, `SparseTensor` or `RaggedTensor` of shape
+        `[batch_size, d1, ..., dm]`
+
+    Output shape: An int64 tensor of shape `[batch_size, d1, ..., dm]`
+
+    """
+
+    def __init__(self, num_bins, default_value=0, base=None):
+        super(LogRound, self).__init__()
+        self.num_bins = num_bins
+        self.default_value = default_value
+        self.base = base
+
+    def call(self, inputs):
+        if isinstance(inputs, tf.SparseTensor):
+            id_values = self._log_round(inputs.values)
+            result = tf.SparseTensor(
+                indices=inputs.indices,
+                values=id_values,
+                dense_shape=inputs.dense_shape,
+            )
+        elif ragged_tensor.is_ragged(inputs):
+            result = ragged_functional_ops.map_flat_values(
+                self._log_round, inputs
+            )
+        else:
+            result = self._log_round(inputs)
+        return tf.cast(result, tf.int64)
+
+    def _log_round(self, values):
+        values = tf.cast(values, tf.float64)
+        values = tf.math.round(log(values, base=self.base))
+        values = tf.cast(values, tf.int64)
+        num_bins = tf.cast(self.num_bins, tf.int64)
+        default_value = tf.cast(self.default_value, tf.int64)
+        values = tf.where(
+            tf.logical_or(values < 0, values >= num_bins),
+            x=tf.fill(dims=tf.shape(values), value=default_value),
+            y=values,
+        )
+        return values
+
+    def compute_output_shape(self, input_shape):
+        return input_shape
+
+    def get_config(self):
+        config = {
+            "num_bins": self.num_bins,
+            "base": self.base,
+            "default_value": self.default_value,
+        }
+        base_config = super(LogRound, self).get_config()
+        return dict(list(base_config.items()) + list(config.items()))


Test patch:
diff --git a/elasticdl_preprocessing/tests/log_round_test.py b/elasticdl_preprocessing/tests/log_round_test.py
new file mode 100644
--- /dev/null
+++ b/elasticdl_preprocessing/tests/log_round_test.py
@@ -0,0 +1,48 @@
+import unittest
+
+import numpy as np
+import tensorflow as tf
+
+from elasticdl_preprocessing.layers import LogRound
+from elasticdl_preprocessing.tests.test_utils import (
+    ragged_tensor_equal,
+    sparse_tensor_equal,
+)
+
+
+class LogRoundTest(unittest.TestCase):
+    def test_round_indentity(self):
+        log_round = LogRound(num_bins=20, base=2)
+
+        dense_input = tf.constant([[0.0], [2.6], [0.2], [3.1], [1024]])
+        output = log_round(dense_input)
+        expected_out = np.array([[0], [1], [0], [2], [10]])
+
+        self.assertTrue(np.array_equal(output.numpy(), expected_out))
+
+        ragged_input = tf.ragged.constant([[1.1, 3.4], [1025]])
+        ragged_output = log_round(ragged_input)
+        expected_ragged_out = tf.ragged.constant(
+            [[0, 2], [10]], dtype=tf.int64
+        )
+        self.assertTrue(
+            ragged_tensor_equal(ragged_output, expected_ragged_out)
+        )
+
+        sparse_input = ragged_input.to_sparse()
+        sparse_output = log_round(sparse_input)
+        expected_sparse_out = expected_ragged_out.to_sparse()
+        self.assertTrue(
+            sparse_tensor_equal(sparse_output, expected_sparse_out)
+        )
+
+    def test_model_with_loground(self):
+        inputs = tf.keras.Input(shape=(1,), dtype=tf.string)
+        log_round = LogRound(num_bins=20, base=2)(inputs)
+        model = tf.keras.Model(inputs=inputs, outputs=log_round)
+        out = model.call(tf.constant([[1.2], [2.6], [0.2], [3.1], [1024]]))
+        self.assertTrue(
+            np.array_equal(
+                np.array([[0], [1], [0], [2], [10]], dtype=int), out.numpy()
+            )
+        )