Наиболее эффективный способ применения функции к массиву NumPy
Какой самый эффективный способ применения функции к массиву numpy? В настоящее время я использую следующий код:
import numpy as np
x = np.array([1, 2, 3, 4, 5])
# Получаем массив квадратов каждого элемента в x
squarer = lambda t: t ** 2
squares = np.array([squarer(xi) for xi in x])
Тем не менее, это, вероятно, очень неэффективно, так как я использую списковое включение для создания нового массива в виде списка Python, прежде чем снова преобразовать его в массив numpy. Можем ли мы сделать это лучше?
4 ответ(ов)
В более новой версии (я использую 1.13) библиотеки NumPy вы можете просто вызвать функцию, передав в неё массив NumPy, который вы написали для скалярных типов. NumPy автоматически применит вызов функции к каждому элементу массива и вернёт вам новый массив.
Например:
>>> import numpy as np
>>> squarer = lambda t: t ** 2
>>> x = np.array([1, 2, 3, 4, 5])
>>> squarer(x)
array([ 1, 4, 9, 16, 25])
Таким образом, ваша функция squarer
будет применена ко всем элементам массива x
, и результат будет представлен в виде нового массива NumPy.
Все вышеуказанные ответы хорошо сопоставимы, но если вам нужно использовать пользовательскую функцию для отображения, и у вас есть numpy.ndarray
, и вам необходимо сохранить форму массива, то вот полезное сравнение.
Я протестировал только два подхода, но оба сохраняют форму ndarray
. Я использовал массив с 1 миллионом элементов для сравнения. В данном примере используется функция возведения в квадрат, которая также встроена в NumPy и обеспечивает отличную производительность. Если вам понадобится что-то другое, вы можете использовать функцию по вашему выбору.
import numpy, time
def timeit():
y = numpy.arange(1000000)
now = time.time()
# Вариант с использованием спискового включения
numpy.array([x * x for x in y.reshape(-1)]).reshape(y.shape)
print(time.time() - now)
now = time.time()
# Вариант с использованием numpy.fromiter
numpy.fromiter((x * x for x in y.reshape(-1)), y.dtype).reshape(y.shape)
print(time.time() - now)
now = time.time()
# Вариант с использованием встроенной функции
numpy.square(y)
print(time.time() - now)
Вывод
>>> timeit()
1.162431240081787 # списковое включение и дальнейшее создание numpy массива
1.0775556564331055 # использование numpy.fromiter
0.002948284149169922 # использование встроенной функции
Как видно из вывода, numpy.fromiter
показывает хорошую производительность по сравнению с простым подходом, но если вы можете воспользоваться встроенной функцией, то всегда лучше использовать её.
Используйте функцию numpy.fromfunction()
, чтобы создать массив, заполняя его значениями, генерируемыми заданной функцией. Аргументы этой функции включают function
, которая определяет, как будут вычисляться значения массива, shape
, которая задает форму создаваемого массива, и дополнительные параметры **kwargs
, которые могут быть переданы в функцию.
Пример использования:
import numpy as np
def func(i, j):
return i + j
# Создаем массив 3x3, где значения равны сумме индексов
array = np.fromfunction(func, (3, 3), dtype=int)
print(array)
В этом примере func
принимает два индекса (i и j), и создает 2D массив размером 3x3, где каждое значение равно сумме индексов. Полученный массив будет выглядеть так:
[[0 1 2]
[1 2 3]
[2 3 4]]
Вы можете найти больше информации о numpy.fromfunction
в документации NumPy.
Подход, который ещё не так распространен, но легко реализуется и работает быстро, — это Zig и Ziglang.
Установите пакет с помощью команды:
pip install ziglang
Создайте файл zinptest.zig
export fn npprod(inarray: usize, outarray: usize, lenarray: usize) void {
const inarraydata: [*]u64 = @ptrFromInt(inarray);
var outarraydata: [*]u64 = @ptrFromInt(outarray);
for (0..lenarray) |i| {
outarraydata[i] = inarraydata[i] * inarraydata[i];
}
}
export fn npprod2(inarray: usize, outarray: usize, lenarray: usize) void {
const inarraydata: [*]u64 = @ptrFromInt(inarray);
var outarraydata: [*]u64 = @ptrFromInt(outarray);
for (0..lenarray) |i| {
outarraydata[i] = inarraydata[i] + 2 * inarraydata[i] * inarraydata[i] + 4 * inarraydata[i] * inarraydata[i] * inarraydata[i];
}
}
Напишите обертку
import subprocess
import os
import sys
import ctypes
import numpy as np
# pip install ziglang
def compile_dll():
subprocess.run(
[
sys.executable,
"-m",
"ziglang",
"build-lib",
"zinptest.zig",
"-dynamic",
"-O",
"ReleaseFast",
],
shell=True,
env=os.environ,
cwd=this_folder,
)
def zigproduct(a):
out = np.empty_like(a)
inaddress = a.ctypes._arr.__array_interface__["data"][0] # сырой адрес памяти, будьте внимательны! (данные должны быть правильно выровнены)
outaddress = out.ctypes._arr.__array_interface__["data"][0]
lena = np.prod(a.shape)
zigprod(inaddress, outaddress, lena)
return out
def zigproduct2(a):
out = np.empty_like(a)
inaddress = a.ctypes._arr.__array_interface__["data"][0]
outaddress = out.ctypes._arr.__array_interface__["data"][0]
lena = np.prod(a.shape)
zigprod2(inaddress, outaddress, lena)
return out
def f(x):
return x + 2 * x * x + 4 * x * x * x
this_folder = os.path.dirname(__file__)
zigdll = os.path.normpath(os.path.join(this_folder, "zinptest.dll"))
if not os.path.exists(zigdll):
compile_dll()
# Первая функция Zig (**2)
zigdllloaded = ctypes.cdll.LoadLibrary(zigdll)
zigprod = zigdllloaded.npprod
zigprod.argtypes = [ctypes.c_uint64, ctypes.c_uint64, ctypes.c_uint64]
zigprod.restype = None
# Вторая функция Zig (x + 2 * x * x + 4 * x * x * x)
zigprod2 = zigdllloaded.npprod2
zigprod2.argtypes = [ctypes.c_uint64, ctypes.c_uint64, ctypes.c_uint64]
zigprod2.restype = None
a = np.arange(100000000, dtype=np.uint64)
Наслаждайтесь результатами
# Встроенные функции Numpy очень оптимизированы, здесь не так много, что можно улучшить
# In [3]: %timeit a**2
# 213 ms ± 1.85 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
# In [4]: %timeit zigproduct(a)
# 215 ms ± 1.01 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
# Теперь используем функцию от @ead return x+2*x*x+4*x*x*x
# In [1]: %timeit zigproduct2(a)
# 206 ms ± 606 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
# In [2]: %timeit f(a)
# 1.66 s ± 17.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# zigproduct2 работает в 8 раз быстрее, даже быстрее, чем zigproduct (Бог знает, почему?!?)
# Результаты совпадают
# In[3]: np.all(zigproduct2(a) == f(a))
# Out[3]: True
Заключение
Язык с великолепным будущим, если вы выполняете действия с меньшим количеством операций записи в память (например, np.where / np.argwhere), Zig будет ещё быстрее!
Дополнительные преимущества
Компилятор Zig также компилирует C-код без изменения команды, которую вы видели выше: просто создайте файл с именем "zinptest.c" и используйте его в качестве замены (216 мс для npprod2). Работать непосредственно с C-кодом, на мой взгляд, проще, чем расшифровывать странные сообщения об ошибках Numba. Если вам нужно работать с размерами, используйте шаги или атрибут a.shape и операции /
и %
.
void npprod(unsigned long long inarray, unsigned long long outarray,
unsigned long long lenarray) {
unsigned long long *inarraydata = (unsigned long long *)inarray;
unsigned long long *outarraydata = (unsigned long long *)outarray;
for (int i = 0; i < lenarray; i++) {
outarraydata[i] = inarraydata[i] * inarraydata[i];
}
}
void npprod2(unsigned long long inarray, unsigned long long outarray,
unsigned long long lenarray) {
unsigned long long *inarraydata = (unsigned long long *)inarray;
unsigned long long *outarraydata = (unsigned long long *)outarray;
for (int i = 0; i < lenarray; i++) {
outarraydata[i] = inarraydata[i] + 2 * inarraydata[i] * inarraydata[i] +
4 * inarraydata[i] * inarraydata[i] * inarraydata[i];
}
}
Интересно, что наиболее очевидное решение — использование C, если вы хотите добиться скорости C, не было упомянуто последние 8 лет. C не является магией, и чаще всего, особенно при работе с массивами NumPy (100% C массивы), это проще и быстрее. Компилировать C-код стало проще, чем когда-либо, с использованием Zig.
Самый быстрый способ проверить наличие значения в списке
Как измерить прошедшее время в Python?
Получить различия между двумя списками с уникальными элементами
Почему код Python выполняется быстрее в функции?
Как получить доступ к i-му столбцу многомерного массива NumPy?