Практическое руководство по использованию asyncio в приложениях машинного обучения

За последние несколько лет я потратил много времени на развертывание приложений машинного обучения в рабочей среде. Когда я был активным участником Kaggle в 2017 году, меня мало заботили сложность или производительность модели, например, задержка, пропускная способность и так далее. Затем я занимался подготовкой данных и проектированием признаков, пробовал современные модели, выполнял строгую настройку гиперпараметров, опробовал причудливые методы увеличения и т. д. Единственной целью для меня было создать максимально точную модель. Не поймите меня неправильно, это критически важные части проекта машинного обучения, и часто вы можете получить лучшую модель, если хорошо знаете свое дело и хорошо выполняете описанные выше шаги. Однако на этом проект машинного обучения обычно не заканчивается в реальной жизни.

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

Независимо от того, находитесь ли вы на стороне разработчика, когда пишете код для обслуживания модели, или вы на стороне потребителя, пишете скрипты для интеграции стороннего сервиса в свое приложение, вы можете получить огромную пользу, если понимаете использование asyncio в python. . В следующем разделе мы рассмотрим основные концепции асинхронного программирования в Python, а также познакомим вас с фантастическим пакетом asyncio и его функциональностью.

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

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

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

С этим покончено, давайте приступим прямо к делу. Весь код, использованный в этом посте, доступен в моем репозитории Github.

Когда следует использовать асинхронность

У вас когда-нибудь был случай, когда вам нужно было загрузить кучу изображений, аудиофайлов или текстовых расшифровок, прежде чем вы могли запустить на них свою модель? В качестве альтернативы, сталкивались ли вы с ситуацией, когда вам приходилось извлекать несколько файлов из баз данных или хранилищ функций и объединять их, прежде чем делать прогнозы с вашей моделью?

Ключевой деталью, на которую следует обратить внимание, является тот факт, что вышеуказанные задачи могут выполняться параллельно (или одновременно). Если вы согласны с этим, следующий вопрос, который вы можете себе задать, — как мне распараллелить эти задачи. Достаточно справедливо, давайте посмотрим наши варианты. Для полноты картины давайте также рассмотрим самый наивный подход, когда мы загружаем эти файлы по одному.

1. Последовательный

В приведенном выше фрагменте кода мы загружаем десять случайных изображений из unsplash с помощью пакета Python requests, а также регистрируем общее время, необходимое для выполнения операции. Больше ничего не нужно, поэтому я покажу вам вывод приведенного выше фрагмента (вывод, наблюдаемый в моем случае, может отличаться от того, что вы получите в зависимости от конфигурации вашей системы и скорости сети).

Downloading the images sequentially...
image 0 shape: (300, 300)
image 1 shape: (300, 300)
image 2 shape: (300, 300)
image 3 shape: (300, 300)
image 4 shape: (300, 300)
image 5 shape: (300, 300)
image 6 shape: (300, 300)
image 7 shape: (300, 300)
image 8 shape: (300, 300)
image 9 shape: (300, 300)
elapsed: 18.88 seconds

Всего на загрузку всех изображений ушло 19 секунд. Это не так уж хорошо. Давайте переключим передачу и рассмотрим следующий вариант для нас.

2. Многопроцессорность

Чтобы придерживаться темы этой статьи, я не буду вдаваться в подробности многопроцессорности в python, но на высоком уровне это означает порождение нескольких различных процессов на вашем ЦП, которые могут выполнять вычисления отдельно и параллельно. Эти процессы имеют свою собственную выделенную память (ОЗУ) и не обращают внимания на существование друг друга, в то время как родительский процесс управляет ими от вашего имени для сопоставления и сбора выходных данных и другой информации по мере необходимости.

Когда я запускаю вышеуказанное, я получаю вывод ниже:

downloading images using multi-processing...
image 3 shape: (300, 300)
image 2 shape: (300, 300)
image 1 shape: (300, 300)
image 0 shape: (300, 300)
image 4 shape: (300, 300)
image 6 shape: (300, 300)
image 5 shape: (300, 300)
image 7 shape: (300, 300)
image 8 shape: (300, 300)
image 9 shape: (300, 300)
elapsed: 5.67 seconds

Вау! С 19 секунд наше время выполнения сократилось до 6 секунд. Это ускорение более чем в 3 раза. Здесь следует отметить одну деталь: для этого примера я создаю четыре процесса. Вы можете поиграть и попробовать различное количество процессов и проверить, сможете ли вы еще больше сократить время. Однако помните, что большее количество процессов не всегда означает более высокую производительность, так как существуют накладные расходы на создание и внутреннюю организацию этих процессов. Хорошо, мы могли бы сделать лучше, чем это? Давайте выясним.

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

3. Многопоточность

Наш третий вариант — использовать потоки вместо процессов. Темы немного более абстрактны и их сложнее объяснить. Как и выше, мы не будем углубляться в определение или объяснение тем. Однако стоит отметить, что в отличие от процессов, потоки не обязательно имеют отдельную память для них, и они часто делят ресурсы между собой. Это усложняет реализацию многопоточных программ на python и может легко привести к запутанным результатам, если сделать это неправильно.

Однако для нашей простой программы нам не нужно беспокоиться о таком непоследовательном поведении. Итак, приступим к реализации.

Когда я запускаю этот код, я получаю вывод, как показано ниже.

downloading images using multi-threading...
image 1 shape: (300, 300)
image 2 shape: (300, 300)
image 3 shape: (300, 300)
image 0 shape: (300, 300)
image 6 shape: (300, 300)
image 7 shape: (300, 300)
image 4 shape: (300, 300)
image 5 shape: (300, 300)
image 9 shape: (300, 300)
image 8 shape: (300, 300)
elapsed: 3.88 seconds

Это на 33% больше ускорения по сравнению с многопроцессорной обработкой. Кстати, вы обратили внимание на порядок вывода? В отличие от последовательного выполнения, многопроцессорность и многопоточность не обязательно гарантируют порядок вывода. Это то, что нужно помнить при использовании этих методов в вашей программе.

Хорошо, я строил свое дело до сих пор. Теперь давайте посмотрим, о чем идет речь на самом деле. Давайте возьмем асинхронность в качестве нашего последнего варианта.

4. асинкио

Выше приведен эквивалентный код для загрузки этих изображений с использованием асинхронного программирования. На этот раз мы больше не могли использовать функцию download_random_image, которую использовали во всех других реализациях. Причина в том, что эта функция использует библиотеку requests, которая не поддерживает асинхронные вызовы. Вместо этого мы используем другую библиотеку aiohttp, которая позволяет выполнять загрузку асинхронно.

Если вы не совсем понимаете два последних утверждения, ничего страшного. На самом деле, я не хочу, чтобы вы слишком много внимания уделяли самому коду. Здесь мы просто демонстрируем вариант использования и сравниваем различные подходы. Как только вы поймете основы asyncio, они автоматически обретут смысл.

Хорошо, давайте запустим код выше, хорошо?

Downloading images using async...
image 1 shape: (300, 300)
image 7 shape: (300, 300)
image 3 shape: (300, 300)
image 4 shape: (300, 300)
image 9 shape: (300, 300)
image 6 shape: (300, 300)
image 2 shape: (300, 300)
image 8 shape: (300, 300)
image 10 shape: (300, 300)
image 5 shape: (300, 300)
elapsed: 1.16 seconds

Хайп настоящий. Все еще не доверяете мне? Попробуйте выполнить код и убедитесь в этом сами. Мы получили 4-кратное ускорение по сравнению с многопоточностью и 19-кратное ускорение по сравнению с последовательным кодом. Это безумие, не так ли? Но что делает асинхронное программирование таким мощным? Да, кстати, я упоминал, что приведенный выше код выполняется в одном процессе и одном потоке? 🤯

Мы раскроем секреты такой производительности в следующей части этого поста. Следите за обновлениями.