接上篇文章,本文继续介绍用于处理间序列的LSTM模型–多变量LSTM模型( Multivariate LSTM Models)。
多变量(多元)时间序列数据是指每个时间步长有多个观测值的数据。对于多元时间序列数据,有两个常用的模型:
一个问题可能有两个或多个并行输入序列和一个依赖于输入序列的输出序列。输入序列是并行的,每个序列在同一时间步上都有一个观测值。即时间序列预测预测任务,通过以往的数据信息,预测之后某段时间内的数据,比如降雨量预测。在这个任务中,有多个输入序列,比如历史的温度、湿度、光照强度、风速、风向、降雨量等,这些不同类型的数据,我们称之为特征(features),这些特征在相同间隔时间点的采样值构成了并行输入序列。该任务的目的是要使用LSTM模型预测降雨量,那么我们可以使用指定 步长(time steps) 和指定窗口宽度的滑动窗口来提取一小段一小段的序列片段作为训练数据 X i X_i Xi,这些序列片段最后一行数据对应的降雨量作为期望的预测输出 y i y_i yi。那么,一个序列数据片段(一个窗口所截取的数据) X 1 X_1 X1 和一个期望输出 y 1 y_1 y1 就称为一个样本(samples),滑动窗口不断滑动截取数据,可以从原始数据序列中提取多个样本,组成训练数据集。
本文通过由两个并行输入序列和一个输出序列的例子来演示,其中输出序列是输入序列的简单相加。代码实现:
in_seq1 = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90])
in_seq2 = np.array([15, 25, 35, 45, 55, 65, 75, 85, 95])
out_seq = np.array([in_seq1[i]+in_seq2[i] for i in range(len(in_seq1))])
dataset = np.hstack((in_seq1, in_seq2, out_seq))
生成的序列数据为:
[[ 10 15 25]
[ 20 25 45]
[ 30 35 65]
[ 40 45 85]
[ 50 55 105]
[ 60 65 125]
[ 70 75 145]
[ 80 85 165]
[ 90 95 185]]
其实在实际的业务中,基本上都是 txt文件或者csv文件,通过pandas来读取,然后转换成DataFrame结构,再做进一步处理。这样看就比较清晰了,每一列代表一个特征,每一行代表这些特征在不同时间点的采样值。
与单变量时间序列一样,我们必须以输入/输出形式将这些数据构造成样本。LSTM模型通过输入输出的上下文关联来学习输入到输出的映射。LSTM支持单变量或特征的并行输入时间序列。因此,我们需要按照一定的窗口宽度来把数据分成样本,划分过程中要保持两个输入序列的顺序,训练过程中不能shuffle!假设我们选择窗口宽度为3,即通过3个先前时间步长的前两个特征的采样值来预测第三个特征一个时间步长的值,划分样本的滑动步长为1,为了增加文章可读性,代码会在文末的完整代码中给出,此处仅贴出划分结果:
[[10 15]
[20 25]
[30 35]] 65
[[20 25]
[30 35]
[40 45]] 85
[[30 35]
[40 45]
[50 55]] 105
[[40 45]
[50 55]
[60 65]] 125
[[50 55]
[60 65]
[70 75]] 145
[[60 65]
[70 75]
[80 85]] 165
[[70 75]
[80 85]
[90 95]] 185
训练样本和训练标签的shape分别为:
(7, 3, 2) (7,)
数据划分好之后,接下来训练LSTM模型。上一篇文章中提到的任何一种LSTM都可以使用,例如Vanilla、Stacked、Bidirectional、CNN或Conv LSTM模型。下面代码使用普通的(Vanilla)LSTM 定义,通过 input_shape
参数为输入层指定滑动窗口所包含的时间步数(窗口宽度)和并行序列的数目(特征数),其中,sw_width为窗口宽度,经过前文分析,本例中为3,n_features表示特征数目,本例中为2。注意:这里的特征数目是训练数据 X i X_i Xi 所包含的特征数目,所需预测的特征 y y y 不包含在内,因此本例中 n_features = 2
,而不是3。
model = Sequential()
model.add(LSTM(50, activation='relu', input_shape=(sw_width, n_features)))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse', metrics=['accuracy'])
定义完模型,需要使用 fit()
方法训练模型,训练完成之后使用 predict()
方法对新的输入数据进行预测。需要注意的是:在进行预测时,测试数据必须重塑成跟训练数据相同的shape,本例中训练集的shape = (7,3,2)
,因此一个测试数据的shape应该是 (1,3,2)
,代码如下:
x_test = array([[80, 85], [90, 95], [100, 105]])
x_test = x_test.reshape((1, sw_width, n_features))
yhat = model.predict(x_test, verbose=0)
交替时间序列问题是存在多个并行时间序列且必须为每个并行时间序列预测一个值的情况。例如,给定上一节中的数据:
[[ 10 15 25]
[ 20 25 45]
[ 30 35 65]
[ 40 45 85]
[ 50 55 105]
[ 60 65 125]
[ 70 75 145]
[ 80 85 165]
[ 90 95 185]]
假设要通过之前的一定时间步长的所有特征的采样值来预测下一个时间步的这些特征的值,这类任务就称为多元预测。假设我们是用之前3个时间步的采样值(滑动窗口宽度为3)作为样本输入数据(X),下一个时间步的采样值作为样本期望输出(y)。那么,可以上以上序列数据划分为:
[[10 15 25]
[20 25 45]
[30 35 65]] [40 45 85]
[[20 25 45]
[30 35 65]
[40 45 85]] [ 50 55 105]
[[ 30 35 65]
[ 40 45 85]
[ 50 55 105]] [ 60 65 125]
[[ 40 45 85]
[ 50 55 105]
[ 60 65 125]] [ 70 75 145]
[[ 50 55 105]
[ 60 65 125]
[ 70 75 145]] [ 80 85 165]
[[ 60 65 125]
[ 70 75 145]
[ 80 85 165]] [ 90 95 185]
这其实是通过函数划分之后输出的结果,具体怎么实现会在下文的代码中给出,先领会样本划分的思想。训练样本和样本标签的shape分别为:
(6, 3, 3) (6, 3)
上一篇文章中的任何一种LSTM都可以使用,例如Vanilla、Stacked、Bidirectional、CNN或Conv LSTM模型。此处使用Stacked LSTM进行演示。注意此处的 n_features = 3
,因为使用了三个特征作为训练数据来预测这三个特征(三列数据)下一个时间步的输出值,这与多输入序列的特征数是不同的。
model = Sequential()
model.add(LSTM(100, activation='relu', return_sequences=True, input_shape=(sw_width,n_features)))
model.add(LSTM(100, activation='relu'))
model.add(Dense(n_features))
model.compile(optimizer='adam', loss='mse')
如果要测试数据,也需要把测试数据的shape重塑成 [1,3,3]
,通过代码实现:
x_input = array([[70,75,145], [80,85,165], [90,95,185]])
x_input = x_input.reshape((1, n_steps, n_features))
yhat = model.predict(x_input, verbose=0)
比较难理解的地方已经做了注释,数据转化过程已经打印输出了,很容易理解。
import numpy as np
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Dense, LSTM, Flatten
def mock_seq(seq1, seq2):
'''
构造虚拟序列数据
实现将多个单列序列数据构造成类似于实际数据的列表的格式
'''
seq1 = np.array(seq1)
seq2 = np.array(seq2)
seq3 = np.array([seq1[i]+seq2[i] for i in range(len(seq1))])
seq1 = seq1.reshape((len(seq1), 1))
seq2 = seq2.reshape((len(seq2), 1))
seq3 = seq3.reshape((len(seq3), 1))
# 对于二维数组,沿第二个维度堆叠,相当于列数增加;可以想象成往书架里一本一本的摆书
dataset = np.hstack((seq1, seq2, seq3))
return dataset
class MultiInputModels:
'''
单变量时间序列LSTM模型
'''
def __init__(self, train_seq, test_seq, sw_width, epochs_num, verbose_set):
'''
初始化变量和参数
'''
self.train_seq = train_seq
self.test_seq = test_seq
self.sw_width = sw_width
self.epochs_num = epochs_num
self.verbose_set = verbose_set
self.X, self.y = [], []
def split_sequence_multi_input(self):
'''
该函数实现多输入序列数据的样本划分
'''
for i in range(len(self.train_seq)):
# 找到最后一个元素的索引,因为for循环中i从1开始,切片索引从0开始,切片区间前闭后开,所以不用减去1;
end_index = i + self.sw_width
# 如果最后一个滑动窗口中的最后一个元素的索引大于序列中最后一个元素的索引则丢弃该样本;
# 这里len(self.sequence)没有减去1的原因是:保证最后一个元素的索引恰好等于序列数据索引时,能够截取到样本;
if end_index > len(self.train_seq) :
break
# 实现以滑动步长为1(因为是for循环),窗口宽度为self.sw_width的滑动步长取值;
# [i:end_index, :-1] 截取第i行到第end_index-1行、除最后一列之外的列的数据;
# [end_index-1, -1] 截取第end_index-1行、最后一列的单个数据,其实是在截取期望预测值y;
seq_x, seq_y = self.train_seq[i:end_index, :-1], self.train_seq[end_index-1, -1]
self.X.append(seq_x)
self.y.append(seq_y)
self.X, self.y = np.array(self.X), np.array(self.y)
self.features = self.X.shape[2]
self.test_seq = self.test_seq.reshape((1, self.sw_width, self.test_seq.shape[1]))
for i in range(len(self.X)):
print(self.X[i], self.y[i])
print('X:\n{}\ny:\n{}\ntest_seq:\n{}\n'.format(self.X, self.y, self.test_seq))
print('X.shape:{}, y.shape:{}, test_seq.shape:{}\n'.format(self.X.shape, self.y.shape, self.test_seq.shape))
return self.X, self.y, self.features, self.test_seq
def split_sequence_parallel(self):
'''
该函数实现多输入序列数据的样本划分
'''
for i in range(len(self.train_seq)):
# 找到最后一个元素的索引,因为for循环中i从1开始,切片索引从0开始,切片区间前闭后开,所以不用减去1;
end_index = i + self.sw_width
# 如果最后一个滑动窗口中的最后一个元素的索引大于序列中最后一个元素的索引则丢弃该样本;
# 这里len(self.sequence)减去1的原因是:保证最后一个元素的索引恰好等于序列数据索引时,能够截取到样本;
if end_index > len(self.train_seq) - 1:
break
# 实现以滑动步长为1(因为是for循环),窗口宽度为self.sw_width的滑动步长取值;
# [i:end_index, :] 截取第i行到第end_index-1行、所有列的数据;
# [end_index-1, :] 截取第end_index行、所有列的数据;
seq_x, seq_y = self.train_seq[i:end_index, :], self.train_seq[end_index, :]
self.X.append(seq_x)
self.y.append(seq_y)
self.X, self.y = np.array(self.X), np.array(self.y)
self.features = self.X.shape[2]
self.test_seq = self.test_seq.reshape((1, self.sw_width, self.test_seq.shape[1]))
for i in range(len(self.X)):
print(self.X[i], self.y[i])
print('X:\n{}\ny:\n{}\ntest_seq:\n{}\n'.format(self.X, self.y, self.test_seq))
print('X.shape:{}, y.shape:{}, test_seq.shape:{}\n'.format(self.X.shape, self.y.shape, self.test_seq.shape))
return self.X, self.y, self.features, self.test_seq
def vanilla_lstm(self):
model = Sequential()
model.add(LSTM(50, activation='relu',
input_shape=(self.sw_width, self.features)))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse', metrics=['accuracy'])
print(model.summary())
history = model.fit(self.X, self.y, epochs=self.epochs_num, verbose=self.verbose_set)
print('\ntrain_acc:%s'%np.mean(history.history['accuracy']), '\ntrain_loss:%s'%np.mean(history.history['loss']))
print('yhat:%s'%(model.predict(self.test_seq)),'\n-----------------------------')
def stacked_lstm(self):
model = Sequential()
model.add(LSTM(100, activation='relu', return_sequences=True,
input_shape=(self.sw_width, self.features)))
model.add(LSTM(100, activation='relu'))
model.add(Dense(self.features))
model.compile(optimizer='adam', loss='mse', metrics=['accuracy'])
history = model.fit(self.X, self.y, epochs=self.epochs_num, verbose=self.verbose_set)
print('\ntrain_acc:%s'%np.mean(history.history['accuracy']), '\ntrain_loss:%s'%np.mean(history.history['loss']))
print('yhat:%s'%(model.predict(self.test_seq)),'\n-----------------------------')
if __name__ == '__main__':
orig_seq1 = [10, 20, 30, 40, 50, 60, 70, 80, 90]
orig_seq2 = [15, 25, 35, 45, 55, 65, 75, 85, 95]
train_seq = mock_seq(orig_seq1, orig_seq2)
test_seq_multi = np.array([[80, 85], [90, 95], [100, 105]])
test_seq_paral = np.array([[70,75,145], [80,85,165], [90,95,185]])
sw_width = 3
epochs_num = 500
verbose_set = 0
print('-----------以下为 【多输入序列LSTM模型】 相关信息-----------------')
MultiInputLSTM = MultiInputModels(train_seq, test_seq_multi, sw_width, epochs_num, verbose_set)
MultiInputLSTM.split_sequence_multi_input()
MultiInputLSTM.vanilla_lstm()
print('-----------以下为 【多并行序列LSTM模型】 相关信息-----------------')
MultiInputLSTM = MultiInputModels(train_seq, test_seq_paral, sw_width, epochs_num, verbose_set)
MultiInputLSTM.split_sequence_parallel()
MultiInputLSTM.stacked_lstm()
-----------以下为 【多输入序列LSTM模型】 相关信息-----------------
[[10 15]
[20 25]
[30 35]] 65
[[20 25]
[30 35]
[40 45]] 85
[[30 35]
[40 45]
[50 55]] 105
[[40 45]
[50 55]
[60 65]] 125
[[50 55]
[60 65]
[70 75]] 145
[[60 65]
[70 75]
[80 85]] 165
[[70 75]
[80 85]
[90 95]] 185
X:
[[[10 15]
[20 25]
[30 35]]
[[20 25]
[30 35]
[40 45]]
[[30 35]
[40 45]
[50 55]]
[[40 45]
[50 55]
[60 65]]
[[50 55]
[60 65]
[70 75]]
[[60 65]
[70 75]
[80 85]]
[[70 75]
[80 85]
[90 95]]]
y:
[ 65 85 105 125 145 165 185]
test_seq:
[[[ 80 85]
[ 90 95]
[100 105]]]
X.shape:(7, 3, 2), y.shape:(7,), test_seq.shape:(1, 3, 2)
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm (LSTM) (None, 50) 10600
_________________________________________________________________
dense (Dense) (None, 1) 51
=================================================================
Total params: 10,651
Trainable params: 10,651
Non-trainable params: 0
_________________________________________________________________
None
train_acc:0.0
train_loss:855.7574371619153
yhat:[[205.53085]]
-----------------------------
-----------以下为 【多并行序列LSTM模型】 相关信息-----------------
[[10 15 25]
[20 25 45]
[30 35 65]] [40 45 85]
[[20 25 45]
[30 35 65]
[40 45 85]] [ 50 55 105]
[[ 30 35 65]
[ 40 45 85]
[ 50 55 105]] [ 60 65 125]
[[ 40 45 85]
[ 50 55 105]
[ 60 65 125]] [ 70 75 145]
[[ 50 55 105]
[ 60 65 125]
[ 70 75 145]] [ 80 85 165]
[[ 60 65 125]
[ 70 75 145]
[ 80 85 165]] [ 90 95 185]
X:
[[[ 10 15 25]
[ 20 25 45]
[ 30 35 65]]
[[ 20 25 45]
[ 30 35 65]
[ 40 45 85]]
[[ 30 35 65]
[ 40 45 85]
[ 50 55 105]]
[[ 40 45 85]
[ 50 55 105]
[ 60 65 125]]
[[ 50 55 105]
[ 60 65 125]
[ 70 75 145]]
[[ 60 65 125]
[ 70 75 145]
[ 80 85 165]]]
y:
[[ 40 45 85]
[ 50 55 105]
[ 60 65 125]
[ 70 75 145]
[ 80 85 165]
[ 90 95 185]]
test_seq:
[[[ 70 75 145]
[ 80 85 165]
[ 90 95 185]]]
X.shape:(6, 3, 3), y.shape:(6, 3), test_seq.shape:(1, 3, 3)
train_acc:0.98333335
train_loss:326.7381566883183
yhat:[[100.013954 105.58289 205.72787 ]]
-----------------------------
至此还有两种模型没有讲,这两种模型将在下一篇文章中介绍。