Архив рубрики ~Лента новостей~

3 хитрости NumPy для повышения производительности численных вычислений

3 хитрости NumPy для повышения производительности численных вычислений
3 хитрости NumPy для повышения производительности численных вычислений

В этой статье мы рассмотрим три важных приема NumPy для оптимизации вашего кода: векторизация и широковещательная рассылка, операции на месте и использование представлений памяти вместо копирования.

3 хитрости NumPy для повышения производительности численных вычислений

# Введение

Экосистема научных вычислений и машинного обучения на Python в значительной степени опирается на NumPy . Он выступает в качестве механизма повышения производительности таких библиотек, как Pandas, Scikit-Learn, SciPy и PyTorch. Скорость NumPy обусловлена его базовой реализацией на оптимизированном языке C, где непрерывные блоки памяти обрабатываются без накладных расходов, связанных с объектной моделью Python и динамическим интерпретатором.

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

В этой статье мы рассмотрим три важных приема NumPy для оптимизации вашего кода:

  • векторизация и вещание
  • операции на месте с использованием параметра out
  • использование представлений памяти вместо копий

# 1. Векторизация и широковещательная передача через явные циклы

Явные циклы for в Python — главный фактор, замедляющий вычислительные процессы. Итерация по элементам структуры данных заставляет интерпретатор Python выполнять проверку типов и поиск методов на каждом шаге.

Распространенная ошибка — использование np.vectorize. Многие разработчики считают, что обертывание стандартной функции Python с помощью np.vectorize преобразует ее в оптимизированный код на C. В действительности, np.vectorize — это всего лишь удобная обертка, которая запускает медленный стандартный цикл Python за более чистым API, не обеспечивая никаких преимуществ в производительности.

Для оптимизации необходимо писать код, используя нативные универсальные функции (ufuncs) и широковещательную рассылку. Широковещательная рассылка позволяет NumPy выполнять операции над массивами различной формы без копирования данных, обрабатывая операции непосредственно в скомпилированном коде C.

Этот наивный подход предполагает построчный и постолбцовый перебор двумерного массива для выполнения стандартизации по столбцам (вычитание среднего значения столбца и деление на стандартное отклонение столбца):

import numpy as np import time # Создание выборочной матрицы (50000 строк, 1000 столбцов) matrix = np.random.rand(50000, 1000) start_time = time.time() # Наивная нормализация столбцов на основе цикла res = matrix.copy() for col in range(matrix.shape[1]): col_mean = np.mean(matrix[:, col]) col_std = np.std(matrix[:, col]) for row in range(matrix.shape[0]): res[row, col] = (matrix[row, col] — col_mean) / col_std duration_loop = time.time() — start_time print(f»Вложенный цикл обработал матрицу за: {duration_loop:.4f} секунд»)

Выход:

Обработка матрицы вложенным циклом заняла: 10,9986 секунд

Вместо циклов мы вычисляем среднее значение и стандартное отклонение вдоль вертикальной оси (ось = 0). NumPy автоматически выравнивает эти одномерные сводные статистические данные со строками двумерной матрицы с помощью широковещательной рассылки:

import numpy as np import time # Создание выборочной матрицы (50000 строк, 1000 столбцов) matrix = np.random.rand(50000, 1000) start_time = time.time() # Вычисление средних значений и стандартных отклонений вдоль оси 0 в скомпилированном C means = np.mean(matrix, axis=0) stds = np.std(matrix, axis=0) # Пусть широковещательная рассылка автоматически расширяет формы и вычисляет в одной строке res_vectorized = (matrix — means) / stds duration_vectorized = time.time() — start_time print(f»Обработка векторизованной широковещательной рассылки матрицы за: {duration_vectorized:.4f} секунд»)

Выход:

Векторизованная матрица, обработанная в процессе вещания, за 0,1972 секунды.

Это примерно 56-кратное ускорение!

В векторизованной реализации операции с матрицей — вычисления средних значений и последующее деление на стандартные отклонения — выполняются с использованием правил широковещательной рассылки NumPy. Поскольку матрица имеет форму (50000, 1000), а массив средних значений — форму (1000,), NumPy концептуально расширяет массив средних значений, чтобы он соответствовал форме матрицы. Внутри системы это расширение происходит мгновенно в памяти без дублирования данных, а вычисления переносятся на инструкции ЦП SIMD (Single Instruction, Multiple Data), что обеспечивает колоссальное ускорение более чем в 50 раз.

# 2. Операции на месте и выходные параметры

Когда вы пишете выражения типа y = 2 * x + 3, вы можете ожидать, что они будут выполняться эффективно. Однако на самом деле NumPy вычисляет это выражение пошагово:

  1. Она выделяет во временной памяти массив для хранения результата вычисления 2 * x.
  2. Она выделяет дополнительный массив для хранения результата добавления 3 к временному массиву.
  3. В итоге, этот второй временный массив связывается с переменной с именем y.

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

Мы можем избежать этих накладных расходов, выполняя вычисления на месте с помощью операторов, таких как *= и +=, или используя параметр out, встроенный почти во все универсальные функции NumPy.

Этот наивный метод выполняет базовое линейное масштабирование большого массива, что приводит к многократному временному выделению памяти:

import numpy as np import time # Создаем большой одномерный массив из 10 миллионов элементов x = np.random.rand(10000000) scale = 2.5 offset = 1.2 start_time = time.time() # Стандартная цепочка вычислений создает временные промежуточные массивы y_naive = scale * x + offset duration_naive = time.time() — start_time print(f»Цепочка вычислений выполнена за: {duration_naive:.4f} секунд»)

Выход:

Выполнение цепочки выражений заняло: 0,0393 секунды

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

import numpy as np import time # Создание большого одномерного массива из 10 миллионов элементов x = np.random.rand(10000000) scale = 2.5 offset = 1.2 start_time = time.time() # Предварительное выделение памяти для итогового массива y_optimized = np.empty_like(x) # Выполнение математических операций непосредственно в целевой буфер без промежуточных переменных np.multiply(x, scale, out=y_optimized) np.add(y_optimized, offset, out=y_optimized) duration_optimized = time.time() — start_time print(f»Оптимизированное выражение, выполненное на месте, за: {duration_optimized:.4f} секунд») print(f»Ускорение: {duration_naive / duration_optimized:.2f}x быстрее!»)

Выход:

Оптимизированное выражение, выполняемое на месте, за: 0,0133 секунды

В оптимизированном примере мы используем np.multiply(x, scale, out=y_optimized), чтобы записать результат умножения непосредственно в предварительно выделенный массив y_optimized. Затем np.add(y_optimized, offset, out=y_optimized) добавляет смещение и записывает результат обратно в тот же буфер. Это полностью исключает выделение и сборку мусора временных буферов, экономя системную память, сохраняя данные в кэше ЦП и повышая скорость выполнения.

# 3. Представления в памяти против копирования в памяти (разделение данных против расширенного индексирования)

Понимание того, когда NumPy возвращает представление массива, а когда — его копию, является одной из важнейших тем в численном программировании:

  • Представление — это новый объект массива, который указывает на тот же самый базовый буфер данных, что и исходный массив. Создание представления — это операция без копирования, которая выполняется за постоянное время и с использованием постоянного пространства $O(1)$.
  • Копирование выделяет совершенно новый буфер данных и дублирует данные. Этот процесс выполняется за $O(N)$ линейное время и пространство.

Базовая нарезка (с использованием индексов начала, конца и шага, например, arr[0:10:2]) всегда возвращает представление. В отличие от этого, расширенная индексация (с использованием списков индексов или булевых масок, например, arr[[0, 2, 4]]) всегда возвращает копию.

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

Здесь мы пытаемся выполнить выборку из большой двумерной матрицы (каждая вторая строка и каждый второй столбец), передавая списки индексов. Это заставляет NumPy выделить большой новый массив и скопировать все его элементы:

import numpy as np import time # Создание матрицы размером 10 000 x 10 000 элементов matrix = np.random.rand(10000, 10000) start_time = time.time() # Расширенная индексация с использованием целочисленных массивов принудительно создает физическое копирование данных rows = np.arange(0, matrix.shape[0], 2) cols = np.arange(0, matrix.shape[1], 2) sub_matrix_copy = matrix[rows[:, None], cols] duration_copy = time.time() — start_time print(f»Копирование с помощью расширенной индексации завершено за: {duration_copy:.4f} секунд»)

Выход:

Расширенное индексирование завершено за: 0,1575 секунды

Теперь выполним ту же операцию, но с использованием базового среза. Вместо копирования данных NumPy мгновенно корректирует метаданные шага, указывая на тот же буфер:

import numpy as np import time # Создание матрицы размером 10 000 x 10 000 элементов matrix = np.random.rand(10000, 10000) start_time = time.time() # Базовая нарезка мгновенно возвращает представление без копирования sub_matrix_view = matrix[::2, ::2] duration_view = time.time() — start_time print(f»Базовая нарезка завершена за: {duration_view:.8f} секунд»)

Выход:

Базовый режим нарезки данных выполнен за: 0,00001001 секунды

При нарезке массива с помощью matrix[::2, ::2] NumPy не затрагивает базовый буфер данных. Он просто создает новый заголовок массива с измененными метаданными: другой формой и новыми шагами (количеством байтов, на которое нужно сделать шаг в каждом измерении, чтобы найти следующий элемент). Эта операция выполняется менее чем за микросекунду, независимо от размера матрицы.

Однако следует учитывать компромисс: поскольку представление использует один и тот же буфер памяти, изменение sub_matrix_view также изменит исходную матрицу. Если вам необходимо избежать изменения исходного массива, необходимо явно вызвать метод .copy().

# Завершение

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

Подведем итог:

  • Забудьте о циклах Python и np.vectorize и позвольте векторизованной широковещательной передаче перенести вычисления в оптимизированный C.
  • Используйте операции на месте и параметр out, чтобы обойти распределитель памяти, предотвращая переполнение кэша и снижая потребление оперативной памяти.
  • Использование мастер-представлений вместо копий позволяет мгновенно создавать фрагменты без копирования, вместо дорогостоящих сложных индексных копий.

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

Мэтью Мэйо ( @mattmayo13 ) имеет степень магистра компьютерных наук и диплом специалиста по анализу данных. Будучи главным редактором KDnuggets & Statology и внештатным редактором Machine Learning Mastery, Мэтью стремится сделать сложные концепции науки о данных доступными для всех. В сферу его профессиональных интересов входят обработка естественного языка, языковые модели, алгоритмы машинного обучения и изучение новых технологий искусственного интеллекта. Его движет стремление демократизировать знания в сообществе специалистов по науке о данных. Мэтью занимается программированием с 6 лет.

Источник: www.kdnuggets.com

✅ Найденные теги: 3, NumPy, новости, Повышения, Производительности, Хитрости, Численных
Читайте также
Архив рубрики ~Обо всем~ Я думал, что разработка данных — это просто написание скриптов. Я ошибался. Архив рубрики ~Обо всем~ Механизмы нарушения регуляции эмоций при биполярном расстройстве Архив рубрики ~Обо всем~ Удостоенный наград исследователь обучил роботов делать обоснованные предположения. Архив рубрики ~Полезное~ Midjourney для чайников за пару минут Архив рубрики ~Полезное~ Как нейросети “понимают” команды: механика Prompt Engineering простыми словами Архив рубрики ~Обо всем~ Электрическое поле подавило температурные пульсации в пламени метана: Физика Архив рубрики ~Обо всем~ Подсказки, ответы и помощь по Wordle за 12 июня, #1819 Архив рубрики ~Обо всем~ Прекратите возвращать плоский текст из PDF-файлов: реляционная структура, необходимая для RAG. Архив рубрики ~Обо всем~ Нейробиология секса. Главное, что нужно знать? Архив рубрики ~Обо всем~ Практический опыт Stranger than Heaven: Сложнее, чем Yakuza? Архив рубрики ~Обо всем~ IPO компании SpaceX: все, что вам нужно знать. Архив рубрики ~Обо всем~ Несмотря на вмешательство США, репрессии против технологических платформ будут продолжены, заявили в канцелярии премьер-министра. Архив рубрики ~Обо всем~ Газета утверждает, что «астероид», к которому приближается японский зонд, на самом деле является заброшенным космическим кораблем Архив рубрики ~Идей копилка~ AI-агент персонального здоровья: как носимые устройства и искусственный интеллект меняют предиктивную медицину Архив рубрики ~Обо всем~ Я думал, что разработка данных — это просто написание скриптов. Я ошибался. Архив рубрики ~Обо всем~ Механизмы нарушения регуляции эмоций при биполярном расстройстве Архив рубрики ~Обо всем~ Удостоенный наград исследователь обучил роботов делать обоснованные предположения. Архив рубрики ~Полезное~ Midjourney для чайников за пару минут Архив рубрики ~Полезное~ Как нейросети “понимают” команды: механика Prompt Engineering простыми словами Архив рубрики ~Обо всем~ Электрическое поле подавило температурные пульсации в пламени метана: Физика Архив рубрики ~Обо всем~ Подсказки, ответы и помощь по Wordle за 12 июня, #1819 Архив рубрики ~Обо всем~ Прекратите возвращать плоский текст из PDF-файлов: реляционная структура, необходимая для RAG. Архив рубрики ~Обо всем~ Нейробиология секса. Главное, что нужно знать? Архив рубрики ~Обо всем~ Практический опыт Stranger than Heaven: Сложнее, чем Yakuza? Архив рубрики ~Обо всем~ IPO компании SpaceX: все, что вам нужно знать. Архив рубрики ~Обо всем~ Несмотря на вмешательство США, репрессии против технологических платформ будут продолжены, заявили в канцелярии премьер-министра. Архив рубрики ~Обо всем~ Газета утверждает, что «астероид», к которому приближается японский зонд, на самом деле является заброшенным космическим кораблем Архив рубрики ~Идей копилка~ AI-агент персонального здоровья: как носимые устройства и искусственный интеллект меняют предиктивную медицину

Оставить комментарий