.. _sec_rnn_scratch:
Implementação de Redes Neurais Recorrentes do Zero
==================================================
Nesta seção, implementaremos uma RNN do zero para um modelo de linguagem
de nível de personagem, de acordo com nossas descrições em
:numref:`sec_rnn`. Tal modelo será treinado em H. G. Wells ’\* The
Time Machine \*. Como antes, começamos lendo o conjunto de dados
primeiro, que é apresentado em :numref:`sec_language_model`.
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
import math
from mxnet import autograd, gluon, np, npx
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
%matplotlib inline
import math
import torch
from torch import nn
from torch.nn import functional as F
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
.. code:: python
%matplotlib inline
import math
import numpy as np
import tensorflow as tf
from d2l import tensorflow as d2l
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
.. parsed-literal::
:class: output
Downloading ../data/timemachine.txt from http://d2l-data.s3-accelerate.amazonaws.com/timemachine.txt...
.. code:: python
train_random_iter, vocab_random_iter = d2l.load_data_time_machine(
batch_size, num_steps, use_random_iter=True)
.. raw:: html
.. raw:: html
Codificação One-Hot
-------------------
Lembre-se de que cada token é representado como um índice numérico em
``train_iter``. Alimentar esses índices diretamente para uma rede neural
pode tornar difícil aprender. Frequentemente, representamos cada token
como um vetor de *features* mais expressivo. A representação mais fácil
é chamada de *codificação one-hot*, que é introduzida em
:numref:`subsec_classification-problem`.
Em resumo, mapeamos cada índice para um vetor de unidade diferente:
suponha que o número de tokens diferentes no vocabulário seja :math:`N`
(``len (vocab)``) e os índices de token variam de 0 a :math:`N-1`. Se o
índice de um token é o inteiro :math:`i`, então criamos um vetor de 0s
com um comprimento de :math:`N` e definimos o elemento na posição
:math:`i` como 1. Este vetor é o vetor one-hot do token original. Os
vetores one-hot com índices 0 e 2 são mostrados abaixo.
.. raw:: html
.. raw:: html
.. code:: python
npx.one_hot(np.array([0, 2]), len(vocab))
.. parsed-literal::
:class: output
array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
.. raw:: html
.. raw:: html
.. code:: python
F.one_hot(torch.tensor([0, 2]), len(vocab))
.. parsed-literal::
:class: output
tensor([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0]])
.. raw:: html
.. raw:: html
.. code:: python
tf.one_hot(tf.constant([0, 2]), len(vocab))
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
A forma do minibatch que amostramos a cada vez é (tamanho do lote,
número de etapas de tempo). A função ``one_hot`` transforma tal
minibatch em um tensor tridimensional com a última dimensão igual ao
tamanho do vocabulário (``len (vocab)``). Freqüentemente, transpomos a
entrada para que possamos obter um saída de forma (número de etapas de
tempo, tamanho do lote, tamanho do vocabulário). Isso nos permitirá mais
convenientemente fazer um loop pela dimensão mais externa para atualizar
os estados ocultos de um minibatch, passo a passo do tempo.
.. raw:: html
.. raw:: html
.. code:: python
X = np.arange(10).reshape((2, 5))
npx.one_hot(X.T, 28).shape
.. parsed-literal::
:class: output
(5, 2, 28)
.. raw:: html
.. raw:: html
.. code:: python
X = torch.arange(10).reshape((2, 5))
F.one_hot(X.T, 28).shape
.. parsed-literal::
:class: output
torch.Size([5, 2, 28])
.. raw:: html
.. raw:: html
.. code:: python
X = tf.reshape(tf.range(10), (2, 5))
tf.one_hot(tf.transpose(X), 28).shape
.. parsed-literal::
:class: output
TensorShape([5, 2, 28])
.. raw:: html
.. raw:: html
Inicializando os Parâmetros do Modelo
-------------------------------------
Em seguida, inicializamos os parâmetros do modelo para o modelo RNN. O
número de unidades ocultas ``num_hiddens`` é um hiperparâmetro
ajustável. Ao treinar modelos de linguagem, as entradas e saídas são do
mesmo vocabulário. Portanto, eles têm a mesma dimensão, que é igual ao
tamanho do vocabulário.
.. raw:: html
.. raw:: html
.. code:: python
def get_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)
# Hidden layer parameters
W_xh = normal((num_inputs, num_hiddens))
W_hh = normal((num_hiddens, num_hiddens))
b_h = np.zeros(num_hiddens, ctx=device)
# Output layer parameters
W_hq = normal((num_hiddens, num_outputs))
b_q = np.zeros(num_outputs, ctx=device)
# Attach gradients
params = [W_xh, W_hh, b_h, W_hq, b_q]
for param in params:
param.attach_grad()
return params
.. raw:: html
.. raw:: html
.. code:: python
def get_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size
def normal(shape):
return torch.randn(size=shape, device=device) * 0.01
# Hidden layer parameters
W_xh = normal((num_inputs, num_hiddens))
W_hh = normal((num_hiddens, num_hiddens))
b_h = torch.zeros(num_hiddens, device=device)
# Output layer parameters
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device)
# Attach gradients
params = [W_xh, W_hh, b_h, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params
.. raw:: html
.. raw:: html
.. code:: python
def get_params(vocab_size, num_hiddens):
num_inputs = num_outputs = vocab_size
def normal(shape):
return tf.random.normal(shape=shape,stddev=0.01,mean=0,dtype=tf.float32)
# Hidden layer parameters
W_xh = tf.Variable(normal((num_inputs, num_hiddens)), dtype=tf.float32)
W_hh = tf.Variable(normal((num_hiddens, num_hiddens)), dtype=tf.float32)
b_h = tf.Variable(tf.zeros(num_hiddens), dtype=tf.float32)
# Output layer parameters
W_hq = tf.Variable(normal((num_hiddens, num_outputs)), dtype=tf.float32)
b_q = tf.Variable(tf.zeros(num_outputs), dtype=tf.float32)
params = [W_xh, W_hh, b_h, W_hq, b_q]
return params
.. raw:: html
.. raw:: html
Modelo RNN
----------
Para definir um modelo RNN, primeiro precisamos de uma função
``init_rnn_state`` para retornar ao estado oculto na inicialização. Ele
retorna um tensor preenchido com 0 e com uma forma de (tamanho do lote,
número de unidades ocultas). O uso de tuplas torna mais fácil lidar com
situações em que o estado oculto contém várias variáveis, que
encontraremos em seções posteriores.
.. raw:: html
.. raw:: html
.. code:: python
def init_rnn_state(batch_size, num_hiddens, device):
return (np.zeros((batch_size, num_hiddens), ctx=device), )
.. raw:: html
.. raw:: html
.. code:: python
def init_rnn_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )
.. raw:: html
.. raw:: html
.. code:: python
def init_rnn_state(batch_size, num_hiddens):
return (tf.zeros((batch_size, num_hiddens)), )
.. raw:: html
.. raw:: html
A seguinte função ``rnn`` define como calcular o estado oculto e a saída
em uma etapa de tempo. Observe que o modelo RNN percorre a dimensão mais
externa de ``entradas`` para que ela atualize os estados ocultos ``H``
de um minibatch, passo a passo do tempo. Além do mais, a função de
ativação aqui usa a função :math:`\tanh`. Como descrito em
:numref:`sec_mlp`, o o valor médio da função :math:`\tanh` é 0, quando
os elementos são uniformemente distribuídos sobre os números reais.
.. raw:: html
.. raw:: html
.. code:: python
def rnn(inputs, state, params):
# Shape of `inputs`: (`num_steps`, `batch_size`, `vocab_size`)
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
# Shape of `X`: (`batch_size`, `vocab_size`)
for X in inputs:
H = np.tanh(np.dot(X, W_xh) + np.dot(H, W_hh) + b_h)
Y = np.dot(H, W_hq) + b_q
outputs.append(Y)
return np.concatenate(outputs, axis=0), (H,)
.. raw:: html
.. raw:: html
.. code:: python
def rnn(inputs, state, params):
# Here `inputs` shape: (`num_steps`, `batch_size`, `vocab_size`)
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
# Shape of `X`: (`batch_size`, `vocab_size`)
for X in inputs:
H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)
Y = torch.mm(H, W_hq) + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)
.. raw:: html
.. raw:: html
.. code:: python
def rnn(inputs, state, params):
# Here `inputs` shape: (`num_steps`, `batch_size`, `vocab_size`)
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
# Shape of `X`: (`batch_size`, `vocab_size`)
for X in inputs:
X = tf.reshape(X,[-1,W_xh.shape[0]])
H = tf.tanh(tf.matmul(X, W_xh) + tf.matmul(H, W_hh) + b_h)
Y = tf.matmul(H, W_hq) + b_q
outputs.append(Y)
return tf.concat(outputs, axis=0), (H,)
.. raw:: html
.. raw:: html
Com todas as funções necessárias sendo definidas, em seguida, criamos
uma classe para envolver essas funções e armazenar parâmetros para um
modelo RNN implementado do zero.
.. raw:: html
.. raw:: html
.. code:: python
class RNNModelScratch: #@save
"""An RNN Model implemented from scratch."""
def __init__(self, vocab_size, num_hiddens, device, get_params,
init_state, forward_fn):
self.vocab_size, self.num_hiddens = vocab_size, num_hiddens
self.params = get_params(vocab_size, num_hiddens, device)
self.init_state, self.forward_fn = init_state, forward_fn
def __call__(self, X, state):
X = npx.one_hot(X.T, self.vocab_size)
return self.forward_fn(X, state, self.params)
def begin_state(self, batch_size, ctx):
return self.init_state(batch_size, self.num_hiddens, ctx)
.. raw:: html
.. raw:: html
.. code:: python
class RNNModelScratch: #@save
"""A RNN Model implemented from scratch."""
def __init__(self, vocab_size, num_hiddens, device,
get_params, init_state, forward_fn):
self.vocab_size, self.num_hiddens = vocab_size, num_hiddens
self.params = get_params(vocab_size, num_hiddens, device)
self.init_state, self.forward_fn = init_state, forward_fn
def __call__(self, X, state):
X = F.one_hot(X.T, self.vocab_size).type(torch.float32)
return self.forward_fn(X, state, self.params)
def begin_state(self, batch_size, device):
return self.init_state(batch_size, self.num_hiddens, device)
.. raw:: html
.. raw:: html
.. code:: python
class RNNModelScratch: #@save
"""A RNN Model implemented from scratch."""
def __init__(self, vocab_size, num_hiddens,
init_state, forward_fn):
self.vocab_size, self.num_hiddens = vocab_size, num_hiddens
self.init_state, self.forward_fn = init_state, forward_fn
def __call__(self, X, state, params):
X = tf.one_hot(tf.transpose(X), self.vocab_size)
X = tf.cast(X, tf.float32)
return self.forward_fn(X, state, params)
def begin_state(self, batch_size):
return self.init_state(batch_size, self.num_hiddens)
.. raw:: html
.. raw:: html
Vamos verificar se as saídas têm as formas corretas, por exemplo, para
garantir que a dimensionalidade do estado oculto permaneça inalterada.
.. raw:: html
.. raw:: html
.. code:: python
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
init_rnn_state, rnn)
state = net.begin_state(X.shape[0], d2l.try_gpu())
Y, new_state = net(X.as_in_context(d2l.try_gpu()), state)
Y.shape, len(new_state), new_state[0].shape
.. parsed-literal::
:class: output
((10, 28), 1, (2, 512))
.. raw:: html
.. raw:: html
.. code:: python
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
init_rnn_state, rnn)
state = net.begin_state(X.shape[0], d2l.try_gpu())
Y, new_state = net(X.to(d2l.try_gpu()), state)
Y.shape, len(new_state), new_state[0].shape
.. parsed-literal::
:class: output
(torch.Size([10, 28]), 1, torch.Size([2, 512]))
.. raw:: html
.. raw:: html
.. code:: python
# defining tensorflow training strategy
device_name = d2l.try_gpu()._device_name
strategy = tf.distribute.OneDeviceStrategy(device_name)
num_hiddens = 512
with strategy.scope():
net = RNNModelScratch(len(vocab), num_hiddens, init_rnn_state, rnn)
state = net.begin_state(X.shape[0])
params = get_params(len(vocab), num_hiddens)
Y, new_state = net(X, state, params)
Y.shape, len(new_state), new_state[0].shape
.. parsed-literal::
:class: output
(TensorShape([10, 28]), 1, TensorShape([2, 512]))
.. raw:: html
.. raw:: html
Podemos ver que a forma de saída é (número de etapas de tempo
:math:`\times` tamanho do lote, tamanho do vocabulário), enquanto a
forma do estado oculto permanece a mesma, ou seja, (tamanho do lote,
número de unidades ocultas).
Predição
--------
Vamos primeiro definir a função de predição para gerar novos personagens
seguindo o ``prefixo`` fornecido pelo usuário, que é uma string contendo
vários caracteres. Ao percorrer esses caracteres iniciais em
``prefixo``, continuamos passando pelo estado escondido para a próxima
etapa sem gerando qualquer saída. Isso é chamado de período de
*aquecimento*, durante o qual o modelo se atualiza (por exemplo,
atualizar o estado oculto) mas não faz previsões. Após o período de
aquecimento, o estado oculto é geralmente melhor do que seu valor
inicializado no início. Assim, geramos os caracteres previstos e os
emitimos.
.. raw:: html
.. raw:: html
.. code:: python
def predict_ch8(prefix, num_preds, net, vocab, device): #@save
"""Generate new characters following the `prefix`."""
state = net.begin_state(batch_size=1, ctx=device)
outputs = [vocab[prefix[0]]]
get_input = lambda: np.array([outputs[-1]], ctx=device).reshape((1, 1))
for y in prefix[1:]: # Warm-up period
_, state = net(get_input(), state)
outputs.append(vocab[y])
for _ in range(num_preds): # Predict `num_preds` steps
y, state = net(get_input(), state)
outputs.append(int(y.argmax(axis=1).reshape(1)))
return ''.join([vocab.idx_to_token[i] for i in outputs])
.. raw:: html
.. raw:: html
.. code:: python
def predict_ch8(prefix, num_preds, net, vocab, device): #@save
"""Generate new characters following the `prefix`."""
state = net.begin_state(batch_size=1, device=device)
outputs = [vocab[prefix[0]]]
get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))
for y in prefix[1:]: # Warm-up period
_, state = net(get_input(), state)
outputs.append(vocab[y])
for _ in range(num_preds): # Predict `num_preds` steps
y, state = net(get_input(), state)
outputs.append(int(y.argmax(dim=1).reshape(1)))
return ''.join([vocab.idx_to_token[i] for i in outputs])
.. raw:: html
.. raw:: html
.. code:: python
def predict_ch8(prefix, num_preds, net, vocab, params): #@save
"""Generate new characters following the `prefix`."""
state = net.begin_state(batch_size=1)
outputs = [vocab[prefix[0]]]
get_input = lambda: tf.reshape(tf.constant([outputs[-1]]), (1, 1)).numpy()
for y in prefix[1:]: # Warm-up period
_, state = net(get_input(), state, params)
outputs.append(vocab[y])
for _ in range(num_preds): # Predict `num_preds` steps
y, state = net(get_input(), state, params)
outputs.append(int(y.numpy().argmax(axis=1).reshape(1)))
return ''.join([vocab.idx_to_token[i] for i in outputs])
.. raw:: html
.. raw:: html
Agora podemos testar a função ``predict_ch8``. Especificamos o prefixo
como ``viajante do tempo`` e fazemos com que ele gere 10 caracteres
adicionais. Visto que não treinamos a rede, isso vai gerar previsões sem
sentido.
.. raw:: html
.. raw:: html
.. code:: python
predict_ch8('time traveller ', 10, net, vocab, d2l.try_gpu())
.. parsed-literal::
:class: output
'time traveller iiiiiiiiii'
.. raw:: html
.. raw:: html
.. code:: python
predict_ch8('time traveller ', 10, net, vocab, d2l.try_gpu())
.. parsed-literal::
:class: output
'time traveller ufcr ufcr '
.. raw:: html
.. raw:: html
.. code:: python
predict_ch8('time traveller ', 10, net, vocab, params)
.. parsed-literal::
:class: output
'time traveller skjtafjtaf'
.. raw:: html
.. raw:: html
Recorte de Gradiente
--------------------
Para uma sequência de comprimento :math:`T`, calculamos os gradientes ao
longo desses :math:`T` passos de tempo em uma iteração, que resulta em
uma cadeia de produtos-matriz com comprimento :math:`\mathcal{O}(T)`
durante a retropropagação. Conforme mencionado em
:numref:`sec_numerical_stability`, pode resultar em instabilidade
numérica, por exemplo, os gradientes podem explodir ou desaparecer,
quando :math:`T` é grande. Portanto, os modelos RNN geralmente precisam
de ajuda extra para estabilizar o treinamento.
De um modo geral, ao resolver um problema de otimização, executamos
etapas de atualização para o parâmetro do modelo, diga na forma vetorial
:math:`\mathbf{x}`, na direção do gradiente negativo :math:`\mathbf{g}`
em um minibatch. Por exemplo, com :math:`\eta > 0` como a taxa de
aprendizagem, em uma iteração nós atualizamos :math:`\mathbf{x}` como
:math:`\mathbf{x} - \eta \mathbf{g}`. Vamos supor ainda que a função
objetivo :math:`f` é bem comportada, digamos, *Lipschitz contínuo* com
:math:`L` constante. Quer dizer, para qualquer :math:`\mathbf{x}` e
:math:`\mathbf{y}`, temos
.. math:: |f(\mathbf{x}) - f(\mathbf{y})| \leq L \|\mathbf{x} - \mathbf{y}\|.
Neste caso, podemos assumir com segurança que, se atualizarmos o vetor
de parâmetro por :math:`\eta \mathbf{g}`, então
.. math:: |f(\mathbf{x}) - f(\mathbf{x} - \eta\mathbf{g})| \leq L \eta\|\mathbf{g}\|,
o que significa que não observaremos uma mudança de mais de
:math:`L \eta \|\mathbf{g}\|`. Isso é uma maldição e uma bênção. Do lado
da maldição, limita a velocidade de progresso; enquanto do lado da
bênção, limita até que ponto as coisas podem dar errado se seguirmos na
direção errada.
Às vezes, os gradientes podem ser muito grandes e o algoritmo de
otimização pode falhar em convergir. Poderíamos resolver isso reduzindo
a taxa de aprendizado :math:`\eta`. Mas e se nós *raramente* obtivermos
gradientes grandes? Nesse caso, essa abordagem pode parecer totalmente
injustificada. Uma alternativa popular é cortar o gradiente
:math:`\mathbf{g}` projetando-o de volta para uma bola de um determinado
raio, digamos :math:`\theta` via
.. math:: \mathbf{g} \leftarrow \min\left(1, \frac{\theta}{\|\mathbf{g}\|}\right) \mathbf{g}.
Fazendo isso, sabemos que a norma do gradiente nunca excede
:math:`\theta` e que o gradiente atualizado é totalmente alinhado com a
direção original de :math:`\mathbf{g}`. Também tem o efeito colateral
desejável de limitar a influência de qualquer minibatch (e dentro dele
qualquer amostra dada) pode exercer sobre o vetor de parâmetro. Isto
confere certo grau de robustez ao modelo. O recorte de gradiente fornece
uma solução rápida para a explosão do gradiente. Embora não resolva
totalmente o problema, é uma das muitas técnicas para aliviá-lo.
Abaixo, definimos uma função para cortar os gradientes de um modelo que
é implementado do zero ou um modelo construído pelas APIs de alto nível.
Observe também que calculamos a norma do gradiente em todos os
parâmetros do modelo.
.. raw:: html
.. raw:: html
.. code:: python
def grad_clipping(net, theta): #@save
"""Clip the gradient."""
if isinstance(net, gluon.Block):
params = [p.data() for p in net.collect_params().values()]
else:
params = net.params
norm = math.sqrt(sum((p.grad ** 2).sum() for p in params))
if norm > theta:
for param in params:
param.grad[:] *= theta / norm
.. raw:: html
.. raw:: html
.. code:: python
def grad_clipping(net, theta): #@save
"""Clip the gradient."""
if isinstance(net, nn.Module):
params = [p for p in net.parameters() if p.requires_grad]
else:
params = net.params
norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
if norm > theta:
for param in params:
param.grad[:] *= theta / norm
.. raw:: html
.. raw:: html
.. code:: python
def grad_clipping(grads, theta): #@save
"""Clip the gradient."""
theta = tf.constant(theta, dtype=tf.float32)
norm = tf.math.sqrt(sum((tf.reduce_sum(grad ** 2)).numpy()
for grad in grads))
norm = tf.cast(norm, tf.float32)
new_grad = []
if tf.greater(norm, theta):
for grad in grads:
new_grad.append(grad * theta / norm)
else:
for grad in grads:
new_grad.append(grad)
return new_grad
.. raw:: html
.. raw:: html
Treinamento
-----------
Antes de treinar o modelo, vamos definir uma função para treinar o
modelo em uma época. É diferente de como treinamos o modelo de:
:numref:`sec_softmax_scratch` em três lugares:
1. Diferentes métodos de amostragem para dados sequenciais (amostragem
aleatória e particionamento sequencial) resultarão em diferenças na
inicialização de estados ocultos.
2. Cortamos os gradientes antes de atualizar os parâmetros do modelo.
Isso garante que o modelo não diverge, mesmo quando os gradientes
explodem em algum ponto durante o processo de treinamento.
3. Usamos perplexidade para avaliar o modelo. Conforme discutido em
:numref:`subsec_perplexity`, isso garante que as sequências de
comprimentos diferentes sejam comparáveis.
Especificamente, quando o particionamento sequencial é usado,
inicializamos o estado oculto apenas no início de cada época. Uma vez
que o exemplo de subsequência :math:`i^\mathrm{th}` no próximo minibatch
é adjacente ao exemplo de subsequência :math:`i^\mathrm{th}` atual, o
estado oculto no final do minibatch atual será usado para inicializar o
estado oculto no início do próximo minibatch. Nesse caminho, informação
histórica da sequência armazenado no estado oculto pode fluir em
subsequências adjacentes dentro de uma época. No entanto, o cálculo do
estado oculto em qualquer ponto depende de todos os minibatches
anteriores na mesma época, o que complica o cálculo do gradiente. Para
reduzir o custo computacional, destacamos o gradiente antes de processar
qualquer minibatch de modo que o cálculo do gradiente do estado oculto é
sempre limitado aos passos de tempo em um minibatch.
Ao usar a amostragem aleatória, precisamos reinicializar o estado oculto
para cada iteração, uma vez que cada exemplo é amostrado com uma posição
aleatória. Igual à função ``train_epoch_ch3`` em
:numref:`sec_softmax_scratch`, ``atualizador`` é uma função geral para
atualizar os parâmetros do modelo. Pode ser a função ``d2l.sgd``
implementada do zero ou a função de otimização integrada em uma
estrutura de aprendizagem profunda.
.. raw:: html
.. raw:: html
.. code:: python
#@save
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
"""Train a model within one epoch (defined in Chapter 8)."""
state, timer = None, d2l.Timer()
metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens
for X, Y in train_iter:
if state is None or use_random_iter:
# Initialize `state` when either it is the first iteration or
# using random sampling
state = net.begin_state(batch_size=X.shape[0], ctx=device)
else:
for s in state:
s.detach()
y = Y.T.reshape(-1)
X, y = X.as_in_ctx(device), y.as_in_ctx(device)
with autograd.record():
y_hat, state = net(X, state)
l = loss(y_hat, y).mean()
l.backward()
grad_clipping(net, 1)
updater(batch_size=1) # Since the `mean` function has been invoked
metric.add(l * d2l.size(y), d2l.size(y))
return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
.. raw:: html
.. raw:: html
.. code:: python
#@save
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
"""Train a net within one epoch (defined in Chapter 8)."""
state, timer = None, d2l.Timer()
metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens
for X, Y in train_iter:
if state is None or use_random_iter:
# Initialize `state` when either it is the first iteration or
# using random sampling
state = net.begin_state(batch_size=X.shape[0], device=device)
else:
if isinstance(net, nn.Module) and not isinstance(state, tuple):
# `state` is a tensor for `nn.GRU`
state.detach_()
else:
# `state` is a tuple of tensors for `nn.LSTM` and
# for our custom scratch implementation
for s in state:
s.detach_()
y = Y.T.reshape(-1)
X, y = X.to(device), y.to(device)
y_hat, state = net(X, state)
l = loss(y_hat, y.long()).mean()
if isinstance(updater, torch.optim.Optimizer):
updater.zero_grad()
l.backward()
grad_clipping(net, 1)
updater.step()
else:
l.backward()
grad_clipping(net, 1)
# Since the `mean` function has been invoked
updater(batch_size=1)
metric.add(l * y.numel(), y.numel())
return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
.. raw:: html
.. raw:: html
.. code:: python
#@save
def train_epoch_ch8(net, train_iter, loss, updater, params, use_random_iter):
"""Train a model within one epoch (defined in Chapter 8)."""
state, timer = None, d2l.Timer()
metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens
for X, Y in train_iter:
if state is None or use_random_iter:
# Initialize `state` when either it is the first iteration or
# using random sampling
state = net.begin_state(batch_size=X.shape[0])
with tf.GradientTape(persistent=True) as g:
g.watch(params)
y_hat, state= net(X, state, params)
y = tf.reshape(tf.transpose(Y), (-1))
l = loss(y, y_hat)
grads = g.gradient(l, params)
grads = grad_clipping(grads, 1)
updater.apply_gradients(zip(grads, params))
# Keras loss by default returns the average loss in a batch
# l_sum = l * float(d2l.size(y)) if isinstance(
# loss, tf.keras.losses.Loss) else tf.reduce_sum(l)
metric.add(l * d2l.size(y), d2l.size(y))
return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
.. raw:: html
.. raw:: html
A função de treinamento suporta um modelo RNN implementado ou do zero ou
usando APIs de alto nível.
.. raw:: html
.. raw:: html
.. code:: python
def train_ch8(net, train_iter, vocab, lr, num_epochs, device, #@save
use_random_iter=False):
"""Train a model (defined in Chapter 8)."""
loss = gluon.loss.SoftmaxCrossEntropyLoss()
animator = d2l.Animator(xlabel='epoch', ylabel='perplexity',
legend=['train'], xlim=[10, num_epochs])
# Initialize
if isinstance(net, gluon.Block):
net.initialize(ctx=device, force_reinit=True,
init=init.Normal(0.01))
trainer = gluon.Trainer(net.collect_params(),
'sgd', {'learning_rate': lr})
updater = lambda batch_size: trainer.step(batch_size)
else:
updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size)
predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device)
# Train and predict
for epoch in range(num_epochs):
ppl, speed = train_epoch_ch8(
net, train_iter, loss, updater, device, use_random_iter)
if (epoch + 1) % 10 == 0:
animator.add(epoch + 1, [ppl])
print(f'perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}')
print(predict('time traveller'))
print(predict('traveller'))
.. raw:: html
.. raw:: html
.. code:: python
#@save
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,
use_random_iter=False):
"""Train a model (defined in Chapter 8)."""
loss = nn.CrossEntropyLoss()
animator = d2l.Animator(xlabel='epoch', ylabel='perplexity',
legend=['train'], xlim=[10, num_epochs])
# Initialize
if isinstance(net, nn.Module):
updater = torch.optim.SGD(net.parameters(), lr)
else:
updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size)
predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device)
# Train and predict
for epoch in range(num_epochs):
ppl, speed = train_epoch_ch8(
net, train_iter, loss, updater, device, use_random_iter)
if (epoch + 1) % 10 == 0:
print(predict('time traveller'))
animator.add(epoch + 1, [ppl])
print(f'perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}')
print(predict('time traveller'))
print(predict('traveller'))
.. raw:: html
.. raw:: html
.. code:: python
#@save
def train_ch8(net, train_iter, vocab, num_hiddens, lr, num_epochs, strategy,
use_random_iter=False):
"""Train a model (defined in Chapter 8)."""
with strategy.scope():
params = get_params(len(vocab), num_hiddens)
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
updater = tf.keras.optimizers.SGD(lr)
animator = d2l.Animator(xlabel='epoch', ylabel='perplexity',
legend=['train'], xlim=[10, num_epochs])
predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, params)
# Train and predict
for epoch in range(num_epochs):
ppl, speed = train_epoch_ch8(
net, train_iter, loss, updater, params, use_random_iter)
if (epoch + 1) % 10 == 0:
print(predict('time traveller'))
animator.add(epoch + 1, [ppl])
device = d2l.try_gpu()._device_name
print(f'perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}')
print(predict('time traveller'))
print(predict('traveller'))
.. raw:: html
.. raw:: html
Agora podemos treinar o modelo RNN. Como usamos apenas 10.000 tokens no
conjunto de dados, o modelo precisa de mais épocas para convergir
melhor.
.. raw:: html
.. raw:: html
.. code:: python
num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu())
.. parsed-literal::
:class: output
perplexity 1.0, 24455.4 tokens/sec on gpu(0)
time traveller after the pauserequired for the proper assimilati
travelleryou can show black is white by argument said filby
.. figure:: output_rnn-scratch_546c4d_160_1.svg
.. raw:: html
.. raw:: html
.. code:: python
num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu())
.. parsed-literal::
:class: output
perplexity 1.0, 62511.3 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_rnn-scratch_546c4d_163_1.svg
.. raw:: html
.. raw:: html
.. code:: python
num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, num_hiddens, lr, num_epochs, strategy)
.. parsed-literal::
:class: output
perplexity 1.0, 15630.7 tokens/sec on /GPU:0
time traveller for so it will be convenient to speak of himwas e
traveller with a slight accession ofcheerfulness really thi
.. figure:: output_rnn-scratch_546c4d_166_1.svg
.. raw:: html
.. raw:: html
Finalmente, vamos verificar os resultados do uso do método de amostragem
aleatória.
.. raw:: html
.. raw:: html
.. code:: python
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu(),
use_random_iter=True)
.. parsed-literal::
:class: output
perplexity 1.3, 22753.1 tokens/sec on gpu(0)
time traveller held in his hand was a glitteringmetallic framewo
traveller but now you begin to seethe object of my investig
.. figure:: output_rnn-scratch_546c4d_172_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu(),
use_random_iter=True)
.. parsed-literal::
:class: output
perplexity 1.3, 59672.6 tokens/sec on cuda:0
time travellerit s against reason said filbywhat os have been at
travellerit s against reason said filbywhat os have been at
.. figure:: output_rnn-scratch_546c4d_175_1.svg
.. raw:: html
.. raw:: html
.. code:: python
params = get_params(len(vocab_random_iter), num_hiddens)
train_ch8(net, train_random_iter, vocab_random_iter, num_hiddens, lr,
num_epochs, strategy, use_random_iter=True)
.. parsed-literal::
:class: output
perplexity 1.5, 14944.2 tokens/sec on /GPU:0
time traveller came back andfilby s anecdote collapsedthe thing
traveller came back andfilby s anecdote collapsedthe thing
.. figure:: output_rnn-scratch_546c4d_178_1.svg
.. raw:: html
.. raw:: html
Embora a implementação do modelo RNN acima do zero seja instrutiva, não
é conveniente. Na próxima seção, veremos como melhorar o modelo RNN, por
exemplo, como torná-lo mais fácil de implementar e fazê-lo funcionar
mais rápido.
Resumo
------
- Podemos treinar um modelo de linguagem de nível de caractere baseado
em RNN para gerar texto seguindo o prefixo de texto fornecido pelo
usuário.
- Um modelo de linguagem RNN simples consiste em codificação de
entrada, modelagem RNN e geração de saída.
- Os modelos RNN precisam de inicialização de estado para treinamento,
embora a amostragem aleatória e o particionamento sequencial usem
maneiras diferentes.
- Ao usar o particionamento sequencial, precisamos separar o gradiente
para reduzir o custo computacional.
- Um período de aquecimento permite que um modelo se atualize (por
exemplo, obtenha um estado oculto melhor do que seu valor
inicializado) antes de fazer qualquer previsão.
- O recorte de gradiente evita a explosão do gradiente, mas não pode
corrigir gradientes que desaparecem.
Exercícios
----------
1. Mostre que a codificação one-hot é equivalente a escolher uma
incorporação diferente para cada objeto.
2. Ajuste os hiperparâmetros (por exemplo, número de épocas, número de
unidades ocultas, número de etapas de tempo em um minibatch e taxa de
aprendizado) para melhorar a perplexidade.
- Quão baixo você pode ir?
- Substitua a codificação one-hot por *embeddings* que podem ser
aprendidos. Isso leva a um melhor desempenho?
- Será que funcionará bem em outros livros de H. G. Wells, por
exemplo, `The War of the
Worlds `__?
3. Modifique a função de previsão para usar amostragem em vez de
escolher o próximo caractere mais provável.
- O que acontece?
- Desvie o modelo para resultados mais prováveis, por exemplo,
amostrando de
:math:`q(x_t \mid x_{t-1}, \ldots, x_1) \propto P(x_t \mid x_{t-1}, \ldots, x_1)^\alpha`
para :math:`\alpha > 1`.
4. Execute o código nesta seção sem cortar o gradiente. O que acontece?
5. Altere o particionamento sequencial para que não separe os estados
ocultos do gráfico computacional. O tempo de execução muda? Que tal a
perplexidade?
6. Substitua a função de ativação usada nesta seção por ReLU e repita os
experimentos nesta seção. Ainda precisamos de recorte de gradiente?
Por quê?
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
.. raw:: html