Пример ML проекта с Pipelines+Optuna+GBDT

от автора

Введение (с чего всё началось)

Началось всё с того, что я открыл для себя Kaggle. В частности, я принимаю участие в публичном соревновании Spaceship Titanic. Это более «молодая» версия классического Титаника. На момент написания этой статьи в соревновании принимают участие 2737 человек (команд). Код, продемонстированный в этой статье, позволил мне занять 697-е место в публичном рейтинге со второго сабмита. Я знаю, что он не идеален и работаю над этим.

Данные

Тренировочный датасет доступен по ссылке. Для того, чтобы его скачать, нужно стать участником соревнования. Кроме тренировочного датасета доступен тестовый датасет. По понятным причинам в нём отсутствует колонка с таргетом. Также присутствует пример выгрузки для сабмита (sample submission).

Анализ данных и подготовка признаков

Для анализа данных я использую Pandas Profiling и SweetWiz. Это очень мощные библиотеки, экономят массу времени.

Пример формирования отчёта с помощью Pandas Profiling

profile_train = ProfileReport(train_data, title="Train Data Profiling Report") profile_train.to_file("train_profile.html") profile_train.to_widgets()

Пример формирования отчёта с помощью SweetWiz

train_report = sv.analyze(train_data)  train_report.show_html(filepath='TRAIN_REPORT.html',              open_browser=True,              layout='widescreen',              scale=None)  train_report.show_notebook( w=None,                  h=None,                  scale=None,                 layout='widescreen',                 filepath=None)

Тренировочные данные сбалансированы по таргету, без дубликатов, присутствует определённое количество нулевых значений (я их не удаляю), также наблюдается корелляция по числовым признакам.

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

# Вытаскиваю номер палубы из номера каюты def get_deck(cabin):     if cabin is None:         return None     if isinstance(cabin, float):         return None     return cabin.split("/")[0]  #print(get_deck('F/1534/S')) #print(get_deck("G/1126/P"))  train_data['deck'] = train_data.apply(lambda x: get_deck(x.Cabin), axis=1) test_data['deck'] = test_data.apply(lambda x: get_deck(x.Cabin), axis=1)  # Вытаскиваю отдельно параметр side из номера кабины def get_side(cabin):     if cabin is None:         return None     if isinstance(cabin, float):         return None     return cabin.split("/")[2]  #print(get_side('F/1534/S')) #print(get_side('G/1126/P'))  train_data['side'] = train_data.apply(lambda x: get_side(x.Cabin), axis=1) test_data['side'] = test_data.apply(lambda x: get_side(x.Cabin), axis=1)

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

num_cols = ['Age','RoomService', 'FoodCourt','ShoppingMall', 'Spa', 'VRDeck'] cat_cols = ['HomePlanet', 'CryoSleep', 'Destination', 'VIP', 'deck', 'side']

Настройка pipelines

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

Я создал отдельно Pipeline для числовых признаков и Pipeline для категориальных признаков

num_pipeline = Pipeline(steps=[     ('impute', SimpleImputer(strategy='median')),     ('scale',StandardScaler()) ]) cat_pipeline = Pipeline(steps=[     ('impute', SimpleImputer(strategy='most_frequent')),     ('one-hot',OneHotEncoder(handle_unknown='ignore', sparse=False)) ])

Для числовых признаков использовал SimpleImputer, заполняющий пропуски медианными значениями, для категориальных признаков пропуски заполняются наиболее часто втречающимися значениями. Кроме того, применил StandardScaler и OneHotEncoder. Кстати, в официальной документации scikit-learn есть прекрасная статья, в которой предоставлен сравнительный анализ многих скейлеров.

На основе полученных пайплайнов собираю ColumnTransformer.

col_trans = ColumnTransformer(transformers=[     ('num_pipeline',num_pipeline,num_cols),     ('cat_pipeline',cat_pipeline,cat_cols)     ],     remainder='drop',     n_jobs=-1)

Настройка Pipelines и Optuna

Optuna — это очень мощный фреймворк, позволяющий автоматизировать подбор гиперпараметров для модели в процессе машинного обучения. Для того, чтобы нормально настроить и начать использовать в проекте, нужно потратить определённое время. Благо, на официальном сайте разработчиков есть обширная документация и видео, облегчающие процесс обучения. Ниже код, который я использую в своём проекте.

def objective(trial):         # Список гиперпараметров для перебора (для CatBoostClassifier)     param = {         "objective": trial.suggest_categorical("objective", ["Logloss", "CrossEntropy"]),         "colsample_bylevel": trial.suggest_float("colsample_bylevel", 0.01, 0.1),         "depth": trial.suggest_int("depth", 1, 12),         "boosting_type": trial.suggest_categorical("boosting_type", ["Ordered", "Plain"]),         "bootstrap_type": trial.suggest_categorical(             "bootstrap_type", ["Bayesian", "Bernoulli", "MVS"]         ),         "used_ram_limit": "3gb",     }      if param["bootstrap_type"] == "Bayesian":         param["bagging_temperature"] = trial.suggest_float("bagging_temperature", 0, 10)     elif param["bootstrap_type"] == "Bernoulli":         param["subsample"] = trial.suggest_float("subsample", 0.1, 1)          # Определяю модель машинного обучения, которой передаются гиперпараметры     estimator = CatBoostClassifier(**param, verbose=False)      # Прикручиваю пайплайны     clf_pipeline = Pipeline(steps=[             ('col_trans', col_trans),             ('model', estimator)         ])     # Код для вычисления метрики качества.     # В этом проекте я вычисляю Accuracy методом кросс-валидации     accuracy = cross_val_score(clf_pipeline, features_train, target_train, cv=3, scoring= 'accuracy').mean()     return accuracy  # Инициализирую подбора гиперпараметров. # Можно сохранять все промежуточные результаты в БД SQLLite (этот код я закомментировал) #study = optuna.create_study(direction="maximize", study_name="CBC-2023-01-14-14-30", storage='sqlite:///db/CBC-2023-01-14-14-30.db') study = optuna.create_study(direction="maximize", study_name="CBC-2023-01-14-14-30") # Запускаю процесс подбора гиперпараметров study.optimize(objective, n_trials=300) # Вывожу на экран лучший результат print(study.best_trial)

Несколько слов о градиентном бустинге

В переменную estimator можно сохранять любую ML модель. Я экспериментировал с DecisionTreeClassifier, RandomForestClassifier, LogisticRegression, но более-менее существенных результатов в соревнованиях смог добиться после того, как начал использовать модели градиентного бустинга. Разобраться в материале мне очень помогла вот эта статья. Я экспериментировал с LGBMClassifier, XGBClassifier, CatBoostClassifier. В прилагаемом примере использован CatBoostClassifier.

Заключение

Код, который у меня получился на текущий момент, доступен на GitHub. Он не идеален. Я продолжаю его совершенствовать (также как и свои навыки). Например,

  • он довольно медленный. Процесс перебора может длиться от 2 до 5 часов;

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


ссылка на оригинал статьи https://habr.com/ru/post/712080/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *