时间序列数据的深度学习秘籍-全-

时间序列数据的深度学习秘籍(全)

原文:annas-archive.org/md5/412c498dab2bec44414eb60081361de1

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书的写作灵感来源于对实际时间序列分析和预测方法日益增长的需求。各行业组织依赖时间序列分析来获取关于其运营的洞察。通过利用时间序列数据,这些组织能够做出更明智的决策并优化其绩效。准确的预测在零售、经济学等多个应用领域中都是宝贵的资产。这些预测有助于减少不确定性,并实现更好的运营规划。总的来说,时间序列分析是数据科学家理解并从随着时间变化的观察数据中提取有意义洞察力的重要技能。

与此同时,深度学习推动了近期重要的科学和技术进展。它是机器学习和人工智能的一个子集,基于人工神经网络构建模型。深度学习是我们今天使用和听说的许多技术的基础,包括 ChatGPT、自动驾驶汽车和先进的图像识别工具。与此同时,深度学习方法需要大量的技术专业知识才能产生有意义的结果。

本书为机器学习从业者和对将深度学习应用于时间序列数据学习感兴趣的爱好者提供指导。我们提供了清晰易懂的代码示例,帮助你将深度学习应用于时间序列数据。虽然内容针对初学者,但经验丰富的机器学习专业人士也能从更高级技术的细节中找到价值。本书采取了边做边学的方法,确保你不仅理解核心概念,还能有效地应用这些概念。

本书涵盖了几个常见的时间序列问题,例如预测、异常检测和分类。这些任务通过不同的深度神经网络架构来解决,包括卷积神经网络或变换器。我们使用的是基于 Python 的流行深度学习框架——PyTorch 生态系统。

在本书结束时,你将能够使用深度学习方法解决不同的时间序列任务。

本书适合谁阅读

本书主要为数据科学初学者设计,尤其是那些渴望深入研究深度学习在时间序列分析和预测中应用的读者。我们假设你已经具备基本的 Python 知识,这将帮助你更轻松地理解和使用本书中的代码示例。我们还依赖于流行的数据处理库,如 pandas 和 NumPy,因此熟悉这些库会提升你的阅读体验。

我们期望你具备基本的机器学习概念和技术的知识。理解监督学习和无监督学习等基本概念,并熟悉分类、回归、交叉验证和评估方法等内容,对于充分利用本书非常重要。

本书内容

第一章时间序列入门,介绍了时间序列背后的主要概念。本章首先定义了时间序列,并描述了它如何代表多个现实世界系统。接下来,我们探讨了时间序列数据的主要特征,包括趋势和季节性。你还将学习多种时间序列分析的方法和技巧。

第二章PyTorch 入门,概述了如何使用 PyTorch 在 Python 中开发深度学习模型。我们首先指导你完成 PyTorch 的安装过程,包括如何设置适当的环境。接着,我们介绍了如何在 PyTorch 中定义神经网络结构,包括定义层和激活函数。之后,我们讲解了训练神经网络的过程。在本章结束时,你将理解使用 PyTorch 进行深度学习的基础知识,并准备好利用这些新技能解决预测任务。

第三章单变量时间序列预测,专注于使用 PyTorch 开发用于单变量时间序列的深度学习预测模型。我们首先指导你如何准备时间序列进行监督学习。接下来,我们介绍了不同类型的神经网络,包括前馈神经网络、循环神经网络和卷积神经网络。我们解释了它们如何进行训练,以及如何利用它们解决时间序列预测问题。我们还涵盖了常见的时间序列问题,如趋势和季节性,以及如何将这些问题融入神经网络模型中。

第四章使用 PyTorch Lightning 进行预测,探索了 PyTorch Lightning 生态系统以及如何使用它来构建基于时间序列的神经网络。你将学习数据模块和数据加载器,以及它们如何帮助加速构建预测模型的过程。我们还探索了 TensorBoard 和回调函数,这些都是推动训练过程的重要工具。

第五章全球预测模型,描述了如何处理涉及时间序列集合的预测问题。你还将了解预测中一些特定问题的复杂性,如多步预测和多变量预测。最后,我们还将探讨如何使用 Ray Tune 优化神经网络的参数。

第六章用于时间序列预测的高级深度学习架构,提供了使用最先进架构进行时间序列预测的全面指南。我们介绍了如何训练多种模型,如 DeepAR、N-BEATS 和 TFT。此外,我们还解释了每个模型的架构和内部工作原理,以及如何将其应用于特定的预测问题。

第七章概率性时间序列预测,描述了如何使用深度学习进行概率性时间序列预测。我们介绍了概率性预测的概念以及与传统点预测的关键区别。本章提供了几个概率性预测问题的示例,这些问题可以通过特定的深度学习架构来解决。

第八章基于深度学习的时间序列分类,重点介绍了如何使用深度学习解决时间序列分类问题。本章介绍了时间序列分类的概念,即将类别标签分配给时间序列。我们展示了如何使用不同的深度学习架构(包括残差网络和卷积神经网络)解决时间序列分类问题。

第九章基于深度学习的时间序列异常检测,概述了如何使用深度学习检测时间序列中的异常模式。在这个用例中,我们介绍了生成对抗网络和自编码器,这些都是用于检测时间序列异常的流行方法。

为了最大化地从本书中受益

我们假设您已经具备基础的 Python、数据科学和机器学习知识。使用 NumPy 或 pandas 等库进行编程和数据处理应该是您熟悉的内容,以便顺利阅读本书。读者还应了解机器学习的基本概念和技术,包括监督学习和无监督学习、分类、回归、交叉验证和评估。

书中涉及的软件/硬件 操作系统要求
Python (3.9) Windows, Mac OS X 或 Linux(任何版本)
PyTorch Lightning (2.1.2)
pandas (>=2.1)
scikit-learn (1.3.2)
NumPy (1.26.2)
torch (2.1.1)
PyTorch Forecasting (1.0.0)
GluonTS (0.14.2)

进一步的要求将在各章节的介绍中详细说明。

如果您使用的是本书的数字版本,我们建议您亲自输入代码或通过 GitHub 仓库访问代码(链接将在下一节提供)。这样做可以帮助您避免与复制粘贴代码相关的潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,地址为github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook。如果代码有更新,GitHub 上的现有仓库也会更新。

我们还有其他来自丰富书籍和视频目录的代码包,您可以在github.com/PacktPublishing/查看。快去看看吧!

本书使用的约定

本书中使用了多种文本约定。

文中的代码:表示文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账户名。举个例子:“这个模块的主要组件是setup()方法。”

一段代码设置如下:

from statsmodels.tsa.seasonal import seasonal_decompose
result = seasonal_decompose(x=series_daily, model='additive', period=365)

任何命令行输入或输出均按如下方式编写:

pip install pyod

章节

在本书中,您会发现几个经常出现的标题(准备工作如何操作...它是如何工作的...更多内容...,以及另请参阅)。

为了提供明确的步骤,完成一个食谱,使用以下章节进行指导:

准备工作

本节向您说明食谱中的内容,并描述如何设置所需的软件或任何前期设置。

如何操作……

本节包含执行该食谱所需的步骤。

它是如何工作的……

本节通常包含对上一节内容发生情况的详细解释。

更多内容……

本节包含有关该食谱的附加信息,以便让您对食谱有更深的了解。

另请参阅

本节提供有助于该食谱的其他有用信息链接。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何部分有疑问,请在邮件主题中提到书名,并通过 customercare@packtpub.com 与我们联系。

勘误:尽管我们已尽最大努力确保内容的准确性,但错误总是会发生。如果您在本书中发现错误,我们将非常感激您向我们报告。请访问www.packtpub.com/support/errata,选择您的书籍,点击“勘误提交表单”链接,并输入相关详情。

盗版:如果您在互联网上发现我们作品的任何非法副本,我们将非常感激您能提供该副本的地址或网站名称。请通过版权@packtpub.com 与我们联系,并附上相关材料的链接。

如果您有兴趣成为作者:如果您在某个领域具有专长并有兴趣撰写或参与书籍创作,请访问authors.packtpub.com

分享您的想法

阅读完《深度学习时间序列食谱》后,我们很想听听您的想法!请点击这里直接访问亚马逊书评页面并分享您的反馈。

您的反馈对我们和技术社区非常重要,将帮助我们确保提供卓越质量的内容。

下载本书的免费 PDF 副本

感谢购买本书!

您喜欢在路上阅读,但无法随身携带纸质书籍吗?

您的电子书购买与所选设备不兼容吗?

别担心,现在每本 Packt 书籍,你都可以免费获得该书的无 DRM PDF 版本。

随时随地,在任何设备上阅读。直接从你最喜欢的技术书籍中搜索、复制并粘贴代码到你的应用程序中。

福利不止于此,你还可以独享折扣、新闻通讯,以及每天通过电子邮件收到的精彩免费内容。

按照以下简单步骤获取优惠:

  1. 扫描二维码或访问以下链接。

packt.link/free-ebook/978-1-80512-923-3

  1. 提交你的购买凭证。

  2. 就是这样!我们会直接将免费的 PDF 和其他福利发送到你的邮箱。

第一章:开始时间序列分析

本章介绍了时间序列分析中使用的主要概念和技术。本章首先定义时间序列并解释为什么分析这些数据集是数据科学中的一个重要话题。接下来,我们描述了如何使用 pandas 库加载时间序列数据。本章深入探讨了时间序列的基本组成部分,如趋势和季节性。本章涵盖的一个关键概念是平稳性。我们将探讨几种使用统计检验评估平稳性的方法。

本章将涵盖以下食谱:

  • 使用 pandas 加载时间序列

  • 可视化时间序列

  • 重采样时间序列

  • 处理缺失值

  • 分解时间序列

  • 计算自相关

  • 检测平稳性

  • 处理异方差性

  • 加载并可视化多变量时间序列

  • 重采样多变量时间序列

  • 分析变量对之间的相关性

本章结束时,您将对时间序列分析的主要方面有一个扎实的基础。这包括加载和预处理时间序列数据、识别其基本组成部分、分解时间序列、检测平稳性,并将这些理解扩展到多变量设置。这些知识将为后续章节打下基础。

技术要求

要完成本章内容,您需要在计算机上安装 Python 3.9。我们将使用以下库:

  • pandas (2.1.4)

  • numpy (1.26.3)

  • statsmodels (0.14.1)

  • pmdarima (2.0.4)

  • seaborn (0.13.2)

您可以使用 pip 安装这些库:

pip install pandas numpy statsmodels pmdarima seaborn

在我们的环境中,我们使用了 pip 版本 23.3.1。该章的代码可以在以下 GitHub URL 上找到:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

使用 pandas 加载时间序列

在本章的第一个食谱中,我们首先使用 pandas 在 Python 会话中加载数据集。在本书中,我们将使用 pandas 数据结构处理时间序列。pandas 是一个用于数据分析和处理的有用 Python 包。一元时间序列可以结构化为 pandas Series 对象,其中序列的值具有与 pandas.Index 结构相关联的索引或时间戳。

准备工作

我们将专注于一个与太阳辐射相关的数据集,该数据集由美国农业部收集。该数据包含关于太阳辐射(以每平方米瓦特为单位)的信息,数据跨度从 2007 年 10 月 1 日到 2013 年 10 月 1 日。数据以每小时为频率,共计 52,608 个观测值。

你可以从本章的技术要求部分提供的 GitHub URL 下载数据集。你也可以在以下 URL 找到原始数据源:catalog.data.gov/dataset/data-from-weather-snow-and-streamflow-data-from-four-western-juniper-dominated-experimenta-b9e22

如何操作…

该数据集是一个.csv文件。在pandas中,我们可以使用pd.read_csv``()函数加载.csv文件:

import pandas as pd
data = pd.read_csv('path/to/data.csv',
                   parse_dates=['Datetime'],
                   index_col='Datetime')
series = data['Incoming Solar']

在前面的代码中,注意以下几点:

  • 首先,我们使用import关键字导入pandas。导入这个库是使其方法在 Python 会话中可用的必要步骤。

  • pd.read_csv的主要参数是文件位置。parse_dates参数会自动将输入变量(在此案例中是Datetime)转换为日期时间格式。index_col参数将数据的索引设置为Datetime列。

  • 最后,我们使用方括号对data对象进行子集化,以获取Incoming Solar列,该列包含每个时间步长的太阳辐射信息。

如何操作…

以下表格显示了数据的一个样本。每行表示特定小时的时间序列水平。

日期时间 入射太阳辐射
2007-10-01 09:00:00 35.4
2007-10-01 10:00:00 63.8
2007-10-01 11:00:00 99.4
2007-10-01 12:00:00 174.5
2007-10-01 13:00:00 157.9
2007-10-01 14:00:00 345.8
2007-10-01 15:00:00 329.8
2007-10-01 16:00:00 114.6
2007-10-01 17:00:00 29.9
2007-10-01 18:00:00 10.9
2007-10-01 19:00:00 0.0

表 1.1:每小时单变量时间序列示例

包含时间序列的series对象是一个pandas Series 数据结构。该结构包含多个时间序列分析方法。我们也可以通过调用pd.Series,并提供数据集和相应的时间序列,来创建 Series 对象。以下是一个示例:pd.Series(data=values, index=timestamps),其中values是时间序列的值,timestamps表示每个观测的时间戳。

可视化时间序列

现在,我们已经将时间序列加载到 Python 会话中。这个教程将带你完成在 Python 中可视化时间序列的过程。我们的目标是创建一个时间序列的折线图,图中日期位于X轴,系列的值位于Y轴。

准备工作

在 Python 中有多个数据可视化库。可视化时间序列有助于快速识别趋势或季节性影响等模式。图形是理解数据动态和发现其中异常的简便方式。

在本教程中,我们将使用两个不同的库创建时间序列图:pandasseabornseaborn是一个流行的数据可视化 Python 库。

如何操作…

pandas Series 对象包含一个用于可视化时间序列的 plot() 方法。你可以按如下方式使用它:

series.plot(figsize=(12,6), title='Solar radiation time series')

调用 plot() 方法并传入两个参数。我们使用 figsize 参数来更改图表的大小。在此案例中,我们将图表的宽度和高度分别设置为 12 英寸和 6 英寸。另一个参数是 title,我们将其设置为 Solar radiation time series。你可以查看 pandas 文档,获取完整的参数列表。

你可以按如下方式使用 seaborn 绘制时间序列图:

import matplotlib.pyplot as plt
import seaborn as sns
series_df = series.reset_index()
plt.rcParams['figure.figsize'] = [12, 6]
sns.set_theme(style='darkgrid')
sns.lineplot(data=series_df, x='Datetime', y='Incoming Solar')
plt.ylabel('Solar Radiation')
plt.xlabel('')
plt.title('Solar radiation time series')
plt.show()
plt.savefig('assets/time_series_plot.png')

上述代码包括以下步骤:

  1. 导入 seabornmatplotlib,这两个是数据可视化库。

  2. 通过调用 reset_index() 方法,将时间序列转换为 pandas DataFrame 对象。这个步骤是必须的,因为 seaborn 以 DataFrame 对象作为主要输入。

  3. 使用 plt.rcParams 配置图形大小,设置宽度为 12 英寸,高度为 6 英寸。

  4. 使用 set_theme() 方法将图表主题设置为 darkgrid

  5. 使用 lineplot() 方法构建图表。除了输入数据外,它还需要指定每个轴的列名:分别为 x 轴的 Datetimey 轴的 Incoming Solar

  6. 配置图表参数,即 y 轴标签(ylabel)、x 轴标签(xlabel)和 title

  7. 最后,我们使用 show 方法来展示图表,并使用 savefig 方法将图表保存为 .png 文件。

它是如何工作的…

以下图表展示了从 seaborn 库获得的图形:

图 1.1:使用 seaborn 绘制的时间序列图

图 1.1:使用 seaborn 绘制的时间序列图

示例时间序列显示了强烈的年度季节性,其中年初的平均水平较低。除了部分波动和季节性因素外,时间序列的长期平均水平在时间上保持稳定。

我们学习了两种创建时间序列图的方法。一种使用 pandas 中的 plot() 方法,另一种使用 seaborn,这是一个专注于数据可视化的 Python 库。第一种方法提供了一种快速可视化数据的方式。而 seaborn 提供了更强大的可视化工具包,能够帮助你创建美观的图表。

还有更多内容……

本教程中创建的图表类型称为折线图。pandasseaborn 都可以用来创建其他类型的图表。我们鼓励你阅读文档,了解更多类型的图表。

对时间序列进行重采样

时间序列重采样是改变时间序列频率的过程,例如将其从每小时调整为每日。这是时间序列分析中的一个常见预处理步骤,本教程展示了如何使用 pandas 完成此任务。

准备工作

更改时间序列的频率是分析前常见的预处理步骤。例如,前面章节中使用的时间序列具有每小时的粒度。然而,我们的目标可能是研究日常变化。在这种情况下,我们可以将数据重新采样为不同的周期。重新采样也是处理不规则时间序列的有效方法——那些在不规则间隔的时间段内收集的数据。

它是如何操作的……

我们将讨论重新采样时间序列可能有用的两种不同情况:改变采样频率和处理不规则时间序列。

以下代码将时间序列重新采样为每日粒度:

series_daily = series.resample('D').sum()

每日粒度通过输入Dresample()方法中指定。每天的每个对应值通过sum()方法进行求和。

大多数时间序列分析方法假设时间序列是规则的;换句话说,它是按照规则的时间间隔(例如每天一次)收集的。但有些时间序列本身是自然不规则的。例如,零售产品的销售发生在任意时间戳,随着顾客进入商店。

让我们使用以下代码模拟销售事件:

import numpy as np
import pandas as pd
n_sales = 1000
start = pd.Timestamp('2023-01-01 09:00')
end = pd.Timestamp('2023-04-01')
n_days = (end – start).days + 1
irregular_series = pd.to_timedelta(np.random.rand(n_sales) * n_days,
                                   unit='D') + start

上述代码创建了从2023-01-01 09:002023-04-011000个销售事件。此序列的示例如下表所示:

ID 时间戳
1 2023-01-01 15:18:10
2 2023-01-01 15:28:15
3 2023-01-01 16:31:57
4 2023-01-01 16:52:29
5 2023-01-01 23:01:24
6 2023-01-01 23:44:39

表 1.2:不规则时间序列示例

不规则时间序列可以通过重新采样转化为规则频率。在销售数据的情况下,我们将计算每天发生了多少销售:

ts_sales = pd.Series(0, index=irregular_series)
tot_sales = ts_sales.resample('D').count()

首先,我们基于不规则的时间戳(ts_sales)创建一个零值时间序列。然后,我们将此数据集重新采样为每日频率(D),并使用count方法统计每天发生的观察次数。重建后的tot_sales时间序列可以用于其他任务,例如预测每日销售额。

它是如何工作的……

重建后的与太阳辐射相关的时间序列示例如下表所示:

日期时间 入射太阳辐射
2007-10-01 1381.5
2007-10-02 3953.2
2007-10-03 3098.1
2007-10-04 2213.9

表 1.3:重新采样后的太阳辐射时间序列

重新采样是时间序列分析中的一个基础预处理步骤。这项技术可以用来将时间序列转换为不同的粒度,或者将不规则时间序列转换为规则时间序列。

总结统计量是一个需要考虑的重要输入。在第一个案例中,我们使用sum来将每天观察到的小时太阳辐射值加总。在不规则时间序列的情况下,我们使用count()方法来计算每个周期内发生了多少事件。不过,您可以根据需求使用其他的总结统计量。例如,使用均值可以取每个周期的平均值来重新采样时间序列。

还有更多内容…

我们已将数据重新采样至日粒度。可用选项的列表在此:pandas.pydata.org/docs/user_guide/timeseries.html#dateoffset-objects

处理缺失值

在这个教程中,我们将讨论如何对时间序列中的缺失值进行插补。我们将探讨不同的插补方法以及在选择方法时需要考虑的因素。我们还将展示如何使用pandas解决这个问题的示例。

准备工作

缺失值是困扰各种数据的问题,包括时间序列。由于传感器故障或标注错误等原因,观察值往往无法获取。在这种情况下,可以使用数据插补来克服这个问题。数据插补是通过根据某些规则(如均值或预定义值)分配一个值来实现的。

如何操作…

我们从模拟缺失数据开始。以下代码从一组两年的太阳辐射时间序列中移除了 60%的观察值:

import numpy as np
sample_with_nan = series_daily.head(365 * 2).copy()
size_na=int(0.6 * len(sample_with_nan))
idx = np.random.choice(a=range(len(sample_with_nan)),
                       size=size_na,
                       replace=False)
sample_with_nan[idx] = np.nan

我们利用numpy中的np.random.choice()方法随机选择时间序列中的一部分样本。这些样本的观察值将被更改为缺失值(np.nan)。

在没有时间顺序的数据集中,通常使用中心统计量(如均值或中位数)来插补缺失值。可以按照如下方式进行:

average_value = sample_with_nan.mean()
imp_mean = sample_with_nan.fillna(average_value)

时间序列插补必须考虑到观察值的时间特性。这意味着所分配的值应该遵循序列的动态。时间序列中更常见的方法是用最后一个已知的观察值来插补缺失数据。这个方法可以通过ffill()函数实现:

imp_ffill = sample_with_nan.ffill()

另一种较不常见的方法是利用观察值的顺序,使用bfill()函数:

imp_bfill = sample_with_nan.bfill()

bfill()方法使用数据集中下一个可用的观察值来插补缺失数据。

它的工作原理…

下图展示了使用每种方法插补后的重建时间序列:

图 1.2:使用不同策略插补缺失数据

图 1.2:使用不同策略插补缺失数据

mean插补方法忽略了时间序列的动态,而ffillbfill则能保持时间序列与原始序列相似的动态。通常情况下,ffill更为优选,因为它不会打乱观察值的时间顺序,即不使用未来的信息来改变(插补)过去的值。

还有更多内容…

插补过程也可以在某些条件下进行,例如限制插补观测值的数量。您可以在这些函数的文档页面中了解更多信息,例如pandas.pydata.org/docs/reference/api/pandas.DataFrame.ffill.html

时间序列分解

时间序列分解是将时间序列拆分成其基本组成部分的过程,如趋势或季节性。这个配方探讨了不同的技术来解决这个任务以及如何在它们之间做出选择。

准备工作

时间序列由三个部分组成——趋势、季节性和残差:

  • 趋势描述了时间序列水平的长期变化。趋势可以是上升(水平增加)或下降(水平减少),并且它们可能随着时间变化。

  • 季节性是指在固定时间周期内的规律性变化,比如每天一次。前面配方中绘制的太阳辐射时间序列显示了明显的年度季节性。夏季太阳辐射较高,冬季较低。

  • 时间序列的残差(也称为不规则部分)是去除趋势和季节性成分后的剩余部分。

将时间序列分解成其组件有助于理解数据的基本结构。

我们将通过两种方法描述时间序列分解过程:经典分解方法和基于局部回归的方法。你还将学习如何将后者方法扩展到具有多个季节模式的时间序列。

如何操作…

有几种方法可以将时间序列分解成其基本部分。最简单的方法被称为经典分解。此方法在statsmodels库中实现,可以按如下方式使用:

from statsmodels.tsa.seasonal import seasonal_decompose
result = seasonal_decompose(x=series_daily,
                            model='additive',
                            period=365)

除了数据集外,您还需要指定周期和模型类型。对于具有年度季节性的日常时间序列,周期应设置为365,即一年中的天数。model参数可以是additive(加法模型)或multiplicative(乘法模型)。我们将在下一部分详细讨论。

每个组件都作为结果对象的一个属性存储:

result.trend
result.seasonal
result.resid

这些属性中的每一个都返回一个包含相应组件的时间序列。

可以说,时间序列分解中最流行的方法之一是statsmodels

from statsmodels.tsa.seasonal import STL
result = STL(endog=series_daily, period=365).fit()

对于STL方法,我们不需要像经典方法那样指定模型。

通常,时间序列分解方法假设数据集只包含单一的季节模式。然而,以高采样频率(如每小时或每天)收集的时间序列可能包含多个季节模式。例如,每小时的时间序列可能同时显示日常和每周的规律性变化。

MSTL方法(period参数的简称)如下代码所示:

from statsmodels.tsa.seasonal import MSTL
result = MSTL(endog=series_daily, periods=(7, 365)).fit()

在之前的代码中,我们传递了两个周期作为输入:7365。这些周期尝试捕捉日常时间序列中的每周和每年季节性。

它是如何工作的…

在给定的时间步 i 中,时间序列的值(Yi)可以使用加性模型进行分解,如下所示:

Yi = 趋势i+季节性i+残差i

这种分解也可以是乘法性的:

Yi = 趋势i×季节性i×残差i

最合适的方法,加性还是乘性,取决于输入数据。但你可以通过对数据应用对数函数,将乘法分解转换为加法分解。对数可以稳定方差,从而使得时间序列的成分在加性方面表现得更加一致。

类别分解的结果如下面的图所示:

图 1.3: 经典方法分解后的时间序列成分

图 1.3: 经典方法分解后的时间序列成分

在经典分解中,趋势是通过移动平均估算的,例如,最近 24 小时的平均值(对于每小时的序列)。季节性是通过对每个周期的值进行平均来估算的。STL 是一种更灵活的时间序列分解方法。它能够处理复杂的模式,如不规则的趋势或异常值。STL 利用LOESS(局部加权散点平滑法)来提取每个成分。

还有更多…

分解通常用于数据探索目的。但它也可以作为预测的预处理步骤。例如,一些研究表明,在训练神经网络之前去除季节性成分,可以提高预测性能。

另请参见

你可以通过以下参考文献了解更多信息:

  • Hewamalage, Hansika, Christoph Bergmeir, 和 Kasun Bandara. “基于递归神经网络的时间序列预测:现状与未来方向。” 国际预测期刊 37.1 (2021): 388-427.

  • Hyndman, Rob J., 和 George Athanasopoulos. Forecasting: Principles and Practice. OTexts, 2018.

计算自相关

本教程将引导你计算自相关。自相关是衡量时间序列与自身在不同滞后期之间相关性的一种度量,它有助于理解时间序列的结构,特别是量化过去的值如何影响未来。

准备工作

相关性是衡量两个随机变量之间线性关系的统计量。自相关将这一概念扩展到时间序列数据。在时间序列中,给定时间步观察到的值通常与之前观察到的值相似。自相关函数量化了时间序列与其滞后版本之间的线性关系。滞后时间序列是指经过若干期移位后的时间序列。

如何操作…

我们可以使用 statsmodels 计算自相关函数:

from statsmodels.tsa.stattools import acf
acf_scores = acf(x=series_daily, nlags=365)

该函数的输入是一个时间序列和要分析的滞后期数。在这个案例中,我们计算了直到365个滞后的自相关,即一整年的数据。

我们也可以使用statsmodels库计算偏自相关函数。这个度量通过控制时间序列在较短滞后期的相关性,扩展了自相关:

from statsmodels.tsa.stattools import pacf
pacf_scores = pacf(x=series_daily, nlags=365)

statsmodels库还提供了绘制自相关分析结果的函数:

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
plot_acf(series_daily, lags=365)
plot_pacf(series_daily, lags=365)

它是如何工作的…

下图展示了每日太阳辐射时间序列的自相关,滞后期数为365

图 1.4:自相关分数至 365 个滞后。波动表示季节性

图 1.4:自相关分数至 365 个滞后。波动表示季节性

该图中的波动是由于年度季节性模式引起的。自相关分析是检测季节性的一种有效方法。

还有更多…

每个季节性滞后的自相关通常很大且为正。此外,有时自相关沿着滞后期逐渐衰减,这表明存在趋势。你可以通过以下网址了解更多信息:otexts.com/fpp3/components.html

偏自相关函数是识别自回归模型阶数的重要工具。其基本思路是选择偏自相关显著的滞后期数。

检测平稳性

平稳性是时间序列分析中的一个核心概念,也是许多时间序列模型的重要假设。本文将指导你如何测试一个时间序列的平稳性。

准备工作

一个时间序列是平稳的,如果它的统计特性不发生变化。这并不意味着序列随时间不变化,而是指其变化的方式本身不随时间变化。这包括时间序列的水平,在平稳条件下是恒定的。时间序列中的趋势或季节性会破坏平稳性。因此,在建模之前,处理这些问题可能会有所帮助。正如我们在时间序列分解一节中描述的那样,移除季节性会改善深度学习模型的预测。

我们可以通过差分来稳定时间序列的均值水平。差分是计算连续观测值之间差异的过程。这个过程分为两步:

  1. 估计所需的差分步骤以实现平稳性。

  2. 应用所需的差分操作次数。

如何做…

我们可以通过统计检验来估计所需的差分步骤,比如增强型迪基-富勒检验(Augmented Dickey-Fuller Test)或 KPSS 检验。这些检验可以通过ndiffs()函数在pmdarima库中实现:

from pmdarima.arima import ndiffs
ndiffs(x=series_daily, test='adf')

除了时间序列,我们还传递了test='adf'作为输入,设置方法为增强型 Dickey-Fuller 检验。该函数的输出是差分步骤的次数,在这个例子中是1。然后,我们可以使用diff()方法对时间序列进行差分:

series_changes = series_daily.diff()

差分也可以应用于季节性周期。在这种情况下,季节性差分涉及计算同一季节性周期之间连续观察值的差异:

from pmdarima.arima import nsdiffs
nsdiffs(x=series_changes, test='ch', m=365)

除了数据和检验(ch表示 Canova-Hansen 检验),我们还指定了周期数。在此案例中,该参数设置为365(一年中的天数)。

它是如何工作的……

下图显示了差分后的时间序列。

图 1.5:差分后连续时期之间变化序列的示例

图 1.5:差分后连续时期之间变化序列的示例

差分作为一种预处理步骤。首先,对时间序列进行差分,直到它变为平稳序列。然后,基于差分后的时间序列创建预测模型。通过反向差分操作,可以将模型提供的预测结果转换回原始尺度。

还有更多……

在这个食谱中,我们重点介绍了两种测试平稳性的方法。你可以在函数文档中查看其他选项:alkaline-ml.com/pmdarima/modules/generated/pmdarima.arima.ndiffs.html

处理异方差性

在这个食谱中,我们深入探讨了时间序列的方差。时间序列的方差是衡量数据分布程度以及这种分布如何随时间变化的指标。你将学习如何处理具有变化方差的数据。

准备工作

时间序列的方差可能会随时间变化,这也违反了平稳性。在这种情况下,时间序列被称为异方差性,通常显示长尾分布。这意味着数据是左偏或右偏的。这种情况是有问题的,因为它会影响神经网络和其他模型的训练。

如何做……

处理非恒定方差是一个两步过程。首先,我们使用统计检验来检查时间序列是否为异方差性。然后,我们使用如对数变换等方法来稳定方差。

我们可以使用统计检验方法,如 White 检验或 Breusch-Pagan 检验,来检测异方差性。以下代码基于statsmodels库实现了这些检验:

import statsmodels.stats.api as sms
from statsmodels.formula.api import ols
series_df = series_daily.reset_index(drop=True).reset_index()
series_df.columns = ['time', 'value']
series_df['time'] += 1
olsr = ols('value ~ time', series_df).fit()
_, pval_white, _, _ = sms.het_white(olsr.resid, olsr.model.exog)
_, pval_bp, _, _ = sms.het_breuschpagan(olsr.resid, olsr.model.exog)

上述代码按照以下步骤执行:

  1. 导入statsmodels模块olsstats

  2. 根据时间序列的值和数据采集的行号(1表示第一次观察)创建一个 DataFrame。

  3. 创建一个线性模型,将时间序列的值与time列相关联。

  4. 运行het_white(White)和het_breuschpagan(Breusch-Pagan)来应用方差检验。

测试的输出是一个 p 值,其中原假设认为时间序列具有恒定方差。因此,如果 p 值低于显著性值,我们就拒绝原假设,并假设存在异方差性。

处理非恒定方差的最简单方法是使用对数变换数据。该操作可以按如下方式实现:

import numpy as np
class LogTransformation:
    @staticmethod
    def transform(x):
        xt = np.sign(x) * np.log(np.abs(x) + 1)
        return xt
    @staticmethod
    def inverse_transform(xt):
        x = np.sign(xt) * (np.exp(np.abs(xt)) - 1)
        return x

上述代码是一个名为LogTransformation的 Python 类。它包含两个方法:transform``()inverse_transform``()。第一个方法使用对数变换数据,第二个方法则恢复该操作。

我们将transform``()方法应用于时间序列,方法如下:

series_log = LogTransformation.transform(series_daily)

对数是scipy库中可用的 Box-Cox 变换的特殊情况。你可以按如下方式实现该方法:

series_transformed, lmbda = stats.boxcox(series_daily)

stats.boxcox``()方法估算一个变换参数lmbda,该参数可以用来恢复该操作。

它是如何工作的…

本节中概述的变换可以稳定时间序列的方差。它们还将数据分布拉近正态分布。这些变换对于神经网络特别有用,因为它们有助于避免饱和区。在神经网络中,当模型对不同输入变得不敏感时,就会发生饱和,从而影响训练过程。

还有更多…

Yeo-Johnson 幂变换类似于 Box-Cox 变换,但它允许时间序列中出现负值。你可以通过以下链接了解更多关于该方法的信息:docs.scipy.org/doc/scipy/reference/generated/scipy.stats.yeojohnson.html

另见

你可以通过以下参考文献了解更多关于对数变换的重要性:

Bandara, Kasun, Christoph Bergmeir, 和 Slawek Smyl。“使用群体相似系列的递归神经网络进行跨时间序列数据库的预测:一种聚类方法。” Expert Systems with Applications 140 (2020): 112896。

加载和可视化多元时间序列

到目前为止,我们已学习如何分析单变量时间序列。然而,多元时间序列在现实问题中同样具有重要意义。本节将探讨如何加载多元时间序列。之前,我们使用了pandas的 Series 结构来处理单变量时间序列。而多元时间序列则更适合使用pandas的 DataFrame 对象来处理。

准备工作

多元时间序列包含多个变量。时间序列分析中的基本概念被扩展到多个变量随时间变化并彼此相互关联的情况。不同变量之间的关系可能难以建模,尤其是当这些变量的数量很大时。

在许多实际应用中,多个变量可能相互影响并表现出时间依赖性。例如,在天气建模中,入射太阳辐射与其他气象变量(如空气温度或湿度)相关联。将这些变量纳入单一的多变量模型中,可能对于建模数据的动态行为并获得更好的预测至关重要。

我们将继续研究太阳辐射数据集。这次时间序列通过添加额外的气象信息来扩展。

如何操作……

我们将从读取一个多变量时间序列开始。与 使用 pandas 加载时间序列 方法一样,我们依赖 pandas 来读取 .csv 文件并将其导入 DataFrame 数据结构:

import pandas as pd
data = pd.read_csv('path/to/multivariate_ts.csv',
                   parse_dates=['datetime'],
                   index_col='datetime')

parse_datesindex_col 参数确保 DataFrame 的索引是一个 DatetimeIndex 对象。这一点非常重要,因为 pandas 会将这个对象视为时间序列。加载时间序列后,我们可以使用 plot () 方法对其进行转换和可视化:

data_log = LogTransformation.transform(data)
sample = data_log.tail(1000)
mv_plot = sample.plot(figsize=(15, 8),
                      title='Multivariate time series',
                      xlabel='',
                      ylabel='Value')
mv_plot.legend(fancybox=True, framealpha=1)

上述代码遵循了以下步骤:

  1. 首先,我们使用对数变换数据。

  2. 我们选取最后 1,000 个观察值,以使得可视化图表不显得过于杂乱。

  3. 最后,我们使用 plot () 方法创建可视化图表。我们还调用 legend 来配置图表的图例。

它是如何工作的……

以下图展示了一个多变量时间序列的示例:

图 1.6:多变量时间序列图

图 1.6:多变量时间序列图

加载多变量时间序列的过程与单变量的情况类似。主要的区别是,多变量时间序列在 Python 中是作为 DataFrame 对象存储,而不是 Series 对象。

从前面的图表中,我们可以注意到,不同的变量遵循不同的分布,并且有着不同的平均值和离散程度。

重采样多变量时间序列

本方法回顾了重采样的主题,但重点讲解了多变量时间序列。我们将解释为什么重采样对多变量时间序列来说可能有点棘手,因为通常需要对不同的变量使用不同的汇总统计量。

准备工作

在重采样多变量时间序列时,可能需要根据不同的变量应用不同的汇总统计量。例如,您可能希望将每小时观察到的太阳辐射加总,以估算您能生成多少能量。然而,在总结风速时,取平均值而不是总和更为合理,因为风速变量不是累积的。

如何操作……

我们可以传递一个 Python 字典,详细说明每个变量应应用哪种统计量。然后,我们可以将这个字典传递给 agg () 方法,如下所示:

stat_by_variable = {
    'Incoming Solar': 'sum',
    'Wind Dir': 'mean',
    'Snow Depth': 'sum',
    'Wind Speed': 'mean',
    'Dewpoint': 'mean',
    'Precipitation': 'sum',
    'Vapor Pressure': 'mean',
    'Relative Humidity': 'mean',
    'Air Temp': 'max',
}
data_daily = data.resample('D').agg(stat_by_variable)

我们使用不同的汇总统计量将时间序列聚合为日周期性。例如,我们想要汇总每天观察到的太阳辐射。对于空气温度变量(Air Temp),我们选择每天观察到的最大值。

它是如何工作的……

通过使用字典传递不同的汇总统计量,我们可以以更灵活的方式调整时间序列的频率。请注意,如果你希望对所有变量应用均值,则无需使用字典。更简单的方法是运行data.resample('D').mean()

分析变量对之间的相关性

本教程将引导你通过使用相关性分析多变量时间序列的过程。这个任务对于理解系列中不同变量之间的关系,并进一步理解其动态特征非常有用。

准备工作

分析多个变量动态的常见方法是计算每对变量的相关性。你可以利用这些信息进行特征选择。例如,当变量对之间高度相关时,你可能只想保留其中一个。

如何做到……

首先,我们计算每对变量之间的相关性:

corr_matrix = data_daily.corr(method='pearson')

我们可以使用seaborn库的热力图来可视化结果:

import seaborn as sns
import matplotlib.pyplot as plt
sns.heatmap(data=corr_matrix,
            cmap=sns.diverging_palette(230, 20, as_cmap=True),
            xticklabels=data_daily.columns,
            yticklabels=data_daily.columns,
            center=0,
            square=True,
            linewidths=.5,
            cbar_kws={"shrink": .5})
plt.xticks(rotation=30)

热力图是可视化矩阵的常用方式。我们从sns.diverging_palette中选择一个渐变色调来区分负相关(蓝色)和正相关(红色)。

它是如何工作的……

下图显示了带有相关性结果的热力图:

图 1.7: 多变量时间序列的相关矩阵

图 1.7: 多变量时间序列的相关矩阵

corr()方法计算data_daily对象中每对变量之间的相关性。在这个例子中,我们使用的是皮尔逊相关性,并指定method='pearson'参数。Kendall 和 Spearman 是皮尔逊相关性的两种常见替代方法。

第二章:开始使用 PyTorch

在本章中,我们将探索 PyTorch,一个领先的 Python 深度学习库。

我们将介绍一些有助于理解如何使用 PyTorch 构建神经网络的操作。除了张量操作,我们还将探讨如何训练不同类型的神经网络。具体来说,我们将重点关注前馈神经网络、循环神经网络、长短期记忆LSTM)和 1D 卷积神经网络。

在后续章节中,我们还将介绍其他类型的神经网络,例如 Transformer。这里,我们将使用合成数据进行演示,这将帮助我们展示每种模型背后的实现和理论。

完成本章后,您将对 PyTorch 有深入的理解,并掌握进行更高级深度学习项目的工具。

在本章中,我们将介绍以下几种方法:

  • 安装 PyTorch

  • PyTorch 中的基本操作

  • PyTorch 中的高级操作

  • 使用 PyTorch 构建一个简单的神经网络

  • 训练前馈神经网络

  • 训练循环神经网络

  • 训练 LSTM 神经网络

  • 训练卷积神经网络

技术要求

在开始之前,您需要确保您的系统满足以下技术要求:

  • Python 3.9:您可以从www.python.org/downloads/ 下载 Python。

  • pip(23.3.1)或 Anaconda:这些是 Python 的常用包管理器。pip 默认与 Python 一起安装。Anaconda 可以从www.anaconda.com/products/distribution下载。

  • torch(2.2.0):本章中我们将使用的主要深度学习库。

  • CUDA(可选):如果您的计算机上有支持 CUDA 的 GPU,您可以安装支持 CUDA 的 PyTorch 版本。这将使您能够在 GPU 上进行计算,并且可以显著加快深度学习实验的速度。

值得注意的是,本章中介绍的代码是平台无关的,并且应当可以在满足前述要求的任何系统上运行。

本章的代码可以在以下 GitHub 地址找到:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

安装 PyTorch

要开始使用 PyTorch,首先需要安装它。根据写作时的信息,PyTorch 支持 Linux、macOS 和 Windows 平台。在这里,我们将引导您完成这些操作系统上的安装过程。

准备工作

PyTorch 通常通过 pip 或 Anaconda 安装。我们建议在安装库之前创建一个新的 Python 环境,特别是当您需要在系统上进行多个 Python 项目时。这是为了避免不同项目可能需要的 Python 库版本之间发生冲突。

如何实现…

让我们看看如何安装PyTorch。我们将描述如何使用pipAnaconda来完成此操作。我们还将提供有关如何使用 CUDA 环境的一些信息。

如果你使用pip,Python 的包管理器,你可以在终端中运行以下命令来安装 PyTorch:

pip install torch

使用 Anaconda Python 发行版,你可以使用以下命令安装 PyTorch:

conda install pytorch torchvision -c pytorch

如果你的机器上有支持 CUDA 的 GPU,你可以安装支持 CUDA 的PyTorch版本,以便在 GPU 上进行计算。这可以显著加速你的深度学习实验。PyTorch 官网提供了一个工具,根据你的需求生成相应的安装命令。访问 PyTorch 官网,在Quick Start Locally部分选择你的偏好(如操作系统、包管理器、Python 版本和 CUDA 版本),然后将生成的命令复制到终端中。

它是如何工作的…

安装PyTorch后,你可以通过打开 Python 解释器并运行以下代码来验证一切是否正常工作:

import torch
print(torch.__version__)

这应该输出你安装的PyTorch版本。现在,你已经准备好开始使用PyTorch进行深度学习了!

在接下来的章节中,我们将熟悉PyTorch的基础知识,并构建我们的第一个神经网络。

PyTorch 中的基本操作

在我们开始使用PyTorch构建神经网络之前,理解如何使用这个库操作数据是至关重要的。在PyTorch中,数据的基本单元是张量,它是矩阵的一种推广,支持任意维度(也称为多维数组)。

准备开始

张量可以是一个数字(0 维张量),一个向量(1 维张量),一个矩阵(2 维张量),或者任何多维数据(3 维张量、4 维张量等等)。PyTorch提供了多种函数来创建和操作张量。

如何做…

让我们从导入PyTorch开始:

import torch

我们可以使用各种技术在PyTorch中创建张量。让我们从使用列表创建张量开始:

t1 = torch.tensor([1, 2, 3])
print(t1)
t2 = torch.tensor([[1, 2], [3, 4]])
print(t2)

PyTorch可以与NumPy无缝集成,允许从NumPy数组轻松创建张量:

import numpy as np
np_array = np.array([5, 6, 7])
t3 = torch.from_numpy(np_array)
print(t3)

PyTorch还提供了生成特定值(如零或一)张量的函数:

t4 = torch.zeros((3, 3))
print(t4)
t5 = torch.ones((3, 3))
print(t5)
t6 = torch.eye(3)
print(t6)

这些是NumPy中常用的方法,PyTorch中也可以使用这些方法。

它是如何工作的…

现在我们知道如何创建张量,让我们来看看一些基本操作。我们可以对张量执行所有标准的算术操作:

result = t1 + t3
print(result)
result = t3 - t1
print(result)
result = t1 * t3
print(result)
result = t3 / t1
print(result)

你可以使用.reshape()方法来重塑张量:

t7 = torch.arange(9) # Creates a 1D tensor [0, 1, 2, ..., 8]
t8 = t7.reshape((3, 3)) # Reshapes the tensor to a 3x3 matrix
print(t8)

这是PyTorch中张量操作的简要介绍。随着你深入学习,你会发现PyTorch提供了多种操作来处理张量,给予你实现复杂深度学习模型和算法所需的灵活性和控制力。

PyTorch 中的高级操作

探索了基本的张量操作后,现在让我们深入了解 PyTorch 中的更高级操作,特别是构成深度学习中大多数数值计算基础的线性代数操作。

准备工作

线性代数是数学的一个子集。它涉及向量、向量空间及这些空间之间的线性变换,如旋转、缩放和剪切。在深度学习的背景下,我们处理的是高维向量(张量),对这些向量的操作在模型的内部工作中起着至关重要的作用。

如何做……

让我们从回顾上一节中创建的张量开始:

print(t1)
print(t2)

两个向量的点积是一个标量,衡量向量的方向和大小。在 PyTorch 中,我们可以使用 torch.dot() 函数计算两个 1D 张量的点积:

dot_product = torch.dot(t1, t3)
print(dot_product)

与逐元素相乘不同,矩阵乘法,也叫做点积,是将两个矩阵相乘以产生一个新的矩阵的操作。PyTorch 提供了 torch.mm() 函数来执行矩阵乘法:

matrix_product = torch.mm(t2, t5)
print(matrix_product)

矩阵的转置是一个新矩阵,它的行是原始矩阵的列,而列是原始矩阵的行。你可以使用 .T 属性来计算张量的转置:

t_transposed = t2.T
print(t_transposed)

你可以执行其他操作,例如计算矩阵的行列式和求矩阵的逆。让我们看几个这样的操作:

det = torch.det(t2)
print(det)
inverse = torch.inverse(t2)
print(inverse)

注意,这两个操作仅在 2D 张量(矩阵)上定义。

它是如何工作的……

PyTorch 是一个高度优化的库,特别适用于执行基本和高级操作,尤其是深度学习中至关重要的线性代数操作。

这些操作使得 PyTorch 成为构建和训练神经网络以及在更一般的背景下执行高阶计算的强大工具。在下一节中,我们将使用这些构建块开始构建深度学习模型。

使用 PyTorch 构建一个简单的神经网络

本节将从头开始构建一个简单的两层神经网络,仅使用基本的张量操作来解决时间序列预测问题。我们旨在演示如何手动实现前向传播、反向传播和优化步骤,而不依赖于 PyTorch 的预定义层和优化例程。

准备工作

我们使用合成数据进行这个演示。假设我们有一个简单的时间序列数据,共 100 个样本,每个样本有 10 个时间步。我们的任务是根据前面的时间步预测下一个时间步:

X = torch.randn(100, 10)
y = torch.randn(100, 1)

现在,让我们创建一个神经网络。

如何做……

让我们从定义模型参数及其初始值开始。在这里,我们创建了一个简单的两层网络,因此我们有两组权重和偏置:

我们使用 requires_grad_() 函数告诉 PyTorch,我们希望在反向传播时计算这些张量的梯度。

接下来,我们定义我们的模型。对于这个简单的网络,我们将在隐藏层使用 sigmoid 激活函数:

input_size = 10
hidden_size = 5
output_size = 1
W1 = torch.randn(hidden_size, input_size).requires_grad_()
b1 = torch.zeros(hidden_size, requires_grad=True)
W2 = torch.randn(output_size, hidden_size).requires_grad_()
b2 = torch.zeros(output_size, requires_grad=True)
def simple_neural_net(x, W1, b1, W2, b2):
    z1 = torch.mm(x, W1.t()) + b1
    a1 = torch.sigmoid(z1)
    z2 = torch.mm(a1, W2.t()) + b2
    return z2

现在,我们已经准备好训练模型了。让我们定义学习率和训练的轮次(epochs):

lr = 0.01
epochs = 100
loss_fn = torch.nn.MSELoss()
for epoch in range(epochs):
    y_pred = simple_neural_net(X, W1, b1, W2, b2)
    loss = loss_fn(y_pred.squeeze(), y)
    loss.backward()
    with torch.no_grad():
        W1 -= lr * W1.grad
        b1 -= lr * b1.grad
        W2 -= lr * W2.grad
        b2 -= lr * b2.grad
    W1.grad.zero_()
    b1.grad.zero_()
    W2.grad.zero_()
    b2.grad.zero_()
    if epoch % 10 == 0:
        print(f'Epoch: {epoch} \t Loss: {loss.item()}')

这段基本代码演示了神经网络的基本部分:前向传播,我们计算预测值;反向传播,我们计算梯度;以及更新步骤,我们调整权重以最小化损失。

还有更多内容…

本章重点探讨神经网络训练过程的复杂性。在未来的章节中,我们将展示如何训练深度神经网络,而无需担心这些细节。

训练前馈神经网络

本教程将带你逐步完成使用 PyTorch 构建前馈神经网络的过程。

准备工作

前馈神经网络,也被称为多层感知器MLPs),是最简单的人工神经网络之一。数据从输入层流向输出层,经过隐藏层,不包含任何循环。在这种类型的神经网络中,一层的所有隐藏单元都与下一层的单元相连。

如何实现…

让我们使用 PyTorch 创建一个简单的前馈神经网络。首先,我们需要导入必要的 PyTorch 模块:

import torch
import torch.nn as nn

现在,我们可以定义一个带有单一隐藏层的简单前馈神经网络:

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(10, 5)
        self.fc2 = nn.Linear(5, 1)
    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = self.fc2(x)
        return x
net = Net()
print(net)

在上述代码中,nn.ModulePyTorch 中所有神经网络模块的基类,我们的网络是它的一个子类。

该类中的 forward() 方法表示网络的前向传播过程。这是网络在将输入转换为输出时执行的计算。以下是逐步的解释:

  • forward() 方法接收一个输入张量 x。这个张量表示输入数据,它的形状应该与网络的层兼容。在这里,作为第一个线性层(self.fc1)期望有 10 个输入特征,x 的最后一个维度应该是 10

  • 输入张量首先通过线性变换处理,由 self.fc1 表示。这个对象是 PyTorchnn.Linear 类的一个实例,它执行线性变换,涉及用权重矩阵乘以输入数据并加上偏置向量。正如在 __init__() 方法中定义的那样,这一层将 10 维空间转化为 5 维空间,使用的是线性变换。这个降维过程通常被视为神经网络“学习”或“提取”输入数据中的特征。

  • 第一层的输出随后通过 torch.relu() 进行处理。这是一个简单的非线性函数,它将张量中的负值替换为零。这使得神经网络能够建模输入和输出之间更复杂的关系。

  • ReLU()函数的输出接着通过另一个线性变换self.fc2。和之前一样,这个对象是PyTorchnn.Linear类的一个实例。这个层将张量的维度从5(前一层的输出大小)缩减到1(所需的输出大小)。

最后,第二个线性层的输出由forward()方法返回。这个输出可以用于多种目的,例如计算用于训练网络的损失,或者作为推理任务中的最终输出(即网络用于预测时)。

它是如何工作的…

要训练网络,我们需要一个数据集,一个损失函数,以及一个优化器。

我们使用与前一个示例相同的合成数据集:

X = torch.randn(100, 10)
Y = torch.randn(100, 1)

我们可以使用均方误差MSE)损失来进行我们的任务,这是回归问题中常用的损失函数。PyTorch 提供了这个损失函数的内置实现:

loss_fn = nn.MSELoss()

我们将使用随机梯度下降SGD)作为我们的优化器。SGD 是一种迭代方法,用于优化目标函数:

optimizer = torch.optim.SGD(net.parameters(), lr=0.01)

现在我们可以训练我们的网络。我们将训练100个周期:

for epoch in range(100):
    output = net(X)
    loss = loss_fn(output, Y)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print(f'Epoch {epoch+1}, Loss: {loss.item()}')

在每个周期中,我们执行前向传播,计算损失,进行反向传播以计算梯度,然后更新权重。

你现在已经使用PyTorch训练了一个简单的前馈神经网络。在接下来的章节中,我们将深入探讨更复杂的网络架构及其在时间序列分析中的应用。

训练递归神经网络

递归神经网络RNNs)是一类神经网络,特别适用于涉及序列数据的任务,如时间序列预测和自然语言处理。

准备工作

RNN 通过具有隐藏层,能够将序列中的信息从一个步骤传递到下一个步骤,从而利用序列信息。

如何操作…

类似于前馈神经网络,我们首先定义了RNN类。为了简化,假设我们定义了一个单层的RNN

class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(RNN, self).__init__()
        self.hidden_size = hidden_size
        self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)
    def forward(self, x):
        h0 = torch.zeros(1, x.size(0), self.hidden_size).to(x.device)
        out, _ = self.rnn(x, h0)  # get RNN output
        out = self.fc(out[:, -1, :])
        return out
rnn = RNN(10, 20, 1)
print(rnn)

这里,input_size是每个时间步的输入特征数量,hidden_size是隐藏层中的神经元数量,output_size是输出特征的数量。在forward()方法中,我们将输入x和初始隐藏状态h0传递给递归层。RNN 返回输出和最终的隐藏状态,我们暂时忽略隐藏状态。然后,我们取序列的最后一个输出(out[:, -1, :]),并通过一个全连接层得到最终输出。隐藏状态充当网络的记忆,编码输入的时间上下文,直到当前时间步,这也是这种类型的神经网络在序列数据中非常有用的原因。

让我们注意一下在代码中使用的一些细节:

  • x.device:指的是张量x所在的设备。在PyTorch中,张量可以位于 CPU 或 GPU 上,而.device是一个属性,表示张量当前所在的设备。当你在 GPU 上进行计算时,所有输入到计算的张量必须位于相同的设备上。在代码行h0 = torch.zeros(1, x.size(0), self.hidden_size).to(x.device)中,我们确保初始隐藏状态张量h0x输入张量位于同一设备上。

  • x.size(0):指的是张量x的第0维的大小。在PyTorch中,size()返回张量的形状,而size(0)给出第一维的大小。在这个 RNN 的上下文中,x预计是一个形状为(batch_sizesequence_lengthnum_features)的 3D 张量,因此x.size(0)会返回批次大小。

它是如何工作的…

RNN 的训练过程与前馈网络类似。我们将使用之前示例中的相同合成数据集、损失函数(MSE)和优化器(SGD)。不过,让我们将输入数据修改为 3D 格式,以满足 RNN 的要求(batch_sizesequence_lengthnum_features)。RNN 输入张量的三个维度代表以下方面:

  • batch_size:表示每个批次数据中的序列数。在时间序列中,你可以把一个样本看作是一个子序列(例如,过去五天的销售数据)。因此,一个批次包含多个这样的样本或子序列,允许模型同时处理和学习多个序列。

  • sequence_length:本质上是你用来观察数据的窗口大小。它指定了每个输入子序列包含的时间步数。例如,如果你是基于过去的数据预测今天的温度,sequence_length就决定了模型每次查看的数据向后回溯了多少天。

  • num_features:该维度表示数据序列中每个时间步的特征(变量)数量。在时间序列的上下文中,单变量序列(例如某一地点的每日温度)在每个时间步只有一个特征。相比之下,多变量序列(例如同一地点的每日温度、湿度和风速)在每个时间步有多个特征。

让我们创建一个合成数据集作为示例:

X = torch.randn(100, 5, 10)
Y = torch.randn(100, 1)

现在,我们可以开始训练我们的网络。我们将进行100轮训练:

loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(rnn.parameters(), lr=0.01)
for epoch in range(100):
    output = rnn(X)
    loss = loss_fn(output, Y)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print(f"Epoch {epoch+1}, Loss: {loss.item()}")

现在,我们已经训练了一个 RNN。这是将这些模型应用于现实世界时间序列数据的一个重要步骤,我们将在下一章讨论这一点。

训练 LSTM 神经网络

RNN 存在一个根本问题,即“梯度消失”问题,由于神经网络中反向传播的性质,较早输入对整体误差的影响在序列长度增加时急剧减小。在存在长期依赖的序列处理任务中尤其严重(即未来的输出依赖于很早之前的输入)。

准备工作

LSTM 网络被引入以克服这个问题。与 RNN 相比,它们为每个单元使用了更复杂的内部结构。具体来说,LSTM 能够根据一个叫做细胞的内部结构来决定丢弃或保存哪些信息。这个细胞通过门控(输入门、遗忘门和输出门)来控制信息的流入和流出。这有助于保持和操作“长期”信息,从而缓解梯度消失问题。

如何实现…

我们首先定义 LSTM 类。为了简化起见,我们将定义一个单层的 LSTM 网络。请注意,PyTorch 的 LSTM 期望输入是 3D 的,格式为 batch_sizeseq_lengthnum_features

class LSTM(nn.Module):
  def __init__(self, input_size, hidden_size, output_size):
     super(LSTM, self).__init__()
     self.hidden_size = hidden_size
     self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
     self.fc = nn.Linear(hidden_size, output_size)
  def forward(self, x):
     h0 = torch.zeros(1, x.size(0), self.hidden_size).to(x.device)
     c0 = torch.zeros(1, x.size(0), self.hidden_size).to(x.device)
     out, _ = self.lstm(x, (h0, c0))  # get LSTM output
     out = self.fc(out[:, -1, :])
     return out
lstm = LSTM(10, 20, 1) # 10 features, 20 hidden units, 1 output
print(lstm)

forward() 方法与我们之前介绍的 RNN 方法非常相似。主要的区别在于,在 RNN 的情况下,我们初始化了一个单一的隐藏状态 h0,并将其与输入 x 一起传递给 RNN 层。而在 LSTM 中,你需要初始化隐藏状态 h0 和细胞状态 c0,这是因为 LSTM 单元的内部结构。然后,这些状态作为元组与输入 x 一起传递给 LSTM 层。

它是如何工作的……

LSTM 网络的训练过程与前馈网络和 RNN 的训练过程相似。我们将使用之前示例中的相同合成数据集、损失函数(MSE)和优化器(SGD):

X = torch.randn(100, 5, 10)
Y = torch.randn(100, 1)
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(lstm.parameters(), lr=0.01)
for epoch in range(100):
      output = lstm(X)
      loss = loss_fn(output, Y)
      optimizer.zero_grad()
      loss.backward()
      optimizer.step()
      print(f'Epoch {epoch+1}, Loss: {loss.item()}')

训练卷积神经网络

卷积神经网络CNNs)是一类特别适用于网格状输入数据(如图像、音频谱图,甚至某些类型的时间序列数据)的神经网络。

准备工作

CNN 的核心思想是使用卷积滤波器(也称为内核)对输入数据进行卷积操作,这些滤波器滑过输入数据并产生输出特征图。

如何实现…

为了简化起见,我们定义一个单层的 1D 卷积神经网络,这特别适用于时间序列和序列数据。在 PyTorch 中,我们可以使用 nn.Conv1d 层来实现:

class ConvNet(nn.Module):
    def __init__(self,
        input_size,
        hidden_size,
        output_size,
        kernel_size,
        seq_length):
        super(ConvNet, self).__init__()
        self.conv1 = nn.Conv1d(input_size, hidden_size, kernel_size)
        self.fc = nn.Linear(hidden_size*(seq_length-kernel_size+1),
            output_size)
    def forward(self, x):
        x = x.transpose(1, 2)
        out = torch.relu(self.conv1(x))
        out = out.view(out.size(0), -1)  # flatten the tensor
        out = self.fc(out)
        return out
convnet = ConvNet(5, 20, 1, 3, 10)
print(convnet)

forward 方法中,我们将输入通过卷积层,然后是 ReLU() 激活函数,最后通过一个全连接层。Conv1d 层期望输入的形状为(batch_sizenum_channels,和 sequence_length)。其中,num_channels 指输入通道的数量(相当于时间序列数据中的特征数量),sequence_length 则指每个样本的时间步数。

Linear层将接受来自Conv1d层的输出,并将其缩减到所需的输出大小。Linear层的输入计算为hidden_size*(seq_length-kernel_size+1),其中hidden_sizeConv1d层的输出通道数,seq_length-kernel_size+1是卷积操作后的输出序列长度。

它是如何工作的……

1D CNN 的训练过程与前面的网络类型类似。我们将使用相同的损失函数(MSE)和优化器(SGD),但我们将修改输入数据的大小为(batch_sizesequence_lengthnum_channels)。请记住,通道数等于特征的数量:

X = torch.randn(100, 10, 5)
Y = torch.randn(100, 1)

现在,我们可以训练我们的网络。我们将进行100个训练周期:

loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(convnet.parameters(), lr=0.01)
for epoch in range(100):
    output = convnet(X)
    loss = loss_fn(output, Y)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print(f'Epoch {epoch+1}, Loss: {loss.item()}')

在前面的代码中,我们对每个训练周期进行迭代。每个训练周期结束后,我们将模型的误差打印到控制台,以便监控训练过程。

第三章:单变量时间序列预测

在本章中,我们将开发深度学习模型来解决单变量时间序列预测问题。我们将涉及时间序列预处理的几个方面,例如为监督学习准备时间序列以及处理趋势或季节性等情况。

我们将涵盖不同类型的模型,包括简单的基准模型,如天真预测或历史均值法。我们还将简要介绍一种流行的预测技术——自回归积分滑动平均ARIMA)。接下来,我们将解释如何使用不同类型的深度学习方法创建预测模型。这些方法包括前馈神经网络、长短期记忆LSTM)、门控循环单元GRU)、堆叠LSTM卷积神经网络CNNs)。你还将学习如何解决时间序列建模中常见的问题,例如如何使用一阶差分处理趋势,如何通过对数变换稳定方差。通过本章的学习,你将能够解决单变量时间序列预测问题。

本章将引导你完成以下方案:

  • 构建简单的预测模型

  • 使用 ARIMA 进行单变量预测

  • 为监督学习准备时间序列

  • 使用前馈神经网络进行单变量预测

  • 使用 LSTM 进行单变量预测

  • 使用 GRU 进行单变量预测

  • 使用堆叠 LSTM 进行单变量预测

  • 将 LSTM 与多个全连接层结合

  • 使用 CNN 进行单变量预测

  • 处理趋势 – 采用一阶差分

  • 处理季节性 – 季节虚拟变量和傅里叶级数

  • 处理季节性 – 季节差分

  • 处理季节性 – 季节分解

  • 处理非恒定方差 – 对数变换

技术要求

在深入探讨单变量时间序列预测问题之前,我们需要确保系统中已安装适当的软件和库。这里,我们将讨论实现本章中描述的程序的主要技术要求:

  • 我们主要需要 Python 3.9 或更高版本,pip 或 Anaconda,PyTorch 和 CUDA(可选)。你可以参考上一章的 安装 PyTorch 方案,了解有关这些工具的更多信息。

  • NumPy (1.26.3) 和 pandas (2.1.4):这两个 Python 库提供了多种数据处理和分析方法。

  • statsmodels (0.14.1):该库实现了几种统计方法,包括一些有用的时间序列分析技术。

  • scikit-learn (1.4.0):scikit-learn 是一个流行的 Python 库,用于统计学习。它包含了几种解决不同任务的方法,例如分类、回归和聚类。

  • sktime (0.26.0):一个 Python 库,提供了一个框架来处理涉及时间序列的多个问题。

你可以使用 pip,Python 的包管理器,来安装这些库。例如,要安装 scikit-learn,你可以运行以下代码:

pip install -U scikit-learn

本章的代码可以在以下 GitHub URL 中找到:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

构建简单的预测模型

在深入探讨更复杂的方法之前,让我们从一些简单的预测模型开始:朴素模型、季节性朴素模型和均值模型。

准备工作

在本章中,我们专注于涉及单变量时间序列的预测问题。让我们从加载我们在第一章中探索的一个数据集开始:

import pandas as pd
serie = pd.read_csv(
    "assets/datasets/time_series_solar.csv",
    parse_dates=["Datetime"],
    index_col="Datetime",
)['Incoming Solar']

在前面的代码中,series 是一个 pandas Series 对象,包含单变量时间序列。

如何实现…

我们现在可以使用以下三种方法来预测我们的时间序列:

  • Python 中可以像下面这样简单实现:

    series.shift(1)
    
  • m,在 Python 中,可以这样实现:

    m = 12
    series.shift(m)
    
  • Python如下:

    series.expanding().mean()
    

这三种方法是有用的基准,可以用来评估其他更复杂预测解决方案的表现。

它是如何工作的…

这些简单的模型对时间序列数据做了一些假设:

  • 朴素模型假设序列是随机的,每个观测值与前一个观测值是独立的。

  • 季节性朴素模型通过识别固定间隔或“季节”的模式,增加了一些复杂性。

  • 均值模型假设序列围绕一个常数均值波动,未来的值将回归到该均值。

还有更多…

尽管这些简单模型看起来过于基础,但它们有两个关键作用:

  • 基准:像这样的简单模型通常用作更复杂模型的基准。如果复杂模型无法超过这些简单方法的表现,那么就说明复杂模型可能存在问题,或者时间序列数据没有可预测的模式。

  • 理解数据:这些模型还可以帮助我们理解数据。如果时间序列数据能够通过朴素模型或均值模型进行良好的预测,那么这表明数据可能是随机的,或围绕一个常数均值波动。

实现简单的预测模型,如朴素模型、季节性朴素模型和历史均值模型,可能非常简单,但利用现有的库来提供这些模型的现成实现可能会更有益。这些库不仅简化了实现过程,而且通常还提供了额外的功能,如内置模型验证、优化和其他实用工具。

这里有两个提供这些模型的库的示例:

上述库在进行预测任务时可以作为一个很好的起点。它们不仅提供了简单预测模型的实现,还包含了许多其他复杂的模型和工具,可以帮助简化开发和验证时间序列预测模型的过程。

在接下来的教程中,我们将看到如何放宽或扩展这些假设,以构建更复杂的模型。

使用 ARIMA 进行单变量预测

ARIMA 是一种基于两个组成部分的单变量时间序列预测方法:自回归部分和移动平均部分。在自回归中,滞后指的是时间序列数据中用于预测未来值的先前数据点。例如,如果我们使用一个滞后为 1,那么我们将使用前一个时间步的值来建模当前观测值。移动平均部分则使用过去的误差来建模时间序列的未来观测值。

准备工作

要使用 ARIMA 模型,如果尚未安装 statsmodels Python 包,则需要安装它。你可以使用 pip 安装:

pip install -U statsmodels

对于本教程,我们将使用与前一个教程相同的数据集。

如何操作…

在 Python 中,你可以使用 statsmodels 库中的 ARIMA 模型。以下是如何拟合 ARIMA 模型的一个基本示例:

import pandas as pd
from statsmodels.tsa.arima.model import ARIMA
series = pd.read_csv(
    "assets/datasets/time_series_solar.csv",
    parse_dates=["Datetime"],
    index_col="Datetime",
)['Incoming Solar']
model = ARIMA(series, order=(1, 1, 1), freq='H')
model_fit = model.fit()
forecasts = model_fit.predict(start=0, end=5, typ='levels')

工作原理…

ARIMA 模型基于时间序列的过去值来解释时间序列。它们结合了 自回归AR)模型、积分I)模型和 移动平均MA)模型的特点:

  • AR 部分涉及回归,其中时间序列的下一个值是基于前 p 个滞后值来建模的。

  • ARIMA 是为平稳数据定义的,因此可能需要在建模前对数据进行预处理。这是通过 I 部分完成的,I 部分表示使序列平稳所需的差分操作次数(d)。

  • MA 组件是另一种回归模型,其中系列的下一个值是基于过去 q 个误差来建模的。

这些操作的顺序表示为一个元组(p, d, q)。最佳组合取决于输入数据。在这个例子中,我们使用了(1, 1, 1)作为示例。

通过 model_fit.predict() 函数对未来六个观察值进行预测。预测的起始和结束索引分别设置为 05typ='levels' 参数用于直接返回预测值,而不是差分后的值。

还有更多……

确定 ARIMA 模型的正确顺序(p, d, q)可能具有挑战性。这通常涉及检查 2 测量时间序列与其在过去两个时间周期的值之间的相关性。另一方面,PACF 测量自相关,同时控制前一个滞后。这意味着,PACF 在滞后 2 时,测量的是系列与其两个时间周期前的值之间的相关性,但去除了一个时间周期滞后的线性依赖。你可以通过以下网址了解更多:otexts.com/fpp3/acf.html。通过检查 ACF 和 PACF 图,我们可以更好地理解时间序列的潜在模式,从而做出更准确的预测。

此外,ARIMA 模型假设时间序列是平稳的,但这并不总是正确的。因此,可能需要使用如差分或对数等转换方法来使时间序列平稳,从而适应 ARIMA 模型。

季节性 ARIMA 模型通常用于具有季节性成分的非平稳时间序列。该模型添加了一组参数,专门用来建模时间序列的季节性成分。

请注意,调节 ARIMA 参数有自动化的方法。一种常见的方法是使用 pmdarima 库中的 auto_arima() 函数。另一个有用的实现是 statsforecast 包中提供的实现。你可以通过以下网址了解更多:nixtlaverse.nixtla.io/statsforecast/index.html

除了 ARIMA,你还可以探索指数平滑方法,这是另一种流行的经典预测方法。指数平滑方法的实现也可以在 statsmodelsstatsforecast 中找到,例如。

为监督学习准备时间序列

在这篇教程中,我们将重点介绍机器学习方法用于预测。我们首先描述将时间序列从一系列值转换为适合监督学习格式的过程。

准备工作

监督学习涉及一个包含解释变量(输入)和目标变量(输出)数据集。时间序列由一系列带有时间戳的值组成。因此,我们需要重构时间序列,以适应监督学习。常见的做法是使用滑动窗口。序列中的每个值基于其之前的最近值(也称为滞后值)。

在准备这一部分时,您需要确保时间序列数据可用,并以pandas DataFrame 的形式存在,同时安装了pandas和 NumPy 库。如果没有,您可以使用pip进行安装:

pip install -U pandas numpy

我们还将单变量时间序列加载到 Python 会话中:

series = pd.read_csv(
    "assets/datasets/time_series_solar.csv",
    parse_dates=["Datetime"],
    index_col="Datetime",
)['Incoming Solar']

如何实现…

以下 Python 函数接受单变量时间序列和窗口大小作为输入,并返回监督学习问题的输入(X)和输出(y):

import pandas as pd
def series_to_supervised(data, n_in=1, n_out=1, dropnan=True):
    n_vars = 1 if len(data.shape) == 1 else data.shape[1]
    df = pd.DataFrame(data)
    cols, names = list(), list()
    for i in range(n_in, 0, -1):
        cols.append(df.shift(i))
        names += [('var%d(t-%d)' % (j + 1, i)) for j in range(n_vars)]
     for i in range(0, n_out):
        cols.append(df.shift(-i))
        if i == 0:
            names += [('var%d(t)' % (j + 1)) for j in range(n_vars)]
        else:
            names += [('var%d(t+%d)' % (j + 1, i)) for j in range(n_vars)]
    agg = pd.concat(cols, axis=1)
    agg.columns = names
    if dropnan:
        agg.dropna(inplace=True)
     return agg
data = series_to_supervised(series, 3)
print(data)

series_to_supervised函数是此脚本的核心,它接受四个参数:时间序列数据、滞后观测值的数量(n_in)、作为输出的观测值数量(n_out),以及是否删除包含NaN值的行(dropnan):

  1. 函数首先检查数据类型,并为列(cols)及其名称(names)准备一个空列表。然后,它通过平移 DataFrame 并将这些列附加到cols中,以及将相应的列名附加到names中,创建输入序列(t-n, ..., t-1)。

  2. 该函数继续以相似的方式创建预测序列t, t+1 ..., t+n,并将其附加到colsnames中。然后,它将所有列聚合到一个新的 DataFrame(agg)中,分配列名,并可选择删除包含NaN值的行。

  3. 然后,脚本加载有关太阳辐射的时间序列数据集(time_series_solar.csv)到一个 DataFrame(df)中,提取Incoming Solar列到一个 NumPy 数组(values),并使用series_to_supervised函数将该数组转换为具有三个滞后观测值的监督学习数据集。

  4. 最后,它会打印转换后的数据,其中包含作为输入的滞后观测序列,以及作为输出的相应未来观测值。该格式已准备好用于任何监督学习算法。

它是如何工作的…

在监督学习中,目标是训练一个模型,以学习输入变量与目标变量之间的关系。然而,在处理时间序列数据时,这种结构并不直接可用。数据通常是随时间变化的观测序列(例如,温度和股价)。因此,我们必须将时间序列数据转换为适合监督学习的格式。这正是series_to_supervised函数的作用。

转化过程涉及使用滑动窗口方法创建原始时间序列数据的滞后版本。这是通过将时间序列数据按一定步数(在代码中由n_in表示)进行平移来创建输入特征。这些滞后观察值作为解释变量(输入),其思想是过去的值会影响许多现实世界时间序列的未来值。

目标变量(输出)通过将时间序列按相反方向平移一定步数(预测跨度),由n_out表示。这意味着,对于每个输入序列,我们有相应的未来值,模型需要预测这些值。

例如,假设我们准备使用大小为3的滑动窗口来为一个简单的预测任务准备单变量时间序列。在这种情况下,我们可以将序列[1, 2, 3, 4, 5, 6]转化为以下监督学习数据集:

输入 (t-3, t-2, t-1) 输出 (t)
1, 2, 3 4
2, 3, 4 5
3, 4, 5 6

表 3.1:将时间序列转化为监督学习数据集的示例

series_to_supervised()函数接收一系列观察值作为输入,n_in指定作为输入的滞后观察值数量,n_out指定作为输出的观察值数量,还有一个布尔参数dropnan用于去除包含NaN值的行。它返回一个适用于监督学习的 DataFrame。

该函数通过在输入数据上迭代指定次数,每次平移数据并将其附加到列表(cols)中。然后将列表连接成一个 DataFrame,并适当地重命名列。如果dropnan=True,则删除任何包含缺失值的行。

还有更多…

窗口大小决定了我们应该使用多少过去的时间步来预测未来的时间步,这取决于具体问题和时间序列的性质。过小的窗口可能无法捕捉到重要的模式,而过大的窗口可能会包含不相关的信息。测试不同的窗口大小并比较模型性能是选择合适窗口大小的常见方法。

使用前馈神经网络进行单变量预测

本教程将引导你通过使用单变量时间序列构建前馈神经网络进行预测的过程。

准备就绪

在将时间序列数据转化为适用于监督学习的格式后,我们现在准备使用它来训练一个前馈神经网络。我们策略性地决定对数据集进行重采样,从每小时数据转为每日数据。这一优化显著加速了我们的训练过程:

series = series.resample('D').sum()

如何操作…

以下是使用 PyTorch 构建和评估前馈神经网络的步骤:

  1. 我们首先将数据拆分为训练集和测试集并进行归一化。需要注意的是,Scaler 应该在训练集上进行拟合,并用于转换训练集和测试集:

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import MinMaxScaler
    scaler = MinMaxScaler(feature_range=(-1, 1))
    train, test = train_test_split(data, test_size=0.2, 
        shuffle=False)
    train = scaler.fit_transform(train)
    test = scaler.transform(test)
    X_train, y_train = train[:, :-1], train[:, -1]
    X_test, y_test = test[:, :-1], test[:, -1]
    X_train = torch.from_numpy(X_train).type(torch.Tensor)
    X_test = torch.from_numpy(X_test).type(torch.Tensor)
    y_train = torch.from_numpy(y_train).type(torch.Tensor).view(-1)
    y_test = torch.from_numpy(y_test).type(torch.Tensor).view(-1)
    
  2. 然后,我们使用 PyTorch 创建一个简单的前馈神经网络,包含一个隐藏层。input_dim 代表滞后的数量,通常称为回溯窗口。hidden_dim 是神经网络隐藏层中的隐藏单元数量。最后,output_dim 是预测范围,在以下示例中设置为 1。我们使用 ReLU 激活函数,这是我们在上一章的 训练前馈神经网络 章节中描述的:

    class FeedForwardNN(nn.Module):
        def __init__(self, input_dim, hidden_dim, output_dim):
            super(FeedForwardNN, self).__init__()
            self.fc1 = nn.Linear(input_dim, hidden_dim)
            self.fc2 = nn.Linear(hidden_dim, output_dim)
            self.activation = nn.ReLU()
        def forward(self, x):
            out = self.activation(self.fc1(x))
            out = self.fc2(out)
            return out
    model = FeedForwardNN(input_dim=X_train.shape[1],
                          hidden_dim=32,
                          output_dim=1)
    
  3. 接下来,我们定义loss函数和optimizer,并训练模型:

    loss_fn = nn.MSELoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    epochs = 200
    for epoch in range(epochs):
        model.train()
        optimizer.zero_grad()
        out = model(X_train).reshape(-1,)
        loss = loss_fn(out, y_train)
        loss.backward()
        optimizer.step()
        if epoch % 10 == 0:
            print(f"Epoch: {epoch}, Loss: {loss.item()}")
    
  4. 最后,我们在测试集上评估模型:

    model.eval()
    y_pred = model(X_test).reshape(-1,)
    test_loss = loss_fn(y_pred, y_test)
    print(f"Test Loss: {test_loss.item()}")
    

它是如何工作的……

这个脚本首先将数据分为训练集和测试集。MinMaxScaler 用于将特征缩放到 -11 之间。需要注意的是,我们只在训练集上拟合 scaler,以避免数据泄漏。

接下来,我们定义一个简单的前馈神经网络模型,包含一个隐藏层。FeedForwardNN类继承自nn.Module,这是 PyTorch 中所有神经网络模块的基类。类构造函数定义了网络的层,forward方法指定了前向传播的过程。

然后,模型使用均方误差损失函数和 Adam 优化器进行训练。模型参数在多个迭代周期中不断更新。

最后,模型在测试集上进行评估,未见数据的损失衡量了模型在训练数据之外的泛化能力。

还有更多……

这是一个简单的例子,展示了如何使用前馈神经网络进行时间序列预测。你可以通过以下几种方式改进此模型:

  • 你可以尝试不同的网络架构,例如,通过增加更多层或改变隐藏层中神经元的数量。你还可以尝试不同的激活函数、优化器和学习率。

  • 使用更复杂的方法准备训练集和测试集可能会更有益;例如,使用滚动窗口验证策略。

  • 另一个改进可以是使用早停法来防止过拟合。我们将在下一章学习这一技术。

  • 最后但同样重要的是,递归神经网络RNNs)和 LSTM 网络是专门为序列数据设计的,能够为时间序列预测提供更好的结果。

使用 LSTM 进行单变量预测

本食谱将引导你完成构建 LSTM 神经网络以进行单变量时间序列预测的过程。

准备开始

正如我们在第二章中看到的那样,LSTM 网络作为 RNN 的一种变体,因其在时间序列和序列数据上的表现而受到广泛关注。LSTM 网络特别适合这个任务,因为它们能够有效地捕捉输入数据中的长期时间依赖性,得益于其内在的记忆单元。

本节将使用 PyTorch 扩展我们的一元时间序列预测到 LSTM 网络。因此,我们继续使用前一个食谱中创建的对象(使用前馈神经网络进行一元预测)。

如何操作……

我们将使用前一部分中的相同训练集和测试集。对于 LSTM,我们必须将输入数据重塑为 3D 格式。正如我们在前一章中探讨的那样,LSTM 输入张量的三个维度分别表示以下内容:

  • 样本:一个子序列(例如,过去五个滞后值)就是一个样本。一个批次是一组样本。

  • 时间步长:窗口大小;每个时间点使用多少过去的观测值。

  • 特征:模型中使用的变量数量。一元时间序列始终只有一个特征。

以下代码将输入的解释性变量转换为 3D 格式:

X_train = X_train.view([X_train.shape[0], X_train.shape[1], 1])
X_test = X_test.view([X_test.shape[0], X_test.shape[1], 1])

在前面的代码行中,X_train.shape[0]X_test.shape[0]表示样本的数量(即序列的数量),而X_train.shape[1]X_test.shape[1]表示时间步长的数量(窗口大小)。重塑操作中的最后一个维度,设置为1,表示特征的数量。我们的一元时间序列只有一个特征,所以设置为1。如果我们有一个多元时间序列,这个值将对应于数据中的变量数量。

PyTorch 中的view()函数用于重塑tensor对象。它等同于 NumPy 中的reshape()函数,允许我们重新组织数据以匹配 LSTM 模型所需的输入形状。以这种方式重塑数据可以确保 LSTM 模型接收到期望格式的数据。这对于 LSTM 有效建模时间序列数据中的时间依赖性至关重要。

然后,我们定义 LSTM 模型:

class LSTM(nn.Module):
    def __init__(self, input_dim, hidden_dim, num_layers, output_dim):
        super(LSTM, self).__init__()
        self.hidden_dim = hidden_dim
        self.num_layers = num_layers
        self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers,
            batch_first=True)
        self.fc = nn.Linear(hidden_dim, output_dim)
    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0),
            self.hidden_dim).requires_grad_()
        c0 = torch.zeros(self.num_layers, x.size(0),
            self.hidden_dim).requires_grad_()
        out, (hn, cn) = self.lstm(x, (h0.detach(), c0.detach()))
        out = self.fc(out[:, -1, :])
        return out
model = LSTM(input_dim=1,
             hidden_dim=32,
             output_dim=1,
             num_layers=1)

请注意,对于 LSTM,input_dim的输入维度是1,表示时间序列中的变量数量。这一点与我们在前一个食谱中传递给前馈神经网络的input_dim参数不同。在那个例子中,input_dim设置为3,表示滞后数或特征数。

现在,我们继续训练模型:

epochs = 200
for epoch in range(epochs):
    model.train()
    optimizer.zero_grad()
    out = model(X_train).reshape(-1,)
    loss = loss_fn(out, y_train)
    loss.backward()
    optimizer.step()
    if epoch % 10 == 0:
        print(f"Epoch: {epoch}, Loss: {loss.item()}")

最后,我们评估模型:

model.eval()
y_pred = model(X_test)
test_loss = loss_fn(y_pred, y_test)
print(f"Test Loss: {test_loss.item()}")

它是如何工作的……

在第一步中,我们将训练集和测试集重塑为 LSTM 所期望的输入形状,即batch_sizesequence_lengthnumber_of_features

LSTM 类继承自 nn.Module,这意味着它是 PyTorch 中的自定义神经网络。LSTM 模型具有指定数量的隐藏维度和层数的 LSTM 层,然后是一个全连接(线性)层,用于输出最终预测。

forward() 函数定义了 LSTM 模型的前向传播。我们首先用零初始化 LSTM 的隐藏状态 (h0) 和细胞状态 (c0)。然后,将输入数据和初始状态传入 LSTM 层,它会返回 LSTM 的输出以及最终的隐藏和细胞状态。请注意,我们只使用 LSTM 的最终时间步输出传入全连接层以生成输出。

然后,我们实例化模型,将 loss() 函数定义为用于训练网络的 Adam 优化器。

在训练过程中,我们首先将模型设置为训练模式,重置梯度,执行前向传播,计算损失,通过 loss.backward 执行反向传播,然后执行单一优化步骤。

最后,我们在测试数据上评估模型并打印测试损失。请注意,我们没有进行超参数调整,这在训练神经网络时是非常重要的步骤。我们将在下一章学习这个过程。

这还不止一点……

LSTM 模型特别适用于时间序列预测,因为它们能够捕捉长期依赖关系。然而,它们的性能可能会极大地依赖于超参数的选择。因此,进行超参数调优以找到最佳配置可能是有用的。一些重要的超参数包括隐藏维度的数量、LSTM 层的数量和学习率。

还要记住,像所有深度学习模型一样,如果模型复杂度过高,LSTM 也可能会过拟合。可以使用诸如 dropout、早停止或正则化(L1L2)等技术来防止过拟合。

此外,还可以使用高级变体的 LSTM,如双向 LSTM,或其他类型的 RNN,如 GRU,来可能提升性能。

最后,尽管 LSTM 强大,但由于计算和内存需求,尤其是对于非常大的数据集或复杂模型,它们并不总是最佳选择。在这些情况下,可能更适合使用简单的模型或其他类型的神经网络。

使用 GRU 进行单变量预测

这个示例将引导你完成构建用于单变量时间序列预测的 GRU 神经网络的过程。

准备工作

现在我们已经看到了如何使用 LSTM 进行单变量时间序列预测,让我们现在把注意力转向另一种称为 GRURNN 架构。GRULSTM 一样,被设计用来有效地捕捉序列数据中的长期依赖关系,但其内部结构略有不同,更少复杂。这通常使得它们训练速度更快。

对于这一部分,我们将使用与前几部分相同的训练和测试数据集。同样,输入数据应重新调整为一个 3D 张量,维度分别表示观测值、时间步和特征:

X_train = X_train.view([X_train.shape[0], X_train.shape[1], 1])
X_test = X_test.view([X_test.shape[0], X_test.shape[1], 1])

如何实现…

让我们通过以下步骤开始构建 GRU 网络:

  1. 我们首先在 PyTorch 中构建 GRU 网络:

    class GRUNet(nn.Module):
        def init(self, input_dim, hidden_dim, output_dim=1, 
            num_layers=2):
            super(GRUNet, self).init()
            self.hidden_dim = hidden_dim
            self.num_layers = num_layers
            self.gru = nn.GRU(input_dim, hidden_dim, num_layers, 
                batch_first=True)
            self.fc = nn.Linear(hidden_dim, output_dim)
            def forward(self, x):
            h0 = torch.zeros(self.num_layers, x.size(0), 
                self.hidden_dim).to(x.device)
            out, _ = self.gru(x, h0)
            out = self.fc(out[:, -1, :])
            return out
    model = GRUNet(input_dim=1,
                   hidden_dim=32,
                   output_dim=1,
                   num_layers=1)
    
  2. 如同之前一样,我们定义了我们的 loss 函数和 optimizer

    loss_fn = nn.MSELoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
    
  3. 我们训练我们的模型:

    epochs = 200
    for epoch in range(epochs):
        model.train()
        optimizer.zero_grad()
        out = model(X_train).reshape(-1,)
        loss = loss_fn(out, y_train)
        loss.backward()
        optimizer.step()
        if epoch % 10 == 0:
            print(f"Epoch: {epoch}, Loss: {loss.item()}")
    
  4. 最后,我们评估我们的模型:

    model.eval()
    y_pred = model(X_test).reshape(-1,)
    test_loss = loss_fn(y_pred, y_test)
    print(f"Test Loss: {test_loss.item()}")
    

它是如何工作的…

类似于 LSTM,GRU 也需要 3D 输入数据。我们首先相应地调整输入数据的形状。接下来,我们定义我们的 GRU 模型。这个模型包含一个 GRU 层和一个线性层。GRU 的初始隐藏状态被定义并初始化为零。

然后,我们定义我们的 loss 函数和优化器,训练模型。模型在最后一个时间步的输出用于预测。最后,我们在测试集上评估我们的模型,并打印测试损失。

还有更多内容…

有许多方法可以改进这个模型:

  • 尝试不同的 GRU 架构或调整 GRU 层数可能会得到更好的结果

  • 使用不同的损失函数或优化器也可能改善模型性能

  • 实现提前停止或其他正则化技术有助于防止过拟合

  • 应用更复杂的数据准备技术,如序列填充或截断,可以更好地帮助模型处理不同长度的序列

  • 更先进的模型,如序列到序列模型或 Transformer,可能会为更复杂的时间序列预测任务提供更好的结果

使用堆叠 LSTM 进行单变量预测

本配方将引导你完成使用多层 LSTM 神经网络进行单变量时间序列预测的过程。

准备开始

对于复杂的时间序列预测问题,单一的 LSTM 层可能不足以处理。在这种情况下,我们可以使用堆叠 LSTM,它本质上是多个 LSTM 层堆叠在一起。这可以提供更高层次的输入抽象,并可能提升预测性能。

我们将继续使用前一个配方中相同的重塑训练和测试数据集:

X_train = X_train.view([X_train.shape[0], X_train.shape[1], 1])
X_test = X_test.view([X_test.shape[0], X_test.shape[1], 1])

我们还使用了在单变量预测与 LSTM 配方中定义的 LSTM 神经网络:

class LSTM(nn.Module):
    def __init__(self, input_dim, hidden_dim, num_layers, output_dim):
        super(LSTM, self).__init__()
        self.hidden_dim = hidden_dim
        self.num_layers = num_layers
        self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers,
            batch_first=True)
        self.fc = nn.Linear(hidden_dim, output_dim)
    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0),
            self.hidden_dim).requires_grad_()
        c0 = torch.zeros(self.num_layers, x.size(0),
            self.hidden_dim).requires_grad_()
        out, (hn, cn) = self.lstm(x, (h0.detach(), c0.detach()))
        out = self.fc(out[:, -1, :])
        return out

我们将使用这些元素来训练一个堆叠 LSTM 模型。

如何实现…

要在 PyTorch 中构建堆叠 LSTM,我们需要调用 LSTM 类,并输入 num_layers=2,如下所示:

model = LSTM(input_dim=1, hidden_dim=32, output_dim=1, num_layers=2)

其余的训练过程与我们在前面配方中做的相似。我们定义了损失函数和 optimizer

loss_fn = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

我们训练模型:

epochs = 200
for epoch in range(epochs):
    model.train()
    optimizer.zero_grad()
    out = model(X_train).reshape(-1,)
    loss = loss_fn(out, y_train)
    loss.backward()
    optimizer.step()
    if epoch % 10 == 0:
        print(f"Epoch: {epoch}, Loss: {loss.item()}")

最后,我们评估我们的模型:

model.eval()
y_pred = model(X_test).reshape(-1,)
test_loss = loss_fn(y_pred, y_test)
print(f"Test Loss: {test_loss.item()}")

它是如何工作的…

堆叠 LSTM 模型的设置类似于单层 LSTM 模型。主要的区别在于 LSTM 层,我们指定要使用多个 LSTM 层。通过将 num_layers 设置为 2 或更多层来实现这一点。

堆叠 LSTM 的前向传递与单层 LSTM 相同。我们用零初始化隐藏状态h0和细胞状态c0,将输入和初始状态传入 LSTM 层,然后使用最后时间步的输出进行预测。

测试集的损失与之前的结果紧密对齐。多个因素可能导致了这一观察结果。可能是数据有限,或者数据的表达能力未能从我们模型的复杂性中受益。此外,我们没有进行任何超参数优化,这可能会进一步提高模型的性能。在随后的部分中,我们将更深入地探讨这些方面,探索潜在的解决方案和进一步改进的策略。

将 LSTM 与多个全连接层结合

有时,将不同类型的神经网络组合成一个模型可能是有价值的。在这个食谱中,你将学习如何将一个 LSTM 模块与全连接层结合,而全连接层是前馈神经网络的基础。

准备工作

在本节中,我们将使用一个混合模型,它将 LSTM 层与多个全连接(也称为密集)层结合。这使我们能够从序列中进一步抽象特征,然后学习到输出空间的复杂映射。

我们继续使用前几节中重塑过的训练和测试集。

如何做到……

为了在 PyTorch 中构建这个混合模型,我们在 LSTM 层后添加了两个全连接层:

class HybridLSTM(nn.Module):
    def __init__(self, input_dim, hidden_dim, 
        output_dim=1, num_layers=1):
        super(HybridLSTM, self).__init__()
        self.hidden_dim = hidden_dim
        self.num_layers = num_layers
        self.lstm = nn.LSTM(input_dim, hidden_dim,
            num_layers, batch_first=True)
        self.fc1 = nn.Linear(hidden_dim, 50)
        self.fc2 = nn.Linear(50, output_dim)
    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0),
            self.hidden_dim).to(x.device)
        c0 = torch.zeros(self.num_layers,x.size(0),
            self.hidden_dim).to(x.device)
        out, _ = self.lstm(x, (h0, c0))
        out = F.relu(self.fc1(out[:, -1, :]))
        out = self.fc2(out)
        return out
model = HybridLSTM(input_dim=1, hidden_dim=32, output_dim=1, 
    num_layers=1)

我们定义我们的损失函数和optimizer

loss_fn = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

我们像之前的食谱一样训练和评估我们的模型。

它是如何工作的……

混合 LSTM 模型的设置包括一个LSTM层,后跟两个全连接层。通过LSTM层后,最后时间步的输出被全连接层处理。在这些层之间使用ReLU()激活函数引入了非线性,使得我们的模型能够捕捉数据中的更复杂关系。

请注意,LSTM层的输出是一个形状为(batch_size, seq_length, hidden_dim)的张量。这是因为LSTM默认输出序列中每个时间步的隐藏状态,且每个批次项都如此。

在这个特定模型中,我们只关注最后一个时间步的隐藏状态,以输入到全连接层。我们通过out[:, -1, :]来实现这一点,有效地选择了批次中每个序列的最后一个时间步的隐藏状态。结果是一个形状为(batch_size, hidden_dim)的张量。

重塑后的输出通过self.fc1(out[:, -1, :])函数调用传递到第一个全连接(线性)层。该层有 50 个神经元,因此输出的形状变化为(batch_size, 50)

在应用ReLU激活函数后,输出会传递到第二个全连接层self.fc2(out),其大小等于output_dim,将张量的形状缩小为(batch_size, output_dim)。这是模型的最终输出。

记住,隐藏维度(hidden_dim)是 LSTM 的超参数,可以自由选择。第一个全连接层中的神经元数量(在本例中为50)也是一个超参数,可以根据特定任务进行调整。

还有更多…

在使用混合模型时,请考虑以下建议:

  • 改变全连接层的数量及其大小,以探索不同的模型复杂度。

  • 全连接层中使用不同的激活函数可能会导致不同的性能表现。

  • 随着模型复杂度的增加,计算成本也会增加。请务必平衡复杂度和计算效率。

使用 CNN 进行单变量预测

现在,我们将注意力转向卷积神经网络,这些网络在时间序列数据中也表现出了有前景的结果。让我们学习如何将这些方法用于单变量时间序列预测。

准备就绪

CNN 通常用于处理图像相关问题,但它们也可以应用于时间序列预测任务。通过将时间序列数据视为“序列图像”,CNN 可以从数据中提取局部特征和依赖关系。为了实现这一点,我们需要像处理 LSTM 模型一样准备时间序列数据。

如何实现…

让我们在 PyTorch 中定义一个简单的 CNN 模型。在这个示例中,我们将使用一个卷积层,后面跟着一个全连接层:

class CNNTimeseries(nn.Module):
    def __init__(self, input_dim, output_dim=1):
        super(CNNTimeseries, self).__init__()
        self.conv1 = nn.Conv1d(in_channels=input_dim,
                               out_channels=64,
                               kernel_size=3,
                               stride=1,
                               padding=1)
        self.fc = nn.Linear(in_features=64,
                            out_features=output_dim)
     def forward(self, x):
        x = F.relu(self.conv1(x))
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        return x
model = CNNTimeseries(input_dim=3, output_dim=1)

我们像之前的章节一样训练和评估我们的模型。

它是如何工作的…

CNN 模型基于卷积层,这些层用于直接从输入数据中提取局部特征。这些特征随后被传递到一个或多个全连接层,用来建模时间序列的未来值。这种类型的神经网络的训练阶段与其他模型类似,例如 LSTM。

让我们来看看我们的神经网络架构。它具有以下特点:

  • 一个输入层,接受形状为(batch_size, sequence_length, number_of_features)的时间序列数据。对于单变量时间序列预测,number_of_features1

  • 一个具有64个滤波器和3大小卷积核的卷积层,在 PyTorch 中定义为self.conv1 = nn.Conv1d(in_channels=1, out_channels=64, kernel_size=3)

  • 一个全连接(或线性)层,将卷积层的输出映射到我们的预测值。

让我们看看这些层如何转换数据:

  • (batch_size, sequence_length, 1)

  • Conv1d:对时间序列数据进行 1D 卷积。卷积核在序列上滑动,计算权重与输入的点积。经过这次卷积操作后,我们数据的形状为 (batch_size, out_channels, sequence_length-kernel_size+1),或者在这个例子中是 (batch_size, 64, sequence_length-3+1)

  • (batch_size, remaining_dims)remaining_dims 是通过乘以张量的剩余维度(在我们例子中是 64sequence_length-2)来计算的。最终的形状将是 (batch_size, 64 * (sequence_length-2))。我们可以通过使用 PyTorch 中的 view() 函数实现,如下所示:x = x.view(x.size(0), -1)

现在,x 已经准备好输入到全连接层 self.fc = nn.Linear(64 * (sequence_length-2), output_dim),其中 output_dim 是输出空间的维度,对于单变量时间序列预测来说是 1。该层的输出形状为 (batch_size, output_dim),即 (batch_size, 1),这些就是我们的最终预测结果。

这样,我们可以看到张量形状如何在通过每一层网络时得到处理和转换。理解这一过程对于故障排除和设计自己的架构至关重要。

还有更多…

CNN 可以通过多种方式进行扩展:

  • 可以堆叠多个卷积层来构建更深的网络

  • 卷积层后可以添加池化层,以减少维度和计算成本

  • 可以应用 Dropout 或其他正则化技术来防止过拟合

  • 模型可以扩展为 ConvLSTM,结合 CNN 和 LSTM 在处理空间和时间依赖性方面的优势

处理趋势——计算第一次差分

第一章 中,我们学习了不同的时间序列模式,如趋势或季节性。这个步骤描述了在训练深度神经网络之前处理时间序列趋势的过程。

准备就绪

正如我们在 第一章 中学到的,趋势是时间序列中的长期变化。当时间序列的平均值发生变化时,这意味着数据不是平稳的。非平稳时间序列更难建模,因此将数据转换为平稳序列非常重要。

趋势通常通过计算第一次差分来从时间序列中去除,直到数据变得平稳。

首先,让我们从将时间序列分成训练集和测试集开始:

from sklearn.model_selection import train_test_split
train, test = train_test_split(series, test_size=0.2, shuffle=False)

我们将最后 20% 的观察数据留作测试。

如何操作…

我们可以使用 pandas 通过两种方式计算连续观察值之间的差异:

  1. 让我们从使用 diff() 方法的标准方法开始:

    train.diff(periods=1)
    test.diff(periods=1)
    

    periods参数指明了用于计算差分的步数。在本例中,periods=1意味着我们计算连续观测值之间的差分,也就是第一差分。例如,将周期数设置为7将计算每个观测值与前 7 个时间步骤的观测值之间的差值。对于每日时间序列来说,这是一种有效的去除季节性的方法。不过,稍后会详细介绍这一点。

    另一种对时间序列进行差分的方法是使用shift()方法:

    train_shifted = train.shift(periods=1)
    train_diff = train - train_shifted
    test_shifted = test.shift(periods=1)
    test_diff = test - test_shifted
    
  2. 我们创建了一个移位了所需周期数(在本例中为1)的第二个时间序列。然后,我们将这个序列从原始序列中减去,得到一个差分后的序列。

    差分能够稳定序列的水平。不过,我们仍然可以将数据标准化为一个统一的值范围:

    scaler = MinMaxScaler(feature_range=(-1, 1))
    train_diffnorm = scaler.fit_transform(
        train_diff.values.reshape(-1, 1))
    test_diffnorm = scaler.transform(test_diff.values.reshape(-1,1))
    
  3. 最后,我们像前面的步骤一样,使用series_to_supervised()函数将时间序列转换为监督学习格式:

    train_df = series_to_supervised(train_diffnorm, n_in=3).values
    test_df = series_to_supervised(test_diffnorm, n_in=3).values
    
  4. 模型训练阶段将与之前的步骤相同:

    X_train, y_train = train_df[:, :-1], train_df[:, -1] 
    X_test, y_test = test_df[:, :-1], test_df[:, -1]
    X_train = torch.from_numpy(X_train).type(torch.Tensor)
    X_test = torch.from_numpy(X_test).type(torch.Tensor) 
    y_train = torch.from_numpy(y_train).type(torch.Tensor).view(-1) 
    y_test = torch.from_numpy(y_test).type(torch.Tensor).view(-1) 
    X_train = X_train.view([X_train.shape[0], X_train.shape[1], 1])
    X_test = X_test.view([X_test.shape[0], X_test.shape[1], 1])
    model = LSTM(input_dim=1, hidden_dim=32, output_dim=1, 
        num_layers=2)
    loss_fn = nn.MSELoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    epochs = 200
    for epoch in range(epochs):
        model.train()
        optimizer.zero_grad()
        out = model(X_train).reshape(-1, )
        loss = loss_fn(out, y_train)
        loss.backward()
        optimizer.step()
        if epoch % 10 == 0:
            print(f"Epoch: {epoch}, Loss: {loss.item()}")
    
  5. 但我们的工作还没有完成。前面提到的神经网络是基于差分数据训练的。因此,预测结果也是差分过的:

    model.eval()
    y_pred = model(X_test).reshape(-1, )
    
  6. 接下来,我们需要恢复数据转换过程,以便在时间序列的原始尺度上获取预测结果。

  7. 首先,我们对时间序列进行反标准化:

    y_pred_np = y_pred.detach().numpy().reshape(-1, 1)
    y_diff = scaler.inverse_transform(y_pred_np).flatten()
    
  8. 然后,我们通过加回移位后的时间序列来恢复差分操作:

    y_orig_scale = y_diff + test_shifted.values[4:]
    

在前面的代码中,我们跳过了前三个值,因为它们在通过series_to_supervised()函数进行转换过程中已经被使用。

其工作原理……

差分通过稳定时间序列的水平使其变为平稳。神经网络不直接建模序列的实际值,而是建模变化序列;即时间序列如何从一个时间步骤变化到另一个时间步骤。神经网络输出的原始预测结果代表了预测的变化。我们需要恢复差分过程,才能获得原始尺度上的预测结果。

还有更多内容……

你也可以通过在输入数据中加入时间信息来处理趋势。一个表示每个观测值收集时步骤的解释变量。例如,第一个观测值的值为1,第二个观测值的值为2。这种方法在趋势是确定性且我们不期望其发生变化时是有效的。差分提供了一种更通用的处理趋势的方法。

处理季节性——季节虚拟变量和傅里叶级数

在本篇中,我们将描述如何使用季节性虚拟变量和傅里叶级数处理时间序列中的季节性。

准备工作

季节性代表了在给定周期内重复出现的模式,例如每年一次。季节性是时间序列中的一个重要组成部分,捕捉季节性非常重要。文献中的共识是,神经网络无法最优地捕捉季节性效应。建模季节性最好的方法是通过特征工程或数据转换。处理季节性的一种方式是添加额外的信息,捕捉模式的周期性。这可以通过季节性虚拟变量或傅里叶级数来完成。

我们首先通过使用series_to_supervised()函数准备数据:

train, test = train_test_split(series, test_size=0.2, shuffle=False)
scaler = MinMaxScaler(feature_range=(-1, 1))
train_norm = scaler.fit_transform(
    train.values.reshape(-1, 1)).flatten()
train_norm = pd.Series(train_norm, index=train.index)
test_norm = scaler.transform(test.values.reshape(-1, 1)).flatten()
test_norm = pd.Series(test_norm, index=test.index)
train_df = series_to_supervised(train_norm, 3)
test_df = series_to_supervised(test_norm, 3)

在这个食谱中,为了简单起见,我们将跳过趋势移除部分,专注于建模季节性。因此,train_dftest_df对象包含训练集和测试集的滞后值。

如何做……

季节性虚拟变量和傅里叶级数都可以作为额外的解释变量添加到输入数据中。让我们首先探索季节性虚拟变量。

季节性虚拟变量

季节性虚拟变量是描述每个观测周期的二进制变量。例如,给定的值是否在周一收集。

为了构建季节性虚拟变量,我们首先获取每个观测点的周期信息。这可以通过以下方式使用sktimeDateTimeFeatures类来完成:

from sktime.transformations.series.date import DateTimeFeatures
date_features = DateTimeFeatures(ts_freq='D', 
    keep_original_columns=False, feature_scope='efficient')
train_dates = date_features.fit_transform(train_df.iloc[:, -1])

DateTimeFeatures的主要参数是ts_freq,我们将其设置为D。这意味着我们告诉此方法,我们的数据是按日粒度进行处理的。然后,我们使用训练集拟合DateTimeFeatures对象,传递该数据的前几期观察值(train_df.iloc[:, -1])。这会生成一个包含以下表格中详细信息的pandas DataFrame:

表 3.2:每个观测周期的周期信息

表 3.2:每个观测周期的周期信息

为了简化操作,我们将继续使用包含星期几和年份月份信息的列。我们可以通过以下代码获取这些列:

train_dates = train_dates[['month_of_year', 'day_of_week']]

然后,我们使用sklearn的一个热编码方法(OneHotEncoder)将这些数据转换为二进制变量:

from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(drop='first', sparse=False) 
encoded_train = encoder.fit_transform(train_dates) 
train_dummies = pd.DataFrame(encoded_train, 
    columns=encoder.get_feature_names_out(),dtype=int)

这会得到一组季节性虚拟变量,如下表所示:

表 3.3:每个观测周期的二进制变量信息

表 3.3:每个观测周期的二进制变量信息

我们使用测试集重复此过程:

test_dates = date_features.transform(test_df.iloc[:, -1]) 
test_dates = test_dates[['month_of_year', 'day_of_week']]
test_encoded_feats = encoder.transform(test_dates)
test_dummies = pd.DataFrame(test_encoded_feats,
                            columns=encoder.get_feature_names_out(),
                            dtype=int)

注意,我们在训练数据上使用DateTimeFeaturesOneHotEncoder进行拟合(使用fit_transform()方法)。对于测试集,我们可以使用相应对象的transform()方法。

傅里叶级数

傅里叶级数由确定性的正弦波和余弦波组成。这些波的振荡使得季节性能够建模为一种重复的模式。

我们可以通过以下方式使用sktime计算基于傅里叶的特征:

from sktime.transformations.series.fourier import FourierFeatures
fourier = FourierFeatures(sp_list=[365.25],
                          fourier_terms_list=[2],
                          keep_original_columns=False)
train_fourier = fourier.fit_transform(train_df.iloc[:, -1]) 
test_fourier = fourier.transform(test_df.iloc[:, -1])

我们使用FourierFeatures转换器提取傅里叶特征。该操作器有两个主要参数:

  • sp_list:数据的周期性。在这个例子中,我们将该参数设置为365.25,它捕捉了年度变化。

  • fourier_terms_list:每个正弦和余弦函数的傅里叶波数。我们将此参数设置为2,即计算2个正弦序列加上2个余弦序列。

建模

在提取了季节性虚拟变量和傅里叶级数后,我们将额外的变量添加到数据集中:

X_train = np.hstack([X_train, train_dummies, train_fourier])
X_test = np.hstack([X_test, test_dummies, test_fourier])

np.hstack()函数用于水平合并多个数组(按列合并)。在这种情况下,我们将季节性虚拟变量和傅里叶级数与使用series_to_supervised()函数计算的滞后特征合并。

最后,我们将这些数据输入神经网络,就像我们在之前的食谱中所做的那样:

X_train = torch.from_numpy(X_train).type(torch.Tensor) 
X_test = torch.from_numpy(X_test).type(torch.Tensor) 
y_train = torch.from_numpy(y_train).type(torch.Tensor).view(-1) 
y_test = torch.from_numpy(y_test).type(torch.Tensor).view(-1)
X_train = X_train.view([X_train.shape[0], X_train.shape[1], 1])
X_test = X_test.view([X_test.shape[0], X_test.shape[1], 1])
model = LSTM(input_dim=1, hidden_dim=32, output_dim=1, num_layers=2)
loss_fn = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) 
epochs = 200
for epoch in range(epochs):
    model.train()
    optimizer.zero_grad()
    out = model(X_train).reshape(-1, )
    loss = loss_fn(out, y_train)
    loss.backward()
    optimizer.step()
    if epoch % 10 == 0:
        print(f"Epoch: {epoch}, Loss: {loss.item()}")
model.eval()
y_pred = model(X_test).reshape(-1, )
test_loss = loss_fn(y_pred, y_test)
y_pred_np = y_pred.detach().numpy().reshape(-1, 1)
y_pred_orig = scaler.inverse_transform(y_pred_np).flatten()

在使用季节性虚拟变量或傅里叶级数时,推断步骤后无需执行任何额外的转换。在之前的代码中,我们逆转了标准化过程,以便在其原始尺度上获得预测结果。

它是如何工作的……

季节性虚拟变量和傅里叶级数是捕捉季节性模式重复的变量。它们作为解释变量,添加到输入数据中。傅里叶级数的周期性特征如下图所示:

图 3.1:捕捉季节性的傅里叶确定性序列

图 3.1:捕捉季节性的傅里叶确定性序列

请注意,这一过程与用于训练的神经网络无关。在这个食谱中,我们使用了 TCN,但我们也可以选择任何用于多重回归的学习算法。

还有更多……

傅里叶级数或季节性虚拟变量的替代方法是重复基函数。与使用三角级数不同,季节性通过径向基函数来建模。这些在sklego Python包中实现。你可以查看以下链接的文档:scikit-lego.netlify.app/api/preprocessing.html#sklego.preprocessing.RepeatingBasisFunction

有时,时间序列可能会在多个周期内表现出季节性。例如,示例中的日度时间序列不仅每月会有重复的模式,还可能每年重复。在本例中,我们计算了季节性虚拟变量,这些变量提供了有关不同周期的信息,即月份和星期几。但你也可以通过传递多个周期来使用傅里叶级数。以下是如何使用傅里叶级数捕捉每周和每年的季节性:

fourier = FourierFeatures(sp_list=[7, 365.25],
                          fourier_terms_list=[2, 2],
                          keep_original_columns=False)

上述代码将为每个周期计算个傅里叶级数(每个周期两个正弦和两个余弦波)。

另一个在时间序列中常见的重要现象是节假日,其中一些节假日是每年变动的(例如复活节)。一种常见的建模这些事件的方法是使用二元虚拟变量。

处理季节性 – 季节性差分

在这个方法中,我们展示了如何使用差分来建模时间序列中的季节性模式。

准备工作

我们已经学会了使用第一次差分来去除时间序列中的趋势。差分也可以用于季节性。但是,不是取连续观察值之间的差异,而是对每个点,从同一季节中减去前一年的相应观测值。例如,假设你正在建模月度数据。你通过从当前年 2 月的值中减去上一年 2 月的值来执行季节性差分。

该过程与我们通过第一次差分去除趋势时做的相似。我们首先加载数据:

time_series = df["Incoming Solar"]
train, test = train_test_split(time_series, test_size=0.2, shuffle=False)

在这个方法中,我们将使用季节性差分来去除每年的季节性。

如何操作…

我们使用shift()方法来应用差分操作:

periods = 365
train_shifted = train.shift(periods=periods)
train_diff = train - train_shifted
test_shifted = test.shift(periods=periods)
test_diff = test - test_shifted
scaler = MinMaxScaler(feature_range=(-1, 1))
train_diffnorm = scaler.fit_transform(train_diff.values.reshape(-1,1))
test_diffnorm = scaler.transform(test_diff.values.reshape(-1, 1))
train_df = series_to_supervised(train_diffnorm, 3).values
test_df = series_to_supervised(test_diffnorm, 3).values

差分序列后,我们使用series_to_supervised将其转化为监督学习格式。然后,我们可以用差分后的数据训练神经网络:

X_train, y_train = train_df[:, :-1], train_df[:, -1] 
X_test, y_test = test_df[:, :-1], test_df[:, -1]
X_train = torch.from_numpy(X_train).type(torch.Tensor) 
X_test = torch.from_numpy(X_test).type(torch.Tensor) 
y_train = torch.from_numpy(y_train).type(torch.Tensor).view(-1) 
y_test = torch.from_numpy(y_test).type(torch.Tensor).view(-1)
X_train = X_train.view([X_train.shape[0], X_train.shape[1], 1])
X_test = X_test.view([X_test.shape[0], X_test.shape[1], 1])
model = LSTM(input_dim=1, hidden_dim=32, output_dim=1, num_layers=2)
loss_fn = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
epochs = 200
for epoch in range(epochs):
    model.train()
    optimizer.zero_grad()
    out = model(X_train).reshape(-1, )
    loss = loss_fn(out, y_train)
    loss.backward()
    optimizer.step()
    if epoch % 10 == 0:
        print(f"Epoch: {epoch}, Loss: {loss.item()}")

在这种情况下,我们需要反转差分操作,以便获得原始时间序列尺度上的预测值。我们按以下方式操作:

model.eval()
y_pred = model(X_test).reshape(-1, )
y_diff = scaler.inverse_transform(
    y_pred.detach().numpy().reshape(-1, 1)).flatten()
y_original = y_diff + test_shifted.values[(periods+3):]

本质上,我们将位移后的测试序列添加回去,与去标准化后的预测值结合。

它是如何工作的…

季节性差分去除了周期性变化,从而稳定了序列的水平,使其平稳。

当季节性模式在幅度和周期性上发生变化时,季节性差分特别有效。在这种情况下,季节性差分通常比季节性虚拟变量或傅里叶级数更有效。

处理季节性 – 季节性分解

这个方法描述了另一种建模季节性的方法,这次使用时间序列分解方法。

准备工作

我们在第一章中学习了时间序列分解方法。分解方法旨在提取组成时间序列的各个部分。

我们可以使用这种方法来处理季节性。其思想是将季节性成分与其余部分(趋势加残差)分开。我们可以使用深度神经网络来建模季节调整后的序列。然后,使用简单模型来预测季节成分。

再次,我们将从日常太阳辐射时间序列开始。这一次,我们不拆分训练和测试数据,以展示预测是如何在实践中获得的。

如何操作…

我们首先使用 STL 对时间序列进行分解。在第一章中我们学习了这种方法:

from statsmodels.tsa.api import STL
series_decomp = STL(series, period=365).fit()
seas_adj = series – series_decomp.seasonal

季节调整后的序列和季节成分如下图所示:

图 3.2:季节部分和剩余的季节调整后序列

图 3.2:季节性部分和剩余的季节性调整序列

然后,我们使用 LSTM 模型对季节性调整后的序列进行建模。我们将采用类似于之前在其他配方中的方法:

scaler = MinMaxScaler(feature_range=(-1, 1))
train_norm = scaler.fit_transform(
    seas_adj.values.reshape(-1, 1)).flatten()
train_norm = pd.Series(train_norm, index=time_series.index)
train_df = series_to_supervised(train_norm, 3)
X_train, y_train = train_df.values[:, :-1], train_df.values[:, -1]
X_train = torch.from_numpy(X_train).type(torch.Tensor)
y_train = torch.from_numpy(y_train).type(torch.Tensor).view(-1)
X_train = X_train.view([X_train.shape[0], X_train.shape[1], 1])
model = LSTM(input_dim=1, hidden_dim=32, output_dim=1, num_layers=2)
loss_fn = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
epochs = 200
for epoch in range(epochs):
    model.train()
    optimizer.zero_grad()
    out = model(X_train).reshape(-1, )
    loss = loss_fn(out, y_train)
    loss.backward()
    optimizer.step()

上述代码在季节性调整后的序列上训练了 LSTM 模型。现在,我们用它来预测接下来 14 天的数据:

latest_obs = train_norm.tail(3)
latest_obs = latest_obs.values.reshape(1, 3, -1)
latest_obs_t = torch.from_numpy(latest_obs).type(torch.Tensor)
model.eval()
y_pred = model(latest_obs_t).reshape(-1, ).detach().numpy()
y_denorm = scaler.inverse_transform(y_pred.reshape(-1,1)).flatten()

这就是我们在前面的代码中看到的:

  1. 我们获取时间序列中的最新三个滞后值,并将其结构化为输入数据

  2. 我们使用模型预测序列的下一个值

  3. 然后,我们使用scaler对象对预测结果进行去归一化处理

现在,我们需要预测季节性成分。这通常通过季节性简单方法来完成。在本节中,我们将使用sktime包中的实现:

from sktime.forecasting.naive import NaiveForecaster
seas_forecaster = NaiveForecaster(stra'egy='last', sp=365)
seas_forecaster.fit(series_decomp.seasonal)
seas_preds = seas_forecaster.predict(fh=[1])

NaiveForecaster对象与季节性成分拟合。该方法的思路是使用来自同一季节的已知前一个值来预测未来的观测值。

最后,我们通过加上这两个预测结果得到最终的预测值:

preds = y_denorm + seas_preds

这个加法操作恢复了之前执行的分解过程,我们得到了原始序列尺度下的预测值。

它是如何工作的…

使用分解方法建模季节性,涉及去除季节性部分,并使用神经网络对季节性调整后的时间序列进行建模。另一个更简单的模型用于预测季节性部分的未来。

这个过程不同于使用季节性虚拟变量、傅里叶级数或季节差分的情况。季节性虚拟变量或傅里叶级数作为额外的输入变量,用于神经网络建模。而在分解或差分的情况下,时间序列会在建模之前进行变换。这意味着在使用神经网络进行预测后,我们需要恢复这些变换。对于分解来说,这意味着要加上季节性部分的预测值。差分也是通过加回来自同一季节的前一个值来恢复的。

处理非恒定方差 – 对数变换

我们已经学会了如何处理由于趋势或季节性模式导致的时间序列水平变化。在本节中,我们将处理时间序列方差的变化。

准备工作

我们在第一章中已经学到,一些时间序列是异方差的,这意味着方差随时间变化。非恒定方差是一个问题,因为它使得学习过程更加困难。

让我们开始将太阳辐射时间序列分割为训练集和测试集:

train, test = train_test_split(time_series, test_size=0.2, 
    shuffle=False)

再次,我们将最后的 20%的观测值留作测试。

如何操作…

我们将展示如何使用对数变换和 Box-Cox 幂变换来稳定时间序列的方差。

对数变换

第一章中,我们定义了应用对数变换的LogTransformation类:

import numpy as np
class LogTransformation:
    @staticmethod
    def transform(x):
        xt = np.sign(x) * np.log(np.abs(x) + 1)
        return xt
    @staticmethod
    def inverse_transform(xt):
        x = np.sign(xt) * (np.exp(np.abs(xt)) - 1)
        return x

你可以按照如下方式应用变换:

train_log = LogTransformation.transform(train)
test_log = LogTransformation.transform(test)

train_logtest_log对象是具有稳定方差的转换后的数据集。

Box-Cox 转换

对数通常是稳定方差的有效方法,它是 Box-Cox 方法的一个特例。你可以使用scipy中的boxcox()函数应用这种方法:

from scipy import stats
train_bc, bc_lambda = stats.boxcox(train)
train_bc = pd.Series(train_bc, index=train.index)

Box-Cox 方法依赖于lambda参数(bc_lambda),我们使用训练集来估算该参数。然后,我们也用它来转换测试集:

test_bc = stats.boxcox(test, lmbda=bc_lambda)
test_bc = pd.Series(test_bc, index=test.index)

在使用对数或 Box-Cox 转换后,我们将训练一个神经网络。

建模

训练过程与我们在之前的配方中所做的完全相同。我们将继续使用经过对数转换的系列进行该配方(但 Box-Cox 情况下的过程也是相同的):

scaler = MinMaxScaler(feature_range=(-1, 1))
train_norm = scaler.fit_transform(train_log.values.reshape(-1, 1))
test_norm = scaler.transform(test_log.values.reshape(-1, 1))
train_df = series_to_supervised(train_norm, 3).values
test_df = series_to_supervised(test_norm, 3).values
X_train, y_train = train_df[:, :-1], train_df[:, -1]
X_test, y_test = test_df[:, :-1], test_df[:, -1]
X_train = torch.from_numpy(X_train).type(torch.Tensor)
X_test = torch.from_numpy(X_test).type(torch.Tensor)
y_train = torch.from_numpy(y_train).type(torch.Tensor).view(-1)
y_test = torch.from_numpy(y_test).type(torch.Tensor).view(-1)
X_train = X_train.view([X_train.shape[0], X_train.shape[1], 1])
X_test = X_test.view([X_test.shape[0], X_test.shape[1], 1])
model = LSTM(input_dim=1, hidden_dim=32, output_dim=1, num_layers=2)
loss_fn = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
epochs = 200
for epoch in range(epochs):
    model.train()
    optimizer.zero_grad()
    out = model(X_train).reshape(-1, )
    loss = loss_fn(out, y_train)
    loss.backward()
    optimizer.step()

训练完毕后,我们会在测试集上运行模型。预测结果需要恢复到时间序列的原始尺度。这可以通过以下代码完成:

model.eval()
y_pred = model(X_test).reshape(-1, )
y_pred_np = y_pred.detach().numpy().reshape(-1, 1)
y_pred_denorm = scaler.inverse_transform(y_pred_np).flatten()
y_pred_orig = LogTransformation.inverse_transform(y_pred_denorm)

在将预测结果去归一化后,我们还使用inverse_transform()方法来恢复对数转换。对于 Box-Cox 转换,以下是可以执行的步骤:

from scipy.special import inv_boxcox
y_pred_orig = inv_boxcox(y_pred_denorm, bc_lambda)

在前面的代码中,我们传入了转换后的预测结果和bc_lambda转换参数,以获取原始尺度下的预测值。

它是如何工作的…

本文中进行的处理旨在缓解方差不稳定的问题。对数转换和 Box-Cox 方法都可以用来稳定方差。这些方法还可以将数据拉近正态分布。这种转换有助于神经网络的训练,因为它有助于避免优化过程中的饱和区域。

转换方法直接作用于输入数据,因此它们与学习算法无关。模型使用转换后的数据,这意味着预测结果需要转换回时间序列的原始尺度。

第四章:使用 PyTorch Lightning 进行预测

在本章中,我们将使用 PyTorch Lightning 构建预测模型。我们将探讨该框架的几个方面,例如创建数据模块来处理数据预处理,或创建 LightningModel 结构来封装神经网络的训练过程。我们还将探索 TensorBoard 来监控神经网络的训练过程。接下来,我们将描述几种用于评估深度神经网络预测效果的指标,如 均方绝对缩放误差 (MASE) 和 对称平均绝对百分比误差 (SMAPE)。在本章中,我们将重点讨论多变量时间序列,这些序列包含多个变量。

本章将引导你完成以下几个实例:

  • 准备多变量时间序列进行监督学习

  • 使用多变量时间序列训练线性回归预测模型

  • 用于多变量时间序列预测的前馈神经网络

  • 用于多变量时间序列预测的 LSTM 神经网络

  • 评估深度神经网络的预测效果

  • 使用 Tensorboard 监控训练过程

  • 使用回调函数 – EarlyStopping

技术要求

在本章中,我们将使用以下 Python 库,所有这些库都可以通过 pip 安装:

  • PyTorch Lightning (2.1.4)

  • PyTorch Forecasting (1.0.0)

  • torch (2.2.0)

  • ray (2.9.2)

  • numpy (1.26.3)

  • pandas (2.1.4)

  • scikit-learn (1.4.0)

  • sktime (0.26.0)

本章的代码可以在本书的 GitHub 仓库找到:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

准备多变量时间序列进行监督学习

本章的第一个实例解决了如何准备多变量时间序列进行监督学习的问题。我们将展示在上一章中使用的滑动窗口方法如何扩展来解决这个任务。接着,我们将演示如何使用 TimeSeriesDataSet(一个 PyTorch Forecasting 类,用于处理时间序列的预处理步骤)来准备时间序列数据。

准备工作

我们将使用在 第一章 中分析的相同时间序列。我们需要使用以下代码,通过 pandas 加载数据集:

import pandas as pd
data = pd.read_csv('assets/daily_multivariate_timeseries.csv',
                   parse_dates=['Datetime'],
                   index_col='Datetime')

下图展示了时间序列的示例。请注意,为了便于可视化,坐标轴已被转置:

图 4.1:多变量时间序列示例。系列的变量显示在 x 轴上,以便于可视化

图 4.1:多变量时间序列示例。系列的变量显示在 x 轴上,以便于可视化

上述数据集包含九个与气象条件相关的变量。就像在 第三章 中一样,目标是预测下一个太阳辐射值。我们将使用额外可用变量的滞后值作为输入解释变量。在下一章中,你将学习如何为需要预测多个变量的情况准备多变量时间序列。

如何做到这一点……

我们将对多变量时间序列进行监督学习的转换。首先,我们将描述如何使用我们在 第三章 中使用的滑动窗口方法。然后,我们将展示如何使用基于 PyTorch 的 TimeSeriesDataSet 数据结构简化此过程。

使用滑动窗口

在上一章中,我们使用滑动窗口方法将单变量时间序列从一个序列转换为矩阵格式。为监督学习准备多变量时间序列需要类似的过程:我们对每个变量应用滑动窗口技术,然后将结果合并。这个过程可以按以下方式进行:

TARGET = 'Incoming Solar'
N_LAGS = 7
HORIZON = 1
input_data = []
output_data = []
for i in range(N_LAGS, data.shape[0]-HORIZON+1):
    input_data.append(data.iloc[i - N_LAGS:i].values)
    output_data.append(data.iloc[i:(i+HORIZON)][TARGET])
input_data, output_data = np.array(input_data), np.array(output_data)

上述代码遵循以下步骤:

  1. 首先,我们定义滞后数和预测视野。我们将滞后数设置为 7N_LAGS=7),预测视野设置为 1HORIZON=1),目标变量设置为 Incoming Solar.

  2. 然后,我们遍历多变量时间序列中的每个时间步骤。在每个点,我们检索前 N_LAGS 的数据,将其添加到 input_data 中,并将下一个太阳辐射值添加到输出数据中。这意味着我们将使用每个变量的过去 7 个值来预测下一个太阳辐射值。

  3. 最后,我们将输入和输出数据从 Python 列表转换为 NumPy array 结构。

output_data 是一个一维向量,表示未来的太阳辐射值。input_data 有三个维度:第一个维度表示样本数量,第二个维度表示滞后数量,第三个维度表示序列中的变量数量。

使用 TimeSeriesDataSet

到目前为止,我们一直在使用滑动窗口方法来预处理时间序列,供监督学习使用。这个功能和训练神经网络所需的其他预处理任务都通过 TimeSeriesDataSet 类进行了自动化,该类可在 PyTorch Forecasting 库中找到。

TimeSeriesDataSet 提供了一种简单且有效的方法来准备数据并将其传递给模型。让我们来看一下如何使用这个结构来处理多变量时间序列。首先,我们需要将时间序列组织成一个包含三种主要信息的 pandas DataFrame 结构:

  • group_id:一个列,用于标识时间序列的名称。如果数据集包含单一时间序列,该列将显示一个常量值。有些数据集涉及多个时间序列,可以通过此变量区分。

  • time_index:存储某一时间点上给定序列捕获的值。

  • 其他变量:存储时间序列值的额外变量。多变量时间序列包含多个变量。

我们的时间序列已经包含了多个变量。现在,我们需要添加关于time_indexgroup_id的信息,可以通过如下方式完成:

mvtseries['group_id'] = 0
mvtseries['time_index'] = np.arange(mvtseries.shape[0])

group_id的值始终为0,因为我们正在处理单个时间序列。我们随便使用0。你可以使用任何适合的名称。我们使用np.arange``()函数来创建这个时间序列的time_index。这会创建一个变量,对第一个观察值给出0,对第二个观察值给出1,依此类推。

然后,我们必须创建TimeSeriesDataSet类的一个实例,如下所示:

dataset = TimeSeriesDataSet(
    data=mvtseries,
    group_ids=["group_id"],
    target="Incoming Solar",
    time_idx="time_index",
    max_encoder_length=7,
    max_prediction_length=1,
    time_varying_unknown_reals=['Incoming Solar',
                                'Wind Dir',
                                'Snow Depth',
                                'Wind Speed',
                                'Dewpoint',
                                'Precipitation',
                                'Vapor Pressure',
                                'Relative Humidity',
                                'Air Temp'],
)

我们可以将TimeSeriesDataSet数据集转换为DataLoader类,如下所示:

data_loader = dataset.to_dataloader(batch_size=1, shuffle=False)

DataLoader用于将观察值传递给模型。以下是一个观察值的示例:

x, y = next(iter(data_loader))
x['encoder_cont']
y

我们使用next``()iter``()方法从数据加载器中获取一个观察值。这个观察值被存储为xy,分别表示输入和输出数据。主要的输入是encoder_cont项,表示每个变量的7个滞后值。这个数据是一个 PyTorch 张量,形状为(1, 7, 9),表示(批量大小、滞后数、变量数)。批量大小是一个参数,表示神经网络每次训练迭代中使用的样本数。输出数据是一个浮动值,表示太阳辐射变量的下一个值。

它是如何工作的……

TimeSeriesDataSet构造函数需要一些参数:

  • data:一个包含之前描述的三个元素的时间序列数据集

  • group_idsdata中标识数据集每个时间序列的列

  • targetdata中我们想要预测的列(目标变量)

  • time_idxdata中包含每个观察值的时间信息的列

  • max_encoder_length:用于构建自回归模型的滞后数

  • max_prediction_length:预测视野——即,应该预测多少未来时间步长

  • time_varying_unknown_realsdata中列出的描述哪些数值变量随时间变化的列

还有其他与time_varying_unknown_reals相关的参数。这个特定的输入详细描述了所有未来值对用户未知的数值观测值,例如我们想要预测的变量。然而,在某些情况下,我们知道一个观测值的未来值,例如产品价格。这类变量应该包含在time_varying_known_reals输入中。还有time_varying_known_categoricalstime_varying_unknown_categoricals输入,可用于代替数值型变量的分类变量。

关于预测任务,我们在这个示例中进行的转换是名为自回归分布滞后模型ARDL)的一种建模方法的基础。ARDL 是自回归的扩展,也包括外生变量的滞后作为输入。

使用多元时间序列训练线性回归模型进行预测

在这个示例中,我们将使用 PyTorch 训练一个线性回归模型,作为我们第一个在多元时间序列上拟合的预测模型。我们将展示如何使用TimeSeriesDataSet处理训练模型的数据预处理步骤,并将数据传递给模型。

准备工作

我们将从之前示例中使用的mvtseries数据集开始:

import pandas as pd
mvtseries = pd.read_csv('assets/daily_multivariate_timeseries.csv',
            parse_dates=['datetime'],
            index_col='datetime')

现在,让我们看看如何使用这个数据集来训练一个 PyTorch 模型。

如何做到这一点…

在接下来的代码中,我们将描述准备时间序列和构建线性回归模型所需的步骤:

  1. 我们从预处理时间序列开始。这包括创建组标识符和时间索引列:

    mvtseries["target"] = mvtseries["Incoming Solar"]
    mvtseries["time_index"] = np.arange(mvtseries.shape[0])
    mvtseries["group_id"] = 0
    
  2. 然后,我们必须将数据划分为不同的部分。对于这个示例,我们只保留训练集的索引:

    time_indices = data["time_index"].values
    train_indices, _ = train_test_split(
        time_indices,
        test_size=test_size,
        shuffle=False)
    train_indices, _ = train_test_split(train_indices,
                                        test_size=0.1,
                                        shuffle=False)
    train_df = data.loc[data["time_index"].isin(train_indices)]
     train_df_mod = train_df.copy()
    
  3. 然后,我们必须使用StandardScaler操作符对时间序列进行标准化:

    target_scaler = StandardScaler()
    target_scaler.fit(train_df_mod[["target"]])
    train_df_mod["target"] = target_scaler.transform
        (train_df_mod[["target"]])
    train_df_mod = train_df_mod.drop("Incoming Solar", axis=1)
     feature_names = [
        col for col in data.columns
        if col != "target" and col != "Incoming Solar"
    ]
    
  4. 预处理后的时间序列被传递给一个TimeSeriesDataSet实例:

    training_dataset = TimeSeriesDataSet(
        train_df_mod,
        time_idx="time_index",
        target="target",
        group_ids=["group_id"],
        max_encoder_length=n_lags,
        max_prediction_length=horizon,
        time_varying_unknown_reals=feature_names,
        scalers={name: StandardScaler()
                 for name in feature_names},
    )
    loader = training_dataset.to_dataloader(batch_size=batch_size,
                                            shuffle=False)
    

    TimeSeriesDataSet对象被转换成一个数据加载器,可以用于将样本批次传递给模型。这是通过to_dataloader()方法完成的。我们将所有这些数据准备步骤封装成一个名为create_training_set的函数。你可以在本书的 GitHub 仓库中查看该函数的源代码。

  5. 接下来,我们调用create_training_set()函数来创建训练数据集:

    N_LAGS = 7
    HORIZON = 1
    BATCH_SIZE = 10
    data_loader = create_training_set(
        data=mvtseries,
        n_lags=N_LAGS,
        horizon=HORIZON,
        batch_size=BATCH_SIZE,
        test_size=0.3
    )
    
  6. 然后,我们必须使用 PyTorch 定义线性回归模型,如下所示:

    import torch
    from torch import nn
    class LinearRegressionModel(nn.Module):
        def __init__(self, input_dim, output_dim):
            super(LinearRegressionModel, self).__init__()
            self.linear = nn.Linear(input_dim, output_dim)
        def forward(self, X):
            X = X.view(X.size(0), -1)
            return self.linear(X)
    

    在这里,我们定义了一个名为LinearRegressionModel的类,来实现多元线性回归模型。它包含一个线性变换层(nn.Linear)。这个类接受输入和输出的大小作为输入,分别对应train_inputtrain_output对象的第二维。我们通过传入这些参数来创建该模型。

  7. 现在,我们将按照以下方式创建这个模型的一个实例:

    num_vars = mvtseries.shape[1] + 1
    model = LinearRegressionModel(N_LAGS * num_vars, HORIZON)
    

    num_vars包含时间序列中的变量数量。然后,我们将模型的输入定义为num_vars乘以N_LAGS,输出则定义为预测时长。

  8. 我们可以使用以下代码进行训练过程:

    criterion = nn.MSELoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    num_epochs = 10
    for epoch in range(num_epochs):
        for batch in data_loader:
            x, y = batch
            X = x["encoder_cont"].squeeze(-1)
            y_pred = model(X)
            y_pred = y_pred.squeeze(1)
            y_actual = y[0].squeeze(1)
            loss = criterion(y_pred, y_actual)
            loss.backward()
            optimizer.step()
            optimizer.zero_grad()
        print(f"epoch: {epoch + 1}, loss = {loss.item():.4f}")
    

    在这里,我们将学习率设置为0.001,优化器设置为 Adam。Adam 是一个常见的替代方法,比 SGD 等方法具有更好的收敛特性。

    在每个训练周期,我们从数据加载器中获取每个批次的滞后,并使用模型对其进行处理。注意,每个批次都会被重新调整为线性模型所需的二维格式。这是在LinearRegressionModel类的forward()方法中完成的。

它是如何工作的…

我们使用TimeSeriesDataSet类来处理数据准备过程。然后,我们通过to_dataloader()方法将数据集转换为DataLoader类。这个数据加载器为模型提供数据批次。虽然我们没有显式定义它,但每个批次都遵循自回归的建模方式。输入基于时间序列的过去几次观测,输出代表未来的观测值。

我们将线性回归模型实现为一个类,以便它遵循与上一章相同的结构。为了简化,我们可以通过model = nn.Linear(input_size, output_size)来创建模型。

用于多变量时间序列预测的前馈神经网络

在这个食谱中,我们将重新关注深度神经网络。我们将展示如何使用深度前馈神经网络为多变量时间序列构建预测模型。我们将描述如何将DataModule类与TimeSeriesDataSet结合,以封装数据预处理步骤。我们还将把PyTorch模型放在LightningModule结构中,这样可以标准化神经网络的训练过程。

准备就绪

我们将继续使用与太阳辐射预测相关的多变量时间序列:

import pandas as pd
mvtseries = pd.read_csv('assets/daily_multivariate_timeseries.csv',
                        parse_dates=['datetime'],
                        index_col='datetime')
n_vars = mvtseries.shape[1]

在这个食谱中,我们将使用来自pytorch_lightning的数据模块来处理数据预处理。数据模块是包含所有数据预处理步骤并与模型共享数据的类。以下是数据模块的基本结构:

import lightning.pytorch as pl
class ExampleDataModule(pl.LightningDataModule):
    def __init__(self,
                 data: pd.DataFrame,
                 batch_size: int):
        super().__init__()
        self.data = data
        self.batch_size = batch_size
    def setup(self, stage=None):
        pass
    def train_dataloader(self):
        pass
    def val_dataloader(self):
        pass
    def test_dataloader(self):
        pass
    def predict_dataloader(self):
        pass

所有数据模块都继承自LightningDataModule类。我们需要实现几个关键方法:

  • setup():此方法包含所有主要的数据预处理步骤

  • train_dataloader()val_dataloader()test_dataloader()predict_dataloader():这些是获取相应数据集(训练、验证、测试和预测)数据加载器的一组方法

除了DataModule类外,我们还将利用LightningModule类来封装所有模型过程。这些模块具有以下结构:

class ExampleModel(pl.LightningModule):
    def __init__(self):
        super().__init__()
        self.network = ...
    def forward(self, x):
        pass
    def training_step(self, batch, batch_idx):
        pass
    def validation_step(self, batch, batch_idx):
        pass
    def test_step(self, batch, batch_idx):
        pass
    def predict_step(self, batch, batch_idx, dataloader_idx=0):
        pass
    def configure_optimizers(self):
        pass

让我们更仔细地看看ExampleModel

  • 我们在类的属性中定义任何必要的神经网络元素(例如self.network

  • forward()方法定义了网络元素如何相互作用并建模时间序列

  • training_stepvalidation_steptesting_step分别描述了网络的训练、验证和测试过程

  • predict_step详细描述了获取最新观测值并进行预测的过程,模拟了部署场景

  • 最后,configure_optimizers()方法详细描述了网络的优化设置

让我们看看如何创建一个数据模块来预处理多变量时间序列,以及它如何与TimeSeriesDataSet结合。然后,我们将实现一个LightningModule结构来处理前馈神经网络的训练和测试过程。

如何进行…

以下代码展示了如何定义数据模块以处理预处理步骤。首先,让我们看一下类的构造函数:

from pytorch_forecasting import TimeSeriesDataSet
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
class MultivariateSeriesDataModule(pl.LightningDataModule):
    def __init__(
            self,
            data: pd.DataFrame,
            n_lags: int,
            horizon: int,
            test_size: float,
            batch_size: int
    ):
        super().__init__()
        self.data = data
        self.feature_names = 
            [col for col in data.columns if col != "Incoming Solar"]
        self.batch_size = batch_size
        self.test_size = test_size
        self.n_lags = n_lags
        self.horizon = horizon
        self.target_scaler = StandardScaler()
        self.training = None
        self.validation = None
        self.test = None
        self.predict_set = None

在构造函数中,我们定义了所有必要的数据准备元素,如滞后数、预测时间跨度和数据集。这包括初始化target_scaler属性,该属性用于标准化时间序列的值。

然后,我们创建setup()方法,其中包括数据预处理逻辑:

def setup(self, stage=None):
    self.preprocess_data()
    train_indices, val_indices, test_indices = self.split_data()
    train_df = self.data.loc
        [self.data["time_index"].isin(train_indices)]
    val_df = self.data.loc[self.data["time_index"].isin(val_indices)]
    test_df = self.data.loc
        [self.data["time_index"].isin(test_indices)]
     self.target_scaler.fit(train_df[["target"]])
    self.scale_target(train_df, train_df.index)
    self.scale_target(val_df, val_df.index)
    self.scale_target(test_df, test_df.index)
    train_df = train_df.drop("Incoming Solar", axis=1)
    val_df = val_df.drop("Incoming Solar", axis=1)
    test_df = test_df.drop("Incoming Solar", axis=1)
    self.training = TimeSeriesDataSet(
        train_df,
        time_idx="time_index",
        target="target",
        group_ids=["group_id"],
        max_encoder_length=self.n_lags,
        max_prediction_length=self.horizon,
        time_varying_unknown_reals=self.feature_names,
        scalers={name: StandardScaler() for name in 
            self.feature_names},
    )
    self.validation = TimeSeriesDataSet.from_dataset
        (self.training, val_df)
    self.test = TimeSeriesDataSet.from_dataset(self.training, test_df)
    self.predict_set = TimeSeriesDataSet.from_dataset(
    self.training, self.data, predict=True)

一些方法,如self.preprocess_data(),已被省略以简化内容。你可以在本书的 GitHub 仓库中找到它们的源代码。

最后,我们必须构建数据加载器,负责将数据传递给模型:

    def train_dataloader(self):
        return self.training.to_dataloader
            (batch_size=self.batch_size, shuffle=False)
    def val_dataloader(self):
        return self.validation.to_dataloader
            (batch_size=self.batch_size, shuffle=False)
    def test_dataloader(self):
        return self.test.to_dataloader
            (batch_size=self.batch_size, shuffle=False)
    def predict_dataloader(self):
        return self.predict_set.to_dataloader
            (batch_size=1, shuffle=False)

让我们仔细看看这个数据模块:

  • 数据预处理步骤在setup()方法中完成。这包括通过包括time_indexgroup_id变量来转换时间序列,以及训练、验证和测试拆分。数据集使用TimeSeriesDataSet类来构建。请注意,我们只需要为其中一个数据集定义一个TimeSeriesDataSet实例。我们可以使用from_dataset()方法为另一个数据集设置一个现有的TimeSeriesDataSet实例。

  • 预处理步骤的信息可以通过DataModule类的构造函数传递,例如滞后数(n_lags)或预测的horizon

  • 数据加载器可以通过在相应的数据集上使用to_dataloader()方法获得。

然后,我们可以设计神经网络架构。我们将创建一个名为FeedForwardNet的类,来实现一个包含三层的前馈神经网络:

import torch
from torch import nn
class FeedForwardNet(nn.Module):
    def __init__(self, input_size, output_size):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_size, 16),
            nn.ReLU(),
            nn.Linear(16, 8),
            nn.ReLU(),
            nn.Linear(8, output_size),
        )
    def forward(self, X):
        X = X.view(X.size(0), -1)
        return self.net(X)

网络架构在self.net属性中定义。网络的各层通过nn.Sequential容器堆叠在一起:

  • 第一层接收大小为input_size的输入数据。这是一个线性变换(nn.Linear),包含16个单元,并使用ReLU()激活函数(nn.ReLU)。

  • 结果会传递到第二层,这一层也是线性变换类型和激活函数。该层包含8个单元。

  • 最后一层也是对来自前一层的输入进行线性变换。其大小与output_size相同,在时间序列的情况下,指的是预测的时间跨度。

然后,我们将此神经网络插入到LightningModule模型类中。首先,让我们看一下类的构造函数和forward()方法:

from pytorch_forecasting.models import BaseModel
class FeedForwardModel(BaseModel):
    def __init__(self, input_dim: int, output_dim: int):
        self.save_hyperparameters()
        super().__init__()
        self.network = FeedForwardNet(
            input_size=input_dim,
            output_size=output_dim,
        )
        self.train_loss_history = []
        self.val_loss_history = []
        self.train_loss_sum = 0.0
        self.val_loss_sum = 0.0
        self.train_batch_count = 0
        self.val_batch_count = 0
    def forward(self, x):
        network_input = x["encoder_cont"].squeeze(-1)
        prediction = self.network(network_input)
        output = self.to_network_output(prediction=prediction)
        return output

构造函数存储网络元素,而forward()方法详细说明了这些元素在网络前向传播中的交互方式。forward()方法还使用to_network_output()方法将输出转化为原始数据尺度。训练步骤和网络优化器定义如下:

def training_step(self, batch, batch_idx):
    x, y = batch
    y_pred = self(x).prediction
    y_pred = y_pred.squeeze(1)
    y_actual = y[0].squeeze(1)
    loss = F.mse_loss(y_pred, y_actual)
    self.train_loss_sum += loss.item()
    self.train_batch_count += 1
    self.log("train_loss", loss)
    return loss
def configure_optimizers(self):
    return torch.optim.Adam(self.parameters(), lr=0.01)

configure_optimizers()方法是我们设置优化过程的地方。在训练步骤中,我们获取一批样本,将输入传递给神经网络,然后使用实际数据计算均方误差。然后,我们将误差信息存储在不同的属性中。

验证和测试步骤与训练阶段的工作方式类似:

def validation_step(self, batch, batch_idx):
    x, y = batch
    y_pred = self(x).prediction
    y_pred = y_pred.squeeze(1)
    y_actual = y[0].squeeze(1)
    loss = F.mse_loss(y_pred, y_actual)
    self.val_loss_sum += loss.item()
    self.val_batch_count += 1
    self.log("val_loss", loss)
    return loss
def test_step(self, batch, batch_idx):
    x, y = batch
    y_pred = self(x).prediction
    y_pred = y_pred.squeeze(1)
    y_actual = y[0].squeeze(1)
    loss = F.mse_loss(y_pred, y_actual)
    self.log("test_loss", loss)

在预测步骤中,我们只需将输入数据传递给神经网络,然后获取其输出:

def predict_step(self, batch, batch_idx):
    x, y = batch
    y_pred = self(x).prediction
    y_pred = y_pred.squeeze(1)
    return y_pred

让我们看一下前面的FeedForwardModel模块:

  • 基于PyTorch的神经网络在self.network属性中定义

  • forward()方法描述了神经网络如何处理从数据加载器获取的实例

  • 优化器设置为Adam,学习率为0.01

  • 最后,我们使用Trainer类来训练模型:

datamodule = MultivariateSeriesDataModule(data=mvtseries,
                                          n_lags=7,
                                          horizon=1,
                                          batch_size=32,
                                          test_size=0.3)
model = FeedForwardModel(input_dim=N_LAGS * n_vars, output_dim=1)
trainer = pl.Trainer(max_epochs=30)
trainer.fit(model, datamodule)

训练过程运行30个周期。为了测试模型,我们可以使用Trainer实例中的test()方法:

trainer.test(model=model, datamodule=datamodule)
forecasts = trainer.predict(model=model, datamodule=datamodule)

未来的观察结果通过predict()方法进行预测。在这两种情况下,我们将模型和数据模块都传递给Trainer实例。

它是如何工作的……

数据模块封装了所有准备步骤。任何需要在数据集上执行的特定转换都可以包含在setup()方法中。与模型相关的逻辑由LightningModule实例处理。使用DataModuleLightningModule方法提供了一种模块化、更整洁的深度学习模型开发方式。

TimeSeriesDataSet类中的scalers参数用于传递应该用于预处理时间序列的解释变量的缩放器。在这种情况下,我们使用了以下内容:

scalers={name: StandardScaler() for name in self.feature_names}

在这里,我们使用StandardScaler将所有解释变量转换为一个共同的数值范围。我们通过self.target_scaler属性标准化了时间序列的目标变量,其中包括一个StandardScaler操作符。我们在TimeSeriesDataSet之外对目标变量进行了归一化,以便对目标变量拥有更多的控制权。这可以作为一个示例,展示如何进行那些在软件包中可能不可用的转换。

还有更多内容……

我们使用nn.Sequential容器定义了前馈神经网络。另一种可能的方法是将每个元素定义为自己的类属性,并在forward方法中显式调用它们:

class FeedForwardNetAlternative(nn.Module):
    def __init__(self, input_size, output_size):
        super().__init__()
        self.l1 = nn.Linear(input_size, 16)
        self.relu_l1 = nn.ReLU()
        self.l2 = nn.Linear(16, 8)
        self.relu_l2 = nn.ReLU()
        self.l3 = nn.Linear(8, output_size)
    def forward(self, x):
        X = X.view(X.size(0), -1)
        l1_output = self.l1(x)
        l1_actf_output = self.relu_l1(l1_output)
        l2_output = self.l2(l1_actf_output)
        l2_actf_output = self.relu_l2(l2_output)
        l3_output = self.l3(l2_actf_output)
        return l3_output

两种方法是等效的。虽然第一种方法更加整洁,但第二种方法更具灵活性。

用于多变量时间序列预测的 LSTM 神经网络

在这个示例中,我们将继续构建一个模型,利用多变量时间序列预测太阳辐射的下一个值。这一次,我们将训练一个 LSTM 递归神经网络来解决这个任务。

准备就绪

数据设置与我们在前面的食谱中所做的类似。所以,我们将使用之前定义的数据模块。现在,让我们学习如何使用LightningModule类构建 LSTM 神经网络。

如何操作……

使用 PyTorch Lightning 训练 LSTM 神经网络的工作流程是相似的,但有一个小而重要的细节。对于 LSTM 模型,我们将输入数据保持在一个三维结构中,形状为(样本数、滞后数、特征数)。以下是模块的代码,从构造函数和forward()方法开始:

class MultivariateLSTM(pl.LightningModule):
    def __init__(self, input_dim, hidden_dim, num_layers, output_dim):
        super().__init__()
        self.hidden_dim = hidden_dim
        self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, 
            batch_first=True)
        self.fc = nn.Linear(hidden_dim, output_dim)
    def forward(self, x):
        h0 = torch.zeros(self.lstm.num_layers, x.size(0), 
            self.hidden_dim).to(self.device)
        c0 = torch.zeros(self.lstm.num_layers, x.size(0), 
            self.hidden_dim).to(self.device)
        out, _ = self.lstm(x, (h0, c0))
        out = self.fc(out[:, -1, :])
        return out

这一次,我们不需要将网络的输入压缩成二维向量,因为 LSTM 接受的是三维输入。LSTM 背后的逻辑在forward()方法中实现。其余的方法与我们在前面食谱中所做的完全相同。以下是training_step的示例:

    def training_step(self, batch, batch_idx):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        y_pred = y_pred.squeeze(1)
        loss = F.mse_loss(y_pred, y[0])
        self.log('train_loss', loss)
        return loss

你可以在本书的 GitHub 仓库中找到其余的方法。

定义完模型后,我们可以按如下方式使用它:

n_vars = mvtseries.shape[1] - 1
model = MultivariateLSTM(input_dim=n_vars,
                         hidden_dim=10,
                         num_layers=1,
                         output_dim=1)
trainer = pl.Trainer(max_epochs=10)
trainer.fit(model, datamodule)
trainer.test(model, datamodule.test_dataloader())
forecasts = trainer.predict(model=model, datamodule=datamodule)

如前面的代码所示,PyTorch Lightning 使得测试和预测过程在各个模型中保持一致。

它是如何工作的……

LSTM 是一种递归神经网络架构,旨在对时间序列等顺序数据进行建模。这类网络相较于前馈神经网络,包含一些额外的元素,如额外的输入维度或隐藏单元状态。在本节中,我们在 LSTM 层上堆叠了两个全连接层。LSTM 层通常会传递给全连接层,因为前者的输出是一个内部状态。因此,全连接层会在我们所需的特定维度上处理该输出。

LSTM 的类构造函数接收四个输入参数——时间序列中的变量数目(input_size)、预测的时间范围(output_size)、LSTM层的数量(num_layers)以及每个LSTM层中的隐藏单元数(hidden_size)。

我们在__init__构造函数方法中定义了三层。除了LSTM外,我们创建了两个全连接层,其中一个代表输出层。

网络的前向传播如下工作:

  1. 使用零初始化隐藏状态(h0)和单元状态(c0)。这是通过调用init_hidden_state()方法来完成的。

  2. 将输入数据传递给 LSTM 堆栈。LSTM 返回它的输出以及每个 LSTM 层的隐藏状态和单元状态。

  3. 接下来,我们获取最后一个 LSTM 层的隐藏状态,将其传递给ReLU()激活函数。

  4. ReLU的输出被传递到第一个全连接层,其输出再次通过ReLU函数进行转换。最后,输出被传递到一个线性全连接输出层,该层提供预测结果。

这一逻辑在LightningModule实例的forward()方法中进行了编写。

还有更多内容……

我们创建了一个具有单个 LSTM 层的深度神经网络(num_layers=1)。然而,我们可以根据需要增加该值。具有多个 LSTM 层的模型被称为堆叠 LSTM模型。

使用 Tensorboard 监控训练过程

训练深度学习模型通常需要调整多个超参数、评估不同的架构等。为了便于这些任务,必须使用可视化和监控工具。tensorboard是一个强大的工具,可以在训练过程中追踪和可视化各种指标。本节将指导你如何将tensorboard与 PyTorch Lightning 集成,用于监控训练过程。

准备工作

在使用tensorboard与 PyTorch Lightning 之前,你需要先安装tensorboard。你可以使用以下命令进行安装:

pip install -U tensorboard

安装完成后,确保你正在利用 PyTorch Lightning 内置的tensorboard日志记录功能。

如何实现……

以下是如何使用tensorboard来监控训练过程:

  1. 首先,确保tensorboard已导入到你的脚本中。

  2. 接下来,你需要创建一个tensorboard日志记录器,并将其传递给 PyTorch Lightning 的Trainer

    from lightning.pytorch.loggers import TensorBoardLogger
    import lightning.pytorch as pl
    logger = TensorBoardLogger('logs/')
    trainer = pl.Trainer(logger=logger)
    
  3. 然后,你可以通过在终端运行以下命令来启动tensorboard

    tensorboard --logdir=logs/
    
  4. 在你的网页浏览器中打开tensorboard,通过访问终端中显示的 URL;通常是http://localhost:6006。你将看到实时更新的各种指标,例如周期数、训练、验证和测试损失等。

以下图展示了上一章节中 LSTM 性能的一些图示。在此案例中,我们可以看到周期数以及训练和验证损失是如何变化的:

图 4.2:周期、训练损失和验证损失的比较

图 4.2:周期、训练损失和验证损失的比较

它是如何工作的……

tensorboard提供了各种训练指标、超参数调优、模型图形等的可视化。当与 PyTorch Lightning 集成时,以下内容会发生:

  • 在训练过程中,日志记录器将指定的指标发送到tensorboard

  • tensorboard读取日志并提供交互式可视化

  • 用户可以实时监控训练的各个方面

还有更多……

以下是一些需要注意的额外细节:

  • 你可以记录其他信息,例如图像、文本、直方图等

  • 通过探索不同的可视化内容,你可以深入了解模型的表现,并进行必要的调整

  • Tensorboard 与 PyTorch Lightning 的集成简化了监控过程,使得模型开发更加高效

使用tensorboard与 PyTorch Lightning 提供了一个强大的解决方案,用于监控和可视化训练过程,使得在模型开发中可以做出更明智的决策。

评估用于预测的深度神经网络

评估预测模型的表现对于理解它们如何对未见数据进行泛化至关重要。常用的评估指标包括均方根误差RMSE)、平均绝对百分比误差MAPE)、平均绝对缩放误差MASE)和对称平均绝对百分比误差SMAPE)等。我们将使用 Python 实现这些指标,并向您展示如何应用它们来评估模型的表现。

准备好了吗

我们需要来自训练模型的预测值和相应的真实值,以计算这些指标。因此,我们必须先在测试集上运行我们的模型,以获取预测结果。

为了简化实现,我们将使用scikit-learnsktime库,因为它们提供了有用的类和方法来帮助我们完成这个任务。由于我们还没有安装sktime,请运行以下命令:

pip install sktime

现在,是时候导入用于不同评估指标的类和方法了:

from sklearn.metrics import mean_squared_error
from sktime.performance_metrics.forecasting 
import mean_absolute_scaled_error, MeanAbsolutePercentageError
import numpy as np

如何实现…

为了评估我们的模型表现,我们必须计算scikit-learn库中的相关指标。对于sktime库,它提供了现成可用的函数来计算这些指标。

以下是计算这些指标的代码:

def mean_absolute_percentage_error(y_true, y_pred):
    y_true, y_pred = np.array(y_true), np.array(y_pred)
    return np.mean(np.abs((y_true - y_pred) / y_true)) * 100
y_pred = model(X_test).detach().numpy()
y_true = y_test.detach().numpy()
rmse_sklearn = np.sqrt(mean_squared_error(y_true, y_pred)) print(f"RMSE (scikit-learn): {rmse_sklearn}")
mape = mean_absolute_percentage_error(y_true, y_pred) 
print(f"MAPE: {mape}")
mase_sktime = mean_absolute_scaled_error(y_true, y_pred) 
print(f"MASE (sktime): {mase_sktime}")
smape_sktime = symmetric_mean_absolute_percentage_error
    (y_true, y_pred)
 print(f"SMAPE (sktime): {smape_sktime}")

它是如何工作的…

这些指标各自评估模型表现的不同方面:

  • RMSE:该指标计算预测值与实际值之间的平均平方差的平方根。它对较大误差给予更高的惩罚。

  • 1表示与朴素预测相等的表现,而 MASE 值小于1则表示模型表现优于朴素预测。

  • MAPE:该指标计算实际值与预测值之间绝对百分比差异的平均值。它以百分比的形式表达平均绝对误差,这在您想了解相对预测误差时非常有用。

  • SMAPE:该指标计算平均绝对百分比误差,处理低估和高估的误差时给予同等的权重。它将误差表示为实际值的百分比,这对于比较模型和预测不同规模的数据非常有用。

还有更多内容…

记住,评估指标的选择取决于具体问题和业务需求。例如,如果低估模型的成本高于高估模型的成本,那么一个区分这两种误差类型的指标可能更合适。根据问题的不同,其他指标,如 MAE,也可以使用。使用多种指标评估模型始终是个好主意,这样可以更全面地了解模型的表现。

使用回调——EarlyStopping

在 PyTorch Lightning 中,回调是可重用的组件,允许你在训练、验证和测试的各个阶段注入自定义行为。它们提供了一种将功能与主训练逻辑分离的方式,提供了一个模块化和可扩展的方法来管理辅助任务,例如记录指标、保存检查点、早停等。

通过定义一个自定义类继承自 PyTorch Lightning 的基础Callback类,你可以重写与训练过程中的不同阶段相对应的特定方法,例如on_epoch_starton_batch_end。当训练器初始化时,如果传入一个或多个这些回调对象,定义的行为将自动在训练过程中的相应阶段执行。这使得回调成为组织训练管道的强大工具,能够增加灵活性而不使主训练代码变得混乱。

准备工作

在定义并训练 LSTM 模型后,如上一节所述,我们可以通过引入早停技术进一步增强训练过程。该技术通过在指定的指标停止改进时暂停训练过程来避免过拟合。为此,PyTorch Lightning 提供了一个早停回调,我们将把它集成到现有的训练代码中。

如何操作…

要应用早停,我们需要通过添加EarlyStopping回调来修改现有的 PyTorch Lightning Trainer。下面是实现该功能的代码:

import lightning.pytorch as pl
from lightning.pytorch.callbacks import EarlyStopping
early_stop_callback = EarlyStopping(
    monitor="val_loss",
    min_delta=0.00,
    patience=3,
    verbose=False,
    mode="min"
)
trainer = pl.Trainer(max_epochs=100,
                     callbacks=[early_stop_callback]) 
trainer.fit(model, datamodule)

在这段代码中,monitor设置为验证损失(val_loss),如果该值在patience连续的验证周期中没有至少减少min_delta,训练过程将停止。

它是如何工作的…

早停是一种正则化技术,可以防止神经网络的过拟合。它监控一个指定的指标(在这里是验证损失),并在该指标停止改进时暂停训练过程。

这是在我们 LSTM 模型中的工作方式:

  • val_loss)在验证阶段。

  • 对于patience连续的训练周期,若min_delta未达到要求,训练过程将被暂停。

  • mode参数可以设置为minmax,表示被监控的指标应该最小化还是最大化。在我们的案例中,我们希望最小化验证损失。

通过提前停止训练过程,我们可以节省时间和资源,并且可能获得一个在未见数据上泛化更好的模型。

还有更多内容…

让我们看看一些进一步的细节:

  • 早停回调(early stopping callback)是高度可配置的,允许你根据特定需求调整其行为——例如,你可以更改patience参数,使得停止标准更加严格或宽松。

  • 早停可以与其他回调和技术结合使用,例如模型检查点(model checkpointing),以创建一个强大而高效的训练管道。

  • 适当地使用早停可以使模型在未见数据上表现更好,因为它能防止模型过拟合训练数据。

这个EarlyStopping回调与 PyTorch Lightning 以及我们现有的 LSTM 模型完美集成,展示了 PyTorch Lightning 回调系统的可扩展性和易用性。

第五章:全球预测模型

在本章中,我们将探讨各种时间序列预测场景,并学习如何使用深度学习处理这些场景。这些场景包括多步和多输出预测任务,以及涉及多个时间序列的问题。我们将涵盖这些案例,解释如何准备数据、训练适当的神经网络模型,并对其进行验证。

本章结束时,你应该能够为不同的时间序列数据集构建深度学习预测模型。这包括超参数优化,这是模型开发中的重要阶段。

本章将引导你完成以下配方:

  • 多变量时间序列的多步预测

  • 多变量时间序列的多步和多输出预测

  • 为全局模型准备多个时间序列

  • 使用多个时间序列训练全局 LSTM

  • 季节性时间序列的全球预测模型

  • 使用 Ray Tune 进行超参数优化

技术要求

本章需要以下 Python 库:

  • numpy(1.26.3)

  • pandas(2.0.3)

  • scikit-learn(1.4.0)

  • sktime(0.26.0)

  • torch(2.2.0)

  • pytorch-forecasting(1.0.0)

  • pytorch-lightning(2.1.4)

  • gluonts(0.14.2)

  • ray(2.9.2)

你可以使用 pip 一次性安装这些库:

pip install -U pandas numpy scikit-learn sktime torch pytorch-forecasting pytorch-lightning gluonts

本章中的配方将遵循基于 PyTorch Lightning 的设计理念,这种理念提供了一种模块化和灵活的方式来构建和部署 PyTorch 模型。有关本章代码,可以在以下 GitHub URL 中找到:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

多变量时间序列的多步预测

到目前为止,我们一直在处理单一变量时间序列的下一个值预测。预测下一个观测值的值被称为一步预测。在本配方中,我们将扩展上一章中开发的模型,以进行多步预测。

准备工作

多步预测是提前预测多个观测值的过程。这个任务对于减少时间序列的长期不确定性非常重要。

事实证明,我们之前所做的大部分工作也适用于多步预测的设置。TimeSeriesDataSet 类使得将一步预测问题扩展到多步预测变得非常简单。

在本配方中,我们将预测范围设置为 7,并将滞后期数设置为 14

N_LAGS = 7
HORIZON = 14

实际上,这意味着预测任务是基于过去 14 天的数据来预测未来 7 天的太阳辐射。

如何实现…

对于多步预测问题,需要改变两件事:

  • 其中一项是神经网络模型的输出维度。与表示下一个值的1不同,输出维度需要与预测步数相匹配。这可以通过模型中的 output_dim 变量来实现。

  • 数据模块的预测长度需要设置为预测时段。这可以通过TimeSeriesDataSet类中的max_prediction_length参数来完成。

这两个输入可以按如下方式传递给数据和模型模块:

datamodule = MultivariateSeriesDataModule(data=mvtseries,
    n_lags=N_LAGS,
    horizon=HORIZON,
    batch_size=32,
    test_size=0.3)
model = MultivariateLSTM(input_dim=n_vars,
    hidden_dim=32,
    num_layers=1,
    output_dim=HORIZON)

然后,模型的训练和测试保持不变:

early_stop_callback = EarlyStopping(monitor="val_loss",
    min_delta=1e-4,
    patience=10,
    verbose=False,
    mode="min")
trainer = Trainer(max_epochs=20, callbacks=[early_stop_callback])
trainer.fit(model, datamodule)
trainer.test(model=model, datamodule=datamodule)

我们训练了模型 20 个周期,然后在测试集上评估了它,测试集通过数据模块中定义的数据加载器进行获取。

它是如何工作的…

传统的监督学习模型通常从一维目标变量中学习。在预测问题中,这个变量可以是,例如,下一个时间段时间序列的值。然而,多步预测问题需要在每个时间点预测多个值。深度学习模型天生就是多输出算法。因此,它们可以使用一个模型处理多个目标变量。

其他针对多步预测的方法通常涉及创建多个模型或将相同的模型用于不同的预测时段。然而,多输出方法更为可取,因为它能够捕捉不同预测时段之间的依赖关系。这可能带来更好的预测性能,正如以下文章所记录的那样:Taieb, Souhaib Ben 等,基于 NN5 预测竞赛的多步时间序列预测策略回顾与比较。《专家系统与应用》39.8(2012):7067-7083

还有更多…

我们可以使用深度学习神经网络进行多步预测的其他方法有很多。以下是另外三种流行的方法:

  • 递归: 训练一个神经网络进行一步预测,并通过递归方式使用它进行多步预测

  • 直接: 为每个预测时段训练一个神经网络

  • DirRec: 为每个预测时段训练一个神经网络,并将前一个预测结果作为输入传递给下一个预测

使用多元时间序列进行多步和多输出预测

在这个案例中,我们将扩展 LSTM 模型,以预测多元时间序列的多个变量的多个时间步。

准备工作

到目前为止,在这一章节中,我们已经构建了多个模型来预测某一特定变量——太阳辐射的未来。我们利用时间序列中的额外变量来改善太阳辐射的建模。

然而,在处理多元时间序列时,我们通常关心的是预测多个变量,而不仅仅是一个。一个常见的例子是在处理时空数据时出现的。时空数据集是多元时间序列的一个特例,其中在不同位置观察到一个现实世界的过程。在这种数据集中,目标是预测所有这些位置的未来值。同样,我们可以利用神经网络是多输出算法的特点,在一个模型中处理多个目标变量。

在这个例子中,我们将继续使用太阳辐射数据集,和之前的例子一样。不过,我们的目标是预测三个变量的未来值——太阳辐射、蒸气压和气温:

N_LAGS = 14
HORIZON = 7
TARGET = ['Incoming Solar', 'Air Temp', 'Vapor Pressure']
mvtseries = pd.read_csv('assets/daily_multivariate_timeseries.csv',
    parse_dates=['datetime'],
    index_col='datetime')

关于数据准备,过程与我们之前所做的类似。不同之处在于,我们将目标变量(TARGET)设置为前面列出的变量,而不是仅仅设置为太阳辐射。TimeSeriesDataSet类和数据模块会处理所有的预处理和数据共享工作。

如何实现…

我们首先调整数据模块,以处理多个目标变量。下面的代码展示了我们所做的必要更改。让我们从定义模块的构造函数开始:

class MultivariateSeriesDataModule(pl.LightningDataModule):
    def __init__(
            self,
            data: pd.DataFrame,
            target_variables: List[str],
            n_lags: int,
            horizon: int,
            test_size: float = 0.2,
            batch_size: int = 16,
    ):
        super().__init__()
        self.data = data
        self.batch_size = batch_size
        self.test_size = test_size
        self.n_lags = n_lags
        self.horizon = horizon
        self.target_variables = target_variables
        self.target_scaler = {k: MinMaxScaler() 
            for k in target_variables}
        self.feature_names = [col for col in data.columns
            if col not in self.target_variables]
        self.training = None
        self.validation = None
        self.test = None
        self.predict_set = None
        self.setup()

构造函数包含了一个新的参数target_variables,我们用它来传递目标变量的列表。除此之外,我们还对self.target_scaler属性做了小改动,现在它是一个字典对象,包含了每个目标变量的缩放器。接着,我们构建了如下的setup()方法:

def setup(self, stage=None):
    self.preprocess_data()
    train_indices, val_indices, test_indices = self.split_data()
    train_df = self.data.loc
        [self.data["time_index"].isin(train_indices)]
    val_df = self.data.loc[self.data["time_index"].isin(val_indices)]
    test_df = self.data.loc
        [self.data["time_index"].isin(test_indices)]
    for c in self.target_variables:
        self.target_scaler[c].fit(train_df[[c]])
    self.scale_target(train_df, train_df.index)
    self.scale_target(val_df, val_df.index)
    self.scale_target(test_df, test_df.index)
    self.training = TimeSeriesDataSet(
        train_df,
        time_idx="time_index",
        target=self.target_variables,
        group_ids=["group_id"],
        max_encoder_length=self.n_lags,
        max_prediction_length=self.horizon,
        time_varying_unknown_reals=self.feature_names + 
            self.target_variables,
        scalers={name: MinMaxScaler() for name in self.feature_names},
    )
    self.validation = TimeSeriesDataSet.from_dataset
        (self.training, val_df)
    self.test = TimeSeriesDataSet.from_dataset(self.training, test_df)
    self.predict_set = TimeSeriesDataSet.from_dataset(
        self.training, self.data, predict=True
    )

与之前的例子相比,主要的不同点如下。我们将目标变量的列表传递给TimeSeriesDataSet类的目标输入。目标变量的缩放过程也变更为一个for循环,遍历每个目标变量。

我们还更新了模型模块,以处理多个目标变量。让我们从构造函数和forward()方法开始:

class MultiOutputLSTM(LightningModule):
    def __init__(self, input_dim, hidden_dim, num_layers, 
        horizon, n_output):
        super().__init__()
        self.n_output = n_output
        self.horizon = horizon
        self.hidden_dim = hidden_dim
        self.input_dim = input_dim
        self.output_dim = int(self.n_output * self.horizon)
        self.lstm = nn.LSTM(input_dim, hidden_dim,
            num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_dim, self.output_dim)
    def forward(self, x):
        h0 = torch.zeros(self.lstm.num_layers, x.size(0),
            self.hidden_dim).to(self.device)
        c0 = torch.zeros(self.lstm.num_layers, x.size(0),
            self.hidden_dim).to(self.device)
        out, _ = self.lstm(x, (h0, c0))
        out = self.fc(out[:, -1, :])
        return out

forward()方法与上一章相同。我们在构造函数中存储了一些额外的元素,比如预测时间跨度(self.horizon),因为它们在后续步骤中是必要的:

    def training_step(self, batch, batch_idx):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        y_pred = y_pred.unsqueeze(-1).view(-1, self.horizon, 
            self.n_output)
        y_pred = [y_pred[:, :, i] for i in range(self.n_output)]
        loss = [F.mse_loss(y_pred[i], 
            y[0][i]) for i in range(self.n_output)]
        loss = torch.mean(torch.stack(loss))
        self.log('train_loss', loss)
        return loss
    def test_step(self, batch, batch_idx):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        y_pred = y_pred.unsqueeze(-1).view(-1, self.horizon, 
            self.n_output)
        y_pred = [y_pred[:, :, i] for i in range(self.n_output)]
        loss = [F.mse_loss(y_pred[i],
            y[0][i]) for i in range(self.n_output)]
        loss = torch.mean(torch.stack(loss))
        self.log('test_loss', loss)
    def predict_step(self, batch, batch_idx, dataloader_idx=0):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        y_pred = y_pred.unsqueeze(-1).view(-1,
            self.horizon, self.n_output)
        y_pred = [y_pred[:, :, i] for i in range(self.n_output)]
        return y_pred
    def configure_optimizers(self):
        return torch.optim.Adam(self.parameters(), lr=0.001)

让我们来分析一下前面的代码:

  • 我们向构造函数添加了一个n_output参数,详细说明了目标变量的数量(在本例中是3

  • 输出维度设置为目标变量数乘以预测时间跨度(self.n_output * self.horizon

  • 在训练和测试步骤中处理数据时,预测结果会被重新调整为合适的格式(批大小、时间跨度和变量数)

  • 我们为每个目标变量计算 MSE 损失,然后使用torch.mean(torch.stack(loss))对它们取平均。

然后,剩余的过程与我们在之前基于 PyTorch Lightning 的例子中所做的类似:

model = MultiOutputLSTM(input_dim=n_vars,
    hidden_dim=32,
    num_layers=1,
    horizon=HORIZON,
    n_vars=len(TARGET))
datamodule = MultivariateSeriesDataModule(data=mvtseries,
    n_lags=N_LAGS,
    horizon=HORIZON,
    target_variables=TARGET)
early_stop_callback = EarlyStopping(monitor="val_loss",
    min_delta=1e-4,
    patience=10,
    verbose=False,
    mode="min")
trainer = pl.Trainer(max_epochs=20, callbacks=[early_stop_callback])
trainer.fit(model, datamodule)
trainer.test(model=model, datamodule=datamodule)
forecasts = trainer.predict(model=model, datamodule=datamodule)

工作原理…

本例中使用的建模方法遵循了向量自回归VAR)的思想。VAR 通过将多元时间序列中各变量的未来值建模为这些变量过去值的函数来工作。预测多个变量在多个场景下可能很有意义,例如时空预测。

在本节中,我们将 VAR 原理应用于深度学习环境,特别是通过使用 LSTM 网络。与传统的 VAR 模型根据过去的观察线性预测未来值不同,我们的深度学习模型能够捕捉多时间步和多个变量之间的非线性关系和时间依赖性。

为了计算我们模型的loss函数——这对于训练和评估模型性能至关重要——我们需要对training_step()test_step()方法做一些修改。在网络生成预测后,我们按变量对输出进行分段。这种分段允许我们分别计算每个变量的 MSE 损失。然后,这些单独的损失会被聚合,形成一个复合损失度量,指导模型的优化过程。

为全球模型准备多个时间序列

现在,是时候开始处理涉及多个时间序列的时间序列问题了。在本节中,我们将学习全球预测模型的基本原理及其工作方式。我们还将探索如何为预测准备包含多个时间序列的数据集。同样,我们利用TimeSeriesDataSetDataModule类的功能来帮助我们完成这项任务。

准备开始

到目前为止,我们一直在处理涉及单一数据集的时间序列问题。现在,我们将学习全球预测模型,包括以下内容:

  • 从本地模型到全球模型的过渡:最初,我们在时间序列预测中只处理单一数据集,其中模型根据一个系列的历史数据预测未来值。这些所谓的本地模型是针对特定时间序列量身定制的,而全球模型则涉及处理多个相关的时间序列,并捕捉它们之间的相关信息。

  • 利用神经网络:神经网络在数据丰富的环境中表现出色,使其成为全球预测的理想选择。这在零售等领域尤为有效,在这些领域中,了解不同产品销售之间的关系可以带来更准确的预测。

我们将学习如何使用一个关于运输的数据集构建全球预测模型,名为NN5。这个数据集曾在一个先前的预测竞赛中使用,包括 111 个不同的时间序列。

数据可以通过gluonts Python 库获得,并可以通过以下方式加载:

N_LAGS = 7
HORIZON = 7
from gluonts.dataset.repository.datasets import get_dataset
dataset = get_dataset('nn5_daily_without_missing', regenerate=False)

这里是数据集中五个时间序列的样本:

图 5.1:NN5 时间序列数据集样本

图 5.1:NN5 时间序列数据集样本

此数据集的原始来源可以在以下链接找到:zenodo.org/records/3889750

现在,让我们构建一个DataModule类来处理数据预处理步骤。

如何实现……

我们将构建一个LightningDataModule类,处理包含多个时间序列的数据集,并将其传递给模型。以下是构造函数的样子:

import lightning.pytorch as pl
class GlobalDataModule(pl.LightningDataModule):
    def __init__(self,
                 data,
                 n_lags: int,
                 horizon: int,
                 test_size: float,
                 batch_size: int):
        super().__init__()
        self.data = data
        self.batch_size = batch_size
        self.test_size = test_size
        self.n_lags = n_lags
        self.horizon = horizon
        self.training = None
        self.validation = None
        self.test = None
        self.predict_set = None
        self.target_scaler = LocalScaler()

本质上,我们存储了训练和使用模型所需的元素。这包括基于LocalScaler类的self.target_scaler属性。

LocalScaler类的主要方法是transform()

def transform(self, df: pd.DataFrame):
    df = df.copy()
    df["value"] = LogTransformation.transform(df["value"])
    df_g = df.groupby("group_id")
    scaled_df_l = []
    for g, df_ in df_g:
        df_[["value"]] = self.scalers[g].transform(df_[["value"]])
        scaled_df_l.append(df_)
    scaled_df = pd.concat(scaled_df_l)
    scaled_df = scaled_df.sort_index()
    return scaled_df

此方法对数据集应用了两种预处理操作:

  • 对时间序列进行对数转换以稳定方差

  • 对数据集中每个时间序列进行标准化

你可以扩展此类,以包括你需要对数据集执行的任何转换。LocalScaler类的完整实现可以在 GitHub 仓库中找到。

接着,我们在setup()函数中对数据进行了预处理:

def setup(self, stage=None):
    data_list = list(self.data.train)
    data_list = [pd.Series(ts['target'],
        index=pd.date_range(start=ts['start'].to_timestamp(),
        freq=ts['start'].freq,
        periods=len(ts['target'])))
        for ts in data_list]
    tseries_df = pd.concat(data_list, axis=1)
    tseries_df['time_index'] = np.arange(tseries_df.shape[0])
    ts_df = tseries_df.melt('time_index')
    ts_df = ts_df.rename(columns={'variable': 'group_id'})
    unique_times = ts_df['time_index'].sort_values().unique()
    tr_ind, ts_ind = \
        train_test_split(unique_times,
            test_size=self.test_size,
            shuffle=False)
    tr_ind, vl_ind = \
        train_test_split(tr_ind,
            test_size=0.1,
            shuffle=False)
    training_df = ts_df.loc[ts_df['time_index'].isin(tr_ind), :]
    validation_df = ts_df.loc[ts_df['time_index'].isin(vl_ind), :]
    test_df = ts_df.loc[ts_df['time_index'].isin(ts_ind), :]
    self.target_scaler.fit(training_df)
    training_df = self.target_scaler.transform(training_df)
    validation_df = self.target_scaler.transform(validation_df)
    test_df = self.target_scaler.transform(test_df)
    self.training = TimeSeriesDataSet(
        data=training_df,
        time_idx='time_index',
        target='value',
        group_ids=['group_id'],
        max_encoder_length=self.n_lags,
        max_prediction_length=self.horizon,
        time_varying_unknown_reals=['value'],
    )
    self.validation = TimeSeriesDataSet.from_dataset
        (self.training, validation_df)
    self.test = TimeSeriesDataSet.from_dataset(self.training, test_df)
    self.predict_set = TimeSeriesDataSet.from_dataset
        (self.training, ts_df, predict=True)

在前面的代码中,我们将数据分成了训练集、验证集、测试集和预测集,并设置了相应的TimeSeriesDataSet实例。最后,数据加载器与我们在之前的实例中做的类似:

    def train_dataloader(self):
        return self.training.to_dataloader(batch_size=self.batch_size,
            shuffle=False)
    def val_dataloader(self):
        return self.validation.to_dataloader
            (batch_size=self.batch_size, shuffle=False)
    def test_dataloader(self):
        return self.test.to_dataloader(batch_size=self.batch_size,
            shuffle=False)
    def predict_dataloader(self):
        return self.predict_set.to_dataloader(batch_size=1,
            shuffle=False)

我们可以像下面这样调用数据模块:

datamodule = GlobalDataModule(data=dataset,
    n_lags=N_LAGS,
    horizon=HORIZON,
    test_size=0.2,
    batch_size=1)

使用此模块,数据集中的每个独立时间序列都将以使用最后N_LAGS个值来预测下一个HORIZON个观察值的方式进行处理。

它是如何工作的……

全局方法在多个时间序列上进行训练。其思路是不同时间序列之间存在共同模式。因此,神经网络可以利用这些序列的观察值来训练更好的模型。

在前一节中,我们通过get_dataset()函数从gluonts Python 库中检索了一个包含多个时间序列的数据集。准备一个包含多个时间序列的监督学习数据集的过程与我们之前做的类似。TimeSeriesDataSet实例的关键输入是group_id变量,它详细说明了每个观察值所属的实体。

主要的工作发生在setup()方法中。首先,我们将数据集转换为具有长格式的pandas DataFrame。以下是该数据的示例:

图 5.2:NN5 时间序列数据集的长格式示例

图 5.2:NN5 时间序列数据集的长格式示例

在这种情况下,group_id列不是常量,它详细说明了观察值所对应的时间序列。由于每个时间序列是单变量的,因此有一个名为value的数值变量。

使用多个时间序列训练全局 LSTM

在前一个实例中,我们学习了如何为全局预测模型准备多个时间序列的监督学习数据集。在本实例中,我们将继续这一主题,描述如何训练一个全局 LSTM 神经网络进行预测。

准备工作

我们将继续使用在前一个实例中使用的数据模块:

N_LAGS = 7
HORIZON = 7
from gluonts.dataset.repository.datasets import get_dataset, dataset_names
dataset = get_dataset('nn5_daily_without_missing', regenerate=False)
datamodule = GlobalDataModule(data=dataset,
    n_lags=N_LAGS,
    horizon=HORIZON,
    batch_size=32,
    test_size=0.3)

让我们看看如何创建一个 LSTM 模块来处理包含多个时间序列的数据模块。

如何做到……

我们创建了一个包含 LSTM 实现的LightningModule类。首先,让我们看一下类的构造函数和forward()方法:

class GlobalLSTM(pl.LightningModule):
    def __init__(self, input_dim, hidden_dim, num_layers, output_dim):
        super().__init__()
        self.hidden_dim = hidden_dim
        self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, 
            batch_first=True)
        self.fc = nn.Linear(hidden_dim, output_dim)
    def forward(self, x):
        h0 = torch.zeros(self.lstm.num_layers, x.size(0), 
            self.hidden_dim).to(self.device)
        c0 = torch.zeros(self.lstm.num_layers, x.size(0), 
            self.hidden_dim).to(self.device)
        out, _ = self.lstm(x, (h0, c0))
        out = self.fc(out[:, -1, :])
        return out

神经网络的逻辑与我们之前处理单一时间序列数据集时所做的相似。对于剩下的方法也是如此:

    def training_step(self, batch, batch_idx):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        loss = F.mse_loss(y_pred, y[0])
        self.log('train_loss', loss)
        return loss
    def validation_step(self, batch, batch_idx):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        loss = F.mse_loss(y_pred, y[0])
        self.log('val_loss', loss)
        return loss
    def test_step(self, batch, batch_idx):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        loss = F.mse_loss(y_pred, y[0])
        self.log('test_loss', loss)
    def predict_step(self, batch, batch_idx, dataloader_idx=0):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        return y_pred
    def configure_optimizers(self):
        return torch.optim.Adam(self.parameters(), lr=0.01)

接下来,我们可以调用模型并进行训练,如下所示:

model = GlobalLSTM(input_dim=1,
    hidden_dim=32,
    num_layers=1,
    output_dim=HORIZON)
early_stop_callback = EarlyStopping(monitor="val_loss",
    min_delta=1e-4,
    patience=10,
    verbose=False,
    mode="min")
trainer = pl.Trainer(max_epochs=20, callbacks=[early_stop_callback])
trainer.fit(model, datamodule)
trainer.test(model=model, datamodule=datamodule)
forecasts = trainer.predict(model=model, datamodule=datamodule)

使用 PyTorch Lightning 设计,训练、测试和预测步骤与我们在其他基于该框架的食谱中所做的类似。

它是如何工作的…

如您所见,包含 LSTM 的LightningModule类与我们为单一多变量时间序列构建的完全相同。这个类仅处理模型定义的部分,因此无需更改。主要工作是在数据预处理阶段完成的。因此,我们只需要修改数据模块中的setup()方法,以反映之前食谱中所解释的必要更改。

我们从一个本地 LSTM 模型过渡到了一个全球 LSTM 模型,后者能够同时处理多个时间序列。主要的区别在于数据的准备和呈现方式,而不是神经网络架构本身的变化。无论是本地模型还是全球模型,都使用相同的 LSTM 结构,其特点是能够处理数据序列并预测未来值。

在本地 LSTM 设置中,模型的输入通常遵循结构[batch_sizesequence_lengthnum_features],输出的形状与预测的时间跨度匹配,通常为[batch_sizehorizon]。此设置非常直观,因为它处理的是来自单一序列的数据。

转向全球 LSTM 模型后,输入和输出配置在维度上保持基本一致。然而,现在输入聚合了多个时间序列的信息。它增强了神经网络学习新模式和依赖关系的能力,不仅限于单一序列,还跨越多个序列。因此,全球 LSTM 模型的输出旨在同时为多个时间序列生成预测,反映整个数据集的预测结果。

全球预测模型用于季节性时间序列

本食谱展示了如何扩展数据模块,将额外的解释变量包含在TimeSeriesDataSet类和DataModule类中。我们将使用一个关于季节性时间序列的特定案例。

准备工作

我们加载了在上一个食谱中使用的那个数据集:

N_LAGS = 7
HORIZON = 7
from gluonts.dataset.repository.datasets import get_dataset
dataset = get_dataset('nn5_daily_without_missing', regenerate=False)

该数据集包含了日粒度的时间序列。在这里,我们将使用Fourier级数建模周季节性。与我们在上一章中所做的不同(在处理季节性:季节虚拟变量和 Fourier 级数一节中),我们将学习如何使用TimeSeriesDataSet框架来包含这些特征。

如何操作…

这是更新后的DataModule,包含了Fourier级数。为了简洁起见,我们只描述了setup()方法的一部分,其余方法保持不变,您可以在 GitHub 仓库中查看:

from sktime.transformations.series.fourier import FourierFeatures
def setup(self, stage=None):
    […]
    fourier = FourierFeatures(sp_list=[7],
        fourier_terms_list=[2],
        keep_original_columns=False)
    fourier_features = fourier.fit_transform(ts_df['index'])
    ts_df = pd.concat
        ([ts_df, fourier_features], axis=1).drop('index', axis=1)
    […]
    self.training = TimeSeriesDataSet(
        data=training_df,
        time_idx='time_index',
        target='value',
        group_ids=['group_id'],
        max_encoder_length=self.n_lags,
        max_prediction_length=self.horizon,
        time_varying_unknown_reals=['value'],
        time_varying_known_reals=['sin_7_1', 'cos_7_1',
            'sin_7_2', 'cos_7_2']
    )

setup()方法中,我们使用数据集的日期和时间信息计算Fourier项。这样会生成四个确定性变量:sin_7_1cos_7_1sin_7_2cos_7_2。这些是我们用来建模季节性的Fourier级数。将它们通过pd.concat([tseries_long, fourier_features], axis=1)加入数据集后,我们使用time_varying_known_reals参数来告知这些特征随时间变化,但变化是可预测的。

在 LSTM 中,我们需要将输入维度更新为5,以反映数据集中变量的数量(目标变量加上四个Fourier级数)。这一步骤如下所示:

model = GlobalLSTM(input_dim=5,
    hidden_dim=32,
    num_layers=1,
    output_dim=HORIZON)
datamodule = GlobalDataModuleSeas(data=dataset,
    n_lags=N_LAGS,
    horizon=HORIZON,
    batch_size=128,
    test_size=0.3)
early_stop_callback = EarlyStopping(monitor="val_loss",
    min_delta=1e-4,
    patience=10,
    verbose=False,
    mode="min")
trainer = pl.Trainer(max_epochs=20, callbacks=[early_stop_callback])
trainer.fit(model, datamodule)
trainer.test(model=model, datamodule=datamodule)
forecasts = trainer.predict(model=model, datamodule=datamodule)

再次强调,训练和推理阶段与之前的步骤类似,因为这里唯一的不同是在数据模块处理的数据预处理阶段。

它是如何工作的……

使用Fourier级数建模季节性涉及通过傅里叶变换从数据集中提取额外变量来丰富数据集。这种方法在DataModule实例的setup()方法中实现,将这些变量并入到TimeSeriesDataSet对象中。

Fourier级数分解使我们能够通过将复杂的周期性模式分解成更简单的正弦波来捕捉季节性。Fourier级数的每个组件对应于不同的频率,捕捉时间序列数据中的不同季节性周期。这对神经网络特别有益,原因有几点:

  • Fourier级数作为自动特征工程,创建了直接编码周期性行为的有信息特征。这可以显著提升模型识别和预测季节性模式的能力,哪怕在复杂或嘈杂的数据中也能发挥作用。由于Fourier特征是加入到输入数据中的,它们可以与任何神经网络算法或架构兼容。

  • Fourier级数可以同时建模这些多重季节性水平,提供一种更为细致的数据表示,这种表示是传统季节性分解方法难以实现的。

  • 改进泛化能力:通过提供季节性的明确数学表示,Fourier特征帮助神经网络更好地从观测数据推断到未见的未来时期。这减少了过拟合噪声和数据异常的风险,使模型的学习更加专注于潜在的周期性趋势。

还有更多……

你可以访问以下网址,学习如何将额外的类别变量(如节假日)包含到数据集中:pytorch-forecasting.readthedocs.io/en/stable/tutorials/stallion.html#Load-data

使用 Ray Tune 进行超参数优化

神经网络有一些超参数,这些超参数定义了其结构和学习过程。超参数包括学习率、隐藏层的数量和单元数等。不同的超参数值会影响学习过程和模型的准确性。不恰当的值可能导致欠拟合或过拟合,从而降低模型的性能。因此,优化超参数值以最大限度地发挥深度学习模型的作用非常重要。在本教程中,我们将探讨如何使用 Ray Tune 进行超参数优化,包括学习率、正则化参数、隐藏层的数量等。这些参数的优化对于我们模型的表现至关重要。往往,由于超参数选择不当,我们在拟合神经网络模型时会得到较差的结果,这可能导致欠拟合或过拟合未见数据。

准备工作

在我们开始进行超参数优化之前,如果尚未安装 Ray Tune,我们需要先进行安装。可以使用以下命令:

pip install -U 'ray[data,train,tune,serve]'

我们将使用相同的数据和 LSTM 模型进行优化:

class GlobalDataModule(pl.LightningDataModule):
    ...
class GlobalLSTM(pl.LightningModule):
    ...
from ray.train.lightning import RayTrainReportCallback
from ray import tune
from ray.tune.schedulers import ASHAScheduler
from ray.train import RunConfig, ScalingConfig, CheckpointConfig
from ray.train.torch import TorchTrainer

在上述代码中,我们还导入了所有本教程所需的库。

如何实现…

让我们讨论一下如何使用 Ray Tune 实现超参数优化:

  1. 定义搜索空间:首先,定义你想要探索的超参数空间。

  2. 配置 Ray Tune:初始化 Tune 实验,设置所需的设置,例如试验次数、资源等。

  3. 运行优化:通过传递训练函数和定义的搜索空间来执行实验。

  4. 分析结果:利用 Ray Tune 的工具分析结果,并确定最佳超参数。

让我们首先定义搜索空间:

search_space = {
    "hidden_dim": tune.choice([8, 16, 32]),
    "num_layers": tune.choice([1, 2]),
}

在这个示例中,我们只优化两个参数:LSTM 神经网络中隐藏单元的数量和层数。

然后,我们在一个函数中定义训练循环:

def train_tune(config_hyper):
    hidden_dim = config_hyper["hidden_dim"]
    num_layers = config_hyper["num_layers"]
    model = GlobalLSTM(input_dim=1,
        hidden_dim=hidden_dim,
        output_dim=HORIZON,
        num_layers=num_layers)
    data_module = GlobalDataModule(dataset,
        n_lags=N_LAGS,
        horizon=HORIZON,
        batch_size=128,
        test_size=0.3)
    trainer = Trainer(callbacks=[RayTrainReportCallback()])
    trainer.fit(model, data_module)

定义完训练函数后,我们将其传递给 TorchTrainer 类实例,并与运行配置一起使用:

scaling_config = ScalingConfig(
    num_workers=2, use_gpu=False, 
        resources_per_worker={"CPU": 1, "GPU": 0}
)
run_config = RunConfig(
    checkpoint_config=CheckpointConfig(
        num_to_keep=1,
        checkpoint_score_attribute="val_loss",
        checkpoint_score_order="min",
    ),
)
ray_trainer = TorchTrainer(
    train_tune,
    scaling_config=scaling_config,
    run_config=run_config,
)

ScalingConfig 实例中,我们配置了计算环境,指定了该过程是否应在 GPU 或 CPU 上运行、分配的工作节点数量以及每个工作节点的资源。同时,RunConfig 实例用于定义优化过程,包括在此过程中应监控的指标。

然后,我们创建一个 Tuner 实例,将这些信息结合在一起:

scheduler = ASHAScheduler(max_t=30, grace_period=1, reduction_factor=2)
tuner = tune.Tuner(
    ray_trainer,
    param_space={"train_loop_config": search_space},
    tune_config=tune.TuneConfig(
        metric="val_loss",
        mode="min",
        num_samples=10,
        scheduler=scheduler,
    ),
)

Tuner 实例需要一个调度器作为其输入之一。为此,我们使用 ASHAScheduler,它采用异步成功折半算法ASHA)高效地在不同配置之间分配资源。这种方法通过根据性能反复缩小搜索空间来帮助识别最有效的配置。最终,通过运行这个过程,我们可以确定最佳配置:

results = tuner.fit()
best_model_conf = \
    results.get_best_result(metric='val_loss', mode='min')

在前面的代码中,我们获得了最小化验证损失的配置。

在根据验证损失选择最佳超参数后,我们可以在测试集上评估模型。从检查点中获取模型权重,并加载调优过程中的最佳超参数。然后,使用这些参数加载模型并在测试数据上进行评估:

path = best_model_conf.get_best_checkpoint(metric='val_loss',
    mode='min').path
config = best_model_conf.config['train_loop_config']
best_model = \
    GlobalLSTM.load_from_checkpoint(checkpoint_path=f'{path}/
        checkpoint.ckpt',
        **config)
data_module = GlobalDataModule(dataset, n_lags=7, horizon=3)
trainer = Trainer(max_epochs=30)
trainer.test(best_model, datamodule=data_module)

在前面的代码中,我们加载了具有最佳配置的模型,并在DataModule类中定义的测试集上进行测试。

它是如何工作的……

我们的超参数优化过程包括定义搜索空间、配置和执行优化,以及分析结果。本节共享的代码片段提供了一个逐步指南,说明如何将 Ray Tune 集成到任何机器学习工作流中,从而帮助我们探索并找到最适合模型的超参数:

  • search_space字典定义了超参数搜索空间。

  • train_tune()函数封装了训练过程,包括模型配置、数据准备和拟合。

  • ScalingConfig类定义了优化过程的计算环境,例如是否在 GPU 或 CPU 上运行。

  • RunConfig类设置了优化的执行方式,例如在此过程中应跟踪的指标。

  • ASHAScheduler类是一个调度器,定义了如何从不同的可能配置中进行选择。

Ray Tune 通过使用诸如随机搜索、网格搜索或更先进的算法(如 ASHA)等多种算法,高效地探索超参数空间。它并行化试验以有效利用可用资源,从而加速搜索过程。

还有更多……

Ray Tune 提供了几个额外的功能和优势。它可以与其他库集成,使其与流行的机器学习框架(如 PyTorch、TensorFlow 和 Scikit-Learn)兼容。此外,它还提供了先进的搜索算法,如贝叶斯优化和基于群体的训练,使用户能够灵活地尝试不同的优化策略。最后,Ray Tune 支持可视化工具,允许用户利用 TensorBoard 或 Ray 提供的自定义工具有效地可视化和分析超参数搜索过程。

第六章:用于时间序列预测的高级深度学习架构

在前面的章节中,我们学习了如何使用不同类型的神经网络创建预测模型,但到目前为止,我们只处理了基本的架构,如前馈神经网络或 LSTM。本章将介绍如何使用最先进的方法,如 DeepAR 或 Temporal Fusion Transformers 来构建预测模型。这些方法由 Google 和 Amazon 等科技巨头开发,并已在不同的 Python 库中提供。这些先进的深度学习架构旨在解决各种类型的预测问题。

我们将涵盖以下几个食谱:

  • 使用 N-BEATS 进行可解释的预测

  • 使用 PyTorch Forecasting 优化学习率

  • 使用 GluonTS 入门

  • 使用 GluonTS 训练 DeepAR 模型

  • 使用 NeuralForecast 训练 Transformer

  • 使用 GluonTS 训练 Temporal Fusion Transformer

  • 使用 NeuralForecast 训练 Informer 模型

  • 使用 NeuralForecast 比较不同的 Transformer

到本章结束时,你将能够训练最先进的深度学习预测模型。

技术要求

本章需要以下 Python 库:

  • numpy(1.23.5)

  • pandas(1.5.3)

  • scikit-learn(1.2.1)

  • sktime(0.24.0)

  • torch(2.0.1)

  • pytorch-forecasting(1.0.0)

  • pytorch-lightning(2.1.0)

  • gluonts(0.13.5)

  • neuralforecast(1.6.0)

你可以通过 pip 一次性安装这些库:

pip install -U pandas numpy scikit-learn sktime torch pytorch-forecasting pytorch-lightning gluonts neuralforecast

本章的代码可以在以下 GitHub 地址找到:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

使用 N-BEATS 进行可解释的预测

本食谱介绍了 用于可解释时间序列预测的神经基扩展分析N-BEATS),这是一种用于预测问题的深度学习方法。我们将向你展示如何使用 PyTorch Forecasting 训练 N-BEATS 并解释其输出。

准备工作

N-BEATS 特别设计用于处理多个单变量时间序列的问题。因此,我们将使用前一章中介绍的数据集(例如,参见 为全局模型准备多个时间序列 食谱):

import numpy as np
import pandas as pd
from gluonts.dataset.repository.datasets import get_dataset
from pytorch_forecasting import TimeSeriesDataSet
import lightning.pytorch as pl
from sklearn.model_selection import train_test_split
dataset = get_dataset('nn5_daily_without_missing', regenerate=False)
N_LAGS = 7
HORIZON = 7
datamodule = GlobalDataModule(data=dataset,
    n_lags=N_LAGS,
    horizon=HORIZON)

我们的目标是根据过去七个滞后值(N_LAGS),预测时间序列的下七个值(HORIZON)。

如何操作…

让我们创建训练、验证和测试数据集:

  1. 我们通过调用 GlobalDataModule 类中的 setup() 方法开始:

    datamodule.setup()
    
  2. N-BEATS 已经可以在 PyTorch Forecasting 中直接使用。你可以按如下方式定义模型:

    from pytorch_forecasting import NBeats
    model = NBeats.from_dataset(
        dataset=datamodule.training,
        stack_types=['trend', 'seasonality'],
        num_blocks=[3, 3],
        num_block_layers=[4, 4],
        widths=[256, 2048],
        sharing=[True],
        backcast_loss_ratio=1.0,
    )
    

    我们使用前面的代码通过 from_dataset() 方法创建了一个 NBeats 实例。以下参数需要定义:

    • dataset:包含训练集的 TimeSeriesDataSet 实例。

    • stack_types:你希望运行 N-BEATS 的模式。trendseasonality 类型的堆栈使得模型具有可解释性,而 ['generic'] 设置通常更为准确。

    • num_blocks:块是 N-BEATS 模型的基石。它包含一组完全连接层,用于建模时间序列。

    • num_block_layers:每个块中完全连接层的数量。

    • widths:每个块中完全连接层的宽度。

    • sharing:一个布尔参数,表示每个堆栈块是否共享权重。在可解释模式下,该参数应设置为 True

    • backcast_loss_ratio:模型中反向预测损失的相关性。反向预测(预测输入样本)是 N-BEATS 训练中的一个重要机制。这个参数平衡了反向预测损失与预测损失。

  3. 创建模型后,您可以将其传递给 PyTorch Lightning 的 Trainer 进行训练:

    import lightning.pytorch as pl
    from lightning.pytorch.callbacks import EarlyStopping
    early_stop_callback = EarlyStopping(monitor="val_loss",
        min_delta=1e-4,
        patience=10,
        verbose=False,
        mode="min")
    trainer = pl.Trainer(
        max_epochs=30,
        accelerator="auto",
        enable_model_summary=True,
        gradient_clip_val=0.01,
        callbacks=[early_stop_callback],
    )
    
  4. 我们还包括了一个早停回调,用于指导训练过程。模型使用 fit() 方法进行训练:

    trainer.fit(
        model,
        train_dataloaders=datamodule.train_dataloader(),
        val_dataloaders=datamodule.val_dataloader(),
    )
    

    我们将训练数据加载器传递给模型进行训练,并使用验证数据加载器进行早停。

  5. 拟合模型后,我们可以评估其测试性能,并用它进行预测。在此之前,我们需要从保存的检查点加载模型:

    best_model_path = trainer.checkpoint_callback.best_model_path
    best_model = NBeats.load_from_checkpoint(best_model_path)
    
  6. 您可以通过以下方式从测试集获取预测值及其真实值:

    predictions = best_model.predict(datamodule.test.to_dataloader(batch_size=1, shuffle=False))
    actuals = torch.cat(
        [y[0] for x, y in iter(
            datamodule.test.to_dataloader(batch_size=1, 
                shuffle=False))])
    
  7. 我们通过计算这两个数量之间的平均绝对差来评估预测性能(即,平均绝对误差):

    (actuals - predictions).abs().mean()
    

    根据您的设备,您可能需要使用 predictions.cpu()predictions 对象转换为 PyTorch 的 tensor 对象,然后再计算前面代码中指定的差异。

  8. 数据模块还简化了为新实例进行预测的工作流:

    forecasts = best_model.predict(datamodule.predict_dataloader())
    

    本质上,数据模块获取最新的观测值并将其传递给模型,后者会生成预测。

    N-BEATS 最有趣的方面之一是其可解释性组件。这些组件对于检查预测及其背后的驱动因素非常有价值:

  9. 我们可以将预测拆解成不同的组件,并使用 plot_interpretation() 方法将它们绘制出来。为此,我们需要事先获取原始预测,如下所示:

    raw_predictions = best_model.predict
        (datamodule.val_dataloader(),
        mode="raw",
        return_x=True)
    best_model.plot_interpretation(x=raw_predictions[1],
        output=raw_predictions[0],
        idx=0)
    

在前面的代码中,我们为测试集的第一个实例调用了绘图(idx=0)。以下是该绘图的样子:

图 6.1:将 N-BEATS 预测拆解成不同部分

图 6.1:将 N-BEATS 预测拆解成不同部分

上图展示了预测中的 trendseasonality 组件。

它是如何工作的…

N-BEATS 基于两个主要组件:

  • 一个包含预测和反向预测的残差连接双堆栈。在 N-BEATS 的上下文中,反向预测是指重建时间序列的过去值。它通过强迫模型在两个方向上理解时间序列结构,帮助模型学习更好的数据表示。

  • 一个深层的密集连接层堆栈。

这种组合使得模型既具有高预测准确性,又具备可解释性能力。

训练、评估和使用模型的工作流程遵循 PyTorch Lightning 提供的框架。数据准备逻辑是在数据模块组件中开发的,特别是在 setup() 函数中。建模阶段分为两个部分:

  1. 首先,你需要定义 N-BEATS 模型架构。在这个示例中,我们使用 from_dataset() 方法根据输入数据直接创建 NBeats 实例。

  2. 然后,训练过程逻辑在 Trainer 实例中定义,包括你可能需要的任何回调函数。

一些回调函数,比如早停,会将模型的最佳版本保存在本地文件中,你可以在训练后加载该文件。

需要注意的是,解释步骤是通过 plot_interpretation 部分进行的,这是 N-BEATS 的一个特殊功能,帮助从业人员理解预测模型所做的预测。这也有助于了解模型在实际应用中不适用的条件。

N-BEATS 是一个在预测工具库中非常重要的模型。例如,在 M5 预测竞赛中,该竞赛包含了一组需求时间序列,N-BEATS 模型被应用于许多最佳解决方案。你可以在这里查看更多细节:www.sciencedirect.com/science/article/pii/S0169207021001874

还有更多……

有一些方法可以最大化 N-BEATS 的潜力:

关于可解释性,除了 N-BEATS 外,你还可以采用另外两种方法:

使用 PyTorch Forecasting 优化学习率

在本例中,我们展示了如何基于 PyTorch Forecasting 优化模型的学习率。

准备工作

学习率是所有深度学习方法的基石参数。顾名思义,它控制着网络学习过程的速度。在本示例中,我们将使用与前一个示例相同的设置:

datamodule = GlobalDataModule(data=dataset,
    n_lags=N_LAGS,
    horizon=HORIZON,
    batch_size=32,
    test_size=0.2)
datamodule.setup()

我们还将以 N-BEATS 为例。然而,所有基于 PyTorch Forecasting 的模型,其过程是相同的。

如何做到这一点…

学习率的优化可以通过 PyTorch Lightning 的 Tuner 类进行。以下是使用 N-BEATS 的示例:

from lightning.pytorch.tuner import Tuner
import lightning.pytorch as pl
from pytorch_forecasting import NBeats
trainer = pl.Trainer(accelerator="auto", gradient_clip_val=0.01)
tuner = Tuner(trainer)
model = NBeats.from_dataset(
    dataset=datamodule.training,
    stack_types=['trend', 'seasonality'],
    num_blocks=[3, 3],
    num_block_layers=[4, 4],
    widths=[256, 2048],
    sharing=[True],
    backcast_loss_ratio=1.0,
)

在前面的代码中,我们定义了一个 Tuner 实例,作为 Trainer 对象的封装。我们还像前一部分一样定义了一个 NBeats 模型。然后,我们使用 lr_optim() 方法优化学习率:

lr_optim = tuner.lr_find(model,
    train_dataloaders=datamodule.train_dataloader(),
    val_dataloaders=datamodule.val_dataloader(),
    min_lr=1e-5)

完成此过程后,我们可以查看推荐的学习率值,并检查不同测试值的结果:

lr_optim.suggestion()
fig = lr_optim.plot(show=True, suggest=True)
fig.show()

我们可以在下图中可视化结果:

图 6.2:使用 PyTorch Forecasting 进行学习率优化

图 6.2:使用 PyTorch Forecasting 进行学习率优化

在此示例中,推荐的学习率大约是 0.05

它是如何工作的…

PyTorch Lightning 的 lr_find() 方法通过测试不同的学习率值,并选择一个最小化模型损失的值来工作。此方法使用训练和验证数据加载器来实现这一效果。

选择合适的学习率非常重要,因为不同的学习率值会导致不同性能的模型。较大的学习率收敛较快,但可能会收敛到一个次优解。然而,较小的学习率可能会需要过长的时间才能收敛。

在优化完成后,您可以像我们在之前的示例中一样,使用选定的学习率创建一个模型。

还有更多…

您可以在 PyTorch Forecasting 的 教程 部分了解更多关于如何充分利用像 N-BEATS 这样的模型的内容,详情请访问以下链接:pytorch-forecasting.readthedocs.io/en/stable/tutorials.html

开始使用 GluonTS

GluonTS 是一个灵活且可扩展的工具包,用于使用 PyTorch 进行概率时间序列建模。该工具包提供了专门为时间序列任务设计的最先进的深度学习架构,以及一系列用于时间序列数据处理、模型评估和实验的实用工具。

本节的主要目标是介绍gluonts库的基本组件,强调其核心功能、适应性和用户友好性。

准备开始

为了开始我们的学习之旅,确保安装了gluonts及其后端依赖pytorch

pip install gluonts pytorch

安装完成后,我们可以深入探索gluonts的功能。

如何操作…

我们首先访问由库提供的示例数据集:

from gluonts.dataset.repository.datasets import get_dataset
dataset = get_dataset("nn5_daily_without_missing", regenerate=False)

这将加载nn5_daily_without_missing数据集,这是gluonts提供的用于实验的数据集之一。

加载数据集后,可以检查其特性,使用get_dataset()函数进行操作。每个dataset对象包含元数据,提供关于时间序列频率、相关特征和其他相关属性的信息。你可以通过查看以下元数据了解数据集的更多信息:

print(dataset.metadata)

为了增强时间序列数据,gluonts提供了一系列转换器。例如,AddAgeFeature数据转换器为数据集添加了一个age特征,表示每个时间序列的生命周期:

from gluonts.transform import AddAgeFeature
transformation_with_age = Chain([
    AddAgeFeature(output_field="age",
    target_field="target",
    pred_length=dataset.metadata.prediction_length)
])
transformed_train_with_age = TransformedDataset(dataset.train, 
    transformation_with_age)

用于gluonts训练的数据通常表示为一个字典集合,每个字典代表一个时间序列,并附带可能的特征:

training_data = list(dataset.train)
print(training_data[0])

gluonts中的基本模型之一是SimpleFeedForwardEstimator模型。以下是它的设置:

首先,通过确定预测长度、上下文长度(表示考虑的前几个时间步的数量)和数据频率等参数来初始化估算器:

from gluonts.torch.model.simple_feedforward import SimpleFeedForwardEstimator
estimator_with_age = SimpleFeedForwardEstimator(
    hidden_dimensions=[10],
    prediction_length=dataset.metadata.prediction_length,
    context_length=100,
    trainer_kwargs={'max_epochs': 100}
)

要训练模型,只需在估算器上调用train()方法并提供训练数据:

predictor_with_age = estimator_with_age.train
    (transformed_train_with_age)

该过程使用提供的数据训练模型,生成一个准备好进行预测的预测器。以下是我们如何从模型中获取预测:

forecast_it_with_age, ts_it_with_age = make_evaluation_predictions(
    dataset=dataset.test,
    predictor=predictor_with_age,
    num_samples=100,
)
forecasts_with_age = list(forecast_it_with_age)
tss_with_age = list(ts_it_with_age)
fig, ax = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
ts_entry_with_age = tss_with_age[0]
ax[0].plot(ts_entry_with_age[-150:].to_timestamp())
forecasts_with_age[0].plot(show_label=True, ax=ax[0])
ax[0].set_title("Forecast with AddAgeFeature")
ax[0].legend()

在前面的代码中,可以使用make_evaluation_predictions()方法生成预测,然后将其与实际值进行对比绘制。以下是包含预测和实际值的图表:

图 6.3:使用和不使用 AddAgeFeature 的预测对比分析

图 6.3:使用和不使用 AddAgeFeature 的预测对比分析

在前面的图中,我们展示了使用和不使用AddAgeFeature的预测对比分析。使用该特征可以提高预测准确性,这表明它是此数据集中一个重要的变量。

它是如何工作的…

GluonTS 提供了一系列内置功能,有助于时间序列分析和预测。例如,数据转换器使你能够快速基于原始数据集构建新特征。如我们实验中所用,AddAgeFeature转换器将一个age属性附加到每个时间序列。时间序列的年龄往往能为模型提供相关的上下文信息。一个典型的应用场景是股票数据,较旧的股票可能会展现出与较新的股票不同的波动模式。

在 GluonTS 中训练采用基于字典的结构,每个字典对应一个时间序列,并包含其他相关的特征。这种结构使得附加、修改或删除特征变得更为容易。

在我们的实验中,我们测试了一个简单的模型,使用了SimpleFeedForwardEstimator模型。我们定义了两个模型实例,一个使用了AddAgeFeature,另一个没有使用。使用age特征训练的模型显示了更好的预测准确性,如我们在图 6.3中所看到的那样。这一改进突显了在时间序列分析中,特征工程的重要性。

使用 GluonTS 训练 DeepAR 模型

DeepAR 是一种先进的预测方法,利用自回归循环网络来预测时间序列数据的未来值。该方法由亚马逊提出,旨在解决需要较长预测周期的任务,如需求预测。当需要为多个相关的时间序列生成预测时,这种方法特别强大。

准备工作

我们将使用与前一个示例相同的数据集:

from gluonts.dataset.repository.datasets import get_dataset
dataset = get_dataset("nn5_daily_without_missing", regenerate=False)

现在,让我们看看如何使用这些数据构建 DeepAR 模型。

如何做…

我们从格式化数据开始进行训练:

  1. 我们通过使用ListDataset数据结构来实现:

    from gluonts.dataset.common import ListDataset
    from gluonts.dataset.common import FieldName
    train_ds = ListDataset(
        [
            {FieldName.TARGET: entry["target"], 
                FieldName.START: entry["start"]}
            for entry in dataset.train
        ],
        freq=dataset.metadata.freq,
    )
    
  2. 接下来,使用DeepAREstimator类定义 DeepAR 估计器,并指定诸如prediction_length(预测周期)、context_length(滞后数)和freq(采样频率)等参数:

    from gluonts.torch.model.deepar import DeepAREstimator
    N_LAGS=7
    HORIZON=7
    estimator = DeepAREstimator(
        prediction_length=HORIZON,
        context_length=N_LAGS,
        freq=dataset.metadata.freq,
        trainer_kwargs={"max_epochs": 100},
    )
    
  3. 在定义估计器后,使用train()方法训练 DeepAR 模型:

    predictor = estimator.train(train_ds)
    
  4. 使用训练好的模型对测试数据进行预测并可视化结果:

    forecast_it, ts_it = make_evaluation_predictions(
        dataset=dataset.test,
        predictor=predictor,
        num_samples=100,
    )
    forecasts = list(forecast_it)
    tss = list(ts_it)
    fig, ax = plt.subplots(1, 1, figsize=(10, 6))
    ts_entry = tss[0]
    ax.plot(ts_entry[-150:].to_timestamp())
    forecasts[0].plot(show_label=True, ax=ax, intervals=())
    ax.set_title("Forecast with DeepAR")
    ax.legend()
    plt.tight_layout()
    plt.show()
    

这是预测结果的图表:

图 6.4:DeepAR 预测与我们数据集中的真实值的比较

图 6.4:DeepAR 预测与我们数据集中的真实值的比较

该模型能够紧密匹配真实值。

它是如何工作的…

DeepAR 使用 RNN 架构,通常利用 LSTM 单元或 GRU 来建模时间序列数据。

context_length参数至关重要,因为它决定了模型在做出预测时将考虑多少过去的观测值作为其上下文。例如,如果将context_length设置为7,则模型将使用过去一周的数据来预测未来的值。

相反,prediction_length参数定义了预测的时间范围(即模型应预测的未来步数)。在给定的代码中,我们使用了一周的预测范围。

DeepAR 的一个突出特点是能够生成概率性预测。它不仅提供单一的点估计,而是提供一个可能未来值的分布,从而帮助我们理解预测中蕴含的不确定性。

最后,在处理多个相关时间序列时,DeepAR 利用序列间的共性来提高预测的准确性。

还有更多…

当满足以下条件时,DeepAR 表现尤为出色:

  • 你有多个相关时间序列;DeepAR 可以利用所有序列中的信息来改善预测。

  • 你的数据具有季节性或周期性模式。

  • 你希望生成概率性预测,这些预测会给出点估计并提供不确定性区间。我们将在下一章讨论不确定性估计。

你可以训练一个单一的 DeepAR 模型来处理全局数据集,并为数据集中的所有时间序列生成预测。另一方面,对于单独的时间序列,DeepAR 也可以分别训练每个序列,尽管这可能效率较低。

该模型特别适用于零售需求预测、股票价格预测和网站流量预测等应用。

使用 NeuralForecast 训练 Transformer 模型

现在,我们将目光转向近年来在人工智能各领域推动进步的 Transformer 架构。在本节中,我们将展示如何使用 NeuralForecast Python 库训练一个基础的 Transformer 模型。

准备工作

Transformer 已成为深度学习领域的主流架构,尤其在自然语言处理NLP)任务中表现突出。Transformer 也已被应用于 NLP 以外的各种任务,包括时间序列预测。

与传统模型逐点分析时间序列数据不同,Transformer 能够同时评估所有时间步。这种方法类似于一次性观察整个时间线,确定每个时刻与其他时刻的相关性,以便对特定时刻进行评估。

Transformer 架构的核心是注意力机制。该机制根据特定输入的相关性,计算输入值或来自前一层的值的加权和。与逐步处理输入的 RNN 不同,这使得 Transformer 能够同时考虑输入序列的所有部分。

Transformer 的关键组成部分包括以下内容:

  • 自注意力机制:计算所有输入值对的注意力得分,然后基于这些得分创建加权组合。

  • 多头注意力机制:该模型可以通过并行运行多个注意力机制,针对不同的任务或原因,集中注意力于输入的不同部分

  • 逐位置前馈网络:这些网络对注意力层的输出应用线性变换

  • 位置编码:由于 Transformer 本身没有任何固有的顺序感知,因此会将位置编码添加到输入嵌入中,以为模型提供序列中每个元素的位置相关信息

让我们看看如何训练一个 Transformer 模型。在本教程中,我们将再次使用 gluonts 库提供的数据集。我们将使用 NeuralForecast 库中提供的 Transformer 实现。NeuralForecast 是一个 Python 库,包含了多种专注于预测问题的神经网络实现,包括几种 Transformer 架构。

如何执行…

首先,让我们为 Transformer 模型准备数据集。与逐步处理输入序列的序列到序列模型(如 RNN、LSTM 或 GRU)不同,Transformer 会一次性处理整个序列。因此,如何格式化和输入数据可能会有所不同:

  1. 让我们从加载数据集和必要的库开始:

    from gluonts.dataset.repository.datasets import get_dataset
    import pandas as pd
    from sklearn.preprocessing import StandardScaler
    import matplotlib.pyplot as plt
    from neuralforecast.core import NeuralForecast
    from neuralforecast.models import VanillaTransformer
    dataset = get_dataset("nn5_daily_without_missing", regenerate=False)
    N_LAGS = 7
    HORIZON = 7
    
  2. 接下来,将数据集转换为 pandas DataFrame 并进行标准化。请记住,标准化是任何深度学习模型拟合的关键:

    data_list = list(dataset.train)
    data_list = [
        pd.Series(
            ds["target"],
            index=pd.date_range(
                start=ds["start"].to_timestamp(),
                freq=ds["start"].freq,
                periods=len(ds["target"]),
            ),
        )
        for ds in data_list
    ]
    tseries_df = pd.concat(data_list, axis=1)
    tseries_df[tseries_df.columns] = 
        \StandardScaler().fit_transform(tseries_df)
    tseries_df = tseries_df.reset_index()
    df = tseries_df.melt("index")
    df.columns = ["ds", "unique_id", "y"]
    df["ds"] = pd.to_datetime(df["ds"])
    
  3. 数据准备好后,我们将训练一个 Transformer 模型。与使用递归架构的 DeepAR 模型不同,Transformer 将依靠其注意力机制,在做出预测时考虑时间序列的各个部分:

    model = [
        VanillaTransformer(
            h=HORIZON,
            input_size=N_LAGS,
            max_steps=100,
            val_check_steps=5,
            early_stop_patience_steps=3,
        ),
    ]
    nf = NeuralForecast(models=model, freq="D")
    Y_df = df[df["unique_id"] == 0]
    Y_train_df = Y_df.iloc[:-2*HORIZON]
    Y_val_df = Y_df.iloc[-2*HORIZON:-HORIZON]
    training_df = pd.concat([Y_train_df, Y_val_df])
    nf.fit(df=training_df, val_size=HORIZON)
    
  4. 最后,展示预测结果:

    forecasts = nf.predict()
    Y_df = df[df["unique_id"] == 0]
    Y_hat_df = forecasts[forecasts.index == 0].reset_index()
    Y_hat_df = Y_test_df.merge(Y_hat_df, how="outer", 
        on=["unique_id", "ds"])
    plot_df = pd.
        concat([Y_train_df, Y_val_df, Y_hat_df]).set_index("ds")
    plot_df = plot_df.iloc[-150:]
    fig, ax = plt.subplots(1, 1, figsize=(20, 7))
    plot_df[["y", "VanillaTransformer"]].plot(ax=ax, linewidth=2)
    ax.set_title("First Time Series Forecast with Transformer", fontsize=22)
    ax.set_ylabel("Value", fontsize=20)
    ax.set_xlabel("Timestamp [t]", fontsize=20)
    ax.legend(prop={"size": 15})
    ax.grid()
    plt.show()
    

下图展示了 Transformer 预测值与时间序列实际值的对比:

图 6.5:Transformer 预测与我们数据集的真实值对比

图 6.5:Transformer 预测与我们数据集的真实值对比

它是如何工作的…

neuralforecast 库要求数据采用特定格式。每个观察值由三部分信息组成:时间戳、时间序列标识符以及对应的值。我们从准备数据集开始,确保其符合此格式。Transformer 实现于 VanillaTransformer 类中。我们设置了一些参数,例如预测范围、训练步数或与提前停止相关的输入。你可以在以下链接查看完整的参数列表:nixtla.github.io/neuralforecast/models.vanillatransformer.html。训练过程通过 NeuralForecast 类实例中的 fit() 方法进行。

Transformer 通过使用自注意力机制对整个序列进行编码来处理时间序列数据,从而捕捉依赖关系,而不考虑它们在输入序列中的距离。这种全局视角在存在长时间跨度的模式或依赖关系时特别有价值,或者当过去数据的相关性动态变化时。

位置编码用于确保 Transformer 识别数据点的顺序。没有它们,模型会将时间序列视为一堆没有内在顺序的值。

多头注意力机制使 Transformer 能够同时关注不同的时间步长和特征,特别适用于具有多个交互模式和季节性变化的复杂时间序列。

还有更多…

由于以下原因,Transformer 在时间序列预测中可能非常有效:

  • 它们捕捉数据中长期依赖关系的能力

  • 在大规模数据集上的可扩展性

  • 在建模单变量和多变量时间序列时的灵活性

与其他模型一样,Transformer 也能通过调整超参数来受益,例如调整注意力头的数量、模型的大小(即层数和嵌入维度)以及学习率。

使用 GluonTS 训练一个时序融合变换器

TFT 是一种基于注意力机制的架构,由 Google 开发。它具有递归层,用于学习不同尺度的时间关系,并结合自注意力层以提高可解释性。TFT 还使用变量选择网络进行特征选择,门控层用于抑制不必要的成分,并采用分位数损失作为其损失函数,用以生成预测区间。

本节将深入探讨如何使用 GluonTS 框架训练并进行 TFT 模型的推理。

准备工作

确保你的环境中安装了 GluonTS 库和 PyTorch 后端。我们将使用来自 GluonTS 仓库的nn5_daily_without_missing数据集作为工作示例:

from gluonts.dataset.common import ListDataset, FieldName
from gluonts.dataset.repository.datasets import get_dataset
dataset = get_dataset("nn5_daily_without_missing", regenerate=False)
train_ds = ListDataset(
    [
        {FieldName.TARGET: entry["target"], FieldName.START: entry["start"]}
        for entry in dataset.train
    ],
    freq=dataset.metadata.freq,
)

在接下来的部分,我们将使用这个数据集训练一个 TFT 模型。

如何实现…

数据集准备好后,接下来定义 TFT 估计器:

  1. 我们将从指定超参数开始,例如预测长度、上下文长度和训练频率:

    from gluonts.torch.model.tft import TemporalFusionTransformerEstimator
    N_LAGS = 7
    HORIZON = 7
    estimator = TemporalFusionTransformerEstimator(
        prediction_length=HORIZON,
        context_length=N_LAGS,
        freq=dataset.metadata.freq,
        trainer_kwargs={"max_epochs": 100},
    )
    
  2. 在定义估计器后,继续使用训练数据集训练 TFT 模型:

    predictor = estimator.train(train_ds)
    
  3. 训练完成后,我们可以使用模型进行预测。利用make_evaluation_predictions()函数来实现这一点:

    from gluonts.evaluation import make_evaluation_predictions
    forecast_it, ts_it = make_evaluation_predictions(
        dataset=dataset.test,
        predictor=predictor,
        num_samples=100,
    )
    
  4. 最后,我们可以通过可视化预测结果来了解模型的表现:

    import matplotlib.pyplot as plt
    ts_entry = tss[0]
    ax.plot(ts_entry[-150:].to_timestamp())
    forecasts[0].plot(show_label=True, ax=ax, intervals=())
    ax.set_title("Forecast with Temporal Fusion Transformer")
    ax.legend()
    plt.tight_layout()
    plt.show()
    

以下是模型预测与数据集实际值的比较。

图 6.6:TFT 预测与我们数据集中的真实值比较

图 6.6:TFT 预测与我们数据集中的真实值比较

它是如何工作的…

我们使用gluonts中提供的 TFT 实现。主要参数包括滞后数(上下文长度)和预测时段。你还可以测试模型某些参数的不同值,例如注意力头的数量(num_heads)或 Transformer 隐状态的大小(hidden_dim)。完整的参数列表可以在以下链接中找到:ts.gluon.ai/stable/api/gluonts/gluonts.torch.model.tft.estimator.html

TFT 适用于多种使用场景,因为它具备完整的特征集:

  • 时间处理:TFT 通过序列到序列模型解决了整合过去观察值和已知未来输入的挑战,利用 LSTM 编码器-解码器。

  • 注意力机制:该模型使用注意力机制,能够动态地为不同的时间步分配重要性。这确保了模型只关注相关的历史数据。

  • 门控机制:TFT 架构利用门控残差网络,提供在建模过程中的灵活性,能够适应数据的复杂性。这种适应性对于处理不同的数据集尤其重要,特别是对于较小或噪声较多的数据集。

  • 变量选择网络:该组件用于确定每个协变量与预测的相关性。通过加权输入特征的重要性,它过滤掉噪声,仅依赖于重要的预测因子。

  • 静态协变量编码器:TFT 将静态信息编码为多个上下文向量,丰富了模型的输入。

  • 分位数预测:通过预测每个时间步的不同分位数,TFT 提供了可能结果的范围。

  • 可解释输出:尽管 TFT 是一个深度学习模型,但它提供了特征重要性方面的见解,确保预测的透明性。

还有更多……

除了架构创新之外,TFT 的可解释性使其成为当需要解释预测是如何生成时的良好选择。诸如变量网络选择和时序多头注意力层等组件,揭示了不同输入和时间动态的重要性,使 TFT 不仅仅是一个预测工具,还是一个分析工具。

使用 NeuralForecast 训练 Informer 模型

在本教程中,我们将探索neuralforecast Python 库,用于训练 Informer 模型,Informer 是另一种基于 Transformer 的深度学习预测方法。

准备开始

Informer 是一种针对长期预测而量身定制的 Transformer 方法——即,具有较长预测时段的预测。与标准 Transformer 相比,Informer 的主要区别在于其改进的自注意力机制,这大大减少了运行模型和生成长序列预测的计算需求。

在这个教程中,我们将向你展示如何使用neuralforecast训练 Informer 模型。我们将使用与之前教程相同的数据集:

from gluonts.dataset.repository.datasets import get_dataset
dataset = get_dataset('nn5_daily_without_missing')

如何实现……

这次,我们不是创建DataModule来处理数据预处理,而是使用基于neuralforecast模型的典型工作流程:

  1. 我们首先准备时间序列数据集,以符合neuralforecast方法所期望的特定格式:

    import pandas as pd
    from sklearn.preprocessing import StandardScaler
    data_list = list(dataset.train)
    data_list = [pd.Series(ds['target'],
        index=pd.date_range(start=ds['start'].to_timestamp(),
            freq=ds['start'].freq,
            periods=len(ds['target'])))
        for ds in data_list]
    tseries_df = pd.concat(data_list, axis=1)
    tseries_df[tseries_df.columns] = \
        StandardScaler().fit_transform(tseries_df)
    tseries_df = tseries_df.reset_index()
    df = tseries_df.melt('index')
    df.columns = ['ds', 'unique_id', 'y']
    df['ds'] = pd.to_datetime(df['ds'])
    n_time = len(df.ds.unique())
    val_size = int(.2 * n_time)
    
  2. 我们将数据集转化为一个包含三列的 pandas DataFrame:dsunique_idy。它们分别表示时间戳、时间序列的 ID 和对应时间序列的值。在前面的代码中,我们使用scikit-learn的标准缩放器将所有时间序列转换为一个共同的数值范围。我们还将验证集的大小设置为时间序列大小的 20%。现在,我们可以如下设置 Informer 模型:

    from neuralforecast.core import NeuralForecast
    from neuralforecast.models import Informer
    N_LAGS = 7
    HORIZON = 7
    model = [Informer(h=HORIZON,
        input_size=N_LAGS,
        max_steps=1000,
        val_check_steps=25,
        early_stop_patience_steps=10)]
    nf = NeuralForecast(models=model, freq='D')
    
  3. 我们将 Informer 的上下文长度(滞后数)设置为7,以便在每个时间步预测接下来的 7 个值。训练步数设置为1000,我们还设置了早期停止机制以帮助拟合过程。这些仅是设置 Informer 时可以使用的部分参数。你可以通过以下链接查看完整的参数列表:nixtla.github.io/neuralforecast/models.informer.html。模型被传递到NeuralForecast类实例中,我们还将时间序列的频率设置为每日(D关键字)。然后,训练过程如下进行:

    nf.fit(df=df, val_size=val_size)
    
  4. nf对象用于拟合模型,然后可以用来进行预测:

    forecasts = nf.predict()
    forecasts.head()
    

预测结果以 pandas DataFrame 的形式结构化,因此你可以通过使用head()方法查看预测的样本。

它是如何工作的……

neuralforecast库提供了一个简单的框架,用于训练强大的时间序列问题模型。在这种情况下,我们将数据逻辑处理放在框架外部,因为它会在内部处理数据传递给模型的过程。

NeuralForecast类实例接受一个模型列表作为输入(在本例中只有一个Informer实例),并负责训练过程。如果你想直接使用最先进的模型,这个库可以是一个不错的解决方案。其限制是,它的灵活性不如基础的 PyTorch 生态系统。

还有更多…

在这个教程中,我们描述了如何使用neuralforecast训练一个特定的 Transformer 模型。但这个库包含了其他你可以尝试的 Transformers,包括以下几种:

  • Vanilla Transformer

  • TFT

  • Autoformer

  • PatchTST

你可以在以下链接查看完整的模型列表:nixtla.github.io/neuralforecast/core.html

使用 NeuralForecast 比较不同的 Transformer

NeuralForecast 包含几种深度学习方法,你可以用来解决时间序列问题。在本节中,我们将引导你通过 neuralforecast 比较不同基于 Transformer 的模型的过程。

准备工作

我们将使用与前一节相同的数据集(df 对象)。我们将验证集和测试集的大小分别设置为数据集的 10%:

val_size = int(.1 * n_time)
test_size = int(.1 * n_time)

现在,让我们来看一下如何使用 neuralforecast 比较不同的模型。

如何操作…

我们首先定义要比较的模型。在这个例子中,我们将比较一个 Informer 模型和一个基础版 Transformer,我们将模型设置如下:

from neuralforecast.models import Informer, VanillaTransformer
models = [
    Informer(h=HORIZON,
        input_size=N_LAGS,
        max_steps=1000,
        val_check_steps=10,
        early_stop_patience_steps=15),
    VanillaTransformer(h=HORIZON,
        input_size=N_LAGS,
        max_steps=1000,
        val_check_steps=10,
        early_stop_patience_steps=15),
]

每个模型的训练参数设置相同。我们可以使用 NeuralForecast 类,通过 cross_validation() 方法比较不同的模型,方法如下:

from neuralforecast.core import NeuralForecast
nf = NeuralForecast(
    models=models,
    freq='D')
cv = nf.cross_validation(df=df,
    val_size=val_size,
    test_size=test_size,
    n_windows=None)

cv 对象是比较的结果。以下是每个模型在特定时间序列中的预测样本:

图 6.7:示例时间序列中两个 Transformer 模型的预测结果

图 6.7:示例时间序列中两个 Transformer 模型的预测结果

Informer 模型似乎产生了更好的预测结果,我们可以通过计算平均绝对误差来验证这一点:

from neuralforecast.losses.numpy import mae
mae_informer = mae(cv['y'], cv['Informer'])
mae_transformer = mae(cv['y'], cv['VanillaTransformer'])

Informer 的误差为 0.42,优于 VanillaTransformer 得到的 0.53 分数。

工作原理…

在背后,cross_validation() 方法的工作原理如下。每个模型使用训练集和验证集进行训练。然后,它们在测试实例上进行评估。测试集上的预测性能为我们提供了一个可靠的估计,表示我们期望模型在实际应用中达到的性能。因此,你应该选择能最大化预测性能的模型,并用整个数据集重新训练它。

neuralforecast 库包含其他可以进行比较的模型。你也可以比较同一种方法的不同配置,看看哪种最适合你的数据。

第七章:概率时间序列预测

在前几章中,我们从点预测的角度探讨了时间序列问题。点预测模型预测的是一个单一值。然而,预测本质上是充满不确定性的,因此量化预测的不确定性是有意义的。这就是概率预测的目标,它可以作为一种有价值的方法,帮助做出更明智的决策。

在本章中,我们将重点介绍三种类型的概率预测设置。我们将深入探讨超限概率预测,它帮助我们估计时间序列超过预定阈值的可能性。我们还将讨论预测区间,它提供一个可能值的范围,未来的观测值可能会落在该范围内。最后,我们将探讨预测概率预测,它提供了对单个结果的概率评估,从而为未来的可能性提供更细致的视角。

本章涵盖以下食谱:

  • 超限概率预测简介

  • 使用 LSTM 进行超限概率预测

  • 使用符合预测法创建预测区间

  • 使用 LSTM 进行概率预测

  • 使用 DeepAR 进行概率预测

  • 高斯过程简介

  • 使用 Prophet 进行概率预测

技术要求

本章将重点介绍 PyTorch 生态系统。以下是本章将使用的库的完整列表:

  • NumPy (1.26.2)

  • pandas (2.1.3)

  • scikit-learn (1.3.2)

  • PyTorch Forecasting (1.0.0)

  • PyTorch Lightning (2.1.2)

  • torch (2.1.1)

  • statsforecast (1.6.0)

  • GluonTS (0.14.2)

  • gpytorch (1.11)

  • prophet (1.1.5)

你可以使用 pip,Python 的包管理器,来安装这些库。例如,要安装 scikit-learn,你可以运行以下命令:

pip install -U scikit-learn

本章的代码可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

超限概率预测简介

本食谱介绍了超限概率预测问题。超限事件发生在时间序列在预定的未来时期超过预定义的阈值。这类问题在时间序列分布的尾部可能对领域产生重大影响时尤为相关。例如,考虑到经济中的通货膨胀率。中央银行利用这种预测评估通货膨胀率可能超过某个临界阈值的可能性,如果超过该阈值,它们可能考虑提高利率。

从数据科学的角度来看,超限事件是二分类问题。因此,通常使用二元概率分类模型来处理这些问题。一个挑战是,代表超限事件的类别是稀有的,这使得学习任务更加困难。

准备工作

我们将使用一个多变量时间序列作为示例,描述什么是超越概率任务,以及它们为什么重要。具体来说,我们将使用前几章中使用的太阳辐射数据集(例如,参见为监督学习准备多变量时间序列食谱,来自第四章)。

让我们首先使用pandas加载数据集:

import pandas as pd
mvtseries = pd.read_csv
    ('assets/data/daily_multivariate_timeseries.csv',
    parse_dates=['datetime'],
    index_col='datetime')

现在,让我们看看如何使用这个时间序列定义一个超越问题。

如何做...

超越概率预测是预测一个时间序列在未来某一时期超过某个临界阈值的概率的过程。我们将使用来自 PyTorch Lightning 的数据模块,它可以处理定义任务所需的所有步骤。

这个模块的主要组件是setup()方法。大部分步骤已经在多变量时间序列预测中的前馈神经网络食谱中解释过了。为了创建超越任务,我们必须首先定义新的二元目标变量,具体如下:

mvtseries['target'] = 
    \(mvtseries['Incoming Solar'].diff() < -2000).astype(int)

在上述代码中,我们使用diff()方法计算连续观测中太阳辐射值的变化。然后,我们检查总日太阳辐射(单位:瓦特/平方米)是否在一天到第二天之间下降了2000。这个值是随意设定的。直觉上,这应该是我们感兴趣的重大事件。对于这个案例研究,太阳辐射的这种显著下降意味着电力系统将无法从光伏设备中产生大量太阳能。因此,及时预测这些事件可以使电力系统更加高效地从替代能源源中产生能源。

这是差分序列和所选阈值的图表:

图 7.1:连续观测中总日太阳辐射的差异

图 7.1:连续观测中总日太阳辐射的差异

之后,我们将此变量作为目标变量传递给数据模块中的TimeSeriesDataSet实例。让我们首先加载所需的库并构建数据模块的构造函数:

import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from pytorch_forecasting import TimeSeriesDataSet
from pytorch_lightning import LightningDataModule
class ExceedanceDataModule(LightningDataModule):
    def __init__(self,
                 data: pd.DataFrame,
                 test_size: float = 0.2,
                 batch_size: int = 1):
        super().__init__()
        self.data = data
        self.var_names = self.data.columns.tolist()
        self.batch_size = batch_size
        self.test_size = test_size
        self.training = None
        self.validation = None
        self.test = None
        self.predict_set = None

在构造函数中,我们存储了数据预处理阶段中使用的所有元素。该类的setup()方法在以下代码中实现:

    def setup(self, stage=None):
        self.data['target'] = (
            self.data['Incoming Solar'].diff() < -2000).astype(int)
        self.data['time_index'] = np.arange(self.data.shape[0])
        self.data['group_id'] = 0
        unique_times = self.data['time_index'].sort_values().unique()
        tr_ind, ts_ind = \
            train_test_split(unique_times,
                test_size=self.test_size,
                shuffle=False)
        tr_ind, vl_ind = train_test_split(tr_ind,
                test_size=0.1, shuffle=False)
        training_df = self.data.loc[
            self.data['time_index'].isin(tr_ind), :]
        validation_df = self.data.loc[
            self.data['time_index'].isin(vl_ind), :]
        test_df = self.data.loc[
            self.data['time_index'].isin(ts_ind), :]
        self.training = TimeSeriesDataSet(
            data=training_df,
            time_idx="time_index",
            target="target",
            group_ids=['group_id'],
            max_encoder_length=14,
            max_prediction_length=7,
            time_varying_unknown_reals=self.var_names,
            scalers={k: MinMaxScaler()
                     for k in self.var_names
                     if k != 'target'}
        )
        self.validation = TimeSeriesDataSet.from_dataset(
            self.training, validation_df)
        self.test = TimeSeriesDataSet.from_dataset(
            self.training, test_df)
        self.predict_set = TimeSeriesDataSet.from_dataset(
            self.training, self.data, predict=True)

这个函数的工作方式类似于标准的自回归管道。关键的不同之处在于,我们将目标变量设置为一个二元变量,表示是否发生了超越事件。我们还设置了训练、验证和测试集,用于构建和评估模型。我们将滞后数设置为14max_encoder_length),预测时长设置为7max_prediction_length)。

LightningDataModule实例的其余方法与我们在上一章中构建的类似(例如,参见多变量时间序列预测中的前馈神经网络食谱):

    def train_dataloader(self):
        return self.training.to_dataloader(
            batch_size=self.batch_size, shuffle=False)
    def val_dataloader(self):
        return self.validation.to_dataloader(
            batch_size=self.batch_size, shuffle=False)
    def test_dataloader(self):
        return self.test.to_dataloader(
            batch_size=self.batch_size, shuffle=False)
    def predict_dataloader(self):
        return self.predict_set.to_dataloader(
            batch_size=1, shuffle=False)

以下是如何使用该数据模块获取单个观测值:

datamodule = ExceedanceDataModule(data=mvtseries)
datamodule.setup()
x, y = next(iter(datamodule.train_dataloader()))

在前面的代码中,我们创建了一个ExceedanceDataModule的实例,随后使用iter()next()方法从中获取观测值。

工作原理…

超越问题也可以通过自回归方法来解决。因此,我们可以基于时间序列最近观测值的值来预测超越事件的概率。

超越概率预测问题是一种特定类型的二分类任务,可以使用时间序列来定义,其中事件是由超越定义的。然而,也可以定义其他类型的事件,这些事件不一定基于超越事件,可以据此构建概率模型。所需的逻辑都设置在setup()方法中,该方法封装了所有的预处理步骤。

还有更多内容…

在本示例中,我们使用单一的多变量时间序列来描述超越任务。然而,我们强调,我们的方法可以通过数据模块框架轻松地为涉及多个时间序列的数据集定义。

与超越概率预测任务相关的一个问题是时间序列分类,其中给定的时间序列有一个关联标签。我们将在下一章中学习这个问题。

使用 LSTM 进行超越概率预测

本示例描述了如何创建一个概率深度学习模型,通过多变量时间序列解决超越任务。

准备工作

我们将继续使用太阳辐射数据集作为示例。以下是我们在之前的示例中定义的数据模块:

N_LAGS = 14
HORIZON = 7
mvtseries = pd.read_csv('assets/daily_multivariate_timeseries.csv',
        parse_dates=['datetime'],
        index_col='datetime')
datamodule = ExceedanceDataModule(data=mvtseries,
        batch_size=64, test_size=0.3)

现在,让我们看看如何使用 LSTM 神经网络和 PyTorch 的LightningModule创建一个分类器。

如何操作…

我们将使用 PyTorch Lightning 的LightningModule来设置一个二分类任务。以下是构造函数和forward()方法:

import torch.nn as nn
import lightning.pytorch as pl
class ExceedanceLSTM(pl.LightningModule):
    def __init__(self, input_dim, hidden_dim, num_layers):
        super().__init__()
        self.hidden_dim = hidden_dim
        self.lstm = nn.LSTM(input_dim, self.hidden_dim,
                    num_layers, batch_first=True)
        self.fc = nn.Linear(self.hidden_dim, 1)
    def forward(self, x):
        h0 = torch.zeros(self.lstm.num_layers, x.size(0),
            self.hidden_dim).to(self.device)
        c0 = torch.zeros(self.lstm.num_layers, x.size(0),
            self.hidden_dim).to(self.device)
        out, _ = self.lstm(x, (h0, c0))
        out = self.fc(out[:, -1, :])
        out = torch.sigmoid(out)
        return out

LSTM 架构与我们在第四章中学习的类似——我们基于 PyTorch 创建一个 LSTM 层,并设置关于层数、单元数和输入维度(时间序列变量的数量)的配置。在前向传播过程中,LSTM 层的输出会传递给一个线性层。在之前涉及预测未来观测值数值的示例中,这将是网络的最后一层。然而,对于分类任务,我们添加了一个 sigmoid 层(torch.sigmoid),它将模型的输出转换为介于01之间的值。

模块的训练和验证步骤的代码如下:

    def training_step(self, batch, batch_idx):
        x, y = batch
        y_bin = (y[0] > 0).any(axis=1).long().type(torch.FloatTensor)
        y_pred = self(x['encoder_cont'])
        loss = F.binary_cross_entropy(y_pred.squeeze(-1), y_bin)
        self.log('train_loss', loss)
        return loss
    def validation_step(self, batch, batch_idx):
        x, y = batch
        y_bin = (y[0] > 0).any(axis=1).long().type(torch.FloatTensor)
        y_pred = self(x['encoder_cont'])
        loss = F.binary_cross_entropy(y_pred.squeeze(-1), y_bin)
        self.log('val_loss', loss)
        return loss
    def configure_optimizers(self):
        return torch.optim.Adam(self.parameters(), lr=0.001)

在前面的代码中,训练和验证方法遵循相似的步骤:

  1. 首先,我们检查预测时间范围内的任何观测值是否为正(在y[0] > 0).any(axis=1).long()代码片段中)。实际上,我们正在构建一个神经网络,用于建模未来7个观测值中是否存在超越事件。

  2. 我们将此测试的输出转换为torch.FloatTensor数据结构,这是loss()函数工作的必需结构。

  3. 然后,我们使用二元交叉熵(F.binary_cross_entropy)比较预测值和实际值,这是用于训练模型的方式。

除了这些方法,我们还将优化器设置为 Adam,并使用 0.001 的学习率。最后,我们设置了测试方法:

    def test_step(self, batch, batch_idx):
        x, y = batch
        y_bin = (y[0] > 0).any(axis=1).long().type(torch.FloatTensor)
        y_pred = self(x['encoder_cont'])
        loss = F.binary_cross_entropy(y_pred.squeeze(-1), y_bin)
        auroc = AUROC(task='binary')
        auc_score = auroc(y_pred, y_bin)
        self.log('test_bce', loss)
        self.log('test_auc', auc_score)

在上述代码中,我们增加了ROC曲线下面积作为评估指标,这是常用于测试二分类模型的一个指标。

最后,我们必须训练和测试模型:

model = ExceedanceLSTM(input_dim=10, hidden_dim=32, num_layers=1)
early_stop_callback = EarlyStopping(monitor="val_loss",
                                    min_delta=1e-4,
                                    patience=10,
                                    verbose=False,
                                    mode="min")
trainer = pl.Trainer(
    max_epochs=100,
    accelerator="cpu",
    callbacks=[early_stop_callback]
)
trainer.fit(model, datamodule)
trainer.test(model, datamodule)

如你所见,工作流遵循 PyTorch Lightning 风格,其中Trainer实例使用神经网络模型和数据模块进行训练和测试。

工作原理…

我们在前几章中构建的深度学习模型可以扩展用于分类预测任务。在这种情况下,我们增加了一个 sigmoid 层,该层将前一层的输出映射到0–1的值范围内。这个值可以解释为观察值属于正类的概率,在我们这里即为超越事件。

分类模型不再通过均方误差等指标进行优化。对于二分类问题,我们使用二元交叉熵。在测试阶段,我们增加了ROC曲线下面积作为辅助评估指标,这有助于理解模型如何区分两个类别。下图展示了 ROC 曲线的结果:

图 7.2:超越概率模型在 ROC 曲线中的结果

图 7.2:超越概率模型在 ROC 曲线中的结果

ROC 曲线提供了一种可视化方法,用于展示不同决策阈值下概率分类器的性能。位于对角线上的结果表示与随机猜测相同的性能。当曲线朝左上角移动时,表示模型的表现越好。

除了这些管道调整,PyTorch Lightning 提供的设计模式使得整体代码类似于我们在前几章中构建点预测模型时所用的代码。

还有更多…

我们注意到,尽管这里重点讨论的是 LSTM,但其他架构也可以使用,例如前馈神经网络或卷积神经网络。

使用符合预测创建预测区间

在这个教程中,我们将探索如何创建预测区间。预测区间描述了一个范围,在这个范围内,未来的观察值很可能会落入某个置信水平。所需的置信度越高,区间就越大。

在实践中,模型不仅预测一个单一的点,而是为未来的观察值预测一个分布。构建这些区间的方法有很多种,包括假设误差服从特定分布的参数方法,以及使用经验数据估计区间的非参数方法。

我们将采用符合预测方法,这种方法在数据科学从业者中越来越流行。

准备工作

我们将为 ARIMA 模型构建预测区间,这是一个流行的预测方法。然而,符合预测方法与底层模型无关,可以应用于其他预测方法。

让我们从加载一个时间序列开始。在这个例子中,我们将处理一个单变量时间序列:

import pandas as pd
dataset = pd.read_csv(
    "assets/daily_multivariate_timeseries.csv",
    parse_dates=["datetime"],
)

让我们看看如何使用这个数据集创建预测区间。

如何操作……

数据被分为训练集和测试集,用于拟合 ARIMA 模型。我们将重点使用statsforecast Python 库,因此我们需要将时间序列转换为包含三列的pandas DataFrame:

  • ds:对应观测值的时间步长

  • unique_id:时间序列的标识符,由于我们处理的是单一时间序列,因此它是常数。

  • y:观测值

这个过程如下所示:

series = dataset[['Incoming Solar']].reset_index()
series['id'] = 'Solar'
series = series.rename(columns={'index': 'ds', 'Incoming Solar': 'y', 
    'id': 'unique_id'})

接下来,我们必须将数据分为训练集和测试集:

from sklearn.model_selection import train_test_split
HORIZON = 7
train, test = train_test_split(series, test_size=HORIZON)

测试集由最后7个观测值组成。

现在,我们必须使用statsforecast中的ConformalIntervals类设置符合方法。我们还需要创建一个 ARIMA 模型,并将符合实例传递给它:

from statsforecast.utils import ConformalIntervals
intervals = ConformalIntervals(h=HORIZON)
models = [
    ARIMA(order=(2, 0, 2),
          season_length=365,
          prediction_intervals=intervals),
]

在前面的代码中,我们将季节长度设置为365,因为我们的数据是按日记录的,我们预期太阳辐射会呈现出重复的年度变化。

最后,我们必须使用StatsForecast类的实例来获取模型的预测值:

sf = StatsForecast(
    df=train,
    models=models,
    freq='D',
)
forecasts = sf.forecast(h=HORIZON, level=[95])

在这里,我们将预测区间的水平设置为95。这意味着我们期望实际值以 95%的置信度位于相应的区间内。

这是我们获得的预测区间的图示:

 图 7.3:ARIMA 模型预测及其相应的区间

图 7.3:ARIMA 模型预测及其相应的区间

它是如何工作的……

符合预测方法通过使用历史数据的一个子集来拟合 ARIMA 模型。然后,使用另一个子集来校准符合预测,通常是通过计算不符合性分数来衡量实际观测值与模型预测之间的偏差。校准步骤允许确定一个阈值,该阈值对应于所需的置信水平(例如,95%)。这个阈值将用于未来的预测,以便在预测值周围构建区间,提供一个范围,在该范围内,实际值预计将以指定的置信水平落入其中。

符合预测有助于量化点预测背后的不确定性,通过在这些预测周围构建区间。在这个例子中,我们训练了一个 ARIMA 模型,并使用符合预测构建了它的预测区间。我们将置信水平设置为95,但我们也可以同时探索多个值。例如,你可以通过将level参数改为level=[80, 95]来实现。

总体而言,这个食谱遵循一个简单的训练加测试周期,使用 statsforecast Python 库框架。

使用 LSTM 进行概率预测

本食谱将引导你构建一个用于概率预测的 LSTM 神经网络,使用 PyTorch Lightning。

准备工作

在这个食谱中,我们将介绍使用 LSTM 网络进行概率预测。这种方法结合了 LSTM 模型在捕捉序列数据中长期依赖关系方面的优势和概率预测的细致视角。这种方法超越了传统的点估计,通过预测一系列可能的未来结果,每个结果都附带一个概率。这意味着我们在预测中加入了不确定性。

这个食谱使用了与我们在第四章多变量时间序列预测的前馈神经网络 食谱中相同的数据集。我们还将使用在该食谱中创建的相同数据模块,名为 MultivariateSeriesDataModule

让我们探索如何使用这个数据模块来构建一个用于概率预测的 LSTM 模型。

如何操作…

在本小节中,我们将定义一个概率 LSTM 模型,该模型输出时间序列每个预测点的预测均值和标准差。此技术涉及设计 LSTM 模型以预测定义未来结果概率分布的参数,而不是输出单一值。该模型通常配置为输出特定分布的参数,如高斯分布的均值和方差。这些分别描述了未来值的期望值和分布:

  1. 让我们首先定义一个回调函数:

    class LossTrackingCallback(Callback):
        def __init__(self):
            self.train_losses = []
            self.val_losses = []
        def on_train_epoch_end(self, trainer, pl_module):
            if trainer.logged_metrics.get("train_loss_epoch"):
                self.train_losses.append(
                    trainer.logged_metrics["train_loss_epoch"].item())
        def on_validation_epoch_end(self, trainer, pl_module):
            if trainer.logged_metrics.get("val_loss_epoch"):
                self.val_losses.append(
                    trainer.logged_metrics["val_loss_epoch"].item())
    

    LossTrackingCallback 类用于监控整个训练过程中的训练和验证损失。这对于诊断模型的学习过程、识别过拟合以及决定何时停止训练至关重要。

  2. 接下来,我们必须基于 PyTorch Lightning 的 LightningModule 类构建 LSTM 模型:

    class ProbabilisticLSTM(LightningModule):
        def __init__(self, input_size,
                     hidden_size, seq_len,
                     num_layers=2):
            super().__init__()
            self.save_hyperparameters()
            self.lstm = nn.LSTM(input_size, hidden_size,
                num_layers, batch_first=True)
            self.fc_mu = nn.Linear(hidden_size, 1)
            self.fc_sigma = nn.Linear(hidden_size, 1)
            self.hidden_size = hidden_size
            self.softplus = nn.Softplus()
        def forward(self, x):
            lstm_out, _ = self.lstm(x)
            lstm_out = lstm_out[:, -1, :]
            mu = self.fc_mu(lstm_out)
            sigma = self.softplus(self.fc_sigma(lstm_out))
            return mu, sigma
    

    ProbabilisticLSTM 类定义了我们用于概率预测的 LSTM 架构。该类包括计算预测均值(fc_mu)和预测分布标准差(fc_sigma)的层。标准差通过 Softplus () 激活函数传递,以确保其始终为正,反映了标准差的特性。

  3. 以下代码实现了训练和验证步骤,以及网络配置参数:

        def training_step(self, batch, batch_idx):
            x, y = batch[0]["encoder_cont"], batch[1][0]
            mu, sigma = self.forward(x)
            dist = torch.distributions.Normal(mu, sigma)
            loss = -dist.log_prob(y).mean()
            self.log(
                "train_loss", loss, on_step=True,
                on_epoch=True, prog_bar=True, logger=True
            )
            return {"loss": loss, "log": {"train_loss": loss}}
        def validation_step(self, batch, batch_idx):
            x, y = batch[0]["encoder_cont"], batch[1][0]
            mu, sigma = self.forward(x)
            dist = torch.distributions.Normal(mu, sigma)
            loss = -dist.log_prob(y).mean()
            self.log(
                "val_loss", loss, on_step=True,
                on_epoch=True, prog_bar=True, logger=True
            )
            return {"val_loss": loss}
        def configure_optimizers(self):
            optimizer = optim.Adam(self.parameters(), lr=0.0001)
            scheduler = optim.lr_scheduler.ReduceLROnPlateau(
                optimizer, "min")
            return {
                "optimizer": optimizer,
                "lr_scheduler": scheduler,
                "monitor": "val_loss",
            }
    
  4. 在定义模型架构后,我们初始化数据模块并设置训练回调函数。正如我们之前看到的,EarlyStopping回调函数是一个有效的工具,通过在模型在验证集上停止改进时终止训练过程,防止过拟合。ModelCheckpoint回调函数确保我们根据模型在验证集上的表现捕获并保存最佳模型。结合这些回调函数,它们优化了训练过程,有助于开发出一个强大且调优良好的模型:

    datamodule = ContinuousDataModule(data=mvtseries)
    datamodule.setup()
    model = ProbabilisticLSTM(
        input_size = input_size, hidden_size=hidden_size,
        seq_len=seq_len
    )
    early_stop_callback = EarlyStopping(monitor="val_loss", 
        patience=5)
    checkpoint_callback = ModelCheckpoint(
        dirpath="./model_checkpoint/", save_top_k=1, 
        monitor="val_loss"
    )
    loss_tracking_callback = LossTrackingCallback()
    trainer = Trainer(
        max_epochs=100,
        callbacks=[early_stop_callback, checkpoint_callback,
        loss_tracking_callback],
    )
    trainer.fit(model, datamodule)
    

    使用 PyTorch Lightning 中的Trainer类简化了训练过程,内部处理复杂的训练循环,让我们能够专注于定义模型及其行为。它提高了代码的可读性和可维护性,使得尝试不同模型配置变得更加容易。

  5. 训练完成后,评估模型的性能并可视化其概率性预测非常重要。预测均值的图形表示,与其不确定性区间相结合,并与实际值对比,清晰地展示了模型的预测能力以及预测中固有的不确定性。我们构建了一个可视化框架来绘制预测图。你可以在以下链接查看相关函数:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

下图展示了我们时间序列的真实值(蓝色),预测均值由虚线红色线条表示:

图 7.4:带有不确定性区间和真实值的概率性预测

图 7.4:带有不确定性区间和真实值的概率性预测

阴影区域表示不确定性区间,其计算方法是以预测均值为基础的标准差。相比于点估计,这种概率性预测方法提供了更为全面的图景,因为它考虑了时间序列数据中固有的变异性和不确定性。不确定性区间与实际值的重叠区域表明模型在这些区域内对预测的信心较高。相反,较宽的区间可能表示更大的不确定性,可能是由于数据中的固有噪声或模型更难捕捉的复杂潜在动态。

此外,下图提供了我们概率性 LSTM 模型训练动态的深入分析:

图 7.5:训练和验证损失随训练轮次的变化,展示了概率性 LSTM 模型的学习进展

图 7.5:训练和验证损失随训练轮次的变化,展示了概率性 LSTM 模型的学习进展

相对稳定且较低的验证损失表明我们的模型能够很好地泛化,而不会对训练数据过拟合。

它是如何工作的……

概率 LSTM 模型超越了传统的点预测模型。与输出单一期望值的点预测不同,该模型预测一个由均值和标准差参数特征化的完整分布。

这种概率方法通过捕捉数据中固有的不确定性,提供了更丰富的表示。分布的均值给出了预测的期望值,而标准差则量化了对预测的信心,表达了围绕均值的预期波动性。

为了训练这个模型,我们使用与点预测模型不同的损失函数。与最小化预测值和实际值之间差异的 MSE 或 MAE 不同,概率 LSTM 使用负对数似然损失函数。这个损失函数,通常称为概率损失,最大化预测分布下观察到数据的似然。

这种概率损失函数特别适用于不确定性估计,因为它直接惩罚预测概率分布与观察值之间的偏差。当预测分布给实际观察值分配较高的概率时,负对数似然值较低,因此损失较低。

使用DeepAR进行概率预测

这次,我们将重点关注DeepAR,一种用于概率预测的最先进方法。我们还将利用neuralforecast框架,示范如何应用DeepAR来完成这个任务。

准备工作

我们将继续使用之前食谱中的相同数据集。

由于我们使用了不同的 Python 包,我们需要更改预处理步骤,将数据转换为合适的格式。现在,每一行对应于给定时间点某个特定时间序列的单个观察值。这与我们在使用符合预测 的预测区间食谱中所做的类似:

def load_and_prepare_data(file_path, time_column, series_column, 
    aggregation_freq):
    """Load the time series data and prepare it for modeling."""
    dataset = pd.read_csv(file_path, parse_dates=[time_column])
    dataset.set_index(time_column, inplace=True)
    target_series = (
        dataset[series_column].resample(aggregation_freq).mean()
    )
    return target_series
def add_time_features(dataframe, date_column):
    """Add time-related features to the DataFrame."""
    dataframe["week_of_year"] = (
        dataframe[date_column].dt.isocalendar().week.astype(float)
    )
    dataframe["month"] = dataframe[date_column].dt.month.astype(float)
    dataframe["sin_week"] = np.sin(
        2 * np.pi * dataframe["week_of_year"] / 52
    )
    dataframe["cos_week"] = np.cos(
        2 * np.pi * dataframe["week_of_year"] / 52
    )
    dataframe["sin_2week"] = np.sin(
        4 * np.pi * dataframe["week_of_year"] / 52
    )
    dataframe["cos_2week"] = np.cos(
        4 * np.pi * dataframe["week_of_year"] / 52
    )
    dataframe["sin_month"] = np.sin(
        2 * np.pi * dataframe["month"] / 12
    )
    dataframe["cos_month"] = np.cos(
        2 * np.pi * dataframe["month"] / 12
    )
    return dataframe
def scale_features(dataframe, feature_columns):
    """Scale features."""
    scaler = MinMaxScaler()
    dataframe[feature_columns] = (
        scaler.fit_transform(dataframe[feature_columns])
    )
    return dataframe, scaler
FILE_PATH = "assets/daily_multivariate_timeseries.csv"
TIME_COLUMN = "datetime"
TARGET_COLUMN = "Incoming Solar"
AGGREGATION_FREQ = "W"
weekly_data = load_and_prepare_data(
    FILE_PATH, TIME_COLUMN, TARGET_COLUMN, AGGREGATION_FREQ
)
weekly_data = (
    weekly_data.reset_index().rename(columns={TARGET_COLUMN: "y"})
)
weekly_data = add_time_features(weekly_data, TIME_COLUMN)
numerical_features = [
    "y",
    "week_of_year",
    "sin_week",
    "cos_week",
    "sin_2week",
    "cos_2week",
    "sin_month",
    "cos_month",
]
features_to_scale = ["y", "week_of_year"]
weekly_data, scaler = scale_features(weekly_data, features_to_scale)

在这种情况下,我们选择数据集中的目标序列,并将其重新采样为每周频率,使用均值聚合数据点。

接下来,我们展示如何通过添加与时间相关的特征来增强数据集。我们引入了与年份的周和月相关的傅里叶级数成分。通过结合正弦和余弦变换,我们捕捉了数据中的时间周期性。此外,我们使用MinMaxScaler对目标进行缩放。

最后,我们将数据集分为训练集和测试集:

def split_data(dataframe, date_column, split_time):
    """Split the data into training and test sets."""
    train = dataframe[dataframe[date_column] <= split_time]
    test = dataframe[dataframe[date_column] > split_time]
    return train, test
SPLIT_TIME = weekly_data["ds"].max() - pd.Timedelta(weeks=52)
train, test = split_data(weekly_data, "ds", SPLIT_TIME)

现在,让我们看看如何使用neuralforecast构建一个DeepAR模型。

如何实现……

数据准备好后,我们可以定义并训练DeepAR模型。NeuralForecast类接受一个模型列表作为输入。在这个例子中,我们只定义了DeepAR类。该库提供了一种简便的方式来指定模型的架构和训练行为。训练后,我们使用predict()方法生成预测:

nf = NeuralForecast(
    models=[
        DeepAR(
            h=52,
            input_size=52,
            lstm_n_layers=3,
            lstm_hidden_size=128,
            trajectory_samples=100,
            loss=DistributionLoss(
                distribution="Normal", level=[80, 90], 
                    return_params=False
            ),
            futr_exog_list=[
                "week_of_year",
                "sin_week",
                "cos_week",
                "sin_2week",
                "cos_2week",
                "sin_month",
                "cos_month",
            ],
            learning_rate=0.001,
            max_steps=1000,
            val_check_steps=10,
            start_padding_enabled=True,
            early_stop_patience_steps=30,
            scaler_type="identity",
            enable_progress_bar=True,
        ),
    ],
    freq="W",
)
nf.fit(df=train, val_size=52)
Y_hat_df = nf.predict(
    futr_df=test[
        [
            "ds",
            "unique_id",
            "week_of_year",
            "sin_week",
            "cos_week",
            "sin_2week",
            "cos_2week",
            "sin_month",
            "cos_month",
        ]
    ]
)

下图展示了由DeepAR生成的概率预测:

图 7.6:DeepAR 概率预测,显示均值预测和相关的不确定性

图 7.6:DeepAR 概率预测,显示均值预测和相关的不确定性

实线代表均值预测,而阴影区域显示了 80% 和 95% 置信区间的不确定性范围。该图展示了可能的未来值范围,相比单一预测值,它为不确定性下的决策提供了更多信息。

它是如何工作的……

DeepAR 是一种概率预测方法,它为每个未来时间点生成一个概率分布,如正态分布或负二项分布。再次强调,我们关注的是捕捉预测中的不确定性,而不仅仅是生成点预测。

DeepAR 模型使用自回归递归网络结构,并基于过去的观察值、协变量以及时间序列的嵌入进行条件化。输出是一组参数,通常是均值和方差,定义了未来值的分布。在训练过程中,模型最大化在给定这些参数下观测数据的似然。

DeepAR 旨在处理多个相关的时间序列,使其能够学习跨相似序列的复杂模式,并通过利用跨序列信息提高预测准确性。

高斯过程简介

在本节中,我们将介绍高斯过程GP),一种强大的概率性机器学习算法。

准备工作

高斯过程(GP)提供了一种灵活的、概率性的机器学习建模方法。本节介绍了高斯过程的概念,并为使用 GP 模型进行预测做好了必要的环境准备。

我们需要导入一个新库来拟合 GP,即 gpytorch

import torch
import gpytorch
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from pytorch_lightning import LightningDataModule

然后,我们必须读取多元时间序列数据并进行处理,缩放特征和目标变量,因为缩放后的数据通常能显著提高 GP 建模的性能。

如何操作……

我们将使用 gpytorch 库来实现 GP 模型:

  1. GP 模型中的关键组件是均值和协方差函数,它们在 GPModel 类中定义:

    class GPModel(gpytorch.models.ExactGP):
        def init(self, train_x, train_y, likelihood):
            super(GPModel, self).init(train_x, train_y, likelihood)
            self.mean_module = gpytorch.means.ConstantMean()
            self.covar_module = gpytorch.kernels.ScaleKernel(gpytorch.kernels.RBFKernel()) + gpytorch.kernels.ScaleKernel(gpytorch.kernels.PeriodicKernel())
        def forward(self, x):
            mean_x = self.mean_module(x)
            covar_x = self.covar_module(x)
            return gpytorch.distributions.MultivariateNormal(
                mean_x, covar_x)
    
  2. 然后使用标准的 PyTorch 训练循环对模型进行训练,优化边际对数似然:

    likelihood = GaussianLikelihood()
    model = GPModel(datamodule.train_x[:, 0], datamodule.train_y, 
        likelihood)
    model.train()
    likelihood.train()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
    mll = ExactMarginalLogLikelihood(likelihood, model)
    training_iter = 100
    for i in range(training_iter):
        optimizer.zero_grad()
        output = model(datamodule.train_x[:, 0])
        loss = -mll(output, datamodule.train_y)
        loss.backward()
        optimizer.step()
    

    训练后,GP 模型可以对新的数据点进行预测。GP 的主要优势在于它能够量化这些预测中的不确定性。以下代码片段演示了如何使用训练好的 GP 模型进行预测:

    with torch.no_grad(), gpytorch.settings.fast_pred_var():
        observed_pred = likelihood
            (model(datamodule.original_x[:, 0]))
    

    这里是模型拟合值的可视化:

图 7.7:GP 模型预测的可视化

图 7.7:GP 模型预测的可视化

上面的图示展示了 GP 模型如何拟合历史数据,并以量化的不确定性预测未来值。预测值周围的阴影区域直观地表示了模型的预测置信度,间隔越宽,表示不确定性越大。

工作原理…

GP 提供了一种复杂的方式来分析和理解复杂的数据集。它与典型的模型不同,因为它不依赖固定数量的参数来描述数据。相反,GP 使用无限可能的函数范围,这使得它能非常灵活地适应任何类型的数据生成过程。

GP 是一组随机变量。这些变量的关键特性在于一个变量会影响或与其他变量的值相关联。它们之间的依赖关系由高斯分布决定。GP 可以建模多种不同类型的函数(例如,非线性、噪声等)。这种方法在我们当前的应用场景中尤其有用,因为它采用了概率方法进行建模。GP 不仅可以预测最可能的结果,还能量化这些预测的“不确定性”。

GP 由均值和核函数定义:

  • 均值函数:函数值的基准期望值。它为预测提供了起始点,通常设置为零。

  • 核函数:GP 的核心,它通过编码函数的属性(例如,平滑性、周期性等)来确定数据点之间的关系。它影响着如何通过评估数据点之间的相似性来进行预测。

核函数是 GP 模型预测准确性的关键组成部分,它将模型适应于数据的潜在结构。在实际应用中,可以混合不同的核函数来捕捉数据的各种特征。例如,将适用于平滑数据的核与适用于周期性数据的核结合,可以帮助建模同时具有这两种特征的数据。

训练一个 GP 涉及微调核函数中的特定参数,以使其最好地拟合你的数据。通常使用优化技术,如梯度下降,来完成这一过程。一旦 GP 训练完成,它就可以预测新的数据点。

使用 Prophet 进行概率预测

在本教程中,我们将展示如何使用 Prophet 进行概率预测。

准备工作

Prophet 是 Facebook 开发的一个工具,用于时间序列数据的预测。它特别擅长处理具有强季节性模式和不规则事件(如节假日)的数据。要开始使用 Prophet,我们需要准备好数据和环境。

过程从加载和预处理时间序列数据开始,使其符合 Prophet 所要求的格式。Prophet 中的每个时间序列必须有两列——ds(时间戳)和 y(我们希望预测的值):

import pandas as pd
from prophet import Prophet
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
mvtseries = pd.read_csv(
"assets/daily_multivariate_timeseries.csv",
parse_dates=["datetime"],
)
mvtseries['ds'] = mvtseries['datetime']
mvtseries['y'] = mvtseries['Incoming Solar']

现在,让我们来看一下如何构建 Prophet 模型。

操作步骤…

按照以下步骤构建 Prophet 模型:

  1. 经过预处理后,我们必须将数据集分为训练集和测试集。然后,使用 Prophet 对训练数据进行拟合:

    train_data, test_data = train_test_split(mvtseries, 
        test_size=0.2, shuffle=False)
    
  2. 然后,我们必须创建一个 Prophet 实例并进行训练,如下所示:

    model = Prophet()
    model.fit(train_data[['ds', 'y']])
    
  3. 一旦模型训练完成,我们可以使用该模型进行未来预测。这涉及到创建一个用于预测期的未来 dataframe,然后使用模型进行值的预测:

    future = model.make_future_dataframe(periods=len(test_data))
    forecast = model.predict(future)
    
  4. 这是如何可视化预测的:

    fig = model.plot(forecast)
    plt.show()
    

    这些预测结果展示在下图中:

图 7.8:带有不确定性区间和观测数据的 Prophet 预测

图 7.8:带有不确定性区间和观测数据的 Prophet 预测

除了基本的预测模型外,Prophet 还提供了功能来分解和理解时间序列的各个组件。这对于深入了解数据的潜在模式特别有用。以下是如何按各个组件可视化预测结果:

fig = model.plot_components(forecast)
plt.show()

这是该图:

图 7.9:展示趋势以及每周和每年季节性的 Prophet 模型组件

图 7.9:展示趋势以及每周和每年季节性的 Prophet 模型组件

顶部图展示了数据随时间的总体趋势。在这里,我们可以看到一个大致的上升趋势,表明我们预测的值在逐年增加。中间的图展示了每周季节性。这张图表明了一周中的每一天如何影响我们的预测值。例如,某些特定的日子可能会因每周事件或习惯而出现峰值。底部图表示了每年季节性,展示了一年中的不同时间如何影响预测。这可能会捕捉到某些月份或季节性效应的活动增加。

它是如何工作的……

Prophet 是一个加性模型,将时间序列分解为多个组件:趋势、季节性和假期。在本食谱中,我们使用了模型的默认参数。然而,你可以通过多个参数来调整 Prophet。更多信息,请查看文档 facebook.github.io/prophet/docs/quick_start.html

prophet 库要求数据采用特定格式:一个包含两列的 pandas DataFrame:ds(时间戳)和 y(值)。然后,通过使用 fit() 方法和 predict() 方法分别进行训练和推断。你还可以按组件可视化预测结果,这使得模型更具可解释性。

还有更多……

Prophet 模型有一个更新版叫做 NeuralProphet。它结合了神经网络模型,以改善预测,特别是在复杂模式和多季节性场景下。

第八章:深度学习在时间序列分类中的应用

在本章中,我们将使用深度学习解决 时间序列分类 (TSC) 问题。顾名思义,TSC 是一个涉及时间序列数据的分类任务。数据集包含多个时间序列,每个序列都有一个相关的分类标签。这个问题类似于标准的分类任务,但输入的解释变量是时间序列。我们将探索如何使用不同的方法来处理这个问题。除了使用 K-最近邻 模型来处理这个任务外,我们还将开发不同的神经网络,比如 残差神经网络 (ResNet) 和卷积神经网络。

在本章结束时,你将能够使用 PyTorch Lightning 数据模块设置 TSC 任务,并用不同的模型解决它。你还将学习如何使用 sktime Python 库来解决这个问题。

本章包含以下食谱:

  • 使用 K-最近邻解决 TSC 问题

  • 为 TSC 构建一个 DataModule

  • 时间序列分类的卷积神经网络

  • 使用 ResNet 解决 TSC 问题

  • 使用sktime解决时间序列分类问题

技术要求

我们将专注于 PyTorch Lightning 生态系统来构建深度学习模型。此外,我们还将使用 scikit-learn 来创建基准模型。总体而言,本包中使用的库列表如下:

  • scikit-learn (1.3.2)

  • pandas (2.1.3)

  • NumPy (1.26.2)

  • Torch (2.1.1)

  • PyTorch Lightning (2.1.2)

  • sktime (0.24.1)

  • keras-self-attention (0.51.0)

作为示例,我们将使用以下链接中提供的Car数据集:www.timeseriesclassification.com。你可以在以下文献中了解更多关于该数据集的信息:

Thakoor, Ninad, 和 Jean Gao. 基于广义概率下降方法和隐马尔可夫描述符的形状分类器。第十届 IEEE 计算机视觉国际会议 (ICCV’05) 第 1 卷。IEEE,2005 年。

本章中使用的代码和数据集可以在以下 GitHub 链接中找到:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

使用 K-最近邻解决 TSC 问题

在这个食谱中,我们将展示如何使用一种叫做 K-最近邻的流行方法来处理 TSC 任务。本食谱的目标是向你展示如何使用标准的机器学习模型来解决这个问题。

准备工作

首先,让我们使用 pandas 来加载数据:

import pandas as pd
data_directory = 'assets/datasets/Car'
train = pd.read_table(f'{data_directory}/Car_TRAIN.tsv', header=None)
test = pd.read_table(f'{data_directory}/Car_TEST.tsv', header=None)

数据集已经分为训练集和测试集,因此我们会分别读取它们。现在,让我们看看如何使用这个数据集构建一个 K-最近邻模型。

如何实现……

在这里,我们描述了使用 scikit-learn 构建时间序列分类器所需的步骤:

  1. 首先,我们将目标变量从解释变量中分离出来:

    y_train = train.iloc[:, 0]
    y_test = test.iloc[:, 0]
    X_train = train.iloc[:, 1:]
    X_test = test.iloc[:, 1:]
    

    每个数据集的第一列(索引 0)包含目标变量,我们将其分配给 y_trainy_test 对象,用于训练集和测试集。X_trainX_test 对象包含相应数据集的输入解释时间序列。

    这个特定的数据集包含四个不同的类别。以下是它们的分布情况:

图 8.1: 数据集中四个类别的分布

图 8.1: 数据集中四个类别的分布

  1. 接下来,我们需要对时间序列进行标准化。我们使用 scikit-learn 中的 MinMaxScaler 方法来实现这一点,它将所有值压缩到 01 之间:

    from sklearn.preprocessing import MinMaxScaler
    scaler = MinMaxScaler()
    X_train = scaler.fit_transform(X_train)
    X_test = scaler.transform(X_test)
    

    在前面的代码中,我们使用训练集拟合标准化器,然后用它来转换两个数据集中的数据。

  2. 最后,我们准备创建一个 K-最近邻分类模型:

    classifier = KNeighborsTimeSeriesClassifier()
    classifier.fit(X_train, y_train)
    predictions = classifier.predict(X_test)
    

    在前面的代码中,我们创建了一个 KNeighborsTimeSeriesClassifier 实例,它实现了 K-最近邻算法,并使用训练集对其进行拟合。然后,我们通过调用 predict() 方法将此模型应用于测试集。

    以下图显示了 K-最近邻模型预测的混淆矩阵:

图 8.2: K-最近邻模型预测的混淆矩阵

图 8.2: K-最近邻模型预测的混淆矩阵

它是如何工作的……

时间序列分类(TSC)问题类似于标准分类任务,其中输入的解释变量是时间序列。因此,解决该问题的过程类似。在将解释变量(X)与目标变量(y)分开后,我们使用标准化函数等操作符对解释变量进行预处理。然后,我们可以使用任何分类器来解决这个任务。在本节中,我们使用了 K-最近邻模型,这是一种简单而有效的方法。

请注意,使用 MinMaxScaler 进行标准化步骤是非常重要的,能够将所有观测值映射到一个共同的值范围内。

还有更多内容……

sktime Python 库提供了多种解决 TSC 问题的方法。这里是文档的链接:www.sktime.net/en/stable/examples/02_classification.html

在本节中,我们使用了默认参数的 K-最近邻模型。例如,我们使用了明科夫斯基度量(Minkowski metric),但这可能不是最好的选择。对于时间序列,像动态时间规整(dynamic time warping)这样的距离度量通常是更好的选择。

为 TSC 构建 DataModule 类

在本节中,我们将回到 PyTorch Lightning 框架。我们将构建一个 DataModule 类来封装数据预处理和将观测值传递给模型的过程。

准备就绪

让我们从前面的食谱中加载数据集:

import pandas as pd
data_directory = 'assets/datasets/Car'
train = pd.read_table(f'{data_directory}/Car_TRAIN.tsv', header=None)
test = pd.read_table(f'{data_directory}/Car_TEST.tsv', header=None)

接下来,我们将构建一个 DataModule 类来处理这个数据集。

如何操作……

在前面的章节中,我们使用了 PyTorch Forecasting 中的TimeSeriesDataSet来处理数据准备工作。这个类管理了多个步骤,包括数据的归一化和转化,供监督学习使用。然而,在 TSC 中,一个观察值使用整个时间序列作为输入:

  1. 我们将开始创建一个更简化的TimeSeriesDataSet变体,以便将观察值传递给模型:

    from torch.utils.data import Dataset
    class TSCDataset(Dataset):
        def __init__(self, X_data, y_data):
            self.X_data = X_data
            self.y_data = y_data
        def __getitem__(self, index):
            return self.X_data[index], self.y_data[index]
        def __len__(self):
            return len(self.X_data)
    

    __getitem__``()方法在内部用于从数据集中获取观察值并将其传递给模型,而__len__``()方法则输出数据集的大小。

  2. 然后,我们准备好构建LightningDataModule类了。这里是构造函数:

    from torch.utils.data import Dataset, DataLoader
    import lightning.pytorch as pl
    from sklearn.preprocessing import MinMaxScaler, OneHotEncoder
    class TSCDataModule(pl.LightningDataModule):
        def __init__(self, train_df, test_df, batch_size=1):
            super().__init__()
            self.train_df = train_df
            self.test_df = test_df
            self.batch_size = batch_size
            self.scaler = MinMaxScaler()
            self.encoder = OneHotEncoder(categories='auto',
                sparse_output=False)
            self.train = None
            self.validation = None
            self.test = None
    

    TSCDataModule包含了self.trainself.validationself.test数据集属性,这些属性将在setup``()方法中填充。此外,构造函数还会基于MinMaxScaler设置归一化方法,并使用名为OneHotEncoder的独热编码器。我们使用独热编码器将目标变量转换为一组二元变量。这一过程是训练神经网络所必需的。

  3. 然后,setup``()方法的实现如下:

    def setup(self, stage=None):
        y_train = self.encoder.fit_transform(
            self.train_df.iloc[:, 0].values.reshape(-1, 1)
        )
        y_test = self.encoder.transform(
            self.test_df.iloc[:,0].values.reshape(-1, 1))
        X_train = train.iloc[:, 1:]
        X_test = test.iloc[:, 1:]
        X_train = self.scaler.fit_transform(X_train)
        X_test = self.scaler.transform(X_test)
        X_train, X_val, y_train, y_val = train_test_split(
            X_train, y_train, test_size=0.2, stratify=y_train
        )
        X_train, X_val, X_test = [
            torch.tensor(arr, dtype=torch.float).unsqueeze(1)
            for arr in [X_train, X_val, X_test]
        ]
        y_train, y_val, y_test = [
            torch.tensor(arr, dtype=torch.long)
            for arr in [y_train, y_val, y_test]
        ]
        self.train = TSCDataset(X_train, y_train)
        self.validation = TSCDataset(X_val, y_val)
        self.test = TSCDataset(X_test, y_test)
    

    在前面的代码中,我们使用编码器转换目标变量,并使用归一化方法对解释变量进行相同的转换。然后,我们基于训练实例创建验证集。我们还使用torch.tensor()方法将数据对象转换为 torch 数据结构。最后,我们基于训练、验证和测试集创建TSCDataset实例。

  4. 我们直接在相应数据集上使用DataLoader类创建数据加载方法:

        def train_dataloader(self):
            return DataLoader(self.train, 
                batch_size=self.batch_size)
        def val_dataloader(self):
            return DataLoader(self.validation, 
                batch_size=self.batch_size)
        def test_dataloader(self):
            return DataLoader(self.test, batch_size=self.batch_size)
    
  5. 最后,这里是如何使用这个数据模块获取一个观察值的示例:

    datamodule = TSCDataModule(train_df=train, test_df=test)
    datamodule.setup()
    x, y = next(iter(datamodule.train_dataloader()))
    

在下一个教程中,我们将学习如何使用这个数据模块构建一个分类模型。

它是如何工作的…

我们创建了一个针对 TSC 分类问题定制的DataModule类。我们将数据逻辑封装在setup``()方法中,从而使我们能够使用 PyTorch Lightning 生态系统来构建深度学习模型。

在这种情况下,TSC 问题不涉及自回归。因此,我们创建了一个简单变体的TimeSeriesDataSet类,用于处理将数据传递给模型的过程。

用于 TSC 的卷积神经网络

在这个教程中,我们将带你构建一个卷积神经网络来解决 TSC 问题。我们将使用前面教程中创建的DataModule类来实现这一目标。

准备工作

我们首先再次导入前面教程中使用的数据集:

import pandas as pd
data_directory = 'assets/datasets/Car'
train = pd.read_table(f'{data_directory}/Car_TRAIN.tsv', header=None)
test = pd.read_table(f'{data_directory}/Car_TEST.tsv', header=None)
datamodule = TSCDataModule(train_df=train,
                           test_df=test,
                           batch_size=8)

我们还创建了一个TSCDataModule数据模块的实例,它是在前面的教程中定义的。让我们看看如何创建一个卷积神经网络分类器来处理这个任务。

如何做到这一点…

在这里,我们将通过以下步骤为 TSC 问题构建一个卷积神经网络,使用 PyTorch 实现:

  1. 我们先从基于 PyTorch 创建神经网络开始:

    from torch import nn
    class ConvolutionalTSC(nn.Module):
        def __init__(self, input_dim, output_dim=1):
            super(ConvolutionalTSC, self).__init__()
            self.conv1 = nn.Conv1d(in_channels=input_dim,
                                   out_channels=64,
                                   kernel_size=3,
                                   stride=1,
                                   padding=1)
            self.conv2 = nn.Conv1d(in_channels=64,
                                   out_channels=32,
                                   kernel_size=3,
                                   stride=1,
                                   padding=1)
            self.conv3 = nn.Conv1d(in_channels=32,
                                   out_channels=16,
                                   kernel_size=3,
                                   stride=1,
                                   padding=1)
            self.maxp = nn.MaxPool1d(kernel_size=3)
            self.fc1 = nn.Linear(in_features=336, out_features=32)
            self.fc2 = nn.Linear(in_features=32, 
                out_features=output_dim)
        def forward(self, x):
            x = F.relu(self.conv1(x))
            x = self.maxp(x)
            x = F.relu(self.conv2(x))
            x = self.maxp(x)
            x = F.relu(self.conv3(x))
            x = self.maxp(x)
            x = x.view(x.size(0), -1)
            x = self.fc1(x)
            x = self.fc2(x)
            return x
    
  2. 然后,我们将该模型封装在一个来自 PyTorch Lightning 的 LightningModuleTSCCnnModel 中:

    import torch.nn.functional as F
    import lightning.pytorch as pl
    class TSCCnnModel(pl.LightningModule):
        def __init__(self, output_dim):
            super().__init__()
            self.network = ConvolutionalTSC(
                input_dim=1,
                output_dim=output_dim,
            )
        def forward(self, x):
            x = x.type(torch.FloatTensor)
            return self.network(x)
    

    该模块还包含了常见的训练、验证和测试步骤,具体实现如下:

        def training_step(self, batch, batch_idx):
            x, y = batch
            y_pred = self.forward(x)
            loss = F.cross_entropy(y_pred, 
                y.type(torch.FloatTensor))
            self.log('train_loss', loss)
            return loss
        def validation_step(self, batch, batch_idx):
            x, y = batch
            y_pred = self(x)
            loss = F.cross_entropy(y_pred, 
                y.type(torch.FloatTensor))
            self.log('val_loss', loss)
            return loss
        def test_step(self, batch, batch_idx):
            x, y = batch
            y_pred = self(x)
            loss = F.cross_entropy(y_pred, 
                y.type(torch.FloatTensor))
            self.log('test_loss', loss)
        def configure_optimizers(self):
            return torch.optim.Adam(self.parameters(), lr=0.01)
    

    这些步骤与为预测问题开发的步骤类似,但在这种情况下,我们使用交叉熵作为 loss() 函数。

  3. 现在我们已经准备好训练模型,接下来我们通过 Trainer 实例进行训练,如下所示:

    import lightning.pytorch as pl
    from lightning.pytorch.callbacks import EarlyStopping
    model = TSCCnnModel(output_dim=4)
    early_stop_callback = EarlyStopping(monitor="val_loss",
        min_delta=1e-4,
        patience=10,
        verbose=False,
        mode="min")
    trainer = pl.Trainer(
        max_epochs=30,
        accelerator='cpu',
        log_every_n_steps=2,
        enable_model_summary=True,
        callbacks=[early_stop_callback],
    )
    trainer.fit(model, datamodule)
    

    在前面的代码中,我们在创建 TSCCnnModel 类的实例时,将输出维度设置为 4,这代表了数据集中的类别数。我们还设置了一个提前停止回调,用以推动网络的训练过程。

它是如何工作的……

卷积神经网络已成功应用于时间序列分类(TSC)问题。在本示例中,我们探索了基于 PyTorch Lightning 的 LightningModule 开发分类器。我们创建了一个扩展自 nn.Module 类的 ConvolutionalTSC 类。在该类的构造函数中,我们定义了网络的层次:三个卷积层(conv1conv2conv3),以及两个全连接层(fc1fc2)。forward() 方法详细说明了这些层是如何组合在一起的。然后,卷积层依次堆叠,并在每个卷积后应用最大池化操作(MaxPool1d)。最后,我们堆叠了两个全连接层,其中最后一层是输出层。

下图显示了卷积神经网络的混淆矩阵:

图 8.3:卷积神经网络的混淆矩阵

图 8.3:卷积神经网络的混淆矩阵

使用神经网络得到的结果优于使用 K 最近邻(K-NN)模型的结果。

该模型的工作流与基于 PyTorch Lightning 的其他示例遵循相同的逻辑。需要注意的主要区别是我们正在处理分类问题。因此,我们需要设置一个处理该问题的损失函数。交叉熵是训练神经网络用于分类任务时通常使用的函数。神经网络的输出维度也根据类别的数量进行设置。基本上,每个数据集中的类别都会有一个输出单元。

用于 TSC 的 ResNet

本示例演示了如何训练一个用于 TSC 任务的 ResNet。ResNet 是一种广泛用于计算机视觉问题(如图像分类或目标检测)的深度神经网络架构。在这里,你将学习如何将其应用于时间序列数据建模。

准备工作

我们将继续使用与前一个示例中相同的数据集和数据模块:

import pandas as pd
data_directory = 'assets/datasets/Car'
train = pd.read_table(f'{data_directory}/Car_TRAIN.tsv', header=None)
test = pd.read_table(f'{data_directory}/Car_TEST.tsv', header=None)
datamodule = TSCDataModule(train_df=train,
    test_df=test,
    batch_size=8)

让我们看看如何使用 PyTorch Lightning 构建并训练 ResNet。

如何做…

本节中,我们描述了为 TSC 任务创建 ResNet 的过程:

  1. 首先,使用 torch 库中的 nn.Module 创建一个 ResNet:

    class ResidualNeuralNetworkModel(nn.Module):
        def __init__(self,
                     in_channels: int,
                     out_channels: int = 64,
                     num_classes: int = 1):
            super().__init__()
            self.input_args = {
                'in_channels': in_channels,
                'num_classes': num_classes
            }
            self.layers = nn.Sequential(*[
                ResNNBlock(in_channels=in_channels,
                    out_channels=out_channels),
                ResNNBlock(in_channels=out_channels,
                    out_channels=out_channels * 2),
                ResNNBlock(in_channels=out_channels * 2,
                    out_channels=out_channels * 2),
            ])
            self.fc = nn.Linear(mid_channels * 2, num_classes)
        def forward(self, x):
            x = self.layers(x)
            return self.fc(x.mean(dim=-1))
    

    然后,我们将 ResidualNeuralNetworkModel 封装在 pl.LightningModule 中:

    class TSCResNet(pl.LightningModule):
        def __init__(self, output_dim):
            super().__init__()
            self.resnet = \
                ResidualNeuralNetworkModel(in_channels=1,
                    num_pred_classes=output_dim)
        def forward(self, x):
            out = self.resnet.forward(x)
            return out
    
  2. 训练、验证和测试步骤的实现与前面食谱中的实现完全相同:

        def training_step(self, batch, batch_idx):
            x, y = batch
            x = x.type(torch.FloatTensor)
            y = y.type(torch.FloatTensor)
            y_pred = self.forward(x)
            loss = F.cross_entropy(y_pred, y)
            self.log('train_loss', loss)
            return loss
        def validation_step(self, batch, batch_idx):
            x, y = batch
            x = x.type(torch.FloatTensor)
            y = y.type(torch.FloatTensor)
            y_pred = self(x)
            loss = F.cross_entropy(y_pred, y)
            self.log('val_loss', loss)
            return loss
        def test_step(self, batch, batch_idx):
            x, y = batch
            x = x.type(torch.FloatTensor)
            y = y.type(torch.FloatTensor)
            y_pred = self(x)
            loss = F.cross_entropy(y_pred, y)
            acc = Accuracy(task='multiclass', num_classes=4)
            acc_score = acc(y_pred, y)
            self.log('acc_score', acc_score)
            self.log('test_loss', loss)
        def configure_optimizers(self):
            return torch.optim.Adam(self.parameters(), lr=0.01)
    

    请注意,我们将预测值和实际值转换为 torch.FloatTensor 结构,以计算 loss () 函数。我们将交叉熵设置为 loss () 函数,这是分类问题中常用的函数。在测试阶段,我们还评估模型的准确性。

  3. 最后,这是基于 PyTorch Lightning 的 Trainer 进行模型训练和测试的工作流程:

    model = TSCResNet(output_dim=4)
    datamodule = TSCDataModule(train_df=train, test_df=test, 
        batch_size=8)
    early_stop_callback = EarlyStopping(monitor="val_loss",
        min_delta=1e-4,
        patience=20,
        verbose=False,
        mode="min")
    trainer = pl.Trainer(
        max_epochs=100,
        accelerator='cpu',
        log_every_n_steps=2,
        enable_model_summary=True,
        callbacks=[early_stop_callback],
    )
    trainer.fit(model, datamodule)
    trainer.test(model, datamodule)
    

    本质上,在定义模型之后,使用 PyTorch Lightning 的 Trainer 进行训练和测试的过程与前面食谱中展示的类似。

它是如何工作的……

ResNet 在 TSC 问题中表现出了良好的效果。其思想是学习原始输入和通过卷积层得到的转换输出之间的差异(残差)。在前面的代码中,我们创建了一个神经网络,该网络以三个参数作为输入:

  • in_channels:输入通道的数量,由于我们的时间序列是单变量的,因此等于 1。

  • out_channels:每个残差块的输出通道数量

  • num_classes:数据集中类别的数量

神经网络的层由三个名为 ResNNBlock 的残差块组成。残差块是 ResNet 的基石,旨在解决梯度消失问题。你可以在以下网址查看名为 ResNNBlock 的残差块实现:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook。残差块的输出传递给一个线性层。

下图展示了 ResNet 的混淆矩阵:

图 8.4:ResNet 的混淆矩阵

图 8.4:ResNet 的混淆矩阵

与前一个食谱类似,我们将 ResNet 的实现与 PyTorch Lightning 框架结合。在此食谱的测试阶段,我们还包含了准确率度量,告诉我们模型正确预测的案例百分比。这个度量通常用于 TSC 问题,尽管对于目标分布不平衡的数据集,它可能不是很有意义。

使用 sktime 解决 TSC 问题

在这个食谱中,我们探索了 PyTorch 的替代方法——sktimesktime 是一个专门用于时间序列建模的 Python 库,其中包括几个用于 TSC 的神经网络模型。

准备工作

你可以使用 pip 安装 sktime。你还需要安装 keras-self-attention 库,其中包含运行 sktime 中某些方法所需的自注意力方法:

pip install 'sktime[dl]'
pip install keras-self-attention

在安装 sktime 时,方括号中的 dl 标签表示你希望包括该库中提供的可选深度学习模型。

在这个食谱中,我们将使用 sktime 中提供的一个示例数据集。我们将在下一节加载它。

如何操作……

正如其名称所示,sktime库遵循类似于 scikit-learn 的设计模式。因此,我们使用sktime构建深度学习模型的方法将类似于使用 K 近邻解决 TSC 问题方案中描述的工作流程。

让我们从加载数据集开始:

from sktime.datasets import load_italy_power_demand
X_train, y_train = \
    load_italy_power_demand(split="train", return_type="numpy3D")
X_test, y_test = load_italy_power_demand(split="test",
    return_type="numpy3D")

在前面的代码中,我们加载了关于意大利能源需求的数据集。你可以查看以下链接以获取更多关于此数据集的信息:www.timeseriesclassification.com/description.php?Dataset=ItalyPowerDemand

数据最初用于以下工作:

Keogh, Eamonn 等人。智能图标:将轻量级数据挖掘与可视化集成到 GUI 操作系统中。第六届国际数据挖掘会议ICDM'06)。IEEE,2006 年。

我们使用load_italy_power_demand来加载训练集和测试集,作为numpy数据结构。

现在,让我们看看如何使用这个数据集构建不同类型的神经网络。

全连接神经网络

我们从训练一个全连接神经网络开始。该网络的配置,包括loss``()activation``()函数,作为参数传递给FCNClassifier实例:

from sktime.classification.deep_learning.fcn import FCNClassifier
fcn = FCNClassifier(n_epochs=200,
    loss='categorical_crossentropy',
    activation='sigmoid',
    batch_size=4)
fcn.fit(X_train, y_train)
fcn_pred = fcn.predict(X_test)

训练和推断步骤分别使用fit``()predict``()方法完成。如果你熟悉 scikit-learn 的方法,这种方式应该对你来说很熟悉。

卷积神经网络

正如我们在卷积神经网络用于 TSC方案中学到的,卷积模型可以是分类时间序列的有效方法。这里是sktime中使用CNNClassifier的实现:

from sktime.classification.deep_learning.cnn import CNNClassifier
cnn = CNNClassifier(n_epochs=200,
    loss='categorical_crossentropy',
    activation='sigmoid',
    kernel_size=7,
    batch_size=4)
cnn.fit(X_train, y_train)
cnn_pred = cnn.predict(X_test)

你还可以设置与卷积相关的其他参数,如avg_pool_sizen_conv_layers。查看文档以获取完整的参数列表,链接如下:www.sktime.net/en/stable/api_reference/auto_generated/sktime.classification.deep_learning.CNNClassifier.html

LSTM-FCN 神经网络

循环神经网络对这个问题也很有用。这里是一个将 LSTM 与全连接层结合的模型,能够在LSTMFCNClassifier中使用:

from sktime.classification.deep_learning.lstmfcn import( 
    LSTMFCNClassifier)
lstmfcn = LSTMFCNClassifier(n_epochs=200,
                            attention=True,
                            batch_size=4)
lstmfcn.fit(X_train, y_train)
lstmfcn_pred = lstmfcn.predict(X_test)

该方法还包含一个注意力机制,显著提高了分类准确性。

TapNet 模型

sktime

from sktime.classification.deep_learning.tapnet import(
    TapNetClassifier)
tapnet = TapNetClassifier(n_epochs=200,
                          loss='categorical_crossentropy',
                          batch_size=4)
tapnet.fit(X_train, y_train)
tapnet_pred = tapnet.predict(X_test)

该模型可以处理低维空间(特征数较少),并且在半监督设置下表现良好——即,当有大量未标记的观察数据时。

InceptionTime 模型

InceptionTime是解决 TSC 问题的最先进深度学习方法。实际上,InceptionTime是一个深度卷积神经网络的集成,灵感来自于为计算机视觉任务创建的 Inception 架构:

from sktime.classification.deep_learning import( 
    InceptionTimeClassifier)
inception = InceptionTimeClassifier(n_epochs=200,
        loss='categorical_crossentropy',
        use_residual=True,
        batch_size=4)
inception.fit(X_train, y_train)
inception_pred = inception.predict(X_test)

该模型还包括可选的残差连接,在前面的代码中,我们通过将 use_residual 参数设置为 True 来使用它。

评估

我们可以使用标准的分类指标来评估 TSC 模型的性能。以下是如何计算我们在这个食谱中训练的模型的准确性:

from sklearn.metrics import accuracy_score
perf = {
    'FCN': accuracy_score(y_test, fcn_pred),
    'CNN': accuracy_score(y_test, cnn_pred),
    'InceptionTime': accuracy_score(y_test, inception_pred),
    'TapNet': accuracy_score(y_test, tapnet_pred),
    'LSTMFCN': accuracy_score(y_test, lstmfcn_pred),
}

结果如下面的图所示:

图 8.5:模型的准确性

图 8.5:模型的准确性

总的来说,InceptionTime 似乎是解决这个特定问题的最佳方法。

工作原理……

在这个食谱中,我们使用 sktime Python 库来构建深度学习模型以解决 TSC 问题。虽然你可以像我们在本章的其他食谱中展示的那样使用 PyTorch,sktime 提供了一个广泛的工具包来处理 TSC 任务。由于 sktime 遵循 scikit-learn 的理念,大部分工作是通过相应类的 fitpredict 方法完成的。

还有更多……

你可以查看 sktime 的文档,了解其他模型,包括一些非深度学习的模型。这里是链接:www.sktime.net/en/stable/users.html

在我们使用的这个食谱中的大多数模型中,我们将参数设置为默认值。但是,你可以创建一个验证集并优化模型的配置,以获得更好的性能。

第九章:深度学习在时间序列异常检测中的应用

本章我们将深入探讨使用时间序列数据进行异常检测问题。这个任务涉及到检测与数据集中大部分样本显著不同的稀有观测值。我们将探索不同的方法来解决这个问题,比如基于预测的方法或基于重构的方法。包括使用强大的方法,如自编码器AEs)、变分自编码器VAEs)或生成对抗网络GANs)。

到本章结束时,你将能够使用不同的方法在 Python 中定义时间序列异常检测问题。

本章包含以下内容:

  • 使用自回归积分滑动平均ARIMA)进行时间序列异常检测

  • 基于预测的异常检测使用深度学习DL

  • 使用长短期记忆LSTM)自编码器进行异常检测

  • 使用 PyOD 构建自编码器

  • 为时间序列异常检测创建 VAE

  • 使用 GANs 进行时间序列异常检测

技术要求

本章中开发的模型基于不同的框架。首先,我们展示如何使用 statsforecastneuralforecast 库开发基于预测的方法。其他方法,如 LSTM 自编码器,将使用 PyTorch Lightning 生态系统进行探索。最后,我们还将使用 PyOD 库创建基于 GANs 或 VAEs 等方法的异常检测模型。当然,我们也依赖于典型的数据处理库,如 pandas 或 NumPy。以下列表包含本章所需的所有库:

  • scikit-learn (1.3.2)

  • pandas (2.1.3)

  • NumPy (1.26.2)

  • statsforecast (1.6.0)

  • datasetsforecast (0.08)

  • 0neuralforecast (1.6.4)

  • torch (2.1.1)

  • PyTorch Lightning (2.1.2)

  • PyTorch Forecasting (1.0.0)

  • PyOD (1.1.2)

本章中使用的代码和数据集可以在以下 GitHub URL 中找到:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

使用 ARIMA 进行时间序列异常检测

时间序列异常检测是诸如医疗保健或制造业等应用领域中的一项重要任务。异常检测方法旨在识别不符合数据集典型行为的观测值。在实际操作中,异常可能代表机器故障或欺诈行为等现象。异常检测是机器学习ML)中的一项常见任务,尤其是在涉及时间序列数据时,它有一些专门的方法。这种类型的数据集及其中的模式可能会随时间演变,这使得建模过程和检测器的有效性变得更加复杂。用于时间序列异常检测问题的统计学习方法通常采用基于预测的方法或基于重构的方法。在本食谱中,我们将描述如何使用 ARIMA 方法为单变量时间序列创建一个基于预测的异常检测系统。

准备工作

我们将专注于来自M3数据集的单变量时间序列,该数据集可以在datasetsforecast库中找到。以下是获取此数据的方法:

from datasetsforecast.m3 import M3
dataset, *_ = M3.load('./data', 'Quarterly')
q1 = dataset.query('unique_id=="Q1"')

在前面的代码中,我们首先使用load()方法加载M3数据集。然后,我们使用query()方法获取标识符为Q1的单变量时间序列(unique_id列)。现在,让我们看看如何在这个数据集中检测异常。

如何操作…

我们将构建一个预测模型,并使用相应的预测区间来检测异常。

  1. 我们从创建一个预测模型开始。虽然任何模型都能工作,但在这个例子中,我们专注于 ARIMA。以下是如何使用statsforecast库定义该模型:

    from statsforecast import StatsForecast
    from statsforecast.models import AutoARIMA
    models = [AutoARIMA(season_length=4)]
    sf = StatsForecast(
        df=q1,
        models=models,
        freq='Q',
        n_jobs=1,
    )
    
  2. 现在,我们准备好拟合模型并获取预测:

    forecasts = sf.forecast(h=8, level=[99], 
        fitted=True).reset_index()
    insample_forecasts = sf.forecast_fitted_values().reset_index()
    

    首先,我们使用forecast()方法获取预测结果。在此示例中,我们将预测时间设定为8h=8)。我们还传递了两个额外的参数:level=[99],这意味着我们还希望模型预测具有99%置信度的区间;fitted=True,这告诉模型计算训练预测。我们使用forecast_fitted_values()方法从训练集获取预测结果。

  3. 然后,我们根据预测点是否在模型所做的预测区间内来识别异常。具体操作如下:

    anomalies = insample_forecasts.loc[
        (
            insample_forecasts['y'] >= 
            insample_forecasts['AutoARIMA-hi-99']
        ) | (
            insample_forecasts['y'] <= 
            insample_forecasts['AutoARIMA-lo-99'])]
    

    前面的代码检查训练预测(insample_forecasts['y']对象)是否在99%预测区间内。任何未通过此检查的观测值都被视为异常。

  4. 最后,我们使用StatsForecast类中的plot()方法来绘制异常:

    StatsForecast.plot(insample_forecasts, unique_ids=['Q1'], 
        plot_anomalies=True)
    

    下面是该图的样子:

图 9.1:ARIMA 识别的异常示例

图 9.1:ARIMA 识别的异常示例

其工作原理…

我们使用了statsforecast库中可用的AutoARIMA实现来创建 ARIMA 模型。这种方法会自动选择模型的最佳参数。由于数据是季度数据,我们将季节性长度设置为4。拟合过程通过一个StatsForecast类实例来执行。

基于预测的方法通过将给定模型的预测值与时间序列的实际值进行比较来工作。在这种情况下,我们使用 ARIMA 模型,但也可以使用其他方法。此外,我们还考虑了基于预测区间的方法。具体来说,如果观察值的数值超出了预测区间,就认为该值是异常值。在前面一节中的代码中,我们考虑了 99%置信度的预测区间,但你可以根据你的问题测试不同的值。

还有更多内容……

在本食谱中,我们专注于使用 ARIMA 来获取预测区间,但你也可以使用任何具有此功能的其他模型。

你可以通过以下网址查看更多关于如何使用statsforecast库进行基于预测的异常检测的详细信息:nixtla.github.io/statsforecast/docs/tutorials/anomalydetection.html

基于预测的异常检测方法,使用深度学习(DL)

在本食谱中,我们继续探索基于预测的方法。这一次,我们将创建一个基于深度学习(DL)的预测模型。此外,我们还将使用点预测的误差作为检测异常的参考。

准备工作

我们将使用纽约市出租车行程数的时间序列数据集。这个数据集被认为是时间序列异常检测任务的基准问题。你可以通过以下链接查看源数据:databank.illinois.edu/datasets/IDB-9610843

让我们首先使用pandas加载时间序列:

from datetime import datetime
import pandas as pd
dataset = pd.read_csv('assets/datasets/taxi/taxi_data.csv')
labels = pd.read_csv('assets/datasets/taxi/taxi_labels.csv')
dataset['ds'] = pd.Series([datetime.fromtimestamp(x) 
    for x in dataset['timestamp']])
dataset = dataset.drop('timestamp', axis=1)
dataset['unique_id'] = 'NYT'
dataset = dataset.rename(columns={'value': 'y'})
is_anomaly = []
for i, r in labels.iterrows():
    dt_start = datetime.fromtimestamp(r.start)
    dt_end = datetime.fromtimestamp(r.end)
    anomaly_in_period = [dt_start <= x <= dt_end 
        for x in dataset['ds']]
    is_anomaly.append(anomaly_in_period)
dataset['is_anomaly']=pd.DataFrame(is_anomaly).any(axis=0).astype(int)
dataset['ds'] = pd.to_datetime(dataset['ds'])

上述代码包含几个步骤:

  1. 使用pd.read_csv()函数加载数据集及其对应的标签。

  2. 将数据集处理为一个表格格式,包含三个主要信息:时间序列标识符(unique_id)、时间戳(ds)和观察值的数值(y)。

  3. 将标签处理成一个新的布尔型列is_anomaly,用于标记相应的观察值是否为异常。

    下面是该时间序列的样子:

图 9.2:标记异常的纽约市数据集

图 9.2:标记异常的纽约市数据集

如何执行…

现在,我们使用出租车行程数据集来训练一个预测模型。在本食谱中,我们将使用neuralforecast库,它包含了多个深度学习算法的实现:

  1. 让我们开始定义模型,如下所示:

    from neuralforecast import NeuralForecast
    from neuralforecast.models import NHITS
    horizon = 1
    n_lags = 144
    models = [NHITS(h=horizon,
        input_size=n_lags,
        max_steps=30,
        n_freq_downsample=[2, 1, 1],
        mlp_units=3 * [[128, 128]],
        accelerator='cpu')]
    nf = NeuralForecast(models=models, freq='30T')
    

    我们使用144作为输入大小(n_lags),这对应于 3 天的数据,因为时间序列每30分钟收集一次数据(freq='30T')。

  2. 在定义模型之后,我们可以使用fit()方法来训练它:

    nf.fit(df=dataset.drop('is_anomaly', axis=1), val_size=n_lags)
    

    在拟合过程之前,我们会删除包含异常信息的is_anomaly变量。现在的想法是使用该模型来预测时间序列的值。任何与实际值有显著偏差的情况都被视为异常。让我们看看训练预测结果。

  3. 通过调用predict_insample()方法,我们可以获取训练(或样本内)预测,如下所示:

    insample = nf.predict_insample()
    insample = insample.tail(-n_lags)
    abs_error = (insample['NHITS'] - insample['y']).abs()
    

    在上述代码中,我们获取训练样本,并移除最初的n_lag观察以将预测与实际数据对齐。然后,通过模型的绝对误差来衡量模型的性能。

  4. 可视化训练数据中的绝对误差以及标记的异常:

    preds = pd.DataFrame(
        {
            "Error": abs_error.values,
            "ds": dataset["ds"].tail(-n_lags),
            "is_anomaly": dataset["is_anomaly"].tail(-n_lags),
        }
    )
    preds = preds.set_index("ds")
    predicted_anomaly_periods = find_anomaly_periods(
        preds["is_anomaly"])
    setup_plot(preds.rename(columns={"Error": "y"}), 
        predicted_anomaly_periods, "Error")
    

    为了简洁起见,绘图功能未显示。您可以在 GitHub 存储库中查看它们。图表显示在以下图中:

图 9.3:神经分层时间序列实现(NHITS)模型的绝对误差和标记的异常

图 9.3:神经分层时间序列实现(NHITS)模型的绝对误差和标记的异常

在两个异常情况下会发生较大的误差,尽管模型也会错过一些异常情况。

工作原理…

如前一示例所示,我们使用预测模型来识别时间序列中的异常。在这种情况下,我们不使用预测区间,而是依赖模型的绝对误差。较大的误差表明时间序列中可能存在异常。

我们使用neuralforecast框架基于 NHITS 方法构建 DL 预测模型。NHITS 是一种扩展神经基础扩展分析NBEATS)的模型,基于多层感知器MLP)类型的架构。

这涉及将数据转换为适当的格式,并使用自回归方法训练模型。

还有更多…

在本示例中,我们专注于单变量时间序列数据集和特定的预测方法(NHITS)。然而,重要的是注意,基于预测的异常检测方法可以应用于不同的设置(例如多变量时间序列)和其他预测方法。

在训练阶段,我们需要定义一个误差阈值,超过这个阈值的观测值将被标记为异常。我们将在后续的示例中探讨几种带有此功能的实现。

使用 LSTM AE 进行异常检测

在本示例中,我们将构建一个 AE 来检测时间序列中的异常。AE 是一种神经网络NN)类型,试图重构输入数据。使用此类模型进行异常检测的动机在于,异常数据的重构过程比典型观测更为困难。

准备工作

在这个示例中,我们将继续使用纽约市出租车的时间序列数据。在框架方面,我们将展示如何使用 PyTorch Lightning 构建 AE 模型。这意味着我们将构建一个数据模块来处理数据预处理,另一个模块则用于处理神经网络的训练和推理。

如何实现…

这个示例分为三部分。首先,我们基于 PyTorch 构建数据模块。然后,我们创建 AE 模块。最后,我们将这两个部分结合起来,构建一个异常检测系统:

  1. 让我们从构建数据模块开始。我们创建一个名为TaxiDataModule的类,它继承自pl.LightningDataModule。以下是该类的构造函数:

    Import numpy as np
    import pandas as pd
    import lightning.pytorch as pl
    from pytorch_forecasting import TimeSeriesDataSet
    from sklearn.model_selection import train_test_split
    class TaxiDataModule(pl.LightningDataModule):
        def __init__(self,
                     data: pd.DataFrame,
                     n_lags: int,
                     batch_size: int):
            super().__init__()
            self.data = data
            self.batch_size = batch_size
            self.n_lags = n_lags
            self.train_df = None
            self.test_df = None
            self.training = None
            self.validation = None
            self.predict_set = None
    

    TaxiDataModule类除了数据集外,还接受两个输入:滞后数(上下文长度)和批次大小。

  2. 接下来,我们编写setup()方法,在该方法中准备训练和测试模型所需的数据:

        def setup(self, stage=None):
            self.data['timestep'] = np.arange(self.data.shape[0])
            unique_times = \
                self.data['timestep'].sort_values().unique()
            tr_ind, ts_ind = \
                train_test_split(unique_times, test_size=0.4,
                    shuffle=False)
            tr_ind, vl_ind = \
                train_test_split(tr_ind, test_size=0.1,
                    shuffle=False)
            self.train_df = \
                self.data.loc[self.data['timestep'].isin(tr_ind), :]
            self.test_df = \
                self.data.loc[self.data['timestep'].isin(ts_ind), :]
            validation_df = \
                self.data.loc[self.data['timestep'].isin(vl_ind), :]
            self.training = TimeSeriesDataSet(
                data=self.train_df,
                time_idx="timestep",
                target="y",
                group_ids=['unique_id'],
                max_encoder_length=self.n_lags,
                max_prediction_length=1,
                time_varying_unknown_reals=['y'],
            )
            self.validation = \
                TimeSeriesDataSet.from_dataset(
                    self.training, validation_df)
            self.test = \
                TimeSeriesDataSet.from_dataset(
                    self.training, self.test_df)
            self.predict_set = \
                TimeSeriesDataSet.from_dataset(
                    self.training, self.data, predict=True)
    

    在前面的代码中,我们首先将数据拆分为训练集、验证集和测试集。每个数据集都被转换为TimeSeriesDataSet类的实例。

  3. 数据加载器的实现如下:

        def train_dataloader(self):
            return self.training.to_dataloader(
                batch_size=self.batch_size, shuffle=False)
        def val_dataloader(self):
            return self.validation.to_dataloader(
                batch_size=self.batch_size, shuffle=False)
        def predict_dataloader(self):
            return self.predict_set.to_dataloader(
                batch_size=1, shuffle=False)
    

    本质上,数据加载过程与我们之前在预测任务中所做的类似。例如,你可以查看第五章中的多步多输出的多变量时间序列预测示例。

  4. 现在,我们关注 AE 模型,它分为两部分:编码器和解码器。以下是名为Encoder的类中的编码器实现:

    from torch import nn
    import torch
    class Encoder(nn.Module):
        def __init__(self, context_len, n_variables, 
            embedding_dim=2):
            super(Encoder, self).__init__()
            self.context_len, self.n_variables = \
                context_len, n_variables
            self.embedding_dim, self.hidden_dim = \
                embedding_dim, 2 * embedding_dim
            self.lstm1 = nn.LSTM(
                input_size=self.n_variables,
                hidden_size=self.hidden_dim,
                num_layers=1,
                batch_first=True
            )
            self.lstm2 = nn.LSTM(
                input_size=self.hidden_dim,
                hidden_size=embedding_dim,
                num_layers=1,
                batch_first=True
            )
        def forward(self, x):
            batch_size = x.shape[0]
            x, (_, _) = self.lstm1(x)
            x, (hidden_n, _) = self.lstm2(x)
            return hidden_n.reshape((batch_size, 
                self.embedding_dim))
    
  5. 解码器在一个名为Decoder的类中实现,该类也继承自nn.Module

    class Decoder(nn.Module):
        def __init__(self, context_len, n_variables=1, input_dim=2):
            super(Decoder, self).__init__()
            self.context_len, self.input_dim = \
                context_len, input_dim
            self.hidden_dim, self.n_variables = \
                2 * input_dim, n_variables
            self.lstm1 = nn.LSTM(
                input_size=input_dim,
                hidden_size=input_dim,
                num_layers=1,
                batch_first=True
            )
            self.lstm2 = nn.LSTM(
                input_size=input_dim,
                hidden_size=self.hidden_dim,
                num_layers=1,
                batch_first=True
            )
            self.output_layer = nn.Linear(self.hidden_dim, 
                self.n_variables)
        def forward(self, x):
            batch_size = x.shape[0]
            x = x.repeat(self.context_len, self.n_variables)
            x = x.reshape((batch_size, self.context_len, 
                self.input_dim))
            x, (hidden_n, cell_n) = self.lstm1(x)
            x, (hidden_n, cell_n) = self.lstm2(x)
            x = x.reshape((batch_size, self.context_len, 
                self.hidden_dim))
            return self.output_layer(x)
    
  6. 这两个部分被结合在一个名为AutoencoderLSTM的类中,该类继承自pl.LightningModule

    import torch
    class AutoencoderLSTM(pl.LightningModule):
        def __init__(self, context_len, n_variables, embedding_dim):
            super().__init__()
            self.encoder = Encoder(context_len, n_variables, 
                embedding_dim)
            self.decoder = Decoder(context_len, n_variables, 
                embedding_dim)
        def forward(self, x):
            xh = self.encoder(x)
            rec_x = self.decoder(xh)
            return rec_x
        def configure_optimizers(self):
            return torch.optim.Adam(self.parameters(), lr=0.001)
    

    forward()方法中,编码器部分接收原始输入(self.encoder(x))并将其转换为降维后的表示(xh对象)。然后,解码器基于xh重建原始输入数据。

  7. 然后,我们实现训练、验证和预测步骤:

        import torch.nn.functional as F
        def training_step(self, batch, batch_idx):
            x, y = batch
            y_pred = self(x['encoder_cont'])
            loss = F.mse_loss(y_pred, x['encoder_cont'])
            self.log('train_loss', loss)
            return loss
        def validation_step(self, batch, batch_idx):
            x, y = batch
            y_pred = self(x['encoder_cont'])
            loss = F.mse_loss(y_pred, x['encoder_cont'])
            self.log('val_loss', loss)
            return loss
        def predict_step(self, batch, batch_idx):
            x, y = batch
            y_pred = self(x['encoder_cont'])
            loss = F.mse_loss(y_pred, x['encoder_cont'])
            return loss
    
  8. 我们使用 PyTorch Lightning 中的Trainer类训练神经网络。我们使用144个滞后,这相当于 3 天的数据。同时,我们应用了提前停止机制来指导训练过程:

    N_LAGS = 144
    N_VARIABLES = 1
    from lightning.pytorch.callbacks import EarlyStopping
    datamodule = \
        TaxiDataModule(
            data=dataset.drop('is_anomaly', axis=1),
            n_lags=N_LAGS,
            batch_size=32)
    model = AutoencoderLSTM(n_variables=1,
            context_len=N_LAGS,
            embedding_dim=4)
    early_stop_callback = EarlyStopping(monitor="val_loss",
        min_delta=1e-4,
        patience=5,
        verbose=False,
        mode="min")
    trainer = pl.Trainer(max_epochs=20,
        accelerator='cpu',
        callbacks=[early_stop_callback])
    trainer.fit(model, datamodule)
    
  9. 训练后,我们可以按如下方式将模型应用于测试数据:

    dl = datamodule.test.to_dataloader(batch_size=1, shuffle=False)
    preds = trainer.predict(model, dataloaders=dl)
    preds = pd.Series(np.array([x.numpy() for x in preds]))
    

    在前面的代码中,我们将数据模块中的test对象转换为数据加载器。我们使用批次大小为1,不进行洗牌,以便按顺序处理每个实例。然后,我们使用trainer对象获取预测。以下图表展示了测试集中的重建误差:

图 9.4:AE 的重建误差和标记的异常点

图 9.4:AE 的重建误差和标记的异常点

在大多数情况下,重建误差的峰值与异常点重合。

它是如何工作的…

数据模块中的工作流程可能很熟悉,因为它遵循了我们在其他章节构建的预测模型的相同思路;例如,在 多步多输出的多变量时间序列预测 食谱中 第五章。但在这种情况下,我们并不关心预测序列的未来值。相反,在每个时间步,模型的输入和输出都是序列的最近滞后值。

AE 由两个主要部分组成:编码器和解码器。编码器的目标是将输入数据压缩成一个小维度,这个小维度被称为瓶颈。将输入数据转化为小维度对于使神经网络(NN)聚焦于数据中的最重要模式、忽略噪音至关重要。然后,解码器接收在压缩维度中编码的数据,并尝试重建原始输入数据。编码器和解码器的神经网络均基于堆叠 LSTM AE。不过,你也可以使用不同的架构来构建这些组件。

Encoder 类继承自 torchnn.Module 类。这个特定的编码器由两层 LSTM 组成,这些层像在 forward() 方法中详细说明的那样堆叠在一起。Decoder 类也包含两层堆叠的 LSTM 层,后面接一个密集连接的层。

在 AE 的训练步骤中,我们将一批滞后的时间序列(x['encoder_cont'])传递给模型。它会生成一个名为 y_pred 的对象,即重建后的输入数据。然后,我们计算 F.mse_loss,它用于比较原始输入和重建后的输入。

使用 PyOD 构建 AE

PyOD 是一个专门用于异常检测的 Python 库。它包含了多个基于重建的算法,如 AE。在本食谱中,我们将使用 PyOD 构建 AE 来检测时间序列中的异常。

准备就绪

你可以使用以下命令安装 PyOD:

pip install pyod

我们将使用与前一个食谱相同的数据集。因此,我们从在 基于预测的异常检测使用深度学习 食谱中创建的数据集对象开始。让我们看看如何转换这些数据来使用 PyOD 构建 AE。

操作步骤...

以下步骤展示了如何构建 AE 并预测异常的概率:

  1. 我们首先通过以下代码使用滑动窗口转换时间序列:

    import pandas as pd
    from sklearn.preprocessing import StandardScaler
    N_LAGS = 144
    series = dataset['y']
    input_data = []
    for i in range(N_LAGS, series.shape[0]):
        input_data.append(series.iloc[i - N_LAGS:i].values)
    input_data = np.array(input_data)
    input_data_n = StandardScaler().fit_transform(input_data)
    input_data_n = pd.DataFrame(input_data_n)
    

    在前面的代码中:

    • 我们获取时间序列的值列,并将其存储在系列对象中。

    • 然后,我们通过类似自回归方法的滑动窗口遍历数据集。这样,时间序列在每个时间步上都由其过去的滞后值(N_LAGS)表示。

    • 我们使用 scikit-learn 中的 StandardScaler 来标准化数据,这是训练神经网络(如 AE)时非常重要的一步。

  2. 在预处理数据后,我们根据 PyOD 定义 AE 并使用数据集进行拟合:

    from pyod.models.auto_encoder_torch import AutoEncoder
    model = AutoEncoder(
        hidden_neurons=[144, 4, 4, 144],
        hidden_activation="relu",
        epochs=20,
        batch_norm=True,
        learning_rate=0.001,
        batch_size=32,
        dropout_rate=0.2,
    )
    model.fit(input_data_n)
    anomaly_scores = model.decision_scores_
    

    这是分数的分布:

图 9.5:由 AE 产生的异常分数直方图

图 9.5:AE 生成的异常分数直方图

  1. 关于推断步骤,我们可以使用 predict()predict_proba() 方法。predict() 方法的工作原理如下:

    predictions = model.predict(input_data_n)
    
  2. predict_proba() 方法的工作原理如下:

    probs = model.predict_proba(input_data_n)[:, 1]
    probabilities = pd.Series(probs, \
        index=series.tail(len(probs)).index)
    
  3. 概率表示每个观察值是异常值的概率。你可以使用以下代码绘制概率图:

    ds = dataset.tail(-144)
    ds['Predicted Probability'] = probabilities
    ds = ds.set_index('ds')
    anomaly_periods = find_anomaly_periods(ds['is_anomaly'])
    setup_plot(ds, anomaly_periods)
    

    这是训练集中的概率分布:

图 9.6: AE 生成的异常概率分数

图 9.6:AE 生成的异常概率分数

同样,异常概率分数的峰值与一些异常值重合。

它的工作原理…

PyOD 库遵循与 scikit-learn 类似的设计模式。因此,每个方法,如 AutoEncoder,都使用 fit() 方法进行训练,并基于 predictpredict_proba() 方法进行预测。

我们使用来自 auto_encoder_torch 模块的 AutoEncoder 类实例。该库还包含相应的方法,但具有 TensorFlow 后端。我们创建模型的实例并设置一些参数:

  • hidden_neurons=[144, 2, 2, 144]:这些参数详细说明了每层的隐藏单元数。输入层和输出层的单元数与输入大小相等,即滞后数。AE 的隐藏层通常具有较少的单元数,以便在重建之前压缩输入数据。

  • hidden_activation:激活函数,设置为修正线性函数。

  • batch_norm:一个布尔值,表示是否应用批量归一化。你可以通过以下链接了解更多信息:pytorch.org/docs/stable/generated/torch.nn.BatchNorm1d.html

  • learning_rate:学习率,设置为 0.001

  • batch_size:批处理大小,设置为 64

  • dropout_rate:层之间的 dropout 率,用于正则化,设置为 0.3

在这个示例中,我们创建了另一个用于异常检测的 AE。这涉及使用滑动窗口转换时间序列,类似于我们为自回归模型构建预测模型时所做的。该模型基于异常值分数预测每个观察值是否为异常。阈值由模型自动设置,当然你也可以自定义阈值。

还有更多内容…

判断一个观察值是否为异常涉及分析模型的异常分数。你可以使用不同的方法,如百分位数或标准差。例如,如果重建误差超过某个百分位数(例如 95),或者重建误差超过两个标准差,则可以认为该观察值为异常。

注意

我们使用训练数据的预测进行说明。使用测试数据的方法类似。

创建用于时间序列异常检测的 VAE

在前一个例子的基础上,我们现在将注意力转向 VAE,这是在时间序列数据中进行异常检测的一种更复杂且概率性的方式。与传统的自动编码器不同,VAE 引入了概率解释,使其更擅长处理现实世界数据中的固有不确定性。

准备工作

本例中的代码基于 PyOD。我们还使用与前一个例子相同的数据集:

N_LAGS = 144
series = dataset['y']

现在,让我们看看如何为时间序列异常检测创建 VAE。

如何操作……

我们首先准备数据集,如同在前一个例子中所做的那样:

  1. 数据集首先使用滑动窗口进行转换,这是一种帮助模型理解时间序列内时间依赖性的技术:

    import pandas as pd
    from sklearn.preprocessing import StandardScaler
    import numpy as np
    input_data = []
    for i in range(N_LAGS, series.shape[0]):
        input_data.append(series.iloc[i - N_LAGS:i].values)
        input_data = np.array(input_data)
        input_data_n = StandardScaler().fit_transform(input_data)
        input_data_n = pd.DataFrame(input_data_n)
    
  2. 转换数据集后,我们使用 PyOD 的 VAE 类来定义和拟合 VAE 模型。VAE 类的配置包括指定编码器和解码器网络的架构以及各种训练参数:

    from pyod.models.vae import VAE
    from tensorflow.keras.losses import mean_squared_error
    model = VAE(encoder_neurons=[144, 4],
            decoder_neurons=[4, 144],
            latent_dim=2,
            hidden_activation='relu',
            output_activation='sigmoid',
            loss=mean_squared_error,
            optimizer='adam',
            epochs=20,
            batch_size=32,
            dropout_rate=0.2,
            l2_regularizer=0.1,
            validation_size=0.1,
            preprocessing=True,
            verbose=1)
    model.fit(input_data_n)
    
  3. 经过拟合的 VAE 模型随后被用来生成异常分数。这些分数反映了每个数据点与模型学习到的模式的一致性。分数较高的点更可能是异常值:

    anomaly_scores = model.decision_scores_
    

工作原理……

VAE 是一种神经网络模型,因其能够处理数据的潜在或隐藏方面而突出。与传统的自动编码器(AE)不同,后者将输入映射到潜在空间中的固定点,VAE 将输入转换为概率分布,通常是正态分布,特征包括均值和方差。通过这种方式,每个输入与潜在空间中的一个区域相关联,而不是一个单一的点,从而引入了随机性和变异性。

解码器网络从这些估计的分布中采样点,并尝试重建原始输入数据。训练过程涉及两个关键目标:

  • 最小化重建误差确保解码器能够准确地从潜在表示重建输入数据。

  • 正则化潜在空间分布,使其接近标准正态分布。通常通过最小化 Kullback-Leibler 散度来实现这一点。正则化过程可以防止过拟合,并确保潜在空间结构良好且连续。

训练完成后,可以使用 VAE 进行异常检测。VAE 应该能够以较低的误差重建正常数据(类似于它训练时使用的数据)。相反,训练集之外的数据点(潜在异常值)可能会以较高的误差重建。因此,重建误差可以作为异常分数。

高重建误差表明数据点与学习到的数据分布不太一致,将其标记为异常:

图 9.7:真实值、真实异常值,以及 VAE 预测的异常概率

图 9.7:真实值、真实异常值,以及 VAE 预测的异常概率

这个比较帮助我们评估 VAE 在现实场景中的表现。

还有更多内容…

VAE 最有趣的方面之一是它们能够生成新的数据点。通过从潜在空间中学习到的分布进行采样,我们可以生成与训练数据相似的新实例。这个特性在需要数据增强的场景中特别有用。

此外,VAE 的概率性质提供了一种自然的方式来量化不确定性。这在需要评估模型预测置信度的场景中尤为有用。

使用 GAN 进行时间序列异常检测

GAN 在机器学习的各个领域中获得了显著的关注,特别是在图像生成和修改方面。然而,它们在时间序列数据中的应用,尤其是在异常检测方面,仍然是一个新兴的研究和实践领域。在这篇文档中,我们专注于利用 GAN,特别是 生成对抗网络进行异常检测 (AnoGAN),来检测时间序列数据中的异常。

正在准备中…

在开始实现之前,请确保已经安装了 PyOD 库。我们将继续使用出租车行程数据集,该数据集为时间序列异常检测提供了一个真实的背景。

如何实现…

实现涉及多个步骤:数据预处理、定义和训练 AnoGAN 模型,最后进行异常检测:

  1. 我们首先加载数据集并为 AnoGAN 模型做准备。数据集通过滑动窗口方法进行转换,与之前的方法相同:

    import pandas as pd
    from sklearn.preprocessing import StandardScaler
    import numpy as np
    N_LAGS = 144
    series = dataset['y']
    input_data = []
    for i in range(N_LAGS, series.shape[0]):
        input_data.append(series.iloc[i - N_LAGS:i].values)
        input_data = np.array(input_data)
        input_data_n = StandardScaler().fit_transform(input_data)
        input_data_n = pd.DataFrame(input_data_n)
    
  2. 然后,AnoGAN 在特定的超参数下进行定义,并在预处理后的数据上进行训练:

    from pyod.models.anogan import AnoGAN
    model = AnoGAN(activation_hidden='tanh',
        dropout_rate=0.2,
        latent_dim_G=2,
        G_layers=[20, 10, 3, 10, 20],
        verbose=1,
        D_layers=[20, 10, 5],
        index_D_layer_for_recon_error=1,
        epochs=20,
        preprocessing=False,
        learning_rate=0.001,
        learning_rate_query=0.01,
        epochs_query=1,
        batch_size=32,
        output_activation=None,
        contamination=0.1)
    model.fit(input_data_n)
    
  3. 一旦模型训练完成,我们就使用它来预测数据中的异常:

    anomaly_scores = model.decision_scores_
    predictions = model.predict(input_data_n)
    
  4. 最后,我们通过可视化结果,将模型预测与实际异常进行比较:

图 9.8:真实值、真实异常和 GAN 预测的异常概率

图 9.8:真实值、真实异常和 GAN 预测的异常概率

它是如何工作的…

AnoGAN 是一个利用 GAN 原理进行时间序列数据异常检测的模型。AnoGAN 的核心思想是生成器和判别器之间的相互作用。

生成器的任务是创建与真实时间序列数据相似的合成数据。它学习捕捉输入数据的潜在模式和分布,努力生成与真实数据无法区分的输出。

判别器,另一方面,充当评论员。它的角色是辨别它审查的数据是否真实(来自数据集的实际数据点)或虚假(由生成器生成的输出)。在训练过程中,这两个组件进行着一场持续的博弈:生成器提高其生成真实数据的能力,而判别器则在识别伪造数据方面变得更加精确。

重建误差再次被用来识别异常。生成器只在正常数据上进行训练,因此在重建异常值或离群点时会遇到困难。因此,当数据点的重建版本与原始数据显著偏离时,我们就能发现潜在的异常。

在实际应用中,重建误差可以通过各种方法来计算,例如均方误差(MSE)或其他距离度量,具体取决于数据的性质和任务的具体要求。

还有更多……

虽然 AnoGAN 提供了一种新颖的时间序列异常检测方法,但值得探索其变种和改进。例如,可以考虑调整模型的架构,或尝试不同类型的 GAN,如条件生成对抗网络CGANs)或Wasserstein 生成对抗网络WGANs)。

posted @ 2025-07-10 11:38  绝不原创的飞龙  阅读(12)  评论(0)    收藏  举报