LSTM:在Python中使用PyTorch使用LSTM进行时间序列预测

3,615 阅读12分钟

LSTM:在Python中使用PyTorch使用LSTM进行时间序列预测

Time Series Prediction using LSTM with PyTorch in Python

翻译自:stackabuse.com/time-series…

时间序列数据,顾名思义,是一种随着时间改变的数据。例如,24小时气温数据,一个月得分产品价格数据,某一公司股票价格年度数据。高级深度学习模型,比如长短期记忆网络(LSTM),能够捕获到时间序列数据中的变化模式,进而能够预测数据的未来趋势。在这篇文章中,你将会看到如何利用LSTM算法来对时间序列数据进行预测。

在我早些时候的文章中,我展示了如何运用Keras库并利用LSTM进行时间序列分析,以预测未来的股票价格。将使用PyTorch库,它是最常用的深度学习的Python库之一。

在你继续之前,假定你对Python编程语言有中级水平的熟练度,并且你已经安装了PyTorch库。此外,对基本的机器学习概念和深度学习概念的了解也会有所帮助。如果你还没有安装PyTorch,你可以通过以下pip命令来安装。

$ pip install pytorch

数据集和问题定义

我们将使用Seaborn库的内建数据集。首先,让我们导入需要的库,然后倒入数据集:

import torch
import torch.nn as nn

import seaborn as sns
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

%matplotlib inline

让我们打印一下Seaborn的所有内建数据库:

sns.get_dataset_names()

输出:

['anscombe',
 'attention',
 'brain_networks',
 'car_crashes',
 'diamonds',
 'dots',
 'exercise',
 'flights',
 'fmri',
 'gammas',
 'iris',
 'mpg',
 'planets',
 'tips',
 'titanic']

由于众所周知的原因,大家可以在Github上先下载整个数据包。

我们将使用flights数据集。如果没有网络问题可以用如下代码导入:

flight_data = sns.load_dataset("flights")
flight_data.head()

数据集有3列:年,月和乘客数量。乘客数量一列描述了单月内航班乘客总数。数据集的形状:

flight_data.shape

output:
(144,3)

可以看到,一共有144行和3列数据,即数据集包含12年的乘客记录。 我们的任务是利用前132个月的数据预测最后12个月乘客数。也就是说前132个月的数据用作训练,最后12个月的数据用作验证以评估模型。

让我们来绘制每个月乘客出行的频率。下面的脚本增加了默认的绘图大小。

fig_size = plt.rcParams["figure.figsize"]
fig_size[0] = 15
fig_size[1] = 5
plt.rcParams["figure.figsize"] = fig_size

而接下来的这个脚本绘制了乘客数量的每月频率。

plt.title('Month vs Passenger')
plt.ylabel('Total Passengers')
plt.xlabel('Months')
plt.grid(True)
plt.autoscale(axis='x',tight=True)
plt.plot(flight_data['passengers'])

months_vs_passagers.png

如图所示,多年来,乘飞机旅行的平均人数增加了。一年内旅行的乘客数量是波动的,这是有道理的,因为在夏季或冬季休假期间,旅行的乘客数量比一年中的其他时间增加。

数据处理

数据集中列的类型是 object,如下面的代码所示:

flight_data.columns

输出:

Index(['year', 'month', 'passengers'], dtype='object')

数据处理的第一步是将乘客数量一列的数据类型转换为float

all_data = flight_data['passengers'].values.astype(float)

现在,如果你打印all_data numpy数组,你应该看到以下float类型的值。

[112. 118. 132. 129. 121. 135. 148. 148. 136. 119. 104. 118. 115. 126.
 141. 135. 125. 149. 170. 170. 158. 133. 114. 140. 145. 150. 178. 163.
 172. 178. 199. 199. 184. 162. 146. 166. 171. 180. 193. 181. 183. 218.
 230. 242. 209. 191. 172. 194. 196. 196. 236. 235. 229. 243. 264. 272.
 237. 211. 180. 201. 204. 188. 235. 227. 234. 264. 302. 293. 259. 229.
 203. 229. 242. 233. 267. 269. 270. 315. 364. 347. 312. 274. 237. 278.
 284. 277. 317. 313. 318. 374. 413. 405. 355. 306. 271. 306. 315. 301.
 356. 348. 355. 422. 465. 467. 404. 347. 305. 336. 340. 318. 362. 348.
 363. 435. 491. 505. 404. 359. 310. 337. 360. 342. 406. 396. 420. 472.
 548. 559. 463. 407. 362. 405. 417. 391. 419. 461. 472. 535. 622. 606.
 508. 461. 390. 432.]

接下来,我们将把我们的数据集分为训练集和测试集。LSTM算法将在训练集上进行训练。然后,该模型将被用来对测试集进行预测。预测结果将与测试集的实际值进行比较,以评估训练模型的性能。

前132条记录将被用来训练模型,最后12条记录将被用作测试集。下面的脚本将数据分为训练集和测试集。

test_data_size = 12

train_data = all_data[:-test_data_size]
test_data = all_data[-test_data_size:]

我们的数据集目前还没有被规范化(normalization)。最初几年的乘客总数与后来几年的乘客总数相比要少得多。对于时间序列预测来说,将数据标准化是非常重要的。我们将对数据集进行最小/最大缩放,使数据在一定的最小值和最大值范围内正常化。我们将使用sklearn.preprocessing模块中的MinMaxScaler类来扩展我们的数据。关于最小/最大缩放器实现的进一步细节,请访问这个链接

下面的代码使用最小/最大标度器对我们的数据进行标准化处理,最小值和最大值分别为-1和1。

from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler(feature_range=(-1, 1))
train_data_normalized = scaler.fit_transform(train_data .reshape(-1, 1))

这里需要提到的是,数据规范化只适用于训练数据,而不是测试数据。如果在测试数据上应用归一化,有可能会有一些信息从训练集泄露到测试集。

下一步是将我们的数据集转换成张量,因为PyTorch模型是使用张量进行训练的。为了将数据集转换为张量,我们可以简单地将我们的数据集传递给FloatTensor对象的构造函数,如下所示。

train_data_normalized = torch.FloatTensor(train_data_normalized).view(-1)

最后的预处理步骤是将我们的训练数据转换成序列和相应的标签。

你可以使用任何序列长度,这取决于领域知识。然而,在我们的数据集中,使用12的序列长度是很方便的,因为我们有月度数据,一年有12个月。如果我们有每日数据,更好的序列长度是365,即一年中的天数。因此,我们将训练时的输入序列长度设置为12。

train_window = 12

接下来,我们将定义一个名为create_inout_sequences的函数。该函数将接受原始输入数据,并将返回一个元组列表。在每个元组中,第一个元素将包含12个项目的列表,对应于12个月内旅行的乘客数量,第二个元组元素将包含一个项目,即12+1个月内的乘客数量。

def create_inout_sequences(input_data, tw):
    inout_seq = []
    L = len(input_data)
    for i in range(L-tw):
        train_seq = input_data[i:i+tw]
        train_label = input_data[i+tw:i+tw+1]
        inout_seq.append((train_seq ,train_label))
    return inout_seq

运行这个脚本来创造用来训练的列表和相关的标签:

train_inout_seq = create_inout_sequences(train_data_normalized, train_window)

如果你打印train_inout_seq列表的长度,你会发现它包含120个项目。这是因为虽然训练集包含132个元素,但序列长度为12,这意味着第一个序列由前12个项目组成,第13个项目是第一个序列的标签。同样地,第二个序列从第二项开始,在第13项结束,而第14项是第二个序列的标签,以此类推。

现在让我们打印train_inout_seq列表的前5项。

train_inout_seq[:5]

Output:

[(tensor([-0.9648, -0.9385, -0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066,
          -0.8593, -0.9341, -1.0000, -0.9385]), tensor([-0.9516])),
 (tensor([-0.9385, -0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593,
          -0.9341, -1.0000, -0.9385, -0.9516]),
  tensor([-0.9033])),
 (tensor([-0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341,
          -1.0000, -0.9385, -0.9516, -0.9033]), tensor([-0.8374])),
 (tensor([-0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000,
          -0.9385, -0.9516, -0.9033, -0.8374]), tensor([-0.8637])),
 (tensor([-0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000, -0.9385,
          -0.9516, -0.9033, -0.8374, -0.8637]), tensor([-0.9077]))]

你可以看到,每个项目都是一个元组,其中第一个元素由一个序列的12个项目组成,第二个元组元素包含相应的标签。

建立LSTM模型

我们已经预处理了数据,现在是时候训练我们的模型了。我们将定义一个LSTM类,它继承于PyTorch库的nn.Module类。请看我上一篇文章,看看如何用PyTorch创建分类模型。那篇文章将帮助你理解以下代码中发生的事情。

class LSTM(nn.Module):
    def __init__(self,input_size=1,hidden_layer_size=100,output_size=1):
        super().__init()
        self.hidden_layer_size = hidden_layer_size
        self.lstm = nn.LSTM(input_size,hidden_layer_size)
        self.linear = nn.Linear(hidden_layer_size,output_size)
        self.hidden_cell = (torch.zeros(1,1,self.hidden_layer_size),torch.zeros(1,1,self.hidden_layer_size))

    def forward(self,input_seq):
        lstm_out,self.hidden_cell = self.lstm(input_seq.view(len(input_seq),1,-1), self.hidden_cell)
        predictions = self.linear(lstm_out.view(len(input_seq), -1))
        return predictions[-1]

让我总结一下上述代码中发生了什么。LSTM类的构造函数接受三个参数:

  1. input_size:对应于输入中的特征数量。虽然我们的序列长度是12,但对于每个月,我们只有一个值,即乘客总数,因此输入的大小将是1。

  2. hidden_layer_size:指定隐藏层的数量以及每层中的神经元数量。我们将有一个100个神经元的层。

  3. output_size: 输出中的项目数量,因为我们要预测未来1个月的乘客数量,所以输出的大小将是1。

接下来,在构造函数中,我们创建变量hidden_layer_size、lstm、linear和hidden_cell。LSTM算法接受三个输入:先前的隐藏状态、先前的单元格状态和当前的输入。hidden_cell变量包含之前的隐藏状态和单元状态。lstm和线性层变量用于创建LSTM和线性层。

在正向传播方法中,input_seq作为一个参数被传递,它首先被传递到lstm层。lstm层的输出是当前时间步长的隐藏状态和单元状态,同时还有输出。lstm层的输出被传递到线性层。预测的乘客数量被存储在预测列表的最后一项中,并返回给调用函数。

下一步是创建一个LSTM()类的对象,定义一个损失函数和优化器。由于我们正在解决一个分类问题,我们将使用交叉熵损失函数。对于优化器函数,我们将使用adam optimizer优化器。

model = LSTM()
loss_function = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

训练模型

我们将对我们的模型进行150次的训练。如果你愿意,你可以尝试更多次。损失将在每25次后被打印出来。

epochs = 150

for i in range(epochs):
    for seq, labels in train_inout_seq:
        optimizer.zero_grad()
        model.hidden_cell = (torch.zeros(1, 1, model.hidden_layer_size),
                        torch.zeros(1, 1, model.hidden_layer_size))

        y_pred = model(seq)

        single_loss = loss_function(y_pred, labels)
        single_loss.backward()
        optimizer.step()

    if i%25 == 1:
        print(f'epoch: {i:3} loss: {single_loss.item():10.8f}')

print(f'epoch: {i:3} loss: {single_loss.item():10.10f}')

Output:

epoch:   1 loss: 0.00517058
epoch:  26 loss: 0.00390285
epoch:  51 loss: 0.00473305
epoch:  76 loss: 0.00187001
epoch: 101 loss: 0.00000075
epoch: 126 loss: 0.00608046
epoch: 149 loss: 0.0004329932

预测

现在我们的模型已经训练完毕,我们可以开始进行预测。由于我们的测试集包含了过去12个月的乘客数据,而我们的模型被训练为使用12的序列长度进行预测。我们将首先从训练集中筛选出最后12个值。

fut_pred = 12

test_inputs = train_data_normalized[-train_window:].tolist()
print(test_inputs)

Output:

[0.12527473270893097, 0.04615384712815285, 0.3274725377559662, 0.2835164964199066, 0.3890109956264496, 0.6175824403762817, 0.9516483545303345, 1.0, 0.5780220031738281, 0.33186814188957214, 0.13406594097614288, 0.32307693362236023]

最初,test_inputs项目将包含12个项目。在一个for循环中,这12个项目将被用来对测试集的第一个项目进行预测,即项目编号133。然后,预测值将被追加到test_inputs列表中。在第二次迭代中,最后的12个项目将再次被用作输入,并作出新的预测,然后再次追加到test_inputs列表中。由于测试集有12个元素,for循环将执行12次。在循环结束时,test_inputs列表将包含24个项目。最后的12项将是测试集的预测值。

下面的脚本是用来进行预测的:

model.eval()

for i in range(fut_pred):
    seq = torch.FloatTensor(test_inputs[-train_window:])
    with torch.no_grad():
        model.hidden = (torch.zeros(1, 1, model.hidden_layer_size),
                        torch.zeros(1, 1, model.hidden_layer_size))
        test_inputs.append(model(seq).item())

如果你打印test_inputs列表的长度,你会发现它包含24个项目。最后12个预测项可以打印出来,如下所示:

[0.882350504398346,
 1.109002947807312,
 1.215394139289856,
 1.3009188175201416,
 1.3665611743927002,
 1.4133175611495972,
 1.4486150741577148,
 1.4749577045440674,
 1.4951767921447754,
 1.5129151344299316,
 1.528343677520752,
 1.5400480031967163]

需要再次提到的是,你可能会得到不同的值,这取决于训练LSTM时使用的权重。

由于我们对训练的数据集进行了归一化处理,预测值也被归一化了。我们需要将规范化的预测值转换成实际的预测值。我们可以通过将归一化的值传递给min/max scaler对象的inverse_transform方法来实现,我们用它来归一化我们的数据集。

actual_predictions = scaler.inverse_transform(np.array(test_inputs[train_window:] ).reshape(-1, 1))
print(actual_predictions)

Output:

[[532.23473975]
 [583.79817063]
 [608.00216669]
 [627.45903099]
 [642.39266717]
 [653.02974516]
 [661.05992937]
 [667.05287778]
 [671.65272021]
 [675.68819308]
 [679.19818664]
 [681.86092073]]

现在让我们把预测值与实际值作对比。请看下面的代码。

x = np.arange(132, 144, 1)
print(x)

Output:

[132 133 134 135 136 137 138 139 140 141 142 143]

在上面的脚本中,我们创建了一个包含过去12个月的数值的列表。第一个月的索引值为0,因此最后一个月的索引值为143。

在下面的脚本中,我们将绘制144个月的乘客总数,以及过去12个月的预测乘客数。

plt.title('Month vs Passenger')
plt.ylabel('Total Passengers')
plt.grid(True)
plt.autoscale(axis='x', tight=True)
plt.plot(flight_data['passengers'])
plt.plot(x,actual_predictions)
plt.show()

Output months_vs_passagers1.png 我们的LSTM所做的预测是由橙色的线条描述的。你可以看到,我们的算法不是太准确,但它仍然能够捕捉到过去12个月内旅行的乘客总数的上升趋势,以及偶尔的波动。你可以尝试在LSTM层中使用更多的epochs和更多的神经元,看看你是否能获得更好的性能。

为了更好地了解输出,我们可以绘制过去12个月的实际和预测的乘客数量,如下所示:

months_vs_passagers2.png

同样,预测不是很准确,但该算法能够捕捉到未来几个月的乘客数量应该高于前几个月的趋势,偶尔会有波动。

结论

LSTM是解决序列问题最广泛使用的算法之一。在这篇文章中,我们看到了如何用LSTM的时间序列数据进行未来预测。你还看到了如何用PyTorch库实现LSTM,然后如何将预测结果与实际值进行对比,以了解训练后的算法表现如何。