:NumPy数组操作与矩阵计算)
目录一、矩阵基础1. 什么是矩阵2. 为什么数据分析都用矩阵3. 矩阵的基本运算3.1 矩阵加法3.2 矩阵数乘3.3 矩阵转置3.4 矩阵乘法4. 特殊矩阵4.1 单位矩阵4.2 对角矩阵二、索引与切片1. 一维数组的索引与切片2. 二维数组的索引与切片3. 多维数组索引4. 切片的核心机制三、数组变形1. reshape()2. flatten() 与 revel()3. transpose() 与 .T四、广播机制1. 什么是广播机制2. 广播机制规则3. 广播机制典型场景4. 代码案例五、矢量化计算1. 什么是矢量化2. 为什么矢量化计算效率高3. 代码实测for 循环与矢量化计算六、矩阵乘法1. 矩阵乘法 API总结一、矩阵基础在正式探索复杂的数组变形、切片索引与矢量化计算之前我们需要先把数据分析中最常打交道的矩阵彻底搞清楚这里我们不谈复杂的线性代数推导只从数据分析与编程落地的角度来看看矩阵到底是什么、为什么它在现代数据科学中无处不在以及它最基本的几项数学运算1. 什么是矩阵在数据分析的世界里矩阵的定义非常直观矩阵就是一个排布成 行 与 列 的二维数字网格如果你用过 Excel 电子表格或 SQL 数据库你就已经对矩阵非常熟悉了一个典型的 m * n 矩阵就是包含了m 行和n 列的二维表列 1 列 2 列 3 ┌────────┬───────┬────────┐ 行 1 │ 10 │ 180 │ 75 │ 行 2 │ 25 │ 165 │ 52 │ 行 3 │ 40 │ 175 │ 68 │ └────────┴───────┴────────┘ 3 行 x 3 列 矩阵在 NumPy 中这种 m * n 的矩阵就是 ndim2shape 为 (m, n)的二维 ndarray 对象2. 为什么数据分析都用矩阵你在日常工作中处理的数据无论来自用户行为日志、金融交易记录还是图像像素最终绝大多数都可以表达为矩阵形式[ 特征矩阵 (Feature Matrix) ] 特征 1 特征 2 特征 3 (例如:年龄) (例如:身高) (例如:体重) ┌───────────┬───────────┬───────────┐ 样本 1 │ 25 │ 175 │ 70 │ 样本 2 │ 30 │ 165 │ 55 │ 样本 3 │ 18 │ 180 │ 80 │ └───────────┴───────────┴───────────┘数据分析统一采用矩阵结构核心原因有两个天然契合 样本-特征 抽象每一行代表一个样本如一个用户、一笔订单、一张图片每一列代表一个特征/属性如年龄、价格、点击次数批量化计算效率 如果用传统的方式处理 100 万个用户的属性你需要写一个极长的循环逐行处理但如果抽象为矩阵算法可以把整张表看作一个单一实体一次性完成 100 万个样本的批量映射与计算3. 矩阵的基本运算理解了矩阵的物理含义后我们来看看在 NumPy 中最常用的四种矩阵运算。3.1 矩阵加法规则只有当两个矩阵的维度相同时才能进行加法运算。加法是对应坐标位置上的元素逐位相加业务场景合并同一业务在 上半月 与 下半月 的各维度销售指标import numpy as np A np.array([[1, 2], [3, 4]]) B np.array([[5, 6], [7, 8]]) # 矩阵加法 print(A B \n, A B)输出结果3.2 矩阵数乘规则用一个标量数字 k 乘以矩阵代表让矩阵中的每一个元素都乘以该数字 k业务场景全量数据按比例缩放如将所有商品的美元价格转换为人民币价格统一乘以汇率 k7.2A np.array([[1, 2], [3, 4]]) # 矩阵数乘 print(2 * A \n, 2 * A)输出结果3.3 矩阵转置规则沿主对角线翻转矩阵把原矩阵的行变成列列变成行。一个 m * n 的矩阵转置后会变成 n * m 的矩阵符号表示或在 NumPy 中直接使用 A.T业务场景改变数据维度形态调整 样本-特征 的排列方向以适应特定模型的输入要求A np.array([[1, 2, 3], [4, 5, 6]]) # shape: (2, 3) # 矩阵转置 print(A 的转置 (A.T) \n, A.T) # shape: (3, 2)输出结果3.4 矩阵乘法重点注意矩阵乘法不同于逐元素相乘它是线性代数中最重要的组合运算匹配规则只有当第一个矩阵 A 的列数等于第二个矩阵 B 的行数时才能进行乘法。即计算规则结果矩阵 C 中第 i 行第 j 列的元素是由矩阵 A 的第 i行向量与矩阵 B 的第 j列向量对应点乘相加得到的业务场景计算多元线性回归的预测值、特征加权求和等A np.array([[1, 2], [3, 4]]) B np.array([[5, 6], [7, 8]]) # 矩阵乘法运算符 (或者使用 np.dot / np.matmul) print(A B \n, A B)输出结果4. 特殊矩阵在日常分析与模型建模中有两类特殊的方阵需要重点认识4.1 单位矩阵概念主对角线上的元素全为 1其余位置全为 0 的方阵通常用 I 表示作用单位矩阵相当于普通代数运算中的数字 1。任何矩阵 A 乘以单位矩阵 I结果依然是它本身# 在 NumPy 中通过 np.eye 生成 3x3 单位矩阵 I np.eye(3) print(3x3 单位矩阵:\n, I)输出结果4.2 对角矩阵概念除了主对角线上有非零数字外其余位置所有元素全部为 0的矩阵作用在数据预处理中对角矩阵经常被用来对不同特征进行独立缩放# 在 NumPy 中通过 np.diag 生成对角矩阵 D np.diag([3, 0.5, 2]) print(3x3 对角矩阵:\n, D)输出结果二、索引与切片在获取或构建好 ndarray 对象后数据分析中最基础的操作就是定位、提取和修改数据。这一过程在 NumPy 中被称为索引与切片与 Python 原生列表相比NumPy 针对多维数组设计了一套优雅且高效的索引语法不仅支持快速提取单个元素还能轻松切割出子矩阵1. 一维数组的索引与切片一维数组的索引和切片逻辑与 Python 标准 list 完全一致遵循 0-indexed从 0 开始计数与负数倒序索引机制基本语法arr[start:stop:step]start起始位置包含。stop终止位置不包含半开区间 [start, stop)step步长默认为 1可以为负数代表逆序提取import numpy as np arr np.array([10, 20, 30, 40, 50, 60]) # 1. 单元素索引 print(第一个元素:, arr[0]) # 输出: 10 # 2. 连续切片 print(前 3 个元素:, arr[0:3]) # 输出: [10 20 30] (索引 0, 1, 2) # 3. 带步长的切片 print(隔一个取一个:, arr[::2]) # 输出: [10 30 50]输出结果2. 二维数组的索引与切片在处理矩阵时我们需要同时在行与列两个维度上进行定位语法差异Python 原生 List必须写两次方括号如 list[row][col]原理是先取出子列表再取元素ndarray在单个方括号内用逗号分隔不同轴格式为arr[row_index, col_index]① 定位单个元素matrix np.array([ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12] ]) # 提取第 2 行、第 3 列的元素 (注意索引从 0 开始) element matrix[1, 2] print(第 2 行第 3 列的元素:, element) # 输出: 7输出结果② 提取完整的行或列在二维切片中使用冒号:代表 选择该轴上的所有元素# 提取第 0 行的所有列 row_0 matrix[0, :] print(第 0 行全量数据:, row_0) # 输出: [1 2 3 4] # 提取第 1 列的所有行 col_1 matrix[:, 1] print(第 1 列全量数据:, col_1) # 输出: [2 6 10]输出结果③ 子矩阵切片格式为matrix[row_start:row_stop, col_start:col_stop]列 0 列 1 列 2 列 3 ┌──────┬──────┬──────┬──────┐ 行 0 │ 1 │ 2 │ 3 │ 4 │ ├──────┼──────┼──────┼──────┤ 行 1 │ 5 │ 6 │ 7 │ 8 │ -- 提取子矩阵 [0:2, 1:3] ├──────┼──────┼──────┼──────┤ 行 2 │ 9 │ 10 │ 11 │ 12 │ └──────┴──────┴──────┴──────┘ █ █ 被切片选中的区域 (2行 x 2列)# 提取 行 0~1列 1~2 的 2x2 子矩阵 sub_matrix matrix[0:2, 1:3] print(提取的 2x2 子矩阵:\n, sub_matrix)输出结果3. 多维数组索引对于 3 维甚至更高维度的张量索引语法依然保持用逗号分隔 N 个轴的切片规则对于 3D 数组坐标格式为arr[axis_0, axis_1, axis_2]# 创建一个 2x3x4 的 3D 数组 (2 块, 每块 3 行 4 列) tensor np.arange(24).reshape(2, 3, 4) # 提取第 0 块第 1 行第 2 列的元素 val tensor[0, 1, 2] print(3D 数组提取:, val) # 输出: 6 # 提取第 1 块的所有行、第 0 列 slice_3d tensor[1, :, 0] print(3D 切片结果:, slice_3d) # 输出: [12 16 20]4. 切片的核心机制这是使用 NumPy 切片时最需要警惕的地方也是 NumPy 性能极高的原因之一核心原则NumPy 数组的切片返回的是原数组的 视图而不是 副本在 Python 原生列表中new_list old_list[:] 会深拷贝出一个全新的列表但在 NumPy 中切片操作不会分配新的内存它只是为原有的连续内存块建立了一个 新视角这意味着修改切片后的子数组原数组的数据也会同步被修改视图修改影响示例import numpy as np # 原始数组 orig_arr np.array([10, 20, 30, 40, 50]) # 获取切片 (视图) sub_arr orig_arr[1:4] # 包含 [20, 30, 40] # 修改切片中的第一个元素 sub_arr[0] 999 print(切片数组:, sub_arr) # 输出: [999 30 40] print(原始数组:, orig_arr) # 输出: [10 999 30 40 50]输出结果如何强制创建副本如果你希望对提取出的子数据进行独立修改而不破坏原数组必须显式调用.copy()方法orig_arr np.array([10, 20, 30, 40, 50]) # 使用 .copy() 强制深拷贝 safe_sub_arr orig_arr[1:4].copy() # 修改副本 safe_sub_arr[0] 8888 print(副本数组:, safe_sub_arr) # 输出: [8888 30 40] print(原数组:, orig_arr) # 输出: [10 20 30 40 50]输出结果三、数组变形在真实的数据分析与机器学习建模中数据的原始形状往往不符合算法模型的输入要求例如图像处理模型要求输入的 Batch 形状为 (张数, 通道数, 高度, 宽度)而你读取的数据却是扁平的一维数组或者你需要将单列特征拉直为一维向量以计算统计指标数组变形允许我们在不改变底层数据内容的前提下重新组织数组的维度与轴结构1. reshape()reshape() 是最常用的变形函数它可以将数组重构为任意指定的新形状原则元素总数必须保持绝对一致变形前后的数组必须满足size_before size_after。例如包含 12 个元素的数组可以变形为 (3, 4)、(2, 6) 或 (2, 2, 3)但绝不能变形为 (3, 5)import numpy as np # 创建包含 12 个元素的一维数组 arr np.arange(12) # 转换为 3 行 4 列的二维矩阵 mat_34 arr.reshape(3, 4) print(3x4 矩阵:\n, mat_34) # 转换为 2x2x3 的三维张量 tensor_3d arr.reshape(2, 2, 3) print(\n2x2x3 张量 shape:, tensor_3d.shape)输出结果占位符 -1 的自动推导机制在实际开发中我们常常只确定了部分维度如希望将数据集重构为包含 4 列的特征矩阵但懒得去算到底有多少行这时可以使用-1作为某个维度的占位符NumPy 会根据元素总数自动推算出该维度的数值arr np.arange(12) # 将 12 个元素重构为 4 列行的数量设为 -1 (自动计算为 12 / 4 3 行) auto_mat arr.reshape(-1, 4) print(自动推导行的矩阵 shape:, auto_mat.shape) # 输出: (3, 4)输出结果注意在一个 reshape() 调用中-1 只能使用一次否则 NumPy 无法完成多元方程推导2. flatten() 与 revel()将多维矩阵压缩/拉直为一维向量的操作称为展平。NumPy 提供了两个名称相似但底层逻辑完全不同的函数flatten() 与 ravel()多维矩阵展平 │ ┌──────────────────────┴──────────────────────┐ │ │ arr.flatten() arr.ravel() ┌─────────────────────────┐ ┌─────────────────────────┐ │ 物理内存: 深拷贝 (Copy) │ │ 物理内存: 视图 (View) │ │ 性能: 开辟新内存较慢 │ │ 性能: 零拷贝极速 │ │ 修改新数组: 原数组不变 │ │ 修改新数组: 原数组随之变 │ └─────────────────────────┘ └─────────────────────────┘代码实测与对比import numpy as np matrix np.array([[1, 2], [3, 4]]) # 1. 使用 flatten()返回副本 flat_arr matrix.flatten() flat_arr[0] 999 print(使用 flatten() 修改后:) print(新数组:, flat_arr) # [999 2 3 4] print(原矩阵:, matrix) # [[1 2] [3 4]] # 2. 使用 ravel()返回视图 ravel_arr matrix.ravel() ravel_arr[0] 888 print(\n使用 ravel() 修改后:) print(新数组:, ravel_arr) # [888 2 3 4] print(原矩阵:, matrix) # [[888 2] [3 4]]输出结果最佳实践优先选择 ravel()在绝大多数算法链条中使用 ravel() 可以省去内存开销执行效率更高显式选择 flatten()只有确定需要安全隔离原数组、防止下游逻辑意外修改原数据时才选择 flatten()3. transpose() 与 .T在前文矩阵基础中我们介绍了矩阵转置概念。从数组变形的角度来看转置本质上是重新排列数组各个轴的顺序① .T 属性二维矩阵快速转置对于 2D 矩阵.T 属性直接交换 Axis 0行与 Axis 1列mat np.array([[1, 2, 3], [4, 5, 6]]) # shape: (2, 3) print(转置后 shape:, mat.T.shape) # shape: (3, 2)输出结果② transpose() 高维张量重排对于 3 维及以上的数据简单的 .T 属性无法满足复杂的通道置换需求此时需要使用 transpose() 并传入指定轴顺序的索引元组import numpy as np # 模拟一张彩色图像高 100宽 2003 通道 (RGB) # shape 为 (100, 200, 3) - 轴索引分别对应 (0, 1, 2) img_hwc np.zeros((100, 200, 3)) # 将格式转换为 PyTorch 所需的 (3, 100, 200) - 轴顺序重排为 (2, 0, 1) img_chw img_hwc.transpose(2, 0, 1) print(原始 HWC 形状:, img_hwc.shape) # 输出: (100, 200, 3) print(转换后 CHW 形状:, img_chw.shape) # 输出: (3, 100, 200)输出结果四、广播机制在前面学习矩阵运算时我们强调过 只有当两个矩阵形状完全一致时才能进行逐元素的加减法。但在真实的数据清洗与特征工程中我们经常遇到不同维度数据的计算需求例如如何将整张二维特征矩阵的每一列都减去该列的平均值如果按照硬性形状匹配的规则你必须先手动把一维的均值向量复制扩展成和特征矩阵一模一样的形状这不仅代码繁琐还会产生大量不必要的内存开销NumPy 提供的广播机制完美解决了这个问题它是 NumPy 中最核心的技术特性之一1. 什么是广播机制广播机制是指NumPy 在执行逐元素算术运算时自动隐式扩展不同形状数组以使其维度相匹配的机制优势 广播过程完全发生在 NumPy 底层的 C 语言层级不会在内存中真实复制数据。它通过调整迭代步长来实现计算既节省了大量内存又极大地提升了计算速度2. 广播机制规则NumPy 能否对两个形状不同的数组成功执行广播计算完全取决于以下两个匹配规则若在向左比较的过程中发现某个维度既不相等、且其中没有一个是 1系统就会直接抛出 ValueError 错误3. 广播机制典型场景让我们通过直观的视觉图解看看 NumPy 底层是如何把 较小 的数组扩展开来的场景一标量与二维矩阵相加标量可以看作没有任何维度的点它在计算时会被自动拉伸并广播到矩阵的每一个元素上场景二一维行向量与二维矩阵相加假设矩阵形状为 (3, 3)一维向量形状为 (3,)右对齐补 1 后为 (1, 3)。因为 Axis 1 长度均为 3Axis 0 其中一个是 1满足规则向量沿Axis 0垂直向下复制广播场景三列向量 (3, 1) 与 行向量 (1, 3) 相加当两个向量分别在不同的维度长度为 1 时它们会向彼此拓展最终生成一个 (3, 3) 的网格矩阵4. 代码案例数据中心化 / 中心去均值在机器学习预处理中常常需要把特征矩阵每一列的平均值减掉import numpy as np # 模拟 4 个样本、3 个特征的数据矩阵 X np.array([ [100, 10, 1.0], [200, 20, 2.0], [300, 30, 3.0], [400, 40, 4.0] ]) # shape: (4, 3) # 计算每一列的均值 mean X.mean(axis0) print(每列均值 shape:, mean.shape) # 利用广播机制(4, 3) 矩阵直接减去 (3,) 向量 X_centered X - mean print(\n中心化后的矩阵:\n, X_centered)输出结果五、矢量化计算NumPy 之所以能够从众多计算库中脱颖而出最核心的性能武器正是矢量化。本章我们将深入解析 NumPy 如何通过矢量化技术彻底取代传统循环实现数十倍乃至上百倍的计算性能提升1. 什么是矢量化简单来说矢量化是指将原本针对单个数值逐一执行的计算直接应用到整个数组上的编程范式。在传统编程思维中处理一堆数据时我们习惯通过 for 循环逐个取出元素并计算而在 NumPy 的矢量化思维中整个数组被看作一个单一的算术实体import numpy as np # 传统 Python 思路显式 for 循环遍历 py_list [1, 2, 3, 4] res_list [] for x in py_list: res_list.append(x * 2) # NumPy 矢量化思路直接对整个数组执行算术运算 np_arr np.array([1, 2, 3, 4]) res_arr np_arr * 2 # 一行代码向量化执行2. 为什么矢量化计算效率高NumPy 矢量化计算之所以高效主要得益于两项核心技术支撑① C 语言高效驱动NumPy 的核心计算例程全都是用预先编译好的C 语言编写的当你在 Python 中写下 a b 时NumPy 会立刻将指令下沉至底层的 C 代码。C 语言循环直接在连续的物理内存块上按固定步长偏移并执行计算完全跳过了 Python 解释器的类型检查与字节码解释开销② SIMD 硬件级并行SIMD是Single Instruction, Multiple Data单指令多数据流的缩写这是现代 CPU内置的核心并行计算技术普通 CPU 处理方式SISD一条指令只能处理一对数字计算 4 对数字需要执行 4 次加法指令SIMD 硬件加速方式CPU 内部拥有宽达 128 位、256 位或 512 位的向量寄存器。一条 SIMD 加法指令可以在同一个 CPU 时钟周期内并行对 4 个、8 个甚至 16 个浮点数同时完成加法3. 代码实测for 循环与矢量化计算下面通过一段基准代码来直观对比在对100万个浮点数执行复杂数学公式计算y sin(x) x^2 时两者的性能差距import time import math import numpy as np N 1_000_000 py_list [float(x) for x in range(N)] np_array np.arange(N, dtypefloat) start time.perf_counter() python_result [math.sin(x) x**2 for x in py_list] python_time time.perf_counter() - start start time.perf_counter() numpy_result np.sin(np_array) np_array**2 numpy_time time.perf_counter() - start print(fPython 循环耗时{python_time:.4f} 秒) print(fNumPy 运算耗时{numpy_time:.4f} 秒) print(fNumPy 快约{python_time / numpy_time:.1f} 倍)输出结果六、矩阵乘法在 NumPy 中星号运算符进行的是 逐元素相乘只有当对应的形状匹配或满足广播规则时它才会将相同位置上的数字相乘而真正的线性代数矩阵乘法在 NumPy 中由 dot()、matmul() 和 运算符来实现1. 矩阵乘法 APINumPy 提供了三种方式来进行矩阵乘法它们在二维矩阵上的计算结果完全一致但在高维张量和语义表达上略有差异① np.dot()经典点乘定位NumPy 的通用点乘函数行为逻辑1D 向量计算向量内积返回标量2D 矩阵执行标准矩阵乘法高维张量执行严格的张量积即拿 A 的最后一个轴去与 B 的倒数第二个轴做乘加结果会将高维轴进行组合拼接② np.matmul()专门的矩阵乘法函数定位NumPy 针对现代数据科学与深度学习专门优化的矩阵乘法函数行为逻辑1D 向量自动补充维度进行向量乘法返回标量2D 矩阵执行标准矩阵乘法高维张量将高维部分视作 Batch批量维度自动进行批量矩阵乘法。这是深度学习和多通道图像处理中最需要的运算方式③ 中缀运算符 现代代码推荐定位Python 3.5 引入的中缀运算符行为逻辑A B 在底层完全等价于 np.matmul(A, B)核心优势极大地提升了公式代码的可读性避免了多层嵌套函数调用import numpy as np A np.array([[1, 2], [3, 4]]) B np.array([[5, 6], [7, 8]]) # 1. 逐元素相乘 (*) print(逐元素相乘 (*):\n, A * B) # 2. 矩阵乘法 ( / matmul / dot) print(\n矩阵乘法 ():\n, A B) print(矩阵乘法 (matmul):\n, np.matmul(A, B)) print(矩阵乘法 (dot):\n, np.dot(A, B))输出结果高维张量下 dot() 与 matmul() / 的差异示例假设我们要处理包含 10 个 Batch 的矩阵运算每个矩阵均为 3 * 4 与 4 * 5# 形状分别为 (10, 3, 4) 与 (10, 4, 5) batch_A np.ones((10, 3, 4)) batch_B np.ones((10, 4, 5)) # 使用 将前导维度 (10,) 看作 Batch返回 10 个 (3, 5) 的矩阵 result_matmul batch_A batch_B print(matmul 高维形状:, result_matmul.shape) # 输出: (10, 3, 5) # 使用 dot会进行多维轴组合生成复杂的张量积 result_dot np.dot(batch_A, batch_B) print(dot 高维形状: , result_dot.shape) # 输出: (10, 3, 10, 5)输出结果最佳实践在编写现代 Python / NumPy 数据分析代码时一律推荐优先使用 运算符或显式调用 np.matmul()仅在需要计算一维向量内积时使用 np.dot()总结本章围绕 NumPy 数组的操作展开学习了数组的数据类型、索引与切片、数组变形以及矩阵的基本运算并了解了广播机制和矢量化运算的工作原理。同时我们还掌握了矩阵乘法的实现方法能够利用 NumPy 高效地完成常见的数值计算任务通过本章的学习我们已经具备了使用 NumPy 对数组进行灵活操作和计算的能力。下一篇将继续介绍 NumPy 中常用的统计函数、数学函数以及数据处理函数为后续的数据分析打下基础