// - - - - - - - - - - - - - - - - - - - - - // File: neuralnetwork.cpp | // Purpose: implements class NeuralNetwork | // Author: Taivo Lints, Estonia | // Date: May, 2003 | // Copyright: see copyright.txt | // - - - - - - - - - - - - - - - - - - - - - #include "neuralnetwork.h" #include "neuron.h" #include <vector> #include <fstream> #include <string> #include <sstream> using namespace std; // - - - - - Class NeuralNetwork - - - - - // ****************************** // * Construction & Destruction * // ****************************** // - NeuralNetwork constructor - - // Creates a network from your configuration file. If file not found, // then you will get an empty network. You can check it by calling // get_number_of_layers(). It will be zero when network is empty. NeuralNetwork::NeuralNetwork(char* config_file) { load_config_file(config_file); create_unset_connections(); error_limit = 0.1; max_iterations = 20000; iterations = 0; } // - NeuralNetwork copy-constructor - - NeuralNetwork::NeuralNetwork(const NeuralNetwork& rN) { error_limit = rN.error_limit; max_iterations = rN.max_iterations; iterations = rN.iterations; v_int_layers = rN.v_int_layers; vPatterns = rN.vPatterns; NeuralNetwork& rN2 = const_cast<NeuralNetwork&>(rN); // A suspicious thing to do, but compiler will throw warnings // about invalid conversions in stl_container when I don't // cast away the const-ness. At the moment I don't have time // to trace the source of this problem. At least it SEEMS to // be a harmless problem... // Must NOT point to the neurons of the OTHER NeuralNetwork, // must create its own neurons. for(vector<Neuron*>::iterator i = rN2.vpNeurons.begin(); i != rN2.vpNeurons.end(); i++) { vpNeurons.push_back(new Neuron(NULL, false)); *(*(--vpNeurons.end())) = *(*i); (*(--vpNeurons.end()))->pvpNeurons = &vpNeurons; // This neuron now belongs to the vpNeurons // vector of our new NeuralNetwork. } } // - Operator= overloading - - NeuralNetwork& NeuralNetwork::operator=(const NeuralNetwork& rN) { if(&rN != this) { // Check for self-assignment. error_limit = rN.error_limit; max_iterations = rN.max_iterations; iterations = rN.iterations; v_int_layers = rN.v_int_layers; vPatterns = rN.vPatterns; NeuralNetwork& rN2 = const_cast<NeuralNetwork&>(rN); // A suspicious thing to do, but compiler will throw warnings // about invalid conversions in stl_container when I don't // cast away the const-ness. At the moment I don't have time // to trace the source of this problem. At least it SEEMS to // be a harmless problem... // Must NOT point to the neurons of the OTHER NeuralNetwork, // must create its own neurons. for(vector<Neuron*>::iterator i = rN2.vpNeurons.begin(); i != rN2.vpNeurons.end(); i++) { vpNeurons.push_back(new Neuron(NULL, false)); *(*(--vpNeurons.end())) = *(*i); (*(--vpNeurons.end()))->pvpNeurons = &vpNeurons; // This neuron now belongs to the vpNeurons // vector of our new NeuralNetwork. } } return *this; } // - NeuralNetwork destructor - - // Performs clean-up. NeuralNetwork::~NeuralNetwork() { // Deletes all Neurons (to prevent memory leak). for(vector<Neuron*>::iterator i = vpNeurons.begin(); i != vpNeurons.end(); i++) delete *i; } // ********************************************* // * Functions for getting general information * // ********************************************* // - Function: get_number_of_inputs() - - // Returns the number of input nodes. int NeuralNetwork::get_number_of_inputs() { if(get_number_of_layers() == 0) return(0); return(*v_int_layers.begin()); } // - Function: get_number_of_outputs() - - // Returns the number of output nodes. int NeuralNetwork::get_number_of_outputs() { if(get_number_of_layers() == 0) return(0); return(*(--v_int_layers.end())); } // - Function: get_number_of_layers() - - // Returns the number of layers, including the input layer (which is made of // buffer nodes, not real neurons). int NeuralNetwork::get_number_of_layers() { return(static_cast<int>(v_int_layers.size())); } // - Function: get_number_of_nodes() - - // Returns the total number of nodes. int NeuralNetwork::get_number_of_nodes() { return(static_cast<int>(vpNeurons.size())); } // ************************************************ // * Functions for setting some neuron parameters * // ************************************************ // - Function: set_treshold - - // Sets treshold value in all neurons. void NeuralNetwork::set_treshold(double treshold) { for(vector<Neuron*>::iterator i = vpNeurons.begin(); i != vpNeurons.end(); i++) (*i)->treshold = treshold; } // - Function: set_slope_parameter - - // Sets slope_parameter value in all neurons. void NeuralNetwork::set_slope_parameter(double slope_parameter) { for(vector<Neuron*>::iterator i = vpNeurons.begin(); i != vpNeurons.end(); i++) (*i)->slope_parameter = slope_parameter; } // ****************************************** // * Functions for basic use of the network * // ****************************************** // - Function: set_inputs - - // Sets network inputs. Returns false if size of given input vector is incorrect. bool NeuralNetwork::set_inputs(vector<double>* pv_dbl_inputvalues) { // How many inputs does our network have? int num_of_inputs = get_number_of_inputs(); // Check, if input vector size is correct or not. if(static_cast<int>(pv_dbl_inputvalues->size()) != num_of_inputs) return(false); // Assign input values to buffer neurons in first layer. for(int i = 0; i < num_of_inputs; i++) vpNeurons[i]->output = (*pv_dbl_inputvalues)[i]; return(true); } // - Function: update() - - // Updates the network (calls update() for all neurons). void NeuralNetwork::update() { // Update starts from the beginning of vpNeurons and goes linearly through // this vector. So when (imaginary) layers are correctly ordered in this // vector of neurons, then the information from inputs is propagated through // all the network during this "for" cycle. for(vector<Neuron*>::iterator i = vpNeurons.begin(); i != vpNeurons.end(); i++) (*i)->update(); } // - Function: get_outputs - - // Writes network outputs into given vector. If vector size is incorrect, // then will fix it. void NeuralNetwork::get_outputs(vector<double>* pv_outputs) { // How many outputs and neurons do we have? int num_of_outputs = get_number_of_outputs(), num_of_neurons = get_number_of_nodes(); // If given vector is of wrong size, then will fix it. if(static_cast<int>(pv_outputs->size()) != num_of_outputs) pv_outputs->resize(num_of_outputs); // What is the number of first output neuron in vpNeurons vector? int first_output_node = num_of_neurons - num_of_outputs; // Writing neuron output values into given vector. for(int i = first_output_node; i < num_of_neurons; i++) (*pv_outputs)[i - first_output_node] = vpNeurons[i]->output; } // ************************************************** // * Functions for setting some training parameters * // ************************************************** // - Function: set_learning_rate - - // Sets learning rate in all neurons. void NeuralNetwork::set_learning_rate(double learning_rate) { for(vector<Neuron*>::iterator i = vpNeurons.begin(); i != vpNeurons.end(); i++) (*i)->learning_rate = learning_rate; } // - Function: set_max_weight - - // Sets max_weight value in all neurons. void NeuralNetwork::set_max_weight(double max_weight) { for(vector<Neuron*>::iterator i = vpNeurons.begin(); i != vpNeurons.end(); i++) (*i)->max_weight = max_weight; } // - Function: add_training_pattern - - // Adds a training pattern to vPatterns. void NeuralNetwork::add_training_pattern(Pattern myPattern) { vPatterns.push_back(myPattern); } // - Function: verify_all_patterns - - // Verifies that the number of input and output values in // all patterns is correct. Returns "true" if all OK. bool NeuralNetwork::verify_all_patterns() { // How many inputs and outputs do we have? int num_of_inputs = get_number_of_inputs(), num_of_outputs = get_number_of_outputs(); // Verify all patterns for(vector<Pattern>::iterator i = vPatterns.begin(); i != vPatterns.end(); i++) if( (static_cast<int>((*i).vInputs.size()) != num_of_inputs) or (static_cast<int>((*i).vOutputs.size()) != num_of_outputs) ) return(false); return(true); } // - Function: erase_all_patterns - - // Deletes all training patterns. void NeuralNetwork::erase_all_patterns() { vPatterns.clear(); } // - Function: get_number_of_patterns - - // Returns the number of training patterns in vPatterns vector. int NeuralNetwork::get_number_of_patterns() { return(static_cast<int>(vPatterns.size())); } // ************************** // * Functions for training * // ************************** // - Function: train - - // Trains network using backpropagation algorithm. Returns 0, // if training successful; returns 1, if goal not reached; // returns 2, if problems with patterns. Also sets iterations // parameter (see "neuralnetwork.h" for more information). // If you don't want to use training file (a rare case, though), // then call train(NULL). int NeuralNetwork::train(char* training_file) { // If training file IS given, then load it. if(training_file != NULL) { erase_all_patterns(); // Deletes old data. if(load_training_file(training_file) > 0) // Reads new data from file. return(2); } // If wrong patterns, then exit function. if(not verify_all_patterns()) return(2); int i; // We want to preserve the value of "for" cycle iterator beyond // the scope of "for". // Repeats training until ready, or until max_iterations limit reached. for(i = 0; i < max_iterations; i++) { // First we have to make sure if training is necessary (maybe network // already behaves like we want?). bool goal_reached = true; // Goes through all patterns. for(vector<Pattern>::iterator iter = vPatterns.begin(); iter != vPatterns.end(); iter++) { set_inputs( &((*iter).vInputs) ); update(); vector<double> v_outputs; get_outputs(&v_outputs); // Checks, if outputs are inside or outside allowed limits. for(int output_number = 0; output_number < static_cast<int>(v_outputs.size()); output_number++) { double target_output = (*iter).vOutputs[output_number], output = v_outputs[output_number], error = abs(target_output - output); if(error > error_limit) { goal_reached = false; goto learn; // Using "goto" is generally considered as a poor // programming style. However, this is one of the // rare cases where using "goto" is not strongly // deprecated (i.e. jumping out of multiple loops, // alternatively done by writing several flag // testings and breaks. Of course, the situation // must still be analyzed carefully before using "goto" // to avoid breaking your code (e.g. nonlocal goto // may cause skipping some destructors -- a very bad // thing to do!)). } } } // If goal is reached, then exits this function. if(goal_reached) { iterations = i; return(0); } // If goal is NOT reached, then performs a training procedure. learn: // A label for goto. // What is the number of first output neuron in vpNeurons vector? int first_output_node = get_number_of_nodes() - get_number_of_outputs(); // Goes through all patterns. for(vector<Pattern>::iterator iter = vPatterns.begin(); iter != vPatterns.end(); iter++) { set_inputs( &((*iter).vInputs) ); update(); vector<double> v_outputs; get_outputs(&v_outputs); // Sets error value in output neurons. for(int output_number = 0; output_number < static_cast<int>(v_outputs.size()); output_number++) { double target_output = (*iter).vOutputs[output_number], output = v_outputs[output_number]; vpNeurons[first_output_node + output_number]-> error = target_output - output; } // Commands each neuron to learn (and to backpropagate the error). // Goes through vpNeurons in reverse direction because of the // BACKpropagation algorithm. for(vector<Neuron*>::reverse_iterator r = vpNeurons.rbegin(); r != vpNeurons.rend(); r++) (*r)->learn(); } } // Apparently the goal was not reached. iterations = i; return(1); } // ************************************ // * Functions for saving the network * // ************************************ // - Function: save_network - - // Saves your network into a file. You can use this generated file // for initializing a new network (instead of the usual configuration // file, where weights are not described). void NeuralNetwork::save_network(char* savefile) { // Creates an output file (if already exists, then overwrites it). ofstream out_file(savefile); // Sets some output parameters. out_file.setf(ios::fixed, ios::floatfield); out_file.precision(30); // Won't ever be so big, but it automatically cuts // down to the max. possible precision. I hope ;) // If no neurons, then nothing to save. if(get_number_of_nodes() == 0) { out_file << "Network is empty, nothing to save here." << endl; return; } // Saves input layer information. out_file << "input_layer: " << v_int_layers[0] << endl; // For counting nodes (to detect the end of current layer). int node_counter = 0; int layer_number = 0; // Goes through all neurons. for(int neuron_number = 0; neuron_number < get_number_of_nodes(); neuron_number++) { // Checking the end of a layer. if(node_counter == v_int_layers[layer_number]) { layer_number++; out_file << endl << "comp_layer: " << v_int_layers[layer_number] << endl; node_counter = 0; } if(layer_number > 0) { out_file << " weights:"; // Saves all weights of this neuron. for(int connection_number = 0; connection_number < static_cast<int>(vpNeurons[neuron_number]->vpConnections.size()); connection_number++) { double this_weight = vpNeurons[neuron_number]-> vpConnections[connection_number]->weight; // For keeping columns a bit more straight. if(abs(this_weight) < 10) out_file << " "; if(this_weight < 0) out_file << " " << this_weight; else out_file << " " << this_weight; } out_file << endl; } node_counter++; } // Saves some other parameters, too (assuming that all neurons // have same parameters!). out_file << endl; out_file << "treshold: " << get_treshold() << endl; out_file << "slope_parameter: " << get_slope_parameter() << endl; out_file << "max_weight: " << get_max_weight() << endl; } // ************************************************ // * Functions for getting some neuron parameters * // ************************************************ // All these functions assume, that all neurons have similar // parameters. So they just use values taken from the first // neuron in vpNeurons vector (which is actually an input node, // but still should have all these parameters set correctly). // If network is empty, then all these functions return 0. // - Function: get_treshold - - double NeuralNetwork::get_treshold() { if(get_number_of_nodes() == 0) return(0); return(vpNeurons[0]->treshold); } // - Function: get_slope_parameter - - double NeuralNetwork::get_slope_parameter() { if(get_number_of_nodes() == 0) return(0); return(vpNeurons[0]->slope_parameter); } // - Function: get_learning_rate - - double NeuralNetwork::get_learning_rate() { if(get_number_of_nodes() == 0) return(0); return(vpNeurons[0]->learning_rate); } // - Function: get_max_weight - - // Returns the value of parameter max_weight, NOT the maximum // weight found in network. double NeuralNetwork::get_max_weight() { if(get_number_of_nodes() == 0) return(0); return(vpNeurons[0]->max_weight); } // ***************** // * Private stuff * // ***************** // You can't use that stuff from outside code. // That's why the "density" of comments is lower here... // - Function: load_config_file - - // Loads info from configuration file, if this file exists. // It loads as much as it can without raising any alarms, // so make sure your configuration file is correct. void NeuralNetwork::load_config_file(char* config_file) { // Opens the configuration file ifstream inp_file(config_file); // If no file found, then exits this function. if(!inp_file) return; // Some variables. string inp_line_string; // For storing a line read from file int neuron_number = 0; // Neuron counter for weight adding. // Now we will go through the file, reading it line by line. while(getline(inp_file, inp_line_string)) { // Converts string to stringstream for easier use. stringstream inp_line_stream(inp_line_string); // Some variables. string param_label = ""; int num_of_neurons = 0; double inp_treshold, inp_slope_parameter; // Extracts the first non-white-space word. inp_line_stream >> param_label; // Creates input layer. if(param_label == "input_layer:") { inp_line_stream >> num_of_neurons; for(int i = 0; i < num_of_neurons; i++) // Adds neurons. vpNeurons.push_back(new Neuron(&vpNeurons, false)); v_int_layers.push_back(num_of_neurons); // Adds layer size. } // Creates computational layer. if(param_label == "comp_layer:") { inp_line_stream >> num_of_neurons; for(int i = 0; i < num_of_neurons; i++) // Adds neurons. vpNeurons.push_back(new Neuron(&vpNeurons, true)); if(num_of_neurons > 0) // Adds layer only if layer exists. v_int_layers.push_back(num_of_neurons); neuron_number = 0; // Resets neuron counter for weight adding. } // Adjusts weights, if they are given (no max_weight checking!) if(param_label == "weights:") { int size_this_lr = *(v_int_layers.end() - 1); if( (get_number_of_layers() < 2) or // Input layer can't have connections! (neuron_number >= size_this_lr) ) // Too much "weights:" are given! break; int size_prev_lr = *(v_int_layers.end() - 2), start_of_this_lr = get_number_of_nodes() - size_this_lr, start_of_prev_lr = start_of_this_lr - size_prev_lr, connections_added = 0; double weight; while( (inp_line_stream >> weight) and (connections_added < size_prev_lr) ) { vpNeurons[start_of_this_lr + neuron_number]-> add_connection(start_of_prev_lr + connections_added, weight); connections_added++; } neuron_number++; } // Sets treshold values. if(param_label == "treshold:") if(inp_line_stream >> inp_treshold) set_treshold(inp_treshold); // Sets slope_parameter values. if(param_label == "slope_parameter:") if(inp_line_stream >> inp_slope_parameter) set_slope_parameter(inp_slope_parameter); } } // - Function: create_unset_connections - - // Creates missing connections so that network will be a // fully connected multilayer network. void NeuralNetwork::create_unset_connections() { // If less than two layers then no connections to create. if(get_number_of_layers() < 2) return; // Initializes random number generator. srand(time(0)); // Pointer to the first neuron in first comp_layer. vector<Neuron*>::iterator i = vpNeurons.begin() + v_int_layers[0]; int current_layer = 1, size_prev_lr = v_int_layers[0], size_this_lr = v_int_layers[1], start_of_prev_lr = 0, start_of_this_lr = size_prev_lr, neuron_number = 0; // Will go through the vpNeurons one neuron at a time. while(i != vpNeurons.end()) { // First node from previous layer without a connection to this neuron. int source = start_of_prev_lr + static_cast<int>((*i)->vpConnections.size()); // Adds all missing connections. while(static_cast<int>((*i)->vpConnections.size()) < size_prev_lr) { // Finds a random weight in range -max_weight..+max_weight. double help_value = (1 - static_cast<double>(rand()) * 2 / RAND_MAX), rnd_weight = (*i)->max_weight * help_value; (*i)->add_connection(source, rnd_weight); source++; } neuron_number++; // Update the layer and neuron counting. if( (neuron_number == size_this_lr) and (current_layer < get_number_of_layers() - 1) ) { start_of_prev_lr += size_prev_lr; start_of_this_lr += size_this_lr; current_layer++; size_prev_lr = v_int_layers[current_layer - 1]; size_this_lr = v_int_layers[current_layer]; neuron_number = 0; } i++; } } // - Function: load_training_file - - // Loads info from training file. Returns the number of loading errors. // But be aware that zero errors does not guarantee that everything is // OK. Use verify_all_patterns(), too. int NeuralNetwork::load_training_file(char* training_file) { // Opens the training file. ifstream inp_file(training_file); // If no file found, then exits this function. if(!inp_file) return(1); // Some variables. int loading_errors = 0; // How many errors found in training file. string inp_line_string; // For storing a line read from file // Now we will go through the file, reading it line by line. while(getline(inp_file, inp_line_string)) { // Converts string to stringstream for easier use. stringstream inp_line_stream(inp_line_string); // Some variables. string param_label = ""; int inp_max_iterations; double inp_error_limit, inp_learning_rate, inp_max_weight; // Extracts the first non-white-space word. inp_line_stream >> param_label; // Creates a pattern. if(param_label == "pattern:") { Pattern ConstrPattern; bool inputs = true; // First numbers represent inputs. string word; // Goes through the line word by word. while(inp_line_stream >> word) { stringstream helper_stream(word), helper_stream_copy(word); double number; if(helper_stream >> number) { if(inputs) ConstrPattern.vInputs.push_back(number); else ConstrPattern.vOutputs.push_back(number); } else { helper_stream_copy >> word; if(word == "->") inputs = false; // After "->" will come outputs. else loading_errors++; } } add_training_pattern(ConstrPattern); } // Sets error_limit value. if(param_label == "error_limit:") if(inp_line_stream >> inp_error_limit) error_limit = inp_error_limit; // Sets max_iterations value. if(param_label == "max_iterations:") if(inp_line_stream >> inp_max_iterations) max_iterations = inp_max_iterations; // Sets learning_rate values. if(param_label == "learning_rate:") if(inp_line_stream >> inp_learning_rate) set_learning_rate(inp_learning_rate); // Sets max_weight values. if(param_label == "max_weight:") if(inp_line_stream >> inp_max_weight) set_max_weight(inp_max_weight); } return(loading_errors); }