时序回归问题旨在根据历史时间数据进行预测。例如,如果有一年或两年内的月度销售数据,建议预测下个月的销售情况。时序回归通常很难运行,可以使用多种不同的技术。
在本文中,我将展示如何结合使用滚动窗口数据和神经网络,从而执行时序回归分析。最好通过举例介绍这个想法。请查看图 1 中的演示程序。演示程序分析了从 1949 年 1 月到 1960 年 12 月的每月航空旅客数。
图 1:滚动窗口时序回归演示
演示数据来自 Internet 上许多地方都有的知名基准数据集,可以从本文随附的下载内容中获取。原始数据如下:
"1949-01";112"1949-02";118"1949-03";132"1949-04";129"1949-05";121"1949-06";135"1949-07";148"1949-08";148
..."1960-11";390"1960-12";432
共有 144 个原始数据项。第一个字段是年份和月份。第二个字段是每月国际航空旅客总人数,以千人为单位。演示程序创建定型数据的方法为,使用大小为 4 的滚动窗口生成 140 个定型项。定型数据的规范化方式为,用每个旅客计数除以 100:
[ 0] 1.12 1.18 1.32 1.29 1.21
[ 1] 1.18 1.32 1.29 1.21 1.35
[ 2] 1.32 1.29 1.21 1.35 1.48
[ 3] 1.29 1.21 1.35 1.48 1.48
...
[139] 6.06 5.08 4.61 3.90 4.32
请注意,数据中删除了明确的时间值。第一个窗口由前四个用作预测指标值的旅客计数(1.12、1.18、1.32、1.29)组成,后跟第五个计数 (1.21),即要预测的值。
下一个窗口由第二个到第五个计数(即下一组预测指标值 1.18、1.32、1.29、1.21)组成,后跟第六个计数 (1.35),即要预测的值。简单来说,就是使用每组连续四个旅客计数来预测下一个计数。
演示程序使用的神经网络包含 4 个输入节点、12 个隐藏的处理节点和 1 个输出节点。输入节点数量对应于滚动窗口中的预测指标数量。
必须通过反复试验来确定窗口大小,这是此项技术的最大缺点。神经网络的隐藏节点数量也必须通过反复试验进行确定,
神经网络一向如此。只有一个输出节点,因为时序回归预测只提前一个时间单位。
神经网络有 (4 * 12) + (12 * 1) = 60 个节点间权重,以及 (12 + 1) = 13 个偏差,这其实就定义了神经网络模型。借助滚动窗口数据,演示程序可以使用基本的随机反向传播算法对网络进行定型,其中学习率设置为 0.01,固定迭代次数设置为 10,000。
在定型过程中,演示程序每 2,000 次迭代就会显示一次预测输出值与正确输出值的均方误差。定型误差很难解释,之所以监视此误差主要是为了确定是否有非常奇怪的情况发生(这种情况相当常见)。在此示例中,定型误差似乎在大约 4,000 次迭代后开始趋于稳定。
完成定型后,演示代码显示 73 个权重和偏差值。再强调一遍,大部分情况下,这被用作健全性检查。对于时序回归问题,通常必须使用自定义准确度指标。在这种情况下,正确的预测结果为,未规范化的预测旅客计数与实际计数的差值不超过 30。根据这一定义,演示程序的准确度为 91.43%。也就是说,在 140 个预测旅客计数中,128 个正确,12 个不正确。
演示程序最后使用定型的神经网络,预测 1961 年 1 月(定型数据范围过后的第一时间段)的旅客计数。这就是所谓的“外推法”。预测的旅客计数为 433。此值可用作 1961 年 2 月旅客计数的预测指标变量,以此类推。
为了能够更好地理解本文,需要拥有中等或更高水平编程技能,并掌握神经网络方面的基本知识,但不必对时序回归了解得面面俱到。虽然演示程序是使用 C# 进行编码,但也可以将代码重构为其他语言(如 Java 或 Python),应该不会遇到太多麻烦。演示程序因太长而无法在本文中全部展示,但可以在本文随附的文件下载中获取整个源代码。
时序回归问题通常用折线图表示,如图 2 所示。蓝线表示从 1949 年 1 月一直到 1960 年 12 月 144 个未规范化的实际旅客计数(以千人为单位)。
浅红色的线表示神经网络时序模型生成的预测旅客计数。请注意,因为此模型使用的滚动窗口包含四个预测指标值,所以第一个预测旅客计数直到 5 月才出现。此外,我还在定型数据范围外,多预测了九个月的旅客计数。
这些预测旅客计数用红色虚线表示。
图 2:时序回归折线图
除了能够在定型数据范围外进行预测外,时序回归分析还可用于发现异常的数据点。演示程序的旅客计数数据不存在这种问题。可以看到,预测计数与实际计数非常相近。
例如,月份 t = 67 的实际旅客计数为 302(图 2 中心附近的蓝点),预测计数为 272。不过,如果假设月份 t = 67 的实际旅客计数为 400,则可以明显看出,月份 67 对应的实际旅客计数为离群值。
也可以使用编程方法,通过时序回归发现异常数据。例如,可以标记任何时间值,其中实际数据值与预测值的差值超过某固定阈值,如预测数据值与实际数据值的标准偏差的四倍。
为了对演示程序进行编码,我启动了 Visual Studio,并新建了名为 Neural-TimeSeries 的 C# 控制台应用程序。虽然我使用的是 Visual Studio 2015,但演示程序并不非常依赖 .NET Framework,因此任何新发布的版本都可以正常运行。
在编辑器窗口中加载模板代码后,我右键单击了“解决方案资源管理器”窗口中的 Program.cs 文件,并将此文件重命名为“NeuralTimeSeriesProgram.cs”,然后就是允许 Visual Studio 为我自动重命名类 Program。
在模板生成的代码顶部,我删除了所有不必要的 using 语句,仅留下引用顶级 System 命名空间的语句。
图 3 展示了整体程序结构(为节省空间,进行了少量小幅改动)。
using System;namespace NeuralTimeSeries
{ class NeuralTimeSeriesProgram
{ static void Main(string[] args)
{
Console.WriteLine("Begin times series demo");
Console.WriteLine("Predict airline passengers ");
Console.WriteLine("January 1949 to December 1960 "); double[][] trainData = GetAirlineData();
trainData = Normalize(trainData);
Console.WriteLine("Normalized training data:");
ShowMatrix(trainData, 5, 2, true); // first 5 rows
int numInput = 4; // Number predictors
int numHidden = 12; int numOutput = 1; // Regression
Console.WriteLine("Creating a " + numInput + "-" + numHidden + "-" + numOutput + " neural network");
NeuralNetwork nn = new NeuralNetwork(numInput, numHidden,
numOutput); int maxEpochs = 10000; double learnRate = 0.01; double[] weights = nn.Train(trainData, maxEpochs, learnRate);
Console.WriteLine("Model weights and biases: ");
ShowVector(weights, 2, 10, true); double trainAcc = nn.Accuracy(trainData, 0.30);
Console.WriteLine("\nModel accuracy (+/- 30) on training " + "data = " + trainAcc.ToString("F4")); double[] future = new double[] { 5.08, 4.61, 3.90, 4.32 }; double[] predicted = nn.ComputeOutputs(future);
Console.WriteLine("January 1961 (t=145): ");
Console.WriteLine((predicted[0] * 100).ToString("F0"));
Console.WriteLine("End time series demo ");
Console.ReadLine();
} // Main
static double[][] Normalize(double[][] data) { . . } static double[][] GetAirlineData() {. . } static void ShowMatrix(double[][] matrix, int numRows, int decimals, bool indices) { . . } static void ShowVector(double[] vector, int decimals, int lineLen, bool newLine) { . . } public class NeuralNetwork { . . }
} // ns
图 3:NeuralTimeSeries 程序结构
演示程序使用从头开始实现的单一隐藏层简单神经网络。此外,也可以结合使用本文介绍的技术和神经网络库(如 Microsoft 认知工具包 (CNTK))。
演示程序从设置定型数据入手,如图 4 所示。
double[][] trainData = GetAirlineData();
trainData = Normalize(trainData);
Console.WriteLine("Normalized training data:");
ShowMatrix(trainData, 5, 2, true);
Method GetAirlineData is defined as:static double[][] GetAirlineData()
{ double[][] airData = new
double[140][];
airData[0] = new double[] { 112, 118, 132, 129, 121 };
airData[1] = new double[] { 118, 132, 129, 121, 135 };
...
airData[139] = new double[] { 606, 508, 461, 390, 432 }; return airData;
}
图 4:设置定型数据
随后,对滚动窗口数据进行硬编码,窗口大小为 4。在编写时序程序之前,我编写了一个简短的实用工具程序,以根据原始数据生成滚动窗口数据。
在大多数的非演示情况下,请从文本文件中读取原始数据,再以编程方式生成滚动窗口数据,其中窗口大小已经过参数化,可以试验不同的大小。
Normalize 方法直接用所有数据值除以常数 100。我这样做纯粹是出于实际原因。第一次尝试时,我使用的是非规范化数据,结果非常糟糕,但在执行规范化后,结果得到了很大的改善。从理论上讲,使用神经网络时,数据无需进行规范化,但在实践中,规范化往往会产生巨大影响。
下面创建了神经网络:
int numInput = 4;
int numHidden = 12;int numOutput = 1;
NeuralNetwork nn = new NeuralNetwork(numInput, numHidden, numOutput);
输入节点数量被设置为 4 个,因为每个滚动窗口都有四个预测指标值。输出节点数量被设置为 1 个,因为每组窗口值都用于预测下个月的情况。隐藏节点数量被设置为 12 个,这是通过反复试验确定的。
神经网络通过以下语句进行定型和评估:
int maxEpochs = 10000;double learnRate = 0.01;double[] weights = nn.Train(trainData, maxEpochs, learnRate);
ShowVector(weights, 2, 10, true);
Train 方法使用基本的反向传播算法。变体有许多,包括使用动量或自适应学习率提高定型速度,以及使用 L1/L2 正则化或 Dropout 防止模型过度拟合。帮助程序方法 ShowVector 显示矢量,其中实际值的格式为 2 位小数,每行 10 个值。
创建神经网络时序模型后,评估它的预测准确度:
double trainAcc = nn.Accuracy(trainData, 0.30);
Console.WriteLine("\nModel accuracy (+/- 30) on " +
" training data = " + trainAcc.ToString("F4"));
对于时序回归,确定预测值的正确与否取决于所要调查的问题。对于航空旅客数据,如果未规范化的预测计数与实际原始计数的差值不超过 30,Accuracy 方法就会将预测旅客计数标记为正确。在演示数据中,t = 5 至 t = 9 的前五个预测计数都正确,但 t = 10 的预测计数不正确:
t actual predicted
= = = = = = = = = = =
5 121 129
6 135 128
7 148 137
8 148 153
9 136 140
10 119 141
演示程序最后使用末尾四个旅客计数(t = 141 至 t = 144),预测定型数据范围外首个时间段(t = 145,即 1961 年 1 月)的旅客计数:
double[] predictors = new double[] { 5.08, 4.61, 3.90, 4.32 };double[] forecast = nn.ComputeOutputs(predictors);
Console.WriteLine("Predicted for January 1961 (t=145): ");
Console.WriteLine((forecast[0] * 100).ToString("F0"));
Console.WriteLine("End time series demo");
请注意,由于时序模型是使用规范化数据(除以 100)进行定型,因此预测时也进行了规范化,所以演示程序在显示时用预测值乘以 100。
定义神经网络时,必须指定隐藏层节点和输出层节点使用的激活函数。简要地说,我建议使用双曲正切 (tanh) 函数激活隐藏层,并使用恒等函数激活输出层。
使用神经网络库或系统(如 Microsoft CNTK 或 Azure 机器学习)时,必须明确指定激活函数。演示程序会对这些激活函数进行硬编码。关键代码出现在 ComputeOutputs 方法中。下面计算了隐藏节点值:
for (int j = 0; j < numHidden; ++j)
for (int i = 0; i < numInput; ++i)
hSums[j] += this.iNodes[i] * this.ihWeights[i][j];for (int i = 0; i < numHidden; ++i) // Add biases
hSums[i] += this.hBiases[i];for (int i = 0; i < numHidden; ++i) // Apply activation
this.hNodes[i] = HyperTan(hSums[i]); // Hardcoded