Logo
Section 1 / 5

一个简单的RNN单元

了解循环神经网络(Recurrent Neural Networks),并学习它们如何用于处理数据序列。

导入

一个简单的RNN单元

了解循环神经网络(Recurrent Neural Networks),并学习它们如何用于处理数据序列。

正如我们所知,卷积层专门用于处理网格结构的数据(例如图像)。相反,循环层则专为处理序列数据而设计。

为了将循环神经网络(RNN)与全连接层区分开来,我们将非循环网络称为前馈神经网络(feedforward NN)。

循环网络中的最小计算单元是“单元”(cell)。单元是循环网络的基本组成单位。

循环单元是一种用于处理序列数据的神经网络,通常结构较小。

循环模型帮助我们处理随时间变化的信号,因此在使用时应始终牢记“时间”和“时间步”的概念。

最简单的循环单元:序列展开

我们可以通过将当前时间步的输出连接到下一个时间步的输入,来构建一个最简化的循环单元!

这是循环网络的核心思想,称为序列展开(unrolling)。注意,这种展开可以在任意维度进行,但我们通常指的是时间维度。

但为什么我们要选择时间维度?

我们选择用RNN建模时间维度,是因为我们希望学习序列中时间上的依赖关系,尤其是长期依赖。

因此,通过逐个时间步地处理整个序列,我们的算法能够考虑序列之前的各个状态。

这样一来,我们就有了最初的“记忆”概念——即“单元”。下面用一张图来总结上述所有内容:

...
Sequence unrolling

优势与直观理解

  • 大多数常见的循环单元还能处理长度可变的序列。这一点对许多应用至关重要,比如视频包含不同数量的帧图像。

  • 我们可以把RNN单元看作一个普通的神经网络,但在多个时间步之间共享权重。通过这一机制,单元的权重便能访问序列的先前状态。

但是,如何才能训练这样的序列模型呢?我们之前学过的普通反向传播在存在循环连接的情况下无法直接使用。

这时,“随时间反向传播”(backpropagation through time)就派上用场了。

训练RNN:随时间反向传播

RNN的神奇之处源于输入的展开。这意味着,对于一个长度为 NN 的序列 X=[x1,x2,...,xN]X = [x_1, x_2, ... , x_N],我们会逐个时间步地处理输入。

从训练角度看,问题在于我们现在面对的是一个时间序列。正因如此,输入展开是让反向传播得以实现的唯一方式!

那么,我们该如何学习一个时间序列?理想情况下,我们希望单元的记忆(参数)能够综合考虑所有输入时间步的信息。否则,就无法学到理想的映射关系。

本质上,反向传播要求每个时间步对应一个独立的层,且所有层共享相同的权重(即输入展开)!

下图有助于理解这一复杂概念:

...
Backpropagation through time with input unrolling

其中 xix_i 表示第 ii 个时间步的输入。

需要注意的是,处理当前时间步时必须依赖前一时间步的输出。

对随时间反向传播的分析

实际上,你并不需要手动修改代码。像 PyTorch 这样的现有框架在使用 torch.nn.LSTM() 等现成层时,已经自动实现了随时间反向传播。

你只需传入整个序列,系统会自动将其分块并逐时间步输入。但如果你使用的是单元级别的实现(如 torch.nn.LSTMCell()),则不会自动完成这一过程。

无论如何,建立一定的直觉理解对于高效使用这些模型至关重要。

随时间反向传播正是基于这样一个前提:我们需要为每个时间步设置一个独立的层,并且所有层共享同一组权重。

因此,基于展开后的输入,我们可以为每个时间步分别计算损失,然后将多个损失的误差反向传播回记忆单元。

在这种机制下,可以从多条路径(即多个时间步)计算梯度,再将这些梯度相加得到最终的梯度。

换句话说,我们将RNN表示为一个重复的前馈网络,并在其上执行反向传播。

局限性

更重要的是,生成RNN输出所需的时间和空间复杂度,与输入长度(时间步数)呈渐近线性关系。

序列展开与随时间反向传播是密不可分的。这也是RNN层在训练时速度较慢的原因。

这一实际瓶颈限制了我们在计算资源上训练非常长序列的能力。

与梯度累积的类比

事实上,当你的GPU显存较小,却想以更大的批量大小训练模型时,实践中也会采用类似的思想。

你可以先对第一个小批量数据进行前向传播并计算损失,但暂不更新梯度;

接着对第二个小批量重复相同操作,并将不同批次的损失取平均。

这样就实现了梯度的累积。这种“低成本机器学习者”的技巧,本质上执行的操作与随时间反向传播非常相似。

以下是一个Python伪代码示例:

1accumulate_gradient_steps = 2 2 3for counter, data in enumerate(dataloader): 4 inputs, targets = data 5 predictions = model(inputs) 6 loss = criterion(predictions, targets)/accumulate_gradient_steps 7 loss.backward() 8 9 if (counter + 1) % accumulate_gradient_steps == 0: 10 optimizer.step() 11 optimizer.zero_grad()

卷积神经网络

LSTM:长短期记忆单元