时间序列预测

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源码 下载笔记本

本教程是使用 TensorFlow 进行时间序列预测的入门指南。它构建了几种不同类型的模型,包括卷积神经网络 (CNN) 和循环神经网络 (RNN)。

本教程主要分为两个部分,包含若干小节:

  • 单时间步预测
    • 单特征预测。
    • 全特征预测。
  • 多步预测
    • 单次预测 (Single-shot):一次性完成所有预测。
    • 自回归预测 (Autoregressive):一次进行一个预测,并将输出反馈回模型。

设置

import os
import datetime

import IPython
import IPython.display
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import tensorflow as tf

mpl.rcParams['figure.figsize'] = (8, 6)
mpl.rcParams['axes.grid'] = False
2024-08-16 02:37:06.588180: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:485] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2024-08-16 02:37:06.609911: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:8454] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2024-08-16 02:37:06.616329: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1452] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered

天气数据集

本教程使用由 马克斯·普朗克生物地球化学研究所 记录的天气时间序列数据集

该数据集包含 14 个不同的特征,例如气温、大气压和湿度。这些数据从 2003 年开始,每 10 分钟收集一次。为了提高效率,我们将仅使用 2009 年至 2016 年之间收集的数据。该数据集的这一部分由 François Chollet 为他的著作《Python 深度学习》整理。

zip_path = tf.keras.utils.get_file(
    origin='https://storage.googleapis.com/tensorflow/tf-keras-datasets/jena_climate_2009_2016.csv.zip',
    fname='jena_climate_2009_2016.csv.zip',
    extract=True)
csv_path, _ = os.path.splitext(zip_path)
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/jena_climate_2009_2016.csv.zip
13568290/13568290 ━━━━━━━━━━━━━━━━━━━━ 0s 0us/step

本教程仅处理每小时预测,因此首先将数据从 10 分钟间隔采样为 1 小时间隔。

df = pd.read_csv(csv_path)
# Slice [start:stop:step], starting from index 5 take every 6th record.
df = df[5::6]

date_time = pd.to_datetime(df.pop('Date Time'), format='%d.%m.%Y %H:%M:%S')

让我们浏览一下数据。以下是前几行数据:

df.head()

以下是几个特征随时间演变的情况:

plot_cols = ['T (degC)', 'p (mbar)', 'rho (g/m**3)']
plot_features = df[plot_cols]
plot_features.index = date_time
_ = plot_features.plot(subplots=True)

plot_features = df[plot_cols][:480]
plot_features.index = date_time[:480]
_ = plot_features.plot(subplots=True)

png

png

检查与清理

接下来,查看数据集的统计信息。

df.describe().transpose()

风速

值得注意的一点是风速 (wv (m/s)) 和最大风速 (max. wv (m/s)) 列的 min 值。这个 -9999 很可能是错误的。

由于有单独的风向列,因此风速应大于零 (>=0)。将其替换为零。

wv = df['wv (m/s)']
bad_wv = wv == -9999.0
wv[bad_wv] = 0.0

max_wv = df['max. wv (m/s)']
bad_max_wv = max_wv == -9999.0
max_wv[bad_max_wv] = 0.0

# The above inplace edits are reflected in the DataFrame.
df['wv (m/s)'].min()
0.0

特征工程

在深入构建模型之前,了解数据并确保以正确的格式将数据传递给模型非常重要。

数据的最后一列 wd (deg) 给出了以度为单位的风向。角度不适合作为模型输入:360° 和 0° 应该非常接近并平滑衔接。如果风没吹,方向就不重要了。

目前,风力数据的分布如下所示:

plt.hist2d(df['wd (deg)'], df['wv (m/s)'], bins=(50, 50), vmax=400)
plt.colorbar()
plt.xlabel('Wind Direction [deg]')
plt.ylabel('Wind Velocity [m/s]')
Text(0, 0.5, 'Wind Velocity [m/s]')

png

但是,如果您将风向和风速列转换为风矢量,模型将更容易解释这些数据:

wv = df.pop('wv (m/s)')
max_wv = df.pop('max. wv (m/s)')

# Convert to radians.
wd_rad = df.pop('wd (deg)')*np.pi / 180

# Calculate the wind x and y components.
df['Wx'] = wv*np.cos(wd_rad)
df['Wy'] = wv*np.sin(wd_rad)

# Calculate the max wind x and y components.
df['max Wx'] = max_wv*np.cos(wd_rad)
df['max Wy'] = max_wv*np.sin(wd_rad)

对于模型而言,风矢量的分布更容易被正确理解。

plt.hist2d(df['Wx'], df['Wy'], bins=(50, 50), vmax=400)
plt.colorbar()
plt.xlabel('Wind X [m/s]')
plt.ylabel('Wind Y [m/s]')
ax = plt.gca()
ax.axis('tight')
(-11.305513973134667, 8.24469928549079, -8.27438540335515, 7.7338312955467785)

png

时间

同样,Date Time 列非常有用,但这种字符串形式不行。首先将其转换为秒数。

timestamp_s = date_time.map(pd.Timestamp.timestamp)

与风向类似,以秒为单位的时间不是有用的模型输入。作为气象数据,它具有明显的每日和每年周期性。处理周期性的方法有很多。

您可以使用正弦和余弦变换来提取清晰的“一天中的时间”和“一年中的时间”信号,从而获得可用的信号:

day = 24*60*60
year = (365.2425)*day

df['Day sin'] = np.sin(timestamp_s * (2 * np.pi / day))
df['Day cos'] = np.cos(timestamp_s * (2 * np.pi / day))
df['Year sin'] = np.sin(timestamp_s * (2 * np.pi / year))
df['Year cos'] = np.cos(timestamp_s * (2 * np.pi / year))
plt.plot(np.array(df['Day sin'])[:25])
plt.plot(np.array(df['Day cos'])[:25])
plt.xlabel('Time [h]')
plt.title('Time of day signal')
Text(0.5, 1.0, 'Time of day signal')

png

这使得模型能够访问最重要的频率特征。在这种情况下,您预先知道了哪些频率是重要的。

如果您没有这些信息,可以通过快速傅里叶变换提取特征来确定哪些频率是重要的。为了验证假设,这里是温度随时间变化的 tf.signal.rfft。请注意在 1/年1/天 频率附近的明显峰值。

fft = tf.signal.rfft(df['T (degC)'])
f_per_dataset = np.arange(0, len(fft))

n_samples_h = len(df['T (degC)'])
hours_per_year = 24*365.2524
years_per_dataset = n_samples_h/(hours_per_year)

f_per_year = f_per_dataset/years_per_dataset
plt.step(f_per_year, np.abs(fft))
plt.xscale('log')
plt.ylim(0, 400000)
plt.xlim([0.1, max(plt.xlim())])
plt.xticks([1, 365.2524], labels=['1/Year', '1/day'])
_ = plt.xlabel('Frequency (log scale)')
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1723775833.614540   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775833.618414   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775833.622101   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775833.625816   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775833.638786   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775833.642295   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775833.645790   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775833.649212   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775833.652657   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775833.656173   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775833.659653   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775833.663146   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.884491   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.886647   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.888756   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.890846   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.892883   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.894854   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.896838   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.898826   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.900762   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.902728   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.904708   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.906685   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.945076   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.947150   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.949185   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.951205   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.953161   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.955132   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.957121   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.959115   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.961075   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.963543   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.965909   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355
I0000 00:00:1723775834.968304   80658 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355

png

拆分数据

我们将使用 (70%, 20%, 10%) 的比例拆分训练集、验证集和测试集。请注意,数据在拆分前没有进行随机打乱。原因有两个:

  1. 它确保了将数据切分为连续样本窗口仍然是可能的。
  2. 它确保验证/测试结果更加真实,即在模型训练之后收集的数据上进行评估。
column_indices = {name: i for i, name in enumerate(df.columns)}

n = len(df)
train_df = df[0:int(n*0.7)]
val_df = df[int(n*0.7):int(n*0.9)]
test_df = df[int(n*0.9):]

num_features = df.shape[1]

归一化数据

在训练神经网络之前对特征进行缩放非常重要。归一化是一种常用的缩放方法:减去均值并除以每个特征的标准差。

均值和标准差应仅使用训练数据计算,以确保模型无法访问验证集和测试集中的数值。

也有人认为模型在训练时不应该接触训练集中的未来数值,并且这种归一化应该使用移动平均值来完成。这不是本教程的重点,验证集和测试集确保您获得(某种程度上)诚实的指标。因此,为了简洁起见,本教程使用简单的平均值。

train_mean = train_df.mean()
train_std = train_df.std()

train_df = (train_df - train_mean) / train_std
val_df = (val_df - train_mean) / train_std
test_df = (test_df - train_mean) / train_std

现在,查看特征的分布。一些特征确实有长尾,但没有像 -9999 风速值那样明显的错误。

df_std = (df - train_mean) / train_std
df_std = df_std.melt(var_name='Column', value_name='Normalized')
plt.figure(figsize=(12, 6))
ax = sns.violinplot(x='Column', y='Normalized', data=df_std)
_ = ax.set_xticklabels(df.keys(), rotation=90)
/tmpfs/tmp/ipykernel_80658/3214313372.py:5: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  _ = ax.set_xticklabels(df.keys(), rotation=90)

png

数据窗口化

本教程中的模型将基于数据中的连续样本窗口进行一系列预测。

输入窗口的主要特征是:

  • 输入窗口和标签窗口的宽度(时间步数)。
  • 它们之间的时间偏移。
  • 哪些特征被用作输入、标签或两者兼有。

本教程构建了多种模型(包括线性、DNN、CNN 和 RNN 模型),并将它们用于:

  • 单输出多输出预测。
  • 单时间步多时间步预测。

本节重点介绍如何实现数据窗口化,以便将其重用于所有这些模型。

根据任务和模型类型,您可能需要生成各种数据窗口。以下是一些示例:

  1. 例如,要在给定 24 小时历史记录的情况下预测未来 24 小时后的单个点,您可以定义如下窗口:

    One prediction 24 hours into the future.

  2. 一个在给定 6 小时历史记录的情况下预测未来 1 小时情况的模型,需要如下窗口:

    One prediction one hour into the future.

本节的其余部分定义了一个 WindowGenerator 类。该类可以:

  1. 处理上述图表中显示的索引和偏移量。
  2. 将特征窗口拆分为 (特征, 标签) 对。
  3. 绘制生成的窗口内容。
  4. 使用 tf.data.Dataset 有效地从训练、评估和测试数据中生成这些窗口的批次。

1. 索引和偏移量

首先创建 WindowGenerator 类。__init__ 方法包含了输入和标签索引的所有必要逻辑。

它还将训练、评估和测试 DataFrame 作为输入。稍后这些将被转换为 tf.data.Dataset 窗口。

class WindowGenerator():
  def __init__(self, input_width, label_width, shift,
               train_df=train_df, val_df=val_df, test_df=test_df,
               label_columns=None):
    # Store the raw data.
    self.train_df = train_df
    self.val_df = val_df
    self.test_df = test_df

    # Work out the label column indices.
    self.label_columns = label_columns
    if label_columns is not None:
      self.label_columns_indices = {name: i for i, name in
                                    enumerate(label_columns)}
    self.column_indices = {name: i for i, name in
                           enumerate(train_df.columns)}

    # Work out the window parameters.
    self.input_width = input_width
    self.label_width = label_width
    self.shift = shift

    self.total_window_size = input_width + shift

    self.input_slice = slice(0, input_width)
    self.input_indices = np.arange(self.total_window_size)[self.input_slice]

    self.label_start = self.total_window_size - self.label_width
    self.labels_slice = slice(self.label_start, None)
    self.label_indices = np.arange(self.total_window_size)[self.labels_slice]

  def __repr__(self):
    return '\n'.join([
        f'Total window size: {self.total_window_size}',
        f'Input indices: {self.input_indices}',
        f'Label indices: {self.label_indices}',
        f'Label column name(s): {self.label_columns}'])

以下是创建本节开头图表中所示的 2 个窗口的代码:

w1 = WindowGenerator(input_width=24, label_width=1, shift=24,
                     label_columns=['T (degC)'])
w1
Total window size: 48
Input indices: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
Label indices: [47]
Label column name(s): ['T (degC)']
w2 = WindowGenerator(input_width=6, label_width=1, shift=1,
                     label_columns=['T (degC)'])
w2
Total window size: 7
Input indices: [0 1 2 3 4 5]
Label indices: [6]
Label column name(s): ['T (degC)']

2. 拆分

split_window 方法会将连续输入的列表转换为输入窗口和标签窗口。

您之前定义的示例 w2 将按如下方式拆分:

The initial window is all consecutive samples, this splits it into an (inputs, labels) pairs

此图表未显示数据的 features 轴,但此 split_window 函数也处理 label_columns,因此它可以用于单输出和多输出示例。

def split_window(self, features):
  inputs = features[:, self.input_slice, :]
  labels = features[:, self.labels_slice, :]
  if self.label_columns is not None:
    labels = tf.stack(
        [labels[:, :, self.column_indices[name]] for name in self.label_columns],
        axis=-1)

  # Slicing doesn't preserve static shape information, so set the shapes
  # manually. This way the `tf.data.Datasets` are easier to inspect.
  inputs.set_shape([None, self.input_width, None])
  labels.set_shape([None, self.label_width, None])

  return inputs, labels

WindowGenerator.split_window = split_window

试一试

# Stack three slices, the length of the total window.
example_window = tf.stack([np.array(train_df[:w2.total_window_size]),
                           np.array(train_df[100:100+w2.total_window_size]),
                           np.array(train_df[200:200+w2.total_window_size])])

example_inputs, example_labels = w2.split_window(example_window)

print('All shapes are: (batch, time, features)')
print(f'Window shape: {example_window.shape}')
print(f'Inputs shape: {example_inputs.shape}')
print(f'Labels shape: {example_labels.shape}')
All shapes are: (batch, time, features)
Window shape: (3, 7, 19)
Inputs shape: (3, 6, 19)
Labels shape: (3, 1, 1)

通常,TensorFlow 中的数据被打包到数组中,其中最外层的索引跨越示例(“批次”维度)。中间的索引是“时间”或“空间”(宽、高)维度。最内层的索引是特征。

上面的代码获取了一批三个 7 时间步长的窗口,每个时间步有 19 个特征。它将它们拆分为一批 6 时间步长 19 特征的输入,以及一个 1 时间步长 1 特征的标签。标签只有一个特征,因为 WindowGenerator 初始化时带有 label_columns=['T (degC)']。最初,本教程将构建预测单个输出标签的模型。

3. 绘图

这是一个绘图方法,允许对拆分的窗口进行简单可视化:

w2.example = example_inputs, example_labels
def plot(self, model=None, plot_col='T (degC)', max_subplots=3):
  inputs, labels = self.example
  plt.figure(figsize=(12, 8))
  plot_col_index = self.column_indices[plot_col]
  max_n = min(max_subplots, len(inputs))
  for n in range(max_n):
    plt.subplot(max_n, 1, n+1)
    plt.ylabel(f'{plot_col} [normed]')
    plt.plot(self.input_indices, inputs[n, :, plot_col_index],
             label='Inputs', marker='.', zorder=-10)

    if self.label_columns:
      label_col_index = self.label_columns_indices.get(plot_col, None)
    else:
      label_col_index = plot_col_index

    if label_col_index is None:
      continue

    plt.scatter(self.label_indices, labels[n, :, label_col_index],
                edgecolors='k', label='Labels', c='#2ca02c', s=64)
    if model is not None:
      predictions = model(inputs)
      plt.scatter(self.label_indices, predictions[n, :, label_col_index],
                  marker='X', edgecolors='k', label='Predictions',
                  c='#ff7f0e', s=64)

    if n == 0:
      plt.legend()

  plt.xlabel('Time [h]')

WindowGenerator.plot = plot

此图表根据项目所指的时间对齐输入、标签和(稍后的)预测:

w2.plot()

png

您可以绘制其他列,但示例窗口 w2 配置仅包含 T (degC) 列的标签。

w2.plot(plot_col='p (mbar)')

png

4. 创建 tf.data.Dataset

最后,此 make_dataset 方法将获取一个时间序列 DataFrame,并使用 tf.keras.utils.timeseries_dataset_from_array 函数将其转换为 tf.data.Dataset 格式的 (输入窗口, 标签窗口) 对。

def make_dataset(self, data):
  data = np.array(data, dtype=np.float32)
  ds = tf.keras.utils.timeseries_dataset_from_array(
      data=data,
      targets=None,
      sequence_length=self.total_window_size,
      sequence_stride=1,
      shuffle=True,
      batch_size=32,)

  ds = ds.map(self.split_window)

  return ds

WindowGenerator.make_dataset = make_dataset

WindowGenerator 对象持有训练、验证和测试数据。

添加属性以使用前面定义的 make_dataset 方法将它们作为 tf.data.Dataset 访问。此外,添加一个标准示例批次以便于访问和绘图。

@property
def train(self):
  return self.make_dataset(self.train_df)

@property
def val(self):
  return self.make_dataset(self.val_df)

@property
def test(self):
  return self.make_dataset(self.test_df)

@property
def example(self):
  """Get and cache an example batch of `inputs, labels` for plotting."""
  result = getattr(self, '_example', None)
  if result is None:
    # No example batch was found, so get one from the `.train` dataset
    result = next(iter(self.train))
    # And cache it for next time
    self._example = result
  return result

WindowGenerator.train = train
WindowGenerator.val = val
WindowGenerator.test = test
WindowGenerator.example = example

现在,WindowGenerator 对象让您可以访问 tf.data.Dataset 对象,因此您可以轻松遍历数据。

Dataset.element_spec 属性告诉您数据集元素的结构、数据类型和形状。

# Each element is an (inputs, label) pair.
w2.train.element_spec
(TensorSpec(shape=(None, 6, 19), dtype=tf.float32, name=None),
 TensorSpec(shape=(None, 1, 1), dtype=tf.float32, name=None))

遍历 Dataset 会产生具体的批次:

for example_inputs, example_labels in w2.train.take(1):
  print(f'Inputs shape (batch, time, features): {example_inputs.shape}')
  print(f'Labels shape (batch, time, features): {example_labels.shape}')
Inputs shape (batch, time, features): (32, 6, 19)
Labels shape (batch, time, features): (32, 1, 1)

单步模型

在此类数据上您可以构建的最简单的模型是仅基于当前条件预测单个特征值——未来 1 个时间步(一小时)。

因此,首先构建模型来预测未来一小时的 T (degC) 值。

Predict the next time step

配置一个 WindowGenerator 对象来生成这些单步 (输入, 标签) 对:

single_step_window = WindowGenerator(
    input_width=1, label_width=1, shift=1,
    label_columns=['T (degC)'])
single_step_window
Total window size: 2
Input indices: [0]
Label indices: [1]
Label column name(s): ['T (degC)']

window 对象从训练集、验证集和测试集中创建 tf.data.Dataset,使您可以轻松遍历数据批次。

for example_inputs, example_labels in single_step_window.train.take(1):
  print(f'Inputs shape (batch, time, features): {example_inputs.shape}')
  print(f'Labels shape (batch, time, features): {example_labels.shape}')
Inputs shape (batch, time, features): (32, 1, 19)
Labels shape (batch, time, features): (32, 1, 1)

基准线 (Baseline)

在构建可训练模型之前,最好有一个性能基准,作为与稍后更复杂模型进行比较的依据。

第一个任务是在给定所有特征当前值的情况下,预测未来一小时的温度。当前值包括当前温度。

因此,首先从一个仅返回当前温度作为预测的模型开始,即预测“无变化”。这是一个合理的基准,因为温度变化缓慢。当然,如果您预测更远的未来,这个基准的表现会变差。

Send the input to the output

class Baseline(tf.keras.Model):
  def __init__(self, label_index=None):
    super().__init__()
    self.label_index = label_index

  def call(self, inputs):
    if self.label_index is None:
      return inputs
    result = inputs[:, :, self.label_index]
    return result[:, :, tf.newaxis]

实例化并评估此模型:

baseline = Baseline(label_index=column_indices['T (degC)'])

baseline.compile(loss=tf.keras.losses.MeanSquaredError(),
                 metrics=[tf.keras.metrics.MeanAbsoluteError()])

val_performance = {}
performance = {}
val_performance['Baseline'] = baseline.evaluate(single_step_window.val, return_dict=True)
performance['Baseline'] = baseline.evaluate(single_step_window.test, verbose=0, return_dict=True)
1/439 ━━━━━━━━━━━━━━━━━━━━ 2:27 337ms/step - loss: 0.0075 - mean_absolute_error: 0.0657
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1723775844.435380   80829 service.cc:146] XLA service 0x7eff1c004a90 initialized for platform CUDA (this does not guarantee that XLA will be used). Devices:
I0000 00:00:1723775844.435411   80829 service.cc:154]   StreamExecutor device (0): Tesla T4, Compute Capability 7.5
I0000 00:00:1723775844.435416   80829 service.cc:154]   StreamExecutor device (1): Tesla T4, Compute Capability 7.5
I0000 00:00:1723775844.435419   80829 service.cc:154]   StreamExecutor device (2): Tesla T4, Compute Capability 7.5
I0000 00:00:1723775844.435422   80829 service.cc:154]   StreamExecutor device (3): Tesla T4, Compute Capability 7.5
I0000 00:00:1723775844.614710   80829 device_compiler.h:188] Compiled cluster using XLA!  This line is logged at most once for the lifetime of the process.
439/439 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - loss: 0.0121 - mean_absolute_error: 0.0769

这打印出了一些性能指标,但这些指标并不能让您直观感受到模型的表现。

WindowGenerator 有一个绘图方法,但对于单个样本,图表不会很有趣。

因此,创建一个更宽的 WindowGenerator,每次生成 24 小时的连续输入和标签窗口。新的 wide_window 变量不会改变模型的运行方式。模型仍然基于单个输入时间步预测未来一小时。在这里,时间轴的作用类似于批次轴:每个预测都是独立进行的,时间步之间没有交互。

wide_window = WindowGenerator(
    input_width=24, label_width=24, shift=1,
    label_columns=['T (degC)'])

wide_window
Total window size: 25
Input indices: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
Label indices: [ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]
Label column name(s): ['T (degC)']

这个扩展窗口可以直接传递给同一个 baseline 模型,无需更改任何代码。这是可能的,因为输入和标签具有相同的时间步数,并且基准模型只是将输入转发到输出。

One prediction 1h into the future, ever hour.

print('Input shape:', wide_window.example[0].shape)
print('Output shape:', baseline(wide_window.example[0]).shape)
Input shape: (32, 24, 19)
Output shape: (32, 24, 1)

通过绘制基准模型的预测,请注意它仅仅是向右移动了一小时的标签。

wide_window.plot(baseline)

png

在上述三个示例的图表中,单步模型在 24 小时的时间跨度内运行。这值得解释一下:

  • 蓝色的 Inputs 线显示了每个时间步的输入温度。模型接收所有特征,此图仅显示温度。
  • 绿色的 Labels 点显示目标预测值。这些点显示在预测时间,而不是输入时间。这就是为什么标签的范围相对于输入移动了 1 个步长。
  • 橙色的 Predictions 十字是模型对每个输出时间步的预测。如果模型预测完美,预测将直接落在 Labels 上。

线性模型

您可以应用于此任务的最简单的可训练模型是在输入和输出之间插入线性变换。在这种情况下,一个时间步的输出仅取决于该步骤。

A single step prediction

没有设置 activationtf.keras.layers.Dense 层是一个线性模型。该层仅将数据的最后一个轴从 (批次, 时间, 输入) 转换为 (批次, 时间, 单元);它被独立应用于跨批次时间轴的每个项目。

linear = tf.keras.Sequential([
    tf.keras.layers.Dense(units=1)
])
print('Input shape:', single_step_window.example[0].shape)
print('Output shape:', linear(single_step_window.example[0]).shape)
Input shape: (32, 1, 19)
Output shape: (32, 1, 1)

本教程训练了许多模型,因此将训练过程封装成一个函数:

MAX_EPOCHS = 20

def compile_and_fit(model, window, patience=2):
  early_stopping = tf.keras.callbacks.EarlyStopping(monitor='val_loss',
                                                    patience=patience,
                                                    mode='min')

  model.compile(loss=tf.keras.losses.MeanSquaredError(),
                optimizer=tf.keras.optimizers.Adam(),
                metrics=[tf.keras.metrics.MeanAbsoluteError()])

  history = model.fit(window.train, epochs=MAX_EPOCHS,
                      validation_data=window.val,
                      callbacks=[early_stopping])
  return history

训练模型并评估其性能:

history = compile_and_fit(linear, single_step_window)

val_performance['Linear'] = linear.evaluate(single_step_window.val, return_dict=True)
performance['Linear'] = linear.evaluate(single_step_window.test, verbose=0, return_dict=True)
Epoch 1/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 4s 2ms/step - loss: 0.6140 - mean_absolute_error: 0.5149 - val_loss: 0.0133 - val_mean_absolute_error: 0.0868
Epoch 2/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 2s 2ms/step - loss: 0.0114 - mean_absolute_error: 0.0798 - val_loss: 0.0090 - val_mean_absolute_error: 0.0699
Epoch 3/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0093 - mean_absolute_error: 0.0704 - val_loss: 0.0087 - val_mean_absolute_error: 0.0684
Epoch 4/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0091 - mean_absolute_error: 0.0696 - val_loss: 0.0087 - val_mean_absolute_error: 0.0681
Epoch 5/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0091 - mean_absolute_error: 0.0695 - val_loss: 0.0086 - val_mean_absolute_error: 0.0684
Epoch 6/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0091 - mean_absolute_error: 0.0695 - val_loss: 0.0086 - val_mean_absolute_error: 0.0683
Epoch 7/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0091 - mean_absolute_error: 0.0694 - val_loss: 0.0086 - val_mean_absolute_error: 0.0685
Epoch 8/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0091 - mean_absolute_error: 0.0694 - val_loss: 0.0086 - val_mean_absolute_error: 0.0681
Epoch 9/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0091 - mean_absolute_error: 0.0694 - val_loss: 0.0087 - val_mean_absolute_error: 0.0687
439/439 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - loss: 0.0087 - mean_absolute_error: 0.0680

baseline 模型一样,线性模型可以在宽窗口的批次上调用。以这种方式使用时,模型对连续时间步进行了一组独立的预测。时间轴充当了另一个批次轴。每个时间步的预测之间没有交互。

A single step prediction

print('Input shape:', wide_window.example[0].shape)
print('Output shape:', linear(wide_window.example[0]).shape)
Input shape: (32, 24, 19)
Output shape: (32, 24, 1)

这是它在 wide_window 上的示例预测图,请注意在许多情况下预测明显优于直接返回输入温度,但在少数情况下则更差。

wide_window.plot(linear)

png

线性模型的一个优点是它们相对容易解释。您可以提取层的权重并可视化分配给每个输入的权重:

plt.bar(x = range(len(train_df.columns)),
        height=linear.layers[0].kernel[:,0].numpy())
axis = plt.gca()
axis.set_xticks(range(len(train_df.columns)))
_ = axis.set_xticklabels(train_df.columns, rotation=90)

png

有时模型甚至不会给输入 T (degC) 分配最大的权重。这是随机初始化的风险之一。

Dense

在应用真正对多个时间步进行操作的模型之前,值得检查一下更深、更强大的单输入步模型的性能。

这是一个类似于 linear 模型的模型,不同之处在于它在输入和输出之间堆叠了几个 Dense 层:

dense = tf.keras.Sequential([
    tf.keras.layers.Dense(units=64, activation='relu'),
    tf.keras.layers.Dense(units=64, activation='relu'),
    tf.keras.layers.Dense(units=1)
])

history = compile_and_fit(dense, single_step_window)

val_performance['Dense'] = dense.evaluate(single_step_window.val, return_dict=True)
performance['Dense'] = dense.evaluate(single_step_window.test, verbose=0, return_dict=True)
Epoch 1/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 5s 2ms/step - loss: 0.0476 - mean_absolute_error: 0.1151 - val_loss: 0.0084 - val_mean_absolute_error: 0.0669
Epoch 2/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0081 - mean_absolute_error: 0.0650 - val_loss: 0.0071 - val_mean_absolute_error: 0.0604
Epoch 3/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0075 - mean_absolute_error: 0.0619 - val_loss: 0.0071 - val_mean_absolute_error: 0.0611
Epoch 4/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0072 - mean_absolute_error: 0.0606 - val_loss: 0.0066 - val_mean_absolute_error: 0.0564
Epoch 5/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0071 - mean_absolute_error: 0.0596 - val_loss: 0.0065 - val_mean_absolute_error: 0.0561
Epoch 6/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0069 - mean_absolute_error: 0.0587 - val_loss: 0.0065 - val_mean_absolute_error: 0.0564
Epoch 7/20
1534/1534 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - loss: 0.0069 - mean_absolute_error: 0.0584 - val_loss: 0.0068 - val_mean_absolute_error: 0.0572
439/439 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - loss: 0.0068 - mean_absolute_error: 0.0570

多步 Dense

单时间步模型对输入当前的数值没有上下文。它无法看到输入特征随时间如何变化。为了解决这个问题,模型在进行预测时需要访问多个时间步。

Three time steps are used for each prediction.

baselinelineardense 模型独立处理每个时间步。在这里,模型将获取多个时间步作为输入以产生单个输出。

创建一个 WindowGenerator,它将生成三小时输入和一小时标签的批次:

请注意,Windowshift 参数是相对于两个窗口末端的。

CONV_WIDTH = 3
conv_window = WindowGenerator(
    input_width=CONV_WIDTH,
    label_width=1,
    shift=1,
    label_columns=['T (degC)'])

conv_window
Total window size: 4
Input indices: [0 1 2]
Label indices: [3]
Label column name(s): ['T (degC)']
conv_window.plot()
plt.suptitle("Given 3 hours of inputs, predict 1 hour into the future.")
Text(0.5, 0.98, 'Given 3 hours of inputs, predict 1 hour into the future.')

png

您可以通过添加 tf.keras.layers.Flatten 作为模型的第一层,在多输入步窗口上训练 dense 模型:

multi_step_dense = tf.keras.Sequential([
    # Shape: (time, features) => (time*features)
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(units=32, activation='relu'),
    tf.keras.layers.Dense(units=32, activation='relu'),
    tf.keras.layers.Dense(units=1),
    # Add back the time dimension.
    # Shape: (outputs) => (1, outputs)
    tf.keras.layers.Reshape([1, -1]),
])
print('Input shape:', conv_window.example[0].shape)
print('Output shape:', multi_step_dense(conv_window.example[0]).shape)
Input shape: (32, 3, 19)
Output shape: (32, 1, 1)
history = compile_and_fit(multi_step_dense, conv_window)

IPython.display.clear_output()
val_performance['Multi step dense'] = multi_step_dense.evaluate(conv_window.val, return_dict=True)
performance['Multi step dense'] = multi_step_dense.evaluate(conv_window.test, verbose=0, return_dict=True)
438/438 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - loss: 0.0080 - mean_absolute_error: 0.0608
conv_window.plot(multi_step_dense)

png

这种方法的主要缺点是生成的模型只能在完全相同形状的输入窗口上执行。

print('Input shape:', wide_window.example[0].shape)
try:
  print('Output shape:', multi_step_dense(wide_window.example[0]).shape)
except Exception as e:
  print(f'\n{type(e).__name__}:{e}')
Input shape: (32, 24, 19)

ValueError:Exception encountered when calling Sequential.call().

Input 0 of layer "dense_4" is incompatible with the layer: expected axis -1 of input shape to have value 57, but received input with shape (32, 456)

Arguments received by Sequential.call():
  • inputs=tf.Tensor(shape=(32, 24, 19), dtype=float32)
  • training=None
  • mask=None

下一节中的卷积模型解决了这个问题。

卷积神经网络

卷积层 (tf.keras.layers.Conv1D) 也将多个时间步作为每个预测的输入。

下面是与 multi_step_dense 相同的模型,用卷积重写:

请注意以下更改:

conv_model = tf.keras.Sequential([
    tf.keras.layers.Conv1D(filters=32,
                           kernel_size=(CONV_WIDTH,),
                           activation='relu'),
    tf.keras.layers.Dense(units=32, activation='relu'),
    tf.keras.layers.Dense(units=1),
])

在示例批次上运行它,以检查模型是否产生了预期形状的输出:

print("Conv model on `conv_window`")
print('Input shape:', conv_window.example[0].shape)
print('Output shape:', conv_model(conv_window.example[0]).shape)
Conv model on `conv_window`
Input shape: (32, 3, 19)
Output shape: (32, 1, 1)

conv_window 上训练并评估它,它应该给出与 multi_step_dense 模型相似的性能。

history = compile_and_fit(conv_model, conv_window)

IPython.display.clear_output()
val_performance['Conv'] = conv_model.evaluate(conv_window.val, return_dict=True)
performance['Conv'] = conv_model.evaluate(conv_window.test, verbose=0, return_dict=True)
438/438 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - loss: 0.0059 - mean_absolute_error: 0.0540

这个 conv_modelmulti_step_dense 模型的区别在于,conv_model 可以在任何长度的输入上运行。卷积层被应用于输入的滑动窗口。

Executing a convolutional model on a sequence

如果您在更宽的输入上运行它,它会产生更宽的输出:

print("Wide window")
print('Input shape:', wide_window.example[0].shape)
print('Labels shape:', wide_window.example[1].shape)
print('Output shape:', conv_model(wide_window.example[0]).shape)
Wide window
Input shape: (32, 24, 19)
Labels shape: (32, 24, 1)
Output shape: (32, 22, 1)
W0000 00:00:1723775965.411205   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.430143   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.431321   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.432466   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.433586   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.434754   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.435894   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.437041   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.438188   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.439356   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.440494   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.441646   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.442802   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.443962   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.459825   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced

请注意,输出比输入短。为了使训练或绘图正常工作,您需要标签和预测具有相同的长度。因此,构建一个 WindowGenerator 来生成带有几个额外输入时间步的宽窗口,以便标签和预测长度匹配。

LABEL_WIDTH = 24
INPUT_WIDTH = LABEL_WIDTH + (CONV_WIDTH - 1)
wide_conv_window = WindowGenerator(
    input_width=INPUT_WIDTH,
    label_width=LABEL_WIDTH,
    shift=1,
    label_columns=['T (degC)'])

wide_conv_window
Total window size: 27
Input indices: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 24 25]
Label indices: [ 3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26]
Label column name(s): ['T (degC)']
print("Wide conv window")
print('Input shape:', wide_conv_window.example[0].shape)
print('Labels shape:', wide_conv_window.example[1].shape)
print('Output shape:', conv_model(wide_conv_window.example[0]).shape)
Wide conv window
Input shape: (32, 26, 19)
Labels shape: (32, 24, 1)
Output shape: (32, 24, 1)
W0000 00:00:1723775965.630979   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.632244   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.633402   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.634575   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.635713   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.636852   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.637993   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.639168   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.640323   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.641462   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.642607   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.643764   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.644903   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.646054   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced
W0000 00:00:1723775965.647249   80658 gpu_timer.cc:114] Skipping the delay kernel, measurement accuracy will be reduced

现在,您可以绘制模型在更宽窗口上的预测。注意第一次预测之前的 3 个输入时间步。此处的每个预测都基于前面的 3 个时间步。

wide_conv_window.plot(conv_model)

png

循环神经网络

循环神经网络 (RNN) 是一种非常适合时间序列数据的神经网络类型。RNN 逐步处理时间序列,在时间步之间保持内部状态。

您可以在使用 RNN 进行文本生成教程和使用 Keras 的循环神经网络 (RNN) 指南中了解更多信息。

在本教程中,您将使用名为长短期记忆 (tf.keras.layers.LSTM) 的 RNN 层。

对于所有 Keras RNN 层(例如 tf.keras.layers.LSTM),一个重要的构造函数参数是 return_sequences。此设置可以以两种方式之一配置该层:

  1. 如果为 False(默认值),该层仅返回最后一个时间步的输出,给模型时间在进行单次预测之前预热其内部状态。

An LSTM warming up and making a single prediction

  1. 如果为 True,该层会为每个输入返回一个输出。这对于以下情况很有用:
    • 堆叠 RNN 层。
    • 同时在多个时间步上训练模型。

An LSTM making a prediction after every time step

lstm_model = tf.keras.models.Sequential([
    # Shape [batch, time, features] => [batch, time, lstm_units]
    tf.keras.layers.LSTM(32, return_sequences=True),
    # Shape => [batch, time, features]
    tf.keras.layers.Dense(units=1)
])

通过 return_sequences=True,模型可以一次在 24 小时的数据上进行训练。

print('Input shape:', wide_window.example[0].shape)
print('Output shape:', lstm_model(wide_window.example[0]).shape)
Input shape: (32, 24, 19)
Output shape: (32, 24, 1)
history = compile_and_fit(lstm_model, wide_window)

IPython.display.clear_output()
val_performance['LSTM'] = lstm_model.evaluate(wide_window.val, return_dict=True)
performance['LSTM'] = lstm_model.evaluate(wide_window.test, verbose=0, return_dict=True)
438/438 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - loss: 0.0057 - mean_absolute_error: 0.0524
wide_window.plot(lstm_model)

png

性能

对于此数据集,通常每个模型都比前一个模型稍微好一点。

cm = lstm_model.metrics[1]
cm.metrics
[<MeanAbsoluteError name=mean_absolute_error>]
val_performance
{'Baseline': {'loss': 0.012845644727349281,
  'mean_absolute_error': 0.07846628874540329},
 'Linear': {'loss': 0.008695926517248154,
  'mean_absolute_error': 0.06866316497325897},
 'Dense': {'loss': 0.006793886888772249,
  'mean_absolute_error': 0.05716359242796898},
 'Multi step dense': {'loss': 0.007616413291543722,
  'mean_absolute_error': 0.06059327721595764},
 'Conv': {'loss': 0.006222909316420555,
  'mean_absolute_error': 0.05451442673802376},
 'LSTM': {'loss': 0.0056776562705636024,
  'mean_absolute_error': 0.05233458802103996} }
x = np.arange(len(performance))
width = 0.3
metric_name = 'mean_absolute_error'
val_mae = [v[metric_name] for v in val_performance.values()]
test_mae = [v[metric_name] for v in performance.values()]

plt.ylabel('mean_absolute_error [T (degC), normalized]')
plt.bar(x - 0.17, val_mae, width, label='Validation')
plt.bar(x + 0.17, test_mae, width, label='Test')
plt.xticks(ticks=x, labels=performance.keys(),
           rotation=45)
_ = plt.legend()

png

for name, value in performance.items():
  print(f'{name:12s}: {value[metric_name]:0.4f}')
Baseline    : 0.0852
Linear      : 0.0663
Dense       : 0.0584
Multi step dense: 0.0633
Conv        : 0.0543
LSTM        : 0.0533

多输出模型

到目前为止,所有模型都预测了单个输出特征 T (degC),且仅针对单个时间步。

所有这些模型都可以通过仅更改输出层中的单元数并调整训练窗口以包含 labels (example_labels) 中的所有特征,从而转换为预测多个特征:

single_step_window = WindowGenerator(
    # `WindowGenerator` returns all features as labels if you
    # don't set the `label_columns` argument.
    input_width=1, label_width=1, shift=1)

wide_window = WindowGenerator(
    input_width=24, label_width=24, shift=1)

for example_inputs, example_labels in wide_window.train.take(1):
  print(f'Inputs shape (batch, time, features): {example_inputs.shape}')
  print(f'Labels shape (batch, time, features): {example_labels.shape}')
Inputs shape (batch, time, features): (32, 24, 19)
Labels shape (batch, time, features): (32, 24, 19)

请注意,标签的 features 轴现在的深度与输入相同,而不是 1

基准线 (Baseline)

此处可以使用相同的基准模型 (Baseline),但这次是重复所有特征,而不是选择特定的 label_index

baseline = Baseline()
baseline.compile(loss=tf.keras.losses.MeanSquaredError(),
                 metrics=[tf.keras.metrics.MeanAbsoluteError()])
val_performance = {}
performance = {}
val_performance['Baseline'] = baseline.evaluate(wide_window.val, return_dict=True)
performance['Baseline'] = baseline.evaluate(wide_window.test, verbose=0, return_dict=True)
438/438 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - loss: 0.0885 - mean_absolute_error: 0.1591

Dense

dense = tf.keras.Sequential([
    tf.keras.layers.Dense(units=64, activation='relu'),
    tf.keras.layers.Dense(units=64, activation='relu'),
    tf.keras.layers.Dense(units=num_features)
])
history = compile_and_fit(dense, single_step_window)

IPython.display.clear_output()
val_performance['Dense'] = dense.evaluate(single_step_window.val, return_dict=True)
performance['Dense'] = dense.evaluate(single_step_window.test, verbose=0, return_dict=True)
439/439 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - loss: 0.0684 - mean_absolute_error: 0.1312

RNN

%%time
wide_window = WindowGenerator(
    input_width=24, label_width=24, shift=1)

lstm_model = tf.keras.models.Sequential([
    # Shape [batch, time, features] => [batch, time, lstm_units]
    tf.keras.layers.LSTM(32, return_sequences=True),
    # Shape => [batch, time, features]
    tf.keras.layers.Dense(units=num_features)
])

history = compile_and_fit(lstm_model, wide_window)

IPython.display.clear_output()
val_performance['LSTM'] = lstm_model.evaluate( wide_window.val, return_dict=True)
performance['LSTM'] = lstm_model.evaluate( wide_window.test, verbose=0, return_dict=True)

print()
438/438 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - loss: 0.0612 - mean_absolute_error: 0.1196

CPU times: user 4min 33s, sys: 51.8 s, total: 5min 25s
Wall time: 1min 57s

进阶:残差连接 (Residual connections)

早期的 Baseline 模型利用了序列在不同时间步之间不会发生剧烈变化这一事实。本教程中训练的每个模型都是随机初始化的,然后必须学习到输出只是与前一个时间步的小变化。

虽然您可以通过仔细初始化来绕过这个问题,但将其内置到模型结构中更简单。

在时间序列分析中,构建模型时预测的不是下一个值,而是值在下一个时间步如何变化,这很常见。同样,深度学习中的残差网络(或 ResNet)是指每一层都添加到模型累积结果中的架构。

这就是如何利用“变化应该很小”这一知识的方法。

A model with a residual connection

本质上,这初始化模型以匹配 Baseline。对于此任务,它有助于模型更快收敛,性能略有提升。

此方法可以与本教程中讨论的任何模型结合使用。

在这里,它被应用于 LSTM 模型,注意使用 tf.initializers.zeros 以确保初始预测的变化很小,并且不会压倒残差连接。这里没有梯度的对称性破坏问题,因为 zeros 仅用于最后一层。

class ResidualWrapper(tf.keras.Model):
  def __init__(self, model):
    super().__init__()
    self.model = model

  def call(self, inputs, *args, **kwargs):
    delta = self.model(inputs, *args, **kwargs)

    # The prediction for each time step is the input
    # from the previous time step plus the delta
    # calculated by the model.
    return inputs + delta
%%time
residual_lstm = ResidualWrapper(
    tf.keras.Sequential([
    tf.keras.layers.LSTM(32, return_sequences=True),
    tf.keras.layers.Dense(
        num_features,
        # The predicted deltas should start small.
        # Therefore, initialize the output layer with zeros.
        kernel_initializer=tf.initializers.zeros())
]))

history = compile_and_fit(residual_lstm, wide_window)

IPython.display.clear_output()
val_performance['Residual LSTM'] = residual_lstm.evaluate(wide_window.val, return_dict=True)
performance['Residual LSTM'] = residual_lstm.evaluate(wide_window.test, verbose=0, return_dict=True)
print()
438/438 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - loss: 0.0622 - mean_absolute_error: 0.1184

CPU times: user 1min 40s, sys: 19.1 s, total: 1min 59s
Wall time: 43.8 s

性能

以下是这些多输出模型的整体性能:

x = np.arange(len(performance))
width = 0.3

metric_name = 'mean_absolute_error'
val_mae = [v[metric_name] for v in val_performance.values()]
test_mae = [v[metric_name] for v in performance.values()]

plt.bar(x - 0.17, val_mae, width, label='Validation')
plt.bar(x + 0.17, test_mae, width, label='Test')
plt.xticks(ticks=x, labels=performance.keys(),
           rotation=45)
plt.ylabel('MAE (average over all outputs)')
_ = plt.legend()

png

for name, value in performance.items():
  print(f'{name:15s}: {value[metric_name]:0.4f}')
Baseline       : 0.1638
Dense          : 0.1308
LSTM           : 0.1215
Residual LSTM  : 0.1193

上述性能是在所有模型输出上取平均值的。

多步模型

前几节中的单输出和多输出模型都进行了单时间步预测,即预测未来一小时的情况。

本节将探讨如何扩展这些模型以进行多时间步预测

在多步预测中,模型需要学习预测未来的一系列值。因此,与仅预测未来单个点的单步模型不同,多步模型预测的是未来值序列。

对此大致有两种方法:

  1. 单次预测,即一次预测整个时间序列。
  2. 自回归预测,即模型仅进行单步预测,其输出被反馈作为输入。

在本节中,所有模型都将预测所有输出时间步上的所有特征

对于多步模型,训练数据再次包含小时样本。然而,在这里,模型将学习在给定过去 24 小时数据的情况下,预测未来 24 小时的情况。

这是一个从数据集中生成这些切片的 Window 对象:

OUT_STEPS = 24
multi_window = WindowGenerator(input_width=24,
                               label_width=OUT_STEPS,
                               shift=OUT_STEPS)

multi_window.plot()
multi_window
Total window size: 48
Input indices: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
Label indices: [24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47]
Label column name(s): None

png

基准线

此任务的一个简单基准是将最后一个输入时间步重复所需的输出时间步数:

Repeat the last input, for each output step

class MultiStepLastBaseline(tf.keras.Model):
  def call(self, inputs):
    return tf.tile(inputs[:, -1:, :], [1, OUT_STEPS, 1])

last_baseline = MultiStepLastBaseline()
last_baseline.compile(loss=tf.keras.losses.MeanSquaredError(),
                      metrics=[tf.keras.metrics.MeanAbsoluteError()])

multi_val_performance = {}
multi_performance = {}

multi_val_performance['Last'] = last_baseline.evaluate(multi_window.val, return_dict=True)
multi_performance['Last'] = last_baseline.evaluate(multi_window.test, verbose=0, return_dict=True)
multi_window.plot(last_baseline)
437/437 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - loss: 0.6271 - mean_absolute_error: 0.4992

png

由于此任务是在给定过去 24 小时的情况下预测未来 24 小时,另一种简单的方法是重复前一天,假设明天会类似:

Repeat the previous day

class RepeatBaseline(tf.keras.Model):
  def call(self, inputs):
    return inputs

repeat_baseline = RepeatBaseline()
repeat_baseline.compile(loss=tf.keras.losses.MeanSquaredError(),
                        metrics=[tf.keras.metrics.MeanAbsoluteError()])

multi_val_performance['Repeat'] = repeat_baseline.evaluate(multi_window.val, return_dict=True)
multi_performance['Repeat'] = repeat_baseline.evaluate(multi_window.test, verbose=0, return_dict=True)
multi_window.plot(repeat_baseline)
437/437 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - loss: 0.4241 - mean_absolute_error: 0.3948

png

单次预测模型 (Single-shot)

解决此问题的一种高级方法是使用“单次”模型,即模型在单一步骤中完成整个序列预测。

这可以有效地实现为具有 OUT_STEPS*features 输出单元的 tf.keras.layers.Dense。模型只需要将该输出重塑为所需的 (OUTPUT_STEPS, features) 即可。

线性

基于最后一个输入时间步的简单线性模型比任何基准都要好,但功能不足。模型需要通过线性投影从单个输入时间步预测 OUTPUT_STEPS 时间步。它只能捕获行为的一个低维切片,可能主要基于一天中的时间和一年中的时间。

Predict all timesteps from the last time-step

multi_linear_model = tf.keras.Sequential([
    # Take the last time-step.
    # Shape [batch, time, features] => [batch, 1, features]
    tf.keras.layers.Lambda(lambda x: x[:, -1:, :]),
    # Shape => [batch, 1, out_steps*features]
    tf.keras.layers.Dense(OUT_STEPS*num_features,
                          kernel_initializer=tf.initializers.zeros()),
    # Shape => [batch, out_steps, features]
    tf.keras.layers.Reshape([OUT_STEPS, num_features])
])

history = compile_and_fit(multi_linear_model, multi_window)

IPython.display.clear_output()
multi_val_performance['Linear'] = multi_linear_model.evaluate(multi_window.val, return_dict=True)
multi_performance['Linear'] = multi_linear_model.evaluate(multi_window.test, verbose=0, return_dict=True)
multi_window.plot(multi_linear_model)
437/437 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - loss: 0.2547 - mean_absolute_error: 0.3051

png

Dense

在输入和输出之间添加 tf.keras.layers.Dense 给线性模型增加了更多的能力,但仍然仅基于单个输入时间步。

multi_dense_model = tf.keras.Sequential([
    # Take the last time step.
    # Shape [batch, time, features] => [batch, 1, features]
    tf.keras.layers.Lambda(lambda x: x[:, -1:, :]),
    # Shape => [batch, 1, dense_units]
    tf.keras.layers.Dense(512, activation='relu'),
    # Shape => [batch, out_steps*features]
    tf.keras.layers.Dense(OUT_STEPS*num_features,
                          kernel_initializer=tf.initializers.zeros()),
    # Shape => [batch, out_steps, features]
    tf.keras.layers.Reshape([OUT_STEPS, num_features])
])

history = compile_and_fit(multi_dense_model, multi_window)

IPython.display.clear_output()
multi_val_performance['Dense'] = multi_dense_model.evaluate(multi_window.val, return_dict=True)
multi_performance['Dense'] = multi_dense_model.evaluate(multi_window.test, verbose=0, return_dict=True)
multi_window.plot(multi_dense_model)
437/437 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - loss: 0.2161 - mean_absolute_error: 0.2798

png

CNN

卷积模型基于固定宽度的历史记录进行预测,这可能比 Dense 模型带来更好的性能,因为它可以观察到事物随时间的变化。

A convolutional model sees how things change over time

CONV_WIDTH = 3
multi_conv_model = tf.keras.Sequential([
    # Shape [batch, time, features] => [batch, CONV_WIDTH, features]
    tf.keras.layers.Lambda(lambda x: x[:, -CONV_WIDTH:, :]),
    # Shape => [batch, 1, conv_units]
    tf.keras.layers.Conv1D(256, activation='relu', kernel_size=(CONV_WIDTH)),
    # Shape => [batch, 1,  out_steps*features]
    tf.keras.layers.Dense(OUT_STEPS*num_features,
                          kernel_initializer=tf.initializers.zeros()),
    # Shape => [batch, out_steps, features]
    tf.keras.layers.Reshape([OUT_STEPS, num_features])
])

history = compile_and_fit(multi_conv_model, multi_window)

IPython.display.clear_output()

multi_val_performance['Conv'] = multi_conv_model.evaluate(multi_window.val, return_dict=True)
multi_performance['Conv'] = multi_conv_model.evaluate(multi_window.test, verbose=0, return_dict=True)
multi_window.plot(multi_conv_model)
437/437 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - loss: 0.2133 - mean_absolute_error: 0.2794

png

RNN

循环模型可以学习使用长的输入历史记录(如果它与模型正在进行的预测相关)。在这里,模型将在做出未来 24 小时的单一预测之前,累积 24 小时的内部状态。

在这种单次格式中,LSTM 只需要在最后一个时间步产生输出,因此在 tf.keras.layers.LSTM 中设置 return_sequences=False

The LSTM accumulates state over the input window, and makes a single prediction for the next 24 hours

multi_lstm_model = tf.keras.Sequential([
    # Shape [batch, time, features] => [batch, lstm_units].
    # Adding more `lstm_units` just overfits more quickly.
    tf.keras.layers.LSTM(32, return_sequences=False),
    # Shape => [batch, out_steps*features].
    tf.keras.layers.Dense(OUT_STEPS*num_features,
                          kernel_initializer=tf.initializers.zeros()),
    # Shape => [batch, out_steps, features].
    tf.keras.layers.Reshape([OUT_STEPS, num_features])
])

history = compile_and_fit(multi_lstm_model, multi_window)

IPython.display.clear_output()

multi_val_performance['LSTM'] = multi_lstm_model.evaluate(multi_window.val, return_dict=True)
multi_performance['LSTM'] = multi_lstm_model.evaluate(multi_window.test, verbose=0, return_dict=True)
multi_window.plot(multi_lstm_model)
437/437 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - loss: 0.2160 - mean_absolute_error: 0.2858

png

进阶:自回归模型

上述模型都可以在单一步骤中预测整个输出序列。

在某些情况下,模型将此预测分解为单独的时间步可能很有帮助。然后,每个模型的输出可以在每一步被反馈回其自身,并且预测可以以之前的一个为条件,就像经典的使用循环神经网络生成序列一样。

这种风格模型的一个明显优势是它可以设置为产生不同长度的输出。

您可以采用本教程前半部分训练的任何单步多输出模型,并在自回归反馈循环中运行,但在这里我们将重点构建明确为此目的而训练的模型。

Feedback a model's output to its input

RNN

本教程仅构建了一个自回归 RNN 模型,但这种模式可以应用于任何被设计为输出单个时间步的模型。

该模型将具有与之前单步 LSTM 模型相同的基本形式:一个 tf.keras.layers.LSTM 层,后面跟着一个 tf.keras.layers.Dense 层,它将 LSTM 层的输出转换为模型预测。

tf.keras.layers.LSTM 是包裹在更高级别 tf.keras.layers.RNN 中的 tf.keras.layers.LSTMCell,它为您管理状态和序列结果(查看使用 Keras 的循环神经网络 (RNN) 指南了解详情)。

在这种情况下,模型必须手动管理每一步的输入,因此它直接使用 tf.keras.layers.LSTMCell 进行较低级别的单时间步接口。

class FeedBack(tf.keras.Model):
  def __init__(self, units, out_steps):
    super().__init__()
    self.out_steps = out_steps
    self.units = units
    self.lstm_cell = tf.keras.layers.LSTMCell(units)
    # Also wrap the LSTMCell in an RNN to simplify the `warmup` method.
    self.lstm_rnn = tf.keras.layers.RNN(self.lstm_cell, return_state=True)
    self.dense = tf.keras.layers.Dense(num_features)
feedback_model = FeedBack(units=32, out_steps=OUT_STEPS)

此模型需要的第一个方法是 warmup 方法,用于基于输入初始化其内部状态。训练后,此状态将捕获输入历史的相关部分。这等同于之前单步 LSTM 模型。

def warmup(self, inputs):
  # inputs.shape => (batch, time, features)
  # x.shape => (batch, lstm_units)
  x, *state = self.lstm_rnn(inputs)

  # predictions.shape => (batch, features)
  prediction = self.dense(x)
  return prediction, state

FeedBack.warmup = warmup

此方法返回单个时间步的预测和 LSTM 的内部状态。

prediction, state = feedback_model.warmup(multi_window.example[0])
prediction.shape
TensorShape([32, 19])

利用 RNN 的状态和初始预测,您现在可以继续迭代模型,将每一步的预测反馈作为输入。

收集输出预测的最简单方法是在循环后使用 Python 列表和 tf.stack

def call(self, inputs, training=None):
  # Use a TensorArray to capture dynamically unrolled outputs.
  predictions = []
  # Initialize the LSTM state.
  prediction, state = self.warmup(inputs)

  # Insert the first prediction.
  predictions.append(prediction)

  # Run the rest of the prediction steps.
  for n in range(1, self.out_steps):
    # Use the last prediction as input.
    x = prediction
    # Execute one lstm step.
    x, state = self.lstm_cell(x, states=state,
                              training=training)
    # Convert the lstm output to a prediction.
    prediction = self.dense(x)
    # Add the prediction to the output.
    predictions.append(prediction)

  # predictions.shape => (time, batch, features)
  predictions = tf.stack(predictions)
  # predictions.shape => (batch, time, features)
  predictions = tf.transpose(predictions, [1, 0, 2])
  return predictions

FeedBack.call = call

在示例输入上测试运行此模型:

print('Output shape (batch, time, features): ', feedback_model(multi_window.example[0]).shape)
Output shape (batch, time, features):  (32, 24, 19)

现在,训练模型:

history = compile_and_fit(feedback_model, multi_window)

IPython.display.clear_output()

multi_val_performance['AR LSTM'] = feedback_model.evaluate(multi_window.val, return_dict=True)
multi_performance['AR LSTM'] = feedback_model.evaluate(multi_window.test, verbose=0, return_dict=True)
multi_window.plot(feedback_model)
437/437 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.2244 - mean_absolute_error: 0.2973

png

性能

对于这个问题,模型复杂性带来的收益明显存在边际递减效应。

x = np.arange(len(multi_performance))
width = 0.3

metric_name = 'mean_absolute_error'
val_mae = [v[metric_name] for v in multi_val_performance.values()]
test_mae = [v[metric_name] for v in multi_performance.values()]

plt.bar(x - 0.17, val_mae, width, label='Validation')
plt.bar(x + 0.17, test_mae, width, label='Test')
plt.xticks(ticks=x, labels=multi_performance.keys(),
           rotation=45)
plt.ylabel(f'MAE (average over all times and outputs)')
_ = plt.legend()

png

本教程前半部分中多输出模型的指标显示了所有输出特征的平均性能。这些表现相似,但也跨越了输出时间步进行了平均。

for name, value in multi_performance.items():
  print(f'{name:8s}: {value[metric_name]:0.4f}')
Last    : 0.5157
Repeat  : 0.3774
Linear  : 0.2980
Dense   : 0.2765
Conv    : 0.2732
LSTM    : 0.2767
AR LSTM : 0.2910

从 Dense 模型到卷积和循环模型所取得的收益只有几个百分点(如果有的话),而自回归模型的表现明显更差。因此,这些更复杂的方法在这个问题上可能并不值得,但如果不尝试就无法知道,而且这些模型可能对的问题有所帮助。

后续步骤

本教程是对使用 TensorFlow 进行时间序列预测的快速介绍。

要了解更多信息,请参考:

此外,请记住,您可以在 TensorFlow 中实现任何经典时间序列模型——本教程仅关注 TensorFlow 的内置功能。