Документация Engee
Notebook

Прогнозирование многомерных временных рядов гибридным методом

Введение

В задачах анализа и прогнозирования временных рядов классической проблемой остаётся разделение наблюдаемого сигнала на детерминированную и стохастическую компоненты. Если детерминированная часть допускает точное параметрическое описание, то остаток, как правило, содержит коррелированную структуру, которая не сводится к простому шуму, но и не описывается линейными методами. Гибридные схемы, сочетающие классическую декомпозицию с нейросетевым моделированием остатка, позволяют использовать сильные стороны обоих методов.

В настоящей работе рассматривается гибридный метод прогнозирования многомерных временных рядов, включающий три этапа:

  1. удаление линейного тренда методом наименьших квадратов (МНК);
  2. подбор двух доминирующих синусоидальных компонент с оптимизацией частот методом Нелдера-Мида;
  3. прогнозирование остатка рекуррентной нейронной сетью LSTM.

Качество прогноза оценивается метриками RMSE и MAE, а также сопоставляется с моделью без нейросетевого блока.

Рассмотрим некоторые определения, применяемые в данной работе:

  • Тренд – это долговременная, монотонная составляющая временного ряда, отражающая общее направление его изменения: рост, спад или стагнацию.
  • Сезонность – это периодическая составляющая ряда, повторяющаяся через примерно равные интервалы времени с постоянной амплитудой и фазой.
  • LSTM (Long Short-Term Memory, долгая краткосрочная память) – разновидность рекуррентной нейронной сети, способная сохранять информацию о предыдущих состояниях последовательности на длительных интервалах. В отличие от классической рекуррентной нейронная сети, LSTM использует механизм, управляющий записью, чтением и стиранием информации в скрытом состоянии, что позволяет ей эффективно моделировать долгосрочные зависимости во временных рядах.
  • Остаточная энергия – сумма квадратов невязок между сигналом и его моделью: . Это стандартная целевая функция МНК; её минимизация означает поиск параметров модели, при которых она наилучшим образом описывает сигнал. В данной работе остаточная энергия используется как критерий при оптимизации частот синусоид.
  • RMS (Root Mean Square, среднеквадратичное значение) – корень из среднего квадрата величины: ​​. Для остатка ряда RMS показывает типичную амплитуду невязки в тех же единицах, что и сигнал. RMS остатка на обучающем участке характеризует "необъяснённую" часть ряда, которая далее передаётся на вход LSTM.
  • RMSE (Root Mean Square Error, корень из средней квадратичной ошибки) – метрика качества прогноза: ​. Измеряется в единицах сигнала и сильнее штрафует крупные промахи, чем MAE.
  • MAE (Mean Absolute Error, средняя абсолютная ошибка) – метрика: . Показывает среднюю «типичную» ошибку прогноза в единицах сигнала и менее чувствительна к выбросам, чем RMSE.
  • МНК (метод наименьших квадратов) – способ оценки параметров линейной модели, минимизирующий сумму квадратов невязок. Применяется для оценки линейного тренда и амплитуд синусоид.
  • Метод Нелдера-Мида – алгоритм безусловной оптимизации, не требующий производных; используется для уточнения пары частот синусоид по критерию остаточной энергии.

Присоединение библиотек

Подключим необходимые библиотеки:

Flux – обучение и применение нейронной сети;

Optim – оптимизация частот синусоид;

Statistics– вычисление статистик остатка для нормировки;

LinearAlgebra – решение МНК-систем для вычисления линейного тренда и амплитуд синусоид;

Random – для воспроизводимости обучения.

In [ ]:
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)
Out[0]:
TaskLocalRNG()

Генерация данных

Сгенерируем три временных ряда , , длиной 5000 точек.

Каждый ряд содержит общий тренд и сезонную компоненту со сдвигом фазы и константу . Для и амплитуда общего тренда масштабируется, а фаза сезонной составляющей сдвигается.

Построим график всех трёх рядов.

In [ ]:
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="Временные ряды", lw=1.8, label=["X" "Y" "Z"])
Out[0]:

Разбиение на обучающую и тестовую выборки

Разобьём данные на обучающую (первые 4000 точек) и тестовую (последние 1000 точек) выборки, сформируем общий массив данные_все (5000×3) и вектор времени tt. Отдельно выделим временную ось t_обуч для обучающего участка.

In [ ]:
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_обуч]
Out[0]:
4000-element Vector{Float64}:
    0.0
    1.0
    2.0
    3.0
    4.0
    5.0
    6.0
    7.0
    8.0
    9.0
   10.0
   11.0
   12.0
    ⋮
 3988.0
 3989.0
 3990.0
 3991.0
 3992.0
 3993.0
 3994.0
 3995.0
 3996.0
 3997.0
 3998.0
 3999.0

Удаление линейного тренда

Оценим линейный тренд по обучающей выборке методом наименьших квадратов (МНК) для каждого ряда, построим тренд на всей длине и вычтем его из исходных данных. В результате получим детрендированные данные детренд_все и детренд_обуч.

In [ ]:
A = hcat(ones(N_обуч), t_обуч)
коэф = A \ данные_обуч                         
тренд_все = hcat(ones(N), tt) * коэф   
детренд_все   = данные_все   .- тренд_все
детренд_обуч  = детренд_все[1:N_обуч, :]
Out[0]:
4000×3 Matrix{Float64}:
 -2.05727    2.91277    2.60508
 -1.92836    2.9912     2.62286
 -1.79948    3.06851    2.63953
 -1.67064    3.14469    2.65511
 -1.54187    3.21973    2.66961
 -1.41318    3.29362    2.68304
 -1.28461    3.36634    2.6954
 -1.15617    3.43789    2.70671
 -1.02789    3.50826    2.71697
 -0.89978    3.57744    2.7262
 -0.771873   3.64541    2.73441
 -0.644186   3.71218    2.7416
 -0.516743   3.77772    2.74779
  ⋮                    
 -1.76054   -0.549052  -7.43301
 -1.69803   -0.570166  -7.49135
 -1.63633   -0.592207  -7.54943
 -1.57544   -0.615167  -7.60724
 -1.51537   -0.639036  -7.66476
 -1.45614   -0.663805  -7.72197
 -1.39776   -0.689464  -7.77885
 -1.34024   -0.716004  -7.83538
 -1.28359   -0.743413  -7.89154
 -1.22782   -0.771681  -7.94732
 -1.17294   -0.800798  -8.00268
 -1.11896   -0.830753  -8.05761

Функция остаточной энергии

Определим функцию остаточная_энергия(частоты, сигнал, t), которая по заданной паре частот строит базис из константы, косинусов и синусов, подбирает коэффициенты МНК и возвращает сумму квадратов остатков. Эта функция будет использоваться как целевая при оптимизации частот.

In [ ]:
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
Out[0]:
остаточная_энергия (generic function with 1 method)

Функция поиска двух синусоид

Определим функцию синусоиды(сигнал, t), которая ищет две доминирующие частоты в сигнале: сначала грубым перебором по сетке находит первую частоту, затем, вычтя её вклад, вторую, а после уточняет обе частоты методом NelderMead. Возвращает найденную пару (f1, f2).

In [ ]:
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
Out[0]:
синусоиды (generic function with 1 method)

Подбор синусоид по рядам

Применим функцию синусоиды к каждому из трёх детрендированных рядов, сравним найденные частоты с истинными значениями и , выполним финальный МНК-подбор амплитуд на обучающем участке и построим синусоидальные компоненты на всей длине ряда. Выведем RMS по каждому ряду.

In [ ]:
_, 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
┌ Warning: x_tol is deprecated. Use x_abstol or x_reltol instead. The provided value (1.0e-12) will be used as x_abstol.
└ @ Optim /opt/depot/demos/packages/Optim/z6105/src/types.jl:128
┌ Warning: f_tol is deprecated. Use f_abstol or f_reltol instead. The provided value (1.0e-15) will be used as f_reltol.
└ @ Optim /opt/depot/demos/packages/Optim/z6105/src/types.jl:138
Ряд 1: f1 = 0.0009511555  (истина 0.0009549297, Δ = 3.7741976086258684e-6)
       f2 = 0.0025495928  (истина 0.0025464791, Δ = 3.113760232510091e-6)
Ряд 2: f1 = 0.000951996  (истина 0.0009549297, Δ = 2.933630766334557e-6)
       f2 = 0.0025460388  (истина 0.0025464791, Δ = 4.402527278529571e-7)
┌ Warning: x_tol is deprecated. Use x_abstol or x_reltol instead. The provided value (1.0e-12) will be used as x_abstol.
└ @ Optim /opt/depot/demos/packages/Optim/z6105/src/types.jl:128
┌ Warning: f_tol is deprecated. Use f_abstol or f_reltol instead. The provided value (1.0e-15) will be used as f_reltol.
└ @ Optim /opt/depot/demos/packages/Optim/z6105/src/types.jl:138
Ряд 3: f1 = 0.0009523338  (истина 0.0009549297, Δ = 2.5958343298196265e-6)
       f2 = 0.0025436879  (истина 0.0025464791, Δ = 2.791216266237375e-6)

RMS:
┌ Warning: x_tol is deprecated. Use x_abstol or x_reltol instead. The provided value (1.0e-12) will be used as x_abstol.
└ @ Optim /opt/depot/demos/packages/Optim/z6105/src/types.jl:128
┌ Warning: f_tol is deprecated. Use f_abstol or f_reltol instead. The provided value (1.0e-15) will be used as f_reltol.
└ @ Optim /opt/depot/demos/packages/Optim/z6105/src/types.jl:138
  Ряд 1: 1.02
  Ряд 2: 0.662
  Ряд 3: 0.815

Нормировка остатка

Вычислим среднее и стандартное отклонение остатка по обучающей выборке, заменим слишком маленькие на 1.0 и получим нормированный остаток остаток_норм с нулевым средним и единичной дисперсией по каждому ряду.

In [ ]:
μ = mean(остаток_обуч, dims=1)
σ = std(остаток_обуч,  dims=1)
σ[σ .< 1e-10] .= 1.0
остаток_норм = (остаток_обуч .- μ) ./ σ     
Out[0]:
4000×3 Matrix{Float64}:
 -1.84503  -2.04697  -1.95095
 -1.84156  -2.04278  -1.94441
 -1.83812  -2.03856  -1.93787
 -1.83471  -2.03432  -1.93134
 -1.83132  -2.03005  -1.92483
 -1.82797  -2.02577  -1.91834
 -1.82465  -2.02146  -1.91185
 -1.82135  -2.01712  -1.90539
 -1.81809  -2.01277  -1.89894
 -1.81485  -2.0084   -1.89252
 -1.81165  -2.00401  -1.88611
 -1.80848  -1.99959  -1.87972
 -1.80534  -1.99516  -1.87336
  ⋮                  
  1.69907   1.76467   1.58288
  1.70712   1.76956   1.58819
  1.71519   1.77445   1.59355
  1.72328   1.77934   1.59896
  1.73139   1.78422   1.60442
  1.73952   1.7891    1.60992
  1.74766   1.79398   1.61547
  1.75582   1.79885   1.62107
  1.76399   1.80372   1.62671
  1.77218   1.80859   1.6324
  1.78038   1.81345   1.63814
  1.78859   1.8183    1.64392

Формирование окон для LSTM

Сформируем обучающие последовательности для LSTM: длина окна L = 100, по каждому из трёх каналов. Получим массивы X_посл формы (3, 100, 3900) – входные окна, и Y_посл формы (3, 3900) – целевые значения.

In [ ]:
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

Определим модель LSTM (вход 3 → скрытое состояние 32 → выход 3) с полносвязным слоем на выходе. Настроим оптимизатор Adam и обучим модель 10 эпох мини-батчами по 64 примера, минимизируя MSE между предсказанием на последнем шаге окна и истинным значением. Выведем значение функции потерь на каждой эпохе.

In [ ]:
модель = 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
эпоха 1  потери = 0.5232
эпоха 2  потери = 0.01699
эпоха 3  потери = 0.004644
эпоха 4  потери = 0.003566
эпоха 5  потери = 0.003047
эпоха 6  потери = 0.002708
эпоха 7  потери = 0.002429
эпоха 8  потери = 0.002257
эпоха 9  потери = 0.002112
эпоха 10  потери = 0.002007

Прогноз остатка

Определим функцию прогноз_остатка, которая рекурсивно прогнозирует остаток на N_тест шагов вперёд: на каждом шаге берётся последнее окно длиной L, подаётся в LSTM, полученное предсказание добавляется в буфер и используется для формирования следующего окна. Затем денормируем прогноз обратно в исходный масштаб с помощью и . В результате получим остаток_полн размерностью 5000×3.

In [ ]:
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_тест)
Out[0]:
5000×3 Matrix{Float64}:
 -1.88057  -1.35504  -1.58977
 -1.87703  -1.35226  -1.58444
 -1.87352  -1.34947  -1.57911
 -1.87004  -1.34666  -1.57379
 -1.86659  -1.34384  -1.56849
 -1.86317  -1.341    -1.56319
 -1.85979  -1.33815  -1.55791
 -1.85643  -1.33528  -1.55265
 -1.8531   -1.3324   -1.54739
 -1.84981  -1.3295   -1.54216
 -1.84654  -1.3266   -1.53693
 -1.84331  -1.32367  -1.53173
 -1.84011  -1.32074  -1.52655
  ⋮                  
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256

Итоговый прогноз

Сложим три компоненты: линейный тренд тренд_все, синусоидальную составляющуюсинус_все и прогноз остатка остаток_полн. Получим полный прогноз y_полн и выделим тестовую часть y_прогноз. Истинные значения для сравнения сохраним в y_истина.

In [ ]:
y_полн = тренд_все .+ синус_все .+ остаток_полн
y_прогноз = y_полн[N_обуч+1:end, :]
y_истина = данные_тест
Out[0]:
1000×3 Matrix{Float64}:
 51.3555  47.4012  49.5032
 51.4128  47.3738  49.4556
 51.4691  47.3456  49.4086
 51.5245  47.3166  49.362
 51.5789  47.2869  49.316
 51.6324  47.2563  49.2705
 51.685   47.2251  49.2256
 51.7365  47.193   49.1813
 51.7871  47.1603  49.1375
 51.8368  47.1269  49.0944
 51.8854  47.0927  49.0519
 51.9331  47.0579  49.0101
 51.9797  47.0224  48.969
  ⋮                
 47.1826  43.8875  57.4425
 47.1695  43.9503  57.5326
 47.1579  44.0142  57.6226
 47.1479  44.0793  57.7128
 47.1393  44.1455  57.8029
 47.1323  44.2128  57.893
 47.1268  44.2812  57.9831
 47.1228  44.3507  58.0731
 47.1204  44.4213  58.163
 47.1196  44.4929  58.2529
 47.1203  44.5655  58.3426
 47.1225  44.6392  58.4322

Метрики качества

Вычислим метрики RMSE и MAE для каждого из трёх рядов на тестовом участке. Дополнительно рассчитаем прогноз без учёта LSTM для сравнения. Он покажет, насколько LSTM улучшает качество прогноза.

In [ ]:
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("Ряд", 5), rpad("RMSE (с 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
Качество прогноза (t = 4000..4999):

Ряд  RMSE (с LSTM)   RMSE / размах   MAE (с LSTM)    RMSE без LSTM   Улучшение по RMSE
X    0.517           ≈ 2.3 %         0.472           2.327           в 4.5 раза
Y    0.38            ≈ 2.0 %         0.333           1.477           в 3.9 раза
Z    0.516           ≈ 2.1 %         0.45            1.845           в 3.6 раза

Визуализация прогноза

Построим три подграфика (, , ), на каждом обучающий участок синим, истинные значения на тесте оранжевым пунктиром и прогноз красным.

In [ ]:
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))

Заключение

В данной работе построен и исследован гибридный метод прогнозирования многомерных временных рядов, сочетающий МНК-декомпозицию с LSTM-моделированием остатка. Предложенный метод показал высокое качество: ошибка прогноза на тестовом участке составила ≈ 2 % от размаха сигнала (RMSE ≈ 0.38–0.52 при размахе ≈ 25 ед.), а по сравнению с моделью без нейросетевого блока точность улучшилась в 3.5–4.5 раза.

Гибридные схемы востребованы в широком круге прикладных задач: прогнозирование энергопотребления и нагрузки электросетей, спроса и продаж, финансовых и товарных рядов, метеорологических и климатических показателей, параметров технологических процессов и телеметрии, трафика и логистики. Классическая декомпозиция обеспечивает интерпретируемость и устойчивость к малым выборкам, а LSTM – учёт нелинейных и долгосрочных зависимостей в остатке.