一、为什么使用Numpy?
1.1、简介
Python科学计算基础包,提供 多维数组对象 、派生对象(掩码数组、矩阵) 数组的快速操作(数学计算、逻辑、形状变化、排序、选择、输入输出、离散傅里叶变换、基本线性代数、基本统计运算 和 随机模拟 等等)
❓: 要搞清楚的概念:
- 什么是傅里叶变换?
- 什么是离散傅里叶变换? 它的用途是什么?
- 基本基本线性代数、统计学运算和随机模拟 都有什么操作?都有什么用途? 如何和绘图工具将它可视化?
核心:矢量化和广播,加速计算。
Numpy的使用场景:
- 批处理数据
- 机器学习、深度学习中涉及 矩阵、向量 运算
- 统计信息后,对格式化数据做批量计算
- ......
二、安装Numpy
- 直接使用pip命令安装 pip install numpy
- 使用conda命令安装 conda install numpy
三、Numpy array 和 Python List的异同
-
1.在大规模计算上,Numpy数组 速度更快一些。
import time import numpy as np # 初始化一个Python列表和一个NumPy数组 l_list = list(range(1000)) l_array = np.array(l_list) # 记录开始时间,然后对列表使用map函数进行10000次操作 l_start = time.time() for _ in range(10000): l = list(map(lambda i: i+1,l_list)) # 使用map和lambda函数将列表中的每个元素加1 print("Python list with map spend{:.3f}s".format(time.time()-l_start)) # 打印操作所花费的时间 # 记录开始时间,然后对NumPy数组进行10000次加1操作 l_array_start = time.time() for _ in range(10000): l_array +=1 # NumPy数组的元素整体加1 print("Numpy array spend {:.3f}s".format(time.time() - l_array_start)) # 打印操作所花费的时间 > Python list with map spend0.221s > Numpy array spend 0.006s
Numpy array 使用内存中一块连续的物理地址存储数据 Python list 并不是连续存储的,数据分散在不同的物理空间
-
- Numpy数组 创建时具有固定大小,List 可动态增长 。 更改ndarray 大小 将创建新的数组并删除对原来的数组的引用。
import numpy as np # 创建一个固定大小的NumPy数组,形状为(3, 4),元素类型为整数,初始值为0 fixed_size_array = np.zeros((3, 4), dtype=int) print(fixed_size_array) 输出: [[0 0 0 0] [0 0 0 0] [0 0 0 0]]
# 创建一个空列表 dynamic_list = [] # 动态添加元素 for i in range(5): dynamic_list.append(i) print(dynamic_list) 输出: [0, 1, 2, 3, 4]
import numpy as np # 创建一个初始数组 initial_array = np.array([1, 2, 3, 4, 5]) print("Initial array:") print(initial_array) # 改变数组大小:创建一个新数组,大小为(2, 7),并用旧数组的元素填充 new_shape = (2, 7) new_array = np.resize(initial_array, new_shape) print("\\nNew resized array:") print(new_array) # 此时,initial_array 仍存在,但已不再使用,其引用计数减1 # 若后续无其他引用指向 initial_array,它将在适当时候被垃圾回收机制自动清理
-
3.Numpy数组 数组内元素类型相同 , 内存中的大小相同,目的是确保 内存高效性和矢量运算的便利性。有例外情况:Numpy中包含Python对象
import numpy as np # 创建一个NumPy数组,其中每个元素都是一个Python字典 # 注意:尽管元素是字典,但整个数组仍然具有同一种数据类型,即Python对象类型(object) nested_arrays = np.array([{'a': 1, 'b': 2.0}, {'c': 'hello', 'd': (1, 2, 3)}]) print(nested_arrays) 在这个例子中: 创建了一个NumPy数组 nested_arrays,它的每个元素都是一个Python字典。 虽然这些字典的内容各异(第一个字典包含整数和浮点数,第二个字典包含字符串和一个元组),但它们都被视为Python对象,并且在NumPy数组中统一以dtype=object表示。 即便如此,每个字典在内存中占用的空间大小可能会不同,因为它们内部包含的数据结构(如整数、浮点数、字符串、元组等)大小可能不一致。 但是,从NumPy数组的角度来看,它只关心每个元素作为一个整体在内存中的起始地址,而不会深入管理元素内部的具体数据结构。 总结来说,当NumPy数组的元素是Python对象(如字典、列表、自定义类实例等)时,虽然从NumPy数组层面看元素具有相同的数据类型(即object), 但这些Python对象内部可以包含不同大小的数据结构。这可以被视为某种程度上的“例外情况”, 这种做法通常会牺牲NumPy数组的部分性能优势,因为它不再能进行高效的矢量化计算。
-
4.Numpy数组 有大量高级数学和其他操作,这些方法执行效率更高。
#向量加法 # Python原生列表 list_a = [1, 2, 3, 4, 5] list_b = [6, 7, 8, 9, 10] result_list = [] for i in range(len(list_a)): result_list.append(list_a[i] + list_b[i]) print(result_list) # 输出:[7, 9, 11, 13, 15] import numpy as np # NumPy数组 array_a = np.array([1, 2, 3, 4, 5]) array_b = np.array([6, 7, 8, 9, 10]) result_array = array_a + array_b print(result_array) # 输出:[ 7 9 11 13 15]
#矩阵乘法 # Python原生列表(矩阵乘法) matrix_a = [[1, 2], [3, 4]] matrix_b = [[5, 6], [7, 8]] result_matrix = [[sum(a * b for a, b in zip(row_a, col_b)) for col_b in zip(*matrix_b)] for row_a in matrix_a] print(result_matrix) # 输出:[[19, 22], [43, 50]] import numpy as np # NumPy数组(矩阵乘法) matrix_a = np.array([[1, 2], [3, 4]]) matrix_b = np.array([[5, 6], [7, 8]]) result_matrix = np.dot(matrix_a, matrix_b) print(result_matrix) # 输出:[[19 22] # [43 50]]
#计算统计量 # Python原生列表(计算平均值) data_list = [1, 2, 3, 4, 5] mean = sum(data_list) / len(data_list) print(mean) # 输出:3.0 # Python原生列表(计算标准差) import math mean = sum(data_list) / len(data_list) variance = sum((x - mean) ** 2 for x in data_list) / len(data_list) std_dev = math.sqrt(variance) print(std_dev) # 输出:1.4142135623730951 import numpy as np # NumPy数组(计算平均值和标准差) data_array = np.array([1, 2, 3, 4, 5]) mean = np.mean(data_array) std_dev = np.std(data_array) print(mean) # 输出:3.0 print(std_dev) # 输出:1.4142135623730951
四、为什么Numpy 这么快?
两个核心: 矢量化 和 广播
矢量化和广播带来的优点:
- 1.避免显示循环,换句话说就是矢量化替换了原生计算中产生for循环。
data = [1, 2, 3, 4, 5]
squared_data = []
for value in data:
squared_data.append(value ** 2)
#矢量化
import numpy as np
data = np.array([1, 2, 3, 4, 5])
squared_data = data ** 2
-
2.利用底层优化;
NumPy库的矢量化操作底层是由C和Fortran等编译型语言实现的高效算法,这些算法能够利用CPU的向量化指令集(如SSE、AVX等)进行并行计算。相较于Python解释器执行的循环,矢量化代码能够大幅减少内存访问次数、减少Python函数调用开销,并利用现代处理器的并行能力,从而获得显著的性能提升。
-
3.直观的高级数学和统计操作;
NumPy提供了大量的向量化函数和方法,可以直接对数组进行复杂的数学运算、统计分析、线性代数操作等。
矢量数学运算:
统计函数:
线性代数:
数据过滤和条件操作:
- 加减乘除:直接使用Python的算术运算符(
+
,-
,*
,/
)在NumPy数组上进行操作。 - 指数、对数:
numpy.exp()
(指数函数)、numpy.log()
(自然对数)、numpy.log10()
(以10为底的对数)、numpy.log2()
(以2为底的对数)等。 - 三角函数:
numpy.sin()
(正弦)、numpy.cos()
(余弦)、numpy.tan()
(正切)、numpy.arcsin()
(反正弦)、numpy.arccos()
(反余弦)、numpy.arctan()
(反正切)等。还有相应的双曲函数如numpy.sinh()
、numpy.cosh()
、numpy.tanh()
等。 - 求和:
numpy.sum()
。 - 平均值:
numpy.mean()
。 - 中位数:
numpy.median()
。 - 标准差:
numpy.std()
。 - 最大值、最小值:
numpy.max()
,numpy.min()
。 - 百分位数:
numpy.percentile()
。 - 矩阵乘法:
numpy.dot()
(二维数组间的乘法)或numpy.matmul()
(一般化的矩阵乘法,支持广播)。 - 行列式:
numpy.linalg.det()
。 - 逆矩阵:
numpy.linalg.inv()
。 - 特征值与特征向量:
numpy.linalg.eig()
或numpy.linalg.eigh()
(对于Hermitian或实对称矩阵)。 - 奇异值分解:
numpy.linalg.svd()
。 - 使用布尔数组进行索引:直接使用布尔数组(通常是比较操作的结果)作为索引来提取满足条件的元素,如
arr[arr > threshold]
。 numpy.where()
:实现三元条件运算,如numpy.where(condition, x, y)
,根据condition
选择x
或y
的元素构建新数组。- 逻辑运算:使用逻辑运算符(
&
、|
、~
)进行元素级别的与、或、非操作,或者使用numpy.logical_and()
、numpy.logical_or()
、numpy.logical_not()
等函数。
- 加减乘除:直接使用Python的算术运算符(
-
4.广播机制
广播机制允许不同形状的数组在某些条件下进行有效的元素级运算,无需显式地对较小数组进行复制扩展。
广播规则允许形状不完全匹配的数组在运算时自动调整其形状以匹配较大的数组,从而简化了数组间运算的代码编写。
# 向量与标量运算 import numpy as np # 一个一维数组(向量) vector = np.array([1, 2, 3, 4, 5]) # 一个标量(单个数值) scalar = 10 # 直接进行元素级加法运算 result = vector + scalar print("向量与标量相加结果:", result) #向量与标量相加结果: [11 12 13 14 15]
#长度不同的向量计算 import numpy as np # 两个不同长度的一维数组(向量) vector1 = np.array([1, 2, 3]) vector2 = np.array([4, 5, 6, 7, 8]) # 直接进行元素级乘法运算 try: result = vector1 * vector2 except ValueError as e: print("错误:", e) #错误: operands could not be broadcast together with shapes (3,) (5,) #尝试将长度不同的两个向量进行元素级乘法运算会导致ValueError,因为它们的形状无法进行有效的广播。
#向量与同维数但不同长度的向量运算(合法广播) import numpy as np # 两个不同长度但具有相同维数的一维数组(向量) vector1 = np.array([1, 2, 3, 4, 5]) vector2 = np.array([10, 20, 30]) # 将较短的向量在前面补零,使其长度与较长向量匹配 vector2_padded = np.pad(vector2, (0, len(vector1) - len(vector2)), 'constant', constant_values=0) # 元素级乘法运算 result = vector1 * vector2_padded print("向量与同维数但不同长度的向量相乘结果(手动补零):", result) # 使用NumPy广播规则进行相同运算 print("向量的shape:",vector1.shape,vector2[:, np.newaxis].shape) print(vector2[:,np.newaxis]) result_broadcasted = vector1 * vector2[:, np.newaxis] # 注意这里广播机制,[1,2,3,4,5] * [10] 这一个元素,广播成 [1,2,3,4,5] * [10,10,10,10,10] print("向量与同维数但不同长度的向量相乘结果(使用广播):", result_broadcasted,"shape:",result_broadcasted.shape) 【输出】: 向量与同维数但不同长度的向量相乘结果(手动补零): [10 40 90 0 0] 向量的shape: (5,) (3, 1) [[10] [20] [30]] 向量与同维数但不同长度的向量相乘结果(使用广播): [[ 10 20 30 40 50] [ 20 40 60 80 100] [ 30 60 90 120 150]] shape: (3, 5)
import numpy as np # 两个不同长度但具有相同维数的一维数组(向量) vector1 = np.array([[0, 0, 0], [10, 10, 10], [20, 20, 20], [30, 30, 30]]) vector2 = np.array([0, 1,2]) vector_add = vector1 + vector2 print(vector_add) #输出 [[ 0 1 2] [10 11 12] [20 21 22] [30 31 32]]
-
5.代码可读性与简洁性
矢量化代码通常更简洁、更接近数学表达式,增强了代码的可读性和可维护性。例如,矢量化代码可以直观地表达矩阵乘法、卷积、傅立叶变换等复杂操作,而无需关注底层循环细节。
# 定义两个二维数组(矩阵) matrix_A = [[1, 2], [3, 4]] matrix_B = [[5, 6], [7, 8]] # 使用循环实现矩阵乘法 result_matrix = [[0, 0], [0, 0]] # 初始化结果矩阵 for i in range(len(matrix_A)): for j in range(len(matrix_B[0])): for k in range(len(matrix_B)): result_matrix[i][j] += matrix_A[i][k] * matrix_B[k][j] print(result_matrix) import numpy as np # 定义两个二维数组(矩阵) matrix_A = np.array([[1, 2], [3, 4]]) matrix_B = np.array([[5, 6], [7, 8]]) # 使用NumPy的矩阵乘法函数(矢量化操作),直接对应于数学中的矩阵乘法符号 A × B result_matrix = np.dot(matrix_A, matrix_B) print(result_matrix)