时间序列深度学习的构建模块
尽管我们在前一章中奠定了深度学习的基础,但那是非常通用的。深度学习是一个广阔的领域,在所有可能的领域都有应用,但在本书中,我们将重点关注在时间序列预测中的应用。
因此,在本章中,让我们通过查看几个常用于时间序列预测的深度学习构建模块来巩固基础。尽管全局机器学习模型在时间序列问题中表现良好,但一些深度学习方法也展现出了很好的前景。由于它们在建模时提供的灵活性,它们是你工具箱中的良好补充。
在本章中,我们将涵盖以下主题:
- 理解编码器-解码器范式
- 前馈网络
- 递归神经网络
- 长短期记忆网络
- 门控循环单元
- 卷积网络
技术要求
你需要按照本书前言中的说明设置 Anaconda 环境,按照环境中的说明,按照前言中的说明,以获得一个包含本书代码所需所有库和数据集的可用环境。任何额外的库将在运行 notebook 时安装。本书的
本章的相关代码可在此找到:https://github.com/PacktPublishing/Modern-Time-Series-Forecasting-with-Python-2E/tree/main/notebooks/Chapter12..
理解编码器-解码器范式
在第5章《时间序列预测作为回归》中,我们了解到机器学习就是学习一个将输入映射到所需输出的函数:
y = h((x))
其中 x 是输入,y 是我们所需的输出。
将此适应于时间序列预测(为简单起见,使用单变量时间序列预测),我们可以将其重写如下:
yt = h((yt-1, , yt-2, …, , …, yt-N))
这里,t 是当前时间步,N 是在时间 t 可用的历史总量。
深度学习,与其他机器学习方法一样,任务是学习这个将历史映射到未来的函数。在第11章《深度学习简介》中,我们看到了深度学习如何利用表示学习学习良好的特征,然后利用学习到的特征执行手头的任务。这种理解可以通过使用编码器-解码器范式进一步细化到时间序列视角。
像研究中的一切一样,编码器-解码器架构的想法是谁在何时提出并不完全清楚。1997年,Ramon Neco 和 Mikel Forcada 提出了一个用于机器翻译的架构,其中包含让人联想到编码器-解码器范式的想法。2013年,Nal Kalchbrenner 和 Phil Blunsom 提出了一个用于机器翻译的编码器-解码器模型,尽管他们没有这样称呼它。但直到 Ilya Sutskever 等人(2014)和 Cho 等人(2014)分别独立提出两个新的机器翻译模型时,这个想法才得以腾飞。Cho 等人称之为编码器-解码器架构,而 Sutskever 等人称之为 Seq2Seq 架构。它带来的关键创新是以端到端的方式建模可变长度输入和输出的能力。
参考文献核实:
Ramon Neco 等人、Nal Kalchbrenner 等人、Cho 等人和 Ilya Sutskever 等人的研究论文分别被引用在 参考文献 部分的 1、2、3 和 4 条目中。
这个想法非常直接,但在深入之前,我们需要对潜在空间和特征/输入空间有一个高层次的理解。
特征空间(或 输入空间)是数据所在的向量空间。如果数据有 10 个维度,那么输入空间就是 10 维向量空间。潜在空间是一个抽象的向量空间,它编码了特征空间的有意义的内部表示。为了理解这一点,我们可以思考人类如何识别老虎。我们不会记住老虎的每一个细微细节,而是对老虎的外观及其显著特征(如条纹)有一个大致概念。这是一种压缩的理解,帮助大脑更快地处理和识别老虎。
在机器学习领域,有一些技术(如 主成分分析(PCA))会进行类似的变换到潜在空间,保留输入数据的基本特征。有了这种直觉,重新阅读定义可能会对概念有更清晰的理解。
现在我们对潜在空间有了概念,来看看编码器-解码器架构的作用。
编码器-解码器架构有两个主要部分——编码器和解码器:
- 编码器:编码器接收输入向量 x,并将其编码到潜在空间中。这种编码后的表示称为潜在向量 z。
- 解码器:解码器接收潜在向量 z,并将其解码为我们所需的输出类型($\hat{y}$)。
下图直观地展示了编码器-解码器的设置:

图12.1:编码器-解码器架构
在时间序列预测的上下文中,编码器消耗历史数据并保留解码器生成预测所需的信息。正如我们之前所学的,时间序列预测可以写作如下形式:
yt = h((yt-1, , yt-2, …, , …, yt-N))
现在,利用编码器-解码器范式,我们可以将其重写如下:
zt = h((yt-1, , yt-2, …, , …, yt-N))
yt = g((zt))
这里,h 是编码器,且是编码器,g 是解码器。是解码器。
每个编码器和解码器都可能是专用于时间序列预测的特殊架构。我们来看看编码器-解码器范式中常用的一些常见组件。
前馈网络
前馈网络(FFN)或全连接网络是神经网络可以采用的最基本架构。我们在第11章《深度学习入门》中讨论了感知机。如果我们堆叠多个感知机(包括线性单元和非线性激活函数),并创建一个这样的单元网络,我们就得到了所谓的FFN。下图将帮助我们理解这一点:

图12.2:前馈网络(FFN)
FFN接受一个固定大小的输入向量,并将其通过一系列计算层传递,最终得到期望输出。这种架构称为前馈,因为信息在网络中向前传递。这也称为全连接网络,因为每一层中的每个单元都与前一层的每个单元以及后一层的每个单元相连。
第一层称为输入层,其大小等于输入维度。最后一层称为输出层,其定义取决于我们期望的输出。如果我们需要单个输出,则需要1个单元;如果需要10个输出,则需要10个单元。介于两者之间的所有层称为隐藏层。两个超参数定义了网络的结构——隐藏层的数量和每层的单元数量。例如,在图12.2中,我们有一个具有两个隐藏层、每层八个单元的网络。
在时间序列预测的上下文中,FFN既可以用作编码器也可以用作解码器。作为编码器,我们可以像在第5章(时间序列预测作为回归)中使用机器学习模型那样使用FFN。我们嵌入时间,将时间序列问题转化为回归问题,然后输入到FFN中。作为解码器,我们将其用于潜在向量(编码器的输出)以得到最终输出(这是FFN在时间序列预测中最常见的用法)。
扩展阅读:
本书将全程使用PyTorch进行深度学习。如果你对PyTorch不熟悉,不用担心——我会在必要时解释相关概念。要提前入门,你可以查看01-PyTorch_Basics.ipynb笔记本中的Chapter12章节,我们在其中探讨了张量的基本功能,并使用PyTorch从头训练了一个非常小的神经网络。我还建议你阅读本章末尾的扩展阅读部分,那里有一些学习PyTorch的资源。
现在,让我们戴上实践的帽子,看看其中的一些实际应用。PyTorch是一个开源深度学习框架,主要由Facebook AI Research(FAIR)实验室开发。尽管它是一个可以操作张量(即n维矩阵)并通过GPU加速这些操作的库,但该库的主要应用场景是构建和训练深度学习系统。因此,PyTorch提供了许多即用组件,可用于构建深度学习系统。让我们看看如何将PyTorch用于FFN。
笔记本提示:
要跟随完整代码,请使用02-Building_Blocks.ipynb文件夹中的Chapter12笔记本以及src文件夹中的代码。
正如我们之前在章节中学到的,FFN是由线性单元和非线性单元组成的网络。线性操作包括将输入向量X乘以权重矩阵W,再加上偏置项b。这个操作WX + b被封装在PyTorch库的nn模块中的Linear类里。我们可以通过torch.nn import Linear从库中导入它。但通常,我们必须导入整个nn模块,因为我们会使用该模块中的许多组件。对于非线性,我们使用ReLU(如第11章《深度学习入门》中所介绍的),它也是nn模块中的一个类。
在继续之前,让我们创建一个长度为20的随机游走时间序列:
N = 20
df = pd.DataFrame({
"date": pd.date_range(periods=N, start="2021-04-12", freq="D"),
"ts": np.random.randn(N)
})
我们可以直接在这个张量上使用FFN,但通常我们会采用滑动窗口技术来分割张量并训练网络。这样做的原因有多种:
- 我们可以将其视为一种数据增强技术,与仅使用整个序列一次相比,它可以创建更多的样本。
- 它有助于我们通过将计算限制在固定窗口内来减少和限制计算量。
现在让我们来做这件事:
ts = torch.from_numpy(df.ts.values).float()
window = 15
# Creating windows of 15 over the dataset
ts_dataset = ts.unfold(0, size=window, step=1)
现在,我们有一个张量 ts_dataset,其尺寸为 6x15(当我们在序列长度上滑动窗口时,可以创建6个样本,每个样本有15个输入特征)。对于标准FFN,输入形状指定为 批量大小 x 输入特征。因此,6成为我们的批量大小,15成为输入特征大小。
现在,我们定义FFN中的各层。对于本练习,假设网络结构如下:

图12.3:FFN——矩阵乘法视角
输入数据(6x15)将逐层通过这些层。在这里,我们可以看到张量维度在网络中流动时如何变化。每个线性层本质上是一个矩阵乘法,将输入转换为指定维度的输出。每次线性变换后,我们堆叠一个非线性激活函数。这些交替的线性和非线性模块赋予了神经网络强大的表达能力。线性层是向量空间的仿射变换(旋转、平移等),而非线性层压缩向量空间。它们共同将输入空间变形,使其适用于当前任务。现在,让我们看看如何在PyTorch中实现这一点。
我们将使用PyTorch一个方便模块,即Sequential,它允许我们轻松堆叠不同的子组件并使用它们:
# The FFN we define would have this architecture
# window(windowed input) >> 64 (hidden layer 1) >> 32 (hidden layer 2) >> 32 (hidden layer 2) >> 1 (output)
ffn = nn.Sequential(
nn.Linear(in_features=window,out_features=64), # (batch-size x window) --> (batch-size x 64)
nn.ReLU(),
nn.Linear(in_features=64,out_features=32), # (batch-size x 64) --> (batch-size x 32)
nn.ReLU(),
nn.Linear(in_features=32,out_features=32), # (batch-size x 32) --> (batch-size x 32)
nn.ReLU(),
nn.Linear(in_features=32,out_features=1), # (batch-size x 32) --> (batch-size x 1)
)
现在我们已经定义了FFN,来看看如何使用它:
ffn(ts_dataset)
# or more explicitly
ffn.forward(ts_dataset)
这将返回一个张量,其形状基于批量大小 x 输出单元。我们可以拥有任意数量的输出单元,而不仅仅是一个。因此,当使用编码器时,我们可以为潜变量向量设定任意维度。然后,当将其用作解码器时,我们可以使输出单元等于预测的时间步数。
预告:
到目前为止,我们还没有看到多步预测,因为它将在第18章,多步预测中详细介绍。但现在只需理解,在某些情况下我们需要预测未来多个时间步。经典统计模型直接支持这一点。但对于机器学习和深度学习,我们需要设计能够实现这一目标的系统。幸运的是,有几种不同的技术可以实现,这将在本章后面介绍。
FFN是为非时间序列数据设计的。我们可以通过将数据按时间嵌入,然后将其传递给网络来使用FFN。此外,FFN的计算成本与嵌入中使用的内存(即作为特征包含的先前时间步数)成正比。在这种设置下,我们也无法处理变长序列。
现在,让我们看一下另一种专为时间序列数据设计的常见架构。
递归神经网络
递归神经网络(RNNs)是一类专门设计用于处理序列数据的神经网络。它们最早由Rumelhart et al(1986)在其开创性工作Learning Representations by Back-Propagating Errors中提出。该工作借鉴了统计学和机器学习领域先前研究中的参数共享和循环思想,形成了一种神经网络架构,有助于克服FFN在处理序列数据时的许多缺点。
RNN架构
参数共享是指在模型的不同部分使用同一组参数。除了正则化效果(限制模型为多个任务使用同一组权重,通过约束搜索空间来正则化模型),参数共享使我们能够将模型扩展到并应用于不同形式的样本。RNN因此可以扩展到更长的序列。在FFN中,每个时间步(每个特征)都有一个固定权重,即使我们寻找的模式仅偏移一个时间步,网络也可能无法正确捕获。而在通过参数共享实现的RNN中,这些模式能够以更好的方式被捕获。
在一个句子(也是一个序列)中,我们可能希望模型能识别“Tomorrow I will go to the bank”和“I will go to the bank tomorrow”是同一回事。FFN无法做到这一点,但RNN能够做到,因为它在所有位置使用相同的参数,并且能够识别出“I will go to the bank”这个模式出现在何处。直观上,我们可以将RNN视为在每个时间窗口应用相同的FFN,但增加了某种记忆来存储当前任务的相关信息。
让我们可视化RNN如何处理输入:

图12.4:RNN如何处理输入序列
假设我们讨论的是一个包含四个元素的序列,x1到x4。任何RNN模块(目前将其视为黑箱)消耗输入和隐藏状态(记忆),产生输出。一开始没有记忆,所以我们从一个初始记忆(H0)开始,通常是一个全零数组。现在,RNN模块接收第一个输入(x1)和初始隐藏状态(H0),产生输出(o1)和隐藏状态(H1)。
为了处理序列中的第二个元素,同一个RNN模块接收上一时间步的隐藏状态(H1)和当前时间步的输入(x2),产生第二时间步的输出(o2)和新的隐藏状态(H2)。这个过程一直持续到序列结束。处理完整个序列后,我们将得到每个时间步的所有输出(o1到o4)和最终的隐藏状态(H4)。
这些输出和隐藏状态已编码了序列中包含的信息,可用于进一步处理,例如使用解码器预测下一步。RNN模块还可以用作解码器,接收编码表示并产生输出。由于这种灵活性,RNN模块可以安排以适应多种输入和输出组合,例如以下几种:
- 多对一,即多个输入和一个输出——例如单步预测或时间序列分类
- 多对多,即多个输入对应多个输出——例如,多步预测。
现在,我们来看看RNN内部发生了什么。
设RNN在时间t的输入为xt,上一时间步的隐藏状态为Ht-1。更新方程如下:
$$A_t = W \cdot H_{t-1} + U \cdot x_t + b_1$$
$$H_t = \tanh(A_t)$$
$$o_t = V \cdot H_t + b_2$$
这里,U、V和W是可学习的权重矩阵,b1和b2是两个可学习的偏置向量。根据它们执行的变换类型,可以很容易地将U、V和W分别记忆为输入到隐藏、隐藏到输出和隐藏到隐藏矩阵。直观地说,我们可以将RNN执行的操作视为一种根据需要学习和遗忘信息的过程。正如我们在第11章深度学习简介中所见,tanh激活函数产生一个介于-1和1之间的值,这类似于遗忘和记忆。因此,RNN将输入转换为潜在维度,使用tanh激活函数决定从当前时间步和先前记忆中保留和遗忘哪些信息,并利用这个新记忆生成输出。
在标准反向传播中,我们从一层向另一层反向传播梯度。但在循环网络中,我们面临一种特殊情况,即必须在一个单元内部沿时间或不同时间步反向传播梯度。一种特殊的反向传播方法,称为时间反向传播(BPTT),已被开发用于RNN。
幸运的是,所有主要的深度学习框架都能毫无问题地做到这一点。如需更详细理解BPTT的数学基础,请参阅延伸阅读部分。
PyTorch将RNN提供为即用模块——你只需从库中导入一个模块并开始使用即可。但在那之前,我们需要了解更多的概念。
我们将看到的第一个概念是,堆叠多个RNN层的可能性,即将每个时间步的输出作为下一层RNN的输入。每一层都会有一个隐藏状态或记忆。这使得分层特征学习成为可能,这是当今成功深度学习的基础之一。
另一个概念是双向RNN,由Schuster和Paliwal于1997年提出。双向RNN与普通RNN非常相似。在普通RNN中,我们按顺序从头到尾处理输入(正向)。然而,双向RNN使用一组输入到隐藏和隐藏到隐藏的权重从头到尾处理输入,然后使用另一组权重从尾到头(反向)处理输入,并将两个方向的隐藏状态拼接起来。在这个拼接后的隐藏状态上应用输出方程。
参考文献核实:
Rumelhart等人以及Schuster和Paliwal的研究论文在参考文献部分分别列为5和6。
PyTorch中的RNN
现在,让我们了解RNN的PyTorch实现。与Linear模块类似,RNN模块也可从torch.nn中获得。让我们看看该实现在初始化时提供的不同参数:
input_size:输入中期望的特征数量。如果我们只使用时间序列的历史值,该值为1。然而,当我们将历史值与其他特征一起使用时,该值大于1。hidden_size:隐藏状态的维度。这定义了输入到隐藏和隐藏到隐藏矩阵的大小。num_layers:这是堆叠在一起的RNN层数。默认值为1。nonlinearity:要使用的非线性函数。虽然tanh是最初提出的非线性函数,但PyTorch也允许我们使用ReLU(relu)。默认值为tanh。bias:该参数决定是否在我们之前讨论的更新方程中添加偏置。如果参数为False,则没有偏置。默认值为True。batch_first:RNN单元可以使用两种输入数据配置——我们可以将输入设置为(batch size, sequence length, number of features)或(sequence length, batch size, number of features)。batch_first = True选择前者作为期望的输入维度。默认值为False。dropout:如果此参数非零,则在除最后一层外的每个RNN层的输出上使用dropout层。Dropout是一种流行的正则化技术,在训练过程中随机选择神经元忽略(扩展阅读部分包含提出该技术的论文链接)。dropout概率将等于dropout。默认值为0。bidirectional:该参数启用双向RNN。如果为True,则使用双向RNN。默认值为False。
为了继续将模型应用于本章前面生成的相同合成数据,我们按如下方式初始化RNN模型:
rnn = nn.RNN(
input_size=1,
hidden_size=32,
num_layers=1,
batch_first=True,
dropout=0,
bidirectional=False,
)
现在,我们来看RNN单元期望的输入和输出。
与我们之前看到的Linear层不同,RNN单元接收两个输入——输入序列和隐藏状态向量。输入序列可以是(batch size, sequence length, number of features)或(sequence length, batch size, number of features),取决于我们是否设置了batch_first=True。隐藏状态是一个大小为(D*number of layers, batch size, hidden size)的张量,其中D = 1表示bidirectional=False,D = 2表示bidirectional=True。隐藏状态是一个可选输入,如果留空则默认为零张量。
RNN单元有两个输出:一个输出和一个隐藏状态。输出可以是(batch size, sequence length, D*hidden size)或(sequence length, batch size, D*hidden size),取决于batch_first。隐藏状态的维度为(D*number of layers, batch size, hidden size)。这里D = 1或2基于bidirectional参数。
因此,让我们将序列通过RNN运行并查看输入和输出(更详细的步骤请参考随附的笔记本):
#input dim: torch.Size([6, 15, 1])
# batch size = 6, sequence length = 15 and number of features = 1, batch_first = True
output, hidden_states = rnn(rnn_input)
# output.shape -> torch.Size([6, 15, 32])
# hidden_states.shape -> torch.Size([1, 6, 32]))
虽然我们看到RNN单元包含输出和隐藏状态,但我们也知道输出只是隐藏状态的仿射变换。因此,为了给用户提供灵活性,PyTorch仅在模块中实现关于隐藏状态的更新方程。在某些情况下,我们不需要每个时间步的输出(例如在多对一场景中),如果不在每个步骤进行输出更新,我们可以节省计算量。因此,来自PyTorch RNN的output只是每个时间步的隐藏状态,而hidden_states是最新的隐藏状态。
我们可以通过检查隐藏状态张量是否等于最后一个输出张量来验证这一点:
torch.equal(hidden_states[0], output[:,-1]) # -> True
为了更清晰,我们通过可视化来理解:

图12.5:堆叠RNN的PyTorch实现
每个时间步的隐藏状态用作后续RNN层的输入,而最后一层RNN的隐藏状态被收集作为输出。但每一层都有一个隐藏状态(不与其他层共享),并且PyTorch的RNN从每一层收集最后一个隐藏状态并也提供给我们。
现在,由我们决定如何使用这些输出。例如,在一步超前预测中,我们可以使用输出的隐藏状态,并在其上堆叠几个线性层来获得下一个时间步的预测。或者,我们可以使用隐藏状态将记忆转移到另一个RNN作为解码器,并生成多个时间步的预测。我们可以使用这个输出的方法还有很多,PyTorch提供了这种灵活性。
RNN在建模序列方面非常有效,但有一个重大缺陷。由于BPTT,需要反向传播的单元数量随着用于训练的序列长度急剧增加。当我们需要通过如此长的计算图进行反向传播时,我们会遇到梯度消失或梯度爆炸。这是指梯度在网络中反向传播时,要么收缩为零,要么爆炸到非常大的数值。前者导致网络停止学习,后者导致学习不稳定。
我们可以把这种现象理解为类似于将一个标量重复自乘的结果。如果该数小于1,每次相乘后,数值会变得越来越小,直到几乎为零。如果该数大于1,则数值会以指数级增长。这一现象分别由Hochreiter在他的学位论文(1991年)以及Yoshua Bengio等人在1993年和1994年发表的两篇论文中独立发现。多年来,针对这一缺陷,研究者提出了许多模型和训练过程的改进。如今,原始RNN在实际中几乎不再使用,几乎完全被其更新的变体所取代。
参考文献核实:
Hochreiter(1991)和Bengio等人(1993, 1994)的参考文献分别列在参考文献部分的第7、8和9条。
现在,让我们看看对RNN架构进行的两项关键改进,它们表现良好,在机器学习社区中广受欢迎。
长短期记忆(LSTM)网络
Hochreiter和Schmidhuber在1997年对经典RNN提出了一种改进——LSTM网络。它旨在解决原始RNN中的梯度消失和梯度爆炸问题。LSTM的设计受计算机逻辑门的启发。它引入了一个新组件,称为记忆单元,它作为长期记忆,除了经典RNN的隐藏状态记忆外还被使用。在LSTM中,多个门负责从这些记忆单元中读取、添加和遗忘信息。这个记忆单元充当了一条梯度高速公路,使得梯度可以相对无阻碍地通过网络。这是避免RNN中梯度消失的关键创新。
LSTM架构
假设LSTM在时间t的输入为xt,上一个时间步的隐藏状态为Ht-1。现在,有三个门处理信息。每个门无非是两个可学习的权重矩阵(一个用于输入,一个用于上一步的隐藏状态)和一个偏置项,它们与输入和隐藏状态相乘/相加,最后通过sigmoid激活函数。
这些门的输出将是0到1之间的实数。让我们详细看看每个门:
- 输入门:这个门的功能是决定从当前输入和上一个隐藏状态中读取多少信息。其更新公式为:
$$I_t = \sigma(W_{xi} \cdot x_i + W_{hi} \cdot H_{t-1} + b_i)$$
- 遗忘门:遗忘门决定从长期记忆中遗忘多少信息。其更新方程如下:
$$F_t = \sigma(W_{xf} \cdot x_i + W_{hf} \cdot H_{t-1} + b_f)$$
- 输出门:输出门决定当前细胞状态有多少应用于创建当前隐藏状态,即细胞的输出。其更新方程如下:
$$O_t = \sigma(W_{xo} \cdot x_i + W_{ho} \cdot H_{t-1} + b_o)$$
这里,Wxi、Wxf、Wxo、Whi、Whf和Who是可学习的权重参数,bi、bf和bo是可学习的偏置参数。
现在,我们引入新的长期记忆(细胞状态)Ct。前面提到的三个门用于从此记忆中更新和遗忘。如果上一时间步的细胞状态是Ct-1,那么LSTM细胞会计算一个候选细胞状态$\tilde{C}_t$,通过另一个门,但这次使用tanh激活函数:
$$\tilde{C}_t = \tanh(W_{xc} \cdot x_t + W_{hc} \cdot H_{t-1} + b_c)$$
这里,Wxc和Wxh是可学习的权重参数,bc是可学习的偏置参数。
现在,我们来看关键更新方程,它更新细胞的细胞状态或长期记忆:
$$C_t = F_t \odot C_{t-1} + I_t \odot \tilde{C}_t$$
这里,$\odot$是逐元素乘法。我们使用遗忘门决定从前一时间步携带多少信息,并使用输入门决定当前候选细胞状态有多少写入长期记忆。
最后但同样重要的是,我们使用新创建的当前细胞状态和输出门来决定有多少信息通过当前隐藏状态传递给预测器:
$$H_t = O_t \odot \tanh(C_t)$$
该过程的可视化效果如图12.6所示。

图12.6:LSTM的门控图
PyTorch中的LSTM
现在,我们来理解LSTM的PyTorch实现。这与我们之前看到的RNN实现非常相似,但有一个关键区别:初始化类的参数几乎相同。该API可以在https://pytorch.org/docs/stable/generated/torch.nn.LSTM.html#torch.nn.LSTM找到。关键区别在于隐藏状态的使用方式。RNN将单个张量作为隐藏状态,而LSTM期望一个由相同维度的张量组成的元组:(隐藏状态, 细胞状态)。
LSTM与RNN一样,具有堆叠和双向变体,PyTorch以相同方式处理它们。
现在,让我们初始化一些LSTM模块,并使用我们一直在使用的合成数据来看看它们的效果:
lstm = nn.LSTM(
input_size=1,
hidden_size=32,
num_layers=5,
batch_first=True,
dropout=0,
# bidirectional=True,
)
output, (hidden_states, cell_states) = lstm(rnn_input)
output.shape # -> [6, 15, 32]
hidden_states.shape # -> [5, 6, 32]
cell_states.shape # -> [5, 6, 32]
现在,让我们看看对标准RNN进行的另一项改进,它解决了梯度消失和梯度爆炸问题。
门控循环单元(GRU)
2014年,Cho等人提出了RNN的另一种变体,称为GRU,其结构比LSTM简单得多。其背后的直觉与使用一组门来控制通过细胞的信息流类似,但GRU消除了长期记忆组件,仅使用隐藏状态来传播信息。因此,不是记忆细胞成为梯度高速公路,而是隐藏状态本身成为“梯度高速公路”。沿用上一节中使用的相同符号约定,让我们看看GRU的更新方程。
GRU架构
LSTM中有三个门,而GRU中只有两个:
- 重置门:该门决定了前一隐藏状态有多少将被视为当前时间步候选隐藏状态。其公式如下
$$R_t = \sigma(W_{xr} \cdot x_t + W_{hr} \cdot H_{t-1} + b_r)$$
- 更新门:更新门决定了前一隐藏状态应保留多少,以及当前候选隐藏状态有多少会被写入隐藏状态。其公式如下
$$U_t = \sigma(W_{xu} \cdot x_t + W_{hu} \cdot H_{t-1} + b_u)$$
这里 Wxr、Wxu、Whr 和 Whu 是可学习的权重参数,且 br 和 bu 是可学习的偏置参数。
现在,我们可以按如下方式计算候选隐藏状态($\tilde{H}_t$):按如下方式:
$$\tilde{H}_t = \tanh(W_{xh} \cdot x_t + W_{hh} \cdot R_t \odot H_{t-1} + b_h)$$
这里,Wxh 和 Whh 是可学习的权重参数,且 bh 是可学习的偏置参数。这里,我们使用重置门来控制从前一隐藏状态到当前候选隐藏状态的信息流。
最后,当前隐藏状态(传给预测器的输出)使用以下方程计算:
$$H_t = U_t \odot H_{t-1} + (1 - U_t) \odot \tilde{H}_t$$
我们使用更新门来决定从前一隐藏状态和当前候选中分别有多少信息传递到下一个时间步或预测器。
参考文献核实:
关于LSTM和GRU的研究论文分别在参考文献部分作为10和11引用。
该过程的可视化表示可在图12.7中找到:

图12.7:GRU的门控图
PyTorch中的GRU
现在,让我们了解GRU在PyTorch中的实现。API、输入和输出与RNN相同。可在此处参考API:https://pytorch.org/docs/stable/generated/torch.nn.GRU.html#torch.nn.GRU。关键区别在于模块的内部工作方式,使用GRU更新方程代替标准RNN的更新方程。
现在,让我们初始化一个GRU模块,并使用我们一直在使用的合成数据来观察其运行情况:
Gru = nn.GRU(
input_size=1,
hidden_size=32,
num_layers=5,
batch_first=True,
dropout=0,
# bidirectional=True,
)
output, hidden_states = gru(rnn_input)
output.shape # -> [6, 15, 32]
hidden_states.shape # -> [5, 6, 32]
现在,让我们看看另一个可用于序列数据的主要组件。
卷积网络
卷积网络,也称为卷积神经网络(CNN),是一种用于处理网格形式数据的神经网络。这种网格可以是二维(如图像)、一维(如时间序列)、三维(如来自LIDAR传感器的数据)等等。尽管本书是关于时间序列的,并且通常在一维时间序列预测中使用一维卷积,但在二维上下文中理解卷积(如图像等)然后回到时间序列的单维网格更容易理解。
CNN背后的基本思想受人类视觉工作原理的启发。1979年,Fukushima提出了Neocognitron(参考文献12)。它是一种独一无二的架构,直接受人类视觉工作原理的启发。但CNN在我们今天所熟知的形式下诞生于1989年,当时Yann Le Cun使用反向传播来学习这样的网络,通过在 handwritten digit recognition(参考文献13)中取得最先进的结果证明了这一点。2012年,当AlexNet(一种用于图像识别的CNN架构)以较大优势赢得年度图像识别挑战赛ImageNet时,人们对CNN的兴趣和研究达到了顶峰。人们很快发现,除了图像,CNN对序列(如语言和时间序列数据)也很有效。
卷积
CNN的核心是一种称为卷积的数学运算。卷积运算的数学解释超出了本书的范围,但如果你想了解更多,在延伸阅读部分有一些链接。就我们的目的而言,我们将建立对卷积运算的直观理解。
由于CNN凭借图像数据而流行起来,我们首先讨论图像领域,然后再转向序列领域。
任何图像(为简单起见,假设是灰度图)都可以视为一个像素值网格,每个值表示点的亮度,1代表纯白,0代表纯黑。在开始讨论卷积之前,我们先理解什么是核。现在,将核视为一个包含一些值的二维矩阵。通常,核的大小小于我们使用的图像大小。由于核小于图像,我们可以将核“放入”图像内部。首先将核与左上角对齐。在当前位置,核覆盖了图像中的一组值。我们可以对图像的这一子集和核进行逐元素相乘,然后将所有元素求和为一个标量。现在,我们可以通过“滑动”核到图像的所有位置来重复这个过程。例如,下面显示了一个大小为4x4的示例图像输入,以及如何使用大小为2x2的核在其上执行卷积操作:

图12.8:对二维和一维输入的卷积操作
因此,如果将2x2核放在左上角并执行逐元素相乘和求和,我们将得到3x3输出中的左上角元素。如果将核向右滑动一个位置,将得到输出第一行的下一个元素,依此类推。类似地,如果将核向下滑动一个位置,将得到输出第一列的第二个元素。
尽管这很有趣,但我们希望从时间序列的角度理解卷积。为此,我们将范式转换到一维卷积——对一维数据(如序列)执行的卷积。在上图中,我们还可以看到一个一维卷积的示例,其中将一维核在序列上滑动,得到1x3的输出。
虽然我们设置的核权重便于理解和计算,但实际上这些权重是由网络从数据中学习得到的。如果我们将核大小设置为n,并将所有核权重设为1/n,这样的卷积会得到什么?这是我们在第6章《时间序列预测的特征工程》中讨论过的。是的,它们会得到窗口大小为n的滚动均值。记住,我们曾将其作为机器学习模型的特征工程技术。因此,一维卷积可以被视为一个更强大的特征生成器,其中特征是从数据中学习得到的。通过不同的核权重,我们将提取不同的特征。在学习用于时间序列数据的CNN时,我们应该牢记这一概念。
填充、步幅和膨胀
既然我们已经理解了卷积操作,还需要理解几个术语,如填充、步幅和膨胀。
在开始讨论这些术语之前,我们先来看一个给出卷积层输出维度(O)的公式,给定输入维度(L)、核大小(k)、填充大小(pl为左填充,pr为右填充)、步幅(s)和膨胀(d):
$$O = \frac{L + p_l + p_r - d \times (k - 1) - 1}{s} + 1$$
这些术语的默认值(填充、步幅和膨胀是卷积过程的特殊情况)是 pr、pl = 0、= 0、s = 1、= 1、d = 1。如果你不理解公式或其中的术语,不必担心——只需记住这些默认值,这样当我们理解每个术语时,就可以忽略其他项。
在图12.8中,我们看到卷积操作总是减小输入的大小。因此,在默认情况下,公式变为 O = L – (k - 1)。这是因为我们可以将卷积核放置在序列中的最早位置是从 t = 0 到 t = k。然后,通过在序列上进行卷积,我们在输出中得到 L – (k - 1) 个项。填充是指我们在序列的开头或结尾添加一些值。我们用于填充的值取决于具体问题。通常,我们选择零作为填充值。因此,填充序列本质上是增加了输入的大小。因此,在前面的公式中,我们可以将 L + pl + pr 视为填充后序列的有效长度。
接下来的两个术语(步幅和膨胀)与卷积层的感受野密切相关。卷积层的感受野是输入空间中影响卷积层生成特征的区域。换句话说,它是我们执行卷积操作的输入窗口的大小。对于单个卷积层(使用默认设置),这基本上就是卷积核的大小。对于多层CNN,由于层次结构,这个计算变得稍微复杂一些(进一步阅读部分包含一篇由Arujo等人推导CNN感受野计算公式的论文链接)。但一般来说,增加CNN的感受野与提高CNN的准确性相关。对于计算机视觉,Araujo等人指出:
“我们观察到分类准确率与感受野大小之间存在对数关系,这表明大的感受野对于高级识别任务是必要的,但收益递减。”
在时间序列中,这一点很重要,因为如果CNN的感受野小于我们想要捕捉的长期依赖关系(如季节性),那么网络将无法做到这一点。通过堆叠更多卷积层使CNN更深是增加网络感受野的一种方式。然而,有几种方法可以增加单个卷积层的感受野。步幅和膨胀就是其中两种方式:
- 步幅:之前,当我们谈到将卷积核在序列上滑动时,我们提到每次将卷积核移动一个位置。这被称为卷积层的步幅,并且步幅不一定必须为1。如果将步幅设置为2,则卷积操作会在执行时跳过一个位置,如图12.9所示。这可以使卷积网络的每一层查看更大范围的历史数据,从而增加感受野。
- 膨胀:另一种调整基本卷积层的方法是对输入连接进行膨胀。在标准卷积层中,卷积核大小为3时,我们对输入中三个连续的元素应用卷积核,膨胀率为1。如果我们将膨胀率增加到2,则卷积核将在空间上膨胀并应用。而不是应用于三个连续的元素,会跳过一个元素。图12.8展示了其工作原理。如我们所见,这也可以增加网络的感受野。
这两种技术相似但不同,并且相互兼容。下图显示了同时应用步幅和膨胀时的情况(尽管这种情况不常见):

现在,如果我们希望输出维度与输入维度相同会怎样?通过一些基础代数和重排前面的公式,我们得到以下结果:
Pl + pr = d((k-1) + L((s-1) – (s-1)-1)
在时间序列中,我们通常更倾向于在左侧而不是右侧进行填充,因为通常存在强自相关性。用零或其他值填充最近的几个条目会使预测函数的学习变得非常困难,因为最新的隐藏状态直接受到填充值的影响。进一步阅读部分包含一篇Kilian Batzner关于自回归卷积的文章链接。如果你希望真正理解我们在此讨论的概念并了解一些局限性,这是一篇必读文章。进一步阅读部分还包含一个GitHub仓库的链接,其中包含针对2D输入的卷积动画,这将让你对实际情况有很好的直观理解。
还有一个术语你可能经常听到,特别是在时间序列中——因果卷积。你需要记住的是,因果卷积并非特殊的卷积类型。只要确保在训练时不会使用未来时间步来预测当前时间步,我们就是在执行因果操作。通常通过偏移目标和对输入进行填充来实现。
PyTorch中的卷积
现在,让我们了解CNN(一维CNN,通常用于序列如时间序列)的PyTorch实现。我们来看一下初始化时该实现提供的不同参数。我们刚刚已经讨论了以下术语,现在你应该对它们很熟悉了:
in_channels:输入中期望的特征数量。如果我们只使用时间序列的历史,那么这将为1。但当我们使用历史以及其他一些特征时,这将大于1。对于后续层,你在前一层使用的out_channels将成为当前层的in_channels。out_channels:应用于输入的核或滤波器数量。每个核/滤波器使用自己的权重产生一次卷积操作。kernel_size:我们用于卷积的核大小。stride:卷积的步长。默认值为1。padding:添加到两侧的填充。如果我们将值设置为2,则传递给该层的序列将在左侧和右侧都有一个填充位置。我们也可以给出valid或same作为输入。这些是说明我们需要添加的填充类型的简便方法。padding='valid'等同于无填充。padding='same'对输入进行填充,使得输出形状与输入相同。然而,此模式不支持除1之外的任何步长值。默认值为0。padding_mode:这定义了填充位置应如何填充值。最常见和默认的选项是zeros,其中所有填充标记都用零填充。与时间序列相关的另一个有用模式是replicate,它类似于pandas中的前向填充和后向填充。其他两个选项——reflect和circular——更小众,仅用于特定用例。默认值为zeros。dilation:卷积的膨胀率。默认值为1。groups:此参数允许你控制输入通道连接到输出通道的方式。在groups中指定的数字表示将形成多少个组,使得卷积在组内进行而不跨组。例如,group=2表示一半的输入通道将由一组核进行卷积,另一半由另一组核进行卷积。这相当于并列运行两个卷积层。有关此参数的更多信息,请参阅文档。同样,这是针对小众用例的。默认值为1。bias:此参数为卷积添加可学习的偏置。默认值为True。
让我们在前文生成的相同合成数据上应用一个核大小为3的CNN模型:
conv = nn.Conv1d(in_channels=1, out_channels=1, kernel_size=k)
现在,我们来看一下CNN期望的输入和输出格式。
Conv1d 期望输入具有三个维度 —(batch size, number of channels, sequence length)。对于初始输入层,通道数是你输入到网络中的特征数量;对于中间层,它是前一层使用的核数量。来自 Conv1d 的输出形式为 (batch size, number of channels (output), sequence length (output))。
因此,让我们将序列输入 Conv1d 并查看输入和输出(有关更详细的步骤,请参阅 02-Building_Blocks.ipynb notebook):
#input dim: torch.Size([6, 1, 15])
# batch size = 6, number of features = 1 and sequence length = 15
output = conv(cnn_input)
# Output should be in_dim - k + 1
assert output.size(-1)==cnn_input.size(-1)-k+1
output.shape #-> torch.Size([6, 1, 13])
该 notebook 对 Conv1d 进行了稍微更详细的分析,包含表格展示了超参数对输出形状的影响、使用哪种填充来使输入和输出维度相同,以及等权重的卷积如何类似于滚动均值。我强烈建议你查看并尝试不同的选项,以感受该层的作用。
Conv1d 内置的填充源于图像处理,因此填充技术默认向两侧添加。然而,对于序列,最好使用左侧填充,因此最好单独处理输入序列的填充方式,而不使用内置机制。torch.nn.functional 有一个方便的方法 pad,可用于实现此效果。
其他构建块也用于时间序列预测,因为深度神经网络的结构仅受创造力的限制。但本章的目的是向你介绍在许多不同架构中常见的构建块。我们还故意省略了当今最流行的架构之一:transformer。这是因为我们在查看 transformer 之前用了另一章(第14章,注意力和 Transformers 在时间序列中的应用)来理解注意力。另一个逐渐流行的重要块是图神经网络,它可以被看作是在基于图的数据而非网格上操作的专门化 CNN。然而,这些超出了本书的范围,因为它们是一个活跃的研究领域。
小结
在上一章介绍深度学习之后,本章我们更深入地了解了用于时间序列预测的常见架构块。解释了编码器-解码器范式作为构建深度神经网络进行预测的基本方法。然后,我们学习了 FFN、RNN(LSTM 和 GRU)以及 CNN,探讨了它们如何用于处理时间序列。我们还通过配套 notebook 了解了如何在 PyTorch 中使用所有这些主要块,并动手编写了一些 PyTorch 代码。
在下一章中,我们将学习几种用于排列这些块以进行时间序列预测的主要模式。
参考文献
本章使用了以下参考文献:
- Neco, R. P. 和 Forcada, M. L. (1997),使用递归神经网络的异步翻译。Neural Networks, 1997., International Conference on (Vol. 4, pp. 2535–2540). IEEE: https://ieeexplore.ieee.org/document/614693。
- Kalchbrenner, N. 和 Blunsom, P. (2013),递归连续翻译模型。EMNLP (Vol. 3, No. 39, p. 413): https://aclanthology.org/D13-1176/。
- Kyunghyun Cho, Bart van Merriënboer, Caglar Gulcehre, Dzmitry Bahdanau, Fethi Bougares, Holger Schwenk 和 Yoshua Bengio. (2014),使用RNN编码器-解码器学习短语表示用于统计机器翻译。Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP), pages 1724–1734, Doha, Qatar. Association for Computational Linguistics: https://aclanthology.org/D14-1179/。
- Ilya Sutskever, Oriol Vinyals 和 Quoc V. Le. (2014),使用神经网络的序列到序列学习。Proceedings of the 27th International Conference on Neural Information Processing Systems – Volume 2: https://dl.acm.org/doi/10.5555/2969033.2969173。
- Rumelhart, D., Hinton, G. 和 Williams, R (1986). 通过反向传播误差学习表征。Nature 323, 533–536: https://doi.org/10.1038/323533a0。
- Schuster, M. 和 Paliwal, K. K. (1997). 双向递归神经网络。IEEE Transactions on Signal Processing, 45(11), 2673–2681: https://doi.org/10.1109/78.650093。
- Sepp Hochreiter (1991) 动态神经网络的研究。Diploma thesis, TU Munich: https://people.idsia.ch/~juergen/SeppHochreiter1991ThesisAdvisorSchmidhuber.pdf。
- Y. Bengio, P. Frasconi 和 P. Simard (1993),递归网络中学习长期依赖的问题。IEEE International Conference on Neural Networks, pp. 1183-1188 vol.3: 10.1109/ICNN.1993.298725.
- Y. Bengio, P. Simard 和 P. Frasconi (1994) 用梯度下降学习长期依赖是困难的 in IEEE Transactions on Neural Networks, vol. 5, no. 2, pp. 157–166, March 1994: 10.1109/72.279181.
- Hochreiter, S. 和 Schmidhuber, J. (1997). 长短期记忆。Neural Computation, 9(8), 1735–1780: https://doi.org/10.1162/neco.1997.9.8.1735。
- Cho, K., Merrienboer, B.V., Gülçehre, Ç., Bahdanau, D., Bougares, F., Schwenk, H. 和 Bengio, Y. (2014). 使用RNN编码器-解码器学习短语表示用于统计机器翻译。EMNLP: https://www.aclweb.org/anthology/D14-1179.pdf。
- Fukushima, K. Neocognitron: 一种不受位置偏移影响的模式识别机制的自组织神经网络模型。Biol. Cybernetics 36, 193–202 (1980): https://doi.org/10.1007/BF00344251。
- Y. Le Cun, B. Boser, J. S. Denker, R. E. Howard, W. Habbard, L. D. Jackel, and D. Henderson. 1990. 基于反向传播网络的手写数字识别. Advances in neural information processing systems 2. Morgan Kaufmann Publishers Inc., San Francisco, CA, USA, 396–404: https://proceedings.neurips.cc/paper/1989/file/53c3bce66e43be4f209556518c2fcb54-Paper.pdf..
延伸阅读
请查看以下资源,以了解更多关于本章所涵盖主题的内容:
- 官方PyTorch教程:https://pytorch.org/tutorials/beginner/basics/intro.html
- 线性代数的本质,作者:3Blue1Brown:https://www.youtube.com/playlist?list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab
- 神经网络——线性代数视角,作者:Manu Joseph:https://deep-and-shallow.com/2022/01/15/neural-networks-a-linear-algebra-perspective/
- 深度学习,作者:Ian Goodfellow、Yoshua Bengio和Aaron Courville:https://deep-and-shallow.com/2022/01/15/neural-networks-a-linear-algebra-perspective/
- 理解LSTM,作者:Christopher Olah:http://colah.github.io/posts/2015-08-Understanding-LSTMs/
- 直观理解卷积:https://betterexplained.com/articles/intuitive-convolution/
- 计算卷积神经网络的感受野,作者:Andre Araujo、Wade Norris和Jack Sim:https://distill.pub/2019/computing-receptive-fields/
- 自回归神经网络中的卷积,作者:Kilian Batzner:https://theblog.github.io/post/convolution-in-autoregressive-neural-networks/
- 卷积算术,作者:Vincent Dumoulin和Francesco Visin:https://github.com/vdumoulin/conv_arithmetic
- Dropout: A Simple Way to Prevent Neural Networks from Overfitting,作者:Nitish Srivastava 等人:https://jmlr.org/papers/v15/srivastava14a.html