Regressione Logistica con Theano per il Deep Learning

Regressione Logistica con Theano – Deep Learning

In questa lezione descriviamo come implementare la regressione logistica con Theano, una libreria Python per implementare modelli di Deep Learning. Negli ultimi dieci anni il deep learning è diventato uno dei temi più discussi nel campo del machine learning e dell’intelligenza artificiale.

Il deep learning richiede impegno per essere appreso e per iniziare bisogna avere familiarità con alcuni concetti matematici di livello universitario. Dobbiamo conoscere le basi dell’algebra lineare, del calcolo vettoriale (gradienti, derivate parziali) e della probabilità (stima di massima verosimiglianza). Oltre agli aspetti matematici, serve una buona comprensione della programmazione orientata agli oggetti e, per ottenere efficienza, una conoscenza di base delle operazioni su GPU. Introduciamo comunque molti di questi concetti nel momento in cui diventano rilevanti. Questa serie di lezioni risulta abbastanza accessibile per chi possiede un background matematico.

In queste lezioni spieghiamo cos’è il deep learning, perché ha guadagnato popolarità, come funziona e come possiamo applicarlo alla finanza quantitativa per migliorare le prestazioni dei modelli e la redditività dei portafogli. Mostriamo come sfruttare una libreria Python chiamata Theano [5] e l’uso delle GPU per accelerare l’addestramento dei modelli predittivi.

In particolare, durante questa serie, affrontiamo i seguenti argomenti:

  • Regressione logistica con Theano (questa lezione)
  • Reti neurali e perceptron multistrato
  • Reti neurali convoluzionali (ConvNets)
  • Denoising Autoencoder e Stacked Denoising Autoencoder
  • Macchine di Boltzmann limitate
  • Deep Belief Networks
  • Reinforcement Learning e Q-Learning
  • Deep Learning per l’analisi delle serie temporali

Che cos’è il deep learning?

Il deep learning rappresenta un sottoinsieme del machine learning, con l’obiettivo di modellare astrazioni complesse nei dati per potenziare l’apprendimento supervisionato e non supervisionato [9].

Raggiungiamo questo obiettivo tramite più livelli di elaborazione, ciascuno con funzioni di trasformazione non lineari che apprendono le rappresentazioni presenti nei dati.

Questo approccio si ispira direttamente ai meccanismi del cervello umano. Durante la vita, apprendiamo concetti semplici e li combiniamo per formare idee sempre più complesse. Il deep learning adotta questa logica per affrontare i compiti di machine learning.

Perché usare il deep learning?

Una delle difficoltà principali del machine learning tradizionale riguarda il tempo necessario per creare manualmente le feature, ossia i predittori, per ottenere algoritmi di classificazione o regressione efficaci. Ad esempio, per prevedere un indice azionario potremmo includere tassi d’interesse, prezzi delle materie prime o dati fondamentali delle aziende. Per dati non numerici come audio, video o testi, dobbiamo trasformarli in feature numeriche efficaci.

Scegliere le feature ottimali richiede tempo e competenze. Dobbiamo spesso costruirle a mano attraverso riduzioni dimensionali o trasformazioni utili a migliorare l’accuratezza predittiva. Alcuni esempi includono il modello Bag-of-Words e i metodi di term frequency–inverse document frequency per l’elaborazione del linguaggio naturale.

Il deep learning ci permette di superare questa laboriosa fase di feature engineering grazie a reti in grado di apprendere automaticamente le feature da grandi quantità di dati non etichettati. La chiave risiede nella rappresentazione gerarchica delle feature appresa dalla rete.

L’attuale ricerca si concentra proprio sull’addestramento di modelli in grado di apprendere astrazioni dai dati grezzi. Questo è essenziale, poiché i dati non etichettati sono molto più disponibili rispetto a quelli etichettati, che richiedono un processo costoso.

Il deep learning ha guadagnato attenzione negli ultimi dieci anni grazie a tre articoli fondamentali scritti da Geoff Hinton, Yoshua Bengio, Yann LeCun e collaboratori [2]. Oltre a questi lavori, negli ultimi anni è stata pubblicata un’enorme quantità di letteratura sull’argomento.

Grazie all’aumento della potenza di calcolo, alla riduzione dei costi e alla diffusione delle GPU, oggi possiamo accedere facilmente al deep learning anche al di fuori dell’ambito accademico.

Alcune tecniche producono risultati eccezionali in visione artificiale, riconoscimento vocale, elaborazione delle immagini e linguaggio naturale. Vale quindi la pena esplorare come applicarle anche alla finanza quantitativa.

Deep Learning nella finanza quantitativa

Il deep learning è utile nella finanza quantitativa? Secondo noi sì. Possiamo applicarlo con successo all’analisi delle serie temporali [11], considerando attentamente la componente temporale nei modelli. Questa sfida richiede ricerca, ma offre grandi opportunità.

Inoltre, possiamo utilizzarlo per il rilevamento di anomalie temporali, campo in cui risulta competitivo rispetto ad altri strumenti come le reti bayesiane. Alcuni modelli quantitativi richiedono di identificare i “cambi di regime” e il deep learning può risultare molto efficace.

Il valore reale per un trader quantitativo risiede nella capacità di analizzare grandi set di immagini e generare segnali da questi dati visivi. Come possiamo sfruttarlo nelle nostre strategie? Consideriamo solo i prezzi storici o anche dati alternativi?

Oggi abbiamo accesso a fonti di dati geospaziali fornite da satelliti in orbita bassa (LEO) e droni che generano immagini dettagliate. Queste immagini ci offrono informazioni preziose su parametri prima difficili da ottenere. Le osservazioni mensili ci forniscono anche la componente temporale. Se tutto ciò sembra fantascientifico, invitiamo a esplorare realtà come Orbital Insight e Satellite Imaging Corporation che già offrono questi servizi.

Gli hedge fund utilizzano queste tecniche da tempo. Oggi, grazie alla diffusione di immagini satellitari, librerie di deep learning e infrastrutture cloud, anche noi trader retail possiamo implementarle, con la giusta determinazione.

Nelle prossime lezioni mostriamo come condurre queste analisi, con l’obiettivo di aiutarvi a integrare strategie non correlate nei portafogli.

Librerie di deep learning

La popolarità del deep learning ha favorito la nascita di molte librerie open source. Su Wikipedia troviamo un elenco completo. Tra le più potenti segnaliamo TensorFlow, Torch, Theano e Caffe.

Non ci soffermiamo a confrontarle. Tutte queste librerie consentono di creare modelli di deep learning in ambienti diversi, con API e prestazioni variabili. Se vogliamo approfondire quale sia la più adatta ai nostri progetti, consigliamo di consultare la pagina Wikipedia sopra indicata.

In queste lezioni utilizziamo Theano perché si basa su Python, supporta CPU e GPU, ed è necessario per PyMC3, già usato nella lezione sull’MCMC bayesiano.

In questa lezione introduciamo Theano e lo applichiamo a un semplice modello di regressione logistica.

Introduzione a Theano

In questa serie di lezioni utilizziamo la libreria Theano di Python.

Cos’è Theano?

Theano è una libreria per il calcolo numerico che ci permette di definire, ottimizzare e valutare espressioni matematiche complesse con array multidimensionali, sfruttando in modo efficiente sia la CPU sia la GPU [5].

Cosa implica questo? Molti modelli di machine learning elaborano grandi array multidimensionali per rappresentare i valori e i pesi dei parametri. Anche i dati da analizzare vengono archiviati in array simili. Per questo motivo, conviene adottare una libreria capace di gestire efficientemente questi array.

Theano risulta particolarmente interessante nel contesto del deep learning perché utilizza espressioni simboliche. Invece di scrivere direttamente il codice Python delle formule, rappresentiamo l’equazione con oggetti. Theano interpreta questa rappresentazione e ne ottimizza automaticamente l’esecuzione, semplificandoci il lavoro. 

Un aspetto cruciale per le applicazioni di deep learning (soprattutto con discesa stocastica del gradiente) è la possibilità di derivare simbolicamente le espressioni. Spieghiamo questo concetto in dettaglio tra poco.

Mettiamo in evidenza un punto chiave: con Theano scriviamo le specifiche del modello, non la sua implementazione. Questo approccio si rivela molto utile perché Theano si integra bene con la GPU, offrendoci significativi vantaggi in termini di velocità durante l’allenamento dei modelli di deep learning.

Possiamo quindi collocare Theano a metà strada tra NumPy e la libreria di matematica simbolica SymPy di Python.

Sottolineiamo infine che non ci limitiamo a usare Theano solo per la ricerca sul deep learning. Ad esempio, PyMC3, la nota libreria di programmazione probabilistica bayesiana, incorpora Theano in parte del suo codice.

Installazione di Theano

Per installare Theano, seguiamo le istruzioni fornite nel sito ufficiale, dato che supporta diverse piattaforme e versioni di Python.

Seguiamo questa lezione con maggiore facilità su un sistema Unix-based come Linux/Ubuntu o macOS. Tali ambienti semplificano l’interazione con la GPU, mentre risulta più complicato configurarla con Anaconda su Windows.

Procediamo ora con l’implementazione della regressione logistica con Theano.

Regressione logistica con Theano

Abbiamo già illustrato i motivi per cui il deep learning rappresenta un approccio valido e merita attenzione. In questa lezione costruiamo il nostro primo modello statistico: una regressione logistica multiclasse, usando Theano per comprenderne il funzionamento. 

Anche se non parliamo ancora di deep learning vero e proprio, questo passaggio costituisce una base essenziale per affrontare architetture più avanzate come i perceptron multistrato e le reti neurali convoluzionali.

Applichiamo la regressione logistica a un famoso dataset: MNIST, una raccolta di cifre scritte a mano. Verifichiamo come questo approccio riesca a ottenere prestazioni di classificazione soddisfacenti. L’aspetto più importante, tuttavia, è imparare a costruire un modello significativo in Theano e usarlo come base per sviluppi futuri nel deep learning.

Iniziamo con la descrizione del dataset MNIST, ripassiamo i concetti fondamentali della regressione logistica e introduciamo la discesa stocastica del gradiente, l’algoritmo di ottimizzazione alla base di molti modelli.

Presentiamo infine le funzioni di verosimiglianza, che ci forniscono la “funzione obiettivo” per l’addestramento. Implementiamo tutte queste tecniche in Theano, addestriamo e testiamo il nostro modello di regressione logistica usando sia la CPU che la GPU.

Set di dati MNIST

Il dataset Mixed National Institute of Standards and Technology (MNIST) [16] raccoglie immagini di cifre scritte a mano, spesso utilizzate per l’addestramento e il test di modelli di machine learning. Include 60.000 immagini per l’allenamento e 10.000 per il test, tutte in formato 28×28 pixel.

trading-machine-learning-mnist

Gli algoritmi di deep learning sono attualmente lo stato dell’arte per le prestazioni di classificazione sul set di dati MNIST. In particolare, un approccio con reti neurali a convoluzione gerarchica ha raggiunto un tasso di errore di appena lo 0,23% nel 2012.

In questa serie di articoli  usiamo il set di dati MNIST poiché è facile da lavorare e ci consente di verificare come le prestazioni di classificazione migliorano con la crescente sofisticazione delle architetture che  introduciamo.

Il set di dati MNIST, in una  semplice forma per essere usata in questi tutorial, può essere trovata qui: http://www.iro.umontreal.ca/~lisa/deep/data/mnist/mnist.pkl.gz.

Affinché questo tutorial sia eseguito correttamente, questo file deve essere posizionato nella stessa directory del codice Python, che abbiamo chiamato deep_logistic_regression_theano.py. Non ha bisogno di essere decompresso. La decompressione verrà gestita all’interno di Python tramite il modulo gzip.

Nei sistemi Linux/Ubuntu possiamo direttamente scaricare il file con la seguente riga di comando:

				
					$ wget http://www.iro.umontreal.ca/~lisa/deep/data/mnist/mnist.pkl.gz
				
			

Modello di regressione logistica

In questa lezione applichiamo il modello probabilistico di regressione logistica multiclasse per classificare le cifre scritte a mano del dataset MNIST. Nonostante il nome, la regressione logistica rappresenta una tecnica di classificazione.

Non affrontiamo qui una trattazione dettagliata sulla regressione logistica. Per un’introduzione accessibile all’argomento suggeriamo James et al [13], mentre Hastie et al [12] offre una discussione più avanzata di livello accademico.

Utilizziamo la regressione logistica per assegnare una probabilità a ciascuna classe in base all’immagine di una cifra, dopo aver addestrato il modello su coppie immagine-etichetta precedenti. Operiamo quindi in un regime di apprendimento supervisionato.

Indichiamo con \(Y\) la risposta della classe etichettata (la cifra) e con \(x\) il nostro vettore di feature in ingresso (un’immagine 28×28 pixel). La probabilità che un’immagine venga classificata in una determinata cifra \(K\) si calcola così [12]:

\(\begin{eqnarray}P(Y=k \mid x) = \frac{\text{exp}(\beta_{k0} + \beta^{T}_k x)}{1 + \sum^{K-1}_{l=1} \text{exp}(\beta_{l0} + \beta^{T}_{l} x)}\end{eqnarray}\)

Dove \(k \in \{1,\ldots,K-1 \}\).

Per semplificare i parametri \(\beta_j\) riscriviamo la formula in forma matriciale. Se definiamo \(W\) come la matrice dei pesi e \(b\) come il vettore dei bias, otteniamo:

\(\begin{eqnarray}P(Y=k \mid x; W,b) = \frac{\text{exp}(W_k x + b_k)}{\sum^{K}_{l=1} \text{exp}(W_l x + b_l)}\end{eqnarray}\)

Una volta trovato un metodo per calcolare \(W\) e \(b\), individuiamo la cifra più probabile associata al vettore di feature \(x\) tramite \(P(Y=k \mid x; W,b)\).

Classifichiamo quindi l’immagine assegnandole la cifra con la probabilità più alta tra 0 e 9. Scriviamo questa regola con la funzione \(\text{argmax}\), ottenendo la previsione del modello \(y_{\text{pred}}\):

\(\begin{eqnarray}y_{\text{pred}} = \text{argmax}_k P(Y=k \mid x; W,b)\end{eqnarray}\)

A questo punto, ci concentriamo sulla determinazione di \(W\) e \(b\). Per farlo introduciamo i concetti di funzione obiettivo, verosimiglianza e addestramento. Prima, però, descriviamo un algoritmo fondamentale: la discesa stocastica del gradiente.

Discesa stocastica del gradiente

Nel deep learning, consideriamo l’ottimizzazione come parte essenziale. Per addestrare correttamente una rete, minimizziamo una funzione obiettivo (o funzione di costo).

Un esempio noto ci arriva dalla statistica classica: nella regressione lineare impieghiamo i minimi quadrati per trovare la retta di miglior adattamento. Nella regressione logistica, come vediamo in questa lezione, ottimizziamo invece la negative log-likelihood (che discuteremo tra poco).

Un algoritmo molto usato nel deep learning è la discesa stocastica del gradiente. Per comprenderla, partiamo dalla versione classica della discesa del gradiente.

La discesa del gradiente si basa sull’idea che una funzione obiettivo differenziabile \(f(x)\), dove \(x \in \mathbb{R}^n\), diminuisce più rapidamente se ci muoviamo nella direzione opposta al gradiente, cioè \(- \nabla f(a)\), intorno a un punto a. In sostanza, seguiamo la pendenza più ripida per scendere dalla collina.

L’algoritmo aggiorna il punto \(x_n\) nella direzione del gradiente negativo:

\(\begin{eqnarray}x_{n+1} = x_n – \gamma_n \nabla f(x_n)\end{eqnarray}\)

Dove \(\gamma_n\) rappresenta lo step size o learning rate, ovvero la lunghezza del passo.

Questa sequenza dovrebbe condurci verso un minimo locale. In una rete di deep learning, ciò equivale a ottimizzare i parametri dell’addestramento.

Funzione obiettivo

Nel machine learning, incontriamo spesso funzioni obiettivo del tipo:

\(\begin{eqnarray}f(x) = \sum_{i=1}^n f_i (x)\end{eqnarray}\)

Ossia, la funzione obiettivo è la somma di contributi \(f_i\), ciascuno relativo all’i-esima osservazione del dataset [14].

La discesa stocastica del gradiente si distingue perché, invece di valutare tutte le derivate parziali a ogni passo, consideriamo solo un sottoinsieme casuale. In questo modo riduciamo i costi computazionali, soprattutto per le reti ad alta dimensione.

Utilizziamo questo metodo per calcolare la nostra funzione obiettivo, non solo in questa lezione ma anche nelle prossime lezioni dedicate alle architetture di deep learning.

Applichiamo una variante chiamata discesa stocastica del gradiente con mini-batch (MSGD), che valuta la funzione obiettivo su più istanze selezionate invece che su una sola. Questo riduce la varianza nella stima del gradiente e introduce un nuovo parametro, \(B\), la dimensione del mini-batch. Vedremo come \(B\) influenzi la velocità di convergenza e la stabilità numerica.

Proprio come nei modelli avanzati di deep learning, anche nei modelli statistici classici utilizziamo un meccanismo per trovare i parametri ottimali utili per previsione e inferenza. Un esempio familiare è il metodo dei minimi quadrati ordinari per la regressione lineare.

Negative Log-Likelihood

Per la regressione logistica adottiamo il metodo della massima verosimiglianza (MLE). Stimiamo così i parametri del modello che si adattano meglio ai dati. In particolare, vogliamo determinare la migliore matrice W dei pesi e il vettore b dei bias per i nostri dati di training.

Il MLE punta a massimizzare la funzione di verosimiglianza, aumentando così il fitting del modello sui dati.

Le funzioni di verosimiglianza sono spesso prodotti di probabilità condizionate. Per trovare il massimo, calcoliamo le derivate parziali rispetto ai parametri e le eguagliamo a zero, come da prassi.

Tuttavia, calcolare le derivate di un prodotto può risultare complesso. Usiamo allora la log-verosimiglianza, che trasforma i prodotti in somme grazie al logaritmo naturale. Poiché il logaritmo è monotono, massimizzare la log-likelihood equivale a massimizzare la verosimiglianza.

Nel nostro caso, la negative log-likelihood per N osservazioni risulta:

\(\begin{eqnarray}\ell (\theta = \{ W, d \} \mid \mathcal{D}) = \sum_{i=1}^N \log (P(Y=k \mid x_i; \theta))\end{eqnarray}\)

Ovvero, per i dati \(\mathcal{D}\), la negative log-likelihood dei parametri \(\theta\) (matrice W e vettore b) corrisponde alla somma dei logaritmi delle probabilità che ciascuna cifra venga predetta correttamente dato il vettore delle feature \(x_i\).

Valutiamo questa funzione obiettivo con Theano. Di norma la differenziamo, ma le derivate possono diventare complesse e instabili numericamente [15]. Usiamo quindi l’operatore grad di Theano per calcolarle in modo efficiente.

Implementazione della Regressione Logistica con Theano

Finalmente siamo arrivati al- punto di iniziare a scrivere del codice con Theano. Descriviamo ora i singoli frammenti di codice mentre procediamo con l’implementazione-

Prima di iniziare dobbiamo installare Theano:

				
					$ pip install theano
				
			

Come già detto, nel sito ufficiale sono disponibili le istruzioni di installazione di Theano per varie piattaforme.

Iniziamo con l’importazione delle librerie necessarie:

				
					import gzip
import six.moves.cPickle as pickle
import timeit

import numpy as np 
import theano
import theano.tensor as T
				
			

Importiamo gzip e una versione più veloce di pickle per decomprimere e deserializzare il database MNIST. Inoltre importiamo timeit per calcolare il valore di elapsed real time per il confronto CPU/GPU. Numpy è necessario per la creazione delle matrici e degli array, mentre importiamo la libreria tensor di Theano come T per comodità.

La prima cosa da fare è creare una classe che implementa il nostro modello di regressione logistica con Theano. Questa classe contiene le variabili per memorizzare la matrice W dei pesi e il vettore b dei bias polarizzazione. Conterrà anche un meccanismo per calcolare la probabilità di appartenenza alla classe \(P(Y=k \mid x; W,b)\) e conseguentemente, la previsione alla nuova classe \(y_{\text{pred}}\).

Calcolo della regressione logistica

Diamo un’occhiata al codice che implementa la classe LogisticRegression.

Il nostro primo codice definisce la matrice dei pesi:

				
					self.W = theano.shared(
    value=np.zeros(
        (num_in, num_out), dtype=theano.config.floatX
    ),
    name='W', borrow=True
)
				
			

Questo codice usa utilizza l’oggetto theano.shared, che consente di condividere una variabile simbolica tra le funzioni. Richiede una matrice di zeri di Numpy di un tipo di dati fisso floatX (che può essere a 32 o 64 bit). Dobbiamo dargli un nome, in questo caso ‘W’ e infine usiamo il parametro borrow=True per evitare il deepcopying dei dati in memoria. Questo è simile al passaggio di puntatori in C++. Tuttavia questo parametro non avrà alcun effetto su una GPU, come approfondito nel tutorial di Theano. In questa fase W NON è stata “addestrata” ma solo inizializzata con una matrice di zeri.

Ora creiamo uno codice simile per il vettore dei bias. È simile con l’eccezione che creiamo un vettore di zeri invece di una matrice di zeri:

				
					self.b = theano.shared(
    value=np.zeros(
        (num_out,), dtype=theano.config.floatX
    ),
    name='b', borrow=True
)
				
			

abbiamo anche bisogno di un’espressione simbolica da memorizzare \(P(Y=k \mid x; W,B)\). Il codice è riportato di seguito:

				
					self.p_y_x = T.nnet.softmax(T.dot(x, self.W) + self.b)
				
			

Sembra piuttosto complicato! Cosa si sta facendo in questo frammento di codice? In primo luogo, notiamo l’uso della funzione softmax. Questa è una particolare funzione matematica (che si può approfondire qui) data da:

\(\begin{eqnarray}\sigma({\bf z}) = \frac{\text{exp}(z_k)}{\sum^{K}_{l=1} \text{exp}(z_l)}\end{eqnarray}\)

 Dove \(k \in \{ 1, \ldots, K \}\). Questa formula è nella stessa forma della probabilità della classe etichetta Y, dato un vettore di feature x. Nella nostra formula possiamo quindi utilizzare la funzione softmax implementata nella libreria Theano. Da notare che usiamo l’operatore dot tra il vettore x e la matrice dei pesi W, sommato al vettore dei bias b. Abbiamo quindi una rappresentazione simbolica per il calcolo della probabilità di Y.

L’ultima variabile da inizializzare è il meccanismo per calcolare l’effettiva classe della cifra, data la probabilità. Come descritto nei paragrafi precedenti, questo è caratterizzato dalla funzione \(\text{argmax}\). Quindi il seguente frammento di codice è relativamente facile da interpretare poiché utilizza semplicemente la funzione argmax di Theano:

				
					self.y_pred = T.argmax(self.p_y_x, axis=1)
				
			

Questo completa l’inizializzazione della classe. Da notare ancora una volta che in questa fase W e b non sono stati addestrati e sono semplicemente impostati come matrici di zeri, cioè matrici con tutti gli elementi uguali a zero.

Funzione obiettivo della negative log-likelihood

A questo punto per implementare la regressione logistica con Theano possiamo usare gli operatori di Theano. Con questi operatori possiamo definire la funzione obiettivo della negative log-likelihood con una espressione simbolica. In questo modo possiamo definire un meccanismo per valutare la verosimiglianza per qualsiasi specifico insieme di parametri W e b, che sarebbe molto complicato da implementare in un codice senza l’aiuto degli operatori forniti da Theano:

				
					
def negative_log_likelihood(self, y):
    return -T.mean(T.log(self.p_y_x)[T.arange(y.shape[0]), y])
				
			

Sebbene il codice sia conciso, effettua molte operazioni e quindi è opportuno descriverlo in modo approfondito. y.shape[0] è il numero di esempi nel minibatch di dimensione N, che usato come parametro di T.arange(..) otteniamo il vettore simbolico contenente l’elenco di numeri interi da 0 a N-1.

L’operatore log di Theano agisce sulla probabilità della classe \(y\), dato vettore delle feature \(x\), per produrre una matrice di probabilità logaritmiche. Questa matrice ha tante righe quanti sono gli esempi di addestramento nel minibatch (ad es N) e tante colonne quante sono le classi/cifre, K. Nel nostro caso abbiamo K=10 dato stiamo considerando le cifre \(0..9\). Quindi abbiamo una matrice di dimensioni \(N \times K\) = \(N \times 10\).

Combinando queste funzioni nel comando T.log(self.p_y_x)[T.arange(y.shape[0]), y] otteniamo un vettore che contiene le probabilità logaritmiche per ogni coppia di esempio di addestramento / risposta della classe della cifra. Questo vettore ha lunghezza N. Calcoliamo la media (T.mean) di questo vettore per ottenere la verosimiglianza logaritmica media di tutti gli esempi di addestramento nel minibatch. Infine, prendiamo il negativo per ottenere la negative log likelihood media degli esempi di addestramento.

Notiamo quindi che effettuiamo molti calcoli con una sola riga di comando. Questo è uno dei principali vantaggi quando usiamo Theano. Ci permette di definire le modalità di calcolo di un’espressione piuttosto che obbligarci a implementare il calcolo. Possiamo lasciare a Theano l’arduo compito di trovare un’implementazione ottimale.

Calcolo del tasso di errore

Il passaggio finale è definire un meccanismo per calcolare il tasso di errore per un particolare batch di previsioni delle cifre. Per questo scopo definiamo il metodo errors per la classe LogisticRegression, che accetta un vettore di cifre y e lo confronta con il vettore delle previsioni self.y_pred:

				
					
def errors(self, y):
    # Per prima cosa controlliamo se il vettore y ha
    # la stessa dimensione del vettore di previsione y
    if y.ndim != self.y_pred.ndim:
        raise TypeError(
            "y should have the same shape as self.y_pred",
            ("y", y.type, "y_pred", self.y_pred.type)
        )
    # Controlla se y contiene i tipi (interi) corretti
    if y.dtype.startswith('int'):
        # Possiamo usare l'operatore Theano neq per restituire
        # il vettore di 1s o 0s, dove 1 rappresenta un
        # errore di classificazione
        return T.mean(T.neq(self.y_pred, y))
    else:
        raise NotImplementedError()
				
			

Il metodo utilizza l’operatore neg di Theano, che restituisce un vettore di 1 o 0, dove 1 rappresenta un errore di classificazione. Questo ci permette di stimare l’accuratezza della previsione nell’addestramento del modello.

Addestramento del modello

Prima di poter addestrare il modello è necessario decomprimere il file gzip MNIST che abbiamo scaricato in precedenza, all’interno dello script Python.

Per restituire i corretti set  di training, validazione e test delle coppie feature-risposta (cioè coppie immagine-cifra) abbiamo bisogno di un meccanismo per creare variabili condivise di Theano, in modo da copiare i dati nella memoria della GPU. Se dovessimo copiare un minibatch in sequenza, le prestazioni si degraderebbero in modo significativo poiché il trasferimento della memoria della GPU è lento. Di seguito il metodo shared_dataset che implementa quanto sopra:

				
					
def shared_dataset(data_xy, borrow=True):
    """
    Crea variabili condivise Theano per consentire la
    copia dei dati sulla GPU per evitare il degrado
    delle performance per i dati in minibatch
    """
    data_x, data_y = data_xy
    shared_x = theano.shared(
        np.asarray(
            data_x, dtype=theano.config.floatX
        ), borrow=borrow
    )
    shared_y = theano.shared(
        np.asarray(
            data_y, dtype=theano.config.floatX
        ), borrow=borrow
    )
    return shared_x, T.cast(shared_y, 'int32')
				
			
L’ultima riga richiede una piccola spiegazione. Dato che la GPU archivia i dati in un formato a virgola mobile ma per i scopi di addestramento abbiamo bisogno che i valori delle cifre siano interi, allora dobbiamo eseguire il cast dei dati nel formato intero. Il codice per aprire l’archivio compresso dei dati MNIST con gzip è il seguente:
				
					
def load_mnist_data(filename):
    """
    Carico il MNIST compresso con gzip nei
    dataset di test, convalida e training
    tramite la libreria pickle Python
    """
    # Uso la compressione gzip e le librerie di
    # deserializzazione pickle per aprire le immagini
    # MNIST nei dataset di addestramento, convalida e test
    with gzip.open(filename, 'rb') as gzf:
        try:
            train_set, valid_set, test_set = pickle.load(
                gzf, encoding='latin1'
            )
        except:
            train_set, valid_set, test_set = pickle.load(gzf)

    # Uso la funzione shared_dataset per creare le
    # variabili condivise di Theano da copiare nella GPU
    test_set_x, test_set_y = shared_dataset(test_set)
    valid_set_x, valid_set_y = shared_dataset(valid_set)
    train_set_x, train_set_y = shared_dataset(train_set)

    # Creo la lista di tuple che contiene le coppie
    # feature-risposta per i dataset di addestramento,
    # di validazione e di testing
    rval = [
        (train_set_x, train_set_y),
        (valid_set_x, valid_set_y),
        (test_set_x, test_set_y)
    ]
    return rval
				
			

Questo codice è relativamente semplice.  Usiamo un contesto with per aprire il file tramite la libreria gzip, quindi deserializziamo i dati tramite la libreria pickle di Python, preoccupandi di specificare la corretta codifica.

Quindi utilizziamo la  funzioneshared_dataset, definita in precedenza, per creare le variabili condivise  di Theano per i dati (per essere copiati sulla GPU in modo da ottimizzare le prestazioni). Infine, restituiamo un insieme di coppie feature-risposta per i rispettive dataset di addestramento, convalida e testing.

Addestramento della discesa stocastica del gradiente

Dopo aver descritto il codice per caricare il set di dati, dobbiamo implementare il metodo di addestramento della discesa stocastica del gradiente. Questa è una funzione complessa e contiene molto codice. Proviamo quindi a descrivere i singoli blocchi che la compongono. In questa serie di articolo descriviamo molti esempi di addestramenti SGD, quindi ci sono altre occasioni dove approfondire questi concetti.

Il primo passo è definire la funzione e i suoi parametri:

				
					
def stoch_grad_desc_train_model(
        filename, gamma=0.13, epochs=1000, B=600
):
    """
    Addestramento del modello di regrassione logistica
    usando la discesa stocastica del gradiente.

    filename - il percorso del file del dataset MNIST
    gamma - "learning rate" per la discesa del gradiente
    epochs - numero massimo di epoche per la SGD
    B - la dimensione per ogni minibatch
    """
    # Ottengo la corrette partizioni dei dataset
    datasets = load_mnist_data(filename)
    train_set_x, train_set_y = datasets[0]
    valid_set_x, valid_set_y = datasets[1]
    test_set_x, test_set_y = datasets[2]

    # Calcolo il numero di minibatc per ogni partizioni dei
    # dataset di addestramento, validazione e testting
    # Nota: Uso l'operatore // per restituisce la parte intera
    # del quoziente della divisione, ad es.
    # 1.0//2 is equal to 0.0
    # 1//2 is equal to 0
    n_train_batches = train_set_x.get_value(borrow=True).shape[0] // B
    n_valid_batches = valid_set_x.get_value(borrow=True).shape[0] // B
    n_test_batches = test_set_x.get_value(borrow=True).shape[0] // B
				
			

I parametri di questa funzione sono il dataset filename, la dimensione dello step (o velocità di apprendimento) gamma, il numero massimo di epochs per eseguire l’algoritmo SGD e la dimensione del minibatch B. I valori predefiniti sono stati scelti in base a quelli suggeriti dal tutorial originale di regressione logistica con Theano. 

Prima di tutto la funzione acquisisce i dataset MNIST e lo suddivide nei set di addestramento, convalida e test. Successivamente si calcola il numero di minibatch per ciascuna partizione dividendo le dimensioni di ciascun dataset per la dimensione del batch, B.

Costruzione del modello

Il passaggio successivo è costruire il modello di regressione logistica:

				
					    # COSTRUZIONE DEL MODELLO
    # ===============
    print("Building the logistic regression model...")

    # Creo le variabili simboliche per i dati del minibatch
    index = T.lscalar()  # valori scalari interi
    # Vettore delle feature, ad es. le immagini
    x = T.matrix('x')
    # Vettore degli interi che rappresentano le cifre
    y = T.ivector('y')

    # Inizializzazione del modello di regressione
    # logisita ed assegnazione del costo
    logreg = LogisticRegression(x=x, num_in=28 ** 2, num_out=10)
    cost = logreg.negative_log_likelihood(y)  # Questo deve essere minimizzato con la SGD

    # Creazione di un insieme di funzioni di Theano per i dataser di
    # testing e validazione che calcola gli errori del modello
    # per uno specifico minibatch
    test_model = theano.function(
        inputs=[index],
        outputs=logreg.errors(y),
        givens={
            x: test_set_x[index * B: (index + 1) * B],
            y: test_set_y[index * B: (index + 1) * B]
        }
    )

    validate_model = theano.function(
        inputs=[index],
        outputs=logreg.errors(y),
        givens={
            x: valid_set_x[index * B: (index + 1) * B],
            y: valid_set_y[index * B: (index + 1) * B]
        }
    )

				
			

Le variabili simboliche vengono create per memorizzare i dati del minibatch prima dell’istanziazione della clase LogisticRegression. Da notare la dimensione degli input num_in, che è pari a 28×28 pixel per le immagini della calligrafia MNIST. La dimensione dell’output è 10, che rappresenta ciascuna delle cifre da 0 a 9.

La sezione successiva crea un set di funzioni Theano, che calcola gli errori associati a uno specifico minibatch di dati di test e addestramento. Fanno uso del metodo errors della classe LogisticRegression . Entrambe queste funzioni sono usate di seguito.

L’operatore simbolico grad di Theano è usato per calcolare il gradiente della log-verosimiglianza negativa rispetto alle variazioni dei sottostanti parametri W e b. A questo punto codifichiamo l’intervallo di discesa stocastica del stocastico nella lista updates. Infine usiamo un’ulteriore function di Theano, insieme al parametro updates=updates per valutare gli errori sul minibatch di addestramento e simultaneamente eseguire lo step di aggiornamento SGD sui parametri:

				
					
    # Uso di Theano per calcolare i gradienti simbolici della
    # funzione di costo (negative log likelihood) per i
    # sottostanti parametri W e b
    grad_W = T.grad(cost=cost, wrt=logreg.W)
    grad_b = T.grad(cost=cost, wrt=logreg.b)

    # Questo è la fase della discesa del gradiente.
    # Si specifica una lista di coppie, ognuna contiente una variabile
    # Theano e una espresione su come aggiornarla ad ogni step.
    updates = [
        (logreg.W, logreg.W - gamma * grad_W),
        (logreg.b, logreg.b - gamma * grad_b)
    ]

    # Simile al precedente insieme di funzioni di Theano, ma in questo
    # caso viene eseguita sui dati di allenamento e aggiorna i parametri
    # W, b in quanto valuta il costo per uno specifico minibatch
    train_model = theano.function(
        inputs=[index],
        outputs=cost,
        updates=updates,
        givens={
            x: train_set_x[index * B: (index + 1) * B],
            y: train_set_y[index * B: (index + 1) * B]
        }
    )

				
			
La parte successiva della funzione riguarda l’effettivo ciclo di addestramento del modello:
				
					
    # ADDESTRAMENTO DEL MODELLO
    # ===============
    print("Training the logistic regression model...")

    # Imposto i parametri per fermare anticipatamente il
    # minibatch se le performance sono abbastanza buone
    patience = 5000  # Numero minimo di esempi da valutare
    patience_increase = 2  # Aumento per il nuovo miglior punteggio
    improvement_threshold = 0.995  # Soglia di miglioramento relativo

    # Addrestramento su questo numero di minibatch prima di
    # controllare le prestazioni sul set di convalida
    validation_frequency = min(n_train_batches, patience // 2)

    # Tengo traccia della funzione obiettivo e dei punteggi del test
    best_validation_loss = np.inf
    test_score = 0.
    start_time = timeit.default_timer()
				
			

Definiamo una variabile chiamata patience, che usiamo per determinare il numero minimo iniziale di esempi da esaminare in ogni minibatch. Il valore di questo parametro aumenta all’aumentare delle prestazioni (cioè  al diminuire dell’errore di classificazione), dato che sono necessari più campioni per minibatch per aumentare continuamente le prestazioni relative per minibatch.

Inoltre calcoliamo la variabile validation_frequency per determinare la frequenza con cui valutare la performance della classificazione sul set di validazione. Infine iniziamo a cronometrare la durata della procedura di training tramite il modulo timeit di Python.

Ciclo di addestramento

La seguente sezione del codice riguarda il ciclo di addestramento principale. È piuttosto lunga. Riportiamo il codice completo qui e lo descriviamo in dettaglio successivamente:

				
					
    # Inizio del ciclo di addestramento
    # Il ciclo while esterno sul numero di epoche
    # Il ciclo for interno sui minibatch
    finished = False
    cur_epoch = 0
    while (cur_epoch < epochs) and (not finished):
        cur_epoch = cur_epoch + 1
        # Ciclo sui minibatch
        for minibatch_index in range(n_train_batches):
            # Calcolo della verosimiglianza media per i minibatch
            minibatch_avg_cost = train_model(minibatch_index)
            iter = (cur_epoch - 1) * n_train_batches + minibatch_index

            if (iter + 1) % validation_frequency == 0:
                # Se l'itezione corrente ha raggiunto la frequenza di validazione
                # allora si calcola la funzione obiettivo dei batch validati
                validation_losses = [
                    validate_model(i)
                    for i in range(n_valid_batches)
                ]
                this_validation_loss = np.mean(validation_losses)

                # Stampo i risultati della validazione corrente
                print(
                    "Epoch %i, Minibatch %i/%i, Validation Error %f %%" % (
                        cur_epoch,
                        minibatch_index + 1,
                        n_train_batches,
                        this_validation_loss * 100.
                    )
                )

                # Se il valore dell'ultima funzione obiettio è il migliore
                if this_validation_loss < best_validation_loss:
                    # Se la funzione obiettivo è all'interno della soglia
                    # di miglioramento, allora si incrementa il numero
                    # di iterazione ("patience") fino al prossimo controllo
                    if this_validation_loss < best_validation_loss * \
                            improvement_threshold:
                        patience = max(patience, iter * patience_increase)
                    # Importo la validazione corrente come la migliore
                    # validazione della perdita
                    best_validation_loss = this_validation_loss

                    # Calcolo le funzioni obiettivo sui minibatch nel
                    # dataset di testing. Il "test_score" è la media
                    # delle funzioni obiettivo
                    test_losses = [
                        test_model(i)
                        for i in range(n_test_batches)
                    ]
                    test_score = np.mean(test_losses)

                    # Stampa dei risultati del test corrente
                    print(
                        (
                            "     Epoch %i, Minibatch %i/%i, Test error of"
                            " best model %f %%"
                        ) % (
                            cur_epoch,
                            minibatch_index + 1,
                            n_train_batches,
                            test_score * 100.
                        )
                    )

                    # Salvataggio del modello su disco usando pickle
                    with open('best_model.pkl', 'wb') as f:
                        pickle.dump(logreg, f)
            # Se l'iterazione eccede l'attuale "patience"
            # allora fermo il loop oer questo minibatch
            if iter > patience:
                done_looping = True
                break

    end_time = timeit.default_timer()
    print(
        (
            "Optimization complete with "
            "best validation score of %f %%,"
            "with test performance %f %%"
        ) % (best_validation_loss * 100., test_score * 100.)
    )
    print(
        'The code run for %d epochs, with %f epochs/sec' % (
            cur_epoch,
            1. * cur_epoch / (end_time - start_time)
        )
    )
    print("The code ran for %.1fs" % (end_time - start_time))
				
			

Il ciclo esterno è un ciclo while sul numero di epoche. Il ciclo interno è un ciclo for sul numero di minibatch di addestramento. Per ogni iterazione del ciclo interno, calcoliamo la negative log-likelihood media del minibatch tramite il metodo train_model, definito come una function di Theano.

Se il numero di iterazioni raggiunto è un multiplo della frequenza di convalida, calcoliamo la funzione obiettivo della validazione e stampiamo i risultati sulla console. A questo punto effettuiamo un controllo per verificare se abbiamo ottenuto la miglior funzione obiettivo della validazione, finora calcolata. In questo caso aumentiamo la variabile “patience” in modo da richiedere più iterazioni per il successivo miglioramento della validazione. A questo punto calcoliamo la negative log-likelihood sul dataset di testing e la stampiamo sulla console.

Quindi salviamo su disco (tramite pickle) l’attuale “migliore” modello di training. Se per questo minibatch superiamo il valore di “patience” allora passiamo al prossimo minibatch. Infine calcoliamo l’ora di fine e quindi stampiamo su console la migliore funzione obiettivo della validazione e tempo totale di esecuzione.

Abbiamo ottenuto i parametri W e b. della discesa stocastica del gradiente per il modello di regressione. Questo completa l’implementazione del modello di regressione logistica con Theano.

Testare il modello

Rispetto all’addestramento del modello, la previsione delle cifre per la fase di test con nuovi dati mai visti è relativamente semplice. Il seguente frammento mostra come eseguire una previsione:

				
					
def test_model(filename, num_preds):
    """
    Verifica del modello con un dataset MNIST di testing
    che il modello non ha mai visto
    """
    # Carichiamo il migliore modello salvato
    classifier = pickle.load(open('best_model.pkl'))

    # Uso di Theano per creare una funzione di previsione
    predict_model = theano.function(
        inputs=[classifier.x],
        outputs=classifier.y_pred
    )

    # Caricamento del dal dataset MNIST da "filename"
    # e isolamente dei dati di testing
    datasets = load_mnist_data(filename)
    test_set_x, test_set_y = datasets[2]
    test_set_x = test_set_x.get_value()

    # Predizione delle cifre per il numero 'num_preds' di immagini
    preds = predict_model(test_set_x[:num_preds])
    print(
        "Predicted digits for the first %s " \
        "images in test set:" % num_preds
    )
    print(preds)
				
			

Se hai utilizzato la libreria Scikit-Learn per eseguire qualsiasi lavoro di apprendimento automatico supervisionato, noterai che l’API non è troppo dissimile.

Il primo passaggio consiste nel caricare il modello best_model.pkl in pickled e inserirlo nella variabile classifier. Successivamente creiamo una funzione theano.function, che prende il vettore delle feature x in input e restituisce i valori previsti per le cifre y_pred. Quindi carichiamo il dataset effettivo e isoliamo il set di test. Infine creiamo un vettore di previsioni, preds, che contiene le previsioni di cifre per le prime num_preds immagini nel dataset di test.

Script principale

Di seguito il codice per unire tutti i pezzi e lanciare lo script:

				
					
if __name__ == "__main__":
    # Impostazione del dataset e del numero di
    # predizioni da fare sui dati di testing
    dataset_filename = "mnist.pkl.gz"
    num_preds = 20

    # Addestramento del modello tramite la
    # discesa stocastica del gradiente
    stoch_grad_desc_train_model(dataset_filename)

    # Verifica delle regressione logistica su un
    # dataset di test mai visto
    test_model(dataset_filename, num_preds)
				
			

Esecuzione del modello sulla CPU o sulla GPU

Per eseguire il codice sulla CPU possiamo usare il seguente comando da terminale. Usiamo il simbolo del dollaro per indicare che si tratta di un comando da terminale, quindi assicurati di non includerlo quando copi e incolli il codice nel terminale:
				
					$ THEANO_FLAGS=mode=FAST_RUN,device=cpu,floatX=float32 python deep_logistic_regression_theano.py
				
			
Otteniamo un output simile al seguente:
				
					Building the logistic regression model...
Training the logistic regression model...
Epoch 1, Minibatch 83/83, Validation Error 12.458333 %
     Epoch 1, Minibatch 83/83, Test error of best model 12.375000 %
Epoch 2, Minibatch 83/83, Validation Error 11.010417 %
     Epoch 2, Minibatch 83/83, Test error of best model 10.958333 %
Epoch 3, Minibatch 83/83, Validation Error 10.312500 %
     Epoch 3, Minibatch 83/83, Test error of best model 10.312500 %
Epoch 4, Minibatch 83/83, Validation Error 9.875000 %
     Epoch 4, Minibatch 83/83, Test error of best model 9.833333 %
Epoch 5, Minibatch 83/83, Validation Error 9.562500 %
     Epoch 5, Minibatch 83/83, Test error of best model 9.479167 %
Epoch 6, Minibatch 83/83, Validation Error 9.322917 %
     Epoch 6, Minibatch 83/83, Test error of best model 9.291667 %
..
.. --TRUNCATED--
..
Epoch 71, Minibatch 83/83, Validation Error 7.520833 %
Epoch 72, Minibatch 83/83, Validation Error 7.510417 %
Epoch 73, Minibatch 83/83, Validation Error 7.500000 %
     Epoch 73, Minibatch 83/83, Test error of best model 7.489583 %
Epoch 74, Minibatch 83/83, Validation Error 7.479167 %
     Epoch 74, Minibatch 83/83, Test error of best model 7.489583 %
Optimization complete with best validation score of 7.479167 %,with test performance 7.489583 %
The code run for 1000 epochs, with 37.882747 epochs/sec
The code ran for 26.4s
Predicted digits for the first 20 images in test set:
[7 2 1 0 4 1 4 9 6 9 0 6 9 0 1 5 9 7 3 4]
				
			

Sul mio desktop con un Intel Core i7-4770K, overcloccato a 3,50 Ghz, ci sono voluti 26,4 secondi.

In alternativa, per eseguire  lo script sulla GPU dobbiamo inserire nel terminale il seguente comando:

				
					$ THEANO_FLAGS=mode=FAST_RUN,device=gpu,floatX=float32 python deep_logistic_regression_theano.py
				
			
In questo caso otteniamo un output simile al seguente:
				
					Using gpu device 0: GeForce GTX 780 Ti (CNMeM is disabled, cuDNN not available)
Building the logistic regression model...
Training the logistic regression model...
Epoch 1, Minibatch 83/83, Validation Error 12.458333 %
     Epoch 1, Minibatch 83/83, Test error of best model 12.375000 %
Epoch 2, Minibatch 83/83, Validation Error 11.010417 %
     Epoch 2, Minibatch 83/83, Test error of best model 10.958333 %
Epoch 3, Minibatch 83/83, Validation Error 10.312500 %
     Epoch 3, Minibatch 83/83, Test error of best model 10.312500 %
Epoch 4, Minibatch 83/83, Validation Error 9.875000 %
     Epoch 4, Minibatch 83/83, Test error of best model 9.833333 %
Epoch 5, Minibatch 83/83, Validation Error 9.562500 %
     Epoch 5, Minibatch 83/83, Test error of best model 9.479167 %
Epoch 6, Minibatch 83/83, Validation Error 9.322917 %
     Epoch 6, Minibatch 83/83, Test error of best model 9.291667 %
..
.. --TRUNCATED--
..
Epoch 71, Minibatch 83/83, Validation Error 7.520833 %
Epoch 72, Minibatch 83/83, Validation Error 7.510417 %
Epoch 73, Minibatch 83/83, Validation Error 7.500000 %
     Epoch 73, Minibatch 83/83, Test error of best model 7.489583 %
Epoch 74, Minibatch 83/83, Validation Error 7.479167 %
     Epoch 74, Minibatch 83/83, Test error of best model 7.489583 %
Optimization complete with best validation score of 7.479167 %,with test performance 7.489583 %
The code run for 1000 epochs, with 251.317223 epochs/sec
The code ran for 4.0s
Predicted digits for the first 20 images in test set:
[7 2 1 0 4 1 4 9 6 9 0 6 9 0 1 5 9 7 3 4]
				
			

Sulla stessa macchina desktop con una GeForce GTX 780 Ti abilitata per NVidia CUDA sono stati necessari 4,0 secondi, cioè un fattore di accelerazione di 6,6x per questo script. In prospettiva, un’attività di allenamento che richiede una settimana su una CPU richiederà solo poco più di un giorno sulla GPU. Quindi, se sei seriamente intenzionato a costruire una macchina o un cluster per la ricerca di deep learning, vale la pena considerare l’uso delle GPU. Sono il modo più semplice per aumentare le prestazioni per la maggior parte delle architetture di deep learning.

Conclusioni

Abbiamo descritto come implementare la regressione logistica con Theano. Sebbene la regressione logistica non sia certo una tecnica all’avanguardia ai fini della classificazione, ci consente di esplorare l’API di Theano, costruire un modello non banale su un set di dati di grandi dimensioni, addestrare il modello sia sulla CPU che sulla GPU e prevedere nuove classificazioni da questo modello.

Il passo successivo è implementare il primo modello di rete neurale, ovvero il perceptron multistrato (MLP). Costruiamo un MLP con Theano e lo usiamo per lo stesso compito di classificazione supervisionato per predire le cifre MNIST. Quindi verifichiamo se le prestazioni sono migliori rispetto al modello di regressione logistica.

Risorse aggiuntive per Machine Learning, Deep Learning e GPU

Riferimenti

Il codice completo presentato in questa lezione è disponibile nel seguente repository GitHub: “https://github.com/tradingquant-it/TQResearch

Torna in alto