RNN da zero | Costruire il modello RNN in Python

Contenuti

introduzione

Gli umani non resettano la loro comprensione del linguaggio ogni volta che ascoltiamo una frase. Dato un post, cogliamo il contesto in base alla nostra precedente comprensione di quelle parole. Una delle caratteristiche distintive che abbiamo è la nostra memoria (o mantenere il potere).

Può un algoritmo replicare questo?? La prima tecnica che mi viene in mente è una rete neurale (NN). Ma, purtroppo, gli NN tradizionali non possono farlo. Prendi un esempio di voler prevedere cosa verrà dopo in un video. Una rete neurale tradizionale avrà difficoltà a generare risultati accurati.

È qui che entra in gioco il concetto di reti neurali ricorrenti. (RNN). Gli RNN sono diventati estremamente popolari nello spazio del deep learning, il che rende l'apprendimento ancora più imperativo. Alcune applicazioni reali di RNN includono:

  • Accreditamento vocale
  • Macchina del traduttore
  • Composizione musicale
  • Accreditamento della scrittura a mano
  • Apprendimento della grammatica

fondamenti della rete neuraleIn questo post, esamineremo prima rapidamente i componenti principali di un tipico modello RNN. Successivamente configureremo la dichiarazione del problema che in conclusione risolveremo implementando da zero un modello RNN in Python.

Possiamo sempre sfruttare le librerie Python di alto livello per codificare un RNN. Quindi, Perché codificarlo da zero?? Credo fermamente che il modo migliore per imparare e radicare veramente un concetto sia impararlo da zero.. Ed è quello che mostrerò in questo tutorial.

Questo post presuppone una comprensione di base delle reti neurali ricorrenti. Se hai bisogno di un rapido aggiornamento o stai cercando di imparare le basi di RNN, Ti consiglio di leggere prima i post qui sotto:

Sommario

  • Flashback: un riassunto dei concetti di rete neurale ricorrenti
  • Previsione della sequenza utilizzando RNN
  • Costruire un modello RNN usando Python

Flashback: un riassunto dei concetti di rete neurale ricorrenti

Ricapitoliamo rapidamente le basi dietro le reti neurali ricorrenti.

Lo faremo usando un esempio di dati di sequenza, diciamo le azioni di una determinata società. Un semplice modello di apprendimento automatico, o una rete neurale artificiale, puoi imparare a prevedere il prezzo delle azioni in base a una serie di caratteristiche, come il volume delle azioni, il valore di apertura, eccetera. A parte questi, il prezzo dipende anche da come si è comportato lo stock nelle settimane precedenti e fays. Per un commerciante, questi dati storici sono in realtà un importante fattore decisivo per fare previsioni.

Nelle reti neurali feedforward convenzionali, tutti i casi di test sono considerati indipendenti. Riesci a vedere che non si adatta bene quando si prevedono i prezzi delle azioni?? Il modello NN non prenderebbe in considerazione i valori di prezzo delle azioni di cui sopra, Non è una grande idea!

C'è un altro concetto su cui possiamo fare affidamento quando si tratta di dati sensibili al fattore tempo: reti neurali ricorrenti (RNN).

Un tipico RNN assomiglia a questo:

Questo può sembrare intimidatorio all'inizio. Ma una volta che lo sviluppiamo, le cose iniziano a sembrare molto più semplici:

Ora è più facile per noi visualizzare come queste reti stanno considerando l'andamento dei prezzi delle azioni. Questo ci aiuta a prevedere i prezzi della giornata. Qui, ogni previsione al tempo t (h_t) dipende da tutte le previsioni precedenti e dalle informazioni ottenute da esse. Abbastanza diretto, verità?

Gli RNN possono risolvere in larga misura il nostro scopo di gestione delle sequenze, ma non proprio.

Il testo è un altro buon esempio di dati di sequenza. Essere in grado di prevedere quale parola o frase viene dopo un determinato testo potrebbe essere un vantaggio molto utile.. Vogliamo i nostri modelli scrivere sonetti shakespeariani!

Ora, gli infermieri registrati sono eccellenti quando si tratta di un contesto di natura breve o piccola. Ma essere in grado di costruire una storia e ricordarla, i nostri modelli devono essere in grado di comprendere il contesto dietro le sequenze, come un cervello umano.

Previsione della sequenza utilizzando RNN

In questo post, lavoreremo su un ostacolo di previsione di sequenza usando RNN. Uno dei compiti più semplici per questo è la previsione dell'onda sinusoidale. La sequenza contiene una tendenza visibile ed è facile da correggere utilizzando l'euristica. Ecco come appare un'onda sinusoidale:

Per prima cosa progetteremo una rete neurale ricorrente da zero per risolvere questo problema.. Anche il nostro modello RNN dovrebbe essere ben generalizzabile in modo da poterlo applicare ad altri problemi di sequenza.

Formuleremo il nostro problema in questo modo: data una sequenza di 50 numeri che appartengono a un'onda sinusoidale, predice il numero 51 della serie. È ora di accendere il tuo laptop Jupyter! (o il tuo IDE preferito)!

Codifica RNN usando Python

passo 0: preparazione dei dati

Ah, l'inevitabile primo passo in qualsiasi progetto di data science: preparare i dati prima di fare qualsiasi altra cosa.

In che modo il nostro modello di rete si aspetta che siano i dati?? Accetterei una sequenza di lunghezza singola 50 come input. Quindi, la forma dei dati di input sarà:

(numero_di_record x lunghezza_di_sequenza x tipi_di_sequenze)

Qui, tipi_di_sequenze es 1, perché abbiamo un solo tipo di sequenza: la allora sinusoidale.

D'altra parte, l'output avrebbe un solo valore per ogni record. Certo, questo sarà il valore 51 nella sequenza di input. Allora la sua forma sarebbe:

(numero_di_record x tipi_di_sequenze) #dove si trova tipi_di_sequenze 1

Entriamo nel codice. Primo, importa le librerie indispensabili:

%pilab in linea

importare matematica

Per creare un'onda sinusoidale come dati, useremo la funzione sinusoidale di Python Matematica Biblioteca:

sin_wave = per esempio.Vettore([matematica.privo di(X) per X in per esempio.arrangiare(200)])

Visualizzando l'onda sinusoidale che abbiamo appena generato:

per favore.complotto(sin_wave[:50])

Creeremo i dati ora nel seguente blocco di codice:

X = []
E = []

seq_len = 50
num_records = len(sin_wave) - seq_len

per io in gamma(num_records - 50):
    X.aggiungere(sin_wave[io:io+seq_len])
    E.aggiungere(sin_wave[io+seq_len])
    
X = per esempio.Vettore(X)
X = per esempio.expand_dims(X, asse=2)

E = per esempio.Vettore(E)
E = per esempio.expand_dims(E, asse=1)

Stampa il modulo dati:

Nota che abbiamo fatto un ciclo per (num_records – 50) perché vogliamo prenotare 50 record come i nostri dati di convalida. Possiamo creare questi dati di convalida ora:

X_val = []
Y_val = []

per io in gamma(num_records - 50, num_records):
    X_val.aggiungere(sin_wave[io:io+seq_len])
    Y_val.aggiungere(sin_wave[io+seq_len])
    
X_val = per esempio.Vettore(X_val)
X_val = per esempio.expand_dims(X_val, asse=2)

Y_val = per esempio.Vettore(Y_val)
Y_val = per esempio.expand_dims(Y_val, asse=1)

passo 1: Crea l'architettura per il nostro modello RNN

Il nostro prossimo compito è stabilire tutte le variabili e le funzioni indispensabili che utilizzeremo nel modello RNN.. Il nostro modello prenderà la sequenza di input, lo elaborerà per mezzo di uno strato nascosto di 100 unità e produrrà un unico output di valore:

tasso_di_apprendimento = 0.0001    
nepoco = 25               
T = 50                   # lunghezza della sequenza
nascosto_dime = 100         
output_dim = 1

bptt_truncate = 5
min_clip_value = -10
max_clip_value = 10

Successivamente definiremo i pesi della rete:

tu = per esempio.a caso.uniforme(0, 1, (nascosto_dime, T))
W = per esempio.a caso.uniforme(0, 1, (nascosto_dime, nascosto_dime))
V = per esempio.a caso.uniforme(0, 1, (output_dim, nascosto_dime))

Qui,

  • U è la matrice dei pesi per i pesi tra input e layer nascosti
  • V è la matrice dei pesi per i pesi tra i livelli nascosti e di output
  • W è la matrice dei pesi per i pesi condivisi nello strato RNN (livello nascosto)

In sintesi, definiremo la funzione di attivazione, sigmoidea, da utilizzare nel livello nascosto:

def sigmoide(X):
    Restituzione 1 / (1 + per esempio.esp(-X))

passo 2: addestrare il modello

Ora che abbiamo definito il nostro modello, in conclusione possiamo continuare con l'addestramento sui nostri dati di sequenza. Possiamo suddividere la procedura di formazione in passaggi più piccoli, vale a dire:

passo 2.1: Verifica la perdita di dati di allenamento
passo 2.1.1: Passa in avanti
passo 2.1.2: Calcola l'errore
passo 2.2: Verifica la perdita di dati di convalida
passo 2.2.1: Passa in avanti
passo 2.2.2: Calcola l'errore
passo 2.3: Inizia la formazione vera e propria
passo 2.3.1: Passa in avanti
passo 2.3.2: Errore di retropropagazione
passo 2.3.3: Aggiorna i pesi

Dobbiamo ripetere questi passaggi fino alla convergenza. Se il modello inizia a sovradimensionarsi, Fermare! O semplicemente preimpostare il numero di epoche.

passo 2.1: Verifica la perdita di dati di allenamento

Faremo un passaggio in avanti attraverso il nostro modello RNN e calcoleremo l'errore quadratico delle previsioni per tutti i record al fine di ottenere il valore di perdita.

per epoca in gamma(nepoco):
    # controlla la perdita sul treno
    perdita = 0.0
    
    # fai un passaggio in avanti per ottenere la previsione
    per io in gamma(E.forma[0]):
        X, e = X[io], E[io]                    # ottenere input, valori di output di ogni record
        prev_s = per esempio.zeri((nascosto_dime, 1))   # qui, prev-s è il valore della precedente attivazione del livello nascosto; che è inizializzato come tutti zeri
        per T in gamma(T):
            nuovo_input = per esempio.zeri(X.forma)    # quindi facciamo un passaggio in avanti per ogni timestep nella sequenza
            nuovo_input[T] = X[T]              # per questo, stabiliamo un singolo input per quel timestep
            mulù = per esempio.punto(tu, nuovo_input)
            mulw = per esempio.punto(W, prev_s)
            Inserisci = mulw + mulù
            S = sigmoide(Inserisci)
            mulva = per esempio.punto(V, S)
            prev_s = S

    # calcolare l'errore 
        loss_per_record = (e - mulva)**2 / 2
        perdita += loss_per_record
    perdita = perdita / galleggiante(e.forma[0])

passo 2.2: Verifica la perdita di dati di convalida

Faremo lo stesso per calcolare la perdita nei dati di convalida (nello stesso ciclo):

    # controlla la perdita su val
    val_loss = 0.0
    per io in gamma(Y_val.forma[0]):
        X, e = X_val[io], Y_val[io]
        prev_s = per esempio.zeri((nascosto_dime, 1))
        per T in gamma(T):
            nuovo_input = per esempio.zeri(X.forma)
            nuovo_input[T] = X[T]
            mulù = per esempio.punto(tu, nuovo_input)
            mulw = per esempio.punto(W, prev_s)
            Inserisci = mulw + mulù
            S = sigmoide(Inserisci)
            mulva = per esempio.punto(V, S)
            prev_s = S

        loss_per_record = (e - mulva)**2 / 2
        val_loss += loss_per_record
    val_loss = val_loss / galleggiante(e.forma[0])

    Stampa('Epoca: ', epoca + 1, ', Perdita: ', perdita, ', Val Loss: ', val_loss)

Dovresti ottenere il seguente risultato:

Epoca:  1 , Perdita:  [[101185.61756671]] , Val Loss:  [[50591.0340148]]
...
...

passo 2.3: Inizia la formazione vera e propria

Ora inizieremo con la formazione vera e propria della rete. In questo, prima faremo un passaggio in avanti per calcolare gli errori e un passaggio all'indietro per calcolare i gradienti e aggiornarli. Lascia che ti mostri questi passaggi in modo che tu possa visualizzare come funziona nella tua mente.

passo 2.3.1: Passa in avanti

Nel passaggio anticipato:

  • Per prima cosa moltiplichiamo l'input con i pesi tra l'input e i livelli nascosti.
  • Aggiungilo moltiplicando i pesi nel livello RNN. Questo perché vogliamo acquisire la conoscenza del passaggio temporale precedente.
  • Passalo attraverso una funzione di attivazione sigmoide.
  • Moltiplica questo con i pesi tra i livelli nascosti e di output.
  • Sul livello di output, abbiamo un'attivazione lineare dei valori, quindi non passiamo esplicitamente il valore attraverso un livello di trigger.
  • Salva lo stato nel livello corrente e anche lo stato nel passaggio temporale precedente in un dizionario

Ecco il codice per eseguire un passaggio in avanti (nota che è una continuazione del ciclo precedente):

    # modello di treno
    per io in gamma(E.forma[0]):
        X, e = X[io], E[io]
    
        strati = []
        prev_s = per esempio.zeri((nascosto_dime, 1))
        di = per esempio.zeri(tu.forma)
        dV = per esempio.zeri(V.forma)
        dW = per esempio.zeri(W.forma)
        
        dU_t = per esempio.zeri(tu.forma)
        dV_t = per esempio.zeri(V.forma)
        dW_t = per esempio.zeri(W.forma)
        
        dU_i = per esempio.zeri(tu.forma)
        dW_i = per esempio.zeri(W.forma)
        
        # passaggio in avanti
        per T in gamma(T):
            nuovo_input = per esempio.zeri(X.forma)
            nuovo_input[T] = X[T]
            mulù = per esempio.punto(tu, nuovo_input)
            mulw = per esempio.punto(W, prev_s)
            Inserisci = mulw + mulù
            S = sigmoide(Inserisci)
            mulva = per esempio.punto(V, S)
            strati.aggiungere({'S':S, 'prec_s':prev_s})
            prev_s = S

passo 2.3.2: Errore di retropropagazione

Dopo la fase di propagazione in avanti, calcoliamo i gradienti in ogni strato e propaghiamo gli errori. Useremo la propagazione all'indietro troncata nel tempo (TBPTT), invece della vaniglia che si propaga all'indietro. Può sembrare complesso, ma in realtà è abbastanza semplice.

La differenza centrale tra BPTT e backprop è che la fase di backpropagation viene eseguita per tutte le fasi temporali nel livello RNN. Quindi, se la lunghezza della nostra sequenza è 50, effettueremo la retropropagazione di tutte le fasi temporali precedenti alla fase temporale corrente.

Se hai indovinato, BPTT sembra molto costoso dal punto di vista computazionale. Quindi, invece di propagarsi all'indietro attraverso tutti i passaggi temporali precedenti, propaghiamo fino a x passaggi temporali per risparmiare potenza di calcolo. Considera questo ideologicamente simile alla discesa del gradiente stocastico, dove includiamo un batch di punti dati invece di tutti i punti dati.

Ecco il codice per propagare gli errori all'indietro:

        # derivato di pred
        dmolv = (mulva - e)
        
        # passaggio all'indietro
        per T in gamma(T):
            dV_t = per esempio.punto(dmolv, per esempio.trasporre(strati[T]['S']))
            dsv = per esempio.punto(per esempio.trasporre(V), dmolv)
            
            ds = dsv
            papà = Inserisci * (1 - Inserisci) * ds
            
            dmulw = papà * per esempio.quelli_mi piace(mulw)

            dprev_s = per esempio.punto(per esempio.trasporre(W), dmulw)


            per io in gamma(T-1, max(-1, T-bptt_truncate-1), -1):
                ds = dsv + dprev_s
                papà = Inserisci * (1 - Inserisci) * ds

                dmulw = papà * per esempio.quelli_mi piace(mulw)
                dmulu = papà * per esempio.quelli_mi piace(mulù)

                dW_i = per esempio.punto(W, strati[T]['prec_s'])
                dprev_s = per esempio.punto(per esempio.trasporre(W), dmulw)

                nuovo_input = per esempio.zeri(X.forma)
                nuovo_input[T] = X[T]
                dU_i = per esempio.punto(tu, nuovo_input)
                dx = per esempio.punto(per esempio.trasporre(tu), dmulu)

                dU_t += dU_i
                dW_t += dW_i
                
            dV += dV_t
            di += dU_t
            dW += dW_t

passo 2.3.3: Aggiorna i pesi

Finalmente, aggiorniamo i pesi con i gradienti dei pesi calcolati. Una cosa a cui dobbiamo prestare attenzione è che i gradienti tendono ad esplodere se non li tieni sotto controllo.. Questo è un argomento fondamentale nella formazione delle reti neurali., chiamato problema del gradiente esplosivo. Quindi dobbiamo tenerli in una gamma in modo che non esplodano. Possiamo farlo così

            Se di.max() > max_clip_value:
                di[di > max_clip_value] = max_clip_value
            Se dV.max() > max_clip_value:
                dV[dV > max_clip_value] = max_clip_value
            Se dW.max() > max_clip_value:
                dW[dW > max_clip_value] = max_clip_value
                
            
            Se di.min() < min_clip_value:
                di[di < min_clip_value] = min_clip_value
            Se dV.min() < min_clip_value:
                dV[dV < min_clip_value] = min_clip_value
            Se dW.min() < min_clip_value:
                dW[dW < min_clip_value] = min_clip_value
        
        # aggiornare
        tu -= tasso_di_apprendimento * di
        V -= tasso_di_apprendimento * dV
        W -= tasso_di_apprendimento * dW

Quando si addestra il modello precedente, otteniamo questo risultato:

Epoca:  1 , Perdita:  [[101185.61756671]] , Val Loss:  [[50591.0340148]]
Epoca:  2 , Perdita:  [[61205.46869629]] , Val Loss:  [[30601.34535365]]
Epoca:  3 , Perdita:  [[31225.3198258]] , Val Loss:  [[15611.65669247]]
Epoca:  4 , Perdita:  [[11245.17049551]] , Val Loss:  [[5621.96780111]]
Epoca:  5 , Perdita:  [[1264.5157739]] , Val Loss:  [[632.02563908]]
Epoca:  6 , Perdita:  [[20.15654115]] , Val Loss:  [[10.05477285]]
Epoca:  7 , Perdita:  [[17.13622839]] , Val Loss:  [[8.55190426]]
Epoca:  8 , Perdita:  [[17.38870495]] , Val Loss:  [[8.68196484]]
Epoca:  9 , Perdita:  [[17.181681]] , Val Loss:  [[8.57837827]]
Epoca:  10 , Perdita:  [[17.31275313]] , Val Loss:  [[8.64199652]]
Epoca:  11 , Perdita:  [[17.12960034]] , Val Loss:  [[8.54768294]]
Epoca:  12 , Perdita:  [[17.09020065]] , Val Loss:  [[8.52993502]]
Epoca:  13 , Perdita:  [[17.17370113]] , Val Loss:  [[8.57517454]]
Epoca:  14 , Perdita:  [[17.04906914]] , Val Loss:  [[8.50658127]]
Epoca:  15 , Perdita:  [[16.96420184]] , Val Loss:  [[8.46794248]]
Epoca:  16 , Perdita:  [[17.017519]] , Val Loss:  [[8.49241316]]
Epoca:  17 , Perdita:  [[16.94199493]] , Val Loss:  [[8.45748739]]
Epoca:  18 , Perdita:  [[16.99796892]] , Val Loss:  [[8.48242177]]
Epoca:  19 , Perdita:  [[17.24817035]] , Val Loss:  [[8.6126231]]
Epoca:  20 , Perdita:  [[17.00844599]] , Val Loss:  [[8.48682234]]
Epoca:  21 , Perdita:  [[17.03943262]] , Val Loss:  [[8.50437328]]
Epoca:  22 , Perdita:  [[17.01417255]] , Val Loss:  [[8.49409597]]
Epoca:  23 , Perdita:  [[17.20918888]] , Val Loss:  [[8.5854792]]
Epoca:  24 , Perdita:  [[16.92068017]] , Val Loss:  [[8.44794633]]
Epoca:  25 , Perdita:  [[16.76856238]] , Val Loss:  [[8.37295808]]

Guardando bene! È ora di ottenere le previsioni e tracciarle per avere un'idea visiva di ciò che abbiamo progettato.

passo 3: ottenere previsioni

Faremo un passaggio in avanti attraverso i pesi allenati per ottenere i nostri pronostici:

pred = []
per io in gamma(E.forma[0]):
    X, e = X[io], E[io]
    prev_s = per esempio.zeri((nascosto_dime, 1))
    # Passaggio in avanti
    per T in gamma(T):
        mulù = per esempio.punto(tu, X)
        mulw = per esempio.punto(W, prev_s)
        Inserisci = mulw + mulù
        S = sigmoide(Inserisci)
        mulva = per esempio.punto(V, S)
        prev_s = S

    pred.aggiungere(mulva)
    
pred = per esempio.Vettore(pred)

Tracciare queste previsioni insieme ai valori effettivi:

per favore.complotto(pred[:, 0, 0], 'G')
per favore.complotto(E[:, 0], 'R')
per favore.mostrare()

Questo era nei dati di allenamento. Come facciamo a sapere se il nostro modello non era troppo stretto?? È qui che entra in gioco il set di convalida., che abbiamo creato in precedenza:

pred = []
per io in gamma(Y_val.forma[0]):
    X, e = X_val[io], Y_val[io]
    prev_s = per esempio.zeri((nascosto_dime, 1))
    # Per ogni passo temporale...
    per T in gamma(T):
        mulù = per esempio.punto(tu, X)
        mulw = per esempio.punto(W, prev_s)
        Inserisci = mulw + mulù
        S = sigmoide(Inserisci)
        mulva = per esempio.punto(V, S)
        prev_s = S

    pred.aggiungere(mulva)
    
pred = per esempio.Vettore(pred)

per favore.complotto(pred[:, 0, 0], 'G')
per favore.complotto(Y_val[:, 0], 'R')
per favore.mostrare()

Niente di male. Le previsioni sembrano impressionanti. Anche il punteggio RMSE nei dati di convalida è rispettabile:

a partire dal sklearn.metrics importare mean_squared_error

matematica.sqrt(mean_squared_error(Y_val[:, 0] * max_val, pred[:, 0, 0] * max_val))
0.127191931509431

Note finali

Non posso sottolineare abbastanza quanto siano utili gli RNN quando si lavora con i dati di sequenza. Imploro tutti di prendere questo apprendimento e applicarlo a un set di dati. Affronta un ostacolo di PNL e vedi se riesci a trovare una soluzione. Puoi sempre contattarmi nella sezione commenti qui sotto se hai domande.

In questo post, abbiamo imparato come creare da zero un modello di rete neurale ricorrente usando solo la libreria numpy. Certo, puoi usare una libreria di alto livello come Keras o Caffe, ma è essenziale conoscere il concetto che stai implementando.

Condividi i tuoi pensieri, domande e commenti su questo post qui sotto. Buon apprendimento!

Iscriviti alla nostra Newsletter

Non ti invieremo posta SPAM. Lo odiamo quanto te.