使用 `numpy` 和 `functools` 的矢量化部分函数出现未解释的行为

0 投票
2 回答
59 浏览
提问于 2025-04-14 16:41

我正在尝试将一个部分函数向量化,这个函数接收两个参数,都是列表,然后对这两个列表中的元素进行配对处理(使用 zip)。不过,我遇到了一些意想不到的情况。

看看下面的代码:

import functools
import numpy as np

def f(l1,l2):
    l1 = l1 if isinstance(l1,list) or isinstance(l1,np.ndarray) else [l1]
    l2 = l2 if isinstance(l2,list) or isinstance(l2,np.ndarray) else [l2]
    for e1,e2 in zip(l1,l2):
        print(e1,e2)

f(['a','b'],[1,2])

fp = functools.partial(f,l1=['a','b'])
fp(l2=[1,2])

fv = np.vectorize(fp)
fv(l2=np.array([1,2]))

在Jupyter笔记本中的输出结果如下:

a 1
b 2

a 1
b 2

a 1
a 1
a 2
array([None, None], dtype=object)

我有两个问题:

  • 首先,在 f 的开头进行类型检查是必要的,因为 np.vectorize 似乎会自动将任何输入完全扁平化(否则我会遇到 int32 not iterable 的异常)。 有没有办法避免这个问题呢?
  • 其次,当部分函数 fp 被向量化时,输出显然不是我预期的结果——我不太明白NumPy在这里做了什么,包括最后的空数组输出。无论我如何将 [1,2] 嵌套在列表、元组或数组中,输出似乎总是一样的。 我该如何修正我的代码,使得向量化函数 fv 的行为和 fp 一样,符合预期呢?

编辑
我尝试的另一个方法是:

fpv(l2=[np.array([1,2]), np.array([3,4])])

其输出为:

a 1
a 1
a 2
a 3
a 4

2 个回答

0

在对 isinstance 进行更改后,我进行了进一步分析:

import functools
import numpy as np

def f(l1,l2):
    print('raw', l1, l2)
    l1 = l1 if isinstance(l1,list) or isinstance(l1,np.ndarray) else [l1]
    l2 = l2 if isinstance(l2,list) or isinstance(l2,np.ndarray) else [l2]
    print('preprocessed', l1, l2)
    print('zipped:')
    for e1,e2 in zip(l1,l2):
        print(e1,e2)

print('\ntwo lists')
f(['a','b'],[1,2])

print('\nl1 supplied through funcools.partial')
fp = functools.partial(f,l1=['a','b'])
fp(l2=[1,2])

print('\nvectorized')
fv = np.vectorize(fp)
fv(l2=np.array([1,2]))

输出结果:

two lists
raw ['a', 'b'] [1, 2]
preprocessed ['a', 'b'] [1, 2]
zipped:
a 1
b 2

正如预期的那样。两个列表 [a, b] 和 [1, 2] 被合并在一起。

l1 supplied through funcools.partial
raw ['a', 'b'] [1, 2]
preprocessed ['a', 'b'] [1, 2]
zipped:
a 1
b 2

和上面一样,functools.partial 只是把一个有两个参数的函数包装成一个有一个参数的函数,其中一个参数是由 functools 注入的,另一个是暴露出来的。输入相同,输出也相同。

vectorized
raw ['a', 'b'] 1
preprocessed ['a', 'b'] [1]
zipped:
a 1
raw ['a', 'b'] 1
preprocessed ['a', 'b'] [1]
zipped:
a 1
raw ['a', 'b'] 2
preprocessed ['a', 'b'] [2]
zipped:
a 2

这就是我期望 vectorize 做的事情:将函数 fp 应用到输入 l2 的每个元素上。

所以我期待底层函数调用 f() 会是:

f(l1=['a', 'b'], l2=1)
(with expected output from zip(): a 1)
f(l1=['a', 'b'], l2=2)
(with expected output from zip(): a 2)

这几乎就是我们看到的情况,除了第一次调用被重复了两次。

最小重现场景:

import numpy as np

np.vectorize(print)(np.array([1,2,3]))

打印出 4 行:1, 1, 2, 3。

所以意外的行为出现在 ndarray 类和 np.vectorize 中;它似乎在数组中添加了一个头部,这个头部被当作一个元素来处理。

这个问题也在 为什么 numpy 的 vectorize 函数在第一个元素上执行两次 中讨论过。

解决方法如下:

np.vectorize(fp, otypes=['str'])(l2=np.array([1, 2, 3]))

指定 otypes 将消除对向量中第一个元素的额外计算。

0

我不太明白你为什么要把 partialvectorize 放在一起比较。它们的用途完全不同。

partial 只是让我们可以提前指定一个参数。它对 numpy 没有特别的作用。

我们来修改一下这个函数,让它显示更多关于输入和迭代的信息。

In [75]: def f(l1,l2):
    ...:     print('inputs ',l1,l2)
    ...:     l1 = l1 if isinstance(l1,list) or isinstance(l1,np.ndarray) else [l1]
    ...:     l2 = l2 if isinstance(l2,list) or isinstance(l2,np.ndarray) else [l2]
    ...:     for i,(e1,e2) in enumerate(zip(l1,l2)):
    ...:         print(i,e1,e2)
    ...:     return i
    ...:     

然后应用到你的示例列表上:

In [76]: f(['a','b'],[1,2])
inputs  ['a', 'b'] [1, 2]
0 a 1
1 b 2
Out[76]: 1

这样它就会接收两个列表作为输入,并对它们的配对值进行迭代。

把这个放到 vectorize 里:

In [77]: fv1 = np.vectorize(f)

In [78]: fv1(['a','b'],[1,2])
inputs  a 1
0 a 1
inputs  a 1
0 a 1
inputs  b 2
0 b 2
Out[78]: array([0, 0])

输入是完全不同的。f 被多次调用,每次都是一对标量值。第一次调用时用的是 a 1,这是用来确定返回数据类型的试探调用。我这里返回了一个数字,而你返回的是 None

由于你的 if 语句,标量值被转换成了单元素列表,因此只进行了一个迭代。

为了好玩,我们来把一个输入改成列表的列表,

In [79]: fv1([['a'],['b']],[1,2])
inputs  a 1
0 a 1
inputs  a 1
0 a 1
inputs  a 2
0 a 2
inputs  b 1
0 b 1
inputs  b 2
0 b 2
Out[79]: 
array([[0, 0],
       [0, 0]])

现在 f 被调用了五次,第一次是试探调用,后面是一个 (2,2) 的配对——但都是标量输入。

我本来想加一个使用 signature 参数的例子,但我不太记得需要的语法了。

使用 partial 的时候,执行和我提供两个列表时没有什么不同,[76]:

In [83]: fp = functools.partial(f,l1=['a','b'])
    ...: fp(l2=[1,2])
inputs  ['a', 'b'] [1, 2]
0 a 1
1 b 2
Out[83]: 1

使用 vectorize 我可以简化 f,让它只期待标量值,并直接显示它们:

In [84]: def f(l1,l2):
    ...:     print('inputs ',l1,l2)
    ...:     return 0
    ...: fv2 = np.vectorize(f)    

In [85]: fv2(['a','b'],[1,2])
inputs  a 1
inputs  a 1
inputs  b 2
Out[85]: array([0, 0])

实际上,vectorize 只是替代了你在原始 f 中放的迭代。而且它并没有更快(严格说,vectorize 在处理大数组时的性能稍微好一些,但这并不是真正的 numpy “向量化”。没有任何东西是编译的。

撰写回答