class_docstr: 2D convolution layer (e.g. spatial convolution over images).
  This layer creates a convolution kernel that is convolved
  with the layer input to produce a tensor of
  outputs. If `use_bias` is True,
  a bias vector is created and added to the outputs. Finally, if
  `activation` is not `None`, it is applied to the outputs as well.
  When using this layer as the first layer in a model,
  provide the keyword argument `input_shape`
  (tuple of integers, does not include the sample axis),
  e.g. `input_shape=(128, 128, 3)` for 128x128 RGB pictures
  in `data_format="channels_last"`.
  Arguments:
      filters: Integer, the dimensionality of the output space
          (i.e. the number of output filters in the convolution).
      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).
      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, height, width, channels)` while `channels_first`
          corresponds to inputs with shape
          `(batch, 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.
          Can be a single integer to specify the same value for
          all spatial dimensions.
          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
          (ie. "linear" activation: `a(x) = x`).
      use_bias: Boolean, whether the layer uses a bias vector.
      kernel_initializer: Initializer for the `kernel` weights matrix.
      bias_initializer: Initializer for the bias vector.
      kernel_regularizer: Regularizer function applied to
          the `kernel` weights matrix.
      bias_regularizer: Regularizer function applied to the bias vector.
      activity_regularizer: Regularizer function applied to
          the output of the layer (its "activation")..
      kernel_constraint: Constraint function applied to the kernel matrix.
      bias_constraint: Constraint function applied to the bias vector.
  Input shape:
      4D tensor with shape:
      `(samples, channels, rows, cols)` if data_format='channels_first'
      or 4D tensor with shape:
      `(samples, rows, cols, channels)` if data_format='channels_last'.
  Output shape:
      4D tensor with shape:
      `(samples, filters, new_rows, new_cols)` if data_format='channels_first'
      or 4D tensor with shape:
      `(samples, new_rows, new_cols, filters)` if data_format='channels_last'.
      `rows` and `cols` values might have changed due to padding.
code: def backward(self, d_y):
      x = self._layer_input
      y = self._layer_output
      kernel = self.weights[0].read_value()
      grad_weights = []
  
      # Convert to NCHW format
      if self.data_format != "channels_first":
          x = tfe.transpose(x, perm=[0, 3, 1, 2])
      n_x, _, h_x, w_x = x.shape.as_list()
  
      if self.activation is not None:
          self._activation_deriv = activations.get_deriv(
              self.activation.__name__
          )
          d_y = self._activation_deriv(y, d_y)
  
      # Convert to HWNC format
      if self.data_format == "channels_first":
          d_y = tfe.transpose(d_y, perm=[2, 3, 0, 1])
      else:
          d_y = tfe.transpose(d_y, perm=[1, 2, 0, 3])
  
      inner_padded_d_y = tfe.expand(d_y, self.strides[0])
      padded_d_y = tfe.pad(
          inner_padded_d_y,
          [
              [self.kernel_size[0] - 1, self.kernel_size[0] - 1],
              [self.kernel_size[1] - 1, self.kernel_size[1] - 1],
          ],
      )
  
      # Recover the NCHW format
      padded_d_y = tfe.transpose(padded_d_y, perm=[2, 3, 0, 1])
  
      # Flip h and w axis, and swap in and out channels
      flipped_kernel = tfe.transpose(
          tfe.reverse(kernel, [0, 1]), perm=[0, 1, 3, 2]
      )
  
      # Back prop for dx:
      # https://medium.com/@mayank.utexas/backpropagation-for-convolution-with-strides-8137e4fc2710
      d_x = tfe.conv2d(padded_d_y, flipped_kernel, [1, 1], "VALID")
  
      # Remove padding, if any
      if self.padding == "SAME":
          [
              [pad_top, pad_bottom],
              [pad_left, pad_right],
          ] = self.pad_size(h_x, w_x)
          d_x = d_x[
              :,
              :,
              pad_top : (d_x.shape[2] - pad_bottom),
              pad_left : (d_x.shape[3] - pad_right),
          ]
          x = tfe.pad(
              x,
              [
                  [0, 0],
                  [0, 0],
                  [pad_top, pad_bottom],
                  [pad_left, pad_right],
              ],
          )
  
      if self.data_format != "channels_first":
          d_x = tfe.transpose(d_x, perm=[0, 2, 3, 1])
  
      # Convert to CNHW
      x = tfe.transpose(x, perm=[1, 0, 2, 3])
      # Back prop for dw:
      # https://medium.com/@mayank.utexas/backpropagation-for-convolution-with-strides-fb2f2efc4faa
      # Output is in IOHW format
      d_kernel = tfe.conv2d(x, inner_padded_d_y, [1, 1], "VALID")
      # Convert to HWIO
      d_kernel = tfe.transpose(d_kernel, perm=[2, 3, 0, 1])
      if self.lazy_normalization:
          d_kernel = d_kernel / n_x
      grad_weights.append(d_kernel)
  
      if self.use_bias:
          d_bias = d_y.reduce_sum(axis=[0, 1, 2]).reshape(
              self.bias.shape
          )
          if self.lazy_normalization:
              d_bias = d_bias / n_x
          grad_weights.append(d_bias)
  
      assert (
          d_x.shape == self._layer_input.shape
      ), "Different shapes: {} vs {}".format(
          d_x.shape, self._layer_input.shape
      )
      assert (
          d_kernel.shape == self.kernel_shape
      ), "Different shapes: {} vs {}".format(
          d_kernel.shape, self.kernel_shape
      )
  
      return grad_weights, d_x
  
imports: import logging
  import numpy
  import tensorflow
  from tensorflow.python.keras import initializers
  from tensorflow.python.keras.utils import conv_utils
  import tf_encrypted
  from tf_encrypted.keras import activations
  from tf_encrypted.keras.engine import Layer
  from tf_encrypted.keras.layers.layers_utils import default_args_check
  from tf_encrypted.protocol import TFEPrivateTensor
  from tf_encrypted.protocol import TFEPrivateVariable
label_desc: Use image node_id 4 for calling the Conv2D obj's underlying member method code with example usage: obj.backward(d_y) and returns: grad_weights,  d_x