Nei capitoli precedenti abbiamo introdotto il modello computazionale delle reti approssimanti,
focalizzando poi la nostra attenzione sulle loro proprietà approssimanti e su alcune classi di
reti, in particolare le reti neurali. Da tutto ciò sono emerse le loro potenzialità soprattutto
nella risoluzione di problemi poco formalizzabili da un punto di vista algoritmico, ma facilmente
descrivibili qualitativamente.
Ci chiediamo ora come sia possibile passsare dal modello “astratto” al corrispondente “reale” o
“implementato” su elaboratore, per definire e successivamente risolvere tale classe di problemi.
Si vuole perciò realizzare un “ambiente software”, capace in primo luogo di gestire la struttura
dati rete neurale e di fornire quelle funzioni neccessarie alla sua costruzione, inizializzazione
e al suo addestramento, e in secondo luogo di sposare l’efficienza di calcolo con la facilità di
utilizzo da parte dell’utente (due aspetti che verranno presi in considerazione soprattutto per
le future applicazioni). Abbiamo scelto a tale scopo due “filosofie” di programmazione “opposte”:
Matlab e C.
Nel primo capitolo si è visto che la j-esima componente di una rete approssimante può
essere così caratterizzata:
dove è la dimensione della funzione da
approssimare,
è una data funzione base,
mentre i
e le componenti di
sono i parametri da determinare,
esprimibili anche come unico vettore
Nel seguito ci limiteremo a trattare una classe particolare di reti, cioé le multilayer
feedforward networks, la cui struttura è rappresentata in figura 1-2, per via della loro
“popolarità” e del buon numero di risultati teorici disponibili.
Possiamo notare che, indipendentemente dalle scelte implementative e dal linguaggio di
programmazione utilizzato, il tipo di dato rete neurale deve senz’altro includere le seguenti
informazioni:
· ingressi alla rete
· numero dei livelli o degli strati (solitamente sono due, il primo è lo strato
nascosto, il secondo lo strato di uscita)
· numero dei neuroni per livello
· tipo funzione di attivazione che caratterizza i neuroni di un livello
· pesi di ogni livello
· uscite di ogni livello
· uscite della rete.
Sulla base di queste considerazioni, possiamo osservare che:
a) gli ingressi del primo livello corrispondono agli ingressi della rete
b) le uscite di ogni livello rappresentano gli ingressi del livello successivo
c) le uscite della rete sono le uscite dell’ultimo livello
d) i pesi e le uscite che caratterizzano ogni strato possono essere distribuiti
fra i suoi neuroni costituenti (in questo modo al singolo neurone viene assegnato
un vettore dei pesi, comprendente il suo bias e i pesi sulle uscite del livello
precedente).
Assegnati quindi alla nuova struttura dati rete neurale questi attributi, si vogliono realizzare
quelle funzioni necessarie al suo ciclo di vita ed in particolare quelle strettamente legate
all’agoritmo della back-propagation, scelto come metodo di addestrameto, quali:
· costruttore
· distruttore (se necessario)
· inizializzatore dei pesi
· fase forward
· fase backward
· lettura su file
· scrittura su file
Nei successivi paragrafi, dopo una breve presentazione dei due linguaggi di programmazione,
saranno presentati per le reti neurali feed-forward due modelli alternativi, ma comunque
equivalenti, che si rifanno ai requisiti appena elencati, implementati rispettivamente in
Matlab e in C, specificando ove necessario l’eventuale presenza del “software di contorno”
indispensabile alla loro realizzazione.
La scelta di un determinato linguaggio di programmazione può risultare strategica per il successo
o il fallimento di un prodotto software.
In questo paragrafo saranno brevemente descritte le principali caratteristiche di Matlab e del C,
i due linguaggi scelti come “ambienti” per lo sviluppo di software sulle reti neurali.
Matlab, da Matrix Laboratory, è un interprete di comandi che, grazie al suo ricco
ambiente di sviluppo integrato, offre tutte le possibilità di un vero e proprio linguaggio
di programmazione. È capace di integrare il calcolo, la visualizzazione e la programmazione
in un ambiente di facile utilizzo, dove i problemi e le soluzioni sono espresse in una
notazione matematica familiare.
Matlab è un sistema interattivo il cui tipo base elementare è l’array (vettore) che
non richiede di essere dimensionato. Ciò consente di risolvere molti problemi di calcolo,
specialmente quelli con matrici e vettori, e di formularli in una maniera molto più veloce di
quella che richiederebbe un programma scritto in Fortran o C, linguaggi per nulla interattivi.
Inoltre nelle ultime versioni possiamo trovare il cell-array, nuovo tipo base che
consente di realizzare strutture complesse, caratterizzate dalla presenza di variabili
differenti, oltre che nel tipo, anche nella dimensione.
Per questi motivi, Matlab è un linguaggio di alte performance per il calcolo matematico,
tipicamente utilizzato per lo sviluppo di algoritmi, la modellazione, la simulazione e la
costruzione di prototipi, l’analisi dei dati e la visualizzazione, per la costruzione di
interfacce grafiche utente.
Bisogna sottolineare poi come esso si sia evoluto nel corso degli anni grazie agli utenti
che hanno contribuito, sia in campo universitario sia in quello industriale, all’implementazione
di nuove funzioni. Matlab è caratterizzato così da una famiglia di soluzioni per applicazioni
specifiche, chiamati toolboxes, che consentono di apprendere e sviluppare determinati
problemi, essendo una collezione di funzioni (definite negli m-file) che estentono l’ambiente
Matlab e lo rendono in grado di risolvere particolari classi di problemi.
Le aree nelle quali i toolboxes sono disponibili includono fra le altre signal processing,
control systems, reti neurali, fuzzy logic, simulazione.
Grazie a tutto ciò, Matlab consente sia la “programming in the small” per creare rapidamente
veloci programmi sia la “programming in the large” per applicazioni più complesse.
I motivi per cui abbiamo scelto di sviluppare “ex novo” un pacchetto software per la gestione
delle reti neurali sono dovuti principalmente al fatto che, nelle nostre applicazioni, occorre
una certa flessibilità nell’utilizzo dell’algoritmo della “back propagation”, dovuta alla presenza
del gradiente stocastico, e ciò non è consentito dal toolbox, per altri aspetti molto completo,
di Matlab. Non abbiamo scelto infatti di implementare l’approssimazione di Levemberg -
Marquardt,
tecnica effettivamente molto usata per l’addestramento delle reti neurali, proprio perché
il toolbox dispone di una sua realizzazione molto efficiente.
Per una più completa panoramica sulle caratteristiche e sulle potenzialità di Matlab rimandiamo,
per esempio, a [8] e [9].
Per quel che riguarda il C, esso è un passo al di sopra rispetto ai linguaggi orientati
alle macchine, o di basso livello, ma un passo al di sotto dei linguaggi risolutori
di problemi, o di alto livello, essendo abbastanza vicino al computer da fornire un grande
controllo sui dettagli implementativi di un’applicazione, ma abbastanza lontano da poter ignorare
i dettagli dell’hardware.
Nel C vi sono molte meno regole di sintassi che in altri linguaggi ed è così possibile scrivere
un compilatore C di alta qualità che funzioni in soli 256K di memoria totale.
Il C possiede un insieme conciso di parole chiave e, ad esempio, non contiene alcuna funzionalità
integrata di input e output, funzioni per il trattamento delle stringhe o per l’accesso al file
system. Dispone però di un ricco insieme di funzioni di libreria, usato con tale frequenza e
normalità da poter essere considerato quasi come parte integrante del linguaggio stesso.
Vi sono librerie per funzioni matematiche, elementi grafici, trattamento dei file, supporto
di database, realizzazione di finestre sullo schermo, immissione di dati, comunicazioni e
funzioni generiche di supporto.
Oltre a consentire l’allocazione dinamica della memoria e la possibilità di indirizzare
particolari aree di memoria, il C si distingue dagli altri linguaggi di programmazione
perché è l’unico a realizzare un’aritmetica dei puntatori che permette una gestione più
semplice di tali variabili.
Il codice C prodotto dalla maggior parte dei compilatori è molto efficiente. La ristrettezza
del linguaggio, le ridotte dimensioni del sistema run-time, l’uso dei puntatori ed il fatto
che il linguaggio sia vicino all’hardware fanno sì che molti programmi in C siano eseguiti a
velocità vicine a quelle degli equivalenti programmi scritti in linguaggi
assembler.
Il C supporta la programmazione modulare, dove compilazione e linking sono separate, che
consente di ricompilare solo le parti del programma aggiornate durante lo sviluppo. Tale
caratteristica può essere estremamente importante quando si sviluppano programmi di grandi
dimensioni o anche programmi di media grandezza su sistemi lenti. Senza il supporto della
programmazione modulare, la quantità di tempo necessaria per compilare un programma completo
potrebbe rendere il ciclo di compilazione, collaudo e modifica assai lento. Il C è inoltre
un linguaggio non fortemente tipizzato, in quanto offre una grande flessibilità nel manipolare
i dati. Questo consente ad esempio di visualizzare i dati in formato diverso o di eseguire
operazioni di casting, ma anche di scrivere espressioni con valori non precisamente definiti,
che possono causare errori imprevedibili a run-time. Per ulteriori chiarimenti sul C,
si veda [10].
Per la realizzazione del nostro modello, se da una parte abbiamo scelto Matlab per la sua
“immediatezza” o facilità nella gestione di strutture dati complesse, quali che sono le reti
neurali, dall’altra abbiamo deciso di impiegare il C per la sua potenza nello sviluppo di
programmi eseguibili efficienti, oltre naturalmente alle motivazioni già discusse a riguardo
dell’implementazione del gradiente stocastico.
Abbiamo già visto nel primo capitolo come la struttura della j-esima componente di una rete
feedforward possa essere così definita:
Dal momento che le informazioni caratterizzanti una rete neurale sono di vario formato e di
dimensioni spesso variabili (vedi paragrafo 3.1), nell’implementare in Matlab la struttura
dati principale del modello si è scelto di ricorrere al cell-array, addottando perciò una
visione di insieme.
In esso le singole celle contengono rispettivamente:
· numero dei livelli L
· struttura della rete N (cell-array in cui compaiono nell’ordine il numero degli
ingressi della rete e, per ogni livello, la coppia numero di neuroni – tipo funzione di
attivazione)
· ingressi parziali Z (cell-array nel quale si trovano L vettori, uno per
ogni livello s della rete, le cui componenti rappresentano l’ingresso pesato totale
al singolo neurone del livello s)
· uscite parziali Y (cell-array che include il vettore degli ingressi alla rete nella
prima cella, i vettori delle uscite di ogni livello s nelle successive)
· pesi della rete W (cell-array costituito da L matrici bidimensionali, nelle
quali sono contenuti i pesi di ogni livello).
Va subito precisato che le matrici bidimensionali dei pesi hanno m colonne pari al
numero dei neuroni del livello s cui si riferiscono e n righe pari al numero
delle uscite del livello s-1 maggiorato però di 1, per considerare tutti i bias del
livello s.
Definendo in questo modo una rete neurale, in Matlab è stato possibile attraverso solo quattro
funzioni implementare l’intero suo ciclo di vita, dalla sua costruzione e inizializzazione,
sino al suo addestramento.
La funzione costruttrice buildnet è così responsabile della definizione delle proprietà
strutturali della rete. Essa riceve in ingresso la dimensione del vettore degli ingressi, seguito
da una lista di lunghezza variabile di argomenti formata dalle coppie nemero neuroni – tipo
funzione di attivazione che caratterizzano i singoli livelli, inizializzando quindi le celle
L e N.
Effettuando, ad esempio, la seguente chiamata:
rete = buildnet(2,100,’logsig’,2,’purelin’)
possiamo creare una rete feedforward a due strati, con 2 ingressi, 100 neuroni logsig
nel primo livello e 2 neuroni purelin nel secondo.
A questo punto la funzione initnet può determinare le dimensioni delle matrici dei
pesi corrispondenti a ciascun livello, che andranno a costituire il cell-array W.
Essa prende perciò come argomenti la rete stessa e il range di estrazione casuale dei pesi,
restituendo poi la rete i cui pesi sono stati inizializzati nel dominio specificato, come
risulta chiaro dalla chiamata:
new_rete = initnet(old_rete,1)
che inizializza i pesi di old_rete in [-1,1], restituendo poi new_rete.
Per applicare ora alla rete neurale un vettore di ingresso e calcolare la corrispondente
uscita (fase forward della back-propagation), si rende necessario completarne la struttura
con l’introduzione degli ingressi e delle uscite parziali, ovvero dei cell-array Z
e Y.
La funzione forward svolge proprio questo compito. Riceve il vettore degli ingressi
con la rete già inizializzata e produce l’uscita della rete, senza ovviamente modificare in
alcun modo i pesi della rete, costruendo e inizializzando livello dopo livello i vettori
degli ingressi e delle uscite parziali, come possiamo capire dal seguente esempio:
[new_rete, outputs] = forward(old_rete, [0.3 –0.6]’)
dove il vettore degli ingressi [0.3 –0.6] viene applicato a old_rete e l’uscita corrispondente
restituita nel vettore outputs.
Per ogni neurone q del livello s viene dapprima calcolato l’ingresso pesato
totale, quindi la corrispondente uscita, applicando ad esso la funzione di attivazione che lo
caratterizza.
È importante notare qui l’efficenza della funzione Matlab feval, utilizzata all’interno
della forward per determinare l’uscita di ogni singolo neurone, che consente ad esempio
di passare ad una funzione matematica uno o più argomenti di dimensione variabile (scalari,
vettori o matrici).
Occorre inoltre precisare che, per caratterizzare i neuroni del nostro modello, sono stati presi
in considerazione i seguenti quattro tipi funzione di attivazione fra quelli forniti da
Matlab:
·
·
·
·
L’ultima funzione implementata, fondamentale nelle applicazioni che utilizzano le reti neurali
perché responsabile dell’aggiornamento dei pesi dell’intera struttura, è la backward:
in essa viene codificata la seconda fase dell’algoritmo della back-propagation, nella quale si
verifica col metodo del gradiente un passo di discesa nello spazio dei pesi.
Ricordiamo, a questo proposito, come il calcolo delle derivate parziali costituenti il gradiente
possa essere così espresso:
dove le sono calcolate “a ritroso” per
mezzo della (3.9)
Per ottenere ciò, la backward riceve in ingresso:
· la rete inizializzata o frutto di un precedente addestramento
· la lunghezza del passo di discesa
· il vettore delle derivate parziali del costo rispetto alle uscite dell’ultimo livello
e restituisce:
· la rete con le matrici dei pesi aggiornate
· il vettore delle derivate parziali del costo rispetto agli ingressi.
Una tipica chiamata alla funzione backward è perciò la seguente:
[dJy0, new_rete] = backward(old_rete,[1.2 –0.6],0.05)
che aggiorna i pesi di old_rete attraverso un passo di discesa dell’algoritmo del gradiente
(0.05 è la lunghezza del passo), restituendo in dJy0 il vettore delle derivate parziali del
costo rispetto agli ingressi.
Nello sviluppo di questa procedura, si sono apprezzate da una parte la possibilità offerta
da Matlab di esprimere le formule matematiche in notazione “standard” e dall’altra l’enorme
compattezza delle stesse.
È stato infatti possibile implementare con estrema velocità sia il calcolo a ritroso delle
sia l’algoritmo del gradiente per
l’aggiornamento dei pesi, dal momento che l’ambiente consente di chiamare le funzioni anche
su argomenti strutturati (vettori, matrici o cell-array).
Ad esempio, per valutare le di un dato
livello s della rete, sono sufficienti le due seguenti righe di codice:
dJz{s} = dgz'.*dJy{s} ;
dJw{s} = [1;y{s-1}(:,1)]*(dJz{1,s}).
Vedremo più avanti che il C non permette una codifica così rapida e compatta. Per quanto
riguarda infine la lettura da file o la scrittura su file dello “stato” di una rete neurale,
aspetto molto importante perchè spesso si vuole riprendere un vecchio addestramento o eseguire
una fase di simulazione, è sufficiente ricorrere rispettivamente alle funzioni Matlab
load e save che consentono un accesso immediato al file system.
Al contrario di Matlab, dove il tipo base array e quello più evoluto cell-array
hanno consentito di definire quasi “istantaneamente” la rete neurale, in C si è resa necessaria
una fase preliminare di implementazione, allo scopo di dotare l’ambiente delle strutture dati
indispensabili al modello.
Inoltre, non essendo possibile già in compilazione definire variabili statiche (ad esempio vettori)
senza specificarne la dimensione, nella maggior parte dei casi si è fatto ricorso all’allocazione
dinamica della memoria.
Ciò, se da una parte ha garantito un uso efficiente e “diretto” delle variabili con un controllo
pressoché totale su di esse, velocizzando notevolmente l’esecuzione dei programmi, dall’altra ha
rallentato e esteso non di poco l’implementazione/debugging del codice, dovendo questo anche
garantire la massima portabilità rispetto alle “piattaforme” UNIX e MS-DOS.
Ritornando ora al problema dei tipi base sui quali verrà costruita la nostra rete neurale, abbiamo
dovuto realizzare alcune strutture dati indispensabili al modello, quali i vettori, le matrici,
gli insiemi di vettori e gli insiemi di matrici, ricorrendo alle liste dinamicamente linkate.
Un vettore è così caratterizzato da una dimensione n e da un puntatore alla lista degli
n elementi. I costruttori vet_i_allc_ini, vet_l_allc_ini e
vet_d_allc_ini allocano lo spazio di memoria necessario a contenere n valori di
interi, long o double rispettivamente, mentre i distruttori vet_d_free, vet_i_free
e vet_l_free liberano la memoria precedentemente allocata. La funzione vet_d_init
inizializza casualmente gli elementi di un “vettore double”.
Una matrice è invece costituita dal numero delle righe r, dal numero delle colonne
c e da un doppio puntatore ad una lista di n×c elementi. Le funzioni
mat_i_allc_ini o mat_d_allc_ini sono i costruttori, mentre mat_d_free
o mat_i_free i distruttori, rispettivamente nel caso di elementi interi o double.
Come vedremo poi nel capitolo 4, la definizione degli insiemi di vettori o di matrici si rende
necessaria per alcuni tipi di applicazione sulle reti neurali, anche se non rientra nella loro
struttura.
Un insieme di vettori risulta perciò formato dai seguenti campi: numero dei vettori, loro
dimensione e corrispondente lista. Un insieme di matrici è strutturato in maniera analoga,
anche se qui occorre specificare due dimensioni, ovvero il numero delle righe e delle colonne.
Entrambi gli “oggetti” poi dispongono di funzioni per la loro costruzione, inizializzazione
casuale e distruzione, che ovviamente si basano su quelle appena viste per vettori e matrici.
Dopo tutte queste premesse, per arrivare alla realizzazione della rete neurale del nostro modello,
in C si è preferito partire dalla definizione del neurone, quale suo elemento base, di cui la
figura 3-1 ne riporta la struttura.
Un neurone può essere così definito dai seguenti campi:
· ingresso z
· uscita y
· vettore dei pesi .
L’informazione sul tipo di funzione di attivazione g che caratterizza il neurone
(scelta fra le logsig, tansig, radbas e purelin citate
nel paragrafo 3.3) e che applicata all’ingresso z produce l’uscita y qui
non compare, essendo più direttamente legata al suo livello di appartenenza.
Con l’introduzione fra breve dei livelli, il vettore dei pesi del neurone q nel livello s dovrà contenere
scalari double, ovvero gli
pesi sulle uscite del livello precedente
s-1, oltre al bias, e quindi sarà esprimibile come:
Ciò consentirà quindi di determinare il valore z dell’ingresso pesato e dell’uscita
y corrispondente (entrambi double), applicando le note relazioni
Per gestire ora questo tipo di dato, sono stati implementati il costruttore nalloc e
il distruttore de_nalloc (libreria neu_mem.h) che si occupano di allocare o
deallocare rispettivamente uno spazio di memoria sufficiente a contenere un certo numero di
neuroni.
Sfruttando ora i tipi precedentemente introdotti e considerando l’analisi dei requisiti minimi
fatta sul modello (vedi paragrafo 3.1), si giunge alla formulazione della struttura dati rete
neurale in termini di:
· numero dei livelli
· numero degli ingressi
· numero delle uscite
· massimo numero di neuroni fra i livelli
· numero di neuroni per livello (vettore di interi le cui componenti indicano da quanti neuroni
è formato un livello)
· tipo funzione di attivazione per livello (vettore di interi che contiene per ogni livello il
codice della funzione attivazione, ovvero 1 per logsig, 2 per tansig, 3 per
radbas e 4 per purelin)
· insieme di neuroni (array bidimensionale di puntatori a neurone, dove l’elemento
[s,q] rappresenta il neurone q del livello s).
Naturalmente, per i nostri scopi, una rete neurale necessita sì del costruttore ralloc
e del distruttore de_ralloc, responsabili dell’allocazione dinamica della struttura,
ma anche di molte altre funzioni che permettano di inizializzarne i campi (numero ingressi,
numero livelli, ...) e di compiere fasi successive di addestramento per ottimizzarne i pesi.
La prima funzione che analizziamo è la build_net. Essa si occupa di dare inizialmente
alla rete neurale una certa “fisionomia”, definendone il numero dei livelli, il numero di neuroni
e il tipo di funzione di attivazione per livello, e quindi di allocare per ogni neurone un vettore
dei pesi delle giuste dimensioni. Per fare
ciò riceve in ingresso la rete precedentemente allocata, il numero degli ingressi, i vettori del
numero di neuroni e del tipo di funzione di attivazione per livello, come si può vedere dal suo
prototipo:
void build_net (NN *rete, int nlivelli, VET_INT *neu_vet, VET_INT *neu_functions)
Init_net invece inizializza i pesi della rete, o estraendoli casualmente nel range
specificato o caricandoli da file e, per fare ciò, accetta in ingresso la rete già “strutturata”,
il file dei pesi e un parametro booleano che indica se utilizzare tale file, come è qui mostrato:
void int_net(NN *rete, FILE *weight_file,BOOLEAN np)
La funzione free_net invece libera la memoria dalle variabili della struttura
precedentemente allocate con build_net.
Prima di introdurre le due procedure responsabili dell’addestramento della rete, forwardf
e backwardf, sulle quali è interessante operare un confronto con le corrispondenti
funzioni Matlab forward e backward, ricordiamo la presenza di alcune funzioni
di utilità, quali print_weight per visualizzare i pesi della rete, save_state
o save_weights per memorizzare l’intera struttura o soltanto i pesi della rete,
extract per l’estrazione casuale di un numero.
Forwardf riceve una rete già inizializzata, un vettore degli inputs ed un vettore degli
outputs: applica alla rete, quale ingresso, il vettore double degli inputs e calcola la
corrispondente uscita, copiandola quindi nel vettore degli outputs. Il suo prototipo è qundi
il seguente:
void forwardf (NN *rete, VET_DB inputs, VET_DB *outputs)
Per ogni neurone q del livello s, dapprima viene calcolato l’ingresso pesato
totale (3.11), quindi la sua uscita (3.12) attraverso la funzione neu_function, al cui
interno troviamo un’istruzione “case” sul tipo di funzione di attivazione (scelto fra le
logsig, tansig, radbas e purelin precedentemente descritte,
anche se tale insieme può venire facilmente esteso). Al termine del processo, quando si raggiunge
l’ultimo livello della rete, risultano aggiornati l’ingresso z e l’uscita y di
ogni neurone.
A parte la gestione delle variabili puntatore, l’uso continuo di cicli “for” annidati (per
calcolare gli ingressi pesati totali e le corrispondenti uscite di ogni neurone) e la necessità
di costruire/richiamare manualmente le funzioni di attivazione (in Matlab, oltre ad essere già
disponibili, venivano applicate semplicemente attraverso l’operatore feval), non si
notano grandi differenze in merito alle “difficoltà” e ai tempi implementativi rispetto alla
corrispondente forward di Matlab.
Tutte queste “complicazioni” del C incidono, invece, in maniera determinante sullo sviluppo di
backwardf. Essa è responsabile dell’aggiornamento dell’intera rete, ovvero di compiere
un passo di discesa nello spazio dei pesi attraverso l’algoritmo del gradiente.
A tale scopo, riceve in ingresso la rete già inizializzata, i vettori delle derivate parziali
della funzione di costo rispetto agli ingressi della rete e alle uscite dell’ultimo livello,
oltre naturalmente alla variabile che
definisce la lunghezza del passo di discesa (abbiamo visto che essa è esprimibile in funzione
del passo di addestramento k come
,
dove
e
sono costanti che caratterizzano
il problema).
La chiamata alla funzione backwardf si rifà al seguente prototipo:
void backwardf (NN *rete, VET_DB *dJy0, VET_DB dJyL, double tk)
dove è la lunghezza del passo di discesa,
dJyL il vettore delle derivate parziali della funzione di costo rispetto alle uscite dell’ultimo
strato, mentre dJy0 quello rispetto agli ingressi della rete.
Possiamo ora brevemente riassumere i fattori che hanno rallentato la codifica in C della
“fase backward” della back-propagation:
· allocazione dinamica per le
· impiego di variabili temporanee per memorizzare le dei livelli s e s+1
· uso “massiccio” di cicli “for” annidati per calcolare le
di ogni neurone, le derivate parziali
della funzione di costo rispetto al singolo peso e per aggiornare di conseguenza il suo valore
· implementazione delle derivate delle funzioni di attivazione (dlogsig, dtansig,
dradbas e dpurelin).
Rispetto alla versione in Matlab, ad esempio, il calcolo delle
risulta essere assai meno “immediato”,
come possiamo notare dalle seguenti righe che ne codificano l’algoritmo:
for (p=1; p<=(qn->n).vet[s]; p++)
{ delta_new.vet[p] = 0.0 ;
if (s==qn->livelli) delta_new.vet[p] = delta.vet[p] ;
else
for (q=1; q<=(qn->n).vet[s+1]; q++)
delta_new.vet[p] += delta.vet[q] * (qn->net[s+1][q].w).vet[p] ;
delta_new.vet[p] *= dfunzatt(qn->net[s][p].z,(qn->tipo_fa).vet[s]) ;
}
Saranno qui riassunte e brevemente elencate le principali funzioni implementate in Matlab e in C,
capaci nel loro insieme di fornire all’utente uno strumento per lo sviluppo di applicativi
sulle reti neurali.
In Matlab, come abbiamo visto, per gestire l’intero ciclo di vita di una rete neurale sono
sufficienti le seguenti funzioni:
· buildnet (costruttore della rete)
· initnet (inizializzazione casuale dei pesi)
· forward (fase forward della back-propagation)
· backward (fase backward della back-propagation).
Al contrario, in C, si sono dovute implementare molte procedure di supporto alle funzioni
strettamente necessarie per il modello e ciò ha condotto alla realizzazione delle seguenti
librerie:
· neu_def.h (funzioni di attivazione logsig, tansig, radbas
e purelin, con rispettive derivate dlogsig, dtansig, dradbas
e dpurelin)
· neu_mem.h (funzioni per l’allocazione dinamica di matrici e vettori con elementi
interi/reali, quali mat_i_allc_ini, mat_d_allc_ini, vet_i_allc_ini,
vet_l_allc_ini, vet_d_allc_in, mat_d_free, mat_i_free,
vet_d_free, vet_i_free, vet_l_free; per l’allocazione dinamica
dei neuroni, come nalloc e de_nalloc, o delle reti neurali, quali
ralloc e de_ralloc)
· neu_kern.h (funzioni per costruire/ripulire una rete, come build_net e
free_net, funzione per inizializzarla init_net, funzioni per il suo
addestramento, quali forwardf e backwardf, funzioni di interfaccia con
il file system, come save_state, save_weights, loadnet)
· neu_uti.h (funzioni per l’estrazione casuale dei numeri, come rnd o
extract, funzioni per la gestione degli errori, funzioni per l’inizializzazione
casuale di vettori/matrici e insiemi di vettori/ matrici, come vet_d_init o
set_vet_d_init).
Ringraziamenti,
Introduzione,
Capitolo 1,
Capitolo 2,
Capitolo 3,
Capitolo 4,
Bibliografia