O Google I / O retorna de 18 a 20 de maio! Reserve espaço e monte sua agenda Cadastre-se agora

Previsão de série temporal

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

Este tutorial é uma introdução à previsão de série temporal usando o TensorFlow. Ele cria alguns estilos diferentes de modelos, incluindo Redes Neurais Convolucionais e Recorrentes (CNNs e RNNs).

Isso é coberto em duas partes principais, com subseções:

  • Previsão para um único passo de tempo:
    • Um único recurso.
    • Todos os recursos.
  • Preveja várias etapas:
    • Tiro único: Faça as previsões todas de uma vez.
    • Autoregressivo: Faça uma previsão de cada vez e envie a saída de volta para o modelo.

Configurar

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

O conjunto de dados meteorológicos

Este tutorial usa um conjunto de dados de série temporal do tempo registrado pelo Max Planck Institute for Biogeochemistry .

Este conjunto de dados contém 14 recursos diferentes, como temperatura do ar, pressão atmosférica e umidade. Eles foram coletados a cada 10 minutos, começando em 2003. Para eficiência, você usará apenas os dados coletados entre 2009 e 2016. Esta seção do conjunto de dados foi preparada por François Chollet para seu livro Deep Learning with 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

Este tutorial lidará apenas com previsões de hora em hora , portanto, comece subamostrando os dados de intervalos de 10 minutos a 1h:

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

Vamos dar uma olhada nos dados. Aqui estão as primeiras linhas:

df.head()

Aqui está a evolução de alguns recursos ao longo do tempo.

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

Inspecionar e limpar

A seguir, observe as estatísticas do conjunto de dados:

df.describe().transpose()

Velocidade do vento

Uma coisa que deve se destacar é o valor min da velocidade do vento, wv (m/s) e max. wv (m/s) colunas max. wv (m/s) . -9999 este -9999 está errado. Há uma coluna separada de direção do vento, então a velocidade deve ser >=0 . Substitua por zeros:

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

Engenharia de recursos

Antes de mergulhar na construção de um modelo, é importante entender seus dados e certificar-se de que você está transmitindo os dados formatados apropriadamente ao modelo.

Vento

A última coluna dos dados, wd (deg) , fornece a direção do vento em unidades de graus. Os ângulos não são boas entradas de modelo, 360 ° e 0 ° devem estar próximos um do outro e envolver suavemente. A direção não deve importar se o vento não estiver soprando.

No momento, a distribuição dos dados do vento se parece com esta:

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

Mas isso será mais fácil para o modelo interpretar se você converter as colunas de direção e velocidade do vento em um vetor de vento:

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)

A distribuição dos vetores de vento é muito mais simples para o modelo interpretar corretamente.

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

Tempo

Da mesma forma, a coluna Date Time é muito útil, mas não neste formato de string. Comece convertendo para segundos:

timestamp_s = date_time.map(datetime.datetime.timestamp)

Semelhante à direção do vento, o tempo em segundos não é uma entrada de modelo útil. Por serem dados meteorológicos, eles têm uma periodicidade diária e anual clara. Existem muitas maneiras de lidar com a periodicidade.

Uma abordagem simples para convertê-lo em um sinal utilizável é usar sin e cos para converter a hora para limpar os sinais "Hora do dia" e "Hora do ano":

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

Isso dá ao modelo acesso aos recursos de frequência mais importantes. Nesse caso, você sabia com antecedência quais frequências eram importantes.

Se você não sabia, pode determinar quais frequências são importantes usando um fft . Para verificar nossas suposições, aqui está o tf.signal.rfft da temperatura ao longo do tempo. Observe os picos óbvios em frequências próximas a 1/year e 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

Divida os dados

Usaremos uma divisão (70%, 20%, 10%) para os conjuntos de treinamento, validação e teste. Observe que os dados não estão sendo embaralhados aleatoriamente antes da divisão. Isso é por duas razões.

  1. Isso garante que dividir os dados em janelas de amostras consecutivas ainda seja possível.
  2. Garante que os resultados da validação / teste sejam mais realistas, sendo avaliados nos dados coletados após o treinamento do modelo.
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]

Normalize os dados

É importante dimensionar recursos antes de treinar uma rede neural. A normalização é uma maneira comum de fazer esse dimensionamento. Subtraia a média e divida pelo desvio padrão de cada recurso.

A média e o desvio padrão só devem ser calculados usando os dados de treinamento para que os modelos não tenham acesso aos valores nos conjuntos de validação e teste.

Também é discutível que o modelo não deve ter acesso a valores futuros no conjunto de treinamento durante o treinamento, e que essa normalização deve ser feita usando médias móveis. Esse não é o foco deste tutorial, e os conjuntos de validação e teste garantem que você obtenha métricas (de certa forma) honestas. Portanto, no interesse da simplicidade, este tutorial usa uma média simples.

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

Agora dê uma olhada na distribuição dos recursos. Alguns recursos têm caudas longas, mas não há erros óbvios como o valor de velocidade do vento de -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

Janelas de dados

Os modelos neste tutorial farão um conjunto de previsões com base em uma janela de amostras consecutivas dos dados.

Os principais recursos das janelas de entrada são:

  • A largura (número de intervalos de tempo) das janelas de entrada e rótulo
  • A diferença de tempo entre eles.
  • Quais recursos são usados ​​como entradas, rótulos ou ambos.

Este tutorial constrói uma variedade de modelos (incluindo modelos Linear, DNN, CNN e RNN) e os usa para ambos:

  • Previsões de saída única e múltiplas saídas .
  • Predições de passo único e multi-passo de tempo .

Esta seção se concentra na implementação das janelas de dados para que possam ser reutilizadas para todos esses modelos.

Dependendo da tarefa e do tipo de modelo, você pode desejar gerar uma variedade de janelas de dados. aqui estão alguns exemplos:

  1. Por exemplo, para fazer uma única previsão 24 horas no futuro, dadas as 24 horas de histórico, você pode definir uma janela como esta:

    Uma previsão 24h no futuro.

  2. Um modelo que faz uma previsão de 1h no futuro, dadas 6h de histórico, precisaria de uma janela como esta:

    Uma previsão de 1h no futuro.

O restante desta seção define uma classe WindowGenerator . Esta classe pode:

  1. Trate os índices e deslocamentos conforme mostrado nos diagramas acima.
  2. Divida as janelas de recursos em pares de (features, labels) .
  3. Plote o conteúdo das janelas resultantes.
  4. Gere lotes dessas janelas com eficiência a partir dos dados de treinamento, avaliação e teste, usandotf.data.Dataset s.

1. Índices e deslocamentos

Comece criando a classe WindowGenerator . O método __init__ inclui toda a lógica necessária para os índices de entrada e rótulo.

Ele também pega o trem, a avaliação e os dataframes de teste como entrada. Eles serão convertidos emtf.data.Dataset s do Windows posteriormente.

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

Aqui está o código para criar as 2 janelas mostradas nos diagramas no início desta seção:

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 uma lista de entradas consecutivas, o método split_window irá convertê-las em uma janela de entradas e uma janela de rótulos.

O exemplo w2 , acima, será dividido assim:

A janela inicial contém todas as amostras consecutivas, o que a divide em pares (entradas, rótulos)

Este diagrama não mostra o eixo de features dos dados, mas esta função split_window também lida com label_columns para que possa ser usada para exemplos de saída única e multi-saídas.

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

Experimente:

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

Normalmente, os dados no TensorFlow são compactados em matrizes em que o índice externo está entre os exemplos (a dimensão de "lote"). Os índices do meio são as dimensões de "tempo" ou "espaço" (largura, altura). Os índices mais internos são os recursos.

O código acima pegou um lote de janelas de 3, 7 etapas de tempo, com 19 recursos em cada etapa de tempo. Ele os dividiu em um lote de 6 etapas de tempo, 19 entradas de recursos e um rótulo de 1 etapa de 1 tempo e 1 recurso. O rótulo tem apenas um recurso porque o WindowGenerator foi inicializado com label_columns=['T (degC)'] . Inicialmente, este tutorial construirá modelos que prevêem rótulos de saída única.

3. Plotar

Aqui está um método de plotagem que permite uma visualização simples da janela 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 alinha entradas, rótulos e (mais tarde) previsões com base no tempo a que o item se refere:

w2.plot()

png

Você pode plotar as outras colunas, mas a configuração da janela de exemplo w2 só tem rótulos para a coluna T (degC) .

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

png

4. Crietf.data.Dataset s

Finalmente, este método make_dataset um DataFrame série DataFrame e o converterá em umtf.data.Dataset de (input_window, label_window) usando a função 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

O objeto WindowGenerator contém dados de treinamento, validação e teste. Adicione propriedades para acessá-los como tf.data.Datasets usando o método make_dataset acima. Além disso, adicione um lote de exemplo padrão para fácil acesso e plotagem:

@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

Agora, o objeto WindowGenerator fornece acesso aos objetostf.data.Dataset , para que você possa iterar facilmente nos dados.

O Dataset.element_spec propriedade diz que a estrutura, dtypes e formas dos elementos do conjunto de dados.

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

A iteração em um Dataset produz lotes concretos:

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)

Modelos de etapa única

O modelo mais simples que você pode construir neste tipo de dados é aquele que prevê o valor de um único recurso, 1 intervalo de tempo (1h) no futuro com base apenas nas condições atuais.

Portanto, comece construindo modelos para prever o valor T (degC) em 1h no futuro.

Preveja a próxima etapa de tempo

Configure um objeto WindowGenerator para produzir estes pares de etapa única (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)']

O objeto window cria tf.data.Datasets partir dos conjuntos de treinamento, validação e teste, permitindo que você itere facilmente em lotes de dados.

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)

Linha de base

Antes de construir um modelo treinável, seria bom ter uma linha de base de desempenho como um ponto de comparação com os modelos posteriores mais complicados.

A primeira tarefa é prever a temperatura em 1h no futuro, considerando o valor atual de todos os recursos. Os valores atuais incluem a temperatura atual.

Portanto, comece com um modelo que apenas retorne a temperatura atual como a previsão, prevendo "Sem alteração". Esta é uma linha de base razoável, pois a temperatura muda lentamente. É claro que essa linha de base funcionará menos bem se você fizer uma previsão futura.

Envie a entrada para a saída

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]

Instancie e avalie 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 2ms/step - loss: 0.0131 - mean_absolute_error: 0.0791

Isso imprimiu algumas métricas de desempenho, mas elas não dão uma ideia de como o modelo está indo.

O WindowGenerator tem um método de plotagem, mas as plotagens não serão muito interessantes com apenas uma única amostra. Portanto, crie um WindowGenerator mais WindowGenerator que gere janelas 24h de entradas e rótulos consecutivos de uma vez.

O wide_window não muda a forma como o modelo opera. O modelo ainda faz previsões 1h no futuro com base em uma única etapa de tempo de entrada. Aqui, o eixo do time atua como o eixo do batch : cada previsão é feita de forma independente, sem interação entre as etapas de tempo.

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

Essa janela expandida pode ser passada diretamente para o mesmo modelo de baseline sem nenhuma alteração de código. Isso é possível porque as entradas e rótulos têm o mesmo número de etapas de tempo, e a linha de base apenas encaminha a entrada para a saída:

Uma previsão de 1h no futuro, a 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)

Traçando as previsões do modelo de linha de base, você pode ver que são simplesmente os rótulos, deslocados para a direita por 1h.

wide_window.plot(baseline)

png

Nos gráficos acima de três exemplos, o modelo de etapa única é executado ao longo de 24 horas. Isso merece alguma explicação:

  • A linha azul "Entradas" mostra a temperatura de entrada em cada etapa de tempo. O modelo recebe todas as características, este gráfico mostra apenas a temperatura.
  • Os pontos verdes "Etiquetas" mostram o valor de previsão de destino. Esses pontos são mostrados no momento da previsão, não no momento da entrada. É por isso que o intervalo de rótulos é deslocado 1 etapa em relação às entradas.
  • As cruzes laranja de "Predições" são as previsões do modelo para cada intervalo de tempo de saída. Se o modelo previsse perfeitamente, as previsões pousariam diretamente nas "etiquetas".

Modelo linear

O modelo treinável mais simples que você pode aplicar a esta tarefa é inserir a transformação linear entre a entrada e a saída. Neste caso, a saída de uma etapa de tempo depende apenas dessa etapa:

Uma previsão de uma única etapa

Uma layers.Dense . layers.Dense sem conjunto de activation é um modelo linear. A camada apenas transforma o último eixo dos dados de (batch, time, inputs) em (batch, time, units) , é aplicada independentemente a cada item nos eixos de batch e 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)

Este tutorial treina muitos modelos, portanto, empacote o procedimento de treinamento em uma função:

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

Treine o modelo e avalie seu desempenho:

history = compile_and_fit(linear, single_step_window)

val_performance['Linear'] = linear.evaluate(single_step_window.val)
performance['Linear'] = linear.evaluate(single_step_window.test, verbose=0)
Epoch 1/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.4294 - mean_absolute_error: 0.4314 - val_loss: 0.0140 - val_mean_absolute_error: 0.0897
Epoch 2/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0126 - mean_absolute_error: 0.0837 - val_loss: 0.0102 - val_mean_absolute_error: 0.0760
Epoch 3/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0106 - mean_absolute_error: 0.0761 - val_loss: 0.0098 - val_mean_absolute_error: 0.0741
Epoch 4/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0102 - mean_absolute_error: 0.0745 - val_loss: 0.0095 - val_mean_absolute_error: 0.0723
Epoch 5/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0100 - mean_absolute_error: 0.0734 - val_loss: 0.0094 - val_mean_absolute_error: 0.0726
Epoch 6/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0098 - mean_absolute_error: 0.0727 - val_loss: 0.0093 - val_mean_absolute_error: 0.0719
Epoch 7/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0097 - mean_absolute_error: 0.0724 - val_loss: 0.0094 - val_mean_absolute_error: 0.0726
Epoch 8/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0095 - mean_absolute_error: 0.0718 - val_loss: 0.0091 - val_mean_absolute_error: 0.0711
Epoch 9/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0095 - mean_absolute_error: 0.0715 - val_loss: 0.0090 - val_mean_absolute_error: 0.0704
Epoch 10/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0094 - mean_absolute_error: 0.0711 - val_loss: 0.0090 - val_mean_absolute_error: 0.0707
Epoch 11/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0093 - mean_absolute_error: 0.0709 - val_loss: 0.0090 - val_mean_absolute_error: 0.0705
439/439 [==============================] - 1s 2ms/step - loss: 0.0090 - mean_absolute_error: 0.0705

Como o modelo de baseline , o modelo linear pode ser chamado em lotes de janelas amplas. Usado dessa forma, o modelo faz um conjunto de previsões independentes em intervalos de tempo consecutivos. O eixo do time atua como outro eixo do batch . Não há interações entre as previsões em cada etapa de tempo.

Uma previsão de uma única etapa

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)

Aqui está o gráfico de suas previsões de exemplo em wide_window , observe como em muitos casos a previsão é claramente melhor do que apenas retornar a temperatura de entrada, mas em alguns casos é pior:

wide_window.plot(linear)

png

Uma vantagem dos modelos lineares é que eles são relativamente simples de interpretar. Você pode retirar os pesos da camada e ver o peso atribuído 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

Às vezes, o modelo nem mesmo coloca o maior peso na entrada T (degC) . Este é um dos riscos da inicialização aleatória.

Denso

Antes de aplicar modelos que realmente operam em várias etapas de tempo, vale a pena verificar o desempenho de modelos de etapa de entrada única mais profundos, mais poderosos.

Aqui está um modelo semelhante ao modelo linear , exceto que empilha várias camadas Dense entre a entrada e a saída:

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.0529 - mean_absolute_error: 0.1238 - val_loss: 0.0082 - val_mean_absolute_error: 0.0653
Epoch 2/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0079 - mean_absolute_error: 0.0650 - val_loss: 0.0078 - val_mean_absolute_error: 0.0635
Epoch 3/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0075 - mean_absolute_error: 0.0630 - val_loss: 0.0079 - val_mean_absolute_error: 0.0632
Epoch 4/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0072 - mean_absolute_error: 0.0615 - val_loss: 0.0072 - val_mean_absolute_error: 0.0613
Epoch 5/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0069 - mean_absolute_error: 0.0600 - val_loss: 0.0068 - val_mean_absolute_error: 0.0589
Epoch 6/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0068 - mean_absolute_error: 0.0590 - val_loss: 0.0068 - val_mean_absolute_error: 0.0595
Epoch 7/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0067 - mean_absolute_error: 0.0588 - val_loss: 0.0066 - val_mean_absolute_error: 0.0575
Epoch 8/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0067 - mean_absolute_error: 0.0585 - val_loss: 0.0065 - val_mean_absolute_error: 0.0569
Epoch 9/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0066 - mean_absolute_error: 0.0577 - val_loss: 0.0065 - val_mean_absolute_error: 0.0565
Epoch 10/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0065 - mean_absolute_error: 0.0577 - val_loss: 0.0066 - val_mean_absolute_error: 0.0560
Epoch 11/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0064 - mean_absolute_error: 0.0570 - val_loss: 0.0064 - val_mean_absolute_error: 0.0565
Epoch 12/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0064 - mean_absolute_error: 0.0569 - val_loss: 0.0064 - val_mean_absolute_error: 0.0558
Epoch 13/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0064 - mean_absolute_error: 0.0568 - val_loss: 0.0064 - val_mean_absolute_error: 0.0563
Epoch 14/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0063 - mean_absolute_error: 0.0563 - val_loss: 0.0064 - val_mean_absolute_error: 0.0554
439/439 [==============================] - 1s 3ms/step - loss: 0.0064 - mean_absolute_error: 0.0554

Multi-step densa

Um modelo de passo único não tem contexto para os valores atuais de suas entradas. Ele não pode ver como os recursos de entrada estão mudando ao longo do tempo. Para resolver esse problema, o modelo precisa acessar várias etapas de tempo ao fazer previsões:

Três etapas de tempo são usadas para cada previsão.

Os modelos de baseline , linear e dense lidaram com cada etapa de tempo de forma independente. Aqui, o modelo tomará várias etapas de tempo como entrada para produzir uma única saída.

Crie um WindowGenerator que produzirá lotes de 3h de entradas e 1h de rótulos:

Observe que o parâmetro de shift da Window é relativo ao final das duas janelas.

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

Você pode treinar um modelo dense em uma janela de várias etapas de entrada, adicionando uma layers.Flatten como a primeira camada do 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.0071 - mean_absolute_error: 0.0614
conv_window.plot(multi_step_dense)

png

A principal desvantagem dessa abordagem é que o modelo resultante só pode ser executado em janelas de entrada exatamente desta 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)

Os modelos convolucionais na próxima seção corrigem esse problema.

Rede neural de convolução

Uma camada de convolução ( layers.Conv1D ) também leva várias etapas de tempo como entrada para cada previsão.

Abaixo está o mesmo modelo de multi_step_dense , reescrito com uma convolução.

Observe as mudanças:

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

Execute-o em um lote de exemplo para ver se o modelo produz resultados com a 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)

Treine e avalie-o no conv_window e ele deve ter um desempenho semelhante ao modelo multi_step_dense .

history = compile_and_fit(conv_model, conv_window)

IPython.display.clear_output()
val_performance['Conv'] = conv_model.evaluate(conv_window.val)
performance['Conv'] = conv_model.evaluate(conv_window.test, verbose=0)
438/438 [==============================] - 1s 3ms/step - loss: 0.0065 - mean_absolute_error: 0.0560

A diferença entre este conv_model e o modelo multi_step_dense é que o conv_model pode ser executado em entradas de qualquer comprimento. A camada convolucional é aplicada a uma janela deslizante de entradas:

Execução de um modelo convolucional em uma sequência

Se você executá-lo com uma entrada mais ampla, ele produz uma saída mais ampla:

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)

Observe que a saída é mais curta do que a entrada. Para fazer o treinamento ou a plotagem funcionar, você precisa que os rótulos e a previsão tenham a mesma duração. Portanto, crie um WindowGenerator para produzir janelas amplas com algumas etapas extras de tempo de entrada para que o rótulo e os comprimentos de previsão correspondam:

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)

Agora você pode plotar as previsões do modelo em uma janela mais ampla. Observe as 3 etapas de tempo de entrada antes da primeira previsão. Cada previsão aqui é baseada nos 3 passos de tempo anteriores:

wide_conv_window.plot(conv_model)

png

Rede neural recorrente

Uma Rede Neural Recorrente (RNN) é um tipo de rede neural adequada para dados de série temporal. Os RNNs processam uma série temporal passo a passo, mantendo um estado interno passo a passo no tempo.

Para obter mais detalhes, leia o tutorial de geração de texto ou o guia RNN .

Neste tutorial, você usará uma camada RNN chamada Long Short Term Memory ( LSTM ).

Um argumento construtor importante para todas as camadas RNN keras é o argumento return_sequences . Essa configuração pode configurar a camada de duas maneiras.

  1. Se False , o padrão, a camada retorna apenas a saída do passo de tempo final, dando ao modelo tempo para aquecer seu estado interno antes de fazer uma única previsão:

Um lstm aquecendo e fazendo uma única previsão

  1. Se for True a camada retorna uma saída para cada entrada. Isso é útil para:
    • Empilhando camadas RNN.
    • Treinar um modelo em vários passos de tempo simultaneamente.

Um lstm fazendo uma previsão após cada passo de tempo

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

Com return_sequences=True o modelo pode ser treinado em 24 horas de dados por 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 3ms/step - loss: 0.0057 - mean_absolute_error: 0.0525
wide_window.plot(lstm_model)

png

Desempenho

Com esse conjunto de dados, normalmente cada um dos modelos se sai ligeiramente melhor do que o 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.0678
Dense       : 0.0572
Multi step dense: 0.0597
Conv        : 0.0565
LSTM        : 0.0528

Modelos de múltiplas saídas

Os modelos até agora previram um único recurso de saída, T (degC) , para uma única etapa de tempo.

Todos esses modelos podem ser convertidos para prever vários recursos apenas alterando o número de unidades na camada de saída e ajustando as janelas de treinamento para incluir todos os recursos nos 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)

Observe acima que o eixo dos features dos rótulos agora tem a mesma profundidade que as entradas, em vez de 1.

Linha de base

O mesmo modelo de linha de base pode ser usado aqui, mas desta vez repetindo todos os recursos em vez de selecionar um label_index específico.

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

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 3ms/step - loss: 0.0675 - mean_absolute_error: 0.1320

RNN

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

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

history = compile_and_fit(lstm_model, wide_window)

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

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

CPU times: user 4min 4s, sys: 1min, total: 5min 5s
Wall time: 1min 54s

Avançado: conexões residuais

O modelo de Baseline anterior aproveitou o fato de que a sequência não muda drasticamente de uma etapa de tempo para outra. Cada modelo treinado neste tutorial até agora foi inicializado aleatoriamente e, em seguida, teve que aprender que a saída é uma pequena mudança da etapa de tempo anterior.

Embora você possa contornar esse problema com uma inicialização cuidadosa, é mais simples construir isso na estrutura do modelo.

É comum na análise de série temporal construir modelos que, em vez de prever o próximo valor, prevejam como o valor mudará no próximo passo de tempo. Da mesma forma, "Redes residuais" ou "ResNets" no aprendizado profundo referem-se a arquiteturas em que cada camada é adicionada ao resultado acumulado do modelo.

É assim que você aproveita o conhecimento de que a mudança deve ser pequena.

Um modelo com uma conexão residual

Essencialmente, isso inicializa o modelo para corresponder à Baseline . Para essa tarefa, ajuda os modelos a convergirem mais rapidamente, com desempenho um pouco melhor.

Essa abordagem pode ser usada em conjunto com qualquer modelo discutido neste tutorial.

Aqui, ele está sendo aplicado ao modelo LSTM, observe o uso de tf.initializers.zeros para garantir que as alterações iniciais previstas sejam pequenas e não superem a conexão residual. Não há problemas de quebra de simetria para os gradientes aqui, uma vez que os zeros são usados ​​apenas na última camada.

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

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

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

history = compile_and_fit(residual_lstm, wide_window)

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

CPU times: user 2min 15s, sys: 34.3 s, total: 2min 49s
Wall time: 1min 3s

Desempenho

Aqui está o desempenho geral para esses modelos de múltiplas saídas.

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.1212
Residual LSTM  : 0.1194

Os desempenhos acima são a média de todos os resultados do modelo.

Modelos multi-etapas

Os modelos de saída única e de saída múltipla nas seções anteriores fizeram previsões de passo único , 1h no futuro.

Esta seção examina como expandir esses modelos para fazer várias previsões de intervalos de tempo .

Em uma previsão de várias etapas, o modelo precisa aprender a prever uma gama de valores futuros. Assim, ao contrário de um modelo de etapa única, onde apenas um único ponto futuro é previsto, um modelo de várias etapas prevê uma sequência dos valores futuros.

Existem duas abordagens básicas para isso:

  1. Previsões de disparo único em que toda a série temporal é prevista de uma só vez.
  2. Predições autorregressivas em que o modelo só faz predições de etapa única e sua saída é realimentada como entrada.

Nesta seção, todos os modelos preverão todos os recursos em todas as etapas de tempo de saída .

Para o modelo de várias etapas, os dados de treinamento consistem novamente em amostras por hora. Porém, aqui, os modelos aprenderão a prever 24h do futuro, dadas as 24h do passado.

Aqui está um objeto Window que gera essas fatias do conjunto de dados:

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

Baselines

Uma linha de base simples para esta tarefa é repetir a última etapa de tempo de entrada para o número necessário de passos de tempo de saída:

Repita a última entrada, para cada etapa de saída

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

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

multi_val_performance = {}
multi_performance = {}

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

png

Uma vez que esta tarefa é prever 24h dadas 24h, outra abordagem simples é repetir o dia anterior, assumindo que amanhã será semelhante:

Repete o dia 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.4280 - mean_absolute_error: 0.3963

png

Modelos de disparo único

Uma abordagem de alto nível para este problema é usar um modelo "single-shot", onde o modelo faz a predição da sequência inteira em uma única etapa.

Isso pode ser implementado de forma eficiente como uma layers.Dense com OUT_STEPS*features unidades de saída. O modelo precisa apenas remodelar essa saída para o necessário (OUTPUT_STEPS, features) .

Linear

Um modelo linear simples com base na última etapa de tempo de entrada se sai melhor do que qualquer linha de base, mas tem potência insuficiente. O modelo precisa prever etapas de tempo de OUTPUT_STEPS , a partir de uma única etapa de tempo de entrada com uma projeção linear. Ele pode capturar apenas uma fatia de baixa dimensão do comportamento, provavelmente com base principalmente na hora do dia e na época do ano.

Prever todos os passos de tempo da última etapa de tempo

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

png

Denso

Adicionar uma layers.Dense entre a entrada e a saída dá ao modelo linear mais poder, mas ainda é baseado apenas em um único passo de tempo 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 3ms/step - loss: 0.2185 - mean_absolute_error: 0.2805

png

CNN

Um modelo convolucional faz previsões com base em um histórico de largura fixa, o que pode levar a um melhor desempenho do que o modelo denso, pois pode ver como as coisas estão mudando ao longo do tempo:

Um modelo convolucional vê como as coisas mudam ao longo do tempo

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

history = compile_and_fit(multi_conv_model, multi_window)

IPython.display.clear_output()

multi_val_performance['Conv'] = multi_conv_model.evaluate(multi_window.val)
multi_performance['Conv'] = multi_conv_model.evaluate(multi_window.test, verbose=0)
multi_window.plot(multi_conv_model)
437/437 [==============================] - 1s 3ms/step - loss: 0.2163 - mean_absolute_error: 0.2825

png

RNN

Um modelo recorrente pode aprender a usar um longo histórico de entradas, se for relevante para as previsões que o modelo está fazendo. Aqui, o modelo acumulará o estado interno por 24h, antes de fazer uma única previsão para as próximas 24h.

Neste formato de disparo único, o LSTM só precisa produzir uma saída na última etapa de tempo, portanto, defina return_sequences=False .

O lstm acumula estado na janela de entrada e faz uma única previsão para as próximas 24h

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

history = compile_and_fit(multi_lstm_model, multi_window)

IPython.display.clear_output()

multi_val_performance['LSTM'] = multi_lstm_model.evaluate(multi_window.val)
multi_performance['LSTM'] = multi_lstm_model.evaluate(multi_window.test, verbose=0)
multi_window.plot(multi_lstm_model)
437/437 [==============================] - 1s 3ms/step - loss: 0.2127 - mean_absolute_error: 0.2830

png

Avançado: modelo autorregressivo

Todos os modelos acima prevêem toda a sequência de saída em uma única etapa.

Em alguns casos, pode ser útil para o modelo decompor essa previsão em etapas de tempo individuais. Em seguida, a saída de cada modelo pode ser realimentada em cada etapa e as previsões podem ser feitas condicionadas na etapa anterior, como no clássico Gerando Sequências com Redes Neurais Recorrentes .

Uma vantagem clara desse estilo de modelo é que ele pode ser configurado para produzir saídas com comprimento variável.

Você pode pegar qualquer um dos modelos de múltiplas saídas de etapa única treinados na primeira metade deste tutorial e executar em um loop de feedback autoregressivo, mas aqui você se concentrará em construir um modelo que foi explicitamente treinado para fazer isso.

Retorne a saída de um modelo para sua entrada

RNN

Este tutorial constrói apenas um modelo RNN autoregressivo, mas esse padrão pode ser aplicado a qualquer modelo que foi projetado para produzir um único passo de tempo.

O modelo terá a mesma forma básica dos modelos LSTM etapa LSTM : Um LSTM seguido por uma layers.Dense que converte as saídas LSTM em previsões do modelo.

Um layers.LSTM é um layers.LSTMCell envolto em layers.RNN nível superior.RNN que gerencia os resultados de estado e sequência para você (consulte Keras RNNs para obter detalhes).

Nesse caso, o modelo precisa gerenciar manualmente as entradas para cada etapa, para usar layers.LSTMCell diretamente para o nível inferior, interface de etapa única de tempo.

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)

O primeiro método de que esse modelo precisa é um método de warmup para inicializar seu estado interno com base nas entradas. Depois de treinado, esse estado irá capturar as partes relevantes do histórico de entrada. Isso é equivalente ao modelo LSTM etapa LSTM anterior:

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 retorna uma previsão de intervalo de tempo único e o estado interno do LSTM:

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

Com o estado do RNN e uma predição inicial, agora você pode continuar iterando o modelo, alimentando as predições em cada etapa como a entrada.

A abordagem mais simples para coletar as previsões de saída é usar uma lista python e tf.stack após o loop.

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

Teste a execução deste modelo nas entradas de exemplo:

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

Agora treine o 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.2224 - mean_absolute_error: 0.2976

png

Desempenho

Há claramente retornos decrescentes em função da complexidade do modelo neste 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

As métricas para os modelos de várias saídas na primeira metade deste tutorial mostram o desempenho médio em todos os recursos de saída. Esses desempenhos são semelhantes, mas também calculados em média entre os passos de tempo de saída.

for name, value in multi_performance.items():
  print(f'{name:8s}: {value[1]:0.4f}')
Last    : 0.5157
Repeat  : 0.3774
Linear  : 0.2982
Dense   : 0.2741
Conv    : 0.2770
LSTM    : 0.2748
AR LSTM : 0.2892

Os ganhos alcançados indo de um modelo denso para modelos convolucionais e recorrentes são apenas alguns por cento (se houver), e o modelo autoregressivo teve um desempenho claramente pior. Portanto, essas abordagens mais complexas podem não valer a pena neste problema, mas não havia como saber sem tentar, e esses modelos podem ser úteis para o seu problema.

Próximos passos

Este tutorial foi uma introdução rápida à previsão de série temporal usando o TensorFlow.