Il 0% ha trovato utile questo documento (0 voti)
39 visualizzazioni

Deep Learning With Python-1-237 It

Caricato da

Peppe Fetish
Copyright
© © All Rights Reserved
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Il 0% ha trovato utile questo documento (0 voti)
39 visualizzazioni

Deep Learning With Python-1-237 It

Caricato da

Peppe Fetish
Copyright
© © All Rights Reserved
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Sei sulla pagina 1/ 347

SECONDA EDIZIONE

François Chollet

MANNINA
Apprendimento profondo con Python
Apprendiment
o profondo
con Python
SECONDA EDIZIONE

FRANÇOIS CHOLLET

MANIFEST
AZIONE
ISOLA DI SHELTER
Per informazioni e ordini online di questo e altri libri di Manning, visitare il sito
www.manning.com. L'editore offre sconti su questo libro se ordinato in quantità. Per ulteriori
informazioni, contattare
Ufficio vendite speciali
Manning Publications Co.
20 Baldwin Road
Casella postale 761
Shelter Island, NY 11964
Email: [email protected]

©2021 di Manning Publications Co. Tutti i diritti riservati.

Nessuna parte di questa pubblicazione può essere riprodotta, memorizzata in un sistema di


recupero o trasmessa, in qualsiasi forma o con mezzi elettronici, meccanici, di fotocopiatura o altro,
senza previa autorizzazione scritta dell'editore.

Molte delle denominazioni utilizzate da produttori e venditori per distinguere i loro prodotti
sono rivendicate come marchi di fabbrica. Nei casi in cui tali denominazioni compaiono nel
libro e Manning Publications era a conoscenza di una rivendicazione di marchio, le
denominazioni sono state stampate in maiuscolo o tutto maiuscolo.

Riconoscendo l'importanza di preservare ciò che è stato scritto, la politica di Manning prevede che
i libri che pubblichiamo siano stampati su carta priva di acidi, e facciamo del nostro meglio a
questo scopo.
Riconoscendo anche la nostra responsabilità di preservare le risorse del nostro pianeta, i libri
Manning sono stampati su carta riciclata per almeno il 15% e lavorata senza l'uso di cloro
elementare.

L'autore e l'editore hanno fatto ogni sforzo per assicurare che le informazioni contenute in
questo libro fossero corrette al momento della stampa. L'autore e l'editore non si assumono e
declinano ogni responsabilità nei confronti di terzi per eventuali perdite, danni o disservizi
causati da errori od omissioni, siano essi dovuti a negligenza, incidente o qualsiasi altra causa, o
all'uso delle informazioni qui contenute.

Redattore sviluppo: Jennifer Stout


Redattore sviluppo tecnico: Frances Buontempo
Manning Publications Co. Editore della recensione: Aleksandar
Dragosavljevic´ 20 Baldwin Road Redattore di produzione: Keri Hales
Casella postale 761 Copy editor: Andy Carroll
Shelter Island, NY 11964 Correttori di bozze: Katie Tennant e Melody
Dolab Correttore tecnico: Karsten Strøbæk
Tipografo: Dennis Dalinnik
Disegnatore di copertina: Marija
Tudor

ISBN: 9781617296864
Stampato negli Stati Uniti d'America
A mio figlio Sylvain: spero che un giorno leggerai questo libro!
contenuti
sintetici
1 Cos'è il deep learning? 1
2 Gli elementi matematici delle reti
neurali 26
3 Introduzione a Keras e TensorFlow 68
4 Iniziare con le reti neurali: Classificazione e
regressione 95
5 Fondamenti di apprendimento automatico 121
6 Il flusso di lavoro universale dell'apprendimento automatico
153
7 Lavorare con Keras: Un'immersione profonda 172
8 Introduzione all'apprendimento
profondo per la visione
artificiale 201
9 Apprendimento profondo avanzato per la visione artificiale
238
10 Apprendimento profondo per le serie temporali 280
11 Apprendimento profondo per il testo 309
12 Apprendimento profondo generativo 364
13 Migliori pratiche per il mondo reale 412
14 Conclusioni 431
vii
contenuti
prefazione xvii
ringraziamenti xix
questo libro xx
l'autore xxiii
sull'illustrazione di copertina xxiv

1 Che cos'è l'apprendimento profondo? 1


1.1 Intelligenza artificiale, apprendimento
automatico e apprendimento profondo 2
Intelligenza artificiale 2 Apprendimento automatico
■ ■

Apprendimento di regole e rappresentazioni dai dati ■

Il "profondo" in "deep learning " ■7 Capire come


funziona il deep learning, in tre figure 8 Cosa ha realizzato

finora il deep learning 10 Non credete all'hype a breve


termine 11 La promessa dell'IA 12

1.2 Prima del deep learning: Breve storia


dell'apprendimento automatico 13
Modellazione probabilistica
13 Prime

reti neurali14
Metodi kernel 14 ■

Alberi decisionali, foreste casuali e


macchine con gradient boosting 15 Torniamo alle reti

neurali 16 Cosa rende diverso il deep learning 17 Il ■

panorama moderno dell'apprendimento automatico 18


ix
x CONTENUT
I

1.3 Perché l'apprendimento profondo? Perché ora? 20


Hardware 20 Dati
■ 21 Algoritmi 22 Una nuova ondata
■ ■

di investimenti 23 La democratizzazione del deep


learning24 Durerà? 24

2 Gli elementi matematici delle reti neurali


2.1 Un primo sguardo a una rete neurale 27
26

2.2 Rappresentazioni di dati per reti neurali 31


Scalari (tensori di rango 0) 31 Vettori (tensori
■ di
rango 1) 31 Matrici (tensori di rango 2) 32 Tensori di ■

rango 3 e superiore 32 Attributi chiave 32


■ ■

Manipolazione dei tensori in


NumPy34 La nozione di batch di dati 35
■ ■

Mondo reale
esempi di tensori di dati 35 Dati ■ 35 Timeseriesdati o

sequenze di dati 36 Dati di immagini



37 Dati video 37 ■

2.3 Gli ingranaggi delle reti neurali: Operazioni tensoriali 38


Operazioni element-wise 38 Trasmissione 40 Prodotto tensoriale 41
■ ■

Rimodellamento tensoriale 43 Interpretazione geometrica delle


operazioni tensoriali 44 Un'interpretazione geometrica del deep


learning 47
2.4 Il motore delle reti neurali: Ottimizzazione
basata sul gradiente 48
Cos'è una derivata? 49 Derivata di un'operazione tensoriale:

Il gradiente 51 ■
Discesa stocastica del gradiente 52 ■

Concatenamento di derivati: L'algoritmo di retropropagazione


55
2.5 Riprendendo il nostro primo esempio 61
Reimplementare il nostro primo esempio da zero in
TensorFlow63Esecuzione di una fase di addestramento 64 ■

Il ciclo di addestramento completo65


Valutazione del

3 modello66

Introduzione a Keras e TensorFlow 68


3.1 Cos'è TensorFlow? 69
3.2 Cos'è Keras? 69
3.3 Keras e TensorFlow: una breve storia 71
3.4 Impostazione di uno spazio di lavoro per il deep learning 71
Quaderni Jupyter: Il modo preferito per eseguire
esperimenti di deep learning 72 Usare ■

Colaboratory
73
3.5 Primi passi con TensorFlow 75
CONTENUT xi
Tensori e variabili Icostanti 76 Operazioni con i tensori: Fare

matematica in TensorFlow 78 Un secondo sguardo all'API


GradientTape 78 Un esempio end-to-end: Un classificatore


lineare in TensorFlow puro 79


xii CONTENUT
I

3.6 Anatomia di una rete neurale: Comprendere le


API di base di Keras 84
Strati: Gli elementi costitutivi del deeplearning 84 Dai ■

livelli ai modelli 87 La fase di "compilazione": Configurare


il processo di apprendimento 88 Scegliere una funzione di


perdita 90 Comprendereil metodo fit()



91 Monitorare ■

la perdita e le metriche sui dati di


validazione91 Inferenza: Utilizzare un modello dopo l'

4

addestramento93

Come iniziare con le reti neurali: Classificazione e


regressione 95
4.1 Classificare le recensioni di film: Un esempio di
classificazione binaria 97
Il dataset IMDB 97 Preparazione dei dati 98 Costruzione del
■ ■

modello99 Convalida dell'approccio


■ 102 Utilizzo ■

di un modello addestrato per generare previsioni su nuovi dati


105 Ulteriori esperimenti 105 Conclusione 106
■ ■

4.2 Classificare i telegiornali: Un esempio di


classificazione multiclasse 106
Il dataset Reuters 106 Preparazione dei dati 107 Costruzione
■ ■

del modello 108 Convalida dell'approccio 109


■ ■

Generazione di previsioni su nuovi dati111 Un ■

modo diverso di gestire le etichette e la perdita 112 ■

L'importanza di avere strati intermedisufficientementegrandi


112 Ulteriori esperimenti 113

Avvolte 113
4.3 Prevedere i prezzi delle case: Un esempio di regressione 113
Il set di dati sui prezzi delle abitazioni di Boston 114 ■

Preparazione dei dati114Costruzione del modello


115 Convalida dell'approccio utilizzando

Convalida K-fold 115 Generazione di previsioni su


nuovi dati119 Conclusione 119


5 Fondamenti di apprendimento automatico 121


5.1 Generalizzazione: L'obiettivo dell'apprendimento automatico
121
Underfitting e overfitting 122 La natura della

generalizzazione nel deep learning127


5.2 Valutazione dei modelli di apprendimento automatico133
Set di addestramento, validazione e test 133 Battere una linea di base

di buon senso 136 Cose da tenere a mente sui modelli


valutazione 137
5.3 Migliorare l'adattamento del modello 138
CONTENUT xiii
Regolazione dei Iparametri chiave della discesa del
gradiente138 Sfruttamento di migliori

priori dell'architettura 139 Aumento della


capacità del modello140


xiv CONTENUT
I

5.4 Migliorare la generalizzazione 142


Curation del dataset 142 Feature ■

engineering 143 Utilizzo dell'early stopping144


■ ■

Regolarizzazione del modello145

6 Il flusso di lavoro universale dell'apprendimento automatico


6.1 Definire il compito 155
153

Inquadrare il problema 155 Raccogliere un insieme di dati


156 Comprendere i dati 160 Scegliere una misura del


■ ■

successo 160
6.2 Sviluppare un modello 161
Preparare i dati 161 Scegliere un protocollo di

valutazione162 Battere una linea di base


163 Scalare: Sviluppare un modello che si adatta

eccessivamente 164 Regolarizzare e mettere a punto il


modello165
6.3 Distribuire il modello 165
Spiegare il proprio lavoro agli stakeholder e definire le
aspettative 165 Spedire un modello di inferenza 166 ■

Monitorare il modello in natura 169 Mantenere il ■

modello 170

7 Lavorare con Keras: Un'immersione profonda


7.1 Uno spettro di flussi di lavoro 173
172

7.2 Diversi modi per costruire i modelli Keras 173


Il modello sequenziale 174 L'API funzionale 176 Sottoclasse

della classe Model 182 Mescolare e abbinare diversi


componenti 184 Ricordare: Usare lo strumento giusto per il


lavoro185
7.3 Utilizzo dei cicli di formazione e valutazione integrati 185
Scrivere le proprie metriche186 Usare i callback 187 ■

Scrivere le proprie callback 189 Monitoraggio e■

visualizzazione con TensorBoard190


7.4 Scrivere i propri cicli di formazione e valutazione 192
Addestramento e inferenza 194 Uso a basso livello delle

metriche 195 Un ciclo completo di


addestramento e valutazione195 Rendere veloce con

tf. function197 Sfruttare fit() con un ciclo di addestramento


personalizzato 198

8 Introduzione all'apprendimento profondo per la visione artificiale


201
8.1 Introduzione alle reti convettive 202
L'operazione di convoluzione 204 ■
CONTENUT xv
I
L'operazione di max-pooling 209
8.2 Addestramento di una convnet da zero su un piccolo set di dati
211
L'importanza del deep learning per i problemi di piccoli
dati 212Scaricare i dati 212 Costruire il

modello215Preelaborazione dei dati


217 Usare l'

incremento dei dati221


xvi CONTENUT
I

8.3 Sfruttare un modello preaddestrato 224


Estrazione di caratteristiche con un modello preaddestrato
225 Messa a punto di un modello preaddestrato 234

9 Apprendimento profondo avanzato per la visione artificiale


9.1 Tre compiti essenziali di computer vision 238
238

9.2 Un esempio di segmentazione delle immagini 240


9.3 Modelli di architettura convnet moderna 248
Modularità, gerarchia e riutilizzo 249 Connessioni residue 251

Normalizzazione dei lotti 255 ■Convoluzioni separabili in


profondità 257 Mettere insieme il tutto: Un mini modello simile a
Xception 259
9.4 Interpretare l'apprendimento delle reti convettive 261
Visualizzazione delle attivazioni intermedie 262 ■

Visualizzazione dei filtri convnet 268 Visualizzazione delle


heatmap dell'attivazione delle classi 273

10 Apprendimento profondo per le serie temporali 280


10.1 Diversi tipi di compiti per le serie temporali 280
10.2 Un esempio di previsione della temperatura 281
Preparazione dei dati 285 Una linea di base di buon senso, non

basata sull'apprendimento automatico 288 Proviamo un


modello di apprendimento automatico


di base289Proviamo un modello convoluzionale 1D 290 ■

Una prima linea di base ricorrente 292


10.3 Comprendere le reti neurali ricorrenti 293
Uno strato ricorrente in Keras 296
10.4 Uso avanzato delle reti neurali ricorrenti 300
Usare il dropout ricorrente per combattere l'overfitting 300 ■

Impilare strati ricorrenti 303 Usare RNN bidirezionali 304


Andare oltre 307

11 Apprendimento profondo per il testo


11.1
309
Elaborazione del linguaggio naturale: La visione a volo d'uccello
309
11.2 Preparazione dei dati di testo 311
Standardizzazione del testo
312 Suddivisione del testo

(tokenizzazione) 313Indicizzazione del vocabolario


314 Utilizzo della TextVectorization

strato 316
11.3 Due approcci per rappresentare gruppi di parole:
Insiemi e sequenze 319
Preparazione dei dati delle recensioni di film IMDB 320 ■
CONTENUT xvii
I
Elaborazione delle parole come insieme: L'approccio bag-of-
words 322 Elaborare le parole come sequenza: L'approccio del

modello di sequenza 327


xviii CONTENUT
I

11.4 L'architettura del trasformatore 336


Comprendere l'autoattenzione 337 ■

L'attenzione a più teste341 Il codificatore Transformer 342 ■

Quando usare i modelli di sequenza rispetto ai modelli a sacchi di


parole 349
11.5 Oltre la classificazione del testo: Apprendimento
da sequenza a sequenza 350
Un esempio di traduzione automatica 351 Apprendimento

sequenza-sequenza con RNN 354 Apprendimento sequenza-


sequenza con Transformer 358

12 Apprendimento profondo generativo


12.1 Generazione di testo 366
364

Breve storia del deep learning generativo per la generazione


di sequenze366 Come si generano i dati

delle sequenze? 367 L'importanza


della strategiadi campionamento 368 ■

Implementazione della generazione di testo con Keras


369 Un callback per la generazione di testo

con campionamento a temperatura variabile372 Conclusione 376 ■

12.2 DeepDream 376


Implementazione di DeepDream in Keras 377 Conclusione 383 ■

12.3 Trasferimento di stile neurale 383


La perdita di contenuto 384 La perdita di stile384
■ ■

Trasferimento neurale di stile in Keras 385 Conclusione 391


12.4 Generazione di immagini con autoencoder variazionali 391


Campionamento da spazi latenti di immagini 391 Vettori ■

concettualiper l'editing di immagini 393 ■

Autoencoder variazionali393
Implementazione di un VAE con Keras
396 Conclusione 401

12.5 Introduzione alle reti generative avversarie 401


Un'implementazione schematica di GAN402 Un sacchetto di ■

trucchi 403 Mettere le mani sul dataset CelebA 404


Il discriminatore 405 Il generatore 407 La rete


■ ■

avversaria408 Conclusione410

13 Le migliori pratiche per il mondo reale


13.1 Ottenere il massimo dai modelli
Ottimizzazione degli
412
413
iperparametri413 Assemblaggio di modelli
■ 420
13.2 Scalare la formazione dei modelli 421
Accelerazione dell'addestramento su GPU con
precisione mista422Addestramento multi-
CONTENUT xix
GPU I 425■ Addestramento TPU428
xx CONTENUT
I

14 Conclusioni 431
14.1 Concetti chiave in rassegna 432
Vari approcci all'IA 432 Cosa rende speciale il deep learningnel

campo dell'apprendimento automatico Come pensare


al deep learning Le principali tecnologie


abilitanti 434 Il flusso di lavoro universale dell'apprendimento


automatico ■
Le principali architetture di rete 436 Lo ■

spazio delle possibilità440


14.2 I limiti del deep learning 442
Il rischio di antropomorfizzare i modelli di
apprendimento automatico443 Automi
vs. agenti intelligenti 445 Generalizzazione localevs.

generalizzazione estrema 446 Lo scopo ■

dell'intelligenza 448 Scalare lo spettro della generalizzazione449


14.3 Impostare la rotta verso una maggiore generalità nell'IA 450
Sull'importanza di fissare il giusto obiettivo: La regola della
scorciatoia 450 Un nuovo obiettivo 452

14.4 Implementare l'intelligenza: Gli ingredienti mancanti 454


L'intelligenza come sensibilità alle analogie astratte454 I ■

duepoli dell'astrazione 455 La metà


mancante del quadro 458


14.5 Il futuro del deep learning 459
Modelli come programmi 460 Fondere insieme apprendimento

profondo e sintesi di programmi 461 Apprendimento continuo


e riutilizzo modulare di subroutine La ■

visione a lungo termine465


14.6 Rimanere aggiornati in un settore in rapida evoluzione 466
Esercitarsi su problemi reali utilizzando Kaggle 466 ■

Leggeregli ultimi sviluppi su arXiv Esplorare


l'ecosistema Keras 467


14.7 Parole finali 467

indice 469
CONTENUT xxi
I
prefazione
Se avete preso in mano questo libro, probabilmente siete consapevoli degli
straordinari progressi che il deep learning ha rappresentato per il campo
dell'intelligenza artificiale nel recente passato. Siamo passati da una visione
computerizzata e da un'elaborazione del linguaggio naturale quasi inutilizzabili a
sistemi altamente performanti implementati su scala nei prodotti che utilizzate ogni
giorno. Le conseguenze di questo improvviso progresso si estendono a quasi tutti i
settori. Stiamo già applicando l'apprendimento profondo a un'incredibile gamma di
problemi importanti in ambiti diversi come l'imaging medico, l'agricoltura, la guida
autonoma, l'istruzione, la prevenzione di disastri e la produzione.
Tuttavia, credo che il deep learning sia ancora agli albori. Finora ha realizzato solo
una piccola parte del suo potenziale. Con il tempo, si farà strada in tutti i problemi in cui
può essere utile, una trasformazione che avverrà nell'arco di diversi decenni.
Per iniziare a distribuire la tecnologia di deep learning a tutti i problemi che
potrebbe risolvere, dobbiamo renderla accessibile al maggior numero possibile di
persone, compresi i non esperti - persone che non sono ricercatori o studenti laureati.
Affinché l'apprendimento profondo raggiunga il suo pieno potenziale, dobbiamo
democratizzarlo radicalmente. Oggi credo che ci troviamo al culmine di una
transizione storica, in cui l'apprendimento profondo sta uscendo dai laboratori
accademici e dai dipartimenti di ricerca e sviluppo delle grandi aziende tecnologiche
per diventare una parte onnipresente della cassetta degli attrezzi di ogni
sviluppatore, non diversamente dalla traiettoria dello sviluppo web alla fine degli
anni Novanta. Oggi quasi tutti possono creare un sito web o un'applicazione web per
la propria azienda o comunità, di un tipo che nel 1998 avrebbe richiesto un piccolo
team di ingegneri specializzati. In un futuro non così lontano, chiunque abbia un'idea
e competenze di base di codifica sarà in grado di costruire applicazioni intelligenti
che imparano dai dati.
xvii
xviii PREFAZI
ONE

Quando ho rilasciato la prima versione del framework di deep learning Keras nel
marzo 2015, la democratizzazione dell'IA non era quello che avevo in mente.
Facevo ricerca sull'apprendimento automatico da diversi anni e avevo costruito
Keras per aiutarmi con i miei esperimenti. Ma dal 2015, centinaia di migliaia di
nuovi arrivati sono entrati nel campo del deep learning; molti di loro hanno scelto
Keras come strumento di elezione. Mentre osservavo decine di persone intelligenti
utilizzare Keras in modi inaspettati e potenti, ho iniziato a preoccuparmi
profondamente dell'accessibilità e della democratizzazione dell'IA. Mi sono reso
conto che più diffondiamo queste tecnologie, più diventano utili e preziose. L'accessibilità
è diventata presto un obiettivo esplicito nello sviluppo di Keras e, in pochi anni, la
comunità di sviluppatori di Keras ha raggiunto risultati fantastici su questo fronte.
Abbiamo messo il deep learning nelle mani di centinaia di migliaia di persone, che a
loro volta lo utilizzano per risolvere problemi che fino a poco tempo fa erano
ritenuti irrisolvibili.
Il libro che avete in mano è un altro passo avanti per rendere il deep learning
accessibile al maggior numero di persone possibile. Keras ha sempre avuto bisogno di
un corso di accompagnamento che coprisse contemporaneamente i fondamenti del deep
learning, le migliori pratiche di deep learning e i modelli di utilizzo di Keras. Nel 2016 e
nel 2017 ho fatto del mio meglio per produrre un corso di questo tipo, che è diventato la
prima edizione di questo libro, pubblicato nel dicembre 2017. È diventato rapidamente
un best seller sull'apprendimento automatico che ha venduto oltre 50.000 copie ed è
stato tradotto in 12 lingue.
Tuttavia, il campo del deep learning avanza rapidamente. Dalla pubblicazione
della prima edizione, si sono verificati molti sviluppi importanti: il rilascio di
TensorFlow 2, la crescente popolarità dell'architettura Transformer e altro ancora.
Così, alla fine del 2019, ho deciso di aggiornare il mio libro. Inizialmente pensavo,
piuttosto ingenuamente, che avrebbe presentato circa il 50% di nuovi contenuti e
che avrebbe finito per essere più o meno della stessa lunghezza della prima
edizione. In pratica, dopo due anni di lavoro, è risultato più lungo di un terzo, con
circa il 75% di contenuti nuovi. Più che un aggiornamento, è un libro
completamente nuovo.
L'ho scritto con l'obiettivo di rendere i concetti alla base del deep learning e la
loro implementazione il più possibile accessibili. Per farlo, non ho dovuto ridurre
nulla al minimo: credo fermamente che non ci siano idee difficili
nell'apprendimento profondo. Spero che questo libro vi sia utile e che vi consenta di
iniziare a costruire applicazioni intelligenti e di risolvere i problemi che vi stanno a
cuore.
Riconoscimenti
Prima di tutto, vorrei ringraziare la comunità di Keras per aver reso possibile questo
libro. Negli ultimi sei anni, Keras è cresciuto fino a contare centinaia di
collaboratori open source e oltre un milione di utenti. I vostri contributi e feedback
hanno trasformato Keras in quello che è oggi.
Su una nota più personale, vorrei ringraziare mia moglie per il suo infinito
supporto durante lo sviluppo di Keras e la stesura di questo libro.
Vorrei anche ringraziare Google per aver sostenuto il progetto Keras. È stato
fantastico vedere Keras adottato come API di alto livello di TensorFlow.
Un'integrazione fluida tra Keras e TensorFlow è di grande beneficio sia per gli utenti di
TensorFlow che per quelli di Keras e rende il deep learning accessibile a tutti.
Voglio ringraziare le persone di Manning che hanno reso possibile questo libro:
l'editore Marjan Bace e tutti i membri del team editoriale e di produzione, tra cui
Michael Stephens, Jennifer Stout, Aleksandar Dragosavljevic´ e molti altri che
hanno lavorato dietro le quinte.
Un sentito ringraziamento va ai revisori tecnici: Billy O'Callaghan, Christian
Weisstanner, Conrad Taylor, Daniela Zapata Riesco, David Jacobs, Edmon Begoli,
Edmund Ronald PhD, Hao Liu, Jared Duncan, Kee Nam, Ken Fricklas, Kjell Jansson,
Milan Šarenac, Nguyen Cao, Nikos Kanakaris, Oliver Korten, Raushan Jha, Sayak Paul,
Sergio Govoni, Shashank Polasa, Todd Cook e Viton Vitanis e tutte le altre persone che
ci hanno inviato un feedback sulla bozza d e l libro.
Dal punto di vista tecnico, un ringraziamento particolare va a Frances
Buontempo, che ha svolto il ruolo di redattore tecnico del libro, e a Karsten
Strøbæk, che ha svolto il ruolo di correttore tecnico del libro.

xix
Informazioni su
questo libro
Questo libro è stato scritto per tutti coloro che desiderano esplorare l'apprendimento
profondo da zero o ampliare la loro comprensione del deep learning. Che siate
ingegneri di machine learning, sviluppatori di software o studenti universitari,
queste pagine vi saranno utili.
Esplorerete l'apprendimento profondo in modo accessibile, iniziando in modo
semplice per poi arrivare a tecniche all'avanguardia. Troverete in questo libro un
equilibrio tra intuizione, teoria e pratica. Evita la notazione matematica, preferendo
spiegare le idee fondamentali dell'apprendimento automatico e dell'apprendimento
profondo attraverso frammenti di codice dettagliati e modelli mentali intuitivi.
Imparerete grazie ad abbondanti esempi di codice che includono ampi commenti,
consigli pratici e semplici spiegazioni di alto livello di tutto ciò che è necessario sapere
per iniziare a utilizzare l'apprendimento profondo per risolvere i problemi più
comuni.
Gli esempi di codice utilizzano il framework di deep learning Python Keras e
TensorFlow 2 c o m e motore numerico. Essi dimostrano le moderne best practice di
Keras e TensorFlow 2 al 2021.
Dopo aver letto questo libro, avrete una solida conoscenza di cosa sia il deep
learning, di quando sia applicabile e di quali siano i suoi limiti. Conoscerete il flusso
di lavoro standard per affrontare e risolvere i problemi di apprendimento automatico
e saprete come affrontare i problemi più comuni. Sarete in grado di utilizzare Keras
per affrontare problemi reali che spaziano dalla computer vision all'elaborazione del
linguaggio naturale: classificazione di immagini, segmentazione di immagini,
previsione di serie temporali, classificazione di testi, traduzione automatica,
generazione di testi e altro ancora.
xx
SU QUESTO LIBRO xxi

Chi dovrebbe leggere questo libro


Questo libro è stato scritto per le persone con esperienza di programmazione in
Python che vogliono iniziare a lavorare con l'apprendimento automatico e
l'apprendimento profondo. Ma questo libro può essere utile anche a molti altri tipi di
lettori:
◾ Se siete scienziati dei dati che hanno familiarità con l'apprendimento automatico,
questo libro vi fornirà un'introduzione solida e pratica al deep learning, il
sottocampo in più rapida crescita e più significativo dell'apprendimento
automatico.
◾ Se siete ricercatori o professionisti del deep learning e volete iniziare a
lavorare con il framework Keras, questo libro è il corso intensivo ideale su
Keras.
◾ Se state studiando l'apprendimento profondo in un contesto formale, troverete
questo libro un complemento pratico alla vostra formazione, che v i aiuterà a
costruire un'intuizione sul comportamento delle reti neurali profonde e a
familiarizzare con le migliori pratiche chiave.
Anche le persone con una mentalità tecnica, che non utilizzano regolarmente il
codice, troveranno questo libro utile come introduzione ai concetti di base e
avanzati dell'apprendimento profondo.
Per comprendere gli esempi di codice, è necessario avere una discreta
conoscenza di Python. Inoltre, la familiarità con la libreria NumPy sarà utile, anche
se non necessaria. Non è necessaria un'esperienza precedente con l'apprendimento
automatico o il deep learning: questo libro copre, da zero, tutte le basi necessarie.
Non è nemmeno necessario avere un background matematico avanzato: la
matematica di alto livello dovrebbe essere sufficiente per seguire le lezioni.

Informazioni sul codice


Questo libro contiene molti esempi di codice sorgente sia in elenchi numerati che i n
linea con il testo normale. In entrambi i casi, il codice sorgente è formattato con un
carattere a larghezza fissa come questo per separarlo dal testo normale.
In molti casi, il codice sorgente originale è stato riformattato; abbiamo aggiunto
interruzioni di riga e rielaborato l'indentazione per adattarlo allo spazio disponibile
nella pagina del libro. Inoltre, i commenti al codice sorgente sono stati spesso
rimossi dai listati quando il codice è descritto nel testo. Le annotazioni sul codice
accompagnano molti dei listati, evidenziando i concetti importanti.
Tutti gli esempi di codice contenuti in questo libro sono disponibili sul sito web
di Manning all'indirizzo https:// www.manning.com/books/deep-learning-with-
python-second-edition e come notebook Jupyter su GitHub all'indirizzo
https://github.com/fchollet/deep-learning-with-python- notebooks. Possono essere
eseguiti direttamente nel browser tramite Google Colaboratory, un ambiente di
notebook Jupyter ospitato e utilizzabile gratuitamente. Una connessione a Internet e
un browser web desktop sono tutto ciò che serve per iniziare con il deep learning.

Forum di discussione liveBook


L'acquisto di Deep Learning with Python, seconda edizione, include l'accesso gratuito a
un forum web privato gestito da Manning Publications, dove è possibile fare commenti
sul libro.
xxii INFORMAZIONI SU
QUESTO LIBRO

libro, porre domande tecniche e ricevere aiuto dall'autore e da altri utenti. Per
accedere al forum, visitare https://livebook.manning.com/#!/book/deep-learning-
with-python-second-edition/discussion. Per saperne di più sui forum di Manning e
sulle regole di condotta, visitare https://livebook.manning.com/#!/discussion.
L'impegno di Manning nei confronti dei lettori è quello di fornire un luogo in cui
possa svolgersi un dialogo significativo tra i singoli lettori e tra i lettori e l'autore.
Non si tratta di un impegno a una partecipazione specifica da parte dell'autore, il cui
contributo al forum rimane volontario (e non retribuito). Vi suggeriamo di provare a
porre all'autore qualche domanda impegnativa, per evitare che il suo interesse si
allontani! Il forum e l'archivio delle discussioni precedenti saranno accessibili dal
sito dell'editore finché il libro sarà in stampa.
sull'autore
FRANÇOIS CHOLLET è il creatore di Keras, uno dei
framework di deep learning più utilizzati. Attualmente è
ingegnere soft ware presso Google, dove dirige il team Keras.
Inoltre, si occupa di ricerca sull'astrazione, sul ragionamento e
su come ottenere una maggiore generalità nell'intelligenza
artificiale.

xxiii
sull'illustrazione di copertina
La figura sulla copertina di Deep Learning with Python, seconda edizione, è intitolata
"Abito di una dama persiana nel 1568". L'illustrazione è tratta da A Collection of the
Dresses of Different Nations, Ancient and Modern (quattro volumi) di Thomas Jefferys,
Londra, pubblicato tra il 1757 e il 1772. Sul frontespizio si legge che si tratta di incisioni
su rame colorate a mano e arricchite con gomma arabica.
Thomas Jefferys (1719-1771) era chiamato "Geografo di Re Giorgio III". È stato un
cartografo inglese che ha rappresentato il principale fornitore di mappe del suo
tempo. Incise e stampò mappe per il governo e altri enti ufficiali e produsse
un'ampia gamma di mappe commerciali e atlanti, soprattutto del Nord America. Il suo
lavoro di cartografo suscitò l'interesse per le usanze locali delle terre da lui rilevate e
mappate, che sono brillantemente esposte in questa collezione. Il fascino per le terre
lontane e i viaggi di piacere erano fenomeni relativamente nuovi alla fine del XVIII
secolo e collezioni come questa erano molto popolari, in quanto introducevano sia il
turista che il viaggiatore in poltrona agli abitanti di altri Paesi.
La diversità dei disegni nei volumi di Jefferys parla in modo vivido dell'unicità e
dell'individualità delle nazioni del mondo circa 200 anni fa. Da allora i codici di
abbigliamento sono cambiati e la diversità per regione e paese, così ricca all'epoca, è
svanita. Oggi è spesso difficile distinguere gli abitanti di un continente da quelli di un
altro. Forse, cercando di vederla in modo ottimistico, abbiamo barattato una diversità
culturale e visiva con una vita personale più varia o una vita intellettuale e tecnica più
varia e interessante.
In un'epoca in cui è difficile distinguere un libro di informatica da un altro, Manning
mette in risalto l'inventiva e l'iniziativa del settore informatico con copertine di libri
basate sulla ricca diversità della vita regionale di due secoli fa, riportata in vita dalle
immagini di Jefferys.
xxiv
Che cos'è
l'apprendimento
profondo?

Questo capitolo tratta


◾ Definizioni di alto livello dei concetti fondamentali
◾ Cronologia dello sviluppo dell'apprendimento
automatico
◾ Fattori chiave dietro la crescente
popolarità e il potenziale futuro del deep
learning

Negli ultimi anni, l'intelligenza artificiale (AI) è stata oggetto di un intenso clamore
mediatico. L'apprendimento automatico, l'apprendimento profondo e l'IA vengono
citati in innumerevoli articoli, spesso al di fuori di pubblicazioni dedicate alla
tecnologia. Ci viene promesso un futuro di chatbot intelligenti, auto a guida
autonoma e assistenti virtuali, un futuro a volte dipinto in una luce cupa e altre volte
come utopico, in cui i posti di lavoro umani scarseggeranno e la maggior parte delle
attività economiche sarà gestita da robot o agenti AI. Per un futuro o un attuale
praticante dell'apprendimento automatico, è importante essere in grado di riconoscere
il segnale in mezzo al rumore, in modo da poter distinguere gli sviluppi che
cambieranno il mondo dai comunicati stampa esagerati. Il nostro futuro è in gioco, ed
è un futuro in cui avete un ruolo attivo: dopo aver letto questo libro, sarete uno di
coloro che svilupperanno i sistemi di intelligenza artificiale. Affrontiamo quindi
queste domande: Che cosa ha raggiunto finora il deep learning? Quanto è
significativo? Dove siamo diretti? È il caso di credere al clamore?
Questo capitolo fornisce un contesto essenziale sull'intelligenza artificiale,
l'apprendimento automatico e l'apprendimento profondo.

1
2 CAPITOLO 1 Cos'è il deep learning?

1.1 Intelligenza artificiale, apprendimento


automatico e apprendimento
profondo
In primo luogo, dobbiamo definire chiaramente di cosa stiamo parlando quando
parliamo di IA. Cosa sono l'intelligenza artificiale, l'apprendimento automatico e
l'apprendimento profondo (vedi figura 1.1)? Come si relazionano tra loro?

Artificiale
intelligenza

Apprend
imento
automat
ico
Profon
do
apprend
imento
Figura 1.1 Intelligenza artificiale,
apprendimento automatico e
apprendimento profondo

1.1.1 Intelligenza artificiale


L'intelligenza artificiale è nata negli anni Cinquanta, quando un manipolo di pionieri del
nascente campo dell'informatica ha iniziato a chiedersi se fosse possibile far "pensare" i
computer - una domanda le cui ramificazioni sono ancora oggi oggetto di studio.
Sebbene molte delle idee di fondo fossero già state elaborate negli anni e persino nei
decenni precedenti, l'"intelligenza artificiale" si è infine cristallizzata come campo di
ricerca nel 1956, quando John McCarthy, allora giovane assistente di matematica al
Dartmouth College, organizzò un workshop estivo con la seguente proposta:

Lo studio procederà sulla base della congettura che ogni aspetto dell'apprendimento o di
qualsiasi altra caratteristica dell'intelligenza possa essere descritto in linea di principio
in modo così preciso da poter essere simulato da una macchina. Si cercherà di capire
come far sì che le macchine utilizzino il linguaggio, formino astrazioni e concetti,
risolvano tipi di problemi ora riservati agli esseri umani e migliorino se stesse.
Riteniamo che un gruppo di scienziati accuratamente selezionati possa compiere un
progresso significativo in uno o più di questi problemi lavorando insieme per un'estate.

Alla fine dell'estate, il workshop si concluse senza aver risolto completamente l'enigma
che si era proposto di indagare. Ciononostante, vi parteciparono molte persone che
sarebbero diventate pionieri del settore e mise in moto una rivoluzione intellettuale che
dura ancora oggi.
In sintesi, l'IA può essere descritta come lo sforzo di automatizzare i compiti intellettuali
normalmente svolti dagli esseri umani. In quanto tale, l'IA è un campo generale che
comprende l'apprendimento automatico e l'apprendimento profondo, ma che include
anche molti altri approcci che potrebbero non comportare alcun apprendimento. Si
consideri che fino agli anni '80, la maggior parte dei libri di testo sull'IA non
Intelligenza artificiale, apprendimento automatico e 3
apprendimento
menzionava affatto il termineprofondo
"apprendimento".
4 CAPITOLO 1 Cos'è il deep learning?

tutti! I primi programmi di scacchi, per esempio, prevedevano solo regole codificate da
professionisti e non si qualificavano come apprendimento automatico. In realtà, per
molto tempo, la maggior parte degli esperti ha creduto che l'intelligenza artificiale di
livello umano potesse essere raggiunta facendo sì che i programmatori realizzassero a
mano un insieme sufficientemente ampio di regole esplicite per manipolare la
conoscenza memorizzata in database espliciti. Questo approccio è noto come IA
simbolica. È stato il paradigma dominante nell'IA dagli anni Cinquanta alla fine degli
anni Ottanta e ha raggiunto la massima popolarità durante il boom dei sistemi esperti
degli anni Ottanta.
Sebbene l'IA simbolica si sia dimostrata adatta a risolvere problemi logici e ben
definiti, come il gioco degli scacchi, si è rivelata intrattabile nel trovare regole esplicite
per risolvere problemi più complessi e confusi, come la classificazione delle immagini,
il riconoscimento del parlato o la traduzione del linguaggio naturale. Un nuovo
approccio è nato per prendere il posto dell'IA simbolica: l'apprendimento automatico.

1.1.2 Apprendimento automatico


Nell'Inghilterra vittoriana, Lady Ada Lovelace era amica e collaboratrice di Charles
Babbage, l'inventore della Macchina Analitica: il primo computer meccanico di uso
generale conosciuto. Benché visionaria e in anticipo sui tempi, la macchina analitica
non era stata concepita come un computer di uso generale quando fu progettata
negli anni Trenta e Quaranta dell'Ottocento, perché il concetto di calcolo di uso
generale non era ancora stato inventato. Si trattava semplicemente di un modo per
utilizzare le operazioni meccaniche per automatizzare alcuni calcoli nel campo
dell'analisi matematica, da cui il nome Analyti- cal Engine. In quanto tale, era il
discendente intellettuale di precedenti tentativi di codificare le operazioni
matematiche sotto forma di ingranaggi, come la Pascalina o il calcolatore di Leibniz,
una versione raffinata della Pascalina. Progettata da Blaise Pascal nel 1642 (all'età di 19
anni!), la Pascaline fu la prima calcolatrice meccanica al mondo: poteva sommare,
sottrarre, moltiplicare e persino dividere cifre.
Nel 1843, Ada Lovelace commentò l'invenzione della macchina analitica,

Il Motore Analitico non ha alcuna pretesa di dare origine a qualcosa. Può fare tutto
ciò che noi sappiamo ordinargli di fare.
disponibile ciò che già conosciamo.

Anche con 178 anni di prospettiva storica, l'osservazione di Lady Lovelace rimane
sorprendente. Un computer di uso generale potrebbe "dare origine" a qualcosa, o
sarebbe sempre destinato a eseguire in modo noioso i processi che gli esseri umani
comprendono appieno? Potrebbe mai essere capace di un pensiero originale? Potrebbe
imparare dall'esperienza? Potrebbe mostrare creatività?
La sua osservazione fu in seguito citata dal pioniere dell'IA Alan Turing come
"l'obiettivo di Lady Lovelace" nel suo storico articolo del 1950 "Computing Machinery
and Intelligence",1 che introdusse il test di Turing e i concetti chiave che avrebbero dato
forma all'IA.2 Turing

1
A.M. Turing, "Macchine da calcolo e intelligenza", Mind 59, no. 236 (1950): 433-460.
Intelligenza artificiale, apprendimento automatico e 5
2 Sebbene il test di Turingapprendimento profondo
sia stato talvolta interpretato come un test letterale - un obiettivo che il campo dell'IA
dovrebbe prefiggersi di raggiungere - Turing lo intendeva semplicemente come un dispositivo concettuale in
una discussione filosofica sulla natura della cognizione.
6 CAPITOLO 1 Cos'è il deep learning?

era dell'opinione - all'epoca molto provocatoria - che i computer potessero in linea


di principio essere in grado di emulare tutti gli aspetti dell'intelligenza umana.
Il modo abituale per far svolgere un lavoro utile a un computer è che un
programmatore umano scriva delle regole - un programma per computer - da
seguire per trasformare i dati in ingresso in risposte adeguate, proprio come Lady
Lovelace che scrive le istruzioni passo-passo da eseguire per il Motore Analitico.
L'apprendimento automatico ribalta la situazione: la macchina osserva i dati in
ingresso e le risposte corrispondenti e capisce quali dovrebbero essere le regole
(vedi figura 1.2). Un sistema di apprendimento automatico viene addestrato
piuttosto che programmato esplicitamente. Gli vengono presentati molti esempi pertinenti
a un compito e trova in questi esempi una struttura statistica che alla fine gli consente di
elaborare regole per automatizzare il compito. Per esempio, se si volesse
automatizzare il compito di etichettare le foto delle vacanze, si potrebbe presentare a
un sistema di apprendimento automatico molti esempi di foto già etichettate dagli
esseri umani, e il sistema imparerebbe regole statistiche per associare immagini
specifiche a tag specifici.

Dati Programmazio
Risposte
sulle ne classica

rego
le

Risp Apprend
Regole Figura 1.2 Apprendimento
oste ai imento
automat
automatico: un nuovo
dati ico paradigma di programmazione

Sebbene l'apprendimento automatico abbia iniziato a fiorire solo negli anni '90, è
diventato rapidamente il sottocampo più popolare e di maggior successo dell'IA, una
tendenza guidata dalla disponibilità di hardware più veloce e di set di dati più grandi.
L'apprendimento automatico è correlato alla statistica matematica, ma si differenzia
dalla statistica per diversi aspetti importanti, nello stesso senso in cui la medicina è
correlata alla chimica ma non può essere ridotta a quest'ultima, poiché la medicina si
occupa di sistemi distinti con proprietà distinte. A differenza della statistica,
l'apprendimento automatico tende a trattare insiemi di dati grandi e complessi (come un
insieme di milioni di immagini, ciascuna composta da decine di migliaia di pixel) per i
quali l'analisi statistica classica, come l'analisi bayesiana, non sarebbe praticabile. Di
conseguenza, l'apprendimento automatico, e in particolare l'apprendimento profondo,
presenta relativamente poca teoria matematica, forse troppo poca, ed è
fondamentalmente una disciplina ingegneristica. A differenza della fisica teorica o della
matematica, l'apprendimento automatico è un campo molto pratico, guidato da risultati
empirici e profondamente dipendente dai progressi del software e dell'hardware.

1.1.3 Apprendere regole e rappresentazioni dai dati


Intelligenza artificiale, apprendimento automatico e 7
apprendimento profondo

Per definire il deep learning e capire la differenza tra l'apprendimento profondo e altri
approcci di apprendimento automatico, è necessario innanzitutto avere un'idea di
cosa fanno gli algoritmi di apprendimento automatico. Abbiamo appena affermato
che l'apprendimento automatico scopre le regole per l'esecuzione di un
8 CAPITOLO 1 Cos'è il deep learning?

attività di elaborazione dei dati, con esempi di ciò che ci si aspetta. Quindi, per fare
apprendimento automatico, abbiamo bisogno di tre cose:
◾ Punti di dati in ingresso: ad esempio, se il compito è il riconoscimento
vocale, questi punti di dati possono essere file audio di persone che parlano.
Se il compito è il tagging di immagini, potrebbero essere immagini.
◾ Esempi di output atteso - In un compito di riconoscimento vocale, questi
potrebbero essere trascrizioni generate dall'uomo di file audio. In un compito di
immagine, gli output attesi potrebbero essere tag come "cane", "gatto" e così via.
◾ Un modo per misurare se l'algoritmo sta facendo un buon lavoro - È necessario
per determinare la distanza tra l'uscita attuale dell'algoritmo e l'uscita prevista. La
misurazione viene utilizzata come segnale di feedback per regolare il
funzionamento dell'algoritmo. Questa fase di regolazione si chiama
apprendimento.
Un modello di apprendimento automatico trasforma i dati in ingresso in output
significativi, un processo che viene "appreso" grazie all'esposizione a esempi noti di
input e output. Il problema centrale dell'apprendimento automatico e
dell'apprendimento profondo è quindi quello di trasformare i dati in modo
significativo: in altre parole, imparare rappresentazioni utili dei dati in ingresso,
rappresentazioni che ci avvicinino all'output previsto.
Prima di andare avanti: cos'è una rappresentazione? Si tratta di un modo diverso di
guardare i dati, di rappresentarli o di codificarli. Per esempio, un'immagine a colori
può essere codificata nel formato RGB (rosso-verde-blu) o nel formato HSV (tinta-
saturazione-valore): sono due rappresentazioni diverse degli stessi dati. Alcuni
compiti che possono essere difficili con una rappresentazione possono diventare
facili con un'altra. Per esempio, il compito "selezionare tutti i pixel rossi
nell'immagine" è più semplice nel formato RGB, mentre "rendere l'immagine meno
satura" è più semplice nel formato HSV. I modelli di apprendimento automatico si
occupano di trovare rappresentazioni appropriate per i loro dati di ingresso,
trasformazioni dei dati che li rendono più adatti al compito da svolgere.
Rendiamo questo concetto concreto. Si considerino un asse y
x, un asse y e alcuni punti rappresentati dalle loro coordinate
nel sistema (x, y), come mostrato nella figura 1.3.
Come si può vedere, abbiamo alcuni punti bianchi e alcuni
punti neri. Supponiamo di voler sviluppare un algoritmo in
grado di prendere le coordinate (x, y) di un punto e di indicare
se è probabile che quel punto sia nero o bianco. In questo caso,
x
◾ Gli input sono le coordinate dei nostri punti.
◾ I risultati attesi sono i colori dei nostri punti.
Figura 1.3 Alcuni
◾ Un modo per misurare se il nostro algoritmo sta dati di esempio
facendo un buon lavoro potrebbe essere, ad esempio, la
percentuale di punti classificati correttamente.
Abbiamo bisogno di una nuova rappresentazione dei dati che separi in modo netto i
punti bianchi da quelli neri. Una trasformazione che potremmo utilizzare, tra le
tante possibili, è una modifica delle coordinate, illustrata nella figura 1.4.
Intelligenza artificiale, apprendimento automatico e 9
apprendimento profondo
1: Dati grezzi 2: Variazione di 3: Migliore rappresentazione
y
y coordinate y

x x

Figura 1.4 Cambio di coordinate

In questo nuovo sistema di coordinate, si può dire che le coordinate dei nostri punti
sono una nuova rappresentazione dei nostri dati. Ed è una buona rappresentazione!
Con questa rappresentazione, il problema della classificazione bianco/nero può
essere espresso come una semplice regola: "I punti neri sono tali che x > 0" o "I
punti bianchi sono tali che x < 0". Questa nuova rappresentazione, combinata con
questa semplice regola, risolve perfettamente il problema della classificazione.
In questo caso abbiamo definito il cambio di coordinate a mano: abbiamo usato
la nostra intelligenza umana per trovare una rappresentazione appropriata dei dati.
Questo va bene per un problema così semplice, ma si potrebbe fare lo stesso se il
compito fosse quello di clas- sificare immagini di cifre scritte a mano? Potreste scrivere
trasformazioni di immagine esplicite ed eseguibili al computer che illuminino la
differenza tra un 6 e un 8, tra un 1 e un 7, in tutti i tipi di grafie diverse?
Questo è possibile fino a un certo punto. Le regole basate su rappresentazioni
delle cifre come il "numero di anelli chiusi" o gli istogrammi verticali e orizzontali
dei pixel possono fare un lavoro decente per distinguere le cifre scritte a mano. Ma
trovare a mano tali rappresentazioni utili è un lavoro duro e, come si può immaginare, il
sistema basato su regole che ne risulta è fragile, un incubo da mantenere. Ogni volta che
ci si imbatte in un nuovo esempio di scrittura che infrange le regole accuratamente
studiate, è necessario aggiungere nuove trasformazioni di dati e nuove regole,
tenendo conto della loro interazione con ogni regola precedente.
Probabilmente starete pensando: se questo processo è così doloroso, potremmo
automatizzarlo? E se provassimo a cercare sistematicamente diverse serie di
rappresentazioni dei dati generate automaticamente e regole basate su di esse,
identificando quelle valide utilizzando come feedback la percentuale di cifre
classificate correttamente in un certo set di dati di sviluppo? In questo caso si
tratterebbe di apprendimento automatico. L'apprendimento, nel contesto
dell'apprendimento automatico, descrive un processo di ricerca automatica di
trasformazioni dei dati che producono rappresentazioni utili di alcuni dati, guidate
da alcuni segnali-rappresentazioni di feedback che si prestano a regole più semplici
per risolvere il compito in questione.
Queste trasformazioni possono essere cambiamenti di coordinate (come nel
nostro esempio di classificazione delle coordinate 2D), o prendere un istogramma di
pixel e contare i cicli (come nel nostro esempio di classificazione delle cifre), ma
possono anche essere proiezioni lineari, traslazioni, operazioni non lineari (come
"selezionare tutti i punti tali che x > 0") e così via.
10 CAPITOLO 1 Cos'è il deep learning?

Gli algoritmi di apprendimento automatico di solito non sono creativi nel trovare queste
trasformazioni, ma si limitano a cercare in un insieme predefinito di operazioni,
chiamato spazio delle ipotesi. Per esempio, lo spazio di tutte le possibili trasformazioni
delle coordinate sarebbe il nostro spazio di ipotesi nell'esempio di classificazione delle
coordinate 2D.
Ecco cos'è, in sintesi, l'apprendimento automatico: la ricerca di rappresentazioni
e regole utili su alcuni dati di input, all'interno di uno spazio predefinito di
possibilità, utilizzando la guida di un segnale di feedback. Questa semplice idea
consente di risolvere una gamma straordinariamente ampia di compiti intellettuali,
dal riconoscimento vocale alla guida autonoma.
Ora che avete capito cosa intendiamo per apprendimento, diamo un'occhiata a ciò che rende
speciale sull'apprendimento profondo.

1.1.4 Il "profondo" in "deep learning"


L'apprendimento profondo è un sottocampo specifico dell'apprendimento
automatico: un nuovo approccio all'apprendimento di rappresentazioni dai dati che
pone l'accento sull'apprendimento di strati successivi di rappresentazioni sempre più
significative. Il termine "deep" in "deep learning" non si riferisce a un tipo di
comprensione più profonda ottenuta con questo approccio, ma piuttosto all'idea di
strati successivi di rappresentazioni. Il numero di strati che contribuiscono a un
modello dei dati è chiamato profondità del modello. Altri nomi appropriati per il
campo avrebbero potuto essere apprendimento di rappresentazioni a strati o apprendimento
di rappresentazioni gerarchiche. Il moderno deep learning spesso coinvolge decine o
addirittura centinaia di strati successivi di rappresentazioni, tutti appresi
automaticamente dall'esposizione ai dati di addestramento. Nel frattempo, altri
approcci all'apprendimento automatico tendono a concentrarsi sull'apprendimento di
uno o due strati di rappresentazioni dei dati (ad esempio, prendendo un istogramma
di pixel e applicando poi una regola di classificazione); per questo motivo, a volte
sono chiamati apprendimento superficiale.
Nell'apprendimento profondo, queste rappresentazioni stratificate vengono
apprese tramite modelli chiamati reti neurali, strutturati in strati letterali impilati
l'uno sull'altro. Il termine "rete neurale" si riferisce alla neurobiologia, ma sebbene
alcuni dei concetti centrali del deep learning siano stati sviluppati in parte
ispirandosi alla nostra comprensione del cervello (in particolare della corteccia
visiva), i modelli di deep learning non sono modelli del cervello. Non ci sono prove
che il cervello implementi qualcosa di simile ai meccanismi di apprendimento
utilizzati nei moderni modelli di deep learning. Può capitare di imbattersi in articoli di
divulgazione scientifica che affermano che il deep learning funziona come il cervello
o che è stato modellato sul cervello, ma non è così. Sarebbe confuso e
controproducente per i neofiti del settore pensare che il deep learning sia in qualche
modo legato alla neurobiologia; non c'è bisogno di quell'alone di misticismo e
mistero "proprio come la nostra mente", e tanto vale dimenticare tutto ciò che si è
letto su ipotetici collegamenti tra deep learning e biologia. Per i nostri scopi, il deep
learning è una struttura matematica per l'apprendimento di rappresentazioni dai dati.
Che aspetto hanno le rappresentazioni apprese da un algoritmo di apprendimento
profondo? Esaminiamo come una rete a diversi livelli di profondità (vedi figura 1.5)
Intelligenza artificiale, apprendimento automatico e 11
apprendimento
trasforma l'immagine profondo
di una cifra per riconoscere di quale cifra si tratta.
12 CAPITOLO 1 Cos'è il deep learning?

Strato 1 Strato 2 Strato 3 Strato 4


Originale 0
ingresso 1
2
3
4 Final
e
5 uscita
6
7 Figura 1.5 Una rete neurale
8 profonda per la classificazione
9 delle cifre

Come si può vedere nella figura 1.6, la rete trasforma l'immagine della cifra in
rappresentazioni sempre più diverse dall'immagine originale e sempre più informative
sul risultato finale. Si può pensare a una rete profonda come a un processo di
distillazione delle informazioni a più stadi, in cui le informazioni passano attraverso
filtri successivi e ne escono sempre più purificate (cioè, utili rispetto a qualche
compito).

Rappresent Rappresent Rappresent


azioni di livello azioni di livello azioni di livello
1 2 3

Strato 4
rappresentazio
ni (output
finale)
Original 0
e 1
ingres 2
so 3
4
5
6
7
8
9
Strato 1Strato 2Strato 3Strato
4

Figura 1.6 Rappresentazioni dei dati apprese da un modello di classificazione delle cifre

Ecco cos'è tecnicamente l'apprendimento profondo: un modo a più stadi per imparare le
rappresentazioni dei dati. È un'idea semplice ma, come si è visto, meccanismi molto
semplici, sufficientemente scalati, possono finire per sembrare magici.

1.1.5 Capire come funziona il deep learning, in tre figure


A questo punto, si sa che l'apprendimento automatico consiste nel mappare gli input
(come le immagini) in target (come l'etichetta "gatto"), cosa che si fa osservando molti
esempi di input e target. Sapete anche che le reti neurali profonde eseguono questa
mappatura degli input e dei target attraverso una sequenza profonda di semplici
trasformazioni dei dati (strati) e che questi
Intelligenza artificiale, apprendimento automatico e 13
apprendimento profondo
Le trasformazioni dei dati si apprendono con l'esposizione agli esempi. Vediamo ora
come avviene concretamente questo apprendimento.
La specifica di ciò che un livello fa ai suoi dati di ingresso è memorizzata nei
pesi del livello, che in sostanza sono un insieme di numeri. In termini tecnici, diremmo che
la trasformazione attuata da un livello è parametrizzata dai suoi pesi (vedi figura 1.7). (In
questo contesto, apprendere significa trovare un insieme di valori per i pesi di tutti gli
strati di una rete, in modo che la rete mappi correttamente gli ingressi degli esempi
ai loro obiettivi associati. Ma il problema è che una rete neurale profonda può
contenere decine di milioni di parametri. Trovare i valori corretti per tutti i parametri
può sembrare un compito arduo, soprattutto se si considera che la modifica del valore di un
parametro influisce sul comportamento di tutti gli altri!

Ingresso X

Strato
Pesi
Obiettivo: (trasformazione dei
trovare i valori dati)
giusti per
questi pesi Strato
Pesi
(trasformazione dei
dati)

Previsioni
Y' Figura 1.7 Una rete neurale è
parametrizzata dai suoi pesi.

Per controllare qualcosa, bisogna prima essere in grado di osservarla. Per controllare
l'output di una rete neurale, bisogna essere in grado di misurare quanto questo output si
allontana da quello previsto. Questo è il compito della funzione di perdita della rete,
talvolta chiamata anche funzione obiettivo o funzione di costo. La funzione di perdita
prende le previsioni della rete e l'obiettivo vero (ciò che si desiderava che la rete
producesse) e calcola un punteggio di distanza, che indica quanto bene la rete ha fatto su
questo specifico esempio (vedi figura 1.8).

Ingresso X

Strato
Pesi
(trasformazione dei
dati)

Strato
Pesi
(trasformazione dei
dati)

Previsioni Obiettivi veri


Y' Y

Funzione di
perdita
Figura 1.8 Una funzione
di perdita misura la qualità
Punteggio
di perdita dell'output della rete.
14 CAPITOLO 1 Cos'è il deep learning?

Il trucco fondamentale del deep learning consiste nell'utilizzare questo punteggio


come segnale di feedback per aggiustare un po' il valore dei pesi, in modo da
abbassare il punteggio di perdita per l'esempio corrente (vedi figura 1.9). Questa
regolazione è compito dell'ottimizzatore, che implementa il cosiddetto algoritmo di
retropropagazione: l'algoritmo centrale dell'apprendimento profondo. Il prossimo
capitolo spiega in modo più dettagliato come funziona la retropropagazione.

Ingresso X

Strato
Pesi
(trasformazione dei
dati)

Strato
Pesi
(trasformazione dei
dati)

Peso Previsioni Obiettivi veri


aggior Y' Y
namen
to
Ottimizzat Funzione di
ore perdita
Figura 1.9 Il punteggio di
perdita viene utilizzato
Punteggio
di perdita come segnale di feedback
per regolare i pesi.

Inizialmente, ai pesi della rete vengono assegnati valori casuali, quindi la rete si
limita a implementare una serie di trasformazioni casuali. Naturalmente, il suo
output è molto lontano da quello che dovrebbe essere idealmente, e il punteggio di
perdita è di conseguenza molto alto. Ma a ogni esempio che la rete elabora, i pesi
vengono aggiustati un po' nella direzione corretta e il punteggio di perdita diminuisce.
Questo è il ciclo di addestramento che, ripetuto un numero sufficiente di volte (in genere
decine di iterazioni su migliaia di esempi), produce valori di pesi che minimizzano la
funzione di perdita. Una rete con una perdita minima è una rete per la quale le uscite sono
il più vicino possibile agli obiettivi: una rete addestrata. Ancora una volta, si tratta
di un meccanismo semplice che, una volta scalato, finisce per sembrare magico.

1.1.6 I risultati ottenuti finora dall'apprendimento profondo


Sebbene l'apprendimento profondo sia un sottocampo piuttosto antico
dell'apprendimento automatico, è salito alla ribalta solo all'inizio degli anni 2010.
Nei pochi anni successivi, ha realizzato una vera e propria rivoluzione nel campo,
producendo risultati notevoli su compiti percettivi e persino sull'elaborazione del
linguaggio naturale, problemi che coinvolgono abilità che sembrano naturali e
intuitive per gli esseri umani, ma che sono state a lungo inafferrabili per le
macchine.
In particolare, l'apprendimento profondo ha permesso le seguenti scoperte, tutte in
aree storicamente difficili dell'apprendimento automatico:
◾ Classificazione delle immagini a livello quasi umano
◾ Trascrizione del parlato a livello quasi umano
Intelligenza artificiale, apprendimento automatico e 15
apprendimento
◾ Trascrizione profondo
della scrittura a mano di livello quasi umano
16 CAPITOLO 1 Cos'è il deep learning?

◾ Traduzione automatica drasticamente migliorata


◾ Conversione da testo a voce drasticamente migliorata
◾ Assistenti digitali come Google Assistant e Amazon Alexa
◾ Guida autonoma di livello quasi umano
◾ Miglioramento del targeting degli annunci, come quello utilizzato da Google, Baidu o Bing
◾ Migliori risultati di ricerca sul web
◾ Capacità di rispondere a domande in linguaggio naturale
◾ Superhuman Vai a giocare

Stiamo ancora esplorando la portata di ciò che l'apprendimento profondo può fare. Abbiamo
iniziato ad applicarlo con grande successo a un'ampia varietà di problemi che si
pensava fossero impossibili da risolvere solo pochi anni fa: trascrivere
automaticamente le decine di migliaia di antichi manoscritti conservati nell'Archivio
Apostolico Vaticano, rilevare e classificare le malattie delle piante nei campi usando un
semplice smartphone, assistere oncologi o radiologi nell'interpretazione dei dati di
imaging medico, prevedere disastri naturali come inondazioni, uragani o persino
terremoti, e così via. Con ogni pietra miliare, ci avviciniamo a un'era in cui il deep
learning ci assiste in ogni attività e in ogni campo dello sforzo umano: scienza,
medicina, produzione, energia, trasporti, sviluppo di software, agricoltura e persino
creazione artistica.

1.1.7 Non credete alla pubblicità a breve termine


Sebbene l'apprendimento profondo abbia portato a risultati notevoli negli ultimi
anni, le aspettative su ciò che il campo sarà in grado di raggiungere nel prossimo
decennio tendono a essere molto più alte di ciò che sarà probabilmente possibile.
Sebbene alcune applicazioni che cambieranno il mondo, come le automobili
autonome, siano già a portata di mano, molte altre rimarranno probabilmente
inafferrabili per molto tempo, come i sistemi di dialogo credibili, la traduzione
automatica a livello umano di lingue arbitrarie e la comprensione del linguaggio
naturale a livello umano. In particolare, i discorsi sull'intelligenza generale di livello
umano non dovrebbero essere presi troppo sul serio. Il rischio di avere grandi
aspettative a breve termine è che, quando la tecnologia non riesce a dare risultati, gli
investimenti nella ricerca si esauriscano, rallentando i progressi per molto tempo.
È già successo in passato. Per due volte in passato, l'IA ha attraversato un ciclo
di intenso ottimismo seguito da delusione e scetticismo, con conseguente scarsità di
finanziamenti. È iniziato con l'IA simbolica negli anni Sessanta. In quei primi tempi, le
proiezioni sull'IA volavano alte. Uno dei più noti pionieri e sostenitori dell'approccio
all'IA simbolica fu Marvin Minsky, che nel 1967 affermò: "Entro una generazione
... il problema della creazione di una 'intelligenza artificiale' sarà sostanzialmente
risolto". Tre anni dopo, nel 1970, fece una previsione più precisamente quantificata:
"Tra tre o otto anni avremo una macchina con l'intelligenza generale di un essere
umano medio". Nel 2021 un tale risultato appare ancora lontano nel futuro, tanto che
non abbiamo modo di prevedere quanto tempo ci vorrà, ma negli anni '60 e nei primi
anni '70 diversi esperti lo ritenevano proprio dietro l'angolo (come molti oggi). Pochi anni
dopo, quando queste grandi aspettative non si sono concretizzate, i ricercatori e i fondi
governativi si sono rivolti a
Intelligenza artificiale, apprendimento automatico e 17
apprendimento profondo
che si allontana dal campo, segnando l'inizio del primo inverno dell'IA (un riferimento a una
vittoria nucleare, perché questo avvenne poco dopo l'apice della Guerra Fredda).
Non sarebbe stato l'ultimo. Negli anni '80, un nuovo approccio all'IA simbolica, i
sistemi esperti, iniziò a prendere piede tra le grandi aziende. Alcuni successi iniziali
scatenarono un'ondata di investimenti, con aziende di tutto il mondo che avviarono i
propri dipartimenti interni di IA per sviluppare sistemi esperti. Intorno al 1985, le
aziende spendevano più di un miliardo di dollari all'anno per questa tecnologia; ma
all'inizio degli anni '90, questi sistemi si erano dimostrati costosi da mantenere, difficili
da scalare e di portata limitata, e l'interesse si era spento. Iniziò così il secondo inverno
dell'IA.
Forse stiamo assistendo al terzo ciclo di hype e delusioni dell'IA, e siamo ancora
in una fase di intenso ottimismo. È meglio moderare le nostre aspettative a breve
termine e assicurarsi che le persone meno familiari con l'aspetto tecnico del settore
abbiano un'idea chiara di ciò che il deep learning può o non può offrire.

1.1.8 La promessa dell'intelligenza artificiale


Sebbene le nostre aspettative a breve termine per l'IA possano essere irrealistiche, il
p a n o r a m a a lungo termine appare luminoso. Stiamo solo iniziando ad applicare
l'apprendimento profondo a molti problemi importanti per i quali potrebbe rivelarsi
trasformativo, dalle diagnosi mediche agli assistenti digitali. Negli ultimi dieci anni l a
ricerca sull'IA è progredita in modo sorprendentemente rapido, in gran parte grazie a un
livello di finanziamenti mai visto prima nella breve storia dell'IA, ma finora i progressi
compiuti sono stati relativamente poco diffusi nei prodotti e nei processi che
costituiscono il nostro mondo. La maggior parte dei risultati della ricerca
sull'apprendimento profondo non sono ancora applicati, o almeno non sono applicati
all'intera gamma di problemi che potrebbero risolvere in tutti i settori. Il vostro medico
non usa ancora l'IA, e nemmeno il vostro commercialista. Probabilmente non utilizzate
spesso le tecnologie AI nella vostra vita quotidiana. Certo, potete fare semplici domande
al vostro smartphone e ottenere risposte ragionevoli, potete ottenere consigli abbastanza
utili sui prodotti su Amazon.com e potete cercare "compleanno" su Google Foto e
trovare immediatamente le foto della festa di compleanno di vostra figlia del mese
scorso. È una situazione molto lontana da quella in cui si trovavano queste tecnologie.
Ma questi strumenti sono ancora solo accessori della nostra vita quotidiana.
L'Intelligenza Artificiale deve ancora diventare un elemento centrale del nostro modo di
lavorare, pensare e vivere.
In questo momento può sembrare difficile credere che l'IA possa avere un
grande impatto sul nostro mondo, perché non è ancora ampiamente diffusa, così come, nel
1995, sarebbe stato difficile credere all'impatto futuro di Internet. All'epoca, la
maggior parte delle persone non vedeva come internet fosse rilevante per loro e
come avrebbe cambiato le loro vite. Lo stesso vale oggi per il deep learning e l'IA.
Ma non fatevi illusioni: L'IA sta arrivando. In un futuro non così lontano, l'IA sarà il
vostro assistente, persino il vostro amico; risponderà alle vostre domande, vi aiuterà a
educare i vostri figli e vigilerà sulla vostra salute. Consegnerà la spesa a domicilio e
vi accompagnerà dal punto A al punto B. Sarà la vostra interfaccia con un mondo
sempre più complesso e ricco di informazioni. E, cosa ancora più importante, l'IA
aiuterà l'umanità nel suo complesso a progredire, assistendo gli scienziati umani in
18 CAPITOLO 1 Cos'è il deep learning?
nuove scoperte rivoluzionarie in tutti i campi scientifici, dalla genomica alla
matematica.
Prima del deep learning: Breve storia dell'apprendimento 13
automatico
Durante il percorso, potremmo dover affrontare alcune battute d'arresto e forse anche
un nuovo inverno dell'intelligenza artificiale, proprio come l'industria di Internet è stata
sopravvalutata nel 1998-99 e ha subito un crollo che ha prosciugato gli investimenti nei
primi anni 2000. Ma alla fine ci arriveremo. L'IA finirà per essere applicata a quasi tutti
i processi che compongono la nostra società e la nostra vita quotidiana, proprio come
avviene oggi con Internet.
Non credete al clamore a breve termine, ma credete nella visione a lungo
termine. Forse ci vorrà un po' di tempo prima che l'IA venga sfruttata al massimo
delle sue potenzialità - potenzialità che nessuno ha ancora osato sognare - ma l'IA
sta arrivando e trasformerà il nostro mondo in modo fantastico.

1.2 Prima del deep learning: Breve storia


dell'apprendimento automatico
L'apprendimento profondo ha raggiunto un livello di attenzione pubblica e di
investimenti industriali mai visto prima nella storia dell'IA, ma non è la prima forma
di apprendimento automatico di successo. Si può dire che la maggior parte degli
algoritmi di apprendimento automatico utilizzati oggi nel settore non sono algoritmi
di apprendimento profondo. L'apprendimento profondo non è sempre lo strumento
giusto per il lavoro: a volte non ci sono abbastanza dati perché l'apprendimento
profondo sia applicabile e a volte il problema è risolto meglio da un algoritmo
diverso. Se il deep learning è il vostro primo contatto con l'apprendimento
automatico, potreste trovarvi in una situazione in cui tutto ciò che avete è il martello
del deep learning e ogni problema di apprendimento automatico inizia a sembrare un
chiodo. L'unico modo per non cadere in questa trappola è conoscere altri approcci e
praticarli quando è opportuno.
Una discussione dettagliata degli approcci classici all'apprendimento automatico
esula dallo scopo di questo libro, ma li ripercorrerò brevemente e descriverò il
contesto storico in cui sono stati sviluppati. Questo ci permetterà di collocare
l'apprendimento profondo nel contesto più ampio dell'apprendimento automatico e
di capire meglio da dove viene l'apprendimento profondo e perché è importante.

1.2.1 Modellazione probabilistica


La modellazione probabilistica è l'applicazione dei principi della statistica all'analisi dei
dati. È una delle prime forme di apprendimento automatico ed è ancora oggi
ampiamente utilizzata. Uno degli algoritmi più noti di questa categoria è l'algoritmo
di Naive Bayes.
Il Naive Bayes è un tipo di classificatore di apprendimento automatico basato
sull'applicazione del t e o r e m a di Bayes, assumendo che le caratteristiche dei dati
in ingresso siano tutte indipendenti (un'assunzione forte, o "ingenua", da cui d e r i v a il
nome). Questa forma di analisi dei dati è antecedente ai computer e veniva applicata a
mano decenni prima della sua prima implementazione al computer (molto
probabilmente risale agli anni '50). Il teorema di Bayes e le basi della statistica
risalgono al XVIII secolo e sono tutto ciò che serve per iniziare a usare i
classificatori Naive Bayes.
Un modello strettamente correlato è la regressione logistica (logreg in breve), che a
volte è considerata il "Hello World" del moderno apprendimento automatico. Non
14 CAPITOLO 1 Cos'è il deep learning?
lasciatevi trarre in inganno dal nome: logreg è un algoritmo di classificazione piuttosto
che un algoritmo di regressione. Molto
Prima del deep learning: Breve storia dell'apprendimento 15
automatico
Come Naive Bayes, logreg è antecedente all'informatica da molto tempo, ma è ancora
oggi utile grazie alla sua natura semplice e versatile. Spesso è la prima cosa che uno
scienziato dei dati prova su un set di dati per farsi un'idea del compito di classificazione
da svolgere.

1.2.2 Le prime reti neurali


Le prime iterazioni delle reti neurali sono state completamente soppiantate dalle
varianti moderne trattate in queste pagine, ma è utile sapere come è nato il deep
learning. Sebbene le idee di base delle reti neurali siano state studiate in forma
giocattolo già negli anni Cinquanta, l'approccio ha richiesto decenni per essere
avviato. Per molto tempo, il pezzo mancante è stato un modo efficiente per
addestrare reti neurali di grandi dimensioni. La situazione è cambiata a metà degli
anni '80, quando diverse persone hanno riscoperto in modo indipendente l'algoritmo
di Backpropagation, un modo per addestrare catene di operazioni parametriche
utilizzando l'ottimizzazione del gradiente-descente (definiremo con precisione
questi concetti più avanti nel libro), e hanno iniziato ad applicarlo alle reti neurali.
La prima applicazione pratica di successo delle reti neurali risale al 1989,
quando Yann LeCun combinò le idee precedenti delle reti neurali convoluzionali e
della retropropagazione, applicandole al problema della classificazione delle cifre
scritte a mano. La rete risultante, denominata LeNet, è stata utilizzata negli anni '90 dalle
Poste degli Stati Uniti per automatizzare la lettura dei codici di avviamento postale
sulle buste postali.

1.2.3 Metodi kernel


Quando negli anni '90 le reti neurali hanno iniziato a guadagnare un po' di rispetto tra i
ricercatori, grazie a questo primo successo, un nuovo approccio all'apprendimento
automatico è salito alla ribalta e ha rapidamente rispedito le reti neurali nel
dimenticatoio: i metodi kernel. I metodi kernel sono un gruppo di algoritmi di
classificazione, il più noto dei quali è la Support Vector Machine (SVM). La
formulazione moderna di una SVM è stata sviluppata da Vladimir Vapnik e Corinna
Cortes all'inizio degli anni '90 presso i laboratori Bell e pubblicata nel 1995,3 anche se
una formulazione lineare più vecchia era stata pubblicata da Vapnik e Alexey
Chervonenkis già nel 1963.4
SVM è un algoritmo di classificazione che funziona trovando
"confini di decisione" che separano due classi (vedi figura 1.10).
Gli SVM procedono a trovare questi confini in due fasi:
1 I dati vengono mappati in una nuova rappresentazione ad alta
dimensione in cui il confine decisionale può essere espresso
come un iperpiano (se i dati fossero bidimensionali, come
nella f i g u r a 1.10, un iperpiano sarebbe una linea
retta).
2 Un buon confine decisionale (un iperpiano di separazione) è
calcolato cercando di massimizzare la distanza tra Figura 1.10
l'iperpiano e i punti dati più vicini di ciascuna classe, a Un confine decisionale
16 CAPITOLO 1 Cos'è il deep learning?

3
Vladimir Vapnik e Corinna Cortes, "Support-Vector Networks", Machine Learning 20, no. 3 (1995): 273-297.
4
Vladimir Vapnik e Alexey Chervonenkis, "A Note on One Class of Perceptrons", Automation and Remote Con- trol
25 (1964).
Prima del deep learning: Breve storia dell'apprendimento 17
automatico
fase chiamata massimizzazione del margine. Ciò consente al margine di
generalizzare bene a nuovi campioni al di fuori del set di dati di addestramento.
La tecnica di mappare i dati in una rappresentazione ad alta dimensione che semplifica il
problema della classificazione può sembrare buona sulla carta, ma in pratica è spesso
computazionalmente intrattabile. È qui che entra in gioco il trucco del kernel (l'idea
chiave da cui prendono il nome i metodi kernel). Ecco il succo: per trovare buoni
iperpiani decisionali nel nuovo spazio di rappresentazione, non è necessario calcolare
esplicitamente le coordinate dei punti nel nuovo spazio; è sufficiente calcolare la
distanza tra coppie di punti in quello spazio, cosa che può essere fatta in modo efficiente
utilizzando una funzione kernel. Una funzione kernel è un'operazione computabile che
mappa due punti qualsiasi dello spazio iniziale alla distanza tra questi punti nello spazio
di rappresentazione di destinazione, evitando completamente il calcolo esplicito della
nuova rappresentazione. Le funzioni kernel sono tipicamente create a mano piuttosto
che apprese dai dati; n e l caso di un SVM, viene appreso solo l'iperpiano di
separazione.
All'epoca in cui furono sviluppate, le SVM mostravano prestazioni
all'avanguardia su semplici problemi di classificazione ed erano uno dei pochi
metodi di apprendimento automatico supportati da un'ampia teoria e suscettibili di
una seria analisi matematica, che le rendeva ben comprensibili e facilmente
interpretabili. Grazie a queste utili proprietà, le SVM sono diventate estremamente
popolari nel settore per molto tempo.
Ma le SVM si sono dimostrate difficili da scalare su grandi insiemi di dati e non
hanno fornito buoni risultati per problemi percettivi come la classificazione delle
immagini. Essendo un metodo superficiale, l'applicazione di una SVM a problemi
percettivi richiede prima l'estrazione manuale delle rappresentazioni utili (una fase
chiamata feature engineering), che è difficile e fragile. Per esempio, se si vuole usare una
SVM per classificare le cifre scritte a mano, non si può partire dai pixel grezzi; si
devono prima trovare rappresentazioni utili che rendano il problema più trattabile,
come gli istogrammi dei pixel di cui ho parlato prima.

1.2.4 Alberi decisionali, foreste casuali e macchine di gradient boosting


Gli alberi decisionali sono strutture simili a diagrammi di flusso che consentono di
classificare i punti di dati in ingresso o di predefinire i valori di uscita in base agli input
(vedi figura 1.11). Sono facili da visualizzare e da inter- pretare. Gli alberi decisionali
appresi dai dati hanno iniziato a riscuotere un notevole interesse da parte della ricerca
n e g l i anni 2000 e nel 2010 erano spesso preferiti ai metodi kernel.

Dati di ingresso

Domand
a

Categoria
Domand Domand
a a

Categoria Categoria Categoria


18 CAPITOLO 1 Cos'è il deep learning?

Figura 1.11 Un albero


decisionale: i
parametri che
vengono appresi
sono le domande
sui dati. Una
domanda potrebbe
essere, ad esempio,
"Il coefficiente 2 nei
dati è maggiore di
3,5?".
Prima del deep learning: Breve storia dell'apprendimento 19
automatico
In particolare, l'algoritmo Random Forest ha introdotto una soluzione pratica e
robusta per l'apprendimento ad albero decisionale, che prevede la costruzione di un gran
numero di alberi decisionali specializzati e il successivo assemblaggio dei loro risultati.
Le foreste casuali sono applicabili a un'ampia gamma di problemi: si potrebbe dire
che sono quasi sempre il secondo miglior algoritmo per qualsiasi compito di
apprendimento automatico poco approfondito. Quando il popolare sito web di
competizioni di apprendimento automatico Kaggle (http://kaggle.com) è stato
avviato nel 2010, le foreste casuali sono diventate rapidamente le preferite sulla
piattaforma, fino al 2014, quando le macchine di gradient boosting hanno preso il
sopravvento. Una macchina di gradient boosting, come una foresta casuale, è una
tecnica di apprendimento automatico basata sull'assemblaggio di modelli di previsione
deboli, generalmente alberi decisionali. Utilizza il gradient boosting, un modo per
migliorare qualsiasi modello di apprendimento automatico addestrando
iterativamente nuovi modelli specializzati nel risolvere i punti deboli dei modelli
precedenti. Applicato agli alberi decisionali, l'uso della tecnica del gradient boosting
porta a modelli che nella maggior parte dei casi superano le foreste casuali, pur
avendo proprietà simili. Potrebbe essere uno dei migliori, se non il migliore,
algoritmo per trattare i dati non percettivi oggi. Insieme al deep learning, è una delle
tecniche più utilizzate nelle competizioni Kaggle.

1.2.5 Torna alle reti neurali


Intorno al 2010, sebbene le reti neurali fossero quasi del tutto ignorate dalla
comunità scientifica in generale, alcune persone che lavoravano ancora sulle reti
neurali hanno iniziato a fare importanti scoperte: i gruppi di Geoffrey Hinton
dell'Università di Toronto, Yoshua Bengio dell'Università di Montreal, Yann LeCun
della New York University e IDSIA in Svizzera.
Nel 2011, Dan Ciresan dell'IDSIA ha iniziato a vincere gare accademiche di
classificazione delle immagini con reti neurali profonde addestrate da GPU, il primo
successo pratico del deep learning moderno. Ma il momento cruciale è arrivato nel
2012, con la partecipazione del gruppo di Hinton alla sfida annuale di
classificazione delle immagini su larga scala ImageNet (ImageNet Large Scale
Visual Recognition Challenge, in breve ILSVRC). La sfida ImageNet era notoriamente
difficile all'epoca e consisteva nel classificare immagini a colori ad alta risoluzione in
1.000 categorie diverse dopo un addestramento su 1,4 milioni di immagini. Nel
2011, l'accuratezza del modello vincitore, basato su approcci classici alla computer
vision, era solo del 74,3%.5 Poi, nel 2012, un team guidato da Alex Krizhevsky e
consigliato da Geoffrey Hinton è riuscito a raggiungere un'accuratezza dell'83,6%, una
svolta significativa. Da allora, ogni anno la competizione è stata dominata dalle reti
neurali convoluzionali profonde. Nel 2015, il vincitore ha raggiunto un'accuratezza
del 96,4% e il compito di classificazione su ImageNet è stato considerato un
problema completamente risolto.
Dal 2012, le reti neurali convoluzionali profonde (convnet) sono diventate
l'algoritmo di riferimento per tutti i compiti di computer vision; più in generale,
funzionano per tutti i compiti percettivi.
20 CAPITOLO 1 Cos'è il deep learning?
5 La "Top-five accuracy" misura la frequenza con cui il modello seleziona la risposta corretta tra le prime cinque ipotesi
(su 1.000 risposte possibili, nel caso di ImageNet).
Prima del deep learning: Breve storia dell'apprendimento 21
automatico
compiti. In tutte le principali conferenze sulla visione artificiale dopo il 2015, è stato
quasi impossibile trovare presentazioni che non coinvolgessero in qualche modo le reti
convettive. Allo stesso tempo, il deep learning ha trovato applicazione anche in molti
altri tipi di problemi, come l'elaborazione del linguaggio naturale. Ha sostituito
completamente SVM e alberi decisionali in un'ampia gamma di applicazioni. Per
esempio, per diversi anni l'Organizzazione europea per la ricerca nucleare (CERN) ha
utilizzato metodi basati sugli alberi decisionali per analizzare i dati delle particelle
provenienti dal rivelatore ATLAS del Large Hadron Collider (LHC), ma il CERN è poi
passato alle reti neurali profonde basate su Keras grazie alle loro prestazioni più elevate
e alla facilità di addestramento su grandi insiemi di dati.

1.2.6 Cosa rende diverso l'apprendimento profondo


Il motivo principale per cui il deep learning è decollato così rapidamente è che ha
offerto prestazioni migliori per molti problemi. Ma non è l'unica ragione.
L'apprendimento profondo semplifica anche la risoluzione dei problemi, perché
automatizza completamente quella che un tempo era la fase più cruciale del flusso
di lavoro dell'apprendimento automatico: l'ingegnerizzazione delle caratteristiche.
Le precedenti tecniche di apprendimento automatico - apprendimento superficiale - si
limitavano a trasformare i dati di ingresso in uno o due spazi di rappresentazione
successivi, di solito attraverso trasformazioni semplici come proiezioni non lineari
ad alta dimensione (SVM) o alberi decisionali. Ma le rappresentazioni raffinate
richieste dai problemi complessi non possono essere ottenute con queste tecniche.
Per questo motivo, gli esseri umani hanno dovuto impegnarsi a fondo per rendere i
dati di input iniziali più adatti all'elaborazione da parte di questi metodi: hanno
dovuto progettare manualmente buoni strati di rappresentazioni per i loro dati.
Questa operazione è chiamata feature engineering. L'apprendimento profondo,
invece, automatizza completamente questa fase: con l'apprendimento profondo, si
imparano tutte le caratteristiche in un unico passaggio, invece di doverle progettare
da soli. Questo ha semplificato enormemente i flussi di lavoro dell'apprendimento
automatico, spesso sostituendo sofisticate pipeline a più fasi con un unico, semplice
modello di deep learning end-to-end.
Ci si può chiedere: se il nocciolo della questione è avere più strati successivi di
rappresentazione, si possono applicare ripetutamente metodi poco profondi per emulare
gli effetti dell'apprendimento profondo? In pratica, le applicazioni successive di metodi
di apprendimento superficiale producono rendimenti che si riducono rapidamente,
perché il primo strato di rappresentazione ottimale in un modello a tre strati non è il
primo strato ottimale in un modello a uno o due strati. L'aspetto trasformativo del deep
learning è che consente a un modello di apprendere tutti gli strati di rappresentazione in
modo congiunto, allo stesso tempo, anziché in successione (avidamente, come viene
chiamato). Con l'apprendimento congiunto delle caratteristiche, ogni volta che il
modello regola una delle sue caratteristiche interne, tutte le altre caratteristiche che
dipendono da essa si adattano automaticamente alla modifica, senza richiedere
l'intervento umano. Tutto è supervisionato da un unico segnale di feedback: ogni
modifica del modello serve all'obiettivo finale. Questo è molto più potente dell'impilare
avidamente modelli poco profondi, perché consente di apprendere rappresentazioni
complesse e astratte suddividendole in lunghe serie di spazi intermedi (strati); ogni
22 CAPITOLO 1 Cos'è il deep learning?
spazio è solo una semplice trasformazione del precedente.
Queste sono le due caratteristiche essenziali del modo in cui il deep learning
apprende dai dati: il modo incrementale, strato per strato, in cui vengono sviluppate
rappresentazioni sempre più complesse,
Prima del deep learning: Breve storia dell'apprendimento 23
automatico
e il fatto che queste rappresentazioni intermedie incrementali vengono apprese
congiuntamente, ogni strato viene aggiornato per seguire sia le esigenze di
rappresentazione dello strato precedente sia quelle dello strato sottostante. Insieme,
queste due proprietà hanno reso l'apprendimento profondo molto più efficace dei
precedenti approcci all'apprendimento automatico.

1.2.7 Il moderno panorama dell'apprendimento automatico


Un ottimo modo per farsi un'idea del panorama attuale degli algoritmi e degli strumenti
di apprendimento automatico è quello di guardare le gare di apprendimento automatico
su Kaggle. Grazie all'ambiente altamente competitivo (alcune gare hanno migliaia di
partecipanti e premi milionari) e all'ampia varietà di problemi di apprendimento
automatico trattati, Kaggle offre un modo realistico per valutare ciò che funziona e ciò
che non funziona. Che tipo di algoritmo vince in modo affidabile le gare? Quali
strumenti utilizzano i migliori partecipanti?
All'inizio del 2019, Kaggle ha condotto un sondaggio chiedendo ai team che si
sono classificati tra i primi cinque di ogni competizione dal 2017 quale strumento
software principale avessero utilizzato nella competizione (vedi figura 1.12). È
emerso che i team migliori tendono a utilizzare metodi di deep learning (il più delle
volte tramite la libreria Keras) o gradient boosted trees (il più delle volte tramite le
librerie LightGBM o XGBoost).

Keras

LuceGBM

XGBoost

PyTorch

TensorFlow

Scikit-learn

Fastai

Caffè

0 10 20 30 40
Numero di competizioni

Profondo Classico

Figura 1.12 Strumenti di apprendimento automatico utilizzati dai migliori team su Kaggle
24 CAPITOLO 1 Cos'è il deep learning?

Non si tratta solo di campioni della competizione. Kaggle conduce anche un sondaggio
annuale tra i professionisti dell'apprendimento automatico e della scienza dei dati di
tutto il mondo. Con decine di migliaia di partecipanti, questo sondaggio è una delle fonti
più affidabili sullo stato del settore. La Figura 1.13 mostra la percentuale di utilizzo di
diversi framework software per l'apprendimento automatico.

Scikit-learn 82.8%

TensorFlow 50.5%

Keras 50.5%

Xgboost 48.4%

PyTorch 30.9%

LuceGBM 26.1%

Caret 14.1%

Catboost 13.7%

Profeta 10%

Fast.ai 7.5%

Tidymodels 7.2%

H2O3 6%

MXNet 2.1%

Altro 3.7%

Nessuno 3.2%

JAX 0.7%

0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100%

Figura 1.13 Utilizzo di strumenti nel settore dell'apprendimento automatico e della scienza dei dati (Fonte:
www.kaggle.com/ kaggle-survey-2020)

Dal 2016 al 2020, l'intero settore dell'apprendimento automatico e della scienza dei
dati è stato dominato da questi due approcci: deep learning e gradient boosted trees.
In particolare, il gradient boosted trees viene utilizzato per problemi in cui sono
disponibili dati strutturati, mentre il deep learning viene utilizzato per problemi
percettivi come la classificazione delle immagini.
Gli utenti di alberi a gradiente amplificato tendono a usare Scikit-learn,
XGBoost o LightGBM. Nel frattempo, la maggior parte dei praticanti
dell'apprendimento profondo utilizza Keras, spesso in combinazione con
Prima del deep learning: Breve storia dell'apprendimento 25
automatico
il suo framework madre TensorFlow. Il punto in comune di questi strumenti è che sono
tutti librerie Python: Python è di gran lunga il linguaggio più utilizzato per
l'apprendimento automatico e la scienza dei dati.
Queste sono le due tecniche con cui dovreste avere maggiore familiarità per
avere successo nell'apprendimento automatico applicato oggi: gli alberi a gradiente
potenziato, per i problemi di apprendimento superficiale, e l'apprendimento
profondo, per i problemi percettivi. In termini tecnici, ciò significa che dovrete
conoscere Scikit-learn, XGBoost e Keras, le tre librerie che attualmente dominano le
competizioni Kaggle. Con questo libro in mano, siete già un grande passo avanti.

1.3 Perché l'apprendimento profondo? Perché ora?


Le due idee chiave dell'apprendimento profondo per la visione computerizzata - reti neurali
rivoluzionarie e backpropagation - erano già ben comprese nel 1990. L'algoritmo
LSTM (Long Short-Term Memory), fondamentale per l'apprendimento profondo
per le serie temporali, è stato sviluppato nel 1997 e da allora non è praticamente
cambiato. Allora perché il deep learning è decollato solo dopo il 2012? Cosa è
cambiato in questi due decenni?
In generale, sono tre le forze tecniche che guidano i progressi dell'apprendimento
automatico:
◾ Hardware
◾ Set di dati e benchmark
◾ Progressi algoritmici

Poiché il campo è guidato da risultati sperimentali piuttosto che dalla teoria, i


progressi algoritmici diventano possibili solo quando sono disponibili dati e
hardware appropriati per provare nuove idee (o per scalare le vecchie idee, come
spesso accade). L'apprendimento automatico non è matematica o fisica, dove i
principali progressi possono essere fatti con una penna e un pezzo di carta. È una
scienza ingegneristica.
I veri colli di bottiglia negli anni '90 e 2000 sono stati i dati e l'hardware. Ma
ecco cosa è successo in quel periodo: Internet è decollato e sono stati sviluppati chip
grafici ad alte prestazioni per le esigenze del mercato dei giochi.

1.3.1 Hardware
Tra il 1990 e il 2010, le CPU di serie sono diventate più veloci di circa 5.000 volte. Di
conseguenza, oggi è possibile eseguire piccoli modelli di deep learning sul proprio
laptop, mentre 25 anni fa sarebbe stato impossibile farlo.
Ma i tipici modelli di deep learning utilizzati nella computer vision o nel
riconoscimento vocale richiedono ordini di grandezza superiori alla potenza di
calcolo che il vostro laptop è in grado di fornire. Nel corso degli anni 2000, aziende
come NVIDIA e AMD hanno investito miliardi di dollari nello sviluppo di chip
veloci e massicciamente paralleli (unità di elaborazione grafica, o GPU) per
alimentare la grafica di videogiochi sempre più fotorealistici - supercomputer economici e
monouso progettati per eseguire il rendering di scene 3D complesse sullo schermo in
tempo reale. Questo investimento si è rivelato vantaggioso per la comunità
scientifica quando, nel 2007, NVIDIA ha lanciato CUDA
26 CAPITOLO 1 Cos'è il deep learning?
(https://developer.nvidia.com/about-cuda), un'interfaccia di programmazione che
consente di gestire i dati in tempo reale.
Perché l'apprendimento 21
profondo? Perché ora?
per la sua linea di GPU. Un piccolo numero di GPU ha iniziato a sostituire cluster
massicci di CPU in varie applicazioni altamente parallelizzabili, a partire dalla
modellazione fisica. Anche le reti neurali profonde, costituite per lo più da molte piccole
moltiplicazioni di matrici, sono altamente parallelizzabili e intorno al 2011 alcuni
ricercatori hanno iniziato a scrivere implementazioni CUDA di reti neurali: Dan
Ciresan6 e Alex Krizhevsky7 sono stati tra i primi.
È successo che il mercato dei giochi ha sovvenzionato il supercalcolo per la
prossima generazione di applicazioni di intelligenza artificiale. A volte, le grandi
cose iniziano come giochi. Oggi, la NVIDIA Titan RTX, una GPU che costava 2.500
dollari alla fine del 2019, è in grado di offrire un picco di 16 teraFLOPS in singola
precisione (16 trilioni di operazioni float32 al secondo). Si tratta di una potenza di
calcolo circa 500 volte superiore a quella del super computer più veloce al mondo del
1990, l'Intel Touchstone Delta. Su una Titan RTX, ci vogliono solo poche ore per
addestrare un modello ImageNet del tipo che avrebbe vinto la gara ILSVRC nel
2012 o 2013. Nel frattempo, le grandi aziende addestrano i modelli di deep learning
su cluster di centinaia di GPU.
Inoltre, l'industria del deep learning si sta spostando oltre le GPU e sta
investendo in chip sempre più specializzati ed efficienti per l'apprendimento
profondo. Nel 2016, in occasione della convention annuale I/O, Google ha rivelato il
progetto Tensor Processing Unit (TPU): un nuovo design di chip sviluppato da zero
per eseguire reti neurali profonde in modo significativamente più veloce e molto più
efficiente dal punto di vista energetico rispetto alle GPU top di gamma. Oggi, nel
2020, la terza iterazione della scheda TPU rappresenta 420 teraFLOPS di potenza di
calcolo. Si tratta di un valore 10.000 volte superiore a quello dell'Intel Touchstone
Delta del 1990.
Queste schede TPU sono progettate per essere assemblate in configurazioni su larga
scala, chiamate "pod". Un pod (1024 schede TPU) raggiunge un picco di 100
petaFLOPS. Per avere un'idea della scala, si tratta di circa il 10% della potenza di
calcolo di picco dell'attuale più grande supercomputer, l'IBM Summit dell'Oak Ridge
National Lab, che consiste in 27.000 GPU NVIDIA e raggiunge un picco di circa 1,1
exaFLOPS.

1.3.2 Dati
L'intelligenza artificiale è talvolta annunciata come la nuova rivoluzione industriale.
Se il deep learning è la macchina a vapore di questa rivoluzione, i dati sono il suo
carbone: la materia prima che alimenta le nostre macchine intelligenti, senza la
quale nulla sarebbe possibile. Per quanto riguarda i dati, oltre al progresso
esponenziale dell'hardware di archiviazione negli ultimi 20 anni (seguendo la legge di
Moore), il cambiamento di gioco è stato l'ascesa di Internet, che ha reso possibile la
raccolta e la distribuzione di serie di dati molto grandi per l'apprendimento automatico. Oggi
le grandi aziende lavorano con dataset di immagini, video e linguaggio naturale che
non avrebbero potuto essere raccolti senza internet. I tag delle immagini generate
dagli utenti su Flickr, ad esempio

6 Si veda "Flexible, High Performance Convolutional Neural Networks for Image Classification", Proceedings of
the 22nd International Joint Conference on Artificial Intelligence (2011), www.ijcai.org/Proceedings/11/Papers/
22 CAPITOLO 1 Cos'è il deep learning?
210.pdf.
7
Si veda "ImageNet Classification with Deep Convolutional Neural Networks", Advances in Neural Information
Pro- cessing Systems 25 (2012), http://mng.bz/2286.
Perché l'apprendimento 23
profondo? Perché ora?
sono stati un tesoro di dati per la computer vision. Lo stesso vale per i video di
YouTube. E Wikipedia è un set di dati fondamentale per l'elaborazione del linguaggio
naturale.
Se c'è un set di dati che ha catalizzato l'ascesa del deep learning, è il set di dati
ImageNet, composto da 1,4 milioni di immagini annotate a mano con 1.000 categorie di
immagini (una categoria per immagine). Ma ciò che rende ImageNet speciale non sono
solo le sue grandi dimensioni, ma anche la competizione annuale ad esso associata.8
Come dimostra Kaggle dal 2010, le competizioni pubbliche sono un modo
eccellente per motivare ricercatori e ingegneri a spingersi oltre. Avere dei
benchmark comuni che i ricercatori competono per battere ha aiutato molto l'ascesa
del deep learning, evidenziandone il successo rispetto agli approcci classici
dell'apprendimento automatico.

1.3.3 Algoritmi
Oltre all'hardware e ai dati, fino alla fine degli anni 2000 mancava un modo
affidabile per addestrare reti neurali molto profonde. Di conseguenza, le reti neurali
erano ancora piuttosto basse, utilizzando solo uno o due strati di rappresentazioni; di
conseguenza, non erano in grado di competere con metodi più raffinati e poco
profondi, come le SVM e le foreste casuali. Il problema principale era quello della
propagazione del gradiente attraverso strati profondi. Il segnale di feedback utilizzato per
addestrare le reti neurali si affievoliva con l'aumentare del numero di strati.
La situazione è cambiata intorno al 2009-2010 con l'avvento di alcuni semplici ma
importanti miglioramenti algoritmici che hanno permesso una migliore propagazione del
gradiente:
◾ Migliori funzioni di attivazione per gli strati neurali
◾ Migliori schemi di inizializzazione dei pesi, a partire dal preaddestramento dei livelli,
poi rapidamente abbandonato.
◾ Migliori schemi di ottimizzazione, come RMSProp e Adam

Solo quando questi miglioramenti hanno iniziato a consentire l'addestramento di


modelli con 10 o più strati, il deep learning ha iniziato a brillare.
Infine, nel 2014, 2015 e 2016 sono stati scoperti modi ancora più avanzati per
migliorare la propagazione del gradiente, come la normalizzazione dei lotti, le
connessioni residue e le convoluzioni separabili in profondità.
Oggi possiamo addestrare modelli arbitrariamente profondi partendo da zero. Ciò ha
reso possibile l'uso di modelli estremamente grandi, che possiedono un notevole potere
rappresentativo, ovvero che codificano spazi di ipotesi molto ricchi. Questa estrema
scalabilità è una delle caratteristiche che definiscono il moderno deep learning. Le
architetture di modelli su larga scala, caratterizzate da decine di strati e decine di milioni
di parametri, hanno portato a progressi critici sia nella visione artificiale (ad esempio,
architetture come ResNet, Inception o Xception) sia nell'elaborazione del linguaggio
naturale (ad esempio, architetture basate su Transformer di grandi dimensioni come
BERT, GPT-3 o XLNet).
24 CAPITOLO 1 Cos'è il deep learning?
8 ImageNet Large Scale Visual Recognition Challenge (ILSVRC), www.image-net.org/challenges/LSVRC.
Perché l'apprendimento 25
profondo? Perché ora?
1.3.4 Una nuova ondata di investimenti
Quando nel 2012-2013 l'apprendimento profondo è diventato il nuovo stato dell'arte
per la computer vision, e in seguito per tutti i compiti percettivi, i leader del settore
hanno preso nota. Ne è seguita un'ondata graduale di investimenti nel settore che ha
superato di gran lunga quanto visto in precedenza nella storia dell'IA (cfr. figura
1.14).

Investimenti totali stimati in start-up Al, 2011-17 e primo semestre 2018


Per posizione di avvio
USA Cina UE Israele Canada Giappone Altro India
Miliardi di dollari
18

16
Israel
e
14 UE

12
Cina
10

8
Israel
e
UE
6
Cina
UE
4 ST
ATI
ST UN
2 ST ST ATI ITI
ATI ATI UN
ST UN UN ITI
ST ST ATI ITI ITI
0 ATI ATI UN
UN
2011 UN
2012 2013
ITI 2014 2015 2016 2017
ITI ITI

Figura 1.14 Stima OCSE degli investimenti totali in startup AI (Fonte: http://mng.bz/zGN6)

Nel 2011, poco prima che il deep learning salisse alla ribalta, il totale degli
investimenti di venture capital nell'IA a livello mondiale era inferiore a un miliardo
di dollari, destinato quasi esclusivamente ad applicazioni pratiche di approcci di
apprendimento automatico poco profondi. Nel 2015, la cifra era salita a oltre 5
miliardi di dollari e nel 2017 a un'incredibile cifra di 16 miliardi di dollari. In questi
anni sono state lanciate centinaia di startup che hanno cercato di capitalizzare il
successo del deep learning. Nel frattempo, grandi aziende tecnologiche come
Google, Amazon e Microsoft hanno investito in dipartimenti di ricerca interni per
importi che probabilmente superano il flusso di denaro dei venture capital.
L'apprendimento automatico, e in particolare l'apprendimento profondo, è diventato
centrale nella strategia di produzione di questi giganti tecnologici. Alla fine del 2015, il
CEO di Google Sundar Pichai ha dichiarato: "L'apprendimento automatico è un modo
fondamentale e trasformativo con cui stiamo ripensando il modo in cui siamo
26 CAPITOLO 1 Cos'è il deep learning?

per tutto. Lo stiamo applicando in modo ponderato a tutti i nostri prodotti, che si
tratti di ricerca, annunci, YouTube o Play. Siamo ancora agli inizi, ma ci vedrete applicare
in modo sistematico il machine learning in tutte queste aree". 9
Grazie a questa ondata di investimenti, il numero di persone che lavorano sul deep
learning è passato da poche centinaia a decine di migliaia in meno di 10 anni e i
progressi della ricerca hanno raggiunto un ritmo frenetico.

1.3.5 La democratizzazione del deep learning


Uno dei fattori chiave che ha spinto l'afflusso di nuovi volti nel deep learning è stata
la democratizzazione degli strumenti utilizzati in questo campo. All'inizio,
l'apprendimento profondo richiedeva competenze significative in C++ e CUDA, che
pochi possedevano.
Al giorno d'oggi, per fare ricerca avanzata sull'apprendimento profondo è
sufficiente una conoscenza di base dello scripting Python. Ciò è avvenuto
soprattutto grazie allo sviluppo della libreria Theano, ormai defunta, e poi della
libreria TensorFlow, due framework di manipolazione simbolica dei tensori per
Python che supportano l'autodifferenziazione, semplificando notevolmente
l'implementazione di nuovi modelli, e grazie all'ascesa di librerie di facile utilizzo
come Keras, che rende il deep learning facile come manipolare i mattoncini LEGO.
Dopo il suo rilascio all'inizio del 2015, Keras è diventato rapidamente la soluzione
di deep learning preferita da un gran numero di nuove startup, studenti laureati e
ricercatori che si affacciano su questo campo.

1.3.6 Durerà?
C'è qualcosa di speciale nelle reti neurali profonde che le rende l'approccio "giusto"
su cui le aziende devono investire e i ricercatori devono accorrere? Oppure il deep
learning è solo una moda che potrebbe non durare? Utilizzeremo ancora le reti
neurali profonde tra 20 anni?
L'apprendimento profondo ha diverse proprietà che giustificano il suo status di
rivoluzione dell'IA ed è destinato a rimanere. Forse tra due decenni non useremo più le
reti neurali, ma qualsiasi cosa useremo erediterà direttamente dal moderno deep
learning e dai suoi concetti fondamentali. Queste importanti proprietà possono
essere suddivise in tre categorie:
◾ Semplicità: l'apprendimento approfondito elimina la necessità di ingegnerizzare
le funzionalità, sostituendo pipeline complesse, fragili e pesanti dal punto di vista
ingegneristico con modelli semplici e addestrabili end-to-end, che in genere
vengono costruiti utilizzando solo cinque o sei operazioni tensoriali diverse.
◾ Scalabilità: l'apprendimento profondo è altamente adattabile alla parallelizzazione su
GPU o TPU, in modo da poter sfruttare appieno la legge di Moore. Inoltre, i
modelli di deep learning vengono addestrati iterando su piccoli lotti di dati, il
che consente di addestrarli su insiemi di dati di dimensioni arbitrarie. (L'unico
collo di bottiglia è la quantità di potenza di calcolo parallelo disponibile che,
grazie alla legge di Moore, è una barriera in rapida evoluzione).
◾ Versatilità e riutilizzabilità - A differenza di molti approcci precedenti
all'apprendimento automatico, i modelli di deep learning possono essere
Perché l'apprendimento 27
profondo? Perché
addestrati su dati aggiuntivi senza ora?
dover ripartire da zero.

9 Sundar Pichai, conferenza stampa di Alphabet, 22 ottobre 2015.


28 CAPITOLO 1 Cos'è il deep learning?

e questo li rende utilizzabili per l'apprendimento continuo online, una


proprietà importante per i modelli di produzione di grandi dimensioni. Inoltre,
i modelli di deep learning addestrati sono riproponibili e quindi riutilizzabili:
per esempio, è possibile prendere un modello di deep learning addestrato per
la classificazione delle immagini e inserirlo in una pipeline di elaborazione
video. Questo ci permette di reinvestire il lavoro precedente in modelli
sempre più complessi e potenti. Questo rende il deep learning applicabile
anche a insiemi di dati piuttosto piccoli.
L'apprendimento profondo è sotto i riflettori solo da pochi anni e forse non abbiamo
ancora stabilito la portata di ciò che può fare. Ogni anno che passa, scopriamo nuovi
casi d'uso e miglioramenti ingegneristici che eliminano i limiti precedenti. Dopo una
rivoluzione scientifica, il progresso segue in genere una curva sigmoide: inizia con
un periodo di rapidi progressi, che si stabilizzano gradualmente quando i ricercatori
incontrano forti limitazioni, e poi ulteriori miglioramenti diventano incrementali.
Quando ho scritto la prima edizione di questo libro, nel 2016, ho previsto che il
deep learning si trovava ancora nella prima metà del sigmoide, con progressi molto
più trasformativi in arrivo negli anni successivi. Ciò si è dimostrato vero nella
pratica, poiché il 2017 e il 2018 hanno visto l'ascesa di modelli di deep learning
basati su Transformer per l'elaborazione del linguaggio naturale, che hanno
rappresentato una rivoluzione nel campo, mentre il deep learning ha continuato a
fornire progressi costanti anche nella computer vision e nel riconoscimento del
parlato. Oggi, nel 2021, il deep learning sembra essere entrato nella seconda metà
del sigmoide. Dobbiamo ancora aspettarci progressi significativi negli anni a venire,
ma probabilmente siamo usciti dalla fase iniziale di progressi esplosivi.
Oggi sono estremamente entusiasta della diffusione della tecnologia di deep
learning per tutti i problemi che può risolvere: l'elenco è infinito. L'apprendimento
profondo è ancora una rivoluzione in atto e ci vorranno molti anni per realizzarne il
pieno potenziale.
Perché l'apprendimento 29
profondo? Perché ora?

I blocchi
matematici
delle reti neurali

Questo capitolo tratta


◾ Un primo esempio di rete neurale
◾ Tensori e operazioni con i tensori
◾ Come le reti neurali apprendono attraverso la
retropropagazione e la discesa del gradiente

La comprensione dell'apprendimento profondo richiede la familiarità con molti


semplici concetti matematici: tensori, operazioni tensoriali, differenziazione, discesa
del gradiente e così via. Il nostro obiettivo in questo capitolo sarà quello di farvi
acquisire l'intuizione di queste nozioni senza scendere troppo nel tecnico. In
particolare, ci terremo alla larga dalla notazione matematica, che può introdurre
barriere inutili per chi non ha una base matematica e non è necessaria per
spiegare bene le cose. La descrizione più precisa e univoca di un'operazione
matematica è il suo codice eseguibile.
Per fornire un contesto sufficiente all'introduzione dei tensori e della discesa
del gradiente, inizieremo il capitolo con un esempio pratico di rete neurale. Poi
esamineremo punto per punto tutti i nuovi concetti introdotti. Tenete presente che
questi concetti saranno essenziali per comprendere gli esempi pratici dei capitoli
successivi!

26
Un primo sguardo a una rete 27
neurale
Dopo aver letto questo capitolo, avrete una comprensione intuitiva della teoria
matematica che sta alla base del deep learning e sarete pronti per iniziare a
immergervi in Keras e TensorFlow nel capitolo 3.

2.1 Un primo sguardo a una rete neurale


Vediamo un esempio concreto di rete neurale che utilizza la libreria Python Keras per
imparare a classificare le cifre scritte a mano. A meno che non abbiate già esperienza
con Keras o librerie simili, non capirete s u b i t o tutto di questo primo esempio. Ma va
bene così. Nel prossimo capitolo rivedremo ogni elemento dell'esempio e lo
spiegheremo in dettaglio. Non preoccupatevi quindi se alcuni passaggi vi sembreranno
arbitrari o magici! Da qualche parte bisogna pur cominciare.
Il problema che cerchiamo di risolvere è quello di classificare le immagini in scala di
grigi di dieci cifre scritte a mano (28 × 28 pixel) nelle loro 10 categorie (da 0 a 9).
Utilizzeremo il dataset MNIST, un classico della comunità dell'apprendimento
automatico, che esiste quasi da sempre e che è stato studiato intensamente. Si tratta di
un insieme di 60.000 immagini di addestramento, più 10.000 immagini di prova,
assemblate dal National Institute of Standards and Technology (il NIST in MNIST)
negli anni Ottanta. Si può pensare di "risolvere" MNIST come il "Hello World" del deep
learning: è ciò che si fa per verificare che gli algoritmi funzionino come previsto. Man
mano che si diventa un professionista dell'apprendimento automatico, si vedrà MNIST
comparire più volte in articoli scientifici, post di blog e così via. L a figura 2.1 mostra
alcuni esempi di MNIST.

Figura 2.1 Cifre campione MNIST

NOTA Nell'apprendimento automatico, una categoria in un problema di


classificazione è chiamata classe. I punti dati sono chiamati campioni. La
classe associata a un campione specifico è chiamata etichetta.

Non è necessario provare a riprodurre questo esempio sulla propria macchina. Se lo


desiderate, dovrete prima configurare uno spazio di lavoro per l'apprendimento profondo,
di cui si parla nel capitolo 3.
Il dataset MNIST è precaricato in Keras, sotto forma di un insieme di quattro
array NumPy.

Listato 2.1 Caricamento del dataset MNIST in Keras

da tensorflow.keras.datasets importa mnist


(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

train_images e train_labels costituiscono il set di addestramento, ovvero i


dati da cui il modello apprenderà. Il modello verrà poi testato sul set di test, test_images
e test_labels.
28 CAPITOLO 2 Gli elementi matematici delle reti neurali

Le immagini sono codificate come array NumPy e le etichette sono array di cifre, da
0 a 9. Le immagini e le etichette hanno una corrispondenza uno a uno. Le immagini
e le etichette hanno una corrispondenza uno-a-uno.
Esaminiamo i dati di allenamento:

>>> train_images.shape
(60000, 28, 28)
>>> len(train_labels)
60000
>>> addestramento_labelle
array([5, 0, 4, ..., 5, 6, 8], dtype=uint8)

Ed ecco i dati del test:

>>> test_images.shape
(10000, 28, 28)
>>> len(test_labels)
10000
>>> test_labels
array([7, 2, 1, ..., 4, 5, 6], dtype=uint8)

Il flusso di lavoro sarà il seguente: Per prima cosa, alimenteremo la rete neurale con i
dati di addestramento, train_images e train_labels. La rete imparerà quindi ad
associare immagini ed etichette. Infine, chiederemo alla rete di produrre previsioni per
test_images e verificheremo se queste previsioni corrispondono alle etichette di
test_labels.
Costruiamo la rete - ancora una volta, ricordate che non ci si aspetta che
comprendiate tutto di questo esempio.

Listato 2.2 L'architettura di rete

da tensorflow importa keras


da tensorflow.keras import layers
model = keras.Sequential([
layers.Dense(512, attivazione="relu"),
layers.Dense(10,
attivazione="softmax")
])

L'elemento centrale delle reti neurali è il livello. Si può pensare a un livello come a un filtro
per i dati: alcuni dati entrano e ne escono in una forma più utile. In particolare, i
livelli estraggono rappresentazioni dai dati in essi immessi, auspicabilmente
rappresentazioni più significative per il problema in questione. La maggior parte
dell'apprendimento profondo consiste nel concatenare semplici livelli che implementano
una forma di distillazione progressiva dei dati. Un modello di deep learning è come un
setaccio per l'elaborazione dei dati, costituito da una successione di filtri sempre più
raffinati: gli strati.
In questo caso, il nostro modello consiste in una sequenza di due strati Dense, che
sono strati neurali densamente connessi (detti anche fully connected). Il secondo (e
ultimo) strato è uno strato di classificazione softmax a 10 vie, il che significa che
restituirà un array di 10 punteggi di probabilità (con somma a 1). Ogni punteggio sarà la
probabilità che l'immagine della cifra corrente appartenga a una delle 10 classi di cifre.
Un primo sguardo a una rete 29
neurale
Per rendere il modello pronto per l'addestramento, dobbiamo scegliere altre tre
cose come parte della fase di compilazione:
◾ Un ottimizzatore: il meccanismo attraverso il quale il modello si aggiorna in base ai
dati di addestramento che vede, in modo da migliorare le sue prestazioni.
◾ Una funzione di perdita: il modo in cui il modello sarà in grado di misurare le
proprie prestazioni sui dati di addestramento e quindi di orientarsi nella giusta
direzione.
◾ Metriche da monitorare durante l'addestramento e il test: in questo caso, ci
interesserà solo l'accuratezza (la frazione di immagini classificate correttamente).
Lo scopo esatto della funzione di perdita e dell'ottimizzatore sarà chiarito nei
prossimi due capitoli.

Listato 2.3 La fase di compilazione

model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])

Prima dell'addestramento, preprocesseremo i dati rimodellandoli nella forma che il


modello si aspetta e scalandoli in modo che tutti i valori siano nell'intervallo [0, 1].
In precedenza, le nostre immagini di addestramento erano memorizzate in un array di
forma (60000, 28, 28) di tipo uint8 con valori nell'intervallo [0, 255]. La
trasformeremo in una matrice float32 di tipo (60000, 28 * 28) con valori
compresi tra 0 e 1.

Listato 2.4 Preparazione dei dati dell'immagine

train_images = train_images.reshape((60000, 28 * 28))


train_images = train_images.astype("float32") / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype("float32") / 255

Ora siamo pronti ad addestrare il modello, cosa che in Keras viene fatta tramite una chiamata
alla funzione
metodo fit(): adattiamo il modello ai dati di addestramento.

Listato 2.5 "Adattamento" del modello

>>> model.fit(train_images, train_labels, epochs=5, batch_size=128)


Epoch 1/5
60000/60000 [===========================] - 5s - perdita: 0.2524 - acc: 0.9273
Epoca 2/5
51328/60000 [=====================>.....] - ETA: 1s - perdita: 0.1035 - acc: 0.9692

Durante l'addestramento vengono visualizzate due grandezze: la perdita del modello


sui dati di addestramento e l'accuratezza del modello sui dati di addestramento.
Abbiamo raggiunto rapidamente un'accuratezza di 0,989 (98,9%) sui dati di
addestramento.
Ora che abbiamo un modello addestrato, possiamo usarlo per predire le probabilità delle
classi per
30 CAPITOLO 2 Gli elementi matematici delle reti neurali
nuove cifre - immagini che non facevano parte dei dati di addestramento, come quelle del set
di test.
Un primo sguardo a una rete 31
neurale

Listato 2.6 Utilizzo del modello per fare previsioni


>>> test_digits = test_images[0:10]
>>> previsioni = model.predict(test_digits)
>>> previsioni[0]
array([1.0726176e-10, 1.6918376e-10, 6.1314843e-08, 8.4106023e-06,
2.9967067e-11, 3.0331331e-09, 8.3651971e-14, 9.9999106e-01,
2,6657624e-08, 3,8127661e-07], dtype=float32)

Ogni numero dell'indice i in tale matrice corrisponde alla probabilità che l'immagine della
cifra
test_digits[0] appartiene alla classe i.
Questa prima cifra di prova ha il punteggio di probabilità più alto (0,99999106, quasi
1) con l'indice 7, quindi, secondo il nostro modello, deve essere un 7:

>>> previsioni[0].argmax()
7
>>> previsioni[0][7]
0.99999106

Possiamo verificare che l'etichetta di prova sia conforme:

>>> test_labels[0]
7

In media, quanto è bravo il nostro modello a classificare queste cifre mai viste prima?
Verifichiamo calcolando l'accuratezza media sull'intero set di test.

Listato 2.7 Valutazione del modello su nuovi dati

>>> test_loss, test_acc = model.evaluate(test_images, test_labels)


>>> print(f "test_acc: {test_acc}")
test_acc: 0,9785

L'accuratezza del set di test risulta essere del 97,8%, un valore nettamente inferiore
all'accuratezza del set di addestramento (98,9%). Questo divario tra l'accuratezza
dell'addestramento e l'accuratezza del test è un esempio di overfitting: il fatto che i
modelli di apprendimento automatico tendono a ottenere risultati peggiori sui nuovi
dati rispetto ai dati di addestramento. L'overfitting è un argomento centrale del
capitolo 3.
Questo conclude il nostro primo esempio: avete appena visto come si può
costruire e addestrare una rete neurale per classificare le cifre scritte a mano in
meno di 15 righe di codice Python. In questo capitolo e nel prossimo, entreremo nel
dettaglio di ogni pezzo mobile che abbiamo appena visto e chiariremo cosa succede
dietro le quinte. Impareremo a conoscere i tensori, gli oggetti che memorizzano i
dati nel modello; le operazioni sui tensori, che costituiscono gli strati; e la discesa
del gradiente, che consente al modello di imparare dagli esempi di addestramento.
Rappresentazioni di dati per reti neurali 31

2.2 Rappresentazioni di dati per reti neurali


Nell'esempio precedente, siamo partiti da dati memorizzati in array
multidimensionali NumPy, chiamati anche tensori. In generale, tutti gli attuali sistemi di
apprendimento automatico utilizzano i tensori come struttura dati di base. I tensori sono
fondamentali in questo campo, tanto che TensorFlow ha preso il loro nome. Che
cos'è un tensore?
In sostanza, un tensore è un contenitore di dati, di solito numerici. Quindi, è un
contenitore di numeri. Forse avete già familiarità con le matrici, che sono tenori di
rango 2: i tensori sono una generalizzazione delle matrici a un numero arbitrario di
dimensioni (notate che nel contesto dei tensori, una dimensione è spesso chiamata
asse).

2.2.1 Scalari (tensori di rango 0)


Un tensore che contiene un solo numero è chiamato scalare (o tensore scalare, o tensore
di rango 0, o tensore 0D). In NumPy, un numero float32 o float64 è un tensore
scalare (o array scalare). È possibile visualizzare il numero di assi di un tensore NumPy
tramite l'attributo ndim; un tensore scalare ha 0 assi (ndim == 0). Il numero di assi di
un tensore è chiamato anche rango. Ecco uno scalare NumPy:

>>> importare numpy come np


>>> x = np.array(12)
>>> x
array(12)
>>> x.ndim
0

2.2.2 Vettori (tensori di rango 1)


Una matrice di numeri è chiamata vettore, o tensore di rango 1, o tensore 1D. Si dice
che un tensore di rango 1 abbia esattamente un asse. Di seguito è riportato un vettore
NumPy:

>>> x = np.array([12, 3, 6, 14, 7])


>>> x
array([12, 3, 6, 14, 7])
>>> x.ndim
1

Questo vettore ha cinque voci ed è quindi chiamato vettore a 5 dimensioni. Non bisogna
confondere un vettore 5D con un tensore 5D! Un vettore 5D ha un solo asse e ha cinque
dimensioni lungo il suo asse, mentre un tensore 5D ha cinque assi (e può avere un
numero qualsiasi di dimensioni lungo ogni asse). La dimensionalità può indicare sia il
numero di voci lungo un asse specifico (come nel caso del nostro vettore 5D) sia il
numero di assi di un tensore (come il tensore 5D), il che a volte può creare confusione.
In quest'ultimo caso, è tecnicamente più corretto parlare di un tensore di rango 5 (il
rango di un tensore è il numero di assi), ma la notazione ambigua di tensore 5D è
comune comunque.
32 CAPITOLO 2 Gli elementi matematici delle reti neurali

2.2.3 Matrici (tensori di rango 2)


Una matrice di vettori è una matrice, o tensore di rango 2, o tensore 2D. Una matrice ha
due assi (spesso indicati come righe e colonne). È possibile interpretare visivamente una
matrice come una griglia rettangolare di numeri. Questa è una matrice NumPy:

>>> x = np.array([[5, 78, 2, 34, 0],


[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]])
>>> x.ndim
2

Le voci del primo asse sono chiamate righe e le voci del secondo asse sono chiamate
colonne. Nell'esempio precedente, [5, 78, 2, 34, 0] è la prima riga di x e [5, 6,
7] è la prima colonna.

2.2.4 Tensori di rango 3 e di rango superiore


Se si impacchettano tali matrici in un nuovo array, si ottiene un tensore di rango 3 (o
tensore 3D), che si può interpretare visivamente come un cubo di numeri. Di seguito è
riportato un tensore di rango 3 di NumPy:

>>> x = np.array([[5, 78, 2, 34, 0],


[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]],
[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]],
[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]]])
>>> x.ndim
3

Impacchettando i tensori di rango 3 in un array, è possibile creare un tensore di


rango 4 e così via. Nell'apprendimento profondo, in genere si manipolano tensori
con rango da 0 a 4, anche se si può arrivare a 5 se si elaborano dati video.

2.2.5 Attributi chiave


Un tensore è definito da tre attributi chiave:
◾ Numero di assi (rango) - Ad esempio, un tensore di rango 3 ha tre assi, mentre
una matrice ha due assi. Nelle librerie Python, come NumPy o TensorFlow,
questo numero è chiamato anche ndim del tensore.
◾ Forma - È una tupla di numeri interi che descrive quante dimensioni ha il tensore
lungo ciascun asse. Per esempio, l'esempio della matrice precedente ha forma
(3, 5) e l'esempio del tensore di rango 3 ha forma (3, 3, 5). Un vettore ha una
forma con un singolo elemento, come (5,), mentre uno scalare ha una forma
vuota, ().
Rappresentazioni di dati per reti neurali 33

◾ Tipo di dati (solitamente chiamato dtype nelle librerie Python): è il tipo di dati
contenuti nel tensore; ad esempio, il tipo di un tensore può essere float16,
float32, float64, uint8 e così via. In TensorFlow è probabile che si
incontrino anche tensori di tipo stringa.
Per rendere il tutto più concreto, riprendiamo i dati elaborati nell'esempio di
MNIST. Innanzitutto, carichiamo il set di dati MNIST:

da tensorflow.keras.datasets importa mnist


(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

Quindi, visualizziamo il numero di assi del tensore train_images, l'attributo ndim:

>>> train_images.ndim
3

Ecco la sua forma:

>>> train_images.shape
(60000, 28, 28)

E questo è il suo tipo di dati, l'attributo dtype:

>>> train_images.dtype
uint8

Si tratta quindi di un tensore di rango 3 di numeri interi a 8 bit. Più precisamente, si


tratta di una matrice di 60.000 matrici di 28 × 28 interi. Ciascuna di queste matrici è
un'immagine in scala di grigi, con coefficienti compresi tra 0 e 255.
Visualizziamo la quarta cifra di questo tensore di rango 3, utilizzando la libreria
Matplotlib (una nota libreria Python per la visualizzazione dei dati, preinstallata in
Colab); vedi figura 2.2.

Figura 2.2 Il quarto campione


del nostro dataset
34 CAPITOLO 2 Gli elementi matematici delle reti neurali

Listato 2.8 Visualizzazione della quarta cifra


importare matplotlib.pyplot come
plt digit = train_images[4]
plt.imshow(digit, cmap=plt.cm.binary)
plt.show()

Naturalmente, l'etichetta corrispondente è il numero intero 9:

>>> train_labels[4]
9

2.2.6 Manipolazione dei tensori in NumPy


Nell'esempio precedente, abbiamo selezionato una cifra specifica accanto al primo asse
utilizzando la sintassi train_images[i]. La selezione di elementi specifici in un tensore
è chiamata affettatura del tensore. Vediamo le operazioni di affettamento del tensore che
si possono eseguire sugli array di NumPy.
L'esempio seguente seleziona le cifre da #10 a #100 (#100 non è incluso) e le
inserisce in una matrice di forma (90, 28, 28):

>>> my_slice = train_images[10:100]


>>> my_slice.shape
(90, 28, 28)

È equivalente a questa notazione più dettagliata, che specifica un indice iniziale e un


indice finale per la fetta lungo ciascun asse del tensore. Si noti che : equivale a
selezionare l'intero asse:
Equivalente
>>> my_slice = train_images[10:100, :, :]
all'esempio
>>> my_slice.shape precedente
(90, 28, 28)
>>> my_slice = train_images[10:100, 0:28, 0:28]
>>> my_slice.shape Equivale anche all'esempio
(90, 28, 28) precedente

In generale, è possibile selezionare fette tra due indici qualsiasi lungo ciascun asse
del tensore. Ad esempio, per selezionare 14 × 14 pixel nell'angolo inferiore destro di
tutte le immagini, si procede in questo modo:

my_slice = train_images[:, 14:, 14:]

È anche possibile utilizzare indici negativi. Come gli indici negativi negli elenchi
Python, indicano una posizione relativa alla fine dell'asse corrente. Per ritagliare le
immagini in patch di 14 × 14 pixel centrate al centro, si procede in questo modo:

my_slice = train_images[:, 7:-7, 7:-7]


Rappresentazioni di dati per reti neurali 35

2.2.7 La nozione di batch di dati


In generale, il primo asse (asse 0, perché l'indicizzazione inizia da 0) in tutti i tensori di
dati che si incontrano nel deep learning sarà l'asse dei campioni (a volte chiamato
dimensione dei campioni). Nell'esempio di MNIST, i "campioni" sono immagini di
cifre.
Inoltre, i modelli di deep learning non elaborano un intero set di dati in una sola
volta, ma suddividono i dati in piccoli lotti. Concretamente, ecco un lotto dei nostri dati
MNIST, con una dimensione di 128:

batch = train_images[:128]

Ed ecco il prossimo lotto:

batch = train_images[128:256]

E l'ennesimo lotto:

n = 3
batch = train_images[128 * n:128 * (n + 1)]

Quando si considera un tensore batch di questo tipo, il primo asse (asse 0) è


chiamato asse batch o dimensione batch. È un termine che si incontra spesso quando si
usa Keras e altre librerie di deep learning.

2.2.8 Esempi reali di tensori di dati


Rendiamo più concreti i tensori di dati con alcuni esempi simili a quelli che incontrerete
in seguito. I dati che manipolerete rientreranno quasi sempre in una delle seguenti
categorie:
◾ Dati vettoriali - tensori Rank-2 di forma (campioni, caratteristiche), dove
ogni campione è un vettore di attributi numerici ("caratteristiche")
◾ Dati di serie temporali o di sequenze: tensori Rank-3 di forma (campioni,
intervalli di tempo, caratteristiche), dove ogni campione è una
sequenza (di lunghezza pari agli intervalli di tempo) di vettori di
caratteristiche.
◾ Immagini-Rank-4 tensori di forma (campioni, altezza, larghezza,
canali), dove ogni campione è una griglia 2D di pixel, e ogni pixel è
rappresentato da un vettore di valori ("canali")
◾ Video -Rank-5 tensori di forma (campioni, fotogrammi, altezza, larghezza,
canali), dove ogni campione è una sequenza (di fotogrammi di lunghezza) di
immagini

2.2.9 Dati vettoriali


Questo è uno dei casi più comuni. In un set di dati di questo tipo, ogni singolo punto di
dati può essere codificato come un vettore e quindi un gruppo di dati sarà codificato
come un tensore di rango 2 (cioè un array di vettori), dove il primo asse è l'asse dei
campioni e il secondo asse è l'asse delle caratteristiche.
36 CAPITOLO 2 Gli elementi matematici delle reti neurali

Vediamo due esempi:


◾ Un set di dati attuariali di persone, in cui si considerano l'età, il sesso e il
reddito di ciascuna persona. Ogni persona può essere caratterizzata come un
vettore di 3 valori, e quindi un intero set di dati di 100.000 persone può essere
memorizzato in un tensore di rango 2 di forma (100000, 3).
◾ Un insieme di documenti di testo, in cui ogni documento è rappresentato dal
conteggio di quante volte ogni parola appare in esso (su un dizionario di
20.000 parole comuni). Ogni documento può essere codificato come un
vettore di 20.000 valori (un conteggio per ogni parola del dizionario), e quindi
un intero dataset di 500 documenti può essere memorizzato in un tensore di
forma (500, 20000).

2.2.10 Dati di serie temporali o di sequenza


Quando il tempo è importante nei dati (o la nozione di ordine di sequenza), ha senso
memorizzarli in un tensore di rango 3 con un asse temporale esplicito. Ogni campione
può essere codificato come una sequenza di vettori (un tensore di rango 2) e quindi un
gruppo di dati sarà codificato come un tensore di rango 3 (vedi figura 2.3).

Caratteristiche

Campio
ni Figura 2.3 Un tensore di dati
Tempi di serie temporali di rango 3

L'asse del tempo è sempre il secondo asse (asse di indice 1) per convenzione. Vediamo
alcuni esempi:
◾ Un set di dati sui prezzi delle azioni. Ogni minuto, memorizziamo il prezzo
corrente del titolo, il prezzo più alto dell'ultimo minuto e il prezzo più basso
dell'ultimo minuto. In questo modo, ogni minuto è codificato come un vettore 3D,
un'intera giornata di trading è codificata come una matrice di forma (390, 3)
(ci sono 390 minuti in una giornata di trading) e 250 giorni di dati possono essere
memorizzati in un tensore di rango 3 di forma (250, 390, 3). In questo caso,
ogni campione corrisponde a un giorno di dati.
◾ Un insieme di tweet, in cui ogni tweet viene codificato come una sequenza di 280
caratteri su un alfabeto di 128 caratteri unici. In questo contesto, ogni carattere
può essere codificato come un vettore binario di dimensione 128 (un vettore di tutti
zeri, tranne una voce 1 all'indice corrispondente al carattere). Quindi ogni tweet
può essere codificato come un tensore di rango 2 di forma (280, 128) e un set
di dati di 1 milione di tweet può essere memorizzato in un tensore di forma
(1000000, 280, 128).
Rappresentazioni di dati per reti neurali 37

2.2.11 Dati immagine


Le immagini hanno tipicamente tre dimensioni: altezza, larghezza e profondità di
colore. Sebbene le immagini in scala di grigi (come le nostre cifre MNIST) abbiano un
solo canale di colore e possano quindi essere memorizzate in tensori di rango 2, per
convenzione i tensori delle immagini sono sempre di rango 3, con un canale di colore
monodimensionale per le immagini in scala di grigi. Un gruppo di 128 immagini in
scala di grigi di dimensioni 256 × 256 potrebbe quindi essere memorizzato in un tensore
di forma (128, 256, 256, 1), mentre un gruppo di 128 immagini a colori potrebbe
essere memorizzato in un tensore di forma (128, 256, 256, 3) (vedi figura 2.4).

Canali colore

Altezza

Campioni

Figura 2.4 Un
Larghezza tensore di dati di
immagine di rango
4

Esistono due convenzioni per le forme dei tensori di immagini: la convenzione


channels-last (che è standard in TensorFlow) e la convenzione channels-first (che è
sempre più in disuso).
La convenzione channels-last pone l'asse della profondità del colore alla fine:
(samples, height, width, color_depth). Al contrario, la convenzione
channels-first colloca l'asse della profondità del colore subito dopo l'asse del lotto:
(samples, color_depth, height, width). Con la convenzione channels-first, gli
esempi precedenti diventerebbero (128, 1, 256, 256) e (128, 3, 256, 256).
L'API di Keras supporta entrambi i formati.

2.2.12 Dati video


I dati video sono uno dei pochi tipi di dati del mondo reale per i quali è necessario
disporre di tenori di rango 5. Un video può essere inteso come una sequenza di
fotogrammi, ognuno dei quali è un'immagine a colori. Un video può essere inteso come
una sequenza di fotogrammi, ognuno dei quali è un'immagine a colori. Poiché ogni
fotogramma può essere memorizzato in un tensore di rango 3 (altezza, larghezza,
profondità_colore), una sequenza di fotogrammi può essere memorizzata in un
tensore di rango 4 (fotogrammi, altezza, larghezza,
profondità_colore), e quindi un gruppo di video diversi può essere memorizzato
in un tensore di rango 5 di forma (campioni, fotogrammi, altezza, larghezza,
profondità_colore).
38 CAPITOLO 2 Gli elementi matematici delle reti neurali

Ad esempio, un videoclip di YouTube di 60 secondi, 144 × 256, campionato a 4


fotogrammi al secondo, avrebbe 240 fotogrammi. Un gruppo di quattro video clip di
questo tipo verrebbe memorizzato in un tensore di forma (4, 240, 144, 256, 3).
Si tratta di un totale di 106.168.320 valori! Se il
Rappresentazioni di dati per reti neurali 39

Il tipo di tensore era float32, ogni valore sarebbe stato memorizzato in 32 bit,
quindi il tensore avrebbe rappresentato 405 MB. Pesante! I video che si incontrano nella
vita reale sono molto più leggeri, perché non sono memorizzati in float32 e sono
tipicamente compressi da un grande fattore (come nel formato MPEG).

2.3 Gli ingranaggi delle reti neurali: Operazioni tensoriali


Così come qualsiasi programma per computer può essere ridotto a un piccolo
insieme di operazioni binarie su ingressi binari (AND, OR, NOR e così via), tutte le
trasformazioni apprese dalle reti neurali profonde possono essere ridotte a una manciata di
operazioni tensoriali (o funzioni tensoriali) applicate a tensori di dati numerici. Ad
esempio, è possibile sommare tensori, moltiplicare tensori e così via.
Nel nostro esempio iniziale, abbiamo costruito il nostro modello impilando livelli
densi uno sopra l'altro. Un'istanza di livello Keras ha questo aspetto:

keras.layers.Dense(512, attivazione="relu")

Questo strato può essere interpretato come una funzione che prende in input una
matrice e restituisce un'altra matrice, una nuova rappresentazione del tensore di
input. In particolare, la funzione è la seguente (dove W è una matrice e b è un
vettore, entrambi attributi dello strato):

output = relu(dot(input, W) + b)

Scomponiamolo. Abbiamo tre operazioni tensoriali:


◾ Un prodotto di punti (dot) tra il tensore di ingresso e un tensore chiamato W
◾ Un'addizione (+) tra la matrice risultante e un vettore b
◾ Un'operazione di relu: relu(x) è max(x, 0); "relu" sta per "unità lineare rettificata".

NOTA Sebbene questa sezione tratti interamente di espressioni di algebra


lineare, qui non si troverà alcuna notazione matematica. Ho scoperto che i
concetti matematici possono essere padroneggiati più facilmente da
programmatori senza background matematico se vengono espressi sotto
forma di brevi frammenti Python invece che di equazioni matematiche. Per
questo motivo, in tutto il testo useremo codice NumPy e TensorFlow.

2.3.1 Operazioni elementari


L'operazione relu e l'addizione sono operazioni element-wise: operazioni che
vengono applicate in modo indipendente a ogni voce dei tensori considerati. Ciò
significa che queste operazioni sono molto adatte a implementazioni massicciamente
parallele (implementazioni vettoriali, un termine che deriva dall'architettura dei
supercomputer a processore vettoriale del periodo 1970-90). Se si vuole scrivere
un'implementazione Python ingenua di un'operazione elementare, si usa un ciclo for,
come in questa implementazione ingenua di un'operazione relu elementare:

def naive_relu(x): x è un tensore


assert len(x.shape) == 2 NumPy di rango 2.
Gli ingranaggi delle reti neurali: Operazioni 39
tensoriali
x = x.copy()
Evita di
per i in range(x.shape[0]): sovrascrivere il
per j in range(x.shape[1]): tensore di
x[i, j] = max(x[i, j], 0) ingresso.
restituire x

Si può fare lo stesso per l'addizione:

def naive_add(x, y): x e y sono tensori


assert len(x.shape) == 2 NumPy di rango
assert x.shape == y.shape 2.
x = x.copy()
per i in range(x.shape[0]): Evita di
per j in range(x.shape[1]): sovrascrivere il
x[i, j] += y[i, j] tensore di
ingresso.
restituire x

In base allo stesso principio, è possibile eseguire moltiplicazioni, sottrazioni e così via.
In pratica, quando si ha a che fare con gli array NumPy, queste operazioni sono
disponibili come funzioni integrate NumPy ben ottimizzate, che a loro volta delegano il
lavoro pesante a un'implementazione di Basic Linear Algebra Subprograms (BLAS). I
BLAS sono routine di manipolazione dei tensori di basso livello, altamente parallele ed
efficienti, che vengono tipicamente implementate
in Fortran o C.
Quindi, in NumPy, è possibile eseguire la seguente operazione element-wise e sarà
velocissima:

importare numpy Addizione elementare


come np z = x + y
z = np.maximum(z, 0.) Rilievo elementare

Vediamo di cronometrare la differenza:

tempo di importazione

x = np.random.random((20, 100))
y = np.random.random((20, 100))

t0 = time.time()
per _ in range(1000):
z = x + y
z = np.maximum(z, 0.)
print("Preso: {0:.2f} s".format(time.time() - t0))

Questo richiede 0,02 s. Mentre la versione ingenua richiede ben 2,45 s:

t0 = time.time()
per _ in range(1000):
z = naive_add(x, y)
z = naive_relu(z)
print("Preso: {0:.2f} s".format(time.time() - t0))
40 CAPITOLO 2 Gli elementi matematici delle reti neurali

Allo stesso modo, quando si esegue il codice TensorFlow su una GPU, le operazioni
elementari vengono eseguite tramite implementazioni CUDA completamente
vettoriali, in grado di utilizzare al meglio l'architettura del chip GPU altamente par-
ticolare.

2.3.2 Trasmissione
La nostra precedente implementazione ingenua di naive_add supporta solo l'aggiunta di
tensori di rango 2 con forme identiche. Ma nel livello Dense introdotto in
precedenza, abbiamo aggiunto un tensore di rango 2 con un vettore. Cosa succede
con l'addizione quando le forme dei due tensori da aggiungere sono diverse?
Quando è possibile, e se non ci sono ambiguità, il tensore più piccolo viene
trasmesso in modo che corrisponda alla forma del tensore più grande. La trasmissione
consiste in due fasi:
1 Al tensore più piccolo vengono aggiunti degli assi (chiamati assi di trasmissione)
che corrispondono all'ndim del tensore più grande.
2 Il tensore più piccolo viene ripetuto accanto a questi nuovi assi per ottenere la
forma completa del tensore più grande.
Vediamo un esempio concreto. Consideriamo X con forma (32, 10) e y con forma
(10,):
X è una matrice casuale
di forma (32, 10).
importare numpy come np
X = np.random.random((32, y è un vettore
10)) y = casuale di forma
np.random.random((10,)) (10,).

Per prima cosa, aggiungiamo un primo asse vuoto a y, la cui forma diventa (1, 10):

y = np.expand_dims(y, axis=0) La forma di y è


ora (1, 10).

Poi, ripetiamo y 32 volte lungo questo nuovo asse, in modo da ottenere un tensore Y
con forma (32, 10), dove Y[i, :] == y per i in range(0, 32):

Y = np.concatenate([y] * 32, asse=0) Ripetere y 32 volte lungo l'asse 0


per ottenere Y, che ha forma (32,
10).

A questo punto, possiamo procedere a sommare X e Y, perché hanno la stessa forma.


In termini di implementazione, non viene creato un nuovo tensore di rango 2,
perché sarebbe terribilmente inefficiente. L'operazione di ripetizione è interamente
virtuale: avviene a livello algoritmico e non a livello di memoria. Ma pensare che il
vettore venga ripetuto 10 volte accanto a un nuovo asse è un modello mentale utile.
Ecco come apparirebbe un'implementazione ingenua:
x è un tensore
def naive_add_matrix_and_vector(x, y): NumPy di rango 2.
assert len(x.shape) == 2 assert
len(y.shape) == 1 assert y è un vettore NumPy.
x.shape[1] == y.shape[0] x =
x.copy()
Evita di
per i in range(x.shape[0]): sovrascrivere il
Gli ingranaggi delle reti neurali: Operazioni 41
tensoriali

tensore di ingresso.
42 CAPITOLO 2 Gli elementi matematici delle reti neurali

per j in range(x.shape[1]):
x[i, j] += y[j]
restituire x

Con il broadcasting, è generalmente possibile eseguire operazioni element-wise che


prendono due tensori in ingresso se un tensore ha forma (a, b, ... n, n + 1, ... m)
e l'altro ha forma (n, n + 1, ... m). La trasmissione avverrà quindi
automaticamente per gli assi da a a n - 1.
L'esempio seguente applica l'operazione di massimo elementare a due tensori di forma
diversa tramite la trasmissione:

importare numpy come np x è un tensore casuale di forma


x = np.random.random((64, 3, 32, 10))
(64, 3, 32, 10).
y = np.random.random((32, 10))
y è un tensore
z = np.maximum(x, y) casuale di forma
L'uscita z ha forma (64, (32, 10).
3, 32, 10) come x.

2.3.3 Prodotto tensoriale


Il prodotto tensoriale, o prodotto dei punti (da non confondere con il prodotto elemento-saggio,
il prodotto
*), è una delle operazioni tensoriali più comuni e utili.
In NumPy, il prodotto tensoriale viene eseguito con la funzione np.dot (perché la
notazione matematica per il prodotto tensoriale è solitamente un punto):

x = np.random.random((32,))
y = np.random.random((32,))
z = np.dot(x, y)

In notazione matematica, l'operazione viene indicata con un punto (-):

z = x - y

Dal punto di vista matematico, a cosa s e r v e l'operazione con il punto? Cominciamo


con il prodotto del punto di due vettori, x e y. Si calcola come segue:

def naive_vector_dot(x, y):


assert len(x.shape) == 1 x e y sono
assert len(y.shape) == 1 vettori NumPy.
assert x.shape[0] == y.shape[0]
z = 0.
per i in range(x.shape[0]):
z += x[i] * y[i]
restituire z

Avrete notato che il prodotto del punto tra due vettori è uno scalare e che solo i
vettori con lo stesso numero di elementi sono compatibili per un prodotto del punto.
È anche possibile eseguire il prodotto di punti tra una matrice x e un vettore y, che
restituisce un vettore in cui i coefficienti sono i prodotti di punti tra y e le righe di x. Si
implementa come segue:
Gli ingranaggi delle reti neurali: Operazioni 43
tensoriali
def naive_matrix_vector_dot(x, y):
assert len(x.shape) == 2 assert
x è una matrice NumPy.
len(y.shape) == 1 assert
y è un vettore NumPy.
x.shape[1] == y.shape[0] z =
np.zeros(x.shape[0]) La prima dimensione di
per i in range(x.shape[0]): x deve essere uguale alla
per j in range(x.shape[1]): dimensione 0 di y!
z[i] += x[i, j] * y[j]
restituire z Questa operazione restituisce
un vettore di 0 con la stessa
forma di y.

Si può anche riutilizzare il codice scritto in precedenza, che evidenzia la relazione


tra un prodotto matrice-vettore e un prodotto vettoriale:

def naive_matrix_vector_dot(x, y):


z = np.zeros(x.shape[0])
per i in range(x.shape[0]):
z[i] = naive_vector_dot(x[i, :], y)
restituire z

Si noti che non appena uno dei due tensori ha un ndim maggiore di 1, dot non è più
simmetrico, il che significa che dot(x, y) non è uguale a dot(y, x).
Naturalmente, il prodotto di punti si generalizza a tensori con un numero arbitrario
di assi. L'applicazione più comune è il prodotto di punti tra due matrici. È possibile
prendere il prodotto di punti di due matrici x e y (dot(x, y)) se e solo se x.shape[1]
== y.shape[0]. Il risultato è una matrice con forma (x.shape[0], y.shape[1]),
dove i coefficienti sono i prodotti vettoriali tra le righe di x e le colonne di y. Ecco
l'implementazione ingenua:

def naive_matrix_dot(x, y):


assert len(x.shape) == 2
assert len(y.shape) == 2 x e y sono matrici NumPy.
assert x.shape[1] == y.shape[0] La prima dimensione di x deve essere la
z = np.zeros((x.shape[0], y.shape[1])) uguale alla dimensione 0 di y!
Questa per i in range(x.shape[0]):
operazione per j in range(y.shape[1]): Itera le righe di x . . .
restituisce riga_x = x[i, :]
. . . e sulle colonne di y.
una matrice colonna_y = y[:, j]
di 0 con una z[i, j] = naive_vector_dot(riga_x, colonna_y)
forma restituire z
specifica.
Per comprendere la compatibilità di forma del prodotto di punti, è utile visualizzare i
tensori di ingresso e di uscita allineandoli come mostrato nella figura 2.5.
Nella figura, x, y e z sono rappresentati come rettangoli (scatole letterali di
coefficienti). Poiché le righe di x e le colonne di y devono avere la stessa dimensione,
ne consegue che la larghezza di x deve corrispondere all'altezza di y. Se svilupperete
nuovi algoritmi di apprendimento automatico, probabilmente disegnerete spesso
diagrammi di questo tipo.
44 CAPITOLO 2 Gli elementi matematici delle reti neurali

y.shape:
(b, c)
x-y=z

b Colonna di
y

x.shape: z.shape:
(a, b) (a, c)
a
z [ i, j ] Figura 2.5 Diagramma
Riga di x matriciale del prodotto a
punti

Più in generale, è possibile eseguire il prodotto di punti tra tensori di dimensioni


superiori, seguendo le stesse regole di compatibilità di forma descritte in precedenza per
il caso 2D:

(a, b, c, d) - (d,) → (a, b, c)


(a, b, c, d) - (d, e) → (a, b, c, e)

E così via.

2.3.4 Rimodellamento dei tensori


Un terzo tipo di operazione sui tensori che è essenziale comprendere è il
rimodellamento dei tensori. Sebbene non sia stata utilizzata negli strati densi nel
nostro primo esempio di rete neurale, l'abbiamo usata quando abbiamo preelaborato i
dati delle cifre prima di inserirli nel nostro modello:

train_images = train_images.reshape((60000, 28 * 28))

Rimodellare un tensore significa riorganizzarne le righe e le colonne in modo che


corrispondano alla forma desiderata. Naturalmente, il tensore rimodellato ha lo stesso
numero totale di coefficienti del tensore iniziale. Il rimodellamento si comprende
meglio con semplici esempi:

>>> x = np.array([[0., 1.]


[2., 3.],
[4., 5.]])
>>> x.shape
(3, 2)
>>> x = x.reshape((6, 1))
>>> x
array([[ 0.]
[ 1.],
[ 2.],
[ 3.],
[ 4.],
[ 5.]])
Gli ingranaggi delle reti neurali: Operazioni 45
tensoriali
>>> x = x.reshape((2, 3))
>>> x
array([[ 0., 1., 2.],
[ 3., 4., 5.]])

Un caso particolare di rimodellamento che si incontra comunemente è la trasposizione.


Trasporre una matrice significa scambiarne le righe e le colonne, in modo che x[i, :]
diventi x[:, i]:

>>> x = np.zeros((300, 20))


Crea una
>>> x = np.transpose(x) matrice di
>>> x.shape tutti gli zeri di
(20, 300) forma (300, 20)

2.3.5 Interpretazione geometrica delle operazioni sui tensori


Poiché i contenuti dei tensori manipolati dalle operazioni tensoriali possono essere
inter- preti come coordinate di punti in uno spazio geometrico, tutte le operazioni
tensoriali hanno un'interpretazione geometrica. Per esempio, consideriamo
l'addizione. Inizieremo con il vettore seguente:

A = [0.5, 1]

È un punto in uno spazio 2D (vedi figura 2.6). È consuetudine immaginare un vettore


come una freccia che collega l'origine al punto, come mostrato nella figura 2.7.

1 A [0.5, 1] 1 A [0.5, 1]

1 1

Figura 2.6 Un punto in Figura 2.7 Un punto in


uno spazio 2D uno spazio 2D
rappresentato come una
freccia

Consideriamo un nuovo punto, B = [1, 0,25], che aggiungeremo al precedente.


Questo viene fatto geometricamente concatenando le frecce dei vettori, e il punto
risultante è il vettore che rappresenta la somma dei due vettori precedenti (vedi figura
2.8). Come si vede, l'aggiunta di un vettore B a un vettore A rappresenta l'azione di
copiare il punto A in una nuova posizione, la cui distanza e direzione dal punto originale
A è determinata dal vettore B. Se si applica la stessa addizione vettoriale a un gruppo di
punti nel piano (un "oggetto"), si crea una copia dell'intero oggetto in una nuova
posizione (si veda la figura 2.8).
46 CAPITOLO 2 Gli elementi matematici delle reti neurali

A+B

1 A

B
1
Figura 2.8 Interpretazione
geometrica della somma di due
vettori

figura 2.9). L'addizione tensoriale rappresenta quindi l'azione di traslare un oggetto


(spostandolo senza d i s t o r c e r l o ) di una certa quantità in una certa direzione.

Fattore orizzontale x
Fattore verticale + y

K Fattore verticale
K Figura 2.9
Traslazione 2D
Fattore orizzontale come
addizione
vettoriale

In generale, le operazioni geometriche elementari come traslazione, rotazione,


scalatura, inclinazione e così via possono essere espresse come operazioni
tensoriali. Ecco alcuni esempi:
◾ Traduzione: Come si è appena visto, l'aggiunta di un vettore a un punto lo sposta
di una quantità fissa in una direzione fissa. Applicata a un insieme di punti (come
un oggetto 2D), questa operazione viene chiamata "traslazione" (vedi figura 2.9).
◾ Rotazione: Una rotazione in senso antiorario di un vettore 2D di un angolo theta (vedi
figura 2.10) può essere ottenuta mediante un prodotto di punti con una matrice 2 × 2
R = [[cos(theta),
-sin(theta)], [sin(theta), cos(theta)]].

cos(theta) -sin(theta) x
sin(theta) cos(theta) y

K K Theta
Figura 2.10 Rotazione
2D (in senso antiorario)
Gli ingranaggi delle reti neurali: Operazioni 47
tensoriali

come prodotto dei


punti
48 CAPITOLO 2 Gli elementi matematici delle reti neurali

◾ Scalatura: È possibile ottenere un ridimensionamento verticale e orizzontale


dell'immagine (vedere la figura 2.11) mediante un prodotto di punti con una matrice
2 × 2 S = [[fattore_orizzontale, 0], [0, fattore_verticale]]
(si noti che una matrice di questo tipo è chiamata "matrice diagonale", perché ha solo
coefficienti non nulli nella sua "diagonale", che va dall'alto a sinistra al basso a
destra).

K
10 x
0 -0.5 y

Figura 2.11
Scala 2D come
prodotto di
punti

◾ Trasformazione lineare : Un prodotto di punti con una matrice arbitraria


implementa una trasformazione lineare. Si noti che la scalatura e la rotazione,
elencate in precedenza, sono per definizione trasformazioni lineari.
◾ Trasformazione affine: Una trasformazione affine (vedi figura 2.12) è la
combinazione di una trasformazione lineare (ottenuta tramite un prodotto di punti
con una matrice) e di una traslazione (ottenuta tramite un'addizione vettoriale).
Come avrete capito, si tratta esattamente del calcolo y = W - x + b
implementato dallo strato Dense! Uno strato denso senza funzione di attivazione
è uno strato affine.

W-x+b

K Figura 2.12
Trasformazione affine nel
piano

◾ Strato denso con attivazione relu: Un'osservazione importante sulle


trasformazioni affini è che se si applicano molte di esse ripetutamente, si ottiene
comunque una trasformazione affine (quindi sarebbe bastato applicare una sola
trasformazione affine). Proviamo con due: affine2(affine1(x)) = W2 - (W1
- x + b1)
+ b2 = (W2 - W1) - x + (W2 - b1 + b2). Si tratta di una trasformazione affine in
cui la parte lineare è la matrice W2 - W1 e la parte di traslazione è il vettore W2 -
b1 + b2. Di conseguenza, una rete neurale multistrato costituita interamente da
Gli ingranaggi delle reti neurali: Operazioni 49
strati densi tensoriali
senza
50 CAPITOLO 2 Gli elementi matematici delle reti neurali

attivazioni equivarrebbe a un singolo strato denso. Questa rete neurale "profonda"


sarebbe solo un modello lineare sotto mentite spoglie! Per questo motivo
abbiamo bisogno di funzioni di attivazione, come la relu (vista in azione
nella figura 2.13). Grazie alle funzioni di attivazione, una catena di strati densi
può implementare trasformazioni geometriche non lineari molto complesse,
ottenendo spazi di ipotesi molto ricchi per le reti neurali profonde. Tratteremo
questa idea in modo più dettagliato nel prossimo capitolo.

relu(W - x + b)

K Figura 2.13
Trasformazione affine
seguita da attivazione
della relu

2.3.6 Un'interpretazione geometrica del deep learning


Avete appena imparato che le reti neurali sono costituite interamente da catene di
operazioni tensoriali e che queste operazioni tensoriali sono semplici trasformazioni
geometriche dei dati in ingresso. Ne consegue che è possibile interpretare una rete
neurale come una trasformazione geo- metrica molto complessa in uno spazio ad
alta dimensione, implementata attraverso una serie di semplici passaggi.
In 3D, può essere utile la seguente immagine mentale. Immaginate due fogli di carta
colorata: uno rosso e uno blu. Mettetene uno sopra l'altro. Ora accartocciateli
insieme fino a formare una pallina. Questa palla di carta accartocciata è il vostro
dato di ingresso, e ogni foglio di carta è una classe di dati in un problema di
classificazione. Il compito di una rete neurale è quello di trovare una trasformazione
della palla di carta che la renda non accartocciata, in modo da rendere le due classi
nuovamente separabili (vedi figura 2.14). Con l'apprendimento profondo, questa
trasformazione verrebbe implementata come una serie di semplici trasformazioni
dello spazio 3D, come quelle che si possono applicare alla palla di carta con le dita,
un movimento alla volta.

Figura 2.14 Scomposizione


di un complicato collettore di
dati

Scomporre le palline di carta è l'obiettivo dell'apprendimento automatico: trovare


rappresentazioni ordinate per manifesti di dati complessi e altamente piegati in spazi
ad alta dimensione (una piega è una superficie continua, come il nostro foglio di
Gli ingranaggi delle reti neurali: Operazioni 51
tensoriali
carta accartocciato). A questo punto si dovrebbe avere un'idea abbastanza chiara del
perché l'apprendimento profondo eccelle in questo campo: prende il
52 CAPITOLO 2 Gli elementi matematici delle reti neurali

L'approccio è quello di scomporre in modo incrementale una trasformazione


geometrica complicata in una lunga catena di trasformazioni elementari, che è più o
meno la strategia che un essere umano seguirebbe per districare una palla di carta.
Ogni strato di una rete profonda applica una trasformazione che districa un po' i
dati, e una pila di strati profondi rende praticabile un processo di districazione
estremamente complicato.

2.4 Il motore delle reti neurali: Ottimizzazione


basata sul gradiente
Come si è visto nella sezione precedente, ogni strato neurale del nostro primo
esempio di modello trasforma i dati di ingresso come segue:

output = relu(dot(input, W) + b)

In questa espressione, W e b sono tensori che sono attributi dello strato. Sono chiamati
i pesi o i parametri addestrabili dello strato (rispettivamente gli attributi kernel e bias).
Questi pesi contengono le informazioni apprese dal modello grazie all'esposizione ai
dati di addestramento.
Inizialmente, queste matrici di pesi vengono riempite con piccoli valori casuali (una fase
chiamata
inizializzazione casuale). Naturalmente, non c'è motivo di aspettarsi che
relu(dot(input, W))
+ b), quando W e b sono casuali, produrrà qualsiasi rappresentazione utile. Le
rappresentazioni risultanti sono prive di significato, ma sono un punto di partenza. Il
passo successivo è quello di regolare gradualmente questi pesi, sulla base di un segnale
di feedback. Questo aggiustamento graduale, chiamato anche addestramento, è
l'apprendimento di cui si occupa l'apprendimento automatico.
Questo avviene all'interno di quello che viene chiamato ciclo di allenamento, che
funziona come segue. Ripetete questi passaggi in un ciclo, finché la perdita non sembra
sufficientemente bassa:
1 Disegnare un gruppo di campioni di allenamento, x, e i corrispondenti target, y_true.
2 Eseguireil modello su x (una fase chiamata forward pass) per ottenere le previsioni,
y_pred.
3 Calcolo della perdita del modello sul lotto, una misura del disallineamento tra
y_pred e y_true.
4 Aggiornare tutti i pesi del modello in modo da ridurre leggermente la perdita su
questo lotto.
Alla fine si otterrà un modello che ha una perdita molto bassa sui dati di addestramento: una
bassa discrepanza tra le previsioni, y_pred, e gli obiettivi previsti, y_true. Il modello ha
"imparato" a mappare i suoi input verso gli obiettivi corretti. Da lontano può
sembrare una magia, ma quando si riduce il tutto a passi elementari, si scopre che è
semplice.
Il passo 1 sembra abbastanza facile: basta il codice di I/O. I passi 2 e 3 non sono
altro che l'applicazione di una manciata di operazioni sui tensori, per cui si potrebbero
implementare questi passi solo sulla base di quanto appreso nella sezione precedente.
La parte difficile è il passo 4: l'aggiornamento dei pesi del modello. Dato un
Gli ingranaggi delle reti neurali: Operazioni 53
coefficiente di pesotensoriali
individuale nel modello, come si può calcolare se il coefficiente
deve essere aumentato o diminuito e di quanto?
Una soluzione ingenua potrebbe essere quella di congelare tutti i pesi del modello,
tranne quello del coefficiente di rischio considerato, e provare diversi valori per questo
coefficiente. Diciamo che
Il motore delle reti neurali: Ottimizzazione basata sul gradiente 49

il valore iniziale del coefficiente è 0,3. Dopo il forward pass su un batch di dati, la
perdita del modello sul batch è di 0,5. Se si modifica il valore del coefficiente a 0,35 e si
esegue nuovamente il forward pass, la perdita aumenta a 0,6. Se si modifica il valore del
coefficiente a 0,35 e si esegue nuovamente il forward pass, la perdita aumenta a 0,6. Ma
se si abbassa il coefficiente a 0,25, la perdita aumenta. Se invece si abbassa il
coefficiente a 0,25, la perdita scende a 0,4. In questo caso, sembra che l'aggiornamento
del coefficiente di -0,05 contribuisca a minimizzare la perdita. Questa operazione
dovrebbe essere ripetuta per tutti i coefficienti del modello.
Ma un approccio di questo tipo sarebbe terribilmente inefficiente, perché
bisognerebbe calcolare due passaggi in avanti (che sono costosi) per ogni singolo
coefficiente (di cui ce ne sono molti, di solito migliaia e a volte fino a milioni).
Fortunatamente, esiste un approccio molto migliore: la discesa del gradiente.
La discesa del gradiente è la tecnica di ottimizzazione che alimenta le moderne
reti neurali. Ecco il nocciolo della questione. Tutte le funzioni utilizzate nei nostri
modelli (come il punto o il +) trasformano il loro input in modo fluido e continuo:
se si guarda a z = x + y, ad esempio, una piccola variazione di y si traduce in
una piccola variazione di z, e se si conosce la direzione della variazione di y, si può
dedurre la direzione della variazione di z. Matematicamente si direbbe che queste
funzioni sono differenziabili. Se si concatenano tali funzioni, la funzione più grande che
si ottiene è ancora differenziabile. In particolare, questo vale per la funzione che
mappa i coefficienti del modello alla perdita del modello su un gruppo di dati: una
piccola variazione dei coefficienti del modello si traduce in una piccola e
prevedibile variazione del valore della perdita. Ciò consente di utilizzare un
operatore matematico chiamato gradiente per descrivere come varia la perdita
quando si spostano i coefficienti del modello in direzioni diverse. Se si calcola questo
gradiente, è possibile utilizzarlo per spostare i coefficienti (tutti insieme in un unico
aggiornamento, anziché uno alla volta) in una direzione che riduce la perdita.
Se sapete già cosa significa differenziabile e cosa è un gradiente, potete passare alla
sezione 2.4.3. Altrimenti, le due sezioni seguenti vi aiuteranno a capire questi concetti.
Altrimenti, le due sezioni seguenti vi aiuteranno a comprendere questi concetti.

2.4.1 Cos'è un derivato?


Consideriamo una funzione continua e liscia f(x) = y, che mappa un numero, x, in
un nuovo numero, y. Possiamo usare la funzione della figura 2.15 come esempio.

y = f(x)
y
Figura 2.15 Un continuo,
x funzione liscia

Poiché la funzione è continua, una piccola variazione di x può comportare solo una
piccola variazione di y: questa è l'intuizione alla base della continuità.
50 CAPITOLO 2 Gli elementi matematici delle reti neurali
Supponiamo di aumentare x di un piccolo fattore, epsilon_x: ciò si traduce in una
piccola variazione di epsilon_y in y, come mostrato nella figura 2.16.
Il motore delle reti neurali: Ottimizzazione basata sul gradiente 51

epsilon_y

y = f(x) Figura 2.16 Con una funzione continua,


y una piccola variazione di x si traduce
epsilon_x
x in una piccola variazione di y.

Inoltre, poiché la funzione è liscia (la sua curva non presenta angoli bruschi), quando
epsilon_x è abbastanza piccolo, intorno a un certo punto p, è possibile approssimare
f come una funzione lineare di pendenza a, in modo che epsilon_y diventi a *
epsilon_x:

f(x + epsilon_x) = y + a * epsilon_x

Ovviamente, questa approssimazione lineare è valida solo quando x è sufficientemente vicino a p.


La pendenza a è chiamata la derivata di f in p. Se a è negativa, significa che un
piccolo aumento di x nell'intorno di p si tradurrà in una diminuzione di f(x) (come
mostrato nella figura 2.17), mentre se a è positiva, un piccolo aumento di x si tradurrà
in un aumento di f(x). Inoltre, il valore assoluto di a (la grandezza della derivata)
indica la velocità con cui avverrà questo aumento o questa diminuzione.

Lineare locale
approssimazion f, con
e di
pendenza a

y y = f(x)

x Figura 2.17 Derivata di f in p

Per ogni funzione differenziabile f(x) (differenziabile significa "derivabile": ad


esempio, le funzioni lisce e continue possono essere derivate), esiste una funzione
derivata f'(x), che mappa i valori di x alla pendenza dell'approssimazione lineare
locale di f in quei punti. Ad esempio, la derivata di cos(x) è -sin(x), la derivata di
f(x) = a * x è f'(x) = a, e così via.
La capacità di derivare le funzioni è uno strumento molto potente quando si tratta di
ottimizzazione, il compito di trovare i valori di x che minimizzano il valore di f(x). Se
si sta cercando di aggiornare x di un fattore epsilon_x per minimizzare f(x) e si
conosce la derivata di f, il lavoro è fatto: la derivata descrive completamente
l'evoluzione di f(x) al variare di x. Se si vuole ridurre il valore di f(x), basta
spostare x un po' nella direzione opposta rispetto alla derivata.
52 CAPITOLO 2 Gli elementi matematici delle reti neurali

2.4.2 Derivata di un'operazione tensoriale: Il gradiente


La funzione che abbiamo appena visto trasforma un valore scalare x in un altro valore
scalare y: si può tracciare come una curva in un piano 2D. Immaginate ora una funzione
che trasformi una tupla di scalari (x, y) in un valore scalare z: questa sarebbe
un'operazione vettoriale. Si potrebbe tracciare come una superficie 2D in uno spazio 3D
(indicizzata dalle coordinate x, y, z). Allo stesso modo, si possono immaginare
funzioni che prendono in ingresso matrici, funzioni che prendono in ingresso tensori di
rango 3, ecc.
Il concetto di derivata può essere applicato a qualsiasi funzione di questo tipo,
purché le superfici descritte siano continue e lisce. La derivata di un'operazione
tensoriale (o di una funzione tensoriale) è chiamata gradiente. I gradienti non sono
altro che la generalizzazione del concetto di derivata alle funzioni che hanno come
input i tensori. Ricordate che per una funzione scalare la derivata rappresenta la
pendenza locale della curva della funzione? Allo stesso modo, il gradiente di una
funzione tensoriale rappresenta la curvatura della superficie multidi- mensionale
descritta dalla funzione. Caratterizza il modo in cui l'uscita della funzione varia al
variare dei suoi parametri di ingresso.
Vediamo un esempio basato sull'apprendimento automatico. Consideriamo
◾ Un vettore di input, x (un campione in un set di dati)
◾ Una matrice, W (i pesi di un modello)
◾ Un obiettivo, y_true (ciò che il modello dovrebbe imparare ad associare a x)
◾ Una funzione di perdita, loss (intesa come misura del divario tra le previsioni
attuali del modello e y_true)
È possibile utilizzare W per calcolare un candidato target y_pred e quindi calcolare la perdita,
o mismatch, tra il candidato target y_pred e il target y_true:
Utilizziamo i pesi del modello, W,
y_pred = dot(W, x) per fare una previsione su x.
valore_perdita = perdita(y_pred,
Stimiamo quanto era
y_vero)
sbagliata la previsione.
Ora vorremmo utilizzare i gradienti per capire come aggiornare W in modo da rendere il valore
di perdita
più piccolo. Come possiamo farlo?
Dati gli input fissi x e y_true, le operazioni precedenti possono essere interpretate
come una funzione che mappa i valori di W (i pesi del modello) in valori di perdita:

valore_di_perdita f descrive la curva (o superficie ad alta


= f(W) dimensionalità) formata dai valori di perdita
al variare di W.

Supponiamo che il valore attuale di W sia W0. Allora la derivata di f nel punto W0 è un
tensore grad(valore_perdita, W0), con la stessa forma di W, dove ogni coefficiente
grad(valore_perdita, W0)[i, j] indica la direzione e l'entità della variazione del
valore_perdita che si osserva modificando W0[i, j]. Il tensore
grad(loss_value, W0) è il gradiente della funzione f(W) = loss_value in W0,
chiamato anche "gradiente del loss_value rispetto a W nell'intorno di W0".
Il motore delle reti neurali: Ottimizzazione basata sul gradiente 53

Derivate parziali
L'operazione tensoriale grad(f(W), W) (che prende in ingresso una matrice W)
può essere espressa come una combinazione di funzioni scalari,
grad_ij(f(W), w_ij), ognuna delle quali restituisce la derivata di loss_value
= f(W) rispetto al coefficiente W[i, j] di W, assumendo che tutti gli altri
coefficienti siano costanti. grad_ij è chiamata la derivata parziale di f rispetto a
W[i, j].

Concretamente, cosa rappresenta grad(loss_value, W0)? Si è visto in precedenza che la


derivata di una funzione f(x) a singolo coefficiente può essere interpretata come la
pendenza della curva di f. Allo stesso modo, grad(loss_value, W0) può essere
interpretato come il tensore che descrive la direzione della salita più ripida di
loss_value = f(W) intorno a W0, nonché la pendenza di questa salita. Ogni derivata
parziale descrive la pendenza di f in una direzione specifica.
Per questo motivo, allo stesso modo in c u i , per una funzione f(x), si può ridurre il
valore di f(x) spostando x un po' nella direzione opposta alla derivata, con una
funzione f(W) di un tensore, si può ridurre il valore_perdita = f(W) spostando W
nella direzione opposta al gradiente: per esempio, W1 = W0 - step * grad(f(W0),
W0) (dove step è un piccolo fattore di scala). Ciò significa andare contro la direzione
di forte ascesa di f, che intuitivamente dovrebbe collocarsi più in basso sulla curva. Si
noti che il fattore di scala step è necessario perché grad(loss_value, W0)
approssima la curva solo quando si è vicini a W0, quindi non si vuole allontanarsi troppo
da W0.

2.4.3 Discesa stocastica del gradiente


Data una funzione differenziabile, è teoricamente possibile trovare il suo minimo in
modo analitico: è noto che il minimo di una funzione è un punto in cui la derivata è
0, quindi basta trovare tutti i punti in cui la derivata va a 0 e verificare per quale di
questi punti la funzione ha il valore minimo.
Applicato a una rete neurale, ciò significa trovare analiticamente la combinazione di
valori di peso che produce la funzione di perdita più piccola possibile. Ciò può essere
fatto risolvendo l'equazione grad(f(W), W) = 0 per W. Si tratta di un'equazione
polinomiale di N variabili, dove N è il numero di coefficienti del modello. Anche se
sarebbe possibile risolvere tale equazione per N = 2 o N = 3, farlo è intrattabile per
le reti neurali reali, dove il numero di parametri non è mai inferiore a qualche migliaio e
spesso può essere di diverse decine di milioni.
Si può invece utilizzare l'algoritmo in quattro fasi descritto all'inizio di questa
sezione: modificare i parametri poco alla volta in base al valore di perdita corrente
per un gruppo casuale di dati. Poiché si tratta di una funzione differenziabile, è
possibile calcolare il suo gradiente, il che consente di implementare in modo
efficiente il passaggio 4. Se si aggiornano i pesi nella direzione opposta al gradiente,
la perdita sarà ogni volta leggermente inferiore:
1Disegnare un gruppo di campioni di allenamento, x, e i corrispondenti target, y_true.
2Eseguire il modello su x per ottenere le previsioni, y_pred (questo è chiamato passaggio in
avanti).
54 CAPITOLO 2 Gli elementi matematici delle reti neurali

3 Calcolare la perdita del modello sul lotto, una misura della mancata
corrispondenza tra y_pred e y_true.
4 Calcolare il gradiente della perdita rispetto ai parametri del modello (questo è il
cosiddetto backward pass).
5 Spostare i parametri un po' nella direzione opposta al gradiente, ad esempio W -=
learning_rate * gradient, riducendo così un po' la perdita sul batch. Il
tasso di apprendimento (learning_rate in questo caso) è un fattore scalare
che modula la "velocità" del processo di discesa del gradiente.
Abbastanza facile! Quello che abbiamo appena descritto si chiama mini-batch stochastic
gradient descent (mini-batch SGD). Il termine stocastico si riferisce al fatto che ogni
lotto di dati viene estratto a caso (stocastico è un sinonimo scientifico di casuale). La
Figura 2.18 illustra ciò che accade in 1D, quando il modello ha un solo parametro e si
dispone di un solo campione di allenamento.

Valo Tasso di apprendimento


re di
Avvio
perdit
a
punto (t=0)

t=1

t=2
t=3

Valore del Figura 2.18 SGD lungo una


parame curva di perdita 1D (un
tro parametro apprendibile)

Come si può notare, intuitivamente è importante scegliere un valore ragionevole per il


fattore tasso_di_apprendimento. Se è troppo piccolo, la discesa lungo la curva
richiederà molte iterazioni e potrebbe bloccarsi in un minimo locale. Se il fattore
learning_rate è troppo grande, gli aggiornamenti potrebbero portare a posizioni
completamente casuali sulla curva.
Si noti che una variante dell'algoritmo SGD mini-batch consisterebbe nel disegnare
un singolo campione e bersaglio a ogni iterazione, anziché disegnare un lotto di dati.
Questo sarebbe un vero SGD (al contrario di un SGD mini-batch). In alternativa,
andando all'estremo opposto, si potrebbe eseguire ogni passo su tutti i dati disponibili, il
che si chiama batch gradient descent. Ogni aggiornamento sarebbe più accurato, ma
molto più costoso. Il compromesso efficiente tra questi due estremi consiste
nell'utilizzare mini-batch di dimensioni ragionevoli.
Sebbene la figura 2.18 illustri la discesa del gradiente in uno spazio dei
parametri 1D, nella pratica si utilizzerà la discesa del gradiente in spazi altamente
dimensionali: ogni coefficiente di peso in una rete neurale è una dimensione libera
nello spazio e possono essercene decine di migliaia o addirittura milioni. Per aiutare
l'intuizione delle superfici di perdita, si può anche visualizzare la discesa del gradiente
lungo una superficie di perdita 2D, come mostrato nella figura 2.19. Ma non è possibile
Il motore delle reti neurali: Ottimizzazione basata sul gradiente 55

visualizzare l'aspetto del processo di addestramento di una rete neurale.


56 CAPITOLO 2 Gli elementi matematici delle reti neurali

non è possibile rappresentare uno spazio a 1.000.000 di dimensioni in un modo che


abbia senso per gli esseri umani. Pertanto, è bene tenere presente che le intuizioni
sviluppate attraverso queste rappresentazioni a bassa dimensione potrebbero non
essere sempre accurate nella pratica. Questo aspetto è stato storicamente fonte di
problemi nel mondo della ricerca sull'apprendimento profondo.

Punto di
partenza
45
40
35
30
25
20
15
10
5

Figura 2.19 Discesa del


Punto gradiente su una superficie di
finale perdita 2D (due parametri
apprendibili)

Inoltre, esistono diverse varianti di SGD che si differenziano per il fatto di tenere conto
degli aggiornamenti precedenti dei pesi nel calcolo del prossimo aggiornamento dei
pesi, anziché limitarsi a considerare il valore attuale dei gradienti. Esiste, ad esempio,
l'SGD con il metodo del momen- to, così come l'Adagrad, l'RMSprop e molti altri. Tali
varianti sono note come metodi di ottimizzazione o ottimizzatori. In particolare, merita
attenzione il concetto di momento, utilizzato in molte di queste varianti. Il momentum
affronta due problemi con SGD: la velocità di convergenza e i minimi locali. Si
consideri la figura 2.20, che mostra la curva di una perdita in funzione di un parametro
del modello.

Valor
e di
perdit
a

Minimo
locale

Minimo
globale

Parametro Figura 2.20 Un minimo locale


valore e un minimo globale

Come si può notare, intorno a un certo valore del parametro si verifica un minimo locale:
Il motore delle reti neurali: Ottimizzazione basata sul gradiente 57
in quel punto, spostandosi verso sinistra la perdita aumenterebbe, ma anche spostandosi
verso destra.
58 CAPITOLO 2 Gli elementi matematici delle reti neurali

Se il parametro in esame viene ottimizzato tramite SGD con un piccolo tasso di


apprendimento, il processo di ottimizzazione potrebbe bloccarsi al minimo locale
invece di raggiungere il minimo globale.
È possibile evitare questi problemi utilizzando la quantità di moto, che trae
ispirazione dalla fisica. Un'immagine mentale utile è quella di pensare al processo di
ottimizzazione come a una pallina che rotola lungo la curva delle perdite. Se ha
abbastanza slancio, la palla non rimarrà bloccata in un burrone e finirà al minimo
globale. Lo slancio viene applicato spostando la pallina a ogni passo in base non
solo al valore attuale della pendenza (accelerazione attuale), ma anche alla velocità
attuale (risultante dall'accelerazione passata). In pratica, ciò significa aggiornare il
parametro w in base non solo al valore del gradiente corrente, ma anche
all'aggiornamento del parametro precedente, come in questa implementazione
ingenua:

past_velocity = 0. Fattore di quantità di moto costante


momento = 0,1 Anello di ottimizzazione
mentre la perdita
> 0,01:
w, perdita, gradiente = get_current_parameters()
velocità = velocità_passata * momento - tasso di apprendimento
* gradiente w = w + momento * velocità - tasso di
apprendimento * gradiente velocità_passata = velocità
aggiorna_parametro(w)

2.4.4 Concatenamento di derivati: L'algoritmo di Backpropagation


Nell'algoritmo precedente abbiamo ipotizzato che, essendo una funzione differenziata,
possiamo calcolare facilmente il suo gradiente. Ma è vero? Come possiamo calcolare il
gradiente di espressioni complesse nella pratica? Nel modello a due strati con cui
abbiamo iniziato il capitolo, come possiamo ottenere il gradiente della perdita rispetto ai
pesi? È qui che entra in gioco l'algoritmo di retropropagazione.
LA REGOLA DELLA CATENA
La retropropagazione è un modo per utilizzare le derivate di operazioni semplici (come
l'addizione, il relu o il prodotto tensoriale) per calcolare facilmente il gradiente di
combinazioni arbitrariamente complesse di queste operazioni atomiche. In particolare,
una rete neurale è costituita da molte operazioni tensoriali concatenate, ognuna delle
quali ha una derivata semplice e nota. Per esempio, il modello definito nel listato 2.2
può essere espresso come una funzione parametrizzata dalle variabili W1, b1, W2 e b2
(appartenenti rispettivamente al primo e al secondo strato Dense), che coinvolge le
operazioni atomiche dot, relu, softmax e +, nonché la nostra funzione di perdita
loss, tutte facilmente differenziabili:

loss_value = loss(y_true, softmax(dot(relu(dot(inputs, W1) + b1), W2) + b2))

Il calcolo ci dice che una tale catena di funzioni può essere derivata utilizzando la
seguente identità, detta regola della catena.
Consideriamo due funzioni f e g, nonché la funzione composta fg tale che
fg(x) == f(g(x)):
Il motore delle reti neurali: Ottimizzazione basata sul gradiente 59

def fg(x):
x1 = g(x)
y = f(x1)
return y

La regola della catena afferma che grad(y, x) == grad(y, x1) * grad(x1,


x). Questo permette di calcolare la derivata di fg a patto di conoscere le derivate di f
e g. La regola della catena si chiama così perché quando si aggiungono altre funzioni
intermedie, inizia a sembrare una catena:

def fghj(x):
x1 = j(x)
x2 = h(x1)
x
x3 = g(x2)
y = f(x3)
return y

grad(y, x) == (grad(y, x3) * grad(x3, x2) * W1 pun


grad(x2, x1) * grad(x1, x)) to

L'applicazione della regola della catena al calcolo dei


valori di gradiente di una rete neurale dà origine a un b1 +
algoritmo chiamato backpropagation. Vediamo come
funziona concretamente.
DIFFERENZIAZIONE AUTOMATICA CON GRAFICI DI CALCOLO relu
Un modo utile per pensare alla retropropagazione è
in termini di grafi di calcolo. Un grafo di calcolo è la
struttura dati al centro di TensorFlow e della
W2 pun
rivoluzione del deep learning in generale. Si tratta to
di un grafo aciclico diretto di operazioni, nel nostro
caso di operazioni tensoriali. Ad esempio, la figura
2.21 mostra la rappresentazione del grafo del nostro b2 +
primo modello.
I grafi di calcolo sono stati un'astrazione di
grande successo nell'informatica perché ci
softmax
permettono di trattare la computazione come un dato:
un'espressione computazionale è codificata come
una struttura di dati leggibile dalla macchina che
può essere usata come input o out- put di un altro y_true perd
ita
programma. Per esempio, si potrebbe immaginare
un programma che riceve un grafo di calcolo e
valore_perdita
restituisce un nuovo grafo di calcolo che
implementa una versione distribuita su larga scala
Figura 2.21
dello stesso calcolo: ciò significherebbe che si può Rappresentazione del
distribuire qualsiasi calcolo senza dover scrivere la grafico di calcolo del nostro
logica di distribuzione. Oppure immaginiamo un modello a due strati
programma che riceva un grafo di calcolo e possa
60 CAPITOLO 2 Gli elementi matematici delle reti neurali

genera automaticamente la derivata dell'espressione che rappresenta. È molto più facile


fare queste cose se il calcolo è espresso come una struttura dati esplicita di un grafo
piuttosto che, ad esempio, come righe di caratteri ASCII in un file .py.
Per spiegare chiaramente la retropropagazione, esaminiamo un esempio molto
elementare di grafico di com- putazione (vedi figura 2.22). Considereremo una versione
semplificata della figura 2.21, in cui abbiamo solo uno strato lineare e in cui tutte le
variabili sono scalari. Prendiamo due variabili scalari w e b, un ingresso scalare x, e
applichiamo loro alcune operazioni per combinarle in un'uscita y. Infine, applichiamo
una funzione di perdita dell'errore in valore assoluto: loss_val = abs(y_true -
y). Poiché vogliamo aggiornare w e b in modo da minimizzare loss_val, siamo
interessati a calcolare grad(loss_val, b) e grad(loss
_val, w).

w *

x1

b +

x2

y_true perd
ita

Figura 2.22 Esempio di base


valore_perdita di un grafo di calcolo

Impostiamo valori concreti per i "nodi di ingresso" del grafo, c i o è l'ingresso x, il target
y_true, w e b. Propagheremo questi valori a tutti i nodi d e l grafo, dall'alto verso il
basso, fino a raggiungere loss_val. Questo è il passaggio in avanti (vedi figura 2.23).
Ora "invertiamo" il grafico: per ogni spigolo del grafico che va da A a B, creiamo
uno spigolo opposto da B ad A e chiediamoci: quanto varia B al variare di A? Ovvero,
qual è il grad(B, A)? Annoteremo ogni bordo invertito con questo valore. Questo
grafico inverso rappresenta il passaggio all'indietro (vedi figura 2.24).
Il motore delle reti neurali: Ottimizzazione basata sul gradiente 61

2
x

3
w *

x1 = 6
1
b +

x2 = 7
4

y_true perd
ita

valore_perdita = 3 Figura 2.23 Esecuzione di un passaggio in avanti

2
x

3 grad(x1, w) = 2
w *

x1grad (x2, x1) = 1


1 grad(x2, b) = 1
b +

x2grad(loss_val, x2) = 1
4

y_true perdita

valore_p Figura 2.24 Esecuzione di un passaggio all'indietro


erdita
62 CAPITOLO 2 Gli elementi matematici delle reti neurali

Abbiamo quanto segue:


◾ grad(loss_val, x2) = 1, perché al variare di x2 di una quantità epsilon,
loss_val = abs(4 - x2) varia della stessa quantità.
◾ grad(x2, x1) = 1, perché al variare di x1 di una quantità epsilon, x2 = x1 +
b = x1 + 1 varia della stessa quantità.
◾ grad(x2, b) = 1, perché al variare di b di una quantità epsilon, x2 = x1 + b = 6 +
b
varia della stessa entità.
◾ grad(x1, w) = 2, perché al variare di w di una quantità epsilon, x1 = x * w
= 2 * w varia di 2 * epsilon.
La regola della catena dice che è possibile ottenere la derivata di un nodo rispetto a un altro
nodo moltiplicando le derivate per ogni bordo lungo il percorso che collega i due nodi. Per
esempio, grad(loss_val, w) = grad(loss_val, x2) * grad(x2, x1) *
grad(x1, w) (vedi figura 2.25).

2
x

3 grad(x1, w) = 2
w *

x1grad (x2, x1) = 1


1 grad(x2, b) = 1
b +

x2grad(loss_val, x2) = 1
4

y_true abs_diff

valore_perdita

Figura 2.25 Percorso da loss_val a w nel grafo retrospettivo

Applicando la regola della catena al nostro grafico, otteniamo ciò che stavamo cercando:
◾ grad(loss_val, w) = 1 * 1 * 2 = 2
◾ grad(loss_val, b) = 1 * 1 = 1
Il motore delle reti neurali: Ottimizzazione basata sul gradiente 63

NOTA Se ci sono più percorsi che collegano i due nodi di interesse, a e b, nel
grafo a ritroso, otterremmo grad(b, a) sommando i contributi di tutti i
percorsi.

E con questo abbiamo appena visto la retropropagazione in azione! La


retropropagazione è semplicemente l'applicazione della regola della catena a un grafo di
calcolo. Non c'è nulla di più. Il backpropagation parte dal valore di perdita finale e
lavora a ritroso dagli s t r a t i superiori a quelli inferiori, calcolando il contributo di
ciascun parametro a l valore di perdita. È da qui che deriva il nome "backpropagation":
"backpropagation" i contributi alle perdite dei diversi nodi di un grafo di calcolo.
Oggi le reti neurali vengono implementate in framework moderni che sono in
grado di effettuare la differenziazione automatica, come TensorFlow. La differenziazione
automatica è implementata con il tipo di grafico di calcolo appena visto. La
differenziazione automatica permette di recuperare i gradienti di composizioni arbitrarie
di operazioni tensoriali differenziabili senza fare altro lavoro oltre alla scrittura del
passaggio in avanti. Quando ho scritto le mie prime reti neurali in C negli anni
2000, dovevo scrivere i gradienti a mano. Ora, grazie ai moderni strumenti di
differenziazione automatica, non dovrete mai implementare da soli la
backpropagation. Consideratevi fortunati!
IL NASTRO DEL GRADIENTE IN TENSORFLOW
L'API attraverso la quale è possibile sfruttare le potenti capacità di differenziazione
automatica di TensorFlow è il GradientTape. Si tratta di uno scope Python che
"registra" le operazioni tensoriali eseguite al suo interno, sotto forma di un grafico di
calcolo (talvolta chiamato "nastro"). Questo grafico può essere usato per recuperare il
gradiente di qualsiasi risultato rispetto a qualsiasi variabile o insieme di variabili
(istanze della classe tf.Variable). Una tf.Variable è un tipo specifico di
tensore destinato a contenere uno stato mutabile: per esempio, i pesi di una rete neurale
sono sempre istanze di tf.Variable.
Istanziare una variabile
scalare con un valore Aprire un ambito GradientTape.
iniziale pari a 0.
All'interno dell'ambito,
importare tensorflow applicare alcune
come tf x = operazioni tensoriali
tf.Variable(0.) alla nostra variabile.
con tf.GradientTape() come
nastro: y = 2 * x + 3
Utilizzate il nastro per
grad_di_y_wrt_x = tape.gradient(y, x) recuperare il gradiente
dell'uscita y rispetto alla
variabile x.

Il GradientTape funziona con operazioni


tensoriali:
Istanziare una variabile con forma
(2, 2) e un valore iniziale di tutti zeri.
x = tf.Variabile(tf.random.uniform((2, 2)))
con tf.GradientTape() come
nastro: y = 2 * x + 3 grad_of_y_wrt_x è un tensore di forma (2,
grad_di_y_wrt_x = tape.gradient(y, x) 2) (come x) che descrive la curvatura di y =
2*a
+ 3 intorno a x = [[0, 0], [0, 0]].
Riprendendo il nostro primo 61
esempio
Funziona anche con elenchi di variabili:

W = tf.Variabile(tf.random.uniform((2, matmul è come dici tu


2))) b = tf.Variabile(tf.zeros((2,))) "Prodotto di punti" in TensorFlow.
x = tf.random.uniform((2, 2))
con tf.GradientTape() come nastro:
grad_of_y_wrt_W_e_b è un
y = tf.matmul(x, W) + b elenco di due tensori con lo stesso
valore di
grad_di_y_wrt_W_e_b = tape.gradient(y, [W, b]) forme come W e b, rispettivamente.

Il nastro gradiente verrà illustrato nel prossimo capitolo.

2.5 Riprendendo il nostro primo esempio


Siete quasi giunti alla fine di questo capitolo e dovreste avere una conoscenza
generale di ciò che accade dietro le quinte di una rete neurale. Quella che all'inizio
del capitolo era una magica scatola nera si è trasformata in un'immagine più chiara,
come illustrato nella figura 2.26: il modello, composto da strati concatenati, mappa i
dati di ingresso in previsioni. La funzione di perdita confronta quindi queste
previsioni con gli obiettivi, producendo un valore di perdita: una misura di quanto le
predizioni del modello corrispondano a quanto previsto. L'ottimizzatore utilizza
questo valore di perdita per aggiornare i pesi del modello.

Ingresso X

Strato
Pesi
(trasformazione dei
dati)

Strato
Pesi
(trasformazione dei
dati)

Peso Previsioni Obiettivi veri


aggior Y' Y
namen
to
Ottimizzat Funzione di
ore perdita
Figura 2.26 Relazione tra rete,
livelli, funzione di perdita e
Punteggio
di perdita ottimizzatore

Torniamo al primo esempio di questo capitolo e rivediamone ogni elemento alla


luce di quanto appreso in seguito.
Questi erano i dati di input:

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()


train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype("float32") / 255
62 CAPITOLO 2 Gli elementi matematici delle reti neurali

Ora si capisce che le immagini in ingresso sono memorizzate in tensori NumPy, qui
formattati come tensori float32 di forma (60000, 784) (dati di allenamento) e
(10000, 784) (dati di test) rispettivamente.
Questo era il nostro modello:

model = keras.Sequential([
layers.Dense(512, activation="relu"),
layers.Dense(10, activation="softmax")
])

Ora si capisce che questo modello consiste in una catena di due strati Dense, che ogni
strato applica alcune semplici operazioni tensoriali ai dati di ingresso e che queste
operazioni coinvolgono i tensori peso. I tensori dei pesi, che sono attributi degli
strati, sono il luogo in cui risiede la conoscenza del modello.
Questa era la fase di compilazione del modello:

model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])

Ora si è capito che sparse_categorical_crossentropy è la funzione di perdita


usata come segnale di feedback per l'apprendimento dei tensori di peso e che la fase di
addestramento cercherà di minimizzare. Si sa anche che questa riduzione della perdita
avviene tramite una discesa del gradiente stocastica in mini-batch. Le regole esatte che
governano un uso specifico della discesa del gradiente sono definite dall'ottimizzatore
rmsprop passato come primo argomento.
Infine, questo era il circuito di allenamento:

model.fit(train_images, train_labels, epochs=5, batch_size=128)

Ora si capisce cosa succede quando si chiama fit: il modello inizierà a iterare sui
dati di addestramento in mini-batch di 128 campioni, per 5 volte (ogni iterazione su
tutti i dati di addestramento è chiamata epoch). Per ogni lotto, il modello calcola il
gradiente della perdita rispetto ai pesi (utilizzando l'algoritmo di retropropagazione,
che deriva dalla regola della catena nel calcolo) e sposta i pesi nella direzione che
ridurrà il valore della perdita per questo lotto.
Dopo queste 5 epoche, il modello avrà eseguito 2.345 aggiornamenti del gradiente
(469 per epoca) e la perdita del modello sarà sufficientemente bassa da consentirgli di
classificare le cifre scritte a mano con elevata precisione.
A questo punto, sapete già la maggior parte di ciò che c'è da sapere sulle reti
neurali. Dimostriamolo reimplementando una versione semplificata del primo
esempio "da zero" in TensorFlow, passo dopo passo.
Riprendendo il nostro primo 63
esempio
2.5.1 Reimplementare da zero il nostro primo esempio in TensorFlow
Cosa dimostra meglio una comprensione completa e non ambigua se non
l'implementazione di ogni cosa da zero? Naturalmente, il significato di "da zero" è
relativo: non reimplementeremo le operazioni tensoriali di base e non
implementeremo la retropropagazione. Ma andremo a un livello così basso che non
useremo quasi nessuna funzionalità di Keras.
Non preoccupatevi se non capite ancora tutti i dettagli di questo esempio. Il
prossimo capitolo approfondirà l'API di TensorFlow. Per ora, cercate di seguire il
succo di ciò che sta accadendo: l'intento di questo esempio è quello di aiutarvi a
cristallizzare la comprensione della matematica del deep learning utilizzando
un'implementazione concreta. Andiamo!
UNA SEMPLICE CLASSE DENSA
Si è appreso in precedenza che il livello Dense implementa la seguente trasformazione
dell'input, dove W e b sono parametri del modello e l'attivazione è una funzione
elementare (di solito relu, ma sarebbe softmax per l'ultimo livello):

uscita = attivazione(punto(W, ingresso) + b)

Implementiamo una semplice classe Python, NaiveDense, che crea due variabili
TensorFlow, W e b, ed espone un metodo call () che applica la trasformazione
precedente.

importare tensorflow come tf


Crea una
classe NaiveDense: matrice, W, di
def init (self, input_size, output_size, activation): forma
self.activation = activation (input_size,
output_size),
w_shape = (dimensione_ingresso, dimensione_uscita) inizializzata
con valori
casuali.
w_initial_value = tf.random.uniform(w_shape, minval=0, maxval=1e-1)
self.W = tf.Variable(w_initial_value)
Creare un vettore, b, di forma
b_shape = (output_size, (output_size,), inizializzato con degli zeri.
b_initial_value = tf.zeros(b_shape)
self.b = tf.Variable(b_initial_value)
Applicare il passaggio in avanti.
def call (self, input)::
restituire self.activation(tf.matmul(input, self.W) + self.b)

@proprietà Metodo pratico per recuperare i


def pesi(self):
pesi degli strati
restituire [self.W, self.b]

UNA SEMPLICE CLASSE SEQUENZIALE


Ora, creiamo una classe NaiveSequential per concatenare questi livelli. Essa avvolge
un elenco di livelli ed espone un metodo call () che richiama semplicemente i livelli
sottostanti s u g l i ingressi, in ordine. Dispone anche di una proprietà weights per
tenere facilmente traccia dei parametri dei livelli.
64 CAPITOLO 2 Gli elementi matematici delle reti neurali

classe NaiveSequential:
def init (self, layers):
self.layers = layers

def call (self, input): x


= input
per layer in self.layers:
x = layer(x)
restituire x

@proprietà
def pesi(self):
pesi = []
per layer in self.layers:
pesi += layer.weights
restituire i pesi

Utilizzando questa classe NaiveDense e questa classe NaiveSequential, possiamo


creare un modello mock di Keras:

modello = NaiveSequential([
NaiveDense(input_size=28 * 28, output_size=512, attivazione=tf.nn.relu),
NaiveDense(input_size=512, output_size=10, attivazione=tf.nn.softmax)
])
assert len(model.weights) == 4

UN GENERATORE DI BATCH
Quindi, abbiamo bisogno di un modo per iterare sui dati MNIST in mini-batch. È facile:

Importazione di matematica

classe BatchGenerator:
def init (self, images, labels, batch_size=128):
assert len(immagini) ==
len(etichette) self.index = 0
self.images = immagini
self.labels = etichette
self.batch_size = batch_size
self.num_batches = math.ceil(len(images) / batch_size)

def next(self):
images = self.images[self.index : self.index + self.batch_size]
labels = self.labels[self.index : self.index + self.batch_size]
self.index += self.batch_size
restituire immagini, etichette

2.5.2 Esecuzione di una fase di allenamento


La parte più difficile del processo è la "fase di addestramento": aggiornare i pesi del
modello dopo averlo eseguito su un lotto di dati. È necessario
1 Calcolare le previsioni del modello per le immagini del lotto.
2 Calcolare il valore di perdita per queste previsioni, date le etichette reali.
Riprendendo il nostro primo 65
esempio
3 Calcolare il gradiente della perdita rispetto ai pesi del modello.
4 Spostare i pesi di una piccola quantità nella direzione opposta al gradiente.

Per calcolare il gradiente, utilizzeremo l'oggetto GradientTape di TensorFlow che


abbiamo introdotto nella sezione 2.4.4:

def one_training_step(modello, images_batch, labels_batch):


Eseguire il con tf.GradientTape() as tape:
"passaggio in predictions = model(images_batch)
avanti" per_sample_losses = tf.keras.losses.sparse_categorical_crossentropy(
(calcolare le labels_batch, predictions)
previsioni del
average_loss = tf.reduce_mean(per_sample_losses)
modello sotto un
gradients = tape.gradient(average_loss, model.weights)
GradientTape update_weights(gradienti, model.weights)
campo di
applicazione).
restituire media_perdita Calcolo del gradiente della perdita
rispetto ai pesi. I gradienti in uscita sono un
Aggiornare i pesi utilizzando i gradienti elenco in cui ogni voce corrisponde a
(questa funzione sarà definita a un peso dall'elenco model.weights.
breve).

Come già sapete, lo scopo del passo di "aggiornamento dei pesi" (rappresentato dalla
funzione update_weights) è quello di spostare i pesi di "un po'" in una direzione che
riduca la perdita su questo batch. L'entità dello spostamento è determinata dal "tasso di
apprendimento", in genere una piccola quantità. Il modo più semplice per implementare
questa funzione update_weights è sottrarre gradiente * learning_rate da ogni
peso:

tasso_di_apprendimento = 1e-3

def update_weights(gradienti, pesi): assign_sub è


per g, w in zip(gradienti, pesi): l'equivalente di -=
w.assign_sub(g * learning_rate) per le variabili di
TensorFlow.

In pratica, non si implementa quasi mai una fase di aggiornamento del peso come
questa a mano. Si userebbe invece un'istanza di Optimizer di Keras, come questa:

da tensorflow.keras import optimizers optimizer

= optimizers.SGD(learning_rate=1e-3)

def update_weights(gradienti, pesi):


optimizer.apply_gradients(zip(gradienti, pesi))

Ora che la fase di addestramento per lotto è pronta, possiamo passare


all'implementazione di un'intera epoca di addestramento.

2.5.3 Il ciclo di formazione completo


Un'epoca di addestramento consiste semplicemente nel ripetere la fase di
addestramento per ogni batch dei dati di addestramento, e l'intero ciclo di
addestramento è semplicemente la ripetizione di un'epoca:

def fit(modello, immagini, etichette, epochs, batch_size=128):


per epoch_counter in range(epochs):
66 CAPITOLO 2 Gli elementi matematici delle reti neurali

print(f "Epoca {epoch_counter}")


Riprendendo il nostro primo 67
esempio
batch_generator = BatchGenerator(immagini, etichette)
per batch_counter in range(batch_generator.num_batches):
images_batch, labels_batch = batch_generator.next()
loss = one_training_step(model, images_batch, labels_batch)
se batch_counter % 100 == 0:
print(f "perdita al lotto {batch_counter}: {perdita:.2f}")

Facciamo un giro di prova:

da tensorflow.keras.datasets importa mnist


(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

train_images = train_images.reshape((60000, 28 * 28))


train_images = train_images.astype("float32") / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype("float32") / 255

fit(modello, train_images, train_labels, epochs=10, batch_size=128)

2.5.4 Valutazione del modello


Possiamo valutare il modello prendendo l'argmax delle sue previsioni sulle immagini di
prova e confrontandolo con le etichette previste:

predictions = model(test_images)
predictions = predictions.numpy()
Chiamando .numpy() su
predicted_labels = np.argmax(predictions, un tensore TensorFlow lo si
axis=1) matches = predicted_labels == converte in un tensore
test_labels print(f "accuracy: NumPy.
{matches.mean():.2f}")

Tutto fatto! Come si può vedere, è un bel po' di lavoro fare "a mano" ciò che si può
fare in poche righe di codice Keras. Ma dopo aver seguito questi passaggi, ora dovreste
avere una comprensione cristallina di ciò che accade all'interno di una rete neurale
quando si chiama fit(). Questo modello mentale di basso livello di ciò che il vostro
codice fa dietro le quinte vi permetterà di sfruttare meglio le funzioni di alto livello
dell'API di Keras.

Sintesi
◾ I tensori costituiscono la base dei moderni sistemi di apprendimento automatico.
Sono disponibili in diverse varianti di tipo, grado e forma.
◾ È possibile manipolare i tensori numerici tramite operazioni sui tensori (come
l'addizione, il prodotto di tensori o la moltiplicazione elementare), che
possono essere interpretate come codifica di trasformazioni geometriche. In
generale, tutto ciò che riguarda il deep learning è suscettibile di
un'interpretazione geometrica.
◾ I modelli di apprendimento profondo sono costituiti da catene di semplici
operazioni tensoriali, parametrizzate da pesi, che sono a loro volta tensori. I pesi
di un modello sono il luogo in cui viene memorizzata la sua "conoscenza".
◾ Apprendere significa trovare un insieme di valori per i pesi del modello che
minimizzi una funzione di perdita per un dato insieme di campioni di dati di
addestramento e i loro corrispondenti obiettivi.
Sintesi 67

◾ L'apprendimento avviene estraendo a caso lotti di campioni di dati e i relativi


tar- get, e calcolando il gradiente dei parametri del modello rispetto alla
perdita sul lotto. I parametri del modello vengono quindi spostati un po'
(l'entità dello spostamento è definita dal tasso di apprendimento) nella
direzione opposta al gradiente. Questa procedura è chiamata mini-batch stochastic
gradient descent.
◾ L'intero processo di apprendimento è reso possibile dal fatto che tutte le
operazioni tensoriali nelle reti neurali sono differenziabili, e quindi è possibile
applicare la regola della derivazione a catena per trovare la funzione di gradiente
che mappa i parametri correnti e l'attuale gruppo di dati in un valore di
gradiente. Questa operazione è chiamata backpropagation.
◾ Due concetti chiave che si vedranno spesso nei prossimi capitoli sono le perdite e gli
ottimizzatori. Si tratta di due elementi da definire prima di iniziare a inserire i
dati in un modello.
– La perdita è la quantità che si cercherà di ridurre al minimo durante
l'allenamento, quindi dovrebbe rappresentare una misura del successo del
compito che si sta cercando di risolvere.
– L'ottimizzatore specifica il modo esatto in cui il gradiente della perdita verrà
utilizzato per aggiornare i parametri: ad esempio, potrebbe essere
l'ottimizzatore RMSProp, SGD con momentum e così via.
Introduzione a Keras
e TensorFlow

Questo capitolo tratta


◾ Uno sguardo più approfondito a
TensorFlow, Keras e alla loro relazione
◾ Impostazione di uno spazio di lavoro per
l'apprendimento profondo
◾ Una panoramica di come i concetti fondamentali
dell'apprendimento profondo si traducono in
Keras e TensorFlow.

Questo capitolo ha lo scopo di fornire tutto ciò che serve per iniziare a fare deep
learning nella pratica. Vi presenterò rapidamente Keras (https://keras.io) e
TensorFlow (https://tensorflow.org), gli strumenti di deep learning basati su
Python che utilizzeremo nel corso del libro. Scoprirete come impostare uno
spazio di lavoro per il deep learning, con TensorFlow, Keras e il supporto delle
GPU. Infine, sulla base del primo contatto con Keras e TensorFlow nel capitolo 2,
rivedremo i componenti fondamentali delle reti neurali e come si traducono nelle
API di Keras e TensorFlow.
Alla fine di questo capitolo, sarete pronti per passare alle applicazioni pratiche e
reali, che inizieranno nel capitolo 4.

68
Cos'è Keras? 69

3.1 Cos'è TensorFlow?


TensorFlow è una piattaforma di apprendimento automatico open source, gratuita e
basata su Python, sviluppata principalmente da Google. Come NumPy, lo scopo
principale di TensorFlow è quello di consentire a ingegneri e ricercatori di manipolare
espressioni matematiche su tensori numerici. Ma TensorFlow va ben oltre la portata di
NumPy nei seguenti modi:
◾ Può calcolare automaticamente il gradiente di qualsiasi espressione
differenziabile (come si è visto nel capitolo 2), il che lo rende molto adatto
all'apprendimento automatico.
◾ Può essere eseguito non solo su CPU, ma anche su GPU e TPU, acceleratori
hard ware altamente paralleli.
◾ Il calcolo definito in TensorFlow può essere facilmente distribuito su molte
macchine.
◾ I programmi TensorFlow possono essere esportati in altri runtime, come C++,
Java- Script (per applicazioni basate su browser), o TensorFlow Lite (per
applicazioni eseguite su dispositivi mobili o embedded), ecc. Ciò rende le
applicazioni TensorFlow facili da distribuire in contesti pratici.
È importante ricordare che TensorFlow è molto più di una singola libreria. È una
piattaforma, che ospita un vasto ecosistema di componenti, alcuni sviluppati da Google
e altri da terze p a r t i . Per esempio, c'è TF-Agents per la ricerca sull'apprendimento
rinforzato, TFX per la gestione del flusso di lavoro dell'apprendimento automatico,
TensorFlow Serving per la distribuzione in produzione e il repository TensorFlow Hub
di modelli pre-addestrati. Insieme, questi componenti coprono un'ampia gamma di casi
d'uso, dalla ricerca d'avanguardia alle applicazioni di produzione su larga scala.
TensorFlow ha una buona scalabilità: per esempio, gli scienziati dell'Oak Ridge
National Lab lo hanno usato per addestrare un modello di previsione meteorologica
estrema da 1,1 exaFLOPS sulle 27.000 GPU del supercomputer IBM Summit. Allo
stesso modo, Google ha utilizzato Tensor Flow per sviluppare applicazioni di deep
learning ad alta intensità di calcolo, come l'agente AlphaZero che gioca a scacchi e a
Go. Per i vostri modelli, se ne avete la possibilità, potete realisticamente sperare di
scalare a circa 10 petaFLOPS su un piccolo pod TPU o su un grande cluster di GPU
affittato su Google Cloud o AWS. Si tratterebbe comunque di circa
L'1% della potenza di calcolo di picco del miglior supercomputer del 2019!

3.2 Cos'è Keras?


Keras è un'API di deep learning per Python, costruita sulla base di TensorFlow, che fornisce
un modo semplice per definire e addestrare qualsiasi tipo di modello di deep
learning. Keras è stato inizialmente sviluppato per la ricerca, con l'obiettivo di
consentire una rapida sperimentazione del deep learning.
Grazie a TensorFlow, Keras può essere eseguito su diversi tipi di hardware (vedi
figura 3.1) -GPU, TPU o semplici CPU - e può essere scalato senza problemi a migliaia
di macchine.
Keras è noto per dare priorità all'esperienza degli sviluppatori. È un'API per
esseri umani, non per macchine. Segue le migliori pratiche per la riduzione del
carico cognitivo: offre
70 CAPITOLO 3 Introduzione a Keras e TensorFlow

Sviluppo dell'apprendimento
Keras profondo: livelli, modelli,
ottimizzatori, perdite, metriche...

Infrastruttura di manipolazione dei


TensorFlow tensori: tensori, variabili,
differenziazione automatica,
distribuzione...

CPU GPU TPU


Hardware: esecuzione

Figura 3.1 Keras e TensorFlow: TensorFlow è una piattaforma di calcolo tensoriale


di basso livello e Keras è un'API di deep learning di alto livello.

Il sistema è in grado di fornire flussi di lavoro semplici e coerenti, di ridurre al


minimo il numero di azioni necessarie per i casi d'uso più comuni e di fornire un
feedback chiaro e perseguibile in caso di errore da parte dell'utente. Tutto ciò rende
Keras facile da imparare per un principiante e altamente produttivo per un esperto.
Alla fine del 2021 Keras ha superato il milione di utenti, che vanno da
r i c e r c a t o r i accademici, ingegneri e data scientist di startup e grandi aziende
a studenti laureati e hobbisti. Keras è utilizzato da Google, Netflix, Uber, CERN,
NASA, Yelp, Instacart, Square e da centinaia di startup che lavorano su una vasta
gamma di problemi in tutti i settori. Le raccomandazioni di YouTube provengono da
modelli Keras. Le auto a guida autonoma di Waymo sono sviluppate con modelli Keras.
Keras è anche un framework popolare su Kaggle, il sito web dedicato alle
competizioni di apprendimento automatico, dove la maggior parte delle
competizioni di deep learning sono state vinte utilizzando Keras.
Poiché Keras ha una base di utenti ampia e diversificata, non costringe a seguire un
unico "vero" modo di costruire e addestrare i modelli. Piuttosto, consente un'ampia
gamma di flussi di lavoro diversi, dal livello più alto a quello più basso, corrispondenti a
diversi profili di utenti. Ad esempio, si ha una serie di modi per costruire i modelli e una
serie di modi per addestrarli, ognuno dei quali rappresenta un certo compromesso tra
usabilità e flessibilità. Nel capitolo 5 esamineremo in dettaglio una buona parte di
questo spettro di flussi di lavoro. Potreste usare Keras come fareste con Scikit-learn -
chiamando semplicemente fit() e lasciando che il framework faccia le sue cose -
oppure potreste usarlo come NumPy - prendendo il pieno controllo di ogni minimo
dettaglio.
Ciò significa che tutto ciò che state imparando ora, mentre state iniziando, s a r à
ancora rilevante quando sarete diventati esperti. Si può iniziare facilmente e poi
immergersi gradualmente in flussi di lavoro in cui si scrive sempre più logica da zero.
Non dovrete passare a un framework completamente diverso quando passerete da
studenti a ricercatori o da scienziati dei dati a ingegneri dell'apprendimento profondo.
Questa filosofia non è diversa da quella di Python stesso! Alcuni linguaggi
offrono un solo modo di scrivere programmi, ad esempio la programmazione
orientata agli oggetti o la programmazione funzionale. Invece Python è un linguaggio
multiparadigma: offre una serie di possibili modelli di utilizzo che funzionano tutti
insieme. Questo rende Python adatto a un'ampia gamma di casi d'uso molto diversi:
Cos'è Keras? 71
amministrazione di sistema, scienza dei dati, apprendimento automatico, ecc.
Impostazione di uno spazio di lavoro 71
per l'apprendimento profondo
ingegneria, sviluppo web . . o semplicemente per imparare a programmare. Allo
stesso modo, si può pensare a Keras come al Python del deep learning: un
linguaggio di deep learning facile da usare che offre una varietà di flussi di lavoro
per diversi profili di utenti.

3.3 Keras e TensorFlow: una breve storia


Keras precede TensorFlow di otto mesi. È stato rilasciato nel marzo 2015, mentre
TensorFlow è stato rilasciato nel novembre 2015. Ci si può chiedere: se Keras è
costruito sopra TensorFlow, come può esistere prima del rilascio di TensorFlow? Keras è
stato originariamente costruito sulla base di Theano, un'altra libreria di manipolazione
dei tensori che forniva differenziazione automatica e supporto per le GPU, la prima
del suo genere. Theano, sviluppato presso il Montréal Institute for Learning
Algorithms (MILA) dell'Università di Montréal, è stato per molti versi un precursore di
TensorFlow. È stato il pioniere dell'idea di utilizzare grafici di com- putazione statici per
la differenziazione automatica e per la compilazione del codice sia per la CPU che
per la GPU.
Alla fine del 2015, dopo il rilascio di TensorFlow, Keras è stato rifattorizzato in
un'architettura multiback end: è diventato possibile utilizzare Keras con Theano o
TensorFlow, e passare da uno all'altro è stato facile come cambiare una variabile
d'ambiente. A settembre 2016, TensorFlow aveva raggiunto un livello di maturità
tecnica tale da renderlo l'opzione backend predefinita per Keras. Nel 2017 sono state
aggiunte a Keras due nuove opzioni di backend: CNTK (sviluppato da Microsoft) e
MXNet (sviluppato da Amazon). Attualmente, sia Theano che CNTK sono fuori
sviluppo e MXNet non è molto utilizzato al di fuori di Amazon. Keras è tornato a essere
un'API single-backend, in cima a TensorFlow.
Keras e TensorFlow hanno avuto una relazione simbiotica per molti anni. Nel corso del
2016 e del 2017, Keras è diventato famoso come il modo più semplice per sviluppare
applicazioni TensorFlow, incanalando nuovi utenti nell'ecosistema TensorFlow.
Alla fine del 2017, la maggior parte degli utenti di TensorFlow lo utilizzava
attraverso Keras o in combinazione con Keras. Nel 2018, la leadership di TensorFlow ha
scelto Keras come API ufficiale di alto livello di TensorFlow. Di conseguenza, l'API
Keras è in primo piano in TensorFlow 2.0, rilasciato nel settembre 2019, una
riprogettazione completa di TensorFlow e Keras che tiene conto di oltre quattro anni
di feedback degli utenti e di progressi tecnici.
A questo punto, non vedrete l'ora di iniziare a eseguire codice Keras e TensorFlow
nella pratica. Cominciamo.

3.4 Impostazione di uno spazio di lavoro per l'apprendimento profondo


Prima di iniziare a sviluppare applicazioni di deep learning, è necessario impostare
l'ambiente di sviluppo. È altamente consigliato, anche se non strettamente
necessario, eseguire il codice di deep learning su una moderna GPU NVIDIA
piuttosto che sulla CPU del computer. Alcune applicazioni, in particolare
l'elaborazione delle immagini con reti convoluzionali, saranno estremamente lente su
una CPU, anche su una veloce CPU multicore. E anche per le applicazioni che possono
essere realisticamente eseguite su CPU, in genere la velocità aumenta di un fattore 5
o 10 utilizzando una GPU recente.
72 CAPITOLO 3 Introduzione a Keras e TensorFlow

Per eseguire il deep learning su una GPU, avete tre opzioni:


◾ Acquistare e installare una GPU NVIDIA fisica sulla propria workstation.
◾ Utilizzare istanze GPU su Google Cloud o AWS EC2.
◾ Utilizzate il runtime GPU gratuito di Colaboratory, un servizio di notebook in
hosting offerto da Google (per i dettagli su cosa sia un "notebook", vedere la
sezione successiva).
Colaboratory è il modo più semplice per iniziare, in quanto non richiede l'acquisto di
hardware né l'installazione di software: basta aprire una scheda nel browser e iniziare a
codificare. È l'opzione che raccomandiamo per eseguire gli esempi di codice in questo
libro. Tuttavia, la versione gratuita di Colaboratory è adatta solo per piccoli carichi di
lavoro. Se volete scalare, dovrete usare la prima o la seconda opzione.
Se non avete già una GPU che potete usare per il deep learning (una recente
GPU NVIDIA di fascia alta), l'esecuzione di esperimenti di deep learning nel cloud
è un modo semplice e a basso costo per passare a carichi di lavoro più grandi senza dover
acquistare altro hardware. Se si sviluppa utilizzando i notebook Jupyter, l'esperienza
di esecuzione nel cloud non è diversa da quella dell'esecuzione in locale.
Ma se si fa un uso intensivo del deep learning, questa configurazione non è
sostenibile a lungo termine, e nemmeno per più di qualche mese. Le istanze cloud non
s o n o a buon mercato: paghereste
2,48 dollari all'ora per una GPU V100 su Google Cloud a metà del 2021. Nel
frattempo, una solida GPU di classe consumer costerà tra i 1.500 e i 2.500 dollari,
un prezzo che è rimasto abbastanza stabile nel tempo, anche se le specifiche di
queste GPU continuano a migliorare. Se siete utenti abituali del deep learning,
prendete in considerazione la possibilità di creare una workstation locale con una o
più GPU.
Inoltre, sia che si esegua in locale o nel cloud, è meglio utilizzare una workstation
Unix. Sebbene sia tecnicamente possibile eseguire Keras direttamente su Windows, non
lo consigliamo. Se siete utenti Windows e volete eseguire il deep learning sulla vostra
workstation, la soluzione più semplice per far funzionare tutto è impostare un dual boot
Ubuntu sulla vostra macchina, oppure sfruttare Windows Subsystem for Linux (WSL),
un livello di compatibilità che consente di eseguire applicazioni Linux da Windows. Può
sembrare una seccatura, ma a lungo andare vi farà risparmiare molto tempo e problemi.

3.4.1 Quaderni Jupyter: Il modo preferito per eseguire


esperimenti di deep learning
I taccuini Jupyter sono un ottimo modo per eseguire esperimenti di deep learning, in
particolare i numerosi esempi di codice contenuti in questo libro. Sono ampiamente
utilizzati nelle comunità della scienza dei dati e dell'apprendimento automatico. Un
taccuino è un file generato dall'applicazione Jupyter Notebook (https://jupyter.org) che
si può modificare nel browser. Unisce l'abilità di eseguire codice Python con
funzionalità di modifica del testo per annotare ciò che si sta facendo. Un taccuino
permette anche di suddividere esperimenti lunghi in pezzi più piccoli che possono
essere eseguiti indipendentemente, il che rende lo sviluppo interattivo e permette di
non dover rieseguire tutto il codice precedente se qualcosa va storto all'ultimo
momento di un esperimento.
Impostazione di uno spazio di lavoro 73
per l'apprendimento profondo
Per iniziare a lavorare con Keras, consiglio di utilizzare i taccuini Jupyter, anche
se non è un requisito obbligatorio: si possono anche eseguire script Python
standalone o eseguire il codice da un IDE come PyCharm. Tutti gli esempi di codice
contenuti in questo libro sono disponibili come notebook open source; potete
scaricarli da GitHub all'indirizzo github.com/fchollet/deep- learning-with-python-
notebooks.

3.4.2 Utilizzo del Colaboratorio


Colaboratory (o Colab in breve) è un servizio gratuito di notebook Jupyter che non
richiede installazione e funziona interamente nel cloud. In pratica, è una pagina web
che consente di scrivere ed eseguire subito gli script di Keras. Offre l'accesso a un
runtime per GPU gratuito (ma limitato) e anche a un runtime per TPU, in modo da
non dover acquistare una propria GPU. Colaboratory è il programma consigliato per
l'esecuzione degli esempi di codice contenuti in questo libro.
PRIMI PASSI CON IL COLABORATORIO
Per iniziare a lavorare con Colab, accedere a https://colab.research.google.com e
fare clic sul pulsante Nuovo blocco note. Verrà visualizzata l'interfaccia standard
del blocco note illustrata nella figura 3.2.

Figura 3.2 Un quaderno Colab

Si notano due pulsanti nella barra degli strumenti: + Codice e + Testo. Servono
rispettivamente a creare celle di codice Python e celle di testo di annotazione. Dopo aver
inserito il codice in una cella di codice, premere Maiuscole-Invio per eseguirlo (vedere
figura 3.3).
In una cella di testo è possibile utilizzare la sintassi Markdown (vedi figura 3.4). Se
si preme Maiuscole-Invio su una cella di testo, questa viene visualizzata.
Le celle di testo sono utili per dare una struttura leggibile ai vostri quaderni:
usatele per annotare il codice con titoli di sezioni e lunghi paragrafi di spiegazione o
per incorporare figure. I blocchi note sono pensati per essere un'esperienza
multimediale!
74 CAPITOLO 3 Introduzione a Keras e TensorFlow

Figura 3.3 Creazione di una cella di codice

Figura 3.4 Creazione di una cella di testo

INSTALLAZIONE DEI PACCHETTI CON PIP


L'ambiente predefinito di Colab è già d o t a t o di TensorFlow e Keras, quindi si può
iniziare a usarlo subito senza bisogno di alcuna installazione. Ma se doveste aver
bisogno di installare qualcosa con pip, potete farlo usando la seguente sintassi in una
cella di codice (notate che la riga inizia con ! per indicare che si tratta di un comando
di shell piuttosto che di codice Python):

pip install nome_pacchetto


Primi passi con 75
TensorFlow
UTILIZZO DEL RUNTIME GPU
Per utilizzare il runtime GPU con Colab, selezionare Runtime > Cambia tipo di
runtime nel menu e selezionare GPU per l'acceleratore hardware (vedere figura 3.5).

Figura 3.5 Utilizzo del runtime GPU con Colab

TensorFlow e Keras vengono eseguiti automaticamente su GPU se è disponibile una


GPU, quindi non è necessario fare altro dopo aver selezionato il runtime GPU.
Si noterà che c'è anche un'opzione di runtime TPU nel menu a discesa
dell'acceleratore hardware. A differenza del runtime per GPU, l'uso del runtime per
TPU con TensorFlow e Keras richiede un po' di configurazione manuale nel codice.
Ne parleremo nel capitolo 13. Per il momento, si consiglia di attenersi al runtime per
GPU per seguire gli esempi di codice del libro.
Ora avete un modo per iniziare a eseguire il codice Keras nella pratica. Vediamo ora
come le idee chiave apprese nel capitolo 2 si traducono in codice Keras e TensorFlow.

3.5 Primi passi con TensorFlow


Come si è visto nei capitoli precedenti, l'addestramento di una rete neurale ruota attorno
ai seguenti concetti:
◾ In primo luogo, la manipolazione dei tensori a basso livello, l'infrastruttura che
sta alla base di tutto l'apprendimento automatico m o d e r n o . Questo si
traduce nelle API di TensorFlow:
– Tensori, compresi i tensori speciali che memorizzano lo stato della rete (variabili).
– Operazioni con i tensori, come addizione, relu, matmul
76 CAPITOLO 3 Introduzione a Keras e TensorFlow

– Backpropagation, un modo per calcolare il gradiente di espressioni


matematiche (gestito in TensorFlow tramite l'oggetto GradientTape)
◾ In secondo luogo, concetti di deep learning di alto livello. Questo si traduce nelle API di
Keras:
– Strati, che vengono combinati in un modello
– Una funzione di perdita, che definisce il segnale di feedback utilizzato per
l'apprendimento.
– Un ottimizzatore, che determina come procede l'apprendimento
– Metriche per valutare le prestazioni del modello, come ad esempio l'accuratezza
– Un ciclo di addestramento che esegue una discesa stocastica del gradiente in mini-batch.
Nel capitolo precedente si è già avuto un primo contatto con alcune delle API di
TensorFlow e Keras: si è usata brevemente la classe Variable di TensorFlow,
l'operazione matmul e il GradientTape. Avete istanziato i livelli densi di Keras, li
avete impacchettati in un modello sequenziale e avete addestrato tale modello con
il metodo fit().
Vediamo ora di approfondire il modo in cui tutti questi concetti possono essere
affrontati nella pratica utilizzando TensorFlow e Keras.

3.5.1 Tensori e variabili costanti


Per fare qualsiasi cosa in TensorFlow, abbiamo bisogno di alcuni tensori. I tensori
devono essere creati con un valore iniziale. Ad esempio, si possono creare tensori di tipo
all-ones o all-zeros (si veda l'elenco 3.1), oppure tensori di valori estratti da una
distribuzione casuale (si veda l'elenco 3.2).

Listato 3.1 Tensori all-ones o all-zeros

>>> importare tensorflow come tf


>>> x = tf.ones(shape=(2, 1))
Equivalente a
>>> print(x) np.ones(shape=(2, 1))
tf.Tensor(
[[1.]
[1.]], shape=(2, 1), dtype=float32)
>>> x = tf.zeros(shape=(2, 1))
Equivalente a
>>> print(x) np.zeros(shape=(2, 1))
tf.Tensor(
[[0.]
[0.]], shape=(2, 1), dtype=float32)

Listato 3.2 Tensori casuali


>>> x = tf.random.normal(shape=(3, 1), mean=0., stddev=1.)
>>> print(x)
tf.Tensor(
Tensore di valori casuali estratti da una distribuzione
normale con media 0 e deviazione standard 1.
[[-0.14208166]
Equivale a np.random.normal(size=(3, 1), loc=0.,
[-0.95319825]
scale=1.).
[ 1.1096532 ]], shape=(3, 1), dtype=float32)
>>> x = tf.random.uniform(shape=(3, 1), minval=0., maxval=1.)
>>> print(x)
Tensore di valori casuali estratti da una distribuzione uniforme tra 0 e 1.
tf.Tensor( Equivale a np.random.uniform(size=(3, 1), low=0., high=1.).
Primi passi con 77
TensorFlow
[[0.33779848]
[0.06692922]
[0.7749394 ]], shape=(3, 1), dtype=float32)

Una differenza significativa tra gli array NumPy e i tensori TensorFlow è che i tensori
Tensor-Flow non sono assegnabili: sono costanti. Per esempio, in NumPy, si può fare
quanto segue.

Listato 3.3 Gli array di NumPy sono assegnabili

importare numpy come np


x = np.ones(shape=(2, 2))
x[0, 0] = 0.

Se si prova a fare la stessa cosa in TensorFlow, si ottiene un errore: "L'oggetto EagerTensor


non supporta l'assegnazione di elementi".

Listato 3.4 I tensori di TensorFlow non sono assegnabili

x = tf.ones(shape=(2, 2)) Questa operazione


x[0, 0] = 0. fallirà, poiché un
tensore non è
assegnabile.

Per addestrare un modello, dobbiamo aggiornare il suo stato, che è un insieme di


tensori. Se i tensori non s o n o assegnabili, come si fa? È qui che entrano in gioco le
variabili. tf.Variable è la classe che gestisce lo stato modificabile in TensorFlow.
L'abbiamo già vista brevemente in azione nell'implementazione del ciclo di allenamento
alla fine del capitolo 2.
Per creare una variabile, è necessario fornire un valore iniziale, ad esempio un tensore casuale.

Listato 3.5 Creazione di una variabile TensorFlow

>>> v = tf.Variable(initial_value=tf.random.normal(shape=(3, 1))


>>> print(v)
array([[-0.75133973],
[-0.4872893 ],
[ 1.6626885 ]], dtype=float32)>.

Lo stato di una variabile può essere modificato tramite il suo metodo assign, come segue.

Listato 3.6 Assegnare un valore a una variabile TensorFlow

>>> v.assign(tf.ones((3, 1))


array([[1.]
[1.],
[1.]], dtype=float32)>.

Funziona anche per un sottoinsieme dei coefficienti.


78 CAPITOLO 3 Introduzione a Keras e TensorFlow

Listato 3.7 Assegnare un valore a un sottoinsieme di una variabile TensorFlow


>>> v[0, 0].assign(3.)
array([[3.]
[1.],
[1.]], dtype=float32)>.

Allo stesso modo, assign_add() e assign_sub() sono equivalenti efficienti di += e -


=, come mostrato di seguito.

Listato 3.8 Uso di assign_add()

>>> v.assign_add(tf.ones((3, 1))


array([[2.]
[2.],
[2.]], dtype=float32)>.

3.5.2 Operazioni con i tensori: Fare i conti in TensorFlow


Proprio come NumPy, TensorFlow offre una vasta gamma di operazioni sui tensori per
esprimere formule matematiche. Ecco alcuni esempi.

Listato 3.9 Alcune operazioni matematiche di base

a = tf.ones((2, 2))
b = tf.square(a) Prendete la piazza.
c = tf.sqrt(a) Prendere la radice quadrata.
d = b + c
Aggiungere due tensori (in modo elementare).
e = tf.matmul(a, b)
e *= d Prendiamo il prodotto di due
tensori (come discusso nel
Moltiplica due
capitolo 2).
tensori (in
modo
elementare).

È importante notare che ciascuna delle operazioni precedenti viene eseguita al volo: in
qualsiasi m o m e n t o è possibile stampare il risultato corrente, proprio come in NumPy.
Questa viene chiamata esecuzione "eager".

3.5.3 Un secondo sguardo all'API GradientTape


F i n o r a , TensorFlow sembra assomigliare molto a NumPy. Ma ecco qualcosa che NumPy
non può fare: recuperare il gradiente di qualsiasi espressione differenziabile rispetto
a uno qualsiasi dei suoi ingressi. Basta aprire uno scope GradientTape, applicare
qualche calcolo a uno o più tensori di ingresso e recuperare il gradiente del risultato
rispetto agli ingressi.

Listato 3.10 Utilizzo del GradientTape

input_var = tf.Variabile(valore_iniziale=3.)
con tf.GradientTape() come tape:
risultato = tf.square(input_var)
gradiente = tape.gradient(risultato, input_var)
Primi passi con 79
TensorFlow
Questo è il metodo più comunemente usato per recuperare i gradienti della perdita di un
modello rispetto ai suoi pesi: gradienti = tape.gradient(loss, weights).
L'abbiamo visto in azione nel capitolo 2.
Finora abbiamo visto solo il caso in cui i tensori in ingresso a tape.gradient() erano
variabili TensorFlow. In realtà è possibile che questi input siano qualsiasi tensore
arbitrario. Tuttavia, per impostazione predefinita, vengono tracciate solo le variabili
addestrabili. Con un tensore costante, è necessario contrassegnarlo manualmente come
tracciato chiamando tape.watch() su di esso.

Listato 3.11 Utilizzo di GradientTape con input tensoriali costanti

input_const = tf.constant(3.)
con tf.GradientTape() come tape:
tape.watch(input_const)
risultato = tf.square(input_const)
gradiente = tape.gradient(risultato, input_const)

Perché è necessario? Perché sarebbe troppo costoso memorizzare preventivamente le


informazioni necessarie per calcolare il gradiente di qualsiasi cosa rispetto a qualsiasi
cosa. Per evitare di sprecare risorse, il nastro deve sapere cosa guardare. Le variabili
addestrabili sono osservate per impostazione predefinita perché il calcolo del gradiente
di una perdita rispetto a un elenco di variabili addestrabili è l'uso più comune d e l
nastro dei gradienti.
Il nastro dei gradienti è un'utilità potente, in grado di calcolare anche i gradienti del
secondo ordine, cioè il gradiente di un gradiente. Per esempio, il gradiente della
posizione di un oggetto rispetto al tempo è la velocità di quell'oggetto e il gradiente del
secondo ordine è la sua accelerazione.
Se si misura la posizione di una mela che cade lungo un asse verticale nel tempo e si
scopre che essa verifica posizione(tempo) = 4,9 * tempo ** 2, qual è la sua
accelerazione? Utilizziamo due nastri gradienti annidati per scoprirlo.

Listato 3.12 Utilizzo di nastri di gradienti annidati per calcolare gradienti del secondo ordine

tempo = tf.Variabile(0.)
con tf.GradientTape() come outer_tape: Utilizziamo il nastro
con tf.GradientTape() come inner_tape: esterno per calcolare il
posizione = 4,9 * tempo ** 2 gradiente del gradiente
velocità = inner_tape.gradient(posizione, del nastro interno.
tempo) accelerazione = Naturalmente la risposta è
4,9 * 2 = 9,8.
outer_tape.gradient(velocità, tempo)

3.5.4 Un esempio end-to-end: Un classificatore lineare in TensorFlow puro


Conoscete i tensori, le variabili e le operazioni sui tensori e sapete come calcolare i
gradienti. Questo è sufficiente per costruire qualsiasi modello di apprendimento
automatico basato sulla discesa dei gradienti. E siete solo al capitolo 3!
In un colloquio di lavoro sull'apprendimento automatico, potrebbe esservi chiesto di
implementare una classe lineare da zero in TensorFlow: un compito molto semplice che
funge da filtro tra i candidati che hanno un minimo di background nell'apprendimento
automatico e quelli che non c e l ' h a n n o .
80 CAPITOLO 3 Introduzione a Keras e TensorFlow

Superiamo questo filtro e usiamo la nuova conoscenza di TensorFlow per implementare


un classificatore lineare.
Per prima cosa, creiamo dei dati sintetici ben separabili linearmente c o n cui lavorare:
due classi di punti in un piano 2D. Genereremo ogni classe di punti estraendo le loro
coordinate da una distribuzione casuale con una specifica matrice di covarianza e
una specifica media. Intuitivamente, la matrice di covarianza descrive la forma della
nuvola di punti e la media la sua posizione nel piano (vedi figura 3.6). Utilizzeremo
la stessa matrice di covarianza per entrambe le nuvole di punti, ma useremo due
valori medi diversi: le nuvole di punti avranno la stessa forma, ma posizioni diverse.

Listato 3.13 Generazione di due classi di punti casuali in un piano 2D

num_campioni_per_classe = 1000 Generare la prima classe di


negative_samples = np.random.multivariate_normal( punti: 1000 punti 2D casuali.
mean=[0, 3], cov=[[1, 0.5],[0.5, 1]]
cov=[[1, 0,5],[0,5, 1]], corrisponde a una nuvola di
dimensione=numero_campioni_per_classe) punti di forma ovale orientata
positive_samples = np.random.multivariate_normal( dal basso a sinistra all'alto a
mean=[3, 0], destra.
cov=[[1, 0,5],[0,5, 1]], Generare l'altra classe di punti
dimensione=numero_campioni_per_classe)
con una media diversa e la stessa
matrice di covarianza.

Nel codice precedente, campioni_negativi e campioni_positivi sono entrambi


array con forma (1000, 2). Impiliamoli in un unico array con forma (2000, 2).

Listato 3.14 Impilare le due classi in un array con forma (2000, 2)

input = np.vstack((campioni_negativi, campioni_positivi)).astype(np.float32)

Generiamo le etichette di destinazione corrispondenti, una matrice di zeri e di uni della forma
(2000, 1), dove target[i, 0] è 0 se input[i] appartiene alla classe 0 (e viceversa).

Listato 3.15 Generazione dei target corrispondenti (0 e 1)

target = np.vstack((np.zeros((num_campioni_per_classe, 1), dtype="float32"),


np.ones((num_campioni_per_classe, 1), dtype="float32")))

Quindi, tracciamo i nostri dati con Matplotlib.

Listato 3.16 Tracciare le due classi di punti (vedi figura 3.6)

importare matplotlib.pyplot come plt


plt.scatter(input[:, 0], input[:, 1], c=target[:, 0])
plt.show()
Primi passi con 81
TensorFlow

Figura 3.6 I nostri dati


sintetici: due classi di punti
casuali nel piano 2D

Ora creiamo un classificatore lineare che impari a separare questi due blob. Un
classificatore lineare è una trasformazione affine (predizione = W - input + b)
addestrata per minimizzare il quadrato della differenza tra le predizioni e gli obiettivi.
Come vedrete, si tratta di un esempio molto più semplice rispetto a quello di una rete
neurale giocattolo a due strati, visto alla fine del capitolo 2. Tuttavia, questa volta
dovrete essere in grado di capire tutto ciò che riguarda il codice, riga per riga. Tuttavia,
questa volta dovreste essere in grado di capire tutto del codice, riga per riga.
Creiamo le nostre variabili, W e b, inizializzate rispettivamente con valori casuali e
con zeri.

Listato 3.17 Creazione delle variabili del classificatore lineare

Gli input Le previsioni in uscita saranno un singolo


saranno punteggio per ogni campione (vicino a 0 se il
punti 2D. campione è previsto nella classe 0 e vicino a
1 se il campione è previsto nella classe 1).
input_dim = 2
output_dim = 1
W= tf.Variabile(valore iniziale=tf.random.uniform(forma=(input_dim,
output_dim)) b = tf.Variabile(valore iniziale=tf.zeros(forma=(output_dim,))

Ecco la nostra funzione di passaggio in avanti.

Listato 3.18 La funzione forward pass

def model(input):
restituire tf.matmul(ingressi, W) + b

Poiché il nostro classificatore lineare opera su ingressi 2D, W è in realtà solo due coefficienti
scalari, w1 e w2: W = [[w1], [w2]]. Nel frattempo, b è un singolo coefficiente scalare.
Di conseguenza, per un dato punto di ingresso [x, y], il suo valore di predizione è
predizione = [[w1], [w2]] - [x, y] + b = w1 * x + w2 * y + b.
Il seguente elenco mostra la nostra funzione di perdita.
82 CAPITOLO 3 Introduzione a Keras e TensorFlow

Listato 3.19 La funzione di perdita dell'errore quadratico medio


per_sample_losses sarà un tensore con la stessa forma di target
e prediction, contenente i punteggi di perdita per campione.
def square_loss(targets, predictions):
per_sample_losses = tf.square(targets - predictions)
restituire tf.reduce_mean(per_sample_losses)

Dobbiamo calcolare la media di questi punteggi di


perdita per campione in un unico valore di perdita
scalare: questo è ciò che fa reduce_mean.

La fase successiva è quella di addestramento, che riceve alcuni dati di addestramento e aggiorna i
pesi W
e b in modo da ridurre al minimo la perdita di dati.

Listato 3.20 La funzione passo passo dell'addestramento

tasso_di_apprendimento = 0,1 Recuperare il


gradiente della perdita
def training_step(input, target):
rispetto a
with tf.GradientTape() as tape:
ai pesi.
predictions = model(input)
Passaggio in avanti,
perdita = square_loss(previsioni,
obiettivi)
all'interno di un
campo di
applicazione del
nastro gradiente
grad_loss_wrt_W, grad_loss_wrt_b = tape.gradient(loss, [W, b])
W.assign_sub(grad_loss_wrt_W * learning_rate)
Aggiornare i pesi.
b.assign_sub(grad_loss_wrt_b * learning_rate)
return loss

Per semplicità, faremo un addestramento batch invece di un addestramento mini-batch:


eseguiremo ogni passo di addestramento (calcolo del gradiente e aggiornamento dei pesi)
per tutti i dati, invece di iterare sui dati in piccoli lotti. Da un lato, ciò significa che ogni
fase di addestramento richiederà molto più tempo, poiché calcoleremo il passaggio in
avanti e i gradienti per 2.000 campioni in una volta sola. D'altro canto, ogni
aggiornamento del gradiente sarà molto più efficace nel ridurre la perdita sui dati di
addestramento, poiché comprenderà le informazioni di tutti i campioni di addestramento
anziché, ad esempio, solo 128 campioni casuali. Di conseguenza, avremo bisogno di
molti meno passi di addestramento e dovremmo usare un tasso di apprendimento
maggiore di quello che useremmo di solito per l'addestramento in mini-batch (useremo
learning_rate = 0,1, definito nell'elenco 3.20).

Listato 3.21 Il ciclo di addestramento batch

per passo nell'intervallo(40):


perdita = training_step(input, target)
print(f "Perdita al passo {passo}: {perdita:.4f}")

Dopo 40 passi, la perdita di addestramento sembra essersi stabilizzata intorno a 0,025.


Vediamo come il nostro modello lineare classifica i punti dei dati di addestramento.
Poiché i nostri obiettivi sono zero e uno, un dato punto di ingresso sarà classificato
come "0" se il suo valore di previsione è inferiore a 0,5 e come "1" se è superiore a 0,5
Primi passi con 83
TensorFlow

(vedi figura 3.7):

previsioni = modello(input)
plt.scatter(input[:, 0], input[:, 1], c=predictions[:, 0] > 0.5)
plt.show()
84 CAPITOLO 3 Introduzione a Keras e TensorFlow

Figura 3.7 Previsioni del


nostro modello sugli input
di addestramento:
abbastanza simili agli
obiettivi di
addestramento

Ricordiamo che il valore di predizione per un dato punto [x, y] è semplicemente


predizione == [[w1], [w2]] - [x, y] + b == w1 * x + w2 * y + b.
Pertanto, la classe 0 è definita come w1 * x + w2
* y + b < 0,5, e la classe 1 è definita come w1 * x + w2 * y + b > 0,5. Si noterà che
quella che si sta osservando è in realtà l'equazione di una retta nel piano 2D: w1 * x + w2 * y
+ b = 0,5. Al di sopra della retta si trova la classe 1, mentre al di sotto della retta si trova
la classe 0. Forse siete abituati a vedere le equazioni delle rette nel formato y = a * x +
b; nello stesso formato, la nostra retta diventa y = - w1 / w2
* x + (0,5 - b) / w2.
Tracciamo questa linea (figura 3.8):

Generate 100 numeri Questa è


regolarmente distanziati tra -1 e l'equazione
4, che utilizzeremo per tracciare della nostra
la nostra linea. linea.
x = np.linspace(-1, 4, 100)
y = - W[0] / W[1] * x + (0,5 - b) / W[1] Tracciare la linea
plt.plot(x, y, "-r") ("-r" significa
"tracciarla come
linea rossa").
plt.scatter(input[:, 0], input[:, 1], c=predictions[:, 0] > 0.5)
Tracciare le previsioni del nostro modello sullo stesso grafico.

Figura 3.8 Il nostro


Primi passi con 85
TensorFlow

modello, visualizzato
come una linea
86 CAPITOLO 3 Introduzione a Keras e TensorFlow

Questo è il vero scopo di un classificatore lineare: trovare i parametri di una linea (o, in
spazi più dimensionali, di un iperpiano) che separa nettamente due classi di dati.

3.6 Anatomia di una rete neurale: Comprendere le API


di base di Keras
A questo punto, si conoscono le basi di TensorFlow e lo si può utilizzare per
implementare un modello giocattolo da zero, come il classificatore lineare batch
della sezione precedente o la rete neurale giocattolo alla fine del capitolo 2. Si tratta
di una solida base su cui costruire. Ora è il momento di passare a un percorso più
produttivo e robusto per l'apprendimento profondo: l'API di Keras.

3.6.1 Strati: Gli elementi costitutivi del deep learning


La struttura dati fondamentale delle reti neurali è il livello, che è stato introdotto nel
capitolo 2. Un livello è un modulo di elaborazione dati che riceve in ingresso un tensore
e in uscita un tensore. Uno strato è un modulo di elaborazione dei dati che riceve in
ingresso uno o più tensori e che produce in uscita uno o più tensori. Alcuni strati sono
privi di stato, ma più spesso gli strati hanno uno stato: i pesi dello strato, uno o più
tensori appresi con la discesa stocastica del gradiente, che insieme contengono la
conoscenza della rete.
Diversi tipi di livelli sono adatti a diversi formati di tensori e a diversi tipi di
elaborazione dei dati. Ad esempio, i dati vettoriali semplici, memorizzati in tensori di
forma di rango 2 (campioni, caratteristiche), sono spesso elaborati da livelli
densamente connessi, chiamati anche livelli completamente connessi o densi (la classe
Dense di Keras). I dati di sequenza, memorizzati in tensori di forma di rango 3
(campioni, tempi, caratteristiche), sono in genere elaborati da strati ricorrenti,
come uno strato LSTM o strati di convoluzione 1D (Conv1D). I dati delle immagini,
memorizzati in tensori di rango 4, sono solitamente elaborati da strati di convoluzione
2D (Conv2D).
Si può pensare ai layer come ai mattoncini LEGO del deep learning, una
metafora resa esplicita da Keras. La costruzione di modelli di deep learning in Keras
si effettua ritagliando insieme livelli compatibili per formare utili pipeline di
trasformazione dei dati.
LA CLASSE BASE LAYER DI KERAS
Un'API semplice dovrebbe avere un'unica astrazione attorno alla quale ruota tutto. In
Keras, questa è la classe Layer. Tutto in Keras è un Layer o qualcosa che interagisce
strettamente con un Layer.
Un Layer è un oggetto che incapsula alcuni stati (pesi) e alcuni calcoli (un
passaggio in avanti). I pesi sono tipicamente definiti in un metodo build() (anche se
potrebbero essere creati nel costruttore init ()) e il calcolo è definito nel metodo
call().
Nel capitolo precedente, abbiamo implementato una classe NaiveDense che
conteneva due pesi W e b e abbiamo applicato il calcolo output =
activation(dot(input, W) +
b). Ecco come apparirebbe lo stesso livello in Keras.

Listato 3.22 Un livello denso implementato come sottoclasse di Layer


Primi passi con 87
TensorFlow

da tensorflow importa keras


Tutti i livelli di Keras
ereditano dalla classe base
classe
SimpleDense(keras.layers.Layer):
Layer.
Anatomia di una rete neurale: Comprendere le API di base di 85
Keras
def init (self, units, activation=None):
super(). init ()
self.units = unità
self.activation = La creazione del
attivazione peso avviene nel
metodo build().
def build(self, input_shape):
input_dim = input_shape[-1]
self.W = self.add_weight(shape=(input_dim, self.units),
initializer="random_normal")
self.b = self.add_weight(shape=(self.units,),
Definiamo il inizializzatore="zer
calcolo del i") add_weight() è una
passaggio in def call(self, input): scorciatoia
avanti per la creazione dei pesi. È
anche possibile creare
nella
chiamata() y = tf.matmul(input, self.W) + self.b
variabili indipendenti e
metodo. se self.activation non è None:
assegnarle come attributi del livello,
come self.W =
y = self.activation(y)
restituire y
tf.Variabile(tf.random.uniform(w_shape)).

Nella prossima sezione, tratteremo in dettaglio lo scopo di questi metodi build() e call().
metodi. Non preoccupatevi se non avete ancora capito tutto!
Una volta istanziato, un livello come questo può essere usato come una funzione,
prendendo in input un tensore TensorFlow:

>>> mio_denso = SempliceDenso(unità=32, Istanziare il nostro


attivazione=tf.nn.relu)
>>> input_tensor = tf.ones(shape=(2, 784)) definito in
Creare
>>>tensore_uscita = precedenza.
mio_denso(sensore_ingresso) alcuni test
>>> print(output_tensor.shape) Richiamare il
(2, 32)) livello su ingressi.
gli ingressi,
proprio come
una funzione.
Vi starete chiedendo: perché abbiamo dovuto implementare call() e build(), visto che
abbiamo finito per usare il nostro livello chiamandolo semplicemente, cioè usando il suo
metodo call ()? Perché vogliamo essere in grado di creare lo stato appena in tempo.
Vediamo come funziona.
INFERENZA AUTOMATICA DELLE FORME: COSTRUIRE STRATI AL VOLO
Proprio come con i mattoncini LEGO, è possibile "agganciare" tra loro solo livelli
compatibili. La nozione di compatibilità dei livelli si riferisce in particolare al fatto che
ogni livello accetta solo tensori di ingresso di una certa forma e restituisce tensori di
uscita di una certa forma. Si consideri il seguente esempio:

da tensorflow.keras importa strati Uno strato denso


layer = layers.Dense(32, activation="relu")
con 32 unità di
uscita

Questo livello restituirà un tensore la cui prima dimensione è stata trasformata in


32. Può essere collegato solo a uno strato a valle che prevede in ingresso vettori a
32 dimensioni.
Quando si usa Keras, la maggior parte delle volte non ci si deve preoccupare della
compatibilità delle dimensioni, perché i livelli aggiunti ai modelli sono costruiti
86 CAPITOLO 3 Introduzione a Keras e TensorFlow

dinamicamente per adattarsi alla forma del livello in arrivo. Per esempio, supponiamo di
scrivere quanto segue:
Anatomia di una rete neurale: Comprendere le API di base di 87
Keras
from tensorflow.keras import models
from tensorflow.keras import layers
model = models.Sequential([
layers.Dense(32, activation="relu"),
layers.Dense(32)
])

Gli strati non h a n n o ricevuto alcuna informazione sulla forma dei loro input, ma
hanno dedotto automaticamente la forma dei primi i n p u t c h e hanno visto.
Nella versione giocattolo del livello Dense che abbiamo implementato nel capitolo
2 (che abbiamo chiamato NaiveDense), dovevamo passare esplicitamente al
costruttore la dimensione di ingresso del livello per poter creare i suoi pesi. Questo non
è l'ideale, perché porterebbe a modelli come questo, in cui ogni nuovo livello deve
conoscere la forma del livello precedente:

modello = NaiveSequential([
NaiveDense(input_size=784, output_size=32, attivazione="relu"),
NaiveDense(input_size=32, output_size=64, attivazione="relu"),
NaiveDense(input_size=64, output_size=32, attivazione="relu"),
NaiveDense(input_size=32, output_size=10, attivazione="softmax")
])

Sarebbe ancora peggio se le regole utilizzate da uno strato per produrre la forma in
uscita fossero complesse. Per esempio, cosa succederebbe se il nostro strato restituisse
output di forma (batch, input_size * 2 se input_size % 2 == 0 altrimenti
input_size * 3)?
Se dovessimo reimplementare il nostro livello NaiveDense come un livello Keras
in grado di fare inferenza automatica sulle forme, sarebbe simile al precedente livello
SimpleDense (vedi listato 3.22), con i suoi metodi build() e call().
In SimpleDense, non creiamo più i pesi nel costruttore come nell'esempio
Naive-Dense, ma li creiamo in un metodo di creazione dello stato dedicato,
build(), che riceve come argomento la prima forma di input vista dallo strato. Il
metodo build() viene richiamato automaticamente la prima volta che il livello viene
chiamato (tramite il suo metodo
metodo call ()). In effetti, questo è il motivo per cui abbiamo definito il calcolo in
un metodo call() separato, anziché direttamente nel metodo call (). Il metodo
call () del livello base si presenta schematicamente come segue:

def call (self, input):


if not self.built:
self.build(inputs.shape)
self.built = True
restituire self.call(input)

Con l'inferenza automatica della forma, il nostro esempio precedente diventa semplice e
pulito:

model = keras.Sequential([
SimpleDense(32, attivazione="relu"),
SimpleDense(64, attivazione="relu"),
88 CAPITOLO 3 Introduzione a Keras e TensorFlow

SimpleDense(32, attivazione="relu"),
SimpleDense(10, attivazione="softmax")
])

Si noti che l'inferenza automatica della forma non è l'unica cosa che la classe Layer
gestisce il metodo call (). Si occupa di molte altre cose, in particolare
dell'instradamento tra l'esecuzione eager e quella a grafo (un concetto che si
apprenderà nel capitolo 7) e del mascheramento degli ingressi (di cui si parlerà nel
capitolo 11). Per ora, ricordate: quando implementate i vostri livelli, mettete il
passaggio in avanti nel metodo call().

3.6.2 Dai livelli ai modelli


Un modello di deep learning è un grafo di livelli. In Keras, questa è la classe Model.
Finora abbiamo visto solo modelli sequenziali (una sottoclasse di Model), che sono
semplici pile di livelli, che mappano un singolo input a un singolo output. Ma man
mano che si va avanti, ci si troverà di fronte a una varietà molto più ampia di topologie
di rete. Queste sono alcune di quelle più comuni:
◾ Reti a due rami
◾ Reti multitesta
◾ Connessioni residue

La topologia della rete può essere molto complessa. Ad esempio, la figura 3.9
mostra la topologia del grafo dei livelli di un trasformatore, un'architettura comune
progettata per elaborare dati di testo.
In genere esistono due modi per costruire tali modelli in Keras: si può sottoclasse
direttamente la classe Model, oppure si può utilizzare l'API funzionale, che consente di
fare di più con meno codice. Tratteremo entrambi gli approcci nel capitolo 7.
La topologia di un modello definisce uno spazio di ipotesi. Forse ricorderete che nel
capitolo 1 abbiamo descritto l'apprendimento automatico come la ricerca di
rappresentazioni utili di alcuni dati di ingresso, all'interno di uno spazio predefinito di
possibilità, utilizzando la guida di un segnale di feedback. Scegliendo una topologia di
rete, si vincola lo spazio delle possibilità (spazio ipotetico) a una serie specifica di
operazioni tensoriali, che mappano i dati di ingresso in quelli di uscita. Si cercherà
quindi un buon insieme di valori per i tensori dei pesi coinvolti in queste operazioni
tensoriali.
Per imparare dai dati, è necessario fare delle ipotesi su di essi. Queste ipotesi
definiscono ciò che può essere appreso. Per questo motivo, la struttura dello spazio
delle ipotesi - l'architettura del modello - è estremamente importante. Essa codifica
le ipotesi che si fanno sul problema, la conoscenza preliminare da cui parte il
modello. Per esempio, se si lavora su un problema di classificazione a due classi con un
modello costituito da un singolo strato denso senza attivazione (una trasformazione affine
pura), si assume che le due classi siano linearmente separabili.
Scegliere la giusta architettura di rete è più un'arte che una scienza e, sebbene
esistano alcune best practice e principi su cui fare affidamento, solo la pratica può
aiutare a diventare un corretto architetto di reti neurali. I prossimi capitoli
insegneranno
Anatomia di una rete neurale: Comprendere le API di base di 89
Keras

StratoNormalizzazi
one

Denso

Denso
StratoNormalizzazi
one

StratoNormalizzazi
one
+

Denso +

MultiHeadAttention
Denso

StratoNormalizzazio StratoNormalizzazio
ne ne

+ +

MultiHeadAttention MultiHeadAttention

Figura 3.9 L'architettura di Transformer (trattata nel capitolo 11). Qui c'è molto da fare.
Nei prossimi capitoli si cercherà di capire meglio.

L'utente potrà conoscere i principi espliciti per la costruzione delle reti neurali e
sviluppare l'intuizione di ciò che funziona o non funziona per problemi specifici. Si
acquisirà una solida intuizione su quali tipi di architetture di modelli funzionano per
diversi tipi di problemi, su come costruire queste reti nella pratica, su come
scegliere la giusta configurazione di apprendimento e su come modificare un
modello fino a ottenere i risultati desiderati.

3.6.3 La fase di "compilazione": Configurare il processo di apprendimento


Una volta definita l'architettura del modello, è necessario scegliere ancora tre cose:
◾ Funzione di perdita (funzione obiettivo): la quantità che deve essere minimizzata
durante l'addestramento. Rappresenta una misura del successo del compito da
svolgere.
90 CAPITOLO 3 Introduzione a Keras e TensorFlow

◾ Ottimizzatore: determina il modo in cui la rete verrà aggiornata in base alla funzione
di perdita. Implementa una variante specifica della discesa stocastica del
gradiente (SGD).
◾ Metriche: le misure di successo che si desidera monitorare durante l'addestramento e
la valutazione, come l'accuratezza della classificazione. A differenza della perdita,
l'addestramento non ottimizza direttamente queste metriche. Pertanto, non è
necessario che le metriche siano differenziabili.
Dopo aver scelto la perdita, l'ottimizzatore e le metriche, si possono usare i metodi
integrati compile() e fit() per iniziare l'addestramento del modello. In alternativa,
è possibile scrivere i propri cicli di addestramento personalizzati, che verranno illustrati
nel capitolo 7. È un lavoro molto più impegnativo! Per ora, diamo un'occhiata a
compile() e fit().
Il metodo compile() configura il processo di addestramento, che è già stato
introdotto nel primo esempio di rete neurale nel capitolo 2. Prende gli argomenti
ottimizzatore, perdita e metrica (un elenco):
Definire un classificatore lineare. Specificare l'ottimizzatore
the
model = per nome: RMSprop
keras.Sequential([keras.layers.Dense(1)]) (non fa distinzione
model.compile(optimizer="rmsprop",
tra maiuscole e
minuscole).
loss="mean_squared_error",
metrics=["accuracy"]) Specificare la
Specificare un elenco di perdita per
metriche: in questo nome: errore
caso, solo l'accuratezza. quadratico
medio.

Nella precedente chiamata a compile(), abbiamo passato l'ottimizzatore, la perdita e


le metriche come stringhe (come "rmsprop"). Queste stringhe sono in realtà
scorciatoie che vengono convertite in oggetti Python. Per esempio, "rmsprop" diventa
keras.optimizers.RMSprop(). È importante notare che è anche possibile
specificare questi argomenti come istanze di oggetti, come in questo caso:

model.compile(optimizer=keras.optimizers.RMSprop(),
loss=keras.losses.MeanSquaredError(),
metrics=[keras.metrics.BinaryAccuracy()])

Questo è utile se si vogliono passare perdite o metriche personalizzate o se si


vogliono configurare ulteriormente gli oggetti che si stanno usando, ad esempio passando
un argomento learning_rate all'ottimizzatore:

model.compile(optimizer=keras.optimizers.RMSprop(learning_rate=1e-4),
loss=my_custom_loss,
metrics=[my_custom_metric_1, my_custom_metric_2])

Nel capitolo 7 si parlerà di come creare perdite e metriche personalizzate. In generale,


non s a r à necessario creare perdite, metriche o ottimizzatori da zero, perché Keras
offre un'ampia gamma di opzioni integrate che probabilmente includono ciò di cui avete
bisogno:
Ottimizzatori:
Anatomia di una rete neurale: Comprendere le API di base di 91
Keras

◾ SGD (con o senza slancio)


◾ RMSprop
◾ Adamo
92 CAPITOLO 3 Introduzione a Keras e TensorFlow

◾ Adagrad
◾ ecc.

Sconfitte:
◾ Crossentropia categorica
◾ SparseCategoricalCrossentropy
◾ Crossentropia binaria
◾ Errore quadratico medio
◾ KLDivergenza
◾ CosenoSimilarità
◾ ecc.

Metriche:
◾ Precisione categoriale
◾ Precisione sparseCategoriali
◾ Precisione binaria
◾ AUC
◾ Precisione
◾ Richiamo
◾ ecc.

Nel corso di questo libro, vedrete applicazioni concrete di molte di queste opzioni.

3.6.4 Scegliere una funzione di perdita


La scelta della giusta funzione di perdita per il giusto problema è estremamente
importante: la rete prenderà tutte le scorciatoie possibili per minimizzare la perdita,
quindi se l'obiettivo non è completamente correlato con il successo del compito in
questione, la rete finirà per fare cose che non si vorrebbero. Immaginate una stupida IA
onnipotente addestrata tramite SGD con questa funzione obiettivo mal scelta:
"massimizzare il benessere medio di tutti gli esseri umani in vita". Per semplificare il
suo lavoro, questa IA potrebbe scegliere di uccidere tutti gli umani tranne alcuni e
concentrarsi sul benessere di quelli rimasti, perché il benessere medio non è influenzato
dal numero di umani rimasti. Potrebbe non essere quello che volevate! Ricordate che
tutte le reti neurali che costruite saranno altrettanto spietate nell'abbassare la loro
funzione di perdita, quindi scegliete bene l'obiettivo o dovrete affrontare effetti
collaterali indesiderati.
Fortunatamente, quando si tratta di problemi comuni come la classificazione, la
regressione e la previsione di sequenze, esistono semplici linee guida da seguire per
scegliere la perdita corretta. Per esempio, si userà la crossentropia binaria per un
problema di classificazione a due classi, la crossentropia categorica per un problema
di classificazione a più classi e così via. Solo quando si lavora su problemi di ricerca
veramente nuovi si dovranno sviluppare funzioni di perdita proprie. Nei prossimi
capitoli, verranno descritte in modo esplicito le funzioni di perdita da scegliere per
un'ampia gamma di compiti comuni.
Anatomia di una rete neurale: Comprendere le API di base di 93
Keras
3.6.5 Capire il metodo fit()
Dopo compile() viene fit(). Il metodo fit() implementa il ciclo di addestramento
stesso. Questi sono i suoi argomenti chiave:
◾ I dati (input e target) su cui allenarsi. In genere vengono passati sotto forma di
array NumPy o di un oggetto Dataset di TensorFlow. Nei prossimi capitoli si
apprenderà di più sull'API Dataset.
◾ Il numero di epoche di addestramento: quante volte il ciclo di addestramento
deve iterare sui dati passati.
◾ La dimensione del batch da utilizzare in ogni epoca di discesa del gradiente
mini-batch: il numero di esempi di allenamento considerati per calcolare i
gradienti per un passo di aggiornamento del peso.

Listato 3.23 Chiamata di fit() con dati NumPy

storia = model.fit( Gli esempi in ingresso,


input, come array NumPy
target, Gli obiettivi di
epochs=5, addestramento
batch_size=128 corrispondenti,
come array
NumPy
)
Il ciclo di Il ciclo di
addestramento itera i addestramento
dati in gruppi di 128 itera i dati per 5
esempi. volte.

La chiamata a fit() restituisce un oggetto Storia. Questo oggetto contiene un campo


storia, che è un dict che mappa chiavi come "perdita" o nomi di metriche
specifiche all'elenco dei loro valori per epoca.

>>> storia.storia
{"binary_accuracy": [0.855, 0.9565, 0.9555, 0.95, 0.951],
"perdita": [0.6573270302042366,
0.07434618508815766,
0.07687718723714351,
0.07412414988875389,
0.07617757616937161]}

3.6.6 Monitoraggio delle perdite e delle metriche sui dati di convalida


L'obiettivo dell'apprendimento automatico non è ottenere modelli che funzionino bene
sui dati di addestramento, il che è facile: basta seguire il gradiente. L'obiettivo è ottenere
modelli che funzionino bene in generale, e in particolare su punti di dati che il modello
non ha mai incontrato prima. Solo perché un modello si comporta bene sui dati di
addestramento, non significa che si comporterà bene anche su dati che non ha mai visto!
Ad esempio, è possibile che il modello finisca per memorizzare semplicemente una
mappatura tra i campioni di addestramento e i loro obiettivi, che sarebbe inutile per il
compito di prevedere gli obiettivi per i dati che il modello non ha mai visto prima.
Analizzeremo questo punto in maniera molto più dettagliata nel capitolo 5.
94 CAPITOLO 3 Introduzione a Keras e TensorFlow

Per tenere sotto controllo l'andamento del modello su nuovi dati, è prassi riservare
un sottoinsieme dei dati di addestramento come dati di validazione: non si addestrerà il
modello su questi dati, ma li si utilizzerà per calcolare un valore di perdita e un valore di
metrica. Per farlo, si utilizza l'argomento validation_data in fit(). Come i dati
di addestramento, i dati di validazione possono essere passati come array NumPy o
come oggetto TensorFlow Dataset.

Listato 3.24 Utilizzo dell'argomento validation_data

model = keras.Sequential([keras.layers.Dense(1)])
model.compile(optimizer=keras.optimizers.RMSprop(learning_rate=0.1),
loss=keras.losses.MeanSquaredError(),
Per evitare di avere
metrics=[keras.metrics.BinaryAccuracy()])
campioni di una sola
classe nei dati di
indices_permutation = np.random.permutation(len(inputs)) validazione, mescolare
shuffled_inputs = inputs[indices_permutation] gli input e i target
shuffled_targets = targets[indices_permutation] utilizzando una
permutazione casuale
num_validation_samples = int(0.3 * len(inputs)) degli indici.
val_inputs = shuffled_inputs[:num_validation_samples]
val_targets = shuffled_targets[:num_validation_samples] Riservare il 30% degli
training_inputs = shuffled_inputs[num_validation_samples:] input e dei target di
training_targets = shuffled_targets[num_validation_samples:] formazione per la
model.fit( convalida (escluderemo
questi campioni dalla
formazione e li
riserveremo alla
convalida).
training_inputs, Dati di addestramento, calcolare la convalida
training_targets, utilizzati per aggiornare i pesi perdita e metriche).
epochs=5, del modello.
batch_size=16, Dati di convalida, utilizzati solo
validation_data=(val_inputs, val_targets) per monitorare la
) perdita di convalida e le
metriche

Il valore della perdita sui dati di validazione è chiamato "perdita di validazione", per
distinguerlo dalla "perdita di addestramento". È essenziale tenere rigorosamente separati
i dati di addestramento da quelli di convalida: lo scopo della convalida è monitorare se
ciò che il modello sta imparando è effettivamente utile su nuovi dati. Se uno qualsiasi
dei dati di convalida è stato visto dal modello durante l'addestramento, la perdita e le
metriche di convalida saranno errate.
Si noti che se si desidera calcolare la perdita e la metrica di convalida al termine
dell'addestramento, è possibile richiamare il metodo evaluate():

loss_and_metrics = model.evaluate(val_inputs, val_targets, batch_size=128)

evaluate() itera in batch (di dimensione batch_size) sui dati passati e restituisce
un elenco di scalari, dove la prima voce è la perdita di validazione e le voci successive
sono le metriche di validazione. Se il modello non ha metriche, viene restituita solo la
perdita di validazione (anziché un elenco).
Sintesi 93

3.6.7 Inferenza: Utilizzo di un modello dopo l'addestramento


Una volta addestrato il modello, è necessario utilizzarlo per fare previsioni su nuovi
dati. Questa operazione si chiama inferenza. Per fare questo, un approccio ingenuo
sarebbe semplicemente quello di
chiamare () il modello:
Prende un array NumPy o
previsioni = un tensore TensorFlow e
modello(nuovi_ingressi) restituisce un tensore
TensorFlow.

Tuttavia, questo processerà tutti gli input in new_inputs in una volta sola, il che potrebbe
non essere fattibile se si stanno esaminando molti dati (in particolare, potrebbe
richiedere più memoria di quanta ne abbia la GPU).
Un modo migliore per fare inferenza è usare il metodo predict(). Questo metodo itera
i dati in piccoli lotti e restituisce un array NumPy di previsioni. E a differenza di
(), può anche elaborare oggetti TensorFlow Dataset.
Prende un array di
predictions = model.predict(new_inputs, batch_size=128) NumPy o un Dataset e
restituisce un array di
NumPy.

Ad esempio, se utilizziamo predict() su alcuni dei nostri dati di convalida con il


modello lineare che abbiamo addestrato in precedenza, otteniamo punteggi scalari che
corrispondono alla p r e v i s i o n e del modello per ogni campione di input:

>>> predictions = model.predict(val_inputs, batch_size=128)


>>> print(predictions[:10])
[[0.3590725 ]
[0.82706255]
[0.74428225]
[0.682058 ]
[0.7312616 ]
[0.6059811 ]
[0.78046083]
[0.025846 ]
[0.16594526]
[0.72068727]]

Per il momento, questo è tutto ciò che occorre sapere sui modelli di Keras. Siete
pronti per passare alla soluzione di problemi reali di apprendimento automatico con
Keras nel prossimo capitolo.

Sintesi
◾ TensorFlow è un framework per l'elaborazione numerica che può essere
eseguito su CPU, GPU o TPU. È in grado di calcolare automaticamente il
gradiente di qualsiasi espressione differenziabile, può essere distribuito su
molti dispositivi e può esportare i programmi in vari runtime esterni, persino
in JavaScript.
◾ Keras è l'API standard per l'apprendimento profondo con TensorFlow. È quella
che utilizzeremo in questo libro.
◾ Gli oggetti chiave di TensorFlow includono i tensori, le variabili, le
96 CAPITOLO 3 Introduzione a Keras e TensorFlow

operazioni sui tensori e il nastro del gradiente.


94 CAPITOLO 3 Introduzione a Keras e TensorFlow

◾ La classe centrale di Keras è il livello. Un livello incapsula alcuni pesi e


alcuni calcoli. I livelli vengono assemblati in modelli.
◾ Prima di iniziare l'addestramento di un modello, è necessario scegliere un ottimizzatore, una
perdita e qualche
che vengono specificati tramite il metodo model.compile().
◾ Per addestrare un modello, si può usare il metodo fit(), che esegue una discesa
di gradi- mento in mini-batch. Si può anche usare per monitorare la perdita e le
metriche sui dati di valutazione, un insieme di input che il modello non vede
durante l'addestramento.
◾ Una volta addestrato il modello, si utilizza il metodo model.predict() per
generare previsioni su nuovi input.
Come iniziare
con le reti neurali:
Classificazione e regressione

Questo capitolo tratta


◾ I primi esempi di flussi di lavoro reali di
apprendimento automatico
◾ Gestione di problemi di classificazione su dati
vettoriali
◾ Gestione di problemi di regressione continua
su dati vettoriali

Questo capitolo è stato pensato per iniziare a utilizzare le reti neurali per risolvere
problemi reali. Si consolideranno le conoscenze acquisite nei capitoli 2 e 3 e si
applicherà quanto appreso a tre nuovi compiti che coprono i tre casi d'uso più comuni
delle reti neurali: classificazione binaria, classificazione multiclasse e regressione
scalare:
◾ Classificazione delle recensioni di film come positive o negative (classificazione
binaria)
◾ Classificazione dei fili di notizie per argomento (classificazione multiclasse)
◾ Stima del prezzo di una casa, dati i dati immobiliari (regressione scalare)

Questi esempi saranno il vostro primo contatto con i flussi di lavoro end-to-end
dell'apprendimento automatico: verrete introdotti alla pre-elaborazione dei dati,
ai principi di base dell'architettura dei modelli e alla valutazione dei modelli.

95
96 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

Glossario di classificazione e regressione


La classificazione e la regressione comportano molti termini specialistici. Ne avete
incontrati alcuni negli esempi precedenti e ne vedrete altri nei prossimi capitoli.
Hanno definizioni precise, specifiche per l'apprendimento automatico, e
dovrebbero essere familiari:
◾ Campione o input: un punto di dati che viene inserito nel modello.
◾ Previsione o output: ciò che emerge dal modello.
◾ Obiettivo: la verità. Ciò che il vostro modello avrebbe dovuto idealmente
prevedere, secondo una fonte di dati esterna.
◾ Errore di previsione o valore di perdita: misura della distanza tra la
previsione del modello e l'obiettivo.
◾ Classi: insieme di possibili etichette tra cui scegliere in un problema di
classificazione. Ad esempio, quando si classificano immagini di cani e gatti,
"cane" e "gatto" sono le due classi.
◾ Etichetta - Un'istanza specifica dell'annotazione di una classe in un
problema di classificazione. Ad esempio, se l'immagine #1234 è annotata
come contenente la classe "cane", allora "cane" è un'etichetta dell'immagine
#1234.
◾ Verità di base o annotazioni: tutti gli obiettivi di un set di dati, in genere
raccolti da esseri umani.
◾ Classificazione binaria: compito di classificazione in cui ogni campione in
ingresso deve essere classificato in due categorie esclusive.
◾ Classificazione multiclasse: compito di classificazione in cui ogni campione
in ingresso deve essere classificato in più di due categorie: ad esempio, la
classificazione di cifre scritte a mano.
◾ Classificazione multilingue: compito di classificazione in cui a ogni
campione in ingresso possono essere assegnate più etichette. Ad esempio,
una data immagine può contenere sia un gatto che un cane e deve essere
annotata sia con l'etichetta "gatto" che con l'etichetta "cane". Il numero di
etichette per immagine è solitamente variabile.
◾ Regressione scalare - Un compito in cui l'obiettivo è un valore scalare
continuo. Il calcolo dei prezzi delle case è un buon esempio: i diversi prezzi
target formano uno spazio continuo.
◾ Regressione vettoriale - Un'operazione in cui l'obiettivo è un insieme di
valori continui: ad esempio, un vettore continuo. Se si esegue una
regressione su valori multipli (come le coordinate di un riquadro di
delimitazione in un'immagine), si tratta di una regressione vettoriale.
◾ Mini-batch o batch-Un piccolo insieme di campioni (in genere tra 8 e 128)
che vengono elaborati simultaneamente dal modello. Il numero di campioni
è spesso una potenza di 2, per facilitare l'allocazione della memoria sulla
GPU. Durante l'addestramento, un mini-batch viene utilizzato per calcolare
un singolo aggiornamento del gradiente-descente applicato ai pesi del
modello.

Alla fine di questo capitolo, sarete in grado di utilizzare le reti neurali per gestire
semplici compiti di classificazione e regressione su dati vettoriali. Sarete quindi pronti per
iniziare a costruire una comprensione dell'apprendimento automatico basata su
principi e teorie nel capitolo 5.
Classificare le recensioni di film: Un esempio di 97
classificazione binaria

4.1 Classificare le recensioni di film: Un esempio di


classificazione binaria
La classificazione a due classi, o classificazione binaria, è uno dei tipi più comuni di
problemi di apprendimento automatico. In questo esempio, imparerete a classificare le
recensioni di film come positive o negative, in base al contenuto testuale delle
recensioni.

4.1.1 Il set di dati IMDB


Lavorerete con il dataset IMDB: un insieme di 50.000 recensioni altamente polarizzate
provenienti dall'Internet Movie Database. Sono suddivise in 25.000 recensioni per
l'addestramento e 25.000 recensioni per il test, ciascuna composta per il 50% da
recensioni negative e per il 50% da recensioni positive.
Proprio come il dataset MNIST, il dataset IMDB viene fornito con Keras. È già
stato preprocessato: le recensioni (sequenze di parole) sono state trasformate in
sequenze di numeri interi, dove ogni numero intero corrisponde a una parola
specifica in un dizionario. Questo ci permette di concentrarci sulla costruzione,
l'addestramento e la valutazione del modello. Nel capitolo 11 si apprenderà come
elaborare il testo grezzo da zero.
Il codice seguente caricherà il set di dati (alla prima esecuzione, verranno
scaricati circa 80 MB di dati sul computer).

Listato 4.1 Caricamento del set di dati IMDB

da tensorflow.keras.datasets import imdb


(train_data, train_labels), (test_data, test_labels) = imdb.load_data(
num_words=10000)

L'argomento num_words=10000 significa che verranno conservate solo le 10.000


parole più frequenti nei dati di addestramento. Le parole rare saranno scartate. Questo ci
permette di lavorare con dati vettoriali di dimensioni gestibili. Se non avessimo fissato
questo limite, avremmo lavorato con 88.585 parole uniche nei dati di addestramento, un
numero inutilmente elevato. Molte di queste parole sono presenti solo in un singolo
campione e quindi non possono essere utilizzate in modo significativo per la
classificazione.
Le variabili train_data e test_data sono elenchi di recensioni; ogni recensione è
un elenco di indici di parole (che codificano una sequenza di parole). train_labels e
test_labels sono elenchi di 0 e 1, dove 0 sta per negativo e 1 per positivo:

>>> train_data[0]
[1, 14, 22, 16, ... 178, 32]
>>> train_labels[0]
1

Poiché ci limitiamo alle 10.000 parole più frequenti, l'indice delle parole non
supererà le 10.000 unità:

>>> max([max(sequenza) per sequenza in train_data])


9999
98 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione
Per comodità, ecco come decodificare rapidamente una di queste recensioni in parole inglesi.
Classificare le recensioni di film: Un esempio di 99
classificazione binaria

Listato 4.2 Decodificare le recensioni in testo


word_index è una mappatura del dizionario
word_index = imdb.get_word_index() a un indice intero.
reverse_word_index = dict(
[(valore, chiave) per (chiave, valore) in Inverte la
word_index.items()]) decoded_review = " ".join( situazione,
[reverse_word_index.get(i - 3, "?") per i in train_data[0]]) mappando gli
indici interi in
Decodifica la recensione. Si noti che gli indici sono sfalsati di 3
parole
perché 0, 1 e 2 sono indici riservati per "padding", "inizio
sequenza" e "sconosciuto".

4.1.2 Preparazione dei dati


Non si possono inserire direttamente elenchi di numeri interi in una rete neurale.
Hanno tutti lunghezze diverse, ma una rete neurale si aspetta di elaborare gruppi di
dati contigui. È necessario trasformare gli elenchi in tensori. Ci sono due modi per
farlo:
◾ Imbottite le liste in modo che abbiano tutte la stessa lunghezza, trasformatele
in un tensore intero di forma (samples, max_length) e iniziate il modello con
un livello in grado di gestire tali tensori interi (il livello Embedding, che tratteremo
in dettaglio più avanti nel libro).
◾ Codificare gli elenchi in modo da trasformarli in vettori di 0 e 1. Ciò
significherebbe, per esempio, trasformare la sequenza [8, 5] in un vettore di
10.000 dimensioni con tutti 0, tranne che per gli indici 8 e 5, che sarebbero 1. Si
potrebbe quindi utilizzare un livello Dense, in grado di gestire dati vettoriali in
virgola mobile, come primo livello del modello.
Per vettorializzare i dati, sceglieremo quest'ultima soluzione, che verrà eseguita
manualmente per ottenere la massima chiarezza.

Listato 4.3 Codifica delle sequenze di interi tramite la codifica multi-hot

importare numpy come np Crea una matrice con tutti


def vettorizzazione_sequenze(sequenze, gli zeri di forma
dimensione=10000): risultati = (len(sequenze),
dimensione)
np.zeros((len(sequenze), dimensione))
per i, sequenza in enumerate(sequenze):
per j nella sequenza: Imposta gli indici
risultati[i, j] = 1. specifici di
risultati[i] a 1s
restituire i risultati
x_train = vettorializza_sequenze(train_data)
x_test = vettorializza_sequenze(test_data) Dati di
Dati di test vettoriali formazione
vettoriali
Ecco come appaiono ora i campioni:

>>> x_train[0]
array([ 0., 1., 1., ..., 0., 0., 0.])
100 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

È inoltre necessario vettorializzare le etichette, operazione semplice:

y_train = np.asarray(train_labels).astype("float32")
y_test = np.asarray(test_labels).astype("float32")

Ora i dati sono pronti per essere inseriti in una rete neurale.

4.1.3 Costruire il modello


I dati in ingresso sono vettori e le etichette sono scalari (1 e 0): questa è una delle
configurazioni di problema più semplici che si possano incontrare. Un tipo di modello
che si comporta bene con questo tipo di problema è una semplice pila di strati
densamente connessi (Dense) con attivazioni relu.
Ci sono due decisioni chiave da prendere in merito all'architettura di uno stack di Dense
strati:
◾ Quanti strati utilizzare
◾ Quante unità scegliere per ogni strato
Ingresso
(testo vettoriale)
Nel capitolo 5 imparerete i principi formali che vi
guideranno in queste scelte. Per il momento, dovrete
fidarvi di me per le seguenti scelte di architettura: Denso (unità=16)

◾ Due strati intermedi con 16 unità ciascuno


Denso (unità=16)
◾ Un terzo livello che produrrà la previsione
scalare relativa al sentiment della recensione
attuale Denso (unità=1)

La Figura 4.1 mostra l'aspetto del modello. Il seguente


Uscita
elenco mostra l'implementazione di Keras, simile (probabilità)
all'esempio MNIST visto in precedenza.
Figura 4.1 Il modello a tre strati

Listato 4.4 Definizione del modello

da tensorflow importa keras


da tensorflow.keras importare strati

model = keras.Sequential([
layers.Dense(16, attivazione="relu"),
layers.Dense(16, attivazione="relu"),
layers.Dense(1,
attivazione="sigmoid")
])

Il primo argomento passato a ogni strato denso è il numero di unità dello strato: la
dimensionalità dello spazio di rappresentazione dello strato. Si ricorda dai capitoli 2 e 3
che ciascuno di questi strati densi con attivazione relu- gativa implementa la
seguente catena di operazioni tensoriali:

output = relu(dot(input, W) + b)
Classificare le recensioni di film: Un esempio di 101
classificazione binaria
Avere 16 unità significa che la matrice dei pesi W avrà forma (input_dimension, 16):
il prodotto del punto con W proietterà i dati di input su uno spazio di
rappresentazione a 16 dimensioni (e poi si aggiungerà il vettore bias b e si
applicherà l'operazione relu). La dimensionalità dello spazio di rappresentazione
può essere intesa intuitivamente come "quanta libertà si concede al modello
nell'apprendimento delle rappresentazioni interne". Avere più unità (uno spazio di
rappresentazione più alto) permette al modello di apprendere rappresentazioni più
complesse, ma rende il modello più costoso dal punto di vista computazionale e può
portare all'apprendimento di modelli indesiderati (modelli che migliorano le
prestazioni sui dati di addestramento ma non sui dati di test).
Gli strati intermedi utilizzano la funzione di attivazione relu, mentre lo strato
finale utilizza un'attivazione sigmoide per produrre una probabilità (un punteggio
compreso tra 0 e 1 che indica la probabilità che il campione abbia l'obiettivo "1", ovvero
la probabilità che l'esame sia positivo). Una relu (unità lineare rettificata) è una
funzione che ha lo scopo di azzerare i valori negativi (si veda la figura 4.2), mentre una
sigmoide "schiaccia" i valori arbitrari nell'intervallo [0, 1] (si veda la figura 4.3),
producendo qualcosa che può essere interpretato come una probabilità.

Figura 4.2 La funzione unitaria lineare rettificata

Infine, è necessario scegliere una funzione di perdita e un ottimizzatore. Poiché si tratta


di un problema di classificazione binaria e l'output del modello è una probabilità (si
termina il modello con uno strato a unità singola con attivazione sigmoide), è meglio
utilizzare la perdita binary_crossentropy. Non è l'unica scelta possibile: ad esempio,
si può usare l'errore quadratico medio. Ma la crossentropia è di solito la scelta
migliore quando si tratta di
102 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

Figura 4.3 La funzione sigmoide

Cosa sono le funzioni di attivazione e perché sono necessarie?


Senza una funzione di attivazione come la relu (chiamata anche non linearità), lo
strato Dense consisterebbe in due operazioni lineari: un prodotto di punti e
un'addizione:
uscita = punto(ingresso, W) + b

Lo strato potrebbe apprendere solo trasformazioni lineari (trasformazioni affini) dei


dati di ingresso: lo spazio di ipotesi dello strato sarebbe l'insieme di tutte le
possibili trasformazioni lineari dei dati di ingresso in uno spazio a 16 dimensioni.
Uno spazio di ipotesi di questo tipo è troppo ristretto e non trarrebbe beneficio da
più strati di rappresentazione, perché una pila profonda di strati lineari
implementerebbe comunque un'operazione lineare: aggiungere altri strati non
estenderebbe lo spazio di ipotesi (come si è visto nel capitolo 2).
Per avere accesso a uno spazio di ipotesi molto più ricco, che possa trarre
vantaggio dalle rappresentazioni profonde, è necessaria una funzione di
attivazione o di non linearità. relu è la funzione di attivazione più popolare
nell'apprendimento profondo, ma ci sono molti altri candidati, tutti con nomi
altrettanto strani: prelu, elu e così via.

con modelli che producono probabilità. L'entropia incrociata è una quantità del campo
della teoria dell'informazione che misura la distanza tra le distribuzioni di probabilità o,
in questo caso, tra la distribuzione della verità e le vostre previsioni.
Per quanto riguarda la scelta dell'ottimizzatore, sceglieremo rmsprop, che di solito
è una buona scelta predefinita per quasi tutti i problemi.
Classificare le recensioni di film: Un esempio di 103
classificazione binaria
Ecco il passaggio in cui configuriamo il modello con l'ottimizzatore rmsprop e la
funzione di perdita binary_crossentropy. Si noti che monitoreremo anche
l'accuratezza durante l'addestramento.

Listato 4.5 Compilazione del modello

model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])

4.1.4 Convalidare l'approccio


Come si è appreso nel capitolo 3, un modello di deep learning non dovrebbe mai essere
valutato sui dati di addestramento: è prassi comune utilizzare un set di validazione per
monitorare l'accuratezza del modello durante l'addestramento. In questo caso, creeremo
un set di validazione separando 10.000 campioni dai dati di addestramento originali.

Listato 4.6 Impostazione di un set di validazione

x_val = x_train[:10000]
partial_x_train = x_train[10000:]
y_val = y_train[:10000]
partial_y_train = y_train[10000:]

Ora alleneremo il modello per 20 epoche (20 iterazioni su tutti i campioni dei dati di
addestramento) in mini-batch di 512 campioni. Allo stesso tempo, monitoreremo la
perdita e l'accuratezza sui 10.000 campioni che abbiamo separato. Per farlo, passiamo i
dati di validazione come argomento validation_data.

Listato 4.7 Addestramento del modello

storia = model.fit(partial_x_train,
partial_y_train,
epochs=20,
batch_size=512,
validation_data=(x_val, y_val))

Con la CPU, questo richiede meno di 2 secondi per ogni epoch: l'addestramento termina
in 20 secondi. Alla fine di ogni epoca, si verifica una leggera pausa mentre il modello
calcola la perdita e l'accuratezza sui 10.000 campioni dei dati di convalida.
Si noti che la chiamata a model.fit() restituisce un oggetto History, come si è
visto nel capitolo 3. Questo oggetto ha un membro history, che è un dizionario contenente i
dati di ogni cosa accaduta durante l'addestramento. Questo oggetto ha un membro
history, che è un dizionario contenente dati su ogni cosa accaduta durante
l'addestramento. V e d i a m o l o :

>>> history_dict = history.history


>>> history_dict.keys()
[u "accuratezza", u "perdita", u "val_accuracy", u "val_loss"].
104 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

Il dizionario contiene quattro voci: una per ogni metrica monitorata durante la
formazione e la validazione. Nei due listati seguenti, usiamo Matplotlib per tracciare la
perdita di addestramento e quella di validazione (vedi figura 4.4), nonché l'accuratezza
di addestramento e quella di validazione (vedi figura 4.5). Si noti che i risultati
potrebbero variare leggermente a causa di una diversa inizializzazione casuale del
modello.

Figura 4.4 Perdita di formazione e convalida

Figura 4.5 Accuratezza della formazione e della validazione


Classificare le recensioni di film: Un esempio di 105
classificazione binaria

Listato 4.8 Tracciare la perdita di addestramento e di validazione

import matplotlib.pyplot as plt


history_dict = history.history
loss_values = history_dict["loss"]
val_loss_values =
history_dict["val_loss"] epochs = "bo" sta
range(1, len(loss_values) + 1) per "punto
plt.plot(epochs, loss_values, "bo", label="Training loss")
blu".
plt.plot(epochs, val_loss_values, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.xlabel("Epoche") "b" sta
plt.ylabel("Perdita" per "solid blue
) plt.legend() line".
plt.show()

Listato 4.9 Tracciare l'accuratezza della formazione e della validazione

plt.clf()
Cancella la figura
acc = history_dict["accuracy"]
val_acc =
history_dict["val_accuracy"]
plt.plot(epochs, acc, "bo", label="Acc di addestramento")
plt.plot(epochs, val_acc, "b", label="Acc di validazione")
plt.title("Accuratezza di addestramento e validazione")
plt.xlabel("Epoche")
plt.ylabel("Precisione
") plt.legend()
plt.show()

Come si può notare, la perdita di addestramento diminuisce a ogni epoca e


l'accuratezza dell'addestramento aumenta a ogni epoca. È quello che ci si
aspetterebbe quando si esegue un'ottimizzazione a discesa di gradiente: la quantità
che si cerca di minimizzare dovrebbe diminuire a ogni iterazione. Ma non è così per la
perdita e l'accuratezza di validazione: sembrano raggiungere il picco alla quarta epoca.
Questo è un esempio di ciò che abbiamo messo in guardia in precedenza: un
modello che si comporta meglio sui dati di addestramento non è necessariamente un
modello che s i comporterà meglio su dati che non ha mai visto prima. In termini
precisi, si tratta di overfitting: dopo la quarta epoch, si sta sovraottimizzando sui dati
di addestramento e si finisce per apprendere rappresentazioni che sono specifiche
dei dati di addestramento e non si adattano ai dati esterni all'insieme di
addestramento.
In questo caso, per evitare l'overfitting, si potrebbe interrompere l'addestramento
dopo quattro epoche. In generale, è possibile utilizzare una serie di tecniche per mitigare
l'overfitting, che verranno illustrate nel capitolo 5.
Addestriamo un nuovo modello da zero per quattro epoche e poi valutiamolo sui dati
di prova.

Listato 4.10 Riallenamento di un modello da zero

model = keras.Sequential([
layers.Dense(16, activation="relu"),
layers.Dense(16, activation="relu"),
106 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

layers.Dense(1, attivazione="sigmoide")
])
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
model.fit(x_train, y_train, epochs=4, batch_size=512)
results = model.evaluate(x_test, y_test)

I risultati finali sono i seguenti:


Il primo numero, 0,29, è la
>>> risultati perdita del test e il secondo
[0.2929924130630493, 0.88327999999999995] numero, 0,88, è la precisione
del test.

Questo approccio abbastanza ingenuo raggiunge un'accuratezza dell'88%. Con gli


approcci più avanzati, si dovrebbe essere in grado di avvicinarsi al 95%.

4.1.5 Utilizzo di un modello addestrato per generare previsioni su nuovi dati


Dopo aver addestrato un modello, si vorrà utilizzarlo in un contesto pratico. È possibile
generare la probabilità che le recensioni siano positive utilizzando il metodo
predict, come abbiamo imparato nel capitolo 3:

>>> model.predict(x_test)
array([[ 0.98006207]
[ 0.99758697]
[ 0.99975556]
...,
[ 0.82167041]
[ 0.02885115]
[ 0.65371346]], dtype=float32)

Come si può vedere, il modello è sicuro per alcuni campioni (0,99 o più, o 0,01 o
meno) ma meno sicuro per altri (0,6, 0,4).

4.1.6 Ulteriori esperimenti


I seguenti esperimenti vi aiuteranno a convincervi che le scelte architettoniche f a t t e sono
tutte abbastanza ragionevoli, anche se c'è ancora spazio per i miglioramenti:
◾ Sono stati utilizzati due livelli di rappresentazione prima del livello di classificazione
finale. Provate a usare uno o tre livelli di rappresentazione e vedete come questo
influisce sull'accuratezza della validazione e del test.
◾ Provate a utilizzare strati con più unità o meno unità: 32 unità, 64 unità e così via.
◾ Provare a utilizzare la funzione di perdita mse invece di binary_crossentropy.
◾ Provate a utilizzare l'attivazione tanh (un'attivazione molto diffusa agli albori
delle reti neurali) invece di relu.
Classificare le recensioni di film: Un esempio di 107
classificazione binaria
4.1.7 Conclusione
Ecco cosa si deve dedurre da questo esempio:
◾ Di solito è necessario effettuare una certa pre-elaborazione dei dati grezzi per
poterli inserire, come tensori, in una rete neurale. Le sequenze di parole
possono essere codificate come vettori binari, ma esistono anche altre opzioni
di codifica.
◾ Gli stack di livelli densi con attivazioni relu possono risolvere un'ampia
gamma di problemi (compresa la classificazione del sentiment) e probabilmente li
utilizzerete spesso.
◾ In un problema di classificazione binaria (due classi di uscita), il modello
dovrebbe terminare con uno strato denso con un'unità e un'attivazione
sigmoide: l'uscita del modello dovrebbe essere uno scalare tra 0 e 1, che
codifica una probabilità.
◾ Con un'uscita sigmoide scalare su un problema di classificazione binaria, la
funzione di perdita da utilizzare è binary_crossentropy.
◾ L'ottimizzatore rmsprop è generalmente una scelta sufficiente, qualunque sia il
vostro problema. È una cosa in meno di cui preoccuparsi.
◾ Man mano che migliorano sui dati di addestramento, le reti neurali finiscono
per adattarsi in modo eccessivo, ottenendo risultati sempre peggiori su dati
mai visti prima. Assicuratevi di monitorare sempre le prestazioni su dati che
non rientrano nel set di addestramento.

4.2 Classificare i telegiornali: Un esempio di classificazione multiclasse


Nella sezione precedente si è visto come classificare gli input vettoriali in due classi
reciprocamente esclu- sive utilizzando una rete neurale densamente connessa. Ma cosa
succede quando si hanno più di due classi?
In questa sezione, costruiremo un modello per classificare i notiziari Reuters in 46
argomenti mutuamente esclusivi. Poiché abbiamo molte classi, questo problema è
un'istanza di classificazione multiclasse, e poiché ogni punto di dati deve essere
classificato in una sola categoria, il problema è più specificamente un'istanza di
classificazione multiclasse a etichetta singola. Se ogni punto dati potesse appartenere a
più categorie (in questo caso, argomenti), saremmo di fronte a un problema di
classificazione multiclasse multilabel.

4.2.1 Il set di dati Reuters


Lavorerete con il dataset Reuters, un insieme di brevi notizie e relativi argomenti,
pubblicato da Reuters nel 1986. Si tratta di un dataset semplice e ampiamente utilizzato
per la classificazione dei testi. Ci sono 46 argomenti diversi; alcuni argomenti sono più
rappresentati di altri, ma ogni argomento ha almeno 10 esempi nel set di allenamento.
Come IMDB e MNIST, il dataset Reuters viene fornito come parte di Keras. Diamo
un'occhiata.

Listato 4.11 Caricamento del set di dati Reuters

da tensorflow.keras.datasets import reuters


(train_data, train_labels), (test_data, test_labels) = reuters.load_data(
108 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione
num_words=10000)
Classificare i telegiornali: Un esempio di classificazione 107
multiclasse
Come per il set di dati IMDB, l'argomento num_words=10000 limita i dati alle 10.000
parole più frequenti presenti nei dati.
Si hanno 8.982 esempi di addestramento e 2.246 esempi di test:

>>> len(train_data)
8982
>>> len(test_data)
2246

Come per le recensioni di IMDB, ogni esempio è un elenco di numeri interi (indici di parole):

>>> train_data[10]
[1, 245, 273, 207, 156, 53, 74, 160, 26, 14, 46, 296, 26, 39, 74, 2979,
3554, 14, 46, 4689, 4329, 86, 61, 3499, 4795, 14, 61, 451, 4329, 17, 12]

Ecco come si può decodificare in parole, nel caso siate curiosi.

Listato 4.12 Decodifica dei fili di notizie in testo

word_index = reuters.get_word_index()
reverse_word_index = dict(
[(valore, chiave) per (chiave, valore) in word_index.items()])
decoded_newswire = " ".join(
[reverse_word_index.get(i - 3, "?") per i in train_data[0]])
Si noti che gli indici sono sfalsati di 3 perché 0, 1 e 2 sono indici riservati per
"padding", "inizio sequenza" e "sconosciuto".
L'etichetta associata a un esempio è un numero intero compreso tra 0 e 45, un indice di
argomento:

>>> train_labels[10]
3

4.2.2 Preparazione dei dati


È possibile vettorializzare i dati con lo stesso codice dell'esempio precedente.

Listato 4.13 Codifica dei dati di ingresso

x_train = Dati di formazione


vettorializza_sequenze(train_data) x_test vettorizzati Dati di test
= vettorializza_sequenze(test_data)
vettorizzati

Per vettorializzare le etichette, ci sono due possibilità: si può lanciare l'elenco delle
etichette come un tensore intero, oppure si può usare la codifica one-hot. La codifica
one-hot è un formato ampiamente utilizzato per i dati categoriali, chiamato anche
codifica categoriale. In questo caso, la codifica one-hot delle etichette consiste
nell'incorporare ogni etichetta come un vettore tutto zero con un 1 al posto dell'indice
dell'etichetta. Il seguente elenco mostra un esempio.

Listato 4.14 Codifica delle etichette

def to_one_hot(labels, dimension=46):


risultati = np.zeri((len(etichette), dimensione))
108 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

per i, label in enumerate(labels):


risultati[i, label] = 1.
restituire i risultati
y_train = to_one_hot(train_labels) Etichette di
y_test = to_one_hot(test_labels) addestramento vettorizzate
Etichette di test
vettorizzate

Si noti che esiste un modo integrato per farlo in Keras:

da tensorflow.keras.utils import to_categorical


y_train = to_categorical(train_labels)
y_test = to_categorical(test_labels)

4.2.3 Costruire il modello


Questo problema di classificazione degli argomenti è simile al precedente problema di
classificazione delle recensioni cinematografiche: in entrambi i casi, stiamo cercando di
classificare brevi frammenti di testo. Ma qui c'è un nuovo vincolo: il numero di classi di
output è passato da 2 a 46. La dimensionalità dello spazio di output è molto più ampia.
La dimensionalità dello spazio di output è molto più ampia.
In una pila di livelli densi come quelli che abbiamo utilizzato, ogni livello può
accedere solo alle informazioni presenti nell'output del livello precedente. Se uno
strato perde alcune informazioni rilevanti per il problema di classificazione, queste
non potranno mai essere recuperate dagli strati successivi: ogni strato può
potenzialmente diventare un collo di bottiglia per le informazioni. Nell'esempio
precedente, abbiamo usato strati intermedi a 16 dimensioni, ma uno spazio a 16
dimensioni può essere troppo limitato per imparare a separare 46 classi diverse:
strati così piccoli possono agire come colli di bottiglia dell'informazione, perdendo
permanentemente informazioni importanti.
Per questo motivo utilizzeremo strati più grandi. Scegliamo 64 unità.

Listato 4.15 Definizione del modello

model = keras.Sequential([
layers.Dense(64, attivazione="relu"),
layers.Dense(64, attivazione="relu"),
layers.Dense(46,
attivazione="softmax")
])

Ci sono altre due cose da notare su questa architettura.


Per prima cosa, terminiamo il modello con uno strato denso di dimensioni 46. Ciò
significa che per ogni campione in ingresso, la rete produrrà un vettore a 46 dimensioni. Ciò
significa che per ogni campione in ingresso, la rete produrrà un vettore di 46
dimensioni. Ogni voce di questo vettore (ogni dimensione) codificherà una diversa
classe di uscita.
In secondo luogo, l'ultimo livello utilizza un'attivazione softmax. Si è visto questo
schema nell'esempio MNIST. Significa che il modello produrrà una distribuzione di
probabilità sulle 46 diverse classi di output: per ogni campione di input, il modello
Classificare i telegiornali: Un esempio di classificazione 109
multiclasse

produrrà un vettore di output a 46 dimensioni, dove output[i] è la probabilità che il


campione appartenga alla classe i. I 46 punteggi avranno somma 1.
La migliore funzione di perdita da utilizzare in questo caso è
categorical_crossentropy. Essa misura la distanza tra due distribuzioni di
probabilità: in questo caso, tra la probabilità
110 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

distribuzione emessa dal modello e la vera distribuzione delle etichette.


Minimizzando la distanza tra queste due distribuzioni, si addestra il modello a
produrre un risultato il più vicino possibile alle etichette vere.

Listato 4.16 Compilazione del modello

model.compile(optimizer="rmsprop",
loss="categorical_crossentropy",
metrics=["accuracy"])

4.2.4 Convalidare l'approccio


Selezioniamo 1.000 campioni nei dati di addestramento da utilizzare come set di validazione.

Listato 4.17 Impostazione di un insieme di validazione

x_val = x_train[:1000]
partial_x_train = x_train[1000:]
y_val = y_train[:1000]
partial_y_train = y_train[1000:]

Ora addestriamo il modello per 20 epoche.

Listato 4.18 Addestramento del modello

storia = model.fit(partial_x_train,
partial_y_train,
epochs=20,
batch_size=512,
validation_data=(x_val, y_val))

Infine, visualizziamo le curve di perdita e di precisione (vedere le figure 4.6 e 4.7).

Figura 4.6 Perdita di


formazione e
convalida
Classificare i telegiornali: Un esempio di classificazione 111
multiclasse

Figura 4.7 Accuratezza


della formazione e
della validazione

Listato 4.19 Tracciare la perdita di addestramento e di validazione

loss = history.history["loss"]
val_loss =
history.history["val_loss"] epochs =
range(1, len(loss) + 1)
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.xlabel("Epochs")
plt.ylabel("Perdit
a") plt.legend()
plt.show()

Listato 4.20 Tracciare l'accuratezza della formazione e della validazione

plt.clf() Cancella la figura


acc = history.history["accuracy"]
val_acc =
history.history["val_accuracy"]
plt.plot(epochs, acc, "bo", label="Accuratezza di
addestramento") plt.plot(epochs, val_acc, "b",
label="Accuratezza di validazione") plt.title("Accuratezza di
addestramento e validazione") plt.xlabel("Epochs")
plt.ylabel("Precisione
") plt.legend()
plt.show()

Il modello inizia ad adattarsi eccessivamente dopo nove epoche. Addestriamo un nuovo


modello da zero per nove epoche e poi v a l u t i a m o l o sul set di test.

Listato 4.21 Riallenamento di un modello da zero

model = keras.Sequential([
layers.Dense(64, activation="relu"),
112 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

layers.Dense(64, attivazione="relu"),
layers.Dense(46, attivazione="softmax")
])
model.compile(optimizer="rmsprop",
loss="categorical_crossentropy",
metrics=["accuracy"])
model.fit(x_train,
y_train,
epochs=9,
batch_size=512)
risultati = model.evaluate(x_test, y_test)

Ecco i risultati finali:

>>> risultati
[0.9565213431445807, 0.79697239536954589]

Questo approccio raggiunge un'accuratezza di ~80%. Con un problema di


classificazione binaria bilanciata, l'accuratezza raggiunta da un classificatore
puramente casuale sarebbe del 50%. Ma in questo caso abbiamo 46 classi, che
potrebbero non essere rappresentate in modo uguale. Quale sarebbe l'accuratezza di
un classificatore casuale? Potremmo provare a implementarne rapidamente uno per
verificarlo empiricamente:

>>> importare copia


>>> test_labels_copy = copy.copy(test_labels)
>>> np.random.shuffle(test_labels_copy)
>>> hits_array = np.array(test_labels) == np.array(test_labels_copy)
>>> hits_array.mean()
0.18655387355298308

Come si può vedere, un classificatore casuale otterrebbe un'accuratezza di


classificazione del 19% circa, quindi i risultati del nostro modello sembrano piuttosto
buoni sotto questa luce.

4.2.5 Generazione di previsioni su nuovi dati


Chiamando il metodo predict del modello su nuovi campioni, si ottiene una
distribuzione della probabilità di classe su tutti i 46 argomenti per ciascun campione.
Generiamo le previsioni degli argomenti per tutti i dati del test:

previsioni = model.predict(x_test)

Ogni voce di "previsioni" è un vettore di lunghezza 46:

>>> previsioni[0].shape
(46,)

I coefficienti di questo vettore sommano a 1, in quanto formano una distribuzione di probabilità:

>>> np.sum(predictions[0])
1.0
Classificare i telegiornali: Un esempio di classificazione 113
multiclasse
La voce più grande è la classe prevista, quella con la probabilità più alta:

>>> np.argmax(previsioni[0])
4

4.2.6 Un modo diverso di gestire le etichette e la perdita


Abbiamo accennato in precedenza che un altro modo per codificare le etichette
sarebbe quello di lanciarle come un tensore di interi, in questo modo:

y_train = np.array(train_labels)
y_test = np.array(test_labels)

L'unica cosa che cambierebbe in questo approccio è la scelta della funzione di perdita.
La funzione di perdita utilizzata nel listato 4.21, categorical_crossentropy, prevede
che le etichette seguano una codifica categoriale. Con etichette intere, si dovrebbe usare
sparse_categorical_ crossentropy:

model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])

Questa nuova funzione


di perdita è matematicamente uguale a
categorical_crossentropy, ma ha un'interfaccia diversa.

4.2.7 L'importanza di avere strati intermedi sufficientemente grandi


Abbiamo detto in precedenza che, poiché gli output finali sono a 46 dimensioni, si
dovrebbero evitare strati intermedi con molte meno di 46 unità. Vediamo ora cosa
succede quando introduciamo un collo di bottiglia informativo con strati intermedi
significativamente inferiori a 46 dimensioni: per esempio, 4 dimensioni.

Listato 4.22 Un modello con un collo di bottiglia informativo

model = keras.Sequential([
layers.Dense(64, attivazione="relu"),
layers.Dense(4, attivazione="relu"),
layers.Dense(46,
attivazione="softmax")
])
model.compile(optimizer="rmsprop",
loss="categorical_crossentropy",
metrics=["accuracy"])
model.fit(partial_x_train,
partial_y_train,
epochs=20,
batch_size=128,
validation_data=(x_val, y_val))

Il modello raggiunge ora un'accuratezza di convalida del 71% circa, con un calo
assoluto dell'8%. Questo calo è dovuto principalmente al fatto che stiamo cercando di
comprimere molte informazioni (abbastanza
Prevedere i prezzi delle case: Un esempio di 113
regressione
informazioni per recuperare gli iperpiani di separazione di 46 classi) in uno spazio
intermedio troppo poco dimensionale. Il modello è in grado di stipare la maggior
parte delle informazioni necessarie in queste rappresentazioni quadridimensionali,
ma non tutte.

4.2.8 Ulteriori esperimenti


Come nell'esempio precedente, vi invito a provare i seguenti esperimenti per
allenare la vostra intuizione sul tipo di decisioni di configurazione da prendere con
questi modelli:
◾ Provare a utilizzare strati più o meno grandi: 32 unità, 128 unità e così via.
◾ Sono stati utilizzati due livelli intermedi prima del livello di classificazione
softmax finale. Ora provate a usare un solo strato intermedio o tre strati
intermedi.

4.2.9 Conclusione
Ecco cosa si deve dedurre da questo esempio:
◾ Se si cerca di classificare i punti di dati tra N classi, il modello deve terminare
con un livello Dense di dimensione N.
◾ In un problema di classificazione multiclasse a etichetta singola, il modello
dovrebbe terminare con un'attivazione softmax in modo da produrre una
distribuzione di probabilità sulle N classi di uscita.
◾ La crossentropia categoriale è quasi sempre la funzione di perdita da
utilizzare per questi problemi. Essa minimizza la distanza tra le distribuzioni
di probabilità prodotte dal modello e la vera distribuzione degli obiettivi.
◾ Esistono due modi per gestire le etichette nella classificazione multiclasse:
– Codificare le etichette tramite una codifica categoriale (nota anche come
codifica one-hot) e utilizzare la categorical_crossentropy come funzione
di perdita.
– Codificare le etichette come numeri interi e utilizzare la funzione di perdita
sparse_categoriche_cross- entropia
◾ Se dovete classificare i dati in un gran numero di categorie, dovete evitare di
creare colli di bottiglia informativi nel vostro modello a causa di livelli
intermedi troppo piccoli.

4.3 Prevedere i prezzi delle case: Un esempio di regressione


I due esempi precedenti sono stati considerati problemi di classificazione, in cui
l'obiettivo era prevedere una singola etichetta discreta di un punto di dati in
ingresso. Un altro tipo comune di problema di apprendimento automatico è la
regressione, che consiste nel prevedere un valore continuo invece di un'etichetta
discreta: per esempio, prevedere la temperatura di domani, dati i dati meteorologici,
o prevedere il tempo di completamento di un progetto software, date le sue
specifiche.

NOTA Non confondere la regressione con l'algoritmo di regressione logistica. La


regressione logistica non è un algoritmo di regressione, ma un algoritmo di
114 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione
classificazione.
Prevedere i prezzi delle case: Un esempio di 115
regressione
4.3.1 Il dataset dei prezzi delle abitazioni di Boston
In questa sezione cercheremo di prevedere il prezzo mediano delle case in un
determinato sobborgo di Boston a metà degli anni '70, sulla base di dati relativi al
sobborgo in quel momento, come il tasso di criminalità, l'aliquota dell'imposta
locale sulla proprietà e così via. Il set di dati che utilizzeremo presenta una
differenza interessante rispetto ai due esempi precedenti. Ha un numero
relativamente basso di punti dati: solo 506, suddivisi tra 404 campioni di
addestramento e 102 campioni di test. Inoltre, ogni caratteristica dei dati di input
(ad esempio, il tasso di criminalità) ha una scala diversa. Ad esempio, alcuni valori
sono proporzioni, che assumono valori compresi tra 0 e 1, altri assumono valori
compresi tra 1 e 12, altri ancora tra 0 e 100, e così via.

Listato 4.23 Caricamento del set di dati sulle abitazioni di Boston

from tensorflow.keras.datasets import boston_housing


(train_data, train_targets), (test_data, test_targets) = (
boston_housing.load_data())

Esaminiamo i dati:

>>> train_data.shape
(404, 13)
>>> test_data.shape
(102, 13)

Come si può vedere, abbiamo 404 campioni di addestramento e 102 campioni di test,
ciascuno con 13 caratteristiche numeriche, come il tasso di criminalità pro capite, il
numero medio di stanze per abitazione, l'accessibilità alle autostrade e così via.
Gli obiettivi sono i valori mediani delle abitazioni occupate dai proprietari, in migliaia di dollari:

>>> addestramento_obiettivi
[ 15.2, 42.3, 50. ... 19.4, 19.4, 29.1]

I prezzi sono in genere compresi tra 10.000 e 50.000 dollari. Se vi sembrano prezzi
bassi, r i c o r d a t e che si trattava della metà degli anni '70 e che questi prezzi non
sono stati adeguati all'inflazione.

4.3.2 Preparazione dei dati


Sarebbe problematico inserire in una rete neurale valori che hanno tutti intervalli
molto diversi. Il modello potrebbe essere in grado di adattarsi automaticamente a
dati così eterogenei, ma ciò renderebbe sicuramente più difficile l'apprendimento.
Una best practice diffusa per gestire tali dati è la normalizzazione delle
caratteristiche: per ogni caratteristica dei dati di input (una colonna della matrice dei
dati di input), si sottrae la media della caratteristica e si divide per la deviazione
standard, in modo che la caratteristica sia centrata intorno a 0 e abbia una
deviazione standard unitaria. Questa operazione è facilmente realizzabile in NumPy.

Listato 4.24 Normalizzazione dei dati

media =
116 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione
train_data.mean(axis=0)
train_data -= media
Prevedere i prezzi delle case: Un esempio di 117
regressione
std = train_data.std(axis=0)
train_data /= std
test_data -= media
test_data /= std

Si noti che le quantità utilizzate per normalizzare i dati di test sono calcolate con i
dati di addestramento. Nel flusso di lavoro non si deve mai usare una quantità
calcolata sui dati di test, anche per una cosa semplice come la normalizzazione dei
dati.

4.3.3 Costruire il modello


Poiché i campioni disponibili sono pochi, utilizzeremo un modello molto piccolo con
due strati intermedi, ciascuno con 64 unità. In generale, meno dati di addestramento si
hanno, peggiore sarà l'overfitting e l'uso di un modello piccolo è un modo per mitigare
l'overfitting.

Listato 4.25 Definizione del modello

def build_model():
model = keras.Sequential([
Poiché dobbiamo istanziare lo
layers.Dense(64, stesso modello più volte,
activation="relu"), usiamo una funzione per
layers.Dense(64, costruirlo.
activation="relu"), layers.Dense(1)
])
model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"])
restituire il modello

Il modello termina con una singola unità e nessuna attivazione (sarà uno strato
lineare). Questa è una configurazione tipica per la regressione scalare (una
regressione in cui si cerca di prevedere un singolo valore continuo). L'applicazione
di una funzione di attivazione limiterebbe l'intervallo di valori che l'output può
assumere; per esempio, se si applicasse una funzione di attivazione sigmoide all'ultimo
strato, il modello potrebbe imparare a prevedere solo valori compresi tra 0 e 1. Qui,
poiché l'ultimo strato è puramente lineare, il modello è libero di imparare a
prevedere valori in qualsiasi intervallo.
Si noti che il modello viene compilato con la funzione di perdita mse - errore
quadratico medio, il quadrato della differenza tra le previsioni e gli obiettivi. Si tratta di
una funzione di perdita ampiamente utilizzata per i problemi di regressione.
Durante l'addestramento monitoriamo anche una nuova metrica: l'errore assoluto medio
(MAE). È il valore assoluto della differenza tra le previsioni e gli obiettivi. Ad esempio,
un MAE di 0,5 su questo problema significa che le previsioni sono sbagliate in media di
500 dollari.

4.3.4 Convalida dell'approccio con la validazione K-fold


Per valutare il nostro modello mentre continuiamo a regolare i suoi parametri (come
il numero di epoche utilizzate per l'addestramento), potremmo dividere i dati in un
set di addestramento e in un set di validazione, come abbiamo fatto negli esempi
precedenti. Tuttavia, poiché i dati sono così pochi, l'insieme di validazione finirebbe
118 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

per essere molto piccolo (per esempio, circa 100 esempi). Di conseguenza, i
punteggi di validazione potrebbero cambiare molto a seconda dei punti dati scelti
per la validazione e di quelli scelti per l'addestramento: i punteggi di validazione
Prevedere i prezzi delle case: Un esempio di 119
regressione
potrebbe avere un'elevata varianza rispetto allo split di validazione. Questo ci
impedirebbe d i valutare in modo affidabile il nostro modello.
La pratica migliore in queste situazioni è quella di utilizzare la convalida incrociata K-fold
(vedi figura 4.8).

Dati suddivisi in 3 partizioni

Punteggi
Piegatura 1 Convalida Formazione Formazione
o di
convalida
#1

Punteggi Punteggio
Piegatura 2
o di finale:
convalida medio
Formazione Convalida Formazione
#2

Piegatura 3
Punteggi
Formazione Formazione Convalida
o di
Figura 4.8 Convalida incrociata K-fold con convalida
#3
K=3

Consiste nel dividere i dati disponibili in K partizioni (tipicamente K = 4 o 5), istanziare K


modelli identici e addestrare ciascuno di essi su K - 1 partizioni, valutando al
contempo la partizione rimanente. Il punteggio di convalida per il modello utilizzato
è quindi la media dei K punteggi di convalida ottenuti. In termini di codice, la
procedura è semplice.

Listato 4.26 Convalida K-fold

k = 4
num_val_samples = len(train_data) //
k num_epochs = 100
Prepara i
all_scores = [] dati di convalida:
per i in range(k): dati dalla
print(f "Elaborazione della piega partizione #k
#{i}")
val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]
partial_train_data = np.concatenate(
Prepara i dati di
[train_data[:i * num_val_samples], addestramento: i dati di
train_data[(i + 1) * num_val_samples:]], tutte le altre partizioni.
axis=0)
partial_train_targets = np.concatenate(
[train_targets[:i * num_val_samples],
Costruisce il modello Keras
(già compilato)
train_targets[(i + 1) * num_val_samples:]],
axis=0)
modello = build_model()
Addestra il
model.fit(partial_train_data, partial_train_targets, modello (in
modalità
silenziosa,
verbose = 0)
epochs=num_epochs, batch_size=16, verbose=0)
120 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0)


all_scores.append(val_mae)
Valuta il modello sui
dati di convalida.
Prevedere i prezzi delle case: Un esempio di 121
regressione
Eseguendo questa operazione con num_epochs = 100 si ottengono i seguenti risultati:
>>> tutti i punteggi
[2.112449, 3.0801501, 2.6483836, 2.4275346]
>>> np.mean(all_scores)
2,5671294

Le diverse esecuzioni mostrano punteggi di convalida piuttosto diversi, da 2,1 a 3,1.


La media (2,6) è un parametro molto più affidabile di qualsiasi singolo punteggio: è
questo lo scopo della convalida incrociata K-fold. In questo caso, abbiamo uno
scarto medio di 2.600 dollari, che è significativo se si considera che i prezzi variano
da 10.000 a 50.000 dollari.
Proviamo ad allenare il modello un po' più a lungo: 500 epoche. Per tenere traccia
del rendimento del modello a ogni epoca, modificheremo il ciclo di addestramento per
salvare il registro dei punteggi di validazione per ogni epoca.

Listato 4.27 Salvataggio dei log di convalida a ogni ripiegamento

num_epoche = 500
Prepara i
all_mae_histories = [] dati di convalida:
for i in range(k): dati dalla
print(f "Elaborazione della partizione #k
piega #{i}")
val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
val_targets = train_targets[i * num_val_samples: (i + 1) *
num_val_samples]
partial_train_data = np.concatenate(
Prepara i dati di
[train_data[:i * num_val_samples], addestramento: i
train_data[(i + 1) * num_val_samples:]], dati di tutte le altre
axis=0) partizioni.
partial_train_targets = np.concatenate(
[train_targets[:i * num_val_samples],
train_targets[(i + 1) * Costruisce il
num_val_samples:]], axis=0) modello Keras
modello = build_model() (già compilato)
history = model.fit(partial_train_data, partial_train_targets,
Addestra il
validation_data=(val_data, val_targets), modello
epochs=num_epochs, batch_size=16, verbose=0) (in modalità
mae_history = history.history["val_mae"] silenziosa,
all_mae_histories.append(mae_history) verbose=0)

Possiamo quindi calcolare la media dei punteggi MAE per epoche per tutte
le pieghe.

Listato 4.28 Creazione della storia dei punteggi medi successivi della validazione K-fold

media_mae_storia = [
np.mean([x[i] per x in all_mae_histories]) per i in range(num_epochs)]

Tracciamo questo grafico; si veda la figura 4.9.

Listato 4.29 Tracciare i punteggi di validazione

plt.plot(range(1, len(average_mae_history) + 1), average_mae_history)


plt.xlabel("Epoche")
plt.ylabel("Validazione
MAE") plt.show()
122 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

Figura 4.9 MAE della validazione per epoca

La lettura del grafico può risultare un po' difficile a causa di un problema di scala: il
MAE di convalida per le prime epoche è nettamente superiore ai valori successivi.
Tralasciamo i primi 10 punti di dati, che hanno una scala diversa dal resto della curva.

Listato 4.30 Tracciare i punteggi di validazione, escludendo i primi 10 punti di dati

truncated_mae_history = media_mae_history[10:]
plt.plot(range(1, len(truncated_mae_history) + 1), truncated_mae_history)
plt.xlabel("Epoche")
plt.ylabel("Validazione MAE")
plt.show()

Come si può vedere nella figura 4.10, il MAE della validazione smette di migliorare
significativamente dopo 120-140 epoche (questo numero include le 10 epoche che
abbiamo omesso). Oltre questo punto, inizia l'overfitting.
Una volta terminata la regolazione degli altri parametri del modello (oltre al
numero di epoche, si potrebbe anche regolare la dimensione degli strati intermedi),
si può addestrare un modello di produzione finale su tutti i dati di addestramento,
con i parametri migliori, e poi esaminare le sue prestazioni sui dati di test.

Listato 4.31 Formazione del modello finale


Ottiene un fresco,
modello = build_model() modello compilato Treni it on the
model.fit(train_data, train_targets, l'insieme dei dati
epochs=130, batch_size=16, verbose=0)
test_mse_score, test_mae_score = model.evaluate(test_data, test_target)
Prevedere i prezzi delle case: Un esempio di 123
regressione

Figura 4.10 MAE della validazione per epoch, esclusi i primi 10 punti di dati

Ecco il risultato finale:

>>> test_mae_score
2.4642276763916016

Siamo ancora indietro di poco meno di 2.500 dollari. È un miglioramento! Come per i
due compiti precedenti, si può provare a variare il numero di strati del modello o il
numero di unità per strato, per vedere se si riesce a ottenere un errore di prova
inferiore.

4.3.5 Generare previsioni su nuovi dati


Quando si chiama predict() sul nostro modello di classificazione binaria, si
recupera un punteggio scalare compreso tra 0 e 1 per ogni campione in ingresso. Con il
nostro modello di classificazione multiclasse, abbiamo recuperato una distribuzione di
probabilità su tutte le classi per ogni campione. Ora, con questo modello di regressione,
predict() restituisce l'ipotesi del modello per il prezzo del campione in migliaia di
dollari:

>>> previsioni = model.predict(test_data)


>>> previsioni[0]
array([9.990133], dtype=float32)

Si prevede che la prima casa dell'insieme di test abbia un prezzo di circa 10.000 dollari.

4.3.6 Conclusione
Ecco cosa si deve dedurre da questo esempio di regressione scalare:
◾ La regressione viene eseguita utilizzando funzioni di perdita diverse da quelle
utilizzate per la classificazione. L'errore quadratico medio (MSE) è una funzione
di perdita comunemente utilizzata per la regressione.
124 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

◾ Allo stesso modo, le metriche di valutazione da utilizzare per la regressione


differiscono da quelle utilizzate per la classificazione; naturalmente, il concetto di
accuratezza non si applica alla regressione. Una metrica di regressione comune è
l'errore assoluto medio (MAE).
◾ Quando le caratteristiche dei dati di input hanno valori in intervalli diversi,
ogni caratteristica deve essere scalata in modo indipendente come fase di
preelaborazione.
◾ Quando i dati disponibili sono pochi, l'uso della convalida K-fold è un ottimo
modo per valutare in modo affidabile un modello.
◾ Quando sono disponibili pochi dati di addestramento, è preferibile utilizzare un
modello piccolo con pochi strati intermedi (in genere solo uno o due), per evitare
un grave overfitting.

Sintesi
◾ I tre tipi più comuni di compiti di apprendimento automatico su dati vettoriali
sono la classificazione binaria, la classificazione multiclasse e la regressione
scalare.
– Le sezioni "Conclusione" all'inizio del capitolo riassumono i punti importanti
appresi per ciascun compito.
– La regressione utilizza funzioni di perdita e metriche di valutazione diverse
rispetto alla classificazione.
◾ Di solito è necessario preelaborare i dati grezzi prima di inserirli in una rete neurale.
◾ Quando i dati hanno caratteristiche con intervalli diversi, scalare ogni
caratteristica in modo indipendente come parte della preelaborazione.
◾ Con il progredire dell'addestramento, le reti neurali finiscono per sovra-adattarsi e
ottenere risultati peggiori su dati mai visti prima.
◾ Se non s i dispone di molti dati di addestramento, si può utilizzare un modello
piccolo con solo uno o due livelli intermedi, per evitare un grave overfitting.
◾ Se i dati sono suddivisi in molte categorie, si possono creare dei colli di bottiglia
se i livelli intermedi sono troppo piccoli.
◾ Quando si lavora con pochi dati, la validazione K-fold può aiutare a valutare in
modo affidabile il modello.
Fondamenti
di apprendimento
automatico

Questo capitolo tratta


◾ Comprendere la tensione tra generalizzazione e
ottimizzazione, il problema fondamentale
dell'apprendimento automatico.
◾ Metodi di valutazione dei modelli di apprendimento
automatico
◾ Le migliori pratiche per migliorare l'adattamento
del modello
◾ Le migliori pratiche per ottenere una migliore
generalizzazione

Dopo i tre esempi pratici del capitolo 4, dovreste aver iniziato a capire come
affrontare i problemi di classificazione e regressione con le reti neurali e avete
visto il problema centrale dell'apprendimento automatico: l'overfitting. Questo
capitolo formalizzerà alcune delle vostre nuove intuizioni sull'apprendimento
automatico in un solido quadro concettuale, evidenziando l'importanza di una
valutazione accurata dei modelli e l'equilibrio tra addestramento e
generalizzazione.

5.1 Generalizzazione: L'obiettivo dell'apprendimento automatico


Nei tre esempi presentati nel capitolo 4 - previsione di recensioni di film,
classificazione di argomenti e regressione dei prezzi delle case - abbiamo suddiviso i
dati in un set di addestramento, un set di validazione e un set di test. Il motivo per cui
non si valutano i modelli sugli stessi dati è che
126 CAPITOLO 4 Come iniziare con le reti neurali: Classificazione e regressione

121
122 CAPITOLO 5 Fondamenti dell'apprendimento
automatico
Dopo poche epoche, le prestazioni su dati mai visti hanno iniziato a divergere da
quelle sui dati di addestramento, che migliorano sempre con il progredire
dell'addestramento. I modelli hanno iniziato ad adattarsi eccessivamente. L'overfitting si
verifica in ogni problema di apprendimento automatico.
Il problema fondamentale dell'apprendimento automatico è la tensione tra
ottimizzazione e generalizzazione. L'ottimizzazione si riferisce al processo di
regolazione di un modello per ottenere le migliori prestazioni possibili sui dati di
addestramento (l'apprendimento nell'apprendimento automatico), mentre la
generalizzazione si riferisce alle prestazioni del modello addestrato su dati che non
ha mai visto prima. L'obiettivo del gioco è ottenere una buona generalizzazione,
naturalmente, ma non si può controllare la generalizzazione; si può solo adattare il
modello ai dati di addestramento. Se lo si fa troppo bene, si verifica un overfitting e
la generalizzazione ne risente.
Ma cosa causa l'overfitting? Come possiamo ottenere una buona generalizzazione?

5.1.1 Underfitting e overfitting


Per i modelli visti nel capitolo precedente, le prestazioni sui dati di validazione
trattenuti hanno iniziato a migliorare con il proseguire dell'addestramento, per poi
raggiungere inevitabilmente un picco dopo un po' di tempo. Questo schema
(illustrato nella figura 5.1) è universale. Si può osservare con qualsiasi tipo di
modello e con qualsiasi set di dati.

Curva di
Perdi formazione
ta Curva di
valor Underfitting
validazione
e

Overfitting

Adattame
nto
robusto

Tempo di
formazione

Figura 5.1 Comportamento di overfitting canonico

All'inizio dell'addestramento, l'ottimizzazione e la generalizzazione sono correlate:


minore è la perdita sui dati di addestramento, minore è la perdita sui dati di test.
Mentre ciò accade, si dice che il modello è underfit: ci sono ancora progressi da fare;
la rete non ha ancora modellato tutti i modelli rilevanti nei dati di addestramento.
Ma dopo un certo numero di iterazioni sui dati di addestramento, la
generalizzazione smette di migliorare, le metriche di validazione si bloccano e
iniziano a peggiorare: il modello inizia a essere overfit. In altre parole, sta iniziando
ad apprendere modelli specifici per i dati di addestramento, ma che sono fuorvianti
Generalizzazione: L'obiettivo 123
o irrilevanti quando dell'apprendimento
si tratta di nuoviautomatico
dati.
124 CAPITOLO 5 Fondamenti dell'apprendimento
automatico
È particolarmente probabile che si verifichi un overfitting quando i dati sono
rumorosi, se comportano incertezza o se includono caratteristiche rare. Vediamo alcuni
esempi concreti.
DATI DI FORMAZIONE RUMOROSI
Nei dataset del mondo reale, è abbastanza comune che alcuni input non siano validi.
Ad esempio, una cifra MNIST potrebbe essere un'immagine completamente nera, o
qualcosa di simile alla figura 5.2.

Figura 5.2 Alcuni strani campioni


di allenamento MNIST

Cosa sono questi? Non lo so nemmeno io. Ma fanno tutti parte dell'insieme di
addestramento MNIST. La cosa peggiore, tuttavia, è avere input perfettamente validi
che finiscono con l'essere etichettati in modo errato, come quelli della figura 5.3.

Figura 5.3 Campioni di addestramento MNIST con etichetta errata

Se un modello si sforza di incorporare questi outlier, le sue prestazioni di


generalizzazione si deteriorano, come mostrato nella figura 5.4. Ad esempio, un 4
che sembra molto simile al 4 erroneamente etichettato nella figura 5.3 potrebbe
finire per essere classificato come un 9.
Generalizzazione: L'obiettivo 125
dell'apprendimento automatico

Figura 5.4 Gestire gli outlier: robust fit vs. overfitting

CARATTERISTICHE AMBIGUE
Non tutto il rumore dei dati deriva da imprecisioni: anche i dati perfettamente puliti e
ben etichettati possono essere rumorosi quando il problema comporta incertezza e
ambiguità. Nei compiti di classificazione, spesso accade che alcune regioni dello spazio
delle caratteristiche di input siano associate a più classi contemporaneamente.
Supponiamo che si stia sviluppando un modello che prende l'immagine di una banana e
predice se la banana è acerba, matura o marcia. Queste categorie non hanno confini
oggettivi, quindi la stessa immagine potrebbe essere classificata come acerba o matura
da diversi etichettatori umani. Allo stesso modo, molti problemi implicano la casualità.
Si possono usare i dati sulla pressione atmosferica per prevedere se domani pioverà, ma
le stesse misure possono essere seguite a volte dalla pioggia e a volte da un cielo sereno,
con una certa probabilità.
Un modello potrebbe adattarsi in modo eccessivo a tali dati probabilistici, essendo
troppo fiducioso su regioni ambigue dello spazio delle caratteristiche, come nella figura
5.5. Un adattamento più robusto ignorerebbe i singoli punti di dati e guarderebbe al
quadro generale.

Area di incertezza

Figura 5.5 Adattamento robusto e overfitting per un'area ambigua dello spazio delle caratteristiche
126 CAPITOLO 5 Fondamenti dell'apprendimento
automatico
CARATTERISTICHE RARE E CORRELAZIONI SPURIE
Se avete visto solo due gatti soriani arancioni nella vostra vita, ed entrambi e r a n o
terribilmente antisociali, potreste dedurre che i gatti soriani arancioni sono generalmente
inclini ad essere antisociali. Si tratta di un overfitting: se foste stati esposti a una più
ampia varietà di gatti, compresi quelli arancioni, avreste imparato che il colore del gatto
non è ben correlato al carattere.
Allo stesso modo, i modelli di apprendimento automatico addestrati su insiemi
di dati che includono valori di caratteristiche rare sono altamente suscettibili di
overfitting. In un compito di classificazione del sentiment, se la parola "cherimoya"
(un frutto originario delle Ande) compare solo in un testo nei dati di addestramento
e questo testo è negativo nel sentiment, un modello mal regolarizzato potrebbe
attribuire un peso molto elevato a questa parola e classificare sempre come negativi
i nuovi testi che menzionano le cherimoya, mentre, oggettivamente, non c'è nulla di
negativo nelle cherimoya.1
È importante notare che non è necessario che un valore di caratteristica si
presenti solo un paio di volte per portare a correlazioni spurie. Si consideri una
parola che ricorre in 100 campioni nei dati di addestramento e che è associata a un
sentimento positivo il 54% delle volte e a un sentimento negativo il 46% delle volte.
Questa differenza potrebbe essere un puro caso statistico, ma è probabile che il modello
impari a sfruttare questa caratteristica per la sua attività di classificazione. Questa è una
delle fonti più comuni di overfitting.
Ecco un esempio lampante. Prendiamo MNIST. Creare un nuovo set di addestramento
concatenando 784 dimensioni di rumore bianco alle 784 dimensioni esistenti dei dati,
in modo che metà dei dati sia ora costituita da rumore. Per confronto, creare un set
di dati equivalente concatenando 784 dimensioni di rumore. La nostra
concatenazione di caratteristiche senza senso non influisce affatto sul contenuto
informativo dei dati: stiamo solo aggiungendo qualcosa. L'accuratezza della
classificazione umana non sarebbe affatto influenzata da queste trasformazioni.

Listato 5.1 Aggiunta di canali di rumore bianco o di canali all-zeros a MNIST

da tensorflow.keras.datasets importa mnist


importare numpy come np

(train_images, train_labels), _ = mnist.load_data()


train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255

train_images_with_noise_channels = np.concatenate(
[train_images, np.random.random((len(train_images), 784))], axis=1)

train_images_with_zeros_channels = np.concatenate(
[train_images, np.zeros((len(train_images), 784))], axis=1)

Ora, addestriamo il modello del capitolo 2 su entrambi i set di allenamento.

1 Mark Twain lo definì addirittura "il frutto più delizioso conosciuto dagli uomini".
Generalizzazione: L'obiettivo 127
dell'apprendimento automatico

Listato 5.2 Addestramento dello stesso modello su dati MNIST con canali di rumore o c a n a l i tutti a zero
da tensorflow importa keras
da tensorflow.keras importa strati

def get_model():
model = keras.Sequential([
layers.Dense(512, attivazione="relu"),
layers.Dense(10,
attivazione="softmax")
])
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
restituire il modello

model = get_model()
history_noise = model.fit(
train_images_with_noise_channels, train_labels,
epochs=10,
batch_size=128,
validation_split=0.2)

model = get_model()
history_zeros = model.fit(
train_images_with_zeros_channels, train_labels,
epochs=10,
batch_size=128,
validation_split=0.2)

Confrontiamo l'evoluzione dell'accuratezza della convalida di ciascun modello nel tempo.

Listato 5.3 Confronto dell'accuratezza della convalida

importare matplotlib.pyplot come plt


val_acc_noise =
history_noise.history["val_accuracy"] val_acc_zeros
= history_zeros.history["val_accuracy"] epochs =
range(1, 11)
plt.plot(epochs, val_acc_noise, "b-",
label="Accuratezza della validazione con canali di rumore")
plt.plot(epochs, val_acc_zeros, "b--",
label="Accuratezza della validazione con canali a zero")
plt.title("Effetto dei canali di rumore sull'accuratezza della
validazione") plt.xlabel("Epoche")
plt.ylabel("Precisione")
plt.legend()

Nonostante i dati contengano le stesse informazioni in entrambi i casi, l'accuratezza


della convalida del modello addestrato con i canali di rumore finisce per essere
inferiore di circa un punto percentuale (vedere figura 5.6), esclusivamente a causa
dell'influenza delle correlazioni spurie. Più canali di rumore si aggiungono, più
l'accuratezza si riduce.
Le caratteristiche rumorose portano inevitabilmente a un overfitting. Per questo
motivo, nei casi in cui non si è sicuri che le caratteristiche disponibili siano informative o
distraenti, è comune fare delle feature
128 CAPITOLO 5 Fondamenti dell'apprendimento
automatico

Figura 5.6 Effetto dei canali di rumore sull'accuratezza della validazione

selezione prima dell'addestramento. Limitare i dati di IMDB alle 10.000 parole più
comuni era una forma grezza di selezione delle caratteristiche, ad esempio. Il modo
tipico di selezionare le caratteristiche è quello di calcolare un punteggio di utilità per
ogni caratteristica disponibile - una misura di quanto sia informativa la caratteristica
rispetto al compito, come l' informazione reciproca tra la caratteristica e le etichette - e
mantenere solo le caratteristiche che sono al di sopra di una certa soglia. In questo modo
si filtrano i canali di rumore bianco dell'esempio precedente.

5.1.2 La natura della generalizzazione nell'apprendimento profondo


Un fatto notevole dei modelli di deep learning è che possono essere addestrati per
adattarsi a qualsiasi cosa, purché abbiano una potenza rappresentativa sufficiente.
Non mi credete? Provate a rimescolare le etichette di MNIST e ad addestrare un
modello su di esse. Anche se non c'è alcuna relazione tra gli input e le etichette
mischiate, la perdita di addestramento diminuisce bene, anche con un modello
relativamente piccolo. Naturalmente, la perdita di validazione non migliora affatto
nel tempo, poiché in questo caso non c'è possibilità di generalizzazione.

Listato 5.4 Adattamento di un modello MNIST con etichette rimescolate in modo casuale

(train_images, train_labels), _ = mnist.load_data()


train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255

random_train_labels = train_labels[:]
np.random.shuffle(random_train_labels)

model = keras.Sequential([
layers.Dense(512, activation="relu"),
Generalizzazione: L'obiettivo 129
dell'apprendimento automatico
layers.Dense(10, attivazione="softmax")
])
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
model.fit(train_images, random_train_labels,
epochs=100,
batch_size=128,
validation_split=0.2)

In realtà, non è nemmeno necessario farlo con i dati MNIST: basta generare input di
rumore bianco ed etichette casuali. Si potrebbe adattare un modello anche a questo,
purché abbia abbastanza parametri. Il modello finirebbe per memorizzare gli input
specifici, proprio come un dizionario Python.
Se questo è il caso, allora come mai i modelli di deep learning generalizzano? Non
dovrebbero semplicemente imparare una mappatura ad hoc tra gli input e gli obiettivi
dell'addestramento, come una sorta di dettatura? Che aspettativa abbiamo che questa
mappatura funzioni per i nuovi input?
Come si evince, la natura della generalizzazione nell'apprendimento profondo ha poco a
che fare con
con i modelli di deep learning stessi, e molto a che fare con la struttura delle
informazioni nel mondo reale. Diamo un'occhiata a ciò che sta realmente accadendo.
L'IPOTESI DEL MANIFOLD
L'input di un classificatore MNIST (prima della preelaborazione) è una matrice 28
× 28 di numeri interi compresi tra 0 e 255. Il numero totale di possibili valori di
input è quindi 256 alla potenza di 784, molto più grande del numero di atomi
dell'universo. Il numero totale di possibili valori di input è quindi 256 alla potenza
di 784, molto più grande del numero di atomi dell'universo. Tuttavia, pochissimi di
questi input assomigliano a campioni MNIST validi: le cifre effettivamente scritte a
mano sono soltanto
occupano un piccolo sottospazio dello spazio genitore di tutte le possibili matrici 28 × 28 uint8.
Cosa è
Inoltre, questo sottospazio non è solo un insieme di punti sparsi a caso nello spazio
genitore: è altamente strutturato.
In primo luogo, il sottospazio delle cifre valide scritte a mano è continuo: se si
prende un campione e lo si modifica leggermente, sarà ancora riconoscibile come la
stessa cifra scritta a mano. Inoltre, tutti i campioni nel sottospazio valido sono collegati
da percorsi lisci che attraversano il sottospazio. Ciò significa che se si prendono due
cifre MNIST casuali A e B, esiste una sequenza di immagini "intermedie" che
trasformano A in B, in modo che due cifre concrete siano molto vicine tra loro (vedi
figura 5.7). Forse ci saranno alcune forme ambigue vicino al confine tra due classi, ma
anche queste forme sembrerebbero comunque molto simili a cifre.
In termini tecnici, si potrebbe dire che le cifre scritte a mano formano un collettore
all'interno dello spazio delle possibili matrici 28 × 28 uint8. È una parola grossa, ma il
concetto è piuttosto intuitivo. Un "manifold" è un sottospazio di dimensioni inferiori di
un qualche spazio genitore che è
localmente simile a uno spazio lineare (euclideo). Per esempio, una curva liscia nel
piano è un manifold 1D all'interno di uno spazio 2D, perché per ogni punto della curva è
130 CAPITOLO 5 Fondamenti dell'apprendimento
possibile tracciare una tangente (la curva può essere approssimata da una linea in ogni
automatico
punto). Una superficie liscia in uno spazio 3D è un manifesto 2D. E così via.
Generalizzazione: L'obiettivo 131
dell'apprendimento automatico

Figura 5.7 Diverse cifre MNIST si trasformano gradualmente l'una nell'altra, mostrando che lo spazio
delle cifre scritte a mano forma un "collettore". Questa immagine è stata generata usando il codice
del capitolo 12.

Più in generale, l'ipotesi del collettore sostiene che tutti i dati naturali si trovano su un
collettore a bassa dimensionalità all'interno dello spazio ad alta dimensione in cui sono
codificati. È un'affermazione piuttosto forte sulla struttura dell'informazione
nell'universo. Per quanto ne sappiamo, è accurata ed è il motivo per cui il deep learning
funziona. È vero per le cifre MNIST, ma anche per i volti umani, la morfologia degli
alberi, i suoni della voce umana e persino il linguaggio naturale.
L'ipotesi del manifesto implica che
◾ I modelli di apprendimento automatico devono adattarsi solo a sottospazi
relativamente semplici, a bassa dimensione e altamente strutturati all'interno del
loro spazio potenziale di input (mani- ficazioni latenti).
◾ All'interno di uno di questi collettori, è sempre possibile interpolare tra due input,
cioè trasformarne uno in un altro attraverso un percorso continuo lungo il quale
tutti i punti cadono sul collettore.
La capacità di interpolare tra i campioni è la chiave per comprendere la
generalizzazione nel deep learning.
L'INTERPOLAZIONE COME FONTE DI GENERALIZZAZIONE
Se si lavora con punti di dati che possono essere interpolati, si può iniziare a dare un
senso a punti mai visti prima mettendoli in relazione con altri punti che si trovano
vicini sul manifold. In altre parole, è possibile dare un senso alla totalità dello spazio
utilizzando solo un campione dello spazio. È possibile utilizzare l'interpolazione per
riempire gli spazi vuoti.
Si noti che l'interpolazione sul manifold latente è diversa dall'interpolazione
lineare nello spazio genitore, come illustrato nella figura 5.8. Ad esempio, la media
dei pixel tra due cifre MNIST di solito non è una cifra valida.
È importante sottolineare che, sebbene l'apprendimento profondo raggiunga la
generalizzazione attraverso l'interpolazione su un'approssimazione appresa del
manifold dei dati, sarebbe un errore pensare che l'interpolazione sia tutto ciò che
riguarda la generalizzazione. È la punta dell'iceberg. L'interpolazione può solo
aiutare a dare un senso a cose molto simili a quelle già viste:
132 CAPITOLO 5 Fondamenti dell'apprendimento
automatico

Interpolazione del
collettore (punto
intermedio
sul manifold latente)
Figura 5.8 Differenza tra
interpolazione lineare e
interpolazione sul manifold latente.
Ogni punto del manifold latente di
Interpolazione lineare cifre è una cifra valida, ma la media
(media nello spazio di codifica) di due cifre di solito non lo è.

consente la generalizzazione locale. Ma è sorprendente che gli esseri umani abbiano


sempre a che fare con la novità estrema, e se la cavano benissimo. Non è necessario
essere addestrati in anticipo con innumerevoli esempi di tutte le situazioni che si
possono incontrare. Ogni singolo giorno è diverso da qualsiasi altro giorno vissuto in
precedenza e da qualsiasi altro giorno vissuto da chiunque fin dagli albori dell'umanità.
È possibile passare da u n a settimana a New York a una settimana a Shanghai e a una
settimana a Bangalore senza bisogno di migliaia di vite di apprendimento e di prove per
ogni città.
Gli esseri umani sono capaci di una generalizzazione estrema, che è consentita da
meccanismi cognitivi diversi dall'interpolazione: astrazione, modelli simbolici del
mondo, ragionamento, logica, buon senso, conoscenze innate sul mondo - ciò che
generalmente chiamiamo ragione, in contrapposizione all'intuizione e al riconoscimento
dei modelli. Questi ultimi sono in gran parte di natura interpolativa, ma i primi non lo
sono. Entrambe sono essenziali per l'intelligenza. N e parleremo più diffusamente nel
capitolo 14.
PERCHÉ L'APPRENDIMENTO PROFONDO FUNZIONA
Ricordate la metafora della palla di carta accartocciata del capitolo 2? Un foglio di
carta rappresenta un manifold 2D nello spazio 3D (vedi figura 5.9). Un modello di
deep learning è uno strumento che consente di liberare le palline di carta, cioè di
districare i manifold latenti.

Figura 5.9 Scrittura di un


complicato collettore di dati

Un modello di deep learning è fondamentalmente una curva ad alta dimensionalità, una


curva liscia e continua (con vincoli aggiuntivi sulla sua struttura, derivanti dai priori
dell'architettura del modello), poiché deve essere differenziabile. La curva viene adattata
ai punti dati tramite discesa del gradiente, in modo graduale e incrementale. Per sua
natura, l'apprendimento profondo consiste nel prendere una curva grande e complessa,
un manifold, e nel regolare in modo incrementale i suoi parametri finché non si adatta ai
punti di dati di addestramento.
Generalizzazione: L'obiettivo 133
dell'apprendimento automatico
La curva comporta un numero di parametri tale da potersi adattare a qualsiasi
cosa; infatti, se si lascia addestrare il modello per un tempo sufficientemente lungo,
finirà per memorizzare puramente i dati di addestramento e non generalizzerà affatto.
Tuttavia, i dati che si stanno adattando non sono costituiti da punti isolati e
scarsamente distribuiti nello spazio sottostante. I dati formano un collettore
altamente strutturato e poco dimensionale all'interno dello spazio di input: questa è
l'ipotesi del mani- fold. Poiché l'adattamento della curva del modello a questi dati
avviene in modo graduale e regolare nel corso del tempo, con l'avanzare della
discesa del gradiente, durante l'addestramento ci sarà un punto intermedio in cui il
modello si approssima approssimativamente al collettore naturale dei dati, come si
vede nella figura 5.10.

Ulteriore addestramento:
Prima Inizio un adattamento robusto si Stato finale: il modello si
dell'addestrame dell'addestramento: il ottiene, in modo adatta in modo eccessivo ai
nto: il modello si modello viene transitorio, nel processo di dati di addestramento,
avvia gradualmente morphing del modello dal raggiungendo una perdita di
con uno stato iniziale si muove verso un migliore suo stato iniziale a quello addestramento perfetta.
casuale. adattamento. finale.

Tempo di prova:
Tempo di prova: prestazioni del modello
prestazioni del overfit
modello robusto su su nuovi punti di dati
nuovi punti di dati

Figura 5.10 Passaggio da un modello casuale a un modello overfit e raggiungimento di un adattamento robusto come stato
intermedio

Lo spostamento lungo la curva appresa dal modello a quel punto si avvicinerà allo
spostamento lungo l'effettivo manifold latente dei dati; in questo modo, il modello
sarà in grado di dare un senso a input mai visti prima attraverso l'interpolazione tra
gli input di addestramento.
Oltre al fatto banale che hanno una potenza rappresentativa sufficiente, ci sono
alcune proprietà dei modelli di apprendimento profondo che li rendono
particolarmente adatti all'apprendimento di manifold latenti:
◾ I modelli di apprendimento profondo implementano una mappatura liscia e
134 CAPITOLO 5 Fondamenti dell'apprendimento
automatico

continua dai loro ingressi alle loro uscite. Deve essere liscia e continua perché
deve essere necessariamente differenziabile (altrimenti non si potrebbe fare la
discesa del gradiente).
Generalizzazione: L'obiettivo 135
dell'apprendimento automatico
Questa levigatezza aiuta ad approssimare i collettori latenti, che seguono le
stesse proprietà.
◾ I modelli di apprendimento profondo tendono a essere strutturati in modo da
rispecchiare la "forma" delle informazioni presenti nei dati di addestramento
(tramite i priori di architettura). Questo è particolarmente il caso dei modelli
di elaborazione delle immagini (discussi nei capitoli 8 e 9) e dei modelli di
elaborazione delle sequenze (capitolo 10). Più in generale, le reti neurali
profonde strutturano le loro rappresentazioni apprese in modo gerarchico e
modulare, che richiama il modo in cui sono organizzati i dati naturali.
I DATI DI FORMAZIONE SONO FONDAMENTALI
Sebbene il deep learning sia effettivamente adatto all'apprendimento manifold, il
potere di generalizzare è più una conseguenza della struttura naturale dei dati che
una conseguenza di qualsiasi proprietà del modello. La generalizzazione è possibile solo se
i dati formano un insieme in cui i punti possono essere interpolati. Quanto più le
caratteristiche sono informative e meno rumorose, tanto più sarà possibile
generalizzare, poiché lo spazio di input sarà più semplice e meglio strutturato. La
cura dei dati e l'ingegneria delle caratteristiche sono essenziali per la
generalizzazione.
Inoltre, poiché l'apprendimento profondo si basa sull'adattamento delle curve, per
ottenere buone prestazioni un modello deve essere addestrato su un campionamento
denso del suo spazio di input. Un "campionamento denso" in questo contesto significa
che i dati di addestramento devono coprire densamente la totalità del manifold dei dati
di input (vedi figura 5.11). Ciò è particolarmente vero in prossimità dei confini
decisionali. Con un campionamento sufficientemente denso, diventa possibile dare un
senso ai nuovi input interpolando i dati di addestramento passati senza dover ricorrere al
buon senso, al ragionamento astratto o alla conoscenza esterna del mondo, tutte cose a
cui i modelli di apprendimento automatico non hanno accesso.

Spazio latente originale

Campionamento Campionamento
sparso: il modello denso: il modello
appreso non appreso
corrisponde allo approssima bene
spazio latente e porta lo spazio latente, e
a un'interpolazione l'interpolazione
errata. porta alla generalizzazione.

Figura 5.11 Per apprendere un modello in grado di generalizzare con precisione è


necessario un campionamento denso dello spazio di input.
Valutazione dei modelli di 133
apprendimento automatico
Per questo motivo, bisogna sempre tenere presente che il modo migliore per
migliorare un modello di deep learning è quello di addestrarlo su un maggior
numero di dati o su dati migliori (naturalmente, l'aggiunta di dati eccessivamente
rumorosi o inac- curati danneggia la generalizzazione). Una copertura più fitta del
manifold dei dati di input produrrà un modello che generalizza meglio. Non ci si
deve mai aspettare che un modello di apprendimento profondo esegua qualcosa di
più di una rozza interpolazione tra i suoi campioni di addestramento e quindi si deve
fare tutto il possibile per rendere l'interpolazione il più semplice possibile. L'unica
cosa che troverete in un modello di deep learning è ciò che gli avete messo dentro: i
priori codificati nella sua architettura e i dati su cui è stato addestrato.
Quando non è possibile ottenere più dati, la soluzione migliore è quella di
modulare la quantità di informazioni che il modello è in grado di memorizzare, o di
aggiungere vincoli sulla morbidezza della curva del modello. Se una rete può
permettersi di memorizzare solo un piccolo numero di modelli, o modelli molto
regolari, il processo di ottimizzazione la costringerà a concentrarsi sui modelli più
importanti, che hanno maggiori possibilità di generalizzare bene. Il processo di lotta
all'overfitting si chiama regolarizzazione. Le tecniche di regolarizzazione saranno
analizzate in modo approfondito nella sezione 5.4.4.
Prima di iniziare a modificare il modello per aiutarlo a generalizzare meglio, è
necessario un modo per valutare come sta andando il modello. Nella sezione che
segue, scoprirete come monitorare la generalizzazione durante lo sviluppo del
modello: la valutazione del modello.

5.2 Valutazione dei modelli di apprendimento automatico


Si può controllare solo ciò che si può osservare. Poiché il vostro obiettivo è quello
di sviluppare modelli in grado di generalizzare con successo a nuovi dati, è essenziale
essere in grado di misurare in modo affidabile il potere di generalizzazione del vostro
modello. In questa sezione presenterò formalmente i diversi modi in cui è possibile
valutare i modelli di apprendimento automatico. La maggior parte di essi è già stata vista in
azione nel capitolo precedente.

5.2.1 Set di addestramento, validazione e test


La valutazione di un modello si riduce sempre alla suddivisione dei dati disponibili
in tre insiemi: formazione, validazione e test. Ci si allena sui dati di addestramento e
si valuta il modello sui dati di convalida. Una volta che il modello è pronto per la
prima serata, lo si testa un'ultima volta sui dati di test, che devono essere il più
possibile simili ai dati di produzione. A questo punto si può distribuire il modello in
produzione.
Ci si può chiedere: perché non avere due set: un set di allenamento e un set di test?
Si addestrerebbe sui dati di addestramento e si valuterebbe sui dati di test. Molto più
semplice!
Il motivo è che lo sviluppo di un modello implica sempre la messa a punto della sua
configurazione: ad esempio, la scelta del numero di strati o della dimensione degli strati
(chiamati iperpara- metri del modello, per distinguerli dai parametri, che sono i pesi
della rete). La messa a punto avviene utilizzando come segnale di feedback le
134 CAPITOLO 5 Fondamenti dell'apprendimento
prestazioni del modelloautomatico
sui dati di convalida. In sostanza, questa messa a punto è una
forma di apprendimento: una ricerca di una buona configurazione in uno spazio di
parametri. Di conseguenza, la messa a punto della configurazione del modello in base
alle sue prestazioni sul set di validazione può portare rapidamente a un adattamento
eccessivo al set di validazione, anche se il modello non è mai stato addestrato
direttamente su di esso.
Valutazione dei modelli di 135
apprendimento automatico
Al centro di questo fenomeno c'è la nozione di fuga di informazioni. Ogni volta
che si mette a punto un iperparametro del modello in base alle prestazioni del
modello sul set di validazione, alcune informazioni sui dati di validazione trapelano
nel modello. Se lo si fa una sola volta, per un solo parametro, le informazioni che
trapelano sono pochissime e il set di validazione rimane affidabile per la valutazione
del modello. Ma se si ripete l'operazione più volte, eseguendo un esperimento,
valutando l'insieme di convalida e modificando di conseguenza il modello, allora si
disperderà nel modello una quantità sempre più significativa di informazioni
sull'insieme di convalida.
Alla fine della giornata, vi ritroverete con un modello che funziona
artificialmente bene sui dati di convalida, perché è per questo che lo avete
ottimizzato. A voi interessa la performance su dati completamente nuovi, non su
quelli di convalida, quindi dovete usare un set di dati completamente diverso e mai
visto prima per valutare il modello: il set di dati di test. Il modello non dovrebbe
avere accesso ad alcuna informazione sull'insieme di test, nemmeno indirettamente.
Se qualcosa del modello è stato regolato in base alle prestazioni del set di test, la
misura della generalizzazione sarà errata.
La suddivisione dei dati in insiemi di addestramento, convalida e test può sembrare
semplice, ma ci sono alcuni modi avanzati per farlo che possono essere utili quando i
dati disponibili sono pochi. Passiamo in rassegna tre classiche ricette di valutazione: la
semplice validazione holdout, la validazione K-fold e la validazione K-fold iterata con
rimescolamento. Parleremo anche dell'uso di linee di base di buon senso per verificare
che l'addestramento stia andando da qualche parte.
CONVALIDA SEMPLICE DELL'HOLDOUT
Selezionare una frazione dei dati come set di test. Allenatevi sui dati rimanenti e
valutate l'insieme di prova. Come si è visto nelle sezioni precedenti, per evitare fughe di
informazioni, non si dovrebbe sintonizzare il modello in base all'insieme di prova e
quindi si dovrebbe riservare anche un insieme di convalida.
Schematicamente, la convalida dell'holdout si presenta come nella figura 5.12. Il
listato 5.5 mostra una semplice implementazione.

Totale dei dati etichettati disponibili

Set di
Set di formazione convalida
per l'uscita

Allenarsi su Figura 5.12 Semplice


que divisione di convalida
stoValu dell'holdout
tare su
questo

Listato 5.5 Convalida dell'holdout (notare che le etichette sono state omesse per semplicità)
10000 np.random.shuffle(dati)
num_validazione_campioni =
136 CAPITOLO 5 Fondamenti dell'apprendimento
automatico

Di solito è
opportuno
rimescolare i
dati.
Valutazione dei modelli di 137
apprendimento automatico
validation_data = data[:num_validation_samples]
Definisce la Definisce l'insieme di
convalida training_data = data[num_validation_samples:]
set model = get_model() addestramento
model.fit(training_data, ...)
validation_score = model.evaluate(validation_data, ...) Addestra un modello
sull'insieme di dati
dati di addestramento e la
valuta sui dati di
convalida.

... A questo punto è possibile mettere a punto il


modello, riqualificarlo, valutarlo e
metterlo a punto di nuovo.
modello =
get_model() Una volta messa a punto la
model.fit(np.concatenate([training_data, è comune addestrare il modello
validation_data]), ...) finale da zero su tutti i dati non di
test_score = model.evaluate(test_data, ...) prova disponibili.

Questo è il protocollo di valutazione più semplice, ma soffre di un difetto: se i dati


disponibili sono pochi, gli insiemi di validazione e di test possono contenere un
numero di campioni troppo basso per essere statisticamente rappresentativi dei dati
a disposizione. Il problema è facilmente riconoscibile: se diversi cicli di
rimescolamento casuale dei dati prima della suddivisione finiscono per produrre
misure molto diverse delle prestazioni del modello, allora il problema è questo. La
validazione K-fold e la validazione K-fold iterata sono due modi per risolvere
questo problema, come illustrato di seguito.
CONVALIDA K-FOLD
Con questo approccio, si dividono i dati in K partizioni di uguale dimensione. Per
ogni partizione i, si addestra un modello sulle restanti K - 1 partizioni e lo si
valuta sulla partizione i. Il punteggio finale è quindi la media dei K punteggi
ottenuti. Questo metodo è utile quando le prestazioni del modello mostrano
variazioni significative in base alla suddivisione tra addestramento e test. Come la
convalida di holdout, questo metodo non esime dall'utilizzo di un set di convalida
distinto per la calibrazione del modello.
Schematicamente, la convalida incrociata K-fold si presenta come nella figura 5.13.
Il listato 5.6 mostra una semplice implementazione.

Dati suddivisi in 3 partizioni

Punteggi
Piegatura 1 Convalida Formazione Formazione
o di
convalida
#1

Fig incrociata K-fold con


Piegatura 2 ura K=3
5.1
3
Co
Piegatura 3 nv
alid
a
138 CAPITOLO 5 Fondamenti dell'apprendimento
automatico

Formazione Convalida Formazione


Punteggio di convalida #2 P
u
n
t
Punteggio di convalida #3
Formazione Formazione Convalida e
g
g
i
o

f
i
n
a
l
e
:

m
e
d
i
o
Valutazione dei modelli di 139
apprendimento automatico

Listato 5.6 Validazione incrociata K-fold (si noti che le etichette sono omesse per semplicità)
k = 3
num_validation_samples = len(data) // k
np.random.shuffle(data)
validation_scores = [] Seleziona la
per fold in range(k): partizione dei
validation_data = dati[num_validation_samples * fold: dati di
num_validation_samples * (fold + convalida
1)] training_data = np.concatenate(
dati[:num_validation_samples * fold], Crea un nuovo file
dati[num_validation_samples * (fold + 1):]) istanza del modello (non
model = get_model() addestrato)
model.fit(training_data, ...)
validation_score = model.evaluate(validation_data, ...) Punteggio di convalida:
validation_scores.append(validation_score) media dei punteggi di
validation_score = np.average(validation_scores) convalida delle k pieghe.
model = get_model()
model.fit(dati, ...) Addestra il
test_score = model.evaluate(test_data, ...) modello finale su
tutti i dati non di
Utilizza il resto dei dati come dati di
test disponibili.
addestramento. Si noti che l'operatore +
rappresenta la concatenazione di
elenchi, non la somma.

CONVALIDA K-FOLD ITERATA CON RIMESCOLAMENTO


Questo è per le situazioni in cui si hanno a disposizione relativamente pochi dati e si
deve valutare il modello nel modo più preciso possibile. L'ho trovato estremamente utile
nelle competizioni Kaggle. Consiste nell'applicare più volte la validazione K-fold,
mescolando ogni volta i dati prima di dividerli in K modi. Il punteggio finale è la media
dei punteggi ottenuti a ogni esecuzione della validazione K-fold. Si noti che si finisce
per addestrare e valutare P * K modelli (dove P è il numero di iterazioni utilizzate), il
che può essere molto costoso.

5.2.2 Battere una linea di base di buon senso


Oltre ai diversi protocolli di valutazione disponibili, un'ultima cosa da sapere è l'uso di
linee di base di buon senso.
Addestrare un modello di deep learning è un po' come premere un pulsante che
lancia un razzo in un mondo parallelo. Non si può sentire o vedere. Non si può
osservare il processo di apprendimento multiplo: avviene in uno spazio con migliaia di
dimensioni e, anche se lo si proiettasse in 3D, non lo si potrebbe interpretare. L'unico
feedback che avete è la vostra metrica di convalida, come un misuratore di altitudine sul
vostro razzo invisibile.
È particolarmente importante essere in grado di capire se ci s i sta staccando da
terra. A che altitudine siete partiti? Il vostro modello sembra avere un'accuratezza del
15%: è un buon risultato? Prima di iniziare a lavorare con un set di dati, si dovrebbe
sempre scegliere una linea di base banale che si cercherà di battere. Se superate questa
soglia, saprete che state facendo qualcosa di buono: il vostro modello sta effettivamente
utilizzando le informazioni contenute nei dati di input per fare previsioni generalizzate e
potete continuare a lavorare. Questa linea di base potrebbe essere
140 CAPITOLO 5 Fondamenti dell'apprendimento
automatico
le prestazioni di un classificatore casuale o le prestazioni della più semplice tecnica
di apprendimento non automatico che si possa immaginare.
Ad esempio, nell'esempio di classificazione delle cifre MNIST, una semplice linea di
base sarebbe un'accuratezza di convalida superiore a 0,1 (classificatore casuale);
nell'esempio IMDB, sarebbe un'accuratezza di convalida superiore a 0,5. Nell'esempio
di Reuters, sarebbe circa 0,18-0,19, a causa dello squilibrio tra le classi. Se si ha un
problema di classificazione binaria in cui il 90% dei campioni appartiene alla classe A e
il 10% alla classe B, un classificatore che predice sempre A raggiunge già
un'accuratezza di convalida di 0,9 e sarà necessario fare meglio di così.
Avere una base di buon senso a cui fare riferimento è essenziale quando si inizia a
lavorare su un problema che nessuno ha mai risolto prima. Se non riuscite a battere una
soluzione banale, il vostro modello è inutile: forse state usando il modello sbagliato, o
forse il problema che state affrontando non p u ò nemmeno essere affrontato con
l'apprendimento automatico. È ora di tornare al tavolo da disegno.

5.2.3 Cose da tenere a mente per la valutazione dei modelli


Quando scegliete un protocollo di valutazione, tenete d'occhio i seguenti aspetti:
◾ Rappresentatività dei dati - È necessario che sia l'insieme di allenamento che
l'insieme di test siano rappresentativi dei dati in questione. Ad esempio, se si sta
cercando di classificare immagini di cifre e si parte da un array di campioni
ordinati per classe, prendendo il primo 80% dell'array come set di addestramento
e il restante 20% come set di test si otterrà che il set di addestramento contiene
solo le classi 0-7, mentre il set di test conterrà solo le classi 8-9. Questo sembra
un errore ridicolo, ma è sorprendentemente comune. Questo sembra un errore
ridicolo, ma è sorprendentemente comune. Per questo motivo, di solito si
consiglia di mescolare casualmente i dati prima di dividerli in set di
addestramento e set di test.
◾ La freccia del tempo - Se si sta cercando di prevedere il futuro in base al passato
(per esempio, il tempo di domani, i movimenti azionari e così via), non si
dovrebbero mescolare i dati prima di dividerli, perché così facendo si crea una
perdita temporale: il modello sarà effettivamente addestrato sui dati del futuro. In
queste situazioni, bisogna sempre assicurarsi che tutti i dati dell'insieme di test
siano posteriori a quelli dell'insieme di addestramento.
◾ Ridondanza nei dati: se alcuni punti dei dati appaiono due volte (cosa
abbastanza comune con i dati del mondo reale), il rimescolamento dei dati e
la loro suddivisione in un set di addestramento e in un set di convalida
produrrà una ridondanza tra i set di addestramento e di convalida. In effetti,
farete un test su una parte dei dati di addestramento, il che è la cosa peggiore
che possiate fare! Assicuratevi che l'insieme di formazione e l'insieme di
validazione siano separati.
Disporre di un metodo affidabile per valutare le prestazioni del modello è il modo
per monitorare la tensione al centro dell'apprendimento automatico, tra
ottimizzazione e generalizzazione, underfitting e overfitting.
Valutazione dei modelli di 141
apprendimento automatico

5.3 Migliorare l'adattamento del modello


Per ottenere l'adattamento perfetto, è necessario prima sovraadattare. Poiché non si
sa in anticipo dove si trova il confine, è necessario attraversarlo per trovarlo.
Pertanto, l'obiettivo iniziale, quando si inizia a lavorare su un problema, è quello di
ottenere un modello che mostri un certo potere di generalizzazione e che sia in
grado di eseguire l'overfit. Una volta ottenuto un modello di questo tipo, ci si
concentrerà sul perfezionamento della generalizzazione combattendo l'overfitting.
Ci sono tre problemi comuni che si incontrano in questa fase:
◾ L'allenamento non inizia: l a perdita di allenamento non diminuisce nel t e m p o .
◾ L'addestramento viene avviato bene, ma il modello non generalizza in modo
significativo: non si riesce a battere la linea di base di buon senso impostata.
◾ L'allenamento e la perdita di convalida diminuiscono con il tempo e si può
battere la propria linea di base, ma non sembra che si riesca ad andare in
overfit, il che indica che si è ancora in underfit.
Vediamo come affrontare questi problemi per raggiungere il primo grande traguardo
di un progetto di apprendimento automatico: ottenere un modello che abbia un certo
potere di generalizzazione (può battere una banale linea di base) e che sia in grado
di fare overfit.

5.3.1 Regolazione dei parametri chiave della discesa del gradiente


A volte l'allenamento non viene avviato o si blocca troppo presto. La perdita è bloccata.
Questo è sempre un problema che si può superare: ricordate che potete adattare un
modello a dati casuali. Anche se nulla del vostro problema ha senso, dovreste essere in
grado di addestrare qualcosa, anche solo memorizzando i dati di addestramento.
Quando ciò accade, è sempre un problema di configurazione del processo di
discesa del gradiente: la scelta dell'ottimizzatore, la distribuzione dei valori iniziali
dei pesi del modello, il tasso di apprendimento o la dimensione del batch. Tutti
questi parametri sono interdipendenti e quindi di solito è sufficiente regolare il tasso
di apprendimento e la dimensione del batch mantenendo costanti gli altri parametri.
Vediamo un esempio concreto: addestriamo il modello MNIST del capitolo 2
con un tasso di apprendimento inappropriatamente alto, pari a 1.

Listato 5.7 Addestramento di un modello MNIST con un tasso di apprendimento erroneamente alto

(train_images, train_labels), _ = mnist.load_data()


train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255

model = keras.Sequential([
layers.Dense(512, attivazione="relu"),
layers.Dense(10,
attivazione="softmax")
])
model.compile(optimizer=keras.optimizers.RMSprop(1.),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
model.fit(train_images, train_labels,
epochs=10,
Migliorare 139
l'adattamento del
modello
batch_size=128,
validation_split=0.2)

Il modello raggiunge rapidamente un'accuratezza di addestramento e di validazione


nell'ordine del 30%-40%, ma non riesce a superarla. Proviamo ad abbassare il tasso di
apprendimento a un valore più ragionevole di 1e-2.

Listato 5.8 Lo stesso modello con un tasso di apprendimento più appropriato

model = keras.Sequential([
layers.Dense(512, attivazione="relu"),
layers.Dense(10,
attivazione="softmax")
])
model.compile(optimizer=keras.optimizers.RMSprop(1e-2),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
model.fit(train_images, train_labels,
epochs=10,
batch_size=128,
validation_split=0.2)

Il modello è ora in grado di addestrarsi.


Se vi trovate in una situazione analoga, provate a
◾ Ridurre o aumentare il tasso di apprendimento. Un tasso di apprendimento
troppo alto può portare ad aggiornamenti che superano di molto l'adattamento
corretto, come nell'esempio precedente, mentre un tasso di apprendimento
troppo basso può rendere l'addestramento così lento da sembrare in stallo.
◾ Aumentare la dimensione del lotto. Un lotto con più campioni porterà a gradienti
più informativi e meno rumorosi (varianza inferiore).
Alla fine si riuscirà a trovare una configurazione che permetta di avviare la formazione.

5.3.2 Sfruttare migliori priori di architettura


Avete un modello che si adatta, ma per qualche motivo le vostre metriche di
validazione non migliorano affatto. Non sono migliori di quelle che otterrebbe un
classificatore casuale: il modello si addestra ma non generalizza. Cosa sta
succedendo?
Questa è forse la peggiore situazione di apprendimento automatico in cui ci si possa
trovare. Indica che c'è qualcosa di fondamentalmente sbagliato nel vostro approccio e
potrebbe non essere facile capire cosa. Ecco alcuni suggerimenti.
In primo luogo, è possibile che i dati di input utilizzati non contengano
informazioni sufficienti per prevedere gli obiettivi: il problema, così come è stato
formulato, non è risolvibile. È quello che è successo prima, quando abbiamo cercato
di adattare un modello MNIST in cui le etichette erano mischiate: il modello si
addestrava bene, ma l'accuratezza della validazione rimaneva bloccata al 10%,
perché era chiaramente impossibile generalizzare con un set di dati di questo tipo.
Può anche darsi che il tipo di modello che si sta utilizzando non sia adatto al
problema in questione. Per esempio, nel capitolo 10, si vedrà un esempio di previsione
di una serie temporale
140 CAPITOLO 5 Fondamenti dell'apprendimento
automatico
Un problema in cui un'architettura densamente connessa non è in grado di battere
una banale linea di base, mentre un'architettura ricorrente più appropriata riesce a
generalizzare bene. L'uso di un modello che faccia le giuste ipotesi sul problema è
essenziale per ottenere la generalizzazione: è necessario sfruttare i giusti priori
dell'architettura.
Nei capitoli che seguono verranno illustrate le migliori architetture da utilizzare
per una varietà di modalità di dati: immagini, testo, serie temporali e così via. In
generale, è sempre bene informarsi sulle migliori pratiche di architettura per il tipo
di attività che si sta affrontando: è probabile che non siate la prima persona a tentare
di farlo.

5.3.3 Aumentare la capacità del modello


Se riuscite a ottenere un modello che si adatta, in cui le metriche di validazione
scendono e che sembra raggiungere almeno un certo livello di potere di
generalizzazione, congratulazioni: ci siete quasi. Successivamente, dovete fare in
modo che il vostro modello inizi a fare overfitting.
Si consideri il seguente piccolo modello - una semplice regressione logistica -
addestrato sui pixel di MNIST.

Listato 5.9 Una semplice regressione logistica su MNIST

model = keras.Sequential([layers.Dense(10, activation="softmax")])


model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
history_small_model = model.fit(
train_images, train_labels,
epochs=20,
batch_size=128,
validation_split=0.2)

Si ottengono curve di perdita come quelle della figura 5.14:

importare matplotlib.pyplot come plt


val_loss = history_small_model.history["val_loss"]
epochs = range(1, 21)
plt.plot(epochs, val_loss, "b--",
label="Perdita di
convalida")
plt.title("Effetto della capacità insufficiente del modello sulla
perdita di convalida") plt.xlabel("Epoche")
plt.ylabel("Perdita")
plt.legend()

Le metriche di convalida sembrano bloccarsi o migliorare molto lentamente, invece di


raggiungere un picco e invertire la rotta. La perdita di convalida arriva a 0,26 e rimane
lì. Si può adattare, ma non si può chiaramente sovraadattare, anche dopo molte
iterazioni sui dati di addestramento. È probabile che nella vostra carriera vi imbattiate
spesso in curve simili.
Ricordate che il sovrallenamento dovrebbe essere sempre possibile. Come nel
Migliorare 141
l'adattamento
caso in cui la perdita di allenamento non del
si riduce, si tratta di un problema che può
essere sempre risolto. Se modello
142 CAPITOLO 5 Fondamenti dell'apprendimento
automatico

Figura 5.14 Effetto dell'insufficiente capacità del modello sulle curve di perdita

Se non riuscite a fare overfit, è probabile che si tratti di un problema di potenza


rappresentativa del vostro modello: avrete bisogno di un modello più grande, con una
maggiore capacità, c i o è i n grado di immagazzinare più informazioni. È possibile
aumentare la potenza rappresentativa aggiungendo più strati, utilizzando strati più
grandi (strati con più parametri) o utilizzando tipi di strati più appropriati per il
problema in questione (priori di architettura migliori).
Proviamo ad addestrare un modello più grande, con due livelli intermedi di 96
unità ciascuno:

model = keras.Sequential([ layers.Dense(96,


attivazione="relu"), layers.Dense(96,
attivazione="relu"), layers.Dense(10,
attivazione="softmax"),
])
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
history_large_model = model.fit(
train_images, train_labels,
epochs=20,
batch_size=128,
validation_split=0.2)

La curva di validazione ora appare esattamente come dovrebbe: il modello si adatta velocemente
e inizia a fare overfitting dopo 8 epoch (vedi figura 5.15).
Migliorare 143
l'adattamento del
modello

Figura 5.15 Perdita di convalida per un modello con capacità adeguata

5.4 Migliorare la generalizzazione


Una volta che il modello ha dimostrato di avere un certo potere di generalizzazione
e di essere in grado di fare overfit, è il momento di concentrarsi sulla
massimizzazione della generalizzazione.

5.4.1 Curatela del set di dati


Si è già appreso che la generalizzazione nel deep learning ha origine dalla struttura
latente dei dati. Se i vostri dati consentono di interpolare in modo fluido tra i
campioni, sarete in grado di addestrare un modello di deep learning che generalizza.
Se il vostro problema è eccessivamente rumoroso o fondamentalmente discreto,
come ad esempio l'ordinamento di liste, il deep learning non vi aiuterà. Il deep
learning è un adattamento di curve, non una magia.
Per questo motivo, è essenziale assicurarsi di lavorare con un set di dati
appropriato. Spendere maggiori sforzi e denaro per la raccolta dei dati produce quasi
sempre un ritorno sull'investimento molto maggiore rispetto a quello speso per lo
sviluppo di un modello migliore.
◾ Assicuratevi di avere abbastanza dati. Ricordate che è necessario un
campionamento denso dello spazio input-cross-output. Un maggior numero di
dati produrrà un modello migliore. A volte, problemi che all'inizio sembrano
impossibili diventano risolvibili con un set di dati più ampio.
◾ Riducete al minimo gli errori di etichettatura: visualizzate gli input per verificare
la presenza di anomalie e correggete le etichette.
◾ Pulite i dati e gestite i valori mancanti (ne parleremo nel prossimo capitolo).
◾ Se avete molte funzioni e non siete sicuri di quali siano effettivamente utili, fate
una selezione delle funzioni.
Un modo particolarmente importante per migliorare il potenziale di generalizzazione
dei dati è l'ingegneria delle caratteristiche. Per la maggior parte dei problemi di
144 CAPITOLO 5 Fondamenti dell'apprendimento
apprendimento automatico, l'ingegneria delle caratteristiche è un ingrediente chiave per
automatico
il successo. Vediamo di seguito.
Migliorare la 143
generalizzazione
5.4.2 Ingegneria delle caratteristiche
L'ingegneria delle caratteristiche è il processo di utilizzo delle proprie conoscenze sui dati e
sull'algoritmo di apprendimento automatico (in questo caso, una rete neurale) per far
funzionare meglio l'algoritmo applicando trasformazioni codificate (non apprese) ai
dati prima che vengano inseriti nel modello. In molti casi, non è ragionevole
aspettarsi che un modello di apprendimento automatico sia in grado di imparare da
dati completamente arbitrari. I dati devono essere presentati al modello in modo da
facilitargli il lavoro.
Vediamo un esempio intuitivo. Supponiamo di voler sviluppare un modello che
prenda in input l'immagine di un orologio e fornisca in output l'ora del giorno (vedi
figura 5.16).

Dati
grezzi:
griglia di
pixel

Meglio {x1: 0.7, {x1: 0.0,


caratteristiche: y1: 0.7} y2: 1.0}
lancette {x2: 0.5, {x2: -0.38,
dell'orologio
coordinate y2: 0.0} y2: 0.32}

Ancora meglio theta1: 45 theta1: 90


caratteris theta2: 0 theta2: 140
tiche: Figura 5.16 Ingegneria delle
angoli di funzioni per la lettura dell'ora
lancette su un orologio
dell'orologio

Se si sceglie di utilizzare i pixel grezzi dell'immagine come dati di input, si ha per le


mani un difficile problema di apprendimento automatico. Per risolverlo avrete
bisogno di una rete neurale convoluzionale e dovrete spendere un bel po' di risorse
computazionali per addestrare la rete.
Ma se si comprende già il problema ad alto livello (si capisce come gli esseri
umani leggono l'ora sul quadrante di un orologio), si possono trovare caratteristiche
di input molto migliori per un algoritmo di apprendimento automatico: ad esempio,
è facile scrivere uno script Python di cinque righe per seguire i pixel neri delle lancette
dell'orologio e produrre le coordinate (x, y) della punta di ciascuna lancetta. Poi un
semplice algoritmo di apprendimento automatico può imparare ad associare queste
coordinate all'ora del giorno appropriata.
Si può andare oltre: cambiare le coordinate ed esprimere le coordinate (x, y)
come coordinate polari rispetto al centro dell'immagine. Il vostro input diventerà
l'angolo theta di ogni lancetta dell'orologio. A questo punto, le caratteristiche
rendono il problema così semplice che non è necessario l'apprendimento
automatico; una semplice operazione di arrotondamento e la consultazione del
dizionario sono sufficienti per recuperare l'ora approssimativa del giorno.
144 CAPITOLO 5 Fondamenti dell'apprendimento
Questa è l'essenzaautomatico
dell'ingegneria delle caratteristiche: rendere più facile un
problema esprimendolo in modo più semplice. Rendere il manifold latente più
fluido, più semplice, meglio organizzato. Per farlo, di solito è necessario
comprendere a fondo il problema.
Migliorare la 145
generalizzazione
Prima del deep learning, l'ingegneria delle caratteristiche era la parte più
importante del flusso di lavoro dell'apprendimento automatico, perché i classici
algoritmi poco profondi non avevano spazi di ipotesi sufficientemente ricchi per
apprendere da soli caratteristiche utili. Il modo in cui si inviavano i dati all'algoritmo era
assolutamente critico per il suo successo. Per esempio, prima che le reti neurali
convoluzionali avessero successo nel problema della classificazione delle cifre
MNIST, le soluzioni erano tipicamente basate su caratteristiche codificate come il
numero di cicli in un'immagine di cifre, l'altezza di ogni cifra in un'immagine, un
istogramma di valori di pixel e così via.
Fortunatamente, il moderno deep learning elimina la necessità di ingegnerizzare
la maggior parte delle caratteristiche, perché le reti neurali sono in grado di estrarre
automaticamente le caratteristiche utili dai dati grezzi. Questo significa che non ci si
deve preoccupare dell'ingegneria delle caratteristiche finché si utilizzano reti neurali
profonde? No, per due motivi:
◾ Le buone caratteristiche consentono comunque di risolvere i problemi in modo
più elegante, utilizzando meno risorse. Ad esempio, sarebbe ridicolo risolvere il
problema della lettura del quadrante di un orologio utilizzando una rete neurale
convoluzionale.
◾ Buone caratteristiche consentono di risolvere un problema con molti meno
dati. La capacità dei modelli di deep learning di apprendere da soli le
caratteristiche si basa sulla disponibilità di molti dati di addestramento; se si
dispone di pochi campioni, il valore informativo delle caratteristiche diventa critico.

5.4.3 Utilizzo dell'arresto anticipato


Nell'apprendimento profondo, utilizziamo sempre modelli che sono enormemente
sovraparametrizzati: hanno molti più gradi di libertà del minimo necessario per adattarsi
al manufatto latente dei dati. Questa sovraparametrizzazione non è un problema, perché
non si può mai adattare completamente un modello di deep learning. Un tale
adattamento non sarebbe affatto generalizzabile. Si interromperà sempre l'addestramento
molto prima di aver raggiunto la perdita minima possibile per l'addestramento.
Trovare il punto esatto durante l'addestramento in cui si è raggiunto
l'adattamento più generalizzabile, il confine esatto tra una curva di sottoadattamento
e una curva di sovraadattamento, è una delle cose più efficaci che si possono fare
per migliorare la generalizzazione.
Negli esempi del capitolo precedente, abbiamo iniziato ad addestrare i nostri modelli
per un periodo più lungo del necessario per capire il numero di epoche che produceva le
migliori metriche di validazione, e poi abbiamo ri-addestrato un nuovo modello
esattamente per quel numero di epoche. Si tratta di una procedura abbastanza standard,
ma richiede un lavoro ridondante, che a volte può essere costoso. Naturalmente, si
potrebbe semplicemente salvare il modello alla fine di ogni epoca e, una volta trovata
l'epoca migliore, riutilizzare il modello salvato più vicino. In Keras, è tipico fare questo
con un callback EarlyStopping, che interrompe l'addestramento non appena le
metriche di validazione smettono di migliorare, ricordando il miglior stato noto del
modello. Impareremo a usare i callback nel capitolo 7.
146 CAPITOLO 5 Fondamenti dell'apprendimento
automatico
5.4.4 Regolarizzazione del modello
Le tecniche di regolarizzazione sono un insieme di buone pratiche che ostacolano
attivamente la capacità del modello di adattarsi perfettamente ai dati di addestramento,
con l'obiettivo di far funzionare m e g l i o il modello durante la convalida. Questa
operazione viene chiamata "regolarizzazione" del modello, perché tende a rendere il
modello più semplice, più "regolare", la sua curva più dolce, più "generica"; in questo
modo è meno specifico per l'insieme di dati di addestramento e meglio in grado di
generalizzare approssimando più strettamente il manifold latente dei dati.
Tenete presente che la regolarizzazione di un modello è un processo che deve
sempre essere guidato da una procedura di valutazione accurata. La
generalizzazione si ottiene solo se è possibile misurarla.
Passiamo in rassegna alcune delle tecniche di regolarizzazione più comuni e
applichiamole nella pratica per migliorare il modello di classificazione dei film del
capitolo 4.
RIDUZIONE DELLE DIMENSIONI DELLA RETE
Si è già appreso che un modello troppo piccolo non si adatterà in modo eccessivo. Il
modo più semplice per mitigare l'overfitting è ridurre le dimensioni del modello (il
numero di parametri apprendibili nel modello, determinato dal numero di strati e dal
numero di unità per strato). Se il modello ha risorse di memorizzazione limitate, non
potrà semplicemente memorizzare i dati di addestramento; quindi, per minimizzare la
sua perdita, dovrà ricorrere all'apprendimento di rappresentazioni compresse che
abbiano un potere predittivo rispetto agli obiettivi, proprio il tipo di rappresentazioni
c h e ci interessano. Allo stesso tempo, bisogna tenere presente che i modelli devono
avere parametri sufficienti per non essere sottoadattati: il modello non d e v e essere
affamato di risorse di memorizzazione. È necessario trovare un compromesso tra una
capacità eccessiva e una capacità insufficiente.
Purtroppo, non esiste una formula magica per determinare il numero giusto di
strati o la dimensione giusta per ogni strato. È necessario valutare una serie di
architetture diverse (sul set di validazione, non sul set di test, ovviamente) per
trovare la dimensione del modello corretta per i propri dati. Il flusso di lavoro generale
per trovare la dimensione appropriata del modello consiste nell'iniziare con un numero
relativamente basso di strati e di parametri e nell'aumentare la dimensione degli strati
o nell'aggiungere nuovi strati fino a quando non si notano rendimenti decrescenti per
quanto riguarda la perdita di convalida.
Proviamo questo modello di classificazione per le recensioni dei film. Il seguente
elenco mostra il nostro modello originale.

Listato 5.10 Modello originale

da tensorflow.keras.datasets import imdb


(train_data, train_labels), _ = imdb.load_data(num_words=10000)

def vectorize_sequences(sequences, dimension=10000):


results = np.zeros((len(sequences), dimension))
for i, sequence in enumerate(sequences):
risultati[i, sequenza] = 1.
restituire i risultati
Migliorare la 147
generalizzazione
train_data = vettorializza_sequenze(train_data)
148 CAPITOLO 5 Fondamenti dell'apprendimento
automatico
model = keras.Sequential([
layers.Dense(16, attivazione="relu"),
layers.Dense(16, attivazione="relu"),
layers.Dense(1,
attivazione="sigmoid")
])
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_original = model.fit(train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)

Ora proviamo a sostituirlo con questo modello più piccolo.

Listato 5.11 Versione del modello con capacità inferiore

model = keras.Sequential([
layers.Dense(4, attivazione="relu"),
layers.Dense(4, attivazione="relu"),
layers.Dense(1,
attivazione="sigmoid")
])
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_smaller_model = model.fit(
train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)

La Figura 5.17 mostra un confronto delle perdite di convalida del modello originale e
del modello più piccolo.

Figura 5.17 Modello originale vs. modello più piccolo sulla classificazione delle recensioni di IMDB
Migliorare la 149
generalizzazione
Come si può notare, il modello più piccolo inizia l'overfitting più tardi rispetto al
modello di riferimento (dopo sei epoche anziché quattro) e le sue prestazioni si
degradano più lentamente una volta iniziato l'overfitting.
Ora, aggiungiamo al nostro benchmark un modello che ha una capacità molto
maggiore, molto più di quanto il problema giustifichi. Sebbene sia normale lavorare
con modelli che sono significativamente sovraparametrizzati per ciò che stanno
cercando di apprendere, può sicuramente esistere una capacità di memorizzazione
eccessiva. Saprete che il vostro modello è troppo grande se inizia subito a fare
overfitting e se la sua curva di perdita di convalida appare irregolare con un'alta
varianza (sebbene le metriche di convalida irregolari possano anche essere un
sintomo dell'utilizzo di un processo di convalida inaffidabile, come una divisione di
convalida troppo piccola).

Listato 5.12 Versione del modello con capacità maggiore

model = keras.Sequential([
layers.Dense(512,
attivazione="relu"),
layers.Dense(512,
attivazione="relu"), layers.Dense(1,
attivazione="sigmoid")
])
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_larger_model = model.fit(
train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)

La Figura 5.18 mostra come si comporta il modello più grande rispetto al modello di
riferimento.

Figura 5.18 Modello originale vs. modello molto più grande sulla
classificazione delle recensioni di IMDB
150 CAPITOLO 5 Fondamenti dell'apprendimento
automatico
Il modello più grande inizia a fare overfitting quasi subito, dopo una sola epoch, e lo fa
in modo molto più grave. Anche la sua perdita di convalida è più rumorosa. La perdita
di addestramento si avvicina a zero molto rapidamente. Maggiore è la capacità del
modello, più rapidamente riesce a modellare i dati di formazione (con conseguente
bassa perdita di formazione), ma più è suscettibile all'overfiting (con conseguente
grande differenza tra la perdita di formazione e quella di validazione).
AGGIUNTA DELLA REGOLARIZZAZIONE DEL PESO
Forse conoscete il principio del rasoio di Occam: date due spiegazioni per qualcosa, la
spiegazione più probabile è quella più semplice, quella che fa meno ipotesi. Questa idea
si applica anche ai modelli appresi dalle reti neurali: dati alcuni dati di addestramento e
un'architettura di rete, più serie di valori di peso (più modelli) potrebbero spiegare i dati.
I modelli più semplici hanno meno probabilità di adattarsi eccessivamente rispetto a
quelli complessi.
Un modello semplice in questo contesto è un modello in cui la distribuzione dei valori
dei parametri ha un'entropia minore (o un modello con meno parametri, come si è
visto nella sezione precedente). Pertanto, un modo comune per mitigare l'overfitting
è quello di porre dei vincoli alla complessità di un modello, obbligando i suoi pesi
ad assumere solo valori piccoli, il che rende la distribuzione dei valori dei pesi più
regolare. Si tratta della cosiddetta regolarizzazione dei pesi, che si ottiene aggiungendo
alla funzione di perdita del modello un costo associato alla presenza di pesi grandi.
Questo costo è di due tipi:
◾ Regolarizzazione L1: il costo aggiunto è proporzionale al valore assoluto dei
coefficienti di peso (la norma L1 dei pesi).
◾ Regolarizzazione L2: il costo aggiunto è proporzionale al quadrato del valore dei
coefficienti dei pesi (la norma L2 dei pesi). La regolarizzazione L2 è chiamata anche
decadimento dei pesi nel contesto delle reti neurali. Non l a s c i a t e v i ingannare dal
nome diverso: il decadimento dei pesi è matematicamente identico alla
regolarizzazione L2.
In Keras, la regolarizzazione del peso viene aggiunta passando istanze di
regolarizzazione del peso ai livelli come argomenti di parole chiave. Aggiungiamo la
regolarizzazione del peso L2 al nostro modello iniziale di classificazione delle
recensioni di film.

Listato 5.13 Aggiunta al modello della regolarizzazione del peso L2

da tensorflow.keras import regularizers


model = keras.Sequential([
strati.Denso(16,
kernel_regularizer=regularizers.l2(0.002),
activation="relu"),
strati.Denso(16,
kernel_regularizer=regularizers.l2(0.002),
activation="relu"),
layers.Dense(1, attivazione="sigmoide")
])
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
Migliorare la 151
generalizzazione
history_l2_reg = model.fit(
train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)

Nell'elenco precedente, l2(0,002) significa che ogni coefficiente nella matrice dei pesi
del livello aggiungerà 0,002 * valore_del_coefficiente_di_peso ** 2 alla
perdita totale del modello. Poiché questa penalità viene aggiunta solo al momento
dell'addestramento, la perdita di questo modello sarà molto più alta al momento
dell'addestramento che al momento del test.
La Figura 5.19 mostra l'impatto della penalità di regolarizzazione L2. Come si
può notare, il modello con la regolarizzazione L2 è diventato molto più resistente
all'overfitting rispetto al modello di riferimento, anche se entrambi i modelli hanno
lo stesso numero di parametri.

Figura 5.19 Effetto della regolarizzazione del peso L2 sulla perdita di convalida

In alternativa alla regolarizzazione L2, è possibile utilizzare uno dei seguenti regolarizzatori
di peso di Keras.

Listato 5.14 Regolarizzatori di peso diversi disponibili in Keras

da tensorflow.keras import regularizers


regularizers.l1(0.001)
Regolarizzazione L1
regularizers.l1_l2(l1=0.001, l2=0.001)
Regolarizzazione
L1 e L2 simultanea

Si noti che la regolarizzazione dei pesi è più tipicamente utilizzata per i modelli di
deep learning più piccoli. I modelli di deep learning di grandi dimensioni tendono a
essere talmente iperparametrizzati che l'imposizione di vincoli sui valori dei pesi non
ha un grande impatto sulla capacità del modello e sulla generalizzazione. In questi
casi, si preferisce una tecnica di regolarizzazione diversa: il dropout.
152 CAPITOLO 5 Fondamenti dell'apprendimento
automatico
AGGIUNTA DI UN ABBANDONO
Il dropout è una delle tecniche di regolarizzazione più efficaci e più utilizzate per le
reti neurali; è stato sviluppato da Geoff Hinton e dai suoi studenti dell'Università di
Toronto. Il dropout, applicato a uno strato, consiste nell'eliminare in modo casuale
(azzerando) un certo numero di caratteristiche di uscita dello strato durante
l'addestramento. Supponiamo che un dato strato restituisca normalmente un vettore
[0.2, 0.5, 1.3, 0.8, 1.1] per un dato campione di input durante
l'addestramento. Dopo l'applicazione del dropout, questo vettore avrà alcune voci
nulle distribuite a caso: per esempio, [0, 0,5, 1,3, 0, 1,1]. Il tasso di abbandono è
la frazione delle caratteristiche che vengono azzerate; di solito è impostato tra 0,2 e
0,5. Al momento del test, nessuna unità viene eliminata; i valori di uscita dello
strato vengono invece ridotti di un fattore pari al tasso di abbandono, per
compensare il fatto che sono attive più unità rispetto al momento
dell'addestramento.
Si consideri una matrice NumPy contenente l'output di uno strato, layer_output,
di forma (batch_size, features). Al momento dell'addestramento, azzeriamo a caso
una frazione dei valori della matrice:

layer_output *= np.random.randint(0, high=2, size=layer_output.shape)

Al momento dell'addestramento, elimina il 50% delle unità in uscita.

Al momento del test, riduciamo l'output in base al tasso di abbandono. In questo


caso, scaliamo di 0,5 (perché in precedenza abbiamo abbandonato metà delle unità):

layer_output *= 0,5 Al momento del test

Si noti che questo processo può essere implementato eseguendo entrambe le


operazioni al momento dell'addestramento e lasciando l'output invariato al momento
del test, che è spesso il modo in cui viene implementato.
in pratica (cfr. figura 5.20): Al momento dell'allenamento
layer_output *= np.random.randint(0, high=2, size=layer_output.shape)
layer_output /= 0,5
Si noti che in questo caso
stiamo scalando verso l'alto
anziché verso il basso.

0.3 0.2 1.5 0.0 0.0 0.2 1.5 0.0


50%
0.6 0.1 0.0 0.3 abbandono 0.6 0.1 0.0 0.3 Figura 5.20 Caduta applicata a
* 2una matrice di attivazione al
0.2 1.9 0.3 1.2 0.0 1.9 0.3 0.0 momento dell'addestramento,
con un ridimensionamento
0.7 0.5 1.0 0.0 0.7 0.0 0.0 0.0
che avviene durante
l'addestramento. Al momento
del test, la matrice di
attivazione è invariata.

Questa tecnica può sembrare strana e arbitraria. Perché dovrebbe aiutare a ridurre
l'overfiting? Hinton dice di essersi ispirato, tra l'altro, a un meccanismo di
Migliorare la 153
generalizzazione

prevenzione delle frodi utilizzato dalle banche. Per dirla con le sue parole: "Sono
andato nella mia banca. I cassieri continuavano a cambiare e ho chiesto a uno di
loro perché. Mi ha risposto che non lo sapeva, ma che venivano spostati spesso.
154 CAPITOLO 5 Fondamenti dell'apprendimento
automatico
Ho pensato che doveva essere perché per riuscire a frodare la banca era necessaria la
collaborazione tra i dipendenti. Questo mi ha fatto capire che la rimozione casuale di un
sottoinsieme diverso di neuroni per ogni esempio avrebbe impedito le cospirazioni e
quindi ridotto l'over fitting". L'idea di base è che l'introduzione di rumore nei valori di
uscita di uno strato può interrompere modelli casuali che non sono significativi (ciò che
Hinton definisce cospirazioni), che il modello inizierà a memorizzare in assenza di
rumore.
In Keras, è possibile introdurre il dropout in un modello tramite il livello Dropout,
che viene applicato all'output del livello immediatamente precedente. Aggiungiamo due
livelli di dropout al modello IMDB per vedere come riducono l'overfitting.

Listato 5.15 Aggiunta del dropout al modello IMDB

model = keras.Sequential([
layers.Dense(16,
activation="relu"),
layers.Dropout(0.5),
layers.Dense(16,
activation="relu"),
layers.Dropout(0.5),
layers.Dense(1, attivazione="sigmoide")
])
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_dropout = model.fit(
train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)

La Figura 5.21 mostra un grafico dei risultati. Si tratta di un chiaro miglioramento


rispetto al modello di riferimento; sembra inoltre che la regolarizzazione L2 funzioni
molto meglio, dal momento che la perdita di validazione raggiunta è migliorata.

Figura 5.21 Effetto del dropout sulla perdita di convalida


Migliorare la 155
generalizzazione
Riassumendo, questi sono i modi più comuni per massimizzare la generalizzazione e
prevenire l'overfitting nelle reti neurali:
◾ Ottenere più dati di formazione o dati di formazione migliori.
◾ Sviluppare funzioni migliori.
◾ Ridurre la capacità del modello.
◾ Aggiungere la regolarizzazione del peso (per modelli più piccoli).
◾ Aggiungere l'abbandono.

Sintesi
◾ Lo scopo di un modello di apprendimento automatico è quello di generalizzare,
ossia di ottenere risultati accurati su input mai visti prima. È più difficile di
quanto sembri.
◾ Una rete neurale profonda ottiene la generalizzazione apprendendo un
modello parametrico in grado di interpolare con successo tra i campioni di
addestramento: si può dire che il modello abbia appreso il "manifold latente" dei
dati di addestramento. Per questo motivo, i modelli di apprendimento
profondo possono dare un senso solo a input molto simili a quelli che hanno
visto durante l'addestramento.
◾ Il problema fondamentale dell'apprendimento automatico è la tensione tra
ottimizzazione e generalizzazione: per ottenere la generalizzazione, è necessario
innanzitutto ottenere un buon adattamento ai dati di addestramento, ma il
miglioramento dell'adattamento del modello ai dati di addestramento inizierà
inevitabilmente a danneggiare la generalizzazione dopo un po'. Ogni singola best
practice di deep learning si occupa di gestire questa tensione.
◾ La capacità di generalizzazione dei modelli di deep learning deriva dal fatto
che essi riescono a imparare ad approssimare il manifold latente dei loro dati
e possono quindi dare un senso ai nuovi input tramite interpolazione.
◾ È essenziale essere in grado di valutare accuratamente il potere di
generalizzazione del modello mentre lo si sta sviluppando. Avete a
disposizione una serie di metodi di valutazione, dalla semplice convalida in
attesa alla convalida incrociata K-fold e alla convalida incrociata K-fold iterata
con rimescolamento. Ricordate di tenere sempre un set di test completamente
separato per la valutazione finale del modello, poiché potrebbero verificarsi
fughe di informazioni dai dati di convalida al modello.
◾ Quando si inizia a lavorare su un modello, l'obiettivo è innanzitutto quello di
ottenere un modello che abbia un certo potere di generalizzazione e che sia in
grado di adattarsi in modo eccessivo. Le migliori pratiche per raggiungere
questo obiettivo includono la regolazione del tasso di apprendimento e della
dimensione del batch, l'utilizzo di priori architettonici migliori, l'aumento
della capacità del modello o semplicemente un addestramento più lungo.
◾ Quando il modello inizia ad adattarsi eccessivamente, l'obiettivo passa al
miglioramento della generalizzazione attraverso la regolarizzazione del modello. È
possibile ridurre la capacità del modello, aggiungere la regolarizzazione del dropout
o del peso e utilizzare l'arresto anticipato. Naturalmente, un set di dati più
grande o migliore è sempre il modo migliore per aiutare un modello a
156 CAPITOLO 5 Fondamenti dell'apprendimento
generalizzare. automatico
Il flusso di lavoro
universale
dell'apprendimento
automatico

Questo capitolo tratta


◾ Fasi di inquadramento di un problema di
apprendimento automatico
◾ Fasi di sviluppo di un modello di lavoro
◾ Passi per la distribuzione del modello in
produzione e la sua manutenzione

Gli esempi precedenti presuppongono che si disponga già di un set di dati etichettati
da cui partire e che si possa iniziare immediatamente ad addestrare un modello. Nel
mondo reale, spesso non è così. Non si parte da un set di dati, ma da un problema.
Immaginate di avviare il vostro negozio di consulenza sull'apprendimento
automatico. Vi costituite, mettete su un sito web di lusso, informate la vostra rete
di contatti. I progetti iniziano ad arrivare:
◾ Un motore di ricerca fotografico personalizzato per un social network di
condivisione di immagini: digitate "matrimonio" e recuperate tutte le foto
che avete scattato ai matrimoni, senza bisogno di tag manuali.
◾ Segnalazione di spam e contenuti testuali offensivi tra i post di un'app di
chat in erba.
◾ Costruire un sistema di raccomandazione musicale per gli utenti di una radio
online.
◾ Rilevare le frodi con le carte di credito per un sito di e-commerce.

153
154 CAPITOLO 6 Il flusso di lavoro universale
dell'apprendimento automatico
◾ Previsione del tasso di clic degli annunci display per decidere quale annuncio
proporre a un determinato utente in un determinato momento.
◾ Segnalazione di biscotti anomali sul nastro trasportatore di una linea di produzione di biscotti.
◾ Utilizzo di immagini satellitari per prevedere la posizione di siti archeologici ancora
sconosciuti.

Nota sull'etica
A volte vi vengono proposti progetti eticamente discutibili, come "costruire
un'intelligenza artificiale che valuti l'affidabilità di una persona da una foto del suo
volto". Innanzitutto, la validità del progetto è in dubbio: non è chiaro perché
l'affidabilità dovrebbe essere riflessa sul volto di una persona. In secondo luogo,
un compito del genere apre la porta a ogni tipo di problema etico. Raccogliere un
set di dati per questo compito equivale a registrare i pregiudizi delle persone che
etichettano le immagini. I modelli che verrebbero addestrati su questi dati non
farebbero altro che codificare questi stessi pregiudizi in un algoritmo black-box che
darebbe loro una sottile patina di legittimità. In una società largamente analfabeta
come la nostra, "l'algoritmo dell'IA ha detto che questa persona non è affidabile"
sembra stranamente avere più peso e obiettività di "John Smith ha detto che
questa persona non è affidabile", nonostante il primo sia un'approssimazione
appresa del secondo. Il vostro modello riciclerebbe e renderebbe operativi su scala
gli aspetti peggiori del giudizio umano, con effetti negativi sulla vita delle persone
reali.
La tecnologia non è mai neutrale. Se il vostro lavoro ha un impatto sul mondo,
questo impatto ha una direzione morale: le scelte tecniche sono anche scelte
etiche. Siate sempre consapevoli dei valori che volete che il vostro lavoro
sostenga.

Sarebbe molto comodo se si potesse importare il dataset corretto da keras.data- sets


e iniziare ad adattare alcuni modelli di deep learning. Purtroppo, nel mondo reale si
deve partire da zero.
In questo capitolo, imparerete a conoscere un modello universale, passo dopo
passo, che potrete utilizzare per affrontare e risolvere qualsiasi problema di
apprendimento automatico, come quelli dell'elenco precedente. Questo modello
riunirà e consoliderà tutto ciò che avete appreso nei capitoli 4 e 5 e vi fornirà un
contesto più ampio, che dovrebbe essere alla base di ciò che imparerete nei capitoli
successivi.
Il flusso di lavoro universale dell'apprendimento automatico è ampiamente strutturato in tre
parti:
1 Definire il compito - Comprendere il dominio del problema e la logica
aziendale alla base della richiesta del cliente. Raccogliere un set di dati, capire
cosa rappresentano i dati e scegliere come misurare il successo del compito.
2 Sviluppare un modello - Preparare i dati in modo che possano essere elaborati
da un modello di apprendimento automatico, selezionare un protocollo di
valutazione del modello e una semplice linea di base da battere, addestrare un
primo modello che abbia potere di generalizzazione e che sia in grado di fare
overfit, quindi regolarizzare e mettere a punto il modello fino a ottenere le
migliori prestazioni di generalizzazione possibili.
Definire il 155
3 compitoil lavoro agli stakeholder, inviare il modello a un
Distribuire il modello -Presentare
server web, a un'applicazione mobile, a una pagina web o a un dispositivo
incorporato, monitorare il funzionamento del modello.
156 CAPITOLO 6 Il flusso di lavoro universale
dell'apprendimento automatico
e iniziare a raccogliere i dati necessari per costruire il modello di prossima
generazione.
Immergiamoci in questa storia.

6.1 Definire il compito


Non si può fare un buon lavoro senza una profonda comprensione del contesto in
cui si opera. Perché il vostro cliente sta cercando di risolvere questo particolare
problema? Che valore trarranno dalla soluzione: come verrà utilizzato il vostro
modello e come si inserirà nei processi aziendali del cliente? Che tipo di dati sono
disponibili o possono essere raccolti? Che tipo di attività di apprendimento
automatico può essere mappata al problema aziendale?

6.1.1 Inquadrare il problema


L'inquadramento di un problema di apprendimento automatico comporta
solitamente molte discussioni dettagliate con le parti interessate. Ecco le domande
che dovrebbero essere in cima ai vostri pensieri:
◾ Quali saranno i dati di input? Cosa si sta cercando di prevedere? Si può imparare
a prevedere qualcosa solo se si d i s p o n e di dati di addestramento: per
e s e m p i o , s i può imparare a classificare il sentiment delle recensioni di film
solo se si d i s p o n e d i recensioni e annotazioni sul sentiment. Pertanto, la
disponibilità di dati è di solito il fattore limitante in questa fase. In molti casi, si
dovrà ricorrere alla raccolta e all'annotazione di nuovi set di dati (d i cui
parleremo nella prossima sezione).
◾ Che tipo di compito di apprendimento automatico state affrontando? È una
classificazione binaria? Classificazione multiclasse? Regressione scalare?
Regressione vettoriale? Classificazione multiclasse e multietichetta? Segmentazione
di immagini? Classificazione? Qualcos'altro, come il clustering, la generazione o
l'apprendimento per rinforzo? In alcuni casi, è possibile che l'apprendimento
automatico non sia nemmeno il modo migliore per dare un senso ai dati e che
si debba usare qualcos'altro, come la semplice analisi statistica della vecchia
scuola.
– Il progetto del motore di ricerca fotografico è un compito di classificazione multiclasse e
multilabel.
– Il progetto di rilevamento dello spam è un compito di classificazione binaria.
Se si imposta il "contenuto offensivo" come classe separata, si tratta di un
compito di classificazione a tre vie.
– Il motore di raccomandazione musicale risulta essere gestito meglio non
tramite deep learning, ma tramite fattorizzazione matriciale (filtraggio
collaborativo).
– Il progetto di rilevamento delle frodi con carta di credito è un compito di
classificazione binaria.
– Il progetto di previsione del click-through-rate è un compito di regressione scalare.
– Il rilevamento dei biscotti anomali è un'attività di classificazione binaria, ma
richiede anche un modello di rilevamento degli oggetti come prima fase per
ritagliare correttamente i biscotti nelle immagini grezze. Si noti che l'insieme
Definire il 157
compito
delle tecniche di apprendimento automatico note come "rilevamento di
anomalie" non sarebbe adatto a questo contesto!
– Il progetto di ricerca di nuovi siti archeologici da immagini satellitari è un
compito di classificazione della somiglianza delle immagini: è necessario
recuperare le nuove immagini che assomigliano di più ai siti archeologici
conosciuti.
158 CAPITOLO 6 Il flusso di lavoro universale
dell'apprendimento automatico
◾ Che aspetto hanno le soluzioni esistenti? Forse il vostro cliente dispone già di
un algoritmo artigianale che gestisce il filtraggio dello spam o
l'individuazione delle frodi sulle carte di credito, con molte istruzioni if
annidate. Forse un essere umano è attualmente incaricato di gestire
manualmente il processo in esame, monitorando il nastro di controllo
dell'impianto di produzione dei cookie e rimuovendo manualmente i cookie
difettosi, oppure creando playlist di canzoni consigliate da inviare agli utenti a
cui è piaciuto un determinato artista. Dovete assicurarvi di capire quali sono i
sistemi già esistenti e come funzionano.
◾ Ci sono vincoli particolari che dovrete affrontare? Ad esempio, potreste
scoprire che l'applicazione per la quale state costruendo un sistema di
rilevamento dello spam è rigorosamente crittografata end-to-end, quindi il
modello di rilevamento dello spam dovrà vivere sul telefono dell'utente finale
e dovrà essere addestrato su un set di dati esterno. Forse il modello di
filtraggio dei cookie ha vincoli di latenza tali da dover essere eseguito su un
dispositivo incorporato in fabbrica piuttosto che su un server remoto. Dovete
comprendere il contesto completo in cui si inserisce il vostro lavoro.
Una volta effettuata la ricerca, dovreste sapere quali saranno i vostri input, quali saranno
i vostri obiettivi e a quale tipo di attività di apprendimento automatico si riferisce il
problema. In questa fase è necessario essere consapevoli delle ipotesi che si stanno
facendo:
◾ Si ipotizza che gli obiettivi possano essere previsti in base agli input.
◾ Si ipotizza che i dati disponibili (o che si raccoglieranno presto) siano
sufficientemente informativi per imparare la relazione tra input e target.
Finché non si dispone di un modello funzionante, queste sono solo ipotesi, in attesa di
essere convalidate o invalidate. Non tutti i problemi possono essere risolti con
l'apprendimento automatico; il fatto di aver raccolto esempi di input X e di target Y non
significa che X contenga informazioni sufficienti per prevedere Y. Per esempio, se si
cerca di prevedere l'andamento di un titolo in borsa in base alla sua recente storia di
prezzo, è improbabile che si riesca a farlo, perché la storia dei prezzi non contiene molte
informazioni predittive.

6.1.2 Raccogliere un set di dati


Una volta compresa la natura dell'attività e sapendo quali saranno gli input e i tar- get, è
il momento della raccolta dei dati, la parte più ardua, lunga e costosa della maggior
parte dei progetti di apprendimento automatico.
◾ Il progetto del motore di ricerca fotografico richiede innanzitutto di selezionare
l'insieme di etichette che si desidera classificare, scegliendo 10.000 categorie di
immagini comuni. Poi bisogna etichettare manualmente centinaia di migliaia di
immagini caricate dagli utenti con le etichette di questo insieme.
◾ Per il progetto di rilevamento dello spam dell'app di chat, poiché le chat degli
utenti sono crittografate end-to-end, non è possibile utilizzarne il contenuto
per addestrare un modello. È necessario accedere a un set di dati separato,
Definire il 159
composto da decine di compito
migliaia di post non filtrati sui social media, e
etichettarli manualmente come spam, offensivi o accettabili.
160 CAPITOLO 6 Il flusso di lavoro universale
dell'apprendimento automatico
◾ Per il motore di raccomandazione musicale, è sufficiente utilizzare i "mi
piace" degli utenti. Non è necessario raccogliere nuovi dati. Allo stesso modo,
per il progetto di previsione del tasso di clic: disponete di un ampio registro
del tasso di clic per i vostri annunci passati, che risale ad anni fa.
◾ Per il modello di etichettatura dei biscotti, è necessario installare telecamere
sopra le cinture di controllo per raccogliere decine di migliaia di immagini, e
poi qualcuno dovrà etichettare manualmente queste immagini. Le persone che
sanno come farlo lavorano attualmente presso la fabbrica di biscotti, ma non
sembra troppo difficile. Dovreste essere in grado di addestrare le persone a
farlo.
◾ Il progetto di immagini satellitari richiederà a un team di archeologi di
raccogliere un database di siti di interesse esistenti, e per ogni sito sarà necessario
trovare immagini satellitari esistenti scattate in condizioni meteorologiche
diverse. Per ottenere un buon modello, saranno necessari migliaia di siti diversi.

Nel capitolo 5 abbiamo appreso che la capacità di generalizzazione di un modello


deriva quasi interamente dalle proprietà dei dati su cui è stato addestrato: il numero
di punti dati a disposizione, l'affidabilità delle etichette, la qualità delle caratteristiche. Un
buon set di dati è un bene che richiede cura e investimenti. Se si hanno 50 ore in più
da dedicare a un progetto, è probabile che il modo più efficace di allocarle sia quello
di raccogliere più dati piuttosto che cercare miglioramenti incrementali nella
modellazione.
L'affermazione che i dati contano più degli algoritmi è stata resa famosa in un
documento del 2009 dei ricercatori di Google intitolato "The Unreasonable
Effectiveness of Data" (il titolo è una citazione del noto articolo del 1960 "The
Unreasonable Effectiveness of Mathematics in the Natural Sciences" di Eugene Wigner).
Questo avveniva prima che il deep learning diventasse popolare, ma, incredibilmente,
l'ascesa del deep learning non ha fatto altro che aumentare l'importanza dei dati.
Se si sta effettuando un apprendimento supervisionato, una volta raccolti gli
input (come le immagini) si avrà bisogno di annotazioni (come i tag per le
immagini), ovvero gli obiettivi che il modello dovrà addestrare a prevedere. A volte, le
annotazioni possono essere recuperate automaticamente, come quelle per il compito
di raccomandazione musicale o di previsione del tasso di clic. Ma spesso è
necessario annotare i dati a mano. Si tratta di un processo che richiede molto lavoro.
INVESTIRE NELL'INFRASTRUTTURA DI ANNOTAZIONE DEI DATI
Il processo di annotazione dei dati determina la qualità dei target, che a sua volta
determina la qualità del modello. Considerate attentamente le opzioni disponibili:
◾ Dovete annotare voi stessi i dati?
◾ Dovreste utilizzare una piattaforma di crowdsourcing come Mechanical Turk per
raccogliere le etichette?
◾ Dovete ricorrere ai servizi di un'azienda specializzata nell'etichettatura dei dati?

L'outsourcing può potenzialmente farvi risparmiare tempo e denaro, ma vi toglie il


controllo. L'uso di qualcosa come Mechanical Turk è probabilmente poco costoso e ben
scalabile, ma le annotazioni potrebbero risultare piuttosto rumorose.
Definire il 161
compito
Per scegliere l'opzione migliore, bisogna considerare i vincoli con cui si lavora:
◾ Gli etichettatori dei dati devono essere esperti della materia o chiunque può
annotare i dati? Le etichette per un problema di classificazione di immagini tra
gatti e cani possono essere selezionate da chiunque, ma quelle per un compito di
classificazione di razze canine richiedono conoscenze specifiche. Nel frattempo,
l'annotazione di scansioni TC di fratture ossee richiede praticamente una laurea in
medicina.
◾ Se l'annotazione dei dati richiede conoscenze specialistiche, è possibile
addestrare le persone a farlo? In caso contrario, come si può accedere agli esperti
del settore?
◾ Siete in grado di capire il modo in cui gli esperti elaborano le annotazioni? In
caso contrario, dovrete trattare il vostro set di dati come una scatola nera e non sarete
in grado di eseguire l'ingegnerizzazione manuale delle caratteristiche: non è un
aspetto critico, ma può essere limitante.
Se decidete di etichettare i vostri dati internamente, chiedetevi quale software userete
per registrare le annotazioni. È possibile che dobbiate svilupparlo voi stessi. Un
software di annotazione dei dati produttivo vi farà risparmiare molto tempo, quindi vale
la pena investirvi fin dalle prime fasi del progetto.
ATTENZIONE AI DATI NON RAPPRESENTATIVI
I modelli di apprendimento automatico possono dare un senso solo a input simili a
quelli c h e h a n n o già visto in precedenza. Pertanto, è fondamentale che i dati
utilizzati per l'addestramento siano rappresentativi dei dati di produzione. Questa
preoccupazione dovrebbe essere alla base d i tutto il lavoro di raccolta dei dati.
Supponiamo che stiate sviluppando un'applicazione in cui gli utenti possono
fotografare un piatto di cibo per scoprirne il nome. Addestrate un modello utilizzando le
immagini di un social network di condivisione di immagini molto popolare tra i
buongustai. Al momento della distribuzione, iniziano ad arrivare i feedback degli utenti
arrabbiati: la vostra app sbaglia la risposta 8 volte su 10. Cosa succede? Cosa succede?
La vostra accuratezza sul set di test era ben superiore al 90%! Una rapida occhiata ai
dati caricati dagli utenti rivela che le immagini caricate dai cellulari di piatti a caso di
ristoranti a caso, scattate con smartphone a caso, non assomigliano affatto alle immagini
appetitose, ben illuminate e di qualità professionale su cui avete addestrato il modello: i
dati di addestramento non erano rappresentativi dei dati di produzione. Questo è un
peccato capitale: benvenuti nell'inferno dell'apprendimento automatico.
Se possibile, raccogliete i dati direttamente dall'ambiente in cui verrà utilizzato il
modello. Un modello di classificazione del sentiment delle recensioni cinematografiche
dovrebbe essere usato sulle nuove recensioni di IMDB, non sulle recensioni dei
ristoranti di Yelp né sugli aggiornamenti di stato di Twitter. Se volete valutare il
sentiment di un tweet, iniziate raccogliendo e annotando i tweet reali di un gruppo di
utenti simile a quello che vi aspettate in produzione. Se non è possibile addestrarsi sui
dati di produzione, assicurarsi di comprendere appieno le differenze tra i dati di
addestramento e quelli di produzione e di correggere attivamente queste differenze.
Un fenomeno correlato di cui dovete essere consapevoli è la deriva concettuale.
La deriva concettuale è presente in quasi tutti i problemi del mondo reale,
soprattutto in quelli che riguardano i dati generati dagli utenti. La deriva concettuale
162 CAPITOLO 6 Il flusso di lavoro universale
si verifica quando le dell'apprendimento
proprietà dei dati di produzione cambiano nel tempo, causando
automatico
un graduale decadimento dell'accuratezza del modello. Un motore di
raccomandazione musicale addestrato nel 2013 potrebbe non essere molto efficace
oggi. Allo stesso modo, il set di dati IMDB con cui si è lavorato è stato raccolto nel 2011,
e un modello addestrato su di esso sarebbe
Definire il 163
compito
è probabile che non abbia le stesse prestazioni sulle recensioni del 2020 rispetto a quelle
del 2012, poiché il vocabolario, le espressioni e i generi cinematografici si evolvono nel
tempo. La deriva concettuale è particolarmente accentuata in contesti avversi come il
rilevamento delle frodi con le carte di credito, dove i modelli di frode cambiano
praticamente ogni giorno. Affrontare la rapida deriva dei concetti richiede una costante
raccolta di dati, l'annotazione e la riqualificazione dei modelli.
Tenete presente che l'apprendimento automatico può essere utilizzato solo per
memorizzare gli schemi presenti nei dati di addestramento. È in grado di
riconoscere solo ciò che ha già visto in precedenza. Utilizzare l'apprendimento
automatico addestrato sui dati passati per prevedere il futuro significa partire dal
presupposto che il futuro si comporterà come il passato. Spesso non è così.

Il problema del bias di campionamento


Un caso particolarmente insidioso e comune di dati non rappresentativi è il bias di
campionamento. Il bias di campionamento si verifica quando il processo di raccolta
dei dati interagisce con ciò che si sta cercando di prevedere, dando luogo a
misurazioni distorte. Un famoso esempio storico si è verificato nelle elezioni
presidenziali statunitensi del 1948. La sera delle elezioni, il Chi- cago Tribune
stampò il titolo "DEWEY DEFEATS TRUMAN". La mattina dopo, Truman risultò
vincitore. Il direttore del Tribune si era fidato dei risultati di un sondaggio telefonico,
ma gli utenti del telefono nel 1948 non erano un campione casuale e
rappresentativo della popolazione votante. Era più probabile che fossero più ricchi,
conservatori e che votassero per Dewey, il candidato repubblicano.

"DEWEY SCONFIGGE TRUMAN": Un famoso esempio di distorsione del campionamento

Al giorno d'oggi, ogni sondaggio telefonico tiene conto dei bias di campionamento.
Questo non significa che i bias di campionamento siano un ricordo del passato nei
sondaggi politici, tutt'altro. Ma a differenza del 1948, i sondaggisti ne sono
consapevoli e prendono provvedimenti per correggerli.
164 CAPITOLO 6 Il flusso di lavoro universale
dell'apprendimento automatico
6.1.3 Comprendere i dati
È una pratica piuttosto scorretta trattare un set di dati come una scatola nera. Prima di
iniziare l'addestramento delle modalità, è necessario esplorare e visualizzare i dati per
ottenere informazioni su ciò che li rende predittivi, in modo da informare
l'ingegnerizzazione delle funzionalità e individuare potenziali problemi.
◾ Se i vostri dati includono immagini o testo in linguaggio naturale, guardate
direttamente alcuni campioni (e le loro etichette).
◾ Se i dati contengono caratteristiche numeriche, è una buona idea tracciare
l'istogramma dei valori delle caratteristiche per avere un'idea della gamma di
valori assunti e della frequenza dei diversi valori.
◾ Se i dati includono informazioni sulla posizione, tracciateli su una mappa.
Emerge qualche schema chiaro?
◾ In alcuni campioni mancano valori per alcune caratteristiche? In tal caso, è
necessario gestire questo problema quando si preparano i dati (vedremo come
farlo nella prossima sezione).
◾ Se il vostro compito è un problema di classificazione, stampate il numero di
istanze di ciascuna classe nei vostri dati. Le classi sono rappresentate in modo
approssimativo? In caso contrario, è necessario tenere conto di questo squilibrio.
◾ Verificare la presenza di target leaking: la presenza di caratteristiche nei dati che
forniscono informazioni sui target e che potrebbero non essere disponibili in
produzione. Se si sta addestrando un modello sulle cartelle cliniche per prevedere
se una persona sarà trattata per il cancro in futuro, e le cartelle includono la
caratteristica "a questa persona è stato diagnosticato il cancro", allora i target
sono stati fatti trapelare artificialmente nei dati. Chiedetevi sempre: ogni
caratteristica dei vostri dati è disponibile nella stessa forma in produzione?

6.1.4 Scegliere una misura del successo


Per controllare qualcosa, bisogna essere in grado di osservarla. Per ottenere il
successo in un progetto, bisogna prima definire cosa si intende per successo.
Accuratezza? Precisione e richiamo? Tasso di fidelizzazione dei clienti? La vostra
metrica di successo guiderà tutte le scelte tecniche che farete nel corso del progetto.
Dovrebbe essere direttamente in linea con gli obiettivi di livello superiore, come il
successo commerciale del cliente.
Per i problemi di classificazione bilanciata, in cui ogni classe ha la stessa
probabilità, l'accuratezza e l'area sotto una curva caratteristica operativa del ricevitore
(ROC), abbreviata in ROC AUC, sono metriche comuni. Per i problemi di
sbilanciamento delle classi, i problemi di classificazione o la classificazione multilabel,
è possibile utilizzare la precisione e il richiamo, nonché una forma ponderata di
accuratezza o ROC AUC. E non è raro dover definire una propria metrica personalizzata
con cui misurare il successo. Per avere un'idea della diversità delle metriche di successo
dell'apprendimento automatico e del loro rapporto con i diversi domini problematici, è
utile sfogliare le competizioni di scienza dei dati su Kaggle (https://kaggle.com); esse
presentano un'ampia gamma di problemi e metriche di valutazione.
Sviluppare un 161
modello

6.2 Sviluppare un modello


Una volta che si sa come misurare i propri progressi, si può iniziare con lo sviluppo
del modello. La maggior parte dei tutorial e dei progetti di ricerca presuppongono
che questa sia l'unica fase, saltando la definizione del problema e la raccolta dei set
di dati, che si presume siano già state fatte, e saltando l'implementazione e la
manutenzione del modello, che si presume siano gestite da qualcun altro. In realtà,
lo sviluppo del modello è solo una fase del flusso di lavoro dell'apprendimento
automatico e, a mio parere, non è la più difficile. Le cose più difficili
nell'apprendimento automatico sono l'inquadramento dei problemi e la raccolta,
l'annotazione e la pulizia dei dati. Quindi, su con la vita: quello che verrà dopo sarà
facile in confronto!

6.2.1 Preparare i dati


Come abbiamo appreso in precedenza, i modelli di apprendimento profondo in
genere non ingeriscono dati grezzi. La preelaborazione dei dati mira a rendere i dati
grezzi più adatti alle reti neurali. Ciò include la vettorizzazione, la normalizzazione o la
gestione dei valori mancanti. Molte tecniche di preelaborazione sono specifiche del
dominio (ad esempio, specifiche per i dati di testo o per le immagini); le tratteremo
nei capitoli successivi, man mano che le incontreremo negli esempi pratici. Per ora,
rivedremo le basi comuni a tutti i domini di dati.
VETTORIZZAZIONE
Tutti gli input e gli obiettivi di una rete neurale devono essere tipicamente tensori di dati
in virgola mobile (o, in casi specifici, tensori di numeri interi o stringhe). Qualunque
dato si debba elaborare - suoni, immagini, testi - deve essere prima trasformato in
tensori, una fase chiamata vettorizzazione dei dati. Per esempio, nei due precedenti
esempi di classificazione del testo del capitolo 4, siamo partiti da un testo rappresentato
come elenchi di numeri interi (che rappresentano sequenze di parole) e abbiamo usato la
codifica one-hot per trasformarli in un tensore di dati float32. Negli esempi di
classificazione delle cifre e di previsione dei prezzi delle case, i dati sono stati forniti in
forma vettoriale, quindi abbiamo potuto saltare questo passaggio.
NORMALIZZAZIONE DEI VALORI
Nell'esempio di classificazione delle cifre di MNIST del capitolo 2, abbiamo
iniziato con i dati dell'immagine codificati come numeri interi nell'intervallo 0-255,
codificando i valori in scala di grigi. Prima di inserire questi dati nella nostra rete,
abbiamo dovuto trasformarli in float32 e dividerli per 255, in modo da ottenere
valori in virgola mobile nell'intervallo 0-1. Allo stesso modo, quando abbiamo previsto i
prezzi delle case, abbiamo iniziato con caratteristiche che avevano una varietà di
intervalli: alcune caratteristiche avevano piccoli valori in virgola mobile, mentre
altre avevano valori interi piuttosto grandi. Prima di inserire questi dati nella nostra
rete, abbiamo dovuto normalizzare ogni caratteristica in modo indipendente, in
modo che avesse una deviazione standard di 1 e una media di 0.
In generale, non è sicuro alimentare una rete neurale con dati che assumono
valori relativamente grandi (ad esempio, numeri interi a più cifre, che sono molto
più grandi dei valori iniziali assunti dai pesi di una rete) o dati eterogenei (ad
esempio, dati in cui una caratteristica è nell'intervallo 0-1 e un'altra è nell'intervallo
162 CAPITOLO 6 Il flusso di lavoro universale
100-200). In questo modo si possono
dell'apprendimento innescare aggiornamenti del gradiente di
automatico
grandi dimensioni che impediscono alla rete di
Sviluppare un 163
modello
dalla convergenza. Per facilitare l'apprendimento della rete, i dati devono a v e r e l e
seguenti caratteristiche:
◾ Assumete valori piccoli - In genere, la maggior parte dei valori dovrebbe essere compresa
nell'intervallo 0-1.
◾ Essere omogenei - Tutte le caratteristiche devono assumere valori compresi più o meno
nello stesso intervallo.
Inoltre, la seguente pratica di normalizzazione più rigida è comune e può essere
utile, anche se non è sempre necessaria (per esempio, non l ' a b b i a m o f a t t a
nell'esempio di classificazione d e l l e cifre):
◾ Normalizzare ogni caratteristica in modo indipendente in modo che abbia una media
pari a 0.
◾ Normalizzare ogni caratteristica in modo indipendente per avere una

deviazione standard di 1. Questo è facile da fare con gli array NumPy:

x -=
Assumendo che x sia una
x.media(asse=0) x matrice di dati 2D di forma
/= x.std(asse=0) (campioni, caratteristiche)

GESTIONE DEI VALORI MANCANTI


A volte i dati possono presentare valori mancanti. Ad esempio, nell'esempio dei prezzi
delle case, la prima caratteristica (la colonna di indice 0 nei dati) era il tasso di
criminalità pro capite. E se questa caratteristica non fosse disponibile per tutti i
campioni? In questo caso si avrebbero valori mancanti nei dati di addestramento o di
test.
Si potrebbe scartare completamente la funzione, ma non è detto che sia necessario.
◾ Se la caratteristica è categorica, è possibile creare una nuova categoria che
significa "il valore è mancante". Il modello imparerà automaticamente cosa
implica rispetto agli obiettivi.
◾ Se la caratteristica è numerica, evitare di inserire un valore arbitrario come "0",
perché potrebbe creare una discontinuità nello spazio latente formato dalle
caratteristiche, rendendo più difficile la generalizzazione del modello addestrato.
Si può invece considerare di sostituire il valore mancante con il valore medio o
mediano della caratteristica nel set di dati. Si potrebbe anche addestrare un
modello per prevedere il valore della caratteristica in base ai valori di altre
caratteristiche.
Si noti che se si prevedono caratteristiche categoriali mancanti nei dati di test, ma la rete
è stata addestrata su dati senza valori mancanti, la rete non avrà imparato a ignorare i
valori mancanti! In questa situazione, è necessario generare artificialmente campioni di
addestramento con voci mancanti: copiare più volte alcuni campioni di addestramento e
togliere alcune delle caratteristiche categoriali che si prevede saranno probabilmente
mancanti nei dati di test.

6.2.2 Scegliere un protocollo di valutazione


Come si è appreso nel capitolo precedente, lo scopo di un modello è quello di
164 CAPITOLO 6 Il flusso di lavoro universale
dell'apprendimento automatico

raggiungere la generalizzazione, e ogni decisione di modellazione che si prenderà


durante il processo di sviluppo del modello sarà guidata da metriche di validazione
che cercano di misurare le prestazioni di generalizzazione. L'obiettivo del protocollo
di validazione è quello di stimare con precisione le prestazioni del modello.
Sviluppare un 165
modello
La metrica di successo scelta (come l'accuratezza) sarà sui dati di produzione effettivi.
L'affidabilità di questo processo è fondamentale per costruire un modello utile.
Nel capitolo 5 abbiamo esaminato tre protocolli di valutazione comuni:
◾ Mantenere un set di convalida di holdout: questa è la strada da seguire quando si
dispone di molti dati.
◾ Eseguire la convalida incrociata K-fold: questa è la scelta giusta quando si dispone
di un numero troppo esiguo di campioni perché la convalida holdout sia affidabile.
◾ Eseguire una convalida K-fold iterata: serve per eseguire una valutazione altamente
accurata del modello quando sono disponibili pochi dati.
Scegliete uno di questi. Nella maggior parte dei casi, il primo funzionerà abbastanza
bene. Come avete imparato, però, fate sempre attenzione alla rappresentatività del vostro
set di validazione e fate attenzione a non avere campioni ridondanti tra il set di
allenamento e quello di validazione.

6.2.3 Battere una linea di base


Quando si inizia a lavorare sul modello stesso, l'obiettivo iniziale è quello di
raggiungere la potenza statistica, come si è visto nel capitolo 5: cioè, sviluppare un
piccolo modello che sia in grado di battere una semplice linea di base.
In questa fase, queste sono le tre cose più importanti su cui concentrarsi:
◾ Ingegneria delle caratteristiche: filtrare le caratteristiche non informative (selezione
delle caratteristiche) e utilizzare la conoscenza del problema per sviluppare nuove
caratteristiche che potrebbero essere utili.
◾ Selezione dei priori dell'architettura corretta - Quale tipo di architettura del modello
si intende utilizzare? Una rete densamente connessa, una rete convnet, una rete
neurale ricorrente, un trasformatore? Il deep learning è un approccio valido per
questo compito o è meglio usare qualcos'altro?
◾ Selezione di una configurazione di addestramento sufficientemente buona: quale
funzione di perdita utilizzare? Quali sono le dimensioni del batch e il tasso di
apprendimento?

Scegliere la giusta funzione di perdita


Spesso non è possibile ottimizzare direttamente la metrica che misura il successo
di un problema. A volte non c'è un modo semplice per trasformare una metrica in
una funzione di perdita; le funzioni di perdita, infatti, devono essere calcolabili con
un piccolo gruppo di dati (idealmente, una funzione di perdita dovrebbe essere
calcolabile con un solo punto di dati) e devono essere differenziabili (altrimenti non
si può usare la retropropagazione per addestrare la rete). Ad esempio, la metrica
di classificazione ROC AUC, ampiamente utilizzata, non può essere ottimizzata
direttamente. Pertanto, nei compiti di classificazione, è comune ottimizzare una
metrica proxy della ROC AUC, come la crossentropia. In generale, si può sperare
che quanto più bassa è la crossentropia, tanto più alta sarà la ROC AUC.
La tabella seguente può aiutare a scegliere un'attivazione dell'ultimo strato e una
funzione di perdita per alcuni tipi di problemi comuni.
166 CAPITOLO 6 Il flusso di lavoro universale
dell'apprendimento automatico

(continua)
Scegliere la giusta funzione di attivazione e perdita dell'ultimo strato per il proprio modello
Tipo di problema Attivazione dell'ultimo Funzione di perdita
strato
Classificazione binaria sigmoide binario_crossentropia

Classificazione multiclasse, single- softmax categorico_crossentropia

label Classificazione multiclasse, sigmoid binario_crossentropia

multilabel e

Per la maggior parte dei problemi, esistono modelli esistenti da cui partire. Non siete
la prima persona che cerca di costruire un rilevatore di spam, un motore di
raccomandazione musicale o un classificatore di immagini. Assicuratevi di fare una
ricerca sui precedenti per identificare le tecniche di ingegneria delle caratteristiche e
le architetture dei modelli che hanno maggiori probabilità di ottenere buoni risultati
nel vostro compito. Si noti che non è sempre possibile raggiungere la potenza
statistica. Se non si riesce a battere una linea di base semplice dopo aver provato
diverse architetture ragionevoli, è possibile che la risposta alla domanda che si sta
ponendo non sia presente nei dati di input. Ricordate che state
fare due ipotesi:
◾ Si ipotizza che i risultati possano essere previsti in base agli input.
◾ Si ipotizza che i dati disponibili siano sufficientemente informativi per
imparare la relazione tra input e output.
Può darsi che queste ipotesi siano false, nel qual caso bisogna tornare al tavolo da disegno.

6.2.4 Scalare: Sviluppare un modello che si adatti al meglio


Una volta ottenuto un modello con potenza statistica, la domanda diventa: il modello è
sufficientemente potente? Ha abbastanza livelli e parametri per modellare
correttamente il problema in questione? Ad esempio, un modello di regressione
logistica ha una potenza statistica su MNIST, ma non sarebbe sufficiente per risolvere
bene il problema. Ricordate che la tensione universale nell'apprendimento automatico è
tra ottimizzazione e generalizzazione. Il modello ideale è quello che si trova al
confine tra underfitting e overfitting, tra undercapacity e overcapacity. Per capire
dove si trova questo confine, bisogna prima attraversarlo.
Per capire quanto grande sarà il modello di cui avrete bisogno, dovrete sviluppare un
modello che si adatti al meglio.
Si tratta di un'operazione abbastanza semplice, come si è appreso nel capitolo 5:
1 Aggiungere i livelli.
2 Ingrandire i livelli.
3 Allenarsi per più epoche.

Monitorare sempre la perdita di addestramento e la perdita di convalida, nonché i valori


di addestramento e di convalida per tutte le metriche a cui si tiene. Quando si nota che
le p r e s t a z i o n i del modello sui dati di convalida iniziano a peggiorare, si è
Sviluppare un 167
verificato un overfitting. modello
Distribuire il 165
modello
6.2.5 Regolarizzare e mettere a punto il modello
Una volta raggiunta la potenza statistica e la capacità di overfit, si sa che si è sulla strada
giusta. A questo punto, l'obiettivo diventa quello di massimizzare le prestazioni di
generalizzazione.
Questa fase richiederà molto tempo: modificherete ripetutamente il vostro
modello, lo addestrerete, lo valuterete sui dati di convalida (non sui dati di test, a
questo punto), lo modificherete di nuovo e lo ripeterete, fino a quando il modello
non sarà il migliore possibile. Ecco alcune cose da provare:
◾ Provate diverse architetture, aggiungete o togliete strati.
◾ Aggiungere l'abbandono.
◾ Se il modello è piccolo, aggiungere la regolarizzazione L1 o L2.
◾ Provare diversi iperparametri (come il numero di unità per strato o il tasso di
apprendimento dell'ottimizzatore) per trovare la configurazione ottimale.
◾ In alternativa, iterare la cura dei dati o l'ingegnerizzazione delle
caratteristiche: raccogliere e annotare più dati, sviluppare caratteristiche
migliori o rimuovere le caratteristiche che non sembrano essere informative.
È possibile automatizzare gran parte di questo lavoro utilizzando un software di
regolazione automatica degli iperparametri, come KerasTuner. Ne parleremo nel
capitolo 13.
Tenete presente quanto segue: Ogni volta che si utilizza il feedback del processo di
convalida per mettere a punto il modello, si disperdono nel modello informazioni sul
processo di convalida. Se ripetuto poche volte, questo è innocuo; se fatto
sistematicamente per molte iterazioni, finirà per far sì che il modello si adatti in modo
eccessivo al processo di convalida (anche se nessun modello è stato addestrato
direttamente sui dati di convalida). Questo rende il processo di valutazione meno
affidabile.
Una volta sviluppata una configurazione soddisfacente del modello, è possibile
addestrare il modello di produzione finale su tutti i dati disponibili (formazione e
convalida) e valutarlo un'ultima volta sul set di test. Se si scopre che le prestazioni
sul set di test sono significativamente peggiori di quelle misurate sui dati di
convalida, ciò può significare che la procedura di convalida non era affidabile o che
si è iniziato a fare overfitting sui dati di convalida durante la regolazione dei
parametri del modello. In questo caso, si potrebbe passare a un protocollo di
valutazione più affidabile (come la validazione iterata K-fold).

6.3 Distribuire il modello


Il vostro modello ha superato con successo la valutazione finale sul set di prova ed è
pronto per essere distribuito e iniziare la sua vita produttiva.

6.3.1 Spiegare il proprio lavoro alle parti interessate e definire le aspettative


Il successo e la fiducia dei clienti consistono nel soddisfare o superare
costantemente le aspettative delle persone. Il sistema effettivamente consegnato è
solo la metà di questo quadro; l'altra metà è la definizione di aspettative adeguate
prima del lancio.
166 CAPITOLO 6 Il flusso di lavoro universale
Le aspettative dell'apprendimento
dei non specialisti nei confronti dei sistemi di IA sono spesso
automatico
irrealistiche. Ad esempio, potrebbero aspettarsi che il sistema "capisca" il suo compito e
sia capace di
Distribuire il 167
modello
esercitare un buon senso simile a quello umano nel contesto del compito. Per ovviare a
questo problema, dovreste prendere in considerazione la possibilità di mostrare alcuni
esempi delle modalità di fallimento del vostro modello (ad esempio, mostrate l'aspetto
dei campioni classificati in modo errato, soprattutto quelli per i quali l'errore di
classificazione sembra sorprendente).
Ci si potrebbe anche aspettare prestazioni di livello umano, soprattutto per i processi che
in precedenza erano gestiti da persone. La maggior parte dei modelli di apprendimento
automatico, essendo addestrati (in modo imperfetto) per approssimare le etichette
generate dall'uomo, non arrivano a tanto. È necessario comunicare chiaramente le
aspettative sulle prestazioni del modello. Evitate di usare affermazioni astratte come "Il
modello ha un'accuratezza del 98%" (che la maggior parte delle persone arrotonda
mentalmente al 100%) e preferite parlare, ad esempio, di tassi di falsi negativi e
tassi di falsi positivi. Si potrebbe dire: "Con queste impostazioni, il modello di
rilevamento delle frodi avrebbe un tasso di falsi negativi del 5% e un tasso di falsi
positivi del 2,5%. Ogni giorno, una media di 200 transazioni valide verrebbero
segnalate come fraudolente e inviate per una revisione manuale, e una media di 14
transazioni fraudolente non verrebbero rilevate. In media, 266 transazioni fraudolente
verrebbero individuate correttamente". Mettere chiaramente in relazione le metriche di
performance del modello con gli obiettivi aziendali.
Dovreste anche assicurarvi di discutere con gli stakeholder la scelta dei
parametri chiave di lancio, ad esempio la soglia di probabilità con cui una
transazione dovrebbe essere segnalata (soglie diverse produrranno tassi diversi di
falsi negativi e falsi positivi). Tali decisioni comportano compromessi che possono
essere gestiti solo con una profonda conoscenza del contesto aziendale.

6.3.2 Spedire un modello di inferenza


Un progetto di apprendimento automatico non finisce quando si arriva a un
notebook Colab in grado di salvare un modello addestrato. Raramente si mette in
produzione lo stesso identico oggetto modello Python manipolato durante
l'addestramento.
Per prima cosa, è possibile esportare il modello in un formato diverso da Python:
◾ L'ambiente di produzione potrebbe non supportare affatto Python, ad esempio
se si tratta di un'applicazione mobile o di un sistema embedded.
◾ Se il resto dell'applicazione non è in Python (potrebbe essere in JavaScript, C++,
ecc.), l'uso di Python per servire un modello può comportare un overhead
significativo.
In secondo luogo, poiché il modello di produzione verrà utilizzato solo per produrre
previsioni (una fase chiamata inferenza), piuttosto che per l'addestramento, si ha
spazio per eseguire varie ottimizzazioni che possono rendere il modello più veloce e
ridurre l'ingombro in memoria.
Diamo una rapida occhiata alle diverse opzioni di distribuzione dei modelli disponibili.
DISTRIBUZIONE DI UN MODELLO COME API REST
Questo è forse il modo più comune per trasformare un modello in un prodotto: installare
TensorFlow su un server o un'istanza cloud e interrogare le previsioni del modello
tramite un'API REST. Si può costruire la propria applicazione di servizio utilizzando
168 CAPITOLO 6 Il flusso di lavoro universale
qualcosa come Flaskdell'apprendimento
(o qualsiasi altra libreria di sviluppo web in Python), oppure
automatico
utilizzare la libreria di TensorFlow per la distribuzione dei modelli come API, chiamata
TensorFlow Serving (www.tensorflow.org/tfx/guide/serving). Con Tensor Flow Serving
è possibile distribuire un modello Keras in pochi minuti.
Distribuire il 169
modello
Si dovrebbe usare questa configurazione di distribuzione quando
◾ L'applicazione che consumerà le previsioni del modello dovrà avere un
accesso affidabile a Internet (ovviamente). Ad esempio, se la vostra
applicazione è un'applicazione mobile, servire le previsioni da un'API remota
significa che l'applicazione non sarà utilizzabile in modalità aereo o in un
ambiente a bassa connettività.
◾ L'applicazione non ha requisiti di latenza rigidi: il viaggio di andata e ritorno
tra la richiesta, l'inferenza e la risposta dura in genere circa 500 ms.
◾ I dati di input inviati per l'inferenza non sono altamente sensibili: i dati
dovranno essere disponibili sul server in forma decifrata, poiché dovranno
essere visti dal modello (ma si noti che è necessario utilizzare la crittografia
SSL per la richiesta e la risposta HTTP).
Per esempio, il progetto del motore di ricerca di immagini, il sistema di
raccomandazione musicale, il progetto di rilevamento delle frodi sulle carte di
credito e il progetto di immagini satellitari sono tutti adatti a essere serviti tramite
un'API REST.
Una questione importante quando si distribuisce un modello come API REST è se si
vuole ospitare il codice per conto proprio o se si vuole utilizzare un servizio cloud di
terze parti completamente gestito. Ad esempio, Cloud AI Platform, un prodotto di
Google, consente di caricare semplicemente il modello TensorFlow su Google Cloud
Storage (GCS) e fornisce un endpoint API per interrogarlo. Si occupa di molti dettagli
pratici come il batching delle predizioni, il bilanciamento del carico e la scalabilità.
DISTRIBUZIONE DI UN MODELLO SU UN DISPOSITIVO
A volte è necessario che il modello risieda sullo stesso dispositivo che esegue
l'applicazione che lo utilizza, ad esempio uno smartphone, una CPU ARM
incorporata in un robot o un microcontrollore su un piccolo dispositivo. Vi sarà
capitato di vedere una fotocamera in grado di rilevare automaticamente persone e
volti nelle scene su cui la puntate: probabilmente si trattava di un piccolo modello di
deep learning in esecuzione direttamente sulla fotocamera.
Si consiglia di utilizzare questa configurazione quando
◾ Il modello ha vincoli di latenza stringenti o deve essere eseguito in un
ambiente a bassa connettività. Se state costruendo un'applicazione di realtà
aumentata immersiva, interrogare un server remoto non è un'opzione
praticabile.
◾ Il modello può essere reso sufficientemente piccolo da poter essere eseguito
con i limiti di memoria e di potenza del dispositivo di destinazione. A questo
scopo si può usare TensorFlow Model Opti- mization Toolkit
(www.tensorflow.org/model_optimization).
◾ Ottenere la massima precisione possibile non è fondamentale per il vostro
compito. C'è sempre un compromesso tra l'efficienza del tempo di esecuzione
e l'accuratezza, quindi i vincoli di memoria e di potenza spesso impongono di
fornire un modello che non è altrettanto buono quanto il miglior modello che
si potrebbe eseguire su una GPU di grandi dimensioni.
170 CAPITOLO 6 Il flusso di lavoro universale
◾ I dati in ingresso sono strettamente
dell'apprendimento automaticosensibili e quindi non dovrebbero essere
decifrabili su un server remoto.
Distribuire il 171
modello
Il nostro modello di rilevamento dello spam dovrà essere eseguito sullo smartphone
dell'utente finale come parte dell'app di chat, perché i messaggi sono crittografati
end-to-end e quindi non possono essere letti da un modello ospitato in remoto. Allo
stesso modo, il modello di rilevamento dei bad-cookie ha vincoli di latenza molto
stringenti e dovrà essere eseguito in fabbrica. Fortunatamente, in questo caso non
abbiamo vincoli di potenza o di spazio, quindi possiamo eseguire il modello su una
GPU.
Per distribuire un modello Keras su uno smartphone o un dispositivo embedded, la
soluzione ideale è TensorFlow Lite (www.tensorflow.org/lite). Si tratta di un
framework per un'efficiente inferenza di deep learning on-device che funziona su
smartphone Android e iOS, oltre che su computer basati su ARM64, Raspberry Pi o
alcuni microcontrollori. Include un convertitore che può trasformare il modello Keras
in formato TensorFlow Lite.
DISTRIBUZIONE DI UN MODELLO NEL BROWSER
Il deep learning viene spesso utilizzato in applicazioni JavaScript basate su browser o
desktop. Anche se di solito è possibile fare in modo che l'applicazione interroghi un
modello remoto tramite un'API REST, ci possono essere vantaggi fondamentali
nell'eseguire il modello direttamente nel browser, sul computer dell'utente (utilizzando
le risorse della GPU, se disponibili).
Utilizzare questa configurazione quando
◾ Si vuole scaricare l'elaborazione all'utente finale, riducendo così drasticamente i
costi dei server.
◾ I dati di input devono rimanere sul computer o sul telefono dell'utente finale.
Ad esempio, nel nostro progetto di rilevamento dello spam, la versione web e
la versione desktop dell'app di chat (implementata come applicazione
multipiattaforma scritta in Java Script) devono utilizzare un modello eseguito
localmente.
◾ La vostra applicazione ha vincoli di latenza molto stringenti. Sebbene un
modello in esecuzione sul laptop o sullo smartphone dell'utente finale sia
probabilmente più lento di uno in esecuzione su una GPU di grandi
dimensioni sul vostro server, non avete a disposizione i 100 ms in più di andata e
ritorno della rete.
◾ È necessario che l'applicazione continui a funzionare anche in assenza di
connettività, dopo che il modello è stato scaricato e memorizzato nella cache.
Questa opzione va scelta solo se il modello è abbastanza piccolo da non occupare la
CPU, la GPU o la RAM del laptop o dello smartphone dell'utente. Inoltre, poiché
l'intero modello verrà scaricato sul dispositivo dell'utente, è necessario assicurarsi
che nulla del modello debba rimanere riservato. Tenete presente che, dato un
modello di deep learning addestrato, è solitamente possibile recuperare alcune
informazioni sui dati di addestramento: meglio non rendere pubblico il modello
addestrato se è stato addestrato su dati sensibili. Per distribuire un modello in
JavaScript, l'ecosistema TensorFlow include TensorFlow.js
(www.tensorflow.org/js), una libreria JavaScript per il deep learning che implementa
quasi tutte le API di Keras (originariamente sviluppate con il nome di lavoro
WebKeras) e molte API TensorFlow di livello inferiore. È possibile importare
172 CAPITOLO 6 Il flusso di lavoro universale
facilmente un modello Keras salvato automatico
dell'apprendimento in TensorFlow.js per interrogarlo nell'ambito di
un'applicazione JavaScript basata su browser o di un'applicazione di apprendimento
profondo.
l'applicazione Electron per desktop.
Distribuire il 173
modello
OTTIMIZZAZIONE DEL MODELLO DI INFERENZA
L'ottimizzazione del modello per l'inferenza è particolarmente importante quando si
distribuisce in un ambiente con vincoli stringenti sulla potenza e sulla memoria
disponibili (smartphone e dispositivi embedded) o per applicazioni con requisiti di
bassa latenza. Si dovrebbe sempre cercare di ottimizzare il modello prima di
importarlo in TensorFlow.js o esportarlo in TensorFlow Lite.
Esistono due tecniche di ottimizzazione molto diffuse che si possono applicare:
◾ Potenziamento dei pesi: non tutti i coefficienti di un tensore di pesi
contribuiscono allo stesso modo alle previsioni. È possibile ridurre notevolmente
il numero di parametri negli strati del modello, mantenendo solo quelli più
significativi. In questo modo si riduce l'ingombro in memoria e in calcolo del
modello, con un piccolo costo in t e r m i n i di prestazioni. Decidendo la quantità
di potatura da applicare, s i ha il controllo del compromesso tra dimensioni e
accuratezza.
◾ Quantizzazione dei pesi: i modelli di apprendimento profondo vengono addestrati
con pesi in virgola mobile a singola precisione (float32). Tuttavia, è possibile
quantizzare i pesi in numeri interi firmati a 8 bit (int8) per ottenere un modello
di sola inferenza di dimensioni inferiori di un quarto, ma con una precisione
vicina a quella del modello originale.
L'ecosistema di TensorFlow include un toolkit per il potenziamento e la quantizzazione dei
pesi (www.
.tensorflow.org/model_optimization) che è profondamente integrato con l'API di Keras.

6.3.3 Monitorare il modello in natura


Avete esportato un modello di inferenza, lo avete integrato nella vostra applicazione
e avete fatto una prova generale sui dati di produzione: il modello si è comportato
esattamente come vi aspettavate. Avete scritto i test unitari e il codice di registrazione e
monitoraggio dello stato: perfetto. Ora è il momento di premere il grande pulsante rosso
e passare alla produzione.
Anche questa non è la fine. Una volta distribuito un modello, è necessario
continuare a monitorarne il comportamento, le prestazioni su nuovi dati,
l'interazione con il resto dell'applicazione e l'eventuale impatto sulle metriche
aziendali.
◾ Il coinvolgimento degli utenti nella vostra radio online è aumentato o
diminuito dopo l'implementazione del nuovo sistema di raccomandazione
musicale? Il tasso medio di clic degli annunci è aumentato dopo il passaggio
al nuovo modello di previsione del tasso di clic? Considerate l'utilizzo di test
A/B randomizzati per isolare l'impatto del modello stesso da altri
cambiamenti: un sottoinsieme di casi dovrebbe passare al nuovo modello,
mentre un altro sottoinsieme di controllo dovrebbe attenersi al vecchio processo.
Una volta elaborato un numero sufficiente di casi, la differenza di risultati tra i
due è probabilmente attribuibile al modello.
◾ Se possibile, eseguire regolarmente una verifica manuale delle previsioni del
modello sui dati di produzione. In genere è possibile riutilizzare la stessa
infrastruttura utilizzata per l'annotazione dei dati: inviare una frazione dei dati di
174 CAPITOLO 6 Il flusso di lavoro universale
produzione da annotare manualmente
dell'apprendimento automatico e confrontare le previsioni del modello
con le nuove annotazioni. Per esempio, questo dovrebbe essere fatto per il
motore di ricerca di immagini e per il sistema di segnalazione dei bad-cookie.
Distribuire il 175
modello
◾ Quando le verifiche manuali sono impossibili, si possono prendere in
considerazione percorsi di valutazione alternativi, come i sondaggi tra gli
utenti (ad esempio, nel caso del sistema di segnalazione dello spam e dei
contenuti offensivi).

6.3.4 Mantenere il modello


Infine, nessun modello dura per sempre. Avete già imparato a conoscere la deriva
concettuale: nel corso del tempo, le caratteristiche dei vostri dati di produzione
cambieranno, degradando gradualmente le prestazioni e la rilevanza del vostro modello.
La durata del vostro sistema di raccomandazione musicale sarà calcolata in settimane.
Per i sistemi di rilevamento delle frodi sulle carte di credito, saranno giorni. Un paio
d'anni, nel migliore dei casi, per il motore di ricerca di immagini.
Non appena il vostro modello è stato lanciato, dovreste prepararvi a formare la
generazione successiva che lo sostituirà. Per questo motivo,
◾ Osservate i cambiamenti nei dati di produzione. Sono disponibili nuove funzioni? È
necessario ampliare o modificare in altro modo il set di etichette?
◾ Continuare a raccogliere e annotare i dati e migliorare la pipeline di
annotazione nel tempo. In particolare, dovreste prestare particolare attenzione
alla raccolta di campioni che sembrano difficili da classificare per il vostro
modello attuale.
Questo conclude il flusso di lavoro universale dell'apprendimento automatico: sono
molte le cose da tenere a mente. Ci vuole tempo ed esperienza per diventare un
esperto, ma non preoccupatevi, siete già molto più saggi rispetto a qualche capitolo fa.
Ora conoscete il quadro generale, l'intero spettro dei progetti di apprendimento
automatico. Anche se la maggior parte di questo libro si concentra sullo sviluppo
del modello, ora sapete che è solo una parte dell'intero flusso di lavoro. Tenete
sempre presente il quadro generale!

Sintesi
◾ Quando si intraprende un nuovo progetto di apprendimento automatico,
occorre innanzitutto definire il problema da affrontare:
– Comprendete il contesto più ampio di ciò che vi state prefiggendo: qual è
l'obiettivo finale e quali sono i vincoli?
– Raccogliere e annotare un set di dati; assicurarsi di comprendere a fondo i dati.
– Scegliete come misurare il successo del vostro problema: quali metriche
monitorerete sui dati di convalida?
◾ Una volta compreso il problema e una volta che si dispone di un set di dati
appropriato, è necessario sviluppare un modello:
– Preparare i dati.
– Scegliete il vostro protocollo di valutazione: convalida di holdout? Convalida
K-fold? Quale porzione di dati utilizzare per la convalida?
– Raggiungere la potenza statistica: battere una semplice linea di base.
– Scalare: sviluppare un modello in grado di adattarsi a più persone.
Sintesi 171

- Regolarizzare il modello e mettere a punto i suoi iperparametri, in base alle


prestazioni sui dati di validazione. Molte ricerche sull'apprendimento
automatico tendono a concentrarsi solo su questa fase, ma è bene tenere a
mente il quadro generale.
◾ Quando il modello è pronto e produce buone prestazioni sui dati di test, è il
momento di distribuirlo:
– In primo luogo, assicuratevi di stabilire aspettative adeguate con le parti interessate.
– Ottimizzare il modello finale per l'inferenza e inviarlo all'ambiente di
distribuzione prescelto: server web, cellulare, browser, dispositivo incorporato,
ecc.
– Monitorate le prestazioni del modello in produzione e continuate a
raccogliere dati per sviluppare la prossima generazione del modello.
Lavorare con Keras:
Un'immersione profonda

Questo capitolo tratta


◾ Creazione di modelli Keras con Sequential
la classe Functional API e la sottoclasse del
modello
◾ Utilizzo dei cicli di addestramento e valutazione
integrati in Keras
◾ Utilizzo delle callback di Keras per personalizzare
la formazione
◾ Uso di TensorBoard per monitorare le
metriche di formazione e valutazione
◾ Scrivere da zero i cicli di formazione e valutazione

Ora avete un po' di esperienza con Keras: conoscete il modello sequenziale, i livelli
densi e le API integrate per l'addestramento, la valutazione e l'inferenza:
compile(), fit(), evaluate() e predict(). Nel capitolo 3 avete anche imparato
a ereditare dalla classe Layer per creare livelli personalizzati e a usare il Tensor
Flow GradientTape perimplementare un ciclo di addestramento passo-passo.
Nei prossimi capitoli ci occuperemo di computer vision, previsione delle serie
temporali, elaborazione del linguaggio naturale e deep learning generativo.
Queste applicazioni complesse richiederanno molto di più di un'architettura
sequenziale e del ciclo predefinito fit(). Perciò, per prima cosa, vi
trasformeremo in un esperto di Keras! In questo capitolo, otterrete una panoramica
completa dei modi principali per lavorare con le API di Keras: tutto ciò che è possibile
fare con le API di Keras.
Sintesi 173

172
Diversi modi per costruire modelli 173
Keras
per gestire i casi d'uso avanzati dell'apprendimento profondo che incontrerete in
seguito.

7.1 Uno spettro di flussi di lavoro


La progettazione dell'API di Keras è guidata dal principio della divulgazione progressiva
della complessità: rendere facile l'avvio, ma permettere di gestire casi d'uso di elevata
complessità, richiedendo solo un apprendimento incrementale a ogni passo. I casi d'uso
semplici dovrebbero essere facili e accessibili, mentre dovrebbero essere possibili flussi
di lavoro arbitrariamente avanzati: non importa quanto sia di nicchia e complessa la
cosa che si vuole fare, dovrebbe esserci un percorso chiaro per raggiungerla. Un
percorso che si basa sulle varie cose apprese dai flussi di lavoro più semplici. Ciò
significa che è possibile passare da principiante a esperto e utilizzare sempre gli stessi
strumenti, solo in modi diversi.
Per questo motivo, non esiste un unico modo "vero" di utilizzare Keras. Piuttosto,
Keras offre uno spettro di flussi di lavoro, dal più semplice al più flessibile. Esistono
diversi modi per costruire modelli Keras e diversi modi per addestrarli, rispondendo a
diverse esigenze. Poiché tutti questi flussi di lavoro sono basati su API condivise, come
Layer e Model, i componenti di qualsiasi flusso di lavoro possono essere utilizzati in
qualsiasi altro flusso di lavoro: tutti possono parlare tra loro.

7.2 Diversi modi per costruire modelli Keras


Esistono tre API per la costruzione di modelli in Keras (vedi figura 7.1):
◾ Il modello sequenziale, l'API più accessibile, è fondamentalmente un elenco
Python. Come tale, è limitato a semplici pile di livelli.
◾ L'API funzionale, che si concentra su architetture di modelli a grafo.
Rappresenta un buon punto intermedio tra usabilità e flessibilità e, per questo,
è l'API per la costruzione di modelli più comunemente usata.
◾ Sottoclasse del modello, un'opzione di basso livello in cui si scrive tutto da zero.
È l'ideale se si vuole avere il pieno controllo su ogni singolo aspetto. Tuttavia,
non avrete accesso a molte funzioni integrate di Keras e sarete più esposti al
rischio di commettere errori.

API funzionale
+ strati
personalizzati Sottoclasse:
API API funzionale + metriche scrivere tutto
sequenziale + strati personalizzate da zero
+ strati incorporati + perdite
incorporati personalizzate
+ ...

Utenti Ingegneri con Ingegneri con Ricercatori


inesperti, casi d'uso casi d'uso di
modelli standard nicchia che
semplici richiedono
soluzioni
personalizzate

Figura 7.1 Divulgazione progressiva della complessità per la costruzione del modello
174 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
7.2.1 Il modello sequenziale
Il modo più semplice per costruire un modello Keras è utilizzare il modello
sequenziale, che già conosciamo.

Listato 7.1 La classe Sequential

da tensorflow importa keras


da tensorflow.keras importare strati

model = keras.Sequential([
layers.Dense(64, attivazione="relu"),
layers.Dense(10,
attivazione="softmax")
])

Si noti che è possibile costruire lo stesso modello in modo incrementale tramite il metodo
add(), che è simile al metodoappend() di un elenco Python.

Listato 7.2 Costruzione incrementale di un modello sequenziale

model = keras.Sequential()
model.add(layers.Dense(64, activation="relu"))
model.add(layers.Dense(10, activation="softmax"))

Nel capitolo 4 si è visto che i livelli vengono costruiti (cioè creano i loro pesi) solo quando
vengono chiamati per la prima v o l t a . Questo perché la forma dei pesi dei livelli dipende
dalla forma del loro input: finché non si conosce la forma dell'input, non possono
essere creati.
Per questo motivo, il modello sequenziale precedente non ha pesi (listato 7.3)
finché non lo si chiama effettivamente su alcuni dati o non si chiama il suo metodo
build() con una forma di input (listato 7.4).

Listato 7.3 I modelli che non sono ancora stati costruiti non hanno pesi
A quel punto, il
>>> model.weights Il modello non è ancora stato costruito.
ValueError: I pesi per il modello sequenziale_1 non sono ancora stati creati.

Listato 7.4 Richiamo di un modello per la prima volta per costruirlo

>>> model.build(input_shape=(None, 3)) Ora è possibile


>>> model.weights recuperare i pesi del
modello.
[<tf.Variabile "dense_2/kernel:0" shape=(3, 64) dtype=float32, ... >,
<tf.Variabile "dense_2/bias:0" shape=(64,) dtype=float32, ... >
<tf.Variabile "dense_3/kernel:0" shape=(64, 10) dtype=float32, ... >,
<tf.Variabile "dense_3/bias:0" shape=(10,) dtype=float32, ... >]

Costruisce il modello: ora il modello si aspetta campioni di forma (3,). Il


None nella forma di input indica che la dimensione del lotto può essere
qualsiasi.

Dopo che il modello è stato costruito, è possibile visualizzarne il contenuto tramite il metodo
summary(), utile per il debug.
Diversi modi per costruire modelli 175
Keras

Listato 7.5 Il metodo summary()


>>> model.summary()
Modello:
"sequenziale_1"

Strato (tipo) Output ShapeParam #


=================================================================
denso_2 (denso) (Nessuno, 64) 256

denso_3 (Denso) (Nessuno, 10) 650


=================================================================
Totale params: 906
Parametri addestrabili: 906
Parametri non addestrabili: 0

Come si può vedere, questo modello si chiama "sequential_1". In Keras è possibile


dare un nome a tutto, a ogni modello e a ogni livello.

Listato 7.6 Nomi di modelli e livelli con l'argomento nome

>>> model = keras.Sequential(name="my_example_model")


>>> model.add(layers.Dense(64, activation="relu", name="my_first_layer"))
>>> model.add(layers.Dense(10, activation="softmax", name="my_last_layer"))
>>> model.build((None, 3))
>>> model.summary()
Modello:
"mio_modello_di_esempio"

Strato (tipo) Output ShapeParam #


=================================================================
mio_primo_strato (denso)( Nessuno, 64) 256

mio_ultimo_strato (Denso) (Nessuno, 10) 650


=================================================================
Totale params: 906
Parametri addestrabili: 906
Parametri non addestrabili: 0

Quando si costruisce un modello sequenziale in modo incrementale, è utile poter


stampare un riepilogo dell'aspetto del modello corrente dopo l'aggiunta di ciascun
livello. Ma non è possibile stampare un riepilogo finché il modello non è stato costruito!
In realtà c'è un modo per far costruire il Sequential al volo: basta dichiarare in
anticipo la forma degli ingressi del modello. È possibile farlo tramite la classe Input.

Listato 7.7 Specificare in anticipo la forma di ingresso del modello

model = keras.Sequential() Utilizzare Input per dichiarare la


model.add(keras.Input(shape=(3,)) forma degli ingressi. Si noti
model.add(layers.Dense(64, che l'argomento forma deve
activation="relu"))
essere la forma di ciascun
campione, non
la forma di un lotto.
176 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
Ora si può usare summary() per seguire come cambia la forma di output del modello man
mano che si aggiungono altri strati:

>>> model.summary()
Modello:
"sequenziale_2"

Strato (tipo) Output ShapeParam #


=================================================================
denso_4 (Denso) (Nessuno, 64) 256
=================================================================
Totale parametri: 256
Parametri addestrabili: 256
Parametri non addestrabili: 0

>>> model.add(layers.Dense(10, activation="softmax"))


>>> model.summary()
Modello:
"sequenziale_2"

Strato (tipo) Output ShapeParam #


=================================================================
denso_4 (Denso) (Nessuno, 64) 256

denso_5 (Denso) (Nessuno, 10) 650


=================================================================
Totale params: 906
Parametri addestrabili: 906
Parametri non addestrabili: 0

Si tratta di un flusso di debug piuttosto comune quando si ha a che fare con livelli
che trasformano i loro input in modi complessi, come i livelli convoluzionali che si
impareranno a conoscere nel capitolo 8.

7.2.2 L'API funzionale


Il modello sequenziale è facile da usare, ma la sua applicabilità è estremamente
limitata: può esprimere solo modelli con un singolo ingresso e una singola uscita,
applicando uno strato dopo l'altro in modo sequenziale. In pratica, è piuttosto
comune incontrare modelli con più ingressi (ad esempio, un'immagine e i suoi
metadati), più uscite (diverse cose che si vogliono prevedere sui dati) o una
topologia non lineare.
In questi casi, si costruisce il modello utilizzando l'API funzionale. Questo è l'uso
che si fa della maggior parte dei modelli Keras che si incontrano in giro. È divertente e
potente, sembra di giocare con i mattoncini LEGO.
UN SEMPLICE ESEMPIO
Cominciamo con qualcosa di semplice: la pila di due livelli che abbiamo usato nella
sezione precedente. La versione dell'API funzionale si presenta come il seguente
elenco.
Diversi modi per costruire modelli 177
Keras

Listato 7.8 Un semplice modello funzionale con due strati densi


input = keras.Input(shape=(3,), name="my_input") features
= layers.Dense(64, activation="relu")(input) outputs =
layers.Dense(10, activation="softmax")(features) model =
keras.Model(inputs=inputs, outputs=outputs)

Esaminiamo la procedura passo dopo passo.


Abbiamo iniziato dichiarando un Input (si noti che si possono anche dare nomi a
questi oggetti di input, come per ogni altra cosa):

input = keras.Input(shape=(3,), name="my_input")

Questo oggetto input contiene informazioni sulla forma e sul tipo di dati che il
modello elaborerà:
Il modello elabora lotti in cui ogni campione ha
>>> input.shape forma (3,). Il numero di campioni per lotto è
(None, 3) variabile (indicato dalla dimensione del lotto
None).
>>> input.dtype
float32 Questi batch avranno il tipo
float32.

Un oggetto di questo tipo viene chiamato tensore simbolico. Non contiene dati reali, ma
codifica le specifiche dei tensori di dati reali che il modello vedrà quando lo si usa. Sta
per tensori di dati futuri.
Successivamente, abbiamo creato un livello e lo abbiamo richiamato sull'input:

features = layers.Dense(64, activation="relu")(inputs)

Tutti i livelli di Keras possono essere chiamati sia su tensori reali di dati sia su
questi tensori simbolici. In quest'ultimo caso, restituiscono un nuovo tensore
simbolico, con informazioni aggiornate su forma e tipo:

>>>.features.shape
(None, 64)

Dopo aver ottenuto gli output finali, abbiamo istanziato il modello specificando i
suoi input e output nel costruttore Model:

outputs = layers.Dense(10, activation="softmax")(features)


model = keras.Model(inputs=inputs, outputs=outputs)

Ecco la sintesi del nostro modello:

>>> model.summary()
Modello:
"funzionale_1"

Strato (tipo) Output ShapeParam #


=================================================================
my_input (InputLayer) [(None, 3)] 0
178 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
denso_6 (Denso) (Nessuno, 64) 256

denso_7 (Denso) (Nessuno, 10) 650


=================================================================
Totale params: 906
Parametri addestrabili: 906
Parametri non addestrabili: 0

MODELLI A PIÙ INGRESSI E PIÙ USCITE


A differenza di questo modello giocattolo, la maggior parte dei modelli di deep
learning non assomiglia a elenchi, ma a grafici. Ad esempio, possono avere più
ingressi o più uscite. È per questo tipo di modelli che l'API Funzionale brilla
davvero.
Supponiamo che stiate costruendo un sistema per classificare i ticket di assistenza
clienti in base alla priorità e indirizzarli al reparto appropriato. Il modello ha tre input:
◾ Il titolo del biglietto (immissione di testo)
◾ Il corpo del testo del biglietto (input di testo)
◾ Eventuali tag aggiunti dall'utente (input categorico, che qui si presume sia
codificato a un solo punto)
Possiamo codificare gli input di testo come array di uno e zero di dimensione
vocabolario_dimensioni
(si veda il capitolo 11 per informazioni dettagliate sulle tecniche di codifica del testo).
Il modello ha anche due uscite:
◾ Il punteggio di priorità del ticket, uno scalare compreso tra 0 e 1 (output sigmoide)
◾ Il reparto che dovrebbe gestire il ticket (un softmax sull'insieme dei reparti)

È possibile costruire questo modello in poche righe con l'API funzionale.

Listato 7.9 Un modello funzionale a più ingressi e più uscite

vocabolario_dimensioni
= 10000 Combina le caratteristiche in
num_tags = 100 ingresso in un unico
num_dipartimenti = 4 tensore, le caratteristiche,
concatenandole.
Defini title = keras.Input(shape=(vocabulary_size,), name="title")
re gli text_body = keras.Input(shape=(vocabulary_size,), name="text_body")
input tags = keras.Input(shape=(num_tags,), name="tags")
del
model features = layers.Concatenate()([title, text_body, tags])
lo. features = layers.Dense(64, activation="relu")(features)

priority = layers.Dense(1, activation="sigmoid", name="priority")(features)


department = layers.Dense(
num_dipartimenti, attivazione="softmax", nome="dipartimento")(caratteristiche)
Definir
e le
uscite
del
modell
o.
uscite=[priorità, reparto])
model = keras.Model(inputs=[title, text_body, tags],
Diversi modi per costruire modelli 179
Keras

Creare il modello specificando gli ingressi e le uscite.


Applicare un livello
intermedio per
ricombinare le
caratteristiche in
ingresso in
rappresentazioni più
ricche.
180 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
L'API funzionale è un modo semplice, simile ai LEGO, ma molto flessibile per definire
grafici arbitrari di livelli come questi.
FORMAZIONE DI UN MODELLO MULTI-INPUT, MULTI-OUTPUT
Si può addestrare il modello nello stesso modo in cui si addestra un modello
sequenziale, chiamando fit() con elenchi di dati di input e output. Questi elenchi
di dati devono essere nello stesso ordine degli input passati al costruttore del
modello.

Listato 7.10 Addestramento di un modello fornendo liste di array di input e di target

importare numpy come np

num_samples = 1280

Dati di title_data = np.random.randint(0, 2, size=(num_samples, vocabulary_size))


ingre text_body_data = np.random.randint(0, 2, size=(num_samples, vocabulary_size)
sso tags_data = np.random.randint(0, 2, size=(num_samples, num_tags))
fittizi
priority_data = np.random.random(size=(num_samples, 1))
Dati di department_data = np.random.randint(0, 2, size=(num_samples, num_departments))
destinazio
ne fittizi model.compile(optimizer="rmsprop",
loss=["mean_squared_error", "categorical_crossentropy"],
metrics=["mean_absolute_error"], ["accuracy"]])
model.fit([title_data, text_body_data, tags_data],
[priority_data, department_data],
epochs=1)
model.evaluate([title_data, text_body_data, tags_data],
[priority_data, department_data])
priority_preds, department_preds = model.predict(
[title_data, text_body_data, tags_data])

Se non si vuole fare affidamento sull'ordine degli input (ad esempio, perché si
hanno molti input o output), si possono anche sfruttare i nomi dati agli oggetti
Input e ai livelli di output e passare i dati tramite dizionari.

Listato 7.11 Addestramento di un modello fornendo i detti degli array di input e di target

model.compile(optimizer="rmsprop",
loss={"priorità": "mean_squared_error", "department":
"categorical_crossentropy"},
metriche={"priorità": ["errore_assoluto_medio"], "dipartimento":
["accuracy"]})
model.fit({"title": title_data, "text_body": text_body_data,
"tags": tags_data},
{"priorità": priority_data, "reparto": department_data},
epochs=1)
model.evaluate({"title": title_data, "text_body": text_body_data,
"tags": tags_data},
{"priorità": priority_data, "dipartimento": department_data})
priority_preds, department_preds = model.predict(
{"title": title_data, "text_body": text_body_data, "tags": tags_data})
Diversi modi per costruire modelli 181
Keras
LA POTENZA DELL'API FUNZIONALE: ACCESSO ALLA CONNETTIVITÀ DI LIVELLO
Un modello funzionale è una struttura di dati a grafo esplicita. In questo modo è
possibile ispezionare il modo in cui gli strati sono collegati e riutilizzare i nodi
precedenti del grafo (che sono le uscite degli strati) come parte di nuovi modelli. Inoltre,
si adatta perfettamente al "modello mentale" che la maggior parte dei ricercatori utilizza
quando pensa a una rete neurale profonda: un grafo di strati. Ciò consente due
importanti casi d'uso: la visualizzazione del modello e l'estrazione di caratteristiche.
Visualizziamo la connettività del modello appena definito (la topologia del
modello). È possibile tracciare un modello funzionale come un grafico con l'utilità
plot_model() (vedere figura 7.2).

keras.utils.plot_model(model, "ticket_classifier.png")

Figura 7.2 Grafico generato


da plot_model() sul
nostro modello di
classificatore di biglietti

È possibile aggiungere a questo grafico le forme di ingresso e di uscita di ogni livello


del modello, che possono essere utili durante il debug (vedere la figura 7.3).

keras.utils.plot_model(
model, "ticket_classifier_with_shape_info.png", show_shapes=True)

Figura 7.3 Grafico del modello con l'aggiunta delle informazioni sulla forma
182 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
Il "None" nelle forme del tensore rappresenta la dimensione del lotto: questo
modello consente lotti di qualsiasi dimensione.
L'accesso alla connettività dei livelli significa anche che è possibile ispezionare e
riutilizzare i singoli nodi (chiamate di livello) nel grafo. La proprietà model.layers
fornisce l'elenco dei livelli che compongono il modello e per ogni livello è possibile
interrogare layer.input e layer.output.

Listato 7.12 Recuperare gli ingressi o le uscite di un livello in un modello funzionale

>>> model.layers
[<tensorflow.python.keras.engine.input_layer.InputLayer at 0x7fa963f9d358>,
<tensorflow.python.keras.engine.input_layer.InputLayer at 0x7fa963f9d2e8>,
<tensorflow.python.keras.engine.input_layer.InputLayer at 0x7fa963f9d470>,
<tensorflow.python.keras.layers.merge.Concatenate at 0x7fa963f9d860>,
<tensorflow.python.keras.layers.core.Dense at 0x7fa964074390>,
<tensorflow.python.keras.layers.core.Dense at 0x7fa963f9d898>,
<tensorflow.python.keras.layers.core.Dense at 0x7fa963f95470>]
>>> model.layers[3].input
[<tf.Tensor "title:0" shape=(None, 10000) dtype=float32>,
<tf.Tensor "text_body:0" shape=(None, 10000) dtype=float32>,
<tf.Tensor "tags:0" shape=(None, 100) dtype=float32>]
>>> model.layers[3].output
<tf.Tensor "concatenate/concat:0" shape=(None, 20100) dtype=float32>

Ciò consente di effettuare l'estrazione di caratteristiche, creando modelli che


riutilizzano le caratteristiche intermedie di un altro modello.
Supponiamo di voler aggiungere un altro output al modello precedente: vogliamo
stimare quanto tempo ci vorrà per risolvere un determinato ticket, una sorta di
valutazione della difficoltà. Si potrebbe fare questo tramite un livello di classificazione
su tre categorie: "veloce", "medio" e "difficile". Non è necessario ricreare e riqualificare
un modello da zero. Si può partire dalle caratteristiche intermedie del modello
precedente, dato che si ha accesso ad esse, in questo modo.

Listato 7.13 Creare un nuovo modello riutilizzando gli output dei livelli intermedi
strati[4] è il nostro intermedio
caratteristiche = model.layers[4].output Strato denso
difficulty = layers.Dense(3, activation="softmax", name="difficulty")(features)

new_model = keras.Model(
inputs=[title, text_body, tags],
output=[priorità, reparto, difficoltà])

Tracciamo il nostro nuovo modello (vedi figura 7.4):

keras.utils.plot_model(
new_model, "updated_ticket_classifier.png", show_shapes=True)
Diversi modi per costruire modelli 183
Keras

Figura 7.4 Grafico del nostro nuovo modello

7.2.3 Sottoclasse della classe Model


L'ultimo modello di costruzione dei modelli da conoscere è quello più avanzato: La
sottoclasse del modello. Nel capitolo 3 abbiamo imparato a sottoclassare la classe
Layer per creare livelli personalizzati. La sottoclasse di Model è abbastanza simile:
◾ Nel metodo init (), definire i livelli che il modello utilizzerà.
◾ Nel metodo call(), si definisce il passaggio in avanti del modello,
riutilizzando i livelli precedentemente creati.
◾ Istanziare la propria sottoclasse e richiamarla sui dati per creare i pesi.

RISCRITTURA DELL'ESEMPIO PRECEDENTE COME MODELLO SOTTOCLASSIFICATO


Vediamo un semplice esempio: reimplementeremo il modello di gestione dei ticket
dell'assistenza clienti utilizzando una sottoclasse di Model.

Listato 7.14 Un semplice modello sottoclassificato

classe CustomerTicketModel(keras.Model):
Non
def init (self, num_departments): dimenticate di
super(). init () chiamare il
costruttore
super()!
self.concat_layer = layers.Concatenate() Definire i
self.mixing_layer = layers.Dense(64, activation="relu") sottolivelli
self.priority_scorer = layers.Dense(1, nel file
activation="sigmoid") self.department_classifier = costruttore.
layers.Dense(
num_dipartimenti, attivazione="softmax")

def call(self, inputs):


Definire il
title = inputs["title"] passaggio in
text_body = input["text_body"] avanti nel
tags = input["tags"] metodo call().

features = self.concat_layer([title, text_body, tags])


features = self.mixing_layer(features)
184 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
priorità = self.priority_scorer(caratteristiche)
reparto =
self.department_classifier(caratteristiche) return
priorità, reparto

Una volta definito il modello, è possibile istanziarlo. Si noti che il modello creerà i suoi
pesi solo la prima volta che viene chiamato su alcuni dati, come le sottoclassi di Layer:

model = CustomerTicketModel(num_departments=4)

priority, department = model(


{"title": title_data, "text_body": text_body_data, "tags": tags_data})

Fin qui, tutto sembra molto simile alla sottoclasse di Layer, un flusso di lavoro che
abbiamo incontrato nel capitolo 3. Qual è la differenza tra una sottoclasse di Layer e
una sottoclasse di Model? È semplice: un "livello" è un blocco di costruzione che si usa
per creare modelli, mentre un "modello" è l'oggetto di livello superiore che verrà
effettivamente addestrato, esportato per l'inferenza, ecc. In breve, un modello ha i
metodi fit(), evaluate() e predict(). I livelli no. A parte questo, le due classi
sono praticamente identiche. (Un'altra differenza è la possibilità di salvare un modello
in un file su disco, di cui parleremo tra qualche paragrafo).
È possibile compilare e addestrare una sottoclasse Model proprio come una sottoclasse
Sequential o Functional.
modello: La struttura di ciò che viene passato come
argomento di perdita e metrica deve
model.compile(optimizer="rmsprop", corrispondere esattamente a ciò che viene
restituito da call()- in questo caso, un elenco di
due elementi.
loss=["mean_squared_error", "categorical_crossentropy"],
metrics=["mean_absolute_error"], ["accuracy"]])
model.fit({"title": title_data,
"text_body": text_body_data,
"tags": tags_data},
[dati_priorità, dati_reparto], La struttura dell'obiettivo
epochs=1) deve corrispondere esattamente
model.evaluate({"title": title_data, a ciò che viene restituito dal
"text_body": text_body_data, metodo call(), in questo caso
"tags": tags_data}, un elenco di due elementi.
[dati_priorità, dati_reparto])
priority_preds, department_preds = model.predict({"title": title_data,
"text_body": text_body_data,
La struttura dei dati di input deve "tags": tags_data})
corrispondere esattamente a quella prevista
dal metodo call(): in questo caso, un dict con le
chiavi title, text_body e tag.

Il flusso di sottoclassi di Model è il modo più flessibile per costruire un modello.


Permette di costruire modelli che non possono essere espressi come grafi aciclici diretti
di livelli: immaginate, per esempio, un modello in cui il metodo call() utilizza i livelli
all'interno di un ciclo for, o addirittura li chiama in modo ricorsivo. Tutto è possibile:
siete voi a comandare.
ATTENZIONE: COSA NON SUPPORTANO I MODELLI SOTTOCLASSATI
Questa libertà ha un costo: con i modelli sottoclasse, si è responsabili di una maggiore
Diversi modi per costruire modelli 185
Keras

quantità di logica del modello, il che significa che la superficie di errore potenziale è
molto più ampia. Di conseguenza, dovrete fare più lavoro di debug. Si sta sviluppando
un nuovo oggetto Python, non solo unendo i mattoncini LEGO.
186 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
Anche i modelli funzionali e quelli sottoclassificati hanno una natura
sostanzialmente diversa. Un modello funzionale è una struttura di dati esplicita, un grafo
di livelli che è possibile visualizzare, ispezionare e modificare. Un modello sottoclassato
è un pezzo di bytecode, una classe Python con un metodo call() che contiene codice
grezzo. Questa è la fonte della flessibilità del flusso di lavoro di sottoclasse: si può
codificare qualsiasi funzionalità si desideri, ma introduce nuove limitazioni. Ad
esempio, poiché il modo in cui i livelli sono collegati tra loro è nascosto nel corpo del
metodo call(), non è possibile accedere a tali informazioni. Chiamando sum() non
si visualizza la connettività dei livelli e non si può tracciare la topologia del modello con
plot_model(). Allo stesso modo, se si dispone di un modello sottoclassificato, non è
possibile accedere ai nodi del grafo dei livelli per eseguire l'estrazione delle
caratteristiche, perché semplicemente non esiste un grafo. Una volta
il modello viene istanziato, il suo passaggio in avanti diventa una scatola nera completa.

7.2.4 Mescolare e abbinare diversi componenti


In particolare, la scelta di uno di questi modelli - il modello sequenziale, l'API
funzionale o il sottoclasse del modello - non esclude gli altri. Tutti i modelli dell'API
di Keras possono interagire senza problemi tra loro, sia che si tratti di modelli
sequenziali, di modelli funzionali o di modelli sottoclassificati scritti da zero. Fanno
tutti parte dello stesso spettro di flussi di lavoro.
Ad esempio, è possibile utilizzare un livello o un modello sottoclassificato in un modello
funzionale.

Listato 7.15 Creazione di un modello funzionale che include un modello sottoclasse

classe Classificatore(keras.Model):

def init (self, num_classes=2):


super(). init ()
se num_classi == 2:
num_unità = 1
attivazione =
"sigmoide"
altro:
num_unità = num_classi
attivazione = "softmax"
self.dense = layers.Dense(num_units, activation=activation)

def call(self, input):


restituire self.dense(input)

input = keras.Input(shape=(3,))
features = layers.Dense(64, activation="relu")(input)
outputs = Classifier(num_classes=10)(features)
model = keras.Model(input=input, output=output)

Al contrario, è possibile utilizzare un modello funzionale come parte di un livello o modello


sottoclassificato.

Listato 7.16 Creazione di un modello sottoclasse che include un modello funzionale

input = keras.Input(shape=(64,))
outputs = layers.Dense(1, activation="sigmoid")(inputs)
Diversi modi per costruire modelli 187
Keras
binary_classifier = keras.Model(inputs=inputs, outputs=outputs)
Utilizzo di cicli di formazione e valutazione 185
integrati
classe MyModel(keras.Model):

def init (self, num_classes=2):


super(). init ()
self.dense = layers.Dense(64, activation="relu")
self.classifier = binary_classifier

def call(self, input):


caratteristiche = self.dense(input)
return self.classifier(features)

model = MyModel()

7.2.5 Ricordate: Utilizzare lo strumento giusto per il lavoro


Avete imparato a conoscere la gamma di flussi di lavoro per la costruzione di
modelli Keras, dal flusso più semplice, il modello sequenziale, a quello più avanzato,
la sottoclasse del modello. Quando si dovrebbe usare uno piuttosto che l'altro?
Ognuno di essi ha i suoi pro e i suoi contro: scegliete quello più adatto al lavoro da
svolgere.
In generale, l'API funzionale offre un buon compromesso tra facilità d'uso e
flessibilità. Inoltre, consente l'accesso diretto alla connettività dei livelli, che è molto
potente per casi d'uso come il plottaggio del modello o l'estrazione di caratteristiche. Se
si può usare l'API funzionale, cioè se il modello può essere espresso come un grafo
aciclico diretto di livelli, si consiglia di usarla rispetto alla sottoclassificazione del
modello.
In futuro, tutti gli esempi di questo libro utilizzeranno l'API funzionale,
semplicemente perché tutti i modelli con cui lavoreremo sono esprimibili come grafi di
livelli. Tuttavia, faremo spesso uso di livelli sottoclassificati. In generale, l'uso di
modelli funzionali che includono livelli sottoclassati offre il meglio dei due mondi:
un'elevata flessibilità di sviluppo, pur mantenendo i vantaggi dell'API funzionale.

7.3 Utilizzo di cicli di formazione e valutazione integrati


Il principio della divulgazione progressiva della complessità, ovvero l'accesso a uno
spettro di flussi di lavoro che vanno dalla semplicità assoluta alla flessibilità arbitraria,
un passo alla volta, si applica anche all'addestramento dei modelli. Keras offre diversi
flussi di lavoro per l'addestramento dei modelli. Possono essere semplici come la
chiamata a fit() sui dati o avanzati come la scrittura di un nuovo algoritmo di
addestramento da zero.
Conoscete già il flusso di lavoro di compile(), fit(), evaluate(), predict().
Come promemoria, date un'occhiata al seguente elenco.

Listato 7.17 Il flusso di lavoro standard: compile(), fit(), evaluate(), predict()

da tensorflow.keras.datasets importa mnist Creare un modello (lo


inseriamo in una funzione
def get_mnist_model(): separata per poterlo
input = keras.Input(shape=(28 * 28,))
riutilizzare in seguito).
features = layers.Dense(512, activation="relu")(input)
features = layers.Dropout(0.5)(features)
outputs = layers.Dense(10, activation="softmax")(features)
186 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
model = keras.Model(input, output)
Caricare i dati,
restituire il modello riservandone alcuni
per la convalida.
(immagini, etichette), (immagini_di_test, etichette_di_test) =
mnist.load_data() immagini = images.reshape((60000, 28 *
28)).astype("float32") / 255
test_images = test_images.reshape((10000, 28 * 28)).astype("float32") / 255
train_images, val_images = images[10000:], images[:10000]
train_labels, val_labels = labels[10000:], labels[:10000]
Compilare il modello
model = get_mnist_model() specificando il suo
model.compile(optimizer="rmsprop", ottimizzatore, la funzione
loss="sparse_categorical_crossentropy", di perdita da minimizzare
metrics=["accuracy"]) e le metriche da
model.fit(train_images, train_labels, monitorare.
epochs=3,
validation_data=(val_images, val_labels))
test_metrics = model.evaluate(test_images, test_labels) Utilizzare fit() per
predictions = model.predict(test_images)
addestrare il modello,
fornendo
facoltativamente i dati
di validazione per
monitorare le
prestazioni su dati non
visti.
Utilizzare evaluate() Utilizzare predict() per
per calcolare la calcolare le probabilità di
perdita e le metriche classificazione.
sui nuovi dati. su nuovi dati.

È possibile personalizzare questo semplice flusso di lavoro in un paio di modi:


◾ Fornite le vostre metriche personalizzate.
◾ Passare callback al metodo fit() per programmare le azioni da eseguire in
punti specifici durante l'allenamento.
Diamo un'occhiata a questi.

7.3.1 Scrivere le proprie metriche


Le metriche sono fondamentali per misurare le prestazioni del modello, in particolare
per misurare la differenza tra le sue prestazioni sui dati di addestramento e quelle sui
dati di test. Le metriche più comuni per la classificazione e la regressione fanno già
parte del modulo integrato keras.metrics e, nella maggior parte dei casi, sono
quelle che utilizzerete. Ma se fate qualcosa di diverso dal solito, dovrete essere in grado
di scrivere le vostre metriche. È semplice!
Una metrica di Keras è una sottoclasse della classe keras.metrics.Metric. Come i
livelli, una metrica ha uno stato interno memorizzato in variabili TensorFlow. A
differenza dei livelli, queste variabili non vengono aggiornate tramite backpropagation,
quindi è necessario scrivere da soli la logica di aggiornamento dello stato, che avviene
nel metodo update_state().
Ad esempio, ecco una semplice metrica personalizzata che misura l'errore
quadratico medio (RMSE).

Listato 7.18 Implementazione di una metrica personalizzata mediante sottoclasse della classe Metrica
Utilizzo di cicli di formazione e valutazione 187
integrati

importare tensorflow come tf


Sottoclasse della
classe classe Metric.
RootMeanSquaredError(keras.metrics.Metric):
188 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
Definire le def init (self, name="rmse", **kwargs): super().
variabili di stato init (name=nome, **kwargs)
nel costruttore. self.mse_sum = self.add_weight(name="mse_sum", initializer="zeros")
Come per i self.total_samples = self.add_weight(
livelli, si ha name="total_samples", initializer="zeros", dtype="int32")
accesso alla
funzione def update_state(self, y_true, y_pred, sample_weight=None):
add_weight() y_true = tf.one_hot(y_true, depth=tf.shape(y_pred)[1])
metodo. mse = tf.reduce_sum(tf.square(y_true - y_pred))
Per self.mse_sum.assign_add(mse)
corrispondere al num_samples = tf.shape(y_pred)[0]
nostro modello self.total_samples.assign_add(num_samples)
MNIST, ci
aspettiamo Implementare la logica di aggiornamento dello stato in update_state().
previsioni L'argomento y_true è costituito dai target (o etichette) per un lotto,
categoriali ed mentre y_pred rappresenta le previsioni corrispondenti del
etichette intere. modello. È possibile ignorare l'argomento sample_weight, che
non verrà utilizzato in questa sede.

Si utilizza il metodo result() per restituire il valore corrente della metrica:

def result(self):
return tf.sqrt(self.mse_sum / tf.cast(self.total_samples, tf.float32))

Nel frattempo, è necessario esporre un modo per resettare lo stato della metrica
senza doverla ricostituire: ciò consente di utilizzare gli stessi oggetti metrici in
diverse epoche di addestramento o in entrambe le fasi di addestramento e
valutazione. Lo si fa con il metodo reset_state():

def reset_state(self):
self.mse_sum.assign(0.)
self.total_samples.assign(0)

Le metriche personalizzate possono essere utilizzate proprio come quelle integrate. Proviamo la
nostra metrica:

model = get_mnist_model()
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy", RootMeanSquaredError()])
model.fit(train_images, train_labels,
epochs=3,
validation_data=(val_images, val_labels))
test_metrics = model.evaluate(test_images, test_labels)

Ora è possibile vedere la barra di avanzamento di fit() che mostra l'RMSE del modello.

7.3.2 Utilizzo dei callback


Lanciare una sessione di addestramento su un grande set di dati per decine di epoche
utilizzando model.fit() può essere un po' come lanciare un aeroplano di carta: dopo
l'impulso iniziale, non si ha alcun controllo sulla sua traiettoria o sul punto di
atterraggio. Se si vogliono evitare risultati negativi (e quindi lo spreco di aeroplani
di carta), è più intelligente usare, non un aeroplano di carta, ma un drone in grado di
rilevare l'ambiente circostante, inviare dati all'operatore e fare automaticamente
Utilizzo di cicli di formazione e valutazione 189
integrati
decisioni in base al suo stato attuale. L'API delle callback di Keras vi aiuterà a
trasformare la vostra chiamata a model.fit() da un aeroplano di carta in un drone
intelligente e autonomo in grado di auto-introspettarsi e di agire dinamicamente.
Un callback è un oggetto (un'istanza di classe che implementa metodi specifici)
che viene passato al modello nella chiamata a fit() e che viene richiamato dal
modello in vari momenti dell'addestramento. Ha accesso a tutti i dati disponibili
sullo stato del modello e sulle sue prestazioni e può intervenire: interrompere
l'addestramento, salvare un modello, caricare un set di pesi diverso o modificare in
altro modo lo stato del modello.
Ecco alcuni esempi di utilizzo dei callback:
◾ Checkpoint del modello - Salvataggio dello stato corrente del modello in diversi
momenti della formazione.
◾ Arresto anticipato: interrompere l'addestramento quando la perdita di
convalida non migliora più (e ovviamente salvare il modello migliore
ottenuto durante l'addestramento).
◾ Regolazione dinamica del valore di alcuni parametri durante l'addestramento, ad
esempio il tasso di apprendimento dell'ottimizzatore.
◾ Registrare le metriche di addestramento e di convalida durante l'addestramento o
visualizzare le rappresentazioni apprese dal modello mentre vengono aggiornate La
barra di avanzamento di fit() che conosciamo bene è in realtà un callback!
Il modulo keras.callbacks include una serie di callback integrati (l'elenco non è
esaustivo):

keras.callbacks.ModelCheckpoint
keras.callbacks.EarlyStopping
keras.callbacks.LearningRateScheduler
keras.callbacks.ReduceLROnPlateau
keras.callbacks.CSVLogger

Esaminiamone due per darvi un'idea di come utilizzarli: EarlyStopping e


Punto di controllo del modello.
I CALLBACK EARLYSTOPPING E MODELCHECKPOINT
Quando si addestra un modello, ci sono molte cose che non si possono prevedere fin
dall'inizio. In particolare, non si può sapere quante epoche saranno necessarie per
raggiungere una perdita di valore ottimale. I nostri esempi finora hanno adottato la
strategia di allenarsi per un numero di epoch sufficiente ad iniziare l'overfitting,
utilizzare la prima sessione per capire il numero corretto di epoch da allenare e infine
lanciare una nuova sessione di allenamento da zero utilizzando questo numero ottimale.
Naturalmente, questo approccio è dispendioso. Un modo migliore per gestire questo
problema è interrompere l'addestramento quando si misura che la perdita di convalida
non migliora più. Questo si può ottenere utilizzando il callback EarlyStopping.
Il callback EarlyStopping interrompe l'addestramento quando una metrica target
monitorata ha smesso di migliorare per un numero fisso di epoche. Ad esempio, questo
callback consente di interrompere l'addestramento non appena inizia l'overfitting,
evitando così di dover riqualificare il modello per un numero inferiore di epoche.
Questo callback è tipicamente usato in
190 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
in combinazione con ModelCheckpoint, che consente di salvare continuamente il
modello durante l'addestramento (e, facoltativamente, di salvare solo il miglior
modello attuale: la versione del modello che ha ottenuto le migliori prestazioni alla
fine di un'epoca).

Listato 7.19 Utilizzo dell'argomento callback nel metodo fit()

I callback vengono passati al modello Interrompe


tramite l'argomento callback in fit(), che l'allenamento quando il
accetta un elenco di callback. È possibile miglioramento si
passare un numero qualsiasi di callback. interrompe
callbacks_list = [
keras.callbacks.EarlyStopping(
Monitorare
l'accuratezza della
convalida del modello
Salva i monitor="val_accuracy", Interrompe
pesi correnti pazienza=2, l'addestramento
dopo ), quando la precisione ha
smesso di migliorare per
due epoche.
ogni epoca keras.callbacks.ModelCheckpoint(
filepath="checkpoint_path.keras",
Percorso
del file del monitor="val_loss", Questi due argomenti significano che
modello save_best_only=True, non si sovrascriverà il file del modello a
di ) meno che val_loss non sia migliorato, il
destinazion che consente di mantenere il modello
e migliore visto durante l'allenamento.

]
model = get_mnist_model()
model.compile(optimizer="rmsprop", L'accuratezza viene
loss="sparse_categorical_crossentropy", monitorata, quindi
metrics=["accuracy"]) dovrebbe far parte
model.fit(train_images, train_labels,
delle metriche del
modello.
epochs=10,
callbacks=callbacks_list, Si noti che, poiché il callback
validation_data=(val_images, val_labels)) monitorerà la perdita di
convalida e la
per ottenere l'accuratezza della convalida, è
necessario passare validation_data alla
chiamata a fit().

Si noti che è sempre possibile salvare i modelli manualmente dopo l'addestramento, basta
richiamare il comando
model.save('my_checkpoint_path'). Per ricaricare il modello salvato, basta usare

model = keras.models.load_model("checkpoint_path.keras")

7.3.3 Scrivere i propri callback


Se durante l'allenamento è necessario eseguire un'azione specifica che non è coperta
da uno dei callback integrati, è possibile scrivere il proprio callback. I callback sono
implementati mediante una sottoclasse della classe keras.callbacks.Callback. Si
può quindi implementare un numero qualsiasi dei seguenti metodi con nome trasparente,
che vengono richiamati in vari momenti dell'addestramento:
Richiamato ogni epoca
all'inizio di on_epoch_begin(epoch, logs)
Utilizzo di cicli di formazione e valutazione 191
integrati

on_epoch_end(epoch, logs)
on_batch_begin(batch, logs) Chiamato alla fine
on_batch_end(batch, logs)
di ogni epoca
on_train_begin(logs) Richiamato subito prima
on_train_end(logs) dell'elaborazione di ogni lotto

Richiamato subito
dopo l'elaborazione di
ogni lotto
Chiamata alla fine Chiamata
di formazione all'inizio della
formazione
192 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
Tutti questi metodi sono chiamati con un argomento log, che è un dizionario
contenente informazioni sul batch, sull'epoch o sull'allenamento precedente, sulle
metriche di allenamento e di validazione e così via. Anche i metodi on_epoch_* e
on_batch_* hanno come primo argomento (un intero) l'indice dell'epoca o del batch.
Ecco un semplice esempio che salva un elenco di valori di perdita per lotto
durante l'addestramento e salva un grafico di questi valori alla fine di ogni epoca.

Listato 7.20 Creare un callback personalizzato sottoclassando la classe Callback

da matplotlib import pyplot come plt

classe LossHistory(keras.callbacks.Callback):
def on_train_begin(self, logs):
self.per_batch_losses = []

def on_batch_end(self, batch, logs):


self.per_batch_losses.append(logs.get("loss"))

def on_epoch_end(self, epoch, logs):


plt.clf()
plt.plot(range(len(self.per_batch_losses)), self.per_batch_losses,
label="Perdita di allenamento per ogni batch")
plt.xlabel(f "Lotto (epoch {epoch})")
plt.ylabel("Perdita")
plt.legend() plt.savefig(f
"plot_at_epoch_{epoch}")
self.per_batch_losses = []

Facciamo una prova:

model = get_mnist_model()
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
model.fit(train_images, train_labels,
epochs=10,
callbacks=[LossHistory()],
validation_data=(val_images, val_labels))

Si ottengono diagrammi simili a quelli della figura 7.5.

7.3.4 Monitoraggio e visualizzazione con TensorBoard


Per fare una buona ricerca o sviluppare buoni modelli, è necessario un feedback
ricco e frequente su ciò che accade all'interno dei modelli durante gli esperimenti.
Questo è lo scopo degli esperimenti: ottenere informazioni sulle prestazioni di un
modello, il più p o s s i b i l e . Il progresso è un processo iterativo, un ciclo: si parte da
un'idea e la si esprime come esperimento, cercando di convalidare o invalidare la
propria idea. Si esegue l'esperimento e si elaborano le informazioni che genera.
Questo ispira l'idea successiva. Più iterazioni di questo ciclo si riescono a eseguire,
più l'idea si affina e si trasforma in un'altra.
Utilizzo di cicli di formazione e valutazione 193
integrati

Figura 7.5 L'output del callback personalizzato per il tracciamento della cronologia

potenti le vostre idee. Keras vi aiuta a passare dall'idea all'esperimento nel minor
tempo possibile e le GPU veloci possono aiutarvi a passare dall'esperimento al
risultato il più rapidamente possibile. Ma che dire dell'elaborazione dei risultati
dell'esperimento? È qui che entra in gioco Tensor Board (vedi figura 7.6).

Idea

Visualizzazio Apprendiment
ne o profondo
framework: framework:
TensorBoard Keras

Risultati Esperiment
o
GPU, TPU
Figura 7.6 Il ciclo del progresso

TensorBoard (www.tensorflow.org/tensorboard) è un'applicazione basata su browser


c h e può essere eseguita localmente. È il modo migliore per monitorare tutto ciò che
accade all'interno del modello durante l'addestramento. Con TensorBoard è possibile
◾ Monitoraggio visivo delle metriche durante la formazione
◾ Visualizzare l'architettura del modello
◾ Visualizzare gli istogrammi delle attivazioni e dei gradienti
◾ Esplora le incorporazioni in 3D

Se si monitorano più informazioni rispetto alla sola perdita finale del modello, si
può sviluppare una visione più chiara di ciò che il modello fa e non fa, e si possono
fare progressi più rapidamente.
194 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
Il modo più semplice per usare TensorBoard con un modello Keras e il metodo fit()
è usare il callback keras.callbacks.TensorBoard.
Nel c a s o più semplice, basta specificare dove si vuole che il callback scriva i log e il
gioco è fatto:

model = get_mnist_model()
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])

tensorboard = keras.callbacks.TensorBoard(
log_dir="/full_path_to_your_log_dir",
)
model.fit(train_images, train_labels,
epochs=10,
validation_data=(val_images, val_labels),
callbacks=[tensorboard])

Una volta avviata l'esecuzione, il modello scriverà i log nella posizione di


destinazione. Se si esegue lo script Python su una macchina locale, è possibile
lanciare il server Tensor Board locale con il seguente comando (si noti che
l'eseguibile tensorboard dovrebbe essere già disponibile se si è installato
TensorFlow tramite pip; in caso contrario, è possibile installare TensorBoard
manualmente tramite pip install tensorboard):

tensorboard --logdir /full_path_to_your_log_dir

È quindi possibile navigare nell'URL restituito dal comando per accedere


all'interfaccia di TensorBoard.
Se si esegue lo script in un blocco note Colab, è possibile eseguire un'istanza di Ten-
sorBoard incorporata come parte del blocco note, utilizzando i seguenti comandi:

%load_ext tensorboard
%tensorboard --logdir /full_path_to_your_log_dir

Nell'interfaccia di TensorBoard è possibile monitorare i grafici in tempo reale delle


metriche di addestramento e di valutazione (vedi figura 7.7).

7.4 Scrivere i propri cicli di formazione e valutazione


Il flusso di lavoro fit() rappresenta un buon equilibrio tra facilità d'uso e flessibilità.
È quello che si userà la maggior parte delle volte. Tuttavia, non è pensato per supportare
tutto ciò che un ricercatore di deep learning può voler fare, anche con metriche
personalizzate, perdite personalizzate e callback del cliente.
Dopo tutto, il flusso di lavoro integrato fit() è incentrato esclusivamente
sull'apprendimento supervisionato: una configurazione in cui ci sono obiettivi noti
(chiamati anche etichette o annotazioni) associati ai dati di input e in cui si calcola la
perdita in funzione di questi obiettivi e delle previsioni del modello. Tuttavia, non tutte
le forme di apprendimento automatico rientrano in questa categoria.
Scrivere i propri cicli di formazione e 193
valutazione

Figura 7.7 TensorBoard può essere utilizzato per monitorare facilmente le metriche di formazione e valutazione.

categoria. Esistono altre configurazioni in cui non sono presenti obiettivi espliciti, come
l'apprendimento generazionale (di cui parleremo nel capitolo 12), l'apprendimento auto-
supervisionato (in cui gli obiettivi sono ottenuti dagli input) e l'apprendimento per
rinforzo (in cui l'apprendimento è guidato da "ricompense" occasionali, come
l'addestramento di un cane). Anche se si sta facendo un normale apprendimento
supervisionato, come ricercatore si potrebbe voler aggiungere qualche novità che
richiede una flessibilità di basso livello.
Ogni volta che ci si trova in una situazione in cui la funzione integrata fit()
non è sufficiente, è necessario scrivere la propria logica di allenamento
personalizzata. Nei capitoli 2 e 3 abbiamo già visto semplici esempi di cicli di
addestramento di basso livello. Come promemoria, il contenuto di un tipico ciclo di
addestramento è simile a questo:
1 Eseguire il passaggio in avanti (calcolare l'output del modello) all'interno di un
nastro di gradienti per ottenere un valore di perdita per il lotto di dati corrente.
2 Recuperare i gradienti della perdita rispetto ai pesi del modello.
3 Aggiornare i pesi del modello in modo da ridurre il valore di perdita sul lotto di
dati corrente.
194 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
Questi passaggi vengono ripetuti per tutti i lotti necessari. Questo è essenzialmente ciò
che fit() fa sotto il cofano. In questa sezione imparerete a reimplementare fit()
da zero, il che vi fornirà tutte le conoscenze necessarie per scrivere qualsiasi algoritmo
di addestramento che vi venga in mente.
Esaminiamo i dettagli.

7.4.1 Formazione e inferenza


Negli esempi di ciclo di addestramento a basso livello visti finora, il passo 1 (il
passaggio in avanti) è stato eseguito tramite predictions = model(inputs) e il
passo 2 (recupero dei gradienti calcolati dal nastro dei gradienti) è stato eseguito tramite
gradients = tape.gradient(loss, model.weights). Nel caso generale, ci
sono in realtà due sottigliezze da tenere in considerazione.
Alcuni livelli di Keras, come il livello Dropout, hanno comportamenti diversi durante
l'addestramento e durante l'inferenza (quando li si usa per generare previsioni). Tali
livelli espongono un argomento booleano di addestramento nel loro metodo call().
Chiamando dropout(inputs, training=True) si eliminano alcune voci di
attivazione, mentre chiamando dropout(inputs, training=False) non si ottiene
nulla. Per estensione, anche i modelli Funzionali e Sequenziali espongono questo
argomento di addestramento nei loro metodi call(). Ricordarsi di passare
l'addestramento
=True quando si chiama un modello Keras durante il passaggio in avanti! Il nostro
passaggio in avanti diventa quindi predictions = model(inputs, training=True).
Inoltre, si noti che quando si recuperano i gradienti dei pesi del modello, non si
deve usare tape.gradients(loss, model.weights), ma piuttosto tape
.gradients(loss, model.trainable_weights). In effetti, i livelli e i modelli possiedono due
tipi di pesi:
◾ Pesi addestrabili: sono destinati a essere aggiornati tramite backpropagation per
minimizzare la perdita del modello, come il kernel e il bias di uno strato denso.
◾ Pesi non addestrabili: sono destinati a essere aggiornati durante il passaggio in
avanti dai livelli che li possiedono. Per esempio, se si desidera che un livello
personalizzato mantenga un contatore di quanti lotti ha elaborato finora, questa
informazione sarà memorizzata in un peso non addestrabile e a ogni lotto il
livello incrementerà il contatore di uno.
Tra i livelli incorporati in Keras, l'unico che presenta pesi non addestrabili è il livello
BatchNormalization, di cui si parlerà nel capitolo 9. Il livello BatchNormalization ha
bisogno di pesi non addestrabili per tenere traccia delle informazioni sulla media e
sulla deviazione standard dei dati che lo attraversano, in modo da eseguire
un'approssimazione online della normalizzazione delle caratteristiche (un concetto
appreso nel capitolo 6).
Tenendo conto di questi due dettagli, una fase di addestramento con
apprendimento supervisionato si presenta come segue:

def train_step(input, target):


con tf.GradientTape() come nastro:
predictions = model(inputs, training=True)
loss = loss_fn(targets, predictions)
Scrivere i propri cicli di formazione e 195
valutazione
gradients = tape.gradients(loss, model.trainable_weights)
optimizer.apply_gradients(zip(model.trainable_weights, gradients))

7.4.2 Utilizzo a basso livello delle metriche


In un ciclo di addestramento di basso livello, probabilmente si vorrà sfruttare le
metriche di Keras (sia quelle personalizzate che quelle integrate). Abbiamo già imparato
a conoscere l'API delle metriche: basta chiamare update_state(y_true, y_pred)
per ogni gruppo di target e previsioni e poi usare result() per interrogare il valore
corrente della metrica:
metric = keras.metrics.SparseCategoricalAccuracy()
targets = [0, 1, 2]
previsioni = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
metric.update_state(targets, predictions)
current_result = metric.result() print(f
"result: {current_result:.2f}")

Potrebbe anche essere necessario tracciare la media di un valore scalare, come la perdita
del modello. È possibile farlo tramite la metrica keras.metrics.Mean:

values = [0, 1, 2, 3, 4]
mean_tracker = keras.metrics.Mean()
for value in values:
mean_tracker.update_state(valore)
print(f "Media dei valori: {mean_tracker.result():.2f}")

Ricordarsi di usare metric.reset_state() quando si v o g l i o n o resettare i risultati


correnti (all'inizio di un'epoca di addestramento o all'inizio della valutazione).

7.4.3 Un ciclo completo di formazione e valutazione


Combiniamo il passaggio in avanti, il passaggio all'indietro e il tracciamento delle
metriche in una funzione di addestramento simile a fit() che prende un gruppo di
dati e obiettivi e restituisce i log che verrebbero visualizzati dalla barra di avanzamento
di fit().

Listato 7.21 Scrittura di un ciclo di addestramento passo-passo: la funzione training step

modello = Preparare la perdita Preparare


get_mnist_model() funzione. l'ottimizzat
ore.
loss_fn = keras.losses.SparseCategoricalCrossentropy()
Preparare l'elenco
optimizer = keras.optimizers.RMSprop()
delle metriche da
metrics = [keras.metrics.SparseCategoricalAccuracy()] monitorare.
loss_tracking_metric = keras.metrics.Mean()
Preparare un tracker della metrica
media per tenere traccia della
def train_step(input, target): media delle perdite.
con tf.GradientTape() come nastro:
predictions = model(inputs, training=True)
loss = loss_fn(targets, predictions)
Eseguire il passaggio in avanti.
Si noti che si passa
training=True.
gradienti = tape.gradient(loss, model.trainable_weights)
optimizer.apply_gradients(zip(gradienti, model.trainable_weights))
Eseguire il passaggio all'indietro. Si
196 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda

noti che si utilizza


model.trainable_weights.
Scrivere i propri cicli di formazione e 197
valutazione
logs = {}
per metrica in metrica: Tenere
metrica.update_state(target, predictions) traccia
logs[metric.name] = metric.result() delle
metriche.
loss_tracking_metric.update_state(loss)
log["loss"] = loss_tracking_metric.result()
Tenere traccia della
media delle
perdite.
restituire Restituisce i valori correnti
i log
delle metriche e della
perdita.
Dovremo reimpostare lo stato delle nostre metriche all'inizio di ogni epoca e prima di
eseguire la valutazione. Ecco una funzione di utilità per farlo.

Listato 7.22 Scrittura di un ciclo di addestramento passo-passo: azzeramento delle metriche

def reset_metrics():
per metrica in metriche:
metrica.reset_state()
loss_tracking_metric.reset_state()

A questo punto, possiamo definire il nostro ciclo di addestramento completo. Si noti che
usiamo un oggetto tf.data.Dataset per trasformare i nostri dati NumPy in un
iteratore che itera sui dati in batch di dimensione 32.

Listato 7.23 Scrittura di un ciclo di addestramento passo-passo: il ciclo stesso

training_dataset = tf.data.Dataset.from_tensor_slices(
(train_images, train_labels))
training_dataset = training_dataset.batch(32)
epochs = 3
per epoch in range(epochs):
reset_metrics()
per input_batch, target_batch in training_dataset: logs
= train_step(input_batch, target_batch)
print(f "Risultati alla fine dell'epoca {epoch}")
per chiave, valore in logs.items():
print(f"...{chiave}: {valore:.4f}")

Ed ecco il ciclo di valutazione: un semplice ciclo for che richiama ripetutamente una
funzione test_step(), che elabora un singolo lotto di dati. La funzione
test_step() è solo un sottoinsieme della logica di train_step(). Omette il
codice che si occupa dell'aggiornamento dei pesi del modello, cioè tutto ciò che
coinvolge il GradientTape e l'ottimizzatore.

Listato 7.24 Scrittura di un ciclo di valutazione passo-passo


logs["v
def test_step(input, target):
al_" +
predictions = model(inputs, training=False)
metric.
loss = loss_fn(targets, predictions)
name] =
metrica
logs = {}
.result
per metrica in metrica:
()
metrica.update_state(target, predictions)
198 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda

Si noti che si passa training=False.


Scrivere i propri cicli di formazione e 199
valutazione
loss_tracking_metric.update_state(loss)
logs["val_loss"] = loss_tracking_metric.result()
return logs

val_dataset = tf.data.Dataset.from_tensor_slices((val_images, val_labels))


val_dataset = val_dataset.batch(32)
reset_metrics()
per input_batch, target_batch in val_dataset:
logs = test_step(input_batch, target_batch)
print("Risultati della valutazione:")
per chiave, valore in logs.items():
print(f"...{chiave}: {valore:.4f}")

Congratulazioni: avete appena reimplementato fit() e evaluate()! O quasi:


fit() e evaluate() supportano molte più funzioni, tra cui la comu- nicazione
distribuita su larga scala, che richiede un po' più di lavoro. Include anche diverse
ottimizzazioni delle prestazioni.
Vediamo una di queste ottimizzazioni: La compilazione delle funzioni di TensorFlow.

7.4.4 Rendetelo veloce con tf.function


Potreste aver notato che i vostri cicli personalizzati vengono eseguiti in modo
significativamente più lento rispetto alle funzioni integrate fit() e evaluate(),
nonostante implementino essenzialmente la stessa logica. Ciò è dovuto al fatto che,
per impostazione predefinita, il codice di TensorFlow viene eseguito riga per riga, in modo
avido, proprio come il codice NumPy o il normale codice Python. L'esecuzione
immediata facilita il debug del codice, ma è tutt'altro che ottimale dal punto di vista
delle prestazioni.
È più performante compilare il codice TensorFlow in un grafo di calcolo che può
essere ottimizzato globalmente come non può fare il codice interpretato riga per riga. Il
metodo per farlo è molto semplice: è sufficiente aggiungere un @tf.function a
qualsiasi funzione che si desidera compilare prima dell'esecuzione, come mostrato nel
seguente elenco.

Listato 7.25 Aggiunta di un decoratore @tf.function alla nostra funzione evaluation-step

@tf.function
Questa è
def test_step(input, target): l'unica linea
predictions = model(inputs, che è
training=False) loss = loss_fn(targets, cambiata.
predictions)

logs = {}
per metrica in metrica:
metrica.update_state(target, predictions)
logs["val_" + metric.name] = metrica.result()

loss_tracking_metric.update_state(loss)
logs["val_loss"] = loss_tracking_metric.result()
return logs

val_dataset = tf.data.Dataset.from_tensor_slices((val_images, val_labels))


val_dataset = val_dataset.batch(32)
reset_metrics()
200 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
per input_batch, target_batch in val_dataset:
logs = test_step(input_batch, target_batch)
print("Risultati della valutazione:")
per chiave, valore in logs.items():
print(f"...{chiave}: {valore:.4f}")

Con la CPU Colab, il ciclo di valutazione è passato da 1,80 s a soli 0,8 s. Molto più
veloce!
Ricordare che, mentre si esegue il debug del codice, è preferibile eseguirlo in
modalità eager, senza alcun decoratore @tf.function. In questo modo è più facile
individuare i bug. Una volta che il codice funziona e si vuole renderlo più veloce,
aggiungere un decoratore @tf.function al passo di addestramento e al passo di
valutazione o a qualsiasi altra funzione critica per le prestazioni.

7.4.5 Sfruttare fit() con un ciclo di allenamento personalizzato


Nelle sezioni precedenti, abbiamo scritto il nostro ciclo di addestramento interamente da
zero. In questo modo si ottiene la massima flessibilità, ma si finisce per scrivere molto
codice e contemporaneamente si perdono molte delle comode funzioni di fit(), come
i call back o il supporto integrato per l'addestramento distribuito.
Cosa succede se si ha bisogno di un algoritmo di addestramento personalizzato,
ma si vuole comunque sfruttare la potenza della logica di addestramento integrata di
Keras? Esiste una via di mezzo tra fit() e un ciclo di addestramento scritto da
zero: si può fornire una funzione di addestramento personalizzata e lasciare che il
framework faccia il resto.
È possibile farlo sovrascrivendo il metodo train_step() della classe Model.
Questa è la funzione che viene chiamata da fit() per ogni gruppo di dati. Si potrà
quindi richiamare fit() come di consueto, eseguendo il proprio algoritmo di
apprendimento.
Ecco un semplice esempio:
◾ Creiamo una nuova classe che sottoclassi keras.Model.
◾ Sovrascriviamo il metodo train_step(self, data). Il suo contenuto è quasi
identico a quello utilizzato nella sezione precedente. Restituisce un dizionario
che mappa i nomi delle metriche (compresa la perdita) con i loro valori
attuali.
◾ Implementiamo una proprietà metrica che tiene traccia delle istanze di
metrica del modello. Ciò consente al modello di chiamare automaticamente
reset_state() sulle metriche del modello all'inizio di ogni epoca e all'inizio
di una chiamata a evaluate(), senza doverlo fare a mano.

Listato 7.26 Implementazione di un passo di addestramento personalizzato da usare con fit()

loss_fn = keras.losses.SparseCategoricalCrossentropy() Questo oggetto metrico verrà


loss_tracker = keras.metrics.Mean(name="loss") utilizzato per tenere traccia della
media delle perdite per lotto durante
l'addestramento e la valutazione.
classe CustomModel(keras.Model):
def train_step(self, data):
Sovrascriviamo il Utilizziamo self(input,
metodo train_step. training=True) invece
input, target = data
con tf.GradientTape() come di model(input,
nastro:
Scrivere i propri cicli di formazione e 201
valutazione

predictions = self(input, training=True) training=True), poiché il


loss = loss_fn(targets, predictions) nostro modello è la classe
stessa.
202 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda
gradienti = tape.gradient(loss, model.trainable_weights)
optimizer.apply_gradients(zip(gradienti, model.trainable_weights))

loss_tracker.update_state(loss)
Aggiorniamo la
return {"loss": loss_tracker.result()} metrica del loss
tracker che tiene
@proprietà traccia della
media
della perdita.
def metrics(self): Qui vanno elencate
restituire tutte le metriche La perdita media viene
[loss_tracker] che si desidera restituita interrogando la
azzerare tra le varie metrica del loss tracker.
epoche.

Ora possiamo istanziare il nostro modello personalizzato, compilarlo (passiamo solo


l'ottimizzatore, poiché la perdita è già definita al di fuori del modello) e addestrarlo
usando fit() come al solito:

input = keras.Input(shape=(28 * 28,))


features = layers.Dense(512, activation="relu")(input)
features = layers.Dropout(0.5)(features)
outputs = layers.Dense(10, activation="softmax")(features)
model = CustomModel(inputs, outputs)

model.compile(optimizer=keras.optimizers.RMSprop())
model.fit(train_images, train_labels, epochs=3)

Ci sono un paio di punti da notare:


◾ Questo schema non impedisce di costruire modelli con l'API funzionale. Lo si
può fare sia che si costruiscano modelli sequenziali, sia che si costruiscano
modelli con API funzionali, sia che si costruiscano modelli sottoclassificati.
◾ Non è necessario utilizzare un decoratore @tf.function quando si sovrascrive
train_ step: il framework lo fa per noi.
Ora, che dire delle metriche e della configurazione della perdita tramite compile()?
Dopo aver chiamato compile(), si ottiene l'accesso a quanto segue:
◾ self.compiled_loss-La funzione di perdita passata a compile().
◾ self.compiled_metrics-Un wrapper per l'elenco delle metriche passate, che
consente di chiamare self.compiled_metrics.update_state() per
aggiornare tutte le metriche in una volta sola.
◾ self.metrics: l'elenco effettivo delle metriche passate a compile(). Si noti
che include anche una metrica che tiene traccia della perdita, in modo simile a
quanto fatto manualmente con lametrica loss_tracking_metric.
Possiamo quindi scrivere

classe CustomModel(keras.Model):
def train_step(self, data):
input, target = data Calcolare la
con tf.GradientTape() come nastro: perdita
predizioni = self(input, training=True) tramite
loss = self.compiled_loss(targets, predictions) perdita di
gradients = tape.gradient(loss, model.trainable_weights) self.compiled_.
Scrivere i propri cicli di formazione e 203
valutazione
optimizer.apply_gradients(zip(gradients, model.trainable_weights))
self.compiled_metrics.update_state(targets, predictions)
return {m.name: m.result() for m in self.metrics}
Aggiornare le metriche del Restituisce un dict che
modello tramite mappa i nomi delle metriche
self.compiled_metrics. al loro valore attuale.

Proviamo:

input = keras.Input(shape=(28 * 28,))


features = layers.Dense(512, activation="relu")(input)
features = layers.Dropout(0.5)(features)
outputs = layers.Dense(10, activation="softmax")(features)
model = CustomModel(inputs, outputs)

model.compile(optimizer=keras.optimizers.RMSprop(),
loss=keras.losses.SparseCategoricalCrossentropy(),
metrics=[keras.metrics.SparseCategoricalAccuracy()])
model.fit(train_images, train_labels, epochs=3)

Sono state molte informazioni, ma ora ne sapete abbastanza per usare Keras per fare
quasi tutto.

Sintesi
◾ Keras offre uno spettro di flussi di lavoro diversi, basati sul principio della
divulgazione progressiva della complessità. Tutti interagiscono senza problemi tra
loro.
◾ È possibile costruire modelli tramite la classe Sequential, tramite l'API
Functional o tramite una sottoclasse della classe Model. Nella maggior parte dei
casi, si utilizzerà l'API Functional.
◾ Il modo più semplice per addestrare e valutare un modello è attraverso i metodi predefiniti
fit() e
valutare().
◾ Le callback di Keras forniscono un modo semplice per monitorare i modelli durante la
chiamata a
fit() e agire automaticamente in base allo stato del modello.
◾ Si può anche avere il pieno controllo di ciò che fa fit() sovrascrivendo il
metodo train_ step().
◾ Oltre a fit(), è possibile scrivere i propri cicli di addestramento interamente da
zero. Questo è utile per i ricercatori che implementano nuovi algoritmi di
addestramento.
204 CAPITOLO 7 Lavorare con Keras:
Un'immersione profonda

Introduzion
e
all'apprendimento
profondo per la
visione artificiale
Questo capitolo tratta
◾ Capire le reti neurali convoluzionali (convnet)
◾ Utilizzo dell'aumento dei dati per ridurre
l'overfitting
◾ Utilizzo di una convnet preaddestrata
per l'estrazione delle caratteristiche
◾ Messa a punto di una convnet preaddestrata

La visione artificiale è la prima e più grande storia di successo del deep learning.
Ogni giorno si interagisce con i modelli di visione profonda: attraverso Google Foto, la
ricerca di immagini su Google, YouTube, i filtri video nelle app delle fotocamere, i
software OCR e molto altro. Questi modelli sono anche al centro di ricerche
all'avanguardia nel campo della guida autonoma, della robotica, della diagnosi
medica assistita dall'intelligenza artificiale, dei sistemi di cassa autonomi e
persino dell'agricoltura autonoma.
La computer vision è il dominio problematico che ha portato all'ascesa
iniziale del deep learning tra il 2011 e il 2015. Un tipo di modello di deep
learning chiamato reti neurali convoluzionali ha iniziato a ottenere risultati
straordinariamente buoni nelle competizioni di classificazione delle immagini in
quel periodo, dapprima con Dan Ciresan che ha vinto due competizioni di nicchia
(la competizione di riconoscimento dei caratteri cinesi ICDAR 2011 e la IJCNN
201
202 CAPITOLO 8 Introduzione all'apprendimento profondo per la
visione artificiale
2011, il concorso tedesco per il riconoscimento dei segnali stradali) e poi, più in
particolare, nell'autunno 2012, con la vittoria del gruppo di Hinton nella sfida di alto
profilo ImageNet per il r i c o n o s c i m e n t o visivo su larga scala. Molti altri risultati
promettenti hanno iniziato a emergere rapidamente in altre attività di computer
vision.
È interessante notare che questi primi successi non sono stati sufficienti a rendere il
deep learning mainstream all'epoca: ci sono voluti alcuni anni. La comunità di ricerca
sulla computer vision aveva investito molti anni in metodi diversi dalle reti neurali e
non e r a pronta ad abbandonarli solo perché c'era un nuovo arrivato. Nel 2013 e nel
2014, il deep learning si scontrava ancora con un forte scetticismo da parte di molti
ricercatori senior di computer vision. Solo nel 2016 è diventato finalmente dominante.
Ricordo di aver esortato un mio ex professore, nel febbraio 2014, a passare al deep
learning. "È la prossima grande cosa!" Gli dicevo. "Beh, forse è solo una moda", mi
rispose. Nel 2016, tutto il suo laboratorio si occupava di deep learning. Non si può
fermare un'idea il cui momento è arrivato.
Questo capitolo introduce le reti neurali convoluzionali, note anche come reti
convettive, il tipo di modello di apprendimento profondo che oggi viene utilizzato quasi
universalmente nelle applicazioni di computer vision. Imparerete ad applicare le reti
convoluzionali ai problemi di classificazione delle immagini, in particolare a quelli che
coinvolgono piccoli insiemi di dati di addestramento, che sono il caso d'uso più comune
se non siete una grande azienda tecnologica.

8.1 Introduzione alle reti convettive


Stiamo per addentrarci nella teoria di cosa sono le reti convnet e perché hanno avuto
tanto successo nei compiti di computer vision. Ma prima di tutto, diamo un'occhiata
pratica a un semplice esempio di rete convnet che classifica le cifre MNIST, un compito
che abbiamo eseguito nel capitolo 2 utilizzando una rete densamente connessa (la nostra
accuratezza di prova allora era del 97,8%). Anche se la rete convet- ta sarà elementare,
la sua accuratezza farà saltare il nostro modello a connessione densa del capitolo 2.
Il seguente elenco mostra l'aspetto di una convnet di base. È una pila di livelli
Conv2D e MaxPooling2D. Tra poco si vedrà esattamente cosa fanno. Costruiremo il
modello utilizzando l'API Functional, introdotta nel capitolo precedente.

Listato 8.1 Istanziare una piccola convnet

da tensorflow importa keras


da tensorflow.keras import layers inputs
= keras.Input(shape=(28, 28, 1))
x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(input)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
outputs = layers.Dense(10, activation="softmax")(x)
model = keras.Model(inputs=inputs, outputs=outputs)
Introduzione alle reti 203
convettive
È importante notare che una convnet prende in ingresso tensori
di forma
(altezza_immagine, larghezza_immagine, canali_immagine), senza
includere la dimensione del lotto. In questo caso, configureremo la convnet per elaborare
input di dimensione (28, 28, 1), che è il formato delle immagini MNIST.
Visualizziamo l'architettura della nostra convnet.

Listato 8.2 Visualizzazione del riepilogo del modello

>>> model.summary()
Modello: "modello"

Strato (tipo) Output ShapeParam #


=================================================================
input_1 (livello di [(Nessuno, 28, 28, 0
ingresso) 1)].
conv2d (Conv2D) (Nessuno, 26, 26, 32) 320

max_pooling2d (MaxPooling2D) (Nessuno, 13, 13, 32) 0

conv2d_1 (Conv2D) (Nessuno, 11, 11, 64) 18496

max_pooling2d_1 (MaxPooling2 (Nessuno, 5, 5, 64) 0

conv2d_2 (Conv2D) (Nessuno, 3, 3, 128) 73856

appiattire (Flatten) (Nessuno, 1152) 0

denso (Denso) (Nessuno, 10) 11530


=================================================================
Totale parametri: 104,202
Parametri addestrabili: 104,202
Parametri non addestrabili: 0

Si può notare che l'output di ogni livello Conv2D e MaxPooling2D è un tensore di rango
3 di forma (altezza, larghezza, canali). Le dimensioni di larghezza e altezza
tendono a ridursi man mano che si scende in profondità nel modello. Il numero di canali
è controllato d a l primo argomento passato ai livelli Conv2D (32, 64 o 128).
Dopo l'ultimo strato Conv2D, si ottiene un output di forma (3, 3, 128) - una mappa
di caratteristiche 3 × 3 di 128 canali. Il passo successivo consiste nell'inserire questo
output in un classificatore densamente connesso, come quelli che già conosciamo: una
pila di livelli densi. Questi classificatori elaborano vettori, che sono 1D, mentre
l'output attuale è un tensore di rango 3. Per colmare il divario, appiattiamo la mappa
delle caratteristiche su 128 canali. Per colmare il divario, appiattiamo gli output 3D in
1D con un livello Flatten prima di aggiungere i livelli Dense.
Infine, effettuiamo una classificazione a 10 vie, quindi l'ultimo livello ha 10 uscite e
un'attivazione softmax.
Ora addestriamo la convnet sulle cifre MNIST. Riutilizzeremo gran parte del codice
dell'esempio MNIST nel capitolo 2. Poiché stiamo eseguendo una classificazione a 10
vie con un'uscita softmax, useremo la perdita categorica crossentropy e, poiché le nostre
etichette sono numeri interi, useremo la versione rada,
sparse_categorical_crossentropy.
204 CAPITOLO 8 Introduzione all'apprendimento profondo per la
visione artificiale

Listato 8.3 Addestramento di convnet su immagini MNIST


da tensorflow.keras.datasets importa mnist

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()


train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype("float32") / 255
test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype("float32") / 255
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
model.fit(train_images, train_labels, epochs=5, batch_size=64)

Valutiamo il modello sui dati di prova.

Listato 8.4 Valutazione della convnet

>>> test_loss, test_acc = model.evaluate(test_images, test_labels)


>>> print(f "Accuratezza del test:
{test_acc:.3f}") Accuratezza del test:
0,991

Mentre il modello densamente connesso del capitolo 2 aveva un'accuratezza di test


del 97,8%, la convnet di base ha un'accuratezza di test del 99,1%: abbiamo ridotto il
tasso di errore di circa il 60% (relativo). Non male!
Ma perché questa semplice convnet funziona così bene, rispetto a un modello
densamente connesso? Per rispondere a questa domanda, vediamo cosa fanno i livelli
Conv2D e MaxPooling2D.

8.1.1 L'operazione di convoluzione


La differenza fondamentale tra uno strato a connessione densa e uno strato di
convoluzione è la seguente: I livelli densi apprendono modelli globali nello
spazio delle caratteristiche dell'input (ad esempio, per una cifra MNIST, modelli che
coinvolgono tutti i pixel), mentre i livelli di convoluzione apprendono modelli locali - nel
caso delle immagini, modelli trovati in piccole finestre 2D degli input (vedere la
figura 8.1). Nell'esempio precedente, queste finestre erano tutte 3×3.

Figura 8.1 Le immagini possono


essere suddivise in modelli
locali come bordi, texture e così
via.
Introduzione alle reti 205
convettive
Questa caratteristica chiave conferisce alle reti convettive due interessanti proprietà:
◾ I modelli appresi sono indipendenti dalla traduzione. Dopo aver appreso un
determinato modello nell'angolo in basso a destra di un'immagine, una convnet
può riconoscerlo ovunque, ad esempio nell'angolo in alto a sinistra. Un modello
densamente connesso dovrebbe apprendere nuovamente il modello se appare in
una nuova posizione. Questo rende le reti convnet efficienti dal punto di vista dei
dati nell'elaborazione delle immagini (perché il mondo visivo è fondamentalmente
invariante rispetto alla traduzione): hanno bisogno di un minor numero di
campioni di addestramento per apprendere rappresentazioni con potere di
generalizzazione.
◾ Possono apprendere gerarchie spaziali di modelli. Un primo strato di
convoluzione apprenderà piccoli schemi locali come i bordi, un secondo strato di
convoluzione apprenderà schemi più grandi composti dalle caratteristiche dei
primi strati e così via (vedi figura 8.2). Questo permette alle reti di convoluzione
di apprendere in modo efficiente concetti visivi sempre più complessi e astratti,
perché il mondo visivo è fondamentalmente gerarchico dal punto di vista spaziale.

"gatt
o"

Figura 8.2 Il mondo visivo forma una gerarchia spaziale di moduli


visivi: linee o texture elementari si combinano in oggetti semplici come
occhi o orecchie, che si combinano in concetti di alto livello come
"gatto".

Le convoluzioni operano su tensori di rango 3 chiamati mappe di caratteristiche,


con due assi spaziali (altezza e larghezza) e un asse di profondità (chiamato anche
asse dei canali). Per un'immagine RGB, la dimensione dell'asse della profondità è 3,
perché l'immagine ha tre canali di colore: rosso, verde e blu. Per un'immagine in
bianco e nero, come le cifre di MNIST, la profondità è 1 (livelli di grigio).
L'operazione di convoluzione estrae patch dalla sua mappa di caratteristiche di
206 CAPITOLO 8 Introduzione all'apprendimento profondo per la
ingresso e applica la artificiale
visione stessa trasformazione a tutte queste patch, producendo una
mappa di caratteristiche di uscita. Questa mappa di caratteristiche in uscita è ancora un
tensore di rango 3: ha una larghezza e una
Introduzione alle reti 207
convettive
un'altezza. La sua profondità può essere arbitraria, perché la profondità di uscita è
un parametro del livello, e i diversi canali dell'asse di profondità non rappresentano
più colori specifici come nell'input RGB, ma piuttosto filtri. I filtri codificano
aspetti specifici dei dati di input: a un livello elevato, un singolo filtro potrebbe
codificare il concetto di "presenza di un volto nell'input", ad esempio.
Nell'esempio di MNIST, il primo livello di convoluzione prende una mappa di
caratteristiche di dimensioni (28, 28, 1) ed emette una mappa di caratteristiche di
dimensioni (26, 26, 32): calcola 32 filtri sul suo ingresso. Ognuno di questi 32
canali di uscita contiene una griglia di valori 26 × 26, che rappresenta una mappa di
risposta del filtro sull'ingresso, indicando la risposta di quel modello di filtro in diverse
posizioni dell'ingresso (cfr. figura 8.3).

Mappa di risposta, che


quantifica la presenza
d e l pattern del filtro in
Ingresso diverse posizioni.
originale

Filtro
singolo

Figura 8.3 Il concetto di mappa di


risposta: una mappa 2D della
presenza di un modello in diverse
posizioni di un ingresso.

Questo è il significato del termine mappa di caratteristiche: ogni dimensione dell'asse di


profondità è una caratteristica (o filtro) e il tensore di rango 2 output[:, :, n] è la
mappa spaziale 2D della risposta di questo filtro sull'ingresso.
Le convoluzioni sono definite da due parametri chiave:
◾ Dimensione delle patch estratte dagli input: in genere sono 3 × 3 o 5 × 5.
Nell'esempio sono 3 × 3, una scelta comune. Nell'esempio sono 3 × 3, una scelta
comune.
◾ Profondità della mappa delle caratteristiche in uscita: si tratta del numero di filtri
calcolati dalla con- voluzione. L'esempio è partito con una profondità di 32 e si è
concluso con una profondità di 64.
Nei livelli Conv2D di Keras, questi parametri sono i primi argomenti passati al livello:
Conv2D(profondità_uscita, (altezza_finestra, larghezza_finestra)).
La convoluzione funziona facendo scorrere queste finestre di dimensioni 3 × 3 o 5 ×
5 sulla mappa 3D delle caratteristiche in ingresso, fermandosi in ogni punto possibile ed
estraendo il patch 3D delle caratteristiche circostanti (shape (window_height,
window_width, input_depth)). Ciascuna di queste patch 3D viene poi trasformata in
un vettore 1D di forma (profondità_di_uscita), tramite un prodotto tensoriale
con una matrice di pesi appresa, chiamata kernel di convoluzione; lo stesso kernel viene
riutilizzato per ogni patch. Tutti questi vettori (uno per patch) vengono poi riassemblati
spazialmente in una mappa 3D di forma (altezza, larghezza,
profondità_di_uscita). Ogni posizione spaziale nella mappa delle caratteristiche
di output corrisponde alla stessa posizione nella mappa delle caratteristiche di input (ad
208 CAPITOLO 8 Introduzione all'apprendimento profondo per la
visione artificiale

esempio, l'angolo inferiore destro dell'output contiene informazioni sull'angolo inferiore


destro dell'input). Ad esempio, con
Introduzione alle reti 209
convettive
3 × 3 finestre, il vettore output[i, j, :] deriva dall'input patch 3D[i-1:i+1, j-
1:j+1, :]. Il processo completo è illustrato nella figura 8.4.

Larghezza Altezza

Prof Mappa delle caratteristiche in ingresso


ondità
di
ingre
sso

3 × 3 patch di ingresso

Prodotto di
punti con il
kernel

Profon
dità di Toppe trasformate
uscita

Mappa delle caratteristiche in uscita


Profon
dità di
uscita

Figura 8.4 Come


funziona la
convoluzione

Si noti che la larghezza e l'altezza di uscita possono differire da quelle di ingresso


per due motivi:
◾ Effetti di confine, che possono essere contrastati con l'imbottitura della mappa delle
caratteristiche in ingresso
◾ L'uso degli strides, che definirò in un secondo

momento, è un'altra cosa.


CAPIRE GLI EFFETTI DEI BORDI E DEL PADDING
Si consideri una mappa di caratteristiche 5 × 5 (25 tessere in totale). Ci sono solo 9
tessere attorno alle quali è possibile centrare una finestra 3 × 3, formando una
griglia 3 × 3 (vedere figura 8.5). Di conseguenza, la mappa di caratteristiche in uscita
sarà 3 × 3. Si restringe un po': esattamente di due tessere per ogni dimensione, in questo
210 CAPITOLO 8 Introduzione all'apprendimento profondo per la
visione artificiale

caso. È possibile vedere questo effetto di confine in azione nell'esempio precedente:


si inizia con 28 × 28 ingressi, che diventano 26 × 26 dopo il primo livello di
convoluzione.
Se si desidera ottenere una mappa di caratteristiche in uscita con le stesse
dimensioni spaziali dell'input, è possibile utilizzare il padding. Il padding consiste
nell'aggiunta di un numero appropriato di righe
Introduzione alle reti 211
convettive

Figura 8.5 Posizioni valide delle patch 3 × 3 in una mappa di caratteristiche di ingresso 5 × 5

e colonne su ogni lato della mappa delle caratteristiche di input, in modo da poter
inserire finestre di convoluzione al centro intorno a ogni tile di input. Per una finestra
3 × 3, si aggiunge una colonna a destra, una colonna a sinistra, una riga in alto e una
riga in basso. Per una finestra 5 × 5, si aggiungono due righe (vedi figura 8.6).

ecc.

Figura 8.6 Imbottitura di un input 5 × 5 per poter estrarre 25 patch 3 × 3

Nei livelli Conv2D, il padding è configurabile tramite l'argomento padding, che


assume due valori: "valid", che significa nessun padding (verranno utilizzate solo le
posizioni valide della finestra), e "same", che significa "padding in modo da avere un
output con la stessa larghezza e altezza dell'input". L'argomento padding ha come
valore predefinito "valid".
COMPRENDERE I PASSAGGI DI CONVOLUZIONE
L'altro fattore che può influenzare la dimensione dell'output è la nozione di strides.
La nostra descrizione della convoluzione finora ha presupposto che le tessere
centrali delle finestre di convoluzione siano tutte contigue. Ma la distanza tra due
212 CAPITOLO 8 Introduzione all'apprendimento profondo per la
finestre successive è visione
un parametro
artificialedel sistema di convoluzione.
Introduzione alle reti 213
convettive
convoluzione, detta stride, che per impostazione predefinita è 1. È possibile avere
convoluzioni con stride: convoluzioni con stride superiore a 1. Nella figura 8.7, si
possono vedere le patch estratte da una convoluzione 3 × 3 con stride 2 su un input 5 ×
5 (senza padding).

2
1
1 2

3 4
3 4

Figura 8.7 Patch di convoluzione 3 × 3 con passi 2 × 2

L'uso dello stride 2 significa che la larghezza e l'altezza della mappa delle
caratteristiche sono sottocampionate di un fattore 2 (oltre alle modifiche indotte
dagli effetti dei bordi). Le convoluzioni con stride sono raramente utilizzate nei modelli
di classificazione, ma sono utili per alcuni tipi di modelli, come si vedrà nel prossimo
capitolo.
Nei modelli di classificazione, al posto degli strides, si tende a utilizzare l'operazione
di max-pooling per ricampionare le mappe di caratteristiche, che abbiamo visto in
azione nel nostro primo esempio di convnet. Analizziamola in modo più approfondito.

8.1.2 L'operazione di max-pooling


Nell'esempio di convnet, si può notare che le dimensioni delle mappe di caratteristiche
vengono dimezzate dopo ogni livello MaxPooling2D. Ad esempio, prima del primo
strato MaxPooling2D, la mappa delle caratteristiche è 26 × 26, ma l'operazione di max-
pooling la dimezza a 13 × 13. Questo è il ruolo del max pooling: ridimensionare
aggressivamente le mappe delle caratteristiche. Questo è il ruolo del max pooling:
ridurre in modo aggressivo il campionamento delle mappe di caratteristiche, proprio
come le convoluzioni stridenti.
Il max pooling consiste nell'estrarre finestre dalle mappe di caratteristiche in
ingresso e nell'emettere il valore massimo di ciascun canale. È concettualmente simile
alla convoluzione, tranne per il fatto che invece di trasformare le patch locali tramite
una trasformazione lineare appresa (il kernel di convoluzione), vengono trasformate
tramite un'operazione di tensore massimo codificata. Una grande differenza rispetto
alla convoluzione è che il max pooling viene di solito eseguito con finestre 2 × 2 e stride
2, in modo da sottocampionare le mappe di caratteristiche di un fattore 2. D'altra parte,
la convoluzione viene di solito eseguita con finestre 3 × 3 e senza stride (stride 1).
214 CAPITOLO 8 Introduzione all'apprendimento profondo per la
visione artificiale
Perché campionare le feature map in questo modo? Perché non rimuovere i livelli di
max-pooling e mantenere le feature map abbastanza grandi? Esaminiamo questa
opzione. Il nostro modello sarebbe simile al seguente elenco.

Listato 8.5 Una convnet strutturata in modo errato a cui mancano i livelli di max-pooling

input = keras.Input(shape=(28, 28, 1))


x = layers.Conv2D(filtri=32, kernel_size=3, attivazione="relu")(input)
x = layers.Conv2D(filtri=64, kernel_size=3, attivazione="relu")(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
outputs = layers.Dense(10, activation="softmax")(x)
model_no_max_pool = keras.Model(inputs=inputs, outputs=outputs)

Ecco una sintesi del modello:

>>> model_no_max_pool.summary()
Modello: "modello_1"

Strato (tipo) Output ShapeParam #


=================================================================
input_2 (livello di [(Nessuno, 28, 28, 0
ingresso) 1)].
conv2d_3 (Conv2D) (Nessuno, 26, 26, 32) 320

conv2d_4 (Conv2D) (Nessuno, 24, 24, 64) 18496

conv2d_5 (Conv2D) (Nessuno, 22, 22, 128) 73856

flatten_1 (Appiattire) (Nessuno, 61952) 0

denso_1 (Denso) (Nessuno, 10) 619530


=================================================================
Totale params: 712,202
Parametri addestrabili: 712,202
Parametri non addestrabili: 0

Cosa c'è di sbagliato in questa configurazione? Due cose:


◾ Non è favorevole all'apprendimento di una gerarchia spaziale di
caratteristiche. Le finestre 3 × 3 del terzo strato conterranno solo informazioni
provenienti dalle finestre 7 × 7 dell'input iniziale. I modelli di alto livello
appresi dalla convnet saranno ancora molto piccoli rispetto all'input iniziale, il
che potrebbe non essere sufficiente per imparare a classificare le cifre (provate
a riconoscere una cifra guardandola solo attraverso finestre di 7 × 7 pixel!). Le
caratteristiche dell'ultimo strato di convoluzione devono contenere
informazioni sulla totalità dell'input.
◾ La mappa finale delle caratteristiche ha 22 × 22 × 128 = 61.952 coefficienti totali
per campione. È un dato enorme. Se la si appiattisce per sovrapporvi uno strato
Dense di dimensione 10, questo strato avrebbe più di mezzo milione di
parametri. Si tratta di una quantità eccessiva per un modello così piccolo, che
provocherebbe un forte overfitting.
Addestramento di una convnet da zero su un 211
piccolo set di dati
In breve, il motivo per cui si usa il downsampling è ridurre il numero di coefficienti
della mappa delle caratteristiche da elaborare, nonché indurre gerarchie di filtri spaziali
facendo sì che i livelli di convoluzione di successo guardino finestre sempre più ampie
(in termini di frazione dell'input originale che coprono).
Si noti che il max pooling non è l'unico modo per ottenere questo downsampling.
Come si sa, si possono usare anche gli strides nel livello di convoluzione precedente. E
si può usare il pooling medio invece del pooling massimo, dove ogni patch di ingresso
locale viene trasformato prendendo il valore medio di ogni canale nel patch, invece d e l
massimo. Ma il max pooling tende a funzionare meglio di queste soluzioni alternative. Il
motivo è che le caratteristiche tendono a codificare la presenza spaziale di qualche
modello o concetto sulle diverse tessere della mappa di caratteristiche (da qui il termine
mappa di caratteristiche), ed è più informativo guardare alla presenza massima di
diverse caratteristiche che alla loro presenza media. La strategia di sottocampionamento
più ragionevole è quella di produrre prima mappe dense di caratteristiche (tramite
convoluzioni non stridate) e poi esaminare l'attivazione massima delle caratteristiche su
piccole patch, piuttosto che esaminare finestre più ristrette degli ingressi (tramite
convoluzioni stridate) o fare la media delle patch di ingresso, che potrebbero far perdere
o diluire le informazioni sulla presenza delle caratteristiche.
A questo punto, dovreste aver compreso le basi delle convnet - mappe di
caratteristiche, conversazione e max pooling - e dovreste sapere come costruire una
piccola convnet per risolvere un problema giocattolo come la classificazione delle cifre
MNIST. Passiamo ora ad applicazioni pratiche più utili.

8.2 Addestramento di una convnet da zero su un piccolo set di dati


Dover addestrare un modello di classificazione delle immagini utilizzando pochissimi
dati è una situazione comune, che probabilmente si incontra nella pratica se ci si occupa
di computer vision in un contesto professionale. Pochi campioni possono significare da
qualche centinaio a qualche decina di migliaia di immagini. Come esempio pratico, ci
concentreremo sulla classificazione delle immagini come cani o gatti in un set di dati
contenente 5.000 immagini di cani e gatti (2.500 gatti, 2.500 cani). Utilizzeremo 2.000
immagini per l'addestramento, 1.000 per la validazione e 2.000 per il test.
In questa sezione esamineremo una strategia di base per affrontare questo problema:
l'addestramento di un nuovo modello da zero utilizzando i pochi dati a disposizione.
Inizieremo con l'addestramento ingenuo di una piccola convnet sui 2.000 campioni
di addestramento, senza alcuna regolarizzazione, per stabilire una linea di base per i
risultati che si possono ottenere. In questo modo raggiungeremo un'accuratezza di
classificazione di circa il 70%. A quel punto, il problema principale sarà
l'overfitting. Introdurremo quindi l'aumento dei dati, una tecnica potente per mitigare
l'overfitting nella computer vision. Utilizzando l'aumento dei dati, miglioreremo il
modello per raggiungere un'accuratezza dell'80-85%.
Nella prossima sezione, esamineremo altre due tecniche essenziali per l'applicazione
del deep learning a piccoli insiemi di dati: l'estrazione di caratteristiche con un modello
pre-addestrato (che ci porterà a una precisione del 97,5%) e la messa a punto di un
modello pre-addestrato (che ci porterà a una precisione finale del 98,5%). Insieme, queste
tre strategie - l'addestramento di un piccolo modello da zero, l'estrazione delle
caratteristiche con un modello pre-addestrato e la messa a punto di un modello pre-
212 CAPITOLO 8 Introduzione all'apprendimento profondo per la
addestrato - ci permettono di ottenere una precisione del 98,5%.
visione artificiale
Addestramento di una convnet da zero su un 213
piccolo set di dati
costituirà la vostra futura cassetta degli attrezzi per affrontare il problema della
classificazione delle immagini con piccoli insiemi di dati.

8.2.1 L'importanza dell'apprendimento profondo per i problemi di piccoli dati


Ciò che si qualifica come "un numero sufficiente di campioni" per addestrare un
modello è relativo, ad esempio, alle dimensioni e alla profondità del modello che si sta
cercando di addestrare. Non è possibile addestrare una convnet per risolvere un
problema complesso con poche decine di campioni, ma qualche centinaio può
potenzialmente essere sufficiente se il modello è piccolo e ben regolarizzato e il compito
è semplice. Poiché le convnet apprendono caratteristiche locali e indipendenti dalla
traduzione, sono molto efficienti dal punto di vista dei dati per i problemi percettivi.
L'addestramento di una convnet da zero su un set di dati di immagini molto piccolo
produrrà risultati ragionevoli nonostante una relativa mancanza di dati, senza la
necessità di un'ingegneria delle caratteristiche personalizzata. Lo vedremo in azione in
questa sezione.
Inoltre, i modelli di deep learning sono per natura altamente riproponibili: è
possibile prendere, ad esempio, un modello di classificazione delle immagini o di speech-
to-text addestrato su un set di dati su larga scala e riutilizzarlo su un problema
significativamente diverso con solo piccole modifiche. In particolare, nel caso della
computer vision, molti modelli pre-addestrati (di solito addestrati sul dataset
ImageNet) sono ora disponibili pubblicamente per il download e possono essere
utilizzati per avviare potenti modelli di visione a partire da pochissimi dati. Questo è
uno dei maggiori punti di forza del deep learning: il riutilizzo delle funzioni. Lo
esploreremo nella prossima sezione.
Cominciamo a mettere le mani sui dati.

8.2.2 Scaricare i dati


Il dataset Dogs vs. Cats che utilizzeremo non è stato fornito con Keras. È stato reso
disponibile da Kaggle come parte di una competizione di computer vision alla fine del
2013, quando le reti convettive non e r a n o ancora mainstream. È possibile scaricare il
set di dati originale da www.kaggle
.com/c/dogs-vs-cats/data (è necessario creare un account Kaggle se non lo si
possiede già - non preoccupatevi, il processo è indolore). È anche possibile utilizzare
l'API di Kaggle per scaricare il set di dati in Colab (vedere la barra laterale
"Download di un set di dati Kaggle in Google Colaboratory").
214 CAPITOLO 8 Introduzione all'apprendimento profondo per la
visione artificiale

Scaricare un set di dati Kaggle nel Colaboratorio di Google


Kaggle mette a disposizione un'API facile da usare per scaricare in modo
programmatico i dataset ospitati da Kaggle. È possibile utilizzarla per scaricare il
set di dati Cani contro Gatti in un notebook Colab, ad esempio. Questa API è
disponibile come pacchetto kaggle, che è preinstallato su Colab. Scaricare questo
set di dati è facile come eseguire il seguente comando in una cella di Colab:
Scarica le gare di kaggle -c cani-vs-gatti

Tuttavia, l'accesso all'API è limitato agli utenti di Kaggle, quindi per poter eseguire
il comando di pre-cessione è necessario prima autenticarsi. Il pacchetto kaggle
cercherà le credenziali di accesso in un file JSON situato in ~/.kaggle/kaggle.json.
Creiamo questo file.

Potrebbero piacerti anche