def _batch_process(self, x_batch: np.ndarray, y_batch: np.ndarray, l_r: float) -> tuple[float, float, float]:
        """
        Perform the operations of FBF 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.
        :param l_r: learning rate for the optimisation step.
        """
        import torch

        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]
        m = np.prod(x_batch.shape[1:]).item()
        delta = random_sphere(n, m, self._eps, np.inf).reshape(x_batch.shape).astype(ART_NUMPY_DTYPE)
        delta_grad = self._classifier.loss_gradient(x_batch + delta, y_batch)
        delta = np.clip(delta + 1.25 * self._eps * np.sign(delta_grad), -self._eps, +self._eps)
        if self._classifier.clip_values is not None:
            x_batch_pert = np.clip(x_batch + delta, self._classifier.clip_values[0], self._classifier.clip_values[1])
        else:
            x_batch_pert = x_batch + delta

        # Apply preprocessing
        x_preprocessed, y_preprocessed = 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)
        o_batch = torch.from_numpy(y_preprocessed).to(self._classifier._device)

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

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

        # Form the loss function
        loss = self._classifier._loss(model_outputs[-1], o_batch)

        self._classifier._optimizer.param_groups[0].update(lr=l_r)

        # Actual training
        if self._use_amp:  # pragma: no cover
            from apex import amp

            with amp.scale_loss(loss, self._classifier._optimizer) as scaled_loss:
                scaled_loss.backward()
        else:
            loss.backward()

        # clip the gradients
        torch.nn.utils.clip_grad_norm_(self._classifier._model.parameters(), 0.5)
        self._classifier._optimizer.step()

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

        return train_loss, train_acc, train_n

        Perform the operations of FBF 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.
        :param l_r: learning rate for the optimisation step.
        
art/defences/trainer/adversarial_trainer_fbf_pytorch.py