QUI - mtcube

Fondamenti della
programmazione in Java
e introduzione
all’apprendimento
automatico.
Note delle lezioni di laboratorio di programmazione.
7 febbraio 2014
Buoncompagni Luca
[email protected]
Università degli studi di Milano
Indice
Indice
4
1 Introduzione al Corso
1.1 Note dell’autore . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Programma del Corso . . . . . . . . . . . . . . . . . . . . . . . .
5
5
6
2 La programmazione di un Calcolatore
2.1 Introduzione . . . . . . . . . . . . . . . . .
2.2 Tipi di Dati . . . . . . . . . . . . . . . . .
2.3 Operazioni su Dati . . . . . . . . . . . . .
2.4 Cicli e Rami Decisionali . . . . . . . . . .
2.5 Classi e struttura del codice . . . . . . . .
2.6 Esercizio 2.0: Hello World test con Eclipse
2.7 Esercizio 2.1: Ordinare un’array numerico
2.8 Esercizio 2.2: Ordinamento alfabetico . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
9
9
11
14
16
19
24
28
32
3 Polimorfismo ed API in Java
3.1 Polimorfismo . . . . . . . . . . . . . . . . . . . . . . . .
3.2 API: Application Programming Interfaces . . . . . . . .
3.3 Esercizio 3.0: Importare una Libreria Esterna su Eclipse
3.4 Esercizio 3.1: Scrittura e lettura su file . . . . . . . . . .
3.5 Esercizio 3.2: Miglioramento delle capacità di una classe
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
35
35
40
42
43
44
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4 Interfacce Grafiche
45
4.1 Benefici della Virtualizzazione della JM . . . . . . . . . . . . . . 45
4.2 Swing & awt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
4.3 Esercizio 4.0: Creazione di una GUI con WindowsBuildePro . . . 48
4.4 Esercizio 4.1: Struttura delle classi di una GUI . . . . . . . . . . 50
4.5 Esercizio 4.2: Creare un file cliccabile per lanciare un programma 55
5 Fondamenti di Artificial Neural Networks
5.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . .
5.2 La forma del Data Set . . . . . . . . . . . . . . . . .
5.3 il neurone artificiale: Perception . . . . . . . . . . .
5.4 Multi-layer Perception . . . . . . . . . . . . . . . . .
5.5 Esercizio 5.1: Implementare un Perception . . . . . .
5.6 Esercizio 5.2: Organizzazione delle classi in un MLP
3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
57
57
58
59
62
67
68
6 Fondamenti di Machine Learning attraverso le Neural Network
6.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.2 Stima corretta della funzione logica Xor . . . . . . . . . . . . . .
6.3 Identificazione di volti umani Maschili o Femminili da Immagini
6.4 Confronto tra risultati ottenuti con diversi parametri e algoritmi
6.5 L’importanza del Data Set . . . . . . . . . . . . . . . . . . . . . .
7 Soluzione agli Esercizi proposti
7.1 Esercizio 2.2: Ordinamento alfabetico . . . . . . . . . .
7.2 Esercizio 3.1: Scrittura e lettura su file . . . . . . . . . .
7.3 Esercizio 3.2: Miglioramento delle capacità di una classe
7.4 Esercizio 4: Interfacce Grafiche . . . . . . . . . . . . . .
7.5 Esercizio 5.1: Implementare un Perception . . . . . . . .
7.6 Esercizio 5.2: Organizzazione delle classi in un MLP . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
69
69
70
73
77
79
81
. 81
. 84
. 88
. 89
. 95
. 100
8 Appendice A: File Manager API
103
9 Appendice B: Perception, classi usate
111
List of Link
120
Capitolo 1
Introduzione al Corso
1.1
Note dell’autore
Questo documento contiene una linea guida alle lezione di laboratorio sulla programmazione in Java per il corso di Scienze Cognitive e Processi Decisionali dell’Università degli Studi di Milano. L’elaborato non intende essere un manuale alla programmazione e si consiglia vivamente di vedere riferimenti allegati per una
descrizione più formale e meglio contestualizzata della funzionalità proposte da
Java. Infatti, il laboratorio ha l’obiettivo di porre le basi della programmazione
a oggetti e dare indicazioni su dove e come ricercare informazioni a riguardo
senza potersi, purtroppo, soffermare sulle infinite potenzialità di tale linguaggio. Questo tipo di trattazione permette di apprendere capacità pratiche non
solo limitate all’utilizzo della macchina virtuale Java (JWM), ma anche pronte
per essere usate da un qualsiasi teorico calcolo numero; come quello applicato
nel Machine Learning (ML).
Internet è saturo di informazioni riguardanti il fondamenti della programmazione in generale, tanto quanto per affrontare i problemi pratici nell’implementazione attraverso SDK (software development kit). Questa eccessiva mole
di informazione può creare problemi per trovare la giusta parola chiave da dare
ad un motore di ricerca per carpire informazioni utili al suo utilizzo. Per aiutarvi in questo snaturerò la lingua italiana con termini in inglese che spesso sono
proprio le giuste parole da inserire in Google e capire come si fa una certa operazione, per esempio. Suggerisco fortemente a tutti di documentarsi attenendosi
a fonti scritte in inglese, questo non solo da la possibilità di avere un numero
di informazioni infinitamente più grande di quello che si possa avere in italiano,
ma evita traduzioni spesso non coerenti e fonte di incomprensioni. Tuttavia, per
completezza, propongo qui una lista di risorse consigliate per avere informazioni
riguardo la programmazione in Java:
• ORACLE1 è una delle associazione sviluppatrice di Java. Da questo sito
è possibile ottenere informazioni sulle varie versioni del software, sui bug o
releases. Inoltre fornisce il download gratuito e le guide di installazione dei
pacchetti standard di Java. Di particolare interesse, c’è anche un tutorial2
interattivo per principianti e non.
• Stack Overflow3 e un forum completo e molto frequentato che raccoglie
domande su tutti i linguaggi di programmazione. La sezione dedicata a
5
Java è già ampia e perciò capita di rado di dover proporre una nuova
domanda.
• un’altro ottimo modo per apprendere i concetti fondamentali è quello di
seguire gratuitamente le video-lezioni4 del corso CS106A del professore
Mehran Sahami per l’univeristà di Stanford.
• Infine, tra i tanti libri disponibili sui fondamenti della programmazione ad
oggetti consiglio Java 2 by Example, consultabile da questo link5 .
Questi documenti collezionano la maggior parte di quello che serve sapere per
poter programmare, ulteriori riferimenti più specifici ai casi trattati saranno
proposti in seguito.
1.2
Programma del Corso
Il corso si sviluppa in quattro capitoli, ognuno di questi è diviso in una parte
teorica ed una pratica, basata sull’utilizzo dell’IDE (integrated development
environment) Eclipse 3.76 . Quest’ultimo è un editor di testo in grado di semplificare lo sviluppo di un programma grazie alle sue funzioni di intercettazione
di errori e suggerimenti.
Nel primo Capitolo verranno analizzati i concetti fondamentali per implementare una generica logica di programmazione. Questo richiede di definire
una notevole mole di concetti usati nei codici e che hanno significati specifici
che influenzano il comportamento delle programma che si vuole scrivere. Tali
concetti non verranno trattati tutti nel dettaglio ma si darà solo una carrellata
generale, soffermandosi su quelli più importanti. I link di sopra contengono ogni
possibile approfondimento a riguardo. Per fissare e comprendere meglio il comportamento di questi concetti verranno proposti alcuni esempi ed esercitazioni
preliminari.
Il secondo, si focalizza sul saper comprendere ed utilizzare librerie esterne.
Si vedrà inoltre come queste possano essere utilizzate per ampliare facilmente le
capacità del programma che si vuole scrivere. In particolare, l’esempio pratico
proposto a riguardo sarà quello di manipolare file testuali.
Continuando, il terzo capitolo contiene un’introduzione alla creazione di interfacce grafiche attraverso alcuni pacchetti standard di Java. Inoltre, per l’esempio proposto in questo capitolo, verrà analizzato un software in grado di auto
generare il codice che descrive una porzione di grafica impostata attraverso l’utilizzo del mouse (Drug and Drop). In analogia con il primo capitolo la trattazione
proposta qui non vuole essere completamente esaudiente ma proporre solo alcuni punti di partenza per quello che è un campo della programmazione molto
ampio. Come sopra, i link della sezione precedente contengono la maggior parte
degli approfondimenti a riguardo.
Infine, il quarto capitolo, contiene una descrizione preliminare di una Rete
Neurale e una possibile sua implementazione. Inoltre, verranno velocemente
affrontati anche alcuni concetti legati all’implementazione dei processi necessari alla validazione di un algoritmo statistico. Gli esempi per questo parte
delle lezioni saranno basati sul riconoscimento di semplici funzioni logiche e di
immagini di volti umani.
Come ultima nota personale vorrei ringraziare Manuela Testa per alcune
gentili correzioni e la professoressa Folgieri Raffaella per la cordiale disponibilità.
Capitolo 2
La programmazione di un
Calcolatore
2.1
Introduzione
Una domanda che nasce spontanea nell’introdursi all’uso di un calcolatore elettronico è: Quale è la peculiarità che permette di utilizzarlo in un numero veramente alto di applicazioni?. Le risposte possono essere tante ma a mio avviso
quella che si distingue tra tutte e quella chiamata Modularity7 . Utilizzando
un approccio intuitivo, la modularità è quella capacità di incapsulare una certa
operazione come se fosse una scatola che prende in ingresso alcuni parametri
e restituisce un informazione di uscita. La scatola, in questa analogia, si dice
modulare quando non si necessita di altre informazioni per poterla usare. Ergo,
potete completamente ignorare i meccanismi che risiedono al suo interno. Un
esempio palpabile di questa proprietà è quello di un comune browser che può
essere usato per esplorare risorse di diversa natura ignorando completamente
tutti i complessi meccanismi che risiedono dentro la comunicazione internet.
Questo concetto è estremamente importante per riuscire a programmare in
modo soddisfacente e si traduce in modo pratico sulla suddivisione di un problema complesso in più sotto-problemi semplici. Se questi vengono risolti adeguatamente risulterà possibile chiudere la scatola e non dovremmo più preoccuparci
se lo stesso problema si presenterà nuovamente. Inoltre, e forse più importante,
diventerà più semplice usare complesse rete di procedure (scatole) aumentando
così le capacità del software che si sta creando. Questo ultimo concetto viene
chiamato dalla comunità come un aumento del livello di astrazione ed è quello
che ha reso la programmazione così popolare. Un esempio pratico può essere
visto durante lo sviluppo di un’interfaccia grafica, in fatti nel momento in cui
si vuole creare una finestra dello schermo non dovremmo preoccuparci di come
il pixel reagisce ad una diversa intensità di corrente ma basterà dare le sue
coordinate x e y del piano formato dallo schermo. Capite bene che questo semplifica notevolmente la programmazione. La traduzione da coordinate spaziali
e intensità di corrente da mandare allo schermo è gestita da un basso livello
di astrazione, solitamente un driver video, e su di esso si poggiano altri livelli
come ad esempio quello del sistema operativo su cui lavorate. Questo basso liv9
Figura 2.1: grafica rappresentazione di programmazione modulare per livelli di
astrazione.
ello è stato incapsulato molto bene e vi permette di usare la grafica rimanendo
completamente allo scuro di tutti questi passaggi.
Un modo intuitivo di rappresentazione del flusso di informazioni all’interno del calcolatore è proposto in figura 2.1. In questo grafico si vede come un
problema complesso come quello di gestire input, output e manipolazione di
dati all’interno di un computer, è stato suddiviso in molti piccoli sotto-problemi
più semplici (rappresentati come rettangoli o scatole nell’analogia di prima).
Da notare che queste sono organizzate in modo gerarchico, quindi è possibile
che un software sia formato da un’insieme di più procedure a loro volta incapsulate. Durante questo corso ci collocheremo al livello di astrazione più alto
e scriveremo le procedure rappresentate da un rettangolo vuoto. Allo stesso
tempo però è importante sapere che utilizzeremo le procedure rappresentate
come scatole nere, senza sapere quali sono i complessi processi che avvengono
al loro interno. Per capire da dove deriva il nome: innalzamento di astrazione,
basta considerare che al livello più basso, quello macchina, le uniche operazioni
possibile sono quelle Booleane della And e Or logica. Mentre, come vedremo,
al livello di applicazione la complessità delle operazioni possibili con una sola
riga di comando sono ben più complesse. In questo scenario, si può dire che i
compiti dello sviluppatore software sono quelli di: saper suddividere un problema complesso in tanti sotto problemi, scrivere le istruzioni che risolve ognuno
di questi problemi in modo il più possibile indipendente dagli altri, infine, ma
non meno importante, gestire in modo efficiente la comunicazione tra uscite e
ingressi delle diverse soluzioni implementate (rappresentate in figura come frecce). Inoltre è importante sapere che ogni volta che un nuovo programma viene
scritto e necessario compilarlo, cioè darlo in pasto ad un software in grado di
percorrere questa sorta di piramide dal livello più alto al più basso. Il suo com-
pito è quello di tradurre le linee di comando scritte dallo sviluppatore in un
file che contiene solo i simboli 0 e 1 interpretabili dalla macchina. Quando si
vuole usare quel determinato programma basterà lanciare questo file tradotto e
la CPU del calcolatore svolgerà i calcoli specificati.
Fino ad ora abbiamo visto molto velocemente e intuitivamente come si struttura un generico codice di programmazione. Ma rimane ancora in sospeso cosa
si trova dentro alle scatole. Più formalmente questa viene detta funzione o metodo che implementa, in certo linguaggio di programmazione, un algoritmo8 . La
definizione di algoritmo può essere controversa e complicata, e dal punto di vista
scientifico si basa sulla macchina di Turing. Tuttavia, in modo più intuitivo lo
si può definire come:
Un algoritmo è un insieme ordinato di istruzioni
non ambigue che, terminando in un tempo finito,
risolvono una determinata classe di problemi.
Caratteristica comune a tutti gli algoritmi è che questi hanno dei parametri
in ingresso e, dopo un certo tempo, restituiscono un’uscita. Successivamente
vedremo come gestire input e output per fare in modo che più algoritmi possano
collaborare tra di loro ma prima verranno proposte alcune sezioni per descrive
quali sono le istruzioni che possono essere inserite all’interno di un algoritmo
implementato in Java.
Infine è importante notare che per controllare in modo soddisfacente un
flusso di dati così complicato tra algoritmi con basso e alto livello di astrazione
è indispensabile una cura vagamente maniacale dell’ordine con cui il software
viene progettato e scritto. La prima regola è quella di usare nomi che descrivano
la loro funzione in modo tale che il linguaggio di programmazione risulti il più
possibile vicino al linguaggio naturale. Altrettanto importante è la cura dell’indentazione e delle convenzioni9 tipicamente usate nei programmi. Inoltre è
possibile introdurre commenti direttamente nel codice in modo che non vengano
considerati come linee di comando ma solo come informazione per lo sviluppatore. Infine è possibile creare una documentazione che sia automaticamente
generata e standardizzata sotto forma di pagine web html: JavaDoc10 .
2.2
Tipi di Dati
Fondamentalmente un algoritmo non fa altro che manipolare dati in ingresso
per ottenere un risultato voluto in uscita. Tuttavia, dato che quando viene
eseguito, ogni programma è tradotto in 0 e 1 dal compilatore, c’è bisogno di
descrivere ogni possibile tipo di dato in questi termini. Per questo motivo c’è
bisogno ad esempio di dare un significato al simbolo 5, questo può essere trattato
come un numero intero, oppure come un carattere alfanumerico o addirittura
come il mese di Maggio. Vedremo successivamente come tipi di dati possano
essere definiti e quindi creati a piacimento dello sviluppatore, ma è bene sapere
che esistono, oltre hai dati primitivi11 ,alcuni tipi predefiniti e ampiamente
utilizzati:
• Boolean è dato logico, può assumere valori true o false.
• Integer è dato numerico di tipo intero (senza virgola) positivo o negativo
• Float e Double sono dati numerici di tipo reale (con la virgola) positivi
o negativi.
• Char carattere alfanumerico. L’alfabeto e vari simboli sono descritti in
modo sequenziale dalla tabella ASCII12 estremamente importante per
compiere operazione sui caratteri trattandoli come numeri.
• String13 è un vettore di Char e quindi definisce una successione ordinata
di caratteri; una parola per esempio. Nel codice le stringe di caratteri
vengono sempre scritte tra apici (es: questa è una stringa testuale).
• Array14 è una generica collezione ordinata di dati tutti dello stesso tipo,
è definito dal simboli [] dopo il nome del tipo di dato. Ad esempio una
Stringa è definita come Char[] mentre un vettore di numeri interi: Integer[]. Si può adottare questo tipo di notazione per ogni possibile tipo di
dato. Gli elementi all’interno della successione sono indirizzati attraverso
un indice intero che indica la posizione di un elemento nella successione; è
bene ricordare che il valore di questo indice parte da 0 fino a (dimensione
dell’array - 1). La loro dimensione è sempre fissa e quindi è bene usarli
quando si è sicuro che la successione non varia mai la sua lunghezza.
• ArrayList<?>15 è una generica collezione ordinata che si comporta
in modo simile agli Array ma è in grado di modificare la sua massima
lunghezza. Dove il simbolo ? può essere tradotto in un qualsiasi tipo di
dato. Ad esempio, un testo può essere pensato come una successione di
linee (String) e quindi: List<String>.
• HashSet<?>16 si comporta come una List ma non garantisce che i suoi
componenti siano sempre ordinati allo stesso modo. Per questo motivo
non ha senso usare un indice per recuperare i dati al suo interno ma se ne
può solamente chiedere un generico elemento.
• HashMap<?,?>17 costituisce una tabella che mette in relazione una
key con un value senza un preciso ordine. Ad esempio un elenco telefonico
potrebbe essere descritto come HashMap< String, Integer> dove una
Stringa testuale definisce la chiave: il nome della persona. Mentre il valore
di questa mappa è un numero intero che definisce il numero di telefono
di quella determinata persona. Alternativamente, rimane corretto definire
una rubrica anche come HasMap< String, String>.
Gli ultimi quattro elementi di questa lista prendono il nome di tipi composti, perché sono una collezione di più dati semplici. Tuttavia è sempre possibile creare
dati composti di dati composti, quindi ad esempio può esistere ArrayList<
ArrayList< Integer> > che rappresenta una tabella (matrice) di valori interi. Analogamente, se la struttura non cambia di dimensione e la tabella ha un
numero di righe uguali a quelle di colonne si può usare un Array bidimensionale definito come: Integer[][]. Questa struttura potrebbe essere utilizzata per
rappresentare tutti i pixel di una foto, ad esempio. Un altro esempio potrebbe
essere quello di voler implementare un videogame inspirato alla battaglia navale;
dove il valore 0 indica l’assenza di una barca, 1 che la barca c’è, 2 che è affondata. Ovviamente si può creare strutture dati di qualsiasi dimensione, e non
solo su due come in questo esempio. In questa lista sono riassunti solo i tipi più
comunemente utilizzati, ma ne sono disponibili molti altri. La scelta di un tipo
di dato rispetto ad un altro dipende da come quel determinato dato deve essere
manipolato.
Il processo di creazione di un qualsiasi tipo di dato può essere fatto inserendo
nel codice la linea di comando:
T ipoV ariabile nome = new T ipoV ariabile();
(2.1)
ad esempio:
1
2
3
4
Integer x = new Integer();
String text = new String();
ArrayList< Boolean> listOfFlags = new ArrayList< Boolean>();
HasMap< String, String> = new HashMap< String, String>();
Se prendiamo in considerazione la prima linea del codice, ma il discorso è analogo
per tutte, questa chiede al compilatore di creare un’area di memoria vuota, cioè
con il valore null, adatta per contenere un numero intero; L’area di memoria
appena creata avrà la possibilità di essere indirizzata attraverso il suo nome x.
Il dato null è un dato particolare che sta ad indicare l’assenza di informazione
associata alla variabile x, quindi non ha senso fare operazioni su questo tipo di
dato perché non si conosce il loro valore. Ad esempio, il risultato di
x + 1 = null + 1 = ??
NullPointerException18
che restituisce un errore o Exception. L’unica operazioni che è possibile fare
appena il dato è stato creato è quello di assegnargli un valore iniziale, ad esempio:
1
2
x = 12;
x + 1;
ora la cella di memoria indirizzata da x non è più vuota ma contiene il numero
12. Cosi, il risultato della seconda operazione non è più quello di generare
un’errore, ma quello di restituire un valore pari a 13. Questo sta ad indicare che
ogni variabile che si crea deve essere inizializzata ad un valore prima di essere
utilizzata, altrimenti il compilatore ferma il programma e notifica un’ eccezione.
Per i dati composti come: array, List, Set, Map il processo di inizializzazione
è più complesso ma automaticamente gestito alla sua creazione. Tuttavia, per
strutture più complesse è necessario farlo manualmente19 . Per inizializzare una
matrice, ad esempio, c’è bisogno di creare l’area di memoria vuota pronta a
contenere le colonne della tabella usando il comando:
1
new ArrayList< ArrayLisr< Integer>>
e successivamente, per ogni colonna, dovremmo inizializzare le sue righe ripetendo l’operazione
1
new ArrayLisr< Integer>
per tutta la sua lunghezza.
2.3
Operazioni su Dati
Abbiamo visto come dati possono essere strutturati in diverse forme, e vedremo
come la loro forma può cambiare sostanzialmente il tipo di operazioni richieste
per arrivare allo stesso risultato. Per fare questo c’è bisogno di fare un passo
avanti e vedere come i dati possano essere manipolati. Abbiamo già visto l’operazione di assegnazione definita con il simbolo =. Questa fa in modo che il nome
di una variabile (x nella sezione precedente) sia collegata ad una particolare area
di memoria che contiene il valore di quella variabile. Grazie a questa definizione
rimane intuitivo leggere la seguente successione di linee di comando:
1
2
3
4
5
Integer x = new Integer();
Integer y = new Integer();
x = 12;
y = x;
y = y + 1
Da notare però, che questa non è l’operazione di uguaglianza comunemente descritta con il simbolo =. Infatti il valore di x rimane sempre 12 mentre quello
di y è 13, cioè la quarta linea non descrive che x e y sono uguali. In particolare, queste poche linee vengono lette durante l’esecuzione in modo sequenziale.
Quindi prima si crea un’area di memoria vuota collegata al nome x, e poi una
riferita a y. Successivamente si inizializza la memoria riservata a x con il valore
intero 12 e poi si inizializza l’area di memoria di y allo stesso valore di quello che
c’è attualmente in x. In altre parole si fa una copia del valore della variabile x
(=12) e lo si posiziona all’interno della variabile y che era vuota. Alla fine, nella
quinta linea, si fa ancora lo stesso procedimento: si copia il valore dato dall’operazione (y + 1) (=13) e lo si pone all’interno della variabile y sovrascrivendo il
precedente risultato di 12.
Durante questo esempio si è visto il simbolo che identifica l’operazione di
somma “+”, e allo stesso modo si possono usare sottrazione “-”, moltiplicazione
“*”, divisione “/”, elevamento a potenza “ˆ” e resto della divisione intera, detto
modulo “%” ad esempio (5 % 2) vale 1 (molto usato per sapere se un numero
è multiplo di un altro, in particolare α è multiple di β se (α mod β) = 0).
Inoltre ma non meno importante parentesi tonde si possono usare per esprimere
una priorità di calcolo per ogni tipo di operazione; esattamente come utilizzato
comunemente in matematica. Questo tipo di operazione nasce per essere utilizzata prevalentemente su dati di tipo numerico, a eccezione del simbolo “+”
che, se viene usato tra dati di tipo String indica la concatenazione di più serie
di caratteri in una sola.
Come abbiamo già visto nell’introduzione, l’unica cosa che il calcolatore è in
grado di fare al livello di astrazione più basso sono le operazioni logiche, dette
anche booleane e si usano solo con i dati di tipo Boolean. Queste si possono
usare scrivendo: “&&” per la And logica (il risultato è vero solo se tutte le
variabili sono vere), la OR logica: “||” (il risultato è vero anche se una sola
variabile è vera) e la NOT, negazione logica: “!”.
Altro, e ultimo tipo di operazione tra dati elementari sono quelle di controllo.
Ad esempio si può ricevere una riposta positiva (true) se due variabili sono uguali
utilizzando il simbolo “==”, oppure se sono diverse scrivendo “!=”, “>” per
maggiore, “<” per minore e “<=” o “>=” per le comparazioni non strette.
quindi risulta pienamente corretto scrivere ad esempio queste linee di seguito
all’esempio precedente:
1
2
3
4
5
6
7
8
x = ( ( y * x)^ 2) / (( y + ( x / 19));
y = x % y; //qualsiasi calcolo
x == y;
//fornisce il valore true o false
Boolean b = new Boolean();
Boolean b1 = new Boolean;
b = true;
a = true;
( ! ( a && b)) // implementa la Nand, fornisce vero se non sono mai
tutte e due vere
Ancora una volta queste sono le operazioni più comunemente usate ma ne esistono altre, vedere qui per dettagli20 e schematizzazione. Da notare che nei
commenti sopra viene usata la parola: fornisce; vedremo successivamente cosa
è possibile fare il valore fornito.
Risulta estremamente importante da tenere a mente il fatto che operazione
si possano fare solo ed esclusivamente tra tipi di dati dello stesso tipo. In linea
teorica perché non ha senso sommare un valore Integer (12) ad un valore Boolean
(false). In linea pratica perchè l’area di memoria in cui vengono conservati
i valori sono di grandezze diverse, quindi un Integer occupa più memoria di
un Boolean. Consideriamo il seguente esempio per analizzare meglio questa
considerazione:
1
2
3
Integer x = 15;
String str = "1875";
x + str
in questo caso il risultato della terza operazione risulterà pari ad una stringa
concatenata del tipo: “187515”. Mentre invece ci aspetteremmo il risultato 1890,
che si otterrebbe nel caso in cui str sia dello stesso tipo di x: Integer. Per ovviare
a questo problema si può pensare di convertire un dato String in uno Integer e
poi fare l’operazione di addizione:
1
2
3
4
Integer x = 15;
String str = "1875";
Integer strInt = Integer.valueOf( str);
x + strInt
e in questo caso si vede che il risultato della quarta operazione è: 1890. Dove
l’istruzione valueOf(. . . ) permette di convertire una serie di caratteri in un
numero intero. Questo meccanismo è compatibile per la maggior parte dei dati
semplici, quindi ad esempio è corretto scrivere Bollean.valueOf(. . . ). Un’altra
conversione ampiamente usata è quella da da numero a stringa di caratteri, ad
esempio queste linee
1
2
3
Integer x = 15;
String str = "1875";
x.toString() + str
ritornano lo stesso risultato del primo esempio: “187515”. Nel mondo pratico
quasi tutti i tipi di dati hanno il modo per essere convertiti e quindi nascono
così moltissimi metodi che possono essere usati per convertire dati di natura
diversa. Il metodo più generalmente usato è quello del Casting21
1
2
3
4
Integer x = 15;
String str = "1875";
Integer strInt = (Integer) str;
x + strInt
che ritorna nuovamente con il risultato numerico 1890. Dove, alla terza riga,
tra parentesi tonde si indica il tipo di dato in cui si vuole convertire.
2.4
Cicli e Rami Decisionali
Abbiamo visto i tipi di dati e alcune possibili operazioni che si possono fare
con essi. Tuttavia, spesso nasce la necessità di dovere ripetere le operazioni
per un certo determinato numero di volte o di dover decidere se svolgere delle
operazioni o altre. Vediamo ora alcune statament che ci permettono di compiere
queste operazioni.
1
2
3
4
5
6
7
8
9
10
11
12
13
// inizializza automaticamente la variabile
Boolean b = new Boolean( true);
Integer i = new Integer( 10);
Boolean f = ! b; // not b
if( i >= 11){
// se il risultato e’ true
// esegui una serie di operazioni
....
} else {
// se il risultato e’ false
// esegui un’altra serie di operazioni
....
}
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if( b){
// se b e’ true
// esegui una serie di operazioni
....
} else if( f && (i < 11)){
// se b e’ false e se f e’ true e se (i < 11) e’ true
// esegui un’altra serie di operazioni
....
} else {
//se b e’ false e se (f && (i < 11)) e’ false
// esegui ancora un’altra serie di operazioni
....
}
Questo esempio mostra come si può scrivere un ramo decisionale if else. Prima
viene mostrata la versione più semplice della struttura, mentre dopo una più
complessa. In particolare, se l’espressione all’interno delle parentesi tonde dopo
la parola chiave if è vera, o se il suo valore lo è in caso sia un booleano, solo le
linee di codice all’interno delle prime parentesi graffe verrà eseguita; altrimenti
verranno eseguite quelle dopo la parola chiave else. Si noti che non c’è limite al
numero massimo di strutture intentabili ma nel caso in cui ce ne sia bisogno di
molte risulta più comodo la struttura switch-case. Un suo esempio tratto dal
tutorial Oracle:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Integer month = new Integer( 8);
String monthString = new String();
switch (month) {
case 1: monthString = "January";
break;
case 2: monthString = "February";
break;
case 3: monthString = "March";
break;
case 4: monthString = "April";
break;
case 5: monthString = "May";
break;
case 6: monthString = "June";
break;
case 7: monthString = "July";
break;
case 8: monthString = "August";
break;
case 9: monthString = "September";
break;
case 10: monthString = "October";
break;
case 11: monthString = "November";
break;
case 12: monthString = "December";
break;
default: monthString = "Invalid month number";
break;
}
In pratica la struttura controlla se la variabile month è uguale ad un valore indicato dalla parola chiave case, se si svolge le operazioni al suo interno altrimenti
svolge le operazioni definite dalla parola chiave default. Il comando di break
può essere utilizzato in qualsiasi statament e indica al compilatore di uscire dallo
stesso. Ad esempio, considerando che non siano presenti le istruzioni di break
nel precedente esempio e che month sia uguale a 11 il programma assegnerebbe
a month “November”, poi “December” e infine “Invalid month number”. Infine è importante ricordare che, di defualt, l’operazione case prende solo valori
numerici.
Tuttavia a volte può capitare di dover fare un numero di operazioni non
ben definito o comunque molto alto. Per questo ci vengo in aiuto i cicli (loop
statament) che possono avere tre forme diverse ma analogamente funzionali.
1
2
Integer limit = new Integer( 100000);
Integer counter = new Integer( 0);
3
4
5
6
7
8
9
while( count < limit){
// esegui alcune operazioni
// fino a che la condizione
// (count < limit) e’ true
....
counter = counter + 1;
}
Questo è l’ esempio di un ciclo While. In particolare le operazioni all’interno
delle parentesi graffe vengono eseguite fino a che l’espressione definita accanto
alla parola chiave è true. In questo caso il numero di volte che le operazioni
vengono eseguite è di 99999 perché il controllo è fatto in testa. Infatti, quando
la variabile counter diventa uguale a quella di limit il ciclo termina. Alternativa
a questo difetto è quella di utilizzare un ciclo Repeat Until come nel prossimo esempio dove l’istruzione while e stata impostata per ciclare all’infinito e
l’istruzione di break viene usata per uscire dal ciclo quando una determinata
condizione si verifica. Da notare che, visto che il controllo è fatto in coda, questa
volta le operazioni verranno svolte 100000 volte.
1
2
3
4
5
6
7
8
9
10
Integer limit = new Integer( 100000);
Integer counter = new Integer( 0);
while( true){
// cicla sempre
....
if( count < limit){
break; // esci dallo statament
}
counter = counter + 1;
}
Ultimo tipo di statament per implementare i cicli viene usato per semplificare
la scrittura del codice quando il numero di volte per cui e necessario ciclare è
sempre costante.
1
2
3
4
5
6
7
8
9
Integer arraySize = 5; // lunghezza del vettore
// creo un’array di 5 elementi vuoti
Integer[] array = new Integer[ arraySize];
// per tutti gli indici che sono all’interno dell’array
for( Integer index = 0; index < arraySize; index++){
// inizializzo tutti gli elementi dell’array a 0
array[ index] = 0;
....
}
Da notare che l’istruzione index++ è uquivalente a scrivere: index = index +
1. Inoltre è importante vedere che anche in questo caso il numero di volte per
cui si cicla è arraySize-1 che è corretto visto che un array di 5 elementi ha gli
indirizzi che variano tra 0 e 4. Java inoltre propone una versione semplificata di
questo tipo di struttura particolarmente utile per i dati di tipo composto perché
ci permette di trascendere dal concetto di indice. Ad esempio
1
2
Integer arraySize = 5; // lunghezza del vettore
ArrayList< Integer> array = new ArrayList< String>( arraySize);
3
4
5
for( Integer arrayElement : array){
array.add( 0);
}
Questo caso il risultato ottenuto è lo stesso di quel precedente, cioè si crea un
vettore di 5 componenti e li si inizializzano tutti a 0. Da notare la sintassi della
quarta riga che significa: per il successivo elemento del array, arrayElement di
tipo Integer, svolgi le operazioni tra graffe. Se l’array non ha altri elementi esci.
Questo ciclo risulta utile anche per leggere facilmente gli elementi di una certa
struttura dati. Consideriamo di avere una HashSet già inizializzato a qualche
valore incognito che vogliamo copiare in un’altra variabile, si può scrivere
1
2
3
4
5
// data la variabiledi nome set di tipo HasSet<String> gia’
inizializzata
HashSet< String> copy = new HashSet< String>();// creo un Set vuoto
for( String str : set){ // per ogni elemento
copy.add( str); // copia un elemento
}
L’ultimo tipo di statament che si può avere in Java viene usato per intercettare eventuali errori che possono avvenire nel codice. In particolare alcuni
metodi possono riportare un Exception; ad esempio durante l’apertura di un
file si può avere un FileNotFoundException. Per questo, la funzione definita
dalla parola chiave Trhow permette di ricevere un errore e propagarlo ulteriormente fino al livello più alto che reagisce bloccando l’esecuzione del programma
e stampando a video il tipo di errore. Altrimenti si può decidere di intercettarlo
per compiere delle operazioni su di esso. In quest’ultimo caso la sintassi da
utilizzare è:
1
2
3
4
5
6
7
8
9
10
try{
// qualche operazione che puo’ generare errore
....
} catch( Exception ex){
// se l’errore si genera questa parte del codice viene eseguita
e poi si esce dalla struttura
...
} finally {
// se l’errore non e’ stato generato, e se le operazioni di
sopra sono finite esegue questi comandi e poi esci dalla struttura
...
}
2.5
Classi e struttura del codice
Fino ad ora abbiamo visto come si definiscono alcuni tipi di dati e come questi
possono essere manipolati attraverso operatori matematici e logici possibilmente
in modo ripetitivo o decisionale. Seguendo l’analogia della prima sezione di
questo capitolo abbiamo visto come una scatola possa essere plasmata con il
fine di implementare un determinato algoritmo. Analizziamo ora come questo
algoritmo possa essere incapsulato all’interno di una funzione richiamabile da
altre parti del codice. Lo scopo di questo procedimento e quello appunto di
isolare sotto-problemi in modo che il codice risulti più robusto, flessibile e di più
facile comprensione e sviluppo.
Tuttavia per riuscire a comprendere a pieno questo meccanismo, e quindi
cominciare a programmare in maniera opportuna in Java c’è bisogno di conosce
il concetto di Object, da cui deriva il nome Object Oriented Programming
Language, di cui Java è un esempio. Praticamente quello che caratterizza la
Figura 2.2: Astrazione intuiva del ruolo di Object, Class e Instances variables.
programmazione in Java è il fatto che ogni entità che si può definire, e quindi usare, è un Object. Come si nota dal grafico intuitivo di 2.5 tutto quello
che Java mette a disposizione è di un tipo di dato primitivo: l’oggetto. Gli
oggetti hanno funzioni e caratteristiche particolari che affronteremo a breve ma
quello che è importante capire è cosa sia veramente in grado di fare la parola
chiave new. In pratica ogni volta che viene utilizzata, questa crea un’istanza
dell’oggetto chiamato. Instance diverse, come ad esempio il numero intero a e b
sono completamente indipendenti tra loro a meno di particolari condizioni. Una
nuova classe, che definisce un determinato oggetto, creata dallo sviluppatore è
soggetta agli stessi processi di qualsiasi altro. Un problema non banale, che è
stato risolto dai sviluppatori di Java, è quello di regolare il flusso di dati tra
le varie Instance, che possono essere anche nell’ordine delle centinaia di unità,
in modo che siano sempre coerenti. Per fare questo sono state introdotte le
seguenti parole chiave che possono essere utilizzate per qualificare una variabile
o una funzione:
• this: identifica l’Instance corrente.
• public: permette a qualsiasi Instance di accedere a questo dato.
• private: permette solo alla Instance this di accedere a questo dato.
• static: indica che questo dato è condiviso tra tute le Instance appartenenti
allo stesso insieme. In questo contento la parola chiave this e new non ha
alcun significato e quindi non può essere utilizzata.
• final: indica che il dato, dopo essere stato creato, non può essere modificato da nessuno. Utilizzato per definire principalmente costanti.
ancora una volta queste sono quelle più usate, ne esistono altre, come ad esempio
protected o synchronized usate in progetti più complessi di quelli affrontati
in questo corso.
Prima di poter analizzare la struttura di una classe ci manca un ultimo
concetto: la sintassi di base per una generica funzione o metodo.
1
2
3
4
5
6
modifiers OutputDataType methodName( InputDataType1 inputParam1Name,
InputDataType2 inputParam2Name, ...){
//body of the method
...
return( outputParameter) // of type OutputType
}
un’esempio più pratico può essere:
1
2
3
4
5
6
7
8
9
10
static public String getDataTime( String dataFormatParam){
// create un nuovo oggetto di tipo Date
Date date = new Date();
// create un nuovo oggetto di tipo SimpleDateFormat inizializzato
con la stringa di formattazione
SimpleDateFormat dateFormat = new SimpleDateFormat( dataFormatParam);
// fai il parsing (conversione) dall’oggetto alla stringa
String str = dateFormat.format( date);
// ritorna la stringa alla funzione chiamante
return( str);
}
Questa è una metodo di nome: getDataTime, che richiede in ingresso una variabile di tipo String che assume il nome di dataFormatParam. Infine questa
funzione restituisce una stringa che contiene la data corrente visualizzata in
accordo con il parametro di ingresso. Ammettiamo che questo metodo risieda all’interno della classe di nome DataClass sarà possibile scrivere in un’altra
parte del codice:
1
String actualData = DataClass.getDataTime( "yyyy/MM/dd HH:mm:ss");
e cosi il valore della variabile actualData data sarà per esempio: “2013/10/15
16:16:39”. Per maggiore informazioni rispetto alla stringa di formattazione per
la classe SimpleDateFormat, vedere qui22 . Se ad esempio volessimo stampare
a video una generica stringa potremmo pensare di creare il metodo:
1
2
3
public void printString( String input){
System.out.println( input); // stampa a video
}
Si noti che la funzione non ritorna nessun tipo di parametro in uscita e quindi
si deve utilizzare la parola chiave void ed eliminare return. Ammettendo
che anche questa funzione sia all’interno della classe DataClass, sarà possibile
chiamarla digitando le seguenti linee di programmazione in un qualsiasi altro
punto del codice:
1
2
3
4
5
DataClass instance = new DataClass(); // costruttore vuoto
// chiamata ad un metodo statico
String actualData = instance.getDataTime( "yyyy/MM/dd HH:mm:ss");
// chiamata ad un metodo dinamico, ho bisogno dell’Instance variable
instance.printString( actualData);
Ho introdotto in questi esempi il concetto di classe anche se ancora non si è visto come sono definite. Tuttavia risulta davvero importante capire come diversi
oggetti vengono creati e trasferiti da un metodo ad un altro in modo da implementare un programma che abbia il comportamento voluto. Fate attenzione
hai nomi delle variabili e al loro valore. Inoltre cercate di ricostruire i passaggi
sequenziali che vengono effettuati partendo dalla funzione chiamata, descritta
nelle ultimo set di linee. In pratica quando un generico metodo viene chiamato
da un altro questo deve fornire dei parametri in ingresso che siano compatibili
con il tipo richiesto. Dopo di che il passaggio dell’esecuzione del programma
viene dato alla funzione chiamata che fa una copia dei parametri di ingresso,
li manipola per arrivare ad una certo risultato e quando ha finito è costretto
a ritornare un tipo di dato sempre coerente con la sua definizione. A questo
punto il comando dell’esecuzione ritorna al programma chiamante che passa all’istruzione successiva. Questo è uno dei concetti più importanti per imparare
la programmazione, e se volete approfondire le vostre conoscenze propongo qui
un interessante parte del tutorial23 .
Ora siamo finalmente pronti per vedere la struttura completa di una classe,
definita dalla parola chiave class. Per convenzione tutte le classi hanno un
nome che inizia con la lettere maiuscola, nel nostro esempio MyClass, nomi non
posso contenere spazi, devono essere diversi da ogni parola chiave e possono
contenere numeri a patto che non siano posti come primo carattere. I nomi
delle costanti vengono solitamente descritti con lettere tutte maiuscole. Tutte
le classi presentano un constructor che è quel particolare metodo che viene
lanciato automaticamente quando si crea una nuova Instance, questo metodo
non ritorna nessun valore e non necessita della parola chiave void, infine deve
avere il nome esattamente uguale a quello della classe. Solitamente questo viene
utilizzato per inizializzare i diversi attributi della classe. Gli attribute che sono
variabili visibili in qualsiasi parte delle classe stessa. Si noti che variabili definite in un certo punto del programma sono visibili solo all’interno delle parentesi
graffe che la contengono, buona norma per creare un codice incapsulato è quella
di avere variabili che siano visibili per la più piccola porzione di codice possibile.
Ergo le variabili create come attributi devo essere del minor numero possibile.
Infine ci sono i vari metodi che la classe implementa, la loro forma dipende
strettamente da quello che la classe intende implementare. Un tipo particolare
di metodi vengono detti Getter e Setter e servono principalmente per poter
fornire in uscita a acquisire in ingresso attributi. Per fare questo potremmo pensare di impostare gli attributi come pubblici e renderli accessibili dall’esterno,
tuttavia questo porta alla creazione di un programma poco modulare e quindi
è vivamente sconsigliato. Un generale esempio di una classe può essere scritto
come nell’esempio successivo che vuole essere una linea guida nella stesura e
comprensione delle diverse parti che formano una classe, ma che alla fine sono
sempre definite come variabili ed il loro tipo, e come funzioni e il loro tipo di
ingresso e uscita.
1
2
// full package qualifier
package package.name.path.to.MyClassName;
3
4
5
6
// importa classi esterne da instansiare per creare oggetti
import java.lang;
import ...
7
8
9
10
11
12
13
// dichiarazione della classe
public class MyClassName{
// ###### attribute DELLA CLASSE ############
// esempio di una costante stringa
// (si intende implicitamente new String("stringa sempre costante"))
public static final String MY_CONSTANT_STRING = "stringa sempre
costante";
14
15
16
17
// esempio di una variabile condivisa in tutta la classe
private Integer counter = 0; // (si intende implicitamente: new
Integer(0))
private static Boolean created = false; // new Boolean( false)
18
19
20
21
22
23
// ###### CONSTRUCTOR DELLA CLASSE ############
public MyClassName( DataType inputParameter){
// il costruttore viene chiamato automaticamente quando la
classe viene creata utilizzando l’istruzione new. Solitamente e’
utilizato per inizializzare i attribute.
...
}
24
25
26
27
28
29
30
31
// ###### METHOD DELLA CLASSE ############
// metodo che puo’ essere chiamato solo dalla classe stessa
private Boolean checkState(){
// metodo senza parametri in ingresso che puo’ fare una generica
operazione, ad esempio controllare lo stato di attributi e reagire
in base hai loro valori.
...
return( ...);
}
32
33
34
35
36
37
// esempio di metodo che puo’ essere chiamato da una qualsiasi
classe esterna che conosce questa istanza
public void printState(){
// metodo senza parametri ne in ingresso ne in uscita che puo’
fare una generica operazione, ad esempio stampare sullo schermo i
valori dei attribute
...
}
38
39
// metodi setter e getter indispensabili per far modificare, in
ingresso ed uscita un attributo da una classe esterna, visto che
questi devono essere privati a meno che non abbiano il modificatore
configurato come final.
public void setCounter( Integer counterIn){
this.counter = counterIn
}
public Integer getCounter(){
return( this.counter);
}
40
41
42
43
44
45
46
// esempio metodo statico accessibile dall’esterno. Questo puo’
modificare solo variabili statiche.
public static Boolean isCreated(){
return( created);
}
47
48
49
50
51
}
2.6
Esercizio 2.0: Hello World test con Eclipse
Ora che sono state introdotti i concetti fondamentali della programmazione in
Java passiamo alla parte pratica e cerchiamo di fissarli meglio. Il primo esempio
introduce l’editor di testo Eclipse ed implementa una versione leggermente più
complessa del popolare test Hello World. Questa documentazione suppone di
avere già installato il software development kit SDK 6.0, che contiene la Java
Virtual Machine JVM, tipicamente installata in ogni computers per lanciare e
quindi utilizzare qualsiasi applicazione Java, e anche alcune librerie volte a creare
e compilare nuovi programmi. Inoltre, si utilizzerà un famoso integrated development environment IDE in grado di gestire complessi progetti semplificando
notevolmente lo sviluppo.
Vediamo come è possibile creare un esempio per testare che l’installazione
e andata a buon fine e per prendere confidenza con la parte pratica della programmazione. Per prima cosa aprite ECLIPSE, vi comparirà una finestra che
chiede di selezionare una cartella. Questa conterrà tutte le configurazioni di
ECLIPSE e anche i programmi in fase di sviluppo; tipicamente prende il nome
di WorkSpace. Una volta creata e confermata l’operazione, si apre una scheda
di benvenuto, una volta chiusa e accettato il cambio al Prospective di default si
arriva alla schermata base dell’editor. Per creare un nuovo progetto selezionate:
File
New
Project
Java Project
e premete nel tasto Next. La successiva finestra richiede il nome del vostro
progetto, ad esempio helloWorld, e mostra le configurazioni standard di un
progetto Java. Premete Finish e così creerete il progetto. Ora è possibile vedere
sulla sinistra della finestra la gerarchia degli oggetti a disposizione. Questa
contiene una cartella vuota: src, ed una serie di librerie caricate di default
durante la sua creazione. Dentro questa cartella risiederanno tutti i programmi
durante il loro sviluppo.
Qualsiasi programma necessita di un punto da cui far partire la sua esecuzione, in termini più informatici un Main method che risieda dentro una
classe. Per crearla cliccate con il tasto destro del mouse sulla cartella src, vista
prima, e poi digitate:
New
Class
Facendo così si apre una finestra che contiene le impostazioni di default per
creare una classe e richiede: il suo nome, ad esempio MainMethod ed un’eventuale nome del pacchetto, che potete lasciare vuoto. Prima di premere Finish
ricordatevi di spuntare l’opzione:
1
public static void main(String[] args)
per indicare che questa è un tipo di classe speciale, quella che darà inizio all’esecuzione. Una volta premuto Finish si nota che questa operazione ha creato un
file testuale che descrive la classe appena creata. Ripetete la stessa procedura
per crearne una nuova con il nome HelloWorld ma questa volta inserite nel campo Package il nome: tests. La classe che si vuole creare è di tipo standard quindi
non risulta necessario cambiare nessuna opzione. Una volta premuto Finish, a
sinistra della schermata, si potrà notare la presenza di una nuova classe all’interno del pacchetto test. Modificate il file di testo appena creato in modo che
risulti:
1
package tests;
2
3
public class HelloWorld {
4
5
6
7
8
/**
* Stringa constante da visualizzare di default nella console
*/
private static final String textToDisply = "hello world !!!";
9
10
11
12
13
/**
* Stringa da visualizzare nella console
*/
private String text = null;
14
15
16
17
18
19
20
21
/**
* Costruttore della classe, e’ il primo metodo a partire.
* Setta la stringa da stampare di default
*/
public HelloWorld(){
text = textToDisply;
}
22
23
24
25
26
27
28
29
30
/**
* Se chiamata restituisce il testo da visualizzare.
*
* @return il testo da visualizzare
*/
public String getText() {
return text;
}
31
32
33
34
35
36
/**
* Se chiamata sostituisce l’attuale stringa da
* visualizzare con quella esterna.
*
* @param text la nuova stringa da visualizzare
*/
public void setText( String externalText) {
text = externalText;
}
37
38
39
40
41
/**
* Se chiamata scrive sulla console la stringa testuale
* presente in questa classe. Se questa e’ uguale a quella
* di default restituisce vero alla funzione che
* l’ha chiamata, altrimenti ritorna false.
*
* @return vero se la stringa e’ uguale a quella di default
*/
public Boolean printOnConsole(){
// scivi stinga sulla console
System.out.println( text);
42
43
44
45
46
47
48
49
50
51
52
53
// se la stringa e’ uguale a quella di defualt
// ritorna vero, altrimenti false
if( text.equals( textToDisply)){
return( true);
} else {
return( false);
}
54
55
56
57
58
59
60
}
61
62
63
}
Inoltre, modificate la prima classe creata, MainMethod, in modo che risulti
uguale a:
1
2
// importa la classe e rendila visibile
import tets.HelloWorld;
3
4
public class MainMethod {
5
6
7
8
9
10
11
/**
* lancia il programma hello world.
*
* @param args parameteri non usati
*/
public static void main(String[] args) {
12
13
14
15
16
// crea una nuova classe hello world e salvala nella
variabile di nome "cl"
HelloWorld cl = new HelloWorld();
// stampa la stringa di defualt e ignora il risultato di
ritorno
cl.printOnConsole();
17
18
// ottieni la stringa conservata nella classe cl e
salvala nella variabile di nome str
String str = cl.getText();
// aggiungi un’altri caratteri nella stringa
str = str + " -- altri caratteri --";
// risetta la stringa nella classe h
cl.setText( str);
// stampa la nuova riga e conserva il risultato vero se
e’ uguale a quella
// di defualt nella variabile flag
Boolean flag = cl.printOnConsole();
19
20
21
22
23
24
25
26
27
// controlla se la stringa e’ uguale a quella di default
checkChanges( flag);
28
29
30
// crea una nuova classe HelloWorld con nome cl2
indipendente da cl
HelloWorld cl2 = new HelloWorld();
// stampa il testo e controlla se e’ quello di default
checkChanges( cl2.printOnConsole());
}
31
32
33
34
35
36
/**
* Dato un valore vero o falso in ingresso questo metogo
* scrive su schermo se la stringa e’ quella di default o meno.
*
* @param bool
*/
public static void checkChanges( Boolean bool){
if( bool == true){
// se si, stampa nella console che la stringe
non e’ stata cambiata
System.out.println( "la stringa non e’ stata
cambiata");
} else {
// se no, stampa che la stringa e’ stata cambiata
System.out.println( "la stringa e’ stata
cambiata");
}
}
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
}
Questo semplice programma implementa una classe in grado di stampare
una stringa testuale e usa il metodo main per gestirla. Il codice è ampiamente
commentato, o come si usa in termini informatici self-documented, e propone
un ottimo punto di partenza per abituarsi al tipo di formalismo generalmente
utilizzato nei linguaggi di programmazione. Per lanciare il programma basterà
cliccare nella barra alta di ECLIPSE su:
Run
Run
Una volta lanciato tutto, il metodo main(. . . ) viene eseguito sequenzialmente
ed il risultato delle operazioni verrà visualizzato nella console che si trova in
basso. A meno di errori si potrà leggere:
Figura 2.3: gerarchia delle classi
hello world !!!
hello world !!! – altri caratteri –
la stringa è stata cambiata
hello world !!!
la stringa non è stata cambiata
Inoltre è importante notare la gerarchia delle classi che abbiamo appena
creato. Da qui si nota che è possibile descrivere le classi in modo gerarchico
e questo è ben visibile sotto forma di cartelle se va all’interno di src presente
nel workspace di Eclipse. Tale proprietà suggerisce come classi possano essere
definite in modo gerarchico, questo permetterebbe di fornire gerarchie logiche
tra classi che descrivono diversi oggetti. In fatti in maniera similare all’organizzazione ontologica, le classi possono essere relazionate tra di loro fornendo così
un potentissimo software perfettamente modulare. Si è detto che tutto quello che è descritto in Java è un’Object, ad esempio String, Integer, cl, cl2. . . ,
in realtà anche HelloWorld è un Object. Un oggetto di tipo molto particolare
chiamato Class; questo è un file testuale in grado di essere compilato insieme
all’istruzione new, così da poter richiedere al calcolatore di creare e manipolare
nuovi oggetti. In particolare, il primo oggetto che l’istruzione crea, sarà dello
stesso tipo della classe: cl, cl2. Così facendo, è vero anche che i file testuali possono avere delle relazioni logiche tra di loro, ma non è detto che gli oggetti che
ne vengono creati lo abbiano. Per esempio una persona potrebbe appartenere
alla classe degli animali così come un cane, ma non è detto che si influenzano
a vicenda. Certo rimane che una parte della definizione di persona e uguale a
quella di un cane, sono entrambi animali.
2.7
Esercizio 2.1: Ordinare un’array numerico
Qui si propone lo sviluppo di una classe in grado di ordinare un vettore numerico
in modo crescente o decrescente. Inoltre questa classe presenta anche la capacità
di tenere a memoria del proprio stato, quindi di ricordarsi cosa ha fatto la volta
prima che è stata chiamata. Si consiglia di guardare la classe SimpleSorter in
modo da capire come è strutturata, per poi seguire passa passo, partendo dalla
prima all’ultima istruzione del metodo main della classe Runner, saltando parti
dei codici chiamate da ciascuna funzione in modo sequenziale. Lo scopo è capire
qual’è il valore delle variabili ad ogni instante computazionale e perché viene
generato quel particolare output testuale.
1
package tests;
2
3
public class SimpleSorter {
4
5
6
7
8
// valore constante da settare di default
public static final Integer[] DEFAULT_TEST =
new Integer[]{
5,12,9,8,2,6,6,2,2,8,0,3,2,1,56,45,6,43,6,5,12,43,265,
4765,756,43,4234,48,77,63,21,54,69,1,0,39,78,54};
9
10
11
12
13
14
15
16
// attribute, rappresentano lo stato della classe (cosa sa di
se).
// la lista data da ordinare
private Integer[] toSort;
// ultima lista ordinata
private Integer[] sorted;
// ultima lista sorted deriva da quella toSort, o ne e’ stata
immessa un’altra? si, no
private Boolean isSorted = false;
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// constructor di defualt
public SimpleSorter( Integer dimension){
// crea l’oggeto: array di interi vuoto
toSort = new Integer[ dimension];
// inizializza l’array toSort con i primi dimension-1
elementi di DEFAULT_TEST
for( int i = 0; i < dimension; i++){
toSort[ i] = DEFAULT_TEST[ i];
}
}
// constructor configurabile
public SimpleSorter( Integer[] incomingTest){
// il dato in input e’ considerato gia’ creato e
inizializzato
toSort = incomingTest;
}
32
33
34
35
36
37
38
39
40
41
42
// algoritmo che implementa ordinamento per irsezione,
// se parametero in ingresso e’ true ordina in modo crescente,
// se e’ falso in modo decresente
public Integer[] sortList( Boolean orderInputPar){
// per tutti gli elementi dell’array
for(int i = 1; i < toSort.length; i++) {
Integer key = toSort[i]; // ottieni un’elemento (i-esimo)
Integer k = i - 1;
//while(( k >= 0) && (toSort[k] > key)) { //implements
only ordine crescente
while(( k >= 0) && ( ascendingOrdering( toSort[k], key,
orderInputPar))) {
43
44
45
46
47
48
49
50
// in modalita’ crescente: se negli elementi
prima c’e’ ne uno maggiore spostalo piu’ avanti
toSort[k + 1] = toSort[k];
k--;
//eventualmente stampa passo passo
dell’algoritmo.
//System.out.println( this.toString());
}
toSort[k + 1] = key;
}
51
52
53
54
// ora che l’operazione e’ stata fatta aggiorna lo stato
della classe
sorted = toSort;
isSorted = true;
55
return( sorted);
56
57
}
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// metodi privati per rerndere l’algoritmo modulare
private Boolean ascendingOrdering( Integer toSortElement,
Integer key, Boolean order){
if( order){ // true -> ascendending order
if( toSortElement > key){
return( true);
} else {
return( false);
}
} else { // false -> discendending orger
if( toSortElement < key){
return( true);
} else {
return( false);
}
}
}
75
76
77
78
79
80
81
82
83
84
85
// getter e setter per il campo toSort
public Integer[] getToSort() {
return toSort;
}
public void setToSort(Integer[] toSort) {
this.toSort = toSort;
// se e’ stat settata una nuova lista da ordinare
// non sara’ di sicuro gia’ stata ordinata
this.isSorted = false;
}
86
87
88
89
90
91
// getter e setter per il campo sorted
public Integer[] getSorted() {
// ritorna nul nel caso in cui venga chiamato prima di
sortList()
return sorted;
}
// no!!!!!!!!!!!!
//public void setSorted(Integer[] sorted) {
//
this.sorted = sorted;
//}
// nessuno dall’esterno mi dara’ mai una lista ordinata
92
93
94
95
96
97
// getter e setter per il campo isSorted
public Boolean getIsSorted() {
return isSorted;
}
// no!!!!!!!!!!
// public void setIsSorted(Boolean isSorted) {
//
this.isSorted = isSorted;
// }
// nessuno dall’esterno mi puo’ dire e’ se ho ordinato o meno la
lista
98
99
100
101
102
103
104
105
106
107
public String toString(){
String output = "";
for( int i = 0; i < toSort.length; i++){
output = output + toSort[i].toString() + " , ";
}
return( output);
}
108
109
110
111
112
113
114
115
}
di seguito la classe che parte all’avvio ed utilizza quella precedente
1
import tests.SimpleSorter;
2
3
public class SortRunner {
4
5
6
7
8
9
// metodo main, da dove parte l’esecuzione
public static void main(String[] args) {
// flag per configurare il tipo di ordinazione
// true per crescente o false per decrescente
Boolean ascendingOrder = true;
10
11
12
13
14
15
16
17
18
19
// creo nuovo oggetto della classe sorter,
// con i primi dieci elementi di default
SimpleSorter sorter = new SimpleSorter( 10);
// ordino la lista
Integer[] sorted = sorter.sortList( ascendingOrder);
// stampo l’oggetto array
System.out.println( "lista ordinata : " + sorted);
// stampo l’oggetto SimpleSorter
System.out.println( "lista ordinata : " +
sorter.toString() + "... e’ ordinata? " + sorter.getIsSorted());
20
21
System.out.println("-----------------------------------------");
22
23
24
// creo una nuova lista
Integer[] newListToOrdered = new Integer[]{9,8,6,3,1,34};
// setto la lista all’interno dell sorter
sorter.setToSort( newListToOrdered);
// chiedo al sorter se la lista e’ ordinata
Boolean flag = sorter.getIsSorted();
// ordino lista
sorted = sorter.sortList( ascendingOrder);
// stampo l’oggetto array in modo comprensibile
System.out.print( "lista ordinata : ");
for( int i = 0; i < sorted.length; i++){
// stampa sensa andare a capo
System.out.print( sorted[i] + " , ");
}
// stampo se la lista era ed e’ ordinata e vado a capo
System.out.print( "... era ordinata? " + flag);
System.out.print( "... e’ ordinata? " +
sorter.getIsSorted() + "\n");
}
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
}
L’output generato dal metodo mail sarà simile a:
lista ordinata : [Ljava.lang.Integer;@16181be]
lista ordinata : 2 , 2 , 2 , 5 , 6 , 6 , 8 , 8 , 9 , 12 , ... e’ ordinata? true
—————————————–
lista ordinata : 1 , 3 , 6 , 8 , 9 , 34 , ... era ordinata? false... e’ ordinata? true
2.8
Esercizio 2.2: Ordinamento alfabetico
Provate come esercizio, a scrive un programma simile a quello dell’esempio 1.1
dove però si tenta di ordinare una stringa seguendo il metodo alfanumerico.
Ad esempio, inserendo in ingresso il valore “JavaProgramming” il risultato voluto è: “JPaaaggimmnorrv” considerando un ordinamento crescente, oppure
“vrronmmiggaaaPJ” considerandone uno decrescente.
Ci possono essere molti metodi per implementare una cosa simile. In un caso
reale di programmazione si predilige usare librerie già disponibili nelle classi de
default di Java in quanto permettono di avere un risultato più stabile e robusto,
ad esempio considerate che il caso di prima può essere risolto anche scrivendo
semplicemente:
1
2
3
4
5
6
Integer[] newListToOrdered = new Integer[]{9,8,6,3,1,34};
Arrays.sort( newListToOrdered);
for(int i = 0; i < newListToOrdered.length; i++){
// stampa risultato
System.err.print( newListToOrdered[i] + " ");
}
Tuttavia è utile vedere i meccanismi che stanno dietro queste istruzioni almeno
una volta per comprendere a pieno il linguaggio. Una strada sicuramente modulare può essere quella di usare il codice ASCII per assegnare ad ogni lettera
un numero e convertire così una stringa lista di numeri interi. Dopo di che
sarà possibile riutilizzare la classe SimpleSorter per ordinare un array di nu-
meri. Tuttavia vi serve sapere che la conversione da numero Integer a Stringa,
e viceversa, seconda la mappatura ASCII si può ottenere con le istruzioni:
1
2
3
4
Integer asciiCode = 65; // = ’A’ ascii code to str
String str = ((char) (int) asciiCode);
String str1 = "A";
Integer asciiCode1 = ((int) (char) str1); // = 65 str to asci code
Capitolo 3
Polimorfismo ed API in
Java
3.1
Polimorfismo
Uno dei migliori pregi dati da un Object Oriented Language come Java è appunto il Polimorfismo. Nello scorso capitolo abbiamo già visto qualche semplice
caso anche se non è stato sottolineato il loro significato. Ad esempio si è implementato costruttori che svolgono diverse operazioni anche se presentano lo
stesso nome. Infatti questa è appunto la forma più semplice di polimorfismo:
due metodi possono avere lo stesso nome a patto che abbiano input e output di
tipo diversi o che appartengano a classi diverse; questo non è sempre possibile in
diverse tipi di linguaggi. Ma allo stesso tempo si è visto come, in qualche modo,
classi possano essere descritte in modo gerarchico, definendo dipendenze tra di
loro. In maniera generale, tutte le loro funzioni vengono definite dall’opportuno utilizzo delle parole chiavi: extends, implements, interface, abstract
e super, più l’opzionale annotazione @Override.
Queste permettono di considerare una classe come un sottoinsieme di un’altra e quindi ne eredita la maggior parte delle capacità a meno di specializzarsi
in una particolare operazione. Un esempio calzante è quello di voler definire
il mondo animale; una classe base della descrizione sarà quella classe che contiene la descrizione di tutte le informazioni comune a tutti gli animali. Dopo
di che sarà possibile creare un’altra classe che estende la prima e che descrive
le peculiarità di un mammifero. Oppure un’altra ancora che estende la prima
e descrive un pesce piuttosto che un insetto. Tuttavia sarà poi possibile creare
una classe che descrive l’uomo, che estenderà quella dei mammiferi; e così via.
Grazie a questo meccanismo la classe degli umani continuerà ad avere tutte le
caratteristiche che hanno tutti i mammiferi e gli animali senza bisogno di doverle
ridefinire ogni volta. Un altro esempio più completo è quello delle ontologie configurabili da Protege editor. Infatti se provate a creare una qualsiasi ontologia
e poi cliccate su:
Tools
Generate-ProtegeOWL Java Code
noterete che alcune classi Java vengono create in modo da descrive l’ontologia
specificata. All’interno di esse si trovano pochissime istruzioni perché le diverse
35
entità ontologiche vengono descritte come dipendenze tra classi distinte. Per
ogni dubbio riguardante il polimorfismo questo rimane uno dei migliori modi per
capirlo a pieno perché permette di fare delle modifiche logiche sull’ontologia e
automaticamente vedere i cambiamenti che subisce il codice. Alternativamente,
una spiegazione altrettanto semplice ed esaudiente si può trovare nella sezione
dedicata nel tutorial di oracle24 . Per capire a pieno il modo in cui le parole
chiave nominate sopra vengono usate c’è bisogno di conoscere qual’è il loro
significato, per questo verranno analizzate una ad una.
L’operazione extends è forse quella più usata e importante. La sua sintassi
di utilizzo è ad esempio:
1
2
3
public class Dog extends Canine{
...
}
dove non c’è nessun tipo di limitazione sulla forma delle classi: Dog e Canine;
cioè qualsiasi classe può estendere ed essere estesa da qualsiasi altra. Quello che
questo tipo di istruzione permette di fare è quello di ereditare tutti i metodi, e
gli attributi che non sono privati, della classe Canine. Quindi se, ad esempio
questa è definita come:
1
2
3
4
5
6
7
public class Canine extends Animal{
...
public void howl(){ //ululare
...
}
...
}
e a sua volta:
1
2
3
4
5
6
7
public class Animal{
...
public void eat(){
...
}
...
}
risulterà possibile usare questi due metodi all’interno della classe Dog senza
doverli riscrivere, quindi:
1
2
3
4
5
6
7
8
9
public class Dog extends Canine{
...
public void foodFound(){
this.howl();
...
this.eat();
}
...
}
In questo scenario può risultare conveniente usare anche il comando su-
per(...) che sta ad indicare che la classe utilizza lo stesso costruttore di quella
estesa. Quindi ad esempio se modifichiamo la classe Animal di prima in:
1
2
3
4
5
6
7
8
9
10
11
12
public class Animal{
...
// constructur
public Animal( Date dateOfBorn){
born( dateOfBorn);
}
...
public void eat(){
...
}
...
}
potremmo pensare di usare lo stesso costruttore per tutti gli animali visto che
tutti nascono in qualche modo. Quindi l’esempio di prima diventerebbe:
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Canine extends Animal{
...
// constructor
public Dog( Date dateOfBorn){
super( dateOfBorn);
}
...
public void howl(){ //ululare
...
}
...
}
public class Dog extends Canine{
...
// constructor
public Dog( Date dateOfBorn){
super( dateOfBorn);
}
...
public void foodFound(){
this.howl();
...
this.eat();
}
...
}
Così facendo il costruttore per qualsiasi tipo di oggetto Animale risulta lo stesso,
indipendentemente che sia un cane o meno. Questo favorisce anche la possibilità
di creare l’oggetto cane in maniera più flessibile rispetto ai precedenti esempi.
Infatti in questo è corretto fare un’istanza della classe come:
1
Doog fuffi = new Doog( fuffiBirthDay);
2
3
Doog poseidone = new Caine( poseidoneBithDay);
Doog pippo = new Animal( pippoBirthDay);
Visto che tutti i Dog sono anche Animal e Canine. Ovviamente il contrario non
è accettato e trattato come un’errore dal compilatore.
Un’altra parola chiave utile se si vogliono fare questo tipo di operazioni è
l’annotazione @Override che viene utilizzata per sovrascrivere un metodo che
altrimenti sarebbe descritto in un’altra classe. Per esempio consideriamo un
particolare tipo di Cane che è particolarmente giovane non ancora in grado di
ululare e capace di bere solamente latte. In questo caso lo si potrebbe descrivere
attraverso la classe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class JungDog extends Dog{
...
// constructor
public JungDog( Date dateOfBorn){
super( dateOfBorn);
}
...
@Override
public void howl(){
// non fare niente
}
...
@Override
public void eat(){
drinkMilk();
}
...
}
Ancora una volta non vi è nessuna limitazione sul tipo di metodo che può essere sovrascritto. In java, di default ogni classe estende la classe Object; per
questo tutti gli elementi disponibili sono anche degli oggetti. Infatti, ogni classe
eredita alcuni metodi comuni a tutti gli oggetti; ad esempio: toString(), getClass(), wait(), notify(). . . una lista completa delle loro definizione è consultabile
attraverso la JavaDoc a questo indirizzo25 .
Continuando nella lista le prossime due parole chiave si allontano un po’
da questo tipo di esempi. abstract sta ad indicare la presenza di un metodo
o di una classe di cui non si è in grado di dare un’implementazione. Per cui
la si definisce in termini di parametri in ingresso e in uscita in modo da poter
continuare a creare il codice che tiene di conto di quel determinato metodo
anche se al momento non presenta nessuna implementazione. Lo svantaggio di
questo comando è che la classe non può essere stanziata con il comando new. Il
vantaggio è che permette di sviluppare un’implementazione parziale lasciando
alcune implementazioni a terzi. Un esempio di questo tipo di comando è:
1
2
3
4
5
public abstract class MyAbstractClassName{
// definisci gli attributi
....
public MyClassName( .. ){
// definisci il costruttore
...
}
...
private String myMethod1( .. ){
// definisci il metodo
...
}
...
public abstract Boolean myMethod2( String st1, Integer n);
// questo metodo non ha il corpo ma solo la definizione
// non ho gli elementi per definirla e quindi la lascio astratta
...
6
7
8
9
10
11
12
13
14
15
16
17
18
}
Visto che la classe è incompleta, per creare una sua istanza c’è bisogno di
completarla ad esempio con:
1
2
3
4
5
6
7
public class MyClassName extends MyAbstractClassName{
@Override
public Boolean myMethod2( String st1, Integer n){
// implementa il metodo
...
}
}
Ovviamente questa classe è soggetta a tutti i comportamenti che l’istruzione
extends comporta ma dovrà almeno implementare tutti i metodi astratti. Ora
sarà possibile stanziare con il comando new la classe MyClassName e usufruire
così anche di tutte le capacità contenute nella classe MyAbstractClassName.
Infine le parole chiave interface e implements sono legate tra loro e vengono utilizzate per definire la forma di una certa classe senza però darne nessuna
implementazione. Utile nel momento in cui classi di natura diverse devono contenere gli stessi tipi di metodi da trattare in modo coerente per ognuna di loro.
Un esempio, tratto dal tutoria oracle26 ,di un interfaccia è ad esempio:
1
2
3
4
public interface Predator {
boolean chasePrey(Prey p);
void eatPrey(Prey p);
}
Da notare, che come per le classi astratte i metodi contenuti non presentano
nessuna istruzione e quindi nessun body. Tuttavia a differenze delle classi astratte non è possibile fare alcuna operazione, le uniche istruzioni ammesse sono
quelle di definizione dei metodi a meno del costruttore. Per rendere la classe
stanziabile attraverso il comando new c’è bisogno di implementarla attraverso
un’ulteriore classe; ad esempio:
1
2
3
public class Lion implements Predator {
// definsci gli attributi ed il costruttore
....
4
5
6
@Override
public boolean chasePrey(Prey p) {
// definisci un’implementazione
7
}
8
9
@Override
public void eatPrey (Prey p) {
// definisci un’implementazione
}
10
11
12
13
14
...
15
16
}
ora sarà possibile stanziare la classe nei seguenti modi:
1
2
Predator pre = new Lion();
Lion lion = new Lion();
L’utilizzo dei package è finalizzato a indirizzare i file testuali che descrivono
classi diversi dentro una comune sotto cartella di src. Questa organizzazione gerarchica viene utilizzata per tenere un certo ordine logico e pratico all’interno di
progetti complessi; è grazie a questo ulteriore servizio di modularità che si riesce
a caricare attraverso un IDE come Eclipse librerie complesse senza nemmeno
accorgercene. Tuttavia, è importante tenere a mente che questi meccanismi non
possono essere utilizzati in un contesto static.
3.2
API: Application Programming Interfaces
Una delle operazioni che si effettua più spesso durante la stesura di un codice
è quella di importare librerie esterne da poter utilizzare nel codice. Queste librerie altro non sono che una serie di classi scritte da un terzo sviluppatore,
documentate attraverso una JavaDoc e pronte per essere utilizzate. Il loro
utilizzo dipenda da come sono state progettate e non è ancora ben standardizzato, comunque se ne possono descrivere di due tipi. Una che richiede di usare
istruzioni di comando simili a quelle viste negli esempi del Capitolo 1, l’altra
che utilizza meccanismi polimorfi per configurare un generico comportamento
che, altrimenti, è predefinito di default. Le prime sono generalmente più facili
da utilizzare ma richiedo un numero di linee di codice da scrivere notevolmente
superiori alle seconde.
Per cercare di impara ad usare librerie esterne c’è bisogno di sapere intuitivamente come sono strutturate ma, più importante, c’è bisogno di sapere
quale parte della documentazione è bene consultare per risolvere un determinato problema. Considereremo ora una semplicissima collezione di classi che
implementano gli oggetti necessari per scrivere e leggere da file. Una capacità
decisamente interessante per elaborare un grande numero dati senza saturare
la ram, a discapito della velocità. Oppure per stampare informazioni da poter
riconsultare in seguito, magari dopo che un algoritmo di intelligenza virtuale,
o un esperimento con sensori, ha funzionato per ore. La libreria che propongo
di usare può essere semplicemente schematizzata attraverso la formalizzazione
UML27 (Unified Modeling Language). Tutto il modello del software parte
dalla definizione dell’interface FileManager che raccoglie la definizione delle operazioni elementari che un qualsiasi oggetto, che agisce sul file, deve avere. Sono
Figura 3.1: UML schema di un programma polimorfo per leggere e scrivere su
File. Esempio di semplice API
presenti inoltre 3 classi abstract che implements questo tipo di interfaccia: CommonFileOperations, LazyReader e LazyWriter; non consideriamo queste ultime
due per il momento ma concentriamoci solamente sulla prima. Questa implementa tipi di operazioni che sono comuni sia nel caso della scrittura che in quello
della lettura. In questo caso particolare gestisce la stringa testuale che rappre-
senta l’indirizzo in cui risiede il file e l’operazione di notifica di eventuali errori,
dovuti dalla mancanza del file ad esempio. Tuttavia in questa classe si è deciso
di non implementare alcuni metodi che dipendono dal tipo di operazione che
si vuole dare, che quindi rimangono astratti. Due ulteriori classi: FileReader e
FileWriter extends CommonFileOperations ereditando così i metodi già implementati. Quest ultime classi implementano due diverse operazioni di apertura e
chiusura del file perché si basano su due oggetti forniti da java diversi: BufferedReader e BufferedWriter (vedi qui per alcuni esempi28 ). Ora i metodi di tipo
astratto, per ogni rispettiva classe, sono quelli descritti dal nome: manipulateFile(). Da notare che anche gli unici metodi astratti per le classi LazyReader
e LazyWriter sono le stesse. Questo perché questa API è stata realizzata con
l’intento di indicare allo sviluppatore che intende usarla, che deve creare una
sua classe (chiamata Reader e Writer in questo caso) la quale extends una tra
le classi FileReader, FileWriter, LazyReader e LazyWriter. Cosi facendo questa
dovrà solamente implementare l’ultimo metodo rimasto ancora astratto mentre tutte le restanti funzioni sono già pronte per essere utilizzate. Solitamente
questo tipo di descrizione è fornita attraverso una documentazione dedicata che
vedremo a breve.
3.3
Esercizio 3.0: Importare una Libreria Esterna su Eclipse
Solitamente le librerie sono composte da un unico file di esenzione .jar; tipicamente richiedono un controllo accurato nel verificare che le versioni siano
compatibili per evitare problemi di instabilità nel software. In generale per
importare una libreria di questo tipo basta creare una cartella di nome lib all’interno del progetto voluto che si trova nel workspace di eclipse. All’interno di
questa cartella collezioneremo tutte le librerie utilizzate solo in quel particolare
progetto. Quindi copiamo il file FileManager-SimpleAPI.jar all’interno di questa directory. Dopo di che cliccare con il tasto destro del mouse sopra l’icona
che identifica questo progetto sull’albero delle directory presente a sinistra della
finestra di java; ora:
Properties
Java Build Path
Libraries
Add JARs..
e navigate fino all’interno della cartella lib poco prima creata. Nel caso in cui
questa non sia ancora presente e consigliabile uscire dalla finestra delle proprietà
e cliccare su:
File
Refresh
stando attenti di avere ancora il progetto selezionato sull’albero a sinistra della
schermata principale. Quindi tornate ancora sull’opzione Ad JARs e terminate
quello che prima non era possibile fare. Una volta confermata l’aggiunta del nuovo pacchetto può rivelarsi utile includere non solo i file eseguibili, ma anche quelli
sorgenti (quelli testuali) e la documentazione Java. Per fare questo è necessario
espandere la libreria appena aggiunta cliccando sulla piccola freccia che si trova
a sinistra del nome. Dopo di che fare doppio click sulla voce source attachment
e navigare fino allo stesso file .jar aggiunto in precedenza. Compiere la stessa
operazione per la voce Java Doc location ed inserire la posizione dello stesso
file .jar attraverso l’opzione: Java Doc in archive. Utilizzate il bottone Validate per essere sicuri che Eclipse riconosca la posizione all’interno dell’archivio.
Se questa operazione dà risultato negativo, inseritela manualmente attraverso
l’opzione: Path within archive. Cliccate ok sia su questa finestra, che su quella
delle proprietà. Ora la libreria dovrebbe essere stata importata completamente,
per assicurarsi di ciò basta andare in un qualsiasi metodo di una qualsiasi classe
all’interno di quel progetto e digitare una riga di comando corretta che sia formata dal nome di una classe presente nel pacchetto esterno. Nel nostro esempio
basterà scrivere:
1
LazyWriter a;
Dopo questa operazione il nome della classe sarà sottolineato in rosso, avvicinarsi con il mouse fino a che non compare una finestra e controllare che sia
presenta la voce: Import NomeClasse (nomePacchetto). In questo esempio:
Import LazyWriter (fileManagerApi). Se questo accade vuole dire che la classe
è riconosciuta all’interno del progetto, basterà cliccare su quella voce per aggiungere automaticamente una riga di comando sulle prime righe del file, che
indicano che questa classe ha bisogno della classe LazyWriter, ed eliminare il
segnale di errore presente in precedenza.
Per poter vedere il codice associato a quella determinata classe basterà
premere ctr+mouse sinistro per essere automaticamente indirizzati alla sua
definizione (questo vale per tutti gli oggetti presenti). Da qui si può non solo
vedere le linee di comando, ma anche la definizione della documentazione che
è scritta in un linguaggio dedicato molto simile all’html. Solitamente la documentazione viene inserita nel pacchetto jar già compilata, ma se lo si vuole
generare nuovamente basterà andare sulla barra in alto della finestra principale
e cliccare su:
Project
Generate Java Doc
Da qui si apre una finestra su cui potete spuntare le classi per cui creare la documentazione e da cui dovete inoltre impostare la cartella dentro la quale salvare
la documentazione generata. Basterà cliccare su Finish per far partire la sua
compilazione e quindi la creazione. La documentazione può essere consultata
aprendo con un browser i file (di solito index.html) presenti nella cartella dedicata alla documentazione. Un’altra alternativa è quella di andare nella scheda
chiamata appunto Javadoc che si trova sulla stessa finestra dove c’è la console.
Qui è visualizzata in modo sintetico e lo stesso tipo di informazioni vengono
date avvicinandosi con il mouse ad un qualsiasi nome presente nel codice. Tuttavia il metodo consigliato è quello di aprirla tramite la terza icona a partire da
destra presente nella scheda JavaDoc vista in precedenza. Facendo così si apre
un’interfaccia che contiene tutte le informazioni indispensabili per utilizzare la
libreria importata.
3.4
Esercizio 3.1: Scrittura e lettura su file
Come esercizio si propone di leggere la documentazione e di capire a pieno la
struttura del codice. Solo quando il precedente punto è stato risolto si chiede di
implementare le due classi Writer e Reader introdotti nello schema precedente.
In queste classi si dovrà implementare almeno il metodo astratto, seguendo le
indicazioni date dalla documentazione, che implementi come le linee testuali
vengano manipolate sia in scrittura che in lettura al file. Come prima prova,
implementare una logica semplice per assicurarsi che il basso livello del software
funzioni (ad esempio copiare il contenuto di un file testuale all’interno di un
altro). E buona norma creare una cartella all’interno del progetto denominata
files e mettere qui tutti i file che volete considerare. Inoltre, tenete bene a mente
che se si perde un file a questo livello di utilizzo del calcolatore non è possibile
ricuperarlo. Per risolvere l’esercizio avrete bisogno anche di un metodo main
all’interno di una classe da cui far partire l’esecuzione e gestire gli oggetti di tipo
Writer e Reader appena implementati. Un aggiunta opzionale a questo esercizio
è quella di aggiungere un valore Boolean in modo da poter decidere se copiare
incollare il contenuto di un file da un altro oppure fare un’operazione di tipo
taglia e incolla.
3.5
Esercizio 3.2: Miglioramento delle capacità
di una classe
Per vedere quanto questo tipo di programmazione è flessibile cercate di usare il
primo codice dove però le classi Writer e Reader non espandono più FileWriter e FileReader ma LazyWriter e LezyReader. Concentratevi sul riconoscere
come due implementazioni diverse degli stessi metodi danno risultati completamente diversi anche senza cambiare completamente tutta la strutta. Ovviamente questo esempio è molto elementare ma il concetto di base comunque non
cambia. Come ultimo passo opzionale si propone di consultare le risorse in rete
per cercare di aggiungere capacità alle classi Writer e Reader (nuovamente configurate in modo da espandere le classi FileWriter e FileReader). In particolare
cercati di aggiungere metodi modulari in modo da poter essere anche in grado
di eliminare e rinominare un file.
Capitolo 4
Interfacce Grafiche
4.1
Benefici della Virtualizzazione della JM
Una delle caratteristiche più interessanti di Java è quella di permettere ad un
qualsiasi programma di funzionare indipendentemente dal basso livello del computer che si sta usando, come ad esempio il sistema operativo. Per proporre un
altro esempio, considerate inoltre che ogni programma scritto in Java è anche in
grado di funzionare all’interno di una pagina web. Tuttavia si deve considerare
che a volte possono nascere incongruenze tra i diversi sistemi operativi, come
ad esempio diversi simboli per descrivere il percorso delle directory o una nuova
linea in un file. Per ovviare a questi problemi è buona norma quella di riferire, a
qualsiasi informazione, che dipende dal sistema operativo attraverso il comando:
1
System.getProperty( proprty);
dove property è una stringa che può assumere questi valori29 . Durante l’esecuzione del programma questo comando richiede al sistema operativo che viene
usato di fornirgli l’informazione indicata.
Questo breve preambolo intende giustificare il perché si dovrebbe scegliere di
utilizzare Java per la creazione di GUI (Graphical User Interface). Il motivo è
che così facendo si può creare un’applicazione in grado di essere usata sulla maggior parte dei dispositivi, anche attraverso la rete. In particolare, nelle sezioni
successive si proporrà una rassegna molto veloce delle capacità date dalle API
swing e awt30 fornite di default da Java. Queste lavorano in collaborazione
tra di loro permettendo la creazione di interfacce grafiche. Tuttavia è bene ricordare che, in linea generale, non è mai consigliato usare nello stesso progetto
due librerie diverse che implementano lo stesso tipo di oggetti. Inoltre, un’altra
caratteristica molto importante che la macchina virtuale Java propone è la possibilità di usare complessi meccanismi Real-Time in modo automatico (senza il
bisogno di nessun comando). Infatti Java risolve autonomamente problemi, per
niente banali, come ad esempio quello di sincronizzare il programma rispetto
ad un input proveniente dall’utente. Tuttavia questo rimane vero fino a che si
considerano GUI realizzate da un’unica finestra e che non debbano fare calcoli
che richiedano troppo tempo.
45
4.2
Swing & awt
Il numero delle classi e oggetti grafici che queste librerie mettono a disposizione è
molto alto e non avremo modo di affrontarle tutte durante questo elaborato (una
lista abbastanza completa si può trovare qui31 ). Queste classi implementano
oggetti che possono essere visualizzati a schermo, questi possono interagire tra
di loro e con input da tastiera o da mouse. Il comportamento dei diversi tipi
di componenti che formano una GUI sono stati standardizzati e ben conosciuti
dall’utente, anche se non abbiamo mai usato il loro nome; una lista dei diversi
componenti la si può consultare qui32 . Infine si proporre anche la completa e
interattiva descrizione di queste librerie fornita da oracle33 .
Tra gli elementi principali di una interfaccia, è sicuramente presente la finestra implementata dall’oggetto JFrame; per crearne una si consiglia di scrivere:
1
2
import java.awt.BorderLayout;
import java.awt.EventQueue;
3
4
5
6
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
7
8
9
public class MyFrame extends JFrame {
10
private JPanel contentPane;
11
12
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
// lancia un thread indipendente che gestisce la GUI
public void run() {
try {
MyFrame frame = new MyFrame();
frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// costruttore
public MyFrame() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(100, 100, 450, 300);
contentPane = new JPanel();
contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
contentPane.setLayout(new BorderLayout(0, 0));
setContentPane(contentPane);
}
27
28
29
30
31
32
33
34
35
36
37
}
In questa classe è presenta il metodo main da cui parte l’esecuzione del pro-
gramma. In questo caso, nel main viene implementato un metodo che permette
di far partire un programma completamente indipendente dal primo. Infatti
il programma che parte dal main finisce subito, mentre la GUI rimane aperta.
Questo meccanismo viene utilizzato per non doversi preoccupare delle relazioni
temporali e di sincronizzazione tra i diversi oggetti ed è estremamente consigliato
di averne uno solo per GUI.
Un altro tipo di elemento che analizzeremo è il JPanel, o l’analogo JScrollPane. Questo implementa un pannello dentro al quale è possibile inserire altri
oggetti dell’interfaccia. Il metodo con cui vengono inseriti oggetti è simile a
quello con cui si gestiscono le strutture di dati composti: List e Set. Infatti
basterà usare:
1
2
3
JPanel panel = new JPanel();
JButton button = new JButton();
panel.add( button);
per aggiungere un bottone ad un pannello ad esempio. La posizione in cui tale
bottone viene messo all’interno del panello è definita dalla proprietà layout del
pannello stesso. Esempi di layaout disponibili sono consultabili a questo link34 .
Ricordate che è sempre possibile aggiungere un pannello ad un altro anche con
layout diversi, questa struttura gerarchica permette di posizionare oggetti con
molta flessibilità.
Un caratteristica di tutti gli elementi di un’interfaccia è quella di poter reagire a degli input dati dagli utenti. Per farlo c’è bisogno di quello che viene
comunemente chiamato listener, cioè un programma che, ciclando su se stesso,
rimane in ascolto di un particolare evento legato ad un oggetto e, eventualmente,
ne notifica l’occorrenza. Ad esempio, sarà possibile inserire il seguente codice
all’interno di un metodo:
1
2
3
4
5
6
7
8
9
10
11
....
JButton btn = new JButton( "click here");
contentPane.add(btn, BorderLayout.SOUTH);
btn.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
// scrivere qui cosa fare quando il bottone viene premuto
.....
}
});
....
che permette di scrivere il codice da considerare nel momento in cui il mouse
è stato rilasciato sopra il pulsante. Esistono molti altri tipi di listeners come
descritto in questa lista35 . Da notare che nell’esempio precedente compare
l’utilizzo della parola chiave Override. Infatti, come nell’esempio del capitolo
precedente, tutta questa API si basa su meccanismi polimorfi. Questo permette
di riuscire a fare cose di una certa difficoltà scrivendo poche righe di programmazione, ma d’altro canto richiede una profonda conoscenza del funzionamento
di ogni classe.
Un aiuto decisamente interessate viene proposto dal plug-in per Eclipse Windows Builder Pro36 che permette di usare un intuitivo meccanismo di drag &
drop per generare automaticamente linee di codice simili a quelle che abbiamo
visto in precedenza. Tuttavia ha lo svantaggio di fornire codici difficilmente
interpretabile e, inoltre, di difficile manutenzione. Il resto di questo capitolo si
baserà sull’utilizzo di questo software per generare la base dell’interfaccia in modo semplice. Dopo di che, il codice verrà modificato manualmente in modo che
il programma esegui le operazioni volute. In particolare, vedremo come riuscire
ad utilizzare queste librerie per fare un semplice questionario.
4.3
Esercizio 4.0: Creazione di una GUI con
WindowsBuildePro
In figura 4.3 è presente l’organizzazione degli oggetti utilizzati per generare
questo semplice questionario. Vediamo come è possibile crearlo utilizzando il
plug-in descritto sopra. Per prima cosa creare un nuovo progetto e dopo fate
click sulla piccola freccia che c’è a sinistra della seconda icona a partire da
sinistra della barra più in alto della finestra di Eclipse. Notate che questa icona
non è presente se non si è istallato il plug-in. Dopo di che andate su:
swing
JFrame
si aprirà una finestra in cui si richiede di mettere il nome della classe che stiamo
per creare, ad esempio: GuiSample, posizionata nella source folder src del vostro
progetto. Così facendo si apre la classe che, a differenza delle altre volte, presenta
due tipi di presentazioni. Infatti, potete notare che in basso c’è la presenza delle
linguette che aprono due schede diverse; denominate Source e Design. La prima
contiene il file testuale mentre la seconda la visualizzazione grafica di quello che
implementa la prima. I due file dovrebbero rimanere sempre sincronizzati tra
Figura 4.1: fig:esempio di un’interfaccia, organizzazione degli oggetti
loro, ed è per questo che a volte è necessario fare une reparsing tramite il terzo
pulsante della barra in alto, presente nella scheda di design. Così facendo è
possibile creare oggetti tramite il mouse e vedere come questi vengono mappati
all’interno del codice. Inoltre è possibile usare il comando di run per vedere
la finestra nella sua vera forma. All’interno della scheda di design notate che
è presenta l’albero degli oggetti aggiunti alla finestra, in alto a sinistra. Le
proprietà dell’oggetto selezionato, come ad esempio il nome e l’inizializzazione,
la si può modificare dal riquadro in basso a sinistra. Infine è presente una lista
dei possibili oggetti che possono essere aggiunti, nel centro.
Come esercizio cercate di riprodurre la finestra di figura seguendo queste
operazioni nell’ordine indicato:
1. JFrame e contentPanel: sono aggiunti di default durante la creazione della
classe, questo perché abbiamo scelto di creare una finestra. Di default
questi elementi sono configurati in modo da aver il layout: BorderLayout.
2. JLabel: è aggiunto al contentPane nella sezione North del layout. Inserite
qui un titolo o fate in modo che, nella sezione Source del plug-in, sia pari
al valore di una costante definita.
3. JButton: è aggiunto nella sezione South del content-panel. Una volta
aggiunto cliccare con il tasto destro sopra al bottone. Qui sono presenti
alcune proprietà, tra cui Add event handle. Da qui potete scegliere quale
listener collegare a questo oggetto, scegliete: mouse
mouseReleased.
4. JScrollPane: è aggiunto nella parte centrale del contentPanel. Notate che
aggiungendolo si perde la possibilità di usare la parte est e ovest. Se non
si fossero fatti i primi due punti avremmo perso anche la possibilità di
usare quella nord e sud.
5. JPanel: è aggiunto all’interno di JScrollPane e permette di potervi inserire più oggetti diversi. Configurate il layout di questa componente
come BoxLayout, e nelle sue proprietà: layout
constructor configurate
Y_AXIS. Quest’ultima proprietà farà in modo che tutti gli oggetti inseriti
successivamente su questo pannello saranno messi uno sotto l’altro.
Facendo così si è creata la parte base del questionario. Per il concetto di modularità visto nel primo capitolo, risulta inefficiente mettere su questa classe anche
la descrizione di tutte le domande. Infatti, è più comodo descrivere un’unica
domanda generica, e poi usarla tutte le volte che è necessario. Per farlo cliccate nuovamente nella seconda icona in alto, come avete fatto per creare la
precedente finestra; ma questa volta digitate:
Swing
JPanel
e assegnategli un nome, ad esempio: Question. Questa classe servirà per estendere un generico JPanel in modo da definire un determinato tipo di pannello
che si vuole usare per visualizzare ogni singola domanda. Una volta posizionati
sulla scheda Design di questo nuovo pannello configurate il suo layout come
BorderLayout e seguite i punti:
6. JPanel: è aggiunto sulla parte nord nel pannello Question. Settate il suo
layout come: FlowLayout, così tutti i prossimi oggetti che verranno inseriti
saranno messi uno accanto all’altro.
7. JLabel: è aggiunto al precedente pannello e conterrà il numero della
domanda.
8. JLabel: è ancora aggiunto al pannello precedente e conterrà il testo della
domanda.
9. JPanel: è aggiunto sulla parte centrale del pannello Question
10. JRadioButton: è aggiunto sul pannello del punto precedente. Aggiungetene
solo uno così da generare il codice adeguato, dopo dovremmo cambiarlo/copiarlo per fare in modo che si comporti come desiderato.
Per effettuare l’ultima parte dell’ultimo punto spostatevi sulla scheda Source
del plag-in. Da qui si vede il codice che genererà il pannello Question come
desiderato. Da notare che la classe extende JPanel, quindi erediterà tutte le
sue funzionalità. A questo punto aggiungete tra gli attributi della classe questa
variabile:
1
private ButtonGroup answerGroup = new ButtonGroup();
che descrive un oggetto in grado di raccogliere tutti i JRadioButton di una domanda in modo esclusivo, cioè non sarà possibile selezionare più di una risposta.
Ora cercate il codice generato da eclipse per aggiungere un unico JRadioButton
e sostituitelo con:
1
2
3
4
5
6
answerGroup = new ButtonGroup();
for( .... ){ // per tutte le possibili risposte s
JRadioButton rdbtnNewRadioButton = new JRadioButton( s);
answerGroup.add( rdbtnNewRadioButton);
panel_1.add( rdbtnNewRadioButton);
}
dove panel_1 è la variabile che indica il pannello inserito al punto 9. Qui dovrete
implementare la logica del ciclo for in un contesto più ampio che affronteremo
nella prossima sezione.
4.4
Esercizio 4.1: Struttura delle classi di una
GUI
Ora che è stata implementata la parte base dell’interfaccia grafica, vediamo come
si può strutturare il flusso dei dati tra le due classi in modo da visualizzare un
semplice questionario. L’idea è quella di creare una terza classe in grado di
descrivere pienamente una generica domanda. Ad esempio la definizione dei
suoi metodi e attributi potrebbe essere:
1
public class QuestionFactory {
2
3
4
5
6
7
// contiene testo della domanda
private String question;
// contiene numero della domanda
private Integer questionNumber;
// contiene lista di tutti i testi delle risposte
private List< String> answers = new ArrayList< String>();
// per ogni testo della risposta contiene true se corretto,
false altrimenti
private List< Boolean> isCorrect = new ArrayList< Boolean>();
8
9
10
11
// costruttore, inizializza il testo e il numero della domanda
public QuestionFactory( Integer questionNumber, String
question){...}
12
13
14
// aggiungi una risposta e se e’ corretta o meno
public void addAnswers( String answer, Boolean isCorrect){...}
15
16
17
// ritorna il numero della domanda
public Integer getQuestionNumber(){...}
18
19
20
// ritorna il testo della domanda
public String getQuestion(){...}
21
22
23
// ritorna la risposta numero idx
public String getAnswer( Integer idx){...}
24
25
26
// ritorna tutte le risposte
public List< String> getAnswer(){...}
27
28
29
// ritorna true se la risposta con indice idx e’ corretta
public Boolean getIsCorrect( Integer idx){...}
30
31
32
// ritorna il numero delle risposte
public Integer getNumberOfAnswers(){...}
33
34
35
// ritorna se la risposta ’’answer’’ e’ corretta o meno
public Boolean isCorrectAnswer( String answer){...}
36
37
38
}
Quello che vi viene chiesto di fare in questo esercizio è: implementare la classe sopra indicata. Stanziarla per tutte le domande volute all’inizio del metodo main
e visualizzarla nella finestra principale al termine del costruttore della classe
GuiSample. Per fare questo ultimo punto riferitevi all’esempio precedente in cui
un bottone veniva aggiunto ad un pannello; in questo caso dovrete aggiungere
un pannello ad un pannello di tipo Question. Se provate a lanciare il programma a questo punto dell’implementazione dovreste essere in grado di vedere il
questionario visualizzato ma senza nessuna azione da parte del pulsante. Per
concludere implementate il listener del pulsante in modo tale che:
• Se il questionario non è completato quando si preme il pulsante il programma restituisce un’errore
• Altrimenti, il programma restituisce un messaggio in cui si notifica il
numero totale delle risposte giuste e l’indice della domanda per quelle
sbagliate.
Un modo elegante per far visualizzare un’errore, o un messaggio, attraverso una
finestra pop-up è:
1
2
3
4
5
6
7
8
// visualizza una finestra (pop-up) per le informazioni.
// Opzioni accettate:
// int option = JOptionPane.ERROR_MESSAGE;
// int option = JOptionPane.INFORMATION_MESSAGE;
private static void displayPanel( String info, String title, int option){
JOptionPane popUp = new JOptionPane();
a.showMessageDialog( popUp, info, title, option);
}
Vediamo ora, per punti, come modificare il codice in modo da ottenere il
comportamento voluto. Un modo semplice di descrivere l’intero passaggio dei
dati tra le tre classi può essere il seguente:
GuiSample
classe
• aggiungi un attributo che servirà per tenere in memoria tutte le istanze
della classe Question che verranno create in seguito.
1
2
// lista di tutte le domande nel test
private final List< Question> questionList = new ArrayList<
Question>();
GuiSample.main()
lanciatore del programma
• Create tutte le domande (new QuestionFactory( ...)) e salvate gli oggetti
relativi in un array.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// crea un array di domande
List< QuestionFactory> questions = new ArrayList<
QuestionFactory>();
// crea la prima domanda
QuestionFactory q1 = new QuestionFactory( 1, " questa e’ la prima
domanda?");
// aggiungi le risposte
q1.addAnswers( "si.", true);
q1.addAnswers( "no", false);
q1.addAnswers( "bho", false);
// aggiungi la prima domanda e le sue risposte al array
questions.add( q1);
// crea la seconda domanda
QuestionFactory q2 = new QuestionFactory( 2, " Mentre questa e’ la
prima domanda?");
// aggiungi le risposte
q2.addAnswers( "si.", false);
q2.addAnswers( "no", true);
// aggiungi la seconda domanda e le sue risposte al array
questions.add( q2);
• Il programma creerà una nuova GuiSample (utilizzando il codice auto
generato da WindowsBuilderPro) ma in aggiunta c’è bisogno di fornire
(come parametro d’ingresso al costruttore) tutte le domande inizializzate
al punto precedente, contenute nella variabile questions.
GuiSample.GuiSample( List< QuestionFactory> questions)
constructor
• La finestra verrà creata utilizzando il codice auto generato dal plug-in di
eclipse.
• Aggiungete tutte le domande alla finestra (new Question()) utilizzando i
valori della QuestionFactory stanziata in precedenza.
1
2
3
4
5
6
7
8
9
// per tutte le domande passate al costruttore della finestra
for( QuestionFactory q : questions){
// crea un nuovo pannello che contiene la domanda
Question newQuestion = new Question( q);
// aggiungi la domanda al pannello creato al punto 9
panel_1.add( newQuestion);
// aggiungi la domanda all’attributo della classe
questionList.add( newQuestion);
}
Question
class
• aggiungete gli attributi necessari per tenere in memoria sia il gruppo di
JRadioButton di ogni domanda, che la classe della domanda.
1
2
private ButtonGroup answerGroup = new ButtonGroup();
private QuestionFactory question;
Question.Question()
constructor
• Il programma mostrerà la domanda e le risposte (usando il codice generato
e modificato come indicato nella sezione precedente).
• Implementate il metodo getAnswersCorrecteness() che ritorni true o false
in base alla risposta giusta o sbagliata rispettivamente. Se non viene data
nessuna risposta questo metodo ritornerà null.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ottieni la risposta selezionata e controlla se e’ corretta
public Boolean getAnswersCorrecteness(){
// ottieni un elemento in grado di ciclare su tutti i
RadioButton
Enumeration<AbstractButton> allRadioButton =
answerGroup.getElements();
String answer = null;
// per tutti gli elementi dentro al RadioGroup
while(allRadioButton.hasMoreElements()){
// recupera un RadioButton
JRadioButton temp= (JRadioButton)
allRadioButton.nextElement();
// controlla se e’ selezionato
if( temp.isSelected()){
// ottieni testo della selezione
answare = temp.getText();
// esci (solo una puo’ essere selezionata)
break;
}
}
// se c’e’ stata una risposta
if( answer != null)
// controlla che sia corretta
return( question.isCorrectAnswer( answer));
else return( null);
17
18
19
20
21
22
}
23
Button.MouseReleased()
listener
• Per tutte le istanze di Question salvate nell’attributo questionList, chiamate getAnswersCorrecteness() e definisci il comportamento del programma secondo le specifiche
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void mouseReleased(MouseEvent e) {
// crea quantita’ interessanti per questa funzione
Integer correctCounter = 0;
Boolean incomplete = false;
List< Integer> wrongAnswer = new ArrayList< Integer>();
// per tutte le domande in questa finestra
for( int i = 0; i < questionList.size(); i++){
// ottieni la selezione della domanda
// true -> corretta, false -> sbagliata, null ->
assente
Boolean answer = questionList.get(
i).getAnswersCorrecteness();
if( answer != null){
if( answer){
// se e’ corretta contala
correctCounter = correctCounter +
1;
} else {
// se e’ sbagliata salva il suo
numero
wrongAnswer.add(
questions.get(i).getQuestionNumber());
}
} else {
// se non c’e’ una risposta alza il flag e
esci
incomplete = true;
break;
}
25
26
27
28
29
30
31
32
}
// se il flag e’ alto ritorna un’errore
if( incomplete){
displayPanel( " non hai risposto a tutte le
domande, si prega di farlo",
"Questionarion non
Completato",JOptionPane.ERROR_MESSAGE);
} else {
// se il flag e’ basso mostra informazioni
displayPanel( " Grazie per la partecipazione al
test." + System.getProperty( "line.separator") +
" Risposte corrette " +
correctCounter + ". Numero delle risposte sbagliate: " +
wrongAnswer,
"Questionarion
completato",JOptionPane.INFORMATION_MESSAGE);
}
33
34
35
36
37
4.5
}
Esercizio 4.2: Creare un file cliccabile per
lanciare un programma
Una caratteristica che abbiamo visto all’inizio di questo capitolo è quella di poter
esportare un programma per tutti i sistemi operativi muniti di JVM. Vediamo
come è possibile farlo su Eclipse. Per prima cosa andate su:
File
Export
Java
Runnable Jar File
e premete Next. Digitate il percorso e il nome del file che volete creare e premete
Finish. Ora sarà possibile fare doppio click sul file appena creato per lanciare
l’applicazione. Se non dovesse funzionare, si può scegliere (con l’istruzione apri
con) l’applicazione java più consona al programma creato, solitamente OracleJava. Se anche questo metodo dovesse fallire da un qualsiasi prompt di comandi
potete navigare fino al file che volete lanciare e scrivere:
1
java -jar nameOfFile.jar
Da notare che all’interno delle opzioni di Export, viste poco prima, c’è anche
l’opzione Jar File che permette, con operazioni del tutto analoghe alle precedenti, di creare l’archivio Jar per esportare le vostre classi come librerie. Esattamente il metodo con cui è stato creato il pacchetto FileManager.jar visto nella
precedente esercitazione.
Capitolo 5
Fondamenti di Artificial
Neural Networks
5.1
Introduzione
Le Neural Network (NN) sono un algoritmi utilizzati per trovare soluzione numerica in forma aperta a problemi complessi, di cui solitamente non si riesce
a trovare una soluzione analitica in forma chiusa. Questo fa parte degli algoritmi sviluppati in Machine Learning (ML) che, a sua volta, è una branchia
dell’Intelligenza Artificiale (AI). Come è noto dalla letteratura questo tipo di
processo matematico è inspirato alla biologia del cervello, da cui prende il nome,
ma si basa su formule matematiche; in modo che sia possibile implementarlo in
un calcolatore. Tipicamente, questo tipo di algoritmo viene usato per classificare dati in classi statistiche, chiamate anche claster. L’esempio più facile è
quello di avere due classi, ad esempio vero e falso, ma ci possono essere anche
casi in cui si anno un numero maggiore di classificazioni. Un’evoluzione dell’algoritmo permette inoltre di stimare valori continui, come ad esempio funzioni
temporali; questo processo prende il nome di regression ma non avremo modo
di analizzarlo all’interno di questo laboratorio che si baserà solamente sul primo
caso di classificazione.
Questo capitolo affronterà prima il componente primitivo della rete: il Perception; per poi passare ad una delle più note configurazione della rete: il Multilayer Perception (MLP). Per quanto riguarda il primo, oltre alla sua formulazione matematica, verrà proposta anche un’implementazione dell’algoritmo in
Java. Mentre, per il secondo argomento, verranno affrontate solo poste le basi
teoriche senza entrare nella implementazione pratica. Questo per evitare di
dover risolvere problemi matematici e tecnici non banali ma, e sicuramente più
importante, perché risulta più affidabile adoperare librerie esterne sopratutto se
si intende ampliare lo studio con accorgimenti matematici più all’avanguardia.
Tuttavia, utilizzare una libreria esterna richiede ugualmente un’approfondita
conoscenza dei meccanismi di funzionamento e soprattutto dei paramenti che
devono essere configurati dallo sviluppatore. Per questo si è scelto di affidarsi alla libreria encog37 che presenta un’ampia documentazione grazie alla sua
Wiki, e un’approfondita serie di video lezioni38 , tenute dallo stesso sviluppatore della libreria. Inoltre, un interessante punto di partenza per capire le reti
57
neurale viene proposto sempre dalla documentazione di queste API, e si trova
nelle sezioni preliminari39 ; dove è presente una spiegazione che non richiede
particolari conoscenza matematiche.
5.2
La forma del Data Set
Questo è l’elemento che sta alla base di ogni calcolo statistico. Durante questo
capitolo, ci focalizzeremo sulla stima di semplici funzioni logiche come: OR,
AND e XOR; definiti come in tabelle: dove, per motivi matematici che tra
x1
0
0
1
1
x2
0
1
0
1
y
-1
1
1
1
Tabella 5.1: OR
x1
0
0
1
1
x2
0
1
0
1
y
-1
-1
-1
1
Tabella 5.2: AND
x1
0
0
1
1
x2
0
1
0
1
y
-1
1
1
-1
Tabella 5.3: XOR
poco saranno chiari, i concetti: true e flase, sono rappresentati dai valori 1 e -1
rispettivamente. Per quanto detto nell’introduzione ci focalizzeremo soprattutto
sulla stima di queste semplici funzioni logiche.
Tuttavia gli algoritmi trattati possono essere utilizzati per stimare qualsiasi
funzione, a patto che questa venga definita in un modo simile. In particolare,
viene chiamato data set un qualunque collezione di dati espressi in termini di
features e labels. I primi stanno ad indicare le colonne della tabella, nell’esempio di prima: x1 e x2 , in termini più generali si può avere un qualsiasi numero
di colonne, solitamente indicato con la lettera d (quindi: x1 , x2 , . . . , xd ). I
dati contenuti in diverse colonne sono misurazioni indipendenti tra di loro., per
esempio effettuate con sensori diversi. Lo scopo dell’algoritmo è quello di determinare una relazione statistica tra essi. Inoltre viene chiamato sample una
generica linea della tabella, costituita da d elementi; per esempio il secondo
sample delle tabelle è: 0 1. Tipicamente nelle reti neurali, così come in molti
algoritmi di apprendimento automatico si considera che i sample sono indipendenti tra di loro, quindi è possibile mischiare le righe della tabella senza nessuna
ripercussione. Questo indica che gli algoritmi trattati non possono essere utilizzati per fare stime di dati temporali visto che, in questo caso, una determinata
riga dipenderebbe da quelle precedenti (instanti di tempo precedenti). Analogamente a prima, in un caso generale si possono avere un certo numero di righe,
solitamente indicato con la lettera n. Quindi la tabella, a meno dei labels che
verranno analizzati a breve, avrà dimensione: n × d (4 × 2 in questo esempio).
Infine dobbiamo analizzare i labels, che rappresentano, forze, la parte fondamentale del data set. Questi sono le osservazioni che vengono fatte rispettivamente
per ogni sample, e che sono date per vere. Vengono solitamente indicati con
la lettera y e caratterizzano la funzione che si vuole stimare. Tipicamente il
vettore dei label ha solo una componente, quindi ha dimensione n × 1; tuttavia,
anche se non verrà considerato in questo laboratorio, e possibile averne di più,
in generale: y1 , y2 , . . . , yp . Questo implica che la rete neurale avrebbe bisogno
di più output, in particolare un numero pari a p. Anche se nel banale esempio
delle funzioni logiche non è strettamente vero di solito, per avere risultati statis-
ticamente sensati c’è bisogno che n d e che n p; dove con molto maggiore
si intende qualche decina di volte superiore.
Infine è bene ricordare che, ha causa di problemi matematici, è consigliato
avere tutti i valori del data set normalizzati tra i valori 0 e 1. Per fare questo
basta semplicemente scalare tutti i dati di ogni colonna all’interno di questo
intervallo utilizzando la formula:
αj =
Aj − mini {xi }
∀ j ∈ ad una colonna
maxi {xi } − mini {xi }
(5.1)
Il che vuol dire che ogni valore A appartenente ad una colonna i (con i che va
da 1 a d e j da 1 a n) deve essere sostituito con il valore α che viene calcolato
dividendolo per il risultato della sottrazione del massimo numero presente in
quella colonna con il minimo numero presente nella stessa colonna. Se il data
set lo richiede, ad esempio in un’immagine deve le colonne dei pixel non sono
propriamente indipendenti tra di loro, si può pensare di usare il massimo ed il
minimo numero presente in tutta la tabella invece che in una sola colonna. Infine
è importante ricordare che la normalizzazione non è sempre richiesta in tutti i
processi di Machine Learning perché può portare sia vantaggi che svantaggi.
5.3
il neurone artificiale: Perception
Questo è l’elemento su cui si basa una qualsiasi rete neurale, e può essere schematizzato come in figura 5.1. Da qui si possono vedere come i d input vengono
pesati per un numero corrispondente di weigh denominati con la lettera w. Da
notare anche la presenza di uno speciale tipo di peso che sta ad indicare il
pregiudizio, visto che non dipende dagli ingressi, che un certo neurone ha: il
bias, indicato con la lettera b. La combinazione lineare tra questi pesi e gli ingressi generano l’uscita stimata f . La quale, viene introdotta in una funzione di
attivazione utilizzata per classificare i sui valori all’interno degli intervalli dati
dal data set, y. Dal punto di vista matematico l’uscita stimata viene calcolata
come:
f = W ·X +b
(5.2)
Figura 5.1: Schematizzazione ideologica del Perception
dove con le lettere maiuscole si indicato le matrici o i vettori che raggruppano
ogni rispettiva lettera minuscola, in particolare:
 
w1
 w2 
 
; X = x1 x2 . . . x d
(5.3)
W =  . 
.
 . 
wd
Quindi, nell’esempio delle funzioni logiche della sezione precedente si potrà
calcolare l’uscita come:
w1
f=
· x1 x2 + b = (w1 x1 ) + (w2 x2 ) + b
∈ R
(5.4)
w2
Come descritto nell’ultima relazione questo è un numero reale, c’è bisogno ora
di una funzione in grado di classificarla. Quella più elementare è rappresentata
da un gradino, che matematicamente si descrive come:
(
f ≥ 0 if y = 1 (true)
(5.5)
f < 0 if y = −1 (f alse)
Questa activation function elementare si presta bene per l’esempio delle funzioni
logiche stimate da un unico neurone, ma è bene ricordare che non può essere
usata in una rete neurale. Per ovviare a questo problema, che sarà più chiaro
nella prossima sezione, se ne usa di più sofisticate come ad esempio la tangente
iperbolica o, più in generale, un qualsiasi sigmoid.
La capacità più interessante di questa formalizzazione è quella di poter utilizzare un processo chiamato: Hebbian Learning. Questo permette di calcolare
un valore numerico dei pesi e del bias in grado di classificare in modo soddisfacente i diversi stati della funzione che si intende stimare in modo automatico,
partendo da un qualsiasi punto iniziale; solitamente un numero random tra 0 e
1. La formula matematica in grado di definire tale processo può essere scritta
in modo semplificato come:


W (t = 0) = random




b(t = 0) = random






(
yt x0t if yt ft 6 0
(5.6)

W (t + 1) = W (t) +


0
altrimenti


(



yt if yt ft 6 0



b(t + 1) = b(t) + 0 altrimenti
Dove t sta a significare il numero di iterazioni, infatti questa formula può essere
implementata attraverso un tipo di algoritmo chiamato iterativo. Cioè un algoritmo che parte da un valore iniziale, random in questo caso, e poi richiama
se stesso per un certo numero di volte. Ad ogni volta che viene richiamato il
nuovo valore dei pesi viene calcolato attraverso la somma del valore vecchio e
una nuova componente che permette di addestrare il neurone. Notate che per
come è stato formalizzato il Perception (relazione 5.5) il valore di yt moltiplicato
per ft è minore di zero solo se i due hanno segno discorde, ergo c’è un’errore
tra la stima f e l’uscita reale y. In questo caso si aggiunge al vecchio peso una
quantità sperando che il nuovo sia migliore. Alternativamente, yt ft ha segno
positivo nel caso in cui le uscite sono concordi, quindi non c’è errore tra la stima
e il label; quindi non ha senso cambiare il valore del vecchio peso visto che da
un risultato corretto. La formula è stata tipicamente utilizzata grazie al Perception Learning Theorem, il quale permette di dimostrare che è ottima, cioè
non è possibile trovarne una migliore.
Questo tipo di implementazioni può risolvere solo casi lineari. Dal punto di
vista grafico questo significa che le due regioni di classificazione possono essere
suddivise solo da una retta e non da una linea curva. Esempi di questi sono
riportati nelle figure 5.2, 5.3; dove i punti di colore diverso stanno ad indicare
diversi valori di y. Da notare che non è possibile dividere la regione di spazio
con una retta in modo che la funzione Xor risulti classificata senza errori.
Di cruciale importanza risulta definire le condizioni per le quali terminare
le iterazioni dell’algoritmo descritto in precedenza. Infatti si è visto che non
possiamo pensare di iterare finché non ci siano più errori perché questo potrebbe
non terminare mai, come nel caso dell’Xor. Un approccio potrebbe essere quello
di definire un numero minimo di errori accettabili. Facendo così però si rischia
di terminare il calcolo senza trovare una soluzione migliore. Un altro approccio
potrebbe essere quello di definire un numero massimo di iterazioni possibili, ma
anche in questo caso la configurazione di questo parametro è delicata e dipende
strettamente dall’utilizzo che si intende fare dell’algoritmo.
Infine si intende mostrare in questa sezione la descrizione della regola di
learning (relazione 5.6) in pseudo-codice. Questo è un formalismo che si avvicina molto a come deve essere implementato in un certo linguaggio di programmazione ma presenta ancora delle ambiguità perché vuole discernere da un
linguaggio preciso.
Figura 5.2: Esempio di classificatore
lineare rispetto alla funzione OR
Figura 5.3: Grafico della funzione
logica XOR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
X := read all samples
Y := read all labels
W := initialise all weighs randomly
b := initialise the bias randomly
error := number of miss classified samples
i := 1 // data set index
Ni := 1 // number of iteration
while( error > 0) or ( i > max number of iteration) do
f = W X( i) + b
if( Y( i) * f <= 0) then
// Y(i) is a label: a number, X(i) is a sample: a vector
W := W + Y( i) * X( i)^T
b := b + Y(i)
error := number of miss classified samples
end if
i := i + 1
N1 := Ni + 1
if( i > n) // start again over samples ( n is the number of sample)
i := 0
end while
5.4
Multi-layer Perception
Un metodo ampiamente usato per poter discriminare regioni dello spazio in modo non lineare, ad esempio per riuscire a stimare la funzione Xor senza errori, è
appunto l’MLP. Questo è tipicamente il primo caso di Neural Network visto che
usa un’insieme di semplici neuroni per compiere qualcosa di più complesso; un
esempio di rete e proposto in figura 5.4. In particolare la figura mostra una rete
a due uscite, anche se solitamente si usa configurazioni ad una sola. Inoltre presenta un layer zero, quello di input, ed un layer finale che rappresenta l’output.
Non c’è particolari limitazioni sul numero di layer intermedi, chiamati anche
hidden; ma solitamente se ne usa uno solo per problemi tecnici di prestazioni
durante l’addestramento che altrimenti, può diventare molto lungo. Il numero
di neuroni all’interno dei layer dipende dalle applicazioni e dal tipo di data set a
disposizione e, tipicamente, sono parametri che lo sviluppatore deve configurare
attraverso prove statistiche.
Anche in questo caso e possibile utilizzare meccanismi di addestramento sulla
falsa linea di quello visto in precedenza. Tuttavia è impossibile usare esattamente quel meccanismo perché non tiene di conto delle interazioni tra neuroni.
Però il metodo di analisi, così come succede in tutti i processi scientifici e in
molti casi della vita quotidiana rimane lo stesso: definire un certo tipo di errore
che sia misurabile, in base ad esso cambiare il modo di agire e infine misurare
l’errore nuovamente per vedere se si è migliorato; se no ripetere l’operazione.
Dal punto di vista matematico l’errore viene definito attraverso una funzione
continua che dipende dagli ingressi. Questo viene fatto perché così si hanno
a disposizione molti metodi di ottimizzazione che, in pratica, cercano di minimizzare l’errore. Ottenendo così buoni risultati senza dover necessariamente
muoversi casualmente alla ricerca di un miglioramento, magari provando tutte
le possibili soluzioni. Sempre dal punto di vista matematico questo si traduce
nel calcolo del gradiente, o derivate spaziali (lungo tutte le dimensioni d del data
set). In generale la derivata sta ad indicare la pendenza di una funzione così,
esattamente come l’acqua scende sempre a valle seguendo la pendenza massima,
si possono usare metodi che trovano il minimo errore scendendo la funzione che
lo definisce nel modo più velocemente possibile. Purtroppo la matematica che
sta alla base di questi meccanismi di calcolo diventa velocemente complessa e
per questo non andremo nei suoi dettagli. Tuttavia è possibile fare delle analogie
per cercare di capire come funziona l’idea di base di tali approcci. Un metodo
molto utilizzato viene chiamato Gradient Descendent e si basa sul calcolo della
pendenza in diverse direzioni ma rimanendo sostanzialmente fermo nello stesso
punto della funzione, per poi scegliere il migliore. Immaginatevi di trovarvi su
una collina bendati e che volete scendere il più possibile a valle. Potete pensare
di fare un passo in una delle quattro direzioni cardinali, poi tornare indietro e
provarne un’altra. Quando ne avrete esplorate tutte siete in grado di sapere
quale è la direzione che scende di più, e allora potete decidere di percorrerla
facendo dei passo più o meno veloci. Nel momento in cui vi accorgete che la
collina ricomincia a salire ancora vuol dire che siete in quello che viene detto
minimo locale. Ora potete decidere di fermarvi lì o riprovare il metodo partendo
ad esplorare le direzioni. Notate che se per qualche motivo decidete di correre
potreste non accorgervi di un punto in cui la collina è più bassa o, alternativamente, potete non accorgervi di un piccola cunetta che vi avrebbe fatto fermare
nel caso in cui andiate molto lentamente. Questo parametro dell’algoritmo si
chiama appunto step ed è cruciale per la configurazione del sistema. Ovviamente
esistono molti altri tipi di approcci ma la filosofia con cui li si costruisce rimane
più o meno sempre la stessa, anche se richiede una formalizzazione matematica più rigorosa e complessa. Un possibile esempio di difficoltà tecniche che si
possono incontrare può essere il fatto che la pendenza viene calcolata in termini
di derivata e la derivata di una funzione discontinua non può essere calcolata. Dal punto di vista intuitivo immaginate di vole definire numericamente la
pendenza del muro di un grattacielo; questa sarebbe infinita. Questo è il motivo del perché non è possibile usare la funzione di attivazione definita della
sezione precedente e ci si deve affidare a funzioni più dolci come i sigmoid, che
complicano ulteriormente la trattazione. Fortunatamente, grazie al concetto di
modularità visto all’inizio del corso, e a quello che è stato visto nell’introduzione
di questo capitolo non è necessario conoscere tutti i dettagli di un algoritmo per
poterlo adoperare, tuttavia risulta indispensabile conosce a fondo il significato
dei parametri che richiede. Per questo analizzeremo ora come viene formalizzata
una semplice rete neurale.
All’interno del schema è possibile identificare i pesi w, i bayas b e le uscite
stimate di ogni neurone f . Inoltre ci sarà bisogno delle indicazioni del gradiente
∆. Queste grandezze hanno un apice, che sta ad indicare il layer l a cui appartengono, e due pedici che indicano il loro significato e quindi la dimensione
delle matrici in cui sono raccolte. I layer sono numerati da sinistra a destra,
mente i neuroni dall’alto verso il basso In particolare:
• i ∼ j indica il neurone j-esimo della layer i-esimo.
l
• wi,j
è il peso della connessione tra il neurone j-esimo (del layer l) ed il
neurone i-esimo (del layer l − 1).
• bli è il bias del neurone i-esimo del layer l-esimo.
Figura 5.4: esempio di rete neurale a più strati
l
• fi,j
è l’output del neurone j-esimo del layer l, quando l’i-esimo sample è
applicato (uscita per il sample Xi )
l
• δi,j
è il gradiente del neurone j (del layer l) quando l’i-esimo sample è
applicato (errore per il sample Xi )
Seguendo queste definizione è possibile scrivere le matrici W , B, F , e ∆ per
ogni layer. Nel particolare esempio di figura si ha:
W1

1
w1,1
= 
1
w2,1
B1
1
w1,2
1
w1,3
1
w2,2
1
w2,3

 ; W2

w2
 1,1
 2
= w2,1

2
w3,1
 
 
b1
 1
b21
 1
= b2  ; B 2 =  
 
b22
1
b3
2
w1,2


2 

w2,2

2
w3,2
(5.7)
(5.8)
F0

x
 1,1

x2,1
= 

x3,1

x4,1
x1,2



x2,2 
 ; F1

x3,2 

x4,2
1
∆

δ1
 1,1
 1
δ2,1
= 
 1
δ3,1

1
δ4,1

f1
 1,1
 1
f2,1
= 
 1
f3,1

1
f4,1
1
δ1,2
1
δ2,2
1
δ3,2
1
δ4,2
1
f1,2
1
f2,2
1
f3,2
1
f4,2
1
f1,3

1 

f2,3
 ; F2
1 
f3,3 

1
f4,3

1
δ1,3


1 

δ2,3
 ; ∆2
1 

δ3,3

1
δ4,3

δ2
 1,1
 2
δ2,1
= 
 2
δ3,1

2
δ4,1

f2
 1,1
 2
f2,1
= 
 2
f3,1

2
f4,1
2
f1,2


2 

f2,2
 (5.9)
2 

f3,2

2
f4,2

2
δ1,2

2 

δ2,2

2 

δ3,2

2
δ4,2
(5.10)
Dove, dato hl , il numero di neuroni per il layer l. Si può dire in modo generale
che: W l ∈ Rhl−1 ×hl (con h0 = d) ; B l ∈ Rhl ; F l ∈ Rhl ×n e ∆l ∈ Rhl ×n .
Grazie a questa definizione è possibile utilizzare le regole di prodotti matriciali40
(un valido aiuto può venire anche dal software Matlab da cui viene ripreso il
simbolo anche *.) per costruire il seguente pseudo-codice in grado di addestrare
di un multi layer Perception:
1
2
3
4
5
6
7
8
9
10
11
12
13
Ni := 0 // number of iteration
step := 0.75
old_NMSE := infinity //Normalized Squared Error
end := false
// initialise all weighs (w) and bias (b) elements
W := 1.472*(1-2*random( between 0 and 1))/ numberOfInputsNeuron
B := 1.472*(1-2*random( between 0 and 1))/ numberOfInputsNeuron
while( not( end))
//feed forward phase
// L is the number of layers
for i := 1 to L
F(i) := tanh( F(i-1) * W(i) + 1*B^T) // index i should be a
apex
end for
14
15
16
17
18
19
20
21
// back propagation output layer (D is delta)
// (Nl is the number of neurons in the output layer)
D := 2.0 / ( n*Nl * (Y - F(L)) *. (1 - F(L)) *. F(L))
// back propagation in the remaining layers
for i = L-1 to 1
D(i) := D(i+1) * W(i+1)^T *. (1 - F(i) *. F(i))
end for
22
23
24
25
26
27
28
29
30
// compute error
for i := 1 to L
// error over the weighs
Dw := F(i-1)^T * D(i)
// error over the bias
Db = D(i) * 1
end for
31
32
33
34
35
36
37
38
39
40
41
42
43
// check if done
Ni := Ni + 1
GN := squareRoot( summ for all the components ( Dw + Db)^2) //
gradient norm
error := 0
for i := 1 to n
if ( Y(i) * F(L)(i) < 0) then
error := error + 1
end if
end for
if( GN < 10^-4) or ( NMSE < threshold) or (Ni > threshold) or
(error = 0) then
end := true
save weighs and bias for all layers
end if
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// Volg’s acceleration
if( NMSE < old_NMSE) then
step := step * 1.05
alpha := 0.9
else if ( NMSE <= old_Nmese * 1.05) then
step := step * 0.7
alpha := 0
old_NMSE := NMSE
else
step := step * 0.7
alpha := 0
// back step
for i := 1 to L
Dw(i) := old_Dw(i)
Db(i) := old_Db(i)
W(i) := W(i) - step_W(i)
B(i) := B(i) - step_Bi)
end for
end if
64
65
66
67
68
69
70
71
72
73
74
// update weighs
for i := 1 to L
old_Dw(i) := Dw(i)
old_Db(i) := Db(i)
step_W := step * Dw(i) + alpha * step_W(i)
step_B := step * Db(i) + alpha * step_B(i)
W(i) = W(i) + step_W(i)
B(i) = B(i) + step_B(i)
end for
end while
Da qui si nota che il codice è diviso principalmente in tre parti, una in cui si
effettua quella che viene chiamata Back Propagatio ed un’altra in cui cerca di
raggiungere il minimo errore nel modo più velocemente possibile; infine si esegue
l’aggiornamento dei pesi ricercando una soluzione migliore, come si è visto anche
nel neurone singolo. In particolare prima si esegue il processo di feed forward
attraverso il quale si propagano tutte le uscite stimate f attraverso la rete fino
all’uscita. Ora siamo in grado di usare questo risultato per calcolare l’errore ∆.
Tuttavia il valore rimane limitato al neurone di uscita, per questo c’è bisogno
di propagarlo indietro nella rete e trovare una stima degli errori per tutti i
layer. Successivamente l’errore viene tradotto in termini di step da compiere
sia per i pesi che per il bias. Dopo di c’è si effettua un controllo per vedere
se il risultato ottenuto e soddisfacente in termini di classificazioni sbagliate,
errore nella funzione continua e stima di quanto si può migliore continuando ad
iterare. Il risultato viene considerato buono in termini di limiti massimi, e se
è vero l’algoritmo si fermerà alla prossima iterazione. Continuando si compie
quella che viene chiamato un’accelerazione, cioè si cerca la direzione che tenta
di minimizzare più velocemente possibile l’errore stimato. Infine si aggiorna
i pesi in modo che nella prossima iterazione l’errore tendi a diminuire. Da
notare che molte regole empiriche vengono adoperate in questo tipo di calcoli e
sicuramente esistono molti metodi diversi per implementare un comportamento
simile, fondamentalmente basato su diversi tipo di applicazioni. Tuttavia questo
rimane lo schema guida più utilizzati in tutti i processi di addestramento di una
rete neurale.
5.5
Esercizio 5.1: Implementare un Perception
Si chiede in questo esercizio di implementare un Perception seguendo lo pseudocodice proposto nelle sezioni precedenti. Si consiglia di scaricare ed importare
la libreriaJama-1.0.3.jar41 (seguendo le istruzioni date nella lezione 2). Queste
permettere di compiere semplicemente operazioni tra matrici utilizzando la
classe Matrix. Questa classe contiene anche i metodi:
times( Matrix m),
transpose( Matrix m), plus( Matrix m) . . . . Inoltre si consiglia di usare la classe
DataSetFactory (che a sua volta ritorna un’istanza della classe DataSet) per
creare il data set della funzione logica voluta. Un esempio di utilizzo è:
1
2
3
4
5
6
// create data set
DataSet dataSet = DataSetFactory.getOrSet();
//DataSet dataSet = DataSetFactory.getAndSet();
//DataSet dataSet = DataSetFactory.getXorSet();
// stampare il data set
System.out.println( "
features \t | label " +
System.getProperty("line.separator") + dataSet);
Infine si mette a disposizione anche la classe MatrixComputation che, oltre ad essere essenziale per poter eseguire i precedenti comandi è anche utile in fase di debugging per vedere cosa stà facendo il codice. I metodi che mette a disposizione
sono: printMatrixDimention( Matrix m), printMatrix( Matrix m), getRowVector( double[] vector) e getColumnVector( double[] vector) che ritornano oggetti
di tipo Matrix.
Inoltre si ricorda che per creare un vettore, ad esempio a valori random tra
0 e 1, si può digitare:
1
2
3
4
5
double[] w = new double[ numberOfInput];
for( int i = 0; i < numberOfInput; i++){
w[ i] = Math.random() * 0.1;
}
double bias = Math.random()*0.1;
6
7
// converti in Matrici
Matrix weighs = MatrixComputation.getColumnVector( w);
dove numberOfInput è una variabile intera che indica la lunghezza di un sample
(d). Mentre per calcolare l’uscita f , utilizzando gli oggetti di tipo Matrix, si
può utilizzare:
1
double f = sample.times(weighs).getArray()[0][0] + bias;
Mentre i pesi possono essere aggiornati usando i comandi;
1
2
3
4
5
for( int i = 0; i < weighs.getRowDimension(); i++){
Double elem = weighs.get( i,0) + label * sample.get( 0,i);
weighs.set( i, 0, elem);
}
bias = bias + label;
Infine il numero di classificazioni errate si trova facendo:
1
2
3
4
5
6
Integer missClassified = 0;
for( int i = 0; i < label.getRowDimension(); i++){
if( this.computeError( dataSet.getMatrixSample( i), label.get(
i, 0))){
missClassified = missClassified + 1;
}
}
Dove il metodo computeError( Double output, Double label) svolge l’operazione
della riga numero 9 dello pseudo-codice.
5.6
Esercizio 5.2: Organizzazione delle classi in
un MLP
Conoscendo in modo approssimativo la struttura di MLP, come si può pensare
di suddividere il flusso di dati tra le classi? Quali classi andrebbero create e
quale sarebbe il loro ruolo? Prova a scrivere su carta gli attributi, i metodi e i
costruttori che dovrebbero avere, specificando tutti i dati e il loro tipo.
Capitolo 6
Fondamenti di Machine
Learning attraverso le
Neural Network
6.1
Introduzione
Nel precedente capitolo si è visto un particolare tipo di algoritmo appartenente
all’insieme delle procedure tipicamente utilizzate per il Machine Learning. Mentre, in quest’ultima sezione del documento, si vuole introdurre le regole che stanno alla base della teoria dell’apprendimento automatico. Queste si basano su
approcci statistici che permettono di stimare qual’è la configurazione migliore
dei parametri di un algoritmo per minimizzare il numero di errori fatti in termini di classificazioni. Infatti, analogamente come per le Neural Network, la
maggior parte delle procedure vengono utilizzate per classificare l’uscita all’interno di range di valori dipendentemente dagli ingressi che, per loro natura, non
mostrano dipendenze evidenti tra di loro. Il procedimento tipicamente adoperato in questo tipo di studi è quello di creare un modello matematico, solitamente
individuato da una serie di parametri (come ad esempio i pesi di una rete neurale ed il numero di neuroni utilizzati), in modo che poi questo possa essere
utilizzato successivamente per classificare eventi descritti da un’insieme di dati
statici: un sample. La caratteristica principale di tale metodo è quella di fare un
preliminare processo di addestramento (training) attraverso il quale i parametri
vengono identificati automaticamente dall’evoluzione dell’algoritmo; da notare
che tale apprendimento è basato su scelte casuali, quindi può essere diverso ogni
volta e, soprattutto, non è possibile sapere come venga effettivamente calcolato.
Per fare ciò è necessario un data set (come descritto nel capitolo precedente), che
presenti informazioni in grado di legare tutti i sample ad una classificazione che
viene considerata assolutamente vera: i label. Tipicamente, il processo di training è computazionalmente dispendioso, in generale può richiedere anche molte
ore di calcolo da parte del computer, visto che necessita di un’analisi statistica
e quindi di un gran numero di prove. D’altro canto, una volta che il modello è
identificato, utilizzarlo per classificare i dati non comporta particolari problemi
ed è solitamente molto veloce. Per questi motivi, molto spesso, si predilige uti69
lizzare linguaggi si programmazione dedicati per il calcolo matriciale, come ad
esempio Matlab o R-reccomanded, attraverso i quali identificare i parametri che
descrivono il modello. Successivamente, linguaggi come Java o C/C++ possono
essere utilizzati per usare tali modelli durante applicazioni reali, questa scelta
può essere motivata dal fatto che sono compatibili con un numero maggiore di
dispositivi e più preformanti. Tuttavia, questa rimane una scelta più pratica
che tecnologica perché linguaggi come Java non mostrano nessuna limitazione
di implementazione a riguardo.
Un punto che è sicuramente alla base di ogni processo di apprendimento è
quello descritto dal Non free lunch Theorem. Questo dimostra, attraverso
una formalizzazione matematica, che a priori non è mai possibile dire che un
algoritmo è migliore di un altro. Questo vuol dire che, per un generico data set,
nessuno algoritmo può essere migliore di una scelta casuale. Infatti è necessario,
per ogni studio, testare diversi algoritmi su un particolare data set. Solo così sarà
possibile affermare che un algoritmo è migliore di un altro in quel determinato
caso. In maniera informale, nella comunità, si dice che tutti i modelli sono
sbagliati, ma alcuni sono utili. Gli esempi più popolari di algoritmi utilizzati
nell’ambito statico (cioè dove i sample sono indipendenti tra di loro, per esempio
non sono correlati di istanti di tempo successivi) sono: Neural network e Naive
bias, Support Vector Machine, k-nearest neighbor, Decision Tree e molti altri.
Purtroppo non avremo modo di vedere come questi approcci funzionino ma è
importante considerare che l’idea di fondo rimana sempre la stessa, cioè: definire
un’errore e minimizzarlo. Il metodo attraverso la quale è possibile comparare
i diversi risultati dati da ogni tipo di algoritmo si basa sulla stima dell’errore
generalizzato, che verrà descritto nelle prossime lezioni, e che risulta anche utile
per cercare di migliorare i risultati dati da ognuno di questi approcci.
Per rendere più chiara questa trattazione procederemo considerando solo il
MLP, descritto nel capitolo precedente, ma è importante ricordare che i metodi
descritti in questo capitolo rimangono veri per tutti i tipi di algoritmi sopra citati. Infine per facilitare l’implementazione e la visione pratica dei risultati che
si possono ottenere si considererà l’utilizzo della piattaforma encog 3.2.0 (distribuita attraverso l’archivio jar42 che definisce le sue API, una GUI e anche
molti esempi pratici e utili). Questa viene scelta inoltre perché presenta una dettagliata documentazione nel campo delle reti neurali consultabile gratuitamente
in rete. Tuttavia è bene ricordare che questa libreria non presenta implementazioni per altri tipi di algoritmi e procedure utilizzate per questo tipo di studio,
a proposito si propone anche la più completa piattaforma Java weka43 .
6.2
Stima corretta della funzione logica Xor
Come visto nel capitolo precedente, il solo neurone non è in grado di classificare
senza errori i valori descritti dalla funzione logica Xor. Per fare questo c’è
bisogno di un classificatore non lineare come ad esempio una Neural Network
che utilizza un metodo di back propagation per identificare i propri parametri.
Analizziamo ora il codice attraverso il quale è possibile fare questo, utilizzando
la libreria encog e le classi utilizzate per l’esercizio precedente (data setFactory,
data set e MatrixComputation).
1
import org.encog.Encog;
2
3
4
5
6
7
8
9
import org.encog.engine.network.activation.ActivationSigmoid;
import org.encog.ml.data.MLData;
import org.encog.ml.data.MLDataPair;
import org.encog.ml.data.MLDataSet;
import org.encog.ml.data.basic.BasicMLDataSet;
import org.encog.neural.networks.BasicNetwork;
import org.encog.neural.networks.layers.BasicLayer;
import
org.encog.neural.networks.training.propagation.back.Backpropagation;
10
11
12
import Networking.DataSet;
import Networking.DataSetFactory;
13
14
public class EncogXorExample {
15
16
17
18
public static void main(final String args[]) {
// carico la funzione Xor, ovviamente funziona anche per la
Or e And
DataSet dataSet = DataSetFactory.getXorSet( false);
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// crea una rete vuota
BasicNetwork network = new BasicNetwork();
// aggiungi un layer con tanh activation faction, bayas
e 2 neuron (ingresso)
network.addLayer(new BasicLayer(null,true,2));
// aggiungi un layer con Sigmoid activation function,
bayas e 5 neuroni nascosti
network.addLayer(new BasicLayer(new
ActivationSigmoid(),true, 5));
// aggiungi un layer con Sigmoid activation function,
senza bayas e 1 nuerone (uscita)
network.addLayer(new BasicLayer(new
ActivationSigmoid(),false,1));
// stanzia la rete
network.getStructure().finalizeStructure();
network.reset();
// inizzializza adeguatamente i pesi
new ConsistentRandomizer(-1,1,500).randomize(network);
33
34
35
// crea il training set compatibile con encog
MLDataSet trainingSet = new BasicMLDataSet(
dataSet.getData(), dataSet.getArrayLabels());
36
37
38
39
40
41
42
43
44
// configura back propagation per addestrare la rete
final Backpropagation train = new
Backpropagation(network, trainingSet, 0.7, 0.3);
// inizializza il numero di iterazioni
int epoch = 1;
// addestra fino a che l’errore e’ grande ( all’inizio
l’errore e’ 0)
while(train.getError() > 0.01 || epoch == 1) {
// fai una nuova iterazione e stampa l’errore
train.iteration();
System.out.println("Epoch #" + epoch + " Error:"
+ train.getError());
// aggiorna il numero di iterazioni
epoch++;
}
45
46
47
48
49
// ora che e’ stato creato il modello testa la rete
System.out.println("Neural Network Results:");
for(MLDataPair pair: trainingSet ) {
final MLData output =
network.compute(pair.getInput());
System.out.print(pair.getInput().getData(0) + ","
+ pair.getInput().getData(1) + ", output = "
+ output.getData(0) + ", label = "
+ pair.getIdeal().getData(0));
50
51
52
53
54
55
56
57
58
// classificazione
if( output.getData( 0) >= 0.5)
System.out.println("classification:
59
60
61
true");
else
62
System.out.println("classification:
63
false");
}
64
65
// chiudi l’Encog framework
Encog.getInstance().shutdown();
66
67
}
68
69
}
Da notare che questa libreria richiede di utilizzare labels pari a 0 per identificare il valore falso (e non -1 come nel capitolo precedente) e 1 per indicare il
valore vero. Inoltre è importante notare come sia intuitiva la creazione di rete
complesse, dove però è importante ricordare che il numero di neuroni nel primo layer deve necessariamente essere uguale al numero di features del data set
d. Mentre il numero di neuroni presente nell’ultimo layer deve essere uguale al
numero di colonne dei label p. Inoltre è importante sapere che il flag booleano
introdotto come parametro di ingresso all’oggetto BasicLayer indica la presenza
o meno di quello che viene chiamato bias di layer44 , cioè un bias comune a
tutti i neuroni del layer, diversamente dalla composizione classica di una rete
neurale. Inoltre, di default questa classe considera una funzione di attivazione
di tipo tanh (tangente iperbolica), ma può essere configurata ad esempio come
un sigmoid.
Una volta definita la forma della rete viene configurato il tipo di training che
si intende fare, in questo caso back propagation45 . Notate che questo prende
in ingresso due parametri, rispettivamente: il learning rate che indica quanto
il gradiente influenza l’aggiornamento dei pesi; ed il moumentom che indica
quanto la precedente iterazione dell’apprendimento influenza quella successiva,
utilizzato per evitare minimi locali. Se questi parametri non vengono dati la
libreria li sceglie in modo automatico. Dopo di che inizia l’addestramento vero
e proprio che si compie fino a che l’errore non è abbastanza basso. Ad ogni
ciclo si stampa il numero di iterazioni ed il valore dell’errore. Se l’errore scende
all’aumentare delle iterazioni significa che la rete sta imparando, se il valore
rimane costante siamo in un caso di Neuron Paralyses. Questo significa che non
si è in grado di trovare un punto che dia errore minore e quindi il programma
non è mai in grado di terminare.
Alla fine di questo ciclo, il modello matematico è stato identificato e tutti i
suoi pesi hanno un valore ben preciso. Ora potrebbe essere possibile salvarli ed
utilizzarli per risolvere il problema della Xor logica tutte le volte che si vuole
senza dover rifare il training. In questo semplice esempio si usano solo per
una volta in modo da poter stampare a schermo i risultati dell’algoritmo. Da
notare che l’uscita è di tipo continuo e quindi necessita di una classificazione,
un esempio giustificato dal range di valori della y dati nel data set, può essere
fatta semplicemente considerando:
(
y = 1 if f ≥ 0.5
(6.1)
y = 0 if f < 0.5
6.3
Identificazione di volti umani Maschili o Femminili da Immagini
A meno di piccole accortezze pratiche che avremo modo di vedere poco più
avanti, questo programma è in grado di risolvere problemi ben più complessi
di quello visto in precedenza. Un famoso esempio, ormai utilizzato in molti
dispositivi muniti di fotocamera, è quello del riconoscimento di un volto umano
all’interno di un’immagine.
In particolare analizzeremmo immagini di volti umani 60 × 60 pixels, dove
si intende classificare un volto maschile differentemente da uno femminile; due
esempi sono dati in figure 6.1, 6.2. In questo caso il data set viene fornito sotto
forma di file testuale (di estenzione csv oppure txt), questo contiene numeri
reali normalizzati tra 0 e 1 che sono stati visualizzati in scala di grigi (dove 0 è
bianco e 1 è nero), mentre i labels valgono 0 per ogni immagine che rappresenta
un volto femminile e 1 per ogni volto maschile; infine il data set è composta da
90 immagini. Questo vuol dire che avremo una tabella formata da 90 colonne
Figura 6.1:
maschile
Esempio di volto
Figura 6.2:
femminile
Esempio di volto
(n = 90) e 3600 features (d = 60 · 60 = 3600). Notate che per quanto si è detto
nel capitolo precedente questo data set non è dato nella forma migliore visto
che n non è molto maggiore di d tuttavia è adeguato per il tipo di esempio che
si vuole proporre. Infine notate che un numero elevato di pixel in un’immagine,
richiede di avere un data set con molti sample e quindi di avere le immagini di
molte persone diverse. Inoltre riduce notevolmente le prestazioni dell’algoritmo
perché si devono fare molti calcoli, questi sono i motivi principali del perché si
predilige immagini a bassa risoluzione.
Le caratteristiche del data set così composto rientrano appieno nelle specifiche richieste da un algoritmo di Machine Learning, quindi si può pensare di
utilizzare il programma di prima per addestrare la rete neurale e cercare di
trovare buoni risultati. Un primo problema pratico che si affronta è quello che
sarà difficile avere una perfetta classificazione, quindi sarà necessario configurare
un numero massimo di iterazioni dopo il quale terminare un algoritmo che altrimenti è molto probabile che non finisca mai. Tuttavia questo non è sufficiente
ad ottenere buoni risultati perché, ha causa del non free lunch theorem, i valori dei parametri configurati nella precedente prova non è detto che funzionino
in modo corretto in un altro data set. Infatti così facendo sarà estremamente
probabile paralizzare la rete ad un’errore elevato.
L’unico modo che viene dato a disposizione per trovare il nuovo tipo di valori
è provare fino a che non si arriva ad un valore accettabile, anche se un aiuto
intuitivo, anche se non sempre valido, può venire dalla conoscenza del significato
dei parametri, dall’esperienza e da alcune regole empiriche. Ad esempio si è
notato che il metodo di back propagation tende a funzionare male nel caso in
cui si aumenta troppo il numero dei neuroni nascosti e dei layer. Inoltre, un
basso valore di learning rate permette di cercare il minimo facendo piccoli passi,
utile quando le differenze che si vogliono discriminare sono basse; poco utile
quando si vuole provare un grande intervallo di soluzioni che minimizzino la
funzione errore (da cui si parte da un valore random). Infine, un elevato valore
del momento permette di uscire da minimi locali che paralizzano il processo di
apprendimento probabilmente in un punto non accettabile, ma potrebbe causare
la perdita di qualche punto di minimo e quindi di soluzioni buone. Infine si deve
considerare che gli stessi parametri che danno un buon risultato una volta,
potrebbero dare risultati cattivi altre volte perché la scelta del punto iniziale è
casuale.
Purtroppo, tutte le indicazioni sono qualitative, non quantitative e presentano i propri pro e contro. Inoltre questi variano a seconda del data set e
i parametri si riferiscono al solo processo di back propagation visto che ogni algoritmo si basa su valori definiti in modo diverso. Di seguito si propone
un’implementazione che ha buone probabilità di dare risultati non ottimi ma
accettabili:
1
2
3
4
5
6
7
8
9
import
import
import
import
import
import
import
import
import
org.encog.Encog;
org.encog.engine.network.activation.ActivationSigmoid;
org.encog.mathutil.randomize.ConsistentRandomizer;
org.encog.ml.data.MLData;
org.encog.ml.data.MLDataPair;
org.encog.ml.data.MLDataSet;
org.encog.ml.data.basic.BasicMLDataSet;
org.encog.neural.networks.BasicNetwork;
org.encog.neural.networks.layers.BasicLayer;
10
import
org.encog.neural.networks.training.propagation.back.Backpropagation;
11
12
13
import Networking.DataSet;
import Networking.DataSetFactory;
14
15
16
public class EncogFaceExample {
17
18
19
20
21
public static void main(final String args[]) {
// ottieni il data set
DataSet dataSet = DataSetFactory.getFaceSet( false);
MLDataSet trainingSet = new BasicMLDataSet(
dataSet.getData(), dataSet.getArrayLabels());
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// crea una rete vuota
BasicNetwork network = new BasicNetwork();
// aggiungi un layer con tanh activation faction, bias e
n neuroni (ingresso)
network.addLayer(new BasicLayer(null, false,
dataSet.getNumberOfFeatures()));
// aggiungi un layer con Sigmoid activation function,
bias e 5 neuroni
network.addLayer(new BasicLayer(new ActivationSigmoid(),
false, 10));
// aggiungi un layer con Sigmoid activation function,
senza bias e p nueroni (uscita)
network.addLayer(new BasicLayer(new ActivationSigmoid(),
false, dataSet.getNumberOfLabel()));
// stanzia la rete
network.getStructure().finalizeStructure();
network.reset();
// inizzializza adeguatamente i pesi
new ConsistentRandomizer(-1,1,500).randomize(network);
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// configura back propagation per addestrare la rete
final Backpropagation train = new
Backpropagation(network, trainingSet, 0.0001, 0.3);
train.fixFlatSpot(false);
// inizializza il numero di iterazioni
int epoch = 1;
// addestra fino a che l’errore e’ grande ( all’inizio
l’errore e’ 0)
while( (train.getError() > 0.1 || epoch == 1 ) && epoch
< 70000) {
// fai una nuova iterazione e stampa l’errore
train.iteration();
System.out.println("Epoch #" + epoch + " Error:"
+ train.getError());
// aggiorna il numero di iterazioni
epoch++;
}
50
51
// testa la rete
System.out.println("Neural Network Results:");
Integer numberOfCorrect = 0;
Integer numberOfMissClassified = 0;
for(MLDataPair pair: trainingSet) {
final MLData output =
network.compute(pair.getInput());
System.out.print(pair.getInput().getData(0) +
"," + pair.getInput().getData(1)
+ ", output = " +
output.getData(0) + ", label = " + pair.getIdeal().getData(0));
52
53
54
55
56
57
58
59
// classificazione
if( output.getData( 0) > 0.5){
System.out.println( " classification:
60
61
62
true");
if( pair.getIdeal().getData( 0) == 0)
numberOfMissClassified =
63
64
numberOfMissClassified + 1;
else
65
numberOfCorrect =
66
numberOfCorrect + 1;
}else{
67
System.out.println( " classification:
68
false");
if( pair.getIdeal().getData( 0) == 0)
numberOfCorrect =
69
70
numberOfCorrect + 1;
else
71
numberOfMissClassified =
72
numberOfMissClassified + 1;
73
}
}
System.out.println( " number of correct classification :
" + numberOfCorrect);
System.out.println( " number of miss
classification :
" + numberOfMissClassified);
74
75
76
77
78
// chiudi l’Encog framework
Encog.getInstance().shutdown();
79
80
}
81
82
83
}
Da notare come la stima dell’errore viene fatta durante il test della rete, contando il numero di volte che la classificazione è corretta, sia per le facce maschili
che femminili; così come contando il numero di volte che la classificazione risulta
sbagliata.
Infine è interessante notare i risultati che si ottengono nel caso in cui si
cerca di stampare le tabelle dei pesi addestrati W l , come immagini in scala di
grigio, esattamente seguendo gli stessi procedimenti fatti per creare il data set
dall’immagine. Si può notare che quello che risulta sono immagini che ricordano
alcuni tratti delle facce umane. Dal punto di vista intuitivo è come se ogni
Figura 6.3: pesi del secondo, terzo e quinto neurone del layer nascosto
rispettivamente
neurone si specializzasse a riconoscere un determinato carattere della fisionomia
umana; e questo processo è anche visibile se si affronta il problema di dover
riconoscere i caratteri di una scrittura manuale. Alcuni esempi sono mostrati
nella figura 6.3.
6.4
Confronto tra risultati ottenuti con diversi
parametri e algoritmi
Risulta importante sottolineare che il metodo utilizzato segue un’idea giusta ma
i dati ottenuti fin qui, anche se buoni, non hanno nessun significato statistico.
Infatti non è stato fatto ancora niente per determinare l’errore se consideriamo di
usare un sample di cui l’algoritmo non conosce il label; un nuovo dato durante
l’utilizzo della rete dopo averla addestrata per esempio. Per fare questo c’è
bisogno di utilizzare tutta una serie di accorgimenti statistici che analizzeremo.
Prima però c’è bisogno di introdurre un tipo di problema grazie al quale è
possibile capire perché i risultati ottenuti fino ad ora non hanno alcun senso;
questo viene chiamato overfitting e underfitting46 . In pratica, dal punto di
vista statistico, più sample si hanno a disposizione più la stima sarà accurata
e questo è vero, ma non è vero che facendo il processo di learning su molti
sample il processo di classificazione migliora. In fatti quello che si farebbe in
questo caso è specializzarsi troppo sui dati ottenuti, avendo così un problema di
overfitting. Immaginate di avere molti dati, in questo caso è più probabilmente
che ci siano sample rumorosi, ad esempio volti maschili che assomigliano molto
a volti femminili. Questo è un facile errore per il processo di classificazione e
si può pensare che sia bene cercare di eliminarlo. Tuttavia, se ci riusciamo, il
processo ci porta ad addestrare una rete che è troppo specializzata sui dati a
disposizione durante il training e quindi, quando la si usa per un dato di cui non
conosciamo il label è più probabile che una faccia femminile venga caratterizzata
come maschile perché simile a quel maschio che era simile ad una donna usato
durante il training. In sostanza, quello che si vuole è che un volto maschile molto
simile a quello femminile sia caratterizzato in modo sbagliato durante il training,
questo porterà la rete ad essere più accurata durante il test. D’altro canto il
problema di underfitting è esattamente l’opposto, se usiamo pochi sample si
rischia il caso in cui la rete non si specializza affatto a risolvere il problema di
classificazione che si vuole risolvere. Quindi quello che vogliamo considerare non
sono solo i risultati dell’errore durante il training ma quello che viene chiamato
errore generalizzato. Cioè, in poche parole, l’errore che si ha quando si usano
sample non utilizzati durante la fase di addestramento.
Per risolvere questo tipo di problema si divide tipicamente il data set in tre
parti: training, validation e test set dove, indicativamente, sono formati rispettivamente dal 70%, 20% e 10% dell’intero data set (presi in modo casuale).
L’idea di base è quella di definire una serie di parametri che si vuole testare e
di cui si cerca il migliore. Fare l’addestramento per tutti i valori prima definiti
utilizzando, solo ed esclusivamente, il training set e salvando i modelli ottenuti. Utilizzarli quindi per stimare l’errore usando solamente il validation set e
verificare. Il modello che dà il numero di errori minimo sul validation set sarà
quello migliore di tutti quelli provati. Come ultima cosa testare questo modello
sul test set per ottenere una stima finalmente corretta dell’errore generalizzato. Importante notare che se il data set complessivo è piccolo questo metodo
restituisce comunque una stima falsata, perché i tre set ricavati dal primo non
saranno sufficientemente grandi.
Nel caso in cui il training set non sia abbastanza grande. Cosa che potrebbe
accadere anche nel caso in cui si vogliano testare più parametri di algoritmi
diversi, visto che il training set andrebbe diviso in parti uguali per tutti gli algoritmi che si considerano, esistono altri modi che permettono di stimare l’errore
generalizzato. Consideriamo ad esempio di voler testare tre algoritmi, ognuno
dei quali richiede di fare la prova per quattro parametri diversi. In questo caso
si deve ancora dividere il data set complessivo in: training, validation e test set.
Dopodiché si deve dividere ulteriormente il training set in tre parti uguali per i
tre algoritmi diversi e si deve ammettere che questi ora non siano più così grandi.
In questo caso si dovrebbe utilizzare uno dei tre metodi che a breve analizzeremo in modo da stimare i parametri migliori per ogni algoritmo. Così facendo
sarà possibile trovare i tre modelli che rappresentano la migliore configurazione
per ciascun algoritmo. Questi vanno provati nel validation set e quello che darà
il risultato più basso risulterà il migliore. Provatelo nel test set in modo da
avere una stima accurata del errore generalizzato più basso che si può trovare
da queste prove. I tre metodi prima citati per stimare i parametri migliori di
ciascun algoritmo si basano sulle seguenti procedure:
• leave-one-out: considerando uno solo dei quattro parametri, eliminate il
primo sample dalla sottoparte del training set che è dedicata all’algoritmo che desiderate, fate il training sulla parte rimanente e salvate l’errore.
Considerando sempre lo stesso algoritmo e parametro. Rimettete il sample che avete rimosso prima e togliete il secondo. Rifate il training e
valutate nuovamente l’errore. Rimettete il secondo sample e togliete il
terzo e ripetete l’operazione, e così per tutti i sample. Alla fine l’errore di
quell’algoritmo con quei parametri sarà dato dalla media di tutti gli errori
trovati in precedenza. Considerando sempre lo stesso algoritmo fatelo per
tutti e quattro parametri. A questo punto avrete quattro errori, scegliete
il parametro che genera l’errore più basso. Quello identificherà il modello
migliore per quell’algoritmo.
• Cross Validation: detto anche k-Folder (solidatemnte 10-Folder). Si comporta esattamente come il precedente ma questa volta invece di togliere
una sample solo ne togliere 10 o, in modo più generale k.
• Bootstrap: considerate sempre un solo parametro e algoritmo alla volta.
Costruite un numero q di copie del training set e da ognuna di queste
toglietene un numero r di sample in modo casuale. Usate questi nuove
collezioni di dati per fare il training e stimate l’errore. Fate la media di
tutti gli errori e così avrete l’errore generato di quell’algoritmo con quel
certo parametro. Ripetete il processo per tutti i parametri e quello che
genera l’errore minore sarà il migliore. I valori di q e r dovrebbero essere
configurati in modo tale che se vengono aumentati il valore degli errori
sostanzialmente non cambia.
Tipicamente il primo si usa quando il training non richiede molto tempo perché
implica di farlo molte volte, però è in grado di darvi una stima molto accurata.
Il secondo emula il primo nel caso in cui il training richieda tempi lunghi ma
è meno accurato. Infine il terzo si usa quando la dimensione del training set
è decisamente piccola, ma rimane comunque accurato. Si ricorda che queste
procedure sono valide anche nel caso in cui si voglia capire qual’è il parametro
migliore di un solo algoritmo. Inoltre i procedimenti rimangono uguali se invece
di un paramento ne considerate più di uno, a patto che non cambino durante lo
svolgimento della procedura.
6.5
L’importanza del Data Set
In maniera estremamente veloce si vuole sottolineare in quest’ultima sezione
che la buona riuscita di un algoritmo di apprendimento automatico dipende
principalmente dai dati che si usano. Questo sempre per il no free lunch theorem.
Capite bene che se i label sono sbagliati tutto il procedimento risulterà vano,
ma questo può accadere anche nel caso in cui i sample siano troppo rumorosi o
incompleti. A questo proposito esistono algoritmi dedicati in grado di lavorare
con l’assenza di alcuni dati e molte altre procedure che prendono il nome di
preprocessing. Inoltre risulta decisamente utile un’analisi statistica preliminare,
spesso fatta attraverso visualizzazione grafiche, per indagare le proprietà di tutte
le features del dataset. Ad esempio, hanno una distribuzione simile a quella
Gaussiana? Qual’è la correlazione tra di loro? ci sono outlyers? Qual’è la
media e la mediana. . . ? Infine si vuole citare anche alcuni algoritmi in grado
di semplificare il data set riducendone la sua dimensione, senza però perdere
informazioni, come ad esempio Principal Component Analysis, Multidimentional
Scaling e Feature Selection. . .
Capitolo 7
Soluzione agli Esercizi
proposti
7.1
Esercizio 2.2: Ordinamento alfabetico
Si propone qui una possibile implementazione per risolvere l’esercizio 2.2. Questa è basata sull’utilizzo della classe SimopleSorter definita nella soluzione all’esercizio 2.1
1
package tests;
2
3
public class StringSorter{
4
5
6
7
// constant, valori di default
private static Integer DEFAULT_DIMENSION = 10;
// non puo’ essere maggiore dalla dimensione della lista
SimpleSorter.DEFAULT_TEST
8
9
10
// attribute
private SimpleSorter numericSorter;
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// costruttore di defualt della classe
public StringSorter(){
// get the first 10 elemnts of the default list of
numbers
Integer[] defaultList = SimpleSorter.DEFAULT_TEST;
String defaultStr = "";
for( int i = 0; i < DEFAULT_DIMENSION; i++){
defaultStr = defaultStr + defaultList[ i];
}
// convert this String in list of ascii values
Integer[] asciiList = stringToAsciiList( defaultStr);
numericSorter = new SimpleSorter( asciiList);
}
// construttore manuale della classe
public StringSorter( String toSort){
// converti una stringa in numeri interi secon la
codifica ascii
81
Integer[] strToList = stringToAsciiList( toSort);
numericSorter = new SimpleSorter( strToList);
27
28
29
}
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// metodi utilizzato solo da questa classe per gestire il suo
attributo
// (convertire una stringa di caratteri in un lista in codice
ASCII e viceversa)
private String asciiListToString( Integer[] asciiList){
// creo una stringa vuota
String listToStr = new String( "");
// convwerto da codice ascii a string
for( int i = 0; i < asciiList.length; i++){
// converti nel dato primitivo int
int primitiveData = (int) asciiList[i];
// converti nel rispettivo valore ascii ed
appendilo al risultato
listToStr = listToStr + (char) primitiveData;
}
return( listToStr);
}
private Integer[] stringToAsciiList( String str){
// crea un’arrai vuoto
Integer[] strToList = new Integer[ str.length()];
// converto in codice ASCII
for( int i = 0; i < str.length(); i++){
strToList[ i] = (int) str.charAt( i);
}
return( strToList);
}
54
55
56
57
58
59
// propagate the data given by the attribure
public void sortString(Boolean orderInputPar){
numericSorter.sortList( orderInputPar);
}
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public String getToSort() {
// get to sort from the attribute
Integer[] toSortAscii = numericSorter.getToSort();
// converte to string
String out = asciiListToString( toSortAscii);
return( out);
}
public void setToSort( String toSort) {
// converte to ascii array
Integer[] toSortAscii = stringToAsciiList( toSort);
// set nel sorter
numericSorter.setToSort( toSortAscii);
}
74
75
76
77
public String getSorted() {
// get data from the attribute
Integer[] asciiList = numericSorter.getSorted();
// convert to string
String out = asciiListToString( asciiList);
return( out);
78
79
80
}
81
82
public Boolean getIsSorted() {
return numericSorter.getIsSorted();
}
83
84
85
86
1
}
import java.util.Arrays;
2
3
import tests.StringSorter;
4
5
6
public class StringSortRunner {
7
8
9
10
11
12
// metodo main, da dove parte l’esecuzione
public static void main(String[] args) {
// flag per configurare il tipo di ordinazione
// true per crescente o false per decrescente
Boolean ascendingOrder = true;
13
14
15
16
17
18
19
20
21
22
23
24
// creo nuovo oggetto della classe StringSorter,
// con i primi dieci elementi di default
StringSorter sorter = new StringSorter();
// ordino la lista
sorter.sortString( ascendingOrder);
String sorted = sorter.getSorted();
// stampo la stringa ordinata
System.out.println( "lista ordinata : " + sorted);
// stampo l’oggetto StringSorther
System.out.println( "lista ordinata : " +
sorter.toString() +
"... e’ ordinata? " +
sorter.getIsSorted());
25
26
System.out.println("---------------------------------------");
27
28
29
30
31
32
33
34
35
36
37
// creo una nuova lista
String newListToOrdered = "JavaProgramming";
// setto la lista all’interno dell sorter
sorter.setToSort( newListToOrdered);
// chiedo al sorter se la lista e’ ordinato
Boolean flag = sorter.getIsSorted();
// ordino lista
sorter.sortString( ascendingOrder);
sorted = sorter.getSorted();
// stampo l’oggetto array
System.out.print(
System.out.print(
System.out.print(
System.out.print(
sorter.getIsSorted() + "\n");
}
38
39
40
41
42
43
"lista ordinata : ");
sorted);
"... era ordinata? " + flag);
"... e’ ordinata? " +
}
Il risultato testuale generato dal metodo main sarà per l’ordinamento crescente:
lista ordinata : 12222566889
lista ordinata : [email protected] è ordinata? true
—————————————————lista ordinata : JPaaaggimmnorrv... era ordinata? false... è ordinata? true
7.2
Esercizio 3.1: Scrittura e lettura su file
di seguito una possibile soluzione:
1
package myFileManager;
2
3
4
5
6
7
import
import
import
import
import
java.io.BufferedReader;
java.io.FileNotFoundException;
java.io.IOException;
java.util.ArrayList;
java.util.List;
8
9
10
import fileManagerAPI.FileReader;
import fileManagerAPI.LazyReader;
11
12
13
14
15
16
17
18
// estende una super classe
public class Reader extends FileReader< Boolean>{
//extends LazyReader<Boolean>{
// estende una classe con parametri booleani perche’ il
// metodo manipulateFile() e’ stato creato in modo da
// tornare vero se la lettura e’ andata a buon fine
// o false se non
19
20
21
22
23
// usa solo il costruttore della super-classe
public Reader(String path, Boolean isRelative) {
super(path, isRelative);
}
24
25
26
27
28
29
30
31
32
// leggi tutte le righe del file
@Override
public Boolean manipulateFile() {
try {
this.openFile(); // inizializza manipolatore
// ottiemi il manipolatore
BufferedReader reader =
this.getFileMatipolator();
// se non c’e’ stato nessun errore di input
output
if( reader != null){
// inizializza list dove verranno
racvolti i dati letti
List<String> lines = new
ArrayList<String>();
// leggi la prossima linea
String line = reader.readLine();
// fino a che ci sono nuove linee
while( line != null){
// salva una linea nella lista
lines.add( line);
// ottieni la prossima linea
line = reader.readLine();
}
// setta le linee lette nell’atributo
della superclasse
this.setLines( lines);
}
// la superclasse si preoccupa degli errori
} catch (FileNotFoundException e) {
this.showError( e);
return( false);
} catch (IOException e) {
this.showError( e);
return( false);
} finally {
try {
// se hai finito con successo
// chidi la comunicazione con il file
this.closeFile();
} catch (IOException e) {
this.showError( e);
return( false);
}
}
return true;
}
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
1
}
package myFileManager;
2
3
4
5
6
import
import
import
import
java.io.BufferedWriter;
java.io.FileNotFoundException;
java.io.IOException;
java.util.List;
7
8
9
import fileManagerAPI.FileWriter;
import fileManagerAPI.LazyWriter;
10
11
12
13
14
// dove non e’ diveramente indicato questa classe
// si comporta analogamente come Reader.
public class Writer extends FileWriter< Boolean>{
//extends LazyWriter< Boolean>{
15
public Writer(String path, Boolean isRelative, Boolean
appendFile) {
super(path, isRelative, appendFile);
}
16
17
18
19
@Override
public Boolean manipulateFile() {
try {
this.openFile();
BufferedWriter writer =
this.getFileMatipolator();
// ottieni le stringa da scrivere dalla
superclasse
List< String> toAppend = this.getToAppend();
// se non ci sono stati errori e se c’e’
qualcosa da scrivere
if(( writer != null) && ( toAppend != null) && (
! toAppend.isEmpty())){
// per ogni elemento della lista
for( String line : toAppend){
// scrivi la linea
writer.write( line);
// scrivi il carattere "vai a
capo" (\n)
writer.newLine();
}
// pulisci perche’ tutto e’ stato scritto
this.getToAppend().clear();
} else {
return( false);
}
} catch (FileNotFoundException e) {
this.showError( e);
return( false);
} catch (IOException e) {
this.showError( e);
} finally {
try {
this.closeFile();
} catch (IOException e) {
this.showError( e);
return( false);
}
}
return true;
}
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
1
2
}
import java.io.File;
import java.io.IOException;
3
4
import java.util.List;
import java.util.ArrayList;
5
6
7
8
9
10
import
import
import
import
import
myFileManager.Reader;
myFileManager.Writer;
fileManagerAPI.FileManager;
fileManagerAPI.FileReader;
fileManagerAPI.FileWriter;
11
12
13
public class FileManagerRunner {
14
15
16
17
18
/**
* @param args
*/
public static void main(String[] args) {
19
20
21
FileWriter writer1 = createSimpleFile(
"/files/fristText.txt", true, true);
String newPath = createAnoterFileInTheSameDirectory(
writer1.getAbsolutePath());
22
synchrounizeFile( writer1.getAbsolutePath(), newPath,
23
24
false);
}
25
26
27
28
29
30
31
32
private static void synchrounizeFile( String fromThis_path,
String toThis_path, Boolean arePathRelative){
// creo un reader sul primo file
FileReader<?> reader = new Reader( fromThis_path,
arePathRelative);
// leggo tutte le linee tesutali dal primo file
reader.manipulateFile();
// ottengo le linee appena lette
List<String> lineRead = reader.getLines();
33
34
35
36
37
38
39
40
// creo un writer del secondo file (che sovrascrive il
contenuto che c’era prima)
FileWriter writer2 = new Writer( toThis_path,
arePathRelative, false);
// carico la copia del primo file
writer2.setToAppend( lineRead);
// la scivo nel seondo
writer2.manipulateFile();
}
41
42
43
44
45
46
47
private static FileWriter createSimpleFile( String path, Boolean
isRelativve, Boolean override){
// creare le linee testuali che si vogliono scrive in un
nuovo file
Integer dimension = new Integer( 100);
List< String> line = new ArrayList< String>();
for( int i = 1; i <= dimension; i++){
line.add( "add new line " + i);
}
// inizializza l’oggetto writer( path, isRelativePath,
48
49
toAppend)
FileWriter<?> writer = new Writer( path , isRelativve, !
50
override);
// set le linee da scrivere
writer.setToAppend( line);
// scrivi line su file (se nn esiste lo crea)
writer.manipulateFile();
51
52
53
54
55
return( writer);
56
}
57
58
private static String createAnoterFileInTheSameDirectory( String
absolutePath){
// ottiene il separatore per ogni sistema operativo
String sepSymb = System.getProperty("file.separator");
// path assoluta = /Absolute/directory/Path/to/file.tx
//
=
/Absolute/directory/Path/to/
absolutePath = absolutePath.substring( 0,
absolutePath.lastIndexOf( sepSymb) + 1);
// crea un file sempre nella stessa cartella del primo
ma con nome diverso
String newPath = absolutePath + "copyText.txt";
try {
File f = new File( newPath);
FileWriter.createFile( f);
} catch (IOException e) {
// stampa se c’e’ un’errore
e.printStackTrace();
}
return( newPath);
}
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
}
7.3
Esercizio 3.2: Miglioramento delle capacità
di una classe
In risposta al punto 3.2 si nota che nel caso in cui si espandano le classi: Reader e
Writer, con le super-classi FilleReader o LazyReader ( o FileWriter, LazyWriter
rispettivamente). Si compie l’operazione di implementare la capacità di manipolare il file ad una classe invece che ad un’altra. Nel caso in cui questa capacità
viene data alle classi LazyReader (o LazyWriter), si ottiene un comportamento
competentemente diverso anche se la struttura dei metodi rimane invariata. Un
esempio più intelligente di altre classi che potrebbero essere utilizzati con questo
caso sono: ImmageReader, VideoReader, MyModelReader . . .
Nel caso in cui risultino utili altri comportamenti comuni a tutti i file,
come ad esempio: creare, rimuovere, rinominare. . . la loro locazione più corretta
risulta essere nell’interfaccia FileManager, ad esempio:
1
package fileManagerAPI;
2
3
4
import java.io.FileNotFoundException;
import java.io.IOException;
5
6
public interface FileManager<E,T> {
7
// metodi gia’ esistenti
public E manipulateFile();
public T getFileMatipolator();
public String getRelativePath();
public String getAbsolutePath();
public void closeFile() throws IOException;
public void openFile() throws IOException, FileNotFoundException;
public void showError( Exception e);
8
9
10
11
12
13
14
15
16
// metodi da aggiungere
// ricevono in ingresso la variabile inizializzata al file
connesso a questa classe. Non ritornano nulla perche’ modificaono
gli attributi interni ad essa.
public void createFile( T manipulator);
public void delateFile( T manipulator);
public void renameFIle( T manipulator);
...
17
18
19
20
21
22
23
}
Questi nuovi metodi dovranno essere così implementati anche da tutte le classi che usano implements FileManager; per definizione di interface. Quindi la
classe CommonFileOperations dovrà contenere la definizione abstract di queste
funzioni. In fine, le classi che espandano COmmonFileOperations dovrebbero
avere tutte le informazioni per poter implementare questo tipo di operazioni.
7.4
Esercizio 4: Interfacce Grafiche
L’implementazione della classe che descrive una generica domanda è:
1
2
import java.util.ArrayList;
import java.util.List;
3
4
5
public class QuestionFactory {
6
7
8
9
10
11
12
13
14
// testo della domanda
private String question;
// numero della domanda
private Integer questionNumber;
// lista di tutti i testi delle risposte
private List< String> answers = new ArrayList< String>();
// per ogni testo della risposta contiene true se corretto,
false altrimenti
private List< Boolean> isCorrect = new ArrayList< Boolean>();
15
16
// costruttore, inizializza il testo e il numero della domanda
17
18
19
20
public QuestionFactory( Integer questionNumber, String question){
this.question = question;
this.questionNumber = questionNumber;
}
21
22
23
24
25
26
// aggiungi una risposta e se e’ corretta o meno
public void addAnswers( String answer, Boolean isCorrect){
this.answers.add( answer);
this.isCorrect.add( isCorrect);
}
27
28
29
30
31
// ritorna il numero della domanda
public Integer getQuestionNumber(){
return( questionNumber);
}
32
33
34
35
36
// ritorna il testo della domanda
public String getQuestion(){
return( question);
}
37
38
39
40
41
// ritorna una risposta
public String getAnswer( Integer idx){
return( answers.get( idx));
}
42
43
44
45
46
// ritorna tutte le risposte
public List< String> getAnswer(){
return( answers);
}
47
48
49
50
51
// ritorna true se la risposta con indice idx e’ corretta
public Boolean getIsCorrect( Integer idx){
return( isCorrect.get( idx));
}
52
53
54
55
56
// ritorna il numero delle risposte
public Integer getNumberOfAnswers(){
return( answers.size());
}
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// ritorna se la risposta e’ corretta o meno
public Boolean isCorrectAnswer( String answer){
Integer counter = 0;
// per tutte le risposte
for( String s : answers){
// se una risposta e’ uguale a quella data
if( s.equals( answer)){
// se e’ corretta
if( isCorrect.get( counter))
return( true);
else // se e’ sbagliata
return( false);
}
counter = counter + 1;
}
return( null);
71
72
73
}
74
75
}
Mentre l’implementazione del panello che visualizza la domanda:
1
2
3
4
5
import
import
import
import
import
javax.swing.JPanel;
java.awt.BorderLayout;
javax.swing.JLabel;
java.awt.FlowLayout;
java.util.Enumeration;
import
import
import
import
javax.swing.AbstractButton;
javax.swing.BoxLayout;
javax.swing.ButtonGroup;
javax.swing.JRadioButton;
6
7
8
9
10
11
12
13
public class Question extends JPanel {
14
15
16
private ButtonGroup answerGroup = new ButtonGroup();
private QuestionFactory question;
17
18
19
20
21
22
// costruttore
public Question( QuestionFactory question) {
// crea il JPanel
this.question = question;
setLayout(new BorderLayout(0, 0));
23
// aggiungi un pannelo per visualizzare la domanda
JPanel panel = new JPanel();
add(panel, BorderLayout.NORTH);
panel.setLayout(new FlowLayout(FlowLayout.LEFT, 5, 5));
// aggiungi numero della domanda
JLabel label = new JLabel( question.getQuestionNumber()
24
25
26
27
28
29
+ ") ");
30
31
32
33
panel.add(label);
// aggiungi domanda
JLabel lblQuestaLa = new JLabel( question.getQuestion());
panel.add(lblQuestaLa);
34
35
36
37
38
39
40
41
42
43
// aggiungi un pannello per le risposte
JPanel panel_1 = new JPanel();
add( panel_1, BorderLayout.CENTER);
panel_1.setLayout(new BoxLayout(panel_1,
BoxLayout.Y_AXIS));
// aggiungi tutte le risposte
answerGroup = new ButtonGroup();
for( String s : question.getAnswer()){
JRadioButton rdbtnNewRadioButton = new
JRadioButton( s);
answerGroup.add( rdbtnNewRadioButton);
panel_1.add( rdbtnNewRadioButton);
44
}
45
}
46
47
// ottieni la risposta selezionata e controlla se e’ corretta
public Boolean getAnswersCorrecteness(){
// ottieni un elemento in grado di ciclare su tutti i
RadioButton
Enumeration<AbstractButton> allRadioButton =
answerGroup.getElements();
String answer = null;
// per tutti gli elementi dentro al RadioGroup
while(allRadioButton.hasMoreElements()){
// recupera un RadioButton
JRadioButton temp= (JRadioButton)
allRadioButton.nextElement();
// controlla se e’ selezionato
if( temp.isSelected()){
// ottieni testo della selezione
answer = temp.getText();
// esci (solo uno puo’ essere
selezionato)
break;
}
}
// se c’e’ stata una risposta
if( answer != null)
// controlla che sia corretta
return( question.isCorrectAnswer( answare));
else return( null);
}
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
}
Infine quella del main e della classe che crea la finestra:
1
import java.awt.BorderLayout;
2
3
4
public class GuiSample extends JFrame {
5
6
7
8
// stringe costanti
private static final String BUTTON_LABEL = " fatto ! ";
private static final String FRAME_TITLE = " TITOLO: esmpio di un
questionario in swing/awt ";
9
10
11
12
13
// campo necessario dal main
private JPanel contentPane;
// lista di tutte le domande nel test
private final List< Question> questionList = new ArrayList<
Question>();
14
15
16
17
// visualizza la GUI
public static void main(String[] args) {
18
19
20
21
22
23
24
25
26
27
28
// inizializza le domande
final List< QuestionFactory> questions = new ArrayList<
QuestionFactory>();
QuestionFactory q1 = new QuestionFactory( 1, " questa e’
la prima domanda?");
q1.addAnswers( "si.", true);
q1.addAnswers( "no", false);
q1.addAnswers( "bho", false);
questions.add( q1);
QuestionFactory q2 = new QuestionFactory( 2, " Mentre
questa e’ la prima domanda?");
q2.addAnswers( "si.", false);
q2.addAnswers( "no", true);
questions.add( q2);
29
// lancia la gui
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
GuiSample frame = new GuiSample(
30
31
32
33
34
questions);
frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
35
36
37
38
}
39
});
40
41
}
42
43
44
45
46
47
48
49
50
51
52
53
// crea ed inizializza il JFrame
public GuiSample( final List< QuestionFactory> questions) {
// chiudi il programma chiudendo la finestra
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// dimensioni e posizioni della finestra
setBounds(100, 100, 450, 300);
// crea e inizializza pannello di base della finestra
contentPane = new JPanel();
contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
setContentPane(contentPane);
contentPane.setLayout(new BorderLayout(0, 0));
54
55
56
57
58
59
60
61
// aggiungi il pannello del titolo
JPanel panel = new JPanel();
contentPane.add(panel, BorderLayout.NORTH);
panel.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));
// aggiungi il titolo
JLabel lblTitolo = new JLabel( FRAME_TITLE);
panel.add(lblTitolo);
62
63
64
65
66
67
// aggiungi il bottone ed il suo comportamento
JButton btnDone = new JButton( BUTTON_LABEL);
contentPane.add(btnDone, BorderLayout.SOUTH);
btnDone.addMouseListener(new MouseAdapter() {
@Override
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public void mouseReleased(MouseEvent e) {
// crea quantita’ interessanti per questa funzione
Integer correctCounter = 0;
Boolean incomplete = false;
List< Integer> wrongAnswer = new ArrayList< Integer>();
// per tutte le domande in questa finestra
for( int i = 0; i < questionList.size(); i++){
// ottieni la selezione della domanda
// true -> corretta, false -> sbagliata, null -> assente
Boolean answer = questionList.get(
i).getAnswersCorrecteness();
if( answer != null){
if( answer){
// se e’ corretta contala
correctCounter = correctCounter + 1;
} else {
// se e’ sbagliata savla il suo numero
wrongAnswer.add(
questions.get(i).getQuestionNumber());
}
} else {
// se non c’e’ una risposta alza il flag e esci
incomplete = true;
break;
}
91
}
// se il flag e’ alto ritorna un’errore
if( incomplete){
displayPanel( " non hai risposto a tutte le domande, si
prega di farlo",
"Questionarion non
Completato",JOptionPane.ERROR_MESSAGE);
} else {
// se il flagg e’ basso mostra informazioni
displayPanel( " Grazie per la partecipazione al test." +
System.getProperty( "line.separator") +
" Risposte corrette " + correctCounter +
". Numero delle risposte sbagliate: " + wrongAnswer,
"Questionarion
completato",JOptionPane.INFORMATION_MESSAGE);
}
92
93
94
95
96
97
98
99
100
101
102
103
104
}
});
105
// aggiungi un pannelo con lo scroll se le domande sono
106
lunghe
107
108
109
// (il testo non va’ a capo automaticamente)
JScrollPane scrollPane = new JScrollPane();
contentPane.add(scrollPane, BorderLayout.CENTER);
110
111
112
113
// aggiungi un’altro pannello dentro lo scroll
JPanel panel_1 = new JPanel();
scrollPane.setViewportView(panel_1);
panel_1.setLayout(new BoxLayout(panel_1,
BoxLayout.Y_AXIS));
114
115
// aggiungi pannelli delle domande
for( QuestionFactory q : questions){
Question newQuestion = new Question( q);
panel_1.add( newQuestion);
questionList.add( newQuestion);
}
116
117
118
119
120
121
}
122
123
// visualizza una finestra (pop-up) per le informazioni
private static void displayPanel( String info, String title, int
option){
JOptionPane a = new JOptionPane();
a.showMessageDialog( a, info, title, option);
}
124
125
126
127
128
129
}
7.5
Esercizio 5.1: Implementare un Perception
L’implementazione della classe main è riportata di seguito:
1
2
3
4
import Networking.DataSet;
import Networking.DataSetFactory;
import Networking.Neuron;
5
6
7
public class PerceptrnTest {
8
9
public static String newLine =
System.getProperty("line.separator");
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
// create data set
DataSet dataSet = DataSetFactory.getOrSet();
//DataSet dataSet = DataSetFactory.getAndSet();
//DataSet dataSet = DataSetFactory.getXorSet();
// stampare il data set
System.out.println( "
features \t | label " + newLine
+ dataSet);
Integer d = dataSet.getNumberOfFeatures();
Integer n = dataSet.getNumberOfSample();
20
21
22
23
24
25
26
27
// inizializzo il neurone
Neuron perceptron = new Neuron( d);
System.out.println( "create new " + perceptron);
// faccio il training
Integer iterationNumb = perceptron.train( dataSet);
// stampo risultati
System.out.println( "training complete within " +
iterationNumb + " iterations");
String weigth = "";
for( int i = 0; i <
perceptron.getWeighs().getRowDimension(); i++){
weigth += perceptron.getWeighs().get(i,0) + ",
";
}
System.out.println( "final model parameters [w1, w2,
bias]= [" + weigth + perceptron.getbias() + "]");
for( int i = 0; i < n; i ++){
System.out.println( "sample " + (i+1) + ".
Error? " + perceptron.computeError( dataSet, i));
System.out.println( "estimate out " +
perceptron.computeOutput( dataSet.getMatrixSample( i)));
System.out.println( "
real out " +
dataSet.getLabel( i));
}
}
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
}
Mentre quella della classe che implementa il neurone:
1
package Networking;
2
3
import Jama.Matrix;
4
5
public class Neuron {
6
7
8
9
10
// massimo numero (default) di iterazioni per fermare il traing
public static Integer MAX_TRAIN_ITERATION = 10000000;
// soglia degli (default) errori possibili per fermare il
training
public static Integer MINUMUM_TRAINING_ERROR = 0;
11
12
13
14
// arrai dei pesi di questo neurone
//private List< Double> weighs = new ArrayList< Double>();
private Matrix weighs;
15
16
17
18
19
20
21
// bias di questo neurone
private Double bias;
// valori settati per il minimo numero di errori e massimo
numero di iterazioni
// per fermare il training
private Integer maxIteration;
private Integer minErroNumber;
22
23
24
25
26
27
// costruttore della classe
public Neuron( Integer numberOfInput){
double[] w = new double[ numberOfInput];
for( int i = 0; i < numberOfInput; i++){
w[ i] = Math.random() * 0.1;
}
weighs = MatrixComputation.getColumnVector( w);
bias = Math.random()*0.1;
28
29
30
31
// inizializzo i parametri per fermare il training
minErroNumber = MINUMUM_TRAINING_ERROR;
maxIteration = MAX_TRAIN_ITERATION;
32
33
34
35
}
36
37
38
39
40
41
42
43
44
45
46
// da to un sample (x) di ingresso questa funzione
// calcola l’output nel neurono: f = w*x + b
public Double computeOutput( Matrix sample){
if( weighs.getRowDimension() ==
sample.getColumnDimension()){
double out =
sample.times(weighs).getArray()[0][0] + bias;
return( out);
}
System.err.println( "neuron cannoot compute outputs");
return( null);
}
47
48
49
50
51
52
53
54
// calcola l’output del modello e poi l’errore.
public Boolean computeError( Matrix sample, Double label){
return( computeError( computeOutput( sample), label));
}
public Boolean computeError( DataSet data, Integer indx){
return( computeError( data.getMatrixSample( indx),
data.getLabel( indx)));
}
55
56
57
58
59
60
61
62
63
// ritorna true se l’output del modello e’ diverso da quello del
label (errore). False altrimenti
public Boolean computeError( Double output, Double label){
if( output * label <= 0.0){
return( true); // e’ un’errore
} else {
return( false); // e’ corretto
}
}
64
65
66
67
68
69
70
71
72
73
// aggiorna i pesi utilizando la Hebbian Learning Role
// il sample dato in ingresso deve essere della stessa
dimensione dei pesi
private void updateWeighs( Matrix sample, Double label){
if( sample.getColumnDimension() ==
weighs.getRowDimension()){
//MatrixComputation.printMatrix( sample);
for( int i = 0; i < weighs.getRowDimension();
i++){
Double elem = weighs.get( i,0) + label *
sample.get( 0,i);
weighs.set( i, 0, elem);
}
} else {
74
System.err.println( "neurono cannot update
75
weighs");
76
77
78
79
80
81
82
83
84
85
86
}
}
// aggiorna il bias utilizzando la Hebbian Learning Role
private void updatebias( Double label){
bias = bias + label;
}
// aggiorna i pesi e il bias chiamando: this.updatebias e
this.updateWeighs
public void updateWeighsAndbias( Matrix sample, Double label){
updateWeighs( sample, label);
updatebias( label);
}
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// addestra il neurone
public Integer train( DataSet data){
// inizializza variabili
Integer error = data.getNumberOfSample(); //
inizialmente tutti errori
Integer idx = 0; // indice di un determinato sample e il
suo rispettivo label
Integer iterationCount = 0;
while( error > minErroNumber){
// calcola l’output del neurone
Double output = this.computeOutput(
data.getMatrixSample( idx));
// se c’e’ un’errore
if( this.computeError( data.getLabel( idx),
output)){
// modifica i pesi e il bias
this.updateWeighsAndbias(
data.getMatrixSample( idx), data.getLabel(idx));
// ottieni il nuovo numero di
classificazioni sbagliate
error = this.getMissClasificationRate(
data, data.getMatrixLabels());
}
idx = idx + 1; // passa al sample e al label
successivo
if( idx >= data.getNumberOfSample()){
// se siamo alla fine del dataset
riparti dall’inizio
idx = 0;
}
109
110
111
112
113
114
115
// conta le iterazioni
iterationCount = iterationCount + 1;
if( iterationCount >= maxIteration){
//se sono troppe esci
System.err.println( " too many
iterations (" + iterationCount + "), stop Training");
break;
}
}
return( iterationCount);
116
117
118
119
}
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// ritorna il numero di classificazioni sbagliate di questo
modello
// sul dataset dato in ingresso. dataSet.size() deve avera la
stessa dimesione di
// label.size()
public Integer getMissClasificationRate( DataSet dataSet, Matrix
label){
if( dataSet.getMatrixData().getColumnDimension() ==
label.getRowDimension()){
Integer counter = 0;
for( int i = 0; i < label.getRowDimension();
i++){
if( this.computeError(
dataSet.getMatrixSample( i), label.get( i, 0))){
counter = counter + 1;
}
}
return( counter);
} else {
System.err.println( " I cannon compute the miss
classification rate");
return( null);
}
}
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// ritorna l’array dei pesi
public Matrix getWeighs(){
return( weighs);
}
// setta l’array dei pesi
public void setWeighs( Matrix newWeighs){
this.weighs = newWeighs;
}
// ritorna il bias
public Double getbias(){
return( bias);
}
// setta il bias
public void setbias( Double newbias){
bias = newbias;
}
// ritorna il numero degli ingressi al neurone
public Integer getOrder(){
return weighs.getRowDimension();
}
159
160
161
162
// setta il massimo numero di iterazioni per fermare il training
public void setMaxTrainIteration( Integer iterationNumber){
maxIteration = iterationNumber;
}
163
164
// setta il minimo numero di errore per fermare il training
public void setMinimumNumberOfTrainingError( Integer num){
minErroNumber = num;
}
165
166
167
168
169
// per stampare facilmente usando System.out.println(
this.toString());
@Override
public String toString(){
String out = "neuron with weighs: ";
for( int i = 0; i < weighs.getRowDimension(); i++)
out += weighs.get( i, 0)+ ", ";
out += " bias: " + bias;
return( out);
}
170
171
172
173
174
175
176
177
178
179
}
7.6
Esercizio 5.2: Organizzazione delle classi in
un MLP
Per creare in modo efficiente e flessibile una rete neurale usando un linguaggio
di programmazione ad oggetti, il metodo più usato è quello di creare classi che
identifichino il concetto di: neurone, layer e network. Il primo contiene tutte le
informazioni proprie del singolo neurone, mentre il secondo conterrà una lista
di neuroni e, gerarchicamente, il terzo una lista del secondo.
In particolare, il neurone avrà come attributi: il vettore dei suoi pesi W
(double[]), il valore del bias (double) b, il valore dell’uscita (double) f e quello del
gradiente (double) δ. Sostanzialmente i metodi di cui ha bisogno sono solamente
i relativi getter e setter. Infine, il costruttore richiederà di inserire il numero di
ingressi al neurone e dovrà provvedere all’inizializzazione casuale dei pesi e del
bias.
Inoltre, la classe che identifica un layer, dovrà contenere come attributo
solo una lista ordinata di neuroni; tutti appartenenti allo stesso layer appunto.
Questa classe avrà bisogno dei metodi getter e setter che ritornino le rispettive
matrici W l , B l , F l e ∆l . In particolare, per costruire tali matrici, i getter
dovranno usare i metodi definiti all’interno della classe neurone. Mentre i setter
dovranno usare le procedure scritte nello pseudo-codice all’interno dei cicli che
utilizzano il dato L (numero di layer); ad esempio quelli della riga: 12 e 20. Il
costruttore di questa classe dovrà ricevere il numero di neuroni appartenenti allo
specifico layer, che verrà usato per creare nuove istanze della classe neurone.
Infine, l’attributo della classe che identifica la rete sarà una lista ordinata di
layer. Qui dovrà essere implementato il metodo train che richiederà il data set
come parametro d’ingresso. Questo dovrà implementare tutto lo pseudo codice
andando a richiamare metodi già definiti nelle precedenti classi. Inoltre potrebbe
essere di aiuto implementare un metodo che calcoli l’uscita della rete in modo
che, una volta addestrata e quindi una volta che i pesi siano configurati, la si
possa usare per determinare l’uscita di un nuovo sample, di cui non si conosce
il label. Il costruttore di questa classe dovrà ricevere il numero di layer e il
numero di neuroni per ognuno di queste, così da creare la rete creando nuove
istanze delle classi precedenti.
Capitolo 8
Appendice A: File Manager
API
di seguito sono riportati i codici sorgenti utilizzati per implementare l’archivio
jar introdotto nell’esercizio 3. Qui si utilizza l’interfaccia gia’ analizzata nella
sezione 7.3. In più e’ presente:
1
package fileManagerAPI;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.FileNotFoundException;
import java.io.IOException;
/**
*
* This class implements some common methods defined in {@link
FileManager}.
* In particular it has been designed to care about the initialization
and storage
* of the directory path of the file, both in terms of relative and
absolute path.
* It also implements a basic error notification through the command
{@code e.printStackTrace();}.
* Finally it delegates the implementation of the other methods of the
type FileManager
* through the modifier {@code abstract}
*
* @author Buoncompagni Luca
*
* @param <E> generic returning type of the method {@link
#manipulateFile()}
* @param <T> generic returning type of the method {@link
#getFileMatipolator()}
*
* @see FileManager
*/
public abstract class CommonFileOperations<E, T> implements
FileManager<E, T> {
// constants
/**
103
24
25
26
27
* describe the directory path with respect to the folder in
which the software is running.
* It is based on the command: <br>{@code RELATIVE_PATH =
System.getProperty("user.dir");}
*/
public static String RELATIVE_PATH =
System.getProperty("user.dir") +
System.getProperty("file.separator");
28
29
30
private Boolean pathRelative = null; // is path relative?
private String absolutePath = null; // contains the absolute path
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* Constructor to initialize the class with a path that can be
either relative or absolute.
* Where a relative path is the directory address starting for
the folder in which
* the program is actually running. While absolute path is the
directory address starting
* for the system root folder.
*
* @param path is the directory path to the file in relative or
absolute notation.
* @param isRelative if it is true, it identifies that the
parameter {@code path}
* defines a relative path. Otherwise, if it is false, it
denotes that the parameter
* {@code path} is an absolute address.
*/
public CommonFileOperations( String path, Boolean isRelative){
this.pathRelative = isRelative;
if( pathRelative){ // is true
this.absolutePath = RELATIVE_PATH + path;
} else { // is false
this.absolutePath = path;
}
}
51
52
53
54
55
56
57
@Override
public String getRelativePath() {
// elimino la sottostringa uguale alla path relativa
sostituendula con niente
String relativePath =
absolutePath.replace(RELATIVE_PATH, "");
return( relativePath);
}
58
59
60
61
62
@Override
public String getAbsolutePath() {
return absolutePath;
}
63
64
65
@Override
public void showError(Exception e) {
e.printStackTrace();
66
}
67
68
@Override
public abstract E manipulateFile();
69
70
71
@Override
public abstract T getFileMatipolator();
72
73
74
@Override
public abstract void closeFile() throws IOException;
75
76
77
@Override
public abstract void openFile() throws FileNotFoundException,
IOException;
78
79
80
81
1
}
package fileManagerAPI;
2
3
4
5
6
7
import
import
import
import
import
java.io.BufferedWriter;
java.io.File;
java.io.IOException;
java.util.ArrayList;
java.util.List;
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* This class implements the operations used to be able to write lines
into a file.
* Moreover, it propagates the implementation of the method {@link
#manipulateFile()}
* through the modifier {@code abstract}. To Note that the generic type
of data {@code T},
* defined in {@link FileManager} and propagate in {@link
CommonFileOperations} has been
* fixed in this class to be an {@link BufferedWriter} object.
*
* A call to the function {@link #openFile()} will generate the proper
initialization of the
* data returned by {@link #getFileMatipolator()} with respect to the
parameters given
* in inputs to the constructor. Since file manipulator is of rime
BufferedWriter it is
* possible to just use:
* <br><code>
*
&nbsp&nbspString line = "something to write"<br>
* &nbsp&nbspgetFileMatipolator().write( line);<br>
* <br></code>
* To make permanent the changes over the file {@link #closeFile()}
should be called. This
* will also effect the value of the writer: {@link
#getFileMatipolator()}. It must
* be reinitialized to be used again.
27
28
29
30
31
32
33
34
35
36
* </code>
* @author Buoncomapagni Luca
*
* @param <E> generic type of data returned by the method {@link
#manipulateFile()}.
*
* @see CommonFileOperations
* @see FileManager
*
*/
public abstract class FileWriter< E> extends CommonFileOperations< E,
BufferedWriter>{
37
38
39
40
41
private
private
private
private
BufferedWriter writer = null;
java.io.FileWriter fw = null;
Boolean appendFileFlag = false;
List<String> toAppend = new ArrayList< String>();
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* Constructor which just call the construction of {@link
CommonFileOperations}
* and uses the third parameter to set the method {@link
#setAppendingFileType(Boolean)}.
*
* @param path is the directory path to the file in relative or
absolute notation.
* @param isRelative if it is true, it identifies that the
parameter {@code path}
* defines a relative path. Otherwise, if it is false, it
denotes that the parameter
* {@code path} is an absolute address.
* @param appendFile if it is true that the new lines will be
written at the end of
* the file. Otherwise, if it is false, the file will be
replaced with an empty one, and
* than the data will be written on it.
*/
public FileWriter(String path, Boolean isRelative, Boolean
appendFile) {
super(path, isRelative);
this.setAppendingFileType( appendFile);
}
59
60
61
@Override
public abstract E manipulateFile();
62
63
64
65
66
@Override
public BufferedWriter getFileMatipolator() {
return writer;
}
67
68
69
70
@Override
public void closeFile() throws IOException {
writer.close();
fw.close();
71
}
72
73
@Override
public void openFile() throws IOException {
File f = new File( this.getAbsolutePath());
if( ! f.exists()){
createFile( f);
}
74
75
76
77
78
79
80
fw = new java.io.FileWriter( this.getAbsolutePath(),
appendFileFlag);
writer = new BufferedWriter( fw);
}
81
82
83
84
/**
* Create a new file given an initialized object of type {@link
File}.
* It is also automatically called by the method {@link
#openFile()}
* when the given directory does not contains any file with such
name.
* Namely, if it does not exist it will be created
*
* @param f description of the file to create.
* @throws IOException
*/
public static void createFile( File f) throws IOException{
f.createNewFile();
}
85
86
87
88
89
90
91
92
93
94
95
96
97
private void setAppendingFileType( Boolean append){
this.appendFileFlag = append;
}
98
99
100
101
public List<String> getToAppend(){
return( toAppend);
}
102
103
104
105
public void setToAppend( List< String> lines){
toAppend = lines;
}
106
107
108
109
1
}
package fileManagerAPI;
2
3
4
5
6
7
8
9
import
import
import
import
import
import
java.io.BufferedReader;
java.io.File;
java.io.FileInputStream;
java.io.FileNotFoundException;
java.io.IOException;
java.io.InputStreamReader;
10
11
import java.util.ArrayList;
import java.util.List;
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* This class implements the operations used to be able to read the
lines from a file.
* Moreover, it propagates the implementation of the method {@link
#manipulateFile()}
* through the modifier {@code abstract}. To Note that the generic type
of data {@code T},
* defined in {@link FileManager} and propagate in {@link
CommonFileOperations} has been
* fixed in this class to be an {@link BufferedReader} object.
* <br><br>
*
* A call to the function {@link #openFile()} will generate the proper
initialization of the
* data returned by {@link #getFileMatipolator()} with respect to the
parameters given
* in inputs to the constructor. Since file manipulator is of rime
BufferedReader it is
* possible to loop along all the lines of a file using:
* <br><code>
*
&nbsp&nbspString line = getFileMatipolator().readLine();<br>
*
&nbsp&nbspWhile( line != null){<br>
*
&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp// do something <br>
*
&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp ....<br>
*
&nbsp&nbsp&nbsp&nbsp&nbsp&nbspline =
getFileMatipolator().readLine();<br>
* &nbsp&nbsp}<br>
* To make permanent the changes over the file {@link #closeFile()}
should be called. This
* will also effect the value of the reader: {@link
#getFileMatipolator()}. It must
* be reinitialized to be used again.
* </code>
*
* @author Buoncompagni Luca
*
* @param <E> generic type of data returned by the method {@link
#manipulateFile()}.
*
* @see CommonFileOperations
* @see FileManager
*/
public abstract class FileReader<E> extends CommonFileOperations<E,
BufferedReader>{
46
47
48
49
// attributes
private BufferedReader reader = null;
private FileInputStream fis = null;
50
51
List< String> lines = new ArrayList<String>();
52
/**
* Constructor which just call the construction of {@link
CommonFileOperations}
* and does not process any further the data.
*
* @param path is the directory path to the file in relative or
absolute notation.
* @param isRelative if it is true, it identifies that the
parameter {@code path}
* defines a relative path. Otherwise, if it is false, it
denotes that the parameter
* {@code path} is an absolute address.
*/
public FileReader(String path, Boolean isRelative) {
super(path, isRelative);
}
53
54
55
56
57
58
59
60
61
62
63
64
65
@Override
public abstract E manipulateFile();
66
67
68
@Override
public BufferedReader getFileMatipolator(){
return( reader);
}
69
70
71
72
73
@Override
public void closeFile() throws IOException {
reader.close();
fis.close();
}
74
75
76
77
78
79
@Override
public void openFile() throws FileNotFoundException {
// ottieni un puntatore al file
File f = new File( this.getAbsolutePath());
fis = new FileInputStream( f);
// inizializa l’oggetto reader
InputStreamReader isr = new InputStreamReader( fis);
reader = new BufferedReader( isr);
}
80
81
82
83
84
85
86
87
88
89
public void setLines( List<String> lines){
this.lines = lines;
}
90
91
92
93
public List<String> getLines(){
return( lines);
}
94
95
96
97
}
Capitolo 9
Appendice B: Perception,
classi usate
di seguito sono riportati i codici sorgenti utilizzati per l’esercizio in sezione 5.4
1
package Networking;
2
3
import java.util.List;
4
5
import Jama.Matrix;
6
7
public class MatrixComputation {
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// creato dalla classe DataSet per creare il data set
// data una lista ritorna una matrice colonna se order = 2, riga
se order = 1, null altrimenti
public static Matrix listToMatrix( List< Double> list, Integer
order){
if( order == 2){
double[][] array = new double[1][ list.size()];
int i = 0;
for( Double l : list){
array[0][ i] = l;
i = i + 1;
}
Matrix out = new Matrix( array);
return( out);
} else if( order == 1){
double[][] array = new double[ list.size()][1];
int i = 0;
for( Double l : list){
array[ i][0] = l;
i = i + 1;
}
Matrix out = new Matrix( array);
return( out);
} else {
111
31
32
33
34
System.out.println( "wrong matrix dimention, 1
for row, 2 for column");
return null;
}
}
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// utilizzato dalla classe DataSet per creare il dato
public static Matrix listOfListToMatrix( List< List< Double>>
list){
double[][] array = new double[ list.size()][ list.get(
0).size()];
int i = 0;
for( List< Double> sub : list){
int j = 0;
for( Double ele : sub){
array[i][j] = ele;
j = j + 1;
}
i = i + 1;
}
Matrix out = new Matrix( array);
return( out);
}
51
52
53
54
55
56
57
58
59
60
// dato un vettore ritorna una matrice di tipo riga
public static Matrix getRowVector( double[] vector){
double[][] w = new double[ 1][ vector.length];
for( int i = 0; i < vector.length; i++){
w[ 0][ i] = vector[ i];
}
return( new Matrix( w));
}
61
62
63
64
65
66
67
68
69
// dato un vettore ritorna una matrice di tipo colonna
public static Matrix getColumnVector( double[] vector){
double[][] w = new double[ vector.length][ 1];
for( int i = 0; i < vector.length; i++){
w[ i][ 0] = vector[ i];
}
return( new Matrix( w));
}
70
71
72
73
74
75
76
77
78
79
80
81
// stampa a schermo una matrice o un’array di array
public static void printMatrix( double[][] m){
printMatrix( new Matrix(m));
}
public static void printMatrix( Matrix m){
for( int i = 0; i < m.getColumnDimension(); i++){
for( int j = 0; j < m.getRowDimension(); j++){
System.out.print( m.get(j, i) + "\t");
}
System.out.println();
}
}
public
82
83
84
85
86
87
static void printMatrix( double[] m){
for( int j = 0; j < m.length; j++){
System.out.print( m[ j] + "\t");
}
System.out.println(System.getProperty("line.separator"));
}
88
89
90
// stampa a schermo la dimensione delle matrici
public static void printMatrixDimention( Matrix sample){
System.out.println(
sample.getRowDimension()+"x"+sample.getColumnDimension());
}
public static void printMatrixDimention( Matrix sample, String
hearder){
System.out.println( hearder + " " +
sample.getRowDimension()+"x"+sample.getColumnDimension());
}
91
92
93
94
95
96
97
98
1
}
package Networking;
2
3
4
import java.util.ArrayList;
import java.util.List;
5
6
7
import FileManager.ImmageReader;
import FileManager.LabelReader;
8
9
public class DataSetFactory {
10
11
12
13
14
15
16
// Ritorna il data set per la funzione logica OR
// se minusOne e’ vero ritorna i label tra -1 e 1 (utili per il
perceptron)
// se minusOne e’ falso ritorna i label tra 0 e 1 (utili per il
MLP)
public static DataSet getOrSet( boolean minusOne){
double vero = 1.0;
double falso = getFalseValue( minusOne);
17
Integer n = 4; // number of the samples
Integer d = 2; // number of the features
List< List< Double>> x = new ArrayList< List<Double>>(n);
List< Double> y = new ArrayList< Double>(n);
List< Double> tmp = new ArrayList< Double>(d); //
18
19
20
21
22
temporaneo
23
24
25
26
27
28
29
// inizializzo il data set con la funzione logica
tmp.add( 0.0); tmp.add( 0.0); x.add( tmp); y.add(
tmp = new ArrayList< Double>();
tmp.add( 0.0); tmp.add( 1.0); x.add( tmp); y.add(
tmp = new ArrayList< Double>(); // temporaneo
tmp.add( 1.0); tmp.add( 0.0); x.add( tmp); y.add(
tmp = new ArrayList< Double>(); // temporaneo
OR
falso);
vero);
vero);
tmp.add( 1.0); tmp.add( 1.0); x.add( tmp); y.add( vero);
30
31
DataSet m = new DataSet( x, y);
return( m);
32
33
34
}
35
36
37
38
39
40
41
// Ritorna il data set per la funzione logica AND
// se minusOne e’ vero ritorna i label tra -1 e 1 (utili per il
perceptron)
// se minusOne e’ falso ritorna i label tra 0 e 1 (utili per il
MLP)
public static DataSet getAndSet(boolean minusOne){
double vero = 1.0;
double falso = getFalseValue( minusOne);
42
Integer n = 4; // number of the samples
Integer d = 2; // number of the features
List< List< Double>> x = new ArrayList< List<Double>>(n);
List< Double> y = new ArrayList< Double>(n);
List< Double> tmp = new ArrayList< Double>(d); //
43
44
45
46
47
temporaneo
// inizializzo il data set con la funzione logica
tmp.add( 0.0); tmp.add( 0.0); x.add( tmp); y.add(
tmp = new ArrayList< Double>();
tmp.add( 0.0); tmp.add( 1.0); x.add( tmp); y.add(
tmp = new ArrayList< Double>(); // temporaneo
tmp.add( 1.0); tmp.add( 0.0); x.add( tmp); y.add(
tmp = new ArrayList< Double>(); // temporaneo
tmp.add( 1.0); tmp.add( 1.0); x.add( tmp); y.add(
48
49
50
51
52
53
54
55
OR
falso);
falso);
falso);
vero);
56
DataSet m = new DataSet( x, y);
return( m);
57
58
59
}
60
61
62
63
64
65
66
// Ritorna il data set per la funzione logica XOR
// se minusOne e’ vero ritorna i label tra -1 e 1 (utili per il
perceptron)
// se minusOne e’ falso ritorna i label tra 0 e 1 (utili per il
MLP)
public static DataSet getXorSet(boolean minusOne){
double vero = 1.0;
double falso = getFalseValue( minusOne);
67
Integer n = 4; // number of the samples
Integer d = 2; // number of the features
List< List< Double>> x = new ArrayList< List<Double>>(n);
List< Double> y = new ArrayList< Double>(n);
List< Double> tmp = new ArrayList< Double>(d); //
68
69
70
71
72
temporaneo
73
74
75
76
77
// inizializzo il data set con la funzione logica OR
tmp.add( 0.0); tmp.add( 0.0); x.add( tmp); y.add( falso);
tmp = new ArrayList< Double>();
tmp.add( 0.0); tmp.add( 1.0); x.add( tmp); y.add( vero);
tmp = new ArrayList< Double>(); // temporaneo
tmp.add( 1.0); tmp.add( 0.0); x.add( tmp); y.add( vero);
tmp = new ArrayList< Double>(); // temporaneo
tmp.add( 1.0); tmp.add( 1.0); x.add( tmp); y.add( falso);
78
79
80
81
DataSet m = new DataSet( x, y);
return( m);
82
83
}
84
85
// Ritorna il data set per le immagini di volti umani
// se minusOne e’ vero ritorna i label tra -1 e 1 (utili per il
perceptron)
// se minusOne e’ falso ritorna i label tra 0 e 1 (utili per il
MLP)
public static DataSet getFaceSet(boolean minusOne, String
setName){
ImmageReader rw = new ImmageReader( "files/face_x"+
setName +".txt", true);
rw.manipulateFile();
List<List<Double>> x = rw.getLines();
LabelReader lrw = new LabelReader("files/face_y"+
setName +".txt", true);
lrw.manipulateFile();
List<Double> y = lrw.getLines();
86
87
88
89
90
91
92
93
94
95
96
if( ! minusOne){
for( int i = 0; i < y.size(); i++){
if( y.get(i) == -1.0)
y.set(i, 0.0);
}
}
97
98
99
100
101
102
103
DataSet m = new DataSet( x, y);
return( m);
104
105
}
public static DataSet getFaceSet(boolean minusOne){
return getFaceSet( minusOne, "");
}
106
107
108
109
110
private static double getFalseValue( boolean minusOne){
if( minusOne)
return -1.0;
else
return 0.0;
}
111
112
113
114
115
116
117
1
}
package Networking;
2
3
import java.util.List;
4
5
import Jama.Matrix;
6
7
public class DataSet {
8
9
private double[][] data;
private double[] label;
10
11
12
13
14
// creare un data set da vettori
public DataSet( double[][] data, double[] label){
initialize( data, label);
}
15
16
17
18
19
20
21
// creare un data set da liste
public DataSet( List< List< Double>> data, List< Double> label){
double[][] da = MatrixComputation.listOfListToMatrix(
data).getArray();
double[] la = MatrixComputation.listToMatrix( label,
2).getArray()[0];
initialize( da, la);
}
22
23
24
25
26
27
// inizializza la classe, chiamata da entrambi i costruttori
private void initialize( double[][] data, double[] label){
if( label.length == data.length){
this.data = data;
this.label = label;
28
} else {
29
System.err.print( "Data set not correct");
30
}
31
32
}
33
34
35
36
37
// ritorna il data set come un array di array
public double[][] getData() {
return data;
}
38
39
40
41
42
// ritorna i labels come array
public double[] getLabels() {
return label;
}
43
44
45
46
47
// ritorna i labels come array di array
public double[][] getArrayLabels() {
return this.getMatrixLabels().getArray();
}
48
49
50
51
52
53
54
55
56
57
// ritorna un il sample a indice idx del dataset
public double[] getSample( Integer idx){
if( idx < getNumberOfSample()){
double[] out = new double[
getNumberOfFeatures()];
for( int i = 0; i < getNumberOfFeatures(); i++){
out[ i] = data[ idx][ i];
}
return( out);
} else {
System.err.println( " getSample( " + idx + "),
58
idex out of rage");
return( null);
59
}
60
}
61
62
// ritorna il label all’indice idx
public double getLabel( Integer idx){
return( label[ idx]);
}
63
64
65
66
67
// ritorna il numero di sample (n)
public Integer getNumberOfSample(){
return( data.length);
}
68
69
70
71
72
// ritorna il numero di feactures (d)
public Integer getNumberOfFeatures(){
return( data[ 0].length);
}
73
74
75
76
77
// ritorna il numero delle colonne dei laber (uscita della rete)
78
(p)
79
80
81
public Integer getNumberOfLabel(){
return( getMatrixLabels().getColumnDimension());
}
82
83
84
85
86
// ritorna tutto il data set come una matrice
public Matrix getMatrixData(){
return( new Matrix( data).transpose());
}
87
88
89
90
91
// ritorna tutti i label come una matrice
public Matrix getMatrixLabels(){
return( MatrixComputation.getColumnVector( label));
}
92
93
94
95
96
// ritorna il sample a indice idx come una matrice
public Matrix getMatrixSample( Integer idx){
return( MatrixComputation.getRowVector( getSample(
idx)));
}
97
98
99
100
101
102
103
104
105
106
// usato per scrivere a schermo il dataset
@Override
public String toString(){
String out = "";
for( int j = 0; j < this.getNumberOfSample(); j++){
for( int i = 0; i < this.getNumberOfFeatures();
i++){
out += data[ j][ i] + "\t";
}
out += " | " + label[ j] +
System.getProperty("line.separator");
}
return( out);
107
108
}
109
110
}
List of Links
1
2
3
4
5
6
http://www.oracle.com/index.html . . . . . . . . . . . . . . . .
5
http://docs.oracle.com/javase/tutorial/ . . . . . . . . . . . .
5
http://stackoverflow.com/ . . . . . . . . . . . . . . . . . . . . .
5
http://www.youtube.com/watch?v=KkMDCCdjyW8&list=PL84A56BC7F4A1F852 6
http://www.ebook3000.com/Java-2-by-Example_58552.html . .
6
http://www.eclipse.org/downloads/packages/eclipse-classic372/indigosr2 . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
7 http://www.techopedia.com/definition/25972/modular-programming 9
8 http://en.wikipedia.org/wiki/Algorithm . . . . . . . . . . . . 11
9 http://www.oracle.com/technetwork/java/javase/documentation/
codeconvtoc-136057.html . . . . . . . . . . . . . . . . . . . . . 11
10 http://www.oracle.com/technetwork/java/javase/documentation/
index-137868.html . . . . . . . . . . . . . . . . . . . . . . . . . 11
11 http://www.tutorialspoint.com/java/java_basic_datatypes.
htm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
12 http://commons.wikimedia.org/wiki/File:ASCII-Table.svg . 12
13 http://docs.oracle.com/javase/6/docs/api/java/lang/String.
html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
14 http://docs.oracle.com/javase/tutorial/java/nutsandbolts/
arrays.html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
15 http://docs.oracle.com/javase/7/docs/api/java/util/List.
html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
16 http://docs.oracle.com/javase/7/docs/api/java/util/Set.
html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
17 http://docs.oracle.com/javase/7/docs/api/java/util/Map.
html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
18 http://javaeesupportpatterns.blogspot.it/2012/01/javalangnullpointerexceptionhow-to.html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
19 http://stackoverflow.com/questions/1067073/initialisinga-multidimensional-array-in-java . . . . . . . . . . . . . . . 13
20 http://docs.oracle.com/javase/tutorial/java/nutsandbolts/
operators.html . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
21 http://mrbool.com/java-data-type-conversion/29257 . . . . 16
22 http://java67.blogspot.it/2013/01/how-to-format-date-injava-simpledateformat-example.html . . . . . . . . . . . . . . 21
23 http://docs.oracle.com/javase/tutorial/java/javaOO/methods.
html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
24 http://docs.oracle.com/javase/tutorial/java/IandI/polymorphism.
html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
119
25 http://docs.oracle.com/javase/7/docs/api/java/lang/Object.
html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
26 http://en.wikipedia.org/wiki/Interface_%28Java%29 . . . . 39
27 http://wiki.danse.us//danse/index.php?title=Reading_UML_
Diagrams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
28 http://www.caveofprogramming.com/frontpage/articles/java/
java-file-reading-and-writing-files-in-java/ . . . . . . 42
29 http://docs.oracle.com/javase/tutorial/essential/environment/
sysprop.html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
30 http://it.wikibooks.org/wiki/Java/AWT_e_Swing . . . . . . . 45
31 http://www.d.umn.edu/~gshute/java/swing/components.html 46
32 http://www.usability.gov/how-to-and-tools/methods/userinterface-elements.html . . . . . . . . . . . . . . . . . . . . . 46
33 http://docs.oracle.com/javase/tutorial/uiswing/components/
toplevel.html . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
34 http://docs.oracle.com/javase/tutorial/uiswing/layout/visual.
html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
35 http://docs.oracle.com/javase/7/docs/api/javax/swing/event/
package-summary.html . . . . . . . . . . . . . . . . . . . . . . . 47
36 http://cs.unibg.it/scandurra/material/INF3B_1112/windowbuilderTutorial.
pdf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
37 http://www.heatonresearch.com/encog . . . . . . . . . . . . . . 57
38 http://www.youtube.com/watch?v=TH7WyX4E3dE . . . . . . . . . 57
39 http://www.heatonresearch.com/dload/ebook/StartingEncog3Java.
zip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
40 http://math.ec.unipi.it/algebra/matric/opmatr1.htm . . . . 65
41 http://math.nist.gov/javanumerics/jama/ . . . . . . . . . . . 67
42 ogle.com/p/encog-java/downloads/list . . . . . . . . . . . . . 70
43 http://www.cs.waikato.ac.nz/ml/weka/ . . . . . . . . . . . . . 70
44 http://www.heatonresearch.com/wiki/Bias . . . . . . . . . . . 72
45 http://www.heatonresearch.com/wiki/Back_Propagation . . . 72
46 http://en.wikipedia.org/wiki/Overfitting . . . . . . . . . . 77