Update the gradients of marginal log likelihood with respect to the parameters of warping function

        Parameters
        ----------
        X : array_like, shape = (n_samples, n_features)
            The input BEFORE warping

        dL_dW : array_like, shape = (n_samples, n_features)
            The gradient of marginal log likelihood with respect to the Warped input

        Math
        ----
        let w = f(x), the input after warping, then
        dW_da = b * (1 - x^a)^(b - 1) * x^a * ln(x)
        dW_db = - (1 - x^a)^b * ln(1 - x^a)
        dL_da = dL_dW * dW_da
        dL_db = dL_dW * dW_db

def update_grads(self, X, dL_dW):
        """Update the gradients of marginal log likelihood with respect to the parameters of warping function

        Parameters
        ----------
        X : array_like, shape = (n_samples, n_features)
            The input BEFORE warping

        dL_dW : array_like, shape = (n_samples, n_features)
            The gradient of marginal log likelihood with respect to the Warped input

        Math
        ----
        let w = f(x), the input after warping, then
        dW_da = b * (1 - x^a)^(b - 1) * x^a * ln(x)
        dW_db = - (1 - x^a)^b * ln(1 - x^a)
        dL_da = dL_dW * dW_da
        dL_db = dL_dW * dW_db
        """
        for i_seq, i_fea in enumerate(self.warping_indices):
            ai, bi = self.params[i_seq][0], self.params[i_seq][1]

            # cache some value for save some computation
            x_pow_a = np.power(self.X_normalized[:, i_fea], ai)

            # compute gradient for ai, bi on all X
            dz_dai = bi * np.power(1 - x_pow_a, bi-1) * x_pow_a * np.log(self.X_normalized[:, i_fea])
            dz_dbi = - np.power(1 - x_pow_a, bi) * np.log(1 - x_pow_a)

            # sum gradients on all the data
            dL_dai = np.sum(dL_dW[:, i_fea] * dz_dai)
            dL_dbi = np.sum(dL_dW[:, i_fea] * dz_dbi)
            self.params[i_seq][0].gradient[:] = dL_dai
            self.params[i_seq][1].gradient[:] = dL_dbi