Oglądaj prezentacje, sesje produktowe, warsztaty i nie tylko z playlisty Google I / O See

Prognozowanie szeregów czasowych

Zobacz na TensorFlow.org Uruchom w Google Colab Wyświetl źródło w serwisie GitHub Pobierz notatnik

Ten samouczek jest wprowadzeniem do prognozowania szeregów czasowych przy użyciu TensorFlow. Buduje kilka różnych stylów modeli, w tym konwolucyjne i rekurencyjne sieci neuronowe (CNN i RNN).

Jest to omówione w dwóch głównych częściach, z podrozdziałami:

  • Prognoza dla pojedynczego kroku czasowego:
    • Jedna funkcja.
    • Wszystkie funkcje.
  • Prognozuj wiele kroków:
    • Pojedynczy strzał: wszystkie prognozy naraz.
    • Autoregresja: wykonaj jedną prognozę na raz i przekaż dane wyjściowe z powrotem do modelu.

Ustawiać

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

Zbiór danych pogodowych

W tym samouczku wykorzystano zestaw danych szeregów czasowych dotyczących pogody zarejestrowany przez Instytut Biogeochemii im . Maxa Plancka .

Ten zestaw danych zawiera 14 różnych funkcji, takich jak temperatura powietrza, ciśnienie atmosferyczne i wilgotność. Były one zbierane co 10 minut, począwszy od 2003 r. Aby zwiększyć wydajność, będą używane tylko dane zebrane w latach 2009–2016. Ta sekcja zbioru danych została przygotowana przez François Cholleta na potrzeby jego książki 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

Ten samouczek zajmie się tylko prognozami godzinowymi , więc zacznij od podpróbkowania danych z 10-minutowych interwałów do 1 godziny:

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

Rzućmy okiem na dane. Oto kilka pierwszych wierszy:

df.head()

Oto ewolucja kilku funkcji w czasie.

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

Sprawdź i wyczyść

Następnie spójrz na statystyki zbioru danych:

df.describe().transpose()

Prędkość wiatru

Jedyną rzeczą, która powinna się wyróżniać, jest min wartość prędkości wiatru, wv (m/s) i max. wv (m/s) kolumny. To -9999 jest prawdopodobnie błędne. Jest oddzielna kolumna kierunku wiatru, więc prędkość powinna wynosić >=0 . Zastąp go zerami:

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

Inżynieria cech

Przed przystąpieniem do tworzenia modelu ważne jest, aby zrozumieć swoje dane i upewnić się, że przekazujesz modelowi odpowiednio sformatowane dane.

Wiatr

Ostatnia kolumna danych, wd (deg) , podaje kierunek wiatru w stopniach. Kąty nie są dobrym wyborem dla modelu, 360 ° i 0 ° powinny być blisko siebie i płynnie się zawijać. Kierunek nie powinien mieć znaczenia, jeśli wiatr nie wieje.

W tej chwili rozkład danych o wietrze wygląda następująco:

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

Ale modelowi łatwiej będzie to zinterpretować, jeśli przekonwertujesz kolumny kierunku i prędkości wiatru na wektor wiatru:

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)

Rozkład wektorów wiatru jest znacznie prostszy, aby model mógł poprawnie zinterpretować.

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

Czas

Podobnie kolumna Date Time jest bardzo przydatna, ale nie w tej postaci ciągu. Zacznij od konwersji na sekundy:

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

Podobnie jak w przypadku kierunku wiatru, czas w sekundach nie jest użyteczną wartością wejściową modelu. Będąc danymi pogodowymi, ma wyraźną częstotliwość dzienną i roczną. Istnieje wiele sposobów radzenia sobie z okresowością.

Prostym podejściem do przekształcenia go na sygnał użyteczny jest użycie sin i cos do konwersji czasu w celu usunięcia sygnałów „Pora dnia” i „Pora roku”:

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

Daje to modelowi dostęp do najważniejszych cech częstotliwości. W tym przypadku wiedziałeś z wyprzedzeniem, które częstotliwości są ważne.

Jeśli nie wiesz, możesz określić, które częstotliwości są ważne, używając fft . Aby sprawdzić nasze założenia, oto tf.signal.rfft temperatury w czasie. Zwróć uwagę na oczywiste szczyty przy częstotliwościach bliskich 1/year i 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

Podziel dane

Użyjemy podziału (70%, 20%, 10%) dla zestawów uczących, walidacyjnych i testowych. Zwróć uwagę, że dane nie są losowo tasowane przed podzieleniem. Dzieje się tak z dwóch powodów.

  1. Zapewnia to, że nadal możliwe jest dzielenie danych na okna kolejnych próbek.
  2. Zapewnia, że ​​wyniki walidacji / testów są bardziej realistyczne i są oceniane na podstawie danych zebranych po przeszkoleniu modelu.
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]

Normalizuj dane

Ważne jest, aby skalować funkcje przed uczeniem sieci neuronowej. Normalizacja jest powszechnym sposobem wykonywania tego skalowania. Odejmij średnią i podziel przez odchylenie standardowe każdej cechy.

Średnią i odchylenie standardowe należy obliczać wyłącznie przy użyciu danych uczących, aby modele nie miały dostępu do wartości w zestawach walidacyjnych i testowych.

Można również argumentować, że model nie powinien mieć dostępu do przyszłych wartości w zbiorze uczącym podczas treningu i że ta normalizacja powinna być przeprowadzona przy użyciu średnich kroczących. Nie na tym skupia się ten samouczek, a zestawy walidacyjne i testowe zapewniają uzyskanie (w pewnym stopniu) rzetelnych wskaźników. Dlatego w interesie prostoty w tym samouczku zastosowano prostą średnią.

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

Teraz spójrz na rozkład funkcji. Niektóre funkcje mają długie ogony, ale nie ma oczywistych błędów, takich jak wartość prędkości wiatru -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

Okienkowanie danych

Modele w tym samouczku utworzą zestaw prognoz na podstawie okna kolejnych próbek z danych.

Główne cechy okien wprowadzania to:

  • Szerokość (liczba przedziałów czasowych) okien wprowadzania i etykiet
  • Przesunięcie czasu między nimi.
  • Które funkcje są używane jako dane wejściowe, etykiety lub oba.

Ten samouczek tworzy różne modele (w tym modele liniowe, DNN, CNN i RNN) i używa ich do obu:

  • Prognozy dla jednego i wielu wyjść .
  • Prognozy oparte na jednym i wielu krokach w czasie .

Ta sekcja koncentruje się na implementacji okienek danych, aby można je było ponownie wykorzystać we wszystkich tych modelach.

W zależności od zadania i typu modelu możesz chcieć wygenerować różne okna danych. Oto kilka przykładów:

  1. Na przykład, aby wykonać pojedynczą prognozę za 24 godziny w przyszłość, biorąc pod uwagę 24 godziny historii, możesz zdefiniować takie okno:

    Jedna prognoza za 24 godziny w przyszłość.

  2. Model, który przewiduje prognozę 1 godzinę w przyszłość, biorąc pod uwagę 6 godzin historii, wymagałby takiego okna:

    Jedna prognoza na 1 godzinę w przyszłość.

W pozostałej części tej sekcji zdefiniowano klasę WindowGenerator . Ta klasa może:

  1. Obsługuj indeksy i przesunięcia, jak pokazano na diagramach powyżej.
  2. Podziel okna elementów na pary (features, labels) .
  3. Wykreśl zawartość powstałych okien.
  4. Efektywnie generuj partie tych okien na podstawie danych szkoleniowych, ewaluacyjnych i testowych, korzystając ztf.data.Dataset s.

1. Indeksy i przesunięcia

Zacznij od utworzenia klasy WindowGenerator . Metoda __init__ zawiera całą niezbędną logikę dla indeksów wejściowych i etykiet.

Pobiera również ramki danych pociągu, oceny i testowania jako dane wejściowe. Zostaną one później przekonwertowane natf.data.Dataset systemu Windows.

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

Oto kod służący do tworzenia 2 okien pokazanych na diagramach na początku tej sekcji:

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

Mając listę kolejnych wejść, metoda split_window przekształci je w okno wejść i okno etykiet.

Przykładowy w2 powyżej zostanie podzielony w następujący sposób:

Początkowe okno to wszystkie kolejne próbki, co powoduje podzielenie go na pary (wejścia, etykiety)

Ten diagram nie pokazuje osi features danych, ale ta funkcja split_window obsługuje również label_columns więc może być używana zarówno w przykładach z pojedynczym wyjściem, jak i wieloma wyjściami.

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

Wypróbuj to:

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

Zazwyczaj dane w TensorFlow są pakowane w tablice, w których najbardziej zewnętrzny indeks znajduje się w przykładach (wymiar „partii”). Indeksy środkowe to wymiar (y) „czasu” lub „przestrzeni” (szerokość, wysokość). Najbardziej wewnętrzne indeksy to cechy.

Powyższy kod obejmował pakiet 3 okien z 7 krokami czasowymi, z 19 funkcjami na każdym etapie czasowym. Podzielił je na zestaw 6-stopniowych kroków, 19 wejść funkcji i 1-krokową 1-funkcyjną etykietę. Etykieta ma tylko jedną funkcję, ponieważ WindowGenerator został zainicjowany z label_columns=['T (degC)'] . Początkowo ten samouczek pozwoli zbudować modele przewidujące pojedyncze etykiety wyjściowe.

3. Działka

Oto metoda kreślenia, która umożliwia prostą wizualizację podzielonego okna:

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

Ten wykres wyrównuje dane wejściowe, etykiety i (później) prognozy na podstawie czasu, do którego odnosi się element:

w2.plot()

png

Możesz wykreślić inne kolumny, ale przykładowa konfiguracja okna w2 ma tylko etykiety dla kolumny T (degC) .

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

png

4. Utwórztf.data.Dataset s

Na koniec ta metoda make_dataset pobierze szereg DataFrame i przekonwertuje go natf.data.Dataset (input_window, label_window) przy użyciu funkcji 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

Obiekt WindowGenerator przechowuje dane treningowe, walidacyjne i testowe. Dodaj właściwości umożliwiające dostęp do nich jako tf.data.Datasets przy użyciu powyższej metody make_dataset . Dodaj również standardową przykładową partię dla łatwego dostępu i drukowania:

@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

Teraz obiekt WindowGenerator zapewnia dostęp do obiektówtf.data.Dataset , dzięki czemu można łatwo iterować po danych.

Właściwość Dataset.element_spec informuje o strukturze, dtypes i kształtach elementów zestawu danych.

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

Iteracja po zestawie Dataset daje konkretne partie:

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)

Modele jednoetapowe

Najprostszym modelem, jaki można zbudować na podstawie tego rodzaju danych, jest taki, który przewiduje wartość pojedynczej funkcji, 1 krok czasu (1 godz.) W przyszłości na podstawie tylko bieżących warunków.

Zacznij więc od zbudowania modeli do przewidywania wartości T (degC) 1 godzinę w przyszłość.

Wytypuj następny krok czasowy

Skonfiguruj obiekt WindowGenerator aby utworzyć te jednoetapowe pary (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)']

Obiekt window tworzy tf.data.Datasets podstawie tf.data.Datasets , walidacyjnych i testowych, umożliwiając łatwe iterowanie partii danych.

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)

Linia bazowa

Przed zbudowaniem możliwego do nauczenia modelu dobrze byłoby mieć podstawę wydajności jako punkt do porównania z późniejszymi, bardziej skomplikowanymi modelami.

Pierwszym zadaniem jest przewidzenie temperatury w przyszłości w 1h przy aktualnej wartości wszystkich cech. Bieżące wartości obejmują aktualną temperaturę.

Zacznij więc od modelu, który po prostu zwraca bieżącą temperaturę jako prognozę, przewidując „brak zmian”. Jest to rozsądna podstawa, ponieważ temperatura zmienia się powoli. Oczywiście ta linia bazowa będzie działać gorzej, jeśli dokonasz prognozy w przyszłości.

Wyślij dane wejściowe do wyjścia

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]

Utwórz wystąpienie i oceń ten model:

baseline = Baseline(label_index=column_indices['T (degC)'])

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

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

To spowodowało wydrukowanie niektórych wskaźników wydajności, ale nie dają one poczucia, jak dobrze model sobie radzi.

WindowGenerator ma metodę WindowGenerator , ale wykresy nie będą zbyt interesujące z tylko jedną próbką. WindowGenerator więc szerszy WindowGenerator który generuje okna 24- WindowGenerator kolejnych danych wejściowych i etykiet na raz.

wide_window nie zmienia sposobu działania modelu. Model nadal prognozuje 1 godzinę w przyszłość na podstawie pojedynczego wejściowego kroku czasowego. W tym przypadku oś time działa jak oś batch : każde przewidywanie jest wykonywane niezależnie, bez interakcji między krokami czasowymi.

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

To rozszerzone okno można przekazać bezpośrednio do tego samego modelu baseline bez żadnych zmian w kodzie. Jest to możliwe, ponieważ dane wejściowe i etykiety mają taką samą liczbę kroków czasowych, a linia bazowa po prostu przekazuje dane wejściowe do danych wyjściowych:

Jedna przepowiednia 1 godzinę w przyszłość, co godzinę.

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)

Wykreślając prognozy modelu bazowego, widać, że to po prostu etykiety, przesunięte w prawo o 1 godzinę.

wide_window.plot(baseline)

png

Na powyższych wykresach trzech przykładów model jednoetapowy jest uruchamiany w ciągu 24 godzin. To zasługuje na wyjaśnienie:

  • Niebieska linia „Wejścia” pokazuje temperaturę wejściową w każdym kroku czasowym. Model posiada wszystkie cechy, ten wykres pokazuje tylko temperaturę.
  • Zielone kropki „Etykiety” pokazują przewidywaną wartość docelową. Te kropki są wyświetlane w czasie prognozy, a nie w czasie wprowadzania. Dlatego zakres etykiet jest przesuwany o 1 krok w stosunku do danych wejściowych.
  • Pomarańczowe krzyżyki „Prognozy” to prognozy modelu dla każdego wyjściowego kroku czasowego. Gdyby model przewidywał doskonale, prognozy trafiałyby bezpośrednio na „etykiety”.

Model liniowy

Najprostszym możliwym do nauczenia modelem, który można zastosować do tego zadania, jest wstawienie transformacji liniowej między danymi wejściowymi i wyjściowymi. W tym przypadku wynik z kroku czasowego zależy tylko od tego kroku:

Prognozowanie jednoetapowe

layers.Dense bez zestawu activation to model liniowy. Warstwa przekształca tylko ostatnią oś danych z (batch, time, inputs) na (batch, time, units) , jest stosowana niezależnie do każdego elementu na osi batch i 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)

Ten samouczek szkoli wiele modeli, więc spakuj procedurę szkoleniową do funkcji:

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

Wytrenuj model i oceń jego wydajność:

history = compile_and_fit(linear, single_step_window)

val_performance['Linear'] = linear.evaluate(single_step_window.val)
performance['Linear'] = linear.evaluate(single_step_window.test, verbose=0)
Epoch 1/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.3887 - mean_absolute_error: 0.3448 - val_loss: 0.0281 - val_mean_absolute_error: 0.1269
Epoch 2/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0230 - mean_absolute_error: 0.1127 - val_loss: 0.0132 - val_mean_absolute_error: 0.0867
Epoch 3/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0113 - mean_absolute_error: 0.0792 - val_loss: 0.0093 - val_mean_absolute_error: 0.0718
Epoch 4/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0092 - mean_absolute_error: 0.0707 - val_loss: 0.0087 - val_mean_absolute_error: 0.0689
Epoch 5/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0090 - mean_absolute_error: 0.0696 - val_loss: 0.0087 - val_mean_absolute_error: 0.0682
Epoch 6/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0090 - mean_absolute_error: 0.0695 - val_loss: 0.0089 - val_mean_absolute_error: 0.0685
Epoch 7/20
1534/1534 [==============================] - 4s 3ms/step - loss: 0.0090 - mean_absolute_error: 0.0696 - val_loss: 0.0087 - val_mean_absolute_error: 0.0682
439/439 [==============================] - 1s 2ms/step - loss: 0.0087 - mean_absolute_error: 0.0682

Podobnie jak model baseline model liniowy można wywoływać na partiach szerokich okien. Wykorzystany w ten sposób model tworzy zestaw niezależnych prognoz na kolejnych etapach czasowych. Oś time działa jak inna oś batch . Nie ma interakcji między przewidywaniami na każdym etapie czasowym.

Prognozowanie jednoetapowe

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)

Oto wykres jego przykładowych prognoz w oknie wide_window , zwróć uwagę, że w wielu przypadkach prognoza jest wyraźnie lepsza niż tylko zwracanie temperatury wejściowej, ale w kilku przypadkach jest gorsza:

wide_window.plot(linear)

png

Jedną z zalet modeli liniowych jest to, że są one stosunkowo proste w interpretacji. Możesz wyciągnąć wagi warstwy i zobaczyć wagę przypisaną do każdego wejścia:

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

Czasami model nie przykłada nawet największej wagi do wejściowej T (degC) . Jest to jedno z zagrożeń związanych z losową inicjalizacją.

Gęsty

Przed zastosowaniem modeli, które faktycznie działają w wielu krokach czasowych, warto sprawdzić działanie głębszych, mocniejszych modeli z jednym krokiem wejściowym.

Oto model podobny do modelu linear , z tą różnicą, że układa kilka warstw Dense między wejściem a wyjściem:

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.0174 - mean_absolute_error: 0.0826 - val_loss: 0.0078 - val_mean_absolute_error: 0.0640
Epoch 2/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0079 - mean_absolute_error: 0.0643 - val_loss: 0.0079 - val_mean_absolute_error: 0.0656
Epoch 3/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0075 - mean_absolute_error: 0.0625 - val_loss: 0.0072 - val_mean_absolute_error: 0.0615
Epoch 4/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0073 - mean_absolute_error: 0.0614 - val_loss: 0.0073 - val_mean_absolute_error: 0.0631
Epoch 5/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0070 - mean_absolute_error: 0.0598 - val_loss: 0.0068 - val_mean_absolute_error: 0.0584
Epoch 6/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0069 - mean_absolute_error: 0.0593 - val_loss: 0.0079 - val_mean_absolute_error: 0.0645
Epoch 7/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0069 - mean_absolute_error: 0.0590 - val_loss: 0.0070 - val_mean_absolute_error: 0.0602
439/439 [==============================] - 1s 2ms/step - loss: 0.0070 - mean_absolute_error: 0.0602

Gęsty wieloetapowo

Model z pojedynczym krokiem czasowym nie ma kontekstu dla bieżących wartości jego danych wejściowych. Nie widzi, jak funkcje wprowadzania zmieniają się w czasie. Aby rozwiązać ten problem, model musi mieć dostęp do wielu etapów czasowych podczas prognozowania:

Dla każdej prognozy używane są trzy przedziały czasowe.

Model baseline , linear i dense obsługiwane niezależnie dla każdego kroku czasowego. W tym przypadku model będzie wymagał wielu kroków czasowych jako danych wejściowych, aby wygenerować jedno wyjście.

Utwórz WindowGenerator który będzie generował partie z 3 godzin danych wejściowych i 1 godziny etykiet:

Zauważ, że parametr shift Window odnosi się do końca dwóch okien.

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

Możesz wytrenować dense model w oknie z wieloma krokami wejściowymi, dodając layers.Flatten jako pierwszą warstwę modelu:

multi_step_dense = tf.keras.Sequential([
    # Shape: (time, features) => (time*features)
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(units=32, activation='relu'),
    tf.keras.layers.Dense(units=32, activation='relu'),
    tf.keras.layers.Dense(units=1),
    # Add back the time dimension.
    # Shape: (outputs) => (1, outputs)
    tf.keras.layers.Reshape([1, -1]),
])
print('Input shape:', conv_window.example[0].shape)
print('Output shape:', multi_step_dense(conv_window.example[0]).shape)
Input shape: (32, 3, 19)
Output shape: (32, 1, 1)
history = compile_and_fit(multi_step_dense, conv_window)

IPython.display.clear_output()
val_performance['Multi step dense'] = multi_step_dense.evaluate(conv_window.val)
performance['Multi step dense'] = multi_step_dense.evaluate(conv_window.test, verbose=0)
438/438 [==============================] - 1s 2ms/step - loss: 0.0061 - mean_absolute_error: 0.0546
conv_window.plot(multi_step_dense)

png

Główną wadą tego podejścia jest to, że wynikowy model można wykonać tylko w oknach wejściowych o dokładnie takim kształcie.

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)

Modele splotowe w następnej sekcji rozwiązują ten problem.

Neuronowa sieć konwolucyjna

Warstwa splotu ( layers.Conv1D ) również przyjmuje wiele kroków czasowych jako dane wejściowe dla każdej prognozy.

Poniżej znajduje się ten sam model, co multi_step_dense , przepisany za pomocą splotu.

Zwróć uwagę na zmiany:

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

Uruchom go na przykładowej partii, aby zobaczyć, że model generuje dane wyjściowe o oczekiwanym kształcie:

print("Conv model on `conv_window`")
print('Input shape:', conv_window.example[0].shape)
print('Output shape:', conv_model(conv_window.example[0]).shape)
Conv model on `conv_window`
Input shape: (32, 3, 19)
Output shape: (32, 1, 1)

conv_window i oceń go w conv_window i powinien dać wydajność podobną do modelu 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.0072 - mean_absolute_error: 0.0611

Różnica między tym conv_model a modelem multi_step_dense polega na tym, że conv_model może być uruchamiana na danych wejściowych o dowolnej długości. Warstwa splotowa jest nakładana na przesuwne okno danych wejściowych:

Wykonywanie modelu konwolucyjnego na sekwencji

Jeśli uruchomisz go na szerszym wejściu, wygeneruje szerszy wynik:

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)

Zwróć uwagę, że wyjście jest krótsze niż wejście. Aby uczenie lub kreślenie działało, potrzebujesz etykiet i prognoz o tej samej długości. Zbuduj więc WindowGenerator aby tworzyć szerokie okna z kilkoma dodatkowymi krokami czasu wejściowego, aby długość etykiety i prognozy były zgodne:

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)

Teraz możesz wykreślić przewidywania modelu w szerszym oknie. Zwróć uwagę na 3 wejściowe przedziały czasowe przed pierwszą prognozą. Każda prognoza jest oparta na 3 poprzednich krokach czasowych:

wide_conv_window.plot(conv_model)

png

Powtarzająca się sieć neuronowa

Cykliczna sieć neuronowa (RNN) to typ sieci neuronowej dobrze dostosowany do danych szeregów czasowych. Sieci RNN przetwarzają szeregi czasowe krok po kroku, utrzymując stan wewnętrzny od etapu do etapu.

Aby uzyskać więcej informacji, przeczytaj samouczek dotyczący generowania tekstu lub przewodnik RNN .

W tym samouczku użyjesz warstwy RNN o nazwie Long Short Term Memory ( LSTM ).

Ważnym argumentem konstruktora dla wszystkich warstw RNN keras jest argument return_sequences . To ustawienie umożliwia skonfigurowanie warstwy na jeden z dwóch sposobów.

  1. Jeśli wartość False , wartość domyślna, warstwa zwraca wynik tylko z ostatniego kroku czasowego, dając modelowi czas na rozgrzanie swojego stanu wewnętrznego przed wykonaniem pojedynczej prognozy:

Rozgrzewka i zrobienie jednej prognozy

  1. Jeśli True warstwa zwraca dane wyjściowe dla każdego wejścia. Jest to przydatne w przypadku:
    • Układanie warstw RNN.
    • Trenowanie modelu w wielu krokach czasowych jednocześnie.

Lstm dokonujący prognozy po każdym kroku czasowym

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

Z return_sequences=True model można trenować na 24 godzinnych danych naraz.

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

png

Występ

W przypadku tego zbioru danych każdy z modeli działa nieco lepiej niż poprzedni.

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.0666
Dense       : 0.0623
Multi step dense: 0.0552
Conv        : 0.0650
LSTM        : 0.0527

Modele wielowyjściowe

Wszystkie dotychczasowe modele przewidywały pojedynczą funkcję wyjściową, T (degC) , dla pojedynczego kroku czasowego.

Wszystkie te modele można przekonwertować w celu przewidywania wielu cech, po prostu zmieniając liczbę jednostek w warstwie wyjściowej i dostosowując okna szkoleniowe, aby uwzględnić wszystkie funkcje na 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)

Zauważ powyżej, że oś features etykiet ma teraz taką samą głębokość jak dane wejściowe, zamiast 1.

Linia bazowa

W tym miejscu można zastosować ten sam model bazowy, ale tym razem wszystkie funkcje zostaną powtórzone zamiast wybierania określonego label_index .

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

Gęsty

dense = tf.keras.Sequential([
    tf.keras.layers.Dense(units=64, activation='relu'),
    tf.keras.layers.Dense(units=64, activation='relu'),
    tf.keras.layers.Dense(units=num_features)
])
history = compile_and_fit(dense, single_step_window)

IPython.display.clear_output()
val_performance['Dense'] = dense.evaluate(single_step_window.val)
performance['Dense'] = dense.evaluate(single_step_window.test, verbose=0)
439/439 [==============================] - 1s 2ms/step - loss: 0.0680 - mean_absolute_error: 0.1296

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.0615 - mean_absolute_error: 0.1204

CPU times: user 4min 32s, sys: 1min 2s, total: 5min 34s
Wall time: 1min 49s

Zaawansowane: Pozostałe połączenia

Model Baseline z wcześniejszego okresu wykorzystywał fakt, że sekwencja nie zmienia się drastycznie w każdym przedziale czasowym. Każdy model wyszkolony do tej pory w tym samouczku został losowo zainicjowany, a następnie musiał się nauczyć, że wynik jest niewielką zmianą w stosunku do poprzedniego kroku czasowego.

Chociaż możesz obejść ten problem, ostrożnie inicjując, łatwiej jest wbudować to w strukturę modelu.

W analizie szeregów czasowych często buduje się modele, które zamiast przewidywać następną wartość, przewidują, jak ta wartość zmieni się w następnym kroku czasowym. Podobnie „sieci szczątkowe” lub „sieci zerowe” w uczeniu głębokim odnoszą się do architektur, w których każda warstwa dodaje się do kumulatywnego wyniku modelu.

W ten sposób wykorzystujesz wiedzę, że zmiana powinna być niewielka.

Model z resztkowym połączeniem

Zasadniczo inicjuje to model w celu dopasowania do Baseline . W tym zadaniu pomaga modelom zbiegać się szybciej, z nieco lepszą wydajnością.

Tego podejścia można używać w połączeniu z dowolnym modelem omówionym w tym samouczku.

Tutaj jest on stosowany do modelu LSTM, zwróć uwagę na użycie tf.initializers.zeros aby upewnić się, że początkowe przewidywane zmiany są niewielkie i nie obezwładniają szczątkowego połączenia. Nie ma tutaj obaw związanych z łamaniem symetrii gradientów, ponieważ zeros są używane tylko na ostatniej warstwie.

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.0621 - mean_absolute_error: 0.1178

CPU times: user 2min, sys: 27.8 s, total: 2min 28s
Wall time: 49.1 s

Występ

Oto ogólna wydajność tych modeli wielowyjściowych.

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.1317
LSTM           : 0.1219
Residual LSTM  : 0.1189

Powyższe wydajności są uśrednione dla wszystkich wyników modelu.

Modele wieloetapowe

Zarówno modele jednowyjściowe, jak i wielowyjściowe w poprzednich sekcjach przewidywały pojedynczy krok w czasie , 1 godzinę w przyszłość.

W tej sekcji omówiono, jak rozszerzyć te modele, aby uzyskać prognozy dotyczące wielu kroków czasowych .

W prognozowaniu wieloetapowym model musi nauczyć się przewidywać zakres przyszłych wartości. Dlatego w przeciwieństwie do modelu jednoetapowego, w którym przewiduje się tylko jeden przyszły punkt, model wieloetapowy przewiduje sekwencję przyszłych wartości.

Istnieją dwa przybliżone podejścia do tego:

  1. Prognozy jednostrzałowe, w których przewidywane są jednocześnie całe szeregi czasowe.
  2. Prognozy autoregresywne, w których model dokonuje tylko jednoetapowych prognoz, a jego dane wyjściowe są przekazywane jako dane wejściowe.

W tej sekcji wszystkie modele przewidują wszystkie cechy we wszystkich wyjściowych krokach czasowych .

W przypadku modelu wieloetapowego dane uczące ponownie składają się z próbek godzinowych. Jednak tutaj modele nauczą się przewidywać 24 godziny w przyszłości, biorąc pod uwagę 24 godziny w przeszłości.

Oto obiekt Window który generuje te wycinki z zestawu danych:

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

Linie bazowe

Prostą podstawą tego zadania jest powtórzenie ostatniego wejściowego kroku czasowego dla wymaganej liczby wyjściowych kroków czasowych:

Powtórz ostatnie wejście dla każdego kroku wyjściowego

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

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

multi_val_performance = {}
multi_performance = {}

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

png

Ponieważ zadanie to polega na przewidywaniu 24 godziny na dobę, innym prostym podejściem jest powtórzenie dnia poprzedniego, zakładając, że jutro będzie podobnie:

Powtórz poprzedni dzień

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

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

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

png

Modele jednostrzałowe

Jednym z wysokopoziomowych podejść do tego problemu jest użycie modelu „pojedynczego strzału”, w którym model przewiduje prognozowanie całej sekwencji w jednym kroku.

Można to efektywnie zaimplementować jako layers.Dense z OUT_STEPS*features jednostki wyjściowe. Model musi tylko zmienić kształt tych danych wyjściowych na wymagane (OUTPUT_STEPS, features) .

Liniowy

Prosty model liniowy oparty na ostatnim wejściowym przedziale czasowym działa lepiej niż którakolwiek z linii bazowych, ale ma zbyt małą moc. Model musi przewidywać kroki czasowe OUTPUT_STEPS z pojedynczego wejściowego kroku czasowego z rzutowaniem liniowym. Może uchwycić tylko niskowymiarowy wycinek zachowania, prawdopodobnie oparty głównie na porze dnia i roku.

Przewiduj wszystkie kroki czasowe od ostatniego kroku czasowego

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.2552 - mean_absolute_error: 0.3054

png

Gęsty

Dodawanie layers.Dense między wejściem a wyjściem daje modelowi liniowemu większą moc, ale nadal opiera się tylko na pojedynczym przedziale czasu na wejściu.

multi_dense_model = tf.keras.Sequential([
    # Take the last time step.
    # Shape [batch, time, features] => [batch, 1, features]
    tf.keras.layers.Lambda(lambda x: x[:, -1:, :]),
    # Shape => [batch, 1, dense_units]
    tf.keras.layers.Dense(512, activation='relu'),
    # Shape => [batch, out_steps*features]
    tf.keras.layers.Dense(OUT_STEPS*num_features,
                          kernel_initializer=tf.initializers.zeros()),
    # Shape => [batch, out_steps, features]
    tf.keras.layers.Reshape([OUT_STEPS, num_features])
])

history = compile_and_fit(multi_dense_model, multi_window)

IPython.display.clear_output()
multi_val_performance['Dense'] = multi_dense_model.evaluate(multi_window.val)
multi_performance['Dense'] = multi_dense_model.evaluate(multi_window.test, verbose=0)
multi_window.plot(multi_dense_model)
437/437 [==============================] - 1s 2ms/step - loss: 0.2199 - mean_absolute_error: 0.2813

png

CNN

Model splotowy tworzy prognozy na podstawie historii o stałej szerokości, co może prowadzić do lepszych wyników niż model gęsty, ponieważ pozwala zobaczyć, jak rzeczy zmieniają się w czasie:

Model splotowy pokazuje, jak rzeczy zmieniają się w czasie

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

history = compile_and_fit(multi_conv_model, multi_window)

IPython.display.clear_output()

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

png

RNN

Model rekurencyjny może nauczyć się korzystać z długiej historii danych wejściowych, jeśli ma to znaczenie dla przewidywań tworzonych przez model. Tutaj model będzie akumulował stan wewnętrzny przez 24 godziny, przed wykonaniem pojedynczej prognozy na następne 24 godziny.

W tym pojedynczym formacie LSTM musi generować dane wyjściowe tylko w ostatnim kroku czasowym, więc ustaw return_sequences=False .

Lstm gromadzi stan w oknie wejściowym i dokonuje pojedynczej prognozy na następne 24 godziny

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

history = compile_and_fit(multi_lstm_model, multi_window)

IPython.display.clear_output()

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

png

Zaawansowany: model autoregresyjny

Wszystkie powyższe modele przewidują całą sekwencję wyjściową w jednym kroku.

W niektórych przypadkach może być pomocne, aby model rozłożył tę prognozę na poszczególne kroki czasowe. Następnie dane wyjściowe każdego modelu mogą zostać przekazane z powrotem do siebie na każdym kroku, a przewidywania można uzależnić od poprzedniego, tak jak w klasycznym generowaniu sekwencji z powtarzającymi się sieciami neuronowymi .

Jedną z wyraźnych zalet tego stylu modelu jest to, że można go skonfigurować do wytwarzania wydruków o różnej długości.

Możesz wziąć dowolny z jednoetapowych modeli wielowyjściowych wyszkolonych w pierwszej połowie tego samouczka i uruchomić w autoregresywnej pętli sprzężenia zwrotnego, ale tutaj skupisz się na budowaniu modelu, który został wyraźnie przeszkolony do tego.

Przekaż dane wyjściowe modelu do jego danych wejściowych

RNN

Ten samouczek buduje tylko autoregresywny model RNN, ale ten wzorzec można zastosować do dowolnego modelu zaprojektowanego do generowania pojedynczego kroku czasowego.

Model będzie miał taką samą podstawową postać jak jednoetapowe modele LSTM : LSTM po LSTM następują layers.Dense który przekształca wyniki LSTM w przewidywania modelu.

layers.LSTM to layers.LSTMCell opakowane w layers.LSTMCell wyższego layers.RNN który zarządza stanem i wynikami sekwencji za Ciebie (szczegóły w Keras RNNs ).

W tym przypadku model musi ręcznie zarządzać wejściami dla każdego kroku, więc używa layers.LSTMCell bezpośrednio dla interfejsu pojedynczego kroku niższego poziomu.

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)

Pierwszą metodą, której ten model potrzebuje, jest metoda warmup celu zainicjowania jego stanu wewnętrznego na podstawie danych wejściowych. Po przeszkoleniu ten stan będzie przechwytywał odpowiednie części historii danych wejściowych. Jest to odpowiednik jednoetapowego modelu LSTM z wcześniejszych wersji:

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

Ta metoda zwraca prognozę pojedynczego kroku czasowego i stan wewnętrzny LSTM:

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

Mając stan RNN i wstępną prognozę, możesz teraz kontynuować iterację modelu, dostarczając prognozy na każdym kroku wstecz jako dane wejściowe.

Najprostszym podejściem do zbierania prognoz wyjściowych jest użycie listy Pythona i tf.stack po pętli.

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

Przetestuj ten model na przykładowych danych wejściowych:

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

Teraz wytrenuj model:

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.2274 - mean_absolute_error: 0.3005

png

Występ

Wyniki tego problemu wyraźnie maleją w zależności od złożoności modelu.

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

Metryki dotyczące modeli z wieloma wyjściami w pierwszej połowie tego samouczka przedstawiają uśrednioną wydajność wszystkich funkcji wyjściowych. Te wyniki są podobne, ale również uśrednione w etapach wyjściowych.

for name, value in multi_performance.items():
  print(f'{name:8s}: {value[1]:0.4f}')
Last    : 0.5157
Repeat  : 0.3774
Linear  : 0.2980
Dense   : 0.2751
Conv    : 0.2769
LSTM    : 0.2768
AR LSTM : 0.2936

Zyski osiągnięte przy przechodzeniu od modelu gęstego do modeli konwolucyjnych i rekurencyjnych wynoszą tylko kilka procent (jeśli w ogóle), a model autoregresyjny radził sobie wyraźnie gorzej. Więc te bardziej złożone metody mogą nie być warto na ten problem, ale nie było sposobu, aby wiedzieć, nie próbując, a modele te mogą być pomocne dla Twojego problemu.

Następne kroki

Ten samouczek był szybkim wprowadzeniem do prognozowania szeregów czasowych przy użyciu TensorFlow.