【深度学习-Day 6】掌握 NumPy:ndarray 创建、索引、运算与性能优化指南
Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来
Python系列文章目录
PyTorch系列文章目录
机器学习系列文章目录
深度学习系列文章目录
Java系列文章目录
JavaScript系列文章目录
深度学习系列文章目录
01-【深度学习-Day 1】为什么深度学习是未来?一探究竟AI、ML、DL关系与应用
02-【深度学习-Day 2】图解线性代数:从标量到张量,理解深度学习的数据表示与运算
03-【深度学习-Day 3】搞懂微积分关键:导数、偏导数、链式法则与梯度详解
04-【深度学习-Day 4】掌握深度学习的“概率”视角:基础概念与应用解析
05-【深度学习-Day 5】Python 快速入门:深度学习的“瑞士军刀”实战指南
06-【深度学习-Day 6】掌握 NumPy:ndarray 创建、索引、运算与性能优化指南
文章目录
- Langchain系列文章目录
- Python系列文章目录
- PyTorch系列文章目录
- 机器学习系列文章目录
- 深度学习系列文章目录
- Java系列文章目录
- JavaScript系列文章目录
- 深度学习系列文章目录
- 前言
- 一、初识 NumPy:为何选择它?
- 1.1 Python 列表的局限性
- 1.2 NumPy 的核心优势
- 二、NumPy 核心:ndarray 对象
- 2.1 创建 ndarray
- 2.1.1 从 Python 列表或元组创建
- 2.1.2 使用 NumPy 内建函数创建
- 2.2 ndarray 的重要属性
- 三、ndarray 的索引与切片
- 3.1 基础索引
- 3.2 切片操作
- 3.2.1 一维数组切片
- 3.2.2 多维数组切片
- 3.2.3 切片的视图 (View) vs 副本 (Copy)
- 3.3 布尔索引与花式索引
- 3.3.1 布尔索引 (Boolean Indexing)
- 3.3.2 花式索引 (Fancy Indexing)
- 四、NumPy 的核心运算
- 4.1 元素级运算 (Element-wise Operations)
- 4.2 聚合运算 (Aggregation Operations)
- 4.3 线性代数运算
- 五、广播机制 (Broadcasting)
- 5.1 什么是广播?
- 5.2 广播规则
- 5.3 广播实例
- 六、NumPy 实战:性能对比
- 6.1 场景设定
- 6.2 纯 Python 实现
- 6.3 NumPy 实现
- 6.4 性能比较
- 七、总结
前言
欢迎来到深度学习之旅的第六天!在前几天的学习中,我们已经了解了深度学习的基本概念,并掌握了 Python 语言的基础(或者复习了它)。从今天开始,我们将接触到数据科学领域至关重要的工具库。而 NumPy (Numerical Python) 无疑是其中最基础、最核心的一个。
为什么 NumPy 如此重要?想象一下,深度学习本质上就是对大量的数字(如图像像素、词向量、模型参数)进行复杂的数学运算。NumPy 提供了一个强大的 N 维数组对象 ndarray
,以及一系列用于高效处理这些数组的函数。它不仅运算速度远超纯 Python 实现,更是 SciPy、Pandas、Matplotlib、Scikit-learn 等众多高级数据科学库的底层依赖。可以说,掌握 NumPy 是进行数据分析、机器学习乃至深度学习的必备技能。
本篇文章将带你深入探索 NumPy 的世界,从核心对象 ndarray
的创建与属性,到灵活的索引与切片,再到强大的数组运算和广播机制,最后通过实战对比展示其性能优势。无论你是数据科学新手,还是希望巩固 NumPy 基础的开发者,相信都能从中获益。
一、初识 NumPy:为何选择它?
在我们深入 NumPy 的细节之前,首先要理解为什么我们需要它。Python 原生的列表(List)虽然灵活,但在处理大规模数值计算时,存在一些显著的局限性。
1.1 Python 列表的局限性
Python 列表可以包含任意类型的对象,这种灵活性带来了内存开销和计算效率上的问题:
- 内存消耗大: 列表中存储的是对象的引用,而非数据本身。对于大量数值数据,这会占用过多内存。
- 计算效率低: 对列表中的元素进行数学运算通常需要显式的循环,这在 Python 解释器层面执行效率较低,尤其是在处理大规模数据时。
让我们看一个简单的例子,计算两个列表对应元素的和:
# 纯 Python 实现列表元素求和
list_a = [1, 2, 3, 4, 5]
list_b = [6, 7, 8, 9, 10]
result = []
for i in range(len(list_a)):result.append(list_a[i] + list_b[i])
print(f"纯 Python 列表求和结果: {result}")
# 输出: 纯 Python 列表求和结果: [7, 9, 11, 13, 15]# 如果数据量巨大,这个循环会非常慢
large_list_a = list(range(1000000))
large_list_b = list(range(1000000))# 尝试计算 (仅为说明,实际运行时会比较耗时)
# start_time = time.time()
# large_result = [large_list_a[i] + large_list_b[i] for i in range(len(large_list_a))]
# end_time = time.time()
# print(f"处理大型列表耗时: {end_time - start_time:.4f} 秒") # 实际运行会显示具体时间
1.2 NumPy 的核心优势
为了解决上述问题,NumPy 应运而生,它提供了以下核心优势:
- 高效的 N 维数组对象 (ndarray): NumPy 的核心是
ndarray
,它是一个存储 同类型 元素的多维数组。由于元素类型相同且在内存中连续存储,NumPy 可以利用优化过的 C 语言底层代码进行快速计算,极大地提高了运算效率和内存使用效率。 - 矢量化运算 (Vectorization): NumPy 允许你直接对整个数组执行数学运算,而无需编写显式循环。这种矢量化操作不仅代码简洁,而且执行速度极快,因为它利用了底层的 C 实现和 CPU 的 SIMD(Single Instruction, Multiple Data)指令。
- 广播 (Broadcasting) 功能: NumPy 能够自动处理不同形状数组之间的运算,使得代码更加灵活简洁。
- 丰富的函数库: 提供了大量用于线性代数、傅里叶变换、随机数生成等的数学函数。
二、NumPy 核心:ndarray 对象
ndarray
(N-dimensional array) 是 NumPy 库的基石。让我们学习如何创建它以及了解它的重要属性。
2.1 创建 ndarray
创建 ndarray
的方式多种多样,以下是一些常用的方法:
2.1.1 从 Python 列表或元组创建
最常见的方式是使用 np.array()
函数将 Python 的列表或元组转换为 ndarray
。
import numpy as np # 约定俗成的导入方式# 从列表创建一维数组
list_data = [1, 2, 3, 4, 5]
arr1d = np.array(list_data)
print(f"从列表创建的一维数组:\n{arr1d}")
print(f"数组类型: {type(arr1d)}") # <class 'numpy.ndarray'># 从嵌套列表创建二维数组 (矩阵)
nested_list_data = [[1, 2, 3], [4, 5, 6]]
arr2d = np.array(nested_list_data)
print(f"\n从嵌套列表创建的二维数组:\n{arr2d}")
2.1.2 使用 NumPy 内建函数创建
NumPy 提供了一些函数用于创建特定类型的数组:
np.zeros(shape)
: 创建指定形状 (shape) 且所有元素为 0 的数组。np.ones(shape)
: 创建指定形状且所有元素为 1 的数组。np.full(shape, fill_value)
: 创建指定形状且所有元素为指定值fill_value
的数组。np.arange(start, stop, step)
: 类似于 Python 的range()
,创建等差数列数组。np.linspace(start, stop, num)
: 创建包含num
个元素的等间隔数列数组,包含start
和stop
。np.random.rand(d0, d1, ..., dn)
: 创建指定形状的、元素在 [0, 1) 之间均匀分布的随机数组。np.random.randn(d0, d1, ..., dn)
: 创建指定形状的、元素服从标准正态分布(均值为0,方差为1)的随机数组。np.eye(N)
或np.identity(N)
: 创建一个 N 阶单位矩阵。
# 创建全零数组 (2行3列)
zeros_arr = np.zeros((2, 3))
print(f"\n全零数组:\n{zeros_arr}")# 创建全一数组 (一维,长度为4)
ones_arr = np.ones(4)
print(f"\n全一数组:\n{ones_arr}")# 创建等差数列数组
arange_arr = np.arange(0, 10, 2) # 从0开始,到10结束(不包含),步长为2
print(f"\n等差数列数组:\n{arange_arr}")# 创建等间隔数列数组
linspace_arr = np.linspace(0, 1, 5) # 从0到1,均匀取5个数
print(f"\n等间隔数列数组:\n{linspace_arr}")# 创建 3x3 的随机数组 (均匀分布)
rand_arr = np.random.rand(3, 3)
print(f"\n3x3 随机数组 (均匀分布):\n{rand_arr}")# 创建 2x4 的随机数组 (标准正态分布)
randn_arr = np.random.randn(2, 4)
print(f"\n2x4 随机数组 (标准正态分布):\n{randn_arr}")# 创建 3x3 单位矩阵
identity_matrix = np.eye(3)
print(f"\n3x3 单位矩阵:\n{identity_matrix}")
2.2 ndarray 的重要属性
了解 ndarray
的属性有助于我们更好地操作和理解数组:
ndim
: 数组的维数(轴的数量)。例如,一维数组ndim
为 1,二维数组(矩阵)ndim
为 2。shape
: 数组的维度。返回一个元组,表示数组在每个维度上的大小。例如,一个 2 行 3 列的矩阵,shape
为(2, 3)
。size
: 数组中元素的总个数,等于shape
元组中各元素的乘积。dtype
: 数组中元素的数据类型。NumPy 支持多种数据类型(如int32
,float64
,bool
等),这对于优化内存和计算至关重要。itemsize
: 数组中每个元素占用的字节数。data
: 指向数组数据内存的缓冲区。通常我们不需要直接操作它。
arr = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
print(f"\n示例数组:\n{arr}")print(f"数组维数 (ndim): {arr.ndim}") # 输出: 2
print(f"数组形状 (shape): {arr.shape}") # 输出: (2, 3)
print(f"元素总数 (size): {arr.size}") # 输出: 6
print(f"元素类型 (dtype): {arr.dtype}") # 输出: float64
print(f"每个元素字节数 (itemsize): {arr.itemsize}") # 输出: 8 (float64 占 8 字节)
我们可以使用 astype()
方法显式地转换数组的数据类型:
float_arr = np.array([1.1, 2.7, 3.5])
print(f"\n原始浮点数组: {float_arr}, 类型: {float_arr.dtype}")# 转换为整数类型 (小数部分会被截断)
int_arr = float_arr.astype(np.int32)
print(f"转换后的整数数组: {int_arr}, 类型: {int_arr.dtype}")
三、ndarray 的索引与切片
掌握如何访问和修改 ndarray
中的元素是 NumPy 操作的基础。NumPy 提供了比 Python 列表更强大和灵活的索引机制。
3.1 基础索引
对于一维数组,索引方式与 Python 列表类似,使用方括号 []
和从 0 开始的下标。
arr1d = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]
print(f"\n一维数组: {arr1d}")# 获取第一个元素
print(f"第一个元素: {arr1d[0]}") # 输出: 0# 获取第五个元素
print(f"第五个元素: {arr1d[4]}") # 输出: 4# 修改元素
arr1d[0] = 100
print(f"修改后的数组: {arr1d}") # 输出: [100 1 2 3 4 5 6 7 8 9]
对于多维数组(以二维为例),可以使用逗号分隔的索引元组 arr[row, column]
来访问特定元素。
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(f"\n二维数组:\n{arr2d}")# 获取第1行第2列的元素 (索引从0开始)
element = arr2d[0, 1]
print(f"第1行第2列的元素: {element}") # 输出: 2# 也可以使用分开的方括号 (效果相同,但不推荐)
element_alt = arr2d[0][1]
print(f"另一种方式获取元素: {element_alt}") # 输出: 2# 修改第3行第1列的元素
arr2d[2, 0] = 77
print(f"修改后的二维数组:\n{arr2d}")
3.2 切片操作
切片(Slicing)允许我们获取数组的子集(子数组)。其语法是 start:stop:step
,与 Python 列表切片类似,但不包含 stop
索引。
3.2.1 一维数组切片
arr1d = np.arange(10) # [ 0 1 2 3 4 5 6 7 8 9]
print(f"\n原始一维数组: {arr1d}")# 获取索引 2 到 5 (不含) 的元素
slice1 = arr1d[2:5]
print(f"arr1d[2:5]: {slice1}") # 输出: [2 3 4]# 获取从头开始到索引 5 (不含) 的元素
slice2 = arr1d[:5]
print(f"arr1d[:5]: {slice2}") # 输出: [0 1 2 3 4]# 获取从索引 5 到末尾的元素
slice3 = arr1d[5:]
print(f"arr1d[5:]: {slice3}") # 输出: [5 6 7 8 9]# 获取所有元素 (步长为2)
slice4 = arr1d[::2]
print(f"arr1d[::2]: {slice4}") # 输出: [0 2 4 6 8]
3.2.2 多维数组切片
可以在每个维度上分别进行切片。
arr2d = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
print(f"\n原始二维数组:\n{arr2d}")# 获取前两行 (索引0和1)
slice2d_1 = arr2d[:2]
print(f"\n前两行:\n{slice2d_1}")# 获取前两行,以及第2列到第4列 (不含) 的数据
slice2d_2 = arr2d[:2, 1:3]
print(f"\n前两行,第2、3列:\n{slice2d_2}")# 获取第一行的所有列
slice2d_3 = arr2d[0, :] # 或者 arr2d[0]
print(f"\n第一行:\n{slice2d_3}")# 获取第二列的所有行
slice2d_4 = arr2d[:, 1]
print(f"\n第二列:\n{slice2d_4}")
3.2.3 切片的视图 (View) vs 副本 (Copy)
非常重要的一点: NumPy 数组的切片默认返回的是原始数组的 视图 (View),而不是副本 (Copy)。这意味着对视图的修改 会影响 原始数组。
arr = np.arange(5) # [0 1 2 3 4]
print(f"\n原始数组: {arr}")# 创建切片 (视图)
arr_slice = arr[1:4] # [1 2 3]
print(f"切片: {arr_slice}")# 修改切片中的元素
arr_slice[0] = 99
print(f"修改切片后,切片变为: {arr_slice}") # 输出: [99 2 3]
print(f"修改切片后,原始数组变为: {arr}") # 输出: [ 0 99 2 3 4] <--- 原始数组被修改了!# 如果需要副本而不是视图,可以使用 .copy() 方法
arr_copy = arr[1:4].copy()
print(f"\n创建副本: {arr_copy}")
arr_copy[0] = 111 # 修改副本
print(f"修改副本后,副本变为: {arr_copy}") # 输出: [111 2 3]
print(f"修改副本后,原始数组保持不变: {arr}") # 输出: [ 0 99 2 3 4]
理解视图和副本的区别对于避免意外修改数据至关重要。
3.3 布尔索引与花式索引
NumPy 还支持更高级的索引方式。
3.3.1 布尔索引 (Boolean Indexing)
我们可以使用一个布尔类型的数组来选择元素。布尔数组的形状通常与原数组相同,True
对应的位置的元素会被选中。
names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
data = np.random.randn(7, 4) # 假设是与 names 对应的数据
print(f"\nNames 数组: {names}")
print(f"Data 数组:\n{data}")# 创建布尔数组,判断 names 是否等于 'Bob'
is_bob = (names == 'Bob')
print(f"\n布尔数组 (names == 'Bob'): {is_bob}") # 输出: [ True False False True False False False]# 使用布尔数组选择 data 中对应的行
bob_data = data[is_bob]
print(f"\n选择 'Bob' 对应的行数据:\n{bob_data}")# 也可以结合布尔运算 (&, |, ~)
is_bob_or_will = (names == 'Bob') | (names == 'Will')
print(f"\n布尔数组 (names == 'Bob' or names == 'Will'): {is_bob_or_will}")
bob_or_will_data = data[is_bob_or_will]
print(f"\n选择 'Bob' 或 'Will' 对应的行数据:\n{bob_or_will_data}")# 也可以用布尔数组来赋值
data[names != 'Joe'] = 0 # 将非 'Joe' 对应的行设置为 0
print(f"\n将非 'Joe' 行设置为 0 后的 Data 数组:\n{data}")
3.3.2 花式索引 (Fancy Indexing)
花式索引使用一个整数数组(或列表)作为索引,来选择特定的行、列或元素。
arr = np.zeros((8, 4))
for i in range(8):arr[i] = i # 给每行赋不同的值
print(f"\n原始 8x4 数组:\n{arr}")# 选择第 4, 3, 0, 6 行 (注意顺序)
selected_rows = arr[[4, 3, 0, 6]]
print(f"\n选择第 4, 3, 0, 6 行:\n{selected_rows}")# 使用负数索引 (从末尾开始)
selected_rows_neg = arr[[-1, -3, -5]] # 选择最后一行、倒数第三行、倒数第五行
print(f"\n使用负数索引选择行:\n{selected_rows_neg}")# 选择特定行列的元素
# 假设要选择 (1, 0), (5, 3), (7, 1) 三个位置的元素
rows = np.array([1, 5, 7])
cols = np.array([0, 3, 1])
selected_elements = arr[rows, cols]
print(f"\n选择 (1, 0), (5, 3), (7, 1) 元素: {selected_elements}")
# 输出: [1. 5. 7.] (分别是 arr[1,0], arr[5,3], arr[7,1])
注意: 花式索引返回的是数据的 副本 (Copy),而不是视图。
四、NumPy 的核心运算
NumPy 的真正威力在于其丰富的、高效的数组运算功能。
4.1 元素级运算 (Element-wise Operations)
对数组执行算术运算(加、减、乘、除、幂等)时,NumPy 会自动将运算应用到数组的 每个元素 上。这称为元素级运算或矢量化运算。
arr1 = np.array([[1., 2., 3.], [4., 5., 6.]])
arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
print(f"\n数组 arr1:\n{arr1}")
print(f"数组 arr2:\n{arr2}")# 元素级加法
print(f"\narr1 + arr2:\n{arr1 + arr2}")# 元素级乘法
print(f"\narr1 * arr2:\n{arr1 * arr2}")# 标量乘法 (标量会广播到数组的每个元素)
print(f"\narr1 * 0.5:\n{arr1 * 0.5}")# 元素级比较 (返回布尔数组)
print(f"\narr1 > arr2:\n{arr1 > arr2}")# NumPy 通用函数 (ufunc)
# 这些函数也执行元素级操作
print(f"\n对 arr1 开平方根:\n{np.sqrt(arr1)}")
print(f"\n对 arr1 计算指数:\n{np.exp(arr1)}")
对比纯 Python 实现,NumPy 的元素级运算极其高效简洁。
4.2 聚合运算 (Aggregation Operations)
NumPy 提供了许多用于计算数组统计信息的聚合函数,如求和、平均值、最大值、最小值等。
np.sum()
: 计算数组元素之和。np.mean()
: 计算数组元素的平均值。np.std()
: 计算数组元素的标准差。np.var()
: 计算数组元素的方差。np.min()
: 找出数组的最小值。np.max()
: 找出数组的最大值。np.argmin()
: 找出数组最小值的索引。np.argmax()
: 找出数组最大值的索引。np.cumsum()
: 计算元素的累积和。np.cumprod()
: 计算元素的累积积。
这些函数可以作用于整个数组,也可以沿着指定的 轴 (axis) 进行计算。
- 对于二维数组:
axis=0
表示沿着行的方向(计算每列的统计值),axis=1
表示沿着列的方向(计算每行的统计值)。
arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
print(f"\n聚合运算示例数组:\n{arr}")# 计算所有元素的和
print(f"所有元素之和: {np.sum(arr)}") # 输出: 36 (或 arr.sum())# 计算每列的和 (沿着行的方向, axis=0)
print(f"每列的和 (axis=0): {np.sum(arr, axis=0)}") # 输出: [ 9 12 15] (或 arr.sum(axis=0))# 计算每行的平均值 (沿着列的方向, axis=1)
print(f"每行的平均值 (axis=1): {np.mean(arr, axis=1)}") # 输出: [1. 4. 7.] (或 arr.mean(axis=1))# 找出整个数组的最大值
print(f"最大值: {np.max(arr)}") # 输出: 8 (或 arr.max())# 找出每行最大值的索引 (axis=1)
print(f"每行最大值的索引 (axis=1): {np.argmax(arr, axis=1)}") # 输出: [2 2 2] (或 arr.argmax(axis=1))
4.3 线性代数运算
NumPy 提供了 linalg
模块,包含丰富的线性代数运算功能,这对于机器学习和深度学习至关重要(回顾 Day 2 的线性代数!)。
np.dot(a, b)
或a @ b
: 矩阵乘法(对于二维数组)或向量内积(对于一维数组)。arr.T
或np.transpose(arr)
: 矩阵转置。np.linalg.inv(arr)
: 计算矩阵的逆。np.linalg.det(arr)
: 计算矩阵的行列式。np.linalg.eig(arr)
: 计算矩阵的特征值和特征向量。np.linalg.svd(arr)
: 计算奇异值分解 (SVD)。
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
v = np.array([9, 10])
print(f"\n矩阵 A:\n{A}")
print(f"矩阵 B:\n{B}")
print(f"向量 v: {v}")# 矩阵乘法
dot_product = np.dot(A, B)
# 或者使用 @ 运算符 (Python 3.5+)
dot_product_alt = A @ B
print(f"\n矩阵乘法 A @ B:\n{dot_product}")
print(f"矩阵乘法 A @ B (使用@):\n{dot_product_alt}")# 矩阵与向量乘法
mat_vec_product = A @ v
print(f"\n矩阵向量乘法 A @ v: {mat_vec_product}") # 输出: [29 67]# 矩阵转置
A_transpose = A.T
print(f"\n矩阵 A 的转置:\n{A_transpose}")# 计算矩阵 A 的行列式
det_A = np.linalg.det(A)
print(f"\n矩阵 A 的行列式: {det_A:.2f}") # 输出: -2.00# 计算矩阵 A 的逆
inv_A = np.linalg.inv(A)
print(f"\n矩阵 A 的逆:\n{inv_A}")# 验证逆矩阵 A @ inv(A) 约等于单位矩阵
identity_check = A @ inv_A
print(f"\nA @ inv(A) (应接近单位矩阵):\n{np.round(identity_check)}") # 使用 round 消除微小误差
五、广播机制 (Broadcasting)
广播是 NumPy 中一项强大的机制,它允许 NumPy 在执行元素级运算时,自动扩展(或“广播”)较小数组的维度,以匹配较大数组的形状,而无需显式地创建扩展后的数组副本。这使得代码更简洁,内存效率更高。
5.1 什么是广播?
想象一下你想让一个数组的每个元素都加上一个相同的标量值。NumPy 允许你直接写 arr + scalar
。实际上,NumPy 会将标量 scalar
“广播”成一个与 arr
形状相同的数组,然后执行元素级加法。
arr = np.array([1, 2, 3])
scalar = 10# NumPy 自动广播标量
result = arr + scalar
print(f"\n数组加标量 (广播):\n{result}") # 输出: [11 12 13]
# 相当于 NumPy 内部执行了类似下面的操作:
# broadcasted_scalar = np.array([10, 10, 10])
# result = arr + broadcasted_scalar
5.2 广播规则
并非所有不同形状的数组都能进行广播。NumPy 遵循一套严格的规则来确定两个数组是否兼容:
规则 1: 如果两个数组的维数 ndim
不同,那么在较小数组的 shape
前面补 1,直到它们的维数相同。
规则 2: 比较两个数组 从末尾维度开始 的各个轴的长度:
* 如果两个数组在某个轴上的长度相同,或者
* 其中一个数组在某个轴上的长度为 1,
那么认为它们在这个轴上是兼容的。
规则 3: 如果在所有轴上都兼容,则可以进行广播。
规则 4: 广播后结果数组的形状是两个输入数组在各个轴上长度的 最大值。
规则 5: 如果不满足以上条件,则会引发 ValueError
。
图示理解 (以两个二维数组为例):
假设数组 A 的 shape 为 (3, 4)
,数组 B 的 shape 为 (1, 4)
。
- 维数相同,都是 2。
- 比较末尾轴(轴 1):长度都是 4,兼容。
- 比较前一个轴(轴 0):A 的长度是 3,B 的长度是 1,兼容(因为有一个是 1)。
- 兼容,可以广播。
- 结果数组的 shape 为
(max(3, 1), max(4, 4))
,即(3, 4)
。NumPy 会将 B 的第一行“复制”3次,使其形状变为(3, 4)
,然后与 A 进行运算。
假设数组 C 的 shape 为 (3, 4)
,数组 D 的 shape 为 (3, 1)
。
- 维数相同,都是 2。
- 比较末尾轴(轴 1):C 的长度是 4,D 的长度是 1,兼容。
- 比较前一个轴(轴 0):长度都是 3,兼容。
- 兼容,可以广播。
- 结果数组的 shape 为
(max(3, 3), max(4, 1))
,即(3, 4)
。NumPy 会将 D 的第一列“复制”4次,使其形状变为(3, 4)
,然后与 C 进行运算。
假设数组 E 的 shape 为 (3, 4)
,数组 F 的 shape 为 (2, 4)
。
- 维数相同,都是 2。
- 比较末尾轴(轴 1):长度都是 4,兼容。
- 比较前一个轴(轴 0):E 的长度是 3,F 的长度是 2,不兼容(既不相等,也没有一个是 1)。
- 无法广播,会报错。
5.3 广播实例
# 示例 1: 二维数组加一维数组
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr1d = np.array([10, 20, 30])
print(f"\n二维数组:\n{arr2d}") # shape (3, 3)
print(f"一维数组: {arr1d}") # shape (3,)# arr1d 会被广播到 arr2d 的每一行
result = arr2d + arr1d
print(f"\n广播加法 (二维 + 一维):\n{result}")
# [[11 22 33]
# [14 25 36]
# [17 28 39]]# 示例 2: 需要调整形状以利用广播
# 假设想让 arr2d 的每一列都加上 [100, 200, 300]
col_vector = np.array([[100], [200], [300]]) # shape (3, 1)
print(f"\n列向量:\n{col_vector}")# col_vector 会被广播到 arr2d 的每一列
result_col = arr2d + col_vector
print(f"\n广播加法 (二维 + 列向量):\n{result_col}")
# [[101 102 103]
# [204 205 206]
# [307 308 309]]
广播是 NumPy 中一个极其有用的特性,但初学者可能会觉得有些困惑。多动手实践,结合广播规则进行思考,就能逐渐掌握它。
六、NumPy 实战:性能对比
理论说了这么多,NumPy 的高性能究竟体现在哪里?让我们通过一个简单的实验来直观感受一下。
6.1 场景设定
我们将创建一个包含一百万个随机数的大型列表和对应的 NumPy 数组,然后分别使用纯 Python 循环和 NumPy 矢量化运算来计算每个元素的平方,并比较所花费的时间。
6.2 纯 Python 实现
import time
import random# 创建大型 Python 列表
n_elements = 1000000
python_list = [random.random() for _ in range(n_elements)]# 使用纯 Python 循环计算平方
start_time_py = time.time()
result_py = []
for x in python_list:result_py.append(x * x)
end_time_py = time.time()print(f"\n--- 性能对比 ---")
print(f"纯 Python 循环处理 {n_elements} 个元素耗时: {end_time_py - start_time_py:.4f} 秒")
6.3 NumPy 实现
# 创建等效的 NumPy 数组
numpy_array = np.array(python_list) # 或者直接 np.random.rand(n_elements)# 使用 NumPy 矢量化运算计算平方
start_time_np = time.time()
result_np = numpy_array * numpy_array # 或者 np.square(numpy_array)
end_time_np = time.time()print(f"NumPy 矢量化运算处理 {n_elements} 个元素耗时: {end_time_np - start_time_np:.4f} 秒")
6.4 性能比较
运行上述代码(具体时间会因机器性能而异),你会发现 NumPy 的矢量化运算速度通常比纯 Python 循环快 几十倍甚至上百倍!
--- 性能对比 ---
纯 Python 循环处理 1000000 个元素耗时: 0.1234 秒 # 示例时间,实际会变化
NumPy 矢量化运算处理 1000000 个元素耗时: 0.0025 秒 # 示例时间,实际会变化
这个简单的对比清晰地展示了 NumPy 在数值计算方面的巨大性能优势。这正是为什么它成为 Python 数据科学生态系统不可或缺的一部分。在处理大规模数据时,优先考虑使用 NumPy 的矢量化操作是提高效率的关键。
七、总结
恭喜你完成了 NumPy 核心知识的学习!通过本篇文章,我们系统地探讨了 NumPy 的关键特性与应用:
- NumPy 的价值: 我们理解了 Python 列表在数值计算上的局限性,以及 NumPy 通过高效的
ndarray
对象、矢量化运算和广播机制带来的性能和便利性优势,它是 Python 科学计算的基石。 - ndarray 对象: 掌握了多种创建
ndarray
的方法(从列表、使用zeros/ones/arange/linspace/random
等),并熟悉了其重要属性(ndim
,shape
,size
,dtype
)。 - 索引与切片: 学会了使用基础索引、切片(区分视图与副本)、布尔索引和花式索引来灵活地访问和操作数组数据。
- 核心运算: 掌握了 NumPy 的核心运算能力,包括高效的元素级运算(矢量化)、强大的聚合运算(
sum
,mean
,max
等,可指定axis
)以及基础的线性代数运算(矩阵乘法、转置、求逆等)。 - 广播机制: 理解了广播的概念、工作规则及其在处理不同形状数组运算时的便利性。
- 性能优势: 通过实战对比,直观感受了 NumPy 矢量化运算相对于纯 Python 循环的巨大性能提升。
熟练掌握 NumPy 是后续学习 Pandas、Matplotlib、Scikit-learn 乃至 TensorFlow、PyTorch 等深度学习框架的重要前提。务必多加练习,将今天学习的知识运用到实际操作中。在下一篇文章中,我们将继续探索数据处理的另一大利器——Pandas 库!敬请期待!