Predicting Stock Price with LSTM

Învățarea mașinilor și-a găsit aplicații în multe domenii interesante de-a lungul acestor ani. Îmblânzirea pieței bursiere este unul dintre ele. Mă gândeam să fac o încercare de ceva timp; mai ales pentru a-mi consolida cunoștințele de lucru despre LSTM-uri. Și, în cele din urmă, am terminat proiectul și sunt destul de încântat să împărtășesc experiența mea.

Motivație și public țintă

Voi scrie despre experiența mea de-a lungul unei serii de bloguri. Scopul acestei serii nu este de a explica noțiunile de bază ale LSTM sau ale conceptelor de Machine Learning. Prin urmare, voi presupune că cititorul și-a început călătoria cu Machine Learning și are cunoștințe de bază precum Python, familiaritate cu SkLearn, Keras, LSTM etc. Motivul este că există deja articole excelente pe teme precum „Cum funcționează LSTM-urile?”, scrise de persoane mult mai calificate pentru a explica matematica din spatele acestora. Dar voi împărtăși linkuri către astfel de articole, ori de câte ori simt că ar putea lipsi cunoștințele de bază. Deși există o mulțime de articole care vă spun cum să preziceți prețurile acțiunilor având în vedere un set de date, de cele mai multe ori autorii nu dezvăluie/explică cum au ajuns la acea configurație specială pentru o rețea neuronală sau cum au selectat acel set special de hiperparametri. Așadar, scopul real al acestui articol este de a împărtăși astfel de pași, greșelile mele și unii pași care mi s-au părut foarte utili. Ca atare, acest articol nu se limitează la problema Predicției prețului acțiunilor.

Iată care sunt lucrurile pe care le vom analiza :

  1. Citerea și analiza datelor. (Pandas)
  2. Normalizarea datelor. (SkLearn)
  3. Convertirea datelor în serii de timp și problema învățării supravegheate.
  4. Crearea modelului (Keras)
  5. Reglarea fină a modelului (în articolul următor)
  6. Învățarea, predicția și vizualizarea rezultatului.
  7. Tips & instrumente care mi s-au părut foarte utile (ultimul articol din serie)

Rețineți că acest prim articol vorbește despre etapele de preprocesare și terminologiile LSTM. Dacă sunteți destul de încrezători cu privire la acești pași, puteți sări la următorul articol.

Să începem!

Citerea și analiza datelor

Pentru acest articol voi folosi datele istorice ale prețului acțiunilor GE. Puteți găsi datele pe site-ul meu kaggle aici. Nu-mi amintesc sursa datelor, deoarece le descărcasem cu mult timp în urmă. Putem citi datele în cadru așa cum se arată mai jos :

df_ge = pd.read_csv(os.path.join(INPUT_PATH, "us.ge.txt"), engine='python')
df_ge.tail()

După cum puteți vedea, există aproximativ 14060 de elemente, fiecare reprezentând atributele bursiere ale unei zile pentru companie. Să vedem cum arată pe un grafic :

from matplotlib import pyplot as pltplt.figure()
plt.plot(df_ge)
plt.plot(df_ge)
plt.plot(df_ge)
plt.plot(df_ge)
plt.title('GE stock price history')
plt.ylabel('Price (USD)')
plt.xlabel('Days')
plt.legend(, loc='upper left')
plt.show()

Se pare că prețurile – Open, Close, Low, High – nu variază prea mult unul față de celălalt, cu excepția unor ușoare scăderi ocazionale ale prețului Low.

Acum să verificăm graficul pentru volum :

plt.figure()
plt.plot(df_ge)
plt.title('GE stock volume history')
plt.ylabel('Volume')
plt.xlabel('Days')
plt.show()

Huh. Ați văzut ceva interesant? Există o creștere destul de mare a numărului de tranzacții în jurul datei de 12000 de zile pe cronologie, care se întâmplă să coincidă cu scăderea bruscă a prețului acțiunilor. Poate că putem să ne întoarcem la acea dată anume și să căutăm articole de știri vechi pentru a afla ce a cauzat acest lucru.

Acum să vedem dacă avem vreo valoare nulă/Nan de care să ne facem griji. Se pare că nu avem nicio valoare nulă. Minunat!

print("checking if any null values are present\n", df_ge.isna().sum())

Normalizarea datelor

Datele nu sunt normalizate și intervalul pentru fiecare coloană variază, în special Volumul. Normalizarea datelor ajută algoritmul la convergență, adică la găsirea eficientă a minimului local/ global. Voi folosi MinMaxScaler din Sci-kit Learn. Dar, înainte de aceasta, trebuie să împărțim setul de date în seturi de date de instruire și de testare. De asemenea, voi converti DataFrame în ndarray în acest proces.

Conversia datelor în serii de timp și problema învățării supravegheate

Acest lucru este destul de important și oarecum complicat. Aici este nevoie de cunoștințele LSTM. Aș da o scurtă descriere a conceptelor cheie care sunt necesare aici, dar vă recomand cu tărie să citiți blogul lui Andre karpathy aici, care este considerat una dintre cele mai bune resurse despre LSTM de acolo și aceasta. Sau puteți viziona și videoclipul lui Andrew Ng (care, apropo, menționează și blogul lui Andre).

LSTM-urile consumă intrare în format ; a 3- dimensional array.

  • Batch Size spune câte eșantioane de intrare doriți să vadă rețeaua neuronală înainte de a actualiza ponderile. Deci, să spunem că aveți 100 de eșantioane (set de date de intrare) și doriți să actualizați ponderile de fiecare dată când NN dvs. a văzut o intrare. În acest caz, dimensiunea lotului ar fi 1, iar numărul total de loturi ar fi 100. În mod similar, dacă ați dori ca rețeaua dvs. să actualizeze ponderile după ce a văzut toate eșantioanele, dimensiunea lotului ar fi 100, iar numărul de loturi ar fi 1. Se pare că utilizarea unor loturi de dimensiuni foarte mici reduce viteza de instruire și, pe de altă parte, utilizarea unor loturi de dimensiuni prea mari (cum ar fi întregul set de date) reduce capacitatea modelelor de a generaliza la date diferite și, de asemenea, consumă mai multă memorie. Dar este nevoie de mai puțini pași pentru a găsi minimele pentru funcția obiectiv. Prin urmare, trebuie să încercați diferite valori pe datele dvs. și să găsiți punctul optim. Este un subiect destul de vast. Vom vedea cum să le căutăm într-un mod oarecum mai inteligent în următorul articol.
  • Pașii de timp definesc câte unități înapoi în timp doriți ca rețeaua dvs. să vadă. De exemplu, dacă lucrați la o problemă de predicție a caracterelor în care aveți un corpus de text pe care să vă antrenați și decideți să alimentați rețeaua dvs. cu 6 caractere la un moment dat. Atunci pasul de timp este 6. În cazul nostru, vom folosi 60 ca pas de timp, adică ne vom uita la 2 luni de date pentru a prezice prețul din zilele următoare. Mai multe despre acest lucru mai târziu.
  • Caracteristicile sunt numărul de atribute utilizate pentru a reprezenta fiecare pas de timp. Luați în considerare exemplul de predicție a caracterelor de mai sus și presupuneți că folosiți un vector codificat cu un singur foc de dimensiune 100 pentru a reprezenta fiecare caracter. Atunci dimensiunea caracteristicilor aici este 100.

Acum că am lămurit oarecum terminologiile, haideți să convertim datele noastre de stoc într-un format adecvat. Să presupunem, pentru simplitate, că am ales 3 ca pasul nostru de timp (dorim ca rețeaua noastră să se uite înapoi la 3 zile de date pentru a prezice prețul în a 4-a zi), atunci vom forma setul nostru de date astfel:

Eșantioanele de la 0 la 2 ar fi prima noastră intrare, iar Prețul de închidere al eșantionului 3 ar fi valoarea de ieșire corespunzătoare; ambele încadrate de dreptunghiul verde. În mod similar, eșantioanele de la 1 la 3 ar fi a doua noastră intrare, iar prețul de închidere al eșantionului 4 ar fi valoarea de ieșire; reprezentate de dreptunghiul albastru. Și așa mai departe. Așadar, până acum avem o matrice de forma (3, 5), 3 fiind pasul de timp și 5 fiind numărul de caracteristici. Acum gândiți-vă câte astfel de perechi intrare-ieșire sunt posibile în imaginea de mai sus? 4.

De asemenea, amestecați dimensiunea lotului cu aceasta. Să presupunem că alegem dimensiunea lotului de 2. Atunci perechea intrare-ieșire 1 (dreptunghiul verde) și perechea 2 (dreptunghiul albastru) ar constitui lotul unu. Și așa mai departe. Iată fragmentul de cod python pentru a face acest lucru:

‘y_col_index’ este indexul coloanei de ieșire. Acum, să presupunem că după ce ați convertit datele în format de învățare supravegheată, așa cum se arată mai sus, aveți 41 de eșantioane în setul de date de instruire, dar dimensiunea lotului dvs. este de 20, atunci va trebui să tăiați setul de instruire pentru a elimina eșantioanele ciudate rămase pe dinafară. Voi căuta o modalitate mai bună de a evita acest lucru, dar deocamdată iată ce am făcut:

Acum, folosind funcțiile de mai sus, să formăm seturile de date de instruire, validare și testare

x_t, y_t = build_timeseries(x_train, 3)
x_t = trim_dataset(x_t, BATCH_SIZE)
y_t = trim_dataset(y_t, BATCH_SIZE)
x_temp, y_temp = build_timeseries(x_test, 3)
x_val, x_test_t = np.split(trim_dataset(x_temp, BATCH_SIZE),2)
y_val, y_test_t = np.split(trim_dataset(y_temp, BATCH_SIZE),2)

Acum că datele noastre sunt gata, ne putem concentra pe construirea modelului.

Crearea modelului

Vom folosi LSTM pentru această sarcină, care este o variație a rețelei neuronale recurente. Crearea modelului LSTM este la fel de simplă ca aceasta:

Acum că aveți modelul compilat și gata de a fi antrenat, antrenați-l așa cum se arată mai jos. Dacă vă întrebați ce valori să folosiți pentru parametrii cum ar fi epochs, mărimea lotului etc., nu vă faceți griji, vom vedea cum să le aflăm în următorul articol.

Antrenarea acestui model (cu hiperparametrii bine reglate) a dat cea mai bună eroare de 3,27e-4 și cea mai bună eroare de validare de 3,7e-4. Iată cum arată pierderea de antrenament vs pierderea de validare:

Eroare de antrenament vs eroare de validare

Aceasta este modul în care a arătat predicția cu modelul de mai sus:

predicție vs date reale

Am constatat că această configurație pentru LSTM funcționează cel mai bine dintre toate combinațiile pe care le-am încercat (pentru acest set de date), și am încercat mai mult de 100! Așadar, întrebarea este cum ajungi la arhitectura perfectă (sau, în aproape toate cazurile, aproape perfectă) pentru rețeaua ta neuronală? Acest lucru ne conduce la următoarea secțiune importantă, care va fi continuată în următorul articol.

Puteți găsi toate programele complete pe profilul meu Github aici.

NOTA: O rugăminte umilă către cititori – Sunteți cu toții bineveniți să vă conectați cu mine pe LinkedIn sau Twitter, dar dacă aveți o întrebare referitoare la blogurile mele, vă rog să o postați în secțiunea de comentarii a blogului respectiv în loc de mesaje personale, astfel încât, dacă altcineva are aceeași întrebare, să o găsească chiar aici, iar eu să nu fiu nevoit să o explic individual. Cu toate acestea, sunteți în continuare bineveniți să îmi trimiteți personal întrebări care nu au legătură cu blogurile sau întrebări tehnice generale. Mulțumesc 🙂

UPDATE 13/4/19

  1. Am aflat, de când am scris acest articol, că este posibil ca modelul meu folosit pentru acest blog să fi fost supraajustat. Deși nu am confirmat acest lucru, este probabil. Așadar, vă rog să fiți atenți în timp ce implementați acest lucru în proiectele dumneavoastră. Ați putea încerca lucruri cum ar fi epoci mai mici, o rețea mai mică, mai multe scăderi etc.
  2. Am folosit activarea Sigmoid pentru ultimul strat, care poate suferi de limitarea de a nu putea prezice un preț mai mare decât prețul „maxim” din setul de date. Ați putea încerca activarea ‘Linear’ pentru ultimul strat pentru a rezolva acest lucru.
  3. Am corectat o greșeală de scriere în secțiunea „Converting data to time-series”.

Mulțumesc cititorilor pentru că mi-au adus la cunoștință aceste aspecte.

UPDATE 21/1/2020

Lasă un răspuns

Adresa ta de email nu va fi publicată.