Skip to main content
LadeStack LogoLadeStack
AI Development 6 мин чтения 25 июня 2024 г.

Пайплайны машинного обучения для веб-разработчиков: от сбора данных до инференса в браузере и на сервере

Полное руководство по MLOps для веб-инженеров: сбор данных, предотвращение data leakage, инференс в браузере через ONNX Runtime Web, микросервисы на FastAPI и мониторинг дрейфа данных.

Архитектурная диаграмма пайплайна машинного обучения: сбор данных, обучение, инференс в браузере через ONNX и микросервис FastAPI

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

Однако перенос прототипа из Jupyter Notebook в распределенную веб-инфраструктуру сопряжен со значительными архитектурными рисками. Несоответствие предобработки на этапе обучения и в проде (training-serving skew), утечка данных (data leakage), неконтролируемые задержки сети и деградация точности с течением времени (data drift) могут свести на нет всю ценность модели.

В этом руководстве мы рассмотрим построение надежного ML-пайплайна с точки зрения веб-разработчика: от валидации данных и выбора между клиентским и серверным инференсом до реализации на ONNX Runtime Web и FastAPI, а также мониторинга в проде.


1. Сквозная архитектура ML-пайплайна

Надежная производственная система объединяет четыре изолированных этапа:

[ Этап 1: Сбор и валидация данных ]
Логи пользователей / Реляционные СУБД / Очереди событий
       │
       ▼
Валидация схемы и очистка (Pydantic / Great Expectations)
       │
       ▼
Хронологическое разделение Train / Val / Test (защита от утечки данных)

─────────────────────────────────────────────────────────────────

[ Этап 2: Обучение и экспорт артефактов ]
Feature Store и обучение пайплайна (Scikit-Learn / PyTorch)
       │
       ▼
Стандартизация формата (ONNX / TorchScript) и квантование (INT8 / FP16)
       │
       ▼
Реестр моделей (S3 / MLflow) и версионирование

─────────────────────────────────────────────────────────────────

[ Этап 3: Развертывание и инференс ]
       ┌───────────────────────────────┐
       ▼                               ▼
[ Инференс в браузере ]         [ Серверный микросервис ]
ONNX Runtime Web (WASM / WebGPU) FastAPI / Triton Inference Server
Нулевые затраты на сервер・Приватность Сложные модели・Защита интеллектуальной собственности

─────────────────────────────────────────────────────────────────

[ Этап 4: MLOps и мониторинг дрейфа ]
Отслеживание распределения признаков (критерий Колмогорова-Смирнова / PSI)

2. Сбор данных и предотвращение утечек (Data Leakage)

Одна из самых опасных и трудноуловимых ошибок при проектировании пайплайна — утечка данных (Data Leakage).

Типичные источники утечек данных

  1. Преждевременная нормализация: Вызов StandardScaler.fit() на всем датасете до разделения на train и test приводит к тому, что будущие средние и дисперсии просачиваются в процесс обучения, завышая метрики валидации.
  2. Случайное перемешивание временных рядов: Применение стандартной случайной кросс-валидации к данным с временной зависимостью приводит к тому, что модель обучается на будущих событиях для предсказания прошлого.

Пайплайн предобработки без утечек (Python)

# pipeline/preprocess.py
import pandas as pd
import numpy as np
from sklearn.model_selection import TimeSeriesSplit
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression

def build_leak_free_pipeline(df: pd.DataFrame, target_col: str, timestamp_col: str):
    # Строгая сортировка по временной метке
    df = df.sort_values(timestamp_col).reset_index(drop=True)
    X = df.drop(columns=[target_col, timestamp_col])
    y = df[target_col]

    # Последние 20% данных отводятся под тестовый холдаут
    split_index = int(len(df) * 0.8)
    X_train, X_test = X.iloc[:split_index], X.iloc[split_index:]
    y_train, y_test = y.iloc[:split_index], y.iloc[split_index:]

    # Инкапсуляция скейлера внутри Pipeline гарантирует отсутствие skew
    pipeline = Pipeline([
        ('scaler', StandardScaler()),
        ('classifier', LogisticRegression(C=1.0, solver='lbfgs', max_iter=500))
    ])

    pipeline.fit(X_train, y_train)
    holdout_score = pipeline.score(X_test, y_test)
    print(f"Точность на тестовом холдауте: {holdout_score:.4f}")

    return pipeline

3. Сравнение клиентского и серверного инференса

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

Критерий Клиентский инференс (ONNX Runtime Web) Серверный инференс (FastAPI / Triton)
Сетевая задержка 0 мс (нет сетевого взаимодействия) Задержка сетевого RTT (50–250 мс)
Стоимость серверов 0 $ (вычисления на стороне клиента) Требует серверных мощностей и масштабирования
Офлайн-работа Полная (работает через Service Worker) Не поддерживается (нужен интернет)
Приватность данных Максимальная (данные не покидают браузер) Требует отправки данных на сервер
Защита модели (IP) Невозможна (бинарный файл загружается клиенту) Максимальная (веса скрыты на сервере)
Размер модели Компактные модели (< 50 МБ) Любой масштаб (десятки ГБ весов)

4. Реализация клиентского инференса: ONNX Runtime Web

Для легковесных моделей классификации текста или обработки изображений стандарт де-факто — ONNX Runtime Web с поддержкой WebAssembly (WASM) и аппаратного ускорения через WebGPU.

// src/lib/ml/browserInference.ts
import * as ort from 'onnxruntime-web';

ort.env.wasm.numThreads = 2;
ort.env.wasm.simd = true;

export interface PredictionResult {
  probabilities: number[];
  predictedClass: number;
  inferenceDurationMs: number;
}

export class BrowserInferenceService {
  private session: ort.InferenceSession | null = null;

  async loadModel(modelPath: string): Promise<void> {
    this.session = await ort.InferenceSession.create(modelPath, {
      executionProviders: ['wasm'],
      graphOptimizationLevel: 'all'
    });
  }

  async predict(features: number[]): Promise<PredictionResult> {
    if (!this.session) {
      throw new Error("Сессия модели не инициализирована. Вызовите loadModel().");
    }

    const start = performance.now();
    const inputTensor = new ort.Tensor('float32', Float32Array.from(features), [1, features.length]);
    const inputName = this.session.inputNames[0];
    const feeds: Record<string, ort.Tensor> = {};
    feeds[inputName] = inputTensor;

    const results = await this.session.run(feeds);
    const outputName = this.session.outputNames[0];
    const outputTensor = results[outputName];
    const outputData = Array.from(outputTensor.data as Float32Array);

    let maxIdx = 0;
    let maxVal = outputData[0];
    for (let i = 1; i < outputData.length; i++) {
      if (outputData[i] > maxVal) {
        maxVal = outputData[i];
        maxIdx = i;
      }
    }

    return {
      probabilities: outputData,
      predictedClass: maxIdx,
      inferenceDurationMs: performance.now() - start
    };
  }
}

5. Серверный микросервис инференса: реализация на FastAPI

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

# server/app.py
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
import numpy as np
import onnxruntime as ort
import time

app = FastAPI(title="ML Inference Service", version="1.0.0")

class InferenceRequest(BaseModel):
    user_id: str
    features: list[float] = Field(..., min_items=5, max_items=5)

class InferenceResponse(BaseModel):
    user_id: str
    prediction: int
    confidence: float
    latency_ms: float

session = ort.InferenceSession("models/production_classifier.onnx")
input_name = session.get_inputs()[0].name

@app.post("/api/v1/predict", response_model=InferenceResponse)
async def predict_endpoint(payload: InferenceRequest):
    try:
        start_time = time.perf_counter()
        input_array = np.array([payload.features], dtype=np.float32)

        raw_outputs = session.run(None, {input_name: input_array})
        probabilities = raw_outputs[0][0]
        predicted_class = int(np.argmax(probabilities))
        confidence = float(probabilities[predicted_class])
        duration_ms = (time.perf_counter() - start_time) * 1000

        return InferenceResponse(
            user_id=payload.user_id,
            prediction=predicted_class,
            confidence=confidence,
            latency_ms=duration_ms
        )
    except Exception as exc:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Ошибка инференс-движка: {str(exc)}"
        )

6. MLOps и мониторинг дрейфа данных в проде

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

Ключевые метрики мониторинга

  1. Дрейф входных признаков: Проведение теста Колмогорова-Смирнова (KS-test) или вычисление индекса стабильности популяции (PSI) для выявления отклонений боевого трафика от обучающей выборки.
  2. Падение уверенности предсказаний: Резкое снижение средней вероятности класса сигнализирует о поступлении аномальных данных, не встречавшихся при обучении.
  3. Теневое развертывание (Shadow Deployment): Параллельный запуск новой версии модели (v2) на реальном трафике в фоновом режиме без выдачи ответов пользователям для безопасной валидации задержки и точности.

7. Заключение

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

Для дальнейшего погружения в архитектуру современных ИИ-систем рекомендуем статьи Создание RAG-приложений с векторными базами данных и Тонкая настройка LLM для специализированных задач.

More Engineering Articles