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`.
        """
        # Apply preprocessing
        x_preprocessed, _ = self._apply_preprocessing(x, None, fit=False)

        x_in = np.empty(len(x_preprocessed), dtype=object)
        x_in[:] = list(x_preprocessed)

        # 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)

        # Transform data into the model input space
        inputs, targets, input_rates, target_sizes, _ = self._transform_model_input(x=x_in, y=y, compute_gradient=True)

        # Compute real input sizes
        input_sizes = input_rates.mul_(inputs.size()[-1]).int()

        # Call to DeepSpeech model for prediction
        outputs, output_sizes, _ = self._model(inputs.to(self._device), input_sizes.to(self._device))
        outputs = outputs.transpose(0, 1)

        if self._version == 2:
            outputs = outputs.float()
        else:
            outputs = outputs.log_softmax(-1)

        # Compute the loss
        loss = self.criterion(outputs, targets, output_sizes, target_sizes).to(self._device)

        # Average the loss by mini batch if version 2 of DeepSpeech is used
        if self._version == 2:
            loss = loss / inputs.size(0)

        # Compute gradients
        if self.use_amp:  # pragma: no cover
            from apex import amp

            with amp.scale_loss(loss, self.optimizer) as scaled_loss:
                scaled_loss.backward()

        else:
            loss.backward()

        # Get results
        results_list = []
        for i, _ in enumerate(x_in):
            results_list.append(x_in[i].grad.cpu().numpy().copy())

        results = np.array(results_list)

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

        results = self._apply_preprocessing_gradient(x, 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

        # 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_deep_speech.py