Repository: keras-team/keras (python)

Issue:
Support for channels first data format with zeropadding1D
Hi Keras team,

It appears that the 1D ZeroPadding layer always assumes a channels-last data format. It would be beneficial to add support for cases where the data format is channels-first. Currently, the 1D ZeroPadding layer does not accept an input variable for data_format, unlike the ZeroPadding2D layer which includes this parameter.

https://github.com/keras-team/keras/blob/v3.3.3/keras/src/layers/reshaping/zero_padding1d.py

Thank you.


Pull request description:
Zero pad 1D data format parameter
Add `data_format` parameter to `ZeroPadding1D`. Parameter is available for 2D and 3D layers.

Fix #19984


Patch:
diff --git a/keras/src/backend/common/variables.py b/keras/src/backend/common/variables.py
index 34b69958f..e76930f0a 100644
--- a/keras/src/backend/common/variables.py
+++ b/keras/src/backend/common/variables.py
@@ -237,12 +237,13 @@ class KerasVariable:
             scope.add_update((self, value))
         else:
             self._direct_assign(value)
+        return value
 
     def assign_add(self, value):
-        self.assign(self + value)
+        return self.assign(self + value)
 
     def assign_sub(self, value):
-        self.assign(self - value)
+        return self.assign(self - value)
 
     @property
     def dtype(self):
diff --git a/keras/src/layers/reshaping/zero_padding1d.py b/keras/src/layers/reshaping/zero_padding1d.py
index 2777423b7..c9e50d889 100644
--- a/keras/src/layers/reshaping/zero_padding1d.py
+++ b/keras/src/layers/reshaping/zero_padding1d.py
@@ -1,3 +1,4 @@
+from keras.src import backend
 from keras.src import ops
 from keras.src.api_export import keras_export
 from keras.src.layers.input_spec import InputSpec
@@ -39,16 +40,34 @@ class ZeroPadding1D(Layer):
               the padding dimension (axis 1).
             - If tuple of 2 ints: how many zeros to add at the beginning and the
               end of the padding dimension (`(left_pad, right_pad)`).
+        data_format: A string, one of `"channels_last"` (default) or
+            `"channels_first"`. The ordering of the dimensions in the inputs.
+            `"channels_last"` corresponds to inputs with shape
+            `(batch_size, axis_to_pad, channels)` while `"channels_first"`
+            corresponds to inputs with shape
+            `(batch_size, channels, axis_to_pad)`.
+            When unspecified, uses `image_data_format` value found in your Keras
+            config file at `~/.keras/keras.json` (if exists). Defaults to
+            `"channels_last"`.
 
     Input shape:
-        3D tensor with shape `(batch_size, axis_to_pad, features)`
+        3D tensor with shape:
+        - If `data_format` is `"channels_last"`:
+          `(batch_size, axis_to_pad, features)`
+        - If `data_format` is `"channels_first"`:
+          `(batch_size, features, axis_to_pad)`
 
     Output shape:
-        3D tensor with shape `(batch_size, padded_axis, features)`
+        3D tensor with shape:
+        - If `data_format` is `"channels_last"`:
+          `(batch_size, padded_axis, features)`
+        - If `data_format` is `"channels_first"`:
+          `(batch_size, features, padded_axis)`
     """
 
-    def __init__(self, padding=1, **kwargs):
+    def __init__(self, padding=1, data_format=None, **kwargs):
         super().__init__(**kwargs)
+        self.data_format = backend.standardize_data_format(data_format)
         self.padding = argument_validation.standardize_tuple(
             padding, 2, "padding", allow_zero=True
         )
@@ -56,15 +75,19 @@ class ZeroPadding1D(Layer):
 
     def compute_output_shape(self, input_shape):
         output_shape = list(input_shape)
-        if output_shape[1] is not None:
-            output_shape[1] += self.padding[0] + self.padding[1]
+        padding_dim = 2 if self.data_format == "channels_first" else 1
+        if output_shape[padding_dim] is not None:
+            output_shape[padding_dim] += self.padding[0] + self.padding[1]
         return tuple(output_shape)
 
     def call(self, inputs):
-        all_dims_padding = ((0, 0), self.padding, (0, 0))
+        if self.data_format == "channels_first":
+            all_dims_padding = ((0, 0), (0, 0), self.padding)
+        else:
+            all_dims_padding = ((0, 0), self.padding, (0, 0))
         return ops.pad(inputs, all_dims_padding)
 
     def get_config(self):
-        config = {"padding": self.padding}
+        config = {"padding": self.padding, "data_format": self.data_format}
         base_config = super().get_config()
         return {**base_config, **config}


Test patch:
diff --git a/keras/src/backend/common/variables_test.py b/keras/src/backend/common/variables_test.py
index ab40c905c..2b36fbda5 100644
--- a/keras/src/backend/common/variables_test.py
+++ b/keras/src/backend/common/variables_test.py
@@ -297,18 +297,36 @@ class VariableNumpyValueAndAssignmentTest(test_case.TestCase):
         v.assign(np.array([4, 5, 6]))
         self.assertAllClose(v.value, np.array([4, 5, 6]))
 
+    def test_variable_assign_return(self):
+        """Test assigning a new value and returning."""
+        v = backend.Variable(initializer=np.array([1, 2, 3]))
+        r = v.assign(np.array([4, 5, 6]))
+        self.assertAllClose(r, np.array([4, 5, 6]))
+
     def test_variable_assign_add(self):
         """Test the assign_add method on a variable."""
         v = backend.Variable(initializer=np.array([1, 2, 3]))
         v.assign_add(np.array([1, 1, 1]))
         self.assertAllClose(v.value, np.array([2, 3, 4]))
 
+    def test_variable_assign_add_return(self):
+        """Test assign_add a new value and returning."""
+        v = backend.Variable(initializer=np.array([1, 2, 3]))
+        r = v.assign_add(np.array([1, 1, 1]))
+        self.assertAllClose(r, np.array([2, 3, 4]))
+
     def test_variable_assign_sub(self):
         """Test the assign_sub method on a variable."""
         v = backend.Variable(initializer=np.array([2, 3, 4]))
         v.assign_sub(np.array([1, 1, 1]))
         self.assertAllClose(v.value, np.array([1, 2, 3]))
 
+    def test_variable_assign_sub_return(self):
+        """Test assign_sub a new value and returning."""
+        v = backend.Variable(initializer=np.array([2, 3, 4]))
+        r = v.assign_sub(np.array([1, 1, 1]))
+        self.assertAllClose(r, np.array([1, 2, 3]))
+
     def test_deferred_initialize_within_stateless_scope(self):
         """Test deferred init within a stateless scope."""
         with backend.StatelessScope():
diff --git a/keras/src/layers/reshaping/zero_padding1d_test.py b/keras/src/layers/reshaping/zero_padding1d_test.py
index 90767d5b0..8af0bfa48 100644
--- a/keras/src/layers/reshaping/zero_padding1d_test.py
+++ b/keras/src/layers/reshaping/zero_padding1d_test.py
@@ -7,18 +7,30 @@ from keras.src import testing
 
 
 class ZeroPadding1DTest(testing.TestCase, parameterized.TestCase):
-    def test_zero_padding_1d(self):
+    @parameterized.parameters(
+        {"data_format": "channels_first"},
+        {"data_format": "channels_last"},
+    )
+    def test_zero_padding_1d(self, data_format):
         inputs = np.random.rand(1, 2, 3)
-        outputs = layers.ZeroPadding1D(padding=(1, 2))(inputs)
-
-        for index in [0, -1, -2]:
-            self.assertAllClose(outputs[:, index, :], 0.0)
-        self.assertAllClose(outputs[:, 1:-2, :], inputs)
+        outputs = layers.ZeroPadding1D(padding=(1, 2), data_format=data_format)(
+            inputs
+        )
+        if data_format == "channels_last":
+            for index in [0, -1, -2]:
+                self.assertAllClose(outputs[:, index, :], 0.0)
+            self.assertAllClose(outputs[:, 1:-2, :], inputs)
+        else:
+            for index in [0, -1, -2]:
+                self.assertAllClose(outputs[:, :, index], 0.0)
+            self.assertAllClose(outputs[:, :, 1:-2], inputs)
 
     @parameterized.named_parameters(("one_tuple", (2, 2)), ("one_int", 2))
     def test_zero_padding_1d_with_same_padding(self, padding):
         inputs = np.random.rand(1, 2, 3)
-        outputs = layers.ZeroPadding1D(padding=padding)(inputs)
+        outputs = layers.ZeroPadding1D(
+            padding=padding, data_format="channels_last"
+        )(inputs)
 
         for index in [0, 1, -1, -2]:
             self.assertAllClose(outputs[:, index, :], 0.0)
@@ -26,11 +38,15 @@ class ZeroPadding1DTest(testing.TestCase, parameterized.TestCase):
 
     def test_zero_padding_1d_with_dynamic_spatial_dim(self):
         input_layer = layers.Input(batch_shape=(1, None, 3))
-        padded = layers.ZeroPadding1D((1, 2))(input_layer)
+        padded = layers.ZeroPadding1D((1, 2), data_format="channels_last")(
+            input_layer
+        )
         self.assertEqual(padded.shape, (1, None, 3))
 
         input_layer = layers.Input(batch_shape=(1, 2, 3))
-        padded = layers.ZeroPadding1D((1, 2))(input_layer)
+        padded = layers.ZeroPadding1D((1, 2), data_format="channels_last")(
+            input_layer
+        )
         self.assertEqual(padded.shape, (1, 5, 3))
 
     @parameterized.parameters(
@@ -42,10 +58,15 @@ class ZeroPadding1DTest(testing.TestCase, parameterized.TestCase):
         with self.assertRaises(ValueError):
             layers.ZeroPadding1D(padding)
 
-    def test_zero_padding_1d_get_config(self):
-        layer = layers.ZeroPadding1D(padding=(1, 2))
+    @parameterized.parameters(
+        {"data_format": "channels_first"},
+        {"data_format": "channels_last"},
+    )
+    def test_zero_padding_1d_get_config(self, data_format):
+        layer = layers.ZeroPadding1D(padding=(1, 2), data_format=data_format)
         expected_config = {
             "dtype": dtype_policies.serialize(layer.dtype_policy),
+            "data_format": data_format,
             "name": layer.name,
             "padding": (1, 2),
             "trainable": layer.trainable,