.. _sec_lstm:
Memória Longa de Curto Prazo (LSTM)
===================================
O desafio de abordar a preservação de informações de longo prazo e
entrada de curto prazo pulando modelos de variáveis latentes existem há
muito tempo. Uma das primeiras abordagens para resolver isso foi a longa
memória de curto prazo (LSTM) :cite:`Hochreiter.Schmidhuber.1997`. Ele
compartilha muitos dos as propriedades da GRU. Curiosamente, os LSTMs
têm um design um pouco mais complexo do que os GRUs mas antecede GRUs em
quase duas décadas.
Célula de Memória Bloqueada
---------------------------
Indiscutivelmente, o design da LSTM é inspirado pelas portas lógicas de
um computador. LSTM introduz uma *célula de memória* (ou *célula* para
abreviar) que tem a mesma forma que o estado oculto (algumas literaturas
consideram a célula de memória como um tipo especial de estado oculto),
projetado para registrar informações adicionais. Para controlar a célula
de memória precisamos de vários portões. Um portão é necessário para ler
as entradas do célula. Vamos nos referir a isso como o *portão de
saída*. Uma segunda porta é necessária para decidir quando ler os dados
para o célula. Chamamos isso de *porta de entrada*. Por último,
precisamos de um mecanismo para redefinir o conteúdo da célula,
governado por um *portão de esquecimento*. A motivação para tal design é
o mesmo das GRUs, ou seja, ser capaz de decidir quando lembrar e quando
ignorar entradas no estado oculto por meio de um mecanismo dedicado.
Deixe-nos ver como isso funciona na prática.
Porta de entrada, porta de esquecimento e porta de saída
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Assim como em GRUs, os dados que alimentam as portas LSTM são a entrada
na etapa de tempo atual e o estado oculto da etapa de tempo anterior,
conforme ilustrado em :numref:`lstm_0`. Eles são processados por três
camadas totalmente conectadas com uma função de ativação sigmóide para
calcular os valores de a entrada, esqueça. e portas de saída. Como
resultado, os valores das três portas estão na faixa de :math:`(0, 1)`.
.. _lstm_0:
.. figure:: ../img/lstm-0.svg
Calculando a porta de entrada, a porta de esquecimento e a porta de
saída em um modelo LSTM.
Matematicamente, suponha que existam :math:`h` unidades ocultas, o
tamanho do lote é :math:`n` e o número de entradas é :math:`d`. Assim, a
entrada é :math:`\mathbf{X}_t \in \mathbb{R}^{n \times d}` e o estado
oculto da etapa de tempo anterior é
:math:`\mathbf{H}_{t-1} \in \mathbb{R}^{n \times h}`.
Correspondentemente, as portas na etapa de tempo :math:`t` são definidos
da seguinte forma: a porta de entrada é
:math:`\mathbf{I}_t \in \mathbb{R}^{n \times h}`, a porta de
esquecimento é :math:`\mathbf{F}_t \in \mathbb{R}^{n \times h}`, e a
porta de saída é :math:`\mathbf{O}_t \in \mathbb{R}^{n \times h}`. Eles
são calculados da seguinte forma:
.. math::
\begin{aligned}
\mathbf{I}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{xi} + \mathbf{H}_{t-1} \mathbf{W}_{hi} + \mathbf{b}_i),\\
\mathbf{F}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{xf} + \mathbf{H}_{t-1} \mathbf{W}_{hf} + \mathbf{b}_f),\\
\mathbf{O}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{xo} + \mathbf{H}_{t-1} \mathbf{W}_{ho} + \mathbf{b}_o),
\end{aligned}
onde
:math:`\mathbf{W}_{xi}, \mathbf{W}_{xf}, \mathbf{W}_{xo} \in \mathbb{R}^{d \times h}`
e
:math:`\mathbf{W}_{hi}, \mathbf{W}_{hf}, \mathbf{W}_{ho} \in \mathbb{R}^{h \times h}`
são parâmetros de pesos e
:math:`\mathbf{b}_i, \mathbf{b}_f, \mathbf{b}_o \in \mathbb{R}^{1 \times h}`
são parâmetros viéses.
Célula de Memória Candidata
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Em seguida, projetamos a célula de memória. Como ainda não especificamos
a ação das várias portas, primeiro introduzimos a célula de memória
*candidata* :math:`\tilde{\mathbf{C}}_t \in \mathbb{R}^{n \times h}`.
Seu cálculo é semelhante ao das três portas descritas acima, mas usando
uma função :math:`\tanh` com um intervalo de valores para :math:`(-1,1)`
como a função de ativação. Isso leva à seguinte equação na etapa de
tempo :math:`t`:
.. math:: \tilde{\mathbf{C}}_t = \text{tanh}(\mathbf{X}_t \mathbf{W}_{xc} + \mathbf{H}_{t-1} \mathbf{W}_{hc} + \mathbf{b}_c),
onde :math:`\mathbf{W}_{xc} \in \mathbb{R}^{d \times h}` and
:math:`\mathbf{W}_{hc} \in \mathbb{R}^{h \times h}` são parâmetros de
pesos :math:`\mathbf{b}_c \in \mathbb{R}^{1 \times h}` é um parâmetro de
viés.
Uma ilustração rápida da célula de memória candidata é mostrada em
:numref:`lstm_1`.
.. _lstm_1:
.. figure:: ../img/lstm-1.svg
Computando a célula de memória candidata em um modelo LSTM.
Célula de Memória
~~~~~~~~~~~~~~~~~
Em GRUs, temos um mecanismo para controlar a entrada e o esquecimento
(ou salto). De forma similar, em LSTMs, temos duas portas dedicadas para
tais propósitos: a porta de entrada :math:`\mathbf{I}_t` governa o
quanto levamos os novos dados em conta via :math:`\tilde{\mathbf{C}}_t`
e a porta de esquecer :math:`\mathbf{F}_t` aborda quanto do conteúdo da
célula de memória antiga
:math:`\mathbf{C}_{t-1} \in \mathbb{R}^{n \times h}` retemos. Usando o
mesmo truque de multiplicação pontual de antes, chegamos à seguinte
equação de atualização:
.. math:: \mathbf{C}_t = \mathbf{F}_t \odot \mathbf{C}_{t-1} + \mathbf{I}_t \odot \tilde{\mathbf{C}}_t.
Se a porta de esquecimento é sempre aproximadamente 1 e a porta de
entrada é sempre aproximadamente 0, as células de memória anteriores
:math:`\mathbf{C}_{t-1}` serão salvas ao longo do tempo e passadas para
o intervalo de tempo atual. Este projeto é introduzido para aliviar o
problema do gradiente de desaparecimento e para melhor capturar
dependências de longo alcance dentro de sequências.
Assim, chegamos ao diagrama de fluxo em :numref:`lstm_2`.
.. _lstm_2:
.. figure:: ../img/lstm-2.svg
Calculando a célula de memória em um modelo LSTM.
Estado Oculto
~~~~~~~~~~~~~
Por último, precisamos definir como calcular o estado oculto
:math:`\mathbf{H}_t \in \mathbb{R}^{n \times h}`. É aqui que a porta de
saída entra em ação. No LSTM, é simplesmente uma versão bloqueada do
:math:`\tanh` da célula de memória. Isso garante que os valores de
:math:`\mathbf{H}_t` estejam sempre no intervalo :math:`(-1,1)`.
.. math:: \mathbf{H}_t = \mathbf{O}_t \odot \tanh(\mathbf{C}_t).
Sempre que a porta de saída se aproxima de 1, passamos efetivamente
todas as informações da memória para o preditor, enquanto para a porta
de saída próxima de 0, retemos todas as informações apenas dentro da
célula de memória e não realizamos nenhum processamento posterior.
:numref:`lstm_3` tem uma ilustração gráfica do fluxo de dados.
.. _lstm_3:
.. figure:: ../img/lstm-3.svg
Calculando o estado oculto em um modelo LSTM.
Implementação do zero
---------------------
Agora, vamos implementar um LSTM do zero. Da mesma forma que os
experimentos em :numref:`sec_rnn_scratch`, primeiro carregamos o
conjunto de dados da máquina do tempo.
.. raw:: html
.. raw:: html
.. code:: python
from mxnet import np, npx
from mxnet.gluon import rnn
from d2l import mxnet as d2l
npx.set_np()
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
.. raw:: html
.. raw:: html
.. code:: python
import torch
from torch import nn
from d2l import torch as d2l
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
.. raw:: html
.. raw:: html
Inicializando os parâmetros do modelo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Em seguida, precisamos definir e inicializar os parâmetros do modelo.
Como anteriormente, o hiperparâmetro ``num_hiddens`` define o número de
unidades ocultas. Inicializamos os pesos seguindo uma distribuição
gaussiana com desvio padrão de 0,01 e definimos os vieses como 0.
.. raw:: html
.. raw:: html
.. code:: python
def get_lstm_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size
def normal(shape):
return np.random.normal(scale=0.01, size=shape, ctx=device)
def three():
return (normal((num_inputs, num_hiddens)),
normal((num_hiddens, num_hiddens)),
np.zeros(num_hiddens, ctx=device))
W_xi, W_hi, b_i = three() # Input gate parameters
W_xf, W_hf, b_f = three() # Forget gate parameters
W_xo, W_ho, b_o = three() # Output gate parameters
W_xc, W_hc, b_c = three() # Candidate memory cell parameters
# Output layer parameters
W_hq = normal((num_hiddens, num_outputs))
b_q = np.zeros(num_outputs, ctx=device)
# Attach gradients
params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc,
b_c, W_hq, b_q]
for param in params:
param.attach_grad()
return params
.. raw:: html
.. raw:: html
.. code:: python
def get_lstm_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size
def normal(shape):
return torch.randn(size=shape, device=device)*0.01
def three():
return (normal((num_inputs, num_hiddens)),
normal((num_hiddens, num_hiddens)),
torch.zeros(num_hiddens, device=device))
W_xi, W_hi, b_i = three() # Input gate parameters
W_xf, W_hf, b_f = three() # Forget gate parameters
W_xo, W_ho, b_o = three() # Output gate parameters
W_xc, W_hc, b_c = three() # Candidate memory cell parameters
# Output layer parameters
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device)
# Attach gradients
params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc,
b_c, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params
.. raw:: html
.. raw:: html
Definindo o modelo
~~~~~~~~~~~~~~~~~~
Na função de inicialização, o estado oculto do LSTM precisa retornar uma
célula de memória *adicional* com um valor de 0 e uma forma de (tamanho
do lote, número de unidades ocultas). Consequentemente, obtemos a
seguinte inicialização de estado.
.. raw:: html
.. raw:: html
.. code:: python
def init_lstm_state(batch_size, num_hiddens, device):
return (np.zeros((batch_size, num_hiddens), ctx=device),
np.zeros((batch_size, num_hiddens), ctx=device))
.. raw:: html
.. raw:: html
.. code:: python
def init_lstm_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device),
torch.zeros((batch_size, num_hiddens), device=device))
.. raw:: html
.. raw:: html
O modelo real é definido exatamente como o que discutimos antes:
fornecer três portas e uma célula de memória auxiliar. Observe que
apenas o estado oculto é passado para a camada de saída. A célula de
memória :math:`\mathbf{C}_t` não participa diretamente no cálculo de
saída.
.. raw:: html
.. raw:: html
.. code:: python
def lstm(inputs, state, params):
[W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,
W_hq, b_q] = params
(H, C) = state
outputs = []
for X in inputs:
I = npx.sigmoid(np.dot(X, W_xi) + np.dot(H, W_hi) + b_i)
F = npx.sigmoid(np.dot(X, W_xf) + np.dot(H, W_hf) + b_f)
O = npx.sigmoid(np.dot(X, W_xo) + np.dot(H, W_ho) + b_o)
C_tilda = np.tanh(np.dot(X, W_xc) + np.dot(H, W_hc) + b_c)
C = F * C + I * C_tilda
H = O * np.tanh(C)
Y = np.dot(H, W_hq) + b_q
outputs.append(Y)
return np.concatenate(outputs, axis=0), (H, C)
.. raw:: html
.. raw:: html
.. code:: python
def lstm(inputs, state, params):
[W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,
W_hq, b_q] = params
(H, C) = state
outputs = []
for X in inputs:
I = torch.sigmoid((X @ W_xi) + (H @ W_hi) + b_i)
F = torch.sigmoid((X @ W_xf) + (H @ W_hf) + b_f)
O = torch.sigmoid((X @ W_xo) + (H @ W_ho) + b_o)
C_tilda = torch.tanh((X @ W_xc) + (H @ W_hc) + b_c)
C = F * C + I * C_tilda
H = O * torch.tanh(C)
Y = (H @ W_hq) + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H, C)
.. raw:: html
.. raw:: html
Treinamento e previsão
~~~~~~~~~~~~~~~~~~~~~~
Vamos treinar um LSTM da mesma forma que fizemos em :numref:`sec_gru`,
instanciando a classe ``RNNModelScratch`` como introduzida em
:numref:`sec_rnn_scratch`.
.. raw:: html
.. raw:: html
.. code:: python
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_lstm_params,
init_lstm_state, lstm)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
.. parsed-literal::
:class: output
perplexity 1.1, 9074.3 tokens/sec on gpu(0)
time traveller for so it will be convenient to speak of himwas e
travelleryou can show black is white by argument said filby
.. figure:: output_lstm_86eb9f_39_1.svg
.. raw:: html
.. raw:: html
.. code:: python
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_lstm_params,
init_lstm_state, lstm)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
.. parsed-literal::
:class: output
perplexity 1.1, 20015.8 tokens/sec on cuda:0
time traveller for so it will be convenient to speak of himwas e
travelleryou can show black is white by argument said filby
.. figure:: output_lstm_86eb9f_42_1.svg
.. raw:: html
.. raw:: html
Implementação concisa
---------------------
Usando APIs de alto nível, podemos instanciar diretamente um modelo
``LSTM``. Isso encapsula todos os detalhes de configuração que tornamos
explícitos acima. O código é significativamente mais rápido, pois usa
operadores compilados em vez de Python para muitos detalhes que
explicamos em detalhes antes.
.. raw:: html
.. raw:: html
.. code:: python
lstm_layer = rnn.LSTM(num_hiddens)
model = d2l.RNNModel(lstm_layer, len(vocab))
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
.. parsed-literal::
:class: output
perplexity 1.2, 127777.3 tokens/sec on gpu(0)
time traveller after the pauserequired for the proper assimilati
traveller ascer thingscint in the fittle for instance of ha
.. figure:: output_lstm_86eb9f_48_1.svg
.. raw:: html
.. raw:: html
.. code:: python
num_inputs = vocab_size
lstm_layer = nn.LSTM(num_inputs, num_hiddens)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
.. parsed-literal::
:class: output
perplexity 1.1, 282016.6 tokens/sec on cuda:0
time traveller for so it will be convenient to speak of himwas e
traveller with a slight accession ofcheerfulness really thi
.. figure:: output_lstm_86eb9f_51_1.svg
.. raw:: html
.. raw:: html
LSTMs são o modelo autorregressivo de variável latente prototípica com
controle de estado não trivial. Muitas variantes foram propostas ao
longo dos anos, por exemplo, camadas múltiplas, conexões residuais,
diferentes tipos de regularização. No entanto, treinar LSTMs e outros
modelos de sequência (como GRUs) são bastante caros devido à dependência
de longo alcance da sequência. Mais tarde, encontraremos modelos
alternativos, como transformadores, que podem ser usados em alguns
casos.
Resumo
------
- Os LSTMs têm três tipos de portas: portas de entrada, portas de
esquecimento e portas de saída que controlam o fluxo de informações.
- A saída da camada oculta do LSTM inclui o estado oculto e a célula de
memória. Apenas o estado oculto é passado para a camada de saída. A
célula de memória é totalmente interna.
- LSTMs podem aliviar gradientes que desaparecem e explodem.
Exercícios
----------
1. Ajuste os hiperparâmetros e analise sua influência no tempo de
execução, perplexidade e sequência de saída.
2. Como você precisaria mudar o modelo para gerar palavras adequadas em
vez de sequências de caracteres?
3. Compare o custo computacional para GRUs, LSTMs e RNNs regulares para
uma determinada dimensão oculta. Preste atenção especial ao custo de
treinamento e inferência.
4. Uma vez que a célula de memória candidata garante que o intervalo de
valores está entre :math:`-1` e :math:`1` usando a função
:math:`\tanh`, por que o estado oculto precisa usar a função
:math:`\tanh` novamente para garantir que a saída o intervalo de
valores está entre :math:`-1` e :math:`1`?
5. Implemente um modelo LSTM para predição de série temporal em vez de
predição de sequência de caracteres.
.. raw:: html
.. raw:: html
`Discussão `__
.. raw:: html
.. raw:: html
`Discussão `__
.. raw:: html
.. raw:: html
.. raw:: html