本文首发于“生信补给站”公众号 https://mp.weixin.qq.com/s/0-9zUTxZC6qefyjLyw1SSA
本节介绍四大类数组计算,具体有
- 元素层面 (element-wise) 计算
- 线性代数 (linear algebra) 计算
- 元素整合 (element aggregation) 计算
- 广播机制 (broadcasting) 计算
5.1 元素层面计算
Numpy 数组元素层面计算包括:
- 二元运算 (binary operation):加减乘除
- 数学函数:倒数、平方、指数、对数
- 比较运算 (comparison)
先定义两个数组 arr1 和 arr2。
arr1 = np.array([[1., 2., 3.], [4., 5., 6.]])arr2 = np.ones((2,3)) * 2print( arr1 )print( arr2 )
[[1. 2. 3.] [4. 5. 6.]] [[2. 2. 2.] [2. 2. 2.]]
加、减、乘、除
print( arr1 + arr2 + 1 )print( arr1 - arr2 )print( arr1 * arr2 )print( arr1 / arr2 )
[[4. 5. 6.] [7. 8. 9.]] [[-1. 0. 1.] [ 2. 3. 4.]] [[ 2. 4. 6.] [ 8. 10. 12.]] [[0.5 1. 1.5] [2. 2.5 3. ]]
倒数、平方、指数、对数
print( 1 / arr1 )print( arr1 ** 2 )print( np.exp(arr1) )print( np.log(arr1) )
[[1. 0.5 0.33333333] [0.25 0.2 0.16666667]] [[ 1. 4. 9.] [16. 25. 36.]] [[ 2.71828183 7.3890561 20.08553692] [ 54.59815003 148.4131591 403.42879349]] [[0. 0.69314718 1.09861229] [1.38629436 1.60943791 1.79175947]]
比较
arr1 > arr2arr1 > 3
array([[False, False, True], [ True, True, True]]) array([[False, False, False], [ True, True, True]])
从上面结果可知
- 「数组和数组间的二元运算」都是在元素层面上进行的
- 「作用在数组上的数学函数」都是作用在数组的元素层面上的。
- 「数组和数组间的比较」都是在元素层面上进行的
但是在「数组和标量间的比较」时,python 好像先把 3 复制了和 arr1 形状一样的数组 [[3,3,3], [3,3,3]],然后再在元素层面上作比较。上述这个复制标量的操作叫做「广播机制」,是 NumPy 里最重要的一个特点,在下一节会详细讲到。
5.2 线性代数计算
在机器学习、金融工程和量化投资的编程过程中,因为运行速度的要求,通常会向量化 (vectorization) 而涉及大量的线性代数运算,尤其是矩阵之间的乘积运算。
但是,在 NumPy 默认不采用矩阵运算,而是数组 (ndarray) 运算。矩阵只是二维,而数组可以是任何维度,因此数组运算更通用些。
如果你非要二维数组 arr2d 进项矩阵运算,那么可以通过调用以下函数来实现:
- A = np.mat(arr2d)
- A = np.asmatrix(arr2d)
下面我们分别对「数组」和「矩阵」从创建、转置、求逆和相乘四个方面看看它们的同异。
创建
创建数组 arr2d 和矩阵 A,注意它们的输出有 array 和 matrix 的关键词。
arr2d = np.array([[1,2],[3,1]])arr2d
array([[1, 2], [3, 1]])
A = np.asmatrix(arr2d)A
matrix([[1, 2], [3, 1]])
转置
数组用 arr2d.T 操作或 arr.tranpose() 函数,而矩阵用 A.T 操作。主要原因就是 .T 只适合二维数据,上贴最后也举了个三维数组在轴 1 和轴 2 之间的转置,这时就需要用函数 arr2d.tranpose(1, 0, 2) 来实现了。
print( arr2d.T )print( arr2d.transpose() )print( A.T )
[[1 3] [2 1]] [[1 3] [2 1]] [[1 3] [2 1]]
求逆
数组用 np.linalg.inv() 函数,而矩阵用 A.I 和 A**-1 操作。
print( np.linalg.inv(arr2d) )print( A.I )print( A**-1 )
[[-0.2 0.4] [ 0.6 -0.2]] [[-0.2 0.4] [ 0.6 -0.2]] [[-0.2 0.4] [ 0.6 -0.2]]
相乘
相乘是个很模棱两可的概念
- 数组相乘是在元素层面进行,
- 矩阵相乘要就是数学定义的矩阵相乘 (比如第一个矩阵的列要和第二个矩阵的行一样)
看个例子,「二维数组」相乘「一维数组」,「矩阵」相乘「向量」,看看有什么有趣的结果。
首先定义「一维数组」arr 和 「列向量」b:
arr = np.array([1,2])b = np.asmatrix(arr).Tprint( arr.shape, b.shape )
(2,) (2, 1)
由上面结果看出, arr 的形状是 (2,),只含一个元素的元组只说明 arr 是一维,数组是不分行数组或列数组的。而 b 的形状是 (2,1),显然是列向量。
相乘都是用 * 符号,
print( arr2d*arr )print( A*b )
[[1 4] [3 2]] [[5] [5]]
由上面结果可知,
- 二维数组相乘一维数组得到的还是个二维数组,解释它需要用到「广播机制」,这是下节的重点讨论内容。现在大概知道一维数组 [1 2] 第一个元素 1 乘上 [1 3] 得到 [1 3],而第二个元素 2 乘上 [2 1] 得到 [4 2]。
- 而矩阵相乘向量的结果和我们学了很多年的线代结果很吻合。
再看一个例子,「二维数组」相乘「二维数组」,「矩阵」相乘「矩阵」
print( arr2d*arr2d )print( A*A )
[[1 4] [9 1]] [[7 4] [6 7]]
由上面结果可知,
- 虽然两个二维数组相乘得到二维数组,但不是根据数学上矩阵相乘的规则得来的,而且由元素层面相乘得到的。两个 [[1 2], [3,1]] 的元素相乘确实等于 [[1 4], [9,1]]。
- 而矩阵相乘矩阵的结果和我们学了很多年的线代结果很吻合。
问题来了,那么怎么才能在数组上实现「矩阵相乘向量」和「矩阵相乘矩阵」呢?用点乘函数 dot()。
print( np.dot(arr2d,arr) )print( np.dot(arr2d,arr2d) )
[5 5] [[7 4] [6 7]]
结果对了,但还有一个小小的差异
- 矩阵相乘列向量的结果是个列向量,写成 [[5],[5]],形状是 (2,1)
- 二维数组点乘一维数组结果是个一维数组,写成 [5, 5],形状是 (2,)
由此我们来分析下 NumPy 里的 dot() 函数,计算数组和数组之间的点乘结果。
点乘函数
本节的内容也来自〖张量 101〗,通常我们也把 n 维数组称为张量,点乘左右两边最常见的数组就是
- 向量 (1D) 和向量 (1D)
- 矩阵 (2D) 和向量 (1D)
- 矩阵 (2D) 和矩阵 (2D)
分别看看三个简单例子。
例一:np.dot(向量, 向量) 实际上做的就是内积,即把两个向量每个元素相乘,最后再加总。点乘结果 10 是个标量 (0D 数组),形状 = ()。
x = np.array( [1, 2, 3] )y = np.array( [3, 2, 1] )z = np.dot(x,y)print( z.shape )print( z )
() 10
例二:np.dot(矩阵, 向量) 实际上做的就是普通的矩阵乘以向量。点乘结果是个向量 (1D 数组),形状 = (2, )。
x = np.array( [1, 2, 3] )y = np.array( [[3, 2, 1], [1, 1, 1]] )z = np.dot(y,x)print( z.shape )print( z )
(2,) [10 6]
例三:np.dot(矩阵, 矩阵) 实际上做的就是普通的矩阵乘以矩阵。点乘结果是个矩阵 (2D 数组),形状 = (2, 3)。
x = np.array( [[1, 2, 3], [1, 2, 3], [1, 2, 3]] )y = np.array( [[3, 2, 1], [1, 1, 1]] )z = np.dot(y,x)print( z.shape )print( z )
(2, 3) [[ 6 12 18] [ 3 6 9]]
从例二和例三看出,当 x 第二个维度的元素 (x.shape[1]) 和 y 第一个维度的元素 (y.shape[0]) 个数相等时,np.dot(X, Y) 才有意义,点乘得到的结果形状 = (X.shape[0], y.shape[1])。
上面例子都是低维数组 (维度 ≤ 2) 的点乘运算,接下来我们看两个稍微复杂的例子。
例四:当 x 是 3D 数组,y 是 1D 数组,np.dot(x, y) 是将 x 和 y 最后一维的元素相乘并加总。此例 x 的形状是 (2, 3, 4),y 的形状是 (4, ),因此点乘结果的形状是 (2, 3)。
x = np.ones( shape=(2, 3, 4) )y = np.array( [1, 2, 3, 4] )z = np.dot(x,y)print( z.shape )print( z )
(2, 3) [[10. 10. 10] [10. 10. 10]]
例五:当 x 是 3D 数组,y 是 2D 数组,np.dot(x, y) 是将 x 的最后一维和 y 的倒数第二维的元素相乘并加总。此例 x 的形状是 (2, 3, 4),y 的形状是 (4, 2),因此点乘结果的形状是 (2, 3, 2)。
x = np.random.normal( 0, 1, size=(2, 3, 4) )y = np.random.normal( 0, 1, size=(4, 2) )z = np.dot(x,y)print( z.shape )print( z )
(2, 3, 2) [[[ 2.11753451 -0.27546168] [-1.23348676 0.42524653] [-4.349676 -0.3030879 ]] [[ 0.15537744, 0.44865273] [-3.09328194, -0.43473885] [ 0.27844225, -0.48024693]]]
例五的规则也适用于 nD 数组和 mD 数组 (当 m ≥ 2 时) 的点乘。
5.3 元素整合计算
在数组中,元素可以以不同方式整合 (aggregation)。拿求和 (sum) 函数来说,我们可以对数组
- 所有的元素求和
- 在某个轴 (axis) 上的元素求和
先定义数组
arr = np.arange(1,7).reshape((2,3))arr
array([[1, 2, 3], [4, 5, 6]])
不难看出它是一个矩阵,分别对全部元素、跨行 (across rows)、跨列 (across columns) 求和:
print( 'The total sum is', arr.sum() )print( 'The sum across rows is', arr.sum(axis=0) )print( 'The sum across columns is', arr.sum(axis=1) )
The total sum is 21 The sum across rows is [5 7 9] The sum across columns is [ 6 15]
分析上述结果:
- 1, 2, 3, 4, 5, 6 的总和是 21
- 跨行求和 = [1 2 3] + [4 5 6] = [5 7 9]
- 跨列求和 = [1+2+3 4+5+6] = [6 15]
行和列这些概念对矩阵 (二维矩阵) 才适用,高维矩阵还是要用轴 (axis) 来区分每个维度。让我们抛弃「行列」这些特殊概念,拥抱「轴」这个通用概念来重看数组 (一到四维) 把。
规律:n 维数组就有 n 层方括号。最外层方括号代表「轴 0」即 axis=0,依次往里方括号对应的 axis 的计数加 1。
严格来说,numpy 打印出来的数组可以想象带有多层方括号的一行数字。比如二维矩阵可想象成
[[1, 2, 3],[4, 5, 6]]
三维矩阵可想象成
[[[1,2,3], [4,5,6]], [[7,8,9], [10,11,12]]]
由于屏幕的宽度不够,我们才把它们写成一列列的,如下
[ [ [1, 2, 3]
[4, 5, 6] ]
[ [7, 8, 9]
[10, 11, 12] ] ]
但在你脑海里,应该把它们想成一整行。这样会便于你理解如何按不同轴做整合运算。
有了轴的概念,我们再来看看 sum() 求和函数。