.. _sec_self-attention-and-positional-encoding: Autoatenção e Codificação Posicional ==================================== No aprendizado profundo, costumamos usar CNNs ou RNNs para codificar uma sequência. Agora, com os mecanismos de atenção, imagine que alimentamos uma sequência de tokens no *pooling* de atenção para que o mesmo conjunto de tokens atue como consultas, chaves e valores. Especificamente, cada consulta atende a todos os pares de valores-chave e gera uma saída de atenção. Como as consultas, chaves e valores vêm do mesmo lugar, isso executa *autoatenção* :cite:`Lin.Feng.Santos.ea.2017,Vaswani.Shazeer.Parmar.ea.2017`, que também é chamado *intra-atenção* :cite:`Cheng.Dong.Lapata.2016,Parikh.Tackstrom.Das.ea.2016,Paulus.Xiong.Socher.2017`. Nesta seção, discutiremos a codificação de sequência usando autoatenção, incluindo o uso de informações adicionais para a ordem da sequência. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python import math from mxnet import autograd, np, npx from mxnet.gluon import nn from d2l import mxnet as d2l npx.set_np() .. raw:: html
.. raw:: html
.. code:: python import math import torch from torch import nn from d2l import torch as d2l .. raw:: html
.. raw:: html
Autoatenção ----------- Dada uma sequência de tokens de entrada :math:`\mathbf{x}_1, \ldots, \mathbf{x}_n` onde qualquer :math:`\mathbf{x}_i \in \mathbb{R}^d` (:math:`1 \leq i \leq n`), suas saídas de autoatenção é uma sequência do mesmo comprimento :math:`\mathbf{y}_1, \ldots, \mathbf{y}_n`, Onde .. math:: \mathbf{y}_i = f(\mathbf{x}_i, (\mathbf{x}_1, \mathbf{x}_1), \ldots, (\mathbf{x}_n, \mathbf{x}_n)) \in \mathbb{R}^d de acordo com a definição de concentração de :math:`f` em :eq:`eq_attn-pooling`. Usando a atenção de várias cabeças, o seguinte trecho de código calcula a autoatenção de um tensor com forma (tamanho do lote, número de etapas de tempo ou comprimento da sequência em tokens, :math:`d`). O tensor de saída tem o mesmo formato. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python num_hiddens, num_heads = 100, 5 attention = d2l.MultiHeadAttention(num_hiddens, num_heads, 0.5) attention.initialize() batch_size, num_queries, valid_lens = 2, 4, np.array([3, 2]) X = np.ones((batch_size, num_queries, num_hiddens)) attention(X, X, X, valid_lens).shape .. parsed-literal:: :class: output (2, 4, 100) .. raw:: html
.. raw:: html
.. code:: python num_hiddens, num_heads = 100, 5 attention = d2l.MultiHeadAttention(num_hiddens, num_hiddens, num_hiddens, num_hiddens, num_heads, 0.5) attention.eval() .. parsed-literal:: :class: output MultiHeadAttention( (attention): DotProductAttention( (dropout): Dropout(p=0.5, inplace=False) ) (W_q): Linear(in_features=100, out_features=100, bias=False) (W_k): Linear(in_features=100, out_features=100, bias=False) (W_v): Linear(in_features=100, out_features=100, bias=False) (W_o): Linear(in_features=100, out_features=100, bias=False) ) .. code:: python batch_size, num_queries, valid_lens = 2, 4, torch.tensor([3, 2]) X = torch.ones((batch_size, num_queries, num_hiddens)) attention(X, X, X, valid_lens).shape .. parsed-literal:: :class: output torch.Size([2, 4, 100]) .. raw:: html
.. raw:: html
.. _subsec_cnn-rnn-self-attention: Comparando CNNs, RNNs e Autoatenção ----------------------------------- Vamos comparar arquiteturas para mapear uma sequência de :math:`n` tokens para outra sequência de igual comprimento, onde cada token de entrada ou saída é representado por um vetor :math:`d`-dimensional. Especificamente, consideraremos CNNs, RNNs e autoatenção. Compararemos sua complexidade computacional, operações sequenciais e comprimentos máximos de caminho. Observe que as operações sequenciais evitam a computação paralela, enquanto um caminho mais curto entre qualquer combinação de posições de sequência torna mais fácil aprender dependências de longo alcance dentro da sequência :cite:`Hochreiter.Bengio.Frasconi.ea.2001`. .. _fig_cnn-rnn-self-attention: .. figure:: ../img/cnn-rnn-self-attention.svg Comparando CNN (tokens de preenchimento são omitidos), RNN e arquiteturas de autoatenção. Considere uma camada convolucional cujo tamanho do kernel é :math:`k`. Forneceremos mais detalhes sobre o processamento de sequência usando CNNs em capítulos posteriores. Por enquanto, só precisamos saber que, como o comprimento da sequência é :math:`n`, os números de canais de entrada e saída são :math:`d`, a complexidade computacional da camada convolucional é :math:`\mathcal{O}(knd^2)`. Como mostra :numref:`fig_cnn-rnn-self-attention`, CNNs são hierárquicas, então existem :math:`\mathcal{O}(1)` operações sequenciais e o comprimento máximo do caminho é :math:`\mathcal{O}(n/k)`. Por exemplo, :math:`\mathbf{x}_1` e $:raw-latex:`\mathbf{x}`\_5 estão dentro do campo receptivo de um CNN de duas camadas com tamanho de kernel 3 em :numref:`fig_cnn-rnn-self-attention`. Ao atualizar o estado oculto de RNNs, multiplicação da matriz de pesos :math:`d \times d` e o estado oculto :math:`d`-dimensional tem uma complexidade computacional de :math:`\mathcal{O}(d^2)`. Uma vez que o comprimento da sequência é :math:`n`, a complexidade computacional da camada recorrente é :math:`\mathcal{O}(nd^2)`. De acordo com :numref:`fig_cnn-rnn-self-attention`, existem :math:`\mathcal{O}(n)` operações sequenciais que não pode ser paralelizadas e o comprimento máximo do caminho também é :math:`\mathcal{O}(n)`. Na autoatenção, as consultas, chaves e valores são todas matrizes :math:`n \times d` Considere a atenção do produto escalonado em :eq:`eq_softmax_QK_V`, onde uma matriz :math:`n \times d` é multiplicada por uma matriz :math:`d \times n`, então a matriz de saída :math:`n \times n` é multiplicada por uma matriz :math:`n \times d`. Como resultado, a autoatenção tem uma complexidade computacional :math:`\mathcal{O}(n^2d)` Como podemos ver em :numref:`fig_cnn-rnn-self-attention`, cada token está diretamente conectado a qualquer outro token via auto-atenção. Portanto, a computação pode ser paralela com :math:`\mathcal{O}(1)` operações sequenciais e o comprimento máximo do caminho também é :math:`\mathcal{O}(1)`. Contudo, tanto CNNs quanto autoatenção desfrutam de computação paralela e a autoatenção tem o menor comprimento de caminho máximo. No entanto, a complexidade computacional quadrática em relação ao comprimento da sequência torna a auto-atenção proibitivamente lenta em sequências muito longas. .. _subsec_positional-encoding: Codificação Posicional ---------------------- Ao contrário dos RNNs que processam recorrentemente tokens de uma sequência, um por um, a autoatenção desvia as operações sequenciais em favor de computação paralela. Para usar as informações de ordem de sequência, podemos injetar informações posicionais absolutas ou relativas adicionando *codificação posicional* às representações de entrada. Codificações posicionais podem ser aprendidas ou corrigidas. A seguir, descrevemos uma codificação posicional fixa baseada nas funções seno e cosseno :cite:`Vaswani.Shazeer.Parmar.ea.2017`. Suponha que a representação de entrada :math:`\mathbf{X} \in \mathbb{R}^{n \times d}` contém as características :math:`d`-dimensionais embutidas para :math:`n` tokens de uma sequência. A codificação posicional gera :math:`\mathbf{X} + \mathbf{P}` usando uma matriz de *embedding* posicional :math:`\mathbf{P} \in \mathbb{R}^{n \times d}` da mesma forma, cujo elemento na linha :math:`i^\mathrm{th}` row and the :math:`(2j)^\mathrm{th}` ou a coluna :math:`(2j + 1)^\mathrm{th}` é .. math:: \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} :label: eq_positional-encoding-def À primeira vista, esse design de função trigonométrica parece estranho. Antes das explicações deste design, vamos primeiro implementá-lo na seguinte classe ``PositionalEncoding``. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python #@save class PositionalEncoding(nn.Block): def __init__(self, num_hiddens, dropout, max_len=1000): super(PositionalEncoding, self).__init__() self.dropout = nn.Dropout(dropout) # Create a long enough `P` self.P = np.zeros((1, max_len, num_hiddens)) X = np.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) .. raw:: html
.. raw:: html
.. code:: python #@save class PositionalEncoding(nn.Module): def __init__(self, num_hiddens, dropout, max_len=1000): super(PositionalEncoding, self).__init__() self.dropout = nn.Dropout(dropout) # Create a long enough `P` self.P = torch.zeros((1, max_len, num_hiddens)) X = torch.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) .. raw:: html
.. raw:: html
Na matriz de incorporação posicional :math:`\mathbf{P}`, as linhas correspondem às posições dentro de uma sequência e as colunas representam diferentes dimensões de codificação posicional. No exemplo abaixo, podemos ver que as colunas :math:`6^{\mathrm{th}}` e :math:`7^{\mathrm{th}}` da matriz de embedding posicional têm uma frequência maior do que :math:`8^{\mathrm{th}}` e as colunas :math:`9^{\mathrm{th}}`. O deslocamento entre o :math:`6^{\mathrm{th}}` e o :math:`7^{\mathrm{th}}` (o mesmo para as colunas :math:`8^{\mathrm{th}}` e :math:`9^{\mathrm{th}}`) é devido à alternância das funções seno e cosseno. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python 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(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)]) .. figure:: output_self-attention-and-positional-encoding_d76d5a_31_0.svg .. raw:: html
.. raw:: html
.. code:: python encoding_dim, num_steps = 32, 60 pos_encoding = PositionalEncoding(encoding_dim, 0) pos_encoding.eval() X = pos_encoding(torch.zeros((1, num_steps, encoding_dim))) P = pos_encoding.P[:, :X.shape[1], :] d2l.plot(torch.arange(num_steps), P[0, :, 6:10].T, xlabel='Row (position)', figsize=(6, 2.5), legend=["Col %d" % d for d in torch.arange(6, 10)]) .. figure:: output_self-attention-and-positional-encoding_d76d5a_34_0.svg .. raw:: html
.. raw:: html
Informação Posicional Absoluta ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Para ver como a frequência monotonicamente diminuída ao longo da dimensão de codificação se relaciona com a informação posicional absoluta, vamos imprimir as representações binárias de :math:`0, 1, \ldots, 7`. Como podemos ver, o bit mais baixo, o segundo bit mais baixo e o terceiro bit mais baixo se alternam em cada número, a cada dois números e a cada quatro números, respectivamente. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python for i in range(8): print(f'{i} in binary is {i:>03b}') .. parsed-literal:: :class: output 0 in binary is 000 1 in binary is 001 2 in binary is 010 3 in binary is 011 4 in binary is 100 5 in binary is 101 6 in binary is 110 7 in binary is 111 .. raw:: html
.. raw:: html
.. code:: python for i in range(8): print(f'{i} in binary is {i:>03b}') .. parsed-literal:: :class: output 0 in binary is 000 1 in binary is 001 2 in binary is 010 3 in binary is 011 4 in binary is 100 5 in binary is 101 6 in binary is 110 7 in binary is 111 .. raw:: html
.. raw:: html
Em representações binárias, um bit mais alto tem uma frequência mais baixa do que um bit mais baixo. Da mesma forma, conforme demonstrado no mapa de calor abaixo, a codificação posicional diminui as frequências ao longo da dimensão de codificação usando funções trigonométricas. Uma vez que as saídas são números flutuantes, tais as representações são mais eficientes em termos de espaço do que as representações binárias. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python 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') .. figure:: output_self-attention-and-positional-encoding_d76d5a_49_0.svg .. raw:: html
.. raw:: html
.. code:: python P = P[0, :, :].unsqueeze(0).unsqueeze(0) d2l.show_heatmaps(P, xlabel='Column (encoding dimension)', ylabel='Row (position)', figsize=(3.5, 4), cmap='Blues') .. figure:: output_self-attention-and-positional-encoding_d76d5a_52_0.svg .. raw:: html
.. raw:: html
Informação Posicional Relativa ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Além de capturar informações posicionais absolutas, a codificação posicional acima também permite que um modelo aprenda facilmente a atender por posições relativas. Isso ocorre porque para qualquer deslocamento de posição fixa :math:`\delta`, a codificação posicional na posição :math:`i + \delta` pode ser representada por uma projeção linear daquela na posição :math:`i`. Essa projeção pode ser explicada matematicamente. Denotando :math:`\omega_j = 1/10000^{2j/d}`, qualquer par de :math:`(p_{i, 2j}, p_{i, 2j+1})` em :eq:`eq_positional-encoding-def` pode ser linearmente projetado para :math:`(p_{i+\delta, 2j}, p_{i+\delta, 2j+1})` para qualquer deslocamento fixo :math:`\delta`: .. math:: \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} onde a matriz de projeção :math:`2\times 2` não depende de nenhum índice de posição :math:`i`. Resumo ------ - Na atenção própria, as consultas, chaves e valores vêm todos do mesmo lugar. - Tanto as CNNs quanto a autoatenção desfrutam de computação paralela e a autoatenção tem o menor comprimento de caminho máximo. No entanto, a complexidade computacional quadrática em relação ao comprimento da sequência torna a autoatenção proibitivamente lenta para sequências muito longas. - Para usar as informações de ordem de sequência, podemos injetar informações posicionais absolutas ou relativas adicionando codificação posicional às representações de entrada. Exercícios ---------- 1. Suponha que projetemos uma arquitetura profunda para representar uma sequência, empilhando camadas de autoatenção com codificação posicional. Quais podem ser os problemas? 2. Você pode projetar um método de codificação posicional que possa ser aprendido? .. raw:: html
mxnetpytorch
.. raw:: html
`Discussions `__ .. raw:: html
.. raw:: html
`Discussions `__ .. raw:: html
.. raw:: html
.. raw:: html