آمادهسازی سری زمانی برای مدلهای یادگیری ماشین
پس از ساخت دو Baseline یعنی Naive Forecasting و SARIMA، فصل به سراغ مدلهای Machine Learning میرود. هدف این است که تعداد مسافران فردا با استفاده از ۸ هفتهٔ گذشته، یعنی ۵۶ روز، پیشبینی شود. در نتیجه هر Training Instance یک Window با طول ۵۶ دارد و Target مقدار بلافاصله بعد از آن Window است.
ساخت Window با timeseries_dataset_from_array()
Keras تابع کمکی tf.keras.utils.timeseries_dataset_from_array() را برای تبدیل یک سری به Dataset از Windowها فراهم میکند:
import tensorflow as tf
my_series = [0, 1, 2, 3, 4, 5]
my_dataset = tf.keras.utils.timeseries_dataset_from_array(
my_series,
targets=my_series[3:],
sequence_length=3,
batch_size=2
)
برای این مثال Windowهای [0,1,2]، [1,2,3] و [2,3,4] ساخته میشوند و Targetهای متناظر 3، 4 و 5 هستند.
ساخت Window با tf.data
روش انعطافپذیرتر استفاده از Dataset.window() است. این متد ابتدا Datasetی از Datasetها میسازد و سپس با flat_map() میتوان هر Window را به Tensor تبدیل کرد:
def to_windows(dataset, length):
dataset = dataset.window(length, shift=1, drop_remainder=True)
return dataset.flat_map(lambda window_ds: window_ds.batch(length))
dataset = to_windows(tf.data.Dataset.range(6), 4)
dataset = dataset.map(lambda window: (window[:-1], window[-1]))
dataset = dataset.batch(2)
این روش نسبت به Utility آماده پیچیدهتر است، اما بعداً برای ساخت Targetهای Sequence-to-Sequence کنترل بیشتری میدهد.
تقسیم زمانی Train، Validation و Test
در Time Series معمولاً نباید داده را Random Split کرد؛ زیرا اطلاعات آینده ممکن است به Train نشت کند. در مثال کتاب، Rail Ridership به سه بازه تقسیم میشود و برای Scale مناسب بر یک میلیون تقسیم میگردد:
rail_train = df["rail"]["2016-01":"2018-12"] / 1e6
rail_valid = df["rail"]["2019-01":"2019-05"] / 1e6
rail_test = df["rail"]["2019-06":] / 1e6
Windowهای Train Shuffle میشوند، اما ترتیب داخل هر Window حفظ میشود:
seq_length = 56
train_ds = tf.keras.utils.timeseries_dataset_from_array(
rail_train.to_numpy(),
targets=rail_train[seq_length:],
sequence_length=seq_length,
batch_size=32,
shuffle=True,
seed=42
)
valid_ds = tf.keras.utils.timeseries_dataset_from_array(
rail_valid.to_numpy(),
targets=rail_valid[seq_length:],
sequence_length=seq_length,
batch_size=32
)
Forecast با مدل Linear
ابتدا یک Dense Layer ساده با Huber Loss و Early Stopping آموزش داده میشود:
model = tf.keras.Sequential([
tf.keras.layers.Dense(1, input_shape=[seq_length])
])
early_stopping_cb = tf.keras.callbacks.EarlyStopping(
monitor="val_mae", patience=50, restore_best_weights=True)
opt = tf.keras.optimizers.SGD(learning_rate=0.02, momentum=0.9)
model.compile(loss=tf.keras.losses.Huber(), optimizer=opt, metrics=["mae"])
model.fit(train_ds, validation_data=valid_ds, epochs=500,
callbacks=[early_stopping_cb])
Validation MAE حدود 37,866 است: بهتر از Naive Forecast، اما ضعیفتر از SARIMA.
Forecast با Simple RNN
سادهترین RNN فقط یک Recurrent Neuron دارد:
model = tf.keras.Sequential([
tf.keras.layers.SimpleRNN(1, input_shape=[None, 1])
])
Recurrent Layer در Keras ورودی 3D با Shape برابر [batch size, time steps, dimensionality] انتظار دارد. برای سری Univariate، Dimension آخر 1 است. مقدار None برای Time Steps یعنی مدل میتواند Sequenceهایی با طولهای مختلف دریافت کند.
این مدل بسیار ضعیف است؛ فقط سه Parameter دارد و Output تابع tanh نیز به بازهٔ -1 تا +1 محدود میشود. راهحل این است که تعداد Recurrent Unitها را افزایش دهیم و یک Dense Output بدون Activation اضافه کنیم:
univar_model = tf.keras.Sequential([
tf.keras.layers.SimpleRNN(32, input_shape=[None, 1]),
tf.keras.layers.Dense(1)
])
این مدل Validation MAE حدود 27,703 میگیرد و از SARIMA نیز بهتر میشود.
Deep RNN
برای ساخت RNN عمیق، چند Recurrent Layer روی هم Stack میشوند. Layerهای میانی باید return_sequences=True داشته باشند تا خروجی سهبعدی همهٔ Time Stepها را به Layer بعدی بدهند.
شکل 15-10. Deep RNN
deep_model = tf.keras.Sequential([
tf.keras.layers.SimpleRNN(32, return_sequences=True,
input_shape=[None, 1]),
tf.keras.layers.SimpleRNN(32, return_sequences=True),
tf.keras.layers.SimpleRNN(32),
tf.keras.layers.Dense(1)
])
در این مثال Deep RNN حدود 31,211 MAE دارد؛ یعنی از Baselineها بهتر است، اما از مدل Shallower قبلی ضعیفتر است. بزرگتر بودن Model همیشه به معنی بهتر بودن نیست.
سری زمانی چندمتغیره
Neural Networkها با تغییر کوچک میتوانند Multivariate Time Series را پردازش کنند. برای پیشبینی Rail میتوان Bus، Rail و نوع روز فردا را همزمان وارد Model کرد. Day Type چون از قبل معلوم است، یک روز Shift میشود و سپس One-hot Encoding روی آن انجام میگیرد:
df_mulvar = df[["bus", "rail"]] / 1e6
df_mulvar["next_day_type"] = df["day_type"].shift(-1)
df_mulvar = pd.get_dummies(df_mulvar)
در نتیجه در هر Time Step پنج Feature داریم. تنها تغییر Model نسبت به حالت Univariate، Input Shape است:
mulvar_model = tf.keras.Sequential([
tf.keras.layers.SimpleRNN(32, input_shape=[None, 5]),
tf.keras.layers.Dense(1)
])
این Model به MAE حدود 22,062 میرسد. همچنین میتوان با دو Output، Bus و Rail را همزمان Forecast کرد. Multi-task Learning گاهی بهدلیل Sharing Featureها و اثر Regularization بهتر عمل میکند، هرچند در این Dataset مدل اختصاصی برای هر Target کمی بهتر است.
پیشبینی چند Time Step آینده
برای Forecast چند روز آینده سه رویکرد اصلی بررسی میشود.
۱. Forecast تکراری یک Step
Model یک روز را پیشبینی میکند، Prediction به انتهای Input اضافه میشود و Model دوباره اجرا میشود. مشکل این روش این است که Errorها در Stepهای بعدی جمع میشوند.
شکل 15-11. ۱. Forecast تکراری یک Step
X = rail_valid.to_numpy()[np.newaxis, :seq_length, np.newaxis]
for step_ahead in range(14):
y_pred_one = univar_model.predict(X)
X = np.concatenate([X, y_pred_one.reshape(1, 1, 1)], axis=1)
۲. پیشبینی ۱۴ مقدار در یک Shot
میتوان Output Layer را ۱۴ Unit کرد و Target را Vector چهاردهروزه ساخت. این روش Error را مانند رویکرد قبلی انباشته نمیکند:
ahead_model = tf.keras.Sequential([
tf.keras.layers.SimpleRNN(32, input_shape=[None, 5]),
tf.keras.layers.Dense(14)
])
۳. مدل Sequence-to-Sequence
روش بهتر این است که در هر Time Step، Model نه فقط یک Forecast بلکه Vector شامل ۱۴ Forecast آینده تولید کند. در این حالت Loss برای Output تمام Time Stepها محاسبه میشود، بنابراین Gradientهای بیشتری به شبکه میرسند و مسیر Backpropagation در زمان کوتاهتر میشود. این موضوع Training را سریعتر و پایدارتر میکند.
def to_seq2seq_dataset(series, seq_length=56, ahead=14,
target_col=1, batch_size=32,
shuffle=False, seed=None):
ds = to_windows(tf.data.Dataset.from_tensor_slices(series), ahead + 1)
ds = to_windows(ds, seq_length).map(lambda S: (S[:, 0], S[:, 1:, 1]))
if shuffle:
ds = ds.shuffle(8 * batch_size, seed=seed)
return ds.batch(batch_size)
seq2seq_model = tf.keras.Sequential([
tf.keras.layers.SimpleRNN(32, return_sequences=True,
input_shape=[None, 5]),
tf.keras.layers.Dense(14)
])
برای Forecast زمان t+1 MAE حدود 25,519 و برای t+14 حدود 34,322 است؛ هرچه Horizon دورتر میشود دقت افت میکند.
چرا Sequenceهای طولانی دشوارند؟
وقتی RNN روی Sequence بلند اجرا میشود، شبکهٔ Unrolled بسیار عمیق میشود. در نتیجه دو مشکل اصلی رخ میدهد: Gradient ناپایدار و فراموش شدن اطلاعات ابتدای Sequence. ReLU میتواند حتی باعث Exploding Output شود، زیرا Weightهای یکسان بارها در زمان استفاده میشوند. کاهش Learning Rate، Gradient Clipping، Dropout و Normalization میتوانند کمک کنند. در بخش بعد به Layer Normalization، LSTM و GRU میرسیم.