PyTorch教程-9.5. 从零开始的递归神经网络实现

电子说

1.3w人已加入

描述

我们现在准备好从头开始实施 RNN。特别是,我们将训练此 RNN 作为字符级语言模型(参见 第 9.4 节),并按照第 9.2 节中概述的数据处理步骤,在由 HG Wells 的《时间机器》的整个文本组成的语料库上对其进行训练. 我们首先加载数据集。

 

%matplotlib inline
import math
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

 

 

%matplotlib inline
import math
from mxnet import autograd, gluon, np, npx
from d2l import mxnet as d2l

npx.set_np()

 

 

%matplotlib inline
import math
import jax
from flax import linen as nn
from jax import numpy as jnp
from d2l import jax as d2l

 

 

%matplotlib inline
import math
import tensorflow as tf
from d2l import tensorflow as d2l

 

9.5.1. 循环神经网络模型

我们首先定义一个类来实现 RNN 模型(第 9.4.2 节)。请注意,隐藏单元的数量num_hiddens是一个可调的超参数。

 

class RNNScratch(d2l.Module): #@save
  """The RNN model implemented from scratch."""
  def __init__(self, num_inputs, num_hiddens, sigma=0.01):
    super().__init__()
    self.save_hyperparameters()
    self.W_xh = nn.Parameter(
      torch.randn(num_inputs, num_hiddens) * sigma)
    self.W_hh = nn.Parameter(
      torch.randn(num_hiddens, num_hiddens) * sigma)
    self.b_h = nn.Parameter(torch.zeros(num_hiddens))

 

 

class RNNScratch(d2l.Module): #@save
  """The RNN model implemented from scratch."""
  def __init__(self, num_inputs, num_hiddens, sigma=0.01):
    super().__init__()
    self.save_hyperparameters()
    self.W_xh = np.random.randn(num_inputs, num_hiddens) * sigma
    self.W_hh = np.random.randn(
      num_hiddens, num_hiddens) * sigma
    self.b_h = np.zeros(num_hiddens)

 

 

class RNNScratch(nn.Module): #@save
  """The RNN model implemented from scratch."""
  num_inputs: int
  num_hiddens: int
  sigma: float = 0.01

  def setup(self):
    self.W_xh = self.param('W_xh', nn.initializers.normal(self.sigma),
                (self.num_inputs, self.num_hiddens))
    self.W_hh = self.param('W_hh', nn.initializers.normal(self.sigma),
                (self.num_hiddens, self.num_hiddens))
    self.b_h = self.param('b_h', nn.initializers.zeros, (self.num_hiddens))

 

 

class RNNScratch(d2l.Module): #@save
  """The RNN model implemented from scratch."""
  def __init__(self, num_inputs, num_hiddens, sigma=0.01):
    super().__init__()
    self.save_hyperparameters()
    self.W_xh = tf.Variable(tf.random.normal(
      (num_inputs, num_hiddens)) * sigma)
    self.W_hh = tf.Variable(tf.random.normal(
      (num_hiddens, num_hiddens)) * sigma)
    self.b_h = tf.Variable(tf.zeros(num_hiddens))

 

下面的方法forward定义了如何计算任何时间步的输出和隐藏状态,给定当前输入和模型在前一个时间步的状态。请注意,RNN 模型循环遍历 的最外层维度inputs,一次更新隐藏状态。这里的模型使用了tanh激活函数(第 5.1.2.3 节)。

 

@d2l.add_to_class(RNNScratch) #@save
def forward(self, inputs, state=None):
  if state is None:
    # Initial state with shape: (batch_size, num_hiddens)
    state = torch.zeros((inputs.shape[1], self.num_hiddens),
             device=inputs.device)
  else:
    state, = state
  outputs = []
  for X in inputs: # Shape of inputs: (num_steps, batch_size, num_inputs)
    state = torch.tanh(torch.matmul(X, self.W_xh) +
             torch.matmul(state, self.W_hh) + self.b_h)
    outputs.append(state)
  return outputs, state

 

 

@d2l.add_to_class(RNNScratch) #@save
def forward(self, inputs, state=None):
  if state is None:
    # Initial state with shape: (batch_size, num_hiddens)
    state = np.zeros((inputs.shape[1], self.num_hiddens),
             ctx=inputs.ctx)
  else:
    state, = state
  outputs = []
  for X in inputs: # Shape of inputs: (num_steps, batch_size, num_inputs)
    state = np.tanh(np.dot(X, self.W_xh) +
             np.dot(state, self.W_hh) + self.b_h)
    outputs.append(state)
  return outputs, state

 

 

@d2l.add_to_class(RNNScratch) #@save
def __call__(self, inputs, state=None):
  if state is not None:
    state, = state
  outputs = []
  for X in inputs: # Shape of inputs: (num_steps, batch_size, num_inputs)
    state = jnp.tanh(jnp.matmul(X, self.W_xh) + (
      jnp.matmul(state, self.W_hh) if state is not None else 0)
             + self.b_h)
    outputs.append(state)
  return outputs, state

 

 

@d2l.add_to_class(RNNScratch) #@save
def forward(self, inputs, state=None):
  if state is None:
    # Initial state with shape: (batch_size, num_hiddens)
    state = tf.zeros((inputs.shape[1], self.num_hiddens))
  else:
    state, = state
    state = tf.reshape(state, (-1, self.num_hiddens))
  outputs = []
  for X in inputs: # Shape of inputs: (num_steps, batch_size, num_inputs)
    state = tf.tanh(tf.matmul(X, self.W_xh) +
             tf.matmul(state, self.W_hh) + self.b_h)
    outputs.append(state)
  return outputs, state

 

我们可以将一小批输入序列输入 RNN 模型,如下所示。

 

batch_size, num_inputs, num_hiddens, num_steps = 2, 16, 32, 100
rnn = RNNScratch(num_inputs, num_hiddens)
X = torch.ones((num_steps, batch_size, num_inputs))
outputs, state = rnn(X)

 

 

batch_size, num_inputs, num_hiddens, num_steps = 2, 16, 32, 100
rnn = RNNScratch(num_inputs, num_hiddens)
X = np.ones((num_steps, batch_size, num_inputs))
outputs, state = rnn(X)

 

 

batch_size, num_inputs, num_hiddens, num_steps = 2, 16, 32, 100
rnn = RNNScratch(num_inputs, num_hiddens)
X = jnp.ones((num_steps, batch_size, num_inputs))
(outputs, state), _ = rnn.init_with_output(d2l.get_key(), X)

 

 

batch_size, num_inputs, num_hiddens, num_steps = 2, 16, 32, 100
rnn = RNNScratch(num_inputs, num_hiddens)
X = tf.ones((num_steps, batch_size, num_inputs))
outputs, state = rnn(X)

 

让我们检查一下 RNN 模型是否产生了正确形状的结果,以确保隐藏状态的维数保持不变。

 

def check_len(a, n): #@save
  """Check the length of a list."""
  assert len(a) == n, f'list's length {len(a)} != expected length {n}'

def check_shape(a, shape): #@save
  """Check the shape of a tensor."""
  assert a.shape == shape, 
      f'tensor's shape {a.shape} != expected shape {shape}'

check_len(outputs, num_steps)
check_shape(outputs[0], (batch_size, num_hiddens))
check_shape(state, (batch_size, num_hiddens))

 

9.5.2. 基于循环神经网络的语言模型

下面的类定义了一个基于 RNN 的语言模型,我们通过方法的参数 RNNLMScratch传入我们的 RNN 。在训练语言模型时,输入和输出来自相同的词汇表。因此,它们具有相同的维度,即词汇量大小。请注意,我们使用困惑来评估模型。正如 第 9.3.2 节中所讨论的,这确保了不同长度的序列是可比较的。rnn__init__

 

class RNNLMScratch(d2l.Classifier): #@save
  """The RNN-based language model implemented from scratch."""
  def __init__(self, rnn, vocab_size, lr=0.01):
    super().__init__()
    self.save_hyperparameters()
    self.init_params()

  def init_params(self):
    self.W_hq = nn.Parameter(
      torch.randn(
        self.rnn.num_hiddens, self.vocab_size) * self.rnn.sigma)
    self.b_q = nn.Parameter(torch.zeros(self.vocab_size))

  def training_step(self, batch):
    l = self.loss(self(*batch[:-1]), batch[-1])
    self.plot('ppl', torch.exp(l), train=True)
    return l

  def validation_step(self, batch):
    l = self.loss(self(*batch[:-1]), batch[-1])
    self.plot('ppl', torch.exp(l), train=False)

 

 

class RNNLMScratch(d2l.Classifier): #@save
  """The RNN-based language model implemented from scratch."""
  def __init__(self, rnn, vocab_size, lr=0.01):
    super().__init__()
    self.save_hyperparameters()
    self.init_params()

  def init_params(self):
    self.W_hq = np.random.randn(
      self.rnn.num_hiddens, self.vocab_size) * self.rnn.sigma
    self.b_q = np.zeros(self.vocab_size)
    for param in self.get_scratch_params():
      param.attach_grad()
  def training_step(self, batch):
    l = self.loss(self(*batch[:-1]), batch[-1])
    self.plot('ppl', np.exp(l), train=True)
    return l

  def validation_step(self, batch):
    l = self.loss(self(*batch[:-1]), batch[-1])
    self.plot('ppl', np.exp(l), train=False)

 

 

class RNNLMScratch(d2l.Classifier): #@save
  """The RNN-based language model implemented from scratch."""
  rnn: nn.Module
  vocab_size: int
  lr: float = 0.01

  def setup(self):
    self.W_hq = self.param('W_hq', nn.initializers.normal(self.rnn.sigma),
                (self.rnn.num_hiddens, self.vocab_size))
    self.b_q = self.param('b_q', nn.initializers.zeros, (self.vocab_size))

  def training_step(self, params, batch, state):
    value, grads = jax.value_and_grad(
      self.loss, has_aux=True)(params, batch[:-1], batch[-1], state)
    l, _ = value
    self.plot('ppl', jnp.exp(l), train=True)
    return value, grads

  def validation_step(self, params, batch, state):
    l, _ = self.loss(params, batch[:-1], batch[-1], state)
    self.plot('ppl', jnp.exp(l), train=False)

 

 

class RNNLMScratch(d2l.Classifier): #@save
  """The RNN-based language model implemented from scratch."""
  def __init__(self, rnn, vocab_size, lr=0.01):
    super().__init__()
    self.save_hyperparameters()
    self.init_params()

  def init_params(self):
    self.W_hq = tf.Variable(tf.random.normal(
      (self.rnn.num_hiddens, self.vocab_size)) * self.rnn.sigma)
    self.b_q = tf.Variable(tf.zeros(self.vocab_size))

  def training_step(self, batch):
    l = self.loss(self(*batch[:-1]), batch[-1])
    self.plot('ppl', tf.exp(l), train=True)
    return l

  def validation_step(self, batch):
    l = self.loss(self(*batch[:-1]), batch[-1])
    self.plot('ppl', tf.exp(l), train=False)

 

9.5.2.1. 一次性编码

回想一下,每个标记都由一个数字索引表示,该数字索引指示相应单词/字符/单词片段在词汇表中的位置。您可能想构建一个具有单个输入节点(在每个时间步长)的神经网络,其中索引可以作为标量值输入。当我们处理价格或温度等数值输入时,这是有效的,其中任何两个足够接近的值都应该被类似地对待。但这并不完全合理。这45th和 46th我们词汇表中的词恰好是“他们的”和“说的”,它们的含义并不相似。

处理此类分类数据时,最常见的策略是用单热编码表示每个项目(回忆 4.1.1 节)。one-hot 编码是一个向量,其长度由词汇表的大小给出N,其中所有条目都设置为0,除了与我们的令牌对应的条目,它被设置为1. 例如,如果词汇表有 5 个元素,那么索引 0 和 2 对应的单热向量如下。

 

F.one_hot(torch.tensor([0, 2]), 5)

 

 

tensor([[1, 0, 0, 0, 0],
    [0, 0, 1, 0, 0]])

 

 

npx.one_hot(np.array([0, 2]), 5)

 

 

array([[1., 0., 0., 0., 0.],
    [0., 0., 1., 0., 0.]])

 

 

jax.nn.one_hot(jnp.array([0, 2]), 5)

 

 

Array([[1., 0., 0., 0., 0.],
    [0., 0., 1., 0., 0.]], dtype=float32)

 

 

tf.one_hot(tf.constant([0, 2]), 5)

 

 


 

我们在每次迭代中采样的小批量将采用形状(批量大小、时间步数)。一旦将每个输入表示为一个单热向量,我们就可以将每个小批量视为一个三维张量,其中沿第三轴的长度由词汇表大小 ( ) 给出len(vocab)。我们经常转置输入,以便获得形状的输出(时间步数、批量大小、词汇量大小)。这将允许我们更方便地循环遍历最外层维度以更新小批量的隐藏状态,时间步长(例如,在上述方法中forward)。

 

@d2l.add_to_class(RNNLMScratch) #@save
def one_hot(self, X):
  # Output shape: (num_steps, batch_size, vocab_size)
  return F.one_hot(X.T, self.vocab_size).type(torch.float32)

 

 

@d2l.add_to_class(RNNLMScratch) #@save
def one_hot(self, X):
  # Output shape: (num_steps, batch_size, vocab_size)
  return npx.one_hot(X.T, self.vocab_size)

 

 

@d2l.add_to_class(RNNLMScratch) #@save
def one_hot(self, X):
  # Output shape: (num_steps, batch_size, vocab_size)
  return jax.nn.one_hot(X.T, self.vocab_size)

 

 

@d2l.add_to_class(RNNLMScratch) #@save
def one_hot(self, X):
  # Output shape: (num_steps, batch_size, vocab_size)
  return tf.one_hot(tf.transpose(X), self.vocab_size)

 

9.5.2.2. 转换 RNN 输出

语言模型使用全连接输出层将 RNN 输出转换为每个时间步的标记预测。

 

@d2l.add_to_class(RNNLMScratch) #@save
def output_layer(self, rnn_outputs):
  outputs = [torch.matmul(H, self.W_hq) + self.b_q for H in rnn_outputs]
  return torch.stack(outputs, 1)

@d2l.add_to_class(RNNLMScratch) #@save
def forward(self, X, state=None):
  embs = self.one_hot(X)
  rnn_outputs, _ = self.rnn(embs, state)
  return self.output_layer(rnn_outputs)

 

 

@d2l.add_to_class(RNNLMScratch) #@save
def output_layer(self, rnn_outputs):
  outputs = [np.dot(H, self.W_hq) + self.b_q for H in rnn_outputs]
  return np.stack(outputs, 1)

@d2l.add_to_class(RNNLMScratch) #@save
def forward(self, X, state=None):
  embs = self.one_hot(X)
  rnn_outputs, _ = self.rnn(embs, state)
  return self.output_layer(rnn_outputs)

 

 

@d2l.add_to_class(RNNLMScratch) #@save
def output_layer(self, rnn_outputs):
  outputs = [jnp.matmul(H, self.W_hq) + self.b_q for H in rnn_outputs]
  return jnp.stack(outputs, 1)

@d2l.add_to_class(RNNLMScratch) #@save
def forward(self, X, state=None):
  embs = self.one_hot(X)
  rnn_outputs, _ = self.rnn(embs, state)
  return self.output_layer(rnn_outputs)

 

 

@d2l.add_to_class(RNNLMScratch) #@save
def output_layer(self, rnn_outputs):
  outputs = [tf.matmul(H, self.W_hq) + self.b_q for H in rnn_outputs]
  return tf.stack(outputs, 1)

@d2l.add_to_class(RNNLMScratch) #@save
def forward(self, X, state=None):
  embs = self.one_hot(X)
  rnn_outputs, _ = self.rnn(embs, state)
  return self.output_layer(rnn_outputs)

 

让我们检查前向计算是否产生具有正确形状的输出。

 

model = RNNLMScratch(rnn, num_inputs)
outputs = model(torch.ones((batch_size, num_steps), dtype=torch.int64))
check_shape(outputs, (batch_size, num_steps, num_inputs))

 

 

model = RNNLMScratch(rnn, num_inputs)
outputs = model(np.ones((batch_size, num_steps), dtype=np.int64))
check_shape(outputs, (batch_size, num_steps, num_inputs))

 

 

model = RNNLMScratch(rnn, num_inputs)
outputs, _ = model.init_with_output(d2l.get_key(),
                  jnp.ones((batch_size, num_steps),
                       dtype=jnp.int32))
check_shape(outputs, (batch_size, num_steps, num_inputs))

 

 

model = RNNLMScratch(rnn, num_inputs)
outputs = model(tf.ones((batch_size, num_steps), dtype=tf.int64))
check_shape(outputs, (batch_size, num_steps, num_inputs))

 

9.5.3. 渐变剪裁

虽然您已经习惯于将神经网络视为“深度”网络,即许多层甚至在单个时间步内将输入和输出分开,但序列的长度引入了新的深度概念。除了在输入到输出方向上通过网络之外,第一个时间步的输入必须通过一系列T沿着时间步长分层,以影响模型在最后时间步长的输出。从后向的角度来看,在每次迭代中,我们通过时间反向传播梯度,从而产生一系列具有长度的矩阵积 O(T). 如第 5.4 节所述 ,这会导致数值不稳定,导致梯度根据权重矩阵的属性爆炸或消失。

处理梯度消失和爆炸是设计 RNN 时的一个基本问题,并激发了现代神经网络架构中一些最大的进步。在下一章中,我们将讨论旨在缓解梯度消失问题的专门架构。然而,即使是现代 RNN 仍然经常遭受梯度爆炸的困扰。一种不优雅但普遍存在的解决方案是简单地裁剪梯度,强制生成的“裁剪”梯度采用较小的值。

一般来说,当通过梯度下降优化一些目标时,我们迭代地更新感兴趣的参数,比如一个向量 x, 但将它推向负梯度方向g(在随机梯度下降中,我们在随机采样的小批量上计算这个梯度)。例如,学习率η>0, 每次更新都采用以下形式 x←x−ηg. 让我们进一步假设目标函数f足够光滑。形式上,我们说目标是Lipschitz 连续的L,意味着对于任何x和 y, 我们有

(9.5.1)|f(x)−f(y)|≤L‖x−y‖.

如您所见,当我们通过减去更新参数向量时 ηg,目标值的变化取决于学习率,梯度的范数和L如下:

(9.5.2)|f(x)−f(x−ηg)|≤Lη‖g‖.

换句话说,目标的变化不能超过 Lη‖g‖. 此上限值较小可能被视为好事或坏事。不利的一面是,我们限制了降低目标价值的速度。从好的方面来说,这限制了我们在任何一个梯度步骤中可能出错的程度。

当我们说梯度爆炸时,我们的意思是‖g‖ 变得过大。在这种最坏的情况下,我们可能会在单个梯度步骤中造成如此大的破坏,以至于我们可以撤消在数千次训练迭代过程中取得的所有进展。当梯度如此之大时,神经网络训练通常会发散,无法降低目标值。在其他时候,训练最终会收敛,但由于损失的巨大峰值而变得不稳定。

一种限制大小的方法Lη‖g‖是缩小学习率η到微小的值。这里的一个优势是我们不会对更新产生偏见。但是,如果我们很少获得大梯度怎么办?这种激烈的举动减慢了我们在所有步骤中的进度,只是为了应对罕见的梯度爆炸事件。一种流行的替代方法是采用梯度裁剪启发式投影梯度 g到某个给定半径的球上θ如下:

(9.5.3)g←min(1,θ‖g‖)g.

这确保梯度范数永远不会超过θ并且更新后的梯度完全与原始方向对齐g. 它还具有理想的副作用,即限制任何给定的小批量(以及其中任何给定的样本)对参数向量施加的影响。这赋予了模型一定程度的鲁棒性。需要明确的是,这是一个 hack。梯度裁剪意味着我们并不总是遵循真正的梯度,并且很难对可能的副作用进行分析推理。然而,它是一个非常有用的 hack,并且在大多数深度学习框架的 RNN 实现中被广泛采用。

fit_epoch下面我们定义了一个方法来裁剪渐变,该方法由类的方法调用 d2l.Trainer(参见 第 3.4 节)。请注意,在计算梯度范数时,我们将所有模型参数连接起来,将它们视为一个巨大的参数向量。

 

@d2l.add_to_class(d2l.Trainer) #@save
def clip_gradients(self, grad_clip_val, model):
  params = [p for p in model.parameters() if p.requires_grad]
  norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
  if norm > grad_clip_val:
    for param in params:
      param.grad[:] *= grad_clip_val / norm

 

 

@d2l.add_to_class(d2l.Trainer) #@save
def clip_gradients(self, grad_clip_val, model):
  params = model.parameters()
  if not isinstance(params, list):
    params = [p.data() for p in params.values()]
  norm = math.sqrt(sum((p.grad ** 2).sum() for p in params))
  if norm > grad_clip_val:
    for param in params:
      param.grad[:] *= grad_clip_val / norm

 

 

@d2l.add_to_class(d2l.Trainer) #@save
def clip_gradients(self, grad_clip_val, grads):
  grad_leaves, _ = jax.tree_util.tree_flatten(grads)
  norm = jnp.sqrt(sum(jnp.vdot(x, x) for x in grad_leaves))
  clip = lambda grad: jnp.where(norm < grad_clip_val,
                 grad, grad * (grad_clip_val / norm))
  return jax.tree_util.tree_map(clip, grads)

 

 

@d2l.add_to_class(d2l.Trainer) #@save
def clip_gradients(self, grad_clip_val, grads):
  grad_clip_val = tf.constant(grad_clip_val, dtype=tf.float32)
  new_grads = [tf.convert_to_tensor(grad) if isinstance(
    grad, tf.IndexedSlices) else grad for grad in grads]
  norm = tf.math.sqrt(sum((tf.reduce_sum(grad ** 2)) for grad in new_grads))
  if tf.greater(norm, grad_clip_val):
    for i, grad in enumerate(new_grads):
      new_grads[i] = grad * grad_clip_val / norm
    return new_grads
  return grads

 

9.5.4. 训练

使用时间机器数据集 ( ),我们基于从头开始实施的 RNN ()data训练字符级语言模型 ( )。请注意,我们首先计算梯度,然后裁剪它们,最后使用裁剪的梯度更新模型参数。modelrnn

 

data = d2l.TimeMachine(batch_size=1024, num_steps=32)
rnn = RNNScratch(num_inputs=len(data.vocab), num_hiddens=32)
model = RNNLMScratch(rnn, vocab_size=len(data.vocab), lr=1)
trainer = d2l.Trainer(max_epochs=100, gradient_clip_val=1, num_gpus=1)
trainer.fit(model, data)

 

pytorch

 

data = d2l.TimeMachine(batch_size=1024, num_steps=32)
rnn = RNNScratch(num_inputs=len(data.vocab), num_hiddens=32)
model = RNNLMScratch(rnn, vocab_size=len(data.vocab), lr=1)
trainer = d2l.Trainer(max_epochs=100, gradient_clip_val=1, num_gpus=1)
trainer.fit(model, data)

 

pytorch

 

data = d2l.TimeMachine(batch_size=1024, num_steps=32)
rnn = RNNScratch(num_inputs=len(data.vocab), num_hiddens=32)
model = RNNLMScratch(rnn, vocab_size=len(data.vocab), lr=1)
trainer = d2l.Trainer(max_epochs=100, gradient_clip_val=1, num_gpus=1)
trainer.fit(model, data)

 

pytorch

 

data = d2l.TimeMachine(batch_size=1024, num_steps=32)
with d2l.try_gpu():
  rnn = RNNScratch(num_inputs=len(data.vocab), num_hiddens=32)
  model = RNNLMScratch(rnn, vocab_size=len(data.vocab), lr=1)
trainer = d2l.Trainer(max_epochs=100, gradient_clip_val=1)
trainer.fit(model, data)

 

pytorch

9.5.5. 解码

一旦学习了语言模型,我们不仅可以使用它来预测下一个标记,还可以继续预测每个后续标记,将先前预测的标记视为输入中的下一个标记。有时我们只想生成文本,就好像我们从文档的开头开始一样。但是,根据用户提供的前缀来调节语言模型通常很有用。例如,如果我们正在为搜索引擎开发自动完成功能或帮助用户编写电子邮件,我们会希望输入他们到目前为止所写的内容(前缀),然后生成可能的延续。

以下predict方法生成一个延续,一次一个字符,在摄取用户提供的字符后,prefix循环遍历 中的字符时prefix,我们不断将隐藏状态传递到下一个时间步,但不生成任何输出。这称为 预热期。摄取前缀后,我们现在准备开始发出后续字符,每个字符都将作为后续时间步的输入反馈回模型。

 

@d2l.add_to_class(RNNLMScratch) #@save
def predict(self, prefix, num_preds, vocab, device=None):
  state, outputs = None, [vocab[prefix[0]]]
  for i in range(len(prefix) + num_preds - 1):
    X = torch.tensor([[outputs[-1]]], device=device)
    embs = self.one_hot(X)
    rnn_outputs, state = self.rnn(embs, state)
    if i < len(prefix) - 1: # Warm-up period
      outputs.append(vocab[prefix[i + 1]])
    else: # Predict num_preds steps
      Y = self.output_layer(rnn_outputs)
      outputs.append(int(Y.argmax(axis=2).reshape(1)))
  return ''.join([vocab.idx_to_token[i] for i in outputs])

 

 

@d2l.add_to_class(RNNLMScratch) #@save
def predict(self, prefix, num_preds, vocab, device=None):
  state, outputs = None, [vocab[prefix[0]]]
  for i in range(len(prefix) + num_preds - 1):
    X = np.array([[outputs[-1]]], ctx=device)
    embs = self.one_hot(X)
    rnn_outputs, state = self.rnn(embs, state)
    if i < len(prefix) - 1: # Warm-up period
      outputs.append(vocab[prefix[i + 1]])
    else: # Predict num_preds steps
      Y = self.output_layer(rnn_outputs)
      outputs.append(int(Y.argmax(axis=2).reshape(1)))
  return ''.join([vocab.idx_to_token[i] for i in outputs])

 

 

@d2l.add_to_class(RNNLMScratch) #@save
def predict(self, prefix, num_preds, vocab, params):
  state, outputs = None, [vocab[prefix[0]]]
  for i in range(len(prefix) + num_preds - 1):
    X = jnp.array([[outputs[-1]]])
    embs = self.one_hot(X)
    rnn_outputs, state = self.rnn.apply({'params': params['rnn']},
                      embs, state)
    if i < len(prefix) - 1: # Warm-up period
      outputs.append(vocab[prefix[i + 1]])
    else: # Predict num_preds steps
      Y = self.apply({'params': params}, rnn_outputs,
              method=self.output_layer)
      outputs.append(int(Y.argmax(axis=2).reshape(1)))
  return ''.join([vocab.idx_to_token[i] for i in outputs])

 

 

@d2l.add_to_class(RNNLMScratch) #@save
def predict(self, prefix, num_preds, vocab, device=None):
  state, outputs = None, [vocab[prefix[0]]]
  for i in range(len(prefix) + num_preds - 1):
    X = tf.constant([[outputs[-1]]])
    embs = self.one_hot(X)
    rnn_outputs, state = self.rnn(embs, state)
    if i < len(prefix) - 1: # Warm-up period
      outputs.append(vocab[prefix[i + 1]])
    else: # Predict num_preds steps
      Y = self.output_layer(rnn_outputs)
      outputs.append(int(tf.reshape(tf.argmax(Y, axis=2), 1)))
  return ''.join([vocab.idx_to_token[i] for i in outputs])

 

在下文中,我们指定前缀并让它生成 20 个额外的字符。

 

model.predict('it has', 20, data.vocab, d2l.try_gpu())

 

 

'it has of the the the the '

 

 

model.predict('it has', 20, data.vocab, d2l.try_gpu())

 

 

'it has in the the the the '

 

 

model.predict('it has', 20, data.vocab, trainer.state.params)

 

 

'it has in the time tree th'

 

 

model.predict('it has', 20, data.vocab)

 

 

'it has it the the prount o'

 

虽然从头开始实施上述 RNN 模型具有指导意义,但并不方便。在下一节中,我们将了解如何利用深度学习框架来使用标准架构启动 RNN,并通过依赖高度优化的库函数来获得性能提升。

9.5.6. 概括

我们可以训练基于 RNN 的语言模型来生成遵循用户提供的文本前缀的文本。一个简单的 RNN 语言模型由输入编码、RNN 建模和输出生成组成。在训练过程中,梯度裁剪可以减轻梯度爆炸的问题,但不能解决梯度消失的问题。在实验中,我们实现了一个简单的 RNN 语言模型,并在文本序列上使用梯度裁剪对其进行训练,并在字符级别进行标记化。通过以前缀为条件,我们可以使用语言模型来生成可能的延续,这在许多应用程序中被证明是有用的,例如,自动完成功能。

9.5.7. 练习

实施的语言模型是否根据时间机器中的第一个标记之前的所有过去标记预测下一个标记?

哪个超参数控制用于预测的历史长度?

证明 one-hot 编码等同于为每个对象选择不同的嵌入。

调整超参数(例如,epoch 数、隐藏单元数、minibatch 中的时间步数和学习率)以提高困惑度。坚持使用这个简单的架构,你能做到多低?

用可学习的嵌入替换单热编码。这会带来更好的性能吗?

进行实验以确定在时间机器上训练的这种语言模型在 HG Wells 的其他书籍(例如世界大战)中的效果如何。

进行另一项实验以评估此模型对其他作者所写书籍的困惑度。

修改预测方法,例如使用采样而不是选择最有可能的下一个字符。

会发生什么?

将模型偏向更可能的输出,例如,通过从 q(xt∣xt−1,…,x1)∝P(xt∣xt−1,…,x1)α 为了α>1.

在不剪切渐变的情况下运行本节中的代码。会发生什么?

将本节中使用的激活函数替换为 ReLU,并重复本节中的实验。我们还需要梯度裁剪吗?为什么?

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分