10.4 Dvikrypčiai rekurentiniai neuroniniai tinklai
Iki šiol mūsų pagrindinis sekų mokymosi užduoties pavyzdys buvo kalbos modeliavimas, kai siekiame prognozuoti kitą teksto vienetą, žinodami visus ankstesnius sekos teksto vienetus. Šiuo atveju norime sąlygoti tik kairiojo konteksto atžvilgiu, todėl standartinio RNN vienakryptė grandinė atrodo tinkama. Tačiau yra daug kitų sekų mokymosi užduočių kontekstų, kuriuose visiškai tinka prognozę kiekvienu laiko žingsniu sąlygoti ir kairiuoju, ir dešiniuoju kontekstu. Pavyzdžiui, kalbos dalių nustatymas. Kodėl neturėtume atsižvelgti į kontekstą abiem kryptimis, vertindami su duotu žodžiu susijusią kalbos dalį?
Kita dažna užduotis – dažnai naudinga kaip išankstinio mokymo pratimas prieš modelio adaptaciją tikrajai dominančiai užduočiai – yra užmaskuoti atsitiktinius teksto dokumento teksto vienetus ir tada apmokyti sekų modelį prognozuoti trūkstamų teksto vienetų reikšmes. Atkreipkite dėmesį, kad priklausomai nuo to, kas eina po tuščios vietos, tikėtina trūkstamo teksto vieneto reikšmė smarkiai keičiasi:
- Aš esu
___. - Aš esu
___alkanas. - Aš esu
___alkanas ir galiu suvalgyti pusę kiaulės.
Pirmajame sakinyje „laimingas“ atrodo tikėtinas kandidatas. Žodžiai „ne“ ir „labai“ atrodo tinkami antrajame sakinyje, tačiau „ne“ atrodo nesuderinamas su trečiuoju sakiniu.
Laimei, paprasta technika bet kurį vienakryptį RNN paverčia dvikrypčiu RNN (Schuster ir Paliwal, 1997). Tiesiog įgyvendiname du vienakrypčius RNN sluoksnius, sujungtus priešingomis kryptimis ir veikiančius tą pačią įvestį (10.4.1 pav.). Pirmajam RNN sluoksniui pirmoji įvestis yra , o paskutinė – , tačiau antrajam RNN sluoksniui pirmoji įvestis yra , o paskutinė – . Šio dvikrypčio RNN sluoksnio išvesčiai gauti tiesiog sujungiame (angl. concatenate) atitinkamas dviejų pagrindinių vienakrypčių RNN sluoksnių išvestis.
10.4.1 pav. Dvikrypčio RNN architektūra.
Formaliai, bet kuriam laiko žingsniui nagrinėjame minirinkinio įvestį (pavyzdžių skaičius ; įvesčių skaičius kiekviename pavyzdyje ) ir tegul paslėptojo sluoksnio aktyvacijos funkcija yra . Dvikryptėje architektūroje šio laiko žingsnio tiesioginė ir atvirkštinė paslėptosios būsenos yra atitinkamai ir , kur yra paslėptųjų vienetų skaičius. Tiesioginės ir atvirkštinės paslėptųjų būsenų atnaujinimai yra tokie:
kur svoriai , o poslinkiai ir yra visi modelio parametrai.
Toliau sujungiame tiesioginę ir atvirkštinę paslėptąsias būsenas ir , kad gautume paslėptąją būseną , paduodamą į išvesties sluoksnį. Giliuosiuose dvikrypčiuose RNN su keliais paslėptaisiais sluoksniais tokia informacija perduodama kaip įvestis į kitą dvikryptį sluoksnį. Galiausiai išvesties sluoksnis apskaičiuoja išvestį (išvesčių skaičius ):
Čia svorių matrica ir poslinkis yra išvesties sluoksnio modelio parametrai. Nors techniškai dvi kryptys gali turėti skirtingą paslėptųjų vienetų skaičių, šis dizaino pasirinkimas praktikoje daromas retai. Dabar pademonstruosime paprastą dvikrypčio RNN įgyvendinimą.
from d2l import mxnet as d2l
from mxnet import npx, np
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 jax import numpy as jnp
10.4.1 Įgyvendinimas nuo nulio
Jei norime įgyvendinti dvikryptį RNN nuo nulio,
galime įtraukti du vienakrypčius RNNScratch egzempliorius
su atskirais mokomais parametrais.
class BiRNNScratch(d2l.Module):
def __init__(self, num_inputs, num_hiddens, sigma=0.01):
super().__init__()
self.save_hyperparameters()
self.f_rnn = d2l.RNNScratch(num_inputs, num_hiddens, sigma)
self.b_rnn = d2l.RNNScratch(num_inputs, num_hiddens, sigma)
self.num_hiddens *= 2 # The output dimension will be doubled
class BiRNNScratch(d2l.Module):
num_inputs: int
num_hiddens: int
sigma: float = 0.01
def setup(self):
self.f_rnn = d2l.RNNScratch(num_inputs, num_hiddens, sigma)
self.b_rnn = d2l.RNNScratch(num_inputs, num_hiddens, sigma)
self.num_hiddens *= 2 # The output dimension will be doubled
Tiesioginio ir atvirkštinio RNN būsenos atnaujinamos atskirai, o šių dviejų RNN išvestys sujungiamos.
@d2l.add_to_class(BiRNNScratch)
def forward(self, inputs, Hs=None):
f_H, b_H = Hs if Hs is not None else (None, None)
f_outputs, f_H = self.f_rnn(inputs, f_H)
b_outputs, b_H = self.b_rnn(reversed(inputs), b_H)
outputs = [d2l.concat((f, b), -1) for f, b in zip(
f_outputs, reversed(b_outputs))]
return outputs, (f_H, b_H)
10.4.2 Glaustas įgyvendinimas
Naudodami aukšto lygio API, dvikrypčius RNN galime įgyvendinti glausčiau. Čia kaip pavyzdį imame GRU modelį.
Flax API nesiūlo RNN sluoksnių, todėl nėra
jokio bidirectional argumento sampratos. Jei reikalingas dvikryptis sluoksnis,
reikia rankiniu būdu apversti įvestis, kaip parodyta
įgyvendinime nuo nulio.
class BiGRU(d2l.RNN):
def __init__(self, num_inputs, num_hiddens):
d2l.Module.__init__(self)
self.save_hyperparameters()
if tab.selected('mxnet'):
self.rnn = rnn.GRU(num_hiddens, bidirectional=True)
if tab.selected('pytorch'):
self.rnn = nn.GRU(num_inputs, num_hiddens, bidirectional=True)
self.num_hiddens *= 2
10.4.3 Santrauka
Dvikrypčiuose RNN kiekvieno laiko žingsnio paslėptoji būsena vienu metu nustatoma pagal duomenis prieš dabartinį laiko žingsnį ir po jo. Dvikrypčiai RNN daugiausia naudingi sekų kodavimui ir stebėjimų įvertinimui duotame dvikrypčiame kontekste. Dvikrypčius RNN apmokyti labai brangu dėl ilgų gradientų grandinių.
10.4.4 Pratimai
- Jei skirtingos kryptys naudoja skirtingą paslėptųjų vienetų skaičių, kaip pasikeis forma?
- Suprojektuokite dvikryptį RNN su keliais paslėptaisiais sluoksniais.
- Polisemija natūraliosiose kalbose yra dažna. Pavyzdžiui, žodis „bank“ (angl. krantas / bankas) turi skirtingas reikšmes kontekstuose „i went to the bank to deposit cash“ ir „i went to the bank to sit down“. Kaip galime suprojektuoti neuroninio tinklo modelį, kad pagal duotą konteksto seką ir žodį būtų grąžinamas to žodžio vektorinis vaizdas teisingame kontekste? Kokio tipo neuroninės architektūros pageidautinos polisemijai spręsti?
