Bevezetés
A szekvencia-előrejelzési problémák már régóta léteznek. Az adattudományban az egyik legnehezebben megoldható problémának számítanak. A problémák széles skáláját foglalják magukban; az eladások előrejelzésétől a tőzsdei adatokban található minták megtalálásáig, a filmek cselekményének megértésétől a beszédmód felismeréséig, a nyelvi fordításoktól a következő szó megjósolásáig az iPhone billentyűzetén.
Az adattudományban a közelmúltban bekövetkezett áttörésekkel az derült ki, hogy szinte az összes ilyen szekvencia-előrejelzési probléma esetében a hosszú rövid távú memóriájú hálózatok, más néven LSTM-ek a leghatékonyabb megoldásnak bizonyultak.
A LSTM-ek több szempontból is előnyben vannak a hagyományos feed-forward neurális hálózatokkal és az RNN-ekkel szemben. Ennek oka az a tulajdonságuk, hogy hosszú időn keresztül szelektíven emlékeznek a mintákra. Ennek a cikknek az a célja, hogy elmagyarázza az LSTM-et, és lehetővé tegye a valós problémákban való alkalmazását. Nézzük meg!
Megjegyzés: A cikk átfutásához alapvető ismeretekkel kell rendelkeznie a neurális hálózatokról és a Keras (egy mélytanulási könyvtár) működéséről. A fogalmak megértéséhez lásd az említett cikkeket:
- Understanding Neural Network From Scratch
- Fundamentals of Deep Learning – Introduction to Recurrent Neural Networks
- Tutorial: Optimizing Neural Networks using Keras (with Image recognition case study)
Table of Contents
- Flashback: A Recurrent Neural Networks (RNN)
- Limitations of RNNs
- Improvement over RNN : Hosszú rövid távú memória (LSTM)
- Az LSTM felépítése
- Felejtsd el a kaput
- Bemeneti kapu
- Kimeneti kapu
- Szöveggenerálás LSTM-ekkel.
Flashback: Egy pillantás a rekurrens neurális hálózatokra (RNN)
Vegyünk egy példát szekvenciális adatokra, ami lehet a tőzsdei adatok egy adott részvényre vonatkozóan. Egy egyszerű gépi tanulási modell vagy mesterséges neurális hálózat megtanulhatja megjósolni a részvényárfolyamokat számos jellemző alapján: a részvény mennyisége, nyitóértéke stb. Miközben a részvény árfolyama ezektől a jellemzőktől függ, nagymértékben függ az előző napok részvényértékeitől is. Valójában egy kereskedő számára ezek az előző napi értékek (vagy a trend) az egyik fő döntő tényező a jóslatok szempontjából.
A hagyományos feed-forward neurális hálózatokban minden tesztesetet függetlennek tekintünk. Vagyis amikor egy adott napra illesztjük a modellt, nem vesszük figyelembe az előző napok részvényárfolyamait.
Ezt az időfüggőséget a rekurrens neurális hálózatokon keresztül érjük el. Egy tipikus RNN így néz ki:
Ez első látásra ijesztő lehet, de ha kibontjuk, sokkal egyszerűbbnek tűnik:
Most már könnyebb szemléltetni, hogyan veszik figyelembe ezek a hálózatok a részvényárfolyamok trendjét, mielőtt megjósolják a mai napra vonatkozó részvényárfolyamokat. Itt minden előrejelzés a t időpontban (h_t) az összes korábbi előrejelzéstől és az azokból tanult információtól függ.
Az RNN-ek nagymértékben, de nem teljesen oldják meg a szekvenciakezelési célunkat. Azt szeretnénk, ha a számítógépünk elég jó lenne ahhoz, hogy Shakespeare szonetteket írjon. Nos, az RNN-ek nagyszerűek, ha rövid összefüggésekről van szó, de ahhoz, hogy képesek legyünk egy történetet felépíteni és megjegyezni, szükségünk van arra, hogy modelljeink képesek legyenek megérteni és megjegyezni a szekvenciák mögötti összefüggéseket, akárcsak az emberi agy. Ez egy egyszerű RNN-nel nem lehetséges.
Miért? Nézzük meg.
A RNN-ek korlátai
A rekurrens neurális hálózatok akkor működnek jól, ha rövid távú függőségekkel foglalkozunk. Vagyis az olyan problémákra alkalmazva, mint:
Az RNN-ek igen hatékonynak bizonyulnak. Ennek az az oka, hogy ennek a problémának semmi köze az állítás kontextusához. Az RNN-nek nem kell emlékeznie arra, hogy mit mondtak előtte, vagy mi volt a jelentése, csak annyit kell tudnia, hogy a legtöbb esetben az ég kék. Így a jóslat a következő lenne:
A vanília RNN-ek azonban nem képesek megérteni a bemenet mögötti kontextust. Valamit, ami már régen elhangzott, nem lehet felidézni, amikor a jelenben jóslatokat készítenek. Értsük ezt példaként:
Itt megérthetjük, hogy mivel a szerző 20 éve Spanyolországban dolgozik, nagy valószínűséggel jó spanyol nyelvtudással rendelkezhet. A megfelelő előrejelzéshez azonban az RNN-nek emlékeznie kell erre a kontextusra. A releváns információt hatalmas mennyiségű irreleváns adat választja el attól a ponttól, ahol szükség van rá. Ez az a pont, ahol a rekurrens neurális hálózat kudarcot vall!
Az ok, ami emögött áll, az eltűnő gradiens problémája. Ahhoz, hogy ezt megértsük, szükségünk lesz némi ismeretre arról, hogyan tanul egy feed-forward neurális hálózat. Tudjuk, hogy egy hagyományos feed-forward neurális hálózat esetében az egy adott rétegre alkalmazott súlyfrissítés a tanulási ráta, az előző rétegből származó hibaterminus és az adott réteg bemenetének többszöröse. Így egy adott réteg hibatétele valahol az összes előző réteg hibájának a szorzata. Amikor olyan aktiválási függvényekkel foglalkozunk, mint a szigmoid függvény, annak (a hibafüggvényben előforduló) deriváltjainak kis értékei többszörös szorzatot kapnak, ahogy a kezdő rétegek felé haladunk. Ennek következtében a gradiens szinte eltűnik, ahogy a kezdő rétegek felé haladunk, és nehézzé válik ezeknek a rétegeknek a képzése.
Hasonló eset figyelhető meg a rekurrens neurális hálózatoknál. Az RNN csak kis időtartamokra jegyez meg dolgokat, azaz ha kis idő elteltével szükségünk van az információra, akkor az reprodukálható lehet, de ha egyszer sok szót táplálunk be, akkor ez az információ valahol elveszik. Ez a probléma megoldható az RNN-ek egy kissé módosított változatának – a hosszú rövidtávú memóriahálózatoknak – az alkalmazásával.
Az RNN-ekkel szembeni javulás: LSTM (Long Short-Term Memory) Networks
Amikor a naptárunkat rendezzük a napra, rangsoroljuk a találkozóinkat, igaz? Ha esetleg helyet kell szorítanunk valami fontosnak, akkor tudjuk, hogy melyik találkozót lehet lemondani egy esetleges találkozó érdekében.
Kiderült, hogy egy RNN nem ezt teszi. Ahhoz, hogy új információt adjon hozzá, egy függvény alkalmazásával teljesen átalakítja a meglévő információt. Emiatt a teljes információ összességében módosul, azaz nincs tekintettel a “fontos” és a “kevésbé fontos” információra.
Az LSTM-ek ezzel szemben szorzásokkal és összeadásokkal kis mértékben módosítják az információt. Az LSTM-eknél az információ a sejtállapotok néven ismert mechanizmuson keresztül áramlik. Ily módon az LSTM-ek képesek szelektíven emlékezni vagy elfelejteni dolgokat. Az információ egy adott sejtállapotban három különböző függőséggel rendelkezik.
Ezt egy példával szemléltetjük. Vegyük a példát egy adott részvény árfolyamának előrejelzésére. A mai részvényárfolyam függ:
- A trendtől, amelyet a részvény az előző napokban követett, esetleg egy csökkenő vagy egy emelkedő trendet.
- A részvény előző napi árfolyamától, mert sok kereskedő a vásárlás előtt összehasonlítja a részvény előző napi árfolyamát.
- Azoktól a tényezőktől, amelyek befolyásolhatják a részvény mai árfolyamát. Ilyen lehet egy új vállalati politika, amelyet széles körben kritizálnak, vagy a vállalat nyereségének csökkenése, esetleg egy váratlan változás a vállalat felső vezetésében.
Ezek a függőségek bármilyen problémára általánosíthatók a következőképpen:
- Az előző cellás állapot (azaz az előző időlépés után a memóriában lévő információ)
- Az előző rejtett állapot (azaz. ez megegyezik az előző cella kimenetével)
- Az aktuális időlépéskori bemenet (azaz az új információ, ami abban a pillanatban kerül be)
Az LSTM másik fontos jellemzője a futószalaggal való analógia!
Így van!
Az iparban ezeket használják a termékek mozgatására a különböző folyamatokhoz. Az LSTM-ek ezt a mechanizmust használják az információ mozgatására.
Elképzelhető, hogy az információt hozzáadjuk, módosítjuk vagy eltávolítjuk, miközben az a különböző rétegeken keresztül áramlik, ahogyan egy terméket is formázhatunk, festhetünk vagy csomagolhatunk, miközben az a futószalagon van.
A következő ábra az LSTM-ek és a futószalagok szoros kapcsolatát magyarázza.
Forrás
Bár ez az ábra közel sem felel meg egy LSTM tényleges felépítésének, a célunkat egyelőre megoldja.
Az LSTM-ek ezen tulajdonsága miatt, miszerint nem a teljes információt manipulálják, hanem kissé módosítják, képesek szelektíven felejteni és emlékezni dolgokra. Hogy hogyan teszik ezt, azt a következő részben fogjuk megismerni?
Az LSTM-ek architektúrája
Az LSTM működését egy hírcsatorna egy gyilkossági történetről tudósító csapatának működésének megértésével szemléltethetjük. Nos, egy híranyag tényekből, bizonyítékokból és sok ember vallomásaiból épül fel. Amikor egy új esemény történik, a három lépés valamelyikét megteszi.
Tegyük fel, hogy azt feltételeztük, hogy a gyilkosságot az áldozat “megmérgezésével” követték el, de a most érkezett boncolási jegyzőkönyv szerint a halál oka “a fejre mért ütés” volt. A híradós csapat tagjaként mit teszel? Azonnal elfelejted a korábbi halál okát és minden történetet, ami e tény köré szövődött.
Mi van, ha egy teljesen új gyanúsított kerül a képbe. Egy olyan személy, akinek haragja volt az áldozattal, és ő lehet a gyilkos? Ezt az információt beviszed a hírfolyamodba, ugye?
Most ezeket a töredezett információkat nem lehet tálalni a mainstream médiában. Tehát egy bizonyos időintervallum után össze kell foglalnod ezeket az információkat, és ki kell adnod a releváns dolgokat a közönségednek. Talán a következő formában: “XYZ kiderült, hogy ő az első számú gyanúsított.”.
Most térjünk rá az LSTM-hálózat felépítésének részleteire:
Forrás
Most, ez közel sem hasonlít a korábban látott egyszerűsített változathoz, de hadd menjek végig rajta. Egy tipikus LSTM hálózat különböző memóriablokkokból, úgynevezett cellákból
(a képen látható téglalapok) áll. Két állapot van, amely átkerül a következő cellába; a cella állapota és a rejtett állapot. A memóriablokkok felelősek az emlékezésért, és a memóriával való manipuláció három fő mechanizmuson, úgynevezett kapukon keresztül történik. Ezek mindegyikét az alábbiakban tárgyaljuk.
4.1 Forget Gate
Elvesszük egy szövegjóslási probléma példáját. Tegyük fel, hogy egy LSTM-be betápláljuk, a következő mondatot:
Amint a “személy” utáni első pont után találkozunk, a forget gate felismeri, hogy a következő mondatban kontextusváltás következhet. Ennek következtében a mondat alanya elfelejtődik, és az alany helye megüresedik. És amikor “Dan”-ról kezdünk beszélni, az alany ezen helyét “Dan” kapja meg. Az alany elfelejtésének ezt a folyamatát a felejtőkapu idézi elő.
A felejtőkapu felelős az információ eltávolításáért a sejt állapotából. Az LSTM számára a dolgok megértéséhez már nem szükséges vagy kevésbé fontos információkat egy szűrő szorzatán keresztül távolítja el. Erre az LSTM hálózat teljesítményének optimalizálásához van szükség.
Ez a kapu két bemenetet fogad be; h_t-1 és x_t.
h_t-1 az előző cellából származó rejtett állapot vagy az előző cella kimenete, x_t pedig az adott időlépéskori bemenet. Az adott bemeneteket megszorozzuk a súlymátrixokkal, és hozzáadunk egy torzítást. Ezt követően a szigmoid függvényt alkalmazzuk erre az értékre. A szigmoid függvény egy vektort ad ki, amelynek értékei 0-tól 1-ig terjednek, és a cella állapotának minden egyes számának megfelelnek. Alapvetően a szigmoid függvény felelős annak eldöntéséért, hogy mely értékeket kell megtartani és melyeket kell elvetni. Ha egy “0” értéket ad ki egy adott értékre a sejtállapotban, az azt jelenti, hogy a felejtéskapu azt akarja, hogy a sejtállapot teljesen elfelejtse az adott információt. Hasonlóképpen, egy ‘1’ azt jelenti, hogy a felejtéskapu az adott információt teljes egészében megjegyzi. Ezt a szigmoid függvényből származó vektor kimenetet a sejtállapotra szorozzuk.
4.2 Bemeneti kapu
Oké, vegyünk egy másik példát, ahol az LSTM egy mondatot elemez:
Most itt az a fontos információ, hogy “Bob” tud úszni, és hogy négy évig szolgált a haditengerészetnél. Ez hozzáadható a sejtállapothoz, az azonban, hogy mindezt telefonon mondta el, kevésbé fontos tény, és figyelmen kívül hagyható. Ez az új információ hozzáadásának folyamata a bemeneti kapun keresztül történhet.
Itt a felépítése:
A bemeneti kapu felelős az információ hozzáadásáért a sejt állapotához. Ez az információ hozzáadása alapvetően háromlépéses folyamat, ahogy a fenti ábrán látható.
- Egy szigmoid függvény bevonásával szabályozza, hogy milyen értékeket kell hozzáadni a sejt állapotához. Ez alapvetően nagyon hasonló a forget gate-hez, és a h_t-1 és x_t összes információjának szűrőjeként működik.
- Egy vektor létrehozása, amely tartalmazza az összes lehetséges értéket, amelyet hozzá lehet adni (a h_t-1 és x_t alapján érzékelve) a sejt állapotához. Ehhez a tanh függvényt használjuk, amely -1 és +1 közötti értékeket ad ki.
- A létrehozott vektorral (a tanh függvénnyel) a szabályozó szűrő (a szigmoid kapu) értékét megszorozzuk, majd ezt a hasznos információt összeadási művelettel hozzáadjuk a sejt állapotához.
Amikor ezzel a háromlépéses folyamattal végeztünk, biztosítjuk, hogy csak az az információ kerüljön hozzá a sejt állapotához, ami fontos és nem redundáns.
4.3. Kimeneti kapu
Nem minden információ, amely végigfut a cella állapotán, alkalmas arra, hogy egy adott időpontban kimenjen. Ezt egy példával szemléltetjük:
Ebben a mondatban az üres helyre többféle lehetőség is lehet. De tudjuk, hogy az aktuális bemeneti ‘bátor’, egy melléknév, amelyet egy főnév leírására használunk. Így bármilyen szó következik is, nagy valószínűséggel főnév lesz. És így Bob lehet a megfelelő kimenet.
Ezt a feladatot, hogy az aktuális sejtállapotból kiválasszuk a hasznos információt, és azt kimenetként megjelenítsük, a kimeneti kapun keresztül végezzük. Íme a felépítése:
A kimeneti kapu működése ismét három lépésre bontható:
- Egy vektor létrehozása a tanh függvénynek a cella állapotára való alkalmazása után, ezáltal az értékek skálázása a -1 és +1 közötti tartományba.
- Egy szűrő készítése a h_t-1 és x_t értékek felhasználásával, úgy, hogy az képes legyen szabályozni a fent létrehozott vektorból kimenő értékeket. Ez a szűrő ismét egy szigmoid függvényt alkalmaz.
- Ez a szabályozó szűrő értékét megszorozzuk az 1. lépésben létrehozott vektorral, és kimenetként, valamint a következő cella rejtett állapotába küldjük ki.
A fenti példában a szűrő gondoskodik arról, hogy a ‘Bob’-on kívül minden más értéket kicsinyítsen. A szűrőt tehát a bemeneti és a rejtett állapotértékekre kell felépíteni, és a cellaállapot-vektorra kell alkalmazni.
Szöveggenerálás LSTM-ekkel
Elég volt az LSTM-ek elméleti fogalmaiból és működéséből. Most egy olyan modellt próbálnánk építeni, amely a Macbeth eredeti szövege után néhány n karakterszámot képes megjósolni. A klasszikus szövegek többsége már nem áll szerzői jogi védelem alatt, és itt található. A .txt fájl frissített változata itt található.
A Keras könyvtárat fogjuk használni, amely egy magas szintű API a neurális hálózatokhoz, és a TensorFlow vagy a Theano tetején működik. Győződjünk meg tehát arról, hogy mielőtt belevetnénk magunkat ebbe a kódba, a Keras telepítve van és működik.
Oké, akkor generáljunk egy kis szöveget!
-
Függőségek importálása
# 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
Az összes szükséges függőséget importáljuk, és ez nagyjából magától értetődő.
-
Szövegfájl betöltése és a karakterek egész számokhoz való hozzárendelésének létrehozása
# 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})
A szövegfájlt megnyitjuk, és minden karaktert kisbetűvé alakítunk. A következő lépések megkönnyítése érdekében minden karaktert egy megfelelő számhoz képeznénk le. Ez azért történik, hogy az LSTM számítási részét megkönnyítsük.
-
Adatkészlet előkészítése
# 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)
Az adatokat olyan formátumban készítjük elő, hogy ha azt szeretnénk, hogy az LSTM a ‘HELLO’-ban lévő ‘O’-t jósolja meg, akkor bemenetként és várható kimenetként is betáplálnánk. Hasonlóképpen itt is rögzítjük a kívánt sorozat hosszát (a példában 50-re állítva), majd elmentjük az első 49 karakter kódolását X-ben és a várható kimenetet, azaz az 50. karaktert Y-ban.
-
X átformálása
# 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)
Az LSTM hálózat a bemenetet olyan formában várja el, ahol a minták a rendelkezésünkre álló adatpontok száma, az időlépések az időfüggő lépések száma, amelyek egyetlen adatpontban vannak, a jellemzők pedig az Y-ban a megfelelő valós értékhez tartozó változók számára utalnak. Ezután az X_módosítottban lévő értékeket 0 és 1 között skálázzuk, és egy hot kódoljuk a valódi értékeinket az Y_módosítottban.
-
Az LSTM modell definiálása
# 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')
A szekvenciális modellt használjuk, amely egy lineáris réteghalmaz. Az első réteg egy LSTM réteg 300 memóriaegységgel, és szekvenciákat ad vissza. Ez azért történik, hogy a következő LSTM réteg szekvenciákat kapjon, és ne csak véletlenszerűen elszórt adatokat. Minden LSTM-réteg után egy kieső réteget alkalmazunk, hogy elkerüljük a modell túlillesztését. Végül az utolsó réteg egy teljesen összekapcsolt réteg, “softmax” aktiválással és az egyedi karakterek számával megegyező számú neuronnal, mivel egy forró kódolt eredményt kell kiadnunk.
-
A modell illesztése és a karakterek generálása
# 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
A modell illesztése 100 epochán keresztül történik, 30 kötegmérettel. Ezután rögzítünk egy véletlenszerű magot (a könnyű reprodukálhatóság érdekében), és elkezdjük a karakterek generálását. A modellből kapott előrejelzés kiadja a megjósolt karakter kódolását, ezt dekódoljuk vissza a karakterértékre, és csatoljuk a mintához.
Így nézne ki a hálózat kimenete
Egy idő után, elegendő gyakorló epocha után, idővel egyre jobb eredményeket fog adni. Így használnánk az LSTM-et egy szekvencia-előrejelzési feladat megoldására.
Végjegyzetek
Az LSTM-ek nagyon ígéretes megoldást jelentenek a szekvenciákkal és idősorokkal kapcsolatos problémákra. Az egyetlen hátrányuk azonban, amit tapasztaltam velük kapcsolatban, a betanításuk nehézsége. Sok időt és rendszererőforrást igényel még egy egyszerű modell képzése is. De ez csak egy hardveres korlát! Remélem, sikerült alapvető ismereteket nyújtanom ezekről a hálózatokról. Ha bármilyen probléma vagy kérdés merül fel a bloggal kapcsolatban, kérjük, nyugodtan kommentáljon alább.