Эта страница была переведа с помощью Cloud Translation API.
Switch to English

Прогнозирование временных рядов

Посмотреть на TensorFlow.org Запускаем в Google Colab Посмотреть исходный код на GitHub Скачать блокнот

Это руководство представляет собой введение в прогнозирование временных рядов с помощью TensorFlow. Он строит модели нескольких разных стилей, включая сверточные и рекуррентные нейронные сети (CNN и RNN).

Он состоит из двух основных частей с подразделами:

  • Прогноз для одного временного шага:
    • Единственная особенность.
    • Все особенности.
  • Прогнозируйте несколько шагов:
    • Однократный: делайте все прогнозы сразу.
    • Авторегрессия: делайте один прогноз за раз и возвращайте результат обратно в модель.

Настроить

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

Набор данных о погоде

В этом руководстве используется набор данных временных рядов погоды, записанный Институтом биогеохимии Макса Планка .

Этот набор данных содержит 14 различных характеристик, таких как температура воздуха, атмосферное давление и влажность. Они собирались каждые 10 минут, начиная с 2003 года. Для эффективности вы будете использовать только данные, собранные в период с 2009 по 2016 год. Этот раздел набора данных был подготовлен Франсуа Шоле для его книги « Глубокое обучение с помощью 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
13574144/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()

Скорость ветра

Следует отметить min значение скорости ветра wv (m/s) и max. wv (m/s) столбцов. -9999 это -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(datetime.datetime.timestamp)

Как и направление ветра, время в секундах не является полезным вводом модели. Данные о погоде имеют четкую ежедневную и годовую периодичность. Есть много способов справиться с периодичностью.

Простой способ преобразовать его в полезный сигнал - использовать sin и cos для преобразования времени в сигналы «Время дня» и «Время года»:

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

Это дает модели доступ к наиболее важным частотным характеристикам. В этом случае вы заранее знали, какие частоты важны.

Если вы не знали, вы можете определить, какие частоты важны, используя fft . Чтобы проверить наши предположения, вот tf.signal.rfft температуры во времени. Обратите внимание на очевидные пики на частотах около 1/year и 1/day :

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)')

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 скорости ветра -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)

PNG

Окно данных

Модели в этом руководстве будут делать набор прогнозов на основе окна последовательных выборок из данных.

Основные особенности окон ввода:

  • Ширина (количество временных шагов) окон ввода и меток
  • Временной сдвиг между ними.
  • Какие функции используются в качестве входных данных, меток или и того, и другого.

В этом руководстве создаются различные модели (в том числе линейные модели, модели DNN, CNN и RNN) и используются их для обоих:

  • Прогнозы с одним и несколькими выходами .
  • Прогнозы с одним и несколькими шагами по времени .

В этом разделе основное внимание уделяется реализации окна данных, чтобы его можно было повторно использовать для всех этих моделей.

В зависимости от задачи и типа модели вы можете захотеть создать различные окна данных. Вот некоторые примеры:

  1. Например, чтобы сделать одно предсказание на 24 часа в будущем, учитывая 24 часа истории, вы можете определить такое окно:

    Один прогноз на 24 часа в будущее.

  2. Для модели, которая делает прогноз на 1 час вперед, учитывая 6 часов истории, потребуется такое окно:

    Одно предсказание на 1 час в будущее.

Остальная часть этого раздела определяет класс WindowGenerator . Этот класс может:

  1. Обрабатывайте индексы и смещения, как показано на схемах выше.
  2. Разделите окна функций на пары (features, labels) .
  3. Постройте содержимое получившихся окон.
  4. Эффективно генерируйте пакеты этих окон из данных обучения, оценки и тестирования, используя tf.data.Dataset s.

1. Индексы и смещения

Начнем с создания класса WindowGenerator . Метод __init__ включает всю необходимую логику для индексов ввода и меток.

Он также принимает в качестве входных данных фреймы данных train, eval и test. tf.data.Dataset они будут преобразованы в 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}'])

Вот код для создания двух окон, показанных на схемах в начале этого раздела:

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 , приведенный выше, будет разделен следующим образом:

Начальное окно - это все последовательные образцы, это разбивает его на пары (входы, метки)

Эта диаграмма не показывает 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 упаковываются в массивы, где внешний индекс находится в примерах («пакетное» измерение). Средние индексы - это измерения «времени» или «пространства» (ширина, высота). Внутренние показатели - это особенности.

В приведенном выше коде использовался пакет из 3 окон с 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(3, 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 s

Наконец, этот метод make_dataset принимает DataFrame временного ряда и преобразует его в tf.data.Dataset из (input_window, label_window) с помощью функции preprocessing.timeseries_dataset_from_array .

def make_dataset(self, data):
  data = np.array(data, dtype=np.float32)
  ds = tf.keras.preprocessing.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 содержит данные обучения, проверки и тестирования. Добавьте свойства для доступа к ним как tf.data.Datasets используя вышеуказанный метод make_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 сообщает вам структуру, dtypes и формы элементов набора данных.

# 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 (1 час) в будущем на основе только текущих условий.

Итак, начните с построения моделей для прогнозирования значения T (degC) 1 час в будущем.

Предсказать следующий временной шаг

Настройте объект WindowGenerator для создания этих одношаговых пар (input, label) :

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.Datasets из tf.data.Datasets для обучения, проверки и тестирования, что позволяет легко перебирать пакеты данных.

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)

Исходный уровень

Перед построением обучаемой модели было бы хорошо иметь базовый уровень производительности в качестве точки для сравнения с более поздними более сложными моделями.

Эта первая задача - предсказать температуру на 1 час в будущем с учетом текущего значения всех функций. Текущие значения включают текущую температуру.

Итак, начните с модели, которая просто возвращает текущую температуру в качестве прогноза с прогнозом «Без изменений». Это разумный исходный уровень, поскольку температура изменяется медленно. Конечно, этот базовый уровень будет работать хуже, если вы будете делать дальнейшие прогнозы в будущем.

Отправить вход на выход

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.losses.MeanSquaredError(),
                 metrics=[tf.metrics.MeanAbsoluteError()])

val_performance = {}
performance = {}
val_performance['Baseline'] = baseline.evaluate(single_step_window.val)
performance['Baseline'] = baseline.evaluate(single_step_window.test, verbose=0)
439/439 [==============================] - 1s 2ms/step - loss: 0.0128 - mean_absolute_error: 0.0785

Это напечатало некоторые показатели производительности, но они не дают вам представления о том, насколько хорошо работает модель.

WindowGenerator имеет метод построения графиков, но графики не будут очень интересными только с одним образцом. Итак, создайте более широкий WindowGenerator который генерирует окна 24 WindowGenerator последовательных входов и меток за раз.

wide_window не меняет способ работы модели. Модель по-прежнему делает прогнозы на 1 час в будущее на основе одного входного временного шага. Здесь ось time действует как ось batch : каждый прогноз делается независимо, без взаимодействия между временными шагами.

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 модель без каких-либо изменений кода. Это возможно, потому что входы и метки имеют одинаковое количество временных шагов, а базовая линия просто перенаправляет вход на выход:

Одно предсказание на 1 час в будущее, каждый час.

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)

Построив прогнозы базовой модели, вы увидите, что это просто метки, сдвинутые вправо на 1 час.

wide_window.plot(baseline)

PNG

На приведенных выше графиках трех примеров одношаговая модель запускается в течение 24 часов. Это заслуживает некоторого объяснения:

  • Синяя линия «Входы» показывает температуру на входе на каждом временном шаге. Модель обладает всеми функциями, этот график показывает только температуру.
  • Зеленые точки «Ярлыки» показывают целевое значение прогноза. Эти точки отображаются во время прогнозирования, а не во время ввода. Поэтому диапазон меток сдвинут на 1 шаг относительно входов.
  • Оранжевые крестики «Прогнозы» - это прогнозы модели для каждого выходного временного шага. Если бы модель предсказывала идеально, предсказания попадали бы прямо на «метки».

Линейная модель

Самая простая обучаемая модель, которую вы можете применить к этой задаче, - это вставить линейное преобразование между входом и выходом. В этом случае результат временного шага зависит только от этого шага:

Одношаговое предсказание

layers.Dense без набора activation - это линейная модель. Слой преобразует только последнюю ось данных из (batch, time, inputs) в (batch, time, units) , он применяется независимо к каждому элементу по осям batch и time .

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.losses.MeanSquaredError(),
                optimizer=tf.optimizers.Adam(),
                metrics=[tf.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)
performance['Linear'] = linear.evaluate(single_step_window.test, verbose=0)
Epoch 1/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.2864 - mean_absolute_error: 0.2848 - val_loss: 0.0163 - val_mean_absolute_error: 0.0975
Epoch 2/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0121 - mean_absolute_error: 0.0817 - val_loss: 0.0103 - val_mean_absolute_error: 0.0752
Epoch 3/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0103 - mean_absolute_error: 0.0749 - val_loss: 0.0098 - val_mean_absolute_error: 0.0738
Epoch 4/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0099 - mean_absolute_error: 0.0733 - val_loss: 0.0095 - val_mean_absolute_error: 0.0731
Epoch 5/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0096 - mean_absolute_error: 0.0721 - val_loss: 0.0092 - val_mean_absolute_error: 0.0719
Epoch 6/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0095 - mean_absolute_error: 0.0715 - val_loss: 0.0091 - val_mean_absolute_error: 0.0716
Epoch 7/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0094 - mean_absolute_error: 0.0710 - val_loss: 0.0091 - val_mean_absolute_error: 0.0716
Epoch 8/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0093 - mean_absolute_error: 0.0707 - val_loss: 0.0090 - val_mean_absolute_error: 0.0706
Epoch 9/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0092 - mean_absolute_error: 0.0704 - val_loss: 0.0090 - val_mean_absolute_error: 0.0712
Epoch 10/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0092 - mean_absolute_error: 0.0703 - val_loss: 0.0091 - val_mean_absolute_error: 0.0715
439/439 [==============================] - 1s 2ms/step - loss: 0.0091 - mean_absolute_error: 0.0715

Как и baseline модель, линейная модель может вызываться для пакетов широких окон. При таком использовании модель делает набор независимых прогнозов на последовательных временных шагах. Ось time действует как другая ось batch . Между предсказаниями на каждом временном шаге нет взаимодействий.

Одношаговое предсказание

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 , обратите внимание, что во многих случаях прогноз явно лучше, чем просто возврат входной температуры, но в некоторых случаях он хуже:

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) входное значение T (degC) . Это один из рисков случайной инициализации.

Плотный

Прежде чем применять модели, которые фактически работают с несколькими временными шагами, стоит проверить производительность более глубоких и мощных моделей с одним входом.

Вот модель, похожая на 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)
performance['Dense'] = dense.evaluate(single_step_window.test, verbose=0)
Epoch 1/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0159 - mean_absolute_error: 0.0814 - val_loss: 0.0086 - val_mean_absolute_error: 0.0693
Epoch 2/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0079 - mean_absolute_error: 0.0645 - val_loss: 0.0076 - val_mean_absolute_error: 0.0629
Epoch 3/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0074 - mean_absolute_error: 0.0622 - val_loss: 0.0085 - val_mean_absolute_error: 0.0666
Epoch 4/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0072 - mean_absolute_error: 0.0608 - val_loss: 0.0071 - val_mean_absolute_error: 0.0593
Epoch 5/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0070 - mean_absolute_error: 0.0595 - val_loss: 0.0067 - val_mean_absolute_error: 0.0579
Epoch 6/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0068 - mean_absolute_error: 0.0588 - val_loss: 0.0072 - val_mean_absolute_error: 0.0594
Epoch 7/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0068 - mean_absolute_error: 0.0583 - val_loss: 0.0066 - val_mean_absolute_error: 0.0564
Epoch 8/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0066 - mean_absolute_error: 0.0576 - val_loss: 0.0078 - val_mean_absolute_error: 0.0637
Epoch 9/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0066 - mean_absolute_error: 0.0576 - val_loss: 0.0066 - val_mean_absolute_error: 0.0564
Epoch 10/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0065 - mean_absolute_error: 0.0569 - val_loss: 0.0062 - val_mean_absolute_error: 0.0551
Epoch 11/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0065 - mean_absolute_error: 0.0571 - val_loss: 0.0070 - val_mean_absolute_error: 0.0596
Epoch 12/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0064 - mean_absolute_error: 0.0565 - val_loss: 0.0063 - val_mean_absolute_error: 0.0551
439/439 [==============================] - 1s 3ms/step - loss: 0.0063 - mean_absolute_error: 0.0551

Многоступенчатая плотная

Модель с одним временным шагом не имеет контекста для текущих значений входных данных. Он не может видеть, как входные функции меняются с течением времени. Для решения этой проблемы модели требуется доступ к нескольким временным шагам при прогнозировании:

Для каждого прогноза используются три временных шага.

baseline , linear и dense модели обрабатывались каждый временной шаг независимо. Здесь модель примет несколько временных шагов в качестве входных данных для получения единственного выхода.

Создайте WindowGenerator который будет создавать пакеты по 3 часа ввода и 1 час меток:

Обратите внимание, что параметр shift Window относится к концу двух окон.

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.title("Given 3h as input, predict 1h into the future.")
Text(0.5, 1.0, 'Given 3h as input, predict 1h into the future.')

PNG

Вы можете обучить dense модель в окне с несколькими layers.Flatten ввода, добавив layers.Flatten в качестве первого слоя модели:

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)
performance['Multi step dense'] = multi_step_dense.evaluate(conv_window.test, verbose=0)
438/438 [==============================] - 1s 2ms/step - loss: 0.0078 - mean_absolute_error: 0.0637

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)

InvalidArgumentError:Matrix size-incompatible: In[0]: [32,456], In[1]: [57,32] [Op:MatMul]

Сверточные модели в следующем разделе решают эту проблему.

Сверточная нейронная сеть

Сверточный слой ( 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 и оцените его в 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)
performance['Conv'] = conv_model.evaluate(conv_window.test, verbose=0)
438/438 [==============================] - 1s 3ms/step - loss: 0.0063 - mean_absolute_error: 0.0556

Разница между этой conv_model и моделью multi_step_dense заключается в том, что модель conv_model может запускаться на входах любой длины. Сверточный слой применяется к скользящему окну входов:

Выполнение сверточной модели на последовательности

Если вы запустите его на более широком входе, он даст более широкий вывод:

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)

Обратите внимание, что вывод короче ввода. Чтобы обучение или построение графиков работали, вам нужно, чтобы метки и прогноз имели одинаковую длину. Итак, создайте 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)

Теперь вы можете построить прогнозы модели в более широком окне. Обратите внимание на 3 временных шага ввода до первого прогноза. Каждый прогноз здесь основан на трех предыдущих временных шагах:

wide_conv_window.plot(conv_model)

PNG

Рекуррентная нейронная сеть

Рекуррентная нейронная сеть (RNN) - это тип нейронной сети, хорошо подходящий для данных временных рядов. RNN обрабатывают временной ряд шаг за шагом, поддерживая внутреннее состояние от временного шага к временному шагу.

Для получения дополнительной информации прочтите руководство по созданию текста или руководство по RNN .

В этом руководстве вы будете использовать слой RNN под названием Long Short Term Memory ( LSTM ).

Важным аргументом конструктора для всех слоев keras RNN является аргумент return_sequences . Этот параметр позволяет настроить слой одним из двух способов.

  1. Если значение по умолчанию равно False , слой возвращает только результат последнего временного шага, давая модели время, чтобы прогреть свое внутреннее состояние перед выполнением единственного прогноза:

Разминка lstm и создание единого прогноза

  1. Если True слой возвращает вывод для каждого ввода. Это полезно для:
    • Укладка слоев RNN.
    • Обучение модели на нескольких временных шагах одновременно.

Lstm делает прогноз после каждого временного шага

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 return_sequences=True данных за раз.

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)
performance['LSTM'] = lstm_model.evaluate(wide_window.test, verbose=0)
438/438 [==============================] - 1s 3ms/step - loss: 0.0057 - mean_absolute_error: 0.0523

wide_window.plot(lstm_model)

PNG

Спектакль

С этим набором данных каждая из моделей обычно работает немного лучше, чем предыдущая.

x = np.arange(len(performance))
width = 0.3
metric_name = 'mean_absolute_error'
metric_index = lstm_model.metrics_names.index('mean_absolute_error')
val_mae = [v[metric_index] for v in val_performance.values()]
test_mae = [v[metric_index] 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[1]:0.4f}')
Baseline    : 0.0852
Linear      : 0.0694
Dense       : 0.0566
Multi step dense: 0.0667
Conv        : 0.0572
LSTM        : 0.0528

Модели с несколькими выходами

До сих пор все модели предсказывали одну выходную характеристику T (degC) для одного временного шага.

Все эти модели можно преобразовать для прогнозирования нескольких функций, просто изменив количество единиц в выходном слое и настроив окна обучения, чтобы включить все функции в 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.

Исходный уровень

Здесь можно использовать ту же базовую модель, но на этот раз с повторением всех функций вместо выбора конкретного label_index .

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

Плотный

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)
performance['Dense'] = dense.evaluate(single_step_window.test, verbose=0)
439/439 [==============================] - 1s 3ms/step - loss: 0.0706 - mean_absolute_error: 0.1362

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)
performance['LSTM'] = lstm_model.evaluate( wide_window.test, verbose=0)

print()
438/438 [==============================] - 1s 3ms/step - loss: 0.0613 - mean_absolute_error: 0.1192

CPU times: user 6min 18s, sys: 1min 36s, total: 7min 55s
Wall time: 2min 53s

Дополнительно: Остаточные соединения

Baseline модель ранее использовала тот факт, что последовательность не меняется кардинально от временного шага к временному шагу. Каждая модель, обученная в этом руководстве, была инициализирована случайным образом, а затем должна была узнать, что результат - небольшое изменение по сравнению с предыдущим временным шагом.

Хотя вы можете обойти эту проблему с помощью тщательной инициализации, проще встроить ее в структуру модели.

В анализе временных рядов часто строят модели, которые вместо предсказания следующего значения предсказывают, как значение изменится на следующем временном шаге. Аналогичным образом, «остаточные сети» или «ResNets» в глубоком обучении относятся к архитектурам, в которых каждый уровень добавляет к результату накопления модели.

Вот как вы пользуетесь знанием того, что изменение должно быть небольшим.

Модель с остаточной связью

По сути, это инициализирует модель в соответствии с 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 timestep 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
        # So 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)
performance['Residual LSTM'] = residual_lstm.evaluate(wide_window.test, verbose=0)
print()
438/438 [==============================] - 1s 3ms/step - loss: 0.0619 - mean_absolute_error: 0.1181

CPU times: user 2min 2s, sys: 31.2 s, total: 2min 33s
Wall time: 56.9 s

Спектакль

Вот общая производительность этих моделей с несколькими выходами.

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

metric_name = 'mean_absolute_error'
metric_index = lstm_model.metrics_names.index('mean_absolute_error')
val_mae = [v[metric_index] for v in val_performance.values()]
test_mae = [v[metric_index] 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[1]:0.4f}')
Baseline       : 0.1638
Dense          : 0.1367
LSTM           : 0.1208
Residual LSTM  : 0.1197

Вышеуказанные характеристики усреднены по всем выходным данным модели.

Многоступенчатые модели

И модели с одним выходом, и с несколькими выходами в предыдущих разделах делали прогнозы с одним временным шагом , на 1 час вперед.

В этом разделе рассматривается, как расширить эти модели для прогнозирования нескольких временных шагов .

При многоэтапном прогнозировании модель должна научиться прогнозировать диапазон будущих значений. Таким образом, в отличие от одношаговой модели, где прогнозируется только одна будущая точка, многоступенчатая модель прогнозирует последовательность будущих значений.

Есть два грубых подхода к этому:

  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

Исходные данные

Простой базовый план для этой задачи - повторить последний временной шаг ввода для необходимого количества временных шагов вывода:

Повторите последний ввод для каждого шага вывода

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.losses.MeanSquaredError(),
                      metrics=[tf.metrics.MeanAbsoluteError()])

multi_val_performance = {}
multi_performance = {}

multi_val_performance['Last'] = last_baseline.evaluate(multi_window.val)
multi_performance['Last'] = last_baseline.evaluate(multi_window.test, verbose=0)
multi_window.plot(last_baseline)
437/437 [==============================] - 1s 2ms/step - loss: 0.6285 - mean_absolute_error: 0.5007

PNG

Поскольку эта задача - предсказать 24 часа с учетом 24 часов, другой простой подход - повторить предыдущий день, предполагая, что завтра будет аналогичным:

Повторить предыдущий день

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

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

multi_val_performance['Repeat'] = repeat_baseline.evaluate(multi_window.val)
multi_performance['Repeat'] = repeat_baseline.evaluate(multi_window.test, verbose=0)
multi_window.plot(repeat_baseline)
437/437 [==============================] - 1s 2ms/step - loss: 0.4270 - mean_absolute_error: 0.3959

PNG

Одноразовые модели

Одним из высокоуровневых подходов к этой проблеме является использование «однократной» модели, в которой модель выполняет прогнозирование всей последовательности за один шаг.

Это может быть эффективно реализовано в виде layers.Dense с OUT_STEPS*features единицы вывода. Модель просто должна изменить форму этого вывода на требуемые (OUTPUT_STEPS, features) .

Линейный

Простая линейная модель, основанная на последнем временном шаге ввода, работает лучше, чем любая базовая линия, но не обладает достаточной мощностью. Модель должна прогнозировать временные шаги OUTPUT_STEPS из одного временного шага ввода с линейной проекцией. Он может фиксировать только низкоразмерный срез поведения, вероятно, основанный в основном на времени суток и времени года.

Прогнозировать все временные шаги с последнего временного шага

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)
multi_performance['Linear'] = multi_linear_model.evaluate(multi_window.test, verbose=0)
multi_window.plot(multi_linear_model)
437/437 [==============================] - 1s 2ms/step - loss: 0.2556 - mean_absolute_error: 0.3050

PNG

Плотный

Добавление 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)
multi_performance['Dense'] = multi_dense_model.evaluate(multi_window.test, verbose=0)
multi_window.plot(multi_dense_model)
437/437 [==============================] - 1s 3ms/step - loss: 0.2192 - mean_absolute_error: 0.2807

PNG

CNN

Сверточная модель делает прогнозы на основе истории фиксированной ширины, что может привести к лучшей производительности, чем плотная модель, поскольку она может видеть, как вещи меняются с течением времени:

Сверточная модель видит, как вещи меняются со временем

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)
multi_performance['Conv'] = multi_conv_model.evaluate(multi_window.test, verbose=0)
multi_window.plot(multi_conv_model)
437/437 [==============================] - 1s 3ms/step - loss: 0.2142 - mean_absolute_error: 0.2805

PNG

RNN

Рекуррентная модель может научиться использовать длинную историю входных данных, если это имеет отношение к прогнозам, которые делает модель. Здесь модель будет накапливать внутреннее состояние в течение 24 часов, прежде чем делать единичный прогноз на следующие 24 часа.

В этом однократном формате LSTM должен выдавать результат только на последнем временном шаге, поэтому установите return_sequences=False .

Lstm накапливает состояние во входном окне и делает одно предсказание на следующие 24 часа.

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)
multi_performance['LSTM'] = multi_lstm_model.evaluate(multi_window.test, verbose=0)
multi_window.plot(multi_lstm_model)
437/437 [==============================] - 1s 3ms/step - loss: 0.2146 - mean_absolute_error: 0.2829

PNG

Дополнительно: модель авторегрессии

Все вышеперечисленные модели предсказывают всю выходную последовательность как за один шаг.

В некоторых случаях для модели может быть полезно разложить этот прогноз на отдельные временные шаги. Затем выходные данные каждой модели могут быть возвращены в себя на каждом этапе, и прогнозы могут быть сделаны с учетом предыдущего, как в классической генерации последовательностей с помощью рекуррентных нейронных сетей .

Одним из явных преимуществ этого стиля модели является то, что ее можно настроить для вывода продукции различной длины.

Вы можете взять любую из одношаговых моделей с несколькими выходами, обученных в первой половине этого руководства, и запустить в цикле авторегрессионной обратной связи, но здесь вы сосредоточитесь на построении модели, специально обученной для этого.

Обратная связь между выходом модели и ее входом

RNN

В этом руководстве создается только авторегрессивная модель RNN, но этот шаблон можно применить к любой модели, которая была разработана для вывода одного временного шага.

Модель будет иметь ту же базовую форму, что и одношаговые модели LSTM : LSTM за которым следуют layers.Dense , layers.Dense которая преобразует выходные данные LSTM в прогнозы модели.

layers.LSTM является layers.LSTMCell завернутым в более высоком уровне layers.RNN , управляющее состоянием и последовательности результатов для вас (см Keras RNNs для деталей).

В этом случае модель должна вручную управлять входными данными для каждого шага, поэтому она использует 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)
multi_performance['AR LSTM'] = feedback_model.evaluate(multi_window.test, verbose=0)
multi_window.plot(feedback_model)
437/437 [==============================] - 3s 8ms/step - loss: 0.2352 - mean_absolute_error: 0.3116

PNG

Спектакль

Очевидно, что отдача от этой проблемы уменьшается в зависимости от сложности модели.

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


metric_name = 'mean_absolute_error'
metric_index = lstm_model.metrics_names.index('mean_absolute_error')
val_mae = [v[metric_index] for v in multi_val_performance.values()]
test_mae = [v[metric_index] 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[1]:0.4f}')
Last    : 0.5157
Repeat  : 0.3774
Linear  : 0.2980
Dense   : 0.2754
Conv    : 0.2724
LSTM    : 0.2770
AR LSTM : 0.3026

Прирост, достигнутый при переходе от плотной модели к сверточным и рекуррентным моделям, составляет всего несколько процентов (если таковые имеются), а авторегрессионная модель работает явно хуже. Таким образом, эти более сложные подходы могут не иметь смысла для решения этой проблемы, но не было возможности узнать, не попробовав, и эти модели могут быть полезны для вашей проблемы.

Следующие шаги

Это руководство было кратким введением в прогнозирование временных рядов с помощью TensorFlow.