def _compute_perturbation(
        self, x: "torch.Tensor", x_init: "torch.Tensor", y: "torch.Tensor", training_mode: bool = False
    ) -> "torch.Tensor":
        """
        Compute perturbations.

        :param x: Current adversarial examples.
        :param x_init: initial input samples.
        :param y: Target values (class labels) one-hot-encoded of shape `(nb_samples, nb_classes)` or indices of shape
                  (nb_samples,). Only provide this parameter if you'd like to use true labels when crafting adversarial
                  samples. Otherwise, model predictions are used as labels to avoid the "label leaking" effect
                  (explained in this paper: https://arxiv.org/abs/1611.01236). Default is `None`.
        :param training_mode: training mode of classifier for attack generation. training_mode=False puts classifier in
                                eval mode
        """
        import torch

        self._classifier.model.train(mode=training_mode)
        self._lpips_classifier.model.train(mode=training_mode)

        # Backpropagation through RNN modules in eval mode raises RuntimeError due to cudnn issues and require training
        # mode, i.e. RuntimeError: cudnn RNN backward can only be called in training mode. Therefore, if the model is
        # an RNN type we always use training mode but freeze batch-norm and dropout layers if training_mode=False.
        if self._classifier.is_rnn:
            self._classifier.model.train(mode=True)
            if not training_mode:
                logger.debug(
                    "Freezing batch-norm and dropout layers for gradient calculation in train mode with eval parameters"
                    "of batch-norm and dropout."
                )
                self._classifier.set_batchnorm(train=False)
                self._classifier.set_dropout(train=False)

        if self._lpips_classifier.is_rnn:
            self._lpips_classifier.model.train(mode=True)
            if not training_mode:
                logger.debug(
                    "Freezing batch-norm and dropout layers for gradient calculation in train mode with eval parameters"
                    "of batch-norm and dropout."
                )
                self._lpips_classifier.set_batchnorm(train=False)
                self._lpips_classifier.set_dropout(train=False)

        # Apply preprocessing
        if self._classifier.all_framework_preprocessing:
            if isinstance(x, torch.Tensor):
                x_grad = x.clone().detach().requires_grad_(True)
            else:
                x_grad = torch.tensor(x).to(self._classifier.device)
                x_grad.requires_grad = True
            if isinstance(y, torch.Tensor):
                y_grad = y.clone().detach()
            else:
                y_grad = torch.tensor(y).to(self._classifier.device)
            if isinstance(x_init, torch.Tensor):
                x_init = x_init.clone().detach()
            else:
                x_init = torch.tensor(x_init).to(self._classifier.device)
            inputs_t, y_preprocessed = self._classifier._apply_preprocessing(x_grad, y=y_grad, fit=False, no_grad=False)
            inputs_init, _ = self._classifier._apply_preprocessing(x_init, y=y_grad, fit=False, no_grad=False)
        elif isinstance(x, np.ndarray):
            x_preprocessed, y_preprocessed = self._classifier._apply_preprocessing(x, y=y, fit=False, no_grad=True)
            x_grad = torch.from_numpy(x_preprocessed).to(self._classifier.device)
            x_grad.requires_grad = True
            inputs_t = x_grad
            x_init_preprocessed, _ = self._classifier._apply_preprocessing(x_init, y=y, fit=False, no_grad=True)
            x_init = torch.from_numpy(x_init_preprocessed).to(self._classifier.device)
            x_init.requires_grad = False
            inputs_init = x_init
        else:
            raise NotImplementedError("Combination of inputs and preprocessing not supported.")

        # Check label shape
        if self._classifier._reduce_labels:
            y_preprocessed = self._classifier.reduce_labels(y_preprocessed)

        if isinstance(y_preprocessed, np.ndarray):
            labels_t = torch.from_numpy(y_preprocessed).to(self._classifier.device)
        else:
            labels_t = y_preprocessed

        # Compute the gradient and return
        model_outputs = self._classifier.model(inputs_t)

        lpips_distance = self.calculate_lpips_distance(
            p_classifier=self._lpips_classifier,
            input_1=inputs_t,
            input_2=inputs_init,
            layers=self._train_params["layer_names_activation"],
        )
        loss = self._classifier.loss(model_outputs, labels_t) - self._train_params["i_lpips_weight"] * torch.mean(
            lpips_distance
        )

        # Clean gradients
        self._classifier.model.zero_grad()
        self._lpips_classifier.model.zero_grad()

        loss.backward()

        if x_grad.grad is not None:
            grad = x_grad.grad
        else:
            raise ValueError("Gradient term in PyTorch model is `None`.")

        if not self._classifier.all_framework_preprocessing:
            grad = self._classifier._apply_preprocessing_gradient(x, grad)
        assert grad.shape == x.shape

        # Check for nan before normalisation and replace with 0
        if torch.any(grad.isnan()):  # pragma: no cover
            logger.warning("Elements of the loss gradient are NaN and have been replaced with 0.0.")
            grad[grad.isnan()] = 0.0

        # Apply norm bound
        if self._train_params["norm"] in ["inf", np.inf]:
            grad = grad.sign()

        elif self._train_params["norm"] == 1:
            ind = tuple(range(1, len(x.shape)))
            grad = grad / (torch.sum(grad.abs(), dim=ind, keepdims=True) + EPS)  # type: ignore

        elif self._train_params["norm"] == 2:
            ind = tuple(range(1, len(x.shape)))
            grad = grad / (torch.sqrt(torch.sum(grad * grad, axis=ind, keepdims=True)) + EPS)  # type: ignore

        assert x.shape == grad.shape

        return grad

        Compute perturbations.

        :param x: Current adversarial examples.
        :param x_init: initial input samples.
        :param y: Target values (class labels) one-hot-encoded of shape `(nb_samples, nb_classes)` or indices of shape
                  (nb_samples,). Only provide this parameter if you'd like to use true labels when crafting adversarial
                  samples. Otherwise, model predictions are used as labels to avoid the "label leaking" effect
                  (explained in this paper: https://arxiv.org/abs/1611.01236). Default is `None`.
        :param training_mode: training mode of classifier for attack generation. training_mode=False puts classifier in
                                eval mode
        
art/defences/trainer/adversarial_trainer_oaat_pytorch.py