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

Посмотреть на 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 , скорее всего, ошибочно. Есть отдельный столбец направления ветра, поэтому скорость должна быть >=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)

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

Простой способ преобразовать его в полезный сигнал - использовать 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(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 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 1ms/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 [==============================] - 4s 3ms/step - loss: 0.2373 - mean_absolute_error: 0.2848 - val_loss: 0.0148 - val_mean_absolute_error: 0.0918
Epoch 2/20
1534/1534 [==============================] - 4s 2ms/step - loss: 0.0121 - mean_absolute_error: 0.0817 - val_loss: 0.0107 - val_mean_absolute_error: 0.0766
Epoch 3/20
1534/1534 [==============================] - 4s 2ms/step - loss: 0.0108 - mean_absolute_error: 0.0765 - val_loss: 0.0101 - val_mean_absolute_error: 0.0743
Epoch 4/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0102 - mean_absolute_error: 0.0744 - val_loss: 0.0097 - val_mean_absolute_error: 0.0734
Epoch 5/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0099 - mean_absolute_error: 0.0730 - val_loss: 0.0094 - val_mean_absolute_error: 0.0722
Epoch 6/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0097 - mean_absolute_error: 0.0722 - val_loss: 0.0096 - val_mean_absolute_error: 0.0729
Epoch 7/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0095 - mean_absolute_error: 0.0716 - val_loss: 0.0094 - val_mean_absolute_error: 0.0721
Epoch 8/20
1534/1534 [==============================] - 4s 2ms/step - loss: 0.0094 - mean_absolute_error: 0.0712 - val_loss: 0.0092 - val_mean_absolute_error: 0.0713
Epoch 9/20
1534/1534 [==============================] - 4s 2ms/step - loss: 0.0094 - mean_absolute_error: 0.0710 - val_loss: 0.0090 - val_mean_absolute_error: 0.0698
Epoch 10/20
1534/1534 [==============================] - 4s 2ms/step - loss: 0.0093 - mean_absolute_error: 0.0707 - val_loss: 0.0090 - val_mean_absolute_error: 0.0698
Epoch 11/20
1534/1534 [==============================] - 4s 2ms/step - loss: 0.0093 - mean_absolute_error: 0.0705 - val_loss: 0.0089 - val_mean_absolute_error: 0.0695
Epoch 12/20
1534/1534 [==============================] - 4s 2ms/step - loss: 0.0092 - mean_absolute_error: 0.0703 - val_loss: 0.0093 - val_mean_absolute_error: 0.0713
Epoch 13/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0092 - mean_absolute_error: 0.0703 - val_loss: 0.0089 - val_mean_absolute_error: 0.0694
439/439 [==============================] - 1s 2ms/step - loss: 0.0089 - mean_absolute_error: 0.0694

Как и 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.0121 - mean_absolute_error: 0.0767 - val_loss: 0.0081 - val_mean_absolute_error: 0.0657
Epoch 2/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0079 - mean_absolute_error: 0.0647 - val_loss: 0.0079 - val_mean_absolute_error: 0.0643
Epoch 3/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0075 - mean_absolute_error: 0.0620 - val_loss: 0.0069 - val_mean_absolute_error: 0.0588
Epoch 4/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0072 - mean_absolute_error: 0.0606 - val_loss: 0.0070 - val_mean_absolute_error: 0.0591
Epoch 5/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0070 - mean_absolute_error: 0.0596 - val_loss: 0.0075 - val_mean_absolute_error: 0.0636
439/439 [==============================] - 1s 2ms/step - loss: 0.0075 - mean_absolute_error: 0.0636

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

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

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

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.0068 - mean_absolute_error: 0.0597
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: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)

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

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

Сверточный слой ( 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 2ms/step - loss: 0.0069 - mean_absolute_error: 0.0581

Разница между этой 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 2ms/step - loss: 0.0055 - mean_absolute_error: 0.0510
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.0675
Dense       : 0.0639
Multi step dense: 0.0578
Conv        : 0.0602
LSTM        : 0.0515

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

До сих пор все модели предсказывали одну выходную характеристику 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 1ms/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 2ms/step - loss: 0.0683 - mean_absolute_error: 0.1313

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 2ms/step - loss: 0.0617 - mean_absolute_error: 0.1208

CPU times: user 3min 34s, sys: 49.9 s, total: 4min 24s
Wall time: 1min 24s

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

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 2ms/step - loss: 0.0620 - mean_absolute_error: 0.1178

CPU times: user 1min 50s, sys: 25.7 s, total: 2min 16s
Wall time: 44.1 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.1331
LSTM           : 0.1220
Residual LSTM  : 0.1194

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

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

И модели с одним выходом, и с несколькими выходами в предыдущих разделах делали прогнозы с одним временным шагом на 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 1ms/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 1ms/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.2557 - mean_absolute_error: 0.3055

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 2ms/step - loss: 0.2203 - mean_absolute_error: 0.2829

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 2ms/step - loss: 0.2141 - mean_absolute_error: 0.2802

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 2ms/step - loss: 0.2157 - mean_absolute_error: 0.2861

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 6ms/step - loss: 0.2253 - mean_absolute_error: 0.3008

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.2985
Dense   : 0.2766
Conv    : 0.2756
LSTM    : 0.2792
AR LSTM : 0.2914

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

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

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