不同维度间数组的运算

在很多人工智能或者数据处理的流程中,我们会经常遇见大量的数组处理问题。很多时候我们需要处理不同维度的数组,或者做改变数组维度等操作。一个优秀的工程师,尤其是一个愿意沉浸在数据变化过程中的工程师必须熟练的掌握处理不同维度的数组之间运算的问题。

在实际应用中,很多数据操作都会改变数组的维度。例如对3维数组的最后一维度做求和操作,则会把原来的数组变成2维的。

>>> a = torch.tensor([[[1,2,3]], [[2,3,4]]])
>>> a.sum(-1)
tensor([[6],
        [9]])
>>> a.sum(-1).shape
torch.Size([2, 1])
>>> a.shape
torch.Size([2, 1, 3])

然后,我们又会有很多运算会对原维度的数据和改变维度后的数组之间做四则运算。例如,我们想让a数组中最后一个维度的数据,每一个都减去其当前维度的和。比如说,我们想让torch.tensor([[[1,2,3]], [[2,3,4]]])变成 torch.tensor([ [[1-6, 2- 6, 3 - 6]], [[2-9, 3-9, 4-9]] ])。那我们里所当然想尝试这么做:

>>> a
tensor([[[1, 2, 3]],
        [[2, 3, 4]]])
>>> a - torch.tensor([6,9])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: The size of tensor a (3) must match the size of tensor b (2) at non-singleton dimension 2

那么,我们看到,这么直观的让a - [6,9]是不行的。要准确的实现我们的运算想法,就必须得先充分理解不同数组之间做运算的机制。

  1. 一般情况下,不同维度数量,或者同维度的长度不一样的数组之间是不能做四则运算的。标准的数组之间的四则运算只能运行在具备同样维度数量,且每个相对应维度的长度都一样长的数组之间进行。
  2. 不同维度,不同维度长度之间的数组之间只能在各自分别执行自动适配后,双双能满足1的条件后,它们才能达成运算条件。
  3. 数组的自动适配分两个步骤,第一,自动统一维度数量,这个步骤由维度数量少的数组,按最后一个维度对齐,然后再在前面补维度来与维度数量大的数组对齐;第二,在维度数量对齐后,分别对每个维度做长度的对齐操作,如果某个维度上它们长度相同,则看作已经对齐。如果长度比较短的那个数组在该维度的长度为1,则对该数字在该维度上做维度长度的扩张操作,且补齐的数据为那1个数据的拷贝。如果长度较短的那个数组在该维度的长度不为1,则视为此对数组无法自动适配。维度长度的对齐操作是从最后一个维度开始到第0维度结尾的顺序执行。

以上这段定义,可能主要是自动适配的描述不方便理解。下面举例说明:

>>> a
tensor([[[1, 2, 3]],
        [[2, 3, 4]]])
>>> b = torch.tensor([6,9])
>>> a - b

执行a - torch.tensor([6,9])的操作时,首先要让数组的维度数量对齐。a 的shape为[2,1,3], b的shape为[2], 于是首先b的shape会补充为[1,1,2], 即b变成[[[6,9]]]。然后,从最后一个维度开始做维度的长度对齐。b最后一个维度的长度为2, a为3。因此,a, b是无法自动适配了。因为有这条规定"如果长度较短的那个数组在该维度的长度不为1,则视为此对数组无法自动适配。"。因此,我们要想a - b能够执行,则需要b在完成第一步维度数量扩展后的shape为[1,1,1]或者[1,1,3]。我们改变下b的形态:

>>> b = torch.tensor([[6], [9]])
>>> a - b
tensor([[[-5, -4, -3],
         [-8, -7, -6]],

        [[-4, -3, -2],
         [-7, -6, -5]]])

我们这样改变以后,a - b能够顺利执行了。因为b 首先会适配成[[[6], [9]]],然后会适配成[ [[6,6,6], [9,9,9]], [[6,6,6], [9,9,9]] ], 这样b的shape变成了[2,2,3], 而a还是[2,1,3];那么因为a的第二维度的长度为1, 因此a的维度也会自动适配,适配之后的a变为[ [[1,2,3], [1,2,3]], [[2,3,4], [2,3,4]]]。所以这个时候做a - b运算,实际上,在完成互相适配后,是在做如下数组之间的运算:

a - b = [ [[1,2,3], [1,2,3]], [[2,3,4], [2,3,4]] ] - 
[ [[6,6,6], [9,9,9]], [[6,6,6], [9,9,9]] ]

因此也就得到了上述答案:

>>> a - b
tensor([[[-5, -4, -3],
         [-8, -7, -6]],

        [[-4, -3, -2],
         [-7, -6, -5]]])

但,这不是我们最初想要的答案。我们想要的是torch.tensor([[[1,2,3]], [[2,3,4]]])变成 torch.tensor([ [[1-6, 2- 6, 3 - 6]], [[2-9, 3-9, 4-9]] ])。为了达成我们的效果,我们可以做逆向思维。我们想要的答案需要对如下两个数组运算才能达到:

a - b = [ [[1,2,3]], [[2,3,4]] ] - 
[[[6,6,6]], [[9,9,9]]]

那么,接下来我们对[[[6,6,6]], [[9,9,9]]]做压缩,首先做维度长度的压缩,把这个数组变成[[[6]], [[9]]],这个数组的shape变成了[2,1,1],然后第0个维度的长度已经无法进行压缩了,因为[[6]]和[[9]]不相等。接下来,我们看维度数量能不能压缩,压缩维度数量只能搜索排序在前面而且维度长度为1的维度。这里,第0维度的长度是2,因此维度数量无法再进行压缩。所以最后要达成我们的效果b的最简形式为[[[6]], [[9]]]。接下来,让我们验证一下:

>> a 
tensor([[[1, 2, 3]],

        [[2, 3, 4]]])
>>> b = torch.tensor([[[6]], [[9]]])
>>> a - b
tensor([[[-5, -4, -3]],

        [[-7, -6, -5]]])

这个结果,是符合我们预期的。我们知道 a.sum(-1) = [[6], [9]]的,那我们如何优雅的让a.sum(-1)变成当前b的形态,然后再做a - b的运算呢?答案是,使用unsqueeze函数。

>>> a
tensor([[[1, 2, 3]],

        [[2, 3, 4]]])
>>> a.sum(-1).unsqueeze(-1)
tensor([[[6]],

        [[9]]])
>>> a - a.sum(-1).unsqueeze(-1)
tensor([[[-5, -4, -3]],

        [[-7, -6, -5]]])

unsqueeze函数能够给指定维度位置添加维度。我们可以利用它来改变数组的维度。

通过以上说明和示例,我们应该理解到,本质上不同维度形状的数组是不能做四则运算的。要进行四则运算,则需要自动适配。要自动适配成功,就必须满足一些条件。要想利用自动适配原理,来设计不同维度形状之间的数组做运算,可以采用反推的方法,看看当前的数组需要进行怎样变形或者不变形来达到自己的需求。

发表评论

邮箱地址不会被公开。 必填项已用*标注