¡El Día de la Comunidad de ML es el 9 de noviembre! Únase a nosotros para recibir actualizaciones de TensorFlow, JAX, y más Más información

Pronóstico de series de tiempo

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHubDescargar cuaderno

Este instructivo es una introducción al pronóstico de series de tiempo con TensorFlow. Construye algunos estilos diferentes de modelos, incluidas las redes neuronales convolucionales y recurrentes (CNN y RNN).

Esto se cubre en dos partes principales, con subsecciones:

  • Pronóstico para un solo paso de tiempo:
    • Una sola característica.
    • Todas las características.
  • Pronostique varios pasos:
    • Disparo único: haz todas las predicciones a la vez.
    • Autorregresivo: haga una predicción a la vez y retroalimente la salida al modelo.

Configuración

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
2021-08-03 01:29:35.290679: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcudart.so.11.0

El conjunto de datos meteorológicos

En este tutorial se utiliza un conjunto de datos de series de tiempo El tiempo registrado por el Instituto Max Planck de Biogeoquímica .

Este conjunto de datos contiene 14 características diferentes, como la temperatura del aire, la presión atmosférica y la humedad. Estos se recogieron cada 10 minutos, a partir de 2003. Por razones de eficiencia, que va a utilizar sólo los datos recogidos entre 2009 y 2016. Esta sección del conjunto de datos fue preparado por François Chollet por su libro profundo aprender con 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 [==============================] - 1s 0us/step

Este tutorial simplemente tratar con predicciones por hora, así que empieza por sub-muestreo de los datos de intervalos de 10 minutos a intervalos de una hora:

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

Echemos un vistazo a los datos. Aquí están las primeras filas:

df.head()

Aquí está la evolución de algunas características a lo largo del tiempo:

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

Inspeccionar y limpiar

A continuación, observe las estadísticas del conjunto de datos:

df.describe().transpose()

Velocidad del viento

Una cosa que debe destacarse es el min valor de la velocidad del viento ( wv (m/s) ) y el valor máximo ( max. wv (m/s) ) columnas. Este -9999 es probablemente errónea.

Hay una columna de dirección del viento por separado, por lo que la velocidad debe ser mayor que cero ( >=0 ). Reemplácelo con ceros:

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

Ingeniería de características

Antes de sumergirse en la construcción de un modelo, es importante comprender sus datos y asegurarse de que está pasando los datos del modelo con el formato adecuado.

Viento

La última columna de los datos, wd (deg) -Ofrece la dirección del viento en unidades de grados. Los ángulos no son buenas entradas de modelo: 360 ° y 0 ° deben estar cerca uno del otro y envolver suavemente. La dirección no debería importar si el viento no sopla.

En este momento, la distribución de los datos del viento se ve así:

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

Pero esto será más fácil para el modelo de interpretar si convierte las columnas de dirección y velocidad del viento a un vector de viento:

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)

La distribución de los vectores de viento es mucho más simple para que el modelo la interprete correctamente:

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

Tiempo

Del mismo modo, la Date Time columna es muy útil, pero no en esta forma de cadena. Empiece por convertirlo en segundos:

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

Similar a la dirección del viento, el tiempo en segundos no es una entrada útil del modelo. Al tratarse de datos meteorológicos, tiene una clara periodicidad diaria y anual. Hay muchas formas de lidiar con la periodicidad.

Puede obtener señales utilizables mediante el uso de transformaciones de seno y coseno para borrar las señales de "Hora del día" y "Época del año":

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

Esto le da al modelo acceso a las características de frecuencia más importantes. En este caso, sabía de antemano qué frecuencias eran importantes.

Si usted no tiene esa información, se puede determinar qué frecuencias son importantes mediante la extracción de características con Transformada Rápida de Fourier . Para comprobar los supuestos, aquí está el tf.signal.rfft de la temperatura con el tiempo. Tenga en cuenta los picos evidentes en las frecuencias cercanas a 1/year y 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)')
2021-08-03 01:29:41.976610: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcuda.so.1
2021-08-03 01:29:42.626738: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-03 01:29:42.627640: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1733] Found device 0 with properties: 
pciBusID: 0000:00:05.0 name: Tesla V100-SXM2-16GB computeCapability: 7.0
coreClock: 1.53GHz coreCount: 80 deviceMemorySize: 15.78GiB deviceMemoryBandwidth: 836.37GiB/s
2021-08-03 01:29:42.627697: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcudart.so.11.0
2021-08-03 01:29:42.630402: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcublas.so.11
2021-08-03 01:29:42.630533: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcublasLt.so.11
2021-08-03 01:29:42.631505: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcufft.so.10
2021-08-03 01:29:42.631863: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcurand.so.10
2021-08-03 01:29:42.632610: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcusolver.so.11
2021-08-03 01:29:42.633312: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcusparse.so.11
2021-08-03 01:29:42.633541: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcudnn.so.8
2021-08-03 01:29:42.633650: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-03 01:29:42.634551: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-03 01:29:42.635454: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1871] Adding visible gpu devices: 0
2021-08-03 01:29:42.636067: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 AVX512F FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
2021-08-03 01:29:42.636598: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-03 01:29:42.637440: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1733] Found device 0 with properties: 
pciBusID: 0000:00:05.0 name: Tesla V100-SXM2-16GB computeCapability: 7.0
coreClock: 1.53GHz coreCount: 80 deviceMemorySize: 15.78GiB deviceMemoryBandwidth: 836.37GiB/s
2021-08-03 01:29:42.637536: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-03 01:29:42.638404: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-03 01:29:42.639205: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1871] Adding visible gpu devices: 0
2021-08-03 01:29:42.639284: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcudart.so.11.0
2021-08-03 01:29:43.206803: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1258] Device interconnect StreamExecutor with strength 1 edge matrix:
2021-08-03 01:29:43.206840: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1264]      0 
2021-08-03 01:29:43.206848: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1277] 0:   N 
2021-08-03 01:29:43.207063: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-03 01:29:43.208016: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-03 01:29:43.208856: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-03 01:29:43.209685: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1418] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 14646 MB memory) -> physical GPU (device: 0, name: Tesla V100-SXM2-16GB, pci bus id: 0000:00:05.0, compute capability: 7.0)
2021-08-03 01:29:43.470908: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcufft.so.10

png

Divide los datos

Vamos a usar una (70%, 20%, 10%) de división para los conjuntos de entrenamiento, validación y pruebas. Tenga en cuenta los datos no está siendo mezcladas al azar antes de separarse. Esto es por dos razones:

  1. Garantiza que aún sea posible dividir los datos en ventanas de muestras consecutivas.
  2. Garantiza que los resultados de la validación / prueba sean más realistas y se evalúen en función de los datos recopilados después de que el modelo haya sido entrenado.
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]

Normalizar los datos

Es importante escalar las características antes de entrenar una red neuronal. La normalización es una forma común de hacer esta escala: reste la media y divida por la desviación estándar de cada característica.

La media y la desviación estándar solo deben calcularse utilizando los datos de entrenamiento para que los modelos no tengan acceso a los valores en los conjuntos de validación y prueba.

También se puede argumentar que el modelo no debería tener acceso a valores futuros en el conjunto de entrenamiento durante el entrenamiento, y que esta normalización debería realizarse utilizando promedios móviles. Ese no es el enfoque de este tutorial, y los conjuntos de validación y prueba garantizan que obtenga métricas (algo) honestas. Entonces, en aras de la simplicidad, este tutorial utiliza un promedio simple.

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

Ahora, eche un vistazo a la distribución de las funciones. Algunas características no tienen colas largas, pero no hay errores obvios, como el -9999 valor de la velocidad del viento.

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

Ventana de datos

Los modelos de este tutorial harán un conjunto de predicciones basadas en una ventana de muestras consecutivas de los datos.

Las principales características de las ventanas de entrada son:

  • El ancho (número de pasos de tiempo) de las ventanas de entrada y etiqueta.
  • El tiempo de compensación entre ellos.
  • Qué características se utilizan como entradas, etiquetas o ambas.

Este tutorial crea una variedad de modelos (incluidos los modelos Linear, DNN, CNN y RNN) y los usa para ambos:

  • -Salida única y múltiples salidas predicciones.
  • En tiempo de un solo paso y multi-tiempo-step predicciones.

Esta sección se centra en implementar la ventana de datos para que pueda reutilizarse para todos esos modelos.

Dependiendo de la tarea y el tipo de modelo, es posible que desee generar una variedad de ventanas de datos. Aquí hay unos ejemplos:

  1. Por ejemplo, para hacer una sola predicción 24 horas en el futuro, dadas las 24 horas de historial, puede definir una ventana como esta:

    Una predicción de 24 horas en el futuro.

  2. Un modelo que hace una predicción a una hora en el futuro, dadas seis horas de historia, necesitaría una ventana como esta:

    Una predicción de una hora en el futuro.

El resto de esta sección define una WindowGenerator clase. Esta clase puede:

  1. Maneje los índices y compensaciones como se muestra en los diagramas de arriba.
  2. Ventanas divididas de características en (features, labels) pares.
  3. Trace el contenido de las ventanas resultantes.
  4. Eficientemente generar lotes de estas ventanas de la formación, evaluación, y datos de ensayos, usando tf.data.Dataset s.

1. Índices y compensaciones

Comience por crear la WindowGenerator clase. El __init__ método incluye toda la lógica necesaria para los índices de entrada y de la etiqueta.

También toma los DataFrames de entrenamiento, evaluación y prueba como entrada. Estos se convertirán en tf.data.Dataset s de las ventanas más tarde.

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

Aquí hay un código para crear las 2 ventanas que se muestran en los diagramas al comienzo de esta sección:

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. Dividir

Dada una lista de entradas consecutivas, el split_window método los convertirá en una ventana de entradas y una ventana de etiquetas.

El ejemplo w2 se define anteriormente se dividirá así:

La ventana inicial son todas las muestras consecutivas, esto la divide en pares (entradas, etiquetas)

Este diagrama no muestra las features eje de los datos, pero esto split_window función también se encarga de las label_columns para que pueda ser usado para los ejemplos tanto la única salida y multi-salida.

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

Pruébalo:

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

Por lo general, los datos en TensorFlow se empaquetan en arreglos donde el índice más externo está en los ejemplos (la dimensión "lote"). Los índices intermedios son las dimensiones de "tiempo" o "espacio" (ancho, alto). Los índices más internos son las características.

El código anterior tomó un lote de tres ventanas de 7 pasos con 19 características en cada paso de tiempo. Los divide en un lote de entradas de 19 funciones de paso de 6 veces y una etiqueta de función de paso 1 de 1 vez. La etiqueta sólo tiene una característica porque el WindowGenerator se inicializa con label_columns=['T (degC)'] . Inicialmente, este tutorial creará modelos que predicen etiquetas de salida únicas.

3. Parcela

Aquí hay un método de trazado que permite una visualización simple de la ventana dividida:

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

Este gráfico alinea las entradas, las etiquetas y las predicciones (posteriores) en función del tiempo al que se refiere el elemento:

w2.plot()

png

Puede representar las otras columnas, pero el ejemplo de ventana w2 configuración sólo tiene etiquetas para la T (degC) columna.

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

png

4. Crear tf.data.Dataset s

Por último, este make_dataset método se llevará a una serie de tiempo trama de datos y convertirlo a un tf.data.Dataset de (input_window, label_window) pares utilizando el preprocessing.timeseries_dataset_from_array función:

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

El WindowGenerator objeto contiene el entrenamiento, validación y los datos de prueba.

Añadir propiedades para acceder a ellos como tf.data.Dataset s utilizando el make_dataset método definido previamente. Además, agregue un lote de ejemplo estándar para facilitar el acceso y el trazado:

@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

Ahora, el WindowGenerator objeto le da acceso a la tf.data.Dataset objetos, por lo que fácilmente puede iterar sobre los datos.

El Dataset.element_spec propiedad que le dice a la estructura, tipos de datos, y las formas de los elementos del conjunto de datos.

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

Iterar sobre un Dataset produce lotes de hormigón:

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)
2021-08-03 01:29:50.245637: I tensorflow/compiler/mlir/mlir_graph_optimization_pass.cc:176] None of the MLIR Optimization Passes are enabled (registered 2)
2021-08-03 01:29:50.246163: I tensorflow/core/platform/profile_utils/cpu_utils.cc:114] CPU Frequency: 2000179999 Hz

Modelos de un solo paso

El modelo más simple que puede construir a partir de este tipo de datos es uno que predice el valor de una sola característica: 1 paso de tiempo (una hora) en el futuro basado únicamente en las condiciones actuales.

Por lo tanto, empezar por la construcción de modelos para predecir la T (degC) el valor de una hora de futuro.

Predecir el próximo paso de tiempo

Configurar un WindowGenerator objeto para producir estos de un solo paso (input, label) pares:

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

La window objeto crea tf.data.Dataset s del entrenamiento, validación, y equipos de prueba, lo que le permite fácilmente iterar sobre lotes de datos.

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)

Base

Antes de construir un modelo entrenable, sería bueno tener una línea de base de rendimiento como punto de comparación con los modelos posteriores más complicados.

Esta primera tarea es predecir la temperatura una hora en el futuro, dado el valor actual de todas las características. Los valores actuales incluyen la temperatura actual.

Entonces, comience con un modelo que simplemente devuelva la temperatura actual como predicción, prediciendo "Sin cambios". Esta es una línea de base razonable ya que la temperatura cambia lentamente. Por supuesto, esta línea de base funcionará menos bien si realiza una predicción adicional en el futuro.

Envía la entrada a la salida

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]

Cree una instancia y evalúe este modelo:

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

Eso imprimió algunas métricas de rendimiento, pero esas no le dan una idea de qué tan bien está funcionando el modelo.

El WindowGenerator tiene un método trama, pero las parcelas no va a ser muy interesante con sólo una única muestra.

Por lo tanto, crear un amplio WindowGenerator que genera ventanas 24 horas de entradas y etiquetas consecutivas a la vez. El nuevo wide_window variables no cambia la forma en que el modelo funciona. El modelo aún hace predicciones una hora en el futuro basándose en un solo paso de tiempo de entrada. Aquí, el time eje actúa como el batch eje: cada predicción se hace de forma independiente con ninguna interacción entre pasos de tiempo:

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

Esta ventana ampliada se puede pasar directamente a la misma baseline modelo sin ningún cambio de código. Esto es posible porque las entradas y las etiquetas tienen el mismo número de pasos de tiempo, y la línea de base simplemente reenvía la entrada a la salida:

Una predicción de 1h en el futuro, cada hora.

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)

Al trazar las predicciones del modelo de línea de base, observe que son simplemente las etiquetas desplazadas a la derecha una hora:

wide_window.plot(baseline)

png

En los gráficos anteriores de tres ejemplos, el modelo de un solo paso se ejecuta en el transcurso de 24 horas. Esto merece alguna explicación:

  • Los azules Inputs línea muestra la temperatura de entrada en cada paso de tiempo. El modelo recibe todas las características, este gráfico solo muestra la temperatura.
  • Los verdes Labels puntos muestran el valor de predicción de destino. Estos puntos se muestran en el momento de la predicción, no en el momento de la entrada. Es por eso que el rango de etiquetas se desplaza 1 paso con respecto a las entradas.
  • La naranja Predictions cruces son la predicción del modelo es para cada paso de tiempo de salida. Si el modelo se predice perfectamente las predicciones podrían aterrizar directamente en las Labels .

Modelo lineal

El modelo más simple entrenable se puede aplicar a esta tarea consiste en insertar transformación lineal entre la entrada y la salida. En este caso, la salida de un paso de tiempo solo depende de ese paso:

Una predicción de un solo paso

A tf.keras.layers.Dense capa sin activation conjunto es un modelo lineal. La capa sólo transforma el último eje de los datos a partir de (batch, time, inputs) a (batch, time, units) ; se aplica de forma independiente para cada artículo a través de los batch y el time ejes.

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)
2021-08-03 01:29:52.088029: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcublas.so.11
Output shape: (32, 1, 1)
2021-08-03 01:29:52.467708: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcublasLt.so.11

Este tutorial entrena muchos modelos, así que empaquete el procedimiento de entrenamiento en una función:

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

Entrene el modelo y evalúe su desempeño:

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.3780 - mean_absolute_error: 0.3770 - val_loss: 0.0286 - val_mean_absolute_error: 0.1310
Epoch 2/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0156 - mean_absolute_error: 0.0945 - val_loss: 0.0105 - val_mean_absolute_error: 0.0761
Epoch 3/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0102 - mean_absolute_error: 0.0750 - val_loss: 0.0093 - val_mean_absolute_error: 0.0718
Epoch 4/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0095 - mean_absolute_error: 0.0717 - val_loss: 0.0089 - val_mean_absolute_error: 0.0700
Epoch 5/20
1534/1534 [==============================] - 4s 2ms/step - loss: 0.0092 - mean_absolute_error: 0.0704 - val_loss: 0.0088 - val_mean_absolute_error: 0.0697
Epoch 6/20
1534/1534 [==============================] - 4s 2ms/step - loss: 0.0091 - mean_absolute_error: 0.0700 - val_loss: 0.0087 - val_mean_absolute_error: 0.0694
Epoch 7/20
1534/1534 [==============================] - 4s 2ms/step - loss: 0.0091 - mean_absolute_error: 0.0697 - val_loss: 0.0087 - val_mean_absolute_error: 0.0697
Epoch 8/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0091 - mean_absolute_error: 0.0697 - val_loss: 0.0088 - val_mean_absolute_error: 0.0698
439/439 [==============================] - 1s 2ms/step - loss: 0.0088 - mean_absolute_error: 0.0698

Al igual que la baseline del modelo, el modelo lineal se puede llamar en lotes de amplias ventanas. Usado de esta manera, el modelo hace un conjunto de predicciones independientes en pasos de tiempo consecutivos. El time eje actúa como otro batch eje. No hay interacciones entre las predicciones en cada paso de tiempo.

Una predicción de un solo paso

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)

Aquí está la trama de sus predicciones en el ejemplo wide_window , nota cómo en muchos casos, la predicción es claramente mejor que acaban de volver la temperatura de entrada, pero en algunos casos es peor:

wide_window.plot(linear)

png

Una ventaja de los modelos lineales es que son relativamente simples de interpretar. Puede extraer los pesos de la capa y visualizar el peso asignado a cada entrada:

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

A veces, el modelo no tiene ni siquiera colocar el mayor peso en la entrada T (degC) . Este es uno de los riesgos de la inicialización aleatoria.

Denso

Antes de aplicar modelos que realmente operan en múltiples pasos de tiempo, vale la pena verificar el rendimiento de modelos de pasos de entrada únicos más profundos y potentes.

Aquí está un modelo similar a la linear modelo, excepto que las pilas varios unos Dense capas entre la entrada y la salida:

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 3ms/step - loss: 0.0143 - mean_absolute_error: 0.0809 - val_loss: 0.0078 - val_mean_absolute_error: 0.0638
Epoch 2/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0081 - mean_absolute_error: 0.0655 - val_loss: 0.0086 - val_mean_absolute_error: 0.0695
Epoch 3/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0075 - mean_absolute_error: 0.0627 - val_loss: 0.0070 - val_mean_absolute_error: 0.0597
Epoch 4/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0072 - mean_absolute_error: 0.0607 - val_loss: 0.0067 - val_mean_absolute_error: 0.0583
Epoch 5/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0071 - mean_absolute_error: 0.0603 - val_loss: 0.0066 - val_mean_absolute_error: 0.0572
Epoch 6/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0069 - mean_absolute_error: 0.0592 - val_loss: 0.0065 - val_mean_absolute_error: 0.0570
Epoch 7/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0068 - mean_absolute_error: 0.0585 - val_loss: 0.0072 - val_mean_absolute_error: 0.0608
Epoch 8/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0068 - mean_absolute_error: 0.0583 - val_loss: 0.0066 - val_mean_absolute_error: 0.0578
439/439 [==============================] - 1s 2ms/step - loss: 0.0066 - mean_absolute_error: 0.0578

Denso de varios pasos

Un modelo de paso de tiempo único no tiene contexto para los valores actuales de sus entradas. No puede ver cómo cambian las características de entrada con el tiempo. Para abordar este problema, el modelo necesita acceso a múltiples pasos de tiempo al hacer predicciones:

Se utilizan tres pasos de tiempo para cada predicción.

La baseline , linear y dense modelos manejados cada paso de tiempo independiente. Aquí el modelo tomará múltiples pasos de tiempo como entrada para producir una única salida.

Crear un WindowGenerator que va a producir lotes de entradas de tres horas y etiquetas de una hora:

Tenga en cuenta que la Window 's shift parámetro es relativo al final de las dos ventanas.

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 3 hours of inputs, predict 1 hour into the future.")
Text(0.5, 1.0, 'Given 3 hours of inputs, predict 1 hour into the future.')

png

Se podría formar una dense modelo en una ventana de entrada de paso múltiple mediante la adición de un tf.keras.layers.Flatten como la primera capa del modelo:

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.0061 - mean_absolute_error: 0.0539
conv_window.plot(multi_step_dense)

png

El principal inconveniente de este enfoque es que el modelo resultante solo se puede ejecutar en ventanas de entrada de exactamente esta forma.

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)

Los modelos convolucionales de la siguiente sección solucionan este problema.

Red neuronal de convolución

Una capa de convolución ( tf.keras.layers.Conv1D ) también tiene múltiples pasos de tiempo como entrada para cada predicción.

A continuación se muestra el mismo modelo que multi_step_dense , re-escrito con una convolución.

Tenga en cuenta los cambios:

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),
])

Ejecútelo en un lote de ejemplo para comprobar que el modelo produce salidas con la forma esperada:

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)

Tren y evaluarla en el conv_window y debe dar un rendimiento similar al multi_step_dense modelo.

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.0063 - mean_absolute_error: 0.0550

La diferencia entre este conv_model y la multi_step_dense modelo es que el conv_model se puede ejecutar en entradas de cualquier longitud. La capa convolucional se aplica a una ventana deslizante de entradas:

Ejecutando un modelo convolucional en una secuencia

Si lo ejecuta en una entrada más amplia, produce una salida más amplia:

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)

Tenga en cuenta que la salida es más corta que la entrada. Para que el entrenamiento o el trazado funcionen, necesita que las etiquetas y la predicción tengan la misma longitud. Así que construir una WindowGenerator para producir amplias ventanas con algunas medidas adicionales de tiempo de entrada por lo que las etiquetas y de predicción longitudes coinciden:

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)

Ahora, puede trazar las predicciones del modelo en una ventana más amplia. Tenga en cuenta los 3 pasos de tiempo de entrada antes de la primera predicción. Cada predicción aquí se basa en los 3 pasos de tiempo anteriores:

wide_conv_window.plot(conv_model)

png

Red neuronal recurrente

Una red neuronal recurrente (RNN) es un tipo de red neuronal que se adapta bien a los datos de series de tiempo. Los RNN procesan una serie de tiempo paso a paso, manteniendo un estado interno de paso de tiempo a paso de tiempo.

Se puede obtener más información en la generación de texto con un RNN tutorial y las redes neuronales recurrentes (RNN) con Keras guía.

En este tutorial, utilizará una capa RNN llamado Long memoria a corto plazo ( tf.keras.layers.LSTM ).

Un argumento del constructor importante para todas las capas Keras RNN, como tf.keras.layers.LSTM , es la return_sequences argumento. Esta configuración puede configurar la capa de una de estas dos formas:

  1. Si False , el valor predeterminado, la capa de sólo devuelve la salida del paso de tiempo final, dando tiempo al modelo para calentar su estado interno antes de hacer una única predicción:

Un LSTM calentando y haciendo una sola predicción

  1. Si True , la capa devuelve una salida para cada entrada. Esto es útil para:
    • Apilamiento de capas RNN.
    • Entrenamiento de un modelo en múltiples pasos de tiempo simultáneamente.

Un LSTM haciendo una predicción después de cada paso de tiempo.

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

Con return_sequences=True , el modelo puede ser entrenado en 24 horas de datos a la vez.

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.0056 - mean_absolute_error: 0.0514
wide_window.plot(lstm_model)

png

Rendimiento

Con este conjunto de datos, por lo general, cada uno de los modelos funciona un poco mejor que el anterior:

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.0682
Dense       : 0.0600
Multi step dense: 0.0549
Conv        : 0.0548
LSTM        : 0.0527

Modelos de salida múltiple

Los modelos hasta ahora todos predijeron una sola característica de salida, T (degC) , para un solo paso de tiempo.

Todos estos modelos se pueden convertir para predecir múltiples funciones con sólo cambiar el número de unidades en la capa de salida y el ajuste de las ventanas de formación para incluir todas las características de las labels ( example_labels ):

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

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

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

Nota anterior que las features eje de las etiquetas ahora tiene la misma profundidad que las entradas, en vez de 1 .

Base

El modelo misma línea base ( Baseline ) se puede utilizar aquí, pero esta vez la repetición de todas las características en lugar de seleccionar una específica 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

Denso

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.0690 - mean_absolute_error: 0.1302

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.1210

CPU times: user 3min 47s, sys: 51.9 s, total: 4min 38s
Wall time: 1min 29s

Avanzado: conexiones residuales

La Baseline el modelo de antes aprovechó el hecho de que la secuencia no cambia drásticamente de un intervalo de tiempo de paso de tiempo. Todos los modelos entrenados en este tutorial hasta ahora se inicializaron aleatoriamente y luego tuvieron que aprender que el resultado es un pequeño cambio con respecto al paso de tiempo anterior.

Si bien puede solucionar este problema con una inicialización cuidadosa, es más sencillo integrarlo en la estructura del modelo.

Es común en el análisis de series de tiempo construir modelos que en lugar de predecir el siguiente valor, predicen cómo cambiará el valor en el próximo paso de tiempo. Del mismo modo, las redes residuales -o ResNets-en el aprendizaje profundo se refieren a arquitecturas donde cada capa se añade a resultado la acumulación del modelo.

Así es como se aprovecha el conocimiento de que el cambio debe ser pequeño.

Un modelo con una conexión residual

Esencialmente, esto inicializa el modelo para que coincida con la Baseline . Para esta tarea, ayuda a que los modelos converjan más rápido, con un rendimiento ligeramente mejor.

Este enfoque se puede utilizar junto con cualquier modelo analizado en este tutorial.

En este caso, se aplica al modelo LSTM, tenga en cuenta el uso de los tf.initializers.zeros para asegurar que los cambios previstos iniciales son pequeñas, y no apaguen a la conexión residual. No hay problemas de ruptura de simetría para las gradientes aquí, ya que los zeros se utilizan solamente en la última capa.

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

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

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

history = compile_and_fit(residual_lstm, wide_window)

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

CPU times: user 1min 49s, sys: 25.1 s, total: 2min 14s
Wall time: 43.4 s

Rendimiento

A continuación, se muestra el rendimiento general de estos modelos de múltiples salidas.

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.1321
LSTM           : 0.1229
Residual LSTM  : 0.1191

Los rendimientos anteriores se promedian en todos los resultados del modelo.

Modelos de varios pasos

Tanto los modelos de salida múltiple y salida única y en las secciones anteriores hacen predicciones paso sola vez, a una hora en el futuro.

En esta sección se analiza la forma de ampliar estos modelos para hacer varias predicciones paso de tiempo.

En una predicción de varios pasos, el modelo debe aprender a predecir un rango de valores futuros. Por lo tanto, a diferencia de un modelo de un solo paso, donde solo se predice un único punto futuro, un modelo de varios pasos predice una secuencia de los valores futuros.

Hay dos enfoques aproximados para esto:

  1. Predicciones de un solo disparo donde se predice toda la serie de tiempo a la vez.
  2. Predicciones autorregresivas donde el modelo solo hace predicciones de un solo paso y su salida se retroalimenta como su entrada.

En esta sección todos los modelos predicen todas las características a través de todos los pasos de tiempo de salida.

Para el modelo de varios pasos, los datos de entrenamiento nuevamente constan de muestras por hora. Sin embargo, aquí, los modelos aprenderán a predecir 24 horas en el futuro, dadas las 24 horas del pasado.

Aquí está una Window objeto que genera estas rebanadas del conjunto de datos:

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

Líneas de base

Una línea de base simple para esta tarea es repetir el último paso de tiempo de entrada para el número requerido de pasos de tiempo de salida:

Repita la última entrada, para cada paso de salida

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

Dado que esta tarea es predecir 24 horas en el futuro, dadas las 24 horas del pasado, otro enfoque simple es repetir el día anterior, asumiendo que mañana será similar:

Repetir el día anterior

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

Modelos de un solo disparo

Un enfoque de alto nivel para este problema es utilizar un modelo de "disparo único", donde el modelo hace la predicción de la secuencia completa en un solo paso.

Esto se puede implementar de manera eficiente como tf.keras.layers.Dense con OUT_STEPS*features unidades de salida. El modelo sólo tiene que formar de nuevo que la producción de los requeridos (OUTPUT_STEPS, features) .

Lineal

Un modelo lineal simple basado en el último paso de tiempo de entrada funciona mejor que cualquier línea de base, pero tiene poca potencia. El modelo tiene que predecir OUTPUT_STEPS pasos de tiempo, a partir de un solo paso de tiempo de entrada con una proyección lineal. Solo puede capturar una porción de baja dimensión del comportamiento, probablemente basado principalmente en la hora del día y la época del año.

Predecir todos los pasos de tiempo desde el último paso de tiempo

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.2555 - mean_absolute_error: 0.3052

png

Denso

Adición de un tf.keras.layers.Dense entre la entrada y la salida da la potencia lineal modelo más, pero todavía sólo se basa en un único paso de tiempo de entrada.

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.2214 - mean_absolute_error: 0.2850

png

CNN

Un modelo convolucional hace predicciones basadas en un historial de ancho fijo, lo que puede conducir a un mejor rendimiento que el modelo denso, ya que puede ver cómo cambian las cosas con el tiempo:

Un modelo convolucional ve cómo cambian las cosas con el tiempo

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.2136 - mean_absolute_error: 0.2788

png

RNN

Un modelo recurrente puede aprender a usar un largo historial de entradas, si es relevante para las predicciones que hace el modelo. Aquí, el modelo acumulará el estado interno durante 24 horas, antes de realizar una única predicción durante las próximas 24 horas.

En este formato de un solo tiro, el LSTM sólo necesita para producir una salida en el último paso de tiempo, por lo que figuran return_sequences=False en tf.keras.layers.LSTM .

El LSTM acumula el estado sobre la ventana de entrada y hace una sola predicción para las próximas 24 horas.

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.2147 - mean_absolute_error: 0.2866

png

Avanzado: modelo autorregresivo

Todos los modelos anteriores predicen la secuencia de salida completa en un solo paso.

En algunos casos, puede ser útil que el modelo descomponga esta predicción en pasos de tiempo individuales. Entonces, la producción de cada modelo puede ser alimentada de nuevo en sí mismo en cada paso y se pueden hacer predicciones condicionado a la anterior, como en los clásicos Generación de secuencias con recurrentes Redes Neuronales .

Una clara ventaja de este estilo de modelo es que se puede configurar para producir resultados con una longitud variable.

Puede tomar cualquiera de los modelos de salida múltiple de un solo paso entrenados en la primera mitad de este tutorial y ejecutarlo en un ciclo de retroalimentación autorregresivo, pero aquí se enfocará en construir un modelo que haya sido entrenado explícitamente para hacer eso.

Retroalimentar la salida de un modelo a su entrada

RNN

Este tutorial solo crea un modelo RNN autorregresivo, pero este patrón se puede aplicar a cualquier modelo que haya sido diseñado para generar un solo paso de tiempo.

El modelo tendrá la misma forma básica como los modelos LSTM de un solo paso desde antes: un tf.keras.layers.LSTM capa seguido por un tf.keras.layers.Dense capa que convierte la LSTM salidas de capa para las predicciones del modelo.

Un tf.keras.layers.LSTM es un tf.keras.layers.LSTMCell envuelto en el nivel más alto tf.keras.layers.RNN que gestiona los resultados estatales y de secuencia para usted (Confirmar la recurrente Redes Neuronales (RNN) con Keras guía para obtener más detalles).

En este caso, el modelo tiene que manejar manualmente las entradas para cada paso, por lo que utiliza tf.keras.layers.LSTMCell directamente para el nivel inferior, sola interfaz de paso de tiempo.

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)

El primer método necesita este modelo es un warmup método para inicializar su estado interno basado en las entradas. Una vez entrenado, este estado capturará las partes relevantes del historial de entrada. Esto es equivalente a la de un solo paso LSTM modelo de antes:

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

Este método devuelve un solo predicción paso de tiempo y el estado interno de la LSTM :

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

Con la RNN estado 's, y así predecir ahora se puede continuar la iteración del modelo de la alimentación de las predicciones en cada paso atrás que la entrada.

El método más sencillo para la recogida de las predicciones de salida es utilizar una lista de Python y una tf.stack después del bucle.

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

Pruebe este modelo en las entradas de ejemplo:

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

Ahora, entrena el modelo:

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 7ms/step - loss: 0.2267 - mean_absolute_error: 0.3020

png

Rendimiento

Hay rendimientos claramente decrecientes en función de la complejidad del modelo en este problema:

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

Las métricas para los modelos de múltiples salidas en la primera mitad de este tutorial muestran el rendimiento promedio en todas las funciones de salida. Estos rendimientos son similares pero también promediados en los pasos de tiempo de salida.

for name, value in multi_performance.items():
  print(f'{name:8s}: {value[1]:0.4f}')
Last    : 0.5157
Repeat  : 0.3774
Linear  : 0.2984
Dense   : 0.2800
Conv    : 0.2727
LSTM    : 0.2755
AR LSTM : 0.2903

Las ganancias logradas al pasar de un modelo denso a modelos convolucionales y recurrentes son solo un pequeño porcentaje (si lo hay), y el modelo autorregresivo tuvo un desempeño claramente peor. Por lo que estos enfoques más complejos pueden no valer la pena en este problema, pero no había manera de saber sin tratar, y estos modelos puedan ser de utilidad para su problema.

Próximos pasos

Este tutorial fue una introducción rápida a la previsión de series de tiempo con TensorFlow.

Para obtener más información, consulte:

Además, recuerde que usted puede implementar cualquier modelo de serie temporal clásica en TensorFlow-este tutorial solo se centra en TensorFlow de funcionalidad integrada.