Python-时间序列的机器学习-全-

Python 时间序列的机器学习(全)

原文:annas-archive.org/md5/6fe7868ac7b4b689aa7ceab9954a4809

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:使用 Python 介绍时间序列

这本书讲的是关于 Python 的时间序列机器学习,你可以将本章看作是时间序列的入门课程。在本章中,我们将介绍时间序列、时间序列研究的历史,以及如何使用 Python 进行时间序列分析。

我们将从时间序列的定义及其主要特性开始。接着,我们将探讨不同科学领域中时间序列研究的历史,这些领域为该学科奠定了基础,例如人口学、天文学、医学和经济学。

然后,我们将探讨 Python 在时间序列方面的能力,以及为什么 Python 是做时间序列机器学习的首选语言。最后,我将介绍如何安装 Python 中用于时间序列分析和机器学习的主要库,并将涵盖与时间序列和机器学习相关的 Python 基础知识。

我们将覆盖以下主题:

  • 什么是时间序列?

    • 时间序列的特性
  • 时间序列与预测 – 过去与现在

    • 人口学

    • 遗传学

    • 天文学

    • 经济学

    • 气象学

    • 医学

    • 应用统计学

  • Python 与时间序列

那么,什么是时间序列呢?让我们从定义开始!

什么是时间序列?

由于这是一本关于时间序列数据的书,我们应该首先澄清我们所讨论的内容。在本节中,我们将介绍时间序列及其特性,并深入探讨与机器学习和统计学相关的不同问题和分析类型。

许多学科,如金融、公共行政、能源、零售和医疗保健,主要依赖时间序列数据。微观经济学和宏观经济学的很多领域依赖于应用统计学,特别是侧重于时间序列分析和建模。以下是一些时间序列数据的例子:

  • 股票指数的每日收盘值

  • 每周某疾病的感染人数

  • 每周的火车事故系列

  • 每日降水量

  • 传感器数据,如每小时的温度测量

  • 每年人口增长

  • 一家公司若干年内的季度盈利

这仅仅是举几个例子。任何涉及时间变化的数据都可以视为时间序列。

也许有必要简要定义一下什么被视为时间序列。

定义:时间序列是按时间顺序排列的观测数据集。

这是一个非常广泛的定义。或者我们也可以说,时间序列是按时间顺序排列的数据点序列,或者说时间序列是一个随机过程的结果。

从形式上讲,我们可以有两种方式定义时间序列。第一种是将其定义为从时间域到实数域的映射:

其中

另一种定义时间序列的方法是将其视为一个随机过程:

在这里, 表示随机变量 X 在时间点 t 的值。

如果 T 是实数集,它就是一个连续时间随机过程。如果 T 是整数集,我们称其为离散时间的随机过程。在后者的情况下,惯例是写作

由于时间是数据集的主要索引,因此,时间序列数据集描述了世界如何随时间变化。它们通常涉及如何通过过去影响现在或未来的问题。

监控和数据收集的增加带来了对统计学和机器学习技术的需求,这些技术应用于时间序列,以预测和描述复杂系统或系统内组件的行为。与时间序列打交道的一个重要部分是如何基于过去预测未来。这就是所谓的预测。

一些方法允许将商业周期作为额外特征添加。这些额外的特征称为外生特征——它们是依赖于时间的解释性变量。我们将在第三章时间序列预处理中讨论特征生成的示例。

时间序列的特征

这是一个从 Google 趋势导出的时间序列数据集示例,展示了 Python、R 和 Julia 的搜索量:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/(A Document Being Saved By screencaptureui 2)/Screenshot 2021-03-11 at 17.13.02.png

图 1.1:时间序列数据集的提取

这是一个多变量时间序列,包含 Python、R 和 Julia 的列。第一列是索引列,是一个日期列,周期为每月。如果我们只有一个变量,我们称其为单变量序列。如果我们只有一种编程语言而不是三种,那么这个数据集将是单变量的。

时间序列通常以离散时间形式出现,其中每个点之间的时间差是相同的。时间序列的最重要特征如下:

  • 值的长期变化(趋势

  • 季节性变化(季节性

  • 不规则或周期性成分

趋势是某事物发展或变化的一般方向,例如序列中的长期增加或减少。一个可以观察到趋势的例子是全球变暖,这是过去半个世纪温度持续上升的过程。

下面是来自 NASA 发布的 GISS 表面温度分析数据集中的过去 100 年全球表面温度变化的图表:

temperatures.png

图 1.2:1880 年至 2019 年间的 GISS 表面温度分析

如你在图 1.2中所见,温度变化在 20 世纪中期之前围绕 0 波动;然而,从那时起,整体温度逐年上升的趋势变得十分明显。

季节性是指在特定的、不到一年的规律间隔内发生的变化。季节性可以在不同的时间跨度上发生,例如每日、每周、每月或每年。每周季节性的一个例子是冰淇淋的销售量在每个周末都会增加。此外,根据你所居住的地方,冰淇淋可能只在春夏季节出售。这是一种年度变化。

除了季节性变化和趋势外,还有一些变动,它们的频率不固定,或者以非季节性频率的方式波动。我们中的一些人可能能基于现有知识对这些变动进行解释。

作为一个不规则的周期性变化例子,银行假期每年可能会落在不同的日历日期上,而促销活动则可能取决于商业决策,例如推出新产品。作为一个非季节性变化的周期性变化例子,毫秒级别的变化或发生在超过一年时间段的变化,将不被称为季节性效应。

平稳性是指时间序列在时间推移中其分布不会发生变化,如其总结统计量所描述的那样。如果一个时间序列是平稳的,那么它意味着该序列没有趋势,也没有确定性的季节性变化,尽管其他周期性变化是允许的。这是我们将在第五章使用移动平均和自回归模型进行预测中讨论的算法中的一个重要特性。为了应用这些方法,我们需要通过去除季节性和趋势,将非平稳数据转换为平稳数据。

我们将在第二章使用 Python 进行时间序列分析第三章时间序列预处理中更详细地讨论这些以及其他概念。

确定、量化和分解这些以及其他特征的任务被称为时间序列 分析。探索性时间序列分析通常是在进行任何特征转换和机器学习之前的第一步。

时间序列与预测——过去与现在

时间序列自古以来就被研究,而从那时起,时间序列分析和预测已经取得了长足的进展。多种学科为应用于时间序列的技术发展做出了贡献,包括数学、天文学、人口统计学和统计学。许多创新最初来自数学,后来是统计学,最终是机器学习。许多应用统计学的创新源于人口统计学(用于公共管理)、经济学或其他领域。

在本节中,我将概述从简单方法到今天可用的机器学习方法的发展路径。我将尝试绘制自工业革命以来到现代,与时间序列相关的概念发展图谱。我们将在第四章基于时间序列的机器学习导论中处理更技术性和最新的内容。

时间序列的研究仍有很多进展可期。可穿戴传感器和物联网的发展意味着大数据可以用来进行分析和预测。近几年,随着大量数据集的可用性用于基准测试和竞赛,已经帮助创造出新的方法,正如我们将在后续章节中讨论的那样。

人口学

大部分早期用于建立时间序列分析理论和实践的工作来源于公共管理中应用的人口学。本节中提到的许多人要么是公共服务人员,要么出于对抽象问题的兴趣在私人领域作出了贡献。

约翰·格朗特,最初是一名帽商,后来对伦敦教区记录的死亡记录产生了兴趣。在 1662 年,他在《自然与政治观察:关于死亡账单的研究》一书中发布了公共卫生统计数据。该书除了包含有关流行病学的统计数据外,还包含了第一张生命表。生命表(也叫死亡表或精算表)是一种表格,显示了每个年龄段的人在其下一次生日之前死亡的概率。格朗特通过死亡账单得出结论,这些账单由教区书记员生成,记录了伦敦市及其周边地区英格兰教会墓地的埋葬情况。

格朗特的书籍具有深远的影响,他被广泛认为是人口学的奠基人。格朗特被选为皇家学会会员;然而,在 1666 年伦敦大火中,他的房屋被烧毁,导致破产,最终他在 53 岁时死于黄疸和肝病。

其中,他启发了瑞士数学家雅各布·伯努利的著作《猜想艺术》,该书写于 1684 年至 1689 年之间,并于 1713 年死后出版,是组合数学和概率论中的一部里程碑式著作,其中包括了——在众多内容中——大数法则的第一个版本。

大数法则描述了当实验重复进行多次时会发生什么。伯努利证明,在一个有两个结果的机会游戏中(比如抛硬币),无论是胜利还是失败,如果重复很多次,游戏的胜利次数比例会趋近于真实的、预期的概率。

人口学历史上的另一个重大里程碑出现在 1689 年,由德国教授和神职人员卡斯帕·诺伊曼(Caspar Neumann)撰写的文章《关于布雷斯劳出生与死亡的生与死的反思》中(译名:Reflections about the Life and Death of People Who Were Born and Died in Breslau)。诺伊曼将这篇论文发送给了著名哲学家和数学家戈特弗里德·莱布尼茨,并将他的数据提供给了伦敦皇家学会。

许多后续的研究工作都基于本文中的数据和统计资料。1693 年,埃德蒙·哈雷在《皇家学会哲学汇刊》上发表了一篇关于生命年金的文章(《人类死亡率的估算》),他基于诺伊曼的数据编制了死亡率表。年金是定期支付的款项,例如抵押贷款、保险和养老金支付。哈雷的文章为精算科学的发展提供了指导,并帮助英国政府根据购买者的年龄,确定出售退休收入保险的适当价格。稍后我们将在天文学部分再次提到哈雷。

亚伯拉罕·德·莫伊夫是一位法国人,由于法国胡格诺派的宗教迫害,他年轻时移居英格兰。今天,他最为人知的是他在正态分布和概率论方面的贡献。1724 年,他出版了一本名为《基于生命的年金》的书,书封面如下面所示,内容涉及死亡率统计和年金理论的基础。

../../Desktop/Screenshot%202021-04-15%20at%2022.55.21.pn

图 1.3:基于生命的年金

德·莫伊夫还因提出二项分布的近似方法和研究泊松分布(后来以西蒙·丹尼斯·泊松的名字命名)而被铭记。

在具备了一些统计学基础后,我们现在开始进入对未来的预测,这时时间序列预测就派上用场了。1751 年,本杰明·富兰克林在他的文章《关于人口增长、人类繁衍等的观察》中研究了人口增长及其极限,预测了英国殖民地的指数增长。他预测英国王室殖民地的人口每 25 年翻一番,并认为这一增长有潜力传播自由政治传统,增强英格兰的力量。他的预测被证明是正确的,指数增长一直持续到 19 世纪 50 年代,当时美国人口超过了英格兰。

受富兰克林的影响,英格兰牧师托马斯·罗伯特·马尔萨斯提出了人口增长将超出粮食生产增长的担忧。在他的假设中,虽然人口增长是指数型的,但粮食供应和其他资源的增长是线性的,这最终将导致社会崩溃和大规模人口死亡。18 世纪末,他描述了日益加剧的饥荒和贫困(后来被称为“马尔萨斯陷阱”)。

许多其他统计学和数学概念是基于人口数据发展出来的。阿道夫·凯特莱(Adolphe Quetelet),一位来自今天比利时根特的天文学家、数学家和社会学家,将统计方法引入社会科学,用来描述犯罪率、婚姻率和自杀率等背后的关系。他呼吁建立一种“社会物理学”,揭示社会现象背后的规律,从而显示上帝的工作。除了其他成就外,他还发展了身体质量指数,最初被称为凯特莱指数。在他 1835 年的书《论人类》(英译名:Treatise on Man)中,他根据正态分布描述了“平均人”的概念。凯特莱的学生皮埃尔·费尔霍斯特(Pierre Verhulst)发展了逻辑斯蒂函数,作为人口增长模型。

西蒙·丹尼斯·泊松(Siméon Denis Poisson)于 1837 年发表了《论刑事和民事案件判断的概率》(法文原名:《Recherches sur la probabilité des jugements en matière criminelle et en matière civile》),在其中阐述了关于在给定区间内发生的离散事件的概率理论。泊松分布就是以他的名字命名的。

威廉·莱克西斯(Wilhelm Lexis),人口统计学时间序列分析的先驱,发表了名为《统计系列稳定性理论》的论文(1879),介绍了如今被称为莱克西斯比率的量度。该比率区分了稳定系列,其中引发观察到的比率的潜在概率分布保持不变,和非稳定系列。这些稳定的时间序列不会受到除了随机噪声以外的任何力量的影响。用今天的术语来说,稳定的时间序列将被称为白噪声过程或零阶移动平均。

为了区分稳定与非稳定的时间序列,莱克西斯创建了一个检验统计量,该统计量等于观察到的比率的离散度与如果每个观察到的比率的潜在概率在所有观察中相等时应有的离散度之比。如果这个比率 Q 大于 1.41,他认为这意味着该时间序列是不稳定的,或者用他的话说,受物理力量的影响。莱克西斯后来成为德国联邦保险监管局保险咨询委员会的成员。

遗传学

弗朗西斯·高尔顿(Francis Galton),一位维多利亚时代的英国科学家,出生在一个著名的银行家和枪械制造商家族中,家族中有多位皇家学会成员。高尔顿是一位高产的作家和研究者。今天,他主要因创造了“优生学”一词而被人们记住,优生学是研究未来世代种族质量变化的学科,重点关注人类的理想品质。优生学与种族主义和白人至上主义有着密切的联系。

加尔顿对许多科学学科感兴趣,如心理学、统计学、心理物理学、摄影等。凭借他的贡献,他在 1909 年被封为爵士。除此之外,他还为人体测量学做出了贡献,这是一种对人体进行系统测量和描述的方法。为此,他重新发现了相关性的概念(该概念最早由法国物理学家奥古斯特·布雷维斯于 1846 年提出),并描述了前臂长度与身高、头宽与头深、头长与身高之间的相关性(1888 年)。

他的一个门徒(兼传记作者)是卡尔·皮尔逊,皮尔逊出生于伦敦伊斯灵顿的贵格会家庭,父亲是皇室律师(Queen's Counsel)。他在剑桥大学学习数学,在海德堡大学学习物理学与哲学,在柏林大学学习生理学,之后回到伦敦学习法律。在伦敦,他结识了加尔顿,两人保持了联系。

加尔顿去世后,皮尔逊成为首位担任加尔顿在遗嘱中捐赠的优生学讲座教授的人。皮尔逊主要的兴趣在于将生物统计学应用于遗传背景中。他被认为是标准差的发明者,标准差是衡量正态分布变异性的一种方法,替代了卡尔·弗里德里希·高斯的平均误差概念。他还对统计学做出了贡献,包括卡方检验、统计显著性的 p 值、现代相关性、主成分分析和直方图等。

皮尔逊继费舍尔之后成为了加尔顿优生学教授(后改名为加尔顿遗传学讲座教授)。费舍尔在进化理论方面做出了许多创新,涉及拟态、亲代投资以及 1:1 性别比例背后的费舍尔原理。在统计学方面,他描述了线性判别分析、费舍尔信息量、F 分布以及学生 t 分布。

他在统计学方面的贡献为时间序列分析中的统计检验和一些经典模型奠定了基础。费舍尔于 1952 年被伊丽莎白二世女王封为骑士。然而,他与种族主义观点的关联——例如,他支持德国纳粹党提出的灭绝政策,目的是改善遗传基因——导致了近期对他工作的重新评估。

因此,伦敦大学学院UCL)的罗纳德·费舍尔中心被更名为计算生物学中心,UCL 也就其在传播优生学中的作用公开道歉。

天文学

彗星、小行星的观测以及太阳和行星的运动已被记录了很长时间,人们通过研究这些记录来理解这些运动的规律性、相互关系以及我们在宇宙中的位置。我们在前面提到的人口学部分中提到的英国天文学家和地球物理学家埃德蒙·哈雷,应用了艾萨克·牛顿在 1687 年提出的运动定律,研究历史上的彗星现象。哈雷彗星是一颗肉眼可见的彗星,几乎在全球范围内都有目击记录,且至少已有约 2000 年的历史,被古希腊的文献和巴比伦的天文表格所记载。

例如,在公元前 12 年,它出现在接近耶稣基督诞生日期的时刻,导致有人猜测这可能是圣经故事中伯利恒之星的来源。1066 年,这颗彗星在英格兰出现,被认为是神的讯息,预示着哈罗德二世的命运,哈罗德二世在同年于黑斯廷斯战役中阵亡,战斗对手是由征服者威廉领导的诺曼征服军。

哈雷将多次彗星出现联系在一起,得出结论认为每次看到的是同一颗彗星,并计算出其周期约为 75-76 年。今天,这颗彗星以他的名字命名,以示敬意,称为哈雷彗星。这一结论发表于《彗星天文学概要》(1705 年)。哈雷彗星将于 2061 年重新出现。

该图显示了哈雷彗星的轨道(来源:Wikimedia Commons):

ile:Halley's Comet animation.gif - Wikimedia Commons

图 1.4:哈雷彗星轨道

德国博学家卡尔·弗里德里希·高斯于 1801 年设计了一种确定矮行星谷神星轨道的方法。谷神星位于火星和木星之间的小行星带中。高斯基于一位天主教神父兼天文学家朱塞佩·皮亚奇的观察结果进行推算,皮亚奇在同年 1 月至 2 月间追踪了一个天体,后来失去了该天体的踪迹。

此后,线性拟合方法被应用于天体的运动,最著名的是最小二乘法。这一方法最早由阿德里安-玛丽·勒让德在 1805 年描述("méthode des moindres carrés"),但今天通常将其归功于高斯。高斯在 1809 年后来发表了关于该方法的研究;然而,他在勒让德的基础上进行了大幅扩展,其中之一就是发明了以他命名的高斯分布(也叫正态分布或钟形曲线)。

最小二乘法是线性回归方法的基础,在该方法中,方程组中的参数被估计出来。这是一种统计程序,通过最小化绘制曲线的平方残差和来找到一组数据点的最佳拟合。

仅仅一年后,皮埃尔-西蒙·拉普拉斯证明了中心极限定理,大致说明独立变量的和,即使它们不来自正态分布,仍会趋向正态分布。这为最小二乘法和正态分布在大数据集中的应用提供了重要的理论支持。从那时起,正态分布在统计学领域产生了深远的影响,均值和标准差等度量方法被广泛用来描述它。

拉普拉斯对行星运动非常感兴趣,但他也提出了潮汐运动的动态系统理论和概率论。有趣的是,拉普拉斯曾是拿破仑·波拿巴的考试官,在 1784 年拿破仑在巴黎的军事学校就读时,拉普拉斯担任了他的考官。

拉普拉斯最著名的贡献之一是继承法则,描述的是给定过去的事件,某个事件发生的概率。他提出的日出例子用来说明继承法则,就是给定太阳过去升起的天数,明天太阳升起的概率:

拉普拉斯对日出问题的假设是,我们对这件事的了解仅限于用于公式的观察天数。他实际上警告过,按照这个背景应用规则是错误的,因为我们对太阳和地球的运动了解要远远超过这些。

受天文计算的启发,奥古斯丁-路易·柯西于 1847 年发明了梯度下降优化算法(发表于法国科学院的期刊《法国科学院公报》),其中通过反向梯度的重复步伐来寻找局部最小值。

随后出现了许多其他的优化和曲线拟合创新。1944 年,由肯尼斯·莱文伯格首次发布,1963 年由唐纳德·马奎特重新发现的莱文伯格-马奎特算法(也称为阻尼最小二乘法)可用于曲线拟合问题,其中因变量是模型参数的非线性组合(非线性问题)。它结合了高斯-牛顿算法,这是高斯在 1809 年发布的牛顿算法的一种变体,以及大约一百年前发明的梯度下降法。

经济学

威廉·普雷费尔于 1759 年出生在苏格兰,来自一个牧师家庭,是家中的第四个儿子。他与安德鲁·梅克尔(打谷机的发明者)成为学徒,之后成为詹姆斯·瓦特在博尔顿与瓦特蒸汽机厂的私人助理。

他的生活如此多彩,以至于可以写成几本小说。1789 年,他参与了巴黎攻占巴士底狱的事件。之后,他作为威廉·杜尔(William Duer)的一名代理人,参与了一个可能是挪用公款的计划,与 Scioto 公司一起向愿意移民的法国人出售价值无用的俄亥俄州土地契约。回到伦敦后,他开设了一家银行,最终破产。后来,他因欠债被关进了债务人监狱——弗利特监狱,关了几年。之后,他成为了英国的秘密特工,从 1789 年到 1796 年,伪造法国货币“指定票”以破坏法国政府。指定票很快变得毫无价值,通货膨胀进一步削弱了法国政府。他还为金属加工机械和船只获得了几项发明专利。

然而,普雷福的主要成就之一,是他推广了几种可视化图表,如饼图、柱状图和时间序列图。虽然尼古拉·奥雷姆(Nicole Oresme)在几百年前的出版物中就展示了柱状图,但普雷福有时被认为是柱状图的发明人。

这里有两张图表,分别是柱状图和时间序列图,均来自他于 1786 年出版的《商业与政治地图集》(图片来源:维基百科):

图 1.5:普雷福的《商业与政治地图集》中的可视化图表

左侧是普雷福用来进行苏格兰进出口数据定量比较的柱状图。右侧是时间序列图,用来展示英国的贸易平衡随时间变化的情况。

气象学

古希腊哲学家亚里士多德是第一个撰写关于天气及其测量的文献的人;然而,直到很久以后,才开始有了第一次的天气预报。副海军上将罗伯特·菲茨罗伊(Robert FitzRoy)于 1854 年创立了英国的国家气象局——气象办公室。菲茨罗伊已经在历史上占据了重要地位,他曾是贝格尔号(HMS Beagle)的舰长,这艘船带着刚刚毕业的博物学家查尔斯·达尔文(Charles Darwin)环游世界,且在进化论和自然选择理论的形成过程中起到了关键作用。

在电报和气压计(气压计的一种形式)的支持下,气象局收集了伦敦不同地点的天气数据。1859 年,当皇家查尔特号蒸汽快船从澳大利亚墨尔本返回利物浦时,在风暴中在威尔士海岸的岩石上触礁,导致约 450 人丧生。这场灾难促成了风暴预警系统的发展,后来这一系统被扩展到一般天气预报。事实上,正是 FitzRoy 创造了forecast(预报)这个词,尽管当时许多同代人称其为“庸医般的天气预言”。目前尚不清楚他的预报是否遵循了任何系统。他因这一工作而遭到科学界的广泛嘲笑,特别是由 Sir Francis Galton 批评,他曾出版过一本名为《Meteorographica》的书,并后来出版了第一张天气图。FitzRoy 在 1865 年割喉自杀。风暴预警系统暂时中断,几年前才恢复并延续至今。

第一个使用大气和海洋的天气模型是在 1920 年代由 Lewis Fry Richardson 尝试的,他的工作基于挪威人 Vilhelm Bjerknes 的研究,后者是斯德哥尔摩大学流体动力学和热力学微分方程讲师。然而,在计算机出现之前,这些模型并不实际可行——Richardson 花了大约六周的时间进行有限区域的天气预报。尽管他的预测方法基本正确,但由于数值不稳定性,他的预测最终是错误的。当他意识到自己的工作可能会对化学武器设计者有价值时,他放弃了这项工作。

第一个计算机化的天气模型是在电子数值积分和计算机ENIAC)上编程的。ENIAC 由 John Mauchly 和 J. Presper Eckert 设计,能够运行任意的操作序列;然而,它并不是从磁带读取程序,而是通过插线板开关来读取。这个 15x9 米的巨型机器今天展览于华盛顿特区的史密森学会。ENIAC 由 17,500 个真空管组成,最初用于氢弹的计算,然后被用于采用新的数值天气预测方法将天气预报延伸到一天或两天以上。该计算机由 Klara von Neumann 编程。

这是 ENIAC 的照片(来源:维基共享资源):

图 1.6:电子数值积分和计算机(ENIAC)

你可以看到 Betty Snyder,ENIAC 的最早程序员之一,站在 ENIAC 前面。

后来,约瑟夫·斯马戈林斯基(Joseph Smagorinsky)和道格拉斯·利利(Douglas Lilly)开发了一个用于计算流体力学的湍流数学模型。这个模型——斯马戈林斯基-利利模型,至今仍在使用,使用了关于风、云层覆盖、降水、大气压力和地球与太阳辐射的数据作为输入。斯马戈林斯基继续领导关于全球变暖的研究,研究气候对二氧化碳浓度增加的敏感性。

移动传感器阵列和计算机模型的引入大大提高了预测的准确性。气象局或其他来源(最重要的是商业飞机飞行时收集)的传感器收集了有价值的温度和风速数据。今天,在七天的窗口期内,天气预报大约 80%的时间是准确的。COVID 大流行期间,由于某些时期航班减少了约 75%,商业航班的停飞导致近期预测准确性降低。

医学

1901 年,威廉·恩特霍芬(Willem Einthoven)将电报接收器中使用的弦式电流计应用于生理学。恩特霍芬在荷兰莱顿工作,他改进了之前的设计,制造了首个实用的心电图(ECG)。心电图对监测和筛查心脏功能至关重要,它可以检测心律失常、冠状动脉血流不足和电解质紊乱。由于这项创新的重要性,恩特霍芬于 1924 年获得了诺贝尔生理学或医学奖。

汉斯·伯格(Hans Berger)在 1924 年记录了首个人体脑电图(EEG)记录。EEG 通过将电极放置在头皮上来测量大脑的电活动。脑电图记录展示了大脑在一段时间内的自发电活动。

这是一个脑电图(EEG)信号的图表(来自德国 DHBW 的 Oliver Roesler 上传的 EEG Eye State 数据集):

eeg_signal.png

图 1.7:脑电图信号

在脑电图中,大脑的电活动通过放置在头皮上的电极进行记录。其信号通常表现为强烈的振荡(也称为脑波),覆盖多个频率范围,最显著的有:

  • 阿尔法波(8-12 赫兹)通常发生在放松状态下,尤其是在闭眼时

  • 贝塔波(16-31 赫兹)信号表示更加活跃的思维

  • 伽玛波(大于 32 赫兹)表示跨模态的感官处理

脑电图的医学用途非常广泛——除了其他用途外,EEG 可用于诊断癫痫、睡眠障碍、肿瘤、中风、麻醉深度、昏迷和脑死亡。

应用统计学

应用统计学和数学也为时间序列的工作提供了灵感和基础。托马斯·贝叶斯(Thomas Bayes)牧师(发音为 /bez/)证明了一个定理,该定理描述了基于先验知识的事件发生的概率。贝叶斯定理被认为是贝叶斯推断的基础,这是一种统计推断方法。

我们将在第九章时间序列的概率模型中进一步讨论这一点。贝叶斯的手稿在他去世后两年(1761 年)由他的朋友理查德·普赖斯以大量编辑过的形式呈交给皇家学会。

傅里叶变换在滤波中非常重要,它将信号从时间域转换为频域表示。函数的三角分解由约瑟夫·傅里叶于 1807 年发现,但一个快速算法最早由高斯在 1805 年左右发明(尽管直到他去世后才发表,且是以拉丁文发表的),然后 160 年后由 J.W. Cooley 和约翰·图基重新发现。

经典的时间序列建模方法由乔治·博克斯和吉威廉·詹金斯于 1970 年在他们的著作《时间序列分析、预测与控制》中提出。最重要的是,他们正式化了 ARIMA 和 ARMAX 模型,并描述了如何将其应用于时间序列预测。我们将在第五章利用移动平均和自回归模型进行预测中讨论这些模型。

Python 在时间序列中的应用

对于时间序列,主要有两种语言,R 和 Python,值得简要比较这两者,并描述是什么让 Python 与众不同。Python 是目前最受欢迎的编程语言之一。根据 2021 年 2 月的 TIOBE 数据,它仅次于 C 和 Java。

排名 语言 评分
1 C 16.34%
2 Java 11.29%
3 Python 10.86%
4 C++ 6.88%
... ... ...
11 R 1.56%
... ... ...
29 Julia 0.52%

图 1.8:TIOBE 语言使用统计数据

我加入了 R 和 Julia 这两种用于数据科学的其他语言,以支持 Python 是最受欢迎的数据科学语言这一观点。在比较 Python、R 和 Julia 的搜索量时,我们可以看到,Python 远远超过 R,而 Julia 则排在第三位,遥不可及。实际上,Python 的排名类似于 C、Java 和 C++等语言。而 R 的排名则接近汇编语言和 Groovy,Julia 则处于 Prolog 等专业语言的水平。

R 的社区由统计学家和数学家组成,R 的优势在于统计学和绘图(ggplot)。R 的弱点在于其工具支持以及几乎没有一致的代码风格规范。

另一方面,Python 在统计学和科学计算方面追赶上了,通过 NumPy、SciPy 和 pandas 等库,它在数据科学的使用率和可用性方面已经超越了 R。

Python 在机器学习库方面非常突出。以下库完全或主要是用 Python 编写的:

  • Scikit-learn 是用 Python 和 Cython(类似于 C 编程语言的 Python 方言)编写的。它提供了一个非常大范围的算法实现,用于训练和评估机器学习模型。

  • Statsmodels 提供了统计测试和模型,如广义线性模型(GLM)、ARMA 等。

  • Keras 是一个用于训练神经网络的 Python 抽象,它与 TensorFlow 和其他库进行交互。

一些最受欢迎的机器学习框架——那些在开发中得到广泛使用并具有大量可扩展算法的框架,例如 TensorFlow、PyTorch 和 XGBoost——也主要用 Python 编写,或为 Python 提供一流的接口。

此外,作为一种通用语言,Python 非常适合在你想超越数据分析的情况下使用。借助 Python,你可以实现构建端到端机器学习系统所需的完整数据流,并能够将其部署并与公司后端平台集成。

以下时间序列图展示了根据 Google Trends 的数据显示,Python 和 R 的流行程度。由于 Julia 几乎没有在图表底部出现,因此被省略。

最近,COVID 疫情影响了 Python 的流行度,但其他编程语言的搜索量也出现下降。

python_timeline.png

图 1.9:Python 与 R 的使用情况随时间变化

Python 在过去几年明显超过了 R,尽管必须承认这种比较并不完全公平,因为 Python 的应用范围远远大于 R。然而,Python 也是数据科学(尤其是时间序列)中最受支持的语言之一。到 2021 年 2 月,如果我们在 GitHub 上搜索时间序列,发现其库数量大约是 R 的五倍(包括 Jupyter 笔记本的库)。对于 Julia,我发现大约有 104 个库。具体数字请见下表:

语言 时间序列库
Jupyter Notebook 11,297
Python 4,891
R 3,656
Julia 104

图 1.10:TIOBE 语言使用统计数据

为了简要展示专注于时间序列的 Python 机器学习项目,以下是 GitHub 上一些著名库的简短列表:

  • prophet

  • sktime

  • gluon-ts

  • tslearn

  • pyts

  • seglearn

  • darts

  • cesium

  • pmdarima

这些截图(来自 gitcompare.com)总结了这些库的一些统计数据,如星标数(有多少人喜欢该库)、分支数(有多少人复制了该库以便研究或做更改)、库的年龄(库存在了多长时间)以及其他:

/Users/ben/Dropbox/vimwiki/_html/assets/time-series-libraries1.png/Users/ben/Dropbox/vimwiki/_html/assets/time-series-libraries2.png

图 1.11:著名 Python 库的库统计数据

在本书中,我们将介绍许多时间序列库。接下来的章节中,我们会讨论一些 Python 数据科学库,但如果你想全面了解这些库中的任何一个,你应该阅读专门介绍 Python 数据科学的书籍,甚至是 NumPy 和 pandas 的书籍。

安装库

本书中,你将需要的用于维护和安装 Python 库的两个主要工具是 conda 和 pip。

请注意,接下来两个小节中的命令应该从系统终端执行,或者 – 在 conda 的情况下 – 使用 Anaconda 导航器。对于 Windows 和 Mac 用户,有图形用户界面可用,可以搜索并安装库,而不需要依赖终端。

conda 与 Python、R 和其他语言一起工作,用于依赖管理和环境封装。conda 还通过维护与 Python 库相关的库列表来帮助安装系统库。

开始使用 conda 的最佳方式是按照以下链接中的说明安装 anaconda:docs.continuum.io/anaconda/install/

还有一个带有精美设计的 conda 图形界面,如下截图所示:

anaconda%20navigator.png

图 1.12:Anaconda 导航器

Anaconda 导航器可以在 macOS 和 Microsoft Windows 上安装。

或者,你可以完全依赖终端。例如,下面是如何从终端安装 NumPy 库:

conda install numpy 

另外,如果你想使用 R 编程语言,也可以使用 conda:

conda install r-caret 

请参阅 conda 文档,获取详细的介绍和教程。conda 还会安装 Python 和 pip 的版本,所以你可以使用 pip 或 conda 安装 Python 库,同时通过 conda 管理你的环境。

终端命令可以从系统终端或从 Jupyter 环境中执行,在 notebook 或 JupyterLab 中,通过在命令前加上感叹号。

例如,来自终端中的一个命令:

pip install xgboost 

可以在 Jupyter 环境中按如下方式编写:

!pip install xgboost 

在 Jupyter 中的感叹号告诉解释器这是一个 shell 命令。在 Jupyter 的最新版本中,使用 pip 命令时已不再需要感叹号。

让我们快速看看如何在终端中启动 Python 并安装库的简单会话:

terminal.png

图 1.13:终端窗口

pip 是 Python 库的包管理器。以下是来自终端的一些有用命令:

# install NumPy: 
pip install numpy
# install a particular version:
pip install numpy==1.20.0
# upgrade a library:
pip install -U numpy
# install all libraries listed in a requirements file:
pip install -r production/requirements.txt
# write a list of all installed libraries and their versions to a file:
pip freeze > production/requirements.txt 

你可以安装不同版本的 Python 和 pip 以及不同版本的库。这些可以作为环境进行维护,你可以在它们之间进行切换。Virtualenv 是一个用于维护环境的工具:

# create a new environment myenv:
virtualenv myenv
# activate the myenv environment:
source myenv/bin/activate
# install dependencies or run python, for example:
python
# leave the environment again:
deactivate 

activate命令将会修改你的$PATH变量,指向virtualenv bin/目录,其中包含 Python 和 pip 可执行文件等内容。这意味着你可以使用这些作为选项。通常,你应该会看到提示符反映这一变化。

请注意,对于环境的激活,你可以使用完整路径或相对路径。在 Windows 中,激活命令略有不同——你需要运行一个 shell 脚本:

# activate the myenv environment:
myenv\Scripts\activate.bat 

Jupyter 笔记本和 JupyterLab

Jupyter 代表 Julia、Python、R。它是一个用于运行这些和其他支持语言(如 Scala 和 C)脚本的交互式平台。

你可以通过终端在电脑上启动一个笔记本服务器,像这样:

jupyter notebook 

你应该会看到浏览器打开一个新标签页,显示 Jupyter 笔记本。我加载数据科学语言时间序列的笔记本开头大概是这样的:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/(A Document Being Saved By screencaptureui 3)/Screenshot 2021-03-11 at 17.29.18.png

图 1.14:Jupyter 笔记本

或者,我们也可以使用 JupyterLab,这是下一代笔记本服务器,带来了显著的可用性改进。

你可以通过终端启动一个 JupyterLab 笔记本服务器,像这样:

jupyter lab 

JupyterLab 看起来与默认的 Jupyter 服务器有些不同,正如下面的截图所示(来自 JupyterLab 的 GitHub 仓库):

i_glow_up

图 1.15:JupyterLab

这两个工具,无论是 Jupyter 笔记本还是 JupyterLab,都将为你提供一个集成开发环境IDE),用于编写本书中将介绍的代码。

最后,了解如何在 Jupyter 内部获得帮助是非常实用的。这时,问号就派上用场了。问号(?)用于在笔记本中提供帮助,像这样:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_jyFfFP/Screenshot 2021-06-26 at 22.06.11.png

图 1.16:笔记本内帮助

如果你想访问函数的签名或完整的源代码列表,也可以在函数名后加一个或两个问号。这项功能可以节省大量时间——你不需要再去 Google 搜索代码或类、函数的定义,可以在毫秒内直接获取信息。

NumPy

NumPy 是 Python 科学计算的基础库,因为许多库都依赖它。像 PyTorch 和 TensorFlow 这样的库提供了与 NumPy 的接口,使得数据的导入/导出变得非常简单。pandas 基本上是一个围绕 NumPy 数组构建的高级接口。

SciPy 也建立在 NumPy 之上。SciPy 代表科学 Python,包含从数学常数到积分、优化、插值、信号处理等功能。

NumPy 允许你在不同维度上操作矩阵并对其进行计算。你可能主要使用 pandas 或其他库,并不常接触 NumPy;然而,为了更深入的理解和高性能,了解 NumPy 无疑是非常重要的。

以下是 NumPy 中的一些基本命令。这些命令应该在 Python 解释器中执行。我们将创建一维和二维数组:

import numpy as np
# 1 dimensional array:
x1 = np.array([1, 2, 3])
>>> array([0, 1, 2])
x2 = np.arange(3)
>>> array([0, 1, 2])
x1 == x2 
>>> True
# 2 dimensional array: 
y = np.array([(1, 2, 3),(4, 5, 6)]) 

NumPy 提供了非常方便的文档功能;例如,要检索 optimize.fmin 函数的文档,可以使用此命令(为简洁起见,我省略了一些行):

>> np.info(optimize.fmin)
fmin(func, x0, args=(), xtol=0.0001, ftol=0.0001, maxiter=None, maxfun=None,full_output=0, disp=1, retall=0, callback=None)
Minimize a function using the downhill simplex algorithm.
Parameters
----------
func : callable func(x,*args)
    The objective function to be minimized.
x0 : ndarray
    Initial guess.
args : tuple
    Extra arguments passed to func, i.e. ``f(x,*args)``.
callback : callable
    Called after each iteration, as callback(xk), where xk is the
    current parameter vector.
Returns
-------
xopt : ndarray
    Parameter that minimizes function.
fopt : float
    Value of function at minimum: ``fopt = func(xopt)``.
iter : int
    Number of iterations performed.
…
Notes
-----
Uses a Nelder-Mead simplex algorithm to find the minimum of function of
one or more variables. 

pandas

pandas 是一个允许通过列名等索引访问矩阵或数组作为表格的库——这称为 DataFrame。单个列或单个行可以作为 Series 访问,Series 是 pandas 中的另一种数据类型。这些 Series 实际上是 NumPy 数组。

pandas 库包含用于从 CSV、Excel 和许多其他格式导入和导出数据的函数和类;用于选择和切片数据的函数;以及类似于结构化查询语言(SQL)的合并、连接、分组和聚合函数。你还可以直接从 pandas 绘制图表,因为 pandas 集成了 matplotlib,但它也与其他绘图库如 bokeh 一起使用:

import pandas as pd
# read a csv file:
df = pd.read_csv('value.csv')
# find how many rows in a dataframe:
len(df)
# return the head or tail of a dataframe:
df.head()
df.tail()
# print the full dataframe:
with pd.option_context(
  'display.max_rows', None,
  'display.max_columns', None
):
  print(df)
# create a dataframe:
df2 = pd.DataFrame({"A": [1, 2], "B": [3, 4]})
# plot two columns against each other:
df2.plot(x='A', y='B', kind='scatter')
# save the dataframe to a csv:
df2.to_csv('new_file.csv', index=False)
# output to NumPy matrix:
df2.to_numpy() 

上一条命令的输出应该如下所示:

array([[1, 3],
       [2, 4]]) 

Python 中的最佳实践

在本节中,我想谈谈良好的编码规范。关于这个话题已经有整本书籍写成,本节内容无法全面涵盖;不过,我的目标是至少提供一些基础要点和提示。对于初学者以上的编码,尤其是在企业环境或任何组织中,良好的实践显得尤为重要。

写出通用代码使其便于维护和增强并不容易,这需要经验。只有以其他人能够读懂的方式表达想法的代码,才对团队有用。最重要的原则之一是 DRY(不要重复自己),通过减少重复,使每个功能在系统中找到其独特的表示方式。

这不是一个完整的列表,但其他一些原则包括以下内容:

  • 文档

  • 依赖管理

  • 代码验证

  • 错误处理

  • 测试(特别是单元测试)

其中一些原则已经有整本书籍来阐述,细节内容超出了本节的范围。每一个原则在你进入生产环境时都至关重要,以确保你的代码可靠。

每当我回到自己的项目中,无论是工作还是私人生活,我都会觉得自己是个傻瓜,意识到自己没有写足够的文档。发生这种情况时,我不得不花费精力重新构建代码的正确思维表示。如果做得正确,编写文档可以帮助你进入工作状态。其他人阅读你的代码,或者几个月后重新查看的你自己,会很高兴你写了文档,尤其是针对函数、类和模块(文档字符串)。

依赖封装意味着你的代码是隔离的、可移植的,并且是可重现的。在过去几年中,Python 中用于管理依赖关系和环境的两个主要工具是 conda 和 pip,这些我们在前面的章节中已经提到过。

风格和约定的混乱会使任何项目变得一团糟,不仅难以阅读,也难以维护。Python 最重要的编码风格之一是 Python 增强提案 8,简称 PEP 8。你可以在bit.ly/3evsgIW找到 PEP 8 的风格指南。

一些工具已经被开发出来,用于检查 Python 代码是否遵循 PEP 8(以及一些额外的约定)。这些工具可以帮助你使代码更具可读性和可维护性,同时节省时间和脑力;例如,Flake8、Black、mypy 或 Pylint。Flake8 和 Pylint 不仅检查编码风格,还检查常见的编码错误和潜在的 bug。如果你想对 Python 脚本运行 Flake8 测试,你可以例如输入:

flake8 --shore-source --show-pep8 myscript.py 

Black 可以提醒你格式问题,或自动修复文件、模块甚至整个项目中的格式问题。pydocstyle 检查文档的存在以及文档是否符合文档风格指南。

此外,由一些高水平项目的开发人员创建了更深入的开发和编码风格,并且这些风格非常具有指导性。scikit-learn 项目的指南可以在bitly.com/3etFrtz找到。对于 pandas,你可以在bit.ly/2OlpCKZ比较风格。

单元测试是一种为模块、类和其他代码单元设置测试的方法。Python 中最受欢迎的单元测试库之一是 pytest。你可以在 pytest 文档中了解更多关于 pytest 的信息:docs.pytest.org/en/stable/

摘要

在本章中,我们介绍了时间序列、时间序列研究的历史以及 Python 在时间序列中的应用。

我们从时间序列的定义及其主要特性开始。然后,我们回顾了时间序列在不同科学学科中的研究历史,如人口学和遗传学、天文学、经济学、气象学、医学和应用统计学。

然后,我们介绍了 Python 在时间序列分析方面的功能,并探讨了为什么 Python 是进行时间序列机器学习的首选语言。最后,我描述了如何安装和使用 Python 进行时间序列分析和机器学习,并介绍了与时间序列和机器学习相关的 Python 基础知识。

在下一章,我们将讨论使用 Python 进行时间序列分析。

第二章:使用 Python 进行时间序列分析

时间序列分析围绕熟悉数据集并提出想法和假设展开。它可以被看作是“数据科学家的讲故事”,并且是机器学习中的一个关键步骤,因为它可以为在训练机器学习模型时测试的假设和结论提供信息和帮助。大致而言,时间序列分析和机器学习之间的主要区别在于,时间序列分析不包括正式的统计建模和推断。

尽管这可能让人感到不知所措,似乎很复杂,但它通常是一个非常结构化的过程。在本章中,我们将通过 Python 来处理时间序列模式的基础知识。在 Python 中,我们可以通过交互式查询数据,使用我们手头的一些工具来进行时间序列分析。这从创建和加载时间序列数据集开始,到识别趋势和季节性。我们将概述时间序列分析的结构,并通过例子展示在 Python 中的理论和实践组成部分。

主要的例子将使用伦敦和德里的空气污染数据集。你可以在本书的 GitHub 仓库中找到这个例子,作为 Jupyter notebook。

我们将涵盖以下主题:

  • 什么是时间序列分析?

  • 在 Python 中处理时间序列

  • 理解变量

  • 揭示变量之间的关系

  • 识别趋势和季节性

我们将从时间序列分析的特征化和定义开始。

什么是时间序列分析?

时间序列分析TSA)一词指的是时间序列的统计方法,或趋势和季节性分析。它通常是一个 临时的 探索性分析,通常涉及可视化分布、趋势、周期模式以及特征之间、特征与目标之间的关系。

更一般地说,我们可以说 TSA 大致是针对时间序列数据的 探索性数据分析EDA)。然而,这种比较可能会误导,因为 TSA 可以同时包含描述性和探索性元素。

我们快速来看一下描述性分析和探索性分析之间的区别:

  • 描述性分析总结数据集的特征

  • 探索性分析分析模式、趋势或变量之间的关系

因此,TSA 是对数据集的初步调查,目的是发现模式,特别是趋势和季节性,并获得初步的见解、检验假设,并提取有意义的总结性统计数据。

定义:时间序列分析(TSA)是从时间序列中提取摘要和其他统计信息的过程,最重要的是趋势和季节性的分析。

由于 TSA 的一个重要部分是通过可视化收集统计数据并以图形方式表示数据集,因此在本章中,我们将进行很多绘图。许多在本章中描述的统计数据和图形是特定于 TSA 的,因此即使你熟悉 EDA,你也会发现一些新的内容。

TSA 的一部分是收集和审查数据,检查变量的分布(以及变量类型),并检查错误、异常值和缺失值。某些错误、变量类型和异常可以得到修正,因此 EDA 通常与预处理和特征工程一起进行,其中选择和转换列及字段。从数据加载到机器学习的整个过程是高度迭代的,可能会在不同阶段涉及多个 TSA 实例。

下面是处理时间序列时的几个关键步骤:

  • 导入数据集

  • 数据清理

  • 理解变量

  • 揭示变量之间的关系

  • 识别趋势和季节性

  • 预处理(包括特征工程)

  • 训练机器学习模型

导入数据可以看作是 TSA 之前的步骤,而数据清理、特征工程和训练机器学习模型并不严格属于 TSA。

导入数据包括解析,例如提取日期。TSA 的三个核心步骤是理解变量、揭示变量之间的关系以及识别趋势和季节性。每一个步骤都有很多内容要讲解,在本章中,我们将在专门的章节里更详细地讨论它们。

属于 TSA 的步骤,导向预处理(特征工程)和机器学习的过程是高度迭代的,可以通过以下时间序列机器学习飞轮清晰地看到:

Flywheel%20-%20page%201.png

图 2.1:时间序列机器学习飞轮

这个飞轮强调了工作的迭代性质。例如,数据清理通常在加载数据后进行,但在我们对变量有了新的发现后,数据清理会再次出现。我已将 TSA 高亮显示为深色,而不是严格属于 TSA 的步骤则用灰色显示。

让我们从一些实际操作开始吧!我们将从加载数据集开始。在导入数据后,我们会提出一些问题,比如数据集的大小是多少(观察值的数量)?我们有多少个特征或列?这些列的类型是什么?

我们通常会查看直方图或分布图。为了评估特征与目标变量之间的关系,我们会计算相关性,并将其可视化为相关性热图,其中变量之间的相关强度会映射为不同的颜色。

我们会查找缺失值——在电子表格中,这些通常是空单元格——然后清理并尽可能修正这些不规则之处。

我们将分析变量之间的关系,在时间序列分析(TSA)中,它的一个特殊之处是我们需要研究时间与每个变量之间的关系。

通常,区分不同技术的一种有用方式是区分单变量分析和多变量分析,以及图形化技术和非图形化技术。单变量分析意味着我们只关注单一变量。这意味着我们可以检查值以获取均值和方差,或者——在图形化方面——绘制分布图。我们将在 理解变量 部分总结这些技术。

另一方面,多变量分析意味着我们在计算变量之间的相关性,或者——在图形化方面——例如绘制散点图。我们将在 揭示变量之间的关系 部分深入探讨这些技术。

在继续之前,我们先了解一些 Python 时间序列的基础知识。这将涵盖时间序列数据的基本操作作为介绍。之后,我们将使用实际数据集通过 Python 命令进行操作。

在 Python 中处理时间序列

Python 有很多用于时间序列的库和包,比如 datetimetimecalendardateutilpytz,这可能会让初学者感到非常困惑。同时,还有许多不同的数据类型,如 datetimedatetimetzinfotimedeltarelativedelta 等。

在使用它们时,关键在于细节。举个例子:许多这些类型对时区不敏感。然而,你可以放心,入门时,熟悉这些库和数据类型的一个小子集就足够了。

要求

在这一章中,我们将使用几个库,可以通过终端(或类似的 Anaconda Navigator)快速安装:

pip install -U dython scipy numpy pandas seaborn scikit-learn 

我们将从 Python(或 IPython)终端执行命令,但同样也可以从 Jupyter notebook(或其他环境)中执行它们。

如果我们至少了解 datetimepandas 这两个非常重要的库,那是一个不错的开始,接下来我们将在后续两节中介绍它们。我们将创建基本对象并对其进行简单操作。

Datetime

datedatetime 数据类型在 Python 中不是原始类型,像数字(floatint)、stringlistdictionarytuplefile 那样。要使用 datedatetime 对象,我们必须导入 datetime,这是 Python 标准库的一部分,它是 CPython 和其他主要 Python 发行版默认带有的库。

datetime 提供了如 datedatetimetimetimedelta 等对象。datetimedate 对象的区别在于,datetime 对象除了日期外,还包含了时间信息。

要获取一个日期,我们可以这样做:

from datetime import date 

要获取今天的日期:

today = date.today() 

要获取其他日期:

other_date = date(2021, 3, 24) 

如果我们想要一个datetime对象(时间戳),我们也可以这样做:

from datetime import datetime
now = datetime.now() 

这将获取当前的时间戳。我们也可以为特定的日期和时间创建一个datetime

some_date = datetime(2021, 5, 18, 15, 39, 0)
some_date.isoformat() 

我们可以得到一个 isoformat 的字符串输出:

'2021-05-18T15:39:00' 

isoformat 是 ISO 8601 格式的缩写,是表示日期和时间的国际标准。

我们还可以使用timedelta处理时间差:

from datetime import timedelta 
year = timedelta(days=365) 

这些timedelta对象可以添加到其他对象进行计算。例如,我们可以使用timedelta对象进行计算:

year * 10 

这应该给我们以下输出:

datetime.timedelta(days=3650) 

datetime 库可以将字符串输入解析为datedatetime类型,并将这些对象输出为string

from datetime import date
some_date = date.fromisoformat('2021-03-24') 

或者:

some_date = datetime.date(2021, 3, 24) 

我们可以使用字符串格式选项格式化输出,例如像这样:

some_date.strftime('%A %d. %B %Y') 

这将给我们:

'Wednesday 24\. March 2021' 

同样地,我们可以从字符串中读取datedatetime对象,并且可以使用相同的格式选项:

from datetime import datetime
dt = datetime.strptime('24/03/21 15:48', '%d/%m/%y %H:%M') 

你可以在这里找到完整的格式选项列表,既可以用于解析字符串,也可以用于打印datetime对象:strftime.org/

这个表中列出了一些重要的内容:

格式字符串 含义
%Y 四位数表示的年份
%y 两位数表示的年份
%m 月份的数字表示
%d
%H 两位数表示的小时
%M 两位数表示的分钟

图 2.2:日期格式字符串

记住这些带有格式选项的字符串很有用。例如,用斜杠分隔的美国日期的格式字符串看起来像这样:

'%d/%m/%Y' 

pandas

在上一章中我们介绍了 pandas 库。pandas 是 Python 生态系统中数据科学中最重要的库之一,用于数据处理和分析。最初发布于 2008 年,它已成为 Python 成功的重要驱动因素之一。

pandas 具有重要的时间序列功能,如日期范围生成、频率转换、移动窗口统计、日期偏移和滞后。

让我们通过一些基础知识来创建一个时间序列,如下所示:

import pandas as pd
pd.date_range(start='2021-03-24', end='2021-09-01') 

这给我们一个像这样的DateTimeIndex

DatetimeIndex(['2021-03-24', '2021-03-25', '2021-03-26', '2021-03-27',
               '2021-03-28', '2021-03-29', '2021-03-30', '2021-03-31',
               '2021-04-01', '2021-04-02',
               ...
               '2021-08-23', '2021-08-24', '2021-08-25', '2021-08-26',
               '2021-08-27', '2021-08-28', '2021-08-29', '2021-08-30',
               '2021-08-31', '2021-09-01'],
              dtype='datetime64[ns]', length=162, freq='D') 

我们还可以按如下方式创建一个时间序列:

pd.Series(pd.date_range("2021", freq="D", periods=3)) 

这将给我们一个类似这样的时间序列:

0   2021-01-01
1   2021-01-02
2   2021-01-03
dtype: datetime64[ns] 

正如你所见,这种类型称为DatetimeIndex。这意味着我们可以使用这种数据类型来索引数据集。

其中最重要的功能之一是从string或单独的列解析到datedatetime对象:

import pandas as pd
df = pd.DataFrame({'year': [2021, 2022],
    'month': [3, 4],
    'day': [24, 25]}
)
ts1 = pd.to_datetime(df)
ts2 = pd.to_datetime('20210324', format='%Y%m%d') 

我们创建了两个时间序列。

你可以像这样进行滚动窗口计算:

s = pd.Series([1, 2, 3, 4, 5])
s.rolling(3).sum() 

你能猜到这个的结果吗?如果不能,为什么不在你的 Python 解释器中试试?

一个时间序列通常会是一个带有时间对象的索引和一个或多个带有数字或其他类型的列,如下所示:

import numpy as np 
rng = pd.date_range('2021-03-24', '2021-09-01', freq='D')
ts = pd.Series(np.random.randn(len(rng)), index=rng) 

我们可以看一下我们的时间序列:

2021-03-24   -2.332713
2021-03-25    0.177074
2021-03-26   -2.136295
2021-03-27    2.992240
2021-03-28   -0.457537
                 ...
2021-08-28   -0.705022
2021-08-29    1.089697
2021-08-30    0.384947
2021-08-31    1.003391
2021-09-01   -1.021058
Freq: D, Length: 162, dtype: float64 

我们可以像处理任何其他 pandas Series 或 DataFrame 一样索引这些时间序列数据集。ts[:2].index会给我们:

DatetimeIndex(['2021-03-24', '2021-03-25'], dtype='datetime64[ns]', freq='D') 

有趣的是,我们可以直接使用字符串或 datetime 对象进行索引。例如,ts['2021-03-28':'2021-03-30']会给我们:

2021-03-28   -0.457537
2021-03-29   -1.089423
2021-03-30   -0.708091
Freq: D, dtype: float64 

你可以使用shift方法将时间序列的值向前或向后移动。这会改变数据的对齐方式:

ts.shift(1)[:5] 

我们还可以改变时间序列对象的分辨率,例如这样:

ts.asfreq('M') 

请注意datetimepd.DateTimeIndex之间的区别。尽管它们编码的是相同类型的信息,但它们是不同的数据类型,并且可能并不总是能很好地互相配合。因此,我建议在进行比较时总是显式地转换数据类型。

在下一节中,让我们通过一个基本的示例来演示如何导入时间序列数据集,获取汇总统计数据,并绘制一些变量。

了解变量

我们将加载一个空气污染的时间序列数据集,然后对变量进行一些非常基础的检查。

这一步是对每个变量单独执行的(单变量分析),可以包括每个变量的汇总统计数据、直方图、查找缺失值或异常值,并检验平稳性。

连续变量最重要的描述量是均值和标准差。提醒一下,均值和标准差的公式如下。我们稍后会基于这些公式构建更复杂的公式。均值通常指的是算术平均值,它是最常用的平均数,定义如下:

标准差是该均值的平均平方差的平方根:

标准误差SE)是样本数据标准差的近似值。它衡量的是样本均值围绕总体均值的离散程度,但通过样本量的平方根进行标准化。参与计算的数据点越多,标准误差通常越小。SE 等于标准差除以样本量的平方根:

标准误差(SE)的一个重要应用是估计均值的置信区间。置信区间为一个参数提供了一个值的范围。例如,95^(th) 百分位上置信限,,定义为:

同样,替换加号为减号,下置信区间定义为:

中位数是另一种平均数,特别适用于数据不能被均值和标准差准确描述的情况。比如数据有长尾、多个峰值,或者向某一方向偏斜。中位数定义如下:

这假设 X 按照升序或降序排列。然后,位于中间的值,正好在 处,是中位数。中位数是第 50^(th) 百分位数,意味着它高于 X 中恰好一半或 50% 的数据点。其他重要的百分位数是第 25^(th) 和第 75^(th),它们也分别是第一个 四分位数 和第三个四分位数。它们之间的差值被称为 四分位差

这些是最常见的描述符,但即使如此,它们也不是唯一的。我们在这里不会深入讨论,但稍后会看到更多的描述符。

让我们用一些代码来动手实践吧!

我们将导入 datetime、pandas、matplotlib 和 seaborn 以便稍后使用。Matplotlib 和 seaborn 是绘图的库。代码如下:

import datetime
import pandas as pd
import matplotlib.pyplot as plt 

然后我们将读取一个 CSV 文件。数据来自 Our World in Data (OWID) 网站,这是一个关于世界状况的统计和文章集合,由牛津大学经济学研究主任 Max Roser 维护。

我们可以加载本地文件或互联网上的文件。在这种情况下,我们将从 GitHub 加载一个数据集。这是一个关于空气污染物随时间变化的数据集。在 pandas 中,你可以直接将 URL 传递给 read_csv() 方法:

pollution = pd.read_csv(
    'https://raw.githubusercontent.com/owid/owid-datasets/master/datasets/Air%20pollution%20by%20city%20-%20Fouquet%20and%20DPCC%20(2011)/Air%20pollution%20by%20city%20-%20Fouquet%20and%20DPCC%20(2011).csv'
)
len(pollution) 
331 
pollution.columns 
Index(['Entity', 'Year', 'Smoke (Fouquet and DPCC (2011))',
       'Suspended Particulate Matter (SPM) (Fouquet and DPCC (2011))'],
      dtype='object') 

如果你在下载文件时遇到问题,可以通过手动从书籍的 GitHub 仓库 chapter2 文件夹下载。

现在我们知道数据集的大小(331 行)和列名。列名有点长,让我们通过重命名来简化它们,然后继续:

pollution = pollution.rename(
    columns={
        'Suspended Particulate Matter (SPM) (Fouquet and DPCC (2011))':            'SPM',
           'Smoke (Fouquet and DPCC (2011))' : 'Smoke',
        'Entity': 'City'
    }
)
pollution.dtypes 

这是输出结果:

City                                object
Year                                 int64
Smoke                              float64
SPM                                float64
dtype: object 
pollution.City.unique() 
array(['Delhi', 'London'], dtype=object) 
pollution.Year.min(), pollution.Year.max() 

最小年份和最大年份如下:

(1700, 2016) 

pandas 提供了很多方法来探索和发现数据集——min()max()mean()count()describe() 都非常实用。

City、Smoke 和 SPM 是更清晰的变量名称。我们已经知道数据集涵盖了伦敦和德里两座城市,以及 1700 年至 2016 年之间的时间段\。

我们将把 Year 列从 int64 转换为 datetime。这将有助于绘图:

pollution['Year'] = pollution['Year'].apply(
    lambda x: datetime.datetime.strptime(str(x), '%Y')
)
pollution.dtypes 
City             object
Year     datetime64[ns]
Smoke           float64
SPM             float64
dtype: object 

Year 现在是 datetime64[ns] 类型。它是 64 位的 datetime 类型。每个值描述一个纳秒,默认单位。

让我们检查是否有缺失值,并获取列的描述性统计信息:

pollution.isnull().mean() 
City                               0.000000
Year                               0.000000
Smoke                              0.090634
SPM                                0.000000
dtype: float64 
pollution.describe() 
 Smoke    SPM
count    301.000000    331.000000
mean     210.296440    365.970050
std      88.543288     172.512674
min      13.750000     15.000000
25%      168.571429    288.474026
50%      208.214286    375.324675
75%      291.818182    512.609209
max      342.857143    623.376623 

Smoke 变量有 9% 的缺失值。目前,我们可以专注于没有缺失值的 SPM 变量。

pandas 的 describe() 方法为我们提供了非空值的计数、均值和标准差、第 25、第 50 和第 75 百分位数,以及范围(最小值和最大值)。

直方图,最早由卡尔·皮尔逊提出,是在一系列范围(称为区间或桶)内的值的计数。变量首先被分为一系列区间,然后统计每个区间内的所有点数(区间计数)。我们可以通过条形图来直观地呈现这些计数。

让我们绘制 SPM 变量的直方图:

n, bins, patches = plt.hist(
    x=pollution['SPM'], bins='auto',
    alpha=0.7, rwidth=0.85
)
plt.grid(axis='y', alpha=0.75)
plt.xlabel('SPM')
plt.ylabel('Frequency') 

这是我们得到的图表:

pollution_hist.png

图 2.3:SPM 变量的直方图

如果你有连续的测量数据并且想要了解数值的分布,直方图可以提供帮助。此外,直方图还可以指示是否存在异常值。

这结束了我们 TSA 的第一部分。稍后我们会回到我们的空气污染数据集。

揭示变量之间的关系

如果我们不是在处理单变量时间序列(只有一个变量),那么变量之间的关系需要进行调查。这包括任何相关性的方向和大致大小。这一点非常重要,以避免特征泄漏和共线性。

特征泄漏是指一个变量无意中泄露了目标。例如,名为amount_paid的变量会泄露标签has_paid。一个更复杂的例子是,如果我们正在分析一个在线超市的数据,我们的数据集包括了诸如年龄、过去购买次数、访问时长以及他们购物车中的商品内容等客户变量。我们想要预测的目标是他们的购买决策结果,可能是放弃(取消购买)或已支付。我们可能会发现购买与购物车中的袋子有很高的相关性,仅仅是因为袋子是在最后一步添加的。然而,得出结论认为我们应该向客户提供袋子,当他们进入我们的网站时,可能就会错失重点,实际上决定变量可能是他们的停留时间,而通过小工具或客服干预可能更有效。

共线性指的是自变量(特征)之间存在相关性。在线性模型中,后一种情况可能会带来问题。因此,如果我们进行线性回归并发现两个变量之间高度相关,我们应该去除其中一个,或使用降维技术,如主成分分析(PCA)。

皮尔逊相关系数由卡尔·皮尔逊(Karl Pearson)开发,我们在前一章中已经讨论过他,并以他命名,以便将其与其他相关系数区分开来。两个变量XY之间的皮尔逊相关系数定义如下:

是两个变量之间的协方差,它定义为每个点与变量均值的差异的期望值(均值):

是变量X的标准差。

展开后,公式看起来是这样的:

相关性有三种类型:正相关、负相关和无相关性。正相关意味着当一个变量增加时,另一个也会增加。在皮尔逊相关系数的情况下,一个变量对另一个变量的增加应该是线性的。

如果我们查看从 1800 年开始的全球预期寿命图表,我们会看到随着时间轴的推移,预期寿命逐年增加。您可以看到基于 OWID 数据的全球预期寿命图:

life_expectancy.png

图 2.4:1800 年至今的预期寿命

我们可以看到,自 19 世纪末以来,预期寿命一直在稳步上升,直到今天。

这个图表被称为运行图或时间折线图

为了计算皮尔逊相关系数,我们可以使用 SciPy 中的一个函数:

from scipy import stats
def ignore_nans(a, b):
    index = ~a.isnull() & ~b.isnull()
    return a[index], b[index]
stats.pearsonr(*ignore_nans(pollution['Smoke'], pollution['SPM'])) 

这是皮尔逊相关系数和表示显著性的 p 值(越低越显著)

(0.9454809183096181, 3.313283689287137e-10 

我们看到时间与预期寿命之间有一个非常强的正相关,相关系数为 0.94,显著性非常高(返回值中的第二个数字)。您可以在 OWID 网站上找到有关数据集的更多细节。

相反,我们会看到时间与儿童死亡率之间的负相关关系——年份增加时,儿童死亡率降低。这个图表显示了每千名儿童的儿童死亡率,数据来自 OWID:

child_mortality.png

图 2.5:英国、法国和美国从 1800 年至今的儿童死亡率

在这个图表中,我们可以看到,自 19 世纪开始到今天,所有三个国家的儿童死亡率都在不断下降。

对于美国,我们将发现儿童死亡率与时间之间存在-0.95 的负相关。

我们还可以将这些国家相互比较。我们可以计算每个特征之间的相关性。在这种情况下,每个特征包含了三个国家的数值。

这给出了一个相关矩阵,是 3x3 的,我们可以将其可视化为热图:

correlation_heatmap.png

图 2.6:英国、法国和美国之间儿童死亡率的相关热图

在这张相关热图中,我们可以看到各国之间的高度相关性(例如,法国和英国之间的相关性为 0.78)。

相关矩阵的对角线总是 1.0,并且矩阵沿对角线是对称的。因此,有时我们只显示对角线下方的下三角(或者有时显示上三角)。我们可以看到,英国的儿童死亡率与美国更为相似,而不是与法国。

这是否意味着英国经历了与美国相似的发展?这些统计数据和可视化常常会引发问题,或者提出我们可以检验的假设。

如前所述,不同数据集的完整笔记本已在 GitHub 上提供,但这里是热图的代码片段:

import dython
dython.nominal.associations(child_mortality[countries], figsize=(12, 6)); 

相关系数在增加非线性或不连续,或(由于平方项)出现异常值的情况下难以发挥作用。例如,如果我们从 18 世纪开始观察空气污染,我们会看到煤炭带来的空气污染急剧增加,而随着蒸汽机的引入,污染物反而有所减少。

散点图可以用来显示和比较数值。它将两个变量的值相互对比。通常,变量是数值型的——否则我们称之为表格。如果散点图在某些区域过于拥挤,可能会产生误导,因此如果不能在视觉上识别这一点,图表就会具有误导性。通过添加抖动和透明度可以在一定程度上改善这种情况,然而,我们还可以将散点图与我们正在对比的变量的直方图结合,这样我们就可以看到在每个变量上有多少点被显示出来。散点图通常会有一条最佳拟合线,以便可视化一个变量是如何依赖于另一个变量的。

这是如何绘制带有边际直方图的散点图的一个示例,数据来自污染数据集:

plt.figure(figsize=(12, 6))
sns.jointplot(
    x="Smoke", y="SPM",
    edgecolor="white",
    data=pollution
)
plt.xlabel("Smoke")
plt.ylabel("SPM"); 

这是生成的图表:

Machine-Learning%20for%20Time-Series%20with%20Python/spm_scatter.png

图 2.7:带有边际直方图的散点图,显示烟雾与 SPM 的关系

在散点图中,我们可以看到这两个变量非常相似——所有的数值都位于对角线上。这两个变量之间的相关性完美无缺,达到 1.0,这意味着它们实际上是相同的。

我们之前看过悬浮颗粒物SPM)的数据集。现在让我们将 SPM 随时间变化的情况绘制出来:

pollution = pollution.pivot("Year", "City", "SPM")
plt.figure(figsize=(12, 6))
sns.lineplot(data=pollution)
plt.ylabel('SPM'); 

这是该图表:

Machine-Learning%20for%20Time-Series%20with%20Python/spm_1700_to_today.png

图 2.8:从 1700 年代到今天的悬浮颗粒物

我们可以在图中看到,伦敦的空气质量(以悬浮颗粒物为标准)在 1880 年之前变得越来越差(可能是因为使用了木材和煤等取暖材料),然而,之后开始逐渐改善。

我们发现相关系数为-0.36,并且具有很高的显著性。从 1880 年开始,污染物的急剧下降压倒了之前 180 年缓慢增长的趋势。如果我们分别观察 1700 年至 1880 年和 1880 年至今的时间段,我们会发现相关系数分别为 0.97 和-0.97,这分别是非常强的正相关和非常强的负相关。

斯皮尔曼等级相关比皮尔逊相关系数更好地处理离群值和非线性关系——尽管它无法处理像上面那种非连续的情况。斯皮尔曼相关系数就是皮尔逊相关系数,只不过它是应用于变量值的等级顺序,而不是变量值本身。对于空气污染的时间序列,斯皮尔曼相关系数为-0.19,而对于 1880 年之前和之后的两个时间段,我们分别得到 0.99 和-0.99。

在斯皮尔曼相关系数的情况下,数值差异被忽略——关键是点的顺序。在这种情况下,两个时间段内的点的顺序几乎完美地对齐。

在接下来的部分,我们将讨论趋势和季节性。

识别趋势和季节性

趋势、季节性和周期性变化是时间序列中最重要的特征。趋势是指序列中长期的增加或减少。季节性是指在特定的规律间隔内发生的变化,通常间隔小于一年。季节性变化可以在不同的时间跨度上发生,比如每天、每周、每月或每年。最后,周期性变化是指频率不固定的上升和下降。

时间序列的一个重要特征是平稳性。这指的是时间序列在时间推移中分布不变的性质,换句话说,产生该时间序列的过程随着时间的推移并不发生变化。不随时间变化的时间序列被称为平稳序列(或平稳过程)。许多模型或度量假设时间序列是平稳的,如果数据不是平稳的,这些模型可能无法正常工作。因此,对于这些算法,时间序列应该首先分解成主要信号,然后再分解出季节性和趋势成分。在这个分解过程中,我们会从原始时间序列中减去趋势和季节性成分。

在这一节中,我们将首先通过一个示例来展示如何使用曲线拟合估算趋势和季节性。然后,我们将介绍其他有助于发现趋势、季节性和周期性变化的工具。这些工具包括统计方法,如自相关性和扩展的 Dickey–Fuller 检验,以及可视化工具,如自相关图(也叫滞后图)和周期图。

让我们从一个清晰的例子开始,展示如何在仅仅几行 Python 代码中估算季节性和趋势。为此,我们将回到 NASA 发布的 GISS 地表温度分析数据集。我们将加载该数据集,并进行曲线拟合,NumPy 中自带有此功能。

我们将从 Datahub 下载数据集(datahub.io/core/global-temp),或者你也可以在书本的 GitHub 仓库中找到它(在chapter2文件夹中)。

然后,我们可以加载并对其进行透视处理:

temperatures = pd.read_csv('/Users/ben/Downloads/monthly_csv.csv')
temperatures['Date'] = pd.to_datetime(temperatures['Date'])
temperatures = temperatures.pivot('Date', 'Source', 'Mean') 

现在我们可以使用 NumPy 的 polyfit 功能。它拟合的是如下形式的多项式:

在这个公式中,k是多项式的次数,b是我们要找的系数。

这只是 NumPy 中的一个函数,用于拟合系数。我们可以使用相同的函数来拟合季节性变化和趋势。由于趋势可能主导季节性变化,因此在估算季节性之前,我们需要去除趋势:

from numpy import polyfit
def fit(X, y, degree=3):
    coef = polyfit(X, y, degree)
    trendpoly = np.poly1d(coef)
    return trendpoly(X)
def get_season(s, yearly_periods=4, degree=3):
    X = [i%(365/4) for i in range(0, len(s))]
    seasonal = fit(X, s.values, degree)
    return pd.Series(data=seasonal, index=s.index)
def get_trend(s, degree=3):
    X = list(range(len(s)))
    trend = fit(X, s.values, degree)
    return pd.Series(data=trend, index=s.index) 

让我们在全球气温上升的基础上绘制季节性和趋势图!

import seaborn as sns
plt.figure(figsize=(12, 6))
temperatures['trend'] = get_trend(temperatures['GCAG'])
temperatures['season'] = get_season(temperatures['GCAG'] - temperatures['trend'])
sns.lineplot(data=temperatures[['GCAG', 'season', 'trend']])
plt.ylabel('Temperature change'); 

这是我们得到的图表:

temperatures_trend_seasonality.png

图 2.9:从 19 世纪末到今天的温度变化

这展示了你可以使用 NumPy 中的插件功能进行曲线拟合,以便找到趋势和季节性。如果你想进一步实验,可以调整多项式的阶数或季节性分量,看看能否得到更好的拟合,或者找到另一个季节性成分。我们本来也可以使用其他库的功能,比如statsmodels中的seasonal.seasonal_decompose(),或者 Facebook 的 Prophet,它通过傅里叶系数来分解季节性成分。

现在我们已经看到了如何估计季节性和趋势,接下来我们将讨论其他统计量和可视化。继续使用污染数据集,并且拿起我们在第一章中看到的 EEG 数据集,我们将在 Python 中实际展示如何获取这些统计量和图表,并如何识别趋势和季节性。

自相关是信号与其滞后版本之间的相关性。自相关图绘制了自相关与滞后之间的函数关系。自相关图有助于发现重复的模式,通常用于信号处理。自相关有助于识别周期性信号。让我们绘制污染数据的自相关:

pollution = pollution.pivot("Year", "City", "SPM")
pd.plotting.autocorrelation_plot(pollution['London']) 

这是我们得到的图:

autocorrelation.png

图 2.10:伦敦污染的自相关图

我们可以看到高自相关仅在几年的滞后下出现。在大约 100 年时,有一个负自相关点,之后自相关保持在 0 附近。

SPM 图清楚地表明,空气污染不是一个平稳过程,因为自相关并没有平坦。你还可以比较污染的走势,显示出有趋势,因此均值也发生变化——这是序列非平稳的另一个指示。

我们还可以进行统计检验。检验平稳性的方法是扩展的迪基–富勒检验:

from statsmodels.tsa import stattools
stattools.adfuller(pollution['London']) 
(-0.33721640804242853,
 0.9200654843183897,
 13,
 303,
 {'1%': -3.4521175397304784,
  '5%': -2.8711265007266666,
  '10%': -2.571877823851692},
 1684.6992663493872) 

第二个返回值是 p 值,它表示在给定零假设的情况下,得到至少与观察结果一样极端的测试结果的显著性或概率。当 p 值小于 5%或 0.05 时,我们通常会拒绝零假设,并且可以假设我们的时间序列是平稳的。在我们的案例中,我们不能假设该序列是平稳的。

我们在第一章《使用 Python 进行时间序列分析简介》中看到了脑电图EEG)信号的图,并提到 EEG 信号显示了多个频率范围的脑电波。

我们可以很漂亮地可视化这个。让我们一步步在 Python 中实现。首先我们需要进行一些导入:

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.dates import DateFormatter
import seaborn as sns
from sklearn.datasets import fetch_openml 

OpenML 是一个提供基准数据集并有一个用于比较机器学习算法的网站的项目。scikit-learn 库提供了一个与 OpenML 的接口,允许我们从 OpenML 中获取数据。整个测量过程持续了 117 秒。所以我们需要在 pandas 中正确设置它作为索引:

eeg = fetch_openml(data_id=1471, as_frame=True)
increment = 117 / len(eeg['data'])
import numpy as np
index = np.linspace(
    start=0,
    stop=increment*len(eeg['data']),
    num=len(eeg['data'])
)
ts_index = pd.to_datetime(index, unit='s')
v1 = pd.Series(name='V1', data=eeg['data']['V1'].values, index=ts_index) 

我们可以直接切片我们的数据集。请注意,DatetimeIndex的基准时间是 1970 年,但在这里我们可以安全地忽略这一点:

slicing = (v1.index >= '1970-01-01 00:00:08') & (v1.index <='1970-01-01 00:01:10.000000000')
v1[slicing] 

这是切片:

1970-01-01 00:00:08.006208692    4289.74
1970-01-01 00:00:08.014019627    4284.10
1970-01-01 00:00:08.021830563    4280.00
1970-01-01 00:00:08.029641498    4289.74
1970-01-01 00:00:08.037452433    4298.46
                                  ...   
1970-01-01 00:01:09.962547567    4289.74
1970-01-01 00:01:09.970358502    4283.08
1970-01-01 00:01:09.978169437    4284.62
1970-01-01 00:01:09.985980373    4289.23
1970-01-01 00:01:09.993791308    4290.77
Name: V1, Length: 7937, dtype: float64 

这种切片避免了在大约 1:20 时发生的伪影,即强烈的峰值。

我们在第一章中看到的图,可以如下绘制:

date_formatter = DateFormatter("%S")
ax = v1[slicing].plot(figsize=(12, 6))
ax.xaxis.set_major_formatter(date_formatter)
plt.ylabel('voltage') 

这是再次展示的图:

Machine-Learning%20for%20Time-Series%20with%20Python/voltage_over_time.png

图 2.11:EEG 信号随时间变化的电压

这是 EEG 信号随时间变化的图。

我们也可以重新采样数据,以更粗略的方式查看该系列,分辨率较低,例如如下所示:

plt.subplot(311)
ax1 = v1[slicing].resample('1s').mean().plot(figsize=(12, 6))
ax1.xaxis.set_major_formatter(date_formatter)
plt.subplot(312)
ax1 = v1[slicing].resample('2s').mean().plot(figsize=(12, 6))
ax1.xaxis.set_major_formatter(date_formatter)
plt.subplot(313)
ax2 = v1[slicing].resample('5s').mean().plot(figsize=(12, 6))
ax2.xaxis.set_major_formatter(date_formatter) 
plt.xlabel('seconds'); 

这是从重新采样得到的三幅子图,分别对应 1 秒、2 秒和 5 秒的频率:

eeg_resampled.png

图 2.12:重新采样的 EEG 信号

图中每个重新采样的信号可能根据应用的不同,分析的效果有所不同。对于高频分析,我们不应该进行重新采样,而如果我们试图去除尽可能多的噪声,则应将时间分辨率采样得更粗一些。

我们可以在谱密度图上查看周期性活动。我们可以通过应用傅里叶变换来实现。这里我们使用了 Welch 方法,它先对时间进行平均,然后再应用离散傅里叶变换:

from scipy import signal
fs = len(eeg['data']) // 117
f, Pxx_den = signal.welch(
    v1[slicing].values,
    fs,
    nperseg=2048,
    scaling='spectrum'
)
plt.semilogy(f, Pxx_den)
plt.xlabel('frequency [Hz]')
plt.ylabel('PSD [V**2/Hz]') 

谱密度图,即周期图,如下所示:

spectral_eeg.png

图 2.13:EEG 信号的周期图

这个图的信息类似于我们为污染绘制的自相关图,然而,它给了我们关于某些频率的显著性的更多信息。在这种情况下,我们看到低频特别强劲。换句话说,信号显示出缓慢的振荡。

这也标志着本章的结束。让我们总结一下我们所涵盖的内容。

总结

在本章中,我们介绍了 TSA(时间序列分析)作为从时间序列中提取摘要和其他统计信息的过程。我们将这一过程分解为理解变量、揭示变量之间的关系以及识别趋势和季节性。

我们介绍了 datetime 和 pandas,这两个在 TSA 中不可或缺的库及其在时间序列中的功能;例如,重新采样。在本章中,我们列举并定义了许多摘要统计量,包括均值、标准差、中位数、标准误、置信区间、皮尔逊相关系数和协方差。

我们还讨论了季节性、周期性变化和平稳性等概念。我们讨论了为什么平稳性很重要,以及如何测试平稳性。

我们还展示了使用 Matplotlib 和 Seaborn 绘图的功能,以及如何生成不同类型的图表,如运行图、时间序列图、相关性热图、直方图、散点图、自相关图和周期图。在实际示例中,我们使用了自相关图,它展示了不同时间步之间的相关性,以及周期图,它可视化了功率谱密度。

第三章:时间序列的预处理

预处理是机器学习中的关键步骤,但却常常被忽视。许多书籍对预处理没有深入讨论,甚至完全跳过了预处理。在向外部人员展示机器学习项目时,大家的好奇心自然会集中在算法上,而不是数据集或预处理上。

预处理相对较少被关注的原因之一可能是它比机器学习本身不那么吸引人。然而,它通常是最耗时的步骤,有时被估计占整个机器学习过程的 98%。而且,预处理往往是那种相对简单的工作对最终机器学习模型性能产生巨大影响的步骤。数据的质量在很大程度上决定了结果——低质量的输入在最糟糕的情况下甚至可能完全使机器学习工作无效(这也就是俗话所说的“垃圾进,垃圾出”)。

预处理包括对数据的整理和筛选,这与上一章中提到的分析过程有所重叠,第二章使用 Python 进行时间序列分析。预处理的预期输出是一个更适合进行机器学习的数据集。这意味着它可能比原始数据集更可靠、噪声更少。

你可以在本书的 GitHub 仓库中找到本章的代码,形式为 Jupyter notebook。

我们将涵盖以下主题:

  • 什么是预处理?

  • 特征变换

  • 特征工程

  • Python 实践

我们将从讨论预处理的基础知识开始。

什么是预处理?

任何曾在机器学习项目中工作过的人都知道,现实世界中的数据是杂乱无章的。数据通常是从多个来源或多个平台或记录设备中汇总的,而且它是不完整且不一致的。在预处理阶段,我们希望提高数据质量,以便能够成功应用机器学习模型。

数据预处理包括以下一组技术:

  • 特征变换

    • 缩放

    • 幂/对数变换

    • 缺失值填补

  • 特征工程

这些技术大致分为两类:一种是针对机器学习算法的假设进行调整(特征变换),另一种则是关注从多个基础特征中构造更复杂的特征(特征工程)。我们将仅讨论单变量特征变换,也就是一次只对一个特征进行变换。我们不会讨论多变量特征变换(数据降维),例如变量选择或降维,因为它们不特定于时间序列数据集。

缺失值是机器学习中的常见问题,因此我们将在本章讨论缺失值的填补(插补)。

我们将讨论特征作为预处理的基本单元。我们希望为机器学习过程创建输入特征,这些特征能够使模型更容易训练、更容易评估,或提高模型预测的质量。我们的目标是拥有能够预测目标且不相关(相互之间不冗余)的特征。去相关性是线性模型的要求,但对更现代的算法(例如基于树的算法)来说不那么重要。

虽然我们主要处理特征工程,但我们也会提到目标转换。我们可以更具体地称目标转换为目标工程;然而,由于应用于目标的方法与应用于特征的方法相同,所以我将它们包括在特征工程或特征转换的同一章节下。

请注意,我们将预处理的主要目标定义为提高特征的预测能力,换句话说,我们希望提高机器学习模型预测的质量。我们本可以将数据质量定义为准确性、完整性和一致性,这样定义会包括数据聚合和清洗技术,以及数据质量评估方法,范围更广。

在本章中,我们务实地将讨论范围缩小到在机器学习中的实用性。如果我们的模型不符合预期,我们可能需要重新收集数据、进行更多特征工程,或构建更好的模型。这再次强调了数据分析、预处理和机器学习是一个迭代过程。

分箱离散化也可以是预处理的一部分,但也可以用于根据数据点的相似性对其进行分组。我们将在第六章中讨论离散化与其他聚类技术,无监督时间序列方法

在继续之前,让我们先了解一些使用 Python 预处理时间序列数据集的基础知识。这将作为时间序列数据操作的理论介绍。

特征变换

许多模型或训练过程依赖于数据符合正态分布的假设。即使是最常用的描述符——算术平均值和标准差——如果数据集存在偏态或多个峰值(多模态),也几乎没有用处。不幸的是,观察到的数据通常不符合正态分布,这样传统算法可能会产生无效结果。

当数据不符合正态分布时,会对数据进行变换,使其尽可能接近正态分布,从而提高相关统计分析的有效性。

通常,处理时间序列数据时,避免使用传统的机器学习算法,而转而使用更新的、所谓的非线性方法,这些方法不依赖于数据的分布。

最后需要提到的是,尽管所有以下的变换和缩放方法可以直接应用于特征,但在时间序列数据集中的一个有趣之处是,时间序列随着时间的推移而变化,并且我们可能无法完全了解时间序列。许多变换方法都有在线变种,可以动态估算并调整所有统计数据。你可以查看第八章时间序列的在线学习,以获取有关此主题的更多细节。

在下一节中,我们将讨论缩放问题,这在回归分析中是一个普遍问题。

缩放

有些特征有自然的界限,比如一个人的年龄或产品的生产年份。如果这些范围在不同特征之间有所不同,一些模型类型(尤其是线性模型)会遇到困难,更倾向于类似的范围和均值。

两种非常常见的缩放方法是最小-最大缩放和 Z-score 标准化。

最小-最大缩放涉及将特征的范围限制在两个常数 ab 之间。其定义如下:

在特殊情况下,当 a 为 0 且 b 为 1 时,这会将特征的范围限制在 0 和 1 之间。

Z-score 标准化是将特征的均值设置为 0,方差设置为 1(单位方差),如下所示:

如果 x 来自高斯分布, 则是标准正态分布。

在下一节中,我们将研究对数和幂变换。这些变换非常重要,特别是对于传统的时间序列模型,我们将在第五章使用移动平均和自回归模型进行时间序列预测中遇到这些模型。

对数和幂变换

对数和幂变换都可以将覆盖大范围的值压缩到一个较窄的输出范围内。对数变换是一种特征变换,其中每个值 x 被替换为 log(x)

对数函数是指数函数的反函数,重要的是要记住,范围在 0 和 1 之间的值会被映射为负数(),而 x>=1 的值会被压缩到正数范围内。对数的选择通常是在自然对数和以 10 为底的对数之间,但也可以选择任何能帮助你的特征更接近对称的钟形分布(即正态分布)的对数。

对数变换可以说是所有变换方法中最受欢迎的之一,特别是在将数据分布逼近高斯分布时。对数变换可以用来减少分布的偏斜度。在最佳情况下,如果特征遵循对数正态分布,那么对数变换后的数据将遵循正态分布。不幸的是,你的特征可能并非按照对数正态分布分布,因此应用这种变换并没有效果。

通常,我建议在进行数据变换时要小心。在变换前后,你应始终检查数据。你希望特征的方差能够反映目标变量的方差,因此需要确保不会丢失分辨率。此外,你可能还需要检查数据是否符合正态分布——这是目标之一。许多统计方法已经开发出来用以检验观测数据的正态性假设,但即使是简单的直方图也能给出分布的大致情况。

幂变换通常用于将数据从原始分布转化为更接近正态分布的形式。如在介绍中所讨论的,这对机器学习算法找到解决方案的能力有巨大影响。

幂变换是保持原始顺序(该特性称为单调性)的变换,使用幂函数。幂函数是此形式的函数:

其中

n 是一个大于 1 的整数时,根据 n 是奇数还是偶数,我们可以做出两种主要的区分。如果是偶数,函数 将随着大 x(无论是正数还是负数)的增加而趋向正无穷。

如果是奇数,f(x) 随着 x 的增加将趋向正无穷,但随着 x 的减少将趋向负无穷。

幂变换通常定义如下:

其中 GM(x)x 的几何平均值:

这将变换简化为参数 的最优选择。实际应用中,最常用的两种幂变换是:

  • Box-Cox 变换

  • Yeo–Johnson

对于 Box-Cox 变换,有两种变体:单参数变体和双参数变体。单参数 Box-Cox 变换定义如下:

参数 的值可以通过不同的优化方法来确定,例如最大似然估计,即变换后的特征是高斯分布。

所以,lambda 的值对应于幂运算的指数,例如,

趣事:Box-Cox 变换以统计学家 George E.P Box 和 David Cox 的名字命名,他们决定合作,因为 Box-Cox 听起来很不错。

Yeo–Johnson 变换是 Box-Cox 变换的扩展,允许 x 的值为零或负数。 可以是任何实数,其中 = 1 会产生恒等变换。变换定义为:

最后,分位数转换可以根据累计分布函数的估计将特征映射到均匀分布。可选地,这可以在第二步中映射到正态分布。这种转换的优势,类似于我们之前谈到的其他转换,是使特征更方便处理和绘图,并且即使它们是在不同的尺度上测量的,也更容易进行比较。

在下一节中,我们将讨论填充,这字面意思是通过推理分配值,但在机器学习中,通常更狭义地指代替换缺失值。

填充

填充是对缺失值的替换。这对任何不能处理缺失值的机器学习算法来说都很重要。通常,我们可以区分以下几种填充技术:

  • 单位填充——将缺失值替换为常数,例如均值或 0

  • 基于模型的填充——将缺失值替换为机器学习模型的预测值

单位填充是迄今为止最流行的填充技术,部分原因是它非常简单,而且比基于模型的填充对计算资源的需求较低。

我们将在本章的实践部分进行填充。在下一节中,我们将讨论特征工程。

特征工程

机器学习算法可以使用不同的输入特征表示。正如我们在介绍中提到的,特征工程的目标是产生可以帮助我们进行机器学习过程的新特征。一些特征的表示或增强可以提升性能。

我们可以区分手工特征提取和自动化特征提取,其中手工特征提取意味着我们查看数据并尝试提出可能有用的表示,或者我们可以使用以前由研究人员和实践者提出的一组已建立的特征。一个已建立特征集的例子是Catch22,它包括 22 个特征和从相位依赖的区间提取的简单摘要统计量。Catch22 特征集是高度比较时间序列分析HCTSA)工具箱的一个子集,另一个特征集。

另一个区分是可解释特征和不可解释特征。可解释特征可以是摘要特征,例如均值、最大值、最小值等。这些特征可以在时间段内进行汇总,或者在窗口内汇聚,以提供更多特征。

在时间序列特征中,一些预处理方法与其推荐的机器学习模型一起使用。例如,ROCKET 模型是在 ROCKET 特征基础上的线性模型。

极端情况下,这可以是一种模型堆叠的形式,其中模型的输出作为其他模型的输入。这可以通过在监督环境中训练较简单(特征较少、参数较少)的模型,并利用它们的输出训练其他模型,从而有效地分解学习问题。

请注意,重要的是任何新特征只能依赖于过去和现在的输入。在信号处理领域,这种操作称为因果滤波器。因果一词表示,时间t的值的滤波器输出仅使用时间t时可用的信息,而不窥视未来。相反,输出也依赖于未来输入的滤波器是非因果的。我们将在第十章《时间序列的深度学习》中讨论时序卷积网络,基本上是因果卷积。

我们应该在训练和测试中非常小心,确保在预处理过程中提取和应用的任何统计数据都经过仔细考虑——如果模型依赖于在预测过程中不应当可用的数据,最好的情况下模型性能会过于乐观。我们将在下一章讨论数据泄漏问题,第四章时间序列的机器学习

如果我们有许多特征,可能想通过修剪可用特征并仅使用子集(特征选择)来简化模型构建过程,或者使用描述特征本质的新特征集合(降维)。

我们可以区分以下几种时间序列特征:

  • 日期和时间相关特征

    • 日历特征(日期相关)

    • 时间相关特征

  • 基于窗口的特征

日历和时间相关特征非常相似,因此我们将在同一节中讨论它们。

基于窗口的特征是将特征集成到一个(滚动)窗口中的特征,也就是说,集成到一个时间段内。例如,15 分钟窗口内的平均值或 7 天内的销售额。由于我们在第二章使用 Python 进行时间序列分析中讨论了滚动窗口,本章将讨论更复杂的特征,如卷积和形状特征。

许多预处理算法都已在sktime中实现。另一个方便的库是 tsfresh,它计算时间序列的海量可解释特征。在本章的代码中,我们通过特征工具访问了 tsfresh 特征。

让我们在 Python 中做更多时间序列预处理!接下来我们将讨论日期和时间相关特征。

日期和时间相关特征

日期和时间变量包含关于日期、时间或其组合(日期时间)的信息。我们在上一章中看到了几个例子,第二章使用 Python 进行时间序列分析——其中之一是与污染相关的年份。其他的例子可能包括一个人的出生年份或贷款申请的日期。

如果我们想将这些字段输入到机器学习模型中,我们需要提取相关信息。例如,我们可以将年份作为整数输入,但还有更多从日期时间变量中提取的特征示例,我们将在本节中讨论。通过使用这些提取的特征来丰富我们的数据集,我们可以显著提升机器学习模型的性能。

Workalendar 是一个 Python 模块,提供能够处理日历的类,包括银行和宗教假期列表,并提供与工作日相关的函数。Python-holidays 是一个类似的库,但这里我们将使用 workalendar。

在下一节中,我们将讨论 ROCKET 特征。

火箭

研究论文ROCKET: 使用随机卷积核进行异常快速和准确的时间序列分类(作者:Angus Dempster, François Petitjean, Geoffrey I. Webb;2019)提出了一种新的方法,通过随机核对时间序列数据进行卷积,从而可以提高机器学习模型的准确性并加速训练时间。使这篇论文独特的地方在于,它利用了卷积神经网络的最新成功,并将其转移到时间序列数据集的预处理上。

我们将深入探讨这篇论文的细节。ROCKET,即随机卷积核变换的缩写,基于卷积,因此让我们从卷积开始。

卷积是一种非常重要的变换,尤其在图像处理领域,它是深度神经网络在图像识别中的重要构建模块之一。卷积由前馈连接组成,这些连接被称为滤波器,它们应用于图像的矩形区域(上一层)。每个生成的图像就是核在整个图像上滑动窗口的结果。简单来说,在图像的情况下,核是用来修改图像的矩阵。

锐化核可以是这样的:

如果我们将此核依次乘以所有局部邻域,我们将得到一张更锐利的图像,如下所示(左侧为原图,右侧为锐化后的图像):

merged.png

图 3.1:锐化滤波器

这张图片是“一个女人被分成两部分,代表生与死”的灰色版本(该作品归伦敦的 Wellcome Collection 博物馆和展览中心所有,采用 CC BY 4.0 许可)。

锐化核强调相邻像素值之间的差异。你可以看到右侧的图片明显更加颗粒感或生动——这是卷积的结果。

我们不仅可以将核应用于图像,还可以将其应用于向量或矩阵,这将我们带回到 ROCKET。ROCKET 从每个核和特征卷积中计算出两个汇总特征。这两个特征是通过广为人知的全局/平均最大池化方法以及我们稍后会介绍的全新方法创建的。

全局最大池化(Global max pooling)输出卷积结果中的最大值,而最大池化(max pooling)则是在池化大小内选择最大值。例如,如果卷积结果为 0,1,2,2,5,1,2,则全局最大池化返回 5,而池化大小为 3 的最大池化会在 3 个窗口内选择最大值,所以结果为 2,2,5,5,5。

正比例值PPV)是论文中的方法学,它是卷积结果中正值(或高于偏置阈值)的比例(百分比)。

我们可以通过应用带有卷积核的变换来提高时间序列的机器学习准确度。每个特征都通过随机核进行变换,核的数量是算法的一个参数。默认设置为 10,000。变换后的特征现在可以作为输入,输入到任何机器学习算法中。作者建议使用线性算法,如岭回归分类器或逻辑回归。

ROCKET 的思想与卷积神经网络(CNN)非常相似,我们将在第十章《时间序列深度学习》中讨论这一点。然而,有两个显著的区别:

  1. ROCKET 不使用任何隐藏层或非线性函数。

  2. 卷积是针对每个特征独立应用的。

在下一部分,我们将讨论形状特征。

形状特征

时间序列的形状特征在研究论文《时间序列形状特征:数据挖掘的新原语》(Lexiang Ye 和 Eamonn Keogh,2009)中提出。形状特征的基本思想是将时间序列分解为具有判别性的子部分(形状特征)。

在第一步中,形状特征被学习。算法计算可能候选项的信息增益,并选择最佳候选项来创建一个包含判别子部分的形状特征字典。这可能是非常耗费计算的。然后,根据特征的形状特征分解,可以应用决策树或其他机器学习算法。

形状特征相比其他方法有几个优势:

  • 它们可以提供可解释的结果。

  • 形状特征的应用可以非常快速——只需要根据特征与形状特征字典的匹配来进行。

  • 基于形状特征的机器学习算法的表现通常非常具有竞争力。

是时候通过实际数据集来进行 Python 练习了。

Python 实践

NumPy 和 SciPy 提供了我们所需的大部分功能,但我们可能需要一些额外的库。

在这一部分,我们将使用几个库,这些库可以通过终端、Jupyter Notebook 或类似的 Anaconda Navigator 快速安装:

pip install -U tsfresh workalendar astral "featuretools[tsfresh]" sktime 

所有这些库都非常强大,每一个都值得我们在本章中所能给予的更多空间。

让我们从对数和幂变换开始。

对数变换和幂变换的实践应用

让我们创建一个不是正态的分布,然后进行对数变换。我们将原始分布和变换后的分布进行对比,并应用正态性检验。

首先让我们创建该分布:

from scipy.optimize import minimize
import numpy as np
np.random.seed(0)
pts = 10000
vals = np.random.lognormal(0, 1.0, pts) 

值是从对数正态分布中采样的。我添加了对随机数生成器种子函数的调用,以确保结果对读者是可重复的。

我们可以将我们的数组可视化为一个直方图:

lognormal_hist.png

图 3.2:从对数正态分布中采样的数组

我在 y 轴上使用了对数尺度。我们可以看到,值分布在多个数量级上。

我们可以应用标准化 z 分数变换。我们还可以对其中一个变换后的分布进行正态性检验:

from sklearn.preprocessing import StandardScaler
from scipy.stats import normaltest
scaler = StandardScaler()
vals_ss = scaler.fit_transform(vals.reshape(-1, 1))
_, p = normaltest(vals_ss)
print(f"significance: {p:.2f}") 

该统计检验的零假设是样本来自正态分布。因此,低于阈值(通常设置为 0.05 或 0.01)的显著性值(p 值)将使我们拒绝零假设。

我们得到的输出是:significance: 0.00

我们可以从测试中得出结论:通过标准缩放变换得到的分布不是零假设分布。

这应该很明显,但还是说清楚:我们对 minmax 变换后的值得到了相同的显著性值:

from sklearn.preprocessing import minmax_scale
vals_mm = minmax_scale(vals)
_, p = normaltest(vals_mm.squeeze())
print(f"significance: {p:.2f}") 

因此我们得出相同的结论:minmax 变换没有帮助我们得到接近正态的分布。

我们可以将原始分布和标准缩放后的分布相互对比。毫不意外地,从视觉上看,两个分布除了缩放外看起来相同。

standard_scaled.png

图 3.3:线性变换与原始值的对比

我们可以看到所有数据点都位于对角线上。

我们来进行对数变换:

log_transformed = np.log(vals)
_, p = normaltest(log_transformed)
print(f"significance: {p:.2f}") 

我们得到了 0.31 的显著性值。这使我们得出结论,我们无法拒绝原假设。我们的分布类似于正态分布。实际上,我们得到的标准差接近 1.0,均值接近 0.0,符合正态分布的预期。

我们可以看到,对数正态分布是一个连续的概率分布,其对数是正态分布的,因此我们得到这个结果也不完全令人惊讶。

我们可以绘制对数变换后分布的直方图:

log_transformed_hist.png

图 3.4:对数变换后的对数正态数组

如我们所见,对数变换看起来更接近正态分布。

我们还可以应用 Box-Cox 变换:

from scipy.stats import boxcox
vals_bc = boxcox(vals, 0.0) 

我们得到了 0.46 的显著性值。再次,我们可以得出结论,我们的 Box-Cox 变换接近正态分布。我们也可以通过绘制 Box-Cox 变换后的分布来看到这一点:

bc_transformed_hist.png

图 3.5:Box-Cox 变换后的对数正态数组

这看起来非常像正态分布。这个图与之前的对数变换图几乎一样,考虑到对数操作对应于 Box-Cox 变换中的 lambda 参数为 0,这一点并不令人惊讶。

这是一些转换的小选择,可以帮助我们将数据与经典预测方法中的常见正态性假设对齐。

让我们看看填补的实际操作。

填补

机器学习算法直接处理缺失值并不常见。相反,我们通常需要用常数替代缺失值,或者根据其他特征推断可能的值。

scikit-learn 文档中列出了一个简单的单位填补示例:

import numpy as np
from sklearn.impute import SimpleImputer
imp_mean = SimpleImputer(missing_values=np.nan, strategy='mean')
imp_mean.fit([[7, 2, 3], [4, np.nan, 6], [10, 5, 9]])
SimpleImputer()
df = [[np.nan, 2, 3], [4, np.nan, 6], [10, np.nan, 9]]
print(imp_mean.transform(df)) 

我们再次使用(类似之前的标准化器)scikit-learn 的转换器,它们带有fit()transform()方法。

我们得到以下填补后的值:

[[ 7\.   2\.   3\. ]
 [ 4\.   3.5  6\. ]
 [10\.   3.5  9\. ]] 

缺失值将被列的均值替代。

让我们看看将假期注释为派生日期特征。

假期特征

如果我们想获取英国的假期,可以这样做:

from workalendar.europe.united_kingdom import UnitedKingdom
UnitedKingdom().holidays() 

我们得到以下假期:

[(datetime.date(2021, 1, 1), 'New year'),
 (datetime.date(2021, 4, 2), 'Good Friday'),
 (datetime.date(2021, 4, 4), 'Easter Sunday'),
 (datetime.date(2021, 4, 5), 'Easter Monday'),
 (datetime.date(2021, 5, 3), 'Early May Bank Holiday'),
 (datetime.date(2021, 5, 31), 'Spring Bank Holiday'),
 (datetime.date(2021, 8, 30), 'Late Summer Bank Holiday'),
 (datetime.date(2021, 12, 25), 'Christmas Day'),
 (datetime.date(2021, 12, 26), 'Boxing Day'),
 (datetime.date(2021, 12, 27), 'Christmas Shift'),
 (datetime.date(2021, 12, 28), 'Boxing Day Shift')] 

我们可以按年份生成假期,然后按日期查找假期。

类似地,我们可以获取其他地方的假期,例如美国加利福尼亚州。我们还可以提取假期列表,并添加自定义假期:

from typing import List
from dateutil.relativedelta import relativedelta, TH
import datetime
from workalendar.usa import California
def create_custom_holidays(year: int) -> List:
      custom_holidays = California().holidays()
      custom_holidays.append((
        (datetime.datetime(year, 11, 1) + relativedelta(weekday=TH(+4)) + datetime.timedelta(days=1)).date(),
        "Black Friday"
      ))
      return {k: v for (k, v) in custom_holidays}
custom_holidays = create_custom_holidays(2021) 

这为我们提供了 2021 年的自定义假期:

{datetime.date(2021, 1, 1): 'New year',
 datetime.date(2021, 1, 18): 'Birthday of Martin Luther King, Jr.',
 datetime.date(2021, 2, 15): "Washington's Birthday",
 datetime.date(2021, 3, 31): 'Cesar Chavez Day',
 datetime.date(2021, 5, 31): 'Memorial Day',
 datetime.date(2021, 7, 4): 'Independence Day',
 datetime.date(2021, 7, 5): 'Independence Day (Observed)',
 datetime.date(2021, 9, 6): 'Labor Day',
 datetime.date(2021, 11, 11): 'Veterans Day',
 datetime.date(2021, 11, 25): 'Thanksgiving Day',
 datetime.date(2021, 11, 26): 'Thanksgiving Friday',
 datetime.date(2021, 12, 24): 'Christmas Day (Observed)',
 datetime.date(2021, 12, 25): 'Christmas Day',
 datetime.date(2021, 12, 31): 'New Years Day (Observed)',
 datetime.date(2016, 11, 25): 'Black Friday'} 

请注意,我们在上面的代码片段中使用了类型提示。我们像这样声明函数的签名:

def create_custom_holidays(year: int) -> List: 

这意味着我们期望一个名为year的整数,并期望一个List作为输出。注释是可选的,且不会被检查(如果你没有调用 mypy),但它们可以使 Python 代码更加清晰。

现在我们可以实现一个简单的查找操作,如下所示:

def is_holiday(current_date: datetime.date):
    """Determine if we have a holiday."""
    return custom_holidays.get(current_date, False)
today = datetime.date(2021, 4, 11)
is_holiday(today) 

即使我希望它是一个假期,我得到的也是False

这对机器学习模型来说是一个非常有用的特征。例如,我们可以设想在银行假期或工作日申请贷款的用户群体有所不同。

日期注释

calendar 模块提供了许多方法,例如,monthrange() - calendar.monthrange返回给定年份和月份的第一天是星期几,以及该月的天数。星期几以整数表示,星期一为 0,星期日为 6。

import calendar
calendar.monthrange(2021, 1) 

我们应该得到(4, 31)。这意味着 2021 年的第一天是星期五。2021 年 1 月有 31 天。

我们还可以提取与年份相关的日期特征。以下函数提供了从前一年结束到当前年结束的天数:

from datetime import date
def year_anchor(current_date: datetime.date):
      return (
        (current_date - date(current_date.year, 1, 1)).days,
        (date(current_date.year, 12, 31) - current_date).days,
      )
year_anchor(today) 

这个特征可以提供我们在一年中的大致位置。这对于估计趋势和捕捉周期性变化都非常有用。

类似地,我们可以提取从每月的第一天到月底的天数:

def month_anchor(current_date: datetime.date):
      last_day = calendar.monthrange(current_date.year, current_date.month)[0]

      return (
        (current_date - datetime.date(current_date.year, current_date.month, 1)).days,
        (current_date - datetime.date(current_date.year, current_date.month, last_day)).days,
      )
month_anchor(today) 

这样的特征也可以提供一些有用的信息。我得到的是(10, 8)

在零售行业,预测顾客的消费行为非常重要。因此,在接下来的章节中,我们将为发薪日编写注解。

发薪日

我们可以想象一些人会在月中或月底领取工资,然后访问我们的网站购买我们的产品。

大多数人会在月末的最后一个星期五领到工资,因此我们来编写一个函数来处理这个问题:

def get_last_friday(current_date: datetime.date, weekday=calendar.FRIDAY):
      return max(week[weekday]
        for week in calendar.monthcalendar(
            current_date.year, current_date.month
        ))
get_last_friday(today) 

我得到的 30 是最后一个星期五。

季节也可以具有预测性。

季节

我们可以获取特定日期的季节:

YEAR = 2021
seasons = [
    ('winter', (date(YEAR,  1,  1),  date(YEAR,  3, 20))),
    ('spring', (date(YEAR,  3, 21),  date(YEAR,  6, 20))),
    ('summer', (date(YEAR,  6, 21),  date(YEAR,  9, 22))),
    ('autumn', (date(YEAR,  9, 23),  date(YEAR, 12, 20))),
    ('winter', (date(YEAR, 12, 21),  date(YEAR, 12, 31)))
]
def is_in_interval(current_date: datetime.date, seasons):
      return next(season for season, (start, end) in seasons
                if start <= current_date.replace(year=YEAR) <= end)

is_in_interval(today, seasons) 

我们应该在这里获得spring,但鼓励读者尝试不同的值。

太阳与月亮

Astral 模块提供了关于日出、月相等信息。让我们获取伦敦某一天的日照时间:

from astral.sun import sun
from astral import LocationInfo
CITY = LocationInfo("London", "England", "Europe/London", 51.5, -0.116)
def get_sunrise_dusk(current_date: datetime.date, city_name='London'):
      s = sun(CITY.observer, date=current_date)
      sunrise = s['sunrise']
      dusk = s['dusk']
      return (sunrise - dusk).seconds / 3600
get_sunrise_dusk(today) 

我得到的是9.788055555555555小时的日照时间。

我们常常可以观察到,日照时间越长,商业活动越多。我们可以推测,这一特征可能有助于预测我们的销售量。

工作日

同样地,如果一个月有更多的工作日,我们可能会预计零售店的销售量会更多。另一方面,如果我们正在出售风帆冲浪课程,我们可能想知道某个月的假期数量。以下函数提取了一个月中的工作日和周末/假期数量:

def get_business_days(current_date: datetime.date):
      last_day = calendar.monthrange(current_date.year, current_date.month)[1]
      rng = pd.date_range(current_date.replace(day=1), periods=last_day, freq='D')
      business_days = pd.bdate_range(rng[0], rng[-1])
      return len(business_days), last_day - len(business_days)
get_business_days(date.today()) 

我们应该得到(22, 9)——22 个工作日和 9 个周末及假期。

自动特征提取

我们还可以使用像 featuretools 这样的模块中的自动特征提取工具。Featuretools 可以计算许多与日期时间相关的函数。这里是一个快速示例:

import featuretools as ft
from featuretools.primitives import Minute, Hour, Day, Month, Year, Weekday
data = pd.DataFrame(
    {'Time': ['2014-01-01 01:41:50',
              '2014-01-01 02:06:50',
              '2014-01-01 02:31:50',
              '2014-01-01 02:56:50',
              '2014-01-01 03:21:50'],
     'Target': [0, 0, 0, 0, 1]}
)        
data['index'] = data.index
es = ft.EntitySet('My EntitySet')
es.entity_from_dataframe(
    entity_id='main_data_table',
    index='index',
    dataframe=data,
    time_index='Time'
)
fm, features = ft.dfs(
    entityset=es, 
    target_entity='main_data_table', 
    trans_primitives=[Minute, Hour, Day, Month, Year, Weekday]
) 

我们的特征有MinuteHourDayMonthYearWeekday。这是我们的 DataFrame,fm

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/(A Document Being Saved By screencaptureui 3)/Screenshot 2021-04-11 at 21.58.52.png

图 3.6:Featuretools 输出

我们可以提取更多的特征。请参阅 featuretools 文档以获取更多详细信息。

tsfresh 模块还提供了自动化的特征提取功能:

from tsfresh.feature_extraction import extract_features
from tsfresh.feature_extraction import ComprehensiveFCParameters
settings = ComprehensiveFCParameters()
extract_features(data, column_id='Time', default_fc_parameters=settings) 

请注意,tsfresh 通过使用 statsmodels 的自回归优化特征,(截至我上次检查)仍未更新为使用statsmodels.tsa.AutoReg,而statsmodels.tsa.AR已经被弃用。

我们获得了 1,574 个特征,用于描述我们的time对象。这些特征可能在机器学习模型中帮助我们。

让我们演示如何从时间序列中提取 ROCKET 特征。

ROCKET

我们将使用sktime库中的 ROCKET 实现。

sktime 库以嵌套 DataFrame 的形式表示数据。每一列代表一个特征,正如预期的那样,但令人惊讶的是,每一行都是一个时间序列的实例。每个单元格包含一个数组,表示某个特征在时间维度上的所有值。换句话说,每个单元格都有一个嵌套的对象结构,其中存储了实例-特征组合。

这种结构是合理的,因为它允许我们在同一个 DataFrame 中存储多个时间序列实例,然而,刚开始时这并不直观。幸运的是,SkTime 提供了实用函数来解嵌套 SkTime 数据集,正如我们接下来会看到的那样。

如果我们想在 SkTime 中加载一个示例时间序列,我们可以这样做:

from sktime.datasets import load_arrow_head
from sktime.utils.data_processing import from_nested_to_2d_array
X_train, y_train = load_arrow_head(split="train", return_X_y=True)
from_nested_to_2d_array(X_train).head() 

我们得到一个未嵌套的 DataFrame,如下所示:

../../../../../../Desktop/Screenshot%202021-04-12%20at%2

图 3.7:ROCKET 特征

同样,每一行是一个时间序列。这里只有一个特征,叫做 dim_0。这个时间序列有 251 个测量值。

我们可以导入 ROCKET,然后创建 ROCKET 特征。我们首先需要学习这些特征,然后应用它们。这是机器学习中的一个典型模式。在 scikit-learn 中,我们会使用 fit()predict() 方法来训练模型,其中 fit() 应用在训练数据上,predict() 用于在测试集上给出预测。

学习步骤应该只应用于训练集。ROCKET 的一个参数是内核数量。我们在这里将其设置为 1,000,但也可以设置为更高的数字。默认值是 10,000 个内核:

from sktime.transformations.panel.rocket import Rocket
rocket = Rocket(num_kernels=1000)
rocket.fit(X_train)
X_train_transform = rocket.transform(X_train) 

返回的数据集没有嵌套,包含 2,000 列。每一列描述整个时间序列,但它是来自不同内核的结果。

在下一节中,我们将进行一个形状体练习。

形状体在实践中的应用

让我们为之前在查看 ROCKET 时使用的数据集创建形状体。我们将再次使用 SkTime

from sktime.transformations.panel.shapelets import ContractedShapeletTransform
shapelets_transform = ContractedShapeletTransform(
    time_contract_in_mins=1,
    num_candidates_to_sample_per_case=10,
    verbose=0,
)
shapelets_transform.fit(X_train, y_train) 

训练可能需要几分钟。我们将得到一些关于正在检查的候选项的输出。

我们可以再次使用我们的形状体转换器来变换时间序列。其工作原理如下:

X_train_transform = shapelets_transform.transform(X_train) 

这为我们提供了一个转化后的数据集,我们可以将其用于机器学习模型。我们鼓励读者在这些特征集上进行实验。

这就是我们的 Python 实践的结束。

总结

预处理是机器学习中一个至关重要的步骤,但通常被忽视。许多书籍没有将预处理作为话题,或者完全跳过了预处理。然而,正是在预处理阶段,通常可以获得相对简单的胜利。数据的质量决定了结果。

预处理包括策划和筛选数据。预处理的预期输出是一个更易于进行机器学习的数据集。这意味着该数据集比原始数据集更可靠且噪声更少。

我们已经讨论过时间序列数据的特征变换和特征工程方法,并且还讨论过自动化方法。

在接下来的章节中,我们将探讨如何在机器学习模型中使用这些提取出的特征。在下一章节,第四章时间序列的机器学习简介中,我们将讨论特征组合和建模算法。在第五章使用移动平均和自回归模型进行时间序列预测中,我们将使用机器学习管道,将特征提取和机器学习模型连接起来。

第四章:时间序列机器学习简介

在前面的章节中,我们讨论了时间序列、时间序列分析和预处理。在本章中,我们将讨论时间序列的机器学习。机器学习是通过经验改进的算法研究。这些算法或模型可以根据数据做出系统的、可重复的、经过验证的决策。本章旨在介绍我们在本书剩余部分将使用的许多内容的背景和技术基础。

我们将介绍机器学习在时间序列中的不同问题和应用,以及与机器学习和时间序列分析相关的各种分析类型。我们将解释与时间序列相关的主要机器学习问题,如预测、分类、回归、分割和异常检测。然后,我们将回顾与时间序列相关的机器学习基础知识。接着,我们将探讨机器学习在时间序列中的历史与当前应用。

我们将涵盖以下主题:

  • 时间序列中的机器学习

    • 监督学习、无监督学习和强化学习

    • 机器学习的历史

  • 机器学习工作流

    • 交叉验证

    • 时间序列的误差度量

    • 比较时间序列

  • 时间序列的机器学习算法

我们将从时间序列的机器学习概述开始。

时间序列中的机器学习

在本节中,我将介绍时间序列中机器学习的应用及其主要类别。

时间序列的机器学习方法在经济学、医学、气象学、人口学等领域至关重要。时间序列数据集无处不在,广泛应用于医疗保健、经济学、社会科学、物联网应用、运营管理、数字营销、云基础设施、机器人系统仿真等多个领域。这些数据集具有巨大的实际意义,因为它们可以被用来更有效地预测和检测异常,从而支持决策制定。

时间序列中的机器学习应用技术丰富多样。以下是一些应用实例:

  • 曲线拟合

  • 回归

  • 分类

  • 预测

  • 分割/聚类

  • 异常检测

  • 强化学习

我们将在本书中探讨这些技术应用。这些不同的应用背后有不同的统计方法和模型,它们可能会有所重叠。

我们将简要回顾这些应用,以便对接下来的章节有所预期。

曲线拟合是将数学函数(曲线)拟合到一系列数据点的任务。数学函数由参数定义,这些参数通过优化调整以适应时间序列。曲线拟合可以作为图表的视觉辅助工具,也可以用于推断(外推)。

回归是一个统称,指的是寻找独立变量(特征)与因变量(目标)之间关系的统计方法。例如,我们可能在基于二氧化碳和甲烷的释放来预测准确的温度。如果有多个结果变量,这称为多目标(或多输出)。一个例子可能是同时预测不同地点的温度。

当问题是为时间序列(或其一部分)分配标签时,这称为分类。与回归的主要区别在于,预测结果是分类的而不是连续的。用于分类的模型通常称为分类器。分类可以是二分类,即只有两个类别,或者是多分类,即有多个类别。一个例子可能是检测脑电图信号中的眼动或癫痫。

关于未来的预测称为预测。预测可以仅基于时间序列本身,也可以基于其他变量。技术可以从曲线拟合到外推,从分析当前趋势和变异性到复杂的机器学习技术。例如,我们可能基于过去 100 年的数据来预测全球温度,或者我们可能在预测一个国家的经济福祉。预测的反义词是回溯,即对过去进行预测。我们可能在没有数据可用之前,从过去回溯预测温度。

分段,或称聚类,是将时间序列的部分数据按照不同的状态、行为或基线分组的过程。例如,大脑波动中的不同活动水平就是一个例子。

在时间序列的背景下,异常检测,也称为离群点检测,是识别稀有事件或不符合常规的事件的任务。这些事件可能是新颖的、状态变化、噪声或只是例外。例如,一个相对粗略的例子是通过电压的突然下降检测到电网停运。更微妙的例子可能是,在特定时间段内,呼叫中心接到的电话数量增加。在这两种情况下,异常检测可以提供可行的商业见解。异常检测技术可以从简单的阈值设置或统计方法到一组规则,甚至是基于时间序列分布的模式识别方法。

最后,强化学习是基于最大化一系列决策的期望奖励来进行学习的实践。强化学习算法被应用于不确定性较高的环境中。这可能意味着条件不稳定(高变异性),或者普遍缺乏信息。应用包括股票交易或一般拍卖中的竞价和定价算法,以及控制任务。

让我们更详细地了解这些术语的含义。

监督、无监督和强化学习

机器学习可以大致分为监督学习、无监督学习和强化学习,正如该图所示:

taxonomy.png

图 4.1:将机器学习划分为不同类别

监督学习中,特征与结果相匹配!在一个叫做预测(有时叫做推理)的过程中。

在监督的情况下,参数是从带标签的观测值中估算出来的。我们需要为每个观测值提供结果,作为目标列(或者多列)。

因此,机器学习算法会找到从 X 到 Y 的映射。

函数!只是输入分布 X 到输出分布 Y 的一种可能映射或模型。

监督机器学习可以分为分类和回归。在回归中,我们的目标是连续的,目标是预测值。

目标 Y 可以是实数值,可以是单一值或更高维度(多输出)。

标签与数据集的长度相匹配,但每个观测值也可以有多个标签(多输出)。

一个例子是商店在特定日期销售的产品数量,或者下个月管道中流过的石油量。特征可以包括当前销售量、需求或星期几。

分类中,目标是预测观测值的类别。在这种情况下,可以从一个类别分布中抽取,例如整数的顺序值,如

有时,我们希望找到一个函数,能够为给定的观测值提供概率或得分:

在实际应用中,回归和分类是非常相似的问题,通常回归和分类算法可以互换应用。然而,理解两者的区别对以适当的方式处理具体问题至关重要。

在时间序列预测中,历史值会被外推到未来。唯一的特征是过去的值。例如,我们可以基于过去两年的电话量,估算下个月呼叫中心的电话量。预测任务可以是单变量的,依赖于并外推单一特征,或者是多变量的,其中多个特征被预测到未来。

无监督学习中,算法的任务是根据特征对观测值进行分类。无监督学习的例子包括聚类或推荐算法。

在本书的大部分内容中,我们将讨论监督算法,尽管我们也会讨论一些无监督任务,如变化检测和聚类。

映射函数 f 预测一个结果 每个函数由一组参数指定,优化结果是一组参数,最小化 Y 与 之间的匹配度。通常,这一过程是通过启发式方法完成的。

Y 与 之间的匹配(不匹配)通过误差函数 来衡量,因此优化的目标是估计参数 ,以最小化误差:

在此公式中,由于误差函数是优化的一部分, 被称为目标函数

强化学习中,代理通过动作与环境互动,并通过奖励的形式获得反馈。你可以在第十一章时间序列的强化学习中了解更多关于时间序列的强化学习内容。

与监督学习中的情况相反,强化学习中没有标签数据可用,而是通过探索和利用环境来期望获得累计奖励。

机器学习,即通过经验不断改进的算法研究,可以追溯到 1960 年代,当时统计方法(在第一章使用 Python 进行时间序列分析中讨论)被发现。让我们从机器学习的简短历史开始,以便提供一些背景。这将提供一些术语以及机器学习中主要方向的基本概念。我们将在相关章节中提供更详细的背景。

机器学习的历史

生物神经网络最早由沃伦·麦卡洛克和沃尔特·皮茨于 1943 年提出,作为一种数学模型,奠定了人工神经网络的基础。

弗兰克·罗森布拉特在 1958 年开发了所谓的感知器,在今天的术语中,这是一种全连接的前馈神经网络。该示意图展示了一个具有两个输入神经元和一个输出神经元的感知器(基于维基共享资源上的图像):

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_SIwGeX/Screenshot 2021-08-15 at 18.23.06.png

图 4.3:感知器

输出神经元 y 的连接具有权重 w[1] 和 w[2]。这是一个简单的线性模型。

另一个重要步骤是如何通过网络反向传播误差。反向传播的基础由亨利·J·凯利在不久后(1960 年)发表,作为训练这些网络的一种机制。

然而,这项研究在 1969 年遭遇了严重打击,当时马文·明斯基(Marvin Minsky)和西摩·帕珀特(Seymour Papert)出版了《感知机》一书,书中简单地证明了线性函数(如二层感知机)无法建模非线性函数。根据作者的观点,这意味着感知机在实践中既无用也不值得研究。书中忽略了感知机可以有超过两层,并且这些层的参数可以通过反向传播学习的事实。人工神经网络的研究直到 1980 年代才重新开始。

最近邻算法由伊夫琳·费克斯(Evelyn Fix)和约瑟夫·霍奇斯(Joseph Hodges)于 1951 年描述,随后由托马斯·科弗(Thomas Cover)和彼得·E·哈特(Peter E. Hart)在 1967 年进行了扩展。最近邻算法可以应用于分类和回归。它通过检索一个新数据点与数据集中所有已知实例之间最相似的 k 个实例(k 是一个参数)来工作。在分类的情况下,算法投票选出最频繁的标签;在回归的情况下,它对标签取平均值。

另一个重要的里程碑是决策树算法的发展。ID3 决策树算法(迭代二分法 3)由罗斯·昆兰(Ross Quinlan)在 1979 年的论文中发布,它是今天使用的决策树算法的前身。

CART算法(分类与回归树)由利奥·布雷曼(Leo Breiman)于 1984 年发布。C4.5算法,ID3 的后代,于 1992 年由罗斯·昆兰发布,被认为是机器学习领域的一个里程碑。

决策树的革命性在于它由步骤函数组成,这些函数将数据点的特征空间划分成若干口袋,每个口袋的输出结果相似。尽管许多机器学习算法在考虑多重交互时表现不佳,但决策树在这些情况下表现得很好。以下图示展示了决策树的形态:

../../../../Desktop/Screenshot%202021-04-26%20at%2000.09

图 4.4:决策树

树中的每个节点或分支都是一个基于特征值的单一问题。在树构建的每次迭代中,都会应用一种称为分裂标准的统计函数,用来决定查询最佳特征。分裂标准的典型选择包括基尼不纯度和信息熵,它们都可以最小化分支内目标变量的变异性。

决策树反过来成为集成技术的基础,例如随机森林或梯度提升树。集成技术主要有两种:提升(boosting)和自助法(bagging)。提升由罗伯特·沙皮尔(Robert Schapire)于 1990 年发明,它通过级联的方式逐步增加基学习器。基学习器(也称为弱学习器)是一个非常简单的模型,本身与目标变量的相关性很弱。每次向现有的基学习器中添加新学习器时,都会重新平衡训练集中数据点的重要性(权重)。

这意味着在每次迭代中,算法会处理越来越多它难以应对的样本,随着每次新增一个基学习器,精度也会提高。

这为AdaBoost(自适应提升算法)奠定了基础,该算法的发明者 Yoav Freund 和 Robert Schapire 因此获得了 Gödel 奖,这是理论计算机科学领域最具声望的论文奖项。

这张插图(来自 Wikipedia)展示了每个基分类器是如何在数据集的不同子集上依次训练的,并且每次新的训练都改变了权重:

图 4.5:提升法

袋装法是随机森林的基础,由 Leo Breiman 于 1994 年发明。袋装法包括两个部分:自助法(bootstrap)和聚合(aggregation)。自助法是从训练集中进行有放回的抽样。每个样本可以独立地训练一个模型,这些模型共同形成一个集成。各个模型的预测结果可以被聚合成一个综合决策,例如通过取平均值。

以下图(来源:Wikipedia)展示了一个袋装集成如何被训练并用于预测。这就是随机森林(Leo Breiman, 2001)如何使用决策树作为基学习器进行学习的方式。

图 4.6:袋装法

以下表格展示了袋装法和提升法之间的主要区别:

Bagging Boosting
基学习器的训练方式: 独立训练(可以并行训练) 顺序训练
权重: 保持不变 每次迭代后改变
基学习器的权重: 相等 根据训练表现

图 4.7:袋装法与提升法的区别

梯度提升(由 Friedman 等人开发)是提升方法的进一步扩展,采用了 Unhyphenate。在梯度提升中,新的弱学习器以与损失函数负梯度最大相关的方式被添加。这些是梯度提升树的一些流行实现:

  • CatBoost(由 Yandex 的 Andrey Gulin 等人开发)

  • Light Gradient Boosting Machine(LightGBM,微软公司)

  • XGBoost

反向传播算法是由 David Rumelhart、Geoffrey Hinton 和 Ronald J. Williams 在 1986 年重新发现的。随后,发展出了更深层的网络,能够应用于更有趣的问题,并引起了关注。

1995 到 1997 年间,Sepp Hochreiter 和 Jürgen Schmidhuber 提出了一种循环神经网络架构——长短期记忆网络 (LSTM)。多年来,LSTM 在语音识别、翻译等许多应用中占据了领先地位。如今,循环神经网络已经在许多任务中被 transformer 或 ConvNets 取代,即使是序列建模任务。鉴于 LSTM 对计算资源的高需求,一些人甚至认为 LSTM 已过时,尤其是在有了替代方案之后。

支持向量机 (SVMs)是在 1990 年代初期,由弗拉基米尔· Vapnik 和同事们在 AT&T 贝尔实验室开发的,基于 Vapnik 和 Chervonenkis 描述的统计学习框架。在分类中,SVM 通过最大化投影空间中两类之间的距离来进行操作。在训练过程中,构建一个超平面(称为支持向量)来分离正负例子。

在下一节中,我们将介绍机器学习建模的基础知识以及模型验证中的科学实践。

机器学习工作流

在下一节中,我们将介绍时间序列和机器学习的基础知识。

机器学习主要处理数值数据,这些数据以大小为的矩阵形式呈现。布局通常是每一行表示一个观察,每一列表示一个特征。

在时间序列问题中,与时间相关的列不一定作为特征使用,而是作为索引来切割和排序数据集。然而,时间列可以被转换为特征,正如我们在第三章《时间序列预处理》中看到的那样。

每个观察由 M 个特征的向量描述。虽然一些机器学习算法可以内部处理非数值数据,但通常每个特征要么是数值型的,要么在输入机器学习算法之前被转换为数字。例如,Male 可以表示为 0,Female 表示为 1。简单来说,每个特征可以定义如下:

机器学习工作流可以分为三个过程,如下图所示。我已经添加了数据加载和时间序列分析,它们为机器学习提供了信息。

machine_learning_workflow_cropped.png

图 4.8:机器学习工作流

我们首先必须对数据进行转换(或预处理),训练或拟合一个模型,然后可以将训练好的模型应用于新数据。这个图示虽然可能过于简化,但将焦点放在了机器学习过程的三个不同阶段。每个阶段都有其挑战和针对时间序列数据的特殊性。

这也有助于思考数据流的过程,从输入到转换,再到训练和预测。我们应该牢记可用的历史数据及其局限性,以及将用于预测的未来数据点。

在接下来的部分,我们将讨论交叉验证的一般原则。

交叉验证

这是机器学习中一条广为人知的格言,归功于我们在本书中已经遇到过几次的乔治·博克斯:“所有的模型都是错的,但有些是有用的。”

机器学习算法做出可重复的决策,并且在正确的控制条件下,这些决策可以摆脱许多人类决策中的认知偏差。关键是通过验证性能来确保我们的模型是有用的。在机器学习中,在未见过的数据上测试模型的过程称为交叉验证(有时也叫样本外测试)。

为了确保在有限数据集上估计的参数仍然适用于更多的数据,我们必须进行验证,以确保质量保持一致。验证时,我们通常将数据集拆分为至少两个部分,即训练集和测试集。我们在训练集上估计参数,然后在测试集上运行模型,以了解模型在未见过的数据点上的表现。这个过程在下图中有所示意:

图 4.9:交叉验证

通常,在机器学习中,我们会在划分训练集和测试集之前随机打乱数据点。然而,在时间序列中,我们会将较早的数据点用于训练,较新的数据点用于测试。例如,如果我们有一年的数据可用,记录了客户打开电子邮件的倾向,我们会用 9 个月的数据训练模型,利用 2 个月的数据验证模型,并在数据集上测试最终的性能。

验证集和测试集的使用可以被看作是一个嵌套过程,测试集检查涉及验证数据集的主要测试过程。通常,验证集和测试集的划分会被省略,因此数据集仅被拆分为训练集和测试集。

术语说明:虽然损失函数是训练模型优化的一部分,但度量用于评估模型。评估可以是事后进行的,也可以是在训练过程中作为附加信息进行的。在本节中,我们将讨论度量和损失函数。

最好的做法是,在项目开始时,先评估如何衡量性能。我们需要选择如何衡量性能,将商业问题转化为度量标准或损失。一些算法允许在选择目标函数时具有灵活性,另一些则不允许,但我们可以通过不同的度量来衡量性能。

接下来,我们将讨论回归和分类的误差与损失度量。

时间序列的误差度量

时间序列数据被定义为包含有关不同时间点的详细信息的数据点集合。通常,时间序列数据包含在相等时间间隔内采样或观察到的数据点。

对于我们之前讨论的不同应用,我们需要能够量化模型的性能,无论是回归、分类还是其他类型的模型,并选择一个能够捕捉我们希望达到的性能的指标。一旦选择了适合我们模型的指标,我们就可以构建并训练模型以改善它们。通常,我们会从一个较简单的模型开始,然后尝试在这个简单模型的基础上提高性能。最终,我们希望找到一个根据我们指标表现最好的模型。

在这一部分,我们将讨论常用的性能度量及其特性。通常,对于误差度量,值越小,预测(或预测结果)越好。在改变模型参数时,我们希望减少误差。

并没有单一的指标适用于任何任意应用或数据集。根据数据集的不同,你可能需要尝试不同的误差指标,看看哪一个最能捕捉你的目标。在某些情况下,你甚至可能想要定义你自己的指标。

回归

时间序列回归是识别与时间序列行为相关的特征中的模式和信号的任务,例如,技能如何随着练习时间的投入而提高。

在训练过程中,当你的回归模型在训练集上给出结果时,我们可以使用一个指标来将模型输出与训练集值进行比较;在验证过程中,我们可以计算相同的度量,以了解我们的回归预测与验证集目标的匹配程度。误差指标总结了机器学习模型预测值与实际值之间的差异。

如果!是模型在时间步t的预测值,而实际目标值是y[t],直观地说,对于数据集中的某一点t预测误差(也叫预测误差残差)是目标实际值与我们模型预测值之间的差异:

该图比较了实际目标 Y 与预测目标!。根据这个公式,当预测值高于实际目标值时,误差为负。残差的平方和(SS,也叫残差平方和)忽略了误差的方向:

尽管残差和平方残差已经可以用来衡量时间序列预测的性能,但它们并不常用作回归指标或损失函数。

让我们从回归中最常用的度量开始:决定系数。这是一个相对简单的公式,基于残差平方和 SS 与总平方和 TSS 的比率,TSS 是一种变异性度量:

在这个分数中,分子是残差的平方和,SS,表示未解释的方差。

分母是 TSS,总平方和。它被定义为 ,其中 是序列的均值,。总平方和表示时间序列的解释方差。

基本上,我们是在衡量残差的平方和与时间序列总方差的关系。这个分数介于 0 和 1 之间,0 表示最好——没有误差,1 表示最差。通过从 1 中减去该值来反转它,因此最终 0 是最差的,1 是最好的。

扩展后,结果如下所示:

表示可以从自变量预测的因变量方差的比例。如前所述,它的值在 0 和 1 之间,其中 1 表示存在完美的关系,0 表示没有任何关系。

决定系数,,不是一个误差度量,因为误差度量表示残差的分布,高的不好,低的好。然而,我们可以表达一个误差度量,称之为r-误差(RE),它与上述非常相似,定义如下:

这种方法在实际中很少使用。一个与 RE 非常相似的误差度量是均值相对绝对误差MRAE),我们将在后面进一步讨论。

天真地说,我们可以取平均误差,即仅对预测误差求均值——即均值误差:

这里,N 是点的数量(或离散时间步数)。我们计算每个点的误差,然后对这些误差取平均。

如果 ME 为正,模型会系统性地低估目标值;如果为负,则会高估目标值。虽然这可能有用,但作为误差度量,这仍然是一个严重的问题,因为正误差和负误差的效应会相互抵消。因此,低 ME 并不意味着预测很好,而是表示平均值接近零。

此外,大多数回归模型包括一个常数项,该常数等于目标的均值,因此这个值将正好为 0。总之,我们的天真度量在实际设置中是无用的。

我包括了 ME 来讨论为什么大多数常用的度量会忽略误差的方向,并强调基本误差指标的主要组成部分的重要性:

  • 残差操作

  • 积分

在均值误差(ME)的情况下,残差操作是恒等函数,这意味着残差不会改变。更常见的是使用平方或绝对值函数。误差的整合通常是(算术)平均值,但有时也可以是中位数;不过,它也可以是更复杂的操作。

实际应用中,最常用的误差度量是均方误差(MSE)、平均绝对误差(MAE)和均方根误差(RMSE)。以下表格定义了这些最重要的误差度量:

指标名称 定义
均方误差
平均绝对误差
均方根误差

图 4.10:常见的回归指标

对于均方误差(MSE),我们计算每个点的残差,然后对它们进行平方,这样正误差和负误差就不会互相抵消。然后我们取这些平方误差的平均值。MSE 为 0 表示完美表现。这种情况可能出现在你玩弄的小数据集上;然而,实际上,只有在你在构建数据集或验证过程中出错时,才会出现这种情况,因为现实生活总是比你能通过模型捕捉到的更复杂。

平均绝对误差(MAE)与 MSE 非常相似,不同之处在于,我们没有对残差进行平方,而是取它们的绝对值。与 MSE 不同,所有误差都是线性贡献的(而不是被平方)。

选择绝对值与平方之间的一个主要区别在于如何处理异常值或极端值。平方函数对与众不同的值赋予更高的权重。在 MSE 中,误差是按平方而不是线性增长的,这意味着 MSE 对极端值的惩罚远远比 MAE 更强,因此它对数据集中的异常值不如 MAE 稳健。误差分布是选择合适的误差度量时的重要考虑因素。

另一个常见的指标是均方根误差(RMSE),或均方根偏差(RMSD),顾名思义,它是均方误差(MSE)的平方根。从这个意义上说,RMSE 是 MSE 的缩放版本。选择这两个指标中的哪一个,取决于展示的需求——它们都会导致相同的模型。

RMSE 作为选择的有趣之处在于,它与预测变量具有相同的单位和尺度,使得它更加直观。最后,RMSE 等同于标准差或误差。标准差与误差分布之间的联系非常有意义,你可以用其他度量来总结误差分布,比如标准误差或置信区间(我们在第二章《使用 Python 进行时间序列分析》中讨论过这两者)。

还有许多其他的指标,它们各自有其作用。下表总结了时间序列建模中一些更常见的误差指标:

指标名称 定义
中位数绝对误差
均值绝对百分比误差
对称均值绝对百分比误差
归一化均方误差

图 4.11:更多回归指标

中位数绝对误差 (MdAE) 类似于 MAE。但是,与均值操作不同,中位数被用来进行集成。由于中位数不受尾部数据的影响,因此这个度量比 MAE 更加稳健。

均值百分比误差 (MAPE) 是通过目标值标准化的平均误差。0 代表完美的模型,值大于 1 意味着模型的预测系统性地高于目标值。MAPE 没有上限。此外,由于它是基于目标的百分比(通过目标进行缩放或除法运算),正残差和负残差会被区别对待。因此,如果预测值大于目标值,MAPE 会比同样方向的误差要高。根据残差的符号,MAPE 会更高或更低!

常见的分母选择是目标值;但是,你也可以通过预测值和目标值的均值进行缩放。这被称为对称均值绝对百分比误差 (SMAPE)。SMAPE 不仅有下限,还有上限,这使得百分比更加易于理解。

缩放也可以带来不同的好处。如果你想比较在不同数据集上验证的模型,那么之前提到的度量方法将无法提供帮助。训练集、验证集和测试集的划分是随机的,因此,当你比较模型的表现时,这些度量方法会混淆数据集差异在验证集中的影响以及模型性能本身的影响。

因此,归一化均方误差 (NMSE) 作为一种展示选择,比 MSE 更直观,因为它根据偏差对模型的表现进行缩放。NMSE 在将 MSE 除以目标方差后进行归一化。

还有许多其他的误差度量。某些误差度量将预测值与返回平均目标值的简单模型的预测进行比较。

该简单模型的预测性能为:

我们可以通过将预测误差除以该简单模型的预测误差来对预测误差进行归一化。

这样,我们可以定义几个其他的度量:

指标名称 定义
均值相对绝对误差
中位数相对绝对误差
相对均方根误差

图 4.12:归一化回归指标

如果你心中有朴素模型的概念,并且想通过相同的误差度量来比较朴素模型的表现,那么这些度量应该是直观的。

均值相对绝对误差MRAE)与决定系数非常相似,唯一的区别是 MRAE 使用平均值而不是总和。

另一个误差是均方根对数误差RMSLE)。

度量名称 定义
均方根对数误差

图 4.13:均方根对数误差

对于 RMSLE,基本操作是对残差取对数。这是为了避免在预测值和真实值都很大的情况下,误差的巨大差异被惩罚得过重。由于对数在 1 处有拐点,RMSLE 具有一个独特的特性,它对低估实际值的惩罚比对高估实际值的惩罚更严重。这在误差分布不遵循正态分布时非常有用,类似于我们在第三章时间序列预处理中讨论的缩放操作。

如果我们考虑基于熵的度量,比如泰尔不确定性,我们可以扩展度量的数量。泰尔不确定性(Theil's U)是一个标准化的预测误差度量。U 的值介于 0 和 1 之间,其中 0 表示完美拟合。它基于条件熵的概念,还可以作为不确定性度量,甚至可以作为类别-类别情况下的相关性度量。

如这些标题所示,前两项专注于量化模型的表现。最后一部分对于基于距离的模型非常有用,这些模型通常作为性能的坚实基准。

现在我们切换到分类任务的误差度量。

分类

许多度量特定于二分类任务(即只有两个类别),尽管其中一些可以扩展到多类别分类任务,其中类别数大于两个。

在二分类中,我们可以通过混淆矩阵将预测结果与实际结果进行对比,其中预测结果与实际结果会交叉列出,如下所示:

实际结果
假阴性(TN)
预测结果 假阴性(TN) 假阳性(FP)
真正 假阳性(FP) 真阳性(TP)

图 4.14:混淆矩阵

这是分类任务中的一个关键可视化图表,许多度量都是基于对这一图表的总结来进行的。

分类任务中最重要的两个度量是精确度和召回率。召回率是正确预测的正实例数与所有正实例数之比。我们也可以在混淆矩阵中按如下方式表达:

召回率也叫做真正正例率灵敏度。它关注的是正确预测的正例,忽略了负例;然而,我们也可能想知道正例预测的准确性。这就是精度,定义如下:

我们可以通过以下方式可视化这两个指标:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/(A Document Being Saved By screencaptureui 12)/Screenshot 2021-04-26 at 00.05.38.png

图 4.15:假正例(FP)、真正正例(TP)和假负例(FN)

在这张图中,假正例FP)、真正正例TP)和假负例FN)被显示出来。你可以看到实际上为真的实例,模型将其分类为真的实例,以及两者的交集——既为真的实例,且被模型分类为真的实例。

我们可以快速计算精度和召回率。我们有四个真正的正例和六个假正例。因此,此示例的精度为!

我们有四个假负例。召回率为!

召回率和精度显然都很重要,那么为什么不将它们结合起来呢?得分是精度和灵敏度的调和平均值:

我们还可以参数化召回率和精度的相对重要性。这是得分的广义版本,即得分:

另一个非常有用的指标来自接收者操作特征曲线(ROC),它绘制了在不同阈值设置下,真正正例率TPR)与假正例率FPR)之间的关系。假正例率,也称为假警报率,与真正正例率(召回率)的定义类似,如下所示:

ROC 图展示了灵敏度和特异性之间的关系,其基本思路是,很难做到两者兼得,同时找到所有正实例(灵敏度)并做到正确分类。往往你需要在灵敏度和特异性之间做出折衷。这个图形展示了你的模型如何处理这个问题。曲线下面积总结了这个图形,是实践中常用的一个指标。

另一个不太常见的指标是相关比率,它由卡尔·皮尔逊(Karl Pearson)提出,作为分类-连续型关联的度量:

其中!是类别x中观测值的数量,我们定义为:

相关比率是基于各个类别内的方差与整个数据集的方差之间的比例。!的取值范围为[0,1],其中 0 表示该类别无关联,1 表示该类别与绝对确定性相关。

在接下来的部分,我们将考察时间序列之间的相似度度量。

比较时间序列

相似度度量在时间序列索引、搜索检索、聚类、预测、回归和分类中有广泛应用,但是如果我们想决定两个时间序列是否相似,应该如何衡量相似度呢?

最简单的方式是使用皮尔逊相关系数;然而,其他度量方法可能更具信息性。

我们将通过一系列度量来比较一对时间序列:

  • 欧几里得距离

  • 动态时间规整

  • 格兰杰因果关系

欧几里得距离,一种通用距离,适用于任何一对向量,包括时间序列:

欧几里得距离在某些情况下是有用的;然而,在实践中,对于时间序列,你可以做得更好。你可以对经过快速傅里叶变换到频域的时间序列使用欧几里得距离。

直观地,时间序列中事件的精确时间位置及其持续时间可能会有所不同。动态时间规整DTW)是衡量两个可能在速度上有所不同的时间序列相似性的算法之一。直观地,时间序列中事件的精确时间位置及其持续时间可能会有所不同。时间序列之间的相似度度量应该能够处理这些时间的位移和延长。

一般来说,DTW 是一种根据启发式方法计算两个给定时间序列之间最佳匹配的算法,具有某些限制和规则。基本上,它尝试将第一个序列的索引与另一个序列的索引匹配。DTW 是一种编辑距离——它表示将序列 t1 转换为 t2 的代价。

由于 DTW 能够处理不同的速度,它已经被应用于自动语音识别。然而,DTW 在量化不匹配序列之间的不相似性方面存在问题。

DTW 应用于每个特征维度,然后可以将距离相加。或者,规整可以通过计算两个点之间的欧几里得距离来同时覆盖所有特征。因此,这种依赖规整()是一种多变量方法。

格兰杰因果关系用于判断一个时间序列是否能够帮助预测另一个时间序列。尽管该度量中的真正因果关系问题存在争议,但该度量考虑了一个序列在时间上先于另一个序列的值,可以认为该度量显示了时间关系或预测意义上的关系。

格兰杰因果关系在其思想和公式中都非常直观。它的两个原则(简化版)是:

  1. 原因必须先于结果

  2. 原因对结果有独特的影响

因此,如果我们可以拟合一个模型,表明 X 和 Y 之间存在一种关系,其中 Y 系统地跟随 X,这意味着 X 对 Y 具有格兰杰因果关系。

用于时间序列的机器学习算法

时间序列机器学习中的一个重要区别是单变量与多变量的区别,单变量算法只能处理单一特征,而多变量算法则可以处理多个特征。

在单变量数据集中,每个案例只有一个序列和一个类别标签。早期的模型(经典建模)侧重于单变量数据集和应用。这也体现在数据集的可用性上。

时间序列数据集最重要的存储库之一是UCR加利福尼亚大学河滨分校)档案,首次发布于 2002 年,已为单变量时间序列提供了宝贵的资源。现在它包含大约 120 个数据集,但缺少多变量数据集。此外,M 竞赛(特别是 M3、M4 和 M5)提供了大量的时间序列数据集。

多变量时间序列是具有多个特征维度的数据集。许多现实生活中的数据集本质上是多变量的——在实践中,多变量情况比单变量情况要常见得多。示例包括人类活动识别、基于心电图ECG)、脑电图EEG)、脑磁图MEG)的诊断以及系统监控。

最近(Anthony Bagnall 等人,2018 年)创建了UAE东英吉利大学)档案,包含 30 个多变量数据集。另一个多变量数据集档案是 MTS 档案。

在下一节中,我们将简要讨论基于距离的方法。

基于距离的方法

在我们之前提到的 k 近邻方法(简称 kNN)中,训练样本被存储,然后在推理时,当需要对一个新的数据点进行预测时,预测是基于最接近的 k 个邻居。这需要一种样本之间的距离度量。

我在本章早些时候介绍了用于时间序列的两种度量方法,动态时间规整DTW)和欧几里得距离。许多基于距离的方法采用这些作为距离度量。

另一种尝试过的方法是从时间序列中提取特征,然后将这些提取的特征存储起来,通过 kNN 进行检索。这些特征包括形状特征或尺度不变特征SIFT)。SIFT 特征是从时间序列中提取的形状,围绕着极值点(Adeline Bailly 等人,2015 年)。

我们在第三章《时间序列预处理》中已经分别讨论了形状特征和 ROCKET,因此我们将简要描述它们,但重点放在它们在机器学习中的应用。

形状特征

我们在第三章中已讨论过形状元(shapelets),时间序列预处理,因此在这里简要介绍。时间序列的形状元在研究论文“时间序列形状元一种新技术,可以实现准确、可解释且快速的分类”(Lexiang Ye 和 Eamonn Keogh,2011)中提出。形状元的基本思想是将时间序列分解为具有判别力的子部分(称为形状元)。基于形状元特征,已经提出了一些方法。

形状元变换分类器STC;Hills 等人,2014)由将形状元作为特征转换,然后将形状元输入到机器学习算法中组成。他们测试了 C4.5 决策树、朴素贝叶斯、1NN、SVM 和旋转森林,但在分类设置中没有发现这些方法之间的显著差异。

广义随机形状元森林gRFS;Karlsson 等人,2016)遵循随机森林的思路。每棵树是基于一组随机长度的形状元构建的,这些形状元从每棵树的一个随机维度中提取。然后在这些形状元上训练决策树。这些随机形状元树被整合为集成模型,构成 gRFS。

ROCKET

我们在第三章中已解释过 ROCKET,时间序列预处理。每个输入特征通过 10,000 个随机内核单独转换(这个数字可以更改)。在实践中,这是一个非常快速的过程。这些转换后的特征可以输入到机器学习算法中。其发明者 Angus Dempster、François Petitjean 和 Geoff Webb 在原始出版物(2019 年)中推荐了线性模型。

最近,发布了一个新的变体 MINIROCKET,其速度是 ROCKET 的约 75 倍,同时保持大致相同的准确性——MINIROCKET:一个非常快速(几乎)确定性的时间序列分类变换Angus Dempster, Daniel F. Schmidt, 和 Geoff Webb, 2020)。

在机器学习研究中,关键差异(CD)图是比较多个算法结果的强大可视化工具。平均排名表示算法之间的相对表现(排名越低越好)。算法结果通过统计方法进行比较——一条水平线连接算法,表示它们之间的差异无法在统计学上分开。

这里是一个关键差异图,展示了 MiniRocket 与其他算法的比较性能(来自 Dempster 等人发布的 MiniRocket 代码库):

图 4.16:MiniRocket 在 109 个 UCR 数据集上的准确率与其他最先进方法的平均排名

这些数字显示了算法在 109 个数据集上的排名。我们可以看到,MiniRocket 优于 Rocket,但不如 TS-CHIEF 和 HIVE-COTE,尽管它们之间的差异在统计学上并不显著。

我们将在第十章时间序列深度学习中讨论 InceptionTime。其他提到的方法将在接下来的章节中介绍。

时间序列森林和典型区间森林

时间序列森林TSF;由 Houtao Deng 等人于 2013 年提出)的主要创新是引入了作为树节点分割标准的入口增益。他们展示了基于简单特征(如均值、偏差和斜率)的集成分类器,优于使用动态时间规整(DTW)的 1NN 分类器,同时在计算上也更为高效(由于并行处理)。

接近森林(PF),由 Geoff Webb 领导的一组研究人员提出,是一个基于每个时间序列与一组参考时间序列相似度(基于距离的特征)的树集成。他们发现 PF 的分类性能与 BOSS 和形状转换(Shapelet Transforms)相当。

TS-CHIEF,即时间序列异质与集成嵌入森林(Time-Series Combination of Heterogeneous and Integrated Embedding Forest)的缩写,来自同一组研究人员(Ahmed Shifaz、Charlotte Pelletier、François Petitjean 和 Geoff Webb,2020 年),它通过引入基于字典的(BOSS)和基于区间的(RISE)分割器,扩展了 PF,同时保持了 PF 中引入的原始特征。作者声称,根据数据集的大小,它的运行速度比 HIVE-COTE 快 900 倍到 46,000 倍不等。

典型区间森林CIF;由 Matthew Middlehurst、James Large 和 Anthony Bagnall 于 2020 年提出)的理念是通过引入 Catch22 特征来扩展 TSF。它是一个基于 22 个 Catch22 特征和从相位依赖区间提取的总结统计量的时间序列树集成。他们还为树使用了入口增益标准。

在下一节中,我们将描述符号方法的演变,从 BOSS 到时间字典集成TDE)。

符号方法

符号方法是将数值时间序列转换为字母表符号的方法。

符号聚合近似法SAX)首次由 Eamonn Keogh 和 Jessica Lin 于 2002 年提出。它扩展了分段聚合近似法PAA),后者计算时间序列中各相等片段内的均值。在 SAX 中,这些均值被量化(分箱),因此字母表对应于原始数值区间。两个重要的参数是 PAA 中的片段数和箱的数量。

下图(来自 Thach Le Nguyen 的 MrSEQL GitHub 库)展示了 SAX 的工作原理:

图 4.17:SAX

你可以将这些片段视为沿X轴排列的网格,将这些箱视为沿Y轴排列的网格。每个片段都会被其均值所替代。时间序列通过在每个片段中用箱 ID(图中的字母)替代,从而被离散化。

符号傅里叶近似SFA;Patrick Schäfer 和 Mikael Högqvist,2012)也将时间序列转换为符号表示,但使用频域。首先,通过执行离散傅里叶变换、低通滤波,然后量化,从而减少数据集的维度。

SFA 符号包(BOSS;Patrick Schäfer,2015 和 2016)基于 n-gram 的直方图,通过 SFA 表示来形成模式包BoP)。BOSS 已经扩展为向量空间中的 BOSSBOSS VS)。BOSS VS 分类器比现有的最先进技术快一到四个数量级,并且比 1-NN DTW 更为准确。

合同 BOSS(cBOSS;Matthew Middlehurst,William Vickers,和 Anthony Bagnall,2019)通过引入限制基模型数量的新参数来加速 BOSS。

SEQL(Thach Le Nguyen,Severin Gsponer,和 Georgiana Ifrim,2017)是一种符号序列学习算法,利用贪婪的梯度下降方法选择最具辨别性的子序列以供线性模型使用。如下所示(来自 MrSEQL GitHub 仓库):

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/(A Document Being Saved By screencaptureui 11)/Screenshot 2021-04-25 at 23.48.05.png

图 4.18:SEQL

多重表示序列学习器(MrSEQL;Thach Le Nguyen,Severin Gsponer,Iulia Ilie,和 Georgiana Ifrim,2019)通过在多个分辨率和多个领域中选择转换特征,扩展了 SEQL。

WEASEL+MUSE(Patrick Schäfer 和 Ulf Leser,2017 和 2018)由两个阶段组成。WEASEL 代表时间序列分类的词提取,而 MUSE 代表多变量无监督符号与导数。这一点值得强调——虽然 WEASEL 是单变量方法,MUSE 将此方法扩展到了多变量问题。

在第一步中,WEASEL 从多个长度的窗口中提取特征,这些窗口来自截断的傅里叶变换和离散化。这一过程类似于低通滤波器,仅保留前l个系数。然后,这些系数被离散化为固定大小的字母表,并在直方图中计数为模式包BOP)。这是为每个特征单独完成的。

在第二步(MUSE)中,直方图特征在不同维度上进行连接,并使用统计检验(χ2 检验)进行基于滤波器的特征选择,结果是一个更小但更具辨别力的特征集。

然后,这些 BOP 被输入到逻辑回归算法中进行分类。

HIVE-COTE

基于转换集成的分层投票集体HIVE-COTE)是当前分类准确度方面的最先进技术。

该方法在 2016 年提出,并在 2020 年进行了调整(Anthony Bagnall 等人,2020),它是一种集成方法,结合了不同方法的异质集合:

  • 形状转换分类器STC

  • 时间序列森林 (TSF)

  • 符号傅里叶近似符号的可收缩袋 (CBOSS)

  • 随机区间光谱集成 (RISE)

随机区间光谱集成(RISE)是一个基于树的时间序列分类算法,最初在 HIVE-COTE(Jason Lines,Sarah Taylor,和 Anthony Bagnall,2016)发布时作为随机区间特征RIF)引入。在 RISE 的每次迭代中,提取一组傅里叶、自相关和部分自相关特征,并训练决策树。RISE 的运行时复杂度是与序列长度的平方成正比,这可能是一个问题,且已经发布了新版本c-RISEc代表收缩),该算法可以提前停止。

HIVE-COTE 的运行时复杂度是与序列长度的平方成正比,这是其应用的最大障碍之一。STC 和另一个模型,弹性集成EE)是 2016 年原始算法中两个最慢的基础模型。新版本(1.0)的主要区别之一就是去掉了 EE。他们重新实现了 STC 和 BOSS,使它们更高效,并用 c-RISE 替代了 RISE。

这些基础学习器分别训练。基础学习器基于交叉验证 准确度加权概率集成CAWPE)结构(James Large,Jason Lines,和 Anthony Bagnall,2019)按概率加权。

在 HIVE-COTE 1.0 之后的出版物中,该团队展示了通过将 CIF 替换为 TSF(2020)并将 BOSS 替换为时间字典集成TDE,2021),集成效果更强。

在下一节中,我们将讨论不同方法的性能和权衡。

讨论

通常,准确性与预测时间之间存在权衡,在这些方法中,时间复杂度和模型准确性差异巨大。此图表展示了这种妥协(来自 Patrick Schäfer 的 GitHub SFA 仓库):

图 4.19:机器学习算法:查询时间与准确度的关系

特征可能是简单操作的结果,或本身就是机器学习模型的输出。我们可以将二阶特征看作是原始特征的组合,三阶特征则是二阶特征的组合,依此类推,形成一个可能庞大的预处理管道,其中特征被组合和创建。

我们可以在下表中总结不同的算法:

类型 单变量 多变量
基于距离 DTW,邻近森林(PF) DTW-D
基于字典/符号 BOSS,CBOSS,S-BOSS,WEASEL,时间字典集成(TDE),SAX-VSM,BOSS WEASEL+MUSE
形状特征 形状变换分类器(STC),MrSEQL
基于区间和光谱 时间序列森林(TSF),随机区间光谱集成(RISE)
深度学习 ResNet,FCN,InceptionTime TapNet
集成 基于变换集成的分层投票集合(HIVE-COTE)、时间序列异构与集成嵌入森林(TS-CHIEF)

图 4.20:时间序列机器学习算法的详细分类

这个分类远非完美,但希望对你有所帮助。TDE 既是一个集成模型,也是一个基于字典的模型。HIVE-COTE 基于 BOSS 特征。此外,两个特征化方法——随机卷积核变换(ROCKET)和规范时间序列特征(Catch22),分别对特征进行操作;然而,基于这些特征作为输入进行训练和预测的机器学习算法,能够在多变量环境下工作。实际上,ROCKET 特征与线性分类器结合的表现,确实与多变量方法竞争力强。由于高维度,机器学习模型有可能考虑到原始特征之间的交互作用。

我强烈推荐给读者的一篇综述文章是 "The great multivariate time-series classification bake-off",由 Alejandro Pasos Ruiz、Michael Flynn 和 Anthony Bagnall(2020)撰写。文章对 26 个来自 UAE 档案的多变量数据集上的最先进算法(其中 16 个在分析中被纳入)进行了比较。所涉及的算法包括以下几种:

  • 动态时间规整

  • MUSE+WEASEL

  • RISE

  • CBOSS

  • TSF

  • gRSF

  • ROCKET

  • HIVE-COTE 1.0

  • CIF

  • ResNet

  • STC

临界差异图(见于timeseriesclassification.com)显示了 26 个数据集上算法的排名。算法之间的链接表明,它们之间的差异不能通过统计方法分离(基于 Wilcoxon 秩和检验):

../../../../Desktop/Screenshot%202021-04-25%20at%2021.24

图 4.21 时间序列分类算法的临界差异图

他们发现了一组表现最佳的分类器,ROCKET 位居榜首,在更短的时间内取得了至少数量级的显著提升。紧随其后的是 HIVE-COTE 和 CIF。

在 2019 年的一项研究中,Hassan Fawaz 等人比较了 12 个来自 MTSC 档案的多变量数据集上的深度学习算法。全连接卷积网络(FCN)表现最佳,其次是 ResNet——在来自 UCR 仓库的 85 个单变量数据集中,ResNet 超越了 FCN,取得了第一的位置(在 85 个数据集中的 50 个中获胜)。在一项仅涉及 ResNet 与一些最先进的非深度学习方法的对比中,他们发现 ResNet 的表现不如 HIVE-COTE,尽管在数据集之间并没有显著的差距,同时超越了其他方法,如 BOSS 和使用 DTW 的 1NN(后者在统计上具有显著差异)。我们将在第十章时间序列的深度学习中进一步讨论这篇文章。

在另一个关于 20 个数据集的多变量时间序列分类的比较研究中(Bhaskar Dhariyal, Thach Le Nguyen, Severin Gsponer, 和 Georgiana Ifrim,2020),结果表明,ROCKET 在 14 个数据集上获胜,并且比大多数深度学习算法表现得要好,同时也是最快的方法——ROCKET 在 20 个数据集上的运行时间为 34 分钟,而 DTW 则需要几天。

这是通过哈桑·法瓦兹(Hassan Fawaz)的 Python 脚本从他们的结果中创建的关键图表:

../../../../Downloads/Machine-Learning%20for%20Time-Series%20with%20Python/mtsc_cd-di

图 4.22:多变量时间序列分类的关键差异图

尝试了许多不同的特征集,其中最佳的(9_stat_MulPAA)结果并不远离理想值。

实现

伟大的算法如果没有提供它们的可靠软件,实际上会变得价值不大。这样的软件能够让它们易于使用,并且在公司生产环境中可靠地使用。另一方面,从零开始实现算法可能需要时间,并且也不是没有复杂性。因此,Python 中有许多可靠且可用的实现是一个巨大的福音。

以下表总结了回归和分类的监督算法实现:

算法 sktime Pyts
自回归积分滑动平均(ARIMA) X
DTW X X
BATS X
MUSE+WEASEL X X
MrSEQL X
ROCKET X X
BOSS X X
向量空间中的 SFA 符号袋(BOSSVS) X
CBOSS X
SAX-VSM X
RISE X
HIVE-COTE X
时间序列森林 X

图 4.23:Pyts 与 SkTime 机器学习算法实现对比

sktime 有这么多实现并非偶然。它在东英吉利大学安东尼·巴格纳尔(Anthony Bagnall)团队的研究活动中得到了广泛应用。Pyts 由约翰·法乌兹(Johann Faouzi)和希沙姆·贾纳提(Hicham Janati)维护,他们是巴黎脑科学研究所和应用数学中心CMAP)的博士后研究员。约翰·法乌兹还是实现时间序列分析和特征提取算法的 tslearn 库的幕后推手。

我省略了深度学习算法,因为它们通常作为不同库的一部分实现。请注意,sktime 通过相同的接口支持使用 prophet 预测器。例如,sktime-DL 库实现了 ResNet、InceptionTime 和 TapNet 算法,而 dl-4-tsc 实现了十多个深度学习模型。我们将在第十章时间序列的深度学习中讨论深度学习模型的实现。

Facebook 的 Prophet 包含一个单一模型,是广义加性模型GAM)的特殊情况。Statsmodels 库包含一个 GAM 模型,以及线性回归模型和广义线性模型GLM)、移动平均MA)、自回归积分滑动平均模型ARIMA)和向量自回归模型VAR)。

Darts 库提供了一个一致的接口,可以访问多种时间序列处理和预测模型。它包括经典算法和深度学习算法:

  • 指数平滑

  • ARIMA

  • 时间卷积网络

  • Transformer

  • N-BEATS

这就是我们对 Python 中时间序列机器学习库的概述。

摘要

在本章中,我们讨论了时间序列机器学习的背景和技术背景。机器学习算法或模型可以基于数据做出系统的、可重复的、经过验证的决策。我们解释了与时间序列相关的主要机器学习问题,如预测、分类、回归、分割和异常检测。然后,我们回顾了与时间序列相关的机器学习基础知识,并考察了机器学习在时间序列中的历史和当前应用。

我们讨论了基于方法和使用的特征的不同类型方法。此外,我们讨论了许多算法,集中在最先进的机器学习方法上。

我将在专门的章节中讨论包括深度学习或经典模型(如自回归模型和移动平均模型)的不同方法(例如,在第五章使用移动平均和自回归模型进行时间序列预测,以及第十章时间序列的深度学习中)。

第五章:使用移动平均和自回归模型进行预测

本章介绍基于移动平均和自回归的时间序列建模。这个主题包含了一大类模型,在不同学科中都非常流行,包括计量经济学和统计学。我们将讨论自回归和移动平均模型,以及一些将这两者结合起来的模型,如 ARMA、ARIMA、VAR、GARCH 等。

这些模型仍然受到高度评价并且有广泛的应用。然而,自那时以来,许多新模型涌现出来,证明它们与这些简单模型具有竞争力,甚至能够超越它们。然而,在其主要应用领域,即单变量预测中,简单模型往往能够提供准确或足够准确的预测,因此这些模型在时间序列建模中仍然占据主导地位。

我们将涵盖以下主题:

  • 经典模型是什么?

    • 移动平均和自回归

    • 模型选择和阶数

    • 指数平滑法

    • ARCH 和 GARCH

    • 向量自回归

  • Python 库

    • statsmodels
  • Python 实践

    • 在 Python 中建模

我们将从经典模型的介绍开始。

经典模型是什么?

在本章中,我们将讨论那些有着更长传统的模型,这些模型根植于统计学和数学领域。它们在计量经济学和统计学中有着广泛应用。

虽然统计学和机器学习方法之间有相当多的重叠,而且两个领域的学者都在吸收对方的研究成果,但仍然存在一些关键差异。统计学论文仍然主要是正式和推理性的,而机器学习研究者则更加务实,依赖于模型的预测准确性。

我们在 第一章《Python 时间序列介绍》中讨论了时间序列模型的早期历史。在本章中,我们将讨论用于预测的移动平均和自回归方法。这些方法在 20 世纪初期被提出,并在 1970 年由 George Box 和 Gwilym Jenkins 在他们的著作《时间序列分析:预测与控制》中推广。关键的是,在这本书中,Box 和 Jenkins 使 ARIMA 模型得到了形式化,并描述了如何将其应用于时间序列预测。

许多时间序列展现出趋势和季节性,而本章中的许多模型假设时间序列是平稳的。如果一个时间序列是平稳的,它的均值和标准差会随着时间保持不变。这意味着该时间序列没有趋势,也没有周期性波动。

因此,去除不规则成分、趋势和季节波动是应用这些模型的一个内在方面。模型然后预测去除季节性和趋势后的残余部分:商业周期。

因此,应用经典模型时,时间序列通常需要分解为不同的组成部分。因此,经典模型通常按以下方式应用:

  1. 平稳性检验

  2. 差分处理【如果检测到平稳性】

  3. 拟合方法和预测

  4. 加回趋势和季节性

本章中的大多数方法仅适用于单变量时间序列。尽管已经提出了扩展到多变量时间序列的方法,但它们不像单变量版本那样流行。单变量时间序列由单个向量组成,换句话说,就是一个随时间变化的值。尽管如此,我们将在本章末尾看到向量自回归VAR),这是一种多变量时间序列的扩展。

另一个重要的考虑因素是,大多数经典模型都是线性的,这意味着它们假设时间点之间以及不同时间步长之间的值之间存在线性依赖关系。实际上,本章中的模型在处理一系列平稳时间序列时效果良好。平稳性意味着分布随时间保持不变。一个例子就是温度随时间的变化。这些模型在数据量较小的情况下尤其有价值,因为在这种情况下,非线性模型中的额外估计误差会超过精度上的潜在增益。

然而,平稳性假设意味着本章中模型的应用仅限于具有这一属性的时间序列。否则,我们就需要对时间序列进行预处理,以强制其平稳性。相比之下,非线性时间序列分析与预测的统计方法发展较少受到关注;然而,仍然存在一些模型,例如阈值自回归模型(我们在这里不讨论)。

最后,需要指出的是,虽然这是一个合理的初步方法,但许多时间序列(如温度)的预测通过基于物理的大气高维模型,比通过统计模型更为准确。这说明了复杂性的问题:本质上,建模是将一组假设凝练并与参数一起形式化的过程。

现实世界的时间序列来自复杂的过程,这些过程可能是非线性和非平稳的,描述它们的方式有很多种,每种方法都有其优缺点。因此,我们可以从很多参数的角度看待建模问题,或者仅将其视为单一或几个参数。在下面的专门部分,我们将讨论如何根据参数数量和精度,从一组备选模型中选择一个模型的问题。

目前,非线性模型来源于不同的研究方向,主要是神经网络或更广泛的机器学习领域。我们将在第十章中讨论神经网络,《时间序列的深度学习》,并将在第七章中讨论并应用最先进的机器学习方法,《时间序列的机器学习模型》

移动平均和自回归

经典模型可以分为几类模型——移动平均MA)、自回归AR)模型、ARMA 和 ARIMA。这些模型经过时间的推敲,由许多数学家和统计学家在书籍和论文中正式化并普及开来,包括彼得·惠特尔(1951 年)和乔治·博克斯与吉威尔姆·詹金斯(1970 年)。不过,让我们从更早的时候开始。

移动平均标志着现代时间序列预测的开始。在移动平均中,通常会对过去一段时间内(时间框架)一定数量的时间点的值取平均(通常是算术平均)。

更正式地说,简单移动平均是一个无权重的平均值,计算范围为 k 个点,公式为:

其中 x[i] 表示观察到的时间序列。

移动平均可以用来平滑时间序列,从而去除短期内发生的噪声和周期波动,实际上起到低通滤波器的作用。因此,正如数学家雷金纳德·胡克在 1902 年的一篇出版物中指出的那样,移动平均可以用来分离趋势和振荡成分。他将趋势概念化为忽略振荡后,序列前进的方向。

移动平均可以平滑时间序列中的趋势和周期;然而,作为一种模型,移动平均也可以用于预测未来。时间序列是当前序列值与观察值(误差项)之间的线性回归。移动平均模型的阶数为 q,即 MA(q),可以表示为:

其中 x[t] 的平均值(期望值)(通常假设为 0), 是参数, 是随机噪声。

胡克在剑桥大学接受教育,曾在英国农业、渔业和食品部的统计部门工作。他是一个业余统计学家,撰写关于气象学和社会经济话题的文章,如工资、婚姻率、贸易以及作物预测等。

AR 技术的发明可以追溯到 1927 年,英国统计学家乌德尼·尤尔(Udny Yule)的论文(《On a Method of Investigating Periodicities in Disturbed Time-Series with special reference to Wolfer's Sunspot Numbers》),尤尔是胡克的朋友。自回归模型(autoregressive model)将变量与其自身的滞后值回归。换句话说,当前值由紧接其后的值通过线性组合驱动。

太阳黑子的变化具有高度周期性,正如这张显示太阳黑子随时间变化的图表所示(通过 statsmodels 数据工具加载):

../../../../../Downloads/Machine-Learning%20for%20Time-Series%20with%20Python/sunspot_act

图 5.1:按年划分的太阳黑子观测数据

尤尔提出了一个由噪声驱动的线性模型,用来应用于太阳黑子数目,即太阳外壳上的黑斑数量。这些黑斑源自巨大的爆炸,标志着太阳的磁活动,并且与日冕物质抛射等现象相关。

下面是根据太阳黑子数目展示的低太阳活动和高太阳活动的两幅图像(来自 NASA):

hat Will Solar Cycle 25 Look Like? | NASA

图 5.2:太阳活动

今天,我们知道太阳周期是太阳磁活动的一个几乎周期性的 11 年变化,表现为高磁活动(太阳极大期)和低磁活动(太阳极小期)之间的变化。在高点时,爆炸(太阳耀斑)会将带电粒子释放到太空中,可能危及地球上的生命。

尤尔(Yule)在伦敦大学学院UCL)学习工程学,后来与海因里希·赫兹(Heinrich Hertz)一起在波恩工作,随后回到 UCL 与卡尔·皮尔逊(Karl Pearson)合作,并最终晋升为 UCL 的助理教授。在 UCL 担任统计学职位后,他移居剑桥。他因其关于统计学的著作《统计学理论导论》而被人们铭记,这本书首次出版于 1911 年,经过了多个版本的修订。此外,他还因描述了现在被称为优先附着过程的现象而闻名,该过程描述了网络中新节点的分配方式是根据节点已经拥有的数量来决定的;这一过程有时被称为“富者愈富”。

安德烈·柯尔莫哥洛夫(Andrey Kolmogorov)在 1931 年定义了平稳过程这一术语,尽管路易·巴谢列(Louis Bachelier)在早些时候(1900 年)就用不同的术语提出了类似的定义。平稳性由三个特征定义:

  1. 有限变差

  2. 恒定均值

  3. 恒定变差

恒定变差意味着时间序列在两个点之间的窗口内的变化随着时间的推移保持恒定:,尽管它可能随窗口的大小变化。

这是弱平稳性。在文献中,除非另有说明,通常平稳性指的是弱平稳性。严格平稳性意味着时间序列具有随时间不变的概率密度函数。换句话说,在严格平稳性下,联合分布在 上与在 上是相同的。

1938 年,挪威数学家赫尔曼·奥勒·安德烈亚斯·沃尔德(Herman Ole Andreas Wold)描述了平稳时间序列的分解。他观察到,平稳时间序列可以表示为一个确定性成分(自回归)与一个随机成分(噪声)的和。如今,这一分解方法以他的名字命名,称为沃尔德分解

这导致了自回归模型的形式化,阶数为 AR(p),表示为:

其中 是模型参数,c 是一个常数,而 则代表噪声。在这个方程中,p 是时间序列连续值之间自相关性的度量。

此工作后来在 1951 年被新西兰人彼得·惠特尔的博士论文《时间序列假设检验》中推广到多变量时间序列中,彼得·惠特尔的导师是沃尔德。彼得·惠特尔也因将 AR 和 MA 模型整合为一个而受到赞誉,这就是自回归移动平均ARMA)的另一个里程碑,将尤尔和胡克的工作结合在一起。

ARMA 模型包括两种类型的滞后值,一种用于自回归分量,另一种用于移动平均分量。因此,我们写成ARMA(p, q),其中第一个参数 p 表示自回归的顺序,第二个q表示移动平均的顺序,如下所示:

ARMA 假设该系列是稳定的。在实践中,为了确保稳定性,必须应用预处理。

模型参数是通过最小二乘法估计的,直到乔治·Box 和 Gwilym Jenkins 推广了他们的最大似然估计参数方法。

乔治·Box 是不仅在古典时间序列预测领域中最有影响力的人物之一,也在更广泛的统计领域中卓有成效。在二战期间,他未完成化学学习就被征召入伍,为军队进行毒气实验,在此过程中自学了统计学以进行分析。

战争结束后,他在伦敦大学学院学习数学和统计学,并在埃冈·皮尔逊(Karl Pearson 之子)的指导下完成了他的博士学位。后来,他在普林斯顿大学领导了一个研究团队,然后在威斯康星大学麦迪逊分校创立了统计学系。

Box 和 Jenkin 的 1970 年著作 "Time-Series Analysis: Forecasting and Control",详细介绍了时间序列预测和季节调整的许多应用示例。所谓的 Box-Jenkins 方法是最流行的预测方法之一。他们的书籍还描述了自回归积分移动平均模型(ARIMA)。

ARIMA(p, d, q)包括一个数据预处理步骤,称为积分,使时间序列保持稳定,通过替换值来减去即时过去的值,这个转换称为差分

模型的积分由参数 d 参数化,它是当前值和先前值之间进行差分的次数。正如提到的那样,这三个参数代表模型的三个部分。

有一些特殊情况;ARIMA(p,0,0)代表 AR(p),ARIMA(0,d,0)代表 I(d),而 ARIMA(0,0,q)则是 MA(q)。I(0)有时被用作一个约定,指的是不需要任何差分即可保持稳定的时间序列。

虽然 ARIMA 类型模型有效地考虑了平稳过程,但作为 ARMA 模型扩展的 季节性自回归积分滑动平均 模型(SARIMA)可以描述在季节内外展现非平稳行为的过程。

季节性 ARIMA 模型通常表示为 ARIMA(p,d,q)(P,D,Q)m。各个参数需要进一步解释:

  • m 表示一个季节中的周期数

  • P、D、Q 参数化季节部分的自回归、差分和移动平均组件

  • p、d、q 是我们之前讨论过的 ARIMA 项。

P 是度量时间序列中连续季节组件之间自相关性的指标。

我们可以列出季节部分来使其更清楚。季节性自回归SAR,可以表示为:

其中 s 是季节性周期的长度。

类似地,季节性移动平均SMA,可以写作如下:

请注意,这些组件中的每一个将使用一组不同的参数。

例如,模型 SARIMA(0,1,1)(0,1,1)12 过程将包含一个非季节性 MA(1) 项(对应参数 ),以及一个季节性 MA(1) 项(对应参数 )。

模型选择与顺序

ARMA 中的参数 q 通常为 3 或更少,但这更多地反映了计算资源的限制,而非统计问题。如今,为了设置 p 和 q 参数,我们通常会查看自相关和偏自相关图,其中我们可以看到每个滞后的相关性峰值。

当我们有不同的模型,比如不同的 p 和 q 模型,每个模型都在相同的数据集上进行训练时,我们怎么知道应该使用哪个模型呢?这就是模型选择的作用。

模型选择是决定竞争模型之间选择的方法论。模型选择中的一个主要思想是奥卡姆剃刀原则,得名于英国方济各会修士、经院哲学家威廉·奥卡姆(William of Ockham),他生活在公元 1287 年至 1347 年间。

根据奥卡姆剃刀原则,在选择竞争性解决方案时,应优先选择假设最少的解释。奥卡姆基于这一思想主张,神圣干预的原则是如此简单,奇迹便是一个简洁的解释。这个规则,也叫做拉丁语中的 "lex parsimoniae",表明一个模型应当简洁,即使它简单,却应具有较高的解释力。

在科学中,出于可证伪性原则,通常偏好更简单的解释。科学解释越简单,越容易进行测试,甚至可能被推翻——这为模型提供了科学的严谨性。

ARMA 和其他模型通常通过 最大似然估计MLE)进行估计。在 MLE 中,这意味着最大化似然函数,以便在给定模型参数的情况下,观察到的数据是最有可能的。

最大似然法中最常用的模型选择标准之一是 赤池信息准则 (AIC),得名于 Hirotugu Akaike,他在 1973 年首次用英语发布了这一准则。

AIC 采用最大似然法中的对数似然值 l 和模型中的参数个数 k

这表示 AIC 等于参数个数的两倍减去对数似然的两倍。在模型选择中,我们会选择 AIC 最小的模型,这意味着它的参数少,但对数似然高。

对于 ARIMA 模型,我们可以更具体地写出:

我已经省略了参数 d,因为它不会引入额外的估计。

贝叶斯信息准则 (BIC) 是由 Gideon Schwarz 在几年前(1978 年)提出的用于模型选择的方法,和 AIC 非常相似。它额外考虑了 N,数据集中的样本数量:

根据 BIC,我们希望选择一个具有较少参数和较高对数似然的模型,但同时样本量也要小。

指数平滑

指数平滑,追溯至 Siméon Poisson 的工作,是一种通过指数窗口函数平滑时间序列数据的技术,可以用于预测具有季节性和趋势的时间序列。

最简单的方法,简单指数平滑SESs[t] 的时间序列 x[t] 可以表示为:

其中 是指数平滑因子(一个介于 0 和 1 之间的值)。

本质上,这是一个加权移动平均,权重为 α。你可以把第二项 看作递归的,在展开时, 会被重复相乘——这就是指数项。

参数 控制平滑值在当前值和前值之间的决定权重。与移动平均法相似,这个公式的效果是结果变得更加平滑。

有趣的是,John Muth 在 1960 年展示了 SES 在时间序列中提供了最佳预测,其中在每个时间步,值会随机偏离前一个值,而且步长是独立同分布的,加上噪声。这种时间序列称为随机游走,有时,波动的股票价格假定遵循这种行为。

另一种指数平滑方法是Theta 方法,它对从业者尤其具有吸引力,因为它在 2000 年的 M3 竞赛中表现出色。M3 竞赛得名于其组织者斯皮罗斯·马克里达基斯(Spyros Makridakis),他是尼科西亚大学的教授兼未来研究所的主任。该竞赛涉及 3003 个来自微观经济学、工业、金融、人口学等领域的时间序列。其主要结论之一是,非常简单的方法也可以在单变量时间序列预测中表现良好。M3 竞赛被证明是预测领域的一个转折点,提供了基准和最先进的技术SOTA),尽管自那时以来,SOTA 已经发生了显著变化,正如我们在第七章《时间序列的机器学习模型》中将看到的那样。

Theta 方法由瓦西里斯·阿西马科普洛斯(Vassilis Assimakopoulos)和科斯塔斯·尼科洛普洛斯(Konstantinos Nikolopoulos)于 2000 年提出,并由罗布·海德曼(Rob Hyndman)和巴基·比拉(Baki Billah)在 2001 年重新阐述。Theta 模型可以理解为简单指数平滑SES)带有漂移项。

该方法基于将去季节化数据分解为两条线。第一条所谓的“theta”线估计长期成分——趋势,然后将此趋势与 SES 的加权平均值相结合。

让我们更正式地阐述这一点!趋势成分的预测公式如下:

在这个方程中,c是截距,是乘以时间步长的系数,是残差。可以通过普通最小二乘法拟合。

Theta 的公式是将这一趋势与 SES 加权求和:

这里,是时间步长t时刻的X预测值。

最流行的指数平滑方法之一是霍尔茨-温特斯方法。芝加哥大学的查尔斯·霍尔茨教授于 1957 年首次发布了一种双重指数平滑方法,该方法允许基于趋势和水平进行预测。他的学生彼得·温特斯在 1960 年扩展了该方法,以捕捉季节性(“通过指数加权移动平均法预测销售额”)。即便在后期,霍尔茨-温特斯平滑法也被进一步扩展,以考虑多重季节性(n 阶平滑)。

为了应用霍尔茨-温特斯方法,我们首先去除趋势和季节性。然后我们预测时间序列并将季节性和趋势加回来。

我们可以区分方法的加性和乘法变体。趋势和季节性都可以是加性或乘法的。

加性季节性是独立于系列值添加的季节性。乘法季节性成分是按比例添加的,当季节效应随着时间序列中的值(或趋势)的增加或减少而变化时。通过视觉检查可以帮助决定使用哪种变体。

Holtz-Winters 方法也称为三重指数平滑法,因为它应用了三次指数平滑,正如我们所见。Holtz-Winters 方法捕捉了三个组成部分:

  • 对每个时间点的水平估计,L[t] —— 这可能是一个平均值

  • 趋势分量 T

  • 季节性 S[t],具有 m 个季节(即一年中的季节数)

对于加性趋势和季节性,在数学上,Holtz-Winters 预测值 定义为:

对于 乘法季节性,我们将其与季节性相乘:

水平更新如下:

我们基于两个项的加权平均来更新当前水平,其中 为两者之间的权重。这两个项分别是前一水平和去季节化后的系列值。

在这个方程中,我们通过除以季节性来进行去季节化:

之前的趋势分量会像这样被加到前一个水平:

趋势更新如下(对于加性趋势):

最终,(乘法)季节性更新如下:

我们可以根据需要将这些方程切换为加性变体。本书的范围之外会有更详细的讨论——我们就此止步。

ARCH 和 GARCH

麻省理工学院经济学教授 Robert F. Engle 提出了一个时间序列预测模型(1982),他将其命名为 ARCH自回归条件异方差)。

对于金融机构而言,风险价值(value at risk),即在特定时间段内的金融风险水平,是风险管理中的一个重要概念。因此,考虑资产收益的协方差结构至关重要。这就是 ARCH 所做的,也解释了其重要性。

事实上,鉴于他在时间序列计量经济学领域的贡献,Engle 与之前提到的 Clive Granger 一起,于 2003 年获得了诺贝尔经济学奖(诺贝尔经济科学纪念奖)。表彰中特别提到了他在 ARCH 方面的开创性工作。

在 ARMA 型模型中,收益被建模为独立且在时间上相同分布,而 ARCH 通过对收益在不同频率下的高阶依赖性进行参数化,从而允许时间变化的(异方差)误差项。

在 ARCH 中,残差表示为由一个随机项 z[t] 和一个标准差 组成,这两者都是时间相关的:

时间 t 处残差的标准差模型依赖于前期点的残差:

其中 q 是方差依赖的前期时间点的数量。

模型 ARCH(q) 可以通过最小二乘法来确定。

最小二乘法算法是求解线性方程 y=X.β 以获得 β。它的原理是找到最小化误差平方和的参数,即

GARCH广义自回归条件异方差模型)是在 Tim Bollerslev(1986)和 Stephen Taylor(1986)分别扩展 Engle 模型使其更具普遍性时诞生的。GARCH 和 ARCH 之间的主要区别在于,残差来自 ARCH 模型,而不是自回归模型 AR。

通常,在应用 GARCH 或 ARCH 模型之前,会进行同方差性的统计检验,换句话说,就是检验方差是否随时间保持恒定。常用的检验方法是 ARCH-LM 检验,零假设为时间序列没有 ARCH 效应。

向量自回归

本章介绍的所有预测方法都针对单变量时间序列,即由单一时间依赖变量组成的时间序列,一个单一向量。在实践中,我们通常知道比单一测量序列更多的信息。

例如,如果我们的时间序列是关于冰淇淋销量的,我们可能还知道温度或泳衣的销量。我们可以预期冰淇淋销量与温度高度相关,实际上,当气温较高时,冰淇淋的消费可能会增加。同样,我们也可以推测泳衣的销量与冰淇淋的销量要么同时发生,要么先于冰淇淋销量,或晚于冰淇淋销量。

向量自回归模型可以追踪多个变量随时间变化的关系。它们可以捕捉时间序列与当前时间戳之前的数值向量之间的线性依赖,将 AR 模型推广到多变量时间序列。

VAR 模型的特点是其阶数,即模型中考虑的前几个时间点的数量。最简单的情况是 VAR(1),其中模型只考虑时间序列的一个滞后项,公式如下:

其中 c 是常数,即直线的截距, 是模型的系数,而 是 t 时刻的误差项。x[t] 和 c 是长度为 k 的向量,而 是一个 矩阵。

p 阶模型,VAR(p),表示为:

VAR 假设误差项的均值为 0,且误差项之间没有序列相关性。

就像向量自回归是自回归的多变量推广,向量 ARIMAVARIMA)是对单变量 ARIMA 模型的扩展,用于多变量时间序列。尽管早在 1957 年就已经正式提出,但可用的软件实现直到后来才出现。

在下一节中,我们将介绍一些可以在 Python 中用于经典模型预测的库。

Python 库

Python 中有一些流行的经典时间序列建模库,但其中最流行的无疑是 statsmodels。以下图表比较了各库在 GitHub 上的星标数,显示它们的受欢迎程度:

forecasting_libraries.png

图 5.3:Python 经典时间序列预测库的受欢迎程度

Statsmodels 显然是这些库中最受欢迎的。我只选择了那些积极维护并直接实现算法的库,而不是从其他库导入的库。例如,SkTime 或 Darts 库提供传统的预测模型,但这些模型并未在它们那里实现,而是在 statsmodels 中实现的。

pmdarima(最初是 pyramid-arima)包含一个参数搜索功能,帮助拟合最佳 ARIMA 模型到单变量时间序列。Anticipy 包含一些模型,如指数衰减模型和阶梯模型。Arch 实现了金融计量经济学工具和 自回归条件异方差性ARCH)的功能。

尽管 statsmodels 的活跃度不如 Scikit-Learn,且仅由少数人维护,但它仍是时间序列传统统计和计量经济学方法的首选库,尤其在参数估计和统计检验上,强调程度远高于机器学习。

Statsmodels

statsmodels 库可以帮助估计统计模型并进行统计检验。它基于 SciPy 和 NumPy,包含许多统计函数和模型。

以下表格展示了与本章相关的一些建模类:

类别 描述
ar_model.AutoReg 单变量自回归模型
arima.model.ARIMA 自回归积分滑动平均(ARIMA)模型
ExponentialSmoothing Holt Winter’s 指数平滑
SimpleExpSmoothing 简单指数平滑

图 5.4:statsmodels 中实现的几个模型

ARIMA 类还通过 seasonal_order 参数支持 SARIMA,即带有季节性组件的 ARIMA。根据定义,ARIMA 还支持 MA、AR 和差分(集成)。

还有一些其他模型,如 Markov 自回归模型,但我们不会逐一讲解,我们将选择性地讲解一些。

这里列出了其他一些有用的函数:

函数 描述
stattools.kpss Kwiatkowski-Phillips-Schmidt-Shin 平稳性检验
stattools.adfuller 扩展的 Dickey-Fuller 单位根检验
stattools.ccf 互相关函数
stattools.pacf 偏自相关估计
stats.diagnostic.het_arch Engle 的自回归条件异方差性(ARCH)检验,也称为 ARCH-LM 检验
stattools.q_stat Ljung-Box Q 统计量
tsa.seasonal.seasonal_decompose 使用移动平均法进行季节性分解
tsa.tsatools.detrend 去趋势一个向量

图 5.5:statsmodels 中的有用函数

按照惯例,我们像这样导入 statsmodels:

import statsmodels.api as sm 

这些 statsmodels 算法也可以通过 SkTime 使用,它通过类似 Sklearn 接口的方式提供访问。

这应该足够提供一个简短的概览。接下来我们进入建模部分!

Python 实践

正如本章引言所述,我们将使用 statsmodels 库进行建模。

需求

在本章中,我们将使用几个库,这些库可以从终端(或类似地从 Anaconda Navigator)快速安装:

pip install statsmodels pandas_datareader 

我们将从 Python(或 IPython)终端执行这些命令,当然,我们也可以从 Jupyter notebook(或其他环境)执行。

让我们开始建模吧!

Python 中的建模

我们将使用 Yahoo 财经的股票代码数据集,通过 yfinance 库下载。我们首先加载数据集,进行一些快速探索,然后构建本章中提到的几个模型。

我们将加载一系列标准普尔存托凭证(SPDR S&P 500 ETF 信托基金):

from datetime import datetime
import yfinance as yf

start_date = datetime(2005, 1, 1)
end_date = datetime(2021, 1, 1)
df = yf.download(
    'SPY',
    start=start_date,
    end = end_date
) 

我们需要指定日期范围和股票代码。每日价格包括开盘价、收盘价等。我们将使用开盘价进行分析。

索引列已经是 pandas 的 DateTimeIndex,因此我们无需进行转换。现在,让我们绘制这个时间序列图!

import matplotlib.pyplot as plt
plt.title('Opening Prices between {} and {}'.format(
    start_date.date().isoformat(),
    end_date.date().isoformat()
))
df['Open'].plot()
plt.ylabel('Price')
plt.xlabel('Date'); 

这将生成以下图表:

ticker_price.png

图 5.6:标准普尔存托凭证价格随时间变化

由于这是每日数据,且每年有 253 或 252 个工作日,我决定将数据重采样为每周数据,使每年数据保持一致。

df1 = df.reset_index().resample('W', on="Date")['Open'].mean()
df1 = df1[df1.index.week < 53] 

有些年份有 53 周。我们无法处理这种情况,因此将去掉第 53 周。现在我们有跨越 16 年的 52 周的每周数据。

最后一个修正:statsmodels 可以使用与 DateTimeIndex 相关联的频率信息;然而,这通常没有设置,df1.index.freqNone。所以,我们将自己设置:

df1 = df1.asfreq('W').fillna(method='ffill') 

如果我们现在检查,df1.index.freq<Week: weekday=6>

设置频率可能会导致缺失值。因此,我们使用 fillna() 操作将缺失值替换为最后一个有效值。如果不这样做,某些模型将无法收敛,并且会返回 NaN(非数字)值,而不是预测值。

现在我们需要了解模型阶数的合理范围。我们将查看自相关和部分自相关函数来帮助确定:

import statsmodels.api as sm
fig, axs = plt.subplots(2)
fig.tight_layout()
sm.graphics.tsa.plot_pacf(df1, lags=20, ax=axs[0])
sm.graphics.tsa.plot_acf(df1, lags=20, ax=axs[1]) 

这将生成以下图表:

pcf_acf.png

图 5.7:部分自相关与自相关

这些图表显示了时间序列在最多 20 个时间步的滞后期上的自相关性。R 或 值接近 0 表示滞后期的连续观察值之间没有相关性。相反,接近 1 或 -1 的相关性表示这些滞后期的观察值之间存在强烈的正相关或负相关。

自相关和部分自相关都返回置信区间。如果自相关值超出了置信区间(表示为阴影区域),则表明相关性显著。

我们可以看到滞后期为 1 的部分自相关非常高,而较高滞后的自相关则较低。所有滞后的自相关都显著且较高,但随着滞后期的增加,显著性逐渐降低。

让我们继续讨论自回归模型。从这里开始,我们将使用 statsmodels 的建模功能。这个接口非常方便,正如你将看到的那样。

我们不能直接使用自回归模型,因为它需要时间序列是平稳的,这意味着均值和方差在时间上是恒定的——没有季节性,没有趋势。

我们可以使用 statsmodels 工具来查看时间序列的季节性和趋势性:

from statsmodels.tsa.seasonal import seasonal_decompose
result = seasonal_decompose(df, model='additive', period=52)
result.plot() 

我们将周期设置为 1,因为每个数据点(行)对应一年。

让我们看看各个组成部分的样子:

seasonal_decompose.png

图 5.8:时间序列的季节性分解

第一个子图是原始时间序列。这个数据集包含季节性和趋势性,您可以在子图中看到这些成分被分离出来。

如前所述,我们需要一个平稳的序列来进行建模。为了建立平稳性,我们需要去除季节性和趋势性成分。我们也可以去除我们之前估算的季节性或趋势性成分。或者,我们可以使用 statsmodels 中的封装功能,或者在 ARIMA 中设置 d 参数。

我们可以使用扩展的迪基-富勒(Augmented Dickey-Fuller)和 KPSS 检验来检查平稳性:

from arch.unitroot import KPSS, ADF
ADF(df1) 

我们本可以使用statsmodels.tsa.stattools.adfullerstatsmodels.tsa.stattools.kpss,但我们更喜欢 ARCH 库版本的方便性。我们将留给用户检查 KPSS 检验的输出。我们得到了如下输出:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_Y7Tw2W/Screenshot 2021-08-22 at 15.36.52.png

图 5.9:KPSS 检验平稳性的输出

给定 p 值为 0.997,我们可以拒绝单位根的原假设,并得出结论,我们的过程是弱平稳的。

那么我们如何找到合适的差分值呢?我们可以使用 pmdarima 库,它提供了一个专门用于此目的的函数:

from pmdarima.arima.utils import ndiffs
# ADF Test:
ndiffs(df1, test='adf') 

我们得到了一个值 1。对于 KPSS 和 PP 检验,我们会得到相同的值。这意味着我们可以从第一次差分开始工作。

让我们从自回归模型开始。

提醒一下,ARIMA 模型由参数 p、d、q 来定义,其中:

  • p 表示自回归模型:AR(p)

  • d 表示积分

  • q 表示移动平均:MA(q)

因此,ARIMA(p, d, 0) 就是带有差分阶数 d 的 AR(p)模型。

知道 statsmodels 会检查并警告平稳性假设是否成立,让人放心。让我们尝试运行以拟合以下 AR 模型:

mod = sm.tsa.arima.ARIMA(endog=df, order=(1, 0, 0))
res = mod.fit()
print(res.summary()) 
UserWarning: Non-stationary starting autoregressive parameters found. Using zeros as starting parameters.
  warn('Non-stationary starting autoregressive parameters' 

由于我们已经知道需要进行一次差分处理,我们可以将 d 设置为 1。再试一次。这次,我们将使用 STLForecast 包装器,它能去除季节性并重新加回季节性。这是必要的,因为 ARIMA 无法直接处理季节性:

from statsmodels.tsa.forecasting.stl import STLForecast
mod = STLForecast(
  df1, sm.tsa.arima.ARIMA,
  model_kwargs=dict(order=(1, 1, 0), trend="t")
)
res = mod.fit().model_result
print(res.summary()) 

我们得到以下总结:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_EjesO5/Screenshot 2021-08-22 at 17.07.45.png

图 5.10:我们的 ARIMA 模型概述

这个结果总结提供了所有关键统计数据。我们看到模型是 ARIMA(1, 1, 0)。对数似然值为 -1965。我们还看到了 BIC 和 AIC 值,这些值可以用于模型选择。

请注意,我们在这里需要设置 trend="t",这样模型就会包含常数项。如果不设置,我们将得到一个虚假的回归结果。

我们如何使用这个模型?让我们进行一些预测!

STEPS = 20
forecasts_df = res.get_forecast(steps=STEPS).summary_frame() 

这给我们提供了一个未来 20 步的预测。

让我们来可视化一下!

ax = df1.plot(figsize=(12, 6))
plt.ylabel('SPY')
forecasts_df['mean'].plot(style='k--')
ax.fill_between(
    forecasts_df.index,
    forecasts_df['mean_ci_lower'],
    forecasts_df['mean_ci_upper'],
    color='k',
    alpha=0.1
) 

这是我们得到的结果:

spy_forecast.png

图 5.11:SPY 股票代码的价格预测

实线表示我们已知的数据,虚线代表我们预测的未来 20 年的结果。我们预测值周围的灰色区域是 95% 的置信区间。

这个看起来还不错。作为读者的练习,可以尝试使用不同的参数。值得更改的有趋势参数和模型的阶数。

对于移动平均,我们创建不同的模型来观察它们的预测差异!

首先,我们将生成预测:

forecasts = []
qs = []
for q in range(0, 30, 10):
    mod = STLForecast(
            df1, sm.tsa.arima.ARIMA, 
            model_kwargs=dict(order=(0, 1, q), trend="t")
        )
    res = mod.fit()
    print(f"aic ({q}): {res.aic}")
    forecasts.append(
            res.get_forecast(steps=STEPS).summary_frame()['mean']
        )
    qs.append(q)
forecasts_df = pd.concat(forecasts, axis=1)
forecasts_df.columns = qs 

在循环中,我们正在迭代不同的 q 参数,选择 0、10 和 20。我们使用这些 q 值估计移动平均模型,并预测未来 20 年的价格。同时,我们还打印出每个 q 对应的 AIC 值。这是我们得到的结果:

aic (0): 3989.0104184919096
aic (10): 3934.375909262983
aic (20): 3935.3355340835 

现在,让我们像之前一样绘制三个预测图:

ax = df1.plot()
plt.ylabel('SPY')
forecasts_df.plot(ax=ax) 

这是新的图表:

forecasts_qs.png

图 5.12:使用不同 q 参数的预测

那么,这些模型中哪一个在统计学上更好呢?

让我们回到 AIC。AIC 值越低,模型的效果越好,因为它考虑了对数似然和参数的数量。

在这种情况下,q=10 的阶数给出了最低的 AIC 值,根据这一标准,我们应该选择 q=10。当然,我们只尝试了三个不同的值。接下来我将留给读者作为练习,找出一个更合理的 q 参数值。

请注意,pmdarima 库具有查找最优参数值的功能,而 SkTime 库提供了 ARIMA 模型最优阶数的自动发现实现:AutoARIMA。

接下来,我们使用指数平滑模型进行预测。

在循环中,我们正在迭代不同的 q 参数,选择 0、10 和 20。我们使用这些 q 值估计移动平均模型,并预测未来 20 年的价格。同时,我们还打印出每个 q 对应的 AIC 值。这是我们得到的结果:

mod = sm.tsa.ExponentialSmoothing(
        endog=df1, trend='add'
    )
res = mod.fit() 

这将模型拟合到我们的数据上。

让我们为接下来的 20 年获取预测:

forecasts = pd.Series(res.forecast(steps=STEPS)) 

现在,让我们绘制预测图:

ax = df.plot(figsize=(12, 6))
plt.ylabel('SPY')
forecasts.plot(style='k--') 

这是图表:

exponential_forecast.png

图 5.13:指数平滑预测

直到现在,我们只是看了 20 步预测的图表。我们还没有对模型的表现进行深入分析。让我们看看误差!

为此,我们首先需要将数据集划分为训练集和测试集。我们可以进行 n 步预测并检查误差。我们只需将截至某一时间点的时间序列作为训练数据,之后的时间点作为测试数据,然后将预测值与实际数据点进行比较:

from statsmodels.tsa.forecasting.theta import ThetaModel
train_length = int(len(df1) * 0.8)
tm = ThetaModel(df1[:train_length], method="auto",deseasonalize=True)
res = tm.fit()
forecasts = res.forecast(steps=len(df1)-train_length)
ax = df1.plot(figsize=(12, 6))
plt.ylabel('SPY')
forecasts.plot(style='k--') 

这是图表:

theta_forecast.png

图 5.14:Theta 模型预测

虚线是预测值。它似乎与时间序列的实际行为不太吻合。让我们使用我们在上一章《时间序列的机器学习入门》中讨论过的误差度量来量化这个问题:

from sklearn import metrics
metrics.mean_squared_error(forecasts, df1[train_length:], squared=False) 

我们得到一个值为37.06611385754943。这是均方根误差(我们将squared参数设置为False)。

在预测比赛中,比如 Kaggle 网站上的比赛,最低误差获胜。在现实中,简洁性(简单性)也很重要;然而,我们通常仍然追求尽可能低的误差(无论采用哪种度量标准)。

还有很多其他模型可以探索和尝试,但现在是时候总结本章内容了。

总结

在这一章中,我们讨论了基于移动平均和自回归的时间序列预测。这个主题包含了一大套模型,在不同领域(如计量经济学和统计学)中非常流行。这些模型是时间序列建模的支柱,并提供了最先进的预测方法。

我们已经讨论了自回归和移动平均模型,以及结合这两者的其他模型,包括 ARMA、ARIMA、VAR、GARCH 等。在实践环节中,我们已经将一些模型应用于股票价格数据集。

第六章:时间序列的无监督方法

我们在前一章已经讨论了预测方法,接下来我们将在下一章中讨论时间序列的预测。这些预测模型的性能很容易受到数据中重大变化的影响。识别这些变化是无监督学习的领域。

在本章中,我们将描述使用时间序列数据进行无监督学习的具体挑战。无监督学习的核心是从时间序列中提取结构,最重要的是识别子序列之间的相似性。这正是异常检测的本质(同样适用于:离群点检测),我们希望识别出与其他序列明显不同的序列。

时间序列数据通常是非平稳的、非线性的,并且是动态演化的。处理时间序列的一个重要挑战是识别潜在过程中的变化。这被称为变点检测(CPD)或漂移检测。数据随着时间的推移而变化,识别这些变化的程度至关重要。这是值得深入探讨的,因为变点和异常点的存在是现实应用中的常见问题。

在本章中,我们将集中讨论异常检测和变点检测(CPD),而在第八章中,时间序列的在线学习,我们将更详细地探讨漂移检测。我们将从概述和定义开始,然后研究大科技公司在行业中的实践。

我们将讨论以下主题:

  • 时间序列的无监督方法

  • 异常检测

  • 变更检测

  • 聚类

  • Python 实践

我们将从无监督学习与时间序列的总体介绍开始。

时间序列的无监督方法

时间序列与其他类型数据的主要区别在于它对时间轴的依赖;在某一点 t[1] 上的相关结构可能与在点 t[2] 上的相同结构包含完全不同的信息。时间序列通常包含大量噪声且维度较高。

为了减少噪声并降低维度,可以应用降维、波形分析或信号处理技术,例如傅里叶分解。这些通常是异常检测或变点检测(CPD)技术的基础,我们将在本章讨论这些技术。我们将在第八章中讨论漂移检测,在线时间序列方法

我们将详细讨论异常值和变化点,查看它们的表现形式可能会有所帮助。在 Ilona Otto 等人的文章《2050 年通过社会动态稳定地球气候的社会临界动态》中,他们分析了基于社会动态的温室气体排放变化是否以及如何将各国转变为碳中和社会。他们根据不同的情景预测了全球变暖,以下图显示了 2010 年及 2020 年代初期的临界点(图表改编自他们的文章):

图 6.1:基于温室气体排放的可能变化点

全球气温在冰川期和温暖期之间循环变化,每个周期大约持续数万年。在过去几千年里,气候逐渐变冷,直到 1970 年代,关于可能导致下一次冰河时期的降温趋势有了广泛的猜测。然而,数据表明,自工业化开始以来,主要由燃烧化石燃料推动,全球气温已上升了约 1°C。

因此,工业化时期的开始可以视为全球气温的一个变化点,如下图所示(来源:维基媒体共享资源):

ile:Temperature reconstruction last two millennia.svg

图 6.2:全球气温变化点:工业时代的开始

在上面的图表中,工业革命开始时的变化点出现在现代气温上升的异常值之前。

对于人类来说,指出变化点或异常值相对容易,特别是在事后,历史数据完全可得的情况下。对于自动检测,有很多不同的方法可以找到显著的点。在实际应用中,重要的是要仔细平衡检测率和假阳性。

异常检测

在异常检测中,我们希望识别出与其余序列明显不同的序列。异常值或离群点有时可能是测量误差或噪声的结果,但它们也可能表明被观察系统的行为发生了变化或出现了异常行为,这可能需要采取紧急措施。

异常检测的一个重要应用是对潜在复杂、高维数据集的自动实时监控。

是时候尝试给出一个定义了(参考 D.M. Hawkins, 1980,《异常值识别》):

定义:异常值是指与其他观测值差异极大的数据点,以至于它可能是由不同的机制产生的。

让我们从一个图表开始,这样我们可以直观地看到异常值在图形上可能的表现。这也为我们的讨论提供了背景。

异常值检测方法可以分为单变量方法和多变量方法。参数化异常值检测方法通过选择其分布参数(例如,算术平均值),对底层分布做出假设——通常是高斯分布。这些方法标记出偏离模型假设的异常值。

在最简单的情况下,我们可以定义一个异常值,如下所示,观察值 x[i] 相对于分布参数的 z-得分:

z-得分衡量每个点与移动平均或样本均值之间的距离,,以移动或样本标准差的单位!。对于高于均值的值,它为正;对于低于均值的值,它为负。

在此公式中, 是时间序列的估计均值和标准差,x 是我们想要测试的点。最后, 是一个依赖于我们感兴趣的置信区间的阈值——通常,选择 2 或 1.96,对应于 95% 的置信区间。通过这种方式,异常值是指发生概率小于或等于 5% 的点。

z-得分假设数据是正态分布的;然而,以上异常值公式中使用的均值和标准差可以被其他去除此假设的度量替代。像中位数或四分位距(在第二章中有讨论,使用 Python 进行时间序列分析)等度量对分布更加稳健。

Hampel 滤波器(也叫 Hampel 标识符)是这一特殊情况,其中使用了中位数和中位绝对偏差MAD):

在这个方程中,样本均值被(样本)中位数取代,标准差被 MAD(绝对中位差)取代,MAD 的定义如下:

中位数是按顺序排列的数值列表中的中间数。

在 Hampel 滤波器中,每个观察值 x 将与中位数进行比较。在正态分布的情况下,Hampel 滤波器等同于 z-得分,epsilon 可以像 z-得分一样选择。

在多变量情况下,异常值函数可以表示为到模型分布中某一点的距离(或者相反:相似度),例如重心、均值。例如,我们可以计算新观察值与均值之间的协方差。

尽管这些前述方法仅限于低维或单变量时间序列,但基于距离的方法可以处理更大的空间。基于距离的异常值检测方法有效地将点聚类为不同的组,其中小组会被标记为异常值。在这些方法中,距离度量的选择至关重要。

检测时间序列中异常值的挑战之一是:

  • 缺乏异常值的定义

  • 输入数据中的噪声

  • 时间序列的复杂性

  • 高度不平衡

我们通常并不知道异常值长什么样。在实际应用中,我们往往没有异常值的标签——这使得基于真实案例的基准测试变得不可能。至于复杂性,时间序列会随着时间变化,它们通常是非平稳的,变量之间的依赖关系可能是非线性的。最后,我们通常有比异常值更多的正常观测值。

部署大规模异常检测模型作为服务的一个要求是,它们应该能够实时检测到异常。

异常检测的应用包括此图中的内容:

anomaly_detection_application.png

图 6.3:异常检测的应用

一些例子可能包括支付中的欺诈检测、网络安全(网络入侵)、医学监控或传感器网络。在医学监控中,我们希望实时监测生理变量,包括心率、脑电图和心电图,以便在急性紧急情况下发出警报。传感器网络中的异常警报有助于防止工业损害的发生。

该图表说明了根据数据集的可用知识,异常检测方法的主要类型:

Anomaly_Detection.png

图 6.4:根据可用知识的异常检测方法

最早的异常检测例子是基于规则的系统。当模式可以清晰定义时,这种方法有效。当我们有一个标注好的异常集时,我们可以应用监督或半监督方法,如分类器或回归模型。然而,最常见的使用场景是异常没有标注,我们需要无监督方法来基于密度或分布检测异常点或异常区间。

观察大科技公司(如 Alphabet(谷歌)、Amazon、Facebook、Apple 和 Microsoft(GAFAM))在异常检测方面的做法是很有启发性的。我们逐一看看它们是如何处理异常检测的。

微软

在论文《微软的时间序列异常检测服务》(Hansheng Ren 等,2019)中,介绍了一种为微软生产数据的异常检测而部署的时间序列服务。其核心是谱残差SR)和卷积神经网络(CNN),应用于单变量时间序列的无监督在线异常检测。

它们借用了来自计算机视觉中显著性图(saliency map)概念的 SR 方法。显著性图突出显示图像中对人类观察者来说突出的点。该算法对数据执行傅里叶变换,然后应用变换信号的对数幅度的 SR,最后通过逆傅里叶变换将频谱数据投影回时域。

作为扩展,他们基于人工数据使用 SR 方法训练了一个卷积神经网络(CNN)。他们展示了在公开可用数据上的基准测试,支持他们的主张:他们的方法是异常检测领域的最新技术。

他们进一步声称,在微软生产数据上,他们的检测准确性(F1 得分)提高了超过 20%。你可以在 alibi-detect 库中找到基本实现("谱残差"方法)。

Google

在 Google Analytics 的常见问题中(support.google.com/analytics/answer/7507748?hl=en),谷歌提到了一个贝叶斯状态空间时间序列模型("用贝叶斯结构时间序列预测当前状态",作者 Steven L. Scott 和 Hal Varian,2013),用于变化点和异常检测。

谷歌发布了一个具有更具体时间序列功能的 R 包——CausalImpact。描述该包背后研究的论文于 2015 年发布("通过贝叶斯结构时间序列模型推断因果影响",作者 Kay H. Brodersen, Fabian Gallusser, Jim Koehler, Nicolas Remy, Steven L. Scott)。CausalImpact 基于结构化贝叶斯时间序列模型估计干预的因果效应。该方法已被移植到 Python(pycausalimpact 库)。我们将在第九章时间序列的概率模型中实验使用贝叶斯结构时间序列(BSTS)进行因果影响分析。

Amazon

亚马逊通过其亚马逊云服务AWS)平台提供大规模的机器学习解决方案,其中包含异常检测功能,作为其资源和应用监控解决方案 CloudWatch 的一部分。尽管其解决方案的具体原理尚不清楚,但经济学家 Corey Quinn 在一条推文中推测,他们的解决方案可能是指数平滑。作为其中的一部分,他们很可能将季节性分解作为算法的第一步。

他们还有第二项异常检测服务:Amazon Lookout for Metrics。关于该服务的具体工作原理也不清楚。该服务旨在监控业务指标,并且——根据文档——在亚马逊内部用于大规模监控。在此服务中,用户可以从不同细分的数据源中选择字段,例如,通过选择数据库列page_viewsdevice_type,用户可以分别查看每种设备类型下页面浏览量的异常变化。

至于亚马逊在异常检测领域的研究,他们在声学场景与事件检测与分类研讨会(DCASE 2020)中的 117 个参赛作品中荣获前三名。他们在这次与时间序列异常检测类似的挑战中获得了最佳论文奖,论文题为"基于组掩蔽自编码器的音频异常检测密度估计器"(作者 Ritwik Giri 等,2020)。

Facebook

Facebook 的核心数据科学团队在 GitHub 上开源了他们用于时间序列预测和异常检测的实现。这个库叫做 Prophet。在 2017 年宣布这个库的博客文章中,他们表示,Prophet 是 Facebook 能够大规模创建预测的关键工具,并且在决策过程中被认为是重要的信息来源。

Sean J Taylor 和 Benjamin Letham(2017)在论文《大规模预测》中描述了他们在 Facebook 的设置,包括一个分析师参与的环节,并且能够自动标记预测结果进行人工审查和调整。异常检测建立在来自广义加法模型(GAM)预测的不确定性基础上。

Prophet 已在基准测试中与其他概率模型和非概率模型进行了比较,且很少展现出突出的成功。microprediction.com 的 Elo 评分表明,Prophet 在单变量预测方面表现不如指数移动平均和许多其他标准方法。

Twitter

Twitter 也发布了一个 R 包,名为 AnomalyDetection。该方法基于广义极端学生化偏差(ESD)测试,用于检测单变量近似正态分布的时间序列中的异常。该方法发表于 2017 年(“通过统计学习在云端进行自动异常检测”,Jordan Hochenbaum, Owen Vallis, Arun Kejariwal)。

对于他们的 ESD 测试的适配,季节性混合 ESD 方法在应用阈值之前,加入了基于 LOESS 的季节性趋势分解(STL),并对 z-score 应用了阈值(如上所述),或者对于异常值较多的数据集,基于中位数和 MAD 进行阈值化。Twitter 模型已被移植到 Python 中(sesd 库)。

实现

我们将以 Python 中现有的异常检测实现概述来结束。市面上有很多实现方法。它们的使用场景非常相似,但实现方式和用户群体各不相同。

以下是按 GitHub 上的星标数量排序的列表(截至 2021 年 5 月):

Library 实现 维护者 星标
Prophet 关于预测趋势成分的估计的不确定性区间 Facebook 核心研究 12.7k
PyOD 适用于多变量时间序列的 30 种检测算法——从经典的 LOF(SIGMOD 2000)到 COPOD(ICDM 2020) Yue Zhao 及其他人 4.5k
alibi-detect 多种异常检测算法——特定于时间序列的有似然比、Prophet、谱残差、Seq2Seq、模型蒸馏 Seldon Technologies Ltd 683
Scikit-Lego 通过 PCA/UMAP 重构 Vincent D. Warmerdam 及其他人 499
Luminaire Luminaire 窗口密度模型 Zillow 371
Donut 用于季节性关键绩效指标(KPI)的变分自编码器 清华大学网络管理实验室 327
rrcf 用于流数据异常检测的鲁棒随机切割森林算法 密歇根大学实时水系统实验室 302
banpei 霍特林理论 Hirofumi Tsuruta 245
STUMPY 针对单变量和多变量时间序列的矩阵概况算法,如 STUMP、FLUSS 和 FLOSS(也可参考 matrixprofile-ts) TD Ameritrade 169
PySAD 超过十种流式异常检测算法 Selim Yilmaz, Selim 和 Suleyman Kozat 98

图 6.5:Python 中的异常检测方法

这些方法各自有其背景和形式化基础;然而,本章的范围并不涵盖对它们的详细描述。

这张图展示了三大仓库的星标历史(来自 star-history.t9t.io):

anomaly_detection-star_history.png

图 6.6:Prophet、PyOD 和 alibi-detect 的星标历史

Prophet 和 PyOD 的受欢迎程度(GitHub 星标数)一直在持续增长。

最近,许多深度学习算法已经被应用于异常检测,既包括单变量时间序列,也包括多变量时间序列。

深度学习模型的特别之处在于,应用范围可以更加广泛:例如闭路电视中的视频监控异常检测。我们将在第十章时间序列的深度学习中更详细地探讨深度学习架构。

变化点检测

时间序列的一个常见问题是观察系统行为的变化。一般来说,变化点表示在生成该序列的过程中,系统状态之间发生了突变和重大转变。例如,趋势可能会突然发生变化,而变化点可以指示趋势变化的位置。这在交易中的技术图表模式分析中非常常见。

这个列表展示了一些 变化点检测CPD)的应用:

  • 语音识别:检测单词和句子的边界

  • 图像分析:对闭路电视视频监控进行监视

  • 健身:根据智能设备(如手表或手机)上的运动传感器数据,分割人的活动时间段

  • 金融:识别趋势模式的变化,可能表明从熊市到牛市,或反之的转变。

以股票市场为例,说明 CPD 的重要性。描述市场演变的时间序列数据,如股票价格,遵循趋势——它要么上涨,要么下跌,或者没有显著变化(停滞)。

当股票上涨时,投资者想要买入该股票。否则,当股票下跌时,投资者不希望持有该股票,而是希望将其卖出。不改变仓位会导致账面价值的损失——在最好的情况下,这会导致流动性问题。

对于投资者而言,了解市场从上涨到下跌,或从下跌到上涨的变化时机至关重要。识别这些变化可能决定是否盈利。

在预测中,诸如黑色星期五、圣诞节、选举、新闻发布或法规变动等特殊事件可能会对趋势或序列的水平造成短期(可能当时被视为异常)或长期的变化。这将不可避免地导致传统模型产生异常的预测。

CPD 算法面临的一个特别有趣的挑战是实时检测这些拐点。这意味着在拐点到来时立即检测到变化点(或者至少在下一个变化点发生之前)。

我们可以区分 CPD 的在线和离线方法,其中在线指的是实时处理,处理每一个新到的数据点。而离线算法则可以一次性处理整个时间序列。我们将在第八章时间序列的在线学习中更多地讨论在线处理。

CPD 与分段、边缘检测、事件检测和异常检测相关,类似的技术可以应用于所有这些应用。CPD 可以看作与异常检测非常相似,因为识别变化点的一种方法是通过异常检测算法的异常分数。

从这个角度来看,变化点与高度异常点是相同的,任何超过某个阈值的点都对应一个变化。与异常检测相似,CPD 可以定义为在两个备择假设之间进行假设检验的问题,零假设为"没有变化发生",而备择假设为"发生了变化。"

CPD 算法由三个组成部分构成:代价函数、搜索方法和约束条件。我们将逐一讲解这些内容。代价函数是可以应用于时间序列的一个子段(多变量或单变量)的距离函数。

代价函数的一个例子是 最小绝对偏差LAD),它是一个估算分布中心点(均值、中位数和众数)变化的估计量,定义如下:

在这个定义中,l 是时间序列 x 中一个子段的索引,而 x 的中心点。

搜索函数然后会遍历时间序列来检测变化点。这可以是近似的,例如基于窗口的检测、从下到上的方法或二分分割,或者是穷举的,如动态规划或 修剪精确线性时间Pelt)方法。

Pelt(Gachomo Dorcas Wambui 等,2015)依赖于修剪启发式方法,计算成本与时间序列的点数成线性关系,。动态规划方法的计算成本要高得多,,其中 n 是预期变化点的最大数量。

最后,约束条件可以作为搜索算法中的惩罚项参与其中。这个惩罚项可以编码为一个成本预算,或者是我们预期找到的变化点数量的知识。

评估 CPD 算法的性能一直是个难题,因为缺乏基准数据集。直到最近(2020 年),来自阿兰·图灵研究所和爱丁堡大学的 Gerrit van den Burg 和 Christopher Williams 发布了一个基准数据集,包含来自世界银行、欧盟统计局、美国人口普查局、GapMinder 和维基百科等来源的 37 个时间序列。他们的基准数据集已发布在 GitHub 上,并且提到该数据集的变点注释集中在 2007-08 年的金融危机、英国的安全带法规、蒙特利尔议定书(用于调控氯氟烃排放)以及美国的自动电话呼叫监管等领域。

在同一篇论文("An Evaluation of Change Point Detection Algorithm")中,作者评估了各种 CPD 方法。他们指出,假设没有任何变点的“零”基准方法在 F1 度量和基于 Jaccard 指数的聚类重叠度量上优于许多其他方法。这是因为数据集中变点的比例很小,且这些方法返回了大量的假阳性结果。他们得出结论,二分法和贝叶斯在线 CPD 是时间序列中最有效的几种方法。

二分法("On Tests for Detecting Change in Mean",作者:Ashish Sen 和 Muni S. Srivastava,1975)属于基于窗口的 CPD 方法。二分法是一种贪心算法,它通过如下定义最小化代价之和:

是发现的变点,而c()是类似于 LAD 的代价函数,我们之前在本节中看到了。其基本思想是,当两个子序列高度不相似时,表示存在变点。

二分法是顺序执行的,这意味着首先在整个时间序列中检测变点,然后在变点前后两个子序列中再次检测。这也解释了其低复杂度!,其中 T 为时间序列的长度。这个计算成本使得它可以扩展到更大的数据集。

该表格概述了 CPD 方法的不同种类:

实现方式 维护者
Greykite 通过自适应 lasso 进行 CPD LinkedIn
ruptures 离线 CPD:二分法、动态规划、Pelt、基于窗口的 Charles Truong
贝叶斯变点检测 贝叶斯 CPD Johannes Kulick
banpei 奇异谱变换 Hirofumi Tsuruta
changepy Pelt 算法 Rui Gil
onlineRPCA 在线移动窗口稳健主成分分析 Wei Xiao

图 6.7:Python 中的 CPD 方法

我们省略了 Facebook 的 Prophet 库,因为它不是专门的 CPD 包。

下图展示了 CPD 方法随时间变化的受欢迎程度。

change_point_detection-star_history.png

图 6.8:CPD 方法的历史演变

LinkedIn 的 Greykite 自发布以来在 GitHub 星标上迅速增长。同时,ruptures 也在流行度上大幅上升。

聚类

聚类分析或聚类是根据数据点或对象的相似性,在数据集中寻找有意义的组(簇)的过程。作为这种无监督数据挖掘技术的结果,我们希望每个簇中的点彼此相似,同时与其他簇中的点有所不同。

时间序列的聚类具有挑战性,因为每个数据点都是一个时间段(有序序列)。它已在多个领域得到应用,帮助发现模式,推动时间序列分析,从复杂数据集中提取洞察。

我们不打算深入讨论时间序列聚类,但下表提供了 Python 库在时间序列聚类中的概述:

实现 维护者 星标
tslearn 时间序列 K 均值、K-Shape 聚类、KernelKMeans Romain Tavenard 1.7k
river DBStream、时间序列 K 均值、CluStream、DenStream、STREAMKMeans Albert Bifet 及其他人 1.7k

图 6.9:Python 中时间序列的聚类方法

你可以查看历史上顶级实现的 GitHub 星标数据:

clustering-star_history.png

图 6.10:tslearn 和 river 的星标历史

两个库都在蓬勃发展。我们将在第八章时间序列的在线学习中重新讨论 river。

Python 实践

首先让我们做一个异常检测的例子,然后再做一个 CPD 的例子。我们先看看下一节所需的库。

要求

在本章中,我们将使用多个库,这些库可以通过终端快速安装(或通过 anaconda navigator 进行安装):

pip install ruptures alibi_detect 

我们将从 Python(或 IPython)终端执行命令,但同样可以从 Jupyter 笔记本(或其他环境)中执行它们。

我们现在应该准备好深入实施 Python 中的无监督时间序列算法了。

异常检测

alibi-detect 提供了多个用于时间序列异常检测的基准数据集:

  • fetch_ecg—来自 BIDMC 充血性心力衰竭数据库的 ECG 数据集

  • fetch_nab—Numenta 异常基准

  • fetch_kdd—KDD Cup '99 计算机网络入侵数据集

这些中的最后一个是通过 scikit-learn 加载的。

让我们加载计算机网络入侵的时间序列(KDD99):

from alibi_detect.datasets import fetch_kdd
intrusions = fetch_kdd() 

intrusions是一个字典,其中data键返回一个 494021x18 的矩阵。时间序列的 18 个维度是数据集的连续特征,主要是误差率和计数:

intrusions['feature_names']
['srv_count',
 'serror_rate',
 'srv_serror_rate',
 'rerror_rate',
 'srv_rerror_rate',
 'same_srv_rate',
 'diff_srv_rate',
 'srv_diff_host_rate',
 'dst_host_count',
 'dst_host_srv_count',
 'dst_host_same_srv_rate',
 'dst_host_diff_srv_rate',
 'dst_host_same_src_port_rate',
 'dst_host_srv_diff_host_rate',
 'dst_host_serror_rate',
 'dst_host_srv_serror_rate',
 'dst_host_rerror_rate',
 'dst_host_srv_rerror_rate'] 

另一个键target包含异常的注释。

既然我们已经准备好了注释,我们本可以训练一个分类器,然而,我们将坚持使用无监督方法。此外,由于我们将使用的谱方法适用于单变量数据,我们只会从多变量数据集中提取一个维度,因此我们将完全忽略注释。

这是我们时间序列的快速图(我们将随意选择数据集的第一个维度):

import pandas as pd
pd.Series(intrusions['data'][:, 0]).plot() 

这是图表:

intrusions_dim1.png

图 6.11:时间序列图

我们将加载并运行实现微软提出方法的 SpectralResidual 模型:

from alibi_detect.od import SpectralResidual
od = SpectralResidual(
    threshold=1.,
    window_amp=20,
    window_local=20,
    n_est_points=10,
    n_grad_points=5
) 

然后,我们可以获取时间序列中每个点的异常得分:

scores = od.score(intrusions['data'][:, 0]) 

让我们将得分绘制在时间序列之上!

import matplotlib
ax = pd.Series(intrusions['data'][:, 0], name='data').plot(legend=False, figsize=(12, 6))
ax2 = ax.twinx()
ax = pd.Series(scores, name='scores').plot(ax=ax2, legend=False, color="r", marker=matplotlib.markers.CARETDOWNBASE)
ax.figure.legend(bbox_to_anchor=(1, 1), loc='upper left'); 

我们在同一个图表中使用双 y 轴来绘制得分和数据。如下所示:

intrusions_scores.png

图 6.12:带有异常值的时间序列

由于傅里叶滤波器移除了信号的周期性,一些点未被识别为离群点。

变化点检测

我们将首先使用 ruptures 库创建一个合成的多元时间序列。我们将维度数设置为 3,时间序列的长度设置为 500,并且我们的时间序列将有 3 个变化点,标准差为 5.0 的高斯噪声将被叠加:

import numpy as np
import matplotlib.pylab as plt
import ruptures as rpt
signal, bkps = rpt.pw_constant(
  n_samples=500, n_features=3, n_bkps=3,
  noise_std=5.0, delta=(1, 20)
) 

信号是一个 500x3 的 NumPy 数组。bkps是变化点的数组(123、251 和 378)。

我们可以使用一个实用函数绘制该时间序列,函数会突出显示由变化点分隔的子部分:

 rpt.display(signal, bkps) 

这是我们带有三个变化点的时间序列图:

ruptures_time_series.png

图 6.13:带有变化点的时间序列

我们可以将二分段方法应用于此时间序列。ruptures 遵循 scikit-learn 的约定,因此,如果你之前使用过 scikit-learn,使用起来应该非常直观:

 algo = rpt.Binseg(model="l1").fit(signal)
my_bkps = algo.predict(n_bkps=3) 

我们有几个二分段约束选项——可以选择l1l2rbflinearnormalar

我们可以用另一个实用函数绘制二分段的预测结果:

 rpt.show.display(signal, bkps, my_bkps, figsize=(10, 6)) 

这是我们从二分段模型中获得的变化点预测图:

rupture_bs_predictions.png

图 6.14:带有检测到的变化点的时间序列(Binary Segmentation)

让我们总结一下本章中的一些信息!

总结

在本章中,我们集中讨论了时间序列无监督方法的两个方面:

  • 异常检测

  • 变化点检测

异常检测(也叫:离群点检测)的本质是识别出明显与其他序列不同的部分。我们已经研究了不同的异常检测方法,以及一些大公司如何在大规模中处理它。

在处理时间序列时,重要的是要注意数据随时间变化,这可能导致模型失效(模型陈旧)。这就是所谓的变化点检测和漂移检测。

本章我们探讨了变化点检测。在第八章时间序列的在线学习中,我们将更详细地探讨漂移检测。

第七章:时间序列的机器学习模型

近年来,机器学习取得了长足的进展,这一点在时间序列预测方法中得到了体现。我们在第四章时间序列机器学习导论中介绍了一些最先进的时间序列机器学习方法。在本章中,我们将介绍更多的机器学习方法。

我们将介绍一些常用的基准方法,或者在性能、易用性或适用性方面表现突出的算法。我将介绍基于动态时间规整和梯度提升的 k 近邻算法作为基准,我们还将讨论其他方法,如 Silverkite 和梯度提升。最后,我们将进行一个应用练习,使用这些方法中的一些。

我们将涵盖以下主题:

  • 更多的时间序列机器学习方法

  • 带有动态时间规整的 k 近邻算法

  • Silverkite

  • 梯度提升

  • Python 练习

如果你想了解最先进的机器学习算法,请参阅第四章时间序列机器学习导论。本章的算法讨论将假设你已经掌握了该章节的一些内容。我们将在接下来的部分中介绍的算法在预测和预报任务中都非常具有竞争力。

我们将在这里更详细地讨论这些算法。

更多的时间序列机器学习方法

我们将在本节中介绍的算法在预测和预报任务中都具有高度竞争性。如果你想了解最先进的机器学习算法,请参阅第四章时间序列机器学习导论

在上述章节中,我们简要讨论了其中的一些算法,但我们将在这里更详细地讲解它们,还会介绍一些我们之前未曾讨论过的算法,如 Silverkite、梯度提升和 k 近邻。

我们将专门设置一个实践部分,介绍一个 2021 年发布的库——Facebook 的 Kats。Kats 提供了许多高级功能,包括超参数调优和集成学习。在这些功能的基础上,它们实现了基于 TSFresh 库的特征提取,并包含多个模型,包括 Prophet、SARIMA 等。它们声称,与其他超参数调优算法相比,Kats 在时间序列上的超参数调优在基准测试中快了 6 到 20 倍。

该图展示了所选时间序列机器学习库的流行度概览:

more_machine_learning-star_history.png

图 7.1:时间序列机器学习库的流行度

截至 2021 年中期,Kats 和 GreyKite 库最近才发布,尽管它们在 GitHub 上获得了越来越多的星标,但它们的受欢迎程度还未能与 TSFresh 相抗衡。我尽管 TSFresh 是一个特征生成库,而非预测库,但还是将其包含在内,因为我觉得它在本章所使用的其他库中非常重要。TSFresh 之后,SKTime 排名第二,并且在相对较短的时间内吸引了大量星标。

在本章的实际示例中,我们将使用一些这些库。

另一个重要的问题是验证,值得单独讨论这个问题。

验证

我们在第四章时间序列的机器学习入门中已经讨论过验证。通常在机器学习任务中,我们使用 k 折交叉验证,其中数据的拆分是伪随机进行的,因此训练集和测试/验证集可以来自数据的任何部分,只要这些部分没有被用于训练(样本外数据)。

对于时间序列数据,这种验证方法可能会导致对模型性能的过度自信,因为现实中,时间序列往往随着趋势、季节性和时间序列特征的变化而变化。

因此,在时间序列中,验证通常采用所谓的前向验证(walk-forward validation)。这意味着我们在过去的数据上训练模型,然后在最新的数据片段上进行测试。这将消除过于乐观的偏差,并在模型部署后为我们提供更为现实的性能评估。

在训练、验证和测试数据集方面,这意味着我们将完全依赖训练和验证数据集来调整模型参数,并且我们将基于一个时间上更先进的数据集来评估测试,具体如下面的图示所示(来源:Greykite 库的 GitHub 仓库):

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/(A Document Being Saved By screencaptureui 20)/Screenshot 2021-06-06 at 21.39.47.png

图 7.2:前向验证

在前向验证中,我们先在数据的初始片段上训练,然后在训练集之后的某个时间段进行测试。接着,我们向前推进并重复这一过程。这样,我们有多个样本外的时间段,可以将这些时间段的结果进行整合。通过前向验证,我们不太可能遭遇过拟合的问题。

动态时间规整的 K 近邻算法

K 近邻是一个著名的机器学习方法(有时也被称为基于案例的推理)。在 kNN 中,我们可以使用距离度量来找到相似的数据点。然后,我们可以将这些最近邻的已知标签作为输出,并通过某种函数将它们整合在一起。

图 7.3 展示了 kNN 分类的基本思路(来源 – WikiMedia Commons: commons.wikimedia.org/wiki/File:KnnClassification.svg):

/Users/ben/Downloads/Machine-Learning for Time-Series with Python/knn.png

图 7.3:用于分类的 K 最近邻

我们已经知道一些数据点。在前面的示意图中,这些数据点分别用方形和三角形表示,代表了两个不同类别的数据点。给定一个新的数据点,用圆圈表示,我们找到与之最接近的已知数据点。在这个例子中,我们发现新点与三角形相似,因此我们可以假设新点也属于三角形类。

尽管这种方法在概念上非常简单,但它通常作为一种强基线方法,或者有时甚至能与更复杂的机器学习算法竞争,即使我们只比较最接近的邻居(𝑘=1))。

该算法中的重要超参数有:

  • 你希望根据其来生成输出的邻居数(k)

  • 集成函数(例如,平均值或最常见值)

  • 用于找到最近数据点的距离函数

我们在第四章时间序列机器学习导论》中讨论过动态时间扭曲(Dynamic Time Warping),它是一种用于比较两条时间序列相似性(或等价地,距离)的方法。这些序列甚至可以有不同的长度。动态时间扭曲已经证明自己是一个异常强大的时间序列距离度量。

我们可以将 kNN 和动态时间扭曲结合起来,作为一种距离度量来寻找相似的时间序列,这种方法已经证明其在某些情况下很难被超越,尽管当前的技术已经超过了它。

Silverkite

Silverkite 算法与 LinkedIn 发布的 Greykite 库一起发布。它的设计目标是快速、准确且直观。该算法在 2021 年的文章中有描述(《生产系统的灵活预测模型》,Reza Hosseini 等人)。

根据 LinkedIn,它能够处理各种趋势和季节性因素,例如每小时、每日、每周、重复事件、假期以及短期效应。在 LinkedIn 内部,它既用于短期预测,例如 1 天的预测,也用于长期预测,例如 1 年后的预测。

在 LinkedIn 中的应用场景包括优化预算决策、设定业务指标目标,以及提供足够的基础设施来应对峰值流量。此外,一个应用场景是建模 COVID-19 大流行后的恢复情况。

时间序列被建模为趋势、变化点和季节性的加性组合,其中季节性包括假期/事件效应。趋势随后被建模如下:

其中 K 是变化点的数量,t[i] 是第 i 个变化点的时间索引。因此, 是第 i 个变化点的指示函数。函数 f(t) 可以是线性、平方根、二次、任意组合或完全自定义。

Silverkite 还构建了假期的指示变量。假期可以通过名称或国家指定,甚至可以完全自定义。

变化点可以手动指定,或者可以通过回归模型自动检测候选变化点,随后使用自适应 Lasso 算法(Hui Zhou,2006)进行选择。

除了趋势、季节性和假期,Silverkite 还包括一个自回归项,该项是基于窗口平均值计算的,而不是独立地取滞后项(《为降水过程选择二元马尔科夫模型》,Reza Hosseini 等,2011)。

该自回归项使用 Pasty 库指定,使用类似以下字符串的公式迷你语言形式:

y ~ a + a:b + np.log(x) 

在这个公式中,左侧的 y 定义为三个项的总和,aa:bnp.log(x)。项 a:b 是两个因子 a 和 b 之间的交互作用。Pasty 中的模型模板本身具有高度的可定制性,因此该接口提供了很高的灵活性。

最后,Silverkite 提供了几种模型类型,如岭回归、弹性网和提升树,并支持损失函数,如均方误差(MSE)和分位数损失,用于稳健回归。

根据 LinkedIn 对多个数据集的基准测试,Silverkite 在预测误差方面优于 auto-Arima(pmdarima 库)和 Prophet。然而,Silverkite 的速度约为 Prophet 的四倍,我们将在 第九章概率模型 中介绍 Prophet。

梯度提升

XGBoost(即极限梯度提升的缩写)是梯度提升的高效实现(Jerome Friedman,《贪婪函数逼近:一种梯度提升机器》,2001),用于分类和回归问题。梯度提升也被称为梯度提升机GBM)或梯度提升回归树GBRT)。一个特例是用于排序应用的 LambdaMART。除了 XGBoost,其他实现包括微软的轻量级梯度提升机(LightGBM)和 Yandex 的 Catboost。

梯度提升树是树的集成。这与类似于随机森林的袋装算法类似;然而,由于这是一个提升算法,每棵树都会被计算出来,逐步减少误差。每次新的迭代都会贪心地选择一棵树,并将其预测值基于权重项加到先前的预测值中。还有一个正则化项,用于惩罚复杂性并减少过拟合,类似于正则化贪婪森林(RGF)。

XGBoost算法由陈天奇和卡洛斯·格斯特林于 2016 年发布("XGBoost:一种可扩展的树提升系统"),并推动了许多分类和回归基准的突破。它被用于许多 Kaggle 问题的获胜解决方案。事实上,在 2015 年,29 个挑战获胜解决方案中,有 17 个解决方案使用了 XGBoost。

它的设计非常具有可扩展性,并且扩展了梯度提升算法,用于加权分位数,并通过更智能的缓存模式、分片以及对稀疏性处理的改进,提升了可扩展性和并行化性能。

作为回归的一个特例,XGBoost 可以用于预测。在这种情况下,模型基于过去的值进行训练,以预测未来的值,这可以应用于单变量和多变量时间序列。

Python 练习

让我们将本章迄今为止学到的内容付诸实践。

至于依赖项,在本章中,我们将分别为每个部分安装依赖。可以通过终端、笔记本或 Anaconda Navigator 进行安装。

在接下来的几个部分中,我们将演示如何在预测中进行分类,因此这些方法中的一些可能无法进行比较。我们邀请读者使用每种方法进行预测和分类,并进行结果比较。

需要注意的是,Kats 和 Greykite(在写作时)都是非常新的库,因此它们的依赖项可能还会频繁变化。它们可能会锁定您的 NumPy 版本或其他常用库。因此,我建议您为每个部分分别在虚拟环境中安装这些库。

我们将在下一节中详细介绍这个设置过程。

虚拟环境

在 Python 虚拟环境中,安装到其中的所有库、二进制文件和脚本都是与其他虚拟环境中安装的内容以及系统中安装的内容隔离的。这意味着我们可以安装不同的库,如 Kats 和 Greykite,而无需担心它们之间的兼容性问题,或与我们计算机上安装的其他库之间的兼容性问题。

让我们通过一个简短的教程,介绍如何在使用 Anaconda 的 Jupyter 笔记本中使用虚拟环境(类似地,您也可以使用如 virtualenv 或 pipenv 等工具)。

第一章使用 Python 进行时间序列分析简介中,我们介绍了 Anaconda 的安装,因此我们会跳过这部分安装。请参考那一章,或者访问 conda.io 获取安装说明。

要创建一个虚拟环境,必须指定一个名称:

conda create --name myenv 

这将创建一个同名的目录(myenv),其中所有库和脚本将被安装。

如果我们想使用这个环境,我们必须首先激活它,这意味着我们需要将PATH变量设置为包括我们新创建的目录:

conda activate myenv 

现在我们可以使用像 pip 这样的工具,默认情况下它会使用与 conda 捆绑在一起的版本,或者直接使用 conda 命令来安装库。

我们可以在环境中安装 Jupyter 或 Jupyter Lab 然后启动它。这意味着我们的 Jupyter 环境将包含所有依赖项,因为我们已将它们单独安装。

让我们从一个带有动态时间规整(DTW)的 kNN 算法开始。正如我提到的,这个算法通常作为一个不错的基准进行比较。

使用动态时间规整(DTW)的 k-近邻算法(kNN)在 Python 中实现

在这一节中,我们将基于机器人在一段时间内的力和扭矩测量来分类故障。

我们将使用一个非常简单的分类器,kNN,并且或许我们应该提醒一下,这种方法涉及到逐点计算距离,这通常会成为计算瓶颈。

在这一节中,我们将把 TSFresh 的特征提取与 kNN 算法结合在一个管道中。时间序列管道确实能帮助我们简化过程,正如你在阅读代码片段时会发现的那样。

让我们安装 tsfresh 和 tslearn:

pip install tsfresh tslearn 

我们将在 tslearn 中使用 kNN 分类器。我们甚至可以使用 scikit-learn 中的 kNN 分类器,它允许指定自定义的度量标准。

在这个示例中,我们将从 UCI 机器学习库下载一个机器人执行故障的数据集并将其存储到本地。该数据集包含故障检测后的机器人力和扭矩测量数据。对于每个样本,任务是分类机器人是否会报告故障:

from tsfresh.examples import load_robot_execution_failures
from tsfresh.examples.robot_execution_failures import download_robot_execution_failures
download_robot_execution_failures()
df_ts, y = load_robot_execution_failures() 

列包括时间和六个来自传感器的时间序列信号,分别是 F_xF_yF_zT_xT_yT_z。目标变量 y,其值可以是 True 或 False,表示是否发生了故障。

始终检查两个类别的频率非常重要:

print(f"{y.mean():.2f}") 

y 的均值是 0.24。

然后,我们可以使用 TSFresh 提取时间序列特征,正如在第三章,时间序列预处理中讨论的那样。我们可以填补缺失值,并根据与目标变量的相关性选择特征。在 TSFresh 中,统计测试的 p 值被用来计算特征的重要性:

from tsfresh import extract_features
from tsfresh import select_features
from tsfresh.utilities.dataframe_functions import impute
extracted_features = impute(extract_features(df_ts, column_id="id", column_sort="time"))
features_filtered = select_features(extracted_features, y) 

我们可以继续使用 features_filtered DataFrame,它包含我们的特征——来自传感器的信号和 TSFresh 特征。

让我们通过进行网格搜索来找到一个合适的邻居数值:

from sklearn.model_selection import TimeSeriesSplit, GridSearchCV
from tslearn.neighbors import KNeighborsTimeSeriesClassifier
knn = KNeighborsTimeSeriesClassifier()
param_search = {
    'metric' : ['dtw'],
    'n_neighbors': [1, 2, 3]
}
tscv = TimeSeriesSplit(n_splits=2)
gsearch = GridSearchCV(
    estimator=knn,
    cv=tscv,
    param_grid=param_search
)
gsearch.fit(
    features_filtered,
    y
) 

我们正在使用 scikit-learn 的 TimeSeriesSplit 来划分时间序列。这是为了网格搜索(GridSearch)。

或者,我们也可以仅仅基于索引进行拆分。

我们可以尝试许多参数,特别是在 kNN 分类器中的距离度量。如果你想尝试一下,请参阅 TSLEARN_VALID_METRICS 获取 tslearn 支持的度量标准的完整列表。

让我们预测一些 COVID 案例。在下一节中,我们将从 Silverkite 算法开始。Silverkite 是 LinkedIn 在 2021 年发布的 Greykite 库的一部分。

Silverkite

在写作时,Greykite 的版本为 0.1.1——它尚未完全稳定。它的依赖项可能与一些常用库的较新版本发生冲突,包括 Jupyter Notebooks。不过,如果你在虚拟环境或 Google Colab 中安装该库,不必担心。

只需安装该库及其所有依赖项:

pip install greykite 

现在 Greykite 已经安装好了,我们可以使用它了。

我们将加载来自Our World in Data数据集的 COVID 病例数据,它可能是可用 COVID 数据中最好的来源之一:

import pandas as pd
owid_covid = pd.read_csv("**https://covid.ourworldindata.org/data/owid-covid-data.csv**")
owid_covid["**date**"] = pd.to_datetime(owid_covid["**date**"])
df = owid_covid[owid_covid.location == "**France**"].set_index("**date**", drop=True).resample('**D**').interpolate(method='**linear**') 

我们专注于法国的病例。

我们首先设置 Greykite 的元数据参数。然后,我们将此对象传递给预测器配置:

from greykite.framework.templates.autogen.forecast_config import (
    ForecastConfig, MetadataParam
)
metadata = MetadataParam(
    time_col="date",
    value_col="new_cases",
    freq="D"
) 

我们的时间列是date,值列是new_cases

现在我们将创建forecaster对象,它用于生成预测并存储结果:

import warnings
from greykite.framework.templates.forecaster import Forecaster
from greykite.framework.templates.model_templates import ModelTemplateEnum
forecaster = Forecaster()
    warnings.filterwarnings("ignore", category=UserWarning)
    result = forecaster.run_forecast_config(
        df=yahoo_df,
        config=ForecastConfig(
            model_template=ModelTemplateEnum.SILVERKITE_DAILY_90.name,
            forecast_horizon=90,
            coverage=0.95,
            metadata_param=metadata
        )
    ) 

预测的时间跨度为 90 天;我们将预测未来 90 天。我们的预测区间为 95%。Silverkite 和 Prophet 都支持通过预测区间来量化不确定性。95%的覆盖率意味着 95%的实际值应该落在预测区间内。在 Greykite 中,_components.uncertainty模型提供了关于不确定性的额外配置选项。

我已经添加了一行代码,在训练过程中忽略UserWarning类型的警告,否则会有大约 500 行关于目标列中 0 值的警告。

让我们从结果对象中绘制原始时间序列图,我们可以将预测结果叠加在其上:

forecast = result.forecast
forecast.plot().show(renderer="**colab**") 

如果你不是在 Google Colab 中,请不要使用renderer参数!

我们得到了以下图表:

图 7.4:预测与实际时间序列(Silverkite)

预测结果存储在forecast对象的df属性中:

forecast.df.head().round(2) 

这些是预测结果的上限和下限置信区间:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_wsZ8St/Screenshot 2021-08-30 at 21.25.06.png

图 7.5:预测与实际时间序列的表格(Silverkite)

我们可能需要获取一些关于模型的性能指标。我们可以像这样获取历史预测在保留测试集上的性能:

from collections import defaultdict
backtest = result.backtest
backtest_eval = defaultdict(list)
for metric, value in backtest.train_evaluation.items():
    backtest_eval[metric].append(value)
    backtest_eval[metric].append(backtest.test_evaluation[metric])
metrics = pd.DataFrame(backtest_eval, index=["train", "test"]).T
metrics.head() 

我们的性能指标如下所示:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_UgQpeb/Screenshot 2021-08-30 at 21.28.22.png

图 7.6:在保留数据上的性能指标(Silverkite)

我已将指标缩减为前五个。

我们可以方便地将模型应用到新的数据上,方法如下:

model = result.model
future_df = result.timeseries.make_future_dataframe(
    periods=4,
    include_history=False
)
model.predict(future_df) 

预测结果如下所示:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_LxjFCi/Screenshot 2021-08-30 at 21.31.08.png

图 7.7:预测数据框(Silverkite)

请注意,您的结果可能会有所不同。

通过更改预测器运行配置中的model_template参数,我们可以使用其他预测模型。例如,我们可以将其设置为ModelTemplateEnum.PROPHET.name,以使用 Facebook 的 Prophet 模型。

这就结束了我们对 Silverkite 的介绍。接下来,我们将通过应用 XGBoost 的监督回归方法进行预测。让我们进行一些梯度提升吧!

梯度提升

我们也可以使用监督式机器学习进行时间序列预测。为此,我们可以使用日期和前期值来预测未来。

首先,我们需要安装 XGBoost:

pip install xgboost 

在这个示例中,我们将使用 Yahoo 的每日收盘数据,和本章其他实践部分一样。

让我们一步一步地通过准备和建模过程。

我们首先需要对数据进行特征化。这里,我们通过提取日期特征来做到这一点,但请参见 kNN 部分,在那里我们使用了 TSFresh 的特征提取。你也许想通过结合这两种特征提取策略,或完全依赖 TSFresh,来改变这个示例。

我们将像之前一样重新加载来自Our World in Data数据集的 COVID 新病例数据:

import pandas as pd
owid_covid = pd.read_csv("**https://covid.ourworldindata.org/data/owid-covid-data.csv**")
owid_covid["**date**"] = pd.to_datetime(owid_covid["**date**"])
df = owid_covid[owid_covid.location == "**France**"].set_index("**date**", drop=True).resample('**D**').interpolate(method='**linear**').reset_index() 

对于特征提取,转换器非常方便。转换器基本上是一个类,包含fit()transform()方法,可以使转换器适应数据集并相应地转换数据。以下是用于根据日期注释数据集的DateFeatures转换器的代码:

from sklearn.base import TransformerMixin, BaseEstimator
class DateFeatures(TransformerMixin, BaseEstimator):
    features = [
        "hour",
        "year",
        "day",
        "weekday",
        "month",
        "quarter",
    ]

    def __init__(self):
        super().__init__()
    def transform(self, df: pd.DataFrame):
        Xt = []
        for col in df.columns:
            for feature in self.features:
                date_feature = getattr(
                    getattr(
                        df[col], "dt"
                    ), feature
                )
                date_feature.name = f"{col}_{feature}"
                Xt.append(date_feature)

        df2 = pd.concat(Xt, axis=1)
        return df2
    def fit(self, df: pd.DataFrame, y=None, **fit_params):
        return self 

这个转换器相对简单,它为日期列提取一系列特征,例如小时、年份、日期、星期几、月份、年度周数和季度。这些特征在机器学习上下文中,可能对描述或注释时间序列数据非常有用。

你可以在 GitHub 上找到这个示例的完整代码。我提供了一个额外的转换器,用于处理章节中未涉及的周期性特征。

我们按如下方式应用转换器到 DataFrame 的date列:

from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline, make_pipeline
preprocessor = ColumnTransformer(
    transformers=[(
        "**date**",
        make_pipeline(
            DateFeatures(),
            ColumnTransformer(transformers=[
                ("**cyclical**", CyclicalFeatures(),
                  ["**date_day**", "**date_weekday**", "**date_month**"]
                )
            ], remainder="passthrough")
        ), ["**date**"],
  ),], remainder="passthrough"
) 

如果我们希望为预测提供额外的外生特征,可以设置remainder="passthrough"参数。

我们可以定义一个包含这些预处理步骤和模型的管道,这样就可以进行拟合并应用于预测:

from xgboost import XGBRegressor
pipeline = Pipeline(
    [
        ("**preprocessing**", preprocessor),
         ("xgb", XGBRegressor(objective="**reg:squarederror**", n_estimators=**1000**))
    ]
) 

预测器是一个 XGBoost 回归器。我在调整方面并没有做太多工作。唯一会改变的参数是估计器的数量。我们将使用 1,000 个树的集成大小(即树的数量)。

现在是时候将数据集拆分为训练集和测试集了。这包括两个问题:

  • 我们需要提前对特征和数值进行对齐

  • 我们需要根据截止时间将数据集拆分为两部分

首先设定基本参数。首先,我们希望基于时间范围预测未来。其次,我们需要决定用于训练和测试的数据点数量:

TRAIN_SIZE = int(len(df) * **0.9**)
HORIZON = **1**
TARGET_COL = "**new_cases**" 

我们使用 90%的数据进行训练,并预测未来 90 天的情况:

X_train, X_test = df.iloc[HORIZON:TRAIN_SIZE], df.iloc[TRAIN_SIZE+HORIZON:]
y_train = df.shift(periods=HORIZON).iloc[HORIZON:TRAIN_SIZE][TARGET_COL]
y_test = df.shift(periods=HORIZON).iloc[TRAIN_SIZE+HORIZON:][TARGET_COL] 

这既进行了对齐,也设定了预测范围。因此,我们有了用于测试和训练的数据集,两个数据集都包含了我们希望用 XGBoost 预测的特征和标签。

现在我们可以训练我们的 XGBoost 回归模型,根据我们通过转换器生成的特征和当前值,预测未来 HORIZON 内的值。

我们可以按如下方式拟合我们的管道:

FEATURE_COLS = ["date"]
pipeline.fit(X_train[FEATURE_COLS], y_train) 

我们可以看到以下管道参数:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_VPuhp5/Screenshot 2021-08-30 at 23.08.52.png

图 7.8:管道参数

如果我们从开始到结束创建一个日期系列,我们可以获得整个时间段内模型的预测:

MAX_HORIZON = **90**
X_test_horizon = pd.Series(pd.date_range(
    start=df.date.**min()**, 
    periods=**len**(df) + MAX_HORIZON,
    name="**date**"
)).reset_index() 

应用于X_test的管道的predict()方法为我们提供了预测结果:

forecasted = pd.concat(
    [pd.Series(pipeline.predict(X_test_horizon[FEATURE_COLS])), pd.Series(X_test_horizon.date)],
    axis=1
)
forecasted.columns = [TARGET_COL, "**date**"] 

我们也可以对实际病例做同样的操作:

actual = pd.concat(
    [pd.Series(df[TARGET_COL]), pd.Series(df.date)],
    axis=1
)
actual.columns = [TARGET_COL, "**date**"] 

现在,我们可以通过图表将预测结果与实际值y_test进行对比:

fig, ax = plt.subplots(figsize=(12, 6))
forecasted.set_index("date").plot(linestyle='--', ax=ax)
actual.set_index("date").plot(linestyle='-.', ax=ax)
plt.legend(["forecast", "actual"]) 

这是我们得到的图表:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_Vnji3v/Screenshot 2021-08-30 at 23.29.17.png

图 7.9:预测与实际值对比(XGBoost)

我们可以通过以下方式提取测试期间的性能指标:

from sklearn.metrics import mean_squared_error
test_data = actual.merge(forecasted, on="**date**", suffixes=("**_actual**", "**_predicted**"))
mse = mean_squared_error(test_data.new_cases_actual, test_data.new_cases_predicted, squared=False)  # RMSE
**print("The root mean squared error (RMSE) on test set: {:.2f}".format(mse))** 

我们应该看到类似这样的结果:

The root mean squared error (RMSE) on test set: 12753.41 

接下来,我们将在 Kats 中创建一个用于时间序列预测的集成模型。

Kats 的集成方法

Kats 的安装应该很简单,只需两个步骤。首先,我们安装 Facebook 的 Prophet 库的旧版本 fbprophet:

conda install -c conda-forge fbprophet 

现在我们使用 pip 安装 Kats:

pip install kats 

或者,在 Colab 上,我们可以这样安装 Kats:

!MINIMAL=1 pip install kats
!pip install "numpy==1.20" 

我们将像之前一样加载 COVID 病例数据集。这里只展示最后一行:

df = owid_covid[owid_covid.location == "**France**"].set_index("**date**", drop=True).resample('**D**').interpolate(method='**linear**').reset_index() 

我们将配置集成模型,拟合它,然后进行预测。

首先是我们的集成模型的配置:

from kats.models.ensemble.ensemble import EnsembleParams, BaseModelParams
from kats.models.ensemble.kats_ensemble import KatsEnsemble
from kats.models import linear_model, quadratic_model
model_params = EnsembleParams(
            [
                BaseModelParams("linear", linear_model.LinearModelParams()),
                BaseModelParams("quadratic", quadratic_model.QuadraticModelParams()),
            ]
        ) 

这里,我们只包括了两种不同的模型,但我们本可以加入更多模型,也可以定义更好的参数。这只是一个示例;对于更现实的练习(我留给读者自己做),我建议加入 ARIMA 和 Theta 模型。我们需要为每个预测模型定义超参数。

我们还需要创建集成参数,定义如何计算集成聚合以及如何进行分解:

KatsEnsembleParam = {
    "**models**": model_params,
    "**aggregation**": "**weightedavg**",
    "**seasonality_length**": 30,
    "**decomposition_method**": "**additive**",
} 

要在 Kats 中使用时间序列,我们必须将数据从 DataFrame 或系列转换为 Kats 时间序列对象。我们可以如下转换我们的 COVID 病例数据:

from kats.consts import TimeSeriesData
TARGET_COL = "new_cases"
df_ts = TimeSeriesData(
    value=df[TARGET_COL], time=df["date"]
) 

转换的关键是 Kats 能够推断索引的频率。这可以通过pd.infer_freq()进行测试。在我们的案例中,pd.infer_freq(df["date"])应返回D,表示日频率。

现在我们可以创建我们的KatsEnsemble并进行拟合:

m = KatsEnsemble(
    data=df_ts, 
    params=KatsEnsembleParam
).fit() 

我们可以使用predict()方法为每个模型获取单独的预测。如果我们想要获取集成输出,必须在predict()之后调用aggregate()

m.predict(steps=90)
m.aggregate()
m.plot()
plt.ylabel(TARGET_COL) 

我们预测了 90 天的未来。这些预测结果作为模型的一部分被存储,因此我们不需要捕捉返回的预测结果。然后,我们可以将每个模型的预测结果进行聚合。再次说明,我们不需要获取返回的 DataFrame,因为它已经存储在模型对象内部(m.fcst_df)。

最终,我们使用 Kats 的便捷函数绘制了聚合后的 DataFrame:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_Ou1y0x/Screenshot 2021-08-30 at 23.38.36.png

图 7.10:Kats 集成模型预测

由于我们可以通过更改基础模型参数和添加新模型来调整这个集成模型,因此它为我们提供了大量的改进空间。

是时候总结一下本章我们所学到的内容了。

总结

本章我们讨论了 Python 中流行的时间序列机器学习库。接着,我们讨论并尝试了带有动态时间规整的 k 最近邻算法,用于机器故障分类。我们还谈到了时间序列预测中的验证,并尝试了三种不同的方法来预测 COVID-19 疫情:Silverkite、XGBoost 梯度提升以及 Kats 中的集成模型。

第八章:时间序列的在线学习

在本章中,我们将深入探讨时间序列的在线学习和流数据。在线学习意味着随着新数据的到来,我们不断更新模型。在线学习算法的优势在于,它们能够处理高速度和可能的大规模流数据,并能够适应数据的新分布。

我们将讨论漂移,漂移非常重要,因为机器学习模型的性能可能会因为数据集的变化而受到强烈影响,直到模型变得过时(陈旧)。

我们将讨论什么是在线学习,数据如何变化(漂移),以及自适应学习算法如何结合漂移检测方法来适应这种变化,从而避免性能下降或代价高昂的再训练。

我们将涵盖以下主题:

  • 时间序列的在线学习

    • 在线算法
  • 漂移

    • 漂移检测方法
  • 自适应学习方法

  • Python 实践

我们将从在线学习的讨论开始。

时间序列的在线学习

学习有两种主要场景——在线学习和离线学习。在线学习意味着您在数据流入时逐步拟合模型(流数据)。另一方面,离线学习,即更常见的方式,意味着您有一个从一开始就已知的静态数据集,机器学习算法的参数一次性调整整个数据集(通常将整个数据集加载到内存中或分批处理)。

在线学习有三种主要的应用场景:

  • 大数据

  • 时间限制(例如,实时)

  • 动态环境

通常,在在线学习环境中,您有更多的数据,且非常适合大数据。在线学习可以应用于大数据集,因为在这些数据集上训练整个数据集在计算上不可行。

在线学习的另一个应用场景是在时间限制下进行推断和拟合(例如,实时应用),与离线算法相比,许多在线算法在资源消耗上非常高效。

在线学习的一个常见应用是在时间序列数据上,特别的挑战是时间序列观察的基础生成过程可能随时间变化。这被称为概念漂移。在离线设置中,参数是固定的,而在在线学习中,参数会根据新数据持续调整。因此,在线学习算法可以处理数据的变化,其中一些算法能够处理概念漂移。

下表总结了在线学习和离线学习的一些区别:

离线 在线
需要监控 是的,模型可能变得陈旧(模型将失去性能) 适应变化的数据
再训练成本 昂贵(从头开始) 便宜(增量式)
内存需求 可能需要较高的内存
应用领域 图像分类、语音识别等,假设数据是静态的 金融、电商、经济学和医疗保健等,数据是动态变化的
工具 tslearn, sktime, prophet Scikit-Multiflow, River

图 8.1:在线学习与离线学习方法的对比(时间序列)

还有许多不专门针对在线学习的工具,但支持在线学习,例如最受欢迎的深度学习库——PyTorch 和 TensorFlow,这些库的模型本身支持在线学习,数据加载器支持流式场景——通过迭代器,可以按需加载数据。

监督式机器学习问题的流式处理公式可以如下表示:

  1. 数据点 在时间t到达

  2. 在线算法预测标签

  3. 在下一个数据点到达之前,真实标签会被揭示

在批处理设置中,一组n个数据点 会在时间t同时到达,所有n个数据点将在真实标签被揭示之前,由在线模型进行预测,然后才会到达下一批数据点。

我们可以通过 Python 代码片段演示差异,展示在线与离线设置下机器学习的典型模式。你应该对离线学习熟悉,它的表现形式如下:特征X,目标向量y,和模型参数params

from sklearn import linear_model
offline_model = linear_model.LogisticRegression(params)
offline_model.fit(X, Y) 

这应该是从之前的章节中熟悉的内容,比如第七章时间序列的机器学习模型。为了简化,我们省略了数据加载、预处理、交叉验证和参数调优等问题。

在线学习遵循以下模式:

from river import linear_model
online_model = linear_model.LogisticRegression(params)
for xi, yi in zip(X, y):
    online_model.learn_one(xi, yi) 

这里,我们是逐个数据点喂给模型。再次强调,这只是简化版——我省略了设置参数、加载数据集等内容。

这些代码片段应该清楚地说明主要的区别:一次性在整个数据集上学习(离线)与逐个数据点学习(在线)。

我应该提到在线方法的评估方法:

  • 保留集

  • 预先评估

保留集方法中,我们可以将当前模型应用于独立的测试集。这种方法在批处理和在线(流式)学习中都很流行,并能提供无偏的性能估计。

预先评估中,我们在通过数据序列的过程中进行测试。每个新的数据点会先进行测试,然后再进行训练。

在线学习的一个有趣方面是模型选择,也就是如何在一组候选模型中选择最佳模型。我们在第四章时间序列的机器学习模型中讨论了时间序列模型的模型选择。在在线环境中,模型选择有不同的选项。

多臂强盗(也称为K 臂强盗)问题中,有限的资源必须在多个竞争选项之间分配,以最大化预期收益。每个选择(“臂”)都有其回报,可以随着时间的推移进行学习。随着时间的推移,我们可以调整对这些臂的偏好,并根据预期回报进行最优选择。同样,通过学习不同分类或回归模型的预期回报,基于多臂强盗的方法可以应用于模型选择。在实践部分,我们将讨论用于模型选择的多臂强盗算法。

在接下来的章节中,我们将更详细地探讨增量方法和漂移。

在线算法

当数据逐渐变得可用,或其大小超出系统内存限制时,增量式机器学习算法(无论是监督学习还是无监督学习)可以在数据的部分上更新参数,而不是从头开始学习。增量学习是指通过不断调整模型来适应新的输入数据。

一些机器学习方法天生支持增量学习。神经网络(如深度学习)、最近邻算法和进化方法(例如遗传算法)都是增量式的,因此可以应用于在线学习环境,其中它们会不断更新。

增量算法可能会随机访问以前的样本或原型(选定的样本)。这些算法,如基于最近邻算法的增量算法,称为具有部分记忆的增量算法。它们的变体适用于周期性漂移场景。

许多著名的机器学习算法都有增量式的变体,如自适应随机森林、自适应 XGBoost 分类器或增量支持向量机。

强化学习和主动学习可以看作是在线学习的类型,因为它们以在线或主动的方式进行工作。我们将在第十一章中讨论强化学习,时间序列的强化学习

在在线学习中,更新是持续进行的。其核心是运行统计,因此展示如何在在线环境中增量计算均值和方差会很有帮助。

让我们来看一下在线算术平均值和在线方差的公式。至于在线均值,在时间点t更新均值 可以按以下方式进行:

其中 是之前更新的次数——有时也写作

在线方差 可以基于在线均值和运行中的平方和 进行计算:

离线算法的一个缺点是,它们有时更难实现,并且在掌握库、算法和方法时会有一定的学习曲线。

scikit-learn 是 Python 中机器学习的标准库,但它只有有限的增量算法,主要集中在批量学习模型上。相比之下,有许多专门用于在线学习的库,这些库具有自适应和增量算法,能够覆盖许多使用场景,如不平衡数据集。

来自新西兰怀卡托大学、巴黎电信学院(Télécom ParisTech)和巴黎综合理工学院(École Polytechnique)的研究工程师、学生和机器学习研究者们一直在开发River 库。River 是由两个库合并而成:Creme(作为增量的双关语)和 Scikit-Multiflow。River 包含了许多元方法和集成方法。作为点睛之笔,这些元方法或集成方法中的许多可以使用 scikit-learn 模型作为基础模型。

截至目前,River 库拥有 1700 个星标,并实现了许多无监督和有监督算法。虽然截至目前,River 的文档仍在完善中,但许多功能已经可用,我们将在本章结尾的实践部分中看到。

该图展示了 River 和 Scikit-Multiflow 随着时间的推移的受欢迎程度(以 GitHub 上的星标数为依据):

online_learning-star_history.png

图 8.2:River 和 Scikit-Multiflow 库的星标历史

我们可以看到,尽管 Scikit-Multiflow 稳步上升,但这一增长大多保持平稳。River 在 2019 年超过了 Scikit-Multiflow,并继续获得 GitHub 用户的大量星标。这些星标类似于社交媒体平台上的“点赞”。

本表展示了一些在线算法,其中部分适用于漂移场景:

算法 描述
非常快速决策树VFDT 基于少量示例进行分裂的决策树,也叫做 Hoeffding 树。对漂移较为敏感。
极快速决策树EFDT 当有足够信心时增量构建树,并在有更好分裂时替换当前分裂。假设数据分布是平稳的。
Learn++.NSE 用于非平稳环境的增量学习分类器集成。

图 8.3:在线机器学习算法——其中一些适用于漂移场景

最具代表性的在线算法是Hoeffding 树(Geoff Hulten、Laurie Spencer 和 Pedro Domingos,2001),也叫做非常快速决策树VFDT)。它是最广泛使用的在线决策树归纳算法之一。

尽管一些在线学习算法相对高效,但其性能可能对数据点的顺序极为敏感,且它们可能永远无法摆脱由早期样本驱动的局部最小值。令人吸引的是,VFDT(非常快的决策树)提供了高分类精度,并且具有理论保证,随着时间的推移,它们将趋向于决策树的表现。事实上,VFDT 和传统训练树在树分裂上的差异的概率会随着样本数量的增加而指数下降。

Hoeffding 界限,由 Wassily Hoeffding 于 1963 年提出,表示以概率,随机变量Z的计算均值n个样本中计算后,与真实均值的偏差小于

在这个方程中,R是随机变量Z的范围。这个界限与生成观测值的概率分布无关。

随着数据的输入,Hoeffding 树会不断添加新的分支,并且淘汰过时的分支。然而,问题在于,在概念漂移的情况下,某些节点可能不再满足 Hoeffding 边界。

在接下来的章节中,我们将讨论漂移,为什么你需要关注它,以及如何处理漂移。

漂移

数据质量的一个主要决定因素是漂移。漂移(也称为:数据集漂移)意味着数据中的模式随着时间的推移发生变化。漂移很重要,因为机器学习模型的表现可能会因为数据集的变化而受到不利影响。

漂移过渡可以突如其来,也可以逐渐发生、增量式发生,或者是周期性发生。下面是一个示例:

../Conceptdrift4%20-%20page%201.png

图 8.4:四种类型的概念漂移过渡

当过渡突然而至时,它会从一个时间步骤跳跃到另一个时间步骤,没有明显的准备或警告。与此相对,它也可能是逐步的,首先是小幅变化,然后是更大的变化,再然后是更大的变化。

当过渡逐渐发生时,它可能表现为不同力量之间的来回波动,直到建立一个新的基线。另一种过渡类型是周期性的,当存在不同基线之间的规律性或重复性的变化时。

漂移有不同种类:

  • 协变量漂移

  • 先验概率漂移

  • 概念漂移

协变量漂移描述了自变量(特征)的变化。一个例子可能是法规干预,其中新的法律会震动市场格局,消费者行为也会与之前不同。例如,如果我们想预测吸烟行为下的慢性疾病风险,并且吸烟变得不再普遍,因为有了新的法律,这意味着我们的预测可能会变得不那么可靠。

概率漂移是目标变量的变化。例如,在欺诈检测中,欺诈发生的比例发生变化;在零售中,商品的平均价值增加。漂移的一个原因可能是季节性因素——例如,在冬季卖出更多的外套。

概念漂移中,自变量与目标变量之间的关系发生了变化。这个术语所指的概念是自变量与因变量之间的关系。例如,如果我们想预测吸烟的数量,我们可以假设,在新法律出台后,我们的模型将变得无效。请注意,通常“概念漂移”一词在更广泛的意义上应用,指的是任何非平稳的变化。

协变量漂移:特征P(x)的变化。

标签漂移(先验概率漂移):目标变量P(y)的变化。

概念漂移:(在有监督的机器学习中)目标条件分布的变化——换句话说,自变量与因变量之间的关系发生了变化P(y|X)

通常,在构建机器学习模型时,我们假设数据集不同部分中的数据点属于相同的分布。

尽管偶尔出现的异常现象,如异常事件,通常会被视为噪声并被忽略,但当分布发生变化时,模型通常需要基于新的样本从头开始重建,以捕捉最新的特征。这就是我们在第七章时间序列的机器学习模型》中讨论的使用前向验证测试时间序列模型的原因。然而,从头开始训练可能非常耗时并且需要大量的计算资源。

漂移给机器学习模型带来了问题,因为模型可能会变得陈旧——它们随着时间推移变得不可靠,因为它们捕捉到的关系不再有效。这导致了这些模型的性能下降。因此,预测、分类、回归或异常检测的方法应能够及时检测并应对概念漂移,以便尽快更新模型。机器学习模型通常会定期重新训练,以避免性能下降。或者,也可以根据模型的性能监控或基于变化检测方法在需要时触发重新训练。

对于时间序列的应用,在许多领域,如金融、电商、经济学和医疗健康,时间序列的统计特性可能会发生变化,从而使得预测模型变得无用。令人困惑的是,尽管漂移问题的概念在文献中已有充分研究,但在使用时间序列方法应对这一问题方面,投入的努力却很少。

Gustavo Oliveira 等人于 2017 年提出("《在概念漂移下的时间序列预测:一种基于 PSO 的方法》")训练多个时间序列预测模型。在每个时间点,这些模型的参数会根据最新的性能加权变化(粒子群优化)。当最佳模型(最佳粒子)超出某个置信区间时,便触发了模型的重新训练。

下图展示了错误触发的重新训练和在线学习相结合的一种时间序列预测方法:

图 8.5:时间序列预测的在线学习与重新训练(IDPSO-ELM-S)

你可以看到随着概念漂移的发生,误差率周期性地增加,并且根据漂移检测的概念,触发了重新训练。

许多在线模型已经专门适应了对概念漂移的鲁棒性或处理能力。在本节中,我们将讨论一些最流行或表现最佳的模型。我们还将讨论漂移检测方法。

漂移检测方法

有许多不同的方法可以显式地检测数据流中的漂移和分布变化。Page-Hinkley(Page,1954)和几何移动平均(Roberts,2000)是其中的先驱者。

漂移检测器通常通过性能指标来监控模型性能,但它们也可以基于输入特征,尽管这更多的是一种例外。基本思想是,当样本的类别分布发生变化时,模型不再与当前分布相对应,性能下降(误差率增加)。因此,模型性能的质量控制可以作为漂移检测的依据。

漂移检测方法可以至少分为三类(根据 João Gama 等人,2014 年):

  • 统计过程控制

  • 顺序分析

  • 基于窗口的比较

统计过程控制方法考虑了模型预测的汇总统计量,如均值和标准差。例如,漂移检测方法DDM;João Gama 等人,2004)会在误差率超过之前记录的最小误差率三倍标准差时发出警报。根据统计学习理论,在持续训练的模型中,随着样本数量的增加,错误应该减少,因此只有在发生漂移的情况下,这个阈值才会被超过。

顺序方法基于模型预测的阈值。例如,在线性四率(Wang,2015)方法中,列联表中的比率会递增更新。显著性是根据一个在开始时通过蒙特卡洛抽样估算的阈值来计算的。该方法比 DDM 更能处理类别不平衡。

列联表:比较变量频率分布的表格。具体来说,在机器学习分类中,表格显示了预测标签在测试集上的数量与实际标签的对比。在二分类的情况下,单元格显示真正例、假正例、假反例和真反例。

基于窗口的方法监控错误的分布。例如,ADWIN (自适应滑动窗口) 是由 Albert Bifet 和 Ricard Gavaldà于 2007 年提出的。时间窗口W内的预测错误被划分成更小的窗口,并将这些窗口内的平均误差率差异与 Hoeffding 界限进行比较。原始版本提出了一种变体,其时间复杂度为O(log W),其中W是窗口的长度。

这里列出了一些漂移检测方法:

算法 描述 类型
自适应窗口法 (ADWIN) 基于阈值的自适应滑动窗口算法。 基于窗口
漂移检测方法 (DDM) 基于模型错误率应随时间减少的前提。 统计方法
早期漂移检测法 (EDDM) 基于两个错误之间的平均距离统计。与 DDM 类似,但更适用于渐进式漂移。 统计方法
霍夫丁漂移检测 (HDDM) 基于 Hoeffding 界限的非参数方法——移动平均检验或加权移动平均检验。 基于窗口
Kolmogorov-Smirnov 窗口法 (KSWIN) 在时间序列的窗口中进行 Kolmogorov-Smirnov 检验。 基于窗口
Page-Hinkley 用于高斯信号均值变化的统计检验。 序列化方法

图 8.6:漂移检测算法

Kolmogorov-Smirnov 是一个非参数检验,用于检验连续一维概率分布的相等性。

这些方法可以用于回归和分类(以及预测)场景中。它们可以用来触发模型的重新训练。例如,Hassan Mehmood 等人(2021)在检测到漂移时,会重新训练时间序列预测模型(其中包括 Facebook 的 Prophet 模型)。

漂移检测器都有其关于输入数据的假设。了解这些假设非常重要,我尝试在表格中概述了这些假设,以便你能选择适合你的数据集的检测器。

上述列出的漂移检测方法都有标签成本。由于它们都监控基本分类器或集成分类器的预测结果,因此要求在预测后立即获得类标签。在某些实际问题中,这一限制是不切实际的。这里没有列出其他一些方法,这些方法可以基于异常检测(或新颖性检测)、特征分布监控或模型依赖监控。我们在第六章时间序列的无监督方法中看到了一些这些方法。

在下一节中,我们将介绍一些旨在抵抗漂移的方法。

自适应学习方法

自适应学习是指具有漂移调整的增量方法。这个概念指的是通过在线更新预测模型,以应对概念漂移。目标是通过考虑漂移,使得模型能够确保与当前数据分布的一致性。

集成方法可以与漂移检测器结合使用,以触发基础模型的重新训练。它们可以监控基础模型的表现(通常使用 ADWIN)——表现不佳的模型会被重新训练过的更精确模型替换。

作为一个例子,自适应 XGBoost 算法 (AXGB;Jacob Montiel 等人,2020 年) 是 XGBoost 在处理不断变化的数据流时的适应性改编,其中新的子树是从数据的小批量中创建的,随着新数据的到来。这种算法的最大集成大小是固定的,一旦达到该大小,集成就会在新数据上进行更新。

在 Scikit-Multiflow 和 River 库中,有几种方法将机器学习方法与漂移检测方法结合,这些方法调节适应性。这些方法中的许多都是由这两个库的维护者发布的。以下是其中一些方法的列表:

算法 描述
K-近邻 (KNN) 分类器与 ADWIN 漂移检测器 使用 ADWIN 漂移检测器的 KNN,决定保留或遗忘哪些样本。
自适应随机森林 每棵树都包含漂移检测器。它在检测到警告后开始在后台训练,如果发生漂移,则替换旧的树。
加法专家集成分类器 实现了修剪策略 —— 最旧或最弱的基础模型将被移除。
Hoeffding 自适应树 (HAT) 将 ADWIN 与 Hoeffding 树模型结合,检测漂移并进行学习。
非常快速的决策规则 类似于 VFDT,但使用规则集成而不是树。在 Scikit-Multiflow 中支持通过 ADWIN、DDM 和 EDDM 进行漂移检测。
Oza Bagging ADWIN 不同于有放回采样,每个样本都赋予一个权重。在 River 中,可以将其与 ADWIN 漂移检测器结合使用。
在线 CSB2 一种在线提升算法,折衷了 AdaBoost 和 AdaC2,并可选择使用漂移检测器。
在线提升 带有 ADWIN 漂移检测的 AdaBoost。

图 8.7:自适应学习算法

这些方法通过调节适应性或学习,以漂移检测的概念来应对漂移。

让我们试试这些方法中的一些!

Python 实践

本章的安装非常简单,因为在本章中,我们只会使用 River。我们可以从终端快速安装它(或通过 Anaconda Navigator 安装):

pip install river 

我们将在 Python(或 IPython)终端执行这些命令,但同样,我们也可以在 Jupyter Notebook(或其他环境)中执行它们。

漂移检测

让我们从尝试使用人工时间序列进行漂移检测开始。这个例子来自 River 库的测试。

我们将首先创建一个可以测试的人工时间序列:

import numpy as np
np.random.seed(12345)
data_stream = np.concatenate(
    (np.random.randint(2, size=1000), np.random.randint(8, size=1000))
) 

这个时间序列由两组具有不同特征的序列组成。让我们看看漂移检测算法多快能够检测到这一点。

在这个数据集上运行漂移检测器意味着我们需要遍历数据集,并将值输入到漂移检测器中。我们将为此创建一个函数:

def perform_test(drift_detector, data_stream):
    detected_indices = []
    for i, val in enumerate(data_stream):
        in_drift, in_warning = drift_detector.update(val)
        if in_drift:
            detected_indices.append(i)
    return detected_indices 

现在我们可以在这个时间序列上尝试 ADWIN 漂移检测方法。让我们创建另一个方法,绘制漂移点与时间序列的叠加图:

import matplotlib.pyplot as plt
def show_drift(data_stream, indices):
    fig, ax = plt.subplots(figsize=(16, 6))
    ax.plot(data_stream)
    ax.plot(
        indices,
        data_stream[indices],
        "ro",
        alpha=0.6,
        marker=r'$\circ$',
        markersize=22,
        linewidth=4
    )
plt.tight_layout() 

这是 ADWIN 漂移点的图示:

ADWIN_drift_detection.png

图 8.9:我们人工数据集上的 ADWIN 漂移点

我鼓励你尝试一下,并尝试其他漂移检测方法。

接下来,我们将进行回归任务。

回归

我们将估计中等强度太阳耀斑的发生。

为此,我们将使用来自 UCI 机器学习库的太阳耀斑数据集。River 库附带了该数据集的压缩列分隔数据集,我们将加载它,指定列类型,并选择我们感兴趣的输出。

现在让我们绘制 ADWIN 结果:

from river import stream
from river.datasets import base
class SolarFlare(base.FileDataset):
    def __init__(self):
        super().__init__(
            n_samples=1066,
            n_features=10,
            n_outputs=1,
            task=base.MO_REG,
            filename="solar-flare.csv.zip",
        )
    def __iter__(self):
        return stream.iter_csv(
            self.path,
            target="m-class-flares",
            converters={
                "zurich-class": str,
                "largest-spot-size": str,
                "spot-distribution": str,
                "activity": int,
                "evolution": int,
                "previous-24h-flare-activity": int,
                "hist-complex": int,
                "hist-complex-this-pass": int,
                "area": int,
                "largest-spot-area": int,
                "c-class-flares": int,
                "m-class-flares": int,
                "x-class-flares": int,
            },
        ) 

请注意,我们如何选择目标数量和转换器,这些转换器包含所有特征列的类型。

让我们看看这会是什么样子:

from pprint import pprint
from river import datasets
for x, y in SolarFlare():
    pprint(x)
    pprint(y)
    break 

我们看到数据集的第一个点(数据集的第一行):

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_XK8zOB/Screenshot 2021-07-05 at 23.37.50.png

图 8.10:中等强度太阳耀斑数据集的第一个点

我们看到十个特征列作为字典,输出为浮动值。

让我们在 River 中构建我们的模型管道:

import numbers
from river import compose
from river import preprocessing
from river import tree
num = compose.SelectType(numbers.Number) | preprocessing.MinMaxScaler()
cat = compose.SelectType(str) | preprocessing.OneHotEncoder(sparse=False)
model = tree.HoeffdingTreeRegressor()
pipeline = (num + cat) | model 

这样的管道非常易于阅读:数值特征进行最小-最大缩放,而字符串特征则进行独热编码。预处理后的特征输入到 Hoeffding 树模型中进行回归。

现在我们可以按预序列化的方式训练我们的模型,预测值并进行训练,就像之前讨论过的那样:

from river import evaluate
from river import metrics
metric = metrics.MAE()
evaluate.progressive_val_score(SolarFlare(), pipeline, metric) 

我们使用平均绝对误差MAE)作为我们的评估指标。

我们得到的 MAE 为 0.096979。

这个预序列化评估evaluate.progressive_val_score()等同于以下内容:

errors = []
for x, y in SolarFlare():
    y_pred = pipeline.predict_one(x)
    metric = metric.update(y, y_pred)
    errors.append(metric.get())
    pipeline = pipeline.learn_one(x, y) 

我已添加了两行代码来收集算法学习过程中的误差。

让我们绘制一下这个图:

fig, ax = plt.subplots(figsize=(16, 6))
ax.plot(
    errors,
    "ro",
    alpha=0.6,
    markersize=2,
    linewidth=4
)
ax.set_xlabel("number of points")
ax.set_ylabel("MAE") 

这张图显示了这个误差如何随着算法遇到的点数而变化:

solar_flares_regression_mae.png

图 8.11:根据点数计算的 MAE

我们可以看到,在 20 到 30 个点之后,度量稳定后,Hoeffding 树开始学习,误差不断下降,直到大约 800 个点,之后误差再次增加。这可能是行排序效应。

概念漂移的数据集是适应性模型的典型应用场景。让我们在一个有概念漂移的数据集上比较适应性和非适应性模型:

from river import (
    synth, ensemble, tree,
    evaluate, metrics
)
models = [
    tree.HoeffdingTreeRegressor(),
    tree.HoeffdingAdaptiveTreeRegressor(),
    ensemble.AdaptiveRandomForestRegressor(seed=42)
] 

我们将比较 Hoeffding 树回归器、适应性 Hoeffding 树回归器和适应性随机森林回归器。我们将采用每个模型的默认设置。

我们可以使用一个合成数据集来进行此测试。我们可以在数据流上训练前述的每个模型,并查看均方误差MSE)指标:

for model in models:
    metric = metrics.MSE()
    dataset = synth.ConceptDriftStream(
        seed=42, position=500, width=40
    ).take(1000)
    evaluate.progressive_val_score(dataset, model, metric)
    print(f"{str(model.__class__).split('.')[-1][:-2]}: {metric.get():e}") 

evaluate.progressive_val_score方法遍历数据集的每个点并更新度量。我们得到如下结果:

HoeffdingTreeRegressor: 8.427388e+42
HoeffdingAdaptiveTreeRegressor: 8.203782e+42 AdaptiveRandomForestRegressor: 1.659533037987239+42 

由于这些算法的性质,你的结果可能会有所不同。我们可以设置随机数生成器的种子来避免这种情况,然而,我认为强调这一点是值得的。

我们看到模型误差(MSE)以科学计数法表示,这有助于理解这些数字,因为它们相当大。你会看到误差分为两部分,首先是因子,然后是以 10 为指数的数量级。三个模型的数量级相同,然而,适应性随机森林回归器的误差大约只有其他两个模型的五分之一。

我们还可以通过可视化误差随时间的变化,观察模型的学习和适应过程:

performance_adaptive_models.png

图 8.12:概念漂移数据流的模型表现(MSE)

River 中没有非适应性版本的随机森林算法,所以我们无法进行比较。我们无法得出适应性算法是否真正更有效的明确结论。

如果你想尝试不同的模型、元模型和预处理器,还有很多其他选项可以尝试。

模型选择

我们在本章早些时候提到过使用多臂赌博机进行模型选择,这里我们将通过一个实际示例来演示。这基于 River 文档中的内容。

让我们使用UCBRegressor来选择线性回归模型的最佳学习率。相同的模式可以更广泛地应用于选择任何一组(在线)回归模型。

首先,我们定义模型:

from river import compose
from river import linear_model
from river import preprocessing
from river import optim
models = [
    compose.Pipeline(
        preprocessing.StandardScaler(),
        linear_model.LinearRegression(optimizer=optim.SGD(lr=lr))
    )
    for lr in [1e-4, 1e-3, 1e-2, 1e-1]
] 

我们在 TrumpApproval 数据集上构建并评估我们的模型:

from river import datasets
dataset = datasets.TrumpApproval() 

我们将应用 UCB 赌博机算法,该算法计算回归模型的奖励:

from river.expert import UCBRegressor
bandit = UCBRegressor(models=models, seed=1) 

该赌博机提供了以在线方式训练模型的方法:

for x, y in dataset:
    bandit = bandit.learn_one(x=x, y=y) 

我们可以检查每个“臂”被拉动的次数(百分比)。

for model, pct in zip(bandit.models, bandit.percentage_pulled):
    lr = model["LinearRegression"].optimizer.learning_rate
    print(f"{lr:.1e} — {pct:.2%}") 

四个模型的百分比如下:

1.0e-04 — 2.45%
1.0e-03 — 2.45%
1.0e-02 — 92.25%
1.0e-01 — 2.85% 

我们还可以查看每个模型的平均奖励:

for model, avg in zip(bandit.models, bandit.average_reward):
    lr = model["LinearRegression"].optimizer.learning_rate
    print(f"{lr:.1e} — {avg:.2f}") 

奖励如下:

1.0e-04 — 0.00
1.0e-03 — 0.00
1.0e-02 — 0.74
1.0e-01 — 0.05 

我们还可以绘制奖励随时间的变化,并根据模型表现进行更新:

bandit_reward.png

图 8.13:奖励随时间的变化

你可以看到,随着我们逐步处理数据并更新模型,奖励逐渐变得明显。模型奖励在大约 100 个时间步长时明显分开,而在大约 1000 个时间步长时,似乎已经收敛。

我们还可以绘制不同模型在每个步骤中被选择的百分比(这是基于奖励的):

bandit_percentage.png

图 8.14:随着时间推移选择的模型比例

这个分布大致跟随随时间变化的奖励分布。这是可以预期的,因为模型选择依赖于奖励(以及一个调节探索的随机数)。

我们还可以选择最佳模型(即具有最高平均奖励的模型)。

best_model = bandit.best_model 

赌博机选择的学习率是:

best_model["LinearRegression"].intercept_lr.learning_rate 

学习率为 0.01。

总结

在本章中,我们讨论了在线学习。我们谈到了在线学习方法的一些优点:

  • 它们高效,能够处理高速吞吐量。

  • 它们可以处理非常大的数据集。

  • 它们能够适应数据分布的变化。

概念漂移是数据与目标之间关系的变化。我们已经讨论了漂移的重要性,机器学习模型的性能可能会受到数据集变化的强烈影响,甚至导致模型过时(陈旧)。

漂移检测器不直接监控数据本身,而是用于监控模型性能。漂移检测器可以使流学习方法对概念漂移具有鲁棒性,在 River 中,许多自适应模型使用漂移检测器进行部分重置或调整学习参数。自适应模型是将漂移检测方法结合使用的算法,旨在避免性能退化或避免高成本的重新训练。我们概述了几种自适应学习算法。

在 Python 实践中,我们尝试了 River 库中的一些算法,包括漂移检测、回归和使用多臂赌博机方法的模型选择。

第九章:时间序列的概率模型

概率是衡量某事发生可能性的指标。在销售预测中,不确定性的估计至关重要,因为这些预测提供了有关现金流、利润率和收入的洞察,进而驱动了商业决策,这些决策关系到财务稳定性和员工生计。这时,时间序列的概率模型派上了用场。它们帮助我们在需要确定性估计时做出决策。

在本章中,我将介绍 Prophet、马尔可夫模型和模糊时间序列模型。最后,我们将通过这些方法进行一个实际的练习。

概率建模的另一个应用是估计反事实,其中我们可以估计实验中的治疗效果。我们将讨论贝叶斯结构时间序列模型的概念,并在实践部分通过一个时间序列的实际例子进行演示。

我们将涵盖以下主题:

  • 时间序列的概率模型

  • Prophet

  • 马尔可夫模型

  • 模糊建模

  • 贝叶斯结构时间序列模型

  • Python 练习:

    • 先知

    • 马尔可夫转换模型

    • 模糊时间序列

    • 贝叶斯结构时间序列模型

我们将从时间序列概率预测的介绍开始。

时间序列的概率模型

如前言中所述,概率模型可以帮助我们在不确定性下做出决策,尤其是在估计必须带有量化置信度的情况下,如在财务预测中,这可能至关重要。对于销售或现金流的预测,将概率附加到模型预测中,可以使财务控制人员和经理更容易根据新信息采取行动。

一些著名的算法包括专门用于监控操作指标和关键 绩效指标 (KPIs)的 Prophet 和马尔可夫模型。其他则是随机深度学习模型,如DeepARDeepState。由于我们将在第十章《深度学习模型》中讨论深度学习模型,因此本章不会详细讨论它们。

Prophet 模型源自 Facebook(Taylor 和 Letham,2017),并基于一个可分解的模型,具有可解释的参数。一个设计原则是参数可以由分析师直观地进行调整。

Prophet 和我们在第七章《时间序列的机器学习模型》中介绍的 Silverkite 算法,旨在对具有变化趋势、季节性和重复事件(如假期)以及短期效应的时间序列进行准确预测,因此非常适合数据科学中的许多应用,尤其是在资源规划、优化财务决策和追踪运营分析进展等任务中,这些都是运筹学中的典型任务。

在时间序列应用中,其他类型的模型也特别重要,包括马尔可夫模型,我们将在专门的章节中进行讨论。

贝叶斯结构时间序列BSTS)模型,我们在第六章无监督模型用于时间序列中提到过,允许量化各个组件的后验不确定性,控制组件的方差,并对模型施加先验信念。BSTS 模型是一种可以用于特征选择、时间序列预测和推断因果关系的技术。最后一点,因果推断,是概率模型在时间序列中的另一种应用。例如,在 A/B 测试中,了解干预的影响非常重要。

以下图表展示了适用于时间序列预测的几种选定概率库的受欢迎程度:

probabilistic-star_history.png

图 9.1:用于概率建模时间序列的库

你可以看到,在这三种库中,pyFTS 排名高于另外两个。我没有包含 statsmodels 库,它包括了一些概率模型。我也没有包括 Prophet。statsmodels 和 Prophet 都远远超过了 HMMs,一个用于隐马尔可夫模型的库,以及 Pints,一个用于噪声时间序列的库。

我也没有包含神经网络或深度学习库,如 TensorFlow Probability 或 Gluon-TS。深度学习将在第十章时间序列的深度学习中讨论。

让我们从 Prophet 中的一个预测模型开始!

Prophet

Facebook 的 Prophet 既是一个 Python/R 库,也是附带的算法。该算法于 2017 年发布(《Forecasting at Scale》由 Sean Taylor 和 Benjamin Letham 编写)。作者指出,实践中的预测和异常检测问题涉及处理各种特殊的预测问题,这些问题在 Facebook 中具有分段趋势、多重季节性和浮动假期,并且在整个组织中建立对这些预测的信任。

考虑到这些目标,Prophet 被设计成可以扩展到多个时间序列,足够灵活以适应各种可能具有特殊性、与业务相关的时间序列,并且足够直观,便于领域专家配置,即使他们对时间序列方法了解甚少。

Prophet 算法类似于广义加法模型GAM),并正式化了三种模型组件的预测关系:趋势(增长)、季节性和假期,如下所示:

误差项 epsilon 代表残差——模型未能适应的特殊变化。所有函数都以时间作为回归变量。这三种效应是加法的;然而,Sean Taylor 和 Benjamin Letham 建议,可以通过对数变换实现乘法季节性,其中季节效应是一个乘以 g(t) 的因子。

趋势或增长函数可以是线性或对数型,适用于饱和增长。两者都可以通过变化点来融入分段效应。季节性模型基于傅里叶级数模拟周期性效应。

Prophet 中的变化点选择是自动化的。参数通过 Broyden–Fletcher–Goldfarb–Shanno(BFGS)算法进行优化,该算法在统计建模平台 Stan 中得到了实现。

概率模型的优点在于能够为预测提供一个确定性度量;然而,它们的预测不一定优于非概率模型。Prophet 与其他模型的基准测试结果呈现出混合效果。

在他们 2020 年的论文《销售预测的概率时间序列模型分析》中,Seungjae Jung 及其团队在大规模数据集上验证了概率时间序列模型。该单变量时间序列包含了一个电子商务网站的每日销售数据。

他们将两个深度学习概率模型 DeepAR 和 DeepState 与 Prophet 进行了比较,并与基准模型(包括移动平均MA)、线性回归LR)、多层感知器MLP)和季节性 ARIMASARIMA))进行了对比。你应该记得在第五章移动平均和自回归模型中,MA 是前几天的简单无权均值。他们对 Prophet 和所有基准模型尝试了 72 种不同的超参数。

他们发现,测试中的概率模型甚至未能在均方根误差RMSE)和平均绝对百分比误差MAPE)方面超越最简单的基准模型,如 MLP 和 LR。总体而言,Prophet 表现最差。正如往常一样,模型的表现依赖于数据集和任务本身——没有万能的解决方案。

让我们看看马尔可夫模型是如何工作的!

马尔可夫模型

马尔可夫链是一种概率模型,描述满足马尔可夫性质的可能事件序列。

马尔可夫性质:在具有马尔可夫性质的序列或随机过程中,每个事件的概率仅取决于紧接其前的状态(而不是早期的状态)。这些序列或过程也可以称为马尔可夫过程,或者马尔可夫过程

以俄罗斯数学家安德烈·马尔可夫(Andrey Markov)命名,马尔可夫性质非常理想,因为它显著降低了问题的复杂性。在预测中,代替考虑所有之前的状态(t-1,t-2,…,0),只考虑 t-1。

同样,马尔可夫假设,对于一个数学或机器学习模型来说,是指序列满足马尔可夫性质。在如马尔可夫链和隐马尔可夫模型等模型中,过程或序列假设为马尔可夫过程。

离散时间马尔科夫链DTMC)中,序列(或链)在离散的时间步长之间转换状态。马尔科夫链也可以在连续时间步长下运行。这种不太常见的模型称为连续时间马尔科夫链CTMC)。

隐马尔科夫模型HMM)中,假设过程 X 遵循不可观察的状态 Y,Y 是另一个过程,其行为依赖于 X。HMM 基于 X 建模这个潜在或隐藏的过程 Y。

另一种马尔科夫类型模型是非线性状态转移模型(也称为:马尔科夫切换模型)。该模型由詹姆斯·汉密尔顿于 1989 年发明,专门用于处理突变情况,在这些情况下,传统的线性模型往往难以捕捉到明显的行为变化。状态转移模型是一种自回归模型,其中过程的均值在不同状态之间切换。

对于实际例子,我们将遵循statsmodels库的实现,并构建一个模型来复制汉密尔顿 1989 年的模型。汉密尔顿在 1951 年到 1984 年间建模了实际国民生产总值(RGNP)的时间序列,RGNP 是一个调整了价格变化的宏观经济指标,衡量经济产出的价值。

我们将使用一个四阶马尔科夫切换模型,可以写成如下形式:

对于每个状态(或时期),状态转换按照以下转移概率矩阵进行:

是从状态 i 转移到状态 j 的概率。

在这种情况下,我们正在建模两个状态。

在下一节中,我们将讨论一种基于模糊方法的时间序列建模方法。

模糊建模

模糊逻辑和模糊集理论是由洛特菲·扎德(Lotfi Zadeh)在 1960 年代和 70 年代发展起来的,当时他是加利福尼亚大学伯克利分校的教授。扎德出生于阿塞拜疆巴库,父母分别是波斯人和犹太俄罗斯人,他在伊朗德黑兰完成了学业,后来移居美国,在麻省理工学院和哥伦比亚大学学习。因此,他对不同文化中如何理解概念以及不同语言中如何表达这些概念非常熟悉。这激发了他对近似推理和语言变量的研究方法,并将其形式化为模糊理论。

模糊集理论是一种能够处理与模糊、主观和不精确判断相关问题的方法。模糊性是日常语言中的固有特征,而模糊性理论正是为了解释这一点并以直观的方式进行操作。模糊逻辑表达了主观信念和模糊性。可以说,概率论是模糊逻辑的一个子集。

模糊集是具有成员度的集合。在模糊逻辑中,规则推理的基础不再使用二值(布尔)真值“真”和“假”,而是使用单位区间[0, 1]。更正式地,成员函数表示元素属于集合的确定程度,由成员映射函数来表征:

例如,基于 k-means 的著名算法模糊 C 均值(James Bezdek,1981),返回点属于各个簇的成员度。这意味着每个点可以属于每个簇,但程度不同。与此相对的是,其他聚类算法通常返回的所谓清晰划分,其中一个点要么属于某个簇,要么不属于。

对于模糊逻辑,所有集合运算,如等价、子集与超集、并集和交集,都必须重新定义。两个模糊集(或关系)之间的并集被定义为每个点上的最大操作,而交集则定义为最小操作。更正式地,两个集合 A 和 B 之间的并集,,被定义为:

其中 是点 x 的成员函数。

Song 和 Chissom(1993)提出了一种一阶、时不变的模糊时间序列模型,用于预测阿拉巴马大学的入学人数。其形式化如下:

其中 是第 t 年的入学人数,R是模糊关系的并集, 是模糊 Max-Min 组合运算符。

Max-Min 组合运算,是通过对 A 的第 i 行和 B 的第 j 列逐项取最小值,然后取这些 n 个最小值的最大值来获得的。

这在下面的图示中得到了说明(来自维基百科的矩阵乘法页面):

atrix multiplication diagram 2.svg

图 9.2:Max-Min 组合运算符

用圆圈标记的各位置的值按以下方式计算:

在 Song 和 Chissom 的方法中,提取当前时刻 t 与其之前时刻的值之间的关系,并将其用于预测。算法中的一个必要预处理步骤是将时间序列 X 转换为模糊时间序列 Y。这个过程称为模糊化,指的是将来自实数集的输入约束为离散集的模糊成员。这个量化过程可以通过向量量化方法来执行,比如 Kohonen 自组织映射(SOM),这是一种无监督机器学习方法,能够产生低维表示。

尽管模糊时间序列模型尚未广泛应用,但在一些应用中,它们已被证明与传统方法(如 SARIMA)具有竞争力(例如,Maria Elena 等,2012)。它们适用于离散和连续时间序列,并为预测提供可解释的模型。

在接下来的部分,我们将做一些概率时间序列预测的练习示例。

贝叶斯结构时间序列模型

在因果推断中,我们想分析某个治疗的效果。治疗可以是任何与我们关心的系统或环境互动的行为,从改变网站按钮的颜色到发布一个产品。我们可以选择采取行动(例如发布产品),从而观察治疗下的结果,或者不采取行动,这时我们观察到未治疗下的结果。这个过程在下图中有所示例:

../causal%20(1).png

图 9.3:治疗的因果效应

在图示中,是否采取某个行动(给患者服药),根据是否采取行动,患者可能会康复(循环恢复)或进入重症监护室。

因果效应是治疗下发生的事情与未治疗下发生的事情之间的差异。问题在于,我们无法同时观察到两个潜在的结果。

然而,我们可以通过实验来观察治疗下的潜在结果和未治疗下的潜在结果,例如在 A/B 测试中,治疗只施加在总人口的一个子集上,条件 B 与控制条件 A 进行比较。

我们可以像这样列出潜在结果:

单位 治疗状态, 治疗下的结果, 未治疗下的结果, 协变量,
1 1 对号,Segoe UI 符号字体,字符代码 2714 十六进制 估计 对号,Segoe UI 符号字体,字符代码 2714 十六进制
2 1 对号,Segoe UI 符号字体,字符代码 2714 十六进制 估计 对号,Segoe UI 符号字体,字符代码 2714 十六进制
3 0 估计 对号,Segoe UI 符号字体,字符代码 2714 十六进制 对号,Segoe UI 符号字体,字符代码 2714 十六进制
4 0 估计 对号,Segoe UI 符号字体,字符代码 2714 十六进制 对号,Segoe UI 符号字体,字符代码 2714 十六进制

图 9.4:实验中的潜在结果

在第一列中,单位,我们看到了样本索引。每一行都指的是人口中的一个单独单位或样本。第二列(治疗状态)编码了是否施行了治疗(1)或未施行治疗(0)。在第三和第四列中,治疗下的结果非治疗下的结果分别记录。

这些标记显示了显而易见的事实:当有治疗时,我们可以观察治疗下的结果,但无法观察非治疗下的结果。反之亦然,当没有治疗时,我们可以观察非治疗下的结果,但无法观察治疗下的结果。

最后,在最后一列中,还有一些无论治疗与否都可以帮助我们的模型的额外变量。

使用贝叶斯结构时间序列BSTS),重点是在没有实验的情况下估计治疗效应。我们可以估计或插补反事实,即实验中未知的潜在结果。这允许我们比较治疗下的结果与非治疗下的结果,从而量化因果治疗效应。

该模型由三个主要组件组成:

  1. 卡尔曼滤波器

  2. 变量选择

  3. 贝叶斯模型平均

卡尔曼滤波器用于时间序列分解。这允许对趋势、季节性和假期进行建模。在贝叶斯变量选择步骤(尖峰与平板技术)中,选择最重要的回归预测变量。最后,在模型平均中,将预测结果结合起来。

在 2013 年的论文"用贝叶斯结构时间序列预测现在"中描述了 BSTS 模型在变点和异常检测中的应用,作者是 Steven L. Scott 和 Hal Varian。

2015 年 Google Research 发表了一篇论文,概述了 BSTS 在估计干预因果效应中的应用("使用贝叶斯结构时间序列模型推断因果影响",作者是 Kay H. Brodersen、Fabian Gallusser、Jim Koehler、Nicolas Remy 和 Steven L. Scott)。

这一数学细节超出了本章的范围。幸运的是,我们可以使用 Python 库应用 BSTS 模型。我们将在本章的实践部分中运行一个示例。

我们现在可以在本章学到的一些理论上进行实践。

Python 练习

让我们把到目前为止在本章学到的东西付诸实践。我们将在 Prophet 中做一个模型,一个 Markov 切换模型,一个模糊时间序列模型和一个 BSTS 模型。

让我们开始使用 Prophet 吧!

Prophet

首先,让我们确保我们已经安装了我们需要的一切。我们可以通过终端(或类似地通过 Anaconda 导航器)快速安装所需的库。

pip install -U pandas-datareader plotly 

您将需要一个最新版本的 pandas-datareader,否则可能会出现RemoteDataError

我们将通过 Facebook 的 Prophet 库使用 Prophet 模型。让我们安装它:

pip install prophet 

一旦完成这些,我们就可以开始了。

在这个例子中,我们将使用第七章时间序列的机器学习模型中使用过的 Yahoo 每日收盘股票数据。

简而言之,我们可以通过以下方式下载 2001 到 2021 年的 Yahoo 每日股票历史数据,使用 pandas-datareader:

import pandas as pd
import numpy as np
from pandas_datareader.data import DataReader
from datetime import datetime
yahoo_data = DataReader('JPM',  'yahoo', datetime(2001,6,1), datetime(2021,6,1))
yahoo_df = yahoo_data['Adj Close'].to_frame().reset_index('Date') 

这为我们提供了一个 pandas DataFrame,包含两个列:调整后的每日收盘值和日期。让我们快速检查这两列的数据类型:

yahoo_df.dtypes 

这些是数据类型:

Date         datetime64[ns]
Adj Close           float64
dtype: object 

Date列是以纳秒为单位的日期时间。Adj Close是浮动类型。

我们将把数据输入到fit()方法中进行训练:

from prophet import Prophet
forecaster = Prophet()
forecaster.fit(
  yahoo_df.rename(columns={"**Date**": "**ds**", "**Adj Close**": "**y**"})
) 

我们需要将列重命名为dsy,以符合 Prophet 的约定。现在我们已经有了一个训练好的 Prophet 模型。

然后我们将创建一个新的 DataFrame,其中包含未来的日期。我们可以将这个 DataFrame 传递给 Prophet 模型的predict()方法:

future = forecaster.make_future_dataframe(periods=90) 

预测是通过这个新的 DataFrame 调用predict()方法:

forecast = forecaster.predict(future) 

forecast DataFrame 包含了预测值以及上下置信区间。ds列是与预测值对应的日期。

让我们将预测与实际数据进行对比绘图:

forecaster.plot(forecast, figsize=(12, 6)); 

这是图表:

图 9.5:预测与实际时间序列(Prophet)

你可能想把这个图与第七章时间序列的机器学习模型中的图进行比较。实际数据是粗体且较粗,而预测则较细。上下置信区间围绕着预测值。

我们可以通过查看 DataFrame 来检查预测结果:

图 9.6:预测表(Prophet)

获取第一个模型非常简单,并且有很多方法可以进行调整。

马尔科夫切换模型

对于马尔科夫切换模型,我们将使用statsmodels库。如果你还没有安装它,可以通过以下方式安装:

pip install statsmodels 

在这个例子中,我们将使用statsmodels的数据集。这个数据集基于statsmodels关于马尔科夫切换自回归模型的教程。我们可以从 Stata Press 出版社的网站上获取数据集:

from statsmodels.tsa.regime_switching.tests.test_markov_autoregression import statsmodels.api as sm
import seaborn as sn
import pandas as pd
dta = pd.read_stata('https://www.stata-press.com/data/r14/rgnp.dta').iloc[1:]
dta.index = pd.DatetimeIndex(dta.date, freq='QS')
dta_hamilton = dta.rgnp 

这为我们提供了一个 RGNP 的 pandas 序列,索引标注了日期。让我们快速绘制这个图:

dta_hamilton.plot(title='Growth rate of RGNP') 

我们得到以下图表:

RGNP.png

图 9.7:RGNP 增长率

我们将建模国内衰退和扩张。该模型将包括这两个状态之间的转移概率,并预测每个时间点扩张或衰退的概率。

我们将拟合四阶马尔科夫切换模型。我们将指定两个状态:

import statsmodels.api as sm
mod_hamilton = sm.tsa.MarkovAutoregression(dta_hamilton, k_regimes=2, order=4, switching_ar=False)
res_hamilton = mod_hamilton.fit() 

现在我们已经通过最大似然估计拟合了 RGNP 数据。我们设置了switching_ar=False,因为statsmodels的实现默认使用切换自回归系数。

让我们来看看statsmodels模型的摘要:

print(res_hamilton.summary()) 

我们得到以下输出(截断):

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_qPxkLo/Screenshot 2021-07-11 at 18.36.54.png

图 9.8:马尔可夫切换模型结果

我们可以看到我们有两组参数,每组对应一个制度。我们还得到了统计模型质量的度量(如 AIC 和 BIC)。

在相同输出的底部,我们可以看到制度转换参数:

../../Desktop/Screenshot%202021-07-11%20at%2018.40.01.pn

图 9.9:制度转换参数

这些是我们在马尔可夫切换模型理论部分中提到的制度转换。

让我们来看一下衰退和扩张的长度:

res_hamilton.expected_durations 

输出 array([ 4.07604793, 10.4258926 ]) 以财务季度为单位。因此,预计衰退将持续约四个季度(一年),扩张将持续十个季度(两年半)。

接下来,我们将绘制每个时间点的衰退概率。然而,如果我们叠加由美国国家经济研究局(NBER)提供的衰退指标,这将更具信息性,我们可以使用 pandas-dataloader 来加载这些数据:

from pandas_datareader.data import DataReader
from datetime import datetime
usrec = DataReader('USREC', 'fred', start=datetime(1947, 1, 1), end=datetime(2013, 4, 1)) 

这为我们提供了一个数据框,指示了衰退情况。以下是前五行数据:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_P2RnuW/Screenshot 2021-07-11 at 19.00.46.png

图 9.10:由 NBER 提供的衰退指标

在前五行数据中,根据 NBER 指标没有发生衰退。

我们现在将绘制 NBER 衰退指标与模型制度预测的对比:

import matplotlib.pyplot as plt
_, ax = plt.subplots(1) ax.plot(res_hamilton.filtered_marginal_probabilities[0]) ax.fill_between(
  usrec.index, 0, 1, where=usrec['USREC'].values,
  color='gray', alpha=0.3
)
ax.set(
  xlim=(dta_hamilton.index[4], dta_hamilton.index[-1]),
  ylim=(0, 1),
  title='Filtered probability of recession'
) 

这为我们提供了实际的衰退数据与模型预测的对比:

filtered_probability_of_recession.png

图 9.11:过滤后的衰退概率

我们可以看到,模型预测和实际衰退指标之间似乎有很好的匹配。

不幸的是,statsmodels 实现并未提供预测或样本外预测的功能,因此我们将在此结束简短的演示。

Statsmodels 包含其他用于制度切换模型的数据集供我们探索。

在接下来的实践部分,我们将使用 pyFTS 库(由巴西米纳斯吉拉斯联邦大学(UFMG)的 MINDS 实验室开发)将宋氏和奇索姆的模型应用于时间序列预测问题。

模糊时间序列

在本节中,我们将加载两个时间序列,分别是纳斯达克指数和标准普尔 500 指数,并使用宋氏和奇索姆 1993 年的算法进行预测。这与该库中的示例教程非常接近。

首先,我们将从终端(或类似的 Anaconda 导航器)安装该库:

pip install pyFTS SimpSOM 

然后,我们将定义我们的数据集:

from pyFTS.data import NASDAQ, SP500
datasets = {
  "SP500": SP500.get_data()[11500:16000],
  "NASDAQ": NASDAQ.get_data()
} 

两个数据集,即我们 datasets 字典中的条目,是大约 4000 个标量值的向量。我们将使用其中约 50% 的点进行训练,并将其设置为常量:

train_split = 2000 

该模型假设是平稳过程,因此我们需要按照第二章《探索性时间序列分析》中讨论的那样,通过时间差分预处理我们的时间序列。

我们将定义一个一阶差分操作进行预处理:

from pyFTS.common import Transformations
tdiff = Transformations.Differential(1) 

让我们绘制我们的时间序列和其变换:

import matplotlib.pyplot as plt
fig, ax = plt.subplots(nrows=2, ncols=2)
for count, (dataset_name, dataset) in enumerate(datasets.items()):
  dataset_diff = tdiff.apply(dataset)
  ax[0][count].plot(dataset)
  ax[1][count].plot(dataset_diff)
  ax[0][count].set_title(dataset_name) 

原始时间序列和转化后的时间序列的图表如下所示:

nasdaq_sp500.png

图 9.12: 纳斯达克和标准普尔 500 指数——原始和转化后的时间序列

在这个 GitHub 仓库中,您可以看到应用于变换后时间序列的增广迪基-富勒单位根检验。该平稳性检验为我们提供了绿灯,我们继续进行模型构建。

下一步是训练我们针对两个转化后的(差分)时间序列的模型:

from pyFTS.models import song
from pyFTS.partitioners import Grid
models = {}
for count, (dataset_name, dataset) in enumerate(datasets.items()):
  partitioner_diff = Grid.GridPartitioner(data=dataset, npart=15, transformation=tdiff)
  model = song.ConventionalFTS(partitioner=partitioner_diff)
  model.name = dataset_name
  model.append_transformation(tdiff)
  model.fit(
    dataset[:train_split], 
    order=1
  )
  models[dataset_name] = model 

我们遍历数据集,并为每个数据集训练一个独立的模型,将其保存在字典 models 中。训练过程包括从训练集中提取关系。

作为模型训练的一部分,预处理的时间序列会按照本章中模糊时间序列模型的理论部分进行量化。

我们可以绘制两个模型的预测结果:

_, ax = plt.subplots(nrows=2, ncols=1, figsize=[12, 6])
for count, (dataset_name, dataset) in enumerate(datasets.items()):
    ax[count].plot(dataset[train_split:train_split+200])
    model = models[dataset_name]
    forecasts = model.predict(dataset[train_split:train_split+200], steps_ahead=1)
    ax[count].plot(forecasts)
    ax[count].set_title(dataset_name)

plt.tight_layout() 

再次,我们遍历这两个数据集。这次,我们将测试集中的原始值(200 个点)与预测的一步前值进行对比。请注意,在预测过程中模型并不会根据新数据进行更新。

这是我们将预测结果与测试集中的实际值进行比较的图表:

fuzzy_predicted_actual.png

图 9.13: 模糊时间序列预测与实际值对比(标准普尔 500,纳斯达克)。

看这些图表,预测看起来相当有希望,但让我们看看一些实际数字!

PyFTS 提供了一个方便的函数来提取 RMSE、MAPE,以及最后的 Theil's U,这是一个相关性度量。我们在第二章中介绍了这些度量,探索性时间序列分析与时间序列

from pyFTS.benchmarks import Measures
rows = []
for count, (dataset_name, dataset) in enumerate(datasets.items()):
    row = [dataset_name]
    test = dataset[train_split:train_split+200]
    model = models[dataset_name]
    row.extend(Measures.get_point_statistics(test, model))
    rows.append(row)

pd.DataFrame(
  rows,columns=["Dataset", "RMSE", "MAPE", "Theil's U"]
).set_index("Dataset") 

我们得到这些统计数据:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_PJYAmO/Screenshot 2021-07-11 at 23.36.47.png

图 9.14: 纳斯达克和标准普尔 500 的模糊时间序列建模统计数据

我将把比较这两个模型与其他模型的练习留给读者,通过这些误差指标进行对比。

贝叶斯结构时间序列建模

在这个例子中,我们将应用 BSTS 建模来理解时间序列中治疗的因果效应。

首先,我们将安装这个库:

pip install tfcausalimpact 

现在,我们将加载一个数据集,并估计治疗的结果。

在这里,我们将估计 2015 年 9 月大众汽车排放丑闻的影响。我们将使用三家大公司的股票价值数据,分别是大众汽车、宝马和安联。数据集与 Python Causal Impact(tfcausalimpact)库一起提供:

import pandas as pd
from causalimpact import CausalImpact
data = pd.read_csv(**"https://raw.githubusercontent.com/WillianFuks/tfcausalimpact/master/tests/fixtures/volks_data.csv"**, header=0, sep=**'** **'**, index_col=**'Date'**, parse_dates=True) 

现在我们已经有了股票价值。让我们绘制它们:

data.plot() 

以下是股票的时间变化:

emission_scandal_stocks.png

图 9.15: 三家大公司的股票价值(大众汽车,宝马,安联)

我们可以看到,2015 年底大众汽车股票的价值急剧下跌。让我们尝试找出排放丑闻的实际影响。我们可以这样构建我们的模型:

pre_period = [str(np.min(data.index.values)), "2015-09-13"]
post_period = ["2015-09-20", str(np.max(data.index.values))]
ci = CausalImpact(data.iloc[:, 0], pre_period, post_period, model_args={'nseasons': 52, 'fit_method': 'vi'}) 

模型统计数据为我们提供了因果影响估计:

print(ci.summary()) 

我们在这里看到了这些统计数据:

Posterior Inference {Causal Impact}
                          Average              Cumulative
Actual                    126.91               10026.07
Prediction (s.d.)         171.28 (17.33)       13531.49 (1369.17)
95% CI                    [136.07, 204.01]     [10749.78, 16116.83]
Absolute effect (s.d.)    -44.37 (17.33)       -3505.42 (1369.17)
95% CI                    [-77.1, -9.16]       [-6090.76, -723.71]
Relative effect (s.d.)    -25.91% (10.12%)     -25.91% (10.12%)
95% CI                    [-45.01%, -5.35%]    [-45.01%, -5.35%]
Posterior tail-area probability p: 0.01
Posterior probability of a causal effect: 99.2% 

图 9.16:因果影响估计与模型统计

如前所述,Google 开发的因果影响模型通过将 BSTS 模型拟合到观察数据来工作,随后用于预测如果在给定时间段内没有发生干预,结果会是怎样。

总体估计效应约为 44 个点——如果没有排放丑闻,股票价格将高出 44 个点。影响总结报告为我们提供了以下分析(摘录):

During the post-intervention period, the response variable had
an average value of approx. 126.91\. By contrast, in the absence of an intervention, we would have expected an average response of 171.28\. The 95% interval of this counterfactual prediction is [136.07, 204.01].
Subtracting this prediction from the observed response yields
an estimate of the causal effect the intervention had on the
response variable. This effect is -44.37 with a 95% interval of [-77.1, -9.16]. For a discussion of the significance of this effect, see below. 

图 9.17:因果影响分析报告

这为我们提供了关于模型估计的非常好的概念。

我们也可以绘制效应图:

ci.plot(panels=["original"] 

绘图如下:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_OzvijR/Screenshot 2021-07-18 at 17.03.38.png

图 9.18:因果影响图

再次,我们看到原始时间序列与预测的反事实值。

排放丑闻使大众汽车损失了大量的价值。这 44 个点可以为我们提供作弊排放测试对大众汽车造成的金钱损失。

总结

在本章中,我们讨论了如何使用时间序列的概率模型帮助我们在金融预测的背景下做出带有不确定性估计的决策。这些预测驱动着财务规划的商业决策。

我已经介绍了 Prophet、马尔可夫模型和模糊时间序列模型。我们讨论了 Facebook 的 Prophet 模型的组成部分。对于马尔可夫模型,我们讨论了主要的概念,如马尔可夫性质,并详细讨论了转换模型。接着,我解释了模糊集合理论的一些基础知识以及它如何应用于时间序列。

最后,我们深入探讨了 BSTS 模型的直觉以及在实验中估计处理效应的一些理论。

最后,我们通过每种方法进行了一次应用练习。在 BSTS 实践中,我们研究了大众排放丑闻的影响。

第十章:时间序列的深度学习

深度学习是机器学习的一个子领域,关注与神经网络相关的算法。神经网络,或者更准确地说,人工神经网络ANNs),得名于它们与人类大脑中生物神经网络的松散关联。

近年来,深度学习在多个应用领域推动了技术的进步。对于非结构化数据集,如文本、图像、视频和音频,这一点尤为明显;然而,表格数据集和时间序列迄今为止对深度学习的适应性较差。

深度学习提供了非常高的灵活性,并且能够结合第八章中讨论的在线学习(时间序列的在线学习)和第九章中讨论的概率方法(时间序列的概率模型)的优势。然而,由于其高度参数化的模型,找到合适的模型可能会是一个挑战。

深度学习为时间序列带来的贡献包括数据增强、迁移学习、长时间序列的预测、以及使用生成对抗网络GANs)的数据生成。然而,直到最近,深度学习方法在预测、分类和回归任务中才变得具有竞争力。

在这一章中,我们将讨论应用于时间序列的深度学习,特别是针对时间序列设计的算法和方法。我们将探讨当前的挑战、前景广阔的研究方向以及将深度学习引入时间序列的竞争性方法。我们将详细介绍深度学习在时间序列领域的最新创新。

我们将涵盖以下主题:

  • 深度学习简介

  • 时间序列的深度学习

    • 自编码器

    • InceptionTime

    • DeepAR

    • N-BEATS

    • 循环神经网络

    • 卷积神经网络(ConvNets)

    • Transformer 架构

    • Informer

  • Python 实践

    • 完全连接网络

    • 循环神经网络

    • 膨胀因果卷积神经网络

让我们从深度学习及其核心概念的介绍开始。

深度学习简介

深度学习基于一些早在 20 世纪初就出现的基础概念——神经元之间的连接。神经元通过所谓的神经突起在化学和电气方面进行交流。

这种神经网络的连接图首次由西班牙神经科学家圣地亚哥·拉蒙·卡哈尔(Santiago Ramón y Cajal)描述和绘制。他绘制了大脑的解剖图以及大脑神经网络的结构。他因在生理学或医学领域的贡献,于 1906 年获得诺贝尔奖,与卡米洛·戈尔吉(Camillo Golgi)共享该奖项,戈尔吉发明了用于神经元染色的钾二铬酸盐和硝酸银染色法,而拉蒙·卡哈尔在显微镜研究中应用了这些染色方法。

下面的图表仅仅是他精心绘制的神经元之间神经连接的树突状分支(称为神经突起——树突和轴突)图(来源:Wikimedia Commons):

ile:Debuixos Santiago Ramón y Cajal.jpg

图 10.1:Ramon y Cajal 绘制的大脑中神经元网络图

在示意图中,您可以看到神经元作为大脑各层中的灰色点。在神经元之间是树突和轴突,大脑的“线路”。每个神经元通过门控站接收环境信息,然后将其传递到称为突触的神经突起。

Ramón y Cajal 及其学生们提出了电缆理论,其中神经突起上通过电流传递的电流被数学模型建模。通过树突接收到来自不同部位和时间的突触输入的电压被认定为感觉和其他信息在细胞之间传递。这为今天在研究中使用的详细神经元模型奠定了基础,用于模拟突触和神经反应。

神经元的基本功能由 Frank Rosenblatt 于 1958 年正式化为感知机——一个包含现代大多数深度学习概念精髓的模型。

../perceptron.png

图 10.2:感知机模型

在感知机模型中,一个神经元——图中间的椭圆——接收来自其他神经元的输入。在模型中,这些输入可以代表文本、图像、声音或任何其他类型的信息。这些输入通过求和的方式进行整合。在这个和中,每个来自神经元i的输入都有一个权重w[i],表示它的重要性。这个整合后的输入可以通过神经元的激活函数g来引发神经激活。

在最简单的情况下,激活函数可以只是一个阈值函数,当输入的加权和超过某个值时,神经元会被激活。在现代神经网络中,激活函数是非线性函数,例如 sigmoid 函数或修正线性函数,其中输出在阈值以上是线性的,而阈值以下被裁剪。

当网络受到数据刺激时,输入神经元被激活,并将信号传递给二级神经元,随后再传递给其他神经元,直到激活输出层。这被称为前向传播

感知机由一层集成神经元组成,这些神经元通过它们的输入连接对输入进行求和。Marvin Minsky 和 Seymour Pappert 在他们的著作《感知机》(1969)中证明,这些神经元类似于简单的线性模型,无法逼近现实世界中复杂的函数。

然而,多层神经网络可以克服这一限制,这也正是我们逐步进入深度学习领域的地方。这些网络可以通过一种称为反向传播的算法进行训练——通常归功于 Paul Werbos(1975)。在反向传播中,可以将输出与目标进行比较,并将误差导数反馈到网络中,以计算连接中权重的调整。

神经网络中的另一个创新再次来源于神经科学。在 1950 年代和 1960 年代,David Hubel 和 Torsten Wiesel 发现,猫视觉皮层(V1)中的神经元对视觉场的局部区域做出反应。他们称这一区域为感受野(1959 年,“猫条纹皮层中单个神经元的感受野”)。他们区分了两种基本细胞类型:

  • 简单细胞——这些细胞的特征主要是对输入的求和

  • 复杂细胞——对不同位置的多种刺激做出反应的细胞

复杂细胞启发了神经网络中采用卷积的计算层,最早由 Kunihiko Fukushima 于 1980 年提出。我们在第三章《时间序列预处理》中讨论了卷积。

具有卷积层的神经网络是图像处理、分类和分割等应用中主要的模型类型。Yann LeCun 及其同事在 1989 年提出了 LeNet 架构,其中通过反向传播学习卷积核,用于手写数字图像的分类。

深度学习网络通常不仅仅由层构成,输入从一层传播到下一层(前馈)。这些连接也可以是递归的,连接到同一层的神经元,甚至回到同一神经元。

一种递归神经网络框架,长短期记忆LSTM)由 Jürgen Schmidhuber 和 Sepp Hochreiter 于 1997 年提出。与之前的模型相比,LSTM 能够在更长时间内检索和学习信息。该模型架构曾一度驱动着行业模型,如 Android 智能手机的语音识别软件,但后来大多被卷积模型取代。

2012 年,AlexNet 由 Alex Krizhevsky 与 Ilya Sutskever 和 Geoffrey Hinton 合作创建,在 ImageNet 大规模视觉识别挑战赛(简称 ImageNet)中取得突破,在该比赛中,数百万张图像需要被归类到 20,000 个类别中。AlexNet 将 Top-5 错误率从大约 25%降至约 15%。该模型利用由图形处理单元GPU)驱动的大规模并行硬件,将全连接层与卷积层和池化层结合起来。

这只是对不同任务(包括图像)的性能极大提升的开始。AlexNet 的性能在第二年被 ResNet 超越。

由于 ResNet 论文具有重要影响力,值得稍作绕道来解释它是如何工作的。ResNet 由 Kaiming He 和微软研究院的其他人于 2015 年提出(“深度残差学习用于图像识别”)。深度神经网络的一个常见问题是,随着更多层的添加,其性能可能会饱和并退化,部分原因是梯度消失问题,在该问题中,优化中计算的误差梯度变得太小,无法发挥作用。

受到大脑金字塔细胞的启发,残差神经网络采用了所谓的跳跃连接,基本上是跳过中间层的捷径。ResNet 是包含跳跃连接(残差块)的网络,如下图所示:

../resnet%20(2).png

图 10.3:带跳跃连接的残差块

在所示的残差块中,第二层的输出如下:

其中 分别是第二层的激活函数和跳跃连接。 通常是恒等函数,其中第一层的激活值保持不变。如果第一层和第二层的输出维度不匹配,则会使用填充或卷积。

通过这些跳跃连接,何凯明等人成功训练了包含多达 1000 层的网络。2015 年发布的原始 ResNet 在图像处理方面非常成功。它赢得了多个图像分类和目标检测的顶级竞赛奖项:ILSVRC 2015,ILSVRC 2015,COCO 2015 竞赛中的 ImageNet 检测,ImageNet 定位,Coco 检测和 Coco 分割等。

我在这里总结了人工神经网络和深度学习的早期历史:

../timeline%20of%20deep%20learning%20(2).png

图 10.4:人工神经网络与深度学习的时间轴

请注意,这只是一个高度简化的版本,省略了许多重要的里程碑。我将时间轴结束于 2015 年,当时 ResNet 首次发布。

深度学习中有许多架构和方法,本图展示了这些方法的类型学:

deep%20learning%20models.png

图 10.5:深度学习方法的类型学

我们在本节中提到了一些方法,接下来的章节将更详细地解释其中的一些方法,因为它们与时间序列相关。

基于深度神经网络的技术的计算复杂性,首先由输入数据的维度驱动,并且取决于通过反向传播训练的隐藏层数量。高维数据往往需要更多的隐藏层,以确保更高层次的特征学习,每一层基于上一层提取更高层次的特征。随着神经元数量的增加,训练时间和复杂性也会增加—有时超参数的数量可达到数百万或数十亿。

深度学习的表示能力通过构建一系列派生特征作为学习的一部分,使模型能够摆脱手工制作的特征。使用深度学习模型的进一步优势包括其在选择架构、超参数(如激活函数、正则化、层大小和损失目标)方面的灵活性,但这与其参数数量的复杂性以及难以探查其内部工作原理之间的权衡。

与其他机器学习方法相比,深度学习方法在多个时间序列数据集上提供了更好的表示和预测;然而,到目前为止,它们尚未在其他领域取得的影响。

时间序列的深度学习

近年来,深度神经网络得到了广泛应用,在各个应用领域取得了前所未有的改进,特别是在图像、自然语言处理和声音领域。深度学习模型的潜在优势在于,它们比其他类型的模型更准确,从而推动了视觉、声音和自然语言处理NLP)等领域的进展。

在预测中,特别是需求预测,数据通常是高度不稳定、不连续或突发的,这违反了经典技术的核心假设,如高斯误差、平稳性或同方差性,正如第五章中所讨论的《时间序列预测》。应用于预测、分类或回归任务的深度学习技术可以克服经典方法面临的许多挑战,最重要的是,它们可以提供一种建模非线性动态的方法,而这些动态通常被传统方法(如 Box-Jenkins、指数平滑(ES)或状态空间模型)忽视。

最近,许多深度学习算法被应用于时间序列,包括单变量和多变量时间序列。模型架构包括递归神经网络(RNN),最显著的是长短期记忆(LSTM)模型,以及变换器和卷积模型,或不同类型的自编码器。

然而,关于它们在时间序列中的应用,它们还未能挑战该领域的顶级模型。例如,正如 Spyros Makridakis 等人(2020)所指出的,在 M4 竞赛中,作为单变量时间序列预测的最重要基准,排名最高的方法是广泛使用的经典统计技术的集成,而不是纯粹的机器学习方法。

这至少部分可能与竞争的性质有关。正如 Slawek Smyl 所指出的,季节性系列的去季节化在 M4 竞赛中非常重要,因为这些系列以标量向量的形式提供,没有时间戳,因此无法加入日历特征,如星期几或月份。

在 M4 竞赛中,在 60 个参赛作品中,排名第 23 的机器学习方法为首。然而,值得注意的是,M4 竞赛的获胜者是一个基于扩展 LSTM 和注意力机制与 Holt-Winters 统计模型的混合模型。另一个竞争者,由 Rob Hyndman 团队开发,使用了基于梯度提升树的集成方法,将传统模型的输出作为输入(FFORMA:基于特征的预测模型平均,2020)。

这些排名让 Spyros Makridakis 等人得出结论,混合或结合传统与机器学习方法是未来的方向。目前正在寻找一种深度学习架构,能够为研究和应用带来类似 AlexNet 或 Inception 在图像领域的转折点。

第四章时间序列机器学习导论中,我们首先讨论了击败基准方法(如最近邻算法结合动态时间规整DTW))的难度,然后是最先进的方法。从性能角度来看,最具竞争力的模型是HIVE-COTE基于变换集成的层次投票集合),它由多个机器学习模型的集成组成——由于计算量大且运行时间长,资源消耗非常高。

挖苦的读者可能会评论说,这听起来已经像深度学习了,并质疑深度学习是否已经取代了作为最先进方法的地位。一般来说,深度学习模型的复杂度远高于传统模型或其他机器学习技术。这可以说是深度学习模型最大的特点之一。

是否存在比 HIVE 更简单或相似复杂度的深度学习模型架构,能够达到竞争力的结果?

我已将一些实现时间序列深度学习算法的库汇总在此表中:

维护者 算法 框架
dl-4-tsc Hassan Ismail Fawaz 多层感知机MLP),全连接网络FCN),ResNet,编码器(基于 CNN),多尺度卷积神经网络MCNN),时间 Le-Nett-LeNet),多通道深度卷积神经网络MCDCNN),时间 CNN,时间规整不变回声状态网络TWIESN),InceptionTime TensorFlow/Keras
Sktime-DL 英国东安格利亚大学的 Tony Bagnell 团队 ResNet,CNN,InceptionTime(通过与其他库的接口) TensorFlow/Keras
Gluon-TS 亚马逊网络服务实验室 Gluon-TS 专注于概率神经网络模型,如:卷积神经网络CNN),DeepAR,循环神经网络RNN),多层感知机MLP MXNET
Pytorch Forecasting Daniel Hoyos 等人 循环网络(GRU,LSTM)、时序融合变换器、N-Beats、多层感知器、DeepAR PyTorch Lightning

图 10.6:几个深度学习库在时间序列中的概览

Sktime-DL 是 sktime 的扩展,由同一研究小组维护。到 2021 年 8 月,该库正在重写中。

Gluon-TS 基于 MXNET 深度学习建模框架,除了表格中列出的网络架构外,还包含许多其他功能,如支持向量机SVMs)和高斯过程GP)的内核,以及用于概率网络模型的分布。

dl-4-tsc 是 Hassan Ismail Fawaz 等人(2019)为多种时间序列深度学习算法的综述论文准备的 GitHub 伴随库。它包括了他们实现的 TensorFlow/Keras 实现。它不是一个库本身,因为它不像一个库那样安装,模型在数据集上运行;然而,由于这些算法已在 TensorFlow 和 Keras 中实现,任何熟悉这些框架的人都会觉得很亲切。

Pytorch-forecasting、sktime-DL 和 Gluon-TS 都有自己的数据集抽象,帮助自动化常见任务。虽然 Sktime-DL 构建于 sktime 抽象之上,Pytorch-Forecasting 和 Gluon-TS 则内置了用于深度学习的工具,处理常见任务,如变量的缩放和编码、目标变量的归一化和下采样。然而,这些抽象带来了学习曲线,我要提醒那些急于求成的读者,这可能需要一些时间才能掌握,这也是我在实践部分中省略它们的原因。

我已从此表中省略了仅实现单一算法的库。在接下来的可视化中,我包含了一些这样的库,如 Informer 模型或神经先知的库。在下图中,您可以看到几个时间序列深度学习库的受欢迎程度:

deep_learning-star_history.png

图 10.7:时间序列深度学习库的受欢迎程度

一如既往,我尽量选择最受欢迎的库——以及最近有更新的库。可以看出,Gluon-TS 是最受欢迎的库。在实现多个算法的库中,Pytorch Forecasting 紧随其后,最近在受欢迎程度上有所突破。

在接下来的部分,我们将重点介绍时间序列深度学习中的一些最新和具有竞争力的方法。我们将详细介绍一些最突出的算法:自动编码器、InceptionTime、DeepAR、N-BEATS、RNN(尤其是 LSTM)、卷积神经网络(ConvNets)和变换器(包括 Informer)。

自动编码器

自编码器AEs)是人工神经网络,能够学习高效地压缩和编码数据,并通过重建误差进行训练。一个基本的线性自编码器本质上在功能上等同于主成分分析PCA),尽管在实际应用中,自编码器通常会进行正则化处理。

自编码器由两部分组成,编码器和解码器,如下所示(来源:维基百科):

图 10.8:自编码器架构

编码器和解码器通常具有相同的架构,这取决于具体领域。例如,在图像处理中,它们通常包含像LeNet这样的卷积层。在建模时间依赖性时,它们可以包括因果卷积或递归层,用来建模时间依赖性。

自编码器是减少噪声的自然方式。它们常用于时间序列中的异常检测。

InceptionTime

在一次大规模的测试中,哈桑·伊斯梅尔·法瓦兹(Hassan Ismail Fawaz)和其他人在 60 个 GPU 的集群上运行了深度学习算法,测试了单变量 UCR/UEA 时间序列分类档案(85 个时间序列)和 13 个多变量时间序列MTS)分类档案中的数据集。他们在 2019 年的论文“时间序列分类中的深度学习:综述”中展示了这项工作。

他们对 11 个模型进行了系统评估,包括 LeNet、全连接网络FCNs)、Time-CNN 和 ResNet。仅有 9 个算法完成了所有测试。与单变量数据集(UCR/UEA)上的深度学习算法相比,ResNet 在 85 个问题中赢得了 50 个,并且在统计上优于下一个最佳算法全卷积神经网络FCNN)。同时,它在统计上与 HIVE-COTE(时间序列分类中的顶级模型)并无显著差异。在多变量基准测试中,FCNN 获胜,尽管他们没有发现网络之间有任何统计显著差异。

在另一篇论文中,哈桑·伊斯梅尔·法瓦兹(Hassan Ismail Fawaz)和一个扩展的研究小组,包括来自莫纳什大学的 Geoff Webb 等人(我们在第三章,“时间序列预处理”中曾遇到过他们),提出了一个新模型,称为 InceptionTime。

InceptionTime这个名字参考了 Inception 模型(“Going Deeper with Convolutions”,2014),这是由谷歌的研究人员和北卡罗来纳大学及密歇根大学的研究人员提出的一个网络。Inception 架构由前馈层和卷积层组成,类似于我们在本章早些时候提到的 LeNet。一个 22 层的变种因此也被称为 GoogleLetNet(或者:Inception 模型版本 1)。粗略来说,Inception 模型由多个模块(“Inception 模块”)组成,这些模块将不同大小的卷积连接在一起。

InceptionTime 采用不同超参数(不同长度的滤波器)的 Inception 类型模型的集成。它们对集成中的网络数量和滤波器大小进行了实验,最终证明它们的模型在 UCR 档案的 85 个数据集上显著超越了 ResNet,并且在统计上与 HIVE-COTE 相当,同时训练时间相比 HIVE-COTE 大幅减少。

DeepAR

DeepAR 是亚马逊德国研究院提出的一种概率预测方法。它基于训练一个自回归循环网络模型。在他们的文章"DeepAR: Probabilistic forecasting with autoregressive recurrent networks"(2019 年)中,David Salinas、Valentin Flunkert 和 Jan Gasthaus 通过在多个真实世界预测数据集(如零部件、电力、交通、ec-sub 和 ec)上的广泛实证评估,展示了相比于最先进方法,准确性提高了约 15%。

DeepAR 是为需求预测而设计的,采用 RNN 架构,并结合了负二项分布似然,用于无界计数数据,这些数据可以跨越几个数量级。蒙特卡洛采样被用于计算预测区间内所有子区间的分位数估计。当时间序列的幅度变化较大时,它们还通过依赖于时间序列均值和网络输出的因子,对负二项似然的均值和方差参数进行了缩放。

N-BEATS

这是一个用于单变量时间序列点预测的模型架构,基于前向和后向残差连接,以及一个非常深的全连接 ReLU 神经元的多层网络。N-BEATS 使用深度学习原语,如残差块,而不是任何时间序列特定的组件,是第一个展示不使用任何时间序列特定组件的深度学习架构,能够超越成熟统计方法的架构。

该网络由 Yoshua Bengio 团队于 2019 年发布("N-BEATS: Neural basis expansion analysis for interpretable time-series forecasting"),在两种配置下达到了最先进的性能,并在 M3、M4 和旅游竞赛数据集的基准测试中超越了所有其他方法,包括传统统计方法的集成方法。

深度学习的一个常见批评是其学习过程的黑箱性,或者反过来说,就是网络的行为缺乏透明度。N-BEATS 可以通过少量改动实现可解释性,而不会显著降低准确性。

循环神经网络

RNN,特别是 LSTM,已广泛应用于多变量电力消费预测。电力预测是一个长序列时间序列,需要精确捕捉序列中各项之间随时间变化的长程相关性。

早期的研究探讨了 LSTM 与膨胀、残差连接和注意力机制的结合。这些为 M4 竞赛的获胜者(Slawek Smyl,2020)提供了基础。

Smyl 提出了一种将标准指数平滑ES)模型与 LSTM 网络相结合的方法。ES 方程使得该方法能够有效捕捉个别系列的主要组成部分,如季节性和基线水平,而 LSTM 网络则能够建模非线性趋势。

RNN,包括 LSTM,的一个问题是它们无法轻松地进行并行化,这会导致训练时间和计算资源的浪费。也有人认为,LSTM 无法捕捉长距离依赖关系,因为它们在处理超过大约 100 时间步的序列时表现不佳。RNN 通过编码过去的隐藏状态来捕捉与之前项目的依赖关系,而由于长依赖关系,它们的性能会下降。

在下一部分中,我们将介绍 transformer 架构,它在性能和最近的流行度方面,正在取代 LSTM 模型。

已经证明,卷积架构在音频处理和机器翻译等任务中可以优于递归网络,并且它们也已应用于时间序列任务。

卷积神经网络(ConvNets)

卡内基梅隆大学和英特尔实验室的研究人员("An Empirical Evaluation of Generic Convolutional and Recurrent Networks for Sequence Modeling" 2018)比较了用于序列建模的通用卷积网络和递归网络(如 LSTM/GRU),并在广泛的任务中进行了测试。这些任务使用了大型文本数据集,并涉及序列问题,如加法问题、记忆任务的复制或多音音乐。像这些问题和数据集通常用于对递归网络进行基准测试。

他们发现,一种简单的卷积架构——时间卷积网络TCN),在许多任务中比经典的递归网络(如 LSTM)表现更好,并且展示了更长的有效记忆。

TCN 中卷积的重要特征是它们是因果的。如果卷积的输出仅由当前和过去的输入决定,那么它就是因果的。以下是一个示例(来源:keras-tcn,GitHub):

图 10.9:时间因果卷积

在时间 t 的输出仅与时间 t 及之前的元素进行卷积。这意味着未来的信息不能泄露到过去。这种基本设计的一个缺点是,为了实现长的有效历史大小,我们需要一个极其深的网络或非常大的滤波器。

卷积相较于 RNN 的优势包括并行性、灵活的感受野大小(指定模型可以看到的范围)和稳定的梯度——反向传播过程中,RNN 会面临梯度消失问题。

Transformer 还解决了 RNNs 被认为存在的不足之处。

Transformer 架构

Transformer 是由 Google Brain 和多伦多大学的研究人员在 2017 年发布的文章《Attention is all you need》中提出的,旨在避免递归,以便支持并行计算。

Transformer 引入了两个核心构件——多头注意力和位置嵌入。与按顺序处理不同,序列被作为整体处理,而不是逐项处理。它们采用自注意力机制,存储句子中各个元素之间的相似度分数。

Transformer 最初是为机器翻译而提出的,研究表明它们在翻译性能上超越了 Google 的神经机器翻译模型。因此,Transformer 的核心是两个序列的对齐。与递归不同,Transformer 引入了位置嵌入,权重编码了与序列中某个特定位置相关的信息。

Transformer 由堆叠的模块组成,首先是编码器模块,然后是解码器模块。每个编码器模块由一个自注意力层和一个前馈层组成,而解码器模块则包括自注意力层、编码器-解码器注意力层和前馈层。这些模块可以堆叠起来,从而创建非常大的模型,能够学习海量数据集。

Transformer 在自然语言处理(NLP)领域推动了新一轮的创新,尤其是在翻译和语言理解方面。此外,OpenAI 强大的 GPT-3 语言生成模型也是基于 Transformer 架构,DeepMind 的 AlphaFold 2 模型也采用了 Transformer 架构,该模型通过基因序列预测蛋白质结构。

Transformer 能够在更长的序列中保持性能。然而,它们只能捕捉到训练时使用的固定输入大小内的依赖关系。为了处理超过固定输入宽度的更长句子,像 Transformer-XL 这样的架构重新引入了递归机制,通过存储已编码句子的隐藏状态,在后续编码下一个句子时加以利用。

在文章《Temporal Fusion Transformers for Interpretable Multi-horizon Time-Series Forecasting》中,来自牛津大学和 Google Cloud AI 的研究人员提出了一种基于注意力的架构,他们称之为时间融合 TransformerTFT)。为了在不同尺度上学习时间关系,TFT 使用了递归层进行局部处理,并采用可解释的自注意力层来捕捉长期依赖关系。此外,一系列门控层可以抑制不必要的成分。

在各种真实世界的数据集上,它们展示了其架构在广泛基准测试中的显著性能提升。除此之外,它们还在多个方面超过了亚马逊的 DeepAR,性能提升幅度在 36%到 69%之间。

Informer

Transformer 的问题在于二次时间复杂度和内存使用,以及编码器-解码器架构的局限性。这使得对长时间段(例如,510 个小时的电力消耗数据)进行预测变得复杂。为了解决这些问题,来自北航大学、加州大学伯克利分校、罗格斯大学和 SEDD 公司的研究人员设计了一种高效的基于 Transformer 的长序列时间序列预测模型,命名为 Informer —— “Informer: Beyond Efficient Transformer for Long Sequence Time-Series Forecasting”。该论文在 2021 年的 AAAI 会议上获得了杰出论文奖。

生成式解码器通过在一次前向操作中预测长时间序列序列,而不是逐步预测,从而减轻了编码器-解码器的时间复杂度。他们用一种新的自注意力机制——ProbSparse 自注意力——替代了位置嵌入,它实现了 ,其中 L 是序列的长度,而不是二次时间复杂度和内存使用,,同时在序列对齐上的表现保持相当。

最后,自注意力蒸馏将级联层的输入减半,并有效处理极长的输入序列。这将复杂度从 降低到 ,其中 J 是 Transformer 层的数量。

Informer 架构在这个框架图中进行了说明(来自官方 Informer 仓库):

图 10.10:Informer 架构

该图表显示,Informer 在长期时间序列预测数据集上明显优于现有方法,例如电力变压器温度(ETT)、电力消耗负载(ECL)和天气数据。

在单变量数据集上,除了两个案例外,它们相比所有竞争者表现更好,其中 DeepAR 略微优于其他方法,如下所示(来源:Informer GitHub 仓库):

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_WVOQFx/Screenshot 2021-08-01 at 22.05.06.png

图 10.11:单变量长序列时间序列预测性能

最重要的是,它们击败了包括 ARIMA、prophet、LSTM 和其他基于 Transformer 的架构在内的竞争者。

在多变量基准测试中,它们也超过了包括其他基于 Transformer 的模型和 LSTM 在内的竞争者。

我们现在将把其中的一些内容付诸实践。

Python 实践

让我们建模飞机乘客数据。我们将预测每月的乘客数量。

这个数据集被认为是经典的时间序列之一,由 George E.P. Box 和 Gwilym Jenkins 与《时间序列分析:预测与控制》一书(1976 年)一起发布。我已将该数据集的副本提供在书籍 GitHub 仓库的chapter10文件夹中。你可以从那里下载,或者直接在pd.read_csv()中使用 URL。

我们将首先从一个简单的 FCN 开始,然后应用递归网络,最后应用一种非常新的架构:膨胀因果卷积神经网络。

FCN 是首选。

全连接网络

在这个第一次练习中,我们将使用 TensorFlow 库,可以通过终端快速安装(或者通过 Anaconda Navigator 以类似的方式):

pip install -U tensorflow 

我们将在 Python(或 IPython)终端中执行这些命令,但同样,我们也可以在 Jupyter Notebook(或其他环境)中执行它们。

安装可能需要一段时间——TensorFlow 库的大小约为 200MB,并且附带一些依赖项。

让我们加载数据集。这里假设你已经将它下载到你的计算机上:

import pandas as pd
passengers = pd.read_csv(
  "passengers.csv", parse_dates=["date"]
).set_index("date") 

让我们简单地尝试使用 FCN,也叫做 MLP。

让我们设置一些导入并设置几个全局常量:

import tensorflow as tf
import tensorflow.keras as keras
from tensorflow.keras.layers import Dense, Input, Dropout
DROPOUT_RATIO = 0.2
HIDDEN_NEURONS = 10
callback = tf.keras.callbacks.EarlyStopping(
  monitor='loss', patience=3
) 

我们将使用这些常量来构建我们的模型架构。

Dropout(或称:稀疏化)是一种正则化技术,有助于减少过拟合。Dropout 意味着在训练过程中,一部分连接(在我们这个案例中是 20%)会被随机移除。

提前停止是另一种正则化形式,其中训练会在某些条件下停止。在我们的案例中,我们已设定若损失连续三次没有改进,训练就应该停止。如果模型停止改进,就没有继续训练的意义,尽管我们可能被困在一个局部最小值中,但我们仍然可能逃脱。提前停止的一个大优点是它可以帮助我们快速看到模型是否有效。

我们可以在这个函数中定义我们的模型:

def create_model(passengers):
  input_layer = Input(len(passengers.columns))
  hiden_layer = Dropout(DROPOUT_RATIO)(input_layer)
  hiden_layer = Dense(HIDDEN_NEURONS, activation='relu')(hiden_layer)
  output_layer = Dropout(DROPOUT_RATIO)(hiden_layer)
  output_layer = Dense(1)(output_layer)
  model = keras.models.Model(
    inputs=input_layer, outputs=output_layer
  )
  model.compile(
    loss='mse',
  optimizer=keras.optimizers.Adagrad(),
    metrics=[keras.metrics.RootMeanSquaredError(), keras.metrics.MeanAbsoluteError()])
  return model 

使用 Keras 功能 API,我们定义了一个两层神经网络,其中隐藏层有HIDDEN_NEURONS个神经元,并且使用整流线性单元(ReLU)激活函数。

让我们将数据集划分为训练集和测试集。我们将根据上一个时间段(上个月)的乘客数来预测乘客数:

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
  passengers, passengers.passengers.shift(-1), shuffle=False
) 

我们将基于数据集的前 75%进行学习——这是train_test_split函数中test_size参数的默认值。

现在我们可以训练我们的简单 FCN:

model = create_model(X_train)
model.fit(X_train, y_train, epochs=1000, callbacks=[callback]) 

我们应该在每个 epoch 结束时获得损失和度量的输出:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_zCMSbK/Screenshot 2021-08-06 at 22.50.40.png

图 10.12:模型训练

理想情况下,我们会看到误差(损失)在下降,并且在最后看到较低的误差。我没有包含任何修复随机数生成器的代码(tf.random.set_seed),因此你的输出可能会有所不同。

然后,我们可以像这样获取测试集的预测:

predicted = model.predict(X_test) 

现在,将乘客预测与实际乘客值进行可视化会很好。

我们可以使用这个函数:

import matplotlib.pyplot as plt
def show_result(y_test, predicted):
  plt.figure(figsize=(16, 6))
  plt.plot(y_test.index, predicted, 'o-', label="predicted")
  plt.plot(y_test.index, y_test, '.-', label="actual")
  plt.ylabel("Passengers")
  plt.legend() 

然后让我们可视化我们的预测!

show_result(y_test, predicted) 

这是图表:

fcn_naive.png

图 10.13:预测与实际飞机乘客对比:天真的全连接网络

我们可以看到模型已经学会了部分月度变化。然而,它在系统性低估 - 它已经从训练集 1949-1958 年的基线学到了一些东西,当时乘客数量较少。

让我们把这个变得更加复杂和更好一些。

这第一个模型仅在上一个旅客数上进行了训练。

作为第一步,我们将包括年份和月份作为预测变量。年份可以用来模拟趋势,而月份则与月度变化密切相关 - 这似乎是一个自然的步骤。

这将基于 DateTimeIndex 将月份和年份列添加到 DataFrame 中:

passengers["month"] = passengers.index.month.values
passengers["year"] = passengers.index.year.values 

现在我们可以重新定义我们的模型 - 我们需要增加更多的输入列:

model = create_model(passengers) 

现在我们已经准备好进行另一轮训练:

X_train, X_test, y_train, y_test = train_test_split(
  passengers, passengers.passengers.shift(-1), shuffle=False
)
model.fit(X_train, y_train, epochs=100, callbacks=[callback])
predicted = model.predict(X_test)
show_result(y_test, predicted) 

让我们看看模型预测与测试数据的匹配情况:

fcn_with_year_month.png

图 10.14:预测与实际飞机乘客对比;带年份和月份的全连接网络

请注意,由于参数数量多,并且学习过程中涉及随机性,结果可能在不同运行中有显著差异。这确实是深度学习所涉及的问题之一。

这看起来已经好多了。年份特征帮助我们的模型学习了基线。模型已经学会了一些关于月度变化的东西,但还不足以真正逼近它。

让我们创建一个不那么天真的版本。我们将在这个模型中做一些改动:

  • 我们将添加一个月份特征的嵌入

  • 我们将年份视为线性预测器

  • 我们将前一个月的乘客添加到我们的预测中

  • 最后,我们将根据训练数据集中的标准差来缩放我们的预测。

这是相当复杂的。让我们更慢地过一遍这些。

我们将月份作为从 1 到 12 的值输入到我们的先前模型中。然而,我们可以直觉地猜测 1 月(1)和 12 月(12)可能比 11 月(11)和 12 月更相似。我们知道 12 月和 1 月都有很多旅行者,但 11 月的体积可能较低。我们可以根据数据捕捉这些关系。

这可以在嵌入层中完成。嵌入层是将不同类别映射到实数的映射。此映射将作为网络优化的一部分进行更新。

年份与整体趋势密切相关。每年航空旅客人数都在增加。我们可以用非线性或线性模型来建模这种关系。在这里,我决定只建立年份特征和结果之间的线性关系。

假设上个月的乘客人数与本月乘客人数之间的关系仍然是线性的。

最后,我们可以对预测结果进行缩放,类似于标准变换的逆变换。你应该记得来自第三章《时间序列预处理》中的标准归一化方法,如下所示:

其中 是总体均值, 是总体标准差。

其逆操作如下:

我们的公式如下:

其中 是时间点 t 的航空公司乘客人数, 是基于嵌入的月份和年份的预测结果。

我们假设网络会学习到基准线,但可能无法完美学习到比例——因此我们将提供帮助。

一张插图可能会有所帮助(来自 TensorBoard,TensorFlow 的可视化工具包):

../../Desktop/Screenshot%202021-08-06%20at%2022.08.12.pn

图 10.15:模型架构:带嵌入、缩放和基准线的全连接网络

我们可以看到三个输入,其中一个(月份)经过嵌入层处理,另一个经过(线性)投影处理。它们都聚合(连接)起来并经过一个全连接层,在此层上执行其他数学运算。

我们首先需要导入一些内容:

from tensorflow.keras.layers.experimental preprocessing
from tensorflow.keras.layers import Embedding, Flatten, Concatenate
from tensorflow.keras.metrics import (
  RootMeanSquaredError, MeanAbsoluteError
) 

现在,我们重新定义我们的网络如下:

def create_model(train):
  scale = tf.constant(train.passengers.std())
  cont_layer = Input(shape=1)
  cat_layer = Input(shape=1)
  embedded = Embedding(12, 5)(cat_layer)
  emb_flat = Flatten()(embedded)
  year_input = Input(shape=1)
  year_layer = Dense(1)(year_input)
  hidden_output = Concatenate(-1)([emb_flat, year_layer, cont_layer])
  output_layer = keras.layers.Dense(1)(hidden_output)
  output = output_layer * scale + cont_layer
  model = keras.models.Model(inputs=[
    cont_layer, cat_layer, year_input
  ], outputs=output)
  model.compile(loss='mse', optimizer=keras.optimizers.Adam(),
    metrics=[RootMeanSquaredError(), MeanAbsoluteError()])
  return model 

我们重新初始化我们的模型:

model = create_model(X_train) 

在训练和预测过程中,我们需要像这样分别输入三种类型的数据:

model.fit(
  (X_train["passengers"], X_train["year"], X_train["month"]),
  y_train, epochs=1000,
  callbacks=[callback]
)
predicted = model.predict((X_test["passengers"], X_test["year"], X_test["month"])) 

你可能会注意到,在这种配置下,训练时间明显更长。

这张图展示了我们通过新网络所取得的拟合效果:

fcn_more_sophisticated.png

图 10.16:预测与实际航空乘客人数对比:带嵌入、缩放和基准线的全连接网络

这比之前的网络好得多。我们把进一步改进这个网络的任务留给读者来做。

接下来我们将设置一个 RNN。

循环神经网络

我们在理论部分讨论过,循环神经网络非常擅长建模时间序列中点之间的长期关系。让我们设置一个 RNN。

我们将使用与之前相同的数据集——航空公司乘客的单变量值。在这种情况下,我们的网络需要为每个训练样本提供一系列数据点。在每个训练步骤中,RNN 将在接下来的乘客数量之前,训练基于一系列(乘客)点。

请注意,我们可以使用 TensorFlow(甚至是 statsmodels 的 lagmat())工具函数来完成这个任务(我们将在第十二章《案例研究》中使用它们),但在这个例子中,我们将快速自己编写这个代码。

我们需要按如下方式重新采样乘客数据:

def wrap_data(df, lookback: int):
  dataset = []
  for index in range(lookback, len(df)+1):
    features = {
        f"col_{i}": float(val) for i, val in enumerate(
          df.iloc[index-lookback:index].values
        )
    }
    row = pd.DataFrame.from_dict([features])
    row.index = [df.index[index-1]]
    dataset.append(row)
  return pd.concat(dataset, axis=0) 

这个函数完成了任务。它遍历数据集中的所有点,并提取到该点的序列。新序列中的点数由参数lookback定义。

让我们开始使用它:

LOOKBACK = 10
dataset = wrap_data(passengers, lookback=LOOKBACK)
dataset = dataset.join(passengers.shift(-1)) 

我们使用了一个回溯窗口为 10。我故意选择了一个并非最优的值。我把选择一个更好的值并尝试的任务留给读者。

上面代码中的最后一行将目标(前瞻 1)与序列连接在一起。

我们准备好定义我们的网络了,但先处理一下导入模块:

import tensorflow.keras as keras
from tensorflow.keras.layers import Input, Bidirectional, LSTM, Dense
import tensorflow as tf 

网络由这个函数定义:

def create_model(passengers):
  input_layer = Input(shape=(LOOKBACK, 1))
  recurrent = Bidirectional(LSTM(20, activation="tanh"))(input_layer)
  output_layer = Dense(1)(recurrent)
  model = keras.models.Model(inputs=input_layer, outputs=output_layer)
  model.compile(loss='mse', optimizer=keras.optimizers.Adagrad(),
    metrics=[keras.metrics.RootMeanSquaredError(), keras.metrics.MeanAbsoluteError()])
  return model 

这是一个双向 LSTM 网络。最后一层的输出经过线性投影,作为我们的输出。我将 LSTM 的激活函数设置为tanh,以便你在 GPU 环境中运行时,可以利用 NVIDIA 的 GPU 加速库 cuDNN。我们提取了与之前练习中相同的指标。

以下是一些你应该在上一节中已经熟悉的预备知识:

from sklearn.model_selection import train_test_split
callback = tf.keras.callbacks.EarlyStopping(monitor='loss', patience=3)
model = create_model(passengers)
X_train, X_test, y_train, y_test = train_test_split(
    dataset.drop(columns="passengers"),
    dataset["passengers"],
    shuffle=False
) 

让我们开始训练:

model.fit(X_train, y_train, epochs=1000, callbacks=[callback]) 

结果看起来已经相当不错——尽管我们做了一些次优的选择:

rnn_passengers.png

图 10.17:乘客预测的递归神经网络

鉴于这个设置非常简单,这看起来已经非常有前景。

现在让我们试试因果卷积神经网络!

膨胀因果卷积神经网络

这个例子基于 Krist Papadopoulos 的 SeriesNet 实现,参考了 Anastasia Borovykh 等人的论文《使用卷积神经网络进行条件时间序列预测》。

我们将一起实现这个模型,并将其应用于两个数据集,看看它的表现如何。我在这个例子中不会讨论如何调整数据和架构。

首先是导入模块:

import numpy as np
import pandas as pd
from keras.layers import Conv1D, Input, Add, Activation, Dropout
from keras.models import Sequential, Model
from keras.layers.advanced_activations import LeakyReLU, ELU
from keras import optimizers
import tensorflow as tf 

也许令人惊讶的是,在 TensorFlow 中实现因果卷积是如此简单。Conv1D 带有一个参数padding,可以设置为'causal'。这会根据因果特性将层的输入用零填充,其中时间 t 的输出仅依赖于之前的时间步,<t。请参考本章 ConvNets 部分的讨论。

这意味着我们可以预测帧中早期时间步的值。

这个网络的主要思想是一个带有因果卷积的残差块。这个代码段构建了相应的网络架构:

def DC_CNN_Block(nb_filter, filter_length, dilation):
    def f(input_):
        residual =    input_
        layer_out =   Conv1D(
            filters=nb_filter, kernel_size=filter_length, 
            dilation_rate=dilation, 
            activation='linear', padding='causal', use_bias=False
        )(input_)                    
        layer_out =   Activation('selu')(layer_out)        
        skip_out =    Conv1D(1, 1, activation='linear', use_bias=False)(layer_out)        
        network_in =  Conv1D(1, 1, activation='linear', use_bias=False)(layer_out)                      
        network_out = Add()([residual, network_in])        
        return network_out, skip_out    
    return f 

我简化了这个部分,使它更容易阅读。

网络本身仅仅堆叠这些层,作为一个 SkipNet,后续使用卷积:

def DC_CNN_Model(length):
    input = Input(shape=(length,1))
    l1a, l1b = DC_CNN_Block(32, 2, 1)(input)    
    l2a, l2b = DC_CNN_Block(32, 2, 2)(l1a) 
    l3a, l3b = DC_CNN_Block(32, 2, 4)(l2a)
    l4a, l4b = DC_CNN_Block(32, 2, 8)(l3a)
    l5a, l5b = DC_CNN_Block(32, 2, 16)(l4a)
    l6a, l6b = DC_CNN_Block(32, 2, 32)(l5a)
    l6b = Dropout(0.8)(l6b)
    l7a, l7b = DC_CNN_Block(32, 2, 64)(l6a)
    l7b = Dropout(0.8)(l7b)
    l8 =   Add()([l1b, l2b, l3b, l4b, l5b, l6b, l7b])
    l9 =   Activation('relu')(l8)   
    l21 =  Conv1D(1, 1, activation='linear', use_bias=False)(l9)
    model = Model(inputs=input, outputs=l21)
    model.compile(loss='mae', optimizer=optimizers.Adam(), metrics=['mse'])
    return model 

这是针对单变量时间序列的。对于多变量时间序列,需要进行一些修改,这部分我们在这里不讨论。

让我们再次预测乘客数量。我们将像之前的练习部分那样加载 DataFrame:

passengers = pd.read_csv(
  "passengers.csv", parse_dates=["date "]
).set_index("date") 

我们将再次将数据拆分为测试集和训练集:

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
    passengers.passengers, passengers.passengers.shift(-1), shuffle=False
) 

我们将使用这个函数来训练模型:

def fit_model(timeseries):
    length = len(timeseries)-1
    model = DC_CNN_Model(length)
    model.summary()
    X = timeseries[:-1].reshape(1,length, 1)
    y = timeseries[1:].reshape(1,length, 1)
    model.fit(X, y, epochs=3000, callbacks=[callback])
    return model 

这个函数将为我们做预测:

def forecast(model, timeseries, horizon: int):
    length = len(timeseries)-1
    pred_array = np.zeros(horizon).reshape(1, horizon, 1)
    X_test_initial = timeseries[1:].reshape(1,length,1)
    pred_array[: ,0, :] = model.predict(X_test_initial)[:, -1:, :]
    for i in range(horizon-1):
        pred_array[:, i+1:, :] = model.predict(
            np.append(
                X_test_initial[:, i+1:, :], 
                pred_array[:, :i+1, :]
            ).reshape(1, length, 1))[:, -1:, :]
    return pred_array.flatten() 

这个预测是通过基于前面的预测来预测紧接着的下一个未来值而产生的。参数 horizon 是预测的时间跨度。

为了方便起见,我们将把这一切整理成一个单一函数:

def evaluate_timeseries(series, horizon: int):
    model = fit_model(series)
    pred_array = forecast(model, series, horizon)
    return pred_array, model 

现在我们已经准备好进行训练了。我们将按照如下方式运行一切:

HORIZON = len(y_test)
predictions, model = evaluate_timeseries(
    X_train.values.reshape(-1, 1), horizon= HORIZON
) 

这个模型非常深,但由于卷积的原因,在参数数量上并不算很大。我们会看到它有 865 个可训练的参数。

然而,模型的拟合效果并不好,无论是 MSE 还是图表看起来都不太令人印象深刻:

convnet_passengers.png

图 10.18:ConvNet 预测乘客数量

这个图表可以通过运行show_result(y_test[:HORIZON], predictions[:HORIZON], "Passengers")生成。

这突出了每个模型都有其优缺点的事实,并且如果不根据我们的数据集调整模型以及进行仔细的预处理,我们将无法获得良好的性能。让读者尝试调整这个模型作为练习。

总结

在这一章中,我介绍了许多与时间序列相关的深度学习概念,我们讨论了许多架构和算法,如自编码器、InceptionTime、DeepAR、N-BEATS、ConvNets 和一些 transformer 架构。深度学习算法确实接近时间序列领域的最新技术,它是一个令人兴奋的研究和应用领域。

在实践部分,我实现了一个全连接前馈网络,然后是一个 RNN,最后使用一个因果 ConvNet 进行了实验。

第十二章多元预测中,我们将进行更多深度学习的探讨,包括一个 Transformer 模型和一个 LSTM。

第十一章:强化学习在时间序列中的应用

强化学习是一种广泛成功的控制问题和函数优化范式,不需要标注数据。它是一个强大的框架,支持经验驱动的自主学习,其中智能体通过采取行动与环境直接互动,并通过试错来提高效率。自从总部位于伦敦、由谷歌拥有的 DeepMind 在复杂游戏中取得突破以来,强化学习尤其受到关注。

在这一章节中,我们将讨论强化学习(RL)在时间序列中的一种分类,特别是经济学中的应用,并将探讨基于 RL 的时间序列模型的准确性和适用性。

我们将从与时间序列相关的强化学习核心概念和算法开始,随后讨论当前深度强化学习模型中的开放问题和挑战。

我将涵盖以下主题:

  • 强化学习简介

  • 强化学习在时间序列中的应用

  • 盗贼算法

  • 深度 Q 学习

  • Python 实践

让我们从强化学习的简介和主要概念开始。

强化学习简介

强化学习是机器学习的主要范式之一,与监督学习和无监督学习方法并列。一个主要的区别在于,监督学习或无监督学习是被动的,应对变化,而强化学习是主动的,通过改变环境来寻找新数据。实际上,从机器学习的角度来看,强化学习算法可以被视为在寻找好数据和在这些数据上进行监督学习之间交替进行。

基于强化学习的计算机程序一直在突破各类障碍。在人工智能的一个重要时刻,2016 年 3 月,DeepMind 的 AlphaGo 击败了职业围棋选手李世石。此前,围棋被认为是人类创造力和智能的象征,复杂到机器无法学习。

有人认为,它正将我们带向人工通用智能AGI)的目标。例如,在他们的论文《奖励就足够》(2021 年)中,David Silver、Satinder Singh、Doina Precup 和 Richard S. Sutton 认为,基于奖励的学习足以获得知识、学习、感知、社交、理解和生成语言、概括以及模仿。他们更加强调地指出,强化学习智能体可能是解决 AGI 的关键。

人工通用智能AGI)是指智能体能够理解或学习任何需要智能的智力任务的假设能力。那么,智能到底是什么呢?通常,它被定义为人类能够做到或认为困难的任何事物。根据图灵奖得主、计算机科学家 John McCarthy(“什么是人工智能?”1998 年)的定义,“智能是实现世界目标能力的计算部分。

在强化学习中,代理通过行动与环境进行互动,并通过奖励的形式获得反馈。与监督学习中的情况相反,强化学习中没有标注数据可用,而是基于累积奖励的期望探索和利用环境。这个行动与奖励的反馈循环在下图中有所说明:

图 11.1:强化学习中的反馈循环

强化学习关注的是奖励最大化的目标。通过与环境的互动,代理获得反馈并学习采取更好的行动。通过优化累积奖励,代理发展出目标导向的行为。

强化学习RL)是一种方法,代理通过采取行动与环境直接互动。代理通过试错学习来最大化奖励。

如果你读过第八章时间序列的在线学习,你可能会对强化学习和在线学习的区别感到困惑,值得将这两种方法进行比较。一些最著名的强化学习算法,如 Q 学习和时序差分(TD)学习,仅举几个例子,实际上是在线算法,它们通过更新价值函数的方式来进行学习。

然而,强化学习不专注于预测,而是专注于与环境的互动。在在线学习中,信息是持续处理的,问题明确地定义为什么是正确的,什么是错误的。而在强化学习中,目标是通过与环境互动优化延迟的奖励。这是两种方法之间的主要区别,尽管每种技术的支持者可能会主张自己有很多细节内容。我们将在本章稍后讨论其中的一些内容,例如探索与利用以及经验回放。

一个强化学习问题由三个主要组成部分定义:环境ε、代理A和累积目标。代理是一个决策实体,可以观察环境的当前状态并采取行动。通过执行一个行动 ,代理从一个状态转移到另一个状态,。在特定状态下执行一个行动会为代理提供奖励,这是一个数值评分。奖励是衡量朝目标前进的即时指标。

环境处于某种状态,这个状态依赖于当前状态和采取的行动的某种组合,尽管其中一些变化可能是随机的。代理的目标是最大化累积奖励函数。这个累积奖励目标可以是多个步骤中的奖励总和、折扣总和,或者是随时间变化的平均奖励。

更正式地说,在强化学习的背景下,智能体是一个系统(或程序),它在时间 t 接收来自环境的观察 O[t],并根据其经验历史输出一个动作

与此同时,环境是另一个系统。它在时间 t 接收一个动作 A[t],并根据动作历史、过去的状态以及随机过程 来改变其状态。环境状态在某种程度上对智能体是可访问的,为了简化,我们可以表述为:

最后,奖励是一个标量观察值,在每个时间步 t 由环境发出,提供给智能体有关它执行得如何的即时反馈。

强化学习智能体的核心是一个模型,它估计环境状态的价值或建议在环境中采取的下一步行动。这是强化学习的两大主要类别:在基于价值的学习中,模型通过价值函数(模型)来逼近动作的结果或环境状态的价值,动作选择则简化为采取具有最佳期望结果的动作。在基于策略的学习中,我们专注于通过从环境状态预测动作这一更直接的目标来选择动作。

强化学习还有另一个难题:探索与利用的困境。你可以决定继续做你知道最有效的事情(利用),或者尝试新的途径(探索)。尝试新事物可能会在短期内导致更差的结果,但可能会教会你一些重要的经验,以后可以加以借鉴。

平衡两者的一种简单方法是ε-贪心算法。这是一种通过随机选择探索与利用之间的平衡的简单方法:要么我们遵循模型的建议,要么我们不遵循。Epsilon 是我们做出一个不被模型认为是最佳的动作的概率参数;epsilon 值越高,模型的动作越随机。

深度强化学习DRL)技术是强化学习方法的一个子集,其中模型是深度神经网络(或在更宽松的意义上是多层感知器)。

在下一节中,我们将探讨如何将强化学习应用于时间序列!

时间序列的强化学习

强化学习(RL)可以并且已经应用于时间序列,但问题必须以特定的方式框定。对于强化学习,我们需要在预测和系统的持续行为(动作)之间有显著的反馈。

为了将强化学习应用于时间序列预测或预测,预测必须基于某种动作,因此状态的演化依赖于当前状态和智能体的动作(以及随机性)。假设,奖励可以是关于预测准确性的性能指标。然而,良好或不良预测的后果不会影响原始环境。实质上,这相当于一个监督学习问题。

更有意义的是,如果我们想将我们的情况框架化为一个强化学习(RL)问题,那么系统的状态应该受到智能体决策的影响。例如,在与股市互动的情况下,我们会根据对市场波动的预测来买入或卖出,并将我们影响的因素(如我们的投资组合和资金)纳入状态,或者(只有当我们是市场制造者时)考虑我们对股票波动的影响。

总结来说,强化学习非常适合处理随时间变化的过程,尽管强化学习处理的是那些可以控制或影响的过程。时间序列的一个核心应用是在工业过程和控制中——事实上,这一点已经在 Box 和 Jenkins 的经典著作《时间序列分析:预测与控制》中提到过。

我们可以想到很多强化学习的应用。股市交易是商业增长的主要驱动力,而其中的不确定性和风险使其成为强化学习的一个应用场景。在定价领域,例如保险或零售,强化学习可以帮助探索客户价值主张的空间,从而实现高销售额,并优化利润率。最后,拍卖机制,例如在线广告竞价,是另一个领域。在拍卖中,强化学习智能体必须在其他参与者的存在下开发响应策略。

让我们详细探讨一下几个算法——首先是赌博机问题。

赌博机算法

多臂赌博机MAB)是一个经典的强化学习问题,其中玩家面临一个老丨虎丨机(赌博机),它有k个拉杆(臂),每个拉杆都有不同的奖励分布。智能体的目标是在每次试验中最大化其累计奖励。由于多臂赌博机是一个简单但强大的框架,用于在不确定性下做出决策的算法,因此大量的研究文章致力于此问题。

赌博机学习指的是旨在优化一个未知的静态目标函数的算法。智能体从一组动作中选择一个动作!。环境在时间t时揭示所选动作的奖励!。随着信息在多轮中积累,智能体可以建立一个良好的价值(或奖励)分布表示!

因此,一个好的策略可能会收敛,使得选择的臂变得最优。根据一种策略,UCB1(由 Peter Auer、Nicolò Cesa-Bianchi 和 Paul Fischer 在 2002 年发布的《有限时间分析多臂强盗问题》中提出),在给定每个行动的预期值的情况下,选择能够最大化该标准的行动:

第二项指的是基于我们积累的信息,奖励值的上置信界。这里,t表示到目前为止的迭代次数,即时间步长,而表示到目前为止执行行动a的次数。这意味着方程中的分子随着时间以对数方式增加,而分母在每次我们从行动中获取奖励信息时都会增加。

当可用的奖励是二元的(赢或输,是或否,收费或不收费)时,这可以用贝塔分布来描述。贝塔分布有两个参数,分别是,用于表示赢和输。均值是

汤普森采样中,我们从每个行动(臂)的贝塔分布中采样,并选择具有最高预期回报的行动。随着尝试次数的增加,贝塔分布会变窄,因此那些尝试较少的行动具有较宽的分布。因此,贝塔采样模型估计了均值奖励和估计的置信度。在狄利克雷采样中,我们不从贝塔分布中采样,而是从狄利克雷分布中采样(也称为多元贝塔分布)。

上下文强盗将环境信息融入其中,用于更新奖励预期。如果你考虑广告,这种上下文信息可能是广告是否与旅行相关。上下文强盗的优点在于,代理可以对环境编码更丰富的信息。

在上下文强盗中,代理选择一个臂,奖励被揭示,代理的奖励预期被更新,但带有上下文特征:,其中x是编码环境的一组特征。在许多实现中,上下文通常限制为离散值,但至少在理论上,它们可以是分类的或数值的。价值函数可以是任何机器学习算法,如神经网络(NeuralBandit)或随机森林(BanditForest)。

强盗算法在多个领域中都有应用,包括信息检索模型,如推荐系统和排名系统,这些系统被用于搜索引擎或消费者网站中。概率排名原则(PRP;来自 S.E. Robertson 的文章“信息检索中的概率排名原则”,1977 年)为概率模型提供了理论基础,而这些模型已经主导了信息检索领域。PRP 指出,文章应该按照相关性概率的递减顺序进行排序。这也是我们将在练习部分中讲解的内容。

现在让我们深入探讨 Q 学习和深度 Q 学习。

深度 Q 学习

Q 学习,由 Chris Watkins 于 1989 年提出,是一种学习在特定状态下动作价值的算法。Q 学习围绕表示在给定状态下执行某个动作的期望回报展开。

状态-动作组合的期望回报由 Q 函数近似:

Q被初始化为一个固定值,通常是随机的。在每个时间步* t *,智能体选择一个动作,并看到环境的新状态,作为结果并获得回报。

价值函数Q可以根据贝尔曼方程更新,作为旧价值和新信息的加权平均值:

权重由表示,学习率——学习率越高,Q 函数越适应。折扣因子通过其即时性对回报进行加权——折扣因子越高,智能体越不耐烦(近视)。

表示当前回报。是通过学习率加权后的获得的回报,而是从状态中获得的加权最大回报。

最后一部分可以递归地分解成更简单的子问题,如下所示:

在最简单的情况下,Q可以是一个查找表,称为 Q 表。

2014 年,Google DeepMind 申请了一种名为深度 Q 学习的算法的专利。该算法首次在《自然》杂志上的文章“通过深度强化学习实现人类水平控制”中提出,并应用于 Atari 2600 游戏。

在深度 Q 学习中,神经网络作为非线性函数逼近器被用于 Q 函数。他们使用卷积神经网络从像素值中学习期望回报。他们引入了一种名为经验回放的技术,通过随机抽取先前动作的样本来更新 Q。这是为了减少 Q 更新的学习不稳定性。

Q 学习的大致伪代码如下所示:

import numpy as np
memory = []
for episode in range(N):
  for ts in range(T):
    if eps np.random.random() > epsilon:
      a = A[np.argmax([Q(a) for a in A])]
    else:
      a = np.random.choice(A)
    r, s_next = env.execute(a)
    memory.append((s, a, r, s_next))
    learn(np.random.choice(memory, L) 

该库实现了一个 epsilon-贪心策略,其中根据概率epsilon做出一个随机(探索性)选择。还假设了其他一些变量。环境句柄env允许我们执行一个动作。我们有一个学习函数,通过对 Q 函数应用梯度下降法,根据贝尔曼方程学习更好的值。参数L是用于学习的先前值的数量。

内存重放部分显然被简化了。实际上,我们会有一个内存的最大大小,一旦内存达到最大容量,我们就会用新的状态、动作和奖励替换旧的关联。

现在我们将实际操作一下。

Python 实践

让我们开始建模。我们将从给用户提供一些基于 MAB 的推荐开始。

推荐

对于这个例子,我们将收集用户的笑话偏好,并使用这些数据模拟网站上推荐笑话的反馈。我们将使用这些反馈来调整我们的推荐。我们的目标是选择 10 个最好的笑话,展示给访问我们网站的人。这些推荐将由 10 个 MAB 生成,每个 MAB 有与笑话数量相同的臂。

这是从 GitHub 上 Kenza-AI 的mab-ranking库中的一个示例改编的。

这是一个方便的库,提供了不同强盗算法的实现。我在该库的分支中简化了库的安装,因此我们将在这里使用我的分支:

pip install git+https://github.com/benman1/mab-ranking 

完成后,我们可以直接开始!

我们将从 S3 下载jester数据集,其中包含笑话偏好。下载地址如下:

URL = 'https://raw.githubusercontent.com/PacktPublishing/Machine-Learning-for-Time-Series-with-Python/main/chapter11/jesterfinal151cols.csv' 

我们将使用 pandas 下载这些数据:

import pandas as pd
jester_data = pd.read_csv(URL, header=None) 

我们将做一些外观上的调整。行表示用户,列表示笑话。我们可以让这一点更加清晰:

jester_data.index.name = "users" 

选择的编码有点奇怪,所以我们也将修复这一点:

for col in jester_data.columns:
    jester_data[col] = jester_data[col].apply(lambda x: 0.0 if x>=99 or x<7.0 else 1.0) 

所以,要么人们选择了笑话,要么他们没有选择任何笑话。我们将去除那些没有选择任何笑话的人:

jester_data = jester_data[jester_data.sum(axis=1) > 0] 

我们的数据集现在看起来是这样的:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_oTdq4A/Screenshot 2021-09-04 at 16.41.24.png

图 11.2:Jester 数据集

我们将按以下方式设置我们的强盗:

from mab_ranking.bandits.rank_bandits import IndependentBandits
from mab_ranking.bandits.bandits import DirichletThompsonSampling
independent_bandits = IndependentBandits(
    num_arms=jester_data.shape[1],
    num_ranks=10,
    bandit_class=DirichletThompsonSampling
) 

我们从 Beta 分布中使用汤普森采样选择独立的强盗。我们推荐最好的 10 个笑话。

然后我们可以开始模拟。我们假设的网站有很多访问者,我们将根据独立强盗选择的笑话展示,并获得反馈:

from tqdm import trange
num_steps = 7000
hit_rates = []
for _ in trange(1, num_steps + 1):
    selected_items = set(independent_bandits.choose())
    # Pick a users choices at random
    random_user = jester_data.sample().iloc[0, :]
    ground_truth = set(random_user[random_user == 1].index)
    hit_rate = len(ground_truth.intersection(selected_items)) / len(ground_truth)
    feedback_list = [1.0 if item in ground_truth else 0.0 for item in selected_items]
    independent_bandits.update(selected_items, feedback_list)
    hit_rates.append(hit_rate) 

我们正在模拟 7,000 次迭代(访问)。在每次访问中,我们将根据更新后的奖励预期改变我们的选择。

我们可以按以下方式绘制命中率,以及用户选择的笑话:

import matplotlib.pyplot as plt
stats = pd.Series(hit_rates)
plt.figure(figsize=(12, 6))
plt.plot(stats.index, stats.rolling(200).mean(), "--")
plt.xlabel('Iteration')
plt.ylabel('Hit rate') 

我引入了一个滚动平均(基于 200 次迭代),以获得更平滑的图表:

图 11.3:随着时间推移的命中率(Dirichlet 采样)

mab-ranking 库支持上下文信息,因此我们可以尝试提供额外信息。想象这些信息为不同的用户组(队列)。我们可以考虑使用不同搜索或过滤功能的用户,例如我们假设的网站上的 "最新笑话" 或 "最流行"。或者它们可能来自不同的地区。或者它可以是对应用户访问我们网站的时间的时间戳类别。

让我们提供分类用户组信息,即上下文。我们将根据其偏好对用户进行聚类,并将使用这些群集作为上下文:

from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler().fit(jester_data)
kmeans = KMeans(n_clusters=5, random_state=0).fit(scaler.transform(jester_data))
contexts = pd.Series(kmeans.labels_, index=jester_data.index) 

这创造了 5 个用户组。

我们需要重置我们的强盗:

independent_bandits = IndependentBandits(
    num_arms=jester_data.shape[1],
    num_ranks=10,
    bandit_class=DirichletThompsonSampling
) 

然后,我们可以重新进行我们的模拟。现在,我们将提供用户上下文:

hit_rates = []
for _ in trange(1, num_steps + 1):
    # Pick a users choices at random
    random_user = jester_data.sample().iloc[0, :]
    context = {"previous_action": contexts.loc[random_user.name]}
    selected_items = set(independent_bandits.choose(
        context=context
    ))
    ground_truth = set(random_user[random_user == 1].index)
    hit_rate = len(ground_truth.intersection(selected_items)) / len(ground_truth)
    feedback_list = [1.0 if item in ground_truth else 0.0 for item in selected_items]
    independent_bandits.update(selected_items, feedback_list, context=context)
    hit_rates.append(hit_rate) 

我们可以再次可视化随时间变化的命中率:

图 11.4:随时间变化的命中率(具有上下文的狄利克雷抽样)

我们可以看到命中率比之前略高。

此模型忽略了在我们假设的网站上推荐笑话的顺序。还有其他模型化排名的强盗实现。

我将留给读者更多地进行探索。一个有趣的练习是创建奖励期望的概率模型。

在接下来的部分中,我们将尝试深度 Q 学习交易机器人。这是一个更复杂的模型,需要更多的关注。我们将把它应用到加密货币交易中。

使用 DQN 进行交易

这是基于 TensorTrade 库的教程,我们将在此示例中使用它。TensorTrade 是一个通过强化学习构建、训练、评估和部署稳健交易算法的框架。

TensorTrade 依赖于诸如 OpenAI Gym、Keras 和 TensorFlow 等现有工具,以便快速实验算法交易策略。我们将像往常一样通过 pip 安装它。我们会确保从 GitHub 安装最新版本:

pip install git+https://github.com/tensortrade-org/tensortrade.git 

我们还可以安装 ta 库,它可以提供对交易有用的额外信号,但在这里我们将其省略。

让我们先完成几个导入:

import pandas as pd
import tensortrade.env.default as default
from tensortrade.data.cdd import CryptoDataDownload
from tensortrade.feed.core import Stream, DataFeed
from tensortrade.oms.exchanges import Exchange
from tensortrade.oms.services.execution.simulated import execute_order
from tensortrade.oms.instruments import USD, BTC, ETH
from tensortrade.oms.wallets import Wallet, Portfolio
from tensortrade.agents import DQNAgent
%matplotlib inline 

这些导入涉及(模拟)交易、投资组合和环境的实用程序。此外,还有用于数据加载和将其提供给模拟的实用程序,用于货币转换的常量,最后还有一个深度 Q 代理,它包含一个深度 Q 网络(DQN)。

请注意,matplotlib 的魔术命令 (%matplotlib inline) 是为了让 Plotly 图表按预期显示而需要的。

作为第一步,让我们加载一个历史加密货币价格数据集:

cdd = CryptoDataDownload()
data = cdd.fetch("Bitstamp", "USD", "BTC", "1h")
data.head() 

这个数据集包含以美元计价的比特币每小时价格。它看起来像这样:

![](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ml-ts-py/img/Screenshot 2021-09-05 at 10.57.42.png)

图 11.5:加密货币数据集

我们将添加一个相对强弱指标信号,这是一个用于金融市场的技术指标。它通过比较最近交易周期的收盘价来衡量市场的强弱。我们还将添加一个移动平均收敛/发散MACD)指标,它旨在揭示趋势的强度、方向、动量和持续时间的变化。

这两个定义如下:

def rsi(price: Stream[float], period: float) -> Stream[float]:
    r = price.diff()
    upside = r.clamp_min(0).abs()
    downside = r.clamp_max(0).abs()
    rs = upside.ewm(alpha=1 / period).mean() / downside.ewm(alpha=1 / period).mean()
    return 100*(1 - (1 + rs) ** -1)
def macd(price: Stream[float], fast: float, slow: float, signal: float) -> Stream[float]:
    fm = price.ewm(span=fast, adjust=False).mean()
    sm = price.ewm(span=slow, adjust=False).mean()
    md = fm - sm
    signal = md - md.ewm(span=signal, adjust=False).mean()
    return signal 

或者,我们也可以使用 ta 库中的交易信号。

我们现在将设置进入决策过程的数据流:

features = []
for c in data.columns[1:]:
    s = Stream.source(list(data[c]), dtype="float").rename(data[c].name)
    features += [s]
cp = Stream.select(features, lambda s: s.name == "close") 

我们选择了收盘价作为特征。

现在,我们将添加我们的指标作为额外的特征:

features = [
    cp.log().diff().rename("lr"),
    rsi(cp, period=20).rename("rsi"),
    macd(cp, fast=10, slow=50, signal=5).rename("macd")
]
feed = DataFeed(features)
feed.compile() 

除了 RSI 和 MACD,我们还添加了一个趋势指标(LR)。

我们可以查看数据流中的前五行:

for i in range(5):
    print(feed.next()) 

这是我们交易信号特征的展示:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_y5gWwn/Screenshot 2021-09-05 at 11.11.53.png

图 11.6:交易数据流

让我们设置经纪人:

bitstamp = Exchange("bitstamp", service=execute_order)(
    Stream.source(list(data["close"]), dtype="float").rename("USD-BTC")
) 

交易所是让我们执行订单的接口。交易所需要一个名称、一个执行服务和价格数据流。目前,TensorTrade 支持使用模拟或随机数据的模拟执行服务。

现在我们需要一个投资组合:

portfolio = Portfolio(USD, [
    Wallet(bitstamp, 10000 * USD),
    Wallet(bitstamp, 10 * BTC)
]) 

投资组合可以是交易所支持的任何组合的交易所和工具。

TensorTrade 包含许多监控工具,称为渲染器,可以附加到环境中。例如,它们可以绘制图表(PlotlyTradingChart)或记录到文件(FileLogger)。这是我们的设置:

renderer_feed = DataFeed([
    Stream.source(list(data["date"])).rename("date"),
    Stream.source(list(data["open"]), dtype="float").rename("open"),
    Stream.source(list(data["high"]), dtype="float").rename("high"),
    Stream.source(list(data["low"]), dtype="float").rename("low"),
    Stream.source(list(data["close"]), dtype="float").rename("close"), 
    Stream.source(list(data["volume"]), dtype="float").rename("volume") 
]) 

最后,这就是交易环境,它是 OpenAI Gym 的一个实例(OpenAI Gym 提供了各种各样的模拟环境):

env = default.create(
    portfolio=portfolio,
    action_scheme="managed-risk",
    reward_scheme="risk-adjusted",
    feed=feed,
    renderer_feed=renderer_feed,
    renderer=default.renderers.PlotlyTradingChart(),
    window_size=20
) 

如果你之前做过强化学习,你可能会熟悉 Gym 环境。

让我们查看 Gym 数据:

env.observer.feed.next() 

这是输出内容:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_GBkklu/Screenshot 2021-09-05 at 11.40.11.png

图 11.7:交易机器人环境数据流

这是交易机器人将依赖于执行交易决策的依据。

现在我们可以设置并训练我们的 DQN 交易代理:

agent = DQNAgent(env)
agent.train(n_steps=200, n_episodes=2, save_path="agents/") 

这里也许是个好机会来解释一下 epoch 和 episode 之间的区别。读者可能会熟悉 epoch,它是对所有训练样本的单次遍历,而 episode 是强化学习中的专有概念。episode 是由一系列状态、动作和奖励组成的序列,以终止状态结束。

我们从渲染器中获得了大量的绘图输出。这是我获得的第一个输出(你的可能会有所不同):

../trading_renderer.png

图 11.8:PlotlyPlotRenderer – Episode 2/2 Step 51/200

这个图展示了我们交易机器人市场操作的概览。第一个子图显示了价格的涨跌。接下来的第二个子图展示了投资组合中的股票交易量,而最底部的子图则显示了投资组合的净资产。

如果你想查看随时间变化的净资产(不仅仅是上面提到的第一张快照),你也可以绘制这个图:

performance["net_worth"].plot() 

这是随时间变化的投资组合净资产:

networth.png

图 11.9:投资组合随时间变化的价值

看起来我们的交易机器人可能需要更多的训练才能在真实市场中投入使用。我亏损了,所以很庆幸没有投入真实资金。

就这些了。让我们总结一下。

总结

在线学习,我们在第八章中讨论过,时间序列的在线学习处理的是传统的监督学习,而强化学习则试图应对环境。在本章中,我介绍了与时间序列相关的强化学习概念,并讨论了许多算法,如深度 Q 学习和MABs

强化学习算法在某些场景下非常有用,比如推荐系统、交易,或者更一般地说,控制场景。在实践部分,我们实现了一个使用 MAB 的推荐系统和一个带有 DQN 的交易机器人。

在下一章,我们将查看带有时间序列的案例研究。除此之外,我们还会看一下多元预测的能源需求。

第十二章:多变量预测

如果你一直在关注本书,你可能已经注意到,过去十年时间序列领域取得了很多进展。许多扩展和新技术出现,用于将机器学习应用于时间序列。在每一章中,我们涵盖了许多关于预测、异常和漂移检测、回归和分类以及包括传统方法、梯度提升机器学习和其他方法、强化学习、在线学习、深度学习和概率模型的不同问题。

在本章中,我们将更深入地实践一些内容。到目前为止,我们主要涵盖了单变量时间序列,但在本章中,我们将应用预测到能源需求的一个案例。鉴于世界各地持续存在能源或供应危机,这是一个非常及时的主题。我们将使用多变量时间序列,并使用不同的方法进行多步预测。

我们将涵盖以下主题:

  • 多变量时间序列预测

  • 时间序列的下一步是什么?

第二部分将展望时间序列应用和研究的未来。但首先让我们讨论多变量序列。然后我们将应用几个模型来进行能源需求预测。

多变量时间序列预测

时间序列预测是学术界的一个活跃研究课题。预测长期趋势不仅是一项有趣的挑战,而且对于战略规划和运营研究在实际应用中具有重要的影响,例如 IT 运营管理、制造业和网络安全。

多变量时间序列具有多个因变量。这意味着每个因变量不仅依赖于其自身的过去值,还可能依赖于其他变量的过去值。这引入了复杂性,如共线性,其中因变量不是独立的,而是相关的。共线性违反了许多线性模型的假设,因此更有吸引力的是使用能够捕获特征交互作用的模型。

此图显示了一个多变量时间序列的示例,即各国 COVID-19 死亡情况(来自维基百科关于 COVID-19 大流行的英文文章):

图 12.1:COVID-19 每 10 万人口死亡人数作为多变量时间序列的示例。

各国之间的 COVID 死亡人数存在相关性,尽管它们可能会有所偏移,或者可能属于不同的群体。

我们在第五章时间序列机器学习简介中提到过 Makridakis 竞赛。作为主办方的 Spyros Makridakis 是尼科西亚大学的教授,专门研究时间序列预测。这些竞赛作为最佳算法的基准,研究人员和实践者相互竞争,争夺现金奖励。这项竞赛的希望是能够激发并促进机器学习的发展,并为未来的工作开辟方向。

M4 竞赛使用了来自 ForeDeCk 数据库的 100,000 个多变量时间序列,涵盖了不同的应用领域和时间尺度,结果于 2020 年发布。49 个参赛者或团队提交了点预测,测试了主要的机器学习和统计方法的准确性。

M4 的组织者 Spyros Makridakis、Evangelos Spiliotis 和 Vassilios Assimakopoulos 在("M4 竞赛:100,000 个时间序列和 61 种预测方法",2020 年)中观察到,主要由成熟的统计方法组合(混合或集成)往往比纯统计方法或纯机器学习方法更为准确,后者的表现相对较差,通常位于参赛队伍的后半部分。尽管机器学习方法在解决预测问题中的应用日益增加,但统计方法依然强大,尤其在处理低粒度数据时。不过需要注意的是,数据集未包含外生变量或时间戳。深度学习和其他机器学习方法可能在处理高维数据时表现更好,特别是在共线性存在的情况下,因此这些额外的信息可能会提升这些模型的表现。

然而,来自 Uber Technologies 的 Slawek Smyl 以第一名的成绩获得了 9000 欧元,他的模型结合了递归神经网络和统计时间序列模型(Holt-Winters 指数平滑)。这两个组件是通过梯度下降法同时拟合的。作为一位经验丰富的时间序列专家,Smyl 曾在2016 年计算智能预测国际时间序列竞赛中使用递归神经网络获胜。可以说,这个结果表明,机器学习(及深度学习作为一种扩展)与实用主义相结合可以带来成功。

经济学家们长期以来在预测中使用混合模型,例如高斯混合模型或 GARCH 模型的混合。Skaters库提供了各种集成功能,也支持 ARMA 及类似模型的集成。你可以在微预测时间序列排行榜上找到不同集成模型的概述:microprediction.github.io/timeseries-elo-ratings/html_leaderboards/overall.html

在机器学习方面,集成方法,特别是在集成学习中的一种常见方法是训练多个模型,并根据它们的性能对预测结果进行加权。集成学习通过带放回采样来创建训练样本,进而拟合基础模型。袋外(OOB)误差是指在未参与训练集的训练样本上的模型预测误差的均值。

集成模型还可以由不同类型的基础模型组成,这被称为异质集成。Scikit-learn 为回归和分类提供了堆叠方法,最终模型可以根据基础模型的预测,找到加权系数来合并基础模型的预测结果。

在行业中,时间序列分析工作流仍然存在许多痛点。最主要的一个问题是,没有很多软件库支持多变量预测。

截至 2021 年 9 月,尽管它在开发路线图上,Kats 库尚不支持多变量预测(尽管已支持多变量分类)。statsmodels库中有VARVARMAX模型;然而,目前没有对多变量时间序列进行季节性去除的支持。

Salesforce 的 Merlion 库声称支持多变量预测,但似乎不在当前功能中。Darts库提供了几种适用于多变量预测的模型。

神经网络和集成方法,如随机森林或提升决策树,支持对多变量时间序列进行训练。在第七章时间序列的机器学习模型中,我们使用 XGBoost 创建了一个时间序列预测的集成模型。在本书附带的 GitHub 代码库中,我附上了一个笔记本,展示了如何将 scikit-learn 管道和多输出回归器应用于多变量预测。然而,在这一章中,我们将重点介绍深度学习模型。

英国诺里奇东安格利亚大学的 Alejandro Pasos Ruiz 及其同事在他们的论文《伟大的多变量时间序列分类大比拼:近期算法进展的回顾与实验评估》(2020 年)中指出,尽管有很多关于单变量数据集的建模,然而多变量应用却被忽视了。这不仅体现在软件解决方案的可用性上,也体现在数据集、以往的竞赛以及研究中。

他们对 UEA 数据集中的 30 个多变量时间序列进行了时间序列分类的基准测试。结果发现,三个分类器比动态时间规整算法(DTW)准确度高得多:HIVE-COTE、CIF 和 ROCKET(有关这些方法的详细信息,请参考第四章时间序列机器学习简介);然而,深度学习方法 ResNet 的表现与这些领先者差距不大。

Hassan Ismail Fawaz 等人(2019)发表的论文《用于时间序列分类的深度学习:综述》中,其中一项基准测试的发现是,一些深度神经网络可以与其他方法竞争。随后他们进一步展示了神经网络集成在相同数据集上的表现与 HIVE-COTE 不相上下(《深度神经网络集成用于时间序列分类》,2019)。

Pedro Lara-Benítez 等人(2021)在他们的论文《时间序列预测的深度学习架构实验评审》中做了另一次比较。他们运行了一个回声状态网络(ESN)、卷积神经网络(CNN)、时间卷积网络(TCN)、一个全连接的前馈网络(MLP),以及几个递归架构,如 Elman 递归网络、门控递归单元(GRU)网络和长短期记忆(LSTM)网络。

从统计学角度来看,基于平均排名,CNN、MLP、LSTM、TCN、GRU 和 ESN 没有显著区别。

总体而言,深度学习模型非常有前景,且由于其灵活性,它们能够填补多变量预测中的空白。我希望在本章中展示它们的实用性。

在本章中,我们将应用以下模型:

  • N-BEATS

  • 亚马逊的 DeepAR

  • 递归神经网络(LSTM)

  • Transformer

  • 时间卷积网络(TCN)

  • 高斯过程

我们在 第十章时间序列的深度学习》中已经详细介绍了大部分方法,但我将简要概述每个方法的主要特点。

可解释时间序列预测的神经基础扩展分析N-BEATS),该模型在 2020 年 ICLR 大会上展示,相较于 M4 竞赛的冠军模型,提升了 3%的预测精度。作者展示了一种纯深度学习方法,且没有任何时间序列特定组件,能够超越统计方法处理像 M3 和 M4 竞赛数据集及 TOURISM 数据集等具有挑战性的数据集。此方法的另一个优势是其可解释性(尽管我们在本章中不会重点讨论这一点)。

DeepAR 是一个来自亚马逊德国研究院的概率自回归递归网络模型。他们比较了三个不同数据集的分位数预测的准确性,并仅与一种因式分解技术(MatFact)在两个数据集(交通和电力)上进行准确性比较。

长短期记忆网络LSTM)用于序列建模。像 LSTM 这样的递归神经网络的一个重要优势是它们可以学习长时间序列的数据点。

Transformer 是基于注意力机制的神经网络,最初在 2017 年的论文 "Attention Is All You Need" 中提出。它们的关键特点是与特征数量呈线性复杂度,并具备长时记忆能力,使我们能够直接访问序列中的任意点。Transformer 相较于循环神经网络的优势在于,它们是并行执行的,而不是按顺序执行,因此在训练和预测中运行速度更快。

Transformer 是为了解决 自然语言处理NLP)任务中的序列问题而设计的;然而,它们同样可以应用于时间序列问题,包括预测,尽管这种应用不使用诸如位置编码等更特定于句子的特征。

时间卷积网络TCN)由膨胀的、因果的 1D 卷积层组成,具有相同的输入和输出长度。我们使用的是包含残差块的实现,该实现由 Shaojie Bai 等人(2018)提出。

这些方法中的最后一个,高斯过程,不能被 convincingly 地归类为深度学习模型;然而,它们等同于一个具有独立同分布先验参数的单层全连接神经网络。它们可以被视为多元正态分布的无限维推广。

一个有趣的附加方面——尽管我们在这里不深入探讨——是许多这些方法允许使用额外的解释性(外生)变量。

我们将使用一个包含不同州的 10 维时间序列能源需求的数据集。该数据集来自 2017 年全球能源预测竞赛(GEFCom2017)。

每个变量记录特定区域的能源使用情况。这突出了长时记忆的问题——为了突出这一点,我们将进行多步预测。

你可以在我为演示目的创建的 GitHub 仓库中找到 tensorflow/keras 的模型实现及其数据的工具函数:github.com/benman1/time-series

让我们直接进入正题。

Python 实践

我们将加载能源需求数据集,并应用几种预测方法。我们正在使用一个大数据集,并且这些模型有些相当复杂,因此训练可能需要较长时间。我建议你使用 Google Colab 并启用 GPU 支持,或者减少迭代次数或数据集的大小。待会儿在相关时,我会提到性能优化。

首先从上述提到的 GitHub 仓库中安装库:

!pip install git+https://github.com/benman1/time-series 

这不应该花费太长时间。由于需求包括 tensorflownumpy,我建议将它们安装到虚拟环境中。

然后,我们将使用库中的一个工具方法加载数据集,并将其包装在一个 TrainingDataSet 类中:

from time_series.dataset.utils import get_energy_demand
from time_series.dataset.time_series import TrainingDataSet
train_df = get_energy_demand()
tds = TrainingDataSet(train_df) 

如果你想加速训练,你可以减少训练样本的数量。例如,代替前面的那一行,你可以写:tds = TrainingDataSet(train_df.head(500))

我们稍后会对 GaussianProcess 进行操作,它无法处理完整数据集。

对于这些大多数模型,我们将使用 TensorFlow 图模型,这些模型依赖于非 eager 执行。我们必须显式地禁用 eager 执行。此外,对于其中一个模型,我们需要设置中间输出以避免 TensorFlow 问题:Connecting to invalid output X of source node Y which has Z outputs

from tensorflow.python.framework.ops import disable_eager_execution
import tensorflow as tf
disable_eager_execution()  # for graph mode
tf.compat.v1.experimental.output_all_intermediates(True) 

我已经设置了我们将用于所有生成的预测的指标和绘图方法。我们可以直接从时间序列库加载它们:

from time_series.utils import evaluate_model 

我们还将训练中的 epoch 数设置为 100 ——每个模型都相同:

N_EPOCHS = 100 

如果你发现训练时间过长,你可以将此值设置为更小的值,以便训练更早结束。

我们将依次介绍不同的预测方法,首先是DeepAR

from time_series.models.deepar import DeepAR
ar_model = DeepAR(tds)
ar_model.instantiate_and_fit(verbose=1, epochs=N_EPOCHS) 

我们将看到模型的总结,然后是训练误差随时间的变化(此处省略):

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_4ElEIb/Screenshot 2021-10-04 at 22.37.08.png

图 12.2:DeepAR 模型参数。

这个模型相对简单,正如我们所见:只有 360 个参数。显然,我们可以调整这些参数并添加更多。

然后我们将在测试数据集上生成预测:

y_predicted = ar_model.model.predict(tds.X_test)
evaluate_model(tds=tds, y_predicted=y_predicted,
    columns=train_df.columns, first_n=10) 

我们将查看误差——首先是总体误差,然后是每个 10 维度的误差:

MSE: 0.4338
----------
CT: 0.39
MASS: 1.02
ME: 1.13
NEMASSBOST: 1.48
NH: 1.65
RI: 1.48
SEMASS: 1.65
TOTAL: 1.45
VT: 1.23
WCMASS: 1.54 

我们将看到前10个时间步的图表:

图 12.3:DeepAR 对 10 个时间步的预测。

让我们继续下一个方法:N-BEATS:

from time_series.models.nbeats import NBeatsNet
nb = NBeatsNet(tds)
nb.instantiate_and_fit(verbose=1, epochs=N_EPOCHS)
y_predicted = nb.model.predict(tds.X_test)
evaluate_model(tds=tds, y_predicted=y_predicted,
    columns=train_df.columns, first_n=10) 

N-BEATS 训练两个网络。前向网络有 1,217,024 个参数。

让我们看看预测结果:

图 12.4:N-BEATS 预测。

接下来是 LSTM:

from time_series.models.LSTM import LSTM
lstm = LSTM(tds)
lstm.instantiate_and_fit(verbose=1, epochs=N_EPOCHS)
y_predicted = lstm.model.predict(tds.X_test)
evaluate_model(tds=tds, y_predicted=y_predicted,
    columns=train_df.columns, first_n=10) 

这个模型比 DeepAR 需要更多的参数:

/var/folders/80/g9sqgdws2rn0yc3rd5y3nd340000gp/T/TemporaryItems/NSIRD_screencaptureui_0VYxgy/Screenshot 2021-10-04 at 22.45.24.png

图 12.5:LSTM 模型参数。

45,000 个参数——这意味着训练时间比 DeepAR 更长。

在这里我们再次看到预测:

图 12.6:LSTM 预测。

让我们做一下 Transformer:

trans = Transformer(tds)
trans.instantiate_and_fit(verbose=1, epochs=N_EPOCHS)
y_predicted = trans.model.predict(tds.X_test)
evaluate_model(tds=tds, y_predicted=y_predicted,
    columns=train_df.columns, first_n=10) 

这是预测图:

forecast_transformer.png

图 12.7:Transformer 预测。

这个模型训练非常长,且性能是所有模型中最差的。

我们的最后一个深度学习模型是 TCN:

from time_series.models.TCN import TCNModel
tcn_model = TCNModel(tds)
tcn_model.instantiate_and_fit(verbose=1, epochs=N_EPOCHS)
print(tcn_model.model.evaluate(tds.X_test, tds.y_test))
y_predicted = tcn_model.model.predict(tds.X_test)
evaluate_model(tds=tds, y_predicted=y_predicted, columns=train_df.columns, first_n=10 

预测结果如下:

图 12.8:TCN 预测。

不幸的是,高斯过程无法处理我们的数据集——因此,我们只加载了一小部分。高斯过程还依赖于即时执行,因此我们需要重新启动内核,重新导入库,然后执行这段代码。如果你不确定如何操作,请查看本书 GitHub 代码库中的gaussian_process笔记本。

继续往下看:

from time_series.models.gaussian_process import GaussianProcess
tds2d = TrainingDataSet(train_df.head(500), train_split=0.1, two_dim=True)
gp = GaussianProcess(tds2d)
gp.instantiate_and_fit(maxiter=N_EPOCHS)
y_predicted = gp.predict(tds2d.X_test)[0].numpy().reshape(-1, tds.dimensions, tds.n_steps)
evaluate_model(tds=tds, y_predicted=y_predicted,
    columns=train_df.columns, first_n=10) 

预测结果如下:

forecast_gp.png

图 12.9:高斯过程预测。

所有算法(除了高斯过程)都是在99336个数据点上训练的。如前所述,我们将训练轮次设为100,但是有一个早停规则,如果训练损失在5次迭代内没有变化,训练就会停止。

这些模型是在测试集上验证的。

让我们来查看统计数据:

参数 MSE(测试) 轮次
DeepAR 360 0.4338 100
N-BEATS 1,217,024 0.1016 100
LSTM 45,410 0.1569 100
Transformer 51,702 0.9314 55
TCN 145,060 0.0638 100
高斯过程 8 0.4221 100
ES 1 11.28 -

鉴于深度学习方法之间存在巨大的误差差异,可能是变换器的实现出了问题——我将在某个时刻尝试修复它。

我已经将一个基准方法——指数平滑法ES)加入了模型中。你可以在时间序列代码库中找到这部分代码。

这为本章和整本书画上了句号。如果你想更好地理解背后的原理,可以查看代码库,你也可以调整模型参数。

时间序列的未来是什么?

在本书中,我们已探讨了时间序列的许多方面。如果你能读到这里,你应该已经学会了如何分析时间序列,以及如何应用传统的时间序列预测方法。这通常是市场上其他书籍的主要内容;然而,我们超越了这些。

我们探讨了与机器学习相关的时间序列预处理和转换方法。我们还查看了许多应用机器学习的实例,包括无监督和有监督的时间序列预测、异常检测、漂移检测和变更点检测。我们深入研究了在线学习、强化学习、概率模型和深度学习等技术。

在每一章中,我们都在探讨最重要的库,有时甚至是前沿的技术,最后,我们还涉及了广泛的工业应用。我们探讨了最先进的模型,如 HIVE-COTE、预处理方法如 ROCKET、适应漂移的模型(自适应在线模型),并回顾了多种异常检测方法。

我们甚至探讨了使用多臂赌博机在时间序列模型之间切换的场景,或者通过反事实进行因果分析的情景。

由于其普遍性,时间序列建模和预测在多个领域至关重要,并具有很大的经济意义。尽管传统和成熟的方式一直占主导地位,但时间序列的机器学习仍是一个相对较新的研究领域,刚刚走出其初期阶段,深度学习正处于这一革命的最前沿。

对于优秀模型的寻找将持续进行,并扩展到更大的新挑战。正如我在本章前面的部分希望展示的那样,其中一个挑战就是使多变量方法更具实用性。

下一届 Makridakis 竞赛 M5,聚焦沃尔玛提供的层次时间序列(42,000 个时间序列)。最终结果将在 2022 年发布。机器学习模型在时间序列的层次回归上表现出色,超越了一些文献中的成熟模型,正如Mahdi Abolghasemi等人("机器学习在时间序列层次预测中的应用," 2019)在一个包含 61 组具有不同波动性的时间序列的基准测试中所展示的那样。混合效应模型(应用于组和层次)在时间序列预测中也是一个活跃的研究领域。

M6 比赛涉及实时财务预测,包括 S&P500 美国股票和国际 ETF。未来的比赛可能会聚焦于非线性问题,如黑天鹅事件、具有厚尾的时间序列,以及对风险管理和决策至关重要的分布。

packt.com

订阅我们的在线数字图书馆,全面访问超过 7000 本书籍和视频,以及帮助你规划个人发展并推进职业生涯的行业领先工具。欲了解更多信息,请访问我们的网站。

为什么要订阅?

  • 利用来自 4000 多名行业专家的实用电子书和视频,减少学习时间,增加编码时间。

  • 通过特别为你制定的技能计划学习得更好

  • 每月免费获得一本电子书或视频

  • 完全可搜索,便于快速访问关键信息

  • 复制和粘贴、打印及收藏内容

你知道 Packt 为每本出版的书提供电子书版本,并且有 PDF 和 ePub 文件可供选择吗?你可以在 www.Packt.com 升级到电子书版本,作为纸质书客户,你有权获得电子书折扣。详情请通过 customercare@packtpub.com 联系我们。

www.Packt.com,你还可以阅读一系列免费的技术文章,注册各种免费的电子通讯,并获得 Packt 图书和电子书的独家折扣和优惠。

你可能喜欢的其他书籍

如果你喜欢这本书,可能对 Packt 出版的这些其他书籍感兴趣:

学习 Python 编程(第三版)

法布里齐奥·罗马诺

Heinrich Kruger

ISBN: 978-1-80181-509-3

  • 在 Windows、Mac 和 Linux 上启动和运行 Python

  • 在任何情况下编写优雅、可重用且高效的代码

  • 避免常见的陷阱,如重复、复杂的设计和过度工程化

  • 理解何时使用函数式编程或面向对象编程的方法

  • 使用 FastAPI 构建简单的 API,并使用 Tkinter 编写 GUI 应用程序

  • 了解更复杂的主题,如数据持久化和加密学的初步概述

  • 获取、清洗和操作数据,高效利用 Python 的内建数据结构

Python 面向对象编程 – 第四版

Steven F. Lott

Dusty Phillips

ISBN: 978-1-80107-726-2

  • 通过创建类并定义方法,在 Python 中实现对象

  • 使用继承扩展类的功能

  • 使用异常处理不寻常的情况,保持代码的清晰

  • 理解何时使用面向对象的特性,更重要的是,何时不使用它们

  • 探索几种广泛使用的设计模式及其在 Python 中的实现方式

  • 揭开单元测试和集成测试的简单性,并理解它们为何如此重要

  • 学会静态检查你的动态代码类型

  • 理解使用 asyncio 处理并发性以及它如何加速程序

专家 Python 编程 – 第四版

Michał Jaworski

Tarek Ziadé

ISBN: 978-1-80107-110-9

  • 探索设置可重复且一致的 Python 开发环境的现代方法

  • 高效地打包 Python 代码供社区和生产使用

  • 学习 Python 编程的现代语法元素,如 f-strings、枚举和 lambda 函数

  • 通过元类揭开 Python 元编程的神秘面纱

  • 编写 Python 中的并发代码

  • 使用 C 和 C++ 编写的代码扩展和集成 Python

Packt 正在寻找像你这样的作者

如果你有兴趣成为 Packt 的作者,请访问 authors.packtpub.com 并立即申请。我们与成千上万的开发者和技术专业人士合作,帮助他们与全球技术社区分享见解。你可以提交一般申请,申请我们正在招聘作者的特定热门话题,或者提交自己的想法。

分享你的想法

现在你已经完成了 《Python 时间序列机器学习》,我们很想听听你的想法!如果你从 Amazon 购买了这本书,请 点击这里直接进入 Amazon 的评论页面 来分享你的反馈或留下评论。

你的评论对我们和技术社区非常重要,将帮助我们确保提供优质的内容。

索引

A

激活函数 264

激活函数 266

AdaBoost 101

自适应学习 222

方法 222

自适应 XGBoost 222

ADWIN(自适应窗口) 220

智能体 98

赤池信息量准则(AIC) 140

赤池信息量准则(AIC) 156

AlexNet 265

亚马逊 169

anaconda 文档

参考链接 22

年金 7

异常检测 95, 164, 165, 166, 167, 168, 178, 179, 180

亚马逊 169

Facebook 170

Google Analytics 169

实现 170, 171, 172

微软 168, 169

Twitter 170

Anticipy 146

应用统计学 17

ARCH(自回归条件异方差) 143

曲线下面积 115

人工通用智能(AGI) 298

天文学 11, 12

自相关 58, 59

自编码器(AEs) 272, 273

自动特征提取 88, 89

自回归(AR) 135, 136

自回归条件异方差(ARCH) 146

自回归积分滑动平均(ARIMA)129

自回归积分滑动平均模型(ARIMA) 138

自回归模型 135

自回归滑动平均(ARMA)137

B

反向预测 95

反向传播 99,103,264

包袋法 100,101

与提升方法相比 102

模式包(BoP)122

模式包(BOP)123

SFA 符号包(BOSS)122

强盗算法 302,303

基础学习器 100

贝叶斯信息准则(BIC)140

贝叶斯结构时间序列(BSTS)模型 236,242,243,244

实现,使用 Python 256,257,259

生物学 10

提升法 100

自举法 101

向量空间中的 BOSS(BOSS VS)122

Box-Cox 变换 72,81,82

工作日

提取,按月 88

C

C4.5 算法 100

电缆理论 263

典型区间森林(CIF)121

CART 算法(分类与回归树)100

因果滤波器 74

细胞 265

中心极限定理 12

应用数学中心(CMAP)129

变点检测(CPD)172,173,174,175,176,180,181,182

经典模型 132,133

ARCH(自回归条件异方差)143

自回归(AR)134,135,136

GARCH(广义 ARCH)144

模型选择 139

移动平均(MA)134

顺序 139, 140

向量自回归模型 144, 145

分类 95, 97, 113

聚类 95, 176, 177

决定系数 107, 108

共线性 50

复杂细胞 265

概念漂移 217

conda 22

置信区间 45

混淆矩阵 114

上下文博弈 303

列联表 220

连续时间马尔可夫链 (CTMC) 239

ConvNets 276

卷积神经网络 (CNN) 322

卷积神经网络 (CNN) 77

相关性热图 53

相关性矩阵 52

相关比率 115, 116

协变量漂移 217

临界差异 (CD) 图 119

交叉验证 105

交叉验证准确度加权概率集成 (CAWPE) 124

曲线拟合 94

循环变化 56

D

DataFrame 30

数据预处理

关于 68, 69

数据预处理,技术

特征工程 68

特征变换 68

数据集漂移 216

与日期和时间相关的特征 75

日期注释 85, 86

日期时间 39, 40, 41

决策树 100

解码器 273

DeepAR 236, 274

DeepAR 模型 325, 326

深度学习 261, 262

深度学习方法

类型学 268

深度学习,应用于时间序列 269, 270, 271

深度 Q 学习 303, 304, 305

深度 Q 网络(DQN)311

深度强化学习(DRL)301

DeepState 236

人口统计学 6, 7, 8, 9

树突 263

描述性分析 36

差分 138

膨胀因果卷积神经网络 292, 293, 294, 295

狄利克雷采样 303

离散时间马尔可夫链(DTMC)239

基于距离的方法 118

dl-4-tsc 271

漂移 216, 217, 218, 219

概念漂移 217

协变量漂移 217

概率漂移 217

漂移检测 224, 225

方法 219, 220, 222

漂移检测方法(DDM)220

漂移转换 216

随机失活 282

动态时间规整

在 K 近邻算法中的使用 189

动态时间规整(DTW)116

动态时间规整(DTW)118, 270

动态时间规整

K 近邻算法,使用 Python 193, 194, 195

E

提前停止 282

回声状态网络(ESN)322

ECL(电力消耗负载)279

经济学 13, 14

弹性集成(EE)124

心电图(ECG)118

脑电图(EEG)118

脑电图(EEG)16, 17, 60

电子数值积分器和计算机(ENIAC)15, 16

编码器 273

epsilon-greedy 301

错误指标

时间序列 106

ETT(电力变压器温度)279

欧几里得距离 116

经验重放技术 304

探索与开发困境 301

探索性分析 36

探索性数据分析(EDA)36

指数平滑 140, 141, 142

指数平滑(ES)269, 275, 335

指数平滑模型 157, 158

用于创建预测 157

极端学生化偏差(ESD)170

F

Facebook 170

虚警率 115

假阴性(FN)115

假阳性率(FPR)115

假阳性(FP)115

特征工程 68

关于 74, 75

日期和时间相关特征 75

ROCKET 特征 76, 77

形状特征 77

特征泄漏 49

特征变换 68

关于 69

填充 73

对数变换 71

幂变换 71

缩放 70

前馈传播 264

滤波器 76

预测

创建,使用指数平滑模型 156

预测误差 107

预测 95

预测 6

全连接前馈神经网络 98

全连接网络 281, 282, 283, 284, 285, 286, 288

全连接网络(FCNs) 273

全卷积神经网络(FCN) 273

模糊建模 240, 241, 242

模糊集理论 240

模糊时间序列

用 Python 实现 252, 253, 254, 255, 256

G

GARCH(广义 ARCH) 144

门控递归单元(GRU) 323

高斯过程 333, 334

高斯过程(GP) 271

广义加法模型(GAM) 129, 170, 238

广义线性模型(GLM) 19

广义线性模型(GLM) 129

广义随机形状森林(gRFS) 119

生成对抗网络(GANs) 261

全局最大池化 77

全球温度时间序列

参考链接 57

Gluon-TS 271

Google Analytics 169

梯度提升回归树(GBRT) 191

梯度提升树

实现 102

梯度提升 102, 191, 192, 199, 200, 201, 202, 203, 204

梯度提升机(GBM) 191

格兰杰因果关系 117

图形处理单元(GPUs) 265

H

异质集成 321

隐马尔可夫模型(HMM) 239

基于变换的集成模型的层次投票集成(HIVE-COTE)123

HIVE-COTE(基于变换的集成模型的层次投票集成)270

Hoeffding 树 215

留出法 212

节假日特征 83,84,85

霍尔茨-温特斯法(Holtz-Winters method) 142

I

识别函数 266

插补 82,83

插补技术 73

InceptionTime 273,274

推理 96

Informer 278,279

集成开发环境(IDE)27

集成 138

四分位间距 45

J

JupyterLab 26,27

Jupyter Notebook 26

K

K 臂赌博机 212

Kats 安装 205,206,207

核函数 76

K 近邻

使用动态时间规整 189

使用动态时间规整的 Python 193,194,195

L

标签漂移 217

最小二乘算法 144

最小二乘法 12

赖斯法则(lex parsimoniae)139

安装 22,23,25

寿命表 7

Light Gradient Boosting Machine(LightGBM)191

线性四率 220

线性回归(LR)238

折线图 51

对数变换 71,82

对数变换 78,79,81

长短期记忆(LSTM)103,265

长短期记忆(LSTM)323,329

长短期记忆模型(LSTM)269

损失函数 106

M

机器学习 93, 98

历史 98, 99

时间序列 94

工作流 103, 104, 105

机器学习算法

时间序列 117

查询时间与准确度 124, 125

机器学习方法

时间序列 186, 187

脑磁图(MEG)118

马尔可夫假设 239

马尔可夫性 239

马尔可夫模型 239

隐马尔可夫模型(HMM)239

实现,使用 Python 251, 252

马尔可夫过程 239

马尔可夫属性 239

马尔可夫切换模型

实现,使用 Python 248, 249, 250

最大似然估计(MLE)139

最大池化 77

平均值 44

平均绝对误差(MAE)109, 110, 228

平均绝对百分比误差(MAPE)238

平均百分比误差(MAPE)111

平均相对绝对误差(MRAE)108, 113

均方误差(MSE)109, 110

均方误差(MSE)229

中位数 45

中位数绝对偏差(MAD)165

中位数绝对误差(MdAE)111

医学 16, 17

气象学 14

指标 106

微观预测时间序列排行榜

参考链接 321

微软 168

MINIROCKET 119, 120

最小-最大缩放 70

基于模型的插补 73

建模

在 Python 中 148, 149, 150, 151, 152, 153, 154, 155

模型选择 139, 230, 231, 232

模型堆叠 74

单调性 71

移动平均 134

移动平均 (MA) 129, 238

MrSEQL 123

多臂老丨虎丨机 212

多臂老丨虎丨机 (MAB) 302

多层感知器 (MLP) 238

乘法季节性 142

多元分析 38

多元时间序列 4

多元时间序列

预测 320, 321, 322, 323, 324

多元时间序列分类

临界差异图 127

多元时间序列 (MTS) 273

多元无监督符号与导数 123

N

自然语言处理 (NLP) 269

N-BEATS 275

最近邻算法 99

可解释时间序列预测的神经基础扩展分析 (N-BEATS) 323, 327

神经突 262

神经元 262, 263, 264

非线性方法 70

标准化均方误差 (NMSE) 112

标准化回归指标 112

NumPy 28, 29

O

目标函数 98

离线学习 210

与在线学习 210, 211 相对

在线算法 213, 214, 215

在线学习 210

使用案例 210

对比离线学习 210, 211

在线均值 213

在线方差 213

我们的数据世界(OWID)46

异常检测 95

袋外(OOB)误差 321

样本外测试 105

P

pandas 30, 31, 41, 42, 43

发薪日

获取 86

Pearson 相关系数 50

百分位数 45

感知机 98, 264

感知机模型 264

周期图 64

分段聚合近似(PAA)121

pip 25

Pmdarima 146

基于策略的学习 300

正比例值(PPV)77

幂函数 71

幂变换 78, 79, 81

Box-Cox 变换 72

Yeo-Johnson 变换 72

幂变换 71

精度 114

预测 96

预测误差 107

前瞻性评估 212

主成分分析(PCA)272

概率库 237

概率模型 236

时间序列 236

概率 235

概率漂移 217

概率排序原则(PRP)303

Prophet 模型 236, 237

预测模型 237, 238

实现,在 Python 中 245, 246, 247

邻近森林 (PF) 121

修剪的精确线性时间 (Pelt) 174

pytest 文档

参考链接 33

Python

最佳实践 31, 32

用于时间序列 18, 19, 21, 22

建模 148, 149, 150, 151, 152, 153, 154, 155

实践 177

Python 练习 245

关于 192

BSTS 模型,实现 256, 257, 258, 259

模糊时间序列模型,实现 252, 253, 254, 255, 256

梯度提升 199, 200, 201, 202, 203, 204

Kats 安装 205, 206, 207

K-近邻,带有动态时间规整 193, 194, 195

马尔可夫切换模型,实现 248, 249, 250, 251, 252

Prophet 模型,实现 245, 246, 247

Silverkite 195, 197, 198, 199

虚拟环境 192, 193

Python 库 145

datetime 39, 40, 41

pandas 41, 42, 43, 44

要求 39

Statsmodels 146, 147

Python 实践

关于 305

异常检测 178, 179, 180

变化点检测 (CPD) 180, 181, 182

推荐系统 305, 306, 307, 308, 309, 310

要求 177

使用 DQN 的交易 310, 311, 312, 313, 314, 315, 316, 317

Python 实践 223

Pytorch-forecasting 271

Q

分位数变换 73

四分位数 45

R

随机森林 102

随机区间特征 (RIF) 124

随机区间谱集成 (RISE) 124

召回 114

接收者操作特征曲线 (ROC) 115

循环神经网络 289, 290, 291

循环神经网络 (RNNs) 275

回归 95, 97, 107, 225, 226, 228, 229

正则化贪心森林 (RGF) 191

强化学习 95, 98

强化学习 96

强化学习 (RL)

关于 298, 299, 301

对时间序列 301

R 错误(RE) 108

残差 107

残差平方和 107

ResNets 266

River 库 214

ROCKET 119

ROCKET 特征 76, 90, 91

均方根误差(RMSE) 109, 110, 238

均方根偏差(RMSD) 110

均方根对数误差(RMSLE) 113

运行图 51

S

尺度不变特征(SIFT) 118

缩放方法 70

散点图 54

scikit-learn 214

scikit-learn 项目

参考链接 33

SciPy 28

季节

获取特定日期的数据 87

季节性 ARIMA(SARIMA) 238

季节性自回归(SAR) 138

季节性自回归整合滑动平均模型(SARIMA) 138

季节性 56

识别 56, 57, 58, 59, 60, 61, 63, 64

季节性滑动平均(SMA) 139

分割 95

自组织映射(SOM)242

敏感度 114

SEQL 123

形状元件 78, 91

优势 78

形状元件 119

形状元件变换分类器(STC) 119

Silverkite 195, 197, 198, 199

Silverkite 算法 190, 191, 236

简单细胞 265

简单指数平滑(SES)140, 141, 142, 143

简单移动平均 134

跳跃连接 266

Sktime-DL 271

斯皮尔曼等级相关系数 56

标准差 44

标准误差(SE)45

平稳性 6, 56, 136, 137

平稳过程 136

平稳过程 56

Statsmodels 146, 147

Statsmodels 库

用于建模时 147

结构化查询语言(SQL)30

PEP 8 风格指南

参考链接 32

日照小时数

获取,针对特定日期 87

监督算法,用于回归和分类

实现 128, 129

监督学习 96, 97

支持向量机(SVMs)103, 271

悬浮颗粒物(SPM)55

符号聚合近似(SAX)121

符号傅里叶近似(SFA)122

对称平均绝对百分比误差(SMAPE)111

突触 263

T

时间卷积网络(TCN)276, 322, 331, 333

时间字典集成(TDE)124

时间差分(TD)学习 299

时间融合变换器(TFT)278

泰尔 U 指数 113

Theta 方法 141

汤普森采样 303

时间序列 3

特征 4, 5

比较 116

机器学习方法,使用 186, 187

使用 Python 时 38

时间序列 335, 336

参考链接 324

强化学习(RL)301

无监督方法 162, 163, 164

时间序列 6

离线学习 210

在线学习 210

时间序列分析 6

时间序列分析(TSA)36, 37, 38

时间序列分类算法

临界差异图 126, 127

异构与集成嵌入森林的时间序列组合(TS-CHIEF)121

时间序列数据

示例 2

时间序列数据 2

时间序列数据集 94

时间序列预测 97

时间序列森林(TSF)120

时间序列机器学习算法

详细分类法 125, 126

时间序列机器学习飞轮 38

时间序列回归 107

transformer 330, 331

transformer 架构 277

趋势 56

识别 56, 57, 58, 59, 60, 61, 62, 63

三重指数平滑 142

真阳性率 114

真阳性率(TPR)115

真阳性(TP)115

Twitter 170

U

东安格利亚大学(UAE)118

UCR(加利福尼亚大学河滨分校)118

单元插补 73, 82

单变量分析 38

单变量系列 4

米纳斯吉拉斯联邦大学(UFMG)252

无监督学习 96, 97

无监督方法

时间序列 162, 163, 164

V

验证 187, 188

基于值的学习 300

变量 44, 45, 46, 47, 48, 49

关系 49, 50, 52

向量自回归模型 144, 145

向量自回归(VAR)129

向量自回归(VAR)133

非常快速决策树(VFDT)215

虚拟环境 193

W

滚动前进验证 188

弱学习器 100

WEASEL+MUSE123

基于窗口的特征 75

Wold 的分解 137

时间序列分类的词提取 123

Y

Yeo-Johnson 变换 72

Z

Z-score 标准化 70

索引

posted @ 2025-09-03 10:21  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报