Repository: keras-team/keras

Issue:
[Contributions welcome] Implement a `DepthwiseConv1D` layer
Keras features a `DepthwiseConv2D` layer, but not yet `DepthwiseConv1D`. It is already implementable with existing TF ops. If you're interested in adding this feature, please open a PR.


Discussion:
I can implement Depthwise Conv 1D using other libraries. Kindly assign me this issue @fchollet 
I'm also interested in it.

Resolving patch:
diff --git a/keras/layers/__init__.py b/keras/layers/__init__.py
--- a/keras/layers/__init__.py
+++ b/keras/layers/__init__.py
@@ -76,6 +76,7 @@
 from keras.layers.convolutional import Convolution3DTranspose
 from keras.layers.convolutional import SeparableConvolution1D
 from keras.layers.convolutional import SeparableConvolution2D
+from keras.layers.convolutional import DepthwiseConv1D
 from keras.layers.convolutional import DepthwiseConv2D
 
 # Image processing layers.
diff --git a/keras/layers/convolutional.py b/keras/layers/convolutional.py
--- a/keras/layers/convolutional.py
+++ b/keras/layers/convolutional.py
@@ -2254,9 +2254,8 @@ def call(self, inputs):
     return outputs
 
 
-@keras_export('keras.layers.DepthwiseConv2D')
-class DepthwiseConv2D(Conv2D):
-  """Depthwise 2D convolution.
+class DepthwiseConv(Conv):
+  """Depthwise convolution.
 
   Depthwise convolution is a type of convolution in which a single convolutional
   filter is apply to each input channel (i.e. in a depthwise way).
@@ -2269,24 +2268,22 @@ class DepthwiseConv2D(Conv2D):
   - Convolve each input with the layer's kernel (called a depthwise kernel).
   - Stack the convolved outputs together (along the channels axis).
 
-  Unlike a regular 2D convolution, depthwise convolution does not mix
+  Unlike a regular convolution, depthwise convolution does not mix
   information across different input channels.
 
   The `depth_multiplier` argument controls how many
   output channels are generated per input channel in the depthwise step.
 
   Args:
-    kernel_size: An integer or tuple/list of 2 integers, specifying the
-      height and width of the 2D convolution window.
-      Can be a single integer to specify the same value for
-      all spatial dimensions.
-    strides: An integer or tuple/list of 2 integers,
-      specifying the strides of the convolution along the height and width.
-      Can be a single integer to specify the same value for
+    kernel_size: A tuple or list of integers specifying the spatial
+      dimensions of the filters. Can be a single integer to specify the same
+      value for all spatial dimensions.
+    strides: A tuple or list of integers specifying the strides
+      of the convolution. Can be a single integer to specify the same value for
       all spatial dimensions.
-      Specifying any stride value != 1 is incompatible with specifying
+      Specifying any `stride` value != 1 is incompatible with specifying
       any `dilation_rate` value != 1.
-    padding: one of `'valid'` or `'same'` (case-insensitive).
+    padding: One of `"valid"` or `"same"` (case-insensitive).
       `"valid"` means no padding. `"same"` results in padding with zeros evenly
       to the left/right or up/down of the input such that output has the same
       height/width dimension as the input.
@@ -2355,12 +2352,13 @@ class DepthwiseConv2D(Conv2D):
   """
 
   def __init__(self,
+               rank,
                kernel_size,
-               strides=(1, 1),
+               strides=1,
                padding='valid',
                depth_multiplier=1,
                data_format=None,
-               dilation_rate=(1, 1),
+               dilation_rate=1,
                activation=None,
                use_bias=True,
                depthwise_initializer='glorot_uniform',
@@ -2371,7 +2369,8 @@ def __init__(self,
                depthwise_constraint=None,
                bias_constraint=None,
                **kwargs):
-    super(DepthwiseConv2D, self).__init__(
+    super(DepthwiseConv, self).__init__(
+        rank,
         filters=None,
         kernel_size=kernel_size,
         strides=strides,
@@ -2391,20 +2390,19 @@ def __init__(self,
     self.bias_initializer = initializers.get(bias_initializer)
 
   def build(self, input_shape):
-    if len(input_shape) < 4:
-      raise ValueError('Inputs to `DepthwiseConv2D` should have rank 4. '
+    if len(input_shape) != self.rank + 2:
+      raise ValueError('Inputs to `DepthwiseConv` should have rank ',
+                       str(self.rank + 2), '. ',
                        'Received input shape:', str(input_shape))
     input_shape = tf.TensorShape(input_shape)
     channel_axis = self._get_channel_axis()
     if input_shape.dims[channel_axis].value is None:
       raise ValueError('The channel dimension of the inputs to '
-                       '`DepthwiseConv2D` '
+                       '`DepthwiseConv` '
                        'should be defined. Found `None`.')
     input_dim = int(input_shape[channel_axis])
-    depthwise_kernel_shape = (self.kernel_size[0],
-                              self.kernel_size[1],
-                              input_dim,
-                              self.depth_multiplier)
+    depthwise_kernel_shape = self.kernel_size + (input_dim,
+                                                 self.depth_multiplier)
 
     self.depthwise_kernel = self.add_weight(
         shape=depthwise_kernel_shape,
@@ -2422,9 +2420,354 @@ def build(self, input_shape):
     else:
       self.bias = None
     # Set input spec.
-    self.input_spec = InputSpec(ndim=4, axes={channel_axis: input_dim})
+    self.input_spec = InputSpec(min_ndim=self.rank + 2, 
+                                axes={channel_axis: input_dim})
     self.built = True
 
+  def call(self, inputs):
+    raise NotImplementedError
+
+  def get_config(self):
+    config = super(DepthwiseConv, self).get_config()
+    config.pop('filters')
+    config.pop('kernel_initializer')
+    config.pop('kernel_regularizer')
+    config.pop('kernel_constraint')
+    config['depth_multiplier'] = self.depth_multiplier
+    config['depthwise_initializer'] = initializers.serialize(
+        self.depthwise_initializer)
+    config['depthwise_regularizer'] = regularizers.serialize(
+        self.depthwise_regularizer)
+    config['depthwise_constraint'] = constraints.serialize(
+        self.depthwise_constraint)
+    return config
+
+
+@keras_export('keras.layers.DepthwiseConv1D')
+class DepthwiseConv1D(DepthwiseConv):
+  """Depthwise 1D convolution.
+
+  Depthwise convolution is a type of convolution in which a single convolutional
+  filter is apply to each input channel (i.e. in a depthwise way).
+  You can understand depthwise convolution as being
+  the first step in a depthwise separable convolution.
+
+  It is implemented via the following steps:
+
+  - Split the input into individual channels.
+  - Convolve each input with the layer's kernel (called a depthwise kernel).
+  - Stack the convolved outputs together (along the channels axis).
+
+  Unlike a regular 1D convolution, depthwise convolution does not mix
+  information across different input channels.
+
+  The `depth_multiplier` argument controls how many
+  output channels are generated per input channel in the depthwise step.
+
+  Args:
+    kernel_size: An integer, specifying the
+      height and width of the 1D convolution window.
+      Can be a single integer to specify the same value for
+      all spatial dimensions.
+    strides: An integer,
+      specifying the strides of the convolution along the height and width.
+      Can be a single integer to specify the same value for
+      all spatial dimensions.
+      Specifying any stride value != 1 is incompatible with specifying
+      any `dilation_rate` value != 1.
+    padding: one of `'valid'` or `'same'` (case-insensitive).
+      `"valid"` means no padding. `"same"` results in padding with zeros evenly
+      to the left/right or up/down of the input such that output has the same
+      height/width dimension as the input.
+    depth_multiplier: The number of depthwise convolution output channels
+      for each input channel.
+      The total number of depthwise convolution output
+      channels will be equal to `filters_in * depth_multiplier`.
+    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, height, width, channels)` while `channels_first`
+      corresponds to inputs with shape
+      `(batch_size, channels, height, width)`.
+      It defaults to the `image_data_format` value found in your
+      Keras config file at `~/.keras/keras.json`.
+      If you never set it, then it will be 'channels_last'.
+    dilation_rate: A single integer, specifying
+      the dilation rate to use for dilated convolution.
+      Currently, specifying any `dilation_rate` value != 1 is
+      incompatible with specifying any stride value != 1.
+    activation: Activation function to use.
+      If you don't specify anything, no activation is applied (
+      see `keras.activations`).
+    use_bias: Boolean, whether the layer uses a bias vector.
+    depthwise_initializer: Initializer for the depthwise kernel matrix (
+      see `keras.initializers`). If None, the default initializer (
+      'glorot_uniform') will be used.
+    bias_initializer: Initializer for the bias vector (
+      see `keras.initializers`). If None, the default initializer (
+      'zeros') will bs used.
+    depthwise_regularizer: Regularizer function applied to
+      the depthwise kernel matrix (see `keras.regularizers`).
+    bias_regularizer: Regularizer function applied to the bias vector (
+      see `keras.regularizers`).
+    activity_regularizer: Regularizer function applied to
+      the output of the layer (its 'activation') (
+      see `keras.regularizers`).
+    depthwise_constraint: Constraint function applied to
+      the depthwise kernel matrix (
+      see `keras.constraints`).
+    bias_constraint: Constraint function applied to the bias vector (
+      see `keras.constraints`).
+
+  Input shape:
+    4D tensor with shape:
+    `[batch_size, channels, rows, cols]` if data_format='channels_first'
+    or 4D tensor with shape:
+    `[batch_size, rows, cols, channels]` if data_format='channels_last'.
+
+  Output shape:
+    4D tensor with shape:
+    `[batch_size, channels * depth_multiplier, new_rows, new_cols]` if
+    data_format='channels_first' or 4D tensor with shape:
+    `[batch_size, new_rows, new_cols, channels * depth_multiplier]` if
+    data_format='channels_last'. `rows` and `cols` values might have
+    changed due to padding.
+
+  Returns:
+    A tensor of rank 4 representing
+    `activation(depthwiseconv2d(inputs, kernel) + bias)`.
+
+  Raises:
+    ValueError: if `padding` is "causal".
+    ValueError: when both `strides` > 1 and `dilation_rate` > 1.
+  """
+
+  def __init__(self,
+               kernel_size,
+               strides=1,
+               padding='valid',
+               depth_multiplier=1,
+               data_format=None,
+               dilation_rate=1,
+               activation=None,
+               use_bias=True,
+               depthwise_initializer='glorot_uniform',
+               bias_initializer='zeros',
+               depthwise_regularizer=None,
+               bias_regularizer=None,
+               activity_regularizer=None,
+               depthwise_constraint=None,
+               bias_constraint=None,
+               **kwargs):
+    super(DepthwiseConv1D, self).__init__(
+        1,
+        kernel_size=kernel_size,
+        strides=strides,
+        padding=padding,
+        depth_multiplier=depth_multiplier,
+        data_format=data_format,
+        dilation_rate=dilation_rate,
+        activation=activation,
+        use_bias=use_bias,
+        depthwise_initializer=depthwise_initializer,
+        bias_initializer=bias_initializer,
+        depthwise_regularizer=depthwise_regularizer,
+        bias_regularizer=bias_regularizer,
+        activity_regularizer=activity_regularizer,
+        depthwise_constraint=depthwise_constraint,
+        bias_constraint=bias_constraint,
+        **kwargs)
+  
+  def call(self, inputs):
+    if self.data_format == 'channels_last':
+      strides = (1,) + self.strides * 2 + (1,)
+      spatial_start_dim = 1
+    else:
+      strides = (1, 1) + self.strides * 2
+      spatial_start_dim = 2
+    inputs = tf.expand_dims(inputs, spatial_start_dim)
+    depthwise_kernel = tf.expand_dims(self.depthwise_kernel, axis=0)
+    dilation_rate = (1,) + self.dilation_rate
+
+    outputs = tf.nn.depthwise_conv2d(
+        inputs,
+        depthwise_kernel,
+        strides=strides,
+        padding=self.padding.upper(),
+        dilations=dilation_rate,
+        data_format=conv_utils.convert_data_format(self.data_format,
+                                                   ndim=4))
+
+    if self.use_bias:
+      outputs = tf.nn.bias_add(
+          outputs,
+          self.bias,
+          data_format=conv_utils.convert_data_format(self.data_format,
+                                                     ndim=4))
+
+    outputs = tf.squeeze(outputs, [spatial_start_dim])
+
+    if self.activation is not None:
+      return self.activation(outputs)
+
+    return outputs
+
+  @tf_utils.shape_type_conversion
+  def compute_output_shape(self, input_shape):
+    if self.data_format == 'channels_first':
+      rows = input_shape[2]
+      out_filters = input_shape[1] * self.depth_multiplier
+    elif self.data_format == 'channels_last':
+      rows = input_shape[1]
+      out_filters = input_shape[2] * self.depth_multiplier
+
+    rows = conv_utils.conv_output_length(rows, self.kernel_size[0],
+                                         self.padding,
+                                         self.strides[0],
+                                         self.dilation_rate[0])
+    if self.data_format == 'channels_first':
+      return (input_shape[0], out_filters, rows)
+    elif self.data_format == 'channels_last':
+      return (input_shape[0], rows, out_filters)
+
+
+@keras_export('keras.layers.DepthwiseConv2D')
+class DepthwiseConv2D(DepthwiseConv):
+  """Depthwise 2D convolution.
+
+  Depthwise convolution is a type of convolution in which a single convolutional
+  filter is apply to each input channel (i.e. in a depthwise way).
+  You can understand depthwise convolution as being
+  the first step in a depthwise separable convolution.
+
+  It is implemented via the following steps:
+
+  - Split the input into individual channels.
+  - Convolve each input with the layer's kernel (called a depthwise kernel).
+  - Stack the convolved outputs together (along the channels axis).
+
+  Unlike a regular 2D convolution, depthwise convolution does not mix
+  information across different input channels.
+
+  The `depth_multiplier` argument controls how many
+  output channels are generated per input channel in the depthwise step.
+
+  Args:
+    kernel_size: An integer or tuple/list of 2 integers, specifying the
+      height and width of the 2D convolution window.
+      Can be a single integer to specify the same value for
+      all spatial dimensions.
+    strides: An integer or tuple/list of 2 integers,
+      specifying the strides of the convolution along the height and width.
+      Can be a single integer to specify the same value for
+      all spatial dimensions.
+      Specifying any stride value != 1 is incompatible with specifying
+      any `dilation_rate` value != 1.
+    padding: one of `'valid'` or `'same'` (case-insensitive).
+      `"valid"` means no padding. `"same"` results in padding with zeros evenly
+      to the left/right or up/down of the input such that output has the same
+      height/width dimension as the input.
+    depth_multiplier: The number of depthwise convolution output channels
+      for each input channel.
+      The total number of depthwise convolution output
+      channels will be equal to `filters_in * depth_multiplier`.
+    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, height, width, channels)` while `channels_first`
+      corresponds to inputs with shape
+      `(batch_size, channels, height, width)`.
+      It defaults to the `image_data_format` value found in your
+      Keras config file at `~/.keras/keras.json`.
+      If you never set it, then it will be 'channels_last'.
+    dilation_rate: An integer or tuple/list of 2 integers, specifying
+      the dilation rate to use for dilated convolution.
+      Currently, specifying any `dilation_rate` value != 1 is
+      incompatible with specifying any `strides` value != 1.
+    activation: Activation function to use.
+      If you don't specify anything, no activation is applied (
+      see `keras.activations`).
+    use_bias: Boolean, whether the layer uses a bias vector.
+    depthwise_initializer: Initializer for the depthwise kernel matrix (
+      see `keras.initializers`). If None, the default initializer (
+      'glorot_uniform') will be used.
+    bias_initializer: Initializer for the bias vector (
+      see `keras.initializers`). If None, the default initializer (
+      'zeros') will bs used.
+    depthwise_regularizer: Regularizer function applied to
+      the depthwise kernel matrix (see `keras.regularizers`).
+    bias_regularizer: Regularizer function applied to the bias vector (
+      see `keras.regularizers`).
+    activity_regularizer: Regularizer function applied to
+      the output of the layer (its 'activation') (
+      see `keras.regularizers`).
+    depthwise_constraint: Constraint function applied to
+      the depthwise kernel matrix (
+      see `keras.constraints`).
+    bias_constraint: Constraint function applied to the bias vector (
+      see `keras.constraints`).
+
+  Input shape:
+    4D tensor with shape:
+    `[batch_size, channels, rows, cols]` if data_format='channels_first'
+    or 4D tensor with shape:
+    `[batch_size, rows, cols, channels]` if data_format='channels_last'.
+
+  Output shape:
+    4D tensor with shape:
+    `[batch_size, channels * depth_multiplier, new_rows, new_cols]` if
+    data_format='channels_first' or 4D tensor with shape:
+    `[batch_size, new_rows, new_cols, channels * depth_multiplier]` if
+    data_format='channels_last'. `rows` and `cols` values might have
+    changed due to padding.
+
+  Returns:
+    A tensor of rank 4 representing
+    `activation(depthwiseconv2d(inputs, kernel) + bias)`.
+
+  Raises:
+    ValueError: if `padding` is "causal".
+    ValueError: when both `strides` > 1 and `dilation_rate` > 1.
+  """
+
+  def __init__(self,
+               kernel_size,
+               strides=(1, 1),
+               padding='valid',
+               depth_multiplier=1,
+               data_format=None,
+               dilation_rate=(1, 1),
+               activation=None,
+               use_bias=True,
+               depthwise_initializer='glorot_uniform',
+               bias_initializer='zeros',
+               depthwise_regularizer=None,
+               bias_regularizer=None,
+               activity_regularizer=None,
+               depthwise_constraint=None,
+               bias_constraint=None,
+               **kwargs):
+    super(DepthwiseConv2D, self).__init__(
+        2,
+        kernel_size=kernel_size,
+        strides=strides,
+        padding=padding,
+        depth_multiplier=depth_multiplier,
+        data_format=data_format,
+        dilation_rate=dilation_rate,
+        activation=activation,
+        use_bias=use_bias,
+        depthwise_initializer=depthwise_initializer,
+        bias_initializer=bias_initializer,
+        depthwise_regularizer=depthwise_regularizer,
+        bias_regularizer=bias_regularizer,
+        activity_regularizer=activity_regularizer,
+        depthwise_constraint=depthwise_constraint,
+        bias_constraint=bias_constraint,
+        **kwargs)
+  
   def call(self, inputs):
     outputs = backend.depthwise_conv2d(
         inputs,
@@ -2469,21 +2812,6 @@ def compute_output_shape(self, input_shape):
     elif self.data_format == 'channels_last':
       return (input_shape[0], rows, cols, out_filters)
 
-  def get_config(self):
-    config = super(DepthwiseConv2D, self).get_config()
-    config.pop('filters')
-    config.pop('kernel_initializer')
-    config.pop('kernel_regularizer')
-    config.pop('kernel_constraint')
-    config['depth_multiplier'] = self.depth_multiplier
-    config['depthwise_initializer'] = initializers.serialize(
-        self.depthwise_initializer)
-    config['depthwise_regularizer'] = regularizers.serialize(
-        self.depthwise_regularizer)
-    config['depthwise_constraint'] = constraints.serialize(
-        self.depthwise_constraint)
-    return config
-
 
 @keras_export('keras.layers.UpSampling1D')
 class UpSampling1D(Layer):


Test patch:
diff --git a/keras/layers/convolutional_test.py b/keras/layers/convolutional_test.py
--- a/keras/layers/convolutional_test.py
+++ b/keras/layers/convolutional_test.py
@@ -1134,6 +1134,55 @@ def test_cropping_3d(self):
       keras.layers.Cropping3D(cropping=None)
 
 
+@keras_parameterized.run_all_keras_modes
+class DepthwiseConv1DTest(keras_parameterized.TestCase):
+
+  def _run_test(self, kwargs, expected_output_shape=None):
+    num_samples = 2
+    stack_size = 3
+    num_row = 7
+
+    with self.cached_session():
+      testing_utils.layer_test(
+          keras.layers.DepthwiseConv1D,
+          kwargs=kwargs,
+          input_shape=(num_samples, num_row, stack_size),
+          expected_output_shape=expected_output_shape)
+
+  @parameterized.named_parameters(
+      ('padding_valid', {'padding': 'valid'}),
+      ('padding_same', {'padding': 'same'}),
+      ('strides', {'strides': 2}),
+      # Only runs on GPU with CUDA, channels_first is not supported on CPU.
+      # TODO(b/62340061): Support channels_first on CPU.
+      ('data_format', {'data_format': 'channels_first'}),
+      ('depth_multiplier_1', {'depth_multiplier': 1}),
+      ('depth_multiplier_2', {'depth_multiplier': 2}),
+      ('dilation_rate', {'dilation_rate': 2}, (None, 3, 3)),
+  )
+  def test_depthwise_conv1d(self, kwargs, expected_output_shape=None):
+    kwargs['kernel_size'] = 3
+    if 'data_format' not in kwargs or tf.test.is_gpu_available(cuda_only=True):
+      self._run_test(kwargs, expected_output_shape)
+
+  def test_depthwise_conv1d_full(self):
+    kwargs = {
+        'kernel_size': 3,
+        'padding': 'valid',
+        'data_format': 'channels_last',
+        'dilation_rate': 1,
+        'activation': None,
+        'depthwise_regularizer': 'l2',
+        'bias_regularizer': 'l2',
+        'activity_regularizer': 'l2',
+        'depthwise_constraint': 'unit_norm',
+        'use_bias': True,
+        'strides': 2,
+        'depth_multiplier': 1,
+    }
+    self._run_test(kwargs)
+
+
 @keras_parameterized.run_all_keras_modes
 class DepthwiseConv2DTest(keras_parameterized.TestCase):