基于paddle飞浆深度学习框架的LSTM时序数据预测demo
基于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时间序列数据集(单击这里下载),数据集每列的说明:
索引 | 特征 | 描述 |
---|---|---|
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中把上面的代码按顺序复制,然后运行,就可以得到下面的结果啦。
更多推荐
所有评论(0)