Прогнозирование временных рядов с использованием Pytorch

Прогнозирование временных рядов играет важную роль в анализе данных, имея приложения, начиная от прогнозирования трендов на фондовом рынке и заканчивая прогнозированием погоды. В данной статье мы погрузимся в область прогнозирования временных рядов, используя PyTorch и нейронные сети LSTM (Long Short-Term Memory). Мы также рассмотрим критически важные этапы предварительной обработки данных, от которых зависит точность наших прогнозов.

Содержание

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

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

Скользящие средние и традиционные подходы вроде ARIMA испытывают трудности с захватом долгосрочных зависимостей в данных. LSTM — это тип рекуррентной нейронной сети, который преуспевает в захвате зависимостей через время и способен улавливать сложные паттерны.

В этой статье мы будем использовать Pytorch для прогнозирования временных рядов.

Реализация прогнозирования временных рядов:

Предварительное условие

  • Numpy для работы с массивами

  • Pandas для работы с реляционными или помеченными данными

  • Matplotlib для визуализации данных

  • Seaborn для визуализации данных

  • DateTime работать с данными и временем

  • MinMaxScaler для нормализации

  • PyTorch построить нейронную сеть для обучения

Набор данных:

Здесь мы использовали Yahoo Finance, чтобы получить набор данных фондового рынка.

Чтобы установить Yahoo Finance, мы можем использовать следующую команду

!pip install yfinance

Шаг 1. Импортируйте необходимые библиотеки.

import pandas as pd
import numpy as np
import math
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import seaborn as sns
from sklearn.preprocessing import MinMaxScaler
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader

Шаг 2. Загрузка набора данных

На этом этапе мы используем библиотеку yfinance для загрузки исторических данных о фондовом рынке для Apple Inc. (AAPL) с Yahoo Finance.

# Импорт необходимых модулей
import yfinance as yf  # Импорт библиотеки для работы с финансовыми данными
from datetime import date, timedelta, datetime  # Импорт модулей для работы с датами

# Определение конечной даты как текущей даты в формате 'YYYY-MM-DD'
end_date = date.today().strftime("%Y-%m-%d")

# Определение начальной даты как '1990-01-01'
start_date = '1990-01-01'

# Загрузка данных о ценах акций компании Apple (AAPL) с Yahoo Finance
df = yf.download('AAPL', start=start_date, end=end_date)

Шаг 3: Предварительная обработка данных

Постройте тенденцию временных рядов, используя Matplotlib

def data_plot(df):
    # Создание копии DataFrame для построения графиков
    df_plot = df.copy()

    # Вычисление числа столбцов и строк для размещения графиков
    ncols = 2
    nrows = int(round(df_plot.shape[1] / ncols, 0))

    # Создание графического окна с заданным числом строк и столбцов
    fig, ax = plt.subplots(nrows=nrows, ncols=ncols,
                           sharex=True, figsize=(14, 7))

    # Построение линейных графиков для каждого столбца данных
    for i, ax in enumerate(fig.axes):
        sns.lineplot(data=df_plot.iloc[:, i], ax=ax)  # Построение линейного графика
        ax.tick_params(axis="x", rotation=30, labelsize=10, length=0)  # Настройка меток оси X
        ax.xaxis.set_major_locator(mdates.AutoDateLocator())  # Автоматическое определение меток для оси X

    # Автоматическое расположение графиков на холсте
    fig.tight_layout()
    
    # Отображение графиков
    plt.show()

# Вызов функции для построения графиков на основе переданного DataFrame
data_plot(df)

Линейные графики, отображающие характеристики акций Apple Inc. во времени

Разделение набора данных на тестовый и обучающий

Мы следуем общепринятой практике разделения данных на тренировочный и тестовый наборы. Мы вычисляем длину тренировочных наборов данных и выводим их формы для подтверждения разделения. Обычно, разделение составляет 80:20 для тренировочного и тестового наборов.

# Вычисление длины обучающих данных (80% от общего объема данных)
training_data_len = math.ceil(len(df) * .8)

# Вывод длины обучающих данных
training_data_len

# Определение обучающих данных как первых 80% строк первого столбца DataFrame
train_data = df[:training_data_len].iloc[:,:1]

# Определение тестовых данных как оставшихся 20% строк первого столбца DataFrame
test_data = df[training_data_len:].iloc[:,:1]

# Вывод размеров обучающих и тестовых данных
print(train_data.shape, test_data.shape)

Подготовка набора данных для обучения и тестирования

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

Данные обучения

# Выбор значений цен открытия (Open) в обучающих данных
dataset_train = train_data.Open.values

# Преобразование одномерного массива в двумерный массив
dataset_train = np.reshape(dataset_train, (-1,1))

# Вывод размера преобразованного массива
dataset_train.shape

Данные тестирования

# Выбор значений цен открытия (Open) в тестовых данных
dataset_test = test_data.Open.values

# Преобразование одномерного массива в двумерный массив
dataset_test = np.reshape(dataset_test, (-1,1))

# Вывод размера преобразованного массива
dataset_test.shape

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

Нормализация

Мы применили масштабирование Min-Max, которое является стандартным этапом предварительной обработки в машинном обучении и анализе временных рядов, к данным dataset_test. Это позволяет отрегулировать значения в интервале [0, 1], благодаря чему нейронные сети и другие модели могут сходиться быстрее и работать лучше. Нормализованные значения содержатся в массиве scaled_test и готовы к использованию в моделировании или анализе.

from sklearn.preprocessing import MinMaxScaler  # Импорт класса для масштабирования данных

# Создание объекта MinMaxScaler с диапазоном значений от 0 до 1
scaler = MinMaxScaler(feature_range=(0,1))

# Масштабирование обучающих данных
scaled_train = scaler.fit_transform(dataset_train)

# Вывод первых 5 элементов масштабированных обучающих данных
print(scaled_train[:5])

# Масштабирование тестовых данных
scaled_test = scaler.fit_transform(dataset_test)

# Вывод первых 5 элементов масштабированных тестовых данных
print(*scaled_test[:5])

Преобразование данных в последовательность

На этом этапе необходимо разделить временные ряды на X_train и y_train для обучающего набора и X_test и y_test для тестового набора. Временные ряды преобразуются в задачу обучения с учителем, которая может быть использована для разработки модели. При итерации по данным временных рядов цикл создает входные/выходные последовательности длиной 50 для обучающих данных и последовательности длиной 30 для тестовых данных. Используя эту технику, мы можем предсказывать будущие значения, учитывая временную зависимость данных от предыдущих наблюдений.

Мы подготавливаем обучающие и тестовые данные для нейронной сети, генерируя последовательности заданной длины и соответствующие им метки. Затем эти последовательности преобразуются в массивы NumPy и тензоры PyTorch.

Данные обучения

sequence_length = 50  # Длина последовательности

# Инициализация списков для хранения входных и выходных данных обучающего набора
X_train, y_train = [], []

# Создание входных и выходных последовательностей для обучающего набора данных
for i in range(len(scaled_train) - sequence_length):
    X_train.append(scaled_train[i:i+sequence_length])  # Добавление входной последовательности
    y_train.append(scaled_train[i+1:i+sequence_length+1])  # Добавление выходной последовательности

# Преобразование списков в массивы numpy
X_train, y_train = np.array(X_train), np.array(y_train)

# Преобразование массивов numpy в тензоры PyTorch
X_train = torch.tensor(X_train, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.float32)

# Вывод размеров обучающих данных
X_train.shape, y_train.shape

Данные тестирования

sequence_length = 30  # Длина последовательности для тестового набора

# Инициализация списков для хранения входных и выходных данных тестового набора
X_test, y_test = [], []

# Создание входных и выходных последовательностей для тестового набора данных
for i in range(len(scaled_test) - sequence_length):
    X_test.append(scaled_test[i:i+sequence_length])  # Добавление входной последовательности
    y_test.append(scaled_test[i+1:i+sequence_length+1])  # Добавление выходной последовательности

# Преобразование списков в массивы numpy
X_test, y_test = np.array(X_test), np.array(y_test)

# Преобразование массивов numpy в тензоры PyTorch
X_test = torch.tensor(X_test, dtype=torch.float32)
y_test = torch.tensor(y_test, dtype=torch.float32)

# Вывод размеров тестовых данных
X_test.shape, y_test.shape

Чтобы сделать последовательности совместимыми с нашей моделью глубокого обучения, данные были преобразованы в массивы NumPy и тензоры PyTorch.

Шаг 4. Определите модель класса LSTM

Теперь мы определили сеть PyTorch, используя архитектуру LSTM. Класс состоит из слоя LSTM и линейного слоя. В классе LSTMModel мы инициализировали параметры

  • input_size : количество признаков во входных данных на каждом временном шаге

  • hidden_size : скрытые единицы в слое LSTM

  • количество_слоев : количество слоев LSTM

  • batch_first= True: данные на входе будут иметь размер партии в качестве первого измерения

Функция super(LSTMModel, self).init() инициализирует родительский класс для создания нейронной сети.

Метод forward определяет прямой проход модели, при котором входные данные x обрабатываются через слои модели для получения вывода.

class LSTMModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers):
        super(LSTMModel, self).__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)  # Инициализация слоя LSTM
        self.linear = nn.Linear(hidden_size, 1)  # Инициализация линейного слоя

    def forward(self, x):
        # Проход вперёд через слой LSTM
        out, _ = self.lstm(x)
        
        # Проход через линейный слой для получения выхода
        out = self.linear(out)
        
        return out  # Возвращение выхода

Проверьте доступность оборудования

Для кода PyTorch нам нужно проверить аппаратные ресурсы.

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  # Определение устройства для вычислений
print(device)  # Вывод устройства

Определение модели

Теперь мы определяем модель, функцию потерь и оптимизатор для прогнозирования. Мы откорректировали гиперпараметры модели и установили функцию потерь на среднеквадратичную ошибку. Для оптимизации параметров во время обучения мы использовали оптимизатор Adam

Python3

input_size = 1  # Размер входных данных
num_layers = 2  # Количество слоев LSTM
hidden_size = 64  # Размер скрытого состояния LSTM
output_size = 1  # Размер выходных данных

# Инициализация модели LSTM
model = LSTMModel(input_size, hidden_size, num_layers).to(device)

# Определение функции потерь
loss_fn = torch.nn.MSELoss(reduction='mean')

# Определение оптимизатора
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

# Вывод модели
print(model)

Шаг 5. Создание загрузчика данных для пакетного обучения

Загрузчик данных играет ключевую роль на этапах обучения и оценки. Поэтому мы подготовили данные для пакетного обучения и тестирования, создав объекты загрузчика данных.

Python3

batch_size = 16  # Размер пакета данных

# Создание набора данных и загрузчика для обучающих данных
train_dataset = torch.utils.data.TensorDataset(X_train, y_train)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)

# Создание набора данных и загрузчика для тестовых данных
test_dataset = torch.utils.data.TensorDataset(X_test, y_test)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

Шаг 6: Обучение модели и оценка

Теперь мы создали тренировочный цикл на 50 эпох. В предоставленном фрагменте кода модель обрабатывает мини-пакеты обучающих данных, вычисляет потери и обновляет параметры.

num_epochs = 50  # Количество эпох обучения

# Инициализация списков для хранения истории потерь на обучающем и тестовом наборах
train_hist = []
test_hist = []

# Цикл обучения модели на протяжении заданного числа эпох
for epoch in range(num_epochs):
    total_loss = 0.0  # Общая потеря для текущей эпохи
    model.train()  # Установка модели в режим обучения
    for batch_X, batch_y in train_loader:
        batch_X, batch_y = batch_X.to(device), batch_y.to(device)  # Перемещение данных на устройство (GPU или CPU)
        predictions = model(batch_X)  # Получение прогнозов модели
        loss = loss_fn(predictions, batch_y)  # Вычисление потерь
        optimizer.zero_grad()  # Обнуление градиентов
        loss.backward()  # Обратное распространение ошибки
        optimizer.step()  # Обновление параметров модели
        total_loss += loss.item()  # Обновление общей потери
    average_loss = total_loss / len(train_loader)  # Вычисление средней потери на обучающем наборе
    train_hist.append(average_loss)  # Добавление средней потери в историю обучения
    model.eval()  # Установка модели в режим оценки
    with torch.no_grad():
        total_test_loss = 0.0  # Общая потеря на тестовом наборе
        for batch_X_test, batch_y_test in test_loader:
            batch_X_test, batch_y_test = batch_X_test.to(device), batch_y_test.to(device)  # Перемещение данных
            predictions_test = model(batch_X_test)  # Получение прогнозов модели
            test_loss = loss_fn(predictions_test, batch_y_test)  # Вычисление потерь
            total_test_loss += test_loss.item()  # Обновление общей потери на тестовом наборе
        average_test_loss = total_test_loss / len(test_loader)  # Вычисление средней потери на тестовом наборе
        test_hist.append(average_test_loss)  # Добавление средней потери в историю тестирования
    if (epoch+1)%10==0:
        print(f'Epoch [{epoch+1}/{num_epochs}] - Training Loss: {average_loss:.4f}, Test Loss: {average_test_loss:.4f}')

Построение кривой обучения

Мы построили кривую обучения для отслеживания прогресса и получения представления о том, сколько времени и обучения требуется модели для понимания закономерностей.

x = np.linspace(1, num_epochs, num_epochs)  # Генерация последовательности значений по оси X

# Построение графика потерь на обучающем и тестовом наборах данных
plt.plot(x, train_hist, scalex=True, label="Training loss")  # Потери на обучающем наборе
plt.plot(x, test_hist, label="Test loss")  # Потери на тестовом наборе
plt.legend()  # Добавление легенды
plt.show()  # Отображение графика

Кривая обучения для потерь на обучающей выборке и потерь на тестовой выборке

Шаг 7: Прогнозирование

После обучения нейронной сети на предоставленных данных наступает момент прогнозирования на следующий месяц. Модель предсказывает будущую стоимость открытия и сохраняет будущие значения вместе с соответствующими датами. Используя цикл for, мы собираемся выполнить пошаговое прогнозирование, шаги следующие –

  • Мы установили количество будущих временных шагов равным 30 и преобразовали тестовую последовательность в массив NumPy, удалив одиночные измерения с помощью

  • Затем мы преобразовали historical_data в тензор Pytorch. Форма тензора - (1, sequence_length, 1), где sequence_length - длина последовательности исторических данных.

  • модель дополнительно предсказывает следующее значение на основе

  • Прогноз затем преобразуется в массив numpy, и извлекается первый элемент.

После завершения цикла мы получаем прогнозируемые значения, которые сохраняются в списке, и генерируются будущие даты для создания индекса для этих значений.

num_forecast_steps = 30  # Количество шагов прогноза
sequence_to_plot = X_test.squeeze().cpu().numpy()  # Извлечение последовательности для прогнозирования

# Извлечение последнего наблюдения для прогноза
historical_data = sequence_to_plot[-1]
print(historical_data.shape)

forecasted_values = []  # Список для хранения прогнозируемых значений

# Генерация прогнозов на указанное количество шагов
with torch.no_grad():
    for _ in range(num_forecast_steps*2):
        # Преобразование исторических данных в тензор и перемещение на устройство
        historical_data_tensor = torch.as_tensor(historical_data).view(1, -1, 1).float().to(device)
        # Получение прогноза от модели и преобразование в массив NumPy
        predicted_value = model(historical_data_tensor).cpu().numpy()[0, 0]
        # Добавление прогнозируемого значения в список
        forecasted_values.append(predicted_value[0])
        # Обновление исторических данных для следующего шага
        historical_data = np.roll(historical_data, shift=-1)
        historical_data[-1] = predicted_value

# Вычисление дат для будущих прогнозов
last_date = test_data.index[-1]  # Последняя дата из тестового набора
future_dates = pd.date_range(start=last_date + pd.DateOffset(1), periods=30)  # Генерация будущих дат

# Объединение индексов исходного тестового набора и будущих дат
combined_index = test_data.index.append(future_dates)

Последний шаг: Построение графика прогноза

После прогнозирования будущих цен мы можем визуализировать их, используя линейные графики. График был построен для определенного временного интервала. Синяя линия указывает на тестовые данные. Затем мы строим график последних 30 временных шагов индекса тестовых данных, используя график с зеленой линией.

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

plt.rcParams['figure.figsize'] = [14, 4]  # Установка размеров графика

# Построение графика исходных данных тестового набора
plt.plot(test_data.index[-100:-30], test_data.Open[-100:-30], label="test_data", color="b")

# Построение графика фактических значений на основе последней доступной последовательности
original_cases = scaler.inverse_transform(np.expand_dims(sequence_to_plot[-1], axis=0)).flatten()
plt.plot(test_data.index[-30:], original_cases, label='actual values', color='green')

# Построение графика прогнозируемых значений
forecasted_cases = scaler.inverse_transform(np.expand_dims(forecasted_values, axis=0)).flatten()
plt.plot(combined_index[-60:], forecasted_cases, label='forecasted values', color='red') 

# Настройка осей и заголовка
plt.xlabel('Time Step')
plt.ylabel('Value')
plt.legend()
plt.title('Time Series Forecasting')
plt.grid(True)

# Отображение графика
plt.show()

Прогноз цены открытия акций Apple Inc. на следующий месяц

Построив тестовые данные, фактические значения и прогнозируемые данные модели, мы получили четкое представление о том, насколько хорошо прогнозируемые значения соответствуют временным рядам.

В этой работе тщательно исследована захватывающая область прогнозирования временных рядов с использованием нейронных сетей LSTM и PyTorch. Для сбора исторических данных о фондовом рынке была импортирована библиотека yfinance, после чего был начат этап предварительной обработки данных. Затем были применены ключевые шаги, такие как загрузка данных, разделение на обучающую и тестовую выборки, а также масштабирование данных, чтобы убедиться, что наша модель может точно учиться на данных и делать прогнозы.

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

Мы только начали изучать огромное поле прогнозирования временных рядов в этом эссе. Есть еще много чему учиться, от управления многовариантными временными рядами до решения практических проблем новаторскими способами. Имея этот опыт, вы готовы использовать PyTorch и нейронные сети LSTM для самостоятельных приключений в прогнозировании временных рядов.

Last updated