要在GitHub上执行或查看/下载此笔记本
循环神经网络
循环神经网络(RNNs)为处理序列提供了一种自然的方法。
在大多数情况下,序列的元素并不是独立的。特定输出的发射通常取决于周围的元素,甚至整个历史。
为了充分模拟序列的演化,记忆的存在对于跟踪过去或将来的元素至关重要。记忆通过反馈连接实现,引入了“状态”的概念。RNNs依赖于以下公式:
\( h_t = f(x_t, h_{t−1}, θ)\)
其中 (h_t) 和 (h_{t−1}) 是当前和之前的状态,(θ) 表示可训练的参数。
由于这种递归关系,当前状态依赖于前一个状态,而前一个状态又依赖于更早的元素,从而形成了对所有先前元素的依赖。
1. 普通RNN
RNN的最简单形式是vanilla RNN,由以下公式描述:
\( h_t = \tanh(W x_t + Uh_{t−1} + b) \)
在这里,参数(θ)包括权重矩阵(W)(前馈连接)、矩阵(U)(循环权重)和向量(b)(偏置)。
为了训练一个RNN,需要在时间轴上展开网络,如下图所示:
展开后,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。让我们看一个简单的 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中实现的神经网络遵循的标准。
输出具有相同的批次数量(即四个),相同的时间步数(即10),以及转换后的特征维度(在这种情况下,与所选的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])
在这种情况下,我们有两个独立的神经网络,分别从左到右和从右到左扫描输入序列。然后将生成的隐藏状态连接成一个单一的“双向”张量。在示例中,特征维度现在是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])
RNNs 需要通过多个时间步骤反向传播梯度。然而,这一操作可能会因为梯度消失和梯度爆炸而变得复杂。这些问题会影响学习长期依赖关系。
梯度爆炸可以通过简单的裁剪策略来解决。
梯度消失问题更为关键。可以通过在网络设计中添加“梯度捷径”来缓解这一问题(例如残差网络、跳跃连接,甚至注意力机制)。
RNN的一种常见方法依赖于乘法门,其核心思想是引入一种机制,以更好地控制信息在各个时间步中的流动。
最流行的基于门控机制的网络是长短期记忆(LSTM),将在下面进行描述。
2. 长短期记忆 (LSTM)
LSTMs 依赖于一种由记忆单元组成的网络设计,这些记忆单元由遗忘门、输入门和输出门控制:
\(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])
如你所见,输入和输出的维度与普通的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 和偏置。
与普通的RNN类似,我们可以使用参数bidirectional=True来使用双向神经网络。我们还可以使用参数num_layers来堆叠更多的层。
3. 门控循环单元 (GRUs)
LSTMs依赖于由遗忘门、输入门和输出门控制的内存单元。尽管它们有效,但这种复杂的门控机制可能会导致模型过于复杂。
一个值得注意的尝试是简化LSTMs,这导致了一个名为门控循环单元(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])
输出张量的大小与普通的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模型也将权重矩阵收集到更大的张量中。在这种情况下,权重矩阵较小,因为我们只有两个门。你可以尝试使用双向RNN或增加层数来调整bidirectional和num_layers参数。
4. 轻量门控循环单元 (LiGRU)
尽管GRUs取得了有趣的性能,通常与LSTMs相当,但模型的进一步简化是可能的。
最近,一个名为light 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 模型通过以下修改推导出来:
移除重置门:在语音应用中,重置门被证明是多余的。在LiGRU模型中,因此取消了它而没有任何性能损失。结果,神经网络仅基于一个乘法门,从而在速度、参数和内存方面带来了好处。
在候选状态 \(\widetilde{h_{t}}\)* 中使用 ReLU + BatchNorm: ReLU 是前馈神经网络中最流行的激活函数。与 tanh 和
sigmoids不同,它们没有导致小梯度的饱和点。过去,基于 ReLU 的神经元在 RNN 架构中并不常见。这是由于在长时间序列上应用的无界 ReLU 函数引起的数值不稳定性。为了避免这些数值问题,LiGRU 将其与批量归一化结合使用。批量归一化不仅有助于限制数值问题,还能提高性能。共享参数 在使用双向架构时,通常会使用两个独立的模型。然而,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 非常参数高效,在双向情况下,具有与普通 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}
}