def _batch_process(self, x_batch: np.ndarray, y_batch: np.ndarray) -> tuple[float, float, float]:
        """
        Perform the operations of TRADES for a batch of data.
        See class documentation for more information on the exact procedure.

        :param x_batch: batch of x.
        :param y_batch: batch of y.
        :return: Tuple containing batch data loss, batch data accuracy and number of samples in the batch
        """
        import torch
        from torch import nn
        import torch.nn.functional as F

        if self._classifier._optimizer is None:
            raise ValueError("Optimizer of classifier is currently None, but is required for adversarial training.")

        n = x_batch.shape[0]
        self._classifier._model.train(mode=False)
        x_batch_pert = self._attack.generate(x_batch, y=y_batch)

        # Apply preprocessing
        y_batch = check_and_transform_label_format(y_batch, nb_classes=self.classifier.nb_classes)

        x_preprocessed, y_preprocessed = self._classifier._apply_preprocessing(x_batch, y_batch, fit=True)
        x_preprocessed_pert, _ = self._classifier._apply_preprocessing(x_batch_pert, y_batch, fit=True)

        # Check label shape
        if self._classifier._reduce_labels:
            y_preprocessed = np.argmax(y_preprocessed, axis=1)

        i_batch = torch.from_numpy(x_preprocessed).to(self._classifier._device)
        i_batch_pert = torch.from_numpy(x_preprocessed_pert).to(self._classifier._device)
        o_batch = torch.from_numpy(y_preprocessed).to(self._classifier._device)

        self._classifier._model.train(mode=True)

        # Zero the parameter gradients
        self._classifier._optimizer.zero_grad()

        # Perform prediction
        model_outputs = self._classifier._model(i_batch)
        model_outputs_pert = self._classifier._model(i_batch_pert)

        # Form the loss function
        loss_clean = self._classifier._loss(model_outputs[-1], o_batch)
        loss_kl = (1.0 / n) * nn.KLDivLoss(reduction="sum")(
            F.log_softmax(model_outputs_pert[-1], dim=1), torch.clamp(F.softmax(model_outputs[-1], dim=1), min=EPS)
        )
        loss = loss_clean + self._beta * loss_kl
        loss.backward()

        self._classifier._optimizer.step()

        train_loss = loss.item() * o_batch.size(0)
        train_acc = (model_outputs_pert[0].max(1)[1] == o_batch).sum().item()
        train_n = o_batch.size(0)

        self._classifier._model.train(mode=False)

        return train_loss, train_acc, train_n

        Perform the operations of TRADES for a batch of data.
        See class documentation for more information on the exact procedure.

        :param x_batch: batch of x.
        :param y_batch: batch of y.
        :return: Tuple containing batch data loss, batch data accuracy and number of samples in the batch
        
art/defences/trainer/adversarial_trainer_trades_pytorch.py