// Demo: Training of a Neural Network / Back-Propagation algorithm 
// Jan Oostindie, Avans Hogeschool, dd 6-12-2016
// email: jac.oostindie@avans.nl

#include "NeuralNetwork.h"

NeuralNetwork::NeuralNetwork()
{
	V0 = Mat();
	W0 = Mat();
}

NeuralNetwork::~NeuralNetwork()
{
}

Mat NeuralNetwork::Train(Mat& ITset, Mat& OTset)
{
	// V0, W0   : weightfactor matrices
	// dV0, dW0 : weightfactor correction matrices
	Mat dW0, dV0;

	// default number of hiddenNeurons. The definite number is user input  
	// inputNeurons and outputNeurons are implicitly determined via
	// the trainingset, i.e.: inputNeurons = ITset.cols ; outputNeurons = OTset.cols;
	int hiddenNeurons = 8;

	//loadTrainingSet1(ITset, OTset);
	initializeBPN(ITset.cols, hiddenNeurons, OTset.cols, V0, dV0, W0, dW0);
	//testBPN(ITset, OTset, V0, dV0, W0, dW0);


	// IT: current training input of the inputlayer 
	// OT: desired training output of the BPN
	// OH: output of the hiddenlayer
	// OO: output of the outputlayer
	Mat IT, OT, OH, OO;

	// outputError0: error on output for the current input and weighfactors V0, W0
	// outputError1: error on output for the current input and new calculated 
	//               weighfactors, i.e. V1, W1
	double outputError0, outputError1, sumSqrDiffError = MAX_OUTPUT_ERROR + 1;
	Mat V1, W1;

	cout << endl << "Starting neural training..." << endl;

	int runs = 0;
	while ((sumSqrDiffError > MAX_OUTPUT_ERROR) && (runs < MAXRUNS)) {

		sumSqrDiffError = 0;

		for (int inputSetRowNr = 0; inputSetRowNr < ITset.rows; inputSetRowNr++) {

			IT = transpose(getRow(ITset, inputSetRowNr));

			OT = transpose(getRow(OTset, inputSetRowNr));

			calculateOutputHiddenLayer(IT, V0, OH);

			calculateOutputBPN(OH, W0, OO);

			adaptVW(OT, OO, OH, IT, W0, dW0, V0, dV0, W1, V1);

			calculateOutputBPNError(OO, OT, outputError0);

			calculateOutputBPNError(BPN(IT, V1, W1), OT, outputError1);

			sumSqrDiffError += (outputError1 - outputError0) * (outputError1 - outputError0);

			V0 = V1;
			W0 = W1;
		}
		runs++;

		if (runs % 1000 == 0)
			cout << "Completed " << runs << " runs, still working on it.. (" << sumSqrDiffError << ")" << endl;
	}

	cout << "Training complete in " << runs << " runs" << endl;

	Mat inputVectorTrainingSet, outputVectorTrainingSet, outputVectorBPN;

	// druk voor elke input vector uit de trainingset de output vector uit trainingset af 
	// tezamen met de output vector die het getrainde BPN (zie V0, W0) genereerd bij de 
	// betreffende input vector.
	for (int row = 0; row < ITset.rows; row++) {

		// haal volgende inputvector op uit de training set
		inputVectorTrainingSet = transpose(getRow(ITset, row));

		// haal bijbehorende outputvector op uit de training set
		outputVectorTrainingSet = transpose(getRow(OTset, row));

		// bepaal de outputvector die het getrainde BPN oplevert 
		// bij de inputvector uit de trainingset  
		outputVectorBPN = BPN(inputVectorTrainingSet, V0, W0);
	}

	write();

	return outputVectorBPN;
}

void NeuralNetwork::Predict(Mat& ITset, string& name)
{
	Mat output;
	output = transpose(BPN(ITset, V0, W0));

	mat_class(output, name);
}

void NeuralNetwork::Read()
{
	load();
}

void NeuralNetwork::write()
{
	for (pair<int, string> p : classes)
		cout << p.first << " - " << p.second << endl;

	FileStorage fs("factors.yml", FileStorage::WRITE);
	fs << "W0" << W0 << "V0" << V0;
	fs.release();

	FileStorage fs2("classes.yml", FileStorage::WRITE);

	fs2 << "classes" << "{:";

	for (pair<int, string> p : classes)
	{
		string el = "e" + to_string(p.first);
		fs2 << el << p.second;
	}

	fs2 << "}";

	fs2.release();
}

void NeuralNetwork::load()
{
	FileStorage fs("factors.yml", FileStorage::READ);

	fs["W0"] >> W0;
	fs["V0"] >> V0;

	fs.release();

	FileStorage fs2("classes.yml", FileStorage::READ);

	classes.clear();

	FileNode cls = fs2["classes"];
	FileNodeIterator it = cls.begin(), it_end = cls.end();
	int idx = 0;

	// iterate through a sequence using FileNodeIterator
	for (; it != it_end; ++it, idx++)
	{
		cv::FileNode item = *it;
		std::string key = item.name();
		string value = (string)item;

		classes.insert(pair<int, string>(idx, value));
	}

	cout << endl << "Available classes: " << endl;

	for (pair<int, string> p : classes)
		cout << p.first << " - " << p.second << endl;

	cout << endl;

	fs2.release();
}

void NeuralNetwork::getClass(const string& name, Mat& ref)
{
	save_class(name, ref);
}

void NeuralNetwork::save_class(const string& name, Mat& ref)
{
	for (pair<int, string> p : classes)
	{
		if (p.second == name)
		{
			class_mat(p.first, ref);
			return;
		}
	}

	classes.insert(pair<int, string>(classidx, name));
	class_mat(classidx, ref);
	classidx++;
}

void NeuralNetwork::class_mat(int index, Mat& ref)
{
	ref = cv::Mat::zeros(cv::Size(numclasses, 1), CV_32F);
	ref.at<float>(index) = 1;
}

void NeuralNetwork::mat_class(Mat& ref, string& name)
{
	double maxvalue = 0;
	int index = 0;

	for (int i = 0; i < ref.cols; i++)
	{
		double temp = getEntry(ref, 0, i);

		if (temp > maxvalue)
		{
			maxvalue = temp;
			index = i;
		}
	}

	name = classes[index] + " " + to_string(maxvalue*100) + "%";
}