【DL】时间序列的深度学习
🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
本章涵盖
1 不同种类的时间序列任务
时间序列可以是通过定期测量获得的任何数据,例如股票的每日价格、城市每小时的用电量或商店的每周销售额。时间序列无处不在,无论我们是在研究自然现象(如地震活动、河流中鱼类种群的演变或某个地点的天气)还是人类活动模式(如网站的访问者、国家的 GDP 或信贷)卡交易)。与您迄今为止遇到的数据类型不同,使用时间序列需要了解系统的动态——它的周期性周期、它如何随时间变化、它的规律状态和它的突然峰值。
到目前为止,最常见的与时间序列相关的任务是预测:预测什么将在一系列的下一个发生。提前几个小时预测用电量,以便预测需求;提前几个月预测收入,以便您计划预算;提前几天预测天气,这样您就可以计划您的日程安排。预测是本章的重点。但实际上你可以用时间序列做很多其他的事情:
-
事件检测——识别在连续数据流中发生特定的预期事件。一个特别有用的应用是“热词检测”,其中模型监控音频流并检测诸如“Ok Google”或“Hey Alexa”之类的话语。
-
异常检测——检测任何东西在连续数据流中发生不寻常的事情。您的公司网络上的异常活动?可能是攻击者。生产线上的异常读数?是时候让人类去看看了。异常检测通常是通过无监督学习完成的,因为您通常不知道要查找哪种异常,因此您无法针对特定的异常示例进行训练。
使用时间序列时,您会遇到各种特定领域的数据表示技术。例如,您可能已经听说过傅里叶变换,它包括用不同频率的波的叠加来表达一系列值。在预处理任何主要以其周期和振荡为特征的数据(如声音、摩天大楼框架的振动或您的脑电波)时,傅里叶变换非常有价值。在深度学习的背景下,傅里叶分析(或相关的梅尔频率分析)和其他特定领域的表示可以用作特征工程的一种形式,一种在训练模型之前准备数据的方法,以完成工作模型更容易。但是,我们不会在这些页面中介绍这些技术。相反,我们将专注于建模部分。
在本章中,您将了解递归神经网络(RNN)以及如何将它们应用于时间序列预测。
2 温度预测示例
在本章中,我们所有的代码示例都将针对一个问题:预测未来 24 小时的温度,给定一系列每小时测量的大气压力和湿度等量值,这些量值由一组传感器记录在最近的过去。建筑物的屋顶。正如您将看到的,这是一个相当具有挑战性的问题!
我们将使用这个温度预测任务来突出时间序列数据与您迄今为止遇到的数据集类型根本不同的原因。你会看到密集连接的网络和卷积网络并不能很好地处理这种数据集,而另一种机器学习技术——循环神经网络 (RNN)——确实可以解决这类问题。
我们将使用德国耶拿马克斯普朗克生物地球化学研究所气象站记录的天气时间序列数据集。1在这个数据集中,几年来每 10 分钟记录 14 个不同的量(例如温度、压力、湿度、风向等)。原始数据可以追溯到 2003 年,但我们将下载的数据子集仅限于 2009-2016 年。
1 Adam Erickson 和 Olaf Kolle,www.bgc-jena.mpg.de/wetter。
!wget https://s3.amazonaws.com/keras-datasets/jena_climate_2009_2016.csv.zip
!unzip jena_climate_2009_2016.csv.zip
import os
fname = os.path.join("jena_climate_2009_2016.csv")
with open(fname) as f:
data = f.read()
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)"]
现在,将所有 420,551 行数据转换为 NumPy 数组:一个数组用于温度(以摄氏度为单位),另一个用于其余数据——我们将使用这些特征来预测未来温度。请注意,我们丢弃了“日期时间”列。
import numpy as np
temperature = np.zeros((len(lines),))
raw_data = np.zeros((len(lines), len(header) - 1))
for i, line in enumerate(lines):
values = [float(x) for x in line.split(",")[1:]]
temperature[i] = values[1] ❶
raw_data[i, :] = values[:] ❷
❷我们将所有列(包括温度)存储在“raw_data”数组中。
图 1 显示了温度(以摄氏度为单位)随时间的变化曲线。在此图上,您可以清楚地看到温度的年度周期性——数据跨度为 8 年。
from matplotlib import pyplot as plt
plt.plot(range(len(temperature)), temperature)
图 2 显示了前 10 天温度数据的更窄图。因为每 10 分钟记录一次数据,所以每天可以获得 24 × 6 = 144 个数据点。
plt.plot(range(1440), temperature[:1440])
在此图上,您可以看到每日周期性,尤其是最近 4 天。另请注意,这 10 天期间必须来自相当寒冷的冬季月份。
多个时间尺度上的周期性是时间序列数据的一个重要且非常常见的属性。无论您是查看天气、商场停车位占用率、网站流量、杂货店销售情况还是健身追踪器中记录的步数,您都会看到日周期和年周期(人工生成的数据也往往具有每周周期)。在探索您的数据时,请务必寻找这些模式。
使用我们的数据集,如果您试图根据过去几个月的数据预测下个月的平均温度,由于数据具有可靠的年尺度周期性,问题将很容易。但是从几天的数据来看,温度看起来要混乱得多。这个时间序列在日常规模上是否可预测?让我们来了解一下。
在我们所有的实验中,我们将使用前 50% 的数据进行训练,接下来的 25% 用于验证,最后 25% 用于测试。在处理时间序列数据时,使用比训练数据更新的验证和测试数据很重要,因为您试图根据过去预测未来,而不是相反,您的验证/测试拆分应该反映这一点。如果你反转时间轴,一些问题会变得相当简单!
num_train_samples = int(0.5 * len(raw_data))
num_val_samples = int(0.25 * len(raw_data))
num_test_samples = len(raw_data) - num_train_samples - num_val_samples
print("num_train_samples:", num_train_samples)
print("num_val_samples:", num_val_samples)
print("num_test_samples:", num_test_samples)
num_train_samples: 210225 num_val_samples: 105112 num_test_samples: 105114
2.1 准备数据
问题的确切表述如下:给定涵盖前五天的数据,每小时采样一次,我们可以预测 24 小时内的温度吗?
首先,让我们将数据预处理为神经网络可以摄取的格式。这很简单:数据已经是数字的,所以你不需要做任何矢量化。但是数据中的每个时间序列都有不同的规模(例如,以 mbar 为单位测量的大气压力约为 1,000,而以每摩尔毫摩尔为单位测量的 H2OC 约为 3)。我们将独立地对每个时间序列进行归一化,以便它们都采用相似规模的小值。我们将使用前 210,225 个时间步长作为训练数据,因此我们将仅计算这部分数据的均值和标准差。
mean = raw_data[:num_train_samples].mean(axis=0)
raw_data -= mean
std = raw_data[:num_train_samples].std(axis=0)
raw_data /= std
接下来,让我们创建一个Dataset
对象产生过去五天的批量数据以及未来 24 小时的目标温度。因为数据集中的样本是高度冗余的(样本N和样本N +
1
的大部分时间步长是相同的),因此为每个样本显式分配内存会很浪费。相反,我们将即时生成样本,同时仅将原始数据raw_data
和temperature
数组保存在内存中,仅此而已。
我们可以很容易地编写一个 Python 生成器来执行此操作,但是 Keras 中有一个内置的数据集实用程序可以执行此操作 ( timeseries_dataset_from_array()
),因此我们可以通过使用它来节省一些工作。您通常可以将它用于任何类型的时间序列预测任务。
理解timeseries_dataset_from_array()
为了理解timeseries_dataset_from_array()
它的作用,让我们看一个简单的例子。一般的想法是您提供一个时间序列数据数组(data
参数),并timeseries_dataset_from_array()
为您提供从原始时间序列中提取的窗口(我们将它们称为“序列”)。
例如,如果您使用data
=
[0
1
2
3
4
5
6]
and sequence_length=3
,那么timeseries_dataset_from_array()
将生成以下示例:[0
1
2]
, [1
2
3]
, [2
3
4]
, [3
4
5]
, [4
5
6]
.
您还可以将targets
参数(数组)传递给timeseries_dataset_ from_array()
. targets
数组的第一个条目应该匹配将从data
数组生成的第一个序列的所需目标。因此,如果您正在进行时间序列预测,targets
则应该与 相同的数组data
,偏移一些量。
例如,使用data
=
[0
1
2
3
4
5
6
...]
和sequence_length=3
,您可以创建一个数据集,通过传递 来预测系列中的下一步targets
=
[3
4
5
6
...]
。让我们尝试一下:
import numpy as np
from tensorflow import keras
int_sequence = np.arange(10) ❶
dummy_dataset = keras.utils.timeseries_dataset_from_array(
data=int_sequence[:-3], ❷
targets=int_sequence[3:], ❸
sequence_length=3, ❹
batch_size=2, ❺
)
for inputs, targets in dummy_dataset:
for i in range(inputs.shape[0]):
print([int(x) for x in inputs[i]], int(targets[i]))
❷我们生成的序列将从 [0 1 2 3 4 5 6] 中采样。
❸从 data[N] 开始的序列的目标是 data[N + 3]。
[0, 1, 2] 3
[1, 2, 3] 4
[2, 3, 4] 5
[3, 4, 5] 6
[4, 5, 6] 7
我们将使用timeseries_dataset_from_array()
实例化三个数据集:一个用于训练,一个用于验证,一个用于测试。
在制作训练数据集时,我们将通过start_index
=
0
并end_index
=
num_train_samples
仅使用前 50% 的数据。对于验证数据集,我们将传递start_index
=
num_train_samples
并end_index
=
num_train_samples
+
num_val_samples
使用接下来 25% 的数据。最后,对于测试数据集,我们将通过start_index
=
num_train_samples
+
num_val_samples
使用剩余的样本。
sampling_rate = 6
sequence_length = 120
delay = sampling_rate * (sequence_length + 24 - 1)
batch_size = 256
train_dataset = keras.utils.timeseries_dataset_from_array(
raw_data[:-delay],
targets=temperature[delay:],
sampling_rate=sampling_rate,
sequence_length=sequence_length,
shuffle=True,
batch_size=batch_size,
start_index=0,
end_index=num_train_samples)
val_dataset = keras.utils.timeseries_dataset_from_array(
raw_data[:-delay],
targets=temperature[delay:],
sampling_rate=sampling_rate,
sequence_length=sequence_length,
shuffle=True,
batch_size=batch_size,
start_index=num_train_samples,
end_index=num_train_samples + num_val_samples)
test_dataset = keras.utils.timeseries_dataset_from_array(
raw_data[:-delay],
targets=temperature[delay:],
sampling_rate=sampling_rate,
sequence_length=sequence_length,
shuffle=True,
batch_size=batch_size,
start_index=num_train_samples + num_val_samples)
每个数据集产生一个元组(samples,
targets)
,其中samples
256 个样本的批次,每个样本包含 120 个连续小时的输入数据,并且targets
是 256 个目标温度的相应数组。请注意,样本是随机打乱的,因此批次中的两个连续序列(如samples[0]
和samples[1]
)不一定在时间上接近。
for samples, targets in train_dataset:
print("samples shape:", samples.shape)
print("targets shape:", targets.shape)
break
Output:
samples shape: (256, 120, 14) targets shape: (256,)
2.2 一个常识性的、非机器学习的基线
在我们开始使用黑盒深度学习模型来解决温度预测问题之前,让我们尝试一种简单的常识性方法。它将作为健全性检查,并将建立一个我们必须超越的基线,以证明更先进的机器学习模型的有用性。当您处理尚无已知解决方案的新问题时,此类常识性基线可能很有用。一个典型的例子是不平衡的分类任务,其中一些类比其他类更常见。如果您的数据集包含 90% 的 A 类实例和 10% 的 B 类实例,那么分类任务的常识方法是在出现新样本时始终预测“A”。这样的分类器总体准确率为 90%,因此,任何基于学习的方法都应该超过这 90% 的分数,以证明其有用性。有时,这样的基本基线可以证明是令人惊讶的难以超越。
在这种情况下,可以安全地假设温度时间序列是连续的(明天的温度可能接近今天的温度)以及具有每日周期的周期性。因此,一种常识性的方法是始终预测从现在起 24 小时后的温度将等于现在的温度。让我们使用平均绝对值来评估这种方法误差 (MAE) 度量,定义如下:
np.mean(np.abs(preds - targets))
def evaluate_naive_method(dataset):
total_abs_err = 0.
samples_seen = 0
for samples, targets in dataset:
preds = samples[:, -1, 1] * std[1] + mean[1] ❶
total_abs_err += np.sum(np.abs(preds - targets))
samples_seen += samples.shape[0]
return total_abs_err / samples_seen
print(f"Validation MAE: {evaluate_naive_method(val_dataset):.2f}")
print(f"Test MAE: {evaluate_naive_method(test_dataset):.2f}")
❶温度特征位于第 1 列,因此 samples[:, -1, 1] 是输入序列中的最后一个温度测量值。回想一下,我们对特征进行了归一化,因此要检索以摄氏度为单位的温度,我们需要通过将其乘以标准偏差并加回均值来对其进行非归一化。
这个常识性基线实现了 2.44 摄氏度的验证 MAE 和 2.62 摄氏度的测试 MAE。因此,如果您总是假设未来 24 小时的温度将与现在相同,那么您将平均下降两度半。这还不错,但您可能不会基于此启发式启动天气预报服务。现在的游戏就是利用你的深度学习知识来做得更好。
2.3 让我们尝试一个基本的机器学习模型
就像在尝试机器学习方法之前建立常识基线很有用一样,在研究复杂且计算量大的模型(例如 RNN)之前尝试简单、廉价的机器学习模型(例如小型、密集连接的网络)也很有用. 这是确保您在问题上进一步复杂化的最佳方法是合法的并带来真正的好处。
以下清单显示了一个完全连接的模型,该模型首先将数据展平,然后运行它两层Dense
。注意最后一层缺少激活函数Dense
,这是回归问题的典型特征。我们使用均方误差(MSE)作为损失,而不是 MAE,因为与 MAE 不同,它在零附近平滑,这是梯度下降的有用属性。我们将通过将 MAE 添加为compile()
.
from tensorflow import keras
from tensorflow.keras import layers
inputs = keras.Input(shape=(sequence_length, raw_data.shape[-1]))
x = layers.Flatten()(inputs)
x = layers.Dense(16, activation="relu")(x)
outputs = layers.Dense(1)(x)
model = keras.Model(inputs, outputs)
callbacks = [
keras.callbacks.ModelCheckpoint("jena_dense.keras", ❶
save_best_only=True)
]
model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"])
history = model.fit(train_dataset,
epochs=10,
validation_data=val_dataset,
callbacks=callbacks)
model = keras.models.load_model("jena_dense.keras") ❷
print(f"Test MAE: {model.evaluate(test_dataset)[1]:.2f}") ❷
import matplotlib.pyplot as plt
loss = history.history["mae"]
val_loss = history.history["val_mae"]
epochs = range(1, len(loss) + 1)
plt.figure()
plt.plot(epochs, loss, "bo", label="Training MAE")
plt.plot(epochs, val_loss, "b", label="Validation MAE")
plt.title("Training and validation MAE")
plt.legend()
plt.show()
图 3 耶拿温度预测任务上的训练和验证 MAE,具有简单、密集连接的网络
一些验证损失接近无学习基线,但不可靠。这首先表明了拥有此基线的优点:事实证明,要超越它并不容易。您的常识包含许多机器学习模型无法访问的有价值信息。
您可能想知道,如果存在从数据到目标(常识基线)的简单、性能良好的模型,为什么您正在训练的模型没有找到并改进它?好吧,您正在寻找解决方案的模型空间(即您的假设空间)是具有您定义的配置的所有可能的两层网络的空间。常识启发式只是可以在这个空间中表示的数百万模型中的一个。这就像大海捞针。仅仅因为在您的假设空间中技术上存在一个好的解决方案并不意味着您将能够通过梯度下降找到它。
一般来说,这是机器学习的一个非常重要的限制:除非学习算法被硬编码以寻找特定类型的简单模型,否则它有时可能无法找到简单问题的简单解决方案。这就是为什么利用良好的特征工程和相关的架构先验是必不可少的:你需要准确地告诉你的模型它应该寻找什么。
2.4 让我们尝试一维卷积模型
说到利用正确的架构先验,由于我们的输入序列具有每日周期,也许卷积模型可以工作。时间卷积网络可以在不同的日子重用相同的表示,就像空间卷积可以在图像的不同位置重用相同的表示一样。
你已经知道了和层Conv2D
,SeparableConv2D
其中看到他们通过在 2D 网格上滑动的小窗口进行输入。还有1D甚至3D版本的这些层:Conv1D
、SeparableConv1D
和Conv3D
。2该Conv1D
层依赖于在输入序列上滑动的一维窗口,而该Conv3D
层依赖于在输入体积上滑动的立方窗口。
2 请注意,没有SeparableConv3D
层,不是出于任何理论上的原因,而仅仅是因为我还没有实现它。
因此,您可以构建 1D convnets,严格类似于 2D convnets。它们非常适合遵循平移不变性假设的任何序列数据(这意味着如果您在序列上滑动窗口,则窗口的内容应该遵循与窗口位置无关的相同属性)。
让我们尝试一下我们的温度预测问题。我们将选择 24 的初始窗口长度,以便我们一次查看 24 小时的数据(一个周期)。当我们对序列进行下采样(通过MaxPooling1D
层)时,我们将相应地减小窗口大小:
inputs = keras.Input(shape=(sequence_length, raw_data.shape[-1]))
x = layers.Conv1D(8, 24, activation="relu")(inputs)
x = layers.MaxPooling1D(2)(x)
x = layers.Conv1D(8, 12, activation="relu")(x)
x = layers.MaxPooling1D(2)(x)
x = layers.Conv1D(8, 6, activation="relu")(x)
x = layers.GlobalAveragePooling1D()(x)
outputs = layers.Dense(1)(x)
model = keras.Model(inputs, outputs)
callbacks = [
keras.callbacks.ModelCheckpoint("jena_conv.keras",
save_best_only=True)
]
model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"])
history = model.fit(train_dataset,
epochs=10,
validation_data=val_dataset,
callbacks=callbacks)
model = keras.models.load_model("jena_conv.keras")
print(f"Test MAE: {model.evaluate(test_dataset)[1]:.2f}")
图 4 使用一维卷积网络在 Jena 温度预测任务上训练和验证 MAE
事实证明,该模型的性能甚至比密集连接的模型更差,仅实现了约 2.9 度的验证 MAE,与常识基线相差甚远。这里出了什么问题?两件事情:
-
首先,天气数据不太尊重平移不变性假设。虽然数据确实具有每日周期的特征,但早上的数据与晚上或深夜的数据具有不同的属性。天气数据仅在非常特定的时间尺度上是平移不变的。
-
其次,我们数据的顺序很重要——很多。与五天前的数据相比,最近的过去在预测第二天的温度方面提供的信息要多得多。一维卷积网络无法利用这一事实。特别是,我们的最大池化层和全局平均池化层在很大程度上破坏了订单信息。
2.5 第一个循环基线
全连接方法和卷积方法都做得不好,但这并不意味着机器学习不适用于这个问题。密集连接的方法首先将时间序列展平,从而从输入数据中去除了时间的概念。卷积方法以相同的方式处理数据的每一段,甚至应用池化,这会破坏订单信息。相反,让我们看一下数据的本质:一个序列,其中因果关系和顺序很重要。
有一系列专门为此用例设计的神经网络架构:循环神经网络。其中,Long Short Term Memory (LSTM) 层有长期以来一直很受欢迎。我们稍后会看到这些模型是如何工作的,但让我们先尝试一下 LSTM 层。
inputs = keras.Input(shape=(sequence_length, raw_data.shape[-1]))
x = layers.LSTM(16)(inputs)
outputs = layers.Dense(1)(x)
model = keras.Model(inputs, outputs)
callbacks = [
keras.callbacks.ModelCheckpoint("jena_lstm.keras",
save_best_only=True)
]
model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"])
history = model.fit(train_dataset,
epochs=10,
validation_data=val_dataset,
callbacks=callbacks)
model = keras.models.load_model("jena_lstm.keras")
print(f"Test MAE: {model.evaluate(test_dataset)[1]:.2f}")
图 5 显示了结果。好多了!我们实现了低至 2.36 度的验证 MAE 和 2.55 度的测试 MAE。基于 LSTM 的模型终于可以超越常识基线(尽管目前只是一点点),证明了机器学习在这项任务上的价值。
图 5 使用基于 LSTM 的模型在 Jena 温度预测任务上训练和验证 MAE(请注意,我们在此图中省略了 epoch 1,因为在 epoch 1 的高训练 MAE(7.75)会扭曲规模)
但为什么 LSTM 模型的性能明显优于密集连接模型或卷积网络?我们如何进一步完善模型?为了回答这个问题,让我们仔细看看递归神经网络。
3 理解循环神经网络
到目前为止,您看到的所有神经网络(例如密集连接网络和卷积网络)的一个主要特征是它们没有记忆。显示给他们的每个输入都是独立处理的,输入之间不保留任何状态。使用这样的网络,为了处理一个序列或一系列时间数据点,您必须立即向网络显示整个序列:将其转换为单个数据点。例如,这就是我们在密集连接网络示例中所做的:我们将五天的数据扁平化为一个大向量并一次性处理它。这样的网络是称为前馈网络。
相反,当你阅读现在的句子时,你是在逐字逐句处理它——或者更确切地说,逐个眼球扫视——同时保留对之前内容的记忆;这为您提供了该句子所传达含义的流畅表示。生物智能以增量方式处理信息,同时维护其处理内容的内部模型,该模型基于过去的信息构建,并随着新信息的出现而不断更新。
循环神经网络( RNN) 采用了相同的原理,尽管是一个极其简化的版本:它通过迭代序列元素并保持包含与迄今为止所见信息相关的信息的状态来处理序列。实际上,RNN 是一种具有内部循环的神经网络(见图 6)。
RNN 的状态在处理两个不同的独立序列(例如一批中的两个样本)之间重置,因此您仍然将一个序列视为单个数据点:网络的单个输入。不同的是,这个数据点不再单步处理;相反,网络内部循环遍历序列元素。
为了使循环和状态的这些概念更清楚,让我们实现一个玩具 RNN 的前向传递。这个 RNN 将向量序列作为输入,我们将其编码为 size 的 rank-2 张量(timesteps,
input_features)
。它在时间步长上循环,并且在每个时间步长上,它考虑其当前状态 att
和t
(of shape的输入(input_features,)
,并将它们组合以获得在 的输出t
。然后我们将下一步的状态设置为前一个输出。对于第一个时间步,没有定义先前的输出;因此,没有当前状态。因此我们将状态初始化为一个全零向量,称为网络的初始状态。
state_t = 0 ❶
for input_t in input_sequence: ❷
output_t = f(input_t, state_t)
state_t = output_t ❸
您甚至可以充实函数f
:输入和状态到输出的转换将由两个矩阵W
和U
和一个偏置向量进行参数化。它类似于前馈网络中密集连接层操作的转换。
state_t = 0
for input_t in input_sequence:
output_t = activation(dot(W, input_t) + dot(U, state_t) + b)
state_t = output_t
为了使这些概念绝对明确,让我们编写一个简单 RNN 前向传递的简单 NumPy 实现。
import numpy as np
timesteps = 100 ❶
input_features = 32 ❷
output_features = 64 ❸
inputs = np.random.random((timesteps, input_features)) ❹
state_t = np.zeros((output_features,)) ❺
W = np.random.random((output_features, input_features)) ❻
U = np.random.random((output_features, output_features)) ❻
b = np.random.random((output_features,)) ❻
successive_outputs = []
for input_t in inputs: ❼
output_t = np.tanh(np.dot(W, input_t) + np.dot(U, state_t) + b) ❽
successive_outputs.append(output_t) ❾
state_t = output_t ❿
final_output_sequence = np.stack(successive_outputs, axis=0) ⓫
❼ input_t 是一个形状向量 (input_features,)。
❽将输入与当前状态(前一个输出)结合,得到当前输出。我们使用 tanh 来添加非线性(我们可以使用任何其他激活函数)。
⓫最终输出是一个形状为 rank-2 的张量(timesteps,output_features)。
这很容易。总而言之,RNN 是一个for
循环,它重用在循环的前一次迭代中计算的数量,仅此而已。当然,您可以构建许多符合此定义的不同 RNN——此示例是最简单的 RNN 公式之一。RNN 的特征在于它们的阶跃函数,例如本例中的以下函数(见图7)。
output_t = np.tanh(np.dot(W, input_t) + np.dot(U, state_t) + b)
注意在这个例子中,最终输出是一个 rank-2 张量 shape (timesteps,
output_features)
,其中每个时间步长是循环在 time 的输出t
。输出张量中的每个时间步都t
包含有关输入序列中时间步的信息——关于整个过去。出于这个原因,在许多情况下,您不需要这个完整的输出序列;您只需要最后一个输出(在循环末尾),因为它已经包含有关整个序列的信息。0
t
output_t
3.1 Keras 中的循环层
我们刚刚在 NumPy 中天真的实现的过程对应于一个实际的 Keras层——SimpleRNN
层。
有一个小区别:SimpleRNN
处理成批的序列,就像所有其他 Keras 层一样,而不是像 NumPy 示例中的单个序列。这意味着它接受 shape 的输入(batch_size,
timesteps,
input_features)
,而不是(timesteps,
input_features)
. 指定shape
initial 的参数时Input()
,请注意您可以将timesteps
条目设置为None
,这使您的网络能够处理任意长度的序列。
num_features = 14
inputs = keras.Input(shape=(None, num_features))
outputs = layers.SimpleRNN(16)(inputs)
如果您的模型旨在处理可变长度的序列,这将特别有用。但是,如果您的所有序列都具有相同的长度,我建议指定一个完整的输入形状,因为它可以model.summary()
显示输出长度信息,这总是很好的,它可以解锁一些性能优化(参见本章稍后的“关于 RNN 运行时性能的注意事项”侧边栏)。
Keras 中的所有循环层(SimpleRNN
、、LSTM
和shape 的 rank-2 张量)。这两种模式由构造函数参数控制。让我们看一个示例,该示例仅使用并返回最后一个时间步的输出。GRU
(batch_size,
timesteps,
output_features)
(batch_ size,
output_features)
return_sequences
SimpleRNN
num_features = 14
steps = 120
inputs = keras.Input(shape=(steps, num_features))
outputs = layers.SimpleRNN(16, return_sequences=False)(inputs) ❶
print(outputs.shape)
(None, 16)
❶请注意,return_sequences=False 是默认值。
num_features = 14
steps = 120
inputs = keras.Input(shape=(steps, num_features))
outputs = layers.SimpleRNN(16, return_sequences=True)(inputs)
print(outputs.shape)
(120, 16)
为了增加网络的表示能力,一个接一个地堆叠几个循环层有时很有用。在这样的设置中,您必须让所有中间层返回完整的输出序列。
inputs = keras.Input(shape=(steps, num_features))
x = layers.SimpleRNN(16, return_sequences=True)(inputs)
x = layers.SimpleRNN(16, return_sequences=True)(x)
outputs = layers.SimpleRNN(16)(x)
在实践中,您很少会使用SimpleRNN
图层。它通常过于简单而无法真正使用。特别是,SimpleRNN
有一个主要问题:尽管理论上它应该能够保留t
有关之前许多时间步长的输入的时间信息,但这种长期依赖关系在实践中证明是不可能学习的。这是由于梯度消失问题,一种效果这类似于在多层深的非循环网络(前馈网络)中观察到的情况:当您不断向网络添加层时,网络最终变得无法训练。Hochreiter、Schmidhuber 和 Bengio 在 1990 年代初期研究了这种效应的理论原因。3
3 例如,参见 Yoshua Bengio、Patrice Simard 和 Paolo Frasconi,“Learning Long-Term Dependencies with Gradient Descent Is Difficult”,IEEE Transactions on Neural Networks 5,no. 2 (1994)。
值得庆幸的是,SimpleRNN
这不是 Keras 中唯一可用的循环层。那里有两个其他 ,LSTM
和GRU
, 是旨在解决这些问题。
让我们考虑LSTM
层。底层的长短期记忆 (LSTM) 算法是由 Hochreiter 和 Schmidhuber 在 1997 年开发的;4这是他们对消失梯度问题的研究的高潮。
4 Sepp Hochreiter 和 Jürgen Schmidhuber,“长期短期记忆”,《神经计算》 ,第 9 期,第 8 期(1997 年)。
该层是SimpleRNN
您已经知道的层的变体;它增加了一种跨多个时间步携带信息的方法。想象一条传送带与您正在处理的序列平行运行。来自序列的信息可以在任何时候跳到传送带上,传输到稍后的时间步,并在您需要时完整地跳出。这本质上就是 LSTM 所做的:它为以后保存信息,从而防止旧信号在处理过程中逐渐消失。这应该提醒您残余连接,您在第 9 章中了解到:这几乎是相同的想法。
要详细了解这个过程,让我们从SimpleRNN
单元格开始(见图 8)。因为你会有很多权重矩阵,所以在单元格中索引W
和U
矩阵,用字母o
( Wo
and Uo
) 表示output。
图 8 层的起点LSTM
:aSimpleRNN
让我们在这张图片中添加一个额外的数据流,该数据流跨时间步长传递信息。在不同的时间步调用它的值c_t
,其中 C 代表进位。该信息将对单元产生以下影响:它将与输入连接和循环连接结合(通过密集变换:具有权重矩阵的点积,然后是偏置添加和激活函数的应用),它会影响发送到下一个时间步的状态(通过激活函数和乘法运算)。从概念上讲,进位数据流是一种调制下一个输出和下一个状态的方法(见图9)。到目前为止很简单。
图 9 从 SimpleRNN 到 LSTM:添加进位轨道
现在是微妙之处——计算进位数据流的下一个值的方式。它涉及三个不同的转变。这三个都具有SimpleRNN
单元格的形式:
y = activation(dot(state_t, U) + dot(input_t, W) + b)
但是所有三个变换都有自己的权重矩阵,我们将用字母i
、f
和来索引k
。这是我们到目前为止所拥有的(这可能看起来有点武断,但请耐心等待)。
output_t = activation(dot(state_t, Uo) + dot(input_t, Wo) + dot(c_t, Vo) + bo)
i_t = activation(dot(state_t, Ui) + dot(input_t, Wi) + bi)
f_t = activation(dot(state_t, Uf) + dot(input_t, Wf) + bf)
k_t = activation(dot(state_t, Uk) + dot(input_t, Wk) + bk)
c_t
我们通过组合i_t
、f_t
和获得新的进位状态(下一个) k_t
。
c_t+1 = i_t * k_t + c_t * f_t
如图10 所示添加它,就是这样。没那么复杂——只是有点复杂。
如果你想获得哲学,你可以解释这些操作中的每一个是什么意思。例如,您可以说乘以c_t
andf_t
是一种故意忘记携带数据流中不相关信息的方法。同时,i_t
提供k_t
有关现在的信息,用新的信息更新进位轨迹。但归根结底,这些解释并没有多大意义,因为这些操作实际上是什么do 由参数化它们的权重的内容决定;并且权重是以端到端的方式学习的,从每一轮训练开始,因此不可能将这个或那个操作归功于特定的目的。RNN 单元的规格(如前所述)决定了你的假设空间——在训练期间你将在其中搜索良好模型配置的空间——但它并不能确定单元的作用;这取决于单元格的权重。具有不同权重的同一个细胞可以做非常不同的事情。因此,构成 RNN 单元的操作组合更好地解释为对搜索的一组约束,而不是工程意义上的设计。
可以说,这些约束的选择——如何实现 RNN 单元的问题——最好留给优化算法(如遗传算法或强化学习过程)而不是人类工程师。将来,这就是我们构建模型的方式。总结一下:你不需要了解任何关于 LSTM 单元的具体架构的东西;作为一个人,理解它不应该是你的工作。请记住 LSTM 单元的用途:允许稍后重新注入过去的信息,从而解决梯度消失问题。
4 循环神经网络的高级使用
接下来,我们将回顾 RNN 的一些更高级的功能,它们可以帮助您充分利用您的深度学习序列模型。在本节结束时,您将了解有关在 Keras 中使用循环网络的大部分知识。
4.1 使用经常性 dropout 来对抗过拟合
让我们回到我们在 2.5 节中使用的基于 LSTM 的模型——我们的第一个能够超越常识基线的模型。如果您查看训练和验证曲线(图 5),很明显该模型很快就会过拟合,尽管只有很少的单元:训练和验证损失在几个 epoch 后开始显着分歧。您已经熟悉了对抗这种现象的经典技术:dropout,它随机将层的输入单元归零,以破坏层所暴露的训练数据中的偶然相关性。但是如何在循环网络中正确应用 dropout 并不是一个简单的问题。
众所周知,在循环层之前应用 dropout 会阻碍学习,而不是帮助正则化。2016 年,Yarin Gal 作为他关于贝叶斯深度学习的博士论文的一部分,图5确定了在循环网络中使用 dropout 的正确方法:应该在每个时间步应用相同的 dropout 掩码(丢弃单元的相同模式),而不是使用随时间步随机变化的 dropout 掩码。更重要的是,为了规范由层的循环门形成的表示,例如GRU
和LSTM
,应该将时间恒定的 dropout 掩码应用于层的内部循环激活(循环 dropout 掩码)。在每个时间步使用相同的 dropout 掩码可以让网络随着时间正确传播其学习错误;一个时间随机的 dropout 掩码会破坏这个错误信号并且对学习过程有害。
5 请参阅 Yarin Gal,“深度学习中的不确定性”,博士论文 (2016), http: //mng.bz/WBq1。
Yarin Gal 使用 Keras 进行研究,并帮助将这种机制直接构建到 Keras 循环层中。Keras 中的每个循环层都有两个与 dropout 相关的参数:dropout
一个浮点数,指定层的输入单元的丢失率,以及recurrent_dropout
指定循环单元的丢失率。让我们在第一个 LSTM 示例的层中添加经常性LSTM
dropout,看看这样做会如何影响过拟合。
由于 dropout,我们不需要过多地依赖网络大小来进行正则化,因此我们将使用具有两倍多单元的 LSTM 层,希望这应该更具表现力(如果没有 dropout,这个网络就会开始马上过拟合——试试看)。因为使用 dropout 进行正则化的网络总是需要更长的时间才能完全收敛,所以我们将训练模型的 epoch 数是原来的五倍。
清单 22 训练和评估 dropout-regularized LSTM
inputs = keras.Input(shape=(sequence_length, raw_data.shape[-1]))
x = layers.LSTM(32, recurrent_dropout=0.25)(inputs)
x = layers.Dropout(0.5)(x) ❶
outputs = layers.Dense(1)(x)
model = keras.Model(inputs, outputs)
callbacks = [
keras.callbacks.ModelCheckpoint("jena_lstm_dropout.keras",
save_best_only=True)
]
model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"])
history = model.fit(train_dataset,
epochs=50,
validation_data=val_dataset,
callbacks=callbacks)
❶为了规范 Dense 层,我们还在 LSTM 之后添加了一个 Dropout 层。
图 11 显示了结果。成功!在前 20 个 epoch 中,我们不再过度拟合。我们实现了低至 2.27 度的验证 MAE(比无学习基线提高 7%)和 2.45 度的测试 MAE(比基线提高 6.5%)。还不错。
具有很少参数的循环模型,如本章中的模型,在多核 CPU 上往往比在 GPU 上快得多,因为它们只涉及小矩阵乘法,并且由于存在乘法链并不能很好地并行化的一个for
循环。但是较大的 RNN 可以从 GPU 运行时中受益匪浅。
当在 GPU 上使用带有默认关键字参数的 KerasLSTM
或GRU
层时,您的层将利用 cuDNN 内核,这是一个高度优化的、低级的、由 NVIDIA 提供的底层算法实现(我在前一章中提到了这些)。像往常一样,cuDNN 内核是喜忧参半:它们速度很快,但不灵活——如果您尝试执行默认内核不支持的任何操作,您将遭受严重的减速,这或多或少迫使您坚持 NVIDIA 发生的事情提供。例如,LSTM 和 GRU cuDNN 内核不支持经常性 dropout,因此将其添加到您的层会迫使运行时回退到常规 TensorFlow 实现,这通常在 GPU 上慢 2 到 5 倍(即使它的计算费用是一样的)。
作为一种在您无法使用 cuDNN 时加速 RNN 层的方法,您可以尝试展开它。展开for
循环包括删除循环并简单地将其内容内联N次。在for
循环神经网络的情况下,展开可以帮助 TensorFlow 优化底层计算图。但是,它也会大大增加 RNN 的内存消耗——因此,它只适用于相对较小的序列(大约 100 步或更少)。另外,请注意,只有在模型预先知道数据中的时间步数时才能执行此操作(也就是说,如果您将 a 传递给您的 initial ,shape
而没有任何None
条目Input()
)。它是这样工作的:
inputs = keras.Input(shape=(sequence_length, num_features)) ❶
x = layers.LSTM(32, recurrent_dropout=0.2, unroll=True)(inputs) ❷
图 11 使用 dropout-regularized LSTM 在 Jena 温度预测任务上的训练和验证损失
4.2 堆叠循环层
因为您不再过度拟合,但似乎遇到了性能瓶颈,您应该考虑增加网络的容量和表达能力。回想一下通用机器学习工作流程的描述:增加模型的容量通常是一个好主意,直到过度拟合成为主要障碍(假设您已经采取基本步骤来减轻过度拟合,例如使用 dropout)。只要你没有过度拟合太严重,你就很可能容量不足。
增加网络容量通常是通过增加层中的单元数量或添加更多层来完成的。循环层堆叠是构建更强大循环网络的经典方法:例如,不久前,谷歌翻译算法由七个大型 LSTM 层的堆叠提供支持——这是巨大的。
为了在 Keras 中将循环层堆叠在一起,所有中间层都应该返回它们的完整输出序列(一个 rank-3 张量),而不是它们在最后一个时间步的输出。正如您已经了解到的,这是通过指定return_sequences=True
.
在下面的示例中,我们将尝试堆叠两个 dropout-regularized 循环层。作为改变,我们将使用门控循环单元 (GRU) 层来代替的 LSTM。GRU 与 LSTM 非常相似——您可以将其视为 LSTM 架构的稍微简单的流线型版本。它是由 Cho 等人于 2014 年推出的。当循环网络刚刚开始在当时很小的研究社区中重新获得兴趣时。6
6 参见 Cho 等人,“关于神经机器翻译的属性:编码器-解码器方法”(2014 年),https://arxiv.org/abs/1409.1259。
清单23 训练和评估一个 dropout-regularized、stacked GRU 模型
inputs = keras.Input(shape=(sequence_length, raw_data.shape[-1]))
x = layers.GRU(32, recurrent_dropout=0.5, return_sequences=True)(inputs)
x = layers.GRU(32, recurrent_dropout=0.5)(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1)(x)
model = keras.Model(inputs, outputs)
callbacks = [
keras.callbacks.ModelCheckpoint("jena_stacked_gru_dropout.keras",
save_best_only=True)
]
model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"])
history = model.fit(train_dataset,
epochs=50,
validation_data=val_dataset,
callbacks=callbacks)
model = keras.models.load_model("jena_stacked_gru_dropout.keras")
print(f"Test MAE: {model.evaluate(test_dataset)[1]:.2f}")
图 12 显示了结果。我们实现了 2.39 度的测试 MAE(比基线提高了 8.8%)。您可以看到添加的图层确实稍微改善了结果,但效果并不显着。此时,您可能会看到增加网络容量带来的收益递减。
图 12 使用堆叠 GRU 网络的 Jena 温度预测任务的训练和验证损失
4.3 使用双向 RNN
我们将在本节中看到的最后一种技术是双向 RNN。双向 RNN 是一种常见的 RNN 变体,在某些任务上可以提供比常规 RNN 更高的性能。它经常用于自然语言处理——你可以称它为自然语言处理深度学习的瑞士军刀。
RNN 具有明显的顺序依赖性:它们按顺序处理输入序列的时间步,并且改组或反转时间步可以完全改变 RNN 从序列中提取的表示。这正是它们在顺序有意义的问题上表现良好的原因,例如温度预测问题。双向 RNN 利用 RNN 的顺序敏感性:它使用两个常规 RNN,例如您已经熟悉的 GRU 和 LSTM 层,每个层都在一个方向(按时间顺序和逆时间顺序)处理输入序列,然后合并它们申述。通过双向处理序列,双向 RNN 可以捕获单向 RNN 可能忽略的模式。
值得注意的是,本节中的 RNN 层已按时间顺序处理序列(首先是较旧的时间步)这一事实可能是一个任意决定。至少,到目前为止,我们还没有试图质疑这一决定。如果 RNN 按反时间顺序处理输入序列(例如,先使用较新的时间步),它们的性能是否足够好?让我们试试这个,看看会发生什么。您需要做的就是编写一个数据生成器的变体,其中输入序列沿时间维度进行还原(将最后一行替换为yield
samples[:,
::-1,
:],
targets
)。训练你在本节第一个实验中使用的相同的基于 LSTM 的模型,你会得到如图 13 所示的结果。
图 13 Jena 温度预测任务的训练和验证损失,LSTM 在反向序列上训练
逆序 LSTM 的性能甚至远远低于常识基线,这表明在这种情况下,按时间顺序处理对于该方法的成功很重要。这很有意义:底层的 LSTM 层通常比遥远的过去更能记住最近的过去,而且自然地,较新的天气数据点比旧的数据点更能预测问题(这就是常识基线的原因)相当强)。因此,按时间顺序排列的层必然会优于倒序版本。
然而,对于包括自然语言在内的许多其他问题,情况并非如此:直观地说,单词在理解句子中的重要性通常不取决于它在句子中的位置。在文本数据上,逆序处理与按时间顺序处理一样有效——您可以很好地向后阅读文本(试试看!)。尽管词序在理解语言方面确实很重要,但使用哪种顺序并不重要。
重要的是,在反向序列上训练的 RNN 将学习与在原始序列上训练的 RNN 不同的表示,就像在现实世界中时间倒流时你会有不同的心智模型一样——如果你过着第一天就死去的生活,并且在你的最后一天出生。在机器学习中,不同但有用的表示总是值得利用,它们越不同越好:它们提供了一个新的角度来查看数据,捕获其他方法遗漏的数据方面,以及因此,它们可以帮助提高任务的性能。这就是ensembling背后的直觉。
双向 RNN 利用这个想法来提高时间顺序 RNN 的性能。它以两种方式查看其输入序列(见图 14),获得可能更丰富的表示并捕获仅按时间顺序版本可能遗漏的模式。
图 14 双向 RNN 层的工作原理
要在 Keras 中实例化双向 RNN,请使用Bidirectional
层,它的第一个参数是循环层实例。Bidirectional
创建此循环层的第二个单独实例,并使用一个实例按时间顺序处理输入序列,另一个实例用于按相反顺序处理输入序列。您可以在我们的温度预测任务中试一试。
inputs = keras.Input(shape=(sequence_length, raw_data.shape[-1]))
x = layers.Bidirectional(layers.LSTM(16))(inputs)
outputs = layers.Dense(1)(x)
model = keras.Model(inputs, outputs)
model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"])
history = model.fit(train_dataset,
epochs=10,
validation_data=val_dataset)
你会发现它的性能不如普通LSTM
层。原因很容易理解:所有的预测能力都必须来自网络的时间顺序的一半,因为众所周知,逆时间顺序的一半在这项任务上表现严重(再次,因为最近的过去比遥远的过去更重要,在这个案子)。同时,反年代的一半的存在使网络的容量增加了一倍,并导致它更早地开始过度拟合。
然而,双向 RNN 非常适合文本数据,或任何其他类型的数据,其中顺序很重要,但使用哪种顺序并不重要。事实上,在 2016 年的一段时间里,双向 LSTM 被认为是许多自然语言处理任务的最先进技术(在 Transformer 架构兴起之前,您将在下一章中了解它)。
4.4 更进一步
与往常一样,深度学习与其说是科学,不如说是一门艺术。我们可以提供指导方针,建议在给定问题上什么可能有效或无效,但最终,每个数据集都是独一无二的;您必须根据经验评估不同的策略。目前没有任何理论可以提前准确地告诉您应该如何以最佳方式解决问题。你必须迭代。
根据我的经验,将无学习基线提高约 10% 可能是您可以使用此数据集做的最好的事情。这不是很好,但这些结果是有道理的:虽然如果您可以访问来自不同位置的广泛网格的数据,那么近期的天气是高度可预测的,但如果您只有一个位置的测量值,那么它就不是很可预测。您所在地区的天气演变取决于周围地区当前的天气模式。
有些读者一定会想用我在这里介绍的技术来尝试预测股票市场上证券的未来价格(或货币汇率等)的问题。然而,市场具有与天气模式等自然现象截然不同的统计特征。谈到市场,过去的表现并不能很好地预测未来的回报——后视镜是一种糟糕的驾驶方式。另一方面,机器学习适用于过去可以很好地预测未来的数据集,例如天气、电力消耗或商店的人流量。
永远记住,所有交易本质上都是信息套利:获得通过利用其他市场参与者缺少的数据或洞察力来获得优势。试图使用众所周知的机器学习技术和公开可用的数据来击败市场实际上是一条死胡同,因为与其他人相比,你不会拥有任何信息优势。您可能会浪费时间和资源而没有任何可展示的东西。
概括
-
正如您在第一次了解到的那样,在处理一个新问题时,最好首先为您选择的度量标准建立常识基线。如果你没有要超越的基线,你就无法判断你是否正在取得真正的进步。
-
当您拥有对排序很重要的数据时,特别是对于时间序列数据,循环网络非常适合,并且很容易胜过首先扁平化时间数据的模型。Keras 中可用的两个基本 RNN 层是
LSTM
层和GRU
层。 -
要在循环网络中使用 dropout,您应该使用时间常数 dropout 掩码和循环 dropout 掩码。这些都内置在 Keras 循环层中,因此您所要做的就是使用
recurrent_dropout
循环层的参数。 -
堆叠 RNN 比单个 RNN 层提供更多的表示能力。它们也贵得多,因此并不总是值得的。尽管它们在复杂问题(例如机器翻译)上提供了明显的收益,但它们可能并不总是与更小、更简单的问题相关。
相关文章
- 当我们休息时,我们的大脑运动皮层中重放习得的神经放电序列
- CIKM'21序列推荐:时间切片+图神经网络学习用户和item的动态表征
- Datawhale组队学习 -- Task 5: 字典、集合和序列
- 分解学习+对比学习实现更清晰的时间序列预测建模
- Genome Biology | iDNA-ABF: 基于多尺度深度学习的生物序列与功能语义模型实现DNA甲基化可解释性预测
- [Nature Communications | 论文简读] 由多序列比对训练的蛋白质语言模型学习系统发育关系
- 【综合笔试题】难度 2.5/5,状态机序列 DP 运用题
- 离散无记忆与有记忆信源的序列熵
- 【干货书】时间序列算法导论:使用Python实现机器学习和深度学习技术
- 【数字信号处理】序列傅里叶变换 ( 基本序列的傅里叶变换 | 求 a^nu(n) 的傅里叶变换 )
- 修改Oracle数据序列的简易指南(oracle修改序列)
- MySQL获取序列值的方法介绍(mysql获取序列值)
- 如何在Oracle中删除包含序列的表?(oracle删除序列的表)
- 如何在MySQL中添加序列列(mysql中加一列序列)
- 按Oracle系统要求删除序列的方法(oracle中的删除序列)
- python基础教程之序列详解