Numpy矩阵运算

0. 概要

本文讲解了numpy的一些基础操作,同时还介绍了numpy中两个重要的概念,全局函数与广播。最后还介绍了numpy下面的两个包,linalg线性代数计算包与random随机生成包。

1. Numpy简介

numpy-logo.jpg

NumPy是一个运行速度非常快的数学库,主要用于矩阵运算。NumPy是Python在科学计算领域取得成功的关键之一,如果你想通过Python学习数据科学,就必须学习NumPy。

为什么我们不用python原生的list呢?

  • 矩阵运算功能

    NumPy可以让你在Python中使用向量和数学矩阵,它封装了很多矩阵运算操作,这些都是list所不具备的。

  • numpy更省空间

    NumPy中数组的存储效率和输入输出性能均远远优于Python中等价的基本数据结构,且能够提升的性能是与数组中的元素成比例的。

  • 处理速度更快

    NumPy的大部分代码都是用C语言写的,其底层算法在设计时就有着优异的性能,这使得NumPy比纯Python代码高效得多。

关于这方面的讨论细节,大家可以在StackOverflow上的一篇问答中了解更多:

why-numpy-instead-of-python-lists

2. 安装Numpy

在Windows下安装Numpy

pip install numpy

在Linux下安装Numpy

sudo pip3 install numpy

3. 引入Numpy模块

引入numpy模块

🖌 代码片段

import numpy

习惯上,我们引入numpy模块时,会将numpy起一个别名 np ,方便调用 (因为比较简短)

🖌 代码片段

import numpy as np

4. 设置打印选项

设置打印选项,让打印出来的数组可读性更强。

🖌 代码片段

# 设置Numpy的打印选项
# precision: 浮点数精确位数
# suppress:
# - True: 固定浮点 、
# - False: 使用科学计数法
np.set_printoptions(precision=3, suppress=True)

在配置完成后,生成一个长度为10的随机数组

print(np.random.rand(10))

📓 输出日志

[0.469 0.484 0.284 0.089 0.156 0.431 0.479 0.737 0.825 0.921]

5. 数组初始化

NumPy的核心是多维数组 (ndarrays: n-dimension array)。

使用 np.array 函数初始化几个数组,声明数组的方法就是使用 [] 嵌套, , 分割同层级的元素。

创建一个一维数组

🖌 代码片段

# 一维数组
A1 = np.array([1, 2, 3])
print("A1 :\n{}".format(A1))

📓 输出日志

A1 :
[1 2 3]

创建一个二维数组

🖌 代码片段

# 二维数组
A2 = np.array([[1, 2, 3], [4, 5, 6]])
print("A2 :\n{}".format(A2))

📓 输出日志

A2 :
[[1 2 3]
[4 5 6]]

6. 数组的属性

这里给大家展示几个常用的属性:

属性名称 含义
ndarray.ndim 数组的维度,等于Rank
ndarray.shape (行数, 列数)
ndarray.size 元素总个数 = 列数 * 行数
ndarray.dtype 数组元素数据类型
ndarray.itemsize 数组中每个元素,字节大小

🖌 代码片段

print('A2.ndim = %d' % A2.ndim)
print('A2.shape')
print(A2.shape)
print('A2.size = %d' % A2.size)
print('A2.dtype = %s' % A2.dtype)
print('A2.itemsize = %d' % A2.itemsize)

📓 输出日志

A2.ndim = 2
A2.shape
(2, 3)
A2.size = 6
A2.dtype = int32
A2.itemsize = 4

7. 数组的数据类型 dtype

numpy 的ndarray所有的元素数据类型必须相同,这是为矩阵运算做保障的。

Python支持的数据类型有整型、浮点型、复数型,但这些类型不足以满足科学计算的需求,因此NumPy中添加了许多其他的数据类型,如:bool、inti、int64、float32、complex64等。同时,它也有许多其特有的属性和方法。

在Numpy中定义了24种新的Python基础数据类型,大多基于 C语言 的数据类型。

dtype-hierarchy.png

详情见 arrays-scalars-官方文档

我们还可以使用 astype 函数,将原本为int类型的 ndarray 转换为浮点型。

🖌 代码片段

A =  np.array([[1, 2, 3], [4, 5, 6]])
print("转换前的格式: A.dtype = {}".format(A.dtype))
print("A:\n {}".format(A))
# 修改数据格式
A = A.astype("float32")
print('转换后 A.dtype = {}'.format(A.dtype))
print("A:\n {}".format(A))

📓 输出日志

转换前的格式: A.dtype = int32
A:
[[1 2 3]
[4 5 6]]
转换后 A.dtype = float32
A:
[[1. 2. 3.]
[4. 5. 6.]]

8. 等差数列

8.1 整数等差数列

初始化一个数组,我们还可以使用"批量"的方式, np.arange 的使用方法类似Python原生的 range

函数原型

arange([start,] stop[, step,], dtype=None)
  • start :数值区间开始,默认是0
  • stop :数值区间结束
  • step :数值增加间隔,默认为1
  • dtype :数据类型

🖌 代码片段

print('递增数列: ')
print(np.arange(0, 10, 1))

📓 输出日志

递增数列: 
[0 1 2 3 4 5 6 7 8 9]

如果不传入 start 则模式从 0 开始

🖌 代码片段

print(np.arange(10))

📓 输出日志

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

我们接下来实验一下 递减

🖌 代码片段

print('递减数列')
print(np.arange(10, 0, -1))

📓 输出日志

递减数列
[10  9  8  7  6  5  4  3  2  1]

8.2 浮点数等差数列

我们还可以使用 linspace 在一个数值区间内划分若干个段 (浮点数)。

注意第三个参数为插值点的个数。

🖌 代码片段

# 随机间隔
np.linspace(0, 10, 5)

📓 输出日志

array([ 0. ,  2.5,  5. ,  7.5, 10. ])

9. 数组变形

bianxing-zixingcheren.jpg

在矩阵计算中,有时我们需要将矩阵进行变形,例如原来是 4*5 的矩阵,可能会变形为 5*4 或者 10*2

也有可能改变维度,原来是二维的转变为一维的,例如 1*20

这里我们就需要两个函数 resize reshape

定义一维数组

🖌 代码片段

A = np.arange(10)

📓 输出日志

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

9.1 reshape

reshape 函数变形前与变形后的 size 必须相同,否则就会报错。

ndarray 对象 A 在调用 reshape 函数时, 返回一个新的ndarray对象,原来的ndarray并不发生改变。

reshape函数的原型:

reshape(a, newshape, order='C')
  • a :是要被resize的数组
  • new_shape :是新数组的尺寸,类型为tuple元组类型

使用样例

🖌 代码片段

new_dim = (2,5) # 新的数组维度
B = np.reshape(A, new_dim)
print("B 尺寸: {}".format(B.shape))
print(B)
print("A 尺寸: {}".format(A.shape))
print(A)

📓 输出日志

B 尺寸: (2, 5)
[[0 1 2 3 4]
[5 6 7 8 9]]
A 尺寸: (10,)
[0 1 2 3 4 5 6 7 8 9]

另外还可以在数组后调用reshape函数,效果与使用 np.reshape 是一样的。

🖌 代码片段

B = A.reshape(new_dim)

9.2 resize

np.resize的函数原型

resize(a, new_shape)
  • a :要被resize的数组
  • new_shape :新数组的尺寸,类型为tuple元组类型

使用 np.resize 的效果,与 np.reshape 的效果是类似的。

🖌 代码片段

B = np.resize(A, new_dim)
print("B 尺寸: {}".format(B.shape))
print(B)
print("A 尺寸: {}".format(A.shape))
print(A)

📓 输出日志

B 尺寸: (2, 5)
[[0 1 2 3 4]
[5 6 7 8 9]]
A 尺寸: (10,)
[0 1 2 3 4 5 6 7 8 9]

A.resize(new_dim) 函数会直接在原来的数组里面修改,不会返回新的数组。

🖌 代码片段

A.resize(new_dim)
print("A 尺寸: {}".format(A.shape))
print(A)

📓 输出日志

A 尺寸: (2, 5)
[[0 1 2 3 4]
[5 6 7 8 9]]

10. 线性代数运算 np.linalg

numpy的 linalg 里包含着线性代数的函数。

引入linalg模块 ,引入并起名为 LA

🖌 代码片段

import numpy.linalg as LA

声明一个矩阵 A

🖌 代码片段

A = np.random.rand(16).reshape((4, 4))

📓 输出日志

[[0.477 0.084 0.615 0.226]
[0.009 0.246 0.653 0.785]
[0.752 0.377 0.395 0.335]
[0.03  0.997 0.114 0.243]]

10.1 矩阵转置

🖌 代码片段

print(A.T)

📓 输出日志

[[0.477 0.009 0.752 0.03 ]
[0.084 0.246 0.377 0.997]
[0.615 0.653 0.395 0.114]
[0.226 0.785 0.335 0.243]]

10.2 矩阵求逆

🖌 代码片段

LA.inv(A)

📓 输出日志

[[-0.572 -0.414  1.717 -0.499]
[ 0.3   -0.344 -0.232  1.15 ]
[ 2.974 -0.207 -1.903  0.521]
[-2.563  1.559  1.637 -0.788]]

10.3 矩阵乘法

另外声明一个矩阵 B

🖌 代码片段

# 创建矩阵B
B = np.random.rand(8).reshape((2, 4))
print("B = \n{}".format(B))

📓 输出日志

B = 
[[0.401 0.394 0.528 0.917]
[0.513 0.735 0.289 0.533]]

矩阵相乘 dot

🖌 代码片段

print(B.dot(A))

📓 输出日志

[[0.619 1.244 0.817 0.799]
[0.485 0.864 0.97  0.919]]

10.4 行列式求解

🖌 代码片段

print(LA.det(A))

📓 输出日志

0.1799090776573458

11. 随机数 np.random

11.1 生成整数随机数组

🖌 代码片段

# 随机生成2x3的矩阵,矩阵元素取值范围 [low, high)
np.random.randint(low=0, high=100, size=(2,3))

📓 输出日志

array([[24, 13, 88],
    [14, 22, 61]])

11.2 生成0-1区间的浮点数随机数组

🖌 代码片段

print('生成10个0-1区间内的随机数值')
print(np.random.rand(10))

📓 输出日志

生成10个0-1区间内的随机数值
[0.638 0.477 0.969 0.186 0.692 0.245 0.162 0.877 0.457 0.051]

11.3 生成满足正态分布的随机数组

🖌 代码片段

print('生成10个满足正态分布的随机数组')
print(np.random.randn(10))

📓 输出日志

生成10个满足正态分布的随机数组
[ 0.26  -0.702  0.107 -0.105 -2.571 -0.665  0.632 -2.242 -1.283  0.924]

11.4 打乱排序

🖌 代码片段

A = np.arange(10)
print("A = {}".format(A))
np.random.shuffle(A)
print("打乱顺序后\n A = {}".format(A))

📓 输出日志

A = [0 1 2 3 4 5 6 7 8 9]
打乱顺序后
A = [0 5 3 4 9 2 7 6 8 1]

11.5 随机抽样

从数组A中随机抽取两个元素

🖌 代码片段

np.random.choice(A, size=2)

📓输出日志

array([7, 3])

12. 全局函数 ufunc

作用于数组中每个元素的函数我们称之为 ufunc ,英文全称为universal function。只有维度相等的两个数组才能进行全局函数运算。

常用的全局函数如下:

  • + 加法
  • - 减法
  • * 乘法,注意这里不是点乘
  • / 除法
  • 三角函数,数学函数

声明两个尺寸相同的矩阵。 A B

🖌 代码片段

# 随机生成2x3的矩阵,矩阵元素取值范围 [low, high)
A = np.random.randint(low=0, high=10, size=(3,3))
B = np.random.randint(low=0, high=10, size=(3,3))
print("A \n{} \nB\n{}".format(A, B))

📓 输出日志

A 
[[7 9 5]
[5 3 0]
[1 5 9]] 
B
[[5 1 8]
[4 6 6]
[1 4 6]]

12.1 全局加法

🖌 代码片段

A + B

📓 输出日志

array([[12, 10, 13],
    [ 9,  9,  6],
    [ 2,  9, 15]])

12.2 全局减法

🖌 代码片段

A - B

📓 输出日志

array([[ 2,  8, -3],
    [ 1, -3, -6],
    [ 0,  1,  3]])

12.3 全局乘法

🖌 代码片段

A * B

📓 输出日志

array([[35,  9, 40],
    [20, 18,  0],
    [ 1, 20, 54]])

12.4 全局除法

🖌 代码片段

A / B

📓 输出日志

array([[1.4  , 9.   , 0.625],
    [1.25 , 0.5  , 0.   ],
    [1.   , 1.25 , 1.5  ]])

12.5 其他全局函数

如果我们的计算对象是单个ndarray中的每个元素,就需要使用到numpy内置的一些全局函数。

例如 np.sin() ,这个函数的作用就是返回一个矩阵,其中每个元素都是原来矩阵 A 进行 sin 运算的结果。

🖌 代码片段

np.sin(A)

📓 输出日志

array([[ 0.6569866 ,  0.41211849, -0.95892427],
    [-0.95892427,  0.14112001,  0.        ],
    [ 0.84147098, -0.95892427,  0.41211849]])

13. 索引操作 Index

何为索引?

bread-slicing.jpg

一开始这里是一个大大的面包,而我们需要这一整个面包其中的一部分,所以为了达到这个目的,我们要进行索引,可以横着切,也可以竖着切,于是我们就获得了面包片。同样对于多维数组,我们索引是为了获取数组中的一个子区域。

13.1 一维数组索引

Numpy中多维数组的索引操作与Python中list的索引操作一样,同样由 start stop step 三个部分组成。

首先我们声明一个长度为10的一维数组。

🖌 代码片段

A = np.arange(10)

我们需要获取前三个元素

输入 A[:3] ,使用 : 分隔,第一个参数是起始值,默认为0,这里可以省略。后面的 3 是索引的结尾 (但是取不到编号为3的元素)。

🖌 代码片段

A[:3]

📓 输出日志

array([0, 1, 2])

完整版是这种写法:

🖌 代码片段

A[0:3]

📓 输出日志

array([0, 1, 2])

13.2 多维数组索引

针对多维数组的索引,不同之处在于,需要使用 , 分隔。

一图胜千言,相信大家看下面这张图片就会秒懂。

An Introduction to Scientific Python – NumPy

14. 矩阵拼接

在opencv中常会用到图像拼接,这里就需要用到numpy中的矩阵拼接。

矩阵拼接有个前提条件,就是拼接的那一面长度必须相同。

  • hstack() 横向拼接
  • vstack() 纵向拼接

我们先声明两个矩阵, A B

🖌 代码片段

A = np.arange(16).reshape(4,4)
B = np.arange(16).reshape(4,4) * -1
print("A: \n{}".format(A))
print("B: \n{}".format(B))

📓 输出日志

A: 
[[ 0  1  2  3]
[ 4  5  6  7]
[ 8  9 10 11]
[12 13 14 15]]
B: 
[[  0  -1  -2  -3]
[ -4  -5  -6  -7]
[ -8  -9 -10 -11]
[-12 -13 -14 -15]]

然后将两个矩阵先进行 横向拼接 注意这里传入的是tuple

🖌 代码片段

# 注意这里传入的是tuple
np.hstack((A, B))

📓 输出日志

array([[  0,   1,   2,   3,   0,  -1,  -2,  -3],
    [  4,   5,   6,   7,  -4,  -5,  -6,  -7],
    [  8,   9,  10,  11,  -8,  -9, -10, -11],
    [ 12,  13,  14,  15, -12, -13, -14, -15]])

然后再尝试进行 纵向拼接

🖌 代码片段

np.vstack((A, B))

📓 输出日志

array([[  0,   1,   2,   3],
    [  4,   5,   6,   7],
    [  8,   9,  10,  11],
    [ 12,  13,  14,  15],
    [  0,  -1,  -2,  -3],
    [ -4,  -5,  -6,  -7],
    [ -8,  -9, -10, -11],
    [-12, -13, -14, -15]])

15. 内存共享与深度拷贝

如果将一个ndarray直接赋值给另外一个ndarray,那么这两个元素所使用的内存都是同一块。也就意味着,如果A = B,修改B的值,A也会被修改,因为他们共用了同一片内存空间。这样比较高效,但是如果不注意的话,也会造成很多问题。

创建一个数组A,将B赋值为A,并修改B中元素的取值

🖌 代码片段

A = np.array([[1, 2], [3, 4]])
B = A
B[0][0] = -1
print("A =\n {}".format(A))
print("B =\n {}".format(B))

📓 输出日志

A =
[[-1  2]
[ 3  4]]
B =
[[-1  2]
[ 3  4]]

可以看到A和B的值都被修改了。

那么,我们如何才可以复制一个ndarray,修改新的ndarray而不影响之前的值呢?

这里我们需要用到numpy的深度拷贝函数 np.copy

🖌 代码片段

A = np.array([[1, 2], [3, 4]])
B = np.copy(A)
B[0][0] = -1
print("A =\n {}".format(A))
print("B =\n {}".format(B))

📓 输出日志

A =
[[1 2]
[3 4]]
B =
[[-1  2]
[ 3  4]]

当然我们也可以使用另外一种拷贝方式:

🖌 代码片段

B = A.copy()

如果你对python中的浅度拷贝和深度拷贝不是很清楚的话,可以看一下博客园的这篇文章: 图解Python深拷贝和浅拷贝 。了解一下copy包里面的 copy deepcopy 两个函数有哪些不同。

20180129-python-shallow-and-deep.png