Reti neurali attraverso algoritmi genetici in C++. Parte V
12 December 2008 da Francesco
In questa penultima parte della serie di articoli dedicati alle reti neurali, vedremo come creare una classe che riunisca tutto cio’ che abbiamo programmato in precedenza e ne renda piu’ facile l’utilizzo. Era gia’ possibile utilizzare le classi cosi’ com’erano, ma in questo modo ci rendiamo la vita piu’ semplice. Inoltre c’e’ da notare che quello che faremo oramai non ho molto a che fare con l’implementazione della rete, ma e’ piu’ una questione di ordine e OOP.
Riunire il tutto.
In questo piccolo paragrafo vedremo come fare comunicare tra loro le due classi attraverso una terza ed ultima classe che si occupa della gestione di tutte le operazioni. In realtà questa classe è superflua perchè è già possibile, con le sole classi Population e NeuralNet, creare una rete neurale funzionante ma essa ci rende molto più comoda la scritture del codice. Essa contiene per lo più funzioni ausiliarie come quella per caricare il training set (senza dover parsare manualmente il file), o per salvare e caricare i pesi della rete neurale e per questo non mi soffermerò molto sul codice. La funzione più importante è quella che esegue l’allenamento della rete neurale, ma vediamo prima il codice della classe:
class NNController {
public:
NNController(int c_numInputNeurons, int c_numOutputNeurons,
int c_numHiddenLayers, int c_numNeuronsPerLayer);
void loadTrainingSet(string inputFile, string outputFile);
void train();
void saveWeights(string file);
void loadWeights(string file);
void test();
private:
vector< vector > inputData; // Training set
vector< vector > outputData;
NeuralNet* N;
Population* P;
Chromosome* C;
};
Il costruttore ha gli stessi parametri di quello della classe NeuralNet che servono ad inizializzare l’oggetto N all’interno di questa classe.
La funzione loadTrainingSet(string inputFile string outputFile) carica appunto il training set: essa riceve come argomenti i nomi di due file, il primo deve contenere gli input da passare alla rete neurale mentre il secondo gli output desiderati corrispondenti agli input. Per esempio, se stiamo addestrando la rete a fare la somma di due numeri, ed il file contenente l’input contenesse:
0 1
0 2
Quello contenente l’output dovrebbe contenere:
1
2
(Gli input devono essere separati tra di loro da uno spazio ed ogni riga deve contenere ovviamente sempre lo stesso numero di input che deve essere a sua volta uguale al numero dei neuroni di input specificato nel codice).
Le funzioni saveWeights(string file) e loadWeights(string file) servono rispettivamente a salvare i pesi della rete neurale (generalmente dopo che questa è allenata) e a poterli caricare in seguito senza dover ripetere l’addestramento.
La funzione test() serve invece a poter testare la rete con vari input dopo che questa è stata allenata. Ho conservato volutamente alla fine la funzione train() perchè è l’unica un po più complessa e serve ad effettuare l’allenamento della rete.
void NNController::train() {
double currentErr = 0; int i = 0; double bestFit = 99999999;<
while (bestFit > MAX_ERROR) {
if (i >= MAX_EPOCHS) {
break;
}
for (int j = 0; j < POPULATION_SIZE; j++) {
currentErr = 0;
for (int k = 0; k < inputData.size(); k++) {
N->run(inputData[k], (*P)[j]);
currentErr += N->globalError(outputData[k]) / inputData.size()
}
(*P)[j]->setFitness(currentErr);
}
bestFit = P->best()->getFitness();
if (i % UPDATE == 0) {
cout << "(" << i << ") - "<< "Best = " << bestFit
<< "\t" << "Worst = " << P->worst()->getFitness()
<< endl;
}
P->next();
i++;
}
}
La funzione esegue un ciclo while che termina o quando il fitness raggiunge un livello accettabile (cioè l’errore raggiunto è minore o uguale all’errore massimo stabilito in precedenza e definito nella costante MAX_ERROR) oppure quando (vedete il break) il numero di epoche (cioè di cicli di riproduzione della popolazione) supera un certo limite. Dentro questo ciclo while c’è un ulteriore ciclo for che scorre tutti gli elementi della popolazione (che, come oramai dovrebbe essere chiaro, rappresenta una configurazione dei pesi della rete neurale) e per ognuno esegue tutto il training set in input. Il fitness di questo individuo sarà l’errore medio che il cromosoma ha sugli elementi del training set. Un errore pari a 0, che corrisponde alla perfezione assoluta, è praticamente impossibile da raggiungere e quindi, a seconda dei casi, ci si può accontentare di un errore più o meno alto come, per esempio, 0.001.
La funzione stampa anche un output ogni tanto per informare l’utente a che punto è arrivato l’allenamento, e dipende dal valore della costante UPDATE: se per esempio questa vale 50, ogni 50 generazioni sarà stampato un messaggio contente i fitness del migliore e del peggiore cromosoma della popolazione corrente.
Quando questa funzione termina la nostra rete neurale è allenata (con un errore massimo stabilito dalla costante MAX_ERROR) ed è possibile testarla tramite il metodo test() fornito dalla stessa classe.
Vediamo ora un codice di esempio di rete neurale realizzata con le nostre classi:
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <ctime>
#include <cmath>
using namespace std;
/* Misc.hpp è un header che contiene le varie costanti come
MAX_POPULATION, ELITISM, MAX_TOUR, etc...
Gli altri header contengono le varie classi */
#include "Misc.hpp"
#include "GenAlg.hpp"
#include "NeuralNet.hpp"
#include "NNController.hpp"
int main() {
/* Alla riga seguente creiamo una rete neurale con 2 neuroni
di input, 1 neurone di output e 0 layer nascosti */
NNController NC(2, 1, 0, 0);
/* Carica il training set contenuto nei due file */
NC.loadTrainingSet("input", "output");
NC.train();
cout << "*** La rete e' allenata ***" << endl;
NC.test();
}
Come vedete il codice è davvero basilare eppure una rete neurale come quella di questo esempio, sarebbe in grado di imparare a fare la somma di due numeri con soli tre neuroni!
Come impostare una rete neurale.
Abbiamo visto la creazione pratica di una rete neurale ma come avete notato il risultato ottenuto da una rete dipende da una miriade di fattori come per esempio il numero di livelli, il numero di neuroni presente nei livelli nascosti, la funzione di trasferimento, etc… di conseguenza vi starete probabilmente chiedendo quali sono le impostazioni migliori per una rete neurale. Chiaramente non esistono delle impostazioni migliori in assoluto, ma dipende tutto dal problema che dobbiamo analizzare, di conseguenza quelle che scriverò qui saranno solo delle brevi linee guida da tenere in considerazione ma non delle regole fisse.
Ho gia parlato in precedenza delle funzioni di attivazione e di quali sia più comodo usare a seconda dei casi, ora parleremo del numero dei livelli.
Quanti livelli nascosti vanno usati? Nell’esempio precedente abbiamo creato una rete capace di imparare a fare la somma dei due numeri senza utilizzare nessun livello nascosto e in molti casi può verificarsi questa eventualità. Generalmente, nei casi restanti, un solo livello nascosto è più che sufficiente per ottenere risultati corretti. Dal punto di vista matematico possiamo dire che una rete neurale con i soli livelli si input e outputè capace di risolvere problemi linearmente separabili, mentre per problemi non linearmente separabili sono richiesti uno o più livelli nascosti.
Il numero di neuroni per ogni livello nascosto può essere un’altra incognita nella creazione della rete neurale: esso dipende da più fattori come il numero dei neuroni di input e di output, la dimensione del training set, la funzione di attivazione, l’algoritmo di apprendimento, etc… Esistono diverse regole approssimative che cercano di fornire un modo per calcolare il numero di unità nascoste necessarie (per esempio: “il numero di unità nascoste deve essere sempre minore del doppio dei neuroni di input”) ma molto spesso esse sono inutili. La cosa migliore è fare alcune prove e trovare il numero di unità che fornisce un errore minimo.
Tags: Algoritmi genetici, C++, Intelligenza artificiale, Reti neurali
Pubblicato in Informatica | Commenti (0)
No comments yet
