def fit(
        self,
        x: np.ndarray,
        y: np.ndarray,
        batch_size: int = 128,
        nb_epochs: int = 10,
        training_mode: bool = True,
        drop_last: bool = False,
        scheduler: "torch.optim.lr_scheduler._LRScheduler" | None = None,
        verbose: bool = False,
        **kwargs,
    ) -> None:
        """
        Fit the classifier on the training set `(x, y)`.

        :param x: Training data.
        :param y: Target values (class labels) one-hot-encoded of shape (nb_samples, nb_classes) or index labels of
                  shape (nb_samples,).
        :param batch_size: Size of batches.
        :param nb_epochs: Number of epochs to use for training.
        :param training_mode: `True` for model set to training mode and `False` for model set to evaluation mode.
        :param drop_last: Set to ``True`` to drop the last incomplete batch, if the dataset size is not divisible by
                          the batch size. If ``False`` and the size of dataset is not divisible by the batch size, then
                          the last batch will be smaller. (default: ``False``)
        :param scheduler: Learning rate scheduler to run at the start of every epoch.
        :param verbose: Display the training progress bar.
        :param kwargs: Dictionary of framework-specific arguments. This parameter is not currently supported for PyTorch
               and providing it takes no effect.
        """
        import torch
        import torch.nn.functional as F
        from torch.utils.data import TensorDataset, DataLoader

        # Set model mode
        self._model.train(mode=training_mode)

        if self._optimizer is None:  # pragma: no cover
            raise ValueError("An optimizer is needed to train the model, but none for provided")

        y = check_and_transform_label_format(y, nb_classes=self.nb_classes)

        # Apply preprocessing
        x_preprocessed, y_preprocessed = self._apply_preprocessing(x, y, fit=True)

        # Check label shape
        y_preprocessed = self.reduce_labels(y_preprocessed)

        # Create dataloader
        x_tensor = torch.from_numpy(x_preprocessed)
        y_tensor = torch.from_numpy(y_preprocessed)
        dataset = TensorDataset(x_tensor, y_tensor)
        dataloader = DataLoader(dataset=dataset, batch_size=batch_size, shuffle=True, drop_last=drop_last)

        m = torch.distributions.normal.Normal(
            torch.tensor([0.0], device=self.device), torch.tensor([1.0], device=self.device)
        )

        # Start training
        for _ in trange(nb_epochs, disable=not verbose):
            for x_batch, y_batch in dataloader:
                # Move inputs to GPU
                x_batch = x_batch.to(self.device)
                y_batch = y_batch.to(self.device)

                input_size = len(x_batch)

                # Tile samples for Gaussian augmentation
                new_shape = [input_size * self.gaussian_samples]
                new_shape.extend(x_batch[0].shape)
                x_batch = x_batch.repeat((1, self.gaussian_samples, 1, 1)).view(new_shape)

                # Add random noise for randomized smoothing
                noise = torch.randn_like(x_batch, device=self.device) * self.scale
                noisy_inputs = x_batch + noise

                # Get model outputs
                outputs = self.model(noisy_inputs)
                outputs = outputs.reshape((input_size, self.gaussian_samples, self.nb_classes))

                # Classification loss
                outputs_softmax = F.softmax(outputs, dim=2).mean(dim=1)
                outputs_log_softmax = torch.log(outputs_softmax + 1e-10)
                classification_loss = F.nll_loss(outputs_log_softmax, y_batch, reduction="sum")

                # Robustness loss
                beta_outputs = outputs * self.beta
                beta_outputs_softmax = F.softmax(beta_outputs, dim=2).mean(dim=1)
                top2_score, top2_idx = torch.topk(beta_outputs_softmax, 2)
                indices_correct = top2_idx[:, 0] == y_batch
                out0, out1 = top2_score[indices_correct, 0], top2_score[indices_correct, 1]
                robustness_loss = m.icdf(out1) - m.icdf(out0)
                indices = (
                    ~torch.isnan(robustness_loss)
                    & ~torch.isinf(robustness_loss)
                    & (torch.abs(robustness_loss) <= self.gamma)
                )
                out0, out1 = out0[indices], out1[indices]
                robustness_loss = m.icdf(out1) - m.icdf(out0) + self.gamma
                robustness_loss = torch.sum(robustness_loss) * self.scale / 2

                # Final objective function
                loss = classification_loss + self.lmbda * robustness_loss
                loss /= input_size
                self._optimizer.zero_grad()
                loss.backward()
                self._optimizer.step()

            if scheduler is not None:
                scheduler.step()

        Fit the classifier on the training set `(x, y)`.

        :param x: Training data.
        :param y: Target values (class labels) one-hot-encoded of shape (nb_samples, nb_classes) or index labels of
                  shape (nb_samples,).
        :param batch_size: Size of batches.
        :param nb_epochs: Number of epochs to use for training.
        :param training_mode: `True` for model set to training mode and `False` for model set to evaluation mode.
        :param drop_last: Set to ``True`` to drop the last incomplete batch, if the dataset size is not divisible by
                          the batch size. If ``False`` and the size of dataset is not divisible by the batch size, then
                          the last batch will be smaller. (default: ``False``)
        :param scheduler: Learning rate scheduler to run at the start of every epoch.
        :param verbose: Display the training progress bar.
        :param kwargs: Dictionary of framework-specific arguments. This parameter is not currently supported for PyTorch
               and providing it takes no effect.
        
art/estimators/certification/randomized_smoothing/macer/pytorch.py