/****************************************************************************

    Neural Network Library
    Copyright (C) 1998 Daniel Franklin

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Library General Public License for more details.

    You should have received a copy of the GNU Library General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
    02111-1307, USA.
              
 ****************************************************************************/

#include <string.h>

#include <nnwork.h>
#include <math.h>
#include <time.h>
#include <stdlib.h>
#include <limits.h>
#include <iostream>
#include <fstream>
#include <iostream.h>
#include <fstream.h>

#define srandom srand
#define random rand

#include <assert.h>
#include <v_lib.h>

// This class implements a simple three-layer backpropagation network.


// Calculate the sigmoid function. This is the transfer function of the nodes.

vec_t sigmoid (vec_t data)
{
	return (1. / (1. + exp (-data)));
}

// Constructors: create the network.

nnwork::nnwork (int input, int hidden, int output)
{
	input_size = input;
	hidden_size = hidden;
	output_size = output;
	srandom (time (0));
	
	hidden_nodes = new nnlayer (hidden_size, input_size);
	assert (hidden_nodes);
	output_nodes = new nnlayer (output_size, hidden_size);
	assert (output_nodes);
}

nnwork::nnwork ()
{
	input_size = 0;
	hidden_size = 0;
	output_size = 0;
	
	hidden_nodes = 0;
	output_nodes = 0;
}

nnwork::nnwork (char *filename)
{
	input_size = 0;
	hidden_size = 0;
	output_size = 0;
	
	hidden_nodes = 0;
	output_nodes = 0;
	
	if (!load (filename))
		cout << "Error: File failed to load.\n";
}

// Destructor: destroys the network.

nnwork::~nnwork ()
{
	if (output_nodes) delete output_nodes;
	if (hidden_nodes) delete hidden_nodes;
}

// Training routine for the network. Uses data as input, compares output with
// desired output, computes errors, adjusts weights attached to each node,
// then repeats until the mean squared error at the output is less than 
// max_MSE. The learning rate is eta.

void nnwork::train (vec_t data [], vec_t desired [], vec_t max_MSE, vec_t eta)
{
	vec_t *output, *output_weight_delta, *hidden_weight_delta;
	vec_t MSE, MSE_max = max_MSE * 2., sum; // slight speed enhancement
	int i, j, k;
#ifdef WAFFLY
	long n = 0;
#endif
	if (input_size == 0 || hidden_size == 0 || output_size == 0) {
		cout << "Warning: stupid dimensions. No action taken.\n";
		return;
	}
	
	output = new vec_t [output_size];
	assert (output);
	output_weight_delta = new vec_t [output_size];
	assert (output_weight_delta);
	hidden_weight_delta = new vec_t [hidden_size];
	assert (hidden_weight_delta);

// Keep going while the Mean Square Error is too high.

	while (1) {
		run (data, output);
		
// Firstly calculate the output layer error terms

		for (k = 0, MSE = 0; k < output_size; k++) {
			output_weight_delta [k] = desired [k] - output [k];
			MSE += output_weight_delta [k] * output_weight_delta [k];
			output_weight_delta [k] *= output [k] * (1 - output [k]);
		}
		
#ifdef WAFFLY
		n++;	
		
		if (!(n % 10))
			cout << "2*MSE (every 10th) currently is: " << MSE << endl;
#endif		
		if (MSE < MSE_max) break;

// And the hidden layer error terms

		for (j = 0; j < hidden_size; j++) {
			for (k = 0, sum = 0; k < output_size; k++)
				sum += output_weight_delta [k] * output_nodes -> nodes [k].weights [j];

			hidden_weight_delta [j] = sum * hidden_nodes -> nodes [j].output * (1 - hidden_nodes -> nodes [j].output);
		}

// Now update the output weights.

		for (k = 0; k < output_size; k++)
			for (j = 0; j < hidden_size; j++)
				output_nodes -> nodes [k].weights [j] += eta * output_weight_delta [k] * hidden_nodes -> nodes [j].output;

// And the hidden weights.

		for (j = 0; j < hidden_size; j++)
			for (i = 0; i < input_size; i++)
				hidden_nodes -> nodes [j].weights [i] += eta * hidden_weight_delta [j] * data [i];
	}

	delete [] output;
	delete [] output_weight_delta;
	delete [] hidden_weight_delta;
}

// Should only be applied after the network has been suitably trained. When
// it is ready, data are applied at the input, and the output (basically a
// boolean output but could be different) goes into result. Naturally, data
// and result are the same size as the input and output vectors respectively.

void nnwork::run (vec_t data [], vec_t result [])
{
	int i, j, k;
	vec_t sum;

	if (input_size == 0 || hidden_size == 0 || output_size == 0) {
		cout << "Warning: stupid dimensions. No action taken.\n";
		return;
	}

	for (j = 0; j < hidden_size; j++) {
		sum = 0;
		
// Calculate the output value

		for (i = 0; i < input_size; i++)
			sum += hidden_nodes -> nodes [j].weights [i] * data [i];

		hidden_nodes -> nodes [j].output = sigmoid (sum);
	}

	for (k = 0; k < output_size; k++) {
		sum = 0;
		
// Calculate the output value

		for (j = 0; j < hidden_size; j++)
			sum += output_nodes -> nodes [k].weights [j] *
				hidden_nodes -> nodes [j].output;

		result [k] = output_nodes -> nodes [k].output = sigmoid (sum);
	}
}

/* 
	Restore the values of the connection weights from a file. Format:

nnlib network weights data file version 1.0
Size: input_nodes hidden_nodes output_nodes
Hidden layer weights:
wh_0,0 wh_0,1 wh_0,2 ... wh_0,input_nodes-1
wh_1,0 wh_1,1 wh_1,2 ... wh_1,input_nodes-1
wh_2,0 wh_2,1 wh_2,2 ... wh_2,input_nodes-1
...
wh_hidden_nodes-1,0 wh_hidden_nodes-1,1 wh_hidden_nodes-1,2 ... wh_hidden_nodes,input_nodes-1
Output layer weights:
wo_0,0 wo_0,1 wo_0,2 ... wo_0,hidden_nodes-1
wo_1,0 wo_1,1 wo_1,2 ... wo_1,hidden_nodes-1
wo_2,0 wo_2,1 wo_2,2 ... wo_2,hidden_nodes-1
...
wo_output_nodes-1,0 wo_output_nodes-1,1 wo_output_nodes-1,2 ... wo_output_nodes-1,hidden_nodes-1


	THERE IS MINIMAL ERROR CHECKING!!! Please don't manually edit the 
	files.
*/

int nnwork::load (char *filename)
{
	ifstream weights (filename);
	int i, j, k;
	int num_input, num_hidden, num_output;
	char temp [50];

	if (weights.good ()) {
		weights.getline (temp, 50, '\n');
		
		if (strcmp (temp, "nnlib network weights data file version 1.0") == 0) {
			weights >> temp >> num_input >> num_hidden >> num_output;

			if (strcmp (temp, "Size:") != 0) {
				cout << "Error in " << filename << ": no \"Size:\" line.\n";
				weights.close ();
				return 0;
			}
			
// If required, resize the network	

			input_size = num_input;

			if (hidden_size != num_hidden) {
				cout << "Resizing neural network.\n";

				hidden_size = num_hidden;
				if (hidden_nodes) delete hidden_nodes;
				hidden_nodes = new nnlayer (hidden_size, input_size);
				assert (hidden_nodes);
			}
			
			if (output_size != num_output) {
				cout << "Resizing neural network.\n";

				output_size = num_output;
				if (output_nodes) delete output_nodes;
				output_nodes = new nnlayer (output_size, hidden_size);
				assert (output_nodes);
			}
			
// Read the hidden weights
			weights.getline (temp, 2, '\n');
			weights.getline (temp, 50, '\n');
			
			if (strcmp (temp, "Hidden layer weights:") == 0)
				for (j = 0; j < hidden_size; j++)
					for (i = 0; i < input_size; i++) {
						weights >> hidden_nodes -> nodes [j].weights [i];
						if (!weights.good ()) {
							cout << "Premature EOF on data file\n";
							weights.close ();
							return 0;
						}
					}
			else {
				cout << "Error in " << filename << ": no  \"Hidden layer weights:\" line.\n";
				weights.close ();
				return 0;
			}

// And the output weights
			weights.getline (temp, 2, '\n');
			weights.getline (temp, 50, '\n');
			
			if (strcmp (temp, "Output layer weights:") == 0)
				for (k = 0; k < output_size; k++)
					for (j = 0; j < hidden_size; j++) {
						weights >> output_nodes -> nodes [k].weights [j];
						if (!weights.good ()) {
							cout << "Premature EOF on data file\n";
							weights.close ();
							return 0;
						}
					}
			else {
				cout << "Error in " << filename << ": no  \"Output layer weights:\" line.\n";
				weights.close ();
				return 0;
			}

			weights.close ();
		} else {
			cout << "Unsupported file version, or garbage in file.\n";
			return 0;
		}
		
		return 1;
	} else
		return 0;
}

// Save the current weights to <filename>. Suggested extension is .nnw

int nnwork::save (char *filename)
{
	ofstream weights (filename);
	int i, j, k;

	if (input_size == 0 || hidden_size == 0 || output_size == 0) {
		cout << "Warning: silly dimensions. No action taken.\n";
		return 0;
	}

	if (weights.good ()) {
		weights << "nnlib network weights data file version 1.0\n";
		weights << "Size: " << input_size << " " << hidden_size << " " << output_size << endl;
		
// Write out the hidden weights

		weights << "Hidden layer weights:\n";

		for (j = 0; j < hidden_size; j++) {
			for (i = 0; i < input_size; i++)
				weights << hidden_nodes -> nodes [j].weights [i] << "\t";
			weights << endl;
		}
		
		weights << "Output layer weights:\n";

// And the output weights
			
		for (k = 0; k < output_size; k++) {
			for (j = 0; j < hidden_size; j++)
				weights << output_nodes -> nodes [k].weights [j] << "\t";
			weights << endl;
		}

		weights.close ();
		
		return 1;
	} else
		return 0;
}
