www.xbdev.net
xbdev - software development
Saturday June 12, 2021
home | about | contact | Donations



smallnnde: Neural Network that uses Differential Evolutionary (DE) algorithm for training

by Benjamin Kenwright

smallnnde is a flexible neural network implementation with DE training algorithm. Designed to be small, free, and open source. The test case example uses a 1-3-1 network configuration to emulate a sine wave function.

* Neural network with DE training
* Support multiple layers - i.e., multilayer perceptron (MLP)
* Customizable (easy to configure for different topologies/layers/connections)
* Educational version (sometimes using libraries and packages masks the beauty of just how simple a neural network is at its heart)
* Not dependent on any libraries (vanilla C++)
* Importantly, it's a fun piece of code to play around with and learn about neural networks


#include <algorithm> //std::max(..) std::min(..)
#include <iostream> // cout
#include <vector>
#include <map>
#include <math.h>
#include <time.h>


using namespace std;
#define TrainSetSize 20
#define PI 3.141592653589793238463
#define epoch 1000000

#define POPULATIONSIZE 50 

#define DBG_ASSERT(f) { if(!(f)){ __debugbreak(); }; }
double sigmoidF(double x) { return (1.0f / (1.0f + std::exp(-x))); }
//double random(double min, double max) { return min + rand() / (RAND_MAX / (max - min + 1.0) + 1.0); }
double random(double min, double max) { return min + (max - min) * ((double)rand() / (double)RAND_MAX); }

double clamp(double n, double lower, double upper) { return std::max(lower, std::min(n, upper)); }

struct neuron {
	neuron() { id = rand();  bias = random(-1, 1); outputF = -1; }
	int                  	  id;

	std::map<int, neuron*>    incomingtargets;

	double					  bias;
	std::map<int, double >    incomingweights;
	double outputF; // f(x)  forward

	void connect(neuron* n, double weight = random(-1, 1)) {
		n->incomingtargets[this->id] = this;
		n->incomingweights[this->id] = weight;
	}
	double activate(const double* input) {
		if (input != NULL) {
			//this->outputB = 1;      // f'(x)
			this->outputF = *input; // f(x)
		}
		else {
			double sum = 0; // sum (x * w) + b
			map<int, neuron*>::iterator it;
			for (it = this->incomingtargets.begin(); it != this->incomingtargets.end(); it++)
			{
				sum += (this->incomingtargets[it->first]->outputF * this->incomingweights[it->first]);
			}
			sum += this->bias;
			this->outputF = sigmoidF(sum);// f(x)
		}
		return this->outputF;
	}
};

struct network {
	vector<neuron*> inputs;//  Input Layer w/ 1 neurons
	vector<neuron*> hiddens;// Hiddent Layer w/ 3 neurons
	vector<neuron*> outputs;// Output Layer w/ 1 neuron
							// Connect Input Layer to Hidden Layer
	double error;

	network()
	{
		error = 0;
		inputs.push_back(new neuron()); // Input Layer w/ 1 neurons
		hiddens.push_back(new neuron());  hiddens.push_back(new neuron()); hiddens.push_back(new neuron()); // Hiddent Layer w/ 3 neurons
		outputs.push_back(new neuron());  // Output Layer w/ 1 neuron

		inputs[0]->connect(hiddens[0]);		inputs[0]->connect(hiddens[1]);		inputs[0]->connect(hiddens[2]);
		// Connect Hidden Layer to Output Layer
		hiddens[0]->connect(outputs[0]);	hiddens[1]->connect(outputs[0]);	hiddens[2]->connect(outputs[0]);
	}
	~network()
	{
		delete inputs[0];
		delete hiddens[0];
		delete hiddens[1];
		delete hiddens[2];
		delete outputs[0];
	}

	double activate(const double* input, const double* output) {
		for (int i = 0; i < 1; i++) inputs[i]->activate(input);
		for (int i = 0; i < 3; i++) hiddens[i]->activate(NULL);
		for (int i = 0; i < 1; i++) outputs[i]->activate(NULL);
		if (output != NULL) error += abs(*output - outputs[0]->outputF);
		return outputs[0]->outputF;
	}

	double GetFitness() const { return error; }

	int getDimension() const
	{
		return 5 + 6;

	} // 5 neurons (5xbias), 6 connections (6xweights)

	double& getData(int dimensionIndex)
	{
		// bias and incomingweights
		neuron* allneurons[] = { inputs[0], hiddens[0], hiddens[1], hiddens[2], outputs[0] };
		int count = 0;
		for (int i = 0; i<5; i++)
		{
			if (count == dimensionIndex)
			{
				return allneurons[i]->bias;
			}
			count++;
			for (int k = 0; k<(int)allneurons[i]->incomingweights.size(); k++)
			{
				if (count == dimensionIndex)
				{
					return allneurons[i]->incomingweights[k];
				}
				count++;
			}
		}
		DBG_ASSERT(false);
		return allneurons[0]->bias; // never get here!
	}

	void setData(int dimensionIndex, double val)
	{
		// bias and incomingweights
		neuron* allneurons[] = { inputs[0], hiddens[0], hiddens[1], hiddens[2], outputs[0] };
		int count = 0;
		for (int i = 0; i<5; i++)
		{
			if (count == dimensionIndex)
			{
				allneurons[i]->bias = val;
				return;
			}
			count++;
			for (int k = 0; k<(int)allneurons[i]->incomingweights.size(); k++)
			{
				if (count == dimensionIndex)
				{
					allneurons[i]->incomingweights[k] = val;
					return;
				}
				count++;
			}
		}
		DBG_ASSERT(false);
		//return allneurons[0]->bias; // never get here!
	}

};


void main() {
	srand((unsigned int)time(NULL));

	double F  = 0.1;  //differential weight [0,2]
	double CR = 0.5f; //crossover probability [0,1]

	for (int test = 0; test < 10; test++)
	{
		F  = random(0.001, 1.9); //differential weight [0,2]
		CR = random(0.001, 0.9); //crossover probability [0,1]

		vector<network*> population;// = new network*[POPULATIONSIZE];// = new network[ POPULATIONSIZE ];
		for (int i = 0; i < POPULATIONSIZE; i++)
		{
			population.push_back(new network());
		}


		vector<pair<double, double>> trainSet;	trainSet.resize(TrainSetSize);
		for (int i = 0; i < TrainSetSize; i++) { trainSet[i] = make_pair(i / (double)TrainSetSize, sin((i / (double)TrainSetSize) * (2.0 * PI))*0.5 + 0.5); }


		//dimensionality of problem, means how many variables problem has. this case 3 (data1,data2,data3)
		const int N = population[0]->getDimension(); // 5 neurons and each neuron has connected weights and bias (should be 11 for this case)


		for (int j = 0; j < POPULATIONSIZE; j++)
		{
			for (int ts = 0; ts < TrainSetSize; ts++)
			{
				const double input = trainSet[ts].first;
				const double output = trainSet[ts].second;
				population[j]->activate(&input, &output);
			}
		}

		for (int e = 0; e < epoch; e++)
		{

			//F = random(0.001, 0.5);
			//CR = random(0.01, 0.6);

			//main loop of evolution.
			for (int j = 0; j < POPULATIONSIZE; j++)
			{


				//calculate new candidate solution

				//pick random point from population
				int x = ::floor(random(0, POPULATIONSIZE - 1));

				int a, b, c;	//pick three different random points from population
				do { a = ::floor(random(0, POPULATIONSIZE - 1)); } while (a == x);
				do { b = ::floor(random(0, POPULATIONSIZE - 1)); } while (b == x || b == a);
				do { c = ::floor(random(0, POPULATIONSIZE - 1)); } while (c == x || c == a || c == b);

				// Pick a random index [0-Dimensionality]
				int R = ::floor(random(0, N));
				//Compute the agent's new position

				network* original = population[x];
				network* candidate = new network();// = original; 

				network* individual1 = population[a];
				network* individual2 = population[b];
				network* individual3 = population[c];

				//if(i==R | i<CR)
				//candidate=a+f*(b-c)
				//else
				//candidate=x

				for (int s = 0; s < N; ++s)
				{
					//if (s == R || random(0, 1)<CR)
					if (s == R || random(0, 1) < CR)
					{
						//candidate->getData(s) = individual1->getData(s) + F*(individual2->getData(s) - individual3->getData(s));
						double val = individual1->getData(s) + F*(individual2->getData(s) - individual3->getData(s));
						//val = val * 0.5;
						candidate->setData(s, val);
						//candidate->getData(s) *= 0.5;

						//double val = candidate->getData(s);
						//DBG_ASSERT(val > -1000 && val < 1000);
					}
					else
					{
						//candidate->getData(s) = original->getData(s);
						double val = original->getData(s);
						candidate->setData(s, val);
					}
				}// End for b

				 // calculate fitness for our new candidate
				for (int ts = 0; ts < TrainSetSize; ts++)
				{
					const double input = trainSet[ts].first;
					const double output = trainSet[ts].second;
					candidate->activate(&input, &output);
				}

				double candidateFitness = candidate->GetFitness();
				double originalFitness = original->GetFitness();

				// if better replace
				if (candidateFitness < originalFitness)
				{
					population[x] = candidate;
					delete original;
				}
				else
				{
					delete candidate;
				}

			}// end population

#if 1 // print out the fitness as it evolves
			{
				int    bestFitnessIndex = 0;
				double bestFitnessValue = population[bestFitnessIndex]->GetFitness();

				for (int i = 1; i < POPULATIONSIZE; ++i)
				{
					if (population[i]->GetFitness() < bestFitnessValue)
					{
						bestFitnessValue = population[i]->GetFitness();
						bestFitnessIndex = i;
					}
				}// End for i
				cout << "epoch:" << e << " fitness:" << bestFitnessValue << "\r";
			}
#endif

		}// epoch



		int    bestFitnessIndex = 0;
		double bestFitnessValue = population[bestFitnessIndex]->GetFitness();

		for (int i = 1; i < POPULATIONSIZE; ++i)
		{
			if (population[i]->GetFitness() < bestFitnessValue)
			{
				bestFitnessValue = population[i]->GetFitness();
				bestFitnessIndex = i;
			}
		}// End for i
		DBG_ASSERT(bestFitnessIndex > -1);



#if 0
		cout << "input, idea, predicted (y =sin(x))" << "\n";
		for (int i = 0; i < 100; i++) { //Print out test results (ideal vs predicted for sin wave)
			double input = i / 100.0;
			double predicted = population[bestFitnessIndex].activate(&input, NULL);
			std::cout << (input) << ", " << sin(input * (2.0 * PI))*0.5 + 0.5 << ", " << predicted << "\n";
	} system("pause"); // wait for user to press a key before closing
#endif

#if 1
	char filename[256] = { 0 };
	sprintf(filename, "data_F=%.5f_CR=%.5f.txt", F, CR);
	FILE* fp = fopen(filename, "wt");
	fprintf(fp, "epoch %d, populationsize: %d\n", epoch, POPULATIONSIZE);
	fprintf(fp, "error %f\n", bestFitnessValue);
	fprintf(fp, "input, idea, predicted (y =sin(x))\n");
	for (int i = 0; i < 100; i++) { //Print out test results (ideal vs predicted for sin wave)
		double input = i / 100.0;
		double predicted = population[bestFitnessIndex]->activate(&input, NULL);
		double exact = sin(input * (2.0 * PI))*0.5 + 0.5;
		fprintf(fp, "%f %f %f \n", input, exact, predicted);
	}
	fflush(fp);
	fclose(fp);
	fp = NULL;

#endif

	for (int i = 0; i < POPULATIONSIZE; i++)
	{
		delete population[i];
	}
	population.resize(0);

	}// end for test

	system("pause"); // wait for user to press a key before closing 
}





* Optionally saves the trained data to a file (compare the ideal against predicted)
* Runs through various DE parameters (CR/F) to see which provides better convergence on a result (most optimal)
* Prints out the epoch and the fitness (watch the algorithm improve as it searches for the neural network weights and biases)
* Compare the accuracy/time/computational cost with other training version (e.g., back propagation) - however, for problems that have very non-linear constraints or are based on fitness critera (no-data), these alternative training algorithms may be limited.




References:

1. Deep Learning with Javascript: Example-Based Approach (Kenwright) ISBN: 979-8660010767
2. Game C++ Programming: A Practical Introduction (Kenwright) ISBN: 979-8653531095
3. smallnn: Neural Network in 99 lines of C++ (Kenwright)
4. Inverse kinematic solutions for articulated characters using massively parallel architectures and differential evolutionary algorithms (Kenwright). VRIPHYS '17: Proceedings of the 13th Workshop on Virtual Reality Interactions and Physical Simulations, April 2017
5. Neural network in combination with a differential evolutionary training algorithm for addressing ambiguous articulated inverse kinematic problems (Kenwright). SA '18: SIGGRAPH Asia 2018 Technical BriefsDecember 2018





 

 
 Visitor: 9534626
{ 229.27.38.75 }
Copyright (c) 2002-2020 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.