UNIVERSITA’ DEGLI STUDI DI CAGLIARI
FACOLTA’ DI SCIENZE
Corso di Laurea in Informatica
TensorFlow - libreria software open source per l'apprendimento
automatico
Docente di riferimento
Prof. Reforgiato Recupero Diego Angelo Gaetano
Candidato
Pes Nicola
(matr. 48305)
ANNO ACCADEMICO 2015-2016
Indice
1
INTRODUZIONE.........................................................................................................1
1.1
INTELLIGENZAARTIFICIALE.................................................................................................1
1.2
APPRENDIMENTOAUTOMATICO.........................................................................................3
1.2.1 Apprendimentoapprofondito.................................................................................5
2
TENSORFLOW................................................................................................................7
2.1
INTRODUZIONE...............................................................................................................7
3
MODELLO DI PROGRAMMAZIONE E CONCETTI BASE.....................................9
3.1
OPERAZIONI E KERNELS............................................................................................9
3.2
SESSIONI..................................................................................................................10
3.3
VARIABILI..................................................................................................................11
4
IMPLEMENTAZIONE...............................................................................................13
4.1
DISPOSITIVI...............................................................................................................13
4.2
TENSORI...................................................................................................................13
5
ESECUZIONE DI UN SINGOLO DISPOSITIVO....................................................15
6
ESECUZIONE DI PIÙ DISPOSITIVI........................................................................17
6.1
COLLOCAMENTO DEL NODO.....................................................................................17
6.2
COMUNICAZIONE CROSS-DEVICE............................................................................18
7
ESECUZIONE DISTRIBUITA..................................................................................21
7.1
TOLLERANZA AGLI ERRORI.......................................................................................21
8
ESTENSIONI.............................................................................................................23
8.1
CALCOLO DEL GRADIENTE.......................................................................................23
8.2
ESECUZIONE PARZIALE............................................................................................24
8.3
VINCOLI DI DISPOSITIVO...........................................................................................25
8.4
FLUSSO DI CONTROLLO............................................................................................26
8.5
OPERAZIONI DI INPUT...............................................................................................27
8.6
CODE........................................................................................................................27
8.7
CONTENITORI...........................................................................................................28
9
OTTIMIZZAZIONE....................................................................................................29
9.1
ELIMINAZIONE DELLA SOTTO ESPRESSIONE COMUNE............................................29
9.2
CONTROLLO DELLA COMUNICAZIONE DEI DATI E UTILIZZO DELLA MEMORIA.........29
9.3
KERNEL ASINCRONI..................................................................................................29
9.4
LIBRERIE OTTIMIZZATE PER LE IMPLEMENTAZIONI DEL KERNEL.............................30
9.5
COMPRESSIONE CON PERDITA................................................................................30
10
STATO ED ESPERIENZA...................................................................................33
11
IDIOMI DI PROGRAMMAZIONE COMUNI.........................................................37
11.1 TRAINING DI DATI PARALLELO..................................................................................37
11.2 MODELLO DI TRAINING PARALLELO.........................................................................38
11.3 PASSI SIMULTANEI PER IL MODELLO DI CALCOLO PIPELINING................................39
12
TOOLS..................................................................................................................41
12.1 TENSORBOARD: VISUALIZZAZIONE DELLE STRUTTURE DEL GRAFICO E STATISTICHE
RIASSUNTIVE..........................................................................................................................41
12.1.1 Visualizzazione del grafico di calcolo.........................................................41
12.1.2 Visualizzazione dei dati di sintesi................................................................42
13
TRACCIATO DI ESECUZIONE...........................................................................45
14
ESEMPI.................................................................................................................49
14.1 HELLO,TENSORFLOW!...................................................................................................49
14.2 SEMPLICIOPERAZIONI....................................................................................................50
14.2.1
Moltiplicazionetraduevariabili.......................................................................50
15
CONCLUSIONI.....................................................................................................55
RIFERIMENTI BIBLIOGRAFICI......................................................................................57
1 Introduzione
1.1 Intelligenzaartificiale
L'intelligenza artificiale è una disciplina scientifica che studia la possibilità di
realizzare macchine e programmi informatici in grado di risolvere i problemi con
ragionamenti simili a quelli umani. È anche indicata con il termine inglese Artificial
Intelligence (A.I.). L'obiettivo dell'intelligenza artificiale è realizzare un sistema in
grado di simulare il comportamento e il ragionamento umano.
“Le fondamenta dell'intelligenza artificiale (I.A.) sono poste dal matematico
inglese Alan Turing che nel 1936 ipotizza la possibilità di costruire una macchina
ideale in grado di svolgere qualsiasi tipologia di calcolo (macchina di Turing) e nel
1950 pubblica l'articolo "Computing Machinery and Intelligence" in cui definisce un
apposito test (test di Turing) per riconoscere una macchina intelligente. L'espressione
Articial Intelligence (A.I.) viene utilizzata per la prima volta nel 1956
dall'informatico John McCarthy durante un convegno.” [2] La disciplina è suddivisa
in due distinte aree:
o
I.A. forte. L'intelligenza artificiale forte ipotizza la possibilità di realizzare un
computer in grado di svolgere tutte le operazioni dell'uomo e di raggiungere
un livello di intelligenza pari o superiore a quella umana. In base a questo
approccio la macchina è un'entità intelligente autonoma e indipendente
dall'uomo.
o
I.A. debole. L'intelligenza artificiale debole ipotizza la possibilità di costruire
una macchina in grado di svolgere operazioni complesse simulando il
comportamento umano. In base a questo approccio la macchina si limita a
simulare l'intelligenza umana senza mai eguagliarla.
La disciplina comprende campi di studio particolarmente complessi, come la
comprensione del linguaggio naturale, l'analisi visiva, la robotica, i sistemi esperti ecc.
Negli anni '70 e '80 la disciplina I.A. conosce un periodo di rapida evoluzione, in
particolar modo nel settore dei sistemi esperti e delle reti neurali. In questi anni sono
sviluppati agenti razionali più flessibili e potenti.
o
Agenti razionali. Un agente razionale è un programma informatico (algoritmo)
in grado di prendere le decisioni più razionali in condizioni di incertezza sulla
base delle conoscenze e dei dati a disposizione. L'agente razionale interagisce
1
con l'ambiente che lo circonda tramite sensori e attuatori. Sulla base di un
processo inferenziale può accumulare esperienza e modificare la base di
conoscenza. L'agente razionale è detto logico (o basato sulla conoscenza)
quando è in grado di apprendere nuove formule tramite il ragionamento logico.
o
Sistemi esperti. Un sistema esperto è un software in grado di organizzare la
conoscenza su un particolare ambito del sapere. Sulla base di una serie di
premesse, il software esegue delle procedure di inferenza per derivare delle
conclusioni logiche, al fine di risolvere problemi anche complessi. “Un sistema
esperto è composto da una base di conoscenza e da un motore inferenziale. Le
conoscenze iniziali sono alimentate dagli esperti in materia e dagli ingegneri
della conoscenza. Il sistema esperto è progettato per essere utilizzato dagli
utenti e per fornire loro delle soluzioni ai problemi che possono presentarsi in
un particolare ambito.” [8]
o
Reti neurali. Le reti neurali artificiali sono modelli matematici ispirati alle reti
neurali biologiche. La rete è composta da nodi, ognuno dei quali ha specifiche
proprietà, dati e svolge particolari funzioni. I nodi della rete sono interconnessi
tra loro per controllare le funzioni superiori del sistema.
A differenza dei comuni sistemi informatici quelli intelligenti hanno la capacità di
rispondere ai problemi con maggiore flessibilità, trovando il significato corretto anche
nei casi in un cui messaggio sembra essere contraddittorio o ambiguo. Questi sistemi
operano una comparazione con casi simili (ma non uguali) alla situazione osservata
ed elaborano un ventaglio di soluzioni (non una sola) tra cui scegliere. In tal modo
l'intelligenza artificiale simula il processo di scelta umano ed accumula esperienza (IA
debole).
2 Soltanto una parte dei problemi che si incontrano quotidianamente possono essere
risolti mediante una procedura automatizzata o un algoritmo. L'automazione non è in
grado di affrontare problemi troppi complessi senza mettere in campo un metodo di
analisi diverso. Ad esempio, la comprensione automatica del linguaggio naturale
richiede un sistema di analisi più complesso rispetto a quello utilizzato per un
qualsiasi database di computer. In questi campi entra in gioco l'intelligenza artificiale
(IA debole).
A differenza dell'uomo il sistema di intelligenza artificiale ha un enorme vantaggio:
non muore. In teoria, può quindi accumulare un’esperienza e una conoscenza
superiore a quella di qualsiasi essere umano. Finora l'intelligenza artificiale ha
contribuito a creare sistemi esperti in grado di analizzare singoli aspetti della realtà.
Non è escluso che in futuro tali progressi portino alla nascita di una vera e propria vita
artificiale senziente e cosciente (IA forte).
1.2 Apprendimentoautomatico
« un programma apprende da una certa esperienza E se: nel rispetto di una classe di compiti T, con
una misura di prestazione P, la prestazione P misurata nello svolgere il compito T è migliorata
dall'esperienza E.» - Tom M. Mitchell
“L'apprendimento automatico è la capacità di un sistema di ampliare le proprie
conoscenze senza l'intervento dell'uomo. Il concetto di apprendimento automatico è
utilizzato nell'ingegneria del software, nel data mining e, in particolar modo,
nell'intelligenza artificiale.” [5] I sistemi di apprendimento automatico si basano su
diversi metodi di ragionamento, tra i quali si ricordano:
•
Ragionamento induttivo. Il ragionamento induttivo è un processo logico che
consente di giungere a una conoscenza generale a partire dallo studio dei casi
particolari. Ad esempio, gli uccelli hanno le ali e volano, le api hanno le ali e
volano (asserzioni singolari), quindi tutti gli animali con le ali volano
(asserzione generale). Questo tipo di ragionamento può causare errori di
valutazione, dovuto all'eccessiva generalizzazione. Ad esempio, la gallina ha le
ali ma non vola.
3
•
Ragionamento deduttivo. Il ragionamento deduttivo è un processo logico che
consente di giungere a una conoscenza come conseguenza logica delle
premesse. Ad esempio, tutti gli animali mangiano, gli uomini sono animali
(premesse), quindi tutti gli uomini mangiano. Questo tipo di ragionamento
(inferenza deduttiva) preserva la verità contenuta nelle premesse.
A partire da una base di conoscenza, ricca di informazioni, un sistema di
apprendimento automatico ricerca ed estrae le eventuali regolarità tra i dati mediante
le tecniche di data mining. Per avviare un percorso di apprendimento automatico è
necessario utilizzare sia il metodo induttivo e sia il metodo deduttivo, anche se ciò
potrebbe portare a compiere errori. La sola presenza del metodo deduttivo potrebbe
comportare l'interruzione del processo di apprendimento entro i limiti imposti dalle
premesse. Un sistema di apprendimento automatico deve essere in grado di fare
esperienza e di riconoscere gli errori compiuti in passato al fine di evitare di
ricommetterli in futuro. Negli anni '50-'60 per realizzare le prime forme di
ragionamento automatico sono stati adottati due approcci:
•
Approccio deterministico. Nell'approccio deterministico l'uomo inserisce nella
macchina una tabella con tutte le associazioni di causa ed effetto. In una colonna
è indicata una particolare situazione e in un'altra l'azione che la macchina dovrà
eseguire. Tale approccio richiede una grande quantità di dati. Si pensi, ad
esempio, al numero di combinazioni possibili in una partita a scacchi. Inoltre,
non si può parlare di vero e proprio ragionamento automatico poiché la
macchina si limita a seguire meccanicamente ciò che il programmatore ha
impostato. Per quanto sia completa, la tabella non potrà mai prevedere tutte le
situazioni che si possono presentare in un ambiente reale.
•
Approccio probabilistico. Nell'approccio probabilistico l'uomo si limita a
spiegare le regole del gioco alla macchina e una minima conoscenza di base. È
la macchina ad accumulare esperienza. Inizialmente la macchina procede con
azioni casuali analizzando il risultato (effetto) finale. Quando il risultato è
positivo memorizza la scelta come valida e la ripete in situazioni analoghe.
Quando il risultato è negativo la memorizza tra le scelte da evitare. Dopo
numerosi cicli di esecuzione la macchina dovrebbe aver costruito una propria
conoscenza empirica del problema. Ad esempio, in una partita a scacchi il
computer perde quasi tutte le prime partite poiché deve ancora maturare
4 esperienza. Alla n-esima partita è, invece, più difficile battere il computer
poiché quest'ultimo ha già in memoria diverse situazioni e non risponde più
casualmente alle mosse del giocatore umano. Tale approccio è preferibile
rispetto all'approccio deterministico ma non elimina del tutto alcune criticità.
L'associazione causa-effetto di ogni situazione in una tabella richiede
comunque una grande quantità di memoria per ospitare i dati. Quanto
maggiore è la quantità di dati da analizzare, tanto minore è la velocità di
risposta della macchina.
•
Approccio previsionale. Più che un approccio si tratta di una estensione degli
approcci
precedenti
(deterministico
o
probabilistico).
Nell'approccio
previsionale la macchina analizza per ogni sua possibile scelta tutte le possibili
contro-mosse del giocatore avversario. In tal modo la macchina riesce a
prevedere l'evoluzione futura del gioco analizzando tutte le sequenze di azioni
possibili. L'approccio previsionale è relativamente semplice in un gioco astratto
come gli scacchi dove tutte le mosse appartengono a un insieme chiuso e le
regole sono ben delineate. E', invece, molto complesso da realizzare in un
ambiente reale (es. guida automatica su strada) dove gli imprevisti sono la
regola del gioco.
Le innovazioni informatiche degli ultimi trent'anni hanno consentito di lavorare con
macchine sempre più veloci e con memorie sempre più capienti. Ciò nonostante, la
complessità spaziale (memoria) e la complessità temporale (velocità di ragionamento)
degli algoritmi di intelligenza artificiale è ancora oggi uno dei principali problemi
dell'apprendimento e del ragionamento automatico.
1.2.1 Apprendimentoapprofondito
“L'apprendimento approfondito (in inglese deep learning) è quel campo di ricerca
dell'apprendimento automatico e dell'intelligenza artificiale che si basa su diversi
livelli di rappresentazione, corrispondenti a gerarchie di caratteristiche di fattori o
concetti, dove i concetti di alto livello sono definiti sulla base di quelli di basso. Tra le
architetture di apprendimento approfondito si annoverano le reti neurali profonde,
la convoluzione di reti neurali profonde, le Deep belief network, e reti neurali
ricorrenti, che sono state applicati nella computer vision, nel riconoscimento
5
automatico del discorso, nell'elaborazione del linguaggio naturale, nel
riconoscimento audio e nella bioinformatica. "Deep learning" è un'espressione oggi
famosa che ridà lustro al concetto di rete neurale.” [7]
6 2 TensorFlow
TensorFlow è una libreria software open source per l’apprendimento automatico in
diversi tipi di compiti percettivi e di comprensione del linguaggio.
È una seconda generazione di API che è attualmente usata sia in ambito di ricerca che
di produzione da 50 team in dozzine di prodotti commerciali Google, come il
riconoscimento vocale, Gmail, Google Foto, e Ricerca. Questi team hanno usato in
precedenza DistBelief, la prima generazione di API. TensorFlow fu sviluppato dal
team Google Brain e rilasciato sotto la licenza open source Apache 2.0 il 9 novembre
2015. Fornisce API in linguaggio Python, e delle API meno documentate in linguaggio
C++.
Fu
sviluppato
inizialmente
dal
team
di
Google
Brain
all’interno
dell’organizzazione di ricerca Google’s Machine Intelligence con lo scopo di condurre
l’apprendimento automatico e la ricerca sulle reti neurali approfondite, ma il sistema
è abbastanza generale per essere applicato pure in un enorme varietà di altri domini.
L’architettura flessibile permette di impiegare le operazioni in una o più CPU o GPU
in un desktop, server, o dispositivo mobile con una singola API.
2.1 Introduzione
Il progetto Google Brain parte nel 2011 per esplorare l’uso delle reti neurali profonde
in larga scala, sia per la ricerca che per l’uso nei prodotti Google. Come primo lavoro
del progetto, Google Brain costruisce DistBelief; DistBelief è la prima generazione di
API. Tramite questo sistema si è fatta molta ricerca inclusi lavori nell’apprendimento
non supervisionato, nella rappresentazione del linguaggio, nei modelli per la
classificazione di immagini e il riconoscimento di oggetti, la classificazione di video, il
riconoscimento vocale e altre aree. Inoltre in collaborazione con Google Brain team,
7
più di 50 team della Google e altre compagnie Alphabet sono ricorse alle reti neurali
profonde usando DistBelief in un’ampia varietà di prodotti, incluso Google Search, il
sistema di riconoscimento vocale, Google Photos, Google Maps e SteetView, Google
Translate, YouTube e molti altri. Sull’esperienza di DistBelief il team costruisce
TensorFlow, sistema di seconda generazione per l’implementazione e creazione di una
larga scala di modelli di apprendimento automatico. TensorFlow descrive le
operazioni tramite un modello a flusso di dati e mappa questi in una varietà di
piattaforme hardware differenti, variando dalla gestione dell’inferenza nelle
piattaforme dei dispositivi mobili come ad esempio Android e iOS sino a sistemi di
training e inferenza di modeste dimensioni usando singole macchine contenenti una o
più GPU sino a sistemi di training che girano in centinaia di macchine specializzate
con migliaia di GPU. Avere un unico sistema che può estendersi a una così ampia
gamma di piattaforme semplifica significativamente l’uso del sistema di
apprendimento automatico. I calcoli in TensorFlow si esprimono mediante grafici a
flusso di dati stateful. Per scalare la formazione di reti neurali in implementazioni più
grandi, TensorFlow permette di esprimere facilmente vari tipi di parallelismi
attraverso la replica e l’esecuzione parallela di un modello base del grafico a flusso di
dati, con molti dispositivi di calcolo differenti che collaborano tutti per aggiornare un
insieme di parametri condivisi o altro. Alcuni usi di TensorFlow consentono flessibilità
in termini di consistenza di aggiornamenti dei parametri. Comparato a DistBelief, il
modello di programmazione in TensorFlow è più flessibile, la sua performance è
significativamente migliore, e supporta la formazione e l’utilizzo di un’ampia gamma
di modelli su una più ampia varietà di piattaforme hardware eterogenee.
8 3 Modello di programmazione e concetti base
“Un’operazione in TensorFlow è descritta da un grafico composto da un insieme di
nodi. Il grafico rappresenta un’operazione di flusso di dati, con estensioni per
permettere ad alcuni tipi di nodi di mantenere e aggiornare uno stato persistente e per
la ramificazione e le strutture di controllo del loop all’interno del grafico in maniera
simile a Naiad (un sistema di flusso di dati tempestivo).” [1] Tipicamente gli
utilizzatori costruiscono un grafico computazionale usando uno dei linguaggi di
frontend supportati (C++ o Python). In un grafico TensorFlow, ogni nodo ha zero o più
inputs e zero o più outputs, e rappresenta l’istanziazione di un’operazione. I valori che
scorrono lungo i bordi nel grafico (da outputs a inputs) sono tensori, array
multidimensionali dove il tipo dell’elemento sottostante è specificato o dedotto alla
costruzione del grafico. “Possono esserci dei bordi speciali nel grafico detti control
dependencies: in questi non avviene flusso di dati, ma indicano che il nodo sorgente
per il controllo della dipendenza deve terminare l’esecuzione prima che il nodo
destinazione per la dipendenza di controllo inizi l’esecuzione. “ [5]
3.1 Operazioni e Kernels
Un’operazione ha un nome e rappresenta un’operazione astratta (es. “add”).
Un’operazione può avere attributi, e tutti gli attributi devono provvedere o dedurre al
momento della costruzione del grafico in ordine dall’istanziare un nodo a performare
l’operazione. Un uso comune degli attributi è di fare operazioni polimorfe su tensori
di tipi diversi (es. sommare due tensori di tipo float vs sommare due tensori di tipo
int32). Un kernel è una particolare implementazione di un’operazione che può essere
avviata in un particolare tipo di dispositivo (es. CPU o GPU). Un binario TensorFlow
definisce l’insieme delle operazioni e dei kernels disponibili tramite un meccanismo di
registrazione, e quest’insieme può essere esteso collegandolo in un’operazione
aggiuntiva e/o alle definizioni/registrazioni del kernel.
9
3.2 Sessioni
I programmi client interagiscono con il sistema TensorFlow creando una sessione. Per
creare un grafico computazionale, l’interfaccia della sessione supporta un metodo
Extend per aumentare il corrente grafico gestito dalla sessione con nodi e bordi
aggiuntivi (il grafico iniziale, quando una sessione viene creata, è vuoto). L’altra
operazione primaria supportata dall’interfaccia della sessione è Run, che prende un
insieme di nomi di output che richiedono di essere calcolati, così come un insieme
relativo a operazioni dei tensori da immettere nel grafico al posto di output certi dei
nodi. Usando gli argomenti con Run, l’implementazione di TensorFlow può calcolare
la chiusura transitiva di tutti i nodi che devono essere eseguiti in ordine per calcolare
gli output richiesti, e può poi provvedere a eseguire i nodi appropriati in un ordine
che rispetta le loro dipendenze.
10 3.3 Variabili
In molte operazioni un grafico è eseguito molte volte. La maggior parte dei tensori non
sopravvive dopo una singola esecuzione del grafico. Tuttavia, una variabile è un tipo
speciale di operazione che ritorna un puntatore a un persistente tensore mutevole che
sopravvive oltre le esecuzioni di un grafico. “I puntatori a questi persistenti tensori
mutevoli può essere passata a una manciata di operazioni speciali, come Assign e
AssignAdd (equivalente a +=) che cambia i tensori di riferimento.” [3] Per applicazioni
di Machine Learning in TensorFlow, i parametri del modello sono tipicamente
memorizzati in tensori tenuti in variabili, a sono aggiornati come una parte
dell’operazione Run del grafico di training per il modello.
11
12 4 Implementazione
I principali componenti in un sistema TensorFlow sono i client, che usano l’interfaccia
della Sessione per comunicare con il Master, e uno o più Worker processes,
responsabili di arbitrare l’accesso a uno o più dispositivi computazionali (come i core
della CPU o le card della GPU) e per eseguire i nodi del grafico su quei dispositivi
come incaricato dal master. Abbiamo sia l’implementazione locale che quella
distribuita per l’interfaccia di TensorFlow. L’implementazione locale è usata quando
il client, il master e il worker vengono eseguiti in una singola macchina nel contesto di
un singolo processo del sistema operativo (possibilmente con più dispositivi, se per
esempio, la macchina ha più GPU card installate). L’implementazione distribuita
condivide la maggior parte del codice con l’implementazione locale, ma lo estende col
supporto per un ambiente dove il client, il master e i workers possono essere tutti in
differenti processi o differenti macchine. In un sistema distribuito, questi differenti
tasks sono contenitori in compiti gestiti da un sistema di cluster scheduling.
4.1 Dispositivi
I dispositivi sono il cuore computazionale di TensorFlow. Ogni worker è responsabile
per uno o più dispositivi; ogni dispositivo ha un tipo e un nome. I nomi dei dispositivi
sono composti da parti che identificano il tipo di dispositivo, l’indice del dispositivo
nel worker, e, nel nostro ambiente distribuito, una identificazione del job e tasks del
worker (o il localhost per il caso dove i dispositivi sono locali al processo). Esempi dei
nomi
dei
dispositivi
sono
“/job:localhost/device:cpu:0”
o
“/job:worker/task:17/device:gpu:3”. Abbiamo l’implementazione dell’interfaccia del
nostro dispositivo per le CPU e GPU, e la nuova implementazione del dispositivo per
altri tipi di dispositivo possono essere fornite via meccanismo di registrazione. Ogni
oggetto del dispositivo è responsabile della gestione dell’allocazione e deallocazione
della memoria del dispositivo, e per l’organizzazione e esecuzione di qualsiasi kernel
richiesto da livelli più alti nell’implementazione di TensorFlow.
4.2 Tensori
Un tensore è un array multidimensionale tipato. Ci sono molte varietà di tipi di tensori,
13
inclusi gli interi con segno e senza segno che vanno da 8 bits a 64 bits, i tipi IEEE float
e double, un tipo di numero complesso, e un tipo string (un array di byte arbitrari). “Il
backing store della dimensione appropriata è gestito da un allocatore che è specifico
per il dispositivo dove il tensore risiede. I buffers del backing store del tensore sono
relazioni contate e sono deallocati quando non rimane nessuna relazione.” [3]
14 5 Esecuzione di un singolo dispositivo
Consideriamo lo scenario di una semplice esecuzione: un singolo processo worker con
un singolo dispositivo. I nodi del grafico sono eseguiti in un ordine che rispetta le
dipendenze tra i nodi. In particolare, teniamo traccia di un conto per nodo del numero
di dipendenze di quel nodo che non è ancora stato eseguito. Una volta che il conto
arriva a zero, il nodo è idoneo per l’esecuzione ed è aggiunto in una coda. La coda è
elaborata in ordine non specificato, delegando l’esecuzione del kernel per un nodo
all’oggetto del dispositivo. Quando un nodo termina l’esecuzione, il conto di tutti i
nodi che dipendono dal nodo completato viene decrementato.
15
16 6 Esecuzione di più dispositivi
Quando un sistema ha più dispositivi, ci sono due complicazioni principali: decidere
quale dispositivo farà il calcolo di ogni nodo nel grafico, e poi gestire la comunicazione
richiesta dei dati attraverso il termine implicito del dispositivo da queste decisioni di
collocamento.
6.1 Collocamento del nodo
Dato
un
grafico
computazionale,
una
delle
principali
responsabilità
dell’implementazione di TensorFlow è di mappare le operazioni in un insieme di
dispositivi disponibili. Un input dell’algoritmo di posizionamento è un modello di
costo, che contiene stime della grandezza (in bytes) degli input e output dei tensori per
ogni nodo del grafico, insieme con le stime del tempo di calcolo richiesto per ogni nodo
quando presentato con i suoi tensori di input. Questo modello di costo è
statisticamente stimato sulla base di euristiche associate con differenti tipi di
operazione, o è misurato sulla base di un insieme di decisioni di collocamento per le
esecuzioni precedenti del grafico. L’algoritmo di posizionamento prima esegue
un’esecuzione simulata del grafico. La simulazione finisce scegliendo un dispositivo
per ogni nodo del grafico usando le euristiche. Il nodo di posizionamento del
dispositivo generato dalla simulazione viene utilizzato nel posizionamento anche
nella reale esecuzione. L’algoritmo di posizionamento inizia con le fonti del grafico
computazionale, e simula l’attività su ogni dispositivo nel sistema come progredisce.
Per ogni nodo che viene raggiunto durante questo attraversamento, l’insieme di
dispositivi realizzabili viene considerato (un dispositivo potrebbe non essere fattibile
se il dispositivo non fornisce un kernel che implementa quella particolare operazione).
Per i nodi con più dispositivi possibili, l’algoritmo di posizionamento usa un’euristica
che esamina gli effetti sul tempo di completamento ponendo il nodo su ogni
dispositivo possibile. Quest’euristica tiene conto del tempo di esecuzione stimato o
misurato dell’operazione in quel tipo di dispositivo dal modello di costo, e include
anche i costi di ogni comunicazione che sarebbero stati introdotti in ordine degli input
trasmessi a questo nodo da altri dispositivi al dispositivo in esame. Il dispositivo dove
l’operazione del nodo dovrebbe terminare è selezionato come dispositivo di
quell’operazione, e il processo di posizionamento quindi continua poi a prendere
decisioni di posizionamento per altri nodi del grafico, inclusi i nodi per i passi
17
successivi che sono ora pronti per la loro esecuzione simulata.
6.2 Comunicazione cross-device
Una volta che è stata eseguito il posizionamento del nodo, il grafico viene diviso in un
insieme di sottografi, uno per dispositivo. Qualsiasi bordo tra dispositivi da x a y è
rimosso e rimpiazzato da un bordo da x a un nuovo nodo “Send” nel sottografo di x e
un bordo da un nodo corrispondente “Receive” a y nel sottografo di y. In fase di
esecuzione, le implementazioni dei nodi Send e Receive coordina il trasferimento dei
dati tra dispositivi. Questo ci permette di isolare tutte le comunicazioni tra le
implementazioni di Send e Receive, che semplifica il resto della fase di esecuzione.
Quando inseriamo i nodi Send e Receive, indirizziamo tutti gli utenti di un particolare
tensore su un particolare dispositivo per usare un singolo nodo Receive. Ciò che
assicura che i dati per il tensore richiesto sono trasmessi solo una volta da un
dispositivo sorgente -> coppia dispositivo destinazione, e che la memoria per il tensore
nel dispositivo destinazione è allocata una sola volta, piuttosto che più volte. Per la
gestione della comunicazione in questo modo, lasciamo lo scheduling dei nodi
individuali del grafico a dispositivi differenti per decentralizzarli nei workers: i nodi
Send e Receive distribuiscono la sincronizzazione necessaria tra i differenti workers e
dispositivi, e il master ha solo bisogno di emettere una singola richiesta Run per
l’esecuzione del grafico per ogni worker che ha tutti i nodi per il grafico, piuttosto che
essere coinvolto nello scheduling di ogni nodo o ogni comunicazione cross-device.
Questo rende il sistema molto più scalabile.
18 19
20 7 Esecuzione distribuita
L’esecuzione distribuita su un grafico è molto simile all’esecuzione tra più dispositivi.
Dopo il posizionamento del grafico, viene creato un sottografo per dispositivo. Le
coppie di nodi Send/Receive che comunicano attraverso processi worker utilizzano
meccanismi di comunicazione a distanza come TCP o RDMA per spostare dati oltre i
confini della macchina.
7.1 Tolleranza agli errori
I fallimenti in un’esecuzione distribuita possono essere rilevati in una varietà di
impieghi. I principali ai quali ci affidiamo sono un errore nella comunicazione tra la
coppia Send e Receive, e nel controllo periodico del corretto funzionamento dal
processo master a ogni processo worker. Quando viene rilevato un guasto, l’intera
esecuzione del grafico viene interrotta e riavviata da zero. Va ricordato tuttavia che i
nodi della variabile si riferiscono ai tensori che persistono attraverso le esecuzioni del
grafico. Manteniamo un checkpoint e un recupero per questo stato al riavvio. In
particolare, ogni nodo Variable è collegato ad un nodo Save. Questi nodi Save sono
eseguiti periodicamente, una volta ogni N iterazioni, o una volta ogni N secondi.
Quando sono in esecuzione, i contenuti delle variabili vengono scritti nella memoria
persistente, ad esempio, un file system distribuito. Allo stesso modo ogni Variable è
collegata a un nodo Restore che è abilitato solo nella prima iterazione dopo un riavvio.
21
22 8 Estensioni
8.1 Calcolo del gradiente
Molti algoritmi di ottimizzazione, tra cui i comuni algoritmi di training del machine
learning come la discesa del gradiente probabilistico, calcolano il gradiente di una
funzione di costo rispetto a un insieme di input. Poiché questo è un bisogno così
comune, TensorFlow è dotato di un supporto per il calcolo automatico del gradiente.
“Se un tensore C dipende da un grafico TensorFlow, forse attraverso un sottografo
complesso di operazioni, su alcuni insiemi di tensori {Xk}, allora c’è una funzione
incorporata che ritornerà i tensori {dC/dXk}.” [3] I tensori del gradiente sono calcolati,
come gli altri tensori, estendendo il grafico TensorFlow, usando la seguente procedura.
Quando TensorFlow deve calcolare il gradiente di un tensore C rispetto a qualche
tensore I in cui C dipende, esso trova prima il percorso nel calcolo del grafico da I a C;
Poi torna indietro da C a I, e per ogni operazione sul percorso all’indietro aggiunge un
nodo al grafico TensorFlow, componendo i gradienti parziali lungo il percorso a
ritroso con la regola della catena. Il nuovo nodo aggiunto calcola la funzione gradiente
per l’operazione corrispondente nel percorso in avanti. Una funzione gradiente può
essere registrata da qualsiasi operazione. Questa funzione prende come input non solo
i gradienti parziali calcolati già lungo il percorso a ritroso, ma anche, eventualmente,
gli input e output del calcolo in avanti. La figura 5 mostra i gradienti per un costo
calcolato dall’esempio della figura 2. Le frecce grigie mostrano input potenziali a
funzioni del gradiente che non vengono utilizzati per le particolari operazioni
mostrate. L’aggiunta necesseria in figura 1 per calcolare questi gradienti è:
In generale un’operazione può avere più output, e C può dipendere solo da alcuni di
essi. “Se, per esempio, l’operazione O ha due output y1 e y2, e C dipende solo da y2,
allora il primo input della funzione gradiente di O è l’insieme da O a dC/dy1 = 0.” [3]
Il calcolo automatico del gradiente complica l’ottimizzazione, in particolare l’uso della
memoria. Quando eseguiamo l’operazione del sottografo in avanti, vale a dire, quelli
costruiti esplicitamente dall’utente, un’euristica sensibile rompe i legami al momento
di decidere quale nodo eseguire successivamente osservando l’ordine in cui è stato
costruito il grafico. Ciò significa che le uscite temporanee sono consumate subito dopo
23
la costruzione, per cui la loro memoria può essere riutilizzata rapidamente. Quando
l’euristica è inefficace, l’utente può cambiare l’ordine di costruzione del grafico, o
aggiungere dipendenze di controllo. Quando i nodi del gradiente sono
automaticamente aggiunti al grafico, l’utente ha meno controllo, e le euristiche
possono abbattersi. In particolare, poiché i gradienti invertono l’ordine di calcolo in
avanti, i tensori utilizzati nelle prime fasi dell’esecuzione del grafico sono spesso
nuovamente necessari verso la fine del calcolo del gradiente. Tali tensori possono
tenere su un sacco di memoria scarsa della GPU e limitare inutilmente la dimensione
delle operazioni.
8.2 Esecuzione parziale
Spesso un client vuole eseguire solo un sottografo dell’intero grafico di esecuzione. A
sostegno di ciò, una volta che il cliente ha creato il grafico in una sessione, il metodo
Run permette di eseguire un sottografo arbitrario dell’intero grafico, e di iniettare dati
arbitrari su un qualsiasi bordo del grafico. Ogni nodo nel grafico ha un nome, e ciascun
output di un nodo è identificato dal nome del nodo sorgente e la porta di output del
nodo, numerata da 0 (es. “bar:0” si riferisci al primo output del nodo “bar”, mentre
“bar:1” si riferisce al secondo output). I due argomenti della chiamata Run aiutano a
definire l’esatto sottografo del grafico di calcolo che verrà eseguito. Per prima cosa, la
chiamata Run accetta gli input, una mappatura opzionale dei nomi name:port ai valori
dei tensori “alimentati”. Poi, la chiamata Run accetta output_names, una lista di
specificazioni di output name[:port] indicando quali nodi dovrebbero essere eseguiti,
e, se la porzione di porta è presente in un nome, quel particolare valore del tensore di
output per il nodo dovrebbe ritornare al client se la chiamata Run conclude con
successo. Il grafico si trasforma in base ai valori di input e output.
“Ogni node:port specificato in input è sostituito con un “feed node”, che prenderà il
previsto tensore di input da accessi appositamente inizializzati in un oggetto
Rendezvous utilizzato per la chiamata Run.” [3] Analogamente, ciascun nome di
output di una porta è collegata ad un nodo speciale fetch che organizza per salvare il
tensore di uscita e restituirlo al client quando la chiamata Run è completa.
Infine, una volta che il grafico è stato riscritto con l’inserimento di questi nodi speciali
di feed e fetch, l’insieme dei nodi di esecuzione può essere determinata a partire da
ciascuno dei nodi denominati da qualunque output e lavorando a ritroso nel grafico
24 usando le dipendenze del grafico per determinare l’intero insieme di nodi che devono
essere eseguiti nel grafico riscritto in ordine per calcolare gli output. “La figura 6
mostra un grafico originale a sinistra e a fianco il grafico trasformato quando viene
invocata Run con iputs=={b} e outputs=={f:0}.” [3] Dal momento che abbiamo solo
bisogno di calcolare l’output del nodo f, non eseguiremo i nodi d ed e, poiché non
hanno alcun contributo all’output di f.
8.3 Vincoli di dispositivo
Gli utilizzatori di TensorFlow possono controllare il piazzamento dei nodi nei
dispositivi tramite vincoli parziali per un nodo su quali dispositivi può essere eseguito.
Per esempio “piazzare il nodo solamente in un dispositivo di tipo GPU”, oppure
“questo nodo può essere piazzato su qualsiasi dispositivo in /job:worker/task:17”, o
“collocare questo nodo con il nodo chiamato variable13”. Entro i confini di questi
vincoli, l’algoritmo di posizionamento è responsabile per la scelta dell’assegnazione di
nodi ai dispositivi ai quali fornisce una rapida esecuzione di calcolo e soddisfa anche
vari vincoli imposti dai dispositivi stessi, ad esempio limitando la quantità totale di
memoria necessaria su un dispositivo per eseguire il suo sottoinsieme di nodi del
grafico. Tenere tali vincoli richiede modifiche all’algoritmo di posizionamento.
Prima abbiamo calcolato la serie possibile di dispositivi per ogni nodo, e quindi
usiamo la union-find sul grafico sulla collocazione di vincoli per calcolare i
componenti del grafico che devono essere messi insieme. Per ciascuno di questi
25
componenti, calcoliamo l’intersezione di un insieme di dispositivi fattibili. L’insieme
di dispositivi possibili calcolati per nodo si adatta facilmente al simulatore
dell’algoritmo di posizionamento.
8.4 Flusso di controllo
Anche se i grafici a flusso di dati senza alcun esplicito controllo del flusso sono
abbastanza significativi, abbiamo osservato un numero di casi in cui il supporto di
istruzioni condizionali e cicli può portare a rappresentazioni più concise ed efficienti
di algoritmi di apprendimento automatico. In TensorFlow viene introdotto un piccolo
insieme di controllo primitivo di operatori di flusso e viene generalizzato TensorFlow
per gestire grafici a flusso di dati ciclici. “Gli operatori Switch e Merge ci permettono
di saltare l’esecuzione di un intero sottografo in base ai valori di un tensore booleano.
Gli operatori Enter, Leave e NextIteration permettono di esprimere l’iterazione. I
costrutti della programmazione ad alto livello come if e while possono essere
facilmente compilati nei grafici a flusso di dati con questi operatori di controllo del
flusso. Il runtime TensorFlow implementa un concetto di tags e frames
concettualmente simile alla macchina MIT TaggedToken.” [3] Ciascuna iterazione di
un loop è unicamente identificata da un tag, e il suo stato di esecuzione è rappresentato
da un frame. Un input può entrare in un’iterazione ogni volta che è disponibile;
pertanto, più iterazioni possono essere eseguite in concomitanza. TensorFlow utilizza
un meccanismo di coordinazione distribuita per eseguire grafici con flusso di
controllo. In generale, un loop può contenere nodi che sono assegnati a molti
dispositivi differenti. Pertanto, gestire lo stato di un loop diventa un problema. La
soluzione di TensorFlow si basa sulla riscrittura del grafico. Durante il
partizionamento del grafico, vengono aggiunti automaticamente nodi di controllo per
ogni partizione. Questi nodi implementano una piccola macchina a stati che orchestra
l’inizio e la fine di ogni iterazione, e decide la fine del loop. Per ogni iterazione, il
dispositivo che possiede il predicato di terminazione del loop invia un piccolo
messaggio di controllo per ogni dispositivo partecipante. Quando un modello
comprende le operazioni di controllo del flusso, dobbiamo tenere conto di queste per
il calcolo del gradiente corrispondente. Per esempio, il calcolo del gradiente per un
modello con un if condizionale avrà bisogno di sapere quale ramo dell’istruzione
condizionale è stato preso, quindi applica la logica del gradiente a questo ramo. Allo
stesso modo, il calcolo del gradiente per un modello con un ciclo while avrà bisogno
26 di sapere quante iterazioni impiegare, e farà anche affidamento sui valori intermedi
calcolati in quelle iterazioni. La tecnica di base è riscrivere il grafico in modo da
memorizzare i valori necessari per il calcolo del gradiente.
8.5 Operazioni di input
Anche se i dati in input possono essere forniti a un calcolo tramite i nodi di feed, un
altro meccanismo comune utilizzato per i modelli di apprendimento automatico su
larga scala è quello di avere particolari nodi di calcolo dell’input, che in genere sono
configurati con una serie di filenames e che producono un tensore contenente uno o
più esempi dai dati memorizzati in quell’insieme di files ogni volta che vengono
eseguiti. Ciò consente ai dati di essere letti direttamente dal sistema di storage
sottostante nella memoria della macchina che eseguirà la successiva elaborazione dei
dati. Nelle configurazioni dove il client è separato dal processo worker, se i dati erano
elaborati, tipicamente richiede un salto di rete in più (dal sistema di storage per il client
e poi dal client al worker vs. direttamente dal sistema di storage al worker quando si
utilizzata un nodo di input).
8.6 Code
Le code sono una caratteristica utile in TensorFlow. Esse consentono di eseguire in
modo asincrono diverse parti del grafico, possibilmente a differenti ritmi, e di portare
fuori dati attraverso operazioni di Enqueue e Dequeue. Operazioni di Enqueue
possono bloccare fino a quando lo spazio diventa disponibile nella coda, e le
operazioni di Dequeue possono bloccare fino a quando un numero minimo di elementi
è disponibile nella coda. Un uso delle code è quello di consentire i dati di input da
precaricare a files su disco mentre una serie precedente di dati sono ancora in fase di
elaborazione dalla porzione di calcolo di un modello di apprendimento automatico.
Possono essere utilizzate anche per altri tipi di raggruppamento, compresi
l’accumulare più gradienti in ordine per calcolare alcune più complesse combinazioni
di gradienti su una quantità più grande, o per raggruppare differenti frasi di input per
modelli di linguaggio ricorrenti in contenitori di frasi che sono approssimativamente
della stessa lunghezza, che possono essere processati in modo più efficiente. Oltre alle
normali code FIFO, è stata implementata anche una coda di rimescolamento, che
27
mescola in modo casuale gli elementi all’interno di un buffer di grandi dimensioni in
memoria. La funzione di rimescolamento casuale è utile per gli algoritmi di
apprendimento automatico.
8.7 Contenitori
Un contenitore è il meccanismo in TensorFlow per la gestione dello stato mutevole che
dura di più. L’archivio di backup per una variabile vive in un contenitore. Il
contenitore di default è quello che persiste fino a quando il processo termina. Un
contenitore può essere azzerato cancellando tutto il suo contenuto. Utilizzando i
contenitori è possibile condividere lo stato attraverso grafici di calcolo completamente
disgiunti associati a differenti sessioni.
28 9 Ottimizzazione
9.1 Eliminazione della sotto espressione comune
Poiché la costruzione dei grafici di calcolo è spesso fatta da molti differenti strati di
astrazione nel codice client, il grafico di calcolo può facilmente finire con copie
ridondanti dello stesso calcolo. Per gestire questa situazione, è stata implementata una
sotto espressione comune che va sopra il grafico di calcolo e canonizza più copie di
operazioni con input identici e tipi di operazioni a solamente un nodo dei nodi, e
rindirizza i bordi del grafico in modo appropriato per riflettere questa canonizzazione.
9.2 Controllo della comunicazione dei dati e utilizzo della memoria
Lo scheduling accurato delle operazioni in TensorFlow può risultare in una migliore
performance del sistema, in particolare per quanto riguarda i trasferimenti di dati e
l’utilizzo della memoria. In particolare, lo scheduling può ridurre il tempo durante il
quale i risultati intermedi devono essere tenuti in memoria tra operazioni e quindi il
consumo del picco di memoria. Questa riduzione è particolarmente importante per
dispositivi GPU dove la memoria è scarsa. Inoltre, orchestrando la comunicazione di
dati tra dispositivi in grado di ridurre la contesa per le risorse di rete. Mentre ci sono
molte opportunità per l’ottimizzazione dello scheduling, ci concentriamo su ciò che è
particolarmente necessario ed efficace. Riguarda lo scheduling dei nodi Receive per
leggere valori remoti. Se non si sono prese precauzioni, questi nodi possono iniziare
molto prima del necessario, possibilmente tutti in una volta quando parte l’esecuzione.
Eseguendo il calcolo ASAP/ALAP (as-soon-as-possible/as-late-as-possibile), del tipo
comune nella ricerca delle operazioni, analizziamo i percorsi critici dei grafici, al fine
di stimare quando partono i nodi Receive. Vengono inseriti dei bordi di controllo con
l’obiettivo di ritardare l’inizio di questi nodi fino a poco prima che i loro risultati sono
necessari.
9.3 Kernel asincroni
Oltre ai normali kernel sincroni che completano la loro esecuzione alla fine del metodo
Compute, sono supportati anche kernel non bloccanti. Come i kernel non bloccanti
29
utilizzano un’interfaccia leggermente diversa per cui il metodo Compute è passato in
seguito e invocato quando l’esecuzione del kernel è completo. Si tratta di una
ottimizzazione per gli ambienti in cui avere molti thread attivi è relativamente costoso
in termini di utilizzo della memoria o di altre risorse e ci permettere di evitare di legare
un thread di esecuzione per periodi illimitati di tempo in attesa di I/O o altri eventi che
si verifichino. Esempi di kernels includono il kernel Receive, e i kernels Enqueue e
Dequeue (che potrebbe essere necessario bloccare se lo spazio della coda non è
disponibile o se non sono disponibili dati da leggere, rispettivamente).
9.4 Librerie ottimizzate per le implementazioni del kernel
“Si fa uso di preesistenti librerie numeriche ottimizzate per implementare i kernel per
alcune operazioni. Per esempio, ci sono una serie di librerie ottimizzate per eseguire
matrici moltiplicate in differenti dispositivi, inclusi BLAS e cuBLAS, o librerie GPU
per i kernel convoluzionali per reti neurali profonde come CUDA-convnet e cuDNN.
Molte implementazioni del kernel sono relativamente sottili involucri intorno a tali
librerie ottimizzate. Si fa uso della libreria dell’algebra lineare open-source Elgen per
molte implementazioni del kernel nel sistema. Come una parte dello sviluppo di
TensorFlow, il team di sviluppo ha esteso la libreria Elgen con il supporto per le
operazioni sui tensori di dimensione arbitraria.” [3]
9.5 Compressione con perdita
Alcuni algoritmi di apprendimento automatico, compresi quelli tipicamente utilizzati
per la formazione di reti neurali, sono tolleranti al rumore e la precisione aritmetica
ridotta. In modo analogo al sistema DistBelief, che utilizza spesso la compressione con
perdita di rappresentazioni interne con maggiore precisione per l’invio di dati tra
dispositivi (a volte all’interno della stessa macchina, ma soprattutto attraverso i confini
della macchina). Per esempio, spesso vengono inserite conversioni speciali dei nodi
che convertono le rappresentazioni in virgola mobile a 32 bit in rappresentazioni in
virgola mobile a 16 bit (non il proposto standard IEEE 16 bit floating point standard,
ma piuttosto un formato 32 bit IEEE 794 float, ma con 16 bits con meno precisione nella
mantissa), e poi riconvertire una rappresentazione a 32 bit sull’altro lato del canale di
comunicazione (semplicemente compilando in zeri per la porzione perduta della
mantissa, dato che è meno costoso computazionalmente che fare l’arrotondamento
30 probabilistico matematicamente corretto quando si da questa conversione 32 -> 16 ->
32 bit).
31
32 10 Stato ed esperienza
L’interfaccia di TensorFlow e un’implementazione di riferimento sono open source
sotto la licenza Apache 2.0, e il sistema è disponibile per il download a
www.tensorflow.org. Il sistema include documentazione dettagliata, un numero di
tutorials, e un numero di esempi che dimostra come utilizzare il sistema per una
varietà di diversi tasks di apprendimento automatico. Il sistema include front-ends per
la specifica dei calcoli TensorFlow in Python e C++. Ci sono un bel paio di modelli di
apprendimento automatico in DistBelief che sono stati migrati in TensorFlow. Quindi,
in questa sezione, tratteremo lezioni imparate generalizzabili per la migrazione di
modelli di apprendimento automatico da un sistema all’altro. In particolare vedremo
Inception (rete neurale convenzionale per il riconoscimento di immagini). Questo
sistema di riconoscimento di immagini classifica immagini da 224x224 pixel in
un'unica su 1000 etichette (es. “camion della nettezza urbana” etc…). Tale modello
comprende 13,6 milioni di parametri apprendibili e 36,000 operazioni espresse come
grafico TensorFlow. L’esecuzione di inferenza su una singola immagine richiede 2
miliardi di operazioni di “multiply-add” (moltiplicazione dei primi due inputs, a e b,
e somma del risultato col terzo, c). Dopo la costruzione di tutte le operazioni
matematiche in TensorFlow, l’assemblaggio e il debug di tutte le 36,000 operazioni
nella struttura corretta del grafico risulta problematico. Convalidare la correttezza è
un’impresa difficile perché il sistema è intrinsicamente stocastico e destinato a
comportarsi in un certo modo in attesa – potenzialmente dopo due ore di calcoli. Date
queste circostanze ci sono delle strategie fondamentali per il porting del modello
Inception in TensorFlow:
1 – Costruire strumenti al fine di conoscere il numero esatto di parametri in un determinato
modello.
Tali strumenti hanno dimostrato sottili difetti in una complessa architettura di rete
specifica. In particolare si è riusciti a individuare le operazioni e le variabili istanziate
in modo non corretto.
2 – Partire dal basso e scalare.
La prima rete neurale convoluzionale portata dal precedente sistema era una piccola
rete impiegata sul set di dati CIFAR-10.
33
3 – Assicurarsi sempre che l’obiettivo (funzione loss) corrisponde tra sistemi di apprendimento
automatico quando l’apprendimento è spento.
Impostando la velocità di apprendimento a zero ha aiutato a identificare un
comportamento imprevisto nel modo in cui erano inizializzate a random le variabili
in un modello. Tale errore sarebbe stato difficile identificarlo in una rete di training
dinamica.
4 – Fare un match dell’implementazione di una singola macchina prima di debuggare
un’implementazione distribuita.
Questa strategia ha aiutato a delineare e debuggare le discrepanze. Nelle performance
di training tra sistemi di apprendimento automatico. In particolare, sono stati
identificati i bugs.
5 – Guardia contro errori numerici.
Le librerie numeriche sono incoerenti nel modo in cui gestiscono i valori in virgola
mobile. Le reti neurali convoluzionali sono particolarmente suscettibili all’instabilità
numerica e tenderà a divergere abbastanza regolarmente durante le fasi di
sperimentazione e di debug. A guardia contro questo comportamento, controllando i
valori in virgola mobile, permette di rilevare gli errori in tempo reale rispetto a
identificare il comportamento divergente post-hoc.
6 – Analizzare pezzi di una rete e capire la grandezza dell’errore numerico.
L’esecuzione di sottosezioni della rete neurale in parallelo su due sistemi di
apprendimento automatico fornisce un metodo preciso per garantire che un algoritmo
numerico è identico per due sistemi. Dato che tali algoritmi eseguono con precisione
in virgola mobile, è importante predirre e capire la grandezza dell’errore numerico
previsto al fine di giudicare se un dato componente è correttamente implementato.
Convalidare complesse operazioni matematiche in presenza di un sistema stocastico è
molto impegnativo. Le strategie delineate in precedenza hanno dimostrato di rivelarsi
preziose nel guadagnare fiducia nel sistema e infine nell’istanziare il modello
Inception in TensorFlow. Il risultato finale di questi sforzi ha determinato un
34 miglioramento della velocità di 6 volte in tempi di training vs l’implementazione
esistente DistBelief del modello e tali guadagni di velocità si sono dimostrati
indispensabili nella formazione di una nuova classe di modelli di riconoscimento di
immagini su larga scala.
35
36 11 Idiomi di programmazione comuni
Il modello di grafico a flusso di base di dati di TensorFlow può essere utilizzato in una
varietà di modi per applicazioni di apprendimento automatico. Un dominio che
interessa è accelerare la formazione di modelli computazionalmente intensivi di reti
neurali su grandi datasets. In questa sezione si descrivono tecniche realizzate dal team
TensorFlow e altri al fine di realizzare ciò, e illustra come utilizzare TensorFlow per
realizzare questi diversi approcci. Gli approcci in questa sottosezione assumono che il
modello è stato addestrato con discesa del gradiente stocastico (SGD) con mini-lotti
relativamente di modeste dimensioni da 100 a 1000 esempi.
11.1 Training di dati parallelo
“Una semplice tecnica per accellerare SGD è di parallelizzare il calcolo del gradiente
per un mini-lotto attraverso elementi del mini-lotto. Per esempio, se stiamo
utilizzando una dimensione di mini-lotto di 1000 elementi, possiamo usare 10 repliche
del modello per ogni calcolo del gradiente per 100 elementi, e poi combina i gradienti
e applica aggiornamenti ai parametri in modo sincrono, al fine di comportarsi
esattamente come se stessimo eseguendo l’algoritmo sequenziale SGD con una
grandezza del lotto di 1000 elementi. In questo caso, il grafico TensorFlow ha
semplicemente molte repliche della porzione del grafico che fa la quantità del modello
di calcolo, e un singolo thread client guida l’intero ciclo di training di questo grande
grafico. Ciò è illustrato nella porzione superiore della figura 7.” [3]
37
Questo approccio può essere fatto anche asincrono, dove il grafico TensorFlow ha
molte repliche della porzione del grafico che fa la quantità del modello di calcolo, e
ciascuna di queste repliche applica anche gli aggiornamenti dei parametri ai parametri
del modello in modo asincrono. In questa configurazione, c’è un thread client per
ciascuna delle repliche del grafico. Questo è illustrato nella parte inferiore della figura
7.
11.2 Modello di training parallelo
Il modello di training parallelo, dove diverse porzioni del modello di calcolo sono fatti
su diversi dispositivi computazionali contemporaneamente per lo stesso lotto di
esempi; è facile da esprimere in TensorFlow. La figura 8 mostra un esempio di un
ricorrente, modello profondo LSTM usato per apprendimento sequenza per sequenza,
parallelizzata in tre dispositivi differenti.
38 11.3 Passi simultanei per il modello di calcolo Pipelining
Un altro modo comune per ottenere un migliore utilizzo per il training delle reti
neurali profonde è quello di pipeline il calcolo del modello entro gli stessi dispositivi,
eseguendo un piccolo numero di passi simultanei all’interno dello stesso insieme di
dispositivi. Questo è mostrato in figura 9.
È in qualche modo simile al parallelismo asincrono dei dati, eccetto che il parallelismo
avviene all’interno dello stesso dispositivo/dispositivi, piuttosto che replicare il grafico
di calcolo su diversi dispositivi. Questo permette di “riempire i vuoti” in cui il calcolo
39
di un singolo lotto di esempi potrebbe non essere in grado di utilizzare pienamente il
parallelismo totale su tutti i dispositivi in ogni momento durante un singolo passo.
40 12 Tools
12.1 TensorBoard: Visualizzazione delle strutture del grafico e statistiche
riassuntive
Al fine di aiutare gli utenti a comprendere la struttura dei loro grafici di calcolo e anche
per capire il comportamento generale di modelli di apprendimento automatico, è stato
costruito TensorBoard, uno strumento di visualizzazione per TensorFlow.
12.1.1 Visualizzazione del grafico di calcolo
Molti dei grafici di calcolo di reti neurali profonde possono essere molto complessi.
Ad esempio, il grafico di calcolo per il training di un modello simile al modello
Inception di Google, una profonda rete neurale convoluzionale che ha avuto “the best
classification performance” nel contest ImageNet 2014, ha più di 36.000 nodi nel suo
grafico di calcolo TensorFlow e alcuni modelli profondi ricorrenti LSTM per il
modellismo del linguaggio ha più di 15.000 nodi. A causa delle dimensioni e della
topologia di questi grafici le ingenue tecniche di visualizzazione producono
diagrammi disordinati. Per aiutare gli utenti a vedere la sottostante organizzazione
dei grafici, gli algoritmi in TensorBoard collassano i nodi in blocchi di alto livello,
mettendo in evidenza gruppi con strutture identiche. Il sistema separa anche i nodi di
alto grado, che spesso svolgono funzioni di compatibilità in una zona separata dello
schermo. In questo modo si riduce l’ingombro visivo e si focalizza l’attenzione sulle
sezioni principali del grafico di calcolo. L’intera visualizzazione è interattiva: gli utenti
possono fare una panoramica, zoomare e espandere i nodi raggruppati scavando a
fondo per i dettagli. Un esempio di visualizzazione per il grafico di un’immagine del
modello convulazionale profondo è mostrato in figura 10.
41
12.1.2 Visualizzazione dei dati di sintesi
Quando si formano modelli di apprendimento automatico, gli utenti spesso vogliono
essere in grado di esaminare lo stato dei vari aspetti del modello, e come questo i vari
cambiamenti di stato nel corso del tempo. A tal fine, TensorFlow supporta una raccolta
di diverse operazioni di sintesi che possono essere inserite nel grafico, compresi
riassunti scalari (ad esempio, per esaminare le proprietà generali del modello, come il
valore della funzione loss mediati attraverso una raccolta di esempi, o il tempo
necessario per eseguire il grafico di calcolo), riassunti basati su istogrammi (ad
esempio, la distribuzione dei valori di peso in un livello di rete neurale), o riassunti
basati su immagini (ad esempio, una visualizzazione dei pesi dei filtri appresi in una
rete neurale convoluzionale). Tipicamente i grafici di calcolo sono impostati in modo
che i nodi di sintesi vengano inclusi per monitorare vari valori interessanti, e ogni tanto
durante l’esecuzione del grafico di training, l’insieme dei nodi di sintesi vengono
eseguiti, in aggiunta alla normale serie di nodi che vengono eseguiti, e il programma
driver del client scrive i dati di sintesi in un file log associato con il training del
modello. Il programma TensorBoard è quindi configurato per vedere questo file log
per i nuovi record di sintesi, e in grado di visualizzare le informazioni di riepilogo e
come cambia nel tempo (con la possibilità di selezionare la misura del “tempo” per
42 essere relativo al “wall time” dall’inizio dell’esecuzione del programma TensorFlow,
tempo assoluto, o “steps”, una misura numerica del numero di esecuzioni del grafico
che si sono verificate dopo l’inizio dell’esecuzione del programma TensorFlow). Una
schermata di visualizzazione dei valori di sintesi in TensorBoard è mostrata in figura
11.
43
44 13 Tracciato di esecuzione
C’è uno strumento interno chiamato EEG (non incluso nella versione open source
iniziale del mese di novembre, 2015) usato per raccogliere e visualizzare le
informazioni a grana molto fine su l’ordinamento e le caratteristiche di esecuzione dei
grafici TensorFlow. Questo strumento funziona sia nelle singole macchine che nelle
implementazioni distribuite ed è molto utile per comprendere i colli di bottiglia nei
modelli di calcolo e di comunicazione di un programma TensorFlow. “Le tracce
vengono raccolte simultaneamente su ogni macchina nel sistema da una varietà di
fonti, tra cui il kernel Linux ftrace, gli strumenti di tracciamento e CUPTI. Con questi
registri possiamo ricostruire l’esecuzione di un passo di formazione distribuito con
dettagli a livello di microsecondo di ogni thread-switch, il lancio del kernel CUDA e il
funzionamento DMA.” [3] Le tracce sono combinate in un server di visualizzazione
che è destinato per estrarre rapidamente eventi in una determinato TimeRange e di
sintetizzare a adeguato livello di dettaglio la risoluzione di interfaccia utente.
Eventuali ritardi significativi a causa di comunicazione, sincronizzazione o gli stalli di
DMA correlato sono identificate e evidenziate con frecce nella visualizzazione.
Inizialmente l’interfaccia utente fornisce una panoramica dell’intera traccia con solo le
più significanti prestazioni evidenziate. Come l’utente zooma progressivamente, sono
resi sempre più dettagli. La figura 12 mostra una visualizzazione EEG di un modello
che viene formato in una piattaforma CPU multi-core.
45
La parte superiore della schermata mostra le operazioni in TensorFlow spedite in
parallelo, secondo i vincoli di flusso di dati. La sezione inferiore del tracciato mostra
come la maggior parte delle operazioni sono decomposte in più elementi di lavoro
eseguiti contemporaneamente in un pool di thread. La figura 13 mostra un’altra
visualizzazione EEG dove il calcolo avviene principalmente sulla CPU. I thread
dell’host possono essere visti come operazioni TensorFlow della GPU di accodamento
man mano che diventano eseguibili (la luce blu dei thread pool, cioè insieme di risorse
che possono essere riutilizzate), e thread di pulizia in background possono essere visti
in altri colori in fase di migrazione attraverso core del processore. Ancora una volta, le
frecce indicano dove i thread sono in stallo su GPU per i trasferimenti della CPU, o
dove gli ops sperimentano un significativo ritardo di code.
Infine, la figura 14 mostra una vista più dettagliata che ci permette di esaminare come
gli operatori TensorFlow della GPU vengono assegnati a più flussi della GPU. Ogni
volta che il grafico dataflow consente l’esecuzione parallela o il trasferimento di dati
si cerca di esporre i vincoli di ordinazione al dispositivo GPU utilizzando flussi e
primitive di dipendenza del flusso.
46 47
48 14 Esempi
È un sistema di programmazione dove i calcoli vengono rappresentati da grafici. I nodi
del grafico sono chiamati ops (abbreviazione di “operations”). Un op prende zero o
più Tensors (tensori), effettua i calcoli, e produce zero o più Tensors. Un tensore è un
tipo di array multi-dimensionale. Un grafico in TensorFlow è una descrizione di
calcoli. Per il calcolo, il grafico dev’essere lanciato in una Session (sessione). Una
Session piazza gli ops del grafico in dispositivi e provvede metodi per eseguirli. I
programmi TensorFlow sono sempre strutturati in una fase di costruzione, che
assembla un grafico, e una fase di esecuzione che usa una sessione per eseguire gli ops
nel grafico. TensorFlow può essere usato in programmi in C, C++ e Python. Per lanciare
un grafico occorre creare un oggetto Session. Senza argomenti il costruttore della
sessione lancia il grafico di default. Il linguaggio utilizzato negli esempi sottostanti è
Python con annesse librerie.
14.1 Hello,Tensorflow!
Qui di seguito, il classico “Hello World” in TensorFlow:
49
Inizialmente si importa l’API, si crea la costante (op), cioè il nodo che viene aggiunto
al grafico di default. Successivamente si fa partire la sessione e infine si avvia il grafico.
14.2 Semplicioperazioni
14.2.1 Moltiplicazionetraduevariabili
Qui, invece vi è una moltiplicazione tra due variabili di tipo float. Si creano le variabili
meta sintattiche di tipo float e la variabile risultante dalla moltiplicazione delle due
tramite l’operazione mul. Infine si crea una sessione per valutare l’espressione
simbolica e quindi poter dare come output il risultato della moltiplicazione con
differenti valori tramite il costrutto sess.run, che fa partire la sessione, e quindi svolge
l’espressione con i parametri per a e b assegnati tramite il costrutto feed_dict.
14.3 RegressioneLineare
50 “La regressione lineare è una tecnica statistica che misura la relazione tra le variabili.
Nel caso di due variabili (regressione semplice) e il caso di più di due variabili
(regressione multipla), la regressione lineare modella la relazione tra una variabile
dipendente, variabili indipendenti xi e un termine casuale b. In questo caso la
51
regressione lineare è semplice, cioè y = W * x + b.” [4] Prima di tutto, importiamo il
package Numpy che useremo per generare punti. Abbiamo punti generati dal
rapporto y = 0.1 * x + 0.3 , anche se con qualche variazione, con una distribuzione
normale, in modo che i punti non corrispondano completamente a una linea.
Il punto successivo è quello di formare l’algoritmo per ottenere i valori di uscita y,
definiti sulla base dei dati x_data. In questo caso, sappiamo già da prima che è una
regressione lineare, possiamo rappresentare il modello con solo due parametri: W e b.
L’obiettivo è di generare un codice TensorFlow che permette di individuare i migliori
parametri W e b, che dai dati di input x_data, aggiunti ai dati di output y_data, nel
nostro caso sarà una linea retta definita da y_data = W * x_data + b. Il lettore sa che W
dovrebbe essere vicino a 0.1 e b per 0.3, ma TensorFlow non lo sa e deve realizzarlo da
sé. Un metodo standard per risolvere tali problemi è quello di far scorrere ogni valore
della serie di dati e modificare i parametri W e b al fine di ottenere una risposta più
precisa ogni volta. “Per scoprire se stiamo migliorando in queste iterazioni, definiamo
una funzione di costo che misura quanto “buona” una certa linea è. Questa funzione
riceve la coppia W e come parametri b e restituisce un valore di errore in base a quanto
bene la linea si adatta ai dati. Con l’errore quadratico medio otteniamo la media degli
“errori” in base alla distanza tra il valore reale e quello stimato in ogni iterazione
dell’algoritmo. Con le variabili definite, possiamo esprimere la funzione di costo in
base alla distanza tra ciascun punto e il punto calcolato con la funzione y = W * x + b.
Dopo di che, si può calcolare il quadrato e la media della somma.” [2] Quindi usiamo
un’espressione che calcola la media delle distanze quadrate tra il punto y_data che
conosciamo e il punto y calcolato dall’input x_data. Se riduciamo al minimo la
funzione di errore troveremo il modello migliore per i nostri dati. L’algoritmo di
discesa lungo il gradiente è un algoritmo che data una funzione definita da una serie
di parametri, si inizia con una prima serie di valori di parametri e iterativamente
muove verso un insieme di valori che minimizzano la funzione. È convenzionale
quadrare la distanza per garantire che sia positivo e per rendere la funzione di errore
derivabile per calcolare il gradiente. L’algoritmo inizia con i valori iniziali di un
insieme di parametri (nel nostro caso W e b), e quindi l’algoritmo regola iterativamente
il valore di tali variabili in modo che, alla fine del processo, i valori delle variabili
minimizzano la funzione di costo. Poi, iniziamo il processo iterativo che ci permetterà
di trovare i valori di W e b sino a che il modello raggiunge un modello di precisione
desiderato sui dati. Possiamo vedere la linea definita dai parametri W = 0.0854 e b =
0.299 con otto iterazioni:
52 53
54 15 Conclusioni
Abbiamo descritto TensorFlow, un modello di programmazione sensibile basato sul
flusso di dati, sia su singola macchina che in implementazioni distribuite di questo
modello di programmazione. Il sistema è utilizzato nell’attività di ricerca e
distribuzione di più di cento progetti di apprendimento automatico per una vasta
gamma di prodotti e servizi Google. C’è una versione Open Source di TensorFlow e si
spera che il numero di utilizzatori cresca e contribuisca sempre di più allo sviluppo.
55
56 Ringraziamenti
Ringraziomiamadre,miopadre,Fabio,Valeria,Pierpaolo,ManueleeRosina.
57
58 Riferimenti bibliografici
[1] JordiTorres,“FirstcontactwithTensorFlow–Part1:Basics”,10/02/2016,
http://www.jorditorres.org/wpcontent/uploads/2016/02/FirstContactWithTensorFlow.part1_.pdf,16/10/2016
[2] JordiTorres,“FirstcontactwithTensorFlow”,10/02/2016,
http://www.jorditorres.org/first-contact-with-tensorflow/,29/10/2016
[3] Mart´ınAbadi,AshishAgarwal,PaulBarham,EugeneBrevdo,ZhifengChen,CraigCitro,
GregS.Corrado,AndyDavis,JeffreyDean,MatthieuDevin,SanjayGhemawat,Ian
Goodfellow,AndrewHarp,GeoffreyIrving,MichaelIsard,YangqingJia,RafalJozefowicz,
LukaszKaiser,ManjunathKudlur,JoshLevenberg,DanMane,RajatMonga,Sherry
Moore,DerekMurray,´ChrisOlah,MikeSchuster,JonathonShlens,BenoitSteiner,Ilya
Sutskever,KunalTalwar,PaulTucker,VincentVanhoucke,VijayVasudevan,Fernanda
Viegas,OriolVinyals,´PeteWarden,MartinWattenberg,MartinWicke,YuanYu,and
XiaoqiangZheng,“TensorFlow:Large-ScaleMachineLearningonHeterogeneous
DistributedSystems”,09/11/2015,
http://download.tensorflow.org/paper/whitepaper2015.pdf,20/10/2016
[4] S.n,“IntroductiontoTensorFlow”,2015,http://flarecast.eu/wpcontent/uploads/2015/04/TensoFlow.pdf,17/10/2016
[5] TensorFlowTM,“BasicUsage”,09/11/2015,
https://www.tensorflow.org/versions/r0.11/get_started/basic_usage.html,15/10/2016
[6] Wikipedia,“Apprendimentoautomatico”,06/09/2016,
https://it.wikipedia.org/wiki/Apprendimento_automatico,25/10/2016
[7] Wikipedia,“Apprendimentoapprofondito”,06/10/2016,
https://it.wikipedia.org/wiki/Apprendimento_approfondito,26/10/2016
[8] Wikipedia,“IntelligenzaArtificiale”,04/11/2016,
https://it.wikipedia.org/wiki/Intelligenza_artificiale,04/11/2016
59