code: def update_grads(self, X, dL_dW):
          
          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
docs: 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