def loss_gradient(self, x: np.ndarray, y: np.ndarray, **kwargs) -> np.ndarray:
        """
        Compute the gradient of the loss function w.r.t. `x`.

        :param x: Samples of shape (nb_samples, seq_length). Note that, it is allowable that sequences in the batch
                  could have different lengths. A possible example of `x` could be:
                  `x = np.array([np.array([0.1, 0.2, 0.1, 0.4]), np.array([0.3, 0.1])])`.
        :param y: Target values of shape (nb_samples). Each sample in `y` is a string, and it may possess different
                  lengths. A possible example of `y` could be: `y = np.array(['SIXTY ONE', 'HELLO'])`.
        :return: Loss gradients of the same shape as `x`.
        """
        x_in = np.empty(len(x), dtype=object)
        x_in[:] = list(x)

        # Put the model in the training mode, otherwise CUDA can't backpropagate through the model.
        # However, model uses batch norm layers which need to be frozen
        self.model.train()
        self.set_batchnorm(train=False)

        # Apply preprocessing
        x_preprocessed, y_preprocessed = self._apply_preprocessing(x_in, y, fit=False)

        # Transform data into the model input space
        batch_dict, batch_idx = self._transform_model_input(x=x_preprocessed, y=y_preprocessed, compute_gradient=True)

        loss, _, _ = self.criterion(self.model, batch_dict)
        loss.backward()

        # Get results
        results_list = []
        src_frames = batch_dict["net_input"]["src_tokens"].grad.cpu().numpy().copy()
        src_lengths = batch_dict["net_input"]["src_lengths"].cpu().numpy().copy()
        for i, _ in enumerate(x_preprocessed):
            results_list.append(src_frames[i, : src_lengths[i], :])

        results = np.array(results_list)

        if results.shape[0] == 1:
            results_ = np.empty(len(results), dtype=object)
            results_[:] = list(results)
            results = results_

        # Rearrange to the original order
        results_ = results.copy()
        results[batch_idx] = results_

        results = self._apply_preprocessing_gradient(x_in, results)

        if x.dtype != object:
            results = np.array([i for i in results], dtype=x.dtype)  # pylint: disable=unnecessary-comprehension
            assert results.shape == x.shape and results.dtype == x.dtype
        else:
            results = np.array([np.squeeze(res) for res in results], dtype=object)

        # Unfreeze batch norm layers again
        self.set_batchnorm(train=True)

        return results

        Compute the gradient of the loss function w.r.t. `x`.

        :param x: Samples of shape (nb_samples, seq_length). Note that, it is allowable that sequences in the batch
                  could have different lengths. A possible example of `x` could be:
                  `x = np.array([np.array([0.1, 0.2, 0.1, 0.4]), np.array([0.3, 0.1])])`.
        :param y: Target values of shape (nb_samples). Each sample in `y` is a string, and it may possess different
                  lengths. A possible example of `y` could be: `y = np.array(['SIXTY ONE', 'HELLO'])`.
        :return: Loss gradients of the same shape as `x`.
        
art/estimators/speech_recognition/pytorch_espresso.py