package yi.stat.nn;

import java.util.*;

/**
 * For a NN with the prediction of true and false (1 or 0), if the positive
 * instance number is quite different from the negative instance number, then
 * the predict results are biased.  For example, if there are a lots of negative
 * samples and only a little bit of positive samples, the trained NN will tend
 * to predict false for all the case.   To balance this, when training, we
 * should favorite more on positive cases.  In general, we allow the user to
 * specify different weights for positive cases and negative cases.  In the case
 * you don't want to miss anything and don't care about false alarm, user can
 * give more weight to positive cases.
 *
 * It works that when training, NN first pick up a random number to decide
 * whether to select from positive samples or negative samples, and then pick
 * another random number to decide which instance to use.
 */

public class WeightedBinaryNN extends NeuralNet{

  /**
   * The ratio of positive instance weight to negative instance weight.
   */
  double m_posNegWeightRatio = -1;

  public WeightedBinaryNN(int d, int nh, int k, double rate){
    super(d, nh, k, rate);
  }

  /**
   * Set the ratio of positive instance weight to negative instance weight.
   * @param ratio
   */
  public void setPosNegWeightRatio(double ratio) {
    m_posNegWeightRatio = ratio;
  }

  public double getPosNegWeightRatio() {
    return m_posNegWeightRatio;
  }

  /**
   * Tell wehter an instance is positive or not.
   * It assumes that the categories are binary categories.  Only 1 and 0 should
   * be found in the categories[i][0].  (i for each image)
   * @param categories
   * @return
   */
  protected boolean isPositive(double[] categories) {
    return categories[0] > 0.5;
  }

  Random random = new Random();


  /**
   * It assumes that the categories are binary categories.  Only 1 and 0 should
   * be found in the categories[i][0].  (i for each image)
   *
   * Since random order is assumed.   It is not true that all the samples will
   * be used exactly once.   For example, if there are 30 positive samples and
   * 70 negative samples, and the positive-negative-weight-ratio is 1 (which
   * means positive samples will get the same probability to be picked up as
   * negative samples), then in the 100 samples tried, there will be about
   * 50 positive samples and 50 negative sample.  It is possible that some
   * positive samples will be used several times (or 0 times) and it is also
   * possible that some negative samples will not be used at all (or several
   * times)
   *
   * Train one iteration on all the data.  User shouldn't call this method
   * directly since this method skips the "normalization" step (!!!)
   * @param x
   * @param categories
   * @return
   */
  protected double train(double[][] x, double[][] categories) {
    int nsamples = categories.length; // number of training samples

    // find out the positive samples and negative samples
    int pos_num = 0;
    for (int i = 0; i < categories.length; i++) {
      if (isPositive(categories[i])){
        pos_num++;
      }
    }
    int neg_num = nsamples - pos_num;
    int[] pos_samples = new int[pos_num];
    int[] neg_samples = new int[neg_num];
    int pos_count = 0, neg_count = 0;
    for (int i=0; i<categories.length; i++) {
      if (categories[i][0] > 0.5) {
        pos_samples[pos_count++] = i;
      } else {
        neg_samples[neg_count++] = i;
      }
    }

    // the first random number decide to pick from positive samples or negative
    // samples.
    // first decide the cut for negative samples or positive samples.  the
    // generated random number is within [0, 1].  If it is less than the "cut"
    // we will pick from negative samples; and vice versa
    // if user didn't specified the positive-negative-weight-ratio, we will
    // use equal probability for positive samples and negative samples
    double cut = 0.5;
    if (m_posNegWeightRatio > 0) {
      // if user specified the positive-negative-weight-ratio, we will use it
      // to define the cut.  If the probability to pick up positive instances
      // is the "ratio", the thant to pick up negative samples is "1".  So the
      // cut should be 1*neg_num/(1*neg_num+ratio*pos_num);
      cut = 1.0 * neg_num / (1.0 * neg_num + m_posNegWeightRatio * pos_num);
    } else {
      m_posNegWeightRatio = (double)neg_num / (double)pos_num;
      System.out.println("use positive-negative-weight-ratio of " + m_posNegWeightRatio);
    }

    double error = 0;
    int neg_used_count = 0, pos_used_count = 0;
    // try "nsamples" samples per iteration
    for (int j = 0; j < nsamples; j++) {
      int selected = 0;
      double r1 = random.nextDouble();
      if (r1 < cut) {
        double r2 = random.nextDouble() * neg_num;
        int r2_int = (int)Math.floor(r2);
        if (r2_int >= neg_num) r2_int = neg_num -1;
        selected = neg_samples[r2_int];
        neg_used_count ++;
      } else {
        double r2 = random.nextDouble() * pos_num;
        int r2_int = (int)Math.floor(r2);
        if (r2_int >= pos_num) r2_int = pos_num -1;
        selected = pos_samples[r2_int];
        pos_used_count++;
      }

      train(x[selected], categories[selected]);

      // sum the errors
      error += getError ();
    }
//    System.out.println("instances used to train : pos = " + pos_used_count + ", neg = " + neg_used_count);

    return error/x.length;
  }

  /**
   * Test on the data (the first dimension is the different data, the second
   * dimension is the components in one data vector).  Return the test result
   * in "y".  If you don't want to have the "y", just pass in "null"
   *
   * Since positive samples and negative samples get different weights, the
   * summation of the errors should consider the weights too.
   * @param x
   * @param target
   * @param y
   * @return
   */
  public double test(double[][] x, double[][] target, double [][] y, boolean ignore_normalization) {
    double sum = 0;
    double sum_weight = 0;
    for (int i=0; i<x.length; i++) {
      boolean positive = isPositive(target[i]);
      double err = test(x[i], target[i], (y == null)? null: y[i], ignore_normalization);
      if (positive) {
        sum += err * m_posNegWeightRatio;
        sum_weight += m_posNegWeightRatio;
      } else {
        sum += err;
        sum_weight += 1;
      }

    }
    return sum/sum_weight;
  }

}