Repository: sql-machine-learning/elasticdl

Issue:
support  Standardize preprocessing layer

x = (x - mean) / std


Discussion:
Maybe a normalizer preprocessing layer with arguments  subtractor and divisor? 
```
x = (x -  subtractor) / divisor
```

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
@@ -8,6 +8,7 @@
 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.normalizer import Normalizer
 from elasticdl_preprocessing.layers.round_identity import RoundIdentity
 from elasticdl_preprocessing.layers.sparse_embedding import SparseEmbedding
 from elasticdl_preprocessing.layers.to_number import ToNumber
diff --git a/elasticdl_preprocessing/layers/normalizer.py b/elasticdl_preprocessing/layers/normalizer.py
new file mode 100644
--- /dev/null
+++ b/elasticdl_preprocessing/layers/normalizer.py
@@ -0,0 +1,60 @@
+import tensorflow as tf
+
+
+class Normalizer(tf.keras.layers.Layer):
+    """Normalize the numeric tensors by (x-subtractor)/divisor
+
+    Example :
+    ```python
+        layer = Normalizer(subtractor=1.0, divisor=2.0)
+        inp = np.asarray([[3.0], [5.0], [7.0]])
+        layer(inp)
+        [[1.0], [2.0], [3.0]]
+    ```
+
+    Arguments:
+        subtractor: A float value.
+        divisor: A float value.
+
+    Input shape: A numeric `Tensor`, `SparseTensor` or `RaggedTensor` of shape
+        `[batch_size, d1, ..., dm]`
+
+    Output shape: An float64 tensor of shape `[batch_size, d1, ..., dm]`
+
+    """
+
+    def __init__(self, subtractor, divisor, **kwargs):
+        super(Normalizer, self).__init__(**kwargs)
+        self._supports_ragged_inputs = True
+        self.subtractor = subtractor
+        self.divisor = divisor
+
+    def get_config(self):
+        config = {"subtractor": self.subtractor, "divisor": self.divisor}
+        base_config = super(Normalizer, self).get_config()
+        return dict(list(base_config.items()) + list(config.items()))
+
+    def compute_output_shape(self, input_shape):
+        return input_shape
+
+    def call(self, inputs):
+        if isinstance(inputs, tf.RaggedTensor):
+            normalized_tensor = tf.ragged.map_flat_values(
+                self._normalize_fn, inputs
+            )
+        elif isinstance(inputs, tf.SparseTensor):
+            normalize_values = self._normalize_fn(inputs.values)
+            normalized_tensor = tf.SparseTensor(
+                indices=inputs.indices,
+                values=normalize_values,
+                dense_shape=inputs.dense_shape,
+            )
+        else:
+            normalized_tensor = self._normalize_fn(inputs)
+
+        return normalized_tensor
+
+    def _normalize_fn(self, x):
+        subtractor = tf.cast(self.subtractor, x.dtype)
+        divisor = tf.cast(self.divisor, x.dtype)
+        return (x - subtractor) / divisor


Test patch:
diff --git a/elasticdl_preprocessing/tests/normalizer_test.py b/elasticdl_preprocessing/tests/normalizer_test.py
new file mode 100644
--- /dev/null
+++ b/elasticdl_preprocessing/tests/normalizer_test.py
@@ -0,0 +1,48 @@
+import unittest
+
+import numpy as np
+import tensorflow as tf
+
+from elasticdl_preprocessing.layers import Normalizer
+from elasticdl_preprocessing.tests.test_utils import (
+    ragged_tensor_equal,
+    sparse_tensor_equal,
+)
+
+
+class NormalizerTest(unittest.TestCase):
+    def test_normalizer(self):
+        normalizer = Normalizer(1.0, 2.0)
+
+        dense_input = tf.constant([[5.0], [7.0], [9.0], [11.0], [13.0]])
+        output = normalizer(dense_input)
+        expected_out = np.array([[2.0], [3.0], [4.0], [5.0], [6.0]])
+
+        self.assertTrue(np.array_equal(output.numpy(), expected_out))
+
+        ragged_input = tf.ragged.constant([[5.0, 7.0], [9.0]])
+        ragged_output = normalizer(ragged_input)
+        expected_ragged_out = tf.ragged.constant(
+            [[2.0, 3.0], [4.0]], dtype=tf.float32
+        )
+        self.assertTrue(
+            ragged_tensor_equal(ragged_output, expected_ragged_out)
+        )
+
+        sparse_input = ragged_input.to_sparse()
+        sparse_output = normalizer(sparse_input)
+        expected_sparse_out = expected_ragged_out.to_sparse()
+        self.assertTrue(
+            sparse_tensor_equal(sparse_output, expected_sparse_out)
+        )
+
+    def test_model_with_normalizer(self):
+        inputs = tf.keras.Input(shape=(1,), dtype=tf.float32)
+        normalize = Normalizer(1.0, 2.0)(inputs)
+        model = tf.keras.Model(inputs=inputs, outputs=normalize)
+        out = model.call(tf.constant([[5.0], [7.0], [9.0], [11.0], [13.0]]))
+        self.assertTrue(
+            np.array_equal(
+                np.array([[2.0], [3.0], [4.0], [5.0], [6.0]]), out.numpy()
+            )
+        )