Что такое дрифт ML-моделей и как с ним бороться - Академия Selectel

Что такое дрифт ML-моделей и как с ним бороться

Дмитрий Домлоджанов Дмитрий Домлоджанов Технический писатель 1 июня 2024

Даже при наличии качественных входных данных качество предсказаний ML-модели со временем ухудшается. На примере MLOps-платформы Neoflex Dognauts рассказываем, как мониторинг помогает обнаружить изменения вовремя.

Изображение записи

Меня зовут Анастасия Коткова, я ML-инженер в Neoflex. В этой статье поделюсь конспектом своего доклада и расскажу, как команда Neoflex справляется с дрифтом ML-моделей с помощью платформы Neoflex Dognauts.

Если вам интересна тема статьи, присоединяйтесь к сообществу «MLечный путь» в Telegram. Там мы вместе обсуждаем проблемы и лучшие практики организации production ML-сервисов, а также делимся собственным опытом. А еще там раз в неделю выходят дайджесты по DataOps и MLOps.

Почему важен мониторинг дрифта

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

  • Дрифт данных — когда качество предсказания ML-модели изменяется под влиянием статистических свойств входных данных. Объясню на примере. Если распределение возраста и доходов покупателей существенно меняется с течением времени, ML-модель больше не сможет точно предсказывать вероятность покупки.
  • Дрифт концепции — когда изменение статистических свойств целевой переменной вызвано изменением концепции того, что мы пытаемся предсказать. Как правило, за это отвечают некоторые глобальные изменения, и примером таких изменений может быть COVID-19.
Ухудшение качества предсказаний при дрифте модели (слева) и предотвращение дрифта при регулярном обновлении модели (справа). Слева видим угасающий график, а справа — колеблющийся на в пределах допустимых значений.

Ухудшение качества предсказаний при дрифте модели (слева) и предотвращение дрифта при регулярном обновлении модели (справа).

Задачи мониторинга

Задачи мониторинга: Ingest → Drift Detect → Publish Metrics → Trigger Retraining.
  • Сбор данных, а в случае дрифта концепций еще и объединение с реальными таргетами.
  • Детекция дрифта. Здесь происходит расчет метрик входных данных и предсказания ML-моделей. Data Science-специалист задает для них пороговые значения при разработке модели.
  • Сохранение (логирование) рассчитанных метрик для дальнейшего анализа и отчетности.
  • При превышении пороговых значений — оповещение ML-инженеров или запуск процесса автоматического переобучения модели.

Методы определения дрифта ML-моделей

Если мы говорим о дрифте данных, то каждый столбец рассматриваем по отдельности. В зависимости от его типа — применяем одну из статистических метрик либо алгоритм поиска дрифта. Для числовых полей это будет алгоритм Колмагорова-Смирнова или расстояние Вассерштейна, а для категориальных — Chi-square, Jensen-Shannon или Z-score.

Если мы говорим о дрифте концепций, то здесь иная ситуация. Мы можем либо использовать PSI (Population Stability Index), либо мониторить основные метрики качества работы модели, к примеру, accuracy, f1_score и другие, сравнивая их с пороговыми или эталонными зачениями.

Опишу последовательность действий при поиске дрифта концепций. 

  1. При обучении модели рассчитываем метрики модели на валидационном датасете.
  2. Определяем пороговые значения для метрик на основе предыдущего пункта.
  3. Рассчитываем метрики модели на последних данных.
  4. Сравниваем рассчитанные метрики с пороговыми значениями.
  5. Фиксируем факты нарушений пороговых значений.

Для расчета метрик вызываем метод mlflow.evaluate, передавая в качестве параметров последние данные и пороговые значения. Полученные результаты анализируем в MLflow UI.

Open source-библиотеки для определения дрифта данных

Data Drift Detector

Эту библиотеку можно назвать наиболее простой из подборки. Data Drift Detector считает основные метрики дрифта и обладает минимальным количеством настроек, но не дает заключения о наличии дрифта в столбцах и датасетах. Библиотека идеально подойдет для домашнего использования или pet-проектов.

Evidently AI

С помощью Evidently AI можно получать как визуализированные отчеты, так и JSON-файлы для дальнейшей обработки. Есть интеграция с Airflow, MLflow, Grafana. На больших датасетах процесс может быть долгим из-за ограничений Pandas, так что для работы с ними советую рассмотреть следующую библиотеку.

Демонстрация интерфейса Evidently AI.

Whylogs

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

Демонстрация интерфейса Whylogs.

Alibi Detect

На мой взгляд, Alibi Detect — это лучшее решение для онлайн-мониторинга дрифта. Среди преимуществ библиотеки можно отметить легкое развертывание модели в Seldon и большой арсенал алгоритмов Drift Detection для режимов онлайн и офлайн.

Архитектура решений 

Расскажу вкратце о нашей базовой модели. Она выдает бинарный ответ на вопрос о том, стоит ли выдавать клиенту кредит. Как мы видим, accuracy на валидационном датасете составляет 88%:

accuracy_score — 0.887

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


    {
  "type": "feast_dataset",
  "fs_path": ".",
  "dataset_path": "loan_approval_baseline",
  "categorical_features": [
    "person_home_ownership",
    "loan_internet",
    "state"
  ],
  "cols_to_drop": [
  "created _timestamp",
  "event_timestamp",
  "loan_id",
  "zipcode",
  "dob_ssn",
  "city",
  "location_type"
  ]
}

Онлайн-детектирование

Для онлайн-детектирования дрифта мы используем уже упомянутую Alibi Detect, так как она обладает большой совместимостью. Вдобавок наша модель деплоится в Seldon.

При работе с онлайн-дрифтом сценарий работы состоит из нескольких действий. 

  1. Определяем пороговые значения для метрик дрифта нашего алгоритма.
  2. Инициализируем и обучаем алгоритм.
  3. Разворачиваем модель как REST-сервис.
  4. Логируем входы и выходы модели.
  5. Полученные данные прогоняем через детектор.
  6. Рассылаем оповещение при наличии дрифтов. 
Сценарий при работе с онлайн-дрифтом.

Оффлайн-детектирование

Алгоритм действий для оффлайн-детектирования дрифта. 

  1. Определяем пороговые значения для метрик дрифта нашего алгоритма.
  2. Инициализируем и обучаем алгоритм.
  3. Применяем алгоритм поиска дрифта до последних данных.
  4. Сохраняем результаты.
Сценарий при работе с оффлайн-дрифтом.

Все пороговые значения наших метрик описываем в thresholds.json:


    {
  "data_drift": [
    {
      "metric_name": "min_chi_square",
      "threshold": 0.05,
      "higher_is_better": true
    },
    {      
      "metric_name": "mean_chi_square",
      "threshold": 0.05,
      "higher_is_better": true
    },
    {
      "metric_name": "min_ks_test",
      "threshold": 0.05,
      "higher_is_better": true
    },
    {
      "metric_name": "mean_ks_test",
      "threshold": 0.05,
      "higher_is_better": true
    }
  ],
  "concept_drift": [
    {
      "metric_name": "accuracy_score",
      "threshold": 0.8,
      "higher_is_better": true
    }
  ]
} 

Замечу, что мы ограничиваем минимальные и средние значения для Chi-square и теста Колмагорова-Смирнова на значении 0,05, что является общей практикой. 

Далее, чтобы применить наш метод, мы переписали функцию evaluate под собственные нужды.


    class BasicDriftEvaluator(ModelEvaluator):
  def_evaluate(self, categorical_columns=None):
      cd = TabularDrift(self.df_prior-values, p_val=drift_threshold, categories_per_feature=categories_per_feature)

      preds = cd.predict(self.df_post.values)
      labels = ['No!', 'Yes!']

      result = {}

      chi_arr = []
      ks_arr = []

      for f in range(cd.n_features):
        stat = 'Chi2' if f in list(categories_per_feature.keys()) else 'K-S'
        chi_arr.append(preds['data']['p_val'][f]) if f in list(categories_per_feature.keys()) else ks_arr.append(preds['data']['p_val'][f])
        fname = feature_names[f]
        stat_val, p_val = preds['data']['distance'][f], preds['data']['p_val'][f]

        col_res = {
          'Drifted': 1 if p_val < drift_ threshold else 0, 
          stat: round(stat_val.item(), 3),
          'p_value': round (p_val.item(), 3),
        }
        result.update({
          fname: dict(col_res),
        })

      json_name = 'drift_result'

    {
  "type": "feast_dataset",
  "fs_path": ".",
  "dataset_path": "loan_approval_baseline",
  "categorical_features": [
    "person_home_ownership",
    "loan_intent",
    "state"
  ],
  "cols_to_drop": [
    "created_timestamp",
    "event_timestamp",
    "loan_id",
    "zipcode",
    "dob_ssn",
    "city",
    "location_type"
  ]
}

Под капотом функция использует алгоритм Tabular Drift из библиотеки Alibi Detect. Из config.json мы собираем данные, с которыми будем сравнивать входящие. Далее — определяем категориальные переменные и удаляем колонки, которые не будут участвовать в детекции дрифта. Упомянутый evaluator возвращает отчет о каждом столбце и наличии дрифта.

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

DAGs в Airflow.

    DATASET _NAME = Variable.get("data_drift_dataset")

with DAG(
  dag_id = "data _drift_detect",
  default_args=default_args,
  # schedule_interval='0 0 * * *',
  schedule_interval='@once', 
  dagrun_timeout=timedelta(minutes=60),
  description='detect model data drift',
  start_date = airflow.utils.dates.days_ago(1),
  catchup=False
) as dag:

В настройках evaluator мы указываем кастомный evaluator и прописываем пороговые значения. Так мы получаем отчеты о валидации.


    try:
  result = mlflow.evaluate(
    baseline_uri, 
    data=eval_data, 
    targets=target, 
    model_type="classifier", 
    evaluators=["basic_drift_evaluator"], 
    validation_thresholds=thresholds,
  )
except ModelValidationFailedException as e:
  with open("evaluate.log", "w") as text_file:
    text_file.write(str(e))

  mlflow. log_artifact("evaluate.log", artifact_path="logs")

run_id = current_run.info.run_id
print(run_id)

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

Результаты для первого месяца: Drift in dataset — 0, mean_chi_square — 0.367, mean_ks_test — 0.745, min_chi_square — 0.093, min_ks_test — 0.245.

Показатели за первый месяц.

Со вторым месяцем ситуация обратная. Дрифт был обнаружен, а минимальные значения значительно ниже пороговых:

Результаты для первого месяца: Drift in dataset — 1, mean_chi_square — 0.242, mean_ks_test — 0.42, min_chi_square — 0.001, min_ks_test — 0

Показатели за второй месяц.

В артефактах MLflow видим сообщения о том, что метрики не прошли валидацию (сравнение с пороговыми значениями):

Сообщения о том, что метрики не прошли валидацию (вкладка Artifacts).

Здесь же можем ознакомиться с отчетом по каждому столбцу:

Отчет по каждому столбцу во вкладке Artifacts.

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

Дрифт концепций

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

  1. Соединяем данные с реальными таргетами.
  2. Определяем пороговые значения для метрик.
  3. Сравниваем метрики между собой.
  4. Сохраняем результаты оценки.

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

Так как на валидационном датасете мы получили 88% качества, поставим ограничение для метрики accuracy в 80%:


    {
  "data_drift": [
    {
      "metric_name": "min_chi_square",
      "threshold": 0.05,
      "higher_is_better": true
    },
    {
      "metric_name": "mean_chi_square",
      "threshold": 0.05,
      "higher_is_better": true
    },
    {
      "metric_name": "min_ks_test",
      "threshold": 0.05,
      "higher_is_better": true
    },
    {
      "metric_name": "mean_ks_test",
      "threshold": 0.05,
      "higher_is_better": true
    }
  ],
  "concept_drift": [
    {
      "metric_name": "accuracy_score",
      "threshold": 0.8,
      "higher_is_better": true
    }
  ]
}

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

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

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

За четвертый квартал значительно упало качество работы модели, а за третий все показатели в норме. Отчет о валидации, которую модель не прошла, доступен в артефактах MLflow. Таким образом мы можем мониторить дрифт концепций:

Мониторинг дрифта концепций.

Заключение 

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

В случае с нашей платформой, причины выбора инструментов вполне прозаичны. MLflow уже стоял на платформе и подходил по функциональности, что сэкономило время на поиск подходящего фреймворка. Для онлайн-мониторинга дрифта мы используем библиотеку Alibi Detect, так как она хорошо работает с Seldon.

С демонстрацией работы модели от Neoflex и другими докладами на тему ML вы можете ознакомиться на YouTube: