深度学习实战案例:基于LSTM 的洗发水销量预测(附完整代码)

在本案例中,你将了解如何为单变量时间序列预测问题开发 LSTM 预测模型。

技术提升

技术要学会分享、交流,不建议闭门造车。一个人走的很快、一堆人可以走的更远。

完整代码、数据、技术交流提升, 均可加入知识星球交流群获取,群友已超过2000人,添加时切记的备注方式为:来源+兴趣方向,方便找到志同道合的朋友。

方式①、添加微信号:pythoner666,备注:来自 CSDN + lstm洗发水预测
方式②、微信搜索公众号:Python学习与数据挖掘,后台回复:资料

洗发水销售数据集

该数据集描述了 3 年期间洗发水的月销量。

下面的示例加载并创建加载数据集的图。

# load and plot dataset
from pandas import read_csv
from pandas import datetime
from matplotlib import pyplot
# load dataset
def parser(x):
returndatetime.strptime('190'+x,'%Y-%m')
series=read_csv('shampoo-sales.csv',header=0,parse_dates=[0],index_col=0,squeeze=True,date_parser=parser)
# summarize first few rows
print(series.head())
# line plot
series.plot()
pyplot.show()

运行该示例将数据集加载为 Pandas 系列并打印前 5 行。

Month
1901-01-01 266.0
1901-02-01 145.9
1901-03-01 183.1
1901-04-01 119.3
1901-05-01 180.3
Name: Sales, dtype: float64

然后创建该系列的线图,显示明显的增加趋势。

训练测试设置

我们将洗发水销售数据集分为两部分:训练集和测试集。

前两年的数据将用于训练数据集,剩余一年的数据将用于测试集。

# split data into train and test
X = series.values
train, test = X[0:-12], X[-12:]

模型将使用训练数据集开发,并将对测试数据集进行预测。

将使用滚动预测场景,也称为前向模型验证。

测试数据集的每个时间步将一次走一个。模型将用于对时间步长进行预测,然后将获取测试集中的实际预期值并将其提供给模型以用于下一个时间步长的预测。

例如:

# walk-forward validation
history = [x for x in train]
predictions = list()
for i in range(len(test)):
	# make prediction...

这模拟了一个真实世界的场景,其中每个月都会提供新的洗发水销售观察结果,并用于下个月的预测。

最后,将收集测试数据集上的所有预测并计算错误分数以总结模型的技能。将使用均方根误差 (RMSE),因为它会惩罚较大的误差并得出与预测数据单位相同的分数,即每月洗发水销量。

例如:

from sklearn.metrics import mean_squared_error
rmse = sqrt(mean_squared_error(test, predictions))
print('RMSE: %.3f' % rmse)

持久性模型预测

具有线性增长趋势的时间序列的良好基线预测是持久性预测。

持久性预测是使用先前时间步长 (t-1) 的观察结果来预测当前时间步长 (t) 的观察结果。

我们可以通过从前向验证积累的训练数据和历史记录中获取最后一次观察,并使用它来预测当前时间步长来实现这一点。

例如:

# make prediction
yhat = history[-1]

我们会将所有预测累积在一个数组中,以便可以将它们直接与测试数据集进行比较。

下面列出了洗发水销售数据集上持久性预测模型的完整示例。

from pandas import read_csv
from pandas import datetime
from sklearn.metrics import mean_squared_error
from math import sqrt
from matplotlib import pyplot
# load dataset
def parser(x):
	return datetime.strptime('190'+x, '%Y-%m')
series = read_csv('shampoo-sales.csv', header=0, parse_dates=[0], index_col=0, squeeze=True, date_parser=parser)
# split data into train and test
X = series.values
train, test = X[0:-12], X[-12:]
# walk-forward validation
history = [x for x in train]
predictions = list()
for i in range(len(test)):
	# make prediction
	predictions.append(history[-1])
	# observation
	history.append(test[i])
# report performance
rmse = sqrt(mean_squared_error(test, predictions))
print('RMSE: %.3f' % rmse)
# line plot of observed vs predicted
pyplot.plot(test)
pyplot.plot(predictions)
pyplot.show()

运行该示例会为测试数据集上的预测打印大约 136 个月洗发水销售额的 RMSE。

RMSE: 136.761

LSTM 数据准备

在我们可以将 LSTM 模型拟合到数据集之前,我们必须转换数据。

本节分为三个步骤:

  1. 将时间序列转化为监督学习问题
  2. 转换时间序列数据,使其平稳。
  3. 将观察结果转换为具有特定比例。

将时间序列转换为监督学习

Keras 中的 LSTM 模型假设你的数据分为输入 (X) 和输出 (y) 部分。

对于时间序列问题,我们可以通过使用上一个时间步 (t-1) 的观察作为输入,并将当前时间步 (t) 的观察作为输出来实现这一点。

我们可以使用 Pandas 中的shift()函数来实现这一点,该函数会将系列中的所有值向下推指定的数字位。我们需要移动 1 位,这将成为输入变量。目前的时间序列将是输出变量。

然后我们可以将这两个系列连接在一起,创建一个为监督学习做好准备的 DataFrame。下推系列将在顶部有一个没有价值的新位置。此位置将使用 NaN(不是数字)值。我们将用 0 值替换这些 NaN 值,LSTM 模型必须将其学习为“系列的开始”或“我这里没有数据”,因为尚未观察到该数据集上销售额为零的月份。

下面的代码定义了一个辅助函数来执行此操作,称为_timeseries_to_supervised()_。它采用原始时间序列数据的 NumPy 数组和滞后或移位序列的数量来创建和用作输入。

# frame a sequence as a supervised learning problem
def timeseries_to_supervised(data, lag=1):
	df = DataFrame(data)
	columns = [df.shift(i) for i in range(1, lag+1)]
	columns.append(df)
	df = concat(columns, axis=1)
	df.fillna(0, inplace=True)
	return df

我们可以使用加载的洗发水销售数据集测试此功能,并将其转换为监督学习问题。

from pandas import read_csv
from pandas import datetime
from pandas import DataFrame
from pandas import concat

# frame a sequence as a supervised learning problem
def timeseries_to_supervised(data, lag=1):
	df = DataFrame(data)
	columns = [df.shift(i) for i in range(1, lag+1)]
	columns.append(df)
	df = concat(columns, axis=1)
	df.fillna(0, inplace=True)
	return df

# load dataset
def parser(x):
	return datetime.strptime('190'+x, '%Y-%m')
series = read_csv('shampoo-sales.csv', header=0, parse_dates=[0], index_col=0, squeeze=True, date_parser=parser)
# transform to supervised learning
X = series.values
supervised = timeseries_to_supervised(X, 1)
print(supervised.head())

运行示例打印新监督学习问题的前 5 行。

            0           0
0    0.000000  266.000000
1  266.000000  145.899994
2  145.899994  183.100006
3  183.100006  119.300003
4  119.300003  180.300003

将时间序列转换为平稳

Shampoo Sales 数据集不是固定的。这意味着数据中存在依赖于时间的结构。具体来说,数据有增加的趋势。

固定数据更容易建模,并且很可能会产生更熟练的预测。趋势可以从观察中移除,然后再添加回预测,以将预测返回到原始规模并计算可比较的错误分数。

消除趋势的标准方法是对数据进行差分。即从当前观察值 (t) 中减去前一时间步长 (t-1) 的观察值。这消除了趋势,我们留下了一个差异序列,或者从一个时间步到下一个时间步的观察变化。

我们可以使用pandas 中的diff() 函数自动实现这一点。或者,我们可以获得更细粒度的控制并编写我们自己的函数来执行此操作,在这种情况下,这是首选的灵活性。

下面是一个名为_difference()_的函数,它计算差分序列。请注意,系列中的第一个观察值被跳过,因为没有可用于计算差值的先前观察值。

# create a differenced series
def difference(dataset, interval=1):
	diff = list()
	for i in range(interval, len(dataset)):
		value = dataset[i] - dataset[i - interval]
		diff.append(value)
	return Series(diff)

我们还需要反转这个过程,以便将对差分序列所做的预测恢复到原来的规模。

下面的函数,称为_inverse_difference()_,反转这个操作。

# invert differenced value
def inverse_difference(history, yhat, interval=1):
	return yhat + history[-interval]

我们可以通过对整个系列进行差分来测试这些功能,然后将其恢复到原始比例,如下所示:

from pandas import read_csv
from pandas import datetime
from pandas import Series

# create a differenced series
def difference(dataset, interval=1):
	diff = list()
	for i in range(interval, len(dataset)):
		value = dataset[i] - dataset[i - interval]
		diff.append(value)
	return Series(diff)

# invert differenced value
def inverse_difference(history, yhat, interval=1):
	return yhat + history[-interval]

# load dataset
def parser(x):
	return datetime.strptime('190'+x, '%Y-%m')
series = read_csv('shampoo-sales.csv', header=0, parse_dates=[0], index_col=0, squeeze=True, date_parser=parser)
print(series.head())
# transform to be stationary
differenced = difference(series, 1)
print(differenced.head())
# invert transform
inverted = list()
for i in range(len(differenced)):
	value = inverse_difference(series, differenced[i], len(series)-i)
	inverted.append(value)
inverted = Series(inverted)
print(inverted.head())

运行示例打印加载数据的前 5 行,然后是差分序列的前 5 行,最后是差分操作反转的前 5 行。

请注意,原始数据集中的第一个观察值已从倒置差异数据中删除。除此之外,最后一组数据与预期的第一组数据匹配。

Month
1901-01-01    266.0
1901-02-01    145.9
1901-03-01    183.1
1901-04-01    119.3
1901-05-01    180.3

Name: Sales, dtype: float64
0   -120.1
1     37.2
2    -63.8
3     61.0
4    -11.8
dtype: float64

0    145.9
1    183.1
2    119.3
3    180.3
4    168.5
dtype: float64

将时间序列转换为比例

与其他神经网络一样,LSTM 期望数据在网络使用的激活函数范围内。

LSTM 的默认激活函数是双曲正切 ( tanh ),其输出值介于 -1 和 1 之间。这是时间序列数据的首选范围。

为了使实验公平,必须在训练数据集上计算缩放系数(最小值和最大值)值,并将其应用于缩放测试数据集和任何预测。这是为了避免用来自测试数据集的知识污染实验,这可能会给模型带来小的优势。

我们可以使用MinMaxScaler 类将数据集转换为范围 [-1, 1] 。与其他 scikit-learn 转换类一样,它需要以具有行和列的矩阵格式提供数据。因此,我们必须在转换之前重塑我们的 NumPy 数组。

例如:

# transform scale
X = series.values
X = X.reshape(len(X), 1)
scaler = MinMaxScaler(feature_range=(-1, 1))
scaler = scaler.fit(X)
scaled_X = scaler.transform(X)

同样,我们必须反转预测的比例以将值返回到原始比例,以便可以解释结果并计算可比较的错误分数。

# invert transform
inverted_X = scaler.inverse_transform(scaled_X)

运行该示例首先打印加载数据的前 5 行,然后是缩放数据的前 5 行,然后是缩放转换后的前 5 行,与原始数据匹配。

Month
1901-01-01    266.0
1901-02-01    145.9
1901-03-01    183.1
1901-04-01    119.3
1901-05-01    180.3

Name: Sales, dtype: float64
0   -0.478585
1   -0.905456
2   -0.773236
3   -1.000000
4   -0.783188
dtype: float64

0    266.0
1    145.9
2    183.1
3    119.3
4    180.3
dtype: float64

现在我们知道如何为 LSTM 网络准备数据,我们可以开始开发我们的模型了。

LSTM 模型开发

长短期记忆网络 (LSTM) 是一种循环神经网络 (RNN)。这种类型的网络的一个好处是它可以学习和记忆长序列,并且不依赖于预先指定的窗口滞后观察作为输入。

在 Keras 中,这被称为有状态的,并且涉及在定义 LSTM 层时将“有状态”参数设置为“ True ”。

默认情况下,Keras 中的 LSTM 层维护一批数据之间的状态。一批数据是来自训练数据集的固定大小的行数,它定义了在更新网络权重之前要处理的模式数。默认情况下,批次之间的 LSTM 层中的状态被清除,因此我们必须使 LSTM 有状态。_这让我们可以通过调用reset_states()_函数对何时清除 LSTM 层的状态进行细粒度控制。

LSTM 层期望输入位于具有以下维度的矩阵中:[样本、时间步长、特征]。

  • 样本:这些是来自领域的独立观察,通常是数据行。
  • 时间步长:这些是给定观察的给定变量的单独时间步长。
  • 特点:这些是在观察时观察到的单独措施。

我们在如何为网络构建洗发水销售数据集方面具有一定的灵活性。我们将保持简单并将问题框定为原始序列中的每个时间步都是一个单独的样本,具有一个时间步和一个特征。

鉴于训练数据集定义为 X 输入和 y 输出,因此必须将其重新整形为 Samples/TimeSteps/Features 格式,例如:

X, y = train[:, 0:-1], train[:, -1]
X = X.reshape(X.shape[0], 1, X.shape[1])

输入数据的形状必须在 LSTM 层中指定,使用“ batch_input_shape ”参数作为一个元组,指定读取每个批次的预期观察数、时间步数和特征数。

批量大小通常远小于样本总数。它与历元数一起定义了网络学习数据的速度(权重更新的频率)。

定义 LSTM 层的最后一个重要参数是神经元的数量,也称为记忆单元或块的数量。这是一个相当简单的问题,1 到 5 之间的数字应该足够了。

下面的行创建了一个 LSTM 隐藏层,它还通过“ batch_input_shape ”参数指定了输入层的期望。

layer = LSTM(neurons, batch_input_shape=(batch_size, X.shape[1], X.shape[2]), stateful=True)

该网络需要输出层中的单个神经元具有线性激活,以预测下一时间步的洗发水销售数量。

指定网络后,必须使用后端数学库(例如 TensorFlow 或 Theano)将其编译成有效的符号表示。

在编译网络时,我们必须指定损失函数和优化算法。我们将使用“ mean_squared_error ”作为损失函数,因为它与我们感兴趣的 RMSE 和高效的 ADAM 优化算法非常匹配。

使用 Sequential Keras API 定义网络,以下代码片段创建并编译网络。

model = Sequential()
model.add(LSTM(neurons, batch_input_shape=(batch_size, X.shape[1], X.shape[2]), stateful=True))
model.add(Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')

下面是一个手动使网络适合训练数据的循环。

for i in range(nb_epoch):
	model.fit(X, y, epochs=1, batch_size=batch_size, verbose=0, shuffle=False)
	model.reset_states()

综上所述,我们可以定义一个名为_fit_lstm()_的函数来训练并返回 LSTM 模型。作为参数,它采用监督学习格式的训练数据集、批量大小、多个时期和多个神经元。

def fit_lstm(train, batch_size, nb_epoch, neurons):
	X, y = train[:, 0:-1], train[:, -1]
	model = Sequential()
	model.add(LSTM(neurons, batch_input_shape=(batch_size, X.shape[1], X.shape[2]), stateful=True))
	model.add(Dense(1))
	model.compile(loss='mean_squared_error', optimizer='adam')
	for i in range(nb_epoch):
		model.reset_states()
	return model

batch_size 必须设置为 1。这是因为它必须是训练和测试数据集大小的一个因素。

模型上的_predict()_函数也受批量大小的限制;那里必须将其设置为 1,因为我们有兴趣对测试数据进行一步预测。

LSTM 预测

一旦 LSTM 模型适合训练数据,它就可以用来进行预测。同样,我们有一些灵活性。我们可以决定在所有训练数据上对模型进行一次拟合,然后从测试数据中一次预测每个新的时间步长(我们称之为固定方法),或者我们可以重新拟合模型或更新测试数据的每个时间步的模型作为测试数据的新观察可用(我们将其称为动态方法)。

在本教程中,我们将采用简单的固定方法,但我们希望动态方法能够产生更好的模型技能。

要进行预测,我们可以在模型上调用_predict()_函数。这需要一个 3D NumPy 数组输入作为参数。在这种情况下,它将是一个包含一个值的数组,即前一时间步的观察值。

_predict()_函数返回一个预测数组,一个对应于提供的每个输入行。因为我们提供的是单一输入,所以输出将是具有一个值的二维 NumPy 数组。

我们可以在下面列出的名为_forecast()_的函数中捕获此行为。给定拟合模型、拟合模型时使用的批量大小(例如 1)和测试数据中的一行,该函数将从测试行中分离出输入数据,对其进行整形,并将预测作为单个结果返回浮点值。

def forecast(model, batch_size, row):
	X = row[0:-1]
	X = X.reshape(1, 1, len(X))
	yhat = model.predict(X, batch_size=batch_size)
	return yhat[0,0]

在训练期间,内部状态在每个纪元后重置。在预测时,我们不想在预测之间重置内部状态。事实上,我们希望模型在我们预测测试数据集中的每个时间步时建立状态。

示例

运行该示例会打印测试数据集中 12 个月中每个月的预期值和预测值。

该示例还打印所有预测的 RMSE。该模型显示 RMSE 为 71.721 的每月洗发水销量,优于实现 136.761 的 RMSE 洗发水销量的持久性模型。

Month=1, Predicted=351.582196, Expected=339.700000
Month=2, Predicted=432.169667, Expected=440.400000
Month=3, Predicted=378.064505, Expected=315.900000
Month=4, Predicted=441.370077, Expected=439.300000
Month=5, Predicted=446.872627, Expected=401.300000
Month=6, Predicted=514.021244, Expected=437.400000
Month=7, Predicted=525.608903, Expected=575.500000
Month=8, Predicted=473.072365, Expected=407.600000
Month=9, Predicted=523.126979, Expected=682.000000
Month=10, Predicted=592.274106, Expected=475.300000
Month=11, Predicted=589.299863, Expected=581.300000
Month=12, Predicted=584.149152, Expected=646.900000
Test RMSE: 71.721

还创建了测试数据(蓝色)与预测值(橙色)的线图,为模型技能提供了上下文。

开发稳健的结果

神经网络的一个困难是它们会在不同的起始条件下给出不同的结果。一种方法可能是修复 Keras 使用的随机数种子以确保结果可重现。另一种方法是使用不同的实验设置来控制随机初始条件。

我们可以多次重复上一节中的实验,然后将平均 RMSE 作为配置在未见数据上的平均性能预期指标。这通常称为多次重复或多次重新启动。

我们可以将模型拟合和步进验证包装在一个固定重复次数的循环中。可以记录每次迭代运行的 RMSE。然后我们可以总结 RMSE 分数的分布。

# repeat experiment
repeats = 30
error_scores = list()
for r in range(repeats):
	# fit the model
	lstm_model = fit_lstm(train_scaled, 1, 3000, 4)
	# forecast the entire training dataset to build up state for forecasting
	train_reshaped = train_scaled[:, 0].reshape(len(train_scaled), 1, 1)
	lstm_model.predict(train_reshaped, batch_size=1)
	# walk-forward validation on the test data
	predictions = list()
	for i in range(len(test_scaled)):
		# make one-step forecast
		X, y = test_scaled[i, 0:-1], test_scaled[i, -1]
		yhat = forecast_lstm(lstm_model, 1, X)
		# invert scaling
		yhat = invert_scale(scaler, X, yhat)
		# invert differencing
		yhat = inverse_difference(raw_values, yhat, len(test_scaled)+1-i)
		# store forecast
		predictions.append(yhat)
	# report performance
	rmse = sqrt(mean_squared_error(raw_values[-12:], predictions))
	print('%d) Test RMSE: %.3f' % (r+1, rmse))
	error_scores.append(rmse)

数据准备和以前一样。

我们将使用 30 次重复,因为这足以提供良好的 RMSE 分数分布。运行该示例会在每次重复时打印 RMSE 分数。运行结束时提供收集的 RMSE 分数的汇总统计信息。

我们可以看到均值和标准差 RMSE 得分分别为每月洗发水销量 138.491905 和 46.313783。这是一个非常有用的结果,因为它表明上面报告的结果可能是统计上的侥幸。实验表明,该模型的平均水平可能与持久性模型 (136.761) 一样好,如果不是稍差的话。

这表明,至少,需要进一步的模型调整。

1) Test RMSE: 136.191
2) Test RMSE: 169.693
3) Test RMSE: 176.553
4) Test RMSE: 198.954
5) Test RMSE: 148.960
6) Test RMSE: 103.744
7) Test RMSE: 164.344
8) Test RMSE: 108.829
9) Test RMSE: 232.282
10) Test RMSE: 110.824
11) Test RMSE: 163.741
12) Test RMSE: 111.535
13) Test RMSE: 118.324
14) Test RMSE: 107.486
15) Test RMSE: 97.719
16) Test RMSE: 87.817
17) Test RMSE: 92.920
18) Test RMSE: 112.528
19) Test RMSE: 131.687
20) Test RMSE: 92.343
21) Test RMSE: 173.249
22) Test RMSE: 182.336
23) Test RMSE: 101.477
24) Test RMSE: 108.171
25) Test RMSE: 135.880
26) Test RMSE: 254.507
27) Test RMSE: 87.198
28) Test RMSE: 122.588
29) Test RMSE: 228.449
30) Test RMSE: 94.427
             rmse
count   30.000000
mean   138.491905
std     46.313783
min     87.198493
25%    104.679391
50%    120.456233
75%    168.356040
max    254.507272

盒须图是根据如下所示的分布创建的。这会捕获数据的中间部分以及范围和离群值结果。

扩展

我们可以考虑对本教程进行许多扩展。
也许你可以自己探索其中的一些,并在下面的评论中发表你的发现。

  • 多步预测。可以更改实验设置以预测接下来的_n 个_时间步长而不是下一个单个时间步长。这也将允许更大的批量和更快的训练。请注意,鉴于模型未更新,我们基本上在本教程中执行一种 12 步预测,尽管有新的观测值可用并用作输入变量。
  • 调整 LSTM 模型。模型没有调整;相反,配置是通过一些快速的反复试验找到的。我相信通过至少调整神经元的数量和训练时期的数量可以获得更好的结果。我还认为通过回调提前停止可能在训练期间很有用。
  • 种子状态实验。目前尚不清楚通过预测所有训练数据在预测之前为系统播种是否有益。这在理论上似乎是个好主意,但这需要证明。此外,也许其他在预测之前为模型播种的方法也会有所帮助。
  • 更新模型。可以在步进验证的每个时间步更新模型。需要进行实验来确定是从头开始重新拟合模型还是使用更多训练周期(包括新样本)更新权重会更好。
  • 输入时间步长。LSTM 输入支持样本的多个时间步长。需要进行实验以查看将滞后观察作为时间步长是否会带来任何好处。
  • 输入滞后特性。滞后观测值可以作为输入特征包含在内。需要进行实验以查看包含滞后特征是否会带来任何好处,这与 AR(k) 线性模型不同。
  • 输入错误系列。可以构造一个误差序列(从持久性模型预测误差)并将其用作附加输入特征,这与 MA(k) 线性模型不同。需要进行实验,看看这是否有任何好处。
  • 学习非平稳。LSTM 网络或许能够了解数据中的趋势并做出合理的预测。需要进行实验,看看是否可以通过 LSTM 学习和有效预测数据中遗留的时间相关结构,如趋势和季节性。
  • 对比无状态。本教程中使用了有状态 LSTM。结果应与无状态 LSTM 配置进行比较。
  • 统计显着性。多次重复实验协议可以进一步扩展以包括统计显着性测试,以证明具有不同配置的 RMSE 结果群体之间的差异是否具有统计显着性。

猜你喜欢

转载自blog.csdn.net/weixin_38037405/article/details/130462165