![]() | ![]() | ![]() | ![]() |
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.
- Prever 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 ao modelo.
Estabelecer
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)
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)
. Este -9999
provavelmente 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 de forma adequada 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]')
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)
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 de "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')
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)')
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. Por duas razões.
- Isso garante que dividir os dados em janelas de amostras consecutivas ainda seja possível.
- 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)
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:
Por exemplo, para fazer uma única previsão 24 horas no futuro, dadas 24 horas de histórico, você pode definir uma janela como esta:
Um modelo que faz uma previsão de 1h no futuro, dadas 6h de histórico, precisaria de uma janela como esta:
O restante desta seção define uma classe WindowGenerator
. Esta classe pode:
- Trate os índices e deslocamentos conforme mostrado nos diagramas acima.
- Divida as janelas de recursos em pares de
(features, labels)
. - Trace o conteúdo das janelas resultantes.
- Gere lotes dessas janelas com eficiência a partir dos dados de treinamento, avaliação e teste, usando
tf.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 os dataframes de treinamento, avaliação e 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:
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 mais 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 únicos.
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(3, 1, n+1)
plt.ylabel(f'{plot_col} [normed]')
plt.plot(self.input_indices, inputs[n, :, plot_col_index],
label='Inputs', marker='.', zorder=-10)
if self.label_columns:
label_col_index = self.label_columns_indices.get(plot_col, None)
else:
label_col_index = plot_col_index
if label_col_index is None:
continue
plt.scatter(self.label_indices, labels[n, :, label_col_index],
edgecolors='k', label='Labels', c='#2ca02c', s=64)
if model is not None:
predictions = model(inputs)
plt.scatter(self.label_indices, predictions[n, :, label_col_index],
marker='X', edgecolors='k', label='Predictions',
c='#ff7f0e', s=64)
if n == 0:
plt.legend()
plt.xlabel('Time [h]')
WindowGenerator.plot = plot
Este gráfico alinha entradas, rótulos e (mais tarde) previsões com base no tempo a que o item se refere:
w2.plot()
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)')
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. Também 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.
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 1h no futuro, dado 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 não funcionará bem se você fizer uma previsão futura.
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.0129 - mean_absolute_error: 0.0787
Isso imprimiu algumas métricas de desempenho, mas 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 maneira 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 passos de tempo, e a linha de base apenas encaminha a entrada para a saída:
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)
Nos gráficos de três exemplos acima, 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 "Labels" 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 nos "rótulos".
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. Nesse caso, a saída de uma etapa de tempo depende apenas dessa etapa:
Uma layers.Dense
. layers.Dense
sem conjunto de activation
é um modelo linear. A camada transforma apenas o último eixo dos dados de (batch, time, inputs)
em (batch, time, units)
, é aplicada de forma independente 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 [==============================] - 6s 4ms/step - loss: 0.2064 - mean_absolute_error: 0.2944 - val_loss: 0.0101 - val_mean_absolute_error: 0.0752 Epoch 2/20 1534/1534 [==============================] - 5s 3ms/step - loss: 0.0099 - mean_absolute_error: 0.0734 - val_loss: 0.0089 - val_mean_absolute_error: 0.0708 Epoch 3/20 1534/1534 [==============================] - 5s 3ms/step - loss: 0.0091 - mean_absolute_error: 0.0700 - val_loss: 0.0088 - val_mean_absolute_error: 0.0702 Epoch 4/20 1534/1534 [==============================] - 6s 4ms/step - loss: 0.0091 - mean_absolute_error: 0.0699 - val_loss: 0.0088 - val_mean_absolute_error: 0.0703 Epoch 5/20 1534/1534 [==============================] - 6s 4ms/step - loss: 0.0091 - mean_absolute_error: 0.0699 - val_loss: 0.0088 - val_mean_absolute_error: 0.0707 439/439 [==============================] - 1s 2ms/step - loss: 0.0088 - mean_absolute_error: 0.0707
Como o modelo de baseline
, o modelo linear pode ser chamado em lotes de janelas largas. 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.
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)
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)
À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 [==============================] - 7s 4ms/step - loss: 0.0701 - mean_absolute_error: 0.1282 - val_loss: 0.0090 - val_mean_absolute_error: 0.0719 Epoch 2/20 1534/1534 [==============================] - 7s 4ms/step - loss: 0.0081 - mean_absolute_error: 0.0653 - val_loss: 0.0069 - val_mean_absolute_error: 0.0592 Epoch 3/20 1534/1534 [==============================] - 7s 4ms/step - loss: 0.0075 - mean_absolute_error: 0.0621 - val_loss: 0.0069 - val_mean_absolute_error: 0.0607 Epoch 4/20 1534/1534 [==============================] - 7s 4ms/step - loss: 0.0073 - mean_absolute_error: 0.0610 - val_loss: 0.0067 - val_mean_absolute_error: 0.0590 Epoch 5/20 1534/1534 [==============================] - 7s 4ms/step - loss: 0.0070 - mean_absolute_error: 0.0597 - val_loss: 0.0065 - val_mean_absolute_error: 0.0571 Epoch 6/20 1534/1534 [==============================] - 7s 4ms/step - loss: 0.0069 - mean_absolute_error: 0.0593 - val_loss: 0.0067 - val_mean_absolute_error: 0.0590 Epoch 7/20 1534/1534 [==============================] - 7s 4ms/step - loss: 0.0069 - mean_absolute_error: 0.0592 - val_loss: 0.0064 - val_mean_absolute_error: 0.0564 Epoch 8/20 1534/1534 [==============================] - 7s 5ms/step - loss: 0.0069 - mean_absolute_error: 0.0588 - val_loss: 0.0074 - val_mean_absolute_error: 0.0628 Epoch 9/20 1534/1534 [==============================] - 7s 4ms/step - loss: 0.0067 - mean_absolute_error: 0.0580 - val_loss: 0.0067 - val_mean_absolute_error: 0.0587 439/439 [==============================] - 1s 3ms/step - loss: 0.0067 - mean_absolute_error: 0.0587
Multi-step densa
Um modelo de passo único não tem contexto para os valores atuais de suas entradas. Ele não consegue ver como os recursos de entrada estão mudando com o tempo. Para resolver esse problema, o modelo precisa de acesso a várias etapas de tempo ao fazer previsões:
Os modelos de baseline
, linear
e dense
trataram cada etapa de tempo independentemente. Aqui, o modelo levará 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 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.')
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.0062 - mean_absolute_error: 0.0559
conv_window.plot(multi_step_dense)
A principal desvantagem dessa abordagem é que o modelo resultante só pode ser executado em janelas de entrada exatamente com 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)
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:
- As
layers.Flatten
e as primeiraslayers.Dense
são substituídas porlayers.Conv1D
. - O
layers.Reshape
não é mais necessário, pois a convolução mantém o eixo do tempo em sua saída.
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 [==============================] - 2s 3ms/step - loss: 0.0068 - mean_absolute_error: 0.0576
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:
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 é menor 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 nas 3 etapas anteriores:
wide_conv_window.plot(conv_model)
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 importante argumento do construtor para todas as camadas return_sequences
RNN é o argumento return_sequences
. Esta configuração pode configurar a camada de duas maneiras.
- 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:
- 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.
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 [==============================] - 2s 4ms/step - loss: 0.0055 - mean_absolute_error: 0.0509
wide_window.plot(lstm_model)
atuação
Com esse conjunto de dados, normalmente cada um dos modelos se sai um pouco 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()
for name, value in performance.items():
print(f'{name:12s}: {value[1]:0.4f}')
Baseline : 0.0852 Linear : 0.0686 Dense : 0.0581 Multi step dense: 0.0577 Conv : 0.0598 LSTM : 0.0516
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.0883 - 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 [==============================] - 2s 3ms/step - loss: 0.0687 - mean_absolute_error: 0.1342
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 [==============================] - 2s 4ms/step - loss: 0.0614 - mean_absolute_error: 0.1198 CPU times: user 6min 10s, sys: 1min 25s, total: 7min 35s Wall time: 2min 55s
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 alteração 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 adiciona ao resultado acumulado do modelo.
É assim que você aproveita o conhecimento de que a mudança deve ser pequena.
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.1180 CPU times: user 2min 25s, sys: 34.6 s, total: 3min Wall time: 1min 7s
atuação
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()
for name, value in performance.items():
print(f'{name:15s}: {value[1]:0.4f}')
Baseline : 0.1638 Dense : 0.1349 LSTM : 0.1214 Residual LSTM : 0.1192
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 , uma hora 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:
- Previsões de disparo único em que toda a série temporal é prevista de uma só vez.
- Predições autorregressivas em que o modelo faz apenas 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. No entanto, 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
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:
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.6279 - mean_absolute_error: 0.5001
Uma vez que esta tarefa é prever 24h dadas 24h, outra abordagem simples é repetir o dia anterior, assumindo que amanhã será semelhante:
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 3ms/step - loss: 0.4262 - mean_absolute_error: 0.3955
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 de sequência inteira em uma única etapa.
Isso pode ser implementado de forma eficiente como 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 OUTPUT_STEPS
tempo de OUTPUT_STEPS
, a partir de um único intervalo de tempo de entrada com uma projeção linear. Ele só pode capturar uma fatia de baixa dimensão do comportamento, provavelmente com base principalmente na hora do dia e na época do ano.
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.2559 - mean_absolute_error: 0.3049
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.2190 - mean_absolute_error: 0.2821
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, uma vez que pode ver como as coisas estão mudando 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.2171 - mean_absolute_error: 0.2814
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
.
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.2170 - mean_absolute_error: 0.2856
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. Então, 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á na construção de um modelo que foi explicitamente treinado para fazer isso.
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, de modo que use 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 prediçã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.2240 - mean_absolute_error: 0.2982
atuação
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()
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 etapas 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.2979 Dense : 0.2769 Conv : 0.2750 LSTM : 0.2797 AR LSTM : 0.2904
Os ganhos obtidos 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.
- Para maior compreensão, consulte:
- Capítulo 15 do aprendizado de máquina prático com Scikit-Learn, Keras e TensorFlow , 2ª edição
- Capítulo 6 de Aprendizado profundo com Python .
- Lição 8 da introdução da Udacity ao TensorFlow para aprendizado profundo e cadernos de exercícios
- Lembre-se também de que você pode implementar qualquer modelo de série temporal clássico no TensorFlow. Este tutorial se concentra apenas na funcionalidade integrada do TensorFlow.