基于paddle飞浆深度学习框架的LSTM时序数据预测demo

前言

  学习pytorch英文文档看得头大,想尝试一下国内的深度学习框架,毕竟官方文档都是中文,于是找到了这个paddle飞浆框架。遗憾的是关于时序数据预测这块官方示例里说明比较少,把代码抄过来不知道怎么结合自己的需求修改,网上去找时序数据预测的例子,感觉很多帖子都有些谜语人,说话说一半,看完还是不知道把他们的例子搬过来以后怎么修改。经过各种折腾自己的demo终于跑通了,把代码和自己的理解写下来,希望能给各位同样卡在代码理解上的同学一些帮助。初学者一枚,可能会出现理解错误的地方,请各位路过的大佬多多批评指正。

时序数据预测模型

  现实生活中有很多时序数据预测的需求,比如风速预测、温度预测、功率预测等等等等。以构建一个风速预测模型为例,要用过去五小时的风速预测未来一小时的风速,为此采集了连续一年的风速数据,然后划分为多组样本数据,每个样本把历史五小时风速作为模型输入,未来一小时风速作为模型输出(需要注意,假设每隔10分钟有一条风速数据,那这里的划分不应该是第一个样本0:00-5:00风速预测5:00-6:00风速,第二个样本6:00-11:00风速预测11:00-12:00风速,而应该是第一个样本0:00-5:00风速预测5:00-6:00风速,第二个样本0:10-5:10风速预测5:10-6:10风速),通过算法找到预测最准确的模型。
  还需要补充一点,我们以要预测的物理量本身作为输出,这一点是确定的,但是输入可以增加一些过去五小时与风速相关的其他物理量,这样这个模型就不单单体现风速本身的因果关系,还有其他物理量对风速的影响,会提高预测的准确度。

参数说明

  下面是一些我程序中涉及到的关键参数(为方便理解,部分参数名字和paddle官方API中的形参,或者约定俗成的名称是一致的):

  • split_ratio:训练/测试数据占比
  • seq_len:前面提到的模型中,历史数据的长度
  • predict_len:前面提到的模型中,预测数据的长度(其实这里长度我觉得用“步数”更准确,比如前面提到的五小时风速预测未来一小时风速,而风速数据10分钟一条,那么seq_len = 5➗(10➗60) = 30,predict_len = 1➗(10➗60) = 6)
  • hidden_size:LSTM隐层神经元的个数
  • num_layers:LSTM隐层层数,神经元个数的增加和层数增加,都会使神经网络挖掘输入输出复杂映射关系的能力增加,但是会增大计算量,并且表示复杂的映射关系前提是训练足够充分
  • epoch_num:训练代数——所有样本全部在模型中训练一遍才算一轮epoch
  • batch_size:一批样本数——神经网络训练不是每一组样本依次送入神经网络,也不是一次把一批送入,而是每次送入batch_size这么多,直到所有样本训练完,epoch+1
  • learning_rate:学习率,影响训练的精度,根据训练效果更改

程序说明(基于LSTM进行温度预测)

  使用paddle官方文档中提到的Jena Climate时间序列数据集(单击这里下载),数据集每列的说明:

索引特征描述
1Date TimeDate-time reference
2p (mbar)The pascal SI derived unit of pressure used to quantify internal pressure. Meteorological reports typically state atmospheric pressure in millibars.
3T (degC)Temperature in Celsius
4Tpot (K)Temperature in Kelvin
5Tdew (degC)Temperature in Celsius relative to humidity. Dew Point is a measure of the absolute amount of water in the air, the DP is the temperature at which the air cannot hold all the moisture in it and water condenses.
6rh (%)Relative Humidity is a measure of how saturated the air is with water vapor, the %RH determines the amount of water contained within collection objects.
7VPmax (mbar)Saturation vapor pressure
8VPact (mbar)Vapor pressure
9VPdef (mbar)Vapor pressure deficit
10sh (g/kg)Specific humidity
11H2OC (mmol/mol)Water vapor concentration
12rho (g/m ** 3)Airtight
13wv (m/s)Wind speed
14max. wv (m/s)Maximum wind speed
15wd (deg)Wind direction in degrees

参数设置

  设置需要用到的参数:

split_ratio = 0.7       # 训练/测试百分比,这里0.7表示全部数据中70%用作训练
seq_len = 30            # 取seq_len步序的值作为历史数据
predict_len = 6         # 预测predict_len步序
hidden_size = 128       # LSTM隐层神经元个数
num_layers = 1          # LSTM隐层层数
epoch_num = 10          # 训练代数
batch_size = 128        # 单次训练中一次喂给网络的数据包大小
learning_rate = 0.005   # 学习率
select_out_column = 1   # 预测的值在第几列(python的行列从0开始)

  因为csv文件中既有要预测的值本身,又有相关联的其他变量,这里特别定义一个变量select_out_column来方便理解代码,代码中遇到select_out_column我们就知道是要对预测值相关的数据,也就是第select_out_column列进行操作了。因为先看过csv文件内容,我已经知道了温度值是在python中的第1列(python里计数是从0开始的,我们日常习惯上从一开始,所以通常说的第一列python里叫第0列。原始csv文件去掉时间那一列后,温度在第二列,python里叫第1列)。

读取数据及数据预处理

  把下载好的csv文件放入python脚本文件相同路径下,读取文件并进行去重、归一化、数据分割等预处理操作。
  DataFrame这种数据类型可以很方便的取出其中指定行/指定列的数据,数据分割很方便,而且可以附加一个index作为索引,如果不需要的话可以不添加index。使用pd.read_csv()返回的就是DataFrame类型,DataFrame的.value成员返回的是numpy矩阵,进行矩阵运算更方便,矩阵运算完再转换为DataFrame方便进行数据分割,结合使用可以利用两者各自的优点。

import paddle
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import warnings
warnings.filterwarnings("ignore")
""" 读取数据及数据预处理 """
csv_path = "jena_climate_2009_2016.csv"             # csv文件名
date_time_key = "Date Time"                         # 时间这一列的列名,方便DataFrame通过列名索引取数据
df = pd.read_csv(csv_path)                          # 读取csv文件,返回的是DataFrame类型
split_index = int(split_ratio * int(df.shape[0]))   # 分开前后多少行
df_data = df.iloc[:, 1:]                            # 去掉第0列时间后剩下数据
df_data.index = df[date_time_key]                   # 使用时间这一列作为索引
df_data.drop_duplicates(keep='first', inplace=True) # 去除原始数据的重复项
def normalize(data):
    data_mean = data.mean(axis=0)
    data_std = data.std(axis=0)
    npdata = (data - data_mean) / data_std
    np2df = pd.DataFrame(npdata)
    return np2df, data_mean, data_std
df_data, data_mean, data_std = normalize(df_data.values)# 数据归一化
train_df_data = df_data.loc[0: split_index - 1]     # 分割出训练数据
test_df_data = df_data.loc[split_index:]            # 分割出测试数据
train_np_data = train_df_data.values                # 将DataFrame转为ndarray类型
test_np_data = test_df_data.values                  # 将DataFrame转为ndarray类型

组装样本,实例化数据集

  上面只是把原始数据分割出来了训练和预测部分,下面就要组装样本了。按照前面的参数设置,要用30步的所有特征历史数据历史预测未来6步的温度,也就是提供给神经网络的一条样本,输入为30*14(除去时间,有14个特征,包括温度自身历史数据),输出为6(未来6步的温度),可能有人会想到用numpy矩阵保存组装好的样本,可惜paddle没法传入numpy矩阵训练,必须传入Dataset类型,下面自己建的类MyDataset就完成了分割出的训练数据/预测数据组装为训练神经网络用的dataset样本过程。
  MyDataset继承自Dataset类,在初始化中对传入的数据进行组装,组装成了训练要用的样本,并且MyDataset要实现返回单条数据的__getitem__和返回长度的__len__方法。从这里也能看出来paddle为什么不能用numpy矩阵训练,因为paddle需要调用获取单条数据和数据长度的两个方法。
  提醒一下,这里是面向对象的写法,class MyDataset缩进里的内容只是这个类的实现,使用这个类一定要将类实例化形成对象,train_dataset和test_dataset就是将类实例化出的两个对象,在初始化时传入了不同参数。

class MyDataset(paddle.io.Dataset): # 继承paddle.io.Dataset类
    def __init__(self, np_data, seq_len, predict_len, select_out_column):
        # 实现 __init__ 函数,初始化数据集,构建输入特征和标签
        super(MyDataset, self).__init__()
        self.seq_len = seq_len
        self.predict_len = predict_len
        self.select_out_column = select_out_column
        self.feature = paddle.to_tensor(self.transform_feature(np_data), dtype='float32')   # 构建输入特征
        self.label = paddle.to_tensor(self.transform_label(np_data), dtype='float32')       # 构建标签

    def transform_feature(self, np_data):   # 构建输入特征的函数
        output = []
        for i in range(len(np_data) - self.seq_len - self.predict_len):
            output.append(np_data[i: (i + self.seq_len)])
        return np.array(output)

    def transform_label(self, np_data):     # 构建标签的函数
        output_ = []
        for i in range(len(np_data) - self.seq_len - self.predict_len):
            output_.append(np_data[(i + self.seq_len):(i + self.seq_len + self.predict_len), self.select_out_column])
        return np.array(output_)

    def __getitem__(self, index):
        # 实现__getitem__函数,传入index时要返回单条数据(输入特征和对应标签)
        one_feature = self.feature[index]
        one_label = self.label[index]
        return one_feature, one_label

    def __len__(self):
        # 实现__len__方法,返回数据集总数目
        return len(self.feature)
train_dataset = MyDataset(train_np_data, seq_len, predict_len, select_out_column)   # 根据训练数据构造dataset
test_dataset = MyDataset(test_np_data, seq_len, predict_len, select_out_column)     # 根据测试数据构造dataset
input_size = train_np_data.shape[1]     # data有几列就有几个输入,也就是神经网络的输入个数

设计LSTM网络

  要设计自己的LSTM网络,需要新建一个类,从nn.Layer继承,然后在类初始化中生成每一层网络的结构,这里设计的结构非常简单,样本输入经过LSTM层后再经过一个线性层映射到输出标签。在__init__中只是体现了网络结构,从输入到输出的计算过程要在forward方法中实现。自己新建的类要继承自nn.Layer,__init__要定义网络结构,名字叫forward的方法实现前向计算过程,这些都是必须要遵守的。
  paddle提供了大量的神经网络层相关函数,通过调用paddle.nn中的函数,我们就可以创建需要的单层神经网络结构,然后在forward中把它们串联起来,就形成了自己定义的神经网络。
  LSTM层调用paddle.nn.LSTM函数,需要提供input_size,hidden_size,num_layers的值,hidden_size和num_layers就是一开始设置的LSTM神经元个数和LSTM层数,因为输入一开始就进入到LSTM层运算,所以input_size和我模型输入的特征数相等,也就是有些人写的程序中feature_num或者叫fea_num,和这里input_size相等。time_major这个参数可以选True和False,它影响的是输入的形状和输出的形状,官方说明中False表示输入形状为[batch_size,time_steps,input_size],第一维度为batch_size,和我的参数含义相同,第二维度time_steps对应的是我参数中的seq_len,也就是输入模型的历史数据用了多少步,第三维度input_size就是输入的特征数,和我的参数含义相同。如果time_major为True那么输入形状为[time_steps,batch_size,input_size],这个个人感觉就是习惯不同,可能有的人前一种形状看的舒服,有的人喜欢后一种形状,只要保证形状合规就可以,张量的维度交换也很简单。LSTM层的输出形状是[time_steps,batch_size,hidden_size],含义前面都已经给出。
  paddle.nn.Linear是线性层,要将LSTM层的输出映射到模型的输出,由于我预测predict_len步,所以Linear输出大小为predict_length。我只用了一些关键参数,还有一些参数使用了默认值,如果需要可以查阅官方文档修改其他参数。

class MyLSTMNet(paddle.nn.Layer):
    def __init__(self, input_size, hidden_size, num_layers, predict_len, batch_size):
        super().__init__()
        self.input_size = input_size    # 有多少个特征
        self.hidden_size = hidden_size  # LSTM隐层神经元个数
        self.num_layers = num_layers    # LSTM隐层层数
        self.predict_length = predict_len    # 单个输出,预测predict_length步
        self.batch_size = batch_size
        self.lstm1 = paddle.nn.LSTM(input_size=self.input_size,
                                    hidden_size=self.hidden_size,
                                    num_layers=self.num_layers,
                                    time_major=False)
        self.fc = paddle.nn.Linear(in_features=self.hidden_size, out_features=self.predict_length)

    def forward(self, x):
        x, (h, c) = self.lstm1(x)   # 输出应该是(batch_size, time_step,hidden_size)
        x = self.fc(x)              # 线性层,输出应该是(batch_size, time_step, predict_length)
        x = x[:, -1, :]             # 最后一个LSTM只要窗口中最后一个特征的输出(batch_size, predict_length)
        return x

封装及设置神经网络模型

  按照paddle规定,在设计网络时新建的类不是直接实例化对象就能使用,需要进行封装,这里使用paddle的高阶API函数paddle.Model实现封装,所谓封装就是把MyLSTMNet作为参数传入paddle.Model,paddle.Model需要的参数前面都有说明,经过封装我们返回了一个"model",这个model就可以传入数据集进行训练了,实例化优化器"opt",并设置学习率,model.parameters()把模型参数传给优化器,model.prepare()设置模型训练用的损失函数。

""" 封装模型,设置模型 """
model = paddle.Model(MyLSTMNet(input_size, hidden_size, num_layers, predict_len, batch_size))   # 封装模型
opt = paddle.optimizer.Adam(learning_rate=learning_rate, parameters=model.parameters())         # 设置优化器
model.prepare(opt, paddle.nn.MSELoss(), paddle.metric.Accuracy())                               # 设置模型

开始训练

  使用高阶API训练,第一个参数就是训练数据构成的dataset,然后设置训练代数,一批样本个数,save_freq是每训练多少代保存一次模型参数,save_dir是保存模型参数的路径,verbose必须为 0,1,2。当设定为0时,不打印日志,设定为1时,使用进度条的方式打印日志,设定为2时,一行一行地打印日志,这里的日志指的是训练的进度,以及目前的模型误差等信息,drop_last为真时会把一代训练中最后一批扔掉(如果最后一批样本数量小于batch_size的话),shuffle为真时会把样本打乱后再送入训练,由于我这里数据本身就是按顺序排列有时间规律的,所以选择了不打乱。

""" 开始训练 """
model.fit(train_dataset,
          epochs=epoch_num,
          batch_size=batch_size,
          save_freq=10,
          save_dir='lstm_checkpoint',
          verbose=1,
          drop_last=True,
          shuffle=False,
          )

加载测试数据进行预测

  其实上面的模型训练完之后可以预测后面6步的温度值,但是方便观察我取了最后第6步的预测值与实际温度比较,把下面程序中的predict_len-1改为其他值就可以比较其他步的预测结果,之前归一化时保存的均值和方差这里反向计算,来把数据还原回归一化之前的值。

model.load('lstm_checkpoint/final')
test_result = model.predict(test_dataset)
test_result = test_result[0]
predict = []
for i in range(len(test_dataset.label)):
    one_line_test_result = test_result[i]
    result = one_line_test_result[0, predict_len-1]
    predict.append(result)
real = test_dataset.label.numpy()
real = real[:, predict_len-1]
predict = np.array(predict)
real = real*data_std[select_out_column]+data_mean[select_out_column]
predict = predict*data_std[select_out_column]+data_mean[select_out_column]
plt.plot(real, label='real')
plt.plot(predict, label='predict')
plt.legend()
plt.show()

测试结果

  程序都准备完毕,在python中把上面的代码按顺序复制,然后运行,就可以得到下面的结果啦。
在这里插入图片描述

  • 9
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
LSTM (Long Short-Term Memory) 是一种特殊的循环神经网络(RNN)架构,用于处理具有长期依赖关系的序列数据。传统的RNN在处理长序列时往往会遇到梯度消失或梯度爆炸的问题,导致无法有效地捕捉长期依赖。LSTM通过引入门控机制(Gating Mechanism)和记忆单元(Memory Cell)来克服这些问题。 以下是LSTM的基本结构和主要组件: 记忆单元(Memory Cell):记忆单元是LSTM的核心,用于存储长期信息。它像一个传送带一样,在整个链上运行,只有一些小的线性交互。信息很容易地在其上保持不变。 输入门(Input Gate):输入门决定了哪些新的信息会被加入到记忆单元中。它由当前时刻的输入和上一时刻的隐藏状态共同决定。 遗忘门(Forget Gate):遗忘门决定了哪些信息会从记忆单元中被丢弃或遗忘。它也由当前时刻的输入和上一时刻的隐藏状态共同决定。 输出门(Output Gate):输出门决定了哪些信息会从记忆单元中输出到当前时刻的隐藏状态中。同样地,它也由当前时刻的输入和上一时刻的隐藏状态共同决定。 LSTM的计算过程可以大致描述为: 通过遗忘门决定从记忆单元中丢弃哪些信息。 通过输入门决定哪些新的信息会被加入到记忆单元中。 更新记忆单元的状态。 通过输出门决定哪些信息会从记忆单元中输出到当前时刻的隐藏状态中。 由于LSTM能够有效地处理长期依赖关系,它在许多序列建模任务中都取得了很好的效果,如语音识别、文本生成、机器翻译、时序预测等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值