Создаем простую нейросеть для предсказания спроса

В этом посте мы проложим путь от постановки задачи до развёртывания модели и мониторинга результатов. Мы разберём реалистичный и воспроизводимый сценарий: построение простой нейросети для предсказания спроса в бизнес-соке. Далее — пошаговый гайд, с акцентом на практические детали, чтобы читатель мог повторить процесс на своих данных.

1. Постановка задачи и бизнес-цели

  • Цель: предсказывать спрос на продукт на горизонтах 1–4 недели вперёд.
  • Метрика успеха: например, RMSE (Root Mean Squared Error) и MAE (Mean Absolute Error) на валидационной выборке. В бизнесе можно дополнительно учитывать потоки денег и удовлетворённость клиентов.
  • Требования к результату: модель должна быть достаточно быстрой для обновления каждую неделю и давать интерпретируемые подсказки для планирования запасов.

2. Сбор и подготовка данных

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

  • Исторические продажи
  • Цены, акции и промо-мероприятия
  • Внешние факторы: погода, календарь (праздники, выходные), сезонность
  • Стоковые уровни и данные по возвращаемости

2.2 Предобработка

  • Приведение временных меток к единому формату и создание временных признаков:
    • месяц, неделя года, день недели
    • скользящие агрегаты: средний спрос за 7/14/28 дней
  • Кодирование категорий (например, по магазинам, продуктовым группам) через one-hot encoding или эмбеддинги.
  • Работа с пропусками: заполнение историческим средним, предиктивное заполнение или удаление рядов.
  • Нормализация числовых признаков (StandardScaler или MinMaxScaler) перед подачей в сеть.

2.3 Разделение данных

  • Разделение на обучающую, валидационную и тестовую выборки по времени:
    • обучающая: до t0
    • валидационная: [t0, t1)
    • тестовая: после t1
  • Избегайте утечки информации через будущее.

3. Архитектура модели

3.1 Выбор подхода

  • Для простоты и воспроизводимости подойдёт многошаровая перцептронная сеть (MLP) с учётом временных признаков, либо простая рекуррентная сеть (RNN)/LSTM, если есть явная зависимость во времени.
  • Если данных много и есть сезонность, можно попробовать простые архитектуры: GRU/LSTM или 1D сверточные слои для последовательностей.

3.2 Примерная архитектура (MLP)

  • Вход: набор признаков (числовые + закодированные категориальные признаки)
  • Скрытые слои: 2–3 слоя по 64–128 нейронов
  • Активации: ReLU
  • Выход: один нейрон без активации (регрессия) or линейная активация
  • Регуляризация: Dropout 0.2–0.3, L2-регуляризация
  • Потери: MSE (или RMSE как метрика на этапе оценки)

3.3 Гиперпараметры (для старта)

  • Учитель: градиентный спуск Adam
  • Скорость обучения: 0.001 – 0.0005
  • Размер батча: 32–128
  • Эпохи: 50–200 (с ранним прекращением по валидной потере)

4. Реализация: пошаговый кодовый каркас

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

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader

# 1. Загрузка данных
# data = pd.read_csv('sales_history.csv')
# Пример структуры:
# data: ['store_id', 'product_id', 'date', 'sales', 'price', 'promo', 'temperature', ...]

# 2. Инжекция временных признаков
def add_time_features(df, date_col='date'):
    df[date_col] = pd.to_datetime(df[date_col])
    df['year'] = df[date_col].dt.year
    df['month'] = df[date_col].dt.month
    df['weekofyear'] = df[date_col].dt.isocalendar().week.astype(int)
    df['dayofweek'] = df[date_col].dt.dayofweek
    df = df.sort_values([date_col])
    # скользящие средние
    df['sales_ma7'] = df.groupby(['store_id', 'product_id'])['sales'].transform(lambda x: x.rolling(7, min_periods=1).mean())
    return df

# 3. Разделение на признаки и цель
# X = data.drop(columns=['sales'])
# y = data['sales']

# 4. Предобработка категорий и чисел
# categorical_cols = ['store_id', 'product_id']
# numeric_cols = [col for col in X.columns if col not in categorical_cols]

# preprocessor = ColumnTransformer(
#     transformers=[
#         ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_cols),
#         ('num', StandardScaler(), numeric_cols)
#     ])

# 5. Разделение на обучающую/валидационную
# X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, shuffle=False)

# 6. Нейросеть на PyTorch
class SalesDataset(Dataset):
    def __init__(self, X, y):
        self.X = torch.tensor(X.values, dtype=torch.float32)
        self.y = torch.tensor(y.values, dtype=torch.float32).unsqueeze(1)
    def __len__(self):
        return len(self.y)
    def __getitem__(self, idx):
        return self.X[idx], self.y[idx]

class SimpleMLP(nn.Module):
    def __init__(self, input_dim, hidden_dims=[128, 64]):
        super().__init__()
        layers = []
        in_dim = input_dim
        for h in hidden_dims:
            layers.append(nn.Linear(in_dim, h))
            layers.append(nn.ReLU())
            layers.append(nn.Dropout(0.2))
            in_dim = h
        layers.append(nn.Linear(in_dim, 1))
        self.net = nn.Sequential(*layers)
    def forward(self, x):
        return self.net(x)

# 7. Обучение
def train_model(model, train_loader, val_loader, epochs=50, lr=1e-3):
    criterion = nn.MSELoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    best_rmse = float('inf')
    for epoch in range(epochs):
        model.train()
        for xb, yb in train_loader:
            preds = model(xb)
            loss = criterion(preds, yb)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        # валидация
        model.eval()
        val_preds, val_true = [], []
        with torch.no_grad():
            for xb, yb in val_loader:
                val_preds.append(model(xb).cpu())
                val_true.append(yb.cpu())
        val_pred = torch.cat(val_preds).squeeze()
        val_true = torch.cat(val_true).squeeze()
        rmse = torch.sqrt(nn.functional.mse_loss(val_pred, val_true))
        if rmse < best_rmse:
            best_rmse = rmse
        print(f"Epoch {epoch+1}: val RMSE={rmse:.4f}, best={best_rmse:.4f}")
    return model

# Пример использования (после подготовки X_train, y_train, X_val, y_val)
# train_ds = SalesDataset(X_train, y_train)
# val_ds = SalesDataset(X_val, y_val)
# train_dl = DataLoader(train_ds, batch_size=64, shuffle=True)
# val_dl = DataLoader(val_ds, batch_size=64)

# model = SimpleMLP(input_dim=X_train.shape[1])
# trained = train_model(model, train_dl, val_dl, epochs=50, lr=0.001)

Приведённый код носит иллюстративный характер и требует адаптации под конкретные данные:

  • реализуйте корректное формирование X из вашего датасета;
  • настройте обработку категориальных признаков (лучше через т. н. embedding-слоя или продвинутый OneHotEncoder);
  • добавьте раннее прекращение по валидной потере для экономии времени.

5. Обучение и валидация

  • Запускайте обучение на исторических данных, которые покрывают несколько сезонов.
  • Следите за динамикой валидной ошибки: устойчивый спад — признак того, что модель учится распознавать паттерны.
  • Проводите абляцию признаков: удаляйте признаки по одному и смотрите влияние на RMSE.

6. Развёртывание модели

6.1 Выбор инфраструктуры

  • Локальное развёртывание на сервере компании или облако (AWS, GCP, Azure).
  • Контейнеризация через Docker для воспроизводимости.
  • Простой API: FastAPI или Flask, который принимает признаки и возвращает прогноз.

6.2 Архитектура сервиса

  • Предсказания на следующий период: запросы к модели, которая возвращает прогноз для каждого магазина/товара.
  • Кэширование часто запрашиваемых результатов.
  • Мониторинг задержек и точности.

6.3 Пример API-ендпойнта (псевдокод)

POST /predict
{
  "features": [
    {"store_id": 12, "product_id": 345, "date": "2025-09-15", "price": 19.99, "promo": 0, ...},
    ...
  ]
}

Ответ:

{
  "predictions": [128.5, 98.2, ...]
}

7. Мониторинг и обслуживание

  • Мониторинг точности: регулярно сравнивайте предсказания с фактическими данными, оценивайте drift.
  • Алерты: если RMSE выходит за пределы допустимого диапазона, инициируйте повторную обучающую выборку.
  • Ревизия данных: следите за качеством входных признаков, изменениями в структуре данных.

8. Этика и качество данных

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

9. Что дальше

  • Расширение: добавление автокодировщиков для аномалий и улучшения представления данных.
  • Прогноз на горизонты большего масштаба, использование ансамблей из нескольких моделей.
  • Внедрение автоматического обновления моделей и регламентированного тестирования новых версий.

Это базовый, но практичный маршрут: от постановки задачи до живого развёртывания и мониторинга. Если хотите, могу адаптировать материал под ваши данные (структура датасета, конкретные признаки, целевые метрики) или подготовить готовый репозиторий с максимумом повторяемости и минимальными установочными усилиями. Также могу предложить готовые заголовки и SEO-словарь для поста в блог.