After a few days of reading articles, watching videos and bugging my head around neural networks, I have finally managed to understand it just so I could write my own feed-forward implementation in C++.
It does have some scratch back-propagation functionality, but it needs further work (not done yet).
Here's my code, I would like you to point out any bad practices, tips, you know :)
main.cpp
#include "neural-net.hpp"
int main(int argc, char **argv)
{
srand(time(NULL));
/* Topology: x-y-z-...-n where a is the input layer and n is the output layer */
/* In this case: 1 input layer with 2 neurons, 1 hidden layer with 3 neurons and an output layer with 1 neuron */
std::vector<unsigned> vecTopology = { 2, 3, 1 };
NeuralNet net(vecTopology, false);
/* Set the input values and expected results for back-propagation (not finished!) */
std::vector<double> vecInputs(vecTopology[0], 1);
std::vector<double> vecExpected(3, 0);
std::cout << "Inputs: ";
for (int i = 0; i < vecInputs.size(); i++) {
std::cout << vecInputs[i] << " ";
}
std::cout << "\n\n";
net.feedForward(vecInputs);
net.backPropagate(vecExpected);
net.status();
}
neural-net.hpp
#ifndef NEURALNET_HPP
#define NEURALNET_HPP
#include "Neuron.hpp"
class NeuralNet
{
public:
NeuralNet(const std::vector<unsigned> &, bool = false);
void status();
void setWeight(unsigned, unsigned, unsigned, double);
void feedForward(const std::vector<double> &);
void backPropagate(const std::vector<double> &);
std::vector<double> getOutput();
private:
std::vector<Layer> vecLayers;
bool useBias;
};
#endif
neural-net.cpp
#include "neural-net.hpp"
NeuralNet::NeuralNet(const std::vector<unsigned> &vecTopology, bool useBias)
{
this->useBias = useBias;
/* 'Build' the network based on the topology */
for (unsigned l = 0; l < vecTopology.size(); l++) {
this->vecLayers.push_back(Layer());
unsigned nAxons = (l == vecTopology.size() - 1) ? 0 : vecTopology[l + 1];
for (unsigned n = 0; n < vecTopology[l] + (this->useBias) ? 1 : 0; n++) {
this->vecLayers[l].push_back(Neuron(n, nAxons));
}
/* We do not want the bias neuron in the output layer, pop it back :) */
if (this->useBias && l == vecTopology.size() - 1)
this->vecLayers[l].pop_back();
}
}
void NeuralNet::status()
{
for (unsigned l = 0; l < this->vecLayers.size(); l++) {
std::cout << "\nLayer " << l;
Layer &vecLayer = this->vecLayers[l];
for (unsigned n = 0; n < vecLayer.size(); n++) {
std::cout << "\n Neuron " << n << "\n";
std::vector<Axon> vecAxons = vecLayer[n].getAxons();
for (unsigned w = 0; w < vecAxons.size(); w++) {
std::cout << " Axon " << w << " weight: " << vecAxons[w].weight << "\n";
std::cout << " Axon " << w << " output: " << vecLayer[n].getOutput() << "\n\n";
}
if (l == this->vecLayers.size() - 1)
std::cout << " Output: " << vecLayer[n].getOutput() << "\n\n";
}
}
}
void NeuralNet::setWeight(unsigned layer, unsigned neuron, unsigned axon, double weight)
{
this->vecLayers[layer][neuron].setWeight(axon, weight);
}
void NeuralNet::feedForward(const std::vector<double> &vecInputs)
{
/* Set outputs of the input layer's neurons to the user's inputs */
for (unsigned n = 0; n < vecInputs.size(); n++) {
this->vecLayers[0][n].setOutput(vecInputs[n]);
}
/* Feed-forward! */
for (unsigned l = 1; l < this->vecLayers.size(); l++) {
Layer &vecLayer = this->vecLayers[l];
for (unsigned n = 0; n < vecLayer.size(); n++) {
vecLayer[n].feedForward(this->vecLayers[l - 1]);
}
}
}
void NeuralNet::backPropagate(const std::vector<double> &vecExpected)
{
/* This needs to be finished, as I am thinking of the best way to implement back-propagation */
Layer &vecLayer = this->vecLayers.back();
for (unsigned n = 0; n < vecLayer.size(); n++) {
vecLayer[n].backPropagate(this->vecLayers[this->vecLayers.size() - 2], vecExpected[n]);
}
}
std::vector<double> NeuralNet::getOutput()
{
/* I think this function is self-explanatory */
std::vector<double> vecOutputs;
Layer &vecLayer = this->vecLayers.back();
for (unsigned n = 0; n < vecLayer.size(); n++) {
vecOutputs.push_back(vecLayer[n].getOutput());
}
return vecOutputs;
}
neuron.hpp
#ifndef NEURON_HPP
#define NEURON_HPP
#include <iostream>
#include <vector>
#include <ctime>
#include <cmath>
#include <random>
class Neuron;
typedef std::vector<Neuron> Layer;
struct Axon
{
double weight;
double deltaWeight;
Axon(double weight = 0): weight(weight) {}
};
class Neuron
{
public:
Neuron(unsigned, unsigned);
std::vector<Axon> getAxons();
void setOutput(double);
double getOutput();
void setWeight(unsigned, double);
void feedForward(const Layer &);
void backPropagate(Layer &, double);
private:
unsigned index;
std::vector<Axon> vecAxons;
double output;
double outputSum;
double randomWeight();
double sigmoid(double);
double sigmoidDerivative(double);
};
#endif
neuron.cpp
#include "neuron.hpp"
Neuron::Neuron(unsigned index, unsigned nAxons)
{
this->index = index;
this->output = 0.0;
/* Axons are just connections between neurons, each given a random weight between 0.0 and 1.0 as a starter */
for (unsigned a = 0; a < nAxons; a++) {
this->vecAxons.push_back(Axon(randomWeight()));
}
}
double Neuron::randomWeight()
{
return rand() / double(RAND_MAX);
}
std::vector<Axon> Neuron::getAxons()
{
return this->vecAxons;
}
void Neuron::setOutput(double output)
{
this->output = output;
}
double Neuron::getOutput()
{
return this->output;
}
void Neuron::setWeight(unsigned axon, double weight)
{
this->vecAxons[axon].weight = weight;
}
void Neuron::feedForward(const Layer &vecPreviousLayer)
{
this->outputSum = 0.0;
/* Calculate the sum of inputs * weights going to the neuron and pass it through the transfer function... */
for (unsigned n = 0; n < vecPreviousLayer.size(); n++) {
this->outputSum += vecPreviousLayer[n].output * vecPreviousLayer[n].vecAxons[this->index].weight;
}
/* ... which is sigmoid in my case */
this->output = sigmoid(this->outputSum);
}
void Neuron::backPropagate(Layer &vecPreviousLayer, double expected)
{
/* This is NOT considered done in any way, or should I say it's working only for output <-> outer hidden layer */
double error = expected - this->output;
double deltaOutputSum = sigmoidDerivative(this->outputSum) * error;
std::cout << "Testing some back-propagation stuff, ignore next 2 lines\n";
std::cout << "Margin of error: " << error << "\n";
std::cout << "Delta output sum: " << deltaOutputSum << "\n\n";
for (unsigned n = 0; n < vecPreviousLayer.size(); n++) {
double output = vecPreviousLayer[n].output;
vecPreviousLayer[n].vecAxons[this->index].deltaWeight = deltaOutputSum * output;
}
}
double Neuron::sigmoid(double t)
{
return (1 / (1 + pow(exp(1.0), -t)));
}
double Neuron::sigmoidDerivative(double t)
{
return (pow(exp(1.0), t) / pow(1 + pow(exp(1.0), t), 2));
}