Я хотел бы выразить огромную благодарность Майклу Гранту и Джиму Кристу из Anaconda, Inc. за внесение многих основных идей в этот пост, а также за многочисленные полезные и содержательные обсуждения.

Введение. Что такое даск и нумба?

Специалисты по обработке данных часто работают с неудобно большими наборами данных, т. Е. Данными, наивные попытки манипулирования которыми утомляют доступные ресурсы памяти. В этих условиях у нас может возникнуть соблазн использовать некоторую форму распределенных вычислений; однако преждевременный переход к распределенной среде может повлечь за собой большие затраты, а иногда даже снижение производительности по сравнению с хорошо реализованными решениями на одной машине. В этом посте мы рассмотрим два инструмента на основе Python, dask и numba, для реализации конвейеров скоринга модели для одной машины в случае, когда существует много возможных выходов модели и ресурсы могут быть ограничены.

И dask, и numba - это библиотеки Python для оптимизации вычислений.

  • Numba позволяет выполнять компиляцию функций для оптимизации одномашинного кода. Это означает, что если вы намереваетесь вызывать функцию несколько раз, вы можете значительно сократить время вычислений, скомпилировав функцию при первом вызове. Таким образом, numba полезна для ускорения отдельных задач.
  • Dask рекламируется как библиотека параллельных вычислений для крупномасштабных (как правило, внеядерных или распределенных) вычислений. В основе dask лежит серия планировщиков задач - алгоритмов для определения того, когда и как запускать различные определяемые пользователем вычислительные задачи; следовательно, dask может автоматически определять, какие задачи можно запускать параллельно, а какие не запускать вообще. Использование планировщиков dask позволяет нам масштабировать до сети из множества взаимосвязанных задач и эффективно вычислять только те выходные данные, которые нам нужны, даже на одной машине.

Мы увидим, что включение как dask, так и numba в наш код Python может быть таким же простым, как применение декораторов функций к нашим ранее существовавшим функциям. Таким образом, эти библиотеки могут быть объединены мощными способами для создания эффективных рабочих процессов обработки данных, которые интегрируются с общим стеком SciPy и не требуют дополнительных инструментов или архитектуры.

В частности, здесь мы сосредоточимся на оценке модели для одной машины (а не на подборе модели), которая представляет собой процесс вычисления прогнозов модели с учетом соответствующего набора входных данных. Например, в финансовых услугах может быть большое количество моделей, которые строятся друг над другом, большое количество изменяющихся во времени выходных данных, связанных с каждой моделью, и большое количество возможных «наложений», которые можно включать / выключать. Может быть сложно написать код, который был бы одновременно эффективным на уровне отдельной модели и эффективным, когда нужно запускать несколько моделей одновременно, но с dask и numba когнитивные издержки минимальны.

Мы начнем с простого введения в numba guvectorize, который мы используем для ускорения оценки модели для отдельной модели. Для этого обсуждения полезно знакомство с декораторами функций в Python и концепцией компиляции кода. Затем мы представляем нашу единую модель в более широком контексте, в котором есть несколько моделей и этапов создания функций, которые, возможно, придется выполнять параллельно. Для этого мы вводим dask.delayed и демонстрируем, как использование планировщика dask приводит к вычислительной эффективности. Все приведенные здесь примеры являются игрушечными, а представленные эксперименты по времени предназначены для выделения возможностей - очевидно, что прирост производительности сильно зависит от приложения.

Весь код предназначен для копирования / вставки в сеанс IPython. Если вы заинтересованы в повторном запуске любого кода, мы использовали следующие версии пакетов:

dask version: 0.14.1
numba version: 0.34.0
numpy version: 1.13.1

число

Начнем с простого случая: одна модель, которая получает 4 входа и возвращает 15 выходов. Для контекста представьте, что мы прогнозируем количество, которое меняется со временем, и эта функция дает прогнозы на следующие 15 месяцев.

При первом вызове функции numba компилирует функцию в машинный код (j ust i n t ime ), так что последующие вызовы будут значительно более эффективными. Чтобы продемонстрировать простое использование jit (полную документацию можно найти здесь):

Мы видим, что в этом случае jit обеспечивает нам ускорение в 1,3 раза для оценки 25 000 наблюдений без каких-либо существенных изменений в нашем коде. Обратите внимание, что для правильной компиляции numba необходимо определить типы входных данных. В этом случае jit будет определять типы во время выполнения и соответственно компилировать; в качестве альтернативы мы могли бы предоставить сигнатуру типа, как мы вскоре увидим.

Наблюдательный читатель может продвинуться дальше: мы выполняем одни и те же вычисления для каждого элемента введенных массивов numpy, есть ли способ сказать numba, чтобы он использовал это в своих интересах, аналогично вызову .apply()? На самом деле есть; Декоратор guvectorize numba позволяет нам писать код, который работает построчно, и он будет соответствующим образом реализовывать вычисления. Давайте посмотрим, как выглядит применение декоратора guvectorize в нашем примере:

Есть несколько отличий от guvectorize, которые мы должны выделить; Первое, что нужно заметить, это то, что декоратору guvectorize требуются два аргумента: подпись типа и подпись формы. В этом случае наша сигнатура типа включает 64-битные целые числа, 64-битные числа с плавающей запятой, массивы 64-битных чисел с плавающей запятой и логическое значение (i8, f8, f8[:] и b1 соответственно). Если мы когда-нибудь вызовем нашу функцию со значениями, которые нельзя принудить, будет выброшено TypeError.

Сигнатура формы сообщает numba относительные формы входных данных, где () означает скаляр. Здесь есть несколько технических замечаний:

· необходимо, чтобы каждое дополнительное выходное измерение имело хотя бы один вход с той же формой, что и желаемый выход; например, в приведенном выше примере у нас фактически нет ввода с 15 столбцами, но наш результат должен содержать 15 столбцов. Поэтому нам нужно предварительно выделить форму вывода перед вызовом fast_predict_over_time и использовать предварительно выделенный массив в качестве одного из наших аргументов (соответствующий подчеркиванию выше).

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

· Кроме того, обратите внимание, что функция не имеет оператора возврата. Это связано с тем, как numba компилирует код под капотом, и выходит за рамки этой публикации. Давайте посмотрим, как это работает на практике:

Использование guvectorize в этом случае дает нам ускорение в 3,5 раза по сравнению с jit и ускорение в 5 раз по сравнению со стандартной реализацией, и все это без выхода из Python! Более того, обратите внимание, что наш код написан с «семантикой уровня наблюдения», т.е. мы пишем функцию так, как если бы она получала входные данные от одного наблюдения. Таким образом, рассуждения о нашем коде кажутся естественными.

+= dask

Теперь мы представляем, что наша модель, представленная выше, интегрирована в гораздо более крупный конвейер моделирования; функции x, y и z созданы из других (возможно, сложных) задач, и есть другие модели, которые мы хотим оценить. Чтобы сделать обсуждение более конкретным, представьте, что у нас есть следующий рабочий процесс:

Каждый квадрат в этом графе задач представляет конкретный вход или конкретный выход; овалы представляют функции. Строчные буквы обозначают вводимые пользователем данные. Мы видим два этапа создания функций (feature_a и complicated_feature_b), которые вводятся в нашу fast_predict_over_time модель двумя разными способами с другими переменными. Существует также другая модель predict_another_thing, которая рассчитана на производство Output 1.

dask позволяет нам сначала настроить этот граф задач без оценки кода (все вычисления будут отложены). Всякий раз, когда нам нужен один (или все) из этих выходных данных, мы говорим dask вычислить его, и он определит наиболее эффективный способ продолжения. Эта вычислительная стратегия обычно называется «ленивым вычислением». Одним из основных преимуществ ленивых вычислений с использованием dask является то, что мы будем вычислять только то, что необходимо для нашего запрошенного вывода. Например, если мы запрашиваем вывод predict_another_thing на приведенном выше графике, то fast_predict никогда не будет вызван или вычислен. Для графиков постоянно увеличивающихся задач это может значительно сэкономить память и время.

Давайте сделаем это обсуждение более конкретным, показав, как мы можем отложить вычисление fast_predict_over_time с помощью dask и перейти к настройке других моделей:

Обратите внимание на небольшое увеличение времени выполнения; это вызвано небольшими накладными расходами, которые требуются для выполнения задачи. Различные планировщики dask имеют разные накладные расходы, по умолчанию используется многопоточный dask.threaded.get.

Переходя к другим моделям, мы можем объявить их как delayed вычислительные объекты с помощью delayed декоратора; Затем мы планируем фактические задачи, вызывая их, как и любую другую функцию. Единственная разница в том, что они не будут вычислены, пока мы не сделаем дополнительный вызов .compute():

Обратите внимание, что на этом этапе абсолютно никаких вычислений не производилось. Всякий раз, когда нам нужен результат от нашего механизма оценки модели, мы можем просто нажать results и вызвать .compute() (если нам нужно несколько выходов, мы можем использовать dask.compute(*args) верхнего уровня, который вернет кортеж запрошенных выходных данных). Например, давайте запросим в нашем графике задач w_overlay и predict_another_thing и посмотрим, что происходит со временем:

Обратите внимание, что время нашего вычисления намного меньше 1 минуты, необходимой для вычисления complicated_feature_b, потому что dask знал, что нам никогда не нужно его вычислять! Если бы мы написали здесь код процедурного стиля, было бы намного труднее избежать 1-минутного узкого места для complicated_feature_b. Это также снижает наши требования к памяти, поскольку нам никогда не нужно хранить все выходные данные в памяти.

Последние мысли

dask и numba позволяют нам значительно ускорить выполнение функций и создавать конвейеры с эффективным использованием памяти. Более того, это достигается без выхода из экосистемы Python и без изменения требований к нашей архитектуре. Конечно, это не надежные решения, которые ускорят все, что есть на полке - разумное приложение имеет решающее значение для написания производительного кода.

ЗАЯВЛЕНИЕ О РАСКРЫТИИ ИНФОРМАЦИИ: это мнение автора. Если в этом посте не указано иное, Capital One не связан и не одобрен ни одной из упомянутых компаний. Все используемые или отображаемые товарные знаки и другая интеллектуальная собственность являются собственностью соответствующих владельцев. Эта статья принадлежит © 2017 Capital One.