到目前为止,我们的序列学习任务的工作示例是语言建模,我们的目标是在给定序列中所有先前标记的情况下预测下一个标记。在这种情况下,我们只希望以左向上下文为条件,因此标准 RNN 的单向链接似乎是合适的。然而,还有许多其他序列学习任务上下文,在这些上下文中,在向左和向右上下文的每个时间步调整预测是非常好的。例如,考虑词性检测。在评估与给定词相关的词性时,为什么我们不应该考虑两个方向的上下文?
另一项常见任务(通常在针对感兴趣的实际任务微调模型之前用作预训练练习)是屏蔽文本文档中的随机标记,然后训练序列模型以预测缺失标记的值。请注意,根据空白后面的内容,缺失标记的可能值会发生显着变化:
我是___。
我___饿了。
我___饿了,我能吃半头猪。
在第一句话中,“快乐”似乎是一个可能的候选者。“不”和“非常”这两个词在第二句中似乎说得通,但“不”与第三句似乎格格不入。
幸运的是,一种简单的技术可以将任何单向 RNN 转换为双向 RNN (Schuster 和 Paliwal,1997)。我们简单地实现两个单向 RNN 层,它们以相反的方向链接在一起并作用于相同的输入(图 10.4.1)。对于第一个 RNN 层,第一个输入是x1最后的输入是 xT,但是对于第二个 RNN 层,第一个输入是 xT最后的输入是x1. 为了产生这个双向 RNN 层的输出,我们只需将两个底层单向 RNN 层的相应输出连接在一起。
图 10.4.1双向 RNN 的架构。
正式地为任何时间步长t,我们考虑一个小批量输入 Xt∈Rn×d(示例数量: n,每个示例中的输入数量:d) 并令隐藏层激活函数为ϕ. 在双向架构中,这个时间步长的前向和后向隐藏状态是H→t∈Rn×h 和H←t∈Rn×h,分别在哪里h是隐藏单元的数量。前向和后向隐藏状态更新如下:
(10.4.1)H→t=ϕ(XtWxh(f)+H→t−1Whh(f)+bh(f)),H←t=ϕ(XtWxh(b)+H←t+1Whh(b)+bh(b)),
权重在哪里 Wxh(f)∈Rd×h,Whh(f)∈Rh×h,Wxh(b)∈Rd×h,andWhh(b)∈Rh×h, 和偏见bh(f)∈R1×h和 bh(b)∈R1×h都是模型参数。
接下来,我们连接前向和后向隐藏状态 H→t和 H←t获得隐藏状态 Ht∈Rn×2h送入输出层。在具有多个隐藏层的深度双向 RNN 中,此类信息作为输入传递到下一个双向层。最后,输出层计算输出 Ot∈Rn×q(输出数量: q):
(10.4.2)Ot=HtWhq+bq.
这里,权重矩阵 Whq∈R2h×q和偏见 bq∈R1×q是输出层的模型参数。虽然从技术上讲,两个方向可以有不同数量的隐藏单元,但在实践中很少做出这种设计选择。我们现在演示双向 RNN 的简单实现。
import torch from torch import nn from d2l import torch as d2l
from mxnet import np, npx from mxnet.gluon import rnn from d2l import mxnet as d2l npx.set_np()
from jax import numpy as jnp from d2l import jax as d2l
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
import tensorflow as tf from d2l import tensorflow as d2l
10.4.1。从零开始实施
要从头开始实现双向 RNN,我们可以包含两个RNNScratch具有独立可学习参数的单向实例。
class BiRNNScratch(d2l.Module): def __init__(self, num_inputs, num_hiddens, sigma=0.01): super().__init__() self.save_hyperparameters() self.f_rnn = d2l.RNNScratch(num_inputs, num_hiddens, sigma) self.b_rnn = d2l.RNNScratch(num_inputs, num_hiddens, sigma) self.num_hiddens *= 2 # The output dimension will be doubled
class BiRNNScratch(d2l.Module): def __init__(self, num_inputs, num_hiddens, sigma=0.01): super().__init__() self.save_hyperparameters() self.f_rnn = d2l.RNNScratch(num_inputs, num_hiddens, sigma) self.b_rnn = d2l.RNNScratch(num_inputs, num_hiddens, sigma) self.num_hiddens *= 2 # The output dimension will be doubled
class BiRNNScratch(d2l.Module): num_inputs: int num_hiddens: int sigma: float = 0.01 def setup(self): self.f_rnn = d2l.RNNScratch(num_inputs, num_hiddens, sigma) self.b_rnn = d2l.RNNScratch(num_inputs, num_hiddens, sigma) self.num_hiddens *= 2 # The output dimension will be doubled
class BiRNNScratch(d2l.Module): def __init__(self, num_inputs, num_hiddens, sigma=0.01): super().__init__() self.save_hyperparameters() self.f_rnn = d2l.RNNScratch(num_inputs, num_hiddens, sigma) self.b_rnn = d2l.RNNScratch(num_inputs, num_hiddens, sigma) self.num_hiddens *= 2 # The output dimension will be doubled
前向和后向 RNN 的状态分别更新,而这两个 RNN 的输出被连接起来。
@d2l.add_to_class(BiRNNScratch) def forward(self, inputs, Hs=None): f_H, b_H = Hs if Hs is not None else (None, None) f_outputs, f_H = self.f_rnn(inputs, f_H) b_outputs, b_H = self.b_rnn(reversed(inputs), b_H) outputs = [torch.cat((f, b), -1) for f, b in zip( f_outputs, reversed(b_outputs))] return outputs, (f_H, b_H)
@d2l.add_to_class(BiRNNScratch) def forward(self, inputs, Hs=None): f_H, b_H = Hs if Hs is not None else (None, None) f_outputs, f_H = self.f_rnn(inputs, f_H) b_outputs, b_H = self.b_rnn(reversed(inputs), b_H) outputs = [np.concatenate((f, b), -1) for f, b in zip( f_outputs, reversed(b_outputs))] return outputs, (f_H, b_H)
@d2l.add_to_class(BiRNNScratch) def forward(self, inputs, Hs=None): f_H, b_H = Hs if Hs is not None else (None, None) f_outputs, f_H = self.f_rnn(inputs, f_H) b_outputs, b_H = self.b_rnn(reversed(inputs), b_H) outputs = [jnp.concatenate((f, b), -1) for f, b in zip( f_outputs, reversed(b_outputs))] return outputs, (f_H, b_H)
@d2l.add_to_class(BiRNNScratch) def forward(self, inputs, Hs=None): f_H, b_H = Hs if Hs is not None else (None, None) f_outputs, f_H = self.f_rnn(inputs, f_H) b_outputs, b_H = self.b_rnn(reversed(inputs), b_H) outputs = [tf.concat((f, b), -1) for f, b in zip( f_outputs, reversed(b_outputs))] return outputs, (f_H, b_H)
10.4.2。简洁的实现
使用高级 API,我们可以更简洁地实现双向 RNN。这里我们以一个 GRU 模型为例。
class BiGRU(d2l.RNN): def __init__(self, num_inputs, num_hiddens): d2l.Module.__init__(self) self.save_hyperparameters() self.rnn = nn.GRU(num_inputs, num_hiddens, bidirectional=True) self.num_hiddens *= 2
Using the high-level APIs, we can implement bidirectional RNNs more concisely. Here we take a GRU model as an example.
class BiGRU(d2l.RNN): def __init__(self, num_inputs, num_hiddens): d2l.Module.__init__(self) self.save_hyperparameters() self.rnn = rnn.GRU(num_hiddens, bidirectional=True) self.num_hiddens *= 2
Flax API does not offer RNN layers and hence there is no notion of any bidirectional argument. One needs to manually reverse the inputs as shown in the scratch implementation, if a bidirectional layer is needed.
Using the high-level APIs, we can implement bidirectional RNNs more concisely. Here we take a GRU model as an example.
10.4.3。概括
在双向 RNN 中,每个时间步的隐藏状态同时由当前时间步之前和之后的数据确定。双向 RNN 主要用于序列编码和给定双向上下文的观察估计。由于长梯度链,双向 RNN 的训练成本非常高。
10.4.4。练习
如果不同方向使用不同数量的隐藏单元,形状将如何Ht改变?
设计具有多个隐藏层的双向 RNN。
多义现象在自然语言中很常见。例如,“银行”一词在“我去银行存款”和“我去银行坐下”的语境中有不同的含义。我们如何设计一个神经网络模型,以便在给定上下文序列和一个词的情况下,返回该词在上下文中的向量表示?哪种类型的神经架构更适合处理多义词?
-
神经网络
+关注
关注
42文章
4771浏览量
100739 -
pytorch
+关注
关注
2文章
808浏览量
13218
发布评论请先 登录
相关推荐
评论