Привет, Хабр! Меня зовут Юля Корышева, я разработчик машинного обучения в команде скоринга в билайне. В этой статье расскажу, как за последние пять лет в нашей команде менялся подход к разработке, валидации и поддержке моделей — с какими вызовами мы столкнулись, как их решали и к каким результатам пришли.

Вызовы в процессе масштабирования

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

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

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

  • Высокие показатели time-to-market

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

Наша команда — не исключение. Несколько лет назад мы поняли, что мы не можем обрабатывать тот поток клиентских кейсов, который к нам приходит. Например, за 2024 год мы обработали около 380 клиентских запросов: от построения новых моделей до расчёта скоров по уже существующим. Из них около 70 моделей были построены с нуля.

  • Количество построенных моделей в год кратно превышает количество человек в команде

Когда мы поняли, что нужно автоматизировать некоторые части пайплайна по построению моделей и таким образом можно заметно увеличить их количество, возник вопрос: как всё это поддерживать и внедрять в продакшн? Не менее важно было понять, как мониторить качество всех этих моделей.

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

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

Каждая модель, которую мы строим внутри команды, проходит одинаковую последовательность шагов, перед продакшном:

  1. Сбор данных и таргета.

  2. Обучение и валидация модели.

  3. Логирование и хранение экспериментов — артефакты, метрики, окружения.

  4. Внедрение модели.

  5. Мониторинг и поддержка модели в проде.

Рассмотрим каждый этап: какие варианты оптимизации мы рассматривали, какие ошибки допускали и какие изменения помогли нам сократить time-to-market и повысить стабильность моделей в продакшене.

1. Сбор данных для построения моделей

В нашей команде построением витрин данных занимаются аналитики и дата-инженеры. Но любая задача ML-специалиста начинается со сбора нужных признаков под конкретную задачу из разных витрин и их объединения по определённым правилам.

Вариант 1. Каждый пишет свои скрипты для своих задач

Самый простой способ — брать подзадачу «сбор выборки» до начала построения модели и каждому писать скрипты под свои нужды. Такой подход вполне рабочий, и мы им пользовались какое-то время, но он имеет ряд минусов:

  • Много ручной работы. Своими скриптами ещё можно пользоваться повторно, но лезть в чужие простыни кода, чтобы понять, годится ли оно для новой задачи — удовольствие так себе :)

  • Риск ошибок — неконсистентные джойны, дата-лики, невоспроизводимость в проде.

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

Вариант 2. Внедрить полноценный open-source Feature Store

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

Мы рассматривали и этот путь, но у нашей команды уже было:

  • 5–7 тысяч фичей с рассчитанной историей;

  • кастомный мониторинг под наши задачи;

  • документация по фичам и витринам.

Полный переход на open-source FS выглядел для нас неоправданно дорогим и трудоемким: нужно было бы перенести всю историю фичей, интегрировать кастомный мониторинг и адаптировать внутренние пайплайны, что с учётом объёма и специфики наших данных было бы сложно.

Вариант 3. Промежуточный путь — свой инструмент

Мы выбрали компромисс: развить внутренний мини-FS на базе нашего DWH.

  1. Сначала мы пытались решить проблему силами только ML-инженеров. Выделили типовые сценарии сбора фичей, зафиксировали правила объединения витрин и написали небольшую библиотеку на PySpark. Сбор выборки свелся не к написанию скриптов, а к заполнению конфига под конкретную задачу. Количество ручной работы снизилось, процесс стал воспроизводимым, а задача практически перешла в фоновый режим.

  2. Затем мы подключили дата-инженеров, чтобы системно решить проблему. Они не просто переписали скрипты на Scala и Spark, а фактически переработали весь процесс выгрузки данных из Hadoop и разработали полноценное веб-приложение. Теперь в UI можно выбрать нужные витрины, модели или признаки и запустить процесс в один клик, удобно следить за прогрессом и перезапускать задачи при ошибках.

Мы храним историю по тысячам фичей за несколько лет, поэтому любая выгрузка — это тяжёлая операция. Благодаря оптимизации джойнов разных партиций и витрин время выгрузки сократилось в несколько раз, а нагрузка на кластер заметно снизилась. Например, в одном из недавних кейсов нужно было выгрузить данные для 100 миллионов объектов и более 5000 фичей, разбросанных по ~10 витринам. Каждая из этих витрин содержит сотни партиций, но для задачи требовались данные из нескольких десятков из них. Новый сервис справился с этой задачей за пару дней, тогда как прежний подход занял бы не меньше месяца.

Конечно, такой инструмент требует поддержки и развития силами нашей команды, но этот trade-off для нас полностью оправдан. 

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

2. Обучение моделей

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

Вариант 1. Индивидуальные наработки 

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

Плюсы такого подхода очевидны:

  • Полная свобода экспериментов.

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

Но минусов оказалось больше. Рабочие решения часто оставались «замкнутыми» внутри ноутбуков отдельных специалистов, разные окружения и реализации мешали воспроизводимости, а внедрение в прод затягивалось. Если ML-инженер увольнялся, большая часть его наработок терялась. А поддерживать его модели другим было крайне сложно из-за отсутствия стандартизации.

Вариант 2. Внутренняя библиотека

Несмотря на то, что методология построения моделей у всех была разная, мы поняли, что некоторые шаги у всех повторяются: подготовка отчётов, базовая обработка данных, попытки ускорить обучение. Различие заключалось лишь в реализации: кто-то использовал GPU, кто-то мультипроцессинг, кто-то просто запараллелил эксперименты в ноутбуках.

Осознание этого подтолкнуло нас к идее создать внутреннюю библиотеку. Первоначально были сомнения: не приведет ли это к накоплению легаси, усложнению онбординга или превращению процесса в коробочное решение, где исчезнет ресерч и останется только рутинная работа? Чтобы минимизировать риски, мы сделали несколько шагов:

  • Объединили ключевые наработки в единую библиотеку и покрыли их тестами.

  • Внедрили версионирование через PDM (Python Dependency Manager) и публикацию во внутренний PyPI через CI/CD.

  • Подготовили документацию на Sphinx для упрощения онбординга.

  • Добавили поддержку разных бэкендов для ускорения обучения — Dask, Dask-ML, cuML, cuDF, Dask-cuDF. Подробнее о том, как мы это реализовали, можно почитать в этой статье.

  • Разработали сервис визуализации для удобного обмена результатами.


Как устроен CI/CD-пайплайн — процесс интеграции и доставки обновленного ПО

Публикация библиотеки полностью автоматизирована — при пуше новой версии в master автоматически запускаются тесты, сборка и публикация пакета во внутренний PyPI-репозиторий.

stages:
  - test
  - publish

test:
  stage: test
  script:
    - pytest tests/unittests
  when: on_success

publish:
  stage: publish
  script:
    - pdm publish --repository internal-pypi --username $PYPI_USER --password $PYPI_PASS
  only:
    - main

Это гарантирует, что все релизы пройдут проверки и станут доступны другим проектам внутри компании.

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

Вариант 3. AutoML под наши задачи

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

  1. Классические метрики качества, зависящие от задачи: Gini, AUC-ROC, F-мера, Precision–Recall и др.

  2. Эти же метрики в динамике — чтобы контролировать дрифт качества во времени.

  3. Метрики стабильности предсказаний на train, OOT и других отложенных выборках: PSI, средние и медианные значения, расстояние Кульбака–Лейблера между месяцами.

  4. Калибровочные кривые.

  5. Возможность строить модели с разным уровнем «переобученности» и фиксировать его на этапе обучения.

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

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

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

3. Хранение экспериментов

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

Вариант 1. Jira

Долгое время мы сохраняли результаты обучения прямо в Jira: отчёты, pickle-файлы с моделями, финальные скоры. Это было удобно — всё в одном месте, не требовалась отдельная инфраструктура и было достаточно легко находить модель по номеру задачи. Но с ростом количества моделей такая схема перестала работать:

  • Одного pickle и отчёта уже не хватало — требовались окружения, скрипты обработки данных и инференса.

  • Неудобно было хранить разные версии одной модели.

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

Вариант 2. MLflow и Open Meta Data

Когда мы пришли к внутренней библиотеке для обучения моделей, стало проще подключить внешние инструменты для хранения артефактов. Мы выбрали MLflow для экспериментов и Open Meta Data (OMD) для сохранения паспорта модели, а также отслеживания связей между моделями и признаками. 

2.1. Хранение артефактов в MLflow

Как и многие, мы сохраняем в MLflow метрики и параметры моделей. Дополнительно мы храним там:

  • отчеты по моделям,

  • информацию о нужном окружении,

  • кастомный инференс и результирующие предсказания модели.

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

2.2. Связка модель → фича в платформе для управления метаданными OpenMetaData

Так как мы работаем с банками и МФО, стабильность моделей во времени критически важна. Если в проде видно, что распределение какой-то модели изменилось, первым делом мы проверяем признаки, которые в ней используются. Конечно, причина может быть внешней: например, изменение клиентского потока. Но нам важно минимизировать влияние факторов на нашей стороне.

Когда моделей становится десятки, без отдельного инструмента для анализа связей «модель ↔ фича» работать становится сложно. Мы используем OpenMetaData, который помогает:

  • Быстро находить из-за каких признаков модель может дрифтовать.

  • Видеть, в каких других моделях встречается проблемная фича.

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

Сейчас при построении моделей мы сразу пушим отчёты в OpenMetaData, чтобы хранить связки «модель ↔ признак». 

Подробнее о том, как мы это реализовали, можно почитать в статье.

from metadata.generated.schema.entity.data.table import Table
from metadata.generated.schema.entity.data.mlmodel import CreateMlModelRequest, MlFeature

def register_model(metadata, model_name, algorithm, sources):
     “”” Register an ML model and its features in OpenMetaData.”””
    features = []
    for source, cols in sources.items():
        table = metadata.get_by_name(entity=Table, fqn=f"HIVE.default.{source}")
        for c in cols:
            features.append(MlFeature(name=c, dataType="numerical"))

    model = CreateMlModelRequest(
        name=model_name,
        algorithm=algorithm,
        mlFeatures=features,
        service="ml-models"
    )
    metadata.create_or_update(model)

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

4. Внедрение моделей в продакшен

Для нашей команды внедрение моделей в продакшен не стало «узким горлышком» — мы выкатываем не все модели, которые обучаем. Однако, возникла другая важная задача: быстро и воспроизводимо проводить ретротесты уже построенных моделей, если хотим предложить их клиенту.

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

Вариант 1. Ручная поддержка построенных моделей

Базовый процесс выглядел так:

  1. Сбор фичей для выборки.

  2. Подготовка окружения под конкретную модель — со всеми рисками несовместимости версий Python и библиотек.

  3. Ручной запуск инференса нужных моделей через ноутбуки и вспомогательные скрипты.

После появления инфраструктурных решений ситуация упростилась: мы стали сохранять в MLflow зависимости (requirements.txt), сериализованные модели, отчёты, метрики и результаты инференса. Дополнительно добавили кастомную библиотеку с версионированием и CI/CD для автоматизации публикаций. Это позволило воспроизводить эксперименты и откатываться к нужной версии без лишних усилий.

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

Вариант 2. Расчет скоров во внутреннем хранилище данных/ онлайн-скоринг через FastAPI

Альтернативный подход — внедрять в продакшен все построенные модели. В этом случае ретротесты упрощаются: модель уже доступна через API или в DWH, и можно легко подтянуть её предсказания для нужной выборки.

Мы используем этот вариант для моделей, которые активно применяются клиентами. Он хорошо работает, пока моделей немного (до пары десятков): удобно тестировать на разных выборках, проводить A/B-тесты, отслеживать метрики в онлайне.

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

В итоге мы пришли к выводу, что стратегия «каждую модель — в прод» не масштабируется в нашем случае: слишком много моделей и слишком высокие издержки на поддержку.

Вариант 3. Seldon Core / KFServing

Хранить модели в MLflow удобно, но этого недостаточно, чтобы сделать их доступными в продакшене. Нужен способ превратить модель в сервис, к которому можно обращаться по API.

Для этого мы используем Seldon Core — платформу, которая разворачивает модели в Kubernetes в виде REST/gRPC сервисов. По сути, она берёт Python-обертку вокруг модели и превращает её в production-endpoint. В MLflow модель реализуется как класс с двумя ключевыми методами —  load_context (загрузка артефактов) и predict (логика инференса). Например:

import mlflow.pyfunc
import pandas as pd
import joblib
import json
import logging

class ModelWrapper(mlflow.pyfunc.PythonModel):
    def __init__(self, artifacts: dict = None):
        self.artifacts = artifacts or {}
        self.model = None

    def load_context(self, context):
        """Load model artifacts when the service starts."""
        logging.info("Loading model artifacts...")
        model_path = self.artifacts.get("model")
        if model_path:
            self.model = joblib.load(model_path)
        logging.info("Model loaded successfully.")

    def predict(self, context, model_input):
        """Perform inference on incoming requests."""
        logging.info("Starting prediction.")
        input_data = json.loads(model_input[0][0])
        df = pd.DataFrame(input_data["data"])
        preds = self.model.predict(df)
        return pd.DataFrame({"prediction": preds})

Дальше модель можно задеплоить одной кнопкой через CI/CD. В  .gitlab-ci.yml мы добавили шаг  deploy, который автоматически создает сервис:

deploy:
  stage: deploy
  script:
    - kubectl apply -f seldon/model.yaml -n seldon
    - kubectl get seldondeployment -n seldon
  when: manual

При необходимости модель  можно также остановить через команду kubectl delete. Такой подход позволил нам масштабировать работу с десятками моделей без роста инфраструктурных затрат. Для каждой модели пайплайн развертывания включает:

  1. автоматическую упаковку модели в Docker-образ;

  2. запуск в Kubernetes-поде;

  3. создание deployment, service и ingress;

  4. предоставление REST-API для инференса.

5. Мониторинг моделей

Как упоминалось выше, ключевое требование для многих клиентов — стабильность моделей во времени. Для этого нужно иметь инструменты для мониторинга десятков моделей и тысяч признаков. Мы используем различные метрики для оценки стабильности, в том числе PSI (population stability index — метрика, показывающая, насколько распределение признаков или предсказаний изменилось со временем), расстояние Кульбака–Лейблера, дивергенцию Йенсена–Шеннона и другие.

Основная сложность мониторинга связана с масштабом: поддерживать 3 модели в продакшене значительно проще, чем 50–100. Поэтому довольно быстро стало понятно, что ручных подходов недостаточно и требуется автоматизация.

Вариант 1. Эвристики и ручная оценка признаков 

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

Вариант 2. Подключение внешних сервисов (например, Grafana) 

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

Вариант 3. Автоматическая оценка стабильности и Streamlit 

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

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

  1. Расчет статистик на кластере.

  2. Автоматическая оценка временных рядов с помощью алгоритмов, например piece-wise regression.

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

  4. Визуализация через сервис на Streamlit (фреймворк для быстрой сборки внутренних веб-интерфейсов на Python).

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

Что дальше

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

В ближайшее время мы планируем активно развивать внутренний AutoML. За годы работы мы накопили множество решений по построению моделей под разные задачи — теперь хотим сделать этот процесс полностью автоматизированным. Кроме того, рассматриваем частичный переход на core Seldon именно в продакшене: это позволит унифицировать деплой и заметно упростит поддержку моделей.

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

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

Комментарии (0)