.. _sec_nadaraya-waston: *Pooling* de Atenção: Regressão de Kernel de Nadaraya-Watson ============================================================ Agora você conhece os principais componentes dos mecanismos de atenção sob a estrutura em :numref:`fig_qkv`. Para recapitular, as interações entre consultas (dicas volitivas) e chaves (dicas não volitivas) resultam em *concentração de atenção*. O *pooling* de atenção agrega valores seletivamente (entradas sensoriais) para produzir a saída. Nesta secção, vamos descrever o agrupamento de atenção em mais detalhes para lhe dar uma visão de alto nível como os mecanismos de atenção funcionam na prática. Especificamente, o modelo de regressão do kernel de Nadaraya-Watson proposto em 1964 é um exemplo simples, mas completo para demonstrar o aprendizado de máquina com mecanismos de atenção. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python from mxnet import autograd, gluon, np, npx from mxnet.gluon import nn from d2l import mxnet as d2l npx.set_np() .. raw:: html
.. raw:: html
.. code:: python import torch from torch import nn from d2l import torch as d2l .. raw:: html
.. raw:: html
Gerando o Dataset ----------------- Para manter as coisas simples, vamos considerar o seguinte problema de regressão: dado um conjunto de dados de pares de entrada-saída :math:`\{(x_1, y_1), \ldots, (x_n, y_n)\}`, como aprender :math:`f` para prever a saída :math:`\hat{y} = f(x)` para qualquer nova entrada :math:`x`? Aqui, geramos um conjunto de dados artificial de acordo com a seguinte função não linear com o termo de ruído :math:`\epsilon`: .. math:: y_i = 2\sin(x_i) + x_i^{0.8} + \epsilon, onde :math:`\epsilon` obedece a uma distribuição normal com média zero e desvio padrão 0,5. Ambos, 50 exemplos de treinamento e 50 exemplos de teste são gerados. Para visualizar melhor o padrão de atenção posteriormente, as entradas de treinamento são classificadas. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python n_train = 50 # No. of training examples x_train = np.sort(np.random.rand(n_train) * 5) # Training inputs def f(x): return 2 * np.sin(x) + x**0.8 y_train = f(x_train) + np.random.normal(0.0, 0.5, (n_train,)) # Training outputs x_test = np.arange(0, 5, 0.1) # Testing examples y_truth = f(x_test) # Ground-truth outputs for the testing examples n_test = len(x_test) # No. of testing examples n_test .. parsed-literal:: :class: output 50 .. raw:: html
.. raw:: html
.. code:: python n_train = 50 # No. of training examples x_train, _ = torch.sort(torch.rand(n_train) * 5) # Training inputs def f(x): return 2 * torch.sin(x) + x**0.8 y_train = f(x_train) + torch.normal(0.0, 0.5, (n_train,)) # Training outputs x_test = torch.arange(0, 5, 0.1) # Testing examples y_truth = f(x_test) # Ground-truth outputs for the testing examples n_test = len(x_test) # No. of testing examples n_test .. parsed-literal:: :class: output 50 .. raw:: html
.. raw:: html
A função a seguir plota todos os exemplos de treinamento (representados por círculos), a função de geração de dados de verdade básica ``f`` sem o termo de ruído (rotulado como “Truth”), e a função de predição aprendida (rotulado como “Pred”). .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python def plot_kernel_reg(y_hat): d2l.plot(x_test, [y_truth, y_hat], 'x', 'y', legend=['Truth', 'Pred'], xlim=[0, 5], ylim=[-1, 5]) d2l.plt.plot(x_train, y_train, 'o', alpha=0.5); .. raw:: html
.. raw:: html
.. code:: python def plot_kernel_reg(y_hat): d2l.plot(x_test, [y_truth, y_hat], 'x', 'y', legend=['Truth', 'Pred'], xlim=[0, 5], ylim=[-1, 5]) d2l.plt.plot(x_train, y_train, 'o', alpha=0.5); .. raw:: html
.. raw:: html
*Pooling* Médio --------------- Começamos com talvez o estimador “mais idiota” do mundo para este problema de regressão: usando o *pooling* médio para calcular a média de todos os resultados do treinamento: .. math:: f(x) = \frac{1}{n}\sum_{i=1}^n y_i, :label: eq_avg-pooling que é plotado abaixo. Como podemos ver, este estimador não é tão inteligente. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python y_hat = y_train.mean().repeat(n_test) plot_kernel_reg(y_hat) .. figure:: output_nadaraya-waston_736177_30_0.svg .. raw:: html
.. raw:: html
.. code:: python y_hat = torch.repeat_interleave(y_train.mean(), n_test) plot_kernel_reg(y_hat) .. figure:: output_nadaraya-waston_736177_33_0.svg .. raw:: html
.. raw:: html
*Pooling* de Atenção não-Paramétrico ------------------------------------ Obviamente, o agrupamento médio omite as entradas :math:`x_i`. Uma ideia melhor foi proposta por Nadaraya :cite:`Nadaraya.1964` e Waston :cite:`Watson.1964` para pesar as saídas :math:`y_i` de acordo com seus locais de entrada: .. math:: f(x) = \sum_{i=1}^n \frac{K(x - x_i)}{\sum_{j=1}^n K(x - x_j)} y_i, :label: eq_nadaraya-waston onde :math:`K` é um *kernel*. O estimador em :eq:`eq_nadaraya-waston` é chamado de *regressão do kernel Nadaraya-Watson*. Aqui não entraremos em detalhes sobre os grãos. Lembre-se da estrutura dos mecanismos de atenção em :numref:`fig_qkv`. Do ponto de vista da atenção, podemos reescrever :eq:`eq_nadaraya-waston` em uma forma mais generalizada de *concentração de atenção*: .. math:: f(x) = \sum_{i=1}^n \alpha(x, x_i) y_i, :label: eq_attn-pooling onde :math:`x` é a consulta e :math:`(x_i, y_i)` é o par de valores-chave. Comparando :eq:`eq_attn-pooling` e :eq:`eq_avg-pooling`, a atenção concentrada aqui é uma média ponderada de valores :math:`y_i`. O *peso de atenção* :math:`\alpha(x, x_i)` em :eq:`eq_attn-pooling` é atribuído ao valor correspondente :math:`y_i` baseado na interação entre a consulta :math:`x` e a chave :math:`x_i` modelado por :math:`\alpha`. Para qualquer consulta, seus pesos de atenção sobre todos os pares de valores-chave são uma distribuição de probabilidade válida: eles não são negativos e somam um. Para obter intuições de concentração de atenção, apenas considere um *kernel gaussiano* definido como .. math:: K(u) = \frac{1}{\sqrt{2\pi}} \exp(-\frac{u^2}{2}). Conectando o kernel gaussiano em :eq:`eq_attn-pooling` e :eq:`eq_nadaraya-waston` dá .. math:: \begin{aligned} f(x) &=\sum_{i=1}^n \alpha(x, x_i) y_i\\ &= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}(x - x_i)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}(x - x_j)^2\right)} y_i \\&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}(x - x_i)^2\right) y_i. \end{aligned} :label: eq_nadaraya-waston-gaussian In :eq:`eq_nadaraya-waston-gaussian`, uma chave :math:`x_i` que está mais próxima da consulta dada :math:`x` obterá *mais atenção* por meio de um *peso de atenção maior* atribuído ao valor correspondente da chave :math:`y_i`. Notavelmente, a regressão do kernel Nadaraya-Watson é um modelo não paramétrico; assim :eq:`eq_nadaraya-waston-gaussian` é um exemplo de *agrupamento de atenção não paramétrica*. A seguir, traçamos a previsão com base neste modelo de atenção não paramétrica. A linha prevista é suave e mais próxima da verdade fundamental do que a produzida pelo agrupamento médio. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python # Shape of `X_repeat`: (`n_test`, `n_train`), where each row contains the # same testing inputs (i.e., same queries) X_repeat = x_test.repeat(n_train).reshape((-1, n_train)) # Note that `x_train` contains the keys. Shape of `attention_weights`: # (`n_test`, `n_train`), where each row contains attention weights to be # assigned among the values (`y_train`) given each query attention_weights = npx.softmax(-(X_repeat - x_train)**2 / 2) # Each element of `y_hat` is weighted average of values, where weights are # attention weights y_hat = np.dot(attention_weights, y_train) plot_kernel_reg(y_hat) .. figure:: output_nadaraya-waston_736177_39_0.svg .. raw:: html
.. raw:: html
.. code:: python # Shape of `X_repeat`: (`n_test`, `n_train`), where each row contains the # same testing inputs (i.e., same queries) X_repeat = x_test.repeat_interleave(n_train).reshape((-1, n_train)) # Note that `x_train` contains the keys. Shape of `attention_weights`: # (`n_test`, `n_train`), where each row contains attention weights to be # assigned among the values (`y_train`) given each query attention_weights = nn.functional.softmax(-(X_repeat - x_train)**2 / 2, dim=1) # Each element of `y_hat` is weighted average of values, where weights are # attention weights y_hat = torch.matmul(attention_weights, y_train) plot_kernel_reg(y_hat) .. figure:: output_nadaraya-waston_736177_42_0.svg .. raw:: html
.. raw:: html
Agora, vamos dar uma olhada nos pesos de atenção. Aqui, as entradas de teste são consultas, enquanto as entradas de treinamento são essenciais. Uma vez que ambas as entradas são classificadas, podemos ver que quanto mais próximo o par de chave de consulta está, o maior peso de atenção está no *pooling* de atenção. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python d2l.show_heatmaps(np.expand_dims(np.expand_dims(attention_weights, 0), 0), xlabel='Sorted training inputs', ylabel='Sorted testing inputs') .. figure:: output_nadaraya-waston_736177_48_0.svg .. raw:: html
.. raw:: html
.. code:: python d2l.show_heatmaps(attention_weights.unsqueeze(0).unsqueeze(0), xlabel='Sorted training inputs', ylabel='Sorted testing inputs') .. figure:: output_nadaraya-waston_736177_51_0.svg .. raw:: html
.. raw:: html
*Pooling* de Atenção Paramétrica -------------------------------- A regressão de kernel não paramétrica de Nadaraya-Watson desfruta do benefício de *consistência*: com dados suficientes, esse modelo converge para a solução ótima. Não obstante, podemos facilmente integrar parâmetros aprendíveis no *pooling* de atenção. Por exemplo, um pouco diferente de :eq:`eq_nadaraya-waston-gaussian`, na sequência a distância entre a consulta :math:`x` e a chave :math:`x_i` é multiplicado por um parâmetro aprendível :math:`w`: .. math:: \begin{aligned}f(x) &= \sum_{i=1}^n \alpha(x, x_i) y_i \\&= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}((x - x_i)w)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}((x - x_i)w)^2\right)} y_i \\&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}((x - x_i)w)^2\right) y_i.\end{aligned} :label: eq_nadaraya-waston-gaussian-para No resto da seção, vamos treinar este modelo aprendendo o parâmetro de a concentração de atenção em :eq:`eq_nadaraya-waston-gaussian-para`. .. _subsec_batch_dot: Multiplicação de Matriz de Lote ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Para computar a atenção com mais eficiência para *minibatches*, podemos aproveitar os utilitários de multiplicação de matrizes em lote fornecidos por *frameworks* de *deep learning*. Suponha que o primeiro minibatch contém :math:`n` matrizes :math:`\mathbf{X}_1, \ldots, \mathbf{X}_n` de forma :math:`a\times b`, e o segundo minibatch contém :math:`n` matrizes :math:`\mathbf{Y}_1, \ldots, \mathbf{Y}_n` da forma :math:`b\times c`. Sua multiplicação da matriz de lote resulta em :math:`n` matrizes :math:`\mathbf{X}_1\mathbf{Y}_1, \ldots, \mathbf{X}_n\mathbf{Y}_n` da forma :math:`a\times c`. Portanto, dados dois tensores de forma (:math:`n`, :math:`a`, :math:`b`) e (:math:`n`, :math:`b`, :math:`c`), a forma de sua saída de multiplicação da matriz em lote é (:math:`n`, :math:`a`, :math:`c`). .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python X = np.ones((2, 1, 4)) Y = np.ones((2, 4, 6)) npx.batch_dot(X, Y).shape .. parsed-literal:: :class: output (2, 1, 6) .. raw:: html
.. raw:: html
.. code:: python X = torch.ones((2, 1, 4)) Y = torch.ones((2, 4, 6)) torch.bmm(X, Y).shape .. parsed-literal:: :class: output torch.Size([2, 1, 6]) .. raw:: html
.. raw:: html
No contexto dos mecanismos de atenção, podemos usar a multiplicação da matriz de minibatch para calcular médias ponderadas de valores em um minibatch. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python weights = np.ones((2, 10)) * 0.1 values = np.arange(20).reshape((2, 10)) npx.batch_dot(np.expand_dims(weights, 1), np.expand_dims(values, -1)) .. parsed-literal:: :class: output array([[[ 4.5]], [[14.5]]]) .. raw:: html
.. raw:: html
.. code:: python weights = torch.ones((2, 10)) * 0.1 values = torch.arange(20.0).reshape((2, 10)) torch.bmm(weights.unsqueeze(1), values.unsqueeze(-1)) .. parsed-literal:: :class: output tensor([[[ 4.5000]], [[14.5000]]]) .. raw:: html
.. raw:: html
Definindo o Modelo ~~~~~~~~~~~~~~~~~~ Usando a multiplicação de matriz de minibatch, abaixo nós definimos a versão paramétrica da regressão do kernel Nadaraya-Watson com base no agrupamento de atenção paramétrica em :eq:`eq_nadaraya-waston-gaussian-para`. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python class NWKernelRegression(nn.Block): def __init__(self, **kwargs): super().__init__(**kwargs) self.w = self.params.get('w', shape=(1,)) def forward(self, queries, keys, values): # Shape of the output `queries` and `attention_weights`: # (no. of queries, no. of key-value pairs) queries = queries.repeat(keys.shape[1]).reshape((-1, keys.shape[1])) self.attention_weights = npx.softmax( -((queries - keys) * self.w.data())**2 / 2) # Shape of `values`: (no. of queries, no. of key-value pairs) return npx.batch_dot(np.expand_dims(self.attention_weights, 1), np.expand_dims(values, -1)).reshape(-1) .. raw:: html
.. raw:: html
.. code:: python class NWKernelRegression(nn.Module): def __init__(self, **kwargs): super().__init__(**kwargs) self.w = nn.Parameter(torch.rand((1,), requires_grad=True)) def forward(self, queries, keys, values): # Shape of the output `queries` and `attention_weights`: # (no. of queries, no. of key-value pairs) queries = queries.repeat_interleave(keys.shape[1]).reshape((-1, keys.shape[1])) self.attention_weights = nn.functional.softmax( -((queries - keys) * self.w)**2 / 2, dim=1) # Shape of `values`: (no. of queries, no. of key-value pairs) return torch.bmm(self.attention_weights.unsqueeze(1), values.unsqueeze(-1)).reshape(-1) .. raw:: html
.. raw:: html
Treinamento ~~~~~~~~~~~ A seguir, transformamos o conjunto de dados de treinamento às chaves e valores para treinar o modelo de atenção. No agrupamento paramétrico de atenção, qualquer entrada de treinamento pega pares de valores-chave de todos os exemplos de treinamento, exceto ela mesma, para prever sua saída. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python # Shape of `X_tile`: (`n_train`, `n_train`), where each column contains the # same training inputs X_tile = np.tile(x_train, (n_train, 1)) # Shape of `Y_tile`: (`n_train`, `n_train`), where each column contains the # same training outputs Y_tile = np.tile(y_train, (n_train, 1)) # Shape of `keys`: ('n_train', 'n_train' - 1) keys = X_tile[(1 - np.eye(n_train)).astype('bool')].reshape((n_train, -1)) # Shape of `values`: ('n_train', 'n_train' - 1) values = Y_tile[(1 - np.eye(n_train)).astype('bool')].reshape((n_train, -1)) .. raw:: html
.. raw:: html
.. code:: python # Shape of `X_tile`: (`n_train`, `n_train`), where each column contains the # same training inputs X_tile = x_train.repeat((n_train, 1)) # Shape of `Y_tile`: (`n_train`, `n_train`), where each column contains the # same training outputs Y_tile = y_train.repeat((n_train, 1)) # Shape of `keys`: ('n_train', 'n_train' - 1) keys = X_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1)) # Shape of `values`: ('n_train', 'n_train' - 1) values = Y_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1)) .. raw:: html
.. raw:: html
Usando a perda quadrada e a descida do gradiente estocástico, treinamos o modelo paramétrico de atenção. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python net = NWKernelRegression() net.initialize() loss = gluon.loss.L2Loss() trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.5}) animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5]) for epoch in range(5): with autograd.record(): l = loss(net(x_train, keys, values), y_train) l.backward() trainer.step(1) print(f'epoch {epoch + 1}, loss {float(l.sum()):.6f}') animator.add(epoch + 1, float(l.sum())) .. figure:: output_nadaraya-waston_736177_93_0.svg .. raw:: html
.. raw:: html
.. code:: python net = NWKernelRegression() loss = nn.MSELoss(reduction='none') trainer = torch.optim.SGD(net.parameters(), lr=0.5) animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5]) for epoch in range(5): trainer.zero_grad() # Note: L2 Loss = 1/2 * MSE Loss. PyTorch has MSE Loss which is slightly # different from MXNet's L2Loss by a factor of 2. Hence we halve the loss l = loss(net(x_train, keys, values), y_train) / 2 l.sum().backward() trainer.step() print(f'epoch {epoch + 1}, loss {float(l.sum()):.6f}') animator.add(epoch + 1, float(l.sum())) .. figure:: output_nadaraya-waston_736177_96_0.svg .. raw:: html
.. raw:: html
Depois de treinar o modelo paramétrico de atenção, podemos traçar sua previsão. Tentando ajustar o conjunto de dados de treinamento com ruído, a linha prevista é menos suave do que sua contraparte não paramétrica que foi traçada anteriormente. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python # Shape of `keys`: (`n_test`, `n_train`), where each column contains the same # training inputs (i.e., same keys) keys = np.tile(x_train, (n_test, 1)) # Shape of `value`: (`n_test`, `n_train`) values = np.tile(y_train, (n_test, 1)) y_hat = net(x_test, keys, values) plot_kernel_reg(y_hat) .. figure:: output_nadaraya-waston_736177_102_0.svg .. raw:: html
.. raw:: html
.. code:: python # Shape of `keys`: (`n_test`, `n_train`), where each column contains the same # training inputs (i.e., same keys) keys = x_train.repeat((n_test, 1)) # Shape of `value`: (`n_test`, `n_train`) values = y_train.repeat((n_test, 1)) y_hat = net(x_test, keys, values).unsqueeze(1).detach() plot_kernel_reg(y_hat) .. figure:: output_nadaraya-waston_736177_105_0.svg .. raw:: html
.. raw:: html
Comparando com o *pooling* de atenção não paramétrico, a região com grandes pesos de atenção torna-se mais nítida na configuração programável e paramétrica. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python d2l.show_heatmaps(np.expand_dims(np.expand_dims(net.attention_weights, 0), 0), xlabel='Sorted training inputs', ylabel='Sorted testing inputs') .. figure:: output_nadaraya-waston_736177_111_0.svg .. raw:: html
.. raw:: html
.. code:: python d2l.show_heatmaps(net.attention_weights.unsqueeze(0).unsqueeze(0), xlabel='Sorted training inputs', ylabel='Sorted testing inputs') .. figure:: output_nadaraya-waston_736177_114_0.svg .. raw:: html
.. raw:: html
Resumo ------ - A regressão do kernel Nadaraya-Watson é um exemplo de *machine learning* com mecanismos de atenção. - O agrupamento de atenção da regressão do kernel Nadaraya-Watson é uma média ponderada dos resultados do treinamento. Do ponto de vista da atenção, o peso da atenção é atribuído a um valor com base em uma função de uma consulta e a chave que está emparelhada com o valor. - O *pooling* de atenção pode ser não paramétrico ou paramétrico. Exercícios ---------- 1. Aumente o número de exemplos de treinamento. Você pode aprender melhor a regressão de kernel não paramétrica Nadaraya-Watson? 2. Qual é o valor de nosso :math:`w` aprendido no experimento paramétrico de concentração de atenção? Por que torna a região ponderada mais nítida ao visualizar os pesos de atenção? 3. Como podemos adicionar hiperparâmetros à regressão de kernel Nadaraya-Watson não paramétrica para prever melhor? 4. Projete outro agrupamento de atenção paramétrica para a regressão do kernel desta seção. Treine este novo modelo e visualize seus pesos de atenção. .. raw:: html
mxnetpytorch
.. raw:: html
`Discussions `__ .. raw:: html
.. raw:: html
`Discussions `__ .. raw:: html
.. raw:: html
.. raw:: html