PyTorch : A Simple MLP model for Univariate Time Series Forecasting

This post explains how to implement a simple MLP model for a univariate time series forecasting using PyTorch. The estimation of the PyTorch model is slightly different from the Keras model, so it's worth focusing on how it differs.


A Simple PyTorch MLP model





Python Jupyter notebook code


The following code downloads historical the Google stock prices (GOOG) using yfinance package, splits training/test samples, and then, converts them into torch.Tensor type.

import pandas as pd
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import yfinance as yf
 
# download
data = yf.download('GOOG'
                   start='2010-01-01'
                   end  ='2023-06-01')
print(data)
# Access the date values from the index
dates = data.index
print(dates)
 
# split traning/test and set lags
nlag = 20
ei_train = 2500
y_raw = data.iloc[nlag:,4:5];  y = y_raw.to_numpy()
X_raw = data.iloc[:-nlag,4:5]; X = X_raw.to_numpy()
date = dates[nlag:]
 
X_train=X[:ei_train,:]; X_test=X[ei_train:,:]
y_train=y[:ei_train,:]; y_test=y[ei_train:,:]
 
# convert torch.Tensor
X_train = torch.Tensor(X_train)
X_test  = torch.Tensor(X_test)
y_train = torch.Tensor(y_train)
y_test  = torch.Tensor(y_test)
 
 
cs


[*********************100%***********************]  1 of 1 completed
              Open        High         Low       Close     Adj Close  \
Date                                                                     
2010-01-04   15.615220   15.678981   15.547723   15.610239   15.610239   
2010-01-05   15.620949   15.637387   15.480475   15.541497   15.541497   
2010-01-06   15.588072   15.588072   15.102393   15.149715   15.149715   
2010-01-07   15.178109   15.193053   14.760922   14.797037   14.797037   
2010-01-08   14.744733   15.024933   14.672753   14.994298   14.994298   
...                ...         ...         ...         ...         ...   
2023-05-24  121.879997  122.750000  120.750000  121.639999  121.639999   
2023-05-25  125.209999  125.980003  122.900002  124.349998  124.349998   
2023-05-26  124.065002  126.000000  123.290001  125.430000  125.430000   
2023-05-30  126.290001  126.379997  122.889999  124.639999  124.639999   
2023-05-31  123.699997  124.900002  123.099998  123.370003  123.370003   
 
 
[3375 rows x 6 columns]
DatetimeIndex(['2010-01-04', '2010-01-05', '2010-01-06', '2010-01-07',
               '2010-01-08', '2010-01-11', '2010-01-12', '2010-01-13',
               '2010-01-14', '2010-01-15',
               ...
               '2023-05-17', '2023-05-18', '2023-05-19', '2023-05-22',
               '2023-05-23', '2023-05-24', '2023-05-25', '2023-05-26',
               '2023-05-30', '2023-05-31'],
          dtype='datetime64[ns]', name='Date', length=3375, freq=None)
 
 
cs


A MLP model can be implemented in a class by inheriting nn.Module. __init__() defines layers and forward() makes links between these layers. I think that PyTorch treats a layer and its output type separately. We can use the torchsummary package to display a model summary.

# Define MLP model for univariate time series forecasting
class Dense1(nn.Module):
    def __init__(self, input_size, h_size1, 
                 h_size2, h_size3, output_size):
        super(Dense1, self).__init__()
        
        # Define the Dense layers
        self.dense1 = nn.Linear(input_size,  h_size1)
        self.dense2 = nn.Linear(h_size1,h_size2)
        self.dense3 = nn.Linear(h_size2,h_size3)
        
        # Define the activation function
        self.relu = nn.ReLU()
 
        # Define the output layer
        self.output = nn.Linear(h_size3, output_size)
    
    def forward(self, x):
        
        # Dense layer 1 with ReLU
        out = self.dense1(x);   out = self.relu(out)
        # Dense layer 2 with ReLU
        out = self.dense2(out); out = self.relu(out)
        # Dense layer 2 with ReLU
        out = self.dense3(out); out = self.relu(out)
        # final linear output
        out = self.output(out)
        
        return out
 
 
num_epochs = 5000 
learning_rate = 0.001 
 
# Set the input size, hidden sizes, and output size
input_size   = 1
h_size1 = 64
h_size2 = 32
h_size3 = 16
output_size  = 1
    
# Create an instance of the DenseLayer
model = Dense1(input_size, 
               h_size1, h_size2, h_size3, 
               output_size)
 
criterion = torch.nn.MSELoss()    
optimizer = torch.optim.Adam(model.parameters(), 
                             lr=learning_rate) 
 
from torchsummary import summary
# Display model summary
summary(model, input_size=(input_size,))
cs


----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Linear-1                   [-1, 64]             128
              ReLU-2                   [-1, 64]               0
            Linear-3                   [-1, 32]           2,080
              ReLU-4                   [-1, 32]               0
            Linear-5                   [-1, 16]             528
              ReLU-6                   [-1, 16]               0
            Linear-7                    [-1, 1]              17
================================================================
Total params: 2,753
Trainable params: 2,753
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.00
Params size (MB): 0.01
Estimated Total Size (MB): 0.01
----------------------------------------------------------------
 
cs


This is the main part of learning parameters with data. This is slightly different from the Keras model. In PyTorch, parameter estimation is done by iterating by num_epochs, within which forward and backward passes are carried out sequentially: prediction, calculation of prediction errors, calculation of loss gradient, and updating.

for epoch in range(num_epochs):
    
    # Forward pass: 
    # Perform the forward pass of the model 
    # to obtain the predicted outputs
    outputs = model.forward(X_train) 
    
    # Zero the gradients in the optimizer 
    # before backward pass
    optimizer.zero_grad()  
    
    # Calculate the loss between the true labels
    # and the predicted outputs 
    loss = criterion(outputs, y_train) 
    
    # Backward pass: 
    # Compute gradients of the loss 
    # with respect to model parameters
    loss.backward() 
    
    # Update the model parameters using the gradients 
    # and the optimizer's update rule
    optimizer.step() 
    
    # Print the loss value every 100 epochs
    if epoch % 100 == 0:
        print("Epoch: %d, loss: %1.5f" % (epoch, loss.item())) 
 
 
cs


Epoch: 0, loss: 1362.96155
Epoch: 100, loss: 7.13212
Epoch: 200, loss: 3.94204
           ...
Epoch: 4700, loss: 3.93202
Epoch: 4800, loss: 3.93175
Epoch: 4900, loss: 3.93146
 
cs


After parameter estimation, we can draw forecast output as follows.

# Estimated and Forecasted prices
X_all = torch.Tensor(X)
 
# apply the estimated model to full dataset
train_predict = model(X_all)
predicted = train_predict.detach().numpy()
 
# Create a DataFrame from the numpy array
y_pred = pd.DataFrame(data=predicted)
# Set the dates as the index of the DataFrame
y_pred.index = pd.DatetimeIndex(date)
axv = date[ei_train]
    
# Figure
plt.figure(figsize=(8,3)) 
plt.axvline(x=axv, c='r', linestyle='--'
 
plt.plot(y_raw , label='Actual',    
         color='deeppink',   linewidth=1
plt.plot(y_pred, label='Predicted'
         color='deepskyblue',linewidth=1
 
#plt.title(str(int(nlag)) + '-day-ahead forecasting')
plt.title(str(int(nlag/20)) + '-month-ahead forecasting')
plt.legend()
plt.show() 
 
cs




For clear presentation, I do not apply various deep learning technique such as feature scaling, multiple lagged variables or hyper parameter optimization. But these parts are similar to those of Keras model. Next time, LSTM model will be considered since it can handle time steps.


No comments:

Post a Comment