Vuoi reagire a questo messaggio? Crea un account in pochi click o accedi per continuare.

Reti neurali artificiali

Andare in basso

Reti neurali artificiali Empty Reti neurali artificiali

Messaggio  TTDH Mer Dic 02, 2009 11:42 pm

Introduzione ai sistemi fuzzy e alle reti neurali


Dati due vettori di grandezze X = x1,...,xn e V = v1,...,vn, tale che ogni elemento vi è dipendente funzionalmente dal corrispondente elemento x_i forall i, si dice media pesata il prodotto scalare di X per V:
= sum_{i=1}^{n} x_i v_i
Sistemi fuzzy

I sistemi fuzzy sono sistemi che si ispirano alla logica fuzzy, una logica polivalente che si può considerare un ampliamento della logica booleana classica, che prende in esame non solo un numero discreto possibile di valori, come lo 0 e 1 nell'algebra di Boole, ma anche possibili valori “intermedi” non numerabili. La logica fuzzy si pone quindi come valida alternativa alla logica tradizionale nell'esame dei problemi reali, in cui i valori che possono assumere le variabili in gioco non sono numerabili, o almeno non facilmente numerabili.
Introduzione alle reti neurali

Le reti neurali sono un'applicazione dell'intelligenza artificiale relativamente vecchia (sono state teorizzate negli anni '60, per poi essere abbandonate sui primi anni '70 in seguito alla pubblicazione, da parte di M.Minsky e S.Papert, di Perceptrons, un libro che metteva in risalto le carenze della tecnologia), ma che ultimamente sta vivendo un periodo di rinascita in seguito al rinnovato interesse nei confronti delle tecniche di intelligenza artificiale e al perfezionamento di questa tecnologia stessa.

Il meccanismo delle reti neurali si ispira dichiaratamente a quello del sistema nervoso degli animali. Le reti neurali non sono progettate per “nascere imparate”, né tantomeno per avere una grande precisione in fatto di calcolo (d'altronde la potenza di calcolo di un cervello umano verrebbe facilmente ridicolizzata da qualsiasi calcolatrice), ma sono progettate per apprendere. La fase di apprendimento di una rete neurale si basa su un campione di dati (“training set”) che viene presentato alla rete stessa, spesso con i risultati che si desiderano ottenere. Ad esempio, se voglio addestrare una rete neurale a risolvere le 4 operazioni fondamentali, posso presentare in input alla rete diversi numeri, e poi i risultati che desidero ottenere con quei numeri. Sarà la rete stessa a imparare, graduatamente, i meccanismi che sono alla base dell'operazione che deve compiere. Ovviamente, più corposo sarà il training set della rete, più precisi saranno i risultati che si potranno ottenere una volta completato l'addestramento.

Le reti neurali, come accennavo prima, non sono molto usate nel campo del calcolo matematico-scientifico, proprio in base alla loro scarsa precisione da sistemi fuzzy, ma si rivelano utilissime (proprio in virtù delle loro caratteristiche fuzzy) nella risoluzione di problemi reali. Un cervello umano non saprà risolvere un integrale definito con il metodo dei rettangoli con la stessa rapidità con cui lo risolverebbe un calcolatore elettronico, ma può riconoscere con una facilità disarmante un cane da un albero, o la voce di un amico da lontano, anche se disturbata da altri rumori. Delle applicazioni simili in campo tecnologico le hanno anche le reti neurali, utili, ad esempio, per il riconoscimento visivo elettronico, per il riconoscimento vocale, e così via.
Struttura di una rete neurale

La struttura di una rete neurale si rifà esplicitamente a quella di una rete neurale umana. Nell'uomo i neuroni sono costituiti da un corpo cellulare (soma) e da dentriti che mettono il neurone in comunicazione con altri neuroni. In presenza di determinati segnali queste comunicazioni si attivano (sinapsi), con il rilascio di sostanze di tipo chimico-ormonale (neurotrasmettitori) che trasmettono lo stimolo da un neurone all'altro. L'input di un neurone non è altro che la media pesata di tutti i segnali provenienti in input dagli altri neuroni per il “peso sinattico” della sinapsi in questione, ovvero l'”importanza” che riveste quella sinapsi nel collegamento. In base a questo valore, chiamato potenziale post-sinattico, il neurone può rispondere con una valore di output, che può essere minore o maggiore in fatto di intensità rispetto al precedente (effetto “calmante” o “eccitante”) in base ai valori degli input in quel dato momento.

Un neurone artificiale ha una struttura simile:
Reti neurali artificiali 1250439385

Dove x1,...,xn sono gli input presentati al neurone o alla rete neurale, w1,...,wn sono i pesi sinattici delle singole connessioni (ovvero quanto quella connessione influenza il risultato finale). La media pesata degli input per i pesi sinattici delle singole connessioni fornisce il potenziale post-sinattico del neurone:
P=sum_{i=1}^{n} w_i x_i
ovvero
P = (w1 * x1) + (w2 * x2) + ecc...

L'output y del neurone è dato da f (P-θ), dove θ è una soglia caratteristica del neurone, mentre f è una funzione di trasferimento. Le principali funzioni di trasferimento utilizzate nelle reti neurali sono la funzione a gradino, la funzione sigmoidale e la tangente iperbolica, tutte funzioni aventi codominio nell'intervallo [0,1] (o [-1,1] nel caso della tangente iperbolica).

La funzione a gradino, il tipo di funzione di trasferimento più semplice usata nelle reti neurali, è una funzione così definita:
Grafico della funzione a gradino
Grafico della funzione a gradino
Reti neurali artificiali 1250439578

Usando questa funzione, il neurone emette un segnale y=1 quando x = (P-θ) ≥ 0, quindi P≥θ, mentre emette un segnale y=0 (quindi rimane inattivo) quando P<θ.

Un'altra caratteristica funzione di trasferimento è la sigmoidale, o curva logistica, di equazione


f(x) = {1 over{1+e^{Ax}}}


Al variare del parametro A la curva può diventare più o meno “ripida”. In particolare, la curva tende alla funzione a gradino g(x) che abbiamo visto prima per A→-∞, mentre invece tende g(-x) quando A→+∞.
Reti neurali artificiali 1250439713
Reti neurali artificiali 1250439792

Se x=0, ovvero se P=θ, allora il valore di uscita del neurone artificiale sarà 0.5, mentre invece sarà approssimativamente 0 (ovvero il neurone è “spento”) per θ≫P e 1 per θ≪P. Una proprietà molto interessante di questa funzione, una proprietà molto utilizzata nella fase di apprendimento delle reti neurali, riguarda sua sua derivata prima. In particolare
{dy over dx} = Ay(1-y)

Questa proprietà implica che la derivata della funzione sigmoidale si può scrivere come un semplice prodotto, sorvolando le regole di derivazione, e questo è molto utile a fine computazionale (un calcolatore potrà trovare facilmente la derivata di una funzione così costruita).

Una funzione alternativa alla sigmoidale, relativamente meno usata nel campo delle reti neurali, è la tangente iperbolica, di equazione


f(x) = {{e^x - e^{-x}} over {e^x + e^{-x}}}


Una funzione definita f : Re rightarrow [-1,1], a differenza delle due che abbiamo visto prima che sono a codominio in [0,1].

Ogni neurone può dare in un dato momento, come abbiamo visto, un solo valore in output in funzione dei suoi input, mentre una rete neurale può complessivamente dare un numero variabile di valori in output. Se quindi una rete ha input n valori x1,x2,...,xn in un dato momento, la rete darà in output m valori y1,y2,...,ym in quel momento, in funzione delle xi. Ovvero
y_j = f_j (x_1,x_2,...,x_n) mbox{ per } 1 leq j leq m

Quindi i valori in input in un certo momento sono un vettore X di n componenti, generalmente compresi tra 0 e 1. Anche gli m valori del vettore di output Y sono compresi tra 0 e 1, in quanto vengono confinati in questo intervallo dalla funzione di trasferimento usata (funzione a gradino o sigmoidale), quindi il vettore X va a identificare un punto A all'interno di un ipercubo booleano di n dimensioni, e Y un punto B all'interno di un'altro ipercubo booleano a m dimensioni. Nel caso di n=m=2 gli ipercubi degenerano in 2 quadrati di 2n = 2m = 4 vertici, mentre invece nel caso n=m=3 gli ipercubi degenerano in 2 cubi di 2n = 2m = 8 vertici. La rete neurale può quindi essere vista come un'applicazione binaria che associa a ogni punto A contenuto nel primo ipercubo un punto B contenuto nel secondo. La grande idea alla base delle reti neurali però non è solo l'applicazione binaria tra i punti di un insieme e i punti di un altro insieme. L'applicazione associa il punto A e anche un suo intorno ad un intorno del punto B del secondo insieme. Questo è molto utile nel caso in cui i segnali di input sono “sporcati”, ad esempio nel caso di una rete neurale per il riconoscimento vocale, in grado di fare il suo dovere anche quando il suono è sporcato da rumori esterni, oppure una rete neurale per il riconoscimento calligrafico, in grado di fare il suo dovere anche quando il simbolo grafico non è perfettamente identico a quello appreso in fase di training. Questa proprietà deriva proprio dalle proprietà tipicamente fuzzy delle reti neurali.
Tecniche di apprendimento

Le reti neurali possono apprendere in due modi diversi: in modo supervisionato e in modo non supervisionato.

Nel primo caso, in ogni istante t vengono presentati alla rete dei campioni xi in input, e i corrispondenti valori di output dj desiderati. Le variazioni dei pesi sinattici
Δw = w(t + 1) − w(t)

sono una funzione dell'errore, e quindi dello scarto yj − dj, dove yj è l'output ottenuto, e dj l'output desiderato. Gli algoritmi di apprendimento in genere hanno come obiettivo quello di minimizzare l'errore quadratico medio. Quindi un'apprendimento supervisionato richiede la conoscenza sia dei valori di input xi, sia dei valori desiderati dj. Questi due tipi di dato forninscono quello che viene definito il training set della rete neurale.

Nel caso dell'apprendimento non supervisionato, vengono forniti alla rete molti campioni di input Xi = (xi1,...,xin), da associare a un numero m di classi C1,...,Cm. Il programmatore non fornisce alla rete la classe di appartenenza di ogni vettore di input; è la rete stessa ad auto-organizzarsi, modificando i suoi pesi sinattici in modo da poter eseguire classificazioni corrette. Gli algoritmi di apprendimento hebbiani sono classificabili all'interno di questa categoria. Questi algoritmi, basati sulla legge di Hebb, rafforzano il peso sinattico wij tra due generici neuroni i, j quando la loro attività è concorde (ovvero quando i risultati delle loro funzioni di trasferimento yi,yj sono di segno concorde), mentre lo indebolisce nel caso opposto, esattamente come accade tra i neuroni del sistema nervoso umano:
Delta w_{ij} = eta y_i y_j mbox{ con } 0 leq eta leq 1

Dove η è un coefficiente compreso tra 0 e 1 da cui dipende la variazione del peso sinattico.
Sviluppo di una rete neurale

Vediamo ora come implementare un'elementare rete neurale sfruttando algoritmi in C. La rete neurale che intendiamo sviluppare è relativamente semplice, ed è addestrata per svolgere la somma algebrica tra due numeri. Anche la sua funzione di attivazione è semplice (per questo esempio non useremo né la funzione a gradino né la sigmoidale viste precedentemente, nonostante siano queste le funzioni più utilizzate), useremo la funzione identità y=f(x)=x (ma potremmo usare una funzione qualsiasi, anzi ve lo lascio come esercizio).

Cominciamo a definire, magari in un file header, i tipi di dato di cui abbiamo bisogno:
Codice:
typedef struct TypeNeuron neuron;
typedef struct TypeSynapsis sinapsi;
typedef struct TypeLayer layer;
typedef struct TypeNN neuralnet;
Ovvero i tipi di dati strutturati per i neurodi, le sinapsi, i layer e la rete neurale. Definiamole in questo modo:
Codice:
#define   _PRECISION   float

struct TypeNeuron {
    _PRECISION trans_value;
    _PRECISION prop_value;
    sinapsi* in_links[16];
    int num_in_links;
    sinapsi* out_links[16];
    int num_out_links;
    _PRECISION (*trans_func)(_PRECISION prop_value);
};
Come valore di precisione della rete ho deciso di usare float (precisione semplice a virgola mobile), ma nulla ci impedisce di usare int, long int o double. Le variabili prop_value e trans_value non identificano altro che, rispettivamente, il valore di propagazione xi del neurone e il suo valore di trasferimento, ovvero il valore prodotto in output dalla funzione di trasferimento. Per il resto, dichiaro 16 collegamenti in entrata e 16 in uscita (ovvero 16 sinapsi che collegano il neurone ad altri 16 neuroni in ingresso e altre 16 che collegano il neurone a 16 neuroni in uscita), e tengo il conto delle sinapsi rispettivamente nelle variabili num_in_links e num_out_links. Infine dichiaro un puntatore a funzione (*trans_func), in modo da poter scegliere successivamente che funzione di trasferimento usare per quello specifico neurone.
Codice:
struct TypeSynapsis {
    _PRECISION delta;   
    _PRECISION weight;
    neuron *in,*out;
};
Qui dichiaro il tipo sinapsi, caratterizzato da un puntatore al neurone di ingresso e uno a quello di uscita (in e out), un suo peso sinattico weight (corrispondente al wi che abbiamo considerato finora nelle formule) e una sua delta, corrispondente alla variazione dei pesi sinattici in fase di apprendimento della legge di Hebb.
Codice:
struct TypeLayer {
    neuron** elements;
    int num_elements;

    void (*update_weights)(layer* lPtr);
};
Un layer consiste in un insieme di neuroni, e conterrà quindi un puntatore alla lista di neuroni che ne fanno parte (elements), il loro numero (num_elements) e un puntatore a una funzione per aggiornare i pesi sinattici nella fase di apprendimento (update_weights). Una rete neurale semplice è generalmente composta di 3 layer:

* un layer di input che prende in ingresso i dati
* un layer di output che fornisce i dati elaborati
* uno o più layer nascosti (nel nostro caso ne basta uno) che connettono i due layer di input e output. Sono deputati alla fase di elaborazione dei dati
Codice:
struct TypeNN {
    int max_epochs;
    _PRECISION l_rate;

    layer*      input_layer;
    layer*      hidden_layer;
    layer*      output_layer;
};
Questa struttura identifica la rete nel suo complesso, con i puntatori ai 3 layer che la compongono e due parametri che useremo in fase di apprendimento. In particolare, max_epochs è il numero massimo di epoche, ovvero di cicli di aggiornamento dei pesi sinattici, che la rete può effettuare, mentre l_rate è il learning rate della rete (corrispondente all'η che abbiamo visto negli algoritmi di apprendimento).

Veniamo ora alle funzioni per inizializzare gli elementi della nostra rete:
Codice:
void init_net(neuralnet *net)  {
net = (neuralnet*) malloc(sizeof(neuralnet));
max_epochs=1024;  // Valore arbitrario
l_rate=0.5;  // Valore arbitrario
}

void new_layer(layer *l)  {
l = (layer*) malloc(sizeof(layer));
num_elements=0;
}

void new_neuron(neuron *n)  {
n = (neuron*) malloc(sizeof(neuron));
num_in_links=0;
num_out_links=0;
}
e ora una funzione per collegare tra di loro i layer:
Codice:
void link_layers(layer* layer_in,layer* layer_out){
    int i,j;
    sinapsi* aux_syn;
    neuron *curr_in,*curr_out;

    for(i=0;i < layer_in->num_elements;i++)  {
   curr_in = layer_in->elements[i];

   for(j=0;j < layer_out->num_elements; j++)  {
       curr_out = layer_out->elements[j];
       aux_syn = (sinapsi*)malloc(sizeof(sinapsi));
       aux_syn->in = curr_in;
       aux_syn->out = curr_out;
       aux_syn->weight = norm(get_rand());
       curr_in->out_links[curr_in->num_out_links++] = aux_syn
       curr_out->in_links[curr_out->num_in_links++] = aux_syn;
   }
    }
}
Questa funzione collega tra di loro due layer (layer_in e layer_out). Per fare ciò alloca memoria per ogni sinapsi tra ogni neurone di layer_in e ogni neurone di layer_out, attraverso due cicli for (il primo cicla su tutto il layer di input e il secondo su tutto il layer di output). Per ogni collegamento neurone-neurone viene creata una sinapsi, una sinapsi che, ovviamente, dovrà sapere che neuroni collegare, quindi:

aux_syn->in = curr_in;
aux_syn->out = curr_out;

e i neuroni stessi dovranno sapere che sinapsi utilizzare per collegarsi:

curr_in->out_links[curr_in->num_out_links++] = aux_syn
curr_out->in_links[curr_out->num_in_links++] = aux_syn;

Per inizializzare il peso della sinapsi ho utilizzato un valore casuale, così calcolato dalla funzione get_rand():
Codice:
float get_rand()  {
    float x,y;

    srand( (unsigned) time(NULL));
    x = (float) rand();
    y = (sin(x)*sin(x))-0.5;

    return y;
}
Quello che faccio è inizializzare il seme dei numeri casuali e assegnare alla variabile x un numero casuale. Per portare questo valore all'interno dell'intervallo [-0.5, 0.5] sfrutto uno stratagemma matematico. Il codominio della funzione seno è in [-1,1], quindi il codominio della funzione sin²x sarà ovviamente in [0,1]. Se sottraggo 0.5 al valore di questa funzione ottengo un valore compreso tra [-0.5, 0.5].

Passiamo ora agli input da fornire alla rete. In questo esempio, forniremo alla rete degli input tramite un file in cui sono salvati dei numeri separati da ';'. La nostra rete dovrà imparare ad effettuare la somma algebrica, quindi nel file i primi due numeri rappresentano le quantità da sommare, e il terzo numero il risultato desiderato. Esempio:

1;2;3;5;7;12;3;5;8;.....

Dapprima dichiariamo una funzione che apra il file in questione in modalità lettura:
Codice:
#define  TRAINING_FILE    “training.txt”

int open_training_file()  {
int fd;

if ((fd=open(TRAINING_FILE, O_RDONLY)) < 0)
return -1;
else
return fd;
}
Vediamo ora la funzione int get_data(float *data, int fd). Questa funzione prende come parametri un puntatore a float (in cui verrà salvato il numero letto) e un file descriptor (ottenuto dalla funzione open_training_file()), e ritorna -1 in caso di errore, altrimenti salva in *data il numero letto dal file fino al successivo ';'.
Codice:
int get_data(float *data, int fd)  {
int curr_char;
int status;
int is_dec;
char buf[1],ch;

// Attenzione: così come è dichiarata questa stringa può essere soggetta
// a buffer overflow. Imponete voi dei controlli ulteriori per evitarlo,
// controllando prima quanti caratteri ci sono nel file fino al prossimo
// ';' e dichiarando la stringa dinamicamente
char aux_str[256];

curr_char=0;

// Ciclo finché ci sono caratteri da leggere nel file
while( ( status = read(fd,buf,sizeof(buf)) ) != 0)  {
     // Se status < 0, c'è qualche errore
    if(status<0)if(_DEBUG)perror(strerror(errno));
    ch=buf[0];

     // Se il carattere letto è proprio un ';', esco dal ciclo
    if(ch == ';') break;

    // Altrimenti continuo. Gli a capo sono ininfluenti
    if(ch == 'n')continue;

     // Gli unici caratteri validi al fine della lettura sono . - e tutti
     // i valori numerici. Se il carattere letto non è uno di quelli,
     // ritorno errore
    if(ch != '.'  && ch != '-' && ( ch < 48 || ch > 57 )) {
   if(_DEBUG)fprintf(stderr,"invalid ch %dn",ch);
   data = NULL;   
   return -1;
    }

     // Controllo quanti punti ci sono nel numero
    if( ch == '.' ){
   // Se è già stato trovato un . allora c'è un errore
   if(is_dec){
       aux_str[curr_char++]=ch;
       aux_str[curr_char]='�';
       fprintf(stderr,"invalid format: two '.' found in %­sn",aux_str);
       return -1;
   }                                            

  // Altrimenti, il numero è decimale
   else
        is_dec=1;
    }

    // Salvo l'ulteriore carattere letto nella stringa aux_str
     aux_str[curr_char++] = ch;
}

// Termino la stringa
aux_str[curr_char]='�';

// Converto la stringa in float e salvo il valore in *data
*data = atof(aux_str);
return 0;
}
Il codice è già abbastanza commentato, quindi non mi dilungherò ulteriormente. A questo punto studiamo il modo in cui la rete processa l'output. Quando la rete legge i valori di input, i neuroni del layer di input cambiano di valore, e i nuovi valori che assumono sono quelli della coppia di numeri. A questo punto, l'informazione passerà ai neuroni del layer nascosto, che calcoleranno prima il potenziale post-sinattico quindi il valore di uscita della funzione di trasferimento che, nel nostro caso, è una semplice funzione del potenziale post-sinattico. In particolare, avendo scelto come funzione di trasferimento la funzione identità, avremo
y = f(x) = x

Per cominciare, facciamo leggere gli input al layer di ingresso:
Codice:
int fd;
int status;
float temp;

// Apro il file con gli input
fd=open_training_file();

// Ciclo su tutti gli elementi del layer di input
for(i=0; i < net->input_layer->num_elements; i++)  {
    // Se la funzione get_data ritorna un valore negativo, allora c'è qualcosa
    // che non va negli input
    if((status = get_data(&temp,fd)) < 0){
   fprintf(stderr,"Invalid input datan");
   free(net);
   return -1;
    }

    // Il valore del potenziale post-sinattico del neurone è quello appena
    // letto da input, e il valore di trasferimento sarà uguale in virtù della
    // scelta di funzione di trasferimento che abbiamo fatto
    net->input_layer->elements[i]->prop_value=(_PRECISION)temp;
    net->input_layer->elements[i]->trans_value=(_PRECISION)temp;   
}
Per quanto riguarda invece il layer nascosto
Codice:
void propagate_into_layer(layer* lPtr){
    int i;
    neuron* nPtr;

    // Ciclo for su tutti gli elementi del layer
    for(i=0;i < lPtr->num_elements;i++)  {
   nPtr = lPtr->elements[i];
   // Per ogni neurone calcolo il potenziale post-sinattico...
   nPtr->prop_value = potential(nPtr);
   // ...e la funzione di trasferimento
   nPtr->trans_value = nPtr->trans_func(nPtr->prop_value); 
    }
}
La funzione potential() ha questo codice:
Codice:
_PRECISION potential(neuron* nPtr){
    _PRECISION aux_value=0;
    int i=0;

    // Per ogni sinapsi in ingresso al neurone...
    for(i=0; i<nPtr->num_in_links; i++)  {
   // ...il valore del potenziale è la sommatoria del peso sinattico della
   // sinapsi in questione moltiplicato per il suo valore di trasferimento
   aux_value += (nPtr->in_links[i]->weight * nPtr->in_links[i]->in->trans_value);
    }
    return aux_value;
}
Per i neuroni appartenenti al layer di output, il discorso è esattamente lo stesso fatto con il layer nascosto, e il codice rimarrà perfettamente identico.

A questo punto, abbiamo già visto che è possibile rendere più preciso il valore di uscita della rete neurale agendo sui singoli pesi sinattici. Per la struttura che abbiamo dato al file di input, la rete legge dal file sia i valori da sommare sia il risultato esatto, quindi provvederemo a far leggere il risultato giusto alla rete con la funzione get_data(). Una volta che abbiamo sia il risultato desiderato, sia il risultato effettivo della rete, opereremo sui pesi sinattici della rete in questo modo:
Δwij = − ηDjxi

Dove η è una costante della rete compresa tra 0 e 1 chiamata learning rate, ed è definita a nostro piacimento (più il valore di η è alto, più la rete modificherà sensibilmente i suoi pesi sinattici in seguito a un errore). Un learning rate alto renderà più veloce l'apprendimento della rete a discapito della precisione, mentre invece un learning rate basso renderà l'apprendimento più lento, ma la rete guadagnerà in fatto di precisione (diciamo pure che un valore intorno a 0.5 rappresenta un buon compromesso). xi è il valore in input al neurone e Dj (delta di output) è così definita:

D_j = (y_j - d_j) {d over dx} f(P_j)

dove yj e dj sono rispettivamente il valore ottenuto in output e il valore desiderato, ed f(Pj) è la funzione di trasferimento calcolata nel potenziale post-sinattico Pj. Questa è la base dell'algoritmo di apprendimento Widrow-Hoff, un algoritmo di apprendimento supervisionato che calcola i pesi necessari partendo da pesi casuali, e apportando a questi delle modifiche progressive in modo da convergere alla soluzione finale.

Nel nostro caso, poiché

y = f (P) = P Rightarrow {d over dx} f(P) = 1

avremo semplicemente
Δwi = yi − di

La variazione verrà calcolata così:
Codice:
_PRECISION compute_output_delta(
_PRECISION output_prop_value, _PRECISION des_out)  {

    _PRECISION delta;

    delta =
(output_prop_value - des_out) * linear_derivate (output_prop_value);

    return delta;
}
dove des_out è il valore desiderato in output e linear_derivate() sarà, nel nostro caso, una funzione che ritornerà sempre 1 (ovviamente cambiando la funzione di trasferimento cambierà anche questa funzione).

Per aggiornare i pesi useremo l'equazione appena esaminata:
Codice:
void update_output_weights(layer* lPtr,_PRECISION delta,_PRECISION l_rate){
    int i,j;
    sinapsi* sPtr;
    neuron* nPtr;

    for(i=0; i<lPtr->num_elements; i++)  {
   nPtr = lPtr->elements[i];
   for(j=0;j < nPtr->num_in_links;j++){
       sPtr = nPtr->in_links[j];
       // ∆wij =  – η Dj xi
       sPtr->delta = -(sPtr->in->trans_value*delta*l_rate);
   }
    }
}
Lo stesso algoritmo sarà valido anche per il layer nascosto. Ora, trovato l'incremento (o decremento) da applicare ai pesi sinattici, basterà ciclare su tutta la rete e applicare a tutte le sinapsi i nuovi pesi:

commit_weight_changes(net->output_layer);
commit_weight_changes(net->hidden_layer);

con
Codice:
void commit_weight_changes(layer* lPtr){
    int i,j;
    neuron* nPtr;
    sinapsi* sPtr;

    // Ciclo su tutti gli elementi del layer
    for(i=0; i < lPtr->num_elements; i++)  {
   nPtr = lPtr->elements[i];

   // Ciclo su tutte le sinapsi collegate ad un certo neurone
   for(j=0; j < nPtr->num_in_links; j++)  {
       // La sinapsi sarà associata al j-esimo collegamento del neurone
       sPtr = nPtr->in_links[j];

       // Il peso della sinapsi viene aggiornato con il delta
       // appena calcolato
       sPtr->weight += sPtr->delta;

       // Resetto il valore di delta, in modo da potergli applicare
       // nuove modifiche
       sPtr->delta = 0;
   }
    }
}
Come intuibile, maggiore sarà il numero di epoche (ovvero di iterazioni di questo tipo, in cui modifico il valore dei pesi per convergere sempre più al valore desiderato), maggiore sarà la precisione dei valori di output della rete. Il valore massimo di iterazioni ammissibile l'avevamo precedentemente stabilito all'interno della variabile max_epochs.

A questo punto, nel nostro main() inseriamo un ciclo che effettua automaticamente questo procedimento per max_epochs volte:
Codice:
// Ciclo per max_epochs volte
for(j=0; j<net->max_epochs; j++)  {
    // Leggo i valori in input dal file, con il procedimento già visto
    // in precedenza
    for(i=0; i<net->input_layer->num_elements; i++)  {
   if((status = get_data(&temp,fd)) < 0){
       fprintf(stderr,"errore irreversibile, closing...n");
       free(net);
       return -1;
   }
   net->input_layer->elements[i]->prop_value=(_PRECISION)temp;
   net->input_layer->elements[i]->trans_value=(_PRECISION)temp;      
    }

    // Passo i valori prima al layer nascosto, quindi al layer di output
    propagate_into_layer(net->hidden_layer);
    propagate_into_layer(net->output_layer);

    // Calcolo la delta di output
    if((status = get_data(&des_out,fd)) < 0){
   fprintf(stderr,"errore irreversibile, closing...n");
   return -1;
    } else {
   out_delta = compute_output_delta(net->output_layer->elements[0]->prop_value,des_out);
   update_output_weights(net->output_layer,out_delta,net->l_rate);
    }

    // Calcolo la variazione dei pesi sinattici per il layer nascosto
    // e aggiorno tutti i pesi sinattici
    update_hidden_weights(net->hidden_layer,out_delta,net->l_rate);
    commit_weight_changes(net->output_layer);
    commit_weight_changes(net->hidden_layer);
    net_output = net->output_layer->elements[0]->prop_value;
    printf("DES=%ftERROR=%ftOUT=%ftDELTA=%fn",des_out,
      (des_out-net_output),net_output,out_delta);
}
Ed ecco che la nostra rete neurale è pronta per l'uso.
Esempio di rete neurale con algoritmo di backtraking in python
Codice:
# Back-Propagation Neural Networks
#
# Written in Python.  See http://www.python.org/
# Placed in the public domain.
# Neil Schemenauer <nas@arctrix.com>

import math
import random
import string

random.seed(0)

# calculate a random number where:  a <= rand < b
def rand(a, b):
    return (b-a)*random.random() + a

# Make a matrix (we could use NumPy to speed this up)
def makeMatrix(I, J, fill=0.0):
    m = []
    for i in range(I):
   m.append([fill]*J)
    return m

# our sigmoid function, tanh is a little nicer than the standard 1/(1+e^-x)
def sigmoid(x):
    return math.tanh(x)

# derivative of our sigmoid function
def dsigmoid(y):
    return 1.0-y*y

class NN:
    def __init__(self, ni, nh, no):
   # number of input, hidden, and output nodes
   self.ni = ni + 1 # +1 for bias node
   self.nh = nh
   self.no = no

   # activations for nodes
   self.ai = [1.0]*self.ni
   self.ah = [1.0]*self.nh
   self.ao = [1.0]*self.no

   # create weights
   self.wi = makeMatrix(self.ni, self.nh)
   self.wo = makeMatrix(self.nh, self.no)
   # set them to random vaules
   for i in range(self.ni):
       for j in range(self.nh):
      self.wi[i][j] = rand(-2.0, 2.0)
   for j in range(self.nh):
       for k in range(self.no):
      self.wo[j][k] = rand(-2.0, 2.0)

   # last change in weights for momentum 
   self.ci = makeMatrix(self.ni, self.nh)
   self.co = makeMatrix(self.nh, self.no)

    def update(self, inputs):
   if len(inputs) != self.ni-1:
       raise ValueError, 'wrong number of inputs'

   # input activations
   for i in range(self.ni-1):
       #self.ai[i] = sigmoid(inputs[i])
       self.ai[i] = inputs[i]

   # hidden activations
   for j in range(self.nh):
       sum = 0.0
       for i in range(self.ni):
      sum = sum + self.ai[i] * self.wi[i][j]
       self.ah[j] = sigmoid(sum)

   # output activations
   for k in range(self.no):
       sum = 0.0
       for j in range(self.nh):
      sum = sum + self.ah[j] * self.wo[j][k]
       self.ao[k] = sigmoid(sum)

   return self.ao[:]


    def backPropagate(self, targets, N, M):
   if len(targets) != self.no:
       raise ValueError, 'wrong number of target values'

   # calculate error terms for output
   output_deltas = [0.0] * self.no
   for k in range(self.no):
       error = targets[k]-self.ao[k]
       output_deltas[k] = dsigmoid(self.ao[k]) * error

   # calculate error terms for hidden
   hidden_deltas = [0.0] * self.nh
   for j in range(self.nh):
       error = 0.0
       for k in range(self.no):
      error = error + output_deltas[k]*self.wo[j][k]
       hidden_deltas[j] = dsigmoid(self.ah[j]) * error

   # update output weights
   for j in range(self.nh):
       for k in range(self.no):
      change = output_deltas[k]*self.ah[j]
      self.wo[j][k] = self.wo[j][k] + N*change + M*self.co[j][k]
      self.co[j][k] = change
      #print N*change, M*self.co[j][k]

   # update input weights
   for i in range(self.ni):
       for j in range(self.nh):
      change = hidden_deltas[j]*self.ai[i]
      self.wi[i][j] = self.wi[i][j] + N*change + M*self.ci[i][j]
      self.ci[i][j] = change

   # calculate error
   error = 0.0
   for k in range(len(targets)):
       error = error + 0.5*(targets[k]-self.ao[k])**2
   return error


    def test(self, patterns):
   for p in patterns:
       print p[0], '->', self.update(p[0])

    def weights(self):
   print 'Input weights:'
   for i in range(self.ni):
       print self.wi[i]
   print
   print 'Output weights:'
   for j in range(self.nh):
       print self.wo[j]

    def train(self, patterns, iterations=1000, N=0.5, M=0.1):
   # N: learning rate
   # M: momentum factor
   for i in xrange(iterations):
       error = 0.0
       for p in patterns:
      inputs = p[0]
      targets = p[1]
      self.update(inputs)
      error = error + self.backPropagate(targets, N, M)
       if i % 100 == 0:
      print 'error %-14f' % error


def demo():
    # Teach network XOR function
    pat = [
   [[0,0], [0]],
   [[0,1], [1]],
   [[1,0], [1]],
   [[1,1], [0]]
    ]

    # create a network with two input, two hidden, and one output nodes
    n = NN(2, 2, 1)
    # train it with some patterns
    n.train(pat)
    # test it
    n.test(pat)



if __name__ == '__main__':
    demo()

Fonte: Blacklight
TTDH
TTDH
Moderatore
Moderatore

Messaggi : 14
Data d'iscrizione : 11.11.09

Torna in alto Andare in basso

Torna in alto


 
Permessi in questa sezione del forum:
Non puoi rispondere agli argomenti in questo forum.