11.6 Saviraiška dėmesio sutelkimo transformacija ir pozicinis kodavimas

Giliajame mokymesi sekoms koduoti dažnai naudojame CNN arba RNN. Dabar, turėdami omenyje dėmesio sutelkimo mechanizmus, įsivaizduokime, kad teksto vienetų seką paduodame į dėmesio sutelkimo mechanizmą taip, kad kiekviename žingsnyje kiekvienas teksto vienetas turi savo užklausą, raktus ir reikšmes. Čia, apskaičiuodamas teksto vieneto vaizdo reikšmę kitame sluoksnyje, teksto vienetas gali sutelkti dėmesį (per savo užklausos vektorių) į bet kurį kitą teksto vienetą (sutapatinant pagal jų raktų vektorius). Naudodami visą užklausos ir rakto suderinamumo įverčių aibę, galime apskaičiuoti kiekvieno teksto vieneto vaizdą, sudarydami atitinkamą svertinę sumą per kitus teksto vienetus. Kadangi kiekvienas teksto vienetas sutelkia dėmesį į kiekvieną kitą teksto vienetą (priešingai nei atveju, kai dekodavimo žingsniai sutelkia dėmesį į kodavimo žingsnius), tokios architektūros paprastai apibūdinamos kaip saviraiškės dėmesio sutelkimo transformacijos (angl. self-attention) modeliai (Lin ir kt., 2017; Vaswani ir kt., 2017), o kitur apibūdinamos kaip vidinio dėmesio (angl. intra-attention) modelis (Cheng ir kt., 2016; Parikh ir kt., 2016; Paulus ir kt., 2017). Šiame skyrelyje aptarsime sekų kodavimą naudojant saviraiškę dėmesio sutelkimo transformaciją, įskaitant papildomos informacijos apie sekos tvarką naudojimą.

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

11.6.1 Saviraiška dėmesio sutelkimo transformacija

Duotai įvesties teksto vienetų sekai x1,…,xn\mathbf{x}_1, \ldots, \mathbf{x}_n, kur bet kuris xi∈Rd\mathbf{x}_i \in \mathbb{R}^d (1≤i≤n1 \leq i \leq n), jos saviraiška dėmesio sutelkimo transformacija išveda to paties ilgio seką y1,…,yn\mathbf{y}_1, \ldots, \mathbf{y}_n, kur

yi=f(xi,(x1,x1),…,(xn,xn))∈Rd\mathbf{y}_i = f(\mathbf{x}_i, (\mathbf{x}_1, \mathbf{x}_1), \ldots, (\mathbf{x}_n, \mathbf{x}_n)) \in \mathbb{R}^d

pagal dėmesio sutelkimo apibrėžimą (11.1.1). Naudodamas lygiagrečią dėmesio sutelkimo transformaciją, toliau pateiktas kodo fragmentas apskaičiuoja tenzoriaus saviraišką dėmesio sutelkimo transformaciją, kurio forma (rinkinio dydis, laiko žingsnių skaičius arba sekos ilgis teksto vienetais, dd). Išvesties tenzorius turi tą pačią formą.

num_hiddens, num_heads = 100, 5
attention = d2l.MultiHeadAttention(num_hiddens, num_heads, 0.5)
batch_size, num_queries, valid_lens = 2, 4, d2l.tensor([3, 2])
X = d2l.ones((batch_size, num_queries, num_hiddens))
d2l.check_shape(attention(X, X, X, valid_lens),
                (batch_size, num_queries, num_hiddens))
num_hiddens, num_heads = 100, 5
attention = d2l.MultiHeadAttention(num_hiddens, num_heads, 0.5)
attention.initialize()
num_hiddens, num_heads = 100, 5
attention = d2l.MultiHeadAttention(num_hiddens, num_heads, 0.5)
num_hiddens, num_heads = 100, 5
attention = d2l.MultiHeadAttention(num_hiddens, num_hiddens, num_hiddens,
                                   num_hiddens, num_heads, 0.5)
batch_size, num_queries, valid_lens = 2, 4, d2l.tensor([3, 2])
X = d2l.ones((batch_size, num_queries, num_hiddens))
d2l.check_shape(attention(X, X, X, valid_lens),
                (batch_size, num_queries, num_hiddens))
batch_size, num_queries, valid_lens = 2, 4, tf.constant([3, 2])
X = tf.ones((batch_size, num_queries, num_hiddens))
d2l.check_shape(attention(X, X, X, valid_lens, training=False),
                (batch_size, num_queries, num_hiddens))
batch_size, num_queries, valid_lens = 2, 4, d2l.tensor([3, 2])
X = d2l.ones((batch_size, num_queries, num_hiddens))
d2l.check_shape(attention.init_with_output(d2l.get_key(), X, X, X, valid_lens,
                                           training=False)[0][0],
                (batch_size, num_queries, num_hiddens))

11.6.2 CNN, RNN ir saviraiškės dėmesio sutelkimo transformacijos palyginimas

Palyginkime architektūras, skirtas nn teksto vienetų sekai atvaizduoti į kitą tokio paties ilgio seką, kur kiekvienas įvesties ar išvesties teksto vienetas vaizduojamas dd matmenų vektoriumi. Konkrečiai, nagrinėsime CNN, RNN ir saviraišką dėmesio sutelkimo transformaciją. Palyginsime jų skaičiavimo sudėtingumą, nuoseklias operacijas ir didžiausius kelio ilgius. Atkreipkite dėmesį, kad nuoseklios operacijos neleidžia lygiagretinti skaičiavimų, o trumpesnis kelias tarp bet kurio sekos pozicijų derinio palengvina ilgo nuotolio priklausomybių sekoje mokymąsi (Hochreiter ir kt., 2001).

CNN (užpildymo teksto vienetai praleisti), RNN ir saviraiškės dėmesio sutelkimo transformacijos architektūrų palyginimas.

11.6.1 pav. CNN (užpildymo teksto vienetai praleisti), RNN ir saviraiškės dėmesio sutelkimo transformacijos architektūrų palyginimas.

Bet kurią teksto seką laikykime „vienmačiu vaizdu“. Panašiai vienmačiai CNN gali apdoroti lokalius požymius, tokius kaip nn-gramos tekste. Duotai nn ilgio sekai apsvarstykime konvoliucinį sluoksnį, kurio branduolio dydis yra kk, o įvesties ir išvesties kanalų skaičius abu yra dd. Konvoliucinio sluoksnio skaičiavimo sudėtingumas yra O(knd2)\mathcal{O}(knd^2). Kaip rodo 11.6.1 pav., CNN yra hierarchiniai, todėl yra O(1)\mathcal{O}(1) nuoseklių operacijų, o didžiausias kelio ilgis yra O(n/k)\mathcal{O}(n/k). Pavyzdžiui, 11.6.1 pav. x1\mathbf{x}_1 ir x5\mathbf{x}_5 patenka į dviejų sluoksnių CNN su 3 dydžio branduoliu lokalią matymo sritį.

Atnaujinant RNN paslėptąją būseną, d×dd \times d svorių matricos ir dd matmenų paslėptosios būsenos daugyba turi O(d2)\mathcal{O}(d^2) skaičiavimo sudėtingumą. Kadangi sekos ilgis yra nn, rekurentinio sluoksnio skaičiavimo sudėtingumas yra O(nd2)\mathcal{O}(nd^2). Pagal 11.6.1 pav. yra O(n)\mathcal{O}(n) nuoseklių operacijų, kurių negalima lygiagretinti, o didžiausias kelio ilgis taip pat yra O(n)\mathcal{O}(n).

Saviraiškėje dėmesio sutelkimo transformacijoje užklausos, raktai ir reikšmės yra visos n×dn \times d matricos. Apsvarstykime mastelio keičiamos skaliarinės sandaugos dėmesį (11.3.4), kur n×dn \times d matrica dauginama iš d×nd \times n matricos, o tada gauta n×nn \times n išvesties matrica dauginama iš n×dn \times d matricos. Dėl to saviraiškė dėmesio sutelkimo transformacija turi O(n2d)\mathcal{O}(n^2d) skaičiavimo sudėtingumą. Kaip matome iš 11.6.1 pav., kiekvienas teksto vienetas tiesiogiai sujungtas su bet kuriuo kitu teksto vienetu per saviraišką dėmesio sutelkimo transformaciją. Todėl skaičiavimas gali būti lygiagretus su O(1)\mathcal{O}(1) nuoseklių operacijų, o didžiausias kelio ilgis taip pat yra O(1)\mathcal{O}(1).

Apskritai ir CNN, ir saviraiškė dėmesio sutelkimo transformacija leidžia lygiagretų skaičiavimą, o saviraiškė dėmesio sutelkimo transformacija turi trumpiausią didžiausią kelio ilgį. Tačiau kvadratinis skaičiavimo sudėtingumas sekos ilgio atžvilgiu daro saviraišką dėmesio sutelkimo transformaciją nepriimtinai lėtą labai ilgoms sekoms.

11.6.3 Pozicinis kodavimas

Skirtingai nei RNN, rekurentiškai apdorojantys sekos teksto vienetus po vieną, saviraiškė dėmesio sutelkimo transformacija atsisako nuosekliųjų operacijų lygiagrečiojo skaičiavimo naudai. Atkreipkite dėmesį, kad saviraiškė dėmesio sutelkimo transformacija pati savaime nesaugo sekos tvarkos. Ką daryti, jei tikrai svarbu, kad modelis žinotų, kokia tvarka atėjo įvesties seka?

Dominuojantis metodas informacijai apie teksto vienetų tvarką išsaugoti yra tai pateikti modeliui kaip papildomą įvestį, susietą su kiekvienu teksto vienetu. Šios įvestys vadinamos poziciniais kodavimais, ir jie gali būti arba išmokti, arba nustatyti a priori. Dabar aprašome paprastą fiksuotų pozicinių kodavimų schemą, pagrįstą sinuso ir kosinuso funkcijomis (Vaswani ir kt., 2017).

Tarkime, kad įvesties vaizdas X∈Rn×d\mathbf{X} \in \mathbb{R}^{n \times d} turi dd matmenų įterpinius nn sekos teksto vienetų. Pozicinis kodavimas išveda X+P\mathbf{X} + \mathbf{P}, naudodamas tos pačios formos pozicinių įterpinių matricą P∈Rn×d\mathbf{P} \in \mathbb{R}^{n \times d}, kurios elementas ii-ojoje eilutėje ir (2j)(2j)-ajame arba (2j+1)(2j + 1)-ajame stulpelyje yra

pi,2j=sin⁡(i100002j/d),pi,2j+1=cos⁡(i100002j/d).(11.6.1)\begin{aligned} p_{i, 2j} &= \sin\left(\frac{i}{10000^{2j/d}}\right),\\p_{i, 2j+1} &= \cos\left(\frac{i}{10000^{2j/d}}\right).\end{aligned} \tag{11.6.1}

Iš pirmo žvilgsnio šis trigonometrinių funkcijų dizainas atrodo keistas. Prieš paaiškindami šį dizainą, pirmiausia įgyvendinkime jį toliau pateiktoje klasėje PositionalEncoding.

class PositionalEncoding(nn.Block):  #@save
    """Positional encoding."""
    def __init__(self, num_hiddens, dropout, max_len=1000):
        super().__init__()
        self.dropout = nn.Dropout(dropout)
        # Create a long enough P
        self.P = d2l.zeros((1, max_len, num_hiddens))
        X = d2l.arange(max_len).reshape(-1, 1) / np.power(
            10000, np.arange(0, num_hiddens, 2) / num_hiddens)
        self.P[:, :, 0::2] = np.sin(X)
        self.P[:, :, 1::2] = np.cos(X)

    def forward(self, X):
        X = X + self.P[:, :X.shape[1], :].as_in_ctx(X.ctx)
        return self.dropout(X)
class PositionalEncoding(nn.Module):  #@save
    """Positional encoding."""
    def __init__(self, num_hiddens, dropout, max_len=1000):
        super().__init__()
        self.dropout = nn.Dropout(dropout)
        # Create a long enough P
        self.P = d2l.zeros((1, max_len, num_hiddens))
        X = d2l.arange(max_len, dtype=torch.float32).reshape(
            -1, 1) / torch.pow(10000, torch.arange(
            0, num_hiddens, 2, dtype=torch.float32) / num_hiddens)
        self.P[:, :, 0::2] = torch.sin(X)
        self.P[:, :, 1::2] = torch.cos(X)

    def forward(self, X):
        X = X + self.P[:, :X.shape[1], :].to(X.device)
        return self.dropout(X)
class PositionalEncoding(tf.keras.layers.Layer):  #@save
    """Positional encoding."""
    def __init__(self, num_hiddens, dropout, max_len=1000):
        super().__init__()
        self.dropout = tf.keras.layers.Dropout(dropout)
        # Create a long enough P
        self.P = np.zeros((1, max_len, num_hiddens))
        X = np.arange(max_len, dtype=np.float32).reshape(
            -1,1)/np.power(10000, np.arange(
            0, num_hiddens, 2, dtype=np.float32) / num_hiddens)
        self.P[:, :, 0::2] = np.sin(X)
        self.P[:, :, 1::2] = np.cos(X)
        
    def call(self, X, **kwargs):
        X = X + self.P[:, :X.shape[1], :]
        return self.dropout(X, **kwargs)
class PositionalEncoding(nn.Module):  #@save
    """Positional encoding."""
    num_hiddens: int
    dropout: float
    max_len: int = 1000

    def setup(self):
        # Create a long enough P
        self.P = d2l.zeros((1, self.max_len, self.num_hiddens))
        X = d2l.arange(self.max_len, dtype=jnp.float32).reshape(
            -1, 1) / jnp.power(10000, jnp.arange(
            0, self.num_hiddens, 2, dtype=jnp.float32) / self.num_hiddens)
        self.P = self.P.at[:, :, 0::2].set(jnp.sin(X))
        self.P = self.P.at[:, :, 1::2].set(jnp.cos(X))

    @nn.compact
    def __call__(self, X, training=False):
        # Flax sow API is used to capture intermediate variables
        self.sow('intermediates', 'P', self.P)
        X = X + self.P[:, :X.shape[1], :]
        return nn.Dropout(self.dropout)(X, deterministic=not training)

Pozicinių įterpinių matricoje P\mathbf{P} eilutės atitinka pozicijas sekoje, o stulpeliai vaizduoja skirtingas pozicinio kodavimo dimensijas. Toliau pateiktame pavyzdyje matome, kad pozicinių įterpinių matricos 66-asis ir 77-asis stulpeliai turi aukštesnį dažnį nei 88-asis ir 99-asis stulpeliai. Poslinkis tarp 66-ojo ir 77-ojo (taip pat 88-ojo ir 99-ojo) stulpelių atsiranda dėl sinuso ir kosinuso funkcijų kaitaliojimosi.

encoding_dim, num_steps = 32, 60
pos_encoding = PositionalEncoding(encoding_dim, 0)
pos_encoding.initialize()
X = pos_encoding(np.zeros((1, num_steps, encoding_dim)))
P = pos_encoding.P[:, :X.shape[1], :]
d2l.plot(d2l.arange(num_steps), P[0, :, 6:10].T, xlabel='Row (position)',
         figsize=(6, 2.5), legend=["Col %d" % d for d in d2l.arange(6, 10)])
encoding_dim, num_steps = 32, 60
pos_encoding = PositionalEncoding(encoding_dim, 0)
X = pos_encoding(d2l.zeros((1, num_steps, encoding_dim)))
P = pos_encoding.P[:, :X.shape[1], :]
d2l.plot(d2l.arange(num_steps), P[0, :, 6:10].T, xlabel='Row (position)',
         figsize=(6, 2.5), legend=["Col %d" % d for d in d2l.arange(6, 10)])
encoding_dim, num_steps = 32, 60
pos_encoding = PositionalEncoding(encoding_dim, 0)
X = pos_encoding(tf.zeros((1, num_steps, encoding_dim)), training=False)
P = pos_encoding.P[:, :X.shape[1], :]
d2l.plot(np.arange(num_steps), P[0, :, 6:10].T, xlabel='Row (position)',
         figsize=(6, 2.5), legend=["Col %d" % d for d in np.arange(6, 10)])
encoding_dim, num_steps = 32, 60
pos_encoding = PositionalEncoding(encoding_dim, 0)
params = pos_encoding.init(d2l.get_key(), d2l.zeros((1, num_steps, encoding_dim)))
X, inter_vars = pos_encoding.apply(params, d2l.zeros((1, num_steps, encoding_dim)),
                                   mutable='intermediates')
P = inter_vars['intermediates']['P'][0]  # retrieve intermediate value P
P = P[:, :X.shape[1], :]
d2l.plot(d2l.arange(num_steps), P[0, :, 6:10].T, xlabel='Row (position)',
         figsize=(6, 2.5), legend=["Col %d" % d for d in d2l.arange(6, 10)])

11.6.3.1 Absoliuti pozicinė informacija

Kad pamatytume, kaip monotoniškai mažėjantis dažnis išilgai kodavimo dimensijos susijęs su absoliučia pozicine informacija, išspausdinkime dvejetainius vaizdus skaičių 0,1,…,70, 1, \ldots, 7. Kaip matome, žemiausias bitas, antras žemiausias bitas ir trečias žemiausias bitas keičiasi atitinkamai kas skaičių, kas du skaičius ir kas keturis skaičius.

for i in range(8):
    print(f'{i} in binary is {i:>03b}')

Dvejetainiuose vaizduose aukštesnis bitas turi žemesnį dažnį nei žemesnis bitas. Panašiai, kaip parodyta toliau pateiktame šilumos žemėlapyje, pozicinis kodavimas mažina dažnius išilgai kodavimo dimensijos, naudodamas trigonometrines funkcijas. Kadangi išvestys yra slankiojo kablelio skaičiai, tokie tolydūs vaizdai yra efektyvesni vietos požiūriu nei dvejetainiai vaizdai.

P = np.expand_dims(np.expand_dims(P[0, :, :], 0), 0)
d2l.show_heatmaps(P, xlabel='Column (encoding dimension)',
                  ylabel='Row (position)', figsize=(3.5, 4), cmap='Blues')
P = P[0, :, :].unsqueeze(0).unsqueeze(0)
d2l.show_heatmaps(P, xlabel='Column (encoding dimension)',
                  ylabel='Row (position)', figsize=(3.5, 4), cmap='Blues')
P = tf.expand_dims(tf.expand_dims(P[0, :, :], axis=0), axis=0)
d2l.show_heatmaps(P, xlabel='Column (encoding dimension)',
                  ylabel='Row (position)', figsize=(3.5, 4), cmap='Blues')
P = jnp.expand_dims(jnp.expand_dims(P[0, :, :], axis=0), axis=0)
d2l.show_heatmaps(P, xlabel='Column (encoding dimension)',
                  ylabel='Row (position)', figsize=(3.5, 4), cmap='Blues')

11.6.3.2 Santykinė pozicinė informacija

Be absoliučios pozicinės informacijos užfiksavimo, aukščiau pateiktas pozicinis kodavimas taip pat leidžia modeliui lengvai išmokti sutelkti dėmesį pagal santykines pozicijas. Taip yra todėl, kad bet kuriam fiksuotam pozicijos poslinkiui δ\delta pozicinis kodavimas pozicijoje i+δi + \delta gali būti pavaizduotas tiesine to paties kodavimo pozicijoje ii projekcija.

Šią projekciją galima paaiškinti matematiškai. Pažymėjus ωj=1/100002j/d\omega_j = 1/10000^{2j/d}, bet kuri pora (pi,2j,pi,2j+1)(p_{i, 2j}, p_{i, 2j+1}) (11.6.1) gali būti tiesiškai suprojektuota į (pi+δ,2j,pi+δ,2j+1)(p_{i+\delta, 2j}, p_{i+\delta, 2j+1}) bet kuriam fiksuotam poslinkiui δ\delta:

[cos⁡(δωj)sin⁡(δωj)−sin⁡(δωj)cos⁡(δωj)][pi,2jpi,2j+1]=[cos⁡(δωj)sin⁡(iωj)+sin⁡(δωj)cos⁡(iωj)−sin⁡(δωj)sin⁡(iωj)+cos⁡(δωj)cos⁡(iωj)]=[sin⁡((i+δ)ωj)cos⁡((i+δ)ωj)]=[pi+δ,2jpi+δ,2j+1],\begin{aligned} \begin{bmatrix} \cos(\delta \omega_j) & \sin(\delta \omega_j) \\ -\sin(\delta \omega_j) & \cos(\delta \omega_j) \\ \end{bmatrix} \begin{bmatrix} p_{i, 2j} \\ p_{i, 2j+1} \\ \end{bmatrix} =&\begin{bmatrix} \cos(\delta \omega_j) \sin(i \omega_j) + \sin(\delta \omega_j) \cos(i \omega_j) \\ -\sin(\delta \omega_j) \sin(i \omega_j) + \cos(\delta \omega_j) \cos(i \omega_j) \\ \end{bmatrix}\\ =&\begin{bmatrix} \sin\left((i+\delta) \omega_j\right) \\ \cos\left((i+\delta) \omega_j\right) \\ \end{bmatrix}\\ =& \begin{bmatrix} p_{i+\delta, 2j} \\ p_{i+\delta, 2j+1} \\ \end{bmatrix}, \end{aligned}

kur 2×22\times 2 projekcijos matrica nepriklauso nuo jokio pozicijos indekso ii.

11.6.4 Santrauka

Saviraiškėje dėmesio sutelkimo transformacijoje užklausos, raktai ir reikšmės visos ateina iš to paties šaltinio. Ir CNN, ir saviraiškė dėmesio sutelkimo transformacija leidžia lygiagretų skaičiavimą, o saviraiškė dėmesio sutelkimo transformacija turi trumpiausią didžiausią kelio ilgį. Tačiau kvadratinis skaičiavimo sudėtingumas sekos ilgio atžvilgiu daro saviraišką dėmesio sutelkimo transformaciją nepriimtinai lėtą labai ilgoms sekoms. Kad naudotume sekos tvarkos informaciją, galime įterpti absoliučią arba santykinę pozicinę informaciją, pridėdami pozicinį kodavimą prie įvesties vaizdų.

11.6.5 Pratimai

  1. Tarkime, kad projektuojame gilią architektūrą sekai pavaizduoti, sukraudami saviraiškės dėmesio sutelkimo transformacijos sluoksnius su poziciniu kodavimu. Kokios gali būti galimos problemos?
  2. Ar galite suprojektuoti išmokstamą pozicinio kodavimo metodą?
  3. Ar galime priskirti skirtingus išmoktus įterpinius pagal skirtingus poslinkius tarp užklausų ir raktų, lyginamų saviraiškėje dėmesio sutelkimo transformacijoje? Užuomina: galite remtis santykinių pozicijų įterpiniais (Shaw ir kt., 2018; Huang ir kt., 2018).

Diskusijos

Diskusijos

Diskusijos

Diskusijos