Introducere
Problemele de predicție a secvențelor există de mult timp. Ele sunt considerate ca fiind una dintre cele mai greu de rezolvat probleme din industria științei datelor. Acestea includ o gamă largă de probleme; de la prezicerea vânzărilor la găsirea modelelor în datele de pe piețele bursiere, de la înțelegerea parcelelor de film la recunoașterea modului în care vorbiți, de la traduceri de limbi străine la prezicerea următorului cuvânt pe tastatura iPhone-ului dumneavoastră.
Cu recentele descoperiri care au avut loc în domeniul științei datelor, s-a constatat că, pentru aproape toate aceste probleme de predicție a secvențelor, rețelele cu memorie pe termen scurt, cunoscute și sub numele de LSTM, au fost observate ca fiind cea mai eficientă soluție.
LSTM au un avantaj față de rețelele neuronale convenționale de tip feed-forward și RNN în multe privințe. Acest lucru se datorează proprietății lor de a memora selectiv modele pentru durate lungi de timp. Scopul acestui articol este de a explica LSTM și de a vă permite să le folosiți în probleme din viața reală. Să aruncăm o privire!
Nota: Pentru a parcurge articolul, trebuie să aveți cunoștințe de bază despre rețele neuronale și despre cum funcționează Keras (o bibliotecă de învățare profundă). Puteți consulta articolele menționate pentru a înțelege aceste concepte:
- Understanding Neural Network From Scratch
- Fundamentals of Deep Learning – Introduction to Recurrent Neural Networks
- Tutorial: Optimizarea rețelelor neuronale folosind Keras (cu un studiu de caz de recunoaștere a imaginilor)
Tabel de materii
- Flashback: O privire asupra rețelelor neuronale recurente (RNN)
- Limitări ale RNN-urilor
- Îmbunătățiri față de RNN : Memorie pe termen scurt (LSTM)
- Arhitectura LSTM
- Poarta de uitare
- Poarta de intrare
- Poarta de ieșire
- Generarea de text cu ajutorul LSTM.
Flashback: O privire asupra rețelelor neuronale recurente (RNN)
Să luăm un exemplu de date secvențiale, care pot fi datele de la bursă pentru o anumită acțiune. Un model simplu de învățare automată sau o rețea neuronală artificială poate învăța să prezică prețurile acțiunilor pe baza unui număr de caracteristici: volumul acțiunilor, valoarea de deschidere etc. În timp ce prețul acțiunilor depinde de aceste caracteristici, el depinde, de asemenea, în mare măsură, de valorile acțiunilor din zilele anterioare. De fapt, pentru un trader, aceste valori din zilele anterioare (sau tendința) reprezintă un factor decisiv major pentru predicții.
În rețelele neuronale convenționale de tip feed-forward, toate cazurile de testare sunt considerate independente. Adică atunci când se potrivește modelul pentru o anumită zi, nu se iau în considerare prețurile acțiunilor din zilele anterioare.
Această dependență de timp este realizată prin intermediul rețelelor neuronale recurente. O RNN tipică arată astfel:
Acesta poate fi intimidant la prima vedere, dar odată desfășurat, pare mult mai simplu:
Acum ne este mai ușor să vizualizăm modul în care aceste rețele iau în considerare tendința prețurilor la bursă, înainte de a prezice prețurile acțiunilor pentru astăzi. Aici fiecare predicție la momentul t (h_t) depinde de toate predicțiile anterioare și de informațiile învățate din acestea.
RNN-urile pot rezolva scopul nostru de tratare a secvențelor într-o mare măsură, dar nu în întregime. Vrem ca calculatoarele noastre să fie suficient de bune pentru a scrie sonete shakespeariene. Acum, RNN-urile sunt grozave când vine vorba de contexte scurte, dar pentru a putea construi o poveste și a o reține, avem nevoie ca modelele noastre să fie capabile să înțeleagă și să rețină contextul din spatele secvențelor, exact ca un creier uman. Acest lucru nu este posibil cu un simplu RNN.
De ce? Să aruncăm o privire.
Limitații ale RNN-urilor
Rețelele neuronale recurente funcționează foarte bine atunci când avem de-a face cu dependențe pe termen scurt. Adică atunci când sunt aplicate la probleme precum:
RNN-urile se dovedesc a fi destul de eficiente. Acest lucru se datorează faptului că această problemă nu are nimic de-a face cu contextul enunțului. RNN nu trebuie să-și amintească ce s-a spus înainte de aceasta sau care a fost sensul ei, tot ce trebuie să știe este că în majoritatea cazurilor cerul este albastru. Astfel, predicția ar fi:
Cu toate acestea, RNN-urile de vanilie nu reușesc să înțeleagă contextul din spatele unei intrări. Ceva care a fost spus cu mult timp înainte, nu poate fi reamintit atunci când se fac predicții în prezent. Să înțelegem acest lucru ca un exemplu:
Aici, putem înțelege că, din moment ce autorul a lucrat în Spania timp de 20 de ani, este foarte probabil ca el să posede o bună stăpânire a limbii spaniole. Dar, pentru a face o predicție corectă, RNN trebuie să rețină acest context. Informațiile relevante pot fi separate de punctul în care sunt necesare, printr-o încărcătură uriașă de date irelevante. Acesta este punctul în care o rețea neuronală recurentă eșuează!
Motivul din spatele acestei situații este problema gradientului de dispariție. Pentru a înțelege acest lucru, va trebui să aveți câteva cunoștințe despre cum învață o rețea neuronală feed-forward. Știm că pentru o rețea neuronală feed-forward convențională, actualizarea ponderii care se aplică pe un anumit strat este un multiplu al ratei de învățare, al termenului de eroare din stratul anterior și al intrării în acel strat. Astfel, termenul de eroare pentru un anumit strat este undeva un produs al tuturor erorilor din straturile anterioare. În cazul funcțiilor de activare, cum ar fi funcția sigmoidă, valorile mici ale derivatelor sale (care apar în funcția de eroare) sunt multiplicate de mai multe ori pe măsură ce ne îndreptăm spre straturile inițiale. Ca urmare a acestui fapt, gradientul aproape dispare pe măsură ce ne deplasăm spre straturile inițiale și devine dificil de antrenat aceste straturi.
Un caz similar se observă în cazul rețelelor neuronale recurente. RNN reține lucruri doar pentru durate mici de timp, adică dacă avem nevoie de informația respectivă după o perioadă mică de timp, aceasta poate fi reproductibilă, dar odată ce sunt introduse o mulțime de cuvinte, această informație se pierde undeva. Această problemă poate fi rezolvată prin aplicarea unei versiuni ușor modificate a RNN – Rețelele de memorie pe termen scurt de lungă durată.
îmbunătățire față de RNN: LSTM (Long Short-Term Memory) Networks
Când ne aranjăm calendarul pentru o zi, ne prioritizăm întâlnirile, nu-i așa? Dacă în cazul în care trebuie să facem loc pentru ceva important, știm ce întâlnire ar putea fi anulată pentru a găzdui o posibilă întâlnire.
Se pare că o RNN nu face acest lucru. Pentru a adăuga o informație nouă, ea transformă complet informația existentă prin aplicarea unei funcții. Din această cauză, întreaga informație este modificată, în ansamblu, adică nu se ține cont de informația „importantă” și de informația „mai puțin importantă”.
LSTM, pe de altă parte, efectuează mici modificări ale informațiilor prin înmulțiri și adăugiri. În cazul LSTM-urilor, informațiile circulă printr-un mecanism cunoscut sub numele de stări ale celulelor. În acest fel, LSTM-urile își pot aminti sau uita lucruri în mod selectiv. Informațiile aflate într-o anumită stare a celulei au trei dependențe diferite.
Vom vizualiza acest lucru cu un exemplu. Să luăm exemplul de a prezice prețurile acțiunilor pentru o anumită acțiune. Prețul acțiunilor de astăzi va depinde de:
- Tendința pe care acțiunea a urmat-o în zilele anterioare, poate un trend descendent sau un trend ascendent.
- Prețul acțiunii din ziua precedentă, deoarece mulți comercianți compară prețul acțiunii din ziua precedentă înainte de a o cumpăra.
- Factori care pot afecta prețul acțiunii de astăzi. Aceasta poate fi o nouă politică a companiei care este criticată pe scară largă, sau o scădere a profitului companiei, sau poate o schimbare neașteptată în conducerea superioară a companiei.
Aceste dependențe pot fi generalizate la orice problemă ca:
- Starea anterioară a celulei (adică informația care era prezentă în memorie după pasul de timp anterior)
- Starea ascunsă anterioară (adică. aceasta este aceeași cu ieșirea celulei anterioare)
- Intrarea la pasul de timp curent (adică noile informații care sunt introduse în acel moment)
O altă caracteristică importantă a LSTM este analogia sa cu benzile transportoare!
Adevărat!
Industriile le folosesc pentru a deplasa produsele pentru diferite procese. LSTM-urile folosesc acest mecanism pentru a deplasa informații.
Este posibil să avem unele adăugiri, modificări sau eliminări de informații pe măsură ce acestea circulă prin diferitele straturi, la fel cum un produs poate fi turnat, vopsit sau ambalat în timp ce se află pe o bandă transportoare.
Diagrama de mai jos explică relația strânsă dintre LSTM-uri și benzile transportoare.
Sursa
Deși această diagramă nu este nici măcar apropiată de arhitectura reală a unui LSTM, ea ne rezolvă deocamdată scopul.
Doar datorită acestei proprietăți a LSTM-urilor, prin care nu manipulează întreaga informație, ci mai degrabă o modifică ușor, ele sunt capabile să uite și să rețină lucruri în mod selectiv. Cum fac acest lucru, este ceea ce vom învăța în secțiunea următoare?
Arhitectura LSTM-urilor
Funcționarea LSTM-urilor poate fi vizualizată prin înțelegerea funcționării echipei unui canal de știri care acoperă o poveste de crimă. Acum, o știre este construită în jurul unor fapte, dovezi și declarații ale mai multor persoane. Ori de câte ori apare un eveniment nou, se parcurge unul dintre cei trei pași.
Să spunem că am presupus că crima a fost comisă prin „otrăvirea” victimei, dar raportul autopsiei care tocmai a sosit spune că cauza morții a fost „un impact la cap”. Făcând parte din această echipă de știri, ce faceți? Uitați imediat de cauza anterioară a morții și de toate poveștile care au fost țesute în jurul acestui fapt.
Ce, dacă un suspect complet nou este introdus în peisaj. O persoană care a avut resentimente față de victimă și care ar putea fi criminalul? Introduci această informație în fluxul tău de știri, nu-i așa?
Acum toate aceste informații sparte nu mai pot fi servite în mass-media mainstream. Așadar, după un anumit interval de timp, trebuie să rezumați aceste informații și să scoateți la iveală lucrurile relevante pentru publicul dumneavoastră. Poate sub forma „XYZ se dovedește a fi suspectul principal.”.
Acum să intrăm în detaliile arhitecturii rețelei LSTM:
Sursa
Acum, acest lucru nu este nici pe departe apropiat de versiunea simplificată pe care am văzut-o mai devreme, dar permiteți-mi să vă conduc prin ea. O rețea LSTM tipică este compusă din diferite blocuri de memorie numite celule
(dreptunghiurile pe care le vedem în imagine). Există două stări care sunt transferate către următoarea celulă; starea celulei și starea ascunsă. Blocurile de memorie sunt responsabile de memorarea lucrurilor, iar manipularea acestei memorii se face prin trei mecanisme majore, numite porți. Fiecare dintre ele este discutat mai jos.
4.1 Poarta de uitare
Să luăm exemplul unei probleme de predicție a unui text. Să presupunem că un LSTM este alimentat cu următoarea propoziție:
De îndată ce este întâlnit primul punct după „persoană”, poarta de uitare își dă seama că ar putea exista o schimbare de context în propoziția următoare. Ca urmare a acestui fapt, subiectul propoziției este uitat și locul pentru subiect este eliberat. Iar când începem să vorbim despre „Dan”, această poziție a subiectului este alocată lui „Dan”. Acest proces de uitare a subiectului este provocat de poarta de uitare.
O poartă de uitare este responsabilă de eliminarea informațiilor din starea celulei. Informațiile care nu mai sunt necesare pentru ca LSTM să înțeleagă lucrurile sau informațiile care au o importanță mai mică sunt eliminate prin multiplicarea unui filtru. Acest lucru este necesar pentru optimizarea performanțelor rețelei LSTM.
Această poartă primește două intrări; h_t-1 și x_t.
h_t-1 este starea ascunsă din celula anterioară sau ieșirea celulei anterioare, iar x_t este intrarea la acel pas de timp. Intrările date sunt înmulțite cu matricile de ponderare și se adaugă o polarizare. În continuare, la această valoare se aplică funcția sigmoidă. Funcția sigmoidă produce un vector, cu valori cuprinse între 0 și 1, care corespunde fiecărui număr din starea celulei. Practic, funcția sigmoidă este responsabilă pentru a decide ce valori trebuie păstrate și care trebuie eliminate. Dacă se emite un „0” pentru o anumită valoare din starea celulei, înseamnă că poarta de uitare dorește ca starea celulei să uite complet acea informație. În mod similar, un „1” înseamnă că poarta de uitare dorește să rețină întreaga informație respectivă. Acest vector de ieșire al funcției sigmoide este multiplicat la starea celulei.
4.2 Poarta de intrare
Bine, să luăm un alt exemplu în care LSTM analizează o propoziție:
Acum, informația importantă aici este că „Bob” știe să înoate și că a servit în Marină timp de patru ani. Acest lucru poate fi adăugat la starea celulei, însă, faptul că a spus toate acestea la telefon este un fapt mai puțin important și poate fi ignorat. Acest proces de adăugare a unor informații noi se poate face prin intermediul porții de intrare.
Iată structura acesteia:
Poarta de intrare este responsabilă pentru adăugarea de informații la starea celulei. Această adăugare de informații este practic un proces în trei pași, așa cum se vede din diagrama de mai sus.
- Reglarea valorilor care trebuie adăugate la starea celulei prin implicarea unei funcții sigmoide. Aceasta este practic foarte asemănătoare cu poarta de uitare și acționează ca un filtru pentru toate informațiile de la h_t-1 și x_t.
- Crearea unui vector care conține toate valorile posibile care pot fi adăugate (așa cum sunt percepute de la h_t-1 și x_t) la starea celulei. Acest lucru se realizează cu ajutorul funcției tanh, care produce valori de la -1 la +1.
- Multiplicarea valorii filtrului de reglare (poarta sigmoidă) la vectorul creat (funcția tanh) și apoi adăugarea acestor informații utile la starea celulei prin operația de adiție.
Odată ce acest proces în trei etape este realizat, ne asigurăm că la starea celulei se adaugă doar acele informații care sunt importante și nu sunt redundante.
4.3 Poarta de ieșire
Nu toate informațiile care se derulează de-a lungul stării celulei, sunt potrivite pentru a fi scoase la un moment dat. Vom vizualiza acest lucru cu un exemplu:
În această frază, ar putea exista o serie de opțiuni pentru spațiul gol. Dar știm că intrarea curentă de „curajos”, este un adjectiv care este folosit pentru a descrie un substantiv. Astfel, orice cuvânt care urmează, are o tendință puternică de a fi un substantiv. Și astfel, Bob ar putea fi o ieșire potrivită.
Această sarcină de selectare a informațiilor utile din starea curentă a celulei și de afișare a acestora ca o ieșire este realizată prin intermediul porții de ieșire. Iată care este structura sa:
Funcționarea unei porți de ieșire poate fi din nou împărțită în trei etape:
- Crearea unui vector după aplicarea funcției tanh la starea celulei, scalând astfel valorile în intervalul -1 la +1.
- Realizarea unui filtru folosind valorile h_t-1 și x_t, astfel încât să poată regla valorile care trebuie să iasă din vectorul creat mai sus. Acest filtru folosește din nou o funcție sigmoidă.
- Multiplicarea valorii acestui filtru de reglare la vectorul creat la pasul 1 și trimiterea acestuia ca ieșire și, de asemenea, la starea ascunsă a celulei următoare.
Filtrul din exemplul de mai sus se va asigura că diminuează toate celelalte valori, cu excepția lui ‘Bob’. Astfel, filtrul trebuie să fie construit pe valorile de intrare și de stare ascunsă și să fie aplicat pe vectorul de stare al celulei.
Generarea de text cu ajutorul LSTM-urilor
Ne-am săturat de conceptele teoretice și de funcționarea LSTM-urilor. Acum am vrea să încercăm să construim un model care să poată prezice un număr n de caractere după textul original din Macbeth. Majoritatea textelor clasice nu mai sunt protejate de drepturi de autor și pot fi găsite aici. O versiune actualizată a fișierului .txt poate fi găsită aici.
Vom folosi biblioteca Keras, care este un API de nivel înalt pentru rețele neuronale și funcționează pe lângă TensorFlow sau Theano. Așadar, asigurați-vă că înainte de a vă scufunda în acest cod aveți Keras instalat și funcțional.
Ok, deci haideți să generăm niște text!
-
Importul dependențelor
# Importing dependencies numpy and kerasimport numpyfrom keras.models import Sequentialfrom keras.layers import Densefrom keras.layers import Dropoutfrom keras.layers import LSTMfrom keras.utils import np_utils
Importăm toate dependențele necesare și acest lucru este destul de explicit.
-
Încărcarea fișierului text și crearea de corespondențe între caractere și numere întregi
# load textfilename = "/macbeth.txt"text = (open(filename).read()).lower()# mapping characters with integersunique_chars = sorted(list(set(text)))char_to_int = {}int_to_char = {}for i, c in enumerate (unique_chars): char_to_int.update({c: i}) int_to_char.update({i: c})
Se deschide fișierul text, iar toate caracterele sunt convertite în litere minuscule. Pentru a facilita următorii pași, se va face o mapare a fiecărui caracter cu un număr respectiv. Acest lucru se face pentru a ușura partea de calcul a LSTM.
-
Pregătirea setului de date
# preparing input and output datasetX = Y = for i in range(0, len(text) - 50, 1): sequence = text label =text X.append( for char in sequence]) Y.append(char_to_int)
Datele sunt pregătite într-un format astfel încât, dacă dorim ca LSTM să prezică „O” din „HELLO”, să le introducem ca intrare și ca ieșire așteptată. În mod similar, aici stabilim lungimea secvenței pe care o dorim (setată la 50 în exemplu) și apoi salvăm codificările primelor 49 de caractere în X și rezultatul așteptat, adică al 50-lea caracter în Y.
-
Reformarea lui X
# reshaping, normalizing and one hot encodingX_modified = numpy.reshape(X, (len(X), 50, 1))X_modified = X_modified / float(len(unique_chars))Y_modified = np_utils.to_categorical(Y)
O rețea LSTM se așteaptă ca intrarea să fie de forma în care eșantioane este numărul de puncte de date pe care le avem, pași de timp este numărul de pași dependenți de timp care există într-un singur punct de date, caracteristici se referă la numărul de variabile pe care le avem pentru valoarea adevărată corespunzătoare din Y. Apoi scalăm valorile din X_modified între 0 și 1 și unu la cald codificăm valorile noastre adevărate în Y_modified.
-
Definirea modelului LSTM
# defining the LSTM modelmodel = Sequential()model.add(LSTM(300, input_shape=(X_modified.shape, X_modified.shape), return_sequences=True))model.add(Dropout(0.2))model.add(LSTM(300))model.add(Dropout(0.2))model.add(Dense(Y_modified.shape, activation='softmax'))model.compile(loss='categorical_crossentropy', optimizer='adam')
Se utilizează un model secvențial care este o stivă liniară de straturi. Primul strat este un strat LSTM cu 300 de unități de memorie și returnează secvențe. Acest lucru se face pentru a se asigura că următorul strat LSTM primește secvențe și nu doar date împrăștiate la întâmplare. După fiecare strat LSTM se aplică un strat de abandon pentru a evita supraadaptarea modelului. În cele din urmă, ultimul strat este un strat complet conectat, cu o activare „softmax” și neuroni egali cu numărul de caractere unice, deoarece trebuie să emitem un rezultat codificat la cald.
-
Adaptarea modelului și generarea caracterelor
# fitting the modelmodel.fit(X_modified, Y_modified, epochs=1, batch_size=30)# picking a random seedstart_index = numpy.random.randint(0, len(X)-1)new_string = X# generating charactersfor i in range(50): x = numpy.reshape(new_string, (1, len(new_string), 1)) x = x / float(len(unique_chars)) #predicting pred_index = numpy.argmax(model.predict(x, verbose=0)) char_out = int_to_char seq_in = for value in new_string] print(char_out) new_string.append(pred_index) new_string = new_string
Modelul este adaptat pe 100 de epoci, cu o dimensiune a lotului de 30. Fixăm apoi o sămânță aleatorie (pentru o reproductibilitate ușoară) și începem să generăm caractere. Predicția din model oferă codificarea caracterului prezis, care este apoi decodificată înapoi la valoarea caracterului și adăugată la model.
Așa ar arăta ieșirea rețelei
În cele din urmă, după destule epoci de antrenament, aceasta va da rezultate din ce în ce mai bune de-a lungul timpului. Acesta este modul în care ați putea utiliza LSTM pentru a rezolva o sarcină de predicție a secvențelor.
Note de final
LSTM sunt o soluție foarte promițătoare pentru problemele legate de secvențe și serii temporale. Cu toate acestea, singurul dezavantaj pe care îl găsesc la ele, este dificultatea de a le antrena. Foarte mult timp și resurse de sistem sunt necesare pentru a antrena chiar și un model simplu. Dar aceasta este doar o constrângere hardware! Sper că am reușit să vă ofer o înțelegere de bază a acestor rețele. Pentru orice probleme sau probleme legate de blog, nu ezitați să comentați mai jos.