前言

  学习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时间序列数据集(单击这里下载),数据集每列的说明:

索引 特征 描述
1 Date Time Date-time reference
2 p (mbar) The pascal SI derived unit of pressure used to quantify internal pressure. Meteorological reports typically state atmospheric pressure in millibars.
3 T (degC) Temperature in Celsius
4 Tpot (K) Temperature in Kelvin
5 Tdew (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.
6 rh (%) 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.
7 VPmax (mbar) Saturation vapor pressure
8 VPact (mbar) Vapor pressure
9 VPdef (mbar) Vapor pressure deficit
10 sh (g/kg) Specific humidity
11 H2OC (mmol/mol) Water vapor concentration
12 rho (g/m ** 3) Airtight
13 wv (m/s) Wind speed
14 max. wv (m/s) Maximum wind speed
15 wd (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中把上面的代码按顺序复制,然后运行,就可以得到下面的结果啦。
在这里插入图片描述

更多推荐