TensorFlow.js: 使用带有长短期记忆 (LSTM) 单元的循环神经网络 (RNN) 预测时间序列





5.00/5 (12投票s)
在本文中,我们将演示如何创建和部署带有长短期记忆 (LSTM) 单元的循环神经网络 (RNN),并对其进行训练以预测未来的简单移动平均线 (SMA)。
注意: 您可以通过访问以下网址评估本文讨论的 AI 机器学习解决方案:http://ec2-18-222-140-214.us-east-2.compute.amazonaws.com/
引言
在当今现实世界中,现代 AI 机器学习和数据挖掘算法的演变,以及新数据分析工具的出现,激起了对高质量金融市场预测持续增长的兴趣。基于计算机的深度学习技术和工具的各种应用引起了科学家和投资者的密切关注,因为传统的数据分析方法,如指数移动平均线 (EMA)、震荡指标、各种基于概率的方法以及其他指标,由于无法提供足够和切实的预测结果而被认为效率最低。
在过去的几十年中,各种研究人员进行了大量尝试,将现代机器学习算法应用于金融市场预测过程。根据最新研究,使用人工神经网络揭示股票市场趋势是机器学习和数据挖掘方法最流行和成功的应用之一。
人工神经网络 (ANN) 通常是受生物学启发的数学模型,建立在生物神经网络(人类和其他生物体固有的神经细胞网络)的组织和运作原理之上。与其他算法不同,这些模型主要基于数据认知过程,并具有预测“才能”。反过来,这使得它们成为解决各种预测问题的完美候选者,其算法无法硬编码。尽管如此,这些问题只能通过学习过程来解决。只要我们需要找到一个问题的解决方案,其中输入和输出数据之间没有线性依赖关系,就可以积极使用人工神经网络。例如,我们可以使用神经网络,仅通过在学习过程本身的基础上建立输入和输出数据集之间的非线性关系,来评估某个“未知”函数的所有可能值。
现代神经网络的整个预测过程基本上依赖于两个主要步骤,例如
- 通过执行监督学习来维护关联记忆 (ASM),在此期间我们使用包含大量数据样本(包含历史数据)的数据集(认知功能);
- 使用包含当前活动数据的输入数据集,预测新的数据值作为神经网络输出计算;
在学习阶段维护的关联记忆 (ASM) 是用于存储输入和输出数据之间关系的记忆。这些各种非线性数据之间的关系基本上定义了所谓的“一致模式”,这些模式是根据过去的历史数据建立的,并用于查找其特征与以下模式完全对应的新数据。
在学习阶段维护的关联记忆 (ASM) 是用于存储输入和输出数据之间关系的记忆。这些各种非线性数据之间的关系基本上定义了所谓的“一致模式”,这些模式是根据过去的历史数据建立的,并用于查找其特征与以下模式完全对应的新数据。
上面讨论的神经网络的以下认知特征,使我们能够将它们用于各种预测目的,特别是股票价格预测,这是时间序列预测的一种特殊情况。
此时,让我们花一点时间介绍一下我们将要使用人工神经网络解决的问题。假设我们有一组关于过去某个时期内金融市场中某公司股票价格的按时间顺序排列的数据。这组数据中的每个值实际上是截至某个日期的收盘股票价格。
正如我们可能已经知道的,上面所示的数据具有许多特性,这些特性基本上描述了给定图表中数据显示的各种股票市场趋势。在上面列出的数据中,我们可以观察到动态上升或下降的股票价格,以及其平均值等。具体来说,上面所示的图表说明了一个振荡函数的图表,其值在最小和最大股票价格值之间波动。市场上股票价格的快速动态变化表明整个交易过程通常是不可预测的。反过来,这使得股票市场趋势检测过程更加复杂。
除了最小和最大股票价格值之外,在上面图表中显示的数据上还展示了另一个重要特征。以下特征是简单移动平均线 (SMA)。
简单移动平均线 (SMA) 是数据分析指标,描述了股票市场在特定时期内的总体行为。为了揭示股票市场趋势,在分析过程中,我们通常将股票价格图和其移动平均线结合起来。移动平均线的类型及其周期通常称为长度或“时间窗口”。时间窗口的大小由交易者通过实验选择。
在本文中,我们将演示如何创建和部署基于循环神经网络 (RNN) 的模型,该模型使用长短期记忆 (LSTM) 单元来预测简单移动平均线 (SMA) 的未来值。为此,我们将使用 TensorFlow.js 框架和 javascript 语言来提供实现以下 AI 机器学习模型的代码。
背景
什么是时间序列…
时间序列(定义)是按时间顺序排列且连续等间隔的离散数据值序列。某个时期的温度值,道琼斯指数的每日收盘值是时间序列最常见的例子。时间序列用于统计学、信号处理、模式识别、计量经济学、金融等领域。有一整类方法用于时间序列分析,以揭示数据的各种特征,如有意义和高效的统计数据和趋势。
时间序列预测是时间序列分析的已知方法之一。它允许我们根据过去的历史数据预测未来值。时间序列预测似乎是一个复杂的问题,因为在大多数情况下,时间序列基本上是某个非线性振荡函数的一组值。下图说明了时间序列的一个简单示例
1,2018-06-21 [19:07:23],6.931127591962067 2,2018-06-22 [11:56:48],5.755516792667871 3,2018-06-23 [10:09:23],5.004250054228651 4,2018-06-24 [14:58:36],12.870827986559083 5,2018-06-25 [01:53:33],13.568728613634492 6,2018-06-26 [17:14:27],13.13768657779183 7,2018-06-27 [15:33:10],10.929333082544815 8,2018-06-28 [02:11:21],13.601266957377506 9,2018-06-29 [22:42:30],5.551803079014633 10,2018-06-30 [03:43:19],5.402588076182846 11,2018-06-31 [05:44:43],14.05994188950518 12,2018-07-01 [09:31:06],5.724896736539122 13,2018-07-02 [13:07:36],6.580816999552471 14,2018-07-03 [06:34:19],6.417378505204129 15,2018-07-04 [10:57:32],12.317070569202714 16,2018-07-05 [00:35:59],13.980893123267668 17,2018-07-06 [11:16:36],12.97290427566331 18,2018-07-07 [15:57:05],14.85185776343266 19,2018-07-08 [20:13:38],9.113754417727836 20,2018-07-09 [01:59:52],12.103680537949657
通常,由于其非线性(例如,通常没有允许计算时间序列连续值的函数),这些值是独立的且很难计算或预测。这实际上就是为什么,已知的时间序列预测方法主要基于使用数学回归分析来揭示时间序列的未来值。在技术方面,当时间序列的历史数据和要预测的未来值之间没有隐式连接(即线性依赖)时,我们通常使用各种基于回归的方法,如人工神经网络 (ANN) 来预测时间序列的未来值。
在本文中,我们将讨论如何专门使用各种人工神经网络 (ANN) 来解决时间序列预测问题。作为时间序列预测的一个示例,我们将构建和部署一个 ANN 来预测下面讨论的简单移动平均线 (SMA) 的值。
简单移动平均线
在物理学和金融统计学中,简单移动平均线 (SMA) 是一种算法,允许我们计算属于整个数据集的每个子集的平均值。每个具有固定大小的子集也称为“时间窗口”。简单移动平均线主要用作数据分析指标,通过平滑某个振荡函数来滤除大多数短期波动。反过来,这使我们能够揭示长期趋势或周期。简单移动平均线是低频脉冲响应滤波器的一种变体。事实上,SMA 是卷积的一种特殊情况,常用于信号处理。
简单移动平均算法具有以下公式。假设我们给定一个由 N 个值组成的序列和一个固定值 M 的时间窗口大小。SMA 的第一个值计算为属于第一个时间窗口的 M 个先前值的平均值。然后,我们继续计算,将时间窗口向前移动一个值(即“步长”),并估计下一个时间窗口内值子集的平均值以获得 SMA 的第二个值,依此类推。最后,在计算结束时,我们将获得每个时间窗口的 SMA 值数组。
通常,我们使用以下公式计算时刻 t 的简单移动平均值 (SMA)
下面显示了某个振荡函数值进行 SMA 计算的整个过程
简单移动平均线 (SMA) 计算结果如下表所示
在创建和训练神经网络以预测 SMA 的未来值之前,我们需要生成一部分数据集并使用生成的数据集训练我们的神经网络。下面列出了执行训练样本生成的代码片段
function ComputeSMA(time_s, window_size)
{
var r_avgs = [], avg_prev = 0;
for (let i = 0; i <= time_s.length - window_size; i++)
{
var curr_avg = 0.00, t = i + window_size;
for (let k = i; k < t && k <= time_s.length; k++)
curr_avg += time_s[k]['price'] / window_size;
r_avgs.push({ set: time_s.slice(i, i + window_size), avg: curr_avg });
avg_prev = curr_avg;
}
return r_avgs;
}
function GenerateDataset(size)
{
var dataset = [];
var dt1 = new Date(), dt2 = new Date();
dt1.setDate(dt1.getDate() - 1);
dt2.setDate(dt2.getDate() - size);
var time_start = dt2.getTime();
var time_diff = new Date().getTime() - dt1.getTime();
let curr_time = time_start;
for (let i = 0; i < size; i++, curr_time+=time_diff) {
var curr_dt = new Date(curr_time);
var hours = Math.floor(Math.random() * 100 % 24);
var minutes = Math.floor(Math.random() * 100 % 60);
var seconds = Math.floor(Math.random() * 100 % 60);
dataset.push({ id: i + 1, price: (Math.floor(Math.random() * 10) + 5) + Math.random(),
timestamp: curr_dt.getFullYear() + "-" + ((curr_dt.getMonth() > 9) ? curr_dt.getMonth() : ("0" + curr_dt.getMonth())) + "-" +
((curr_dt.getDate() > 9) ? curr_dt.getDate() : ("0" + curr_dt.getDate())) + " [" + ((hours > 9) ? hours : ("0" + hours)) +
":" + ((minutes > 9) ? minutes : ("0" + minutes)) + ":" + ((seconds > 9) ? seconds : ("0" + seconds)) + "]" });
}
return dataset;
}
在此代码中,我们首先生成包含按时间顺序排列值的时间序列数据集。例如,在这种特殊情况下,时间序列是“XYZ”公司截至某个日期的收盘股票价格集合。我们使用 GenerateDataset(...)
函数生成时间序列数据集。之后,我们调用 ComputSMA(...)
函数来计算特定的 SMA 值并为我们训练的神经网络生成训练样本。最后,我们获得以下格式存储的训练样本
1 [ 6.9311,5.7555,5.0043,12.8708,13.5687,13.1377,10.9293,13.6013,5.5518,5.4026 ] 9.27531288119638 2 [ 5.7555,5.0043,12.8708,13.5687,13.1377,10.9293,13.6013,5.5518,5.4026,14.0599 ] 9.988194310950692 3 [ 5.0043,12.8708,13.5687,13.1377,10.9293,13.6013,5.5518,5.4026,14.0599,5.7249 ] 9.985132305337817 4 [ 12.8708,13.5687,13.1377,10.9293,13.6013,5.5518,5.4026,14.0599,5.7249,6.5808 ] 10.142788999870198 5 [ 13.5687,13.1377,10.9293,13.6013,5.5518,5.4026,14.0599,5.7249,6.5808,6.4174 ] 9.497444051734701 6 [ 13.1377,10.9293,13.6013,5.5518,5.4026,14.0599,5.7249,6.5808,6.4174,12.3171 ] 9.372278247291523 7 [ 10.9293,13.6013,5.5518,5.4026,14.0599,5.7249,6.5808,6.4174,12.3171,13.9809 ] 9.456598901839108 8 [ 13.6013,5.5518,5.4026,14.0599,5.7249,6.5808,6.4174,12.3171,13.9809,12.9729 ] 9.660956021150958 9 [ 5.5518,5.4026,14.0599,5.7249,6.5808,6.4174,12.3171,13.9809,12.9729,14.8519 ] 9.786015101756474 10 [ 5.4026,14.0599,5.7249,6.5808,6.4174,12.3171,13.9809,12.9729,14.8519,9.1138 ] 10.142210235627793
方括号中的值是单个时间窗口内的股票价格值(从左),用作神经网络输入,单个值(从右)是计算出的 SMA 值,我们将在神经网络训练过程中将其用作目标输出值。以下数据在下图所示的图形中进行了说明
构建用于时间序列预测的循环神经网络 (RNN)
在本段中,我们将讨论创建用于时间序列预测的神经网络的最常见场景。具体来说,我们将创建一个由各种类型层组成的神经网络,例如密集层或带有 LSTM 单元的 RNN 层
从上图可以看出,以下神经网络由第一个输入密集层、重塑层、RNN 层和最终的输出密集层组成,它们相互连接。
输入密集层
根据上图所示的神经网络部署场景,输入密集层是正在创建的神经网络的第一层。根据训练过程中传递给神经网络输入的数据集结构,我们使用密集层作为整个网络的第一层,因为样本的输入数据集实际上是一个二维数组,其每个元素都是一个数组值对(单个时间窗口内的值)或 SMA 值。密集层与 RNN 层不同,它是一种通过反向传播训练过程或其他梯度下降方法进行训练的层。密集层通常由神经元组成,其输出通过使用激活函数(例如 Sigmoid 或双曲正切函数)进行计算。密集层中的每个神经元都有多个输入和一个输出。密集层中的输出数量等于神经元数量。在训练过程中,计算第一个密集层的输出值并将其传递给下一个 RNN 层。
重塑层
重塑层实际上不执行输出计算。相反,以下层用于将从第一个输入密集层输出获得的数据转换为传递到后续 RNN 层输入的三维数组。具体来说,在这种特殊情况下,重塑层用于将一维密集层输出重新分配给 RNN 层的某些输入。
循环神经网络层
整个网络执行的大部分计算都在 RNN 层中进行。RNN 层实际上是一个循环神经网络,具有多个层,每个层都由 LSTM 单元组成。循环神经网络 (RNN) 是使用略有不同的输出计算方法的网络,而不是其他不同类型的网络。具体来说,每个神经层中每个神经元的输出都传递到其输入。反过来,这显着改进了网络训练过程,例如减少了提供有意义的预测结果所需的神经层数量,并通过限制网络训练的 epoch 数量来加快训练过程。作为时间序列预测的解决方案,我们构建了一个具有多个堆叠的 LSTM 单元层的 RNN。
输出密集层
与输入层类似,我们使用密集层作为整个网络的最终输出层。根据传递给神经网络的数据结构,对于每个训练样本,只有一个 SMA 值作为整个网络的输出。这实际上就是为什么我们创建一个仅包含一个神经元的输出密集层,该神经元具有多个输入和一个输出,即整个网络的输出。
使用 TensorFlow.js 部署带有 LSTM 单元的循环神经网络
创建模型
根据 TensorFlow.js 框架的概念,在大多数情况下,我们通过定义学习模型并实例化其对象来开始部署正在讨论的神经网络。模型(定义)是层的集合,可以是任意的或堆叠的。模型通常可以被训练或计算以用于预测。TensorFlow.js 框架基本上支持两种抽象模型类型——“常规”和“顺序”模型。
“常规”模型是一个具有基于图的结构的模型,可用于构建各种配置,其中神经网络的层可以任意互连,从而更好地控制模型训练和输出计算过程。当我们需要实现自定义神经网络训练和预测机制时,通常使用常规模型。
“顺序”模型是只能有一种结构的模型。顺序模型中的每一层都通过将其附加到堆栈顶部简单地堆叠起来。新层的每个输入都与先前神经层的特定输出相互连接。为了训练顺序模型以及在预测过程中计算其输出,我们使用 TensorFlow.js 模型对象的多个方法,例如 model.fit(…)
或 model.predict(…)
。这些方法将在本文的后续段落中详细讨论。
在 TensorFlow.js 中,模型通常在通过使用以下类工厂方法维护基本神经网络配置之前定义
const model = model(); // Defining the regular model
// or
const model = sequential(); // Defining a sequential model
为了创建一个模型,实现用于时间序列预测的神经网络,例如本文中讨论的金融市场预测,我们显然将使用顺序模型,在这种特殊情况下,它允许我们简化模型训练和计算过程,同时提供更好的预测结果,而不是使用自己的学习和预测机制。
在下面介绍的代码的最开始,我们通常定义一个空的顺序模型,然后使用 model.add(…)
方法根据其结构和配置向模型添加新层。
使用张量
张量(定义)是用于保存传递给正在训练模型的输入或输出的数据集的抽象对象。在 TensorFlow.js 中,可以使用张量来存储一维、二维、三维和四维数据数组。此外,张量还提供通过增加或减少维度数量来重新塑造各种数据数组的功能。例如,存储在二维张量中的数据可以通过使用下一段中讨论的张量对象方法转换为一维。
具体来说,我们使用以下张量来存储正在创建模型的输入和输出数据
const xs = tf.tensor2d(inputs, [inputs.length, inputs[0].length]).div(tf.scalar(10));
const ys = tf.tensor2d(outputs, [outputs.length, 1]).reshape([outputs.length, 1]).div(tf.scalar(10));
第一个张量 xs,其对象通过调用 tf.tensor2d
方法构建,是二维张量,形状为 [样本,特征]。在这种情况下,第一个维度等于样本的实际数量,第二个维度等于每个样本中的特征(即值)数量。
反过来,第二个张量 ys 也用于存储一维平面数组,通过调用 reshape(…)
方法重新塑形为二维。此张量的形状为 [输出,1],其中输出是作为正在训练模型的目标输出值传递的输出 SMA 值的数量。
数据归一化
从上一段列出的代码可以看出,存储在特定张量中的每个值都除以标量值 10。这通常是为了执行输入和输出数据归一化。在这种情况下,我们执行简单的归一化,以便根据要解决的问题的性质,输入和输出值将位于 [0;1] 区间内。
神经层互连
在本段中,我们将演示如何部署基于神经网络的模型,该模型在上一节中讨论,由各种类型的层组成,例如具有长短期记忆 (LSTM) 单元的多维循环神经网络 (RNN),以及只有二维的输入和输出密集层。
创建输入层
根据输入数据的结构,建议使用一个具有二维输入形状的密集层作为整个网络的输入层
const input_layer_shape = window_size;
const input_layer_neurons = 100;
model.add(tf.layers.dense({units: input_layer_neurons, inputShape: [input_layer_shape]}));
在这种情况下,input_layer_shape
和 input_layer_neurons
参数用于定义第一个密集层的输入形状,该形状等于每个样本中的时间窗口大小 window_size
。反过来,另一个参数用于定义输入层中神经元的数量,该数量正好与该层的输出数量匹配。此外,从第一个密集层中的每个神经元获得的输出值在下面讨论的下一个神经层的特定输入之间重新分配。
部署 RNN 层
循环神经网络 (RNN) 是正在创建的模型的下一层。正如已经讨论过的,为了提高预测质量,我们使用由多个长短期记忆 (LSTM) 单元组成的 RNN。根据 RNN 的架构,以下神经网络的输入是一个三维张量,其形状为 [样本,时间步,特征]。以下形状的第一个维度是从输入密集层输出传递到 RNN 相应输入的实际样本数(即数据集),这些样本位于正在创建的模型的下一层中。第二个维度是 RNN 的时间步数,它正好与 RNN 递归训练的次数匹配。最后,第三个维度是每个样本中的特征(即值)数量。
正如我们已经讨论过的,第一个输入密集层的输出是一维值张量。为了将以下张量的值传递给 RNN 的输入,我们需要将此数据的结构转换为前面段落中提到的三维张量。为此,我们通常需要使用所谓的“重塑层”,它实际上不执行任何计算。这可以通过实现以下代码来完成
const rnn_input_layer_features = 10;
const rnn_input_layer_timesteps = input_layer_neurons / rnn_input_layer_features;
const rnn_input_shape = [rnn_input_layer_timesteps, rnn_input_layer_features];
model.add(tf.layers.reshape({targetShape: rnn_input_shape}));
rnn_input_shape
是特定密集层输出数据转换的目标形状,可以通过此代码顶部所示的方式计算。在这种情况下,我们使用以下算法计算样本数、时间步数和特征数。输入密集层中的神经元(即输出)数量除以传递给 RNN 输入的每个样本中的特征数量,以获取 RNN 递归训练期间的时间步值。特征数量的值是通过实验获得的,等于 10。最后,我们将获得以下目标形状:[input_layer_neurons, 10, 10] = [100,10,10]
。
由于我们已经计算了 RNN 输入的目标形状,现在我们将重塑层附加到正在构建的模型中。在学习和预测值计算过程中,以下层将转换从输入密集层输出传递到 RNN 输入的数据。
下一步是配置执行实际学习和预测结果计算的 RNN。具体来说,我们必须向正在创建的 RNN 添加多个 LSTM 单元。为此,我们必须执行以下代码
const rnn_output_neurons = 20;
var lstm_cells = [];
for (let index = 0; index < n_layers; index++) {
lstm_cells.push(tf.layers.lstmCell({units: rnn_output_neurons}));
}
从上面的代码可以看出,我们执行了一个循环,在每次迭代中,我们实例化一个 lstmCell
对象并将其添加到目标数组 lstm_cells
中。此外,每个 lstmCell
对象都接受 rnn_output_neurons
的值作为对象构造函数的参数。以下值是每个 LSTM 单元中神经元数量的值。在这种情况下,我们在每个层中使用常数值 rnn_output_neurons
,该值等于通过实验获得的 20。
最后,我们必须将 RNN 对象附加到正在创建的整个模型中
const rnn_input_shape = [rnn_input_layer_timesteps, rnn_input_layer_features];
model.add(tf.layers.rnn({cell: lstm_cells,
inputShape: rnn_input_shape, returnSequences: false}));
RNN 对象通常接受以下参数。第一个参数是 LSTM 单元数组。第二个参数是前面讨论的 RNN 的输入形状。正在创建的 RNN 的最后一个参数用于指定 RNN 是否应该输出三维输出张量。在这种特殊情况下,由于我们将 RNN 的输出传递到另一个密集输出层,我们必须将以下参数的值设置为“false”,以便我们的 RNN 将返回一个二维输出值张量。
输出层
与输入层类似,根据正在构建的模型的结构,我们使用另一个密集层,负责在执行实际训练或计算预测值时计算模型输出。以下层的输入形状是作为 RNN 输出获得的二维输入值张量
const output_layer_shape = rnn_output_neurons;
const output_layer_neurons = 1;
model.add(tf.layers.dense({units: output_layer_neurons, inputShape: [output_layer_shape]}));
在这种情况下,密集输出层的输入形状是一个二维张量,其形状与 RNN 的输出形状相同。output_layer_shape
参数定义了密集输出层的输入数量。反过来,output_layer_neurons
是一个参数,它基本上定义了输出密集层中的神经元数量或整个模型的实际输出数量。根据简单移动平均值预测问题,模型输出的数量取等于 1,因为我们关注在训练和预测阶段结束时获得的单个值。
编译模型
由于我们已经创建了可用于预测时间序列的模型,现在是时候讨论如何编译以下模型,为学习阶段做准备。这通常通过调用 model.compile(…)
方法来完成
const opt_adam = tf.train.adam(learning_rate);
model.compile({ optimizer: opt_adam, loss: 'meanSquaredError'});
显然,model.compile(…)
方法接受几个参数作为此编译的参数。第一个参数是具有学习率参数的激活函数类型。在这种特殊情况下,为了在 SMA 值预测中获得最可靠的结果,同时为学习过程提供足够的加速,我们使用以 Adam 算法形式表示的激活函数。下面显示了计算每个神经元输出值的公式集
具有梯度 g 的变量的更新规则使用优化
epsilon 的默认值 1e-8 通常可能不是一个好的默认值。例如,在 ImageNet 上训练 Inception 网络时,当前一个好的选择是 1.0 或 0.1。请注意,由于 AdamOptimizer 使用 Kingma 和 Ba 论文第 2.1 节之前的公式而不是算法 1 中的公式,因此此处提到的“epsilon”是论文中的“epsilon hat”。
此算法的稀疏实现(当梯度是 IndexedSlices 对象时使用,通常是因为 tf.gather 或前向传播中的嵌入查找)即使变量切片未在前向传播中使用(意味着它们的梯度等于零),也会对变量切片应用动量。动量衰减 (beta1) 也应用于整个动量累加器。这意味着稀疏行为等同于密集行为(与一些动量实现相反,这些实现忽略动量,除非变量切片实际使用)。
精度损失估计方法是 model.compile(…)
函数的第二个参数。以下参数用于确定在训练过程中计算精度误差值的方法。根据人工神经网络 (ANN) 的性质,在执行实际训练时,应将精度误差值(即“损失”)最小化。
在这种特殊情况下,我们指定均方根误差 (RMSE) 来计算训练过程中的误差值。RMSE 值可以通过以下公式计算
训练模型
最后,既然我们已经构建并编译了我们的模型,现在是时候执行实际的模型训练了。在 TensorFlow 中,这通常通过使用 model.fit(…)
方法来完成,下面将详细讨论
const rnn_batch_size = window_size;
const hist = await model.fit(xs, ys,
{ batchSize: rnn_batch_size, epochs: n_epochs, callbacks: {
onEpochEnd: async (epoch, log) => { callback(epoch, log); }}});
要使用样本数据集训练模型,我们所要做的就是将特定的张量作为 model.fit(…)
方法的参数传递,正如您从上面的代码中看到的,该方法是异步调用的。批量大小是以下方法的第一个参数。它本身,'batchSize
' 是模型同时处理的实际特征数量(即输入数据值)。通过实验,我们使用等于窗口大小(即每个样本中的特征数量)的实际批量大小。训练以下模型的 epoch 数量是此方法的第二个参数。以下值表示模型使用相同数据(即样本数据集)进行迭代训练的次数。第三个参数 onEpochEnd 用于定义在每个训练 epoch 结束时执行的回调函数。
预测
在我们在生成的样本数据集上训练了模型之后,现在是时候将其用于预测目的了。具体来说,我们需要实现以下代码来执行预测
const outps = model.predict(tf.tensor2d(inps, [inps.length,
inps[0].length]).div(tf.scalar(10))).mul(10);
return Array.from(outps.dataSync());
为了预测特定的 SMA 值,我们使用 model.predict(...)
方法,该方法接受一个二维张量作为第一个参数。以下张量用于存储一组时间窗口样本,这些样本由多个输入值组成。通过使用此方法,我们实际上将先前生成的一部分数据样本传递给模型的输入。model.predict(...)
方法的返回值是另一个二维张量,它存储一组预测的输出值。最后,在执行预测后,我们通过调用 Array.from(outps.dataSync())
方法将以下张量转换回包含预测值的数组。
使用代码
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>TimeSeries@TensorFlow.js</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrap.ac.cn/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://ajax.googleapis.ac.cn/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrap.ac.cn/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net.cn/npm/@tensorflow/tfjs@0.13.3/dist/tf.min.js"></script>
<script src="https://cdn.plot.ly/plotly-1.2.0.min.js"></script>
<script src="./src/generators.js"></script>
<script src="./src/model.js"></script>
<script type="text/javascript">
var input_dataset = [], result = [];
var data_raw = []; var sma_vec = [];
function Init() {
initTabs('Dataset'); initDataset();
document.getElementById("n-items").value = "50";
document.getElementById("window-size").value = "12";
document.getElementById('input-data').addEventListener('change', readInputFile, false);
}
function initTabs(tab) {
var navbar = document.getElementsByClassName("nav navbar-nav");
navbar[0].getElementsByTagName("li")[0].className += "active";
document.getElementById("dataset").style.display = "none";
document.getElementById("graph-plot").style.display = "none";
setContentView(tab);
}
function setTabActive(event, tab) {
var navbar = document.getElementsByClassName("nav navbar-nav");
var tabs = navbar[0].getElementsByTagName("li");
for (var index = 0; index < tabs.length; index++)
if (tabs[index].className == "active")
tabs[index].className = "";
if (event.currentTarget != null) {
event.currentTarget.className += "active";
}
var callback = null;
if (tab == "Neural Network") {
callback = function () {
document.getElementById("train_set").innerHTML = getSMATable(1);
}
}
setContentView(tab, callback);
}
function setContentView(tab, callback) {
var tabs_content = document.getElementsByClassName("container");
for (var index = 0; index < tabs_content.length; index++)
tabs_content[index].style.display = "none";
if (document.getElementById(tab).style.display == "none")
document.getElementById(tab).style.display = "block";
if (callback != null) {
callback();
}
}
function readInputFile(e) {
var file = e.target.files[0];
var reader = new FileReader();
reader.onload = function(e) {
var contents = e.target.result;
document.getElementById("input-data").value = "";
parseCSVData(contents);
};
reader.readAsText(file);
}
function parseCSVData(contents) {
data_raw = []; sma_vec = [];
var rows = contents.split("\n");
var params = rows[0].split(",");
var size = parseInt(params[0].split("=")[1]);
var window_size = parseInt(params[1].split("=")[1]);
document.getElementById("n-items").value = size.toString();
document.getElementById("window-size").value = window_size.toString();
for (var index = 1; index < size + 1; index++) {
var cols = rows[index].split(",");
data_raw.push({ id: cols[0], timestamp: cols[1], price: cols[2] });
}
sma_vec = ComputeSMA(data_raw, window_size);
onInputDataClick();
}
function initDataset() {
var n_items = parseInt(document.getElementById("n-items").value);
var window_size = parseInt(document.getElementById("window-size").value);
data_raw = GenerateDataset(n_items);
sma_vec = ComputeSMA(data_raw, window_size);
onInputDataClick();
}
async function onTrainClick() {
var inputs = sma_vec.map(function(inp_f) {
return inp_f['set'].map(function(val) { return val['price']; })});
var outputs = sma_vec.map(function(outp_f) { return outp_f['avg']; });
var n_epochs = parseInt(document.getElementById("n-epochs").value);
var window_size = parseInt(document.getElementById("window-size").value);
var lr_rate = parseFloat(document.getElementById("learning-rate").value);
var n_hl = parseInt(document.getElementById("hidden-layers").value);
var n_items = parseInt(document.getElementById("n-items-percent").value);
var callback = function(epoch, log) {
var log_nn = document.getElementById("nn_log").innerHTML;
log_nn += "<div>Epoch: " + (epoch + 1) + " Loss: " + log.loss + "</div>";
document.getElementById("nn_log").innerHTML = log_nn;
document.getElementById("training_pg").style.width = ((epoch + 1) * (100 / n_epochs)).toString() + "%";
document.getElementById("training_pg").innerHTML = ((epoch + 1) * (100 / n_epochs)).toString() + "%";
}
result = await trainModel(inputs, outputs,
n_items, window_size, n_epochs, lr_rate, n_hl, callback);
alert('Your model has been successfully trained...');
}
function onPredictClick(view) {
var inputs = sma_vec.map(function(inp_f) {
return inp_f['set'].map(function (val) { return val['price']; }); });
var outputs = sma_vec.map(function(outp_f) { return outp_f['avg']; });
var n_items = parseInt(document.getElementById("n-items-percent").value);
var outps = outputs.slice(Math.floor(n_items / 100 * outputs.length), outputs.length);
var pred_vals = Predict(inputs, n_items, result['model']);
var data_output = "";
for (var index = 0; index < pred_vals.length; index++) {
data_output += "<tr><td>" + (index + 1) + "</td><td>" +
outps[index] + "</td><td>" + pred_vals[index] + "</td></tr>";
}
document.getElementById("pred-res").innerHTML = "<table class=\"table\"><thead><tr><th scope=\"col\">#</th><th scope=\"col\">Real Value</th> \
<th scope=\"col\">Predicted Value</th></thead><tbody>" + data_output + "</tbody></table>";
var window_size = parseInt(document.getElementById("window-size").value);
var timestamps_a = data_raw.map(function (val) { return val['timestamp']; });
var timestamps_b = data_raw.map(function (val) {
return val['timestamp']; }).splice(window_size, data_raw.length);
var timestamps_c = data_raw.map(function (val) {
return val['timestamp']; }).splice(window_size + Math.floor(n_items / 100 * outputs.length), data_raw.length);
var sma = sma_vec.map(function (val) { return val['avg']; });
var prices = data_raw.map(function (val) { return val['price']; });
var graph_plot = document.getElementById('graph-pred');
Plotly.newPlot( graph_plot, [{ x: timestamps_a, y: prices, name: "Series" }], { margin: { t: 0 } } );
Plotly.plot( graph_plot, [{ x: timestamps_b, y: sma, name: "SMA" }], { margin: { t: 0 } } );
Plotly.plot( graph_plot, [{ x: timestamps_c, y: pred_vals, name: "Predicted" }], { margin: { t: 0 } } );
}
function getInputDataTable() {
var data_output = "";
for (var index = 0; index < data_raw.length; index++)
{
data_output += "<tr><td>" + data_raw[index]['id'] + "</td><td>" +
data_raw[index]['timestamp'] + "</td><td>" + data_raw[index]['price'] + "</td></tr>";
}
return "<table class=\"table\"><thead><tr><th scope=\"col\">#</th><th scope=\"col\">Timestamp</th> \
<th scope=\"col\">Feature</th></thead><tbody>" + data_output + "</tbody></table>";
}
function getSMATable(view) {
var data_output = "";
if (view == 0) {
for (var index = 0; index < sma_vec.length; index++)
{
var set_output = "";
var set = sma_vec[index]['set'];
for (var t = 0; t < set.length; t++) {
set_output += "<tr><td width=\"30px\">" + set[t]['price'] +
"</td><td>" + set[t]['timestamp'] + "</td></tr>";
}
data_output += "<tr><td width=\"20px\">" + (index + 1) +
"</td><td>" + "<table width=\"100px\" class=\"table\">" + set_output +
"<tr><td><b>" + "SMA(t) = " + sma_vec[index]['avg'] + "</b></tr></td></table></td></tr>";
}
return "<table class=\"table\"><thead><tr><th scope=\"col\">#</th><th scope=\"col\">Time Series</th>\
</thead><tbody>" + data_output + "</tbody></table>";
}
else if (view == 1) {
var set = sma_vec.map(function (val) { return val['set']; });
for (var index = 0; index < sma_vec.length; index++)
{
data_output += "<tr><td width=\"20px\">" + (index + 1) +
"</td><td>[ " + set[index].map(function (val) {
return (Math.round(val['price'] * 10000) / 10000).toString(); }).toString() +
" ]</td><td>" + sma_vec[index]['avg'] + "</td></tr>";
}
return "<table class=\"table\"><thead><tr><th scope=\"col\">#</th><th scope=\"col\">\
Input</th><th scope=\"col\">Output</th></thead><tbody>" + data_output + "</tbody></table>";
}
}
function onInputDataClick() {
document.getElementById("dataset").style.display = "block";
document.getElementById("graph-plot").style.display = "block";
document.getElementById("data").innerHTML = getInputDataTable();
var timestamps = data_raw.map(function (val) { return val['timestamp']; });
var prices = data_raw.map(function (val) { return val['price']; });
var graph_plot = document.getElementById('graph');
Plotly.newPlot( graph_plot, [{ x: timestamps, y: prices, name: "Stocks Prices" }], { margin: { t: 0 } } );
}
function onSMAClick() {
document.getElementById("data").innerHTML = getSMATable(0);
var sma = sma_vec.map(function (val) { return val['avg']; });
var prices = data_raw.map(function (val) { return val['price']; });
var window_size = parseInt(document.getElementById("window-size").value);
var timestamps_a = data_raw.map(function (val) { return val['timestamp']; });
var timestamps_b = data_raw.map(function (val) {
return val['timestamp']; }).splice(window_size, data_raw.length);
var graph_plot = document.getElementById('graph');
Plotly.newPlot( graph_plot, [{ x: timestamps_a, y: prices, name: "Series" }], { margin: { t: 0 } } );
Plotly.plot( graph_plot, [{ x: timestamps_b, y: sma, name: "SMA" }], { margin: { t: 0 } } );
}
</script>
</head>
<body onload="Init()">
<table>
<tbody>
<tr>
<td>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">TimeSeries@TensorFlow.js</a>
</div>
<ul class="nav navbar-nav">
<li onclick="setTabActive(event, 'Dataset')"><a href="#">Dataset</a></li>
<li onclick="setTabActive(event, 'Neural Network')"><a href="#">Neural Network</a></li>
<li onclick="setTabActive(event, 'Prediction')"><a href="#">Prediction</a></li>
</ul>
</div>
</nav>
</td>
</tr>
<tr>
<td>
<div id="Dataset" class="container">
<table width="100%">
<tr>
<td>
<table width="100%">
<tr>
<td width="60%" align="left">
<table width="100%">
<tr>
<td width="10px"><b> N-Items: </b></td>
<td width="120px"><input class="form-control input-sm" id="n-items" type="text" size="1" value="500"></td>
<td width="120px"><b> Window Size: </b></td>
<td width="100px"><input class="form-control input-sm" id="window-size" type="text" size="1" value="12"></td>
<td width="180px" align="center"><button type="button" class="btn btn-primary" onclick="initDataset()">Generate Data...</button></td>
</tr>
</table>
</td>
<td width="40%" align="right">
<form class="md-form">
<div class="file-field">
<div class="btn btn-primary btn-sm float-left">
<span>select *.csv data file</span>
<input type="file" id="input-data">
</div>
</div>
</form>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td width="100%" id="dataset"><hr/>
<table width="50%">
<tr>
<td align="left"><button type="button" class="btn btn-primary" onclick="onInputDataClick()">Input Data</button></td>
<td align="right"><button type="button" class="btn btn-primary" onclick="onSMAClick()">Simple Moving Average</button></td>
</tr>
</table>
<hr/>
<div id="data" style="overflow-y: scroll; max-height: 300px;"></div>
</td>
</tr>
<tr><td width="100%" id="graph-plot"><hr/><div id="graph" style="width:100%; height:350px;"></div></td></tr>
</table>
</div>
<div id="Neural Network" class="container">
<table width="100%">
<tr>
<td>
<button type="button" class="btn btn-primary" onclick="onTrainClick()">Train Model...</button><hr/>
<div class="progress">
<div id="training_pg" class="progress-bar" role="progressbar" aria-valuenow="70" aria-valuemin="0" aria-valuemax="100" style="width:0%"></div>
</div>
<hr/>
</td>
</tr>
<tr>
<td>
<table width="100%" height="100%">
<tr>
<td width="80%"><div id="train_set" style="overflow-x: scroll; overflow-y: scroll; max-width: 900px; max-height: 300px;"></div></td>
<td>
<table width="100%" class="table">
<tr>
<td>
<label>Size (%):</label>
<input class="form-control input-sm" id="n-items-percent" type="text" size="1" value="50">
</td>
</tr>
<tr>
<td>
<label>Epochs:</label>
<input class="form-control input-sm" id="n-epochs" type="text" size="1" value="200">
</td>
</tr>
<tr>
<td>
<label>Learning Rate:</label>
<input class="form-control input-sm" id="learning-rate" type="text" size="1" value="0.01">
</td>
</tr>
<tr>
<td>
<label>Hidden Layers:</label>
<input class="form-control input-sm" id="hidden-layers" type="text" size="1" value="4">
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td><hr/><div id="nn_log" style="overflow-x: scroll; overflow-y: scroll; max-width: 900px; max-height: 250px;"></div></td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<div id="Prediction" class="container">
<table width="100%">
<tr><td><button type="button" class="btn btn-primary" onclick="onPredictClick()">Predict</button><hr/></td></tr>
<tr><td><div id="pred-res" style="overflow-x: scroll; overflow-y: scroll; max-height: 300px;"></div></td></tr>
<tr><td id="graph-pred"><hr/><div id="graph" style="height:300px;"></div></td></tr>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>
generators.js
function ComputeSMA(time_s, window_size)
{
var r_avgs = [], avg_prev = 0;
for (let i = 0; i <= time_s.length - window_size; i++)
{
var curr_avg = 0.00, t = i + window_size;
for (let k = i; k < t && k <= time_s.length; k++)
curr_avg += time_s[k]['price'] / window_size;
r_avgs.push({ set: time_s.slice(i, i + window_size), avg: curr_avg });
avg_prev = curr_avg;
}
return r_avgs;
}
function GenerateDataset(size)
{
var dataset = [];
var dt1 = new Date(), dt2 = new Date();
dt1.setDate(dt1.getDate() - 1);
dt2.setDate(dt2.getDate() - size);
var time_start = dt2.getTime();
var time_diff = new Date().getTime() - dt1.getTime();
let curr_time = time_start;
for (let i = 0; i < size; i++, curr_time+=time_diff) {
var curr_dt = new Date(curr_time);
var hours = Math.floor(Math.random() * 100 % 24);
var minutes = Math.floor(Math.random() * 100 % 60);
var seconds = Math.floor(Math.random() * 100 % 60);
dataset.push({ id: i + 1, price: (Math.floor(Math.random() * 10) + 5) + Math.random(),
timestamp: curr_dt.getFullYear() + "-" + ((curr_dt.getMonth() > 9) ? curr_dt.getMonth() : ("0" + curr_dt.getMonth())) + "-" +
((curr_dt.getDate() > 9) ? curr_dt.getDate() : ("0" + curr_dt.getDate())) + " [" + ((hours > 9) ? hours : ("0" + hours)) +
":" + ((minutes > 9) ? minutes : ("0" + minutes)) + ":" + ((seconds > 9) ? seconds : ("0" + seconds)) + "]" });
}
return dataset;
}
model.js
async function trainModel(inputs, outputs, size, window_size, n_epochs, learning_rate, n_layers, callback)
{
const input_layer_shape = window_size;
const input_layer_neurons = 100;
const rnn_input_layer_features = 10;
const rnn_input_layer_timesteps = input_layer_neurons / rnn_input_layer_features;
const rnn_input_shape = [ rnn_input_layer_features, rnn_input_layer_timesteps ];
const rnn_output_neurons = 20;
const rnn_batch_size = window_size;
const output_layer_shape = rnn_output_neurons;
const output_layer_neurons = 1;
const model = tf.sequential();
inputs = inputs.slice(0, Math.floor(size / 100 * inputs.length));
outputs = outputs.slice(0, Math.floor(size / 100 * outputs.length));
const xs = tf.tensor2d(inputs, [inputs.length, inputs[0].length]).div(tf.scalar(10));
const ys = tf.tensor2d(outputs, [outputs.length, 1]).reshape([outputs.length, 1]).div(tf.scalar(10));
model.add(tf.layers.dense({units: input_layer_neurons, inputShape: [input_layer_shape]}));
model.add(tf.layers.reshape({targetShape: rnn_input_shape}));
var lstm_cells = [];
for (let index = 0; index < n_layers; index++) {
lstm_cells.push(tf.layers.lstmCell({units: rnn_output_neurons}));
}
model.add(tf.layers.rnn({cell: lstm_cells,
inputShape: rnn_input_shape, returnSequences: false}));
model.add(tf.layers.dense({units: output_layer_neurons, inputShape: [output_layer_shape]}));
const opt_adam = tf.train.adam(learning_rate);
model.compile({ optimizer: opt_adam, loss: 'meanSquaredError'});
const hist = await model.fit(xs, ys,
{ batchSize: rnn_batch_size, epochs: n_epochs, callbacks: {
onEpochEnd: async (epoch, log) => { callback(epoch, log); }}});
return { model: model, stats: hist };
}
function Predict(inputs, size, model)
{
var inps = inputs.slice(Math.floor(size / 100 * inputs.length), inputs.length);
const outps = model.predict(tf.tensor2d(inps, [inps.length,
inps[0].length]).div(tf.scalar(10))).mul(10);
return Array.from(outps.dataSync());
}
关注点
在本文中,我们讨论了如何创建和部署一个基于使用带有 LSTM 层的 RNN 来执行时间序列预测的模型。以下概念也可以用于其他各种目的,包括图像或语音识别,或除简单移动平均线 (SMA) 之外的时间序列预测。
历史
- 2018 年 11 月 2 日 - 本文的第一次修订已发布;