10.3 Gilieji rekurentiniai neuroniniai tinklai

Iki šiol daugiausia dėmesio skyrėme tinklų apibrėžimui, sudarytų iš sekos įvesties, vieno paslėptojo RNN sluoksnio ir išvesties sluoksnio. Nepaisant to, kad tarp įvesties bet kuriuo laiko žingsniu ir atitinkamos išvesties yra tik vienas paslėptasis sluoksnis, tam tikra prasme šie tinklai yra gilūs. Įvestys iš pirmojo laiko žingsnio gali paveikti išvestis paskutiniu laiko žingsniu TT (dažnai praėjus šimtams ar tūkstančiams žingsnių). Šios įvestys TT kartų praeina per rekurentinį sluoksnį, kol pasiekia galutinę išvestį. Tačiau dažnai taip pat norime išlaikyti galimybę išreikšti sudėtingus ryšius tarp įvesčių duotu laiko žingsniu ir išvesčių tuo pačiu laiko žingsniu. Todėl dažnai kuriame RNN, kurie yra gilūs ne tik laiko kryptimi, bet ir kryptimi nuo įvesties iki išvesties. Tai būtent ta gylio samprata, su kuria jau susidūrėme kurdami MLP ir gilius CNN.

Standartinis tokio giliojo RNN kūrimo metodas stulbinamai paprastas: RNN sukrauname vieną ant kito. Duotai TT ilgio sekai pirmasis RNN sukuria išvesčių seką, taip pat TT ilgio. Jos savo ruožtu sudaro kito RNN sluoksnio įvestis. Šiame trumpame skyrelyje iliustruojame šį dizaino šabloną ir pateikiame paprastą pavyzdį, kaip užprogramuoti tokius sukrautus RNN. Toliau 10.3.1 pav. iliustruojame gilųjį RNN su LL paslėptųjų sluoksnių. Kiekviena paslėptoji būsena veikia sekos įvestį ir sukuria sekos išvestį. Be to, bet kuri RNN ląstelė (balta dėžutė 10.3.1 pav.) kiekvienu laiko žingsniu priklauso ir nuo to paties sluoksnio reikšmės ankstesniu laiko žingsniu, ir nuo ankstesnio sluoksnio reikšmės tuo pačiu laiko žingsniu.

Giliojo RNN architektūra.

10.3.1 pav. Giliojo RNN architektūra.

Formaliai, tarkime, kad turime minirinkinio įvestį Xt∈Rn×d\mathbf{X}_t \in \mathbb{R}^{n \times d} (pavyzdžių skaičius =n=n; įvesčių skaičius kiekviename pavyzdyje =d=d) laiko žingsniu tt. Tuo pačiu laiko žingsniu tegul ll-ojo paslėptojo sluoksnio (l=1,…,Ll=1,\ldots,L) paslėptoji būsena yra Ht(l)∈Rn×h\mathbf{H}_t^{(l)} \in \mathbb{R}^{n \times h} (paslėptųjų vienetų skaičius =h=h), o išvesties sluoksnio kintamasis yra Ot∈Rn×q\mathbf{O}_t \in \mathbb{R}^{n \times q} (išvesčių skaičius: qq). Nustačius Ht(0)=Xt\mathbf{H}_t^{(0)} = \mathbf{X}_t, ll-ojo paslėptojo sluoksnio, naudojančio aktyvacijos funkciją ϕl\phi_l, paslėptoji būsena apskaičiuojama taip:

Ht(l)=ϕl(Ht(l−1)Wxh(l)+Ht−1(l)Whh(l)+bh(l)),(10.3.1)\mathbf{H}_t^{(l)} = \phi_l(\mathbf{H}_t^{(l-1)} \mathbf{W}_{\textrm{xh}}^{(l)} + \mathbf{H}_{t-1}^{(l)} \mathbf{W}_{\textrm{hh}}^{(l)} + \mathbf{b}_\textrm{h}^{(l)}), \tag{10.3.1}

kur svoriai Wxh(l)∈Rh×h\mathbf{W}_{\textrm{xh}}^{(l)} \in \mathbb{R}^{h \times h} ir Whh(l)∈Rh×h\mathbf{W}_{\textrm{hh}}^{(l)} \in \mathbb{R}^{h \times h} kartu su poslinkiu bh(l)∈R1×h\mathbf{b}_\textrm{h}^{(l)} \in \mathbb{R}^{1 \times h} yra ll-ojo paslėptojo sluoksnio modelio parametrai.

Galiausiai išvesties sluoksnio skaičiavimas remiasi tik galutinio LL-ojo paslėptojo sluoksnio paslėptąja būsena:

Ot=Ht(L)Whq+bq,\mathbf{O}_t = \mathbf{H}_t^{(L)} \mathbf{W}_{\textrm{hq}} + \mathbf{b}_\textrm{q},

kur svoris Whq∈Rh×q\mathbf{W}_{\textrm{hq}} \in \mathbb{R}^{h \times q} ir poslinkis bq∈R1×q\mathbf{b}_\textrm{q} \in \mathbb{R}^{1 \times q} yra išvesties sluoksnio modelio parametrai.

Kaip ir MLP atveju, paslėptųjų sluoksnių skaičius LL ir paslėptųjų vienetų skaičius hh yra hiperparametrai, kuriuos galime derinti. Įprasti RNN sluoksnių pločiai (hh) yra intervale (64,2056)(64, 2056), o įprasti gyliai (LL) – intervale (1,8)(1, 8). Be to, lengvai galime gauti gilųjį sklendėmis valdomą RNN, pakeitę paslėptosios būsenos skaičiavimą (10.3.1) LSTM ar GRU skaičiavimu.

from d2l import mxnet as d2l
from mxnet import np, npx
from mxnet.gluon import rnn
npx.set_np()
from d2l import torch as d2l
import torch
from torch import nn
from d2l import tensorflow as d2l
import tensorflow as tf
from d2l import jax as d2l
from flax import linen as nn
import jax
from jax import numpy as jnp

10.3.1 Įgyvendinimas nuo nulio

Kad įgyvendintume daugiasluoksnį RNN nuo nulio, kiekvieną sluoksnį galime traktuoti kaip RNNScratch egzempliorių su savais mokomais parametrais.

class StackedRNNScratch(d2l.Module):
    def __init__(self, num_inputs, num_hiddens, num_layers, sigma=0.01):
        super().__init__()
        self.save_hyperparameters()
        self.rnns = [d2l.RNNScratch(num_inputs if i==0 else num_hiddens,
                                    num_hiddens, sigma)
                     for i in range(num_layers)]
class StackedRNNScratch(d2l.Module):
    def __init__(self, num_inputs, num_hiddens, num_layers, sigma=0.01):
        super().__init__()
        self.save_hyperparameters()
        self.rnns = nn.Sequential(*[d2l.RNNScratch(
            num_inputs if i==0 else num_hiddens, num_hiddens, sigma)
                                    for i in range(num_layers)])
class StackedRNNScratch(d2l.Module):
    num_inputs: int
    num_hiddens: int
    num_layers: int
    sigma: float = 0.01

    def setup(self):
        self.rnns = [d2l.RNNScratch(self.num_inputs if i==0 else self.num_hiddens,
                                    self.num_hiddens, self.sigma)
                     for i in range(self.num_layers)]

Daugiasluoksnis tiesioginis skaičiavimas tiesiog atlieka tiesioginį skaičiavimą sluoksnis po sluoksnio.

@d2l.add_to_class(StackedRNNScratch)
def forward(self, inputs, Hs=None):
    outputs = inputs
    if Hs is None: Hs = [None] * self.num_layers
    for i in range(self.num_layers):
        outputs, Hs[i] = self.rnns[i](outputs, Hs[i])
        outputs = d2l.stack(outputs, 0)
    return outputs, Hs

Kaip pavyzdį apmokome gilųjį GRU modelį Laiko mašinos duomenų rinkinyje (kaip 9.5 skyrelis). Kad viskas būtų paprasta, sluoksnių skaičių nustatome į 2.

data = d2l.TimeMachine(batch_size=1024, num_steps=32)
if tab.selected('mxnet', 'pytorch', 'jax'):
    rnn_block = StackedRNNScratch(num_inputs=len(data.vocab),
                                  num_hiddens=32, num_layers=2)
    model = d2l.RNNLMScratch(rnn_block, vocab_size=len(data.vocab), lr=2)
    trainer = d2l.Trainer(max_epochs=100, gradient_clip_val=1, num_gpus=1)
if tab.selected('tensorflow'):
    with d2l.try_gpu():
        rnn_block = StackedRNNScratch(num_inputs=len(data.vocab),
                                  num_hiddens=32, num_layers=2)
        model = d2l.RNNLMScratch(rnn_block, vocab_size=len(data.vocab), lr=2)
    trainer = d2l.Trainer(max_epochs=100, gradient_clip_val=1)
trainer.fit(model, data)

10.3.2 Glaustas įgyvendinimas

Laimei, daug logistinių detalių, reikalingų kelių RNN sluoksnių įgyvendinimui, yra paruoštos aukšto lygio API. Mūsų glaustas įgyvendinimas naudos tokias integruotas funkcijas. Kodas apibendrina tą, kurį anksčiau naudojome 10.2 skyrelis, leisdamas aiškiai nurodyti sluoksnių skaičių, užuot pasirinkus numatytąjį tik vieno sluoksnio variantą.

Flax įgyvendindamas RNN laikosi minimalistinio požiūrio. RNN sluoksnių skaičiaus apibrėžimas ar jo derinimas su dropout nėra pasiekiamas iš dėžės. Mūsų glaustas įgyvendinimas naudos visas integruotas funkcijas ir papildomai pridės num_layers ir dropout galimybes. Kodas apibendrina tą, kurį anksčiau naudojome 10.2 skyrelis, leisdamas aiškiai nurodyti sluoksnių skaičių, užuot pasirinkus numatytąjį vieno sluoksnio variantą.

class GRU(d2l.RNN):  #@save
    """The multilayer GRU model."""
    def __init__(self, num_hiddens, num_layers, dropout=0):
        d2l.Module.__init__(self)
        self.save_hyperparameters()
        self.rnn = rnn.GRU(num_hiddens, num_layers, dropout=dropout)
class GRU(d2l.RNN):  #@save
    """The multilayer GRU model."""
    def __init__(self, num_inputs, num_hiddens, num_layers, dropout=0):
        d2l.Module.__init__(self)
        self.save_hyperparameters()
        self.rnn = nn.GRU(num_inputs, num_hiddens, num_layers,
                          dropout=dropout)
class GRU(d2l.RNN):  #@save
    """The multilayer GRU model."""
    def __init__(self, num_hiddens, num_layers, dropout=0):
        d2l.Module.__init__(self)
        self.save_hyperparameters()
        gru_cells = [tf.keras.layers.GRUCell(num_hiddens, dropout=dropout)
                     for _ in range(num_layers)]
        self.rnn = tf.keras.layers.RNN(gru_cells, return_sequences=True,
                                       return_state=True, time_major=True)

    def forward(self, X, state=None):
        outputs, *state = self.rnn(X, state)
        return outputs, state
class GRU(d2l.RNN):  #@save
    """The multilayer GRU model."""
    num_hiddens: int
    num_layers: int
    dropout: float = 0

    @nn.compact
    def __call__(self, X, state=None, training=False):
        outputs = X
        new_state = []
        if state is None:
            batch_size = X.shape[1]
            state = [nn.GRUCell.initialize_carry(jax.random.PRNGKey(0),
                    (batch_size,), self.num_hiddens)] * self.num_layers

        GRU = nn.scan(nn.GRUCell, variable_broadcast="params",
                      in_axes=0, out_axes=0, split_rngs={"params": False})

        # Introduce a dropout layer after every GRU layer except last
        for i in range(self.num_layers - 1):
            layer_i_state, X = GRU()(state[i], outputs)
            new_state.append(layer_i_state)
            X = nn.Dropout(self.dropout, deterministic=not training)(X)

        # Final GRU layer without dropout
        out_state, X = GRU()(state[-1], X)
        new_state.append(out_state)
        return X, jnp.array(new_state)

Architektūriniai sprendimai, tokie kaip hiperparametrų pasirinkimas, labai panašūs į 10.2 skyrelis. Pasirenkame tiek pat įvesčių ir išvesčių, kiek turime skirtingų teksto vienetų, t. y. vocab_size. Paslėptųjų vienetų skaičius vis dar yra 32. Vienintelis skirtumas, kad dabar (pasirenkame netrivialų paslėptųjų sluoksnių skaičių, nurodydami num_layers reikšmę.)

gru = GRU(num_hiddens=32, num_layers=2)
model = d2l.RNNLM(gru, vocab_size=len(data.vocab), lr=2)

# Running takes > 1h (pending fix from MXNet)
# trainer.fit(model, data)
# model.predict('it has', 20, data.vocab, d2l.try_gpu())
if tab.selected('tensorflow', 'jax'):
    gru = GRU(num_hiddens=32, num_layers=2)
if tab.selected('pytorch'):
    gru = GRU(num_inputs=len(data.vocab), num_hiddens=32, num_layers=2)
if tab.selected('pytorch', 'jax'):
    model = d2l.RNNLM(gru, vocab_size=len(data.vocab), lr=2)
if tab.selected('tensorflow'):
    with d2l.try_gpu():
        model = d2l.RNNLM(gru, vocab_size=len(data.vocab), lr=2)
trainer.fit(model, data)
model.predict('it has', 20, data.vocab, d2l.try_gpu())
model.predict('it has', 20, data.vocab)
model.predict('it has', 20, data.vocab, trainer.state.params)

10.3.3 Santrauka

Gilieji RNN perduoda paslėptosios būsenos informaciją į kitą dabartinio sluoksnio laiko žingsnį ir į dabartinį kito sluoksnio laiko žingsnį. Egzistuoja daug skirtingų gilųjų RNN variantų, tokių kaip LSTM, GRU ar paprasti RNN. Patogu, kad visi šie modeliai pasiekiami kaip giliojo mokymosi karkasų aukšto lygio API dalis. Modelių inicializavimas reikalauja atidumo. Apskritai gilieji RNN reikalauja nemažai darbo (pvz., mokymosi greičio ir apkarpymo derinimo), kad būtų užtikrintas tinkamas konvergavimas.

10.3.4 Pratimai

  1. Pakeiskite GRU į LSTM ir palyginkite tikslumą bei apmokymo greitį.
  2. Padidinkite apmokymo duomenis, įtraukdami kelias knygas. Kokio žemo painumo galite pasiekti?
  3. Ar norėtumėte modeliuodami tekstą derinti skirtingų autorių šaltinius? Kodėl tai gera idėja? Kas gali nepavykti?

Diskusijos

Diskusijos

Diskusijos

Diskusijos