赞
踩
大家好,今天和各位分享一下如何使用 Tensorflow 构建 CNN卷积神经网络和 LSTM 循环神经网络相结合的混合神经网络模型,完成对多特征的时间序列预测。
本文预测模型的主要结构由 CNN 和 LSTM 神经网络构成。气温的特征数据具有空间依赖性。本文选择通过在模型前端使用CNN卷积神经网络提取特征之间的空间关系。同时,气温数据又具有明显的时间依赖性,因此在卷积神经网络后添加 LSTM 长短时记忆模型进行时序处理。
数据集自取:https://download.csdn.net/download/dgvv4/49801464
本文使用GPU加速计算,没有GPU的朋友把下面调用GPU的那段代码删了就行。
数据集中后5项特征是气温数据,前三项是时间,选择'actual'列为标签。任务要求,根据连续10天的气温数据,预测5天后的actual气温值
- import tensorflow as tf
- from tensorflow import keras
- from tensorflow.keras import layers
- import pandas as pd
-
- # 调用GPU加速
- gpus = tf.config.experimental.list_physical_devices(device_type='GPU')
- for gpu in gpus:
- tf.config.experimental.set_memory_growth(gpu, True)
-
- # --------------------------------------- #
- #(1)获取数据集
- # --------------------------------------- #
- filepath = 'temps.csv' # 数据集位置
- data = pd.read_csv(filepath)
- print(data.head()) # 查看前五行数据
数据集信息如下:
如上图所示,列表前三列是时间信息,需要将年月日信息组合在一起,从字符串类型转为datetime类型。在选择特征时,时间特征用不上,先把它处理了。
- import datetime # 将时间信息组合成datetime类型的数据
-
- # 获取数据中的年月日信息
- years = data['year']
- months = data['month']
- days = data['day']
-
- dates = [] # 存放组合后的时间信息
-
- # 遍历一一对应的年月日信息
- for year, month, day in zip(years, months, days):
- # 年月日之间使用字符串拼接
- date = str(year) + '-' + str(month) + '-' + str(day)
- # 将每个时间(字符串类型)保存
- dates.append(date)
-
- # 字符串类型的时间转为datetime类型的时间
- times = []
-
- # 遍历所有的字符串类型的时间
- for date in dates:
- # 转为datetime类型
- time = datetime.datetime.strptime(date, '%Y-%m-%d')
- # 逐一保存转变类型后的时间数据
- times.append(time)
-
- # 查看转换后的时间数据
- print(times[:5])
处理后的时间特征如下
把每个特征的分布曲线绘制出来,对数据有直观的体会
- import matplotlib.pyplot as plt
- # 指定绘图风格
- plt.style.use('fivethirtyeight')
- # 设置画布,2行2列的画图窗口,第一行画ax1和ax2,第二行画ax3和ax4
- fig, ((ax1,ax2), (ax3,ax4)) = plt.subplots(2, 2, figsize=(20, 10))
-
- # ==1== actual特征列
- ax1.plot(times, data['actual'])
- # 设置x轴y轴标签和title标题
- ax1.set_xlabel(''); ax1.set_ylabel('Temperature'); ax1.set_title('actual temp')
- # ==2== 前一天的温度
- ax2.plot(times, data['temp_1'])
- # 设置x轴y轴标签和title标题
- ax2.set_xlabel(''); ax2.set_ylabel('Temperature'); ax2.set_title('temp_1')
- # ==3== 前2天的温度
- ax3.plot(times, data['temp_2'])
- # 设置x轴y轴标签和title标题
- ax3.set_xlabel('Date'); ax3.set_ylabel('Temperature'); ax3.set_title('temp_2')
- # ==4== friend
- ax4.plot(times, data['friend'])
- # 设置x轴y轴标签和title标题
- ax4.set_xlabel('Date'); ax4.set_ylabel('Temperature'); ax4.set_title('friend')
- # 轻量化布局调整绘图
- plt.tight_layout(pad=2)
- plt.show()
简单绘制一下四个特征随时间的分布情况
首先数据中有一列是分类数据,星期几,需要对这种数据进行one-hot编码,给每一个分类添加一个特征列,如果这一行数据是星期一,那么对应的星期一这列的数值就是1,其他列的数值为0。
然后要选择标签值,预测5天后的气温。标签数据取特征数据中的 'label' 列,并且将这一列集体向上移动5行,那么此时,标签数据中最后的5行会出现空缺值nan。最后5行的特征数据没有对应的标签值,因此需要把最后5行从特征数据和标签数据中删除。
接下来就是对所有的气温数据进行标准化预处理,不要对onehot编码后的星期数据进行标准化。
- # 选择特征, 共6列特征
- feats = data.iloc[:,3:]
- # 对离散的星期几的数据进行onehot编码
- feats = pd.get_dummies(feats)
- # 特征列增加到12项
- print(feats.shape) # (348, 12)
-
- # 选择标签数据,一组时间序列预测5天后的真实气温
- pre_days = 5
- # 选择特征数据中的真实气温'actual'具体向上移动5天的气温信息
- targets = feats['actual'].shift(-pre_days)
- # 查看标签信息
- print(targets.shape) #(348,)
-
- # 由于特征值最后5行对应的标签是空值nan,将最后5行特征及标签删除
- feats = feats[:-pre_days]
- targets = targets[:-pre_days]
- # 查看数据信息
- print('feats.shape:', feats.shape, 'targets.shape:', targets.shape) # (343, 12) (343,)
-
-
- # 特征数据标准化处理
- from sklearn.preprocessing import StandardScaler
- # 接收标准化方法
- scaler = StandardScaler()
- # 对特征数据中所有的数值类型的数据进行标准化
- feats.iloc[:,:5] = scaler.fit_transform(feats.iloc[:,:5])
- # 查看标准化后的信息
- print(feats)
预处理后的数据如下:
这里使用到队列deque,先进先出。指定队列最大长度为10,即时间序列窗口长度为10,根据10天的特征数据预测5天后的气温。如果队列的长度超过10,那么队列会自动将头部的元素删除,将新元素追加到队列的尾部,组成一个新的序列。
对于标签数据,例如range(0,10)天的特征预测第15天的气温,而之前已经将标签值集体向上移动5行,那么第15的度气温标签值对应索引9,即 [max_series_days-1]
- import numpy as np
- from collections import deque # 队列,可在两端增删元素
-
- # 将特征数据从df类型转为numpy类型
- feats = np.array(feats)
-
- # 定义时间序列窗口是连续10天的特征数据
- max_series_days = 10
- # 创建一个队列,队列的最大长度固定为10
- deq = deque(maxlen=max_series_days) # 如果长度超出了10,先从队列头部开始删除
-
- # 创建一个列表,保存处理后的特征序列
- x = []
- # 遍历每一行数据,包含12项特征
- for i in feats:
- # 将每一行数据存入队列中, numpy类型转为list类型
- deq.append(list(i))
- # 如果队列长度等于指定的序列长度,就保存这个序列
- # 如果队列长度大于序列长度,队列会自动删除头端元素,在尾端追加新元素
- if len(deq) == max_series_days:
- # 保存每一组时间序列, 队列类型转为list类型
- x.append(list(deq))
-
- # 保存与特征对应的标签值
- y = targets[max_series_days-1:].values
-
- # 保证序列长度和标签长度相同
- print(len(x)) # 334
- print(len(y)) # 334
-
- # 将list类型转为numpy类型
- x, y = np.array(x), np.array(y)
左图x是一个时间序列滑窗包含的10行特征数据,右图标签y是每一个时间序列滑窗对应一个气温标签值。
从划分好了的序列中取前80的数据用于训练,剩下20%分别用来验证和测试。对训练数据随机打乱 shuffle() 避免偶然性。构造一个迭代器 iter(),结合 next() 函数从训练集中取出一个batch,查看数据集信息。
- total_num = len(x) # 一共有多少组序列
- train_num = int(total_num*0.8) # 前80%的数据用来训练
- val_num = int(total_num*0.9) # 前80%-90%的数据用来训练验证
- # 剩余数据用来测试
-
- x_train, y_train = x[:train_num], y[:train_num] # 训练集
- x_val, y_val = x[train_num: val_num], y[train_num: val_num] # 验证集
- x_test, y_test = x[val_num:], y[val_num:] # 测试集
-
- # 构造数据集
- batch_size = 128 # 每次迭代处理128个序列
- # 训练集
- train_ds = tf.data.Dataset.from_tensor_slices((x_train, y_train))
- train_ds = train_ds.batch(batch_size).shuffle(10000)
- # 验证集
- val_ds = tf.data.Dataset.from_tensor_slices((x_val, y_val))
- val_ds = val_ds.batch(batch_size)
- # 测试集
- test_ds = tf.data.Dataset.from_tensor_slices((x_test, y_test))
- test_ds = test_ds.batch(batch_size)
-
- # 查看数据集信息
- sample = next(iter(train_ds)) # 取出一个batch的数据
- print('x_train.shape:', sample[0].shape) # (128, 10, 12)
- print('y_train.shape:', sample[1].shape) # (128,)
输入层的shape为 [None, 10, 12],其中None代表Batch_Size不用写出来,10代表时间序列窗口的大小,12代表数据中的特征个数。
由于这里我使用二维卷积Conv2D,需要在输入数据集中添加通道维度,shape变为 [None, 10, 12, 1] ,和图像处理时类似,使用3*3卷积,步长为1,提取特征
使用池化层下采样,当然也可以使用步长为2的卷积层下采样。下采样时,我选择的池化核大小是[1,2],即只在特征维度上下采样,序列窗口保持不变,下采样方式具体问题具体分析。
在把特征数据从CNN输入至LSTM之前需要调整通道数,融合通道信息,将通道数降为1,然后把通道维挤压掉,shape从四维变为三维,[None,10,6,1]变成[None,10,6]
接下来就是通过LSTM处理数据的时间序列信息,最后经过一个全连接层输出预测结果。全连接层的神经元个数要和标签值的预测个数相同,这里只预测未来某个时间点,神经元个数就是1
- # 输入层要和x_train的shape一致,但注意不要batch维度
- input_shape = sample[0].shape[1:] # [10,12]
-
- # 构造输入层
- inputs = keras.Input(shape=(input_shape)) # [None, 10, 12]
-
- # 调整维度 [None,10,12]==>[None,10,12,1]
- x = layers.Reshape(target_shape=(inputs.shape[1], inputs.shape[2], 1))(inputs)
-
- # 卷积+BN+Relu [None,10,12,1]==>[None,10,12,8]
- x = layers.Conv2D(8, kernel_size=(3,3), strides=1, padding='same', use_bias=False,
- kernel_regularizer=keras.regularizers.l2(0.01))(x)
-
- x = layers.BatchNormalization()(x) # 批标准化
- x = layers.Activation('relu')(x) # relu激活函数
-
- # 池化下采样 [None,10,12,8]==>[None,10,6,8]
- x = layers.MaxPool2D(pool_size=(1,2))(x)
-
- # 1*1卷积调整通道数 [None,10,6,8]==>[None,10,6,1]
- x = layers.Conv2D(1, kernel_size=(3,3), strides=1, padding='same', use_bias=False,
- kernel_regularizer=keras.regularizers.l2(0.01))(x)
-
- # 把最后一个维度挤压掉 [None,10,6,1]==>[None,10,6]
- x = tf.squeeze(x, axis=-1)
-
- # [None,10,6] ==> [None,10,16]
- # 第一个LSTM层, 如果下一层还是LSTM层就需要return_sequences=True, 否则就是False
- x = layers.LSTM(16, activation='relu', kernel_regularizer=keras.regularizers.l2(0.01))(x)
- x = layers.Dropout(0.2)(x) # 随机杀死神经元防止过拟合
-
- # 输出层 [None,16]==>[None,1]
- outputs = layers.Dense(1)(x)
-
- # 构建模型
- model = keras.Model(inputs, outputs)
-
- # 查看模型架构
- model.summary()
网络架构如下:
- Model: "model_12"
- _________________________________________________________________
- Layer (type) Output Shape Param #
- =================================================================
- input_13 (InputLayer) [(None, 10, 12)] 0
- _________________________________________________________________
- reshape_12 (Reshape) (None, 10, 12, 1) 0
- _________________________________________________________________
- conv2d_25 (Conv2D) (None, 10, 12, 8) 72
- _________________________________________________________________
- batch_normalization_13 (Batc (None, 10, 12, 8) 32
- _________________________________________________________________
- activation_13 (Activation) (None, 10, 12, 8) 0
- _________________________________________________________________
- max_pooling2d_13 (MaxPooling (None, 10, 6, 8) 0
- _________________________________________________________________
- conv2d_26 (Conv2D) (None, 10, 6, 1) 72
- _________________________________________________________________
- tf.compat.v1.squeeze_12 (TFO (None, 10, 6) 0
- _________________________________________________________________
- lstm_11 (LSTM) (None, 16) 1472
- _________________________________________________________________
- dropout_14 (Dropout) (None, 16) 0
- _________________________________________________________________
- dense_13 (Dense) (None, 1) 17
- =================================================================
- Total params: 1,665
- Trainable params: 1,649
- Non-trainable params: 16
- _________________________________________________________________
使用回归计算的预测值和真实值之间的平均绝对误差作为损失函数,使用预测值和真实值之间的对数均方误差作为训练时的监控指标
- # 网络编译
- model.compile(optimizer = keras.optimizers.Adam(0.001), # adam优化器学习率0.001
- loss = tf.keras.losses.MeanAbsoluteError(), # 标签和预测之间绝对差异的平均值
- metrics = tf.keras.losses.MeanSquaredLogarithmicError()) # 计算标签和预测之间的对数误差均方值。
-
- epochs = 300 # 迭代300次
-
- # 网络训练, history保存训练时的信息
- history = model.fit(train_ds, epochs=epochs, validation_data=val_ds)
由于history中记录了网络训练时,每次迭代包含的损失信息和指标信息,将它们可视化出来
- history_dict = history.history # 获取训练的数据字典
- train_loss = history_dict['loss'] # 训练集损失
- val_loss = history_dict['val_loss'] # 验证集损失
- train_msle = history_dict['mean_squared_logarithmic_error'] # 训练集的百分比误差
- val_msle = history_dict['val_mean_squared_logarithmic_error'] # 验证集的百分比误差
-
- #(11)绘制训练损失和验证损失
- plt.figure()
- plt.plot(range(epochs), train_loss, label='train_loss') # 训练集损失
- plt.plot(range(epochs), val_loss, label='val_loss') # 验证集损失
- plt.legend() # 显示标签
- plt.xlabel('epochs')
- plt.ylabel('loss')
- plt.show()
-
- #(12)绘制训练百分比误差和验证百分比误差
- plt.figure()
- plt.plot(range(epochs), train_msle, label='train_msle') # 训练集指标
- plt.plot(range(epochs), val_msle, label='val_msle') # 验证集指标
- plt.legend() # 显示标签
- plt.xlabel('epochs')
- plt.ylabel('msle')
- plt.show()
首先使用 evaluate() 对整个测试集计算损失和监控指标,使用 predict() 函数通过测试集的特征值计算预测气温。然后绘制预测值和真实值的对比曲线。
- # 对整个测试集评估
- model.evaluate(test_ds)
-
- # 预测
- y_pred = model.predict(x_test)
-
- # 获取标签值对应的时间
- df_time = times[-len(y_test):]
-
- # 绘制对比曲线
- fig = plt.figure(figsize=(10,5)) # 画板大小
- ax = fig.add_subplot(111) # 画板上添加一张图
- # 绘制真实值曲线
- ax.plot(df_time, y_test, 'b-', label='actual')
- # 绘制预测值曲线
- ax.plot(df_time, y_pred, 'r--', label='predict')
- # 设置x轴刻度
- ax.set_xticks(df_time[::7])
-
- # 设置xy轴标签和title标题
- ax.set_xlabel('Date')
- ax.set_ylabel('Temperature');
- ax.set_title('result')
- plt.legend()
- plt.show()
预测曲线和真实曲线对比如下
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。