Подбор гиперпараметров ML-модели с помощью HYPEROPT

    В машинном обучении гиперпараметрами называют параметры модели, значения которых устанавливаются перед запуском процесса её обучения. Ими могут быть, как параметры самого алгоритма, например, глубина дерева в random forest, число соседей в knn, веса нейронов в нейронный сетях, так и способы обработки признаков, пропусков и т.д. Они используются для управления процессом обучения, поэтому подбор оптимальных гиперпараметров – очень важный этап в построении ML-моделей, позволяющий повысить точность, а также бороться с переобучением. На сегодняшний день существуют несколько популярных подходов к решению задачи подбора, например:

    1. Поиск по решётке. В этом способе значения гиперпараметров задаются вручную, затем выполняется их полный перебор. Популярной реализацией этого метода является Grid Search из sklearn. Несмотря на свою простоту этот метод имеет и серьёзные недостатки:

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

    Часто в целях экономии времени приходится укрупнять шаг перебора, что может привести к тому, что оптимальное значение параметра не будет найдено. Например, если задан диапазон значений от 100 до 1000 с шагом 100 (примером такого параметра может быть количество деревьев в случайном лесе, или градиентном бустинге), а оптимум находится около 550, то GridSearch его не найдёт.

    2. Случайный поиск. Здесь параметры берутся случайным образом из выборки с указанным распределением. В sklearn он этот метод реализован как Randomized Search. В большинстве случаев он быстрее GridSearch, к тому же значения параметров не ограничены сеткой. Однако, даже это не всегда позволяет найти оптимум и не защищает от перебора заведомо неудачных комбинаций.

    3. Байесовская оптимизация. Здесь значения гиперпараметров в текущей итерации выбираются с учётом результатов на предыдущем шаге. Основная идея алгоритма заключается в следующем – на каждой итерации подбора находится компромисс между исследованием регионов с самыми удачными из найденных комбинаций гиперпараметров и исследованием регионов с большой неопределённостью (где могут находиться ещё более удачные комбинации). Это позволяет во многих случаях найти лучшие значения параметров модели за меньшее количество времени.

    В этой статье приведён обзор hyperopt – популярной python-библиотеки для подбора гиперпарметров. В ней реализовано 3 алгоритма оптимизации: классический Random Search, метод байесовской оптимизации Tree of Parzen Estimators (TPE), и Simulated Annealing – метод имитации отжига. Hyperopt может работать с разными типами гиперпараметров –непрерывными, дискретными, категориальными и т.д, что является важным преимуществом этой библиотеки.

    Установить hyperopt очень просто:

    pip install hyperopt

    Оценим работу этой библиотеки в реальной задаче – предсказать, зарабатывает ли человек больше $50 тыс. Загрузим необходимые библиотеки, и подготовим данные на вход модели:

    from functools import partial
    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    %matplotlib inline
    
    import seaborn as sns
    from sklearn.model_selection import cross_val_score, StratifiedKFold
    from sklearn.linear_model import LogisticRegression
    from sklearn.preprocessing import StandardScaler, OneHotEncoder
    from sklearn.pipeline import Pipeline
    from sklearn.impute import SimpleImputer
    from sklearn.compose import ColumnTransformer
    from hyperopt import hp, fmin, tpe, Trials, STATUS_OK
    
    # загружаем данные
    df = pd.read_csv('adult.data.csv')
    
    # удаляем дубликаты
    df.drop_duplicates(inplace=True, ignore_index=True)
    
    # готовим признаки и целевую переменную 
    X = df.drop(labels=['salary', 'native-country'], axis=1).copy()
    y = df['salary'].map({'<=50K':0,'>50K':1}).values

    В данных есть признаки разных типов, которые, соответственно, требуют и разной обработки. Для этого воспользуемся методом ColumnTransformer из библиотеки sklearn, который позволяет задать свой способ обработки для каждой группы признаков. Для категориальных признаков (тип object) будем использовать методы SimpleImputer (заменяет пропуски, которые обозначены символом «?») и OneHotEncoder (выполняет dummy-кодирование). Числовые признаки (остальные типы) будем масштабировать с помощью StandardScaler. В качестве модели выберем логистическую регрессию.

    # выбираем категориальные (тип object)
    # и численные признаки (остальные типы)
    num_columns = np.where(X.dtypes != 'object')[0]
    cat_columns = np.where(X.dtypes == 'object')[0]
    
    # пайплайн для категориальных признаков
    cat_pipe = Pipeline([('imputer', SimpleImputer(missing_values='?', 
                                strategy='most_frequent')),
                         ('ohe', OneHotEncoder(sparse=False, 
                            handle_unknown='ignore'))])
    
    # пайплайн для численных признаков
    num_pipe = Pipeline([('scaler', StandardScaler())])
    
    # соединяем пайплайны вместе
    transformer = ColumnTransformer(
                               transformers=[('cat', cat_pipe, cat_columns),
                                             ('num', num_pipe, num_columns)], 
                                             remainder='passthrough') 
    # итоговая модель
    model = Pipeline([('transformer', transformer),
                      ('lr', LogisticRegression(random_state=1, n_jobs=-1, 
                                solver='liblinear'))])
    

    Сформируем пространство поиска параметров для hyperopt:

    search_space = {
                    'lr__penalty' : hp.choice(label='penalty', 
                              options=['l1', 'l2']),
                    'lr__C' : hp.loguniform(label='C', 
                            low=-4*np.log(10), 
                            high=2*np.log(10))
                    }

    Здесь параметр регуляризации C выбирается из лог-равномерного распределения [- 4ln10, 2ln10], и может принимать значения [10-4, 102], а тип регуляризации равновероятно выбирается из [l1, l2]. Можно выбрать и другие типы распределений, например, равномерное, или нормальное.

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

    def objective(params, pipeline,  X_train, y_train):
        """
        Кросс-валидация с текущими гиперпараметрами
    
        :params: гиперпараметры
        :pipeline: модель
        :X_train: матрица признаков
        :y_train: вектор меток объектов
        :return: средняя точность на кросс-валидации
        """ 
    
        # задаём модели требуемые параметры    
        pipeline.set_params(**params)
    
        # задаём параметры кросс-валидации (стратифицированная 4-фолдовая с перемешиванием)
        skf = StratifiedKFold(n_splits=4, shuffle=True, random_state=1)
    
        # проводим кросс-валидацию  
        score = cross_val_score(estimator=pipeline, X=X_train, y=y_train, 
                                scoring='roc_auc', cv=skf, n_jobs=-1)
    
        # возвращаем результаты, которые записываются в Trials()
        return   {'loss': -score.mean(), 'params': params, 'status': STATUS_OK}

    Укажем объект для сохранения истории поиска (Trials). Это очень удобно, т.к. можно сохранять, а также прерывать и затем продолжать процесс поиска гиперпараметров. И, наконец, запускаем сам процесс подбора с помощью функции fmin. Укажем в качестве алгоритма поиска tpe.suggest – байесовскую оптимизацию. Для Random Search нужно указать tpe.rand.suggest.

    # запускаем hyperopt
    trials = Trials()
    best = fmin( 
              # функция для оптимизации  
                fn=partial(objective, pipeline=model, X_train=X, y_train=y),
              # пространство поиска гиперпараметров  
                space=search_space,
              # алгоритм поиска
                algo=tpe.suggest,
              # число итераций 
              # (можно ещё указать и время поиска) 
                max_evals=40,
              # куда сохранять историю поиска
                trials=trials,
              # random state
                rstate=np.random.RandomState(1),
              # progressbar
                show_progressbar=True
            )

    Выведем результаты в pandas DataFrame с помощью специальной функции и визуализируем:

    def df_results(hp_results):
        """
        Отображаем результаты hyperopt в формате DataFrame 
    
        :hp_results: результаты hyperop
        :return: pandas DataFrame
        """ 
    
        results = pd.DataFrame([{**x, **x['params']} for x in  hp_results])
        results.drop(labels=['status', 'params'], axis=1, inplace=True)
        results.sort_values(by=['loss'], ascending=False, inplace=True)
        return results
    
    results = df_results(trials.results)
    sns.set_context("talk")
    plt.figure(figsize=(8, 8))
    ax = sns.scatterplot(x='lr__C', y='loss', hue='lr__penalty', 
                                                       data=results);
    ax.set_xscale('log')
    ax.set_xlim(1e-4, 2e2)
    ax.grid()
    

    На графике видно, что Hyperopt почти не исследовал районы, где получались низкие значения roc auc, а сосредоточился на районе с наибольшими значениями этой метрики.

    Таким образом, hyperopt – мощный инструмент для настройки модели, которым легко и удобно пользоваться. Дополнительные материалы можно найти в репозитории (для нескольких моделей), а также в 1234.

    Комментарии 6

      0
      Добрый день. Спасибо за статью.
      Байесовская оптимизация. Здесь значения гиперпараметров в текущей итерации выбираются с учётом результатов на предыдущем шаге. Основная идея алгоритма заключается в следующем – на каждой итерации подбора находится компромисс между исследованием регионов с самыми удачными из найденных комбинаций гиперпараметров и исследованием регионов с большой неопределённостью (где могут находиться ещё более удачные комбинации).

      А генетические алгоритмы оптимизации: разве не этим же и, практически, так же занимаются? Там только по другому всё называется — конкретная комбинация значений параметров поиска: хромосома, «самые удачные из найденных комбинаций» — это хромосомы с лучшим значением функции приспособленности, они отбираются в следующую популяцию.
        0
        Про баесовскую оптимизацию более конкретно habr.com/ru/post/494242
          0
          Спасибо за интерес к статье! У байесовских алгоритмов формирование новой популяции основано на оценке всей старой популяции (построение модели распределения), а не отдельных её представителей, как у генетических. Подробнее в работе www.researchgate.net/publication/220742974_A_comparison_study_between_genetic_algorithms_and_bayesian_optimize_algorithms_by_novel_indices
            0
            Спасибо, интересная статья. Чем то напомнило процесс работы гаусовых микстур-моделей кластеризации.
            Погуглил, почитал примеры фромскратч-кодирования BOA, например.
            YuraLia спасибо за ссылку на хабр-статью, добавила, да, к понимаю, наглядно.

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

          Спасибо за статью. Сейчас выбираю фреймворк для оптимизации гиперпараметров. Подскажите, имели ли вы опыт с Optuna? Если да, можете сравнить с hyperopt?

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое