`numpy.einsum` 中的 `out` 参数无法按预期工作
The `out` arguments in `numpy.einsum` can not work as expected
我有两段代码。第一个是:
A = np.arange(3*4*3).reshape(3, 4, 3)
P = np.arange(1, 4)
A[:, 1:, :] = np.einsum('j, ijk->ijk', P, A[:, 1:, :])
结果A
是:
array([[[ 0, 1, 2],
[ 6, 8, 10],
[ 18, 21, 24],
[ 36, 40, 44]],
[[ 12, 13, 14],
[ 30, 32, 34],
[ 54, 57, 60],
[ 84, 88, 92]],
[[ 24, 25, 26],
[ 54, 56, 58],
[ 90, 93, 96],
[132, 136, 140]]])
第二个是:
A = np.arange(3*4*3).reshape(3, 4, 3)
P = np.arange(1, 4)
np.einsum('j, ijk->ijk', P, A[:, 1:, :], out=A[:,1:,:])
结果A
是:
array([[[ 0, 1, 2],
[ 0, 0, 0],
[ 0, 0, 0],
[ 0, 0, 0]],
[[12, 13, 14],
[ 0, 0, 0],
[ 0, 0, 0],
[ 0, 0, 0]],
[[24, 25, 26],
[ 0, 0, 0],
[ 0, 0, 0],
[ 0, 0, 0]]])
所以结果不一样。这里我想用out
来节省内存。这是 numpy.einsum
中的错误吗?或者我错过了什么?
对了,我的numpy
版本是1.13.3。
我以前没有使用过这个新的 out
参数,但过去曾使用过 einsum
,并且大致了解它的工作原理(或至少曾经使用过)。
在我看来,它在迭代开始之前将 out
数组初始化为零。这将解释 A[:,1:,:]
块中的所有 0。相反,如果我初始化单独的 out
数组,则插入所需的值
In [471]: B = np.ones((3,4,3),int)
In [472]: np.einsum('j, ijk->ijk', P, A[:, 1:, :], out=B[:,1:,:])
Out[472]:
array([[[ 3, 4, 5],
[ 12, 14, 16],
[ 27, 30, 33]],
[[ 15, 16, 17],
[ 36, 38, 40],
[ 63, 66, 69]],
[[ 27, 28, 29],
[ 60, 62, 64],
[ 99, 102, 105]]])
In [473]: B
Out[473]:
array([[[ 1, 1, 1],
[ 3, 4, 5],
[ 12, 14, 16],
[ 27, 30, 33]],
[[ 1, 1, 1],
[ 15, 16, 17],
[ 36, 38, 40],
[ 63, 66, 69]],
[[ 1, 1, 1],
[ 27, 28, 29],
[ 60, 62, 64],
[ 99, 102, 105]]])
einsum
的 Python 部分告诉我的不多,除了它如何决定将 out
数组传递给 c
部分,(作为一个tmp_operands
的列表):
c_einsum(einsum_str, *tmp_operands, **einsum_kwargs)
我知道它设置了一个 c-api
等效于 np.nditer
,使用 str
定义轴和迭代。
它在迭代教程中迭代类似于此部分的内容:
https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.nditer.html#reduction-iteration
请特别注意 it.reset()
步骤。在迭代之前将 out
缓冲区设置为 0。然后它遍历输入数组和输出数组的元素,将计算值写入输出元素。因为它做的是产品总和(例如out[:] += ...
),所以它必须从头开始。
我有点猜测实际发生了什么,但对我来说似乎合乎逻辑的是,它应该首先将输出缓冲区清零。如果该数组与其中一个输入相同,那将最终导致计算混乱。
所以我认为这种方法不会奏效,也不会节省您的记忆。它需要一个干净的缓冲区来累积结果。完成后,或者您可以将值写回 A
。但是鉴于 dot
类产品的性质,您不能对输入和输出使用相同的数组。
In [476]: A[:,1:,:] = np.einsum('j, ijk->ijk', P, A[:, 1:, :])
In [477]: A
Out[477]:
array([[[ 0, 1, 2],
[ 3, 4, 5],
[ 12, 14, 16],
[ 27, 30, 33]],
....)
在 einsum
的 C 源代码中,there is a section 将采用 out
指定的数组并进行一些零设置。
但是在 Python source code 中,例如,有执行路径调用 tensordot
函数,然后再降低参数以调用 c_einsum
。
这意味着某些操作可能会在 tensordot
、before 任何 sub -array 曾经被 einsum 的 C 代码中的零 setter 设置为零。
另一种说法是:在进行下一次收缩操作时,NumPy 有很多选择可供选择。直接使用 tensordot
而不进入 C 级 einsum 代码?或者准备参数并传递给 C 级别(这将涉及用全零覆盖输出数组的某些子视图)?或者重新排序操作并重复检查?
根据它为这些优化选择的顺序,您最终可能会得到意想不到的全零子数组。
最好的办法是不要尝试变得如此聪明并为输出使用相同的数组。你说是因为你想节省内存。是的,在某些特殊情况下,einsum 操作可能就地执行。但它目前不检测是否是这种情况并试图避免置零。
并且在大量情况下,在整个操作的中间覆盖输入数组之一会导致很多问题,就像尝试附加到您直接循环的列表等。
我有两段代码。第一个是:
A = np.arange(3*4*3).reshape(3, 4, 3)
P = np.arange(1, 4)
A[:, 1:, :] = np.einsum('j, ijk->ijk', P, A[:, 1:, :])
结果A
是:
array([[[ 0, 1, 2],
[ 6, 8, 10],
[ 18, 21, 24],
[ 36, 40, 44]],
[[ 12, 13, 14],
[ 30, 32, 34],
[ 54, 57, 60],
[ 84, 88, 92]],
[[ 24, 25, 26],
[ 54, 56, 58],
[ 90, 93, 96],
[132, 136, 140]]])
第二个是:
A = np.arange(3*4*3).reshape(3, 4, 3)
P = np.arange(1, 4)
np.einsum('j, ijk->ijk', P, A[:, 1:, :], out=A[:,1:,:])
结果A
是:
array([[[ 0, 1, 2],
[ 0, 0, 0],
[ 0, 0, 0],
[ 0, 0, 0]],
[[12, 13, 14],
[ 0, 0, 0],
[ 0, 0, 0],
[ 0, 0, 0]],
[[24, 25, 26],
[ 0, 0, 0],
[ 0, 0, 0],
[ 0, 0, 0]]])
所以结果不一样。这里我想用out
来节省内存。这是 numpy.einsum
中的错误吗?或者我错过了什么?
对了,我的numpy
版本是1.13.3。
我以前没有使用过这个新的 out
参数,但过去曾使用过 einsum
,并且大致了解它的工作原理(或至少曾经使用过)。
在我看来,它在迭代开始之前将 out
数组初始化为零。这将解释 A[:,1:,:]
块中的所有 0。相反,如果我初始化单独的 out
数组,则插入所需的值
In [471]: B = np.ones((3,4,3),int)
In [472]: np.einsum('j, ijk->ijk', P, A[:, 1:, :], out=B[:,1:,:])
Out[472]:
array([[[ 3, 4, 5],
[ 12, 14, 16],
[ 27, 30, 33]],
[[ 15, 16, 17],
[ 36, 38, 40],
[ 63, 66, 69]],
[[ 27, 28, 29],
[ 60, 62, 64],
[ 99, 102, 105]]])
In [473]: B
Out[473]:
array([[[ 1, 1, 1],
[ 3, 4, 5],
[ 12, 14, 16],
[ 27, 30, 33]],
[[ 1, 1, 1],
[ 15, 16, 17],
[ 36, 38, 40],
[ 63, 66, 69]],
[[ 1, 1, 1],
[ 27, 28, 29],
[ 60, 62, 64],
[ 99, 102, 105]]])
einsum
的 Python 部分告诉我的不多,除了它如何决定将 out
数组传递给 c
部分,(作为一个tmp_operands
的列表):
c_einsum(einsum_str, *tmp_operands, **einsum_kwargs)
我知道它设置了一个 c-api
等效于 np.nditer
,使用 str
定义轴和迭代。
它在迭代教程中迭代类似于此部分的内容:
https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.nditer.html#reduction-iteration
请特别注意 it.reset()
步骤。在迭代之前将 out
缓冲区设置为 0。然后它遍历输入数组和输出数组的元素,将计算值写入输出元素。因为它做的是产品总和(例如out[:] += ...
),所以它必须从头开始。
我有点猜测实际发生了什么,但对我来说似乎合乎逻辑的是,它应该首先将输出缓冲区清零。如果该数组与其中一个输入相同,那将最终导致计算混乱。
所以我认为这种方法不会奏效,也不会节省您的记忆。它需要一个干净的缓冲区来累积结果。完成后,或者您可以将值写回 A
。但是鉴于 dot
类产品的性质,您不能对输入和输出使用相同的数组。
In [476]: A[:,1:,:] = np.einsum('j, ijk->ijk', P, A[:, 1:, :])
In [477]: A
Out[477]:
array([[[ 0, 1, 2],
[ 3, 4, 5],
[ 12, 14, 16],
[ 27, 30, 33]],
....)
在 einsum
的 C 源代码中,there is a section 将采用 out
指定的数组并进行一些零设置。
但是在 Python source code 中,例如,有执行路径调用 tensordot
函数,然后再降低参数以调用 c_einsum
。
这意味着某些操作可能会在 tensordot
、before 任何 sub -array 曾经被 einsum 的 C 代码中的零 setter 设置为零。
另一种说法是:在进行下一次收缩操作时,NumPy 有很多选择可供选择。直接使用 tensordot
而不进入 C 级 einsum 代码?或者准备参数并传递给 C 级别(这将涉及用全零覆盖输出数组的某些子视图)?或者重新排序操作并重复检查?
根据它为这些优化选择的顺序,您最终可能会得到意想不到的全零子数组。
最好的办法是不要尝试变得如此聪明并为输出使用相同的数组。你说是因为你想节省内存。是的,在某些特殊情况下,einsum 操作可能就地执行。但它目前不检测是否是这种情况并试图避免置零。
并且在大量情况下,在整个操作的中间覆盖输入数组之一会导致很多问题,就像尝试附加到您直接循环的列表等。