相信大家在使用numpy和tensorflow的时候都会遇到如下的错误
ValueError: operands could not be broadcast together with shapes (4,3) (4,)这是由于numpy和tensorflow中的张量在进行运算的时候形状不满足广播机制的要求,不理解广播机制的同学可能会通过各种魔改代码来让代码正常运行起来,但是却不知道为什么那样改就可以。
本文将从原理上介绍张量运算中经常用到的广播机制。
广播(broadcasting)指的是不同形状的张量之间的算数运算的执行方式。
通过两个例子直观了解广播
数组与标量值的乘法
import numpy as np
arr = np.arange(5)
arr #-> array([0, 1, 2, 3, 4])
arr * 4 #-> array([ 0, 4, 8, 12, 16])
在上面的乘法运算中,标量值4被广播到了其他所有元素上
通过减去列平均值的方式对数组每一列进行距平化处理
arr = np.random.randn(4,3)
arr #-> array([[ 1.83518156, 0.86096695, 0.18681254],
# [ 1.32276051, 0.97987486, 0.27828887],
# [ 0.65269467, 0.91924574, -0.71780692],
# [-0.05431312, 0.58711748, -1.21710134]])
arr.mean(axis=0) #-> array([ 0.93908091, 0.83680126, -0.36745171])
关于mean
中的axis
参数,可以这样理解:
在numpy
中,axis = 0
为行轴(竖直方向),axis = 1
为列轴(水平方向),指定axis
表示该操作沿axis
进行,得到结果将是一个shape
为除去该axis
的array
,对于多维张量,axis=i
是指运算操作沿着第i
个张量下标变化的方向进行。
在上例中,arr.mean(axis=0)
表示对arr
沿着轴0(竖直方向)求均值。显然,第0
个下标变化的方向即为竖直方向,以第一列为例,4个元素的下标分别为[(0,0),(1,0),(2,0),(3,0)]
。
而arr
的shape为(4,3)
,除去axis=0
的shape,结果为(1,3)
或者(3,)
,这与上面的代码运行结果相符。
广播机制的原理
★如果两个数组的后缘维度(从末尾开始算起的维度)的轴长度相符或其中一方的长度为1,则认为它们是广播兼容的。广播会在缺失维度和(或)轴长度为1的维度上进行。
”
demeaned = arr - arr.mean(axis=0)
demeaned
> array([[ 0.89610065, 0.02416569, 0.55426426],
[ 0.3836796 , 0.1430736 , 0.64574058],
[-0.28638623, 0.08244448, -0.35035521],
[-0.99339402, -0.24968378, -0.84964963]])
demeaned.mean(axis=0)
> array([ -5.55111512e-17, -5.55111512e-17, 0.00000000e+00])
在上面的对arr
每一列减去列平均值的例子中,arr
的后缘维度为3
,arr.mean(0)
后缘维度也是3
,满足轴长度相符的条件,广播会在缺失维度进行。
这里有点奇怪的是缺失维度不是axis=1
,而是axis=0
,个人理解是缺失维度指的是两个arr
除了轴长度匹配的维度,在上面的例子中,正好是axis=0
。
arr.mean(0)
沿着axis=0
广播,可以看作是把arr.mean(0)
沿着竖直方向复制4份,即广播的时候arr.mean(0)
相当于一个shape=(4,3)
的数组,数组的每一行均相同,均为arr.mean(0)
各行减去行均值
row_means = arr.mean(axis=1)
row_means.shape
> (4,)
arr - row_means
> ---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
-10-3d1314c7e700> in ()
----> 1 arr - row_means
ValueError: operands could not be broadcast together with shapes (4,3) (4,)
直接相减,报错,无法进行广播。
回顾上面的原则,要么满足后缘维度轴长度相等,要么满足其中一方长度为1。在这个例子中,两者均不满足,所以报错。根据广播原则,较小数组的广播维必须为1。解决方案是为较小的数组添加一个长度为1的新轴。
numpy
提供了一种通过索引机制插入轴的特殊语法。通过特殊的np.newaxis
属性以及“全”切片来插入新轴。
下面的例子中,我们通过插入新轴的方式实现二维数组各行减去行均值。这里将行均值沿着水平方向进行广播,广播轴为axis=1
,对row_means
添加一个新轴axis=1
row_means[:,np.newaxis].shape
> (4, 1)
arr - row_means[:,np.newaxis]
> array([[ 0.87419454, -0.10002007, -0.77417447],
[ 0.46245243, 0.11956678, -0.58201921],
[ 0.36798351, 0.63453458, -1.00251808],
[ 0.17378588, 0.81521647, -0.98900235]])
另一个例子
a = np.array([1,2,3])
a.shape # -> (3,)
b = np.array([[1,],[2,],[3]]) # -> (3,1)
b - a # -> array([[ 0, -1, -2],
# [ 1, 0, -1],
# [ 2, 1, 0]])
上面的例子输出为什么是一个3*3的数组?
我们来分析一下,根据广播原则,b满足其中一方轴长度为1,那么广播会沿着长度为1的轴,及axis=1
进行,对数组b沿着axis=1
即水平方向进行复制,相当于b变成一个shape
为(3,3)
且各列均为[1,2,3]
的数组。
一个维度为(3,3)
的数组减去一个维度为(3,)
的数组,满足后缘维度轴长度相等,数组a沿着axis=0
即竖直方向进行广播,相当远a变成一个shape
为(3,3)
且个行均为[1,2,3]
的数组。
b-a的时候,
b被广播成为
[[1,1,1],
[2,2,2],
[3,3,3]]
a被广播成为
[[1,2,3],
[1,2,3],
[1,2,3]]
所以b-a的结果是
[[0,-1,-2],
[1, 0,-1],
[2, 1, 0]]
三维情况
下面的例子中,构造一个3*4*5
的随机数组arr_3d
,我们希望实现对arr_3d
的每个元素减去其深度(axis=2)方向的均值
#构造三维数组
arr_3d = np.random.randn(3,4,5)
#求深度方向的均值,想想结果的shape是什么?原始shape是(3,4,5)
#除去axis=2后还剩(3,4)
depth_means = arr_3d.mean(axis=2)
depth_means.shape
> (3, 4)
#arr(3,4,5)和depth_means(3,4)不能直接广播,后缘维度不相符且不存在轴长度为1的轴
arr_3d_new = arr_3d - depth_means[:,:,np.newaxis] #所以我们添加广播轴
arr_3d_new.mean(axis=2) #结果应该为0,这里是接近0的浮点数,符合预期
> array([[ -5.55111512e-17, 4.44089210e-17, 4.44089210e-17,
4.44089210e-17],
[ -8.88178420e-17, -1.11022302e-16, -6.66133815e-17,
0.00000000e+00],
[ 0.00000000e+00, -7.77156117e-17, -2.22044605e-17,
-2.22044605e-17]])