Scuola Politecnica e delle Scienze di Base Corso di Laurea in Ingegneria Informatica Elaborato finale in Teoria dei Segnali Riconoscimento di Cifre mediante Reti Convoluzionali Anno Accademico 2016/2017 Candidato: Emanuele Cennamo matr. N46001451 Dedico questo mio lavoro, che si pone a conclusione di un lungo e faticoso cammino, alle persone che mi hanno sostenuto ed incoraggiato in questi anni: mamma e papà. Indice Indice .................................................................................................................................................. III Introduzione ......................................................................................................................................... 4 Capitolo 1: Neuroni e Reti Neurali ...................................................................................................... 6 1.1 Il Percettrone di Rosenblatt ........................................................................................................ 7 1.2 Neurone Sigma ........................................................................................................................... 9 1.3 Architettura di una Rete Neurale ............................................................................................. 10 Capitolo 2: Riconoscimento di cifre manoscritte tramite MLP ......................................................... 11 2.1 La discesa del gradiente ........................................................................................................... 12 2.2 Backpropagation ...................................................................................................................... 16 2.3 Test della rete ........................................................................................................................... 20 Capitolo 3: Migliorare le prestazioni di una Rete Neurale ................................................................ 22 3.1 The Cross-Entropy cost function ............................................................................................. 22 3.2 Metodologie per la Regolarizzazione ...................................................................................... 26 3.3 Scelta degli Hyper-parameters ................................................................................................. 32 Capitolo 4: Reti Neurali Convoluzionali ........................................................................................... 34 4.1 Idee alla base delle ConvNet .................................................................................................... 34 4.2 Schema di una ConvNet ........................................................................................................... 36 4.3 Test della rete ........................................................................................................................... 37 Conclusioni ........................................................................................................................................ 40 Bibliografia ........................................................................................................................................ 41 Introduzione Questo lavoro di tesi si occupa del problema del riconoscimento di cifre scritte a mano e studia soluzioni basate sull’uso di reti neurali artificiali, ed in particolare reti neurali convoluzionali. Per comprendere il problema, consideriamo un esempio: nella figura sottostante notiamo una sequenza numerica di cifre scritte, evidentemente, a mano; chiunque abbia almeno un titolo elementare sarà in grado di affermare, con una certa semplicità, che la sequenza corrisponde a “5-0-4-1-9-2”. Cosa è accaduto? Perché siamo in grado di riconoscere questa sequenza? Beh, potremmo rispondere a questa domanda affermando che fin da una giovane età, abbiamo visto, volontariamente o involontariamente, diverse volte scrivere uno stesso numero utilizzando calligrafie diverse e nelle prime fasi di apprendimento, quando ovviamente la nostra conoscenza era ancora insufficiente, ogni volta che compivamo un errore questo ci veniva corretto, facendo sì che la nostra capacità di riconoscerli aumentasse gradualmente. Tuttavia, questa operazione non è semplice come sembra: infatti, per arrivare alla soluzione, abbiamo impiegato (inconsciamente) circa 140 milioni di neuroni appartenenti alla corteccia visiva del nostro cervello, tutti opportunamente collegati da miliardi di connessioni per permettere la cooperazione. Messa in questi termini è comprensibile uno scoraggiamento inziale se si desidera realizzare un programma per una macchina in grado di emulare il comportamento del nostro cervello. Una prima idea potrebbe essere quella di implementare una soluzione in grado di riconoscere direttamente le forme delle nostre cifre, ad esempio: 4 consideriamo un immagine raffigurante un “9”, questa presenta un anello nella parte superiore e una barra verticale in basso a destra rispetto all’anello stesso; la difficoltà tuttavia si presenta durante la fase di traduzione in un algoritmo, questo perché, essendo una cifra scritta a mano, dovremmo andare a valutare una quantità talmente grande di eccezioni da rendere la nostra idea impraticabile. Questo ci fa capire che l’approccio è completamente sbagliato! Dobbiamo quindi ricercare tecniche alternative per implementare un programma in grado di adattarsi alle diverse eccezioni senza l’intervento diretto di un programmatore, che sia in grado di imparare da una serie di esempi e sfruttare questa conoscenza per operare. Scopo del seguente elaborato sarà proprio quello di introdurre tali tecniche ed utilizzarle per risolvere il nostro problema di riconoscimento di cifre scritte a mano; organizzato in 4 capitoli, verranno prima esposti i principi fondamentali alla base delle reti neurali, come esse sono organizzate e gli elementi che la compongono; poi, nel capitolo 2, mostreremo come risolvere il problema di riconoscimento tramite semplici Reti neurali multistrato, i Multilayer Perceptrons (MLP); il capitolo 3, invece, sarà dedicato all’introduzione di diverse metodologie per migliorare il comportamento di una rete neurale; ed infine, il capitolo 4, sarà dedicato alle Reti Neurali Convoluzionali, verranno prima introdotte e poi utilizzate per il problema di riconoscimento di cifre manoscritte. Desidero ringraziare sinceramente il Professore Poggi Giovanni, per il materiale e le indicazioni fornite durante la stesura del seguente elaborato. 5 Capitolo 1: Neuroni e Reti Neurali Prima di addentraci nel capitolo richiamiamo la definizione di “Hecht-Nielsen”: Una Rete Neurale può essere identificata come una struttura parallela in grado di elaborare informazioni distribuite; il tutto si basa sul paradigma “connessionista”, ovvero, utilizzare tanti elementi di elaborazione (neuroni) in grado di svolgere un compito elementare, e combinare opportunamente i risultati ottenuti per risolvere un compito complesso. I Neuroni hanno una propria memoria locale, sono in grado di elaborare localmente informazioni ricevute in ingresso e restituire un’uscita ottenuta con la combinazione dei valori di ingresso e del contenuto della memoria locale; la comunicazione tra esse avviene tramite una serie di canali unidirezionali. Dalla figura (1), notiamo, che i neuroni ricevono in ingresso, tramite i canali, un vettore e restituiscono in uscita un valore che viene copiato e distribuito o ad altri neuroni o in uscita, perché soluzione di un problema. L’uscita complessiva, come per l’ingresso, Fig. 1: Esempio di Rete Neurale sarà rappresentata da un vettore, la cui dimensione sarà indipendente da quella del vettore di input. In base alla tipologia di connessioni tra i neuroni possiamo fare una suddivisione delle nostre reti neurali: Reti feed-forward e Reti ricorrenti. Alla prima appartengono tutte le reti caratterizzate da un unico flusso unidirezionale di dati, ovvero prive di cicli; il valore in uscita da ciascun neurone verrà consegnato solo ed esclusivamente a neuroni appartenenti allo strato successivo. Alla seconda categoria, invece, appartengono tutte quelle reti nelle quali i propri neuroni saranno in grado di inviare il proprio risultato anche a elementi appartenenti al medesimo strato (connessioni laterali), a elementi appartenenti a strati precedenti, o in alcuni casi particolari, riproporre l’uscita in 6 ingresso al neurone stesso che l’ha prodotta. Seguendo un approccio top-down, abbiamo analizzato in generale le reti neurali, adesso quindi ci tocca fare una analisi della struttura di un generico neurone: L’insieme [x1, x2, …, xn] rappresenta il vettore del segnale di ingresso; il neurone realizzerà a partire da questi, insieme ai dati memorizzati nella memoria locale, un’uscita “y” utilizzando opportunamente una “funzione di trasferimento”; questa può essere sia semplice, come ad esempio eseguire un semplice prodotto scalare, sia complessa per permettere ai neuroni la possibilità di risolvere problemi Fig. 2: Neurone maggiormente complicati. La memoria locale, rappresentata in figura (2) da un rettangolino, conterrà una specie di conoscenza maturata durante la fase di addestramento, solo in questa sarà possibile alterare i contenuti, in modalità operativa, infatti, la memoria locale sarà a sola lettura. 1.1 Il Percettrone di Rosenblatt Il primo neurone in grado di implementare una vera e propria regola di apprendimento, fu il percettrone; introdotto da Frank Ronsenblatt nel ’58 fu utilizzato nei lavori scientifici per oltre 10 anni, tuttavia con l’avanzare della tecnologia, divennero sempre più evidente le limitazioni di cui soffriva permettendogli di risolvere solo problemi semplici, per questo motivo fu abbandonato a favore di elementi più complessi. In figura (3) abbiamo lo schema del nostro percettrone: riceve in ingresso un vettore di valori binari e restituisce un’uscita esclusivamente binaria; l’insieme [w0, w1, …, wn] rappresenta il vettore Fig. 3: Percettrone dei pesi: numeri reali, che andranno ad indicare una sorta di importanza dell’ingresso associato; i pesi insieme a quella che tra poco definiremo “bias”, rappresentano la memoria locale. La funzione di trasferimento è definita come un prodotto scalare tra il vettore dei pesi e il vettore degli ingressi, l’uscita sarà quindi: 1, π π ∑ π€π π₯π > threshold π ππ’π‘ππ’π‘ = π¦ = 0, { π π ∑ π€π π₯π ≤ threshold π Quindi nel momento in cui il percettrone dovrà esprimere una “decisione” questa verrà 7 influenzata, ovviamente, dagli ingressi e dalla loro importanza. Per fissare il concetto consideriamo un esempio: immaginiamo di voler realizzare un neurone che decida per noi se è il caso o meno di avventurarci verso il cinema, quali saranno gli ingressi in grado di influenzare la nostra decisione? Sicuramente un fattore potrebbe essere il meteo, un altro potrebbe essere la distanza ed un altro ancora il denaro a disposizione, tuttavia è ovvio che avere pochi soldi disponibili dovrebbe avere una maggiore importanza rispetto ad altro, e per questo entreranno in gioco i nostri pesi. Infine definiamo anche la soglia come un parametro in grado di influenzare l’uscita, tornando all’esempio precedente potremmo identificarla con la nostra voglia di andare al cinema: più la soglia è alta, più sarà difficile che questa venga superata dal prodotto scalare e di conseguenza abbiamo una maggiore probabilità che il neurone restituisca “0”. Per una maggiore comprensibilità riscriviamo la nostra espressione in forma vettoriale definendo “w” il vettore dei pesi, “x” il vettore degli ingressi e “b” (bias) l’opposto del nostro valore di threshold: π¦= { 1, 0, se π° ∗ π± + b > 0 se π° ∗ π± + b ≤ 0 Dove * indica il prodotto scalare. Ovviamente un percettrone da solo non potrà mai emulare il processo di decisione associato alla mente umana, quindi l’idea sarebbe quella di farli cooperare realizzando così la già citata rete neurale. Anche se non ne abbiamo ancora introdotto l’architettura, immaginiamo una generica rete per riconoscere una cifra scritta a mano, e supponiamo ad esempio di inserirvi in ingresso le intensità dei pixel appartenenti ad un’immagine raffigurante un “9”. Scopo della nostra rete, sarà quello di modificare i propri pesi e le proprie polarizzazioni (biases) in maniera tale da essere in grado di classificare correttamente la nostra cifra; noi vorremmo che ogni piccola variazione dei pesi portasse solo ad una piccola variazione della nostra uscita, infatti, se questo fosse vero saremmo in grado di far comportare la nostra rete come meglio crediamo; ad esempio supposto che essa abbia riconosciuto la nostra cifra come un “8”, tramite una piccola serie di alterazione dei pesi e delle polarizzazioni potremmo portare la nostra rete sempre più vicino a classificarla come un “9”. Purtroppo, in una rete composta da percettroni, una 8 piccola alterazione può portare la nostra rete a capovolgere completamente l’uscita restituita; quindi, supponendo che siamo anche riusciti a far classificare correttamente l’ingresso come un “9”, potremmo allo stesso tempo aver modificato anche il riconoscimento di tutte le altre cifre. Per questo motivo siamo costretti ad abbandonare l’idea di utilizzare il percettrone, a favore di qualche elemento più sofisticato in grado di soddisfare l’ipotesi sopra scritta. 1.2 Neurone Sigma Il “Sigmoid Neuron” è una versione “migliorata” del nostro percettrone, la differenza sostanziale riguarderà solo l’uscita, che potrà assumere un qualsiasi valore reale compreso tra 0 ed 1. Definiamo quindi la nostra funzione di trasferimento (o funzione sigma), come: 1 π¦ = π(π€ ∗ π₯ + π) πππ£π π(π§) = 1 + π −π§ L’uscita del nostro neurone sarà caratterizzata dall’espressione: 1 π¦= 1 + π − ∑π π€π π₯π − π Possiamo sottolineare la somiglianza con il percettrone tramite un semplice esempio: immaginiamo che “π§ = π€ ∗ π₯ + π” sia un numero molto grande e maggiore di zero, avremo che π −π§ → 0 e di conseguenza π(π§) ≈ 1, invece, supponendo “z” un valore molto grande in valore assoluto ma minore di zero abbiamo che π −π§ → ∞ e quindi π(π§) ≈ 0, ottenendo un’approssimazione di un’uscita binaria. A questo punto, l’ipotesi che ci Fig. 4: Funzione Sigma aveva portato alla bocciatura del percettrone, verrà soddisfatta dal nostro neurone sigma? La risposta è evidentemente affermativa, ed è dovuta al fatto che l’uscita potrà assumere infiniti valori compresi in [0,1]; la variazione di quest’ultima potrà essere vista come: ∂y ∂y Δy ≈ ∑π Δwπ + Δb ∂wπ ∂b Avere un’uscita non binaria può rivelarsi utile o meno a seconda dei casi: potremmo definirla utile se per esempio volessimo usare l’output per rappresentare la media dell’intensità dei pixel in un’immagine, mentre risulta essere meno utile quando vogliamo 9 ad esempio indicare se un valore di output “è un 9” oppure “non è un 9”. In questo caso è ovvio che sarebbe un lavoro più facile da fare utilizzando il percettrone, indicando con 1 o con 0 rispettivamente “è un 9” o “non è un 9”, possiamo tuttavia ovviare a questo problema utilizzando una banalissima approssimazione: se l’uscita sarà maggiore o uguale a 0.5 allora verrà approssimata a 1, altrimenti 0. 1.3 Architettura di una Rete Neurale Quando abbiamo parlato del percettrone abbiamo banalmente affermato che da solo non è in grado di risolvere problemi più complessi, ovviamente questo vale anche per i nostri Neuroni Sigma. Avremo bisogno di una rete composta da molti di questi elementi. In, particolare nel seguito consideriamo una semplice rete chiamata percettrone multilivello (MLP) anche quando l’elemento base è il neurone sigma. Come abbiamo già detto, le reti MLP Fig. 5: Esempio di MLP sono feedforward quindi, i neuroni di uno strato saranno in grado di inviare i propri output solo agli strati direttamente successivi. Possiamo suddividere la nostra architettura in tre tipi di livelli: “input layer”, contenente tutti i neuroni che inoltrano il vettore di ingresso allo strato successivo; “output layer” contenente tutti i neuroni che restituiscono il vettore di uscita; “hidden layer” uno strato di neuroni “nascosti” che non vedono né il vettore degli ingressi né il vettore delle uscite della rete. Il numero degli input è solitamente fissato dal problema, ad esempio poste tre variabili di ingresso avremo tre neuroni appartenenti all’omonimo strato; oppure, in analogia con il nostro problema, posta un’immagine di dimensione 64x64 pixel grayscale, supponendo di voler codificare le intensità dei pixel, avremo un totale di 64 ∗ 64 = 4096 neuroni di ingresso; supponendo inoltre di voler sapere solo se una cifra sia ad esempio un “9” o meno ci basterà utilizzare un solo neurone di uscita. Non andrò ad introdurre esempi di architetture ricorrenti perché non necessarie per risolvere il nostro problema del riconoscimento delle cifre, in realtà queste avrebbero un potenziale maggiore rispetto alle feedforward tuttavia il loro utilizzo è ancora molto ridotto dati gli algoritmi di apprendimento ancora particolarmente limitati. 10 Capitolo 2: Riconoscimento di cifre manoscritte tramite MLP Passiamo ora a ricercare una prima soluzione per il nostro problema del riconoscimento di cifre scritte a mano; possiamo innanzitutto suddividere questo in due sotto-problemi: 1. In primo luogo, è opportuno suddividere, o “segmentare” un’immagine contenente più cifre in più immagini indipendenti ciascuna contenente una sola cifra; ad esempio: 2. Dopo la segmentazione potremo, procedere con la classificazione di ogni singola cifra. Tuttavia, andremo a concentrare la nostra attenzione solo sul secondo punto perché: decisamente più interessante e allo stesso tempo complicato. Alla fine del secondo capitolo abbiamo detto che l’architettura a cui siamo interessati è una MLP, composta da neuroni sigma e suddivisa in tre strati. Il primo è sicuramente lo strato di input contenente tutti i neuroni in grado di codificare i valori dei pixel; questi non hanno un compito decisionale ma solo quello di “smistare” il vettore di ingresso presso lo strato nascosto. Per la nostra rete abbiamo limitato l’utilizzo a sole immagini di dimensioni 28x28 in scala di grigi (il motivo verrà spiegato più avanti), di conseguenza ci aspetteremmo di avere un totale di 784 neuroni di input in grado di “smistare” valori compresi tra 0 e 1; in particolare 0.0 indicherà il bianco, 1.0 il nero e tutti i valori intermedi rappresenteranno le diverse sfumature. Il secondo strato è uno strato nascosto ed indicheremo con “n” il numero di neuroni ad esso appartenenti; visto che non esiste un metodo univoco per la scelta di tale Fig. 6: Esempio di architettura per il nostro problema di riconoscimento delle cifre manoscritte numero il nostro scopo sarà quello di sperimentare la soluzione per diversi valori di “n”. 11 Infine lo strato di uscita della rete contiene 10 neuroni; se il primo avrà uscita circa uno, allora la rete indicherà che la cifra riconosciuta è “0”, se invece sarà il secondo ad approssimare l’unità allora la cifra riconosciuta sarà “1”, e così via. Indichiamo quindi i neuroni con identificativi da 0 a 9, colui che presenterà il valore di attivazione più alto rappresenterà la cifra riconosciuta. 2.1 La discesa del gradiente Ora che abbiamo fissato l’architettura della nostra rete dobbiamo trovare un modo per permettere a quest’ultima di imparare a riconoscere correttamente le cifre. La prima cosa che serve è un insieme di esempi di addestramento per la nostra rete, ovvero un insieme sufficientemente grande di immagini (ognuna delle quali rappresenterà una cifra) con la rispettiva etichetta; lo scopo sarà quello di dare “in pasto” alla rete le immagini, lasciare che questa le classifichi ed infine che le confronti con l’etichetta di riferimento; sarà poi la rete stessa a modificare i propri parametri interni in base ai risultati ottenuti. La costruzione di un adeguato dataset può essere complicata; fortunatamente ne esistono diversi già pronti all’uso, in particolare per i nostri studi andremo ad utilizzare il “MNIST Database” che contiene decine di migliaia di immagini di cifre scritte a mano scansionate, insieme alle rispettive classificazioni corrette. Il nome di MNIST sta per “Modified NIST” (United States’s National Institute of Standards and Technology); esso è composto da un totale di 60000 immagini di addestramento che compongono il training data e 10000 immagini di test che compongono il test data. In figura (7) possiamo notare un esempio delle immagini che sono state inserite nel nell’insieme MNIST; queste sono tutte “grayscale” e di dimensioni 28x28 (questo spiega perché precedentemente avevamo valutato uno strato di input di soli 784 neuroni). Il training set servirà appunto per addestrare la nostra rete, il test set, invece, per valutarne le prestazioni. Fig. 7: Esempio di Cifre MNIST Definiamo adesso il vettore π₯ come l’insieme degli input della rete, date immagini di dimensione 28x28 pixel, allora la dimensione del vettore sarà di 784 elementi. Insieme agli 12 ingressi definiamo le uscite, quindi il vettore π¦ composto da tanti elementi quanti sono i nostri neuroni di uscita, nel nostro caso, 10; ad esempio, per indicare che l’immagine corrisponde ad un “6” il vettore π¦ sarà: π¦(π₯) = (0,0,0,0,0,0,1,0,0,0)π Lo scopo della nostra rete è quello di restituire un’uscita che approssimi al meglio quella desiderata e l’addestramento serve a perseguire tale obiettivo. A questo punto come facciamo a sapere quanto siamo effettivamente vicini al nostro obiettivo? Per rispondere a questa domanda, andiamo a definire la funzione di costo (Mean Squared Error), come: 1 C(w, b) ≡ ∑ β₯ y(x) − a β₯2 2π π₯ Dove, “w” rappresenta tutti i pesi della rete, “b” tutte le polarizzazioni, “n” il numero totale degli ingressi di addestramento, “a” il vettore delle uscite della rete quando “x” è l’ingresso e, infine, “y(x)” l’uscita desiderata. C(w, b) è non negativo, poiché ogni termine della somma sarà maggiore o al più (nei casi ideali) uguale a zero, in più è abbastanza evidente che minore sarà il costo, più le uscite ottenute approssimeranno quelle desiderate, e di conseguenza migliore sarà il comportamento della nostra rete. Il numero delle immagini classificate correttamente, tuttavia, non è una funzione regolare dei “pesi” e delle “biases” della rete; infatti, piccole modifiche a tali parametri potrebbero non causare alcuna modifica nel numero delle immagini correttamente classificate. È evidente che quindi questo complica il tutto, data la difficoltà di capire come scegliere le modifiche per ottenere prestazioni migliori; fortunatamente, sarà qui che entrerà in gioco la nostra funzione di costo quadratico: piuttosto che valutare le modifiche in funzione del numero delle immagini correttamente classificate, conviene valutarle in funzione della nostra C(w, b); in particolare, un algoritmo di addestramento sarà tanto più efficiente quanto più riuscirà a minimizzare la “cost function”. Consideriamo una generica rappresentazione di una funzione di costo C(v), come abbiamo capito il nostro scopo sarà quello di trovare il minimo assoluto del nostro paraboloide in modo tale che la funzione di costo raggiunga il suo valore più piccolo. Per ottenere un punto estremante (come il minimo) possiamo tranquillamente farlo sfruttando le proprietà delle derivate, tuttavia il problema è che le moderne reti neurali possono comprendere miliardi di parametri, quindi ottenere direttamente il nostro minimo potrebbe 13 non essere un compito particolarmente semplice; dobbiamo quindi considerare un algoritmo che si comporti come un risolutore di un problema reale: immaginiamo il nostro paraboloide come una valle, se noi buttassimo una pallina in quest’ultima è evidente che per le leggi fisiche questa tenderà, dopo Fig. 8: Esempio di Cost Function un certo intervallo di tempo, a raggiungere il fondo. Supponiamo ora di spostare la pallina di una piccola quantità βπ£1 con direzione π£1 e di una piccola quantità βπ£2 con direzione π£2 , la variazione di C corrisponderà a: ∂C ∂C 1 ∂π£2 ΔπΆ ≈ ∂π£ βπ£1 + βπ£2 Definiamo il vettore delle variazioni e il gradiente di C rispetto alle variabili π£1 e π£2 come: βv = (βπ£1 , βπ£2 )π ∂C ∂C π ∇C = (∂π£ , ∂π£ ) 1 2 Possiamo quindi riscrivere tramite queste la variazione di C: βC ≈ ∇C ∗ βv Supponiamo adesso di scegliere il vettore delle variazioni βπ£ = −η∇C, ottenendo: βC ≈ −η∇C ∗ ∇C = −ηβ₯ ∇C β₯2 Abbiamo detto che il nostro scopo è quello di minimizzare il valore di costo ottenuto, quindi è evidente che la variazione di C deve essere negativa in modo tale da ottenere valori sempre più piccoli; il secondo parametro per ovvi motivi non può essere negativo, quindi affinché le nostre ipotesi siano verificate è necessario che “η” (tasso di apprendimento) sia un valore strettamente positivo. Abbiamo quindi fatto un passo da gigante verso la nostra soluzione, tuttavia però ci assale un dubbio: abbiamo detto quali sono i vincoli che deve rispettare il nostro tasso, ma non come questo deve essere scelto. Come abbiamo detto, ci stiamo spostando lungo la superfice del nostro paraboloide, quindi dovremmo ripetere più volte questi spostamenti fino a raggiungere un punto che sia sufficientemente piccolo; è ovvio che quanto più grande fosse il nostro tasso, quanto più velocemente “scenderemmo” lungo il paraboloide, tuttavia allo stesso tempo rischieremmo di non raggiungere mai un valore sufficientemente piccolo perché verrebbe sempre superato; di controparte, nemmeno troppo piccolo va bene perché altrimenti prima di raggiungere il nostro obbiettivo saranno necessari 14 tantissimi spostamenti, richiedendo tempi troppo elevati. La giusta scelta sarà quella di considerare un compromesso. Fino ad ora abbiamo considerato un esempio in due variabili, ovviamente possiamo tranquillamente estendere la nostra trattazione anche a vettori di “π” dimensioni. Abbiamo visto come valutare la variazione del costo, ma per poter permettere alla rete di imparare è necessario modificare i suoi parametri interni tramite una generica regola di apprendimento: π£ → π£′ = π£ − η∇C La quale, tradotta in funzione dei “pesi” e delle “biases”, assume l’espressione: ∂C π€π → π€ ′ π = π€π − η ∂π€ ∂C ′ ππ → π π = ππ − η ∂π π π Prendiamo di nuovo in considerazione la nostra funzione di costo, e riscriviamola come: 1 β₯ π¦(π₯) − π β₯2 πΆ = ∑ πΆπ₯ , πππ£π πΆπ₯ = π 2 π₯ Quindi per calcolare il gradiente di C sarà necessario calcolare prima tutti i singoli gradienti di Cx e poi ottenerne la media: ∇C = 1 ∑ ∇πΆπ₯ π π₯ È ovvio che l’apprendimento, a causa di ciò, sarà decisamente lento per una grande quantità di ingressi; un’idea per superare quest’ultimo ostacolo è quello di stimare il gradiente ∇C calcolando ∇πΆπ₯ solo per un sottoinsieme più piccolo di ingressi scelto a caso, garantendo così una buona stima in tempi decisamente ridotti; questa tecnica prenderà il nome di “Stocastic Gradient Descent”, ottenendo: ∑π ∑π₯ ∇πΆπ₯ π=1 ∇πΆππ ∇C = ≈ π π Dove “π” non è altri che la dimensione del sottoinsieme di ingressi scelto; ovviamente dobbiamo modificare anche la nostra regola di apprendimento, ottenendo: ∂πΆππ η π€π → π€ ′ π = π€π − ∑ π ∂π€π π ππ → π ′ π = ππ − ∂πΆππ η ∑ π ∂ππ π 1 Concludiamo il paragrafo, dicendo che in alcuni casi è possibile omettere il fattore π dalla 15 nostra funzione di costo, questo capita quando non è noto a priori il numero totale degli esempi di addestramento perché, ad esempio, potrebbero essere generati a “run time”. 2.2 Backpropagation Abbiamo visto nel paragrafo precedente come le reti neurali possano apprendere modificando i loro parametri, sfruttando un algoritmo di discesa del gradiente; adesso però ci ritroviamo di fronte ad un altro problema, ovvero la necessità di capire come calcolare il gradiente della funzione di costo: andiamo quindi ad introdurre l’algoritmo “backpropagation”. Inizialmente introdotto nel 1970, fu apprezzato solo dopo circa 16 anni, quando comparve in un saggio di David Rumelhart, Geoffrey Hinton, e Ronald Williams, dove fu effettivamente mostrato che questo risultava funzionare molto più velocemente rispetto ai suoi concorrenti. Sulla scia di quanto visto per una rete MLP, identifichiamo: π πππ = π (∑ π€ππ πππ−1 + πππ ) π Dove: π • π€ππ andrà ad indicare il peso per il collegamento tra il k-esimo neurone appartenente allo (l-1)-esimo strato e lo j-esimo neurone appartenente allo l-esimo strato; • πππ indicherà la polarizzazione dello j-esimo neurone appartenente allo l-esimo strato; • πππ indicherà “l’attivazione” dello j-esimo neurone appartenente allo l-esimo strato. Possiamo quindi dedurre che per ogni strato abbiamo una matrice dei pesi π€ π : il generico π peso π€ππ corrisponderà all’elemento della matrice avente riga “j” e colonna “k”; in modo analogo per ogni l-esimo strato abbiamo un vettore di polarizzazione π π , e un vettore di attivazione ππ . Detto questo, potremmo riscrivere la nostra espressione in forma matriciale, ricordando che la funzione “π” verrà applicata per ogni elemento di un generico vettore v; ad esempio, supposta una generica funzione π(π₯) = π₯2 , abbiamo che: π(2) 2 4 π ([ ]) = [ ]=[ ] π(3) 3 9 Adesso siamo in grado di riscrivere la nostra equazione in forma vettorizzata: πΌ π = π(π€ π πΌ π−1 + π π ) Oppure: πΌ π = π(π§ π ) πππ£π π§ π ≡ π€ π πΌ π−1 + π π 16 Le π§ π rappresentano gli ingressi pesati dei neuroni dello l-esimo strato. 2.2.1 Ipotesi fondamentali sulla funzione di costo L’obiettivo dell’algoritmo “backpropagation” è quello di calcolare le derivate parziali della funzione di costo rispetto ai pesi e alle polarizzazioni; a tal fine la funzione di costo deve godere di due particolari proprietà: 1. La funzione di costo deve poter essere scritta come media di funzioni di costo 1 corrispondenti ai singoli esempi di training x, ovvero: πΆ = π ∑π₯ πΆπ₯ ; questo perché l’algoritmo andrà a calcolare le derivate parziali direttamente dei singoli costi Cx; 2. Il costo dovrà sempre poter essere scritto come una funzione delle uscite della nostra rete neurale, ovvero: πΆ = πΆ(ππΏ ); dove ππΏ è il vettore di attivazione dello strato di output. È abbastanza banale dimostrare che la nostra funzione di costo quadratica rispetta perfettamente queste due ipotesi: nel paragrafo precedente abbiamo già dimostrato la prima, per la seconda basta vedere che questa può essere scritta come: 1 1 πΆ = β₯ y − απΏ β₯2 = ∑(yπ − αππΏ )2 2 2 π 2.2.2 Le quattro equazioni su cui si fonda la backpropagation Prima di addentrarci nella discussione, introduciamo una nuova quantità, δππ , che chiameremo “errore” dello j-esimo neurone appartenente allo l-esimo strato; esso sarà in grado di alterare il funzionamento del nostro neurone, infatti, invece di ricevere in uscita un output del tipo π(π§ππ ) otterremo π(π§ππ + βπ§ππ ) dove βπ§ππ sarà una piccola quantità che si sovrapporrà all’ingresso pesato. È abbastanza facile immaginare che questa modifica non altererà solo l’uscita dello j-esimo neurone ma andrà anche a propagarsi attraverso tutti i successivi strati della rete, la cui decisione dipenderà da quella del neurone affetto da errore. Quindi, è evidente che il costo complessivo ne risentirà anche di una quantità: ∂C π π βπ§π ∂π§π L’idea a questo punto è quella di utilizzare l’errore per i nostri scopi, ovvero sfruttare la quantità βπ§ππ per provare a minimizzare il nostro costo. Definiamo δπ il vettore degli errori associati allo l-esimo strato e un generico errore δππ come: 17 δππ ≡ ∂C ∂π§ππ In questo modo possiamo avviare la trattazione delle nostre 4 equazioni: 1. Equazione per l’errore nello strato di output δπΏ : ∂C δππΏ ≡ πΏ π ′ (π§ππΏ ) ∂ππ Il primo termine del secondo membro misura quanto velocemente si modifica il costo in funzione della j-esima attivazione dello strato di output, mentre il secondo termine, ovvero π ′ (π§ππΏ ), rappresenta quanto velocemente la funzione di attivazione π sta cambiando; estendendo in forma matriciale, otterremo: δπΏ = ∇π πΆ β π ′ (π§ πΏ ) Dove β indica il prodotto di Hadamard, o punto a punto, fra matrici, mentre ∇π πΆ rappresenta tutte le derivate parziali di C rispetto a “πππΏ ” per ogni valore di “j”; calcolare la derivata di una funzione costo quadratica, non è particolarmente complicato: 1 1 πΆ = 2 β₯ y − απΏ β₯2 = 2 ∑π(yπ − αππΏ )2 ↔ ∂C ∂πππΏ = −(yπ − αππΏ ) Quindi possiamo riscrivere la nostra equazione anche nella forma: δπΏ = −(y − απΏ ) β π ′ (π§ πΏ ) 2. Equazione per l’errore δπ in termini di errore nello strato successivo δπ+1 : δπ = ((π€ π+1 )π δπ+1 ) β π ′ (π§ π ) Tramite questa, supponendo di conoscere il valore di δπ+1 e la matrice trasposta dei pesi associata allo (l+1)-esimo strato, possiamo recuperare informazioni su come si sposta all’indietro l’errore attraverso la rete. Banalmente, tramite l’utilizzo delle prime due equazioni, possiamo calcolare l'errore δπ per ogni strato. 3. Equazione per il tasso di variazione del costo rispetto a qualsiasi “biases” della rete: ∂C = δππ ∂πππ Ovvero l’errore δππ è esattamente uguale alla derivata parziale rispetto alla polarizzazione di un j-esimo neurone appartenente allo l-esimo strato. Possiamo quindi estendere la relazione a tutta la rete, ottenendo: ∂C = δ ∂π 18 4. Equazione per il tasso di variazione del costo rispetto a qualsiasi peso della rete: ∂C π−1 π π = ππ δπ ∂π€ππ Riscrivendola in forma matriciale: ∂C = ππ−1 δπ ∂π€ π ↔ ∂C = πππ δππ’π‘ ∂π€ Questa seconda riscrittura, decisamente più leggibile, ci lascia intuire che possiamo calcolare la derivata parziale di C rispetto ai pesi di un generico strato “l” attraverso il vettore di attivazione dello strato immediatamente precedente (che quindi corrisponderà all’ingresso dello l-esimo strato) ed il vettore dell’errore associato allo l-esimo strato. Una diretta conseguenza di quest’ultima equazione è che quando πππ è molto piccolo, quindi staremo parlando di un vettore a bassa attivazione, tenderà ad esserlo anche la derivata parziale del costo rispetto ai pesi, portando “w” a variare molto lentamente e a parlare di: “apprendimento lento”. Guardiamo adesso il livello di uscita, quindi faremo riferimento alla prima equazione, possiamo facilmente notare che qui comparirà la funzione π ′ (π§ππΏ ). Nel capitolo precedente abbiamo mostrato il grafico di una sigmoide, notando che per valori di “z” molto grandi o molto piccoli, “π(π§)” tende ad assumere un comportamento costante; ciò porta la derivata, in questi punti, ad essere circa nulla, ottenendo come conseguenza un apprendimento lento. Diremo quindi che un peso nello strato finale imparerà lentamente se il neurone di uscita è a bassa attivazione (~0) o alta attivazione (~1). Possiamo tranquillamente estendere queste considerazione anche agli strati intermedi, nella seconda equazione, infatti, compare nuovamente la derivata della funzione sigma; nel prossimo capitolo vedremo come risolvere questo problema. 2.2.3 Passi per la costruzione di un algoritmo backpropagation Per la costruzione di un algoritmo backpropagation dobbiamo affrontare le seguenti azioni: 1. Input x: Impostare il corrispondente vettore di attivazione per l’input layer (π1 ). 2. Feedforward: Per ogni l = 2, 3, …, L calcolare: π§ π = π€ π ππ−1 + π π , ππ = π(π§ π ) 3. Output error δπΏ : Applicare la prima equazione vista nel sottoparagrafo precedente. 19 4. Backpropagate the error: Per ogni l = L-1, L-2, …, 2 calcolare la seconda equazione vista nel sotto-paragrafo precedente. 5. Output: Il gradiente della funzione di costo è data da: ∂C π−1 π π = ππ δπ ∂π€ππ ∂C = δππ ∂πππ L’algoritmo ha calcolato il gradiente della funzione di costo per un singolo esempio di training “x", quindi in realtà siamo andati a valutare Cx e non C; per considerare un intero training set dovremo combinarlo con un algoritmo di apprendimento come “SGD – Stocastic Gradient Descent”: 1. Inserire in ingresso una serie di esempi di addestramento; 2. Per ogni “x” calcolare il gradiente tramite la backpropagation; 3. Gradient descent: Per ogni l = L, L-1, …, 2 applichiamo la regola di apprendimento: π π€ π → π€ π − ∑ δπ₯,π (π π₯,π−1 )π , π π₯ π π π π → π − ∑ δπ₯,π . π π₯ 2.3 Test della rete Possiamo adesso valutare le prestazione della nostra rete, in particolare analizzeremo la precisione di classificazione sui dati di test. I parametri utilizzati, sono gli stessi presentati nel libro “Neural Networks and Deep Learning” [1], tuttavia i risultati ottenuti li ho generati personalmente sul mio calcolatore; infatti, sarebbe inutile riportare gli stessi risultati dato che: essendo i pesi e le polarizzazioni iniziali generati casualmente tramite v.a. Gaussiane (parleremo di questo in modo approfondito nel prossimo capitolo), i risultati saranno, soprattutto per le prime epoche, diversi; inoltre per motivi di spazio mostrerò i risultati ottenuti solo dalle prime tre e ultime tre epoche di addestramento. Il programma che ho utilizzato per inizializzare la nostra rete prende il nome di “network.py" il cui codice sorgente è disponibile su “github.com” [2]. Ricordiamo velocemente che un mini-batch è un sottoinsieme casuale di esempi di training, e l’algoritmo SGD effettuerà un unico passo di discesa del gradiente per ognuno di essi. In questa prima analisi abbiamo deciso di utilizzare 30 neuroni nascosti e un tasso di apprendimento pari a 3.0; è abbastanza evidente 20 che i risultati sono molto promettenti, infatti, già dopo la prima epoca di addestramento abbiamo riconosciuto un totale di 8993/10000 immagini appartenenti al test set, per poi arrivare alla fine del ciclo di addestramento a saper riconoscere correttamente 9495/10000 immagini, Fig. 9: Tabella di precisione sul Test Set ottenendo una precisione del 94.95% sul test data. Ovviamente è facile immaginare che cambiando i nostri parametri otteniamo una percentuale di precisione diversa: proviamo, ad esempio, ad aumentare da 30 a 100 il totale dei neuroni nascosti e vediamo cosa accade. Per quanto riguarda le prime epoche, non abbiamo un particolare cambio di percentuale, cosa diversa Fig. 10: Tabella di precisione sul Test Set invece verso la fine; al termine del ciclo di apprendimento la percentuale è salita dal 94.95% al 96.51%; in questo caso abbiamo visto che aumentare il numero di neuroni nascosti ci aiuta ad ottenere una rete migliore. Nei paragrafi precedenti abbiamo affermato che la scelta del tasso di apprendimento deve essere ben ponderata, ovvero scegliere un valore che non sia né troppo grande né troppo piccolo, spiegando teoricamente gli effetti che questi avranno sulla rete; a questo punto sembra interessante avere anche una dimostrazione pratica, per questo valuteremo prima Fig. 11: Tabella di precisione sul Test Set un tasso di apprendimento eccessivamente grande (100.0) sia uno eccessivamente piccolo (0.001): Avere un tasso eccessivamente grande significa non raggiungere mai un valore sufficientemente piccolo in grado di minimizzare il più possibile la funzione di costo, infatti notiamo in figura (11) che la rete raggiungerà una precisione del 10.1% senza riuscire mai a migliorarla. Fig. 12: Tabella di precisione sul Test Set Situazione leggermente diversa nell’avere un tasso eccessivamente piccolo, in questo caso fino alla quinta o sesta epoca la precisione è altalenante, dopo inizia a presentare una serie di piccoli miglioramenti, dimostrando che è possibile tramite questi ottenere una rete ottimale ma che 30 epoche non saranno sufficienti. 21 Capitolo 3: Migliorare le prestazioni di una Rete Neurale Nel capitolo precedente abbiamo trovato una soluzione (non particolarmente complessa) per implementare delle reti neurali in grado di risolvere il problema del riconoscimento delle cifre scritte a mano, ottenendo anche una percentuale di precisione molto alta; adesso consideriamo alcuni miglioramenti allo scopo di ottenere una rete più efficiente e allo stesso tempo più veloce. 3.1 The Cross-Entropy cost function Una cosa interessante della mente umana è sicuramente la risposta all’errore, ovvero, un essere umano dopo aver commesso un errore ha maggiori possibilità che questo non venga più; potremmo quindi banalmente dire che un uomo è in grado di imparare più velocemente dopo aver sbagliato. Definiamo adesso un “toy problem” come un problema il cui scopo è esclusivamente illustrare (o mettere alla prova) metodi di risoluzione; questi, quindi, devono essere descritti in modo preciso e sintetico. Consideriamo quindi un “toy problem”: vogliamo valutare un neurone in grado di restituirci in uscita il valore “0” quando in ingresso si presenta un “1”; consideriamo un peso π€ = 0.6 e π = 0.9, Fig. 13: Neurone Sigma questi inizialmente vanno sempre fissati, in maniera tale da avere un punto di partenza per il nostro algoritmo. Considerato π₯ = 1.0, l’uscita sarà uguale a: 1 π¦= = 0.82 −(0.6∗1+0.9) 1+π Notiamo quindi che la risposta del nostro neurone si avvicina molto di più all’unità che allo zero, dobbiamo quindi procedere con l’apprendimento: scegliamo un tasso di 0.15 e un numero di epoche pari a 300. Ho scritto il seguente codice utilizzando la versione 2.7 di python: inizializza un neurone, procede con l’apprendimento e stampa i risultati. 22 from random import choice import numpy as np import fpformat as fp """ ************************* CLASSE NEURONE SIGMA ************************ """ class SigmaNeuron(): def __init__(self, initial_weight, initial_bias): self.weight = initial_weight self.bias = initial_bias def cost_derivative(self, output_activation, y): return (output_activation-y) def training(self, training_data, epoch, eta): print("--------------------- Fase di apprendimento ------------------") x, expected = choice(training_data) for i in xrange(epoch): z = np.dot(self.weight, x) + self.bias result = sigmoid(z) result_derivate = sigmoid_prime(z) delta = self.cost_derivative(result, expected)*result_derivate self.weight = self.weight - (eta)*delta*x/1 self.bias = self.bias - (eta)*delta/1 z = np.dot(self.weight, x) + self.bias self.stamp_result(i, x, sigmoid(z)) def stamp_result(self, i, x, result): print("Epoch [{}]: imput: {} -> output: {} (w,b)=({},{})".format( i,x,fp.fix(result,2),fp.fix(self.weight,2),fp.fix(self.bias,2))) def classify(self, x): print("--------------------- Fase di decisione ---------------------") z = np.dot(self.weight, x) + self.bias result = sigmoid(z) print("imput: {} -> output: {}".format(x, fp.fix(result, 2))) def sigmoid(z): return 1.0/(1.0+np.exp(-z)) def sigmoid_prime(z): return sigmoid(z)*(1-sigmoid(z)) """ ***************************** TEST NEURONE **************************** """ sigma_neuron = SigmaNeuron(0.6, 0.9) training_data = [(1, 0)] sigma_neuron.training(training_data, 300, 0.15) print("") sigma_neuron.classify(1) Alla fine del nostro apprendimento il risultato ottenuto sarà: --------------------- Fase di apprendimento --------------------Epoch [0]: imput: 1 -> output: 0.81 - (w,b)=(0.58,0.88) Epoch [1]: imput: 1 -> output: 0.81 - (w,b)=(0.56,0.86) Epoch [2]: imput: 1 -> output: 0.80 - (w,b)=(0.54,0.84) Epoch [3]: imput: 1 -> output: 0.79 - (w,b)=(0.53,0.83) Epoch [4]: imput: 1 -> output: 0.79 - (w,b)=(0.51,0.81) Epoch [5]: imput: 1 -> output: 0.78 - (w,b)=(0.49,0.79) … Epoch [296]: imput: 1 -> output: 0.09 - (w,b)=(-1.28,-0.98) Epoch [297]: imput: 1 -> output: 0.09 - (w,b)=(-1.28,-0.98) Epoch [298]: imput: 1 -> output: 0.09 - (w,b)=(-1.28,-0.98) Epoch [299]: imput: 1 -> output: 0.09 - (w,b)=(-1.28,-0.98) --------------------- Fase di decisione --------------------imput: 1 -> output: 0.09 Il neurone, dopo 300 epoche di addestramento, riuscirà a restituire un’uscita che sia una buona approssimazione di quella desiderata, il costo verrà fortemente ridotto nelle prime 150 epoche per poi ricevere solo piccole variazioni in quelle successive. Cosa accade invece se inizializziamo i parametri interni del neurone con il valore 2.0? L’uscita sarà: 23 ππ’π‘ππ’π‘ = 1 1+ π −(2∗1+2) = 0.98 Questa volta il neurone restituirà praticamente “1” in uscita; dovrà quindi essere eseguita una fase di apprendimento, ottenendo: --------------------- Fase di apprendimento --------------------Epoch [0]: imput: 1 -> output: 0.98 - (w,b)=(2.00,2.00) Epoch [1]: imput: 1 -> output: 0.98 - (w,b)=(1.99,1.99) Epoch [2]: imput: 1 -> output: 0.98 - (w,b)=(1.99,1.99) Epoch [3]: imput: 1 -> output: 0.98 - (w,b)=(1.99,1.99) … Epoch [39]: imput: 1 -> output: 0.98 - (w,b)=(1.88,1.88) … Epoch [82]: imput: 1 -> output: 0.97 - (w,b)=(1.72,1.72) … Epoch [118]: imput: 1 -> output: 0.96 - (w,b)=(1.54,1.54) … Epoch [153]: imput: 1 -> output: 0.93 - (w,b)=(1.28,1.28) … Epoch [191]: imput: 1 -> output: 0.83 - (w,b)=(0.81,0.81) … Epoch [208]: imput: 1 -> output: 0.72 - (w,b)=(0.47,0.47) … Epoch [230]: imput: 1 -> output: 0.50 - (w,b)=(0.00,0.00) … Epoch [296]: imput: 1 -> output: 0.21 - (w,b)=(-0.67,-0.67) Epoch [297]: imput: 1 -> output: 0.21 - (w,b)=(-0.67,-0.67) Epoch [298]: imput: 1 -> output: 0.20 - (w,b)=(-0.68,-0.68) Epoch [299]: imput: 1 -> output: 0.20 - (w,b)=(-0.68,-0.68) --------------------- Fase di decisione --------------------imput: 1 -> output: 0.20 Cosa notiamo di diverso? Vediamo che per le prime 150 epoche l’apprendimento sembra non avere effetto, i parametri del neurone verranno infatti modificati tramite variazioni troppo piccole. Ben diversa invece è la situazione nelle epoche successive, la velocità di apprendimento aumenterà notevolmente ma ormai troppo tardi, non basteranno infatti le rimanenti epoche per approssimare al meglio lo zero in uscita; l’effetto ottenuto prende il nome di apprendimento lento, permettendoci di dedurre che più il neurone sbaglierà la sua classificazione, maggiore sarà la difficoltà affrontata da quest’ultimo in fase di apprendimento. La causa dell’apprendimento lento è già stata introdotta nel capitolo precedente: abbiamo dimostrato che la colpa è dovuta alla derivata prima della funzione sigma la quale è legata alla derivata della funzione costo secondo le relazioni: ππΆ = (π − π¦)π ′ (π§)π₯ ππ€ ππΆ = (π − π¦)π ′ (π§)π₯ ππ Tornando al nostro esempio, essendo la coppia (x,y) = (1,0) abbiamo che entrambe valgono “ππ ′ (π§)”; dobbiamo quindi passare alla ricerca di una funzione di costo che elimini 24 quest’accoppiamento. Introduciamo la funzione di costo Cross-entropy per un neurone: 1 πΆ = − ∑[π¦ ππ(π) + (1 − π¦)ln(1 − π)] π π₯ Prima di tutto per dimostrare che questa sia una valida funzione di costo, dobbiamo verificare che sia una quantità positiva e allo stesso tempo che tenda a zero: 1. C > 0: vero, perché abbiamo una somma di quantità negative (il logaritmo restituisce un valore negativo quando l’argomento è minore dell’unità), moltiplicata per “-1”. 2. C → 0: per dimostrarla consideriamo nuovamente il problema giocattolo e immaginiamo che il nostro neurone si sta comportando bene, restituendo π ≈ 0, quindi: πΆπ₯ ≈ [0 ∗ ππ(π) + (1 − 0)ππ(1 − 0)] = ππ(1) = 0 Abbiamo dimostrato, quindi, che questa è una funzione di costo valida, tuttavia ciò che a noi interessava era sapere se questa riesce a superare il problema dell’apprendimento lento; consideriamo, quindi, la derivata della nostra funzione π(π§): π(π§) = −π −π§ 1 1+π −π§ 1+π −π§ 1 ↔ π ′ (π§) = − (1+π −π§ )2 = − (1+π −π§ )2 + (1+π −π§ )2 = π(π§)(1 − π(π§)) Sapendo che π = π(π§), riscriviamo la funzione di costo come: ∂C 1 π¦ 1 − π¦ ∂π 1 π¦ 1−π¦ = − ∑( − ) = − ∑( − ) π ′ (π§)π₯π ∂π€π π π(π§) 1 − π(π§) ∂π€π π π(π§) 1 − π(π§) π₯ π₯ π¦(1 − π(π§)) − (1 − π¦)π(π§) ′ 1 1 = − ∑( ) π (π§)π₯π = ∑(π(π§) − π¦) π₯π π π π(π§)(1 − π(π§)) π₯ π₯ Ed ecco il risultato che ci aspettavamo, ovvero la velocità con cui il peso apprende è controllato da (π(π§) − π¦), quindi dall’errore di output e non più dalla derivata dell’attivazione; è possibile fare lo stesso ragionamento anche per le polarizzazioni: ππΆ 1 π¦ 1 − π¦ ππ 1 = − ∑( − ) = ∑(π(π§) − π¦) ππ π π(π§) 1 − π(π§) ππ π π₯ π₯ Tornando ora al nostro esempio, modifichiamo il codice eliminando “result_derivate()” dal calcolo della delta mentre il metodo “cost_derivative()” restituirà il rapporto “(output_activation–y)/(output_activation-output_activation**2)” otterremo: --------------------- Fase di apprendimento --------------------Epoch [0]: imput: 1 -> output: 0.81 - (w,b)=(0.57,0.87) Epoch [1]: imput: 1 -> output: 0.80 - (w,b)=(0.55,0.85) Epoch [2]: imput: 1 -> output: 0.79 - (w,b)=(0.52,0.82) … Epoch [299]: imput: 1 -> output: 0.04 - (w,b)=(-1.73,-1.43) --------------------- Fase di decisione --------------------imput: 1 -> output: 0.04 25 Abbiamo considerato la nostra funzione di costo Cross-entropy per un singolo neurone; tuttavia possiamo tranquillamente estendere tali studi anche per reti neurali multi-strato: 1 πΆ = − ∑ ∑[π¦π ππ(πππΏ ) + (1 − π¦π )ππ(1 − πππΏ )] π π₯ π Applichiamo ora la funzione di costo Crossentropy per il riconoscimento di cifre, e vediamo come, e se, cambia la nostra precisione sul test set. Alla fine del ciclo di apprendimento abbiamo una precisione del 95.50%, circa 0.55% in più Fig. 14: Tabella di precisione sul Test Set rispetto alla vecchia funzione di costo quadratica, ed in più ci viene restituita una precisione elevata già dalle prime epoche. 3.2 Metodologie per la Regolarizzazione Scopo principe di qualunque progetto di macchina intelligente è quello di generalizzare il suo lavoro a nuove situazioni, potremmo dire quindi che il vero test per un generico modello è la sua capacità di fare previsioni anche in situazioni dove non è mai stato esposto. Cerchiamo quindi di capire se la nostra rete per il riconoscimento di cifre si comporta correttamente o meno nei confronti della generalizzazione. Un’eventuale mancata generalizzazione solitamente è più evidente quando il training set non è eccessivamente grande, quindi valutiamo solo 1000 immagini di apprendimento. I prossimi grafici che presenterò li ho generati tramite un programma chiamato “overfitting.py” il cui codice sorgente è messo a disposizione sul sito “github.com” [2]; per ogni grafico sono andato a considerare il “range” compreso tra le epoche 200 e 399; questa decisione è stata presa per avere un buon ingrandimento delle fasi successive all’apprendimento iniziale. Il primo grafico rappresenta i valori assunti dal costo in funzione delle diverse epoche; l’andamento di questo è molto incoraggiante, era infatti proprio quello che Fig. 15: Cost on Training Data ci aspettavamo: una funzione decrescente. Il secondo grafico invece rappresenta la 26 precisione sul test set; questo lascia trapelare che dopo un certo numero di epoche, in questo caso circa 280, la percentuale tende ad assumere valori intorno all’82%, mostrando quindi che il modello non sta più imparando, nonostante la funzione di costo ci mostri un miglioramento continuo. Diremo che la rete oltre l’epoca 280 sarà affetta da overfitting (sovradattamento) o da overtraining (sovrapprendimento). L’overfitting è Fig. 16: Accuracy on Test Data un problema serio nelle reti neurali, soprattutto quando queste sono caratterizzate da un gran numero di pesi e biases; ricordando ad esempio la nostra rete composta da [784; 30; 10] neuroni, abbiamo: 784 ∗ 30 + 30 + 30 ∗ 10 + 10 = 26860 parametri liberi tra pesi e polarizzazioni, quindi anche una rete sufficientemente semplice come quella per riconoscere cifre con soli 30 neuroni nascosti, possiede decine di migliaia di parametri; dobbiamo quindi trovare un modo per rilevare il sovradattamento e delle tecniche alternative per ridurlo. Una tecnica per rilevarlo è già stata introdotta, ovvero vedere quando la rete ha smesso di imparare, e quindi terminare preventivamente l’apprendimento, tuttavia questa non è infallibile: in alcuni casi la precisione sul test rimane costante per un tempo limitato per poi riprendere a crescere, e quindi rischia di essere falsamente dichiarato affetto da overfitting. Quali tecniche potrebbero essere utilizzate per garantire la riduzione dell’overfitting? Una possibilità sarebbe quella di ridurre le dimensioni della nostra rete, tuttavia spesso avere una grande quantità di parametri liberi risulta necessario ai fini della risoluzione del problema quindi dobbiamo bocciarla istantaneamente; fortunatamente esistono un insieme di tecniche di regolarizzazione che andremo ad esporre nei successivi sotto-paragrafi. 3.2.1 L2 Regularization “L2 Regularization” è una delle più efficienti e famose tecniche di regolarizzazione: consiste nell’aggiungere un termine supplementare alla funzione di costo chiamato “termine di regolarizzazione”. Prendiamo la funzione di costo Cross-entropy e regolarizziamola: 1 π πΆ = − ∑ ∑[π¦π ππ(πππΏ ) + (1 − π¦π ) ππ(1 − πππΏ )] + ∑ π€2 π 2π π₯ π π€ Il primo termine del secondo membro è stato già presentato nei paragrafi precedenti, il 27 secondo è dovuto alla regolarizzazione: corrisponde alla somma dei quadrati di tutti i pesi π della rete scalata di un fattore 2π con λ > 0, chiamato “parametro di regolarizzazione”, e π, come al solito, rappresenta la dimensione dell’insieme di apprendimento. Possiamo utilizzare questa tecnica per qualsiasi funzione di costo, quindi da adesso in poi procediamo utilizzando una relazione generale del tipo: πΆ = πΆ0 + π ∑ π€2 2π π€ Lo scopo della regolarizzazione è quello di fare in modo che si raggiunga un compromesso tra il trovare piccoli pesi per la rete e allo stesso tempo minimizzare la funzione di costo originale; l’importanza relativa dei due elementi di compromesso dipende dal valore di λ: quando è molto piccolo allora preferiamo minimizzare la funzione di costo originale, viceversa quando è grande preferiamo operare con piccoli pesi. Le derivate della funzione di costo saranno: ∂C ∂C0 π = + π€ ∂π€ ∂π€ π ∂C ∂C0 = ∂π ∂π Notiamo subito che la regolarizzazione non va ad alterare il valore delle polarizzazioni quindi riscriviamo solo la regola di apprendimento per i pesi: π€ →π€−π ∂C0 π π ∂C0 − π π€ = (1 − π ) π€ − π ∂π€ π π ∂π€ A causa del rapporto dovuto alla regolarizzazione abbiamo che il peso verrà scalato di una certa quantità, per questo Fig. 17: Cost on Training Data motivo questa tecnica di regolarizzazione viene anche chiamata tecnica per il “decadimento di peso”. Torniamo nuovamente al nostro problema della classificazione di cifre MNIST e vediamo come cambia la precisione con l’aggiunta di un fattore λ = 0.1. Notiamo che il costo sui dati di addestramento diminuisce nel tempo in modo Fig. 18: Accuracy on Test Data analogo al caso non regolarizzato, questo non è un problema perché avere un costo decrescente è proprio ciò a cui aspiriamo; quello che ci interessa valutare sono le 28 conseguenze sulla precisione sul test set: notiamo che, nonostante alcune oscillazioni, continua ad aumentare ottenendo una soppressione dell’overfitting. In più, abbiamo che la precisione di picco di classificazione è di circa 87.1%, decisamente superiore rispetto a quella di 82,27% ottenuta nel caso non regolarizzato. Concludiamo il paragrafo informando che esiste una versione di L2 in grado di vincolare anche le biases, tuttavia non viene mai utilizzata perché non altera particolarmente i risultati; Infatti, avere un’elevata polarizzazione non rende un neurone così sensibile ai suoi ingressi come l’avere grandi pesi, anzi una “b” sufficientemente grande garantisce una maggiore flessibilità al comportamento della rete. 3.2.2 L1 Regularization Anche qui andremo a modificare la nostra funzione di costo: π πΆ = πΆ0 + ∑ |π€| 2π π€ In analogia con quanto visto con L2 definiamo la derivata parziale della funzione costo e la regola di apprendimento per i pesi: ∂C ∂C0 π = + π ππ(π€) ∂π€ ∂π€ π π ∂C0 π€ → π€ − π π ππ(π€) − π π ∂π€ Dove “π ππ(π€)” è una classica funzione segno: restituisce valore -1 se π€ < 0, altrimenti 1. In entrambe le regole di aggiornamento, sia questa che quella presentata nella regolarizzazione L2, hanno come scopo di andare a ridurre i pesi, la differenza è il modo con cui raggiungeranno il proprio obiettivo: in L2, i pesi si riducono di una quantità proporzionale alla w, in L1, invece, i pesi si riducono di una quantità costante diversa da zero; questa modalità diversa comporta anche un approccio diverso nei confronti della dimensioni dei pesi: L1 tende a restringere poco il peso se questo è particolarmente grande, viceversa se piccolo viene particolarmente ridotto; deduzione immediata dovuta al fatto che la quantità con cui viene ridotto il peso è sempre uguale indipendentemente dal valore del peso. Il comportamento di L2 è diametralmente opposto, ridurrà particolarmente i pesi molto grandi e ci andrà più leggero per i pesi molto piccoli. 29 3.2.3 Dropout La Dropout è una tecnica radicalmente diversa rispetto alle due regolarizzazioni presentate sopra, questa infatti non si basa sulla modifica della funzione di costo ma tende ad intervenire direttamente sulla rete stessa. Per fissarla meglio partiamo immediatamente con un esempio: 3 neuroni di input, 6 nascosti e 2 di uscita. Il primo passo è disabilitare la metà dei neuroni nascosti, avviare la fase di addestramento sulla rete modificata, una volta terminata ripristinare i neuroni disabilitati ed Fig. 19: Esempio di rete MLP infine sceglierne un nuovo sottoinsieme casuale da inabilitare; ripetere poi questo procedimento diverse volte. È ovvio notare che i nuovi pesi e polarizzazioni sono dovuti quindi ad una fase di training in cui la metà dei neuroni dello strato nascosto erano inabilitati, quindi in fase operativa ci ritroveremo un numero doppio di neuroni attivi rispetto a quello considerato in fase di apprendimento; per Fig. 20: Rete MLP con Neuroni disabilitati compensare questa differenza andremo a dimezzare i pesi in uscita dagli “hidden neurons”. A questo punto la domanda che ci poniamo è capire quali vantaggi porta rispetto alle due regolarizzazioni viste in precedenza. Per capire i guadagni apportati dobbiamo immaginare una serie di reti neurali aventi lo stesso numero di neuroni nello strato di input e output e dobbiamo immaginare di addestrarle tutte utilizzando lo stesso training data; naturalmente, le reti potrebbero essere inizializzate in modo diverso, dovuto ad esempio dalla generazione casuale (tramite v.a. Gaussiane) dei pesi e delle polarizzazioni, di conseguenza è possibile che queste restituiranno risultati diversi in uscita; in particolare supponiamo di avere 5 reti e supponiamo di porre in ingresso un’immagine rappresentante una cifra, supponiamo che 3 di queste restituiscono un “9” mentre le rimanenti due, cifre diverse; allora è abbastanza facile immaginare che il risultato esatto sia proprio il “9” e che le due reti rimanenti hanno effettuato una classificazione errata; abbiamo quindi scelto il risultato che presentava più consensi dalle nostre reti; l’esempio introdotto è analogo a quello che fa nella pratica la nostra Dropout, ovvero formare reti neurali differenti e poi utilizzare una sorta di media per 30 la scelta del risultato. Ogni rete verrà addestrata separatamente e quindi ognuna di esse sarà affetta da un sovradattamento diverso; questa compensazione lo ridurrà fortemente nella fase di test. Un altro effetto interessante di questa procedura è data dal fatto che questa riduce i co-adattamenti dei neuroni; un neurone, infatti, non può invocare la presenza di altri particolari elementi perché disabilitati, portandolo così ad essere più robusto. 3.2.4 Espansione artificiale di un training data Finora abbiamo sempre considerato un sottoinsieme ridotto di 1000 su 50000 immagini di apprendimento; che cosa accade, invece, se andiamo a valutare l’intero insieme di apprendimento? L’overfitting aumenterà, diminuirà o rimarrà invariato? Per verificare cosa accade, utilizziamo ancora una volta il nostro programma “overfitting.py”, tuttavia, visto che “π” va da Fig. 21: Accuracy on Test Data 1000 a 5000, per mantenere il rapporto analogo a quello precedente dobbiamo scegliere π = 5.0 . Grazie alla linea tratteggiata indicante il valore di 96% è facile vedere che, anche se di poco, abbiamo dopo ogni epoca un margine di miglioramento della precisione. Tuttavia questo grafico non riesce a rendere un’idea abbastanza chiara sul come varia la precisione in rapporto all’aumento delle immagini del training set, generiamone quindi un altro utilizzando un altro programma sempre disponibile su “github.com” [2], chiamato: “more_data.py”. Anche in questo grafico è abbastanza chiaro che la precisione di classificazione migliora notevolmente con l’aumentare dei dati di Fig. 22: Accuracy on Validation Data addestramento, allo stesso tempo però quanto più scegliamo un training set grande tanto più il margine di miglioramento è minore. Se pur poca la differenza di percentuale questa esiste e quindi logicamente se andassimo ad utilizzare milioni o miliardi di campioni rispetto ai 50000 appartenenti al MNIST database, possiamo comunque ottenere un miglioramento sostanziale. Abbiamo quindi dedotto che usare un training set sufficientemente ampio aiuta a ridurre l’overfitting; ma come possiamo 31 ottenerlo? Purtroppo questo è un problema difficile da risolvere, o perlomeno costoso, infatti non è semplice recuperare o costruire nuovi dati di allenamento. Allora l’idea a questo punto è: piuttosto che costruire campioni da zero, possiamo recuperarne di nuovi a partire da quelli esistenti. Prendiamo in considerazione una cifra appartenente al dataset MNIST, raffigurante un 5, e ruotiamola di 15 gradi, la cifra è ancora riconoscibile tuttavia a livello di pixel questi sono stati completamente alterati generando quindi un ingresso non presente nel training set. Possiamo fare tante piccole rotazioni a tutte le immagini presenti nel dataset in modo tale da quadruplicare se non di più la sua grandezza. 3.3 Scelta degli Hyper-parameters Gli iper-parametri non sono un argomento nuovo per il nostro elaborato, infatti corrispondono a: tasso di apprendimento, parametro di regolarizzazione, λ e così via; il nostro scopo, a questo punto, è capire come andare a sceglierli in modo tale che la nostra rete tenderà a comportarsi nel modo migliore; purtroppo però ancora non esiste un metodo preciso dovremmo, infatti procedere con l’introduzione di diverse strategie empiriche che possano aiutarci nell’andare a ricercarli. Andremo così ad introdurre la “Broad Strategy”, questa consiste nel procedere per esperimenti per poi scegliere quella che si è comportata meglio; per poter applicare questa strategia, per prima cosa conviene evitare l’utilizzo di un dataset completo per velocizzare i tempi, ad esempio per il caso delle cifre MNIST, conviene ridurre il training set a 1000 immagini, mentre per il set di validazione e di test scegliamo 100 immagini. Partiamo proprio con uno dei parametri più critici, il tasso di apprendimento: l’idea è quella di partire da un valore di 0.01, se il costo diminuisce già durante le prime epoche passiamo a scegliere 0.1, 1.0 e così via fino a trovare un valore per cui il costo tenda ad oscillare o addirittura aumentare; in alternativa, se tende ad oscillare già con il primo parametro scelto, si procede scegliendo 0.001, 0.0001 e così via fino a trovare un valore per cui il costo diminuisce già per le prime epoche. Tramite questa 32 procedura avremo a disposizione un modo per stimare l’ordine di grandezza del nostro tasso; a questo punto ci tocca “raffinarlo” scegliendo un valore come (0.2, 0.5 ecc); ovviamente stiamo sempre considerando una stima, quindi in nessun caso dobbiamo essere particolarmente precisi. Passiamo ora alla scelta del numero di epoche, l’abbiamo già implicitamente detto quando abbiamo parlato di overfitting, ovvero rilevare quando la precisione tende ad avere un comportamento costante, aspettare un po’ per verificare che non sia solo temporaneo e poi terminare l’apprendimento. Invece, per la scelta del parametro di regolarizzazione conviene: inizialmente impostarlo su zero, per scegliere prima un buon valore per il tasso di apprendimento, poi procedere aumentando o diminuendo di un fattore 10 in modo analogo a “η”. 33 Capitolo 4: Reti Neurali Convoluzionali Finora abbiamo utilizzato reti feed-forward in cui gli strati adiacenti sono completamente collegati tra loro; l’idea delle reti neurali convoluzionali è quello di eliminare questa seconda ipotesi per ridurre il numero di parametri da apprendere. In questo capitolo andremo prima ad introdurre le nostre ConvNet, capirne il funzionamento ed infine valuteremo come queste si comportano con il nostro problema di riconoscimento. 4.1 Idee alla base delle ConvNet Le operazioni svolte dal dalle ConvNet si basano su tre idee fondamentali: Local Receptive Fields, Shared Weight and Biases e Pooling Layers. 4.1.1 Local Receptive Fields Negli strati completamente connessi visti finora, gli ingressi erano descritti come “π” neuroni immaginati su di un unico layer, avente il compito di rappresentare univocamente l’intensità di un pixel dell’immagine; questa prima idea si basa sullo stravolgere questo concetto, dovendo andare ad immaginare i neuroni organizzati su di una superfice. Come fatto nei precedenti capitoli, creeremo un collegamento tra gli strati di input e hidden, ma a Fig. 23: Input Neurons differenza di quanto visto finora ogni neurone nascosto sarà collegato ad una sola regione di quelli di input. La regione identificata nello strato di input prende il nome di “campo recettivo locale” per il neurone nascosto. Ogni connessione apprende un peso, nel esempio in figura (24), valutando regioni 5x5 avremo 25 pesi per area; faremo scorrere 34 Fig. 24: Connessione tra lo strato di ingresso e lo strato nascosto il nostro campo recettivo per tutta l’immagine e per ogni spostamento creeremo nuove connessioni. Nell’immagine abbiamo spostato il campo recettivo locale di un pixel per volta, in realtà è possibile procedere anche con diverse lunghezze di “passo”. Procedendo in questo modo avremo uno strato di hidden composto da (28-5+1)2 = 24x24 neuroni. 4.1.2 Shared weights and biases Abbiamo quindi detto che ogni neurone nascosto avrà un totale di 25 pesi e 1 bias dovuti al campo recettivo locale; adesso piuttosto che andare a generare separatamente tutti i pesi e biases, ottenendo così un numero elevatissimo di parametri liberi, valutiamo l’idea di utilizzare gli stessi valori dei pesi e della polarizzazione per tutti i neuroni appartenenti allo strato nascosto (da cui il nome “pesi e polarizzazioni condivisi”). Quindi considerando un generico neurone nascosto in posizione (j,k), l’output sarà: 4 4 π (π + ∑ ∑ π€π,π ππ+π,π+π ) π=0 π=0 Dove “b” sarà il valore condiviso per la polarizzazione, π€π,π sarà una matrice 5x5 composta da pesi condivisi, e ππ₯,π¦ indicherà l’attivazione di ingresso alla posizione (x,y). Questo significa che tutti i neuroni appartenenti allo strato nascosto riconosceranno la stessa feature, collocata in modo diverso nell’immagine di input. Supponiamo un semplice esempio per carpirne i vantaggi: consideriamo l’immagine di un gatto, andiamo adesso ad allontanarla causando inevitabilmente la modifica delle intensità dei pixel di ingresso, la rete alla fine sarà comunque in grado di riconoscere il gatto; diremo quindi che questa godrà delle proprietà di invarianza di un’immagine a seguito di una traslazione. La mappa delle connessioni dallo strato di input a quello nascosto prende il nome di “features map”; a questo punto è ovvio immaginare che una sola caratteristica non sarà sufficiente per riconoscere un’immagine, abbiamo quindi bisogno di considerare più feature map. Nell’esempio in Fig. 25: Esempio con più feature map figura (25) abbiamo considerato 3 mappe, che per molti problemi reali sono ancora poche, infatti, le più moderne possono tranquillamente presentarne un minimo di 20 o 40. Nelle reti utilizzate finora dovevamo andare a definire 35 circa 784 ∗ 30 + 30 = 23550 parametri, adesso invece, valutando anche 20 mappe, abbiamo un totale di soli (5 ∗ 5 + 1) ∗ 20 = 520 parametri da definire; la differenza sarà quindi tutt’altro che trascurabile. Il nome “convoluzionale” deriverà proprio da questa seconda idea, perché l’ingresso pesato “z” è ottenuta come somma di convoluzione: 4 4 π (π + ∑ ∑ π€π,π ππ+π,π+π ) ↔ π1 = π(π + π€ ∗ π0 ) π=0 π=0 Dove π1 indica l’insieme di attivazione in uscita da una feature map, π0 è l’insieme di attivazione dell’input layer e l’operatore “∗” indica un’operazione di convoluzione. 4.1.3 Pooling Layers Questa terza idea prevede l’aggiunta di un nuovo strato alle nostre reti, chiamato strato di raggruppamento; tale inserimento avviene solo dopo gli strati convoluzionali e andranno a semplificare le informazioni in uscita da questi; in particolare, per ogni feature map considerata avremo una feature map Fig. 26: Operazione di Max-pooling “condensata”. In figura (26) è mostrato un esempio: ogni max-pooling unit andrà a semplificare le informazioni trasportate da 4 neuroni. 4.2 Schema di una ConvNet È l’ora di riunire le tre idee presentate nel paragrafo precedente per ottenere uno schema generale di una rete neurale convoluzionale. La rete inizia con un totale di 28x28 neuroni appartenenti allo strato di input, usati per codificare la nostra immagine. Lo strato iniziale è seguito da uno convoluzionale, ottenuto considerando una campo recettivo di grandezza 5x5 e 3 feature map; otterremo quindi uno strato composto da 3x24x24 neuroni nascosti. Il prossimo passo è quello di avviare una procedura di max-pooling applicata a regioni 2x2, ottenendo così un max-pooling layer, composto da 3x12x12 neuroni nascosti. Lo Fig. 27: Esempio di architettura per il nostro problema di riconoscimento delle cifre manoscritte strato finale che restituirà la cifra riconosciuta sarà ancora una volta uno strato completamente collegato, ovvero ogni neurone appartenente al max-pooling layer sarà 36 collegato ad ognuno dei 10 neuroni di output. Nonostante la nostra nuova tipologia di rete presenta un’architettura decisamente diversa rispetto a quelle viste finora, il quadro generale è simile, ovvero: una rete composta da unità semplici, i cui comportamenti sono determinati dai pesi e polarizzazioni ed utilizzare una serie di dati di training per permettere l’apprendimento. Fortunatamente molte cose dette per le MLP saranno adattabili alle ConvNet, basterà fare le giuste modifiche. 4.3 Test della rete Ora che abbiamo appreso le idee alla base delle ConvNet possiamo metterle in pratica per risolvere il problema del riconoscimento delle cifre scritte a mano. Il programma che andremo ad utilizzare per inizializzare la nostra rete prende il nome di “network3.py” disponibile sempre “github.com” [2]. Le reti viste finora utilizzavano solo una libreria chiamata “Numpy” per supportare le diverse operazioni matematiche; per le ConvNet, invece, verrà utilizzata una libreria specifica per l’apprendimento automatico, nota come “Theano”. Oltre al garantire una maggiore semplicità di implementazione, consente l’esecuzione del codice sia su una CPU che se, disponibile, su una GPU. I dati che andrò utilizzare sono ancora una volta prelevati dal libro online “Neural Networks and Deep Learning” [1]; i risultati, invece, sono stati generati sul mio calcolatore utilizzando proprio la GPU impostando il corrispettivo flag. Il programma “network3.py” tra le diverse funzioni ci mette a disposizione la classe: “FullyConnectedLayer()” questa serve per implementare strati completamente connessi, allora per fare anche una sorta di paragone, possiamo ottenere una linea di base, considerando una rete con soli 100 neuroni nello strato di hidden e andremo ad addestrarla con 60 epoche, 10 minibatch e con un tasso di apprendimento pari a 0.1; in figura (28) sono rappresentati i risultati ottenuti: per ogni epoca è stata restituita la percentuale di precisione sia sul validation set, che Fig. 28: Tabella di precisione sul Test Set e Validation Set sul test set. Notiamo comunque un sostanziale miglioramento rispetto ai test fatti finora, il 37 motivo (oltre al doppio delle epoche) è che network3.py utilizza per l’output un “Softmax Layer”; similmente ad un semplice strato di neuroni sigma calcola allo stesso modo il valore degli ingressi pesati, ma invece di usare la funzione sigma, ne utilizzerà una softmax, ottenuta come segue: πΏ πππΏ = π π§π πΏ ∑π π π§π Teniamo presente il risultato ottenuto, ovvero 97.86%, perché sarà interessante vedere come cambierà con l’aggiunta di uno strato convoluzionale. Manteniamo la stessa rete con i 100 neuroni sigma nascosti e gli stessi parametri usati per l’addestramento ma consideriamo l’aggiunta di uno strato convoluzionale-pooling antecedente allo strato dei neuroni nascosti (vedi figura 29). Potremmo immaginare che i primi strati si occupano di andare ad apprendere la struttura spaziale dell’immagine in ingresso, mentre lo strato completamente collegato avrà il compito di apprendere ad un livello più astratto, integrando le informazioni Fig. 29: Esempio di architettura per il nostro problema di riconoscimento delle cifre manoscritte provenienti da tutta l’immagine. Per l’implementazione di uno strato convoluzionale, network3.py ci mette a disposizione la classe “ConvPoolLayer()”, a cui passeremo i parametri della dimensione dell’immagine, del campo recettivo locale, il numero delle feature map ed infine la dimensione della regione per l’operazione di max-pooling. I risultati ottenuti sono veramente interessanti, siamo riusciti ad aumentare la precisione di un Fig. 30: Tabella di precisione sul Test Set e Validation Set altro 0.87%, quindi significa che abbiamo riconosciuto 87 immagini in rispetto al caso precedente. Una rete più profonda (in particolare con l’utilizzo degli strati convoluzionali) è in grado di ottenere risultati migliori; a questo punto ci domandiamo se è possibile “scendere” ancora di più: ovvero considerare ben due strati convoluzionali-pooling, di 38 uguali parametri, per ottenere risultati sempre migliori. Dovremmo però sciogliere un dubbio sulla connessione tra questi due, perché in uscita dal primo abbiamo ben 20 immagini 12x12 e non una Fig. 31: Tabella di precisione sul Test Set e Validation Set sola come visto finora. L’idea è quella di dare la possibilità ad ogni neurone del secondo strato di imparare da tutti i campi recettivi locali di ognuna delle 20 immagini di input, dove i pixel (dell’immagine condensata) rappresenteranno la presenza o l’assenza di particolari caratteristiche localizzate nell’immagine originale; la precisione raggiunta sarà del 99.03%. Nel capitolo 4 abbiamo introdotto diversi modi per migliorare la nostra rete, sarebbe allora interessante vedere di estenderli anche per le ConvNet. Tra i diversi metodi visti, sicuramente quello maggiormente adattabile è l’espansione del training set; in particolare userò il programma “expand_mnist.py” disponibile online [2] per estendere l’insieme MNIST senza recuperare nuovi campioni, passando quindi da 50000 a 250000 immagini di apprendimento; in più per il prossimo test considererò anche una L2 Regularization impostando λ = 0.1; Al termine dell’apprendimento potremmo godere su una percentuale di precisione del 99.22%; in realtà questo non è il risultato migliore, potremmo ottenere ulteriori piccoli miglioramenti andando a modificare opportunamente i parametri tramite la tecnica della “Broad Strategy”, aumentare la profondità aggiungendo un ulteriore strato di neuroni completamente connessi, oppure utilizzando la tecnica Dropout negli strati completamente connessi; negli strati convoluzionali l’utilizzo di questa è ininfluente perché Fig. 32: Tabella di precisione sul Test Set e Validation Set dovendo generare pesi condivisi, sono già costretti a dover imparare da tutta l’immagine, garantendo, quindi, già un ottimo livello di robustezza. 39 Conclusioni Scopo dell’elaborato era quello di introdurre diverse tecniche per risolvere artificialmente il problema del riconoscimento delle cifre scritte a mano; un nodo tutt’altro che semplice da sciogliere data la natura complessa di come la mente umana opera per effettuare un riconoscimento. Inizialmente abbiamo introdotto una soluzione più semplice tramite reti composte da neuroni sigma, ottenendo un riconoscimento corretto di 9495/10000 immagini; risultato notevole considerato essere una macchina, meno se pensiamo al numero totale di immagini riconosciute da un essere umano; allora non ci siamo accontentati: abbiamo introdotto una serie ti tecniche per “raffinare” il nostro studio, permettendo alla rete di aumentare il numero di cifre correttamente riconosciute, aumentare la velocità di apprendimento e allo stesso tempo garantire un alto grado di generalizzazione. Tuttavia, non essendo ancora soddisfatti, abbiamo cercato un modo per sfruttare la componente spaziale presente nelle immagini, introducendo così le reti neurali convoluzionali (da cui il nome dell’elaborato), tramite un lavoro combinato tra queste e quelle completamente connesse abbiamo raggiunto la soglia di 9786/10000 immagini correttamente riconosciute. L’attuale record del mondo (2013) vede una corretta classificazione di 9979/10000 immagini; questo attualmente detenuto da Li Wan, Matthew Zeiler, Sixin Zhang, Yann LeCun, e Rob Fergus, la rete quindi classifica in modo errato solo 21 immagini, e a dover essere sinceri anche io, essere umano, non sono stato in grado di riconoscerle. 40 Bibliografia [1] Michael Nielsen, “Neural Networks and Deep Learning”, http://neuralnetworksanddeeplearning.com/, 07-01-2017 [2] Michael Nielsen, “Code samples for my book Neural Networks and Deep Learning”, https://github.com/mnielsen/neural-networks-and-deep-learning, 07-01-2017 [3] Stuart Russel, Peter Norving, “Artificial Intelligence: A Modern Approach”, 3rd Edition, Prentice-Hall, 2010, pages 727 - 736 41