Время прочтения: 6 мин.

В машинном обучении гиперпараметрами называют параметры модели, значения которых устанавливаются перед запуском процесса её обучения. Ими могут быть, как параметры самого алгоритма, например, глубина дерева в 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 – ещё одна версия Random Search. 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 – мощный инструмент для настройки модели, которым легко и удобно пользоваться. Дополнительные материалы можно найти в моём репозитории (для нескольких моделей), а также в 1, 2, 3, 4.