def forward_backward(self, parameters):
        """ Run the forward backward algorithm with the given parameters. """
        # If the features are sparse, we can use an optimization.
        # I'm not using scipy.sparse here because we want to avoid a scipy dependency and also scipy.sparse doesn't seem
        # to handle arrays of shape higher than 2.
        if isinstance(self.sparse_x, str) and self.sparse_x == 'uninitialized':
            if (self.x == 0).sum() * 1.0 / self.x.size > 0.6:
                self.sparse_x = self._construct_sparse_features(self.x)
            else:
                self.sparse_x = 'not sparse'

        I, J, K = self.x.shape
        if not isinstance(self.sparse_x, str):
            C = self.sparse_x[0].shape[2]
            S, _ = parameters.shape
            x_dot_parameters = np.zeros((I, J, S))
            sparse_multiply(x_dot_parameters, self.sparse_x[0], self.sparse_x[1], parameters.T, I, J, K, C, S)
        else:
            x_dot_parameters = np.dot(self.x, parameters.T)  # Pre-compute the dot product
        alpha = self._forward(x_dot_parameters)
        beta = self._backward(x_dot_parameters)
        classes_to_ints = {k: i for i, k in enumerate(set(self.states_to_classes.values()))}
        states_to_classes = np.array([classes_to_ints[self.states_to_classes[state]]
                                      for state in range(max(self.states_to_classes.keys()) + 1)], dtype='int64')
        if not isinstance(self.sparse_x, str):
            ll, deriv = gradient_sparse(alpha, beta, parameters, states_to_classes,
                                        self.sparse_x[0], self.sparse_x[1], classes_to_ints[self.y],
                                        I, J, self.sparse_x[0].shape[2])
        else:
            ll, deriv = gradient(alpha, beta, parameters, states_to_classes,
                                 self.x, classes_to_ints[self.y], I, J, K)
        return ll, deriv
Run the forward backward algorithm with the given parameters.