1D和2D numpy数组的逐元素加法

3

情况

我有一些对象,它们具有由numpy数组表示的属性:

>> obj = numpy.array([1, 2, 3])

其中123是属性的值。

我即将编写一些方法,这些方法应该能够同样适用于单个对象和一组对象。一组对象由一个二维numpy数组表示:

>>> group = numpy.array([[11, 21, 31],
...                      [12, 22, 32],
...                      [13, 23, 33]])

第一个数字表示对象,第二个数字表示属性。即12是对象1的属性2,21是对象2的属性1。

为什么不颠倒顺序?因为我希望数组索引与属性对应。即object_or_group[0]应该产生第一个属性,无论是作为单个数字还是作为numpy数组,以便进行进一步计算。

好的,所以当我想要计算点积时,例如这样就可以直接使用:

>>> obj = numpy.array([1, 2, 3])
>>> obj.dot(object_or_group)

“元素级加法”无效。
输入:
>>> group
array([[1, 2, 3],
       [4, 5, 6]])
>>> obj
array([10, 20])

结果数组应该是groupobj的第一个元素相加得到的,第二个元素也是类似的:
>>> result = numpy.array([group[0] + obj[0],
...                       group[1] + obj[1]])
>>> result
array([[11, 12, 13],
       [24, 25, 26]])

然而:
>>> group + obj
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: operands could not be broadcast together with shapes (2,3) (2,)

考虑到NumPy的广播规则,这是有道理的。

似乎没有一种NumPy函数可以沿指定轴执行加法(或等效的广播)。虽然我可以使用

>>> (group.T + obj).T
array([[11, 12, 13],
       [24, 25, 26]])

这感觉非常繁琐(如果不考虑组,而是考虑单个对象,则感觉确实不对)。特别是因为numpy覆盖了其使用的每一个角落,我有一种感觉,可能在这里出现了某些概念上的错误。

总之

类似于

>>> obj1
array([1, 2])
>>> obj2
array([10, 20])
>>> obj1 + obj2
array([11, 22])

(执行逐元素 - 或属性 - 加法的函数)我希望对对象组执行相同操作:

>>> group
array([[1, 2, 3],
       [4, 5, 6]])

当使用2D组数组时,其布局必须使单个对象沿第二轴(axis=1)列出,以便能够通过正常索引请求特定属性(或多个属性):obj[0]group[0]应该都返回第一个属性。


我认为你需要非常仔细地考虑使用哪些维度。group[0]group 中的第一个对象,这其实很有道理。而 group[:, 0]group 中所有对象的第一个属性。如果你需要区分一个组和一个对象,只需检查它的 ndim 属性。组将具有 2,对象将具有 1。 - Mad Physicist
那就是重点。我不想从方法的角度区分组和对象。它应该适用于单个对象,并对组表现出类似的行为,而无需进行显式检查。比如说一个方法应该返回第一个属性的平方,那么这就是 obj_or_group[0] ** 2,而不需要检查维度。我希望实现同样的加法,比如一个方法应该返回所有属性加上一些属性向量。我认为 numpy 的一个主要目的是允许在进入更高维度时使用类似的方程。 - a_guest
你可以在像 np.sum 这样的调用中始终指定 axis=-1 - Mad Physicist
点号(.)代表什么意思? - Mad Physicist
当我写下那个评论时,我并没有完全理解你当时的意图。请忽略其中的第二部分。 - Mad Physicist
显示剩余4条评论
2个回答

4

你想要做的事情似乎可以通过这个简单的代码实现!

>>> m
array([[1, 2, 3],
       [4, 5, 6]])
>>> g = np.array([10,20])
>>> m + g[ : , None]
array([[11, 12, 13],
       [24, 25, 26]])


3
您似乎对矩阵的对象和属性的哪一个维度感到困惑,这可以从您示例中对象大小的变化中看出。实际上,正是因为您在交换维度以匹配不断变化的大小,才导致了混淆。您还使用了3x3组的不幸示例来说明点积,这进一步使您的解释变得混乱。
在下面的示例中,对象将是三元向量,即它们每个都有三个属性。示例组将始终具有两行,表示其中有两个对象,并且具有三列,因为对象具有三个属性。
组的第一行,group[0],也称为group [0,:],将是组中的第一个对象。第一列,group [:,0],将是第一个属性。
以下是几个示例对象和组,以说明接下来的要点:
>>> obj1 = np.array([1, 2, 3])
>>> obj2 = np.array([4, 5, 6])
>>> group1 = np.array([[7, 8, 9],
                      [0, 1, 2]])
>>> group2 = np.array([[3, 4, 5]])

现在,由于广播机制的存在,加法可以直接使用:

>>> obj1 + obj2
array([5, 7, 9])
>>> group1 + obj1
array([[ 8, 10, 12],
       [ 1,  3,  5]])

正如您所看到的,相应属性被成功添加。您甚至可以将组合在一起,但前提是它们具有相同的大小,或者其中一个仅包含单个对象:

>>> group1 + group2
array([[10, 12, 14],
       [ 3,  5,  7]])
>>> group1 + group1
array([[14, 16, 18],
       [ 0,  2,  4]])

所有二进制逐元算子都是如此,例如:*-/np.bitwise_and等。
唯一剩下的问题是如何使点积不关心它们是在矩阵还是向量上操作。恰好点积并不关心。您的公共维度始终是属性数,因此第二个操作数(乘数)需要被转置,以便列数变为行数。np.dot(x1, x2.T) 或等效地 x1.dot(x2.T) 将正确地处理无论 x1x2 是组还是对象:
>>> obj1.dot(obj2.T)
32
>>> obj1.dot(group1.T)
array([50,  8])
>>> group1.dot(obj1.T)
array([50,  8])

你可以使用 np.atleast_1dnp.atleast_2d 来始终将结果强制转换为特定的形状,这样你就不会像obj1.dot(obj2.T)这种情况一样得到标量。我建议使用后者,这样无论输入如何,你始终拥有一致的维数。
>>> np.atleast_2d(obj1.dot(obj2.T))
array([[32]])
>>> np.atleast_2d(obj1.dot(group1.T))
array([[50,  8]])

请记住,点积的维度将是第一个操作数中对象的数量乘以第二个操作数中对象的数量(一切都将被视为一组)。属性将被相乘并相加。这是否对您的目的有有效的解释完全由您决定。

更新

目前唯一剩下的问题是属性访问。如上所述,obj1[0]group1[0]意义完全不同。有三种方法来调和这种差异,按我个人的喜好列出,其中1是最可取的:

  1. Use the Ellipsis indexing object to get the last index instead of the first

    >>> obj1[..., 0]
    array([1])
    >>> group1[..., 0]
    array([7, 0])
    

    This is the most efficient way since it does not make any copies, just does a normal index on the original arrays. As you can see, there will be no difference between the result from a single object (1D array) and a group with only one object in it (2D array).

  2. Make all your objects 2D. As you pointed out yourself, this can be done with a decorator, and/or using np.atleast_2d. Personally, I would prefer having the convenience of using 1D arrays as single objects without having to wrap them in 2D.

  3. Always access attributes via a transpose:

    >>> obj1.T[0]
    1
    >>> group1.T[0]
    array([7, 0])
    

    While this is functionally equivalent to #1, it is clunky and unsightly by comparison, in addition to doing something very different under-the-hood. This approach at the very least creates a new view of the underlying array, and may run the risk of making unnecessary copies in certain cases if the group arrays are not laid out just right. I would not recommend this approach even if it does solve the problem if uniform access.


你说得有道理,使用转置布局并确保数组始终为2D可以实现应用程序的一致性。通过装饰器来确保数组始终为2D是可行的,numpy.atleast_2d似乎是一个非常有趣的选择。我还不确定是否会采取这种方式,但这绝对是一个有前途的建议。 - a_guest
我忘记了让属性访问统一化。谢谢提醒。添加了一个更新,其中包含比2D数组更好的解决方案(即在不放弃便利性的情况下更有效地使用numpy)。 - Mad Physicist
谢谢您的更新,使用...看起来确实不错。然而,在使用此语法时,对于包含一个对象的“group”和一个“obj”,会有微妙的差异。“obj[..., 0]”返回“array(0)”而“group[..., 0]”返回“array([0])”。因此(为了保持一致性),我宁愿选择#2。正如提到的,在使用装饰器时,可以将1轴对象包装在2轴组中,然后将它们传递给实际方法(然后在返回时转换回1轴)。 - a_guest
无论如何,我认为这是两种组布局(按行存储或按列存储对象)之间的权衡。使用按行存储(您提出的方式),可以直接进行逐个属性的修改(group + attribute_vector),但逐个对象的修改需要一些技巧(例如,对每个对象添加一个不同的数字以更新该对象的所有属性;需要类似于 (group.T + object_vector).T 或等价的 group + object_vector[:, None])。 - a_guest
@a_guest。尽可能避免使用.T,除非操作已经很昂贵,比如dot。希望我的答案已经表明,只要你稍微调整符号,你可以将对象和组同样对待任何给定的操作。 - Mad Physicist
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接