Я Масаси Шибата из лаборатории искусственного интеллекта CyberAgent (GitHub: @c-bata).

Оптимизация гиперпараметров — один из наиболее важных процессов для обеспечения высокой производительности модели машинного обучения. Optuna [1] — популярная библиотека Python для оптимизации гиперпараметров, простая в использовании и хорошо разработанная программа, поддерживающая различные алгоритмы оптимизации. В этой статье описываются внутренние реализации Optuna, при этом основное внимание уделяется аспектам программного обеспечения.

Чтобы понять внутреннюю реализацию Optuna, вам необходимо знать роли основных компонентов и общий поток выполнения. Однако из-за активной разработки Optuna и роста объема кода стало сложно уловить общий поток от чтения кода. Итак, я создал крошечную программу под названием Minituna. У Minituna есть три версии, каждая из которых содержит 100, 200 и 300 строк кода. Окончательная версия представляет собой небольшую программу всего около 300 строк, но с практичным алгоритмом обрезки, она вполне аутентична.

Minituna: Игрушечная структура оптимизации гиперпараметров, предназначенная для понимания внутреннего устройства Optuna.
https://github.com/CyberAgentAILab/minituna

Я создал каждую версию Minituna, чтобы помочь вам понять, как устроена Optuna, в три этапа:

  1. Разобраться в основных компонентах Optuna и как они называются
  2. Понимать, как использовать категориальные, целочисленные и логарифмические
  3. Понимать API обрезки и алгоритм правила медианной остановки.

Я призываю вас попрактиковаться в чтении кода Minituna, набрав весь его код. Как только вы хорошо поймете, как устроена Minituna, вам не составит труда прочитать код Optuna. В этой статье не будут освещаться простые факты, которые вы можете легко прочитать в коде Minituna. Вместо этого я постараюсь дать несколько советов, которые помогут вам читать и практиковать код Minituna, а также объяснять различия между Minituna и Optuna.

Обратите внимание, что Minituna была реализована на основе кода Optuna v2.1.0. Начиная с версии 2.4.0 (https://github.com/optuna/optuna/releases/tag/v2.4.0) Optuna имеет новый интерфейс, который может передавать несколько целевых значений для поддержки многоцелевой оптимизации. Дизайн существенно не изменился, но это следует иметь в виду при чтении кода Оптуны.

minituna_v1: Роли Trial, Study, Sampler и Storage и как они называются

Minituna v1 — очень маленькая программа, около 100 строк. Но, основные компоненты в ней уже реализованы, и вы можете запустить программу, подобную следующей. Обратите внимание, что все примеры, представленные в этой статье, совместимы с Optuna и будут работать без проблем, даже если вы замените оператор импорта на import optuna as minituna.

Вот как мы определяем целевую функцию в Optuna. Целевая функция принимает пробный объект и возвращает целевое значение. В этом примере целевая функция вызывается 10 раз, и лучшее значение, полученное из 10 испытаний (здесь мы предполагаем проблему минимизации), и параметры выводятся.

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

  • Изучение: компонент, который управляет информацией о заданной задаче оптимизации
    Для заданной задачи оптимизации компонент исследования управляет всей информацией, например, какой алгоритм использовать (сэмплер) и где хранить результаты. результаты испытаний (хранение). Они указываются как аргументы в функции create_study().
  • Испытание: компонент, соответствующий каждому испытанию.
    Целевая функция выбирает параметры из Optuna через API, предоставляемый пробным объектом, и сообщает промежуточные значения для выполнения сокращения.
  • Хранилище: компонент, в котором хранятся результаты испытаний по оптимизации.
    Благодаря этому компоненту Optuna может поддерживать хранилища RDB, обеспечивая сохранение результатов испытаний и распределенную оптимизацию.
  • FrozenTrial: представление каждого испытания на уровне хранилища
    Этот компонент содержит целевое значение и параметры, используемые для оценки целевой функции в каждом испытании. Например, при использовании хранилищ RDB информация, полученная из БД по SQL, помещается в объект FrozenTrial.
  • Sampler: компонент для реализации алгоритма выбора следующего параметра для оценки
    Этот компонент реализует алгоритм для определения того, какие параметры следует оценивать, чтобы получить более объективное значение. Выборка гиперпараметров с байесовской оптимизацией или стратегиями эволюции определяется в компоненте сэмплера. Для простоты объяснения я реализовал в Minituna только случайную выборку. В этой статье не рассматриваются подробности байесовской оптимизации и стратегий развития, поддерживаемых Optuna.

minituna_v2: как использовать категориальный, целочисленный и LogUniform

minituna_v2 поддерживает следующие API предложений в дополнение к suggest_uniform() (образцы реальных параметров из равномерного распределения).

  • suggest_categorical(name, choices=[...]): Примеры категориальных параметров
  • suggest_int(name, low, high) : Примеры целочисленных параметров.
  • suggest_loguniform(name, low, high) : Выбирает реальные параметры из пространства в логарифмическом масштабе.

Это позволяет нам оптимизировать целевую функцию следующим образом:

Ключом к пониманию кода minituna_v2 является то, что все параметры представлены в виде чисел с плавающей запятой внутри хранилища. В предыдущем примере категориальные параметры — это строки, такие как «SVC» и «RandomForest», но даже они представлены как числа с плавающей запятой. Для этого введем следующий абстрактный базовый класс.

Каждый параметр имеет два представления: internal_repr и external_repr. internal_repr — это представление параметра внутри хранилища и значение с плавающей запятой. external_repr — это представление, которое фактически используется целевой функцией, поэтому оно может быть строкой, целым числом или чем-то еще. Чтобы помочь вам понять, я призываю вас попробовать.

Объект распределения необходим для преобразования между internal_repr и external_repr. Поэтому объект раздачи также сохраняется в хранилище. Именно поэтому в FrozenTrial было добавлено поле раздачи.

Есть несколько различий между Minituna и Optuna с точки зрения предлагаемого API.

  1. У Оптуны есть DiscreteUniformDistribution и IntLogUniformDistribution. DiscreteUniformDistribution по сути такой же, как IntLogUniformDistribution, с той лишь разницей, что диапазон дискретизации. IntLogUniformDistribution — это не более чем добавление тех же функций, что и LogUniformDistribution, к IntUniformDistribution. Поэтому вы сможете легко читать Optuna, если потренируетесь читать код minituna_v2. (* Обновление от ноября 2021 г.: Обратите внимание, что недавно были представлены два дополнительных дистрибутива,FloatDistributionиIntDistribution. См. https ://github.com/optuna/optuna/pull/3063 для более подробной информации.)
  2. Optuna предоставляет API под названием suggest_float(name, low, high, step=None, log=False), который был добавлен, потому что слово униформа в suggest_uniform(name, low, high) и suggest_loguniform(name, low, high) немного сбивает с толку, а также для согласованности с suggest_int. Теперь, когда suggest_float официально принята, suggest_uniform, suggest_loguniform и suggest_discrete_uniform могут быть признаны устаревшими, но обсуждение этой темы не продвинулось. (* Обновление от ноября 2021 г.: согласованность предлагаемых API обсуждается на https://github.com/optuna/optuna/issues/2939 и будет улучшена в Optuna V3.)
  3. Чтобы поместить дистрибутив в RDBStorage или RedisStorage, вам нужно сериализовать его в какой-то формат. В Optuna он сериализуется в JSON и затем сохраняется. Это означает, что возможные типы категориальных параметров ограничены сериализуемыми объектами JSON, такими как CategoricalChoiceType = Union[None, bool, int, float, str].

Правильное понимание уровня хранения

Если вы прочитали коды до minituna_v2, вы можете начать читать код Optuna. Далее я кратко расскажу об уровне хранилища, прежде чем углубляться в детали minituna_v3.

Когда я начал читать исходный код Optuna, первое, что я сделал, — это хорошо понял уровень хранения. Это потому, что если вы понимаете, какая информация находится в хранилище, вы можете легко представить, что нужно сделать Оптуне, чтобы реализовать каждую функцию. Вы также можете примерно разобраться в конструктивных различиях между Minituna и Optuna, прочитав код уровня хранилища. Чтобы лучше понять уровень хранения, я рекомендую ознакомиться с Определением модели SQLAlchemy для RDBStorage.

Вот несколько дополнительных вещей, о которых следует помнить, когда вы читаете код на уровне хранилища:

  • Для простоты кода хранилище Minituna может хранить информацию только об одном исследовании, в то время как код Optuna поддерживает несколько исследований.
  • Так как Optuna поддерживает несколько исследований, поле Trial_id в TrialModel не всегда увеличивается постепенно в рамках исследования. Итак, числовое поле было добавлено в Optuna, потому что полезно иметь идентификатор, который просто увеличивается, например, 1, 2, 3 и т. д. в рамках исследования для реализации алгоритма.
  • TrialState имеет два состояния: PRUNED и WAITING. Первый был добавлен для реализации функции обрезки, которая будет объяснена позже в разделе minituna_v3, а второй был добавлен для реализации функции enqueue_trial().

minituna_v3: сокращение по медианному правилу остановки

minituna_v3 — это программа примерно из 300 строк. Поддерживает обрезку (раннюю остановку). Обрезка — это функция, которую используют не все пользователи «Оптуны», поэтому вам не обязательно разбираться в ней, если вы не заинтересованы.

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

  1. trial.report(value, step) для сохранения промежуточных значений в хранилище.
  2. Если trial.shoud_prune() истинно, создайте исключение TrialPruned() и остановите процесс обучения.

Как видно из этого API, все алгоритмы сокращения, поддерживаемые в настоящее время Optuna, решают, следует ли выполнять сокращение, основываясь на промежуточных значениях. В Minituna я реализовал алгоритм под названием Median Stopping Rule [2]. К счастью, ни один из алгоритмов обрезки, поддерживаемых Optuna, не является настолько сложным.

Все эти алгоритмы позволяют преждевременно завершить процесс обучения, основываясь на эмпирическом знании того, что если промежуточное значение низкое, конечное значение также не будет таким хорошим, чтобы сэкономить время. В дополнение к медианному правилу остановки, которое я опишу ниже, Optuna также поддерживает такие алгоритмы, как Successive Halving [3, 4] и Hyperband [5], но основные идеи остаются в основном теми же.

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

Если вы хотите ознакомиться с реализациями SuccessiveHalving и Hyperband в Optuna, стоит помнить одну вещь. Поскольку Optuna не предназначена для приостановки или возобновления конкретного испытания, ее алгоритм был немного изменен и ведет себя иначе, чем алгоритм, описанный в статье. Чтобы узнать больше о реализации последовательного халвинга в Optuna, прочитайте Алгоритм 1 в статье Optuna [1]. Для Hyperband, пожалуйста, прочитайте статью в блоге Как мы реализуем Hyperband в Optuna, опубликованную crcrpar, который реализовал Hyperband.

Я также коснусь вопроса, связанного с конструкцией секатора. Как видно из исходного кода Minituna, в Optuna интерфейсы Pruner и Sampler четко разделены. Это придает коду Optuna хорошую ясность и позволяет нам легко переключаться между Sampler и Pruner и использовать их в любой комбинации. Это большое преимущество.

С другой стороны, некоторые алгоритмы требуют тесной совместной работы Pruner и Sampler, что невозможно реализовать в текущем дизайне. На самом деле, в фоновом режиме была использована небольшая хитрость для реализации Hyperband, чтобы заставить Pruner и Sampler работать вместе. Еще есть место для обсуждения интерфейсов Pruner и Sampler. Если вы прочитали код и придумали какие-то идеи по улучшению, я был бы признателен за ваши предложения.

Как работает совместная выборка в интерфейсе Define-by-Run

Я также опишу Joint Sampling, который не был рассмотрен в Minituna из-за ограниченного размера реализации. Этот механизм уникален для Optuna, который использует интерфейс Define-by-Run. Чтобы реализовать такие алгоритмы оптимизации, как SkoptSampler(GP-BO) и CmaEsSampler, которые учитывают зависимости между параметрами, необходимо понимать концепцию совместной выборки. Если вы проводите исследования в области байесовской оптимизации или стратегий эволюции, вы сможете реализовать свой собственный сэмплер на Optuna после того, как освоите совместную выборку.

Дизайн интерфейса сэмплера Minituna почти идентичен интерфейсу Optuna версии 0.12.0. Однако интерфейсы в Optuna v0.13.0 и более поздних версиях отличаются от таковых. Вы можете увидеть разницу, сравнив следующие интерфейсы Sampler.

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

Пространство поиска этой целевой функции всегда будет таким, как показано ниже.

А как насчет пространства поиска следующей целевой функции?

В интерфейсе Define-by-Run область поиска определяется во время выполнения. В этом примере несколько областей поиска, поскольку в области поиска есть условный оператор if. Он переключается между двумя следующими пространствами поиска в зависимости от контекста.

Затем CmaEsSampler и SkoptSampler извлекут те, которые появляются во всех областях поиска, используя метод Sampler.infer_relative_search_space(study, trial), и передадут их в качестве третьего аргумента в Sampler.sample_relative(study, trial, search_space). Другими словами, только параметр классификатора рассматривается как совместное пространство поиска в приведенном выше примере. GP-BO и CMA-ES используются только для выборок из этого пространства совместного поиска, которое называется совместной выборкой. Вот схема того, как работает совместная выборка.

svc_c, rf_max_depth и другие параметры, не включенные в совместное пространство поиска, будут возвращаться к сэмплерам, где метод не учитывает зависимости между переменными, такими как RandomSampler и TPESampler.

Краткое содержание

Эта статья представила Minituna и дала несколько дополнительных советов о том, как читать коды Minituna и Optuna. Если вы практиковались в чтении кода Minituna, печатая весь его код до версии Minituna v2, и теперь хорошо понимаете весь процесс, вы должны уметь читать код Optuna. Я бы порекомендовал вам начать с компонента, который вас больше всего интересует. Кроме того, команда разработчиков «Оптуна» очень благосклонна и дает подробные PR-обзоры. Я надеюсь, что эта статья побудит вас присоединиться к нам в развитии Optuna в будущем.

Рекомендации

  1. Т. Акиба, С. Сано, Т. Янасэ, Т. Охта и М. Кояма: Optuna: структура оптимизации гиперпараметров следующего поколения. В материалах 25-й Международной конференции ACM SIGKDD по обнаружению знаний и интеллектуальному анализу данных, стр. 2623–2631, 2019 г.
  2. Д. Головин, Б. Соник, С. Мойтра, Г. Кочански, Дж. Карро и Д. Скалли: Google Vizier: служба оптимизации черного ящика. В материалах 23-й Международной конференции ACM SIGKDD по обнаружению знаний и интеллектуальному анализу данных, стр. 1487–1495, 2017 г.
  3. К. Джеймисон и А. Талвалкар: Нестохастическая идентификация наилучшего плеча и оптимизация гиперпараметров. В материалах 19-й Международной конференции по искусственному интеллекту и статистике (AISTATS), PMLR 51: 240–248, 2016 г.
  4. Л. Ли, К. Джеймисон, А. Ростамизаде, Э. Гонина, Дж. Бен-цур, М. Хардт, Б. Рехт, А. Талвалкар. Система массивно-параллельной настройки гиперпараметров. Труды машинного обучения и систем, 2: 230–246, 2020 г.
  5. Л. Ли, К. Джеймисон, Г. ДеСальво, А. Ростамизаде и А. Талвалкар: Hyperband: новый бандитский подход к оптимизации гиперпараметров. Журнал исследований машинного обучения, 18, стр. 1–52, 2017 г.

Благодарности

Я хотел бы поблагодарить команду разработчиков Optuna и г-на Сайто из SkillUp AI за рецензирование этой статьи.

Эта статья является переводом японской публикации в блоге технической студии CyberAgent AI.