Egy alap RNN

A hangfeldolgozás és az ideghálózatok epikus kalandjában Júliával a mai téma az RNN-ek! Végigmegyek, hogyan építettem az első alapvető RNN-t Júliában a Flux.jl segítségével. Mindez a Trebekian-projekt megvalósítására irányul.

aktiválási funkció

Ahogy előző bejegyzésemben leírtam, a projekt, amin dolgozom, Trebekian néven fut, ahol partnerem CLI trivia alkalmazását szeretném kibővíteni azzal, hogy Alex Trebek hangja felolvassa a kérdéseket. Így született meg a Trebekian.jl.

Ma megtanultam, hogyan kell használni a Flux-ot (az összes Julia neurális hálózati csomagot) egy RNN képzésére, amelynek nagyon egyszerű feladata van: adja meg a megadott tömb összes elemének összegét.

RNN: Félreértés

Mi az RNN? Ez egy „visszatérő ideghálót” jelent - alapvetően az RNN egy teljesen összekapcsolt vagy sűrű egység, amelynek állapota van. A bemenetek sorozatának betáplálásakor lineáris műveletet hajt végre (), de ezután a kimenetet bemenetként betáplálja a következő bemenetbe. Tehát az időbeli lépés kimenete a bemenetek, a súlyok és az előfeszítések, valamint az időbeli kimenet függvénye .

A klasszikus RNN egységet általában így ábrázolják:

Ahol a funkció valóban bármi lehet! A klasszikus „RNN” esetében az emberek általában lineáris egységet jelentenek, mint valamilyen aktivációs funkcióval.

Természetesen egy teljes kutatási területet szentelnek az RNN-ek, valamint elméletük és alkalmazásuk tanulmányozásának, de e projekt céljaira (egyelőre) nem megyünk túl messzire a nyúl lyukán. Elég azt mondani, hogy az RNN alapvető „visszatérő” struktúrája sokféle formát ölthet. Ha több olvasmányra vágyik, nézze meg az LSTM-eket (amelyeket a számítógépes látásmódban szekvenciák közötti sorozatra használnak) és a GRU-kat (amelyeket erősen használnak a hangfeldolgozásban). Sok más van, és arra biztatlak, hogy végezzen saját kutatásokat, hogy többet tudjon meg.

Miért RNN

Trebekian esetében RNN-t akarunk használni, mert a cél az adatsorozat (azaz egy mondat) felvétele és egy másik sorrenddé (azaz hangsávvá) alakítása. Ehhez tudjuk, hogy szükségünk lesz valamiféle „rejtett állapotra”, amelyet egy visszatérő modell kínál. Nagyon szórakoztató lesz kitalálni, mi fog működni ennek az alkalmazásnak, de biztosan tudom, hogy visszatérő modell lesz!

Mint mindig, hogy többet megtudjak erről a témáról, egy egyszerű példával kezdem, amelyről tudom, hogy RNN megoldható. A teszteset, amellyel együtt fogunk dolgozni, meglehetősen egyszerűen fogalmazódik meg: ha egy változó hosszúságú bemeneti tömböt kap, számolja ki a bemenetek összegét. Ezt nagyon könnyű tesztelni, könnyen lehet képzési adatokat generálni, és TÉNYLEG egyszerű lineáris függvény, amelyet lineáris egység fejezhet ki. Tehát, kezdjük az összes géppel!

Adatok generálása

Először néhány vonat- és tesztadatot szeretnénk létrehozni. Júliában ez elég egyszerű:

Néhány gyorsbillentyűt úgy állítunk elő, hogy csak véletlenszerű kis tömböket állítunk elő, amelyek az 1 és 10 közötti értékeket tartalmazzák, és a változó hosszúságúak 2 és 7 között vannak. Mivel a feladat, amelyet megpróbálunk megtanulni, meglehetősen egyszerű, mi ezt vállaljuk! A tesztadatokhoz csak azt vesszük, amit már létrehoztunk, és megszorozzuk mind a képzési vektorokat, mind az edzés kimenetét 2-vel - tudjuk, hogy ez még működni fog! Az egyszerűség kedvéért ugyanannyi képzési és tesztelési adatot állítunk elő - ez általában nem így lesz, de ebben a helyzetben, amikor az adatok könnyen elérhetőek, úgy vesszük!

A szintaxis (v -> összeg (v)). (Vonat_adatok) egy névtelen függvényt ((v -> összeg (v))) és a dot operátort használ arra, hogy ezt az összegző függvényt alkalmazza a képzési adataink mindegyik tömbjére.

Hozza létre a modellt

Ezután szeretnénk valóban létrehozni a modellünket. Ez része a gépi tanulás „varázslatának”, mivel helyesen kell megfogalmaznia a modelljét, különben nem érzéki adatokat kap. A Flux használatával minden bizonnyal elegendő funkcióval rendelkezünk ahhoz, hogy elindulhassunk, ezért a következő modellt választottam:

Ez egyetlen lineáris RNN egységet hoz létre, amely egyszerre egy elemet vesz fel, és minden bemenethez egy elemet állít elő. Egy bemenet-egy-kimenetet akarunk, mert akkumulátort akarunk készíteni - minden bemenethez a kimenetnek tartalmaznia kell annak összes összegét és az összes korábbi bemenetet.

A modell visszaküldi a kimenetet magának úgy, hogy (ebben az esetben) nincs aktiválási funkció a kimenetre (mielőtt visszavezetné magát). Az alapértelmezett aktiválási funkció egy tanh függvény a Fluxban, de ez a kimenetet -1 és 1 közé vágja, ami nem jó, ha RNN összegzést próbálsz készíteni! Tehát ehelyett egy anonim funkciót biztosítunk az RNN egységnek a Fluxból, mint aktiválást, amely semmit sem tesz a bemenettel - csak közvetlenül továbbítja a kimenetet a következő egységhez. Ez meglehetősen atipikus a neurális hálózat tervezésével kapcsolatban, de a jó rész az, hogy tudunk valamit a problémánkról - tudjuk, hogy szeretnénk egy összegző gépet, tehát tudjuk, hogy elég egyszerű lenne megtanulni bonyolult aktiválási funkciók nélkül, és valójában az alapértelmezettel lehetetlen! Később ebben a bejegyzésben megmutatom, mi történik, ha ezt megpróbálja kiképezni az alapértelmezett tanh aktiválási funkcióval ...

Van egy teljes elmélet az aktiválási függvényekről, amelyekbe itt nem térek ki. Ez egyike azoknak a nyúllyukaknak, amelyekbe esetleg tovább merülünk Trebekian útjában, de ezen a ponton nem!

Most, hogy ténylegesen értékelje a modellt egy bemenetsorozaton, így kell hívnia:

Figyelje meg itt a dot jelölést - mivel az RNN-nk egyszerre csak egy bemenetet vesz igénybe, az RNN-t az egyesével megadandó bemenetek sorozatára kell alkalmaznunk. Ezután, ha a kimenet utolsó elemét vesszük (miután látta a teljes szekvenciát), akkor várhatóan megnézzük az összes bemenet összegét.

Vonat! és értékelje

Most, hogy meghatároztuk a modellünket, létrehoztuk a képzést és az értékelést. Valószínűleg ez a legkevesebb kód, amelyet valaha használtam egy képzés és értékelés felállításához olyan nyelven, amellyel ideghálózatokat készítettem…

Az egyetlen furcsaság ez a kicsit:

Két fontos dolgot érdemes megjegyezni:

  1. Ha egy RNN-t hívunk egy bemeneti szekvencián, akkor minden bemenethez kimenetet állít elő (mert vissza kell adnia magának). Tehát ha „sok az egyhez” többet szeretne készíteni, vagy egy olyan modellt szeretne létrehozni, amely egyetlen kimenetet generál egy változó hosszúságú bemenethez, akkor az utolsó elemet (Júliában, a [vég] szintaxissal) kell használni mint a kimenet. És ismét a dot jelölést használjuk a modell alkalmazásához, amint azt fentebb tárgyaltuk.
  2. Minden továbbítási/kiértékelési hívás után meg kell hívni a Flux.reset! (Simple_rnn) fájlt. Mivel egy RNN rejtett állapotú, győződjön meg arról, hogy ezzel a rejtett állapottal nem szennyezi az RNN-hez intézett jövőbeni hívásokat. További információkért lásd ezt a Flux dokumentációs oldalt.

Az edzés során kiértékelés visszahívást használunk (max. 1/másodperces fojtással) a kimenet megjelenítéséhez.

Az ehhez a megvalósításhoz választott veszteségfüggvény egyszerű abszolút értékkülönbség-veszteség volt. Akárcsak az aktiválási függvények, a veszteségfüggvények egész elmélete létezik, és a problémájától függ, hogy melyik a legmegfelelőbb. Egyszerű esetünkben egyszerűnek tartjuk!

Az egészet összerakva a következőképpen néz ki a kimenet, miután lefuttattuk a kódrészletet a Julia shellben:

És amikor néhány bemeneten teszteljük a modellt, a következőket kapjuk. Elképesztő, hogyan készítettünk egy összeadó RNN-t, amely negatív számokkal is képes működni, akkor is, ha nincsenek negatív számok az adatkészletünkben!

Azt is szeretnénk, hogy józanul ellenőrizzük eredményeinket, ha közvetlenül megnézzük a paramétereket. Az ilyen típusú RNN-nek 3 paraméterrel kell rendelkeznie: a bemenet súlya, az előző időbeli lépés bemenetének súlya és torzítás. Ha ellenőrizzük modellünk paramétereit, akkor azt várhatnánk, hogy a bemenet két súlya (jelenlegi és előző) egyaránt 1, és az előfeszítés 0, akárcsak egy összegzőnél. Szerencsére pontosan ez van nálunk!

Hurrá! Összeadót készítettünk!

Fentebb utaltam arra, hogy a modellválasztás fontos része a gépi tanulásnak. A napi munkám során erre állandóan emlékeztetem (számítógépes látást, szoftvert, gépi tanulást, adatelemzést végzek a robotika területén), és itt ismét emlékeztetem rá. Mielőtt megnéztem egy RNN Flux definícióját, nem vettem észre, hogy az alapértelmezett aktiválási funkció a tanh, amely a függvényt a [-1, 1] tartományba szorítja. A fenti képzési/értékelési kód futtatása, de ezzel a modellel:

Fantasztikusan gyenge eredményeket hozott:

Vegye figyelembe, hogy a veszteség nem csökkent. Ha az imént képzett modellt a teljes tesztadatkészleten értékeljük, akkor azt látja, hogy mindennek megvan a maximális értéke, amire csak lehet - 1:

Ez a nyom arra késztetett, hogy belemerüljek a Flux RNN megvalósításába, hogy kitaláljam, hogyan kell egy egyedi (ebben az esetben nem) aktiválási függvényt megadni.