UNIVERSITA' POLITECNICA DELLE MARCHE
FACOLTA' DI INGEGNERIA
Corso di Laurea triennale in Ingegneria Informatica e dell'Automazione
IMPLEMENTAZIONE E SPERIMENTAZIONE DI ALGORITMI
DI APPRENDIMENTO FUZZY SU RETI NEURALI DI TIPO
NEURAL GAS
Relatore: Chiar.mo
Prof. Aldo Franco Dragoni
Tesi di Laurea di:
Gianluca Giulianelli
Correlatore:
Dott. Mauro Mazzieri
Anno accademico 2009 - 2010
Un ringraziamento speciale e dal profondo del cuore va a mio padre
Erminio e mia madre Maria che mi hanno sempre sostenuto ed
incoraggiato.
Ringrazio il relatore prof. Aldo Franco Dragoni ed il correlatore dott.
Mauro Mazzieri per la disponibilità e la pazienza con cui mi hanno
seguito fin dall'inizio del tirocinio.
Grazie a tutti i miei amici, che hanno condiviso con me i momenti felici e
che mi hanno sorretto nei momenti difficili.
Indice
INTRODUZIONE
pag 1
CAPITOLO 1
Le Reti Neurali
1.1 - Concetto di rete neurale
1.2 - Reti neurali e Clustering
1.3 - Rete neurale Neural Gas (NG)
1.4 - Rete neurale Growing Neural Gas (GNG)
1.5 - Indici caratteristici delle reti neurali
pag 6
pag 9
pag 11
pag 14
pag 18
CAPITOLO 2
Implementazione delle reti neurali NG e GNG
2.1 - Strutture dati utilizzate
2.2 - Interfaccia ReteNeurale
2.3 - Classe Gng
2.4 - Classe NeuralGas
2.5 - Implementazione versione asimmetrica
pag 20
pag 36
pag 39
pag 51
pag 55
CAPITOLO 3
Implementazione dell' algoritmo Fuzzy
3.1 - Algoritmo di apprendimento Fuzzy
3.2 - Strutture dati utilizzate
3.3 - Classe FuzzyGng
3.4 - Implementazione versione asimmetrica
pag 56
pag 60
pag 65
pag 72
CAPITOLO 4
Valutazione sperimentale
4.1 - Convergenza al variare dei parametri e confronto
4.2 - Andamento degli indici caratteristici
4.3 - Valutazione sperimentale delle reti asimmetriche
4.4 - Visualizzazione grafica - clustering
4.5 - Sperimentazione su dataset di prova
pag 73
pag 79
pag 80
pag 82
pag 89
CONCLUSIONI
pag 92
BIBLIOGRAFIA
pag 93
INTRODUZIONE
In questa tesi viene descritto il lavoro di implementazione in linguaggio
java e sperimentazione di alcuni tipi di reti neurali ad apprendimento non
supervisionato, cioè le reti neurali di tipo Neural Gas (NG) e Global
Neural Gas (GNG).
Per ognuna di queste reti sono stati condotti esperimenti al fine di
valutarne le prestazioni e capire in che misura la variazione dei parametri
di apprendimento incida sull'efficienza delle reti stesse.
Successivamente è stata implementata una versione innovativa
dell'algoritmo di apprendimento: la versione Fuzzy, che rivoluziona il
concetto di connessione tra i nodi della rete.
Anche sulla versione Fuzzy sono stati condotti esperimenti di valutazione
delle performance al variare dei parametri di apprendimento e, pur essendo
un algoritmo ancora in fase di sviluppo, sono state fatte alcune
osservazioni con la speranza che siano utili ai ricercatori che se ne
occuperanno in futuro.
Le tre reti neurali (NG, GNG e Fuzzy-GNG) sono state sottoposte ad uno
stesso training per confrontarne la convergenza e l'apprendimento e per
dimostrare la validità dell'algoritmo di apprendimento Fuzzy.
Delle tre reti neurali è stata implementata e valutata sperimentalmente la
versione asimmetrica , su questa versione si è voluto
anche osservare la distribuzione delle connessioni unidirezionali rispetto a
quelle bidirezionali.
Inoltre si è dato ampio risalto all'applicazione di clustering di dati, alla
quale sono state applicate le reti neurali implementate.
Infatti è stata creata un'applicazione java che sfrutta le librerie JOGL (Java
Open GL) per visualizzare l'evoluzione della rete durante l'apprendimento
ed osservarne la capacità di individuare i cluster.
Infine è stato condotto un esperimento di clustering su dati relativi alla
frequenza con cui compaiono alcune parole all'interno degli articoli
rilasciati da un'agenzia di stampa.
Capitolo 1
LE RETI NEURALI
1.1 Concetto di rete neurale
Una rete neurale è una struttura logico-matematica ispirata all'attività di
apprendimento di nuove informazioni esercitata dal cervello umano
tramite le sue unità fondamentali: i neuroni biologici.
Il neurone biologico ,in prima approssimazione, genera un impulso
elettrico di un certo potenziale sul suo assone (output) il cui valore dipende
dalla somma pesata dei potenziali presenti sui dendriti (input) del neurone
stesso.
Il segnale di output che si propaga lungo l'assone viene a sua volta
modulato dalla sinapsi prima di andare ai dendriti degli altri nodi, sinapsi
diverse modulano il segnale presente sull'assone in modo diverso.
Una rete neurale quindi è una struttura composta da varie unità: nodi;
ogni nodo è sono caratterizzato da un insieme di valori numerici che ne
denota la posizione all'interno dello spazio di stato della rete (di
dimensione arbitraria).
Tra due nodi della rete, può essere presente o meno una connessione che
sta ad indicare che i due nodi sono collegati tra loro; a seconda del tipo di
rete neurale implementata la connessione può essere :
simmetrica -->
connessione(a,b) = connessione(b,a)
asimmetrica--> connessione(a,b) ≠ connessione(b,a)
Dato un nodo “a”, l'insieme dei nodi con cui è connesso, viene detto
insieme dei nodi vicini (topologicamente) o anche dei connessi al nodo.
La rete neurale di conseguenza, oltre ad una sua rappresentazione
geometrica data dalla posizione dei nodi nello spazio di stato, ha anche una
sua topologia definita dalle varie connessioni tra nodi.
Le reti neurali possono apprendere in due modi:
–
apprendimento supervisionato
–
apprendimento non supervisionato
Nell'apprendimento supervisionato alla rete neurale vengono presentati dei
dati in input per cui si conosce già l'output desiderato che deve assumere la
rete alla fine dell'apprendimento.
Questi dati costituiscono il training set della rete , essendo già noto l'errore
tra la posizione dei nodi desiderata e la posizione dei nodi effettiva, i
parametri relativi all'apprendimento vengono modificati ad ogni passo
cercando di ridurre al minimo tale errore.
Alla fine della fase di training quindi, si avranno i parametri che verranno
usati dalla rete neurale durante l' apprendimento vero e proprio.
L'apprendimento non supervisionato è quello utilizzato dalle reti neurali
che verranno descritte nei seguenti paragrafi, non è supervisionato perchè
alla rete vengono presentati solo dati di input significativi per cui non si
conosce un output desiderato. La rete cerca man mano di inserire dati di
input simili all'interno dello stesso gruppo con un apprendimento
autonomo, senza controllo esterno sull'errore.
Questo tipo di apprendimento viene utilizzato spesso quando non è
disponibile un set di dati di riferimento o addirittura non si è a conoscenza
della categoria di appartenenza dell'input.
1.2 Reti neurali e Clustering
Una rete neurale apprende se, quando gli viene proposta una sequenza di
dati di input (dataset), i suoi nodi tendono sempre più ad avvicinarsi
all'input e la topologia che assume la rete permette di evidenziare gruppi di
dati simili tra loro (clustering).
Se prendiamo come esempio banale, una serie di punti appartenenti al
piano cartesiano come dati di input:
si può notare facilmente che usando come criterio di clustering la distanza
euclidea tra i vari punti , i punti si possono raggruppare in prima
approssimazione in 3 cluster :
Se ad una rete neurale vengono proposti in input i suddetti punti, alla fine
dell'apprendimento la rete deve assumere una conformazione tale da poter
estrapolare i 3 cluster .
Un' ipotetica rete neurale che abbia appreso in modo corretto, può avere
questa morfologia:
La rete sopraindicata è composta da nodi (punti bianchi) coerenti con lo
spazio di stato del set di dati di input (punti neri), ed assume una topologia
che evidenzia correttamente i tre cluster in cui si raggruppano i dati.
Il clustering di dati è infatti una delle applicazioni più comuni in cui
vengono utilizzate le reti neurali.
1.3
Rete Neurale Neural Gas (NG)
La rete neurale di tipo Neural Gas è una rete ad apprendimento non
supervisionato che si pone l'obbiettivo di riconoscere ed adattarsi alla
topologia del dataset di input che gli viene proposto, senza avere alcuna
informazione preliminare sui legami di similarità tra gli elementi del
dataset stesso.
In questa tipologia di rete, si ha un numero più o meno consistente di nodi
(N) che va stabilito a priori e che rimane costante durante tutta la fase di
apprendimento.
Di solito la posizione iniziale di ogni singolo nodo è scelta casualmente
all'interno dello spazio dimensionale dell'input o semplicemente ogni nodo
viene scelto tra gli stessi dati del dataset.
Prima del training ogni nodo è isolato, ovvero non possiede alcuna
connessione verso gli altri nodi della rete, associando un' età ad ogni
connessione possiamo riconoscere la mancanza di quest'ultima ponendo
l'età a zero, quindi inizialmente le età di tutte le possibili connessioni
( N X N ) hanno valore zero.
La fase di training è composta da vari adaptation step, in ognuno dei quali
si parte con il sottoporre alla rete un dato di input appartenente al dataset,
questo dato viene confrontato con i nodi della rete e l'insieme dei nodi
viene ordinato in ordine crescente ripetto alla distanza (solitamente
euclidea) tra nodo e dato di input.
Successivamente si procede allo spostamento dei nodi verso l'input ,
l'incremento dello spostamento dipende dalla posizione che ogni nodo
assume nell'ordinamento visto in precedenza.
Inoltre, ad ogni adaptation step viene creata o resettata l'età della
connessione tra il nodo vincitore (primo della lista - più vicino) ed il
secondo , cioè si ha età(vincitore, secondo)=1, mentre tutte le età relative
alle connessioni tra il vincitore e gli altri nodi della rete (se diverse da 0)
vengono incrementate di 1.
Se l'età di una connessione ha raggiunto una determinata età massima,
viene eliminata (età=0).
Questo meccanismo di creazione ed eliminazione delle connessioni
rappresenta l'apprendimento della rete neurale, che alla fine del training
deve portare la rete stessa ad assumere una topologia coerente a quella del
set di dati in input.
Algoritmo NEURAL GAS: [1]
(1)
La rete è composta inizialmente da "n" nodi aventi posizione
casuale all' interno dello spazio di stato.
ωn ε Rⁿ
(2)
viene sottoposto alla rete un dato di input “ζ”.
(3)
Determinare la sequenza di nodi ki (i = 0,1,... n-1) della rete
tale che:
|| ωk0– ζ || < || ωk1 – ζ || <............... < || ωkn – ζ ||
(4)
muovere i nodi della rete verso il dato di input di uno
spostamento pari a:
Δωki = ε · e
i
λ
· || ζ – ωki ||
(5)
se non esiste , creare la connessione tra il nodo k0 (vincitore) ed
il nodo k1 (secondo); se esisteva resettarne l'età.
(6)
Incrementare l'età di tutte le connessioni che collegano k0 agli
altri nodi.
(7)
Rimuovere le connessioni di k0 che superano l'età massima T.
(8)
Se un criterio di stop non è stato soddisfatto tornare al passo 1.
I parametri di apprendimento cambiano ad ogni adaptation step e
dipendono dal rapporto tra il numero di steps effettuati (t) ed il numero
totale degli steps di training previsti (tmax) :
λ = λi · ( λf / λi)
ε = εi · (εf / εi)
t/tmax
t/tmax
T = Ti · (Tf / Ti)
t/tmax
Il passo (3) dell'algoritmo, dove si procede all'ordinamento dei nodi in
base alla distanza dal dato di input, è sicuramente il più oneroso in termini
di tempo di elaborazione. Quindi, osservando che lo spostamento dei nodi
è molto piccolo per i >> λ e che, essendo (λf << λi), con l'aumentare di t
λ tende ad essere piccolo, con il crescere degli adaptation steps si può
ridurre il numero dei nodi da ordinare (i << n).
1.4 Rete Neurale GNG (Growing Neural Gas)
La rete neurale di tipo GNG (Growing Neural Gas) è una rete ad
apprendimento non supervisionato, nata per risolvere alcune problematiche
riscontrate nell'utilizzo della rete neurale di tipo NG (Neural Gas) .
Uno dei problemi che si deve affrontare quando si sottopone un certo set di
dati in input alla NG per effettuare un clustering dei dati stessi, è che
dobbiamo stabilire a priori il numero dei nodi della rete.
A volte questo numero si rivela insufficiente e bisogna rielaborare da capo
il set di dati in input ,sottoponendoli ad una rete NG composta da un
numero maggiore di nodi.
Nella NG il numero di nodi ,di cui si compone la rete, va stabilito a priori e
rimane invariato durante tutta la sua evoluzione; ciò significa che non
vengono aggiunti nuovi nodi, e non vengono rimossi nodi preesistenti,
questa caratteristica da un lato pone un limite certo alla dimensione della
rete, e quindi allo spazio di memoria utilizzato per tener traccia dei nodi e
delle connessioni, però dall'altro lato pone il problema di conoscere già in
fase di inizializzazione il numero dei nodi: questo problema ha ispirato il
l'idea su cui si fonda la Gng .
La rete GNG al contrario della NG, inizialmente è composta da un numero
limitato di nodi che cresce durante la fase di apprendimento.
Infatti, l'idea principale su cui si basa il funzionamento della GNG, è di
aggiungere nuovi nodi alla rete al crescere dei passi di adattamento verso i
dati di input (adaptation step).
I nuovi nodi vengono aggiunti alla rete tenendo conto della sua evoluzione
passata , cioè basandosi su alcune misure statistiche di cui si è tenuta
traccia durante gli adaptation steps precedenti.
Il meccanismo di gestione dell'età delle connessioni tra i nodi è identico a
quello descritto nella trattazione della NG, ovvero:
in ogni adaptation step viene creata una connessione tra il nodo più vicino
ed il secondo e vengono incrementate le età di tutti i connessi al vincitore;
le connessioni la cui età supera l'età massima vengono rimosse.
Quindi la topologia della rete cambia continuamente perchè ad ogni passo
di adattamento possono essere create nuove connessioni tra nodi oppure
eliminate alcune connessioni preesistenti.
Durante la sua crescita, la rete può anche perdere dei nodi, ciò è dovuto al
fatto che alcuni dei nodi rimangono isolati (non connessi con nessun altro
nodo) e quindi vengono eliminati.
Con l'aumentare del numero di adaptation step, i nodi della rete neurale
GNG tendono ad avvicinarsi globalmente ai dati di input, e la topologia
della rete evidenzia con maggiore accuratezza gruppi di dati simili
(clusters).
Un'altra differenza importante rispetto alla NG è che nella GNG i
parametri relativi all'apprendimento si mantengono costanti.
Algoritmo GNG: [2]
(1)
La rete è composta inizialmente da due nodi “a” e “b” aventi
posizione casuale all'interno dello spazio di stato:
ωa ε Rⁿ , ωb ε Rⁿ
(2)
viene sottoposto alla rete un dato di input “ζ”.
(3)
trovare il nodo più vicino “s1” ,ed il secondo più vicino “s2” al
dato di input.
(4)
Incrementare l'età di ogni connessione che parte da s1.
(5)
Aggiungere il quadrato della distanza tra l' input ed il nodo s1,
ad una variabile locale (errore) che fa riferimento ad s1:
Δerror(s1) = ( || ωs1 – ζ || )²
(6)
muovere s1 ed i nodi a lui connessi, verso il dato di input ζ , di
una frazione della distanza tra s1 e ζ ; rispettivamente di “εb”
ed “εn” :
Δ ωs1 = εb · || ζ – ωs1 ||
per il nodo s1
Δ ωsn = εn · || ζ – ωsn ||
per i nodi connessi ad s1
(7)
Se s1 e s2 sono già connessi resettare l'età della connessione,
altrimenti creare la connessione tra i due nodi
(8)
rimuovere le connessioni che hanno un età maggiore ad
“αmax” (età massima) ; se, come conseguenza, uno o più nodi
divengono isolati, eliminarli dalla rete.
(9)
Se il numero di dati sottoposti alla rete finora è un multiplo
intero del parametro “λ”, aggiungere un nuovo nodo alla rete
nel seguente modo:
(9.1) trovare il nodo “q” che ha accumulato maggior errore
(Δerror)
(9.2) inserire il nuovo nodo “r” a metà tra il nodo q el il nodo tra
i suoi connessi che ha maggiore errore “f”:
ωr = 0,5 · (ωq + ωf)
(9.3) connettere il nodo r con i nodi q e f, rimuovere la
connessione tra q ed f
(9.4) ridurre gli errori associati a q ed f, moltiplicandoli per una
costante α < 1 , inizializzare l'errore associato ad r con il
nuovo valore dell'errore di q.
(10)
ridurre gli errori associati a tutti i nodi, moltiplicandoli per una
costante d < 1.
(11)
se un criterio di stop non è stato soddisfatto ,ritorna al passo 1
Come vediamo dall'algoritmo, i nodi vengono spostati verso il dato di
input (passo 6), in questo modo i nodi della rete tenderanno globalmente
ad avvicinarsi al set di dati di input.
La creazione della connessione tra il nodo vincitore (più vicino) ed il
secondo (passo 7), l'incremento dell'età associata alle connessioni
(passo 4), e di conseguenza la rimozione delle connessioni troppo vecchie
e dei nodi isolati (passo 8), permettono di definire una topologia della rete
coerente con quella dei dati in input.
La variabile locale errore, associata ad ogni nodo, serve a sapere in quale
zona è più opportuno inserire il nuovo nodo e come collegarlo.
1.5 Indici caratteristici delle reti Neurali
Lunghezza media di un cammino (L):
In una rete neurale, la distanza tra due nodi, si definisce come il numero
connessioni del cammino più breve che li connette.
La lunghezza media di cammino della rete è la media aritmetica delle
distanze tra coppie di nodi.
Nelle reti che rappresentano ambiti del mondo reale , di solito L non è
molto grande , ciò è dovuto al fenomeno detto “small world”.
Coefficiente di Clustering (C):
Dato un nodo della rete j , connesso direttamente a Kj nodi , si possono
Kj ·(Kj-1)
avere al più
connessioni tra questi nodi.
2
Di solito ce ne sono di meno, per ipotesi supponiamo che ci sono Ej
connessioni effettive (Ej < Kj).
Il coefficiente di clustering del nodo j si definisce come il rapporto :
cioè ,
Cj =
Ej
Kj
2 · Ej
Kj ·(Kj-1)
Il coefficiente di clustering della rete ( C ) è dato dalla media aritmetica di
tutti i coefficienti Cj ed è compreso tra 0 e 1 (1 se la rete è totalmente
connessa) [3].
Distribuzione del grado (P):
Il grado di un nodo è il numero di connessioni che il nodo ha verso gli altri
nodi.
La distribuzione del grado mette in relazione il grado ,con la quantità dei
nodi della rete che possiedono tale grado.
Capitolo 2
IMPLEMENTAZIONE DELLE RETI NEURALI
NG E GNG
2.1 Strutture Dati Utilizzate
A prescindere dalle varie implementazioni di una rete neurale , si possono
comunque individuare alcune strutture dati che rappresentano i principali
componenti della rete stessa: i nodi e le connessioni tra nodi.
Per quanto riguarda i nodi, essi sono caratterizzati da un insieme di valori
C={c1,c2,c3,....,cn}
ci ε R , rappresentativi della loro posizione
all'interno dello spazio di stato della rete.
Se si considera l'insieme dei nodi N = { nodo1 ,nodo2 ,nodo3 ,....., nodom}
numerabile ,allora una possibile struttura dati che mette in relazione i nodi
ai rispettivi valori (N X C) è una matrice come quella raffigurata sotto:
dove la colonna j-esima (j=1,2,3...m) indica i valori caratteristici del nodo j
, cioè Cj= { c1,j , c2,j , c3,j ,........ , cn,j}.
Un altro componente della rete neurale che possiamo rappresentare
mediante una matrice sono le connessioni.
Una connessione rappresenta la presenza di un collegamento tra un nodo
ed un altro , la presenza del collegamento può essere indicata da un flag ,
cioè da una variabile che assume valore pari ad 1 (collegamento presente)
o pari a 0 (collegamento non presente).
Quindi se si associa un flag ad ogni coppia possibile di nodi (N X N), si
hanno le informazioni necessarie per comprendere la topologia della rete
neurale.
Le connessioni quindi possono essere rappresentate in memoria da una
matrice binaria (i suoi elementi possono assumere solo valore pari a 0 o 1)
come quella sottostante:
In questo caso abbiamo che da ogni colonna j-esima (o riga) , si riesce a
capire con quali nodi è connesso il nodo j, osservando gli indici di riga per
i quali: matrice(i,j) = 1 (nodoi connesso a nodoj)
I valori sulla diagonale sono tutti uguali ad 1 perchè si considera che un
nodo sia sempre connesso con se stesso.
Efficienza strutture dati:
E' utile. al fine di trovare la struttura dati più efficiente per rappresentare in
memoria una matrice , sapere se essa può definirsi densa o sparsa.
Una matrice si definisce sparsa se contiene pochi valori diversi da zero ,
mentre al contrario si definisce densa se contiene pochi valori pari a zero.
Questa distinzione è importante perchè le matrici sparse possono essere
memorizzate e gestite in modo più efficiente tramite l'utilizzo di
contenitori associativi che tengono traccia soltanto dei pochi valori diversi
da zero contenuti nella matrice.
L'utilizzo di un classico array bidimensionale per la memorizzazione di
una matrice sparsa comporta spreco di memoria e maggiore lentezza
nell'esecuzione delle operazioni sulla matrice ,come ad esempio la ricerca
di un valore al suo interno.
Nel caso dell'implementazione delle reti neurali , la matrice
rappresentativa delle connessioni è spesso una matrice sparsa perchè
l'interconnessione tra i nodi è ben lontana dall'essere totale.
Matrici Dinamiche:
Un altro aspetto di cui tenere conto per l'implementazione delle strutture
dati da utilizzare è che di solito nelle reti neurali si ha una crescita del
numero di nodi all'aumentare del numero di dati presentati in input
alla rete.
La crescita del numero di nodi comporta quindi una modifica del numero
di righe e/o di colonne delle matrici utilizzate; per esempio, per quanto
riguarda la matrice delle connessioni, si ha una crescita della sua
dimensione.
Di conseguenza, nello sviluppo del codice di implementazione, si è fatto
largo uso di matrici dinamiche.
Implementazione delle strutture dati in linguaggio Java:
Per implementare le suddette strutture dati in linguaggio Java , ho
utilizzato le librerie Parallel-Colt , che sono delle librerie open source
utilizzate per applicazioni scientifiche e tecniche che richiedono
performance elevate.
Queste librerie vengono anche utilizzate nel campo di ricerca dell' High
Energy Physics del CERN di Ginevra, dove si devono affrontare problemi
matematici in dimensioni molto elevate ed è richiesta una grande velocità
di computazione ed allo stesso tempo un'occupazione di memoria minima .
Le classi contenute nei vari package di Parallel-Colt rappresentano vari
costrutti matematici utili in campo scientifico, come riportato nel sito
ufficiale dedicato a questa libreria [4]:
“The Colt library provides fundamental general-purpose data
structures optimized for numerical data, such as resizable arrays,
dense and sparse matrices (multi-dimensional arrays), linear
algebra, associative containers and buffer management. The Jet
library contains mathematical and statistical tools for data
analysis, powerful histogramming functionality, Random Number
Generators and Distributions useful for (event) simulations, and
more.”
Come si evince dalla descrizione del contenuto sopra riportata, il
Parallel-Colt offre strutture dati ottimizzate per l'utilizzo di matrici
dense e sparse che, come già detto, vengono utilizzate
nell'implementazione efficiente dei vari tipi di reti neurali.
Inoltre questa libreria mette a disposizione altri strumenti come gli
array ridimensionabili (dinamici), che si adattano perfettamente a
rappresentare alcune variabili di stato di un sistema in evoluzione
come le reti neurali.
Le classi utilizzate della libreria Parallel-Colt per l'implementazione delle
reti neurali di tipo Neural Gas e Global Neural Gas sono principalmente
quelle relative a matrici sparse di interi e sono:
–
cern.colt.list.tint.IntArrayList
–
cern.colt.matrix.tint.impl.SparseIntMatrix1D
–
cern.colt.matrix.tint.impl.SparseIntMatrix2D
Infatti SparseIntMatrix2D implementa una matrice di interi gestita
tramite contenitori associativi poiché sparsa, mentre a sua volta
SparseIntMatrix1D implementa un vettore sparso che è utile ad
esempio nel rappresentare una riga o una colonna della matrice stessa.
IntArrayList invece implementa un vettore ridimensionabile
(dinamico) di interi che viene spesso utilizzato perchè richiesto da alcuni
fondamentali metodi di SparseIntMatrix2D come ad esempio il metodo
getNonZeros() da cui si ottengono i valori diversi da zero della matrice
sparsa, questo metodo richiede come argomenti 3 oggetti di tipo
IntArrayList.
Però, relativamente all'implementazione di una rete neurale, servono
matrici sparse che siano anche dinamiche, ovvero che sia possibile
variarne le dimensione.
Quindi le matrici dinamiche devono consentire operazioni come l'aggiunta
di una riga e/o di una colonna (aumenta la dimensione) e la rimozione di
una riga e/o di una colonna (riduce la dimensione).
Per implementare la matrice di interi sparsa e dinamica ho sviluppato la
classe:
Mat2DIntSparsaDinamica
(matrice sparsa dinamica):
SparseIntMatrix2D
Mat2DIntSparsaDinamica
La classe Mat2DIntSparsaDinamica eredita dalla classe
SparseIntMatrix2D del package cern.colt.matrix.tint.impl della
libreria Parallel-Colt, quindi è un'implementazione valida di una matrice
sparsa. I costruttori di SparseIntMatrix2D sono stati tutti sovrascritti per
poter sfruttare tutte le funzionalità di tale classe.
I metodi che sono stati aggiunti rispetto alla classe madre hanno il fine di
rendere questa matrice sparsa, una matrice sparsa dinamica.
I metodi sviluppati sono:
–
Mat2DIntSparsaDinamica aggiungiRiga(SparseIntMatrix1D riga)
Questo metodo restituisce una matrice identica a quella originaria con l'aggiunta di
una riga costituita dal vettore sparso “riga”, la riga aggiuntiva viene inserita dopo
l'ultima riga della matrice originaria.
Codice metodo aggiungiRiga(riga):
public Mat2DIntSparsaDinamica aggiungiRiga(SparseIntMatrix1D riga) throws
Exception
{
// controlla se la riga è
della stessa dimensione di quelle della matrice
if (riga.size()!=columns())
throw new Exception("dato non compatibile--> dim riga: "
+riga.size()+"
colonne mat: "+columns());
//nuova matrice, vuota con una riga in più
Mat2DIntSparsaDinamica nuova=new Mat2DIntSparsaDinamica(rows()+1,columns());
// estrapolo le coordinate dei valori diversi da zero
IntArrayList righe=new IntArrayList();
IntArrayList colonne=new IntArrayList();
IntArrayList valori=new IntArrayList();
getNonZeros(righe, colonne, valori);
// inserisco i dati estrapolati nella nuova matrice
for(int i=0;i<righe.size();i++)
nuova.setQuick(righe.get(i), colonne.get(i), valori.get(i));
// estrapolo le coordinate dei valori diversi da 0 dalla nuova riga
IntArrayList indici=new IntArrayList();
IntArrayList valoririga=new IntArrayList();
riga.getNonZeros(indici, valoririga);
// inserisco i dati diversi da 0 della riga nella nuova matrice (ultima riga)
for(int i=0;i<indici.size();i++)
nuova.setQuick(rows(), indici.get(i), valoririga.get(i));
return nuova;
}
Dal codice, possiamo vedere come solo gli elementi diversi da 0 della
matrice e della riga da aggiungere ,vengano effettivamente letti e scritti in
memoria durante l'elaborazione.
–
Mat2DIntSparsaDinamica aggiungiColonna(SparseIntMatrix1D
colonna)
Questo metodo restituisce una matrice identica a quella originaria con l'aggiunta
di una colonna costituita dal vettore sparso “colonna”, la colonna aggiuntiva
viene inserita dopo l'ultima colonna della matrice originaria.
–
Mat2DIntSparsaDinamica rimuoviRiga (int indice_riga)
Questo metodo restituice una matrice composta da tutte le righe della matrice
originaria, tranne la riga di indice “indice_riga”.
Codice metodo rimuoviRiga(indice_riga):
public Mat2DIntSparsaDinamica rimuoviRiga(int indice_riga) throws Exception
{
// controlla se l'indice della riga da eliminare è valido
if ((indice_riga<0)||(indice_riga>=rows()))
throw new Exception("indice non valido--> indice riga:
"+indice_riga+"
righe mat: "+rows());
//nuova matrice, vuota con una riga in meno
Mat2DIntSparsaDinamica nuova=new Mat2DIntSparsaDinamica(rows()-1,columns());
// estrapolo le coordinate dei valori diversi da zero
IntArrayList righe=new IntArrayList();
IntArrayList colonne=new IntArrayList();
IntArrayList valori=new IntArrayList();
getNonZeros(righe, colonne, valori);
// inserisco i dati
nella nuova matrice
for(int i=0;i<righe.size();i++)
{
if(righe.get(i)<indice_riga)
// non necessita lo shift
nuova.setQuick(righe.get(i), colonne.get(i), valori.get(i));
else
if(righe.get(i)>indice_riga) // necessita lo shift di riga
nuova.setQuick(righe.get(i)-1, colonne.get(i), valori.get(i));
}
return nuova;
}
In questo caso c'è da notare che la parte più importante del metodo
consiste nel ricopiare i dati della matrice originaria nella nuova matrice in
modo tale da lasciare intatte le righe di indice inferiore a quello della riga
da eliminare ed invece effettuare lo shift verso l'alto delle righe con indice
superiore .
descritto in pseudocodice ==> nuova_mat(i,j) = vecchia_mat( i+1, j )
–
Mat2DIntSparsaDinamica rimuoviColonna (int indice_colonna)
Questo metodo restituisce una matrice composta da tutte le colonne della matrice
originaria tranne la colonna di indice “indice_colonna”.
Per quanto riguarda il codice dei metodi aggiungiColonna(colonna) e
rimuoviColonna(indice_colonna), esso si può dire analogo rispettivamente
a quello di aggiungiRiga e rimuoviRiga.
Quindi, da un'analisi di questa implementazione, si deduce che rendere
dinamica una matrice Sparsa comporta meno operazioni, garantendo
maggiore velocità di esecuzione nell'aggiungere o rimuovere righe (o
colonne), rispetto ad una matrice normale.
Test delle classe Mat2DIntSparsaDinamica:
Questa classe rappresenta i mattoni con i quali si costruiranno le
implementazioni delle reti neurali, quindi sono state sviluppate delle
opportune classi di test per verificare l'efficacia dei vari metodi e non
ritrovarsi errori banali nelle fasi di sviluppo successive.
Codice del test
int[][] mat={{1,0,0,0,0},
// matrice 5x5 sparsa
{0,2,0,0,0},
{0,0,3,0,0},
{0,2,4,0,0},
{0,2,0,0,5}};
Prova prova=new Prova();
Mat2DIntSparsaDinamica sparsadin=new Mat2DIntSparsaDinamica(5,5);
for(int i=0;i<mat.length;i++)
for (int j=0;j<mat[i].length;j++)
sparsadin.setQuick(i, j, mat[i][j]);
sparsadin.trimToSize();
prova.stampaMatrice(sparsadin);
try{
// togliamo la colonna 3
Mat2DIntSparsaDinamica nuova1=sparsadin.rimuoviColonna(3);
prova.stampaMatrice(nuova1);
// togliamo la riga 0
Mat2DIntSparsaDinamica nuova2=nuova1.rimuoviRiga(0);
prova.stampaMatrice(nuova2);
// aggiungiamo la riga
SparseIntMatrix1D riga=new SparseIntMatrix1D(4);
riga.setQuick(2, 9); // [0 0 9 0]
Mat2DIntSparsaDinamica nuova3=nuova2.aggiungiRiga(riga);
prova.stampaMatrice(nuova3);
// aggiungiamo la colonna
SparseIntMatrix1D colonna=new SparseIntMatrix1D(5);
colonna.setQuick(4, 8); // [0 0 0 0 8]
Mat2DIntSparsaDinamica nuova4=nuova3.aggiungiColonna(colonna);
prova.stampaMatrice(nuova4);
}
catch(Exception e){
System.out.println(e.getMessage());
System.exit(1);
}
public void stampaMatrice(SparseIntMatrix2D sparsa)
{
System.out.println("-----------");
for(int i=0;i<sparsa.rows();i++) // stampa matrice
{
String s="";
for (int j=0;j<sparsa.columns();j++)
s+=" "+sparsa.getQuick(i, j);
System.out.println(s);
}
System.out.println("-----------");
}
di seguito sono riportati i risultati (output su console) del test:
----------1 0 0 0 0
0 2 0 0 0
0 0 3 0 0
0 2 4 0 0
0 2 0 0 5
-----------
--> matrice di partenza
----------1 0 0 0
0 2 0 0
0 0 3 0
--> dopo la rimozione della colonna 3
0 2 4 0
0 2 0 5
--------------------0 2 0 0
0 0 3 0
0 2 4 0
--> dopo la rimozione della riga 0
0 2 0 5
--------------------0 2 0 0
0 0 3 0
0 2 4 0
--> dopo l'aggiunta della riga [0 0 9 0]
0 2 0 5
0 0 9 0
--------------------0 2 0 0 0
0 0 3 0 0
0 2 4 0 0
--> dopo l'aggiunta della colonna [0 0 0 0 8]
0 2 0 5 0
0 0 9 0 8
-----------
NOTE: per completezza è stato verificato anche il lancio delle eccezioni
variando appositamente le dimensioni della riga e della colonna aggiunti,
rendendole (prima una e poi l'altra) incompatibili con la matrice.
Lo stesso per gli indici della riga e della colonna da rimuovere :
- ipotesi di indice negativo.
- ipotesi di indice > indice max.
Altre classi usate:
Un'altra classe che è stata utilizzata nel progetto è la classe OpVettori , in
questa classe sono stati sviluppati una serie di metodi (static) utili per
effettuare le varie operazioni sui vettori (vettori di double e vettori di
interi). In realtà solo una parte di questi metodi si può dire significativa
nell'implementazione delle reti neurali , essi sono:
-double[] aggiungiAVettore(double[] vet, double x)
aggiunge l'elemento x in coda al vettore vet
-double[] copiaDelVettore(double[] vet)
ci restituisce una copia del vettore vet
-double[] differenzaVettori(double[] vet1, double[] vet2)
calcola il vettore differenza tra vet1 e vet2
-double distanzaVettori(double[] v1, double[] v2)
restituisce la distanza tra due vettori
-double distanzaVettoriCoseno(double[] v1, double[] v2)
restituisce la distanza coseno tra due vettori
-boolean elementoPresente(double[] vet, double x)
ci indica se l'elemento x è presente nel vettore vet
-double[] motiplicaPerScalare(double[] vet, double x)
moltiplica il vettore vet per lo scalare x
-double norma(double[] v)
restituisce la norma del vettore, norma 2
-double norma1(double[] v)
restituisce la norma1 del vettore
-double prodottoScalare(double[] v1,double[]v2)
effettua il prodotto scalare dei due vettori
-double[] rimuoviDaVettore(double[] vet, int i)
rimuove l'elemento di indice i dal vettore vet
-double[] sommaVettori(double[] vet1, double[] vet2)
calcola il vettore somma di vet1 e vet2
-double[] vettoreRandom(int dim,double min, double max)
restituisce un vettore a caso di dimensione 3 e con coordinate appartenenti
all'intervallo (min:max)
-double[] vettoreRandom2D(double xmin, double xmax,double ymin,
double ymax)
restituisce un vettore a caso di dimensione 2 e con coordinate appartenenti
all'intervallo (xmin:xmax),(ymin:ymax)
-double[] verroreRandom3D(....
analogo al metodo vettoreRandom2D , restituisce un vettore 3D
-double[] vettoreRandomSfera(double r)
restituisce un vettore a caso di dimensione 3 e con coordinate interne ad una sfera di
raggio r e centro nell'origine
note: Anche per la classe OpVettori è stato effettuato il testing dei metodi,
ed i test sono stati superati.
2.2 Interfaccia Rete Neurale
A prescindere dalla tipologia di rete neurale implementata , ci sono delle
caratteristiche generali comuni ad ogni tipo, come ad esempio l'esser
costituite da nodi oppure la tendenza per cui man mano che la rete
apprende, i nodi tendono a formare dei clusters.
Queste caratteristiche comuni, in un linguaggio Object Oriented come Java
possono essere rappresentate da un'interfaccia, ovvero una sorta di
contratto che obbliga le classi che implementano l'interfaccia a fornire
un'implementazione dei metodi in essa indicati.
Il vantaggio di usare un'interfaccia è soprattutto quello di poter sviluppare
del codice senza la necessità di fare riferimento ad una classe ben definita
di oggetti.
Per esempio le chiamate di metodi che prendono come argomento un
riferimento all'interfaccia, possono essere fatte prendendo come argomento
qualsiasi oggetto che sia definito da una classe che implementi tale
interfaccia.
Inoltre, anche se si fa riferimento ad un interfaccia per chiamare un
metodo, in realtà verrà eseguito il codice della classe di cui fa parte
l'oggetto, ciò è detto polimorfismo, una caratteristica peculiare dei
linguaggi Object Oriented molto utile per la riusabilità del codice e
l'estensione delle applicazioni.
Costituendo l'interfaccia ReteNeurale si vuole ottenere proprio questo,
usare lo stesso codice per i vari tipi di reti neurali implementate.
Ciò è molto utile in questo progetto, perchè sono state sviluppate classi
relative alla sperimentazione che devono essere applicate a tutti i tipi di
reti neurali per ottenere un confronto coerente delle performance.
Anche le classi sviluppate per la visualizzazione grafica della rete neurale
e della sua evoluzione, contengono metodi che richiedono come
argomento un riferimento di tipo ReteNeurale.
Metodi dell'interfaccia ReteNeurale:
- public void aggiungiDato(double[] dato) throws Exception;
sottopone alla rete neurale un nuovo dato di input.
- public int getDimensione();
restituisce il numero di nodi della rete neurale.
- public int getDimensioneNodi();
restituisce la dimensione di ogni nodo della rete neurale ,cioè la
dimensione dello spazio di stato della rete.
-
public double[] getNodo(int indice) throws
IndexOutOfBoundsException;
restituisce le coordinate del nodo identificato dall'indice “indice”
-
public int[] ricerca_connessi(int nodo);
restituisce un vettore che contiene gli indici dei nodi connessi al nodo
-
public int ricercaVincitore(double[] input);
restituisce l'indice del nodo più vicino al dato di input
-
public double getErroreGlobale(ArrayList<double[]> dataset);
restituisce l'errore tra i nodi della rete e i dati del dataset
-
public ArrayList<int[]> getClusters();
restituisce i gruppi di nodi (clusters) formati nella rete
2.3 Classe Gng.java
Implementazione della rete neurale GNG:
ReteNeurale
Gng
La classe Gng contenuta nel package “retineurali” rappresenta
l'implementazione in linguaggio java di una rete neurale di tipo
Growing Neural Gas.
Questa classe implementa i metodi dell'interfaccia ReteNeurale , ognuno
dei quali è stato sviluppato tenendo conto delle particolari caratteristiche
della GNG.
Dall'algoritmo Gng descritto nel capitolo precedente (par 1.4), si evincono
alcune variabili di stato necessarie al corretto funzionamento della rete:
–
L'insieme dei nodi della rete e la rispettiva posizione.
–
l'insieme delle connessioni e la rispettiva età.
–
il vincitore attuale s1 ed il suo secondo s2.
–
l'insieme degli errori locali relativi ad ogni nodo
Inoltre si devono tener presente i parametri costanti :
–
εb , εn
–
αmax
–
λ
–
α
–
d
I valori di default dei parametri sono [2]:
λ=100 , εb=0.2 , εn=0.006 , α=0.5 , αmax=50 , d=0.995
Le variabili d'istanza della classe Gng rappresentano appunto le variabili di
stato ed i parametri della rete.
L'ArrayList nodi rappresenta l'insieme dei nodi di cui è composta la rete.
Ogni nodo della rete è identificato da un indice intero univoco ed è
caratterizzato da una serie di valori di tipo double che ne rappresentano la
posizione all'interno dello spazio di stato, quindi viene stabilita la
corrispondenza:
nodo ==> (int indice , double[] valori).
La struttura dati rappresentata dal contenitore generico
ArrayList<double[]> (package java.util) è quindi adatta a
rappresentare la lista dei nodi.
La variabile d'istanza eta_connessioni è la struttura dati che contiene
l'età di ogni connessione della rete, in pratica stabilisce la corrispondenza:
connessione ==> ([nodo 1 , nodo 2 ], età)
Una matrice è la struttura dati ideale per implementare tale corrispondenza.
Tenendo presente che durante l'apprendimento il numero dei nodi della
rete GNG varia, e che l'interconnessione tra gli stessi, di solito, è ben
lontana dall'esser totale (la non connessione è identificata da età=0), si
evince che una matrice dinamica e sparsa è adatta a rappresentare questa
variabile di stato.
Costruttori della classe Gng
Come visto dall'algoritmo Gng (passo 1), la rete neurale GNG deve essere
inizializzata con solo due nodi.
La classe Gng possiede 4 costruttori :
- public Gng (int dim)
Richiede soltanto di indicare la dimensione del dataset e dei nodi della
rete, cioè dello spazio di stato.
I due nodi iniziali vengono scelti in modo casuale tramite il metodo
double[] verroreRandom(dim,0 , 0.5) della classe OpVettori (package
vettori_matrici), inoltre vengono inizializzate le altre variabili d'istanza e
vengono settati i parametri della rete con i valori di default indicati nel
Fritzke95.
- public Gng (double[] nodo1, double[] nodo2)
Permette di impostare i nodi iniziali della rete, (magari presi a caso dal
dataset di cui si vuole effettuare il clustering).
e poi i costruttori :
–
public Gng (int dim,int l ,double eb ,double en,double
alfa ,int alfa_max, double d)
–
public Gng (double[] nodo1,double[] nodo2, int l ,
double eb ,double en ,double alfa ,
int alfa_max , double d)
Analoghi a quelli visti in precedenza , che però offrono la possibilità di
inizializzare i parametri caratteristici della rete con valori a scelta.
Metodi implementati in Gng.java
Tra i metodi dell'interfaccia ReteNeurale che sono stati implementati nella
classe Gng è particolarmente significativo il metodo:
int[] ricerca_connessi(int indice_nodo) , che restituisce i
connessi al nodo identificato dall'indice “indice_nodo”.
Questo metodo viene richiamato spesso, sia all'interno della classe stessa
(perchè richiesto in molti passi dell'algoritmo Gng : passi 4 , 6 , 9.2 ), sia
dalle altre classi che usano la classe Gng , come ad esempio quelle adibite
alla visualizzazione grafica della rete.
Per trovare i nodi connessi si crea una vista della riga di indice
“indice_nodo” dalla matrice eta_connessioni; essendo la matrice
sparsa, anche questa riga sarà un vettore sparso.
I nodi connessi sono identificati dagli indici dei valori diversi da 0
contenuti in questo vettore (perchè solo le età≠0 rappresentano una
connessione).
Un altro metodo molto importante è:
public ArrayList<int[]> getClusters() che restituisce una lista
contenente i vari gruppi di nodi identificati dalla topologia della rete .
Questa informazione è fondamentale se si usa la rete neurale GNG per il
clustering di dati.
Un esempio dell'applicazione di getClusters() è :
Lo pseudocodice del metodo getClusters() è:
l= lista dei cluster , inizialmente vuota
vis = vettore dei nodi visitati , inizialmente vuoto
per ogni nodo s della rete
{
se il nodo non è in vis
{
nodicluster = vettore dei nodi che fanno parte del cluster,
inizialmente contiene solo s
q = pila dei nodi da espandere, inizialmente contiene solo s
finchè q non è vuoto
{
estrai e togli un elemento u dalla pila q
per ogni nodo v collegato a u
{
se il nodo v non è già in nodicluster
{
aggiungi v al vettore
nodicluster
aggiungi v ai nodi da espandere (pila q)
}
}
}
aggiungi il vettore nodicluster alla lista l
aggiungi i nodi contenuti in nodicluster al vettore dei nodi
visitati vis
}
}
il risultato è la lista dei cluster l
gli altri metodi implementati sono:
public int ricercaVincitore(double[] input) che restituisce
l'indice del nodo più vicino al dato di input .
Viene scandito l'intero ArrayList nodi , per ognuno di essi si calcola la
distanza con l'input tramite il metodo distanzaVettori(input,nodo) della
classe OpVettori (package vettori_matrici) e si tiene traccia dell'indice del
nodo avente distanza minima.
public double getErroreGlobale(ArrayList<double[]>
dataset) che restituisce l'errore tra i nodi della rete e i dati del dataset.
Per ogni dato del dataset trova il nodo della rete meno distante
richiamando il metodo ricercaVincitore(input) visto in precedenza , somma
la relativa distanza ad una variabile e che alla fine sarà l'errore globale
della rete
public void aggiungiDato(double[] dato) throws Exception
Sottopone alla rete neurale un nuovo dato di input.
Prima effettua un controllo sulla dimensione del dato , se non è uguale a
quella dei nodi della rete lancia un'eccezione.
Una volta accertato che l'input è compatibile, chiama il metodo
algoritmoGng(input) che applica l'algoritmo Gng , ciò comporta un
adaptation step della rete neurale.
public double[] getNodo(int indice) throws
IndexOutOfBoundsException
richiama il metodo nodi.get(indice) , ma prima fa un controllo sull'indice e
se non è valido lancia un'eccezione.
public int getDimensione()
richiama il metodo nodi.size()
public int getDimensioneNodi()
richiama il metodo nodi.get(0).size()
Metodo di implementazione dell'algoritmo GNG in Gng.java
il metodo algoritmoGNG(double[] input) rappresenta il metodo
più importante della classe perchè in esso è sviluppata l'implementazione
dell'algoritmo che descrive l'apprendimento della rete neurale GNG.
codice metodo
private void algoritmoGNG(double[] input)
{
int[] s=ricercaPiuVicini(input);
s1=s[0];
//passo 2 GNG Algorithm
s2=s[1];
int[]connessi_vin=ricerca_connessi(s1);
int[] old=incrementaEtaConnessioni(s1,connessi_vin);//passo 3 GNG Algorithm
aggiornaErrore(s1,input);
// passo 4 GNG
Algorithm
muoviVincitore(input,s1);
// passo 5 GNG Algorithm
muoviConnessiVincitore(input,connessi_vin);
resetConnessione(s1,s2);
// passo 6 GNG Algorithm
rimuoviConnessioni(old);
// passo 7 GNG Algorithm
if((n%l)==0)
// passo 8 GNG Algorithm
{
int q=ricercaNodoMaxErrore();
// passo 8.1 GNG Algorithm
int[] connessi_max_errore=ricerca_connessi(q);
int f=ricercaNodoMaxErrore(connessi_max_errore);
double[] nuovo=calcolaNodoIntermedio(q,f);
nodi.add(nuovo);
// passo 8.2 GNG Algorithm
collegaNuovoNodo((getDimensione()-1),q,f);
// passo 8.3 GNG Algorithm
errore[q]=alfa*errore[q];
// passo 8.4 GNG Algorithm
errore[f]=alfa*errore[f];
errore=OpVettori.aggiungiAVettore(errore,errore[q]);
eta_connessioni.trimToSize();
// utile a contenere la memoria occupata
}
for(int i=0;i<errore.length;i++) // passo 9 GNG Algorithm
errore[i]=d*errore[i];
}
Come si vede dal codice quasi tutti i passi dell'algoritmo Gng sono stati
sviluppati in un rispettivo metodo.
La suddivisione dell'algoritmo in più metodi rende l'architettura del codice
modulare, consentendo maggiore flessibilità ai cambiamenti
nell'implementazione di uno o più passi .
Inoltre, essendoci maggiore frammentazione delle funzionalità, nello
sviluppo di ogni metodo si sono dovute affrontare problematiche meno
complesse, con soluzioni semplici, e quindi meno possibilità di ritrovarsi
errori in fase di compilazione.
L'approccio modulare è utile anche in fase di debug, la minor presenza di
istruzioni annidate facilita l'individuazione delle parti di codice che che
provocano gli errori in fase di esecuzione (runtime).
La modularità porta vantaggi anche rispetto alla riusabilità del codice,
perchè una maggiore semplificazione dei metodi di una classe madre
comporta una maggiore probabilità del loro utilizzo da parte delle classi
derivate.
Osservando il codice del metodo, si nota che il nodo più vicino al dato di
input (vincitore) ed il secondo, vengono ricavati nel medesimo metodo
int[] ricercaPiuVicini (input), ciò è stato fatto per ragioni di efficienza,
perchè usare una sola scansione di tutti i nodi per ricavare le relativa
distanza, invece che due (se si usavano due metodi distinti), comporta un
guadagno in termini di velocità di esecuzione dell'algoritmo; questo
aspetto risulta essere cruciale nel caso che la rete neurale debba elaborare
molti dati e di conseguenza giunga ad avere un numero di nodi molto
elevato.
Lo stesso tipo di ragioni sono alla base dell'utilizzo del vettore old, che
contiene i nodi con cui il vincitore (nodo più vicino) ha una connessione
ormai scaduta che deve essere eliminata.
Le connessioni indicate da old sono state individuate durante l'incremento
dell'età delle connessioni che partono dal vincitore ( passo 4), quindi non
c'è bisogno di ricercare le stesse quando si procede all'eliminazione delle
connessioni vecchie ( passo 8). Nello stesso ciclo di scansione si fanno
entrambe le cose (incremento età e ricerca connessioni scadute ) riducendo
quindi il tempo di elaborazione.
2.4
Classe NeuralGas.java
Implementazione della rete neurale NEURAL GAS:
ReteNeurale
Gng
NeuralGas
La classe NeuralGas implementa in linguaggio java una rete neurale di
tipo Neural Gas. Questa classe eredita dalla classe Gng ed implementa
quindi i metodi dell'interfaccia ReteNeurale.
Rispetto alla classe Gng, cambiano le variabili d'istanza relative ai
parametri:
Queste variabili contribuiscono alla determinazione di alcuni importanti
parametri di apprendimento della rete che cambiano ad ogni passo di
apprendimento, in particolare relativamente all'algoritmo Neural Gas visto
nel capitolo precedente (par 1.3):
l_ng = li · (lf / li)
t/tmax
e_ng = ei · (ef / ei)
t/tmax
eta_max = ti · (tf / ti)
t/tmax
==>
coeff. apprendimento λ
==>
coeff. apprendimento ε
==>
età massima connessioni T
I valori di default di queste variabili sono: li = 30 , lf = 0.01 , ei = 0.3 ,
ef = 0.05 , ti = 20 , tf = 200 [1].
C'è da notare che la classe madre Gng possiede già una variabile d'istanza
relativa all'età massima (int alfa_max), la scelta di aggiungere un'altra
variabile (double eta_max) è dovuta principalmente al vantaggio di
osservare meglio i valori che essa assume durante l'apprendimento (tramite
breakpoint in fase di debug); le due variabili, come si vedrà in seguito,sono
comunque correlate da un semplice cast.
Costruttori della classe NeuralGas.java
I costruttori della classe NeuralGas differiscono da quelli della classe Gng,
perchè i nodi iniziali della rete sono più di due:
- public NeuralGas (int dim, int num_nodi)
Richiede, di indicare la dimensione dello spazio di stato ed il numero di
nodi di cui deve essere composta la rete.
I due nodi iniziali vengono scelti in modo casuale tramite il metodo
double[] verroreRandom(dim, 0, 1.0) della classe OpVettori (package
vettori_matrici), quindi all'interno di un ipercubo di lato 1, inoltre
vengono inizializzate le altre variabili d'istanza e vengono settati i
parametri della rete con i valori di default indicati.
- public NeuralGas (ArrayList<double[]> nodirete)
Permette di impostare i nodi della rete, (magari presi a caso dal dataset di
cui si vuole effettuare il clustering), che cui cardinalità rimarrà costante per
tutta la durata dell'apprendimento.
E poi ci sono i costruttori:
- public NeuralGas(ArrayList<double[]> nodirete,double li
,double lf,double ei,double ef,int ti,int tf,int tmax)
- public NeuralGas (int dim,int num_nodi,double li
,double lf,double ei,double ef,int ti,int tf,int tmax)
Analoghi a quelli visti in precedenza , che però offrono la possibilità di
inizializzare i parametri caratteristici della rete con valori a scelta.
Metodi implementati in NeuralGas.java
Rispetto alla Gng, viene aggiunto il metodo getNumeroSteps() che
restituisce il numero di adaptation steps effettuati.
Inoltre vene sovrascritto il metodo aggiungiDato, dove una volta
accertato che l'input è compatibile, viene chiamato il metodo
algoritmoNeuralGas(input) che applica l'algoritmo Neural Gas:
private void algoritmoNeuralGas(double[] input)
{
l_ng=li*Math.pow((lf/li),(double)t/tmax);
e_ng=ei*Math.pow((ef/ei),(double)t/tmax);
eta_max=ti*Math.pow((double)tf/ti,(double)t/tmax);
alfa_max=(int)(eta_max);
int[] k_nodi=ordinaNodi(input);
s1=k_nodi[0];
s2=k_nodi[1];
muoviNodiRete(input,k_nodi);
//passo 2 NG Algorithm
resetConnessione(s1,s2);
// passo 3 NG Algorithm
// passo 4 NG Algorithm
int[]connessi_vin=ricerca_connessi(s1);
// passo 5 NG Algorithm
int[] old=incrementaEtaConnessioni(s1,connessi_vin);
rimuoviConnessioniVecchie(s1,old);
}
// passo 6 NG Algorithm
2.5
Implementazione delle reti asimmetriche in Java
ReteNeurale
Gng
NeuralGas
GngAsimmetrica
NeuralGasAsimmetrica
Le classi implementate: GngAsimmetrica e NeuralGasAsimmetrica
ereditano dalle rispettive classi e sovrascrivono i vari metodi in cui appare
l'aggiornamento di età delle connessioni.
La GngAsimmetrica sovrascrive i metodi protected della classe Gng :
resetConnessione(int a, int b)
eliminaConnessione(int a, int b)
incrementaEtaConnessione(int a, int b)
boolean nodoIsolato(int nodo)
La NeuralGasAsimmetrica similmente sovrascrive gli stessi metodi dalla
classe NeuralGas, tranne nodoIsolato ,che in NeuralGas non è presente.
Capitolo 3
IMPLEMENTAZIONE DELL'ALGORITMO
FUZZY
3.1 Algoritmo di apprendimento fuzzy
Una versione diversa ed innovativa della rete neurale GNG è la sua
versione fuzzy.
La logica di tipo fuzzy prevede che alle connessioni tra i nodi venga
associato un certo peso p Є R, e non una determinata età come visto in
precedenza nella GNG versione classica.
Quando si sottopone un dato di input alla rete neurale Fuzzy, viene cercato
il nodo più vicino (come nella Gng), e vengono aggiornati i pesi delle
connessioni tra il suddetto nodo e tutti gli altri nodi della rete.
Il peso della connessione tra il vincitore (nodo più vicino) ed un altro
nodo viene incrementato maggiormente, se la distanza tra quest'ultimo ed
il dato di input è minore, ovvero il peso della connessione tra il vincitore
ed il secondo nodo più vicino all'input verrà incrementato maggiormente
rispetto al peso della connessione tra il vincitore ed il terzo e così via....
Successivamente tutti i nodi della rete (compreso il vincitore), vengono
avvicinati all'input; lo spostamento è crescente rispetto al peso della
connessione.
Il peso delle varie connessioni è fondamentale per capire la topologia della
rete, infatti anche se tutte le connessioni hanno un certo peso diverso da
zero, saranno le connessioni con peso maggiore ad essere significative
nello stabilire il rapporto di vicinanza tra nodi necessario per il clustering.
Il peso associato alle connessioni inoltre, è importante anche nella fase di
avvicinamento dei nodi stessi ai dati del dataset di input (convergenza
della rete) perchè lo spostamento nello spazio di stato dei nodi dipende
appunto dal peso delle connessioni tra quest'ultimi ed il vincitore.
Come si può notare, un'importate differenza rispetto alla Gng classica è
che ,nella versione fuzzy , ad ogni adaptation step dobbiamo tener conto di
tutti i nodi della rete e non solo di alcuni, perchè ogni volta che alla rete
fuzzy viene sottoposto un dato di input, tutti gli "n" nodi vengono spostati
e tutti i pesi tra gli "n-1" nodi ed il vincitore vengono aggiornati.
Anche nella Gng versione Fuzzy, i nodi iniziali sono 2 e si aggiungono
nuovi nodi alla rete durante l'apprendimento: i nuovi nodi vengono
aggiunti basandosi su alcune misure statistiche dipendenti dall'evoluzione
passata della rete , ma anche tenendo conto del peso relativo ad alcune
connessioni; perciò, l'importanza di una corretta gestione dei pesi delle
connessioni, si nota anche nella fase di crescita della rete.
Algoritmo FUZZY-GNG:
(1)
La rete è composta inizialmente da due nodi “a” e “b” aventi
posizione casuale all'interno dello spazio di stato:
ωa ε Rⁿ
, ωb ε Rⁿ .
(2)
viene sottoposto alla rete un dato di input “ζ”.
(3)
trovare il nodo più vicino "s1" al dato di input
(4)
Aggiungere il quadrato della distanza tra il dato in input ed il
nodo s1, ad una variabile locale (errore) che fa riferimento ad
s1:
(5)
Δerror(s1) = (|| ωs1 – ζ ||)²
incrementare i pesi delle connessioni tra s1 e gli altri nodi della
rete di un incremento:
Δps1,sn = 1 – (
1
-an
1+ e
con an = || ζ – ωsn ||
)
e normalizzare i pesi.
(6)
muovere i nodi della rete verso il dato di input di uno
spostamento pari a:
Δωs1 = b1 · e‾ kw · (1 – Ps1,s1) · || ζ – ωs1 ||
Δωsn = b2 · e‾ kw · (1 – Ps1,sn)
· || ζ – ωn ||
per il nodo s1
per gli altri nodi
con Ps1,sn => peso della connessione tra s1 ed il nodo sn.
(7)
Se il numero di dati sottoposti alla rete finora è un multiplo
intero del parametro “λ”, aggiungere un nuovo nodo alla rete
nel seguente modo:
(7.1) trovare il nodo 'q' che ha accumulato maggior errore
(Δerror)
(7.2) inserire il nuovo nodo 'r' a metà tra il nodo q el il nodo 'f '
a cui corrisponde il maggiore prodotto ( error(f) · Pq,f ) :
ωr = 0,5 · (ωq + ωf)
(7.3) impostare il peso delle connessioni tra il nodo r ed i nodi
q e f, come il peso della connessione tra q ed f:
pr,q = pq,f ;
pr,f = pq,f
settare a zero il peso della connessione tra q ed f:
pq,f = 0
(7.4) ridurre gli errori associati a q ed f, moltiplicandoli per
una costante α < 1 , inizializzare l'errore associato ad r
con il nuovo valore dell'errore associato a q.
(8)
ridurre gli errori associati a tutti i nodi, moltiplicandoli per una
costante d < 1.
(9)
se un criterio di stop non è stato soddisfatto, ritorna al passo 1.
note:
Vediamo dall'algoritmo, che la funzione di incremento dei pesi è una
funzione logistica , che ha un andamento decrescente rispetto all'aumento
della distanza .
La funzione che rappresenta lo spostamento dei nodi verso l'input invece è
una funzione esponenziale con argomento negativo del tipo: eˉ
(1 – peso)
,
essa è crescente per pesi crescenti. Ciò significa che lo spostamento è più
pronunciato se la distanza tra e dato di input è minore (nodo più vicino)
3.2 Strutture dati utilizzate
Relativamente all'implementazione di una rete neurale Gng versione
Fuzzy, serve una matrice che rappresenti i pesi delle connessioni tra i vari
nodi della rete.
A differenza della matrice rappresentativa dell'età delle connessioni vista
nell'implementazione delle reti neurali GNG e NG (capitolo 2), la matrice
rappresentativa dei pesi sarà densa e conterrà valori di tipo double
Per quanto riguarda la creazione e l'utilizzo di matrici dense, la libreria
Parallel-Colt mette a disposizione le seguenti classi:
–
cern.colt.matrix.tdouble.impl.DenseDoubleMatrix1D
–
cern.colt.matrix.tdouble.impl.DenseDoubleMatrix2D
DenseDoubleMatrix2D implementa una matrice densa che contiene
valori di tipo double, questa classe mette a disposizione alcuni metodi
relativi alla possibilità di attuare trasformate di tipo DFT (discrete Fourier
trasformate) o DHT (discrete Hartley trasformate) sulla matrice o sulle sue
righe e/o colonne.
DenseDoubleMatrix1D implementa un vettore denso che è utile ad
esempio nel rappresentare una riga o una colonna della matrice stessa.
Però, visto che il numero di nodi della rete Fuzzy-Gng cresce durante
l'apprendimento, servono una matrice dense che sia anche dinamica,
ovvero che sia possibile variarne le dimensione.
Per implementare tale matrice ho sviluppato la classe:
Mat2DDoubleDensaDinamica
(matrice densa dinamica):
DenseDoubleMatrix2D
Mat2DDoubleDensaDinamica
La classe Mat2DDoubleDensaDinamica eredita dalla classe
DenseDoubleMatrix2D del package
cern.colt.matrix.tdouble.impl della libreria Parallel-Colt,
quindi è un'implementazione valida di una matrice densa.
I metodi che sono stati aggiunti rispetto alla classe madre hanno il fine di
rendere questa matrice densa , una matrice densa dinamica.
Lo scopo di ogni metodo è lo stesso visto per la classe
Mat2DIntSparsaDinamica (par 2.1 pag 26), cioè permettere le operazioni
di aggiunta e rimozione di righe e colonne, per rendere la matrice
dinamica. La maggiore differenza rispetto all'implementazione di
Mat2DIntSparsaDinamica è che ,essendo di fronte ad una matrice densa ,
dovremmo tener conto di tutti i valori della matrice durante le operazioni
di ridimensionamento, ciò comporta una maggiore lentezza nella loro
esecuzione come si può facilmente notare nel codice del metodo
aggiungiRiga(riga):
public Mat2DDoubleDensaDinamica aggiungiRiga(DenseDoubleMatrix1D riga) throws
Exception
{
if (riga.size()!=columns()) // controlla se la riga è della stessa dimensione di
quelle della matrice
throw new Exception("dato non compatibile--> dim riga:
"+riga.size()+"
colonne mat: "+columns());
//nuova matrice, vuota con una riga in più
Mat2DDoubleDensaDinamica nuova=new Mat2DDoubleDensaDinamica(rows()+1,columns());
for (int i=0; i<rows(); i++)
for (int j=0; j<columns(); j++)
nuova.setQuick(i,j,getQuick(i,j));
<== vengono elaborati tutti i valori
for(int i=0;i<riga.size();i++) //inserisco i dati della riga nella nuova matrice
nuova.setQuick(rows(), i, riga.getQuick(i));
return nuova;
}
Anche l'occupazione di memoria è maggiore, sia perchè la matrice è densa
sia perchè contiene valori di tipo double che occupano maggiore quantità
di memoria rispetto agli interi (il double occupa 8 byte).
Come visto per la matrice Mat2DIntSparsaDinamica, la classe è stata
sottoposta ad un test per verificarne le funzionalità, il codice del test è
analogo a quello riportato nel paragrafo 2.1.
Risultati del Test su Mat2DDoubleDensaDinamica
--------------------1.1 0.0 0.0 0.0 0.0
0.0 2.2 0.0 0.0 0.0
0.0 0.0 3.3 0.0 0.0
--> matrice di partenza
0.0 2.2 4.4 0.0 0.0
0.0 2.2 0.0 0.0 5.5
----------------------------------------1.1 0.0 0.0 0.0
0.0 2.2 0.0 0.0
0.0 0.0 3.3 0.0
0.0 2.2 4.4 0.0
0.0 2.2 0.0 5.5
---------------------
--> dopo la rimozione della colonna 3
--------------------0.0 2.2 0.0 0.0
0.0 0.0 3.3 0.0
--> dopo la rimozione della riga 0
0.0 2.2 4.4 0.0
0.0 2.2 0.0 5.5
----------------------------------------0.0 2.2 0.0 0.0
0.0 0.0 3.3 0.0
0.0 2.2 4.4 0.0
--> dopo l'aggiunta della riga [0.0 0.0 9.0 0.0]
0.0 2.2 0.0 5.5
0.0 0.0 9.0 0.0
----------------------------------------0.0 2.2 0.0 0.0 0.0
0.0 0.0 3.3 0.0 0.0
0.0 2.2 4.4 0.0 0.0
--> dopo l'aggiunta della colonna [0.0 0.0 0.0 0.0 8.0]
0.0 2.2 0.0 5.5 0.0
0.0 0.0 9.0 0.0 8.0
---------------------
Package vettori_matrici
Le matrici Mat2DIntSparsaDinamica e Mat2DDoubleDensaDinamica
nonché la classe OpVettori fanno parte del package vettori_matrici che
offre appunto classi per la gestione di vettori e matrici.
3.3 Classe FuzzyGng.java
Implementazione della rete neurale FUZZY-GNG
ReteNeurale
Gng
FuzzyGng
NeuralGas
La classe FuzzyGng contenuta nel package “retineurali” rappresenta
l'implementazione in linguaggio java della versione fuzzy di una rete
neurale GNG (Growing Neural Gas).
Questa classe rappresenta una rete che ha molti meccanismi in comune con
la GNG come si evince osservando i due algoritmi, per questo motivo
eredita dalla classe Gng. Ciò comporta il vantaggio di non dover ripetere
lo sviluppo del codice relativo alle funzionalità già sviluppate in Gng.java.
Analizzando più attentamente l'algoritmo fuzzy, si notano però alcune
differenze sostanziali rispetto alla Gng, di cui si è dovuto tener conto nello
sviluppo della classe FuzzyGng.
Queste differenze si riperquotono subito nelle variabili d'istanza della
classe, che devono rappresentare i nuovi parametri: b1, b2 , kw; e
soprattutto la matrice rappresentativa dei pesi delle connessioni.
La matrice dei pesi , al contrario di quella che rappresenta l'età nella classe
Gng , è una matrice densa perchè ad ogni adaptation step vengono
incrementati tutti i pesi delle connessioni tra il vincitore e gli altri nodi
della rete, ciò implica che conterrà quasi tutti valori diversi da 0.
Allo stesso tempo è una matrice di double perchè la normalizzazione dei
pesi (passo 5) produce valori compresi tra 0 e 1, ed è dinamica perchè il
numero dei nodi della rete, durante l'apprendimento, tende ad aumentare.
Costruttori classe FuzzyGng
I costruttori della classe FuzzyGng sono molto simili a quelli della classe
Gng ; differiscono per lo più sui parametri che si possono impostare (b1,
b2 , kw) per la FuzzyGng, al posto di (λ , εb , εn , α , αmax , d ) per la
Gng.
Sono considerati di default : b1 = 0.2 , b2 = 0.006 , kw = 9.0
Metodi della classe FuzzyGng.java
I metodi che sono stati sviluppati in aggiunta a quelli della classe Gng
sono:
int[] ricerca_connessi(int indice_nodo,double pesosoglia)
Il peso soglia indica il valore minimo del peso, che una connessione deve
avere per rappresentare due nodi connessi topologicamente (facenti parte
dello stesso cluster).
In questo metodo si effettua una scansione della riga relativa al nodo di
indice "indice_nodo" all'interno della matrice dei pesi in cerca di pesi con
valore superiore al "pesosoglia" per stabilire quali sono i nodi connessi.
L'indice di colonna di un peso superiore al peso soglia identifica un nodo
connesso. Questo metodo di ricerca dei connessi viene usato nel metodo
getClusters(double pesosoglia) per ottenere i clusters.
double getPeso(int nodoa, int nodob)
restituisce il peso della connessione tra il nodo "nodoa" ed il nodo
"nodob".
public void aggiungiDato(double[] dato) throws Exception
Dopo aver fatto i dovuti controlli sulla dimensione del dato di input
chiama il metodo algoritmoFuzzyGng(input) che applica l'algoritmo
fuzzy.
Implementazione dell'algoritmo Fuzzy in FuzzyGng.java
Il metodo algoritmoFuzzyGng rappresenta il metodo più importante
della classe perchè in esso è sviluppata l'implementazione dell'algoritmo
fuzzy:
codice metodo
private void algoritmoFuzzyGng(double[] input)
{
int s1=ricercaVincitore(input);
aggiornaErrore(s1,input);
//passo 2 fuzzyGNG
//passo 3 fuzzyGNG
double somma=incrementaPesi(s1,input); // passo 4 fuzzyGng
normalizzaPesi(somma,s1);
// passo 4 fuzzyGng normalizzazione
muoviNodi(input,s1);
// passo 5 fuzzyGng
if((n%l)==0)
// passo 6 fuzzyGng
{
//rimuoviNodiIsolati(); // rimuove i nodi che tendono a rimanere isolati
int q=ricercaNodoMaxErrore();
// passo 6.1 fuzzyGng
int f=ricercaSecondaUnità(q);
double[] nuovo=calcolaNodoIntermedio(q,f);
// passo 6.2 fuzzyGng
nodi.add(nuovo);
collegaNuovoNodo((getDimensione()-1),q,f);
errore[q]=alfa*errore[q];
// passo 6.3 fuzzyGng
// passo 6.4 fuzzyGng
errore[f]=alfa*errore[f];
errore=OpVettori.aggiungiAVettore(errore,errore[q]);
}
for(int i=0;i<errore.length;i++) // passo 7 fuzzyGng
errore[i]=d*errore[i];
}
Valgono le stesse considerazioni fatte nella descrizione del metodo di
implementazione dell'algoritmo in Gng.java, a riguardo dei vantaggi nella
modularità del codice.
note: c'è il metodo commentato rimuoviNodiIsolati che attualmente non è
stato utilizzato, ma che potrebbe essere utile in sviluppi futuri.
Effetti della distanza sulla funzione di incremento dei pesi:
La funzione di incremento dei pesi è una funzione logistica:
Δps1,sn = 1 – (
1
1+ e -an
)
l'esponenziale decrescente al denominatore 1+ e-an dipende dalla distanza
tra il nodo (sn) a cui fa riferimento il peso da incrementare ed il dato di
input:
dist = an = || ζ – ωsn ||
con
Se questa distanza è eccessiva, e
- dist
ζ = input
risulta essere davvero molto piccolo.
Dalle prove effettuate sulla rete fuzzy si è riscontrato, in fase di debug ,
che anche per dataset non eccessivamente dispersivi (dati non troppo
distanti tra loro), il valore assunto da e- dist risulta essere talmente piccolo
che viene approssimato a zero durante l'esecuzione.
Ciò comporta un errore grave perchè la funzione logistica si annulla :
se e- dist
> 0
1–(
1
1+ e-dist
)
>
1-(
1
1
)= 0
Ciò annulla l'incremento dei pesi , e di conseguenza pregiudica
l'apprendimento della rete neurale fuzzy-gng.
Durante le prove infatti si è assistito ad un errore non decrescente quando
la distanza tra dati del dataset era maggiore .
Ad esempio la prova di clustering su dati che nello spazio 3D sono
all'interno di 3 cubi distanti tra loro, fatta con la FuzzyGng, da come esito
una rete che non apprende, se i cubi sono distanziati tra loro di 50.
Una possibile soluzione è inserire un nuovo parametro alla fuzzy del tipo:
1
1–(
1+
e -( g · dist)
)
con 0 < g < 1
Il parametro “g” può abbassare l'ordine di grandezza dell'argomento
dell'esponenziale decrescente.
Es: se dist=50 e g=0.1 ===>
e
-( g · dist)
= e
verrà approssimato a zero durante l'esecuzione.
-( 5 )
che almeno non
3.4 Implementazione versione asimmetrica
La classe FuzzyGngAsimmetrica sovrascrive semplicemente il metodo
protected della classe FuzzyGng impostaPeso, cioè:
protected void impostaPeso(int a, int b, double peso)
{
}
pesi.setQuick(a, b, peso);
al posto del metodo della FuzzyGng (simmetrica):
protected void impostaPeso(int a, int b, double peso)
{
pesi.setQuick(a, b, peso);
pesi.setQuick(b, a, peso);
}
Capitolo 3
VALUTAZIONE SPERIMENTALE
4.1 Convergenza al variare dei parametri e confronto
GNG
errore
Andamento dell'errore al variare del parametro εb
80
70
60
50
Єb=0.05
Єb =0.2
Єb=0.8
40
30
20
10
0
n iterazioni
errore
Andamento dell'errore al variare del parametro εn
90
80
70
60
50
Єn=0.001
Єn =0.006
Єn=0.05
40
30
20
10
0
n iterazioni
note : La valutazione dell'errore è stata fatta su un dataset composto da 200
punti presi a caso all'interno di un cubo (3D) avente lato = 1 .
Il training è costituito da 8000 adaptation steps su dati presi a caso dal
dataset , l'errore è stato ricalcolato per ogni 200 passi.
Le 3 reti sono state inizializzate con gli stessi nodi (2 dati presi a caso dal
dataset) e sono state sottoposte ai medesimi passi di apprendimento.
FUZZY-GNG
Per quanto riguarda la rete neurale implementata nella classe FuzzyGng, è
stato molto importante valutare sperimentalmente l'efficienza della rete al
variare dei parametri di apprendimento, essendo un algoritmo ancora in
fase di sviluppo.
errore al variare di b1
100
90
80
errore
70
60
50
40
.b1=0.05
.b1=0.2
.b1=0.8
30
20
10
0
n iterazioni
errore al variare di b2
90
80
70
errore
60
50
.b2=0.0005
.b2=0.006
.b2=0.05
40
30
20
10
0
n iterazioni
errore al variare di kw
90
80
70
errore
60
50
40
Kw = 1 .0
Kw = 9.0
Kw = 18.0
30
20
10
0
n iterazioni
note: Il dataset, il tipo di training e le altre condizioni di prova sono
analoghe a quelle viste nella Gng (andamento errore al variare dei
parametri).
Confronto convergenza tra GNG, FUZZY-GNG e NG
errore gng - fuzzy gng
90
80
70
errore
60
50
40
GNG
FUZZY-GNG
30
20
10
0
n iterazioni
Le due reti (Gng e Fuzzy-Gng) sono state sottoposte al medesimo training:
stesso dataset, stesso ordine anche se casuale e stesso numero di adaptation
steps.
Anche i nodi iniziali sono gli stessi per entrambe , e sono stati scelti
casualmente nel dataset.
errore gng - fuzzy - ng
80
70
60
errore
50
40
GNG
FUZZY
NG
30
20
10
0
n iterazioni
4.2 Andamento degli indici caratteristici nella Gng:
Lunghezza media
L
5
4,5
4
3,5
3
2,5
2
1,5
1
0,5
0
n iterazioni
C
Coefficiente di clustering
0,7
0,6
0,5
0,4
0,3
0,2
0,1
0
num nodi
n iterazioni
Distribuzione Grado
35
30
25
20
15
10
5
0
1
2
3
4
5
6
7
8
grado
4.3 Valutazione sperimentale delle reti asimmetriche
Attraverso l'utility grafica "Gnuplot" [5], è stato possibile avere una
visualizzazione grafica di una rete neurale Neural Gas Asimmetrica.
I nodi in questa visualizzazione, non sono collegati soltanto da linee,
infatti le connessioni sono rappresentate da frecce che indicano anche il
verso della connessione.
Dalla rappresentazione grafica si può notare come le connessioni tra nodi
vicini sono spesso bidirezionali, ovvero se a e b sono vicini esiste sia la
connessione (a ==> b) che la connessione (a <== b).
Le connessioni tra nodi lontani ,invece, sono spesso unidirezionali.
Questo fenomeno trova spiegazione nel fatto che, durante l'apprendimento,
la rete tende ad costituire connessioni tra nodi che identificano uno stesso
cluster (nodi vicini) e allo stesso tempo eliminare le connessioni residue
tra i nodi di clusters diversi (nodi lontani) .
Non ci sono sostanziali differenze nell'andamento dell'errore rispetto alla
errore
versione asimmetrica delle reti neurali:
errore GNG - GNG Asimmetrica
80
70
60
50
GNG
GNG ASIM
40
30
20
10
0
n iterazioni
errore
errore fuzzy - fuzzy asimmetrica
80
70
60
50
40
F uzzy
Fuzzy asim
30
20
10
0
n iterazioni
L'andamento dell'errore relativo alla rete di tipo Neural Gas è identico a
quello della sua versione asimmetrica perchè il movimento dei nodi verso
l'input non dipende dalla topologia della rete, ma solo dalla distanza tra
quest'ultimo ed ogni nodo.
4.4 Visualizzazione grafica- Clustering
Il package "vis_grafica" è utilizzato per contenere tutte le classi utili per la
visualizzazione grafica dell'evoluzione topologica e spaziale delle reti
neurali.
Le applicazioni che vogliono fare uso di queste classi devono usare un
componente grafico in cui si può visualizzare un disegno 3D: il GLCanvas.
Il GLCanvas è disponibile nella libreria JOGL [6] che implementa le
istruzioni grafiche openGL in java.
Al GLCanvas viene registrato un listener : GraficoGLEventListener
(JOGL) che contiene i metodi per l'aggiornamento grafico del componente
Di conseguenza è stata modificata la classe GraficoGLEventListener, alla
quale si possono dare come parametri al costruttore un elenco di figure, le
quali verranno disegnate sul GLCanvas ad ogni aggiornamento grafico.
Le figure vengono disegnate sfruttando il polimorfismo, perchè le classi
relative ad ogni figura devono tutte implementare l'interfaccia
FiguraOpenGL ed il relativo metodo disegna(GL).
Clustering rete Gng
I punti verdi rappresentano i dati del dataset, che sono uniformemente
contenuti nei 3 cubi disegnati in nero.
I punti rossi rappresentano i nodi della rete implementata dalla classe
Gng , mentre le linee rosse sono le connessioni esistenti tra i vari nodi.
Si nota che la topologia della rete neurale (rossa) all'aumentare degli
adaptation steps (iterazioni), riesce a ricalcare le similarità tra i dati del
dataset , infatti la rete riconosce i 3 cluster rappresentati dai cubi.
Clustering rete NeuralGas
Anche nel caso della rete neurale di tipo Neural Gas, si nota che la
topologia della rete neurale (rossa) all'aumentare degli adaptation steps
(iterazioni), riesce a ricalcare le similarità tra i dati del dataset , infatti la
rete riconosce i 3 cluster rappresentati dai cubi.
Per quanto riguarda la Fuzzy-Gng, si è scelto di usare una classe di test per
il clustering sui 3 cubi (visti in precedenza) che alla fine
dell'apprendimento riporti in 3 file di testo i clusters rilevati dalle reti Gng,
FuzzyGng e NeuralGas, il relativo spezzone di codice è:
ProvaClusteringCubi pc2=new ProvaClusteringCubi(); // classe contenente il main
// I clusters di dati della Gng vengono memorizzati su un file di log
RegistratoreLog rl1=new RegistratoreLog("gruppiclustercubi1.txt");
rl1.salvaVettore(pc2.getElencoClusters(reteneurale1,clusters1, dataset));
System.out.println(rl1.toString());
// I clusters di dati della Fuzzy vengono memorizzati su un file di log
RegistratoreLog rl2=new RegistratoreLog("gruppiclustercubi2.txt");
rl2.salvaVettore(pc2.getElencoClusters(reteneurale2,clusters2, dataset));
System.out.println(rl2.toString());
// I clusters di dati della NG vengono memorizzati su un file di log
RegistratoreLog rl3=new RegistratoreLog("gruppiclustercubi3.txt");
rl3.salvaVettore(pc2.getElencoClusters(reteneurale3,clusters3, dataset));
System.out.println(rl3.toString());
}
Il metodo che estrapola i clusters del dataset tramite la rete neurale è:
public String[] getElencoClusters(ReteNeurale rn,ArrayList<int[]> clusters,
ArrayList<double[]> dataset)
{
int ngruppi=clusters.size();
// costituisce i cluster del dataset
IntArrayList[] gruppi=new IntArrayList[ngruppi];
for(int i=0;i<ngruppi;i++)
gruppi[i]=new IntArrayList();
for(int i=0;i<dataset.size();i++)
{
int vin=rn.ricercaVincitore(dataset.get(i));
for(int j=0;j<ngruppi;j++)
{
int[] gruppo=clusters.get(j);
if (OpVettori.elementoPresente(gruppo, vin))
{
gruppi[j].add(i);
break;
}
}
}
// predispone i cluster ad essere esportati in un file di testo
String[] gruppitesto=new String[dataset.size()+ngruppi];
int k=0;
for(int i=0;i<gruppi.length;i++){
for(int j=0;j<gruppi[i].size();j++){ // inserisce la
parola nel gruppo di parole
double[] dato=dataset.get(gruppi[i].getQuick(j));
gruppitesto[k]="
"+dato[0]+"
"+dato[1]+"
"+dato[2];
k++;
}
gruppitesto[k]="";
k++;
// lascia una riga tra un gruppo ed un'altro
}
return gruppitesto;
}
Da un'analisi dei 3 file di testo è stato osservato che i clusters individuati
dalle 3 reti sono identici ed esatti, essendo corrispondenti ai gruppi
rappresentati dai tre cubi.
4.5 Sperimentazione su Dataset di prova
Implementazione delle reti neurali con metrica coseno
ReteNeurale
Gng
Gng
Coseno
Gng
Asimmetrica
FuzzyGng
FuzzyGng
Asimmetrica
FuzzyGng
Coseno
NeuralGas
NeuralGas
Asimmetrica
NeuralGas
Coseno
In alcune ambiti, le reti neurali devono operare su dataset in cui si fa
riferimento alla metrica coseno e non a quella euclidea per ottenere la
distanza tra due punti appartenenti allo spazio di stato .
prodotto scalare (a,b)
cos(α) =
|| a || * || b ||
Distanza
coseno = (1 ― cos(α))
per implementare il calcolo del nuovo tipo di distanza nelle varie classi di
reti neurali si è semplicemente sovrascritto il metodo
double distanza(double[] input,double[] nodo)
sviluppato in Gng,
chiamando il metodo distanzaVettoriCoseno(input,nodo) della libreria di
funzioni rappresentate dai metodi della classe OpVettori , ad esempio per
la classe GngCoseno.java:
protected double distanza(double[] input,double[] nodo)
{
return (1-OpVettori.distanzaVettoriCoseno(input,nodo));
}
La sperimentazione su dataset di prova è stata effettuata sottoponendo ad
una rete neurale di tipo GNG, implementata nella classe Gng.java, una
serie di dati ed osservando il risultato del clustering ottenuto tramite la rete
stessa.
Il dataset di prova è composto dalle parole contenute nei lanci di un'
agenzia di stampa, ed i valori che caratterizzano le varie parole, sono
identificativi della frequenza con cui la data parola compare in ogni
articolo.
Questi valori sono espressi in metrica coseno, quindi per il test, è stata
utilizzata l' implementazione delle reti neurali in metrica coseno.
I dati elaborati fanno riferimento a 9100 parole contenute in 600
documenti che rappresentano le news date dall'agenzia .
Al termine della prova si osservano vari gruppi di parole (clusters), ogni
gruppo dovrebbe contenere un'insieme di parole che spesso compaiono
insieme nello stesso articolo.
Da un'analisi dei vari clusters si trova conferma a quanto ipotizzato ,
perchè si nota una correlazione tra le parole contenute in uno stesso
cluster.
Ad esempio , in diverse elaborazioni del dataset con la rete neurale (nodi
iniziali diversi, diverso ordine degli adaptation steps) si osserva sempre la
presenza di un cluster contenente la parola “dollaro” e la parola “yen”,
Queste due parole sono evidentemente correlate dal fatto di essere
entrambe identificative di una valuta, e logicamente compaiono spesso
insieme nelle stesse news.
Quindi si può supporre ragionevolmente che la rete neurale adempie al
compito di clustering sul dataset in questione.
CONCLUSIONI
Dalla valutazione sperimentale si osserva che la versione Fuzzy
dell'algoritmo di apprendimento è valida per quanto relativamente alla
convergenza della rete neurale con i dati del dataset.
Per quanto riguarda il clustering, si può stabilire che l'individuazione dei
clusters della rete si complica per via della necessità di determinare il peso
soglia che differenzi una connessione significativa da una trascurabile
però, i molti esperimenti condotti in tal senso hanno dimostrato che, con il
peso soglia appropriato, i clusters estrapolati sono coerenti con il
clustering effettuato tramite la GNG o la NG.
Il confronto sull'efficienza, in termini di difficoltà computazionale e quindi
di velocità nell'esecuzione dell'algoritmo, per adesso, va a favore delle
versioni "classiche" (Gng,NeuralGas); infatti nella versione Fuzzy i pesi
associati alle connessioni sono considerati dei valori double, perciò la
funzione di incremento dei pesi ed il calcolo dello spostamento di tutti i
nodi (ad ogni adaptation step), sono relativamente più pesanti rispetto
all'incremento dell'età delle connessioni e rispettivo spostamento di alcuni
nodi, che avviene nelle reti neurali "classiche".
In conclusione, la versione Fuzzy dell'algoritmo di apprendimento
rappresenta senz'altro un'innovazione molto importante nel campo delle
reti neurali; attualmente è meno conveniente rispetto alle vecchie versioni,
ma rivoluziona in maniera significativa i meccanismi di apprendimento
della rete, perciò è logico pensare che valga la pena continuarne la fase di
sviluppo.
L'implementazione delle reti neurali descritte nei capitoli precedenti, ha
richiesto molte delle potenzialità che un linguaggio Object-Oriented come
java offre, ad esempio l'ereditarietà ed il polimorfismo.
L'augurio è che le classi sviluppate per redarre questa tesi, potranno essere
riusate anche in futuro, soprattutto per la continuazione del lavoro di
sviluppo dell'algoritmo Fuzzy.
BIBLIOGRAFIA
[1] Thomas Martinetz, Klaus Shulten 1991:
A "Neural Gas" Network Learns Topologies
[2] Bernd Fritzke 1995:
A Growing Neural Gas Network Learns Topologies
[3] Rossano Gaeta (Università degli studi di Torino) :
Network complessi e modelli
[4] http://acs.lbl.gov/software/colt/index.html
[5] http://www.gnuplot.info/
[6] http://java.net/projects/jogl/
Scarica

UNIVERSITA` POLITECNICA DELLE MARCHE