分别使用CNN模型和GRU模型进行耶拿温度预测并以及模型改进(含详细代码)

提示:本文主要内容是用keras实现循环实现温度预测。包括三个部分:
1、预准备工作,包括:天气数据集的处理、并将部分温度数据用图像的形式绘制出来查看、数据的准备、进行数据标准化、准备数据生成器这几节。
2、进行温度预测,包含三部分内容:基于常识的、非机器学习的模型,基于CNN的机器学习,基于GRU神经网络的机器学习。
3、对网络进行改进,包含三个操作:在循环层中使用dropout来降低过拟合;使用堆叠循环层;双向循环层。

目录

  • 前言
  • 深度学习前的预准备工作
    • 查看下耶拿天气数据集的数据
    • 绘制温度时间序列
    • 准备数据
    • 数据标准化
    • 生成时间序列样本及其目标的生成器
    • 准备生成器
  • 在预处理的基础上进行温度预测
    • 一种基于常识的、非机器学习的基准方法
    • 基于CNN的机器学习方法
      • 密集连接模型介绍及其训练代码
      • 训练结果绘制
    • 基于GRU的机器学习方法
      • 基于GRU模型的代码
      • 训练结果展示
  • 三个改进网络的技巧
    • 使用循环dropout来降低过拟合
      • 代码显示
      • 结果显示
    • 循环层堆叠
      • 使用dropout正则化的堆叠GRU模型
      • 结果显示
    • 使用双向RNN
      • 训练一个双向GRU

前言

本文展示的温度预测模型所使用的数据集是一个天气时间序列数据集,它由德国耶拿的马克思普朗克生物地球化学研究所的气象站记录。

这个数据集,每十分钟记录十四个不同的量(如气温、气压、湿度、风向等)。本例数据使用的2009-2016年的数据。

深度学习前的预准备工作

此处的预准备工作包括:天气数据集的处理、并将部分温度数据用图像的形式绘制出来查看、数据的准备、进行数据标准化、准备数据生成器这几节。

查看下耶拿天气数据集的数据

import os
data_dir = '/users/fchollet/Downloads/jena_climate'
fname = os.path.join(data_dir, 'jena_climate_2009_2016.csv')
f = open(fname)
data = f.read()
f.close()
lines = data.split('\n')
header = lines[0].split(',')
lines = lines[1:]
print(header)
print(len(lines))

从输出可以看出,共有 420 551 行数据(每行是一个时间步,记录了一个日期和 14 个与天
气有关的值),还输出了下列表头。
[“Date Time”, “p (mbar)”, “T (degC)”, “Tpot (K)”,
“Tdew (degC)”, “rh (%)”, “VPmax (mbar)”, “VPact (mbar)”, “VPdef (mbar)”, “sh (g/kg)”, “H2OC (mmol/mol)”, “rho (g/m**3)”, “wv (m/s)”, “max. wv (m/s)”, “wd (deg)”]

绘制温度时间序列

from matplotlib import pyplot as plt
temp = float_data[:, 1] # 温度(单位:摄氏度)
plt.plot(range(len(temp)), temp)

分别使用CNN模型和GRU模型进行耶拿温度预测并以及模型改进(含详细代码)_第1张图片
下面再绘制前十天的温度时间序列
修改的代码如下:
plt.plot(range(1440),temp[:1440])
分别使用CNN模型和GRU模型进行耶拿温度预测并以及模型改进(含详细代码)_第2张图片
在这张图中,可以看到每天的周期性变化,尤其是最后四天特别明显,可以推测这几天数据来自于很冷的二冬季月份。

但是根据过去几个月的数据预测下个月的平均温度是会出问题的,因为很简单,数据具有可靠的年度周期性。

准备数据

这个问题可以表述如下:一个时间步是10分钟,每steps个时间步采取一次数据,给定过去lookback个时间步之内的数据,看能否预测delay个时间步之后的温度?参数解释如下:
lookback=720,给定过去5天内的观测数据;
steps=6,观测数据的采集频率是每小时一个数据点;
delay=144,目标是未来24小时之后的数据。

将数据预处理为神经网络可以处理的格式。需要对每个时间序列分别做标准化,让他们再相似的范围内都取最小的值。
编写一个puthon生成器,以当前的浮点数数据作为输入,并从最近的数据中生成数据批量,同时生成未来的目标温度。

预处理数据的方法是,将每个时间序列减去其平均值,然后除以其标准差。我们将使用前200 000个时间步作为训练数据,所以只对着部分数据计算平均值和标准差。

数据标准化

代码显示:

mean = float_data[:200000].mean(axis=0)
float_data -= mean
std = float_data[:200000].std(axis=0)
float_data /= std

以上代码是将要用到的生成器,它生成了一个元组(sample,targets),其中samples是输入数据的一个批量,targets是对应的目标温度数组。生成器的参数如下:
data:浮点数数据组成的原始数组,再代码中将其标准化;
lookback:输入数据应该包括过去多少个时间步;
delay:目标应该在未来多少个时间步之后;
min_index和max_index:数据中的索引。

生成时间序列样本及其目标的生成器

程序代码:

def generator(data, lookback, delay, min_index, max_index,shuffle=False, batch_size=128, step=6):
	if max_index is None:
		max_index = len(data) - delay - 1
	i = min_index + lookback
	while 1:
		if shuffle:
			rows = np.random.randint(
					min_index + lookback,
					max_index, 
					size=batch_size)
		else:
			if i + batch_size >= max_index:
				i = min_index + lookback
			rows = np.arange(i, min(i + batch_size, max_index))
			i += len(rows)
		samples = np.zeros((len(rows),
						lookback // step,
						data.shape[-1]))
		targets = np.zeros((len(rows),))
		for j, row in enumerate(rows):
			indices = range(rows[j] - lookback, rows[j], step)
			samples[j] = data[indices]
			targets[j] = data[rows[j] + delay][1]
		yield samples, targets

这个抽象的generator函数可以实例化三个生成器,分别用于训练、验证、测试。每个生成器分别读取数据的不同时间段:训练生成器读取前200 000个时间步,验证生成器读取随后的100 000个时间步,测试生成器读取剩下的时间步。

准备生成器

lookback = 1440
step = 6
delay = 144
batch_size = 128
train_gen = generator(float_data,
 lookback=lookback,
 delay=delay,
 min_index=0,
 max_index=200000,
 shuffle=True,
 step=step,
 batch_size=batch_size)
val_gen = generator(float_data,
 lookback=lookback,
 delay=delay,
 min_index=200001,
 max_index=300000,
 step=step,
 batch_size=batch_size)
test_gen = generator(float_data,
 lookback=lookback,
 delay=delay,
 min_index=300001,
 max_index=None,
 step=step,
 batch_size=batch_size)
val_steps = (300000 - 200001 - lookback) //batch_size 
test_steps = (len(float_data) - 300001 - lookback) //batch_size 

在预处理的基础上进行温度预测

此处有两种方式:
一种是基于常识的、非机器学习的基准方法;一种是基本的机器学习方法。其中机器学习的方法包含两种,简单的CNN和GRU两种模式。

一种基于常识的、非机器学习的基准方法

开始使用深度学习之前,先尝试一种基于常识的简单方法。它可以作为合理性检测,高级的机器学习模型需要打败这个基准才能表现有效性。
一个经典的例子就是不平衡的分类任务,其中某些类别比其他类别更常见。如果数据集中包含90%的类别A实例和10%的类别B实例,那么分类任务的一种基于常识的方法就是对于新样本始终预测类别A,这个分类器的总体精度为90%,因此任何基于学习的精度高于90%才可以证明其有效性。

本例中,我们放心假设,温度时间序列是连续的,并且具有每天的周期性变化。因此,一种基于常识的方法就是始终预测24小时后的温度等于现在的温度。我们使用平均绝对误差指标来评价这种方法。

计算符合常识的基准方法的MAE代码展示:

def evaluate_naive_method():
	batch_maes = []
	for step in range(val_steps):
		samples, targets = next(val_gen)
		preds = samples[:, -1, 1]
		mae = np.mean(np.abs(preds - targets))
		batch_maes.append(mae)
	print(np.mean(batch_maes))
	
evaluate_naive_method()

得到的 MAE 为 0.29。因为温度数据被标准化成均值为 0、标准差为 1,所以无法直接对这个值进行解释。它转化成温度的平均绝对误差为0.29×temperature_std 摄氏度,即 2.57℃。

基于CNN的机器学习方法

在尝试机器学习方法之前,建立一个基于常识的基准方法是很有用的;同样的,在开始研究复杂且计算代价很高的模型之前,尝试使用简单且计算代价低的机器学习模型也是很有用的。比如小型的密集连接网络。

密集连接模型介绍及其训练代码

下面代码给出了一个密集连接模型,首先将数据展平,然后通过两个Dense层并运行。最后一个Dense层没有使用激活函数,这对于回归问题是很常见的。我们使用MAE作为损失。评估数据和评估指标都与常识方法完全相同,所以可以直接比较这两种方法的结果。

from keras.models import Sequential 
from keras import layers
from tensorflow.keras.optimizers import RMSprop
model = Sequential()
model.add(layers.Flatten(input_shape=(lookback // step, float_data.shape[-1]))) 
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(1))
model.compile(optimizer=RMSprop(), loss='mae') 
history = model.fit_generator(train_gen,
					steps_per_epoch=500, 
					epochs=20, 
					validation_data=val_gen, 
					validation_steps=val_steps)

训练结果绘制

import matplotlib.pyplot as plt
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(loss) + 1)
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()

分别使用CNN模型和GRU模型进行耶拿温度预测并以及模型改进(含详细代码)_第3张图片

以上展示了首先建立的这个基准方法的有点,事实证明,超越这个基准并不容易。我们的尝试中包含着大量的有价值的信息,而机器学习并不知道这些信息。

如果从数据到目标之间存在一个简单且表现良好的模型,那为什么我们训练的模型没有找到这个模型并进一步改进呢?

原因在于这个简单的解决方案并不是训练过程所要寻找的目标。我们在模型空间(即假设空间)中搜索解决方案,这
个模型空间是具有我们所定义的架构的所有两层网络组成的空间。这些网络已经相当复杂了。如果你在一个复杂模型的空间中寻找解决方案,那么可能无法学到简单且性能良好的基准方法,虽然技术上来说它属于假设空间的一部分。通常来说,这对机器学习是一个非常重要的限制:如果学习算法没有被硬编码要求去寻找特定类型的简单模型,那么有时候参数学习是无法找到简单问题的简单解决方案的。

基于GRU的机器学习方法

Chung等人在2014年开发了GRU层,之前的内容说过LSTM层。门控循环单元(GRU,gated recurrent unit)层的工作原理于LSTM相同,但是它做了简化,其运算代价更低,但是也相对降低了能力。

机器学习中到处可见这种在计算代价与表达能力的折中。

基于GRU模型的代码

from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
model.add(layers.GRU(32, input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))
model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,
				steps_per_epoch=500,
				epochs=20,
				validation_data=val_gen,
				validation_steps=val_steps)

训练结果展示

分别使用CNN模型和GRU模型进行耶拿温度预测并以及模型改进(含详细代码)_第4张图片
根据以上结果可以看出:新的验证 MAE 约为 0.265(在开始显著过拟合之前),反标准化转换成温度的平均绝对误差为 2.35℃。与最初的误差 2.57℃相比,这个结果确实有所提高,但可能仍有改进的空间。

三个改进网络的技巧

1、循环dropout,这是一种内置的方法,在循环层中使用dropout来降低过拟合;
2、堆叠循环层,这会提高网络的表达能力(代价是更高的计算负荷);
3、双向循环层,将相同的信息以不同的方式呈现给循环网络,可以提高精度并缓解遗忘问题。

使用循环dropout来降低过拟合

从训练和验证曲线中可以明显看出,模型出现过拟合:几轮过后,训练损失和验证损失就开始明显偏离。一种降低过拟合的经典技术——dropout,即将某一层的输入单元随机设为0,目的是打破该层训练数据中的偶然性。但在循环网络中如何正确使用dropout,这并不是一个简单的问题。在循环层前面使用dropout,这种正则化会妨碍学习过程,而不是有所帮助。

2015 年,在关于贝叶斯深度学习的博士论文中 ,Yarin Gal 确定了在循环网络中使用 dropout 的正确方法:对每个时间步应该使用相同的 dropout 掩码(dropout mask,相同模式的舍弃单元),而不是让 dropout 掩码随着时间步的增加而随机变化。此外,为了对 GRU、LSTM 等循环层得到的表示做正则化,应该将不随时间变化的 dropout 掩码应用于层的内部循环激活(叫作循环 dropout 掩码)。对每个时间步使用相同的 dropout 掩码,可以让网络沿着时间正确地传播其学习误差,而随时间随机变化的 dropout 掩码则会破坏这个误差信号,并且不利于学习过程。

Yarin Gal 使用 Keras 开展这项研究,并帮助将这种机制直接内置到 Keras 循环层中。Keras的每个循环层都有两个与 dropout 相关的参数:一个是 dropout,它是一个浮点数,指定该层输入单元的 dropout 比率;另一个是recurrent_dropout,指定循环单元的 dropout 比率。我们向 GRU 层中添加 dropout 和循环 dropout,看一下这么做对过拟合的影响。因为使用 dropout正则化的网络总是需要更长的时间才能完全收敛,所以网络训练轮次增加为原来的 2 倍。

代码显示

from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
model.add(layers.GRU(32,
 dropout=0.2,
 recurrent_dropout=0.2,
 input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))
model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,
 							steps_per_epoch=500,
							epochs=40,
							validation_data=val_gen,
							validation_steps=val_steps)

结果显示

分别使用CNN模型和GRU模型进行耶拿温度预测并以及模型改进(含详细代码)_第5张图片
结果显示,前30轮都没有发生过拟合,评估分数更加稳定。

循环层堆叠

模型不再过拟合,但是如何保证性能没有问题,就要考虑增加网络容量。增加网络容量是一个好主意,知道过拟合变成主要的障碍。如果过拟合不是很严重,模型结果依旧不理想,那么可能需要调节的就是容量问题了。

使用dropout正则化的堆叠GRU模型

from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
model.add(layers.GRU(32,
 dropout=0.1,
 recurrent_dropout=0.5,
 return_sequences=True,
 input_shape=(None, float_data.shape[-1])))
model.add(layers.GRU(64, activation='relu',
 dropout=0.1,
 recurrent_dropout=0.5))
model.add(layers.Dense(1))
model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,
 steps_per_epoch=500,
 epochs=40,
 validation_data=val_gen,
 validation_steps=val_steps)

结果显示

分别使用CNN模型和GRU模型进行耶拿温度预测并以及模型改进(含详细代码)_第6张图片
根据图示结果可以看出:添加一层的确对结果有所改进,但是并不显著。可以得到以下两个结论:
1、因为过拟合不是很严重所以可以增大每层的大小,以进一步改进验证损失。
2、添加一层后模型没有显著改进,提高网络能力的回报在逐渐减小。

使用双向RNN

双向RNN(bidirectional RNN)是一种常见的RNN变体,在某些任务上性能比普通RNN更好。常用于自然语言处理。

RNN特别依赖顺序或时间,RNN按顺序处理输入序列的时间步。双向RNN利用了RNN的顺序敏感性,它包含两个RNN,每个RNN分别沿一个方向对输入序列进行处理,一个是时间正序,一个是时间逆序。然后将他们的表示合并在一起。通过沿用这两个方向处理序列,双向RNN能够捕捉到可能被单向RNN忽略的模式。

如果RNN按照时间逆序处理序列,只用编写一个数据生成器的变体,将输入序列沿着时间维度反转。

训练一个双向GRU

from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
model.add(layers.Bidirectional(
 layers.GRU(32), input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))
model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,
 			steps_per_epoch=500,
 			epochs=40,
 			validation_data=val_gen,
			validation_steps=val_steps)

这个模型的表现与普通 GRU 层差不多一样好。其原因很容易理解:所有的预测能力肯定都来自于正序的那一半网络,因为我们已经知道,逆序的那一半在这个任务上的表现非常糟糕(本例同样是因为,最近的数据比久远的数据更加重要)。

参考:deep learning with python

你可能感兴趣的:(cnn,gru)