חיזוי סדרות זמן

הצג באתר TensorFlow.org הפעל בגוגל קולאב צפה במקור ב-GitHubהורד מחברת

מדריך זה הוא מבוא לחיזוי סדרות זמן באמצעות TensorFlow. זה בונה כמה סגנונות שונים של מודלים, כולל רשתות עצביות קונבולוציוניות וחוזרות (CNNs ו-RNNs).

זה מכוסה בשני חלקים עיקריים, עם סעיפים משנה:

  • תחזית לשלב זמן בודד:
    • תכונה בודדת.
    • כל התכונות.
  • תחזית שלבים מרובים:
    • צילום יחיד: בצע את התחזיות בבת אחת.
    • אוטורגרסיבי: בצע חיזוי אחד בכל פעם והזן את הפלט בחזרה למודל.

להכין

import os
import datetime

import IPython
import IPython.display
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import tensorflow as tf

mpl.rcParams['figure.figsize'] = (8, 6)
mpl.rcParams['axes.grid'] = False

מערך מזג האוויר

הדרכה זו משתמשת במערך נתונים מסדרת זמן של מזג אוויר שתועדה על ידי מכון מקס פלנק לביוגיאוכימיה .

מערך נתונים זה מכיל 14 תכונות שונות כגון טמפרטורת אוויר, לחץ אטמוספרי ולחות. אלה נאספו כל 10 דקות, החל משנת 2003. לצורך יעילות, תשתמש רק בנתונים שנאספו בין 2009 ל-2016. חלק זה של מערך הנתונים הוכן על ידי פרנסואה צ'ולט עבור ספרו 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 [==============================] - 1s 0us/step
13582336/13568290 [==============================] - 1s 0us/step

מדריך זה יעסוק רק בתחזיות לפי שעה , אז התחל בדגימת משנה של הנתונים ממרווחים של 10 דקות למרווחים של שעה אחת:

df = pd.read_csv(csv_path)
# Slice [start:stop:step], starting from index 5 take every 6th record.
df = df[5::6]

date_time = pd.to_datetime(df.pop('Date Time'), format='%d.%m.%Y %H:%M:%S')

בואו נסתכל על הנתונים. להלן השורות הראשונות:

df.head()

להלן ההתפתחות של כמה תכונות לאורך זמן:

plot_cols = ['T (degC)', 'p (mbar)', 'rho (g/m**3)']
plot_features = df[plot_cols]
plot_features.index = date_time
_ = plot_features.plot(subplots=True)

plot_features = df[plot_cols][:480]
plot_features.index = date_time[:480]
_ = plot_features.plot(subplots=True)

png

png

לבדוק ולנקות

לאחר מכן, עיין בסטטיסטיקה של מערך הנתונים:

df.describe().transpose()

מהירות הרוח

דבר אחד שצריך לבלוט הוא הערך min של מהירות הרוח ( wv (m/s) ) והערך המקסימלי ( max. wv (m/s) ) . סביר להניח -9999 זה שגוי.

יש עמודת כיוון רוח נפרדת, כך שהמהירות צריכה להיות גדולה מאפס ( >=0 ). החלף אותו באפסים:

wv = df['wv (m/s)']
bad_wv = wv == -9999.0
wv[bad_wv] = 0.0

max_wv = df['max. wv (m/s)']
bad_max_wv = max_wv == -9999.0
max_wv[bad_max_wv] = 0.0

# The above inplace edits are reflected in the DataFrame.
df['wv (m/s)'].min()
0.0

הנדסת תכונות

לפני שצולל לבניית מודל, חשוב להבין את הנתונים שלך ולהיות בטוח שאתה מעביר את המודל בפורמט מתאים.

רוּחַ

העמודה האחרונה של הנתונים, wd (deg) -נותנת את כיוון הרוח ביחידות של מעלות. זוויות אינן מהוות כניסות טובות לדגם: 360° ו-0° צריכות להיות קרובות אחת לשניה ולהתעטף בצורה חלקה. הכיוון לא צריך להיות משנה אם הרוח לא נושבת.

כרגע ההתפלגות של נתוני הרוח נראית כך:

plt.hist2d(df['wd (deg)'], df['wv (m/s)'], bins=(50, 50), vmax=400)
plt.colorbar()
plt.xlabel('Wind Direction [deg]')
plt.ylabel('Wind Velocity [m/s]')
Text(0, 0.5, 'Wind Velocity [m/s]')

png

אבל זה יהיה קל יותר למודל לפרש אם תמיר את עמודות כיוון הרוח והמהירות לוקטור רוח:

wv = df.pop('wv (m/s)')
max_wv = df.pop('max. wv (m/s)')

# Convert to radians.
wd_rad = df.pop('wd (deg)')*np.pi / 180

# Calculate the wind x and y components.
df['Wx'] = wv*np.cos(wd_rad)
df['Wy'] = wv*np.sin(wd_rad)

# Calculate the max wind x and y components.
df['max Wx'] = max_wv*np.cos(wd_rad)
df['max Wy'] = max_wv*np.sin(wd_rad)

ההתפלגות של וקטורי הרוח היא הרבה יותר פשוטה עבור המודל לפרש נכון:

plt.hist2d(df['Wx'], df['Wy'], bins=(50, 50), vmax=400)
plt.colorbar()
plt.xlabel('Wind X [m/s]')
plt.ylabel('Wind Y [m/s]')
ax = plt.gca()
ax.axis('tight')
(-11.305513973134667, 8.24469928549079, -8.27438540335515, 7.7338312955467785)

png

זְמַן

באופן דומה, העמודה Date Time שימושית מאוד, אבל לא בצורת מחרוזת זו. התחל על ידי המרתו לשניות:

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

בדומה לכיוון הרוח, הזמן בשניות אינו קלט שימושי בדגם. בהיותם נתוני מזג אוויר, יש להם מחזוריות יומית ושנתית ברורה. ישנן דרכים רבות להתמודד עם מחזוריות.

אתה יכול לקבל אותות שמיש על ידי שימוש בהמרות סינוס וקוסינוס כדי לנקות אותות "שעה ביום" ו"זמן בשנה":

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

זה נותן לדגם גישה לתכונות התדר החשובות ביותר. במקרה זה ידעת מבעוד מועד אילו תדרים חשובים.

אם אין לך מידע זה, אתה יכול לקבוע אילו תדרים חשובים על ידי חילוץ תכונות עם Fast Fourier Transform . כדי לבדוק את ההנחות, הנה ה- tf.signal.rfft של הטמפרטורה לאורך זמן. שימו לב לשיאים הברורים בתדרים הקרובים ל 1/year ו 1/day :

fft = tf.signal.rfft(df['T (degC)'])
f_per_dataset = np.arange(0, len(fft))

n_samples_h = len(df['T (degC)'])
hours_per_year = 24*365.2524
years_per_dataset = n_samples_h/(hours_per_year)

f_per_year = f_per_dataset/years_per_dataset
plt.step(f_per_year, np.abs(fft))
plt.xscale('log')
plt.ylim(0, 400000)
plt.xlim([0.1, max(plt.xlim())])
plt.xticks([1, 365.2524], labels=['1/Year', '1/day'])
_ = plt.xlabel('Frequency (log scale)')

png

פצל את הנתונים

אתה תשתמש בחלוקה (70%, 20%, 10%) עבור מערכי ההדרכה, האימות והמבחנים. שימו לב שהנתונים לא עוברים ערבוב אקראי לפני הפיצול. זה משתי סיבות:

  1. זה מבטיח שקיצוץ הנתונים לחלונות של דגימות עוקבות עדיין אפשרי.
  2. זה מבטיח שתוצאות האימות/בדיקה יהיו מציאותיות יותר, תוך הערכה על פי הנתונים שנאספו לאחר הכשרת המודל.
column_indices = {name: i for i, name in enumerate(df.columns)}

n = len(df)
train_df = df[0:int(n*0.7)]
val_df = df[int(n*0.7):int(n*0.9)]
test_df = df[int(n*0.9):]

num_features = df.shape[1]

נרמל את הנתונים

חשוב להתאים תכונות לפני אימון רשת עצבית. נורמליזציה היא דרך נפוצה לעשות קנה מידה זה: מפחיתים את הממוצע ומחלקים בסטיית התקן של כל תכונה.

יש לחשב את הממוצע ואת סטיית התקן רק באמצעות נתוני האימון כך שלמודלים אין גישה לערכים בערכות האימות והבדיקות.

אפשר גם לטעון שלמודל לא צריכה להיות גישה לערכים עתידיים במערך האימונים בעת האימון, ושנורמליזציה זו צריכה להיעשות באמצעות ממוצעים נעים. זה לא המוקד של המדריך הזה, וערכות האימות והבדיקות מבטיחות שתקבל מדדים כנים (קצת). אז, למען הפשטות, הדרכה זו משתמשת בממוצע פשוט.

train_mean = train_df.mean()
train_std = train_df.std()

train_df = (train_df - train_mean) / train_std
val_df = (val_df - train_mean) / train_std
test_df = (test_df - train_mean) / train_std

כעת, הציצו בהפצת התכונות. לחלק מהתכונות יש זנבות ארוכים, אבל אין שגיאות ברורות כמו ערך מהירות הרוח -9999 .

df_std = (df - train_mean) / train_std
df_std = df_std.melt(var_name='Column', value_name='Normalized')
plt.figure(figsize=(12, 6))
ax = sns.violinplot(x='Column', y='Normalized', data=df_std)
_ = ax.set_xticklabels(df.keys(), rotation=90)

png

חלונות נתונים

המודלים במדריך זה יבצעו קבוצה של תחזיות על סמך חלון של דגימות עוקבות מהנתונים.

המאפיינים העיקריים של חלונות הקלט הם:

  • הרוחב (מספר שלבי הזמן) של חלונות הקלט והתווית.
  • קיזוז הזמן ביניהם.
  • אילו תכונות משמשות כקלט, תוויות או שניהם.

מדריך זה בונה מגוון דגמים (כולל דגמי ליניארי, DNN, CNN ו-RNN), ומשתמש בשניהם:

  • חיזויים של פלט בודד ורב פלט .
  • תחזיות של צעדים חד-פעמיים ורב-פעמיים .

סעיף זה מתמקד ביישום חלונות הנתונים כך שניתן לעשות בו שימוש חוזר עבור כל הדגמים הללו.

בהתאם למשימה ולסוג הדגם, ייתכן שתרצה ליצור מגוון חלונות נתונים. הנה כמה דוגמאות:

  1. לדוגמה, כדי לבצע תחזית בודדת 24 שעות לעתיד, בהינתן 24 שעות של היסטוריה, תוכל להגדיר חלון כזה:

    תחזית אחת 24 שעות לעתיד.

  2. מודל שעושה תחזית שעה אחת לעתיד, בהינתן שש שעות של היסטוריה, יצטרך חלון כזה:

    תחזית אחת שעה אחת לעתיד.

שאר החלק הזה מגדיר מחלקה של WindowGenerator . שיעור זה יכול:

  1. טפל באינדקסים ובקיזוזים כפי שמוצג בתרשימים למעלה.
  2. פיצול חלונות של תכונות לזוגות (features, labels) .
  3. תכנן את התוכן של החלונות שהתקבלו.
  4. הפק ביעילות קבוצות של חלונות אלה מנתוני ההדרכה, ההערכה והבדיקה, באמצעות tf.data.Dataset s.

1. אינדקסים וקיזוזים

התחל ביצירת המחלקה WindowGenerator . שיטת __init__ כוללת את כל ההיגיון הדרוש עבור מדדי הקלט והתוויות.

זה גם לוקח את ההדרכה, ההערכה והבדיקה של DataFrames כקלט. אלה יומרו ל- tf.data.Dataset s של חלונות מאוחר יותר.

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

הנה קוד ליצירת 2 החלונות המוצגים בתרשימים בתחילת סעיף זה:

w1 = WindowGenerator(input_width=24, label_width=1, shift=24,
                     label_columns=['T (degC)'])
w1
Total window size: 48
Input indices: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
Label indices: [47]
Label column name(s): ['T (degC)']
w2 = WindowGenerator(input_width=6, label_width=1, shift=1,
                     label_columns=['T (degC)'])
w2
Total window size: 7
Input indices: [0 1 2 3 4 5]
Label indices: [6]
Label column name(s): ['T (degC)']

2. פיצול

בהינתן רשימה של קלט רצוף, שיטת split_window תמיר אותם לחלון קלט וחלון של תוויות.

הדוגמה של w2 שתגדיר קודם תפוצל כך:

החלון הראשוני הוא כל הדגימות עוקבות, זה מפצל אותו לזוגות (קלט, תוויות)

דיאגרמה זו אינה מציגה את ציר features של הנתונים, אך פונקציית split_window זו מטפלת גם ב- label_columns כך שניתן להשתמש בה הן לדוגמאות של פלט יחיד והן לדוגמאות ריבוי פלט.

def split_window(self, features):
  inputs = features[:, self.input_slice, :]
  labels = features[:, self.labels_slice, :]
  if self.label_columns is not None:
    labels = tf.stack(
        [labels[:, :, self.column_indices[name]] for name in self.label_columns],
        axis=-1)

  # Slicing doesn't preserve static shape information, so set the shapes
  # manually. This way the `tf.data.Datasets` are easier to inspect.
  inputs.set_shape([None, self.input_width, None])
  labels.set_shape([None, self.label_width, None])

  return inputs, labels

WindowGenerator.split_window = split_window

נסה את זה:

# Stack three slices, the length of the total window.
example_window = tf.stack([np.array(train_df[:w2.total_window_size]),
                           np.array(train_df[100:100+w2.total_window_size]),
                           np.array(train_df[200:200+w2.total_window_size])])

example_inputs, example_labels = w2.split_window(example_window)

print('All shapes are: (batch, time, features)')
print(f'Window shape: {example_window.shape}')
print(f'Inputs shape: {example_inputs.shape}')
print(f'Labels shape: {example_labels.shape}')
All shapes are: (batch, time, features)
Window shape: (3, 7, 19)
Inputs shape: (3, 6, 19)
Labels shape: (3, 1, 1)

בדרך כלל, הנתונים ב-TensorFlow נארזים במערכים שבהם האינדקס החיצוני ביותר הוא על פני דוגמאות (המימד "אצווה"). המדדים האמצעיים הם הממדים של "זמן" או "מרחב" (רוחב, גובה). המדדים הפנימיים ביותר הם התכונות.

הקוד שלמעלה לקח אצווה של שלושה חלונות של 7 פעמים עם 19 תכונות בכל שלב זמן. זה מחלק אותם לקבוצה של כניסות של 6-פעמים של 19 תכונות, ותווית שלב 1 של תכונות. לתווית יש רק תכונה אחת מכיוון שה- WindowGenerator אותחל עם label_columns=['T (degC)'] . בתחילה, מדריך זה יבנה מודלים המנבאים תוויות פלט בודדות.

3. עלילה

להלן שיטת עלילה המאפשרת הדמיה פשוטה של ​​החלון המפוצל:

w2.example = example_inputs, example_labels
def plot(self, model=None, plot_col='T (degC)', max_subplots=3):
  inputs, labels = self.example
  plt.figure(figsize=(12, 8))
  plot_col_index = self.column_indices[plot_col]
  max_n = min(max_subplots, len(inputs))
  for n in range(max_n):
    plt.subplot(max_n, 1, n+1)
    plt.ylabel(f'{plot_col} [normed]')
    plt.plot(self.input_indices, inputs[n, :, plot_col_index],
             label='Inputs', marker='.', zorder=-10)

    if self.label_columns:
      label_col_index = self.label_columns_indices.get(plot_col, None)
    else:
      label_col_index = plot_col_index

    if label_col_index is None:
      continue

    plt.scatter(self.label_indices, labels[n, :, label_col_index],
                edgecolors='k', label='Labels', c='#2ca02c', s=64)
    if model is not None:
      predictions = model(inputs)
      plt.scatter(self.label_indices, predictions[n, :, label_col_index],
                  marker='X', edgecolors='k', label='Predictions',
                  c='#ff7f0e', s=64)

    if n == 0:
      plt.legend()

  plt.xlabel('Time [h]')

WindowGenerator.plot = plot

העלילה הזו מיישרת קלט, תוויות וחיזוי (מאוחר יותר) על סמך הזמן שהפריט מתייחס אליו:

w2.plot()

png

אתה יכול לשרטט את העמודות האחרות, אבל לתצורת חלון w2 לדוגמה יש תוויות רק עבור העמודה T (degC) .

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

png

4. צור tf.data.Dataset s

לבסוף, שיטת make_dataset זו תיקח סדרת זמן DataFrame ותמיר אותו ל- tf.data.Dataset של זוגות (input_window, label_window) באמצעות הפונקציה tf.keras.utils.timeseries_dataset_from_array :

def make_dataset(self, data):
  data = np.array(data, dtype=np.float32)
  ds = tf.keras.utils.timeseries_dataset_from_array(
      data=data,
      targets=None,
      sequence_length=self.total_window_size,
      sequence_stride=1,
      shuffle=True,
      batch_size=32,)

  ds = ds.map(self.split_window)

  return ds

WindowGenerator.make_dataset = make_dataset

האובייקט WindowGenerator מכיל נתוני הדרכה, אימות ובדיקה.

הוסף מאפיינים לגישה אליהם כ- tf.data.Dataset s באמצעות שיטת make_dataset שהגדרת קודם לכן. כמו כן, הוסף אצווה דוגמה סטנדרטית לגישה נוחה ותכנון:

@property
def train(self):
  return self.make_dataset(self.train_df)

@property
def val(self):
  return self.make_dataset(self.val_df)

@property
def test(self):
  return self.make_dataset(self.test_df)

@property
def example(self):
  """Get and cache an example batch of `inputs, labels` for plotting."""
  result = getattr(self, '_example', None)
  if result is None:
    # No example batch was found, so get one from the `.train` dataset
    result = next(iter(self.train))
    # And cache it for next time
    self._example = result
  return result

WindowGenerator.train = train
WindowGenerator.val = val
WindowGenerator.test = test
WindowGenerator.example = example

כעת, האובייקט WindowGenerator נותן לך גישה לאובייקטים tf.data.Dataset , כך שתוכל לעבור בקלות על הנתונים.

המאפיין Dataset.element_spec אומר לך את המבנה, סוגי הנתונים והצורות של רכיבי הנתונים.

# Each element is an (inputs, label) pair.
w2.train.element_spec
(TensorSpec(shape=(None, 6, 19), dtype=tf.float32, name=None),
 TensorSpec(shape=(None, 1, 1), dtype=tf.float32, name=None))

איטרציה על Dataset מניב קבוצות בטון:

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

דגמי צעד בודד

המודל הפשוט ביותר שאתה יכול לבנות על סוג זה של נתונים הוא כזה שמנבא ערך של תכונה בודדת - צעד זמן אחד (שעה אחת) לעתיד בהתבסס רק על התנאים הנוכחיים.

אז, התחל בבניית מודלים כדי לחזות את ערך ה- T (degC) שעה אחת לעתיד.

חזה את שלב הזמן הבא

קבע את התצורה של אובייקט WindowGenerator כדי לייצר את צמדי הצעדים האלה (input, label) :

single_step_window = WindowGenerator(
    input_width=1, label_width=1, shift=1,
    label_columns=['T (degC)'])
single_step_window
Total window size: 2
Input indices: [0]
Label indices: [1]
Label column name(s): ['T (degC)']

אובייקט window יוצר tf.data.Dataset s ממערכי ההדרכה, האימות והבדיקות, ומאפשר לך לחזור בקלות על אצווה של נתונים.

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)

קו בסיס

לפני בניית מודל הניתן לאימון, יהיה טוב שיהיה קו בסיס ביצועים כנקודת השוואה עם הדגמים המאוחרים יותר המסובכים יותר.

משימה ראשונה זו היא לחזות טמפרטורה שעה אחת לעתיד, בהתחשב בערך הנוכחי של כל התכונות. הערכים הנוכחיים כוללים את הטמפרטורה הנוכחית.

אז, התחל עם מודל שרק מחזיר את הטמפרטורה הנוכחית בתור החיזוי, מנבא "ללא שינוי". זהו קו בסיס סביר מכיוון שהטמפרטורה משתנה לאט. כמובן, קו הבסיס הזה יעבוד פחות טוב אם תבצע תחזית נוספת בעתיד.

שלח את הקלט לפלט

class Baseline(tf.keras.Model):
  def __init__(self, label_index=None):
    super().__init__()
    self.label_index = label_index

  def call(self, inputs):
    if self.label_index is None:
      return inputs
    result = inputs[:, :, self.label_index]
    return result[:, :, tf.newaxis]

הצג והעריך את המודל הזה:

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

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

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

זה הדפיס כמה מדדי ביצועים, אבל אלה לא נותנים לך הרגשה עד כמה המודל מצליח.

ל- WindowGenerator יש שיטת עלילה, אבל העלילות לא יהיו מעניינות במיוחד עם מדגם בודד בלבד.

אז, צור WindowGenerator רחב יותר שיוצר חלונות 24 שעות של קלט ותוויות רצופות בכל פעם. המשתנה wide_window החדש לא משנה את אופן הפעולה של המודל. המודל עדיין עושה תחזיות שעה אחת לעתיד על סמך שלב זמן קלט בודד. כאן, ציר time פועל כמו ציר batch : כל חיזוי מתבצע באופן עצמאי ללא אינטראקציה בין שלבי זמן:

wide_window = WindowGenerator(
    input_width=24, label_width=24, shift=1,
    label_columns=['T (degC)'])

wide_window
Total window size: 25
Input indices: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
Label indices: [ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]
Label column name(s): ['T (degC)']

ניתן להעביר חלון מורחב זה ישירות לאותו מודל baseline ללא שינויי קוד. זה אפשרי מכיוון שלכניסות והתוויות יש את אותו מספר שלבי זמן, וקו הבסיס פשוט מעביר את הקלט לפלט:

תחזית אחת שעה אחת לתוך העתיד, תמיד שעה.

print('Input shape:', wide_window.example[0].shape)
print('Output shape:', baseline(wide_window.example[0]).shape)
Input shape: (32, 24, 19)
Output shape: (32, 24, 1)

על ידי שרטוט התחזיות של מודל הבסיס, שימו לב שזה פשוט התוויות המוזזות ימינה בשעה אחת:

wide_window.plot(baseline)

png

בתרשים של שלוש דוגמאות לעיל, מודל הצעד הבודד מופעל על פני 24 שעות. זה ראוי להסבר:

  • קו Inputs הכחול מציג את טמפרטורת הכניסה בכל שלב בזמן. הדגם מקבל את כל התכונות, העלילה הזו מציגה רק את הטמפרטורה.
  • נקודות Labels הירוקות מציגות את ערך חיזוי היעד. נקודות אלו מוצגות בזמן החיזוי, לא בזמן הקלט. זו הסיבה שטווח התוויות מוסט שלב אחד ביחס לכניסות.
  • צלבי החיזויים הכתומים הם Predictions המודל עבור כל שלב בזמן הפלט. אם המודל היה חוזה בצורה מושלמת, התחזיות היו נוחתות ישירות על Labels .

דגם ליניארי

המודל הניתן לאימון הפשוט ביותר שתוכל ליישם במשימה זו הוא הוספת טרנספורמציה ליניארית בין הקלט והפלט. במקרה זה הפלט משלב זמן תלוי רק בשלב זה:

תחזית צעד אחד

שכבה tf.keras.layers.Dense ללא סט activation היא מודל ליניארי. השכבה הופכת רק את הציר האחרון של הנתונים מ- (batch, time, inputs) ל- (batch, time, units) ; הוא מיושם באופן עצמאי על כל פריט על פני ציר batch time .

linear = tf.keras.Sequential([
    tf.keras.layers.Dense(units=1)
])
print('Input shape:', single_step_window.example[0].shape)
print('Output shape:', linear(single_step_window.example[0]).shape)
Input shape: (32, 1, 19)
Output shape: (32, 1, 1)

הדרכה זו מכשירה דגמים רבים, אז ארוז את הליך ההדרכה לפונקציה:

MAX_EPOCHS = 20

def compile_and_fit(model, window, patience=2):
  early_stopping = tf.keras.callbacks.EarlyStopping(monitor='val_loss',
                                                    patience=patience,
                                                    mode='min')

  model.compile(loss=tf.losses.MeanSquaredError(),
                optimizer=tf.optimizers.Adam(),
                metrics=[tf.metrics.MeanAbsoluteError()])

  history = model.fit(window.train, epochs=MAX_EPOCHS,
                      validation_data=window.val,
                      callbacks=[early_stopping])
  return history

אימן את המודל והעריך את ביצועיו:

history = compile_and_fit(linear, single_step_window)

val_performance['Linear'] = linear.evaluate(single_step_window.val)
performance['Linear'] = linear.evaluate(single_step_window.test, verbose=0)
Epoch 1/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0586 - mean_absolute_error: 0.1659 - val_loss: 0.0135 - val_mean_absolute_error: 0.0858
Epoch 2/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0109 - mean_absolute_error: 0.0772 - val_loss: 0.0093 - val_mean_absolute_error: 0.0711
Epoch 3/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0092 - mean_absolute_error: 0.0704 - val_loss: 0.0088 - val_mean_absolute_error: 0.0690
Epoch 4/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0091 - mean_absolute_error: 0.0697 - val_loss: 0.0089 - val_mean_absolute_error: 0.0692
Epoch 5/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0091 - mean_absolute_error: 0.0697 - val_loss: 0.0088 - val_mean_absolute_error: 0.0685
Epoch 6/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0091 - mean_absolute_error: 0.0697 - val_loss: 0.0087 - val_mean_absolute_error: 0.0687
Epoch 7/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0091 - mean_absolute_error: 0.0698 - val_loss: 0.0087 - val_mean_absolute_error: 0.0680
Epoch 8/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0090 - mean_absolute_error: 0.0695 - val_loss: 0.0087 - val_mean_absolute_error: 0.0683
Epoch 9/20
1534/1534 [==============================] - 5s 3ms/step - loss: 0.0091 - mean_absolute_error: 0.0696 - val_loss: 0.0087 - val_mean_absolute_error: 0.0684
439/439 [==============================] - 1s 2ms/step - loss: 0.0087 - mean_absolute_error: 0.0684

כמו מודל baseline , ניתן לקרוא למודל הליניארי על קבוצות של חלונות רחבים. בשימוש כך המודל יוצר קבוצה של תחזיות עצמאיות על שלבי זמן עוקבים. ציר time פועל כמו ציר batch אחר. אין אינטראקציות בין התחזיות בכל שלב בזמן.

תחזית צעד אחד

print('Input shape:', wide_window.example[0].shape)
print('Output shape:', baseline(wide_window.example[0]).shape)
Input shape: (32, 24, 19)
Output shape: (32, 24, 1)

הנה העלילה של תחזיות הדוגמה שלו ב- wide_window , שימו לב כיצד במקרים רבים החיזוי בבירור טוב יותר מאשר רק החזרת טמפרטורת הקלט, אך במקרים בודדים זה גרוע יותר:

wide_window.plot(linear)

png

יתרון אחד למודלים ליניאריים הוא שהם פשוטים יחסית לפירוש. אתה יכול לשלוף את משקולות השכבה ולדמיין את המשקל שהוקצה לכל קלט:

plt.bar(x = range(len(train_df.columns)),
        height=linear.layers[0].kernel[:,0].numpy())
axis = plt.gca()
axis.set_xticks(range(len(train_df.columns)))
_ = axis.set_xticklabels(train_df.columns, rotation=90)

png

לפעמים הדגם אפילו לא שם את המשקל הרב ביותר על הקלט T (degC) . זהו אחד הסיכונים של אתחול אקראי.

צָפוּף

לפני יישום מודלים שפועלים בפועל על מספר שלבי זמן, כדאי לבדוק את הביצועים של מודלים עמוקים יותר, חזקים יותר, עם שלבי קלט בודדים.

הנה מודל דומה למודל linear , אלא שהוא מערם כמה שכבות Dense בין הקלט והפלט:

dense = tf.keras.Sequential([
    tf.keras.layers.Dense(units=64, activation='relu'),
    tf.keras.layers.Dense(units=64, activation='relu'),
    tf.keras.layers.Dense(units=1)
])

history = compile_and_fit(dense, single_step_window)

val_performance['Dense'] = dense.evaluate(single_step_window.val)
performance['Dense'] = dense.evaluate(single_step_window.test, verbose=0)
Epoch 1/20
1534/1534 [==============================] - 7s 4ms/step - loss: 0.0132 - mean_absolute_error: 0.0779 - val_loss: 0.0081 - val_mean_absolute_error: 0.0666
Epoch 2/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0081 - mean_absolute_error: 0.0652 - val_loss: 0.0073 - val_mean_absolute_error: 0.0610
Epoch 3/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0076 - mean_absolute_error: 0.0627 - val_loss: 0.0072 - val_mean_absolute_error: 0.0618
Epoch 4/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0072 - mean_absolute_error: 0.0609 - val_loss: 0.0068 - val_mean_absolute_error: 0.0582
Epoch 5/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0072 - mean_absolute_error: 0.0606 - val_loss: 0.0066 - val_mean_absolute_error: 0.0581
Epoch 6/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0070 - mean_absolute_error: 0.0594 - val_loss: 0.0067 - val_mean_absolute_error: 0.0579
Epoch 7/20
1534/1534 [==============================] - 6s 4ms/step - loss: 0.0069 - mean_absolute_error: 0.0590 - val_loss: 0.0068 - val_mean_absolute_error: 0.0580
439/439 [==============================] - 1s 3ms/step - loss: 0.0068 - mean_absolute_error: 0.0580

צפוף רב שלבים

למודל חד-פעמי אין הקשר לערכים הנוכחיים של התשומות שלו. הוא לא יכול לראות כיצד תכונות הקלט משתנות לאורך זמן. כדי לטפל בבעיה זו, המודל זקוק לגישה למספר שלבי זמן בעת ​​ביצוע תחזיות:

שלושה שלבי זמן משמשים עבור כל חיזוי.

המודלים baseline , linear dense טופלו בכל צעד זמן באופן עצמאי. כאן המודל ייקח מספר שלבי זמן כקלט כדי לייצר פלט בודד.

צור WindowGenerator שיפיק אצווה של קלט של שלוש שעות ותוויות של שעה אחת:

שימו לב שפרמטר shift של Window הוא ביחס לסוף שני החלונות.

CONV_WIDTH = 3
conv_window = WindowGenerator(
    input_width=CONV_WIDTH,
    label_width=1,
    shift=1,
    label_columns=['T (degC)'])

conv_window
Total window size: 4
Input indices: [0 1 2]
Label indices: [3]
Label column name(s): ['T (degC)']
conv_window.plot()
plt.title("Given 3 hours of inputs, predict 1 hour into the future.")
Text(0.5, 1.0, 'Given 3 hours of inputs, predict 1 hour into the future.')

png

אתה יכול לאמן מודל dense על חלון שלבים מרובה קלט על ידי הוספת tf.keras.layers.Flatten כשכבה הראשונה של המודל:

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

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

png

הצד השלילי העיקרי של גישה זו הוא שניתן להפעיל את המודל המתקבל רק על חלונות קלט בעלי צורה זו בדיוק.

print('Input shape:', wide_window.example[0].shape)
try:
  print('Output shape:', multi_step_dense(wide_window.example[0]).shape)
except Exception as e:
  print(f'\n{type(e).__name__}:{e}')
Input shape: (32, 24, 19)

ValueError:Exception encountered when calling layer "sequential_2" (type Sequential).

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)

Call arguments received:
  • inputs=tf.Tensor(shape=(32, 24, 19), dtype=float32)
  • training=None
  • mask=None

המודלים הקונבולוציוניים בסעיף הבא פותרים בעיה זו.

רשת נוירונים של Convolution

שכבת קונבולציה ( tf.keras.layers.Conv1D ) גם לוקחת מספר שלבי זמן כקלט לכל חיזוי.

להלן אותו דגם כמו multi_step_dense , שנכתב מחדש עם קונבולציה.

שימו לב לשינויים:

conv_model = tf.keras.Sequential([
    tf.keras.layers.Conv1D(filters=32,
                           kernel_size=(CONV_WIDTH,),
                           activation='relu'),
    tf.keras.layers.Dense(units=32, activation='relu'),
    tf.keras.layers.Dense(units=1),
])

הפעל אותו על אצווה לדוגמה כדי לבדוק שהמודל מייצר פלטים עם הצורה הצפויה:

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

אמן והעריך אותו ב- conv_window והוא אמור לתת ביצועים דומים למודל 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.0063 - mean_absolute_error: 0.0568

ההבדל בין conv_model זה למודל multi_step_dense הוא שניתן להפעיל את conv_model על כניסות בכל אורך. השכבה הקונבולוציונית מוחלת על חלון הזזה של כניסות:

ביצוע מודל קונבולוציוני על רצף

אם אתה מפעיל אותו על קלט רחב יותר, הוא מייצר פלט רחב יותר:

print("Wide window")
print('Input shape:', wide_window.example[0].shape)
print('Labels shape:', wide_window.example[1].shape)
print('Output shape:', conv_model(wide_window.example[0]).shape)
Wide window
Input shape: (32, 24, 19)
Labels shape: (32, 24, 1)
Output shape: (32, 22, 1)

שימו לב שהפלט קצר יותר מהקלט. כדי שהאימונים או התכנון יעבדו, אתה צריך שהתוויות והניבוי יהיו באותו אורך. אז בנו WindowGenerator כדי לייצר חלונות רחבים עם כמה שלבי זמן קלט נוספים כך שהתווית ואורכי החיזוי תואמים:

LABEL_WIDTH = 24
INPUT_WIDTH = LABEL_WIDTH + (CONV_WIDTH - 1)
wide_conv_window = WindowGenerator(
    input_width=INPUT_WIDTH,
    label_width=LABEL_WIDTH,
    shift=1,
    label_columns=['T (degC)'])

wide_conv_window
Total window size: 27
Input indices: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 24 25]
Label indices: [ 3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26]
Label column name(s): ['T (degC)']
print("Wide conv window")
print('Input shape:', wide_conv_window.example[0].shape)
print('Labels shape:', wide_conv_window.example[1].shape)
print('Output shape:', conv_model(wide_conv_window.example[0]).shape)
Wide conv window
Input shape: (32, 26, 19)
Labels shape: (32, 24, 1)
Output shape: (32, 24, 1)

כעת, אתה יכול לשרטט את התחזיות של המודל על חלון רחב יותר. שימו לב ל-3 שלבי זמן הקלט לפני החיזוי הראשון. כל תחזית כאן מבוססת על 3 שלבי הזמן הקודמים:

wide_conv_window.plot(conv_model)

png

רשת נוירונים חוזרת

רשת עצבית חוזרת (RNN) היא סוג של רשת עצבית המתאימה היטב לנתוני סדרות זמן. RNNs מעבדים סדרת זמן צעד אחר צעד, שומרים על מצב פנימי משלב זמן לשלב זמן.

אתה יכול ללמוד עוד בדור הטקסט עם הדרכה של RNN ובמדריך הרשתות העצביות החוזרות (RNN) עם Keras .

במדריך זה, תשתמש בשכבת RNN הנקראת זיכרון ארוך טווח קצר ( tf.keras.layers.LSTM ).

ארגומנט בנאי חשוב עבור כל שכבות RNN של Keras, כגון tf.keras.layers.LSTM , הוא הארגומנט return_sequences . הגדרה זו יכולה להגדיר את השכבה באחת משתי דרכים:

  1. אם False , ברירת המחדל, השכבה מחזירה רק את הפלט של שלב הזמן הסופי, ונותנת למודל זמן להתחמם במצבו הפנימי לפני ביצוע חיזוי בודד:

LSTM מתחמם וביצוע חיזוי בודד

  1. אם True , השכבה מחזירה פלט עבור כל קלט. זה שימושי עבור:
    • ערימת שכבות RNN.
    • אימון מודל על מספר שלבי זמן בו זמנית.

LSTM עושה חיזוי לאחר כל שלב בזמן

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

עם return_sequences=True , ניתן לאמן את המודל על 24 שעות של נתונים בכל פעם.

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

png

ביצועים

עם מערך הנתונים הזה, בדרך כלל, כל אחד מהדגמים מצליח מעט יותר מזה שלפניו:

x = np.arange(len(performance))
width = 0.3
metric_name = 'mean_absolute_error'
metric_index = lstm_model.metrics_names.index('mean_absolute_error')
val_mae = [v[metric_index] for v in val_performance.values()]
test_mae = [v[metric_index] for v in performance.values()]

plt.ylabel('mean_absolute_error [T (degC), normalized]')
plt.bar(x - 0.17, val_mae, width, label='Validation')
plt.bar(x + 0.17, test_mae, width, label='Test')
plt.xticks(ticks=x, labels=performance.keys(),
           rotation=45)
_ = plt.legend()

png

for name, value in performance.items():
  print(f'{name:12s}: {value[1]:0.4f}')
Baseline    : 0.0852
Linear      : 0.0666
Dense       : 0.0573
Multi step dense: 0.0586
Conv        : 0.0577
LSTM        : 0.0518

דגמים מרובי פלט

הדגמים עד כה חזו תכונת פלט בודדת, T (degC) , עבור שלב זמן בודד.

ניתן להמיר את כל המודלים הללו לחיזוי תכונות מרובות רק על ידי שינוי מספר היחידות בשכבת הפלט והתאמת חלונות ההדרכה כך שיכללו את כל התכונות labels ( example_labels ):

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

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

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

שים לב לעיל שלציר features של התוויות יש כעת אותו עומק כמו הכניסות, במקום 1 .

קו בסיס

ניתן להשתמש באותו מודל בסיס ( Baseline ) כאן, אך הפעם לחזור על כל התכונות במקום לבחור 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

צָפוּף

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

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

RNN

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

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

history = compile_and_fit(lstm_model, wide_window)

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

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

CPU times: user 5min 14s, sys: 1min 17s, total: 6min 31s
Wall time: 2min 8s

מתקדם: חיבורים שיוריים

מודל Baseline מקודם ניצל את העובדה שהרצף אינו משתנה באופן דרסטי משלב זמן לשלב. כל דגם שהוכשר במדריך הזה עד כה אותחל באקראי, ואז היה צריך ללמוד שהפלט הוא שינוי קטן משלב הזמן הקודם.

אמנם אתה יכול לעקוף את הבעיה עם אתחול קפדני, אבל זה פשוט יותר לבנות את זה במבנה המודל.

מקובל בניתוח סדרות זמן לבנות מודלים שבמקום לחזות את הערך הבא, מנבאים כיצד הערך ישתנה בשלב הזמן הבא. באופן דומה, רשתות שיוריות - או ResNets - בלמידה עמוקה מתייחסות לארכיטקטורות שבהן כל שכבה מוסיפה לתוצאה המצטברת של המודל.

כך מנצלים את הידיעה שהשינוי צריך להיות קטן.

דגם עם חיבור שיורי

בעיקרו של דבר, זה מאתחל את המודל כך שיתאים ל- Baseline . עבור משימה זו זה עוזר לדגמים להתכנס מהר יותר, עם ביצועים מעט טובים יותר.

ניתן להשתמש בגישה זו בשילוב עם כל מודל שנדון במדריך זה.

כאן, הוא מיושם על מודל LSTM, שים לב לשימוש ב- tf.initializers.zeros כדי להבטיח שהשינויים החזויים הראשוניים קטנים, ואינם גוברים על החיבור השיורי. אין כאן דאגות לשבירת סימטריה לשיפועים, מכיוון zeros משמשים רק בשכבה האחרונה.

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

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

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

history = compile_and_fit(residual_lstm, wide_window)

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

CPU times: user 1min 43s, sys: 26.1 s, total: 2min 9s
Wall time: 43.1 s

ביצועים

להלן הביצועים הכוללים של דגמי ריבוי פלטים אלה.

x = np.arange(len(performance))
width = 0.3

metric_name = 'mean_absolute_error'
metric_index = lstm_model.metrics_names.index('mean_absolute_error')
val_mae = [v[metric_index] for v in val_performance.values()]
test_mae = [v[metric_index] for v in performance.values()]

plt.bar(x - 0.17, val_mae, width, label='Validation')
plt.bar(x + 0.17, test_mae, width, label='Test')
plt.xticks(ticks=x, labels=performance.keys(),
           rotation=45)
plt.ylabel('MAE (average over all outputs)')
_ = plt.legend()

png

for name, value in performance.items():
  print(f'{name:15s}: {value[1]:0.4f}')
Baseline       : 0.1638
Dense          : 0.1311
LSTM           : 0.1214
Residual LSTM  : 0.1194

הביצועים שלעיל מוערכים בממוצע על פני כל תפוקות הדגם.

דגמים מרובי שלבים

גם המודלים של פלט יחיד וגם של ריבוי פלט בסעיפים הקודמים עשו תחזיות של צעד זמן בודד , שעה אחת לתוך העתיד.

חלק זה בוחן כיצד להרחיב את המודלים הללו כדי לבצע חיזויים מרובים של צעדי זמן .

בתחזית רב-שלבית, המודל צריך ללמוד לחזות טווח של ערכים עתידיים. לפיכך, בניגוד למודל של צעד אחד, שבו רק נקודה עתידית אחת חזויה, מודל רב-שלבי מנבא רצף של הערכים העתידיים.

ישנן שתי גישות גסות לכך:

  1. תחזיות של צילום בודד שבו כל סדרת הזמן נחזו בבת אחת.
  2. חיזויים אוטורגרסיביים שבהם המודל מבצע רק תחזיות צעד בודד והפלט שלו מוזן בחזרה כקלט שלו.

בסעיף זה כל הדגמים ינבאו את כל התכונות בכל שלבי זמן הפלט .

עבור המודל הרב-שלבי, נתוני ההדרכה מורכבים שוב מדגימות שעתיות. עם זאת, כאן, המודלים ילמדו לחזות 24 שעות אל העתיד, בהינתן 24 שעות מהעבר.

להלן אובייקט Window שיוצר את הפרוסות הללו ממערך הנתונים:

OUT_STEPS = 24
multi_window = WindowGenerator(input_width=24,
                               label_width=OUT_STEPS,
                               shift=OUT_STEPS)

multi_window.plot()
multi_window
Total window size: 48
Input indices: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
Label indices: [24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47]
Label column name(s): None

png

קווי בסיס

קו בסיס פשוט למשימה זו הוא לחזור על שלב זמן הקלט האחרון עבור המספר הנדרש של שלבי זמן הפלט:

חזור על הקלט האחרון, עבור כל שלב פלט

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

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

multi_val_performance = {}
multi_performance = {}

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

png

מכיוון שמשימה זו היא לחזות 24 שעות לעתיד, בהינתן 24 שעות מהעבר, גישה פשוטה נוספת היא לחזור על היום הקודם, בהנחה שמחר יהיה דומה:

חזור על היום הקודם

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

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

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

png

דגמים עם צילום בודד

גישה אחת ברמה גבוהה לבעיה זו היא להשתמש במודל "ירייה בודדת", כאשר המודל מבצע את חיזוי הרצף כולו בשלב אחד.

ניתן ליישם זאת ביעילות כ- tf.keras.layers.Dense עם OUT_STEPS*features יחידות פלט. המודל רק צריך לעצב מחדש את הפלט (OUTPUT_STEPS, features) .

ליניארי

מודל ליניארי פשוט המבוסס על שלב זמן הקלט האחרון עושה טוב יותר מכל אחת מהקו הבסיס, אבל הוא בעל עוצמה נמוכה. המודל צריך לחזות OUTPUT_STEPS שלבי זמן, משלב זמן קלט בודד עם השלכה ליניארית. זה יכול לתפוס רק נתח נמוך של ההתנהגות, ככל הנראה מבוסס בעיקר על השעה ביום והשעה בשנה.

חזה את כל שלבי הזמן משלב הזמן האחרון

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

history = compile_and_fit(multi_linear_model, multi_window)

IPython.display.clear_output()
multi_val_performance['Linear'] = multi_linear_model.evaluate(multi_window.val)
multi_performance['Linear'] = multi_linear_model.evaluate(multi_window.test, verbose=0)
multi_window.plot(multi_linear_model)
437/437 [==============================] - 1s 2ms/step - loss: 0.2559 - mean_absolute_error: 0.3053

png

צָפוּף

הוספת tf.keras.layers.Dense בין הקלט לפלט מעניקה למודל הליניארי יותר כוח, אך עדיין מבוססת רק על שלב זמן קלט בודד.

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

history = compile_and_fit(multi_dense_model, multi_window)

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

png

CNN

מודל קונבולוציוני יוצר תחזיות על סמך היסטוריה ברוחב קבוע, מה שעשוי להוביל לביצועים טובים יותר מהמודל הצפוף מכיוון שהוא יכול לראות כיצד דברים משתנים לאורך זמן:

מודל קונבולוציוני רואה כיצד דברים משתנים עם הזמן

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

history = compile_and_fit(multi_conv_model, multi_window)

IPython.display.clear_output()

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

png

RNN

מודל חוזר יכול ללמוד להשתמש בהיסטוריה ארוכה של תשומות, אם זה רלוונטי לתחזיות שהמודל עושה. כאן המודל יצבור מצב פנימי במשך 24 שעות, לפני ביצוע חיזוי בודד ל-24 השעות הבאות.

בפורמט של צילום יחיד זה, ה-LSTM צריך להפיק פלט רק בשלב הזמן האחרון, אז הגדר return_sequences=False ב- tf.keras.layers.LSTM .

ה-LSTM צובר מצב מעל חלון הקלט, ומבצע חיזוי בודד ל-24 השעות הבאות

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

history = compile_and_fit(multi_lstm_model, multi_window)

IPython.display.clear_output()

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

png

מתקדם: דגם אוטורגרסיבי

המודלים שלעיל כולם חוזים את רצף הפלט כולו בשלב אחד.

במקרים מסוימים עשוי להיות מועיל למודל לפרק את החיזוי הזה לשלבי זמן בודדים. לאחר מכן, ניתן להזין את הפלט של כל דגם בחזרה לתוך עצמו בכל שלב וניתן לבצע תחזיות בתנאי הקודם, כמו בגרסה הקלאסית של Generating Sequences With Recurrent Neural Networks .

יתרון אחד ברור לסגנון הדגם הזה הוא שניתן להגדיר אותו לייצר פלט באורך משתנה.

אתה יכול לקחת כל אחד מהמודלים מרובי-השלבים הבודדים שהוכשרו במחצית הראשונה של המדריך הזה ולרוץ בלולאת משוב אוטורגרסיבית, אבל כאן תתמקד בבניית מודל שעבר הכשרה מפורשת לעשות זאת.

משוב פלט של דגם לקלט שלו

RNN

מדריך זה בונה רק מודל RNN אוטורגרסיבי, אך ניתן ליישם את הדפוס הזה על כל מודל שתוכנן להפיק שלב זמן בודד.

למודל תהיה אותה צורה בסיסית כמו מודלים LSTM חד-שלביים מקודמים: שכבת tf.keras.layers.LSTM ואחריה שכבה tf.keras.layers.Dense הממירה את הפלטים של שכבת LSTM לחיזוי מודל.

tf.keras.layers.LSTM הוא תא tf.keras.layers.LSTMCell עטוף ברמה הגבוהה יותר tf.keras.layers.RNN שמנהל עבורך את המצב ואת תוצאות הרצף (בדוק את הרשתות העצביות החוזרות (RNN) עם Keras מדריך לפרטים).

במקרה זה, המודל צריך לנהל באופן ידני את התשומות עבור כל שלב, ולכן הוא משתמש ב- tf.keras.layers.LSTMCell ישירות עבור ממשק שלב של פעם אחת ברמה נמוכה יותר.

class FeedBack(tf.keras.Model):
  def __init__(self, units, out_steps):
    super().__init__()
    self.out_steps = out_steps
    self.units = units
    self.lstm_cell = tf.keras.layers.LSTMCell(units)
    # Also wrap the LSTMCell in an RNN to simplify the `warmup` method.
    self.lstm_rnn = tf.keras.layers.RNN(self.lstm_cell, return_state=True)
    self.dense = tf.keras.layers.Dense(num_features)
feedback_model = FeedBack(units=32, out_steps=OUT_STEPS)

השיטה הראשונה שמודל זה צריך היא שיטת warmup כדי לאתחל את המצב הפנימי שלו בהתבסס על התשומות. לאחר הכשרה, מצב זה יתפוס את החלקים הרלוונטיים של היסטוריית הקלט. זה שווה ערך למודל LSTM חד-שלבי מקודם:

def warmup(self, inputs):
  # inputs.shape => (batch, time, features)
  # x.shape => (batch, lstm_units)
  x, *state = self.lstm_rnn(inputs)

  # predictions.shape => (batch, features)
  prediction = self.dense(x)
  return prediction, state

FeedBack.warmup = warmup

שיטה זו מחזירה חיזוי צעד-זמן בודד ואת המצב הפנימי של ה- LSTM :

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

עם המצב של ה- RNN , ותחזית ראשונית, כעת תוכל להמשיך לחזור על המודל ולהזין את התחזיות בכל צעד אחורה כקלט.

הגישה הפשוטה ביותר לאיסוף תחזיות הפלט היא להשתמש ברשימת Python וב- tf.stack לאחר הלולאה.

def call(self, inputs, training=None):
  # Use a TensorArray to capture dynamically unrolled outputs.
  predictions = []
  # Initialize the LSTM state.
  prediction, state = self.warmup(inputs)

  # Insert the first prediction.
  predictions.append(prediction)

  # Run the rest of the prediction steps.
  for n in range(1, self.out_steps):
    # Use the last prediction as input.
    x = prediction
    # Execute one lstm step.
    x, state = self.lstm_cell(x, states=state,
                              training=training)
    # Convert the lstm output to a prediction.
    prediction = self.dense(x)
    # Add the prediction to the output.
    predictions.append(prediction)

  # predictions.shape => (time, batch, features)
  predictions = tf.stack(predictions)
  # predictions.shape => (batch, time, features)
  predictions = tf.transpose(predictions, [1, 0, 2])
  return predictions

FeedBack.call = call

הפעל את המודל הזה במבחן על הכניסות לדוגמה:

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

כעת, אמנו את הדגם:

history = compile_and_fit(feedback_model, multi_window)

IPython.display.clear_output()

multi_val_performance['AR LSTM'] = feedback_model.evaluate(multi_window.val)
multi_performance['AR LSTM'] = feedback_model.evaluate(multi_window.test, verbose=0)
multi_window.plot(feedback_model)
437/437 [==============================] - 3s 8ms/step - loss: 0.2269 - mean_absolute_error: 0.3011

png

ביצועים

ישנן תשואות פוחתות בבירור כפונקציה של מורכבות המודל בבעיה זו:

x = np.arange(len(multi_performance))
width = 0.3

metric_name = 'mean_absolute_error'
metric_index = lstm_model.metrics_names.index('mean_absolute_error')
val_mae = [v[metric_index] for v in multi_val_performance.values()]
test_mae = [v[metric_index] for v in multi_performance.values()]

plt.bar(x - 0.17, val_mae, width, label='Validation')
plt.bar(x + 0.17, test_mae, width, label='Test')
plt.xticks(ticks=x, labels=multi_performance.keys(),
           rotation=45)
plt.ylabel(f'MAE (average over all times and outputs)')
_ = plt.legend()

png

המדדים עבור דגמי ריבוי פלטים במחצית הראשונה של מדריך זה מציגים את הביצועים הממוצעים בכל תכונות הפלט. ביצועים אלה דומים אך גם בממוצע לאורך שלבי זמן הפלט.

for name, value in multi_performance.items():
  print(f'{name:8s}: {value[1]:0.4f}')
Last    : 0.5157
Repeat  : 0.3774
Linear  : 0.2977
Dense   : 0.2781
Conv    : 0.2796
LSTM    : 0.2767
AR LSTM : 0.2901

הרווחים שהושגו במעבר מדגם צפוף לדגמים קונבולוציוניים וחוזרים הם אחוזים בודדים בלבד (אם בכלל), והמודל האוטו-רגרסיב הביצועים גרועים יותר. אז אולי הגישות המורכבות יותר האלה לא יועילו לבעיה הזו , אבל לא הייתה דרך לדעת בלי לנסות, והמודלים האלה יכולים להיות מועילים לבעיה שלך .

הצעדים הבאים

מדריך זה היה מבוא מהיר לחיזוי סדרות זמן באמצעות TensorFlow.

למידע נוסף, עיין ב:

כמו כן, זכור שאתה יכול ליישם כל מודל סדרת זמן קלאסי ב-TensorFlow - הדרכה זו רק מתמקדת בפונקציונליות המובנית של TensorFlow.