CNTK - Rete neurale ricorrente

Ora, vediamo come costruire una rete neurale ricorrente (RNN) in CNTK.

introduzione

Abbiamo imparato a classificare le immagini con una rete neurale, ed è uno dei lavori iconici nel deep learning. Ma un'altra area in cui la rete neurale eccelle e molte ricerche in corso sono le reti neurali ricorrenti (RNN). Qui, sapremo cos'è l'RNN e come può essere utilizzato in scenari in cui abbiamo bisogno di gestire dati di serie temporali.

Cos'è la rete neurale ricorrente?

Le reti neurali ricorrenti (RNN) possono essere definite come la razza speciale di NN in grado di ragionare nel tempo. Gli RNN vengono utilizzati principalmente negli scenari, in cui è necessario gestire valori che cambiano nel tempo, ovvero dati di serie temporali. Per capirlo in modo migliore, facciamo un piccolo confronto tra reti neurali regolari e reti neurali ricorrenti -

  • Come sappiamo, in una normale rete neurale, possiamo fornire un solo input. Questo lo limita ai risultati in una sola previsione. Per darti un esempio, possiamo tradurre il lavoro di testo utilizzando normali reti neurali.

  • D'altra parte, nelle reti neurali ricorrenti, possiamo fornire una sequenza di campioni che si traduce in una singola previsione. In altre parole, utilizzando gli RNN possiamo prevedere una sequenza di output in base a una sequenza di input. Ad esempio, ci sono stati alcuni esperimenti di successo con RNN in attività di traduzione.

Usi della rete neurale ricorrente

Gli RNN possono essere utilizzati in diversi modi. Alcuni di loro sono i seguenti:

Previsione di un singolo output

Prima di approfondire i passaggi, che come RNN può prevedere un singolo output in base a una sequenza, vediamo come appare un RNN di base:

Come possiamo nel diagramma sopra, RNN contiene una connessione loopback all'ingresso e ogni volta che forniamo una sequenza di valori elaborerà ogni elemento nella sequenza come fasi temporali.

Inoltre, a causa della connessione loopback, RNN può combinare l'output generato con l'input per l'elemento successivo nella sequenza. In questo modo, RNN costruirà una memoria sull'intera sequenza che può essere utilizzata per fare una previsione.

Per fare previsioni con RNN, possiamo eseguire i seguenti passaggi:

  • Innanzitutto, per creare uno stato nascosto iniziale, dobbiamo alimentare il primo elemento della sequenza di input.

  • Dopodiché, per produrre uno stato nascosto aggiornato, dobbiamo prendere lo stato nascosto iniziale e combinarlo con il secondo elemento nella sequenza di input.

  • Infine, per produrre lo stato nascosto finale e prevedere l'output per l'RNN, dobbiamo prendere l'elemento finale nella sequenza di input.

In questo modo, con l'aiuto di questa connessione loopback possiamo insegnare a un RNN a riconoscere i modelli che si verificano nel tempo.

Prevedere una sequenza

Il modello di base, discusso sopra, di RNN può essere esteso anche ad altri casi d'uso. Ad esempio, possiamo usarlo per prevedere una sequenza di valori basata su un singolo input. In questo scenario, per fare previsioni con RNN possiamo eseguire i seguenti passaggi:

  • In primo luogo, per creare uno stato nascosto iniziale e prevedere il primo elemento nella sequenza di output, dobbiamo inserire un campione di input nella rete neurale.

  • Dopodiché, per produrre uno stato nascosto aggiornato e il secondo elemento nella sequenza di output, dobbiamo combinare lo stato nascosto iniziale con lo stesso campione.

  • Alla fine, per aggiornare ancora una volta lo stato nascosto e prevedere l'elemento finale nella sequenza di output, alimentiamo il campione un'altra volta.

Previsione delle sequenze

Come abbiamo visto come prevedere un singolo valore in base a una sequenza e come prevedere una sequenza in base a un singolo valore. Vediamo ora come possiamo prevedere sequenze per sequenze. In questo scenario, per fare previsioni con RNN possiamo eseguire i seguenti passaggi:

  • Innanzitutto, per creare uno stato nascosto iniziale e prevedere il primo elemento nella sequenza di output, dobbiamo prendere il primo elemento nella sequenza di input.

  • Dopodiché, per aggiornare lo stato nascosto e prevedere il secondo elemento nella sequenza di output, dobbiamo prendere lo stato nascosto iniziale.

  • Infine, per prevedere l'elemento finale nella sequenza di output, dobbiamo prendere lo stato nascosto aggiornato e l'elemento finale nella sequenza di input.

Lavoro di RNN

Per comprendere il funzionamento delle reti neurali ricorrenti (RNN), dobbiamo prima capire come funzionano i livelli ricorrenti nella rete. Quindi prima discutiamo come e può prevedere l'output con uno strato ricorrente standard.

Previsione dell'output con livello RNN standard

Come abbiamo discusso in precedenza, anche uno strato di base in RNN è abbastanza diverso da uno strato normale in una rete neurale. Nella sezione precedente, abbiamo anche dimostrato nel diagramma l'architettura di base di RNN. Per aggiornare lo stato nascosto per la prima sequenza step-in possiamo usare la seguente formula:

Nell'equazione precedente, calcoliamo il nuovo stato nascosto calcolando il prodotto scalare tra lo stato nascosto iniziale e un insieme di pesi.

Ora per il passaggio successivo, lo stato nascosto per il passaggio temporale corrente viene utilizzato come stato nascosto iniziale per il passaggio temporale successivo nella sequenza. Ecco perché, per aggiornare lo stato nascosto per la seconda fase temporale, possiamo ripetere i calcoli eseguiti nella prima fase come segue:

Successivamente, possiamo ripetere il processo di aggiornamento dello stato nascosto per il terzo e ultimo passaggio nella sequenza come di seguito -

E quando abbiamo elaborato tutti i passaggi precedenti nella sequenza, possiamo calcolare l'output come segue:

Per la formula precedente, abbiamo utilizzato una terza serie di pesi e lo stato nascosto dalla fase temporale finale.

Unità ricorrenti avanzate

Il problema principale con lo strato ricorrente di base è il problema del gradiente di scomparsa e per questo motivo non è molto bravo ad apprendere le correlazioni a lungo termine. In parole semplici, il livello ricorrente di base non gestisce molto bene le sequenze lunghe. Questo è il motivo per cui alcuni altri tipi di layer ricorrenti che sono molto più adatti per lavorare con sequenze più lunghe sono i seguenti:

Memoria a lungo termine (LSTM)

Le reti di memoria a lungo termine (LSTM) sono state introdotte da Hochreiter & Schmidhuber. Ha risolto il problema di ottenere un livello ricorrente di base per ricordare le cose per molto tempo. L'architettura di LSTM è data sopra nel diagramma. Come possiamo vedere, ha neuroni di input, celle di memoria e neuroni di output. Per combattere il problema del gradiente di fuga, le reti di memoria a lungo termine utilizzano una cella di memoria esplicita (memorizza i valori precedenti) e le seguenti porte:

  • Forget gate- Come suggerisce il nome, dice alla cella di memoria di dimenticare i valori precedenti. La cella di memoria memorizza i valori fino a quando il gate, ovvero "dimentica porta", gli dice di dimenticarli.

  • Input gate- Come suggerisce il nome, aggiunge nuove cose alla cella.

  • Output gate- Come suggerisce il nome, la porta di uscita decide quando passare lungo i vettori dalla cella al successivo stato nascosto.

Gated Recurrent Units (GRU)

Gradient recurrent units(GRUs) è una leggera variazione della rete LSTM. Ha un gate in meno e sono cablati leggermente diversi dagli LSTM. La sua architettura è mostrata nel diagramma sopra. Ha neuroni di input, celle di memoria con gate e neuroni di output. La rete Gated Recurrent Units ha le seguenti due porte:

  • Update gate- Determina le seguenti due cose:

    • Quale quantità di informazioni dovrebbe essere mantenuta dall'ultimo stato?

    • Quale quantità di informazioni dovrebbe essere inserita dal livello precedente?

  • Reset gate- La funzionalità di reset gate è molto simile a quella di dimenticare gate della rete LSTM. L'unica differenza è che si trova in modo leggermente diverso.

A differenza della rete di memoria a lungo termine, le reti Gated Recurrent Unit sono leggermente più veloci e più facili da eseguire.

Creazione della struttura RNN

Prima di poter iniziare, fare previsioni sull'output da una qualsiasi delle nostre origini dati, dobbiamo prima costruire RNN e costruire RNN è abbastanza uguale a come avevamo costruito una rete neurale regolare nella sezione precedente. Di seguito è riportato il codice per crearne uno -

from cntk.losses import squared_error
from cntk.io import CTFDeserializer, MinibatchSource, INFINITELY_REPEAT, StreamDefs, StreamDef
from cntk.learners import adam
from cntk.logging import ProgressPrinter
from cntk.train import TestConfig
BATCH_SIZE = 14 * 10
EPOCH_SIZE = 12434
EPOCHS = 10

Picchettare più livelli

Possiamo anche impilare più livelli ricorrenti in CNTK. Ad esempio, possiamo usare la seguente combinazione di livelli -

from cntk import sequence, default_options, input_variable
from cntk.layers import Recurrence, LSTM, Dropout, Dense, Sequential, Fold
features = sequence.input_variable(1)
with default_options(initial_state = 0.1):
   model = Sequential([
      Fold(LSTM(15)),
      Dense(1)
   ])(features)
target = input_variable(1, dynamic_axes=model.dynamic_axes)

Come possiamo vedere nel codice sopra, abbiamo i seguenti due modi in cui possiamo modellare RNN in CNTK:

  • Innanzitutto, se vogliamo solo l'output finale di un livello ricorrente, possiamo usare il Fold livello in combinazione con un livello ricorrente, come GRU, LSTM o anche RNNStep.

  • In secondo luogo, in alternativa, possiamo anche utilizzare l'estensione Recurrence bloccare.

Formazione RNN con dati di serie temporali

Una volta costruito il modello, vediamo come possiamo addestrare RNN in CNTK -

from cntk import Function
@Function
def criterion_factory(z, t):
   loss = squared_error(z, t)
   metric = squared_error(z, t)
   return loss, metric
loss = criterion_factory(model, target)
learner = adam(model.parameters, lr=0.005, momentum=0.9)

Ora per caricare i dati nel processo di addestramento, dobbiamo deserializzare le sequenze da un set di file CTF. Il codice seguente ha l'estensionecreate_datasource funzione, che è una funzione di utilità utile per creare sia l'origine dati di addestramento che quella di test.

target_stream = StreamDef(field='target', shape=1, is_sparse=False)
features_stream = StreamDef(field='features', shape=1, is_sparse=False)
deserializer = CTFDeserializer(filename, StreamDefs(features=features_stream, target=target_stream))
   datasource = MinibatchSource(deserializer, randomize=True, max_sweeps=sweeps)
return datasource
train_datasource = create_datasource('Training data filename.ctf')#we need to provide the location of training file we created from our dataset.
test_datasource = create_datasource('Test filename.ctf', sweeps=1) #we need to provide the location of testing file we created from our dataset.

Ora, dopo aver impostato le origini dati, il modello e la funzione di perdita, possiamo avviare il processo di addestramento. È abbastanza simile a quello che abbiamo fatto nelle sezioni precedenti con le reti neurali di base.

progress_writer = ProgressPrinter(0)
test_config = TestConfig(test_datasource)
input_map = {
   features: train_datasource.streams.features,
   target: train_datasource.streams.target
}
history = loss.train(
   train_datasource,
   epoch_size=EPOCH_SIZE,
   parameter_learners=[learner],
   model_inputs_to_streams=input_map,
   callbacks=[progress_writer, test_config],
   minibatch_size=BATCH_SIZE,
   max_epochs=EPOCHS
)

Otterremo l'output simile come segue:

Uscita−

average  since  average  since  examples
loss      last  metric  last
------------------------------------------------------
Learning rate per minibatch: 0.005
0.4      0.4    0.4      0.4      19
0.4      0.4    0.4      0.4      59
0.452    0.495  0.452    0.495   129
[…]

Convalida del modello

In realtà ridipingere con un RNN è abbastanza simile a fare previsioni con qualsiasi altro modello CNK. L'unica differenza è che, dobbiamo fornire sequenze piuttosto che campioni singoli.

Ora, poiché il nostro RNN ha finalmente terminato l'addestramento, possiamo convalidare il modello testandolo utilizzando una sequenza di alcuni campioni come segue:

import pickle
with open('test_samples.pkl', 'rb') as test_file:
test_samples = pickle.load(test_file)
model(test_samples) * NORMALIZE

Uscita−

array([[ 8081.7905],
[16597.693 ],
[13335.17 ],
...,
[11275.804 ],
[15621.697 ],
[16875.555 ]], dtype=float32)