Pana in acest moment tematica laboratorului a fost axata pe studiul de retele neuronale in care informatia este propagata mereu inainte, spre iesirea retelei (Feed Forward Networks). O clasa cu totul aparte de retele este reprezentata de retelele recurente (Recurrent Neural Networks - RNN). Dupa cum indica numele, aceasta familie de retele presupune ca informatia este intoarsa inapoi in retea.
Spre deosebire de problemele studiate pana in acest moment, retelele recurente sunt specializate in analiza unor secvente de esantioane. Fiecare esantion din secventa este analizat in ordine, rezultatul fiind apoi intors in retea, pentru a participa la procesarea urmatorului esantion.
Aceasta abordare este de o utilitate sporita in probleme precum:

In figura de mai sus, reteaua $A$ are drept intrare esantionul $x_t$ si iesire neuronul $h_t$. Diferenta fata de un FFN este dat de faptul ca informatia procesata este intoarsa printr-o bucla in reteaua A.
Aceasta bucla poate fi desfasurata astfel incat reteaua sa capete aspectul unui FFN, conform figurii de mai jos.

Abilitatea de a utiliza informatie procesata in trecut fac ca RNN-urile sa devina niste unelte foarte interesante pentru anumite tipuri de probleme. Din pacate, "memoria" de care da dovada acest tip de arhitectura este greu de gestionat pentru secvente lungi de date. In acest caz, informatia prelucrata la un pas s-ar putea sa fie profund deformata pana la momentul viitor in care aportul ei este critic. In teorie, arhitecturile recurente nu ar trebui sa prezinte astfel de probleme, insa practica demonstreaza dificultatile analizei temporale a datelor.
Problema propagarii informatiei pe durate foarte lungi este adresata in mod direct de arhitectura Long Short Term Memory, prezentata in figura de mai jos (dreapta).

Principalul avantaj al LSTM este prezenta unor informatii suplimentare, care reprezinta "starea" actuala a celulei. Aceasta stare este modificata la fiecare esantion din secventa curenta. In prima instanta, o parte din starea primita la intrare este "uitata" (operatorul de inmultire de sus). Apoi, la noua stare sunt adaugate informatii din prelucrarea esantionului curent si a iesirii venite de la esantionul anterior. Aceasta stare este apoi transmisa pentru a servi prelucrarii urmatorului esantion, dar si pentru a determina iesirea din pasul curent. Astfel, o celula LSTM trimite catre pasul urmator atat iesirea, cat si starea curenta.

In mod evident, o schimbare atat de mare fata de retelele studiate pana in acest moment va atrage si niste modificari in codul necesar pentru descrierea retelei. Pentru inceput, trebuie modificat modul in care intra datele in retea:
x = tf.placeholder(tf.float32,[None,nr_esantioane_per_secventa,nr_trasaturi_per_esantion])
De asemenea, apare o noua structura, anume cea a celulei LSTM, careia trebuie sa ii specificam numarul de iesiri:
neuroni = 32
cell = tf.nn.rnn_cell.LSTMCell(neuroni,state_is_tuple=True)
Acum ca este creata celula, trebuie specificate variabilele de intrare, precum si cele in care se stocheaza iesirile de la fiecare pas si fiecare stare:
val, stare = tf.nn.dynamic_rnn(cell,x,dtype=tf.float32)
O alta particularitate a lucrului cu RNN in Tensorflow este legat de modul in care se obtin rezultatele din celulele recurente:
val_out = tf.transpose(val,[1,0,2])
out_cell = tf.gather(val_out,int(val_out.get_shape()[0])-1)
Pentru a intelege mai usor aceste linii, se vor folosi urmatoarele valori numerice:
val contine un tensor cu iesirile de la propagarea in celula a tuturor celor 64 de esantioane din secventa. Astfel, val are dimensiunea $[128,64,32]$: 128 de secvente studiate, fiecare secventa avand 64 de esantioane, iesirea analizei fiecarui esantion fiind un vector cu 32 de valori:$$secventa\ 0 \ [0,:,:] \rightarrow \begin{cases} esantion \ 0 \ [0,0,:] & x_{0,0,0},x_{0,0,1},...,x_{0,0,31}\\ esantion \ 1 \ [0,1,:] & x_{0,1,0},x_{0,1,1},...,x_{0,1,31}\\ ...\\ esantion \ 63 \ [0,63,:] & x_{0,63,0},x_{0,63,1},...,x_{0,63,31} \end{cases}\\ secventa\ 1 \ [1,:,:] \rightarrow \begin{cases} esantion \ 0 \ [1,0,:] & x_{1,0,0},x_{1,0,1},...,x_{1,0,31}\\ esantion \ 1 \ [1,1,:] & x_{1,1,0},x_{1,1,1},...,x_{1,1,31}\\ ...\\ esantion \ 63 \ [1,63,:] & x_{1,63,0},x_{1,63,1},...,x_{1,63,31} \end{cases}\\ .\\.\\.\\ secventa\ 127 \ [127,:,:] \rightarrow \begin{cases} esantion \ 0 \ [127,0,:] & x_{127,0,0},x_{127,0,1},...,x_{127,0,31}\\ esantion \ 1 \ [127,1,:] & x_{127,1,0},x_{127,1,1},...,x_{127,1,31}\\ ...\\ esantion \ 63 \ [127,63,:] & x_{127,63,0},x_{127,63,1},...,x_{127,63,31} \end{cases} $$
Pentru continuarea retelei este de interes rezultatul obtinut dupa evaluarea ultimului esantion din secventa. Pentru a obtine aceste valori, se foloseste functia tf.gather careia ii trebuie specificata intrarea de la care sa fie adunate datele. Inversand axele intre ele cu tf.transpose, se poate aduce axa cu cele 64 de esantioane din secventa ca prim indice al matricei. In acest moment val_out are dimensiunea $[64,128,32]$.
Tot ce ramane de facut este sa se mentioneze pozitia de la care trebuie adunate datele, in cazul curent, ultima (int(val_out.get_shape()[0])-1). In acest moment se aduna toate valorile de la ultima pozitie a axei 0, ramanand cu cele 32 de trasaturi (iesirea celulei) aferente ultimului esantion din fiecare secventa ale celor 128 de secvente.
Aceste valori pot fi privite ca niste descriptori (cum este cazul dupa straturile convolutionale, sau stratul ascuns dintr-un MLP). Ele sunt apoi introduse intr-un strat Fully Connected, astfel incat sa obtinem numarul de neuroni dorit la iesire:
out = tf.layers.dense(out_cell,nr_clase,name='out')
Modul in care trebuie sa privim problema acum este ca in loc sa analizam toata imaginea, se studiaza linie cu linie imaginea, fiecare linie aducand informatii noi despre rezultatul final.