Эффективно проводятся тысячи экспериментов: Hyperopt с Sacred

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

  • Ручная настройка - обоснованное предположение или интуитивный поиск. Этот метод включает в себя некоторый анализ результатов по мере их поступления и выбор определенных параметров по сравнению с другими при проведении экспериментов.
  • Сетка - методично проверьте все комбинации параметров. Этот метод действительно является единственным методом, который действительно гарантирует достижение оптимальной производительности, но мы подсчитали, что существует более миллиона различных тестов, которые мы могли бы запустить, что, разумеется, просто невозможно сделать за то время, которое у нас есть. Также в параметрах с действительным знаком разрешение сетки может быть серьезной проблемой.
  • Случайно - включение и выключение различных параметров в случайном порядке. Было показано, что Random обеспечивает оптимальную или почти оптимальную производительность для многих различных алгоритмов.
  • Произвольная настройка вручную - выберите, какие параметры следует переключать случайным образом, а какие установить на определенное значение. Например, мы заметили, что метод определения характеристик вектора с пропуском мыслей, описанный Патом в его блоге, последовательно ухудшал производительность алгоритма для определенных наборов данных, поэтому мы уменьшили количество попыток использовать этот метод.
  • Вероятностный - существует ряд оптимизаторов и пакетов с открытым исходным кодом, которые используют методы байесовского моделирования для оптимизации гиперпараметров. Эти методы измеряют влияние параметра на целевую функцию и оценивают, насколько хорошо модель может потенциально работать при использовании этого параметра. По мере того, как модель учится в ходе последовательных экспериментов, лучшие параметры сохраняются, а худшие параметры постепенно исключаются. Существует множество различных пакетов Python с открытым исходным кодом, но по многим причинам большая часть наших усилий и этого сообщения в блоге сосредоточена на hyperopt.

Деревья чего?

Hyperopt использует метод, называемый древовидным оценщиком парзена (TPE); подход, представленный на NIPS 2011 компанией Bergstra et. ал . Одним из основных преимуществ TPE по сравнению с другими вероятностными методами является то, что большинство других методов не могут сохранять зависимости между параметрами. TPE может сохранять эти отношения, поскольку он моделирует плотность параметра для хороших экспериментов и сравнивает ее с его плотностью для плохих экспериментов. Затем он может использовать эти модели для определения ожидаемого улучшения целевой функции для любых значений, которые может принимать параметр.

TPE полезен для проекта Pythia, поскольку у нас есть некоторые параметры, описывающие, какой алгоритм запускать (например, логистическая регрессия по сравнению с XGBoost), а также параметры, относящиеся к этому алгоритму (например, штраф и допуск модели логистической регрессии). Я покажу вам, как это выглядит, чуть позже в этом посте.

Я очень наглядный человек, поэтому мне захотелось увидеть, как работает Hyperopt. Поскольку это практически невозможно в контексте Pythia из-за большого количества параметров, я решил проанализировать гипероптут с помощью другой задачи оптимизации 2D, а именно функции Розенброка. Эта квадратичная функция достаточно сложна, чтобы быть трудной, поскольку она имеет длинную и узкую область, близкую к оптимальной, но все же достаточно проста для понимания. Минимальное значение этой оптимизации равно нулю и находится в точке (1,1). Значение оптимизации отображается ниже в виде тепловой карты. Более низкие значения показаны красным, а более высокие - синим.

Я оценил производительность 100 тестов случайного поиска и сравнил их с производительностью 100 тестов hyperopt. Для этого теста есть два параметра - значение x и значение y. Я позволяю им варьироваться от -0,5 до 2,5. Результаты каждого теста были проанализированы и нанесены на тепловую карту функции Розенброка. В общем, гиперопт приблизился к оптимальной точке, но случайный поиск также показал себя неплохо.

Большинство тестов hyperopt были ближе к оптимальной точке по сравнению со случайным поиском, и нашли лучшее общее решение. Это подсказало мне, что Hyperopt действительно разумным образом сокращает пространство поиска параметров. Во-вторых, random с меньшей вероятностью натолкнется на лучшее решение в проблеме более высокой размерности (прокляните вас проклятие размерности!). Наконец, Bergstra et. ал . находит, что TPE быстрее находит почти оптимальный ввод, чем случайный.

Запуск Hyperopt

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

from hyperopt import fmin, tpe, hp, Trials
number_of_experiments = 100
#Define the Rosenbrock function as the objective
def rosenbrock_objective(args):
    x = args['x']
    y = args['y']
    return (1.-x)**2 + 100.*(y-x*x)**2
#Trials keeps track of all experiments
#These can be saved and loaded back into a new batch of experiments
trials_to_keep = Trials()
#Space is where you define the parameters and parameter search space
space={'x': hp.uniform('x', -0.5, 2.5),
       'y': hp.uniform('y', -0.5, 2.5)}
#The main function to run the experiments
#The algorithm tpe.suggest runs the Tree-structured Parzen estimator
#Hyperopt does have a random search algorithm as well
best = fmin(objective=rosenbrock_objective,
            space=space,
            algo=tpe.suggest,
            max_evals=number_of_experiments,
            trials = trials_to_keep
            )

А теперь мы входим в «Священные деревья Парзена»

Hyperopt выглядит так, как будто он может хорошо работать для нашего варианта использования, но коллега Дейв сказал мне показать мою работу - так как мне написать программу, которая использует Hyperopt и Sacred? Как указывает Дэйв, Sacred - это пакет Python с открытым исходным кодом, который поможет вам настраивать, организовывать, регистрировать и воспроизводить эксперименты. При использовании Sacred с hyperopt вы настраиваете Sacred очень похожим образом, указывая конфигурация и наблюдатель.

Одна из самых больших модификаций Sacred, которую вам, вероятно, следует сделать, - это патч, чтобы избежать необходимости объявлять все параметры (огромный совет моему коллеге Йонасу Тесфайе за обнаружение этого патча). Этот патч очень полезен, если вы не хотите, чтобы hyperopt учитывал некоторые параметры, что часто бывает при экспериментальном тестировании. Например, если мы запускаем Pythia с одним алгоритмом, другие параметры алгоритма не указываются.

from sacred.initialize import Scaffold
def noop(item):
    pass
Scaffold._warn_about_suspicious_changes = noop

Другой важный прием, позволяющий правильно регистрировать все в Sacred, - это использование глобальных переменных. Причина этого в том, что основная функция Sacred Experiment.main () имеет один аргумент - объект функции, который она вызывает без зависимых переменных. Кроме того, функция возвращает больше результатов, чем может обработать гипероптика с одной целью. Глобальные переменные решают обе эти проблемы. Ниже я покажу упрощенный пример, но если вам действительно интересно, посмотрите функцию, которую мы используем для Pythia на Github.

Во-первых, давайте настроим вызываемый объект, который может вызывать Sacred. Sacred будет регистрировать все, что возвращается при вызове основной функции Experiment.main (). В приведенном ниже примере это all_results, словарь, созданный путем выполнения одного полного эксперимента в Pythia. Однако, прежде чем мы вернем все наши результаты для регистрации, мы хотим отслеживать единственную глобальную переменную для использования hyperopt - в данном случае метко названную result. Для Pythia мы будем использовать средний балл F1, и поскольку hyperopt будет пытаться найти минимальное значение, мы предоставим ему отрицательный балл F1.

# This is where we run pythia and set a result hyperopt will use
def run_pythia_with_global_args():
    global args
    global result
    # The primary Pythia function completes one train and test cycle
    all_results = pythia_main.main(args)
    # result is set here and is later used by hyperopt
    # We really care about the average f1 score
    result = -np.mean(all_results['f_score'])
    # We really want to save all the results in Sacred
    # Hence why we return all_results
    return all_results

Теперь мы можем настроить функцию objective (), которую будет использовать hyperopt. Опять же, мы собираемся использовать глобальные значения, чтобы наша функция run_pythia_with_global_args () могла видеть параметры эксперимента и устанавливать переменную result. В основном в нашей функции objective () мы настраиваем и запускаем Священный эксперимент. Вам необходимо настроить Experiment.main () с вызываемой функцией, а затем затем обновить переменные с помощью config_updates = pythia_args.

from sacred import Experiment
import src.pipelines.master_pipeline as pythia_main
# Objective is set up much like the simple hyperopt example above
def objective(pythia_args):
    global args
    global result
ex = Experiment('Hyperopt')  
    ex.main(run_pythia_with_global_args)
    r = ex.run(config_updates=pythia_args)
return result

Последняя часть функции - это чистый гиперопт. Если вы помните, для Hyperopt нужна цель (которая у нас есть), а также пространство параметров и количество экспериментов, которые нужно завершить. Теперь мы действительно можем воспользоваться способностью hyperopt сохранять иерархические параметры. Это показано переменной algorithm_type. Когда выбран алгоритм XGBoost, hyperopt определяет, как установить переменные XGB_LEARNRATE и XGB_MAXDEPTH, но не пытается установить LOG_C или LOG_TOL, поскольку эти параметры связаны с алгоритмом логистической регрессии. Эта способность кажется такой простой, но она действительно важна и полезна! Последний кусочек кода - это фактический вызов для запуска hyperopt, когда все части подходят друг к другу.

def run_pythia_hyperopt():
# Define the space - the Pythia space is quite large!
    space = "algorithm_type":hp.choice('algorithm_type', 
        [ {
            'LOG_REG': True,
            'LOG_C': hp.uniform('log_C', 1e-5,10),
            'LOG_TOL': hp.uniform('log_tol', 1e-5, 10),
            'LOG_PENALTY': hp.choice('log_penalty', ["l1", "l2"])
        }, 
        {
            'XGB': True,
            'XGB_LEARNRATE': hp.uniform('x_learning_rate', 0.01, 1),
            'XGB_MAXDEPTH': hp.choice('x_max_depth',[3,4,5,6]),
         } ]),

    "BOW_APPEND":hp.choice('BOW_APPEND', [True, False]),
    "BOW_DIFFERENCE":hp.choice('BOW_DIFFERENCE', [True, False]),
    "BOW_PRODUCT":hp.choice('BOW_PRODUCT', [True, False]),
    "BOW_COS":hp.choice('BOW_COS', [True, False]),
    "BOW_TFIDF":hp.choice('BOW_TFIDF', [True, False]),
    #many, many more parameters
    'USE_CACHE': True
    }
    # Now we can set up the main hyperopt function fmin
    # It is exactly the same as in the simple example!
    optimal_run = fmin(objective, 
                       space, 
                       algo=tpe.suggest, 
                       max_evals= 1000)

Работает?

Да!… Но требовалась небольшая помощь, чтобы найти самую высокую ценность. Один из основных уроков, которые я извлек из использования hyperopt, заключается в том, что вам по-прежнему требуется большое количество запусков для Hyperopt, чтобы можно было узнать, какие параметры лучше всего. Люди, с другой стороны, часто могут видеть тенденции производительности при меньшем количестве точек данных. Определенно важно признать предвзятость, которая может быть у нас при настройке вручную, но ручная настройка часто работает довольно хорошо.

Чтобы продемонстрировать, сколько образцов вам нужно для получения наилучшего результата, вернемся к нашему 2D-примеру с функцией Rosenbrock. Если вы позволите каждому оптимизатору опробовать 50 различных контрольных точек, TPE найдет лучшую (например, более низкую) точку, чем случайный поиск, примерно в 55% случаев. Однако если вы попробуете 500 образцов, TPE найдет точку ниже, чем случайный поиск в 85% случаев. Так что даже в нашем более простом 2D-примере нам нужно много тестов. Таким образом, мы знаем, что в Pythia нам понадобится провести действительно большое количество экспериментов!

Однако, чтобы провести любое количество экспериментов, мы должны ограничить типы методов определения характеристик, поскольку выполнение некоторых из них просто занимает слишком много времени. Чтобы справиться с этой проблемой, мы использовали настроенную вручную стратегию Hyperopt (кроме того, есть несколько других крутых методов, которые действительно учитывают время выполнения). Подобно настраиваемому вручную случайному правилу, мы выбираем, какие параметры должны переключаться между гипероптами, а какие установить на определенное значение (в данном случае off). К счастью, эти методы обычно не работают, поэтому я не испытываю сильной изжоги, если их не использую. С ручной настройкой Hyperopt завершил некоторые из наиболее эффективных экспериментов менее чем за день, но без ручной настройки может потребоваться от трех до четырех дней для получения сопоставимых результатов.

Мы хотели бы знать, используете ли вы в своей работе Hyperopt или Sacred, или их оба вместе! Каково ваше выступление с Hyperopt?

Lab41 - это испытательная лаборатория Кремниевой долины, в которой эксперты из разведывательного сообщества США (IC), академических кругов, промышленности и In-Q-Tel собираются вместе, чтобы лучше понять, как работать с большими данными и, в конечном итоге, использовать их.

Узнайте больше на lab41.org и подпишитесь на нас в Twitter: @ _lab41