Förutsägelse av aktiekurser med LSTM

Maskininlärning har under dessa år använts inom många intressanta områden. Att tämja aktiemarknaden är ett av dem. Jag hade funderat på att ge det ett försök under en längre tid nu; mest för att befästa mina kunskaper om LSTMs. Och äntligen har jag avslutat projektet och är ganska exalterad över att dela med mig av mina erfarenheter.

Motivation och målgrupp

Jag kommer att skriva om mina erfarenheter i en serie bloggar. Syftet med denna serie är inte att förklara grunderna i LSTM eller begrepp för maskininlärning. Därför kommer jag att anta att läsaren har börjat sin resa med maskininlärning och har grunderna som Python, förtrogenhet med SkLearn, Keras, LSTM osv. Anledningen är att det redan finns utmärkta artiklar om ämnen som ”How LSTMs work?” av personer som är mycket mer kvalificerade att förklara matematiken bakom. Men jag kommer att dela med mig av länkar till sådana artiklar, där jag känner att bakgrundskunskap kan saknas. Även om det finns många artiklar där ute som talar om hur man kan förutsäga aktiekurser utifrån ett dataset, avslöjar/förklarar författarna oftast inte hur de kom fram till just den konfigurationen för ett neuralt nätverk eller hur de valde just den uppsättningen av hyperparametrar. Så det verkliga syftet med den här artikeln är att dela med sig av sådana steg, mina misstag och några steg som jag fann mycket hjälpsamma. Som sådan är den här artikeln inte begränsad till problemet med förutsägelse av aktiekurser.

Här är de saker vi kommer att titta på :

  1. Läsning och analys av data. (Pandas)
  2. Normalisera data. (SkLearn)
  3. Konvertera data till tidsserier och problem med övervakad inlärning.
  4. Skapa modell (Keras)
  5. Finjustera modellen (i nästa artikel)
  6. Träna, förutsäga och visualisera resultatet.
  7. Tips &verktyg som jag fann mycket användbara (sista artikeln i serien)

Bemärk att den här första artikeln talar om förbehandlingssteg och terminologier för LSTM. Om du är ganska säker på dessa steg kan du hoppa över till nästa artikel.

Vi börjar!

Läsning och analys av data

Jag kommer att använda historiska aktiekursdata för GE för det här inlägget. Du kan hitta uppgifterna på min kaggle-webbplats här. Jag minns inte källan till data eftersom jag hade laddat ner den för länge sedan. Vi kan läsa in data i ram som visas nedan :

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

Som ni kan se finns det cirka 14060 poster, som var och en representerar en dags börsattribut för företaget. Låt oss se hur det ser ut på en plott :

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

Det verkar som om priserna – Open, Close, Low, High – inte skiljer sig så mycket från varandra, med undantag för tillfälliga små nedgångar i Low price.

Nu ska vi kolla in handlingen för volym :

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

Huh. Såg du något intressant? Det finns en rejäl ökning av antalet transaktioner runt den 12000:e dagen på tidslinjen, vilket råkar sammanfalla med den plötsliga nedgången i aktiekursen. Vi kan kanske gå tillbaka till just det datumet och gräva fram gamla nyhetsartiklar för att ta reda på vad som orsakade det.

Nu ska vi se om vi har några noll/Nan-värden att oroa oss för. Det visar sig att vi inte har några nollvärden. Bra!

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

Normalisering av data

Data är inte normaliserade och intervallet för varje kolumn varierar, särskilt Volume. Normalisering av data hjälper algoritmen att konvergera, dvs. att hitta lokala/globala miniminivåer på ett effektivt sätt. Jag kommer att använda MinMaxScaler från Sci-kit Learn. Men innan dess måste vi dela upp datasetet i tränings- och testdataset. Jag kommer också att konvertera DataFrame till ndarray i processen.

Konvertering av data till tidsserier och problem med övervakad inlärning

Detta är ganska viktigt och något knepigt. Det är här som kunskapen LSTM behövs. Jag skulle ge en kort beskrivning av nyckelbegreppen som behövs här, men jag rekommenderar starkt att du läser Andre karpathys blogg här, som anses vara en av de bästa resurserna om LSTM där ute och detta. Du kan också titta på Andrew Ng’s video (som förresten nämner Andre’s blogg också).

LSTMs konsumerar indata i formatet ; en 3-dimensionell array.

  • Batch Size säger hur många prover av indata som du vill att ditt neurala nät ska se innan du uppdaterar vikterna. Låt oss säga att du har 100 prover (inmatningsdataset) och att du vill uppdatera vikterna varje gång din NN har sett en inmatning. I det fallet skulle batchstorleken vara 1 och det totala antalet batcher skulle vara 100. Om du vill att ditt nätverk ska uppdatera vikterna efter att det har sett alla prover, skulle batchstorleken vara 100 och antalet batcher skulle vara 1. Det visar sig att om man använder en mycket liten satsstorlek minskar träningshastigheten och om man å andra sidan använder en för stor satsstorlek (t.ex. hela datasetet) minskar modellens förmåga att generalisera till olika data och det förbrukar också mer minne. Men det tar färre steg att hitta minima för målfunktionen. Du måste alltså prova olika värden på dina data och hitta det bästa värdet. Det är ett ganska stort ämne. Vi kommer att se hur man söker dessa på ett något smartare sätt i nästa artikel.
  • Tidssteg definierar hur många enheter bakåt i tiden du vill att ditt nätverk ska se. Till exempel om du arbetar med ett problem med teckenförutsägelse där du har en textkorpus att träna på och du bestämmer dig för att mata ditt nätverk med 6 tecken i taget. Då är ditt tidssteg 6. I vårt fall kommer vi att använda 60 som tidssteg, dvs. vi kommer att titta på två månaders data för att förutsäga nästa dags pris. Mer om detta senare.
  • Funktioner är antalet attribut som används för att representera varje tidssteg. Tänk på exemplet med teckenförutsägelse ovan, och anta att du använder en en enstaka kodad vektor av storlek 100 för att representera varje tecken. Då är funktionsstorleken här 100.

När vi nu har fått bort en viss uppklarad terminologi, låt oss omvandla våra lagerdata till ett lämpligt format. Låt oss för enkelhetens skull anta att vi valde 3 som vårt tidssteg (vi vill att vårt nätverk ska titta tillbaka på 3 dagars data för att förutsäga priset på den 4:e dagen) så skulle vi forma vårt dataset så här:

Proverna 0 till 2 skulle vara vår första ingångsinformation, och priset för slutpriset i prov 3 skulle vara motsvarande utgångsvärde, båda omgärdade av en grön rektangel. På samma sätt skulle proverna 1 till 3 vara vår andra indata och slutpriset för prov 4 skulle vara det motsvarande utgångsvärdet, vilket representeras av en blå rektangel. Och så vidare. Vi har nu en matris med formen (3, 5), där 3 är tidssteget och 5 är antalet funktioner. Tänk nu på hur många sådana input-output-par som är möjliga i bilden ovan? 4.

Mixa också satsstorleken med detta. Låt oss anta att vi väljer en batchstorlek på 2. Då skulle input-output-par 1 (grön rektangel) och par 2 (blå rektangel) utgöra batch ett. Och så vidare. Här är python-kodutdraget för att göra detta:

’y_col_index’ är indexet för din utdatakolumn. Anta att du efter att ha konverterat data till formatet för övervakad inlärning, som visas ovan, har 41 prover i ditt träningsdataset men att din batchstorlek är 20. Då måste du trimma din träningsuppsättning för att ta bort de udda proverna som lämnats utanför. Jag kommer att leta efter ett bättre sätt att komma runt detta, men för tillfället är detta vad jag har gjort:

Med hjälp av ovanstående funktioner kan vi skapa våra tränings-, validerings- och testdatamängder

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)

Nu när våra data är färdiga kan vi koncentrera oss på att bygga modellen.

Skapa modell

Vi kommer att använda LSTM för den här uppgiften, vilket är en variant av Recurrent Neural Network. Att skapa en LSTM-modell är så här enkelt:

Nu när du har kompilerat din modell och är redo att tränas, tränar du den på följande sätt. Om du undrar vilka värden du ska använda för parametrar som epoker, batchstorlek etc., oroa dig inte, vi kommer att se hur du ska räkna ut dem i nästa artikel.

Träning av denna modell (med finjusterade hyperparametrar) gav det bästa felet på 3,27e-4 och det bästa valideringsfelet på 3,7e-4. Här är hur träningsförlusten jämfört med valideringsförlusten såg ut:

Träningsfel jämfört med valideringsfel

Här är hur förutsägelsen såg ut med ovanstående modell:

förutsägelse vs riktiga data

Jag har kommit fram till att den här konfigurationen för LSTM fungerar bäst av alla kombinationer jag har provat (för den här datamängden), och jag har provat mer än 100! Så frågan är hur man hittar den perfekta (eller i nästan alla fall nära den perfekta) arkitekturen för sitt neurala nätverk. Detta leder oss till vårt nästa och viktiga avsnitt, som fortsätter i nästa artikel.

Du kan hitta alla fullständiga program på min Github-profil här.

NOTAT: En ödmjuk förfrågan till läsarna – Ni är alla välkomna att ta kontakt med mig på LinkedIn eller Twitter, men om ni har en fråga om mina bloggar, vänligen lägg in den i kommentarsfältet på respektive blogg istället för i personliga meddelanden, så att om någon annan har samma fråga så kan de hitta den här och jag behöver inte förklara den individuellt. Du är dock fortfarande välkommen att skicka frågor som inte har med bloggar att göra eller allmänna tekniska frågor till mig personligen. Tack 🙂

UPPDATERING 13/4/19

  1. Det har kommit till min kännedom, sedan jag skrev den här artikeln, att min modell som användes för den här bloggen kan ha varit överanpassad. Även om jag inte har bekräftat det är det troligt. Så var försiktig när du implementerar detta i dina projekt. Du kan prova saker som mindre epoker, mindre nätverk, mer dropout etc.
  2. Jag har använt mig av Sigmoid-aktivering för sista lagret som kan drabbas av begränsningen att inte kunna förutsäga ett pris som är större än ”max”-priset i datasetet. Du kan försöka med ”linjär” aktivering för sista lagret för att lösa detta.
  3. Rättade ett skrivfel i avsnittet ”konvertera data till tidsserier”.

Tack till läsarna för att de uppmärksammade mig på detta.

UPPDATERING 21/1/2020

Lämna ett svar

Din e-postadress kommer inte publiceras.