فصل ۱۵: پردازش دنبالهها با RNN و CNN
پیشبینی آینده کاری است که انسان دائماً انجام میدهد؛ از کامل کردن جملهٔ یک دوست تا حدسزدن وضعیت ترافیک یا مصرف انرژی فردا. شبکههای عصبی بازگشتی یا RNN برای همین نوع دادههای ترتیبی ساخته شدهاند. آنها میتوانند سریهای زمانی مانند تعداد کاربران فعال روزانه، دمای ساعتی، مصرف برق، مسیر حرکت خودروها و بسیاری از دنبالههای دیگر را تحلیل کنند. اگر الگوهای گذشته در آینده نیز معتبر بمانند، RNN میتواند از آنها برای Forecasting استفاده کند.
مزیت مهم RNN این است که برخلاف شبکههای Feedforward با ورودی ثابت، میتواند روی Sequenceهایی با طول متغیر کار کند؛ بنابراین برای جمله، سند، صوت و دادههای زمانی مناسب است. در این فصل ابتدا ساختار RNN و Backpropagation Through Time را بررسی میکنیم، سپس یک سری زمانی واقعی را Forecast میکنیم، خانوادهٔ ARMA را بهعنوان Baseline میشناسیم و در ادامه به مشکلات Gradient ناپایدار و حافظهٔ کوتاهمدت میرسیم.
نورون و لایهٔ بازگشتی
در شبکهٔ Feedforward جریان Activation فقط از ورودی به خروجی حرکت میکند، اما در RNN اتصالهایی وجود دارد که خروجی مرحلهٔ قبل را دوباره به شبکه برمیگرداند. سادهترین RNN از یک نورون تشکیل شده است که در زمان t هم ورودی فعلی x(t) و هم خروجی مرحلهٔ قبل ŷ(t-1) را دریافت میکند. در اولین Time Step، چون خروجی قبلی وجود ندارد، معمولاً مقدار آن صفر در نظر گرفته میشود.
شکل 15-1. نورون و لایهٔ بازگشتی
اگر یک نورون بازگشتی را روی محور زمان باز کنیم، همان نورون در چند Time Step دیده میشود. به این نمایش Unrolling Through Time میگویند. همین ایده برای یک لایهٔ کامل از نورونهای بازگشتی نیز برقرار است.
شکل 15-2. نورون و لایهٔ بازگشتی
هر نورون بازگشتی دو مجموعه Weight دارد: یکی برای ورودی فعلی و دیگری برای خروجی قبلی. اگر کل لایه را در نظر بگیریم، این Weightها در دو Matrix به نامهای Wx و Wŷ قرار میگیرند. خروجی یک نمونه در Time Step مشخص را میتوان بهصورت زیر نوشت:
ŷ(t) = φ(Wxᵀ x(t) + Wŷᵀ ŷ(t-1) + b)
برای یک Mini-batch نیز محاسبه به شکل Matrix انجام میشود:
Ŷ(t) = φ(X(t) Wx + Ŷ(t-1) Wŷ + b)
= φ([X(t) Ŷ(t-1)] W + b)
در این رابطه X(t) ماتریس ورودیهای Batch، Ŷ(t) ماتریس خروجیها، b بردار Bias و W ترکیب عمودی دو Matrix وزن است. چون خروجی زمان t به خروجی زمان t-1 وابسته است، در نهایت تابعی از تمام ورودیهای قبلی خواهد بود.
Memory Cell
از آنجا که State فعلی اطلاعاتی از گذشته را در خود نگه میدارد، میگوییم RNN نوعی حافظه دارد. بخشی از شبکه که State را بین Time Stepها حفظ میکند Memory Cell نام دارد. State مخفی را معمولاً با h(t) نشان میدهند:
h(t) = f(x(t), h(t-1))
در Cell ساده، خروجی و State تقریباً یکساناند، ولی در Cellهای پیشرفته مثل LSTM این دو میتوانند متفاوت باشند.
شکل 15-3. Memory Cell
انواع ورودی و خروجی Sequence
- Sequence-to-Sequence: یک Sequence وارد میشود و برای هر Time Step خروجی تولید میشود؛ مناسب Forecasting سری زمانی.
- Sequence-to-Vector: کل Sequence خوانده میشود و فقط خروجی آخر اهمیت دارد؛ مانند Sentiment Analysis یک Review.
- Vector-to-Sequence: یک Vector ثابت وارد میشود و Sequence خروجی تولید میشود؛ مانند تولید Caption برای Image.
- Encoder-Decoder: Encoder ابتدا Sequence را به Representation تبدیل میکند و Decoder از آن Sequence جدید میسازد؛ مانند ترجمهٔ ماشینی.
شکل 15-4. انواع ورودی و خروجی Sequence
آموزش RNN با Backpropagation Through Time
برای Train کردن RNN، شبکه روی محور زمان Unroll میشود و Backpropagation معمولی روی نسخهٔ بازشده اجرا میگردد. این روش BPTT نام دارد. ابتدا Forward Pass انجام میشود، سپس Loss روی خروجیهای موردنیاز محاسبه میشود و Gradientها در زمان به عقب جریان پیدا میکنند. چون Weightهای یک Cell در همهٔ Time Stepها مشترکاند، Gradientهای حاصل از تمام Stepها روی همان Parameterها جمع میشوند.
شکل 15-5. آموزش RNN با Backpropagation Through Time
در یک Sequence-to-Vector ممکن است Loss فقط از آخرین خروجی ساخته شود؛ در این حالت Gradient از همان خروجی نهایی به مراحل قبلی بازمیگردد. Keras این پیچیدگی را بهطور خودکار مدیریت میکند.
Forecasting یک سری زمانی واقعی: مسافران حملونقل شیکاگو
مثال فصل دادههای روزانهٔ سوارشدن مسافران اتوبوس و قطار سازمان حملونقل شیکاگو را از سال ۲۰۰۱ بررسی میکند. داده با Pandas Load، بر اساس تاریخ Sort و Columnهای اضافی یا Duplicate حذف میشوند:
import pandas as pd
from pathlib import Path
path = Path("datasets/ridership/CTA_-_Ridership_-_Daily_Boarding_Totals.csv")
df = pd.read_csv(path, parse_dates=["service_date"])
df.columns = ["date", "day_type", "bus", "rail", "total"]
df = df.sort_values("date").set_index("date")
df = df.drop("total", axis=1)
df = df.drop_duplicates()
Column نوع روز سه مقدار دارد: W برای Weekday، A برای Saturday و U برای Sunday یا Holiday. Plot چند ماه از سال ۲۰۱۹ الگوی هفتگی بسیار واضحی را نشان میدهد.
شکل 15-6. Forecasting یک سری زمانی واقعی: مسافران حملونقل شیکاگو
چون در هر تاریخ چند مقدار داریم، این داده یک Multivariate Time Series است. اگر فقط Column اتوبوس را در نظر بگیریم، سری Univariate خواهد بود.
Naive Forecasting، Lag و Differencing
وقتی Seasonality هفتگی قوی است، یک Baseline ساده این است که مقدار فردا را برابر مقدار یک هفته قبل فرض کنیم. این روش Naive Forecasting است. با Shift کردن سری به اندازهٔ هفت روز و مقایسه با مقدار واقعی میتوان Autocorrelation را دید. تفاوت بین مقدار زمان t و t-7 نیز Differencing نام دارد.
شکل 15-7. Naive Forecasting، Lag و Differencing
برای بازهٔ مارس تا مه ۲۰۱۹، MAE این Baseline حدود 43,916 مسافر برای اتوبوس و 42,143 مسافر برای قطار است. با تقسیم خطا بر مقدار واقعی، MAPE تقریباً 8.3% برای Bus و 9.0% برای Rail به دست میآید. MAE، MAPE و MSE از Metricهای رایج Forecasting هستند و انتخاب آنها باید با هزینهٔ Business Error هماهنگ باشد.
Seasonality سالانه و Trend
میانگین ماهانه و Rolling Average دوازدهماهه نشان میدهد علاوه بر الگوی هفتگی، Seasonality سالانه و Trend بلندمدت نیز وجود دارد.
شکل 15-8. Seasonality سالانه و Trend
Differencing دوازدهماهه Seasonality سالانه را تقریباً حذف میکند و Trend را نیز به مقدار تقریباً ثابت تبدیل میکند. این تکنیک برای نزدیک کردن سری به حالت Stationary بسیار مهم است.
شکل 15-9. Seasonality سالانه و Trend
خانوادهٔ مدلهای ARMA، ARIMA و SARIMA
مدل ARMA Forecast را از دو بخش میسازد: Weighted Sum مقدارهای Lagged و Weighted Sum خطاهای Forecast گذشته:
ŷ(t) = Σ(i=1..p) αᵢ y(t-i) + Σ(i=1..q) θᵢ ε(t-i)
ε(t) = y(t) - ŷ(t)
p تعداد Lagهای بخش Autoregressive و q تعداد خطاهای بخش Moving Average است. ARMA فرض میکند سری Stationary است. اگر Trend وجود داشته باشد، Differencing میتواند آن را حذف کند. اجرای d مرحله Differencing مبنای مدل ARIMA است؛ ARIMA ابتدا سری را d بار Difference میکند و سپس ARMA را Fit میکند.
SARIMA علاوه بر ARIMA، یک مؤلفهٔ Seasonal نیز دارد و در مجموع از پارامترهای (p,d,q) و (P,D,Q,s) استفاده میکند. برای Seasonality هفتگی، s=7 است.
from statsmodels.tsa.arima.model import ARIMA
origin, today = "2019-01-01", "2019-05-31"
rail_series = df.loc[origin:today]["rail"].asfreq("D")
model = ARIMA(rail_series,
order=(1, 0, 0),
seasonal_order=(0, 1, 1, 7))
model = model.fit()
y_pred = model.forecast()
Forecast یک روز خاص ممکن است خطای زیادی داشته باشد، بنابراین باید مدل را روی بازهٔ طولانیتر ارزیابی کرد. با Refit روزانه در سه ماه مارس تا مه، MAE حدود 32,041 به دست میآید که از Naive Forecasting بهتر است. برای انتخاب Hyperparameterها میتوان Grid Search انجام داد؛ معمولاً p,q,P,Q کوچکاند، d,D صفر یا یک هستند و s از Period اصلی Seasonality میآید.
روشهای اصولیتر برای انتخاب Hyperparameter شامل بررسی ACF/PACF و مقایسهٔ AIC/BIC نیز هستند، اما Grid Search نقطهٔ شروع ساده و قابل فهمی است.