Машинное обучение на C++: от основ до внедрения

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

1. Введение в машинное обучение и задачи

Введение в машинное обучение и задачи

Что такое машинное обучение

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

Если упростить, то:

  • В классическом программировании вы явно пишете правила: если A, то B.
  • В машинном обучении вы показываете системе примеры, а она строит модель, которая умеет выдавать ответ на новых, ранее не виденных данных.
  • Важно понимать разницу между:

  • Алгоритмом — процедурой вычислений (например, как обновлять параметры модели)
  • Моделью — конкретной функцией/структурой с обучаемыми параметрами (например, линейная модель, дерево решений)
  • Когда машинное обучение действительно нужно

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

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

    Типы задач машинного обучения

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

    Обучение с учителем

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

  • Классификация: предсказать класс из набора классов (например, спам или не спам).
  • Регрессия: предсказать число (например, стоимость квартиры).
  • Обучение без учителя

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

  • Кластеризация: группировка похожих объектов (например, сегментация пользователей).
  • Снижение размерности: сжатие признаков без сильной потери информации (например, для визуализации).
  • Обучение с подкреплением

    Обучение с подкреплением — это обучение через взаимодействие со средой, где система получает награды или штрафы.

  • Примеры: управление роботом, игровая стратегия, оптимизация поведения агента.
  • Ключевые термины: данные, признаки, модель

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

  • Объект: одна единица наблюдения (например, одна транзакция, одно изображение).
  • Признаки: измеримые характеристики объекта, подаваемые на вход модели (например, сумма транзакции, время, страна).
  • Целевая переменная: то, что мы хотим предсказать (класс или число).
  • Датасет: набор объектов с признаками, а иногда и с целевой переменной.
  • Модель: механизм, который по признакам выдает предсказание.
  • На практике вы почти всегда делите данные на части:

  • Обучающая выборка: на ней модель учится.
  • Валидационная выборка: на ней вы подбираете настройки и сравниваете варианты моделей.
  • Тестовая выборка: финальная независимая проверка качества.
  • Как выглядит процесс машинного обучения в реальном проекте

    Типовой рабочий цикл можно представить так.

    !Конвейер: от данных до внедрения и мониторинга

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

    Чтобы модель обучалась, ей нужен численный критерий ошибки.

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

    Пояснение каждого элемента формулы:

  • — число объектов, по которым считается ошибка.
  • — индекс объекта (от до ).
  • — истинное значение целевой переменной для объекта .
  • — предсказание модели для объекта .
  • — квадрат ошибки на объекте.
  • — суммирование ошибок по всем объектам.
  • — усреднение, чтобы ошибка не росла просто из-за размера выборки.
  • Интуитивно MSE штрафует большие ошибки сильнее, чем маленькие, из-за квадрата.

    Качество, обобщение и типичные ошибки

    Главная цель обучения — не идеально запомнить обучающие данные, а хорошо работать на новых данных. Это называется обобщающей способностью.

    Частые проблемы:

  • Переобучение: на обучающей выборке качество высокое, а на новых данных резко хуже.
  • Недообучение: модель слишком простая или плохо обучена, и качество низкое везде.
  • Утечка данных: когда в признаки случайно попадает информация, недоступная в реальном использовании.
  • Смещение данных: тренировочные данные отличаются от тех, что будут после внедрения.
  • Почему в этом курсе выбран C++

    Большая часть обучения моделей в индустрии действительно делается в Python, но C++ очень важен на уровне инженерии, скорости и внедрения.

    C++ часто выбирают, когда нужно:

  • Высокая производительность и низкие задержки (например, скоринг в реальном времени).
  • Работа на устройствах с ограниченными ресурсами.
  • Встраивание в существующую C++-кодовую базу.
  • Полный контроль над памятью, потоками, SIMD и компиляцией.
  • В ходе курса мы будем опираться на экосистему C++ для математики и ML-задач:

  • Eigen — линейная алгебра.
  • Armadillo — удобная библиотека линейной алгебры.
  • dlib — классические ML-алгоритмы и инструменты.
  • OpenCV — компьютерное зрение и обработка изображений.
  • XGBoost — градиентный бустинг (популярен в табличных задачах).
  • LightGBM — быстрый бустинг по деревьям.
  • Что будет дальше в курсе

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

  • Сначала разберем базовые типы моделей и метрики качества.
  • Затем перейдем к подготовке данных и построению признаков.
  • После этого изучим оптимизацию и обучение моделей на практике.
  • В финале сосредоточимся на внедрении: сериализация, скорость, интеграция в C++-сервисы и мониторинг.
  • 2. Инструменты C++ для ML: сборка, библиотеки, линалг

    Инструменты C++ для ML: сборка, библиотеки, линалг

    Зачем в курсе отдельно говорить про инструменты

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

    В C++ “внедрение” почти всегда означает инженерную работу:

  • собрать проект так, чтобы он воспроизводимо компилировался на CI и на проде
  • подключить линейную алгебру и ускорители (BLAS, MKL)
  • обеспечить предсказуемую производительность и стабильность
  • уметь диагностировать ошибки (санитайзеры, профилирование)
  • Эта статья — про минимальный “инженерный стек”, на котором дальше удобно изучать модели и писать ML-код на C++.

    !Карта основных инструментов, которые обычно нужны для C++ ML-проекта

    Компилятор и стандарт языка

    Для современного ML-кода на C++ удобно ориентироваться на C++17 или C++20.

    На практике чаще всего встречаются:

  • GCC
  • Clang
  • Microsoft Visual C++
  • Рекомендации по настройке:

  • фиксируйте стандарт языка в сборке (например, C++17)
  • включайте предупреждения компилятора и относитесь к ним как к сигналам качества
  • для библиотек и продакшена используйте сборку Release, для разработки — Debug или RelWithDebInfo
  • CMake как базовый стандарт сборки

    В C++ мире сборка проекта почти всегда строится вокруг CMake. Он не компилирует сам, а генерирует проекты для конкретной системы сборки (например, Ninja, Makefiles, Visual Studio).

    Почему CMake критичен в ML-проектах:

  • зависимости (Eigen, OpenCV, dlib) проще подключать и переносить между ОС
  • удобно задавать опции оптимизации и флаги компилятора
  • проще организовать тесты и бенчмарки
  • проще собрать проект в CI
  • Минимальный CMakeLists.txt для проекта с Eigen

    Eigen — заголовочная библиотека, но в Linux часто ставится пакетом и находится через find_package.

    Что здесь важно:

  • CMAKE_CXX_STANDARD 17 фиксирует стандарт
  • find_package(Eigen3 ...) находит установленный Eigen
  • target_link_libraries(... Eigen3::Eigen) подключает include-пути корректно и переносимо
  • Типы сборки: Debug, Release, RelWithDebInfo

    В CMake тип сборки влияет на оптимизации и отладочную информацию.

  • Debug: проще отлаживать, обычно медленно
  • Release: максимально быстро, но отладка сложнее
  • RelWithDebInfo: оптимизации как в Release, но с отладочными символами
  • Практика для ML:

  • разработка и тесты корректности: Debug или RelWithDebInfo
  • замеры скорости и внедрение: Release
  • Как подключать зависимости: варианты и компромиссы

    Зависимости в C++ можно подключать по-разному. Универсального “правильного” способа нет — выбор зависит от команды, ОС и требований к воспроизводимости.

    Системные пакеты

    Подход: ставить библиотеки через пакетный менеджер ОС (apt, pacman, brew).

  • плюс: быстро и просто
  • минус: версии могут отличаться между машинами и CI
  • Встраивание в репозиторий

    Подход: git submodule или скачивание исходников.

  • плюс: полный контроль над версией
  • минус: вы берете на себя поддержку обновлений и сборку библиотеки
  • Менеджеры зависимостей C++

    Два популярных варианта:

  • vcpkg
  • Conan
  • Оба умеют:

  • скачивать и собирать библиотеки
  • фиксировать версии
  • интегрироваться с CMake
  • Рекомендация для учебного курса:

  • если вы в Windows-экосистеме и хотите “поставить библиотеки как пакеты” — vcpkg часто ощущается проще
  • если вы хотите гибко управлять профилями сборки, настройками компилятора и рецептами — Conan очень распространен в индустрии
  • Линейная алгебра — фундамент почти любого ML

    Большая часть классического ML (и существенная часть DL-инференса) сводится к операциям над:

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

    Eigen

    Eigen — одна из самых популярных библиотек линейной алгебры в C++.

  • заголовочная (часто проще подключать)
  • быстрые выражения через шаблоны
  • удобно для прототипирования и продакшена
  • Минимальный пример вычисления MSE в C++ с Eigen:

    Как читать этот код:

  • VectorXd — вектор вещественных чисел динамического размера
  • diff.array().square() — поэлементное возведение в квадрат
  • mean() — среднее значение по всем элементам
  • Armadillo

    Armadillo — другая популярная библиотека линалга, близкая по стилю к MATLAB.

  • удобный API
  • часто использует BLAS/LAPACK под капотом (что может давать ускорение)
  • Выбор между Eigen и Armadillo обычно диктуется:

  • требованиями к зависимостям (Eigen проще как header-only)
  • тем, что уже принято в вашем проекте
  • тем, хотите ли вы “из коробки” опираться на BLAS/LAPACK
  • BLAS/LAPACK и ускоренные бэкенды

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

    Термины:

  • BLAS — стандарт интерфейсов для базовых операций (например, матричное умножение)
  • LAPACK — стандарт интерфейсов для более сложных операций (разложения, решения систем)
  • Популярные реализации:

  • OpenBLAS
  • Intel oneAPI Math Kernel Library (oneMKL)
  • Практическая идея:

  • ваш код “просит” операцию (например, умножение матриц)
  • библиотека линалга вызывает BLAS
  • BLAS выбирает оптимизированный путь (SIMD, многопоточность)
  • Важно: быстрый BLAS иногда дает большой выигрыш без изменения ML-логики.

    Библиотеки ML и смежные инструменты в C++

    В C++ редко пишут “весь ML с нуля”. Чаще берут готовые компоненты.

    Классический ML

  • dlib — набор классических алгоритмов и инструментов
  • Компьютерное зрение и обработка изображений

  • OpenCV — де-факто стандарт для CV-задач и препроцессинга
  • Бустинг по деревьям

  • XGBoost
  • LightGBM
  • Даже если обучение вы делаете не в C++, наличие C++ API полезно для быстрого инференса и интеграции в сервис.

    Многопоточность и производительность: что важно понимать заранее

    ML-нагрузки часто “упираются” в CPU. В C++ есть несколько уровней ускорения.

  • SIMD и оптимизации компилятора (обычно “бесплатно”, если правильно собран Release)
  • многопоточность (например, OpenMP внутри BLAS или внутри вашей логики)
  • правильные структуры данных (не копировать большие матрицы без необходимости)
  • Если вы используете OpenBLAS или MKL, проверьте:

  • сколько потоков они используют
  • не создаете ли вы вложенную многопоточность (когда и вы, и BLAS распараллеливаете одно и то же)
  • Инструменты качества: тесты, санитайзеры, стиль

    В ML-проектах ошибки бывают не только математические, но и инженерные: утечки памяти, гонки, UB, ошибки на границах массивов.

    Тестирование

    Два самых распространенных фреймворка:

  • GoogleTest
  • Catch2
  • Что тестировать в ML-коде на C++:

  • корректность препроцессинга (нормализация, обработка пропусков)
  • стабильность сериализации/десериализации модели
  • совпадение результатов инференса с эталоном
  • граничные случаи (пустые входы, NaN/inf, очень большие значения)
  • Санитайзеры

    Санитайзеры — это режимы инструментирования, которые помогают ловить ошибки во время выполнения.

  • AddressSanitizer (ASan): выход за границы, use-after-free
  • UndefinedBehaviorSanitizer (UBSan): неопределенное поведение
  • ThreadSanitizer (TSan): гонки данных
  • Документация:

  • Sanitizers (LLVM)
  • Стиль и статический анализ

    Инструменты, которые обычно используют в командах:

  • clang-format — единый стиль кода
  • clang-tidy — проверки и подсказки по качеству
  • Cppcheck — статический анализ
  • Цель не “сделать красиво”, а снизить вероятность дорогих багов в продакшене.

    Рекомендованный минимальный стек для учебных проектов курса

    Чтобы дальше по курсу не отвлекаться на инфраструктуру, достаточно следующего набора.

  • CMake (с понятными целями и настройками)
  • Eigen для линейной алгебры
  • один менеджер зависимостей (vcpkg или Conan) по желанию, если вы не хотите зависеть от пакетов ОС
  • тестовый фреймворк (GoogleTest или Catch2)
  • clang-format и clang-tidy (хотя бы базово)
  • В следующих темах мы начнем использовать этот стек для реализации простых моделей и оценки качества — так, чтобы код сразу был близок к тому, что делают в реальных C++ ML-сервисах.

    3. Подготовка данных: чтение, очистка, признаки, пайплайны

    Подготовка данных: чтение, очистка, признаки, пайплайны

    Зачем в ML так много внимания данным

    В предыдущих статьях мы:

  • разобрали, что такое модель, признаки, датасет и разделение на выборки
  • настроили базовый C++-инструментарий (CMake, Eigen, зависимости)
  • Теперь переходим к самому частому источнику “почему модель плохая”: подготовка данных. В реальных проектах качество, стабильность и воспроизводимость пайплайна подготовки данных часто важнее выбора конкретного алгоритма.

    Типовые цели подготовки данных:

  • привести данные к формату, удобному для обучения и инференса
  • устранить пропуски и аномалии
  • построить признаки так, чтобы модель могла извлекать закономерности
  • не допустить утечки данных между train/val/test
  • сделать одинаковую обработку в обучении и на продакшене
  • !Обобщенная карта шагов, которая помогает не потерять важные детали

    Форматы данных и чтение в C++

    На практике данные для классического ML чаще всего приходят в одном из форматов:

  • CSV/TSV файлы
  • логи (текстовые строки)
  • бинарные форматы (реже на старте, чаще в крупных системах)
  • данные из БД (но в учебных примерах обычно начинаем с файлов)
  • Минимальный разбор CSV без внешних библиотек

    Для учебных проектов полезно уметь прочитать “простой CSV” самостоятельно: без кавычек, без запятых внутри полей. Это не универсальный CSV-парсер, но хороший старт.

    Пример: читаем файл, пропускаем заголовок, извлекаем числовые признаки и целевую переменную.

    Что важно понимать про этот подход:

  • он подходит для контролируемых учебных данных
  • в реальных CSV часто бывают кавычки, экранирование, пропуски в виде пустых полей
  • Если вам нужен быстрый “готовый” парсер CSV, можно посмотреть репозиторий fast-cpp-csv-parser. Он популярен, но все равно важно понимать, что именно вы ожидаете от формата.

    Представление данных для ML-кода

    Для большинства классических моделей удобно хранить:

  • матрицу признаков размера
  • вектор ответов размера
  • Где:

  • — число объектов (строк)
  • — число признаков (столбцов)
  • В C++ с Eigen типичный выбор:

  • Eigen::MatrixXd для
  • Eigen::VectorXd для
  • Плюс такого представления: большинство операций (нормализация, вычисление статистик) получаются короткими и быстрыми.

    Очистка данных: пропуски, типы, аномалии

    Очистка — это приведение данных к корректному и предсказуемому виду.

    Пропуски

    Пропуски встречаются почти всегда: не заполнили поле, сломался источник, значение неприменимо.

    Основные стратегии:

  • удалить строки с пропусками (часто плохо, если данных мало)
  • заполнить константой (например, 0 или -1, но это может исказить смысл)
  • заполнить статистикой по train (средним/медианой)
  • добавить бинарный признак “значение было пропущено”
  • Ключевое правило: все параметры заполнения считаются только по обучающей выборке, иначе появится утечка данных.

    Приведение типов и единиц измерения

    Типовые проблемы:

  • числа пришли строками (нужно парсить)
  • разные единицы измерения в разных источниках (например, “рубли” и “тысячи рублей”)
  • разные часовые пояса и форматы дат
  • Практика: в пайплайне явно фиксируйте все преобразования и покрывайте их тестами.

    Выбросы и некорректные значения

    Выброс — это значение, которое сильно выбивается из типичного диапазона.

    Причины:

  • реальная редкая ситуация
  • ошибка ввода/сбоя
  • другой “режим” данных (например, новый сегмент пользователей)
  • Что можно делать:

  • клиппинг: ограничивать значения сверху/снизу (winsorization)
  • лог-преобразования для длинных хвостов
  • отдельные правила валидации (например, возраст не может быть отрицательным)
  • В продакшене обычно дополнительно вводят “защиту инференса”: если на вход пришли NaN/inf или слишком большие числа, сервис должен обработать это предсказуемо.

    Построение признаков: что модель реально “видит”

    Модель не понимает “дату”, “город” или “текст” как человек. Она работает с числами. Признаки — это мост между сырой реальностью и числовым представлением.

    Масштабирование числовых признаков

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

    Самый распространенный вариант — стандартизация:

    Где:

  • — исходное значение признака
  • — среднее значение признака по обучающей выборке
  • — стандартное отклонение признака по обучающей выборке
  • — стандартизированное значение
  • Почему это нельзя считать “на всем датасете сразу”: если вы посчитали и на train+test, то “подсмотрели” распределение test.

    Минимальная реализация “скейлера” на Eigen:

    Обратите внимание:

  • fit вычисляет параметры только на train
  • transform применяет те же параметры к любым данным (val/test/прод)
  • константный признак не должен ломать код
  • Категориальные признаки

    Категориальные признаки — это значения вроде “город=Москва”, “тариф=Премиум”. Сами по себе они не числа.

    Популярные подходы:

  • one-hot encoding: отдельный бинарный столбец на каждую категорию
  • ordinal encoding: сопоставить категории с числами (опасно, если нет естественного порядка)
  • hashing trick: отправлять категорию в фиксированное число корзин хешем
  • Для C++-инференса важен вопрос что делать с неизвестной категорией, которая не встречалась на train:

  • завести специальную категорию __UNK__
  • или игнорировать (в one-hot это “все нули”)
  • Даты и время

    Почти никогда нельзя просто “подать дату строкой”. Часто используют:

  • компоненты: час, день недели, месяц
  • циклические преобразования для периодичности (например, часы по кругу)
  • агрегаты по окнам (скользящие суммы/средние)
  • Даже простые вещи вроде “день недели” могут резко поднять качество, если в данных есть сезонность.

    Нормализация текста и изображений

    В этом курсе упор на классический ML и инженерное внедрение, но важно понимать общий принцип:

  • текст обычно превращают в числовой вектор (например, bag-of-words, TF-IDF, эмбеддинги)
  • изображения часто нормализуют (масштабирование, вычитание среднего) и приводят к нужному размеру
  • Если вы работаете с изображениями в C++, де-факто стандарт препроцессинга — OpenCV.

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

    В первой статье мы обсуждали train/val/test. В подготовке данных эта тема становится практической.

    Правильный порядок действий

    Частая ошибка новичков: “сначала нормализую весь датасет, потом делю”. Это почти всегда утечка.

    Правильный порядок:

  • Разделить данные на train/val/test.
  • На train посчитать все параметры препроцессинга: средние, словари категорий, статистики.
  • Применить полученные параметры к val/test.
  • Особые случаи: временные ряды и группы

    Иногда случайное перемешивание запрещено:

  • временные ряды: train должен быть “раньше”, test “позже”
  • групповые данные: нельзя, чтобы объекты одного пользователя одновременно попали в train и test
  • Иначе вы получите завышенное качество “на бумаге” и провал после внедрения.

    Пайплайн в C++: как сделать обработку воспроизводимой

    Пайплайн — это последовательность шагов, которая из сырого объекта делает вектор признаков, а затем дает предсказание.

    Почему пайплайн важен:

  • вы не хотите дублировать логику “как готовить признаки” в десяти местах
  • вы хотите, чтобы обучение и продакшен-инференс делали одно и то же
  • вы хотите уметь сериализовать не только модель, но и препроцессинг
  • Минимальная архитектура: fit/transform

    Один из самых практичных паттернов:

  • fit учится на train и запоминает параметры
  • transform применяет параметры к данным
  • Ниже упрощенная идея “цепочки преобразований”. Это не библиотека, а каркас, который помогает структурировать код.

    Как это использовать концептуально:

  • добавляете шаги (например, StandardScaler)
  • вызываете pipeline.fit(X_train)
  • затем pipeline.transform(X_val) и pipeline.transform(X_test)
  • Сериализация: модель важна, но препроцессинг тоже

    Если вы нормализовали признаки или кодировали категории, то на проде вам нужны:

  • параметры нормализации (средние, стандартные отклонения)
  • словари категорий
  • порядок признаков и версия схемы
  • Сериализация может быть:

  • бинарная (быстро и компактно, но сложнее отлаживать)
  • текстовая (например, JSON) для удобства проверки
  • Для JSON в C++ часто используют библиотеку nlohmann/json.

    Ключевая инженерная идея: артефакт инференса — это почти всегда “модель + препроцессинг + метаданные”.

    Практический чек-лист перед обучением модели

    Перед тем как обучать первую модель (в следующих темах), полезно пройтись по списку.

  • Определены входные признаки и целевая переменная.
  • Зафиксирован формат данных (что означают столбцы, какие единицы измерения).
  • Есть стратегия обработки пропусков.
  • Есть стратегия обработки неизвестных категорий.
  • Разделение на train/val/test сделано корректно для вашей природы данных.
  • Все параметры препроцессинга считаются только на train.
  • Реализация препроцессинга покрыта тестами на граничные случаи.
  • Подготовка данных оформлена как воспроизводимый пайплайн, который можно применить на проде.
  • В следующей части курса мы начнем обучать простые модели на подготовленных данных и обсуждать метрики качества уже “на реальном коде”, где пайплайн подготовки играет решающую роль.

    4. Обучение с учителем: регрессия и классификация на C++

    Обучение с учителем: регрессия и классификация на C++

    Что мы делаем в обучении с учителем

    В прошлых статьях мы:

  • разобрали, какие бывают задачи ML и как устроен жизненный цикл модели
  • собрали минимальный C++-стек (CMake, Eigen, зависимости)
  • обсудили подготовку данных, пайплайны fit/transform и борьбу с утечками
  • Теперь мы переходим к обучению с учителем на практике: когда для каждого объекта в данных есть правильный ответ.

    В обучении с учителем обычно решают два базовых типа задач:

  • регрессия: предсказываем число (цена, время, спрос)
  • классификация: предсказываем класс (спам/не спам, дефект/норма)
  • !Схема, связывающая данные, препроцессинг, обучение и продакшен-инференс

    Формализация: признаки, цель, предсказание

    Договоримся об обозначениях, которые будут совпадать с тем, как мы храним данные в C++ (например, в Eigen).

  • — число объектов (строк датасета)
  • — число признаков (столбцов)
  • — матрица признаков размера
  • — вектор истинных ответов размера
  • — вектор предсказаний модели размера
  • В простейших моделях мы строим функцию, которая из строки признаков получает предсказание.

    Регрессия на C++: линейная модель как базовый пример

    Линейная регрессия: идея модели

    Самая базовая регрессионная модель — линейная:

    Разберем элементы формулы:

  • — вектор признаков одного объекта (например, площадь, этаж, район)
  • — вектор весов модели, по одному весу на каждый признак
  • — скалярное произведение (взвешенная сумма признаков)
  • — смещение (bias), позволяющее сдвигать предсказание
  • — предсказанное число
  • Почему это полезно в курсе:

  • модель проста, но уже требует правильной подготовки данных (масштабирование)
  • есть понятная функция потерь и оптимизация
  • реализация на Eigen получается компактной и быстрой
  • Функция потерь для регрессии

    Часто используют среднеквадратичную ошибку (MSE):

    Пояснение элементов:

  • — число объектов, по которым считаем ошибку
  • — индекс объекта
  • — истинное значение цели для объекта
  • — предсказание модели для объекта
  • — ошибка на объекте
  • квадрат усиливает штраф за большие промахи
  • усреднение делает метрику сравнимой между датасетами разных размеров
  • На практике иногда удобнее дополнительно смотреть:

  • MAE (средняя абсолютная ошибка) — менее чувствительна к выбросам
  • RMSE (корень из MSE) — в тех же единицах измерения, что и цель
  • Как обучать: градиентный спуск (понятно и универсально)

    Для C++-проектов важно понимать не только готовые библиотеки, но и общий принцип обучения.

    Идея градиентного спуска: мы хотим подобрать параметры так, чтобы MSE стала минимальной. Для этого двигаемся маленькими шагами в сторону уменьшения ошибки.

    Упрощенная запись шага обновления:

    Где:

  • — значение функции потерь на обучающих данных (например, MSE)
  • — градиент потерь по весам (направление роста потерь)
  • — скорость обучения (learning rate), размер шага
  • стрелка означает: “перезаписать новое значение”
  • Важно: вам не обязательно выводить градиенты вручную каждый раз, но полезно понимать, что “обучение” — это управляемая оптимизация.

    Минимальная реализация линейной регрессии на Eigen (batch gradient descent)

    Ниже — учебный пример, который уже похож на реальный код: отдельный класс, fit и predict, и аккуратная работа с размерностями.

    Практические замечания к этому коду:

  • перед обучением крайне желательно масштабировать признаки (например, StandardScaler из прошлой статьи)
  • скорость сходимости и качество сильно зависят от lr и iters
  • это “полный batch” градиентный спуск, он прост, но на очень больших данных обычно используют mini-batch
  • Классификация на C++: логистическая регрессия как базовый пример

    Что меняется по сравнению с регрессией

    В бинарной классификации целевой ответ обычно :

  • — объект принадлежит “позитивному” классу (например, спам)
  • — “негативному” классу
  • Модель должна выдавать не просто число, а вероятность класса 1.

    Сигмоида: превращаем линейный выход в вероятность

    Логистическая регрессия делает линейную комбинацию признаков, а затем применяет сигмоиду:

    Пояснение элементов:

  • — “сырое” линейное значение (логит)
  • — сигмоида, всегда дает число от 0 до 1
  • — основание натурального логарифма
  • — предсказанная вероятность класса 1
  • Чтобы получить класс, обычно используют порог:

  • если , предсказываем 1
  • иначе 0
  • Порог не обязан быть 0.5: его часто подбирают под бизнес-стоимость ошибок.

    Функция потерь для классификации: log loss

    Для вероятностных моделей стандартная функция потерь — логистическая (кросс-энтропия):

    Пояснение элементов:

  • — число объектов
  • — истинный класс (0 или 1)
  • — предсказанная вероятность класса 1
  • — штраф, если модель дает маленькую вероятность при
  • — штраф, если модель дает большую вероятность при
  • минус перед суммой нужен, чтобы “хорошие” вероятности давали меньшую ошибку
  • Важная инженерная деталь: нужно избегать . Обычно делают клиппинг вероятностей (например, ограничивают снизу и сверху малым числом).

    Минимальная реализация логистической регрессии на Eigen

    Что важно в реальных задачах классификации:

  • масштабирование признаков обычно сильно помогает логистической регрессии
  • если классы несбалансированы (например, 1% “фрод”), accuracy может быть обманчивой
  • порог классификации нужно подбирать осмысленно, а не всегда брать 0.5
  • !Иллюстрация решения логистической регрессии: линейная граница и сигмоида

    Метрики качества: как не обмануться

    Регрессия

    На валидации и тесте обычно считают:

  • MSE или RMSE
  • MAE
  • Практический смысл:

  • MAE проще интерпретировать как “средняя ошибка в единицах цели”
  • MSE сильнее штрафует редкие большие промахи
  • Классификация

    Самые популярные метрики для бинарной классификации:

  • accuracy: доля верных ответов
  • precision: доля верных среди предсказанных “позитивных”
  • recall: доля найденных “позитивных” среди всех позитивных
  • ROC-AUC: качество ранжирования по вероятности
  • Accuracy подходит, когда классы сбалансированы и цена ошибок примерно одинакова. Если нет, часто важнее precision/recall.

    Для справки по определениям метрик удобно иметь под рукой базовую страницу:

  • Precision and recall
  • Регуляризация: когда модель начинает “слишком верить” признакам

    Даже простые линейные модели могут переобучаться, особенно если:

  • признаков много
  • признаки коррелируют
  • данных мало
  • Один из базовых способов стабилизации — L2-регуляризация (ridge). Идея: слегка штрафовать большие веса, чтобы модель не делала слишком резких “ставок” на отдельные признаки.

    Концептуально это выглядит так:

  • к функции потерь добавляют штраф за величину весов
  • коэффициент штрафа подбирают на валидации
  • В C++-коде это обычно означает “добавить к градиенту”, но в учебной версии достаточно понимать смысл: контроль сложности модели через веса.

    Как связать это с пайплайном из прошлой статьи

    Чтобы обучение и продакшен-инференс совпадали, полезно держать в голове архитектуру “препроцессинг + модель”. Практический шаблон:

  • Разделяем данные на train/val/test.
  • Делаем scaler.fit(X_train).
  • Преобразуем: X_train_s = scaler.transform(X_train), X_val_s = scaler.transform(X_val).
  • Обучаем модель на X_train_s.
  • Считаем метрики на X_val_s и подбираем гиперпараметры.
  • Финально проверяем на X_test_s.
  • Сериализуем и scaler, и модель.
  • Если вы позже подключите готовые алгоритмы, например из dlib, структура мышления останется та же:

  • данные и препроцессинг должны быть идентичны в train и production
  • параметры препроцессинга считаются только по train
  • Полезная отправная точка для классических алгоритмов в C++:

  • dlib
  • Типичные ошибки в supervised learning на C++

  • Обучили на нормализованных данных, а в проде забыли нормализовать.
  • Посчитали параметры нормализации на всем датасете до разбиения (утечка данных).
  • Сравнили модели по метрике, не подходящей под задачу (например, accuracy при сильном дисбалансе).
  • Не проверили вход на NaN/inf и выбросы на инференсе.
  • Не зафиксировали порядок признаков (в C++ это часто приводит к тихим логическим багам).
  • Что дальше

    На этом этапе у вас есть базовый “скелет” supervised learning:

  • матрица признаков и цель
  • препроцессинг fit/transform
  • простые модели (линейная и логистическая регрессия)
  • метрики, по которым можно честно сравнивать варианты
  • Дальше по курсу логично расширять инструментарий:

  • более сильные модели для табличных данных (деревья, бустинг)
  • подбор гиперпараметров и кросс-валидация
  • сериализация артефакта инференса и интеграция в C++-сервис
  • ускорение (память, SIMD, многопоточность) и мониторинг качества
  • 5. Нейросети и инференс: ONNX Runtime и интеграция

    Нейросети и инференс: ONNX Runtime и интеграция

    Зачем ONNX Runtime в курсе про C++

    В предыдущих статьях мы строили классический пайплайн данные → признаки → обучение → метрики и реализовали простые модели на Eigen. В реальных продуктах нейросети часто обучают не в C++, а в Python (PyTorch или TensorFlow), а затем выносят в продакшен как быстрый и стабильный инференс-компонент.

    ONNX Runtime — популярный способ сделать это переносимо:

  • модель экспортируется в формат ONNX
  • на проде (в C++ сервисе, десктоп-приложении или на устройстве) модель выполняется через ONNX Runtime
  • Ключевая идея статьи: в продакшене важнее предсказуемый инференс и повторяемый препроцессинг, чем “как именно” нейросеть обучалась.

    !Общая картина: как обученная нейросеть попадает в C++ и становится частью сервиса

    Базовые понятия: ONNX, инференс, граф

  • Нейросеть: модель, состоящая из слоев (линейные преобразования, свертки, нормализации, активации).
  • Инференс: применение уже обученной модели для получения предсказания на новых данных.
  • ONNX: формат для представления модели как вычислительного графа (узлы — операции, ребра — тензоры). Официальный сайт: ONNX.
  • ONNX Runtime (ORT): движок, который загружает ONNX-модель и выполняет ее на CPU или ускорителях. Документация: ONNX Runtime.
  • Почему это удобно для C++:

  • формат ONNX отделяет обучение от исполнения
  • один и тот же артефакт можно запускать в разных окружениях
  • у ORT есть C/C++ API, оптимизации графа и настройки производительности
  • Что именно нужно “перенести” в продакшен

    Новички часто думают, что достаточно перенести файл с весами. На практике артефакт инференса почти всегда шире:

  • модель (ONNX-файл)
  • препроцессинг (нормализация, словари категорий, порядок признаков)
  • постпроцессинг (порог, mapping класса в label, NMS для детекции)
  • метаданные (версия, ожидаемые размеры входов, типы)
  • Связь с прошлой статьей про данные и пайплайны:

  • если вы делали StandardScaler.fit(X_train), то его параметры должны использоваться и на инференсе
  • ошибка “забыли нормализацию в проде” ломает нейросети так же, как и линейные модели
  • Экспорт модели в ONNX: практические варианты

    Ниже — типовые сценарии. Детали зависят от фреймворка, но общий смысл один: зафиксировать входы/выходы, размеры и типы.

    PyTorch → ONNX

    Обычно экспорт делается из Python-кода через torch.onnx.export. Официальная документация: PyTorch ONNX Export.

    Практические рекомендации:

  • фиксируйте порядок и названия входов/выходов
  • явно задавайте opset (версию операторов ONNX)
  • если нужны динамические размеры (например, переменная длина последовательности), задавайте dynamic axes, но помните: динамика усложняет интеграцию и тестирование
  • TensorFlow → ONNX

    Для TensorFlow часто используют конвертер tf2onnx. Репозиторий: tf2onnx.

    Важно:

  • проверяйте, что все операции корректно конвертируются
  • тестируйте совпадение выходов TF и ONNX на одинаковом входе
  • Проверка модели до C++ интеграции

    Полезный минимальный контроль качества:

  • прогоните несколько тестовых примеров в Python через ONNX Runtime (там тоже есть API)
  • убедитесь, что выходы совпадают с исходным фреймворком в допустимой погрешности
  • Документация Python API: ONNX Runtime Python API.

    Архитектура C++ интеграции: как выглядит инференс

    Типовой C++ инференс-компонент состоит из:

  • загрузки модели в Session
  • подготовки входных тензоров (память, форма, тип)
  • запуска Run
  • извлечения выходных тензоров
  • В терминах ONNX Runtime:

  • Env: глобальная среда ORT
  • SessionOptions: оптимизации, потоки, провайдеры выполнения
  • Session: загруженная модель, готовая к инференсу
  • Минимальный пример инференса на C++

    Ниже пример “скелета”. Он показывает структуру, а не все проверки и удобства.

    Что в этом коде принципиально важно:

  • тип данных входа (например, float) должен совпадать с моделью
  • форма тензора (например, [1, 3]) должна совпадать с ожидаемой
  • имена "input" и "output" должны соответствовать реальным именам в графе
  • Как узнать имена входов/выходов и их формы

    В учебных проектах часто “зашивают” имена, но в продакшене полезнее проверять модель на старте:

  • сколько у нее входов/выходов
  • какие типы и размерности
  • какие имена
  • В ONNX Runtime C++ API для этого есть методы GetInputCount, GetInputNameAllocated, GetInputTypeInfo и аналогичные для output. Документация C/C++ API: ONNX Runtime C/C++ API.

    Практическая рекомендация:

  • при старте сервиса логируйте: версия модели, входные/выходные имена, ожидаемые формы
  • если модель “не та”, лучше упасть сразу, чем молча считать неправильные результаты
  • Препроцессинг и постпроцессинг: главный источник багов

    Повторяемость препроцессинга

    Связь с прошлой темой про пайплайны fit/transform:

  • все параметры (средние/стандартные отклонения, словари, порядок признаков) считаются на train
  • в C++ инференсе применяются те же параметры
  • На практике это означает, что вам нужен артефакт вроде:

  • model.onnx
  • preprocess.json (или бинарный формат): параметры нормализации, словари
  • schema.json: список фичей и их порядок
  • Если препроцессинг сложный, иногда его тоже переносят в ONNX (как часть графа), но это требует дисциплины при экспорте и тестировании. Спецификация операторов: ONNX Operators.

    Постпроцессинг

    Примеры:

  • бинарная классификация: порог вероятности не обязательно 0.5
  • многоклассовая классификация: выбор argmax по выходному вектору
  • детекция: NMS и фильтрация по score
  • Важно фиксировать постпроцессинг так же строго, как и модель.

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

    ONNX Runtime дает много возможностей, но выигрыши обычно приходят из нескольких базовых источников.

    Оптимизации графа

  • включайте ORT_ENABLE_ALL в SetGraphOptimizationLevel
  • тестируйте на реальных входных формах
  • Потоки и параллелизм

    Параметры, которые чаще всего трогают:

  • SetIntraOpNumThreads: параллелизм внутри одной операции
  • SetInterOpNumThreads: параллелизм между операциями
  • Практическое правило:

  • не делайте “как можно больше потоков” без измерений
  • учитывайте, что внутри ORT и внутри матмул-библиотек может быть собственная многопоточность
  • Уменьшение копирований

    На latency сильно влияют лишние копии входных данных.

    Подходы:

  • формировать входной буфер сразу в нужном формате (например, float32 NCHW)
  • использовать механизмы привязки буферов (например, I/O binding) там, где это оправдано
  • Документация по I/O binding: ONNX Runtime IO Binding.

    Квантование

    Квантование — перевод части вычислений из float в int8/uint8, чтобы ускорить CPU и уменьшить модель.

    Варианты:

  • post-training quantization: после обучения
  • quantization-aware training: модель обучается с учетом квантования
  • В ORT есть инструменты для квантования, чаще используемые через Python-инструментарий. Раздел документации: ONNX Runtime Quantization.

    Сборка и подключение ONNX Runtime в CMake-проект

    ONNX Runtime можно подключать разными способами. Универсального “одного правильного” нет: выбор зависит от CI, ОС и политики зависимостей.

    Вариант: готовые сборки ORT

    ORT публикует релизы с готовыми бинарями. Страница релизов: ONNX Runtime Releases.

    Типовой подход:

  • скачать архив для нужной ОС
  • положить include и lib в заранее известное место
  • подключить через target_include_directories и target_link_libraries
  • Пример CMake-скелета:

    Практика:

  • фиксируйте версию ORT в проекте
  • добавляйте проверку, что нужные файлы реально найдены
  • Вариант: сборка ORT из исходников

    Если нужно специфичное (например, особые execution providers или одинаковые флаги оптимизации), ORT собирают из исходников. Инструкции: ONNX Runtime Build Instructions.

    Надежность: валидация входов и поведение на ошибках

    В продакшен-инференсе важнее “не упасть и не выдать мусор”, чем поддержать любой вход.

    Что стоит проверять до запуска session.Run:

  • размерность и типы (ожидаемые float32/int64 и т.д.)
  • диапазоны значений после препроцессинга (защита от NaN/inf)
  • корректность категорий (что делать с неизвестными значениями)
  • Практический шаблон:

  • если вход некорректен: вернуть предсказуемую ошибку (например, 400 Bad Request) или fallback
  • логировать технические детали, но не утекать персональными данными
  • Тестирование: как убедиться, что C++ инференс “тот же самый”

    Минимальный набор тестов, который реально ловит регрессии:

  • golden tests: фиксированные входы и ожидаемые выходы (с допуском)
  • тест совместимости препроцессинга: один и тот же объект → одинаковый вектор признаков в train-коде и в inference-коде
  • тест на обновление версии модели: сервис стартует, логирует входы/выходы, не падает
  • Полезно хранить рядом:

  • небольшие наборы тестовых входов
  • эталонные выходы, посчитанные в Python
  • Как это связывается с остальным курсом

    Эта тема “сшивает” обучение и внедрение:

  • из статей про данные вы берете принцип одинаковый препроцессинг на train и prod
  • из статьи про supervised learning вы берете дисциплину метрики, валидация, тестовый набор
  • добавляется продакшен-слой: загрузка артефакта, управление версиями, производительность и надежность
  • После этой статьи у вас есть полноценная “трасса” для нейросетей:

  • обучение в удобном фреймворке
  • экспорт в ONNX
  • быстрый инференс в C++ через ONNX Runtime
  • контроль качества через тесты и воспроизводимый препроцессинг
  • 6. Оценка качества: метрики, кросс-валидация, отладка

    Оценка качества: метрики, кросс-валидация, отладка

    Зачем нужна оценка качества

    В предыдущих темах курса мы построили базовый ML-конвейер:

  • подготовка данных и пайплайны fit/transform
  • обучение простых моделей (линейная и логистическая регрессия) на C++
  • перенос нейросетей в прод через ONNX Runtime
  • Оценка качества отвечает на два практических вопроса:

  • насколько модель полезна на новых данных, а не на тех, где она обучалась
  • почему модель ошибается, и что именно надо исправлять: данные, признаки, метрику, алгоритм, порог, или интеграцию
  • Ключевая инженерная мысль: метрика и протокол проверки качества — часть спецификации продукта. Если вы меняете метрику или способ разбиения данных, вы фактически меняете определение “хорошей модели”.

    !Схема протокола оценки качества и запрета утечки данных

    Протокол оценки: train/val/test и что считается “честной” проверкой

    Обычно используют:

  • train: обучение модели и всех параметров препроцессинга
  • validation: выбор гиперпараметров, порогов, вариантов фичей
  • test: финальная независимая оценка, к которой нельзя “подгоняться”
  • Важно, что “препроцессинг” — это тоже обучаемая часть системы:

  • скейлер (средние и стандартные отклонения)
  • словари категорий
  • порядок признаков
  • статистики для заполнения пропусков
  • Правило против утечки данных:

  • все параметры препроцессинга считаются только на train (или на train-фолде в кросс-валидации)
  • Метрики регрессии

    Регрессия — это предсказание числа. В статье про supervised learning мы использовали MSE, здесь расширим набор и научимся выбирать метрику под задачу.

    Обозначения:

  • — число объектов, по которым мы считаем качество
  • — истинное значение целевой переменной для объекта
  • — предсказание модели для объекта
  • MAE

    MAE (mean absolute error) — средняя абсолютная ошибка:

    Пояснение элементов формулы:

  • — модуль ошибки на объекте (без знака)
  • — суммирование по всем объектам
  • — усреднение, чтобы сравнивать качества на выборках разных размеров
  • Практический смысл:

  • MAE хорошо интерпретируется как “средняя ошибка в единицах цели”
  • MAE обычно устойчивее к выбросам, чем MSE
  • MSE и RMSE

    MSE (mean squared error) — среднеквадратичная ошибка:

    Пояснение элементов:

  • — ошибка на объекте
  • квадрат сильнее штрафует большие промахи
  • RMSE (root mean squared error) — корень из MSE:

    Практический смысл:

  • RMSE в тех же единицах, что и цель (как MAE)
  • RMSE сильнее “боится” редких больших ошибок
  • (коэффициент детерминации) показывает, насколько модель лучше (или хуже) наивного предсказания средним.

    Полезные свойства:

  • — очень хорошо
  • — примерно как предсказывать средним значением
  • — хуже, чем предсказывать средним
  • Ссылки для справки:

  • Коэффициент детерминации
  • Что выбрать на практике

  • если цена “редких больших ошибок” высока (например, планирование ресурсов) — часто смотрят MSE/RMSE
  • если важна типичная ошибка и выбросы не должны доминировать — часто смотрят MAE
  • если нужно сравнение “в относительном смысле к базовой линии” — можно использовать , но всегда вместе с MAE/RMSE
  • Метрики классификации

    Классификация — это предсказание класса. Часто модель выдает вероятность, а итоговый класс получают порогом.

    Матрица ошибок и базовые определения

    Для бинарной классификации (класс 1 и класс 0) вводят:

  • TP: предсказали 1, и это действительно 1
  • FP: предсказали 1, но это 0
  • TN: предсказали 0, и это 0
  • FN: предсказали 0, но это 1
  • !Матрица ошибок и обозначения TP/FP/TN/FN

    Accuracy

    Accuracy — доля верных ответов:

    Пояснение элементов:

  • числитель — сколько объектов классифицировано правильно
  • знаменатель — всего объектов
  • Ограничение:

  • при сильном дисбалансе классов accuracy может быть “обманчиво высокой” (например, 99% нормальных и 1% фрода)
  • Precision и recall

    Precision — точность среди предсказанных “позитивных”:

    Пояснение:

  • знаменатель — все, что модель назвала классом 1
  • чем меньше FP, тем выше precision
  • Recall — полнота среди всех истинных “позитивных”:

    Пояснение:

  • знаменатель — все реальные объекты класса 1
  • чем меньше FN, тем выше recall
  • Компромисс:

  • снижая порог, вы обычно увеличиваете recall и уменьшаете precision
  • повышая порог, вы обычно увеличиваете precision и уменьшаете recall
  • Ссылки для справки:

  • Precision and recall
  • F1-score

    F1 — способ “свести” precision и recall в одно число (гармоническое среднее):

    Пояснение элементов:

  • числитель умножает precision и recall, усиливая требование “оба должны быть хорошими”
  • знаменатель нормирует результат
  • F1 удобен, когда:

  • классы несбалансированы
  • важны и FP, и FN
  • нужно сравнить несколько вариантов модели одним числом, но все равно стоит смотреть и precision/recall отдельно
  • ROC-AUC и PR-AUC

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

  • ROC-AUC показывает качество ранжирования по вероятности на всех порогах
  • при сильном дисбалансе классов часто более информативен PR-AUC (площадь под precision-recall кривой)
  • Ссылки для справки:

  • Receiver operating characteristic
  • Precision-recall curve
  • Реализация метрик на C++ (Eigen)

    Ниже — минимальные функции, которые полезно держать в проекте рядом с моделями. Они пригодятся и для моделей на Eigen, и для проверки результатов инференса ONNX.

    Инженерные детали, которые экономят часы отладки:

  • проверяйте размеры входных векторов и матриц (иначе ошибки будут “тихими”)
  • определите поведение на нулевых знаменателях (например, precision при отсутствии предсказаний класса 1)
  • храните метрики и на train, и на val, чтобы видеть переобучение
  • Кросс-валидация: зачем и как делать правильно

    Одна валидационная выборка может “повезти” или “не повезти”. Кросс-валидация уменьшает эту случайность: вы несколько раз переобучаете модель на разных разбиениях и усредняете качество.

    K-fold кросс-валидация

    Идея:

  • делим данные на частей (фолдов)
  • раз повторяем:
  • - один фолд используется как validation - остальные фолдов как train
  • получаем значений метрики и усредняем
  • !Иллюстрация K-fold кросс-валидации

    Что усреднять:

  • обычно усредняют метрику по фолдам (например, среднее RMSE)
  • полезно также смотреть разброс (например, стандартное отклонение), чтобы понимать стабильность
  • Самая частая ошибка в CV: утечка через препроцессинг

    Неправильно:

  • один раз посчитать StandardScaler.fit на всех данных
  • затем делать k-fold и обучать модель на уже нормализованных признаках
  • Правильно:

  • для каждого фолда делать отдельный пайплайн:
  • - scaler.fit(X_train_fold) - X_train_fold_s = scaler.transform(X_train_fold) - X_val_fold_s = scaler.transform(X_val_fold) - обучение модели на X_train_fold_s

    Это продолжение идеи из статьи про подготовку данных: fit делается только на той части данных, которая считается train в текущем эксперименте.

    Стратификация, группы и время

    Обычный K-fold предполагает, что объекты независимы и их можно перемешивать. Часто это неверно.

  • Stratified K-fold: сохраняет доли классов в каждом фолде (важно при дисбалансе)
  • Group split: один пользователь (или один объект группы) должен попадать только в train или только в val, иначе вы переоцените качество
  • Time series split: train должен быть раньше, val позже (иначе вы “подсмотрите будущее”)
  • Практический выбор:

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

    Если вы подбираете гиперпараметры (learning rate, регуляризацию, порог), обычная CV может начать “подгоняться” под валидационные фолды.

    Nested CV разделяет:

  • внешний цикл: честная оценка качества
  • внутренний цикл: подбор гиперпараметров
  • В индустрии nested CV используют там, где цена ошибки оценки высока, но это дороже по вычислениям.

    Отладка качества: как понять, что именно сломано

    Когда метрика “плохая”, полезно идти от простого к сложному. Ниже — практический план диагностики, который хорошо ложится на C++-проекты.

    Базовые sanity-checks

  • сравните модель с простым бейзлайном:
  • - регрессия: предсказывать среднее по train - классификация: предсказывать самый частый класс
  • проверьте, что метрика на train лучше (или хотя бы не хуже), чем на val
  • проверьте диапазоны входов после препроцессинга (NaN/inf, экстремальные значения)
  • Анализ разницы train vs validation

  • сильно лучше на train, сильно хуже на val: похоже на переобучение или утечку
  • плохо и на train, и на val: похоже на недообучение, плохие признаки или ошибка в пайплайне
  • Практические причины переобучения в C++-пайплайне:

  • вы случайно “протащили” таргет в признаки
  • параметры препроцессинга посчитаны не только на train
  • порядок фичей перепутан между обучением и инференсом
  • Ошибки препроцессинга и схемы признаков

    В C++-интеграции частый источник проблем — рассинхронизация схемы:

  • в обучении одна версия списка признаков
  • в инференсе другая версия, другой порядок, другая обработка пропусков
  • Практика из статьи про ONNX Runtime:

  • храните артефакт инференса как “модель + препроцессинг + схема + версия”
  • добавляйте golden tests: фиксированный вход и ожидаемый выход
  • Error analysis: разбор ошибок по срезам

    Иногда “в среднем” качество приемлемо, но есть провалы на конкретных сегментах.

    Идеи срезов:

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

  • вы находите конкретный сегмент, где нужна новая фича, другая модель или отдельная обработка данных
  • Численная стабильность и воспроизводимость

    Для C++ это особенно важно:

  • фиксируйте seed, если есть рандомизация (перемешивание, инициализация)
  • в релизной сборке сравнивайте результаты “в допуске”, а не бит-в-бит
  • избегайте нестабильных операций в метриках:
  • - для логарифмов используйте клиппинг вероятностей - проверяйте деление на ноль

    Проверка инференса (включая ONNX)

    Если модель обучалась в другом окружении (например, Python), а исполняется в C++:

  • подготовьте небольшой набор тестовых входов
  • сохраните эталонные выходы (например, из Python)
  • в C++ сравните выходы с допуском
  • Это ловит:

  • перепутанные порядки входов
  • различия в нормализации
  • неверные типы тензоров (например, float против double или int64)
  • Практический чек-лист перед тем как “поверить” метрике

  • метрика соответствует бизнес-ошибке (что хуже: FP или FN, большие промахи или типичные)
  • протокол разбиения соответствует природе данных (время, группы, дисбаланс)
  • препроцессинг обучен только на train (или train-фолде)
  • есть бейзлайн и модель действительно лучше бейзлайна
  • есть тесты на схему признаков и golden tests для инференса
  • метрики и настройки версионируются вместе с моделью
  • Эта дисциплина замыкает цикл курса: вы не просто обучаете модель в C++, вы умеете честно измерять качество, объяснять причины ошибок и не ломать результат при внедрении.

    7. Продакшен: оптимизация, многопоточность, деплой и тестирование

    Продакшен: оптимизация, многопоточность, деплой и тестирование

    Что означает продакшен в контексте ML на C++

    В предыдущих темах курса мы научились:

  • готовить данные и строить воспроизводимые пайплайны fit/transform
  • обучать базовые supervised-модели на Eigen
  • интегрировать нейросети через ONNX Runtime
  • честно оценивать качество метриками и кросс-валидацией
  • В продакшене к этим знаниям добавляются инженерные требования:

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

    Цели оптимизации: что именно измеряем

    Оптимизация без измерений почти всегда приводит к усложнению кода без выигрыша. Для инференса обычно фиксируют:

  • среднюю задержку и задержку на хвостах (например, p95/p99)
  • время препроцессинга отдельно от времени модели
  • память: пиковое потребление и количество аллокаций
  • стабильность результатов: совпадение с эталоном в допустимом допуске
  • Практическое правило: сначала добейтесь корректности и воспроизводимости (схема фичей, тесты, метрики), затем ускоряйте.

    Оптимизация в C++: сборка, профилирование, память

    Режимы сборки и флаги

    Ошибочный сценарий: измерять производительность в Debug.

    Рекомендации:

  • используйте Release для финальных замеров производительности
  • используйте RelWithDebInfo для профилирования, когда нужны символы отладки
  • фиксируйте стандарт языка и флаги оптимизации в CMake
  • Полезная документация:

  • CMake Documentation
  • Профилирование: найти узкое место, а не гадать

    Разные инструменты отвечают на разные вопросы:

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

  • perf для CPU-профилирования и стэков
  • AddressSanitizer и UndefinedBehaviorSanitizer для поиска тяжелых багов, влияющих на стабильность
  • Ссылки:

  • perf: Linux profiling with performance counters
  • Clang Sanitizers
  • Типичные источники замедлений в ML-инференсе

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

  • выделяйте буферы один раз и переиспользуйте
  • храните схему фичей как фиксированный список и проверяйте ее на старте
  • приводите данные к нужному типу один раз, на границе входа
  • Численная стабильность

    В продакшене “плохие” числа встречаются чаще, чем в учебных данных:

  • NaN и inf
  • пустые строки и пропуски
  • экстремальные значения
  • Что делать:

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

    Где реально появляется параллелизм

    В ML-инференсе обычно есть три слоя параллелизма:

  • параллелизм запросов: несколько запросов обрабатываются одновременно
  • параллелизм внутри модели: матричные операции, BLAS, ONNX Runtime, SIMD
  • параллелизм препроцессинга: подготовка фичей, декодирование, токенизация, ресайз изображений
  • Главный риск: вложенная многопоточность, когда каждый запрос использует много потоков внутри ORT/BLAS, а сервис еще и параллелит запросы. Это приводит к конкуренции за CPU и росту p99.

    Практическая стратегия потоков

    Рекомендуемый подход для серверного инференса:

  • На уровне сервиса ограничьте число одновременно обрабатываемых запросов (пул потоков или асинхронная очередь).
  • Внутри библиотеки (BLAS, ONNX Runtime) выставьте контролируемое число потоков.
  • Измерьте latency и хвосты, затем подберите конфигурацию.
  • Для ONNX Runtime важны:

  • SetIntraOpNumThreads: параллелизм внутри операций
  • SetInterOpNumThreads: параллелизм между операциями
  • Документация:

  • ONNX Runtime C/C++ API
  • Потокобезопасность и состояние

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

    Опасные места:

  • общий кэш или общий буфер без синхронизации
  • ленивые инициализации в первом запросе
  • глобальные генераторы случайных чисел
  • Практика:

  • инициализируйте модель и все структуры при старте процесса
  • используйте const-данные для параметров препроцессинга
  • если нужен кэш, продумайте политику и синхронизацию и измерьте влияние на p99
  • Деплой: что именно должно попасть в прод

    Артефакт инференса: не только модель

    Из прошлых тем важнейшая идея: на проде должна жить не “модель”, а артефакт инференса.

    Минимальный состав:

  • model.onnx или сериализованная модель (для классического ML)
  • preprocess.json или бинарный формат: параметры нормализации, словари категорий, правила пропусков
  • schema.json: список признаков и их порядок
  • metadata.json: версия, дата обучения, ожидаемые входные формы, типы, пороги постпроцессинга
  • Практика:

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

    Частая причина “у нас на ноутбуке работает”: разные версии библиотек и разные оптимизации.

    Подходы:

  • контейнеризация (часто Docker) для фиксации окружения
  • фиксация версий зависимостей (vcpkg/Conan, либо закрепленные системные пакеты)
  • сборка на CI тем же способом, что и релиз
  • Релизная стратегия обновления модели

    Чтобы обновления были безопасными, используют:

  • канареечный релиз: маленький процент трафика на новую версию
  • A/B: сравнение двух версий по онлайн-метрикам
  • быстрый откат: возможность вернуть прошлую модель без пересборки всего сервиса
  • Инженерное правило: версия модели должна быть видна в логах и метриках.

    Тестирование: от корректности до устойчивости

    Что тестировать в ML-инференсе на C++

    Пирамида тестов для ML-компонента:

  • unit-тесты:
  • - препроцессинг: нормализация, обработка пропусков, неизвестные категории - постпроцессинг: пороги, argmax, преобразования классов - метрики и утилиты
  • golden tests:
  • - фиксированный вход -> фиксированный выход (в пределах допуска) - проверка совпадения с эталоном из Python для ONNX-модели
  • integration-тесты:
  • - сервис поднимается, грузит артефакт, отвечает на запросы - проверка схемы фичей и версий
  • нагрузочные тесты:
  • - latency p95/p99, QPS, потребление памяти - стабильность под параллельной нагрузкой

    Фреймворк для unit-тестов:

  • GoogleTest
  • Golden tests: самый дешевый способ не сломать инференс

    Golden test обычно хранит:

  • вход (в удобном формате: JSON, бинарный дамп тензора, набор признаков)
  • эталонный выход
  • допустимую погрешность
  • Что он ловит:

  • перепутанный порядок признаков
  • изменения препроцессинга без обновления версии
  • несовпадение типов (float32 против double)
  • неявные изменения порогов
  • Санитайзеры в CI

    Санитайзеры часто ловят ошибки, которые проявляются только под нагрузкой:

  • выход за границы массива
  • use-after-free
  • неопределенное поведение
  • гонки данных
  • Рекомендация:

  • запускайте отдельный CI job с ASan/UBSan
  • если есть многопоточность, периодически прогоняйте сборку с TSan
  • Ссылка:

  • Clang Sanitizers
  • Наблюдаемость: чтобы деградация не стала сюрпризом

    Даже идеально протестированная модель может деградировать после деплоя из-за смещения данных.

    Минимальный набор того, что стоит собирать:

  • версия модели в каждом ответе или в метках метрик
  • latency (с раздельными таймерами: препроцессинг, модель, постпроцессинг)
  • доля ошибок валидации входа
  • базовые статистики по входным признакам после препроцессинга
  • Связь с темой про оценку качества:

  • офлайн-метрики (на test) не заменяют онлайн-мониторинг
  • мониторинг помогает вовремя заметить смещение данных и принять решение о переобучении
  • Практический чек-лист перед релизом

  • артефакт инференса включает модель, препроцессинг, схему и метаданные
  • на старте сервиса есть валидация входов/выходов модели и схемы фичей
  • есть unit-тесты на препроцессинг и постпроцессинг
  • есть golden tests на фиксированных примерах
  • есть базовый нагрузочный прогон и измерение p95/p99
  • настроено логирование версии модели и ключевых ошибок валидации
  • определен план безопасного обновления (канарейка или A/B) и отката
  • Эта дисциплина замыкает курс: вы умеете не только обучить и встроить модель в C++, но и сделать ее быстрой, стабильной, воспроизводимой и безопасно обновляемой.