8.2. Вычислительная производительность

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

Здесь мы рассмотрим порядки величин, которые можно ожидать от ряда модели scikit-learn в различных контекстах, а также дадим несколько советов и рекомендаций по преодолению узких мест в производительности.

Латентность предсказания измеряется как время, необходимое для выполнения предсказания (например, в микросекундах). Задержка часто рассматривается как распределение, и операционные инженеры часто фокусируются на задержке в определенном процентиле этого распределения (например, в 90 процентиле).

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

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

8.2.1. Латентность прогнозирования

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

Основными факторами, влияющими на задержку предсказаний, являются

  1. Количество признаков

  2. Представление и разреженность входных данных

  3. Сложность модели

  4. Извлечение признаков

Последним важным параметром также является возможность выполнять предсказания в массовом или атомарном режиме.

8.2.1.1. Массовый и атомарный режим

В общем случае выполнение прогнозов в массовом режиме (много экземпляров одновременно) более эффективно по ряду причин (предсказуемость ветвлений, кэш процессора, оптимизация библиотек линейной алгебры и т. д.). Здесь мы видим на сеттинге с небольшим количеством признаков, что независимо от выбора модели массовый режим всегда быстрее, а для некоторых из них на 1-2 порядка:

atomic_prediction_latency

bulk_prediction_latency

Чтобы сравнить различные модели для вашего случая, вы можете просто изменить параметр n_features в этом примере: Prediction Latency. Это должно дать вам оценку порядка величины задержки предсказания.

8.2.1.2. Настройка Scikit-learn для уменьшения накладных расходов на проверку

Scikit-learn выполняет некоторые проверки данных, что увеличивает накладные расходы на вызов predict и подобных функций. В частности, проверка того, что признаки конечны (не NaN или бесконечны), включает полный проход по данным. Если вы уверены, что ваши данные приемлемы, вы можете подавить проверку на конечность, установив переменную окружения SKLEARN_ASSUME_FINITE в непустую строку перед импортом scikit-learn, или настроив ее в Python с помощью set_config. Для большего контроля, чем эти глобальные настройки, config_context позволяет вам установить эту конфигурацию в указанном контексте:

>>> import sklearn
>>> with sklearn.config_context(assume_finite=True):
...     pass  # do learning/prediction here with reduced validation

Обратите внимание, что это повлияет на все использования assert_all_finite в контексте.

8.2.1.3. Влияние количества признаков

Очевидно, что при увеличении количества признаков растет и потребление памяти каждым примером. Действительно, для матрицы из \(M\) экземпляров с \(N\) признаками пространственная сложность составляет \(O(NM)\). С точки зрения вычислений это также означает, что количество базовых операций (например, умножений для векторно-матричных произведений в линейных моделях) также увеличивается. Вот график изменения времени предсказания в зависимости от количества признаков:

influence_of_n_features_on_latency

В целом можно ожидать, что время предсказания будет увеличиваться по крайней мере линейно с ростом числа признаков (возможны нелинейные случаи, зависящие от объема глобальной памяти и модели).

8.2.1.4. Влияние представления входных данных

Scipy предоставляет структуры данных с разреженными матрицами, которые оптимизированы для хранения разреженных данных. Главная особенность разреженных форматов заключается в том, что вы не храните нули, поэтому если ваши данные разрежены, то вы используете гораздо меньше памяти. Ненулевое значение в разреженном (CSR или CSC) представлении будет занимать в среднем только одну 32-битную целочисленную позицию + 64-битное значение с плавающей точкой + дополнительные 32 бита на строку или столбец матрицы. Использование разреженных входных данных в плотной (или разреженной) линейной модели может значительно ускорить предсказание, поскольку только ненулевые признаки влияют на точечное произведение и, следовательно, на предсказания модели. Таким образом, если у вас есть 100 ненулевых признаков в пространстве размерности 1e6, вам потребуется только 100 операций умножения и сложения вместо 1e6.

Вычисления над плотным представлением, однако, могут использовать высоко оптимизированные векторные операции и многопоточность в BLAS, и, как правило, приводят к меньшему количеству пропусков кэша процессора. Поэтому разреженность обычно должна быть довольно высокой (максимум 10 % ненулей, проверяется в зависимости от аппаратного обеспечения), чтобы разреженное входное представление было быстрее плотного входного представления на машине с большим количеством процессоров и оптимизированной реализацией BLAS.

Вот пример кода для проверки разреженности входных данных:

def sparsity_ratio(X):
    return 1.0 - np.count_nonzero(X) / float(X.shape[0] * X.shape[1])
print("input sparsity ratio:", sparsity_ratio(X))

В качестве эмпирического правила можно считать, что если коэффициент разреженности превышает 90%, то вы, вероятно, можете извлечь выгоду из разреженных форматов. Ознакомьтесь с документацией Scipy по форматам разреженных матриц <https://docs.scipy.org/doc/scipy/reference/sparse.html>`_ для получения дополнительной информации о том, как создать (или преобразовать ваши данные в) форматы разреженных матриц. Чаще всего лучше всего работают форматы CSR и CSC.

8.2.1.5. Влияние сложности модели

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

Для sklearn.linear_model (например, Lasso, ElasticNet, SGDClassifier/Regressor, Ridge & RidgeClassifier, PassiveAggressiveClassifier/Regressor, LinearSVC, LogisticRegression…) функция принятия решения, которая применяется во время предсказания, одинакова (точечное произведение), поэтому латентность должна быть эквивалентной.

Вот пример использования SGDClassifier со штрафом elasticnet. Сила регуляризации глобально контролируется параметром alpha. При достаточно высоком alpha можно увеличивать параметр l1_ratio у elasticnet для обеспечения различных уровней разреженности в коэффициентах модели. Большая разреженность здесь интерпретируется как меньшая сложность модели, поскольку нам нужно меньше коэффициентов для ее полного описания. Конечно, разреженность в свою очередь влияет на время предсказания, поскольку разреженный точечный вывод занимает время, примерно пропорциональное количеству ненулевых коэффициентов.

en_model_complexity

Для семейства алгоритмов sklearn.svm с нелинейным ядром время ожидания зависит от количества опорных векторов (чем меньше, тем быстрее). Задержка и пропускная способность должны (асимптотически) линейно расти с числом опорных векторов в модели SVC или SVR. Ядро также влияет на время ожидания, поскольку оно используется для вычисления проекции входного вектора один раз на каждый опорный вектор. На следующем графике параметр nu из NuSVR` был использован для влияния на количество опорных векторов.

nusvr_model_complexity

Для sklearn.ensemble деревьев (например, RandomForest, GBT, ExtraTrees и т.д.) наиболее важную роль играет количество деревьев и их глубина. Задержка и пропускная способность должны линейно изменяться в зависимости от количества деревьев. В данном случае мы использовали непосредственно параметр n_estimators класса GradientBoostingRegressor.

gbt_model_complexity

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

8.2.1.6. Латентность извлечения признаков

Большинство моделей scikit-learn обычно работают довольно быстро, поскольку они реализованы либо в скомпилированных расширениях Cython, либо в оптимизированных вычислительных библиотеках. С другой стороны, во многих реальных приложениях процесс извлечения признаков (т.е. превращение необработанных данных, таких как строки базы данных или сетевые пакеты, в массивы numpy) определяет общее время предсказания. Например, в задаче классификации текстов Reuters вся подготовка (чтение и парсинг SGML-файлов, токенизация текста и хэширование его в общее векторное пространство) занимает в 100-500 раз больше времени, чем собственно код предсказания, в зависимости от выбранной модели.

prediction_time

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

8.2.2. Пропускная способность предсказания

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

throughput_benchmark

Эти показатели достигнуты на одном процессе. Очевидный способ увеличить пропускную способность вашего приложения - породить дополнительные экземпляры (обычно процессы в Python из-за GIL), которые используют одну и ту же модель. Можно также добавить машины, чтобы распределить нагрузку. Подробное объяснение того, как этого добиться, выходит за рамки данной документации.

8.2.3. Советы и трюки

8.2.3.1. Библиотеки линейной алгебры

Поскольку scikit-learn сильно зависит от Numpy/Scipy и линейной алгебры в целом, имеет смысл явно позаботиться о версиях этих библиотек. В основном, вы должны убедиться, что Numpy собран с использованием оптимизированной библиотеки BLAS / LAPACK.

Не все модели выигрывают от оптимизированных реализаций BLAS и Lapack. Например, модели, основанные на (рандомизированных) деревьях решений, обычно не используют вызовы BLAS в своих внутренних циклах, как и ядровые SVM (SVC, SVR, NuSVC, NuSVR). С другой стороны, линейная модель, реализованная с помощью вызова BLAS DGEMM (через numpy.dot), обычно получает огромную пользу от настроенной реализации BLAS и приводит к ускорению на порядки по сравнению с неоптимизированным BLAS.

Вы можете посмотреть реализацию BLAS / LAPACK, используемую вашей установкой NumPy / SciPy / scikit-learn, с помощью следующей команды:

python -c "import sklearn; sklearn.show_versions()"

Оптимизированные реализации BLAS / LAPACK включают:

  • Atlas (need hardware specific tuning by rebuilding on the target machine)

  • OpenBLAS

  • MKL

  • Apple Accelerate and vecLib frameworks (OSX only)

Дополнительную информацию можно найти на странице установки NumPy <https://numpy.org/install/>`_ и в этом посте в блоге <https://danielnouri.org/notes/2012/12/19/libblas-and-liblapack-issues-and-speed,-with-scipy-and-ubuntu/>`_ от Daniel Nouri, где есть несколько хороших пошаговых инструкций по установке для Debian / Ubuntu.

8.2.3.2. Ограничение рабочей памяти

Некоторые вычисления при использовании стандартных векторизованных операций numpy требуют использования большого количества временной памяти. Это может привести к исчерпанию системной памяти. Там, где вычисления могут быть выполнены в фиксированных кусках памяти, мы пытаемся это сделать, и позволяем пользователю указать максимальный размер этой рабочей памяти (по умолчанию 1 ГБ) с помощью set_config или config_context. Ниже предлагается ограничить временную рабочую память до 128 MiB:

>>> import sklearn
>>> with sklearn.config_context(working_memory=128):
...     pass  # do chunked work here

Примером операции chunked, придерживающейся этой настройки, является pairwise_distances_chunked, которая облегчает вычисление сокращений матрицы парных расстояний по строкам.

8.2.3.3. Сжатие моделей

Сжатие моделей в scikit-learn пока что касается только линейных моделей. В данном контексте это означает, что мы хотим контролировать разреженность модели (т.е. количество ненулевых координат в векторах модели). Как правило, хорошей идеей является сочетание разреженности модели с разреженным представлением входных данных.

Вот пример кода, иллюстрирующий использование метода sparsify():

clf = SGDRegressor(penalty='elasticnet', l1_ratio=0.25)
clf.fit(X_train, y_train).sparsify()
clf.predict(X_test)

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

Типичный бенчмарк на синтетических данных дает >30% снижение задержки, когда модель и входные данные разрежены (с отношением ненулевых коэффициентов 0.000024 и 0.027400 соответственно). Ваш пробег может варьироваться в зависимости от разреженности и размера ваших данных и модели. Кроме того, разряженность может быть очень полезен для снижения потребления памяти прогностическими моделями, развернутыми на производственных серверах.

8.2.3.4. Изменение формы модели

Изменение формы модели заключается в выборе только части доступных признаков для подгонки модели. Другими словами, если модель отбрасывает признаки на этапе обучения, мы можем удалить их из входных данных. Это дает несколько преимуществ. Во-первых, это сокращает объем памяти (и, следовательно, время) самой модели. Кроме того, это позволяет отказаться от явных компонентов выбора признаков в конвейере, когда мы знаем, какие признаки нужно сохранить с предыдущего запуска. Наконец, это может помочь сократить время обработки и использование ввода-вывода на уровнях доступа к данным и извлечения признаков, поскольку не нужно собирать и строить признаки, которые отбрасываются моделью. Например, если исходные данные поступают из базы данных, это может позволить писать более простые и быстрые запросы или сократить использование ввода-вывода, заставляя запросы возвращать меньше записей. В настоящее время в scikit-learn изменение формы необходимо выполнять вручную. В случае разреженных данных (особенно в формате CSR), как правило, достаточно не генерировать соответствующие признаки, оставляя их столбцы пустыми.

8.2.3.5. Ссылки