Введение: почему акции и нейросети — идеальная пара?

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

Эта статья — туториал по построению рекуррентной нейронной сети (RNN) на основе TensorFlow для предсказания цен на фондовом рынке. Мы разберём каждый этап: от подготовки данных до обучения полноценной LSTM-модели с нормализацией через скользящее окно — всё по мотивам классической работы Лилиан Вэнг (Lilian Weng).

«Предсказание цен акций — задача непростая. Особенно после нормализации: ценовые тренды выглядят очень зашумлёнными.» — Lilian Weng

ℹ Важный дисклеймер
Эта статья носит образовательный характер. Несмотря на то что были предприняты попытки предсказывать цены акций с помощью алгоритмов анализа временных рядов, их всё ещё нельзя использовать для реальных торгов. Цель — показать, как строить и обучать RNN-модели, а не давать инвестиционные советы.

Почему RNN и LSTM — правильный инструмент

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

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

Стандартные рекуррентные нейросети (RNN) с трудом справляются с длинными последовательностями из-за проблемы исчезающего градиента.

Здесь и появляются LSTM (Long Short-Term Memory) — специальный вид RNN:

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

LSTM преодолевает эти ограничения, обучаясь долгосрочным зависимостям, что делает их мощным инструментом для финансового прогнозирования.

Сравнение подходов к прогнозированию цен

МетодДолгосрочные зависимостиНелинейностьАдаптивность
ARIMA❌ Слабо❌ Нет❌ Нет
Скользящая средняя❌ Нет❌ Нет❌ Нет
Простая RNN⚠️ Ограниченно✅ Да✅ Да
LSTM✅ Отлично✅ Да✅ Да
Transformer✅ Отлично✅ Да✅ Да

Эффективность LSTM распространяется на различные задачи моделирования последовательностей в нескольких прикладных областях, включая видео, NLP, геопространственные данные и анализ временных рядов.


Данные: откуда брать и как подготавливать

Источник данных

В оригинальном туториале Lilian Weng использовались данные S&P 500. Для работы нужно скачать полные данные S&P 500 с Yahoo Finance (^GSPC), выбрав максимальный временной период, и сохранить файл .csv в директорию data/SP500.csv.

Далее запускается скрипт python data_fetcher.py для загрузки цен отдельных акций из S&P 500, каждая сохраняется в data/{{stock_abbreviation}}.csv.

Данные для каждой акции обычно содержат поля:

Набор данных содержит следующие поля: цена открытия, наивысшая цена, наименьшая цена, цена закрытия, скорректированная цена закрытия и объём торгов.

Ключевая проблема: выход значений за пределы обучающей выборки

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

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

Скользящее окно (Sliding Window)

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

💡 Как работает нормализация окна
В нормализованном скользящем окне W'_t в момент времени t все значения делятся на последнюю известную цену — последнюю цену из предыдущего окна W_{t-1}. Это позволяет модели работать с относительными изменениями, а не абсолютными числами.

Пример кода для формирования датасета со скользящим окном:

import numpy as np

def create_sliding_windows(prices, window_size=30, input_size=5):
    """
    prices: массив цен
    window_size: длина каждого окна (num_steps)
    input_size: сколько шагов предсказываем
    """
    X, y = [], []
    for i in range(len(prices) - window_size - input_size):
        window = prices[i : i + window_size]
        target = prices[i + window_size : i + window_size + input_size]
        
        # Нормализация: делим на последнюю цену предыдущего окна
        last_price = prices[i + window_size - 1]
        norm_window = window / last_price
        norm_target = target / last_price
        
        X.append(norm_window)
        y.append(norm_target)
    
    return np.array(X), np.array(y)

Нормализация через MinMaxScaler

Альтернативный подход — глобальная нормализация данных:

from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(df['Close'].values.reshape(-1, 1))

Предобработка включает нормализацию данных с помощью MinMaxScaler; обучающая выборка строится с учётом данных за последние 60 дней как параметров обучения для предсказания следующего дня.


Архитектура модели: строим LSTM на практике


graph TD
    A[Исторические цены] --> B[Скользящее окно]
    B --> C[Нормализация]
    C --> D[LSTM слой 1]
    D --> E[Dropout 0.2]
    E --> F[LSTM слой 2]
    F --> G[Dropout 0.2]
    G --> H[LSTM слой 3]
    H --> I[Dropout 0.2]
    I --> J[Dense выходной слой]
    J --> K[Предсказанная цена]
    K --> L{Денормализация}
    L --> M[Итоговый прогноз]

Описание архитектуры

Модель LSTM содержит num_layers стека LSTM-слоёв, каждый из которых содержит lstm_size ячеек LSTM. После каждой LSTM-ячейки применяется маска дропаута с вероятностью сохранения keep_prob.

Полная реализация на Keras/TensorFlow:

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout

def build_lstm_model(input_shape, lstm_size=128, num_layers=2, keep_prob=0.8):
    model = Sequential()
    
    # Первый LSTM слой
    model.add(LSTM(
        units=lstm_size,
        return_sequences=True,
        input_shape=input_shape
    ))
    model.add(Dropout(1 - keep_prob))
    
    # Средние LSTM слои
    for _ in range(num_layers - 2):
        model.add(LSTM(units=lstm_size, return_sequences=True))
        model.add(Dropout(1 - keep_prob))
    
    # Последний LSTM слой
    model.add(LSTM(units=lstm_size))
    model.add(Dropout(1 - keep_prob))
    
    # Выходной слой
    model.add(Dense(units=1))
    
    return model

model = build_lstm_model(
    input_shape=(30, 1),   # window_size=30, 1 признак
    lstm_size=128,
    num_layers=2,
    keep_prob=0.8
)

model.compile(
    optimizer='adam',
    loss='mean_squared_error'
)

model.summary()

Конфигурация гиперпараметров

Автор оригинального туториала использовала следующую конфигурацию в эксперименте: num_layers=1, keep_prob=0.8, batch_size=64, init_learning_rate=0.001, learning_rate_decay=0.99, init_epoch=5, max_epoch=100, num_steps=30.

ГиперпараметрЗначениеОписание
num_steps30Длина скользящего окна
lstm_size128Размер LSTM-слоя
num_layers1–2Кол-во стеков LSTM
keep_prob0.8Вероятность сохранения (dropout)
batch_size64Размер батча
learning_rate0.001Начальная скорость обучения
max_epoch100Максимум эпох обучения
💡 Совет по learning rate decay
Используйте затухание learning rate с коэффициентом около 0.99 на каждую эпоху. Это позволяет модели уточнять веса на поздних стадиях, не перескакивая через минимумы функции потерь.

Обучение, оценка и типичные ошибки

Тренировочный процесс

history = model.fit(
    X_train, y_train,
    epochs=100,
    batch_size=64,
    validation_split=0.1,
    callbacks=[
        tf.keras.callbacks.LearningRateScheduler(
            lambda epoch, lr: lr * 0.99
        )
    ]
)

В выходном слое используется 1 нейрон для предсказания нормализованной цены акции. В качестве функции потерь применяется MSE, а оптимизатор — Adam (стохастический градиентный спуск).

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

Для оценки результатов чаще всего используют:

  • MSE (Mean Squared Error) — средняя квадратичная ошибка
  • RMSE (Root MSE) — корень из MSE, удобен в единицах цены
  • MAPE (Mean Absolute Percentage Error) — относительная ошибка в процентах
from sklearn.metrics import mean_squared_error
import numpy as np

# Предсказание
y_pred_scaled = model.predict(X_test)

# Денормализация
y_pred = scaler.inverse_transform(y_pred_scaled)
y_true = scaler.inverse_transform(y_test.reshape(-1, 1))

# Метрики
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100

print(f"RMSE: {rmse:.4f}")
print(f"MAPE: {mape:.2f}%")

Типичные проблемы и как их решать

⚠ Ошибка: выход значений за диапазон
Если модель получает числа вне диапазона обучающих данных, качество предсказаний резко падает. Нормализуйте цены в каждом скользящем окне — задача превращается в предсказание относительных изменений вместо абсолютных значений.

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

При увеличении input_size предсказание становится значительно сложнее.


Что дальше: расширение модели

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

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

Также существуют более современные подходы для улучшения базовой модели:

Можно улучшить модель, включив дополнительные признаки — объём торгов, макроэкономические индикаторы (инфляция, процентные ставки) и анализ настроений из новостей и соцсетей. Стоит также поэкспериментировать с моделями на основе Transformer (например, BERT, GPT для финансов) для лучшего понимания последовательностей.

Гибридные модели, комбинирующие LSTM с традиционными моделями временных рядов (ARIMA + LSTM), обеспечивают более устойчивый подход.

📝 Пример запуска из репозитория

Полностью рабочий код доступен в репозитории github.com/lilianweng/stock-rnn. Базовый запуск обучения:

python main.py \
  --stock_count=100 \
  --train \
  --input_size=1 \
  --lstm_size=128 \
  --max_epoch=50

Заключение

В целом предсказание цен акций — непростая задача. Особенно после нормализации ценовые тренды выглядят очень зашумлёнными.

Но именно в этой сложности — ценность задачи как учебного полигона для RNN/LSTM:

  • Вы научились формировать обучающую выборку через скользящие окна
  • Вы разобрали нормализацию как ключевой приём борьбы с out-of-scale проблемой
  • Вы построили многослойную LSTM-модель с дропаутом и decay learning rate
  • Вы поняли ключевые гиперпараметры и их влияние на качество

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

Мотивация этого туториала — продемонстрировать, как строить и обучать RNN-модель в TensorFlow, а не решить задачу предсказания акций как таковую. Код является отправной точкой: добавляйте собственные идеи, связанные с предсказанием акций, чтобы улучшить результаты.