Fondamenti della programmazione in Java e introduzione all’apprendimento automatico. Note delle lezioni di laboratorio di programmazione. 7 febbraio 2014 Buoncompagni Luca buon_luca@yahoo.com 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 : tests.StringSorter@291fc2... è 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> *   String line = "something to write"<br> *   getFileMatipolator().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> *   String line = getFileMatipolator().readLine();<br> *   While( line != null){<br> *       // do something <br> *        ....<br> *       line = getFileMatipolator().readLine();<br> *   }<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
© Copyright 2024 Paperzz