python - 如何避免具有多列的 numpy-arrays 的总和不太精确
问题描述
我一直认为numpy 使用一种pairwise-summationfloat32
,这也确保了- 操作的高精度:
import numpy as np
N=17*10**6 # float32-precision no longer enough to hold the whole sum
print(np.ones((N,1),dtype=np.float32).sum(axis=0))
# [17000000.], kind of expected
但是,如果矩阵具有多于一列,则看起来好像使用了不同的算法:
print(np.ones((N,2),dtype=np.float32).sum(axis=0))
# [16777216. 16777216.] the error is just to big
print(np.ones((2*N,2),dtype=np.float32).sum(axis=0))
# [16777216. 16777216.] error is bigger
可能sum
只是天真地总结所有值。一个迹象是16777216.f+1.0f=16777216.f
,例如:
one = np.array([1.], np.float32)
print(np.array([16777215.], np.float32)+one) # 16777216.
print(np.array([16777216.], np.float32)+one) # 16777216. as well
为什么 numpy 不对多列使用成对求和,并且 numpy 也可以强制对多列使用成对求和?
我的 numpy 版本是 1.14.2,如果这起作用的话。
解决方案
这种行为是由于 numpy 在减少操作期间访问内存的方式(“添加”只是一种特殊情况)以提高缓存的利用率。
对于某些情况(如上所述),可以强制执行成对求和而不会对性能产生很大影响。但总的来说,强制执行它会导致巨大的性能损失——在大多数情况下,使用双精度可能会更容易缓解上述问题。
成对求和可以看作是对“加法”操作的一种非常具体的优化,如果满足某些约束(稍后会详细介绍),就会执行此操作。
求和(和许多其他减少操作)受内存带宽限制。如果我们沿着一个连续的轴求和,生活是美好的:为索引提取到缓存中的内存i
将直接用于计算 index i+1
, i+2
,... 而不会在使用之前从缓存中逐出。
情况不同,当求和不是沿着连续轴时:添加一个 float32 元素 16-float32 被提取到缓存中,但其中 15 个在它们可以使用之前被驱逐,并且必须再次提取 - 什么浪费。
这就是为什么 numpy 在这种情况下按行求和的原因:对第一行和第二行求和,然后将第三行添加到结果中,然后是第四行,依此类推。但是,成对求和仅用于一维求和,不能在此处使用。
在以下情况下执行成对求和:
sum
在一维 numpy 数组上调用sum
沿连续轴调用
numpy 没有(还没有?)提供一种方法来强制执行成对求和而不会对性能产生重大负面影响。
我从中得出的结论是:目标应该是沿连续轴执行求和,这不仅更精确,而且可能更快:
A=np.ones((N,2), dtype=np.float32, order="C") #non-contiguous
%timeit A.sum(axis=0)
# 326 ms ± 9.17 ms
B=np.ones((N,2), dtype=np.float32, order="F") # contiguous
%timeit B.sum(axis=0)
# 15.6 ms ± 898 µs
在这种特殊情况下,一行中只有 2 个元素,开销太大(另请参阅此处解释的类似行为)。
它可以做得更好,例如通过仍然不精确einsum
:
%timeit np.einsum("i...->...", A)
# 74.5 ms ± 1.47 ms
np.einsum("i...->...", A)
# array([16777216., 16777216.], dtype=float32)
甚至:
%timeit np.array([A[:,0].sum(), A[:,1].sum()], dtype=np.float32)
# 17.8 ms ± 333 µs
np.array([A[:,0].sum(), A[:,1].sum()], dtype=np.float32)
# array([17000000., 17000000.], dtype=float32)
这不仅几乎与连续版本一样快(两次加载内存的惩罚不如加载内存 16 次高),而且非常精确,因为sum
它用于一维 numpy 数组。
对于更多列,对于 numpy 和 einsum 方式,与连续大小写的差异要小得多:
B=np.ones((N,16), dtype=np.float32, order="F")
%timeit B.sum(axis=0)
# 121 ms ± 3.66 ms
A=np.ones((N,16), dtype=np.float32, order="C")
%timeit A.sum(axis=0)
# 457 ms ± 12.1 ms
%timeit np.einsum("i...->...", A)
# 139 ms ± 651 µs per loop
但是“精确”技巧的性能非常糟糕,可能是因为延迟不能再被计算隐藏:
def do(A):
N=A.shape[1]
res=np.zeros(N, dtype=np.float32)
for i in range(N):
res[i]=A[:,i].sum()
return res
%timeit do(A)
# 1.39 s ± 47.8 ms
以下是 numpy 实现的血腥细节。
可以从这里FLOAT_add
的with 定义的代码中看到差异:
#define IS_BINARY_REDUCE ((args[0] == args[2])\
&& (steps[0] == steps[2])\
&& (steps[0] == 0))
#define BINARY_REDUCE_LOOP(TYPE)\
char *iop1 = args[0]; \
TYPE io1 = *(TYPE *)iop1; \
/** (ip1, ip2) -> (op1) */
#define BINARY_LOOP\
char *ip1 = args[0], *ip2 = args[1], *op1 = args[2];\
npy_intp is1 = steps[0], is2 = steps[1], os1 = steps[2];\
npy_intp n = dimensions[0];\
npy_intp i;\
for(i = 0; i < n; i++, ip1 += is1, ip2 += is2, op1 += os1)
/**begin repeat
* Float types
* #type = npy_float, npy_double, npy_longdouble#
* #TYPE = FLOAT, DOUBLE, LONGDOUBLE#
* #c = f, , l#
* #C = F, , L#
*/
/**begin repeat1
* Arithmetic
* # kind = add, subtract, multiply, divide#
* # OP = +, -, *, /#
* # PW = 1, 0, 0, 0#
*/
NPY_NO_EXPORT void
@TYPE@_@kind@(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func))
{
if (IS_BINARY_REDUCE) {
#if @PW@
@type@ * iop1 = (@type@ *)args[0];
npy_intp n = dimensions[0];
*iop1 @OP@= pairwise_sum_@TYPE@(args[1], n, steps[1]);
#else
BINARY_REDUCE_LOOP(@type@) {
io1 @OP@= *(@type@ *)ip2;
}
*((@type@ *)iop1) = io1;
#endif
}
else if (!run_binary_simd_@kind@_@TYPE@(args, dimensions, steps)) {
BINARY_LOOP {
const @type@ in1 = *(@type@ *)ip1;
const @type@ in2 = *(@type@ *)ip2;
*((@type@ *)op1) = in1 @OP@ in2;
}
}
}
一旦生成如下所示:
NPY_NO_EXPORT void
FLOAT_add(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func))
{
if (IS_BINARY_REDUCE) {
#if 1
npy_float * iop1 = (npy_float *)args[0];
npy_intp n = dimensions[0];
*iop1 += pairwise_sum_FLOAT((npy_float *)args[1], n,
steps[1] / (npy_intp)sizeof(npy_float));
#else
BINARY_REDUCE_LOOP(npy_float) {
io1 += *(npy_float *)ip2;
}
*((npy_float *)iop1) = io1;
#endif
}
else if (!run_binary_simd_add_FLOAT(args, dimensions, steps)) {
BINARY_LOOP {
const npy_float in1 = *(npy_float *)ip1;
const npy_float in2 = *(npy_float *)ip2;
*((npy_float *)op1) = in1 + in2;
}
}
}
FLOAT_add
可用于一维减少,在这种情况下:
args[0]
是指向结果/初始值的指针(与 相同args[2]
)args[1]
是输入数组steps[0]
和steps[2]
are0
,即指针指向标量。
然后可以使用成对求和(检查IS_BINARY_REDUCE
)。
FLOAT_add
可用于添加两个向量,在这种情况下:
args[0]
第一个输入数组args[1]
第二个输入数组args[2]
输出数组steps
- 对于上述数组,从一个元素到另一个元素。
参数@PW@
仅1
用于求和 - 对于所有其他操作,不使用成对求和。
推荐阅读
- shell - 为什么期望'obexctl'的shell脚本不能按预期工作
- python - 为什么 memory_usage() 和 memory_usage(deep=True) 之间有这样的区别?
- prettier - 将 if 语句放在一行更漂亮
- javascript - Typescript 中 knex 的默认返回类型是什么?
- javascript - 在javascript中将字符aabbbcccc+++压缩为a@2b@3c@4+@3
- javascript - 悬停 id 时更改图像
- java - @Transactional 与 JPA 和 Hibernate 有什么用?
- regex - RegEx - 如何获得 0 到 10,000,000 的范围(2 个小数位)
- android - 如何在其他应用播放音乐时暂停视频并在音乐停止后恢复?
- css - 按钮上的 zIndex -1 使按钮无法点击