在 Colab 中打开GitHub 上执行或查看/下载此 notebook

循环神经网络

循环神经网络 (RNN) 为处理序列提供了一种自然的方法。

在大多数情况下,序列中的元素并非独立。特定输出的生成通常取决于周围的元素甚至整个历史。

为了充分建模序列演变,内存的存在至关重要,以便跟踪过去或未来的元素。内存是通过反馈连接实现的,这引入了“状态”的概念。RNN 依赖于以下等式

\( h_t = f(x_t, h_{t−1}, θ)\)

其中 \(h_t\) 和 \(h_{t−1}\) 是当前和之前状态,\(θ\) 代表可训练参数。

由于这种循环性,当前状态依赖于前一个状态,而前一个状态又依赖于更早的元素,从而建立了对所有之前元素的依赖关系。

1. Vanilla RNN

最简单的 RNN 形式是 Vanilla RNN,由以下等式描述

\( h_t = \tanh(W x_t + Uh_{t−1} + b) \)

在这里,参数 \(θ\) 包括权重矩阵 \(W\) (前馈连接)、矩阵 \(U\) (循环权重) 和向量 \(b\) (偏置)。

为了训练 RNN,需要进行沿时间轴展开的网络,如下图所示

image.png

展开后,RNN 可以被视为一个非常深沿时间轴的前馈神经网络。因此,可以采用训练前馈神经网络相同的算法。有时,在循环神经网络背景下的反向传播算法被称为随时间反向传播,以强调梯度是沿时间轴传播的。

RNN 的一个重要方面是参数在各个时间步长之间共享,这使得模型更容易泛化。现在让我们看一下如何在 SpeechBrain 中使用 Vanilla RNN。首先,让我们安装它并下载一些测试数据。

%%capture
# Installing SpeechBrain via pip
BRANCH = 'develop'
!python -m pip install git+https://github.com/speechbrain/speechbrain.git@$BRANCH

SpeechBrain 在 speechbrain.nnet.RNN 中实现了一系列 RNN。让我们看一个 Vanilla RNN 的示例

import torch
from speechbrain.nnet.RNN import RNN

inp_tensor = torch.rand([4, 10, 20]) # [batch, time, features]
net = RNN(hidden_size=5, input_shape=inp_tensor.shape)
out_tensor, _ = net(inp_tensor)

print(out_tensor.shape)
torch.Size([4, 10, 5])

如您所见,期望的输入必须格式化为 \([batch, time, features]\)。这是 SpeechBrain 中所有实现的神经网络遵循的标准。

输出具有相同的 batch 数(即 4)、相同的时间步长数(即 10)和转换后的特征维度(在这种情况下为 5,与选择的 hidden_size 相同)。

现在让我们看一下参数

for name, param in net.named_parameters():
    if param.requires_grad:
        print(name, param.shape)
rnn.weight_ih_l0 torch.Size([5, 20])
rnn.weight_hh_l0 torch.Size([5, 5])
rnn.bias_ih_l0 torch.Size([5])
rnn.bias_hh_l0 torch.Size([5])
  • 第一个参数是矩阵 \(W\) (输入到隐藏层),维度为 \([5, 20]\),其中 5 是隐藏层大小,20 是输入维度。

  • 第二个参数是 \(U\) (隐藏层到隐藏层),对应于循环权重。它始终是一个方阵。在这种情况下,维度是 \([5,5]\),因为选择了该隐藏层维度。

  • 最后,我们有两个由 hidden_dim 元素组成的向量。这两个张量代表偏置项 \(b\)。在这种情况下,该项被分成两个偏置(一个用于输入,一个用于循环连接)。在其他情况下,使用单个偏置(包含两者)。

当设置 bidirectional=True 时,使用双向 RNN

inp_tensor = torch.rand([4, 10, 20]) # [batch, time, features]
net = RNN(hidden_size=5,
          input_shape=inp_tensor.shape,
          bidirectional=True
          )
out_tensor, _ = net(inp_tensor)

print(out_tensor.shape)
torch.Size([4, 10, 10])

在这种情况下,我们有两个独立的神经网络,分别从左到右和从右到左扫描输入序列。然后将 resulting hidden states 连接成一个“双向”张量。在示例中,特征维度现在是 10,对应于原始隐藏维度的两倍。

在前面的示例中,我们使用了单层 RNN。我们可以通过堆叠更多层来增加模型的特征维度深度

inp_tensor = torch.rand([4, 10, 20]) # [batch, time, features]
net = RNN(hidden_size=5,
          input_shape=inp_tensor.shape,
          bidirectional=True,
          num_layers=3,
          )
out_tensor, _ = net(inp_tensor)

print(out_tensor.shape)
torch.Size([4, 10, 10])

RNN 需要通过许多时间步长反向传播梯度。然而,这个操作可能会因梯度消失和梯度爆炸而变得复杂。这些问题会损害长期依赖的学习。

  • 梯度爆炸可以通过简单的裁剪策略来解决。

  • 相反,梯度消失更为关键。可以通过在网络设计中添加“梯度捷径”来缓解(例如残差网络、跳跃连接或注意力机制)。

RNN 的一种常用方法依赖于乘法门控,其核心思想是引入一种机制,以更好地控制信息流经各个时间步长。

最流行的依赖门控机制的网络是长短期记忆 (LSTM),将在下文描述。

2. 长短期记忆 (LSTM)

LSTM 依赖于由遗忘门、输入门和输出门控制的记忆单元组成的网络设计

\(f_t = \sigma(W_f x_t + U_f h_{t-1} + b_f)\)

\(i_t = \sigma(W_i x_t + U_i h_{t-1} + b_i)\)

\(o_t = \sigma(W_o x_t + U_o h_{t-1} + b_o)\)

\(\widetilde{c}_t = \sigma(W_c x_t + U_c h_{t-1} + b_c)\)

\(c_t = f_t \cdot c_{t-1} + i_t \cdot \widetilde{c}_t \)

\(h_t = o_t \cdot \sigma(c_t)\),

其中 \(\sigma\) 是 sigmoid 函数。

如您所见,网络设计相当复杂,但事实证明该模型用途广泛。

理解为什么这个模型可以学习**长期依赖关系**的最简单方法如下:通过 f_t、i_t 和 o_t 的适当值,我们可以将内部细胞状态 \(c_t\) 存储任意数量的时间步长(如果 \(f_t = 1\)\(i_t=0\))。

让我们看看如何在 SpeechBrain 中使用 LSTM

import torch
from speechbrain.nnet.RNN import LSTM

inp_tensor = torch.rand([4, 10, 20]) # [batch, time, features]
net = LSTM(hidden_size=5, input_shape=inp_tensor.shape)
out_tensor, _ = net(inp_tensor)

print(out_tensor.shape)
torch.Size([4, 10, 5])

如您所见,输入和输出的维度与 Vanilla RNN 相同(当使用相同的 hidden_size 时)。然而,参数数量非常不同

for name, param in net.named_parameters():
    if param.requires_grad:
        print(name, param.shape)
rnn.weight_ih_l0 torch.Size([20, 20])
rnn.weight_hh_l0 torch.Size([20, 5])
rnn.bias_ih_l0 torch.Size([20])
rnn.bias_hh_l0 torch.Size([20])

如您所见,我们将参数组集中到单个大张量中。例如,rnn.weight_ih_l0 汇集了所有四个维度为 [5, 20] 的输入到隐藏层矩阵(对应于 \(f\),\(i\),\(o\),\(c\)),形成一个维度为 \([20,20]\) 的张量。对隐藏层到隐藏层权重 rnn.weight_hh_l0 和偏置项也进行了类似的拼接。

与 Vanilla RNN 类似,我们可以使用参数 bidirectional=True 来使用双向神经网络。我们还可以使用参数 num_layers 堆叠更多层。

3. 门控循环单元 (GRUs)

LSTM 依赖于由遗忘门、输入门和输出门控制的记忆单元。尽管它们有效,但这种复杂的门控机制可能导致模型过于复杂。

一个值得注意的简化 LSTM 的尝试催生了一个名为门控循环单元 (GRU) 的新型模型,该模型仅基于**两个乘法门控**。特别是,GRU 架构由以下等式描述

\(z_{t}=\sigma(W_{z}x_{t}+U_{z}h_{t-1}+b_{z})\)

\(r_{t}=\sigma(W_{r}x_{t}+U_{r}h_{t-1}+b_{r})\)

\(\widetilde{h_{t}} =\tanh(W_{h}x_{t}+U_{h}(h_{t-1} \odot r_{t})+b_{h})\)

\(h_{t}=z_{t} \odot h_{t-1}+ (1-z_{t}) \odot \widetilde{h_{t}}\).

其中 \(z_{t}\)\(r_{t}\) 分别是对应于更新门和重置门的向量,而 \(h_{t}\) 代表当前时间帧 \(t\) 的状态向量。记作 \(\odot\) 的计算表示逐元素相乘。

与 LSTM 类似,GRU 也被设计用于学习长期依赖关系。事实上,GRU 可以将隐藏状态 \(h_t\) 存储任意数量的时间步长。这发生在当 \(z_t = 1\) 时。

现在让我们看看如何在 SpeechBrain 中使用 GRU

import torch
from speechbrain.nnet.RNN import GRU

inp_tensor = torch.rand([4, 10, 20]) # [batch, time, features]
net = GRU(hidden_size=5, input_shape=inp_tensor.shape)
out_tensor, _ = net(inp_tensor)

print(out_tensor.shape)
torch.Size([4, 10, 5])

输出张量的大小与 Vanilla RNN 和 LSTM 相同。模型内部的参数不同

for name, param in net.named_parameters():
    if param.requires_grad:
        print(name, param.shape)
rnn.weight_ih_l0 torch.Size([15, 20])
rnn.weight_hh_l0 torch.Size([15, 5])
rnn.bias_ih_l0 torch.Size([15])
rnn.bias_hh_l0 torch.Size([15])

与 LSTM 类似,GRU 模型也将权重矩阵集中到更大的张量中。在这种情况下,权重矩阵更小,因为我们只有两个门。您可以调整 bidirectional 和 num_layers 参数来使用双向 RNN 或增加更多层。

4. 轻量门控循环单元 (LiGRU)

尽管 GRU 取得了令人关注的性能,通常与 LSTM 相当,但仍有可能进一步简化模型。

最近,提出了一个名为轻量 GRU 的模型,并在语音处理任务中表现良好。该模型基于单个乘法门控,并由以下等式描述

\(z_{t}=\sigma(BN(W_{z}x_{t})+U_{z}h_{t-1})\)

\(\widetilde{h_{t}}=\mbox{ReLU}(BN(W_{h}x_{t})+U_{h}h_{t-1})\)

\(h_{t}=z_{t} \odot h_{t-1}+ (1-z_{t}) \odot \widetilde{h_{t}}\)

LiGRU 可以从标准 GRU 模型通过以下修改导出

  1. 移除重置门:在语音应用中,事实证明重置门是多余的。因此,在 LiGRU 模型中,它被移除,没有任何性能损失。结果,神经网络仅基于一个乘法门控,在速度、参数和内存方面都有益处。

  2. 在候选状态 \(\widetilde{h_{t}}\)使用 ReLU + BatchNorm:ReLU 是前馈神经网络中最流行的激活函数。与 tanhsigmoids 不同,它们没有导致小梯度的饱和点。过去,基于 ReLU 的神经元在 RNN 架构中并不常见。这是由于在长时间序列上应用无界 ReLU 函数引起的数值不稳定。为了解决这些数值问题,LiGRU 将其与批量归一化结合。批量归一化不仅有助于限制数值问题,还能提高性能。

  3. 共享参数 当使用双向架构时,通常会使用两个独立的模型。而 LiGRU 则共享左右扫描的相同参数。这不仅有助于减少参数总量,还能提高泛化能力。

现在让我们看看如何在 speechbrain 中使用 LiGRU 模型

import torch
from speechbrain.nnet.RNN import LiGRU

inp_tensor = torch.rand([4, 10, 20]) # [batch, time, features]
net = LiGRU(hidden_size=5, input_shape=inp_tensor.shape)
out_tensor, _ = net(inp_tensor)

print(out_tensor.shape)
torch.Size([4, 10, 5])

如您所见,输入和输出的维度与其他循环模型相同。然而,参数数量不同

for name, param in net.named_parameters():
    if param.requires_grad:
        print(name, param.shape)
rnn.0.w.weight torch.Size([10, 20])
rnn.0.u.weight torch.Size([10, 5])
rnn.0.norm.weight torch.Size([10])
rnn.0.norm.bias torch.Size([10])

如果我们使用双向模型,参数数量是相同的(由于参数共享)

import torch
from speechbrain.nnet.RNN import LiGRU

inp_tensor = torch.rand([4, 10, 20]) # [batch, time, features]
net = LiGRU(hidden_size=5,
            input_shape=inp_tensor.shape,
            bidirectional=True)
out_tensor, _ = net(inp_tensor)

print(out_tensor.shape)

for name, param in net.named_parameters():
    if param.requires_grad:
        print(name, param.shape)
torch.Size([4, 10, 10])
rnn.0.w.weight torch.Size([10, 20])
rnn.0.u.weight torch.Size([10, 5])
rnn.0.norm.weight torch.Size([10])
rnn.0.norm.bias torch.Size([10])

与 LSTM 和 GRU 类似,LiGRU 可以学习长期依赖关系。例如,当 \(z_{t}=1\) 时,隐藏状态 \(h_{t}\) 可以存储任意数量的时间步长。

最后,让我们比较这里讨论的不同模型使用的参数数量

import torch
from speechbrain.nnet.RNN import RNN, LSTM, GRU, LiGRU

hidden_size = 512
num_layers = 4
bidirectional=True

inp_tensor = torch.rand([4, 10, 80]) # [batch, time, features]

rnn = RNN(hidden_size=hidden_size,
          input_shape=inp_tensor.shape,
          bidirectional=bidirectional,
          num_layers=num_layers
          )

lstm = LSTM(hidden_size=hidden_size,
          input_shape=inp_tensor.shape,
          bidirectional=bidirectional,
          num_layers=num_layers
          )

gru = GRU(hidden_size=hidden_size,
          input_shape=inp_tensor.shape,
          bidirectional=bidirectional,
          num_layers=num_layers
          )

ligru = LiGRU(hidden_size=hidden_size,
          input_shape=inp_tensor.shape,
          bidirectional=bidirectional,
          num_layers=num_layers
          )


def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

print("RNN:", count_parameters(rnn)/10e6, "M")
print("RNN:", count_parameters(lstm)/10e6, "M")
print("RNN:", count_parameters(gru)/10e6, "M")
print("RNN:", count_parameters(ligru)/10e6, "M")
RNN: 0.5332992 M
RNN: 2.1331968 M
RNN: 1.5998976 M
RNN: 0.5332992 M

LiGRU 非常参数高效,并且在双向情况下,其参数数量与 Vanilla RNN 相同(同时能够学习长期依赖关系)。

## 参考文献

[1] S. Hochreiter, J. Schmidhuber, “长短期记忆. 神经计算”, 9, 1735–1780, 1997. pdf

[2] J. Chung, C. Gulcehre, K. Cho, Y. Bengio, “门控循环神经网络在序列建模中的实证评估”, 2014 ArXiv

[3] M. Ravanelli, P. Brakel, M. Omologo, Y. Bengio, “用于语音识别的轻量门控循环单元”, 2018 ArXiv

[4] Y. Bengio; P. Simard; P. Frasconi, “用梯度下降学习长期依赖是困难的”, IEEE Transactions on Neural Networks, 1994

[5] M. Ravanelli, “用于远场语音识别的深度学习”, 博士论文, 2017 ArXiv

引用 SpeechBrain

如果您在研究或商业中使用 SpeechBrain,请使用以下 BibTeX 条目引用它

@misc{speechbrainV1,
  title={Open-Source Conversational AI with {SpeechBrain} 1.0},
  author={Mirco Ravanelli and Titouan Parcollet and Adel Moumen and Sylvain de Langen and Cem Subakan and Peter Plantinga and Yingzhi Wang and Pooneh Mousavi and Luca Della Libera and Artem Ploujnikov and Francesco Paissan and Davide Borra and Salah Zaiem and Zeyu Zhao and Shucong Zhang and Georgios Karakasidis and Sung-Lin Yeh and Pierre Champion and Aku Rouhe and Rudolf Braun and Florian Mai and Juan Zuluaga-Gomez and Seyed Mahed Mousavi and Andreas Nautsch and Xuechen Liu and Sangeet Sagar and Jarod Duret and Salima Mdhaffar and Gaelle Laperriere and Mickael Rouvier and Renato De Mori and Yannick Esteve},
  year={2024},
  eprint={2407.00463},
  archivePrefix={arXiv},
  primaryClass={cs.LG},
  url={https://arxiv.org/abs/2407.00463},
}
@misc{speechbrain,
  title={{SpeechBrain}: A General-Purpose Speech Toolkit},
  author={Mirco Ravanelli and Titouan Parcollet and Peter Plantinga and Aku Rouhe and Samuele Cornell and Loren Lugosch and Cem Subakan and Nauman Dawalatabad and Abdelwahab Heba and Jianyuan Zhong and Ju-Chieh Chou and Sung-Lin Yeh and Szu-Wei Fu and Chien-Feng Liao and Elena Rastorgueva and François Grondin and William Aris and Hwidong Na and Yan Gao and Renato De Mori and Yoshua Bengio},
  year={2021},
  eprint={2106.04624},
  archivePrefix={arXiv},
  primaryClass={eess.AS},
  note={arXiv:2106.04624}
}