.. _sec_rs_async: Асинхронный случайный поиск =========================== Как мы видели в предыдущем :numref:`sec_api_hpo`, из-за высокой стоимости оценки конфигураций гиперпараметров нам может потребоваться ждать часы или даже дни, прежде чем случайный поиск вернёт хорошую конфигурацию гиперпараметров. На практике у нас часто есть доступ к пулу ресурсов, например, к нескольким GPU на одной машине или к нескольким машинам с одним GPU. Это порождает вопрос: *как эффективно распараллелить случайный поиск?* В целом различают синхронную и асинхронную параллельную оптимизацию гиперпараметров (см. :numref:`distributed_scheduling`). В синхронном режиме мы ждем завершения всех одновременно запущенных экспериментов, прежде чем приступать к следующей партии. Рассмотрим пространства конфигураций, содержащие такие гиперпараметры, как количество фильтров или число слоёв в глубокой нейронной сети. Конфигурации гиперпараметров, содержащие большее число слоёв фильтров, будут завершаться естественным образом дольше, и все остальные эксперименты в той же партии будут вынуждены ждать на точках синхронизации (серая область на :numref:`distributed_scheduling`), прежде чем мы сможем продолжить процесс оптимизации. В асинхронном режиме мы немедленно планируем новый эксперимент, как только становятся доступными ресурсы. Это позволяет оптимально использовать ресурсы, поскольку мы можем избежать любых накладных расходов на синхронизацию. При случайном поиске каждая новая конфигурация гиперпараметров выбирается независимо от всех остальных, в частности, без использования наблюдений, полученных в ходе предыдущих оценок. Это означает, что мы можем тривиально распараллелить случайный поиск в асинхронном режиме. Это не так просто для более сложных методов, которые принимают решения на основе предыдущих наблюдений (см. :numref:`sec_sh_async`). Хотя для асинхронного случайного поиска требуется больше ресурсов, чем в последовательном режиме, он демонстрирует линейное ускорение: определённый уровень производительности достигается в :math:`K` раз быстрее, если можно запустить :math:`K` экспериментов параллельно. .. _distributed_scheduling: .. figure:: ../img/distributed_scheduling.svg Распределение процесса оптимизации гиперпараметров может осуществляться как синхронно, так и асинхронно. По сравнению с последовательным подходом, это позволяет сократить общее время выполнения при сохранении общего объёма вычислений. Синхронное планирование может приводить к простоям рабочих узлов из-за наличия «медленных» задач (stragglers). В этом разделе мы рассмотрим асинхронный случайный поиск, при котором испытания выполняются в нескольких процессах Python на одной машине. Распределённое планирование и выполнение задач сложно реализовать с нуля. Мы будем использовать *Syne Tune* :cite:`salinas-automl22`, предоставляющий простой интерфейс для асинхронной оптимизации гиперпараметров (HPO). Syne Tune разработан для работы с различными бэкендами выполнения; заинтересованным читателям предлагается изучить его простые API, чтобы узнать больше о распределённой оптимизации гиперпараметров. .. raw:: latex \diilbookstyleinputcell .. code:: python import logging from d2l import torch as d2l logging.basicConfig(level=logging.INFO) from syne_tune import StoppingCriterion, Tuner from syne_tune.backend.python_backend import PythonBackend from syne_tune.config_space import loguniform, randint from syne_tune.experiments import load_experiment from syne_tune.optimizer.baselines import RandomSearch Целевая функция --------------- Во-первых, нам необходимо определить новую целевую функцию, которая будет возвращать показатели эффективности в Syne Tune через коллбэк ``report``. .. raw:: latex \diilbookstyleinputcell .. code:: python def hpo_objective_lenet_synetune(learning_rate, batch_size, max_epochs): from syne_tune import Reporter from d2l import torch as d2l model = d2l.LeNet(lr=learning_rate, num_classes=10) trainer = d2l.HPOTrainer(max_epochs=1, num_gpus=1) data = d2l.FashionMNIST(batch_size=batch_size) model.apply_init([next(iter(data.get_dataloader(True)))[0]], d2l.init_cnn) report = Reporter() for epoch in range(1, max_epochs + 1): if epoch == 1: # Initialize the state of Trainer trainer.fit(model=model, data=data) else: trainer.fit_epoch() validation_error = trainer.validation_error().cpu().detach().numpy() report(epoch=epoch, validation_error=float(validation_error)) Обратите внимание, что ``PythonBackend`` Syne Tune требует импорта зависимостей внутри определения функции. Асинхронный планировщик ----------------------- Сначала определим количество рабочих процессов, которые будут оценивать эксперименты параллельно. Также необходимо указать продолжительность выполнения случайного поиска, задав верхний предел общего времени работы. .. raw:: latex \diilbookstyleinputcell .. code:: python n_workers = 2 # Needs to be <= the number of available GPUs max_wallclock_time = 12 * 60 # 12 minutes Далее мы указываем метрику, которую хотим оптимизировать, а также направление её оптимизации — минимизацию или максимизацию. При этом значение ``metric`` должно соответствовать имени аргумента, передаваемому в коллбек ``report``. .. raw:: latex \diilbookstyleinputcell .. code:: python mode = "min" metric = "validation_error" Мы используем пространство конфигураций из нашего предыдущего примера. В Syne Tune этот словарь также можно использовать для передачи постоянных атрибутов в скрипт обучения. Мы используем эту возможность для передачи ``max_epochs``. Кроме того, мы указываем первую конфигурацию, подлежащую оценке, в ``initial_config``. .. raw:: latex \diilbookstyleinputcell .. code:: python config_space = { "learning_rate": loguniform(1e-2, 1), "batch_size": randint(32, 256), "max_epochs": 10, } initial_config = { "learning_rate": 0.1, "batch_size": 128, } Далее необходимо указать бэкенд для выполнения задач. В данном разделе мы рассматриваем только распределение на локальной машине, где параллельные задачи выполняются в виде подпроцессов. Однако для масштабного поиска гиперпараметров (HPO) эту конфигурацию можно также развернуть в кластерной или облачной среде, где каждая попытка (trial) использует целый объект (instance). .. raw:: latex \diilbookstyleinputcell .. code:: python trial_backend = PythonBackend( tune_function=hpo_objective_lenet_synetune, config_space=config_space, ) Теперь мы можем создать планировщик для асинхронного случайного поиска, который по поведению аналогичен нашему ``BasicScheduler`` из :numref:`sec_api_hpo`. .. raw:: latex \diilbookstyleinputcell .. code:: python scheduler = RandomSearch( config_space, metric=metric, mode=mode, points_to_evaluate=[initial_config], ) Syne Tune также включает ``Tuner``, в котором основной цикл экспериментов и учёт данных централизованы, а взаимодействие между планировщиком и бэкендом опосредовано. .. raw:: latex \diilbookstyleinputcell .. code:: python stop_criterion = StoppingCriterion(max_wallclock_time=max_wallclock_time) tuner = Tuner( trial_backend=trial_backend, scheduler=scheduler, stop_criterion=stop_criterion, n_workers=n_workers, print_update_interval=int(max_wallclock_time * 0.6), ) Запустим наш распределённый эксперимент по оптимизации гиперпараметров. Согласно критерию остановки, он будет выполняться около 12 минут. .. raw:: latex \diilbookstyleinputcell .. code:: python tuner.run() Логи всех протестированных конфигураций гиперпараметров сохраняются для последующего анализа. В любой момент во время выполнения задачи настройки мы можем легко получить результаты, достигнутые на данный момент, и построить траекторию incumbent-решения. .. raw:: latex \diilbookstyleinputcell .. code:: python d2l.set_figsize() tuning_experiment = load_experiment(tuner.name) tuning_experiment.plot() Визуализация процесса асинхронной оптимизации --------------------------------------------- Ниже мы визуализируем, как эволюционируют кривые обучения каждого испытания (каждый цвет на графике соответствует одному испытанию) в процессе асинхронной оптимизации. В любой момент времени количество одновременно запущенных испытаний равно числу рабочих. Как только одно испытание завершается, мы немедленно запускаем следующее, не дожидаясь завершения остальных. Асинхронное планирование сводит время простоя рабочих к минимуму. .. raw:: latex \diilbookstyleinputcell .. code:: python d2l.set_figsize([6, 2.5]) results = tuning_experiment.results for trial_id in results.trial_id.unique(): df = results[results["trial_id"] == trial_id] d2l.plt.plot( df["st_tuner_time"], df["validation_error"], marker="o" ) d2l.plt.xlabel("wall-clock time") d2l.plt.ylabel("objective function") Резюме ------ Мы можем существенно сократить время ожидания при случайном поиске, распределяя испытания по параллельным ресурсам. В целом мы различаем синхронное и асинхронное планирование. При синхронном планировании мы выбираем новую партию конфигураций гиперпараметров только после завершения предыдущей. Если есть «отстающие» испытания — те, которые занимают больше времени, чем остальные, — наши рабочие узлы вынуждены ждать на точках синхронизации. Асинхронное планирование оценивает новые конфигурации гиперпараметров сразу же, как только становятся доступными ресурсы, и, следовательно, обеспечивает занятость всех рабочих узлов в любой момент времени. Хотя случайный поиск легко распараллелить асинхронно и он не требует изменений самого алгоритма, другие методы нуждаются в дополнительных модификациях. Упражнения ---------- 1. Рассмотрим модель ``DropoutMLP``, реализованную в :numref:`sec_dropout` и использованную в упражнении 1 из :numref:`sec_api_hpo`. 1. Реализуйте целевую функцию ``hpo_objective_dropoutmlp_synetune`` для использования с Syne Tune. Убедитесь, что функция сообщает об ошибке на валидационной выборке после каждой эпохи. 2. Используя настройку из упражнения 1 в :numref:`sec_api_hpo`, сравните случайный поиск с байесовской оптимизацией. Если вы используете SageMaker, вы можете воспользоваться средствами бенчмаркинга Syne Tune для запуска экспериментов параллельно. Подсказка: байесовская оптимизация предоставляется в виде ``syne_tune.optimizer.baselines.BayesianOptimization``. 3. Для выполнения этого упражнения вам потребуется запустить код на объекте с как минимум 4 ядрами процессора. Для одного из рассмотренных выше методов (случайный поиск или байесовская оптимизация) проведите эксперименты с ``n_workers=1``, ``n_workers=2``, ``n_workers=4`` и сравните результаты (траектории incumbent). По крайней мере для случайного поиска вы должны наблюдать линейное масштабирование в зависимости от количества рабочих процессов. Подсказка: для получения надёжных результатов может потребоваться усреднение по нескольким повторениям для каждого случая. 2. *Продвинутый*. Цель этого упражнения — реализовать новый планировщик в Syne Tune. 1. Создайте виртуальное окружение, включающее исходные коды `d2lbook `__ и `syne-tune `__. 2. Реализуйте ``LocalSearcher`` из упражнения 2 в :numref:`sec_api_hpo` в виде нового алгоритма поиска в Syne Tune. Подсказка: ознакомьтесь с `этим руководством `__. В качестве альтернативы вы можете воспользоваться этим `примером `__. 3. Сравните ваш новый ``LocalSearcher`` с ``RandomSearch`` на бенчмарке ``DropoutMLP``. `Обсуждения `__