Сегодня мы ответим на простой вопрос: «Как работает распределенное обучение (в контексте MXNet )?" Все примеры кода протестированы на MXNet v0.10.0 и могут не работать (или работать по-другому) в других версиях, но я считаю, что общие концепции останутся неизменными еще долгое время.
Ну и напоследок, прежде чем мы перейдем к основной части, хочу выразить благодарность за помощь в написании статьи моим коллегам, без которых эта статья была бы невозможна:
- Мадан Джампани;
- Сунил Марти;
Бесплатная машина на AWS вполне подойдет для выполнения кода.
Преамбула окончена, давайте залезем под лодку.
Распределенное обучение с точки зрения MXNet
В MXNet все участники процесса обучения разделены на 3 логические группы:- планировщик;
- сервер;
- рабочий;
Для начала давайте посмотрим на поверхностное объяснение того, что представляет собой каждый из участников:
Планировщик
Планировщик — это центральный узел кластера, отвечающий за первоначальную настройку кластера, предоставление необходимой информации каждому участнику процесса обучения и… не более того.Посмотрим, как он перейдет в анабиоз, как только кластер будет готов начать обучение.
И даже когда кластер завершит обучение, его задачей будет лишь выключиться.
Думаю, все уже догадались, что планировщик в кластере может быть только один.
Сервер
Сервер действует как хранилище параметров модели обучения.То есть, если модель обучена в стиле: Y = AX + B, сервер хранит векторы A и B. Он же отвечает за их правильное обновление.
Серверов может быть более одного, и соответственно существует правило, согласно которому модель распределяется по нескольким серверам.
Но это тема для отдельной статьи.
Рабочий
Фактически это те участники кластера, которые непосредственно выполняют обучение модели.Каждый воркёр получает свою часть данных, на которых ему необходимо обучаться, рассчитывает шаг градиента и отправляет на серверы для обновления модели.
Пример кластера
Давайте возьмем поддельный пример кластера с:- планировщик на отдельной машине;
- два сервера;
- три машины с рабочими;
Эта картинка, как и описанная конфигурация, будет использоваться только для визуализации потока данных.
Инициализация кластера
На практике мы не будем создавать такой большой кластер, как описано выше, а обойдемся гораздо меньшим кластером с 3 узлами на одной физической машине.На это есть несколько причин:
- проще и дешевле;
- все логи будут в одном месте, что упростит объяснение;
Для MXNet распределенное обучение по сути означает использование KVStore. Название представляет собой аббревиатуру от «Хранилище значений ключей».
А по сути, это распределенное хранилище, которое работает на серверах и имеет некоторый дополнительный функционал (например, оно точно знает, как обновить модель после получения шага градиента от воркера).
Также поддержка KVStore доступна только в одном из двух вариантов:Пришло время приступить к созданию логических членов кластера.В этой статье я предполагаю, что будет использоваться MXNet из выпуска DLAMI за июнь/июль (MXNet 0.10.0).
- MXNet собирался вручную с включенным флагом USE_DIST_KVSTORE=1 или
- был использован ДЛАМИ (поскольку фреймворк собирался вручную с включенным флагом USE_DIST_KVSTORE=1)
По-прежнему существует большая вероятность того, что на момент чтения официальный пакет pip MXNet будет поддерживать KVStore.
Чтобы создать участника, вам просто нужно создать несколько переменных среды, а затем импортировать модуль mxnet.
Планировщик
Первым делом запустим планировщик:Давайте остановимся здесь на секунду, чтобы понять, что происходит. Первые 4 строки кода не должны вызывать много вопросов у Python-программистов: просто импортируйте зависимости и создайте среду ОС.ubuntu:~$ python >>> import subprocess >>> import os >>> scheduler_env = os.environ.copy() >>> scheduler_env.update({ … "DMLC_ROLE": "scheduler", … "DMLC_PS_ROOT_PORT": "9000", … "DMLC_PS_ROOT_URI": "127.0.0.1", … "DMLC_NUM_SERVER": "1", … "DMLC_NUM_WORKER": "1", … "PS_VERBOSE": "2" … }) >>> subprocess.Popen("python -c ‘import mxnet’", shell=True, env=scheduler_env) <subprocess.Popen object at 0x7facb0622850>
Что здесь интересно, так это то, какие именно обновления будут внесены в переменные среды: Начнем с рассмотрения DMLC_ROLE. Давайте посмотрим, где именно он используется, а именно в упаковке.
PS-lite .
По словам официального ПРОЧТИ МЕНЯ (свободный перевод):
Простая и эффективная реализация сервера хранения параметров.Ну, точное место, где считывается переменная среды, находится здесь.
здесь (кстати, все ссылки на конкретные коммиты).
val = CHECK_NOTNULL(Environment::Get()->find("DMLC_ROLE")); // here
std::string role(val);
is_worker_ = role == "worker";
is_server_ = role == "server";
is_scheduler_ = role == "scheduler"; // and later here
verbose_ = GetEnv("PS_VERBOSE", 0);
Я не думаю, что вам нужно быть гуру C++, чтобы понять, что здесь происходит. Логическая роль узла определяется строкой в этой самой переменной «DMLC_ROLE».
Забавно, но похоже, что нет проверки того, что эта переменная содержит одно из разрешенных значений.
Это потенциально может привести к интересным проблемам.
Второе, что нас интересует, это не только то, где переменная читается, но и где она используется.
Чтобы об этом поговорить, нужно обратиться к файлу van.cc, с которым мы еще не раз встретимся, вот конкретная строка, где находится переменная использовал и создается переменная «is_scheduler»: scheduler_.hostname = std::string(CHECK_NOTNULL(Environment::Get()->find("DMLC_PS_ROOT_URI")));
scheduler_.port = atoi(CHECK_NOTNULL(Environment::Get()->find("DMLC_PS_ROOT_PORT")));
scheduler_.role = Node::SCHEDULER;
scheduler_.id = kScheduler;
is_scheduler_ = Postoffice::Get()->is_scheduler(); // here
Если быстро пройтись по коду дальше, чтобы увидеть, что там происходит, то вы увидите следующее интересное место: // get my node info
if (is_scheduler_) {
my_node_ = scheduler_;
} else {
auto role = is_scheduler_ ?
Node::SCHEDULER :
(Postoffice::Get()->is_worker() ? Node::WORKER : Node::SERVER);
В этом конкретном примере переменная «role» никогда не будет равна Node::SCHEDULER. Так что это ваш шанс создать запрос на включение, чтобы исправить это (если еще никто этого не сделал).
Также, глядя на это место, понимаешь, что работы планировщику не так уж и много.
Это связано с тем, что в отличие от воркера и сервера планировщик использует переданный ему IP-адрес и порт, а не ищет свободный порт в системе.
Идем дальше, параметр: DMLC_PS_ROOT_PORT. Мы быстро разберемся в этом, используя уже имеющиеся у нас знания.
Вот код, который мы уже видели: scheduler_.hostname = std::string(CHECK_NOTNULL(Environment::Get()->find("DMLC_PS_ROOT_URI")));
scheduler_.port = atoi(CHECK_NOTNULL(Environment::Get()->find("DMLC_PS_ROOT_PORT"))); // here
scheduler_.role = Node::SCHEDULER;
scheduler_.id = kScheduler;
is_scheduler_ = Postoffice::Get()->is_scheduler();
Опять же, это из ван.
cc .
Как нетрудно догадаться, это порт, на котором планировщик должен прослушивать сообщения.
Надеюсь, на этом этапе понятно, что DMLC_PS_ROOT_URI — это просто IP-адрес планировщика.
Итак, давайте сразу перейдем к обсуждению DMLC_NUM_SERVER и DMLC_NUM_WORKER. Так получилось, что каждый логический узел MXNet в кластере должен знать обо всех других узлах.
Таким образом, для каждого узла перед его запуском в переменных среды фиксируется количество воркеров и серверов в кластере (количество планировщиков необязательно, потому что оно всегда равно 1).
Кстати, эта информация хранится в классе Почтовое отделение (вместе с другой информацией о кластере).
Ну и последний параметр, но, пожалуй, один из самых важных — PS_VERBOSE. Это заставит наш вновь созданный процесс выводить отладочную информацию, которая нам сейчас жизненно необходима.
С точки зрения нашей фейковой диаграммы наш кластер теперь выглядит примерно так:
Запуск сервера
Теперь, когда у нас есть планировщик, давайте запустим сервер.
Поскольку все логические узлы мы поднимаем на одной машине, то нам придется создать копию параметров среды и снова внести туда необходимые изменения, чтобы запустить сервер: >>> server_env = os.environ.copy()
>>> server_env.update({
… "DMLC_ROLE": "server",
… "DMLC_PS_ROOT_URI": "127.0.0.1",
… "DMLC_PS_ROOT_PORT": "9000",
… "DMLC_NUM_SERVER": "1",
… "DMLC_NUM_WORKER": "1",
… "PS_VERBOSE": "2"
… })
>>> subprocess.Popen(“python -c ‘import mxnet’”, shell=True, env=server_env)
<subprocess.Popen object at 0x7facb06228d0>
Надеюсь сейчас происходящее в коде не вызывает вопросов, но на всякий случай:
- мы говорим, что новый процесс — это сервер (DMLC_ROLE);
- говорим какой IP у планировщика (DMLC_PS_ROOT_URI);
- говорим на каком порту планировщик слушает входящие соединения (DMLC_PS_ROOT_PORT);
- сообщаем серверу, сколько воркеров в кластере (DMLC_NUM_WORKER)
- сообщаем серверу, сколько серверов в кластере (DMLC_NUM_SERVER)
- Хорошо, установите вывод в режим отладки (2)
Им нужна информация о планировщике, чтобы постучать в его дверь и попросить добавить их в кластер.
После запуска серверов наша диаграмма выглядит так:
Давайте начнем рабочий
Пришло время запустить сам воркер и создать KVStore: >>> os.environ.update({
… "DMLC_ROLE": "worker",
… "DMLC_PS_ROOT_URI": "127.0.0.1",
… "DMLC_PS_ROOT_PORT": "9000",
… "DMLC_NUM_SERVER": "1",
… "DMLC_NUM_WORKER": "1",
… "PS_VERBOSE": "2"
… })
>>> worker_env = os.environ.copy()
>>> import mxnet
>>> kv_store = mxnet.kv.create(‘dist_async’)
Кстати, KVStore может работать в двух режимах:
- dist_sync
- dist_async
После запуска воркеров наша диаграмма будет выглядеть так:
Жизненный цикл узла (Ван)
Прежде чем мы перейдем к обсуждению того, что происходит при создании KVStore, нам нужно поговорить о том, что у каждого узла есть жизненный цикл, который имеет следующие события:- Начинать - запуск
- Останавливаться - останавливаться
- Получение — получение сообщения
О некоторых из них мы подробно поговорим позже в других статьях, а пока просто перечислим:
- Отправлять — отправляет сообщение
- ПакМета — преобразует модель в прототип сообщения
- РаспаковатьМета — распаковывает протосообщение и создаёт модель
- Сердцебиение - отправляет сообщение, что он еще жив
- загружает все данные планировщика в память
- загружает в память информацию о том, какая роль отведена узлу (воркер, планировщик, сервер)
- для непланирующих — найдите свободный порт и соберите все данные о себе который нужно будет отправить планировщику (при необходимости порт можно задать через переменную окружения)
- привязать себя к найденному порту
- запустить поток, который прослушивает входящие сообщения
- для не планировщиков — отправьте планировщику сообщение с просьбой добавить себя в кластер (обсудим чуть позже)
- Запустить поток, который отвечает за отправку сигнала о том, что узел жив.
Инициализация кластера
После выполнения всех вышеперечисленных команд на экране должно появиться много отладочной информации, поступающей от трех ранее запущенных процессов одновременно.Давайте теперь пройдемся по каждой строке, чтобы подробно обсудить, что происходит и как будет выглядеть наша диаграмма на каждом этапе.
[00:33:12] src/van.cc:75: Привязка к роли=рабочий, ip=1.1.1.1, порт=37350, is_recovery=0Это запускает рабочий процесс.
В данном случае это метод Начинать , который сообщает нам, что его адрес — 1.1.1.1, роль — «рабочий» и найденный порт — 37350. Теперь он мгновенно попытается уведомить планировщик о том, что он готов к добавлению в кластер, указав его адрес и порт:
[00:33:12] src/van.cc:136:? => 1. Мета: запрос=0, временная метка=3, control={ cmd=ADD_NODE, node={ role=worker, ip=1.1.1.1, port=37350, is_recovery=0 } }Это конкретное сообщение генерируется в методе Отправлять , Прямо здесь.
Есть несколько вещей, на которые вам нужно обратить внимание:
- is_recovery=0 — указывает, что он не в режиме восстановления, эта часть выходит за рамки данной статьи
- cmd=ADD_NODE — команда планировщику для добавления воркера в кластер
- ? => 1 — каждый узел имеет свой ранг.
Ранг присваивается планировщиком.
Сам планировщик имеет ранг 1. В нашем случае узел без ранга отправляет сообщение узлу с рангом 1 (планировщику).
Давайте двигаться дальше
[00:33:13] src/van.cc:75: Привязка к роли=серверу, ip=2.2.2.2, порт=54160, is_recovery=0Наш сервер проснулся.
Нашёл порт (54160) и сразу пытаюсь оповестить об этом планировщик:
[00:33:13] src/van.cc:136:? => 1. Мета: запрос=0, временная метка=0, control={ cmd=ADD_NODE, node={ role=server, ip=2.2.2.2, port=54160, is_recovery=0 } }На схеме это выглядит так:
Как и в случае с рабочим, наш сервер отправляет команду «ADD_NODE» для регистрации в кластере.
Итак, поскольку сервер еще не зарегистрирован в кластере и не имеет ранга, мы видим: «? => 1”.
[00:33:13] src/van.cc:75: Привязка к роли=планировщику, id=1, ip=127.0.0.1, порт=9000, is_recovery=0Наконец-то планировщик заработал.
Он использует локальный IP и порт 9000 (все узлы кластера уже должны знать его адрес и порт).
Поскольку планировщик включен, логично ожидать, что в этот момент он получит все входящие сообщения, которые ему были отправлены и.
вуаля:
[00:33:13] src/van.cc:161:? => 1. Мета: запрос=0, временная метка=0, control={ cmd=ADD_NODE, node={ role=server, ip=2.2.2.2, port=54160, is_recovery=0 } }Сообщение от сервера.
Ээта часть логов была сгенерирована методом Получать , если быть еще точнее, то Прямо здесь .
Тут же планировщик получает второе сообщение, на этот раз от воркера:
[00:33:13] src/van.cc:161:? => 1. Мета: запрос=0, временная метка=3, контроль={ cmd=ADD_NODE, node={ role=worker, ip=1.1.1.1, порт=37350, is_recovery=0 } }Прежде всего планировщик обязуется присвоить звания сначала рабочему (9):
[00:33:13] src/van.cc:235: назначить ранг=9 узлу role=worker, ip=1.1.1.1, порт=37350, is_recovery=0Теперь к серверу (8):
[00:33:13] src/van.cc:235: назначить ранг=8 узлу role=server, ip=2.2.2.2, порт=54160, is_recovery=0Затем следует довольно важная часть:
[00:33:13] src/van.cc:136:? => 9. Мета: запрос=0, метка времени=0, контроль={ cmd=ADD_NODE, node={ role=worker, id=9, ip=1.1.1.1, порт=37350, is_recovery=0 role=server, id =8, ip=2.2.2.2, порт=54160, is_recovery=0 role=scheduler, id=1, ip=127.0.0.1, порт=9000, is_recovery=0 } }Подобные сообщения указывают на то, что планировщик получил команды «ADD_NODE» от всех узлов кластера (в нашем случае от 1-го рабочего и 1-го сервера) и теперь начал уведомлять все узлы об их рангах и информации обо всех других узлах в кластере.
кластер.
То есть планировщик отправляет ВСЮ информацию о КАЖДОМ узле кластера КАЖДОМУ узлу кластера.
В этом конкретном сообщении мы видим все данные о кластере и это сообщение отправляется узлу с рангом 9 (это воркёр).
Информация о кластере жизненно важна, поскольку она нужна работнику, например, чтобы понять, на какой сервер отправлять обновление модели.
На схеме этот процесс выглядит так:
Следующий вывод:
[00:33:13] src/van.cc:136:? => 8. Мета: запрос=0, метка времени=1, контроль={ cmd=ADD_NODE, node={ role=worker, id=9, ip=1.1.1.1, порт=37350, is_recovery=0 role=server, id =8, ip=2.2.2.2, порт=54160, is_recovery=0 role=scheduler, id=1, ip=127.0.0.1, порт=9000, is_recovery=0 } }Планировщик отправляет такое же подтверждение узлу с рангом 8 (серверу).
Схема выглядит следующим образом:
[00:33:13] src/van.cc:251: планировщик подключен к 1 воркёрам и 1 серверуПланировщик радостно сообщил, что подключен к одному рабочему и одному серверу (ко всем узлам кластера).
Напоминаем — при работе на реальном кластере все эти логи расположены на разных машинах, поэтому сейчас может показаться, что информации здесь больше, чем нужно.
[00:33:13] src/van.cc:161:1 => 2147483647. Мета: request=0, timestamp=0, control={ cmd=ADD_NODE, node={ role=worker, id=9, ip= 1.1.1.1, порт=37350, is_recovery=0 роль=сервер, идентификатор=8, ip=2.2.2.2, порт=54160, is_recovery=0 роль=планировщик, идентификатор=1, ip=127.0.0.1, порт=9000, is_recovery=0 } } [00:33:13] src/van.cc:281: W[9] подключен к другимЭтот работник получил сообщения от планировщика и сообщает, что подключен к кластеру.
Вы можете спросить, что такое «2147483647».
Ответ — Понятия не имею =) скорее всего баг, я ожидал увидеть: «1 => 9».
Поскольку рабочий правильно видит свой ранг: «W[9]», то ошибка, скорее всего, находится где-то в процессе логирования, поэтому вы можете ее исправить и стать контрибьютором проекта.
[00:33:13] src/van.cc:161:1 => 2147483647. Мета: запрос=0, временная метка=1, control={ cmd=ADD_NODE, node={ role=worker, id=9, ip= 1.1.1.1, порт=37350, is_recovery=0 роль=сервер, идентификатор=8, ip=2.2.2.2, порт=54160, is_recovery=0 роль=планировщик, идентификатор=1, ip=127.0.0.1, порт=9000, is_recovery=0 } } [00:33:13] src/van.cc:281: S[8] подключен к другимТо же самое касается и сервера: он получил сообщение и был рад рассказать о нем миру.
[00:33:13] src/van.cc:136:? => 1. Мета: запрос=1, отметка времени=4, контроль={ cmd=BARRIER, барьер_группа=7 } [00:33:13] src/van.cc:136:? => 1. Мета: запрос=1, метка времени=2, контроль={ cmd=BARRIER, барьер_группа=7 } [00:33:13] src/van.cc:136:? => 1. Мета: запрос=1, метка времени=1, контроль={ cmd=BARRIER, барьер_группа=7 }Еще одна важная часть.
До сих пор мы видели только одну команду «ADD_NODE».
Здесь мы видим новый: «БАРЬЕР».
Вкратце, это концепция барьеров, которая, надеюсь, знакома читателю по многопоточному программированию и означает: «остановись, пока все не достигнут этого барьера».
Планировщик отвечает за передачу информации, когда все достигнут барьера и смогут продолжить выполнение.
Первый барьер находится сразу после запуска кластера, но до начала обучения.
Все три узла (включая сам планировщик) отправили сообщение, в котором, по сути, говорилось: «Я достиг барьера, дайте мне знать, когда двигаться дальше».
Также, как видно из сообщения, существует понятие барьерной группы (barrier_group).
Группа барьеров — это группа узлов, которые участвуют в определенном барьере.
Эти группы: 1 — планировщик 2 — серверы 4 — рабочие Как нетрудно догадаться, это степень двойки, поэтому наша группа 7: 4 + 2 + 1. По сути, этот барьер касается всех.
Ну и естественно, поскольку в наших логах мы увидели три отправки сообщений, то логично ожидать три строчки о том, что планировщик получает эти сообщения:
[00:33:13] src/van.cc:161: 1 => 1. Мета: запрос=1, метка времени=2, контроль={ cmd=BARRIER, барьер_группа=7 } [00:33:13] src/van.cc:291: Количество барьеров для 7:1 [00:33:13] src/van.cc:161:9 => 1. Мета: запрос=1, метка времени=4, контроль={ cmd=BARRIER, барьер_группа=7 } [00:33:13] src/van.cc:291: Количество барьеров для 7:2 [00:33:13] src/van.cc:161:8 => 1. Мета: запрос=1, метка времени=1, контроль={ cmd=BARRIER, барьер_группа=7 } [00:33:13] src/van.cc:291: Количество барьеров для 7:3То, что происходит на нашей диаграмме, выглядит так:
Теперь пришло время обсудить, что делает планировщик, когда получает новое сообщение о том, что узел достиг барьера в определенной группе:
- он увеличивает счетчик количества узлов, отправивших команду БАРЬЕР в определенной группе ( Прямо здесь )
- когда счетчик равен количеству узлов в группе, он отправляет всем подтверждение того, что нормальная работа может продолжаться.
Что ж, как только он достиг ожидаемого размера (3), планировщик начал отправлять подтверждения:
[00:33:13] src/van.cc:136:? => 9. Мета: запрос=0, отметка времени=3, контроль={ cmd=BARRIER, барьер_группа=0 } [00:33:13] src/van.cc:136:? => 8. Мета: запрос=0, отметка времени=4, контроль={ cmd=BARRIER, барьер_группа=0 } [00:33:13] src/van.cc:136:? => 1. Мета: запрос=0, отметка времени=5, контроль={ cmd=BARRIER, барьер_группа=0 }На нашей схеме это выглядит так:
Как видите, планировщик даже отправляет подтверждение самому себе.
Ну конечно, раз из планировщика было отправлено сообщение (аж 3), то надо посмотреть журналы, указывающие, что эти сообщения были получены :
[00:33:13] src/van.cc:161:1 => 9. Мета: запрос=0, метка времени=3, контроль={ cmd=BARRIER, барьер_группа=0 } [00:33:13] src/van.cc:161:1 => 8. Мета: запрос=0, метка времени=4, контроль={ cmd=BARRIER, барьер_группа=0 } [00:33:13] src/van.cc:161: 1 => 1. Мета: запрос=0, метка времени=5, контроль={ cmd=BARRIER, барьер_группа=0 }Ну и последние штрихи.
На данный момент планировщик достиг второго барьера, которого достигнут все узлы после окончания обучения, однако, поскольку планировщик не принимает участия в обучении, он уже достиг этого самого барьера.
Поэтому он отправляет барьер_group=7 о том, что достиг барьера, с мгновенным подтверждением получения сообщения и установкой счетчика барьерной группы 7 на 1.
[00:33:13] src/van.cc:136:? => 1. Мета: запрос=1, отметка времени=6, контроль={ cmd=BARRIER, барьер_группа=7 } [00:33:13] src/van.cc:161: 1 => 1. Мета: запрос = 1, временная метка = 6, контроль = { cmd = БАРЬЕР, барьер_группа = 7 } [00:33:13] src/van.cc:291: Количество барьеров для 7:1На этом этапе инициализация кластера завершена, можно приступать к обучению.
Образование
Выполнив весь код, мы имеем инициализированный KVstore. Что теперь? Давайте использовать его для непосредственного обучения.Я буду использовать очень простой пример линейного регрессора.
отсюда .
Я просто прошу, прежде чем продолжить, просмотреть пример, чтобы понять, что происходит. Чтобы сделать обучение в описанном примере распределенным, нужно изменить в коде всего 1 строчку.
Вместо: model.fit(train_iter, eval_iter,
optimizer_params={
'learning_rate':0.005, 'momentum': 0.9},
num_epoch=50,
eval_metric='mse',
batch_end_callback
= mx.callback.Speedometer(batch_size, 2))
Вам нужно написать: model.fit(train_iter, eval_iter,
optimizer_params={
'learning_rate':0.005, 'momentum': 0.9},
num_epoch=50,
eval_metric='mse',
batch_end_callback
= mx.callback.Speedometer(batch_size, 2),
kvstore=kv_store) # updated line
Так просто? короче — да.
Небольшой вывод
Надеюсь, теперь читатель имеет более детальное представление о том, что происходит с кластером MXNet в момент его запуска.Также я надеюсь, что эта статья поможет с отладкой кластера в случае возникновения каких-либо проблем.
Ну и плюс, имея эти знания, можно сделать некоторые выводы о характеристиках сети для кластера, а именно:
- планировщику не критично иметь быстрое соединение с другими
- серверам не критично иметь быстрое соединение друг с другом
- каждый работник должен иметь быстрое соединение с каждым сервером
- Работникам не критично иметь быструю связь друг с другом
Также, если вдруг вы строите системы распределенного машинного обучения на базе AWS с использованием MXNet и у вас есть вопросы, то я рад помочь и ответить ([email protected]).
Ссылки:
- полная версия кода из статьи
- Draw.IO , сервис для рисования диаграмм
- Репозиторий MXNet ps-lite
-
Хлор Активный
19 Oct, 24 -
Выпущен Новый Humble Bundle
19 Oct, 24 -
Плагин Perl5 Для Intellij Idea
19 Oct, 24