├── example ├── results.png └── example.py ├── requirements.txt ├── README.md ├── LICENSE ├── README.MD └── aaforecast_tensorflow ├── plots.py ├── utils.py ├── blocks.py └── model.py /example/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Buildsf409/aa-forecast-tensorflow/HEAD/example/results.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas==1.5.2 2 | numpy==1.23.5 3 | tensorflow==2.11.0 4 | plotly==5.11.0 5 | kaleido==0.2.1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aa-forecast-tensorflow 2 | TensorFlow implementation of AA-Forecast model for time series forecasting 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Flavia Giammarino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/example.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from aaforecast_tensorflow.model import aaforecast 4 | from aaforecast_tensorflow.plots import plot 5 | 6 | # Generate a time series 7 | N = 1000 8 | t = np.linspace(0, 1, N) 9 | trend = 30 + 20 * t + 10 * (t ** 2) 10 | seasonality = 5 * np.cos(2 * np.pi * (10 * t - 0.5)) 11 | noise = np.random.normal(0, 1, N) 12 | y = trend + seasonality + noise 13 | 14 | # Fit the model 15 | model = aaforecast( 16 | y=y, 17 | forecast_period=200, 18 | lookback_period=400, 19 | units=30, 20 | stacks=['trend', 'seasonality'], 21 | num_trend_coefficients=3, 22 | num_seasonal_coefficients=5, 23 | num_blocks_per_stack=2, 24 | share_weights=True, 25 | share_coefficients=False, 26 | ) 27 | 28 | model.fit( 29 | loss='mse', 30 | epochs=100, 31 | batch_size=32, 32 | learning_rate=0.003, 33 | backcast_loss_weight=0.5, 34 | verbose=True 35 | ) 36 | 37 | # Generate the forecasts and backcasts 38 | df = model.forecast(y=y, return_backcast=True) 39 | 40 | # Plot the forecasts and backcasts 41 | fig = plot(df=df) 42 | fig.write_image('results.png', scale=4, width=700, height=400) 43 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # AA-Forecast TensorFlow 2 | 3 | TensorFlow implementation of univariate time series forecasting model introduced in *European machine learning and data mining conference.* 4 | AA-Forecast: Anomaly-Aware Forecast for Extreme Events 5 | 6 | Credit goes to: https://github.com/flaviagiammarino for majority of the work. 7 | ## Dependencies 8 | ```bash 9 | pandas==1.5.2 10 | numpy==1.23.5 11 | tensorflow==2.11.0 12 | plotly==5.11.0 13 | kaleido==0.2.1 14 | ``` 15 | 16 | ## Usage 17 | ```python 18 | import numpy as np 19 | 20 | from aaforecast_tensorflow.model import aaforecast 21 | from aaforecast_tensorflow.plots import plot 22 | 23 | # Generate a time series 24 | N = 1000 25 | t = np.linspace(0, 1, N) 26 | trend = 30 + 20 * t + 10 * (t ** 2) 27 | seasonality = 5 * np.cos(2 * np.pi * (10 * t - 0.5)) 28 | noise = np.random.normal(0, 1, N) 29 | y = trend + seasonality + noise 30 | 31 | # Fit the model 32 | model = aaforecast( 33 | y=y, 34 | forecast_period=200, 35 | lookback_period=400, 36 | units=30, 37 | stacks=['trend', 'seasonality'], 38 | num_trend_coefficients=3, 39 | num_seasonal_coefficients=5, 40 | num_blocks_per_stack=2, 41 | share_weights=True, 42 | share_coefficients=False, 43 | ) 44 | 45 | model.fit( 46 | loss='mse', 47 | epochs=100, 48 | batch_size=32, 49 | learning_rate=0.003, 50 | backcast_loss_weight=0.5, 51 | verbose=True 52 | ) 53 | 54 | # Generate the forecasts and backcasts 55 | df = model.forecast(y=y, return_backcast=True) 56 | 57 | # Plot the forecasts and backcasts 58 | fig = plot(df=df) 59 | fig.write_image('results.png', scale=4, width=700, height=400) 60 | ``` 61 | ![results](example/results.png) 62 | -------------------------------------------------------------------------------- /aaforecast_tensorflow/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.graph_objects as go 2 | 3 | def plot(df): 4 | 5 | ''' 6 | Plot the actual values of the time series together with the forecasts and backcasts. 7 | 8 | Parameters: 9 | __________________________________ 10 | df: pd.DataFrame. 11 | Data frame with the actual values of the time series, forecasts and backcasts. 12 | 13 | Returns: 14 | __________________________________ 15 | fig: go.Figure. 16 | Line chart of the actual values of the time series, forecasts and backcasts. 17 | ''' 18 | 19 | layout = dict( 20 | plot_bgcolor='white', 21 | paper_bgcolor='white', 22 | margin=dict(t=10, b=10, l=10, r=10), 23 | font=dict( 24 | color='#1b1f24', 25 | size=8, 26 | ), 27 | legend=dict( 28 | font=dict( 29 | color='#1b1f24', 30 | size=10, 31 | ), 32 | ), 33 | xaxis=dict( 34 | title='Time', 35 | color='#424a53', 36 | tickfont=dict( 37 | color='#6e7781', 38 | size=6, 39 | ), 40 | linecolor='#eaeef2', 41 | mirror=True, 42 | showgrid=False, 43 | ), 44 | yaxis=dict( 45 | title='Value', 46 | color='#424a53', 47 | tickfont=dict( 48 | color='#6e7781', 49 | size=6, 50 | ), 51 | linecolor='#eaeef2', 52 | mirror=True, 53 | showgrid=False, 54 | zeroline=False, 55 | ), 56 | ) 57 | 58 | data = [] 59 | 60 | data.append( 61 | go.Scatter( 62 | x=df['time_idx'], 63 | y=df['actual'], 64 | name='Actual', 65 | mode='lines', 66 | line=dict( 67 | color='#afb8c1', 68 | width=1 69 | ) 70 | ) 71 | ) 72 | 73 | data.append( 74 | go.Scatter( 75 | x=df['time_idx'], 76 | y=df['forecast'], 77 | name='Forecast', 78 | mode='lines', 79 | line=dict( 80 | color='#0969da', 81 | width=1 82 | ) 83 | ) 84 | ) 85 | 86 | if 'backcast' in df.columns: 87 | 88 | data.append( 89 | go.Scatter( 90 | x=df['time_idx'], 91 | y=df['backcast'], 92 | name='Backcast', 93 | mode='lines', 94 | line=dict( 95 | color='#8250df', 96 | width=1 97 | ) 98 | ) 99 | ) 100 | 101 | fig = go.Figure(data=data, layout=layout) 102 | 103 | return fig 104 | -------------------------------------------------------------------------------- /aaforecast_tensorflow/utils.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import tensorflow as tf 4 | 5 | def cast_target_to_array(y): 6 | 7 | ''' 8 | Cast the time series to np.array. 9 | 10 | Parameters: 11 | __________________________________ 12 | y: np.array, pd.Series, list. 13 | Time series as numpy array, pandas series or list. 14 | 15 | Returns: 16 | __________________________________ 17 | y: np.array. 18 | Time series as numpy array. 19 | ''' 20 | 21 | if type(y) not in [pd.core.series.Series, np.ndarray, list]: 22 | raise ValueError('The input time series must be either a pandas series, a numpy array, or a list.') 23 | 24 | elif pd.isna(y).sum() != 0: 25 | raise ValueError('The input time series cannot contain missing values.') 26 | 27 | elif np.std(y) == 0: 28 | raise ValueError('The input time series cannot be constant.') 29 | 30 | else: 31 | 32 | if type(y) == pd.core.series.Series: 33 | y = y.values 34 | 35 | elif type(y) == list: 36 | y = np.array(y) 37 | 38 | return y 39 | 40 | 41 | def get_training_sequences(y, t, H): 42 | 43 | ''' 44 | Parameters: 45 | __________________________________ 46 | y: np.array. 47 | Time series. 48 | 49 | t: int. 50 | Length of input sequences (lookback period). 51 | 52 | H: int. 53 | Length of output sequences (forecast period). 54 | 55 | Returns: 56 | __________________________________ 57 | X: np.array. 58 | Input sequences, 2-dimensional array with shape (N - t - H + 1, t) where N is the length 59 | of the time series. 60 | 61 | Y: np.array. 62 | Output sequences, 2-dimensional array with shape (N - t - H + 1, H) where N is the length 63 | of the time series. 64 | ''' 65 | 66 | if H < 1: 67 | raise ValueError('The length of the forecast period should be greater than one.') 68 | 69 | if t < H: 70 | raise ValueError('The lookback period cannot be shorter than the forecast period.') 71 | 72 | if t + H >= len(y): 73 | raise ValueError('The combined length of the forecast and lookback periods cannot exceed the length of the time series.') 74 | 75 | X = [] 76 | Y = [] 77 | 78 | for T in range(t, len(y) - H + 1): 79 | X.append(y[T - t: T]) 80 | Y.append(y[T: T + H]) 81 | 82 | X = np.array(X) 83 | Y = np.array(Y) 84 | 85 | return X, Y 86 | 87 | 88 | def get_time_indices(t, H): 89 | 90 | ''' 91 | 92 | Parameters: 93 | __________________________________ 94 | t: int. 95 | Length of input sequences (lookback period). 96 | 97 | H: int. 98 | Length of output sequences (forecast period). 99 | 100 | Returns: 101 | __________________________________ 102 | t_b: tf.Tensor. 103 | Input time index, 1-dimensional tensor with length t used for backcasting. 104 | 105 | t_f: tf.Tensor. 106 | Output time index, 1-dimensional tensor with length H used for forecasting. 107 | ''' 108 | 109 | # Full time index, 1-dimensional tensor with length t + H. 110 | t_ = tf.cast(tf.range(0, t + H), dtype=tf.float32) / (t + H) 111 | 112 | # Input time index, 1-dimensional tensor with length t. 113 | t_b = t_[:t] 114 | 115 | # Output time index, 1-dimensional tensor with length H. 116 | t_f = t_[t:] 117 | 118 | return t_b, t_f 119 | -------------------------------------------------------------------------------- /aaforecast_tensorflow/blocks.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import tensorflow as tf 3 | 4 | def trend_model(theta, t_, p): 5 | 6 | ''' 7 | Generate the trend backcast or forecast by multiplying the matrix of basis expansion coefficients 8 | by the matrix of powers of the time index. See Section 3.3 in the N-BEATS paper. 9 | 10 | Parameters: 11 | __________________________________ 12 | theta: tf.Tensor. 13 | Basis expansion coefficients, 2-dimensional tensor with shape (N, p) where N is the batch 14 | size and p is the number of polynomial terms. 15 | 16 | t_: tf.Tensor. 17 | Time index, 1-dimensional tensor with length t for the trend backcast or with length H for 18 | the trend forecast. 19 | 20 | p: int. 21 | Number of polynomial terms. 22 | 23 | Returns: 24 | __________________________________ 25 | tf.Tensor. 26 | Predicted trend, 2-dimensional tensor with shape (N, t) when backcasting and (N, H) when 27 | forecasting where N is the batch size. 28 | ''' 29 | 30 | # Generate the matrix of polynomial terms. The shape of this matrix is (p, t) when backcasting 31 | # and (p, H) when forecasting. 32 | T_ = tf.map_fn(lambda p: t_ ** p, tf.range(p, dtype=tf.float32)) 33 | 34 | # Calculate the dot product of the matrix of basis expansion coefficients and of the matrix 35 | # of polynomial terms. The shape of this matrix is (N, p) x (p, t) = (N, t) when backcasting 36 | # and (N, p) x (p, H) = (N, H) when forecasting where N is the batch size. 37 | return tf.tensordot(theta, T_, axes=1) 38 | 39 | 40 | def seasonality_model(theta, t_, p): 41 | 42 | ''' 43 | Generate the seasonality backcast or forecast by multiplying the matrix of basis expansion 44 | coefficients by the matrix of Fourier terms. See Section 3.3 in the N-BEATS paper. 45 | 46 | Parameters: 47 | __________________________________ 48 | theta: tf.Tensor. 49 | Basis expansion coefficients, 2-dimensional tensor with shape (N, 2 * p) where N is the 50 | batch size and 2 * p is the total number of Fourier terms, that is p sine functions plus 51 | p cosine functions. 52 | 53 | t_: tf.Tensor. 54 | Time index, 1-dimensional tensor with length t for the seasonality backcast or with 55 | length H for the seasonality forecast. 56 | 57 | p: int. 58 | Number of Fourier terms. 59 | 60 | Returns: 61 | __________________________________ 62 | tf.Tensor. 63 | Predicted seasonality, 2-dimensional tensor with shape (N, t) when backcasting and 64 | (N, H) when forecasting where N is the batch size. 65 | ''' 66 | 67 | # Generate the matrix of Fourier terms. The shape of this matrix is (2 * p, t) when 68 | # backcasting and (2 * p, H) when forecasting. 69 | s1 = tf.map_fn(lambda p_: tf.math.cos(2 * np.pi * p_ * t_), tf.range(p, dtype=tf.float32)) 70 | s2 = tf.map_fn(lambda p_: tf.math.sin(2 * np.pi * p_ * t_), tf.range(p, dtype=tf.float32)) 71 | S = tf.concat([s1, s2], axis=0) 72 | 73 | # Calculate the dot product of the matrix of basis expansion coefficients and of the matrix of 74 | # Fourier terms. The shape of this matrix is (N, 2 * p) x (2 * p, t) = (N, t) when backcasting 75 | # and (N, 2 * p) x (2 * p, H) = (N, H) when forecasting where N is the batch size. 76 | return tf.tensordot(theta, S, axes=1) 77 | 78 | 79 | def trend_block(h, p, t_b, t_f, share_theta): 80 | 81 | ''' 82 | Derive the trend basis expansion coefficients and generate the trend backcast and forecast. 83 | See Section 3.1 and Section 3.3 in the N-BEATS paper. 84 | 85 | Parameters: 86 | __________________________________ 87 | h: tf.Tensor. 88 | Output of 4-layer fully connected stack, 2-dimensional tensor with shape (N, k) where 89 | N is the batch size and k is the number of hidden units of each fully connected layer. 90 | Note that all fully connected layers have the same number of units. 91 | 92 | p: int. 93 | Number of polynomial terms. 94 | 95 | t_b: tf.Tensor. 96 | Input time index, 1-dimensional tensor with length t used for generating the backcast. 97 | 98 | t_f: tf.Tensor. 99 | Output time index, 1-dimensional tensor with length H used for generating the forecast. 100 | 101 | share_theta: bool. 102 | True if the backcast and forecast should share the same basis expansion coefficients, 103 | False otherwise. 104 | 105 | Returns: 106 | __________________________________ 107 | backcast: tf.Tensor. 108 | Trend backcast, 2-dimensional tensor with shape (N, t) where N is the batch size and 109 | t is the length of the lookback period. 110 | 111 | forecast: tf.Tensor. 112 | Trend forecast, 2-dimensional tensor with shape (N, H) where N is the batch size and 113 | H is the length of the forecast period. 114 | ''' 115 | 116 | if share_theta: 117 | 118 | # If share_theta is true, use the same basis expansion 119 | # coefficients for backcasting and forecasting. 120 | theta = tf.keras.layers.Dense(units=p, activation='linear', use_bias=False)(h) 121 | 122 | # Obtain the backcast and forecast as the dot product of their common 123 | # basis expansion coefficients and of the matrix of polynomial terms. 124 | backcast = tf.keras.layers.Lambda(function=trend_model, arguments={'p': p, 't_': t_b})(theta) 125 | forecast = tf.keras.layers.Lambda(function=trend_model, arguments={'p': p, 't_': t_f})(theta) 126 | 127 | else: 128 | 129 | # If share_theta is false, use different basis expansion 130 | # coefficients for backcasting and forecasting. 131 | theta_b = tf.keras.layers.Dense(units=p, activation='linear', use_bias=False)(h) 132 | theta_f = tf.keras.layers.Dense(units=p, activation='linear', use_bias=False)(h) 133 | 134 | # Obtain the backcast and forecast as the dot product of their respective 135 | # basis expansion coefficients and of the matrix of polynomial terms. 136 | backcast = tf.keras.layers.Lambda(function=trend_model, arguments={'p': p, 't_': t_b})(theta_b) 137 | forecast = tf.keras.layers.Lambda(function=trend_model, arguments={'p': p, 't_': t_f})(theta_f) 138 | 139 | return backcast, forecast 140 | 141 | 142 | def seasonality_block(h, p, t_b, t_f, share_theta): 143 | 144 | ''' 145 | Derive the seasonality basis expansion coefficients and generate the seasonality 146 | backcast and forecast. See Section 3.1 and Section 3.3 in the N-BEATS paper. 147 | 148 | Parameters: 149 | __________________________________ 150 | h: tf.Tensor. 151 | Output of 4-layer fully connected stack, 2-dimensional tensor with shape (N, k) where 152 | N is the batch size and k is the number of hidden units of each fully connected layer. 153 | Note that all fully connected layers have the same number of units. 154 | 155 | p: int. 156 | Number of Fourier terms. 157 | 158 | t_b: tf.Tensor. 159 | Input time index, 1-dimensional tensor with length t used for generating the backcast. 160 | 161 | t_f: tf.Tensor. 162 | Output time index, 1-dimensional tensor with length H used for generating the forecast. 163 | 164 | share_theta: bool. 165 | True if the backcast and forecast should share the same basis expansion coefficients, 166 | False otherwise. 167 | 168 | Returns: 169 | __________________________________ 170 | backcast: tf.Tensor. 171 | Seasonality backcast, 2-dimensional tensor with shape (N, t) where N is the batch size and 172 | t is the length of the lookback period. 173 | 174 | forecast: tf.Tensor. 175 | Seasonality forecast, 2-dimensional tensor with shape (N, H) where N is the batch size and 176 | H is the length of the forecast period. 177 | ''' 178 | 179 | if share_theta: 180 | 181 | # If share_theta is true, use the same basis expansion 182 | # coefficients for backcasting and forecasting. 183 | theta = tf.keras.layers.Dense(units=2 * p, activation='linear', use_bias=False)(h) 184 | 185 | # Obtain the backcast and forecast as the dot product of their common 186 | # basis expansion coefficients and of the matrix of Fourier terms. 187 | backcast = tf.keras.layers.Lambda(function=seasonality_model, arguments={'p': p, 't_': t_b})(theta) 188 | forecast = tf.keras.layers.Lambda(function=seasonality_model, arguments={'p': p, 't_': t_f})(theta) 189 | 190 | else: 191 | 192 | # If share_theta is false, use different basis expansion 193 | # coefficients for backcasting and forecasting. 194 | theta_b = tf.keras.layers.Dense(units=2 * p, activation='linear', use_bias=False)(h) 195 | theta_f = tf.keras.layers.Dense(units=2 * p, activation='linear', use_bias=False)(h) 196 | 197 | # Obtain the backcast and forecast as the dot product of their respective 198 | # basis expansion coefficients and of the matrix of Fourier terms. 199 | backcast = tf.keras.layers.Lambda(function=seasonality_model, arguments={'p': p, 't_': t_b})(theta_b) 200 | forecast = tf.keras.layers.Lambda(function=seasonality_model, arguments={'p': p, 't_': t_f})(theta_f) 201 | 202 | return backcast, forecast 203 | 204 | 205 | def generic_block(h, p, t_b, t_f, share_theta): 206 | 207 | ''' 208 | Derive the generic basis expansion coefficients and generate the backcast and 209 | forecast. See Section 3.1 and Section 3.3 in the N-BEATS paper. 210 | 211 | Parameters: 212 | __________________________________ 213 | h: tf.Tensor. 214 | Output of 4-layer fully connected stack, 2-dimensional tensor with shape (N, k) where 215 | N is the batch size and k is the number of hidden units of each fully connected layer. 216 | Note that all fully connected layers have the same number of units. 217 | 218 | p: int. 219 | Number of linear terms. 220 | 221 | t_b: tf.Tensor. 222 | Input time index, 1-dimensional tensor with length t used for generating the backcast. 223 | 224 | t_f: tf.Tensor. 225 | Output time index, 1-dimensional tensor with length H used for generating the forecast. 226 | 227 | share_theta: bool. 228 | True if the backcast and forecast should share the same basis expansion coefficients, 229 | False otherwise. 230 | 231 | Returns: 232 | __________________________________ 233 | backcast: tf.Tensor. 234 | Generic backcast, 2-dimensional tensor with shape (N, t) where N is the batch size and 235 | t is the length of the lookback period. 236 | 237 | forecast: tf.Tensor. 238 | Generic forecast, 2-dimensional tensor with shape (N, H) where N is the batch size and 239 | H is the length of the forecast period. 240 | ''' 241 | 242 | if share_theta: 243 | 244 | # If share_theta is true, use the same basis expansion 245 | # coefficients for backcasting and forecasting. 246 | theta = tf.keras.layers.Dense(units=p, activation='linear', use_bias=False)(h) 247 | 248 | # Obtain the backcast and forecast as a linear function of 249 | # their common basis expansion coefficients. 250 | backcast = tf.keras.layers.Dense(units=len(t_b), activation='linear')(theta) 251 | forecast = tf.keras.layers.Dense(units=len(t_f), activation='linear')(theta) 252 | 253 | else: 254 | 255 | # If share_theta is false, use different basis expansion 256 | # coefficients for backcasting and forecasting. 257 | theta_b = tf.keras.layers.Dense(units=p, activation='linear', use_bias=False)(h) 258 | theta_f = tf.keras.layers.Dense(units=p, activation='linear', use_bias=False)(h) 259 | 260 | # Obtain the backcast and forecast as a linear function of 261 | # their respective basis expansion coefficients. 262 | backcast = tf.keras.layers.Dense(units=len(t_b), activation='linear')(theta_b) 263 | forecast = tf.keras.layers.Dense(units=len(t_f), activation='linear')(theta_f) 264 | 265 | return backcast, forecast 266 | -------------------------------------------------------------------------------- /aaforecast_tensorflow/model.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import tensorflow as tf 4 | pd.options.mode.chained_assignment = None 5 | 6 | from nbeats_tensorflow.utils import cast_target_to_array, get_training_sequences, get_time_indices 7 | from nbeats_tensorflow.blocks import trend_block, seasonality_block, generic_block 8 | 9 | class NBeats(): 10 | 11 | def __init__(self, 12 | y, 13 | forecast_period, 14 | lookback_period, 15 | num_trend_coefficients=3, 16 | num_seasonal_coefficients=5, 17 | num_generic_coefficients=7, 18 | units=100, 19 | stacks=['trend', 'seasonality'], 20 | num_blocks_per_stack=3, 21 | share_weights=True, 22 | share_coefficients=True): 23 | 24 | ''' 25 | Implementation of univariate time series forecasting model introduced in Oreshkin, B. N., Carpov, D., 26 | Chapados, N. and Bengio, Y., 2019. N-BEATS: Neural basis expansion analysis for interpretable time 27 | series forecasting. In International Conference on Learning Representations. 28 | 29 | Parameters: 30 | __________________________________ 31 | y: np.array, pd.Series, list. 32 | Time series. 33 | 34 | forecast_period: int. 35 | Length of forecast period. 36 | 37 | lookback_period: int. 38 | Length of lookback period. 39 | 40 | num_trend_coefficients: int. 41 | Number of basis expansion coefficients of the trend block. This is the number of polynomial terms 42 | used for modelling the trend component. Only used when the model includes a trend stack. 43 | 44 | num_seasonal_coefficients: int. 45 | Number of basis expansion coefficients of the seasonality block. This is the number of Fourier terms 46 | used for modelling the seasonal component. Only used when the model includes a seasonality stack. 47 | 48 | num_generic_coefficients: int. 49 | Number of basis expansion coefficients of the generic block. This is the number of linear terms used 50 | for modelling the generic component. Only used when the model includes a generic stack. 51 | 52 | units: int. 53 | Number of hidden units of each of the 4 layers of the fully connected stack. 54 | 55 | stacks: list of strings. 56 | The length of the list is the number of stacks, the items in the list are strings identifying the 57 | stack types (either 'trend', 'seasonality' or 'generic'). 58 | 59 | num_blocks_per_stack: int. 60 | The number of blocks in each stack. 61 | 62 | share_weights: bool. 63 | True if the weights of the 4 layers of the fully connected stack should be shared by the different 64 | blocks inside the same stack, False otherwise. 65 | 66 | share_coefficients: bool. 67 | True if the forecast and backcast of each block should share the same basis expansion coefficients, 68 | False otherwise. 69 | ''' 70 | 71 | # Cast the data to numpy array. 72 | y = cast_target_to_array(y) 73 | 74 | # Scale the data. 75 | self.y_min, self.y_max = np.min(y), np.max(y) 76 | self.y = (y - self.y_min) / (self.y_max - self.y_min) 77 | 78 | # Extract the input and output sequences. 79 | self.X, self.Y = get_training_sequences(self.y, lookback_period, forecast_period) 80 | 81 | # Save the lengths of the input and output sequences. 82 | self.lookback_period = lookback_period 83 | self.forecast_period = forecast_period 84 | 85 | # Extract the time indices of the input and output sequences. 86 | backcast_time_idx, forecast_time_idx = get_time_indices(lookback_period, forecast_period) 87 | 88 | # Build the model. 89 | self.model = build_fn( 90 | backcast_time_idx, 91 | forecast_time_idx, 92 | num_trend_coefficients, 93 | num_seasonal_coefficients, 94 | num_generic_coefficients, 95 | units, 96 | stacks, 97 | num_blocks_per_stack, 98 | share_weights, 99 | share_coefficients) 100 | 101 | def fit(self, 102 | loss='mse', 103 | learning_rate=0.001, 104 | batch_size=32, 105 | epochs=100, 106 | validation_split=0.2, 107 | backcast_loss_weight=0.5, 108 | verbose=True): 109 | 110 | ''' 111 | Train the model. 112 | 113 | Parameters: 114 | __________________________________ 115 | loss: str, function. 116 | Loss function, see https://www.tensorflow.org/api_docs/python/tf/keras/losses. 117 | 118 | learning_rate: float. 119 | Learning rate. 120 | 121 | batch_size: int. 122 | Batch size. 123 | 124 | epochs: int. 125 | Number of epochs. 126 | 127 | validation_split: float. 128 | Fraction of the training data to be used as validation data, must be between 0 and 1. 129 | 130 | backcast_loss_weight: float. 131 | Weight of backcast in comparison to forecast when calculating the loss, must be between 0 and 1. 132 | A weight of 0.5 means that forecast and backcast loss is weighted the same. 133 | 134 | verbose: bool. 135 | True if the training history should be printed in the console, False otherwise. 136 | ''' 137 | 138 | if backcast_loss_weight < 0 or backcast_loss_weight > 1: 139 | raise ValueError('The backcast loss weight must be between zero and one.') 140 | 141 | # Compile the model. 142 | self.model.compile( 143 | optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate), 144 | loss=[loss, loss], 145 | loss_weights=[backcast_loss_weight, 1 - backcast_loss_weight]) 146 | 147 | # Fit the model. 148 | self.history = self.model.fit( 149 | x=self.X, 150 | y=[self.X, self.Y], 151 | epochs=epochs, 152 | batch_size=batch_size, 153 | validation_split=validation_split, 154 | verbose=0, 155 | callbacks=[callback()] if verbose else None) 156 | 157 | def forecast(self, y, return_backcast=False): 158 | 159 | ''' 160 | Generate the forecasts and backcasts. 161 | 162 | Parameters: 163 | __________________________________ 164 | y: np.array, pd.Series, list. 165 | Past values of the time series. 166 | 167 | return_backcast: bool. 168 | True if the output should include the backcasts, False otherwise. 169 | 170 | Returns: 171 | __________________________________ 172 | df: pd.DataFrame. 173 | Data frame with the actual values of the time series, forecasts and backcasts. 174 | ''' 175 | 176 | # Cast the data to numpy array. 177 | y = cast_target_to_array(y) 178 | 179 | # Scale the data. 180 | y = (y - self.y_min) / (self.y_max - self.y_min) 181 | 182 | # Generate the forecasts and backcasts. 183 | backcast, forecast = self.model(y[- self.lookback_period:].reshape(1, - 1)) 184 | backcast = self.y_min + (self.y_max - self.y_min) * backcast[- 1, :].numpy().flatten() 185 | forecast = self.y_min + (self.y_max - self.y_min) * forecast[- 1, :].numpy().flatten() 186 | 187 | # Organize the forecasts and backcasts in a data frame. 188 | df = pd.DataFrame(columns=['time_idx', 'actual', 'forecast']) 189 | df['time_idx'] = np.arange(len(self.y) + self.forecast_period) 190 | df['actual'].iloc[: - self.forecast_period] = self.y_min + (self.y_max - self.y_min) * self.y 191 | df['forecast'].iloc[- self.forecast_period:] = forecast 192 | 193 | if return_backcast: 194 | df['backcast'] = np.nan 195 | df['backcast'].iloc[- (self.lookback_period + self.forecast_period): - self.forecast_period] = backcast 196 | 197 | # Return the data frame. 198 | return df.astype(float) 199 | 200 | 201 | def get_block_output(stack_type, 202 | dense_layers_output, 203 | backcast_time_idx, 204 | forecast_time_idx, 205 | num_trend_coefficients, 206 | num_seasonal_coefficients, 207 | num_generic_coefficients, 208 | share_coefficients): 209 | 210 | ''' 211 | Generate the block backcast and forecast. 212 | 213 | Parameters: 214 | __________________________________ 215 | stack_type: str. 216 | The stack type, either 'trend', 'seasonality' or 'generic'. 217 | 218 | dense_layers_output: tf.Tensor. 219 | Output of 4-layer fully connected stack, 2-dimensional tensor with shape (N, k) where 220 | N is the batch size and k is the number of hidden units of each fully connected layer. 221 | Note that all fully connected layers have the same number of units. 222 | 223 | backcast_time_idx: tf.Tensor. 224 | Input time index, 1-dimensional tensor with length t used for generating the backcast. 225 | 226 | forecast_time_idx: tf.Tensor. 227 | Output time index, 1-dimensional tensor with length H used for generating the forecast. 228 | 229 | num_trend_coefficients: int. 230 | Number of basis expansion coefficients of the trend block. This is the number of polynomial terms used for 231 | modelling the trend component. 232 | 233 | num_seasonal_coefficients: int. 234 | Number of basis expansion coefficients of the seasonality block. This is the number of Fourier terms used 235 | for modelling the seasonal component. 236 | 237 | num_generic_coefficients: int. 238 | Number of basis expansion coefficients of the generic block. This is the number of linear terms used for 239 | modelling the generic component. 240 | 241 | share_coefficients: bool. 242 | True if the block forecast and backcast should share the same basis expansion coefficients, False otherwise. 243 | ''' 244 | 245 | if stack_type == 'trend': 246 | 247 | return trend_block( 248 | h=dense_layers_output, 249 | p=num_trend_coefficients, 250 | t_b=backcast_time_idx, 251 | t_f=forecast_time_idx, 252 | share_theta=share_coefficients) 253 | 254 | elif stack_type == 'seasonality': 255 | 256 | return seasonality_block( 257 | h=dense_layers_output, 258 | p=num_seasonal_coefficients, 259 | t_b=backcast_time_idx, 260 | t_f=forecast_time_idx, 261 | share_theta=share_coefficients) 262 | 263 | else: 264 | 265 | return generic_block( 266 | h=dense_layers_output, 267 | p=num_generic_coefficients, 268 | t_b=backcast_time_idx, 269 | t_f=forecast_time_idx, 270 | share_theta=share_coefficients) 271 | 272 | 273 | def build_fn(backcast_time_idx, 274 | forecast_time_idx, 275 | num_trend_coefficients, 276 | num_seasonal_coefficients, 277 | num_generic_coefficients, 278 | units, 279 | stacks, 280 | num_blocks_per_stack, 281 | share_weights, 282 | share_coefficients): 283 | 284 | ''' 285 | Build the model. 286 | 287 | Parameters: 288 | __________________________________ 289 | backcast_time_idx: tf.Tensor. 290 | Input time index, 1-dimensional tensor with length t used for generating the backcast. 291 | 292 | forecast_time_idx: tf.Tensor. 293 | Output time index, 1-dimensional tensor with length H used for generating the forecast. 294 | 295 | num_trend_coefficients: int. 296 | Number of basis expansion coefficients of the trend block, corresponds to the number of polynomial terms. 297 | 298 | num_seasonal_coefficients: int. 299 | Number of basis expansion coefficients of the seasonality block, corresponds to the number of Fourier terms. 300 | 301 | num_generic_coefficients: int. 302 | Number of basis expansion coefficients of the generic block, corresponds to the number of linear terms. 303 | 304 | units: int. 305 | Number of hidden units of each of the 4 layers of the fully connected stack. Note that all fully connected 306 | layers have the same number of units. 307 | 308 | stacks: list of strings. 309 | The length of the list is the number of stacks, the items in the list are strings identifying the stack 310 | types (either 'trend', 'seasonality' or 'generic'). 311 | 312 | num_blocks_per_stack: int. 313 | The number of blocks in each stack. 314 | 315 | share_weights: bool. 316 | True if the weights of the 4 layers of the fully connected stack should be shared by the different blocks 317 | inside the same stack, False otherwise. 318 | 319 | share_coefficients: bool. 320 | True if the block forecast and backcast should share the same basis expansion coefficients, False otherwise. 321 | ''' 322 | 323 | # Define the model input, the input shape is 324 | # equal to the length of the lookback period. 325 | x = tf.keras.layers.Input(shape=len(backcast_time_idx)) 326 | 327 | # Loop across the different stacks. 328 | for s in range(len(stacks)): 329 | 330 | # If share_weights is true, use the same 4 fully connected 331 | # layers across all blocks in the stack. 332 | if share_weights: 333 | 334 | d1 = tf.keras.layers.Dense(units=units, activation='relu') 335 | d2 = tf.keras.layers.Dense(units=units, activation='relu') 336 | d3 = tf.keras.layers.Dense(units=units, activation='relu') 337 | d4 = tf.keras.layers.Dense(units=units, activation='relu') 338 | 339 | # Loop across the different blocks in the stack. 340 | for b in range(num_blocks_per_stack): 341 | 342 | if s == 0 and b == 0: 343 | 344 | # For the first block of the first stack, forward pass the 345 | # input directly through the 4 fully connected layers. 346 | if share_weights: 347 | 348 | h = d1(x) 349 | h = d2(h) 350 | h = d3(h) 351 | h = d4(h) 352 | 353 | else: 354 | 355 | h = tf.keras.layers.Dense(units=units, activation='relu')(x) 356 | h = tf.keras.layers.Dense(units=units, activation='relu')(h) 357 | h = tf.keras.layers.Dense(units=units, activation='relu')(h) 358 | h = tf.keras.layers.Dense(units=units, activation='relu')(h) 359 | 360 | # Generate the block backcast and forecast. 361 | backcast_block, forecast_block = get_block_output( 362 | stack_type=stacks[s], 363 | dense_layers_output=h, 364 | backcast_time_idx=backcast_time_idx, 365 | forecast_time_idx=forecast_time_idx, 366 | num_trend_coefficients=num_trend_coefficients, 367 | num_seasonal_coefficients=num_seasonal_coefficients, 368 | num_generic_coefficients=num_generic_coefficients, 369 | share_coefficients=share_coefficients) 370 | 371 | # Calculate the backcast residual by subtracting the block backcast from the input. 372 | # See Section 3.2 in the N-BEATS paper. 373 | backcast = tf.keras.layers.Subtract()([x, backcast_block]) 374 | 375 | # For the first block of the first stack, no adjustment is applied to the forecast. 376 | forecast = forecast_block 377 | 378 | else: 379 | 380 | # For the subsequent blocks and stacks, forward pass the 381 | # backcast residual through the 4 fully connected layers. 382 | if share_weights: 383 | 384 | h = d1(backcast) 385 | h = d2(h) 386 | h = d3(h) 387 | h = d4(h) 388 | 389 | else: 390 | 391 | h = tf.keras.layers.Dense(units=units, activation='relu')(backcast) 392 | h = tf.keras.layers.Dense(units=units, activation='relu')(h) 393 | h = tf.keras.layers.Dense(units=units, activation='relu')(h) 394 | h = tf.keras.layers.Dense(units=units, activation='relu')(h) 395 | 396 | # Generate the block backcast and forecast. 397 | backcast_block, forecast_block = get_block_output( 398 | stack_type=stacks[s], 399 | dense_layers_output=h, 400 | backcast_time_idx=backcast_time_idx, 401 | forecast_time_idx=forecast_time_idx, 402 | num_trend_coefficients=num_trend_coefficients, 403 | num_seasonal_coefficients=num_seasonal_coefficients, 404 | num_generic_coefficients=num_generic_coefficients, 405 | share_coefficients=share_coefficients) 406 | 407 | # Substract the current block backcast from the previous block backcast. 408 | # See Section 3.2 in the N-BEATS paper. 409 | backcast = tf.keras.layers.Subtract()([backcast, backcast_block]) 410 | 411 | # Add the current block forecast to the previous block forecast. 412 | # See Section 3.2 in the N-BEATS paper. 413 | forecast = tf.keras.layers.Add()([forecast, forecast_block]) 414 | 415 | return tf.keras.models.Model(x, [backcast, forecast]) 416 | 417 | 418 | class callback(tf.keras.callbacks.Callback): 419 | def on_epoch_end(self, epoch, logs=None): 420 | if 'val_loss' in logs.keys(): 421 | print('epoch: {}, loss: {:,.6f}, val_loss: {:,.6f}'.format(1 + epoch, logs['loss'], logs['val_loss'])) 422 | else: 423 | print('epoch: {}, loss: {:,.6f}'.format(1 + epoch, logs['loss'])) 424 | --------------------------------------------------------------------------------