def _apply_preprocessing_gradient(self, x, gradients, fit=False):
        """
        Apply the backward pass to the gradients through all preprocessing defences that have been applied to `x`
        and `y` in the forward pass. This function should only be called from function
        `_apply_preprocessing_gradient`.

        The method overrides art.estimators.estimator::LossGradientsMixin._apply_preprocessing_gradient().
        It requires all defenses to have a method estimate_forward().
        It converts numpy arrays to PyTorch tensors first, then chains a series of defenses by calling
        defence.estimate_forward() which contains differentiable estimate of the operations. At the end,
        it converts PyTorch tensors back to numpy arrays.

        :param x: Samples.
        :type x: Format as expected by the `model`
        :param gradients: Gradients before backward pass through preprocessing defences.
        :type gradients: Format as expected by the `model`
        :param fit: `True` if the gradients are computed during training.
        :return: Gradients after backward pass through preprocessing defences.
        :rtype: Format as expected by the `model`
        """
        import torch

        from art.preprocessing.standardisation_mean_std.numpy import StandardisationMeanStd
        from art.preprocessing.standardisation_mean_std.pytorch import StandardisationMeanStdPyTorch

        if not self.preprocessing_operations:
            return gradients

        input_is_tensor = isinstance(x, torch.Tensor)

        if self.all_framework_preprocessing and not (not input_is_tensor and x.dtype == object):
            # Convert np arrays to torch tensors.
            x = torch.tensor(x, device=self._device, requires_grad=True)
            gradients = torch.tensor(gradients, device=self._device)
            x_orig = x

            for preprocess in self.preprocessing_operations:
                if fit:
                    if preprocess.apply_fit:
                        x = preprocess.estimate_forward(x)
                else:
                    if preprocess.apply_predict:
                        x = preprocess.estimate_forward(x)

            x.backward(gradients)

            # Convert torch tensors back to np arrays.
            gradients = x_orig.grad.detach().cpu().numpy()
            if gradients.shape != x_orig.shape:
                raise ValueError(f"The input shape is {x.shape} while the gradient shape is {gradients.shape}")

        elif len(self.preprocessing_operations) == 1 or (
            len(self.preprocessing_operations) == 2
            and isinstance(self.preprocessing_operations[-1], (StandardisationMeanStd, StandardisationMeanStdPyTorch))
        ):
            # Compatible with non-PyTorch defences if no chaining.
            for preprocess in self.preprocessing_operations[::-1]:
                if fit:
                    if preprocess.apply_fit:
                        gradients = preprocess.estimate_gradient(x, gradients)
                else:
                    if preprocess.apply_predict:
                        gradients = preprocess.estimate_gradient(x, gradients)

        else:
            raise NotImplementedError("The current combination of preprocessing types is not supported.")

        return gradients

        Apply the backward pass to the gradients through all preprocessing defences that have been applied to `x`
        and `y` in the forward pass. This function should only be called from function
        `_apply_preprocessing_gradient`.

        The method overrides art.estimators.estimator::LossGradientsMixin._apply_preprocessing_gradient().
        It requires all defenses to have a method estimate_forward().
        It converts numpy arrays to PyTorch tensors first, then chains a series of defenses by calling
        defence.estimate_forward() which contains differentiable estimate of the operations. At the end,
        it converts PyTorch tensors back to numpy arrays.

        :param x: Samples.
        :type x: Format as expected by the `model`
        :param gradients: Gradients before backward pass through preprocessing defences.
        :type gradients: Format as expected by the `model`
        :param fit: `True` if the gradients are computed during training.
        :return: Gradients after backward pass through preprocessing defences.
        :rtype: Format as expected by the `model`
        
art/estimators/pytorch.py