.. _sec_natural-language-inference-attention:
Inferência de Linguagem Natural: Usando a Atenção
=================================================
Introduzimos a tarefa de inferência em linguagem natural e o conjunto de
dados SNLI em :numref:`sec_natural-language-inference-and-dataset`. Em
vista de muitos modelos baseados em arquiteturas complexas e profundas,
Parikh et al. proposto para abordar a inferência de linguagem natural
com mecanismos de atenção e chamou-o de “modelo de atenção decomposto”
:cite:`Parikh.Tackstrom.Das.ea.2016`. Isso resulta em um modelo sem
camadas recorrentes ou convolucionais, alcançando o melhor resultado no
momento no conjunto de dados SNLI com muito menos parâmetros. Nesta
seção, iremos descrever e implementar este método baseado em atenção
(com MLPs) para inferência de linguagem natural, conforme descrito em
:numref:`fig_nlp-map-nli -ention`.
.. _fig_nlp-map-nli-attention:
.. figure:: ../img/nlp-map-nli-attention.svg
Esta seção alimenta o GloVe pré-treinado para uma arquitetura baseada
em atenção e MLPs para inferência de linguagem natural.
O Modelo
--------
Mais simples do que preservar a ordem das palavras em premissas e
hipóteses, podemos apenas alinhar as palavras em uma sequência de texto
com todas as palavras na outra e vice-versa, em seguida, compare e
agregue essas informações para prever as relações lógicas entre
premissas e hipóteses. Semelhante ao alinhamento de palavras entre as
frases fonte e alvo na tradução automática, o alinhamento de palavras
entre premissas e hipóteses pode ser perfeitamente realizado por
mecanismos de atenção.
.. _fig_nli_attention:
.. figure:: ../img/nli-attention.svg
Inferência de linguagem natural usando mecanismos de atenção.
:numref:`fig_nli_attention` descreve o método de inferência de
linguagem natural usando mecanismos de atenção. Em um nível superior,
consiste em três etapas treinadas em conjunto: alinhar, comparar e
agregar. Iremos ilustrá-los passo a passo a seguir.
.. raw:: html
.. raw:: html
.. code:: python
from mxnet import gluon, init, 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 torch.nn import functional as F
from d2l import torch as d2l
.. raw:: html
.. raw:: html
Alinhar
~~~~~~~
A primeira etapa é alinhar as palavras em uma sequência de texto a cada
palavra na outra sequência. Suponha que a premissa seja “preciso dormir”
e a hipótese “estou cansado”. Devido à semelhança semântica, podemos
desejar alinhar “i” na hipótese com “i” na premissa, e alinhe “cansado”
na hipótese com “sono” na premissa. Da mesma forma, podemos desejar
alinhar “i” na premissa com “i” na hipótese, e alinhar “necessidade” e
“sono” na premissa com “cansado” na hipótese. Observe que esse
alinhamento é *suave* usando a média ponderada, onde, idealmente,
grandes pesos estão associados às palavras a serem alinhadas. Para
facilitar a demonstração, :numref:`fig_nli_attention` mostra tal
alinhamento de uma maneira *dura*.
Agora descrevemos o alinhamento suave usando mecanismos de atenção com
mais detalhes. Denotamos por
:math:`\mathbf{A} = (\mathbf{a}_1, \ldots, \mathbf{a}_m)` e
:math:`\mathbf{B} = (\mathbf{b}_1, \ldots, \mathbf{b}_n)` a premissa e
hipótese, cujo número de palavras são :math:`m` e :math:`n`,
respectivamente, onde
:math:`\mathbf{a}_i, \mathbf{b}_j \in \mathbb{R}^{d}`
(:math:`i = 1, \ldots, m, j = 1, \ldots, n`) é um vetor de incorporação
de palavras :math:`d`-dimensional. Para o alinhamento suave, calculamos
os pesos de atenção :math:`e_{ij} \in \mathbb{R}` como
.. math:: e_{ij} = f(\mathbf{a}_i)^\top f(\mathbf{b}_j),
:label: eq_nli_e
onde a função :math:`f` é um MLP definido na seguinte função ``mlp``. A
dimensão de saída de :math:`f` é especificada pelo argumento
``num_hiddens`` de\ ``mlp``.
.. raw:: html
.. raw:: html
.. code:: python
def mlp(num_hiddens, flatten):
net = nn.Sequential()
net.add(nn.Dropout(0.2))
net.add(nn.Dense(num_hiddens, activation='relu', flatten=flatten))
net.add(nn.Dropout(0.2))
net.add(nn.Dense(num_hiddens, activation='relu', flatten=flatten))
return net
.. raw:: html
.. raw:: html
.. code:: python
def mlp(num_inputs, num_hiddens, flatten):
net = []
net.append(nn.Dropout(0.2))
net.append(nn.Linear(num_inputs, num_hiddens))
net.append(nn.ReLU())
if flatten:
net.append(nn.Flatten(start_dim=1))
net.append(nn.Dropout(0.2))
net.append(nn.Linear(num_hiddens, num_hiddens))
net.append(nn.ReLU())
if flatten:
net.append(nn.Flatten(start_dim=1))
return nn.Sequential(*net)
.. raw:: html
.. raw:: html
Deve-se destacar que, em :eq:`eq_nli_e` :math:`f` pega as entradas
:math:`\mathbf{a}_i` and :math:`\mathbf{b}_j` separadamente em vez de
pegar um par delas juntas como entrada. Este truque de *decomposição*
leva a apenas aplicações :math:`m + n` (complexidade linear) de
:math:`f` em vez de :math:`mn` aplicativos (complexidade quadrática).
Normalizando os pesos de atenção em :eq:`eq_nli_e`, calculamos a
média ponderada de todas as palavras incluídas na hipótese para obter a
representação da hipótese que está suavemente alinhada com a palavra
indexada por :math:`i` na premissa:
.. math::
\boldsymbol{\beta}_i = \sum_{j=1}^{n}\frac{\exp(e_{ij})}{ \sum_{k=1}^{n} \exp(e_{ik})} \mathbf{b}_j.
Da mesma forma, calculamos o alinhamento suave de palavras da premissa
para cada palavra indexada por :math:`j` na hipótese:
.. math::
\boldsymbol{\alpha}_j = \sum_{i=1}^{m}\frac{\exp(e_{ij})}{ \sum_{k=1}^{m} \exp(e_{kj})} \mathbf{a}_i.
Abaixo, definimos a classe ``Attend`` para calcular o alinhamento suave
das hipóteses (``beta``) com as premissas de entrada ``A`` e o
alinhamento suave das premissas (``alfa``) com as hipóteses de entrada
``B``.
.. raw:: html
.. raw:: html
.. code:: python
class Attend(nn.Block):
def __init__(self, num_hiddens, **kwargs):
super(Attend, self).__init__(**kwargs)
self.f = mlp(num_hiddens=num_hiddens, flatten=False)
def forward(self, A, B):
# Shape of `A`/`B`: (b`atch_size`, no. of words in sequence A/B,
# `embed_size`)
# Shape of `f_A`/`f_B`: (`batch_size`, no. of words in sequence A/B,
# `num_hiddens`)
f_A = self.f(A)
f_B = self.f(B)
# Shape of `e`: (`batch_size`, no. of words in sequence A,
# no. of words in sequence B)
e = npx.batch_dot(f_A, f_B, transpose_b=True)
# Shape of `beta`: (`batch_size`, no. of words in sequence A,
# `embed_size`), where sequence B is softly aligned with each word
# (axis 1 of `beta`) in sequence A
beta = npx.batch_dot(npx.softmax(e), B)
# Shape of `alpha`: (`batch_size`, no. of words in sequence B,
# `embed_size`), where sequence A is softly aligned with each word
# (axis 1 of `alpha`) in sequence B
alpha = npx.batch_dot(npx.softmax(e.transpose(0, 2, 1)), A)
return beta, alpha
.. raw:: html
.. raw:: html
.. code:: python
class Attend(nn.Module):
def __init__(self, num_inputs, num_hiddens, **kwargs):
super(Attend, self).__init__(**kwargs)
self.f = mlp(num_inputs, num_hiddens, flatten=False)
def forward(self, A, B):
# Shape of `A`/`B`: (`batch_size`, no. of words in sequence A/B,
# `embed_size`)
# Shape of `f_A`/`f_B`: (`batch_size`, no. of words in sequence A/B,
# `num_hiddens`)
f_A = self.f(A)
f_B = self.f(B)
# Shape of `e`: (`batch_size`, no. of words in sequence A,
# no. of words in sequence B)
e = torch.bmm(f_A, f_B.permute(0, 2, 1))
# Shape of `beta`: (`batch_size`, no. of words in sequence A,
# `embed_size`), where sequence B is softly aligned with each word
# (axis 1 of `beta`) in sequence A
beta = torch.bmm(F.softmax(e, dim=-1), B)
# Shape of `alpha`: (`batch_size`, no. of words in sequence B,
# `embed_size`), where sequence A is softly aligned with each word
# (axis 1 of `alpha`) in sequence B
alpha = torch.bmm(F.softmax(e.permute(0, 2, 1), dim=-1), A)
return beta, alpha
.. raw:: html
.. raw:: html
Comparando
~~~~~~~~~~
Na próxima etapa, comparamos uma palavra em uma sequência com a outra
sequência que está suavemente alinhada com essa palavra. Observe que no
alinhamento suave, todas as palavras de uma sequência, embora
provavelmente com pesos de atenção diferentes, serão comparadas com uma
palavra na outra sequência. Para facilitar a demonstração,
:numref:`fig_nli_attention` emparelha palavras com palavras alinhadas
de uma forma *dura*. Por exemplo, suponha que a etapa de atendimento
determina que “necessidade” e “sono” na premissa estão ambos alinhados
com “cansado” na hipótese, o par “cansado - preciso dormir” será
comparado.
Na etapa de comparação, alimentamos a concatenação (operador
:math:`[\cdot, \cdot]`) de palavras de uma sequência e palavras
alinhadas de outra sequência em uma função :math:`g` (um MLP):
.. math:: \mathbf{v}_{A,i} = g([\mathbf{a}_i, \boldsymbol{\beta}_i]), i = 1, \ldots, m\\ \mathbf{v}_{B,j} = g([\mathbf{b}_j, \boldsymbol{\alpha}_j]), j = 1, \ldots, n.
:label: eq_nli_v_ab
Em:eqref:\ ``eq_nli_v_ab``, :math:`\mathbf{v}_{A,i}` é a comparação
entre a palavra :math:`i` na premissa e todas as palavras da hipótese
que estão suavemente alinhadas com a palavra :math:`i`; enquanto
:math:`\mathbf{v}_{B,j}` é a comparação entre a palavra :math:`j` na
hipótese e todas as palavras da premissa que estão suavemente alinhadas
com a palavra :math:`j`. A seguinte classe ``Compare`` define como a
etapa de comparação.
.. raw:: html
.. raw:: html
.. code:: python
class Compare(nn.Block):
def __init__(self, num_hiddens, **kwargs):
super(Compare, self).__init__(**kwargs)
self.g = mlp(num_hiddens=num_hiddens, flatten=False)
def forward(self, A, B, beta, alpha):
V_A = self.g(np.concatenate([A, beta], axis=2))
V_B = self.g(np.concatenate([B, alpha], axis=2))
return V_A, V_B
.. raw:: html
.. raw:: html
.. code:: python
class Compare(nn.Module):
def __init__(self, num_inputs, num_hiddens, **kwargs):
super(Compare, self).__init__(**kwargs)
self.g = mlp(num_inputs, num_hiddens, flatten=False)
def forward(self, A, B, beta, alpha):
V_A = self.g(torch.cat([A, beta], dim=2))
V_B = self.g(torch.cat([B, alpha], dim=2))
return V_A, V_B
.. raw:: html
.. raw:: html
Agregando
~~~~~~~~~
Com dois conjuntos de vetores de comparação :math:`\mathbf{v}_{A,i}`
(:math:`i = 1, \ldots, m`) e :math:`\mathbf{v}_{B,j}`
(:math:`j = 1, \ldots, n`) disponível, na última etapa, agregaremos
essas informações para inferir a relação lógica. Começamos resumindo os
dois conjuntos:
.. math::
\mathbf{v}_A = \sum_{i=1}^{m} \mathbf{v}_{A,i}, \quad \mathbf{v}_B = \sum_{j=1}^{n}\mathbf{v}_{B,j}.
Em seguida, alimentamos a concatenação de ambos os resultados do resumo
na função :math:`h` (um MLP) para obter o resultado da classificação do
relacionamento lógico:
.. math::
\hat{\mathbf{y}} = h([\mathbf{v}_A, \mathbf{v}_B]).
A etapa de agregação é definida na seguinte classe ``Aggregate``.
.. raw:: html
.. raw:: html
.. code:: python
class Aggregate(nn.Block):
def __init__(self, num_hiddens, num_outputs, **kwargs):
super(Aggregate, self).__init__(**kwargs)
self.h = mlp(num_hiddens=num_hiddens, flatten=True)
self.h.add(nn.Dense(num_outputs))
def forward(self, V_A, V_B):
# Sum up both sets of comparison vectors
V_A = V_A.sum(axis=1)
V_B = V_B.sum(axis=1)
# Feed the concatenation of both summarization results into an MLP
Y_hat = self.h(np.concatenate([V_A, V_B], axis=1))
return Y_hat
.. raw:: html
.. raw:: html
.. code:: python
class Aggregate(nn.Module):
def __init__(self, num_inputs, num_hiddens, num_outputs, **kwargs):
super(Aggregate, self).__init__(**kwargs)
self.h = mlp(num_inputs, num_hiddens, flatten=True)
self.linear = nn.Linear(num_hiddens, num_outputs)
def forward(self, V_A, V_B):
# Sum up both sets of comparison vectors
V_A = V_A.sum(dim=1)
V_B = V_B.sum(dim=1)
# Feed the concatenation of both summarization results into an MLP
Y_hat = self.linear(self.h(torch.cat([V_A, V_B], dim=1)))
return Y_hat
.. raw:: html
.. raw:: html
Juntando Tudo
~~~~~~~~~~~~~
Ao reunir as etapas de atendimento, comparação e agregação, definimos o
modelo de atenção decomposto para treinar conjuntamente essas três
etapas.
.. raw:: html
.. raw:: html
.. code:: python
class DecomposableAttention(nn.Block):
def __init__(self, vocab, embed_size, num_hiddens, **kwargs):
super(DecomposableAttention, self).__init__(**kwargs)
self.embedding = nn.Embedding(len(vocab), embed_size)
self.attend = Attend(num_hiddens)
self.compare = Compare(num_hiddens)
# There are 3 possible outputs: entailment, contradiction, and neutral
self.aggregate = Aggregate(num_hiddens, 3)
def forward(self, X):
premises, hypotheses = X
A = self.embedding(premises)
B = self.embedding(hypotheses)
beta, alpha = self.attend(A, B)
V_A, V_B = self.compare(A, B, beta, alpha)
Y_hat = self.aggregate(V_A, V_B)
return Y_hat
.. raw:: html
.. raw:: html
.. code:: python
class DecomposableAttention(nn.Module):
def __init__(self, vocab, embed_size, num_hiddens, num_inputs_attend=100,
num_inputs_compare=200, num_inputs_agg=400, **kwargs):
super(DecomposableAttention, self).__init__(**kwargs)
self.embedding = nn.Embedding(len(vocab), embed_size)
self.attend = Attend(num_inputs_attend, num_hiddens)
self.compare = Compare(num_inputs_compare, num_hiddens)
# There are 3 possible outputs: entailment, contradiction, and neutral
self.aggregate = Aggregate(num_inputs_agg, num_hiddens, num_outputs=3)
def forward(self, X):
premises, hypotheses = X
A = self.embedding(premises)
B = self.embedding(hypotheses)
beta, alpha = self.attend(A, B)
V_A, V_B = self.compare(A, B, beta, alpha)
Y_hat = self.aggregate(V_A, V_B)
return Y_hat
.. raw:: html
.. raw:: html
Treinamento e Avaliação do Modelo
---------------------------------
Agora vamos treinar e avaliar o modelo de atenção decomposto definido no
conjunto de dados SNLI. Começamos lendo o *dataset*.
Lendo o *Dataset*
~~~~~~~~~~~~~~~~~
Baixamos e lemos o conjunto de dados SNLI usando a função definida em
:numref:`sec_natural-language-inference-and-dataset`. O tamanho do
lote e o comprimento da sequência são definidos em :math:`256` e
:math:`50`, respectivamente.
.. raw:: html
.. raw:: html
.. code:: python
batch_size, num_steps = 256, 50
train_iter, test_iter, vocab = d2l.load_data_snli(batch_size, num_steps)
.. parsed-literal::
:class: output
read 549367 examples
read 9824 examples
.. raw:: html
.. raw:: html
.. code:: python
batch_size, num_steps = 256, 50
train_iter, test_iter, vocab = d2l.load_data_snli(batch_size, num_steps)
.. parsed-literal::
:class: output
read 549367 examples
read 9824 examples
.. raw:: html
.. raw:: html
Criando o Modelo
~~~~~~~~~~~~~~~~
Usamos a incorporação GloVe pré-treinada :math:`100`-dimensional para
representar os *tokens* de entrada. Assim, predefinimos a dimensão dos
vetores :math:`\mathbf{a}_i` e :math:`\mathbf{b}_j` em
:eq:`eq_nli_e` como :math:`100`. A dimensão de saída das funções
:math:`f` in :eq:`eq_nli_e` e :math:`g` em :eq:`eq_nli_v_ab` é
definida como :math:`200`. Em seguida, criamos uma instância de modelo,
inicializamos seus parâmetros, e carregamos o *GloVe* embarcado para
inicializar vetores de *tokens* de entrada.
.. raw:: html
.. raw:: html
.. code:: python
embed_size, num_hiddens, devices = 100, 200, d2l.try_all_gpus()
net = DecomposableAttention(vocab, embed_size, num_hiddens)
net.initialize(init.Xavier(), ctx=devices)
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]
net.embedding.weight.set_data(embeds)
.. raw:: html
.. raw:: html
.. code:: python
embed_size, num_hiddens, devices = 100, 200, d2l.try_all_gpus()
net = DecomposableAttention(vocab, embed_size, num_hiddens)
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]
net.embedding.weight.data.copy_(embeds);
.. raw:: html
.. raw:: html
Treinamento e Avaliação do Modelo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Em contraste com a função ``split_batch`` em :numref:`sec_multi_gpu`
que recebe entradas únicas, como sequências de texto (ou imagens),
definimos uma função ``split_batch_multi_inputs`` para obter várias
entradas, como premissas e hipóteses em minibatches.
.. raw:: html
.. raw:: html
.. code:: python
#@save
def split_batch_multi_inputs(X, y, devices):
"""Split multi-input `X` and `y` into multiple devices."""
X = list(zip(*[gluon.utils.split_and_load(
feature, devices, even_split=False) for feature in X]))
return (X, gluon.utils.split_and_load(y, devices, even_split=False))
.. raw:: html
.. raw:: html
Agora podemos treinar e avaliar o modelo no *dataset* SNLI.
.. raw:: html
.. raw:: html
.. code:: python
lr, num_epochs = 0.001, 4
trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': lr})
loss = gluon.loss.SoftmaxCrossEntropyLoss()
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices,
split_batch_multi_inputs)
.. parsed-literal::
:class: output
loss 0.514, train acc 0.797, test acc 0.815
7637.6 examples/sec on [gpu(0), gpu(1)]
.. figure:: output_natural-language-inference-attention_b907c4_81_1.svg
.. raw:: html
.. raw:: html
.. code:: python
lr, num_epochs = 0.001, 4
trainer = torch.optim.Adam(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss(reduction="none")
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)
.. parsed-literal::
:class: output
loss 0.496, train acc 0.804, test acc 0.827
18580.5 examples/sec on [device(type='cuda', index=0), device(type='cuda', index=1)]
.. figure:: output_natural-language-inference-attention_b907c4_84_1.svg
.. raw:: html
.. raw:: html
Usando o Modelo
~~~~~~~~~~~~~~~
Finalmente, defina a função de previsão para produzir a relação lógica
entre um par de premissas e hipóteses.
.. raw:: html
.. raw:: html
.. code:: python
#@save
def predict_snli(net, vocab, premise, hypothesis):
premise = np.array(vocab[premise], ctx=d2l.try_gpu())
hypothesis = np.array(vocab[hypothesis], ctx=d2l.try_gpu())
label = np.argmax(net([premise.reshape((1, -1)),
hypothesis.reshape((1, -1))]), axis=1)
return 'entailment' if label == 0 else 'contradiction' if label == 1 \
else 'neutral'
.. raw:: html
.. raw:: html
.. code:: python
#@save
def predict_snli(net, vocab, premise, hypothesis):
net.eval()
premise = torch.tensor(vocab[premise], device=d2l.try_gpu())
hypothesis = torch.tensor(vocab[hypothesis], device=d2l.try_gpu())
label = torch.argmax(net([premise.reshape((1, -1)),
hypothesis.reshape((1, -1))]), dim=1)
return 'entailment' if label == 0 else 'contradiction' if label == 1 \
else 'neutral'
.. raw:: html
.. raw:: html
Podemos usar o modelo treinado para obter o resultado da inferência em
linguagem natural para um par de frases de amostra.
.. raw:: html
.. raw:: html
.. code:: python
predict_snli(net, vocab, ['he', 'is', 'good', '.'], ['he', 'is', 'bad', '.'])
.. parsed-literal::
:class: output
'contradiction'
.. raw:: html
.. raw:: html
.. code:: python
predict_snli(net, vocab, ['he', 'is', 'good', '.'], ['he', 'is', 'bad', '.'])
.. parsed-literal::
:class: output
'contradiction'
.. raw:: html
.. raw:: html
Resumo
------
- O modelo de atenção decomposto consiste em três etapas para prever as
relações lógicas entre premissas e hipóteses: atendimento, comparação
e agregação.
- Com mecanismos de atenção, podemos alinhar palavras em uma sequência
de texto com todas as palavras na outra e vice-versa. Esse
alinhamento é suave usando a média ponderada, em que, idealmente,
grandes pesos são associados às palavras a serem alinhadas.
- O truque da decomposição leva a uma complexidade linear mais
desejável do que a complexidade quadrática ao calcular os pesos de
atenção.
- Podemos usar a incorporação de palavras pré-treinadas como a
representação de entrada para tarefas de processamento de linguagem
natural *downstream*, como inferência de linguagem natural.
Exercícios
----------
1. Treine o modelo com outras combinações de hiperparâmetros. Você pode
obter melhor precisão no conjunto de teste?
2. Quais são as principais desvantagens do modelo de atenção
decomponível para inferência de linguagem natural?
3. Suponha que desejamos obter o nível de similaridade semântica (por
exemplo, um valor contínuo entre :math:`0` e :math:`1`) para qualquer
par de sentenças. Como devemos coletar e rotular o conjunto de dados?
Você pode projetar um modelo com mecanismos de atenção?
.. raw:: html
.. raw:: html
`Discussõess `__
.. raw:: html
.. raw:: html
`Discussõess `__
.. raw:: html
.. raw:: html
.. raw:: html