@Override
  public double[] probs(double[] feature) {

    double[] probs = new double[classCount];
    for (int i = 0; i < boosters.length; i++) {
      if (roundIndicator[i]) {
        for (int j = 0; j < boosters[i].length; j++)
          probs[j] += LEARNING_RATE * boosters[i][j].boostPredict(feature);
      }
    }

    IntStream.range(0, classCount).forEach(i -> probs[i] = Math.exp(probs[i]));

    ArraySumUtil.normalize(probs);

    return probs;
  }
  @Override
  public void train() {

    for (int i = 0; i < boosters.length; i++) {

      long t1 = System.currentTimeMillis();

      final int ROUND = i;

      service = Executors.newFixedThreadPool(MAX_THREADS);
      countDownLatch = new CountDownLatch(classCount);
      IntStream.range(0, classCount)
          .forEach(
              j ->
                  service.submit(
                      () -> {
                        long tic = System.currentTimeMillis();

                        try {
                          boosters[ROUND][j].boostInitialize(roundData[j], roundIndices);
                          boosters[ROUND][j].boost();
                        } catch (Throwable t) {
                          log.error(t.getMessage(), t);
                        }

                        long toc = System.currentTimeMillis();
                        log.debug(
                            "round {}, task: {}/{} finished, elapsed {} ms",
                            ROUND,
                            j,
                            classCount,
                            toc - tic);
                        countDownLatch.countDown();
                      }));
      try {
        TimeUnit.SECONDS.sleep(10);
        countDownLatch.await();
      } catch (Throwable t) {
        log.error(t.getMessage(), t);
      }
      service.shutdown();

      roundIndicator[ROUND] = true;

      long t2 = System.currentTimeMillis();

      float[] probs = new float[classCount];
      for (int j = 0; j < trainData.getInstanceLength(); j++) {

        double[] x = trainData.getInstance(j);
        double y = trainData.getLabel(j);
        float[] ys = new float[classCount];
        ys[(int) y] = 1;

        for (int k = 0; k < classCount; k++) {
          scoreCache[j][k] += LEARNING_RATE * boosters[ROUND][k].boostPredict(x);
          probs[k] = (float) Math.exp(scoreCache[j][k]);
        }

        ArraySumUtil.normalize(probs);

        roundKL[ROUND] += (float) ArrayUtil.KLDivergence(ys, probs);

        for (int k = 0; k < classCount; k++) tempLabels[k][j] = ys[k] - probs[k];
      }

      roundKL[ROUND] /= trainData.getInstanceLength();

      for (int j = 0; j < classCount; j++)
        roundData[j] = new DataSet(trainData.getFeatureMatrix(), new Label(tempLabels[j], null));

      indices.shuffle(new Random());
      for (int j = 0; j < roundIndices.length; j++) roundIndices[j] = indices.get(j);

      long t3 = System.currentTimeMillis();

      if (NEED_REPORT) {
        statisticReport(ROUND);
      }

      long t4 = System.currentTimeMillis();

      log.info("round {}, KL {}", ROUND, roundKL[ROUND]);
      log.info(
          "boost {} |update gradient {} |report {}| total {}", t2 - t1, t3 - t2, t4 - t3, t4 - t1);
    }

    log.info("GradientBoostClassification training finished ...");

    if (NEED_REPORT) {
      printRoundReport();
    }
  }