lstm处理时序数据结构图_时间序列分析(4) RNN/LSTM

b804bdd9538ea245a9aa39a5002930b4.png

1 前言

在上一章中,我们介绍了线性回归,从特征的角度建立了多元线性模型。本章将介绍两个深度学习模型(非线性模型),RNN/LSTM。时间序列分析(3) Linear Regression:

随风:时间序列分析(3) Linear Regression​zhuanlan.zhihu.com
b75470e1ba0285d566dce5e0226ea362.png

语言:python3

数据集:余额宝在2014-03-01~2014-08-31期间每日申购的总金额(数据来自天池大赛)

数据下载地址:https://tianchi.aliyun.com/competition/entrance/231573/information

2 RNN(Recurrent Neural Network)循环神经网络

2.1 RNN 概述

传统神经网络(DNN)无法对时间序列建模,上一层神经元的输出只能传递给下一层神经元。而在循环神经网络(RNN)中,神经元的输出在下一时刻是可以传递给自身的,同时还输出一个藏隐层状态,给当前层在处理下一个样本时使用,它可以看作是带自循环反馈的全连接神经网络。很多任务的时序信息很重要,即一个样本中前后输入的信息是有关联的。样本出现时间顺序信息对语音识别、自然语言处理、视频识别等问题很重要,所以对于这类问题,可以使用 RNN 建模,经典的 RNN 模型结构如图:

9104304c935b9de5ab868892ac12aa0f.png
RNN 模型结构

上图中左边是没有按时序展开的图(与DNN相似),右边是按时序展开的图,我们重点讨论右边的图。

表示
时刻的输入,
表示
时刻模型的隐藏层状态,
表示
时刻的输出,其中
均为
向量
这三个
矩阵是模型的线性映射参数, 它们在整个网络中是共享的可以在时间上共享不同位置的统计强度,这正是体现了 RNN 模型的“循环反馈”的思想。当序列数据中的某些部分在多个位置出现时,这种参数共享机制就显得尤为重要了。例如,识别两个单词 what、how 中的 “w”字母,我们希望模型通过参数共享机制可以学习到字母 “w” 的抽象特征,从而无论这个字母出现在什么位置,模型都能够识别它。

2.2 激活函数

从生物学的角度,对于某一个神经元,并不是在给定激励信号后一定会有输出信号,神经元会选择性地输出,比如对于一个很小的激励,神经元可能不会产生输出信号。在人工神经网络中,为了模拟神经元的这一特性,人们引入了激活函数。

此外,如果不使用激活函数,每一层输出都是上层输入的线性函数,无论神经网络有多少层,输出都是输入的线性组合。激活函数给神经元引入了非线性因素,使得神经网络可以逼近任意非线性函数,这样神经网络就可以应用到众多的非线性模型中。

常见的激活函数有 sigmoid、tanh、relu、softmax:

(1)sigmoid 函数和一阶导数如下:

c407d8329fd9d85f2a2b5427668ffc1d.png
sigmoid函数(左),一阶导数(右)

sigmoid 函数是将取值为(−∞,+∞)的数据映射到(0,1)之间,取 −∞ 的时候映射为 0,取 +∞ 的时候映射为 1,取 0 的时候映射为 0.5。它的一阶导数取值范围为 0~0.25,数据越趋近 0,导数越趋于 0.25,数据越趋近 ∞,导数越趋于 0。

(2)tanh 函数和一阶导数如下:

90c816a5afaedf350b8c04cf63d0b16b.png
tanh函数(左),一阶导数(右)

tanh 函数是将取值为(−∞,+∞)的数据映射到(-1,1)之间,取 −∞ 的时候映射为 -1,取 +∞ 的时候映射为 1,取 0 的时候映射为 0。它的一阶导数取值范围为 0~1,数据越趋近 0,导数越趋于 1,数据越趋近 ∞,导数越趋于 0。相比于 sigmoid 函数,tanh 函数是关于 0 中心对称的。

下图以二维空间为例说明 0 中心对称问题,其中

为模型的初始参数,
为模型最优解。模型走绿色箭头能够最快收敛,如果采用 sigmoid 作为激活函数,则后面的每一层输入均为正,此时模型为了收敛,可能走类似红色折线箭头逼近最优解,收敛速度就会慢很多。

9eab1d0f14e83a1a24c0e67fe4029b28.png
模型参数训练过程

(3)通过上面分析,sigmoid 函数的一阶导数范围为 0~0.25,tanh 函数的一阶导数范围为 0~1,如果输入数据稍微大一点,梯度就会变得非常小,这样在链式求导法则的累乘过程中,整体梯度非常容易趋近于 0,发生梯度消失问题。Relu 函数可以解决深度神经网络中梯度消失的问题。Relu 函数和一阶导数如下:

318d176594699c52dbf7f9cdf301914a.png
relu函数(左),一阶导数(右)

Relu 函数是一种分段函数(注:Relu 函数本身是非线性函数,但是两个分段部分都是线性函数):它弥补了 sigmoid 函数以及 tanh 函数的梯度消失问题(当输入为正,梯度恒为 1;当输入为负,梯度恒为 0)此外相比于 sigmoid 函数或 tanh 函数,它的计算只有线性关系,因此运算速度会快很多。但是 Rule 函数也有问题,首先它在 0 点处是不可导的,通常我们可以采用中间导数代替(0.5),其次当输入为负的时候,神经元不会被激活,造成梯度消失问题。关于 Relu 函数的改进版本也有很多,比如Leaky Relu,但是 Relu 依然是深度学习中使用最广泛的激活函数。

2.3 RNN 前向传播法

表示
时刻的真实值,
表示
时刻的损失函数,
表示
时刻模型的预测值。则
时刻隐藏层状态
为:

,其中
为激活函数,一般选择
为偏置项。

时刻输出
为:

,其中
为偏置项。

最终模型在

时刻的预测值
为:

,其中
为激活函数,通常选择 softmax 函数。

2.4 RNN 反向传播法(BPTT)

BPTT(back-propagation through time)算法是常用的训练 RNN 的方法,其实本质还是BP 算法,只不过 RNN 处理时间序列数据,所以要基于时间反向传播,故叫随时间反向传播。BPTT 的核心思想和 BP 相同,沿着需要优化的参数的负梯度方向不断寻找最优的点。当然这里的 BPTT 和 DNN 中的 BP 也有很大的不同,这里所有的 U、W、V 在序列的各个位置是共享的,反向传播时我们更新的是相同的参数。由于序列在每个时刻都有损失函数,因此最终的损失 L 为:

,因此可以得到 U、W、V 的偏导数,先来看 V 的偏导数:

,U、W比较复杂,下面以
时刻为例。

对于

,如果激活函数选择 sigmoid 或 tanh,它们的一阶导数在 0~1 之间,随着时间序列的不断深入,小数的累乘就会导致梯度越来越小直到接近于 0,这就会引起梯度消失现象。
这里需要指出,由于 tanh 的一阶导数范围更大,且关于 0 中心对称,因此收敛速度要快于 sigmoid,而梯度消失的速度要慢于 sigmoid。如果激活函数选择 Relu,当输入为正时,梯度恒为 1,因此不会出现累乘后梯度消失的问题,但是在累加的过程中,梯度会越来越大,容易引起梯度爆炸的问题;当输入为负时,梯度恒为 0,会产生梯度消失问题。 目前,关于梯度问题并不是仅通过激活函数来解决,通常结合参数初始化、批正则等方法共同解决。

利用 BPTT 算法训练网络时容易出现梯度消失的问题,当序列很长的时候问题尤其严重,因此RNN 模型一般不能直接应用。而较为广泛使用的是 RNN 的一个特例 LSTM。

2.5 RNN 的分类

(1)单隐层 RNN

从输入到输出,只有一层隐藏层,这是最基本的 RNN 模型,上面对于 RNN 的讲解,也是基于单隐层模型。

e20f8bd4882463524f347c0296fdaaa5.png
单隐层 RNN 模型

(2)多隐层 RNN

单隐层 RNN 可以看作既“深”又“浅”的网络。如果我们把 RNN 网络按时间展开,它的时间链路很长,因此可以看作是一个非常深的网络。另一方面,如果只看同一时刻输入到输出(只包含一个隐藏层),网络是非常浅的。增加循环神经网络的深度,主要是增加隐藏层的数量。

259df8c39d00fc8f00a31a8105fdf9d5.png
多隐层 RNN 模型

(3)双向 RNN

在一些任务中,一个时刻的输出不但和序列前面的信息有关,也和序列后面的信息有关。比如给定一个句子,其中一个词的词性由它的上下文决定,即:包含左右两边的信息。因此,在这些任务中,我们可以增加一个按照时间的逆序来传递信息的网络层,来增强网络的能力。

双向循环神经网络(bidirectional recurrent neural network,Bi-RNN)由两层循环神经网络组成,它们的输入相同,只是信息传递的方向不同。

78563a390f75f32b1fc0a5139a1c8948.png
双向 RNN 模型

3 LSTM(Long Short-Term Memory长短期记忆网络

3.1 LSTM 计算模型

LSTM 是一种改进之后的循环神经网络,可以解决 RNN 无法处理长距离的依赖的问题。针对RNN 存在的问题,LSTM 主要有两点改进:

  1. 设置专门的变量
    来存储单元状态(Cell State),从而使网络具有长期记忆。
  2. 引入“门运算”,将梯度中的累乘变为累加,解决梯度消失问题。

在 RNN 模型中,输入为上一时刻隐层状态

,当前时刻输入值
,输出为当前时刻隐层状态
,中间的运算只包含一个激活函数。

c94154c9a1431794a7826bd74b24667b.png
RNN模型

在 LSTM 模型中,输入为上一时刻的隐层状态

,上一时刻的单元状态
,当前时刻输入值
,输出为当前时刻隐层状态
,当前时刻的单元状态
。相比于 RNN 内部的计算要复杂。通常称内部为三个门运算:遗忘门(forget)、输入门(input)、输出门(output),三个门分别用红线框出。

这里解释一下单元状态,以

时刻为例,我们把图中间的绿色框叫做
时刻的单元,包括了输入输出以及内部的运算,它的状态用向量
表示。左右两边的绿色框分别表示
时刻的单元。
注意,它们是同一个单元在不同时刻的实例,并不是多个单元。

72b9cd969c7876febda5e31378966df2.png
LSTM模型

(1)遗忘门

遗忘门用来计算哪些信息需要忘记,通过 sigmoid 处理后为 0 到 1 的值,1 表示全部保留,0 表示全部忘记:

为 sigmoid 函数。注意这里
都是向量。

27b5d050bfdffbbb85d53edece6f222e.png
遗忘门(forget)

(2)输入门

输入门用来计算哪些当前信息需要保存,分为两部分:

第一部分:

的取值范围是 0 到 1,1 表示全部保留, 0 表示全部丢弃。

第二部分:

表示新信息,结合这两部分来创建一个新记忆。

ce2891e4cca76b835b884560e1f72efe.png
输入门(input)


结合遗忘门和输入门,可以计算当前单元状态

。从公式来看,
表示上一时刻需要记忆的信息加上当前时刻需要记忆的信息。可以看到,单元状态
在单元的最上面一条线上流动,它代表了长期记忆,LSTM 正是通过这一点解决了长期记忆问题。

d3d8a68cdf2b87c2f11f4373849cdf00.png
细胞状态更新

(3)输出门

输出门用来计算哪些信息需要输出,输出为隐藏层状态

。从公式来看,
表示对于当前时刻的状态信息
(经过 tanh 变换),按照一定的比例输出。这里
与 RNN 中的
类似,代表短期记忆。

585b8c0aa5bebf2067afc1b206002f4e.png
输出门(output)

最终模型在

时刻的预测值
为:
,其中
为激活函数。LSTM 模型的参数较多,包括
,类比于 RNN,这 8 个参数是共享的。下面以
为例介绍 LSTM 的反向传播。由于序列在每个时刻都有损失函数,因此最终的损失 L 为:
,以
时刻为例。

7f1564c09f18d356f2c86bab9da65270.png
t=2 时刻链式求导

这里看到 LSTM 与 RNN 的区别了吗,出现了很多累加的情况。如果自己推导一下,会发现这里的链式求导法则“并不顺利“,在 RNN 中,基本上是一路连乘顺着写下去,而在 LSTM 中,出现了很多“分支”,每连乘几步,就出现累加的情况,这里说一点

对于累加行为的贡献很大,从公式看它的计算参数都直接或间接与
有关系。随着
的增大,梯度公式越来越深,而累加项也越来越多,正是由于这样的特性,LSTM 解决了 RNN 中梯度消失的问题。

3.2 LSTM 信息流

如下图所示,假设在我们的训练数据中,每个样本 x 是 3*28 维的矩阵,那么将这个样本的每一行当成一个输入,通过 3 个时间步骤展开 LSTM,在每一个 LSTM 单元中,我们的输入是 28*1 的向量,假设我们要求隐藏层的输出是 128*1 的向量输出层的输出是 10*1 的向量。LSTM 单元把 28*1 维的向量映射为 128*1 维的向量,再把 128*1 维的向量映射为 10*1 维的向量。下一个 LSTM 单元会接收上一个单元隐藏层传递的 128*1 维的向量,结合新的 28*1 维的输入向量,连接之后再映射成一个新的隐藏层的 128*1 维的向量,再映射成一个新的输出层的 10*1 维的向量,就这样一直处理下去,直到网络的最后一个 LSTM 单元的隐藏层输出一个 128*1 维的向量,输出层输出一个10*1 维的向量。

8bf6fa97aa7bdb67b6c0059c30f68ee6.png
LSTM输入输出信息流

(1)遗忘门信息流

维度是 128*1,
维度是 28*1, 将
连接起来构成维度是 156*1,
维度是 128*156,
维度是 128*1,所以
维度是 128*1。在遗忘门中,权重矩阵参数个数为

27b5d050bfdffbbb85d53edece6f222e.png
遗忘门(forget)

(2)输入门信息流

参照遗忘门的分析,

维度是 128*156,
维度是 128*1,所以
维度是 128*1。
维度是 128*156,
维度是 128*1,所以
维度是 128*1。在输入门中,权重矩阵参数个数为

ce2891e4cca76b835b884560e1f72efe.png
输入门(input)

更新过程没有参数需要学习。

(3)输出门信息流

参照上面的分析,

维度是 128*156,
维度是 128*1,所以
维度是 128*1。
的维度与
相同,维度是 128*1,所以
维度是 128*1。在输出门中,权重矩阵参数个数为

585b8c0aa5bebf2067afc1b206002f4e.png
输出门(output)

输出门中的

是隐藏层的输出,最后需要将
映射到输出层的输出
的维度是 10*128,
的维度是 10*1,在输出层中,权重矩阵参数个数为

现在我们可以计算 LSTM 的权重矩阵参数个数了, 20096+40192+20096+1290=81674,由于单元的参数共享,所有的数据只会通过一个单元(根据顺序流入一个单元的不同时刻),然后不断更新它的权重。这里给出 LSTM 权重矩阵参数数量的计算公式:

,其中 m 表示隐藏层神经元个数,n 表示输入神经元个数,k 表示输出神经元个数。

3.3 LSTM 的变形

(1)peephole(窥视孔)连接:三个门不但依赖于输入

和上一时刻的隐状态
,也依赖于
上一个时刻的内部记忆细胞状态

3fa882a5c6ab345d99c95ad0d8d217fe.png
peephole 模型

(2)GRU(Gated Recurrent Unit)

输入门与和遗忘门合并成一个门:更新门

引入重置门

,用来控制输入候选状态
的计算是否依赖上一时刻的状态

去除 LSTM 中的内部细胞记忆单元

,直接在当前状态
和历史状态
之间引入线性依赖关系

ecec886d3f05ffc0df9b727627c0a59f.png
GRU 模型

4 LSTM 实战

4.1 LSTM 参数调优

(1)数据准备、预处理

神经网络具有很强的学习能力(也很容易过拟合),更适用于大规模数据集,因此需要准备大量、高质量并且带有干净标签的数据。从激活函数(sigmoid、tanh)可以看出,模型的输出绝对值一般在 0~1 之间,因此需要对数据进行归一化处理。常见的方法有:

1、Min-Max Normalization:

2、Average Normalization:

3、log function:

1、2 属于线性归一化,缺点是当有新数据加入时,可能导致 max 和 min 的变化,需要重新定义。3 属于非线性归一化,经常用在数据分化比较大的场景,有些数值很大,有些很小。

与归一化相近的概念是标准化:

Z-score规范化:

什么时候用归一化?什么时候用标准化?

1、如果对输出结果范围有要求,用归一化。

2、如果数据较为稳定,不存在极端的最大最小值,用归一化。

3、如果数据存在异常值和较多噪音,用标准化,可以间接通过中心化避免异常值和极端值的影响。

(2)批处理

神经网络一般不会一个一个的训练样本,通常采用 minibatch 的方法一次训练一批数据,但不要使用过大的批处理,因为有可能导致低效和过拟合。

(3)梯度归一化、梯度剪裁

因为采用了批处理,因此计算出来梯度之后,要除以 minibatch 的数量。如果训练 RNN 或者LSTM,务必保证 gradient 的 norm 被约束在 5、10、15(前提还是要先归一化gradient),这一点在 RNN 和 LSTM 中很重要。在训练过程中,最好可以检查下梯度。

(4)学习率

学习率是一个非常重要的参数,学习率太大将会导致训练过程非常不稳定甚至失败。太小将影响训练速度,通常设置为 0.1~0.001。

(5)权值初始化

初始化参数对结果的影响至关重要,常见的初始化方法包括:

1、常量初始化:把权值或者偏置初始化为一个自定义的常数。

2、高斯分布初始化:需要给定高斯函数的均值与标准差。

3、xavier 初始化:对于均值为 0,方差为(1 / 输入的个数) 的均匀分布,如果我们更注重前向传播,可以选择 fan_in,即正向传播的输入个数;如果更注重反向传播,可以选择 fan_out, 因为在反向传播的时候,fan_out 就是神经元的输入个数;如果两者都考虑,就选 average = (fan_in + fan_out) /2。对于 Relu 激活函数,xavier 初始化很适合。

在权值初始化的时候,可以多尝试几种方法。此外,如果使用 LSTM 来解决长时依赖的问题,遗忘门初始化 bias 的时候要大一点(大于 1.0)。

(6)dropout

dropout 通过在训练的时候屏蔽部分神经元的方式,使网络最终具有较好的效果,相比于普通训练,需要花费更多的时间。记得在测试的时候关闭 dropout。LSTM 的 dropout 只出现在同一时刻多层隐层之间,不会出现在不同时刻之间(如果 dropout 跨越不同时刻,将导致随时间传递的状态信息丢失)。

(7)提前终止

在训练的过程中,通常训练误差随着时间推移逐渐减小,而验证误差先减小后增大。期望的训练效果是:在训练集和验证集的效果都很好。训练集和验证集的 loss 都在下降,并且差不多在同个地方稳定下来。采用提前终止的方法,可以有效防止过拟合。

b053c9016418bb0a156ef1d85a68b42e.png
训练集、验证集误差

4.2 单时间序列 LSTM

类比于前面介绍的 ARIMA 模型,通过时间序列的先后关系进行预测。我们读取 user_balance_table.csv 文件,采用 Min-Max Normalization 方法对数据做归一化, 将2014-03-01~2014-07-31 的数据作为训练集,将 2014-08-01~2014-08-21 的数据作为验证集,将 2014-08-22~2014-08-31 的数据作为测试集。

import matplotlib.pyplot as plt
import pandas as pd
import tensorflow as tf
from sklearn.preprocessing import MinMaxScaler
import numpy as np


def generate_data():
    dateparse = lambda dates: pd.datetime.strptime(dates, '%Y%m%d')
    user_balance = pd.read_csv('./origin_data/user_balance_table.csv', parse_dates=['report_date'], date_parser=dateparse)
    user_balance.index = user_balance['report_date']

    user_balance = user_balance.groupby(['report_date'])['share_amt', 'total_purchase_amt'].sum()
    user_balance.reset_index(inplace=True)
    user_balance.index = user_balance['report_date']

    user_balance = user_balance['2014-03-01':'2014-08-31']

    data = {'total_purchase_amt': user_balance['total_purchase_amt']}

    df = pd.DataFrame(data=data, index=user_balance.index)
    df.to_csv(path_or_buf='./mid_data/single_purchase_seq.csv')


# 数据集归一化
def get_normal_data(purchase_seq):
    scaler = MinMaxScaler(feature_range=(0, 1))
    scaled_data = scaler.fit_transform(purchase_seq[['total_purchase_amt']])
    scaled_x_data = scaled_data[0: -1]
    scaled_y_data = scaled_data[1:]
    return scaled_x_data, scaled_y_data, scaler


# 构造训练集
def get_train_data(scaled_x_data, scaled_y_data, divide_train_valid_index, time_step):
    train_x, train_y = [], []
    normalized_train_feature = scaled_x_data[0: -divide_train_valid_index]
    normalized_train_label = scaled_y_data[0: -divide_train_valid_index]
    for i in range(len(normalized_train_feature) - time_step + 1):
        train_x.append(normalized_train_feature[i:i + time_step].tolist())
        train_y.append(normalized_train_label[i:i + time_step].tolist())
    return train_x, train_y


# 构造拟合训练集
def get_train_fit_data(scaled_x_data, scaled_y_data, divide_train_valid_index, time_step):
    train_fit_x, train_fit_y = [], []
    normalized_train_feature = scaled_x_data[0: -divide_train_valid_index]
    normalized_train_label = scaled_y_data[0: -divide_train_valid_index]
    train_fit_remain = len(normalized_train_label) % time_step
    train_fit_num = int((len(normalized_train_label) - train_fit_remain) / time_step)
    temp = []
    for i in range(train_fit_num):
        train_fit_x.append(normalized_train_feature[i * time_step:(i + 1) * time_step].tolist())
        temp.extend(normalized_train_label[i * time_step:(i + 1) * time_step].tolist())
    if train_fit_remain > 0:
        train_fit_x.append(normalized_train_feature[-time_step:].tolist())
        temp.extend(normalized_train_label[-train_fit_remain:].tolist())
    for i in temp:
        train_fit_y.append(i[0])
    return train_fit_x, train_fit_y, train_fit_remain


# 构造验证集
def get_valid_data(scaled_x_data, scaled_y_data, divide_train_valid_index, divide_valid_test_index, time_step):
    valid_x, valid_y = [], []
    normalized_valid_feature = scaled_x_data[-divide_train_valid_index: -divide_valid_test_index]
    normalized_valid_label = scaled_y_data[-divide_train_valid_index: -divide_valid_test_index]
    valid_remain = len(normalized_valid_label) % time_step
    valid_num = int((len(normalized_valid_label) - valid_remain) / time_step)
    temp = []
    for i in range(valid_num):
        valid_x.append(normalized_valid_feature[i * time_step:(i + 1) * time_step].tolist())
        temp.extend(normalized_valid_label[i * time_step:(i + 1) * time_step].tolist())
    if valid_remain > 0:
        valid_x.append(normalized_valid_feature[-time_step:].tolist())
        temp.extend(normalized_valid_label[-valid_remain:].tolist())
    for i in temp:
        valid_y.append(i[0])
    return valid_x, valid_y, valid_remain


# 构造测试集
def get_test_data(scaled_x_data, scaled_y_data, divide_valid_test_index, time_step):
    test_x, test_y = [], []
    normalized_test_feature = scaled_x_data[-divide_valid_test_index:]
    normalized_test_label = scaled_y_data[-divide_valid_test_index:]
    test_remain = len(normalized_test_label) % time_step
    test_num = int((len(normalized_test_label) - test_remain) / time_step)
    temp = []
    for i in range(test_num):
        test_x.append(normalized_test_feature[i * time_step:(i + 1) * time_step].tolist())
        temp.extend(normalized_test_label[i * time_step:(i + 1) * time_step].tolist())
    if test_remain > 0:
        test_x.append(scaled_x_data[-time_step:].tolist())
        temp.extend(normalized_test_label[-test_remain:].tolist())
    for i in temp:
        test_y.append(i[0])
    return test_x, test_y, test_remain

generate_data()

训练 LSTM 模型。前面几章介绍的模型,只要参数确定,无论执行多少次结果都一样。不同于前面几章的模型,这里的权值矩阵采用截断高斯初始化,因此每次训练模型产生的结果都不太一样,最后的结果图是经过多次训练,选择了一个比较好的模型。

import matplotlib.pyplot as plt
import pandas as pd
import tensorflow as tf
from sklearn.preprocessing import MinMaxScaler
import numpy as np


# 模型参数
lr = 1e-3  # 学习率
batch_size = 10  # minibatch 大小
rnn_unit = 30  # LSTM 隐藏层神经元数量
input_size = 1  # 单元的输入数量
output_size = 1  # 单元的输出数量
time_step = 15  # 时间长度
epochs = 1000  # 训练次数
gradient_threshold = 15  # 梯度裁剪阈值
stop_loss = np.float32(0.045)  # 训练停止条件。当训练误差 + 验证误差小于阈值时,停止训练
train_keep_prob = [1.0, 0.5, 1.0]  # 训练时 dropout 神经元保留比率

# 数据切分参数
divide_train_valid_index = 30
divide_valid_test_index = 10

# 数据准备
dateparse = lambda dates: pd.datetime.strptime(dates, '%Y-%m-%d')
single_purchase_seq = pd.read_csv('../mid_data/single_purchase_seq.csv', parse_dates=['report_date'], index_col='report_date', date_parser=dateparse)

scaled_x_data, scaled_y_data, scaler = get_normal_data(single_purchase_seq)
train_x, train_y = get_train_data(scaled_x_data, scaled_y_data, divide_train_valid_index, time_step)
train_fit_x, train_fit_y, train_fit_remain = get_train_fit_data(scaled_x_data, scaled_y_data, divide_train_valid_index, time_step)
valid_x, valid_y, valid_remain = get_valid_data(scaled_x_data, scaled_y_data, divide_train_valid_index, divide_valid_test_index, time_step)
test_x, test_y, test_remain = get_test_data(scaled_x_data, scaled_y_data, divide_valid_test_index, time_step)


def lstm(X, keep_prob):
    batch_size = tf.shape(X)[0]  # minibatch 大小

    # 输入到 LSTM 输入的转换,一层全连接的网络,其中权重初始化采用截断的高斯分布,激活函数采用tanh
    weights = tf.Variable(tf.truncated_normal(shape=[input_size, rnn_unit]))
    biases = tf.Variable(tf.constant(0.1, shape=[rnn_unit, ]))
    input = tf.reshape(X, [-1, input_size])

    tanh_layer = tf.nn.tanh(tf.matmul(input, weights) + biases)
    input_rnn = tf.nn.dropout(tanh_layer, keep_prob[0])
    input_rnn = tf.reshape(input_rnn, [-1, time_step, rnn_unit])

    # 两层 LSTM 网络,激活函数默认采用 tanh,当网络层数较深时,建议使用 relu
    initializer = tf.truncated_normal_initializer()
    cell_1 = tf.nn.rnn_cell.LSTMCell(forget_bias=1.0, num_units=rnn_unit, use_peepholes=True, num_proj=None, initializer=initializer, name='lstm_cell_1')
    cell_1_drop = tf.nn.rnn_cell.DropoutWrapper(cell=cell_1, output_keep_prob=keep_prob[1])

    cell_2 = tf.nn.rnn_cell.LSTMCell(forget_bias=1.0, num_units=rnn_unit, use_peepholes=True, num_proj=output_size, initializer=initializer, name='lstm_cell_2')
    cell_2_drop = tf.nn.rnn_cell.DropoutWrapper(cell=cell_2, output_keep_prob=keep_prob[2])

    mutilstm_cell = tf.nn.rnn_cell.MultiRNNCell(cells=[cell_1_drop, cell_2_drop], state_is_tuple=True)
    init_state = mutilstm_cell.zero_state(batch_size, dtype=tf.float32)

    with tf.variable_scope('lstm', reuse=tf.AUTO_REUSE):
        output, state = tf.nn.dynamic_rnn(cell=mutilstm_cell, inputs=input_rnn, initial_state=init_state, dtype=tf.float32)

    return output, state

# 获取拟合数据,这里用于拟合,关闭 dropout
def get_fit_seq(x, remain, sess, output, X, keep_prob, scaler, inverse):
    fit_seq = []
    if inverse:
        # 前面对数据进行了归一化,这里反归一化还原数据
        temp = []
        for i in range(len(x)):
            next_seq = sess.run(output, feed_dict={X: [x[i]], keep_prob: [1.0, 1.0, 1.0]})
            if i == len(x) - 1:
                temp.extend(scaler.inverse_transform(next_seq[0].reshape(-1, 1))[-remain:])
            else:
                temp.extend(scaler.inverse_transform(next_seq[0].reshape(-1, 1)))
        for i in temp:
            fit_seq.append(i[0])
    else:
        for i in range(len(x)):
            next_seq = sess.run(output,
                                feed_dict={X: [x[i]], keep_prob: [1.0, 1.0, 1.0]})
            if i == len(x) - 1:
                fit_seq.extend(next_seq[0].reshape(1, -1).tolist()[0][-remain:])
            else:
                fit_seq.extend(next_seq[0].reshape(1, -1).tolist()[0])

    return fit_seq


def train_lstm():
    X = tf.placeholder(tf.float32, [None, time_step, input_size])
    Y = tf.placeholder(tf.float32, [None, time_step, output_size])

    keep_prob = tf.placeholder(tf.float32, [None])
    output, state = lstm(X, keep_prob)
    loss = tf.losses.mean_squared_error(tf.reshape(output, [-1]), tf.reshape(Y, [-1]))

    # 梯度优化与裁剪
    optimizer = tf.train.AdamOptimizer(learning_rate=lr)
    grads, variables = zip(*optimizer.compute_gradients(loss))
    grads, global_norm = tf.clip_by_global_norm(grads, gradient_threshold)
    train_op = optimizer.apply_gradients(zip(grads, variables))

    X_train_fit = tf.placeholder(tf.float32, [None])
    Y_train_fit = tf.placeholder(tf.float32, [None])
    train_fit_loss = tf.losses.mean_squared_error(tf.reshape(X_train_fit, [-1]), tf.reshape(Y_train_fit, [-1]))

    X_valid = tf.placeholder(tf.float32, [None])
    Y_valid = tf.placeholder(tf.float32, [None])
    valid_fit_loss = tf.losses.mean_squared_error(tf.reshape(X_valid, [-1]), tf.reshape(Y_valid, [-1]))

    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        fit_loss_seq = []
        valid_loss_seq = []

        for epoch in range(epochs):
            for index in range(len(train_x) - batch_size + 1):
               sess.run(train_op, feed_dict={X: train_x[index: index + batch_size], Y: train_y[index: index + batch_size], keep_prob: train_keep_prob})
                
            # 拟合训练集和验证集
            train_fit_seq = get_fit_seq(train_fit_x, train_fit_remain, sess, output, X, keep_prob, scaler, False)
            train_loss = sess.run(train_fit_loss, {X_train_fit: train_fit_seq, Y_train_fit: train_fit_y})
            fit_loss_seq.append(train_loss)

            valid_seq = get_fit_seq(valid_x, valid_remain, sess, output, X, keep_prob, scaler, False)
            valid_loss = sess.run(valid_fit_loss, {X_valid: valid_seq, Y_valid: valid_y})
            valid_loss_seq.append(valid_loss)

            print('epoch:', epoch + 1, 'fit loss:', train_loss, 'valid loss:', valid_loss)
            
            # 提前终止条件。
            # 常见的方法是验证集达到最小值,再往后训练 n 步,loss 不再减小,实际测试这里使用的效果不好。
            # 这里选择 stop_loss 是经过多次尝试得到的阈值。
            if train_loss + valid_loss <= stop_loss:
                train_fit_seq = get_fit_seq(train_fit_x, train_fit_remain, sess, output, X, keep_prob, scaler, True)
                valid_fit_seq = get_fit_seq(valid_x, valid_remain, sess, output, X, keep_prob, scaler, True)
                test_fit_seq = get_fit_seq(test_x, test_remain, sess, output, X, keep_prob, scaler, True)
                print('best epoch: ', epoch + 1)
                break


    return fit_loss_seq, valid_loss_seq, train_fit_seq, valid_fit_seq, test_fit_seq


fit_loss_seq, valid_loss_seq, train_fit_seq, valid_fit_seq, test_fit_seq = train_lstm()

# 切分训练集、测试集
purchase_seq_train = single_purchase_seq[1:-divide_train_valid_index]
purchase_seq_valid = single_purchase_seq[-divide_train_valid_index:-divide_valid_test_index]
purchase_seq_test = single_purchase_seq[-divide_valid_test_index:]

plt.figure(figsize=(18, 12))

plt.subplot(221)
plt.title('loss')
plt.plot(fit_loss_seq, label='fit_loss', color='blue')
plt.plot(valid_loss_seq, label='valid_loss', color='red')
plt.legend(loc='best')

plt.subplot(222)
plt.title('train')
seq_train_fit = pd.DataFrame(columns=['total_purchase_amt'], data=train_fit_seq, index=purchase_seq_train.index)
plt.plot(purchase_seq_train['total_purchase_amt'], label='value', color='blue')
plt.plot(seq_train_fit['total_purchase_amt'], label='fit_value', color='red')
plt.legend(loc='best')

plt.subplot(223)
plt.title('valid')
seq_valid_fit = pd.DataFrame(columns=['total_purchase_amt'], data=valid_fit_seq, index=purchase_seq_valid.index)
plt.plot(purchase_seq_valid['total_purchase_amt'], label='value', color='blue')
plt.plot(seq_valid_fit['total_purchase_amt'], label='fit_value', color='red')
plt.legend(loc='best')

plt.subplot(224)
plt.title('test')
seq_test_fit = pd.DataFrame(columns=['total_purchase_amt'], data=test_fit_seq, index=purchase_seq_test.index)
plt.plot(purchase_seq_test['total_purchase_amt'], label='value', color='blue')
plt.plot(seq_test_fit['total_purchase_amt'], label='fit_value', color='red')
plt.legend(loc='best')

plt.show()

3e9296b72afe3141b15714a6f71b61af.png
单时间序列 LSTM 效果

4.3 多时间序列 LSTM

类比于前面介绍的多元回归模型,通过不同时间序列之间的关系进行预测。选取的特征序列:share_amt(余额宝今日收益)、mfd_daily_yield(余额宝万份收益)、mfd_7daily_yield(余额宝七日年化收益率)、Interest_O_N(银行隔夜利率)、Interest_1_W(银行1周利率)、Interest_1_M(银行1个月利率)。这些特征列存在于 user_balance_table.csv、mfd_day_share_interest.csv、mfd_bank_shibor 这三个文件中。

我们读取这三个文件,采用 Min-Max Normalization 方法对数据做归一化, 将2014-03-01~2014-07-31 的数据作为训练集,将 2014-08-01~2014-08-21 的数据作为验证集,将 2014-08-22~2014-08-31 的数据作为测试集。

import matplotlib.pyplot as plt
import pandas as pd
import tensorflow as tf
from sklearn.preprocessing import MinMaxScaler
import numpy as np


def generate_data():
    dateparse = lambda dates: pd.datetime.strptime(dates, '%Y%m%d')
    user_balance = pd.read_csv('../origin_data/user_balance_table.csv', parse_dates=['report_date'],
                               date_parser=dateparse)
    user_balance.index = user_balance['report_date']

    mfd_day_share_interest = pd.read_csv('../origin_data/mfd_day_share_interest.csv', parse_dates=['mfd_date'],
                                         date_parser=dateparse)
    mfd_day_share_interest.index = mfd_day_share_interest['mfd_date']

    mfd_bank_shibor = pd.read_csv('../origin_data/mfd_bank_shibor.csv', parse_dates=['mfd_date'], date_parser=dateparse)
    mfd_bank_shibor.index = mfd_bank_shibor['mfd_date']

    user_balance = user_balance.groupby(['report_date'])['share_amt', 'total_purchase_amt'].sum()
    user_balance.reset_index(inplace=True)
    user_balance.index = user_balance['report_date']

    user_balance = user_balance['2014-03-01':'2014-08-31']
    mfd_day_share_interest = mfd_day_share_interest['2014-03-01':'2014-08-31']
    mfd_bank_shibor = mfd_bank_shibor['2014-03-01':'2014-08-31']

    data = {'share_amt': user_balance['share_amt'],
            'total_purchase_amt': user_balance['total_purchase_amt'],
            'mfd_daily_yield': mfd_day_share_interest['mfd_daily_yield'],
            'mfd_7daily_yield': mfd_day_share_interest['mfd_7daily_yield'],
            'Interest_O_N': mfd_bank_shibor['Interest_O_N'],
            'Interest_1_W': mfd_bank_shibor['Interest_1_W'],
            'Interest_1_M': mfd_bank_shibor['Interest_1_M']}

    df = pd.DataFrame(data=data, index=user_balance.index)

    df['Interest_O_N'].fillna(df['Interest_O_N'].median(), inplace=True)
    df['Interest_1_W'].fillna(df['Interest_1_W'].median(), inplace=True)
    df['Interest_1_M'].fillna(df['Interest_1_M'].median(), inplace=True)

    df.to_csv(path_or_buf='../mid_data/multi_purchase_seq.csv')


# 数据集归一化
def get_normal_data(purchase_seq):
    scaler_x = MinMaxScaler(feature_range=(0, 1))
    scaler_y = MinMaxScaler(feature_range=(0, 1))
    scaled_x_data = scaler_x.fit_transform(purchase_seq[ ['Interest_1_M', 'Interest_1_W', 'Interest_O_N', 'mfd_7daily_yield', 'mfd_daily_yield', 'share_amt']])
    scaled_y_data = scaler_y.fit_transform(purchase_seq[['total_purchase_amt']])
    return scaled_x_data, scaled_y_data, scaler_y


# 构造训练集
def get_train_data(scaled_x_data, scaled_y_data, divide_train_valid_index, time_step):
    train_x, train_y = [], []
    normalized_train_feature = scaled_x_data[0: -divide_train_valid_index]
    normalized_train_label = scaled_y_data[0: -divide_train_valid_index]
    for i in range(len(normalized_train_feature) - time_step + 1):
        train_x.append(normalized_train_feature[i:i + time_step].tolist())
        train_y.append(normalized_train_label[i:i + time_step].tolist())
    return train_x, train_y


# 构造拟合训练集
def get_train_fit_data(scaled_x_data, scaled_y_data, divide_train_valid_index, time_step):
    train_fit_x, train_fit_y = [], []
    normalized_train_feature = scaled_x_data[0: -divide_train_valid_index]
    normalized_train_label = scaled_y_data[0: -divide_train_valid_index]
    train_fit_remain = len(normalized_train_label) % time_step
    train_fit_num = int((len(normalized_train_label) - train_fit_remain) / time_step)
    temp = []
    for i in range(train_fit_num):
        train_fit_x.append(normalized_train_feature[i * time_step:(i + 1) * time_step].tolist())
        temp.extend(normalized_train_label[i * time_step:(i + 1) * time_step].tolist())
    if train_fit_remain > 0:
        train_fit_x.append(normalized_train_feature[-time_step:].tolist())
        temp.extend(normalized_train_label[-train_fit_remain:].tolist())
    for i in temp:
        train_fit_y.append(i[0])
    return train_fit_x, train_fit_y, train_fit_remain


# 构造验证集
def get_valid_data(scaled_x_data, scaled_y_data, divide_train_valid_index, divide_valid_test_index, time_step):
    valid_x, valid_y = [], []
    normalized_valid_feature = scaled_x_data[-divide_train_valid_index: -divide_valid_test_index]
    normalized_valid_label = scaled_y_data[-divide_train_valid_index: -divide_valid_test_index]
    valid_remain = len(normalized_valid_label) % time_step
    valid_num = int((len(normalized_valid_label) - valid_remain) / time_step)
    temp = []
    for i in range(valid_num):
        valid_x.append(normalized_valid_feature[i * time_step:(i + 1) * time_step].tolist())
        temp.extend(normalized_valid_label[i * time_step:(i + 1) * time_step].tolist())
    if valid_remain > 0:
        valid_x.append(normalized_valid_feature[-time_step:].tolist())
        temp.extend(normalized_valid_label[-valid_remain:].tolist())
    for i in temp:
        valid_y.append(i[0])
    return valid_x, valid_y, valid_remain


# 构造测试集
def get_test_data(scaled_x_data, scaled_y_data, divide_valid_test_index, time_step):
    test_x, test_y = [], []
    normalized_test_feature = scaled_x_data[-divide_valid_test_index:]
    normalized_test_label = scaled_y_data[-divide_valid_test_index:]
    test_remain = len(normalized_test_label) % time_step
    test_num = int((len(normalized_test_label) - test_remain) / time_step)
    temp = []
    for i in range(test_num):
        test_x.append(normalized_test_feature[i * time_step:(i + 1) * time_step].tolist())
        temp.extend(normalized_test_label[i * time_step:(i + 1) * time_step].tolist())
    if test_remain > 0:
        test_x.append(scaled_x_data[-time_step:].tolist())
        temp.extend(normalized_test_label[-test_remain:].tolist())
    for i in temp:
        test_y.append(i[0])
    return test_x, test_y, test_remain
 
generate_data()

训练 LSTM 模型。大体流程与单时间序列 LSTM 一致,只是这里的输入序列变成了 6 个,因为输入的变化,相应模型参数的取值也发生了变化。

import matplotlib.pyplot as plt
import pandas as pd
import tensorflow as tf
from sklearn.preprocessing import MinMaxScaler
import numpy as np


# 模型参数
lr = 1e-3  # 学习率
batch_size = 10  # minibatch大小
rnn_unit = 40  # LSTM 隐藏层神经元数量
input_size = 6  # 单元的输入数量
output_size = 1  # 单元的输出数量
time_step = 15  # 时间长度
epochs = 1000  # 训练次数
gradient_threshold = 5  # 梯度裁剪阈值
stop_loss = np.float32(0.025)  # 训练停止条件。当训练误差 + 验证误差小于阈值时,停止训练
train_keep_prob = [1.0, 0.5, 1.0]  # 训练时 dropout 神经元保留比率

# 数据切分参数
divide_train_valid_index = 31
divide_valid_test_index = 10

# 数据准备
dateparse = lambda dates: pd.datetime.strptime(dates, '%Y-%m-%d')
purchase_seq = pd.read_csv('../mid_data/multi_purchase_seq.csv', parse_dates=['report_date'], index_col='report_date', date_parser=dateparse)

scaled_x_data, scaled_y_data, scaler_y = get_normal_data(purchase_seq)
train_x, train_y = get_train_data(scaled_x_data, scaled_y_data, divide_train_valid_index, time_step)
train_fit_x, train_fit_y, train_fit_remain = get_train_fit_data(scaled_x_data, scaled_y_data, divide_train_valid_index, time_step)
valid_x, valid_y, valid_remain = get_valid_data(scaled_x_data, scaled_y_data, divide_train_valid_index, divide_valid_test_index, time_step)
test_x, test_y, test_remain = get_test_data(scaled_x_data, scaled_y_data, divide_valid_test_index, time_step)


def lstm(X, keep_prob):
    batch_size = tf.shape(X)[0]  # minibatch 大小

    # 输入到 LSTM 输入的转换,一层全连接的网络,其中权重初始化采用截断的高斯分布,激活函数采用tanh
    weights = tf.Variable(tf.truncated_normal(shape=[input_size, rnn_unit]))
    biases = tf.Variable(tf.constant(0.1, shape=[rnn_unit, ]))
    input = tf.reshape(X, [-1, input_size])

    input_layer = tf.nn.tanh(tf.matmul(input, weights) + biases)
    input_rnn = tf.nn.dropout(input_layer, keep_prob[0])
    input_rnn = tf.reshape(input_rnn, [-1, time_step, rnn_unit])

    # 两层 LSTM 网络,激活函数默认采用 tanh,当网络层数较深时,建议使用 relu
    initializer = tf.truncated_normal_initializer()
    cell_1 = tf.nn.rnn_cell.LSTMCell(forget_bias=1.0, num_units=rnn_unit, use_peepholes=True, num_proj=None, initializer=initializer, name='lstm_cell_1')
    cell_1_drop = tf.nn.rnn_cell.DropoutWrapper(cell=cell_1, output_keep_prob=keep_prob[1])

    cell_2 = tf.nn.rnn_cell.LSTMCell(forget_bias=1.0, num_units=rnn_unit, use_peepholes=True, num_proj=output_size, initializer=initializer, name='lstm_cell_2')
    cell_2_drop = tf.nn.rnn_cell.DropoutWrapper(cell=cell_2, output_keep_prob=keep_prob[2])

    mutilstm_cell = tf.nn.rnn_cell.MultiRNNCell(cells=[cell_1_drop, cell_2_drop], state_is_tuple=True)
    init_state = mutilstm_cell.zero_state(batch_size, dtype=tf.float32)

    with tf.variable_scope('lstm', reuse=tf.AUTO_REUSE):
        output, state = tf.nn.dynamic_rnn(cell=mutilstm_cell, inputs=input_rnn, initial_state=init_state, dtype=tf.float32)

    return output, state

# 获取拟合数据,这里用于拟合,关闭 dropout
def get_fit_seq(x, remain, sess, output, X, keep_prob, scaler, inverse):
    fit_seq = []
    if inverse:
        # 前面对数据进行了归一化,这里反归一化还原数据
        temp = []
        for i in range(len(x)):
            next_seq = sess.run(output, feed_dict={X: [x[i]], keep_prob: [1.0, 1.0, 1.0]})
            if i == len(x) - 1:
                temp.extend(scaler.inverse_transform(next_seq[0].reshape(-1, 1))[-remain:])
            else:
                temp.extend(scaler.inverse_transform(next_seq[0].reshape(-1, 1)))
        for i in temp:
            fit_seq.append(i[0])
    else:
        for i in range(len(x)):
            next_seq = sess.run(output,
                                feed_dict={X: [x[i]], keep_prob: [1.0, 1.0, 1.0]})
            if i == len(x) - 1:
                fit_seq.extend(next_seq[0].reshape(1, -1).tolist()[0][-remain:])
            else:
                fit_seq.extend(next_seq[0].reshape(1, -1).tolist()[0])

    return fit_seq


def train_lstm():
    X = tf.placeholder(tf.float32, [None, time_step, input_size])
    Y = tf.placeholder(tf.float32, [None, time_step, output_size])

    keep_prob = tf.placeholder(tf.float32, [None])
    output, state = lstm(X, keep_prob)
    loss = tf.losses.mean_squared_error(tf.reshape(output, [-1]), tf.reshape(Y, [-1]))

    # 梯度优化与裁剪
    optimizer = tf.train.AdamOptimizer(learning_rate=lr)
    grads, variables = zip(*optimizer.compute_gradients(loss))
    grads, global_norm = tf.clip_by_global_norm(grads, gradient_threshold)
    train_op = optimizer.apply_gradients(zip(grads, variables))

    X_train_fit = tf.placeholder(tf.float32, [None])
    Y_train_fit = tf.placeholder(tf.float32, [None])
    train_fit_loss = tf.losses.mean_squared_error(tf.reshape(X_train_fit, [-1]), tf.reshape(Y_train_fit, [-1]))

    X_valid = tf.placeholder(tf.float32, [None])
    Y_valid = tf.placeholder(tf.float32, [None])
    valid_fit_loss = tf.losses.mean_squared_error(tf.reshape(X_valid, [-1]), tf.reshape(Y_valid, [-1]))

    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        fit_loss_seq = []
        valid_loss_seq = []

        for epoch in range(epochs):
            for index in range(len(train_x) - batch_size + 1):
               sess.run(train_op, feed_dict={X: train_x[index: index + batch_size], Y: train_y[index: index + batch_size], keep_prob: train_keep_prob})

            # 拟合训练集和验证集
            train_fit_seq = get_fit_seq(train_fit_x, train_fit_remain, sess, output, X, keep_prob, scaler_y, False)
            train_loss = sess.run(train_fit_loss, {X_train_fit: train_fit_seq, Y_train_fit: train_fit_y})
            fit_loss_seq.append(train_loss)

            valid_seq = get_fit_seq(valid_x, valid_remain, sess, output, X, keep_prob, scaler_y, False)
            valid_loss = sess.run(valid_fit_loss, {X_valid: valid_seq, Y_valid: valid_y})
            valid_loss_seq.append(valid_loss)

            print('epoch:', epoch + 1, 'fit loss:', train_loss, 'valid loss:', valid_loss)

            # 提前终止条件。
            # 常见的方法是验证集达到最小值,再往后训练 n 步,loss 不再减小,实际测试这里使用的效果不好。
            # 这里选择 stop_loss 是经过多次尝试得到的阈值。
            if train_loss + valid_loss <= stop_loss:
                train_fit_seq = get_fit_seq(train_fit_x, train_fit_remain, sess, output, X, keep_prob, scaler_y, True)
                valid_fit_seq = get_fit_seq(valid_x, valid_remain, sess, output, X, keep_prob, scaler_y, True)
                test_fit_seq = get_fit_seq(test_x, test_remain, sess, output, X, keep_prob, scaler_y, True)
                print('best epoch: ', epoch + 1)
                break

    return fit_loss_seq, valid_loss_seq, train_fit_seq, valid_fit_seq, test_fit_seq


fit_loss_seq, valid_loss_seq, train_fit_seq, valid_fit_seq, test_fit_seq = train_lstm()


# 切分训练集、验证集、测试集
purchase_seq_train = purchase_seq[0:-divide_train_valid_index]
purchase_seq_valid = purchase_seq[-divide_train_valid_index:-divide_valid_test_index]
purchase_seq_test = purchase_seq[-divide_valid_test_index:]

plt.figure(figsize=(18, 12))

plt.subplot(221)
plt.title('loss')
plt.plot(fit_loss_seq, label='fit_loss', color='blue')
plt.plot(valid_loss_seq, label='valid_loss', color='red')
plt.legend(loc='best')

plt.subplot(222)
plt.title('train')
seq_train_fit = pd.DataFrame(columns=['total_purchase_amt'], data=train_fit_seq, index=purchase_seq_train.index)
plt.plot(purchase_seq_train['total_purchase_amt'], label='value', color='blue')
plt.plot(seq_train_fit['total_purchase_amt'], label='fit_value', color='red')
plt.legend(loc='best')

plt.subplot(223)
plt.title('valid')
seq_valid_fit = pd.DataFrame(columns=['total_purchase_amt'], data=valid_fit_seq, index=purchase_seq_valid.index)
plt.plot(purchase_seq_valid['total_purchase_amt'], label='value', color='blue')
plt.plot(seq_valid_fit['total_purchase_amt'], label='fit_value', color='red')
plt.legend(loc='best')

plt.subplot(224)
plt.title('test')
seq_test_fit = pd.DataFrame(columns=['total_purchase_amt'], data=test_fit_seq, index=purchase_seq_test.index)
plt.plot(purchase_seq_test['total_purchase_amt'], label='value', color='blue')
plt.plot(seq_test_fit['total_purchase_amt'], label='fit_value', color='red')
plt.legend(loc='best')

plt.show()

3e2847d915808971d763e167cd284aaf.png
多时间序列 LSTM 效果

由于数据集的限制,这里并没有选择与目标序列相关性最强的特征序列(例如股票序列)。显然,更好的特征序列对模型提升影响极大。另一点需要指出,假如今天是 8 月 21 号,你需要对 8 月 22 号到 31 号的数据做预测,你也并不知道特征序列的取值,也就无法预测。

5 总结

5.1 线性与非线性模型应用于同一时间序列

首先来比较一下 ARIMA、单时间序列 LSTM,ARIMA 从线性自相关的角度进行建模,单时间序列 LSTM 从非线性自相关的角度进行建模。再来比较一下多元 LR、多时间序列 LSTM,多元 LR 从线性互相关的角度进行建模,多时间序列 LSTM 从非线性互相关的角度进行建模。这里有个疑问,为什么我们既可以通过线性相关建模,又可以通过非线性相关建模?

先要明确模型的意义,模型建立的过程就是不断趋近样本函数的过程。例如真实样本函数是直线

,显然我们通过线性函数可以完全拟合,如果我们采用非线性函数
,也是可以拟合的。这里有泰勒公式做支撑
,在约束范围内,
的高次方趋于零。其次,激活函数 sigmoid、tanh 在 0 附近一个很小的范围内是线性的(当然,我们不希望使用这一部分),如果恰好在这一部分激活,激活函数可以看作是线性层。最后,现实中的序列很少只存在线性关系或者只存在非线性关系,如果可以使用线性模型搞定的事情,我们当然不希望使用像 LSTM 这样复杂的模型。

03066742a2cc12f175bf18fe22a151be.png
函数图像

5.2 初始化与正则

混沌原理告诉我们,对于系统初始态任何一个微小的改变,都将导致系统朝着一个不可知的方向发展。在深度学习中,我们通常采用随机初始化来定义权重矩阵,即使在本章中采用截断高斯初始化,每一次初始化的参数也不会相同,这导致每一次训练的模型也会有差别。在多次运行的过程中,确实有极低的概率导致模型最后不收敛。正则是在初始化的基础上,引导模型往好的方向发展,这里通过梯度裁剪、dropout、学习率、提前终止四种方式对模型做了约束。其中学习率、梯度裁剪控制模型的变化程度,如果发现在训练的过程中 loss 跳变比较严重,则需要减小变化量。dropout 用于提升模型的泛化能力。提前终止是为了找到拟合与泛化最佳的平衡点。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值