8. NumPy的广播机制

Broadcasting即广播,是NumPy里的一种神奇机制,它通常发生在两个具有不同shape数组做加减运算时,规则晦涩难懂。

8.1 数组的基础

要想深入理解广播的本质,首先得回忆一下数组的一些属性信息。我们可以通过Numpy的array函数、linspace、arange等函数来创建一些不同维度数组。

from numpy import *
import numpy as np
a = np.array([1, 2, 3])
print a
print a.shape
print a.ndim
b = arange(0, 10).reshape([2, 5])
print b
print b.shape
print b.ndim

程序的执行结果:

[1 2 3]# a
(3,)
1
[[0 1 2 3 4]
 [5 6 7 8 9]] # b
(2, 5)
2

数组的ndim属性是说明数组的维度是多少,a是一维数组,b是二维数组,而shape属性分别给出各个维度的长度,例如a是一维数组,有3个元素,长度为3,记作(3, );b是二维数组各个维度上的数值是2和5可以记作(2, 5),这里的2和5分为是各个周的长度。a和b形状不同。而一个纯数据例如c = 3它本身无维度但可以记作(0,)。

8.2 数组基本运算

数组可以做一些简单的加减乘除等运算,这个知识之前已经学习过了。

from numpy import *
import numpy as np
a = np.arange(0,10)
print a
print a.shape, a.ndim
a = a.reshape([2, 5])
print a
print a.shape, a.ndim
print 2 * a

程序执行结果:

[0 1 2 3 4 5 6 7 8 9]
(10,) 1
[[0 1 2 3 4]
 [5 6 7 8 9]]
(2, 5) 2
[[ 0  2  4  6  8]
 [10 12 14 16 18]]

程序较为简单,关键是语句2 * a看似没啥,幕后的机制就是广播机制。

8.3 广播(计算)

什么是广播(计算)?广播是两数组形同、对应位置上的元素做某种运算,如a和b数组做加法或减法,那么结果是a和b对应位置上的数据加、减后的计算值作为结果这个位置上的数据。

from numpy import *
import numpy as np
a = np.arange(10,20).reshape([2, 5])
b = np.arange(22,32).reshape([2, 5])
print a
print b
print a + b
print b * 1.0 / a

看结果:

[[10 11 12 13 14]
 [15 16 17 18 19]]
[[22 23 24 25 26]
 [27 28 29 30 31]]
[[32 34 36 38 40]
 [42 44 46 48 50]]
[[ 2.2         2.09090909  2.          1.92307692  1.85714286]
 [ 1.8         1.75        1.70588235  1.66666667  1.63157895]]

这就是广播。这里a + b运算,a和b具有相同的shape都是(2, 5),而前边的语句2 * a 的数2和数组a的shape是不同的,那怎么也能正确计算并得到结果了呢?这是因为对于不同的shape的数组NumPy使用了广播机制来预处理一下表达式里的数组使其最终可以实现广播计算功能,注意这里广播和广播机制是两个概念。

8.4 广播机制

广播机制是Numpy让两个不同shape的数组能够做一些运算,需要对参与运算的两个数组做一些处理或者说扩展,最终是参与运算的两个数组的shape一样,然后广播计算(对应位置数据进行某运算)得到结果。

广播时需要对两个数组做广播机制处理,不是所有情况下两个数组都能进行广播机制的处理,有要求即两数组需满足广播兼容,需判断两个数组能否进行广播机制处理成同型shape的数组然后广播。

广播机制首先需要判断参与计算的两个数组能否被广播机制处理?即判断是否广播兼容,规则是,比较两个数组的shape,从shape的尾部开始一一比对。

(1). 如果两个数组的维度相同,对应位置上轴的长度相同或其中一个的轴长度为1,广播兼容,可在轴长度为1的轴上进行广播机制处理。

(2). 如果两个数组的维度不同,那么给低维度的数组前扩展提升一维,扩展维的轴长度为1,然后在扩展出的维上进行广播机制处理。

举例说明:

from numpy import *
import numpy as np
a = np.arange(1, 16).reshape([3, 5])
print a
print a.shape, a.ndim
b = array([2, 3, 4, 5, 6])
print b
print b.shape, b.ndim
print a + b

这里a数组的维度是2即二维数组(3, 5),b是一维数组(5,),判断一下是否广播兼容?b的最后一维的轴长度为5、a的最后一维的轴长度5,满足从后向前开始对应轴的轴长度相同规则。两个数组的维度不同一个是2维一个是1维,那么对b前扩展增加维度变为(1, 5)成为2维数组,此时a和b都是二维数组,维度相同。增维后的b的倒数第二维的长度是1,a倒数第二维轴长度为3,满足维度相同某轴长度为1的兼容规则,继续广播机制,将b倒数第二维轴长度继续加1直至变为和a数组倒数第二轴轴长度相同,这是b的shape经过不断的调整(广播机制)变为(3, 5),和a的shape相同,就可以广播计算了。每次对b倒数第二轴轴长度加1实际是拷贝增加一行。

8.4.1 数组b广播机制变化过程

1).原始b数组(5,)

[2 3 4 5 6]

2). b增加一维(1, 5)变为2维数组

[[2 3 4 5 6]]

3).b扩展变为(2, 5)

[[2 3 4 5 6],
[2 3 4 5 6]]

4).b扩展变为(3, 5),这是b的shape和a的shape一致。

[[2 3 4 5 6],
[2 3 4 5 6],
[2 3 4 5 6]]

从过程可以看出,广播机制在将两数组变成维度和shape相同时,对实施广播机制的数组的处理是拷贝赋值未广播处理的轴的数据。

8.4.2 语句2 * a的广播机制过程

程序如下:

from numpy import *
import numpy as np
a = np.arange(0,10)
print a
print a.shape, a.ndim
a = a.reshape([2, 5])
print a
print a.shape, a.ndim
print 2 * a

这里a的shape是(2, 5),而语句2 * a参与计算的一个是数一个是数组,数可以看出(0,)数组,那么语句的广播机制处理过程如下:

1). 数2先变成数组,shape为(1,)

[2]

2). [2]和a数组的广播兼容判断,[2]的shape是(1,),a的shape是(2, 5),维度不同一个是一维的数组,一个是二维的数组。

3). [2]变化成[2 2 2 2 2]先保证两个数组最后一维上的长度相等,那么[2]变化为[2 2 2 2 2],这时的shape为(5,),好了此时参与运算的两数组最后一轴上的长度相同了。

4). 倒数第二轴判断是否广播兼容?[2 2 2 2 2]的shape是(5,)而a的shape是(2, 5),两个数组维度不同,好对[2 2 2 2 2]进行前扩展增维变为(1, 5)即

[[2 2 2 2 2]]

[[2 2 2 2 2]]的shape是(1, 5)而a的shape是(2, 5),此时对[[2 2 2 2 2]]广播扩展变为:

[[2 2 2 2 2],[2 2 2 2 2]]

[[2 2 2 2 2],[2 2 2 2 2]]的shape变为(2, 5)和a同型的的数组,就可以进行广播计算了,得到结果。

[[ 0  2  4  6  8]
 [10 12 14 16 18]] # 2 * a

8.5 较复杂的广播示例

之前的广播机制处理示例较为简单,只在一个数组的一个维度上进行了广播机制处理,下面看一个例子,两个数组都被广播机制处理了。

from numpy import *
import numpy as np
a = np.arange(1, 5)
b = np.arange(4, 7).reshape((3, 1))
print a
print b
print a + b

程序执行结果:

[1 2 3 4]
[[4]
 [5]
 [6]]
[[ 5  6  7  8]
 [ 6  7  8  9]
 [ 7  8  9 10]]

a是[1 2 3 4]被广播成了

[[1 2 3 4],
 [1 2 3 4],
 [1 2 3 4]]

b原始为[[4] [ 5] [6]]被广播成

[[4 4 4 4],
 [5 5 5 5],
 [6 6 6 6]]

8.5.1 b的变化过程

b变化,首先要和a做广播机制兼容判断,a的shape为(4,)b的shape为(3, 1),从最后向前逐一比对各个轴长度是否相对或为1。a和b是不同维度,a的最后一维的轴长度为4,而b最后一维轴长度为1。那么b最后一维轴长度需要从1变成4。

1). b原始(3, 1)

[[4] [ 5] [6]]

2). b变为(3, 4)记作b‘

[[4 4 4 4],
 [5 5 5 5],
 [6 6 6 6]]

广播机制处理后的b的shape为(3, 4),其最后一维的轴长度4和a的最后一维的轴长度4相等,那就要比对a和b倒数第二维的轴长度是否相对或者为1,缺失前扩展增维。

8.5.2 a的变化过程

此时a还是(4,)a是一维数组,和变化后的b倒数最后一维的轴长度相等,b数组是二维,a是一维,倒数第一维已经满足广播机制兼容条件,可以对a进行前扩展增维变为(1, 4),然后不断的广播机制处理a看最后a能否和b实现广播计算。

1). a原始的shape(4,)

[1 2 3 4]

2). 前增维扩展变为(1, 4)

[[1 2 3 4]]

此时的a倒数第一维的轴长度和变化后b即(3, 4)最后一维的轴长度均是4相等。倒数第二维轴长度a的是1,b的是3,满足兼容规则,那么把变化后的a的倒数第二维进行广播机制处理使其倒数第二维的轴长度变化成和变化后b倒数第二维的轴长度相等。

3). a倒数第二维轴长度继续变化成(3, 4),记作a'

[[1 2 3 4],
 [1 2 3 4],
 [1 2 3 4]]

这是的a的shape为(3, 4)和变化后的b的shape一致,那么此时便可对变化后的a'和b'做加法广播计算了,得到结果:

[[4 4 4 4] +   [[1 2 3 4] = [[5 6 7 8]
 [5 5 5 5]      [1 2 3 4]    [6 7 8 9]
 [6 6 6 6]]     [1 2 3 4]]   [7 8 9 10]]

8.6 广播不兼容示例

上边的示例都是广播能够兼容的能对两数组进行广播机制处理周后进行广播计算,现实程序里会出现两数组不能进行广播机制处理即不能广播计算的情况,下面来分析一下不兼容的情况案例,从而更好的理解广播机制。

from numpy import *
import numpy as np
a = np.arange(1, 9).reshape((2, 4))
x = np.arange(4, 6).reshape((1, 2))
b = x.reshape((2, 1))
print a, "# a"
print b, "# b"
print a + b, "# a + b"
c = x.reshape((1, 2))
print c, "# c"
print a + c, "# a + c"

这个程序有错误,在最后一行,程序执行结果如下:

[[1 2 3 4]
 [5 6 7 8]] # a
[[4]
 [5]] # b
[[ 5  6  7  8]
 [10 11 12 13]] # a + b
[[4 5]] # c
Traceback (most recent call last):
  File "t8b.py", line 11, in <module>
    print a + c, "# a + c"
ValueError: operands could not be broadcast together with shapes (2,4) (1,2) 

从程序执行结果可以看出a + b被执行了广播机制处理,a和b形状不同但能进行加法求和,而a + c报错异常ValueError: operands could not be broadcast together with shapes (2,4) (1,2)信息提示不能执行广播机制处理。

8.6.1 a、b满足广播兼容

这里a是二维数组,shape为(2, 4),b是二维数组,shape是(2, 1),a和b维度都是2,需要从后向前一次比较各轴的轴长度,如果相同或某轴长度为1,广播兼容可以进行广播机制处理后进行广播计算得到结果。a的最后一维的轴长度是4,而b最后一维的轴长度是1,满足兼容规则,需要将b最后一轴的轴长度从1变化到和a最后一轴的轴长度相同即b变为(2, 4),那么b变化后为:

[[4 4 4 4],
 [5 5 5 5]]

这是的b和a完全同型即可广播计算求得a+b的值。

[[ 5  6  7  8]
 [10 11 12 13]] # a + b

8.6.2 a、c不满足广播兼容

b和c都是从x变化而来,为何a + c不行呢?这里c的shape是(1, 2),a的shape是(2, 4),在进行a + c运算前需要判断两个数组是否可广播?如果广播机制兼容规则是从前向后那么a的最高维长度为4、c的最高维轴长度为1满足兼容判断规则应该可以,从程序执行结果看不行,显然这个例子证明了兼容判定规则是从最后向前一次比对轴长度判断的说法。那么好按兼容判定规则说的从后向前一次比对是否相等或有一个轴长度为1呢?c的最后一维轴长度为2而a轴后一维轴长度为4不相等,那么可以断定c和a两个数组不满足广播兼容规则,故a和c不能直接相加。

8.7 思考与寄语

一个三维数组的shape是(3, 4, 5),您能写出多少个不同shape的二维数组能和这个三维数组做广播计算?

可发邮件给本人:mailliao艾特126点儿com。

NumPy的广播机制和重要,在机器学习、深度学习里的tensorflow和keras里使用了大量的广播计算,需深入理解广播机制。

感谢Klang(金浪)智能数据看板klang.org.cn鼎力支持!