使用 `numpy` 和 `functools` 的矢量化部分函数出现未解释的行为
我正在尝试将一个部分函数向量化,这个函数接收两个参数,都是列表,然后对这两个列表中的元素进行配对处理(使用 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 个回答
在对 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 将消除对向量中第一个元素的额外计算。
我不太明白你为什么要把 partial
和 vectorize
放在一起比较。它们的用途完全不同。
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
“向量化”。没有任何东西是编译的。