译自How to Develop LSTM Models for Multi-Step Time Series Forecasting of Household Power Consumption~
随着智能电表的兴起和太阳能电池板等发电技术的广泛应用,有大量可用的用电数据。这些数据代表了一系列与电力相关的多元时间序列,进而可以用来建模甚至预测未来的用电量。
与其他机器学习算法不同,长短时记忆循环神经网络能够从序列数据中自动学习特征,支持多变量数据,并能输出可用于多步预测的变长序列。
在本教程中,您将了解如何开发长期短期记忆循环神经网络,用于多步骤时间序列预测家庭用电量。
完成本教程后,您将知道:
- 如何开发和评估用于多步时间序列预测的单变量和多变量编解码器LSTMs。
- 如何建立和评价一个用于多步时间序列预测的CNN-LSTM编解码器模型。
- 如何建立和评价用于多步时间序列预测的ConvLSTM编解码器模型。
让我们开始吧。
0 教程概述
本教程分为九个部分;它们是:
- 问题描述
- 加载和准备数据集
- 模型评价
- 用于多步预测的LSTMs
- 具有单变量输入和向量输出的LSTM模型
- 单变量输入的编解码器LSTM模型
- 多变量输入的LSTM模型
- 具有单变量输入的CNN-LSTM编解码器模型
- 单变量输入的ConvLSTM编解码器模型
1 问题描述
“家庭用电量”数据集是一个多元时间序列数据集,描述一个家庭四年以上的用电量。这些数据是在2006年12月至2010年11月期间收集的,每分钟收集一次对家庭用电情况的观察。
它是由7个变量(除日期和时间外)组成的多元系列;它们是:
- global_active_power:家庭消耗的总有功功率(千瓦)。
- global_reactive_power:家庭消耗的总无功功率(千瓦)。
- voltage:平均电压(伏特)。
- global_intensity:平均电流强度(amps)。
- sub_metering_1:厨房用的有功能(有功能的瓦特小时)。
- sub_metering_2:洗衣用的有功能(有功能的瓦特小时)。
- sub_metering_3:用于气候控制系统的有功能(有功能瓦特小时)。
有功和无功是指交流电流的技术细节。
通过从总有功功率中减去定义的三个子计量变量之和,可以得到第四个子计量变量:
sub_metering_remainder = (global_active_power * 1000 / 60) - (sub_metering_1 + sub_metering_2 + sub_metering_3)
2 加载和准备数据集
2.1 数据加载
数据集可以从UCI机器学习存储库下载为一个20 mb .zip文件:
下载数据集并将其解压缩到当前工作目录中。现在您将拥有文件“household_power_consumption”。它的大小约为127mb,包含所有的观测数据。
我们可以使用read_csv()函数加载数据,并将前两列合并为一个日期-时间列,作为索引使用。
# load all data
dataset = read_csv('household_power_consumption.txt', sep=';', header=0, low_memory=False, infer_datetime_format=True, parse_dates={'datetime':[0,1]}, index_col=['datetime'])
2.2 数据清预处理
接下来,我们可以用'标记所有缺失的值?'字符,带有NaN值,这是一个浮点数。
这将允许我们将数据作为一个浮点值数组来处理,而不是混合类型(效率较低)。
# load and clean-up data
from numpy import nan
from numpy import isnan
from pandas import read_csv
from pandas import to_numeric
# mark all missing values
dataset.replace('?', nan, inplace=True)
# make dataset numeric
dataset = dataset.astype('float32')
既然已经标记了缺失的值,我们还需要填充它们。
一个非常简单的方法是复制前一天同一时间的观察结果。我们可以在一个名为fill_missing()的函数中实现这一点,该函数将获取数据的NumPy数组并复制24小时前的值。
# fill missing values with a value at the same time one day ago
def fill_missing(values):
one_day = 60 * 24
for row in range(values.shape[0]):
for col in range(values.shape[1]):
if isnan(values[row, col]):
values[row, col] = values[row - one_day, col]
我们可以将此函数直接应用于DataFrame中的数据。
# fill missing
fill_missing(dataset.values)
现在,我们可以使用上一节的计算创建一个包含子计量其余部分的新列。
# add a column for for the remainder of sub metering
values = dataset.values
dataset['sub_metering_4'] = (values[:,0] * 1000 / 60) - (values[:,4] + values[:,5] + values[:,6])
现在,我们可以将数据集的清理版本保存到一个新文件中;在本例中,我们将文件扩展名更改为.csv,并将数据集保存为' household_power_consumption.csv '。
# save updated dataset
dataset.to_csv('household_power_consumption.csv')
将所有这些结合在一起,下面列出了加载、清理和保存数据集的完整示例。
# load and clean-up data
from numpy import nan
from numpy import isnan
from pandas import read_csv
from pandas import to_numeric
# fill missing values with a value at the same time one day ago
def fill_missing(values):
one_day = 60 * 24
for row in range(values.shape[0]):
for col in range(values.shape[1]):
if isnan(values[row, col]):
values[row, col] = values[row - one_day, col]
# load all data
dataset = read_csv('household_power_consumption.txt', sep=';', header=0, low_memory=False, infer_datetime_format=True, parse_dates={'datetime':[0,1]}, index_col=['datetime'])
# mark all missing values
dataset.replace('?', nan, inplace=True)
# make dataset numeric
dataset = dataset.astype('float32')
# fill missing
fill_missing(dataset.values)
# add a column for for the remainder of sub metering
values = dataset.values
dataset['sub_metering_4'] = (values[:,0] * 1000 / 60) - (values[:,4] + values[:,5] + values[:,6])
# save updated dataset
dataset.to_csv('household_power_consumption.csv')
3 模型评价
在本节中,我们将考虑如何为家用电力数据集开发和评估预测模型。
本节共分为四个部分;它们是:
- 问题的框架
- 评价指标
- 训练及测试设备
- Walk-Forward验证
3.1 问题的框架
有许多方法可以利用和探索家庭能耗数据集。
在本教程中,我们将使用这些数据来探索一个非常具体的问题;那就是:
考虑到最近的用电量,未来一周的预期用电量是多少?
这需要一个预测模型预测未来七天每天的总有功功率。
从技术上讲,这个问题的框架被称为一个多步骤时间序列预测问题,给出了多个预测步骤。利用多个输入变量的模型可称为多元多步时间序列预测模型。
这种模式可以帮助家庭内部规划支出。它还可以在供应方面帮助规划特定家庭的电力需求。
数据集的这一框架还表明,将每分钟的耗电量观测数据降至每日总耗电量是有用的。这不是必需的,但是有意义,因为我们对每天的总功率感兴趣。
我们可以很容易地在pandas DataFrame上使用resample()函数来实现这一点。使用参数“D”调用此函数允许按日期-时间索引的加载数据按天分组(请参阅所有偏移别名)。然后,我们可以计算每天所有观察值的总和,并为这八个变量中的每一个创建一个新的日能耗数据集。
完整的示例如下所示。
# resample minute data to total for each day
from pandas import read_csv
# load the new file
dataset = read_csv('household_power_consumption.csv', header=0, infer_datetime_format=True, parse_dates=['datetime'], index_col=['datetime'])
# resample data to daily
daily_groups = dataset.resample('D')
daily_data = daily_groups.sum()
# summarize
print(daily_data.shape)
print(daily_data.head())
# save
daily_data.to_csv('household_power_consumption_days.csv')
运行该示例将创建一个新的每日总能耗数据集,并将结果保存到一个名为“household_power_consumption ption_days.csv”的单独文件中。
我们可以使用这个数据集来拟合和评估问题所选框架的预测模型。
3.2 评价指标
预测将由7个值组成,每个值代表未来一周的每一天。
在多步预测问题中,通常对每个预测的时间步分别进行评估。这样做有几个好处:
- 在特定的准备时间(例如+1天vs +3天)对技能进行评价。
- 根据不同的交付时间(例如,擅长+1天的模型与擅长+5天的模型),对模型进行比较。
总功率的单位是千瓦,有一个同样单位的误差度量是很有用的。均方根误差(RMSE)和平均绝对误差(MAE)都符合这一要求,但是RMSE更常用,并将在本教程中采用。与MAE不同,RMSE对预测错误的惩罚更大。
这个问题的性能指标将是从第1天到第7天的每个交付时间的RMSE。
作为一种捷径,使用单个分数来总结模型的性能,以辅助模型选择,这可能是有用的。
可以使用的一个可能的评分是所有预测日的RMSE。
下面的evaluate_forecasts()函数将实现此行为,并返回基于多个七天预测的模型的性能。
# evaluate one or more weekly forecasts against expected values
def evaluate_forecasts(actual, predicted):
scores = list()
# calculate an RMSE score for each day
for i in range(actual.shape[1]):
# calculate mse
mse = mean_squared_error(actual[:, i], predicted[:, i])
# calculate rmse
rmse = sqrt(mse)
# store
scores.append(rmse)
# calculate overall RMSE
s = 0
for row in range(actual.shape[0]):
for col in range(actual.shape[1]):
s += (actual[row, col] - predicted[row, col])**2
score = sqrt(s / (actual.shape[0] * actual.shape[1]))
return score, scores
运行该函数将首先返回总RMSE,而不考虑日期,然后返回每天的RMSE得分数组。
3.3 训练及测试集
我们将使用前三年的数据来训练预测模型,并使用最后一年的数据来评估模型。
给定数据集中的数据将被划分为标准周。这些星期从星期天开始,到星期六结束。
这是使用所选择的模型框架的一种现实而有用的方法,可以预测未来一周的电力消耗。它还有助于建模,在建模中,模型可以用来预测特定的一天(例如周三)或整个序列。
我们将把数据分成标准周,从测试数据集中向后工作。
数据的最后一年是2010年,2010年的第一个周日是1月3日。该数据将于2010年11月中旬结束,其中最近的一个周六是11月20日。这给出了46周的测试数据。
下面提供测试数据集的每日数据的第一行和最后一行,以供确认。
2010-01-03,2083.4539999999984,191.61000000000055,350992.12000000034,8703.600000000033,3842.0,4920.0,10074.0,15888.233355799992
...
2010-11-20,2197.006000000004,153.76800000000028,346475.9999999998,9320.20000000002,4367.0,2947.0,11433.0,17869.76663959999
下面的函数split_dataset()将日常数据分解为训练和测试集,并将每个数据组织为标准周。
特定的行偏移量用于使用数据集的知识分割数据。然后,使用NumPy split()函数将分割的数据集组织为每周的数据。
# split a univariate dataset into train/test sets
def split_dataset(data):
# split into standard weeks
train, test = data[1:-328], data[-328:-6]
# restructure into windows of weekly data
train = array(split(train, len(train)/7))
test = array(split(test, len(test)/7))
return train, test
我们可以通过加载日常数据集并打印来自火车和测试集的第一行和最后一行数据来测试这个函数,以确认它们符合上面的期望。完整的代码示例如下所示。
# split into standard weeks
from numpy import split
from numpy import array
from pandas import read_csv
# split a univariate dataset into train/test sets
def split_dataset(data):
# split into standard weeks
train, test = data[1:-328], data[-328:-6]
# restructure into windows of weekly data
train = array(split(train, len(train)/7))
test = array(split(test, len(test)/7))
return train, test
# load the new file
dataset = read_csv('household_power_consumption_days.csv', header=0, infer_datetime_format=True, parse_dates=['datetime'], index_col=['datetime'])
train, test = split_dataset(dataset.values)
# validate train data
print(train.shape)
print(train[0, 0, 0], train[-1, -1, 0])
# validate test
print(test.shape)
print(test[0, 0, 0], test[-1, -1, 0])
运行该示例表明,训练数据集确实有159周的数据,而测试数据集有46周。
我们可以看到,训练和测试数据集的第一行和最后一行的总有功功率与我们为每个集合定义的标准周的界限指定的特定日期的数据相匹配。
(159, 7, 8)
3390.46 1309.2679999999998
(46, 7, 8)
2083.4539999999984 2197.006000000004
3.4 Walk-Forward验证
模型将使用一种称为Walk-Forward验证的方案进行评估。
这是需要模型进行一周预测的地方,然后将该周的实际数据提供给模型,以便可以将其用作对下一周进行预测的基础。这对于在实践中如何使用模型是现实的,并且对于允许模型使用最佳可用数据是有益的。
我们可以通过分离输入数据和输出/预测数据来说明这一点。
Input, Predict
[Week1] Week2
[Week1 + Week2] Week3
[Week1 + Week2 + Week3] Week4
...
下面提供了评估此数据集上的预测模型的前向验证方法evaluate_model()。
标准周格式的训练和测试数据集作为参数提供给函数。提供了一个额外的参数n_input,用于定义模型将用作输入的先前观察值的数量,以便进行预测。
调用了两个新函数:一个函数从名为build_model()的培训数据构建模型,另一个函数使用该模型为每个新标准周进行预测,称为forecast()。这些将在后面的小节中讨论。
我们正在研究神经网络,因此,它们通常训练速度较慢,但评估速度较快。这意味着模型的首选用法是在历史数据上构建它们一次,并使用它们来预测前进验证的每个步骤。模型在评估期间是静态的(即没有更新)。
这与其他训练更快的模型不同,在这些模型中,当新数据可用时,模型可以重新适应或更新前向验证的每个步骤。有了足够的资源,就可以用这种方式使用神经网络,但在本教程中我们不会这样做。
完整的evaluate_model()函数如下所示。
# evaluate a single model
def evaluate_model(train, test, n_input):
# fit model
model = build_model(train, n_input)
# history is a list of weekly data
history = [x for x in train]
# walk-forward validation over each week
predictions = list()
for i in range(len(test)):
# predict the week
yhat_sequence = forecast(model, history, n_input)
# store the predictions
predictions.append(yhat_sequence)
# get real observation and add to history for predicting the next week
history.append(test[i, :])
# evaluate predictions days for each week
predictions = array(predictions)
score, scores = evaluate_forecasts(test[:, :, 0], predictions)
return score, scores
一旦我们对模型进行了评估,我们就可以总结性能。
下面名为summarize_scores()的函数将单个行显示模型的性能,以便与其他模型进行比较。
# summarize scores
def summarize_scores(name, score, scores):
s_scores = ', '.join(['%.1f' % s for s in scores])
print('%s: [%.3f] %s' % (name, score, s_scores))
现在,我们有了开始评估数据集上的预测模型的所有元素。
4 用于多步预测的LSTMs
递归神经网络(RNNs)是专门用来工作、学习和预测序列数据的。
递归神经网络是一种神经网络,其中一个时间步长的输出作为后续时间步的输入。这允许模型根据当前时间步长的输入和对前一个时间步的输出的直接知识来决定预测什么。
长短时记忆网络(LSTM)可能是最成功、应用最广泛的RNN。它的成功之处在于克服了训练一个递归神经网络所面临的挑战,得到了稳定的模型。除了利用前一个时间步的输出的循环连接之外,LSTMs还有一个内部内存,其操作类似于一个局部变量,允许它们在输入序列上累积状态。
关于LSTM的使用方法,可以参考老鼠屎的旧博文Keras实战:基于LSTM的股价预测方法。
当涉及到多步时间序列预测时,LSTMs提供了许多好处;它们是:
- 对序列的本机支持。LSTMs是一种递归网络,其目的是将序列数据作为输入,而不像其他模型那样必须将滞后观测作为输入特征。
- 多变量输入。LSTMs直接支持多元输入的多个并行输入序列,而不像其他模型中多元输入以平面结构表示。
- 向量输出。与其他神经网络一样,LSTMs能够将输入数据直接映射到一个可能表示多个输出时间步长的输出向量。
此外,还开发了专门用于进行多步骤序列预测的专用体系结构,通常称为序列到序列预测(sequence-to-sequence prediction,简称seq2seq)。这很有用,因为多步时间序列预测是seq2seq预测的一种类型。
一个为seq2seq问题设计的递归神经网络结构的例子是编码器-解码器LSTM。
一个encoder-decoder LSTM模型包括两个子模型:一个叫编码器读取输入序列和压缩到一个固定长度的内部表示,和一个称为译码器的输出模型解释内部表示和使用它来预测输出序列。
序列预测的编解码器方法已被证明比直接输出向量有效得多,是首选的方法。
一般来说,LSTMs被发现在自动回归类型的问题上不是很有效。在这些问题中,预测下一个时间步骤是最近时间步骤的函数。
关于Encoder-Decoder机制,可以参考老鼠屎的旧博文Attention如何在Encoder-Decoder循环神经网络中见效(原理篇)。
在本教程中,我们将探索一套用于多步骤时间序列预测的LSTM体系结构。具体来说,我们将研究如何开发以下模型:
- 基于向量输出的LSTM模型,用于单变量输入数据的多步预测。
- 用于单变量输入数据多步预测的编解码器LSTM模型。
- 利用多变量输入数据进行多步预测的编解码器LSTM模型。
- 用于单变量输入数据多步预测的CNN-LSTM编解码器模型。
- 利用单变量输入数据进行多步预测的ConvLSTM编解码器模型。
该模型将应用于家庭用电预测问题。如果一个模型比一个简单的模型(在七天的预测中RMSE为465千瓦)的性能更好,那么这个模型就被认为是熟练的。
我们不会专注于调整这些模型以获得最佳性能;相反,与naive的预测相比,我们将在熟练的模型上作短暂停留。所选择的结构和超参数的选取经过了少量的试验和误差。分数应该仅仅作为一个例子,而不是对问题的最优模型或配置的研究。
考虑到模型的随机性,最好对给定的模型进行多次评估,并报告测试数据集的平均性能。为了保持代码的简洁性和简单性,我们将在本教程中展示单次运行的模型。
对于给定的多步预测问题,我们不知道哪种方法是最有效的。探索一套方法是一个好主意,以便发现在您的特定数据集中什么工作得最好。
5 具有单变量输入和向量输出的LSTM模型
我们将从开发一个简单的或普通的LSTM模型开始,该模型读取一系列的日总功耗,并预测下一个标准周的日功耗的向量输出。这将为后面几节中开发的更精细的模型提供基础。
之前用作输入的天数定义了LSTM将读取并学习提取特征的数据的一维(1D)子序列。关于这项投入的规模和性质的一些想法包括:
- 所有之前的日子,直到数年的数据。
- 前七天。
- 前两周。
- 前一个月。
- 前一年。
- 前一周和一年前预测的一周。
没有正确的答案;相反,可以测试每种方法和更多方法,并且可以使用模型的性能来选择输入的性质,从而获得最佳的模型性能。这些选择定义了一些事情:
- 如何准备训练数据以适合模型。
- 如何准备测试数据来评估模型。
- 如何使用该模型对未来的最终模型进行预测。
一个好的开始是使用前七天。
LSTM模型期望数据具有如下形状:
[samples, timesteps, features]
关于LSTM模型的形状,可以参考老鼠屎的旧博文Keras实战:基于LSTM的股价预测方法与如何理解Keras中的TimeDistributed层并在LSTM中使用。
一个样本将包含7个时间步骤,其中一个特性用于7天的总日功耗。
训练数据集有159周的数据,因此训练数据集的形状为:
[159, 7, 1]
这是一个良好的开端。这种格式的数据将使用之前的标准周来预测下一个标准周。问题是159个实例对于训练神经网络来说并不多。
创建更多培训数据的一种方法是在培训期间更改问题,以预测给定前七天的未来七天,而不考虑标准周。
这只影响训练数据,测试问题仍然是相同的:给定前一个标准周,预测下一个标准周的每日功耗。
这将需要一些培训数据的准备工作。
训练数据以标准周为单位,包含8个变量,具体形式为[159,7,8]。第一步是将数据压平,这样我们就有8个时间序列序列。
# flatten data
data = train.reshape((train.shape[0]*train.shape[1], train.shape[2]))
然后,我们需要遍历时间步骤并将数据划分为重叠的窗口;每次迭代都沿着一个时间步骤进行,并预测接下来的七天。例如:
Input, Output
[d01, d02, d03, d04, d05, d06, d07], [d08, d09, d10, d11, d12, d13, d14]
[d02, d03, d04, d05, d06, d07, d08], [d09, d10, d11, d12, d13, d14, d15]
...
我们可以通过跟踪输入和输出的开始和结束索引来实现这一点,因为我们按照时间步长遍历扁平数据的长度。
我们还可以通过参数化输入和输出的数量(例如n_input、n_out)来实现这一点,这样您就可以试验不同的值,或者根据您自己的问题调整它。
下面是一个名为to_supervised()的函数,它接受一个星期列表(历史记录)和作为输入和输出使用的时间步数,并以重叠的移动窗口格式返回数据。
# convert history into inputs and outputs
def to_supervised(train, n_input, n_out=7):
# flatten data
data = train.reshape((train.shape[0]*train.shape[1], train.shape[2]))
X, y = list(), list()
in_start = 0
# step over the entire history one time step at a time
for _ in range(len(data)):
# define the end of the input sequence
in_end = in_start + n_input
out_end = in_end + n_out
# ensure we have enough data for this instance
if out_end < len(data):
x_input = data[in_start:in_end, 0]
x_input = x_input.reshape((len(x_input), 1))
X.append(x_input)
y.append(data[in_end:out_end, 0])
# move along one time step
in_start += 1
return array(X), array(y)
当我们在整个训练数据集上运行这个函数时,我们将159个样本转换为1,099个;具体来说,转换后的数据集具有X=[1099, 7, 1]和y=[1099, 7]的形状。
接下来,我们可以在训练数据上定义和拟合LSTM模型。
这个多步时间序列预测问题是一个自回归问题。这意味着它很可能是最好的模型,在接下来的7天是在之前的时间步长的观测函数。这和相对较少的数据意味着需要一个较小的模型。
我们将开发一个有200个LSTM神经元作为隐含层的模型。隐藏层中的单元数与输入序列中的时间步长无关。LSTM层之后是一个具有100个节点的全连接层,它将解释LSTM层学到的特性。最后,输出层将直接预测一个包含七个元素的向量,在输出序列中每天一个元素。
我们将使用均方误差损失函数,因为它很好地匹配我们选择的RMSE误差度量。我们将使用随机梯度下降的有效Adam实现,并将该模型拟合到70个批次大小为16的时代。
该算法的小批量和随机性意味着,相同的模型在每次训练时将学习到输入到输出的稍微不同的映射。这意味着在评估模型时,结果可能会有所不同。您可以尝试多次运行模型并计算模型性能的平均值。
下面的build_model()准备训练数据,定义模型,并在训练数据上对模型进行拟合,返回准备进行预测的拟合模型。
# train the model
def build_model(train, n_input):
# prepare data
train_x, train_y = to_supervised(train, n_input)
# define parameters
verbose, epochs, batch_size = 0, 70, 16
n_timesteps, n_features, n_outputs = train_x.shape[1], train_x.shape[2], train_y.shape[1]
# define model
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')
# fit network
model.fit(train_x, train_y, epochs=epochs, batch_size=batch_size, verbose=verbose)
return model
既然我们知道了如何拟合模型,我们就可以看看如何使用模型进行预测。
一般情况下,该模型期望数据在进行预测时具有相同的三维形状。
在这种情况下,一个输入模式的期望形状是一个样本,一个特征七天的日耗电量:
[1, 7, 1]
当为测试集进行预测时,以及当最终模型用于预测未来时,数据必须具有这种形状。如果将输入天数更改为14天,那么在进行预测时,训练数据的形状和新样本的形状必须相应地更改为14个时间步长。这是您在使用模型时必须进行的建模选择。
我们使用前一节描述的前向验证来评估模型。
这意味着我们有前一周的观测数据,以便预测下一周。这些数据被收集到一个名为history的标准周数组中。
为了预测下一个标准周,我们需要检索最后几天的观测结果。与训练数据一样,我们必须首先将历史数据进行平化,以删除每周的结构,从而得到8个并行的时间序列。
# flatten data
data = data.reshape((data.shape[0]*data.shape[1], data.shape[2]))
接下来,我们需要检索过去7天每天消耗的总电量(feature index 0)。
我们将像对训练数据所做的那样对其进行参数化,以便将来可以修改作为模型输入的前几天的天数。
# retrieve last observations for input data
input_x = data[-n_input:, 0]
接下来,我们将输入重塑为预期的三维结构。
# reshape into [1, n_input, 1]
input_x = input_x.reshape((1, len(input_x), 1))
然后利用拟合模型和输入数据进行预测,得到七天输出的矢量。
# forecast the next week
yhat = model.predict(input_x, verbose=0)
# we only want the vector forecast
yhat = yhat[0]
下面的forecast()函数实现了这一点,并将模型适合于训练数据集的参数、到目前为止观察到的数据历史以及模型预期的输入时间步长作为参数。
# make a forecast
def forecast(model, history, n_input):
# flatten data
data = array(history)
data = data.reshape((data.shape[0]*data.shape[1], data.shape[2]))
# retrieve last observations for input data
input_x = data[-n_input:, 0]
# reshape into [1, n_input, 1]
input_x = input_x.reshape((1, len(input_x), 1))
# forecast the next week
yhat = model.predict(input_x, verbose=0)
# we only want the vector forecast
yhat = yhat[0]
return yhat
现在,我们已经拥有了在单变量数据集上使用LSTM模型进行多步时间序列预测所需的一切。我们可以把所有这些联系起来。完整的示例如下所示。
# univariate multi-step lstm
from math import sqrt
from numpy import split
from numpy import array
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from matplotlib import pyplot
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import LSTM
# split a univariate dataset into train/test sets
def split_dataset(data):
# split into standard weeks
train, test = data[1:-328], data[-328:-6]
# restructure into windows of weekly data
train = array(split(train, len(train)/7))
test = array(split(test, len(test)/7))
return train, test
# evaluate one or more weekly forecasts against expected values
def evaluate_forecasts(actual, predicted):
scores = list()
# calculate an RMSE score for each day
for i in range(actual.shape[1]):
# calculate mse
mse = mean_squared_error(actual[:, i], predicted[:, i])
# calculate rmse
rmse = sqrt(mse)
# store
scores.append(rmse)
# calculate overall RMSE
s = 0
for row in range(actual.shape[0]):
for col in range(actual.shape[1]):
s += (actual[row, col] - predicted[row, col])**2
score = sqrt(s / (actual.shape[0] * actual.shape[1]))
return score, scores
# summarize scores
def summarize_scores(name, score, scores):
s_scores = ', '.join(['%.1f' % s for s in scores])
print('%s: [%.3f] %s' % (name, score, s_scores))
# convert history into inputs and outputs
def to_supervised(train, n_input, n_out=7):
# flatten data
data = train.reshape((train.shape[0]*train.shape[1], train.shape[2]))
X, y = list(), list()
in_start = 0
# step over the entire history one time step at a time
for _ in range(len(data)):
# define the end of the input sequence
in_end = in_start + n_input
out_end = in_end + n_out
# ensure we have enough data for this instance
if out_end < len(data):
x_input = data[in_start:in_end, 0]
x_input = x_input.reshape((len(x_input), 1))
X.append(x_input)
y.append(data[in_end:out_end, 0])
# move along one time step
in_start += 1
return array(X), array(y)
# train the model
def build_model(train, n_input):
# prepare data
train_x, train_y = to_supervised(train, n_input)
# define parameters
verbose, epochs, batch_size = 0, 70, 16
n_timesteps, n_features, n_outputs = train_x.shape[1], train_x.shape[2], train_y.shape[1]
# define model
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')
# fit network
model.fit(train_x, train_y, epochs=epochs, batch_size=batch_size, verbose=verbose)
return model
# make a forecast
def forecast(model, history, n_input):
# flatten data
data = array(history)
data = data.reshape((data.shape[0]*data.shape[1], data.shape[2]))
# retrieve last observations for input data
input_x = data[-n_input:, 0]
# reshape into [1, n_input, 1]
input_x = input_x.reshape((1, len(input_x), 1))
# forecast the next week
yhat = model.predict(input_x, verbose=0)
# we only want the vector forecast
yhat = yhat[0]
return yhat
# evaluate a single model
def evaluate_model(train, test, n_input):
# fit model
model = build_model(train, n_input)
# history is a list of weekly data
history = [x for x in train]
# walk-forward validation over each week
predictions = list()
for i in range(len(test)):
# predict the week
yhat_sequence = forecast(model, history, n_input)
# store the predictions
predictions.append(yhat_sequence)
# get real observation and add to history for predicting the next week
history.append(test[i, :])
# evaluate predictions days for each week
predictions = array(predictions)
score, scores = evaluate_forecasts(test[:, :, 0], predictions)
return score, scores
# load the new file
dataset = read_csv('household_power_consumption_days.csv', header=0, infer_datetime_format=True, parse_dates=['datetime'], index_col=['datetime'])
# split into train and test
train, test = split_dataset(dataset.values)
# evaluate model and get scores
n_input = 7
score, scores = evaluate_model(train, test, n_input)
# summarize scores
summarize_scores('lstm', score, scores)
# plot scores
days = ['sun', 'mon', 'tue', 'wed', 'thr', 'fri', 'sat']
pyplot.plot(days, scores, marker='o', label='lstm')
pyplot.show()
运行示例适合并评估模型,在所有7天内打印整个RMSE,并为每个提前时间打印每天的RMSE。由于算法的随机性,您的特定结果可能会有所不同。您可能想尝试运行这个示例几次。
我们可以看到,在这种情况下,与单纯的预测相比,模型是熟练的,总体RMSE约为399千瓦,低于单纯模型的465千瓦。
lstm: [399.456] 419.4, 422.1, 384.5, 395.1, 403.9, 317.7, 441.5
还创建了每日RMSE的图表。图表显示,也许周二和周五比其他日子更容易预测,也许周六是标准周末最难预测的日子。
6 单变量输入的编解码器LSTM模型
在本节中,我们可以更新普通的LSTM以使用编解码器模型。
这意味着模型不会直接输出向量序列。相反,该模型将由两个子模型组成,用于读取和编码输入序列的编码器,以及读取编码的输入序列并对输出序列中的每个元素进行一步预测的解码器。这种差别很细微,因为实际上这两种方法都可以预测序列输出。
重要的不同之处在于,解码器使用了LSTM模型,这使得解码器既可以知道前一天在序列中预测了什么,又可以在输出序列时积累内部状态。
让我们仔细看看这个模型是如何定义的。和前面一样,我们定义了一个包含200个单元的LSTM隐藏层。这是解码器模型,它将读取输入序列并输出一个200个元素向量(每个单元一个输出),该元素向量从输入序列捕获特性。我们将使用14天的总功耗作为输入。
# define model
model = Sequential()
model.add(LSTM(200, activation='relu', input_shape=(n_timesteps, n_features)))
我们将使用一个简单的编码器-解码器架构,易于在Keras中实现,这与LSTM自动编码器的架构有很多相似之处。
首先,对输入序列的内部表示进行多次重复,对于输出序列中的每个时间步长重复一次。这个向量序列将被呈现给LSTM解码器。使用 "RepeatVector" 将 Encoder 的输出(最后一个 time step)复制 N 份作为 Decoder 的 N 次输入。
model.add(RepeatVector(7))
然后我们将解码器定义为一个包含200个单元的LSTM隐藏层。重要的是,解码器将输出整个序列,而不仅仅是序列末尾的输出,就像我们对编码器所做的那样。这意味着200个单元中的每一个单元都将为7天中的每一天输出一个值,表示输出序列中每天预测的内容的基础。
model.add(LSTM(200, activation='relu', return_sequences=True))
然后,我们将使用一全连接层来解释最终输出层之前输出序列中的每个时间步长。重要的是,输出层预测输出序列中的一个步骤,不是一次七天,
这意味着我们将对输出序列中的每个步骤使用相同的层。它的意思是相同的全连接层和输出层将用于处理解码器提供的每个时间步长。为了实现这一点,我们将解释层和输出层封装在一个TimeDistributed包装器中,该包装器允许从解码器每次执行步骤时都使用所封装的层。
model.add(TimeDistributed(Dense(100, activation='relu')))
model.add(TimeDistributed(Dense(1)))
这允许LSTM解码器计算出输出序列中每个步骤所需的上下文,以及用于单独解释每个时间步骤的被包装的Dense层,同时重用相同的权重来执行解释。另一种方法是将LSTM解码器创建的所有结构压平,并直接输出矢量。您可以尝试将其作为一个扩展,以查看它是如何进行比较的。
因此,网络输出与输入结构相同的三维向量,具有维数[样本、时间步长、特征]。
它只有一个功能,即每天消耗的总电量,而且总是有7个特征。因此,一个单一的一周预测将有大小:[1,7,1]。
因此,在对模型进行训练时,我们必须对输出数据(y)进行重构,使其具有三维结构,而不是上一节所使用的[sample, features]的二维结构。
# reshape output into [samples, timesteps, features]
train_y = train_y.reshape((train_y.shape[0], train_y.shape[1], 1))
我们可以将所有这些都绑定到下面列出的更新build_model()函数中。
# train the model
def build_model(train, n_input):
# prepare data
train_x, train_y = to_supervised(train, n_input)
# define parameters
verbose, epochs, batch_size = 0, 20, 16
n_timesteps, n_features, n_outputs = train_x.shape[1], train_x.shape[2], train_y.shape[1]
# reshape output into [samples, timesteps, features]
train_y = train_y.reshape((train_y.shape[0], train_y.shape[1], 1))
# define model
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')
# fit network
model.fit(train_x, train_y, epochs=epochs, batch_size=batch_size, verbose=verbose)
return model
下面列出了编码器-解码器模型的完整示例。
# univariate multi-step encoder-decoder lstm
from math import sqrt
from numpy import split
from numpy import array
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from matplotlib import pyplot
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import LSTM
from keras.layers import RepeatVector
from keras.layers import TimeDistributed
# split a univariate dataset into train/test sets
def split_dataset(data):
# split into standard weeks
train, test = data[1:-328], data[-328:-6]
# restructure into windows of weekly data
train = array(split(train, len(train)/7))
test = array(split(test, len(test)/7))
return train, test
# evaluate one or more weekly forecasts against expected values
def evaluate_forecasts(actual, predicted):
scores = list()
# calculate an RMSE score for each day
for i in range(actual.shape[1]):
# calculate mse
mse = mean_squared_error(actual[:, i], predicted[:, i])
# calculate rmse
rmse = sqrt(mse)
# store
scores.append(rmse)
# calculate overall RMSE
s = 0
for row in range(actual.shape[0]):
for col in range(actual.shape[1]):
s += (actual[row, col] - predicted[row, col])**2
score = sqrt(s / (actual.shape[0] * actual.shape[1]))
return score, scores
# summarize scores
def summarize_scores(name, score, scores):
s_scores = ', '.join(['%.1f' % s for s in scores])
print('%s: [%.3f] %s' % (name, score, s_scores))
# convert history into inputs and outputs
def to_supervised(train, n_input, n_out=7):
# flatten data
data = train.reshape((train.shape[0]*train.shape[1], train.shape[2]))
X, y = list(), list()
in_start = 0
# step over the entire history one time step at a time
for _ in range(len(data)):
# define the end of the input sequence
in_end = in_start + n_input
out_end = in_end + n_out
# ensure we have enough data for this instance
if out_end < len(data):
x_input = data[in_start:in_end, 0]
x_input = x_input.reshape((len(x_input), 1))
X.append(x_input)
y.append(data[in_end:out_end, 0])
# move along one time step
in_start += 1
return array(X), array(y)
# train the model
def build_model(train, n_input):
# prepare data
train_x, train_y = to_supervised(train, n_input)
# define parameters
verbose, epochs, batch_size = 0, 20, 16
n_timesteps, n_features, n_outputs = train_x.shape[1], train_x.shape[2], train_y.shape[1]
# reshape output into [samples, timesteps, features]
train_y = train_y.reshape((train_y.shape[0], train_y.shape[1], 1))
# define model
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')
# fit network
model.fit(train_x, train_y, epochs=epochs, batch_size=batch_size, verbose=verbose)
return model
# make a forecast
def forecast(model, history, n_input):
# flatten data
data = array(history)
data = data.reshape((data.shape[0]*data.shape[1], data.shape[2]))
# retrieve last observations for input data
input_x = data[-n_input:, 0]
# reshape into [1, n_input, 1]
input_x = input_x.reshape((1, len(input_x), 1))
# forecast the next week
yhat = model.predict(input_x, verbose=0)
# we only want the vector forecast
yhat = yhat[0]
return yhat
# evaluate a single model
def evaluate_model(train, test, n_input):
# fit model
model = build_model(train, n_input)
# history is a list of weekly data
history = [x for x in train]
# walk-forward validation over each week
predictions = list()
for i in range(len(test)):
# predict the week
yhat_sequence = forecast(model, history, n_input)
# store the predictions
predictions.append(yhat_sequence)
# get real observation and add to history for predicting the next week
history.append(test[i, :])
# evaluate predictions days for each week
predictions = array(predictions)
score, scores = evaluate_forecasts(test[:, :, 0], predictions)
return score, scores
# load the new file
dataset = read_csv('household_power_consumption_days.csv', header=0, infer_datetime_format=True, parse_dates=['datetime'], index_col=['datetime'])
# split into train and test
train, test = split_dataset(dataset.values)
# evaluate model and get scores
n_input = 14
score, scores = evaluate_model(train, test, n_input)
# summarize scores
summarize_scores('lstm', score, scores)
# plot scores
days = ['sun', 'mon', 'tue', 'wed', 'thr', 'fri', 'sat']
pyplot.plot(days, scores, marker='o', label='lstm')
pyplot.show()
运行该示例符合该模型,并总结了测试数据集上的性能。
由于算法的随机性,您的特定结果可能会有所不同。您可能想尝试运行这个示例几次。
我们可以看到,在这种情况下,模型是熟练的,实现了一个总RMSE评分约372千瓦。
lstm: [372.595] 379.5, 399.8, 339.6, 372.2, 370.9, 309.9, 424.8
还创建了一个每日RMSE的线图,显示了与上一节中看到的类似的误差模式。
7 多变量输入的LSTM模型
在本节中,我们将更新上一节中开发的编码器-解码器LSTM,使用8个时间序列变量中的每一个来预测下一个标准周的每日总功耗。
我们将通过将每个一维时间序列作为单独的输入序列提供给模型来实现这一点。
LSTM将依次创建每个输入序列的内部表示,这些输入序列将由解码器一起解释。
使用多元输入有助于解决这样的问题,即输出序列是来自多个不同特征的先前时间步长的某个函数,而不只是(或包括)预测的特征。目前还不清楚在电力消耗问题上是否存在这种情况,但我们可以探索它。
首先,我们必须更新培训数据的准备工作,使其包含所有八个特性,而不仅仅是每天消耗的总能量。它需要一行更改:
X.append(data[in_start:in_end, :])
完成此更改的to_supervised()函数如下所示。
# convert history into inputs and outputs
def to_supervised(train, n_input, n_out=7):
# flatten data
data = train.reshape((train.shape[0]*train.shape[1], train.shape[2]))
X, y = list(), list()
in_start = 0
# step over the entire history one time step at a time
for _ in range(len(data)):
# define the end of the input sequence
in_end = in_start + n_input
out_end = in_end + n_out
# ensure we have enough data for this instance
if out_end < len(data):
X.append(data[in_start:in_end, :])
y.append(data[in_end:out_end, 0])
# move along one time step
in_start += 1
return array(X), array(y)
我们还必须更新使用fit模型进行预测的函数,以使用前面时间步骤中的所有8个特性。
还有一个小变化:
# retrieve last observations for input data
input_x = data[-n_input:, :]
# reshape into [1, n_input, n]
input_x = input_x.reshape((1, input_x.shape[0], input_x.shape[1]))
修改后的complete forecast()函数如下:
# make a forecast
def forecast(model, history, n_input):
# flatten data
data = array(history)
data = data.reshape((data.shape[0]*data.shape[1], data.shape[2]))
# retrieve last observations for input data
input_x = data[-n_input:, :]
# reshape into [1, n_input, n]
input_x = input_x.reshape((1, input_x.shape[0], input_x.shape[1]))
# forecast the next week
yhat = model.predict(input_x, verbose=0)
# we only want the vector forecast
yhat = yhat[0]
return yhat
直接使用相同的模型体系结构和配置,尽管我们将把训练周期的数量从20个增加到50个,因为输入数据量增加了8倍。
完整的示例如下所示。
# multivariate multi-step encoder-decoder lstm
from math import sqrt
from numpy import split
from numpy import array
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from matplotlib import pyplot
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import LSTM
from keras.layers import RepeatVector
from keras.layers import TimeDistributed
# split a univariate dataset into train/test sets
def split_dataset(data):
# split into standard weeks
train, test = data[1:-328], data[-328:-6]
# restructure into windows of weekly data
train = array(split(train, len(train)/7))
test = array(split(test, len(test)/7))
return train, test
# evaluate one or more weekly forecasts against expected values
def evaluate_forecasts(actual, predicted):
scores = list()
# calculate an RMSE score for each day
for i in range(actual.shape[1]):
# calculate mse
mse = mean_squared_error(actual[:, i], predicted[:, i])
# calculate rmse
rmse = sqrt(mse)
# store
scores.append(rmse)
# calculate overall RMSE
s = 0
for row in range(actual.shape[0]):
for col in range(actual.shape[1]):
s += (actual[row, col] - predicted[row, col])**2
score = sqrt(s / (actual.shape[0] * actual.shape[1]))
return score, scores
# summarize scores
def summarize_scores(name, score, scores):
s_scores = ', '.join(['%.1f' % s for s in scores])
print('%s: [%.3f] %s' % (name, score, s_scores))
# convert history into inputs and outputs
def to_supervised(train, n_input, n_out=7):
# flatten data
data = train.reshape((train.shape[0]*train.shape[1], train.shape[2]))
X, y = list(), list()
in_start = 0
# step over the entire history one time step at a time
for _ in range(len(data)):
# define the end of the input sequence
in_end = in_start + n_input
out_end = in_end + n_out
# ensure we have enough data for this instance
if out_end < len(data):
X.append(data[in_start:in_end, :])
y.append(data[in_end:out_end, 0])
# move along one time step
in_start += 1
return array(X), array(y)
# train the model
def build_model(train, n_input):
# prepare data
train_x, train_y = to_supervised(train, n_input)
# define parameters
verbose, epochs, batch_size = 0, 50, 16
n_timesteps, n_features, n_outputs = train_x.shape[1], train_x.shape[2], train_y.shape[1]
# reshape output into [samples, timesteps, features]
train_y = train_y.reshape((train_y.shape[0], train_y.shape[1], 1))
# define model
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')
# fit network
model.fit(train_x, train_y, epochs=epochs, batch_size=batch_size, verbose=verbose)
return model
# make a forecast
def forecast(model, history, n_input):
# flatten data
data = array(history)
data = data.reshape((data.shape[0]*data.shape[1], data.shape[2]))
# retrieve last observations for input data
input_x = data[-n_input:, :]
# reshape into [1, n_input, n]
input_x = input_x.reshape((1, input_x.shape[0], input_x.shape[1]))
# forecast the next week
yhat = model.predict(input_x, verbose=0)
# we only want the vector forecast
yhat = yhat[0]
return yhat
# evaluate a single model
def evaluate_model(train, test, n_input):
# fit model
model = build_model(train, n_input)
# history is a list of weekly data
history = [x for x in train]
# walk-forward validation over each week
predictions = list()
for i in range(len(test)):
# predict the week
yhat_sequence = forecast(model, history, n_input)
# store the predictions
predictions.append(yhat_sequence)
# get real observation and add to history for predicting the next week
history.append(test[i, :])
# evaluate predictions days for each week
predictions = array(predictions)
score, scores = evaluate_forecasts(test[:, :, 0], predictions)
return score, scores
# load the new file
dataset = read_csv('household_power_consumption_days.csv', header=0, infer_datetime_format=True, parse_dates=['datetime'], index_col=['datetime'])
# split into train and test
train, test = split_dataset(dataset.values)
# evaluate model and get scores
n_input = 14
score, scores = evaluate_model(train, test, n_input)
# summarize scores
summarize_scores('lstm', score, scores)
# plot scores
days = ['sun', 'mon', 'tue', 'wed', 'thr', 'fri', 'sat']
pyplot.plot(days, scores, marker='o', label='lstm')
pyplot.show()
8 具有单变量输入的CNN-LSTM编解码器模型
卷积神经网络(CNN)可以作为编解码器结构中的编码器。
CNN不直接支持序列输入;相反,一维CNN能够读取序列输入并自动学习显著特征。然后可以按照正常情况由LSTM解码器解释这些。我们将使用CNN和LSTM的混合模型称为CNN -LSTM模型,在本例中,我们将在一个编解码器体系结构中同时使用它们。
CNN希望输入的数据具有与LSTM模型相同的3D结构,尽管多个特性被读取为不同的通道,最终具有相同的效果。
我们将简化这个示例,并将重点放在具有单变量输入的CNN-LSTM上,但是它也可以很容易地更新为使用多元输入,这只是一个练习。
与之前一样,我们将使用由14天的日总功耗组成的输入序列。
我们将为编码器定义一个简单但有效的CNN架构,它由两个卷积层和一个最大池化层组成,然后将结果扁平化。
第一个卷积层读取输入序列并将结果投影到特征图上。第二层对第一层创建的特征图执行相同的操作,试图放大任何显著的特征。我们将在每个卷积层使用64个feature map,读取内核大小为3个时间步长的输入序列。
最大池层通过保留最大信号值的1/4来简化特征映射。在池化层之后提取的特征映射被压扁成一个长向量,然后可以用作解码过程的输入。
model.add(Conv1D(filters=64, kernel_size=3, activation='relu', input_shape=(n_timesteps,n_features)))
model.add(Conv1D(filters=64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
解码器与前面几节定义的解码器相同。唯一的其他变化是将训练时间设置为20个。带有这些更改的build_model()函数如下所示。
# train the model
def build_model(train, n_input):
# prepare data
train_x, train_y = to_supervised(train, n_input)
# define parameters
verbose, epochs, batch_size = 0, 20, 16
n_timesteps, n_features, n_outputs = train_x.shape[1], train_x.shape[2], train_y.shape[1]
# reshape output into [samples, timesteps, features]
train_y = train_y.reshape((train_y.shape[0], train_y.shape[1], 1))
# define model
model = Sequential()
model.add(Conv1D(filters=64, kernel_size=3, activation='relu', input_shape=(n_timesteps,n_features)))
model.add(Conv1D(filters=64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
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')
# fit network
model.fit(train_x, train_y, epochs=epochs, batch_size=batch_size, verbose=verbose)
return model
现在我们准备使用CNN编码器来尝试编码器-解码器架构。
下面提供了完整的代码清单。
# univariate multi-step encoder-decoder cnn-lstm
from math import sqrt
from numpy import split
from numpy import array
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from matplotlib import pyplot
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import LSTM
from keras.layers import RepeatVector
from keras.layers import TimeDistributed
from keras.layers.convolutional import Conv1D
from keras.layers.convolutional import MaxPooling1D
# split a univariate dataset into train/test sets
def split_dataset(data):
# split into standard weeks
train, test = data[1:-328], data[-328:-6]
# restructure into windows of weekly data
train = array(split(train, len(train)/7))
test = array(split(test, len(test)/7))
return train, test
# evaluate one or more weekly forecasts against expected values
def evaluate_forecasts(actual, predicted):
scores = list()
# calculate an RMSE score for each day
for i in range(actual.shape[1]):
# calculate mse
mse = mean_squared_error(actual[:, i], predicted[:, i])
# calculate rmse
rmse = sqrt(mse)
# store
scores.append(rmse)
# calculate overall RMSE
s = 0
for row in range(actual.shape[0]):
for col in range(actual.shape[1]):
s += (actual[row, col] - predicted[row, col])**2
score = sqrt(s / (actual.shape[0] * actual.shape[1]))
return score, scores
# summarize scores
def summarize_scores(name, score, scores):
s_scores = ', '.join(['%.1f' % s for s in scores])
print('%s: [%.3f] %s' % (name, score, s_scores))
# convert history into inputs and outputs
def to_supervised(train, n_input, n_out=7):
# flatten data
data = train.reshape((train.shape[0]*train.shape[1], train.shape[2]))
X, y = list(), list()
in_start = 0
# step over the entire history one time step at a time
for _ in range(len(data)):
# define the end of the input sequence
in_end = in_start + n_input
out_end = in_end + n_out
# ensure we have enough data for this instance
if out_end < len(data):
x_input = data[in_start:in_end, 0]
x_input = x_input.reshape((len(x_input), 1))
X.append(x_input)
y.append(data[in_end:out_end, 0])
# move along one time step
in_start += 1
return array(X), array(y)
# train the model
def build_model(train, n_input):
# prepare data
train_x, train_y = to_supervised(train, n_input)
# define parameters
verbose, epochs, batch_size = 0, 20, 16
n_timesteps, n_features, n_outputs = train_x.shape[1], train_x.shape[2], train_y.shape[1]
# reshape output into [samples, timesteps, features]
train_y = train_y.reshape((train_y.shape[0], train_y.shape[1], 1))
# define model
model = Sequential()
model.add(Conv1D(filters=64, kernel_size=3, activation='relu', input_shape=(n_timesteps,n_features)))
model.add(Conv1D(filters=64, kernel_size=3, activation='relu'))
model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
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')
# fit network
model.fit(train_x, train_y, epochs=epochs, batch_size=batch_size, verbose=verbose)
return model
# make a forecast
def forecast(model, history, n_input):
# flatten data
data = array(history)
data = data.reshape((data.shape[0]*data.shape[1], data.shape[2]))
# retrieve last observations for input data
input_x = data[-n_input:, 0]
# reshape into [1, n_input, 1]
input_x = input_x.reshape((1, len(input_x), 1))
# forecast the next week
yhat = model.predict(input_x, verbose=0)
# we only want the vector forecast
yhat = yhat[0]
return yhat
# evaluate a single model
def evaluate_model(train, test, n_input):
# fit model
model = build_model(train, n_input)
# history is a list of weekly data
history = [x for x in train]
# walk-forward validation over each week
predictions = list()
for i in range(len(test)):
# predict the week
yhat_sequence = forecast(model, history, n_input)
# store the predictions
predictions.append(yhat_sequence)
# get real observation and add to history for predicting the next week
history.append(test[i, :])
# evaluate predictions days for each week
predictions = array(predictions)
score, scores = evaluate_forecasts(test[:, :, 0], predictions)
return score, scores
# load the new file
dataset = read_csv('household_power_consumption_days.csv', header=0, infer_datetime_format=True, parse_dates=['datetime'], index_col=['datetime'])
# split into train and test
train, test = split_dataset(dataset.values)
# evaluate model and get scores
n_input = 14
score, scores = evaluate_model(train, test, n_input)
# summarize scores
summarize_scores('lstm', score, scores)
# plot scores
days = ['sun', 'mon', 'tue', 'wed', 'thr', 'fri', 'sat']
pyplot.plot(days, scores, marker='o', label='lstm')
pyplot.show()
运行该示例符合该模型,并总结了测试数据集上的性能。一个小实验表明,使用两个卷积层比只使用一个卷积层使模型更加稳定。由于算法的随机性,您的特定结果可能会有所不同。您可能想尝试运行这个示例几次。
我们可以看到,在这种情况下,模型是熟练的,实现了一个总RMSE评分约372千瓦。
lstm: [372.055] 383.8, 381.6, 339.1, 371.8, 371.8, 319.6, 427.2
还创建了每日RMSE的线图。
9 单变量输入的ConvLSTM编解码器模型
CNN-LSTM方法的进一步扩展是对CNN的卷积(例如CNN如何读取输入序列数据)执行LSTM的每个时间步骤。这种组合称为卷积LSTM,简称ConvLSTM,就像CNN -LSTM也用于时空数据一样。
与为了计算内部状态和状态转换而直接读入数据的LSTM不同,与解释CNN模型输出的CNN -LSTM不同,ConvLSTM使用卷积作为直接读入LSTM单元本身的输入的一部分。
Keras库提供了ConvLSTM2D类,该类支持针对2D数据的ConvLSTM模型。它可以配置为一维多变量时间序列预测。
ConvLSTM2D类默认情况下期望输入数据具有如下形状:
[samples, timesteps, rows, cols, channels]
其中数据的每个时间步长都定义为(行*列)数据点的图像。
我们正在处理一个一维的总功耗序列,如果我们假设使用两周的数据作为输入,我们可以将其解释为一行14列。
对于ConvLSTM,这将是一次读取:也就是说,LSTM将读取一个14天的时间步长,并在这些时间步长之间执行卷积。这并不理想。
相反,我们可以将14天分成两个子序列,子序列的长度为7天。ConvLSTM可以跨两个时间步骤读取数据,并对每个时间步骤中的七天数据执行CNN处理。因此,对于所选择的问题框架,ConvLSTM2D的输入为:
[n, 2, 1, 7, 1]
或者:
- samples:n,表示训练数据集中样本的数量。
- timesteps:2,我们将一个14天的窗口分割为两个子序列。
- rows:1,表示每个子序列的一维形状。
- cols:7,表示每个子序列中的七天。
- channels:1,对于作为输入的单个特性。
您可以研究其他配置,比如将21天的输入分成三个7天的子序列,或提供所有8个特性或通道作为输入。
我们现在可以为ConvLSTM2D模型准备数据。首先,我们必须将训练数据集重塑为[samples, timesteps, rows, cols, channels]的预期结构。
# reshape into subsequences [samples, time steps, rows, cols, channels]
train_x = train_x.reshape((train_x.shape[0], n_steps, 1, n_length, n_features))
然后,我们可以将编码器定义为一个ConvLSTM隐藏层,然后是一个准备解码的flatten层。
model.add(ConvLSTM2D(filters=64, kernel_size=(1,3), activation='relu', input_shape=(n_steps, 1, n_length, n_features)))
model.add(Flatten())
我们还将参数化子序列的数量(n_steps)和每个子序列的长度(n_length),并将它们作为参数传递。
模型的其余部分和训练是相同的。带有这些更改的build_model()函数如下所示。
# train the model
def build_model(train, n_steps, n_length, n_input):
# prepare data
train_x, train_y = to_supervised(train, n_input)
# define parameters
verbose, epochs, batch_size = 0, 20, 16
n_timesteps, n_features, n_outputs = train_x.shape[1], train_x.shape[2], train_y.shape[1]
# reshape into subsequences [samples, time steps, rows, cols, channels]
train_x = train_x.reshape((train_x.shape[0], n_steps, 1, n_length, n_features))
# reshape output into [samples, timesteps, features]
train_y = train_y.reshape((train_y.shape[0], train_y.shape[1], 1))
# define model
model = Sequential()
model.add(ConvLSTM2D(filters=64, kernel_size=(1,3), activation='relu', input_shape=(n_steps, 1, n_length, n_features)))
model.add(Flatten())
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')
# fit network
model.fit(train_x, train_y, epochs=epochs, batch_size=batch_size, verbose=verbose)
return model
该模型期望五维数据作为输入。因此,在进行预测时,我们还必须更新forecast()函数中单个样本的准备。
# reshape into [samples, time steps, rows, cols, channels]
input_x = input_x.reshape((1, n_steps, 1, n_length, 1))
下面提供了带有此更改和参数化子序列的forecast()函数。
# make a forecast
def forecast(model, history, n_steps, n_length, n_input):
# flatten data
data = array(history)
data = data.reshape((data.shape[0]*data.shape[1], data.shape[2]))
# retrieve last observations for input data
input_x = data[-n_input:, 0]
# reshape into [samples, time steps, rows, cols, channels]
input_x = input_x.reshape((1, n_steps, 1, n_length, 1))
# forecast the next week
yhat = model.predict(input_x, verbose=0)
# we only want the vector forecast
yhat = yhat[0]
return yhat
我们现在有了所有的元素来评估一个多步时间序列预测的编码器-解码器体系结构,其中使用ConvLSTM作为编码器。
完整的代码示例如下所示。
# univariate multi-step encoder-decoder convlstm
from math import sqrt
from numpy import split
from numpy import array
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from matplotlib import pyplot
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import LSTM
from keras.layers import RepeatVector
from keras.layers import TimeDistributed
from keras.layers import ConvLSTM2D
# split a univariate dataset into train/test sets
def split_dataset(data):
# split into standard weeks
train, test = data[1:-328], data[-328:-6]
# restructure into windows of weekly data
train = array(split(train, len(train)/7))
test = array(split(test, len(test)/7))
return train, test
# evaluate one or more weekly forecasts against expected values
def evaluate_forecasts(actual, predicted):
scores = list()
# calculate an RMSE score for each day
for i in range(actual.shape[1]):
# calculate mse
mse = mean_squared_error(actual[:, i], predicted[:, i])
# calculate rmse
rmse = sqrt(mse)
# store
scores.append(rmse)
# calculate overall RMSE
s = 0
for row in range(actual.shape[0]):
for col in range(actual.shape[1]):
s += (actual[row, col] - predicted[row, col])**2
score = sqrt(s / (actual.shape[0] * actual.shape[1]))
return score, scores
# summarize scores
def summarize_scores(name, score, scores):
s_scores = ', '.join(['%.1f' % s for s in scores])
print('%s: [%.3f] %s' % (name, score, s_scores))
# convert history into inputs and outputs
def to_supervised(train, n_input, n_out=7):
# flatten data
data = train.reshape((train.shape[0]*train.shape[1], train.shape[2]))
X, y = list(), list()
in_start = 0
# step over the entire history one time step at a time
for _ in range(len(data)):
# define the end of the input sequence
in_end = in_start + n_input
out_end = in_end + n_out
# ensure we have enough data for this instance
if out_end < len(data):
x_input = data[in_start:in_end, 0]
x_input = x_input.reshape((len(x_input), 1))
X.append(x_input)
y.append(data[in_end:out_end, 0])
# move along one time step
in_start += 1
return array(X), array(y)
# train the model
def build_model(train, n_steps, n_length, n_input):
# prepare data
train_x, train_y = to_supervised(train, n_input)
# define parameters
verbose, epochs, batch_size = 0, 20, 16
n_timesteps, n_features, n_outputs = train_x.shape[1], train_x.shape[2], train_y.shape[1]
# reshape into subsequences [samples, time steps, rows, cols, channels]
train_x = train_x.reshape((train_x.shape[0], n_steps, 1, n_length, n_features))
# reshape output into [samples, timesteps, features]
train_y = train_y.reshape((train_y.shape[0], train_y.shape[1], 1))
# define model
model = Sequential()
model.add(ConvLSTM2D(filters=64, kernel_size=(1,3), activation='relu', input_shape=(n_steps, 1, n_length, n_features)))
model.add(Flatten())
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')
# fit network
model.fit(train_x, train_y, epochs=epochs, batch_size=batch_size, verbose=verbose)
return model
# make a forecast
def forecast(model, history, n_steps, n_length, n_input):
# flatten data
data = array(history)
data = data.reshape((data.shape[0]*data.shape[1], data.shape[2]))
# retrieve last observations for input data
input_x = data[-n_input:, 0]
# reshape into [samples, time steps, rows, cols, channels]
input_x = input_x.reshape((1, n_steps, 1, n_length, 1))
# forecast the next week
yhat = model.predict(input_x, verbose=0)
# we only want the vector forecast
yhat = yhat[0]
return yhat
# evaluate a single model
def evaluate_model(train, test, n_steps, n_length, n_input):
# fit model
model = build_model(train, n_steps, n_length, n_input)
# history is a list of weekly data
history = [x for x in train]
# walk-forward validation over each week
predictions = list()
for i in range(len(test)):
# predict the week
yhat_sequence = forecast(model, history, n_steps, n_length, n_input)
# store the predictions
predictions.append(yhat_sequence)
# get real observation and add to history for predicting the next week
history.append(test[i, :])
# evaluate predictions days for each week
predictions = array(predictions)
score, scores = evaluate_forecasts(test[:, :, 0], predictions)
return score, scores
# load the new file
dataset = read_csv('household_power_consumption_days.csv', header=0, infer_datetime_format=True, parse_dates=['datetime'], index_col=['datetime'])
# split into train and test
train, test = split_dataset(dataset.values)
# define the number of subsequences and the length of subsequences
n_steps, n_length = 2, 7
# define the total days to use as input
n_input = n_length * n_steps
score, scores = evaluate_model(train, test, n_steps, n_length, n_input)
# summarize scores
summarize_scores('lstm', score, scores)
# plot scores
days = ['sun', 'mon', 'tue', 'wed', 'thr', 'fri', 'sat']
pyplot.plot(days, scores, marker='o', label='lstm')
pyplot.show()
运行该示例符合该模型,并总结了测试数据集上的性能。一个小实验表明,使用两个卷积层比只使用一个卷积层使模型更加稳定。
我们可以看到,在这种情况下,模型是熟练的,实现了一个总RMSE评分约367千瓦。
lstm: [367.929] 416.3, 379.7, 334.7, 362.3, 374.7, 284.8, 406.7
还创建了每日RMSE的线图。
扩展
本节列出了一些扩展教程的想法,您可能希望探索这些想法。
- 输入的大小。探索用作模型输入的天数,例如3天、21天、30天或更多。
- 模型调优。调整模型的结构和超参数,进一步提高模型的平均性能。
- 数据扩展。研究数据扩展(如标准化和规范化)是否可以用于提高任何LSTM模型的性能。
- 学习诊断。使用诸如训练学习曲线、验证损失和均方误差等诊断来帮助调整LSTM模型的结构和超参数。