Tőzsdei árfolyam előrejelzése LSTM-mel

A gépi tanulás számos érdekes területen talált alkalmazást az elmúlt években. A tőzsde megzabolázása az egyik ilyen terület. Már jó ideje gondolkodtam azon, hogy megpróbálkozom vele; leginkább azért, hogy megszilárdítsam az LSTM-ekkel kapcsolatos munkatudásomat. És végül befejeztem a projektet, és meglehetősen izgatottan osztom meg a tapasztalataimat.

Motiváció és célközönség

A tapasztalataimról egy sor blogon keresztül fogok írni. Ennek a sorozatnak nem az a célja, hogy elmagyarázza az LSTM vagy a gépi tanulás fogalmainak alapjait. Ezért feltételezem, hogy az olvasó már megkezdte a gépi tanulással kapcsolatos utazását, és rendelkezik az olyan alapokkal, mint a Python, a SkLearn, Keras, LSTM stb. ismerete. Ennek oka az, hogy már vannak kiváló cikkek olyan témákban, mint a “Hogyan működnek az LSTM-ek?” olyan emberektől, akik sokkal képzettebbek a mögöttes matematika elmagyarázására. De meg fogom osztani az ilyen cikkek linkjeit, ahol úgy érzem, hogy a háttérismeretek hiányozhatnak. Bár rengeteg olyan cikk van, amely megmondja, hogyan lehet megjósolni a részvényárfolyamokat egy adott adatkészlet alapján, a szerzők többnyire nem fedik fel/magyarázzák, hogyan jutottak el a neurális hálózat adott konfigurációjához, vagy hogyan választották ki a hiperparaméterek adott készletét. Tehát ennek a cikknek az az igazi célja, hogy megosszam az ilyen lépéseket, a hibáimat és néhány olyan lépést, amelyet nagyon hasznosnak találtam. Mint ilyen, ez a cikk nem korlátozódik a részvényárfolyam-előrejelzési problémára.

Itt vannak a dolgok, amelyeket megnézünk :

  1. Adatok olvasása és elemzése. (Pandas)
  2. Az adatok normalizálása. (SkLearn)
  3. Az adatok idősorokká alakítása és felügyelt tanulási probléma.
  4. Modell létrehozása (Keras)
  5. A modell finomhangolása (a következő cikkben)
  6. Tréning, előrejelzés és az eredmény vizualizálása.
  7. Tippek & eszközök, amelyeket nagyon hasznosnak találtam (a sorozat utolsó cikke)

Figyelem, ez az első cikk az LSTM előfeldolgozási lépéseiről és fogalmairól szól. Ha eléggé biztos vagy ezekben a lépésekben, akkor átugorhatod a következő cikket.

Kezdjük!

Az adatok olvasása és elemzése

Ez a bejegyzéshez a GE historikus részvényárfolyam-adatait fogom használni. Az adatokat a kaggle oldalamon itt találod. Nem emlékszem az adatok forrására, mivel már régen letöltöttem. Az adatokat az alábbiak szerint olvashatjuk be a keretbe :

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

Amint látható, körülbelül 14060 elem van, mindegyik a vállalat egy napi tőzsdei jellemzőit képviseli. Lássuk, hogyan néz ki a grafikonon :

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()

Az árak – Nyitás, Zárás, Alacsony, Magas – nem különböznek túlságosan egymástól, kivéve az Alacsony ár alkalmankénti enyhe csökkenését.

Most nézzük meg a mennyiségre vonatkozó ábrát :

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

Huh. Láttál valami érdekeset? Az idővonal 12000. napja körül eléggé megugrott a tranzakciók száma, ami történetesen egybeesik a részvényárfolyam hirtelen esésével. Lehet, hogy visszamehetünk arra a bizonyos dátumra, és előáshatjuk a régi hírcikkeket, hogy kiderítsük, mi okozta ezt.

Most nézzük meg, hogy van-e null/Nan érték, ami miatt aggódnunk kell. Mint kiderült, nincsenek nulla értékeink. Nagyszerű!

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

Az adatok normalizálása

Az adatok nincsenek normalizálva, és az egyes oszlopok tartománya eltérő, különösen a Volume. Az adatok normalizálása segíti az algoritmus konvergálását, azaz a lokális/globális minimum hatékony megtalálását. A Sci-kit Learn-ből származó MinMaxScaler-t fogom használni. De előtte fel kell osztanunk az adathalmazt képzési és tesztelési adathalmazokra. Emellett a DataFrame-et ndarray-vé fogom konvertálni a folyamat során.

Az adatok idősorozattá alakítása és a felügyelt tanulás problémája

Ez elég fontos és kissé trükkös. Itt van szükség a tudás LSTM-re. Röviden ismertetném az itt szükséges kulcsfogalmakat, de erősen ajánlom Andre karpathy blogjának elolvasását itt, amelyet az egyik legjobb forrásnak tartanak az LSTM-ről, és ezt. Vagy megnézheti Andrew Ng videóját is (amely egyébként Andre blogját is megemlíti).

Az LSTM-ek a bemenetet formátumban fogyasztják ; egy 3 dimenziós tömb.

  • A Batch Size azt mondja, hogy hány mintát szeretne a Neural Net látni a súlyok frissítése előtt. Tegyük fel, hogy 100 mintája van (bemeneti adathalmaz), és a súlyokat minden alkalommal frissíteni szeretné, amikor az NN látott egy bemenetet. Ebben az esetben a kötegméret 1 lenne, és a kötegek teljes száma 100 lenne. Ugyanígy, ha azt szeretné, hogy a hálózat az összes minta megtekintése után frissítse a súlyokat, akkor a kötegméret 100, a kötegek száma pedig 1 lenne. Mint kiderült, a nagyon kis kötegméret használata csökkenti a képzés sebességét, másrészt a túl nagy kötegméret (például a teljes adathalmaz) csökkenti a modell általánosítási képességét különböző adatokra, és több memóriát is fogyaszt. Viszont kevesebb lépést igényel a célfüggvény minimumának megtalálása. Tehát különböző értékeket kell kipróbálnia az adatain, és meg kell találnia az édes pontot. Ez elég nagy téma. A következő cikkben megnézzük, hogyan lehet ezeket valamivel okosabban megkeresni.
  • Az Időlépések meghatározzák, hogy hány egységgel visszamenőleg akarod látni a hálózatodat az időben. Például, ha egy karakter-előrejelzési problémán dolgozol, ahol van egy szövegkorpusz, amin edzeni kell, és úgy döntesz, hogy egyszerre 6 karaktert táplálsz a hálózatodba. Akkor az időlépés 6. A mi esetünkben 60-at fogunk használni időlépésként, azaz 2 hónapnyi adatot fogunk megvizsgálni, hogy megjósoljuk a következő napi árat. Erről bővebben később.
  • A jellemzők az egyes időlépések reprezentálásához használt attribútumok száma. Tekintsük a fenti karakter-előrejelzési példát, és tegyük fel, hogy minden egyes karakter reprezentálására egy 100-as méretű, együtemű kódolt vektort használunk. Akkor a jellemző mérete itt 100.

Most, hogy némileg tisztáztuk a terminológiákat, konvertáljuk át a készletadatainkat megfelelő formátumba. Tegyük fel, az egyszerűség kedvéért, hogy 3 időt választottunk az időlépésünknek (azt akarjuk, hogy a hálózatunk 3 nap adataira tekintsen vissza, hogy megjósolja a 4. napi árfolyamot), akkor így alakítjuk ki az adatkészletünket:

A 0-2. minta lenne az első bemenetünk, a 3. minta záró ára pedig a megfelelő kimeneti érték; mindkettőt zöld téglalap zárja körül. Hasonlóképpen az 1-3. minta lenne a második bemenetünk, és a 4. minta záróára lenne a kimeneti érték; kék téglalap által ábrázolva. És így tovább. Mostanáig tehát egy (3, 5) alakú mátrixunk van, ahol 3 az időlépés és 5 a jellemzők száma. Most gondoljuk végig, hány ilyen bemeneti-kimeneti pár lehetséges a fenti képen? 4.

Ezzel keverjük össze a tételméretet is. Tegyük fel, hogy a tételméretet 2-re választjuk. Akkor az 1. bemeneti-kimeneti pár (zöld téglalap) és a 2. pár (kék téglalap) alkotná az egyes tételt. És így tovább. Íme az ehhez szükséges python kódrészlet:

‘y_col_index’ a kimeneti oszlop indexe. Most tegyük fel, hogy az adatok felügyelt tanulási formátumba való konvertálása után, a fentiek szerint, 41 minta van a képzési adathalmazban, de a kötegméret 20, akkor meg kell vágnia a képzési halmazt, hogy eltávolítsa a kimaradt páratlan mintákat. Keresni fogok egy jobb módszert ennek megkerülésére, de egyelőre ezt csináltam:

Most a fenti függvények segítségével alakítsuk ki a train, validációs és teszt adathalmazainkat

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)

Most, hogy az adataink készen állnak, koncentrálhatunk a modell építésére.

Modell létrehozása

Ezzel a feladattal LSTM-et fogunk használni, ami a Recurrent Neural Network egy változata. Az LSTM modell létrehozása ilyen egyszerű:

Most, hogy összeállítottuk a modellt és készen áll a betanításra, képezzük ki az alábbiak szerint. Ha azon tűnődik, hogy milyen értékeket használjon az olyan paraméterekhez, mint az epochák, a kötegméret stb., ne aggódjon, a következő cikkben meglátjuk, hogyan kell ezeket kitalálni.

A modell edzése (finomhangolt hiperparaméterekkel) 3,27e-4-es legjobb hibát és 3,7e-4-es legjobb validálási hibát adott. Íme, így nézett ki a Training loss vs Validation loss:

Training error vs Validation error

Így nézett ki az előrejelzés a fenti modellel:

előrejelzés vs valós adatok

Megállapítottam, hogy ez az LSTM konfiguráció működik a legjobban az összes kombináció közül, amit próbáltam (erre az adathalmazra), és több mint 100-at próbáltam! A kérdés tehát az, hogyan jutsz el a tökéletes (vagy szinte minden esetben közel tökéletes) architektúrához a neurális hálózatod számára? Ez elvezet minket a következő és fontos részhez, amelyet a következő cikkben folytatunk.

A teljes programokat a Github-profilomon itt találod.

MEGJEGYZÉS: Egy szerény kérés az olvasókhoz – Mindannyian szívesen látunk benneteket, hogy kapcsolatba lépjetek velem a LinkedIn-en vagy a Twitteren, de ha kérdésetek van a blogjaimmal kapcsolatban, kérlek, hogy személyes üzenetek helyett az adott blog megjegyzés rovatában tegyétek meg, hogy ha valaki másnak ugyanilyen kérdése van, akkor itt találja meg, és ne kelljen külön-külön elmagyaráznom. Azonban továbbra is szívesen látjuk, ha a blogokhoz nem kapcsolódó kérdéseket vagy általános technikai kérdéseket küld nekem személyesen. Köszönöm 🙂

UPDATE 13/4/19

  1. A cikk megírása óta tudomásomra jutott, hogy a bloghoz használt modellem esetleg túlillesztett. Bár ezt nem erősítettem meg, de valószínű. Ezért kérem, legyen óvatos, miközben ezt a projektjeiben megvalósítja. Kipróbálhat olyan dolgokat, mint a kisebb epochák, kisebb hálózat, több kiesés stb.
  2. Az utolsó réteghez Sigmoid aktiválást használtam, ami azzal a korlátozással járhat, hogy nem képes megjósolni a “max” árnál nagyobb árat az adathalmazban. Megpróbálhatja a “Lineáris” aktiválást az utolsó réteghez, hogy megoldja ezt.
  3. Kijavítottam egy elírást az “adatok idősorozattá alakítása” szakaszban.

Köszönöm az olvasóknak, hogy felhívták a figyelmemet ezekre.

UPDATE 21/1/2020

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.