Boost Forecasting With Multiprocessing | Towards Data Science

When forecasting time series data, sometimes you need both speed and accuracy. When working from a laptop with Python, try multiprocessing to get the best of both worlds.
You are trying to optimize a multiprocessing problem in Python on your local machine
You are forecasting time series data with Statsmodels ARIMA, Facebook Prophet, or PyTorch LSTM
You are trying to decide whether multiprocessing is an optimal configuration with PyTorch LSTM if you don’t have access to a GPU
Most of the time you have plenty of data. Some of the time, you have plenty of time to analyze the data, but most of the time, you need to produce something in no time at all. But, as the quantity of data increases, you need bigger and better models to produce useful information. Whether you need to speed things up to debug code or present something for prime time, big data and deep models will to chew up valuable time you cannot afford to lose.
Can I do deep learning faster on my MacBook without a GPU?
 Forecasting performance of Facebook Prophet with and without multiprocessing enabled. Data comprises simulated stock data with a size of 60 batches x 28 chunks x 15 data points per chunk (approx 25,000). Training model on a window size of 15 and predicting the next 15 prices in sequence.   
To forecast an accurate trend and predict future data points, you need lots of data, a great model, or a mix of both. But debugging can be challenging. It might take 5, 10, or 20 minutes to know that a single line of code does not work correctly because that’s how long a model takes to run.
To cut down the amount of time you need to cycle through changes, a faster machine, a GPU, or some kind of cloud solution will do the trick — but what if you have none of the above? What you want to make sure your model functions correctly before spending money on a cloud-based job? Or, what if this is what you are going to deploy in an app? In any case, we want more speed!
In this article, I share the results of a few experiments that might help you architect your own multiprocessing solution for a speed boost.
 Forecasting performance of Statsmodels ARIMA with and without multiprocessing enabled. Data comprises simulated stock data with a size of 60 batches x 28 chunks x 15 data points per chunk (approx 25,000). Training model on a window size of 15 and predicting the next 15 prices in sequence.   
First, a caveat. I am just a guy with a laptop that ran some experiments — your results may vary and my findings and code should be scrutinized. Caveats aside, I think these findings are worth sharing and can save you some time or, at least, make you think about some interesting problems.
It is possible to implement multiprocessing in python with ARIMA, Facebook Prophet, and PyTorchFor Facebook Prophet, 8x pooled processes on an 8-core machine seems to produce optimal results — to the tune of a 70% decrease in clock time for the same data and same computation.For statsmodels ARIMA, 6x pooled processes produced the most optimal results but the difference between 6 and 8 processors was negligible — a 50% decrease in clock time.For a single-layer LSTM built with PyTorch, there were two interesting findings. First, the standard configuration (no multiprocessing) outperformed all other configurations with pooled processes and the clock time to complete jobs got progressively worse with pooled processes. Second, the error, as measured by mean absolute percentage error (MAPE) appeared to steadily decrease as the number of processes increased.
It is possible to implement multiprocessing in python with ARIMA, Facebook Prophet, and PyTorch
For Facebook Prophet, 8x pooled processes on an 8-core machine seems to produce optimal results — to the tune of a 70% decrease in clock time for the same data and same computation.
For statsmodels ARIMA, 6x pooled processes produced the most optimal results but the difference between 6 and 8 processors was negligible — a 50% decrease in clock time.
For a single-layer LSTM built with PyTorch, there were two interesting findings. First, the standard configuration (no multiprocessing) outperformed all other configurations with pooled processes and the clock time to complete jobs got progressively worse with pooled processes. Second, the error, as measured by mean absolute percentage error (MAPE) appeared to steadily decrease as the number of processes increased.
Note about clock time, etc: I averaged the results of three runs per configuration and took clock time as the difference between start time and end time with time.time().
 Forecasting performance of PyTorch LSTM neural network with and without multiprocessing enabled. Data comprises simulated stock data with a window size of 15, step size of 1 across a dataset comprising prices per minute from 9 am to 4 pm for 60 days. Training on every 15 data points to predict the next in sequence. LR=0.001,  epochs=5, batch size=16, n layers=1.  
Faster results? Sounds great, but how? In the next few sections I step through key pointers on how to modify ARIMA, Prophet, and LSTM models to implement multiprocessing.
With Autoregressive Integrated Moving Average module, or “ARIMA” for short, you might be interested in forecasting a trend that has some kind of seasonality. Since ARIMA fundamentals are well covered here and here among many other articles, I will go directly to demonstrating a set up with multiprocessing.
A Basic Setup for ARIMA
# import dependenciesfrom statsmodels.tsa.arima.model import ARIMA# some data...x_i = [4.1, 3.1, 2.5, 4.0]# a simple model on the datam = ARIMA(x_i, order=(0,1,0))# fit the model m_fit = m.fit()# forecast/predict the next n stepsyhat = m_fit.forecast(steps=1)# then, compare yhat predictions to y targets...
Prepare Data for Forecasting
Since ARIMA is best at forecasting a few steps after the training set, I modified a data chunking script that works on a dataframe having a datetime column and a data column. In my experiment, the data are the scaled prices of a stock for each minute of the trading day starting at 9 am and ending at 4 pm.
# dataframe examplecolumns -> 'ds', 'y''ds' data -> 2019-01-01 09:00:00, ...'y' data -> 1.4, ...
The functions that follow in data_chunker.py, return a list of tuples of dataframes where each pair is a chunk of 15 minute data.
Given some chunked data, we can iterate through the list of dataframes to compute a forecast for each chunk with a function wrapper for ARIMA.
Prepare ARIMA for Forecasting
Given some chunked data and a function wrapper for ARIMA as outlined above, we can call the run_arima() function by iterating over the chunked data which is just a list of dataframes.
This step of organizing data into an iterable object and applying arima as a function to that object is very important because it sets up how to implement multiprocessing. If you get this part right, multiprocessing becomes a simple task.
# from main.py, run arima as list comprehensionmodel = run_arimaresults = [model(i) for i in chunked_data]# from main, iterate through a list of data and with arima function# within arima function, iterate through the list of chunked data# return a forecast of each 15 minute period
Import PyTorch Multiprocessor
Since we are working with Facebook APIs (PyTorch and Facebook Prophet), I imported the PyTorch multiprocessor library which provides coverage over the base multiprocessor library.
import torch.multiprocessing as mp# the torch mp is basically the same as# import multiprocessing as mp# get number of available processesprocess_count = mp.cpu_count()
Making Sense of cpu_count()
The cpu_count() method returns a total count of available processes. From what I can gather, on a MacBook Pro with 8 cores, cpu_count() returns a value of 16 — I believe this means 2 hyper-threaded CPU processes per core. During performance testing, as mentioned above, pooling 8x processes performed better than just 2x processes and better than 14x processes. My brief conclusion is that 2x processes are not taking full advantage of the potential compute resources while 14x processes is relatively worse because the processes are fighting for resources on each core.
Implement Multiprocessing for ARIMA
Now that we have chunked data, the arima function wrapper and a method to iterate the arima function, time to implement multiprocessing.
# this needs to be run from main.py or something similar# import dependencies as required here# import helper functions and arima wrapper here# functions must be guarded in thisif __name__ == '__main__':  model = run_arima  # set a pool of 8  p = mp.Pool(8)  # iterate run_arima over chunked data  results = list(p.imap(model, chunked_data))    # close and join pools per the docs  p.close()  p.join()
The code snip above presents a simplified case but the next code gist shows how to manage sending the function parameters while multiprocessing and also, how to add the tqdm progress bar.
Based on the above ARIMA example, we have a template for multiprocessing.
Organize data into chunks as an iterable object like a list of dataframes
Apply the forecasting model to the chunked object
The steps for Facebook Prophet are very similar. The main exception is the actual code for Prophet which expects to consume dataframes and has certain requirements for column names to be ‘ds’ for timestamps and ‘y’ for data.
Similarly to ARIMA, we can just assign the run_prophet function to a model object and iterate with pooling over the model and data.
Here, the framework is not exactly the same with LSTM as it is with ARIMA or Prophet — but that’s okay — they are vastly different models. How is an LSTM different? Whereas ARIMA and Facebook Prophet learn a model and predict the sequences that immediately follow, we can use the LSTM to predict sequences that are far outside the training window. As a simple example, ARIMA and Prophet might be good at training on a week of data and predicting the next week, but they might do poorly at predicting over the next several months — this is where the LSTM can do well.
I adapted the example code from PyTorch to create a multiprocessing version of the LSTM. Given a function that creates the LSTM model and a function that implements training iterations, the following application of multiprocessing can work.
# given a model of LSTM# given a dict of params# given a function called train_modelmodel.share_memory()processes = []# assign processesfor rank in tqdm(range(8)):    # pool data for train_scaled to function train_model    p = mp.Process(target=train_model, kwargs=params)    p.start()    processes.append(p)# join processes after computefor p in tqdm(processes):    p.join()
The key insight that I got from the PyTorch example is that the train_model() function should iterate through the model and data loader objects, while calling a separate function to execute training in each epoch. Consider that without multiprocessing, the above call to train_model() can be as simple as calling the function and passing the data and parameters as a dictionary.
# given a dictionary of paramstrain_model(**params)
I think this highlights an interesting difference between pooling resources for a neural network like LSTM and simpler models such as ARIMA, but I need to learn more about how computations work within each process — perhaps the topic of a future post.
Without going too deep into how to actually construct an LSTM (a whole other subject on its own), here are three tips for configuring the architecture assuming you have some familiarity with the topic:
In train_model(), call the PyTorch DataLoader object, loss function, and initialize the optimizer, then loop through n epochs. Within each epoch, call another function train_epoch().In train_epoch(), enumerate through the DataLoader object and apply the LSTM model there.From main.py, to pass all your arguments for the LSTM, i.e. number of layers, and batch size, etc, create a dictionary and pass it to the process function as kwargs=params. Then, within the function signature for train_model, make sure to have a **kwargs parameter to receive the dictionary values.
In train_model(), call the PyTorch DataLoader object, loss function, and initialize the optimizer, then loop through n epochs. Within each epoch, call another function train_epoch().
In train_epoch(), enumerate through the DataLoader object and apply the LSTM model there.
From main.py, to pass all your arguments for the LSTM, i.e. number of layers, and batch size, etc, create a dictionary and pass it to the process function as kwargs=params. Then, within the function signature for train_model, make sure to have a **kwargs parameter to receive the dictionary values.
It is worth mentioning that, in at least my project’s implementation, while this method of pooling ‘worked’ it performed relatively worse in terms of speed as compared to not pooling. On that front alone, it might not be worth the time to implement. On the other hand, I may have missed an important step to make it work more efficiently since I have some implicitly nested for-loops. In addition, there is also the curious case of why error seems to improve when we have many processes running in parallel — a topic of interest to explore some other time.
The full implementation here: https://github.com/justinhchae/stocks and also, a production credit to my project partner (knowledgewithin) for configuring the LSTM model.
In this article, I demonstrated how it is possible to adapt a multiprocessing framework to forecasting models from ARIMA and Facebook Prophet on the same dataset. In both cases, multiprocessing resulted in between 70% to 50% time decreases by increasing the iterations per second. In addition, I demonstrated how to apply the PyTorch Hogwild example to get multiprocessing with an LSTM to work. However, unlike ARIMA and Prophet, the LSTM did not improve in terms of speed and, in fact, degraded with more processes. Strangely though, the LSTM error improved as the number of parallel processes increased — a subject that might be worth further investigation.
I skimmed over a few relatively trivial steps to provide an overview of how to apply the multiprocessing concept, but they are still important. To get an idea of the specifics from data processing to model setup, see my GitHub repository.
I initially set out to see if I could improve deep learning on my laptop without a GPU, i.e. learn the LSTM faster by pooling CPUs on my MacBook. Unfortunately, it seems that the pooled CPUs do not offer speed advantages for the LSTM and better hardware may ultimately be necessary in this scenario.
As for next steps, it should be worthwhile to migrate this code to something like Google Colab and see how it performs with a GPU.
Edit: If you are looking for a deeper dive to get started with multiprocessing in Python from the ground up, check out a companion post that I wrote to cover all the basics.
python.plainenglish.io
Project Repository
Statsmodels ARIMA, documentation
Applied Forecasting Article by TDS
Source for Prophet Multiprocessing Framework by Spike
Data Chunker Script
PyTorch Multiprocessing Best Practices
Facebook Prophet API
About Multiprocessing by Contentsquare Engine
About LSTM, by TDS