Here is an attempt at implementing the simplest Neural Network, which is an algorithm for learning a binary classifier. In this specific case, it can decide whether an input, of a pair of Cartesian coordinates, belongs to some specific class or not.
main.cpp
#include <iostream>
#include <iomanip>
#include <fstream>
#include <string>
#include <vector>
#include <time.h>
#include "Perceptron.h"
int main()
{
Settings s(0.1, 100, "Perceptron_test1.txt", "Perceptron_statst1.txt");
Perceptron p(s);
std::cout <<"Done\n";
}
Perceptron.h
#ifndef PERCEPTRON_H
#define PERCEPTRON_H
struct Settings
{
Settings(double lr, int mi, const std::string& l, const std::string& s)
: learning_rate(lr), max_iterations(mi), load_file(l), save_file(s) { }
double learning_rate;
int max_iterations;
std::string load_file;
std::string save_file;
};
class Perceptron
{
private:
typedef int INT;
typedef double REAL;
struct Point
{
Point (REAL xx, REAL yy) : x(xx), y(yy) { }
REAL x;
REAL y;
};
struct Statistics
{
Statistics(REAL rmse, INT i) : RMSE(rmse), iteration(i) { }
REAL RMSE; /* Measure accuracy with Root Mean Sqaured Error. */
INT iteration; /* Epoch. */
};
public:
Perceptron();
Perceptron(const Settings& settings);
private:
REAL learning_rate;
INT max_iterations;
INT pattern_count;
std::vector<REAL> weights;
std::vector<Perceptron::Point> input;
std::vector<INT> desired_outputs; /* Supervised Learning. */
std::vector<Perceptron::Statistics> stats;
private:
REAL random_real();
void initialize_weights();
void load (std::istream& ifs);
INT calculate_output(const Perceptron::Point& p);
void learn();
void save (std::ostream& ofs);
};
#include "PerceptronDef.cpp"
#endif
PerceptronDef.cpp
Perceptron::Perceptron(const Settings& settings)
: learning_rate(settings.learning_rate), max_iterations(settings.max_iterations), pattern_count(0)
{
srand( unsigned int( time(NULL) ) );
initialize_weights();
std::ifstream ifs(settings.load_file.c_str());
if (!ifs) std::cerr <<"Can't open input file!\n";
load(ifs);
ifs.close();
learn();
std::ofstream ofs(settings.save_file.c_str());
if (!ofs) std::cerr <<"Can't open output file!\n";
save(ofs);
ofs.close();
}
Perceptron::REAL Perceptron::random_real() { return (REAL)rand() / (REAL)RAND_MAX; }
void Perceptron::initialize_weights()
{
for (size_t i = 0; i < 3; ++i) weights.push_back( random_real() );
}
void Perceptron::load (std::istream& ifs)
{
REAL xx, yy;
INT ou;
while (ifs >> xx >> yy >> ou)
{
input.emplace_back( Point(xx, yy) );
desired_outputs.emplace_back(ou);
pattern_count++;
}
}
/* Activation function: Heaviside step function. */
Perceptron::INT Perceptron::calculate_output(const Perceptron::Point& p)
{
REAL w_sum = p.x * weights[0] + p.y * weights[1] + weights[2]; /* \sum_{i=1}^n (w_i * x_i + bias). */
return (w_sum >= 0) ? 1 : -1; /* Threshold = 0. */
}
void Perceptron::learn()
{
REAL global_error;
INT iteration = 0;
do /* Start learning. */
{
iteration++;
global_error = 0;
for (INT p = 0; p < pattern_count; ++p)
{
INT output = calculate_output(input[p]); /* Calculate the output. */
REAL local_error = desired_outputs[p] - output; /* Update the weights. */
weights[0] += learning_rate * local_error * input[p].x;
weights[1] += learning_rate * local_error * input[p].y;
weights[2] += learning_rate * local_error;
global_error += (local_error * local_error);
}
stats.emplace_back( Statistics(sqrt(global_error / pattern_count), iteration) );
}
while (global_error != 0 && iteration <= max_iterations);
std::cout <<"Decision Equation: "<< weights[0] <<"x + "<< weights[1] <<"y + "<< weights[2] <<" = 0\n";
}
void Perceptron::save (std::ostream& ofs)
{
ofs <<"Decision-boundary line equation coefficients\n";
ofs << weights[0] <<" "<<weights[1] <<" "<<weights[2] <<"\n";
ofs <<"Error Iteration\n";
for (size_t i = 0; i < stats.size(); ++i)
{
ofs << std::setprecision(3) << stats[i].RMSE <<" "<< stats[i].iteration <<'\n';
}
}
The input data file is in the format:
x y 0 (or 1)
Here is an example of what I get:
-0.65x + 0.13y + 1.47 = 0
Done.
The above, together with the input data looks like:
The statistics (RMSE) from all the iterations are:
Questions:
- Any comments on the code structure and style are welcome.
- I'm looking for directions on how to generalize the code (to Nodes and Links, probably?), so that it can be used for Multilayer Perceptron.
- Does the result look remotely reasonable?
- What would be a good extension, what else to include in the class?