3 хитрости NumPy для повышения производительности численных вычислений
В этой статье мы рассмотрим три важных приема 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 вычисляет это выражение пошагово:
- Она выделяет во временной памяти массив для хранения результата вычисления 2 * x.
- Она выделяет дополнительный массив для хранения результата добавления 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
Похожие записи
- Французский Deezer запустил бесплатный инструмент для распознавания сгенерированной музыки на сторонних стримингах
- Я попросила Claude Fable 5 сделать игру одним промптом. Получился симулятор админа ИИ-канала
- Ногти поведали о тяжелой болезни потомка основателя империи Сун. Его состояние резко ухудшилось за десять недель до смерти
Похожие записи
🔎 Вышел конструктор ИИ-агентов для мониторинга всего интернета — Open…
01.01.2026
Новая политика США в области ИИ: отчаянная попытка вернуть единоличное лидерство в научно-технической революции
13.01.2026
