赞
踩
学习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中的形参,或者约定俗成的名称是一致的):
使用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网络,需要新建一个类,从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中把上面的代码按顺序复制,然后运行,就可以得到下面的结果啦。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。