Predicting multidimensional time series using a hybrid method
Introduction
In time series analysis and forecasting, the classical problem remains the separation of the observed signal into deterministic and stochastic components. If the deterministic part allows for an accurate parametric description, then the remainder, as a rule, contains a correlated structure that is not reduced to simple noise, but is also not described by linear methods. Hybrid schemes combining classical decomposition with neural network modeling of the remainder make it possible to use the strengths of both methods.
In this paper, we consider a hybrid method for predicting multidimensional time series, which includes three stages:
- Least Squares Linear trend Removal (OLS);
- Selection of two dominant sinusoidal components with frequency optimization by the Nelder-Mead method;
- Prediction of the remainder by the recurrent neural network LSTM.
The quality of the forecast is assessed using RMSE and MAE metrics, and is also compared with a model without a neural network block.
Let's consider some definitions used in this work.:
- A trend is a long–term, monotonous component of a time series that reflects the general direction of its change: growth, decline, or stagnation.
- Seasonality is a periodic component of a series that repeats at approximately equal time intervals with a constant amplitude and phase.
- LSTM (Long Short-Term Memory) is a type of recurrent neural network capable of storing information about previous states of a sequence at long intervals. Unlike the classical recurrent neural network, LSTM uses a mechanism that controls the writing, reading and erasing of information in a hidden state, which allows it to effectively model long-term dependencies in time series.
- Residual energy is the sum of the squares of the discrepancies between the signal and its model.: . This is a standard OLS objective function; minimizing it means finding the model parameters at which it best describes the signal. In this paper, the residual energy is used as a criterion for optimizing the frequencies of sinusoids.
- RMS (Root Mean Square, RMS value) – the root of the average square of the value: . For the remainder of the series, RMS shows the typical amplitude of the discrepancy in the same units as the signal. The RMS of the remainder in the training area characterizes the "unexplained" part of the series, which is then transmitted to the LSTM input.
- RMSE (Root Mean Square Error, root of the mean square error) is a metric of forecast quality: . It is measured in signal units and penalizes large misses more strongly than MAE.
- MAE (Mean Absolute Error) – metric: . It shows the average "typical" forecast error in signal units and is less sensitive to outliers than RMSE.
- MNC (least squares method) is a method for estimating the parameters of a linear model that minimizes the sum of squared residuals. It is used to estimate the linear trend and the amplitudes of sinusoids.
- The Nelder-Mead method is an unconditional optimization algorithm that does not require derivatives; it is used to refine a pair of sinusoid frequencies by the criterion of residual energy.
Joining libraries
Connecting the necessary libraries:
Flux – neural network training and application;
Optim – optimization of sinusoid frequencies;
Statistics– calculation of remainder statistics for normalization;
LinearAlgebra – solution of MNC systems for calculating linear trend and sinusoid amplitudes;
Random – for reproducibility of learning.
for pkg in ["Flux", "Optim", "Statistics", "LinearAlgebra", "Random"]
try
eval(Meta.parse("using $pkg"))
catch
Pkg.add(pkg)
eval(Meta.parse("using $pkg"))
end
end
Random.seed!(42)
Data generation
We will generate three time series , , 5000 points long.
Each row contains a general trend and the seasonal component with phase shift and constant . For and The amplitude of the overall trend is scaling, and the phase of the seasonal component is shifting.
Let's plot all three rows.
t = 0:1:4999
B = 8 .* sin.(0.006 .* t) .+ 0.006 .* t
c = 30
X = B .+ 5 .* sin.(0.016 .* t) .+ c
Y = 0.8 .* B .+ 5 .* sin.(0.016 .* t .+ π/3) .+ c
Z = 1.2 .* B .+ 5 .* sin.(0.016 .* t .+ 2π/3) .+ c
plot(t, [X Y Z], title="Time series", lw=1.8, label=["X" "Y" "Z"])
Splitting into training and test samples
Let's split the data into a training sample (the first 4000 points) and a test sample (the last 1000 points), and form a common array. данные_все (5000×3) and the time vector tt. Let's highlight the time axis separately. t_обуч for the training area.
N_обуч = 4000
N_тест = 1000
N = N_обуч + N_тест
данные_все = hcat(X, Y, Z)
данные_обуч = данные_все[1:N_обуч, :]
данные_тест = данные_все[N_обуч+1:end, :]
tt = collect(Float64, 0:N-1)
t_обуч = tt[1:N_обуч]
Removing a linear trend
Let's estimate the linear trend using the least squares (OLS) training sample for each row, plot the trend along its entire length and subtract it from the source data. As a result, we get the detrended data. детренд_все and детренд_обуч.
A = hcat(ones(N_обуч), t_обуч)
коэф = A \ данные_обуч
тренд_все = hcat(ones(N), tt) * коэф
детренд_все = данные_все .- тренд_все
детренд_обуч = детренд_все[1:N_обуч, :]
Residual energy function
Define the function остаточная_энергия(частоты, сигнал, t), which, based on a given pair of frequencies, builds a basis of constants, cosines and sines, selects the coefficients of the OLS and returns the sum of the squares of the residuals. This function will be used as a target function for frequency optimization.
function остаточная_энергия(частоты, сигнал, t)
f1, f2 = частоты
(f1 <= 0 || f2 <= 0) && return 1e30
M = hcat(ones(length(t)),
cos.(2π*f1*t), sin.(2π*f1*t),
cos.(2π*f2*t), sin.(2π*f2*t))
β = M \ сигнал
r = сигнал .- M*β
return sum(abs2, r)
end
Two sinusoids search function
Define the function синусоиды(сигнал, t), which searches for two dominant frequencies in the signal: first, it finds the first frequency by brute force searching through the grid, then, by subtracting its contribution, the second, and then clarifies both frequencies using the NelderMead method. Returns the found pair (f1, f2).
function синусоиды(сигнал, t)
сетка_частот = 1e-5 : 1e-5 : 1.2e-2
лучшая_f1 = 0.0; лучший_r = Inf
for f in сетка_частот
M = hcat(ones(length(t)), cos.(2π*f*t), sin.(2π*f*t))
β = M \ сигнал
r = sum(abs2, сигнал .- M*β)
if r < лучший_r
лучший_r = r; лучшая_f1 = f
end
end
M1 = hcat(ones(length(t)), cos.(2π*лучшая_f1*t), sin.(2π*лучшая_f1*t))
сигнал2 = сигнал .- M1 * (M1 \ сигнал)
лучшая_f2 = 0.0; лучший_r2 = Inf
for f in сетка_частот
abs(f - лучшая_f1) < 5e-4 && continue
M = hcat(ones(length(t)), cos.(2π*f*t), sin.(2π*f*t))
β = M \ сигнал2
r = sum(abs2, сигнал2 .- M*β)
if r < лучший_r2
лучший_r2 = r; лучшая_f2 = f
end
end
целевая = частоты -> остаточная_энергия(частоты, сигнал, t)
res = Optim.optimize(целевая, [лучшая_f1, лучшая_f2],
NelderMead(),
Optim.Options(x_tol=1e-12, f_tol=1e-15,
iterations=N))
f1, f2 = Optim.minimizer(res)
return f1, f2
end
Selection of sinusoids by series
Apply the function синусоиды for each of the three determined series, compare the found frequencies with the true values. and . we will perform the final MNC selection of amplitudes in the training area and construct sinusoidal components along the entire length of the row. We will output RMS for each row.
_, n_сигн = size(данные_все)
синус_все = zeros(N, n_сигн)
частоты_итог = zeros(n_сигн, 2)
for k in 1:n_сигн
сигнал = детренд_обуч[:, k]
f1, f2 = синусоиды(сигнал, t_обуч)
частоты_итог[k, :] = [f1, f2]
f1_истина = 0.006 / (2π)
f2_истина = 0.016 / (2π)
println("Ряд $k: f1 = $(round(f1, digits=10)) (истина $(round(f1_истина, digits=10)), Δ = $(abs(f1-f1_истина)))")
println(" f2 = $(round(f2, digits=10)) (истина $(round(f2_истина, digits=10)), Δ = $(abs(f2-f2_истина)))")
M_обуч = hcat(ones(N_обуч),
cos.(2π*f1*t_обуч), sin.(2π*f1*t_обуч),
cos.(2π*f2*t_обуч), sin.(2π*f2*t_обуч))
β = M_обуч \ сигнал
M_все = hcat(ones(N),
cos.(2π*f1*tt), sin.(2π*f1*tt),
cos.(2π*f2*tt), sin.(2π*f2*tt))
синус_все[:, k] = M_все * β
end
остаток_все = детренд_все .- синус_все
остаток_обуч = остаток_все[1:N_обуч, :]
println("\nRMS:")
for k in 1:n_сигн
println(" Ряд $k: $(round(sqrt(mean(abs2, остаток_обуч[:,k])), sigdigits=3))")
end
Normalization of the balance
Calculate the average and the standard deviation the remainder of the training sample, let's replace the ones that are too small by 1.0 , we get the normalized remainder . остаток_норм with zero mean and unit variance for each row.
μ = mean(остаток_обуч, dims=1)
σ = std(остаток_обуч, dims=1)
σ[σ .< 1e-10] .= 1.0
остаток_норм = (остаток_обуч .- μ) ./ σ
Creating windows for LSTM
Let's create training sequences for LSTM: the window length is L = 100, for each of the three channels. Getting arrays X_посл forms (3, 100, 3900) – entrance windows, and Y_посл The forms (3, 3900) are the target values.
L = 100
N = N_обуч - L
K = 3
X_посл = zeros(Float32, K, L, N)
Y_посл = zeros(Float32, K, N)
for i in 1:N
X_посл[:, :, i] = Float32.(остаток_норм[i:i+L-1, :]')
Y_посл[:, i] = Float32.(остаток_норм[i+L, :])
end
LSTM Training
Let's define an LSTM model (input 3 → hidden state 32 → output 3) with a fully connected layer at the output. Let's set up the Adam optimizer and train the model with 10 epochs in mini-batches of 64 examples each, minimizing the MSE between the prediction at the last step of the window and the true value. We will output the value of the loss function for each epoch.
модель = Chain(LSTM(K => 32), Dense(32 => K))
function потери(модель, x, y)
Flux.reset!(модель)
ŷ = модель(x)
return Flux.mse(ŷ[:, end, :], y)
end
сост_опт = Flux.setup(Flux.Adam(1e-3), модель)
размер_батча = 64
эпох = 10
n_батчей = div(N, размер_батча)
for эпоха in 1:эпох
перест = randperm(N)
потери_эпохи = 0.0
for b in 1:n_батчей
idx = перест[(b-1)*размер_батча+1 : b*размер_батча]
xb = X_посл[:, :, idx]
yb = Y_посл[:, idx]
Flux.reset!(модель)
l, grads = Flux.withgradient(модель) do m
Flux.mse(m(xb)[:, end, :], yb)
end
Flux.update!(сост_опт, модель, grads[1])
потери_эпохи += l
end
println("эпоха $эпоха потери = $(round(потери_эпохи/n_батчей, sigdigits=4))")
end
Balance forecast
Define the function прогноз_остатка, which recursively predicts the remainder by N_тест steps forward: at each step, the last window of length is taken L, is fed to the LSTM, the received prediction is added to the buffer and used to form the next window. Then we denormalize the forecast back to the original scale using and . As a result, we get остаток_полн the dimension is 5000×3.
function прогноз_остатка(модель, остаток_норм, μ, σ, N_обуч, L, горизонт)
K = 3
всего = N_обуч + горизонт
буфер = zeros(Float32, всего, K)
буфер[1:N_обуч, :] = Float32.(остаток_норм)
Flux.reset!(модель)
for i in 1:горизонт
idx = N_обуч + i
окно = буфер[idx-L:idx-1, :]'
x_вх = reshape(окно, K, L, 1)
ŷ = модель(x_вх)
буфер[idx, :] = vec(ŷ[:, end, :])
end
return Float64.(буфер) .* Float64.(σ) .+ Float64.(μ)
end
остаток_полн = прогноз_остатка(модель, остаток_норм, μ, σ, N_обуч, L, N_тест)
Final forecast
Let's add up the three components: a linear trend тренд_все, the sinusoidal componentсинус_все and the forecast of the balance остаток_полн. We will get the full forecast y_полн and let's highlight the test part y_прогноз. We will save the true values for comparison in y_истина.
y_полн = тренд_все .+ синус_все .+ остаток_полн
y_прогноз = y_полн[N_обуч+1:end, :]
y_истина = данные_тест
Quality metrics
Let's calculate the RMSE and MAE metrics for each of the three rows in the test area. Additionally, we will calculate the forecast without taking into account the LSTM for comparison. It will show how much LSTM improves the quality of the forecast.
rmse(a, p) = sqrt(mean((a .- p).^2))
mae(a, p) = mean(abs.(a .- p))
имена = ["X", "Y", "Z"]
y_прогноз_безLSTM = (тренд_все .+ синус_все .+ vcat(остаток_обуч, zeros(N_тест,3)))[N_обуч+1:end, :]
println("\nКачество прогноза (t = $N_обуч..$(N_обуч+N_тест-1)):\n")
println(rpad("Row", 5), rpad("RMSE (with LSTM)", 16), rpad("RMSE / размах", 16),
rpad("MAE (с LSTM)", 16), rpad("RMSE без LSTM", 16), "Улучшение по RMSE")
for k in 1:3
истина_k = y_истина[:, k]
прогноз_k = y_прогноз[:, k]
безLSTM_k = y_прогноз_безLSTM[:, k]
r_с = rmse(истина_k, прогноз_k)
r_без = rmse(истина_k, безLSTM_k)
m_с = mae(истина_k, прогноз_k)
размах = maximum(истина_k) - minimum(истина_k)
доля = 100 * r_с / размах
улучш = r_без / r_с
println(rpad(имена[k], 5),
rpad(round(r_с, digits=3), 16),
rpad("≈ $(round(доля, digits=1)) %", 16),
rpad(round(m_с, digits=3), 16),
rpad(round(r_без, digits=3), 16),
"в $(round(улучш, digits=1)) раза")
end
Visualization of the forecast
Let's build three subgraphs (, , ), on each training area in blue, the true values on the test are dotted in orange and the forecast is red.
p1 = plot(tt[1:N_обуч], X[1:N_обуч], label="", color=:blue, linewidth=2,
title="X", legend=false)
plot!(p1, tt[N_обуч+1:end], X[N_обуч+1:end], label="",
color=:orange, linestyle=:dash, linewidth=3)
plot!(p1, tt[N_обуч+1:end], y_прогноз[:,1], label="",
color=:red, linewidth=3)
vline!(p1, [N_обуч+0.5], color=:black, linestyle=:dash, label="", linewidth=1)
p2 = plot(tt[1:N_обуч], Y[1:N_обуч], label="", color=:blue, linewidth=2,
title="Y", legend=false)
plot!(p2, tt[N_обуч+1:end], Y[N_обуч+1:end], label="",
color=:orange, linestyle=:dash, linewidth=3)
plot!(p2, tt[N_обуч+1:end], y_прогноз[:,2], label="",
color=:red, linewidth=3)
vline!(p2, [N_обуч+0.5], color=:black, linestyle=:dash, label="", linewidth=1)
p3 = plot(tt[1:N_обуч], Z[1:N_обуч], label="", color=:blue, linewidth=2,
title="Z", legend=false)
plot!(p3, tt[N_обуч+1:end], Z[N_обуч+1:end], label="",
color=:orange, linestyle=:dash, linewidth=3)
plot!(p3, tt[N_обуч+1:end], y_прогноз[:,3], label="",
color=:red, linewidth=3)
vline!(p3, [N_обуч+0.5], color=:black, linestyle=:dash, label="", linewidth=1)
display(plot(p1, p2, p3, layout=(3,1), size=(950, 950), legend=false))
Conclusion
In this paper, a hybrid method for predicting multidimensional time series is constructed and investigated, combining OLS decomposition with LSTM modeling of the remainder. The proposed method showed high quality: the prediction error in the test section was ≈ 2% of the signal range (RMSE ≈ 0.38–0.52 with a range of ≈ 25 units), and compared with the model without a neural network unit, the accuracy improved 3.5–4.5 times.
Hybrid schemes are in demand in a wide range of applications: forecasting energy consumption and load of power grids, demand and sales, financial and product ranges, meteorological and climatic indicators, parameters of technological processes and telemetry, traffic and logistics. Classical decomposition provides interpretability and stability to small samples, while LSTM takes into account non–linear and long-term dependencies in the remainder.