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