【问题标题】:Vectorizing a "pure" function with numpy, assuming many duplicates假设有许多重复项,使用 numpy 对“纯”函数进行矢量化
【发布时间】:2019-12-20 09:40:27
【问题描述】:

我想将“黑盒”Python 函数 f 应用于大型数组 arr。其他假设是:

  • 函数f是“纯”的,例如是确定性的,没有副作用。
  • 数组arr 有少量唯一元素。

我可以使用一个为arr 的每个唯一元素计算f 的装饰器来实现这一点,如下所示:

import numpy as np
from time import sleep
from functools import wraps


N = 1000
np.random.seed(0)
arr = np.random.randint(0, 10, size=(N, 2))


def vectorize_pure(f):
    @wraps(f)
    def f_vec(arr):
        uniques, ix = np.unique(arr, return_inverse=True)
        f_range = np.array([f(x) for x in uniques])
        return f_range[ix].reshape(arr.shape)
    return f_vec


@np.vectorize
def usual_vectorize(x):
    sleep(0.001)
    return x


@vectorize_pure
def pure_vectorize(x):
    sleep(0.001)
    return x

# In [47]: %timeit usual_vectorize(arr)                                
# 1.33 s ± 6.16 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# In [48]: %timeit pure_vectorize(arr)                                 
# 13.6 ms ± 81.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

我担心np.unique 会在后台对arr 进行排序,考虑到假设,这似乎效率低下。我正在寻找一种实用的方法来实现类似的装饰器

  1. 利用快速的numpy 向量化操作。
  2. 不对输入数组进行排序。

我怀疑使用numba 的答案是“是”,但我对numpy 解决方案特别感兴趣。

另外,根据arr 数据类型,numpy 可能使用基数排序,因此unique 的性能在某些情况下可能会很好。


我在下面找到了一个解决方法,使用pandas.unique;但是,它仍然需要对原始数组进行两次传递,并且pandas.unique 做了一些额外的工作。我想知道pandas._libs.hashtablecython 是否存在更好的解决方案,或者其他任何东西。

【问题讨论】:

  • 在你的例子中,arr 是一个小整数数组。总是这样吗?如果是这样,请查看stackoverflow.com/questions/46575364/…,了解如何比np.unique 更有效地找到独特元素。
  • 整数只是一个例子。一个非常好的方法!
  • 你可以在幕后查看,np.lib.arraysetops._unique1d。它执行sort,然后检查相邻项是否相等。
  • @hpaulj 是的,我在看_unique1d,这就是提出问题的原因。
  • 任何这样的解决方案都很大程度上取决于输入大小和冲突率(1 - (number_of_unique_values / size_of_the_input))。你对你的目标价值观有什么暗示吗?否则,可能很难对不同的解决方案进行合理的测试。在您自己的答案中,您使用的碰撞率比您的问题中高得多。

标签: python pandas numpy unique vectorization


【解决方案1】:

实际上,您可以一次性遍历数组,但它要求您事先知道结果的dtype。否则,您需要对元素进行第二次遍历才能确定它。

暂时忽略性能(和functools.wraps),实现可能如下所示:

def vectorize_cached(output_dtype):
    def vectorize_cached_factory(f):
        def f_vec(arr):
            flattened = arr.ravel()
            if output_dtype is None:
                result = np.empty_like(flattened)
            else:
                result = np.empty(arr.size, output_dtype)

            cache = {}
            for idx, item in enumerate(flattened):
                res = cache.get(item)
                if res is None:
                    res = f(item)
                    cache[item] = res
                result[idx] = res
            return result.reshape(arr.shape)
        return f_vec
    return vectorize_cached_factory

它首先创建结果数组,然后遍历输入数组。一旦遇到尚未在字典中的元素,就会调用该函数(并存储结果) - 否则它只是使用存储在字典中的值。

@vectorize_cached(np.float64)
def t(x):
    print(x)
    return x + 2.5

>>> t(np.array([1,1,1,2,2,2,3,3,1,1,1]))
1
2
3
array([3.5, 3.5, 3.5, 4.5, 4.5, 4.5, 5.5, 5.5, 3.5, 3.5, 3.5])

但这并不是特别快,因为我们在 NumPy 数组上执行 Python 循环。

Cython 解决方案

为了让它更快,我们实际上可以将此实现移植到 Cython(目前仅支持 float32、float64、int32、int64、uint32 和 uint64,但几乎可以轻松扩展,因为它使用了融合类型):

%%cython

cimport numpy as cnp

ctypedef fused input_type:
    cnp.float32_t
    cnp.float64_t
    cnp.uint32_t
    cnp.uint64_t
    cnp.int32_t
    cnp.int64_t

ctypedef fused result_type:
    cnp.float32_t
    cnp.float64_t
    cnp.uint32_t
    cnp.uint64_t
    cnp.int32_t
    cnp.int64_t

cpdef void vectorized_cached_impl(input_type[:] array, result_type[:] result, object func):
    cdef dict cache = {}
    cdef Py_ssize_t idx
    cdef input_type item
    for idx in range(array.size):
        item = array[idx]
        res = cache.get(item)
        if res is None:
            res = func(item)
            cache[item] = res
        result[idx] = res

使用 Python 装饰器(以下代码未使用 Cython 编译):

def vectorize_cached_cython(output_dtype):
    def vectorize_cached_factory(f):
        def f_vec(arr):
            flattened = arr.ravel()
            if output_dtype is None:
                result = np.empty_like(flattened)
            else:
                result = np.empty(arr.size, output_dtype)

            vectorized_cached_impl(flattened, result, f)

            return result.reshape(arr.shape)
        return f_vec
    return vectorize_cached_factory

同样,这仅执行一次,并且每个唯一值仅应用该函数一次:

@vectorize_cached_cython(np.float64)
def t(x):
    print(x)
    return x + 2.5

>>> t(np.array([1,1,1,2,2,2,3,3,1,1,1]))
1
2
3
array([3.5, 3.5, 3.5, 4.5, 4.5, 4.5, 5.5, 5.5, 3.5, 3.5, 3.5])

基准:快速函数,大量重复

但问题是:在这里使用 Cython 有意义吗?

我做了一个快速基准测试(没有sleep)来了解性能有多么不同(使用我的库simple_benchmark):

def func_to_vectorize(x):
    return x

usual_vectorize = np.vectorize(func_to_vectorize)
pure_vectorize = vectorize_pure(func_to_vectorize)
pandas_vectorize = vectorize_with_pandas(func_to_vectorize)
cached_vectorize = vectorize_cached(None)(func_to_vectorize) 
cython_vectorize = vectorize_cached_cython(None)(func_to_vectorize) 


from simple_benchmark import BenchmarkBuilder

b = BenchmarkBuilder()
b.add_function(alias='usual_vectorize')(usual_vectorize)
b.add_function(alias='pure_vectorize')(pure_vectorize)
b.add_function(alias='pandas_vectorize')(pandas_vectorize)
b.add_function(alias='cached_vectorize')(cached_vectorize)
b.add_function(alias='cython_vectorize')(cython_vectorize)

@b.add_arguments('array size')
def argument_provider():
    np.random.seed(0)
    for exponent in range(6, 20):
        size = 2**exponent
        yield size, np.random.randint(0, 10, size=(size, 2))

r = b.run()
r.plot()

根据这些时间,排名将是(从最快到最慢):

  • Cython 版本
  • Pandas 解决方案(来自another answer
  • 纯溶液(原帖)
  • NumPys 向量化
  • 使用缓存的非 Cython 版本

如果函数调用非常便宜,普通的 NumPy 解决方案只会慢 5-10 倍。 pandas 解决方案还有一个更大的常数因子,这使得它对于非常小的数组来说是最慢的。

基准测试:昂贵的函数 (time.sleep(0.001)),大量重复

如果函数调用实际上很昂贵(例如 time.sleep),np.vectorize 解决方案将慢很多,但其他解决方案之间的差异要小得多:

# This shows only the difference compared to the previous benchmark
def func_to_vectorize(x):
    sleep(0.001)
    return x

@b.add_arguments('array size')
def argument_provider():
    np.random.seed(0)
    for exponent in range(5, 10):
        size = 2**exponent
        yield size, np.random.randint(0, 10, size=(size, 2))

基准测试:函数快速,重复次数少

但是,如果您没有那么多重复项,则普通的 np.vectorize 几乎与 pure 和 pandas 解决方案一样快,并且只比 Cython 版本慢一点:

# Again just difference to the original benchmark is shown
@b.add_arguments('array size')
def argument_provider():
    np.random.seed(0)
    for exponent in range(6, 20):
        size = 2**exponent
        # Maximum value is now depending on the size to ensures there 
        # are less duplicates in the array
        yield size, np.random.randint(0, size // 10, size=(size, 2))

【讨论】:

  • 非常好。我没有意识到 cython 可以与 dicts 无缝协作。我敢打赌我将来会使用它。
  • @hilberts_drinking_problem 是的,现在即使 numba 也支持字典 - 但是 numba 会遇到任意函数的约束问题。我也很惊讶 Cython 能从 Python 中挤出多少性能!
  • @hilberts_drinking_problem 感谢您的夸奖!只是为了完整性:如果您需要一个基准测试库,您还可以查看perfplot(Nico Schlömer 的类似库)。它具有类似的功能和界面,并且已经存在了很长时间。
  • @MSeifert 您使用的是哪个 Cython 版本?它对我不起作用(我得到Invalid use of fused types, type cannot be specialized
  • @norok2 我的 Cython 版本是 0.29.12(和 0.29.13)。但是我怀疑是什么导致了错误:您是否尝试使用 Cython 编译我标记为“Python 包装器”的代码?我使用 Cython 编译了以 %%cython 开头的块中的代码。
【解决方案2】:

这个问题实际上很有趣,因为它是计算时间和内存消耗之间权衡的完美示例。

从算法的角度来看,找到唯一元素并最终只计算唯一元素,可以通过两种方式实现:

  • 两次(或更多)通过方法:

    • 找出所有独特的元素
    • 找出独特元素的位置
    • 计算唯一元素的函数
    • 将所有计算出来的唯一元素放到正确的位置
  • 单程方法:

    • 随时随地计算元素并缓存结果
    • 如果一个元素在缓存中,则从那里获取它

算法复杂度取决于输入 N 的大小和唯一元素的数量 U。后者也可以使用独特元素的r = U / N 比率来形式化。

more-passes 方法理论上较慢。但是,它们对于小型NU 具有相当的竞争力。 single-pass 方法理论上更快,但这也很大程度上取决于 缓存 方法以及它们的执行方式取决于U。 当然,无论渐近行为多么重要,实际时间确实取决于恒定的计算时间因素。 与这个问题最相关的是func() 计算时间。

方法

可以比较多种方法:

  • 没有缓存

    • pure() 这将是基本函数,可能已经被矢量化了
    • np.vectorized() 这将是 NumPy 标准矢量化装饰器
  • more-passes 方法

    • np_unique():使用np.unique() 找到唯一值并使用索引(来自np.unique() 输出)来构造结果(基本上相当于来自herevectorize_pure()
    • pd_unique():使用pd.unique() 找到唯一值并使用索引(通过np.searchsorted())构造结果(基本上相当于vectorize_with_pandas() 来自here
    • set_unique():使用简单的set() 找到唯一值并使用索引(通过np.searchsorted())构造结果
    • set_unique_msk():使用简单的set()(如set_unique())找到唯一值,并使用循环和屏蔽来构造结果(而不是索引)
    • nb_unique():使用显式循环和numba JIT 加速找到唯一值及其索引
    • cy_unique():唯一值及其索引是使用cython 的显式循环找到的
  • 单程方法

    • cached_dict():使用 Python dict 进行缓存(O(1) 查找)
    • cached_dict_cy():与上述相同,但使用 Cython(基本上等同于来自 herevectorized_cached_impl()
    • cached_arr_cy():使用数组进行缓存(O(U) 查找)

纯()

def pure(x):
    return 2 * x

np.vectorized()

import numpy as np


vectorized = np.vectorize(pure)
vectorized.__name__ = 'vectorized'

np_unique()

import functools
import numpy as np


def vectorize_np_unique(func):
    @functools.wraps(func)
    def func_vect(arr):
        uniques, ix = np.unique(arr, return_inverse=True)
        result = np.array([func(x) for x in uniques])
        return result[ix].reshape(arr.shape)
    return func_vect

np_unique = vectorize_np_unique(pure)
np_unique.__name__ = 'np_unique'

pd_unique()

import functools
import numpy as np
import pandas as pd


def vectorize_pd_unique(func):
    @functools.wraps(func)
    def func_vect(arr):
        shape = arr.shape
        arr = arr.ravel()
        uniques = np.sort(pd.unique(arr))
        f_range = np.array([func(x) for x in uniques])
        return f_range[np.searchsorted(uniques, arr)].reshape(shape)
    return func_vect

pd_unique = vectorize_pd_unique(pure)
pd_unique.__name__ = 'pd_unique'

set_unique()

import functools


def vectorize_set_unique(func):
    @functools.wraps(func)
    def func_vect(arr):
        shape = arr.shape
        arr = arr.ravel()
        uniques = sorted(set(arr))
        result = np.array([func(x) for x in uniques])
        return result[np.searchsorted(uniques, arr)].reshape(shape)
    return func_vect

set_unique = vectorize_set_unique(pure)
set_unique.__name__ = 'set_unique'

set_unique_msk()

import functools


def vectorize_set_unique_msk(func):
    @functools.wraps(func)
    def func_vect(arr):
        result = np.empty_like(arr)
        for x in set(arr.ravel()):
            result[arr == x] = func(x)
        return result
    return func_vect

set_unique_msk = vectorize_set_unique_msk(pure)
set_unique_msk.__name__ = 'set_unique_msk'

nb_unique()

import functools
import numpy as np
import numba as nb
import flyingcircus as fc


@nb.jit(forceobj=False, nopython=True, nogil=True, parallel=True)
def numba_unique(arr, max_uniques):
    ix = np.empty(arr.size, dtype=np.int64)
    uniques = np.empty(max_uniques, dtype=arr.dtype)
    j = 0
    for i in range(arr.size):
        found = False
        for k in nb.prange(j):
            if arr[i] == uniques[k]:
                found = True
                break
        if not found:
            uniques[j] = arr[i]
            j += 1            
    uniques = np.sort(uniques[:j])
    # : get indices
    num_uniques = j
    for j in nb.prange(num_uniques):
        x = uniques[j]
        for i in nb.prange(arr.size):
            if arr[i] == x:
                ix[i] = j
    return uniques, ix


@fc.base.parametric
def vectorize_nb_unique(func, max_uniques=-1):
    @functools.wraps(func)
    def func_vect(arr):
        nonlocal max_uniques
        shape = arr.shape
        arr = arr.ravel()
        if max_uniques <= 0:
            m = arr.size
        elif isinstance(max_uniques, int):
            m = min(max_uniques, arr.size)
        elif isinstance(max_uniques, float):
            m = int(arr.size * min(max_uniques, 1.0))
        uniques, ix = numba_unique(arr, m)
        result = np.array([func(x) for x in uniques])
        return result[ix].reshape(shape)
    return func_vect

nb_unique = vectorize_nb_unique()(pure)
nb_unique.__name__ = 'nb_unique'

cy_unique()

%%cython -c-O3 -c-march=native -a
#cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True
import numpy as np
import cython as cy

cimport cython as ccy
cimport numpy as cnp


ctypedef fused arr_t:
    cnp.uint16_t
    cnp.uint32_t
    cnp.uint64_t
    cnp.int16_t
    cnp.int32_t
    cnp.int64_t
    cnp.float32_t
    cnp.float64_t
    cnp.complex64_t
    cnp.complex128_t


def sort_numpy(arr_t[:] a):
    np.asarray(a).sort()


cpdef cnp.int64_t cython_unique(
        arr_t[:] arr,
        arr_t[::1] uniques,
        cnp.int64_t[:] ix):
    cdef size_t size = arr.size
    cdef arr_t x
    cdef cnp.int64_t i, j, k, num_uniques
    j = 0
    for i in range(size):
        found = False
        for k in range(j):
            if arr[i] == uniques[k]:
                found = True
                break
        if not found:
            uniques[j] = arr[i]
            j += 1            
    sort_numpy(uniques[:j])
    num_uniques = j
    for j in range(num_uniques):
        x = uniques[j]
        for i in range(size):
            if arr[i] == x:
                ix[i] = j
    return num_uniques
import functools
import numpy as np
import flyingcircus as fc


@fc.base.parametric
def vectorize_cy_unique(func, max_uniques=0):
    @functools.wraps(func)
    def func_vect(arr):
        shape = arr.shape
        arr = arr.ravel()
        if max_uniques <= 0:
            m = arr.size
        elif isinstance(max_uniques, int):
            m = min(max_uniques, arr.size)
        elif isinstance(max_uniques, float):
            m = int(arr.size * min(max_uniques, 1.0))
        ix = np.empty(arr.size, dtype=np.int64)
        uniques = np.empty(m, dtype=arr.dtype)
        num_uniques = cy_uniques(arr, uniques, ix)
        uniques = uniques[:num_uniques]
        result = np.array([func(x) for x in uniques])
        return result[ix].reshape(shape)
    return func_vect

cy_unique = vectorize_cy_unique()(pure)
cy_unique.__name__ = 'cy_unique'

cached_dict()

import functools
import numpy as np


def vectorize_cached_dict(func):
    @functools.wraps(func)
    def func_vect(arr):
        result = np.empty_like(arr.ravel())
        cache = {}
        for i, x in enumerate(arr.ravel()):
            if x not in cache:
                cache[x] = func(x)
            result[i] = cache[x]
        return result.reshape(arr.shape)
    return func_vect

cached_dict = vectorize_cached_dict(pure)
cached_dict.__name__ = 'cached_dict'

cached_dict_cy()

%%cython -c-O3 -c-march=native -a
#cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True
import numpy as np
import cython as cy

cimport cython as ccy
cimport numpy as cnp


ctypedef fused arr_t:
    cnp.uint16_t
    cnp.uint32_t
    cnp.uint64_t
    cnp.int16_t
    cnp.int32_t
    cnp.int64_t
    cnp.float32_t
    cnp.float64_t
    cnp.complex64_t
    cnp.complex128_t

ctypedef fused result_t:
    cnp.uint16_t
    cnp.uint32_t
    cnp.uint64_t
    cnp.int16_t
    cnp.int32_t
    cnp.int64_t
    cnp.float32_t
    cnp.float64_t
    cnp.complex64_t
    cnp.complex128_t


cpdef void apply_cached_dict_cy(arr_t[:] arr, result_t[:] result, object func):
    cdef size_t size = arr.size
    cdef size_t i
    cdef dict cache = {}
    cdef arr_t x
    cdef result_t y
    for i in range(size):
        x = arr[i]
        if x not in cache:
            y = func(x)
            cache[x] = y
        else:
            y = cache[x]
        result[i] = y
import functools
import flyingcircus as fc


@fc.base.parametric
def vectorize_cached_dict_cy(func, dtype=None):
    @functools.wraps(func)
    def func_vect(arr):
        nonlocal dtype
        shape = arr.shape
        arr = arr.ravel()
        result = np.empty_like(arr) if dtype is None else np.empty(arr.shape, dtype=dtype)
        apply_cached_dict_cy(arr, result, func)
        return np.reshape(result, shape)
    return func_vect

cached_dict_cy = vectorize_cached_dict_cy()(pure)
cached_dict_cy.__name__ = 'cached_dict_cy'

cached_arr_cy()

%%cython -c-O3 -c-march=native -a
#cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True
import numpy as np
import cython as cy

cimport cython as ccy
cimport numpy as cnp


ctypedef fused arr_t:
    cnp.uint16_t
    cnp.uint32_t
    cnp.uint64_t
    cnp.int16_t
    cnp.int32_t
    cnp.int64_t
    cnp.float32_t
    cnp.float64_t
    cnp.complex64_t
    cnp.complex128_t

ctypedef fused result_t:
    cnp.uint16_t
    cnp.uint32_t
    cnp.uint64_t
    cnp.int16_t
    cnp.int32_t
    cnp.int64_t
    cnp.float32_t
    cnp.float64_t
    cnp.complex64_t
    cnp.complex128_t


cpdef void apply_cached_arr_cy(
        arr_t[:] arr,
        result_t[:] result,
        object func,
        arr_t[:] uniques,
        result_t[:] func_uniques):
    cdef size_t i
    cdef size_t j
    cdef size_t k
    cdef size_t size = arr.size
    j = 0
    for i in range(size):
        found = False
        for k in range(j):
            if arr[i] == uniques[k]:
                found = True
                break
        if not found:
            uniques[j] = arr[i]
            func_uniques[j] = func(arr[i])
            result[i] = func_uniques[j]
            j += 1
        else:
            result[i] = func_uniques[k]
import functools
import numpy as np
import flyingcircus as fc


@fc.base.parametric
def vectorize_cached_arr_cy(func, dtype=None, max_uniques=None):
    @functools.wraps(func)
    def func_vect(arr):
        nonlocal dtype, max_uniques
        shape = arr.shape
        arr = arr.ravel()
        result = np.empty_like(arr) if dtype is None else np.empty(arr.shape, dtype=dtype)
        if max_uniques is None or max_uniques <= 0:
            max_uniques = arr.size
        elif isinstance(max_uniques, int):
            max_uniques = min(max_uniques, arr.size)
        elif isinstance(max_uniques, float):
            max_uniques = int(arr.size * min(max_uniques, 1.0))
        uniques = np.empty(max_uniques, dtype=arr.dtype)
        func_uniques = np.empty_like(arr) if dtype is None else np.empty(max_uniques, dtype=dtype)
        apply_cached_arr_cy(arr, result, func, uniques, func_uniques)
        return np.reshape(result, shape)
    return func_vect

cached_arr_cy = vectorize_cached_arr_cy()(pure)
cached_arr_cy.__name__ = 'cached_arr_cy'

注意事项

元装饰器@parametric(灵感来自here,在FlyingCircus中作为flyingcircus.base.parametric可用)定义如下:

def parametric(decorator):
    @functools.wraps(decorator)
    def _decorator(*_args, **_kws):
        def _wrapper(func):
            return decorator(func, *_args, **_kws)

        return _wrapper

    return _decorator

Numba 无法比常规 Python 代码更有效地处理 单次传递 方法,因为传递任意 callable 需要启用 Python object 支持,从而排除快速 JIT 循环。

Cython 有一些限制,您需要指定预期的结果数据类型。您也可以从输入数据类型中试探性地猜测它,但这并不理想。

为了简单起见,使用静态 NumPy 数组实现了一些需要临时存储的实现。例如,可以使用 C++ 中的动态数组来改进这些实现,而不会造成太大的速度损失,但会大大改善内存占用。

基准测试

只有 10 个唯一值的慢速函数(小于 ~0.05%)

(这基本上是原始帖子的用例)。

具有 ~0.05% 唯一值的快速函数

具有 ~10% 唯一值的快速函数

具有 ~20% 唯一值的快速函数

完整的基准代码(基于this template)在here 上可用。

讨论与结论

最快的方法取决于NU。 对于慢速函数,所有缓存方法都比vectorized() 更快。当然,这个结果应该持保留态度,因为这里测试的 slow 函数比 fast 函数慢了约 4 个数量级,而且这种慢速分析函数并不太常见。 如果函数可以立即以矢量化形式编写,那是迄今为止最快的方法。

一般来说,只要U / N 小于或等于 20%,cached_dict_cy() 的内存效率和速度都比 vectorized() 快(即使对于快速函数也是如此)。 它的主要缺点是需要 Cython,这是一个有点复杂的依赖项,并且还需要指定结果数据类型。 只要U / N 小于或等于 10%,np_unique() 方法就比vectorized() 快(即使对于快速函数也是如此)。 pd_unique() 方法仅对非常小的 U 和慢功能具有竞争力。

对于非常小的U,散列的好处略少,cached_arr_cy() 是最快的方法。

【讨论】:

    【解决方案3】:

    经过一番探讨,这是一种使用pandas.unique(基于散列)而不是numpy.unique(基于排序)的方法。

    import pandas as pd
    
    def vectorize_with_pandas(f):
        @wraps(f)
        def f_vec(arr):
            uniques = np.sort(pd.unique(arr.ravel()))
            f_range = np.array([f(x) for x in uniques])
            return f_range[
                np.searchsorted(uniques, arr.ravel())
                ].reshape(arr.shape)
        return f_vec
    

    提供以下性能提升:

    N = 1_000_000
    np.random.seed(0)
    arr = np.random.randint(0, 10, size=(N, 2)).astype(float)
    
    @vectorize_with_pandas
    def pandas_vectorize(x):
        sleep(0.001)
        return x
    
    In [33]: %timeit pure_vectorize(arr)                                         
    152 ms ± 2.34 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    
    In [34]: %timeit pandas_vectorize(arr)                                       
    76.8 ms ± 582 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
    

    此外,基于 Warren Weckesser 的 suggestion,如果 arr 是一个小整数数组,您可以走得更快,例如uint8。例如,

    def unique_uint8(arr):
        q = np.zeros(256, dtype=int)
        q[arr.ravel()] = 1
        return np.nonzero(q)[0]
    
    
    def vectorize_uint8(f):
        @wraps(f)
        def f_vec(arr):
            uniques = unique_uint8(arr)
            f_range = np.array([f(x) for x in uniques])
            return f_range[
                np.searchsorted(uniques, arr.ravel())
                ].reshape(arr.shape)
        return f_vec
    

    【讨论】:

    • 很酷的小实验。我打算建议散列可能比np.unique 快得多
    【解决方案4】:

    下面的装饰器是:

    • usual_vectorize 快 10 倍
    • 比你的 vectorize_pure 慢 10 倍
    • 没有进行任何排序(据我所知)
    • 使用numpy矢量化操作

    代码:

    def vectorize_pure2(f):
        @wraps(f)
        def f_vec(arr):
            tups = [tuple(x) for x in arr]
            tups_rows = dict(zip(tups, arr))
            new_arr = np.ndarray(arr.shape)
            for row in tups_rows.values():
                row_ixs = (arr == row).all(axis=1)
                new_arr[row_ixs] = f(row)
            return new_arr
        return f_vec
    

    性能:

    @vectorize_pure2
    def pure_vectorize2(x):
        sleep(0.001)
        return x
    
    In [49]: %timeit pure_vectorize2(arr)
    135 ms ± 879 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
    

    由于这个答案有些功劳:https://stackoverflow.com/a/16992881/4960855

    【讨论】:

    • 我已经通过改进代码编辑了我的答案,以在输入f 时保留行的原始形式,即将它们保留为np.array 而不是tuple。相同的表现。
    • 谢谢,但这并不是真正的矢量化解决方案,因为数组的每个元素都调用了纯 python 函数。我试图在缓存函数值的加速和以矢量化方式传递数组之间找到平衡。我认为你的实现大致相当于np.vectorizelru_cache的组合。
    • 目前你的问题是:“我担心np.unique 会在后台排序arr [..] 有没有一种实用的方法可以在 O(N) 中实现类似的装饰器?”.或许你应该根据你的回复修改一下。
    • 该问题旨在要求一个既矢量化又不排序的解决方案。我将澄清措辞中的第一个要求。
    • 谢谢。我已经编辑了我的答案以利用快速的numpy 矢量化操作。但仍然表现相同。
    猜你喜欢
    • 1970-01-01
    • 2023-03-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-06
    • 2016-06-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多