【时间序列预测/分类】 全系列60篇由浅入深的博文汇总:传送门
文章目录
递归神经网络(RNN)是专门为预测序列数据而设计的,可用于多步时间序列预测。
两篇相关文章介绍 LSTM 模型及其变种处理多步时间序列预测问题。主要包括:
- Vector-Output Vanilla LSTM 模型用于单变量输入数据的多步预测;
- Encoder-Decoder LSTM 模型用于单变量输入数据的多步预测;
- Encoder-Decoder LSTM 模型用于多变量输入数据进行多步预测;
- CNN-LSTM Encoder-Decoder 模型用于单变量输入数据进行多步预测;
- ConvLSTM Encoder-Decoder 模型单变量输入数据的多步预测模型;
本文没有在调整超参数上下功夫,这些内容会在以后的文章介绍。考虑到模型的随机性,应该对给定模型进行多次评估并在测试数据集上统计平均性能。对于给定的多步预测问题,不能确定哪种模型是最有效的。应该进行分析验证,以找到最适合实际业务需求中数据集的方法。
1. 单变量序列输入数据向量输出预测模型
在本节,开发一个普通的LSTM模型(Vanilla LSTM),该模型按日总耗电量的顺序读取数据,并预测下一个标准周每日的总耗电量,预测结果以向量的形式输出。这将为后续章节中开发的更精细的模型提供基础。
1.1 数据处理
使用前七天的数据预测下一周七天的耗电情况。LSTM 模型要求输入数据的shape为:[示例、时间步、功能]([samples, timesteps, features])
。一个样本包含七个时间步的采样值,样本特征数为1,即单变量,预测日总有功功率。训练数据集有159周的数据,因此训练数据集的shape为:[159,7,1]
。
只有159个样本来训练LSTM模型显然是不够的,我们需要扩充数据,即把滑动步长7变为1,如果不明白,请参考之前的文章:传送门。已经讲的很明白,此处不再赘述。
转换完成之后,样本数由原来的159个变为1099个,转换后的数据集的shape为:X=[1099,7,1]
和 y=[1099,7]
。接下来,我们可以在训练数据上定义并拟合LSTM模型。
1.2 LSTM 模型
定义一个具有200个units的单隐层LSTM模型,隐藏层中的units与输入序列中的时间步数无关。 LSTM层之后是有200个节点的全连接层,该层将解释LSTM层学习的特征。最后,输出层将直接预测一个包含七个元素的向量,即一周内每天的总有功功率估计。使用均方误差(mse)损失函数,因为它与RMSE误差度量非常匹配。使用随机梯度下降Adam,训练70个epoch,batch_size设置为16。
1.3 完整代码
其实,中间很多处理过程,在之前的文章中已经介绍过了,此处不再赘述。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 设置中文显示
plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei']
plt.rcParams['axes.unicode_minus'] = False
import math
import sklearn.metrics as skm
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense
def split_dataset(data):
'''
该函数实现以周为单位切分训练数据和测试数据
'''
# data为按天的耗电量统计数据,shape为(1442, 8)
# 测试集取最后一年的46周(322天)数据,剩下的159周(1113天)数据为训练集,以下的切片实现此功能。
train, test = data[1:-328], data[-328:-6]
train = np.array(np.split(train, len(train)/7)) # 将数据划分为按周为单位的数据
test = np.array(np.split(test, len(test)/7))
return train, test
def evaluate_forecasts(actual, predicted):
'''
该函数实现根据预期值评估一个或多个周预测损失
思路:统计所有单日预测的 RMSE
'''
scores = list()
for i in range(actual.shape[1]):
mse = skm.mean_squared_error(actual[:, i], predicted[:, i])
rmse = math.sqrt(mse)
scores.append(rmse)
s = 0 # 计算总的 RMSE
for row in range(actual.shape[0]):
for col in range(actual.shape[1]):
s += (actual[row, col] - predicted[row, col]) ** 2
score = math.sqrt(s / (actual.shape[0] * actual.shape[1]))
print('actual.shape[0]:{}, actual.shape[1]:{}'.format(actual.shape[0], actual.shape[1]))
return score, scores
def summarize_scores(name, score, scores):
s_scores = ', '.join(['%.1f' % s for s in scores])
print('%s: [%.3f] %s\n' % (name, score, s_scores))
def sliding_window(train, sw_width=7, n_out=7, in_start=0):
'''
该函数实现窗口宽度为7、滑动步长为1的滑动窗口截取序列数据
'''
data = train.reshape((train.shape[0] * train.shape[1], train.shape[2])) # 将以周为单位的样本展平为以天为单位的序列
X, y = [], []
for _ in range(len(data)):
in_end = in_start + sw_width
out_end = in_end + n_out
# 保证截取样本完整,最大元素索引不超过原序列索引,则截取数据;否则丢弃该样本
if out_end < len(data):
# 训练数据以滑动步长1截取
train_seq = data[in_start:in_end, 0]
train_seq = train_seq.reshape((len(train_seq), 1))
X.append(train_seq)
y.append(data[in_end:out_end, 0])
in_start += 1
return np.array(X), np.array(y)
def lstm_model(train, sw_width, in_start=0, verbose_set=0, epochs_num=20, batch_size_set=4):
'''
该函数定义 LSTM 模型
单变量
'''
train_x, train_y = sliding_window(train, sw_width, in_start=0)
n_timesteps, n_features, n_outputs = train_x.shape[1], train_x.shape[2], train_y.shape[1]
model = Sequential()
model.add(LSTM(200, activation='relu',
input_shape=(n_timesteps, n_features)))
model.add(Dense(100, activation='relu'))
model.add(Dense(n_outputs))
model.compile(loss='mse', optimizer='adam', metrics=['accuracy'])
print(model.summary())
model.fit(train_x, train_y,
epochs=epochs_num, batch_size=batch_size_set, verbose=verbose_set)
return model
def forecast(model, pred_seq, sw_width):
'''
该函数实现对输入数据的预测
'''
data = np.array(pred_seq)
data = data.reshape((data.shape[0]*data.shape[1], data.shape[2]))
input_x = data[-sw_width:, 0] # 获取输入数据的最后一周的数据
input_x = input_x.reshape((1, len(input_x), 1)) # 重塑形状[1, sw_width, 1]
yhat = model.predict(input_x, verbose=0) # 预测下周数据
yhat = yhat[0] # 获取预测向量
return yhat
def evaluate_model(model, train, test, sd_width):
'''
该函数实现模型评估
'''
history_fore = [x for x in train]
predictions = list() # 用于保存每周的前向验证结果;
for i in range(len(test)):
yhat_sequence = forecast(model, history_fore, sd_width) # 预测下周的数据
predictions.append(yhat_sequence) # 保存预测结果
history_fore.append(test[i, :]) # 得到真实的观察结果并添加到历史中以预测下周
predictions = np.array(predictions) # 评估一周中每天的预测结果
score, scores = evaluate_forecasts(test[:, :, 0], predictions)
return score, scores
def model_plot(score, scores, days, name):
'''
该函数实现绘制RMSE曲线图
'''
plt.figure(figsize=(8,6), dpi=150)
plt.plot(days, scores, marker='o', label=name)
plt.grid(linestyle='--', alpha=0.5)
plt.ylabel(r'$RMSE$', size=15)
plt.title('LSTM 模型预测结果', size=18)
plt.legend()
plt.show()
def main_run(dataset, sw_width, days, name, in_start, verbose, epochs, batch_size):
'''
主函数:数据处理、模型训练流程
'''
# 划分训练集和测试集
train, test = split_dataset(dataset.values)
# 训练模型
model = lstm_model(train, sw_width, in_start, verbose_set=0, epochs_num=20, batch_size_set=4)
# 计算RMSE
score, scores = evaluate_model(model, train, test, sw_width)
# 打印分数
summarize_scores(name, score, scores)
# 绘图
model_plot(score, scores, days, name)
if __name__ == '__main__':
dataset = pd.read_csv('household_power_consumption_days.csv', header=0,
infer_datetime_format=True, engine='c',
parse_dates=['datetime'], index_col=['datetime'])
days = ['sun', 'mon', 'tue', 'wed', 'thr', 'fri', 'sat']
name = 'LSTM'
sliding_window_width= 7
input_sequence_start=0
epochs_num=70
batch_size_set=16
verbose_set=0
main_run(dataset, sliding_window_width, days, name, input_sequence_start,
verbose_set, epochs_num, batch_size_set)
运行示例,可以对模型进行拟合和评估,打印出总体的RMSE和每日的RMSE。可以看到,在这种情况下,该模型与朴素预测相比性能更好,实现了约392千瓦的总体RMSE,比朴素模型所获得的465千瓦少。
Model: "sequential_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm_1 (LSTM) (None, 200) 161600
_________________________________________________________________
dense_2 (Dense) (None, 100) 20100
_________________________________________________________________
dense_3 (Dense) (None, 7) 707
=================================================================
Total params: 182,407
Trainable params: 182,407
Non-trainable params: 0
_________________________________________________________________
None
actual.shape[0]:46, actual.shape[1]:7
LSTM: [392.946] 392.4, 396.6, 382.2, 391.3, 405.8, 322.0, 449.4
还创建了每日的RMSE图。该图显示,与其他几天相比,星期二和星期五可能更容易预测,而标准周结束时的星期六可能是最难预测的日期。
可以将滑动窗口的宽度设置为其他长度重新进行测试,比如14等等。
2. 单变量输入 Encoder-Decoder LSTM
编码器-解码器模型不会直接输出向量序列。该模型将由两个子模型组成:用于读取和编码输入序列的编码器,以及将读取编码的输入序列并对输出序列中的每个元素进行单步预测的解码器。
二者的差异比较小,因为实际上两种方法实际上都可以预测序列输出。重要的区别是解码器中使用了LSTM模型,从而使其既可以知道序列中前一天的预测,又可以在输出序列时积累内部状态。
2.1 编码器解码器 LSTM 模型
与上文一样,定义一个具有200个units的单隐层LSTM模型作为解码器,它读取输入序列并输出200个元素向量(每单位一个输出),该向量捕获输入序列中的特征。使用14天的总功耗作为输入,即滑动窗口的宽度为14。
# 定义解码器
model.add(LSTM(200, activation='relu', input_shape=(n_timesteps, n_features)))
我们将使用一个简单的编码器-解码器架构,该架构易于在Keras中实现,与LSTM自动编码器的架构有很多相似之处。首先,对输入序列的内部表示进行多次重复,对于输出序列中的每个时间步长重复一次。这个向量序列将被提交给LSTM解码器。
# 重复编码
model.add(RepeatVector(7))
解码器定义为具有200个units的单隐层LSTM模型。解码器将输出整个序列,而不仅仅是序列末尾的输出,就像我们对编码器所做的那样。这意味着200个单元中的每一个单元将为七天中的每一天输出一个值。
model.add(LSTM(200, activation='relu', return_sequences=True))
然后,使用一个全连接层解释输出序列中的每个时间步。输出层预测输出序列中的一个步骤,而不是一次七天。这意味着解码器提供的每个时间步将使用相同的全连接层和输出层来处理。为了实现这一点,把解释层和输出层包装在 TimeDistributed 包装器中,该包装器允许被包装的层用于解码器的每个时间步。
# 定义输出模型
model.add(TimeDistributed(Dense(100, activation='relu')))
model.add(TimeDistributed(Dense(1)))
关于本节提到的API,在之前的文章中已经详细介绍过了,此处不再赘述。相关细节请参考:传送门。
本节针对一个特征建模,即每天消耗的总功率。因此,单周预测输出的shape为:[1,7,1]
。因此,在训练模型时,我们必须重构输出数据(y),使其具有三维结构:[samples, timesteps, features]
。
train_y = train_y.reshape((train_y.shape[0], train_y.shape[1], 1))
2.2 完整代码
因为与上一节中的代码有大量重复,这里只需要修改定义的模型和滑动步长以及epoch,其他的都不需要更改。可以封装成类,然后重写模型定义方法。
def lstm_model(train, sw_width, in_start=0, verbose_set=0, epochs_num=20, batch_size_set=4):
'''
该函数定义 Encoder-Decoder LSTM 模型
'''
train_x, train_y = sliding_window(train, sw_width, in_start=0)
n_timesteps, n_features, n_outputs = train_x.shape[1], train_x.shape[2], train_y.shape[1]
train_y = train_y.reshape((train_y.shape[0], train_y.shape[1], 1))
model = Sequential()
model.add(LSTM(200, activation='relu',
input_shape=(n_timesteps, n_features)))
model.add(RepeatVector(n_outputs))
model.add(LSTM(200, activation='relu', return_sequences=True))
model.add(TimeDistributed(Dense(100, activation='relu')))
model.add(TimeDistributed(Dense(1)))
model.compile(loss='mse', optimizer='adam', metrics=['accuracy'])
print(model.summary())
model.fit(train_x, train_y,
epochs=epochs_num, batch_size=batch_size_set, verbose=verbose_set)
return model
输出:
Model: "sequential_5"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm_5 (LSTM) (None, 200) 161600
_________________________________________________________________
repeat_vector_1 (RepeatVecto (None, 7, 200) 0
_________________________________________________________________
lstm_6 (LSTM) (None, 7, 200) 320800
_________________________________________________________________
time_distributed_2 (TimeDist (None, 7, 100) 20100
_________________________________________________________________
time_distributed_3 (TimeDist (None, 7, 1) 101
=================================================================
Total params: 502,601
Trainable params: 502,601
Non-trainable params: 0
_________________________________________________________________
None
actual.shape[0]:46, actual.shape[1]:7
LSTM: [391.213] 377.7, 430.9, 375.7, 376.3, 395.7, 351.0, 424.9
3. 多变量输入 Encoder-Decoder LSTM
3.1 模型定义
在本节中,使用八个时间序列变量来预测下一个标准周的每日总功耗。通过将每个一维时间序列作为单独的输入序列提供给模型来实现这一点。LSTM将依次创建每个输入序列的内部表示,这些输入序列将一起由解码器解释。使用多变量输入有助于解决输出序列是多个不同特征(不只是所预测的特征)的问题。首先,我们必须更新培训数据的准备工作,使之包括所有八个功能,而不仅仅是一个总的每日功耗。
3.2 完整代码
只需要在本文第二章的单变量模型的,基础上,重写以下两个自定义函数即可。为了增加文章可读性,减少冗余,此处不再贴出完整代码,只贴出需要修改的部分。
修改1:
def sliding_window(train, sw_width=7, n_out=7, in_start=0):
'''
该函数实现窗口宽度为7、滑动步长为1的滑动窗口截取序列数据
'''
data = train.reshape((train.shape[0] * train.shape[1], train.shape[2])) # 将以周为单位的样本展平为以天为单位的序列
X, y = [], []
for _ in range(len(data)):
in_end = in_start + sw_width
out_end = in_end + n_out
# 保证截取样本完整,最大元素索引不超过原序列索引,则截取数据;否则丢弃该样本
if out_end < len(data):
# 训练数据以滑动步长1截取
X.append(data[in_start:in_end, :])
y.append(data[in_end:out_end, 0])
in_start += 1
return np.array(X), np.array(y)
修改2:
def forecast(model, pred_seq, sw_width):
'''
该函数实现对输入数据的预测
'''
data = np.array(pred_seq)
data = data.reshape((data.shape[0]*data.shape[1], data.shape[2]))
input_x = data[-sw_width:, :]
input_x = input_x.reshape((1, input_x.shape[0], input_x.shape[1])) # 重塑为[1, n_input, n]
yhat = model.predict(input_x, verbose=0) # 预测下周数据
yhat = yhat[0] # 获取预测向量
return yhat
输出:
Model: "sequential_8"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm_11 (LSTM) (None, 200) 167200
_________________________________________________________________
repeat_vector_4 (RepeatVecto (None, 7, 200) 0
_________________________________________________________________
lstm_12 (LSTM) (None, 7, 200) 320800
_________________________________________________________________
time_distributed_8 (TimeDist (None, 7, 100) 20100
_________________________________________________________________
time_distributed_9 (TimeDist (None, 7, 1) 101
=================================================================
Total params: 508,201
Trainable params: 508,201
Non-trainable params: 0
_________________________________________________________________
None
actual.shape[0]:46, actual.shape[1]:7
LSTM: [410.315] 395.6, 419.7, 386.7, 416.9, 415.4, 365.2, 465.4
下篇文章介绍 CNN-LSTM 和 Conv-LSTM 处理时间序列预测问题。