// file: $isip/class/pr/NeuralNetwork/svm_05.cc
//

// isip include files
//
#include "NeuralNetwork.h"
#include <stdlib.h>
#include <File.h>
#include <Console.h>

// method: init
//
// arguments:
//  long epochs: (input) number of epochs
//  long units: (input) number of hidden units per layer
//  long layers: (input) number of hidden layers
//  float bias: (input) network bias
//  float rate: (input) network learning rate
//  float momentum: (input) network momentum
//
// return: boolean value indicating status
//
// this method loads the output target used train the neural network
//
boolean NeuralNetwork::init(long epochs_a, long units_a, long layers_a,
			    float bias_a, float rate_a, float momentum_a) {

  // error checking
  //
  if (epochs_a < 1) {
    return Error::handle(name(), L"init", Error::ARG, __FILE__, __LINE__);
  }
  if (units_a < 1) {
    return Error::handle(name(), L"init", Error::ARG, __FILE__, __LINE__);
  }    
  if (layers_a != 1) {
    return Error::handle(name(), L"init", Error::ARG, __FILE__, __LINE__);
  }
  
  // set the number of epochs
  //
  epochs_d = epochs_a;
  
  // set the number of hidden units and hidden layers
  //
  hidden_layers_d.setLength(layers_a);
  for (int i=0; i < layers_a; i++) {
    hidden_layers_d(i).setLength(units_a + 1);
  }

  // set the network bias
  //
  bias_d = bias_a;

  // set the network learning rate
  //
  eta_d = rate_a;

  // set the network momentum
  //
  momentum_d = momentum_a;

  // exit gracefully
  //
  return true;  
}

// method: loadFeatures
//
// arguments:
//  String& file: (input) training feature vectors path
//
// return: boolean value indicating status
//
// this method loads the feature vectors used train the neural network
//
boolean NeuralNetwork::loadFeatures(String& file_a) {

  // declare local variables
  //
  File fp;
  String line;
  String substring;
  String substring1;  
  
  long pos = 0;
  int index = 0;
  int dimension = 0;  
  float value1 = 0.0;
  
  // open the file for reading
  //
  if (!fp.open(file_a)) {
    return Error::handle(name(), L"loadFeatures", Error::ARG,
			 __FILE__, __LINE__);
  }

  // read the contents of the file
  //
  while(fp.get(line)) {

    // remove whitespaces from the line
    //
    line.trim();

    // ignore all comment tags
    //
    pos = line.firstStr(DEF_COMMENT_TAG);
    if (pos != 0) {

      // set the feature vector values
      //
      dimension = 0;    
      while (line.tokenize(substring, pos)) {
	substring.get(value1);	
	features_d.setLength(index+1);
	features_d(index).setLength(dimension+1);
	features_d(index)(dimension++) = value1;
      }
      index++;
    }
  }

  // close the file descriptor
  //
  fp.close();

  // exit gracefully
  //
  return true;  
}

// method: loadOutputs
//
// arguments:
//  String& file: (input) output target path
//
// return: boolean value indicating status
//
// this method loads the output target used train the neural network
//
boolean NeuralNetwork::loadOutputs(String& file_a) {

  // declare local variables
  //
  File fp;
  String line;
  String substring;
  String substring1;  
  
  long pos = 0;
  int index = 0;
  int dimension = 0;  
  float value1 = 0.0;
  
  // open the file for reading
  //
  if (!fp.open(file_a)) {
    return Error::handle(name(), L"loadOutputs", Error::ARG,
			 __FILE__, __LINE__);
  }

  // read the contents of the file
  //
  while(fp.get(line)) {

    // remove whitespaces from the line
    //
    line.trim();

    // ignore all comment tags
    //
    pos = line.firstStr(DEF_COMMENT_TAG);
    if (pos != 0) {

      // set the feature vector values
      //
      dimension = 0;    
      while (line.tokenize(substring, pos)) {
	substring.get(value1);	
	outputs_d.setLength(index+1);
	outputs_d(index).setLength(dimension+1);
	outputs_d(index)(dimension++) = value1;
      }
      index++;
    }
  }

  // close the file descriptor
  //
  fp.close();

  // exit gracefully
  //
  return true;  
}

// method: loadTestFeatures
//
// arguments:
//  String& file: (input) test feature vectors path
//
// return: boolean value indicating status
//
// this method loads the feature vectors used test the neural network
//
boolean NeuralNetwork::loadTestFeatures(String& file_a) {

  // declare local variables
  //
  File fp;
  String line;
  String substring;
  String substring1;  
  
  long pos = 0;
  int index = 0;
  int dimension = 0;  
  float value1 = 0.0;
  
  // open the file for reading
  //
  if (!fp.open(file_a)) {
    return Error::handle(name(), L"loadFeatures", Error::ARG,
			 __FILE__, __LINE__);
  }

  // read the contents of the file
  //
  while(fp.get(line)) {

    // remove whitespaces from the line
    //
    line.trim();

    // ignore all comment tags
    //
    pos = line.firstStr(DEF_COMMENT_TAG);
    if (pos != 0) {

      // set the feature vector values
      //
      dimension = 0;    
      while (line.tokenize(substring, pos)) {
	substring.get(value1);	
	test_features_d.setLength(index+1);
	test_features_d(index).setLength(dimension+1);
	test_features_d(index)(dimension++) = value1;
      }
      index++;
    }
  }

  // close the file descriptor
  //
  fp.close();

  // exit gracefully
  //
  return true;  
}

// method: loadTestOutputs
//
// arguments:
//  String& file: (input) output target path
//
// return: boolean value indicating status
//
// this method loads the output target used test the neural network
//
boolean NeuralNetwork::loadTestOutputs(String& file_a) {

  // declare local variables
  //
  File fp;
  String line;
  String substring;
  String substring1;  
  
  long pos = 0;
  int index = 0;
  int dimension = 0;  
  float value1 = 0.0;
  
  // open the file for reading
  //
  if (!fp.open(file_a)) {
    return Error::handle(name(), L"loadOutputs", Error::ARG,
			 __FILE__, __LINE__);
  }

  // read the contents of the file
  //
  while(fp.get(line)) {

    // remove whitespaces from the line
    //
    line.trim();

    // ignore all comment tags
    //
    pos = line.firstStr(DEF_COMMENT_TAG);
    if (pos != 0) {

      // set the feature vector values
      //
      dimension = 0;    
      while (line.tokenize(substring, pos)) {
	substring.get(value1);	
	test_outputs_d.setLength(index+1);
	test_outputs_d(index).setLength(dimension+1);
	test_outputs_d(index)(dimension++) = value1;
      }
      index++;
    }
  }

  // close the file descriptor
  //
  fp.close();

  // exit gracefully
  //
  return true;  
}

// method: writeModel
//
// arguments:
//  String& file: (input) output file
//
// return: boolean value indicating status
//
// this method write the model to file
//
boolean NeuralNetwork::writeModel(String& file_a) {

  // open the file descriptor
  //
  Sof tmp_file;
  tmp_file.open(file_a, File::WRITE_ONLY);
  
  // write the neural network weights
  //
  w1_d.write(tmp_file, (long)0);
  h1_d.write(tmp_file, (long)1);    
  w2_d.write(tmp_file, (long)2);  
    
  // close the file descriptor
  //
  tmp_file.close();
     
  // exit gracefully
  //
  return true;
}

// method: test
//
// arguments:
//  String& model: (input) trained model
//  String& test: (input) test features
//  String& output: (input) test outputs
//
// return: boolean value indicating status
//
// this method loads the testing data and generated the statistics
//
boolean NeuralNetwork::test(String& model_a, String& test_a,
			    String& output_a) {

  // declare local variables
  //
  long N = 0;
  long A = 0;
  long B = 0;
  long C = 0;  
  long misclassified = 0;
  
  float accum = 0;
  float reference = 0;
  float hypothesis = 0;
  float classification = 0;
  
  // load the trained models
  //
  Sof tmp_file;
  tmp_file.open(model_a);

  // read the neural network weights
  //
  w1_d.read(tmp_file, (long)0);
  h1_d.read(tmp_file, (long)1);    
  w2_d.read(tmp_file, (long)2);

  // close the file descriptor
  //
  tmp_file.close();
  
  // load the testing data
  //
  loadTestFeatures(test_a);
  loadTestOutputs(output_a);

  // dimensions checking
  //
  if (test_features_d.length() != features_d.length()) {
    return Error::handle(name(), L"test", Error::ARG, __FILE__, __LINE__);
  }
  if (test_outputs_d.length() != outputs_d.length()) {
    return Error::handle(name(), L"test", Error::ARG, __FILE__, __LINE__);
  }  
  
  // determine the dimensions of the test data
  //
  N = test_features_d.length();
  A = test_features_d(0).length() + 1;
  B = hidden_layers_d(0).length();
  C = test_outputs_d(0).length();

  // classify the test set
  //
  fprintf(stdout, "\n\nClassifying the test examples...\n");
  fflush(stdout);
      
  // outer loop - iterates over each observation
  //
  for (int obser=0; obser < N; obser++) {

    // display the observation we are on
    //
    fprintf(stdout, "classifying observation %d...\n", obser);
    fflush(stdout);
    
    // assign the activation levels to the inputs for the current observation
    //
    input_layer_d(0) = 1;
    for (int j=1; j < A; j++) {
      input_layer_d(j) = test_features_d(obser)(j - 1);
    }

    // propogate the activations form the units in the input layer
    // to the units in the hidden layer
    //
    hidden_layers_d(0)(0) = 1;
    for (int j=1; j < B; j++) {
      accum = 0;
      for (int i=0; i < A; i++) {
	accum += w1_d(i, j) * input_layer_d(i);
      }
      hidden_layers_d(0)(j) = 1 / (1 + exp(-accum));
    }
      
    // propogate the activations form the units in the hidden layer
    // to the units in the output layer
    // test_outputs_d(obser)(j)
    for (int j=0; j < C; j++) {
      accum = 0;
      for (int i=0; i < B; i++) {
	accum += w2_d(i, j) * hidden_layers_d(0)(i);
      }
      output_layer_d(j) = 1 / (1 + exp(-accum));

      // retrieve the reference and hypothesis scores
      //
      reference = (float)test_outputs_d(obser)(j);
      hypothesis = (float)output_layer_d(j);

      // determine the classification error for the example
      //
      if (((reference >= 0.5) && (hypothesis < 0.5)) ||
	  ((reference < 0.5) && (hypothesis >= 0.5))) {
	misclassified++;
      }

      // display the reference and hypothesis for the example
      //
      fprintf(stdout, "reference: %f\n", (float)test_outputs_d(obser)(j));
      fprintf(stdout, "hypothesis: %f\n", (float)output_layer_d(j));      
      fflush(stdout);
    }
    
  } // end outer loop

  // display the classification statistics
  //
  classification = ((float)misclassified/(float)N) * (float)100;
  fprintf(stdout, "--------------------------------------------------\n");  
  fprintf(stdout, "Total number of observations: %ld\n", N);
  fprintf(stdout, "Observations correctly classified: %ld\n", N-misclassified);
  fprintf(stdout, "Observations incorrectly classified: %ld\n", misclassified);
  fprintf(stdout, "Classification error: %f percent\n", classification);
  fprintf(stdout, "--------------------------------------------------\n");    
  fflush(stdout);
      
  // exit gracefully
  //
  return true;
}

// method: train
//
// arguments:
//  String& train: (input) training features
//  String& output: (input) training outputs
//  String& output: (input) trained models output
//
// return: boolean value indicating status
//
// this method loads the training data and start learning
//
boolean NeuralNetwork::train(String& train_a, String& output_a,
			     String& model_a) {

  // declare local variables
  //
  long A = 0;
  long B = 0;
  long C = 0;
  long N = 0;
  long misclassified = 0;
  
  float error = 0;
  float accum = 0;
  float reference = 0;
  float hypothesis = 0;
  float average_squared_error = 0;  
  
  // initialize the random number generator
  //
  srand48(time(NULL));
  
  // load the training data
  //
  loadFeatures(train_a);
  loadOutputs(output_a);

  // set up the neural network dimensions
  //
  N = features_d.length();
  A = features_d(0).length() + 1;
  B = hidden_layers_d(0).length();
  C = outputs_d(0).length();
  
  // set up the neural network
  //
  input_layer_d.setLength(A);
  output_layer_d.setLength(C);    
  
  // initialize the network weights between the input layer and hidden layer
  //
  w1_d.setDimensions(A, B);
  
  for (int i=0; i < A; i++) {
    for (int j=0; j < B; j++) {
      w1_d.setValue(i, j, drand48());
    }
  }
  
  // initialize the network weights between the hidden layer and output layer
  //  
  w2_d.setDimensions(B, C);

  for (int i=0; i < B; i++) {
    for (int j=0; j < C; j++) {
      w2_d.setValue(i, j, drand48());
    }
  }
  
  // outer loop -loop over the number of epochs
  //
  for (int epoch=0; epoch < epochs_d; epoch++) {

    // display the epoch we are on
    //
    fprintf(stdout, "starting epoch %d...\n", epoch);
    fflush(stdout);

    // inner loop - iterates over each observation
    //
    error = 0;
    misclassified = 0;
    for (int obser=0; obser < N; obser++) {

      // assign the activation levels to the inputs for the current observation
      //
      input_layer_d(0) = bias_d;
      for (int j=1; j < A; j++) {
	input_layer_d(j) = features_d(obser)(j - 1);
      }

      // propogate the activations form the units in the input layer
      // to the units in the hidden layer
      //
      hidden_layers_d(0)(0) = bias_d;
      for (int j=1; j < B; j++) {
	accum = 0;
	for (int i=0; i < A; i++) {
	  accum += w1_d(i, j) * input_layer_d(i);
	}
	hidden_layers_d(0)(j) = 1 / (1 + exp(-accum));
      }
      
      // propogate the activations form the units in the hidden layer
      // to the units in the output layer
      //
      for (int j=0; j < C; j++) {
	accum = 0;
	for (int i=0; i < B; i++) {
	  accum += w2_d(i, j) * hidden_layers_d(0)(i);
	}
	output_layer_d(j) = 1 / (1 + exp(-accum));

	// compute the sum squared error
	//
	error += pow((double)(output_layer_d(j) -
			      outputs_d(obser)(j)), (double)2);

	// determine if the netowrk has misclassified the example
	//
	reference = (float)outputs_d(obser)(j);
	hypothesis = (float)output_layer_d(j);
	if (((reference >= 0.5) && (hypothesis < 0.5)) ||
	    ((reference < 0.5) && (hypothesis >= 0.5))) {
	  misclassified++;
	}	  
      }
      
      // compute the errors of the units in the output layer
      //
      VectorFloat delta2(C);
      for (int j=0; j < C; j++) {
	delta2(j) = output_layer_d(j) * (1 - output_layer_d(j)) *
	  (outputs_d(obser)(j) - output_layer_d(j));
      }

      // compute the errors of the units in the hidden layer
      //
      VectorFloat delta1(B);
      for (int j=1; j < B; j++) {
	accum = 0;
	for (int i=0; i < C; i++) {
	  accum += delta2(i) * w2_d(j, i);
	}
	delta1(j) = hidden_layers_d(0)(j) *
	  (1 - hidden_layers_d(0)(j)) * accum;
      }

      // adjust the weights between the hidden layer and the output layer
      //
      MatrixFloat deltaW2(B, C);
      for (int i=0; i < B; i++) {
	for (int j=0; j < C; j++) {
	  deltaW2.setValue(i, j, (eta_d * delta2(j) * hidden_layers_d(0)(i)));
	}
      }
      w2_d.add(deltaW2);
      
      // adjust the weights between the hidden layer and the output layer
      //
      MatrixFloat deltaW1(A, B);
      for (int i=0; i < A; i++) {
	for (int j=1; j < B; j++) {
	  deltaW1.setValue(i, j, (eta_d * delta1(j) * input_layer_d(i)));
	}
      }
      w1_d.add(deltaW1);
      
    } // end of inner loop

    // display the average squared error
    //
    average_squared_error = error / (float)N;    
    fprintf(stdout, "average squared error: %f\n", average_squared_error);
    fflush(stdout);

    // determine if the neural network has converged
    //
    if (misclassified == 0) {

      fprintf(stdout, "neural network has converged...\n");
      fflush(stdout);

      // write the network weights
      //
      writeModel(model_a);
      return true;
    }
    
  } // end of outer loop
  
  // write the network weights
  //
  writeModel(model_a);
  
  // exit gracefully
  //
  return true; 
}

