Neural Network Toolbox In questa sede verrà presentata una trattazione esauriente delle caretteristiche fondamentali del Neuron Network Toolbox presente come pacchetto supplementare al software Matlab ( si farà riferimento alla versione 3.0 del Toolbox ). In particolare si cercherà di illustrare le caratteristiche principali di questo potente pacchetto applicativo facendo riferimento alla notazione sintattica, essenziale per poter apprendere le basi per progettare una qualsisa rete neurale utilizzando le funzionalita' del toolbox. Infine, verranno presentate le topologie di reti più semplici ma nello stesso tempo più facili da comprendere per un eventuale approfondimento futuro. Una rete neurale è composta da semplici elementi che operano in parallelo su uno o piu' livelli. L'idea di fondo è quella di addestrare una rete neurale affinchè approssimi il meglio possibile una certa funzione aggiustando i pesi delle connessioni tra gli elementi stessi. L'addestramento della rete può avvenire in due modi distinti: in una modalità supervisionata, mediante l'utilizzo di un training set, ossia di un campione di coppie input / output effettivo, o, alternativamente, senza supervisore appoggiandosi su una competizione interna tra i neuroni. In questa trattazione verrà presentato esclusivamente il primo modo di allenamento della rete. Il punto di partenza è un insieme di coppie di ingressi e uscite reali. Tali input campione vengono dati in inserimento alla rete e il risultato della computazione viene confrontato con l'output effettivo. L'errore in questo modo calcolato ( a meno che i due risultati non coincidano ) viene utilizzato per aggiornare i pesi delle connessioni tra gli elementi e ripetere l'addestramento della "nuova" rete utilizzando tuttavia gli stessi campioni. Le modalità di addestramento sono varie e dipendono da molteplici fattori: da come viene intesa la funzione di errore tra uscita reale e quella della rete, dall'algoritmo di aggiornamento dei pesi delle connessioni, dal fatto di utilizzare un approccio incrementale, dove i pesi vengono modificati dopo l'esecuzione di ogni esempio, o un approccio post processing, in cui i pesi vengono modificati solo al termine di una epoca, vale a dire una sessione di allenamento completa sfruttando tutti gli esempi campione. Il toolbox permette di agire su tutti questi fattori e, come vedremo in seguito, ogni topologia di rete progettata dipende in parte dalla differenzazione di questi aspetti. Modello del neurone Il neurone è l'unita fondamentale di una qualsiasi rete neurale. Esso è schematizzato nella figura 1 Figura 1 - Schema di un neurone
In ingresso arrivano R valori di input, che possono essere o gli ingressi della rete qualora il neurone appartenga al primo livello della rete stessa, o le uscite degli R neuroni presenti nel livello precedente qualora il neurone in questione faccia parte di un livello intermedio ( o finale ). Di questi input si effettua una sommatoria pesata con i valori delle connessioni che portano gli ingressi al neurone. A tale sommatoria si aggiunge un ulteriore costante opzionale ( se posta a zero ) denominata bias, interpretabile come un peso addizionale in riferimento ad un ingresso unitario. L'uscita della sommatoria è il seguente valore: p1*w1 + p2*w2 +... + pr*wr + b Questa quantità viene passata in ingresso ad una funzione modellatrice la cui uscita rappresenta l'output del neurone. Tale output, a seconda di come è strutturata la rete, può essere interpretato come un valore di uscita della rete stessa, se il neurone è collocato nel suo ultimo livello, o un ingresso dei neuroni del livello successivo, se il neurone analizzato è collocato su un livello intermedio. E' importante sottolineare come il neurone venga caratterizzato quasi esclusivamente dal tipo di funzione di trasferimento scelta. In figura 2 vengono mostrate le tre funzioni più utilizzate: la funzione gradino, la funzione lineare y = x e la funzione sigmoidale. Da notare come, se si utilizza il gradino l'uscita di ogni neurone è vincolata ad assumere solo due valori, 0 e 1; se si utilizza il sigmoide i valori di uscita variano in modo continuo ma in un range limitato dagli estremi 0 e 1 mentre nel caso lineare l'uscita puo' assumere qualsiasi valore. Nonostante tali funzioni siano direttamente implementate nel toolbox, nessuno ci vieta di inizializzare una funzione di trasferimento personalizzata per le nostre reti. Figura 2 - Tre delle più comuni funzioni di uscita di un neurone: il gradino, la retta n = a e il sigmoide Architettura di una rete neurale Due o più neuroni appena analizzati posso essere combinati in un livello e una rete può contenere uno o più di questi livelli. Consideriamo prima di tutto una rete neurale ad un solo livello. Nella figura 3 viene mostrata la tipologia di questa semplice rete, con R ingressi e S neuroni. Figura 3 - Schema di un livello neuronale
In questa rete, ogni elemento del vettore P di input è connesso a ciascun neurone dell'unico livello attraverso un opportuno peso. Per convenzione si definisce Wij il peso della connessione che collega il j-esimo input ( Pj ) all' i-esimo neurone, con 1 < i < S e 1 < j < R. I neuroni sono accomunati dal fatto di ricevere in ingresso gli stessi valori. Questi tuttavia vengono pesati in maniera differente. Se si tiene presente che il bias e la funzione di trasferimento possono variare a seconda del neurone, l'uscita degli stessi risulta generalmente distinta. Essendo il numero dei neuroni pari ad S e dato che ogni neurone calcola un proprio valore di output, in uscita all'unica livello della rete avremo S valori. Una rete a più livelli è concettualmente analoga alla tipologia di rete appena vista. Analizziamola maggiormente in dettaglio facendo riferimento alla figura 4. Figura 4 - Schema di una rete neuronale a più livelli. Nella figura è rappresentata una rete a 3 livelli. L'ultimo livello di una rete siffatta è quello più significativo. I primi due invece vengono definiti livelli nascosti in quanto le loro uscite non vengono direttamente osservate in output alla rete ma bensì fungono da ingressi per il livello successivo. Il numero di neuroni di ogni livello è generalmente differente. Se si definisce Si il numero di neuroni che compongono l' i-esimo livello, Si sarà anche il numero di ingressi al livello successivo ( ossia il livello i + 1 ). Infatti, caratteristica di una rete neurale, è che i collegamenti tra neuroni possono avvenire solo in avanti tra livelli adiacenti. Di conseguenza non è possibile che un neurone riceva in ingresso un valore in uscita da un neurone che appartiene ad un livello diverso da quello precedente. Il numero di uscite di questa rete sarà uguale a S3 cioè al numero di neuroni presenti nel suo ultimo livello. E' molto comodo identificare i valori degli elementi di una rete mediante l'utilizzo della notazione matriciale. P è il vettore colonna di dimensione Rx1 in ingresso alla rete neurale. I pesi vengono rappresentati da matrici Wij, dove convenzionalmente si utilizza l'indice i per identificare il livello di destinazione e l'indice j per quello di partenza. Per esempio, con W32 si rappresenta la matrice dei pesi che collegano le uscite dei neuroni del secondo livello con gli ingressi ai neuroni del terzo. Ogni matrice Wij ha ordine Si X Sj : in ogni riga k-esima sono presenti i pesi delle connessioni che collegano gli Sj neuroni del livello j al k-esimo neurone del livello i. La cosa potrebbe sembare complicata ma in realtà è molto comoda per esprimere in forma matriciale la computazione di una rete neurale anche molto complessa. Di fatto, la stessa rete a 3 livelli vista sopra, può essere schematizzata nella figura 5:
Figura 5 - Schema di una rete neuronale a più livelli con parametri in forma matriciale a1 è l'uscita del primo livello della rete e viene calcolata applicando la funzione alla quantità IW11 * P + B1, dove IW11 è la matrice dei pesi relativa al primo livello ( I sta per ingresso appunto, il primo indice 1 sta a indicare che il livello di destinazione è il primo mentre il secondo indice 1 è relativo al livello precedente, che in questo caso non esiste essendo il primo della rete ) di dimensione S1 xr, P è il vettore colonna di dimensione R x 1 degli ingressi e B1 è il vettore colonna di dimensione S1 x 1 contenente i bias degli S1 neuroni del primo livello. Facendo un breve calcolo sulla dimensione di a1, questo risulta essere un vettore colonna di dimensione S1 x 1 come infatti è ovvio che sia ( un uscita per ogni neurone del livello 1 ). Analogo discorso viene portato avanti per tutti gli altri livelli. Tenendo presente la relazione che sussiste tra gli ingressi del livello K e le uscite del livello K-1, è facile costruire una dipendenza diretta tra le uscite della rete ( a3 ) e il vettore degli ingressi alla rete stessa ( P ) mediante un'equazione matriciale che altrimenti sarebbe stata estremamente lunga da definire. Le reti a più livelli sono sufficientemente potenti. Si consideri infatti che una rete a 2 livelli, il cui primo livello è caratterizzato da una funzione sigmoidale e il secondo da una lineare, può essere addestrata per approssimare arbitrariamente bene qualsiasi funzione con un numero finito di discontinuità. Simulazione concorrente e simulazione sequenziale In questo paragrafo verranno analizzati gli effetti della simulazione di una rete alla luce dei differenti formati dei dati che possono essere portati in ingresso. La pià semplice situazione di simulazione di una rete si presenta quando la rete stessa è statica ( priva di ritardi sugli ingressi e di feedback degli stessi ). In questo caso non è necessario stabilire nessun ordine sulla sequenza dei dati in ingresso che vengono di fatto trattati in modo concorrente. In pratica, se N sono i vettori P di dati da inserire, il risultato è come se ci fossero N reti dello stesso tipo ma distinte che operano in parallelo simulando ognuna uno di questi N vettori. L'uscita in relazione a un certo ingresso viene calcolata in modo indipendente, come se gli altri N-1 dati non esistessero. Dato che l'intento è quello di progettare reti con il Toolbox di Matlab, vediamo un semplice esempio esplicativo di questa situazione. Prima di tutto è necessario creare un neurone. In questo caso si è scelto un neurone con una funzione lineare. Il comando dedicato è newlin che accetta come dati di ingresso nell'ordine: 1. una matrice Rx2 dove nell' i-esima riga sono indicati il minimo e il massimo valore che può assumere l'i-esimo elemento del vettore di ingresso ( di dimensione Rx1 ). 2. il numero S di neuroni del livello 3. il fattore di ritardo 4. il fattore di apprendimento
Nel nostro semplice esempio abbiamo un singolo neurone ( S = 1 ) e due ingressi, mentre non sarà necessario specificare nè il ritardo nè il tasso di apprendimento di cui discuteremo più avanti. Supponendo che gli ingressi possano variare in una range [ -3 ; 3 ] la rete a un livello si costruisce in questo modo: net = newlin( [ -3 3; -3 3 ], 1 ); Il passo successivo è quello di settare i pesi delle connessioni e il valore del bias. Come abbiamo visto in precedenza è necessario costruire una matrice Wij per i pesi delle connessioni tra il livello j e il livello successivo i. Ogni matrice sarà di dimensione Si x Sj. La notazione del toolbox per richiamare ( in lettura e scrittura ) la matrice Wij è la seguente: net.iw{1,1}nel caso si voglia referenziare la matrice dei pesi che collegano gli ingressi della rete al primo livello e net.lw{i,j} nel caso si voglia referenziare la matrice dei pesi che collega due livelli adiacenti intermedi. Una volta creata la rete ( denominata nell'esempio con net ), è possibile visualizzare un elenco completo di tutti i suoi attributi semplicemento digitando il nome della rete stessa come se fosse un comando effettivo di Matlab. Nell'esempio la matrice IW dei pesi ha dimensione 1X2 ( 2 ingressi e 1 neurone di destinazione ), mentre il bias di indice 1 ( ossia il vettore colonna di bias riferito al primo livello ) sarà un semplice scalare avendo a disposizione un unico neurone: net.iw{1,1} = [ 1 2 ]; net.b{1} = 0; Una simulazione avviene solo in corrispondenza di certi dati in ingresso. Se vogliamo simulare il comportamento di una rete in corrispondenza di N vettori di input distinti non è necessario operare N simulazioni differenti. Tutto dipende da come si strutturano i dati da inserire nella rete. Se il vettore dei dati in ingresso ha dimensione Rx1 è sufficiente creare una matrice P di ordine RxN. Nella colonna j-esima di tale matrice si leggono i valori degli ingressi del j-esimo vettore di input. E' importante sottolineare come in una rete a simulazione concorrente l'ordine delle colonne non è significativo e tale matrice può essere costruita come una qualunque permutazione dei vettori colonna di ingresso.non ci resta ora che definire il comando per simulare il comportamento della rete net in relazione agli N vettori di input concatenati nella matrice P. Il comando matlab e sim( net, P ) e restituisce un vettore riga di dimensione 1xS dove S sono le uscite (una per ogni neurone ). P = [ 1 2 2 3; 2 1 3 1]; A = sim( net, P ); Nel caso avessimo a che fare con reti dinamiche, l'ordine in cui sono presentati i vettori di ingresso è fondamentale. Questo accade particolarmente per le reti con ritardo, come illustrato in figura Figura 6 - Rete dinamica a un solo ritardo Si tratta di una semplice rete neurale ad un singolo ritardo. Questo significa che l'uscita della rete in corrispondenza dell' i-esimo vettore di input viene calcolata utilizzando anche le informazioni contenute nel vettore di ingresso precedente ( i -1 ). In figura si enfatizza il legame tra i vettori degli
ingressi con il tempo discreto t, ma il significato rimane invariato. E' quindi facile comprendere come l'ordine relativo dei vettori input è significativo. Ogni permutazione delle colonne della matrice P degli ingressi comporta generalmente un differente vettore riga di uscita dalla rete dopo la simulazione. Per realizzare una rete di questo tipo è sufficiente riprendere i comandi analizzati in precedenza. Si costruisce sempre il solito livello lineare ad un neurone questa volta specificando anche il ritardo. Il settaggio dei pesi e del bias non cambia anche se bisogna sottolineare che la dimensione della matrice dei pesi dipende da quanti ritardi è caratterizzata la rete. Nel nostro esempio consideriamo una rete con un solo ingresso e un ritardo. La matrice dei pesi avrà quindi dimensione 1x2. In generale se D sono i ritardi, la matrice dei pesi ha dimensione1x(d+1). In pratica, anche se l'ingresso di questa rete è un semplice scalare, l'effetto è quello invece di un vettore di ingressi lungo D+1 in quanto il neurone riceve come input non solo l'ingresso effettivo ma anche i D ingressi precedenti. La matrice degli ingressi viene definita come un array di celle. Ogni cella è un vettore di input e la sua posizione all'interno dell'array è, ribadiamo, caratterizzazione fondamentale della simulazione neurale. Di fatto, anche l'uscita della simulazione è un array di celle, ciascuna relativa al vettore input di riferimento. net = newlin( [-3 3; 3 3 ], 1, [0 1] ); net.b{1} = 0; net.iw{1,1} = [ 1 2 ]; P = { [1] [2] [3] [4] } Addestramento di una rete neurale Affinchè una rete neurale approssimi nella sua simulazione una certa funzione degli ingressi, è necessario effettuare un lavoro graduale di aggiornamento dei vari pesi e dei bias mediante l'impiego di un certo numero di esempi predefiniti di cui si conosce esattamente l'output reale che la rete dovrebbe emettere in funzione dei rispettivi ingressi. Questa fase, successiva al progetto della rete, prende comunemente il nome di training. Gli algoritmi di addestramento sono numerosi: se la memoria occupata non è una limitazione del nostro problema, il più adatto in questo senso risulta essere il Levenberg-Marquardt, mentre l'algoritmo di backpropagation puro rimane sempre un ottima soluzione che integra velocità computazionale e risparmio di risorse. Non è intenzione di questa trattazione analizzare tutti gli algoritmi esistenti in letteratura e implementati nel Neural Toolbox. Vi rimando al capitolo 5 del manuale del toolbox per una più approfondita analisi. Ci basti sapere che per reti semplici possono essere utilizzati algoritmi non troppo elaborati ( come nei nostri esempi ). Le modalità di addestramento di una rete si suddividono in due classi fondamentali: incremental training e batch training. Addestramento incrementale La filosofia che sta alla base dell'allenamento incrementale è quella di aggiornare i pesi e i bias della rete ogni volta che un vettore input del training set si presenta in ingresso alla rete stessa. Questa modalità, sebbene adattabile a tutte le tipologie di rete, è maggiormente utilizzata per quelle dinamiche, come i filtri adattativi. Prendiamo in considerazione una banale rete di quelle trattate sino ad ora e vediamo come si procede in pratica all'addestramento della rete, secondo la modalità incrementale. net = newlin( [ -1 1 ; -1 1 ], 1, 0, 0.1 ); net.iw{1,1} = [ 0 0 ]; net.b{1} = 0; Dovrebbe essere tutto chiaro in quanto non è stato introdotto nulla di nuovo. Si noti come il quarto parametro di ingresso alla funzione newlin sia il fattore di apprendimento che è stato settato a 0.1.
Supponiamo di voler allenare una rete a due ingressi affinchè simuli la seguente funzione t = p1 + p2 e di avere un training set di 4 vettori. Allenando la rete in modalità incrementale, l'ordine con cui questi 4 vettori si presentano al suo ingresso è determinante su come la rete stessa venga addestrata. Ricordando la notazione del toolbox su vettori di ingresso sequenziali, il training set P viene espresso mediante un array di celle e così anche per le uscite predefinite T ( che si conoscono ) associate a ciascuno di questi ingressi: P = { [ -1 ; 2 ] [ 2 ; 1 ] [ 2 ; 3 ] [ 3; 1 ] }; T = { 4 5 7 7 }; Il comando per eseguire l'addestramento della rete è il seguente: [ net, a, e ] = adapt( net, P, T ); Semplicemente si passa in ingresso alla funzione adapt la rete da addestrare e il training set ( la matrice P degli ingressi e quella T delle uscite ). Come parametri di uscita si ha: 1. la rete addestrata, con i nuovi valori di pesi e bias 2. il vettore a contenente le uscite della rete dopo l'addestramento in funzione degli ingressi del training set 3. il vettore e contenente gli errori tra l'uscita reale T e l'uscita della rete a Non è infatti detto che dopo l'allenamento la rete riesca a trovare una corretta valutazione dei pesi e dei bias affinche tutto il training set si comporti come desiderato. Questo dipende da tanti fattori, non da meno la tipologia della rete, del training set, della funzione di trasferimento e via dicendo. Se realizzassimo questa semplice situazione con il toolbox noteremo infatti che il vettore a è molto discordante dal vettore T. Batch training E' la modalità di addestramento della rete per la quale i pesi e i bias vengono modificati solo dopo che tutti gli esempi del training set sono stati utilizzati come ingressi alla rete stessa. Rispetto all'allenamento incrementale, dove era necessario costrutire il training set come una sequenza ordinata di esempi, la strutturazione dello stesso dipende dalla morfologia della rete. Di conseguenza se indichiamo con P il training set, questo sarà un array di celle nel caso di reti dimaniche o una matrice nel caso di reti statiche. Nel breve esempio proposto in seguito viene mostrato come implementare il batch training di una rete statica. net = newlin( [ -3 3; -3 3 ], 1, 0, 0.1 ); net.iw{1,1} = [ 0 0 ]; net.b{1} = 0; P = [ 1 2 2 3; 2 1 3 1 ]; T = [ 4 5 7 7 ]; net.train( net, P, T ); L'unica differenza con quanto abbiamo visto nell'allenamento incrementale è l'utilizzo della funzione train al posto di adapt. Train accetta in ingresso la rete e il training set e aggiorna i pesi delle connessioni e i bias solo al termine dell'addestramento. Per verificare che i valori di questi elementi siano stati realmente modificati, è sufficiente accedere ad essi in lettura con i comandi net.iw{1,1} e net.b{1}. E' possibile utilizzare ancora il comando adapt ma solo per il batch training di reti statiche. E' consigliabile comunque l'utilizzo di train che sfrutta algoritmi più sofisticati. L'ultimo aspetto da sottolineare è come, solitamente, il train usato nel modo descritto sopra, è poco utile in quanto se
simuliamo la rete con il training set dopo l'addestramento noteremo che i valori in uscita sono assai diversi da quelli che ci aspettiamo. L'idea è quella di iterare l'addestramento per un certo numero di volte ( epoche ) cosi' da raffinare sempre meglio i valori dei pesi e dei bias. Questa iterazione viene implementata automaticamente dalla funzione train settando il numero di epoche di allenamento: net.trainparam.epochs = 10; Si ricordi tuttavia che se una rete, per sua struttura, non è in grado di implementare certe funzioni, anche aumentando a dismisura il numero di epoche non si avrà mai la piena convergenza tra uscite di rete e uscite reali del training set. Banalmente se il training set è costituito da questi due esempi: P = [ 3 3 ]; T = [ 0 1 ]; nessuna rete sarà in grado di addestrarsi in modo corretto in quanto non è possibile che in relazione allo stesso input ( 3 ) si abbiano due uscite distinte ( 0 e 1 ). Altre situazioni analoghe verranno presentate quando affronteremo la tipologia di rete più semplice ma più solida, il percettrone.