2.15 Python 中的广播(Broadcasting in Python)

这是一个不同食物(每100g)中不同营养成分的卡路里含量表格,表格为3行4列,列表示不同的食物种类,从左至右依次为苹果,牛肉,鸡蛋,土豆。行表示不同的营养成分,从上到下依次为碳水化合物,蛋白质,脂肪。

那么,我们现在想要计算不同食物中不同营养成分中的卡路里百分比。
现在计算苹果中的碳水化合物卡路里百分比含量,首先计算苹果(100g)中三种营养成分卡路里总和56+1.2+1.8 = 59,然后用56/59 = 94.9%算出结果。
可以看出苹果中的卡路里大部分来自于碳水化合物,而牛肉则不同。
对于其他食物,计算方法类似。首先,按列求和,计算每种食物中(100g)三种营养成分总和,然后分别用不用营养成分的卡路里数量除以总和,计算百分比。
那么,能否不使用for循环完成这样的一个计算过程呢?
假设上图的表格是一个3行4列的矩阵A,记为 A_(3×4),接下来我们要使用Python的numpy库完成这样的计算。我们打算使用两行代码完成,第一行代码对每一列进行求和,第二行代码分别计算每种食物每种营养成分的百分比。
在jupyter notebook中输入如下代码,按shift+Enter运行,输出如下。

下面使用如下代码计算每列的和,可以看到输出是每种食物(100g)的卡路里总和。

其中sum的参数axis=0表示求和运算按列执行,之后会详细解释。
接下来计算百分比,这条指令将 3×4的矩阵A除以一个1×4的矩阵,得到了一个 3×4的结果矩阵,这个结果矩阵就是我们要求的百分比含量。

算是沿着哪个轴执行,在numpy中,0轴是垂直的,也就是列,而1轴是水平的,也就是行。
而第二个A/cal.reshape(1,4)指令则调用了numpy中的广播机制。这里使用 3×4的矩阵A除以 1×4的矩阵cal。技术上来讲,其实并不需要再将矩阵cal reshape(重塑)成 1×4,因为矩阵cal本身已经是 1×4了。但是当我们写代码时不确定矩阵维度的时候,通常会对矩阵进行重塑来确保得到我们想要的列向量或行向量。重塑操作reshape是一个常量时间的操作,时间复杂度是O(1),它的调用代价极低。
那么一个 3×4 的矩阵是怎么和 1×4的矩阵做除法的呢?让我们来看一些更多的广播的例子。

在numpy中,当一个 4×1的列向量与一个常数做加法时,实际上会将常数扩展为一个 4×1的列向量,然后两者做逐元素加法。结果就是右边的这个向量。这种广播机制对于行向量和列向量均可以使用。
再看下一个例子。

用一个 2×3的矩阵和一个 1×3 的矩阵相加,其泛化形式是 m×n 的矩阵和 1×n的矩阵相加。在执行加法操作时,其实是将 1×n 的矩阵复制成为 m×n 的矩阵,然后两者做逐元素加法得到结果。针对这个具体例子,相当于在矩阵的第一列加100,第二列加200,第三列加300。这就是在前一张幻灯片中计算卡路里百分比的广播机制,只不过这里是除法操作(广播机制与执行的运算种类无关)。
下面是最后一个例子

这里相当于是一个 m×n 的矩阵加上一个 m×1 的矩阵。在进行运算时,会先将 m×1 矩阵水平复制 n 次,变成一个 m×n 的矩阵,然后再执行逐元素加法。
广播机制的一般原则如下:

这里我先说一下我本人对numpy广播机制的理解,再解释上面这张幻灯片。
首先是numpy广播机制


现在解释上图中的例子


最后,对于Matlab/Octave 有类似功能的函数bsxfun。
总结一下broadcasting,可以看看下面的图:

2.16 关于 python _ numpy 向量的说明(A note on python or numpy vectors)

本节主要讲Python中的numpy一维数组的特性,以及与行向量或列向量的区别。并介绍了老师在实际应用中的一些小技巧,去避免在coding中由于这些特性而导致的bug。
Python的特性允许你使用广播(broadcasting)功能,这是Python的numpy程序语言库中最灵活的地方。而我认为这是程序语言的优点,也是缺点。优点的原因在于它们创造出语言的表达性,Python语言巨大的灵活性使得你仅仅通过一行代码就能做很多事情。但是这也是缺点,由于广播巨大的灵活性,有时候你对于广播的特点以及广播的工作原理这些细节不熟悉的话,你可能会产生很细微或者看起来很奇怪的bug。例如,如果你将一个列向量添加到一个行向量中,你会以为它报出维度不匹配或类型错误之类的错误,但是实际上你会得到一个行向量和列向量的求和。
在Python的这些奇怪的影响之中,其实是有一个内在的逻辑关系的。但是如果对Python不熟悉的话,我就曾经见过的一些学生非常生硬、非常艰难地去寻找bug。所以我在这里想做的就是分享给你们一些技巧,这些技巧对我非常有用,它们能消除或者简化我的代码中所有看起来很奇怪的bug。同时我也希望通过这些技巧,你也能更容易地写没有bug的Python和numpy代码。
为了演示Python-numpy的一个容易被忽略的效果,特别是怎样在Python-numpy中构造向量,让我来做一个快速示范。首先设置a=np.random.randn(5),这样会生成存储在数组 a 中的5个高斯随机数变量。之后输出 a,从屏幕上可以得知,此时 a 的shape(形状)是一个(5,)的结构。这在Python中被称作一个一维数组。它既不是一个行向量也不是一个列向量,这也导致它有一些不是很直观的效果。举个例子,如果我输出一个转置阵,最终结果它会和a看起来一样,所以a和a的转置阵最终结果看起来一样。而如果我输出a和a的转置阵的内积,你可能会想:a乘以a的转置返回给你的可能会是一个矩阵。但是如果我这样做,你只会得到一个数。


所以我建议当你编写神经网络时,不要在它的shape是(5,)还是(n,)或者一维数组时使用数据结构。相反,如果你设置 a 为(5,1),那么这就将置于5行1列向量中。在先前的操作里 a 和 a 的转置看起来一样,而现在这样的 a 变成一个新的 a 的转置,并且它是一个行向量。请注意一个细微的差别,在这种数据结构中,当我们输出 a 的转置时有两对方括号,而之前只有一对方括号,所以这就是1行5列的矩阵和一维数组的差别。

如果你输出 a 和 a 的转置的乘积,然后会返回给你一个向量的外积,是吧?所以这两个向量的外积返回给你的是一个矩阵。





我有时候看见学生因为一维数组不直观的影响,难以定位bug而告终。通过在原先的代码里清除一维数组,我的代码变得更加简洁。而且实际上就我在代码中表现的事情而言,我从来不使用一维数组。因此,要去简化你的代码,而且不要使用一维数组。总是使用 n×1 维矩阵(基本上是列向量),或者 1×n 维矩阵(基本上是行向量),这样你可以减少很多assert语句来节省核矩阵和数组的维数的时间。另外,为了确保你的矩阵或向量所需要的维数时,不要羞于 reshape 操作。

总之,我希望这些建议能帮助你解决一个Python中的bug,从而使你更容易地完成练习。